Files
Agent-System/web/src/engine/templateMatch.test.ts
T
weichen b820e0070d fix(taskrun): 刷新后恢复进行中任务并纠正进度问询
进行中的 run 提前写下消息锚点,并按会话列出活跃任务;进度疑问句改为查询真实状态,交付物已落盘且不是传输失败时不再被空结果否决。
2026-09-28 06:09:42 +00:00

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');
});