/** * @fileoverview Tab-layout server coordinator lifecycle, ownership, and version tests. */ import { describe, expect, it, vi } from 'vitest'; import { TabLayoutService } from '../src/tab-layout-service.js'; import { SseEvent } from '../src/web/sse-events.js'; import type { TabLayout } from '../src/tab-layout.js'; type SessionFact = { id: string; owner?: string; createdAt: number; parentSessionId?: string; pinned?: boolean }; function createHarness( options: { layouts?: Record; order?: string[]; persisted?: SessionFact[]; live?: SessionFact[]; webviews?: Array<{ id: string; owner?: string }>; now?: () => string; } = {} ) { const layouts = { ...(options.layouts ?? {}) }; const persisted = Object.fromEntries((options.persisted ?? []).map((session) => [session.id, session])); const live = new Map((options.live ?? []).map((session) => [session.id, session])); const order = [...(options.order ?? [])]; const readWebviews = vi.fn(async () => options.webviews ?? []); const store = { getTabLayout: vi.fn((owner: string) => layouts[owner] ?? null), getTabLayouts: vi.fn(() => ({ ...layouts })), getSessions: vi.fn(() => persisted), getSessionOrder: vi.fn(() => [...order]), commitTabLayoutProjection: vi.fn( (updates: Readonly>, project: (latest: readonly string[]) => readonly string[]) => { const projected = [...project([...order])]; Object.assign(layouts, structuredClone(updates)); order.splice(0, order.length, ...projected); return { layouts: structuredClone(updates), sessionOrder: [...order] }; } ), }; const broadcast = vi.fn(); const broadcastSessionOrder = vi.fn(); const service = new TabLayoutService({ store: store as never, sessions: live as never, readWebviews, broadcast, broadcastSessionOrder, now: options.now ?? (() => '2026-08-16T12:00:00.000Z'), }); return { service, store, broadcast, broadcastSessionOrder, layouts, order, live, persisted, readWebviews }; } const existing = (refs: TabLayout['ungrouped'], version = 7): TabLayout => ({ version, groups: [], ungrouped: refs, updatedAt: '2026-08-15T00:00:00.000Z', }); describe('TabLayoutService', () => { it('guards destructive transactions by restoration state while allowing skipped test mode', async () => { const h = createHarness({ layouts: { '@single': existing([{ kind: 'session', id: 'mine' }]) } }); const action = vi.fn(async () => 'removed'); await expect(h.service.runSessionDeletion([{ id: 'mine' }], action)).rejects.toThrow(/restoration.*pending/i); expect(action).not.toHaveBeenCalled(); h.service.markRestorationSkipped(); await expect(h.service.runSessionDeletion([{ id: 'mine' }], action)).resolves.toBe('removed'); expect(action).toHaveBeenCalledTimes(1); expect(h.store.commitTabLayoutProjection).not.toHaveBeenCalled(); expect(h.broadcastSessionOrder).not.toHaveBeenCalled(); }); it('degrades an explicit deletion to best-effort after a failed restore, while the stale sweep stays blocked', async () => { // A tmux hiccup at boot used to lock DELETE /api/sessions/:id (and webview // deletes) into opaque 500s for the whole process lifetime. A user's // explicit close now runs without layout coordination; only the AUTOMATED // stale sweep stays fail-closed, because it picks its own victims from // state a failed restore may have left incomplete. const h = createHarness({ layouts: { '@single': existing([{ kind: 'session', id: 'mine' }]) } }); const action = vi.fn(async () => 'removed'); h.service.markRestorationFailed(); await expect(h.service.runSessionDeletion([{ id: 'mine' }], action)).resolves.toBe('removed'); expect(action).toHaveBeenCalledTimes(1); expect(h.store.commitTabLayoutProjection).not.toHaveBeenCalled(); expect(h.broadcastSessionOrder).not.toHaveBeenCalled(); await expect(h.service.webviewDeleted('@single', 'w1')).resolves.toBeUndefined(); const cleanup = vi.fn(); await expect(h.service.runStaleSessionCleanup(new Set(), cleanup)).rejects.toThrow(/restoration.*failed/i); expect(cleanup).not.toHaveBeenCalled(); }); it('prepares and commits a complete-state session deletion around the resource action', async () => { const h = createHarness({ layouts: { alice: existing([{ kind: 'session', id: 'mine' }]) }, live: [{ id: 'mine', owner: 'alice', createdAt: 1 }], }); h.service.markRestorationComplete(); const action = vi.fn(async () => { expect(h.store.commitTabLayoutProjection).not.toHaveBeenCalled(); h.live.delete('mine'); return 'removed'; }); await expect(h.service.runSessionDeletion([{ id: 'mine', owner: 'alice' }], action)).resolves.toBe('removed'); expect(h.layouts.alice.ungrouped).toEqual([]); expect(h.layouts.alice.version).toBe(8); }); it('leaves stale records, layouts, versions, and events intact when bulk preparation fails', async () => { const h = createHarness({ layouts: { alice: existing([{ kind: 'session', id: 'stale' }]) }, persisted: [{ id: 'stale', owner: 'alice', createdAt: 1 }], }); h.service.markRestorationComplete(); const cleanup = vi.fn(() => { delete h.persisted.stale; return { count: 1, cleaned: [{ id: 'stale', owner: 'alice' }] }; }); h.store.getSessions .mockImplementationOnce(() => h.persisted) .mockImplementationOnce(() => { throw new Error('malformed persisted state'); }); await expect(h.service.runStaleSessionCleanup(new Set(), cleanup)).rejects.toThrow('malformed persisted state'); expect(cleanup).not.toHaveBeenCalled(); expect(h.persisted.stale).toBeDefined(); expect(h.layouts.alice).toEqual(existing([{ kind: 'session', id: 'stale' }])); expect(h.store.commitTabLayoutProjection).not.toHaveBeenCalled(); expect(h.broadcast).not.toHaveBeenCalled(); expect(h.broadcastSessionOrder).not.toHaveBeenCalled(); }); it('bulk-cleans multiple owners once each while preserving pinned stale records', async () => { const h = createHarness({ layouts: { alice: existing([ { kind: 'session', id: 'alice-stale' }, { kind: 'session', id: 'alice-pinned' }, ]), bob: existing([{ kind: 'session', id: 'bob-stale' }]), }, persisted: [ { id: 'bob-stale', owner: 'bob', createdAt: 1 }, { id: 'alice-stale', owner: 'alice', createdAt: 2 }, { id: 'alice-pinned', owner: 'alice', createdAt: 3, pinned: true }, ], }); h.service.markRestorationComplete(); const cleanup = vi.fn(() => { delete h.persisted['alice-stale']; delete h.persisted['bob-stale']; return { count: 2 }; }); await expect(h.service.runStaleSessionCleanup(new Set(), cleanup)).resolves.toEqual({ count: 2 }); expect(h.layouts.alice.ungrouped).toEqual([{ kind: 'session', id: 'alice-pinned' }]); expect(h.layouts.bob.ungrouped).toEqual([]); expect(h.layouts.alice.version).toBe(8); expect(h.layouts.bob.version).toBe(8); expect(h.store.commitTabLayoutProjection).toHaveBeenCalledTimes(1); expect(Object.keys(h.store.commitTabLayoutProjection.mock.calls[0][0])).toEqual(['alice', 'bob']); expect(h.broadcast).toHaveBeenCalledTimes(2); }); it('does not commit a prepared stale layout when asynchronous cleanup rejects', async () => { const h = createHarness({ layouts: { alice: existing([{ kind: 'session', id: 'alice-stale' }]) }, persisted: [{ id: 'alice-stale', owner: 'alice', createdAt: 1 }], }); h.service.markRestorationComplete(); const cleanup = vi.fn(async () => { throw new Error('state removal failed'); }); await expect(h.service.runStaleSessionCleanup(new Set(), cleanup)).rejects.toThrow('state removal failed'); expect(h.layouts.alice).toEqual(existing([{ kind: 'session', id: 'alice-stale' }])); expect(h.store.commitTabLayoutProjection).not.toHaveBeenCalled(); expect(h.broadcast).not.toHaveBeenCalled(); }); it('binds stale cleanup to revalidated original candidates after an owner queue wait', async () => { const h = createHarness({ layouts: { alice: existing([{ kind: 'session', id: 'alice-stale' }]), bob: existing([{ kind: 'session', id: 'bob-new-stale' }]), }, order: ['alice-stale'], persisted: [{ id: 'alice-stale', owner: 'alice', createdAt: 1 }], }); h.service.markRestorationComplete(); let releaseFacts!: () => void; const factsBlocked = new Promise((resolve) => { releaseFacts = resolve; }); h.readWebviews.mockImplementationOnce(async () => { await factsBlocked; return []; }); const occupiedOwnerQueue = h.service.get('alice'); await vi.waitFor(() => expect(h.readWebviews).toHaveBeenCalledOnce()); const cleanup = vi.fn((ids: ReadonlySet) => ({ ids: [...ids] })); const pending = h.service.runStaleSessionCleanup(new Set(), cleanup); h.persisted['alice-stale'].pinned = true; h.persisted['bob-new-stale'] = { id: 'bob-new-stale', owner: 'bob', createdAt: 2 }; releaseFacts(); await occupiedOwnerQueue; await expect(pending).resolves.toEqual({ ids: [] }); expect(cleanup).toHaveBeenCalledTimes(1); expect(cleanup).toHaveBeenCalledWith(new Set()); expect(h.persisted['alice-stale']).toBeDefined(); expect(h.persisted['bob-new-stale']).toBeDefined(); expect(h.layouts.alice.ungrouped).toEqual([{ kind: 'session', id: 'alice-stale' }]); expect(h.layouts.bob.ungrouped).toEqual([{ kind: 'session', id: 'bob-new-stale' }]); expect(h.store.commitTabLayoutProjection).not.toHaveBeenCalled(); }); it('allows skipped-mode stale cleanup without pruning or versioning layouts', async () => { const h = createHarness({ layouts: { '@single': existing([{ kind: 'session', id: 'stale' }]) }, persisted: [{ id: 'stale', createdAt: 1 }], }); h.service.markRestorationSkipped(); const cleanup = vi.fn(() => ({ count: 1 })); await expect(h.service.runStaleSessionCleanup(new Set(), cleanup)).resolves.toEqual({ count: 1 }); expect(cleanup).toHaveBeenCalledOnce(); expect(h.store.commitTabLayoutProjection).not.toHaveBeenCalled(); expect(h.broadcast).not.toHaveBeenCalled(); }); it('repairs missing owner refs once while preserving unknown refs', async () => { const h = createHarness({ layouts: { alice: existing([{ kind: 'session', id: 'unknown' }]) }, live: [{ id: 'mine', owner: 'alice', createdAt: 1 }], }); const first = await h.service.get('alice'); expect(first.ungrouped).toEqual([ { kind: 'session', id: 'unknown' }, { kind: 'session', id: 'mine' }, ]); expect(first.version).toBe(8); await h.service.get('alice'); expect(h.store.commitTabLayoutProjection).toHaveBeenCalledTimes(1); expect(h.broadcast).toHaveBeenCalledWith(SseEvent.TabLayoutChanged, { owner: 'alice', version: 8 }); }); it('repairs only the legacy projection on GET when the stored layout is already canonical', async () => { const h = createHarness({ layouts: { alice: existing([ { kind: 'session', id: 'a' }, { kind: 'session', id: 'b' }, ]), }, order: ['b', 'a'], live: [ { id: 'a', owner: 'alice', createdAt: 1 }, { id: 'b', owner: 'alice', createdAt: 2 }, ], }); const first = await h.service.get('alice'); const second = await h.service.get('alice'); expect(first).toEqual( existing([ { kind: 'session', id: 'a' }, { kind: 'session', id: 'b' }, ]) ); expect(second).toEqual(first); expect(h.order).toEqual(['a', 'b']); expect(h.layouts.alice.version).toBe(7); expect(h.store.commitTabLayoutProjection).toHaveBeenCalledTimes(1); expect(h.store.commitTabLayoutProjection.mock.calls[0][0]).toEqual({}); expect(h.broadcast).not.toHaveBeenCalled(); expect(h.broadcastSessionOrder).toHaveBeenCalledExactlyOnceWith({ changedOwnerOrders: { alice: ['a', 'b'] }, globalOrder: ['a', 'b'], globalChanged: true, }); expect(h.broadcastSessionOrder).toHaveBeenCalledExactlyOnceWith({ changedOwnerOrders: { alice: ['a', 'b'] }, globalOrder: ['a', 'b'], globalChanged: true, }); }); it('isolates owner metadata and filters saved webviews by exact owner', async () => { const h = createHarness({ live: [ { id: 'a', owner: 'alice', createdAt: 1 }, { id: 'b', owner: 'bob', createdAt: 2 }, ], webviews: [ { id: 'wa', owner: 'alice' }, { id: 'wb', owner: 'bob' }, ], }); expect((await h.service.get('alice')).ungrouped.map((ref) => ref.id)).toEqual(['a', 'wa']); expect((await h.service.get('bob')).ungrouped.map((ref) => ref.id)).toEqual(['b', 'wb']); }); it('rejects foreign and unknown refs atomically even for an admin-owned layout', async () => { const h = createHarness({ layouts: { admin: existing([{ kind: 'session', id: 'mine' }]) }, live: [ { id: 'mine', owner: 'admin', createdAt: 1 }, { id: 'foreign', owner: 'bob', createdAt: 2 }, ], }); const desired = existing([{ kind: 'session', id: 'foreign' }]); await expect(h.service.put('admin', desired, 7)).rejects.toThrow(/not owned/); expect(h.store.commitTabLayoutProjection).not.toHaveBeenCalled(); }); it.each([6, 8])('conflicts on every non-exact base version (%s)', async (baseVersion) => { const h = createHarness({ layouts: { alice: existing([{ kind: 'session', id: 'mine' }]) }, live: [{ id: 'mine', owner: 'alice', createdAt: 1 }], }); expect(await h.service.put('alice', existing([{ kind: 'session', id: 'mine' }]), baseVersion)).toEqual({ status: 'conflict', layout: existing([{ kind: 'session', id: 'mine' }]), }); expect(h.store.commitTabLayoutProjection).not.toHaveBeenCalled(); expect(h.broadcastSessionOrder).not.toHaveBeenCalled(); }); it('returns an unpersisted prepared layout for a stale first write without broadcasting', async () => { const h = createHarness({ live: [{ id: 'mine', owner: 'alice', createdAt: 1 }], }); await expect(h.service.put('alice', existing([{ kind: 'session', id: 'mine' }]), 7)).resolves.toEqual({ status: 'conflict', layout: { version: 0, groups: [], ungrouped: [{ kind: 'session', id: 'mine' }], updatedAt: '2026-08-16T12:00:00.000Z', }, }); expect(h.layouts.alice).toBeUndefined(); expect(h.store.commitTabLayoutProjection).not.toHaveBeenCalled(); expect(h.broadcast).not.toHaveBeenCalled(); expect(h.broadcastSessionOrder).not.toHaveBeenCalled(); }); it('returns a normalized prepared layout for a stale write without persisting the reconciliation', async () => { const h = createHarness({ layouts: { alice: existing([{ kind: 'session', id: 'unknown' }]) }, live: [{ id: 'mine', owner: 'alice', createdAt: 1 }], }); await expect(h.service.put('alice', existing([{ kind: 'session', id: 'mine' }]), 6)).resolves.toEqual({ status: 'conflict', layout: existing([ { kind: 'session', id: 'unknown' }, { kind: 'session', id: 'mine' }, ]), }); expect(h.layouts.alice).toEqual(existing([{ kind: 'session', id: 'unknown' }])); expect(h.store.commitTabLayoutProjection).not.toHaveBeenCalled(); expect(h.broadcast).not.toHaveBeenCalled(); expect(h.broadcastSessionOrder).not.toHaveBeenCalled(); }); it('folds first-write initialization and the requested update into one commit', async () => { const h = createHarness({ live: [{ id: 'mine', owner: 'alice', createdAt: 1 }], }); const desired = { ...existing([]), groups: [{ id: 'g', name: 'Mine', refs: [{ kind: 'session', id: 'mine' }] }], }; const result = await h.service.put('alice', desired, 0); expect(result).toEqual({ status: 'updated', layout: { version: 1, groups: [{ id: 'g', name: 'Mine', refs: [{ kind: 'session', id: 'mine' }] }], ungrouped: [], updatedAt: '2026-08-16T12:00:00.000Z', }, }); expect(h.store.commitTabLayoutProjection).toHaveBeenCalledTimes(1); expect(h.store.commitTabLayoutProjection.mock.calls[0][0]).toEqual({ alice: result.layout }); expect(h.broadcast).toHaveBeenCalledExactlyOnceWith(SseEvent.TabLayoutChanged, { owner: 'alice', version: 1 }); }); it('uses the prepared authoritative ordering for a matching folded first write with tied session timestamps', async () => { const h = createHarness({ live: [ { id: 'a', owner: 'alice', createdAt: 1 }, { id: 'B', owner: 'alice', createdAt: 1 }, ], }); const result = await h.service.put('alice', existing([]), 0); expect(result).toEqual({ status: 'updated', layout: { version: 1, groups: [], ungrouped: [ { kind: 'session', id: 'B' }, { kind: 'session', id: 'a' }, ], updatedAt: '2026-08-16T12:00:00.000Z', }, }); }); it('folds normalization and a matching requested update into one commit', async () => { const h = createHarness({ layouts: { alice: existing([{ kind: 'session', id: 'unknown' }]) }, live: [{ id: 'mine', owner: 'alice', createdAt: 1 }], }); const desired = { ...existing([]), groups: [{ id: 'g', name: 'Mine', refs: [{ kind: 'session', id: 'mine' }] }], }; const result = await h.service.put('alice', desired, 7); expect(result).toEqual({ status: 'updated', layout: { version: 8, groups: [{ id: 'g', name: 'Mine', refs: [{ kind: 'session', id: 'mine' }] }], ungrouped: [], updatedAt: '2026-08-16T12:00:00.000Z', }, }); expect(h.store.commitTabLayoutProjection).toHaveBeenCalledTimes(1); expect(h.store.commitTabLayoutProjection.mock.calls[0][0]).toEqual({ alice: result.layout }); expect(h.broadcast).toHaveBeenCalledExactlyOnceWith(SseEvent.TabLayoutChanged, { owner: 'alice', version: 8 }); }); it('stores a matching write with one fresh version and minimal event payload', async () => { const h = createHarness({ layouts: { alice: existing([{ kind: 'session', id: 'mine' }]) }, live: [{ id: 'mine', owner: 'alice', createdAt: 1 }], }); const result = await h.service.put( 'alice', { ...existing([{ kind: 'session', id: 'mine' }]), groups: [{ id: 'g', name: ' Group ', refs: [] }] }, 7 ); expect(result.status).toBe('updated'); expect(result.layout).toMatchObject({ version: 8, updatedAt: '2026-08-16T12:00:00.000Z' }); expect(result.layout.groups[0].name).toBe('Group'); expect(h.broadcast).toHaveBeenLastCalledWith(SseEvent.TabLayoutChanged, { owner: 'alice', version: 8 }); }); it('serializes concurrent writes so only one exact base version can win', async () => { const h = createHarness({ layouts: { alice: existing([{ kind: 'session', id: 'mine' }]) }, live: [{ id: 'mine', owner: 'alice', createdAt: 1 }], }); const desired = existing([{ kind: 'session', id: 'mine' }]); const results = await Promise.all([h.service.put('alice', desired, 7), h.service.put('alice', desired, 7)]); expect(results.map((result) => result.status).sort()).toEqual(['conflict', 'updated']); expect(h.store.commitTabLayoutProjection).toHaveBeenCalledTimes(1); expect(h.layouts.alice.version).toBe(8); }); it('places a root at the end and a child after its parent subtree', async () => { const h = createHarness({ layouts: { alice: existing([ { kind: 'session', id: 'parent' }, { kind: 'session', id: 'tail' }, ]), }, live: [ { id: 'parent', owner: 'alice', createdAt: 1 }, { id: 'tail', owner: 'alice', createdAt: 2 }, ], }); h.live.set('root', { id: 'root', owner: 'alice', createdAt: 3 } as never); await h.service.sessionCreated('alice'); expect(h.layouts.alice.ungrouped.map((ref) => ref.id)).toEqual(['parent', 'tail', 'root']); h.live.set('child', { id: 'child', owner: 'alice', createdAt: 4, parentSessionId: 'parent' } as never); await h.service.sessionCreated('alice'); expect(h.layouts.alice.ungrouped.map((ref) => ref.id)).toEqual(['parent', 'child', 'tail', 'root']); expect(h.layouts.alice.version).toBe(9); }); it('appends a newly saved owner webview and rejects over-limit writes atomically', async () => { const webviews: Array<{ id: string; owner?: string }> = []; const h = createHarness({ layouts: { alice: existing([{ kind: 'session', id: 'mine' }]) }, live: [{ id: 'mine', owner: 'alice', createdAt: 1 }], webviews, }); webviews.push({ id: 'dashboard', owner: 'alice' }); await h.service.webviewCreated('alice'); expect(h.layouts.alice.ungrouped.at(-1)).toEqual({ kind: 'webview', id: 'dashboard' }); const tooMany = Array.from({ length: 513 }, (_, index) => ({ kind: 'session' as const, id: `s-${index}` })); await expect(h.service.put('alice', existing(tooMany, 8), 8)).rejects.toThrow(/512/); expect(h.layouts.alice.version).toBe(8); }); it('materializes children and removes an explicitly deleted parent in one mutation', async () => { const h = createHarness({ layouts: { alice: existing([ { kind: 'session', id: 'parent' }, { kind: 'session', id: 'child' }, ]), }, live: [{ id: 'child', owner: 'alice', createdAt: 2, parentSessionId: 'parent' }], }); h.service.markRestorationComplete(); await h.service.sessionsRemoved([{ id: 'parent', owner: 'alice' }]); expect(h.layouts.alice.ungrouped).toEqual([{ kind: 'session', id: 'child', placement: 'manual' }]); expect(h.layouts.alice.version).toBe(8); expect(h.store.commitTabLayoutProjection).toHaveBeenCalledTimes(1); }); it('publishes multi-owner post-removal repairs in one combined transaction', async () => { const h = createHarness({ layouts: { alice: existing([{ kind: 'session', id: 'a-stale' }]), bob: existing([{ kind: 'session', id: 'b-stale' }]), }, order: ['a-stale', 'b-stale'], }); h.service.markRestorationComplete(); await h.service.sessionsRemoved([ { id: 'a-stale', owner: 'alice' }, { id: 'b-stale', owner: 'bob' }, ]); expect(h.store.commitTabLayoutProjection).toHaveBeenCalledTimes(1); expect(Object.keys(h.store.commitTabLayoutProjection.mock.calls[0][0])).toEqual(['alice', 'bob']); expect(h.layouts.alice.ungrouped).toEqual([]); expect(h.layouts.bob.ungrouped).toEqual([]); expect(h.order).toEqual([]); }); it('does not publish any owner when later multi-owner post-removal preparation fails', async () => { const h = createHarness({ layouts: { alice: existing([{ kind: 'session', id: 'a-stale' }]), bob: existing([{ kind: 'session', id: 'b-stale' }]), }, order: ['a-stale', 'b-stale'], }); h.service.markRestorationComplete(); h.readWebviews.mockResolvedValueOnce([]).mockRejectedValueOnce(new Error('bob facts failed')); await expect( h.service.sessionsRemoved([ { id: 'a-stale', owner: 'alice' }, { id: 'b-stale', owner: 'bob' }, ]) ).rejects.toThrow('bob facts failed'); expect(h.store.commitTabLayoutProjection).not.toHaveBeenCalled(); expect(h.layouts.alice.ungrouped.map((ref) => ref.id)).toEqual(['a-stale']); expect(h.layouts.bob.ungrouped.map((ref) => ref.id)).toEqual(['b-stale']); }); it('prepares deletion version metadata before running the irreversible action', async () => { const action = vi.fn(async () => 'removed'); const h = createHarness({ layouts: { alice: existing([{ kind: 'session', id: 'mine' }]) }, live: [{ id: 'mine', owner: 'alice', createdAt: 1 }], now: () => { throw new Error('clock failed'); }, }); h.service.markRestorationComplete(); await expect(h.service.runSessionDeletion([{ id: 'mine', owner: 'alice' }], action)).rejects.toThrow( 'clock failed' ); expect(action).not.toHaveBeenCalled(); expect(h.store.commitTabLayoutProjection).not.toHaveBeenCalled(); }); it('removes a legacy-only ID when deleting before owner layout migration', async () => { const h = createHarness({ order: ['mine'], live: [{ id: 'mine', owner: 'alice', createdAt: 1 }], }); h.service.markRestorationComplete(); const action = vi.fn(async () => { h.live.delete('mine'); return 'removed'; }); await expect(h.service.runSessionDeletion([{ id: 'mine', owner: 'alice' }], action)).resolves.toBe('removed'); expect(h.layouts.alice).toBeUndefined(); expect(h.order).toEqual([]); expect(h.store.commitTabLayoutProjection).toHaveBeenCalledExactlyOnceWith({}, expect.any(Function)); expect(h.broadcast).not.toHaveBeenCalled(); expect(h.broadcastSessionOrder).toHaveBeenCalledExactlyOnceWith({ changedOwnerOrders: { alice: [] }, globalOrder: [], globalChanged: true, }); }); it('removes a deleted legacy ID absent from an existing layout without bumping its version', async () => { const h = createHarness({ layouts: { alice: existing([{ kind: 'session', id: 'keep' }]) }, order: ['gone', 'keep'], live: [ { id: 'gone', owner: 'alice', createdAt: 1 }, { id: 'keep', owner: 'alice', createdAt: 2 }, ], }); h.service.markRestorationComplete(); await h.service.runSessionDeletion([{ id: 'gone', owner: 'alice' }], async () => { h.live.delete('gone'); }); expect(h.layouts.alice).toEqual(existing([{ kind: 'session', id: 'keep' }])); expect(h.order).toEqual(['keep']); expect(h.store.commitTabLayoutProjection.mock.calls[0][0]).toEqual({}); expect(h.broadcast).not.toHaveBeenCalled(); expect(h.broadcastSessionOrder).toHaveBeenCalledExactlyOnceWith({ changedOwnerOrders: { alice: ['keep'] }, globalOrder: ['keep'], globalChanged: true, }); }); it('removes legacy-only IDs from post-removal repair with no migrated layout or layout delta', async () => { const h = createHarness({ layouts: { bob: existing([{ kind: 'session', id: 'keep' }]) }, order: ['alice-gone', 'bob-gone', 'keep'], live: [{ id: 'keep', owner: 'bob', createdAt: 3 }], }); h.service.markRestorationComplete(); await h.service.sessionsRemoved([ { id: 'alice-gone', owner: 'alice' }, { id: 'bob-gone', owner: 'bob' }, ]); expect(h.layouts.alice).toBeUndefined(); expect(h.layouts.bob).toEqual(existing([{ kind: 'session', id: 'keep' }])); expect(h.order).toEqual(['keep']); expect(h.store.commitTabLayoutProjection).toHaveBeenCalledTimes(1); expect(h.store.commitTabLayoutProjection.mock.calls[0][0]).toEqual({}); expect(h.broadcast).not.toHaveBeenCalled(); expect(h.broadcastSessionOrder).toHaveBeenCalledExactlyOnceWith({ changedOwnerOrders: { alice: [], bob: ['keep'] }, globalOrder: ['keep'], globalChanged: true, }); }); it('removes a confirmed stale legacy-only ID even before owner layout migration', async () => { const h = createHarness({ order: ['stale'], persisted: [{ id: 'stale', owner: 'alice', createdAt: 1 }], }); h.service.markRestorationComplete(); const cleanup = vi.fn((ids: ReadonlySet) => { delete h.persisted.stale; return [...ids]; }); await expect(h.service.runStaleSessionCleanup(new Set(), cleanup)).resolves.toEqual(['stale']); expect(h.layouts.alice).toBeUndefined(); expect(h.order).toEqual([]); expect(h.store.commitTabLayoutProjection).toHaveBeenCalledExactlyOnceWith({}, expect.any(Function)); expect(h.broadcast).not.toHaveBeenCalled(); expect(h.broadcastSessionOrder).toHaveBeenCalledExactlyOnceWith({ changedOwnerOrders: { alice: [] }, globalOrder: [], globalChanged: true, }); }); it('does not prune or bump when restoration was skipped', async () => { const h = createHarness({ layouts: { '@single': existing([{ kind: 'session', id: 'gone' }]) } }); h.service.markRestorationSkipped(); await h.service.sessionsRemoved([{ id: 'gone' }]); await h.service.webviewDeleted('@single', 'also-gone'); expect(h.store.commitTabLayoutProjection).not.toHaveBeenCalled(); expect(h.layouts['@single'].version).toBe(7); }); it('retains pinned stopped sessions and prunes saved webviews after restoration', async () => { const h = createHarness({ layouts: { alice: existing([ { kind: 'session', id: 'pinned' }, { kind: 'webview', id: 'gone-web' }, ]), }, persisted: [{ id: 'pinned', owner: 'alice', createdAt: 1, pinned: true }], }); h.service.markRestorationComplete(); await h.service.webviewDeleted('alice', 'gone-web'); expect(h.layouts.alice.ungrouped).toEqual([{ kind: 'session', id: 'pinned' }]); expect(h.layouts.alice.version).toBe(8); }); it('bridges a regular legacy reorder into the owner layout and canonical global projection', async () => { const h = createHarness({ layouts: { alice: existing([ { kind: 'session', id: 'a' }, { kind: 'session', id: 'b' }, ]), }, order: ['a', 'b'], live: [ { id: 'a', owner: 'alice', createdAt: 1 }, { id: 'b', owner: 'alice', createdAt: 2 }, ], }); await expect(h.service.putLegacyOrder({ owner: 'alice', isAdmin: false }, ['b', 'a'])).resolves.toEqual({ order: ['b', 'a'], changedOwnerOrders: { alice: ['b', 'a'] }, globalOrder: ['b', 'a'], globalChanged: true, }); expect((await h.service.get('alice')).ungrouped.map((ref) => ref.id)).toEqual(['b', 'a']); expect(h.layouts.alice.version).toBe(8); expect(h.broadcast).toHaveBeenCalledExactlyOnceWith(SseEvent.TabLayoutChanged, { owner: 'alice', version: 8 }); expect(h.broadcastSessionOrder).toHaveBeenCalledExactlyOnceWith({ changedOwnerOrders: { alice: ['b', 'a'] }, globalOrder: ['b', 'a'], globalChanged: true, }); }); it('preserves an unmapped global slot while a legacy PUT reorders authoritative owner sessions', async () => { const h = createHarness({ layouts: { alice: existing([ { kind: 'session', id: 'unknown' }, { kind: 'session', id: 'a' }, { kind: 'session', id: 'b' }, ]), }, order: ['unknown', 'a', 'b'], live: [ { id: 'a', owner: 'alice', createdAt: 1 }, { id: 'b', owner: 'alice', createdAt: 2 }, ], }); const result = await h.service.putLegacyOrder({ owner: 'alice', isAdmin: false }, ['b', 'a']); expect(h.layouts.alice.ungrouped).toEqual([ { kind: 'session', id: 'unknown' }, { kind: 'session', id: 'b' }, { kind: 'session', id: 'a' }, ]); expect(result.globalOrder).toEqual(['unknown', 'b', 'a']); expect(h.order).toEqual(['unknown', 'b', 'a']); expect(result.order).toEqual(['b', 'a']); }); it('preserves an unmapped global slot during a GET order repair', async () => { const h = createHarness({ layouts: { alice: existing([ { kind: 'session', id: 'unknown' }, { kind: 'session', id: 'b' }, { kind: 'session', id: 'a' }, ]), }, order: ['unknown', 'a', 'b'], live: [ { id: 'a', owner: 'alice', createdAt: 1 }, { id: 'b', owner: 'alice', createdAt: 2 }, ], }); await h.service.get('alice'); expect(h.order).toEqual(['unknown', 'b', 'a']); expect(h.broadcastSessionOrder).toHaveBeenCalledWith({ changedOwnerOrders: { alice: ['b', 'a'] }, globalOrder: ['unknown', 'b', 'a'], globalChanged: true, }); }); it('removes an unmapped global id only when a trusted deletion explicitly excludes it', async () => { const h = createHarness({ layouts: { alice: existing([ { kind: 'session', id: 'unknown' }, { kind: 'session', id: 'a' }, ]), }, order: ['unknown', 'a'], live: [{ id: 'a', owner: 'alice', createdAt: 1 }], }); h.service.markRestorationComplete(); await h.service.runSessionDeletion([{ id: 'unknown', owner: 'alice' }], async () => undefined); expect(h.layouts.alice.ungrouped).toEqual([{ kind: 'session', id: 'a' }]); expect(h.order).toEqual(['a']); }); it('reports an owner layout-order change even when the legacy global slice already matches', async () => { const h = createHarness({ layouts: { alice: existing([ { kind: 'session', id: 'a' }, { kind: 'session', id: 'b' }, ]), }, order: ['b', 'a'], live: [ { id: 'a', owner: 'alice', createdAt: 1 }, { id: 'b', owner: 'alice', createdAt: 2 }, ], }); const result = await h.service.putLegacyOrder({ owner: 'alice', isAdmin: false }, ['b', 'a']); expect(result.changedOwnerOrders).toEqual({ alice: ['b', 'a'] }); expect(result.globalChanged).toBe(false); expect(h.broadcastSessionOrder).toHaveBeenCalledTimes(1); }); it('reports an authoritative owner-order correction when a placement-only PUT repairs stale global order', async () => { const h = createHarness({ layouts: { alice: existing([ { kind: 'session', id: 'a' }, { kind: 'session', id: 'b' }, ]), }, order: ['b', 'a'], live: [ { id: 'a', owner: 'alice', createdAt: 1 }, { id: 'b', owner: 'alice', createdAt: 2 }, ], }); const desired = { ...existing([], 7), groups: [ { id: 'g', name: 'Same order', refs: [ { kind: 'session' as const, id: 'a' }, { kind: 'session' as const, id: 'b' }, ], }, ], }; const result = await h.service.put('alice', desired, 7); expect(result.status).toBe('updated'); expect(h.order).toEqual(['a', 'b']); expect(h.broadcastSessionOrder).toHaveBeenCalledExactlyOnceWith({ changedOwnerOrders: { alice: ['a', 'b'] }, globalOrder: ['a', 'b'], globalChanged: true, }); }); it.each([ ['foreign owner', ['b', 'a']], ['unknown stored ref', ['unknown', 'a']], ])('drops a %s from a regular legacy request instead of rejecting the write', async (_label, requested) => { const original = existing([ { kind: 'session', id: 'a' }, { kind: 'session', id: 'unknown' }, ]); const h = createHarness({ layouts: { alice: original }, order: ['a', 'b', 'unknown'], live: [ { id: 'a', owner: 'alice', createdAt: 1 }, { id: 'b', owner: 'bob', createdAt: 2 }, ], }); // Unknown/foreign ids are dropped, never a 400: the browser's reorder push // is debounced and swallows errors, so a rejection would silently lose the // user's whole reorder when a session dies inside the debounce window. const result = await h.service.putLegacyOrder({ owner: 'alice', isAdmin: false }, requested); expect(result.order).toEqual(['a']); expect(h.layouts.alice).toEqual(original); expect(h.order).toEqual(['a', 'b', 'unknown']); expect(h.broadcast).not.toHaveBeenCalled(); expect(h.broadcastSessionOrder).not.toHaveBeenCalled(); }); it('applies the surviving reorder when the request contains a deleted id (owner path)', async () => { const h = createHarness({ layouts: { alice: existing([ { kind: 'session', id: 'a1' }, { kind: 'session', id: 'a2' }, ]), }, order: ['a1', 'a2'], live: [ { id: 'a1', owner: 'alice', createdAt: 1 }, { id: 'a2', owner: 'alice', createdAt: 2 }, ], }); const result = await h.service.putLegacyOrder({ owner: 'alice', isAdmin: false }, ['a2', 'ghost', 'a1']); expect(result.order).toEqual(['a2', 'a1']); expect(h.order).toEqual(['a2', 'a1']); }); it('applies the surviving reorder when the request contains a deleted id (admin path)', async () => { const h = createHarness({ layouts: { alice: existing([ { kind: 'session', id: 'a1' }, { kind: 'session', id: 'a2' }, ]), }, order: ['a1', 'a2'], live: [ { id: 'a1', owner: 'alice', createdAt: 1 }, { id: 'a2', owner: 'alice', createdAt: 2 }, ], }); const result = await h.service.putLegacyOrder({ owner: 'root', isAdmin: true }, ['a2', 'ghost', 'a1']); expect(result.order).toEqual(['a2', 'a1']); expect(h.order).toEqual(['a2', 'a1']); }); it('keeps containers and anchored slots fixed, materializes ranked children, and merges missing known sessions', async () => { const h = createHarness({ layouts: { alice: { ...existing([ { kind: 'session', id: 'parent' }, { kind: 'session', id: 'child' }, { kind: 'session', id: 'c' }, ]), groups: [ { id: 'g', name: 'Work', refs: [ { kind: 'session', id: 'a' }, { kind: 'webview', id: 'w' }, { kind: 'session', id: 'unknown' }, { kind: 'session', id: 'b' }, ], }, ], }, }, order: ['a', 'b', 'parent', 'child', 'c'], live: [ { id: 'a', owner: 'alice', createdAt: 1 }, { id: 'b', owner: 'alice', createdAt: 2 }, { id: 'parent', owner: 'alice', createdAt: 3 }, { id: 'child', owner: 'alice', createdAt: 4, parentSessionId: 'parent' }, { id: 'c', owner: 'alice', createdAt: 5 }, ], webviews: [{ id: 'w', owner: 'alice' }], }); const result = await h.service.putLegacyOrder({ owner: 'alice', isAdmin: false }, ['b', 'a', 'child', 'parent']); expect(result.order).toEqual(['b', 'a', 'child', 'parent', 'c']); expect(h.layouts.alice.groups).toEqual([ { id: 'g', name: 'Work', refs: [ { kind: 'session', id: 'b' }, { kind: 'webview', id: 'w' }, { kind: 'session', id: 'unknown' }, { kind: 'session', id: 'a' }, ], }, ]); expect(h.layouts.alice.ungrouped).toEqual([ { kind: 'session', id: 'child', placement: 'manual' }, { kind: 'session', id: 'parent' }, { kind: 'session', id: 'c' }, ]); expect(h.broadcastSessionOrder).toHaveBeenCalledExactlyOnceWith({ changedOwnerOrders: { alice: ['b', 'a', 'child', 'parent', 'c'] }, globalOrder: ['b', 'a', 'child', 'parent', 'c'], globalChanged: true, }); expect(await h.service.get('alice')).toEqual(h.layouts.alice); }); it('does not claim or disturb a known foreign session found in a stale owner layout', async () => { const h = createHarness({ layouts: { alice: existing([ { kind: 'session', id: 'a1' }, { kind: 'session', id: 'b1' }, { kind: 'session', id: 'a2' }, ]), bob: existing([ { kind: 'session', id: 'b1' }, { kind: 'session', id: 'b2' }, ]), }, order: ['a1', 'b1', 'a2', 'b2'], live: [ { id: 'a1', owner: 'alice', createdAt: 1 }, { id: 'b1', owner: 'bob', createdAt: 2 }, { id: 'a2', owner: 'alice', createdAt: 3 }, { id: 'b2', owner: 'bob', createdAt: 4 }, ], }); const result = await h.service.putLegacyOrder({ owner: 'alice', isAdmin: false }, ['a2', 'a1']); expect(result).toMatchObject({ order: ['a2', 'a1'], changedOwnerOrders: { alice: ['a2', 'a1'] }, globalOrder: ['a2', 'b1', 'a1', 'b2'], globalChanged: true, }); expect(h.layouts.alice.ungrouped.map((ref) => ref.id)).toEqual(['a2', 'a1']); expect(h.layouts.bob.ungrouped.map((ref) => ref.id)).toEqual(['b1', 'b2']); expect(h.broadcastSessionOrder).toHaveBeenCalledExactlyOnceWith({ changedOwnerOrders: { alice: ['a2', 'a1'] }, globalOrder: ['a2', 'b1', 'a1', 'b2'], globalChanged: true, }); }); it('publishes layout and order together, suppresses duplicate order events, and rolls back store rejection', async () => { const h = createHarness({ layouts: { alice: existing([ { kind: 'session', id: 'a' }, { kind: 'session', id: 'b' }, ]), }, order: ['a', 'b'], live: [ { id: 'a', owner: 'alice', createdAt: 1 }, { id: 'b', owner: 'alice', createdAt: 2 }, ], }); const desired = { ...existing([], 7), groups: [ { id: 'g', name: 'Grouped', refs: [ { kind: 'session' as const, id: 'a' }, { kind: 'session' as const, id: 'b' }, ], }, ], }; await h.service.put('alice', desired, 7); expect(h.order).toEqual(['a', 'b']); expect(h.broadcast).toHaveBeenCalledTimes(1); expect(h.broadcastSessionOrder).not.toHaveBeenCalled(); await h.service.get('alice'); expect(h.broadcastSessionOrder).not.toHaveBeenCalled(); const beforeLayout = structuredClone(h.layouts.alice); const beforeOrder = [...h.order]; h.store.commitTabLayoutProjection.mockImplementationOnce(() => { throw new Error('publication failed'); }); h.broadcast.mockClear(); await expect(h.service.put('alice', { ...desired, groups: [] }, 8)).rejects.toThrow('publication failed'); expect(h.layouts.alice).toEqual(beforeLayout); expect(h.order).toEqual(beforeOrder); expect(h.broadcast).not.toHaveBeenCalled(); expect(h.broadcastSessionOrder).not.toHaveBeenCalled(); }); it('uses latest-state synchronous publication for overlapping owner preparations without a lost update', async () => { const h = createHarness({ layouts: { alice: existing([ { kind: 'session', id: 'a1' }, { kind: 'session', id: 'a2' }, ]), bob: existing([ { kind: 'session', id: 'b1' }, { kind: 'session', id: 'b2' }, ]), }, order: ['a1', 'b1', 'a2', 'b2'], live: [ { id: 'a1', owner: 'alice', createdAt: 1 }, { id: 'b1', owner: 'bob', createdAt: 2 }, { id: 'a2', owner: 'alice', createdAt: 3 }, { id: 'b2', owner: 'bob', createdAt: 4 }, ], }); await Promise.all([ h.service.putLegacyOrder({ owner: 'alice', isAdmin: false }, ['a2', 'a1']), h.service.putLegacyOrder({ owner: 'bob', isAdmin: false }, ['b2', 'b1']), ]); expect(h.order).toEqual(['a2', 'b2', 'a1', 'b1']); expect(h.layouts.alice.ungrouped.map((ref) => ref.id)).toEqual(['a2', 'a1']); expect(h.layouts.bob.ungrouped.map((ref) => ref.id)).toEqual(['b2', 'b1']); }); it('admin publishes all changed owner ranks atomically and can change only cross-owner interleaving', async () => { const makeHarness = () => createHarness({ layouts: { alice: existing([ { kind: 'session', id: 'a1' }, { kind: 'session', id: 'a2' }, ]), bob: existing([ { kind: 'session', id: 'b1' }, { kind: 'session', id: 'b2' }, ]), }, order: ['a1', 'b1', 'a2', 'b2'], live: [ { id: 'a1', owner: 'alice', createdAt: 1 }, { id: 'b1', owner: 'bob', createdAt: 2 }, { id: 'a2', owner: 'alice', createdAt: 3 }, { id: 'b2', owner: 'bob', createdAt: 4 }, ], }); const ranked = makeHarness(); await expect( ranked.service.putLegacyOrder({ owner: 'admin', isAdmin: true }, ['a2', 'b2', 'a1', 'b1']) ).resolves.toEqual({ order: ['a2', 'b2', 'a1', 'b1'], changedOwnerOrders: { alice: ['a2', 'a1'], bob: ['b2', 'b1'] }, globalOrder: ['a2', 'b2', 'a1', 'b1'], globalChanged: true, }); expect(Object.keys(ranked.store.commitTabLayoutProjection.mock.calls[0][0])).toEqual(['alice', 'bob']); expect(ranked.layouts.alice.version).toBe(8); expect(ranked.layouts.bob.version).toBe(8); const interleaved = makeHarness(); await expect( interleaved.service.putLegacyOrder({ owner: 'admin', isAdmin: true }, ['b1', 'a1', 'b2', 'a2']) ).resolves.toEqual({ order: ['b1', 'a1', 'b2', 'a2'], changedOwnerOrders: {}, globalOrder: ['b1', 'a1', 'b2', 'a2'], globalChanged: true, }); expect(interleaved.store.commitTabLayoutProjection.mock.calls[0][0]).toEqual({}); expect(interleaved.layouts.alice.version).toBe(7); expect(interleaved.layouts.bob.version).toBe(7); }); it('admin preserves requested owner slots while canonical grouped owner order wins', async () => { const h = createHarness({ layouts: { alice: { ...existing([{ kind: 'session', id: 'a2' }]), groups: [{ id: 'fixed', name: 'Fixed', refs: [{ kind: 'session', id: 'a1' }] }], }, bob: existing([ { kind: 'session', id: 'b1' }, { kind: 'session', id: 'b2' }, ]), }, order: ['a1', 'b1', 'a2', 'b2'], live: [ { id: 'a1', owner: 'alice', createdAt: 1 }, { id: 'b1', owner: 'bob', createdAt: 2 }, { id: 'a2', owner: 'alice', createdAt: 3 }, { id: 'b2', owner: 'bob', createdAt: 4 }, ], }); const result = await h.service.putLegacyOrder({ owner: 'admin', isAdmin: true }, ['b2', 'a2', 'b1', 'a1']); expect(result.globalOrder).toEqual(['b2', 'a1', 'b1', 'a2']); expect(result.order).toEqual(result.globalOrder); expect(result.changedOwnerOrders).toEqual({ bob: ['b2', 'b1'] }); expect(h.layouts.alice.version).toBe(7); expect(h.layouts.bob.version).toBe(8); }); it('admin publishes prototype-like owner keys as own layout entries', async () => { const owner = '__proto__'; const h = createHarness({ layouts: { [owner]: existing([ { kind: 'session', id: 'p1' }, { kind: 'session', id: 'p2' }, ]), }, order: ['p1', 'p2'], live: [ { id: 'p1', owner, createdAt: 1 }, { id: 'p2', owner, createdAt: 2 }, ], }); await h.service.putLegacyOrder({ owner: 'admin', isAdmin: true }, ['p2', 'p1']); const updates = h.store.commitTabLayoutProjection.mock.calls[0][0]; expect(Object.hasOwn(updates, owner)).toBe(true); expect(h.layouts[owner].version).toBe(8); expect(h.layouts[owner].ungrouped.map((ref) => ref.id)).toEqual(['p2', 'p1']); }); it('admin retries owner discovery when a new trusted owner appears while queues are pending', async () => { const h = createHarness({ layouts: { alice: existing([{ kind: 'session', id: 'a1' }]) }, order: ['a1'], live: [{ id: 'a1', owner: 'alice', createdAt: 1 }], }); let release!: () => void; const blocked = new Promise((resolve) => { release = resolve; }); h.readWebviews.mockImplementationOnce(async () => { await blocked; return []; }); const occupied = h.service.get('alice'); await vi.waitFor(() => expect(h.readWebviews).toHaveBeenCalledOnce()); const admin = h.service.putLegacyOrder({ owner: 'admin', isAdmin: true }, ['b1', 'a1']); h.live.set('b1', { id: 'b1', owner: 'bob', createdAt: 2 } as never); release(); await occupied; await expect(admin).resolves.toMatchObject({ globalOrder: ['b1', 'a1'] }); expect(h.layouts.bob).toMatchObject({ version: 0, ungrouped: [{ kind: 'session', id: 'b1' }] }); expect(Object.hasOwn(h.store.commitTabLayoutProjection.mock.calls.at(-1)![0], 'bob')).toBe(true); }); });