Files
Agent-System/web/src/engine/cardActionEngine.test.ts
T

292 lines
9.1 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!.label, '确认驳回退回编制');
assert.equal(confirmAction!.style, 'danger');
assert.equal(confirmAction!.isAffirmativeDefault, true);
assert.equal(confirmAction!.payload?.docId, 'doc-002');
assert.equal(card.actions.find((a) => a.actionId === 'cancel')?.label, '放弃驳回');
});
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');
});