* 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>
451 lines
9.6 KiB
Go
451 lines
9.6 KiB
Go
package connection
|
|
|
|
import (
|
|
"testing"
|
|
)
|
|
|
|
func TestParseAddress(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
want *Address
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "rig/polecat",
|
|
input: "gastown/rictus",
|
|
want: &Address{Rig: "gastown", Polecat: "rictus"},
|
|
},
|
|
{
|
|
name: "rig/ broadcast",
|
|
input: "gastown/",
|
|
want: &Address{Rig: "gastown"},
|
|
},
|
|
{
|
|
name: "machine:rig/polecat",
|
|
input: "vm:gastown/rictus",
|
|
want: &Address{Machine: "vm", Rig: "gastown", Polecat: "rictus"},
|
|
},
|
|
{
|
|
name: "machine:rig/ broadcast",
|
|
input: "vm:gastown/",
|
|
want: &Address{Machine: "vm", Rig: "gastown"},
|
|
},
|
|
{
|
|
name: "rig only (no slash)",
|
|
input: "gastown",
|
|
want: &Address{Rig: "gastown"},
|
|
},
|
|
{
|
|
name: "empty string",
|
|
input: "",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "empty machine",
|
|
input: ":gastown/rictus",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "empty rig",
|
|
input: "vm:/rictus",
|
|
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 nil", tt.input)
|
|
}
|
|
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 TestAddressString(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: "vm:gastown/rictus",
|
|
},
|
|
{
|
|
addr: &Address{Machine: "vm", Rig: "gastown"},
|
|
want: "vm:gastown/",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.want, func(t *testing.T) {
|
|
got := tt.addr.String()
|
|
if got != tt.want {
|
|
t.Errorf("String() = %q, want %q", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAddressIsLocal(t *testing.T) {
|
|
tests := []struct {
|
|
addr *Address
|
|
want bool
|
|
}{
|
|
{&Address{Rig: "gastown"}, true},
|
|
{&Address{Machine: "", Rig: "gastown"}, true},
|
|
{&Address{Machine: "local", Rig: "gastown"}, true},
|
|
{&Address{Machine: "vm", Rig: "gastown"}, false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.addr.String(), func(t *testing.T) {
|
|
if got := tt.addr.IsLocal(); got != tt.want {
|
|
t.Errorf("IsLocal() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAddressIsBroadcast(t *testing.T) {
|
|
tests := []struct {
|
|
addr *Address
|
|
want bool
|
|
}{
|
|
{&Address{Rig: "gastown"}, true},
|
|
{&Address{Rig: "gastown", Polecat: ""}, true},
|
|
{&Address{Rig: "gastown", Polecat: "rictus"}, false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.addr.String(), func(t *testing.T) {
|
|
if got := tt.addr.IsBroadcast(); got != tt.want {
|
|
t.Errorf("IsBroadcast() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAddressEqual(t *testing.T) {
|
|
tests := []struct {
|
|
a, b *Address
|
|
want bool
|
|
}{
|
|
{
|
|
&Address{Rig: "gastown", Polecat: "rictus"},
|
|
&Address{Rig: "gastown", Polecat: "rictus"},
|
|
true,
|
|
},
|
|
{
|
|
&Address{Machine: "", Rig: "gastown"},
|
|
&Address{Machine: "local", Rig: "gastown"},
|
|
true,
|
|
},
|
|
{
|
|
&Address{Rig: "gastown", Polecat: "rictus"},
|
|
&Address{Rig: "gastown", Polecat: "nux"},
|
|
false,
|
|
},
|
|
{
|
|
&Address{Rig: "gastown"},
|
|
nil,
|
|
false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
name := "equal"
|
|
if !tt.want {
|
|
name = "not equal"
|
|
}
|
|
t.Run(name, func(t *testing.T) {
|
|
if got := tt.a.Equal(tt.b); got != tt.want {
|
|
t.Errorf("Equal() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
})
|
|
}
|
|
}
|