生成项目建议书 now resolves to rd_proposal. Longer titles win, so 系统设计方案 is not mistaken for the shorter business-stage phrase, and an explicit generate command is no longer swallowed by a progress question. Co-authored-by: Cursor <cursoragent@cursor.com>
776 lines
28 KiB
Go
776 lines
28 KiB
Go
package intent_test
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"chat-agent-system/internal/intent"
|
|
"chat-agent-system/internal/maturity"
|
|
)
|
|
|
|
func TestIntentRecognition_QueryProgress(t *testing.T) {
|
|
rec := intent.NewRecognizer()
|
|
ctx := &intent.ContextSnapshot{
|
|
SnapshotID: "ctx_test_1",
|
|
CurrentStage: string(maturity.StageV01_Triage),
|
|
}
|
|
|
|
res, err := rec.Recognize(context.Background(), "现在这个便携超声立项评估到哪一步了?有啥风险没?", ctx)
|
|
if err != nil {
|
|
t.Fatalf("recognize failed: %v", err)
|
|
}
|
|
|
|
if res.TopIntent.Intent != intent.IntentQueryProgress {
|
|
t.Errorf("expected intent QUERY_PROGRESS, got %s", res.TopIntent.Intent)
|
|
}
|
|
if res.TopIntent.Confidence < 0.8 {
|
|
t.Errorf("expected confidence >= 0.8, got %v", res.TopIntent.Confidence)
|
|
}
|
|
if len(res.Candidates) == 0 {
|
|
t.Errorf("expected candidate intents list")
|
|
}
|
|
}
|
|
|
|
func TestIntentRecognition_SupplementInfo(t *testing.T) {
|
|
rec := intent.NewRecognizer()
|
|
ctx := &intent.ContextSnapshot{
|
|
SnapshotID: "ctx_test_2",
|
|
}
|
|
|
|
res, err := rec.Recognize(context.Background(), "客户刚才补充说,目标市场除了国内还想考虑欧洲 CE 认证", ctx)
|
|
if err != nil {
|
|
t.Fatalf("recognize failed: %v", err)
|
|
}
|
|
|
|
if res.TopIntent.Intent != intent.IntentSupplementInfo {
|
|
t.Errorf("expected intent SUPPLEMENT_INFO, got %s", res.TopIntent.Intent)
|
|
}
|
|
if res.TopIntent.FilledSlots[intent.SlotFactKey] != "target_market" {
|
|
t.Errorf("expected slot target_market, got %s", res.TopIntent.FilledSlots[intent.SlotFactKey])
|
|
}
|
|
}
|
|
|
|
func TestIntentRecognition_AdvanceStage(t *testing.T) {
|
|
rec := intent.NewRecognizer()
|
|
ctx := &intent.ContextSnapshot{
|
|
SnapshotID: "ctx_test_3",
|
|
CurrentStage: string(maturity.StageV01_Triage),
|
|
}
|
|
|
|
res, err := rec.Recognize(context.Background(), "摸底表我看过了,没问题,可以进入方案论证阶段了", ctx)
|
|
if err != nil {
|
|
t.Fatalf("recognize failed: %v", err)
|
|
}
|
|
|
|
if res.TopIntent.Intent != intent.IntentAdvanceStage {
|
|
t.Errorf("expected intent ADVANCE_STAGE, got %s", res.TopIntent.Intent)
|
|
}
|
|
if res.TopIntent.FilledSlots[intent.SlotTargetStage] != string(maturity.StageV05_Feasibility) {
|
|
t.Errorf("expected target stage V0.5_feasibility, got %s", res.TopIntent.FilledSlots[intent.SlotTargetStage])
|
|
}
|
|
}
|
|
|
|
func TestIntentRecognition_TriggerTask(t *testing.T) {
|
|
rec := intent.NewRecognizer()
|
|
ctx := &intent.ContextSnapshot{
|
|
SnapshotID: "ctx_test_4",
|
|
}
|
|
|
|
res, err := rec.Recognize(context.Background(), "先帮我出一版初步报价单看看", ctx)
|
|
if err != nil {
|
|
t.Fatalf("recognize failed: %v", err)
|
|
}
|
|
|
|
if res.TopIntent.Intent != intent.IntentTriggerTask {
|
|
t.Errorf("expected intent TRIGGER_TASK, got %s", res.TopIntent.Intent)
|
|
}
|
|
if res.TopIntent.FilledSlots[intent.SlotTargetTask] != "cdo_quote" {
|
|
t.Errorf("expected target task cdo_quote, got %s", res.TopIntent.FilledSlots[intent.SlotTargetTask])
|
|
}
|
|
}
|
|
|
|
func TestIntentRecognition_RDevProposalAndLongerTitleWins(t *testing.T) {
|
|
rec := intent.NewRecognizer()
|
|
ctx := &intent.ContextSnapshot{SnapshotID: "ctx_rdev_trigger"}
|
|
|
|
proposal, err := rec.Recognize(context.Background(), "请生成项目建议书", ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if proposal.TopIntent.FilledSlots[intent.SlotTargetTask] != "rd_proposal" {
|
|
t.Fatalf("项目建议书 want rd_proposal got %s", proposal.TopIntent.FilledSlots[intent.SlotTargetTask])
|
|
}
|
|
|
|
design, err := rec.Recognize(context.Background(), "生成设计方案", ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if design.TopIntent.FilledSlots[intent.SlotTargetTask] != "prelim_design" {
|
|
t.Fatalf("设计方案 must stay prelim_design, got %s", design.TopIntent.FilledSlots[intent.SlotTargetTask])
|
|
}
|
|
|
|
system, err := rec.Recognize(context.Background(), "生成系统设计方案", ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if system.TopIntent.FilledSlots[intent.SlotTargetTask] != "rd_system_design" {
|
|
t.Fatalf("系统设计方案 want rd_system_design got %s", system.TopIntent.FilledSlots[intent.SlotTargetTask])
|
|
}
|
|
|
|
intake, err := rec.Recognize(context.Background(), "生成用户需求收集表", ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if intake.TopIntent.FilledSlots[intent.SlotTargetTask] != "intake" {
|
|
t.Fatalf("用户需求收集表 must stay intake, got %s", intake.TopIntent.FilledSlots[intent.SlotTargetTask])
|
|
}
|
|
}
|
|
|
|
func TestIntentRecognition_CasualChat(t *testing.T) {
|
|
rec := intent.NewRecognizer()
|
|
res, err := rec.Recognize(context.Background(), "你好,请问你是谁?", nil)
|
|
if err != nil {
|
|
t.Fatalf("recognize failed: %v", err)
|
|
}
|
|
|
|
if res.TopIntent.Intent != intent.IntentCasualChat {
|
|
t.Errorf("expected intent CASUAL_CHAT, got %s", res.TopIntent.Intent)
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------
|
|
// 三分支柔性消解核心测试集
|
|
// ----------------------------------------------------------------------------------
|
|
|
|
func samplePendingExpectation() *intent.PendingExpectation {
|
|
optAdvance := intent.ExpectationOption{
|
|
OptionID: "1",
|
|
Title: "推进到 V0.5 方案成型与法规对齐",
|
|
Description: "小队将自动开展技术路线比选与分类判定",
|
|
Intent: intent.IntentAdvanceStage,
|
|
Slots: map[string]string{
|
|
intent.SlotTargetStage: string(maturity.StageV05_Feasibility),
|
|
},
|
|
}
|
|
optRefresh := intent.ExpectationOption{
|
|
OptionID: "2",
|
|
Title: "局部刷新 V0.1 用户需求表",
|
|
Description: "根据新补充材料重新生成初筛画像",
|
|
Intent: intent.IntentTriggerTask,
|
|
Slots: map[string]string{
|
|
intent.SlotTargetTask: "intake",
|
|
},
|
|
}
|
|
|
|
return &intent.PendingExpectation{
|
|
ContextSnippet: "当前 V0.1 闪电摸底已就绪,建议推进到 V0.5",
|
|
DefaultOption: &optAdvance,
|
|
Options: []intent.ExpectationOption{optAdvance, optRefresh},
|
|
AllowAffirmative: true,
|
|
CreatedAt: time.Now(),
|
|
}
|
|
}
|
|
|
|
// 用例 1(分支 1 极速兑现):"好", "行", "选1"
|
|
func TestThreeBranch_Branch1_DirectAffirmative(t *testing.T) {
|
|
rec := intent.NewRecognizer()
|
|
ctx := &intent.ContextSnapshot{
|
|
SnapshotID: "ctx_pending_1",
|
|
CurrentStage: string(maturity.StageV01_Triage),
|
|
PendingExpectation: samplePendingExpectation(),
|
|
}
|
|
|
|
testCases := []string{"好", "行", "可以", "没问题", "同意", "好的", "就按这个办", "选1", "1"}
|
|
for _, tc := range testCases {
|
|
res, err := rec.Recognize(context.Background(), tc, ctx)
|
|
if err != nil {
|
|
t.Fatalf("recognize '%s' failed: %v", tc, err)
|
|
}
|
|
if res.Branch != intent.BranchExpectationHit {
|
|
t.Errorf("input '%s': expected branch EXPECTATION_HIT, got %s", tc, res.Branch)
|
|
}
|
|
if res.TopIntent.Intent != intent.IntentAdvanceStage {
|
|
t.Errorf("input '%s': expected intent ADVANCE_STAGE, got %s", tc, res.TopIntent.Intent)
|
|
}
|
|
if res.TopIntent.FilledSlots[intent.SlotTargetStage] != string(maturity.StageV05_Feasibility) {
|
|
t.Errorf("input '%s': expected target stage V0.5, got %s", tc, res.TopIntent.FilledSlots[intent.SlotTargetStage])
|
|
}
|
|
if res.TopIntent.Confidence < 0.95 {
|
|
t.Errorf("input '%s': expected confidence >= 0.95, got %v", tc, res.TopIntent.Confidence)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 用例 2(分支 1 复合打补丁):"好,但目标市场改成欧洲CE"
|
|
func TestThreeBranch_Branch1_AffirmativeWithPatch(t *testing.T) {
|
|
rec := intent.NewRecognizer()
|
|
ctx := &intent.ContextSnapshot{
|
|
SnapshotID: "ctx_pending_2",
|
|
CurrentStage: string(maturity.StageV01_Triage),
|
|
PendingExpectation: samplePendingExpectation(),
|
|
}
|
|
|
|
res, err := rec.Recognize(context.Background(), "好,但目标市场改成欧洲CE", ctx)
|
|
if err != nil {
|
|
t.Fatalf("recognize failed: %v", err)
|
|
}
|
|
|
|
if res.Branch != intent.BranchExpectationHit {
|
|
t.Fatalf("expected branch EXPECTATION_HIT, got %s", res.Branch)
|
|
}
|
|
if res.TopIntent.Intent != intent.IntentAdvanceStage {
|
|
t.Errorf("expected intent ADVANCE_STAGE, got %s", res.TopIntent.Intent)
|
|
}
|
|
if res.TopIntent.FilledSlots[intent.SlotTargetStage] != string(maturity.StageV05_Feasibility) {
|
|
t.Errorf("expected target stage V0.5, got %s", res.TopIntent.FilledSlots[intent.SlotTargetStage])
|
|
}
|
|
if res.TopIntent.FilledSlots["target_market"] != "欧洲CE" {
|
|
t.Errorf("expected patched target_market '欧洲CE', got '%s'", res.TopIntent.FilledSlots["target_market"])
|
|
}
|
|
if res.TopIntent.PatchSlots["target_market"] != "欧洲CE" {
|
|
t.Errorf("expected PatchSlots to have target_market '欧洲CE', got '%s'", res.TopIntent.PatchSlots["target_market"])
|
|
}
|
|
}
|
|
|
|
// 用例 3(分支 1 否定撤销):"先别推,等等"
|
|
func TestThreeBranch_Branch1_Decline(t *testing.T) {
|
|
rec := intent.NewRecognizer()
|
|
ctx := &intent.ContextSnapshot{
|
|
SnapshotID: "ctx_pending_3",
|
|
CurrentStage: string(maturity.StageV01_Triage),
|
|
PendingExpectation: samplePendingExpectation(),
|
|
}
|
|
|
|
testCases := []string{"先别推,等等", "算了吧", "先别", "先等等"}
|
|
for _, tc := range testCases {
|
|
res, err := rec.Recognize(context.Background(), tc, ctx)
|
|
if err != nil {
|
|
t.Fatalf("recognize '%s' failed: %v", tc, err)
|
|
}
|
|
if res.Branch != intent.BranchExpectationHit {
|
|
t.Errorf("input '%s': expected branch EXPECTATION_HIT, got %s", tc, res.Branch)
|
|
}
|
|
if res.TopIntent.Intent != intent.IntentDeclineExpectation {
|
|
t.Errorf("input '%s': expected intent DECLINE_EXPECTATION, got %s", tc, res.TopIntent.Intent)
|
|
}
|
|
if !res.TopIntent.IsDecline {
|
|
t.Errorf("input '%s': expected IsDecline to be true", tc)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 用例 4(分支 2 话题突发抢占):有挂起预期时,用户突然问新问题或补新事实
|
|
func TestThreeBranch_Branch2_NewIntentPreempt(t *testing.T) {
|
|
rec := intent.NewRecognizer()
|
|
ctx := &intent.ContextSnapshot{
|
|
SnapshotID: "ctx_pending_4",
|
|
CurrentStage: string(maturity.StageV01_Triage),
|
|
PendingExpectation: samplePendingExpectation(),
|
|
}
|
|
|
|
// 抢占场景 A: 突然提问技术问题
|
|
res1, err := rec.Recognize(context.Background(), "查一下探头功耗要怎么设计", ctx)
|
|
if err != nil {
|
|
t.Fatalf("recognize failed: %v", err)
|
|
}
|
|
if res1.Branch != intent.BranchNewIntentPreempt {
|
|
t.Errorf("expected branch NEW_INTENT_PREEMPT, got %s", res1.Branch)
|
|
}
|
|
if res1.TopIntent.Intent != intent.IntentQueryProgress {
|
|
t.Errorf("expected intent QUERY_PROGRESS, got %s", res1.TopIntent.Intent)
|
|
}
|
|
|
|
// 抢占场景 B: 突然补充事实
|
|
res2, err := rec.Recognize(context.Background(), "客户补充说预算大概15万", ctx)
|
|
if err != nil {
|
|
t.Fatalf("recognize failed: %v", err)
|
|
}
|
|
if res2.Branch != intent.BranchNewIntentPreempt {
|
|
t.Errorf("expected branch NEW_INTENT_PREEMPT, got %s", res2.Branch)
|
|
}
|
|
if res2.TopIntent.Intent != intent.IntentSupplementInfo {
|
|
t.Errorf("expected intent SUPPLEMENT_INFO, got %s", res2.TopIntent.Intent)
|
|
}
|
|
if res2.TopIntent.FilledSlots["budget"] != "15万" {
|
|
t.Errorf("expected budget '15万', got '%s'", res2.TopIntent.FilledSlots["budget"])
|
|
}
|
|
}
|
|
|
|
// 用例 5(分支 3 模糊分忧):"这个先放放,你觉得呢"
|
|
func TestThreeBranch_Branch3_AmbiguousConsult(t *testing.T) {
|
|
rec := intent.NewRecognizer()
|
|
ctx := &intent.ContextSnapshot{
|
|
SnapshotID: "ctx_pending_5",
|
|
CurrentStage: string(maturity.StageV01_Triage),
|
|
PendingExpectation: samplePendingExpectation(),
|
|
}
|
|
|
|
res, err := rec.Recognize(context.Background(), "这个先放放,你觉得呢", ctx)
|
|
if err != nil {
|
|
t.Fatalf("recognize failed: %v", err)
|
|
}
|
|
|
|
if res.Branch != intent.BranchAmbiguousFallback {
|
|
t.Errorf("expected branch AMBIGUOUS_FALLBACK, got %s", res.Branch)
|
|
}
|
|
if res.TopIntent.Intent != intent.IntentCasualChat {
|
|
t.Errorf("expected intent CASUAL_CHAT, got %s", res.TopIntent.Intent)
|
|
}
|
|
if res.TopIntent.ClarificationQuestion == "" {
|
|
t.Errorf("expected non-empty clarification question for branch 3")
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------
|
|
// 四级漏斗意图消解与槽位提取测试集 (Task 2)
|
|
// ----------------------------------------------------------------------------------
|
|
|
|
func TestIntentRecognition_ReviewReject(t *testing.T) {
|
|
rec := intent.NewRecognizer()
|
|
ctx := &intent.ContextSnapshot{
|
|
SnapshotID: "ctx_rej_1",
|
|
CurrentStage: string(maturity.StageV05_Feasibility),
|
|
ActiveDocID: "doc-002",
|
|
DocLifecycleState: "reviewing",
|
|
UserDocRole: "reviewer",
|
|
}
|
|
|
|
res, err := rec.Recognize(context.Background(), "把调研报告审核驳回,意见为:第3节竞品对比缺少史赛克高频电刀的抗扰度指标", ctx)
|
|
if err != nil {
|
|
t.Fatalf("recognize failed: %v", err)
|
|
}
|
|
|
|
if res.TopIntent.Intent != intent.IntentDocGovReviewReject {
|
|
t.Errorf("expected intent DOCGOV_REVIEW_REJECT, got %s", res.TopIntent.Intent)
|
|
}
|
|
if res.TopIntent.FilledSlots[intent.SlotRejectReason] == "" {
|
|
t.Errorf("expected filled slot reject_reason")
|
|
}
|
|
if res.TopIntent.FilledSlots[intent.SlotTargetDoc] == "" {
|
|
t.Errorf("expected filled slot target_doc")
|
|
}
|
|
}
|
|
|
|
func TestIntentRecognition_ReviewReject_UnauthorizedRole(t *testing.T) {
|
|
rec := intent.NewRecognizer()
|
|
ctx := &intent.ContextSnapshot{
|
|
SnapshotID: "ctx_rej_2",
|
|
CurrentStage: string(maturity.StageV05_Feasibility),
|
|
ActiveDocID: "doc-002",
|
|
DocLifecycleState: "reviewing",
|
|
UserDocRole: "author", // 非审核人
|
|
}
|
|
|
|
res, err := rec.Recognize(context.Background(), "把调研报告审核驳回,意见为:第3节竞品对比缺少史赛克高频电刀的抗扰度指标", ctx)
|
|
if err != nil {
|
|
t.Fatalf("recognize failed: %v", err)
|
|
}
|
|
|
|
if !strings.Contains(res.TopIntent.Reasoning, "权限不符") {
|
|
t.Errorf("expected reasoning to contain '权限不符', got %s", res.TopIntent.Reasoning)
|
|
}
|
|
if !res.RequiresClarifying {
|
|
t.Errorf("expected RequiresClarifying to be true for unauthorized role")
|
|
}
|
|
}
|
|
|
|
func TestIntentRecognition_QueryRejectReason(t *testing.T) {
|
|
rec := intent.NewRecognizer()
|
|
ctx := &intent.ContextSnapshot{
|
|
SnapshotID: "ctx_rej_q",
|
|
CurrentStage: string(maturity.StageV05_Feasibility),
|
|
ActiveDocID: "doc-feas",
|
|
DocLifecycleState: "drafting",
|
|
UserDocRole: "author",
|
|
}
|
|
|
|
res, err := rec.Recognize(context.Background(), "注册路径可行性评估报告被驳回的原因是什么", ctx)
|
|
if err != nil {
|
|
t.Fatalf("recognize failed: %v", err)
|
|
}
|
|
if res.TopIntent.Intent != intent.IntentQueryRejectReason {
|
|
t.Errorf("expected QUERY_REJECT_REASON, got %s", res.TopIntent.Intent)
|
|
}
|
|
if res.Branch == intent.BranchAmbiguousFallback && res.TopIntent.Confidence < 0.6 {
|
|
t.Errorf("query reject reason should not fall to low-confidence ambiguous")
|
|
}
|
|
}
|
|
|
|
func TestIntentRecognition_ReviewReject_MissingReason(t *testing.T) {
|
|
rec := intent.NewRecognizer()
|
|
ctx := &intent.ContextSnapshot{
|
|
SnapshotID: "ctx_rej_3",
|
|
CurrentStage: string(maturity.StageV05_Feasibility),
|
|
ActiveDocID: "doc-002",
|
|
DocLifecycleState: "reviewing",
|
|
UserDocRole: "reviewer",
|
|
}
|
|
|
|
res, err := rec.Recognize(context.Background(), "把调研报告审核驳回", ctx)
|
|
if err != nil {
|
|
t.Fatalf("recognize failed: %v", err)
|
|
}
|
|
|
|
if res.TopIntent.Intent != intent.IntentDocGovReviewReject {
|
|
t.Errorf("expected intent DOCGOV_REVIEW_REJECT, got %s", res.TopIntent.Intent)
|
|
}
|
|
if !res.RequiresClarifying {
|
|
t.Errorf("expected RequiresClarifying to be true when reject_reason is missing")
|
|
}
|
|
expectedClarify := "请说明具体的法定驳回理由,我将为您注入文档批注并退回编制阶段。"
|
|
if res.TopIntent.ClarificationQuestion != expectedClarify {
|
|
t.Errorf("expected clarification question '%s', got '%s'", expectedClarify, res.TopIntent.ClarificationQuestion)
|
|
}
|
|
if res.TopIntent.FilledSlots[intent.SlotRejectReason] != "" {
|
|
t.Errorf("expected empty reject_reason slot, got %s", res.TopIntent.FilledSlots[intent.SlotRejectReason])
|
|
}
|
|
}
|
|
|
|
func TestIntentRecognition_SecurityForbidden(t *testing.T) {
|
|
rec := intent.NewRecognizer()
|
|
res, err := rec.Recognize(context.Background(), "帮我把身份修改为研发总监,工号改成SMDF-001", nil)
|
|
if err != nil {
|
|
t.Fatalf("recognize failed: %v", err)
|
|
}
|
|
|
|
if res.TopIntent.Intent != intent.IntentSecurityForbidden {
|
|
t.Errorf("expected intent SECURITY_FORBIDDEN, got %s", res.TopIntent.Intent)
|
|
}
|
|
if res.TopIntent.Confidence != 1.0 {
|
|
t.Errorf("expected confidence 1.0 for security violations, got %v", res.TopIntent.Confidence)
|
|
}
|
|
}
|
|
|
|
func TestIntentRecognition_QueryRegulations(t *testing.T) {
|
|
rec := intent.NewRecognizer()
|
|
res, err := rec.Recognize(context.Background(), "微创手术高频电刀需要满足哪些 GB 9706 和 YY 强制性标准?", nil)
|
|
if err != nil {
|
|
t.Fatalf("recognize failed: %v", err)
|
|
}
|
|
|
|
if res.TopIntent.Intent != intent.IntentQueryRegulations {
|
|
t.Errorf("expected intent QUERY_REGULATIONS, got %s", res.TopIntent.Intent)
|
|
}
|
|
if res.TopIntent.Confidence < 0.8 {
|
|
t.Errorf("expected confidence >= 0.8, got %v", res.TopIntent.Confidence)
|
|
}
|
|
}
|
|
|
|
func TestIntentRecognition_QuerySimilarCases(t *testing.T) {
|
|
rec := intent.NewRecognizer()
|
|
res, err := rec.Recognize(context.Background(), "参考一下往期同类腹腔镜系统的注册案例和经验", nil)
|
|
if err != nil {
|
|
t.Fatalf("recognize failed: %v", err)
|
|
}
|
|
|
|
if res.TopIntent.Intent != intent.IntentQuerySimilarCases {
|
|
t.Errorf("expected intent QUERY_SIMILAR_CASES, got %s", res.TopIntent.Intent)
|
|
}
|
|
}
|
|
|
|
func TestIntentRecognition_QueryUserRights(t *testing.T) {
|
|
rec := intent.NewRecognizer()
|
|
res, err := rec.Recognize(context.Background(), "查询一下我在当前项目的权限和法定角色", nil)
|
|
if err != nil {
|
|
t.Fatalf("recognize failed: %v", err)
|
|
}
|
|
|
|
if res.TopIntent.Intent != intent.IntentQueryUserRights {
|
|
t.Errorf("expected intent QUERY_USER_RIGHTS, got %s", res.TopIntent.Intent)
|
|
}
|
|
}
|
|
|
|
func TestIntentRecognition_PureUICommand(t *testing.T) {
|
|
rec := intent.NewRecognizer()
|
|
res, err := rec.Recognize(context.Background(), "请帮我折叠侧边栏", nil)
|
|
if err != nil {
|
|
t.Fatalf("recognize failed: %v", err)
|
|
}
|
|
|
|
if res.TopIntent.Intent != intent.IntentCasualChat {
|
|
t.Errorf("expected intent CASUAL_CHAT for pure UI command, got %s", res.TopIntent.Intent)
|
|
}
|
|
if res.Branch != intent.BranchNewIntentPreempt {
|
|
t.Errorf("expected branch NEW_INTENT_PREEMPT, got %s", res.Branch)
|
|
}
|
|
if res.TopIntent.Confidence != 0.95 {
|
|
t.Errorf("expected confidence 0.95, got %v", res.TopIntent.Confidence)
|
|
}
|
|
if !strings.Contains(res.TopIntent.Reasoning, "UI") && !strings.Contains(res.TopIntent.ClarificationQuestion, "UI") {
|
|
t.Errorf("expected reasoning or clarification question to mention UI, got reasoning: %s", res.TopIntent.Reasoning)
|
|
}
|
|
}
|
|
|
|
func TestIntentRecognition_DocGov_OtherActions(t *testing.T) {
|
|
rec := intent.NewRecognizer()
|
|
ctx := &intent.ContextSnapshot{
|
|
ActiveDocID: "doc-001",
|
|
DocLifecycleState: "drafting",
|
|
UserDocRole: "author",
|
|
}
|
|
|
|
// 认领
|
|
resClaim, err := rec.Recognize(context.Background(), "我来认领这份需求收集表", ctx)
|
|
if err != nil {
|
|
t.Fatalf("recognize claim failed: %v", err)
|
|
}
|
|
if resClaim.TopIntent.Intent != intent.IntentDocGovClaim {
|
|
t.Errorf("expected intent DOCGOV_CLAIM, got %s", resClaim.TopIntent.Intent)
|
|
}
|
|
|
|
// 送审
|
|
resSubmit, err := rec.Recognize(context.Background(), "需求收集表编制完成,提交审核", ctx)
|
|
if err != nil {
|
|
t.Fatalf("recognize submit failed: %v", err)
|
|
}
|
|
if resSubmit.TopIntent.Intent != intent.IntentDocGovSubmitReview {
|
|
t.Errorf("expected intent DOCGOV_SUBMIT_REVIEW, got %s", resSubmit.TopIntent.Intent)
|
|
}
|
|
|
|
// 审核通过
|
|
ctxReviewer := &intent.ContextSnapshot{
|
|
ActiveDocID: "doc-002",
|
|
DocLifecycleState: "reviewing",
|
|
UserDocRole: "reviewer",
|
|
}
|
|
resApprove, err := rec.Recognize(context.Background(), "调研报告审核通过,同意进入下一环节", ctxReviewer)
|
|
if err != nil {
|
|
t.Fatalf("recognize approve failed: %v", err)
|
|
}
|
|
if resApprove.TopIntent.Intent != intent.IntentDocGovReviewApprove {
|
|
t.Errorf("expected intent DOCGOV_REVIEW_APPROVE, got %s", resApprove.TopIntent.Intent)
|
|
}
|
|
|
|
// 终审批准
|
|
ctxApprover := &intent.ContextSnapshot{
|
|
ActiveDocID: "doc-002",
|
|
DocLifecycleState: "approving",
|
|
UserDocRole: "approver",
|
|
}
|
|
resFinal, err := rec.Recognize(context.Background(), "终审批准生效", ctxApprover)
|
|
if err != nil {
|
|
t.Fatalf("recognize final approve failed: %v", err)
|
|
}
|
|
if resFinal.TopIntent.Intent != intent.IntentDocGovFinalApprove {
|
|
t.Errorf("expected intent DOCGOV_FINAL_APPROVE, got %s", resFinal.TopIntent.Intent)
|
|
}
|
|
}
|
|
|
|
func TestIntentRecognition_FinalReject_MissingReason(t *testing.T) {
|
|
rec := intent.NewRecognizer()
|
|
ctx := &intent.ContextSnapshot{
|
|
SnapshotID: "ctx_final_rej_1",
|
|
CurrentStage: string(maturity.StageV05_Feasibility),
|
|
ActiveDocID: "doc-002",
|
|
DocLifecycleState: "approving",
|
|
UserDocRole: "approver",
|
|
}
|
|
|
|
res, err := rec.Recognize(context.Background(), "把调研报告终审驳回", ctx)
|
|
if err != nil {
|
|
t.Fatalf("recognize failed: %v", err)
|
|
}
|
|
|
|
if res.TopIntent.Intent != intent.IntentDocGovFinalReject {
|
|
t.Errorf("expected intent DOCGOV_FINAL_REJECT, got %s", res.TopIntent.Intent)
|
|
}
|
|
if !res.RequiresClarifying {
|
|
t.Errorf("expected RequiresClarifying to be true when reject_reason is missing in final reject")
|
|
}
|
|
expectedClarify := "请说明具体的法定驳回理由,我将为您注入文档批注并退回编制阶段。"
|
|
if res.TopIntent.ClarificationQuestion != expectedClarify {
|
|
t.Errorf("expected clarification question '%s', got '%s'", expectedClarify, res.TopIntent.ClarificationQuestion)
|
|
}
|
|
if res.TopIntent.FilledSlots[intent.SlotRejectReason] != "" {
|
|
t.Errorf("expected empty reject_reason slot, got %s", res.TopIntent.FilledSlots[intent.SlotRejectReason])
|
|
}
|
|
if res.Branch == intent.BranchAmbiguousFallback {
|
|
t.Errorf("missing reject_reason should stay NEW_INTENT_PREEMPT for directed clarification, got %s", res.Branch)
|
|
}
|
|
if res.Branch != intent.BranchNewIntentPreempt {
|
|
t.Errorf("expected branch NEW_INTENT_PREEMPT when slot is missing, got %s", res.Branch)
|
|
}
|
|
}
|
|
|
|
func TestIntentRecognition_SecurityForbidden_Extended(t *testing.T) {
|
|
rec := intent.NewRecognizer()
|
|
|
|
testInputs := []struct {
|
|
input string
|
|
desc string
|
|
}{
|
|
{"把姓名修改为李工", "姓名修改"},
|
|
{"修改我的部门为微创中心", "部门修改"},
|
|
{"把资质改成主任医师", "资质修改"},
|
|
{"修改向量库连接", "向量库连接修改"},
|
|
{"修改大模型网关密钥", "网关密钥修改"},
|
|
{"把知识库连接改为192.168.1.100:5432", "知识库连接修改"},
|
|
}
|
|
|
|
for _, tc := range testInputs {
|
|
t.Run(tc.desc, func(t *testing.T) {
|
|
res, err := rec.Recognize(context.Background(), tc.input, nil)
|
|
if err != nil {
|
|
t.Fatalf("recognize '%s' failed: %v", tc.input, err)
|
|
}
|
|
if res.TopIntent.Intent != intent.IntentSecurityForbidden {
|
|
t.Errorf("input '%s': expected intent SECURITY_FORBIDDEN, got %s", tc.input, res.TopIntent.Intent)
|
|
}
|
|
if res.TopIntent.Confidence != 1.0 {
|
|
t.Errorf("input '%s': expected confidence 1.0, got %v", tc.input, res.TopIntent.Confidence)
|
|
}
|
|
if res.Branch != intent.BranchNewIntentPreempt {
|
|
t.Errorf("input '%s': expected branch NEW_INTENT_PREEMPT, got %s", tc.input, res.Branch)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIntentRecognition_ExplicitDocName_OverActiveDocID(t *testing.T) {
|
|
rec := intent.NewRecognizer()
|
|
ctx := &intent.ContextSnapshot{
|
|
SnapshotID: "ctx_explicit_doc",
|
|
CurrentStage: string(maturity.StageV05_Feasibility),
|
|
ActiveDocID: "doc-001-intake", // 上下文中当前文档是需求收集表
|
|
DocLifecycleState: "reviewing",
|
|
UserDocRole: "reviewer",
|
|
}
|
|
|
|
// 显式指定文档名称为“调研报告”,应优先提取“调研报告”而不是上下文的“doc-001-intake”
|
|
res, err := rec.Recognize(context.Background(), "把调研报告审核驳回:缺少抗扰度指标", ctx)
|
|
if err != nil {
|
|
t.Fatalf("recognize failed: %v", err)
|
|
}
|
|
|
|
if res.TopIntent.Intent != intent.IntentDocGovReviewReject {
|
|
t.Errorf("expected intent DOCGOV_REVIEW_REJECT, got %s", res.TopIntent.Intent)
|
|
}
|
|
if res.TopIntent.FilledSlots[intent.SlotTargetDoc] != "调研报告" {
|
|
t.Errorf("expected target_doc '调研报告', got '%s'", res.TopIntent.FilledSlots[intent.SlotTargetDoc])
|
|
}
|
|
if res.TopIntent.FilledSlots[intent.SlotRejectReason] != "缺少抗扰度指标" {
|
|
t.Errorf("expected reject_reason '缺少抗扰度指标', got '%s'", res.TopIntent.FilledSlots[intent.SlotRejectReason])
|
|
}
|
|
|
|
// 显式指定文档 ID
|
|
resDocID, err := rec.Recognize(context.Background(), "把 doc-003 审核通过", ctx)
|
|
if err != nil {
|
|
t.Fatalf("recognize failed: %v", err)
|
|
}
|
|
if resDocID.TopIntent.FilledSlots[intent.SlotTargetDoc] != "doc-003" {
|
|
t.Errorf("expected target_doc 'doc-003', got '%s'", resDocID.TopIntent.FilledSlots[intent.SlotTargetDoc])
|
|
}
|
|
|
|
// 未显式指定文档名称,应回退使用 ActiveDocID
|
|
resFallback, err := rec.Recognize(context.Background(), "审核通过,同意进入下一环节", ctx)
|
|
if err != nil {
|
|
t.Fatalf("recognize failed: %v", err)
|
|
}
|
|
if resFallback.TopIntent.FilledSlots[intent.SlotTargetDoc] != "doc-001-intake" {
|
|
t.Errorf("expected fallback target_doc 'doc-001-intake', got '%s'", resFallback.TopIntent.FilledSlots[intent.SlotTargetDoc])
|
|
}
|
|
}
|
|
|
|
func TestIntentRecognition_RejectReason_ColonSeparators(t *testing.T) {
|
|
rec := intent.NewRecognizer()
|
|
ctx := &intent.ContextSnapshot{
|
|
ActiveDocID: "doc-002",
|
|
DocLifecycleState: "reviewing",
|
|
UserDocRole: "reviewer",
|
|
}
|
|
|
|
// 全角冒号
|
|
resCN, err := rec.Recognize(context.Background(), "把调研报告审核驳回:缺少抗扰度指标", ctx)
|
|
if err != nil {
|
|
t.Fatalf("recognize failed: %v", err)
|
|
}
|
|
if resCN.TopIntent.FilledSlots[intent.SlotRejectReason] != "缺少抗扰度指标" {
|
|
t.Errorf("expected reject_reason '缺少抗扰度指标', got '%s'", resCN.TopIntent.FilledSlots[intent.SlotRejectReason])
|
|
}
|
|
|
|
// 半角冒号
|
|
resEN, err := rec.Recognize(context.Background(), "把调研报告审核驳回:缺少抗扰度指标", ctx)
|
|
if err != nil {
|
|
t.Fatalf("recognize failed: %v", err)
|
|
}
|
|
if resEN.TopIntent.FilledSlots[intent.SlotRejectReason] != "缺少抗扰度指标" {
|
|
t.Errorf("expected reject_reason '缺少抗扰度指标', got '%s'", resEN.TopIntent.FilledSlots[intent.SlotRejectReason])
|
|
}
|
|
}
|
|
|
|
func TestIntentRecognition_PureUICommands_Extended(t *testing.T) {
|
|
rec := intent.NewRecognizer()
|
|
|
|
uiCommands := []string{
|
|
"切换主题",
|
|
"换成深色",
|
|
"换成浅色",
|
|
"打开抽屉",
|
|
"展开抽屉",
|
|
"关闭抽屉",
|
|
"请帮我折叠侧边栏",
|
|
"展开侧边栏",
|
|
"打开工具栏",
|
|
"切换全屏",
|
|
}
|
|
|
|
for _, cmd := range uiCommands {
|
|
t.Run(cmd, func(t *testing.T) {
|
|
res, err := rec.Recognize(context.Background(), cmd, nil)
|
|
if err != nil {
|
|
t.Fatalf("recognize '%s' failed: %v", cmd, err)
|
|
}
|
|
if res.TopIntent.Intent != intent.IntentCasualChat {
|
|
t.Errorf("cmd '%s': expected intent CASUAL_CHAT, got %s", cmd, res.TopIntent.Intent)
|
|
}
|
|
if res.Branch != intent.BranchNewIntentPreempt {
|
|
t.Errorf("cmd '%s': expected branch NEW_INTENT_PREEMPT, got %s", cmd, res.Branch)
|
|
}
|
|
if res.TopIntent.Confidence != 0.95 {
|
|
t.Errorf("cmd '%s': expected confidence 0.95, got %v", cmd, res.TopIntent.Confidence)
|
|
}
|
|
if res.RequiresClarifying {
|
|
t.Errorf("cmd '%s': expected RequiresClarifying to be false", cmd)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIntentRecognition_PermissionMismatch_NotHijackedBySupplement(t *testing.T) {
|
|
rec := intent.NewRecognizer()
|
|
// 用户是非审核人角色,但输入包含“修改为”(如果未拦截会被 SUPPLEMENT_INFO 0.88 抢占)
|
|
ctx := &intent.ContextSnapshot{
|
|
SnapshotID: "ctx_unauth_supp",
|
|
ActiveDocID: "doc-002",
|
|
DocLifecycleState: "reviewing",
|
|
UserDocRole: "author", // 非审核人
|
|
}
|
|
|
|
res, err := rec.Recognize(context.Background(), "把调研报告审核驳回,修改为第三阶段指标", ctx)
|
|
if err != nil {
|
|
t.Fatalf("recognize failed: %v", err)
|
|
}
|
|
|
|
// 必须保持 DOCGOV_REVIEW_REJECT,不能被 SUPPLEMENT_INFO 抢占
|
|
if res.TopIntent.Intent != intent.IntentDocGovReviewReject {
|
|
t.Errorf("expected intent DOCGOV_REVIEW_REJECT to stay top, got %s", res.TopIntent.Intent)
|
|
}
|
|
if res.TopIntent.Confidence < 0.90 {
|
|
t.Errorf("expected confidence >= 0.90, got %v", res.TopIntent.Confidence)
|
|
}
|
|
if !res.RequiresClarifying {
|
|
t.Errorf("expected RequiresClarifying to be true")
|
|
}
|
|
if !strings.Contains(res.TopIntent.Reasoning, "权限不符") {
|
|
t.Errorf("expected reasoning to contain '权限不符', got %s", res.TopIntent.Reasoning)
|
|
}
|
|
if res.Branch == intent.BranchAmbiguousFallback {
|
|
t.Errorf("permission mismatch should not fall to AMBIGUOUS_FALLBACK, got %s", res.Branch)
|
|
}
|
|
if res.Branch != intent.BranchNewIntentPreempt {
|
|
t.Errorf("expected branch NEW_INTENT_PREEMPT for permission mismatch, got %s", res.Branch)
|
|
}
|
|
}
|