Files
weichen b5a782bb38 feat(chat): dialogue triggers for comments/reopen/pending-supplements with mistouch guards
Wire intent→coordinator DocResolver confirm cards→action_executor kinds for comment, reopen, pending-supplements query, and real submit-review execution.
2026-09-25 20:14:10 +00:00

210 lines
9.3 KiB
Go

package coordinator
import (
"context"
"strings"
"testing"
"chat-agent-system/internal/intent"
"chat-agent-system/internal/maturity"
)
// ============================================================================
// 任务画像黄金用例(确定性规则,可解释可回归)
// ============================================================================
func TestProfileTask_GoldenCases(t *testing.T) {
cases := []struct {
name string
input string
minScore int
form DeliverableForm
upgrade bool
}{
{"简单问候", "你好,你是谁?", 0, FormAnswer, false},
{"单点技术问答", "什么是逆压电效应?", 0, FormAnswer, false},
{"进度闲聊", "这个项目现在怎么样了", 0, FormAnswer, false},
{"复杂自由任务-文档", "帮我做一份掌上超声与进口竞品的横向对标分析报告,覆盖法规注册、工程架构与商务定价三个视角,要详尽成体系", 60, FormDocument, true},
{"复杂自由任务-数据", "给我整理一份竞品参数对比表和成本核算", 60, FormData, true},
}
for _, c := range cases {
p := ProfileTask(c.input, nil)
if p.ComplexityScore < c.minScore {
t.Errorf("[%s] score %d < %d (signals: %v)", c.name, p.ComplexityScore, c.minScore, p.ComplexitySignals)
}
if p.DeliverableForm != c.form {
t.Errorf("[%s] form %s != %s", c.name, p.DeliverableForm, c.form)
}
d := RouteIntent(intent.IntentCasualChat, p)
if d.UpgradeHint != c.upgrade {
t.Errorf("[%s] upgrade=%v, want %v (score=%d, form=%s)", c.name, d.UpgradeHint, c.upgrade, p.ComplexityScore, p.DeliverableForm)
}
}
}
// ============================================================================
// R0 确定性路由表零回归等价(默认即现状原则)
// ============================================================================
func TestRouteIntent_R0Equivalence(t *testing.T) {
// 标准任务/阶段推进 → 小队(现状)
if d := RouteIntent(intent.IntentTriggerTask, TaskProfile{}); d.Channel != ChannelSquadFixed {
t.Errorf("TRIGGER_TASK should stay SQUAD_FIXED, got %s", d.Channel)
}
if d := RouteIntent(intent.IntentAdvanceStage, TaskProfile{}); d.Channel != ChannelSquadFixed {
t.Errorf("ADVANCE_STAGE should stay SQUAD_FIXED, got %s", d.Channel)
}
if d := RouteIntent(intent.IntentTriggerAdhocTask, TaskProfile{}); d.Channel != ChannelSquadAdhoc {
t.Errorf("TRIGGER_ADHOC_TASK should route SQUAD_ADHOC, got %s", d.Channel)
}
// 查询/治理/补充/安全 → 模板(现状)
for _, it := range []intent.IntentType{
intent.IntentQueryProgress, intent.IntentQueryRegulations, intent.IntentQuerySimilarCases,
intent.IntentQueryUserRights, intent.IntentQueryRejectReason, intent.IntentSupplementInfo,
intent.IntentSecurityForbidden, intent.IntentDeclineExpectation,
intent.IntentDocGovClaim, intent.IntentDocGovSubmitReview, intent.IntentDocGovReviewApprove,
intent.IntentDocGovReviewReject, intent.IntentDocGovFinalApprove, intent.IntentDocGovFinalReject,
intent.IntentDocGovProposalDecide, intent.IntentDocGovCommentAdd, intent.IntentDocGovCommentResolve,
intent.IntentDocGovReopenRequest, intent.IntentDocGovReopenApprove, intent.IntentQueryPendingSupplements,
} {
if d := RouteIntent(it, TaskProfile{}); d.Channel != ChannelTemplate {
t.Errorf("%s should stay TEMPLATE, got %s", it, d.Channel)
}
}
// 低复杂度闲聊 → 对话,不升级(现状)
d := RouteIntent(intent.IntentCasualChat, TaskProfile{ComplexityScore: 10, DeliverableForm: FormAnswer})
if d.Channel != ChannelDialogue || d.UpgradeHint {
t.Errorf("simple casual chat should stay plain DIALOGUE, got %+v", d)
}
}
// ============================================================================
// 升级期望构造(复用 PendingExpectation,零新交互范式)
// ============================================================================
func TestBuildUpgradeExpectation(t *testing.T) {
p := ProfileTask("帮我做一份竞品对标分析报告,覆盖法规注册与定价策略", nil)
exp := buildUpgradeExpectation("帮我做一份竞品对标分析报告", p)
if exp.TargetAction != "launch_adhoc_task" {
t.Errorf("target action = %s", exp.TargetAction)
}
if exp.DefaultOption == nil || exp.DefaultOption.Intent != intent.IntentTriggerAdhocTask {
t.Errorf("default option should be TRIGGER_ADHOC_TASK")
}
if exp.DefaultOption.Slots[intent.SlotTaskBrief] == "" {
t.Errorf("default option should carry task brief slot")
}
if !exp.AllowAffirmative || exp.AffirmativeWord == "" {
t.Errorf("affirmative redemption should be allowed")
}
if len(exp.Options) != 3 {
t.Errorf("expected 3 options, got %d", len(exp.Options))
}
}
// ============================================================================
// 集成:模糊分支升级建议挂载 → 冷却 → 「好」一键兑现(零服务路径)
// ============================================================================
func TestUpgradeFlow_AttachCooldownAndRedeem(t *testing.T) {
engine := NewCoordinatorEngine(nil)
newCtx := func() *UserProjectContext {
return &UserProjectContext{
User: UserInfo{Name: "张工", ProjectRole: "composer"},
Project: ProjectInfo{Name: "掌上超声项目", CurrentStage: maturity.StageV01_Triage},
BlackboardFacts: map[string]string{},
}
}
complexInput := "帮我做一份掌上超声与进口竞品的横向对标分析报告,覆盖法规注册、工程架构与商务定价三个视角,要详尽成体系"
// 1. 模糊分支命中复杂画像 → 初答 + 升级建议挂载
rec := &intent.IntentRecognitionResult{
RawInput: complexInput,
Branch: intent.BranchAmbiguousFallback,
TopIntent: &intent.CandidateIntent{Intent: intent.IntentCasualChat, Confidence: 0.2, Branch: intent.BranchAmbiguousFallback},
}
res, err := engine.ProcessIntent(context.Background(), rec, newCtx())
if err != nil {
t.Fatalf("turn 1 failed: %v", err)
}
if res.NextExpectation == nil || res.NextExpectation.TargetAction != "launch_adhoc_task" {
t.Fatalf("expected upgrade expectation attached, got %+v", res.NextExpectation)
}
if !strings.Contains(res.NaturalReply, "升级") {
t.Errorf("reply should mention upgrade hint: %s", res.NaturalReply)
}
// 2. 冷却:同会话再次输入不再主动建议(独立上下文验证)
cooldownCtx := newCtx()
_, _ = engine.ProcessIntent(context.Background(), rec, cooldownCtx)
res2, err := engine.ProcessIntent(context.Background(), rec, cooldownCtx)
if err != nil {
t.Fatalf("turn 2 failed: %v", err)
}
if res2.NextExpectation != nil && res2.NextExpectation.TargetAction == "launch_adhoc_task" {
t.Errorf("upgrade hint should cool down after first offer")
}
// 3. 兑现(独立上下文):挂载升级建议后立即回复「好」→ 分支 1 命中 → TRIGGER_ADHOC_TASK
redeemCtx := newCtx()
if _, err := engine.ProcessIntent(context.Background(), rec, redeemCtx); err != nil {
t.Fatalf("mount failed: %v", err)
}
snap := redeemCtx.ToSnapshot()
recConfirm, err := intent.NewRecognizer().Recognize(context.Background(), "好", snap)
if err != nil {
t.Fatalf("recognize confirm failed: %v", err)
}
if recConfirm.Branch != intent.BranchExpectationHit {
t.Fatalf("expected EXPECTATION_HIT, got %s", recConfirm.Branch)
}
if recConfirm.TopIntent.Intent != intent.IntentTriggerAdhocTask {
t.Fatalf("expected TRIGGER_ADHOC_TASK, got %s", recConfirm.TopIntent.Intent)
}
resConfirm, err := engine.ProcessIntent(context.Background(), recConfirm, redeemCtx)
if err != nil {
t.Fatalf("redeem failed: %v", err)
}
if resConfirm.Status != StatusAccepted {
t.Errorf("expected ACCEPTED, got %s (%s)", resConfirm.Status, resConfirm.NaturalReply)
}
if resConfirm.Intent != intent.IntentTriggerAdhocTask {
t.Errorf("expected intent TRIGGER_ADHOC_TASK, got %s", resConfirm.Intent)
}
if !strings.Contains(resConfirm.NaturalReply, "已收到自由任务") {
t.Errorf("expected zero-service registration reply, got: %s", resConfirm.NaturalReply)
}
if !strings.Contains(resConfirm.NaturalReply, complexInput[:12]) {
t.Errorf("reply should echo task brief, got: %s", resConfirm.NaturalReply)
}
}
// ============================================================================
// 零回归:简单模糊输入不触发升级建议(现有行为逐例等价)
// ============================================================================
func TestUpgradeFlow_NoHintForSimpleInputs(t *testing.T) {
engine := NewCoordinatorEngine(nil)
for _, input := range []string{"这个先放放,你觉得呢", "好的知道了", "今天天气不错"} {
userCtx := &UserProjectContext{
User: UserInfo{Name: "张工", ProjectRole: "composer"},
Project: ProjectInfo{Name: "智能输注泵项目", CurrentStage: maturity.StageV01_Triage},
}
rec := &intent.IntentRecognitionResult{
RawInput: input,
Branch: intent.BranchAmbiguousFallback,
TopIntent: &intent.CandidateIntent{Intent: intent.IntentCasualChat, Confidence: 0.2, Branch: intent.BranchAmbiguousFallback},
}
res, err := engine.ProcessIntent(context.Background(), rec, userCtx)
if err != nil {
t.Fatalf("input %q failed: %v", input, err)
}
if res.NextExpectation != nil && res.NextExpectation.TargetAction == "launch_adhoc_task" {
t.Errorf("input %q should not trigger upgrade suggestion", input)
}
if strings.Contains(res.NaturalReply, "🚀") {
t.Errorf("input %q should not carry upgrade paragraph", input)
}
}
}