Files
orchestra/internal/herdr/tmux_test.go
T
kami 7f12c7fc37 v3 workflow: intent, phases, review, submission, enforcement, burn-in
The v3 stack, previously an uncommitted working tree, plus this session's two
units and the burn-in instrument. This commit is the burn-in build identity:
coordinator and worker must both report this revision before a task is created.

Workflow (earlier sessions, uncommitted until now): human decision events and
reduction, source cursors and reconcile-before-launch, turn-boundary
reconciliation, internal/agentctx as the single renderer, ace-fca phases with
sealed artifacts, the trajectory gate, bounded grilling, independent review,
task pr enforcement, and human review reflection.

Capability restrictions at the agent boundary: an authz.Agent surface at
GatedWrite may ask and may not act. It also fixes two bugs the unit exposed --
gated surfaces could not reach the two endpoints written for them, and
RequestHumanDecision would block an unowned task while rejecting a question
from the session that did own it.

Turn-boundary reconcile-failure escalation: a streak of consecutive failures
asks the session to hand off, fenced on the lease epoch, with reconcile_failure
as a real handoff reason. The worker was dropping the coordinator's verdict on
the floor; it now acts on it.

Burn-in: herdr.WriteLaunchContext dumps the exact agentctx.Build result to
<worktree>/.orchestra/launch.md at every launch, local and federated. BURNIN.md
is the runbook. deploy/build.sh stamps both binaries from one commit.

go build, go vet and go test ./... pass, 20 packages.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 18:31:20 +04:00

101 lines
3.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package herdr
import (
"context"
"os"
"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)
}
}