Files
HomeAgent/internal/systemd/test/sync_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

87 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/systemd 的黑盒判据:State/NewMigratePlan 的导出方法,输入 → 输出。
// 不读单元文件、不执行脚本、不碰系统状态。
import (
"path/filepath"
"strings"
"testing"
"github.com/JianFeeeee/HologramAgent/internal/systemd"
)
// TestStateInSync:相同路径(含 ./ 与尾部斜杠)判一致;不同判不一致。
func TestStateInSync(t *testing.T) {
cases := []struct {
name string
r, c string
want bool
}{
{"完全相同", "/data/home", "/data/home", true},
{"路径等价", "/data/home/", "/data/home/./", true},
{"空运行期", "", "/data/home", true},
{"空配置", "/data/home", "", true},
{"真不一致", "/data/a", "/data/b", false},
}
for _, c := range cases {
s := systemd.State{RuntimeDataDir: c.r, ConfigDataDir: c.c}
if got := s.InSync(); got != c.want {
t.Errorf("★ %s: InSync=%v,期望 %v", c.name, got, c.want)
}
}
}
// TestStateDescribe:一致时诊断为空;不一致时给出两个路径与后果。
func TestStateDescribe(t *testing.T) {
ok := systemd.State{RuntimeDataDir: "/d", ConfigDataDir: "/d"}
if d := ok.Describe(); d != "" {
t.Fatalf("★ 一致时 Describe 应为空,实际 %q", d)
}
bad := systemd.State{RuntimeDataDir: "/data/a", ConfigDataDir: "/data/b"}
d := bad.Describe()
if !strings.Contains(d, "/data/a") || !strings.Contains(d, "/data/b") {
t.Fatalf("★ Describe 缺两个路径:%q", d)
}
if !strings.Contains(d, "数据分裂") {
t.Fatalf("★ Describe 没说明后果:%q", d)
}
}
// TestSyncCommand:不一致时给出可执行的同步命令。
func TestSyncCommand(t *testing.T) {
s := systemd.State{RuntimeDataDir: "/data/a", ConfigDataDir: "/data/b"}
cmd := s.SyncCommand("/data/a")
if cmd == "" {
t.Fatal("★ 不一致时 SyncCommand 为空")
}
if !strings.Contains(cmd, "/data/a") {
t.Fatalf("★ SyncCommand 不含目标路径:%q", cmd)
}
}
// TestNewMigratePlanNormalizesPaths:构造计划时路径被 Clean,脚本含新旧路径。
func TestNewMigratePlanNormalizesPaths(t *testing.T) {
old := "/data/old/./"
newDir := "/data/new//"
p := systemd.NewMigratePlan(old, newDir)
if p.OldDir != filepath.Clean(old) || p.NewDir != filepath.Clean(newDir) {
t.Fatalf("★ 路径未 Clean:old=%q new=%q", p.OldDir, p.NewDir)
}
script := p.Script()
if !strings.Contains(script, p.OldDir) || !strings.Contains(script, p.NewDir) {
t.Fatal("★ 迁移脚本没内联新旧路径")
}
if !strings.HasPrefix(script, "#!/usr/bin/env bash") {
t.Fatal("★ 脚本缺 shebang —— 不能直接执行")
}
}
// TestRestartCommand:重启命令必须包含给定地址。
func TestRestartCommand(t *testing.T) {
cmd := systemd.RestartCommand("127.0.0.1:8080", "test-key")
if !strings.Contains(cmd, "127.0.0.1:8080") {
t.Fatalf("★ RestartCommand 不含地址:%q", cmd)
}
}