Skip to content

Commit 365633a

Browse files
committed
feat(hawk): add mock LLM server, background agent tests, version tests
- Added internal/testutil/mock_llm.go — configurable mock LLM server for testing - Added comprehensive BackgroundAgentPool tests (submit, collect, concurrent, waitall) - Added cmd/version_test.go (SetVersion, SetBuildDate, VersionString, ShortVersion) - Verified full ecosystem integration via go.work (eyrie, tok, yaad, inspect, sight)
1 parent 9b914c7 commit 365633a

3 files changed

Lines changed: 405 additions & 0 deletions

File tree

cmd/version_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package cmd
2+
3+
import "testing"
4+
5+
func TestSetVersion(t *testing.T) {
6+
t.Parallel()
7+
SetVersion("1.2.3")
8+
if version != "1.2.3" {
9+
t.Errorf("version = %q, want %q", version, "1.2.3")
10+
}
11+
}
12+
13+
func TestSetBuildDate(t *testing.T) {
14+
t.Parallel()
15+
SetBuildDate("2026-01-01")
16+
if buildDate != "2026-01-01" {
17+
t.Errorf("buildDate = %q, want %q", buildDate, "2026-01-01")
18+
}
19+
}
20+
21+
func TestVersionString(t *testing.T) {
22+
t.Parallel()
23+
Version = "test-ver"
24+
Commit = "abc123"
25+
Date = "2026-05-15"
26+
s := VersionString()
27+
if s == "" {
28+
t.Error("VersionString() should not be empty")
29+
}
30+
}
31+
32+
func TestShortVersion(t *testing.T) {
33+
t.Parallel()
34+
Version = "0.4.0"
35+
got := ShortVersion()
36+
if got != "0.4.0" {
37+
t.Errorf("ShortVersion() = %q, want %q", got, "0.4.0")
38+
}
39+
}

engine/background_agent_test.go

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
package engine
2+
3+
import (
4+
"context"
5+
"errors"
6+
"sync/atomic"
7+
"testing"
8+
"time"
9+
)
10+
11+
func TestBackgroundAgentPool_NewPool(t *testing.T) {
12+
t.Parallel()
13+
pool := NewBackgroundAgentPool()
14+
if pool == nil {
15+
t.Fatal("NewBackgroundAgentPool returned nil")
16+
}
17+
if pool.HasPending() {
18+
t.Error("new pool should have no pending tasks")
19+
}
20+
if pool.PendingCount() != 0 {
21+
t.Errorf("PendingCount() = %d, want 0", pool.PendingCount())
22+
}
23+
}
24+
25+
func TestBackgroundAgentPool_SubmitAndCollect(t *testing.T) {
26+
t.Parallel()
27+
pool := NewBackgroundAgentPool()
28+
29+
pool.Submit("task-1", "do something", func(ctx context.Context, prompt string) (string, error) {
30+
return "result-1", nil
31+
})
32+
33+
// Give goroutine time to complete
34+
time.Sleep(50 * time.Millisecond)
35+
36+
results := pool.Collect()
37+
if len(results) != 1 {
38+
t.Fatalf("Collect() returned %d results, want 1", len(results))
39+
}
40+
if results[0].ID != "task-1" {
41+
t.Errorf("ID = %q, want %q", results[0].ID, "task-1")
42+
}
43+
if results[0].Output != "result-1" {
44+
t.Errorf("Output = %q, want %q", results[0].Output, "result-1")
45+
}
46+
if results[0].Error != nil {
47+
t.Errorf("Error = %v, want nil", results[0].Error)
48+
}
49+
if results[0].Elapsed <= 0 {
50+
t.Error("Elapsed should be positive")
51+
}
52+
}
53+
54+
func TestBackgroundAgentPool_SubmitError(t *testing.T) {
55+
t.Parallel()
56+
pool := NewBackgroundAgentPool()
57+
expectedErr := errors.New("spawn failed")
58+
59+
pool.Submit("err-task", "fail", func(ctx context.Context, prompt string) (string, error) {
60+
return "", expectedErr
61+
})
62+
63+
time.Sleep(50 * time.Millisecond)
64+
65+
results := pool.Collect()
66+
if len(results) != 1 {
67+
t.Fatalf("Collect() returned %d results, want 1", len(results))
68+
}
69+
if !errors.Is(results[0].Error, expectedErr) {
70+
t.Errorf("Error = %v, want %v", results[0].Error, expectedErr)
71+
}
72+
}
73+
74+
func TestBackgroundAgentPool_CollectEmpty(t *testing.T) {
75+
t.Parallel()
76+
pool := NewBackgroundAgentPool()
77+
results := pool.Collect()
78+
if len(results) != 0 {
79+
t.Errorf("Collect() on empty pool returned %d results", len(results))
80+
}
81+
}
82+
83+
func TestBackgroundAgentPool_MultipleSubmits(t *testing.T) {
84+
t.Parallel()
85+
pool := NewBackgroundAgentPool()
86+
87+
for i := 0; i < 5; i++ {
88+
id := "task-" + string(rune('a'+i))
89+
pool.Submit(id, "prompt", func(ctx context.Context, prompt string) (string, error) {
90+
time.Sleep(10 * time.Millisecond)
91+
return "done", nil
92+
})
93+
}
94+
95+
if pool.PendingCount() != 5 {
96+
t.Errorf("PendingCount() = %d, want 5", pool.PendingCount())
97+
}
98+
99+
time.Sleep(100 * time.Millisecond)
100+
101+
results := pool.Collect()
102+
if len(results) != 5 {
103+
t.Errorf("Collect() returned %d results, want 5", len(results))
104+
}
105+
106+
if pool.HasPending() {
107+
t.Error("HasPending() should be false after all collected")
108+
}
109+
}
110+
111+
func TestBackgroundAgentPool_WaitAll(t *testing.T) {
112+
t.Parallel()
113+
pool := NewBackgroundAgentPool()
114+
115+
pool.Submit("slow", "wait", func(ctx context.Context, prompt string) (string, error) {
116+
time.Sleep(100 * time.Millisecond)
117+
return "waited", nil
118+
})
119+
120+
results := pool.WaitAll()
121+
if len(results) != 1 {
122+
t.Fatalf("WaitAll() returned %d results, want 1", len(results))
123+
}
124+
if results[0].Output != "waited" {
125+
t.Errorf("Output = %q, want %q", results[0].Output, "waited")
126+
}
127+
}
128+
129+
func TestBackgroundAgentPool_AllResults(t *testing.T) {
130+
t.Parallel()
131+
pool := NewBackgroundAgentPool()
132+
133+
pool.Submit("r1", "p1", func(ctx context.Context, prompt string) (string, error) {
134+
return "out1", nil
135+
})
136+
pool.Submit("r2", "p2", func(ctx context.Context, prompt string) (string, error) {
137+
return "out2", nil
138+
})
139+
140+
time.Sleep(50 * time.Millisecond)
141+
pool.Collect()
142+
143+
all := pool.AllResults()
144+
if len(all) != 2 {
145+
t.Errorf("AllResults() returned %d, want 2", len(all))
146+
}
147+
}
148+
149+
func TestBackgroundAgentPool_ConcurrentAccess(t *testing.T) {
150+
t.Parallel()
151+
pool := NewBackgroundAgentPool()
152+
var count atomic.Int32
153+
154+
for i := 0; i < 20; i++ {
155+
pool.Submit("concurrent", "p", func(ctx context.Context, prompt string) (string, error) {
156+
count.Add(1)
157+
time.Sleep(10 * time.Millisecond)
158+
return "ok", nil
159+
})
160+
}
161+
162+
// Concurrent reads while tasks are running
163+
go func() { pool.HasPending() }()
164+
go func() { pool.PendingCount() }()
165+
go func() { pool.Collect() }()
166+
167+
pool.WaitAll()
168+
169+
if count.Load() != 20 {
170+
t.Errorf("expected 20 tasks to run, got %d", count.Load())
171+
}
172+
}
173+
174+
func TestBackgroundAgentPool_FormatResults_Empty(t *testing.T) {
175+
t.Parallel()
176+
result := FormatResults(nil)
177+
if result != "" {
178+
t.Errorf("FormatResults(nil) = %q, want empty", result)
179+
}
180+
}
181+
182+
func TestBackgroundAgentPool_FormatResults_WithResults(t *testing.T) {
183+
t.Parallel()
184+
results := []BackgroundResult{
185+
{ID: "t1", Prompt: "research X", Output: "found Y", Elapsed: time.Second},
186+
{ID: "t2", Prompt: "check Z", Error: errors.New("failed"), Elapsed: 2 * time.Second},
187+
}
188+
formatted := FormatResults(results)
189+
if formatted == "" {
190+
t.Error("FormatResults should produce non-empty output")
191+
}
192+
}

0 commit comments

Comments
 (0)