a0209a2951
The .orchestra directory showed up as untracked work in the very worktree whose own launch context said "uncommitted changes: false". It would have polluted the quality gate, the review diff, and the agent's reading of git status. A .gitignore of "*" inside the directory ignores it including itself. The worker's done marker lives there too and had the same problem. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
128 lines
4.1 KiB
Go
128 lines
4.1 KiB
Go
package herdr
|
||
|
||
import (
|
||
"context"
|
||
"os"
|
||
"os/exec"
|
||
"path/filepath"
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
)
|
||
|
||
func TestTmuxBackendStartsCapturesPromptsAndKillsClaude(t *testing.T) {
|
||
if testing.Short() {
|
||
t.Skip("requires tmux")
|
||
}
|
||
dir := t.TempDir()
|
||
harness := filepath.Join(dir, "fake-claude")
|
||
script := "#!/bin/sh\nprintf '❯ ready\\n'\nwhile IFS= read -r line; do printf 'GOT:%s\\n' \"$line\"; done\n"
|
||
if err := os.WriteFile(harness, []byte(script), 0o755); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
b := NewTmuxBackend(filepath.Join(t.TempDir(), "tmux.sock"), harness)
|
||
if err := b.Check(context.Background()); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
s, err := b.StartAgent(context.Background(), dir, dir, "", "claude", "tmux-backend-test")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
t.Cleanup(func() { _ = b.Kill(context.Background(), s) })
|
||
if s.Worktree != dir || s.Harness != "claude" || !strings.Contains(s.PaneID, ":") || !strings.Contains(s.PaneID, ".") {
|
||
t.Fatalf("unexpected session: %+v", s)
|
||
}
|
||
if err := b.Prompt(context.Background(), s.PaneID, "hello from Orchestra", 0); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
deadline := time.Now().Add(2 * time.Second)
|
||
for {
|
||
capture, err := b.PaneCapture(context.Background(), s, "recent")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if strings.Contains(capture, "GOT:hello from Orchestra") {
|
||
break
|
||
}
|
||
if time.Now().After(deadline) {
|
||
t.Fatalf("prompt was not captured: %q", capture)
|
||
}
|
||
time.Sleep(20 * time.Millisecond)
|
||
}
|
||
for _, line := range []string{"/clear", "@HANDOFF.md"} {
|
||
if err := b.SendText(context.Background(), s, line); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := b.SendKeys(context.Background(), s, []string{"ENTER"}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
deadline = time.Now().Add(2 * time.Second)
|
||
for {
|
||
capture, err := b.PaneCapture(context.Background(), s, "recent")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if strings.Contains(capture, "GOT:/clear") && strings.Contains(capture, "GOT:@HANDOFF.md") {
|
||
break
|
||
}
|
||
if time.Now().After(deadline) {
|
||
t.Fatalf("Claude rollover lines were not captured: %q", capture)
|
||
}
|
||
time.Sleep(20 * time.Millisecond)
|
||
}
|
||
if status, err := b.AgentStatus(context.Background(), s); err != nil || status != "idle" {
|
||
t.Fatalf("status=%q err=%v", status, err)
|
||
}
|
||
if err := b.ReleaseAgent(context.Background(), s, "claude"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := b.Kill(context.Background(), s); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if _, err := b.PaneCapture(context.Background(), s, "recent"); err == nil {
|
||
t.Fatal("killed tmux session remained readable")
|
||
}
|
||
}
|
||
|
||
func TestTmuxBackendRefusesUnverifiedHarnesses(t *testing.T) {
|
||
b := NewTmuxBackend("test", "true")
|
||
if _, err := b.StartAgent(context.Background(), "", t.TempDir(), "", "codex", "task"); err == nil {
|
||
t.Fatal("tmux backend accepted Codex before its terminal behavior was implemented")
|
||
}
|
||
}
|
||
|
||
func TestTmuxSessionNameKeepsCollisionResistantSuffix(t *testing.T) {
|
||
a := tmuxSessionName(strings.Repeat("same-prefix", 10) + "-one")
|
||
b := tmuxSessionName(strings.Repeat("same-prefix", 10) + "-two")
|
||
if a == b || len(a) > 64 || len(b) > 64 {
|
||
t.Fatalf("unsafe tmux session names %q %q", a, b)
|
||
}
|
||
}
|
||
|
||
// The launch dump is Orchestra's scratch space, not the session's work. A
|
||
// worktree that starts dirty pollutes the gate, the review diff, and the
|
||
// agent's own reading of `git status`.
|
||
func TestWriteLaunchContextLeavesTheWorktreeClean(t *testing.T) {
|
||
dir := t.TempDir()
|
||
for _, args := range [][]string{{"init"}, {"config", "user.email", "t@t"}, {"config", "user.name", "t"}, {"commit", "--allow-empty", "-m", "init"}} {
|
||
if out, err := exec.Command("git", append([]string{"-C", dir}, args...)...).CombinedOutput(); err != nil {
|
||
t.Fatalf("git %v: %v: %s", args, err, out)
|
||
}
|
||
}
|
||
if err := WriteLaunchContext(dir, "the instruction"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
b, err := os.ReadFile(filepath.Join(dir, LaunchContextFile))
|
||
if err != nil || string(b) != "the instruction" {
|
||
t.Fatalf("launch context = %q, err %v", b, err)
|
||
}
|
||
out, err := exec.Command("git", "-C", dir, "status", "--short").CombinedOutput()
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if strings.TrimSpace(string(out)) != "" {
|
||
t.Fatalf("worktree is dirty after a launch dump:\n%s", out)
|
||
}
|
||
}
|