Files
Agent-System/web/src/engine/templateMatch.test.ts
T
weichenandCursor 56e052f3cb fix(web): prefer named deliverable over negated 需求
Explicit aliases (including 系统设计输入) must win before keyword fallback so UI-11 does not route to intake.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-13 18:42:44 +00:00

226 lines
9.3 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import {
consistentTaskKeyForControlledDoc,
hasDocumentWriteIntent,
isServerRegisteredControlledDoc,
matchTemplateDeliverable,
refersToCurrentControlledDocument,
resolveRequirementsTaskKey,
resolveTaskKeyFromMessage,
resolveTaskTemplateForMessage,
} from './templateMatch';
import { DeliverableItem } from '../types/workbench';
const base: Omit<DeliverableItem, 'taskKey' | 'title'> = {
docId: 'doc-1',
projectId: 143,
stage: 'V0.1',
category: 'workflow',
creatorId: 9,
creatorName: 'admin',
isFixedWorkflow: true,
isTemplateOnly: true,
lifecycleState: 'drafting',
isClaimed: false,
author: '',
reviewer: '',
approver: '',
updatedAt: '',
};
const ursOnlyPool: DeliverableItem[] = [
{ ...base, docId: 'doc-urs', taskKey: 'user_requirements', title: '用户需求表', stage: 'V0.5' },
];
const bothPool: DeliverableItem[] = [
{ ...base, docId: 'doc-urs', taskKey: 'user_requirements', title: '用户需求表', stage: 'V0.5' },
{ ...base, docId: 'doc-intake', taskKey: 'intake', title: '用户初始需求表', stage: 'V0.1' },
];
test('resolveRequirementsTaskKey prefers intake for 初始需求表', () => {
assert.equal(
resolveRequirementsTaskKey('根据上传的文档,开始编写用户初始需求表'),
'intake',
);
});
test('resolveRequirementsTaskKey selects user_requirements for explicit URS', () => {
assert.equal(resolveRequirementsTaskKey('请编制用户需求表 URS'), 'user_requirements');
});
const UI02_FIRST_ROUND_PROMPT =
'请基于本项目编制首轮《用户需求》摸底文档,覆盖预期用途、关键性能与适用法规框架,直接生成受控交付物。';
// 与 E2E UI-05 原文对齐,仅供单测;不得放入生产模块。
const CURRENT_DOC_HIGH_RISK_REWRITE_PROMPT =
'请按 YY/T、GB 9706.1 与 NMPA 注册法规要求,大幅改写当前受控文档中的风险管理与电气安全章节,引用具体条款并重写适用性结论(二轮高风险候选-确认用例)。';
test('UI-02 first-round prompt with 首轮+用户需求+摸底 must be intake not user_requirements', () => {
assert.equal(resolveRequirementsTaskKey(UI02_FIRST_ROUND_PROMPT), 'intake');
assert.equal(resolveTaskTemplateForMessage(UI02_FIRST_ROUND_PROMPT, bothPool)?.taskKey, 'intake');
});
test('首轮 / 需求摸底 / 摸底 beat bare 用户需求', () => {
assert.equal(resolveRequirementsTaskKey('请编制首轮用户需求文档'), 'intake');
assert.equal(resolveRequirementsTaskKey('进入需求摸底,覆盖用户需求要点'), 'intake');
assert.equal(resolveRequirementsTaskKey('生成本轮摸底文档(含用户需求)'), 'intake');
});
test('bare 用户需求 without V0.1 markers stays user_requirements', () => {
assert.equal(resolveRequirementsTaskKey('请更新用户需求规格'), 'user_requirements');
});
test('matchTemplateDeliverable uses intake key when pool only has URS template', () => {
const hit = matchTemplateDeliverable('开始编写用户初始需求表', ursOnlyPool);
assert.equal(hit?.taskKey, 'intake');
});
test('resolveTaskTemplateForMessage keeps intake key with URS-only pool', () => {
const resolved = resolveTaskTemplateForMessage('开始编写用户初始需求表', ursOnlyPool);
assert.equal(resolved?.taskKey, 'intake');
});
test('matchTemplateDeliverable prefers intake when both templates exist', () => {
const hit = matchTemplateDeliverable('根据上传的文档,开始编写用户初始需求表', bothPool);
assert.equal(hit?.taskKey, 'intake');
});
for (const query of [
'总结一下用户初始需求表',
'查看用户初始需求表',
'用户初始需求表现在谁在编辑',
'分析一下用户需求表里的风险',
'用户初始需求表',
]) {
test(`read-only request does not start a document task: ${query}`, () => {
assert.equal(resolveTaskTemplateForMessage(query, bothPool), undefined);
});
}
test('explicit write action still starts intake', () => {
assert.equal(resolveTaskTemplateForMessage('请开始编写用户初始需求表', bothPool)?.taskKey, 'intake');
});
test('explicit update action still starts URS', () => {
assert.equal(resolveTaskTemplateForMessage('请更新用户需求表 URS', bothPool)?.taskKey, 'user_requirements');
});
const activeIntake: DeliverableItem = {
...base,
docId: 'doc_proj6109_intake',
taskKey: 'intake',
title: '用户初始需求收集表',
isTemplateOnly: false,
};
const activeReg: DeliverableItem = {
...base,
docId: 'doc_proj6109_feasibility_reg',
taskKey: 'feasibility_reg',
title: '注册路径可行性评估报告',
isTemplateOnly: false,
};
const templateIntakeSlot: DeliverableItem = {
...base,
docId: 'doc-p6109-intake',
taskKey: 'intake',
title: '用户初始需求收集表',
isTemplateOnly: true,
};
test('UI-05 prompt without active doc still keyword-routes to feasibility_reg', () => {
assert.equal(resolveTaskKeyFromMessage(CURRENT_DOC_HIGH_RISK_REWRITE_PROMPT), 'feasibility_reg');
assert.equal(resolveTaskTemplateForMessage(CURRENT_DOC_HIGH_RISK_REWRITE_PROMPT, bothPool)?.taskKey, 'feasibility_reg');
});
test('UI-05 prompt + registered intake pins intake and doc_proj6109_intake', () => {
assert.equal(refersToCurrentControlledDocument(CURRENT_DOC_HIGH_RISK_REWRITE_PROMPT), true);
assert.equal(isServerRegisteredControlledDoc(activeIntake), true);
const resolved = resolveTaskTemplateForMessage(CURRENT_DOC_HIGH_RISK_REWRITE_PROMPT, bothPool, activeIntake);
assert.equal(resolved?.taskKey, 'intake');
assert.equal(resolved?.docId, 'doc_proj6109_intake');
assert.equal(resolved?.pinnedToActiveDoc, true);
});
test('read-only current-doc request does not start a task', () => {
assert.equal(hasDocumentWriteIntent('查看当前受控文档的风险管理章节'), false);
assert.equal(
resolveTaskTemplateForMessage('查看当前受控文档的风险管理章节', bothPool, activeIntake),
undefined,
);
});
test('template-only active slot is not pinned; keywords still apply', () => {
assert.equal(isServerRegisteredControlledDoc(templateIntakeSlot), false);
const resolved = resolveTaskTemplateForMessage(
CURRENT_DOC_HIGH_RISK_REWRITE_PROMPT,
bothPool,
templateIntakeSlot,
);
assert.equal(resolved?.taskKey, 'feasibility_reg');
assert.equal(resolved?.pinnedToActiveDoc, undefined);
});
test('explicit named deliverable can switch away from active intake', () => {
const text = '请改写当前受控文档,同时编制注册路径可行性评估报告';
const resolved = resolveTaskTemplateForMessage(text, [...bothPool, activeReg], activeIntake);
assert.equal(resolved?.taskKey, 'feasibility_reg');
assert.notEqual(resolved?.docId, 'doc_proj6109_intake');
});
test('domain 注册法规 without current-doc deixis still routes feasibility_reg', () => {
assert.equal(
resolveTaskTemplateForMessage('请编制注册路径可行性评估并引用 NMPA 法规', bothPool, activeIntake)
?.taskKey,
'feasibility_reg',
);
});
test('mismatched title vs docId suffix prefers docId taskKey', () => {
const mismatch = { ...activeIntake, taskKey: 'feasibility_reg' };
assert.equal(consistentTaskKeyForControlledDoc(mismatch), 'intake');
const resolved = resolveTaskTemplateForMessage(CURRENT_DOC_HIGH_RISK_REWRITE_PROMPT, bothPool, mismatch);
assert.equal(resolved?.taskKey, 'intake');
assert.equal(resolved?.docId, 'doc_proj6109_intake');
});
for (const phrase of ['当前文档', '本文件', '这份文档', '本文档', '本受控文档'] as const) {
test(`deixis ${phrase} pins registered intake`, () => {
const text = `请改写${phrase}中的风险管理章节`;
assert.equal(refersToCurrentControlledDocument(text), true);
const resolved = resolveTaskTemplateForMessage(text, bothPool, activeIntake);
assert.equal(resolved?.taskKey, 'intake');
assert.equal(resolved?.docId, 'doc_proj6109_intake');
assert.equal(resolved?.pinnedToActiveDoc, true);
});
}
test('no active doc does not invent a pin', () => {
const resolved = resolveTaskTemplateForMessage(CURRENT_DOC_HIGH_RISK_REWRITE_PROMPT, bothPool, undefined);
assert.equal(resolved?.taskKey, 'feasibility_reg');
assert.equal(resolved?.docId, undefined);
assert.equal(resolved?.pinnedToActiveDoc, undefined);
});
const UI11_SECOND_DOC_PROMPT =
'请另行生成一份《系统设计输入》受控文档(第二份),不要覆盖上一份需求文档,并给出可打开的交付物。';
test('UI-11 named 系统设计输入 beats negated 需求 background', () => {
assert.equal(resolveTaskKeyFromMessage(UI11_SECOND_DOC_PROMPT), 'prelim_design');
assert.equal(resolveTaskTemplateForMessage(UI11_SECOND_DOC_PROMPT, bothPool, activeIntake)?.taskKey, 'prelim_design');
});
test('mixed generate-design + do-not-cover-requirements stays prelim_design', () => {
const text = '请生成《系统设计输入》,不要覆盖上一份需求文档';
assert.equal(resolveTaskKeyFromMessage(text), 'prelim_design');
});
test('current-doc pin still wins when no other deliverable is named', () => {
const text = '请改写当前受控文档中的风险管理章节,不要覆盖上一份需求文档';
const resolved = resolveTaskTemplateForMessage(text, bothPool, activeIntake);
assert.equal(resolved?.taskKey, 'intake');
assert.equal(resolved?.pinnedToActiveDoc, true);
});