import { describe, expect, it } from 'vitest' import { normalizeDecryptedMessage } from './normalize' import type { DecryptedMessage } from '@/types/api' function makeMessage(content: unknown): DecryptedMessage { return { id: 'msg-1', seq: 1, localId: null, content, createdAt: 1_742_372_800_000 } } describe('normalizeDecryptedMessage', () => { it('drops unsupported Claude system output records', () => { const message = makeMessage({ role: 'agent', content: { type: 'output', data: { type: 'system', subtype: 'stop_hook_summary', uuid: 'sys-1' } } }) expect(normalizeDecryptedMessage(message)).toBeNull() }) it('drops Claude init system output records', () => { const message = makeMessage({ role: 'agent', content: { type: 'output', data: { type: 'system', subtype: 'init', uuid: 'sys-init', session_id: 'session-1' } } }) expect(normalizeDecryptedMessage(message)).toBeNull() }) it('keeps known Claude system subtypes as normalized events', () => { const message = makeMessage({ role: 'agent', content: { type: 'output', data: { type: 'system', subtype: 'turn_duration', uuid: 'sys-2', durationMs: 1200 } } }) expect(normalizeDecryptedMessage(message)).toMatchObject({ id: 'msg-1', role: 'event', isSidechain: false, content: { type: 'turn-duration', durationMs: 1200 } }) }) it('keeps the stringify fallback for unknown non-system agent payloads', () => { const message = makeMessage({ role: 'agent', content: { type: 'output', data: { type: 'assistant', foo: 'bar' } } }) const normalized = normalizeDecryptedMessage(message) expect(normalized).toMatchObject({ id: 'msg-1', role: 'agent', isSidechain: false }) expect(normalized?.role).toBe('agent') if (!normalized || normalized.role !== 'agent') { throw new Error('Expected agent message') } const firstBlock = normalized.content[0] expect(firstBlock).toMatchObject({ type: 'text', }) if (firstBlock.type !== 'text') { throw new Error('Expected fallback text block') } expect(firstBlock.text).toContain('"foo": "bar"') }) it('normalizes user output as sidechain (event extracted by reducer)', () => { const message = makeMessage({ role: 'agent', content: { type: 'output', data: { type: 'user', uuid: 'u-notif', message: { content: ' Background command stopped ' } } } }) const normalized = normalizeDecryptedMessage(message) // Normalizer emits as sidechain (preserving uuid for sentinel detection); // the reducer extracts the summary as an event. expect(normalized).toMatchObject({ role: 'agent', isSidechain: true, }) if (normalized?.role === 'agent') { expect(normalized.content[0]).toMatchObject({ type: 'sidechain', prompt: expect.stringContaining('') }) } }) it('treats without summary as sidechain (dropped by reducer)', () => { const message = makeMessage({ role: 'agent', content: { type: 'output', data: { type: 'user', uuid: 'u3', message: { content: ' killed ' } } } }) const normalized = normalizeDecryptedMessage(message) expect(normalized).toMatchObject({ role: 'agent', isSidechain: true, }) }) it('treats non-sidechain string user output as sidechain', () => { const message = makeMessage({ role: 'agent', content: { type: 'output', data: { type: 'user', isSidechain: false, uuid: 'u1', message: { content: 'This is a subagent prompt' } } } }) const normalized = normalizeDecryptedMessage(message) expect(normalized).toMatchObject({ role: 'agent', isSidechain: true, }) if (normalized?.role !== 'agent') throw new Error('Expected agent') expect(normalized.content[0]).toMatchObject({ type: 'sidechain', prompt: 'This is a subagent prompt' }) }) it('treats user output as sidechain (dropped by reducer)', () => { const message = makeMessage({ role: 'agent', content: { type: 'output', data: { type: 'user', uuid: 'u2', message: { content: 'Some internal reminder' } } } }) const normalized = normalizeDecryptedMessage(message) expect(normalized).toMatchObject({ role: 'agent', isSidechain: true, }) }) it('treats sidechain user output with array content as sidechain', () => { const message = makeMessage({ role: 'agent', content: { type: 'output', data: { type: 'user', uuid: 'u3', isSidechain: true, message: { content: [{ type: 'text', text: 'This is an agent prompt in array form' }] } } } }) const normalized = normalizeDecryptedMessage(message) expect(normalized).toMatchObject({ role: 'agent', isSidechain: true, }) if (normalized?.role !== 'agent') throw new Error('Expected agent') expect(normalized.content[0]).toMatchObject({ type: 'sidechain', prompt: 'This is an agent prompt in array form' }) }) it('keeps "No response requested." text in normalized output (filtered later by reducer)', () => { const message = makeMessage({ role: 'agent', content: { type: 'output', data: { type: 'assistant', uuid: 'a-1', message: { role: 'assistant', content: 'No response requested.' } } } }) const normalized = normalizeDecryptedMessage(message) // Normalizer preserves the text (uuid/parentUUID needed by tracer); // the reducer is responsible for suppressing it during rendering. expect(normalized).not.toBeNull() expect(normalized?.role).toBe('agent') if (normalized?.role === 'agent') { expect(normalized.content).toHaveLength(1) expect(normalized.content[0]).toMatchObject({ type: 'text', text: 'No response requested.' }) } }) it('keeps assistant messages with real content', () => { const message = makeMessage({ role: 'agent', content: { type: 'output', data: { type: 'assistant', uuid: 'a-2', message: { role: 'assistant', content: 'Here is the answer.' } } } }) const normalized = normalizeDecryptedMessage(message) expect(normalized).not.toBeNull() expect(normalized?.role).toBe('agent') }) it('propagates parentUuid from assistant output data to text block parentUUID', () => { const message = makeMessage({ role: 'agent', content: { type: 'output', data: { type: 'assistant', uuid: 'a-3', parentUuid: 'parent-injected-uuid', message: { role: 'assistant', content: 'No response requested.' } } } }) const normalized = normalizeDecryptedMessage(message) expect(normalized).not.toBeNull() if (normalized?.role !== 'agent') throw new Error('Expected agent') expect(normalized.content).toHaveLength(1) expect(normalized.content[0]).toMatchObject({ type: 'text', text: 'No response requested.', parentUUID: 'parent-injected-uuid' }) }) it('sets parentUUID to null when parentUuid is absent in assistant output', () => { const message = makeMessage({ role: 'agent', content: { type: 'output', data: { type: 'assistant', uuid: 'a-4', // No parentUuid field message: { role: 'assistant', content: 'Hello.' } } } }) const normalized = normalizeDecryptedMessage(message) expect(normalized).not.toBeNull() if (normalized?.role !== 'agent') throw new Error('Expected agent') expect(normalized.content[0]).toMatchObject({ type: 'text', parentUUID: null }) }) })