Files
HomeAgent/internal/agent/api
JianFeeeee 3384cee62a test: 按模块导出接口重写黑盒判据(批次 1:config/knowledge/distill/agent-api)
## 方向

「面向模块输入接受模块输出」:只调模块**本来就有**的导出接口,
输入 → 断言可观察输出。不从旧白盒里抽、不读源码、不引用文档、
不构造内部结构、不为测试导出任何新符号。

4 个文件全部放 `<module>/test/`,源码目录仍零 `_test.go`。
共 23 条判据,每模块 1 个文件。

## 各模块的接口清单与判据

### internal/config —— NewConfigRegistry / PluginConfig

    TestCoreSetGetDelete                  Set → Get → Delete → Get=nil
    TestPluginNamespaceIsIsolated         两插件同名 key 不串台;核心表看不到插件键
    TestPluginListByPrefix                List 只返回匹配前缀的键
    TestDefRoundTripPreservesSecret       RegisterDef → ListDefs 保住 Secret
    TestSecretValueIsNotPlaintextOnDisk   Set → GetSecret 往返 + DB 里无明文

**判据按真实契约写**:插件表由 `RegisterDef` 创建,`Set` 不隐式建表。
第一版没先 RegisterDef,实测 `no such table: config_alpha` —— 不是缺陷,
是我把装配顺序当成了实现细节。判据里显式写了这条契约。

### internal/knowledge —— NewStore(root) / Add / List / Stats / Remove / Search / Categories

    TestAddThenListAndStats                  Add → List/Stats
    TestRemoveActuallyRemoves                Remove → 空;二次 Remove 报错
    TestPathTraversalIsRejected             ".." / "." / 空名 必须被拒
    TestCategoriesIncludeIntermediateLevels  tech/go/叶名 ⇒ tech、tech/go 都出现
    TestSearchRanksRelevantAboveUnrelated   只问排序,不钉数值

`TestSearchRanksRelatedAboveUnrelated` 直接对治 engineering-lessons §1.1
那个坑:16 条判据全绿而检索结果完全无关(分数全挤在 0.75~0.79)。

**判据写错过一次并修正**:`Categories()` 返回的是**父路径**,
`tech/go/concurrency` 条目的 Category 是 `tech/go`,不含叶名。
第一版断言叶名也在列表里 ⇒ 红。查实现后按真实契约改成两层。

### internal/memory/distill —— 纯函数,输入输出就是全部语义

    TestBlockTextRoundTrip              ToMemoryBlock → BlockSubject/Dimension/Value
    TestBlockIDDependsOnSubject        不同主语不得撞 ID(静默数据丢失)
    TestBlockIDIsDeterministic         同输入同输出
    TestDimensionValueEdgeCases        无 '=' / 空维度 / 多 '='
    TestSentenceBlockIDIsStable        原句块 ID 稳定
    TestNormalizeDimensionIsIdempotent 归一幂等

### internal/agent/api —— 纯函数 + ProviderManager 公开状态机

    TestEstimateTokensIsMonotonic      更长文本不得更少 token
    TestTruncateByTokensRespectsBudget 截断后不超预算
    TestModelContextWindowAlwaysPositive
    TestIsValidSourceConfig            四字段 + URL scheme 真值表
    TestRegisterGetListDefault         注册顺序/按名取回/默认源切换
    TestAvailabilityStateMachine       MarkUnavailable → 不可用 → Reset 恢复
    TestProviderErrorCarriesStatusAndMessage

`ProviderManager` 用的是真 `NewProviderManager()` + 满足 `Provider`
接口的 stub,不碰 `m.status`、不用任何 ForTest 钩子。

## 变异验证(每条至少一次,改回旧实现必须红)

    config  ListDefs 丢 Secret        → ★ Secret:true 在 RegisterDef → ListDefs 后丢失
    knowledge normalizeName 放行 ".." → ★ Add("..") 被接受了 —— 路径可逃出知识根
    distill BlockID 丢 subject        → ★ 不同主语得到同一个 BlockID
    api 漏 URL scheme 校验            → ★ ftp://x 判成了 true

全部还原后复跑全绿。

## 验证

    go build ./...                            PASS
    go vet ./...                              PASS
    gofmt -l                                  干净
    python3 .github/scripts/check-ci-gates.py PASS
    go test ./... -count=1                    9 个测试包 / 100 个包无测试 / 0 FAIL
    4 个新包 -race                            全绿
    lens 4 个新文件                           LSP 0 诊断
2026-11-09 11:57:48 +08:00
..