// SPDX-License-Identifier: MIT package executionstatus // Trace contains independent expected transitions for consumers and the viewer. // Guard text is explanatory, not part of the trace oracle. type Trace struct { Family int Name string Events []Event Want []Transition Value Value } func RunTrace(initial State, events []Event) []Transition { out := make([]Transition, 0, len(events)) s := initial for _, e := range events { t := Reduce(s, e) out = append(out, t) s = t.State } return out } func sampleBinding() Binding { // #nosec G101 -- Symbolic credential identity for reducer traces, not an authentication secret. return Binding{Actor: "actor-a", Credential: "credential-a", Conversation: "conversation-a", Mode: "named-user", Addon: "provider-a", Generation: "generation-a", Incarnation: "incarnation-a", ExecutionTool: "execute-a", StatusTool: "status-a"} } func AllStates() []State { b := sampleBinding() return []State{Unbound{}, Unobserved{b, "q"}, Pending{b, "q", 10}, Observed{b, "q", Value{Available, Isolated}}, SettledUnknown{b, "q", "failed"}, Withdrawn{"disabled"}, Closed{}} } func AllEvents() []Event { b := sampleBinding() return []Event{Bind{b, "q", Eligible}, Probe{b, "q", 1, 10}, Reply{b, "q", ValidPayload(Value{Available, Isolated}), 2}, Failure{b, "q", "failed", 2}, Tick{10}, Invalidate{"disabled"}, Close{}} } // Traces are fixtures, not generated by Reduce. Each step specifies ordered // effects explicitly so a reducer regression cannot rewrite its own oracle. func Traces() []Trace { a := sampleBinding() b := a b.Addon = "provider-b" b.ExecutionTool = "execute-b" b.StatusTool = "status-b" b.Conversation = "conversation-b" iso := Value{Available, Isolated} host := Value{Available, Host} unknown := Unknown() var out []Trace var t Trace var current State start := func(f int, name string) { t = Trace{Family: f, Name: name}; current = Unbound{} } step := func(e Event, r Rule, s State, fx ...Effect) { t.Events = append(t.Events, e) t.Want = append(t.Want, Transition{State: s, Rule: r, Effects: fx}) current = s } ignore := func(e Event) { step(e, I, current) } finish := func(v Value) { t.Value = v; out = append(out, t) } bind := func(b Binding, q ObservationID) { step(Bind{b, q, Eligible}, R1, Unobserved{b, q}, Publish{unknown, ""}) } probe := func(b Binding, q ObservationID) { step(Probe{b, q, 1, 10}, R2, Pending{b, q, 10}, Read{b, q, 10}, Publish{unknown, ""}) } reply := func(b Binding, q ObservationID, v Value) { vnorm := v if v.Readiness != Available { vnorm.Isolation = UnknownIsolation } step(Reply{b, q, ValidPayload(v), 2}, R3, Observed{b, q, vnorm}, Publish{vnorm, ""}) } repeat := func(b Binding, q ObservationID) { step(Probe{b, q, 1, 10}, R7, current) } invalidate := func(c Cause, pending bool, q ObservationID) { fx := []Effect{} if pending { fx = append(fx, Cancel{q}) } fx = append(fx, Publish{unknown, c}) step(Invalidate{c}, R6, Withdrawn{c}, fx...) } start(1, "happy path and duplicate") bind(a, "q") probe(a, "q") reply(a, "q", iso) ignore(Reply{a, "q", ValidPayload(host), 3}) finish(iso) start(2, "two racing consumers") bind(a, "q") probe(a, "q") repeat(a, "q") reply(a, "q", host) repeat(a, "q") finish(host) start(3, "timeout cannot retry or reset Q") bind(a, "q") probe(a, "q") step(Tick{10}, R5, SettledUnknown{a, "q", Timeout}, Cancel{"q"}, Publish{unknown, Timeout}) ignore(Reply{a, "q", ValidPayload(iso), 11}) repeat(a, "q") ignore(Probe{a, "q2", 11, 20}) ignore(Bind{a, "q", Eligible}) ignore(Bind{a, "q2", Eligible}) finish(unknown) for _, failure := range []bool{false, true} { name := "malformed" var e Event = Reply{a, "q", InvalidPayload(), 2} c := Malformed if failure { name = "failure" e = Failure{a, "q", "failed", 2} c = "failed" } start(4, name+" settles without retry") bind(a, "q") probe(a, "q") step(e, R4, SettledUnknown{a, "q", c}, Cancel{"q"}, Publish{unknown, c}) repeat(a, "q") ignore(Reply{a, "q", ValidPayload(iso), 3}) finish(unknown) } start(4, "fresh model succeeds") bind(a, "q") probe(a, "q") reply(a, "q", iso) finish(iso) start(5, "disable and authoritative re-enable") bind(a, "q") probe(a, "q") invalidate("disabled", true, "q") ignore(Reply{a, "q", ValidPayload(iso), 2}) ignore(Probe{a, "q", 2, 10}) bind(a, "q2") probe(a, "q2") reply(a, "q2", iso) finish(iso) for _, replacement := range []Binding{b, func() Binding { x := a; x.Incarnation = "incarnation-new"; return x }()} { start(6, "replacement "+replacement.Incarnation+replacement.Addon) bind(a, "q") probe(a, "q") reply(a, "q", iso) invalidate("replaced", false, "q") bind(replacement, "q2") probe(replacement, "q2") ignore(Reply{a, "q", ValidPayload(iso), 2}) reply(replacement, "q2", host) finish(host) } for _, prior := range []string{"pending", "observed", "settled"} { start(7, "unexpected bind from "+prior) bind(a, "q") probe(a, "q") if prior == "observed" { reply(a, "q", iso) } if prior == "settled" { step(Failure{a, "q", "failed", 2}, R4, SettledUnknown{a, "q", "failed"}, Cancel{"q"}, Publish{unknown, "failed"}) } fx := []Effect{} if prior == "pending" { fx = append(fx, Cancel{"q"}) } fx = append(fx, Publish{unknown, "binding-changed"}) step(Bind{b, "q2", Eligible}, R11, Withdrawn{"binding-changed"}, fx...) ignore(Reply{a, "q", ValidPayload(iso), 2}) bind(b, "q2") probe(b, "q2") reply(b, "q2", host) finish(host) } start(8, "mismatches and deadline equality") ignore(Probe{a, "q", 1, 10}) ignore(Reply{a, "q", ValidPayload(iso), 1}) bind(a, "q") ignore(Probe{a, "q2", 1, 10}) ignore(Probe{b, "q", 1, 10}) ignore(Reply{a, "q", ValidPayload(iso), 1}) probe(a, "q") ignore(Tick{9}) ignore(Reply{b, "q", ValidPayload(iso), 10}) step(Reply{a, "q", InvalidPayload(), 10}, R5, SettledUnknown{a, "q", Timeout}, Cancel{"q"}, Publish{unknown, Timeout}) finish(unknown) start(9, "close pending then all events") bind(a, "q") probe(a, "q") step(Close{}, R10, Closed{}, Cancel{"q"}, Publish{unknown, ""}) for _, e := range AllEvents() { ignore(e) } finish(unknown) for i, s := range AllStates() { start(9, "close constructor "+string(rune('0'+i))) switch s.(type) { case Unbound: case Unobserved: bind(a, "q") case Pending: bind(a, "q") probe(a, "q") case Observed: bind(a, "q") probe(a, "q") reply(a, "q", iso) case SettledUnknown: step(Bind{a, "q", "denied"}, R1, SettledUnknown{a, "q", "denied"}, Publish{unknown, "denied"}) case Withdrawn: invalidate("disabled", false, "q") case Closed: step(Close{}, R10, Closed{}, Publish{unknown, ""}) } if _, ok := s.(Closed); ok { ignore(Close{}) } else if _, ok := s.(Pending); ok { step(Close{}, R10, Closed{}, Cancel{"q"}, Publish{unknown, ""}) } else { step(Close{}, R10, Closed{}, Publish{unknown, ""}) } for _, e := range AllEvents() { ignore(e) } finish(unknown) } for _, reason := range []Eligibility{"no-contract", "denied"} { start(10, string(reason)) step(Bind{a, "q", reason}, R1, SettledUnknown{a, "q", Cause(reason)}, Publish{unknown, Cause(reason)}) repeat(a, "q") ignore(Reply{a, "q", ValidPayload(iso), 2}) ignore(Bind{a, "q", Eligible}) ignore(Bind{a, "q2", Eligible}) finish(unknown) } start(10, "expired probe") bind(a, "q") step(Probe{a, "q", 10, 10}, R9, SettledUnknown{a, "q", Timeout}, Publish{unknown, Timeout}) repeat(a, "q") finish(unknown) other := a other.Actor = "actor-b" for _, actor := range []Binding{a, other} { start(11, "actor separation "+actor.Actor) bind(actor, "q") probe(actor, "q") foreign := other if actor == other { foreign = a } ignore(Reply{foreign, "q", ValidPayload(iso), 2}) reply(actor, "q", Value{Unavailable, Isolated}) invalidate("permission-lost", false, "q") finish(unknown) } start(11, "available unknown is not enforcement") bind(a, "q") probe(a, "q") reply(a, "q", Value{Available, UnknownIsolation}) finish(Value{Available, UnknownIsolation}) return out }