* 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>
139 lines
3.6 KiB
Go
139 lines
3.6 KiB
Go
package keepalive
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestTouchInWorkspace(t *testing.T) {
|
|
// Create temp directory
|
|
tmpDir := t.TempDir()
|
|
|
|
// Touch the keepalive
|
|
TouchInWorkspace(tmpDir, "gt status")
|
|
|
|
// Read back
|
|
state := Read(tmpDir)
|
|
if state == nil {
|
|
t.Fatal("expected state to be non-nil")
|
|
}
|
|
|
|
if state.LastCommand != "gt status" {
|
|
t.Errorf("expected last_command 'gt status', got %q", state.LastCommand)
|
|
}
|
|
|
|
// Check timestamp is recent
|
|
if time.Since(state.Timestamp) > time.Minute {
|
|
t.Errorf("timestamp too old: %v", state.Timestamp)
|
|
}
|
|
}
|
|
|
|
func TestReadNonExistent(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
state := Read(tmpDir)
|
|
if state != nil {
|
|
t.Error("expected nil state for non-existent file")
|
|
}
|
|
}
|
|
|
|
func TestStateAge(t *testing.T) {
|
|
// Test nil state returns very large age
|
|
var nilState *State
|
|
if nilState.Age() < 24*time.Hour {
|
|
t.Error("nil state should have very large age")
|
|
}
|
|
|
|
// Test fresh state returns accurate age
|
|
freshState := &State{Timestamp: time.Now().Add(-30 * time.Second)}
|
|
age := freshState.Age()
|
|
if age < 29*time.Second || age > 31*time.Second {
|
|
t.Errorf("expected ~30s age, got %v", age)
|
|
}
|
|
|
|
// Test older state returns accurate age
|
|
olderState := &State{Timestamp: time.Now().Add(-5 * time.Minute)}
|
|
age = olderState.Age()
|
|
if age < 4*time.Minute+55*time.Second || age > 5*time.Minute+5*time.Second {
|
|
t.Errorf("expected ~5m age, got %v", age)
|
|
}
|
|
|
|
// NOTE: IsFresh(), IsStale(), IsVeryStale() were removed as part of ZFC cleanup.
|
|
// Staleness classification belongs in Deacon molecule, not Go code.
|
|
}
|
|
|
|
func TestDirectoryCreation(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
workDir := filepath.Join(tmpDir, "some", "nested", "workspace")
|
|
|
|
// Touch should create .runtime directory
|
|
TouchInWorkspace(workDir, "gt test")
|
|
|
|
// Verify directory was created
|
|
runtimeDir := filepath.Join(workDir, ".runtime")
|
|
if _, err := os.Stat(runtimeDir); os.IsNotExist(err) {
|
|
t.Error("expected .runtime directory to be created")
|
|
}
|
|
}
|
|
|
|
// Example functions demonstrate keepalive usage patterns.
|
|
|
|
func ExampleTouchInWorkspace() {
|
|
// TouchInWorkspace signals agent activity in a specific workspace.
|
|
// This is the core function - use it when you know the workspace root.
|
|
|
|
workspaceRoot := "/path/to/workspace"
|
|
|
|
// Signal that "gt status" was run
|
|
TouchInWorkspace(workspaceRoot, "gt status")
|
|
|
|
// Signal a command with arguments
|
|
TouchInWorkspace(workspaceRoot, "gt sling bd-abc123 ai-platform")
|
|
|
|
// All errors are silently ignored (best-effort design).
|
|
// This is intentional - keepalive failures should never break commands.
|
|
}
|
|
|
|
func ExampleRead() {
|
|
// Read retrieves the current keepalive state for a workspace.
|
|
// Returns nil if no keepalive file exists or it can't be read.
|
|
|
|
workspaceRoot := "/path/to/workspace"
|
|
state := Read(workspaceRoot)
|
|
|
|
if state == nil {
|
|
// No keepalive found - agent may not have run any commands yet
|
|
return
|
|
}
|
|
|
|
// Access the last command that was run
|
|
_ = state.LastCommand // e.g., "gt status"
|
|
|
|
// Access when the command was run
|
|
_ = state.Timestamp // time.Time in UTC
|
|
}
|
|
|
|
func ExampleState_Age() {
|
|
// Age() returns how long ago the keepalive was updated.
|
|
// This is useful for detecting idle or stuck agents.
|
|
|
|
workspaceRoot := "/path/to/workspace"
|
|
state := Read(workspaceRoot)
|
|
|
|
// Age() is nil-safe - returns ~1 year for nil state
|
|
age := state.Age()
|
|
|
|
// Check if agent was active recently (within 5 minutes)
|
|
if age < 5*time.Minute {
|
|
// Agent is active
|
|
_ = "active"
|
|
}
|
|
|
|
// Check if agent might be stuck (no activity for 30+ minutes)
|
|
if age > 30*time.Minute {
|
|
// Agent may need attention
|
|
_ = "possibly stuck"
|
|
}
|
|
}
|