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

## internal/memory/vector —— 纯数学 + Store

    TestCosineSimilarity              稀疏向量:同向/正交/零向量
    TestDenseCosine                   稠密向量:三种基本情形 + 非单位向量
    TestStoreInsertSearchRemove       Insert → Search 命中 → Remove → 不再命中

**变异第一版没红**:`DenseCosine` 去掉归一化后,我给的 `{1,0}`
用例点积恰好还是 1 ⇒ 空变异。补 `{4,4} / {2,2}`(不归一化得 16,
归一化得 1)后才真红。**变异必须真的改变被测行为**,这条按纪律记下。

## internal/systemd —— State / NewMigratePlan

    TestStateInSync                   相同/等价/空/不同路径
    TestStateDescribe                 一致→空;不一致→含两路径与后果
    TestSyncCommand                   不一致时给出可执行命令
    TestNewMigratePlanNormalizesPaths  路径 Clean;脚本内联新旧路径 + shebang
    TestRestartCommand                命令含给定地址

不读单元文件、不执行脚本、不碰系统状态。

## internal/recovery —— 文件往返

    TestTaskRoundTrip                 SaveTask → LoadTask 全字段 + Plugins()
    TestTaskPathsAreStable            TaskPath/ResultPath 稳定且互异
    TestResultRoundTrip               SaveResult → LoadResult 结论字段
    TestLoadTaskMissingFileReportsError
    TestSortSteps                     输出可用

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

    vector    不归一化        → ★ 非单位同向 = 16,期望 1
    systemd   InSync 恒 true  → ★ 真不一致: InSync=true
    recovery  丢 PluginList   → ★ PluginList 丢失:[]

全部还原后复跑全绿。

## 验证

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

88 lines
2.8 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/recovery 的黑盒判据:SaveTask/LoadTask/SaveResult/LoadResult,
// 输入 → 输出(文件往返)。不读实现、不碰内部字段。
import (
"path/filepath"
"testing"
"time"
"github.com/JianFeeeee/HologramAgent/internal/recovery"
)
// TestTaskRoundTrip:SaveTask → LoadTask 得回全部字段。
func TestTaskRoundTrip(t *testing.T) {
path := filepath.Join(t.TempDir(), "nested", "task.json")
in := &recovery.Task{
Attempt: 3, MaxAttempts: 5, AttemptTimeout: "120s",
TriggerPrompt: "检查网络", PluginList: []string{"cmd", "files"},
}
if err := recovery.SaveTask(path, in); err != nil {
t.Fatalf("SaveTask: %v", err)
}
out, err := recovery.LoadTask(path)
if err != nil {
t.Fatalf("LoadTask: %v", err)
}
if out.Attempt != 3 || out.MaxAttempts != 5 || out.AttemptTimeout != "120s" {
t.Fatalf("★ 往返后字段丢失:%+v", out)
}
if len(out.PluginList) != 2 || out.PluginList[0] != "cmd" {
t.Fatalf("★ PluginList 丢失:%v", out.PluginList)
}
if got := out.Plugins(); len(got) != 2 {
t.Fatalf("★ Plugins() = %v", got)
}
}
// TestTaskPathsAreStable:TaskPath/ResultPath 由 dataDir 决定且互不相同。
func TestTaskPathsAreStable(t *testing.T) {
dir := "/data/contract"
a := recovery.TaskPath(dir)
b := recovery.TaskPath(dir)
if a != b || a == "" {
t.Fatalf("★ TaskPath 不稳定:%q vs %q", a, b)
}
if recovery.ResultPath(dir) == a {
t.Fatal("★ TaskPath 与 ResultPath 相同")
}
}
// TestResultRoundTrip:SaveResult → LoadResult 得回结论字段。
func TestResultRoundTrip(t *testing.T) {
path := filepath.Join(t.TempDir(), "result.json")
in := &recovery.Result{
Attempt: 2, Success: true, Cause: "network", NetworkOK: true,
RestoredNet: []string{"/etc/resolv.conf"}, ConfigRestored: true,
QuickChatOK: true, Steps: []string{"check", "restore"},
Message: "已恢复", Timestamp: time.Now(),
}
if err := recovery.SaveResult(path, in); err != nil {
t.Fatalf("SaveResult: %v", err)
}
out, err := recovery.LoadResult(path)
if err != nil {
t.Fatalf("LoadResult: %v", err)
}
if !out.Success || out.Cause != "network" || !out.ConfigRestored || !out.QuickChatOK {
t.Fatalf("★ 往返后结论丢失:%+v", out)
}
}
// TestLoadTaskMissingFileReportsError:读不存在的文件必须报错,不静默给空值。
func TestLoadTaskMissingFileReportsError(t *testing.T) {
if _, err := recovery.LoadTask(filepath.Join(t.TempDir(), "nope.json")); err == nil {
t.Fatal("★ 读不存在的任务文件却返回成功")
}
}
// TestSortSteps:步骤排序输出可用(不钉死具体顺序以外的内部结构)。
func TestSortSteps(t *testing.T) {
in := []string{"c", "a", "b"}
out := recovery.SortSteps(in)
if len(out) != 3 {
t.Fatalf("★ SortSteps = %v", out)
}
}