368 lines
14 KiB
TypeScript
368 lines
14 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import {
|
|
consistentTaskKeyForControlledDoc,
|
|
hasDocumentWriteIntent,
|
|
isServerRegisteredControlledDoc,
|
|
matchTemplateDeliverable,
|
|
refersToCurrentControlledDocument,
|
|
resolveRequirementsTaskKey,
|
|
resolveTaskKeyFromMessage,
|
|
resolveTaskTemplateForMessage,
|
|
normalizeTaskKeyToProject,
|
|
} 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('summary/analysis with write verbs stays chat (read-only priority)', () => {
|
|
const phrases = [
|
|
'请总结并生成要点',
|
|
'分析一下这份附件然后编写摘要',
|
|
'概括文档内容并更新说明',
|
|
'查看附件后修改建议列表',
|
|
];
|
|
for (const text of phrases) {
|
|
assert.equal(hasDocumentWriteIntent(text), false, text);
|
|
assert.equal(resolveTaskTemplateForMessage(text, bothPool, activeIntake), undefined, text);
|
|
}
|
|
});
|
|
|
|
test('pure write intent still routes to task', () => {
|
|
assert.equal(hasDocumentWriteIntent('请编制用户初始需求收集表'), true);
|
|
});
|
|
|
|
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);
|
|
});
|
|
|
|
test('v3 bd_ task key wins over unprefixed intake alias', () => {
|
|
const bdIntake: DeliverableItem = {
|
|
...base,
|
|
docId: 'doc_proj6579_bd_intake',
|
|
projectId: 6579,
|
|
taskKey: 'bd_intake',
|
|
title: '用户初始需求收集表',
|
|
isTemplateOnly: true,
|
|
};
|
|
assert.equal(normalizeTaskKeyToProject('intake', [bdIntake]), 'bd_intake');
|
|
assert.equal(normalizeTaskKeyToProject('intake', [bdIntake, { ...bdIntake, taskKey: 'intake', docId: 'doc-p6579-intake' }]), 'intake');
|
|
const resolved = resolveTaskTemplateForMessage('开始编写用户初始需求表', [bdIntake]);
|
|
assert.equal(resolved?.taskKey, 'bd_intake');
|
|
});
|
|
|
|
test('word-order variants of the intake sheet stay on intake, not the shorter URS title', () => {
|
|
const bdIntake: DeliverableItem = {
|
|
...base,
|
|
docId: 'doc_proj6579_bd_intake',
|
|
projectId: 6579,
|
|
taskKey: 'bd_intake',
|
|
title: '用户初始需求收集表',
|
|
isTemplateOnly: true,
|
|
};
|
|
const bdUrs: DeliverableItem = {
|
|
...base,
|
|
docId: 'doc_proj6579_bd_user_requirements',
|
|
projectId: 6579,
|
|
taskKey: 'bd_user_requirements',
|
|
title: '用户需求表',
|
|
stage: 'V0.5',
|
|
isTemplateOnly: true,
|
|
};
|
|
const pool = [bdIntake, bdUrs];
|
|
for (const query of ['编写初始用户需求表', '生成需求表', '编写用户初始需求收集表']) {
|
|
assert.equal(resolveTaskKeyFromMessage(query), 'intake', query);
|
|
assert.equal(resolveTaskTemplateForMessage(query, pool)?.taskKey, 'bd_intake', query);
|
|
assert.equal(resolveTaskTemplateForMessage(query, bothPool)?.taskKey, 'intake', query);
|
|
}
|
|
assert.equal(resolveTaskTemplateForMessage('请更新用户需求表 URS', pool)?.taskKey, 'bd_user_requirements');
|
|
});
|
|
|
|
test('resolveTaskTemplateForMessage blocks cross-workflow document mentions', () => {
|
|
const mainResearch: DeliverableItem = {
|
|
...base,
|
|
taskKey: 'bd_research',
|
|
title: '调研报告',
|
|
workflowKey: 'main',
|
|
};
|
|
const rdevProposal: DeliverableItem = {
|
|
...base,
|
|
taskKey: 'rd_proposal',
|
|
title: '项目建议书',
|
|
workflowKey: 'rdev',
|
|
isTemplateOnly: true,
|
|
};
|
|
const pool = [mainResearch, rdevProposal];
|
|
|
|
const blocked = resolveTaskTemplateForMessage('编写项目建议书', pool, null, 'main');
|
|
assert.ok(blocked?.wrongWorkflow);
|
|
assert.equal(blocked?.wrongWorkflow?.ownerWorkflow, 'rdev');
|
|
assert.equal(blocked?.taskKey, '');
|
|
|
|
const allowed = resolveTaskTemplateForMessage('编写项目建议书', pool, null, 'rdev');
|
|
assert.equal(allowed?.wrongWorkflow, undefined);
|
|
assert.equal(allowed?.taskKey, 'rd_proposal');
|
|
|
|
const legacy = resolveTaskTemplateForMessage('编写项目建议书', pool, null, undefined);
|
|
assert.equal(legacy?.wrongWorkflow, undefined);
|
|
assert.equal(legacy?.taskKey, 'rd_proposal');
|
|
});
|
|
|
|
test('resolveTaskTemplateForMessage keeps in-scope main docs when active is main', () => {
|
|
const research: DeliverableItem = {
|
|
...base,
|
|
taskKey: 'research',
|
|
title: '调研报告',
|
|
workflowKey: 'main',
|
|
isTemplateOnly: true,
|
|
};
|
|
const resolved = resolveTaskTemplateForMessage('生成调研报告', [research], null, 'main');
|
|
assert.equal(resolved?.wrongWorkflow, undefined);
|
|
assert.equal(resolved?.taskKey, 'research');
|
|
});
|
|
|
|
test('weighted mode does not let a cross-workflow title hijack an in-scope write', () => {
|
|
const research: DeliverableItem = {
|
|
...base,
|
|
taskKey: 'bd_research',
|
|
title: '调研报告',
|
|
workflowKey: 'main',
|
|
isTemplateOnly: true,
|
|
};
|
|
const proposal: DeliverableItem = {
|
|
...base,
|
|
taskKey: 'rd_proposal',
|
|
title: '项目建议书',
|
|
workflowKey: 'rdev',
|
|
isTemplateOnly: true,
|
|
};
|
|
const pool = [research, proposal];
|
|
const kept = resolveTaskTemplateForMessage(
|
|
'生成调研报告,不要写成项目建议书',
|
|
pool,
|
|
null,
|
|
'main',
|
|
'weighted',
|
|
);
|
|
assert.equal(kept?.wrongWorkflow, undefined);
|
|
assert.equal(kept?.taskKey, 'bd_research');
|
|
|
|
const blocked = resolveTaskTemplateForMessage('编写项目建议书', pool, null, 'main', 'weighted');
|
|
assert.equal(blocked?.wrongWorkflow?.ownerWorkflow, 'rdev');
|
|
|
|
const fuzzy = resolveTaskTemplateForMessage('编写建议', pool, null, 'main', 'weighted');
|
|
assert.equal(fuzzy?.wrongWorkflow, undefined);
|
|
});
|
|
|
|
test('progress question is read-only; explicit start still resolves research', () => {
|
|
assert.equal(hasDocumentWriteIntent('调研报告编写进度怎么样了'), false);
|
|
assert.equal(resolveTaskKeyFromMessage('调研报告编写进度怎么样了'), undefined);
|
|
assert.equal(resolveTaskKeyFromMessage('开始做调研报告'), 'research');
|
|
});
|