import { describe, it, expect } from 'vitest'; import { resolveChatId } from '../chat-resolve.js'; import type { Sql } from '../../db.js'; // Mock the porsager/postgres surface that chat-resolve.ts uses: a tagged-template // `tx` (dispatched by query substring), `tx.json`, and `sql.begin(fn)` which just // runs fn(tx). Captures the value written back to workspace_panes so we can assert // the WorkspaceState envelope survives the UPDATE. interface MockState { stored: unknown; // initial sessions.workspace_panes value existingChatOpen: boolean; // whether `SELECT id FROM chats ...` finds the active chat newChatId: string; written?: unknown; // captured tx.json(...) payload from `UPDATE sessions` inserted: boolean; // whether INSERT INTO chats ran } interface MockTx { (strings: TemplateStringsArray): Promise; json: (v: unknown) => unknown; } function mockSql(state: MockState): Sql { const tx = ((strings: TemplateStringsArray) => { const q = strings.join(''); if (q.includes('SELECT workspace_panes FROM sessions')) { return Promise.resolve([{ workspace_panes: state.stored }]); } if (q.includes('FROM chats')) { return Promise.resolve(state.existingChatOpen ? [{ id: 'placeholder' }] : []); } if (q.includes('INSERT INTO chats')) { state.inserted = true; return Promise.resolve([{ id: state.newChatId }]); } if (q.includes('UPDATE sessions')) { return Promise.resolve([]); } return Promise.resolve([]); }) as unknown as MockTx; tx.json = (v: unknown) => { state.written = v; return v; }; const sql = { begin: (fn: (t: Sql) => Promise) => fn(tx as unknown as Sql), }; return sql as unknown as Sql; } const ENVELOPE = () => ({ panes: [{ id: 'pane-1', kind: 'coder', chatIds: [] as string[], activeChatIdx: 0 }], tabNumbers: { 'chat-x': 3 }, nextTabNumber: 7, closedPaneStack: [{ kind: 'coder', chatIds: ['old'], activeChatIdx: 0 }], }); describe('resolveChatId — v2.6.5 WorkspaceState envelope', () => { it('reads panes from the envelope without crashing (regression: panes.findIndex is not a function)', async () => { const state: MockState = { stored: ENVELOPE(), existingChatOpen: false, newChatId: 'new-chat-1', inserted: false, }; const chatId = await resolveChatId(mockSql(state), 'session-1', 'pane-1'); expect(chatId).toBe('new-chat-1'); expect(state.inserted).toBe(true); }); it('preserves the envelope (tabNumbers/nextTabNumber/closedPaneStack) on write-back', async () => { const state: MockState = { stored: ENVELOPE(), existingChatOpen: false, newChatId: 'new-chat-1', inserted: false, }; await resolveChatId(mockSql(state), 'session-1', 'pane-1'); const w = state.written as Record; expect(Array.isArray(w.panes)).toBe(true); // envelope, not a bare array expect(w.tabNumbers).toEqual({ 'chat-x': 3 }); expect(w.nextTabNumber).toBe(7); expect(w.closedPaneStack).toEqual([{ kind: 'coder', chatIds: ['old'], activeChatIdx: 0 }]); }); it('returns the existing open chat when the pane already has one', async () => { const env = ENVELOPE(); env.panes[0]!.chatIds = ['existing-1']; const state: MockState = { stored: env, existingChatOpen: true, newChatId: 'should-not-be-used', inserted: false, }; const chatId = await resolveChatId(mockSql(state), 'session-1', 'pane-1'); expect(chatId).toBe('existing-1'); expect(state.inserted).toBe(false); }); it('still accepts a legacy bare WorkspacePane[] array', async () => { const state: MockState = { stored: [{ id: 'pane-1', kind: 'coder', chatId: 'legacy-1', chatIds: ['legacy-1'], activeChatIdx: 0 }], existingChatOpen: true, newChatId: 'should-not-be-used', inserted: false, }; const chatId = await resolveChatId(mockSql(state), 'session-1', 'pane-1'); expect(chatId).toBe('legacy-1'); expect(state.inserted).toBe(false); }); });