diff --git a/apps/kimi-web/dev/stub-daemon.mjs b/apps/kimi-web/dev/stub-daemon.mjs index e5bca1035..967df8190 100644 --- a/apps/kimi-web/dev/stub-daemon.mjs +++ b/apps/kimi-web/dev/stub-daemon.mjs @@ -1072,6 +1072,21 @@ const server = http.createServer((req, res) => { } } + // ---- snapshot (v2 initial sync: GET /sessions/{id}/snapshot) ---- + if (seg[0] === 'sessions' && seg[2] === 'snapshot' && seg.length === 3 && method === 'GET') { + const session = sessions.find((s) => s.id === sid); + if (!session) return res.end(fail(40401, `session ${sid} does not exist`)); + return res.end(ok({ + as_of_seq: seqBySession[sid] || 0, + epoch: 'ep_stub', + session, + messages: { items: messages[sid] || [], has_more: false }, + in_flight_turn: null, + pending_approvals: [], + pending_questions: [], + })); + } + // ---- messages ---- if (seg[0] === 'sessions' && seg[2] === 'messages' && seg.length === 3 && method === 'GET') { const sp = new URLSearchParams(url.split('?')[1] || ''); diff --git a/apps/kimi-web/test/session-url.test.ts b/apps/kimi-web/test/session-url.test.ts index c5f75f938..6a97e93a4 100644 --- a/apps/kimi-web/test/session-url.test.ts +++ b/apps/kimi-web/test/session-url.test.ts @@ -57,6 +57,7 @@ async function setup(opts: { subscribe: vi.fn(), unsubscribe: vi.fn(), bindNextPromptId: vi.fn(), + seedSnapshot: vi.fn(), abort: vi.fn(), close: vi.fn(), }; @@ -74,11 +75,21 @@ async function setup(opts: { return found; }), deleteSession: vi.fn(async () => ({ deleted: true })), - listMessages: vi.fn(async (id: string) => { + getSessionSnapshot: vi.fn(async (id: string) => { if (messageMissingSessions.has(id)) { throw { name: 'DaemonApiError', code: 40401, message: `session ${id} does not exist` }; } - return { items: [], hasMore: false }; + const found = extras.find((s) => s.id === id) ?? listed.find((s) => s.id === id) ?? session(id); + return { + asOfSeq: 0, + epoch: 'ep_test', + session: found, + messages: [], + hasMoreMessages: false, + inFlightTurn: null, + pendingApprovals: [], + pendingQuestions: [], + }; }), listTasks: vi.fn(async () => []), getGitStatus: vi.fn(async () => ({ branch: 'main', ahead: 0, behind: 0, entries: {} })), @@ -196,7 +207,7 @@ describe('session ↔ URL binding', () => { expect(window.location.pathname).toBe('/sessions/sess_1'); }); - it('load() repairs a deep link when the listed session vanishes before messages load', async () => { + it('load() repairs a deep link when the listed session vanishes before its snapshot loads', async () => { const { api, client } = await setup({ sessions: [session('sess_gone'), session('sess_1')], messageMissingSessions: ['sess_gone'], @@ -204,11 +215,11 @@ describe('session ↔ URL binding', () => { }); await client.load(); - expect(api.listMessages).toHaveBeenCalledWith('sess_gone', { pageSize: 100 }); + expect(api.getSessionSnapshot).toHaveBeenCalledWith('sess_gone'); expect(client.activeSessionId.value).toBe('sess_1'); expect(client.sessions.value.map((s) => s.id)).toEqual(['sess_1']); expect(window.location.pathname).toBe('/sessions/sess_1'); - expect(client.warnings.value.some((w) => w.includes('Failed to load messages'))).toBe(false); + expect(client.warnings.value.some((w) => w.includes('Failed to load session snapshot'))).toBe(false); }); it('popstate selects the session from the URL without writing the URL again', async () => { diff --git a/apps/kimi-web/test/steer.test.ts b/apps/kimi-web/test/steer.test.ts index c42d78074..df32a8307 100644 --- a/apps/kimi-web/test/steer.test.ts +++ b/apps/kimi-web/test/steer.test.ts @@ -43,6 +43,7 @@ async function setup(opts?: { submitStatuses?: ('running' | 'queued')[] }) { subscribe: vi.fn(), unsubscribe: vi.fn(), bindNextPromptId: vi.fn(), + seedSnapshot: vi.fn(), abort: vi.fn(), close: vi.fn(), }; @@ -51,7 +52,16 @@ async function setup(opts?: { submitStatuses?: ('running' | 'queued')[] }) { const created = session('sess_1'); const api = { createSession: vi.fn(async () => created), - listMessages: vi.fn(async () => ({ items: [], hasMore: false })), + getSessionSnapshot: vi.fn(async () => ({ + asOfSeq: 0, + epoch: 'ep_test', + session: created, + messages: [], + hasMoreMessages: false, + inFlightTurn: null, + pendingApprovals: [], + pendingQuestions: [], + })), submitPrompt: vi.fn(async () => { promptN += 1; return {