Files
Agent-System/internal/intent/doc_workflow_scope_test.go
T
weichenandCursor 5c6b4f7719 feat(intent): scope slot filling to the active workflow
- ContextSnapshot gains ActiveWorkflowKey; ToSnapshot passes it downstream
- manifest-backed doc-title -> workflow index (main/pm/tm/rdev/sw)
- TRIGGER_TASK and DOCGOV write actions blocked with switch guidance when
  the named document belongs to another active workflow
- legacy projects without an active workflow key keep the old behavior

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-21 07:38:49 +00:00

127 lines
4.7 KiB
Go

package intent_test
import (
"context"
"strings"
"testing"
"chat-agent-system/internal/intent"
)
// ----------------------------------------------------------------------------------
// 工作流作用域填槽: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_SnapshotCarriesActiveWorkflowKey(t *testing.T) {
// ContextSnapshot 现在必须能携带激活工作流键,供消费端与识别器共同约束。
snap := intent.ContextSnapshot{ActiveWorkflowKey: "main"}
if snap.ActiveWorkflowKey != "main" {
t.Fatalf("ActiveWorkflowKey must be settable, got %q", snap.ActiveWorkflowKey)
}
}