package intentflow_test import ( "context" "errors" "testing" "time" "chat-agent-system/internal/app/intentflow" ) func TestDispatchEligible_RecoverReceiptAfterCrash(t *testing.T) { now := time.Date(2026, 9, 22, 12, 0, 0, 0, time.UTC) owner := intentflow.NewFakeActionOwner() p := intentflow.NewPlans(&intentflow.SerialUoW{}, intentflow.NewMemoryStore(), intentflow.Options{ Owner: owner, Now: func() time.Time { return now }, Lease: 10 * time.Second, }) ctx := context.Background() created, err := p.CreatePlan(ctx, intentflow.CreateCommand{ Principal: principal(), ClientTurnID: "t-crash", RequestDigest: "d-crash", Status: intentflow.PlanAwaitingConfirmation, ExecutionMode: intentflow.ModeLive, Steps: []intentflow.StepDraft{{ActionKey: "regulation.query", ActionDigest: "q"}}, }) if err != nil { t.Fatal(err) } confirmed, err := p.DecidePlan(ctx, intentflow.DecisionCommand{ Principal: principal(), PlanID: created.PlanID, ExpectedRevision: created.Revision, DecisionNonce: "n-crash", DecisionDigest: "d-crash", Kind: intentflow.DecisionConfirm, }) if err != nil { t.Fatal(err) } stepID := confirmed.Steps[0].StepID claimed, err := p.ClaimStep(ctx, "w-crash", confirmed.PlanID, stepID) if err != nil || claimed == nil { t.Fatalf("claim: %v %+v", err, claimed) } // Domain receipt persisted; plan write-back has not happened yet (crash window). if _, err := owner.Execute(ctx, intentflow.StepExec{ Principal: claimed.Plan.Principal, PlanID: claimed.Plan.PlanID, StepID: claimed.Step.StepID, ActionKey: claimed.Step.ActionKey, IdempotencyKey: claimed.IdempotencyKey, ActionDigest: claimed.Step.ActionDigest, FencingToken: claimed.FencingToken, }); err != nil { t.Fatal(err) } if owner.ExecCount(claimed.IdempotencyKey) != 1 { t.Fatalf("domain exec count = %d", owner.ExecCount(claimed.IdempotencyKey)) } // Lease expires; re-claim then ExecuteClaimed fails re-auth → RecoverReceipt. now = now.Add(30 * time.Second) owner.Denied = true view, err := p.DispatchEligible(ctx, "w-crash-2", confirmed.PlanID) if err != nil { t.Fatalf("recover path: %v", err) } if view == nil || len(view.Steps) == 0 || view.Steps[0].Status != intentflow.StepSucceeded { t.Fatalf("recovered view: %+v", view) } if owner.ExecCount(claimed.IdempotencyKey) != 1 { t.Fatalf("recover must not re-execute, count=%d", owner.ExecCount(claimed.IdempotencyKey)) } } func TestDispatchEligible_ReauthsOnExecute(t *testing.T) { owner := intentflow.NewFakeActionOwner() p := intentflow.NewPlans(&intentflow.SerialUoW{}, intentflow.NewMemoryStore(), intentflow.Options{ Owner: owner, }) ctx := context.Background() created, err := p.CreatePlan(ctx, intentflow.CreateCommand{ Principal: principal(), ClientTurnID: "t-auth", RequestDigest: "d-auth", Status: intentflow.PlanAwaitingConfirmation, ExecutionMode: intentflow.ModeLive, Steps: []intentflow.StepDraft{{ActionKey: "regulation.query", ActionDigest: "q"}}, }) if err != nil { t.Fatal(err) } confirmed, err := p.DecidePlan(ctx, intentflow.DecisionCommand{ Principal: principal(), PlanID: created.PlanID, ExpectedRevision: created.Revision, DecisionNonce: "n-auth", DecisionDigest: "d-auth", Kind: intentflow.DecisionConfirm, }) if err != nil { t.Fatal(err) } owner.Denied = true _, err = p.DispatchEligible(ctx, "w-auth", confirmed.PlanID) if !errors.Is(err, intentflow.ErrPlanActionForbidden) { t.Fatalf("revoked dispatch must fail closed, got %v", err) } } func TestDispatchEligible_VersionConflict(t *testing.T) { owner := intentflow.NewFakeActionOwner() owner.RequiredVersion = map[string]string{"regulation.query": "v2"} p := intentflow.NewPlans(&intentflow.SerialUoW{}, intentflow.NewMemoryStore(), intentflow.Options{ Owner: owner, }) ctx := context.Background() created, err := p.CreatePlan(ctx, intentflow.CreateCommand{ Principal: principal(), ClientTurnID: "t-ver", RequestDigest: "d-ver", Status: intentflow.PlanAwaitingConfirmation, ExecutionMode: intentflow.ModeLive, Steps: []intentflow.StepDraft{{ ActionKey: "regulation.query", ActionDigest: "q", ExpectedResourceVersion: "v1", }}, }) if err != nil { t.Fatal(err) } // Confirm itself re-auths; version conflict must block before execute. _, err = p.DecidePlan(ctx, intentflow.DecisionCommand{ Principal: principal(), PlanID: created.PlanID, ExpectedRevision: created.Revision, DecisionNonce: "n-ver", DecisionDigest: "d-ver", Kind: intentflow.DecisionConfirm, }) if !errors.Is(err, intentflow.ErrPlanActionVersionConflict) { t.Fatalf("want version conflict on confirm, got %v", err) } }