Persist ComboNeedsConfirm plans at confirm tier without executing writes, then run task.run via OwnerMux and a stable idempotency key. Co-authored-by: Cursor <cursoragent@cursor.com>
158 lines
4.8 KiB
Go
158 lines
4.8 KiB
Go
package intentflow_test
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"chat-agent-system/internal/app/intentflow"
|
|
)
|
|
|
|
func TestHandleTurnAutoReadPersistsAndExecutesQueries(t *testing.T) {
|
|
intentflow.SetFeatureFlagsForTest(intentflow.FeatureFlags{PlanTier: intentflow.TierAutoRead})
|
|
t.Cleanup(intentflow.ResetFeatureFlagsForTest)
|
|
p, _, owner := testPlans()
|
|
ctx := context.Background()
|
|
res, err := p.HandleTurn(ctx, intentflow.TurnInput{
|
|
Principal: principal(), Text: "查一下项目风险,然后查相关法规",
|
|
ClientTurnID: "turn-ar-1", RequestDigest: "d-ar-1",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if res == nil || res.Plan == nil {
|
|
t.Fatal("auto_read must persist a plan")
|
|
}
|
|
if res.Plan.Status != intentflow.PlanExecuting && res.Plan.Status != intentflow.PlanCompleted {
|
|
t.Fatalf("status %s", res.Plan.Status)
|
|
}
|
|
again, err := p.HandleTurn(ctx, intentflow.TurnInput{
|
|
Principal: principal(), Text: "查一下项目风险,然后查相关法规",
|
|
ClientTurnID: "turn-ar-1", RequestDigest: "d-ar-1",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if again.Plan.PlanID != res.Plan.PlanID {
|
|
t.Fatal("replay must reuse plan")
|
|
}
|
|
total := 0
|
|
for _, s := range res.Plan.Steps {
|
|
total += owner.ExecCount(s.StepID)
|
|
}
|
|
_ = total
|
|
if owner.ExecCount("") < 0 {
|
|
t.Fatal("exec map")
|
|
}
|
|
n := 0
|
|
for _, s := range again.Plan.Steps {
|
|
// idempotency keys are not step IDs; count via succeeded status
|
|
if s.Status == intentflow.StepSucceeded {
|
|
n++
|
|
}
|
|
}
|
|
if n == 0 {
|
|
t.Fatalf("expected executed steps, plan=%+v", again.Plan)
|
|
}
|
|
}
|
|
|
|
func TestHandleTurnWriteUtteranceStaysLegacyUnderAutoRead(t *testing.T) {
|
|
intentflow.SetFeatureFlagsForTest(intentflow.FeatureFlags{PlanTier: intentflow.TierAutoRead})
|
|
t.Cleanup(intentflow.ResetFeatureFlagsForTest)
|
|
p, _, _ := testPlans()
|
|
res, err := p.HandleTurn(context.Background(), intentflow.TurnInput{
|
|
Principal: principal(), Text: "查法规并生成报价单",
|
|
ClientTurnID: "turn-w", RequestDigest: "d-w",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if res.Plan != nil {
|
|
t.Fatalf("writes must not persist at auto_read: %+v", res.Plan)
|
|
}
|
|
}
|
|
|
|
func TestQueryGuardRejectsGenerate(t *testing.T) {
|
|
g := intentflow.QueryGuard{Inner: intentflow.NewFakeActionOwner()}
|
|
_, err := g.Execute(context.Background(), intentflow.StepExec{ActionKey: "task.run", Principal: principal()})
|
|
if err == nil {
|
|
t.Fatal("task.run must be forbidden by QueryGuard")
|
|
}
|
|
}
|
|
|
|
func TestHandleTurnConfirmPersistsAwaitingWithoutExecute(t *testing.T) {
|
|
intentflow.SetFeatureFlagsForTest(intentflow.FeatureFlags{PlanTier: intentflow.TierConfirm})
|
|
t.Cleanup(intentflow.ResetFeatureFlagsForTest)
|
|
p, _, owner := testPlans()
|
|
ctx := context.Background()
|
|
res, err := p.HandleTurn(ctx, intentflow.TurnInput{
|
|
Principal: principal(),
|
|
Text: "查一下相关法规,然后帮我生成报价单",
|
|
ClientTurnID: "turn-cf-1",
|
|
RequestDigest: "d-cf-1",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if res == nil || res.Plan == nil {
|
|
t.Fatal("confirm tier must persist a write plan")
|
|
}
|
|
if res.Plan.Status != intentflow.PlanAwaitingConfirmation {
|
|
t.Fatalf("status %s", res.Plan.Status)
|
|
}
|
|
if len(res.Plan.Steps) < 2 {
|
|
t.Fatalf("expected query+generate steps, got %+v", res.Plan.Steps)
|
|
}
|
|
hasGenerate := false
|
|
for _, s := range res.Plan.Steps {
|
|
if s.ActionKey == "task.run" || s.ActionKey == "task.run.adhoc" {
|
|
hasGenerate = true
|
|
}
|
|
if s.Status != intentflow.StepPending {
|
|
t.Fatalf("confirm must not execute before decision, step=%+v", s)
|
|
}
|
|
}
|
|
if !hasGenerate {
|
|
t.Fatalf("expected a generate step, plan=%+v", res.Plan)
|
|
}
|
|
if owner.ExecCount("") != 0 {
|
|
t.Fatal("no owner exec expected before confirm")
|
|
}
|
|
again, err := p.HandleTurn(ctx, intentflow.TurnInput{
|
|
Principal: principal(),
|
|
Text: "查一下相关法规,然后帮我生成报价单",
|
|
ClientTurnID: "turn-cf-1",
|
|
RequestDigest: "d-cf-1",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if again.Plan.PlanID != res.Plan.PlanID {
|
|
t.Fatal("same turn must reuse the awaiting plan")
|
|
}
|
|
}
|
|
|
|
func TestHandleTurnConfirmRejectsSecondActiveWritePlan(t *testing.T) {
|
|
intentflow.SetFeatureFlagsForTest(intentflow.FeatureFlags{PlanTier: intentflow.TierConfirm})
|
|
t.Cleanup(intentflow.ResetFeatureFlagsForTest)
|
|
p, _, _ := testPlans()
|
|
ctx := context.Background()
|
|
first, err := p.HandleTurn(ctx, intentflow.TurnInput{
|
|
Principal: principal(),
|
|
Text: "查一下相关法规,然后帮我生成报价单",
|
|
ClientTurnID: "turn-cf-a",
|
|
RequestDigest: "d-cf-a",
|
|
})
|
|
if err != nil || first == nil || first.Plan == nil {
|
|
t.Fatalf("first plan: %+v %v", first, err)
|
|
}
|
|
_, err = p.HandleTurn(ctx, intentflow.TurnInput{
|
|
Principal: principal(),
|
|
Text: "查一下相关法规,然后帮我生成报价单",
|
|
ClientTurnID: "turn-cf-b",
|
|
RequestDigest: "d-cf-b",
|
|
})
|
|
if err != intentflow.ErrPlanDecisionConflict {
|
|
t.Fatalf("second write plan must conflict, got %v", err)
|
|
}
|
|
}
|