325 lines
10 KiB
Go
325 lines
10 KiB
Go
// 织忆 MemoryWeave — 路由层单元测试
|
||
package routes
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/json"
|
||
"net/http/httptest"
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/xiaoxue/memoryweave/internal/governance"
|
||
"github.com/xiaoxue/memoryweave/internal/selfoptimize"
|
||
)
|
||
|
||
// ─── 核心 API(mock 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 重构删除,改用当前 API(PushGapDetected/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)
|
||
}
|