Skip to content

Commit 30928a3

Browse files
committed
feat: add cascade routing and full ecosystem integration tests
- Add CascadeRouter for automatic model tier selection (planning→Opus, coding→Sonnet, summary→Haiku) - Add ClassifyTask heuristic for keyword-based task classification - Add 5 integration tests exercising full pipeline: hawk→eyrie→tok→yaad→sight→inspect - Tests verify cross-project data flow with mock LLM, real SQLite, and httptest servers
1 parent 56eb628 commit 30928a3

3 files changed

Lines changed: 546 additions & 0 deletions

File tree

‎integration_test.go‎

Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"net/http"
8+
"net/http/httptest"
9+
"path/filepath"
10+
"testing"
11+
12+
"github.com/GrayCodeAI/inspect"
13+
"github.com/GrayCodeAI/sight"
14+
"github.com/GrayCodeAI/tok"
15+
"github.com/GrayCodeAI/yaad/engine"
16+
"github.com/GrayCodeAI/yaad/graph"
17+
"github.com/GrayCodeAI/yaad/storage"
18+
"github.com/GrayCodeAI/hawk/routing"
19+
)
20+
21+
// mockSightProvider implements sight.Provider for integration testing.
22+
type mockSightProvider struct {
23+
response string
24+
}
25+
26+
func (m *mockSightProvider) Chat(_ context.Context, _ []sight.Message, _ sight.ChatOpts) (*sight.Response, error) {
27+
return &sight.Response{Content: m.response, TokensUsed: 100}, nil
28+
}
29+
30+
// setupYaad creates a yaad engine backed by a temp SQLite database.
31+
func setupYaad(t *testing.T) *engine.Engine {
32+
t.Helper()
33+
dbPath := filepath.Join(t.TempDir(), "test.db")
34+
store, err := storage.NewStore(dbPath)
35+
if err != nil {
36+
t.Fatalf("failed to create store: %v", err)
37+
}
38+
t.Cleanup(func() { store.Close() })
39+
g := graph.New(store, store.DB())
40+
return engine.New(store, g)
41+
}
42+
43+
func TestIntegration_SightReviewStoreRecall(t *testing.T) {
44+
// 1. Set up mock LLM provider that returns a code review finding
45+
mockResp := `[{"severity":"high","file":"main.go","line":10,"message":"SQL injection vulnerability","fix":"Use parameterized queries","reasoning":"Direct string concatenation in SQL"}]`
46+
provider := &mockSightProvider{response: mockResp}
47+
48+
// 2. Run sight review with mock provider
49+
diff := `--- a/main.go
50+
+++ b/main.go
51+
@@ -8,0 +9,3 @@
52+
+func query(input string) {
53+
+ db.Exec("SELECT * FROM users WHERE name = '" + input + "'")
54+
+}`
55+
56+
ctx := context.Background()
57+
result, err := sight.Review(ctx, diff, sight.WithProvider(provider))
58+
if err != nil {
59+
t.Fatalf("sight.Review failed: %v", err)
60+
}
61+
if result == nil {
62+
t.Fatal("expected non-nil result")
63+
}
64+
65+
// 3. Store the review findings in yaad
66+
eng := setupYaad(t)
67+
findingsJSON, _ := json.Marshal(result.Findings)
68+
node, err := eng.Remember(ctx, engine.RememberInput{
69+
Content: fmt.Sprintf("Code review findings: %s", string(findingsJSON)),
70+
Type: "decision",
71+
Tags: "review,security",
72+
Project: "/test/project",
73+
})
74+
if err != nil {
75+
t.Fatalf("eng.Remember failed: %v", err)
76+
}
77+
if node == nil || node.ID == "" {
78+
t.Fatal("expected stored node with ID")
79+
}
80+
81+
// 4. Recall the stored findings
82+
recalled, err := eng.Recall(ctx, engine.RecallOpts{
83+
Query: "SQL injection review",
84+
Limit: 5,
85+
Project: "/test/project",
86+
})
87+
if err != nil {
88+
t.Fatalf("eng.Recall failed: %v", err)
89+
}
90+
if len(recalled.Nodes) == 0 {
91+
t.Fatal("expected to recall at least one node")
92+
}
93+
94+
found := false
95+
for _, n := range recalled.Nodes {
96+
if n.ID == node.ID {
97+
found = true
98+
break
99+
}
100+
}
101+
if !found {
102+
t.Fatal("recalled nodes should contain the review finding node")
103+
}
104+
}
105+
106+
func TestIntegration_InspectScanHTTPTest(t *testing.T) {
107+
// 1. Start a test HTTP server with known issues
108+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
109+
w.Header().Set("Content-Type", "text/html")
110+
// Intentionally missing security headers
111+
fmt.Fprint(w, `<!DOCTYPE html><html>
112+
<head><title>Test Site</title></head>
113+
<body>
114+
<a href="/broken-page">Broken Link</a>
115+
<img src="/missing.png">
116+
<form action="/submit"><input type="text" name="q"></form>
117+
</body></html>`)
118+
}))
119+
defer ts.Close()
120+
121+
// 2. Run inspect scan
122+
ctx := context.Background()
123+
report, err := inspect.Scan(ctx, ts.URL, inspect.Quick)
124+
if err != nil {
125+
t.Fatalf("inspect.Scan failed: %v", err)
126+
}
127+
if report == nil {
128+
t.Fatal("expected non-nil report")
129+
}
130+
if report.Target != ts.URL {
131+
t.Fatalf("expected got %s", ts.URL, report.Target)
132+
}
133+
134+
// 3. Store findings in yaad
135+
eng := setupYaad(t)
136+
reportJSON, _ := json.Marshal(report.Stats)
137+
_, err = eng.Remember(ctx, engine.RememberInput{
138+
Content: fmt.Sprintf("Site audit of %s: %d findings. %s", ts.URL, report.Stats.FindingsTotal, string(reportJSON)),
139+
Type: "bug",
140+
Tags: "inspect,audit",
141+
})
142+
if err != nil {
143+
t.Fatalf("eng.Remember failed: %v", err)
144+
}
145+
146+
// 4. Verify recall
147+
recalled, err := eng.Recall(ctx, engine.RecallOpts{Query: "site audit", Limit: 5})
148+
if err != nil {
149+
t.Fatalf("eng.Recall failed: %v", err)
150+
}
151+
if len(recalled.Nodes) == 0 {
152+
t.Fatal("expected to recall audit results")
153+
}
154+
}
155+
156+
func TestIntegration_TokCompression(t *testing.T) {
157+
// Generate a large repetitive text (simulates verbose CLI output)
158+
var large string
159+
for i := 0; i < 100; i++ {
160+
large += fmt.Sprintf("[INFO] 2025-05-08 10:00:%02d Processing item %d of 100... status=ok duration=12ms\n", i%60, i)
161+
}
162+
large += "[ERROR] 2025-05-08 10:01:00 Failed to process item 101: connection timeout\n"
163+
large += "[WARN] 2025-05-08 10:01:01 Retrying with backoff...\n"
164+
165+
// 1. Compress with tok
166+
compressed, stats := tok.Compress(large)
167+
if stats.ReductionPercent <= 0 {
168+
t.Fatalf("expected positive compression, got %.1f%%", stats.ReductionPercent)
169+
}
170+
if compressed == "" {
171+
t.Fatal("compressed output should not be empty")
172+
}
173+
if stats.OriginalTokens <= stats.FinalTokens {
174+
t.Fatalf("expected fewer final tokens (%d) than original (%d)", stats.FinalTokens, stats.OriginalTokens)
175+
}
176+
177+
// 2. Verify token estimation works
178+
tokens := tok.EstimateTokens(large)
179+
if tokens == 0 {
180+
t.Fatal("EstimateTokens should return non-zero for non-empty text")
181+
}
182+
183+
// 3. Store compression stats in yaad
184+
eng := setupYaad(t)
185+
ctx := context.Background()
186+
_, err := eng.Remember(ctx, engine.RememberInput{
187+
Content: fmt.Sprintf("Token compression: %d → %d tokens (%.1f%% reduction)", stats.OriginalTokens, stats.FinalTokens, stats.ReductionPercent),
188+
Type: "convention",
189+
Tags: "tok,performance",
190+
})
191+
if err != nil {
192+
t.Fatalf("eng.Remember failed: %v", err)
193+
}
194+
}
195+
196+
func TestIntegration_CascadeRouting(t *testing.T) {
197+
roles := routing.ModelRoles{
198+
Planner: "claude-opus-4-20250514",
199+
Coder: "claude-sonnet-4-20250514",
200+
Reviewer: "claude-sonnet-4-20250514",
201+
Commit: "claude-haiku-4-20250514",
202+
}
203+
cr := routing.NewCascadeRouter(roles)
204+
205+
tests := []struct {
206+
msg string
207+
wantType routing.TaskType
208+
wantModel string
209+
}{
210+
{"Design the architecture for the payment system", routing.TaskPlanning, "claude-opus-4-20250514"},
211+
{"Implement the payment endpoint", routing.TaskCoding, "claude-sonnet-4-20250514"},
212+
{"Summarize what we did today", routing.TaskSummary, "claude-haiku-4-20250514"},
213+
{"Review this pull request for security issues", routing.TaskReview, "claude-sonnet-4-20250514"},
214+
}
215+
216+
for _, tt := range tests {
217+
taskType := routing.ClassifyTask(tt.msg)
218+
if taskType != tt.wantType {
219+
t.Errorf("ClassifyTask(%q) = %s, want %s", tt.msg, taskType, tt.wantType)
220+
}
221+
222+
model := cr.Route(tt.msg, "")
223+
if model != tt.wantModel {
224+
t.Errorf("Route(%q) = %s, want %s", tt.msg, model, tt.wantModel)
225+
}
226+
}
227+
}
228+
229+
func TestIntegration_FullPipeline(t *testing.T) {
230+
ctx := context.Background()
231+
232+
// 1. Initialize all components
233+
eng := setupYaad(t)
234+
235+
// 2. Use tok to compress some context
236+
verbose := "This is a very long and verbose piece of text that contains a lot of redundant information that could be compressed significantly by removing unnecessary repetition and verbosity from the text content."
237+
compressed, _ := tok.Compress(verbose)
238+
if compressed == "" {
239+
compressed = verbose // fallback if no compression
240+
}
241+
242+
// 3. Use sight to review code (mock)
243+
mockResp := `[{"severity":"medium","file":"app.go","line":5,"message":"Unused variable","fix":"Remove unused var"}]`
244+
provider := &mockSightProvider{response: mockResp}
245+
diff := "--- a/app.go\n+++ b/app.go\n@@ -4,0 +5 @@\n+var unused = 42"
246+
reviewResult, err := sight.Review(ctx, diff, sight.WithProvider(provider))
247+
if err != nil {
248+
t.Fatalf("sight.Review: %v", err)
249+
}
250+
251+
// 4. Store in yaad
252+
_, err = eng.Remember(ctx, engine.RememberInput{
253+
Content: fmt.Sprintf("Review: %d findings in app.go", len(reviewResult.Findings)),
254+
Type: "bug",
255+
Project: "/test",
256+
})
257+
if err != nil {
258+
t.Fatalf("Remember: %v", err)
259+
}
260+
261+
// 5. Use inspect on a test server
262+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
263+
w.Header().Set("Content-Type", "text/html")
264+
fmt.Fprint(w, `<!DOCTYPE html><html><head><title>OK</title></head><body>OK</body></html>`)
265+
}))
266+
defer ts.Close()
267+
268+
report, err := inspect.Scan(ctx, ts.URL, inspect.Quick)
269+
if err != nil {
270+
t.Fatalf("inspect.Scan: %v", err)
271+
}
272+
273+
// 6. Store inspect result
274+
_, err = eng.Remember(ctx, engine.RememberInput{
275+
Content: fmt.Sprintf("Audit %s: %d pages, %d issues", ts.URL, report.Stats.PagesScanned, report.Stats.FindingsTotal),
276+
Type: "spec",
277+
Project: "/test",
278+
})
279+
if err != nil {
280+
t.Fatalf("Remember audit: %v", err)
281+
}
282+
283+
// 7. Use cascade routing to determine model
284+
roles := routing.ModelRoles{Planner: "opus", Coder: "sonnet", Commit: "haiku", Reviewer: "sonnet"}
285+
cr := routing.NewCascadeRouter(roles)
286+
model := cr.Route("Summarize what we did today", "")
287+
if model != "haiku" {
288+
t.Fatalf("expected haiku for summary, got %s", model)
289+
}
290+
291+
// 8. Verify all data is recallable
292+
status, err := eng.Status(ctx, "/test")
293+
if err != nil {
294+
t.Fatalf("Status: %v", err)
295+
}
296+
if status.Nodes < 2 {
297+
t.Fatalf("expected at least 2 nodes, got %d", status.Nodes)
298+
}
299+
300+
// 9. Recall and verify cross-module data flows
301+
recalled, err := eng.Recall(ctx, engine.RecallOpts{Query: "findings", Project: "/test", Limit: 10})
302+
if err != nil {
303+
t.Fatalf("Recall: %v", err)
304+
}
305+
if len(recalled.Nodes) == 0 {
306+
t.Fatal("expected recalled nodes from full pipeline")
307+
}
308+
}

0 commit comments

Comments
 (0)