test: comprehensive test coverage for 5 packages (#351)
* test(util): add comprehensive tests for atomic write functions Add tests for: - File permissions - Empty data handling - Various JSON types (string, int, float, bool, null, array, nested) - Unmarshallable types error handling - Read-only directory permission errors - Concurrent writes - Original content preservation on failure - Struct serialization/deserialization - Large data (1MB) * test(connection): add edge case tests for address parsing Add comprehensive test coverage for ParseAddress edge cases: - Empty/whitespace/slash-only inputs - Leading/trailing slash handling - Machine prefix edge cases (colons, empty machine) - Multiple slashes in polecat name (SplitN behavior) - Unicode and emoji support - Very long addresses - Special characters (hyphens, underscores, dots) - Whitespace in components Also adds tests for MustParseAddress panic behavior and RigPath method. Closes: gt-xgjyp * test(checkpoint): add comprehensive test coverage for checkpoint package Tests all public functions: Read, Write, Remove, Capture, WithMolecule, WithHookedBead, WithNotes, Age, IsStale, Summary, Path. Edge cases covered: missing file, corrupted JSON, stale detection. Closes: gt-09yn1 * test(lock): add comprehensive tests for lock package Add lock_test.go with tests covering: - LockInfo.IsStale() with valid/invalid PIDs - Lock.Acquire/Release lifecycle - Re-acquiring own lock (session refresh) - Stale lock cleanup during Acquire - Lock.Read() for missing/invalid/valid files - Lock.Check() for unlocked/owned/stale scenarios - Lock.Status() string formatting - Lock.ForceRelease() - processExists() helper - FindAllLocks() directory scanning - CleanStaleLocks() with mocked tmux - getActiveTmuxSessions() parsing - splitOnColon() and splitLines() helpers - DetectCollisions() for stale/orphaned locks Coverage: 84.4% * test(keepalive): add example tests demonstrating usage patterns Add ExampleTouchInWorkspace, ExampleRead, and ExampleState_Age to serve as documentation for how to use the keepalive package. * fix(test): correct boundary test timing race in checkpoint_test.go The 'exactly threshold' test case was flaky due to timing: by the time time.Since() runs after setting Timestamp, microseconds have passed, making age > threshold. Changed expectation to true since at-threshold is effectively stale. --------- Co-authored-by: slit <gt@gastown.local>
This commit is contained in:
@@ -190,3 +190,261 @@ func TestAddressEqual(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAddress_EdgeCases(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want *Address
|
||||
wantErr bool
|
||||
}{
|
||||
// Malformed: empty/whitespace variations
|
||||
{
|
||||
name: "empty string",
|
||||
input: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "whitespace only",
|
||||
input: " ",
|
||||
want: &Address{Rig: " "},
|
||||
wantErr: false, // whitespace-only rig is technically parsed
|
||||
},
|
||||
{
|
||||
name: "just slash",
|
||||
input: "/",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "double slash",
|
||||
input: "//",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "triple slash",
|
||||
input: "///",
|
||||
wantErr: true,
|
||||
},
|
||||
|
||||
// Malformed: leading/trailing issues
|
||||
{
|
||||
name: "leading slash",
|
||||
input: "/polecat",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "leading slash with rig",
|
||||
input: "/rig/polecat",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "trailing slash is broadcast",
|
||||
input: "rig/",
|
||||
want: &Address{Rig: "rig"},
|
||||
},
|
||||
|
||||
// Machine prefix edge cases
|
||||
{
|
||||
name: "colon only",
|
||||
input: ":",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "colon with trailing slash",
|
||||
input: ":/",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "empty machine with colon",
|
||||
input: ":rig/polecat",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "multiple colons in machine",
|
||||
input: "host:8080:rig/polecat",
|
||||
want: &Address{Machine: "host", Rig: "8080:rig", Polecat: "polecat"},
|
||||
},
|
||||
{
|
||||
name: "colon in rig name",
|
||||
input: "machine:rig:port/polecat",
|
||||
want: &Address{Machine: "machine", Rig: "rig:port", Polecat: "polecat"},
|
||||
},
|
||||
|
||||
// Multiple slash handling (SplitN behavior)
|
||||
{
|
||||
name: "extra slashes in polecat",
|
||||
input: "rig/pole/cat/extra",
|
||||
want: &Address{Rig: "rig", Polecat: "pole/cat/extra"},
|
||||
},
|
||||
{
|
||||
name: "many path components",
|
||||
input: "a/b/c/d/e",
|
||||
want: &Address{Rig: "a", Polecat: "b/c/d/e"},
|
||||
},
|
||||
|
||||
// Unicode handling
|
||||
{
|
||||
name: "unicode rig name",
|
||||
input: "日本語/polecat",
|
||||
want: &Address{Rig: "日本語", Polecat: "polecat"},
|
||||
},
|
||||
{
|
||||
name: "unicode polecat name",
|
||||
input: "rig/工作者",
|
||||
want: &Address{Rig: "rig", Polecat: "工作者"},
|
||||
},
|
||||
{
|
||||
name: "emoji in address",
|
||||
input: "🔧/🐱",
|
||||
want: &Address{Rig: "🔧", Polecat: "🐱"},
|
||||
},
|
||||
{
|
||||
name: "unicode machine name",
|
||||
input: "マシン:rig/polecat",
|
||||
want: &Address{Machine: "マシン", Rig: "rig", Polecat: "polecat"},
|
||||
},
|
||||
|
||||
// Long addresses
|
||||
{
|
||||
name: "very long rig name",
|
||||
input: "abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789/polecat",
|
||||
want: &Address{Rig: "abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789", Polecat: "polecat"},
|
||||
},
|
||||
{
|
||||
name: "very long polecat name",
|
||||
input: "rig/abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789",
|
||||
want: &Address{Rig: "rig", Polecat: "abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789"},
|
||||
},
|
||||
|
||||
// Special characters
|
||||
{
|
||||
name: "hyphen in names",
|
||||
input: "my-rig/my-polecat",
|
||||
want: &Address{Rig: "my-rig", Polecat: "my-polecat"},
|
||||
},
|
||||
{
|
||||
name: "underscore in names",
|
||||
input: "my_rig/my_polecat",
|
||||
want: &Address{Rig: "my_rig", Polecat: "my_polecat"},
|
||||
},
|
||||
{
|
||||
name: "dots in names",
|
||||
input: "my.rig/my.polecat",
|
||||
want: &Address{Rig: "my.rig", Polecat: "my.polecat"},
|
||||
},
|
||||
{
|
||||
name: "mixed special chars",
|
||||
input: "rig-1_v2.0/polecat-alpha_1.0",
|
||||
want: &Address{Rig: "rig-1_v2.0", Polecat: "polecat-alpha_1.0"},
|
||||
},
|
||||
|
||||
// Whitespace in components
|
||||
{
|
||||
name: "space in rig name",
|
||||
input: "my rig/polecat",
|
||||
want: &Address{Rig: "my rig", Polecat: "polecat"},
|
||||
},
|
||||
{
|
||||
name: "space in polecat name",
|
||||
input: "rig/my polecat",
|
||||
want: &Address{Rig: "rig", Polecat: "my polecat"},
|
||||
},
|
||||
{
|
||||
name: "leading space in rig",
|
||||
input: " rig/polecat",
|
||||
want: &Address{Rig: " rig", Polecat: "polecat"},
|
||||
},
|
||||
{
|
||||
name: "trailing space in polecat",
|
||||
input: "rig/polecat ",
|
||||
want: &Address{Rig: "rig", Polecat: "polecat "},
|
||||
},
|
||||
|
||||
// Edge case: machine with no rig after colon
|
||||
{
|
||||
name: "machine colon nothing",
|
||||
input: "machine:",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "machine colon slash",
|
||||
input: "machine:/",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := ParseAddress(tt.input)
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Errorf("ParseAddress(%q) expected error, got %+v", tt.input, got)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("ParseAddress(%q) unexpected error: %v", tt.input, err)
|
||||
return
|
||||
}
|
||||
if got.Machine != tt.want.Machine {
|
||||
t.Errorf("Machine = %q, want %q", got.Machine, tt.want.Machine)
|
||||
}
|
||||
if got.Rig != tt.want.Rig {
|
||||
t.Errorf("Rig = %q, want %q", got.Rig, tt.want.Rig)
|
||||
}
|
||||
if got.Polecat != tt.want.Polecat {
|
||||
t.Errorf("Polecat = %q, want %q", got.Polecat, tt.want.Polecat)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMustParseAddress_Panics(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Error("MustParseAddress with empty string should panic")
|
||||
}
|
||||
}()
|
||||
MustParseAddress("")
|
||||
}
|
||||
|
||||
func TestMustParseAddress_Valid(t *testing.T) {
|
||||
// Should not panic
|
||||
addr := MustParseAddress("rig/polecat")
|
||||
if addr.Rig != "rig" || addr.Polecat != "polecat" {
|
||||
t.Errorf("MustParseAddress returned wrong address: %+v", addr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddressRigPath(t *testing.T) {
|
||||
tests := []struct {
|
||||
addr *Address
|
||||
want string
|
||||
}{
|
||||
{
|
||||
addr: &Address{Rig: "gastown", Polecat: "rictus"},
|
||||
want: "gastown/rictus",
|
||||
},
|
||||
{
|
||||
addr: &Address{Rig: "gastown"},
|
||||
want: "gastown/",
|
||||
},
|
||||
{
|
||||
addr: &Address{Machine: "vm", Rig: "gastown", Polecat: "rictus"},
|
||||
want: "gastown/rictus",
|
||||
},
|
||||
{
|
||||
addr: &Address{Rig: "a", Polecat: "b/c/d"},
|
||||
want: "a/b/c/d",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.want, func(t *testing.T) {
|
||||
got := tt.addr.RigPath()
|
||||
if got != tt.want {
|
||||
t.Errorf("RigPath() = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user