290 lines
8.9 KiB
TypeScript
290 lines
8.9 KiB
TypeScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {
|
|
findMatchingBlockForReview,
|
|
createRejectConfirmationCard,
|
|
isAffirmativeResponse,
|
|
findLastActiveCard,
|
|
decrementActiveCardsTtl,
|
|
applyConfirmRejectToDoc,
|
|
applyUndoRejectToDoc,
|
|
resolveCardInMessages,
|
|
cancelCardInMessages,
|
|
} from './cardActionEngine';
|
|
import { DocumentAST } from '../types/block';
|
|
import { ChatMessage } from '../types/workbench';
|
|
|
|
const sampleAST: DocumentAST = {
|
|
docId: 'doc-002',
|
|
title: '微创外科全球竞品深度调研',
|
|
taskKey: 'research',
|
|
version: 'V1.0',
|
|
lifecycleState: 'reviewing',
|
|
isClaimed: true,
|
|
author: '张工',
|
|
reviewer: '李主管',
|
|
approver: '王总监',
|
|
updatedAt: '2026-09-08 11:30',
|
|
rootBlock: {
|
|
id: 'root',
|
|
type: 'document',
|
|
content: '',
|
|
children: [
|
|
{
|
|
id: 'blk-h1',
|
|
type: 'heading',
|
|
level: 1,
|
|
numbering: '1',
|
|
content: '微创外科全球竞品深度调研',
|
|
},
|
|
{
|
|
id: 'blk-comp',
|
|
type: 'heading',
|
|
level: 2,
|
|
numbering: '1.1',
|
|
content: '全球主要竞品横向性能对标与临床痛点',
|
|
},
|
|
{
|
|
id: 'blk-emc',
|
|
type: 'paragraph',
|
|
content: '拟在第2节强化补充高频电刀共模抑制比设计指标,满足 GB 9706.1-2020 电气安全与 EMC 滤波要求。',
|
|
},
|
|
{
|
|
id: 'blk-fto',
|
|
type: 'heading',
|
|
level: 2,
|
|
numbering: '1.2',
|
|
content: '自由实施 (FTO) 专利检索与回避流程',
|
|
},
|
|
],
|
|
},
|
|
};
|
|
|
|
test('findMatchingBlockForReview: 能够精准按审查关键词(电气安全/EMC/竞品)定位段落块', () => {
|
|
const matched = findMatchingBlockForReview(sampleAST);
|
|
assert.ok(matched);
|
|
// 应当优先匹配包含竞品/电气安全/EMC 的段落或标题
|
|
assert.ok(
|
|
matched.content.includes('竞品') ||
|
|
matched.content.includes('电气安全') ||
|
|
matched.content.includes('EMC')
|
|
);
|
|
});
|
|
|
|
test('findMatchingBlockForReview: 能够按外部指定 hint 线索精确命中目标块', () => {
|
|
const matched = findMatchingBlockForReview(sampleAST, '自由实施');
|
|
assert.ok(matched);
|
|
assert.equal(matched.id, 'blk-fto');
|
|
assert.ok(matched.content.includes('自由实施'));
|
|
});
|
|
|
|
test('createRejectConfirmationCard: 生成合规危险级驳回门禁卡片', () => {
|
|
const block = sampleAST.rootBlock.children![2];
|
|
const card = createRejectConfirmationCard({ docId: 'doc-002', title: '微创外科调研' }, block);
|
|
|
|
assert.equal(card.cardType, 'CONFIRMATION');
|
|
assert.equal(card.targetDocId, 'doc-002');
|
|
assert.equal(card.status, 'active');
|
|
assert.equal(card.ttlTurns, 3);
|
|
assert.ok(card.actions.length >= 2);
|
|
|
|
const confirmAction = card.actions.find((a) => a.actionId === 'confirm_reject');
|
|
assert.ok(confirmAction);
|
|
assert.equal(confirmAction.style, 'danger');
|
|
assert.equal(confirmAction.isAffirmativeDefault, true);
|
|
assert.equal(confirmAction.payload?.docId, 'doc-002');
|
|
});
|
|
|
|
test('isAffirmativeResponse: 准确识别双轨交互中的肯定意图输入', () => {
|
|
assert.equal(isAffirmativeResponse('好'), true);
|
|
assert.equal(isAffirmativeResponse('好的'), true);
|
|
assert.equal(isAffirmativeResponse('确认'), true);
|
|
assert.equal(isAffirmativeResponse('确认驳回'), true);
|
|
assert.equal(isAffirmativeResponse('同意'), true);
|
|
assert.equal(isAffirmativeResponse('同意驳回'), true);
|
|
assert.equal(isAffirmativeResponse('批准'), true);
|
|
assert.equal(isAffirmativeResponse('ok'), true);
|
|
assert.equal(isAffirmativeResponse('YES'), true);
|
|
|
|
// 否定或无关意图
|
|
assert.equal(isAffirmativeResponse('取消'), false);
|
|
assert.equal(isAffirmativeResponse('不驳回'), false);
|
|
assert.equal(isAffirmativeResponse('请帮我修改第二段'), false);
|
|
});
|
|
|
|
test('findLastActiveCard: 正确在会话历史中提取最新的待决策卡片', () => {
|
|
const messages: ChatMessage[] = [
|
|
{
|
|
id: 'm1',
|
|
sessionId: 'sess-001',
|
|
role: 'assistant',
|
|
content: '旧消息已解决',
|
|
timestamp: '10:00',
|
|
interactiveCard: {
|
|
cardId: 'c1',
|
|
cardType: 'CONFIRMATION',
|
|
title: '旧卡片',
|
|
status: 'resolved',
|
|
ttlTurns: 0,
|
|
actions: [],
|
|
},
|
|
},
|
|
{
|
|
id: 'm2',
|
|
sessionId: 'sess-001',
|
|
role: 'assistant',
|
|
content: '当前待确认卡片',
|
|
timestamp: '10:05',
|
|
interactiveCard: {
|
|
cardId: 'c2',
|
|
cardType: 'CONFIRMATION',
|
|
title: '待驳回卡片',
|
|
status: 'active',
|
|
ttlTurns: 2,
|
|
actions: [{ actionId: 'confirm_reject', label: '确认', style: 'danger', isAffirmativeDefault: true }],
|
|
},
|
|
},
|
|
{
|
|
id: 'm3',
|
|
sessionId: 'sess-002', // 属于其他会话
|
|
role: 'assistant',
|
|
content: '其他会话活跃卡片',
|
|
timestamp: '10:06',
|
|
interactiveCard: {
|
|
cardId: 'c3',
|
|
cardType: 'CONFIRMATION',
|
|
title: '其他会话卡片',
|
|
status: 'active',
|
|
ttlTurns: 3,
|
|
actions: [],
|
|
},
|
|
},
|
|
];
|
|
|
|
const found = findLastActiveCard(messages, 'sess-001');
|
|
assert.ok(found);
|
|
assert.equal(found.card.cardId, 'c2');
|
|
assert.equal(found.card.status, 'active');
|
|
});
|
|
|
|
test('decrementActiveCardsTtl: 多轮对话超时递减与自动失效归档', () => {
|
|
const messages: ChatMessage[] = [
|
|
{
|
|
id: 'm1',
|
|
sessionId: 'sess-001',
|
|
role: 'assistant',
|
|
content: '待确认',
|
|
timestamp: '10:00',
|
|
interactiveCard: {
|
|
cardId: 'c1',
|
|
cardType: 'CONFIRMATION',
|
|
title: '剩余1轮卡片',
|
|
status: 'active',
|
|
ttlTurns: 1,
|
|
actions: [],
|
|
},
|
|
},
|
|
{
|
|
id: 'm2',
|
|
sessionId: 'sess-001',
|
|
role: 'assistant',
|
|
content: '待确认2',
|
|
timestamp: '10:01',
|
|
interactiveCard: {
|
|
cardId: 'c2',
|
|
cardType: 'CONFIRMATION',
|
|
title: '剩余3轮卡片',
|
|
status: 'active',
|
|
ttlTurns: 3,
|
|
actions: [],
|
|
},
|
|
},
|
|
];
|
|
|
|
const decremented = decrementActiveCardsTtl(messages, 'sess-001');
|
|
// c1 原为 1,递减后为 0,应归档为 expired
|
|
assert.equal(decremented[0].interactiveCard?.ttlTurns, 0);
|
|
assert.equal(decremented[0].interactiveCard?.status, 'expired');
|
|
|
|
// c2 原为 3,递减后为 2,仍为 active
|
|
assert.equal(decremented[1].interactiveCard?.ttlTurns, 2);
|
|
assert.equal(decremented[1].interactiveCard?.status, 'active');
|
|
});
|
|
|
|
test('applyConfirmRejectToDoc & applyUndoRejectToDoc: 驳回挂载批注与60s窗口期撤销完整闭环', () => {
|
|
const cardId = 'card-test-999';
|
|
const reviewer = '李主管';
|
|
const reason = '需按 GB 9706.1-2020 补充电气安全绝缘耐压参数';
|
|
|
|
// 1. 确认驳回
|
|
const { updatedDoc, matchingBlock } = applyConfirmRejectToDoc(
|
|
sampleAST,
|
|
cardId,
|
|
reviewer,
|
|
reason
|
|
);
|
|
|
|
// 状态流转验证:回退为 drafting
|
|
assert.equal(updatedDoc.lifecycleState, 'drafting');
|
|
|
|
// 批注挂载验证
|
|
const targetBlock = updatedDoc.rootBlock.children?.find((b) => b.id === matchingBlock.id);
|
|
assert.ok(targetBlock);
|
|
assert.ok(targetBlock.comments && targetBlock.comments.length > 0);
|
|
|
|
const comment = targetBlock.comments.find((c) => c.id === `cmt-reject-${cardId}`);
|
|
assert.ok(comment);
|
|
assert.equal(comment.authorName, reviewer);
|
|
assert.equal(comment.type, 'issue');
|
|
assert.equal(comment.content, reason);
|
|
assert.equal(comment.resolved, false);
|
|
|
|
// 2. 撤回驳回 (Undo)
|
|
const undoneDoc = applyUndoRejectToDoc(updatedDoc, cardId);
|
|
|
|
// 状态流转验证:恢复为 reviewing
|
|
assert.equal(undoneDoc.lifecycleState, 'reviewing');
|
|
|
|
// 批注清理验证:对应 comment 已安全移除
|
|
const targetBlockAfterUndo = undoneDoc.rootBlock.children?.find((b) => b.id === matchingBlock.id);
|
|
const remainingComment = targetBlockAfterUndo?.comments?.find((c) => c.id === `cmt-reject-${cardId}`);
|
|
assert.equal(remainingComment, undefined);
|
|
});
|
|
|
|
test('resolveCardInMessages & cancelCardInMessages: 消息流中卡片终态沉淀', () => {
|
|
const messages: ChatMessage[] = [
|
|
{
|
|
id: 'm1',
|
|
sessionId: 'sess-001',
|
|
role: 'assistant',
|
|
content: '卡片消息',
|
|
timestamp: '10:00',
|
|
interactiveCard: {
|
|
cardId: 'c1',
|
|
cardType: 'CONFIRMATION',
|
|
title: '审核确认',
|
|
status: 'active',
|
|
ttlTurns: 3,
|
|
actions: [],
|
|
},
|
|
},
|
|
];
|
|
|
|
// 1. Resolve
|
|
const resolved = resolveCardInMessages(
|
|
messages,
|
|
'c1',
|
|
'已正式退回编制阶段,批注已挂载至第3节',
|
|
'李主管',
|
|
'2026-09-08T18:00:00.000Z'
|
|
);
|
|
assert.equal(resolved[0].interactiveCard?.status, 'resolved');
|
|
assert.equal(resolved[0].interactiveCard?.resolutionSummary, '已正式退回编制阶段,批注已挂载至第3节');
|
|
assert.equal(resolved[0].interactiveCard?.resolvedBy, '李主管');
|
|
assert.equal(resolved[0].interactiveCard?.resolvedAt, '2026-09-08T18:00:00.000Z');
|
|
|
|
// 2. Cancel
|
|
const cancelled = cancelCardInMessages(messages, 'c1');
|
|
assert.equal(cancelled[0].interactiveCard?.status, 'cancelled');
|
|
});
|