import test from 'node:test'; import assert from 'node:assert/strict'; import { applyPhaseEvent, PHASE_META } from './phaseEventMapper'; test('applyPhaseEvent: appends first phase as running node', () => { const strip = applyPhaseEvent([], { phase_id: 'squad_assembly', message: '组建小队' }); assert.equal(strip.length, 1); assert.equal(strip[0].step, 'squad_assembly'); assert.equal(strip[0].icon, '👥'); assert.equal(strip[0].status, 'running'); }); test('applyPhaseEvent: completes predecessors when a new phase arrives', () => { let strip = applyPhaseEvent([], { phase_id: 'squad_assembly', message: 'a' }); strip = applyPhaseEvent(strip, { phase_id: 'knowledge_retrieval', message: 'b' }); assert.equal(strip.length, 2); assert.equal(strip[0].status, 'completed'); assert.equal(strip[1].status, 'running'); }); test('applyPhaseEvent: same phase refreshes label in place (no duplicate node)', () => { let strip = applyPhaseEvent([], { phase_id: 'quality_gate', message: 'x', progress: 85 }); strip = applyPhaseEvent(strip, { phase_id: 'quality_gate', message: 'y', progress: 95 }); assert.equal(strip.length, 1); assert.match(strip[0].label, /95%/); }); test('applyPhaseEvent: unknown phase falls back to hourglass meta', () => { const strip = applyPhaseEvent([], { phase_id: 'mystery_phase' }); assert.equal(strip[0].icon, '⏳'); assert.equal(strip[0].label, '任务推进'); }); test('applyPhaseEvent: prefers backend message over short label', () => { const strip = applyPhaseEvent([], { phase_id: 'squad_assembly', message: '正在组建真实小队...' }); assert.equal(strip[0].icon, '👥'); assert.equal(strip[0].label, '正在组建真实小队...'); }); test('applyPhaseEvent: step_completed closes running entry without appending', () => { let strip = applyPhaseEvent([], { phase_id: 'squad_assembly', message: 'a' }); strip = applyPhaseEvent(strip, { phase_id: 'step_executing', message: '撰写中' }); strip = applyPhaseEvent(strip, { phase_id: 'step_completed', message: '完成' }); assert.equal(strip.length, 2); assert.equal(strip[0].status, 'completed'); assert.equal(strip[1].status, 'completed'); // 收口后 label 归一化为静态措辞,不再冻结"撰写中"进度中文案;原始叙述保留在 detail。 assert.equal(strip[1].label, '专员执行已完成'); assert.equal(strip[0].label, '组建小队已完成'); assert.equal(strip[1].detail, '撰写中'); }); test('applyPhaseEvent: stage_completed closes entries', () => { let strip = applyPhaseEvent([], { phase_id: 'stage_started', message: 's' }); strip = applyPhaseEvent(strip, { phase_id: 'stage_completed', message: 'done' }); assert.equal(strip.length, 1); assert.equal(strip[0].status, 'completed'); assert.equal(strip[0].label, '阶段启动已完成'); }); test('applyPhaseEvent: task_failed appends warning node', () => { let strip = applyPhaseEvent([], { phase_id: 'step_executing', message: 'e' }); strip = applyPhaseEvent(strip, { phase_id: 'task_failed', message: 'boom' }); assert.equal(strip.length, 2); assert.equal(strip[0].status, 'completed'); assert.equal(strip[1].step, 'task_failed'); assert.equal(strip[1].icon, '⚠️'); assert.equal(strip[1].status, 'running'); }); test('applyPhaseEvent: decorates label with step index / total steps', () => { const strip = applyPhaseEvent([], { phase_id: 'step_executing', message: 'm', step_index: 2, total_steps: 3, }); assert.match(strip[0].label, /第 2\/3 步/); }); test('applyPhaseEvent: repeated step_executing with step_index gets separate nodes', () => { let strip = applyPhaseEvent([], { phase_id: 'step_executing', message: '一', step_index: 1, total_steps: 3 }); strip = applyPhaseEvent(strip, { phase_id: 'step_executing', message: '二', step_index: 2, total_steps: 3 }); assert.equal(strip.length, 2); assert.equal(strip[0].status, 'completed'); assert.match(strip[0].label, /第 1\/3 步/); assert.equal(strip[1].status, 'running'); assert.match(strip[1].label, /第 2\/3 步/); }); test('applyPhaseEvent: ignores event without phase payload', () => { const strip = applyPhaseEvent([], undefined); assert.equal(strip.length, 0); }); test('applyPhaseEvent: step_failed marks running entry as failed (keeps step label)', () => { let strip = applyPhaseEvent([], { phase_id: 'step_executing', message: '撰写专员撰写初稿中...' }); strip = applyPhaseEvent(strip, { phase_id: 'step_failed', message: 'pi remote runner call failed: connection refused' }); assert.equal(strip.length, 1); assert.equal(strip[0].status, 'failed'); // 失败节点保留原步骤文案,错误细节由错误气泡承载 assert.match(strip[0].label, /撰写专员撰写初稿中/); }); test('applyPhaseEvent: step_failed appends failed node when no running entry exists', () => { const strip = applyPhaseEvent( [{ step: 'squad_assembly', icon: '👥', label: '组建小队', status: 'completed' }], { phase_id: 'step_failed', message: '执行未成功' } ); assert.equal(strip.length, 2); assert.equal(strip[1].step, 'step_failed'); assert.equal(strip[1].icon, '⚠️'); assert.equal(strip[1].status, 'failed'); assert.equal(strip[1].label, '执行未成功'); }); test('applyPhaseEvent: retry of a failed step resets it to running', () => { let strip = applyPhaseEvent([], { phase_id: 'step_executing', message: '撰写初稿中' }); strip = applyPhaseEvent(strip, { phase_id: 'step_failed', message: '执行未成功' }); strip = applyPhaseEvent(strip, { phase_id: 'step_executing', message: '重新撰写初稿中' }); assert.equal(strip.length, 1); assert.equal(strip[0].status, 'running'); assert.equal(strip[0].label, '重新撰写初稿中'); }); test('applyPhaseEvent: stores detail from phase.message', () => { const strip = applyPhaseEvent([], { phase_id: 'squad_assembly', message: '正在组建真实小队...' }); assert.equal(strip[0].detail, '正在组建真实小队...'); }); test('PHASE_META: document_parsing and document_summarizing are registered', () => { assert.equal(PHASE_META.document_parsing.icon, '📄'); assert.equal(PHASE_META.document_parsing.label, '文档解析'); assert.equal(PHASE_META.document_summarizing.icon, '📝'); assert.equal(PHASE_META.document_summarizing.label, '正文总结'); const strip = applyPhaseEvent([], { phase_id: 'document_parsing', message: '《说明书》正在解析中…已等待 4s', progress: 20, }); assert.equal(strip[0].icon, '📄'); assert.match(strip[0].label, /解析中/); });