From fac416749962d150342142e61f9fccc6ec1326dc Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Wed, 10 Jun 2026 13:50:24 +0300 Subject: [PATCH] test(sync): update stale tests to current pipeline contracts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four sync tests had been failing for a while (CI doesn't run them, so nobody noticed). All four asserted behavior that was deliberately changed by earlier refactors — the production code is correct: - Three event-pipeline tests still expected message.part.updated events to coalesce in the queue. That coalescing was removed in #1167 to preserve part update ordering (the new contract is covered by event-pipeline.test.ts). Updated the delta-ordering and no-coalescing expectations, and switched the routes-before-queueing test to session.status, which is still a coalescible type, so it keeps proving that coalescing happens on the resolved directory. - One session-ui-store test expected shell sends to run inside an opencodeClient.withDirectory scope. Since #1228 the session directory travels as an explicit request param on shellSession; the test now asserts that contract directly. All 165 sync tests pass. --- .../src/sync/__tests__/event-pipeline.test.js | 43 ++++++++++++------- packages/ui/src/sync/session-ui-store.test.js | 25 +++-------- 2 files changed, 33 insertions(+), 35 deletions(-) diff --git a/packages/ui/src/sync/__tests__/event-pipeline.test.js b/packages/ui/src/sync/__tests__/event-pipeline.test.js index b4d7fa91..712a5e90 100644 --- a/packages/ui/src/sync/__tests__/event-pipeline.test.js +++ b/packages/ui/src/sync/__tests__/event-pipeline.test.js @@ -320,7 +320,8 @@ describe('createEventPipeline', () => { }, }, }, - // T2: message.part.updated for part-A — coalesces with T0 + // T2: a newer message.part.updated for part-A — delivered as its own + // event (part.updated is never coalesced, ordering is preserved). { payload: { type: 'message.part.updated', @@ -337,7 +338,7 @@ describe('createEventPipeline', () => { sdk, onEvent: (dir, payload) => { received.push({ directory: dir, payload }); - if (received.length === 2) { + if (received.length === 3) { cleanup(); releaseStream(); resolve(); @@ -348,10 +349,11 @@ describe('createEventPipeline', () => { await delivered; - expect(received.length).toBe(2); + expect(received.length).toBe(3); expect(received[0].payload.type).toBe('message.part.updated'); expect(received[1].payload.type).toBe('message.part.delta'); expect(received[1].payload.properties.delta).toBe(' world'); + expect(received[2].payload.type).toBe('message.part.updated'); }); it('keeps delta events for other fields on the same part', async () => { @@ -416,7 +418,7 @@ describe('createEventPipeline', () => { expect(received[1].payload.properties.delta).toBe('hello'); }); - it('coalesces message.part.updated events for the same part', async () => { + it('delivers every message.part.updated for the same part (never coalesced)', async () => { installDomStubs(); let releaseStream; @@ -427,6 +429,8 @@ describe('createEventPipeline', () => { const received = []; const directory = '/test/dir'; + // part.updated snapshots must all reach the reducer in order — coalescing + // them can drop intermediate states deltas depend on (see PR #1167). const sdk = createSdkWithEvents([ { payload: { @@ -453,18 +457,20 @@ describe('createEventPipeline', () => { sdk, onEvent: (dir, payload) => { received.push({ directory: dir, payload }); - cleanup(); - releaseStream(); - resolve(); + if (received.length === 2) { + cleanup(); + releaseStream(); + resolve(); + } }, }); }); await delivered; - // Only 1 event should be delivered (coalesced) - expect(received.length).toBe(1); + expect(received.length).toBe(2); expect(received[0].payload.type).toBe('message.part.updated'); + expect(received[1].payload.type).toBe('message.part.updated'); }); it('routes events before queueing so coalescing happens on the resolved directory', async () => { @@ -475,23 +481,28 @@ describe('createEventPipeline', () => { releaseStream = resolve; }); + // Two coalescible session.status events for the same session arrive on + // different transport directories; routing resolves both to the same + // directory, so they must land in one queue and coalesce to the latest. const received = []; const sdk = createSdkWithEvents([ { directory: 'global', payload: { - type: 'message.part.updated', + type: 'session.status', properties: { - part: { id: 'part-A', type: 'text', messageID: 'msg-1' }, + sessionID: 'session-1', + status: { type: 'busy' }, }, }, }, { directory: '/real-dir', payload: { - type: 'message.part.updated', + type: 'session.status', properties: { - part: { id: 'part-A', type: 'text', messageID: 'msg-1', text: 'next' }, + sessionID: 'session-1', + status: { type: 'idle' }, }, }, }, @@ -501,7 +512,7 @@ describe('createEventPipeline', () => { const { cleanup } = createEventPipeline({ sdk, routeDirectory: (directory, payload) => { - if (payload.type === 'message.part.updated') { + if (payload.type === 'session.status') { return '/resolved-dir'; } return directory; @@ -519,8 +530,8 @@ describe('createEventPipeline', () => { expect(received).toHaveLength(1); expect(received[0].directory).toBe('/resolved-dir'); - expect(received[0].payload.type).toBe('message.part.updated'); - expect(received[0].payload.properties.part.text).toBe('next'); + expect(received[0].payload.type).toBe('session.status'); + expect(received[0].payload.properties.status.type).toBe('idle'); }); it('consumes websocket message stream frames when transport is ws', async () => { diff --git a/packages/ui/src/sync/session-ui-store.test.js b/packages/ui/src/sync/session-ui-store.test.js index af637401..fe43e3e6 100644 --- a/packages/ui/src/sync/session-ui-store.test.js +++ b/packages/ui/src/sync/session-ui-store.test.js @@ -193,25 +193,13 @@ describe('session-worktree-store worktree routing', () => { describe('routeMessage directory scoping', () => { test('runs sends in the provided session directory', async () => { + // The session directory travels as an explicit request param (not via + // client-wide directory scoping), so concurrent sends can't cross-talk. const calls = []; - let activeDirectory = '/current/project'; - const originalWithDirectory = opencodeClient.withDirectory; - const originalGetDirectory = opencodeClient.getDirectory; const originalShellSession = opencodeClient.shellSession; - opencodeClient.withDirectory = async (directory, fn) => { - calls.push({ method: 'withDirectory', directory }); - const previousDirectory = activeDirectory; - activeDirectory = directory ?? undefined; - try { - return await fn(); - } finally { - activeDirectory = previousDirectory; - } - }; - opencodeClient.getDirectory = () => activeDirectory; opencodeClient.shellSession = async (params) => { - calls.push({ method: 'session.shell', params }); + calls.push(params); return { info: {}, parts: [] }; }; @@ -225,12 +213,11 @@ describe('routeMessage directory scoping', () => { inputMode: 'shell', }); } finally { - opencodeClient.withDirectory = originalWithDirectory; - opencodeClient.getDirectory = originalGetDirectory; opencodeClient.shellSession = originalShellSession; } - expect(calls[0]).toEqual({ method: 'withDirectory', directory: '/session/project' }); - expect(calls[1].params.directory).toBe('/session/project'); + expect(calls).toHaveLength(1); + expect(calls[0].sessionId).toBe('session-a'); + expect(calls[0].directory).toBe('/session/project'); }); });