From ecfc13889b442204f9c032a4f6bb7f1e66fb845a Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Sun, 18 Oct 2026 18:24:52 -0400 Subject: [PATCH] test(layout-owner): pin that a paired server's workspace never gets layout.apply (#30430) Every renderer sender of layout.apply (sendLayoutApply, recordLayoutApply, the activation terminal create and the live terminal adoption) returns before the call for a worktree or folder workspace a paired server owns, and a source census pins that no other renderer file sends layout.apply. --- .../layout-apply-paired-server-guard.test.ts | 143 ++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 src/renderer/src/runtime/layout-apply-paired-server-guard.test.ts diff --git a/src/renderer/src/runtime/layout-apply-paired-server-guard.test.ts b/src/renderer/src/runtime/layout-apply-paired-server-guard.test.ts new file mode 100644 index 00000000000..3e5219a15a9 --- /dev/null +++ b/src/renderer/src/runtime/layout-apply-paired-server-guard.test.ts @@ -0,0 +1,143 @@ +// Pins that a workspace a paired server owns never reaches main's `layout.apply`: its layout stays +// the server's, mirrored by the window, in a layout-owner build too. +import { readdirSync, readFileSync, statSync } from 'node:fs' +import path from 'node:path' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { WorktreeOperationRouteState } from '@/lib/worktree-operation-route' +import type { AppState } from '@/store/types' + +const mocks = vi.hoisted(() => ({ callRuntimeRpc: vi.fn() })) +vi.mock('@/runtime/runtime-rpc-client', () => ({ callRuntimeRpc: mocks.callRuntimeRpc })) + +import { recordLayoutApply, sendLayoutApply, type WindowLayoutCommand } from './layout-apply-send' +import { runtimeEnvironmentIdUnlessTwin } from '@/lib/resolve-owner' +import { + adoptLiveTerminalThroughLayoutOwner, + createInitialTerminalThroughLayoutOwner +} from '@/store/terminals/terminal-tab-create-through-layout-owner' + +const WORKTREE_ID = 'repo-1::/srv/w' +const FOLDER_ID = 'folder:f1' + +const PAIRED_WORKSPACES: { + name: string + workspaceId: string + rows: WorktreeOperationRouteState +}[] = [ + { + name: 'a worktree on a paired server', + workspaceId: WORKTREE_ID, + rows: { + repos: [{ id: 'repo-1', executionHostId: 'runtime:env-b' }], + worktreesByRepo: { + 'repo-1': [ + { + id: WORKTREE_ID, + repoId: 'repo-1', + hostId: 'runtime:env-b', + runtimeOwnerEnvironmentId: 'env-b' + } + ] + } + } + }, + { + name: 'a folder workspace on a paired server', + workspaceId: FOLDER_ID, + rows: { + folderWorkspaces: [ + { + id: 'f1', + projectGroupId: 'g1', + connectionId: null, + executionHostId: 'runtime:env-b', + diffComments: [] + } + ] + } + } +] + +const COMMANDS: WindowLayoutCommand[] = [ + { type: 'splitPane', tabId: 't', leafId: 'l', direction: 'vertical', newLeafId: 'l2' }, + { type: 'closePane', tabId: 't', leafId: 'l' }, + { type: 'closeTabs', tabIds: ['t'] }, + { type: 'renameTab', tabId: 't', kind: 'custom', title: 'x' }, + { type: 'moveTab', tabId: 't', toGroupId: 'g', index: 0 }, + { type: 'createTerminalTab', tabId: 't', leafId: 'l' }, + { type: 'sleep', tabId: 't', paneKeys: ['t:l'], records: [] }, + { type: 'setDefaultTabsApplied' } +] + +function layoutApplyCalls(): unknown[][] { + return mocks.callRuntimeRpc.mock.calls.filter((call) => call[1] === 'layout.apply') +} + +describe.each(PAIRED_WORKSPACES)('$name', ({ workspaceId, rows }) => { + beforeEach(() => { + mocks.callRuntimeRpc.mockReset() + }) + + it('resolves to its server, so every guard below reads it as paired', () => { + expect(runtimeEnvironmentIdUnlessTwin(rows, workspaceId)).toBe('env-b') + }) + + it.each(COMMANDS)('sendLayoutApply sends no $type', (command) => { + const handlers = { apply: vi.fn(), asToday: vi.fn(), refused: vi.fn() } + + expect(sendLayoutApply(rows, workspaceId, command, handlers)).toBe(false) + recordLayoutApply(rows, workspaceId, command) + + expect(layoutApplyCalls()).toEqual([]) + expect(handlers.apply).not.toHaveBeenCalled() + }) + + it('the activation terminal create sends nothing', () => { + const create = vi.fn() + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the paired-server guard returns before anything but the owner rows is read. + const getState = () => rows as AppState + + expect(createInitialTerminalThroughLayoutOwner(getState, workspaceId, create)).toBe(false) + expect(layoutApplyCalls()).toEqual([]) + }) + + it('the live terminal adoption sends nothing', async () => { + await expect( + adoptLiveTerminalThroughLayoutOwner(() => rows, workspaceId, { ptyId: 'pty-1' }) + ).resolves.toBe('as-today') + expect(layoutApplyCalls()).toEqual([]) + }) +}) + +/** Every renderer source file that names the `layout.apply` method, with its count. */ +function layoutApplySenders(root: string): Record { + const found: Record = {} + const walk = (dir: string): void => { + for (const entry of readdirSync(dir)) { + const full = path.join(dir, entry) + if (statSync(full).isDirectory()) { + walk(full) + continue + } + if (!/\.tsx?$/.test(entry) || /\.(test|spec)\.tsx?$/.test(entry)) { + continue + } + const count = readFileSync(full, 'utf8').split(`'layout.apply'`).length - 1 + if (count > 0) { + found[path.relative(root, full).split(path.sep).join('/')] = count + } + } + } + walk(root) + return found +} + +describe('layout.apply senders', () => { + // A new sender must go through sendLayoutApply or carry the same guard, plus a case above. + it('are only the guarded paths pinned above', () => { + expect(layoutApplySenders(path.resolve(__dirname, '..'))).toEqual({ + 'runtime/layout-apply-send.ts': 1, + 'store/terminals/terminal-tab-create-through-layout-owner.ts': 2 + }) + }) +})