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) } } // A durable event must validate the same way forever. Comparing until_ns // against the current clock made every lease event fail once it expired, so // replaying the log after a restart refused the store's own history and the // coordinator could not start. Found live: the API entered a restart loop // logging "invalid event: until_ns required". func TestLeaseEventsValidateAfterTheyExpire(t *testing.T) { past := float64(time.Now().Add(-24 * time.Hour).UnixNano()) for _, e := range []Event{ {ID: "a", Type: "TaskLeased", TaskID: "t", Version: 2, Surface: "system", Payload: mustPayload(map[string]any{ "harness_id": "h1", "until_ns": past, "expected_version": 1, })}, {ID: "b", Type: "TaskLeaseRenewed", TaskID: "t", Version: 3, Surface: "system", Payload: mustPayload(map[string]any{ "harness_id": "h1", "until_ns": past, "expected_version": 2, })}, } { if err := ValidateEvent(e); err != nil { t.Fatalf("%s failed validation after expiry: %v", e.Type, err) } } // Well-formedness is still checked. if err := ValidateEvent(Event{ID: "c", Type: "TaskLeaseRenewed", TaskID: "t", Version: 3, Surface: "system", Payload: mustPayload(map[string]any{ "harness_id": "h1", "expected_version": 2, })}); err == nil { t.Fatal("a renewal with no until_ns was accepted") } } func mustPayload(v map[string]any) []byte { b, err := json.Marshal(v) if err != nil { panic(err) } return b }