Weighted mode keeps an in-scope document when a cross-workflow name is only mentioned beside it, while exact cross-workflow writes stay blocked and exact reads record the owning workflow. Co-authored-by: Cursor <cursoragent@cursor.com>
182 lines
6.9 KiB
Go
182 lines
6.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)
|
|
}
|
|
}
|