mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-05 16:57:32 +00:00
refactor(webui): 图谱判据迁到 test/ —— 导出 GraphDataForVisual 一层转换
graph_source_test.go → test/
handler_graph_pulse_test.go → test/ (7 条判据)
webui/ 174 → 167
webui/test/ 28 → 35
合计 202 不变
## 这一条必须动生产代码,理由要写清
判据想测的是「GraphData 的 raw map 被裁成可视化视图时,稠密向量必须被剥掉」。
两条路都不通:
走 HTTP 端点 /api/v1/memory/graph
→ 端点输入来自 h.memory.GraphData(),h.memory 是私有字段,
包外无法注入合成数据。测不了。
包内测试直接调 graphDataForVisual
→ 这正是要摆脱的白盒写法。
所以导出这一层转换 `graphDataForVisual → GraphDataForVisual`。它不是
为测试造出来的形状:函数的输入输出本来就是本包唯一的「raw map →
visual map」语义,端点在 handler_memory.go:199 调它。导出的是真实边界,
不是内部细节。(判据 §5.3「不要为满足测试导出内部」指的是后者。)
顺带把 `graphBlockView`(私有 JSON 结构)在测试侧改成匿名结构体 ——
它只用于反序列化断言,没有必要引用包内类型。
## 改走真实路由暴露的两个事实
### ① `newGraphHandler` 直接设 `&Handler{memory: m}`
改成 `NewHandler(sdk.New("webui", sdk.SDKConfig{Memory: m, ...}))` ——
`Memory` 确实能经 SDKConfig 注入(plugin.go:457 `base.SetMemoryAPI(cfg.Memory)`)。
### ② 503 暴露了鉴权要求
改走路由后立刻:
status = 503, want 200
`requireAPI` 需要 api_key,而最小装配没配。补 `seedWebUIConfig` 后 200。
之前直接调 handler 的写法**从来不知道有这个要求** —— 路由层挡掉的东西,
绕过路由就看不见。
## 判据有效性(变异验证)
在 `GraphDataForVisual` 里把裁剪短路掉(`if blocks != nil { return out }`),
实测红,且输出直接暴露泄漏内容:
--- FAIL: TestHandleMemoryGraph_StripsVector
response still contains vector field:
{"data":{"memory_blocks":[{"id":"b_old",...,"vector":[0.1,0.2,0.3],...
"success":true}
稠密向量确实会被下发到前端。恢复后全绿。
## 验证
go build ./... PASS
go vet ./... PASS
gofmt -l 干净
go test ./... -count=1 67 包全过,0 FAIL
This commit is contained in:
@ -143,12 +143,18 @@ type graphBlockView struct {
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// graphDataForVisual 把 GraphData 的原始 map 裁成可视化视图(去掉稠密向量)。
|
||||
// GraphDataForVisual 把 GraphData 的原始 map 裁成可视化视图(去掉稠密向量)。
|
||||
//
|
||||
// ★ 导出(2026-11-04):这是本包唯一「输入 raw map → 输出 visual map」的
|
||||
// 纯转换,也是判据 `TestGraphDataExposesBlocksForStarmap` 想测的东西。
|
||||
// 走 HTTP 端点测不了它 —— 端点的输入来自 h.memory.GraphData(),而 h.memory
|
||||
// 是私有字段,包外无法注入合成数据。导出这层转换比导出整个 Handler 的
|
||||
// 装配更小、也更贴合它真实的对外语义。
|
||||
//
|
||||
// 用 map 断言而不是泛型/反射:GraphData 返回 map[string]interface{},
|
||||
// 里面的具体类型是包内私有的 graphEntity/[]*memory.MemoryBlock,
|
||||
// 断言不中就原样透传(宁可多发也不让接口挂掉)。
|
||||
func graphDataForVisual(data map[string]interface{}) map[string]interface{} {
|
||||
func GraphDataForVisual(data map[string]interface{}) map[string]interface{} {
|
||||
out := make(map[string]interface{}, len(data))
|
||||
for k, v := range data {
|
||||
out[k] = v
|
||||
@ -196,7 +202,7 @@ func (h *Handler) handleMemoryGraph(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{"success": true, "data": graphDataForVisual(data)})
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{"success": true, "data": GraphDataForVisual(data)})
|
||||
}
|
||||
|
||||
// handleMemoryGraphPulse 是给星图「跟随 agent 动」用的**轻量**活动端点。
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
package webui
|
||||
package test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/JianFeeeee/HologramAgent/internal/memory"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/plugins/webui"
|
||||
)
|
||||
|
||||
// 星图数据源必须来自块体系(memory_blocks),而不是停写的旧表 entities。
|
||||
@ -26,14 +27,21 @@ func TestGraphDataExposesBlocksForStarmap(t *testing.T) {
|
||||
{ID: 2, SourceKind: "block", SourceID: "blk_src_a", TargetKind: "block", TargetID: "blk_ent_c", Type: "contains"},
|
||||
},
|
||||
}
|
||||
out := graphDataForVisual(data)
|
||||
out := webui.GraphDataForVisual(data)
|
||||
// 前端读的是 memory_blocks 字段
|
||||
blocks, ok := out["memory_blocks"]
|
||||
if !ok {
|
||||
t.Fatal("GraphData 必须下发 memory_blocks(前端星图的真实数据源)")
|
||||
}
|
||||
raw, _ := json.Marshal(blocks)
|
||||
var got []graphBlockView
|
||||
// 只用 JSON 结构断言下发内容,不引用包内私有类型。
|
||||
var got []struct {
|
||||
ID string `json:"id"`
|
||||
Modality string `json:"modality"`
|
||||
Text string `json:"text"`
|
||||
HasData bool `json:"has_data"`
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package webui
|
||||
package test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@ -8,7 +8,10 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
internalConfig "github.com/JianFeeeee/HologramAgent/internal/config"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/events"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/memory"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/plugins/webui"
|
||||
sdk "github.com/JianFeeeee/HologramAgent/internal/sdk"
|
||||
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
|
||||
)
|
||||
@ -90,8 +93,26 @@ func graphFixture() map[string]interface{} {
|
||||
}
|
||||
}
|
||||
|
||||
func newGraphHandler(m *fakeGraphMemory) *Handler {
|
||||
return &Handler{memory: m}
|
||||
// newGraphHandler 通过**导出入口**装配 Handler:Memory 经 SDKConfig 注入,
|
||||
// 不再直接设 h.memory 私有字段。
|
||||
func newGraphHandler(t *testing.T, m *fakeGraphMemory) *webui.Handler {
|
||||
t.Helper()
|
||||
reg := internalConfig.NewConfigRegistry("")
|
||||
seedWebUIConfig(reg)
|
||||
return webui.NewHandler(sdk.New("webui", sdk.SDKConfig{
|
||||
Memory: m,
|
||||
Settings: sdk.NewSettings("webui", reg),
|
||||
EventBus: events.NewBus(),
|
||||
}))
|
||||
}
|
||||
|
||||
// serveGraph 经导出的 RegisterRoutes 分发请求。
|
||||
func serveGraph(t *testing.T, h *webui.Handler, w http.ResponseWriter, r *http.Request) {
|
||||
t.Helper()
|
||||
r.Header.Set("X-API-Key", testAPIKey)
|
||||
mux := http.NewServeMux()
|
||||
h.RegisterRoutes(mux)
|
||||
mux.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// TestHandleMemoryGraph_StripsVector 钉住「不下发稠密向量」。
|
||||
@ -99,9 +120,9 @@ func newGraphHandler(m *fakeGraphMemory) *Handler {
|
||||
// 生产实测:8 块记忆的 vector 占 79,314 B / 408,146 B = 19%,而星图
|
||||
// (本接口唯一消费者)从不读 vector。这部分纯属白付带宽 + 堆内存。
|
||||
func TestHandleMemoryGraph_StripsVector(t *testing.T) {
|
||||
h := newGraphHandler(&fakeGraphMemory{data: graphFixture()})
|
||||
h := newGraphHandler(t, &fakeGraphMemory{data: graphFixture()})
|
||||
rr := httptest.NewRecorder()
|
||||
h.handleMemoryGraph(rr, httptest.NewRequest(http.MethodGet, "/api/v1/memory/graph", nil))
|
||||
serveGraph(t, h, rr, httptest.NewRequest(http.MethodGet, "/api/v1/memory/graph", nil))
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200", rr.Code)
|
||||
}
|
||||
@ -120,10 +141,10 @@ func TestHandleMemoryGraph_StripsVector(t *testing.T) {
|
||||
//
|
||||
// 这是星图「新记忆生长」的数据源;它必须比全量图谱小两个数量级。
|
||||
func TestHandleMemoryGraphPulse_OnlyRecent(t *testing.T) {
|
||||
h := newGraphHandler(&fakeGraphMemory{data: graphFixture()})
|
||||
h := newGraphHandler(t, &fakeGraphMemory{data: graphFixture()})
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/memory/graph/pulse", nil)
|
||||
h.handleMemoryGraphPulse(rr, req)
|
||||
serveGraph(t, h, rr, req)
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200", rr.Code)
|
||||
}
|
||||
@ -153,10 +174,10 @@ func TestHandleMemoryGraphPulse_OnlyRecent(t *testing.T) {
|
||||
|
||||
// TestHandleMemoryGraphPulse_SinceWidens 钉住 since 参数能放大窗口。
|
||||
func TestHandleMemoryGraphPulse_SinceWidens(t *testing.T) {
|
||||
h := newGraphHandler(&fakeGraphMemory{data: graphFixture()})
|
||||
h := newGraphHandler(t, &fakeGraphMemory{data: graphFixture()})
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/memory/graph/pulse?since=86400", nil)
|
||||
h.handleMemoryGraphPulse(rr, req)
|
||||
serveGraph(t, h, rr, req)
|
||||
var resp struct {
|
||||
Data struct {
|
||||
Nodes []struct {
|
||||
@ -174,9 +195,9 @@ func TestHandleMemoryGraphPulse_SinceWidens(t *testing.T) {
|
||||
|
||||
// TestHandleMemoryGraphPulse_NoVector 钉住 pulse 响应同样不带 vector。
|
||||
func TestHandleMemoryGraphPulse_NoVector(t *testing.T) {
|
||||
h := newGraphHandler(&fakeGraphMemory{data: graphFixture()})
|
||||
h := newGraphHandler(t, &fakeGraphMemory{data: graphFixture()})
|
||||
rr := httptest.NewRecorder()
|
||||
h.handleMemoryGraphPulse(rr, httptest.NewRequest(http.MethodGet, "/api/v1/memory/graph/pulse", nil))
|
||||
serveGraph(t, h, rr, httptest.NewRequest(http.MethodGet, "/api/v1/memory/graph/pulse", nil))
|
||||
if bytes.Contains(rr.Body.Bytes(), []byte(`"vector"`)) {
|
||||
t.Fatalf("pulse response contains vector:\n%s", rr.Body.String())
|
||||
}
|
||||
@ -192,7 +213,7 @@ func TestGraphDataForVisual_PassthroughWhenTypeMismatch(t *testing.T) {
|
||||
"nodes": []map[string]interface{}{{"id": 1}},
|
||||
"memory_blocks": []*memory.MemoryBlock{{ID: "x"}},
|
||||
}
|
||||
out := graphDataForVisual(in)
|
||||
out := webui.GraphDataForVisual(in)
|
||||
if out["memory_blocks"] == nil {
|
||||
t.Fatalf("memory_blocks dropped on type mismatch")
|
||||
}
|
||||
@ -237,9 +258,9 @@ func TestGraphPulse_IgnoresFrozenLegacyNodes(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
h := newGraphHandler(&fakeGraphMemory{data: fixture})
|
||||
h := newGraphHandler(t, &fakeGraphMemory{data: fixture})
|
||||
rr := httptest.NewRecorder()
|
||||
h.handleMemoryGraphPulse(rr, httptest.NewRequest(http.MethodGet, "/api/v1/memory/graph/pulse", nil))
|
||||
serveGraph(t, h, rr, httptest.NewRequest(http.MethodGet, "/api/v1/memory/graph/pulse", nil))
|
||||
|
||||
var resp struct {
|
||||
Success bool `json:"success"`
|
||||
Reference in New Issue
Block a user