- 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>
127 lines
4.7 KiB
Go
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)
|
|
}
|
|
}
|