Files
orca/config/scripts/check-layout-writer-ratchets.test.mjs
T
Jinwoo Hong e969881be5 chore(ratchet): layout-writer ratchets for main and renderer (#27431)
* chore(ratchet): layout-writer ratchets for main and renderer

Two per-file ratchets that may only go down: main-process callers of the
Store's layout setters plus direct edits of saved session partitions, and
renderer writes of the window's layout and per-view selection fields
(including computed-key and spread writes, counted where the key or object
is built). The frozen-mirror writers are exempt. Wired next to the
focus-read ratchet in lint and the PR workflow.

* chore(ratchet): count only membership writes of editor and browser entries

A .map over the existing openFiles or browser tab list keeps its entries
and order, so it is a content update (draft, git status, URL) and is no
longer counted by the renderer layout-writer ratchet.

* chore(ratchet): ignore variable keys and pass-through shorthands

A variable key such as exitCodes[ptyId] names no field, so only string
keys count. A shorthand key counts only in an object handed to set,
setState or a Store writer; elsewhere it passes a value on.

* chore(ratchet): prune renderer baseline after main moved
2026-10-11 14:55:11 -04:00

74 lines
3.1 KiB
JavaScript

import { describe, expect, it } from 'vitest'
import { countLayoutWrites, isScannedPath, RATCHETS } from './check-layout-writer-ratchets.mjs'
const [mainRatchet, rendererRatchet] = RATCHETS
describe('main layout writers', () => {
it('counts Store setter calls, partition edits and session keys, not reads or types', () => {
const src = [
'store.setWorkspaceSession(next, hostId)',
'const save = store.persistPtyBinding.bind(store)',
'removeWorkspaceSessionOwners(state, ids)',
'this.state.workspaceSession = merged',
'session.tabsByWorktree[id] = tabs',
'delete partition.terminalLayoutsByTabId![tabId]',
'tab.ptyId = null',
'const next = { ...state, workspaceSession: merged }',
'const tabs = session.tabsByWorktree[id]',
'type T = { workspaceSession: Session }',
'const reply = { tabsByWorktree: tabs }',
'exitCodes[ptyId] = code',
'const byPty = { [ptyId]: code }'
].join('\n')
expect(countLayoutWrites(src, mainRatchet, 'a.ts')).toBe(8)
})
})
describe('renderer layout writes', () => {
it('counts assignments, object, shorthand and string keys, not same-name copies or types', () => {
const src = [
'set({ tabsByWorktree: next, activeTabId })',
'patch.tabsByWorktree![id] = tabs',
"const KEYS = ['groupsByWorktree', 'layoutByWorktree'] as const",
'patch[key] = value',
'const picked = useAppStore((s) => ({ openFiles: s.openFiles }))',
'const useTabs = () => ({ activeTabId, tabsByWorktree })',
"type Field = Pick<AppState, 'tabsByWorktree'>",
"const read = state['unifiedTabsByWorktree']",
'const { tabsByWorktree } = state'
].join('\n')
expect(countLayoutWrites(src, rendererRatchet, 'a.tsx')).toBe(5)
})
it('skips content updates of existing editor and browser entries, not membership changes', () => {
const src = [
'set({ openFiles: s.openFiles.map((f) => (f.id === id ? { ...f, isDirty: true } : f)) })',
'set({ browserTabsByWorktree: { ...s.browserTabsByWorktree, [wt]: (s.browserTabsByWorktree[wt] ?? []).map(withUrl) } })',
'set({ openFiles: [...s.openFiles, file] })',
'set({ openFiles: s.openFiles.filter((f) => f.id !== id) })',
'set({ tabsByWorktree: s.tabsByWorktree.map(rename) })'
].join('\n')
expect(countLayoutWrites(src, rendererRatchet, 'a.tsx')).toBe(3)
})
it('allows only the frozen mirror writers', () => {
expect(rendererRatchet.allowed('src/renderer/src/store/workspace-layout-cache.ts')).toBe(true)
expect(
rendererRatchet.allowed('src/renderer/src/runtime/remote-workspace-frozen/setters.ts')
).toBe(true)
expect(
rendererRatchet.allowed('src/renderer/src/store/slices/tabs/tabs-create-actions.ts')
).toBe(false)
})
})
describe('isScannedPath', () => {
it('skips tests, fixtures and harnesses', () => {
expect(isScannedPath('src/main/ipc/session.ts')).toBe(true)
expect(isScannedPath('src/main/ipc/session.test.ts')).toBe(false)
expect(isScannedPath('src/main/runtime/terminal-retirement-fixture.ts')).toBe(false)
expect(isScannedPath('src/main/ipc/session.md')).toBe(false)
})
})