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:
189
internal/agent/state_test.go
Normal file
189
internal/agent/state_test.go
Normal 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"`
|
||||
}
|
||||
93
internal/cmd/errors_test.go
Normal file
93
internal/cmd/errors_test.go
Normal 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
100
internal/crew/types_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
31
internal/doctor/errors_test.go
Normal file
31
internal/doctor/errors_test.go
Normal 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
148
internal/dog/types_test.go
Normal 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
193
internal/mail/bd_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
153
internal/opencode/plugin_test.go
Normal file
153
internal/opencode/plugin_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
251
internal/rig/overlay_test.go
Normal file
251
internal/rig/overlay_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
307
internal/runtime/runtime_test.go
Normal file
307
internal/runtime/runtime_test.go
Normal 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
|
||||
}
|
||||
93
internal/session/town_test.go
Normal file
93
internal/session/town_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
169
internal/style/style_test.go
Normal file
169
internal/style/style_test.go
Normal 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)
|
||||
}
|
||||
234
internal/ui/markdown_test.go
Normal file
234
internal/ui/markdown_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
247
internal/ui/terminal_test.go
Normal file
247
internal/ui/terminal_test.go
Normal 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
196
internal/wisp/io_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
25
internal/wisp/types_test.go
Normal file
25
internal/wisp/types_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
230
internal/witness/types_test.go
Normal file
230
internal/witness/types_test.go
Normal 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))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user