memoryweave/go/integration_test.go

363 lines
9.1 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 织忆 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 Laptop4GB 显存",
"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()
// 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")
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",
}
body := map[string]string{"trigger_id": "t1"}
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
}