5ae96c324f
Co-authored-by: nova28 <17953305+nova28@users.noreply.github.com> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
280 lines
10 KiB
Go
280 lines
10 KiB
Go
package engine
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"go/ast"
|
|
"go/parser"
|
|
"go/token"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// TestSessionShapedAndSessionIndependentKindsPartitionCompiledOnEnterKinds
|
|
// asserts the two classification maps in this file are exhaustive and
|
|
// disjoint over exactly the ActionKind values CompileOnEnterAction (types.go)
|
|
// can emit for a single on_enter declaration — the per-kind switch
|
|
// compileOnEnter delegates to. It parses that function's source rather than
|
|
// hand-listing the kinds, so a kind added to the compiler later without a
|
|
// deliberate classification decision fails this test instead of silently
|
|
// defaulting into double execution (dispatched from both
|
|
// HandleTriggerSessionShapedOnly and DispatchStepEntry) or silence
|
|
// (dispatched from neither) — see
|
|
// docs/specs/office/system-design/step-entry-sequence-execution.md
|
|
// ("The two lists together are exhaustive").
|
|
func TestSessionShapedAndSessionIndependentKindsPartitionCompiledOnEnterKinds(t *testing.T) {
|
|
compiled, err := actionKindsAssignedInFunc("types.go", "CompileOnEnterAction")
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, compiled, "CompileOnEnterAction must assign at least one ActionKind for this test to be meaningful")
|
|
|
|
for _, kind := range compiled {
|
|
shaped := isSessionShapedActionKind(kind)
|
|
independent := isSessionIndependentActionKind(kind)
|
|
assert.True(t, shaped || independent,
|
|
"compiled on_enter kind %q is classified in neither sessionShapedActionKinds nor sessionIndependentActionKinds", kind)
|
|
assert.False(t, shaped && independent,
|
|
"compiled on_enter kind %q is classified in both sessionShapedActionKinds and sessionIndependentActionKinds", kind)
|
|
}
|
|
|
|
for kind := range sessionShapedActionKinds {
|
|
assert.Contains(t, compiled, kind,
|
|
"sessionShapedActionKinds lists %q, which CompileOnEnterAction no longer emits", kind)
|
|
}
|
|
for kind := range sessionIndependentActionKinds {
|
|
assert.Contains(t, compiled, kind,
|
|
"sessionIndependentActionKinds lists %q, which CompileOnEnterAction no longer emits", kind)
|
|
}
|
|
}
|
|
|
|
// actionKindsAssignedInFunc parses file (relative to this package directory)
|
|
// and returns every ActionKind identifier assigned to a "Kind" struct field
|
|
// inside the named function's body, in source order with duplicates removed.
|
|
func actionKindsAssignedInFunc(file, funcName string) ([]ActionKind, error) {
|
|
fset := token.NewFileSet()
|
|
f, err := parser.ParseFile(fset, file, nil, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var fn *ast.FuncDecl
|
|
for _, decl := range f.Decls {
|
|
if d, ok := decl.(*ast.FuncDecl); ok && d.Name.Name == funcName {
|
|
fn = d
|
|
break
|
|
}
|
|
}
|
|
if fn == nil {
|
|
return nil, errors.New("function " + funcName + " not found in " + file)
|
|
}
|
|
|
|
seen := map[ActionKind]bool{}
|
|
var kinds []ActionKind
|
|
ast.Inspect(fn.Body, func(n ast.Node) bool {
|
|
kv, ok := n.(*ast.KeyValueExpr)
|
|
if !ok {
|
|
return true
|
|
}
|
|
key, ok := kv.Key.(*ast.Ident)
|
|
if !ok || key.Name != "Kind" {
|
|
return true
|
|
}
|
|
ident, ok := kv.Value.(*ast.Ident)
|
|
if !ok {
|
|
return true
|
|
}
|
|
kind := ActionKind(ident.Name)
|
|
// The Ident's textual name is the ActionKind constant's Go
|
|
// identifier (e.g. "ActionEnablePlanMode"), not its string
|
|
// value ("enable_plan_mode") — resolve it below.
|
|
if resolved, ok := actionKindConstants[kind]; ok {
|
|
kind = resolved
|
|
}
|
|
if !seen[kind] {
|
|
seen[kind] = true
|
|
kinds = append(kinds, kind)
|
|
}
|
|
return true
|
|
})
|
|
return kinds, nil
|
|
}
|
|
|
|
// actionKindConstants maps each ActionKind Go identifier (as it appears in
|
|
// source) to its string constant value, so actionKindsAssignedInFunc can
|
|
// resolve the identifiers it finds via AST inspection back to real
|
|
// ActionKind values without re-parsing types.go's const block.
|
|
var actionKindConstants = map[ActionKind]ActionKind{
|
|
ActionKind("ActionEnablePlanMode"): ActionEnablePlanMode,
|
|
ActionKind("ActionAutoStartAgent"): ActionAutoStartAgent,
|
|
ActionKind("ActionResetAgentContext"): ActionResetAgentContext,
|
|
ActionKind("ActionSetSessionMode"): ActionSetSessionMode,
|
|
ActionKind("ActionRunCodeReview"): ActionRunCodeReview,
|
|
ActionKind("ActionClearDecisions"): ActionClearDecisions,
|
|
ActionKind("ActionQueueRunForEachParticipant"): ActionQueueRunForEachParticipant,
|
|
ActionKind("ActionQueueRun"): ActionQueueRun,
|
|
ActionKind("ActionEnsureParticipantSeat"): ActionEnsureParticipantSeat,
|
|
}
|
|
|
|
// fakeEntryDispatchStore is a minimal TransitionStore fake for
|
|
// DispatchStepEntry tests: only LoadStep is exercised.
|
|
type fakeEntryDispatchStore struct {
|
|
step StepSpec
|
|
err error
|
|
}
|
|
|
|
func (f *fakeEntryDispatchStore) LoadState(context.Context, string, string) (MachineState, error) {
|
|
return MachineState{}, nil
|
|
}
|
|
func (f *fakeEntryDispatchStore) LoadStep(context.Context, string, string) (StepSpec, error) {
|
|
return f.step, f.err
|
|
}
|
|
func (f *fakeEntryDispatchStore) LoadNextStep(context.Context, string, int) (StepSpec, error) {
|
|
return StepSpec{}, nil
|
|
}
|
|
func (f *fakeEntryDispatchStore) LoadPreviousStep(context.Context, string, int) (StepSpec, error) {
|
|
return StepSpec{}, nil
|
|
}
|
|
func (f *fakeEntryDispatchStore) ApplyTransition(context.Context, string, string, string, string, Trigger) error {
|
|
return nil
|
|
}
|
|
func (f *fakeEntryDispatchStore) ApplyTransitionIfAtStep(context.Context, string, string, string, string, Trigger) (bool, error) {
|
|
return false, nil
|
|
}
|
|
func (f *fakeEntryDispatchStore) PersistData(context.Context, string, map[string]any) error {
|
|
return nil
|
|
}
|
|
func (f *fakeEntryDispatchStore) IsOperationApplied(context.Context, string) (bool, error) {
|
|
return false, nil
|
|
}
|
|
func (f *fakeEntryDispatchStore) MarkOperationApplied(context.Context, string) error { return nil }
|
|
|
|
// entryRecordingCallback records every ActionInput it was invoked with and
|
|
// returns a canned error, letting tests assert both "did every declared
|
|
// action run" and "did a failure stop the rest".
|
|
type entryRecordingCallback struct {
|
|
err error
|
|
inputs *[]ActionInput
|
|
}
|
|
|
|
func (c entryRecordingCallback) Execute(_ context.Context, in ActionInput) (ActionResult, error) {
|
|
*c.inputs = append(*c.inputs, in)
|
|
return ActionResult{}, c.err
|
|
}
|
|
|
|
func TestDispatchStepEntry_ExcludesSessionShapedKinds(t *testing.T) {
|
|
var seen []ActionInput
|
|
registry := MapRegistry{
|
|
ActionEnablePlanMode: entryRecordingCallback{inputs: &seen},
|
|
ActionClearDecisions: entryRecordingCallback{inputs: &seen},
|
|
}
|
|
step := StepSpec{
|
|
ID: "step-1",
|
|
Events: map[Trigger][]Action{
|
|
TriggerOnEnter: {
|
|
{Kind: ActionEnablePlanMode},
|
|
{Kind: ActionClearDecisions},
|
|
},
|
|
},
|
|
}
|
|
e := New(&fakeEntryDispatchStore{step: step}, registry)
|
|
|
|
results := e.DispatchStepEntry(context.Background(), "task-1", "wf-1", "step-1", "entry-1")
|
|
|
|
require.Len(t, results, 1, "only the session-independent action should be attempted")
|
|
assert.Equal(t, ActionClearDecisions, results[0].Kind)
|
|
assert.NoError(t, results[0].Err)
|
|
require.Len(t, seen, 1, "only the session-independent callback should have run")
|
|
assert.Equal(t, ActionClearDecisions, seen[0].Action.Kind)
|
|
assert.Equal(t, "entry-1", seen[0].EntryID)
|
|
}
|
|
|
|
func TestDispatchStepEntry_ContinuesAfterOneActionFails(t *testing.T) {
|
|
var seen []ActionInput
|
|
failure := errors.New("boom")
|
|
registry := MapRegistry{
|
|
ActionClearDecisions: entryRecordingCallback{err: failure, inputs: &seen},
|
|
ActionQueueRunForEachParticipant: entryRecordingCallback{inputs: &seen},
|
|
}
|
|
step := StepSpec{
|
|
ID: "step-1",
|
|
Events: map[Trigger][]Action{
|
|
TriggerOnEnter: {
|
|
{Kind: ActionClearDecisions},
|
|
{Kind: ActionQueueRunForEachParticipant},
|
|
},
|
|
},
|
|
}
|
|
e := New(&fakeEntryDispatchStore{step: step}, registry)
|
|
|
|
results := e.DispatchStepEntry(context.Background(), "task-1", "wf-1", "step-1", "entry-1")
|
|
|
|
require.Len(t, results, 2, "both declared actions must be attempted despite the first failing")
|
|
assert.Equal(t, ActionClearDecisions, results[0].Kind)
|
|
assert.ErrorIs(t, results[0].Err, failure)
|
|
assert.Equal(t, ActionQueueRunForEachParticipant, results[1].Kind)
|
|
assert.NoError(t, results[1].Err)
|
|
assert.Len(t, seen, 2)
|
|
}
|
|
|
|
func TestDispatchStepEntry_NoDeclaredActionsIsNoop(t *testing.T) {
|
|
step := StepSpec{ID: "step-1"}
|
|
e := New(&fakeEntryDispatchStore{step: step}, MapRegistry{})
|
|
|
|
results := e.DispatchStepEntry(context.Background(), "task-1", "wf-1", "step-1", "entry-1")
|
|
|
|
assert.Empty(t, results)
|
|
}
|
|
|
|
// TestDispatchStepEntry_RepeatedParticipantSeatRoleDispatchesOnce covers
|
|
// AC-OFFICE-REVIEW-SEATS-001.12/004.7: a step that declares
|
|
// ensure_participant_seat twice for the same role writes at most one seat
|
|
// and emits at most one record for it — DispatchStepEntry must not invoke
|
|
// the callback a second time for a role it already dispatched in this call.
|
|
func TestDispatchStepEntry_RepeatedParticipantSeatRoleDispatchesOnce(t *testing.T) {
|
|
var seen []ActionInput
|
|
registry := MapRegistry{
|
|
ActionEnsureParticipantSeat: entryRecordingCallback{inputs: &seen},
|
|
}
|
|
step := StepSpec{
|
|
ID: "step-1",
|
|
Events: map[Trigger][]Action{
|
|
TriggerOnEnter: {
|
|
{Kind: ActionEnsureParticipantSeat, EnsureParticipantSeat: &EnsureParticipantSeatAction{Role: "reviewer"}},
|
|
{Kind: ActionEnsureParticipantSeat, EnsureParticipantSeat: &EnsureParticipantSeatAction{Role: "reviewer"}},
|
|
},
|
|
},
|
|
}
|
|
e := New(&fakeEntryDispatchStore{step: step}, registry)
|
|
|
|
results := e.DispatchStepEntry(context.Background(), "task-1", "wf-1", "step-1", "entry-1")
|
|
|
|
require.Len(t, results, 1, "the repeated declaration for the same role must not be dispatched a second time")
|
|
assert.Len(t, seen, 1)
|
|
}
|
|
|
|
// TestDispatchStepEntry_DistinctParticipantSeatRolesBothDispatch asserts the
|
|
// dedup in TestDispatchStepEntry_RepeatedParticipantSeatRoleDispatchesOnce
|
|
// is scoped to the repeated role only — two declarations for different
|
|
// roles are unrelated and both must run.
|
|
func TestDispatchStepEntry_DistinctParticipantSeatRolesBothDispatch(t *testing.T) {
|
|
var seen []ActionInput
|
|
registry := MapRegistry{
|
|
ActionEnsureParticipantSeat: entryRecordingCallback{inputs: &seen},
|
|
}
|
|
step := StepSpec{
|
|
ID: "step-1",
|
|
Events: map[Trigger][]Action{
|
|
TriggerOnEnter: {
|
|
{Kind: ActionEnsureParticipantSeat, EnsureParticipantSeat: &EnsureParticipantSeatAction{Role: "reviewer"}},
|
|
{Kind: ActionEnsureParticipantSeat, EnsureParticipantSeat: &EnsureParticipantSeatAction{Role: "approver"}},
|
|
},
|
|
},
|
|
}
|
|
e := New(&fakeEntryDispatchStore{step: step}, registry)
|
|
|
|
results := e.DispatchStepEntry(context.Background(), "task-1", "wf-1", "step-1", "entry-1")
|
|
|
|
require.Len(t, results, 2)
|
|
assert.Len(t, seen, 2)
|
|
}
|