Explicit aliases (including 系统设计输入) must win before keyword fallback so UI-11 does not route to intake. Co-authored-by: Cursor <cursoragent@cursor.com>
226 lines
9.3 KiB
TypeScript
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);
|
|
});
|