Files
Agent-System/internal/squad/repair_workspace_test.go
T
weichenandCursor 01c5972ead fix(squad): void quality repair rounds on backend or containment failure
Promote root output.md only when the runner succeeded, protected parents
were untouched, and the local target is trusted. Restore errors abort
instead of being ignored.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-13 20:20:13 +00:00

195 lines
6.7 KiB
Go

package squad_test
import (
"os"
"path/filepath"
"strings"
"testing"
"chat-agent-system/internal/squad"
)
func TestLocalNeedSnapshotName_DoesNotOccupyTarget(t *testing.T) {
if got := squad.LocalNeedSnapshotName("output.md", "output.md"); got != squad.RepairSourceOutputName {
t.Fatalf("root output.md must snapshot as source_output.md, got %q", got)
}
if got := squad.LocalNeedSnapshotName("tasks/03_review/output.md", "output.md"); got != squad.RepairSourceOutputName {
t.Fatalf("nested output.md must not occupy target, got %q", got)
}
if got := squad.LocalNeedSnapshotName("blackboard.json", "output.md"); got != "blackboard.json" {
t.Fatalf("other needs keep basename, got %q", got)
}
}
func TestSnapshotInputsAndContainedTaskHaveNoParentRefs(t *testing.T) {
ws := t.TempDir()
if err := os.WriteFile(filepath.Join(ws, "output.md"), []byte("BAD DRAFT"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(ws, "template.md"), []byte("# tmpl\n"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(ws, "blackboard.json"), []byte(`{"k":1}`), 0o644); err != nil {
t.Fatal(err)
}
sub := filepath.Join(ws, "tasks", "repair_1")
if err := squad.SnapshotInputsIntoSubdir(ws, sub, []string{"output.md"}, "output.md"); err != nil {
t.Fatal(err)
}
src, err := os.ReadFile(filepath.Join(sub, squad.RepairSourceOutputName))
if err != nil || string(src) != "BAD DRAFT" {
t.Fatalf("source snapshot: %s %v", src, err)
}
if _, err := os.Stat(filepath.Join(sub, "output.md")); err == nil {
t.Fatal("must not copy draft onto the target slot")
}
info, err := os.Lstat(filepath.Join(sub, "template.md"))
if err != nil || !info.Mode().IsRegular() {
t.Fatalf("template.md must be a regular snapshot, not a symlink: %v %v", info, err)
}
step := squad.StepSpec{
ID: "step_repair_1",
Role: "writer",
Title: "fix",
Mode: "execute",
Needs: []string{"output.md"},
}
task := squad.BuildContainedTaskMarkdown(step, "output.md")
if squad.TaskMarkdownEscapesParent(task) {
t.Fatalf("contained TASK.md still has parent refs:\n%s", task)
}
if !strings.Contains(task, squad.RepairSourceOutputName) {
t.Fatalf("TASK.md must point at local source snapshot:\n%s", task)
}
}
func TestTrustedLocalDeliverableRejectsSymlinkAndEmpty(t *testing.T) {
ws := t.TempDir()
sub := filepath.Join(ws, "tasks", "repair_1")
if err := os.MkdirAll(sub, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(ws, "output.md"), []byte("PARENT"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.Symlink(filepath.Join("..", "..", "output.md"), filepath.Join(sub, "output.md")); err != nil {
t.Fatal(err)
}
if _, ok := squad.TrustedLocalDeliverable(sub, "output.md"); ok {
t.Fatal("symlink to parent must not count as a trusted local target")
}
_ = os.Remove(filepath.Join(sub, "output.md"))
if err := os.WriteFile(filepath.Join(sub, "output.md"), []byte(" \n"), 0o644); err != nil {
t.Fatal(err)
}
if _, ok := squad.TrustedLocalDeliverable(sub, "output.md"); ok {
t.Fatal("whitespace-only local target must be rejected")
}
}
func TestDecideRepairPromote(t *testing.T) {
if ok, reason := squad.DecideRepairPromote(nil, nil, true); !ok || reason != squad.RepairPromoteOK {
t.Fatalf("clean success must promote, got %v %s", ok, reason)
}
if ok, reason := squad.DecideRepairPromote(assertErr{}, nil, true); ok || reason != squad.RepairDiscardRunError {
t.Fatalf("runErr must void promote, got %v %s", ok, reason)
}
if ok, reason := squad.DecideRepairPromote(nil, []string{"output.md"}, true); ok || reason != squad.RepairDiscardContainment {
t.Fatalf("mutation must void promote, got %v %s", ok, reason)
}
if ok, reason := squad.DecideRepairPromote(nil, nil, false); ok || reason != squad.RepairDiscardNoLocal {
t.Fatalf("missing local must void promote, got %v %s", ok, reason)
}
}
type assertErr struct{}
func (assertErr) Error() string { return "backend timeout" }
func TestParentInputsMutated_CreateAndDelete(t *testing.T) {
ws := t.TempDir()
if err := os.WriteFile(filepath.Join(ws, "output.md"), []byte("OUT"), 0o644); err != nil {
t.Fatal(err)
}
snap := squad.SnapshotWorkspaceInputs(ws)
if snap.BlackboardOK {
t.Fatal("blackboard must start missing")
}
if err := os.WriteFile(filepath.Join(ws, "blackboard.json"), []byte(`{"created":true}`), 0o644); err != nil {
t.Fatal(err)
}
if err := os.Remove(filepath.Join(ws, "output.md")); err != nil {
t.Fatal(err)
}
changed := squad.ParentInputsMutated(ws, snap)
want := map[string]bool{"output.md": true, "blackboard.json": true}
if len(changed) != 2 {
t.Fatalf("create+delete must both count, got %v", changed)
}
for _, name := range changed {
if !want[name] {
t.Fatalf("unexpected mutation %s in %v", name, changed)
}
}
if err := squad.RestoreWorkspaceInputs(ws, snap); err != nil {
t.Fatal(err)
}
got, err := os.ReadFile(filepath.Join(ws, "output.md"))
if err != nil || string(got) != "OUT" {
t.Fatalf("deleted output.md must be restored, got %q %v", got, err)
}
if _, err := os.Stat(filepath.Join(ws, "blackboard.json")); err == nil {
t.Fatal("created blackboard.json must be removed")
}
}
func TestRestoreWorkspaceInputs_PropagatesWriteError(t *testing.T) {
ws := t.TempDir()
snap := squad.WorkspaceInputSnapshot{OutputOK: true, Output: []byte("x")}
blocked := filepath.Join(ws, "output.md")
if err := os.MkdirAll(filepath.Join(blocked, "nested"), 0o755); err != nil {
t.Fatal(err)
}
err := squad.RestoreWorkspaceInputs(ws, snap)
if err == nil {
t.Fatal("restore must fail when output.md is a non-empty directory")
}
if !strings.Contains(err.Error(), "restore") {
t.Fatalf("error must name restore, got %v", err)
}
}
func TestParentSnapshotRestore(t *testing.T) {
ws := t.TempDir()
orig := []byte("ORIGINAL ROOT")
if err := os.WriteFile(filepath.Join(ws, "output.md"), orig, 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(ws, "template.md"), []byte("T0"), 0o644); err != nil {
t.Fatal(err)
}
snap := squad.SnapshotWorkspaceInputs(ws)
if err := os.WriteFile(filepath.Join(ws, "output.md"), []byte("MUTATED BETTER"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(ws, "template.md"), []byte("T1"), 0o644); err != nil {
t.Fatal(err)
}
changed := squad.ParentInputsMutated(ws, snap)
if len(changed) != 2 {
t.Fatalf("expected output+template mutated, got %v", changed)
}
if err := squad.RestoreWorkspaceInputs(ws, snap); err != nil {
t.Fatal(err)
}
got, _ := os.ReadFile(filepath.Join(ws, "output.md"))
if string(got) != string(orig) {
t.Fatalf("root output.md not restored, got %q", got)
}
gotT, _ := os.ReadFile(filepath.Join(ws, "template.md"))
if string(gotT) != "T0" {
t.Fatalf("template.md not restored, got %q", gotT)
}
}