// 织忆 MemoryWeave — 集成测试 + 边界测试 package memoryweave_test import ( "bytes" "encoding/json" "fmt" "net/http" "net/http/httptest" "os" "sync" "testing" "time" "github.com/xiaoxue/memoryweave/internal/api" "github.com/xiaoxue/memoryweave/internal/governance" "github.com/xiaoxue/memoryweave/internal/selfoptimize" ) var testServer http.Handler func TestMain(m *testing.M) { testServer = api.NewServer() os.Exit(m.Run()) } // ─── 集成测试:全链路 ───────────────────────────────────── func TestIntegration_FullPipeline(t *testing.T) { t.Skip("requires running LanceDB instance — test in CI with LanceDB available") h := testServer body := func(v interface{}) *bytes.Reader { b, _ := json.Marshal(v) return bytes.NewReader(b) } header := map[string]string{ "Content-Type": "application/json", "X-API-Key": "zhiyi-dev-key-2026", } // Step 1: 提交记忆 commitBody := map[string]interface{}{ "agent_id": "hermes", "namespace": "shared", "content": "牧尘的系统使用 Deepin 25,不是 Arch Linux", "category": "system_fact", } req := httptest.NewRequest("POST", "/api/v1/commit", body(commitBody)) for k, v := range header { req.Header.Set(k, v) } w := httptest.NewRecorder() h.ServeHTTP(w, req) if w.Code != 201 { t.Fatalf("commit failed: %d %s", w.Code, w.Body.String()) } // Step 2: 提交第二条 commitBody2 := map[string]interface{}{ "agent_id": "hermes", "namespace": "shared", "content": "牧尘的 GPU 是 RTX 3050 Laptop,4GB 显存", "category": "system_fact", } req2 := httptest.NewRequest("POST", "/api/v1/commit", body(commitBody2)) for k, v := range header { req2.Header.Set(k, v) } w2 := httptest.NewRecorder() h.ServeHTTP(w2, req2) if w2.Code != 201 { t.Fatalf("commit2 failed: %d", w2.Code) } // Step 3: 召回 recallBody := map[string]interface{}{ "query": "牧尘的系统是什么", "limit": 5, "namespace": "shared", } req3 := httptest.NewRequest("POST", "/api/v1/recall", body(recallBody)) for k, v := range header { req3.Header.Set(k, v) } w3 := httptest.NewRecorder() h.ServeHTTP(w3, req3) if w3.Code != 200 { t.Errorf("recall failed: %d %s", w3.Code, w3.Body.String()) } // Step 4: 统计 req4 := httptest.NewRequest("GET", "/api/v1/stats", nil) for k, v := range header { req4.Header.Set(k, v) } w4 := httptest.NewRecorder() h.ServeHTTP(w4, req4) if w4.Code != 200 { t.Errorf("stats failed: %d", w4.Code) } // Step 5: Bootstrap req5 := httptest.NewRequest("GET", "/api/v1/bootstrap?agent_id=hermes", nil) for k, v := range header { req5.Header.Set(k, v) } w5 := httptest.NewRecorder() h.ServeHTTP(w5, req5) if w5.Code == 0 { t.Error("bootstrap got empty response") } // Step 6: 反馈 feedBody := map[string]string{"memory_id": "test_001"} req6 := httptest.NewRequest("POST", "/api/v1/feedback/useful", body(feedBody)) for k, v := range header { req6.Header.Set(k, v) } w6 := httptest.NewRecorder() h.ServeHTTP(w6, req6) if w6.Code != 200 { t.Errorf("feedback failed: %d", w6.Code) } } func TestIntegration_GraphFlow(t *testing.T) { h := testServer header := map[string]string{ "Content-Type": "application/json", "X-API-Key": "zhiyi-dev-key-2026", } // Graph stats req := httptest.NewRequest("GET", "/api/v1/graph/stats", nil) for k, v := range header { req.Header.Set(k, v) } w := httptest.NewRecorder() h.ServeHTTP(w, req) if w.Code != 200 { t.Errorf("graph stats: %d", w.Code) } // Graph navigate navBody := map[string]interface{}{"entity": "Docker", "max_hops": 2} req2 := httptest.NewRequest("POST", "/api/v1/graph/navigate", bytesBody(navBody)) for k, v := range header { req2.Header.Set(k, v) } w2 := httptest.NewRecorder() h.ServeHTTP(w2, req2) if w2.Code != 200 { t.Errorf("graph navigate: %d %s", w2.Code, w2.Body.String()) } } func TestIntegration_AgentRegisterFlow(t *testing.T) { h := testServer header := map[string]string{ "Content-Type": "application/json", "X-API-Key": "zhiyi-dev-key-2026", } // Register regBody := map[string]string{"agent_id": "integration-test-agent"} req := httptest.NewRequest("POST", "/api/v1/agents/register", bytesBody(regBody)) for k, v := range header { req.Header.Set(k, v) } w := httptest.NewRecorder() h.ServeHTTP(w, req) if w.Code != 201 { t.Fatalf("register failed: %d %s", w.Code, w.Body.String()) } var info struct { APIKey string `json:"api_key"` } json.NewDecoder(w.Body).Decode(&info) if info.APIKey == "" { t.Error("no API key in response") } // List req2 := httptest.NewRequest("GET", "/api/v1/agents", nil) for k, v := range header { req2.Header.Set(k, v) } w2 := httptest.NewRecorder() h.ServeHTTP(w2, req2) if w2.Code != 200 { t.Errorf("agent list: %d", w2.Code) } } // ─── 边界测试 ───────────────────────────────────────────── func TestEdge_EmptyCommit(t *testing.T) { h := testServer body := map[string]interface{}{"agent_id": "", "content": ""} req := httptest.NewRequest("POST", "/api/v1/commit", bytesBody(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("X-API-Key", "zhiyi-dev-key-2026") w := httptest.NewRecorder() h.ServeHTTP(w, req) if w.Code != 400 { t.Errorf("empty commit should return 400, got %d", w.Code) } } func TestEdge_NoAuth(t *testing.T) { h := testServer req := httptest.NewRequest("GET", "/api/v1/stats", nil) w := httptest.NewRecorder() h.ServeHTTP(w, req) if w.Code != 401 { t.Errorf("no auth should return 401, got %d", w.Code) } } func TestEdge_HealthNoAuth(t *testing.T) { h := testServer req := httptest.NewRequest("GET", "/health", nil) w := httptest.NewRecorder() h.ServeHTTP(w, req) if w.Code != 200 { t.Errorf("/health should return 200 without auth, got %d", w.Code) } } func TestEdge_LargePayload(t *testing.T) { // 10KB 内容提交 largeContent := make([]byte, 10000) for i := range largeContent { largeContent[i] = 'x' } h := testServer body := map[string]interface{}{ "agent_id": "test", "content": string(largeContent), "namespace": "shared", } req := httptest.NewRequest("POST", "/api/v1/commit", bytesBody(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("X-API-Key", "zhiyi-dev-key-2026") w := httptest.NewRecorder() h.ServeHTTP(w, req) // Should not crash if w.Code == 0 { t.Error("large payload caused empty response") } } func TestEdge_ConcurrentCommits(t *testing.T) { t.Skip("requires running LanceDB instance — test in CI with LanceDB available") h := testServer var wg sync.WaitGroup errs := make(chan error, 20) for i := 0; i < 20; i++ { wg.Add(1) go func(idx int) { defer wg.Done() body := map[string]interface{}{ "agent_id": fmt.Sprintf("agent-%d", idx), "namespace": "shared", "content": fmt.Sprintf("concurrent test message %d", idx), "category": "test", } req := httptest.NewRequest("POST", "/api/v1/commit", bytesBody(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("X-API-Key", "zhiyi-dev-key-2026") w := httptest.NewRecorder() h.ServeHTTP(w, req) if w.Code != 201 { errs <- fmt.Errorf("goroutine %d: expected 201, got %d", idx, w.Code) } }(i) } wg.Wait() close(errs) for err := range errs { t.Error(err) } } func TestEdge_GapDetectionThreshold(t *testing.T) { gd := selfoptimize.NewGapDetector(nil, nil) // 2 misses — no gap if gap := gd.RecordMiss("test"); gap != nil { t.Error("gap should not trigger at 1 miss") } if gap := gd.RecordMiss("test"); gap != nil { t.Error("gap should not trigger at 2 misses") } // 3rd miss — gap gap := gd.RecordMiss("test") if gap == nil { t.Fatal("gap should trigger at 3 misses") } if gap.MissCount != 3 { t.Errorf("expected 3 misses, got %d", gap.MissCount) } } func TestEdge_GraphEmpty(t *testing.T) { g := governance.NewInMemoryGraph() nodes, edges, density := g.Stats() if nodes != 0 || edges != 0 || density != 0 { t.Error("empty graph should return all zeros") } paths, err := g.Navigate("nonexistent", 2, "shared", nil) if err != nil { t.Errorf("navigate on empty graph should not error: %v", err) } if len(paths) != 0 { t.Errorf("expected 0 paths on empty graph, got %d", len(paths)) } } func TestEdge_TriggersFired(t *testing.T) { h := testServer header := map[string]string{ "Content-Type": "application/json", "X-API-Key": "zhiyi-dev-key-2026", } // 触发器 ID 与 routes.Triggers 一致(t1..t8 是 executor 的内部编号,不是 API ID) body := map[string]string{"trigger_id": "t_decay"} req := httptest.NewRequest("POST", "/api/v1/triggers/fire", bytesBody(body)) for k, v := range header { req.Header.Set(k, v) } w := httptest.NewRecorder() h.ServeHTTP(w, req) if w.Code != 200 { t.Errorf("trigger fire: expected 200, got %d", w.Code) } } // ─── 辅助 ───────────────────────────────────────────────── func bytesBody(v interface{}) *bytes.Reader { b, _ := json.Marshal(v) return bytes.NewReader(b) } // Avoid import cycle — this file is in package zhiyid_test, not routes func init() { time.Local = time.UTC }