Files
Codeman/test/tab-layout-service.test.ts
T
Codeman maintainer 7a340fe7bc fix(tabs): review-driven hardening for the tab-layout foundation and vertical rail
Post-merge follow-ups from the deep review of #334 and #335, so they ship in
the same release as the features.

Tab-layout foundation (#335):
- PUT /api/session-order drops unknown/foreign ids again instead of 400ing
  the whole write, in both the owner and the admin path (single-user requests
  are the synthetic admin, so that path is the one the browser hits). The
  frontend debounces its reorder push and swallows errors, so a session
  deleted inside the debounce window silently cost the user the entire
  reorder - and the endpoint sits on the stable /api/v1 surface, where the
  pre-layout server merged leniently.
- A failed mux restore no longer locks explicit deletions into 500s for the
  process lifetime: runSessionDeletion and webviewDeleted degrade to
  best-effort without layout coordination, while the automated stale sweep
  (runStaleSessionCleanup) stays fail-closed.
- sse-events doc comment: no 'suppressed' hook event exists; hooks stay 8.
- registerSessionWithLayout resolves its owner through ownerLayoutKey()
  instead of a hardcoded '@single'.

Vertical rail (#334) - all rail-awareness gaps in sidebar-only predicates,
unified behind the new _isVerticalTabList() (sidebar OR rail):
- Drag-reorder read the insertion side from clientX in the rail, so
  before/after was effectively arbitrary on vertical rows; the drag-over
  indicators now draw as top/bottom edges there like the sidebar's.
- The active tab is scrolled into view in the rail (Alt+N/palette selection
  used to leave the row below the fold).
- Floating subagent/ultracode windows anchor to the RIGHT of rail tabs, and
  the connector redraw gates (render tail + strip scroll) cover the rail.
- Server-seeded tabOrientation is applied when the async settings load
  resolves, not only at boot, so a fresh device shows the rail immediately.
- The pre-paint script stamps data-tab-orientation and --tab-rail-width
  (sidebar-wins and solo carve-outs included), removing the flash of the
  header strip on every vertical-mode load.
- The session name font defaults to 12px, the sidebar's historical 0.75rem
  size, so installs that never touch the new slider are not restyled.

Also documents the rail in CLAUDE.md (second #sessionTabs host, mover
ordering, the axis-predicate rule) and gives tab-rail-resize.js its
@dependency/@loadorder header. Full gate green (6093 tests); the excluded
browser suite was run by hand - only the known environmental failures
(opencode/codex binaries) remain, identical to pristine master.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 00:59:48 +02:00

1276 lines
47 KiB
TypeScript

/**
* @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<string, TabLayout>;
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<Record<string, TabLayout>>, 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<void>((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<string>) => ({ 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<string>) => {
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<void>((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);
});
});