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