import { existsSync } from 'fs';
import * as fsPromises from 'fs/promises';
import * as os from 'os';

import { extractToolResultContent } from '@/core/sdk/toolResultContent';
import {
  collectAsyncSubagentResults,
  deleteSDKSession,
  encodeVaultPathForSDK,
  filterActiveBranch,
  getSDKProjectsPath,
  getSDKSessionPath,
  isValidSessionId,
  loadSDKSessionMessages,
  loadSubagentFinalResult,
  loadSubagentToolCalls,
  parseSDKMessageToChat,
  readSDKSession,
  resolveToolUseResultStatus,
  type SDKNativeMessage,
  sdkSessionExists,
} from '@/utils/sdkSession';

// Mock fs, fs/promises, and os modules
jest.mock('fs', () => ({
  existsSync: jest.fn(),
}));
jest.mock('fs/promises');
jest.mock('os');

const mockExistsSync = existsSync as jest.MockedFunction<typeof existsSync>;
const mockFsPromises = fsPromises as jest.Mocked<typeof fsPromises>;
const mockOs = os as jest.Mocked<typeof os>;

describe('sdkSession', () => {
  beforeEach(() => {
    jest.clearAllMocks();
    mockOs.homedir.mockReturnValue('/Users/test');
  });

  describe('encodeVaultPathForSDK', () => {
    it('encodes vault path by replacing all non-alphanumeric chars with dash', () => {
      const encoded = encodeVaultPathForSDK('/Users/test/vault');
      // SDK replaces ALL non-alphanumeric characters with `-`
      expect(encoded).toBe('-Users-test-vault');
    });

    it('handles paths with spaces and special characters', () => {
      const encoded = encodeVaultPathForSDK("/Users/test/My Vault's~Data");
      expect(encoded).toBe('-Users-test-My-Vault-s-Data');
    });

    it('handles Unicode characters (Chinese, Japanese, etc.)', () => {
      // Unicode characters should be replaced with `-` to match SDK behavior
      const encoded = encodeVaultPathForSDK('/Volumes/[Work]弘毅之鹰/学习/东京大学/2025年 秋');
      // All non-alphanumeric (including Chinese, brackets) become `-`
      expect(encoded).toBe('-Volumes--Work--------------2025---');
      // Verify only ASCII alphanumeric and dash remain
      expect(encoded).toMatch(/^[a-zA-Z0-9-]+$/);
    });

    it('handles brackets and other special characters', () => {
      const encoded = encodeVaultPathForSDK('/Users/test/[my-vault](notes)');
      expect(encoded).toBe('-Users-test--my-vault--notes-');
      expect(encoded).not.toContain('[');
      expect(encoded).not.toContain(']');
      expect(encoded).not.toContain('(');
      expect(encoded).not.toContain(')');
    });

    it('produces consistent encoding', () => {
      const path1 = '/Users/test/my-vault';
      const encoded1 = encodeVaultPathForSDK(path1);
      const encoded2 = encodeVaultPathForSDK(path1);
      expect(encoded1).toBe(encoded2);
    });

    it('produces different encodings for different paths', () => {
      const encoded1 = encodeVaultPathForSDK('/Users/test/vault1');
      const encoded2 = encodeVaultPathForSDK('/Users/test/vault2');
      expect(encoded1).not.toBe(encoded2);
    });

    it('handles backslashes for Windows compatibility', () => {
      // Test that backslashes are replaced (Windows path separators)
      // Note: path.resolve may modify the input, so we check the output contains no backslashes
      const encoded = encodeVaultPathForSDK('C:\\Users\\test\\vault');
      expect(encoded).not.toContain('\\');
      expect(encoded).toContain('-Users-test-vault');
    });

    it('replaces colons for Windows drive letters', () => {
      // Windows paths have colons after drive letter
      const encoded = encodeVaultPathForSDK('C:\\Users\\test\\vault');
      expect(encoded).not.toContain(':');
    });
  });

  describe('getSDKProjectsPath', () => {
    it('returns path under home directory', () => {
      const projectsPath = getSDKProjectsPath();
      expect(projectsPath).toBe('/Users/test/.claude/projects');
    });
  });

  describe('isValidSessionId', () => {
    it('accepts valid UUID-style session IDs', () => {
      expect(isValidSessionId('abc123')).toBe(true);
      expect(isValidSessionId('session-123')).toBe(true);
      expect(isValidSessionId('a1b2c3d4-e5f6-7890-abcd-ef1234567890')).toBe(true);
      expect(isValidSessionId('test_session_id')).toBe(true);
    });

    it('rejects empty or too long session IDs', () => {
      expect(isValidSessionId('')).toBe(false);
      expect(isValidSessionId('a'.repeat(129))).toBe(false);
    });

    it('rejects path traversal attempts', () => {
      expect(isValidSessionId('../etc/passwd')).toBe(false);
      expect(isValidSessionId('..\\windows\\system32')).toBe(false);
      expect(isValidSessionId('foo/../bar')).toBe(false);
      expect(isValidSessionId('session/subdir')).toBe(false);
      expect(isValidSessionId('session\\subdir')).toBe(false);
    });

    it('rejects special characters', () => {
      expect(isValidSessionId('session.jsonl')).toBe(false);
      expect(isValidSessionId('session:123')).toBe(false);
      expect(isValidSessionId('session@host')).toBe(false);
    });
  });

  describe('getSDKSessionPath', () => {
    it('constructs correct session file path', () => {
      const sessionPath = getSDKSessionPath('/Users/test/vault', 'session-123');
      expect(sessionPath).toContain('.claude/projects');
      expect(sessionPath).toContain('session-123.jsonl');
    });

    it('throws error for path traversal attempts', () => {
      expect(() => getSDKSessionPath('/Users/test/vault', '../etc/passwd')).toThrow('Invalid session ID');
      expect(() => getSDKSessionPath('/Users/test/vault', 'foo/../bar')).toThrow('Invalid session ID');
      expect(() => getSDKSessionPath('/Users/test/vault', 'session/subdir')).toThrow('Invalid session ID');
    });

    it('throws error for empty session ID', () => {
      expect(() => getSDKSessionPath('/Users/test/vault', '')).toThrow('Invalid session ID');
    });
  });

  describe('sdkSessionExists', () => {
    it('returns true when session file exists', () => {
      mockExistsSync.mockReturnValue(true);

      const exists = sdkSessionExists('/Users/test/vault', 'session-abc');

      expect(exists).toBe(true);
    });

    it('returns false when session file does not exist', () => {
      mockExistsSync.mockReturnValue(false);

      const exists = sdkSessionExists('/Users/test/vault', 'session-xyz');

      expect(exists).toBe(false);
    });

    it('returns false on error', () => {
      mockExistsSync.mockImplementation(() => {
        throw new Error('Permission denied');
      });

      const exists = sdkSessionExists('/Users/test/vault', 'session-err');

      expect(exists).toBe(false);
    });
  });

  describe('deleteSDKSession', () => {
    it('deletes session file when it exists', async () => {
      mockExistsSync.mockReturnValue(true);
      mockFsPromises.unlink.mockResolvedValue(undefined);

      await deleteSDKSession('/Users/test/vault', 'session-abc');

      expect(mockFsPromises.unlink).toHaveBeenCalledWith(
        '/Users/test/.claude/projects/-Users-test-vault/session-abc.jsonl'
      );
    });

    it('does nothing when session file does not exist', async () => {
      mockExistsSync.mockReturnValue(false);

      await deleteSDKSession('/Users/test/vault', 'nonexistent');

      expect(mockFsPromises.unlink).not.toHaveBeenCalled();
    });

    it('fails silently when unlink throws', async () => {
      mockExistsSync.mockReturnValue(true);
      mockFsPromises.unlink.mockRejectedValue(new Error('Permission denied'));

      // Should not throw
      await expect(deleteSDKSession('/Users/test/vault', 'session-err')).resolves.toBeUndefined();
    });

    it('does nothing for invalid session ID', async () => {
      await deleteSDKSession('/Users/test/vault', '../invalid');

      expect(mockFsPromises.unlink).not.toHaveBeenCalled();
    });
  });

  describe('readSDKSession', () => {
    it('returns empty result when file does not exist', async () => {
      mockExistsSync.mockReturnValue(false);

      const result = await readSDKSession('/Users/test/vault', 'nonexistent');

      expect(result.messages).toEqual([]);
      expect(result.skippedLines).toBe(0);
      expect(result.error).toBeUndefined();
    });

    it('parses valid JSONL file', async () => {
      mockExistsSync.mockReturnValue(true);
      mockFsPromises.readFile.mockResolvedValue([
        '{"type":"user","uuid":"u1","message":{"content":"Hello"}}',
        '{"type":"assistant","uuid":"a1","message":{"content":"Hi there"}}',
      ].join('\n'));

      const result = await readSDKSession('/Users/test/vault', 'session-1');

      expect(result.messages).toHaveLength(2);
      expect(result.messages[0].type).toBe('user');
      expect(result.messages[1].type).toBe('assistant');
      expect(result.skippedLines).toBe(0);
    });

    it('skips invalid JSON lines and reports count', async () => {
      mockExistsSync.mockReturnValue(true);
      mockFsPromises.readFile.mockResolvedValue([
        '{"type":"user","uuid":"u1","message":{"content":"Hello"}}',
        'invalid json line',
        '{"type":"assistant","uuid":"a1","message":{"content":"Hi"}}',
      ].join('\n'));

      const result = await readSDKSession('/Users/test/vault', 'session-2');

      expect(result.messages).toHaveLength(2);
      expect(result.skippedLines).toBe(1);
    });

    it('handles empty lines', async () => {
      mockExistsSync.mockReturnValue(true);
      mockFsPromises.readFile.mockResolvedValue([
        '{"type":"user","uuid":"u1","message":{"content":"Test"}}',
        '',
        '   ',
        '{"type":"assistant","uuid":"a1","message":{"content":"Response"}}',
      ].join('\n'));

      const result = await readSDKSession('/Users/test/vault', 'session-3');

      expect(result.messages).toHaveLength(2);
    });

    it('returns error on read failure', async () => {
      mockExistsSync.mockReturnValue(true);
      mockFsPromises.readFile.mockRejectedValue(new Error('Read error'));

      const result = await readSDKSession('/Users/test/vault', 'session-err');

      expect(result.messages).toEqual([]);
      expect(result.error).toBe('Read error');
    });
  });

  describe('loadSubagentToolCalls', () => {
    it('loads tool calls from subagent sidechain JSONL', async () => {
      mockExistsSync.mockReturnValue(true);
      mockFsPromises.readFile.mockResolvedValue([
        '{"type":"assistant","timestamp":"2024-01-15T10:00:00Z","message":{"content":[{"type":"tool_use","id":"sub-tool-1","name":"Bash","input":{"command":"ls"}}]}}',
        '{"type":"user","timestamp":"2024-01-15T10:00:01Z","message":{"content":[{"type":"tool_result","tool_use_id":"sub-tool-1","content":"ok","is_error":false}]}}',
      ].join('\n'));

      const toolCalls = await loadSubagentToolCalls(
        '/Users/test/vault',
        'session-abc',
        'a123'
      );

      expect(mockFsPromises.readFile).toHaveBeenCalledWith(
        '/Users/test/.claude/projects/-Users-test-vault/session-abc/subagents/agent-a123.jsonl',
        'utf-8'
      );
      expect(toolCalls).toHaveLength(1);
      expect(toolCalls[0]).toEqual(
        expect.objectContaining({
          id: 'sub-tool-1',
          name: 'Bash',
          input: { command: 'ls' },
          status: 'completed',
          result: 'ok',
        })
      );
    });

    it('filters out entries that only have tool_result but no tool_use', async () => {
      mockExistsSync.mockReturnValue(true);
      mockFsPromises.readFile.mockResolvedValue(
        '{"type":"user","timestamp":"2024-01-15T10:00:01Z","message":{"content":[{"type":"tool_result","tool_use_id":"missing","content":"done"}]}}'
      );

      const toolCalls = await loadSubagentToolCalls(
        '/Users/test/vault',
        'session-abc',
        'a123'
      );

      expect(toolCalls).toEqual([]);
    });

    it('returns empty when agent id is invalid', async () => {
      const toolCalls = await loadSubagentToolCalls(
        '/Users/test/vault',
        'session-abc',
        '../bad-agent'
      );

      expect(toolCalls).toEqual([]);
      expect(mockFsPromises.readFile).not.toHaveBeenCalled();
    });
  });

  describe('loadSubagentFinalResult', () => {
    it('returns the latest assistant text from sidecar JSONL', async () => {
      mockExistsSync.mockReturnValue(true);
      mockFsPromises.readFile.mockResolvedValue([
        '{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"First"}]}}',
        '{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"Final answer"}]}}',
      ].join('\n'));

      const result = await loadSubagentFinalResult(
        '/Users/test/vault',
        'session-abc',
        'a123'
      );

      expect(result).toBe('Final answer');
      expect(mockFsPromises.readFile).toHaveBeenCalledWith(
        '/Users/test/.claude/projects/-Users-test-vault/session-abc/subagents/agent-a123.jsonl',
        'utf-8'
      );
    });

    it('falls back to top-level result when assistant text is absent', async () => {
      mockExistsSync.mockReturnValue(true);
      mockFsPromises.readFile.mockResolvedValue([
        '{"type":"progress","result":"Intermediate result"}',
        '{"type":"result","result":"Final result text"}',
      ].join('\n'));

      const result = await loadSubagentFinalResult(
        '/Users/test/vault',
        'session-abc',
        'a123'
      );

      expect(result).toBe('Final result text');
    });

    it('returns null when sidecar file is missing or agent id is invalid', async () => {
      mockExistsSync.mockReturnValue(false);

      const missing = await loadSubagentFinalResult(
        '/Users/test/vault',
        'session-abc',
        'a123'
      );
      expect(missing).toBeNull();

      const invalid = await loadSubagentFinalResult(
        '/Users/test/vault',
        'session-abc',
        '../bad-agent'
      );
      expect(invalid).toBeNull();
      expect(mockFsPromises.readFile).not.toHaveBeenCalled();
    });
  });

  describe('parseSDKMessageToChat', () => {
    it('converts user message with string content', () => {
      const sdkMsg: SDKNativeMessage = {
        type: 'user',
        uuid: 'user-123',
        timestamp: '2024-01-15T10:30:00Z',
        message: {
          content: 'What is the weather?',
        },
      };

      const chatMsg = parseSDKMessageToChat(sdkMsg);

      expect(chatMsg).not.toBeNull();
      expect(chatMsg!.id).toBe('user-123');
      expect(chatMsg!.role).toBe('user');
      expect(chatMsg!.content).toBe('What is the weather?');
      expect(chatMsg!.timestamp).toBe(new Date('2024-01-15T10:30:00Z').getTime());
    });

    it('sets sdkUserUuid on user messages with uuid', () => {
      const sdkMsg: SDKNativeMessage = {
        type: 'user',
        uuid: 'user-rewind-123',
        timestamp: '2024-01-15T10:30:00Z',
        message: { content: 'Hello' },
      };

      const chatMsg = parseSDKMessageToChat(sdkMsg);

      expect(chatMsg!.sdkUserUuid).toBe('user-rewind-123');
      expect(chatMsg!.sdkAssistantUuid).toBeUndefined();
    });

    it('sets sdkAssistantUuid on assistant messages with uuid', () => {
      const sdkMsg: SDKNativeMessage = {
        type: 'assistant',
        uuid: 'asst-rewind-456',
        timestamp: '2024-01-15T10:31:00Z',
        message: {
          content: [{ type: 'text', text: 'Hello back' }],
        },
      };

      const chatMsg = parseSDKMessageToChat(sdkMsg);

      expect(chatMsg!.sdkAssistantUuid).toBe('asst-rewind-456');
      expect(chatMsg!.sdkUserUuid).toBeUndefined();
    });

    it('does not set SDK UUIDs when uuid is absent', () => {
      const sdkMsg: SDKNativeMessage = {
        type: 'user',
        timestamp: '2024-01-15T10:30:00Z',
        message: { content: 'No uuid' },
      };

      const chatMsg = parseSDKMessageToChat(sdkMsg);

      expect(chatMsg!.sdkUserUuid).toBeUndefined();
      expect(chatMsg!.sdkAssistantUuid).toBeUndefined();
    });

    it('converts assistant message with text content blocks', () => {
      const sdkMsg: SDKNativeMessage = {
        type: 'assistant',
        uuid: 'asst-456',
        timestamp: '2024-01-15T10:31:00Z',
        message: {
          content: [
            { type: 'text', text: 'The weather is sunny.' },
            { type: 'text', text: 'Temperature is 72°F.' },
          ],
        },
      };

      const chatMsg = parseSDKMessageToChat(sdkMsg);

      expect(chatMsg).not.toBeNull();
      expect(chatMsg!.id).toBe('asst-456');
      expect(chatMsg!.role).toBe('assistant');
      expect(chatMsg!.content).toBe('The weather is sunny.\nTemperature is 72°F.');
    });

    it('extracts tool calls from content blocks', () => {
      const sdkMsg: SDKNativeMessage = {
        type: 'assistant',
        uuid: 'asst-tool',
        timestamp: '2024-01-15T10:32:00Z',
        message: {
          content: [
            { type: 'text', text: 'Let me search for that.' },
            {
              type: 'tool_use',
              id: 'tool-1',
              name: 'WebSearch',
              input: { query: 'weather today' },
            },
            {
              type: 'tool_result',
              tool_use_id: 'tool-1',
              content: 'Sunny, 72°F',
            },
          ],
        },
      };

      const chatMsg = parseSDKMessageToChat(sdkMsg);

      expect(chatMsg).not.toBeNull();
      expect(chatMsg!.toolCalls).toHaveLength(1);
      expect(chatMsg!.toolCalls![0].id).toBe('tool-1');
      expect(chatMsg!.toolCalls![0].name).toBe('WebSearch');
      expect(chatMsg!.toolCalls![0].input).toEqual({ query: 'weather today' });
      expect(chatMsg!.toolCalls![0].status).toBe('completed');
      expect(chatMsg!.toolCalls![0].result).toBe('Sunny, 72°F');
    });

    it('marks tool call as error when is_error is true', () => {
      const sdkMsg: SDKNativeMessage = {
        type: 'assistant',
        uuid: 'asst-err',
        timestamp: '2024-01-15T10:33:00Z',
        message: {
          content: [
            {
              type: 'tool_use',
              id: 'tool-err',
              name: 'Bash',
              input: { command: 'invalid' },
            },
            {
              type: 'tool_result',
              tool_use_id: 'tool-err',
              content: 'Command not found',
              is_error: true,
            },
          ],
        },
      };

      const chatMsg = parseSDKMessageToChat(sdkMsg);

      expect(chatMsg!.toolCalls![0].status).toBe('error');
    });

    it('keeps tool calls running when no matching tool_result exists yet', () => {
      const sdkMsg: SDKNativeMessage = {
        type: 'assistant',
        uuid: 'asst-running',
        timestamp: '2024-01-15T10:33:30Z',
        message: {
          content: [
            {
              type: 'tool_use',
              id: 'tool-running',
              name: 'Read',
              input: { file_path: 'notes/todo.md' },
            },
          ],
        },
      };

      const chatMsg = parseSDKMessageToChat(sdkMsg);

      expect(chatMsg!.toolCalls![0].status).toBe('running');
      expect(chatMsg!.toolCalls![0].result).toBeUndefined();
    });

    it('extracts thinking content blocks', () => {
      const sdkMsg: SDKNativeMessage = {
        type: 'assistant',
        uuid: 'asst-think',
        timestamp: '2024-01-15T10:34:00Z',
        message: {
          content: [
            { type: 'thinking', thinking: 'Let me consider this...' },
            { type: 'text', text: 'Here is my answer.' },
          ],
        },
      };

      const chatMsg = parseSDKMessageToChat(sdkMsg);

      expect(chatMsg!.contentBlocks).toHaveLength(2);

      const thinkingBlock = chatMsg!.contentBlocks![0];
      expect(thinkingBlock.type).toBe('thinking');
      // Type narrowing for thinking block content check
      expect(thinkingBlock.type === 'thinking' && thinkingBlock.content).toBe('Let me consider this...');

      expect(chatMsg!.contentBlocks![1].type).toBe('text');
    });

    it('preserves text block whitespace in contentBlocks', () => {
      const sdkMsg: SDKNativeMessage = {
        type: 'assistant',
        uuid: 'asst-whitespace',
        timestamp: '2024-01-15T10:34:30Z',
        message: {
          content: [
            { type: 'text', text: '  Preserve leading and trailing space  ' },
          ],
        },
      };

      const chatMsg = parseSDKMessageToChat(sdkMsg);

      expect(chatMsg!.content).toBe('  Preserve leading and trailing space  ');
      expect(chatMsg!.contentBlocks).toEqual([
        { type: 'text', content: '  Preserve leading and trailing space  ' },
      ]);
    });

    it('returns null for system messages', () => {
      const sdkMsg: SDKNativeMessage = {
        type: 'system',
        uuid: 'sys-1',
      };

      const chatMsg = parseSDKMessageToChat(sdkMsg);

      expect(chatMsg).toBeNull();
    });

    it('returns synthetic assistant message for compact_boundary system messages', () => {
      const sdkMsg: SDKNativeMessage = {
        type: 'system',
        subtype: 'compact_boundary',
        uuid: 'compact-1',
        timestamp: '2024-06-15T12:00:00Z',
      };

      const chatMsg = parseSDKMessageToChat(sdkMsg);

      expect(chatMsg).not.toBeNull();
      expect(chatMsg!.id).toBe('compact-1');
      expect(chatMsg!.role).toBe('assistant');
      expect(chatMsg!.content).toBe('');
      expect(chatMsg!.timestamp).toBe(new Date('2024-06-15T12:00:00Z').getTime());
      expect(chatMsg!.contentBlocks).toEqual([{ type: 'compact_boundary' }]);
    });

    it('generates ID for compact_boundary without uuid', () => {
      const sdkMsg: SDKNativeMessage = {
        type: 'system',
        subtype: 'compact_boundary',
      };

      const chatMsg = parseSDKMessageToChat(sdkMsg);

      expect(chatMsg).not.toBeNull();
      expect(chatMsg!.id).toMatch(/^compact-/);
    });

    it('returns null for result messages', () => {
      const sdkMsg: SDKNativeMessage = {
        type: 'result',
        uuid: 'res-1',
      };

      const chatMsg = parseSDKMessageToChat(sdkMsg);

      expect(chatMsg).toBeNull();
    });

    it('returns null for file-history-snapshot messages', () => {
      const sdkMsg: SDKNativeMessage = {
        type: 'file-history-snapshot',
        uuid: 'fhs-1',
      };

      const chatMsg = parseSDKMessageToChat(sdkMsg);

      expect(chatMsg).toBeNull();
    });

    it('generates ID when uuid is missing', () => {
      const sdkMsg: SDKNativeMessage = {
        type: 'user',
        timestamp: '2024-01-15T10:35:00Z',
        message: {
          content: 'No UUID message',
        },
      };

      const chatMsg = parseSDKMessageToChat(sdkMsg);

      expect(chatMsg).not.toBeNull();
      expect(chatMsg!.id).toMatch(/^sdk-/);
    });

    it('uses current time when timestamp is missing', () => {
      const before = Date.now();
      const sdkMsg: SDKNativeMessage = {
        type: 'user',
        uuid: 'no-time',
        message: {
          content: 'No timestamp',
        },
      };

      const chatMsg = parseSDKMessageToChat(sdkMsg);
      const after = Date.now();

      expect(chatMsg!.timestamp).toBeGreaterThanOrEqual(before);
      expect(chatMsg!.timestamp).toBeLessThanOrEqual(after);
    });

    it('marks interrupt messages with isInterrupt flag', () => {
      const sdkMsg: SDKNativeMessage = {
        type: 'user',
        uuid: 'interrupt-1',
        timestamp: '2024-01-15T10:30:00Z',
        message: {
          content: [{ type: 'text', text: '[Request interrupted by user]' }],
        },
      };

      const chatMsg = parseSDKMessageToChat(sdkMsg);

      expect(chatMsg).not.toBeNull();
      expect(chatMsg!.isInterrupt).toBe(true);
      expect(chatMsg!.content).toBe('[Request interrupted by user]');
    });

    it('does not mark non-canonical interrupt text variants', () => {
      const sdkMsg: SDKNativeMessage = {
        type: 'user',
        uuid: 'interrupt-non-canonical',
        timestamp: '2024-01-15T10:30:00Z',
        message: {
          content: 'prefix [Request interrupted by user]',
        },
      };

      const chatMsg = parseSDKMessageToChat(sdkMsg);

      expect(chatMsg).not.toBeNull();
      expect(chatMsg!.isInterrupt).toBeUndefined();
    });

    it('does not mark regular user messages as interrupt', () => {
      const sdkMsg: SDKNativeMessage = {
        type: 'user',
        uuid: 'user-regular',
        timestamp: '2024-01-15T10:30:00Z',
        message: {
          content: 'Hello, how are you?',
        },
      };

      const chatMsg = parseSDKMessageToChat(sdkMsg);

      expect(chatMsg).not.toBeNull();
      expect(chatMsg!.isInterrupt).toBeUndefined();
    });

    it('marks rebuilt context messages with isRebuiltContext flag', () => {
      const sdkMsg: SDKNativeMessage = {
        type: 'user',
        uuid: 'rebuilt-1',
        timestamp: '2024-01-15T10:30:00Z',
        message: {
          content: 'User: hi\n\nAssistant: Hello!\n\nUser: how are you?',
        },
      };

      const chatMsg = parseSDKMessageToChat(sdkMsg);

      expect(chatMsg).not.toBeNull();
      expect(chatMsg!.isRebuiltContext).toBe(true);
    });

    it('marks rebuilt context messages starting with Assistant', () => {
      const sdkMsg: SDKNativeMessage = {
        type: 'user',
        uuid: 'rebuilt-2',
        timestamp: '2024-01-15T10:31:00Z',
        message: {
          content: 'Assistant: Hello\n\nUser: Hi again',
        },
      };

      const chatMsg = parseSDKMessageToChat(sdkMsg);

      expect(chatMsg).not.toBeNull();
      expect(chatMsg!.isRebuiltContext).toBe(true);
    });

    it('does not mark regular messages starting with User as rebuilt context', () => {
      const sdkMsg: SDKNativeMessage = {
        type: 'user',
        uuid: 'user-normal',
        timestamp: '2024-01-15T10:30:00Z',
        message: {
          content: 'User settings should be configurable',
        },
      };

      const chatMsg = parseSDKMessageToChat(sdkMsg);

      expect(chatMsg).not.toBeNull();
      expect(chatMsg!.isRebuiltContext).toBeUndefined();
    });

    it('extracts displayContent from user message with current_note tag', () => {
      const sdkMsg: SDKNativeMessage = {
        type: 'user',
        uuid: 'user-note',
        timestamp: '2024-01-15T10:30:00Z',
        message: {
          content: 'Explain this file\n\n<current_note>\nnotes/test.md\n</current_note>',
        },
      };

      const chatMsg = parseSDKMessageToChat(sdkMsg);

      expect(chatMsg).not.toBeNull();
      expect(chatMsg!.content).toBe('Explain this file\n\n<current_note>\nnotes/test.md\n</current_note>');
      expect(chatMsg!.displayContent).toBe('Explain this file');
    });

    it('extracts displayContent from user message with editor_selection tag', () => {
      const sdkMsg: SDKNativeMessage = {
        type: 'user',
        uuid: 'user-selection',
        timestamp: '2024-01-15T10:30:00Z',
        message: {
          content: 'Refactor this code\n\n<editor_selection path="src/main.ts">\nfunction foo() {}\n</editor_selection>',
        },
      };

      const chatMsg = parseSDKMessageToChat(sdkMsg);

      expect(chatMsg).not.toBeNull();
      expect(chatMsg!.displayContent).toBe('Refactor this code');
    });

    it('extracts displayContent from user message with multiple context tags', () => {
      const sdkMsg: SDKNativeMessage = {
        type: 'user',
        uuid: 'user-multi',
        timestamp: '2024-01-15T10:30:00Z',
        message: {
          content: 'Update this\n\n<current_note>\ntest.md\n</current_note>\n\n<editor_selection path="test.md">\nselected\n</editor_selection>',
        },
      };

      const chatMsg = parseSDKMessageToChat(sdkMsg);

      expect(chatMsg).not.toBeNull();
      expect(chatMsg!.displayContent).toBe('Update this');
    });

    it('does not set displayContent for plain user messages without XML context', () => {
      const sdkMsg: SDKNativeMessage = {
        type: 'user',
        uuid: 'user-plain',
        timestamp: '2024-01-15T10:30:00Z',
        message: {
          content: 'Just a regular question',
        },
      };

      const chatMsg = parseSDKMessageToChat(sdkMsg);

      expect(chatMsg).not.toBeNull();
      expect(chatMsg!.displayContent).toBeUndefined();
    });
  });

  describe('loadSDKSessionMessages', () => {
    it('loads and converts all messages from session file', async () => {
      mockExistsSync.mockReturnValue(true);
      mockFsPromises.readFile.mockResolvedValue([
        '{"type":"user","uuid":"u1","timestamp":"2024-01-15T10:00:00Z","message":{"content":"Hello"}}',
        '{"type":"assistant","uuid":"a1","timestamp":"2024-01-15T10:01:00Z","message":{"content":[{"type":"text","text":"Hi!"}]}}',
        '{"type":"system","uuid":"s1"}',
        '{"type":"user","uuid":"u2","timestamp":"2024-01-15T10:02:00Z","message":{"content":"Thanks"}}',
      ].join('\n'));

      const result = await loadSDKSessionMessages('/Users/test/vault', 'session-full');

      // Should have 3 messages (system skipped)
      expect(result.messages).toHaveLength(3);
      expect(result.messages[0].role).toBe('user');
      expect(result.messages[0].content).toBe('Hello');
      expect(result.messages[1].role).toBe('assistant');
      expect(result.messages[1].content).toBe('Hi!');
      expect(result.messages[2].role).toBe('user');
      expect(result.messages[2].content).toBe('Thanks');
    });

    it('sorts messages by timestamp ascending', async () => {
      mockExistsSync.mockReturnValue(true);
      mockFsPromises.readFile.mockResolvedValue([
        '{"type":"assistant","uuid":"a1","timestamp":"2024-01-15T10:01:00Z","message":{"content":[{"type":"text","text":"Second"}]}}',
        '{"type":"user","uuid":"u1","timestamp":"2024-01-15T10:00:00Z","message":{"content":"First"}}',
        '{"type":"user","uuid":"u2","timestamp":"2024-01-15T10:02:00Z","message":{"content":"Third"}}',
      ].join('\n'));

      const result = await loadSDKSessionMessages('/Users/test/vault', 'session-unordered');

      expect(result.messages[0].content).toBe('First');
      expect(result.messages[1].content).toBe('Second');
      expect(result.messages[2].content).toBe('Third');
    });

    it('returns empty result when session does not exist', async () => {
      mockExistsSync.mockReturnValue(false);

      const result = await loadSDKSessionMessages('/Users/test/vault', 'nonexistent');

      expect(result.messages).toEqual([]);
    });

    it('matches tool_result from user message to tool_use in assistant message', async () => {
      mockExistsSync.mockReturnValue(true);
      mockFsPromises.readFile.mockResolvedValue([
        '{"type":"user","uuid":"u1","timestamp":"2024-01-15T10:00:00Z","message":{"content":"Search for cats"}}',
        '{"type":"assistant","uuid":"a1","timestamp":"2024-01-15T10:01:00Z","message":{"content":[{"type":"text","text":"Let me search"},{"type":"tool_use","id":"tool-1","name":"WebSearch","input":{"query":"cats"}}]}}',
        '{"type":"user","uuid":"u2","timestamp":"2024-01-15T10:02:00Z","toolUseResult":{},"message":{"content":[{"type":"tool_result","tool_use_id":"tool-1","content":"Found 10 results"}]}}',
        '{"type":"assistant","uuid":"a2","timestamp":"2024-01-15T10:03:00Z","message":{"content":[{"type":"text","text":"I found 10 results about cats."}]}}',
      ].join('\n'));

      const result = await loadSDKSessionMessages('/Users/test/vault', 'session-cross-tool');

      // Should have 2 messages (tool_result-only user skipped, assistant messages merged)
      expect(result.messages).toHaveLength(2);
      expect(result.messages[0].content).toBe('Search for cats');
      // Merged assistant message has tool calls and combined content
      expect(result.messages[1].toolCalls).toHaveLength(1);
      expect(result.messages[1].toolCalls![0].id).toBe('tool-1');
      expect(result.messages[1].toolCalls![0].result).toBe('Found 10 results');
      expect(result.messages[1].toolCalls![0].status).toBe('completed');
      expect(result.messages[1].content).toContain('Let me search');
      expect(result.messages[1].content).toContain('I found 10 results about cats.');
    });

    it('hydrates AskUserQuestion answers from result text when toolUseResult has no answers', async () => {
      mockExistsSync.mockReturnValue(true);
      mockFsPromises.readFile.mockResolvedValue([
        '{"type":"assistant","uuid":"a1","timestamp":"2024-01-15T10:01:00Z","message":{"content":[{"type":"tool_use","id":"ask-1","name":"AskUserQuestion","input":{"questions":[{"question":"Color?","options":["Blue","Red"]}]}}]}}',
        '{"type":"user","uuid":"u1","timestamp":"2024-01-15T10:02:00Z","toolUseResult":{},"message":{"content":[{"type":"tool_result","tool_use_id":"ask-1","content":"\\"Color?\\"=\\"Blue\\""}]}}',
      ].join('\n'));

      const result = await loadSDKSessionMessages('/Users/test/vault', 'session-ask-result-fallback');

      expect(result.messages).toHaveLength(1);
      expect(result.messages[0].toolCalls).toHaveLength(1);
      expect(result.messages[0].toolCalls?.[0].resolvedAnswers).toEqual({ 'Color?': 'Blue' });
    });

    it('skips user messages that are tool results', async () => {
      mockExistsSync.mockReturnValue(true);
      mockFsPromises.readFile.mockResolvedValue([
        '{"type":"user","uuid":"u1","timestamp":"2024-01-15T10:00:00Z","message":{"content":"Hello"}}',
        '{"type":"assistant","uuid":"a1","timestamp":"2024-01-15T10:01:00Z","message":{"content":[{"type":"tool_use","id":"t1","name":"Bash","input":{}}]}}',
        '{"type":"user","uuid":"u2","timestamp":"2024-01-15T10:02:00Z","toolUseResult":{},"message":{"content":[{"type":"tool_result","tool_use_id":"t1","content":"done"}]}}',
      ].join('\n'));

      const result = await loadSDKSessionMessages('/Users/test/vault', 'session-skip-tool-result');

      // Should have 2 messages (tool_result user skipped)
      expect(result.messages).toHaveLength(2);
      expect(result.messages[0].role).toBe('user');
      expect(result.messages[0].content).toBe('Hello');
      expect(result.messages[1].role).toBe('assistant');
    });

    it('skips skill prompt injection messages (sourceToolUseID)', async () => {
      mockExistsSync.mockReturnValue(true);
      mockFsPromises.readFile.mockResolvedValue([
        '{"type":"user","uuid":"u1","timestamp":"2024-01-15T10:00:00Z","message":{"content":"/commit"}}',
        '{"type":"assistant","uuid":"a1","timestamp":"2024-01-15T10:01:00Z","message":{"content":[{"type":"tool_use","id":"t1","name":"Skill","input":{"skill":"commit"}}]}}',
        '{"type":"user","uuid":"u2","timestamp":"2024-01-15T10:02:00Z","toolUseResult":{},"message":{"content":[{"type":"tool_result","tool_use_id":"t1","content":"Launching skill: commit"}]}}',
        '{"type":"user","uuid":"u3","timestamp":"2024-01-15T10:02:01Z","sourceToolUseID":"t1","isMeta":true,"message":{"content":[{"type":"text","text":"## Your task\\n\\nCommit the changes..."}]}}',
        '{"type":"assistant","uuid":"a2","timestamp":"2024-01-15T10:03:00Z","message":{"content":[{"type":"text","text":"Committing the changes now."}]}}',
      ].join('\n'));

      const result = await loadSDKSessionMessages('/Users/test/vault', 'session-skip-skill');

      // Should have 2 messages: user query, merged assistant (tool_use + text merged together)
      // Skill prompt injection (u3) and tool result (u2) should be skipped
      // Consecutive assistant messages are merged
      expect(result.messages).toHaveLength(2);
      expect(result.messages[0].role).toBe('user');
      expect(result.messages[0].content).toBe('/commit');
      expect(result.messages[1].role).toBe('assistant');
      expect(result.messages[1].toolCalls?.[0].name).toBe('Skill');
      expect(result.messages[1].content).toContain('Committing');
    });

    it('skips meta messages without sourceToolUseID', async () => {
      mockExistsSync.mockReturnValue(true);
      mockFsPromises.readFile.mockResolvedValue([
        '{"type":"user","uuid":"u1","timestamp":"2024-01-15T10:00:00Z","message":{"content":"Hello"}}',
        '{"type":"user","uuid":"u2","timestamp":"2024-01-15T10:00:01Z","isMeta":true,"message":{"content":"System context injection"}}',
        '{"type":"assistant","uuid":"a1","timestamp":"2024-01-15T10:01:00Z","message":{"content":[{"type":"text","text":"Hi there!"}]}}',
      ].join('\n'));

      const result = await loadSDKSessionMessages('/Users/test/vault', 'session-skip-meta');

      // Should have 2 messages (meta message u2 skipped)
      expect(result.messages).toHaveLength(2);
      expect(result.messages[0].role).toBe('user');
      expect(result.messages[0].content).toBe('Hello');
      expect(result.messages[1].role).toBe('assistant');
    });

    it('preserves /compact command as user message with clean displayContent', async () => {
      // File ordering mirrors real SDK JSONL: compact_boundary written BEFORE /compact command.
      // The timestamp sort must reorder so /compact (earlier) precedes boundary (later).
      mockExistsSync.mockReturnValue(true);
      mockFsPromises.readFile.mockResolvedValue([
        '{"type":"user","uuid":"u1","timestamp":"2024-01-15T10:00:00Z","message":{"content":"Hello"}}',
        '{"type":"assistant","uuid":"a1","timestamp":"2024-01-15T10:01:00Z","message":{"content":[{"type":"text","text":"Hi!"}]}}',
        '{"type":"system","subtype":"compact_boundary","uuid":"c1","timestamp":"2024-01-15T10:02:10Z"}',
        '{"type":"user","uuid":"u2","timestamp":"2024-01-15T10:02:00Z","isMeta":true,"message":{"content":"<local-command-caveat>Caveat</local-command-caveat>"}}',
        '{"type":"user","uuid":"u3","timestamp":"2024-01-15T10:02:01Z","message":{"content":"<command-name>/compact</command-name>\\n<command-message>compact</command-message>\\n<command-args></command-args>"}}',
        '{"type":"user","uuid":"u4","timestamp":"2024-01-15T10:02:11Z","message":{"content":"<local-command-stdout>Compacted </local-command-stdout>"}}',
      ].join('\n'));

      const result = await loadSDKSessionMessages('/Users/test/vault', 'session-compact');

      // Should have: user "Hello", assistant "Hi!", user "/compact", assistant compact_boundary
      // Meta (u2), stdout (u4) should be skipped
      // /compact (10:02:01) sorted before compact_boundary (10:02:10) by timestamp
      expect(result.messages).toHaveLength(4);
      expect(result.messages[0].role).toBe('user');
      expect(result.messages[0].content).toBe('Hello');
      expect(result.messages[1].role).toBe('assistant');
      expect(result.messages[2].role).toBe('user');
      expect(result.messages[2].displayContent).toBe('/compact');
      expect(result.messages[3].role).toBe('assistant');
      expect(result.messages[3].contentBlocks).toEqual([{ type: 'compact_boundary' }]);
    });

    it('renders compact cancellation stderr as interrupt (not filtered)', async () => {
      mockExistsSync.mockReturnValue(true);
      mockFsPromises.readFile.mockResolvedValue([
        '{"type":"user","uuid":"u1","timestamp":"2024-01-15T10:00:00Z","message":{"content":"Hello"}}',
        '{"type":"assistant","uuid":"a1","timestamp":"2024-01-15T10:01:00Z","message":{"content":[{"type":"text","text":"Hi!"}]}}',
        '{"type":"user","uuid":"u2","timestamp":"2024-01-15T10:02:00Z","message":{"content":"<command-name>/compact</command-name>\\n<command-message>compact</command-message>\\n<command-args></command-args>"}}',
        '{"type":"user","uuid":"u3","timestamp":"2024-01-15T10:02:01Z","message":{"content":"<local-command-stderr>Error: Compaction canceled.</local-command-stderr>"}}',
      ].join('\n'));

      const result = await loadSDKSessionMessages('/Users/test/vault', 'session-compact-cancel');

      // Compact cancellation stderr should appear as interrupt, not be filtered
      const interruptMsg = result.messages.find(m => m.isInterrupt);
      expect(interruptMsg).toBeDefined();
      expect(interruptMsg!.isInterrupt).toBe(true);
    });

    it('does not treat embedded compaction stderr mentions as interrupt markers', async () => {
      mockExistsSync.mockReturnValue(true);
      mockFsPromises.readFile.mockResolvedValue([
        '{"type":"user","uuid":"u1","timestamp":"2024-01-15T10:00:00Z","message":{"content":"Hello"}}',
        '{"type":"assistant","uuid":"a1","timestamp":"2024-01-15T10:01:00Z","message":{"content":[{"type":"text","text":"Hi!"}]}}',
        '{"type":"user","uuid":"u2","timestamp":"2024-01-15T10:02:01Z","message":{"content":"## Context\\n<local-command-stderr>Error: Compaction canceled.</local-command-stderr>"}}',
      ].join('\n'));

      const result = await loadSDKSessionMessages('/Users/test/vault', 'session-compact-quoted-cancel');

      expect(result.messages).toHaveLength(2);
      expect(result.messages.some(m => m.isInterrupt)).toBe(false);
    });

    it('preserves slash command invocations with clean displayContent', async () => {
      mockExistsSync.mockReturnValue(true);
      mockFsPromises.readFile.mockResolvedValue([
        '{"type":"user","uuid":"u1","timestamp":"2024-01-15T10:00:00Z","message":{"content":"Hello"}}',
        '{"type":"assistant","uuid":"a1","timestamp":"2024-01-15T10:01:00Z","message":{"content":[{"type":"text","text":"Hi!"}]}}',
        '{"type":"user","uuid":"u2","timestamp":"2024-01-15T10:02:00Z","message":{"content":"<command-message>md2docx</command-message>\\n<command-name>/md2docx</command-name>"}}',
        '{"type":"user","uuid":"u3","timestamp":"2024-01-15T10:02:00Z","isMeta":true,"message":{"content":"Use bash command md2word..."}}',
        '{"type":"assistant","uuid":"a2","timestamp":"2024-01-15T10:03:00Z","message":{"content":[{"type":"text","text":"(no content)"}]}}',
        '{"type":"assistant","uuid":"a3","timestamp":"2024-01-15T10:03:01Z","message":{"content":[{"type":"tool_use","id":"t1","name":"Skill","input":{"skill":"md2docx"}}]}}',
      ].join('\n'));

      const result = await loadSDKSessionMessages('/Users/test/vault', 'session-slash-cmd');

      // user "Hello", assistant "Hi!", user "/md2docx", assistant with Skill tool
      // META (u3) should be skipped; "(no content)" text should be filtered
      expect(result.messages).toHaveLength(4);
      expect(result.messages[2].role).toBe('user');
      expect(result.messages[2].displayContent).toBe('/md2docx');
      expect(result.messages[3].role).toBe('assistant');
      expect(result.messages[3].content).toBe('');
      expect(result.messages[3].toolCalls).toHaveLength(1);
      expect(result.messages[3].toolCalls![0].name).toBe('Skill');
    });

    it('handles tool_result with error flag', async () => {
      mockExistsSync.mockReturnValue(true);
      mockFsPromises.readFile.mockResolvedValue([
        '{"type":"assistant","uuid":"a1","timestamp":"2024-01-15T10:00:00Z","message":{"content":[{"type":"tool_use","id":"t1","name":"Bash","input":{"command":"invalid"}}]}}',
        '{"type":"user","uuid":"u1","timestamp":"2024-01-15T10:01:00Z","toolUseResult":{},"message":{"content":[{"type":"tool_result","tool_use_id":"t1","content":"Command not found","is_error":true}]}}',
      ].join('\n'));

      const result = await loadSDKSessionMessages('/Users/test/vault', 'session-error-result');

      expect(result.messages).toHaveLength(1);
      expect(result.messages[0].toolCalls![0].status).toBe('error');
      expect(result.messages[0].toolCalls![0].result).toBe('Command not found');
    });

    it('returns error pass-through from readSDKSession', async () => {
      mockExistsSync.mockReturnValue(true);
      mockFsPromises.readFile.mockRejectedValue(new Error('Disk failure'));

      const result = await loadSDKSessionMessages('/Users/test/vault', 'session-disk-err');

      expect(result.messages).toEqual([]);
      expect(result.error).toBe('Disk failure');
    });

    it('merges tool calls from consecutive assistant messages', async () => {
      mockExistsSync.mockReturnValue(true);
      mockFsPromises.readFile.mockResolvedValue([
        '{"type":"assistant","uuid":"a1","timestamp":"2024-01-15T10:00:00Z","message":{"content":[{"type":"tool_use","id":"t1","name":"Read","input":{"path":"a.ts"}}]}}',
        '{"type":"assistant","uuid":"a2","timestamp":"2024-01-15T10:00:01Z","message":{"content":[{"type":"tool_use","id":"t2","name":"Write","input":{"path":"b.ts"}}]}}',
      ].join('\n'));

      const result = await loadSDKSessionMessages('/Users/test/vault', 'session-merge-tools');

      // Consecutive assistant messages should merge into one
      expect(result.messages).toHaveLength(1);
      expect(result.messages[0].toolCalls).toHaveLength(2);
      expect(result.messages[0].toolCalls![0].name).toBe('Read');
      expect(result.messages[0].toolCalls![1].name).toBe('Write');
    });

    it('updates sdkAssistantUuid to last entry when merging assistant messages', async () => {
      mockExistsSync.mockReturnValue(true);
      mockFsPromises.readFile.mockResolvedValue([
        '{"type":"user","uuid":"u1","timestamp":"2024-01-15T10:00:00Z","message":{"content":"hello"}}',
        '{"type":"assistant","uuid":"a1-first","parentUuid":"u1","timestamp":"2024-01-15T10:00:01Z","message":{"content":[{"type":"text","text":"thinking..."}]}}',
        '{"type":"assistant","uuid":"a1-mid","parentUuid":"a1-first","timestamp":"2024-01-15T10:00:02Z","message":{"content":[{"type":"tool_use","id":"t1","name":"Read","input":{"path":"a.ts"}}]}}',
        '{"type":"assistant","uuid":"a1-last","parentUuid":"a1-mid","timestamp":"2024-01-15T10:00:03Z","message":{"content":[{"type":"text","text":"Done!"}]}}',
      ].join('\n'));

      const result = await loadSDKSessionMessages('/Users/test/vault', 'session-merge-uuid');

      expect(result.messages).toHaveLength(2);
      const assistant = result.messages[1];
      expect(assistant.role).toBe('assistant');
      // Must be the last UUID so rewind targets the end of the turn
      expect(assistant.sdkAssistantUuid).toBe('a1-last');
    });
  });

  describe('parseSDKMessageToChat - image extraction', () => {
    it('extracts image attachments from user message with image blocks', () => {
      const sdkMsg: SDKNativeMessage = {
        type: 'user',
        uuid: 'user-img',
        timestamp: '2024-01-15T10:30:00Z',
        message: {
          content: [
            { type: 'text', text: 'Check this image' },
            {
              type: 'image',
              source: {
                type: 'base64',
                media_type: 'image/png',
                data: 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk',
              },
            },
          ],
        },
      };

      const chatMsg = parseSDKMessageToChat(sdkMsg);

      expect(chatMsg).not.toBeNull();
      expect(chatMsg!.images).toHaveLength(1);
      expect(chatMsg!.images![0].mediaType).toBe('image/png');
      expect(chatMsg!.images![0].data).toContain('iVBORw0KGgo');
      expect(chatMsg!.images![0].source).toBe('paste');
      expect(chatMsg!.images![0].name).toBe('image-1');
    });

    it('does not extract images from assistant messages', () => {
      const sdkMsg: SDKNativeMessage = {
        type: 'assistant',
        uuid: 'asst-img',
        timestamp: '2024-01-15T10:30:00Z',
        message: {
          content: [
            { type: 'text', text: 'Here is a response' },
          ],
        },
      };

      const chatMsg = parseSDKMessageToChat(sdkMsg);

      expect(chatMsg).not.toBeNull();
      expect(chatMsg!.images).toBeUndefined();
    });

    it('returns null for user message with only tool_result content blocks', () => {
      const sdkMsg: SDKNativeMessage = {
        type: 'user',
        uuid: 'user-tool-only',
        timestamp: '2024-01-15T10:30:00Z',
        message: {
          content: [
            { type: 'tool_result', tool_use_id: 't1', content: 'result data' },
          ],
        },
      };

      const chatMsg = parseSDKMessageToChat(sdkMsg);
      // Array content bypasses the null-return guard even without text/tool_use/images
      expect(chatMsg).not.toBeNull();
    });

    it('returns null for user message with empty string content', () => {
      const sdkMsg: SDKNativeMessage = {
        type: 'user',
        uuid: 'user-empty',
        timestamp: '2024-01-15T10:30:00Z',
        message: {
          content: '',
        },
      };

      const chatMsg = parseSDKMessageToChat(sdkMsg);
      expect(chatMsg).toBeNull();
    });

    it('returns null for user message with no content', () => {
      const sdkMsg: SDKNativeMessage = {
        type: 'user',
        uuid: 'user-nocontent',
        timestamp: '2024-01-15T10:30:00Z',
        message: {},
      };

      const chatMsg = parseSDKMessageToChat(sdkMsg);
      expect(chatMsg).toBeNull();
    });

    it('returns null for queue-operation messages', () => {
      const sdkMsg: SDKNativeMessage = {
        type: 'queue-operation',
        uuid: 'queue-1',
      };

      const chatMsg = parseSDKMessageToChat(sdkMsg);
      expect(chatMsg).toBeNull();
    });
  });

  describe('parseSDKMessageToChat - content block edge cases', () => {
    it('skips text blocks that are whitespace-only in contentBlocks', () => {
      const sdkMsg: SDKNativeMessage = {
        type: 'assistant',
        uuid: 'asst-whitespace',
        timestamp: '2024-01-15T10:30:00Z',
        message: {
          content: [
            { type: 'text', text: '   ' },
            { type: 'text', text: 'Actual content' },
          ],
        },
      };

      const chatMsg = parseSDKMessageToChat(sdkMsg);
      expect(chatMsg).not.toBeNull();
      // The whitespace-only text block should be skipped in contentBlocks
      expect(chatMsg!.contentBlocks).toHaveLength(1);
      expect(chatMsg!.contentBlocks![0].type).toBe('text');
    });

    it('skips thinking blocks with empty thinking field', () => {
      const sdkMsg: SDKNativeMessage = {
        type: 'assistant',
        uuid: 'asst-empty-think',
        timestamp: '2024-01-15T10:30:00Z',
        message: {
          content: [
            { type: 'thinking', thinking: '' },
            { type: 'text', text: 'Some answer' },
          ],
        },
      };

      const chatMsg = parseSDKMessageToChat(sdkMsg);
      expect(chatMsg).not.toBeNull();
      // Empty thinking block should be skipped
      expect(chatMsg!.contentBlocks).toHaveLength(1);
      expect(chatMsg!.contentBlocks![0].type).toBe('text');
    });

    it('skips tool_use blocks without id in contentBlocks', () => {
      const sdkMsg: SDKNativeMessage = {
        type: 'assistant',
        uuid: 'asst-no-id-tool',
        timestamp: '2024-01-15T10:30:00Z',
        message: {
          content: [
            { type: 'tool_use', name: 'Bash', input: {} },
            { type: 'text', text: 'After tool' },
          ],
        },
      };

      const chatMsg = parseSDKMessageToChat(sdkMsg);
      expect(chatMsg).not.toBeNull();
      // tool_use without id should be skipped in contentBlocks
      expect(chatMsg!.contentBlocks).toHaveLength(1);
      expect(chatMsg!.contentBlocks![0].type).toBe('text');
    });

    it('returns undefined contentBlocks when all blocks are filtered out', () => {
      const sdkMsg: SDKNativeMessage = {
        type: 'assistant',
        uuid: 'asst-all-filtered',
        timestamp: '2024-01-15T10:30:00Z',
        message: {
          content: [
            { type: 'tool_result', tool_use_id: 't1', content: 'result' },
          ],
        },
      };

      const chatMsg = parseSDKMessageToChat(sdkMsg);
      // Content is array (so not null), but all blocks filtered → undefined contentBlocks
      expect(chatMsg).not.toBeNull();
      expect(chatMsg!.contentBlocks).toBeUndefined();
    });

    it('handles tool_use without input field', () => {
      const sdkMsg: SDKNativeMessage = {
        type: 'assistant',
        uuid: 'asst-no-input',
        timestamp: '2024-01-15T10:30:00Z',
        message: {
          content: [
            { type: 'tool_use', id: 'tool-noinput', name: 'SomeTool' },
          ],
        },
      };

      const chatMsg = parseSDKMessageToChat(sdkMsg);
      expect(chatMsg).not.toBeNull();
      expect(chatMsg!.toolCalls).toHaveLength(1);
      expect(chatMsg!.toolCalls![0].input).toEqual({});
    });

    it('handles tool_result with non-string content (JSON object)', () => {
      const sdkMsg: SDKNativeMessage = {
        type: 'assistant',
        uuid: 'asst-json-result',
        timestamp: '2024-01-15T10:30:00Z',
        message: {
          content: [
            { type: 'tool_use', id: 'tool-json', name: 'Read', input: {} },
            {
              type: 'tool_result',
              tool_use_id: 'tool-json',
              content: { file: 'test.ts', lines: 42 },
            },
          ],
        },
      };

      const chatMsg = parseSDKMessageToChat(sdkMsg);
      expect(chatMsg).not.toBeNull();
      expect(chatMsg!.toolCalls).toHaveLength(1);
      // Non-string content should be JSON.stringified
      expect(chatMsg!.toolCalls![0].result).toBe('{"file":"test.ts","lines":42}');
    });
  });

  describe('parseSDKMessageToChat - rebuilt context with A: shorthand', () => {
    it('detects rebuilt context using A: shorthand marker', () => {
      const sdkMsg: SDKNativeMessage = {
        type: 'user',
        uuid: 'rebuilt-short',
        timestamp: '2024-01-15T10:30:00Z',
        message: {
          content: 'User: hello\n\nA: hi there',
        },
      };

      const chatMsg = parseSDKMessageToChat(sdkMsg);
      expect(chatMsg).not.toBeNull();
      expect(chatMsg!.isRebuiltContext).toBe(true);
    });
  });

  describe('parseSDKMessageToChat - interrupt tool use variant', () => {
    it('marks tool use interrupt messages', () => {
      const sdkMsg: SDKNativeMessage = {
        type: 'user',
        uuid: 'interrupt-tool',
        timestamp: '2024-01-15T10:30:00Z',
        message: {
          content: '[Request interrupted by user for tool use]',
        },
      };

      const chatMsg = parseSDKMessageToChat(sdkMsg);
      expect(chatMsg).not.toBeNull();
      expect(chatMsg!.isInterrupt).toBe(true);
    });

    it('does not mark quoted compact cancellation mention as interrupt', () => {
      const sdkMsg: SDKNativeMessage = {
        type: 'user',
        uuid: 'interrupt-compact-quoted',
        timestamp: '2024-01-15T10:30:00Z',
        message: {
          content: '## Context\n<local-command-stderr>Error: Compaction canceled.</local-command-stderr>',
        },
      };

      const chatMsg = parseSDKMessageToChat(sdkMsg);
      expect(chatMsg).not.toBeNull();
      expect(chatMsg!.isInterrupt).toBeUndefined();
    });

    it('marks compact cancellation stderr as interrupt', () => {
      const sdkMsg: SDKNativeMessage = {
        type: 'user',
        uuid: 'interrupt-compact',
        timestamp: '2024-01-15T10:30:00Z',
        message: {
          content: '<local-command-stderr>Error: Compaction canceled.</local-command-stderr>',
        },
      };

      const chatMsg = parseSDKMessageToChat(sdkMsg);
      expect(chatMsg).not.toBeNull();
      expect(chatMsg!.isInterrupt).toBe(true);
    });
  });

  describe('loadSDKSessionMessages - merge edge cases', () => {
    it('merges assistant content blocks when first has no content blocks', async () => {
      mockExistsSync.mockReturnValue(true);
      mockFsPromises.readFile.mockResolvedValue([
        '{"type":"assistant","uuid":"a1","timestamp":"2024-01-15T10:00:00Z","message":{"content":[{"type":"tool_use","id":"t1","name":"Bash","input":{}}]}}',
        '{"type":"assistant","uuid":"a2","timestamp":"2024-01-15T10:00:01Z","message":{"content":[{"type":"thinking","thinking":"hmm"},{"type":"text","text":"Result here"}]}}',
      ].join('\n'));

      const result = await loadSDKSessionMessages('/Users/test/vault', 'session-merge-blocks');

      expect(result.messages).toHaveLength(1);
      // Merged: tool call from first + content blocks from both
      expect(result.messages[0].toolCalls).toHaveLength(1);
      expect(result.messages[0].contentBlocks!.length).toBeGreaterThanOrEqual(2);
    });

    it('merges assistant with empty target content', async () => {
      mockExistsSync.mockReturnValue(true);
      mockFsPromises.readFile.mockResolvedValue([
        // First assistant: only tool_use, no text
        '{"type":"assistant","uuid":"a1","timestamp":"2024-01-15T10:00:00Z","message":{"content":[{"type":"tool_use","id":"t1","name":"Bash","input":{}}]}}',
        // Second assistant: has text content
        '{"type":"assistant","uuid":"a2","timestamp":"2024-01-15T10:00:01Z","message":{"content":[{"type":"text","text":"Here is the result"}]}}',
      ].join('\n'));

      const result = await loadSDKSessionMessages('/Users/test/vault', 'session-merge-empty-target');

      expect(result.messages).toHaveLength(1);
      expect(result.messages[0].content).toBe('Here is the result');
    });

    it('handles multiple user images in a message', async () => {
      mockExistsSync.mockReturnValue(true);
      mockFsPromises.readFile.mockResolvedValue([
        JSON.stringify({
          type: 'user',
          uuid: 'u-imgs',
          timestamp: '2024-01-15T10:00:00Z',
          message: {
            content: [
              { type: 'text', text: 'Check these images' },
              { type: 'image', source: { type: 'base64', media_type: 'image/png', data: 'abc123' } },
              { type: 'image', source: { type: 'base64', media_type: 'image/jpeg', data: 'def456' } },
            ],
          },
        }),
      ].join('\n'));

      const result = await loadSDKSessionMessages('/Users/test/vault', 'session-multi-images');

      expect(result.messages).toHaveLength(1);
      expect(result.messages[0].images).toHaveLength(2);
      expect(result.messages[0].images![0].mediaType).toBe('image/png');
      expect(result.messages[0].images![1].mediaType).toBe('image/jpeg');
      expect(result.messages[0].images![1].name).toBe('image-2');
    });

    it('extracts text from Agent tool results with array content', async () => {
      mockExistsSync.mockReturnValue(true);
      mockFsPromises.readFile.mockResolvedValue([
        JSON.stringify({
          type: 'user', uuid: 'u1', timestamp: '2024-01-15T10:00:00Z',
          message: { content: 'Use an agent' },
        }),
        JSON.stringify({
          type: 'assistant', uuid: 'a1', parentUuid: 'u1', timestamp: '2024-01-15T10:00:01Z',
          message: { content: [{ type: 'tool_use', id: 'agent-1', name: 'Agent', input: { description: 'test', prompt: 'do stuff' } }] },
        }),
        // Agent tool result has array content (not string)
        JSON.stringify({
          type: 'user', uuid: 'tr1', parentUuid: 'a1', timestamp: '2024-01-15T10:00:30Z',
          toolUseResult: { status: 'completed', agentId: 'abc123' },
          message: { content: [{
            type: 'tool_result', tool_use_id: 'agent-1', is_error: false,
            content: [
              { type: 'text', text: 'Agent completed the task successfully.' },
              { type: 'text', text: 'agentId: abc123\n<usage>total_tokens: 500</usage>' },
            ],
          }] },
        }),
        JSON.stringify({
          type: 'assistant', uuid: 'a2', parentUuid: 'tr1', timestamp: '2024-01-15T10:00:31Z',
          message: { content: [{ type: 'text', text: 'Done.' }] },
        }),
      ].join('\n'));

      const result = await loadSDKSessionMessages('/Users/test/vault', 'session-agent-result');

      // The Agent tool call should have extracted text, not JSON.stringify'd array
      const assistantMsg = result.messages.find(m => m.role === 'assistant' && m.toolCalls?.length);
      expect(assistantMsg).toBeDefined();
      const agentToolCall = assistantMsg!.toolCalls!.find(tc => tc.name === 'Agent');
      expect(agentToolCall).toBeDefined();
      expect(agentToolCall!.result).toBe(
        'Agent completed the task successfully.\nagentId: abc123\n<usage>total_tokens: 500</usage>'
      );
      // Must NOT contain JSON artifacts
      expect(agentToolCall!.result).not.toContain('"type":"text"');
    });
  });

  describe('filterActiveBranch', () => {
    it('returns all entries for linear chain without resumeSessionAt', () => {
      const entries: SDKNativeMessage[] = [
        { type: 'user', uuid: 'u1', parentUuid: null },
        { type: 'assistant', uuid: 'a1', parentUuid: 'u1' },
        { type: 'user', uuid: 'u2', parentUuid: 'a1' },
        { type: 'assistant', uuid: 'a2', parentUuid: 'u2' },
      ];

      const result = filterActiveBranch(entries);

      expect(result).toHaveLength(4);
      expect(result).toEqual(entries);
    });

    it('truncates linear chain at resumeSessionAt UUID', () => {
      const entries: SDKNativeMessage[] = [
        { type: 'user', uuid: 'u1', parentUuid: null },
        { type: 'assistant', uuid: 'a1', parentUuid: 'u1' },
        { type: 'user', uuid: 'u2', parentUuid: 'a1' },
        { type: 'assistant', uuid: 'a2', parentUuid: 'u2' },
      ];

      const result = filterActiveBranch(entries, 'a1');

      expect(result).toHaveLength(2);
      expect(result.map(e => e.uuid)).toEqual(['u1', 'a1']);
    });

    it('returns only new branch after rewind + follow-up', () => {
      // Original: u1 → a1 → u2 → a2
      // Rewind to a1, follow-up: u3 → a3 (u3.parentUuid = a1)
      const entries: SDKNativeMessage[] = [
        { type: 'user', uuid: 'u1', parentUuid: null },
        { type: 'assistant', uuid: 'a1', parentUuid: 'u1' },
        { type: 'user', uuid: 'u2', parentUuid: 'a1' },
        { type: 'assistant', uuid: 'a2', parentUuid: 'u2' },
        { type: 'user', uuid: 'u3', parentUuid: 'a1' },    // Branch point: a1 has 2 children
        { type: 'assistant', uuid: 'a3', parentUuid: 'u3' },
      ];

      const result = filterActiveBranch(entries);

      // Should include: u1, a1, u3, a3 (new branch), not u2, a2
      expect(result.map(e => e.uuid)).toEqual(['u1', 'a1', 'u3', 'a3']);
    });

    it('returns latest branch after multiple rewinds', () => {
      // Original: u1 → a1 → u2 → a2
      // Rewind 1: u3 → a3 (parent a1)
      // Rewind 2: u4 → a4 (parent a1) — third child of a1
      const entries: SDKNativeMessage[] = [
        { type: 'user', uuid: 'u1', parentUuid: null },
        { type: 'assistant', uuid: 'a1', parentUuid: 'u1' },
        { type: 'user', uuid: 'u2', parentUuid: 'a1' },
        { type: 'assistant', uuid: 'a2', parentUuid: 'u2' },
        { type: 'user', uuid: 'u3', parentUuid: 'a1' },
        { type: 'assistant', uuid: 'a3', parentUuid: 'u3' },
        { type: 'user', uuid: 'u4', parentUuid: 'a1' },
        { type: 'assistant', uuid: 'a4', parentUuid: 'u4' },
      ];

      const result = filterActiveBranch(entries);

      // Last entry with uuid is a4, walk back: a4 → u4 → a1 → u1
      expect(result.map(e => e.uuid)).toEqual(['u1', 'a1', 'u4', 'a4']);
    });

    it('returns all entries when resumeSessionAt UUID not found (safety)', () => {
      const entries: SDKNativeMessage[] = [
        { type: 'user', uuid: 'u1', parentUuid: null },
        { type: 'assistant', uuid: 'a1', parentUuid: 'u1' },
      ];

      const result = filterActiveBranch(entries, 'nonexistent-uuid');

      expect(result).toHaveLength(2);
      expect(result).toEqual(entries);
    });

    it('returns empty for empty entries', () => {
      const result = filterActiveBranch([]);
      expect(result).toEqual([]);
    });

    it('does not misdetect branching when duplicate uuid entries exist', () => {
      // SDK may write the same message twice (e.g., around compaction).
      // Without dedup, duplicate entries inflate childCount causing false branch detection.
      const entries: SDKNativeMessage[] = [
        { type: 'user', uuid: 'u1', parentUuid: null },
        { type: 'assistant', uuid: 'a1', parentUuid: 'u1' },
        { type: 'user', uuid: 'u2', parentUuid: 'a1' },
        { type: 'assistant', uuid: 'a2', parentUuid: 'u2' },
        // Duplicate of u2 — SDK writes this again
        { type: 'user', uuid: 'u2', parentUuid: 'a1' },
      ];

      // Without dedup fix, a1 would have childCount=2 (u2 counted twice),
      // triggering branch detection and excluding u2/a2.
      const result = filterActiveBranch(entries);

      // Should be a no-op (linear chain, no branching)
      expect(result).toHaveLength(4);
      expect(result.map(e => e.uuid)).toEqual(['u1', 'a1', 'u2', 'a2']);
    });

    it('correctly truncates at resumeSessionAt when duplicates exist', () => {
      const entries: SDKNativeMessage[] = [
        { type: 'user', uuid: 'u1', parentUuid: null },
        { type: 'assistant', uuid: 'a1', parentUuid: 'u1' },
        { type: 'user', uuid: 'u2', parentUuid: 'a1' },
        { type: 'assistant', uuid: 'a2', parentUuid: 'u2' },
        // Duplicate of u2
        { type: 'user', uuid: 'u2', parentUuid: 'a1' },
      ];

      const result = filterActiveBranch(entries, 'a1');

      // Should truncate at a1, including only u1 and a1
      expect(result).toHaveLength(2);
      expect(result.map(e => e.uuid)).toEqual(['u1', 'a1']);
    });

    it('preserves no-uuid entries within active branch region', () => {
      const entries: SDKNativeMessage[] = [
        { type: 'user', uuid: 'u1', parentUuid: null },
        { type: 'queue-operation' },  // No uuid — between u1 (active) and a1 (active)
        { type: 'assistant', uuid: 'a1', parentUuid: 'u1' },
        { type: 'user', uuid: 'u2', parentUuid: 'a1' },
        { type: 'assistant', uuid: 'a2', parentUuid: 'u2' },
        { type: 'user', uuid: 'u3', parentUuid: 'a1' },    // Branch
        { type: 'assistant', uuid: 'a3', parentUuid: 'u3' },
      ];

      const result = filterActiveBranch(entries);

      const uuids = result.filter(e => e.uuid).map(e => e.uuid);
      expect(uuids).toEqual(['u1', 'a1', 'u3', 'a3']);
      // queue-operation is between u1 (active) and a1 (active), so preserved
      expect(result.some(e => e.type === 'queue-operation')).toBe(true);
    });

    it('truncates at resumeSessionAt on latest branch when branching exists', () => {
      // Rewind 1 + follow-up created a branch: u3/a3 branch off a1
      // Rewind 2 on the new branch (no follow-up): resumeSessionAt = a1
      // On reload, should truncate at a1, not show u3/a3
      const entries: SDKNativeMessage[] = [
        { type: 'user', uuid: 'u1', parentUuid: null },
        { type: 'assistant', uuid: 'a1', parentUuid: 'u1' },
        { type: 'user', uuid: 'u2', parentUuid: 'a1' },       // old branch
        { type: 'assistant', uuid: 'a2', parentUuid: 'u2' },   // old branch
        { type: 'user', uuid: 'u3', parentUuid: 'a1' },        // new branch (from rewind 1)
        { type: 'assistant', uuid: 'a3', parentUuid: 'u3' },   // new branch
      ];

      // Rewind 2 on new branch: truncate at a1
      const result = filterActiveBranch(entries, 'a1');

      expect(result.map(e => e.uuid)).toEqual(['u1', 'a1']);
    });

    it('truncates at resumeSessionAt mid-branch when branching exists', () => {
      // Branch from a1: old (u2→a2) and new (u3→a3→u4→a4)
      // Rewind on new branch to u4: resumeSessionAt = a3
      const entries: SDKNativeMessage[] = [
        { type: 'user', uuid: 'u1', parentUuid: null },
        { type: 'assistant', uuid: 'a1', parentUuid: 'u1' },
        { type: 'user', uuid: 'u2', parentUuid: 'a1' },
        { type: 'assistant', uuid: 'a2', parentUuid: 'u2' },
        { type: 'user', uuid: 'u3', parentUuid: 'a1' },
        { type: 'assistant', uuid: 'a3', parentUuid: 'u3' },
        { type: 'user', uuid: 'u4', parentUuid: 'a3' },
        { type: 'assistant', uuid: 'a4', parentUuid: 'u4' },
      ];

      const result = filterActiveBranch(entries, 'a3');

      expect(result.map(e => e.uuid)).toEqual(['u1', 'a1', 'u3', 'a3']);
    });

    it('ignores resumeSessionAt not on latest branch', () => {
      // Branch from a1: old (u2→a2) and new (u3→a3)
      // resumeSessionAt points to a2 (on the OLD branch) — should be ignored
      const entries: SDKNativeMessage[] = [
        { type: 'user', uuid: 'u1', parentUuid: null },
        { type: 'assistant', uuid: 'a1', parentUuid: 'u1' },
        { type: 'user', uuid: 'u2', parentUuid: 'a1' },
        { type: 'assistant', uuid: 'a2', parentUuid: 'u2' },
        { type: 'user', uuid: 'u3', parentUuid: 'a1' },
        { type: 'assistant', uuid: 'a3', parentUuid: 'u3' },
      ];

      // a2 is on old branch, not an ancestor of leaf a3
      const result = filterActiveBranch(entries, 'a2');

      // Should return full latest branch (ignoring stale resumeSessionAt)
      expect(result.map(e => e.uuid)).toEqual(['u1', 'a1', 'u3', 'a3']);
    });

    it('drops no-uuid entries in old branch region', () => {
      const entries: SDKNativeMessage[] = [
        { type: 'user', uuid: 'u1', parentUuid: null },
        { type: 'assistant', uuid: 'a1', parentUuid: 'u1' },
        { type: 'user', uuid: 'u2', parentUuid: 'a1' },
        { type: 'assistant', uuid: 'a2', parentUuid: 'u2' },
        { type: 'queue-operation' },  // No uuid — between a2 (old) and u3 (active)
        { type: 'user', uuid: 'u3', parentUuid: 'a1' },    // Branch
        { type: 'assistant', uuid: 'a3', parentUuid: 'u3' },
      ];

      const result = filterActiveBranch(entries);

      const uuids = result.filter(e => e.uuid).map(e => e.uuid);
      expect(uuids).toEqual(['u1', 'a1', 'u3', 'a3']);
      // queue-operation between a2 (not active) and u3 (active) — should be dropped
      expect(result.some(e => e.type === 'queue-operation')).toBe(false);
    });

    it('excludes progress entries and does not treat them as branches', () => {
      // Simulates Agent tool call: assistant issues tool_use, SDK writes progress chain,
      // then next user message is parented to end of progress chain.
      // Without fix: progress creates false branching, losing the conversation branch.
      const entries: SDKNativeMessage[] = [
        { type: 'user', uuid: 'u1', parentUuid: null },
        { type: 'assistant', uuid: 'a1', parentUuid: 'u1' },
        // a1 is a tool_use (Agent). SDK writes tool_result + progress chain as siblings:
        { type: 'user', uuid: 'tr1', parentUuid: 'a1', toolUseResult: {} },  // tool result
        { type: 'assistant', uuid: 'a2', parentUuid: 'tr1' },  // response after tool
        // Progress chain branching off a1 (subagent execution logs):
        { type: 'progress' as SDKNativeMessage['type'], uuid: 'p1', parentUuid: 'a1' },
        { type: 'progress' as SDKNativeMessage['type'], uuid: 'p2', parentUuid: 'p1' },
        { type: 'progress' as SDKNativeMessage['type'], uuid: 'p3', parentUuid: 'p2' },
        // Next conversation message parented to end of progress chain:
        { type: 'user', uuid: 'u2', parentUuid: 'p3' },
        { type: 'assistant', uuid: 'a3', parentUuid: 'u2' },
      ];

      const result = filterActiveBranch(entries);
      const uuids = result.filter(e => e.uuid).map(e => e.uuid);

      // All conversation entries should be present, progress entries excluded
      expect(uuids).toEqual(['u1', 'a1', 'tr1', 'a2', 'u2', 'a3']);
      expect(result.every(e => (e.type as string) !== 'progress')).toBe(true);
    });

    it('reparents through long progress chains to preserve full conversation', () => {
      // Two turns, each with Agent tool calls generating progress entries.
      // The second turn's user message is parented to the end of the first progress chain.
      const entries: SDKNativeMessage[] = [
        { type: 'user', uuid: 'u1', parentUuid: null },
        { type: 'assistant', uuid: 'a1-think', parentUuid: 'u1' },
        { type: 'assistant', uuid: 'a1-tool', parentUuid: 'a1-think' },  // Agent tool_use
        // Conversation branch: tool result → assistant response
        { type: 'user', uuid: 'tr1', parentUuid: 'a1-tool', toolUseResult: {} },
        { type: 'assistant', uuid: 'a1-think2', parentUuid: 'tr1' },
        { type: 'assistant', uuid: 'a1-text', parentUuid: 'a1-think2' },
        // Progress chain off a1-tool:
        { type: 'progress' as SDKNativeMessage['type'], uuid: 'p1', parentUuid: 'a1-tool' },
        { type: 'progress' as SDKNativeMessage['type'], uuid: 'p2', parentUuid: 'p1' },
        // System entry chained to progress:
        { type: 'system', uuid: 'sys1', parentUuid: 'p2' },
        // Second turn parented to system (which is parented to progress chain):
        { type: 'user', uuid: 'u2', parentUuid: 'sys1' },
        { type: 'assistant', uuid: 'a2', parentUuid: 'u2' },
      ];

      const result = filterActiveBranch(entries);
      const uuids = result.filter(e => e.uuid).map(e => e.uuid);

      // Must include BOTH turns' content — nothing lost
      expect(uuids).toContain('a1-text');  // First turn's response
      expect(uuids).toContain('u2');       // Second turn's input
      expect(uuids).toContain('a2');       // Second turn's response
      // Progress entries must be excluded
      expect(uuids).not.toContain('p1');
      expect(uuids).not.toContain('p2');
    });

    it('does not treat parallel tool calls as branches', () => {
      // Assistant sends two tool_use blocks in parallel. SDK writes them as
      // separate entries. Their tool results are parented to respective entries.
      const entries: SDKNativeMessage[] = [
        { type: 'user', uuid: 'u1', parentUuid: null },
        { type: 'assistant', uuid: 'a1-text', parentUuid: 'u1' },
        { type: 'assistant', uuid: 'a1-tool1', parentUuid: 'a1-text' },  // first tool_use
        { type: 'assistant', uuid: 'a1-tool2', parentUuid: 'a1-text' },  // second tool_use (parallel)
        // Tool results:
        { type: 'user', uuid: 'tr1', parentUuid: 'a1-tool1', toolUseResult: {} },
        { type: 'user', uuid: 'tr2', parentUuid: 'a1-tool2', toolUseResult: {} },
        // Assistant continues after both results:
        { type: 'assistant', uuid: 'a2', parentUuid: 'tr2' },
      ];

      const result = filterActiveBranch(entries);
      const uuids = result.filter(e => e.uuid).map(e => e.uuid);

      // Both tool calls and their results should be present
      expect(uuids).toContain('a1-tool1');
      expect(uuids).toContain('a1-tool2');
      expect(uuids).toContain('tr1');
      expect(uuids).toContain('tr2');
      expect(uuids).toContain('a2');
    });

    it('handles real rewind alongside progress entries', () => {
      // Turn 1 with Agent tool (progress entries), then a real rewind at a1.
      const entries: SDKNativeMessage[] = [
        { type: 'user', uuid: 'u1', parentUuid: null },
        { type: 'assistant', uuid: 'a1', parentUuid: 'u1' },
        // Original continuation:
        { type: 'user', uuid: 'u2-old', parentUuid: 'a1' },
        { type: 'assistant', uuid: 'a2-old', parentUuid: 'u2-old' },
        // Progress entries off a1:
        { type: 'progress' as SDKNativeMessage['type'], uuid: 'p1', parentUuid: 'a1' },
        { type: 'progress' as SDKNativeMessage['type'], uuid: 'p2', parentUuid: 'p1' },
        // Rewind: new user message also branching off a1
        { type: 'user', uuid: 'u2-new', parentUuid: 'a1' },
        { type: 'assistant', uuid: 'a2-new', parentUuid: 'u2-new' },
      ];

      const result = filterActiveBranch(entries);
      const uuids = result.filter(e => e.uuid).map(e => e.uuid);

      // Should follow the latest branch (u2-new), not the old one or progress
      expect(uuids).toEqual(['u1', 'a1', 'u2-new', 'a2-new']);
    });

    it('detects rewind when abandoned path continues through assistant/tool nodes', () => {
      const entries: SDKNativeMessage[] = [
        { type: 'user', uuid: 'u1', parentUuid: null },
        { type: 'assistant', uuid: 'a1', parentUuid: 'u1' },
        { type: 'assistant', uuid: 'a1-tool', parentUuid: 'a1' },
        { type: 'user', uuid: 'tr1', parentUuid: 'a1-tool', toolUseResult: {} },
        { type: 'assistant', uuid: 'a2', parentUuid: 'tr1' },
        { type: 'user', uuid: 'u2-new', parentUuid: 'a1' },
        { type: 'assistant', uuid: 'a3-new', parentUuid: 'u2-new' },
      ];

      const result = filterActiveBranch(entries);
      const uuids = result.filter(e => e.uuid).map(e => e.uuid);

      expect(uuids).toEqual(['u1', 'a1', 'u2-new', 'a3-new']);
    });

    it('preserves earlier parallel tool-result descendants when a later rewind exists', () => {
      const entries: SDKNativeMessage[] = [
        { type: 'user', uuid: 'u1', parentUuid: null },
        { type: 'assistant', uuid: 'a1-text', parentUuid: 'u1' },
        { type: 'assistant', uuid: 'a1-tool1', parentUuid: 'a1-text' },
        { type: 'assistant', uuid: 'a1-tool2', parentUuid: 'a1-text' },
        { type: 'user', uuid: 'tr1', parentUuid: 'a1-tool1', toolUseResult: {} },
        { type: 'user', uuid: 'tr2', parentUuid: 'a1-tool2', toolUseResult: {} },
        { type: 'assistant', uuid: 'a2', parentUuid: 'tr2' },
        { type: 'user', uuid: 'u3-old', parentUuid: 'a2' },
        { type: 'assistant', uuid: 'a3-old', parentUuid: 'u3-old' },
        { type: 'user', uuid: 'u3-new', parentUuid: 'a2' },
        { type: 'assistant', uuid: 'a3-new', parentUuid: 'u3-new' },
      ];

      const result = filterActiveBranch(entries);
      const uuids = result.filter(e => e.uuid).map(e => e.uuid);

      expect(uuids).toEqual([
        'u1',
        'a1-text',
        'a1-tool1',
        'a1-tool2',
        'tr1',
        'tr2',
        'a2',
        'u3-new',
        'a3-new',
      ]);
    });
  });

  describe('loadSDKSessionMessages with resumeSessionAt', () => {
    it('returns identical behavior without resumeSessionAt', async () => {
      mockExistsSync.mockReturnValue(true);
      mockFsPromises.readFile.mockResolvedValue([
        '{"type":"user","uuid":"u1","timestamp":"2024-01-15T10:00:00Z","message":{"content":"Hello"}}',
        '{"type":"assistant","uuid":"a1","parentUuid":"u1","timestamp":"2024-01-15T10:01:00Z","message":{"content":[{"type":"text","text":"Hi!"}]}}',
      ].join('\n'));

      const result = await loadSDKSessionMessages('/Users/test/vault', 'session-no-resume');

      expect(result.messages).toHaveLength(2);
    });

    it('truncates messages at resumeSessionAt on linear JSONL', async () => {
      mockExistsSync.mockReturnValue(true);
      mockFsPromises.readFile.mockResolvedValue([
        '{"type":"user","uuid":"u1","timestamp":"2024-01-15T10:00:00Z","message":{"content":"Hello"}}',
        '{"type":"assistant","uuid":"a1","parentUuid":"u1","timestamp":"2024-01-15T10:01:00Z","message":{"content":[{"type":"text","text":"Hi!"}]}}',
        '{"type":"user","uuid":"u2","parentUuid":"a1","timestamp":"2024-01-15T10:02:00Z","message":{"content":"More"}}',
        '{"type":"assistant","uuid":"a2","parentUuid":"u2","timestamp":"2024-01-15T10:03:00Z","message":{"content":[{"type":"text","text":"More response"}]}}',
      ].join('\n'));

      const result = await loadSDKSessionMessages('/Users/test/vault', 'session-truncate', 'a1');

      // Should only have u1 and a1 (truncated at a1)
      expect(result.messages).toHaveLength(2);
      expect(result.messages[0].content).toBe('Hello');
      expect(result.messages[1].content).toBe('Hi!');
    });

    it('returns correct active branch on branched JSONL', async () => {
      mockExistsSync.mockReturnValue(true);
      mockFsPromises.readFile.mockResolvedValue([
        '{"type":"user","uuid":"u1","timestamp":"2024-01-15T10:00:00Z","message":{"content":"Hello"}}',
        '{"type":"assistant","uuid":"a1","parentUuid":"u1","timestamp":"2024-01-15T10:01:00Z","message":{"content":[{"type":"text","text":"Hi!"}]}}',
        '{"type":"user","uuid":"u2","parentUuid":"a1","timestamp":"2024-01-15T10:02:00Z","message":{"content":"Old branch"}}',
        '{"type":"assistant","uuid":"a2","parentUuid":"u2","timestamp":"2024-01-15T10:03:00Z","message":{"content":[{"type":"text","text":"Old response"}]}}',
        '{"type":"user","uuid":"u3","parentUuid":"a1","timestamp":"2024-01-15T10:04:00Z","message":{"content":"New branch"}}',
        '{"type":"assistant","uuid":"a3","parentUuid":"u3","timestamp":"2024-01-15T10:05:00Z","message":{"content":[{"type":"text","text":"New response"}]}}',
      ].join('\n'));

      const result = await loadSDKSessionMessages('/Users/test/vault', 'session-branched');

      // Should have: u1 "Hello", a1 "Hi!", u3 "New branch", a3 "New response"
      // Old branch (u2, a2) should be excluded
      expect(result.messages).toHaveLength(4);
      expect(result.messages[0].content).toBe('Hello');
      expect(result.messages[1].content).toBe('Hi!');
      expect(result.messages[2].content).toBe('New branch');
      expect(result.messages[3].content).toBe('New response');
    });
  });

  describe('extractToolResultContent', () => {
    it('passes through string content unchanged', () => {
      expect(extractToolResultContent('hello world')).toBe('hello world');
    });

    it('extracts text from array of content blocks (Agent results)', () => {
      const content = [
        { type: 'text', text: 'First block of output.' },
        { type: 'text', text: 'agentId: abc123\n<usage>total_tokens: 1000</usage>' },
      ];
      expect(extractToolResultContent(content)).toBe(
        'First block of output.\nagentId: abc123\n<usage>total_tokens: 1000</usage>'
      );
    });

    it('skips non-text blocks in array content', () => {
      const content = [
        { type: 'image', source: { type: 'base64', data: 'abc' } },
        { type: 'text', text: 'The only text.' },
      ];
      expect(extractToolResultContent(content)).toBe('The only text.');
    });

    it('returns empty string for null/undefined content', () => {
      expect(extractToolResultContent(null)).toBe('');
      expect(extractToolResultContent(undefined)).toBe('');
    });

    it('JSON-stringifies unknown content types as fallback', () => {
      expect(extractToolResultContent({ custom: 'value' })).toBe('{"custom":"value"}');
    });

    it('handles empty array content', () => {
      expect(extractToolResultContent([])).toBe('');
    });

    it('JSON-stringifies non-empty array with no text blocks (e.g. tool_reference)', () => {
      const content = [
        { type: 'tool_reference', tool_name: 'WebSearch' },
        { type: 'tool_reference', tool_name: 'Grep' },
      ];
      expect(extractToolResultContent(content)).toBe(JSON.stringify(content));
    });

    it('JSON-stringifies non-empty array with no text blocks using fallbackIndent', () => {
      const content = [
        { type: 'tool_reference', tool_name: 'Read' },
      ];
      expect(extractToolResultContent(content, { fallbackIndent: 2 })).toBe(
        JSON.stringify(content, null, 2)
      );
    });
  });

  describe('collectAsyncSubagentResults', () => {
    it('extracts task-notification data from queue-operation enqueue entries', () => {
      const entries: SDKNativeMessage[] = [
        {
          type: 'queue-operation',
          operation: 'enqueue',
          content: `<task-notification>
<task-id>ae5eb9a</task-id>
<status>completed</status>
<summary>Agent "Review code" completed</summary>
<result>Found 3 issues in the codebase.

1. Missing error handling in auth module.
2. Unused import in utils.ts.
3. Race condition in fetchData.</result>
</task-notification>`,
        },
      ];

      const results = collectAsyncSubagentResults(entries);

      expect(results.size).toBe(1);
      const entry = results.get('ae5eb9a')!;
      expect(entry.status).toBe('completed');
      expect(entry.result).toContain('Found 3 issues');
      expect(entry.result).toContain('Race condition in fetchData.');
    });

    it('collects multiple queue-operation entries', () => {
      const entries: SDKNativeMessage[] = [
        {
          type: 'queue-operation',
          operation: 'enqueue',
          content: '<task-notification><task-id>agent-1</task-id><status>completed</status><result>Result 1</result></task-notification>',
        },
        {
          type: 'queue-operation',
          operation: 'enqueue',
          content: '<task-notification><task-id>agent-2</task-id><status>error</status><result>Task failed</result></task-notification>',
        },
      ];

      const results = collectAsyncSubagentResults(entries);

      expect(results.size).toBe(2);
      expect(results.get('agent-1')!.status).toBe('completed');
      expect(results.get('agent-2')!.status).toBe('error');
      expect(results.get('agent-2')!.result).toBe('Task failed');
    });

    it('skips dequeue operations', () => {
      const entries: SDKNativeMessage[] = [
        {
          type: 'queue-operation',
          operation: 'dequeue',
          sessionId: 'session-1',
        },
      ];

      const results = collectAsyncSubagentResults(entries);
      expect(results.size).toBe(0);
    });

    it('skips entries without task-notification content', () => {
      const entries: SDKNativeMessage[] = [
        {
          type: 'queue-operation',
          operation: 'enqueue',
          content: 'some other content',
        },
      ];

      const results = collectAsyncSubagentResults(entries);
      expect(results.size).toBe(0);
    });

    it('skips entries without task-id or result', () => {
      const entries: SDKNativeMessage[] = [
        {
          type: 'queue-operation',
          operation: 'enqueue',
          content: '<task-notification><status>completed</status><result>No task-id</result></task-notification>',
        },
        {
          type: 'queue-operation',
          operation: 'enqueue',
          content: '<task-notification><task-id>has-id</task-id><status>completed</status></task-notification>',
        },
      ];

      const results = collectAsyncSubagentResults(entries);
      expect(results.size).toBe(0);
    });

    it('defaults status to completed when status tag is missing', () => {
      const entries: SDKNativeMessage[] = [
        {
          type: 'queue-operation',
          operation: 'enqueue',
          content: '<task-notification><task-id>no-status</task-id><result>Done</result></task-notification>',
        },
      ];

      const results = collectAsyncSubagentResults(entries);
      expect(results.get('no-status')!.status).toBe('completed');
    });

    it('ignores non-queue-operation messages', () => {
      const entries: SDKNativeMessage[] = [
        { type: 'user', uuid: 'u1', message: { content: 'hello' } },
        { type: 'assistant', uuid: 'a1', message: { content: [{ type: 'text', text: 'hi' }] } },
      ];

      const results = collectAsyncSubagentResults(entries);
      expect(results.size).toBe(0);
    });
  });

  describe('resolveToolUseResultStatus', () => {
    it('preserves orphaned fallback when the tool result has no stronger signal', () => {
      expect(resolveToolUseResultStatus(undefined, 'orphaned')).toBe('orphaned');
      expect(resolveToolUseResultStatus({ status: 'unknown' }, 'orphaned')).toBe('orphaned');
    });
  });

  describe('loadSDKSessionMessages - async subagent hydration', () => {
    it('populates toolCall.subagent for async Task tools from queue-operation results', async () => {
      mockExistsSync.mockReturnValue(true);
      mockFsPromises.readFile.mockImplementation(async (filePath: any) => {
        const p = String(filePath);
        if (p.endsWith('.jsonl') && !p.includes('subagents')) {
          return [
            '{"type":"user","uuid":"u1","timestamp":"2024-01-15T10:00:00Z","message":{"content":"Run background task"}}',
            // Assistant spawns async Task
            '{"type":"assistant","uuid":"a1","timestamp":"2024-01-15T10:01:00Z","message":{"content":[{"type":"tool_use","id":"task-1","name":"Task","input":{"description":"Review code","prompt":"Check for bugs","run_in_background":true}}]}}',
            // Task tool_result with agentId (SDK launch shape)
            `{"type":"user","uuid":"u2","timestamp":"2024-01-15T10:01:01Z","toolUseResult":{"isAsync":true,"agentId":"ae5eb9a","status":"async_launched","description":"Review code","prompt":"Check for bugs","outputFile":"/tmp/agent.output"},"message":{"content":[{"type":"tool_result","tool_use_id":"task-1","content":"Task launched in background."}]}}`,
            // Queue-operation with full result
            `{"type":"queue-operation","operation":"enqueue","content":"<task-notification><task-id>ae5eb9a</task-id><status>completed</status><summary>Agent completed</summary><result>Found 3 issues:\\n1. Missing error handling\\n2. Unused import\\n3. Race condition</result></task-notification>"}`,
            // Assistant continues after
            '{"type":"assistant","uuid":"a2","timestamp":"2024-01-15T10:05:00Z","message":{"content":[{"type":"text","text":"The review found 3 issues."}]}}',
          ].join('\n');
        }
        // Subagent sidecar file
        return '';
      });

      const result = await loadSDKSessionMessages('/Users/test/vault', 'session-async-hydrate');

      // Should have: user message, merged assistant with Task tool, assistant follow-up
      expect(result.messages.length).toBeGreaterThanOrEqual(2);

      const assistantMsg = result.messages.find(m => m.role === 'assistant' && m.toolCalls?.some(tc => tc.name === 'Task'));
      expect(assistantMsg).toBeDefined();

      const taskToolCall = assistantMsg!.toolCalls!.find(tc => tc.name === 'Task')!;
      expect(taskToolCall.subagent).toBeDefined();
      expect(taskToolCall.subagent!.mode).toBe('async');
      expect(taskToolCall.subagent!.agentId).toBe('ae5eb9a');
      expect(taskToolCall.subagent!.status).toBe('completed');
      expect(taskToolCall.subagent!.asyncStatus).toBe('completed');
      expect(taskToolCall.subagent!.result).toContain('Found 3 issues');
      expect(taskToolCall.subagent!.result).toContain('Race condition');
      // toolCall.result should also be updated
      expect(taskToolCall.result).toContain('Found 3 issues');
      expect(taskToolCall.status).toBe('completed');
    });

    it('prefers queue-operation completion over misleading async tool_result error flags', async () => {
      mockExistsSync.mockReturnValue(true);
      mockFsPromises.readFile.mockImplementation(async (filePath: any) => {
        const p = String(filePath);
        if (p.endsWith('.jsonl') && !p.includes('subagents')) {
          return [
            '{"type":"user","uuid":"u1","timestamp":"2024-01-15T10:00:00Z","message":{"content":"Run background task"}}',
            '{"type":"assistant","uuid":"a1","timestamp":"2024-01-15T10:01:00Z","message":{"content":[{"type":"tool_use","id":"task-1","name":"Task","input":{"description":"Review code","prompt":"Check for bugs","run_in_background":true}}]}}',
            `{"type":"user","uuid":"u2","timestamp":"2024-01-15T10:01:01Z","toolUseResult":{"isAsync":true,"agentId":"ae5eb9a","status":"async_launched"},"message":{"content":[{"type":"tool_result","tool_use_id":"task-1","content":"Task launched in background.","is_error":true}]}}`,
            `{"type":"queue-operation","operation":"enqueue","content":"<task-notification><task-id>ae5eb9a</task-id><status>completed</status><summary>Agent completed</summary><result>Background work finished cleanly</result></task-notification>"}`,
          ].join('\n');
        }
        return '';
      });

      const result = await loadSDKSessionMessages('/Users/test/vault', 'session-async-error-flag');

      const assistantMsg = result.messages.find(m => m.role === 'assistant' && m.toolCalls?.some(tc => tc.name === 'Task'));
      const taskToolCall = assistantMsg!.toolCalls!.find(tc => tc.name === 'Task')!;

      expect(taskToolCall.subagent).toBeDefined();
      expect(taskToolCall.subagent!.status).toBe('completed');
      expect(taskToolCall.subagent!.asyncStatus).toBe('completed');
      expect(taskToolCall.subagent!.result).toBe('Background work finished cleanly');
      expect(taskToolCall.status).toBe('completed');
      expect(taskToolCall.result).toBe('Background work finished cleanly');
    });

    it('uses truncated API result when no queue-operation exists', async () => {
      mockExistsSync.mockReturnValue(true);
      mockFsPromises.readFile.mockImplementation(async (filePath: any) => {
        const p = String(filePath);
        if (p.endsWith('.jsonl') && !p.includes('subagents')) {
          return [
            '{"type":"user","uuid":"u1","timestamp":"2024-01-15T10:00:00Z","message":{"content":"Run task"}}',
            '{"type":"assistant","uuid":"a1","timestamp":"2024-01-15T10:01:00Z","message":{"content":[{"type":"tool_use","id":"task-1","name":"Task","input":{"description":"Test task","prompt":"test","run_in_background":true}}]}}',
            `{"type":"user","uuid":"u2","timestamp":"2024-01-15T10:01:01Z","toolUseResult":{"isAsync":true,"agentId":"abc123"},"message":{"content":[{"type":"tool_result","tool_use_id":"task-1","content":"Task launched."}]}}`,
            // No queue-operation entry
          ].join('\n');
        }
        return '';
      });

      const result = await loadSDKSessionMessages('/Users/test/vault', 'session-no-queue-op');

      const assistantMsg = result.messages.find(m => m.toolCalls?.some(tc => tc.name === 'Task'));
      const taskToolCall = assistantMsg!.toolCalls!.find(tc => tc.name === 'Task')!;

      expect(taskToolCall.subagent).toBeDefined();
      expect(taskToolCall.subagent!.agentId).toBe('abc123');
      expect(taskToolCall.subagent!.status).toBe('running');
      expect(taskToolCall.subagent!.asyncStatus).toBe('running');
      expect(taskToolCall.status).toBe('running');
      // Falls back to the API content (truncated)
      expect(taskToolCall.subagent!.result).toBe('Task launched.');
    });

    it('treats async launch tool results as running even when the content block is flagged as error', async () => {
      mockExistsSync.mockReturnValue(true);
      mockFsPromises.readFile.mockImplementation(async (filePath: any) => {
        const p = String(filePath);
        if (p.endsWith('.jsonl') && !p.includes('subagents')) {
          return [
            '{"type":"user","uuid":"u1","timestamp":"2024-01-15T10:00:00Z","message":{"content":"Run task"}}',
            '{"type":"assistant","uuid":"a1","timestamp":"2024-01-15T10:01:00Z","message":{"content":[{"type":"tool_use","id":"task-1","name":"Task","input":{"description":"Test task","prompt":"test","run_in_background":true}}]}}',
            '{"type":"user","uuid":"u2","timestamp":"2024-01-15T10:01:01Z","toolUseResult":{"isAsync":true,"agentId":"abc123","status":"async_launched"},"message":{"content":[{"type":"tool_result","tool_use_id":"task-1","content":"Task launched in background.","is_error":true}]}}',
          ].join('\n');
        }
        return '';
      });

      const result = await loadSDKSessionMessages('/Users/test/vault', 'session-async-launch-error-flag');

      const assistantMsg = result.messages.find(m => m.toolCalls?.some(tc => tc.name === 'Task'));
      const taskToolCall = assistantMsg!.toolCalls!.find(tc => tc.name === 'Task')!;

      expect(taskToolCall.subagent).toBeDefined();
      expect(taskToolCall.subagent!.status).toBe('running');
      expect(taskToolCall.subagent!.asyncStatus).toBe('running');
      expect(taskToolCall.status).toBe('running');
      expect(taskToolCall.result).toBe('Task launched in background.');
    });

    it('treats queue-operation success status as completed', async () => {
      mockExistsSync.mockReturnValue(true);
      mockFsPromises.readFile.mockImplementation(async (filePath: any) => {
        const p = String(filePath);
        if (p.endsWith('.jsonl') && !p.includes('subagents')) {
          return [
            '{"type":"user","uuid":"u1","timestamp":"2024-01-15T10:00:00Z","message":{"content":"Run task"}}',
            '{"type":"assistant","uuid":"a1","timestamp":"2024-01-15T10:01:00Z","message":{"content":[{"type":"tool_use","id":"task-1","name":"Task","input":{"description":"Test task","prompt":"test","run_in_background":true}}]}}',
            '{"type":"user","uuid":"u2","timestamp":"2024-01-15T10:01:01Z","toolUseResult":{"isAsync":true,"agentId":"abc123","status":"async_launched"},"message":{"content":[{"type":"tool_result","tool_use_id":"task-1","content":"Task launched in background."}]}}',
            '{"type":"queue-operation","operation":"enqueue","content":"<task-notification><task-id>abc123</task-id><status>success</status><result>Background task succeeded</result></task-notification>"}',
          ].join('\n');
        }
        return '';
      });

      const result = await loadSDKSessionMessages('/Users/test/vault', 'session-queue-success');

      const assistantMsg = result.messages.find(m => m.toolCalls?.some(tc => tc.name === 'Task'));
      const taskToolCall = assistantMsg!.toolCalls!.find(tc => tc.name === 'Task')!;

      expect(taskToolCall.subagent).toBeDefined();
      expect(taskToolCall.subagent!.status).toBe('completed');
      expect(taskToolCall.subagent!.asyncStatus).toBe('completed');
      expect(taskToolCall.status).toBe('completed');
      expect(taskToolCall.result).toBe('Background task succeeded');
    });

    it('does not build SubagentInfo for sync Task tools', async () => {
      mockExistsSync.mockReturnValue(true);
      mockFsPromises.readFile.mockImplementation(async (filePath: any) => {
        const p = String(filePath);
        if (p.endsWith('.jsonl') && !p.includes('subagents')) {
          return [
            '{"type":"user","uuid":"u1","timestamp":"2024-01-15T10:00:00Z","message":{"content":"Run sync task"}}',
            '{"type":"assistant","uuid":"a1","timestamp":"2024-01-15T10:01:00Z","message":{"content":[{"type":"tool_use","id":"task-1","name":"Task","input":{"description":"Sync task","prompt":"test","run_in_background":false}}]}}',
            '{"type":"user","uuid":"u2","timestamp":"2024-01-15T10:01:01Z","toolUseResult":{},"message":{"content":[{"type":"tool_result","tool_use_id":"task-1","content":"Sync result"}]}}',
          ].join('\n');
        }
        return '';
      });

      const result = await loadSDKSessionMessages('/Users/test/vault', 'session-sync-task');

      const assistantMsg = result.messages.find(m => m.toolCalls?.some(tc => tc.name === 'Task'));
      const taskToolCall = assistantMsg!.toolCalls!.find(tc => tc.name === 'Task')!;

      // Sync tasks should NOT get SubagentInfo from this pass
      expect(taskToolCall.subagent).toBeUndefined();
    });

    it('loads subagent tool calls from sidecar JSONL', async () => {
      mockExistsSync.mockReturnValue(true);
      mockFsPromises.readFile.mockImplementation(async (filePath: any) => {
        const p = String(filePath);
        if (p.includes('subagents/agent-ae5eb9a.jsonl')) {
          return [
            '{"type":"assistant","timestamp":"2024-01-15T10:02:00Z","message":{"content":[{"type":"tool_use","id":"sub-tool-1","name":"Grep","input":{"pattern":"TODO"}}]}}',
            '{"type":"user","timestamp":"2024-01-15T10:02:01Z","message":{"content":[{"type":"tool_result","tool_use_id":"sub-tool-1","content":"3 matches found"}]}}',
          ].join('\n');
        }
        if (p.endsWith('.jsonl')) {
          return [
            '{"type":"user","uuid":"u1","timestamp":"2024-01-15T10:00:00Z","message":{"content":"Review"}}',
            '{"type":"assistant","uuid":"a1","timestamp":"2024-01-15T10:01:00Z","message":{"content":[{"type":"tool_use","id":"task-1","name":"Task","input":{"description":"Review","prompt":"check","run_in_background":true}}]}}',
            `{"type":"user","uuid":"u2","timestamp":"2024-01-15T10:01:01Z","toolUseResult":{"isAsync":true,"agentId":"ae5eb9a"},"message":{"content":[{"type":"tool_result","tool_use_id":"task-1","content":"Launched"}]}}`,
            `{"type":"queue-operation","operation":"enqueue","content":"<task-notification><task-id>ae5eb9a</task-id><status>completed</status><result>Done reviewing</result></task-notification>"}`,
          ].join('\n');
        }
        return '';
      });

      const result = await loadSDKSessionMessages('/Users/test/vault', 'session-sidecar');

      const assistantMsg = result.messages.find(m => m.toolCalls?.some(tc => tc.name === 'Task'));
      const taskToolCall = assistantMsg!.toolCalls!.find(tc => tc.name === 'Task')!;

      expect(taskToolCall.subagent).toBeDefined();
      expect(taskToolCall.subagent!.toolCalls).toHaveLength(1);
      expect(taskToolCall.subagent!.toolCalls[0].name).toBe('Grep');
      expect(taskToolCall.subagent!.toolCalls[0].result).toBe('3 matches found');
    });
  });
});
