Commit Graph

2 Commits

Author SHA1 Message Date
443110506c test: 按模块导出接口重写黑盒判据(批次 3:memory / agent-core)
延续批次 1/2 的形态:只调模块**本来就有**的导出接口,输入 → 可观察输出。
2 个文件全部在 `<module>/test/`,源码目录仍零 `_test.go`。
共 17 条判据。

## internal/memory(10 条)

纯函数(输入文本 → 输出):

    TestCleanText                     空白/「,」前缀(实现剥两次)
    TestChannelScene                  "qq" → "chan:qq",空 → 空
    TestFilterNoiseTriples            噪音主语/宾语被过滤,且不改输入切片
    TestSentenceBlockIDIsStable       同文本同 ID
    TestNewSentenceBlockFields        Trim 后的 Text 与 ID
    TestBuildCentroid                 中心向量均值 = 输入算术均值

GraphDB 真实构造 → 写入 → 可观察输出:

    TestCommitThenGraphData           Commit → GraphData 给出 nodes 结构
    TestReadOnlyHandleRejectsWrite    OpenGraphDBReadOnly 在 SQLite 层拒写

`TestReadOnlyHandleRejectsWrite` 守的是「子 agent 改不了主记忆」的
**结构性**保证(PRAGMA query_only=1),不是靠调用方自觉。

**判据写错过两次,按真实契约修正**:
① `CleanText` 连剥两次「,」,第一版按"只剥一次"写 ⇒ 红。
② 只读句柄的第一版三元组用了单字符实体名,被 `validEntityName`
   提前拒掉、根本走不到 SQL 层 ⇒ 假绿。换成合法实体名后才真正验到
   query_only。这个坑值得记:**判据的输入必须能走到被测路径**。

## internal/agent/core(7 条)

    TestAgentID                       New(AgentConfig{ID}) → ID()
    TestOutputAllowlist               空白名单=完整授权;非空=白名单
    TestDuplicateInputWindow          同源同内容第二次判重;异源/异内容否
    TestRealtimeFlag                  普通 agent=false;nil receiver 安全
    TestEmptyCollectionsOnFreshAgent  Residents/RTChildren/DumpScheduler 为空
    TestResolveOutputTargetWithoutRegistry  无注册表 → ok=false,不 panic
    TestGetKernelStatusNeverNil       状态查询返回可用对象

只用 `New(AgentConfig)` 装配,不启动事件循环(已核实 `New` 里没有
`go a.xxx` / `Start()`),不构造 TaskFrame、不调 stepLLM/runStage、
不碰 a.sched、不读源码。

## 变异验证(改回旧实现必须红)

    memory  CleanText 不剥前缀        → ★ CleanText(",,你好")=",你好"
    memory  只读句柄不设 query_only  → ★ 只读句柄接受了写入
    core    白名单恒放行             → ★ 白名单外的通道被放行
    core    去重恒 false             → ★ 同源同内容第二次未判重

全部还原后复跑全绿。

## 验证

    go build ./...                            PASS
    go vet ./...                              PASS
    gofmt -l                                  干净
    python3 .github/scripts/check-ci-gates.py PASS
    go test ./... -count=1                    14 个测试包 / 100 包无测试 / 0 FAIL
    2 个新包 -race                            全绿
    源码目录 _test.go                         0
2026-11-09 12:37:52 +08:00
84021ef408 refactor(agent/core): 3 个黑箱判据迁到 core/test + 去掉一处白盒
## 迁移了什么

    stages_test.go          → internal/agent/core/test/
    stages_plugin_test.go   → internal/agent/core/test/
    stages_tooldef_test.go  → internal/agent/core/test/

包名 `core` → `test`。`agent/core` 目录少 3 个测试文件(该目录原有
196 个文件,其中 144 个是测试 —— 这是分步收敛的第一批)。

## 顺带去掉一处白盒

`stages_plugin_test.go` 原本这样断言"stage 表已清空":

    h.mu.RLock()
    _, exists := h.stages[sdk.StageAfterToolcall]
    h.mu.RUnlock()

直接读**私有互斥锁和私有字段**。而 `UnregisterPluginStages` 的返回值
已经表达了同一件事。改为:

    if n := h.UnregisterPluginStages("solo"); n != 1 { ... }
    if n := h.UnregisterPluginStages("solo"); n != 0 { ... }

第二次摘除必须得 0 —— 如果内部表没清空,第二次仍会"摘到"残留计数。
这是黑箱可观测的等价断言。

变异验证:让 `UnregisterPluginStages` 假装摘除但不动表
(`if true { return 1 }`)⇒ 实测红:

    --- FAIL: TestUnregisterPluginStages_RemovesOnlyThatPlugin
    --- FAIL: TestUnregisterPluginStages_DropsEmptyStage
        ★ 重复摘除应得 0(说明内部已清空),实际 1

## 为什么只迁 3 个(agent/core 有 143 个测试文件)

先按「对源码未导出标识符的引用数」分档,实测:

    0 引用            11 个文件
    1 引用            约 20 个
    ... 其余逐步升高

但"0 引用"不等于"能迁" —— 逐个实测发现四种拦路虎:

  ① `newScrubRuntime` / `withScrubRuntime` / `newRTTestAgent` ——
     包内测试辅助,其中 `withScrubRuntime(a *Agent, rt)` 直接设私有字段
  ② `computeTokenBudget` —— 未导出方法(tokenbudget_test 必须留包内)
  ③ `filepath.Glob("*.go")` —— 扫描"当前目录源码"的判据
     (childkind_test),迁到子目录后会 glob 到空目录而**静默失效**
  ④ `newPreemptAgent` 等跨文件辅助

③ 尤其值得记:它不是编译错误,是**判据悄悄失去对象**。

## 踩到的坑(记下来避免重犯)

第一次批量迁 11 个文件、再用脚本自动加 `core.` 前缀,结果把
`import "…/internal/agent/core"` 本身也改了(变成 `core.core.core`),
还误伤了 `strconv.Itoa` 等标准库调用。批量文本替换在"加包名前缀"
这种场景下不可靠 —— 它会改到导入路径、注释、标准库符号。

最终改为**一次只迁一个、编译器逐步报错、手工修**。慢,但不会留下
半成品。agent/core 已完全还原过一次(git checkout HEAD)。

## 验证

    go build ./...            PASS
    go vet ./...              PASS
    gofmt -l                  干净
    go test ./... -count=1    64 包全过,0 FAIL
                             (core/test 是新包,63 → 64)
    core/test 实跑用例数      17(确认不是空壳)
    干净检出(worktree)       build + core/... 全绿

## 后续

agent/core 剩余约 140 个测试文件,按上述四类拦路虎分批:
· 纯黑箱可迁的先迁
· 需要导出入口的,评估"导出是否合理"(如 FrontendAsset 那样)
· 真白盒的(改私有状态、测内部算法)加文件头说明为什么必须白盒
2026-11-04 18:26:46 +08:00