package intent_test import ( "context" "encoding/json" "os" "path/filepath" "runtime" "testing" "chat-agent-system/internal/intent" ) func TestSplitPlanSegments_ConnectorAndNumbered(t *testing.T) { got := intent.SplitPlanSegments("查一下项目风险,然后查相关法规") if len(got) != 2 { t.Fatalf("connector split got %d %#v", len(got), got) } numbered := intent.SplitPlanSegments("1. 查一下相关法规\n2. 查相似案例\n3. 查一下我的权限") if len(numbered) != 3 { t.Fatalf("numbered split got %d %#v", len(numbered), numbered) } } func TestSplitPlanSegments_PeriodAndSemicolon(t *testing.T) { period := intent.SplitPlanSegments("查一下相关法规。查相似案例。") if len(period) != 2 { t.Fatalf("period split got %d %#v", len(period), period) } semi := intent.SplitPlanSegments("查一下相关法规;查相似案例") if len(semi) != 2 { t.Fatalf("semicolon split got %d %#v", len(semi), semi) } } func TestSplitPlanSegments_InternalChainNotSplit(t *testing.T) { got := intent.SplitPlanSegments("请查询相关法规并总结要点") if len(got) != 1 { t.Fatalf("internal chain must stay 1, got %d %#v", len(got), got) } } func TestSplitPlanSegments_FirstThenFinally(t *testing.T) { got := intent.SplitPlanSegments("先查一下相关法规再查相似案例最后查一下我的权限") if len(got) != 3 { t.Fatalf("first/then/finally got %d %#v", len(got), got) } } func TestSplitPlanSegments_FiveNumberedKeptIntact(t *testing.T) { got := intent.SplitPlanSegments("1. 查法规\n2. 查案例\n3. 查进度\n4. 查权限\n5. 查文档状态") if len(got) != 5 { t.Fatalf("must not truncate 5 segments, got %d", len(got)) } } func TestPlanProposal_WriteNotDroppedAndLegacyUnchanged(t *testing.T) { rec := intent.NewRecognizer() res, err := rec.Recognize(context.Background(), "查一下相关法规,然后帮我生成报价单", nil) if err != nil { t.Fatal(err) } if res.IntentPlan != nil { t.Fatal("production IntentPlan must stay nil") } if res.TopIntent == nil || res.TopIntent.Intent != intent.IntentQueryRegulations { t.Fatalf("TopIntent changed: %+v", res.TopIntent) } if res.PlanProposal == nil || len(res.PlanProposal.Segments) < 2 { t.Fatalf("proposal must keep both segments: %+v", res.PlanProposal) } if res.PlanProposal.ComboHint != intent.ComboNeedsConfirm { t.Fatalf("combo hint = %s", res.PlanProposal.ComboHint) } if res.PlanShadow == nil || !res.PlanShadow.LegacyDroppedWrite { t.Fatalf("shadow must record legacy write drop: %+v", res.PlanShadow) } if res.PlanShadow.CounterfactualAutoRead != "would_confirm" { t.Fatalf("auto_read counterfactual = %s", res.PlanShadow.CounterfactualAutoRead) } } func TestPlanProposal_QueryPairAutoReadCandidate(t *testing.T) { rec := intent.NewRecognizer() res, err := rec.Recognize(context.Background(), "查一下项目风险,然后查相关法规", nil) if err != nil { t.Fatal(err) } if res.IntentPlan == nil { t.Fatal("legacy plan still required") } if res.PlanProposal == nil || res.PlanProposal.ComboHint != intent.ComboReadOnly { t.Fatalf("proposal %+v", res.PlanProposal) } if res.PlanShadow.CounterfactualAutoRead != "would_execute" { t.Fatalf("counterfactual = %s", res.PlanShadow.CounterfactualAutoRead) } } func TestPlanProposal_AnaphoraClarify(t *testing.T) { rec := intent.NewRecognizer() res, err := rec.Recognize(context.Background(), "查一下相关法规,然后再看一下它的适用范围", nil) if err != nil { t.Fatal(err) } if res.PlanProposal == nil { t.Fatal("missing proposal") } if res.PlanProposal.ComboHint != intent.ComboNeedsClarify { t.Fatalf("anaphora must clarify, got %s segs=%+v", res.PlanProposal.ComboHint, res.PlanProposal.Segments) } if res.PlanShadow.CounterfactualAutoRead != "would_clarify" { t.Fatalf("counterfactual = %s", res.PlanShadow.CounterfactualAutoRead) } } func TestPlanProposal_SecurityRejectsWhole(t *testing.T) { rec := intent.NewRecognizer() res, err := rec.Recognize(context.Background(), "查一下相关法规,然后修改我的工号", nil) if err != nil { t.Fatal(err) } if res.TopIntent == nil || res.TopIntent.Intent != intent.IntentSecurityForbidden { t.Fatalf("security TopIntent got %+v", res.TopIntent) } if res.PlanProposal == nil || res.PlanProposal.ComboHint != intent.ComboRejected { t.Fatalf("proposal %+v", res.PlanProposal) } if res.PlanShadow.CounterfactualAutoRead != "would_reject" { t.Fatalf("counterfactual = %s", res.PlanShadow.CounterfactualAutoRead) } } func TestPlanProposal_BatchSizeRejected(t *testing.T) { rec := intent.NewRecognizer() res, err := rec.Recognize(context.Background(), "1. 查法规\n2. 查案例\n3. 查进度\n4. 查权限\n5. 查文档状态", nil) if err != nil { t.Fatal(err) } if res.PlanProposal == nil || res.PlanProposal.ComboHint != intent.ComboRejectedBatchSize { t.Fatalf("proposal %+v", res.PlanProposal) } if len(res.PlanProposal.Segments) != 5 { t.Fatalf("must keep 5 spans, got %d", len(res.PlanProposal.Segments)) } } func TestPlanProposal_DigestStable(t *testing.T) { rec := intent.NewRecognizer() input := "查一下项目风险,然后查相关法规" a, err := rec.Recognize(context.Background(), input, nil) if err != nil { t.Fatal(err) } b, err := rec.Recognize(context.Background(), input, nil) if err != nil { t.Fatal(err) } if a.PlanProposal == nil || b.PlanProposal == nil || a.PlanProposal.Digest == "" { t.Fatal("digest missing") } if a.PlanProposal.Digest != b.PlanProposal.Digest { t.Fatalf("digest drifted %s vs %s", a.PlanProposal.Digest, b.PlanProposal.Digest) } } func TestPlanProposalEvalGolden(t *testing.T) { _, file, _, ok := runtime.Caller(0) if !ok { t.Fatal("caller") } raw, err := os.ReadFile(filepath.Join(filepath.Dir(file), "testdata", "plan-proposal-eval-v1.json")) if err != nil { t.Fatal(err) } var gf struct { Cases []struct { ID string `json:"id"` Input string `json:"input"` ExpectCombo string `json:"expect_combo"` ExpectCounterfactual string `json:"expect_counterfactual"` LegacyPlanSteps int `json:"legacy_plan_steps"` HardNegative bool `json:"hard_negative"` } `json:"cases"` } if err := json.Unmarshal(raw, &gf); err != nil { t.Fatal(err) } rec := intent.NewRecognizer() falseAuto := 0 denom := len(gf.Cases) for _, c := range gf.Cases { res, err := rec.Recognize(context.Background(), c.Input, nil) if err != nil { t.Fatalf("%s: %v", c.ID, err) } if res.PlanProposal == nil || res.PlanShadow == nil { t.Fatalf("%s missing proposal/shadow", c.ID) } if string(res.PlanProposal.ComboHint) != c.ExpectCombo { t.Errorf("%s combo=%s want %s", c.ID, res.PlanProposal.ComboHint, c.ExpectCombo) } if res.PlanShadow.CounterfactualAutoRead != c.ExpectCounterfactual { t.Errorf("%s counterfactual=%s want %s", c.ID, res.PlanShadow.CounterfactualAutoRead, c.ExpectCounterfactual) } steps := 0 if res.IntentPlan != nil { steps = len(res.IntentPlan.Steps) } if c.LegacyPlanSteps > 0 && steps != c.LegacyPlanSteps { t.Errorf("%s legacy steps=%d want %d", c.ID, steps, c.LegacyPlanSteps) } if c.HardNegative && res.PlanShadow.CounterfactualAutoRead == "would_execute" { falseAuto++ t.Errorf("%s hard-negative would auto-execute", c.ID) } } if falseAuto != 0 { t.Fatalf("false auto-execute %d / denom %d", falseAuto, denom) } t.Logf("plan-proposal eval denom=%d false_auto_execute=0", denom) } func TestPlanProposal_HardNegativeNoFalseAutoExecute(t *testing.T) { rec := intent.NewRecognizer() cases := []string{ "查一下相关法规,然后帮我生成报价单", "查一下相关法规,然后修改我的工号", "1. 查法规\n2. 查案例\n3. 查进度\n4. 查权限\n5. 查文档状态", "查一下相关法规,然后再看一下它的适用范围", } for _, in := range cases { res, err := rec.Recognize(context.Background(), in, nil) if err != nil { t.Fatal(err) } if res.PlanShadow != nil && res.PlanShadow.CounterfactualAutoRead == "would_execute" { t.Fatalf("hard-negative %q would auto-execute", in) } } }