mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-15 08:51:49 +08:00
test: io/lua/system/tracker 改为模块职责级黑盒契约
## internal/agent/io
原:只有 OutputCapability 位标志 + String
现:
InjectText → 输入事件(source/output_channel/request_id)
InjectInputTo → 显式输出通道被保留
InjectTextSync → 响应回程交到调用方
RegisterDevice → GetDevice → StartAll/StopAll 真实调用设备
重复注册被拒;StartAll 传播设备启动失败
能力标志语义保留(任一位命中)
变异:允许重复注册 / 吞掉启动错误 → 均断言红
## internal/lua
新增:
transform_request 真的执行 Lua body(原来只验证加载与未加载报错)
endpoint/headers 静态声明经公开 API 读回
变异:transform_request 直接返回入参 → 断言红
## internal/system
原:只断言 archived=true 与 baseline 目录非空
现:留档 → 覆盖 → RestoreFileFromBaseline 内容还原为原文
非受保护路径与新建文件不留档
内容未变时重复留档不产生新档案
变异:留档写空内容 / 去掉幂等判断 → 均断言红
## internal/tracker
原:无文件变更时断言无 changeset,rollback 起点为空
现:真实 upper 目录写入 → PostAction 记录 ChangeFileCreated
修改原文 → RollbackLatest 恢复原文内容
新建文件 → RollbackLatest 后消失
空历史定向回滚报错
变异:不记录变更 → 两条断言红
## 验证
go build ./... / go vet ./... PASS
gofmt -l 干净
CI 门禁自检 PASS
go test ./... -count=1 -race 51 测试包 / 100 包无测试 / 0 FAIL
源码目录 _test.go 0
This commit is contained in:
@ -1,46 +1,158 @@
|
||||
package test
|
||||
|
||||
// internal/agent/io 的黑盒判据:OutputCapability 的导出方法,输入 → 输出。
|
||||
// 不构造设备、不碰内部注册表。
|
||||
// internal/agent/io 的模块级黑盒判据:IOManager 的输入注入 → 可观察事件,
|
||||
// 设备注册/查询/生命周期,以及能力标志语义。
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/JianFeeeee/HologramAgent/internal/agent/io"
|
||||
)
|
||||
|
||||
// TestSupportsSingleAndCombinedFlags:位标志的 Supports 语义。
|
||||
type contractDevice struct {
|
||||
name string
|
||||
started int
|
||||
stopped int
|
||||
startErr error
|
||||
}
|
||||
|
||||
func (d *contractDevice) Name() string { return d.name }
|
||||
func (d *contractDevice) Type() io.DeviceType { return io.DeviceIO }
|
||||
func (d *contractDevice) ChannelDef() io.ChannelDef { return io.ChannelDef{} }
|
||||
func (d *contractDevice) Description() string { return "contract device" }
|
||||
func (d *contractDevice) Tools() []io.ToolDef { return nil }
|
||||
func (d *contractDevice) Execute(string, map[string]interface{}) (interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (d *contractDevice) Start() error { d.started++; return d.startErr }
|
||||
func (d *contractDevice) Stop() error { d.stopped++; return nil }
|
||||
func (d *contractDevice) OutputCapabilities() io.OutputCapability { return io.CapText }
|
||||
|
||||
func receiveInput(t *testing.T, m *io.IOManager) *io.InputEvent {
|
||||
t.Helper()
|
||||
select {
|
||||
case evt := <-m.InputChan():
|
||||
return evt
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("injected input never reached the module's input queue")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// TestInjectTextDeliversInputEvent:注入文本必须成为一条可观察的输入事件。
|
||||
func TestInjectTextDeliversInputEvent(t *testing.T) {
|
||||
m := io.NewIOManager()
|
||||
m.InjectText("qq", "你好")
|
||||
evt := receiveInput(t, m)
|
||||
if evt.Source != "qq" || evt.OutputChannel != "qq" {
|
||||
t.Fatalf("event routing = %+v", evt)
|
||||
}
|
||||
if text, _ := evt.Payload["content"].(string); text != "你好" {
|
||||
t.Fatalf("payload text = %v", evt.Payload)
|
||||
}
|
||||
if evt.RequestID == "" {
|
||||
t.Fatal("injected event has no request id")
|
||||
}
|
||||
}
|
||||
|
||||
// TestInjectInputToRoutesToRequestedChannel:显式输出通道必须被保留。
|
||||
func TestInjectInputToRoutesToRequestedChannel(t *testing.T) {
|
||||
m := io.NewIOManager()
|
||||
m.InjectInputTo("qq", "webui", "message", map[string]interface{}{"text": "hi"})
|
||||
evt := receiveInput(t, m)
|
||||
if evt.Source != "qq" || evt.OutputChannel != "webui" {
|
||||
t.Fatalf("routing = %+v", evt)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSyncInjectionWaitsForResponse:同步注入必须把响应交回调用方。
|
||||
func TestSyncInjectionWaitsForResponse(t *testing.T) {
|
||||
m := io.NewIOManager()
|
||||
go func() {
|
||||
evt := <-m.InputChan()
|
||||
if evt.ResponseCh != nil {
|
||||
evt.ResponseCh <- &io.OutputEvent{Payload: map[string]interface{}{"content": "answer"}}
|
||||
}
|
||||
}()
|
||||
got := m.InjectTextSync("qq", "问题")
|
||||
if got == nil {
|
||||
t.Fatal("sync injection returned nil")
|
||||
}
|
||||
if c, _ := got.Payload["content"].(string); c != "answer" {
|
||||
t.Fatalf("response payload = %v", got.Payload)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDeviceRegistrationLifecycle:注册 → 查询 → StartAll/StopAll 真实生效。
|
||||
func TestDeviceRegistrationLifecycle(t *testing.T) {
|
||||
m := io.NewIOManager()
|
||||
dev := &contractDevice{name: "contract-dev"}
|
||||
if err := m.RegisterDevice(dev); err != nil {
|
||||
t.Fatalf("RegisterDevice: %v", err)
|
||||
}
|
||||
if got := m.GetDevice("contract-dev"); got == nil || got.Name() != "contract-dev" {
|
||||
t.Fatalf("GetDevice = %v", got)
|
||||
}
|
||||
if err := m.StartAll(); err != nil {
|
||||
t.Fatalf("StartAll: %v", err)
|
||||
}
|
||||
m.StopAll()
|
||||
if dev.started != 1 || dev.stopped != 1 {
|
||||
t.Fatalf("device lifecycle started=%d stopped=%d", dev.started, dev.stopped)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDuplicateDeviceRejected:重复注册必须报错,不静默覆盖。
|
||||
func TestDuplicateDeviceRejected(t *testing.T) {
|
||||
m := io.NewIOManager()
|
||||
if err := m.RegisterDevice(&contractDevice{name: "dup"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := m.RegisterDevice(&contractDevice{name: "dup"}); err == nil {
|
||||
t.Fatal("duplicate device name accepted")
|
||||
}
|
||||
}
|
||||
|
||||
// TestStartAllPropagatesDeviceFailure:设备启动失败必须向上传播。
|
||||
func TestStartAllPropagatesDeviceFailure(t *testing.T) {
|
||||
m := io.NewIOManager()
|
||||
dev := &contractDevice{name: "bad", startErr: errors.New("boom")}
|
||||
if err := m.RegisterDevice(dev); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err := m.StartAll()
|
||||
if err == nil || !strings.Contains(err.Error(), "bad") {
|
||||
t.Fatalf("StartAll error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSupportsSingleAndCombinedFlags:位标志的 Supports 语义(任一位命中)。
|
||||
func TestSupportsSingleAndCombinedFlags(t *testing.T) {
|
||||
c := io.CapText | io.CapAudio
|
||||
if !c.Supports(io.CapText) || !c.Supports(io.CapAudio) {
|
||||
t.Fatal("★ 已置位的能力报告不支持")
|
||||
t.Fatal("set capability reported as unsupported")
|
||||
}
|
||||
if c.Supports(io.CapImage) {
|
||||
t.Fatal("★ 未置位的能力报告支持")
|
||||
t.Fatal("unset capability reported as supported")
|
||||
}
|
||||
// Supports 的语义是「任一位命中」,不是「全部命中」。
|
||||
if !io.CapText.Supports(io.CapText | io.CapImage) {
|
||||
t.Fatal("★ 任一位命中应报告支持")
|
||||
t.Fatal("any-bit semantics broken")
|
||||
}
|
||||
if io.CapText.Supports(io.CapImage) {
|
||||
t.Fatal("★ 完全不相交却报告支持")
|
||||
t.Fatal("disjoint capability reported supported")
|
||||
}
|
||||
}
|
||||
|
||||
// TestCapabilityStringListsFlags:String 输出列出已置位的能力名。
|
||||
// TestCapabilityStringListsFlags:String 列出已置位能力名。
|
||||
func TestCapabilityStringListsFlags(t *testing.T) {
|
||||
c := io.CapText | io.CapImage
|
||||
s := c.String()
|
||||
s := (io.CapText | io.CapImage).String()
|
||||
if !strings.Contains(s, "text") || !strings.Contains(s, "image") {
|
||||
t.Fatalf("★ String() = %q,缺已置位能力名", s)
|
||||
t.Fatalf("String() = %q", s)
|
||||
}
|
||||
if strings.Contains(s, "audio") {
|
||||
t.Fatalf("★ String() = %q,含未置位能力", s)
|
||||
t.Fatalf("String() = %q contains unset capability", s)
|
||||
}
|
||||
}
|
||||
|
||||
// TestZeroCapabilityString:零能力字符串可用(不 panic)。
|
||||
func TestZeroCapabilityString(t *testing.T) {
|
||||
_ = io.OutputCapability(0).String()
|
||||
}
|
||||
|
||||
@ -75,3 +75,53 @@ func TestGetAdapterEndpointUnknownIsEmpty(t *testing.T) {
|
||||
t.Fatalf("★ 未知适配器端点 = %q,期望空", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTransformActuallyRunsAdapter:加载后调用 transform_request,
|
||||
// 必须真的执行 Lua 脚本(而非只返回输入或报未加载)。
|
||||
func TestTransformActuallyRunsAdapter(t *testing.T) {
|
||||
v := newVM(t)
|
||||
code := `-- name: contract-adapter
|
||||
-- version: 1.0
|
||||
return {
|
||||
transform_request = function(raw) return '{"rewritten":true}' end,
|
||||
transform_response = function(raw) return raw end,
|
||||
transform_stream_chunk = function(raw) return raw end,
|
||||
}`
|
||||
if err := v.LoadAdapterSource("contract-adapter", code); err != nil {
|
||||
t.Fatalf("LoadAdapterSource: %v", err)
|
||||
}
|
||||
out, err := v.CallTransformRequest("contract-adapter", `{"model":"m"}`)
|
||||
if err != nil {
|
||||
t.Fatalf("CallTransformRequest: %v", err)
|
||||
}
|
||||
if !strings.Contains(out, "rewritten") {
|
||||
t.Fatalf("adapter body not executed: %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAdapterHeadersAndEndpointExposed:静态声明的 endpoint/headers 可读回。
|
||||
func TestAdapterHeadersAndEndpointExposed(t *testing.T) {
|
||||
v := newVM(t)
|
||||
code := `-- name: hdr
|
||||
-- version: 1.0
|
||||
return {
|
||||
endpoint = 'http://127.0.0.1:9000/v1',
|
||||
headers = { ['X-Contract'] = 'yes' },
|
||||
transform_request = function(raw) return raw end,
|
||||
transform_response = function(raw) return raw end,
|
||||
transform_stream_chunk = function(raw) return raw end,
|
||||
}`
|
||||
if err := v.LoadAdapterSource("hdr", code); err != nil {
|
||||
t.Fatalf("LoadAdapterSource: %v", err)
|
||||
}
|
||||
if got := v.GetAdapterEndpoint("hdr"); got != "http://127.0.0.1:9000/v1" {
|
||||
t.Fatalf("endpoint = %q", got)
|
||||
}
|
||||
h, err := v.BuildHeaders("hdr", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("BuildHeaders: %v", err)
|
||||
}
|
||||
if h["X-Contract"] != "yes" {
|
||||
t.Fatalf("headers = %v", h)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
package test
|
||||
|
||||
// internal/system 的黑盒判据:导出路径判定函数,输入 → 输出。
|
||||
// 不改全局状态、不碰系统文件。
|
||||
// internal/system 的模块级黑盒判据:受保护路径的写前留档与恢复,
|
||||
// 用真实文件系统输入 → 可观察文件内容输出。
|
||||
|
||||
import (
|
||||
"os"
|
||||
@ -11,64 +11,99 @@ import (
|
||||
"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) {
|
||||
// TestArchiveAndRestoreRoundTrip 覆盖 guard 的完整职责:
|
||||
// 受保护文件被覆盖前留档 → 覆盖后可恢复成原内容。
|
||||
func TestArchiveAndRestoreRoundTrip(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 {
|
||||
|
||||
if err := os.MkdirAll(protected, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
target := filepath.Join(protected, "config.txt")
|
||||
if err := os.WriteFile(target, []byte("original"), 0644); err != nil {
|
||||
const original = "original-content"
|
||||
if err := os.WriteFile(target, []byte(original), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
archived, err := system.ArchiveBeforeWrite(base, target)
|
||||
if err != nil {
|
||||
t.Fatalf("ArchiveBeforeWrite: %v", err)
|
||||
}
|
||||
if !archived {
|
||||
t.Fatal("★ 已存在的文件却没留档")
|
||||
t.Fatal("existing protected file was not archived")
|
||||
}
|
||||
// 留档目录是导出的 FileBaselineDir
|
||||
baseline := system.FileBaselineDir(base)
|
||||
if baseline == "" {
|
||||
t.Fatal("★ FileBaselineDir 为空")
|
||||
|
||||
// 调用方随后覆盖文件(模拟 L0 写入)。
|
||||
if err := os.WriteFile(target, []byte("broken-update"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
restored, err := system.RestoreFileFromBaseline(base, target)
|
||||
if err != nil {
|
||||
t.Fatalf("RestoreFileFromBaseline: %v", err)
|
||||
}
|
||||
if !restored {
|
||||
t.Fatal("restore reported no baseline")
|
||||
}
|
||||
got, err := os.ReadFile(target)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(got) != original {
|
||||
t.Fatalf("restored content = %q, want %q", got, original)
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
// TestArchiveSkipsUnprotectedAndNewFiles:非受保护路径与新建文件都不留档。
|
||||
func TestArchiveSkipsUnprotectedAndNewFiles(t *testing.T) {
|
||||
base := t.TempDir()
|
||||
protected := filepath.Join(base, "etc")
|
||||
other := filepath.Join(base, "home")
|
||||
system.SetProtectedPaths([]string{protected + string(os.PathSeparator)})
|
||||
t.Cleanup(func() { system.SetProtectedPaths(nil) })
|
||||
for _, d := range []string{protected, other} {
|
||||
if err := os.MkdirAll(d, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
unprotected := filepath.Join(other, "file.txt")
|
||||
if err := os.WriteFile(unprotected, []byte("x"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if archived, err := system.ArchiveBeforeWrite(base, unprotected); err != nil || archived {
|
||||
t.Fatalf("unprotected file archived=%v err=%v", archived, err)
|
||||
}
|
||||
|
||||
newFile := filepath.Join(protected, "does-not-exist.txt")
|
||||
if archived, err := system.ArchiveBeforeWrite(base, newFile); err != nil || archived {
|
||||
t.Fatalf("new file archived=%v err=%v", archived, err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(system.FileBaselineDir(base), "etc", "does-not-exist.txt")); err == nil {
|
||||
t.Fatal("baseline written for a file that did not exist")
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoadBaselineMissingReportsError:读不存在的基线必须报错。
|
||||
func TestLoadBaselineMissingReportsError(t *testing.T) {
|
||||
if _, err := system.LoadNetworkBaseline(t.TempDir()); err == nil {
|
||||
t.Fatal("★ 读不存在的基线却返回成功")
|
||||
// TestArchiveIsIdempotentForUnchangedContent:内容未变时重复留档不再产生新档案。
|
||||
func TestArchiveIsIdempotentForUnchangedContent(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, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
target := filepath.Join(protected, "same.txt")
|
||||
if err := os.WriteFile(target, []byte("same"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if archived, err := system.ArchiveBeforeWrite(base, target); err != nil || !archived {
|
||||
t.Fatalf("first archive: archived=%v err=%v", archived, err)
|
||||
}
|
||||
if archived, err := system.ArchiveBeforeWrite(base, target); err != nil || archived {
|
||||
t.Fatalf("second archive on unchanged content: archived=%v err=%v", archived, err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,78 +1,101 @@
|
||||
package test
|
||||
|
||||
// internal/tracker 的黑盒判据:NewTracker/PreAction/PostAction → 可观察输出。
|
||||
// 不碰 t.changeSets、不读内部目录结构(只用公开方法返回的结果)。
|
||||
// internal/tracker 的模块级黑盒判据:真实文件变更 → changeset → 回滚恢复文件。
|
||||
// 使用 upper 目录作为可写工作区的真实输入,不碰 t.changeSets 之外无公开替代的状态。
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/JianFeeeee/HologramAgent/internal/tracker"
|
||||
)
|
||||
|
||||
func newTracker(t *testing.T) *tracker.Tracker {
|
||||
func newTracker(t *testing.T) (*tracker.Tracker, string) {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
tr := tracker.NewTracker(filepath.Join(dir, "data"), filepath.Join(dir, "work"))
|
||||
if err := tr.Init(); err != nil {
|
||||
t.Fatalf("Init: %v", err)
|
||||
}
|
||||
return tr
|
||||
t.Cleanup(func() { _ = tr.Stop() })
|
||||
return tr, filepath.Join(dir, "work", "upper")
|
||||
}
|
||||
|
||||
// TestFreshTrackerHasNoChanges:新 tracker 无 changeset。
|
||||
func TestFreshTrackerHasNoChanges(t *testing.T) {
|
||||
tr := newTracker(t)
|
||||
if tr.HasChanges() {
|
||||
t.Fatal("★ 新 tracker 报告有变更")
|
||||
func writeFile(t *testing.T, path, content string) {
|
||||
t.Helper()
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := tr.ChangeSets(); len(got) != 0 {
|
||||
t.Fatalf("★ ChangeSets() = %v,期望空", got)
|
||||
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewChangeSetCarriesAction:构造出的 changeset 带 action/ID/时间。
|
||||
func TestNewChangeSetCarriesAction(t *testing.T) {
|
||||
cs := tracker.NewChangeSet("write_file")
|
||||
if cs.Action != "write_file" {
|
||||
t.Fatalf("★ Action = %q", cs.Action)
|
||||
func readFile(t *testing.T, path string) string {
|
||||
t.Helper()
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("read %s: %v", path, err)
|
||||
}
|
||||
if cs.ID == "" || cs.Timestamp.IsZero() {
|
||||
t.Fatalf("★ ID/时间未填:%+v", cs)
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// TestPostActionRecordsRealFileChange:真实新建文件后 PostAction 必须记录该文件。
|
||||
func TestPostActionRecordsRealFileChange(t *testing.T) {
|
||||
tr, upper := newTracker(t)
|
||||
tr.PreAction("write_file")
|
||||
writeFile(t, filepath.Join(upper, "new.txt"), "hello")
|
||||
cs := tr.PostAction("write_file")
|
||||
if len(cs.Files) != 1 || cs.Files[0].Path != "new.txt" || cs.Files[0].Type != tracker.ChangeFileCreated {
|
||||
t.Fatalf("changeset = %+v", cs.Files)
|
||||
}
|
||||
if !tr.HasChanges() {
|
||||
t.Fatal("HasChanges false after a tracked change")
|
||||
}
|
||||
}
|
||||
|
||||
// TestPostActionWithoutFileChangeIsNoop:没有文件变更时 PostAction
|
||||
// 不得凭空造出 changeset。
|
||||
func TestPostActionWithoutFileChangeIsNoop(t *testing.T) {
|
||||
tr := newTracker(t)
|
||||
_ = tr.PreAction("write_file")
|
||||
got := tr.PostAction("write_file")
|
||||
if len(got.Files) != 0 {
|
||||
t.Fatalf("★ 无文件变更却记录了 %d 条", len(got.Files))
|
||||
// TestRollbackLatestRestoresModifiedContent:修改已存在文件的原文被回滚恢复。
|
||||
func TestRollbackLatestRestoresModifiedContent(t *testing.T) {
|
||||
tr, upper := newTracker(t)
|
||||
const original = "original"
|
||||
const changed = "changed"
|
||||
writeFile(t, filepath.Join(upper, "cfg.txt"), original)
|
||||
|
||||
tr.PreAction("edit")
|
||||
writeFile(t, filepath.Join(upper, "cfg.txt"), changed)
|
||||
tr.PostAction("edit")
|
||||
if got := readFile(t, filepath.Join(upper, "cfg.txt")); got != changed {
|
||||
t.Fatalf("precondition: %q", got)
|
||||
}
|
||||
if tr.HasChanges() {
|
||||
t.Fatal("★ 无文件变更却 HasChanges=true")
|
||||
|
||||
if err := tr.RollbackLatest(); err != nil {
|
||||
t.Fatalf("RollbackLatest: %v", err)
|
||||
}
|
||||
if got := readFile(t, filepath.Join(upper, "cfg.txt")); got != original {
|
||||
t.Fatalf("after rollback = %q, want %q", got, original)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRollbackLatestWithoutChangesReportsError:无 changeset 时定向回滚报错。
|
||||
// TestRollbackLatestRemovesCreatedFile:新建文件被回滚时应当消失。
|
||||
func TestRollbackLatestRemovesCreatedFile(t *testing.T) {
|
||||
tr, upper := newTracker(t)
|
||||
tr.PreAction("write_file")
|
||||
path := filepath.Join(upper, "temp.txt")
|
||||
writeFile(t, path, "scratch")
|
||||
tr.PostAction("write_file")
|
||||
if err := tr.RollbackLatest(); err != nil {
|
||||
t.Fatalf("RollbackLatest: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
||||
t.Fatalf("created file survived rollback (err=%v)", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRollbackLatestWithoutChangesReportsError:没有变更时定向回滚必须明确报错。
|
||||
func TestRollbackLatestWithoutChangesReportsError(t *testing.T) {
|
||||
tr := newTracker(t)
|
||||
tr, _ := newTracker(t)
|
||||
if err := tr.RollbackLatest(); err == nil {
|
||||
t.Fatal("★ 无 changeset 时 RollbackLatest 应报错")
|
||||
}
|
||||
}
|
||||
|
||||
// TestRollbackClearsChangeSets:Rollback 后视图清空(无需挂载 overlay)。
|
||||
func TestRollbackClearsChangeSets(t *testing.T) {
|
||||
tr := newTracker(t)
|
||||
_ = tr.PreAction("a")
|
||||
_ = tr.PostAction("a")
|
||||
if err := tr.Rollback(); err != nil {
|
||||
t.Fatalf("Rollback: %v", err)
|
||||
}
|
||||
if tr.HasChanges() {
|
||||
t.Fatal("★ Rollback 后仍有变更")
|
||||
t.Fatal("RollbackLatest accepted empty history")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user