Skip to content

Commit 498fcb0

Browse files
authored
Merge pull request #369 from bmf-san/test/increase-line-coverage-for-codecov
test: increase line coverage to ~83.5% for Codecov
2 parents 8a91b1e + 2c07b02 commit 498fcb0

File tree

11 files changed

+1287
-617
lines changed

11 files changed

+1287
-617
lines changed

cmd/version_test.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,3 +391,94 @@ func TestCompareSemanticVersions_DifferentLengths(t *testing.T) {
391391
t.Errorf("compareSemanticVersions(v1.1, v1.0.9) = %d, want 1", got)
392392
}
393393
}
394+
395+
// ─── ensureCreatedAtSet: exercise the CreatedAt="" branch ─────────────────────
396+
397+
func TestVersioner_EnsureCreatedAtSet(t *testing.T) {
398+
tempDir := t.TempDir()
399+
configPath := filepath.Join(tempDir, ".ggcconfig.yaml")
400+
// Empty YAML: LoadConfig will use defaults — CreatedAt stays ""
401+
if err := os.WriteFile(configPath, []byte(""), 0644); err != nil {
402+
t.Fatalf("WriteFile: %v", err)
403+
}
404+
405+
originalHome := os.Getenv("HOME")
406+
if err := os.Setenv("HOME", tempDir); err != nil {
407+
t.Fatalf("Setenv HOME: %v", err)
408+
}
409+
t.Cleanup(func() { _ = os.Setenv("HOME", originalHome) })
410+
411+
originalGetter := getVersionInfo
412+
SetVersionGetter(nil) // no version update — isolate ensureCreatedAtSet
413+
t.Cleanup(func() { getVersionInfo = originalGetter })
414+
415+
var buf bytes.Buffer
416+
v := &Versioner{
417+
gitClient: testutil.NewMockGitClient(),
418+
outputWriter: &buf,
419+
helper: NewHelper(),
420+
execCommand: exec.Command,
421+
}
422+
v.helper.outputWriter = &buf
423+
v.Version([]string{})
424+
425+
output := buf.String()
426+
if strings.Contains(output, "warn: failed to set created-at") {
427+
t.Errorf("unexpected created-at warning: %s", output)
428+
}
429+
if !strings.Contains(output, "ggc version") {
430+
t.Errorf("expected version in output, got: %s", output)
431+
}
432+
}
433+
434+
// ─── updateVersionInfoFromBuild: exercise when getVersionInfo returns new info ─
435+
436+
func TestVersioner_UpdateVersionInfoFromBuild(t *testing.T) {
437+
tempDir := t.TempDir()
438+
configPath := filepath.Join(tempDir, ".ggcconfig.yaml")
439+
if err := os.WriteFile(configPath, []byte(""), 0644); err != nil {
440+
t.Fatalf("WriteFile: %v", err)
441+
}
442+
443+
originalHome := os.Getenv("HOME")
444+
if err := os.Setenv("HOME", tempDir); err != nil {
445+
t.Fatalf("Setenv HOME: %v", err)
446+
}
447+
t.Cleanup(func() { _ = os.Setenv("HOME", originalHome) })
448+
449+
originalGetter := getVersionInfo
450+
SetVersionGetter(func() (string, string) { return "v99.0.0", "commit9999" })
451+
t.Cleanup(func() { getVersionInfo = originalGetter })
452+
453+
var buf bytes.Buffer
454+
v := &Versioner{
455+
gitClient: testutil.NewMockGitClient(),
456+
outputWriter: &buf,
457+
helper: NewHelper(),
458+
execCommand: exec.Command,
459+
}
460+
v.helper.outputWriter = &buf
461+
v.Version([]string{})
462+
463+
output := buf.String()
464+
if !strings.Contains(output, "v99.0.0") {
465+
t.Errorf("expected v99.0.0 in output, got: %s", output)
466+
}
467+
}
468+
469+
// ─── updateConfigValue: exercise the Set-failure warning path ─────────────────
470+
471+
func TestVersioner_UpdateConfigValue_Error(t *testing.T) {
472+
var buf bytes.Buffer
473+
v := &Versioner{
474+
gitClient: testutil.NewMockGitClient(),
475+
outputWriter: &buf,
476+
}
477+
cm := config.NewConfigManager(testutil.NewMockGitClient())
478+
// Empty key fails sanitizeConfigPath → Set returns error → warn logged
479+
v.updateConfigValue(cm, "", "value")
480+
481+
if !strings.Contains(buf.String(), "warn: failed to set") {
482+
t.Errorf("expected warn in output, got: %q", buf.String())
483+
}
484+
}

internal/config/config_test.go

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2211,3 +2211,187 @@ func TestSetMapValue(t *testing.T) {
22112211
t.Errorf("Expected no error setting int value, got %v", err)
22122212
}
22132213
}
2214+
2215+
// ─── failingGitClient forces ConfigSetGlobal to return errors for sync tests ───
2216+
// git.ConfigOps requires ConfigGetGlobal, ConfigSetGlobal, GetVersion, GetCommitHash.
2217+
2218+
type failingGitClient struct {
2219+
failKey string // if empty, all ConfigSetGlobal calls fail
2220+
}
2221+
2222+
func newFailingGitClient(failKey string) *failingGitClient {
2223+
return &failingGitClient{failKey: failKey}
2224+
}
2225+
2226+
func (f *failingGitClient) ConfigGetGlobal(_ string) (string, error) { return "", nil }
2227+
func (f *failingGitClient) ConfigSetGlobal(key, _ string) error {
2228+
if f.failKey == "" || key == f.failKey {
2229+
return fmt.Errorf("forced git error for %s", key)
2230+
}
2231+
return nil
2232+
}
2233+
func (f *failingGitClient) GetVersion() (string, error) { return "test", nil }
2234+
func (f *failingGitClient) GetCommitHash() (string, error) { return "abc", nil }
2235+
2236+
func newTestConfigManagerWithFailingGit(failKey string) *Manager {
2237+
return NewConfigManager(newFailingGitClient(failKey))
2238+
}
2239+
2240+
// TestSyncDefaultSettings_EditorError covers the error return when setting core.editor fails
2241+
func TestSyncDefaultSettings_EditorError(t *testing.T) {
2242+
cm := newTestConfigManagerWithFailingGit("core.editor")
2243+
cfg := cm.GetConfig()
2244+
cfg.Default.Editor = "vim"
2245+
err := cm.syncDefaultSettings(cfg)
2246+
if err == nil {
2247+
t.Error("expected error when core.editor ConfigSetGlobal fails")
2248+
}
2249+
}
2250+
2251+
// TestSyncDefaultSettings_MergeToolError covers the merge.tool error path
2252+
func TestSyncDefaultSettings_MergeToolError(t *testing.T) {
2253+
cm := newTestConfigManagerWithFailingGit("merge.tool")
2254+
cfg := cm.GetConfig()
2255+
cfg.Default.Editor = "" // skip editor
2256+
cfg.Default.MergeTool = "vimdiff"
2257+
err := cm.syncDefaultSettings(cfg)
2258+
if err == nil {
2259+
t.Error("expected error when merge.tool ConfigSetGlobal fails")
2260+
}
2261+
}
2262+
2263+
// TestSyncDefaultSettings_BranchError covers the init.defaultBranch error path
2264+
func TestSyncDefaultSettings_BranchError(t *testing.T) {
2265+
cm := newTestConfigManagerWithFailingGit("init.defaultBranch")
2266+
cfg := cm.GetConfig()
2267+
cfg.Default.Editor = ""
2268+
cfg.Default.MergeTool = ""
2269+
cfg.Default.Branch = "main"
2270+
err := cm.syncDefaultSettings(cfg)
2271+
if err == nil {
2272+
t.Error("expected error when init.defaultBranch ConfigSetGlobal fails")
2273+
}
2274+
}
2275+
2276+
// TestSyncUISettings_ColorError covers the color.ui error path
2277+
func TestSyncUISettings_ColorError(t *testing.T) {
2278+
cm := newTestConfigManagerWithFailingGit("color.ui")
2279+
cfg := cm.GetConfig()
2280+
err := cm.syncUISettings(cfg)
2281+
if err == nil {
2282+
t.Error("expected error when color.ui ConfigSetGlobal fails")
2283+
}
2284+
}
2285+
2286+
// TestSyncUISettings_PagerError covers the core.pager error path.
2287+
// color.ui always uses ConfigSetGlobal; we use a key-specific failing mock so
2288+
// color.ui succeeds but core.pager fails.
2289+
func TestSyncUISettings_PagerError(t *testing.T) {
2290+
cm := newTestConfigManagerWithFailingGit("core.pager")
2291+
cfg := cm.GetConfig()
2292+
cfg.UI.Color = true // color.ui will call ConfigSetGlobal("color.ui","true") → passes
2293+
cfg.UI.Pager = false // triggers the pager branch → fails
2294+
err := cm.syncUISettings(cfg)
2295+
if err == nil {
2296+
t.Error("expected error when core.pager ConfigSetGlobal fails")
2297+
}
2298+
}
2299+
2300+
// TestSyncBehaviorSettings_AutoFetchError covers the fetch.auto error path
2301+
func TestSyncBehaviorSettings_AutoFetchError(t *testing.T) {
2302+
cm := newTestConfigManagerWithFailingGit("fetch.auto")
2303+
cfg := cm.GetConfig()
2304+
err := cm.syncBehaviorSettings(cfg)
2305+
if err == nil {
2306+
t.Error("expected error when fetch.auto ConfigSetGlobal fails")
2307+
}
2308+
}
2309+
2310+
// TestSyncAliasSettings_Error covers the alias set error path
2311+
func TestSyncAliasSettings_Error(t *testing.T) {
2312+
cm := newTestConfigManagerWithFailingGit("alias.co")
2313+
cfg := cm.GetConfig()
2314+
cfg.Aliases = map[string]interface{}{"co": "checkout"}
2315+
err := cm.syncAliasSettings(cfg)
2316+
if err == nil {
2317+
t.Error("expected error when alias ConfigSetGlobal fails")
2318+
}
2319+
}
2320+
2321+
// TestSyncToGitConfig_DefaultSettingsError verifies syncToGitConfig propagates editor error
2322+
func TestSyncToGitConfig_DefaultSettingsError(t *testing.T) {
2323+
cm := newTestConfigManagerWithFailingGit("core.editor")
2324+
cfg := cm.GetConfig()
2325+
cfg.Default.Editor = "vim"
2326+
err := cm.syncToGitConfig()
2327+
if err == nil {
2328+
t.Error("expected syncToGitConfig to return error on editor failure")
2329+
}
2330+
}
2331+
2332+
// ─── helpers for FileOps error injection ───────────────────────────────────────
2333+
2334+
// mockTempFileWriteError is a TempFile whose Write always fails.
2335+
type mockTempFileWriteError struct {
2336+
name string
2337+
}
2338+
2339+
func (m *mockTempFileWriteError) Write(_ []byte) (int, error) {
2340+
return 0, fmt.Errorf("forced write error")
2341+
}
2342+
func (m *mockTempFileWriteError) Close() error { return nil }
2343+
func (m *mockTempFileWriteError) Name() string { return m.name }
2344+
2345+
// MockFileOpsWithWriteError wraps MockFileOps and injects a write error via CreateTemp.
2346+
type MockFileOpsWithWriteError struct {
2347+
*MockFileOps
2348+
}
2349+
2350+
func NewMockFileOpsWithWriteError() *MockFileOpsWithWriteError {
2351+
return &MockFileOpsWithWriteError{MockFileOps: NewMockFileOps()}
2352+
}
2353+
2354+
func (m *MockFileOpsWithWriteError) CreateTemp(dir, pattern string) (TempFile, error) {
2355+
return &mockTempFileWriteError{name: dir + "/tmp_" + pattern}, nil
2356+
}
2357+
2358+
// MockFileOpsWithRenameError wraps MockFileOps and makes Rename always fail.
2359+
type MockFileOpsWithRenameError struct {
2360+
*MockFileOps
2361+
}
2362+
2363+
func NewMockFileOpsWithRenameError() *MockFileOpsWithRenameError {
2364+
return &MockFileOpsWithRenameError{MockFileOps: NewMockFileOps()}
2365+
}
2366+
2367+
func (m *MockFileOpsWithRenameError) Rename(_, _ string) error {
2368+
return fmt.Errorf("forced rename error")
2369+
}
2370+
2371+
// TestWriteTempConfigWithOps_WriteError verifies that a write error is propagated.
2372+
func TestWriteTempConfigWithOps_WriteError(t *testing.T) {
2373+
mockFS := NewMockFileOpsWithWriteError()
2374+
if err := mockFS.MkdirAll("/test", 0700); err != nil {
2375+
t.Fatal(err)
2376+
}
2377+
cm := newTestConfigManager()
2378+
cm.configPath = "/test/cfg.yaml"
2379+
_, err := cm.writeTempConfigWithOps("/test", []byte("data"), mockFS)
2380+
if err == nil {
2381+
t.Error("expected error when temp-file Write fails")
2382+
}
2383+
}
2384+
2385+
// TestReplaceConfigFileWithOps_RenameError simulates a rename failure
2386+
func TestReplaceConfigFileWithOps_RenameError(t *testing.T) {
2387+
mockFS := NewMockFileOpsWithRenameError()
2388+
if err := mockFS.MkdirAll("/test", 0700); err != nil {
2389+
t.Fatal(err)
2390+
}
2391+
cm := newTestConfigManager()
2392+
cm.configPath = "/test/cfg.yaml"
2393+
err := cm.replaceConfigFileWithOps("/test/.tmp-file", mockFS)
2394+
if err == nil {
2395+
t.Error("expected error when rename fails")
2396+
}
2397+
}

internal/keybindings/context_manager_test.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package keybindings
22

33
import (
44
"testing"
5+
"time"
56

67
"github.com/bmf-san/ggc/v8/internal/config"
78
)
@@ -60,3 +61,100 @@ func TestContextManagerStackAndCallbacks(t *testing.T) {
6061
t.Fatalf("exit on empty stack returned %v", got)
6162
}
6263
}
64+
65+
// ── ContextManager: SetContext, ForceEnvironment ─────────────────────────────
66+
67+
func TestContextManager_SetContext(t *testing.T) {
68+
resolver := NewKeyBindingResolver(&config.Config{})
69+
RegisterBuiltinProfiles(resolver)
70+
cm := NewContextManager(resolver)
71+
72+
var transitions [][2]Context
73+
cm.RegisterContextCallback(ContextResults, func(from, to Context) {
74+
transitions = append(transitions, [2]Context{from, to})
75+
})
76+
77+
// SetContext to new context
78+
cm.SetContext(ContextResults)
79+
if cm.GetCurrentContext() != ContextResults {
80+
t.Errorf("SetContext: current = %v, want %v", cm.GetCurrentContext(), ContextResults)
81+
}
82+
if len(transitions) != 1 {
83+
t.Errorf("expected 1 transition callback, got %d", len(transitions))
84+
}
85+
86+
// SetContext to same context should be no-op
87+
cm.SetContext(ContextResults)
88+
if len(transitions) != 1 {
89+
t.Errorf("SetContext same context should not fire callback, got %d transitions", len(transitions))
90+
}
91+
92+
// Stack should be unmodified
93+
if len(cm.GetContextStack()) != 0 {
94+
t.Errorf("SetContext should not modify stack, got %v", cm.GetContextStack())
95+
}
96+
}
97+
98+
func TestContextManager_ForceEnvironment(t *testing.T) {
99+
resolver := NewKeyBindingResolver(&config.Config{})
100+
RegisterBuiltinProfiles(resolver)
101+
cm := NewContextManager(resolver)
102+
103+
// Should not panic
104+
cm.ForceEnvironment("darwin", "xterm-256color")
105+
}
106+
107+
func TestContextManager_ForceEnvironment_NilCM(t *testing.T) {
108+
var cm *ContextManager
109+
// Should not panic
110+
cm.ForceEnvironment("linux", "xterm")
111+
}
112+
113+
// ── ContextTransitionAnimator ────────────────────────────────────────────────
114+
115+
func TestContextTransitionAnimator_FadeAndSlide(t *testing.T) {
116+
cta := NewContextTransitionAnimator()
117+
cta.SetDuration(0) // no sleep in tests
118+
119+
cta.SetStyle("fade")
120+
cta.AnimateTransition(ContextGlobal, ContextResults)
121+
122+
cta.SetStyle("slide")
123+
cta.AnimateTransition(ContextGlobal, ContextInput)
124+
}
125+
126+
func TestContextTransitionAnimator_Disable(t *testing.T) {
127+
cta := NewContextTransitionAnimator()
128+
cta.Disable()
129+
// Should return early without doing anything
130+
cta.AnimateTransition(ContextGlobal, ContextResults)
131+
if cta.enabled {
132+
t.Error("expected disabled animator")
133+
}
134+
}
135+
136+
func TestContextTransitionAnimator_Enable(t *testing.T) {
137+
cta := NewContextTransitionAnimator()
138+
cta.Disable()
139+
cta.Enable()
140+
if !cta.enabled {
141+
t.Error("expected enabled animator")
142+
}
143+
}
144+
145+
func TestContextTransitionAnimator_RegisterAnimation(t *testing.T) {
146+
cta := NewContextTransitionAnimator()
147+
cta.RegisterAnimation(func(from, to Context) {})
148+
cta.RegisterAnimation(func(from, to Context) {})
149+
if len(cta.animations) != 2 {
150+
t.Errorf("expected 2 registered animations, got %d", len(cta.animations))
151+
}
152+
}
153+
154+
func TestContextTransitionAnimator_SetDuration(t *testing.T) {
155+
cta := NewContextTransitionAnimator()
156+
cta.SetDuration(500 * time.Millisecond)
157+
if cta.duration != 500*time.Millisecond {
158+
t.Errorf("duration = %v, want 500ms", cta.duration)
159+
}
160+
}

0 commit comments

Comments
 (0)