Files
HomeAgent/internal/plugins/seq/test/seq_contract_test.go
JianFeeeee b2a5fad95f test: 按模块导出接口重写黑盒判据(批次 9:seq / nlp / pipeline)
3 个文件全部在 `<module>/test/`,源码目录仍零 `_test.go`。
共 17 条判据,只调导出接口,输入 → 可观察输出。

`internal/kernelmod/pluginhost` 本批跳过:它的 PluginCatalog 只有
`newCatalog` 这个未导出构造,没有对外入口。按「不为测试导出内部」的
规矩,不为它造壳。

## internal/plugins/seq —— Parse / Store

    TestParseValidSequence              合法 JSON → 名字/Boot/Groups
    TestParseRejectsUnknownField        拼错字段必须报 unknown field
    TestParseRejectsInvalidJSON         坏 JSON 报错
    TestStoreRoundTrip                  Save → List/Load → Delete
    TestLoadMissingReportsError
    TestCheckNewRejectsSelfRecursion    #self 调用被拒
    TestCheckNewAcceptsValidCall        合法 group 调用通过

**判据写错过三次,都按真实输入契约修正**:
① group 的工具字段叫 `tools` 不叫 `steps`;
② `tools` 在 JSON 里是字符串(若干以 `;` 分隔的 JSON 对象),不是数组;
③ 最后一个工具末尾必须有 `;`。
`CheckNew` 守的是 seq_call 的 target 校验,不是"同名重复",判据按此改。

**还发现一条假绿并修掉**:`TestParseRejectsUnknownField` 第一版只断言
`err != nil`,但输入没带 group —— "没有任何 group"同样会报错,
去掉 `DisallowUnknownFields` 后判据仍绿。改成带合法 group
且断言错误里含 `unknown field`,变异才真红。

## internal/nlp —— 配置 / Extractor

    TestDefaultExtractorConfig          默认权重可用
    TestSetFusionWeightsRoundTrip       设入 → FusionConfig 读回
    TestSetDefaultParserRoundTrip       SetDefaultParser → GetDefaultParser
    TestExtractEmptyTextReturnsEmptySet
    TestExtractNonEmptyTextDoesNotPanic

不带 ONNX 模型时默认 parser 为 nil、`NewExtractor(nil)` 走 fallback,
这是真实契约;判据不要求它非 nil,而是用 stub 验证 Set/Get 往返。

## internal/memory/pipeline —— Distiller

    TestAppendThenRecentRecords          Append → GetRecentRecords
    TestGetRecentRecordsRespectsLimit    limit + 取最新 N 条
    TestPauseResumeStopped               暂停/恢复可观察
    TestStatsIsUsable
    TestRawPathIsUnderDataDir

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

    seq      去掉 DisallowUnknownFields → ★ 未知字段被静默接受
    nlp      SetFusionWeights 丢 alpha → ★ FusionConfig 不符
    pipeline Append 不记录              → ★ GetRecentRecords = 0 条

全部还原后复跑全绿。

## 验证

    go build ./...                            PASS
    go vet ./...                              PASS
    gofmt -l                                  干净
    python3 .github/scripts/check-ci-gates.py PASS
    go test ./... -count=1                    35 个测试包 / 100 包无测试 / 0 FAIL
    3 个新包 -race                            全绿
    源码目录 _test.go                         0
2026-11-09 14:20:59 +08:00

115 lines
3.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package test
// internal/plugins/seq 的黑盒判据:Parse/Store 的导出接口,输入 → 输出。
// 不读序列文件内部格式、不碰 s.dir 之外的状态。
import (
"strings"
"testing"
"github.com/JianFeeeee/HologramAgent/internal/plugins/seq"
)
// TestParseValidSequence:合法 JSON 解析出名字与组。
func TestParseValidSequence(t *testing.T) {
// tools 在 JSON 里是**字符串**(若干以 ';' 分隔的 JSON 对象),
// 不是数组 —— 这是 Parse 的真实输入契约。
data := []byte(`{
"name":"daily",
"description":"每日检查",
"boot":true,
"groups":[{"name":"g1","tools":"{\"tool\":\"cmd_run\",\"args\":{\"command\":\"echo ok\"}} ;"}]
}`)
s, err := seq.Parse(data)
if err != nil {
t.Fatalf("Parse: %v", err)
}
if s.Name != "daily" || !s.Boot {
t.Fatalf("★ Parse 结果不符:%+v", s)
}
if len(s.Groups) != 1 || s.Groups[0].Name != "g1" {
t.Fatalf("★ Groups 丢失:%+v", s.Groups)
}
}
// TestParseRejectsUnknownField:拼错字段必须报错,不静默丢弃。
func TestParseRejectsUnknownField(t *testing.T) {
// 带上合法 group:否则"没有任何 group"也会报错,
// 判据会假绿(断言的其实是任意错误,不是未知字段错误)。
_, err := seq.Parse([]byte(`{
"name":"x","paralll":true,
"groups":[{"name":"g","tools":"{\"tool\":\"cmd_run\"} ;"}]
}`))
if err == nil {
t.Fatal("★ 未知字段被静默接受")
}
if !strings.Contains(err.Error(), "unknown field") {
t.Fatalf("★ 报错来自别处,不是未知字段:%v", err)
}
}
// TestParseRejectsInvalidJSON:坏 JSON 报错。
func TestParseRejectsInvalidJSON(t *testing.T) {
if _, err := seq.Parse([]byte(`{not json`)); err == nil {
t.Fatal("★ 坏 JSON 被接受")
}
}
// TestStoreRoundTrip:Save → List/Load → Delete。
func TestStoreRoundTrip(t *testing.T) {
s := seq.NewStore(t.TempDir())
in := &seq.Sequence{Name: "nightly", Description: "夜间"}
if err := s.Save(in); err != nil {
t.Fatalf("Save: %v", err)
}
list := s.List()
if len(list) != 1 || list[0] != "nightly" {
t.Fatalf("★ List = %v,期望 [nightly]", list)
}
out, err := s.Load("nightly")
if err != nil {
t.Fatalf("Load: %v", err)
}
if out.Name != "nightly" || out.Description != "夜间" {
t.Fatalf("★ Load 内容不符:%+v", out)
}
if err := s.Delete("nightly"); err != nil {
t.Fatalf("Delete: %v", err)
}
if got := s.List(); len(got) != 0 {
t.Fatalf("★ Delete 后 List = %v", got)
}
}
// TestLoadMissingReportsError:读不存在的序列报错。
func TestLoadMissingReportsError(t *testing.T) {
if _, err := seq.NewStore(t.TempDir()).Load("ghost"); err == nil {
t.Fatal("★ 读不存在的序列却成功")
}
}
// TestCheckNewRejectsSelfRecursion:序列不能调用自身(无限递归)。
func TestCheckNewRejectsSelfRecursion(t *testing.T) {
s := seq.NewStore(t.TempDir())
bad := &seq.Sequence{Name: "loop", Groups: []seq.Group{{
Name: "g1",
Tools: []seq.ToolCall{{Tool: "seq_call", Args: map[string]interface{}{"target": "#loop"}}},
}}}
if err := s.CheckNew(bad); err == nil {
t.Fatal("★ 自递归序列被接受")
}
}
// TestCheckNewAcceptsValidCall:指向本序列内已存在 group 的调用应通过。
func TestCheckNewAcceptsValidCall(t *testing.T) {
s := seq.NewStore(t.TempDir())
ok := &seq.Sequence{Name: "chain", Groups: []seq.Group{
{Name: "a", Tools: []seq.ToolCall{{Tool: "seq_call",
Args: map[string]interface{}{"target": "b"}}}},
{Name: "b"},
}}
if err := s.CheckNew(ok); err != nil {
t.Fatalf("★ 合法 group 调用被拒:%v", err)
}
}