Files
orchestra/internal/domain/decision_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

292 lines
11 KiB
Go

package domain
import (
"encoding/json"
"errors"
"testing"
"time"
)
func decisionEvent(t *testing.T, id, taskID string, at time.Time, p map[string]any) Event {
t.Helper()
b, err := json.Marshal(p)
if err != nil {
t.Fatal(err)
}
e := Event{ID: id, Type: EventHumanDecisionRecorded, TaskID: taskID, At: at, Payload: b, Surface: "web", SchemaVersion: CurrentEventSchema}
if err := ValidateEvent(e); err != nil {
t.Fatalf("event %s should validate: %v", id, err)
}
return e
}
func decision(t *testing.T, id, taskID string, at time.Time, kind HumanDecisionKind, subject, value string, supersedes ...string) Event {
t.Helper()
p := map[string]any{
"decision_id": id,
"kind": string(kind),
"subject": subject,
"value": value,
"source": map[string]any{"provider": "web", "external_id": "c1"},
}
if len(supersedes) > 0 {
p["supersedes"] = supersedes
}
return decisionEvent(t, "e-"+id, taskID, at, p)
}
var t0 = time.Date(2026, 8, 26, 12, 0, 0, 0, time.UTC)
// The one that matters: a correction outranks both the original contract and
// whatever the handoff says the next step is.
func TestCorrectionOverridesContractAndHandoff(t *testing.T) {
task := Task{
ID: "task-1",
State: StateLeased,
Description: "implement a",
Acceptance: []string{"a works"},
// Handoff prose says "next: implement a". It must not reach authority.
HandoffRef: "0000000000000000000000000000000000000000000000000000000000000000",
}
events := []Event{decision(t, "d1", "task-1", t0, HumanDecisionCorrection, "strategy", "use b")}
got, err := ReduceIntent(task, events)
if err != nil {
t.Fatal(err)
}
if got.Task.Description != "implement a" || got.Task.Acceptance[0] != "a works" {
t.Fatalf("contract must survive unmodified, got %+v", got.Task)
}
if len(got.Decisions) != 1 {
t.Fatalf("want 1 standing decision, got %d", len(got.Decisions))
}
if d := got.Decisions[0]; d.Value != "use b" || d.Subject != "strategy" || d.Kind != HumanDecisionCorrection {
t.Fatalf("standing decision = %+v", d)
}
if got.Task.HandoffRef != task.HandoffRef {
t.Fatal("reducer must not rewrite handoff fields")
}
}
func TestExplicitSupersessionRetiresPredecessor(t *testing.T) {
events := []Event{
decision(t, "d1", "task-1", t0, HumanDecisionChoice, "strategy", "use a"),
decision(t, "d2", "task-1", t0.Add(time.Hour), HumanDecisionChoice, "strategy", "use b", "d1"),
}
got, err := ReduceIntent(Task{ID: "task-1"}, events)
if err != nil {
t.Fatal(err)
}
if len(got.Decisions) != 1 || got.Decisions[0].ID != "d2" {
t.Fatalf("want only d2 standing, got %+v", got.Decisions)
}
}
// Same subject, no supersedes: both stand. Inferring replacement from subject
// is exactly the ambiguity the explicit edge exists to avoid.
func TestSameSubjectWithoutSupersedesKeepsBoth(t *testing.T) {
events := []Event{
decision(t, "d1", "task-1", t0, HumanDecisionConstraint, "strategy", "no new deps"),
decision(t, "d2", "task-1", t0.Add(time.Hour), HumanDecisionConstraint, "strategy", "stdlib only"),
}
got, err := ReduceIntent(Task{ID: "task-1"}, events)
if err != nil {
t.Fatal(err)
}
if len(got.Decisions) != 2 {
t.Fatalf("want both standing, got %+v", got.Decisions)
}
}
func TestStandaloneSupersededEventRetracts(t *testing.T) {
b, _ := json.Marshal(map[string]any{"decision_id": "d1"})
retract := Event{ID: "e-retract", Type: EventHumanDecisionSuperseded, TaskID: "task-1", At: t0.Add(time.Hour), Payload: b, Surface: "web", SchemaVersion: CurrentEventSchema}
if err := ValidateEvent(retract); err != nil {
t.Fatal(err)
}
events := []Event{decision(t, "d1", "task-1", t0, HumanDecisionAnswer, "q", "yes"), retract}
got, err := ReduceIntent(Task{ID: "task-1"}, events)
if err != nil {
t.Fatal(err)
}
if len(got.Decisions) != 0 {
t.Fatalf("want nothing standing, got %+v", got.Decisions)
}
}
// Log order must not change the answer. Every permutation of a supersession
// chain reduces to the same standing set, including the one where the
// superseding decision is appended before its target.
func TestReductionIsOrderIndependent(t *testing.T) {
d1 := decision(t, "d1", "task-1", t0, HumanDecisionChoice, "strategy", "use a")
d2 := decision(t, "d2", "task-1", t0.Add(time.Hour), HumanDecisionChoice, "strategy", "use b", "d1")
d3 := decision(t, "d3", "task-1", t0.Add(2*time.Hour), HumanDecisionConstraint, "deps", "stdlib only")
for _, order := range [][]Event{
{d1, d2, d3}, {d3, d2, d1}, {d2, d1, d3}, {d2, d3, d1}, {d3, d1, d2}, {d1, d3, d2},
} {
got, err := ReduceIntent(Task{ID: "task-1"}, order)
if err != nil {
t.Fatal(err)
}
if len(got.Decisions) != 2 || got.Decisions[0].ID != "d2" || got.Decisions[1].ID != "d3" {
t.Fatalf("order %v reduced to %+v", ids(order), got.Decisions)
}
}
}
func ids(events []Event) []string {
out := make([]string, 0, len(events))
for _, e := range events {
out = append(out, e.ID)
}
return out
}
func TestDuplicateReplayIsIdempotent(t *testing.T) {
d1 := decision(t, "d1", "task-1", t0, HumanDecisionAnswer, "q", "yes")
got, err := ReduceIntent(Task{ID: "task-1"}, []Event{d1, d1, d1})
if err != nil {
t.Fatal(err)
}
if len(got.Decisions) != 1 {
t.Fatalf("want 1 decision, got %d", len(got.Decisions))
}
}
func TestConflictingDuplicateDecisionIDRejected(t *testing.T) {
a := decision(t, "d17", "task-1", t0, HumanDecisionChoice, "strategy", "use a")
b := decision(t, "d17", "task-1", t0, HumanDecisionChoice, "strategy", "use b")
if _, err := ReduceIntent(Task{ID: "task-1"}, []Event{a, b}); !errors.Is(err, ErrInvalid) {
t.Fatalf("want ErrInvalid, got %v", err)
}
// Reversing the two must fail the same way. Order must never decide.
if _, err := ReduceIntent(Task{ID: "task-1"}, []Event{b, a}); !errors.Is(err, ErrInvalid) {
t.Fatalf("reversed: want ErrInvalid, got %v", err)
}
// A differing timestamp is also a conflict, because At orders the output.
c := decision(t, "d17", "task-1", t0.Add(time.Hour), HumanDecisionChoice, "strategy", "use a")
if _, err := ReduceIntent(Task{ID: "task-1"}, []Event{a, c}); !errors.Is(err, ErrInvalid) {
t.Fatalf("timestamp conflict: want ErrInvalid, got %v", err)
}
// An identical replay, including supersedes, still reduces cleanly.
base := decision(t, "d1", "task-1", t0, HumanDecisionChoice, "s", "a")
sup := decision(t, "d2", "task-1", t0.Add(time.Hour), HumanDecisionChoice, "s", "b", "d1")
got, err := ReduceIntent(Task{ID: "task-1"}, []Event{base, sup, sup, base})
if err != nil {
t.Fatal(err)
}
if len(got.Decisions) != 1 || got.Decisions[0].ID != "d2" {
t.Fatalf("standing set = %+v", got.Decisions)
}
}
func TestUnknownSupersedesTargetRejected(t *testing.T) {
events := []Event{decision(t, "d1", "task-1", t0, HumanDecisionChoice, "strategy", "use b", "ghost")}
if _, err := ReduceIntent(Task{ID: "task-1"}, events); !errors.Is(err, ErrInvalid) {
t.Fatalf("want ErrInvalid, got %v", err)
}
}
// A cross-task reference is an unknown target, not a silent no-op: the other
// task's decision is invisible to this reduction and cannot be retired here.
func TestCannotSupersedeAnotherTasksDecision(t *testing.T) {
events := []Event{
decision(t, "other", "task-2", t0, HumanDecisionChoice, "strategy", "use a"),
decision(t, "d1", "task-1", t0.Add(time.Hour), HumanDecisionChoice, "strategy", "use b", "other"),
}
if _, err := ReduceIntent(Task{ID: "task-1"}, events); !errors.Is(err, ErrInvalid) {
t.Fatalf("want ErrInvalid, got %v", err)
}
// And the other task's own reduction is unaffected by task-1's events.
got, err := ReduceIntent(Task{ID: "task-2"}, events)
if err != nil {
t.Fatal(err)
}
if len(got.Decisions) != 1 || got.Decisions[0].ID != "other" {
t.Fatalf("task-2 standing set = %+v", got.Decisions)
}
}
func TestSupersessionCyclesRejected(t *testing.T) {
for name, events := range map[string][]Event{
"self": {decision(t, "d1", "task-1", t0, HumanDecisionChoice, "s", "v", "d1")},
"pair": {
decision(t, "d1", "task-1", t0, HumanDecisionChoice, "s", "a", "d2"),
decision(t, "d2", "task-1", t0.Add(time.Hour), HumanDecisionChoice, "s", "b", "d1"),
},
"three": {
decision(t, "d1", "task-1", t0, HumanDecisionChoice, "s", "a", "d3"),
decision(t, "d2", "task-1", t0.Add(time.Hour), HumanDecisionChoice, "s", "b", "d1"),
decision(t, "d3", "task-1", t0.Add(2*time.Hour), HumanDecisionChoice, "s", "c", "d2"),
},
} {
if _, err := ReduceIntent(Task{ID: "task-1"}, events); !errors.Is(err, ErrInvalid) {
t.Fatalf("%s cycle: want ErrInvalid, got %v", name, err)
}
}
}
// A transitive chain leaves only the head standing.
func TestTransitiveChainKeepsOnlyHead(t *testing.T) {
events := []Event{
decision(t, "d1", "task-1", t0, HumanDecisionChoice, "s", "a"),
decision(t, "d2", "task-1", t0.Add(time.Hour), HumanDecisionChoice, "s", "b", "d1"),
decision(t, "d3", "task-1", t0.Add(2*time.Hour), HumanDecisionChoice, "s", "c", "d2"),
}
got, err := ReduceIntent(Task{ID: "task-1"}, events)
if err != nil {
t.Fatal(err)
}
if len(got.Decisions) != 1 || got.Decisions[0].ID != "d3" {
t.Fatalf("standing set = %+v", got.Decisions)
}
}
// Lifecycle events are inert to the reducer, so authority cannot be smuggled
// in through a release, a pickup, or an amendment.
func TestLifecycleEventsCarryNoAuthority(t *testing.T) {
amend, _ := json.Marshal(map[string]any{"description": "implement a instead"})
events := []Event{
{ID: "e1", Type: "TaskAmended", TaskID: "task-1", At: t0, Payload: amend, Surface: "system"},
decision(t, "d1", "task-1", t0.Add(time.Hour), HumanDecisionCorrection, "strategy", "use b"),
}
got, err := ReduceIntent(Task{ID: "task-1", Description: "implement a"}, events)
if err != nil {
t.Fatal(err)
}
if len(got.Decisions) != 1 || got.Decisions[0].Value != "use b" {
t.Fatalf("standing set = %+v", got.Decisions)
}
}
func TestDecisionEventValidation(t *testing.T) {
base := func() map[string]any {
return map[string]any{
"decision_id": "d1", "kind": "correction", "subject": "strategy", "value": "use b",
"source": map[string]any{"provider": "web"},
}
}
if err := ValidatePayload(EventHumanDecisionRecorded, base()); err != nil {
t.Fatalf("valid payload rejected: %v", err)
}
for name, mutate := range map[string]func(map[string]any){
"no decision_id": func(p map[string]any) { delete(p, "decision_id") },
"no subject": func(p map[string]any) { delete(p, "subject") },
"no value": func(p map[string]any) { delete(p, "value") },
"bad kind": func(p map[string]any) { p["kind"] = "vibes" },
"no source": func(p map[string]any) { delete(p, "source") },
"no provider": func(p map[string]any) { p["source"] = map[string]any{} },
"supersedes str": func(p map[string]any) { p["supersedes"] = "d0" },
"supersedes nil": func(p map[string]any) { p["supersedes"] = []any{""} },
} {
p := base()
mutate(p)
if err := ValidatePayload(EventHumanDecisionRecorded, p); !errors.Is(err, ErrInvalid) {
t.Fatalf("%s: want ErrInvalid, got %v", name, err)
}
}
if err := ValidatePayload(EventHumanDecisionSuperseded, map[string]any{}); !errors.Is(err, ErrInvalid) {
t.Fatalf("empty supersede payload: want ErrInvalid, got %v", err)
}
}