test(layout): pin the layout-owner save format for downgrade, and keep main's touched-records save with an exhaustive guard (dark, layout-owner build only) (#31341)

* test(layout): downgrade probe for the layout-owner session format, and the touched-records patch's guard

Downgrade probe (ledger L8): a checked-in golden of the session main writes in a layout-owner
build (layout commands through the Store plus a window save), matched byte for byte; a
switch-off Store hands it on unchanged and a switch-off window restores its tabs, panes, groups,
editor file and browser tab. Regenerate with ORCA_UPDATE_LAYOUT_OWNER_GOLDEN=1.

L104: for each writer of the touched-records patch (layout.apply, the cross-partition move, the
window save), the patch equals a full save on every partition the load leaves alone, and differs
only on records the load normalizes otherwise. Two concrete legacy cases are pinned, and a
switch-off window restores each differently after a full save, so the patch stays.

* test(layout): type the guard's partition ids and locate the golden without import.meta

* test(layout): pin every legacy fixture's patch difference, assert restored panes and group selection, keep the golden deterministic
This commit is contained in:
Jinwoo Hong
2026-10-20 23:01:17 -04:00
committed by GitHub
parent 71cf31bf69
commit dda855e837
7 changed files with 1181 additions and 0 deletions
@@ -0,0 +1,536 @@
// Ledger L104: each writer of the touched-records patch against a full save of main's model. They
// differ on 50 of the 51 legacy fixture x writer pairs, only on records the Loader rewrites; every
// pair is pinned exactly, so the patch goes only once no partition holds such rows.
import { describe, expect, it, vi } from 'vitest'
import {
LOCAL_EXECUTION_HOST_ID,
toSshExecutionHostId,
type ExecutionHostId
} from '../../shared/execution-host'
import { collectLayoutLeafIdsInOrder } from '../../shared/workspace-layout/terminal-pane-tree'
import type { LayoutCommand } from '../../shared/workspace-layout/workspace-layout-command-types'
import { LEGACY_WORKSPACE_LAYOUT_PROFILES } from '../../shared/workspace-layout/workspace-layout-legacy-profile.test-fixture'
import { loadWorkspaceLayout } from '../../shared/workspace-layout/workspace-layout-load'
import {
diffStoredSession,
stableJson,
type LayoutLoadChange
} from '../../shared/workspace-layout/workspace-layout-load-report'
import { nameBasedLoadContext } from '../../shared/workspace-layout/workspace-layout-minted-ids'
import {
relaySshSession,
WORKSPACE_LAYOUT_PROFILES,
type WorkspaceLayoutProfile
} from '../../shared/workspace-layout/workspace-layout-profile.test-fixture'
import type * as RecordPatch from '../../shared/workspace-layout/workspace-layout-record-patch'
import { saveWorkspaceLayout } from '../../shared/workspace-layout/workspace-layout-save'
import { SSH_KEY } from '../../shared/workspace-layout/workspace-layout-session.test-fixture'
import type { WorkspaceSessionState } from '../../shared/workspace-session-state-types'
import { moveTabAcrossPartitions } from './terminal-topology/cross-partition-tab-move'
import {
admitPaneToSession,
applyCommandToSession,
applyRenamesToSession
} from './terminal-topology/terminal-pane-admission'
import { windowViewPatchForMain, windowViewSaveForMain } from './window-save-view'
vi.mock('../../shared/layout-owner-switch', () => ({ ORCA_LAYOUT_OWNER_IS_MAIN: true }))
/** Each patch a writer made: the stored partition, the full saves before and after, the patch. */
type PatchCall = {
hostId: ExecutionHostId
stored: WorkspaceSessionState
before: WorkspaceSessionState
full: WorkspaceSessionState
patched: WorkspaceSessionState
}
const patches = vi.hoisted(() => {
const calls: PatchCall[] = []
const hostIds: ExecutionHostId[] = []
return { calls, hostIds }
})
vi.mock('../../shared/workspace-layout/workspace-layout-record-patch', async (importOriginal) => {
const actual = await importOriginal<typeof RecordPatch>()
return {
patchStoredSession: (
stored: WorkspaceSessionState,
before: WorkspaceSessionState,
full: WorkspaceSessionState
) => {
const patched = actual.patchStoredSession(stored, before, full)
patches.calls.push({
hostId: patches.hostIds.shift() ?? LOCAL_EXECUTION_HOST_ID,
stored,
before,
full,
patched
})
return patched
}
}
})
const SSH_HOST = toSshExecutionHostId('target-1')
const MOVED_FILE = {
filePath: '/Users/dev/api/readme.md',
relativePath: 'readme.md',
language: 'markdown'
}
/** Records (or session fields) two partitions hold differently. */
const recordsDiffering = (left: WorkspaceSessionState, right: WorkspaceSessionState) =>
[
...new Set(
diffStoredSession(left, right).map(({ table, record, field }) =>
table === 'session' ? `session/${field}` : `${table}/${record}`
)
)
].sort()
/** What the partition loads to; the revision is exempt, as every load's tombstone pass bumps it. */
const reloaded = (hostId: ExecutionHostId, session: WorkspaceSessionState) =>
stableJson({
...saveWorkspaceLayout(loadWorkspaceLayout(hostId, session, nameBasedLoadContext())),
terminalTopologyRevisionByRepoId: undefined
})
/** The patches one write made, each with the partition it wrote (in the order it patches them). */
function captured(hostIds: readonly ExecutionHostId[], run: () => void): PatchCall[] {
patches.calls = []
patches.hostIds = [...hostIds]
run()
return patches.calls
}
type Writer = (profile: WorkspaceLayoutProfile) => PatchCall[][]
function firstTerminal(profile: WorkspaceLayoutProfile) {
const stored = profile.build()
const loaded = loadWorkspaceLayout(profile.hostId, stored, nameBasedLoadContext())
for (const [workspace, layout] of Object.entries(loaded.layout.workspaces)) {
const tab = layout.tabs.find((candidate) => candidate.kind === 'terminal')
if (tab?.kind === 'terminal') {
const leafId = collectLayoutLeafIdsInOrder(tab.panes.root)[0]
return { stored, workspace, tabId: tab.id, leafId, groupId: layout.groups[0]!.id }
}
}
return null
}
const NEW_LEAF = '99999999-9999-4999-8999-999999999999'
function layoutApplyCommands(
target: NonNullable<ReturnType<typeof firstTerminal>>
): LayoutCommand[] {
const { workspace, tabId, leafId, groupId } = target
return [
{ type: 'renameTab', workspace, tabId, kind: 'custom', title: 'Renamed' },
{
type: 'splitPane',
workspace,
tabId,
leafId: leafId!,
direction: 'vertical',
newLeafId: NEW_LEAF
},
{ type: 'closeTabs', workspace, tabIds: [tabId], force: true },
{
type: 'createTerminalTab',
workspace,
tabId: 'tab-new',
leafId: NEW_LEAF,
groupId,
createdAt: 1
}
]
}
const APPLY = 'layout.apply (terminal-pane-admission)'
const MOVE = 'cross-partition editor move'
const SAVE = 'window save (window-save-view)'
type WriterName = typeof APPLY | typeof MOVE | typeof SAVE
const WRITERS: Record<WriterName, Writer> = {
[APPLY]: (profile) => {
const target = firstTerminal(profile)
if (!target) {
return []
}
return [
...layoutApplyCommands(target).map((command) =>
captured([profile.hostId], () =>
applyCommandToSession(profile.hostId, target.stored, command)
)
),
captured([profile.hostId], () =>
applyRenamesToSession(profile.hostId, target.stored, [
{
type: 'renameTab',
workspace: target.workspace,
tabId: target.tabId,
kind: 'generated',
title: 'vim'
}
])
),
captured([profile.hostId], () =>
admitPaneToSession(profile.hostId, target.stored, {
type: 'splitPane',
workspace: target.workspace,
tabId: target.tabId,
leafId: target.leafId!,
direction: 'horizontal',
newLeafId: NEW_LEAF
})
)
]
},
[MOVE]: (profile) => {
const target = firstTerminal(profile)
if (!target || profile.hostId !== LOCAL_EXECUTION_HOST_ID) {
return []
}
const opened = applyCommandToSession(profile.hostId, target.stored, {
type: 'openEditorTab',
workspace: target.workspace,
tabId: 'e-move',
fileId: MOVED_FILE.filePath,
contentType: 'editor',
groupId: target.groupId,
createdAt: 1,
file: MOVED_FILE
}).session!
return [
captured([profile.hostId, SSH_HOST], () =>
moveTabAcrossPartitions(
{ hostId: profile.hostId, session: opened },
{ hostId: SSH_HOST, session: relaySshSession('target-1') },
{
type: 'moveTabToWorkspace',
workspace: target.workspace,
toWorkspace: SSH_KEY,
tabs: [
{
tabId: 'e-move',
newTabId: 'e-move',
newEntityId: `editor:${SSH_KEY}:${MOVED_FILE.filePath}`
}
],
groupId: 'group-ssh',
file: MOVED_FILE
}
)
)
]
},
[SAVE]: (profile) => {
const stored = profile.build()
const firstTab = Object.values(stored.tabsByWorktree).flat()[0]
// The window echoes the stored partition with a new selection and a title its view reported.
const save: WorkspaceSessionState = {
...stored,
activeTabId: firstTab?.id ?? null,
tabsByWorktree: Object.fromEntries(
Object.entries(stored.tabsByWorktree).map(([key, rows]) => [
key,
rows.map((row, index) => (index === 0 ? { ...row, title: 'reported' } : row))
])
)
}
return [
captured([profile.hostId], () => windowViewSaveForMain(stored, save, profile.hostId)),
captured([profile.hostId], () =>
windowViewPatchForMain(stored, { tabsByWorktree: save.tabsByWorktree }, profile.hostId)
)
]
}
}
/** The session list each record table is stored in: a record the load rewrites can change it. */
const LIST_FIELD_OF_TABLE: Readonly<Record<string, string>> = {
row: 'tabsByWorktree',
terminalEntry: 'unifiedTabs',
entry: 'unifiedTabs',
group: 'tabGroups',
layout: 'terminalLayoutsByTabId',
file: 'openFilesByWorktree',
browser: 'browserTabsByWorktree'
}
/**
* Whether a session list field's workspace keys differ only where the load rewrote a record of
* that list: a full save drops a workspace's emptied list that the patch keeps with its stored row.
*/
function listKeysFollowLoad(change: LayoutLoadChange, normalized: readonly string[]): boolean {
const tables = Object.keys(LIST_FIELD_OF_TABLE).filter(
(table) => LIST_FIELD_OF_TABLE[table] === change.field
)
const keys = (value: unknown) => (Array.isArray(value) ? value.map(String) : [])
const before = keys(change.before)
const after = keys(change.after)
const moved = [
...before.filter((key) => !after.includes(key)),
...after.filter((key) => !before.includes(key))
]
return (
tables.length > 0 &&
moved.length > 0 &&
moved.every((key) =>
normalized.some((record) => tables.some((table) => record.startsWith(`${table}/${key}|`)))
)
)
}
/** The records a writer's patches differ from its full saves in. */
function differingRecords(calls: readonly PatchCall[]): string[] {
const differing = new Set<string>()
for (const call of calls) {
const normalized = recordsDiffering(call.stored, call.before)
const differs = recordsDiffering(call.full, call.patched)
const listKeyChanges = diffStoredSession(call.full, call.patched).filter(
(change) => change.table === 'session' && listKeysFollowLoad(change, normalized)
)
const allowed = [...normalized, ...listKeyChanges.map((change) => `session/${change.field}`)]
// Only records the load rewrites from what the stored partition says can differ.
expect(differs.filter((record) => !allowed.includes(record))).toEqual([])
// Either way the partition loads to main's model.
expect(reloaded(call.hostId, call.patched)).toBe(reloaded(call.hostId, call.full))
differs.forEach((record) => differing.add(record))
}
return [...differing].sort()
}
const GIT = 'repo-1::/Users/dev/orca'
// The two a switch-off window restores differently from their full save, dropping tab-b or adding
// it (legacy-partition-full-save-hydration.test.ts); the others differ only in records on disk.
const SURFACE_TOMBSTONE = 'legacy surface tombstone on a pane still showing its terminal'
const HIDDEN_ROW = 'row without a tab-bar entry or a terminal'
/** Every legacy fixture's difference per writer; absent is none. */
const PINNED: Readonly<Record<string, Partial<Record<WriterName, readonly string[]>>>> = {
[HIDDEN_ROW]: {
[APPLY]: [`entry/${GIT}|$order`, `group/${GIT}|g1`, `terminalEntry/${GIT}|tab-b`],
[MOVE]: [`entry/${GIT}|$order`, `terminalEntry/${GIT}|tab-b`],
[SAVE]: [`entry/${GIT}|$order`, `group/${GIT}|g1`, `terminalEntry/${GIT}|tab-b`]
},
'row without a tab-bar entry or a terminal, selected': {
[APPLY]: [`entry/${GIT}|$order`, `group/${GIT}|g1`, `terminalEntry/${GIT}|tab-b`],
[MOVE]: [`entry/${GIT}|$order`, `terminalEntry/${GIT}|tab-b`],
[SAVE]: [`entry/${GIT}|$order`, `group/${GIT}|g1`, `terminalEntry/${GIT}|tab-b`]
},
'row without a tab-bar entry, its terminal in a workspace slept at shutdown': {
[APPLY]: [`entry/${GIT}|$order`, `group/${GIT}|g1`, `terminalEntry/${GIT}|tab-b`],
[MOVE]: [`entry/${GIT}|$order`, `terminalEntry/${GIT}|tab-b`],
[SAVE]: [`entry/${GIT}|$order`, `group/${GIT}|g1`, `terminalEntry/${GIT}|tab-b`]
},
'row without a tab-bar entry, still bound to a terminal': {
[APPLY]: [`entry/${GIT}|$order`, `group/${GIT}|g1`, `terminalEntry/${GIT}|tab-b`],
[MOVE]: [`entry/${GIT}|$order`, `terminalEntry/${GIT}|tab-b`],
[SAVE]: [`entry/${GIT}|$order`, `group/${GIT}|g1`, `terminalEntry/${GIT}|tab-b`]
},
'row without a tab-bar entry, its terminal only on a relay': {
[APPLY]: [`entry/${GIT}|$order`, `group/${GIT}|g1`, `terminalEntry/${GIT}|tab-b`],
[MOVE]: [`entry/${GIT}|$order`, `terminalEntry/${GIT}|tab-b`],
[SAVE]: [`entry/${GIT}|$order`, `group/${GIT}|g1`, `terminalEntry/${GIT}|tab-b`]
},
'row without a tab-bar entry, with a sleeping agent': {
[APPLY]: [`entry/${GIT}|$order`, `group/${GIT}|g1`, `terminalEntry/${GIT}|tab-b`],
[MOVE]: [`entry/${GIT}|$order`, `terminalEntry/${GIT}|tab-b`],
[SAVE]: [`entry/${GIT}|$order`, `group/${GIT}|g1`, `terminalEntry/${GIT}|tab-b`]
},
'row bound only through its pane, without a tab-bar entry': {
[APPLY]: [
`entry/${GIT}|$order`,
`group/${GIT}|g1`,
`row/${GIT}|tab-b`,
`terminalEntry/${GIT}|tab-b`
],
[MOVE]: [`entry/${GIT}|$order`, `row/${GIT}|tab-b`, `terminalEntry/${GIT}|tab-b`],
[SAVE]: [
`entry/${GIT}|$order`,
`group/${GIT}|g1`,
`row/${GIT}|tab-b`,
`terminalEntry/${GIT}|tab-b`
]
},
'legacy row repeating a tab-bar tab’s terminal': {
[APPLY]: ['layout/tab-legacy', `row/${GIT}|$order`, `row/${GIT}|tab-legacy`],
[MOVE]: ['layout/tab-legacy', `row/${GIT}|$order`, `row/${GIT}|tab-legacy`],
[SAVE]: ['layout/tab-legacy', `row/${GIT}|$order`, `row/${GIT}|tab-legacy`]
},
'legacy row repeating a tab-bar tab’s terminal, selected': {
[APPLY]: [
'layout/tab-legacy',
`row/${GIT}|$order`,
`row/${GIT}|tab-legacy`,
'session/activeTabId',
'session/activeTabIdByWorktree'
],
[MOVE]: [
'layout/tab-legacy',
`row/${GIT}|$order`,
`row/${GIT}|tab-legacy`,
'session/activeTabId',
'session/activeTabIdByWorktree'
],
[SAVE]: [
'layout/tab-legacy',
`row/${GIT}|$order`,
`row/${GIT}|tab-legacy`,
'session/activeTabId',
'session/activeTabIdByWorktree'
]
},
'legacy split row sharing one pane’s terminal with a tab-bar tab': {
[APPLY]: [
`entry/${GIT}|$order`,
`group/${GIT}|g1`,
'layout/tab-legacy',
`row/${GIT}|tab-legacy`,
`terminalEntry/${GIT}|tab-legacy`
],
[MOVE]: [
`entry/${GIT}|$order`,
'layout/tab-legacy',
`row/${GIT}|tab-legacy`,
`terminalEntry/${GIT}|tab-legacy`
],
[SAVE]: [
`entry/${GIT}|$order`,
`group/${GIT}|g1`,
'layout/tab-legacy',
`row/${GIT}|tab-legacy`,
`terminalEntry/${GIT}|tab-legacy`
]
},
'row terminal differs from its focused pane’s': {
[APPLY]: [`row/${GIT}|tab-a`],
[MOVE]: [`row/${GIT}|tab-a`],
[SAVE]: [`row/${GIT}|tab-a`]
},
'row with no terminal while its pane has one': {
[APPLY]: [`row/${GIT}|tab-a`],
[MOVE]: [`row/${GIT}|tab-a`],
[SAVE]: [`row/${GIT}|tab-a`]
},
'tab listed by two groups, its tab-bar entry naming the second': {
[APPLY]: [`group/${GIT}|g1`],
[SAVE]: [`group/${GIT}|g1`]
},
'session from before the tab bar, with an open file': {
[APPLY]: [
`entry/${GIT}|$order`,
`group/${GIT}|$order`,
`group/${GIT}|1a5387f9-46df-5b0c-807c-4cf31d90223e`,
'session/tabGroupLayouts',
'session/tabGroups',
'session/unifiedTabs',
`terminalEntry/${GIT}|tab-a`,
`terminalEntry/${GIT}|tab-b`
],
[MOVE]: [
`entry/${GIT}|$order`,
'session/tabGroupLayouts',
`terminalEntry/${GIT}|tab-a`,
`terminalEntry/${GIT}|tab-b`
],
[SAVE]: [
`entry/${GIT}|$order`,
`group/${GIT}|$order`,
`group/${GIT}|1a5387f9-46df-5b0c-807c-4cf31d90223e`,
'session/tabGroupLayouts',
'session/tabGroups',
`terminalEntry/${GIT}|tab-b`
]
},
'session from before the tab bar, a row with no terminal': {
[APPLY]: [
`entry/${GIT}|$order`,
`group/${GIT}|$order`,
`group/${GIT}|1a5387f9-46df-5b0c-807c-4cf31d90223e`,
'session/tabGroupLayouts',
'session/tabGroups',
'session/unifiedTabs',
`terminalEntry/${GIT}|tab-a`,
`terminalEntry/${GIT}|tab-b`
],
[MOVE]: [
`entry/${GIT}|$order`,
'session/tabGroupLayouts',
`terminalEntry/${GIT}|tab-a`,
`terminalEntry/${GIT}|tab-b`
],
[SAVE]: [
`entry/${GIT}|$order`,
`group/${GIT}|$order`,
`group/${GIT}|1a5387f9-46df-5b0c-807c-4cf31d90223e`,
'session/tabGroupLayouts',
'session/tabGroups',
`terminalEntry/${GIT}|tab-b`
]
},
[SURFACE_TOMBSTONE]: {
[APPLY]: [
`entry/${GIT}|$order`,
`group/${GIT}|g1`,
'layout/tab-b',
`row/${GIT}|$order`,
`row/${GIT}|tab-b`,
'session/terminalSurfaceTombstonesByPaneKey',
'session/terminalTopologyRevisionByRepoId',
'session/unifiedTabs',
`terminalEntry/${GIT}|tab-b`
],
[MOVE]: [
`entry/${GIT}|$order`,
'layout/tab-b',
`row/${GIT}|$order`,
`row/${GIT}|tab-b`,
'session/terminalSurfaceTombstonesByPaneKey',
'session/terminalTopologyRevisionByRepoId',
`terminalEntry/${GIT}|tab-b`
],
[SAVE]: [
`entry/${GIT}|$order`,
`group/${GIT}|g1`,
'layout/tab-b',
`row/${GIT}|$order`,
`row/${GIT}|tab-b`,
'session/terminalSurfaceTombstonesByPaneKey',
'session/terminalTopologyRevisionByRepoId',
`terminalEntry/${GIT}|tab-b`
]
},
'tab-bar label differing from the row title': {
[APPLY]: [`row/${GIT}|tab-a`],
[MOVE]: [`row/${GIT}|tab-a`],
[SAVE]: [`row/${GIT}|tab-a`]
}
}
describe.each(([APPLY, MOVE, SAVE] as const).map((name) => [name, WRITERS[name]] as const))(
'%s: touched-records patch vs a full save',
(writer, write) => {
it.each(WORKSPACE_LAYOUT_PROFILES.map((profile) => [profile.name, profile] as const))(
'%s: the same session, key order aside, as its load normalizes nothing',
(_name, profile) => {
for (const call of write(profile).flat()) {
expect(recordsDiffering(call.stored, call.before)).toEqual([])
expect(stableJson(call.patched)).toBe(stableJson(call.full))
}
}
)
it.each(LEGACY_WORKSPACE_LAYOUT_PROFILES.map((profile) => [profile.name, profile] as const))(
'%s: differs only on records the load normalizes',
(_name, profile) => {
const calls = write(profile).flat()
expect(calls.length).toBeGreaterThan(0)
const differing = differingRecords(calls)
// Exact both ways: a fixture that converges, or a new divergence, fails here.
expect(differing).toEqual(PINNED[profile.name]?.[writer] ?? [])
}
)
}
)
@@ -0,0 +1,185 @@
import type * as NodeCrypto from 'node:crypto'
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import {
LAYOUT_OWNER_WRITTEN_SESSION_GOLDEN_PATH,
LAYOUT_OWNER_WRITTEN_SESSION_WORKTREE_ID as WORKTREE
} from '../../../shared/__fixtures__/layout-owner-written-session-golden-paths'
import type { LayoutCommand } from '../../../shared/workspace-layout/workspace-layout-command-types'
import type { WorkspaceSessionState } from '../../../shared/workspace-session-state-types'
import type { Store } from '../loading-store/store'
import { ProfileStateSqliteAuthority } from '../profile-state/profile-state-sqlite-authority'
import { windowViewSaveForMain } from '../window-save-view'
import {
emptyTerminalSessionProfile,
FIXTURE_GIT_WORKTREE_ID,
openTopologyStore
} from './terminal-topology-profile-fixture'
vi.mock('../../../shared/layout-owner-switch', () => ({ ORCA_LAYOUT_OWNER_IS_MAIN: true }))
vi.mock('../../telemetry/client', () => ({ track: vi.fn() }))
vi.mock('../../ssh/config/ssh-config-parser', () => ({
loadUserSshConfig: () => ({ hosts: [] }),
sshConfigHostsToTargets: () => []
}))
// Ids main mints while it applies the commands are numbered, so the written bytes are stable.
const minting = vi.hoisted(() => ({ on: false, next: 0 }))
vi.mock('node:crypto', async (importOriginal) => {
const actual = await importOriginal<typeof NodeCrypto>()
return {
...actual,
randomUUID: () =>
minting.on
? `00000000-0000-4000-8000-${String(++minting.next).padStart(12, '0')}`
: actual.randomUUID()
}
})
const UPDATE_ENV = 'ORCA_UPDATE_LAYOUT_OWNER_GOLDEN'
const leaf = (n: number): string => `${n}${n}${n}${n}${n}${n}${n}${n}-1111-4111-8111-111111111111`
const on = { workspace: WORKTREE }
const directories: string[] = []
afterEach(() => {
minting.on = false
vi.useRealTimers()
for (const directory of directories.splice(0)) {
rmSync(directory, { recursive: true, force: true })
}
})
/** Two groups, a split, an editor file, a browser tab, a renamed tab and a closed tab. */
const COMMANDS: LayoutCommand[] = [
{ ...on, type: 'createTerminalTab', tabId: 'tab-shell', leafId: leaf(1), createdAt: 1 },
{ ...on, type: 'createTerminalTab', tabId: 'tab-build', leafId: leaf(2), createdAt: 2 },
{
...on,
type: 'splitPane',
tabId: 'tab-shell',
leafId: leaf(1),
direction: 'vertical',
newLeafId: leaf(3)
},
{ ...on, type: 'renameTab', tabId: 'tab-build', kind: 'custom', title: 'Build' },
{
...on,
type: 'openEditorTab',
tabId: 'tab-readme',
fileId: '/fixture/local/README.md',
contentType: 'editor',
createdAt: 3,
file: { filePath: '/fixture/local/README.md', relativePath: 'README.md', language: 'markdown' }
},
{
...on,
type: 'openBrowserTab',
tabId: 'tab-docs',
browserTabId: 'browser-docs',
pageId: 'page-docs',
createdAt: 4,
browserCreatedAt: 4
},
{ ...on, type: 'createTerminalTab', tabId: 'tab-scratch', leafId: leaf(4), createdAt: 5 },
{ ...on, type: 'closeTabs', tabIds: ['tab-scratch'], force: true, recordClose: 'user' }
]
function windowSave(stored: WorkspaceSessionState): WorkspaceSessionState {
const page = {
id: 'page-docs',
workspaceId: 'browser-docs',
worktreeId: WORKTREE,
url: 'https://example.com/docs',
title: 'Docs',
loading: false,
faviconUrl: null,
canGoBack: false,
canGoForward: false,
loadError: null,
createdAt: 4
}
return {
...stored,
activeRepoId: 'repo-local',
activeWorktreeId: WORKTREE,
activeTabId: 'tab-shell',
browserPagesByWorkspace: { 'browser-docs': [page] },
browserTabsByWorktree: {
[WORKTREE]: (stored.browserTabsByWorktree?.[WORKTREE] ?? []).map((tab) => ({
...tab,
url: page.url,
title: page.title
}))
}
}
}
async function apply(store: Store, command: LayoutCommand): Promise<void> {
const result = await store.applyLayoutCommand(command)
expect(result.ok, command.type).toBe(true)
}
/** The session main wrote to disk, as the profile database holds it. */
function writtenSession(directory: string): string {
const authority = new ProfileStateSqliteAuthority(join(directory, 'profile-state.db'), 'topology')
const serialized = authority.readSerializedState()
authority.close()
const profile: { workspaceSession: unknown } = JSON.parse(serialized ?? '{}')
return `${JSON.stringify(profile.workspaceSession, null, 2)}\n`
}
// Proves the current switch-off code reads a switch-on save, not that older released builds do.
describe('the session a layout-owner build writes (downgrade probe, ledger L8)', () => {
it('is the checked-in golden, byte for byte', async () => {
expect(WORKTREE).toBe(FIXTURE_GIT_WORKTREE_ID)
const directory = mkdtempSync(join(tmpdir(), 'orca-layout-owner-golden-'))
directories.push(directory)
const store = await openTopologyStore(directory, emptyTerminalSessionProfile())
vi.useFakeTimers({ toFake: ['Date'], now: 1_700_000_000_000 })
minting.on = true
for (const command of COMMANDS.slice(0, 4)) {
await apply(store, command)
}
const shellGroup = store.getWorkspaceSession().tabGroups?.[WORKTREE]?.[0]?.id
await apply(store, {
...on,
type: 'splitGroup',
tabId: 'tab-build',
besideGroupId: shellGroup!,
direction: 'right'
})
for (const command of COMMANDS.slice(4)) {
await apply(store, command)
}
await store.persistPtyBinding({
worktreeId: WORKTREE,
tabId: 'tab-shell',
leafId: leaf(1),
ptyId: 'pty-shell',
incarnationId: 'inc-shell'
})
minting.on = false
// The window's own save (session:set): its selection and the page its browser tab shows.
const stored = store.getWorkspaceSession()
store.setWorkspaceSession(windowViewSaveForMain(stored, windowSave(stored), undefined))
await store.flushPendingOrThrowAsync()
await store.freezeWritesAsync()
const written = writtenSession(directory)
// Times are pinned (fake Date) and minted ids numbered; no machine path may reach the golden.
expect(written).not.toContain(tmpdir())
if (process.env[UPDATE_ENV] === '1') {
writeFileSync(LAYOUT_OWNER_WRITTEN_SESSION_GOLDEN_PATH, written)
}
const golden = JSON.parse(readFileSync(LAYOUT_OWNER_WRITTEN_SESSION_GOLDEN_PATH, 'utf8'))
// Re-serialized so the formatter's line breaks do not count; key order and values do.
expect(
written,
`the switch-on save format changed: regenerate with ${UPDATE_ENV}=1, then the switch-off ` +
'restore tests (layout-owner-written-session-switch-off-load, ' +
'layout-owner-written-session-hydration) must still pass: they are the downgrade proof'
).toBe(`${JSON.stringify(golden, null, 2)}\n`)
})
})
@@ -0,0 +1,42 @@
import { mkdtempSync, readFileSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { LAYOUT_OWNER_WRITTEN_SESSION_GOLDEN_PATH } from '../../../shared/__fixtures__/layout-owner-written-session-golden-paths'
import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../shared/layout-owner-switch'
import { stableJson } from '../../../shared/workspace-layout/workspace-layout-load-report'
import type { WorkspaceSessionState } from '../../../shared/workspace-session-state-types'
import { emptyTerminalSessionProfile, openTopologyStore } from './terminal-topology-profile-fixture'
vi.mock('../../telemetry/client', () => ({ track: vi.fn() }))
vi.mock('../../ssh/config/ssh-config-parser', () => ({
loadUserSshConfig: () => ({ hosts: [] }),
sshConfigHostsToTargets: () => []
}))
const directories: string[] = []
afterEach(() => {
for (const directory of directories.splice(0)) {
rmSync(directory, { recursive: true, force: true })
}
})
describe.skipIf(ORCA_LAYOUT_OWNER_IS_MAIN)(
'a switch-off build loading the session a layout-owner build wrote (downgrade probe, L8)',
() => {
it('hands the window the session exactly as written', async () => {
const golden: WorkspaceSessionState = JSON.parse(
readFileSync(LAYOUT_OWNER_WRITTEN_SESSION_GOLDEN_PATH, 'utf8')
)
const directory = mkdtempSync(join(tmpdir(), 'orca-layout-owner-downgrade-'))
directories.push(directory)
const profile = { ...JSON.parse(emptyTerminalSessionProfile()), workspaceSession: golden }
const store = await openTopologyStore(directory, JSON.stringify(profile))
expect(stableJson(store.getWorkspaceSession())).toBe(stableJson(golden))
await store.freezeWritesAsync()
})
}
)
@@ -0,0 +1,124 @@
import { readFileSync } from 'node:fs'
import { describe, expect, it, vi } from 'vitest'
import type * as AgentStatusModule from '@/lib/agent-status'
import { reconcileHydratedWorkspaceTabModels } from '../../app-shell/reconcile-hydrated-workspace-tab-models'
import {
LAYOUT_OWNER_WRITTEN_SESSION_GOLDEN_PATH,
LAYOUT_OWNER_WRITTEN_SESSION_WORKTREE_ID as WT
} from '../../../../shared/__fixtures__/layout-owner-written-session-golden-paths'
import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../../shared/layout-owner-switch'
import { collectLayoutLeafIdsInOrder } from '../../../../shared/workspace-layout/terminal-pane-tree'
import type { WorkspaceSessionState } from '../../../../shared/workspace-session-state-types'
import { createTestStore, makeWorktree } from './store-test-helpers'
import { createStoreSessionMockApi } from './store-session-test-harness'
vi.mock('sonner', () => ({ toast: { info: vi.fn(), success: vi.fn(), error: vi.fn() } }))
vi.mock('@/lib/agent-status', async (importOriginal) => {
const actual = await importOriginal<typeof AgentStatusModule>()
return { ...actual, detectAgentStatusFromTitle: vi.fn().mockReturnValue(null) }
})
createStoreSessionMockApi()
/** The startup hydration a switch-off window runs (use-app-startup-hydration.ts). */
function restoredWindow(session: WorkspaceSessionState) {
const store = createTestStore()
store.setState({
repos: [
{
id: 'repo-local',
path: '/fixture/local',
displayName: 'local',
badgeColor: '#000',
addedAt: 0
}
],
worktreesByRepo: {
'repo-local': [makeWorktree({ id: WT, repoId: 'repo-local', path: '/fixture/local' })]
}
})
const actions = store.getState()
actions.hydrateWorkspaceSession(session, {})
actions.hydrateTabsSession(session, {})
actions.hydrateEditorSession(session, {})
actions.hydrateBrowserSession(session, {})
reconcileHydratedWorkspaceTabModels(session, store.getState().reconcileWorktreeTabModels)
return store.getState()
}
const leaf = (n: number): string => `${n}${n}${n}${n}${n}${n}${n}${n}-1111-4111-8111-111111111111`
describe.skipIf(ORCA_LAYOUT_OWNER_IS_MAIN)(
'a switch-off window restoring the session a layout-owner build wrote (downgrade probe, L8)',
() => {
it('shows the same tabs, panes, groups, editor file and browser tab', () => {
const golden: WorkspaceSessionState = JSON.parse(
readFileSync(LAYOUT_OWNER_WRITTEN_SESSION_GOLDEN_PATH, 'utf8')
)
const state = restoredWindow(golden)
expect(
state.tabsByWorktree[WT]?.map((tab) => [tab.id, tab.customTitle ?? tab.title])
).toEqual([
['tab-shell', 'Terminal 1'],
['tab-build', 'Build']
])
const panes = (tabId: string) => {
const layout = state.terminalLayoutsByTabId[tabId]
return {
leaves: collectLayoutLeafIdsInOrder(layout?.root),
active: layout?.activeLeafId,
bindings: layout?.ptyIdsByLeafId
}
}
expect(state.terminalLayoutsByTabId['tab-shell']?.root).toMatchObject({
type: 'split',
direction: 'vertical'
})
// The pane binding must survive the downgrade, or the shell would not reattach.
expect(panes('tab-shell')).toEqual({
leaves: [leaf(1), leaf(3)],
active: leaf(1),
bindings: { [leaf(1)]: 'pty-shell' }
})
expect(panes('tab-build')).toEqual({ leaves: [leaf(2)], active: leaf(2), bindings: {} })
const groups = state.groupsByWorktree[WT] ?? []
expect(groups.map((group) => [group.tabOrder, group.activeTabId])).toEqual([
[['tab-shell', 'tab-readme', 'tab-docs'], 'tab-shell'],
[['tab-build'], 'tab-build']
])
expect(state.layoutByWorktree[WT]).toMatchObject({ type: 'split', direction: 'horizontal' })
const byId = new Map(state.unifiedTabsByWorktree[WT]?.map((tab) => [tab.id, tab]))
expect(
groups.map((group) =>
group.tabOrder.map((id) => [byId.get(id)?.contentType, byId.get(id)?.groupId])
)
).toEqual([
[
['terminal', groups[0]?.id],
['editor', groups[0]?.id],
['browser', groups[0]?.id]
],
[['terminal', groups[1]?.id]]
])
expect(byId.size).toBe(4)
expect(
state.unifiedTabsByWorktree[WT]?.find((tab) => tab.id === 'tab-build')?.customLabel
).toBe('Build')
expect(state.openFiles.map((file) => [file.filePath, file.mode])).toEqual([
['/fixture/local/README.md', 'edit']
])
expect(state.browserTabsByWorktree[WT]?.map((tab) => [tab.id, tab.pageIds])).toEqual([
['browser-docs', ['page-docs']]
])
expect(state.browserPagesByWorkspace['browser-docs']?.map((page) => page.url)).toEqual([
'https://example.com/docs'
])
expect([state.activeWorktreeId, state.activeTabId]).toEqual([WT, 'tab-shell'])
// The closed tab stays closed.
expect(JSON.stringify(state.unifiedTabsByWorktree)).not.toContain('tab-scratch')
expect(state.tabsByWorktree[WT]?.some((tab) => tab.id === 'tab-scratch')).toBe(false)
})
}
)
@@ -0,0 +1,64 @@
// Ledger L104's reason, as a switch-off window shows it: a stored partition an older writer left
// restores differently once a full save of main's model rewrites it, so main patches only the
// records a change touches (record-patch-versus-full-save.test.ts).
import { describe, expect, it, vi } from 'vitest'
import type * as AgentStatusModule from '@/lib/agent-status'
import { reconcileHydratedWorkspaceTabModels } from '../../app-shell/reconcile-hydrated-workspace-tab-models'
import { LEGACY_WORKSPACE_LAYOUT_PROFILES } from '../../../../shared/workspace-layout/workspace-layout-legacy-profile.test-fixture'
import { loadWorkspaceLayout } from '../../../../shared/workspace-layout/workspace-layout-load'
import { nameBasedLoadContext } from '../../../../shared/workspace-layout/workspace-layout-minted-ids'
import { saveWorkspaceLayout } from '../../../../shared/workspace-layout/workspace-layout-save'
import { GIT_KEY } from '../../../../shared/workspace-layout/workspace-layout-session.test-fixture'
import type { WorkspaceSessionState } from '../../../../shared/workspace-session-state-types'
import { createTestStore, makeWorktree } from './store-test-helpers'
import { createStoreSessionMockApi } from './store-session-test-harness'
vi.mock('sonner', () => ({ toast: { info: vi.fn(), success: vi.fn(), error: vi.fn() } }))
vi.mock('@/lib/agent-status', async (importOriginal) => {
const actual = await importOriginal<typeof AgentStatusModule>()
return { ...actual, detectAgentStatusFromTitle: vi.fn().mockReturnValue(null) }
})
createStoreSessionMockApi()
/** The terminal tabs a window restoring `session` shows, in tab-bar order. */
function restoredTabs(session: WorkspaceSessionState): string[][] {
const store = createTestStore()
store.setState({
repos: [
{ id: 'repo-1', path: '/Users/dev/orca', displayName: 'orca', badgeColor: '#000', addedAt: 0 }
],
worktreesByRepo: {
'repo-1': [makeWorktree({ id: GIT_KEY, repoId: 'repo-1', path: '/Users/dev/orca' })]
}
})
store.getState().hydrateWorkspaceSession(structuredClone(session), {})
store.getState().hydrateTabsSession(structuredClone(session), {})
reconcileHydratedWorkspaceTabModels(session, store.getState().reconcileWorktreeTabModels)
return (store.getState().groupsByWorktree[GIT_KEY] ?? []).map((group) => group.tabOrder)
}
function storedAndFullSave(name: string) {
const profile = LEGACY_WORKSPACE_LAYOUT_PROFILES.find((candidate) => candidate.name === name)!
const stored = profile.build()
const full = saveWorkspaceLayout(
loadWorkspaceLayout(profile.hostId, stored, nameBasedLoadContext())
)
return { stored: restoredTabs(stored), full: restoredTabs(full) }
}
describe('a switch-off window restoring a legacy partition, as stored and after a full save', () => {
it('loses a tab whose pane kept an old surface tombstone', () => {
expect(
storedAndFullSave('legacy surface tombstone on a pane still showing its terminal')
).toEqual({ stored: [['tab-a', 'tab-b']], full: [['tab-a']] })
})
it('gains a row the tab bar never listed', () => {
expect(storedAndFullSave('row without a tab-bar entry or a terminal')).toEqual({
stored: [['tab-a']],
full: [['tab-a', 'tab-b']]
})
})
})
@@ -0,0 +1,12 @@
// The session a layout-owner build's main writes for a representative set of layout commands
// (layout-owner-written-session-golden.test.ts), which a switch-off build must restore unchanged.
import { join } from 'node:path'
export const LAYOUT_OWNER_WRITTEN_SESSION_GOLDEN_PATH = join(
__dirname,
'layout-owner-written-session.json'
)
/** The fixture profile's registered git worktree (terminal-topology-profile-fixture.ts). */
export const LAYOUT_OWNER_WRITTEN_SESSION_WORKTREE_ID = 'repo-local::/fixture/local'
@@ -0,0 +1,218 @@
{
"activeRepoId": "repo-local",
"activeWorktreeId": "repo-local::/fixture/local",
"activeTabId": "tab-shell",
"tabsByWorktree": {
"repo-local::/fixture/local": [
{
"id": "tab-shell",
"ptyId": "pty-shell",
"title": "Terminal 1",
"defaultTitle": "Terminal 1",
"worktreeId": "repo-local::/fixture/local",
"customTitle": null,
"color": null,
"sortOrder": 0,
"createdAt": 1
},
{
"id": "tab-build",
"ptyId": null,
"title": "Terminal 2",
"defaultTitle": "Terminal 2",
"worktreeId": "repo-local::/fixture/local",
"customTitle": "Build",
"color": null,
"sortOrder": 1,
"createdAt": 2
}
]
},
"terminalLayoutsByTabId": {
"tab-shell": {
"root": {
"type": "split",
"direction": "vertical",
"first": {
"type": "leaf",
"leafId": "11111111-1111-4111-8111-111111111111"
},
"second": {
"type": "leaf",
"leafId": "33333333-1111-4111-8111-111111111111"
}
},
"activeLeafId": "11111111-1111-4111-8111-111111111111",
"expandedLeafId": null,
"ptyIdsByLeafId": {
"11111111-1111-4111-8111-111111111111": "pty-shell"
}
},
"tab-build": {
"root": {
"type": "leaf",
"leafId": "22222222-1111-4111-8111-111111111111"
},
"activeLeafId": "22222222-1111-4111-8111-111111111111",
"expandedLeafId": null,
"ptyIdsByLeafId": {}
}
},
"openFilesByWorktree": {
"repo-local::/fixture/local": [
{
"filePath": "/fixture/local/README.md",
"relativePath": "README.md",
"language": "markdown",
"worktreeId": "repo-local::/fixture/local"
}
]
},
"markdownFrontmatterVisible": {},
"browserTabsByWorktree": {
"repo-local::/fixture/local": [
{
"id": "browser-docs",
"sessionProfileId": null,
"pageIds": ["page-docs"],
"createdAt": 4,
"worktreeId": "repo-local::/fixture/local",
"url": "https://example.com/docs",
"title": "Docs",
"loading": false,
"faviconUrl": null,
"canGoBack": false,
"canGoForward": false,
"loadError": null
}
]
},
"browserPagesByWorkspace": {
"browser-docs": [
{
"id": "page-docs",
"workspaceId": "browser-docs",
"worktreeId": "repo-local::/fixture/local",
"url": "https://example.com/docs",
"title": "Docs",
"loading": false,
"faviconUrl": null,
"canGoBack": false,
"canGoForward": false,
"loadError": null,
"createdAt": 4
}
]
},
"activeBrowserTabIdByWorktree": {},
"activeFileIdByWorktree": {},
"activeTabTypeByWorktree": {},
"browserUrlHistory": [],
"defaultTerminalTabsAppliedByWorktreeId": {
"repo-local::/fixture/local": true
},
"unifiedTabs": {
"repo-local::/fixture/local": [
{
"id": "tab-shell",
"entityId": "tab-shell",
"groupId": "aa34db2e-c345-5d9d-a3e5-ddd4ac94492b",
"worktreeId": "repo-local::/fixture/local",
"executionHostId": "local",
"contentType": "terminal",
"label": "Terminal 1",
"customLabel": null,
"color": null,
"sortOrder": 0,
"createdAt": 1
},
{
"id": "tab-readme",
"entityId": "/fixture/local/README.md",
"groupId": "aa34db2e-c345-5d9d-a3e5-ddd4ac94492b",
"worktreeId": "repo-local::/fixture/local",
"executionHostId": "local",
"contentType": "editor",
"label": "",
"customLabel": null,
"color": null,
"sortOrder": 1,
"createdAt": 3
},
{
"id": "tab-docs",
"entityId": "browser-docs",
"groupId": "aa34db2e-c345-5d9d-a3e5-ddd4ac94492b",
"worktreeId": "repo-local::/fixture/local",
"executionHostId": "local",
"contentType": "browser",
"label": "",
"customLabel": null,
"color": null,
"sortOrder": 2,
"createdAt": 4
},
{
"id": "tab-build",
"entityId": "tab-build",
"groupId": "00000000-0000-4000-8000-000000000001",
"worktreeId": "repo-local::/fixture/local",
"executionHostId": "local",
"contentType": "terminal",
"label": "Terminal 2",
"customLabel": "Build",
"color": null,
"sortOrder": 0,
"createdAt": 2
}
]
},
"tabGroups": {
"repo-local::/fixture/local": [
{
"id": "aa34db2e-c345-5d9d-a3e5-ddd4ac94492b",
"worktreeId": "repo-local::/fixture/local",
"activeTabId": "tab-shell",
"tabOrder": ["tab-shell", "tab-readme", "tab-docs"]
},
{
"id": "00000000-0000-4000-8000-000000000001",
"worktreeId": "repo-local::/fixture/local",
"activeTabId": "tab-build",
"tabOrder": ["tab-build"]
}
]
},
"tabGroupLayouts": {
"repo-local::/fixture/local": {
"type": "split",
"direction": "horizontal",
"first": {
"type": "leaf",
"groupId": "aa34db2e-c345-5d9d-a3e5-ddd4ac94492b"
},
"second": {
"type": "leaf",
"groupId": "00000000-0000-4000-8000-000000000001"
},
"ratio": 0.5
}
},
"terminalTopologyRevisionByRepoId": {
"repo-local": 5
},
"closedTerminalTabTombstonesByTabId": {
"tab-scratch": {
"reason": "user",
"closedAt": 1700000000000,
"worktreeId": "repo-local::/fixture/local"
}
},
"terminalPtyIncarnationsByPaneKey": {
"tab-shell:11111111-1111-4111-8111-111111111111": "inc-shell"
},
"terminalSurfaceTombstonesByPaneKey": {},
"activeTabIdByWorktree": {
"repo-local::/fixture/local": "tab-shell"
}
}