Confirmation stays a command receipt; FakeActionOwner now fail-closes on revoke and resource version drift. Co-authored-by: Cursor <cursoragent@cursor.com>
127 lines
4.1 KiB
Go
127 lines
4.1 KiB
Go
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)
|
|
}
|
|
}
|