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:
JianFeeeee
2026-11-04 22:06:35 +08:00
parent 2e90c2cb5a
commit bb29096f60
3 changed files with 55 additions and 20 deletions

View File

@ -0,0 +1,54 @@
package test
import (
"encoding/json"
"testing"
"github.com/JianFeeeee/HologramAgent/internal/memory"
"github.com/JianFeeeee/HologramAgent/internal/plugins/webui"
)
// 星图数据源必须来自块体系(memory_blocks),而不是停写的旧表 entities。
//
// 判据:真实图谱里 memory_blocks 远多于 entities,且块侧仍在增长。
// 这里用合成数据钉住「有块时节点数 == 块数」,防未来又切回 nodes。
func TestGraphDataExposesBlocksForStarmap(t *testing.T) {
// 模拟后端 GraphData 的返回:旧表 1 个节点,块表 3 个块 + 2 条边
data := map[string]interface{}{
"nodes": []map[string]interface{}{{"id": int64(1), "name": "旧表实体", "type": "Concept"}},
"edges": []map[string]interface{}{},
"memory_blocks": []memory.MemoryBlock{
{ID: "blk_src_a", Modality: memory.BlockText, Text: "张三喜欢咖啡", Source: "sentence"},
{ID: "blk_ent_b", Modality: memory.BlockText, Text: "张三", Source: "triple", SemanticType: "Person"},
{ID: "blk_ent_c", Modality: memory.BlockText, Text: "咖啡", Source: "triple"},
},
"memory_block_edges": []memory.MemoryBlockEdge{
{ID: 1, SourceKind: "block", SourceID: "blk_src_a", TargetKind: "block", TargetID: "blk_ent_b", Type: "contains"},
{ID: 2, SourceKind: "block", SourceID: "blk_src_a", TargetKind: "block", TargetID: "blk_ent_c", Type: "contains"},
},
}
out := webui.GraphDataForVisual(data)
// 前端读的是 memory_blocks 字段
blocks, ok := out["memory_blocks"]
if !ok {
t.Fatal("GraphData 必须下发 memory_blocks(前端星图的真实数据源)")
}
raw, _ := json.Marshal(blocks)
// 只用 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)
}
if len(got) != 3 {
t.Fatalf("memory_blocks 应下发 3 个,实际 %d", len(got))
}
if _, hasNodes := out["nodes"]; !hasNodes {
t.Fatal("旧 nodes 字段应保留(向后兼容),只是前端不再依赖它")
}
}