Skip to content

Commit e983bbe

Browse files
committed
test(yaad): split integration_test.go into feature and api files
Mechanically split integration_test.go (1027 LOC) into three same-package test files, moving tests verbatim with no behavior changes and preserving the //go:build integration tag and //nolint:noctx directive: - integration_test.go: setup helper plus core remember/recall/graph/phase 3-5 tests - integration_features_test.go: skills, benchmark, profile, conflict, temporal, dedup, compaction, mental-model, intent, and privacy tests - integration_api_test.go: utils, edge-case, TLS, config, storage, REST API, and concurrency tests Verified with go vet, go test, and golangci-lint (all with -tags=integration).
1 parent bac2e74 commit e983bbe

3 files changed

Lines changed: 721 additions & 681 deletions

File tree

integration_api_test.go

Lines changed: 363 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,363 @@
1+
//go:build integration
2+
3+
//nolint:noctx
4+
package yaad_test
5+
6+
import (
7+
"bytes"
8+
"context"
9+
"encoding/json"
10+
"fmt"
11+
"net/http"
12+
"net/http/httptest"
13+
"os"
14+
"path/filepath"
15+
"strings"
16+
"sync"
17+
"testing"
18+
"time"
19+
20+
"github.com/GrayCodeAI/yaad/config"
21+
"github.com/GrayCodeAI/yaad/engine"
22+
"github.com/GrayCodeAI/yaad/internal/server"
23+
yaadtls "github.com/GrayCodeAI/yaad/internal/tls"
24+
"github.com/GrayCodeAI/yaad/profile"
25+
"github.com/GrayCodeAI/yaad/storage"
26+
"github.com/GrayCodeAI/yaad/utils"
27+
)
28+
29+
// This file is part of the yaad_test integration suite. It holds the utils,
30+
// edge-case, profile-merge, TLS, config, storage, REST API, and concurrency
31+
// tests moved verbatim out of integration_test.go for readability; behavior
32+
// is unchanged.
33+
34+
func TestUtilsShortID(t *testing.T) {
35+
cases := []struct{ input, expected string }{
36+
{"abcdefghijklmnop", "abcdefgh"},
37+
{"short", "short"},
38+
{"12345678", "12345678"},
39+
{"", ""},
40+
{"ab", "ab"},
41+
}
42+
for _, c := range cases {
43+
got := utils.ShortID(c.input)
44+
if got != c.expected {
45+
t.Errorf("ShortID(%q) = %q, want %q", c.input, got, c.expected)
46+
}
47+
}
48+
}
49+
50+
func TestEdgeCaseEmptyRecall(t *testing.T) {
51+
eng, cleanup := setup(t)
52+
defer cleanup()
53+
54+
// Recall on empty DB should return empty, not error
55+
result, err := eng.Recall(context.Background(), engine.RecallOpts{Query: "nonexistent", Limit: 5})
56+
if err != nil {
57+
t.Fatal(err)
58+
}
59+
if len(result.Nodes) != 0 {
60+
t.Errorf("empty recall: expected 0 nodes, got %d", len(result.Nodes))
61+
}
62+
}
63+
64+
func TestEdgeCaseContextEmpty(t *testing.T) {
65+
eng, cleanup := setup(t)
66+
defer cleanup()
67+
68+
// Context on empty DB should return empty, not error
69+
result, err := eng.Context(context.Background(), "")
70+
if err != nil {
71+
t.Fatal(err)
72+
}
73+
if result == nil {
74+
t.Error("context: should return empty result, not nil")
75+
}
76+
}
77+
78+
func TestEdgeCaseForgetNonexistent(t *testing.T) {
79+
eng, cleanup := setup(t)
80+
defer cleanup()
81+
82+
// Forget nonexistent node should error gracefully
83+
err := eng.Forget(context.Background(), "nonexistent-id-12345678")
84+
if err == nil {
85+
t.Error("forget: should error on nonexistent node")
86+
}
87+
}
88+
89+
func TestProfileMerge(t *testing.T) {
90+
a := &profile.Profile{
91+
Project: "test",
92+
Static: []string{"Use jose", "Use NATS"},
93+
Dynamic: []string{"[task] rate limiting"},
94+
Stack: []string{"TypeScript", "NATS"},
95+
}
96+
b := &profile.Profile{
97+
Static: []string{"Prefer tabs", "Use jose"}, // "Use jose" is duplicate
98+
Dynamic: []string{"[bug] auth race"},
99+
Stack: []string{"PostgreSQL", "NATS"}, // "NATS" is duplicate
100+
}
101+
merged := profile.Merge(a, b)
102+
// Static should be deduped
103+
if len(merged.Static) != 3 { // jose, NATS, tabs
104+
t.Errorf("merge: expected 3 static, got %d: %v", len(merged.Static), merged.Static)
105+
}
106+
// Stack should be deduped
107+
if len(merged.Stack) != 3 { // TypeScript, NATS, PostgreSQL
108+
t.Errorf("merge: expected 3 stack, got %d: %v", len(merged.Stack), merged.Stack)
109+
}
110+
// Dynamic should be combined (not deduped)
111+
if len(merged.Dynamic) != 2 {
112+
t.Errorf("merge: expected 2 dynamic, got %d", len(merged.Dynamic))
113+
}
114+
}
115+
116+
func TestMultipleRememberAndRecall(t *testing.T) {
117+
eng, cleanup := setup(t)
118+
defer cleanup()
119+
120+
// Store 20 memories of different types
121+
types := []string{"convention", "decision", "bug", "spec", "task"}
122+
for i := 0; i < 20; i++ {
123+
eng.Remember(context.Background(), engine.RememberInput{
124+
Type: types[i%len(types)],
125+
Content: fmt.Sprintf("Memory item %d about topic %d", i, i%5),
126+
Scope: "project",
127+
})
128+
}
129+
130+
// Recall should find results
131+
result, err := eng.Recall(context.Background(), engine.RecallOpts{Query: "topic", Limit: 10})
132+
if err != nil {
133+
t.Fatal(err)
134+
}
135+
if len(result.Nodes) == 0 {
136+
t.Error("bulk recall: expected nodes")
137+
}
138+
t.Logf("Stored 20, recalled %d nodes", len(result.Nodes))
139+
140+
// Status should show correct counts
141+
st, _ := eng.Status(context.Background(), "")
142+
if st.Nodes < 20 {
143+
t.Errorf("status: expected ≥20 nodes, got %d", st.Nodes)
144+
}
145+
}
146+
147+
func TestTLSCertGeneration(t *testing.T) {
148+
dir := t.TempDir()
149+
cfg := yaadtls.Config{Enabled: true}
150+
151+
tlsCfg, err := yaadtls.TLSConfig(cfg, dir)
152+
if err != nil {
153+
t.Fatal(err)
154+
}
155+
if tlsCfg == nil {
156+
t.Fatal("tls: nil config returned")
157+
}
158+
if len(tlsCfg.Certificates) == 0 {
159+
t.Error("tls: no certificates generated")
160+
}
161+
162+
// Verify cert files were created
163+
if _, err := os.Stat(filepath.Join(dir, "cert.pem")); err != nil {
164+
t.Error("tls: cert.pem not created")
165+
}
166+
if _, err := os.Stat(filepath.Join(dir, "key.pem")); err != nil {
167+
t.Error("tls: key.pem not created")
168+
}
169+
}
170+
171+
func TestConfigDefaults(t *testing.T) {
172+
cfg := config.Default()
173+
if cfg.Server.Port != 3456 {
174+
t.Errorf("config: expected port 3456, got %d", cfg.Server.Port)
175+
}
176+
if cfg.Decay.HalfLifeDays != 30 {
177+
t.Errorf("config: expected half_life 30, got %d", cfg.Decay.HalfLifeDays)
178+
}
179+
}
180+
181+
func TestStorageCreateAndQuery(t *testing.T) {
182+
dir := t.TempDir()
183+
store, err := storage.NewStore(filepath.Join(dir, "test.db"))
184+
if err != nil {
185+
t.Fatal(err)
186+
}
187+
defer func() { store.Close() }()
188+
189+
ctx := context.Background()
190+
191+
// Create node
192+
node := &storage.Node{
193+
ID: "test-node-1", Type: "convention", Content: "Test content",
194+
ContentHash: "hash1", Scope: "project", Tier: 1, Confidence: 1.0, Version: 1,
195+
}
196+
if err := store.CreateNode(ctx, node); err != nil {
197+
t.Fatal(err)
198+
}
199+
200+
// Get node
201+
got, err := store.GetNode(ctx, "test-node-1")
202+
if err != nil {
203+
t.Fatal(err)
204+
}
205+
if got.Content != "Test content" {
206+
t.Errorf("storage: expected 'Test content', got '%s'", got.Content)
207+
}
208+
209+
// Create edge
210+
edge := &storage.Edge{
211+
ID: "test-edge-1", FromID: "test-node-1", ToID: "test-node-1",
212+
Type: "relates_to", Acyclic: false, Weight: 1.0,
213+
}
214+
if err := store.CreateEdge(ctx, edge); err != nil {
215+
t.Fatal(err)
216+
}
217+
218+
// Get neighbors
219+
neighbors, err := store.GetNeighbors(ctx, "test-node-1")
220+
if err != nil {
221+
t.Fatal(err)
222+
}
223+
if len(neighbors) == 0 {
224+
t.Error("storage: expected neighbors")
225+
}
226+
227+
// Version history
228+
if err := store.SaveVersion(ctx, "test-node-1", "old content", "test", "test update"); err != nil {
229+
t.Fatal(err)
230+
}
231+
versions, err := store.GetVersions(ctx, "test-node-1")
232+
if err != nil {
233+
t.Fatal(err)
234+
}
235+
if len(versions) == 0 {
236+
t.Error("storage: expected version history")
237+
}
238+
}
239+
240+
func TestRESTAPI(t *testing.T) {
241+
eng, cleanup := setup(t)
242+
defer cleanup()
243+
244+
mux := http.NewServeMux()
245+
rest := server.NewRESTServer(eng, "")
246+
rest.RegisterRoutes(mux)
247+
ts := httptest.NewServer(mux)
248+
defer ts.Close()
249+
250+
// POST /yaad/remember
251+
body, _ := json.Marshal(engine.RememberInput{
252+
Type: "convention", Content: "Always use TypeScript strict mode", Scope: "project",
253+
})
254+
resp, err := http.Post(ts.URL+"/yaad/remember", "application/json", bytes.NewReader(body))
255+
if err != nil {
256+
t.Fatal(err)
257+
}
258+
if resp.StatusCode != 201 {
259+
t.Errorf("remember: expected 201, got %d", resp.StatusCode)
260+
}
261+
var node storage.Node
262+
json.NewDecoder(resp.Body).Decode(&node)
263+
resp.Body.Close()
264+
if node.ID == "" {
265+
t.Error("remember: empty node ID")
266+
}
267+
268+
// POST /yaad/recall
269+
body, _ = json.Marshal(engine.RecallOpts{Query: "TypeScript", Limit: 5})
270+
resp, err = http.Post(ts.URL+"/yaad/recall", "application/json", bytes.NewReader(body))
271+
if err != nil {
272+
t.Fatal(err)
273+
}
274+
if resp.StatusCode != 200 {
275+
t.Errorf("recall: expected 200, got %d", resp.StatusCode)
276+
}
277+
var result engine.RecallResult
278+
json.NewDecoder(resp.Body).Decode(&result)
279+
resp.Body.Close()
280+
if len(result.Nodes) == 0 {
281+
t.Error("recall: expected nodes, got none")
282+
}
283+
284+
// GET /yaad/health
285+
resp, _ = http.Get(ts.URL + "/yaad/health")
286+
if resp.StatusCode != 200 {
287+
t.Errorf("health: expected 200, got %d", resp.StatusCode)
288+
}
289+
resp.Body.Close()
290+
291+
// GET /yaad/context
292+
resp, _ = http.Get(ts.URL + "/yaad/context")
293+
if resp.StatusCode != 200 {
294+
t.Errorf("context: expected 200, got %d", resp.StatusCode)
295+
}
296+
resp.Body.Close()
297+
}
298+
299+
// TestConcurrentSQLiteAccess verifies that concurrent Remember and Recall operations
300+
// against the real SQLite backend do not race or corrupt data.
301+
func TestConcurrentSQLiteAccess(t *testing.T) {
302+
eng, cleanup := setup(t)
303+
defer cleanup()
304+
305+
var wg sync.WaitGroup
306+
numWriters := 5
307+
numReaders := 5
308+
opsPerGoroutine := 10
309+
310+
// Writers
311+
for i := 0; i < numWriters; i++ {
312+
wg.Add(1)
313+
go func(idx int) {
314+
defer wg.Done()
315+
for j := 0; j < opsPerGoroutine; j++ {
316+
_, err := eng.Remember(context.Background(), engine.RememberInput{
317+
Type: "convention",
318+
Content: fmt.Sprintf("writer-%d-op-%d", idx, j),
319+
Scope: "project",
320+
Project: "concurrent-test",
321+
})
322+
if err != nil {
323+
// Under CI load, occasional SQLITE_BUSY is expected even with
324+
// _busy_timeout. Skip individual operations rather than failing.
325+
if strings.Contains(err.Error(), "database is locked") {
326+
time.Sleep(10 * time.Millisecond)
327+
continue
328+
}
329+
t.Errorf("writer %d op %d failed: %v", idx, j, err)
330+
}
331+
}
332+
}(i)
333+
}
334+
335+
// Readers
336+
for i := 0; i < numReaders; i++ {
337+
wg.Add(1)
338+
go func(idx int) {
339+
defer wg.Done()
340+
for j := 0; j < opsPerGoroutine; j++ {
341+
_, err := eng.Recall(context.Background(), engine.RecallOpts{
342+
Query: "writer",
343+
Project: "concurrent-test",
344+
Limit: 10,
345+
})
346+
if err != nil {
347+
t.Errorf("reader %d op %d failed: %v", idx, j, err)
348+
}
349+
}
350+
}(i)
351+
}
352+
353+
wg.Wait()
354+
355+
st, err := eng.Status(context.Background(), "concurrent-test")
356+
if err != nil {
357+
t.Fatalf("status failed: %v", err)
358+
}
359+
expectedNodes := numWriters * opsPerGoroutine
360+
if st.Nodes < expectedNodes {
361+
t.Errorf("expected at least %d nodes, got %d", expectedNodes, st.Nodes)
362+
}
363+
}

0 commit comments

Comments
 (0)