test: Add test coverage for 16 files (40.3% → 45.5%) (#463)

* test: Add test coverage for 16 files (40.3% -> 45.5%)

Add comprehensive tests for previously untested packages:
- internal/agent/state_test.go
- internal/cmd/errors_test.go
- internal/crew/types_test.go
- internal/doctor/errors_test.go
- internal/dog/types_test.go
- internal/mail/bd_test.go
- internal/opencode/plugin_test.go
- internal/rig/overlay_test.go
- internal/runtime/runtime_test.go
- internal/session/town_test.go
- internal/style/style_test.go
- internal/ui/markdown_test.go
- internal/ui/terminal_test.go
- internal/wisp/io_test.go
- internal/wisp/types_test.go
- internal/witness/types_test.go

style_test.go uses func(...string) to match lipgloss variadic Render signature.

* fix(lint): remove unused error return from buildCVSummary

buildCVSummary always returned nil for its error value, causing
golangci-lint to fail with "result 1 (error) is always nil".

The function handles errors internally by returning partial data,
so the error return was misleading. Removed it and updated caller.
This commit is contained in:
Daniel Sauer
2026-01-13 22:19:27 +01:00
committed by GitHub
parent f42ec42268
commit fdd4b0aeb0
16 changed files with 2659 additions and 0 deletions

View File

@@ -0,0 +1,189 @@
package agent
import (
"os"
"path/filepath"
"testing"
)
func TestStateConstants(t *testing.T) {
tests := []struct {
name string
state State
value string
}{
{"StateStopped", StateStopped, "stopped"},
{"StateRunning", StateRunning, "running"},
{"StatePaused", StatePaused, "paused"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if string(tt.state) != tt.value {
t.Errorf("State constant = %q, want %q", tt.state, tt.value)
}
})
}
}
func TestStateManager_StateFile(t *testing.T) {
tmpDir := t.TempDir()
manager := NewStateManager[TestState](tmpDir, "test-state.json", func() *TestState {
return &TestState{Value: "default"}
})
expectedPath := filepath.Join(tmpDir, ".runtime", "test-state.json")
if manager.StateFile() != expectedPath {
t.Errorf("StateFile() = %q, want %q", manager.StateFile(), expectedPath)
}
}
func TestStateManager_Load_NoFile(t *testing.T) {
tmpDir := t.TempDir()
manager := NewStateManager[TestState](tmpDir, "nonexistent.json", func() *TestState {
return &TestState{Value: "default"}
})
state, err := manager.Load()
if err != nil {
t.Fatalf("Load() error = %v", err)
}
if state.Value != "default" {
t.Errorf("Load() value = %q, want %q", state.Value, "default")
}
}
func TestStateManager_Load_Save_Load(t *testing.T) {
tmpDir := t.TempDir()
manager := NewStateManager[TestState](tmpDir, "test-state.json", func() *TestState {
return &TestState{Value: "default"}
})
// Save initial state
state := &TestState{Value: "test-value", Count: 42}
if err := manager.Save(state); err != nil {
t.Fatalf("Save() error = %v", err)
}
// Load it back
loaded, err := manager.Load()
if err != nil {
t.Fatalf("Load() error = %v", err)
}
if loaded.Value != state.Value {
t.Errorf("Load() value = %q, want %q", loaded.Value, state.Value)
}
if loaded.Count != state.Count {
t.Errorf("Load() count = %d, want %d", loaded.Count, state.Count)
}
}
func TestStateManager_Load_CreatesDirectory(t *testing.T) {
tmpDir := t.TempDir()
manager := NewStateManager[TestState](tmpDir, "test-state.json", func() *TestState {
return &TestState{Value: "default"}
})
// Save should create .runtime directory
state := &TestState{Value: "test"}
if err := manager.Save(state); err != nil {
t.Fatalf("Save() error = %v", err)
}
// Verify directory was created
runtimeDir := filepath.Join(tmpDir, ".runtime")
if _, err := os.Stat(runtimeDir); err != nil {
t.Errorf("Save() should create .runtime directory: %v", err)
}
}
func TestStateManager_Load_InvalidJSON(t *testing.T) {
tmpDir := t.TempDir()
manager := NewStateManager[TestState](tmpDir, "test-state.json", func() *TestState {
return &TestState{Value: "default"}
})
// Write invalid JSON
statePath := manager.StateFile()
if err := os.MkdirAll(filepath.Dir(statePath), 0755); err != nil {
t.Fatalf("Failed to create directory: %v", err)
}
if err := os.WriteFile(statePath, []byte("invalid json"), 0644); err != nil {
t.Fatalf("Failed to write file: %v", err)
}
_, err := manager.Load()
if err == nil {
t.Error("Load() with invalid JSON should return error")
}
}
func TestState_String(t *testing.T) {
tests := []struct {
state State
want string
}{
{StateStopped, "stopped"},
{StateRunning, "running"},
{StatePaused, "paused"},
}
for _, tt := range tests {
if string(tt.state) != tt.want {
t.Errorf("State(%q) = %q, want %q", tt.state, string(tt.state), tt.want)
}
}
}
func TestStateManager_GenericType(t *testing.T) {
// Test that StateManager works with different types
type ComplexState struct {
Name string `json:"name"`
Values []int `json:"values"`
Enabled bool `json:"enabled"`
Nested struct {
X int `json:"x"`
} `json:"nested"`
}
tmpDir := t.TempDir()
manager := NewStateManager[ComplexState](tmpDir, "complex.json", func() *ComplexState {
return &ComplexState{Name: "default", Values: []int{}}
})
original := &ComplexState{
Name: "test",
Values: []int{1, 2, 3},
Enabled: true,
}
original.Nested.X = 42
if err := manager.Save(original); err != nil {
t.Fatalf("Save() error = %v", err)
}
loaded, err := manager.Load()
if err != nil {
t.Fatalf("Load() error = %v", err)
}
if loaded.Name != original.Name {
t.Errorf("Name = %q, want %q", loaded.Name, original.Name)
}
if len(loaded.Values) != len(original.Values) {
t.Errorf("Values length = %d, want %d", len(loaded.Values), len(original.Values))
}
if loaded.Enabled != original.Enabled {
t.Errorf("Enabled = %v, want %v", loaded.Enabled, original.Enabled)
}
if loaded.Nested.X != original.Nested.X {
t.Errorf("Nested.X = %d, want %d", loaded.Nested.X, original.Nested.X)
}
}
// TestState is a simple type for testing
type TestState struct {
Value string `json:"value"`
Count int `json:"count"`
}

View File

@@ -0,0 +1,93 @@
package cmd
import (
"errors"
"fmt"
"testing"
)
func TestSilentExitError_Error(t *testing.T) {
tests := []struct {
name string
code int
want string
}{
{"zero code", 0, "exit 0"},
{"success code", 1, "exit 1"},
{"error code", 2, "exit 2"},
{"custom code", 42, "exit 42"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
e := &SilentExitError{Code: tt.code}
got := e.Error()
if got != tt.want {
t.Errorf("SilentExitError.Error() = %q, want %q", got, tt.want)
}
})
}
}
func TestNewSilentExit(t *testing.T) {
tests := []struct {
code int
}{
{0},
{1},
{2},
{127},
}
for _, tt := range tests {
t.Run(fmt.Sprintf("code_%d", tt.code), func(t *testing.T) {
err := NewSilentExit(tt.code)
if err == nil {
t.Fatal("NewSilentExit should return non-nil")
}
if err.Code != tt.code {
t.Errorf("NewSilentExit(%d).Code = %d, want %d", tt.code, err.Code, tt.code)
}
})
}
}
func TestIsSilentExit(t *testing.T) {
tests := []struct {
name string
err error
wantCode int
wantIsSilent bool
}{
{"nil error", nil, 0, false},
{"silent exit code 0", NewSilentExit(0), 0, true},
{"silent exit code 1", NewSilentExit(1), 1, true},
{"silent exit code 2", NewSilentExit(2), 2, true},
{"other error", errors.New("some error"), 0, false},
// Note: wrapped errors require errors.As fix - see PR #462
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
code, isSilent := IsSilentExit(tt.err)
if isSilent != tt.wantIsSilent {
t.Errorf("IsSilentExit(%v) isSilent = %v, want %v", tt.err, isSilent, tt.wantIsSilent)
}
if code != tt.wantCode {
t.Errorf("IsSilentExit(%v) code = %d, want %d", tt.err, code, tt.wantCode)
}
})
}
}
func TestSilentExitError_Is(t *testing.T) {
// Test that SilentExitError works with errors.Is
err := NewSilentExit(1)
var target *SilentExitError
if !errors.As(err, &target) {
t.Error("errors.As should find SilentExitError")
}
if target.Code != 1 {
t.Errorf("errors.As extracted code = %d, want 1", target.Code)
}
}

100
internal/crew/types_test.go Normal file
View File

@@ -0,0 +1,100 @@
package crew
import (
"encoding/json"
"testing"
"time"
)
func TestCrewWorker_Summary(t *testing.T) {
now := time.Now()
worker := &CrewWorker{
Name: "test-worker",
Rig: "gastown",
ClonePath: "/path/to/clone",
Branch: "main",
CreatedAt: now,
UpdatedAt: now,
}
summary := worker.Summary()
if summary.Name != worker.Name {
t.Errorf("Summary.Name = %q, want %q", summary.Name, worker.Name)
}
if summary.Branch != worker.Branch {
t.Errorf("Summary.Branch = %q, want %q", summary.Branch, worker.Branch)
}
}
func TestCrewWorker_JSONMarshaling(t *testing.T) {
now := time.Now().Round(time.Second) // Round for JSON precision
worker := &CrewWorker{
Name: "test-worker",
Rig: "gastown",
ClonePath: "/path/to/clone",
Branch: "feature-branch",
CreatedAt: now,
UpdatedAt: now,
}
// Marshal to JSON
data, err := json.Marshal(worker)
if err != nil {
t.Fatalf("json.Marshal() error = %v", err)
}
// Unmarshal back
var unmarshaled CrewWorker
if err := json.Unmarshal(data, &unmarshaled); err != nil {
t.Fatalf("json.Unmarshal() error = %v", err)
}
if unmarshaled.Name != worker.Name {
t.Errorf("After round-trip: Name = %q, want %q", unmarshaled.Name, worker.Name)
}
if unmarshaled.Rig != worker.Rig {
t.Errorf("After round-trip: Rig = %q, want %q", unmarshaled.Rig, worker.Rig)
}
if unmarshaled.Branch != worker.Branch {
t.Errorf("After round-trip: Branch = %q, want %q", unmarshaled.Branch, worker.Branch)
}
}
func TestSummary_JSONMarshaling(t *testing.T) {
summary := Summary{
Name: "worker-1",
Branch: "main",
}
data, err := json.Marshal(summary)
if err != nil {
t.Fatalf("json.Marshal() error = %v", err)
}
var unmarshaled Summary
if err := json.Unmarshal(data, &unmarshaled); err != nil {
t.Fatalf("json.Unmarshal() error = %v", err)
}
if unmarshaled.Name != summary.Name {
t.Errorf("After round-trip: Name = %q, want %q", unmarshaled.Name, summary.Name)
}
if unmarshaled.Branch != summary.Branch {
t.Errorf("After round-trip: Branch = %q, want %q", unmarshaled.Branch, summary.Branch)
}
}
func TestCrewWorker_ZeroValues(t *testing.T) {
var worker CrewWorker
// Test zero value behavior
if worker.Name != "" {
t.Errorf("zero value CrewWorker.Name should be empty, got %q", worker.Name)
}
summary := worker.Summary()
if summary.Name != "" {
t.Errorf("Summary of zero value CrewWorker should have empty Name, got %q", summary.Name)
}
}

View File

@@ -0,0 +1,31 @@
package doctor
import (
"errors"
"testing"
)
func TestErrCannotFix(t *testing.T) {
// Test that ErrCannotFix is defined and has expected message
if ErrCannotFix == nil {
t.Fatal("ErrCannotFix should not be nil")
}
expected := "check does not support auto-fix"
if ErrCannotFix.Error() != expected {
t.Errorf("ErrCannotFix.Error() = %q, want %q", ErrCannotFix.Error(), expected)
}
}
func TestErrCannotFixIsError(t *testing.T) {
// Verify ErrCannotFix implements the error interface correctly
var err error = ErrCannotFix
if err == nil {
t.Fatal("ErrCannotFix should implement error interface")
}
// Test errors.Is compatibility
if !errors.Is(ErrCannotFix, ErrCannotFix) {
t.Error("errors.Is should return true for ErrCannotFix")
}
}

148
internal/dog/types_test.go Normal file
View File

@@ -0,0 +1,148 @@
package dog
import (
"encoding/json"
"testing"
"time"
)
func TestState_Constants(t *testing.T) {
tests := []struct {
name string
state State
want string
}{
{"idle state", StateIdle, "idle"},
{"working state", StateWorking, "working"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if string(tt.state) != tt.want {
t.Errorf("State constant = %q, want %q", tt.state, tt.want)
}
})
}
}
func TestDog_ZeroValues(t *testing.T) {
var dog Dog
// Test zero value behavior
if dog.Name != "" {
t.Errorf("zero value Dog.Name should be empty, got %q", dog.Name)
}
if dog.State != "" {
t.Errorf("zero value Dog.State should be empty, got %q", dog.State)
}
if dog.Worktrees == nil {
// Worktrees is a map, nil is expected for zero value
} else if len(dog.Worktrees) != 0 {
t.Errorf("zero value Dog.Worktrees should be empty, got %d items", len(dog.Worktrees))
}
}
func TestDogState_JSONMarshaling(t *testing.T) {
now := time.Now().Round(time.Second)
dogState := DogState{
Name: "test-dog",
State: StateWorking,
LastActive: now,
Work: "hq-abc123",
Worktrees: map[string]string{"gastown": "/path/to/worktree"},
CreatedAt: now,
UpdatedAt: now,
}
// Marshal to JSON
data, err := json.Marshal(dogState)
if err != nil {
t.Fatalf("json.Marshal() error = %v", err)
}
// Unmarshal back
var unmarshaled DogState
if err := json.Unmarshal(data, &unmarshaled); err != nil {
t.Fatalf("json.Unmarshal() error = %v", err)
}
if unmarshaled.Name != dogState.Name {
t.Errorf("After round-trip: Name = %q, want %q", unmarshaled.Name, dogState.Name)
}
if unmarshaled.State != dogState.State {
t.Errorf("After round-trip: State = %q, want %q", unmarshaled.State, dogState.State)
}
if unmarshaled.Work != dogState.Work {
t.Errorf("After round-trip: Work = %q, want %q", unmarshaled.Work, dogState.Work)
}
}
func TestDogState_OmitEmptyFields(t *testing.T) {
dogState := DogState{
Name: "test-dog",
State: StateIdle,
LastActive: time.Now(),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
// Work and Worktrees left empty to test omitempty
}
data, err := json.Marshal(dogState)
if err != nil {
t.Fatalf("json.Marshal() error = %v", err)
}
// Check that empty fields are omitted
var raw map[string]interface{}
if err := json.Unmarshal(data, &raw); err != nil {
t.Fatalf("json.Unmarshal() to map error = %v", err)
}
if _, exists := raw["work"]; exists {
t.Error("Field 'work' should be omitted when empty")
}
if _, exists := raw["worktrees"]; exists {
t.Error("Field 'worktrees' should be omitted when empty")
}
// Required fields should be present
requiredFields := []string{"name", "state", "last_active", "created_at", "updated_at"}
for _, field := range requiredFields {
if _, exists := raw[field]; !exists {
t.Errorf("Required field '%s' should be present", field)
}
}
}
func TestDogState_WithWorktrees(t *testing.T) {
dogState := DogState{
Name: "alpha",
State: StateWorking,
Worktrees: map[string]string{"gastown": "/path/1", "beads": "/path/2"},
}
data, err := json.Marshal(dogState)
if err != nil {
t.Fatalf("json.Marshal() error = %v", err)
}
var raw map[string]interface{}
if err := json.Unmarshal(data, &raw); err != nil {
t.Fatalf("json.Unmarshal() to map error = %v", err)
}
worktrees, exists := raw["worktrees"]
if !exists {
t.Fatal("Field 'worktrees' should be present when non-empty")
}
// Verify it's a map
worktreesMap, ok := worktrees.(map[string]interface{})
if !ok {
t.Fatal("worktrees should be a JSON object")
}
if len(worktreesMap) != 2 {
t.Errorf("worktrees should have 2 entries, got %d", len(worktreesMap))
}
}

193
internal/mail/bd_test.go Normal file
View File

@@ -0,0 +1,193 @@
package mail
import (
"errors"
"testing"
)
func TestBdError_Error(t *testing.T) {
tests := []struct {
name string
err *bdError
want string
}{
{
name: "stderr present",
err: &bdError{
Err: errors.New("some error"),
Stderr: "stderr output",
},
want: "stderr output",
},
{
name: "no stderr, has error",
err: &bdError{
Err: errors.New("some error"),
Stderr: "",
},
want: "some error",
},
{
name: "no stderr, no error",
err: &bdError{
Err: nil,
Stderr: "",
},
want: "unknown bd error",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tt.err.Error()
if got != tt.want {
t.Errorf("bdError.Error() = %q, want %q", got, tt.want)
}
})
}
}
func TestBdError_Unwrap(t *testing.T) {
originalErr := errors.New("original error")
bdErr := &bdError{
Err: originalErr,
Stderr: "stderr output",
}
unwrapped := bdErr.Unwrap()
if unwrapped != originalErr {
t.Errorf("bdError.Unwrap() = %v, want %v", unwrapped, originalErr)
}
}
func TestBdError_UnwrapNil(t *testing.T) {
bdErr := &bdError{
Err: nil,
Stderr: "",
}
unwrapped := bdErr.Unwrap()
if unwrapped != nil {
t.Errorf("bdError.Unwrap() with nil Err should return nil, got %v", unwrapped)
}
}
func TestBdError_ContainsError(t *testing.T) {
tests := []struct {
name string
err *bdError
substr string
contains bool
}{
{
name: "substring present",
err: &bdError{
Stderr: "error: bead not found",
},
substr: "bead not found",
contains: true,
},
{
name: "substring not present",
err: &bdError{
Stderr: "error: bead not found",
},
substr: "permission denied",
contains: false,
},
{
name: "empty stderr",
err: &bdError{
Stderr: "",
},
substr: "anything",
contains: false,
},
{
name: "case sensitive",
err: &bdError{
Stderr: "Error: Bead Not Found",
},
substr: "bead not found",
contains: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tt.err.ContainsError(tt.substr)
if got != tt.contains {
t.Errorf("bdError.ContainsError(%q) = %v, want %v", tt.substr, got, tt.contains)
}
})
}
}
func TestBdError_ContainsErrorPartialMatch(t *testing.T) {
err := &bdError{
Stderr: "fatal: invalid bead ID format: expected prefix-#id",
}
// Test partial matches
if !err.ContainsError("invalid bead ID") {
t.Error("Should contain partial substring")
}
if !err.ContainsError("fatal:") {
t.Error("Should contain prefix")
}
if !err.ContainsError("expected prefix") {
t.Error("Should contain suffix")
}
}
func TestBdError_ContainsErrorSpecialChars(t *testing.T) {
err := &bdError{
Stderr: "error: bead 'gt-123' not found (exit 1)",
}
if !err.ContainsError("'gt-123'") {
t.Error("Should handle quotes in substring")
}
if !err.ContainsError("(exit 1)") {
t.Error("Should handle parentheses in substring")
}
}
func TestBdError_ImplementsErrorInterface(t *testing.T) {
// Verify bdError implements error interface
var err error = &bdError{
Err: errors.New("test"),
Stderr: "test stderr",
}
_ = err.Error() // Should compile and not panic
}
func TestBdError_WithAllFields(t *testing.T) {
originalErr := errors.New("original error")
bdErr := &bdError{
Err: originalErr,
Stderr: "command failed: bead not found",
}
// Test Error() returns stderr
got := bdErr.Error()
want := "command failed: bead not found"
if got != want {
t.Errorf("bdError.Error() = %q, want %q", got, want)
}
// Test Unwrap() returns original error
unwrapped := bdErr.Unwrap()
if unwrapped != originalErr {
t.Errorf("bdError.Unwrap() = %v, want %v", unwrapped, originalErr)
}
// Test ContainsError works
if !bdErr.ContainsError("bead not found") {
t.Error("ContainsError should find substring in stderr")
}
if bdErr.ContainsError("not present") {
t.Error("ContainsError should return false for non-existent substring")
}
}

View File

@@ -0,0 +1,153 @@
package opencode
import (
"os"
"path/filepath"
"testing"
)
func TestEnsurePluginAt_EmptyParameters(t *testing.T) {
// Test that empty pluginDir or pluginFile returns nil
t.Run("empty pluginDir", func(t *testing.T) {
err := EnsurePluginAt("/tmp/work", "", "plugin.js")
if err != nil {
t.Errorf("EnsurePluginAt() with empty pluginDir should return nil, got %v", err)
}
})
t.Run("empty pluginFile", func(t *testing.T) {
err := EnsurePluginAt("/tmp/work", "plugins", "")
if err != nil {
t.Errorf("EnsurePluginAt() with empty pluginFile should return nil, got %v", err)
}
})
t.Run("both empty", func(t *testing.T) {
err := EnsurePluginAt("/tmp/work", "", "")
if err != nil {
t.Errorf("EnsurePluginAt() with both empty should return nil, got %v", err)
}
})
}
func TestEnsurePluginAt_FileExists(t *testing.T) {
// Create a temporary directory
tmpDir := t.TempDir()
// Create the plugin file first
pluginDir := "plugins"
pluginFile := "gastown.js"
pluginPath := filepath.Join(tmpDir, pluginDir, pluginFile)
if err := os.MkdirAll(filepath.Dir(pluginPath), 0755); err != nil {
t.Fatalf("Failed to create test directory: %v", err)
}
// Write a placeholder file
existingContent := []byte("// existing plugin")
if err := os.WriteFile(pluginPath, existingContent, 0644); err != nil {
t.Fatalf("Failed to create test file: %v", err)
}
// EnsurePluginAt should not overwrite existing file
err := EnsurePluginAt(tmpDir, pluginDir, pluginFile)
if err != nil {
t.Fatalf("EnsurePluginAt() error = %v", err)
}
// Verify file content is unchanged
content, err := os.ReadFile(pluginPath)
if err != nil {
t.Fatalf("Failed to read plugin file: %v", err)
}
if string(content) != string(existingContent) {
t.Error("EnsurePluginAt() should not overwrite existing file")
}
}
func TestEnsurePluginAt_CreatesFile(t *testing.T) {
// Create a temporary directory
tmpDir := t.TempDir()
pluginDir := "plugins"
pluginFile := "gastown.js"
pluginPath := filepath.Join(tmpDir, pluginDir, pluginFile)
// Ensure plugin doesn't exist
if _, err := os.Stat(pluginPath); err == nil {
t.Fatal("Plugin file should not exist yet")
}
// Create the plugin
err := EnsurePluginAt(tmpDir, pluginDir, pluginFile)
if err != nil {
t.Fatalf("EnsurePluginAt() error = %v", err)
}
// Verify file was created
info, err := os.Stat(pluginPath)
if err != nil {
t.Fatalf("Plugin file was not created: %v", err)
}
if info.IsDir() {
t.Error("Plugin path should be a file, not a directory")
}
// Verify file has content
content, err := os.ReadFile(pluginPath)
if err != nil {
t.Fatalf("Failed to read plugin file: %v", err)
}
if len(content) == 0 {
t.Error("Plugin file should have content")
}
}
func TestEnsurePluginAt_CreatesDirectory(t *testing.T) {
// Create a temporary directory
tmpDir := t.TempDir()
pluginDir := "nested/plugins/dir"
pluginFile := "gastown.js"
pluginPath := filepath.Join(tmpDir, pluginDir, pluginFile)
// Create the plugin
err := EnsurePluginAt(tmpDir, pluginDir, pluginFile)
if err != nil {
t.Fatalf("EnsurePluginAt() error = %v", err)
}
// Verify directory was created
dirInfo, err := os.Stat(filepath.Dir(pluginPath))
if err != nil {
t.Fatalf("Plugin directory was not created: %v", err)
}
if !dirInfo.IsDir() {
t.Error("Plugin parent path should be a directory")
}
}
func TestEnsurePluginAt_FilePermissions(t *testing.T) {
// Create a temporary directory
tmpDir := t.TempDir()
pluginDir := "plugins"
pluginFile := "gastown.js"
pluginPath := filepath.Join(tmpDir, pluginDir, pluginFile)
err := EnsurePluginAt(tmpDir, pluginDir, pluginFile)
if err != nil {
t.Fatalf("EnsurePluginAt() error = %v", err)
}
info, err := os.Stat(pluginPath)
if err != nil {
t.Fatalf("Failed to stat plugin file: %v", err)
}
// Check file mode is 0644 (rw-r--r--)
expectedMode := os.FileMode(0644)
if info.Mode() != expectedMode {
t.Errorf("Plugin file mode = %v, want %v", info.Mode(), expectedMode)
}
}

View File

@@ -0,0 +1,251 @@
package rig
import (
"os"
"path/filepath"
"testing"
)
func TestCopyOverlay_NoOverlayDirectory(t *testing.T) {
tmpDir := t.TempDir()
destDir := t.TempDir()
// No overlay directory exists
err := CopyOverlay(tmpDir, destDir)
if err != nil {
t.Errorf("CopyOverlay() with no overlay directory should return nil, got %v", err)
}
}
func TestCopyOverlay_CopiesFiles(t *testing.T) {
rigDir := t.TempDir()
destDir := t.TempDir()
// Create overlay directory with test files
overlayDir := filepath.Join(rigDir, ".runtime", "overlay")
if err := os.MkdirAll(overlayDir, 0755); err != nil {
t.Fatalf("Failed to create overlay dir: %v", err)
}
// Create test files
testFile1 := filepath.Join(overlayDir, "test1.txt")
testFile2 := filepath.Join(overlayDir, "test2.txt")
if err := os.WriteFile(testFile1, []byte("content1"), 0644); err != nil {
t.Fatalf("Failed to create test file: %v", err)
}
if err := os.WriteFile(testFile2, []byte("content2"), 0644); err != nil {
t.Fatalf("Failed to create test file: %v", err)
}
// Copy overlay
err := CopyOverlay(rigDir, destDir)
if err != nil {
t.Fatalf("CopyOverlay() error = %v", err)
}
// Verify files were copied
destFile1 := filepath.Join(destDir, "test1.txt")
destFile2 := filepath.Join(destDir, "test2.txt")
content1, err := os.ReadFile(destFile1)
if err != nil {
t.Errorf("File test1.txt was not copied: %v", err)
}
if string(content1) != "content1" {
t.Errorf("test1.txt content = %q, want %q", string(content1), "content1")
}
content2, err := os.ReadFile(destFile2)
if err != nil {
t.Errorf("File test2.txt was not copied: %v", err)
}
if string(content2) != "content2" {
t.Errorf("test2.txt content = %q, want %q", string(content2), "content2")
}
}
func TestCopyOverlay_PreservesPermissions(t *testing.T) {
rigDir := t.TempDir()
destDir := t.TempDir()
// Create overlay directory with a file
overlayDir := filepath.Join(rigDir, ".runtime", "overlay")
if err := os.MkdirAll(overlayDir, 0755); err != nil {
t.Fatalf("Failed to create overlay dir: %v", err)
}
testFile := filepath.Join(overlayDir, "test.txt")
if err := os.WriteFile(testFile, []byte("content"), 0755); err != nil {
t.Fatalf("Failed to create test file: %v", err)
}
// Copy overlay
err := CopyOverlay(rigDir, destDir)
if err != nil {
t.Fatalf("CopyOverlay() error = %v", err)
}
// Verify permissions were preserved
srcInfo, _ := os.Stat(testFile)
destInfo, err := os.Stat(filepath.Join(destDir, "test.txt"))
if err != nil {
t.Fatalf("Failed to stat destination file: %v", err)
}
if srcInfo.Mode().Perm() != destInfo.Mode().Perm() {
t.Errorf("Permissions not preserved: src=%v, dest=%v", srcInfo.Mode(), destInfo.Mode())
}
}
func TestCopyOverlay_SkipsSubdirectories(t *testing.T) {
rigDir := t.TempDir()
destDir := t.TempDir()
// Create overlay directory with a subdirectory
overlayDir := filepath.Join(rigDir, ".runtime", "overlay")
if err := os.MkdirAll(overlayDir, 0755); err != nil {
t.Fatalf("Failed to create overlay dir: %v", err)
}
// Create a subdirectory
subDir := filepath.Join(overlayDir, "subdir")
if err := os.MkdirAll(subDir, 0755); err != nil {
t.Fatalf("Failed to create subdirectory: %v", err)
}
// Create a file in the overlay root
testFile := filepath.Join(overlayDir, "test.txt")
if err := os.WriteFile(testFile, []byte("content"), 0644); err != nil {
t.Fatalf("Failed to create test file: %v", err)
}
// Create a file in the subdirectory
subFile := filepath.Join(subDir, "sub.txt")
if err := os.WriteFile(subFile, []byte("subcontent"), 0644); err != nil {
t.Fatalf("Failed to create sub file: %v", err)
}
// Copy overlay
err := CopyOverlay(rigDir, destDir)
if err != nil {
t.Fatalf("CopyOverlay() error = %v", err)
}
// Verify root file was copied
if _, err := os.Stat(filepath.Join(destDir, "test.txt")); err != nil {
t.Error("Root file should be copied")
}
// Verify subdirectory was NOT copied
if _, err := os.Stat(filepath.Join(destDir, "subdir")); err == nil {
t.Error("Subdirectory should not be copied")
}
if _, err := os.Stat(filepath.Join(destDir, "subdir", "sub.txt")); err == nil {
t.Error("File in subdirectory should not be copied")
}
}
func TestCopyOverlay_EmptyOverlay(t *testing.T) {
rigDir := t.TempDir()
destDir := t.TempDir()
// Create empty overlay directory
overlayDir := filepath.Join(rigDir, ".runtime", "overlay")
if err := os.MkdirAll(overlayDir, 0755); err != nil {
t.Fatalf("Failed to create overlay dir: %v", err)
}
// Copy overlay
err := CopyOverlay(rigDir, destDir)
if err != nil {
t.Fatalf("CopyOverlay() error = %v", err)
}
// Should succeed without errors
}
func TestCopyOverlay_OverwritesExisting(t *testing.T) {
rigDir := t.TempDir()
destDir := t.TempDir()
// Create overlay directory with test file
overlayDir := filepath.Join(rigDir, ".runtime", "overlay")
if err := os.MkdirAll(overlayDir, 0755); err != nil {
t.Fatalf("Failed to create overlay dir: %v", err)
}
testFile := filepath.Join(overlayDir, "test.txt")
if err := os.WriteFile(testFile, []byte("new content"), 0644); err != nil {
t.Fatalf("Failed to create test file: %v", err)
}
// Create existing file in destination with different content
destFile := filepath.Join(destDir, "test.txt")
if err := os.WriteFile(destFile, []byte("old content"), 0644); err != nil {
t.Fatalf("Failed to create dest file: %v", err)
}
// Copy overlay
err := CopyOverlay(rigDir, destDir)
if err != nil {
t.Fatalf("CopyOverlay() error = %v", err)
}
// Verify file was overwritten
content, err := os.ReadFile(destFile)
if err != nil {
t.Fatalf("Failed to read dest file: %v", err)
}
if string(content) != "new content" {
t.Errorf("File content = %q, want %q", string(content), "new content")
}
}
func TestCopyFilePreserveMode(t *testing.T) {
tmpDir := t.TempDir()
// Create source file
srcFile := filepath.Join(tmpDir, "src.txt")
if err := os.WriteFile(srcFile, []byte("test content"), 0644); err != nil {
t.Fatalf("Failed to create src file: %v", err)
}
// Copy file
dstFile := filepath.Join(tmpDir, "dst.txt")
err := copyFilePreserveMode(srcFile, dstFile)
if err != nil {
t.Fatalf("copyFilePreserveMode() error = %v", err)
}
// Verify content
content, err := os.ReadFile(dstFile)
if err != nil {
t.Errorf("Failed to read dst file: %v", err)
}
if string(content) != "test content" {
t.Errorf("Content = %q, want %q", string(content), "test content")
}
// Verify permissions
srcInfo, _ := os.Stat(srcFile)
dstInfo, err := os.Stat(dstFile)
if err != nil {
t.Fatalf("Failed to stat dst file: %v", err)
}
if srcInfo.Mode().Perm() != dstInfo.Mode().Perm() {
t.Errorf("Permissions not preserved: src=%v, dest=%v", srcInfo.Mode(), dstInfo.Mode())
}
}
func TestCopyFilePreserveMode_NonexistentSource(t *testing.T) {
tmpDir := t.TempDir()
srcFile := filepath.Join(tmpDir, "nonexistent.txt")
dstFile := filepath.Join(tmpDir, "dst.txt")
err := copyFilePreserveMode(srcFile, dstFile)
if err == nil {
t.Error("copyFilePreserveMode() with nonexistent source should return error")
}
}

View File

@@ -0,0 +1,307 @@
package runtime
import (
"os"
"testing"
"time"
"github.com/steveyegge/gastown/internal/config"
)
func TestSessionIDFromEnv_Default(t *testing.T) {
// Clear all environment variables
oldGSEnv := os.Getenv("GT_SESSION_ID_ENV")
oldClaudeID := os.Getenv("CLAUDE_SESSION_ID")
defer func() {
if oldGSEnv != "" {
os.Setenv("GT_SESSION_ID_ENV", oldGSEnv)
} else {
os.Unsetenv("GT_SESSION_ID_ENV")
}
if oldClaudeID != "" {
os.Setenv("CLAUDE_SESSION_ID", oldClaudeID)
} else {
os.Unsetenv("CLAUDE_SESSION_ID")
}
}()
os.Unsetenv("GT_SESSION_ID_ENV")
os.Unsetenv("CLAUDE_SESSION_ID")
result := SessionIDFromEnv()
if result != "" {
t.Errorf("SessionIDFromEnv() with no env vars should return empty, got %q", result)
}
}
func TestSessionIDFromEnv_ClaudeSessionID(t *testing.T) {
oldGSEnv := os.Getenv("GT_SESSION_ID_ENV")
oldClaudeID := os.Getenv("CLAUDE_SESSION_ID")
defer func() {
if oldGSEnv != "" {
os.Setenv("GT_SESSION_ID_ENV", oldGSEnv)
} else {
os.Unsetenv("GT_SESSION_ID_ENV")
}
if oldClaudeID != "" {
os.Setenv("CLAUDE_SESSION_ID", oldClaudeID)
} else {
os.Unsetenv("CLAUDE_SESSION_ID")
}
}()
os.Unsetenv("GT_SESSION_ID_ENV")
os.Setenv("CLAUDE_SESSION_ID", "test-session-123")
result := SessionIDFromEnv()
if result != "test-session-123" {
t.Errorf("SessionIDFromEnv() = %q, want %q", result, "test-session-123")
}
}
func TestSessionIDFromEnv_CustomEnvVar(t *testing.T) {
oldGSEnv := os.Getenv("GT_SESSION_ID_ENV")
oldCustomID := os.Getenv("CUSTOM_SESSION_ID")
oldClaudeID := os.Getenv("CLAUDE_SESSION_ID")
defer func() {
if oldGSEnv != "" {
os.Setenv("GT_SESSION_ID_ENV", oldGSEnv)
} else {
os.Unsetenv("GT_SESSION_ID_ENV")
}
if oldCustomID != "" {
os.Setenv("CUSTOM_SESSION_ID", oldCustomID)
} else {
os.Unsetenv("CUSTOM_SESSION_ID")
}
if oldClaudeID != "" {
os.Setenv("CLAUDE_SESSION_ID", oldClaudeID)
} else {
os.Unsetenv("CLAUDE_SESSION_ID")
}
}()
os.Setenv("GT_SESSION_ID_ENV", "CUSTOM_SESSION_ID")
os.Setenv("CUSTOM_SESSION_ID", "custom-session-456")
os.Setenv("CLAUDE_SESSION_ID", "claude-session-789")
result := SessionIDFromEnv()
if result != "custom-session-456" {
t.Errorf("SessionIDFromEnv() with custom env = %q, want %q", result, "custom-session-456")
}
}
func TestSleepForReadyDelay_NilConfig(t *testing.T) {
// Should not panic with nil config
SleepForReadyDelay(nil)
}
func TestSleepForReadyDelay_ZeroDelay(t *testing.T) {
rc := &config.RuntimeConfig{
Tmux: &config.RuntimeTmuxConfig{
ReadyDelayMs: 0,
},
}
start := time.Now()
SleepForReadyDelay(rc)
elapsed := time.Since(start)
// Should return immediately
if elapsed > 100*time.Millisecond {
t.Errorf("SleepForReadyDelay() with zero delay took too long: %v", elapsed)
}
}
func TestSleepForReadyDelay_WithDelay(t *testing.T) {
rc := &config.RuntimeConfig{
Tmux: &config.RuntimeTmuxConfig{
ReadyDelayMs: 10, // 10ms delay
},
}
start := time.Now()
SleepForReadyDelay(rc)
elapsed := time.Since(start)
// Should sleep for at least 10ms
if elapsed < 10*time.Millisecond {
t.Errorf("SleepForReadyDelay() should sleep for at least 10ms, took %v", elapsed)
}
// But not too long
if elapsed > 50*time.Millisecond {
t.Errorf("SleepForReadyDelay() slept too long: %v", elapsed)
}
}
func TestSleepForReadyDelay_NilTmuxConfig(t *testing.T) {
rc := &config.RuntimeConfig{
Tmux: nil,
}
start := time.Now()
SleepForReadyDelay(rc)
elapsed := time.Since(start)
// Should return immediately
if elapsed > 100*time.Millisecond {
t.Errorf("SleepForReadyDelay() with nil Tmux config took too long: %v", elapsed)
}
}
func TestStartupFallbackCommands_NoHooks(t *testing.T) {
rc := &config.RuntimeConfig{
Hooks: &config.RuntimeHooksConfig{
Provider: "none",
},
}
commands := StartupFallbackCommands("polecat", rc)
if commands == nil {
t.Error("StartupFallbackCommands() with no hooks should return commands")
}
if len(commands) == 0 {
t.Error("StartupFallbackCommands() should return at least one command")
}
}
func TestStartupFallbackCommands_WithHooks(t *testing.T) {
rc := &config.RuntimeConfig{
Hooks: &config.RuntimeHooksConfig{
Provider: "claude",
},
}
commands := StartupFallbackCommands("polecat", rc)
if commands != nil {
t.Error("StartupFallbackCommands() with hooks provider should return nil")
}
}
func TestStartupFallbackCommands_NilConfig(t *testing.T) {
// Nil config defaults to claude provider, which has hooks
// So it returns nil (no fallback commands needed)
commands := StartupFallbackCommands("polecat", nil)
if commands != nil {
t.Error("StartupFallbackCommands() with nil config should return nil (defaults to claude with hooks)")
}
}
func TestStartupFallbackCommands_AutonomousRole(t *testing.T) {
rc := &config.RuntimeConfig{
Hooks: &config.RuntimeHooksConfig{
Provider: "none",
},
}
autonomousRoles := []string{"polecat", "witness", "refinery", "deacon"}
for _, role := range autonomousRoles {
t.Run(role, func(t *testing.T) {
commands := StartupFallbackCommands(role, rc)
if commands == nil || len(commands) == 0 {
t.Error("StartupFallbackCommands() should return commands for autonomous role")
}
// Should contain mail check
found := false
for _, cmd := range commands {
if contains(cmd, "mail check --inject") {
found = true
break
}
}
if !found {
t.Errorf("Commands for %s should contain mail check --inject", role)
}
})
}
}
func TestStartupFallbackCommands_NonAutonomousRole(t *testing.T) {
rc := &config.RuntimeConfig{
Hooks: &config.RuntimeHooksConfig{
Provider: "none",
},
}
nonAutonomousRoles := []string{"mayor", "crew", "keeper"}
for _, role := range nonAutonomousRoles {
t.Run(role, func(t *testing.T) {
commands := StartupFallbackCommands(role, rc)
if commands == nil || len(commands) == 0 {
t.Error("StartupFallbackCommands() should return commands for non-autonomous role")
}
// Should NOT contain mail check
for _, cmd := range commands {
if contains(cmd, "mail check --inject") {
t.Errorf("Commands for %s should NOT contain mail check --inject", role)
}
}
})
}
}
func TestStartupFallbackCommands_RoleCasing(t *testing.T) {
rc := &config.RuntimeConfig{
Hooks: &config.RuntimeHooksConfig{
Provider: "none",
},
}
// Role should be lowercased internally
commands := StartupFallbackCommands("POLECAT", rc)
if commands == nil {
t.Error("StartupFallbackCommands() should handle uppercase role")
}
}
func TestEnsureSettingsForRole_NilConfig(t *testing.T) {
// Should not panic with nil config
err := EnsureSettingsForRole("/tmp/test", "polecat", nil)
if err != nil {
t.Errorf("EnsureSettingsForRole() with nil config should not error, got %v", err)
}
}
func TestEnsureSettingsForRole_NilHooks(t *testing.T) {
rc := &config.RuntimeConfig{
Hooks: nil,
}
err := EnsureSettingsForRole("/tmp/test", "polecat", rc)
if err != nil {
t.Errorf("EnsureSettingsForRole() with nil hooks should not error, got %v", err)
}
}
func TestEnsureSettingsForRole_UnknownProvider(t *testing.T) {
rc := &config.RuntimeConfig{
Hooks: &config.RuntimeHooksConfig{
Provider: "unknown",
},
}
err := EnsureSettingsForRole("/tmp/test", "polecat", rc)
if err != nil {
t.Errorf("EnsureSettingsForRole() with unknown provider should not error, got %v", err)
}
}
// Helper function
func contains(s, substr string) bool {
return len(s) >= len(substr) && findSubstring(s, substr)
}
func findSubstring(s, substr string) bool {
for i := 0; i <= len(s)-len(substr); i++ {
match := true
for j := 0; j < len(substr); j++ {
if s[i+j] != substr[j] {
match = false
break
}
}
if match {
return true
}
}
return false
}

View File

@@ -0,0 +1,93 @@
package session
import (
"testing"
)
func TestTownSessions(t *testing.T) {
sessions := TownSessions()
if len(sessions) != 3 {
t.Errorf("TownSessions() returned %d sessions, want 3", len(sessions))
}
// Verify order is correct (Mayor, Boot, Deacon)
expectedOrder := []string{"Mayor", "Boot", "Deacon"}
for i, s := range sessions {
if s.Name != expectedOrder[i] {
t.Errorf("TownSessions()[%d].Name = %q, want %q", i, s.Name, expectedOrder[i])
}
if s.SessionID == "" {
t.Errorf("TownSessions()[%d].SessionID should not be empty", i)
}
}
}
func TestTownSessions_SessionIDFormats(t *testing.T) {
sessions := TownSessions()
for _, s := range sessions {
if s.SessionID == "" {
t.Errorf("TownSession %q has empty SessionID", s.Name)
}
// Session IDs should follow a pattern
if len(s.SessionID) < 4 {
t.Errorf("TownSession %q SessionID %q is too short", s.Name, s.SessionID)
}
}
}
func TestTownSession_StructFields(t *testing.T) {
ts := TownSession{
Name: "Test",
SessionID: "test-session",
}
if ts.Name != "Test" {
t.Errorf("TownSession.Name = %q, want %q", ts.Name, "Test")
}
if ts.SessionID != "test-session" {
t.Errorf("TownSession.SessionID = %q, want %q", ts.SessionID, "test-session")
}
}
func TestTownSession_CanBeCreated(t *testing.T) {
// Test that TownSession can be created with any values
tests := []struct {
name string
sessionID string
}{
{"Mayor", "hq-mayor"},
{"Boot", "hq-boot"},
{"Custom", "custom-session"},
}
for _, tt := range tests {
ts := TownSession{
Name: tt.name,
SessionID: tt.sessionID,
}
if ts.Name != tt.name {
t.Errorf("TownSession.Name = %q, want %q", ts.Name, tt.name)
}
if ts.SessionID != tt.sessionID {
t.Errorf("TownSession.SessionID = %q, want %q", ts.SessionID, tt.sessionID)
}
}
}
func TestTownSession_ShutdownOrder(t *testing.T) {
// Verify that shutdown order is Mayor -> Boot -> Deacon
// This is critical because Boot monitors Deacon
sessions := TownSessions()
if sessions[0].Name != "Mayor" {
t.Errorf("First session should be Mayor, got %q", sessions[0].Name)
}
if sessions[1].Name != "Boot" {
t.Errorf("Second session should be Boot, got %q", sessions[1].Name)
}
if sessions[2].Name != "Deacon" {
t.Errorf("Third session should be Deacon, got %q", sessions[2].Name)
}
}

View File

@@ -0,0 +1,169 @@
package style
import (
"bytes"
"fmt"
"io"
"os"
"testing"
)
func TestStyleVariables(t *testing.T) {
// Test that all style variables render non-empty output
tests := []struct {
name string
render func(...string) string
}{
{"Success", Success.Render},
{"Warning", Warning.Render},
{"Error", Error.Render},
{"Info", Info.Render},
{"Dim", Dim.Render},
{"Bold", Bold.Render},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.render == nil {
t.Errorf("Style variable %s should not be nil", tt.name)
}
// Test that Render works
result := tt.render("test")
if result == "" {
t.Errorf("Style %s.Render() should not return empty string", tt.name)
}
})
}
}
func TestPrefixVariables(t *testing.T) {
// Test that all prefix variables are non-empty
tests := []struct {
name string
prefix string
}{
{"SuccessPrefix", SuccessPrefix},
{"WarningPrefix", WarningPrefix},
{"ErrorPrefix", ErrorPrefix},
{"ArrowPrefix", ArrowPrefix},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.prefix == "" {
t.Errorf("Prefix variable %s should not be empty", tt.name)
}
})
}
}
func TestPrintWarning(t *testing.T) {
// Capture stdout
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
PrintWarning("test warning: %s", "value")
w.Close()
os.Stdout = oldStdout
// Read captured output
var buf bytes.Buffer
io.Copy(&buf, r)
output := buf.String()
if output == "" {
t.Error("PrintWarning() should produce output")
}
// Check that warning message is present
if !bytes.Contains(buf.Bytes(), []byte("test warning: value")) {
t.Error("PrintWarning() output should contain the warning message")
}
}
func TestPrintWarning_NoFormatArgs(t *testing.T) {
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
PrintWarning("simple warning")
w.Close()
os.Stdout = oldStdout
var buf bytes.Buffer
io.Copy(&buf, r)
output := buf.String()
if output == "" {
t.Error("PrintWarning() should produce output")
}
if !bytes.Contains(buf.Bytes(), []byte("simple warning")) {
t.Error("PrintWarning() output should contain the message")
}
}
func TestStyles_RenderConsistently(t *testing.T) {
// Test that styles consistently render non-empty output
testText := "test message"
styles := map[string]func(...string) string{
"Success": Success.Render,
"Warning": Warning.Render,
"Error": Error.Render,
"Info": Info.Render,
"Dim": Dim.Render,
"Bold": Bold.Render,
}
for name, renderFunc := range styles {
t.Run(name, func(t *testing.T) {
result := renderFunc(testText)
if result == "" {
t.Errorf("Style %s.Render() should not return empty string", name)
}
// Result should be different from input (has styling codes)
// except possibly for some edge cases
})
}
}
func TestMultiplePrintWarning(t *testing.T) {
// Test that multiple warnings can be printed
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
for i := 0; i < 3; i++ {
PrintWarning("warning %d", i)
}
w.Close()
os.Stdout = oldStdout
var buf bytes.Buffer
io.Copy(&buf, r)
_ = buf.String() // ensure buffer is read
// Should have 3 lines
lineCount := 0
for _, b := range buf.Bytes() {
if b == '\n' {
lineCount++
}
}
if lineCount != 3 {
t.Errorf("Expected 3 lines of output, got %d", lineCount)
}
}
func ExamplePrintWarning() {
// This example demonstrates PrintWarning usage
fmt.Print("Example output:\n")
PrintWarning("This is a warning message")
PrintWarning("Warning with value: %d", 42)
}

View File

@@ -0,0 +1,234 @@
package ui
import (
"os"
"strings"
"testing"
)
func TestRenderMarkdown_AgentMode(t *testing.T) {
oldAgentMode := os.Getenv("GT_AGENT_MODE")
defer func() {
if oldAgentMode != "" {
os.Setenv("GT_AGENT_MODE", oldAgentMode)
} else {
os.Unsetenv("GT_AGENT_MODE")
}
}()
os.Setenv("GT_AGENT_MODE", "1")
markdown := "# Test Header\n\nSome content"
result := RenderMarkdown(markdown)
if result != markdown {
t.Errorf("RenderMarkdown() in agent mode should return raw markdown, got %q", result)
}
}
func TestRenderMarkdown_SimpleText(t *testing.T) {
oldAgentMode := os.Getenv("GT_AGENT_MODE")
oldNoColor := os.Getenv("NO_COLOR")
defer func() {
if oldAgentMode != "" {
os.Setenv("GT_AGENT_MODE", oldAgentMode)
} else {
os.Unsetenv("GT_AGENT_MODE")
}
if oldNoColor != "" {
os.Setenv("NO_COLOR", oldNoColor)
} else {
os.Unsetenv("NO_COLOR")
}
}()
os.Unsetenv("GT_AGENT_MODE")
os.Setenv("NO_COLOR", "1") // Disable glamour rendering
markdown := "Simple text without formatting"
result := RenderMarkdown(markdown)
// When color is disabled, should return raw markdown
if result != markdown {
t.Errorf("RenderMarkdown() with color disabled should return raw markdown, got %q", result)
}
}
func TestRenderMarkdown_EmptyString(t *testing.T) {
oldAgentMode := os.Getenv("GT_AGENT_MODE")
oldNoColor := os.Getenv("NO_COLOR")
defer func() {
if oldAgentMode != "" {
os.Setenv("GT_AGENT_MODE", oldAgentMode)
} else {
os.Unsetenv("GT_AGENT_MODE")
}
if oldNoColor != "" {
os.Setenv("NO_COLOR", oldNoColor)
} else {
os.Unsetenv("NO_COLOR")
}
}()
os.Unsetenv("GT_AGENT_MODE")
os.Setenv("NO_COLOR", "1")
result := RenderMarkdown("")
if result != "" {
t.Errorf("RenderMarkdown() with empty string should return empty, got %q", result)
}
}
func TestRenderMarkdown_GracefulDegradation(t *testing.T) {
oldAgentMode := os.Getenv("GT_AGENT_MODE")
oldNoColor := os.Getenv("NO_COLOR")
defer func() {
if oldAgentMode != "" {
os.Setenv("GT_AGENT_MODE", oldAgentMode)
} else {
os.Unsetenv("GT_AGENT_MODE")
}
if oldNoColor != "" {
os.Setenv("NO_COLOR", oldNoColor)
} else {
os.Unsetenv("NO_COLOR")
}
}()
os.Unsetenv("GT_AGENT_MODE")
os.Setenv("NO_COLOR", "1")
// Test that function doesn't panic and always returns something
markdown := "# Test\n\nContent with **bold** and *italic*"
result := RenderMarkdown(markdown)
if result == "" {
t.Error("RenderMarkdown() should never return empty string for non-empty input")
}
// With NO_COLOR, should return raw markdown
if !strings.Contains(result, "bold") {
t.Error("RenderMarkdown() should contain original content")
}
}
func TestGetTerminalWidth(t *testing.T) {
// This function is unexported, but we can test it indirectly
// The function should return a reasonable width
// Since we can't call it directly, we verify RenderMarkdown doesn't panic
oldAgentMode := os.Getenv("GT_AGENT_MODE")
oldNoColor := os.Getenv("NO_COLOR")
defer func() {
if oldAgentMode != "" {
os.Setenv("GT_AGENT_MODE", oldAgentMode)
} else {
os.Unsetenv("GT_AGENT_MODE")
}
if oldNoColor != "" {
os.Setenv("NO_COLOR", oldNoColor)
} else {
os.Unsetenv("NO_COLOR")
}
}()
os.Unsetenv("GT_AGENT_MODE")
os.Setenv("NO_COLOR", "1")
// This should not panic even if terminal width detection fails
markdown := strings.Repeat("word ", 1000) // Long content
result := RenderMarkdown(markdown)
if result == "" {
t.Error("RenderMarkdown() should handle long content")
}
}
func TestRenderMarkdown_Newlines(t *testing.T) {
oldAgentMode := os.Getenv("GT_AGENT_MODE")
oldNoColor := os.Getenv("NO_COLOR")
defer func() {
if oldAgentMode != "" {
os.Setenv("GT_AGENT_MODE", oldAgentMode)
} else {
os.Unsetenv("GT_AGENT_MODE")
}
if oldNoColor != "" {
os.Setenv("NO_COLOR", oldNoColor)
} else {
os.Unsetenv("NO_COLOR")
}
}()
os.Unsetenv("GT_AGENT_MODE")
os.Setenv("NO_COLOR", "1")
markdown := "Line 1\n\nLine 2\n\nLine 3"
result := RenderMarkdown(markdown)
// With color disabled, newlines should be preserved
if !strings.Contains(result, "Line 1") {
t.Error("RenderMarkdown() should preserve first line")
}
if !strings.Contains(result, "Line 2") {
t.Error("RenderMarkdown() should preserve second line")
}
if !strings.Contains(result, "Line 3") {
t.Error("RenderMarkdown() should preserve third line")
}
}
func TestRenderMarkdown_CodeBlocks(t *testing.T) {
oldAgentMode := os.Getenv("GT_AGENT_MODE")
oldNoColor := os.Getenv("NO_COLOR")
defer func() {
if oldAgentMode != "" {
os.Setenv("GT_AGENT_MODE", oldAgentMode)
} else {
os.Unsetenv("GT_AGENT_MODE")
}
if oldNoColor != "" {
os.Setenv("NO_COLOR", oldNoColor)
} else {
os.Unsetenv("NO_COLOR")
}
}()
os.Unsetenv("GT_AGENT_MODE")
os.Setenv("NO_COLOR", "1")
markdown := "```go\nfunc main() {}\n```"
result := RenderMarkdown(markdown)
// Should contain the code
if !strings.Contains(result, "func main") {
t.Error("RenderMarkdown() should preserve code block content")
}
}
func TestRenderMarkdown_Links(t *testing.T) {
oldAgentMode := os.Getenv("GT_AGENT_MODE")
oldNoColor := os.Getenv("NO_COLOR")
defer func() {
if oldAgentMode != "" {
os.Setenv("GT_AGENT_MODE", oldAgentMode)
} else {
os.Unsetenv("GT_AGENT_MODE")
}
if oldNoColor != "" {
os.Setenv("NO_COLOR", oldNoColor)
} else {
os.Unsetenv("NO_COLOR")
}
}()
os.Unsetenv("GT_AGENT_MODE")
os.Setenv("NO_COLOR", "1")
markdown := "[link text](https://example.com)"
result := RenderMarkdown(markdown)
// Should contain the link text or URL
if !strings.Contains(result, "link text") && !strings.Contains(result, "example.com") {
t.Error("RenderMarkdown() should preserve link information")
}
}

View File

@@ -0,0 +1,247 @@
package ui
import (
"os"
"testing"
)
func TestIsTerminal(t *testing.T) {
// This test verifies the function doesn't panic
// The actual result depends on the test environment
result := IsTerminal()
// In test environment, this is usually false
// The important thing is it doesn't crash and returns a bool
var _ bool = result
}
func TestShouldUseColor_Default(t *testing.T) {
// Clean environment for this test
oldNoColor := os.Getenv("NO_COLOR")
oldClicolor := os.Getenv("CLICOLOR")
oldClicolorForce := os.Getenv("CLICOLOR_FORCE")
defer func() {
if oldNoColor != "" {
os.Setenv("NO_COLOR", oldNoColor)
} else {
os.Unsetenv("NO_COLOR")
}
if oldClicolor != "" {
os.Setenv("CLICOLOR", oldClicolor)
} else {
os.Unsetenv("CLICOLOR")
}
if oldClicolorForce != "" {
os.Setenv("CLICOLOR_FORCE", oldClicolorForce)
} else {
os.Unsetenv("CLICOLOR_FORCE")
}
}()
os.Unsetenv("NO_COLOR")
os.Unsetenv("CLICOLOR")
os.Unsetenv("CLICOLOR_FORCE")
result := ShouldUseColor()
// In non-TTY test environment, should be false
_ = result
}
func TestShouldUseColor_NO_COLOR(t *testing.T) {
oldNoColor := os.Getenv("NO_COLOR")
defer func() {
if oldNoColor != "" {
os.Setenv("NO_COLOR", oldNoColor)
} else {
os.Unsetenv("NO_COLOR")
}
}()
os.Setenv("NO_COLOR", "1")
if ShouldUseColor() {
t.Error("ShouldUseColor() should return false when NO_COLOR is set")
}
}
func TestShouldUseColor_NO_COLOR_AnyValue(t *testing.T) {
oldNoColor := os.Getenv("NO_COLOR")
defer func() {
if oldNoColor != "" {
os.Setenv("NO_COLOR", oldNoColor)
} else {
os.Unsetenv("NO_COLOR")
}
}()
// NO_COLOR with any value (even "0") should disable color
os.Setenv("NO_COLOR", "0")
if ShouldUseColor() {
t.Error("ShouldUseColor() should return false when NO_COLOR is set to any value")
}
}
func TestShouldUseColor_CLICOLOR_0(t *testing.T) {
oldNoColor := os.Getenv("NO_COLOR")
oldClicolor := os.Getenv("CLICOLOR")
defer func() {
if oldNoColor != "" {
os.Setenv("NO_COLOR", oldNoColor)
} else {
os.Unsetenv("NO_COLOR")
}
if oldClicolor != "" {
os.Setenv("CLICOLOR", oldClicolor)
} else {
os.Unsetenv("CLICOLOR")
}
}()
os.Unsetenv("NO_COLOR")
os.Setenv("CLICOLOR", "0")
if ShouldUseColor() {
t.Error("ShouldUseColor() should return false when CLICOLOR=0")
}
}
func TestShouldUseColor_CLICOLOR_FORCE(t *testing.T) {
oldNoColor := os.Getenv("NO_COLOR")
oldClicolorForce := os.Getenv("CLICOLOR_FORCE")
defer func() {
if oldNoColor != "" {
os.Setenv("NO_COLOR", oldNoColor)
} else {
os.Unsetenv("NO_COLOR")
}
if oldClicolorForce != "" {
os.Setenv("CLICOLOR_FORCE", oldClicolorForce)
} else {
os.Unsetenv("CLICOLOR_FORCE")
}
}()
os.Unsetenv("NO_COLOR")
os.Setenv("CLICOLOR_FORCE", "1")
if !ShouldUseColor() {
t.Error("ShouldUseColor() should return true when CLICOLOR_FORCE is set")
}
}
func TestShouldUseEmoji_Default(t *testing.T) {
oldNoEmoji := os.Getenv("GT_NO_EMOJI")
defer func() {
if oldNoEmoji != "" {
os.Setenv("GT_NO_EMOJI", oldNoEmoji)
} else {
os.Unsetenv("GT_NO_EMOJI")
}
}()
os.Unsetenv("GT_NO_EMOJI")
result := ShouldUseEmoji()
_ = result // Result depends on test environment
}
func TestShouldUseEmoji_GT_NO_EMOJI(t *testing.T) {
oldNoEmoji := os.Getenv("GT_NO_EMOJI")
defer func() {
if oldNoEmoji != "" {
os.Setenv("GT_NO_EMOJI", oldNoEmoji)
} else {
os.Unsetenv("GT_NO_EMOJI")
}
}()
os.Setenv("GT_NO_EMOJI", "1")
if ShouldUseEmoji() {
t.Error("ShouldUseEmoji() should return false when GT_NO_EMOJI is set")
}
}
func TestIsAgentMode_Default(t *testing.T) {
oldAgentMode := os.Getenv("GT_AGENT_MODE")
oldClaudeCode := os.Getenv("CLAUDE_CODE")
defer func() {
if oldAgentMode != "" {
os.Setenv("GT_AGENT_MODE", oldAgentMode)
} else {
os.Unsetenv("GT_AGENT_MODE")
}
if oldClaudeCode != "" {
os.Setenv("CLAUDE_CODE", oldClaudeCode)
} else {
os.Unsetenv("CLAUDE_CODE")
}
}()
os.Unsetenv("GT_AGENT_MODE")
os.Unsetenv("CLAUDE_CODE")
if IsAgentMode() {
t.Error("IsAgentMode() should return false by default")
}
}
func TestIsAgentMode_GT_AGENT_MODE(t *testing.T) {
oldAgentMode := os.Getenv("GT_AGENT_MODE")
defer func() {
if oldAgentMode != "" {
os.Setenv("GT_AGENT_MODE", oldAgentMode)
} else {
os.Unsetenv("GT_AGENT_MODE")
}
}()
os.Setenv("GT_AGENT_MODE", "1")
if !IsAgentMode() {
t.Error("IsAgentMode() should return true when GT_AGENT_MODE=1")
}
os.Setenv("GT_AGENT_MODE", "0")
if IsAgentMode() {
t.Error("IsAgentMode() should return false when GT_AGENT_MODE=0")
}
}
func TestIsAgentMode_CLAUDE_CODE(t *testing.T) {
oldAgentMode := os.Getenv("GT_AGENT_MODE")
oldClaudeCode := os.Getenv("CLAUDE_CODE")
defer func() {
if oldAgentMode != "" {
os.Setenv("GT_AGENT_MODE", oldAgentMode)
} else {
os.Unsetenv("GT_AGENT_MODE")
}
if oldClaudeCode != "" {
os.Setenv("CLAUDE_CODE", oldClaudeCode)
} else {
os.Unsetenv("CLAUDE_CODE")
}
}()
os.Unsetenv("GT_AGENT_MODE")
os.Setenv("CLAUDE_CODE", "1")
if !IsAgentMode() {
t.Error("IsAgentMode() should return true when CLAUDE_CODE is set")
}
}
func TestIsAgentMode_CLAUDE_CODE_AnyValue(t *testing.T) {
oldAgentMode := os.Getenv("GT_AGENT_MODE")
oldClaudeCode := os.Getenv("CLAUDE_CODE")
defer func() {
if oldAgentMode != "" {
os.Setenv("GT_AGENT_MODE", oldAgentMode)
} else {
os.Unsetenv("GT_AGENT_MODE")
}
if oldClaudeCode != "" {
os.Setenv("CLAUDE_CODE", oldClaudeCode)
} else {
os.Unsetenv("CLAUDE_CODE")
}
}()
os.Unsetenv("GT_AGENT_MODE")
os.Setenv("CLAUDE_CODE", "any-value")
if !IsAgentMode() {
t.Error("IsAgentMode() should return true when CLAUDE_CODE is set to any value")
}
}

196
internal/wisp/io_test.go Normal file
View File

@@ -0,0 +1,196 @@
package wisp
import (
"encoding/json"
"os"
"path/filepath"
"testing"
)
func TestEnsureDir(t *testing.T) {
tmpDir := t.TempDir()
// Test creating directory in existing root
dir, err := EnsureDir(tmpDir)
if err != nil {
t.Fatalf("EnsureDir() error = %v", err)
}
expectedDir := filepath.Join(tmpDir, WispDir)
if dir != expectedDir {
t.Errorf("EnsureDir() = %q, want %q", dir, expectedDir)
}
// Verify directory exists
info, err := os.Stat(dir)
if err != nil {
t.Fatalf("Failed to stat directory: %v", err)
}
if !info.IsDir() {
t.Error("EnsureDir() should create a directory")
}
// Test calling again (should be idempotent)
dir2, err := EnsureDir(tmpDir)
if err != nil {
t.Fatalf("EnsureDir() second call error = %v", err)
}
if dir2 != dir {
t.Errorf("EnsureDir() second call = %q, want %q", dir2, dir)
}
}
func TestEnsureDir_Permissions(t *testing.T) {
tmpDir := t.TempDir()
dir, err := EnsureDir(tmpDir)
if err != nil {
t.Fatalf("EnsureDir() error = %v", err)
}
info, err := os.Stat(dir)
if err != nil {
t.Fatalf("Failed to stat directory: %v", err)
}
// Check directory permissions are 0755
expectedPerm := os.FileMode(0755)
if info.Mode().Perm() != expectedPerm {
t.Errorf("Directory permissions = %v, want %v", info.Mode().Perm(), expectedPerm)
}
}
func TestWispPath(t *testing.T) {
tests := []struct {
name string
root string
filename string
want string
}{
{
name: "basic path",
root: "/path/to/root",
filename: "bead.json",
want: "/path/to/root/.beads/bead.json",
},
{
name: "nested filename",
root: "/path/to/root",
filename: "subdir/bead.json",
want: "/path/to/root/.beads/subdir/bead.json",
},
{
name: "empty filename",
root: "/path/to/root",
filename: "",
want: "/path/to/root/.beads",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := WispPath(tt.root, tt.filename)
if got != tt.want {
t.Errorf("WispPath() = %q, want %q", got, tt.want)
}
})
}
}
func TestWispPath_WithWispDir(t *testing.T) {
// Verify WispPath uses WispDir constant
root := "/test/root"
filename := "test.json"
expected := filepath.Join(root, WispDir, filename)
got := WispPath(root, filename)
if got != expected {
t.Errorf("WispPath() = %q, want %q (using WispDir=%q)", got, expected, WispDir)
}
}
func TestWriteJSON_Helper(t *testing.T) {
tmpDir := t.TempDir()
testPath := filepath.Join(tmpDir, "test.json")
type TestData struct {
Name string `json:"name"`
Value int `json:"value"`
}
data := TestData{
Name: "test",
Value: 42,
}
// writeJSON is unexported, so we test it indirectly through other functions
// or we can test the behavior through EnsureDir which uses similar patterns
// For now, test that we can write JSON using AtomicWriteJSON from util package
// which is what wisp would typically use
err := writeJSON(testPath, data)
if err != nil {
t.Fatalf("writeJSON() error = %v", err)
}
// Verify file exists
content, err := os.ReadFile(testPath)
if err != nil {
t.Fatalf("Failed to read file: %v", err)
}
// Verify JSON is valid
var decoded TestData
if err := json.Unmarshal(content, &decoded); err != nil {
t.Fatalf("Failed to unmarshal JSON: %v", err)
}
if decoded.Name != data.Name {
t.Errorf("Name = %q, want %q", decoded.Name, data.Name)
}
if decoded.Value != data.Value {
t.Errorf("Value = %d, want %d", decoded.Value, data.Value)
}
// Verify temp file was cleaned up
tmpPath := testPath + ".tmp"
if _, err := os.Stat(tmpPath); err == nil {
t.Error("Temp file should be removed after successful write")
}
}
func TestWriteJSON_Overwrite(t *testing.T) {
tmpDir := t.TempDir()
testPath := filepath.Join(tmpDir, "test.json")
// Write initial data
initialData := map[string]string{"key": "initial"}
if err := writeJSON(testPath, initialData); err != nil {
t.Fatalf("writeJSON() initial error = %v", err)
}
// Overwrite with new data
newData := map[string]string{"key": "updated", "new": "value"}
if err := writeJSON(testPath, newData); err != nil {
t.Fatalf("writeJSON() overwrite error = %v", err)
}
// Verify data was updated
content, err := os.ReadFile(testPath)
if err != nil {
t.Fatalf("Failed to read file: %v", err)
}
var decoded map[string]string
if err := json.Unmarshal(content, &decoded); err != nil {
t.Fatalf("Failed to unmarshal JSON: %v", err)
}
if decoded["key"] != "updated" {
t.Errorf("key = %q, want %q", decoded["key"], "updated")
}
if decoded["new"] != "value" {
t.Errorf("new = %q, want %q", decoded["new"], "value")
}
}

View File

@@ -0,0 +1,25 @@
package wisp
import "testing"
func TestWispDir(t *testing.T) {
// Test that WispDir constant is defined correctly
expected := ".beads"
if WispDir != expected {
t.Errorf("WispDir = %q, want %q", WispDir, expected)
}
}
func TestWispDirNotEmpty(t *testing.T) {
// Test that WispDir is not empty
if WispDir == "" {
t.Error("WispDir should not be empty")
}
}
func TestWispDirStartsWithDot(t *testing.T) {
// Test that WispDir is a hidden directory (starts with dot)
if len(WispDir) == 0 || WispDir[0] != '.' {
t.Errorf("WispDir should start with '.' for hidden directory, got %q", WispDir)
}
}

View File

@@ -0,0 +1,230 @@
package witness
import (
"encoding/json"
"testing"
"time"
"github.com/steveyegge/gastown/internal/agent"
)
func TestStateTypeAlias(t *testing.T) {
// Verify State is an alias for agent.State
var s State = agent.StateRunning
if s != agent.StateRunning {
t.Errorf("State type alias not working correctly")
}
}
func TestStateConstants(t *testing.T) {
tests := []struct {
name string
state State
parent agent.State
}{
{"StateStopped", StateStopped, agent.StateStopped},
{"StateRunning", StateRunning, agent.StateRunning},
{"StatePaused", StatePaused, agent.StatePaused},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.state != tt.parent {
t.Errorf("State constant %s = %v, want %v", tt.name, tt.state, tt.parent)
}
})
}
}
func TestWitness_ZeroValues(t *testing.T) {
var w Witness
if w.RigName != "" {
t.Errorf("zero value Witness.RigName should be empty, got %q", w.RigName)
}
if w.State != "" {
t.Errorf("zero value Witness.State should be empty, got %q", w.State)
}
if w.PID != 0 {
t.Errorf("zero value Witness.PID should be 0, got %d", w.PID)
}
if w.StartedAt != nil {
t.Error("zero value Witness.StartedAt should be nil")
}
}
func TestWitness_JSONMarshaling(t *testing.T) {
now := time.Now().Round(time.Second)
w := Witness{
RigName: "gastown",
State: StateRunning,
PID: 12345,
StartedAt: &now,
MonitoredPolecats: []string{"keeper", "valkyrie"},
Config: WitnessConfig{
MaxWorkers: 4,
SpawnDelayMs: 5000,
AutoSpawn: true,
},
SpawnedIssues: []string{"hq-abc123"},
}
data, err := json.Marshal(w)
if err != nil {
t.Fatalf("json.Marshal() error = %v", err)
}
var unmarshaled Witness
if err := json.Unmarshal(data, &unmarshaled); err != nil {
t.Fatalf("json.Unmarshal() error = %v", err)
}
if unmarshaled.RigName != w.RigName {
t.Errorf("After round-trip: RigName = %q, want %q", unmarshaled.RigName, w.RigName)
}
if unmarshaled.State != w.State {
t.Errorf("After round-trip: State = %v, want %v", unmarshaled.State, w.State)
}
if unmarshaled.PID != w.PID {
t.Errorf("After round-trip: PID = %d, want %d", unmarshaled.PID, w.PID)
}
}
func TestWitnessConfig_ZeroValues(t *testing.T) {
var cfg WitnessConfig
if cfg.MaxWorkers != 0 {
t.Errorf("zero value WitnessConfig.MaxWorkers should be 0, got %d", cfg.MaxWorkers)
}
if cfg.SpawnDelayMs != 0 {
t.Errorf("zero value WitnessConfig.SpawnDelayMs should be 0, got %d", cfg.SpawnDelayMs)
}
if cfg.AutoSpawn {
t.Error("zero value WitnessConfig.AutoSpawn should be false")
}
}
func TestWitnessConfig_JSONMarshaling(t *testing.T) {
cfg := WitnessConfig{
MaxWorkers: 8,
SpawnDelayMs: 10000,
AutoSpawn: false,
EpicID: "epic-123",
IssuePrefix: "gt-",
}
data, err := json.Marshal(cfg)
if err != nil {
t.Fatalf("json.Marshal() error = %v", err)
}
var unmarshaled WitnessConfig
if err := json.Unmarshal(data, &unmarshaled); err != nil {
t.Fatalf("json.Unmarshal() error = %v", err)
}
if unmarshaled.MaxWorkers != cfg.MaxWorkers {
t.Errorf("After round-trip: MaxWorkers = %d, want %d", unmarshaled.MaxWorkers, cfg.MaxWorkers)
}
if unmarshaled.SpawnDelayMs != cfg.SpawnDelayMs {
t.Errorf("After round-trip: SpawnDelayMs = %d, want %d", unmarshaled.SpawnDelayMs, cfg.SpawnDelayMs)
}
if unmarshaled.AutoSpawn != cfg.AutoSpawn {
t.Errorf("After round-trip: AutoSpawn = %v, want %v", unmarshaled.AutoSpawn, cfg.AutoSpawn)
}
if unmarshaled.EpicID != cfg.EpicID {
t.Errorf("After round-trip: EpicID = %q, want %q", unmarshaled.EpicID, cfg.EpicID)
}
if unmarshaled.IssuePrefix != cfg.IssuePrefix {
t.Errorf("After round-trip: IssuePrefix = %q, want %q", unmarshaled.IssuePrefix, cfg.IssuePrefix)
}
}
func TestWitnessConfig_OmitEmpty(t *testing.T) {
cfg := WitnessConfig{
MaxWorkers: 4,
SpawnDelayMs: 5000,
AutoSpawn: true,
// EpicID and IssuePrefix left empty
}
data, err := json.Marshal(cfg)
if err != nil {
t.Fatalf("json.Marshal() error = %v", err)
}
var raw map[string]interface{}
if err := json.Unmarshal(data, &raw); err != nil {
t.Fatalf("json.Unmarshal() to map error = %v", err)
}
// Empty fields should be omitted
if _, exists := raw["epic_id"]; exists {
t.Error("Field 'epic_id' should be omitted when empty")
}
if _, exists := raw["issue_prefix"]; exists {
t.Error("Field 'issue_prefix' should be omitted when empty")
}
// Required fields should be present
requiredFields := []string{"max_workers", "spawn_delay_ms", "auto_spawn"}
for _, field := range requiredFields {
if _, exists := raw[field]; !exists {
t.Errorf("Required field '%s' should be present", field)
}
}
}
func TestWitness_OmitEmpty(t *testing.T) {
w := Witness{
RigName: "gastown",
State: StateRunning,
// PID, StartedAt, MonitoredPolecats, SpawnedIssues left empty/nil
}
data, err := json.Marshal(w)
if err != nil {
t.Fatalf("json.Marshal() error = %v", err)
}
var raw map[string]interface{}
if err := json.Unmarshal(data, &raw); err != nil {
t.Fatalf("json.Unmarshal() to map error = %v", err)
}
// Empty optional fields should be omitted
if _, exists := raw["pid"]; exists {
t.Error("Field 'pid' should be omitted when zero")
}
if _, exists := raw["started_at"]; exists {
t.Error("Field 'started_at' should be omitted when nil")
}
if _, exists := raw["monitored_polecats"]; exists {
t.Error("Field 'monitored_polecats' should be omitted when nil/empty")
}
if _, exists := raw["spawned_issues"]; exists {
t.Error("Field 'spawned_issues' should be omitted when nil/empty")
}
}
func TestWitness_WithMonitoredPolecats(t *testing.T) {
w := Witness{
RigName: "gastown",
State: StateRunning,
MonitoredPolecats: []string{"keeper", "valkyrie", "nux"},
}
data, err := json.Marshal(w)
if err != nil {
t.Fatalf("json.Marshal() error = %v", err)
}
var unmarshaled Witness
if err := json.Unmarshal(data, &unmarshaled); err != nil {
t.Fatalf("json.Unmarshal() error = %v", err)
}
if len(unmarshaled.MonitoredPolecats) != 3 {
t.Errorf("After round-trip: MonitoredPolecats length = %d, want 3", len(unmarshaled.MonitoredPolecats))
}
}