Files
Agent-System/internal/app/intentflow/turn_test.go
T
weichenandCursor 2e99ee3661 feat(intentflow): route confirmed generate steps through a task adapter
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>
2026-09-16 02:24:23 +00:00

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)
}
}