Files
HomeAgent/internal/plugin/test/skill_contract_test.go
JianFeeeee cd4290807c test: 按模块导出接口重写黑盒判据(批次 12:plugin / qwen3vl / chineseclip)
3 个文件全部在 `<module>/test/`,源码目录仍零 `_test.go`。
新增公开输入→输出判据:

## internal/plugin —— SKILL 内容与加载

    TestValidateSKILLContent
      合法内容通过;空内容/无描述/非法工具名分别被拒
    TestLoadSKILLDirectoryExposesMetadataAndTools
      临时 SKILL.md → Name/Version/Description/Tools/IOConfig
    TestLoadSKILLMissingFileReturnsError

## providers/qwen3vl —— BPE tokenizer 公开契约

    TestTokenizerSpecialTokenRoundTrip
      临时 tokenizer.json → VocabSize/SpecialID/Encode/DecodeOne
    TestTokenizerMissingFileReturnsError

## providers/chineseclip —— WordPiece tokenizer 公开契约

    TestTokenizerEncodeUsesSpecialTokensAndPadding
      临时 vocab.txt → 固定长度 ids 与 attention mask
    TestTokenizerRejectsInvalidVocabulary
      缺特殊 token / 非正 maxLength → 报错

所有测试都用 `package test` 导入模块,通过导出 API 操作;不访问私有符号,
不读取源码/文档作为业务断言。

## 变异验证

    plugin       ValidateSKILLContent 放过无描述内容 → no_description 断言红
    qwen3vl      Encode 丢弃特殊 token             → Encode=[] 断言红
    chineseclip  Encode 不补 padding               → 固定长度判据红

## 验证

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

63 lines
2.3 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/plugin 的黑盒判据:公开内容校验与 SKILL 文件加载。
// 测试只通过 ValidateSKILLContent/LoadSKILL 及 Plugin 接口观察结果。
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/JianFeeeee/HologramAgent/internal/plugin"
)
func TestValidateSKILLContent(t *testing.T) {
valid := "# Demo\n\n这是一个用于测试的技能。\n\n## greet\n向用户打招呼。\n- name: 用户名\n"
if err := plugin.ValidateSKILLContent(valid); err != nil {
t.Fatalf("合法 SKILL 被拒:%v", err)
}
for _, tc := range []struct{ name, content, want string }{
{"empty", "", "empty"},
{"no description", "# Only heading", "no description"},
{"bad tool name", "正文\n\n## bad/name\n描述", "invalid tool name"},
} {
t.Run(tc.name, func(t *testing.T) {
if err := plugin.ValidateSKILLContent(tc.content); err == nil || !strings.Contains(err.Error(), tc.want) {
t.Fatalf("错误内容的报错不符:%v", err)
}
})
}
}
func TestLoadSKILLDirectoryExposesMetadataAndTools(t *testing.T) {
dir := t.TempDir()
content := "---\nversion: 2.1\nauthor: test\nio_type: text\nio_route: chat\nio_output_caps: text,markdown\n---\n# Demo\n\n用于回归的技能。\n\n## greet\n向用户打招呼。\n- name: 用户名\n"
if err := os.WriteFile(filepath.Join(dir, "SKILL.md"), []byte(content), 0o644); err != nil {
t.Fatal(err)
}
p, err := plugin.LoadSKILL(dir)
if err != nil {
t.Fatalf("LoadSKILL: %v", err)
}
if p.Name() != filepath.Base(dir) || p.Version() != "2.1" || p.Description() != "用于回归的技能。" {
t.Fatalf("公开元数据不符:name=%q version=%q description=%q", p.Name(), p.Version(), p.Description())
}
if p.PluginType() != plugin.PluginTypeSKILL || !p.Enabled() || p.RawContent() != content {
t.Fatalf("公开属性不符:type=%q enabled=%v", p.PluginType(), p.Enabled())
}
if len(p.Tools()) != 1 || p.Tools()[0].Name != "greet" {
t.Fatalf("工具定义不符:%+v", p.Tools())
}
io := p.IOConfig()
if io == nil || io.Type != "text" || io.InputRoute != "chat" || io.OutputRoute != "chat" || len(io.OutputCaps) != 2 {
t.Fatalf("IO 配置不符:%+v", io)
}
}
func TestLoadSKILLMissingFileReturnsError(t *testing.T) {
if _, err := plugin.LoadSKILL(filepath.Join(t.TempDir(), "missing")); err == nil {
t.Fatal("不存在的 SKILL 路径却加载成功")
}
}