package continuity import ( "crypto/sha256" "encoding/hex" "os" "os/exec" "path/filepath" "strings" "testing" "orchestra/internal/store" ) func TestScratchCommitCapturesAllGitStatesExceptProtocolMarkers(t *testing.T) { repo := t.TempDir() run := func(args ...string) { t.Helper() if out, err := exec.Command("git", append([]string{"-C", repo}, args...)...).CombinedOutput(); err != nil { t.Fatalf("git %v: %v: %s", args, err, out) } } run("init") run("config", "user.email", "t@t") run("config", "user.name", "t") for _, name := range []string{"TASK.md", "deleted.txt", "renamed.txt", "staged.txt"} { if err := os.WriteFile(filepath.Join(repo, name), []byte(name), 0644); err != nil { t.Fatal(err) } } run("add", "-A") run("commit", "-m", "base") if err := os.WriteFile(filepath.Join(repo, "staged.txt"), []byte("staged change"), 0644); err != nil { t.Fatal(err) } run("add", "staged.txt") if err := os.Remove(filepath.Join(repo, "deleted.txt")); err != nil { t.Fatal(err) } run("mv", "renamed.txt", "renamed-new.txt") if err := os.WriteFile(filepath.Join(repo, "untracked.txt"), []byte("new"), 0644); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(repo, ".orchestra-handoff-report.md"), []byte("protocol"), 0644); err != nil { t.Fatal(err) } if err := ScratchCommit(repo, "orchestra/scratch/test", "checkpoint"); err != nil { t.Fatal(err) } for _, want := range []string{"staged.txt", "renamed-new.txt", "untracked.txt"} { if err := exec.Command("git", "-C", repo, "cat-file", "-e", "HEAD:"+want).Run(); err != nil { t.Fatalf("checkpoint omitted %s: %v", want, err) } } if err := exec.Command("git", "-C", repo, "cat-file", "-e", "HEAD:deleted.txt").Run(); err == nil { t.Fatal("checkpoint retained deleted file") } if err := exec.Command("git", "-C", repo, "cat-file", "-e", "HEAD:.orchestra-handoff-report.md").Run(); err == nil { t.Fatal("checkpoint committed protocol marker") } } func TestHandoffCASAndPickup(t *testing.T) { root := t.TempDir() run := func(a ...string) { c := exec.Command("git", append([]string{"-C", root}, a...)...) c.Env = append(os.Environ(), "GIT_AUTHOR_NAME=test", "GIT_AUTHOR_EMAIL=test@example", "GIT_COMMITTER_NAME=test", "GIT_COMMITTER_EMAIL=test@example") if b, e := c.CombinedOutput(); e != nil { t.Fatalf("git: %s %v", b, e) } } os.WriteFile(filepath.Join(root, "TASK.md"), []byte("original"), 0644) run("init") run("add", "TASK.md") run("commit", "-m", "init") head := runOut(t, root, "rev-parse", "HEAD") task := sha256.Sum256([]byte("original")) s, _ := store.Open(t.TempDir()) h := Handoff{Meta: Meta{ID: "h1", Reason: "manual"}, Anchor: Anchor{GitSHA: head, Branch: "main"}, Action: "run the focused tests", Command: "go test ./..."} ref, e := Save(h, s) if e != nil { t.Fatal(e) } got, e := Load(ref, s) if e != nil || got.Meta.ID != "h1" { t.Fatalf("load: %v", e) } if e = ValidatePickup(root, got, hex.EncodeToString(task[:])); e != nil { t.Fatal(e) } } func runOut(t *testing.T, root string, a ...string) string { b, e := exec.Command("git", append([]string{"-C", root}, a...)...).Output() if e != nil { t.Fatal(e) } return string(b[:len(b)-1]) } func TestDecodeRejectsUnknownKnowledgeFields(t *testing.T) { _, e := Decode([]byte(`{"meta":{"id":"x","reason":"manual","rotation_index":0},"unknown":1}`)) if e == nil { t.Fatal("expected strict schema error") } } func TestHandoffRejectsFabricatedOrProseAuthoredFields(t *testing.T) { base := Handoff{Meta: Meta{ID: "h", Reason: "manual"}, Anchor: Anchor{GitSHA: strings.Repeat("a", 40), Branch: "main"}, Action: "run the focused tests"} if err := base.Validate(); err != nil { t.Fatal(err) } badAction := base badAction.Action = "Continue the task from the handoff" if err := badAction.Validate(); err == nil { t.Fatal("expected circular action rejection") } badProse := base badProse.Remaining = []string{"short\n# markdown heading"} if err := badProse.Validate(); err == nil { t.Fatal("expected prose-in-list rejection") } if _, err := Decode([]byte(`{"meta":{"id":"h","reason":"manual","rotation_index":0},"anchor":{"git_sha":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","branch":"main"},"goal":"fabricated","action":"run tests"}`)); err == nil { t.Fatal("expected goal rejection") } } func TestScratchCommitProtectsTask(t *testing.T) { root := t.TempDir() run := func(a ...string) { c := exec.Command("git", append([]string{"-C", root}, a...)...) c.Env = append(os.Environ(), "GIT_AUTHOR_NAME=test", "GIT_AUTHOR_EMAIL=test@example", "GIT_COMMITTER_NAME=test", "GIT_COMMITTER_EMAIL=test@example") if b, e := c.CombinedOutput(); e != nil { t.Fatalf("git: %s %v", b, e) } } os.WriteFile(filepath.Join(root, "TASK.md"), []byte("fixed"), 0644) run("init") run("add", ".") run("commit", "-m", "init") os.WriteFile(filepath.Join(root, "wip.txt"), []byte("wip"), 0644) if err := ScratchCommit(root, "scratch/task", "wip"); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(root, "TASK.md"), []byte("changed"), 0644); err != nil { t.Fatal(err) } if err := ScratchCommit(root, "scratch/other", "bad"); err == nil { t.Fatal("expected immutable TASK.md rejection") } } func TestVerifyTaskFileRejectsMutation(t *testing.T) { root := t.TempDir() if err := os.WriteFile(filepath.Join(root, "TASK.md"), []byte("task"), 0644); err != nil { t.Fatal(err) } sum := sha256.Sum256([]byte("task")) if err := VerifyTaskFile(root, hex.EncodeToString(sum[:])); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(root, "TASK.md"), []byte("changed"), 0644); err != nil { t.Fatal(err) } if err := VerifyTaskFile(root, hex.EncodeToString(sum[:])); err == nil { t.Fatal("expected immutable task check to fail") } } // TestActionCarriesTheBudgetOfBothLinesItIsMadeOf guards F31. Action is not one // authored line: parseHandoffAnswer joins the agent's NEXT and WHY answers, and // the prompt asks for a sentence each. Holding the join to a single line's // budget rejected two ordinary sentences, which failed every rotation on // workpc at "prose smuggled into list" and pinned the worker slot behind it. func TestActionCarriesTheBudgetOfBothLinesItIsMadeOf(t *testing.T) { next := strings.Repeat("a", maxAuthoredLine) why := strings.Repeat("b", maxAuthoredLine) h := Handoff{ Meta: Meta{ID: "h1", Reason: "threshold"}, Anchor: Anchor{GitSHA: strings.Repeat("0", 40), Branch: "main"}, Action: next + " — " + why, } if err := h.Validate(); err != nil { t.Fatalf("two full-length answers rejected: %v", err) } h.Action = next + " — " + why + "c" if err := h.Validate(); err == nil { t.Fatal("an action past both budgets was accepted") } } // An over-budget field must not be reported as smuggled prose. The agent // cannot shorten what it is not told is too long, so it retried the same text. func TestOverBudgetFieldNamesLengthNotProse(t *testing.T) { h := Handoff{ Meta: Meta{ID: "h1", Reason: "threshold"}, Anchor: Anchor{GitSHA: strings.Repeat("0", 40), Branch: "main"}, Action: "do the next thing", Remaining: []string{strings.Repeat("x", maxAuthoredLine+1)}, } err := h.Validate() if err == nil { t.Fatal("over-budget item accepted") } if !strings.Contains(err.Error(), "limit") || strings.Contains(err.Error(), "smuggled") { t.Fatalf("error names the wrong cause: %v", err) } }