Generate 2–4 step L1 proposals with stable digests while leaving TopIntent and the legacy IntentPlan router unchanged. Co-authored-by: Cursor <cursoragent@cursor.com>
242 lines
8.0 KiB
Go
242 lines
8.0 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|