363 lines
9.1 KiB
Go
363 lines
9.1 KiB
Go
// 织忆 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()
|
||
|
||
// 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
|
||
}
|