import { buildSubagentProjectionForEntry, projectSubagentEntries, } from '../src/modules/netaclaw/session-tree/subagent_projection.js'; import type { SessionTreeEntry } from '../src/modules/netaclaw/session-tree/types.js'; describe('session tree subagent projection', () => { it('projects subagent_result entries into canonical panels, evidence, and process events', () => { const entry: SessionTreeEntry = { id: 'entry-result', parentId: 'parent', timestamp: '2026-04-22T10:00:00.000Z', type: 'subagent_result', content: { batchId: 'batch-1', status: 'completed', parentEntryId: 'parent', results: [ { id: 11, sortOrder: 0, status: 'completed', goal: '检查桌面图片', summary: '共有 3 张图片', resultPayload: { evidenceSummary: { kind: 'file-count', title: '图片文件统计', summary: '根据工具结果统计,图片文件共 3 个。', files: ['111.jpg', '11123.jpg', '20260417-185140.jpg'], count: 3, }, processEvents: [ { type: 'run_start', runId: 'subagent-11', timestamp: '2026-04-22T10:00:01.000Z', }, { type: 'tool_call', runId: 'subagent-11', timestamp: '2026-04-22T10:00:02.000Z', toolCallId: 'call-images', name: 'bash', label: '执行命令', args: { command: 'find ~/Desktop -maxdepth 1 -type f -iname "*.jpg"' }, }, { type: 'tool_result', runId: 'subagent-11', timestamp: '2026-04-22T10:00:03.000Z', toolCallId: 'call-images', name: 'bash', label: '执行命令', result: '111.jpg\n11123.jpg\n20260417-185140.jpg', }, ], }, }, ], }, metadata: {}, }; const projection = buildSubagentProjectionForEntry(entry); expect(projection).toBeTruthy(); expect(projection).toMatchObject({ version: 1, source: 'backend-projection', diagnostics: { projectionPath: 'canonical-backend-projection', selectedSource: 'subagent_result', inputSources: ['subagent_result'], rawResultCount: 1, dedupedResultCount: 1, projectedTaskCount: 1, skippedResultCount: 0, fallbackUsed: false, }, }); expect(projection?.taskPanels).toHaveLength(1); expect(projection?.taskPanels[0]).toMatchObject({ key: '11', title: '检查桌面图片', status: 'completed', sortOrder: 0, summary: '共有 3 张图片', }); expect(projection?.taskPanels[0].evidenceSummaries).toHaveLength(1); expect(projection?.taskPanels[0].toolExecutions).toHaveLength(1); expect(projection?.taskPanels[0].toolExecutions[0]).toMatchObject({ toolCallId: 'call-images', status: 'completed', result: '111.jpg\n11123.jpg\n20260417-185140.jpg', }); expect(projection?.evidenceSummaries).toHaveLength(1); expect(projection?.processEvents).toHaveLength(3); }); it('prefers persisted subagent_result metadata.finalResults when present', () => { const entry: SessionTreeEntry = { id: 'entry-result-metadata-final-results', parentId: 'parent', timestamp: '2026-04-22T10:30:00.000Z', type: 'subagent_result', content: { batchId: 'batch-1', status: 'completed', parentEntryId: 'parent', results: [ { id: 11, sortOrder: 0, status: 'completed', goal: '旧 content 任务', summary: '来自 content.results', resultPayload: {}, }, ], }, metadata: { finalResults: [ { id: 22, sortOrder: 1, status: 'completed', goal: '正式 metadata 任务', summary: '来自 metadata.finalResults', resultPayload: {}, }, ], }, }; const projection = buildSubagentProjectionForEntry(entry); expect(projection?.taskPanels).toHaveLength(1); expect(projection?.taskPanels[0]).toMatchObject({ key: '22', title: '正式 metadata 任务', summary: '来自 metadata.finalResults', sortOrder: 1, }); expect(projection?.diagnostics).toMatchObject({ projectionPath: 'canonical-backend-projection', selectedSource: 'subagent_result', rawResultCount: 1, dedupedResultCount: 1, projectedTaskCount: 1, fallbackUsed: false, }); }); it('reads subagent_result top-level metadata evidence and process events through the canonical metadata path', () => { const entry: SessionTreeEntry = { id: 'entry-result-metadata-replay', parentId: 'parent', timestamp: '2026-04-22T10:40:00.000Z', type: 'subagent_result', content: { batchId: 'batch-metadata-replay', status: 'completed', parentEntryId: 'parent', results: [], }, metadata: { finalResults: [ { id: 51, sortOrder: 0, status: 'completed', goal: 'metadata replay task', summary: 'metadata result summary', resultPayload: {}, }, ], evidenceSummaries: [ { kind: 'tool-preview', title: 'Metadata Evidence', summary: 'metadata evidence summary', }, ], processEvents: [ { type: 'run_start', runId: 'subagent-51', timestamp: '2026-04-22T10:40:01.000Z', }, ], }, }; const projection = buildSubagentProjectionForEntry(entry); expect(projection?.taskPanels).toHaveLength(1); expect(projection?.taskPanels[0]).toMatchObject({ key: '51', title: 'metadata replay task', summary: 'metadata result summary', }); expect(projection?.evidenceSummaries).toEqual([ expect.objectContaining({ title: 'Metadata Evidence', summary: 'metadata evidence summary', }), ]); expect(projection?.processEvents).toEqual([ expect.objectContaining({ type: 'run_start', runId: 'subagent-51', }), ]); }); it('projects assistant delegate_parallel metadata and ignores stale entry metadata when message metadata exists', () => { const freshPayload = { delegationMode: 'session-subagent', taskResults: [ { task: { sortOrder: 0, goal: 'message metadata task' }, result: { id: 21, status: 'completed', summary: 'from message metadata', resultPayload: {}, }, }, ], }; const stalePayload = { delegationMode: 'session-subagent', taskResults: [ { task: { sortOrder: 1, goal: 'entry metadata task' }, result: { id: 22, status: 'completed', summary: 'from entry metadata', resultPayload: {}, }, }, ], }; const entry: SessionTreeEntry = { id: 'entry-message', parentId: 'parent', timestamp: '2026-04-22T11:00:00.000Z', type: 'message', message: { role: 'assistant', content: 'done', metadata: { skillExecutions: [ { name: 'delegate_parallel', result: freshPayload, }, ], }, }, metadata: { skillExecutions: [ { name: 'delegate_parallel', result: stalePayload, }, ], }, }; const projection = buildSubagentProjectionForEntry(entry); expect(projection?.taskPanels).toHaveLength(1); expect(projection?.taskPanels[0]).toMatchObject({ key: '21', title: 'message metadata task', summary: 'from message metadata', }); expect(projection?.diagnostics).toMatchObject({ projectionPath: 'canonical-backend-projection', selectedSource: 'message_metadata', inputSources: ['message_metadata', 'entry_metadata'], rawResultCount: 1, dedupedResultCount: 1, projectedTaskCount: 1, skippedResultCount: 0, fallbackUsed: false, ignoredSources: ['entry_metadata'], }); }); it('exposes projection diagnostics for fallback and dedupe decisions', () => { const payload = { delegationMode: 'session-subagent', taskResults: [ { task: { sortOrder: 0, goal: 'fallback task' }, result: { id: 41, status: 'completed', summary: 'first', resultPayload: {}, }, }, { task: { sortOrder: 0, goal: 'fallback task duplicate' }, result: { id: 41, status: 'completed', summary: 'duplicate', resultPayload: {}, }, }, { task: { sortOrder: 2, goal: 'invalid task' }, result: null, }, ], }; const entry: SessionTreeEntry = { id: 'entry-fallback-diagnostics', parentId: 'parent', timestamp: '2026-04-22T11:30:00.000Z', type: 'message', message: { role: 'assistant', content: 'done', metadata: {}, }, metadata: { skillExecutions: [ { name: 'delegate_parallel', result: payload, }, ], }, }; const projection = buildSubagentProjectionForEntry(entry); expect(projection?.taskPanels).toHaveLength(1); expect(projection?.diagnostics).toMatchObject({ projectionPath: 'canonical-backend-projection', selectedSource: 'entry_metadata', inputSources: ['entry_metadata'], rawResultCount: 3, dedupedResultCount: 1, projectedTaskCount: 1, skippedResultCount: 1, fallbackUsed: true, ignoredSources: [], }); }); it('returns entries with metadata.subagentProjection without mutating originals', () => { const entry: SessionTreeEntry = { id: 'entry-result', parentId: 'parent', timestamp: '2026-04-22T12:00:00.000Z', type: 'subagent_result', content: { batchId: 'batch-2', status: 'completed', parentEntryId: 'parent', results: [ { id: 31, status: 'completed', summary: 'done', resultPayload: {}, }, ], }, }; const projected = projectSubagentEntries([entry]); expect(projected).not.toBe([entry]); expect(projected[0]).not.toBe(entry); expect(entry.metadata).toBeUndefined(); expect(projected[0].metadata?.subagentProjection).toMatchObject({ version: 1, source: 'backend-projection', taskPanels: [ { key: '31', summary: 'done', }, ], }); }); it('exposes replay limits and truncation state when projection data is clipped', () => { const entry: SessionTreeEntry = { id: 'entry-clipped-result', parentId: 'parent', timestamp: '2026-04-22T13:00:00.000Z', type: 'subagent_result', content: { batchId: 'batch-clipped', status: 'completed', parentEntryId: 'parent', results: Array.from({ length: 10 }, (_, index) => ({ id: `result-${index}`, sortOrder: index, status: 'completed', summary: `summary ${index}`, resultPayload: { evidenceSummary: { kind: 'tool-preview', title: `Evidence ${index}`, summary: `Evidence summary ${index}`, }, processEvents: Array.from({ length: index === 0 ? 55 : 0 }, (_event, eventIndex) => ({ type: 'log', runId: 'subagent-clipped', text: `event ${eventIndex}`, })), }, })), }, }; const projection = buildSubagentProjectionForEntry(entry); expect(projection).toMatchObject({ replay: { limits: { evidenceSummaries: 8, processEvents: 50, taskProcessEvents: 50, toolExecutions: 24, }, truncated: { evidenceSummaries: true, processEvents: true, taskPanels: false, toolExecutions: false, }, }, }); expect(projection?.evidenceSummaries).toHaveLength(8); expect(projection?.processEvents).toHaveLength(50); expect(projection?.taskPanels[0].processEvents).toHaveLength(50); }); it('marks tool execution truncation only when raw executions exceed the limit', () => { const buildEntry = (toolCallCount: number): SessionTreeEntry => ({ id: `entry-tool-executions-${toolCallCount}`, parentId: 'parent', timestamp: '2026-04-22T14:00:00.000Z', type: 'subagent_result', content: { batchId: `batch-tool-executions-${toolCallCount}`, status: 'completed', parentEntryId: 'parent', results: [ { id: `result-tool-executions-${toolCallCount}`, status: 'completed', summary: 'tool execution count', resultPayload: { processEvents: Array.from({ length: toolCallCount }, (_event, index) => ({ type: 'tool_call', runId: 'subagent-tools', toolCallId: `call-${index}`, name: 'bash', })), }, }, ], }, }); const exactlyAtLimit = buildSubagentProjectionForEntry(buildEntry(24)); const aboveLimit = buildSubagentProjectionForEntry(buildEntry(25)); expect(exactlyAtLimit?.taskPanels[0].toolExecutions).toHaveLength(24); expect(exactlyAtLimit?.replay.truncated.toolExecutions).toBe(false); expect(aboveLimit?.taskPanels[0].toolExecutions).toHaveLength(24); expect(aboveLimit?.replay.truncated.toolExecutions).toBe(true); }); });