package intentflow_test import ( "context" "errors" "testing" "chat-agent-system/internal/app/intentflow" ) func TestConfirmDeniedDoesNotMutatePlan(t *testing.T) { p, _, owner := testPlans() ctx := context.Background() created, err := p.CreatePlan(ctx, draft("digest-a")) if err != nil { t.Fatal(err) } owner.Denied = true _, err = p.DecidePlan(ctx, intentflow.DecisionCommand{ Principal: principal(), PlanID: created.PlanID, ExpectedRevision: created.Revision, DecisionNonce: "n-deny", DecisionDigest: "d-deny", Kind: intentflow.DecisionConfirm, }) if !errors.Is(err, intentflow.ErrPlanActionForbidden) { t.Fatalf("want forbidden, got %v", err) } got, err := p.GetPlan(ctx, principal(), created.PlanID) if err != nil { t.Fatal(err) } if got.Revision != created.Revision || got.Status != intentflow.PlanAwaitingConfirmation { t.Fatalf("confirm must be side-effect free on deny: %+v", got) } } func TestResourceVersionConflictOnConfirm(t *testing.T) { p, _, owner := testPlans() ctx := context.Background() cmd := draft("digest-ver") cmd.Steps[1].ExpectedResourceVersion = "v1" created, err := p.CreatePlan(ctx, cmd) if err != nil { t.Fatal(err) } owner.RequiredVersion = map[string]string{"task.run": "v2"} _, 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, got %v", err) } } func TestRevokeBetweenConfirmAndExecute(t *testing.T) { p, _, owner := testPlans() ctx := context.Background() created, err := p.CreatePlan(ctx, draft("digest-rev")) if err != nil { t.Fatal(err) } confirmed, err := p.DecidePlan(ctx, intentflow.DecisionCommand{ Principal: principal(), PlanID: created.PlanID, ExpectedRevision: created.Revision, DecisionNonce: "n-ok", DecisionDigest: "d-ok", Kind: intentflow.DecisionConfirm, }) if err != nil { t.Fatal(err) } owner.Denied = true stepID := confirmed.Steps[0].StepID claimed, err := p.ClaimStep(ctx, "w1", confirmed.PlanID, stepID) if err != nil { t.Fatal(err) } if _, err := p.ExecuteClaimed(ctx, claimed); !errors.Is(err, intentflow.ErrPlanActionForbidden) { t.Fatalf("revoked execute must fail closed, got %v", err) } } func TestEditBumpsRevisionAndInvalidatesOldCard(t *testing.T) { p, _, _ := testPlans() ctx := context.Background() created, err := p.CreatePlan(ctx, draft("digest-edit")) if err != nil { t.Fatal(err) } edited, err := p.DecidePlan(ctx, intentflow.DecisionCommand{ Principal: principal(), PlanID: created.PlanID, ExpectedRevision: created.Revision, DecisionNonce: "n-edit", DecisionDigest: "d-edit", Kind: intentflow.DecisionEdit, }) if err != nil { t.Fatal(err) } if edited.Revision <= created.Revision { t.Fatalf("edit must bump revision, got %d", edited.Revision) } if edited.Status != intentflow.PlanAwaitingConfirmation { t.Fatalf("edit stays awaiting, got %s", edited.Status) } _, err = p.DecidePlan(ctx, intentflow.DecisionCommand{ Principal: principal(), PlanID: created.PlanID, ExpectedRevision: created.Revision, DecisionNonce: "n-old", DecisionDigest: "d-old", Kind: intentflow.DecisionConfirm, }) if !errors.Is(err, intentflow.ErrPlanStaleRevision) { t.Fatalf("old card must be stale, got %v", err) } } func TestIllegalPartialConfirmMissingDependency(t *testing.T) { p, _, _ := testPlans() ctx := context.Background() cmd := draft("digest-dep") cmd.Steps[1].DependsOn = []string{"stp_missing"} created, err := p.CreatePlan(ctx, cmd) if err != nil { t.Fatal(err) } writeID := created.Steps[1].StepID _, err = p.DecidePlan(ctx, intentflow.DecisionCommand{ Principal: principal(), PlanID: created.PlanID, ExpectedRevision: created.Revision, DecisionNonce: "n-part", DecisionDigest: "d-part", Kind: intentflow.DecisionConfirm, StepIDs: []string{writeID}, }) if !errors.Is(err, intentflow.ErrPlanDependencyInvalid) { t.Fatalf("partial without closure must be invalid, got %v", err) } }