Files
gastown/internal/keepalive/keepalive_test.go
Bo 1f272ffc53 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>
2026-01-11 23:04:03 -08:00

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"
}
}