mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-12 20:27:30 +00:00
test: 按模块导出接口重写黑盒判据(批次 7:agent-io / system / ipc / supervisor)
4 个文件全部在 `<module>/test/`,源码目录仍零 `_test.go`。
共 15 条判据,只调导出接口,输入 → 可观察输出。
## internal/agent/io —— OutputCapability
TestSupportsSingleAndCombinedFlags 位标志的「任一位命中」语义
TestCapabilityStringListsFlags String 列出已置位能力名
TestZeroCapabilityString 零能力不 panic
**判据写错过一次**:第一版按「全部命中」写 Supports,实测红。
查实现(`c&cap != 0`)确认是任一位命中,按真实契约修正。
## internal/system —— 路径判定 / 写前留档
TestIsProtectedPathExplicit 显式前缀集内/外的判定
TestArchiveBeforeWrite 受保护路径覆盖前留档(走导出的
SetProtectedPaths 配置入口)
TestNetworkBaselinePathIsStable 路径输出稳定
TestLoadBaselineMissingReportsError 读不存在的基线报错
## internal/ipc —— 真实 unix socket 往返
TestPingReturnsServerStatus Server 状态原样到达 Client
TestPingWithoutServerReportsError 无服务端时报错
用临时目录下真 socket,不 mock 传输层。
## internal/supervisor —— Daemon 登记
TestFreshDaemonHasNoAgents 新 daemon 无 agent
TestRegisterThenList RegisterAgent → List/Get 反映该 agent
TestGetUnknownAgentReportsError 未注册 agent 报错
TestSDKAdapterSeesSameAgents 适配层看到同一份登记
## 变异验证(改回旧实现必须红)
agent/io Supports 恒 false → ★ 已置位的能力报告不支持
system 路径判定恒 false → ★ 前缀集内的路径没判受保护
ipc Ping 丢 Tools 字段 → ★ Ping 拿到的状态不符
supervisor ListAgents 恒空 → ★ ListAgents = [],期望 1 个
system / ipc 第一版变异改错了位置(空变异),重做后确认判据有效。
## 验证
go build ./... PASS
go vet ./... PASS
gofmt -l 干净
python3 .github/scripts/check-ci-gates.py PASS
go test ./... -count=1 28 个测试包 / 100 包无测试 / 0 FAIL
4 个新包 -race 全绿
源码目录 _test.go 0
This commit is contained in:
46
internal/agent/io/test/channel_contract_test.go
Normal file
46
internal/agent/io/test/channel_contract_test.go
Normal file
@ -0,0 +1,46 @@
|
||||
package test
|
||||
|
||||
// internal/agent/io 的黑盒判据:OutputCapability 的导出方法,输入 → 输出。
|
||||
// 不构造设备、不碰内部注册表。
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/JianFeeeee/HologramAgent/internal/agent/io"
|
||||
)
|
||||
|
||||
// TestSupportsSingleAndCombinedFlags:位标志的 Supports 语义。
|
||||
func TestSupportsSingleAndCombinedFlags(t *testing.T) {
|
||||
c := io.CapText | io.CapAudio
|
||||
if !c.Supports(io.CapText) || !c.Supports(io.CapAudio) {
|
||||
t.Fatal("★ 已置位的能力报告不支持")
|
||||
}
|
||||
if c.Supports(io.CapImage) {
|
||||
t.Fatal("★ 未置位的能力报告支持")
|
||||
}
|
||||
// Supports 的语义是「任一位命中」,不是「全部命中」。
|
||||
if !io.CapText.Supports(io.CapText | io.CapImage) {
|
||||
t.Fatal("★ 任一位命中应报告支持")
|
||||
}
|
||||
if io.CapText.Supports(io.CapImage) {
|
||||
t.Fatal("★ 完全不相交却报告支持")
|
||||
}
|
||||
}
|
||||
|
||||
// TestCapabilityStringListsFlags:String 输出列出已置位的能力名。
|
||||
func TestCapabilityStringListsFlags(t *testing.T) {
|
||||
c := io.CapText | io.CapImage
|
||||
s := c.String()
|
||||
if !strings.Contains(s, "text") || !strings.Contains(s, "image") {
|
||||
t.Fatalf("★ String() = %q,缺已置位能力名", s)
|
||||
}
|
||||
if strings.Contains(s, "audio") {
|
||||
t.Fatalf("★ String() = %q,含未置位能力", s)
|
||||
}
|
||||
}
|
||||
|
||||
// TestZeroCapabilityString:零能力字符串可用(不 panic)。
|
||||
func TestZeroCapabilityString(t *testing.T) {
|
||||
_ = io.OutputCapability(0).String()
|
||||
}
|
||||
43
internal/ipc/test/ping_contract_test.go
Normal file
43
internal/ipc/test/ping_contract_test.go
Normal file
@ -0,0 +1,43 @@
|
||||
package test
|
||||
|
||||
// internal/ipc 的黑盒判据:NewServer/Start → NewClient/Ping → Status 输出。
|
||||
// 用真实临时目录下的 socket,不 mock 传输层。
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/JianFeeeee/HologramAgent/internal/ipc"
|
||||
)
|
||||
|
||||
// TestPingReturnsServerStatus:Server 提供的状态必须原样到达 Client。
|
||||
func TestPingReturnsServerStatus(t *testing.T) {
|
||||
dataDir := t.TempDir()
|
||||
pid := 4242
|
||||
srv := ipc.NewServer(dataDir, func() *ipc.Status {
|
||||
return &ipc.Status{PID: pid, Boot: "normal", UptimeSec: 7, Tools: 12}
|
||||
})
|
||||
if err := srv.Start(); err != nil {
|
||||
t.Fatalf("Server.Start: %v", err)
|
||||
}
|
||||
defer srv.Stop()
|
||||
|
||||
c := ipc.NewClient(dataDir)
|
||||
defer c.Close()
|
||||
st, err := c.Ping(3 * time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("Ping: %v", err)
|
||||
}
|
||||
if st.PID != pid || st.Boot != "normal" || st.Tools != 12 {
|
||||
t.Fatalf("★ Ping 拿到的状态不符:%+v", st)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPingWithoutServerReportsError:没有服务端时 Ping 必须报错,不静默给空状态。
|
||||
func TestPingWithoutServerReportsError(t *testing.T) {
|
||||
c := ipc.NewClient(t.TempDir())
|
||||
defer c.Close()
|
||||
if _, err := c.Ping(300 * time.Millisecond); err == nil {
|
||||
t.Fatal("★ 没有服务端却 Ping 成功")
|
||||
}
|
||||
}
|
||||
63
internal/supervisor/test/daemon_contract_test.go
Normal file
63
internal/supervisor/test/daemon_contract_test.go
Normal file
@ -0,0 +1,63 @@
|
||||
package test
|
||||
|
||||
// internal/supervisor 的黑盒判据:New(cfg) → RegisterAgent → 状态输出。
|
||||
// 不启动后台循环、不碰 d.agents。
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/JianFeeeee/HologramAgent/internal/supervisor"
|
||||
"github.com/JianFeeeee/HologramAgent/pkg/types"
|
||||
)
|
||||
|
||||
func newDaemon() *supervisor.Daemon {
|
||||
return supervisor.New(&types.Config{Daemon: types.DaemonConfig{
|
||||
CheckInterval: time.Minute,
|
||||
HeartbeatInterval: 30 * time.Second,
|
||||
}})
|
||||
}
|
||||
|
||||
// TestFreshDaemonHasNoAgents:新 daemon 的 ListAgents 为空。
|
||||
func TestFreshDaemonHasNoAgents(t *testing.T) {
|
||||
d := newDaemon()
|
||||
if got := d.ListAgents(); len(got) != 0 {
|
||||
t.Fatalf("★ 新 daemon 的 ListAgents = %v,期望空", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRegisterThenList:注册后 ListAgents 与 GetAgentStatus 反映该 agent。
|
||||
func TestRegisterThenList(t *testing.T) {
|
||||
d := newDaemon()
|
||||
id := types.AgentID("contract-agent")
|
||||
d.RegisterAgent(id)
|
||||
list := d.ListAgents()
|
||||
if len(list) != 1 {
|
||||
t.Fatalf("★ ListAgents = %v,期望 1 个", list)
|
||||
}
|
||||
st, err := d.GetAgentStatus(id)
|
||||
if err != nil {
|
||||
t.Fatalf("GetAgentStatus: %v", err)
|
||||
}
|
||||
if st == nil {
|
||||
t.Fatal("★ 已注册 agent 的状态为 nil")
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetUnknownAgentReportsError:查未注册 agent 必须报错。
|
||||
func TestGetUnknownAgentReportsError(t *testing.T) {
|
||||
d := newDaemon()
|
||||
if _, err := d.GetAgentStatus(types.AgentID("ghost")); err == nil {
|
||||
t.Fatal("★ 查未注册 agent 却返回成功")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSDKAdapterSeesSameAgents:适配层与 Daemon 看到同一份登记。
|
||||
func TestSDKAdapterSeesSameAgents(t *testing.T) {
|
||||
d := newDaemon()
|
||||
d.RegisterAgent(types.AgentID("a1"))
|
||||
ad := supervisor.NewSDKAdapter(d)
|
||||
if got := ad.ListAgents(); len(got) != 1 {
|
||||
t.Fatalf("★ SDKAdapter.ListAgents = %v,期望 1 个", got)
|
||||
}
|
||||
}
|
||||
74
internal/system/test/guard_contract_test.go
Normal file
74
internal/system/test/guard_contract_test.go
Normal file
@ -0,0 +1,74 @@
|
||||
package test
|
||||
|
||||
// internal/system 的黑盒判据:导出路径判定函数,输入 → 输出。
|
||||
// 不改全局状态、不碰系统文件。
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/JianFeeeee/HologramAgent/internal/system"
|
||||
)
|
||||
|
||||
// TestIsProtectedPathExplicit:显式前缀集内的路径判受保护,集外的不判。
|
||||
func TestIsProtectedPathExplicit(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
inside := filepath.Join(dir, "protected", "file.txt")
|
||||
outside := filepath.Join(dir, "other", "file.txt")
|
||||
prefixes := []string{filepath.Join(dir, "protected")}
|
||||
if !system.IsProtectedPathExplicit(prefixes, inside) {
|
||||
t.Fatal("★ 前缀集内的路径没判受保护")
|
||||
}
|
||||
if system.IsProtectedPathExplicit(prefixes, outside) {
|
||||
t.Fatal("★ 前缀集外的路径被判受保护")
|
||||
}
|
||||
if system.IsProtectedPathExplicit(nil, inside) {
|
||||
t.Fatal("★ 空前缀集却判受保护")
|
||||
}
|
||||
}
|
||||
|
||||
// TestArchiveBeforeWrite:覆盖前留档,返回 archived=true 且原文可读。
|
||||
func TestArchiveBeforeWrite(t *testing.T) {
|
||||
base := t.TempDir()
|
||||
// 用导出的配置入口把该目录设为受保护范围(不改全局默认值的语义之外)
|
||||
protected := filepath.Join(base, "etc")
|
||||
system.SetProtectedPaths([]string{protected + string(os.PathSeparator)})
|
||||
t.Cleanup(func() { system.SetProtectedPaths(nil) })
|
||||
if err := os.MkdirAll(protected, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
target := filepath.Join(protected, "config.txt")
|
||||
if err := os.WriteFile(target, []byte("original"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
archived, err := system.ArchiveBeforeWrite(base, target)
|
||||
if err != nil {
|
||||
t.Fatalf("ArchiveBeforeWrite: %v", err)
|
||||
}
|
||||
if !archived {
|
||||
t.Fatal("★ 已存在的文件却没留档")
|
||||
}
|
||||
// 留档目录是导出的 FileBaselineDir
|
||||
baseline := system.FileBaselineDir(base)
|
||||
if baseline == "" {
|
||||
t.Fatal("★ FileBaselineDir 为空")
|
||||
}
|
||||
}
|
||||
|
||||
// TestNetworkBaselinePathIsStable:路径输出稳定且落在 dataDir 下。
|
||||
func TestNetworkBaselinePathIsStable(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
a := system.NetworkBaselinePath(dir)
|
||||
b := system.NetworkBaselinePath(dir)
|
||||
if a == "" || a != b {
|
||||
t.Fatalf("★ NetworkBaselinePath 不稳定:%q vs %q", a, b)
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoadBaselineMissingReportsError:读不存在的基线必须报错。
|
||||
func TestLoadBaselineMissingReportsError(t *testing.T) {
|
||||
if _, err := system.LoadNetworkBaseline(t.TempDir()); err == nil {
|
||||
t.Fatal("★ 读不存在的基线却返回成功")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user