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
This commit is contained in:
JianFeeeee
2026-11-09 16:35:21 +08:00
parent 08cbd3e7bd
commit cd4290807c
3 changed files with 180 additions and 0 deletions

View File

@ -0,0 +1,62 @@
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 路径却加载成功")
}
}