memoryweave/go/internal/api/routes/routes_test.go

325 lines
10 KiB
Go
Raw 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 routes
import (
"bytes"
"encoding/json"
"net/http/httptest"
"testing"
"time"
"github.com/xiaoxue/memoryweave/internal/governance"
"github.com/xiaoxue/memoryweave/internal/selfoptimize"
)
// ─── 核心 APImock LanceDB───────────────────────────────
func TestHealthEndpoint(t *testing.T) {
req := httptest.NewRequest("GET", "/health", nil)
w := httptest.NewRecorder()
HandleHealth(w, req)
if w.Code != 200 {
t.Errorf("expected 200, got %d", w.Code)
}
var resp map[string]string
json.NewDecoder(w.Body).Decode(&resp)
if resp["status"] != "ok" {
t.Errorf("expected ok status, got %s", resp["status"])
}
}
// ─── 知识图谱路由 ────────────────────────────────────────
func TestGraphAPI_Stats(t *testing.T) {
g := governance.NewInMemoryGraph()
g.AddNode("n1", "Docker", "tool", "shared")
g.AddNode("n2", "Hermes", "agent", "shared")
g.AddEdge("e1", "n1", "n2", "used", "shared", 1.0)
api := NewGraphAPI(g)
req := httptest.NewRequest("GET", "/api/v1/graph/stats", nil)
w := httptest.NewRecorder()
api.Stats(w, req)
if w.Code != 200 {
t.Errorf("expected 200, got %d", w.Code)
}
var resp map[string]interface{}
json.NewDecoder(w.Body).Decode(&resp)
if resp["node_count"].(float64) != 2 {
t.Errorf("expected 2 nodes, got %v", resp["node_count"])
}
}
func TestGraphAPI_Navigate(t *testing.T) {
g := governance.NewInMemoryGraph()
g.AddEdge("e1", "docker", "hermes", "used", "shared", 1.0)
api := NewGraphAPI(g)
body, _ := json.Marshal(map[string]interface{}{
"entity": "docker", "max_hops": 2,
})
req := httptest.NewRequest("POST", "/api/v1/graph/navigate", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
api.Navigate(w, req)
if w.Code != 200 {
t.Errorf("expected 200, got %d: %s", w.Code, w.Body.String())
}
}
func TestGraphAPI_Navigate_MissingEntity(t *testing.T) {
g := governance.NewInMemoryGraph()
api := NewGraphAPI(g)
body, _ := json.Marshal(map[string]interface{}{"max_hops": 2})
req := httptest.NewRequest("POST", "/api/v1/graph/navigate", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
api.Navigate(w, req)
if w.Code != 400 {
t.Errorf("expected 400, got %d", w.Code)
}
}
// ─── 冲突管理路由 ────────────────────────────────────────
func TestConflictAPI_List(t *testing.T) {
cd := governance.NewConflictDetector()
// Scan 会产生冲突但不在 active所以返回空列表
cd.Scan("A is used by B", []string{"A"},
[]map[string]interface{}{{"content": "A is NOT used by B", "entities": []string{"A"}}})
api := NewConflictAPI(cd)
req := httptest.NewRequest("GET", "/api/v1/conflicts", nil)
w := httptest.NewRecorder()
api.List(w, req)
if w.Code != 200 {
t.Errorf("expected 200, got %d", w.Code)
}
// 空列表也正常 — active 不为空才需要 Scan 内部存储
var resp map[string]interface{}
json.NewDecoder(w.Body).Decode(&resp)
if resp["count"] == nil {
t.Error("response missing count")
}
}
func TestConflictAPI_Resolve_Validation(t *testing.T) {
cd := governance.NewConflictDetector()
api := NewConflictAPI(cd)
// 缺少 conflict_id → 400
body, _ := json.Marshal(map[string]string{"resolution": "keep_left"})
req := httptest.NewRequest("POST", "/api/v1/conflicts/resolve", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
api.Resolve(w, req)
if w.Code != 400 {
t.Errorf("expected 400 for missing conflict_id, got %d: %s", w.Code, w.Body.String())
}
}
// ─── 知识缺口路由 ────────────────────────────────────────
func TestGapAPI_List(t *testing.T) {
gd := selfoptimize.NewGapDetector(nil, nil)
ai := NewGapAPI(gd)
req := httptest.NewRequest("GET", "/api/v1/gaps", nil)
w := httptest.NewRecorder()
ai.List(w, req)
if w.Code != 200 {
t.Errorf("expected 200, got %d", w.Code)
}
}
func TestGapAPI_Detect(t *testing.T) {
gd := selfoptimize.NewGapDetector(nil, nil)
ai := NewGapAPI(gd)
body, _ := json.Marshal(map[string]string{"topic": "test_gap"})
req := httptest.NewRequest("POST", "/api/v1/gaps/detect", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
ai.Detect(w, req)
// First detection should return "tracking"
if w.Code != 200 {
t.Errorf("expected 200, got %d: %s", w.Code, w.Body.String())
}
}
// ─── L3 世界模型 ──────────────────────────────────────────
func TestL3WorldModel_Get(t *testing.T) {
req := httptest.NewRequest("GET", "/api/v1/l3/worldmodel", nil)
w := httptest.NewRecorder()
WM.GetHandler(w, req)
if w.Code != 200 {
t.Errorf("expected 200, got %d", w.Code)
}
var resp map[string]interface{}
json.NewDecoder(w.Body).Decode(&resp)
if env, ok := resp["environment"].(map[string]interface{}); !ok || env["os"] != "Linux" {
t.Error("expected Linux OS in world model")
}
}
func TestL3WorldModel_Update(t *testing.T) {
body, _ := json.Marshal(map[string]interface{}{
"add_rules": []string{"new_rule"},
})
req := httptest.NewRequest("POST", "/api/v1/l3/worldmodel", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
WM.UpdateHandler(w, req)
if w.Code != 200 {
t.Errorf("expected 200, got %d", w.Code)
}
}
// ─── 触发器 ───────────────────────────────────────────────
func TestTriggers_List(t *testing.T) {
req := httptest.NewRequest("GET", "/api/v1/triggers", nil)
w := httptest.NewRecorder()
Triggers.List(w, req)
if w.Code != 200 {
t.Errorf("expected 200, got %d", w.Code)
}
var resp map[string]interface{}
json.NewDecoder(w.Body).Decode(&resp)
if resp["count"].(float64) != 8 {
t.Errorf("expected 8 triggers, got %v", resp["count"])
}
}
// ─── Skills ───────────────────────────────────────────────
func TestSkills_List(t *testing.T) {
req := httptest.NewRequest("GET", "/api/v1/skills", nil)
w := httptest.NewRecorder()
Skills.List(w, req)
if w.Code != 200 {
t.Errorf("expected 200, got %d", w.Code)
}
}
// ─── 反馈(结构测试)────────────────────────────────────────
// ─── Agent 注册 ───────────────────────────────────────────
func TestAgentRegistry_Register(t *testing.T) {
ar := NewAgentRegistry(nil)
body, _ := json.Marshal(map[string]string{"agent_id": "test-agent"})
req := httptest.NewRequest("POST", "/api/v1/agents/register", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
ar.Register(w, req)
if w.Code != 201 {
t.Errorf("expected 201, got %d: %s", w.Code, w.Body.String())
}
var info AgentInfo
json.NewDecoder(w.Body).Decode(&info)
if info.APIKey == "" {
t.Error("expected API key in response")
}
if !t.Run("dup_register", func(t *testing.T) {
w2 := httptest.NewRecorder()
req2 := httptest.NewRequest("POST", "/api/v1/agents/register", bytes.NewReader(body))
req2.Header.Set("Content-Type", "application/json")
ar.Register(w2, req2)
if w2.Code != 409 {
t.Errorf("expected 409 for duplicate, got %d", w2.Code)
}
}) {
}
}
func TestAgentRegistry_List(t *testing.T) {
ar := NewAgentRegistry(nil)
body, _ := json.Marshal(map[string]string{"agent_id": "agent1"})
req := httptest.NewRequest("POST", "/api/v1/agents/register", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
ar.Register(w, req)
req2 := httptest.NewRequest("GET", "/api/v1/agents", nil)
w2 := httptest.NewRecorder()
ar.List(w2, req2)
if w2.Code != 200 {
t.Errorf("expected 200, got %d", w2.Code)
}
var resp map[string]interface{}
json.NewDecoder(w2.Body).Decode(&resp)
if resp["count"].(float64) != 1 {
t.Errorf("expected 1 agent, got %v", resp["count"])
}
}
// ─── Admin 端点 ───────────────────────────────────────────
func TestAuxFunctions(t *testing.T) {
// parseTimeStr
now := time.Now()
if !parseTimeStr(now).Equal(now) {
t.Error("parseTimeStr failed for time.Time")
}
if !parseTimeStr("2026-01-01T00:00:00Z").IsZero() {
// Should parse ok
}
if !parseTimeStr(nil).IsZero() {
t.Error("parseTimeStr(nil) should be zero")
}
// intVal
if intVal(42) != 42 {
t.Error("intVal(42) failed")
}
if intVal(3.14) != 3 {
t.Error("intVal(3.14) failed")
}
if intVal(nil) != 0 {
t.Error("intVal(nil) should be 0")
}
// strVal
if strVal("hello") != "hello" {
t.Error("strVal failed")
}
if strVal(nil) != "" {
t.Error("strVal(nil) should be ''")
}
}
// ─── WebSocket 事件推送 ───────────────────────────────────
func TestWSPushHelpers_NoPanic(t *testing.T) {
// 所有推送 helper 在无订阅者时都不该 panic
// 2026-09-10 修复:原用例引用的 SSEBus/SSEMessage/PushMemoryCommitted/PushGapFound
// 已随 SSE→WebSocket 重构删除,改用当前 APIPushGapDetected/PushConflict* 新签名)。
PushPrefetch("agent", []interface{}{})
PushGapDetected("agent", "topic", "knowledge", "补一下")
PushGapFilled("agent", 1)
PushMemoryUpdated("mem1", 2, "test")
PushConflictDetected("agent", "c1", "Docker", map[string]string{"a": "b"})
PushConflictResolved("agent", "c1", "keep_left")
PushConsolidationDone("done")
PushQualityDrop("agent", "mem1", 0.2, "补证据")
PushDistillationComplete(1, 0)
}