Skip to content

Commit fca93de

Browse files
fullstackjamclaude
andauthored
Add comprehensive test coverage for diff formatting and comparison (#37)
* test: fill unit-test gaps in diff, search, and permissions - diff: add direct tests for ToSet and PluginsEqual (previously untested) - diff: cover HasChanges/TotalChanged dotfiles (dirty, unpushed, repoChanged) and shell branches; all previously untested code paths - diff: add FormatTerminal tests for dotfiles and shell sections (all printDotfilesSection / printShellSection branches exercised) - diff: add FormatJSON tests verifying shell, dotfiles, and dev_tools keys appear in output with correct summary counts - diff: add CompareSnapshotToRemote test with macOS prefs in remote config - diff: add diffDotfiles tests for .git suffix normalization, empty-reference no-change guarantee, and distinct-URL change detection - search: add getAPIBase tests covering default URL, valid HTTPS override, valid localhost override, and disallowed HTTP non-loopback fallback - permissions: test OpenScreenRecordingSettings non-darwin stub returns nil https://claude.ai/code/session_01TfJCwVRqyyW8dcfxs57Y1v * test: drop redundant permissions test, clarify PluginsEqual duplicate case - permissions: merge TestHasScreenRecordingPermission_NonDarwin into _Returns (both asserted IsType on the same call); rename TestOpenScreenRecordingSettings_NonDarwin to _Returns for symmetry - diff: add inline comment on the duplicates-in-a PluginsEqual test case explaining why false is returned (set collapse, not length mismatch) https://claude.ai/code/session_01TfJCwVRqyyW8dcfxs57Y1v --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent f892a1a commit fca93de

File tree

5 files changed

+363
-3
lines changed

5 files changed

+363
-3
lines changed

internal/diff/compare_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,3 +210,66 @@ func TestCompareSnapshots_EmptySnapshots(t *testing.T) {
210210
assert.Equal(t, 0, result.TotalExtra())
211211
assert.Equal(t, 0, result.TotalChanged())
212212
}
213+
214+
func TestCompareSnapshotToRemote_WithMacOSPrefs(t *testing.T) {
215+
isolateHome(t)
216+
system := &snapshot.Snapshot{
217+
MacOSPrefs: []snapshot.MacOSPref{
218+
{Domain: "com.apple.dock", Key: "autohide", Value: "false"},
219+
},
220+
}
221+
remote := &config.RemoteConfig{
222+
MacOSPrefs: []config.RemoteMacOSPref{
223+
{Domain: "com.apple.dock", Key: "autohide", Value: "true"},
224+
{Domain: "com.apple.dock", Key: "tilesize", Value: "48"},
225+
},
226+
}
227+
228+
result := CompareSnapshotToRemote(system, remote, Source{Kind: "remote", Path: "user/cfg"})
229+
230+
require.NotNil(t, result.MacOS)
231+
assert.Len(t, result.MacOS.Changed, 1)
232+
assert.Equal(t, "autohide", result.MacOS.Changed[0].Key)
233+
assert.Equal(t, "false", result.MacOS.Changed[0].System)
234+
assert.Equal(t, "true", result.MacOS.Changed[0].Reference)
235+
assert.Len(t, result.MacOS.Missing, 1)
236+
assert.Equal(t, "tilesize", result.MacOS.Missing[0].Key)
237+
}
238+
239+
func TestDiffDotfiles_GitSuffixNormalization(t *testing.T) {
240+
isolateHome(t)
241+
242+
// Same repo, one with .git suffix and one without — should not report a change.
243+
result := diffDotfiles(
244+
"https://github.com/user/dotfiles.git",
245+
"https://github.com/user/dotfiles",
246+
)
247+
assert.Nil(t, result.RepoChanged, "trailing .git suffix should be normalized away")
248+
}
249+
250+
func TestDiffDotfiles_DifferentRepos(t *testing.T) {
251+
isolateHome(t)
252+
253+
result := diffDotfiles(
254+
"https://github.com/user/dotfiles-old",
255+
"https://github.com/user/dotfiles-new",
256+
)
257+
require.NotNil(t, result.RepoChanged)
258+
assert.Equal(t, "https://github.com/user/dotfiles-old", result.RepoChanged.System)
259+
assert.Equal(t, "https://github.com/user/dotfiles-new", result.RepoChanged.Reference)
260+
}
261+
262+
func TestDiffDotfiles_BothEmpty(t *testing.T) {
263+
isolateHome(t)
264+
result := diffDotfiles("", "")
265+
assert.Nil(t, result.RepoChanged)
266+
assert.False(t, result.Dirty)
267+
assert.False(t, result.Unpushed)
268+
}
269+
270+
func TestDiffDotfiles_EmptyReference(t *testing.T) {
271+
isolateHome(t)
272+
// Reference is empty → no change reported even if system has one.
273+
result := diffDotfiles("https://github.com/user/dotfiles", "")
274+
assert.Nil(t, result.RepoChanged)
275+
}

internal/diff/diff_test.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,3 +200,98 @@ func TestDiffResult_NilSections(t *testing.T) {
200200
assert.Equal(t, 0, r.TotalChanged())
201201
assert.False(t, r.HasChanges())
202202
}
203+
204+
func TestDiffResult_HasChanges_DotfilesDirty(t *testing.T) {
205+
r := &DiffResult{Dotfiles: &DotfilesDiff{Dirty: true}}
206+
assert.True(t, r.HasChanges())
207+
}
208+
209+
func TestDiffResult_HasChanges_DotfilesUnpushed(t *testing.T) {
210+
r := &DiffResult{Dotfiles: &DotfilesDiff{Unpushed: true}}
211+
assert.True(t, r.HasChanges())
212+
}
213+
214+
func TestDiffResult_HasChanges_DotfilesRepoChanged(t *testing.T) {
215+
r := &DiffResult{Dotfiles: &DotfilesDiff{RepoChanged: &ValueChange{System: "a", Reference: "b"}}}
216+
assert.True(t, r.HasChanges())
217+
}
218+
219+
func TestDiffResult_HasChanges_DotfilesEmptyNoChange(t *testing.T) {
220+
r := &DiffResult{Dotfiles: &DotfilesDiff{}}
221+
assert.False(t, r.HasChanges())
222+
}
223+
224+
func TestDiffResult_HasChanges_Shell(t *testing.T) {
225+
r := &DiffResult{Shell: &ShellDiff{ThemeChanged: true}}
226+
assert.True(t, r.HasChanges())
227+
}
228+
229+
func TestDiffResult_TotalChanged_DotfilesRepoChanged(t *testing.T) {
230+
r := &DiffResult{Dotfiles: &DotfilesDiff{RepoChanged: &ValueChange{System: "old", Reference: "new"}}}
231+
assert.Equal(t, 1, r.TotalChanged())
232+
}
233+
234+
func TestDiffResult_TotalChanged_DotfilesNilRepoChanged(t *testing.T) {
235+
r := &DiffResult{Dotfiles: &DotfilesDiff{Dirty: true}}
236+
assert.Equal(t, 0, r.TotalChanged())
237+
}
238+
239+
func TestDiffResult_TotalChanged_Shell(t *testing.T) {
240+
r := &DiffResult{Shell: &ShellDiff{PluginsChanged: true}}
241+
assert.Equal(t, 1, r.TotalChanged())
242+
}
243+
244+
func TestDiffResult_TotalChanged_DotfilesAndShell(t *testing.T) {
245+
r := &DiffResult{
246+
Dotfiles: &DotfilesDiff{RepoChanged: &ValueChange{System: "a", Reference: "b"}},
247+
Shell: &ShellDiff{ThemeChanged: true},
248+
}
249+
assert.Equal(t, 2, r.TotalChanged())
250+
}
251+
252+
func TestToSet(t *testing.T) {
253+
tests := []struct {
254+
name string
255+
input []string
256+
wantKeys []string
257+
}{
258+
{"nil input", nil, nil},
259+
{"empty input", []string{}, nil},
260+
{"single item", []string{"a"}, []string{"a"}},
261+
{"multiple items", []string{"a", "b", "c"}, []string{"a", "b", "c"}},
262+
{"duplicates collapse", []string{"a", "a", "b"}, []string{"a", "b"}},
263+
}
264+
265+
for _, tt := range tests {
266+
t.Run(tt.name, func(t *testing.T) {
267+
got := ToSet(tt.input)
268+
assert.Equal(t, len(tt.wantKeys), len(got))
269+
for _, k := range tt.wantKeys {
270+
assert.True(t, got[k], "expected key %q in set", k)
271+
}
272+
})
273+
}
274+
}
275+
276+
func TestPluginsEqual(t *testing.T) {
277+
tests := []struct {
278+
name string
279+
a, b []string
280+
want bool
281+
}{
282+
{"both nil", nil, nil, true},
283+
{"both empty", []string{}, []string{}, true},
284+
{"identical order", []string{"git", "z"}, []string{"git", "z"}, true},
285+
{"different order", []string{"z", "git"}, []string{"git", "z"}, true},
286+
{"different length", []string{"a"}, []string{"a", "b"}, false},
287+
{"different elements", []string{"a", "b"}, []string{"a", "c"}, false},
288+
// len guard (2==2) passes, but the set built from ["a","a"] lacks "b" → false.
289+
{"duplicates in a", []string{"a", "a"}, []string{"a", "b"}, false},
290+
}
291+
292+
for _, tt := range tests {
293+
t.Run(tt.name, func(t *testing.T) {
294+
assert.Equal(t, tt.want, PluginsEqual(tt.a, tt.b))
295+
})
296+
}
297+
}

internal/diff/format_test.go

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,3 +194,170 @@ func TestFormatTerminal_PackagesOnly(t *testing.T) {
194194
FormatTerminal(result, true)
195195
})
196196
}
197+
198+
func TestFormatTerminal_DotfilesSection(t *testing.T) {
199+
tests := []struct {
200+
name string
201+
result *DiffResult
202+
}{
203+
{
204+
name: "repo changed",
205+
result: &DiffResult{
206+
Source: Source{Kind: "remote", Path: "user/slug"},
207+
Dotfiles: &DotfilesDiff{RepoChanged: &ValueChange{System: "old-repo", Reference: "new-repo"}},
208+
},
209+
},
210+
{
211+
name: "dirty",
212+
result: &DiffResult{
213+
Source: Source{Kind: "local", Path: "test.json"},
214+
Dotfiles: &DotfilesDiff{Dirty: true},
215+
},
216+
},
217+
{
218+
name: "unpushed",
219+
result: &DiffResult{
220+
Source: Source{Kind: "local", Path: "test.json"},
221+
Dotfiles: &DotfilesDiff{Unpushed: true},
222+
},
223+
},
224+
{
225+
name: "all dotfiles conditions",
226+
result: &DiffResult{
227+
Source: Source{Kind: "file", Path: "snap.json"},
228+
Dotfiles: &DotfilesDiff{
229+
RepoChanged: &ValueChange{System: "a", Reference: "b"},
230+
Dirty: true,
231+
Unpushed: true,
232+
},
233+
},
234+
},
235+
{
236+
name: "empty dotfiles skips section",
237+
result: &DiffResult{
238+
Source: Source{Kind: "local", Path: "test.json"},
239+
Dotfiles: &DotfilesDiff{},
240+
},
241+
},
242+
}
243+
244+
for _, tt := range tests {
245+
t.Run(tt.name, func(t *testing.T) {
246+
assert.NotPanics(t, func() {
247+
FormatTerminal(tt.result, false)
248+
})
249+
})
250+
}
251+
}
252+
253+
func TestFormatTerminal_ShellSection(t *testing.T) {
254+
tests := []struct {
255+
name string
256+
result *DiffResult
257+
}{
258+
{
259+
name: "theme changed",
260+
result: &DiffResult{
261+
Source: Source{Kind: "remote", Path: "user/slug"},
262+
Shell: &ShellDiff{
263+
ThemeChanged: true,
264+
LocalTheme: "robbyrussell",
265+
ReferenceTheme: "agnoster",
266+
},
267+
},
268+
},
269+
{
270+
name: "plugins changed",
271+
result: &DiffResult{
272+
Source: Source{Kind: "remote", Path: "user/slug"},
273+
Shell: &ShellDiff{
274+
PluginsChanged: true,
275+
LocalPlugins: []string{"git"},
276+
ReferencePlugins: []string{"git", "z"},
277+
},
278+
},
279+
},
280+
{
281+
name: "theme and plugins changed",
282+
result: &DiffResult{
283+
Source: Source{Kind: "remote", Path: "user/slug"},
284+
Shell: &ShellDiff{
285+
ThemeChanged: true,
286+
LocalTheme: "",
287+
ReferenceTheme: "agnoster",
288+
PluginsChanged: true,
289+
LocalPlugins: nil,
290+
ReferencePlugins: []string{"git"},
291+
},
292+
},
293+
},
294+
{
295+
name: "shell diff with no changes skips section",
296+
result: &DiffResult{
297+
Source: Source{Kind: "remote", Path: "user/slug"},
298+
Shell: &ShellDiff{ThemeChanged: false, PluginsChanged: false},
299+
},
300+
},
301+
}
302+
303+
for _, tt := range tests {
304+
t.Run(tt.name, func(t *testing.T) {
305+
assert.NotPanics(t, func() {
306+
FormatTerminal(tt.result, false)
307+
})
308+
})
309+
}
310+
}
311+
312+
func TestFormatJSON_WithShellAndDotfiles(t *testing.T) {
313+
result := &DiffResult{
314+
Source: Source{Kind: "remote", Path: "user/cfg"},
315+
Shell: &ShellDiff{
316+
ThemeChanged: true,
317+
LocalTheme: "robbyrussell",
318+
ReferenceTheme: "agnoster",
319+
},
320+
Dotfiles: &DotfilesDiff{
321+
RepoChanged: &ValueChange{System: "old", Reference: "new"},
322+
},
323+
}
324+
325+
data, err := FormatJSON(result)
326+
require.NoError(t, err)
327+
328+
var parsed map[string]interface{}
329+
err = json.Unmarshal(data, &parsed)
330+
require.NoError(t, err)
331+
332+
assert.Contains(t, parsed, "shell")
333+
assert.Contains(t, parsed, "dotfiles")
334+
335+
// Shell adds 1 to TotalChanged; dotfiles.RepoChanged adds 1 → total 2
336+
summary := parsed["summary"].(map[string]interface{})
337+
assert.Equal(t, float64(2), summary["changed"])
338+
}
339+
340+
func TestFormatJSON_WithDevToolsSection(t *testing.T) {
341+
result := &DiffResult{
342+
Source: Source{Kind: "local", Path: "snap.json"},
343+
DevTools: &DevToolDiff{
344+
Missing: []string{"rust"},
345+
Extra: []string{"python"},
346+
Changed: []DevToolDelta{{Name: "go", System: "1.22", Reference: "1.24"}},
347+
Common: 2,
348+
},
349+
}
350+
351+
data, err := FormatJSON(result)
352+
require.NoError(t, err)
353+
354+
var parsed map[string]interface{}
355+
err = json.Unmarshal(data, &parsed)
356+
require.NoError(t, err)
357+
358+
assert.Contains(t, parsed, "dev_tools")
359+
summary := parsed["summary"].(map[string]interface{})
360+
assert.Equal(t, float64(1), summary["missing"])
361+
assert.Equal(t, float64(1), summary["extra"])
362+
assert.Equal(t, float64(1), summary["changed"])
363+
}

internal/permissions/screen_recording_test.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,17 @@ import (
66
"github.com/stretchr/testify/assert"
77
)
88

9+
// TestHasScreenRecordingPermission_Returns verifies the function returns a bool.
10+
// On non-darwin/non-cgo builds the stub always returns false; on darwin+cgo
11+
// the result depends on system permissions, so we only check the type.
912
func TestHasScreenRecordingPermission_Returns(t *testing.T) {
1013
result := HasScreenRecordingPermission()
1114
assert.IsType(t, true, result)
1215
}
1316

14-
// TestOpenScreenRecordingSettings is intentionally omitted: the function's only
15-
// effect is opening System Settings UI on macOS, which cannot be meaningfully
16-
// unit-tested without side-effecting the developer's machine.
17+
// TestOpenScreenRecordingSettings_Returns verifies the non-darwin/non-cgo stub
18+
// is a no-op and returns nil.
19+
func TestOpenScreenRecordingSettings_Returns(t *testing.T) {
20+
err := OpenScreenRecordingSettings()
21+
assert.NoError(t, err)
22+
}

internal/search/search_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,35 @@ func TestQueryAPI_CaskAndNpmFlags(t *testing.T) {
217217
}
218218
}
219219

220+
// ---------------------------------------------------------------------------
221+
// getAPIBase env var handling
222+
// ---------------------------------------------------------------------------
223+
224+
func TestGetAPIBase_DefaultURL(t *testing.T) {
225+
t.Setenv("OPENBOOT_API_URL", "")
226+
base := getAPIBase()
227+
assert.Equal(t, "https://openboot.dev/api", base)
228+
}
229+
230+
func TestGetAPIBase_ValidHTTPSOverride(t *testing.T) {
231+
t.Setenv("OPENBOOT_API_URL", "https://staging.openboot.dev")
232+
base := getAPIBase()
233+
assert.Equal(t, "https://staging.openboot.dev/api", base)
234+
}
235+
236+
func TestGetAPIBase_ValidLocalhostOverride(t *testing.T) {
237+
t.Setenv("OPENBOOT_API_URL", "http://localhost:8080")
238+
base := getAPIBase()
239+
assert.Equal(t, "http://localhost:8080/api", base)
240+
}
241+
242+
func TestGetAPIBase_DisallowedURLFallsBack(t *testing.T) {
243+
// Plain HTTP to a non-loopback host must not be accepted.
244+
t.Setenv("OPENBOOT_API_URL", "http://evil.example.com")
245+
base := getAPIBase()
246+
assert.Equal(t, "https://openboot.dev/api", base)
247+
}
248+
220249
// ---------------------------------------------------------------------------
221250
// Package field mapping
222251
// ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)