Skip to content

Commit 8fe7d8e

Browse files
committed
Add tests for router, prompt builder, and log utilities
1 parent f0f1eed commit 8fe7d8e

File tree

3 files changed

+349
-0
lines changed

3 files changed

+349
-0
lines changed

internal/log_test.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package internal
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"strings"
7+
"testing"
8+
)
9+
10+
func TestLogPath_EndsInLogJSONL(t *testing.T) {
11+
p := LogPath()
12+
13+
if p == "" {
14+
t.Fatal("LogPath returned empty string")
15+
}
16+
if !strings.HasSuffix(p, "log.jsonl") {
17+
t.Errorf("LogPath should end in log.jsonl, got %s", p)
18+
}
19+
}
20+
21+
func TestLogPath_ContainsConfigReflex(t *testing.T) {
22+
p := LogPath()
23+
24+
if p == "" {
25+
t.Fatal("LogPath returned empty string")
26+
}
27+
if !strings.Contains(p, filepath.Join(".config", "reflex")) {
28+
t.Errorf("LogPath should contain .config/reflex, got %s", p)
29+
}
30+
}
31+
32+
func TestRotateLog_SmallFileNotModified(t *testing.T) {
33+
dir := t.TempDir()
34+
logFile := filepath.Join(dir, "log.jsonl")
35+
36+
// Write a small file (well under maxLogSize)
37+
content := strings.Repeat(`{"ts":"2025-01-01","status":"ok"}`+"\n", 10)
38+
if err := os.WriteFile(logFile, []byte(content), 0644); err != nil {
39+
t.Fatal(err)
40+
}
41+
42+
before, _ := os.ReadFile(logFile)
43+
rotateLog(logFile)
44+
after, _ := os.ReadFile(logFile)
45+
46+
if string(before) != string(after) {
47+
t.Error("small file should not be modified by rotateLog")
48+
}
49+
}
50+
51+
func TestRotateLog_LargeFileTruncatedToKeepEntries(t *testing.T) {
52+
dir := t.TempDir()
53+
logFile := filepath.Join(dir, "log.jsonl")
54+
55+
// Generate enough lines to exceed maxLogSize (500KB).
56+
// Each line is ~80 bytes, so 7000 lines = ~560KB > 512KB.
57+
totalLines := 7000
58+
var sb strings.Builder
59+
for i := 0; i < totalLines; i++ {
60+
sb.WriteString(`{"ts":"2025-01-01T00:00:00Z","status":"ok","cwd":"/test/working/directory","message_count":1,"model":"gpt-4"}`)
61+
sb.WriteByte('\n')
62+
}
63+
if err := os.WriteFile(logFile, []byte(sb.String()), 0644); err != nil {
64+
t.Fatal(err)
65+
}
66+
67+
// Verify the file is actually over maxLogSize
68+
info, _ := os.Stat(logFile)
69+
if info.Size() < maxLogSize {
70+
t.Fatalf("test file should be over maxLogSize (%d), got %d", maxLogSize, info.Size())
71+
}
72+
73+
rotateLog(logFile)
74+
75+
data, err := os.ReadFile(logFile)
76+
if err != nil {
77+
t.Fatal(err)
78+
}
79+
lines := strings.Split(strings.TrimSpace(string(data)), "\n")
80+
81+
if len(lines) != keepEntries {
82+
t.Errorf("expected %d lines after rotation, got %d", keepEntries, len(lines))
83+
}
84+
}

internal/prompt_test.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package internal
2+
3+
import (
4+
"encoding/json"
5+
"strings"
6+
"testing"
7+
)
8+
9+
func TestBuild_ContainsSystemInstruction(t *testing.T) {
10+
prompt := Build(nil, Registry{})
11+
12+
if !strings.Contains(prompt, "context router") {
13+
t.Error("prompt should contain 'context router' system instruction")
14+
}
15+
}
16+
17+
func TestBuild_ContainsRegistryJSON(t *testing.T) {
18+
registry := Registry{
19+
Docs: []RegistryDoc{{Path: "docs/auth.md", Summary: "auth flow"}},
20+
Skills: []RegistrySkill{{Name: "deploy", Description: "deployment"}},
21+
}
22+
23+
prompt := Build(nil, registry)
24+
25+
regJSON, _ := json.Marshal(registry)
26+
if !strings.Contains(prompt, string(regJSON)) {
27+
t.Errorf("prompt should contain registry JSON\nwant substring: %s", string(regJSON))
28+
}
29+
}
30+
31+
func TestBuild_ContainsMessagesJSON(t *testing.T) {
32+
messages := []Message{
33+
{Type: "user", Text: "help me deploy"},
34+
{Type: "assistant", Text: "sure thing"},
35+
}
36+
37+
prompt := Build(messages, Registry{})
38+
39+
msgJSON, _ := json.Marshal(messages)
40+
if !strings.Contains(prompt, string(msgJSON)) {
41+
t.Errorf("prompt should contain messages JSON\nwant substring: %s", string(msgJSON))
42+
}
43+
}
44+
45+
func TestBuild_ContainsBalancedRule(t *testing.T) {
46+
prompt := Build(nil, Registry{})
47+
48+
if !strings.Contains(prompt, "When in doubt, leave it out") {
49+
t.Error("prompt should contain the balanced rule: 'When in doubt, leave it out'")
50+
}
51+
}
52+
53+
func TestBuild_EmptyRegistryProducesValidPrompt(t *testing.T) {
54+
registry := Registry{Docs: []RegistryDoc{}, Skills: []RegistrySkill{}}
55+
56+
prompt := Build([]Message{{Type: "user", Text: "hello"}}, registry)
57+
58+
if prompt == "" {
59+
t.Fatal("prompt should not be empty")
60+
}
61+
if !strings.Contains(prompt, "context router") {
62+
t.Error("prompt with empty registry should still contain system instruction")
63+
}
64+
// Verify the empty registry is valid JSON in the prompt
65+
regJSON, _ := json.Marshal(registry)
66+
if !strings.Contains(prompt, string(regJSON)) {
67+
t.Error("prompt should contain the empty registry as JSON")
68+
}
69+
}
70+
71+
func TestBuild_EmptyMessagesProducesValidPrompt(t *testing.T) {
72+
registry := Registry{
73+
Docs: []RegistryDoc{{Path: "docs/a.md", Summary: "a"}},
74+
Skills: []RegistrySkill{},
75+
}
76+
77+
prompt := Build(nil, registry)
78+
79+
if prompt == "" {
80+
t.Fatal("prompt should not be empty")
81+
}
82+
if !strings.Contains(prompt, "context router") {
83+
t.Error("prompt with nil messages should still contain system instruction")
84+
}
85+
// nil marshals to "null" in JSON
86+
if !strings.Contains(prompt, "null") {
87+
t.Error("prompt with nil messages should contain 'null' for messages JSON")
88+
}
89+
}
90+
91+
func TestBuild_EmptySliceMessagesProducesValidPrompt(t *testing.T) {
92+
registry := Registry{
93+
Docs: []RegistryDoc{{Path: "docs/a.md", Summary: "a"}},
94+
Skills: []RegistrySkill{},
95+
}
96+
97+
prompt := Build([]Message{}, registry)
98+
99+
if prompt == "" {
100+
t.Fatal("prompt should not be empty")
101+
}
102+
// Empty slice marshals to "[]"
103+
if !strings.Contains(prompt, "[]") {
104+
t.Error("prompt with empty messages slice should contain '[]'")
105+
}
106+
}

internal/router_test.go

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
package internal
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestFilterRegistry_EmptyRegistry(t *testing.T) {
8+
registry := Registry{Docs: []RegistryDoc{}, Skills: []RegistrySkill{}}
9+
session := SessionState{}
10+
11+
got := filterRegistry(registry, session)
12+
13+
if len(got.Docs) != 0 {
14+
t.Errorf("expected 0 docs, got %d", len(got.Docs))
15+
}
16+
if len(got.Skills) != 0 {
17+
t.Errorf("expected 0 skills, got %d", len(got.Skills))
18+
}
19+
}
20+
21+
func TestFilterRegistry_ExcludesDocsInSession(t *testing.T) {
22+
registry := Registry{
23+
Docs: []RegistryDoc{
24+
{Path: "docs/a.md", Summary: "doc a"},
25+
{Path: "docs/b.md", Summary: "doc b"},
26+
},
27+
Skills: []RegistrySkill{},
28+
}
29+
session := SessionState{
30+
DocsRead: []string{"docs/a.md"},
31+
}
32+
33+
got := filterRegistry(registry, session)
34+
35+
if len(got.Docs) != 1 {
36+
t.Fatalf("expected 1 doc, got %d", len(got.Docs))
37+
}
38+
if got.Docs[0].Path != "docs/b.md" {
39+
t.Errorf("expected docs/b.md, got %s", got.Docs[0].Path)
40+
}
41+
}
42+
43+
func TestFilterRegistry_ExcludesSkillsInSession(t *testing.T) {
44+
registry := Registry{
45+
Docs: []RegistryDoc{},
46+
Skills: []RegistrySkill{{Name: "deploy", Description: "deploy skill"}, {Name: "test", Description: "test skill"}},
47+
}
48+
session := SessionState{
49+
SkillsUsed: []string{"deploy"},
50+
}
51+
52+
got := filterRegistry(registry, session)
53+
54+
if len(got.Skills) != 1 {
55+
t.Fatalf("expected 1 skill, got %d", len(got.Skills))
56+
}
57+
if got.Skills[0].Name != "test" {
58+
t.Errorf("expected test skill, got %s", got.Skills[0].Name)
59+
}
60+
}
61+
62+
func TestFilterRegistry_KeepsItemsNotInSession(t *testing.T) {
63+
registry := Registry{
64+
Docs: []RegistryDoc{{Path: "docs/a.md", Summary: "doc a"}},
65+
Skills: []RegistrySkill{{Name: "build", Description: "build skill"}},
66+
}
67+
session := SessionState{
68+
DocsRead: []string{"docs/other.md"},
69+
SkillsUsed: []string{"other-skill"},
70+
}
71+
72+
got := filterRegistry(registry, session)
73+
74+
if len(got.Docs) != 1 {
75+
t.Errorf("expected 1 doc kept, got %d", len(got.Docs))
76+
}
77+
if len(got.Skills) != 1 {
78+
t.Errorf("expected 1 skill kept, got %d", len(got.Skills))
79+
}
80+
}
81+
82+
func TestFilterRegistry_MixedExcludeAndKeep(t *testing.T) {
83+
registry := Registry{
84+
Docs: []RegistryDoc{
85+
{Path: "docs/keep.md", Summary: "keep"},
86+
{Path: "docs/drop.md", Summary: "drop"},
87+
{Path: "docs/also-keep.md", Summary: "also keep"},
88+
},
89+
Skills: []RegistrySkill{
90+
{Name: "keep-skill", Description: "kept"},
91+
{Name: "drop-skill", Description: "dropped"},
92+
},
93+
}
94+
session := SessionState{
95+
DocsRead: []string{"docs/drop.md"},
96+
SkillsUsed: []string{"drop-skill"},
97+
}
98+
99+
got := filterRegistry(registry, session)
100+
101+
if len(got.Docs) != 2 {
102+
t.Fatalf("expected 2 docs, got %d", len(got.Docs))
103+
}
104+
if got.Docs[0].Path != "docs/keep.md" || got.Docs[1].Path != "docs/also-keep.md" {
105+
t.Errorf("unexpected docs: %v, %v", got.Docs[0].Path, got.Docs[1].Path)
106+
}
107+
if len(got.Skills) != 1 {
108+
t.Fatalf("expected 1 skill, got %d", len(got.Skills))
109+
}
110+
if got.Skills[0].Name != "keep-skill" {
111+
t.Errorf("expected keep-skill, got %s", got.Skills[0].Name)
112+
}
113+
}
114+
115+
func TestRoute_EmptyRegistryReturnsSkipReason(t *testing.T) {
116+
input := RouteInput{
117+
Messages: []Message{{Type: "user", Text: "hello"}},
118+
Registry: Registry{Docs: []RegistryDoc{}, Skills: []RegistrySkill{}},
119+
Session: SessionState{},
120+
}
121+
122+
result, _, _, _, skipReason, err := Route(input, DefaultConfig())
123+
124+
if err != nil {
125+
t.Fatalf("unexpected error: %v", err)
126+
}
127+
if skipReason != "no docs or skills in registry" {
128+
t.Errorf("expected skip reason 'no docs or skills in registry', got %q", skipReason)
129+
}
130+
if len(result.Docs) != 0 || len(result.Skills) != 0 {
131+
t.Errorf("expected empty result, got docs=%v skills=%v", result.Docs, result.Skills)
132+
}
133+
}
134+
135+
func TestRoute_AllItemsAlreadyInjectedReturnsSkipReason(t *testing.T) {
136+
input := RouteInput{
137+
Messages: []Message{{Type: "user", Text: "hello"}},
138+
Registry: Registry{
139+
Docs: []RegistryDoc{{Path: "docs/a.md", Summary: "a"}},
140+
Skills: []RegistrySkill{{Name: "build", Description: "build"}},
141+
},
142+
Session: SessionState{
143+
DocsRead: []string{"docs/a.md"},
144+
SkillsUsed: []string{"build"},
145+
},
146+
}
147+
148+
result, _, _, _, skipReason, err := Route(input, DefaultConfig())
149+
150+
if err != nil {
151+
t.Fatalf("unexpected error: %v", err)
152+
}
153+
if skipReason != "all 2 item(s) already injected this session" {
154+
t.Errorf("expected skip reason about 2 items, got %q", skipReason)
155+
}
156+
if len(result.Docs) != 0 || len(result.Skills) != 0 {
157+
t.Errorf("expected empty result, got docs=%v skills=%v", result.Docs, result.Skills)
158+
}
159+
}

0 commit comments

Comments
 (0)