Files
Agent-System/internal/intent/doc_workflow_scope_test.go
T
weichen db859e185e fix(intent): 让「帮我定任务书」进入任务并说清失败
写动词补上制定/拟定与祈使「帮我定」,避免落到阶段对照模板。质量未通过不再声称正文已保存到宿主机路径;研发前置未满足时把中文原因直接回复给用户。
2026-09-28 04:01:02 +00:00

232 lines
8.9 KiB
Go

package intent_test
import (
"context"
"strings"
"testing"
"chat-agent-system/internal/intent"
"chat-agent-system/internal/workflowscope"
)
// ----------------------------------------------------------------------------------
// 工作流作用域填槽:target_doc / target_task 只命中激活工作流;跨流点名拦截引导
// ----------------------------------------------------------------------------------
func TestWorkflowScope_TriggerTaskInActiveFlow(t *testing.T) {
rec := intent.NewRecognizer()
ctx := &intent.ContextSnapshot{SnapshotID: "s_bd", ActiveWorkflowKey: "main"}
// 激活商务流:调研报告属 main,正常填 task
res, err := rec.Recognize(context.Background(), "帮我生成调研报告", ctx)
if err != nil {
t.Fatal(err)
}
if res.TopIntent.Intent != intent.IntentTriggerTask {
t.Fatalf("want TRIGGER_TASK, got %s", res.TopIntent.Intent)
}
if got := res.TopIntent.FilledSlots[intent.SlotTargetTask]; got != "research" {
t.Fatalf("in-scope research want research, got %q", got)
}
if res.TopIntent.FilledSlots[intent.SlotWorkflowScopeMismatch] != "" {
t.Fatalf("in-scope task must not carry mismatch marker")
}
}
func TestWorkflowScope_TriggerTaskCrossFlowBlocked(t *testing.T) {
rec := intent.NewRecognizer()
ctx := &intent.ContextSnapshot{SnapshotID: "s_bd2", ActiveWorkflowKey: "main"}
// 激活商务流却说「编写项目建议书」(研发流 rd_proposal)→ 拦截引导
res, err := rec.Recognize(context.Background(), "编写项目建议书", ctx)
if err != nil {
t.Fatal(err)
}
slots := res.TopIntent.FilledSlots
if slots[intent.SlotWorkflowScopeMismatch] != "true" {
t.Fatalf("cross-flow write must be blocked, slots=%v", slots)
}
if slots[intent.SlotTargetTask] != "" || slots[intent.SlotTargetDoc] != "" {
t.Fatalf("cross-flow must not fill target_doc/target_task, got %v", slots)
}
if !strings.Contains(res.TopIntent.ClarificationQuestion, "切换") {
t.Fatalf("guidance should mention 切换, got %q", res.TopIntent.ClarificationQuestion)
}
if !strings.Contains(res.TopIntent.ClarificationQuestion, "研发设计控制流程") {
t.Fatalf("guidance should name owning flow, got %q", res.TopIntent.ClarificationQuestion)
}
}
func TestWorkflowScope_TriggerTaskAfterSwitch(t *testing.T) {
rec := intent.NewRecognizer()
ctx := &intent.ContextSnapshot{SnapshotID: "s_rd", ActiveWorkflowKey: "rdev"}
// 激活研发流后同名操作 → 正常执行
res, err := rec.Recognize(context.Background(), "编写项目建议书", ctx)
if err != nil {
t.Fatal(err)
}
if res.TopIntent.Intent != intent.IntentTriggerTask {
t.Fatalf("want TRIGGER_TASK after switch, got %s", res.TopIntent.Intent)
}
if res.TopIntent.FilledSlots[intent.SlotWorkflowScopeMismatch] == "true" {
t.Fatalf("must not block in owning flow, slots=%v", res.TopIntent.FilledSlots)
}
if got := res.TopIntent.FilledSlots[intent.SlotTargetTask]; got != "rd_proposal" {
t.Fatalf("want rd_proposal, got %q", got)
}
}
func TestWorkflowScope_DocGovWriteCrossFlowBlocked(t *testing.T) {
rec := intent.NewRecognizer()
ctx := &intent.ContextSnapshot{
SnapshotID: "s_pm",
ActiveWorkflowKey: "product_manager",
UserDocRole: "reviewer",
}
// 激活产品经理流,却驳回商务流文档「调研报告」→ 治理写动作被前置剪枝拦截
res, err := rec.Recognize(context.Background(), "把调研报告审核驳回", ctx)
if err != nil {
t.Fatal(err)
}
if res.TopIntent.Intent == intent.IntentDocGovReviewReject {
t.Fatalf("cross-flow governance reject must be blocked, got %s", res.TopIntent.Intent)
}
if res.TopIntent.FilledSlots[intent.SlotWorkflowScopeMismatch] != "true" {
t.Fatalf("expected mismatch marker, slots=%v", res.TopIntent.FilledSlots)
}
if !res.RequiresClarifying {
t.Fatalf("cross-flow block must require clarification")
}
}
func TestWorkflowScope_LegacyNoActiveWorkflowUnchanged(t *testing.T) {
rec := intent.NewRecognizer()
// 存量单流 / planning 态:无激活工作流 → 不过滤,行为与修复前一致
ctx := &intent.ContextSnapshot{SnapshotID: "s_legacy"}
res, err := rec.Recognize(context.Background(), "编写项目建议书", ctx)
if err != nil {
t.Fatal(err)
}
if res.TopIntent.FilledSlots[intent.SlotWorkflowScopeMismatch] == "true" {
t.Fatalf("no active workflow must never block, got %v", res.TopIntent.FilledSlots)
}
if res.TopIntent.Intent != intent.IntentTriggerTask {
t.Fatalf("want TRIGGER_TASK passthrough, got %s", res.TopIntent.Intent)
}
}
func TestWorkflowScope_WeightedInScopeBeatsCrossPhrase(t *testing.T) {
workflowscope.SetModeForTest(workflowscope.ModeWeighted)
t.Cleanup(workflowscope.ResetModeForTest)
rec := intent.NewRecognizer()
ctx := &intent.ContextSnapshot{SnapshotID: "s_w", ActiveWorkflowKey: "main"}
res, err := rec.Recognize(context.Background(), "生成调研报告,不要写成项目建议书", ctx)
if err != nil {
t.Fatal(err)
}
if res.TopIntent.FilledSlots[intent.SlotWorkflowScopeMismatch] == "true" {
t.Fatalf("in-scope research must not be hijacked, slots=%v", res.TopIntent.FilledSlots)
}
if got := res.TopIntent.FilledSlots[intent.SlotTargetTask]; got != "research" {
t.Fatalf("want research, got %q slots=%v", got, res.TopIntent.FilledSlots)
}
}
func TestWorkflowScope_WeightedReadCrossAllowed(t *testing.T) {
workflowscope.SetModeForTest(workflowscope.ModeWeighted)
t.Cleanup(workflowscope.ResetModeForTest)
rec := intent.NewRecognizer()
ctx := &intent.ContextSnapshot{SnapshotID: "s_r", ActiveWorkflowKey: "main"}
res, err := rec.Recognize(context.Background(), "查看项目建议书", ctx)
if err != nil {
t.Fatal(err)
}
if res.TopIntent.FilledSlots[intent.SlotWorkflowScopeMismatch] == "true" {
t.Fatalf("weighted read must not block, slots=%v", res.TopIntent.FilledSlots)
}
if res.TopIntent.FilledSlots[intent.SlotCrossWorkflowOwner] != "rdev" {
t.Fatalf("want rdev owner, slots=%v", res.TopIntent.FilledSlots)
}
if !strings.Contains(res.TopIntent.FilledSlots[intent.SlotTargetDoc], "项目建议书") {
t.Fatalf("want target doc, slots=%v", res.TopIntent.FilledSlots)
}
}
func TestWorkflowScope_WeightedWriteStillBlocked(t *testing.T) {
workflowscope.SetModeForTest(workflowscope.ModeWeighted)
t.Cleanup(workflowscope.ResetModeForTest)
rec := intent.NewRecognizer()
ctx := &intent.ContextSnapshot{SnapshotID: "s_wb", ActiveWorkflowKey: "main"}
res, err := rec.Recognize(context.Background(), "编写项目建议书", ctx)
if err != nil {
t.Fatal(err)
}
if res.TopIntent.FilledSlots[intent.SlotWorkflowScopeMismatch] != "true" {
t.Fatalf("weighted write must still block, slots=%v intent=%s", res.TopIntent.FilledSlots, res.TopIntent.Intent)
}
}
func TestWorkflowScope_SnapshotCarriesActiveWorkflowKey(t *testing.T) {
// ContextSnapshot 现在必须能携带激活工作流键,供消费端与识别器共同约束。
snap := intent.ContextSnapshot{ActiveWorkflowKey: "main"}
if snap.ActiveWorkflowKey != "main" {
t.Fatalf("ActiveWorkflowKey must be settable, got %q", snap.ActiveWorkflowKey)
}
}
func TestWriteVerb_DingDesignPlanCrossFlow(t *testing.T) {
rec := intent.NewRecognizer()
ctx := &intent.ContextSnapshot{SnapshotID: "s_ding", ActiveWorkflowKey: "main"}
res, err := rec.Recognize(context.Background(), "帮我定设计开发计划任务书", ctx)
if err != nil {
t.Fatal(err)
}
if res.TopIntent.Intent != intent.IntentTriggerTask {
t.Fatalf("want TRIGGER_TASK, got %s reasoning=%s", res.TopIntent.Intent, res.TopIntent.Reasoning)
}
if res.TopIntent.FilledSlots[intent.SlotWorkflowScopeMismatch] != "true" {
t.Fatalf("main flow must block rd_design_plan, slots=%v", res.TopIntent.FilledSlots)
}
if !strings.Contains(res.TopIntent.ClarificationQuestion, "研发设计控制流程") {
t.Fatalf("guidance=%q", res.TopIntent.ClarificationQuestion)
}
if res.Branch != intent.BranchAmbiguousFallback && !res.RequiresClarifying {
t.Fatalf("cross-flow write should require clarifying, branch=%s", res.Branch)
}
}
func TestWriteVerb_ZhidingDesignPlanOnRdev(t *testing.T) {
rec := intent.NewRecognizer()
ctx := &intent.ContextSnapshot{SnapshotID: "s_zhiding", ActiveWorkflowKey: "rdev"}
res, err := rec.Recognize(context.Background(), "帮我制定设计开发计划任务书", ctx)
if err != nil {
t.Fatal(err)
}
if res.TopIntent.Intent != intent.IntentTriggerTask {
t.Fatalf("want TRIGGER_TASK, got %s", res.TopIntent.Intent)
}
if res.TopIntent.FilledSlots[intent.SlotWorkflowScopeMismatch] == "true" {
t.Fatalf("owning flow must not block, slots=%v", res.TopIntent.FilledSlots)
}
if got := res.TopIntent.FilledSlots[intent.SlotTargetTask]; got != "rd_design_plan" {
t.Fatalf("task=%q", got)
}
}
func TestWriteVerb_DecideIsNotWrite(t *testing.T) {
rec := intent.NewRecognizer()
res, err := rec.Recognize(context.Background(), "我决定先放一放,再看看进展", nil)
if err != nil {
t.Fatal(err)
}
if res.TopIntent.Intent == intent.IntentTriggerTask {
t.Fatalf("决定 must not trigger a deliverable, slots=%v", res.TopIntent.FilledSlots)
}
}