Files
orca/src/shared/workspace-session-schema.test.ts
T
OrcaWinandOrcaWin 7d6603aa17 [comment audit] Complete repository-wide comment cleanup (#33249)
* [comment audit] Align legacy failure-read counts with cached display

* [comment audit] Condense shared provider lifecycle and browser recovery comments [skip ci]

* [comment audit] Condense shared generation, path and preview comments [skip ci]

* [comment audit] Condense host, file and Git contract rationale [skip ci]

* [comment audit] Tighten Git lifecycle and provider contract comments [skip ci]

* [comment audit] Clarify shared host authority and launch isolation [skip ci]

* [comment audit] simplify local access mobile and native chat contracts [skip ci]

* [comment audit] simplify native chat presentation and state contracts [skip ci]

* [comment audit] simplify native runtime and provider contracts [skip ci]

* [comment audit] simplify migration and orchestration contracts [skip ci]

* [comment audit] simplify plugin trust and process capture rationale [skip ci]

* [comment audit] simplify terminal delivery and remote compatibility rationale [skip ci]

* [comment audit] clarify remote recovery and launch wire rationale [skip ci]

* [comment audit] shorten shared RPC compatibility and runtime evidence comments [skip ci]

* [comment audit] simplify secure storage shell startup and sidebar rationale [skip ci]

* [comment audit] clarify sidebar, skill and SSH invariants [skip ci]

* [comment audit] condense structured chat delivery and timing rationale [skip ci]

* [comment audit] condense terminal replay and telemetry invariants [skip ci]

* [comment audit] clarify launch permission and terminal delivery rationale [skip ci]

* [comment audit] clarify workspace ownership and layout contracts [skip ci]

* [comment audit] wait for fitted and painted SSH terminal links

* [comment audit] clarify layout persistence and session ownership [skip ci]

* [comment audit] clarify host routing and workspace lifecycle contracts [skip ci]

* [comment audit] clarify startup persistence and app lifetime comments [skip ci]

* [comment audit] clarify recovery and activity boundaries [skip ci]

* [comment audit] condense activity and artifact invariants [skip ci]

* [comment audit] condense automation authority contracts [skip ci]

* [comment audit] simplify automation scheduling and history contracts [skip ci]

* [comment audit] clarify browser chrome and annotation lifecycles [skip ci]

* [comment audit] clarify browser routing and retained guest ownership [skip ci]

* [comment audit] clarify browser recovery and document ownership comments [skip ci]

* [comment audit] restore hosted runners required by native IME fixtures

* [comment audit] shorten preview, palette and dashboard invariants [skip ci]

* [comment audit] clarify terminal preview, dashboard and retention comments [skip ci]

* [comment audit] clarify diff-note recovery and editor lifecycle comments [skip ci]

* [comment audit] shorten rich-text and combined-diff ownership comments [skip ci]

* [comment audit] shorten autosave, draft recovery and model ownership comments [skip ci]

* [comment audit] shorten save-queue, notebook and Markdown constraints [skip ci]

* [comment audit] shorten Monaco, PDF and rich Markdown safeguards [skip ci]

* [comment audit] clarify rich Markdown source and selection safeguards [skip ci]

* [comment audit] clarify editor loading and emulator stream lifetimes [skip ci]

* [comment audit] trim emulator and feature tour narration [skip ci]

* [comment audit] clarify floating workspace and GitHub cache ownership [skip ci]

* [comment audit] trim provider controls and Linear filter explanations [skip ci]

* [comment audit] clarify pairing and chat resume lifetimes [skip ci]

* [comment audit] clarify chat delivery and transcript lifetimes [skip ci]

* [comment audit] clarify session options and host-owned status [skip ci]

* [comment audit] clarify draft durability and live-status precedence [skip ci]

* [comment audit] Trim chat send and model ownership explanations [skip ci]

* [comment audit] Shorten transcript ownership and send recovery comments [skip ci]

* [comment audit] Simplify native chat composer and transcript hook comments [skip ci]

* [comment audit] Shorten native chat scroll and structured session explanations [skip ci]

* [comment audit] Trim workspace picker and onboarding commentary [skip ci]

* [comment audit] Simplify pet rendering, review cache and quick open comments [skip ci]

* [comment audit] Trim sidebar and session history explanations [skip ci]

* [comment audit] Condense session resume and review checks comments [skip ci]

* [comment audit] Shorten checks state and file explorer refresh comments [skip ci]

* [comment audit] Trim plugin bridge and source-control action rationale [skip ci]

* [comment audit] Trim source-control listing and review rationale [skip ci]

* [comment audit] Shorten sidebar lifecycle and explorer safeguards [skip ci]

* [comment audit] Condense Settings account and appearance rationale [skip ci]

* [comment audit] Trim pairing and repository Settings explanations [skip ci]

* [comment audit] Condense Settings search SSH and terminal safeguards [skip ci]

* [comment audit] Shorten Settings voice and provider ownership comments [skip ci]

* [comment audit] Condense Settings draft persistence and host-state rationale [skip ci]

* [comment audit] Trim Settings hooks and sidebar setup rationale [skip ci]

* [comment audit] Condense sidebar deletion and board-filter safeguards [skip ci]

* [comment audit] Condense worktree cards, host ownership and Markdown contracts [skip ci]

* [comment audit] Condense sidebar drag, folder and filtering contracts [skip ci]

* [comment audit] Condense sidebar parity, sleep and host ownership contracts [skip ci]

* [comment audit] Prune focus-read baseline after comment cleanup

* [comment audit] Clarify sidebar visibility, board drops and agent evidence [skip ci]

* [comment audit] Tighten sidebar listing, drag and reveal contracts [skip ci]

* [comment audit] Clarify viewport geometry and skill update safeguards [skip ci]

* [comment audit] Tighten skill inventory, usage and host status comments [skip ci]

* [comment audit] Clarify usage ownership and resource kill safeguards [skip ci]

* [comment audit] Tighten tab selection, rename and search contracts [skip ci]

* [comment audit] Tighten tab-strip geometry and drag ownership comments [skip ci]

* [comment audit] Tighten Tasks recovery, pagination and mutation comments [skip ci]

* [comment audit] Tighten provider loading and terminal recovery comments [skip ci]

* [comment audit] Tighten completion, cadence and PTY ownership comments [skip ci]

* [comment audit] Clarify terminal restore and parked-pane boundaries [skip ci]

* [comment audit] Tighten PTY startup and replay explanations [skip ci]

* [comment audit] Clarify PTY identity and delivery ownership [skip ci]

* [comment audit] Shorten remote reconnect and input replay rationale [skip ci]

* [comment audit] Condense terminal parking clipboard and file link comments [skip ci]

* [comment audit] Clarify IME ownership and trim trace history [skip ci]

* [comment audit] Shorten pane recovery and parked watcher explanations [skip ci]

* [comment audit] Condense snapshot replay and visibility recovery rationale [skip ci]

* [comment audit] Shorten terminal parking and keyboard ownership rationale [skip ci]

* [comment audit] Trim renderer lifecycle and menu rationale [skip ci]

* [comment audit] Condense workspace cleanup ownership and settlement notes [skip ci]

* [comment audit] Clarify renderer hook evidence and source ownership [skip ci]

* [comment audit] Tighten IPC ownership and reconnect explanations [skip ci]

* [comment audit] Trim renderer focus, paste and attention comments [skip ci]

* [comment audit] Shorten scroll, locale and agent lifecycle comments [skip ci]

* [comment audit] Trim agent launch, resume and ownership commentary [skip ci]

* [comment audit] Shorten account, browser and editor comments [skip ci]

* [comment audit] Clarify host-mirror waits, previews and input ownership [skip ci]

* [comment audit] Condense launch recovery and editor tokenization comments [skip ci]

* [comment audit] Condense workspace selection and terminal pane rationale [skip ci]

* [comment audit] Condense terminal fitting and rendering recovery comments [skip ci]

* [comment audit] Condense terminal input and host routing explanations [skip ci]

* [comment audit] Condense ownership and sleeping-session recovery rationale [skip ci]

* [comment audit] Condense structured launch and terminal link explanations [skip ci]

* [comment audit] Condense terminal ownership and workspace file routing rationale [skip ci]

* [comment audit] Condense session persistence and workspace activation rationale [skip ci]

* [comment audit] Condense workspace routing and browser ownership rationale [skip ci]

* [comment audit] Condense mirror recovery and layout authority rationale [skip ci]

* [comment audit] Condense runtime transport and file safety rationale [skip ci]

* [comment audit] Condense runtime graph and structured session rationale [skip ci]

* [comment audit] Condense browser creation and remote tab recovery rationale [skip ci]

* [comment audit] Condense snapshot authority and publisher recovery rationale [skip ci]

* [comment audit] Condense frame, startup and provider cache rationale [skip ci]

* [comment audit] Condense host twin and agent lifecycle rationale [skip ci]

* [comment audit] Condense browser authority and editor cleanup rationale [skip ci]

* [comment audit] Condense editor buffer identity and review cache rationale [skip ci]

* [comment audit] Condense provider lifetime and repository ownership rationale [skip ci]

* [comment audit] Condense reconnect, hydration and terminal retirement rationale [skip ci]

* [comment audit] Condense recovery, reconciliation and persistence rationale [skip ci]

* [comment audit] Condense cleanup ownership and listing admission rationale [skip ci]

* [comment audit] Condense teardown ordering and terminal ownership rationale [skip ci]

* [comment audit] Condense recovery ledger and web host-boundary rationale [skip ci]

* [comment audit] Condense web reconnect and credential lifecycle rationale [skip ci]

* [comment audit] Condense cloud rollout and assignment safety rationale [skip ci]

* [comment audit] Condense relay session and database recovery rationale [skip ci]

* [comment audit] Condense relay placement and protocol compatibility rationale [skip ci]

* [comment audit] Condense build and packaging constraints [skip ci]

* [comment audit] Condense CI gates and validation isolation rationale [skip ci]

* [comment audit] Condense catalog identity and browser validation rationale [skip ci]

* [comment audit] Condense mobile browser and frame-budget rationale [skip ci]

* [comment audit] Shorten mobile route and browser test explanations [skip ci]

* [comment audit] Condense editor and session seam test rationale [skip ci]

* [comment audit] Trim session and terminal closure history [skip ci]

* [comment audit] Shorten terminal browser and census explanations [skip ci]

* [comment audit] Preserve root-only E2E diagnostic baseline

* [comment audit] Condense mobile seam and HTML preview test rationale [skip ci]

* [comment audit] Condense preview instrumentation and render harness rationale [skip ci]

* [comment audit] Condense bundle contracts and native-build constraints [skip ci]

* [comment audit] Shorten native packaging and CI routing rationale [skip ci]

* [comment audit] Condense release tooling and benchmark constraints [skip ci]

* [comment audit] Condense packaging and platform gate rationale [skip ci]

* [comment audit] Shorten mobile route and history rationale [skip ci]

* [comment audit] Condense mobile browser and component rationale [skip ci]

* [comment audit] Shorten drawer and workspace-form rationale [skip ci]

* [comment audit] Trim mobile review and editor-document explanations [skip ci]

* [comment audit] Condense editor lifecycle and Markdown explanations [skip ci]

* [comment audit] Trim mobile file, diagnostics and dictation commentary [skip ci]

* [comment audit] Simplify mobile host-list and workspace-view explanations [skip ci]

* [comment audit] Clarify mobile shell lifetime and bridge delivery constraints [skip ci]

* [comment audit] Condense bridge stream, native request and audio rationale [skip ci]

* [comment audit] Reduce bridge schema and page-client commentary [skip ci]

* [comment audit] Tighten mobile page bridge contracts [skip ci]

* [comment audit] Clarify mobile cache and session constraints [skip ci]

* [comment audit] Condense mobile route and lifecycle rationale [skip ci]

* [comment audit] Tighten mobile shell hook timing rationale [skip ci]

* [comment audit] Clarify route handoff and notification compatibility [skip ci]

* [comment audit] Clarify push registration and dictation cleanup constraints [skip ci]

* [comment audit] Condense keyboard geometry and native media ownership rationale [skip ci]

* [comment audit] Tighten mobile chat scroll and composer concurrency rationale [skip ci]

* [comment audit] Condense mobile reply compatibility and upload ownership rationale [skip ci]

* [comment audit] Condense optimistic echo and scroll ownership rationale [skip ci]

* [comment audit] Tighten stream replay and queued session ownership rationale [skip ci]

* [comment audit] Condense mobile composer and reply-schema constraints [skip ci]

* [comment audit] Clarify mobile transcript recovery and session ownership [skip ci]

* [comment audit] Clarify mobile queue and source-control compatibility [skip ci]

* [comment audit] Condense storage and task launch constraints [skip ci]

* [comment audit] Clarify task schemas and terminal document ownership [skip ci]

* [comment audit] Tighten terminal input and viewport rationale [skip ci]

* [comment audit] Shorten terminal and RPC recorder contracts [skip ci]

* [comment audit] Condense recorder engine and connection contracts [skip ci]

* [comment audit] Shorten host credential and relay recovery comments [skip ci]

* [comment audit] Tighten pairing recovery and bundle transport rationale [skip ci]

* [comment audit] Clarify RPC delivery and workspace catalog boundaries [skip ci]

* [comment audit] Condense catalog and relay entry contracts [skip ci]

* [comment audit] Shorten shutdown and SSH relay delivery contracts [skip ci]

* [comment audit] Condense WSL relay and E2E regression rationale [skip ci]

* [comment audit] Shorten cross-version harness and renderer fixture explanations [skip ci]

* [comment audit] Condense paired-host and E2E lifecycle rationale [skip ci]

* [comment audit] Shorten layout oracle and regression explanations [skip ci]

* [comment audit] Condense paired recovery and ownership regression contracts [skip ci]

* [comment audit] Shorten restart, sidebar and SSH lifecycle contracts [skip ci]

* [comment audit] Condense terminal rendering and IME regression rationale [skip ci]

* [comment audit] Shorten terminal recovery and benchmark contracts [skip ci]

* [comment audit] Condense tooling and Windows lifecycle proof rationale [skip ci]

* [comment audit] Condense workflow routing and release rationale [skip ci]

* [comment audit] Condense mobile security and installer contracts [skip ci]

* [comment audit] Shorten native rationale and stylesheet workarounds [skip ci]

* [comment audit] Shorten Windows visibility probe rationale [skip ci]

* [comment audit] Condense embedded release and signing rationale [skip ci]

* [comment audit] Retain current main test behavior in final reconciliation [skip ci]

* [comment audit] Complete final comment review across current source inventory

* [comment audit] Handle consolidated audit reports before final CI

* [comment audit] Preserve credential storage census detail [skip ci]

* [comment audit] Scale broad changed E2E selections with verified timing shards

* [comment audit] Align E2E assertions with browser close and settled session metadata

* [comment audit] Match existing E2Es to current resource and windowing contracts

* [comment audit] Observe the shared server outage notice in reconnect coverage

* [comment audit] Make existing renderer probes measure settled tab and PTY work

* [comment audit] Align palette E2E selector with task search

* [comment audit] Refresh remaining palette search selectors

* [comment audit] Keep selector correction within selected audit specs

* [comment audit] Await emoji fixture settings inside renderer evaluation

* [comment audit] Cancel fixture fit retries between terminal tests

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-25 04:05:46 -07:00

796 lines
22 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import { parseWorkspaceSession } from './workspace-session-schema'
import { MAX_BROWSER_HISTORY_ENTRIES } from './workspace-session-browser-history'
describe('parseWorkspaceSession', () => {
it('accepts a minimal valid session', () => {
const result = parseWorkspaceSession({
activeRepoId: null,
activeWorktreeId: null,
activeTabId: null,
tabsByWorktree: {},
terminalLayoutsByTabId: {}
})
expect(result.ok).toBe(true)
})
it('preserves external SSH file ownership across session parsing', () => {
const result = parseWorkspaceSession({
activeRepoId: null,
activeWorktreeId: 'wt',
activeTabId: null,
tabsByWorktree: {},
terminalLayoutsByTabId: {},
openFilesByWorktree: {
wt: [
{
filePath: '/tmp/external.png',
relativePath: '/tmp/external.png',
worktreeId: 'wt',
language: 'png',
externalSshTargetId: 'ssh-1'
}
]
}
})
expect(result.ok).toBe(true)
if (result.ok) {
expect(result.value.openFilesByWorktree?.wt?.[0]?.externalSshTargetId).toBe('ssh-1')
}
})
it('drops an open file with blank external SSH ownership, keeping the session', () => {
const result = parseWorkspaceSession({
activeRepoId: null,
activeWorktreeId: 'wt',
activeTabId: null,
tabsByWorktree: {},
terminalLayoutsByTabId: {},
openFilesByWorktree: {
wt: [
{
filePath: '/tmp/external.png',
relativePath: '/tmp/external.png',
worktreeId: 'wt',
language: 'png',
externalSshTargetId: ' '
}
]
}
})
expect(result.ok).toBe(true)
if (result.ok) {
expect(result.value.openFilesByWorktree?.wt).toEqual([])
}
})
it('accepts a fully populated session with optional fields', () => {
const result = parseWorkspaceSession({
activeRepoId: 'repo1',
activeWorktreeId: 'repo1::/path/wt1',
activeTabId: 'tab1',
tabsByWorktree: {
'repo1::/path/wt1': [
{
id: 'tab1',
ptyId: 'daemon-session-abc',
worktreeId: 'repo1::/path/wt1',
title: 'bash',
customTitle: null,
color: null,
sortOrder: 0,
createdAt: 1_700_000_000_000
}
]
},
terminalLayoutsByTabId: {
tab1: {
root: {
type: 'split',
direction: 'vertical',
first: { type: 'leaf', leafId: 'pane:1' },
second: { type: 'leaf', leafId: 'pane:2' }
},
activeLeafId: 'pane:1',
expandedLeafId: null,
ptyIdsByLeafId: { 'pane:1': 'daemon-session-A' }
}
},
activeWorktreeIdsOnShutdown: ['repo1::/path/wt1']
})
expect(result.ok).toBe(true)
})
it('preserves an isolated browser tab session partition across hydration', () => {
// Preserve isolated storage despite a stale renderer profile mirror after restart (#6923).
const result = parseWorkspaceSession({
activeRepoId: null,
activeWorktreeId: null,
activeTabId: null,
tabsByWorktree: {},
terminalLayoutsByTabId: {},
browserTabsByWorktree: {
wt: [
{
id: 'browser-1',
worktreeId: 'wt',
sessionProfileId: 'iso-profile',
sessionPartition: 'persist:orca-browser-session-iso-profile',
url: 'https://example.com',
title: 'Example',
loading: false,
faviconUrl: null,
canGoBack: false,
canGoForward: false,
loadError: null,
createdAt: 1
}
]
}
})
expect(result.ok).toBe(true)
if (!result.ok) {
return
}
expect(result.value.browserTabsByWorktree?.wt?.[0]?.sessionPartition).toBe(
'persist:orca-browser-session-iso-profile'
)
})
it('preserves a valid launchAgent on a terminal tab', () => {
const result = parseWorkspaceSession({
activeRepoId: null,
activeWorktreeId: null,
activeTabId: null,
tabsByWorktree: {
wt: [
{
id: 'tab1',
ptyId: null,
worktreeId: 'wt',
title: 'codex',
customTitle: null,
color: null,
sortOrder: 0,
createdAt: 1,
launchAgent: 'codex'
}
]
},
terminalLayoutsByTabId: {}
})
expect(result.ok).toBe(true)
if (result.ok) {
expect(result.value.tabsByWorktree.wt[0].launchAgent).toBe('codex')
}
})
it('drops a launch pane an older build saved: after a restart its pane is an ordinary pane', () => {
const result = parseWorkspaceSession({
activeRepoId: null,
activeWorktreeId: null,
activeTabId: null,
tabsByWorktree: {
wt: [
{
id: 'tab1',
ptyId: null,
worktreeId: 'wt',
title: 'claude',
customTitle: null,
color: null,
sortOrder: 0,
createdAt: 1,
launchAgent: 'claude',
agentLaunchPane: { leafId: 'leaf-1', outcome: { kind: 'not-started', code: 'boom' } }
},
{
id: 'tab2',
ptyId: null,
worktreeId: 'wt',
title: 'claude',
customTitle: null,
color: null,
sortOrder: 1,
createdAt: 1,
agentLaunchPane: { leafId: 'leaf-2', outcome: { kind: 'exploded' } }
}
]
},
terminalLayoutsByTabId: {}
})
expect(result.ok).toBe(true)
if (result.ok) {
expect(result.value.tabsByWorktree.wt[0]).not.toHaveProperty('agentLaunchPane')
expect(result.value.tabsByWorktree.wt[1]).not.toHaveProperty('agentLaunchPane')
}
})
it('drops an unknown launchAgent without failing the whole session', () => {
const result = parseWorkspaceSession({
activeRepoId: null,
activeWorktreeId: null,
activeTabId: null,
tabsByWorktree: {
wt: [
{
id: 'tab1',
ptyId: null,
worktreeId: 'wt',
title: 'bash',
customTitle: null,
color: null,
sortOrder: 0,
createdAt: 1,
launchAgent: 'some-retired-agent'
}
]
},
terminalLayoutsByTabId: {}
})
expect(result.ok).toBe(true)
if (result.ok) {
expect(result.value.tabsByWorktree.wt[0].launchAgent).toBeUndefined()
}
})
it('drops a tab where ptyId is a number (schema drift) without failing the session', () => {
const result = parseWorkspaceSession({
activeRepoId: null,
activeWorktreeId: null,
activeTabId: null,
tabsByWorktree: {
wt: [
{
id: 'tab1',
ptyId: 42,
worktreeId: 'wt',
title: 'bash',
customTitle: null,
color: null,
sortOrder: 0,
createdAt: 0
}
]
},
terminalLayoutsByTabId: {}
})
expect(result.ok).toBe(true)
if (result.ok) {
expect(result.value.tabsByWorktree.wt).toEqual([])
}
})
it('preserves generated terminal title fields for persistence hydration', () => {
const result = parseWorkspaceSession({
activeRepoId: null,
activeWorktreeId: 'wt',
activeTabId: 'tab1',
tabsByWorktree: {
wt: [
{
id: 'tab1',
ptyId: null,
worktreeId: 'wt',
title: 'Claude working',
defaultTitle: 'Terminal 1',
generatedTitle: 'Refactor auth',
aiVaultTitle: {
agent: 'codex',
sessionId: 'session-1',
title: 'Provider thread name'
},
customTitle: null,
color: null,
sortOrder: 0,
createdAt: 0
}
]
},
terminalLayoutsByTabId: {},
unifiedTabs: {
wt: [
{
id: 'tab1',
entityId: 'tab1',
groupId: 'group1',
worktreeId: 'wt',
executionHostId: 'runtime:host-b',
contentType: 'terminal',
label: 'Claude working',
generatedLabel: 'Refactor auth',
aiVaultTitle: {
agent: 'codex',
sessionId: 'session-1',
title: 'Provider thread name'
},
customLabel: null,
color: null,
sortOrder: 0,
createdAt: 0
}
]
}
})
expect(result.ok).toBe(true)
if (result.ok) {
expect(result.value.tabsByWorktree.wt[0].generatedTitle).toBe('Refactor auth')
expect(result.value.tabsByWorktree.wt[0].aiVaultTitle?.title).toBe('Provider thread name')
expect(result.value.unifiedTabs?.wt[0].generatedLabel).toBe('Refactor auth')
expect(result.value.unifiedTabs?.wt[0].aiVaultTitle?.title).toBe('Provider thread name')
expect(result.value.unifiedTabs?.wt[0].executionHostId).toBe('runtime:host-b')
}
})
it('drops malformed AI Vault titles without rejecting the workspace session', () => {
const result = parseWorkspaceSession({
activeRepoId: null,
activeWorktreeId: 'wt',
activeTabId: 'tab1',
tabsByWorktree: {
wt: [
{
id: 'tab1',
ptyId: null,
worktreeId: 'wt',
title: 'Codex',
aiVaultTitle: { agent: 'future-agent', sessionId: 'session-1', title: 'Name' },
customTitle: null,
color: null,
sortOrder: 0,
createdAt: 0
}
]
},
terminalLayoutsByTabId: {},
unifiedTabs: {
wt: [
{
id: 'tab1',
entityId: 'tab1',
groupId: 'group1',
worktreeId: 'wt',
contentType: 'terminal',
label: 'Codex',
aiVaultTitle: 'malformed',
customLabel: null,
color: null,
sortOrder: 0,
createdAt: 0
}
]
}
})
expect(result.ok).toBe(true)
if (result.ok) {
expect(result.value.tabsByWorktree.wt[0].aiVaultTitle).toBeUndefined()
expect(result.value.unifiedTabs?.wt[0].aiVaultTitle).toBeUndefined()
}
})
it('preserves quick command label fields while accepting older omitted fields', () => {
const result = parseWorkspaceSession({
activeRepoId: null,
activeWorktreeId: 'wt',
activeTabId: 'tab1',
tabsByWorktree: {
wt: [
{
id: 'tab1',
ptyId: null,
worktreeId: 'wt',
title: 'pnpm test',
defaultTitle: 'Terminal 1',
quickCommandLabel: 'Run tests',
customTitle: null,
color: null,
sortOrder: 0,
createdAt: 0
},
{
id: 'tab2',
ptyId: null,
worktreeId: 'wt',
title: 'Terminal 2',
customTitle: null,
color: null,
sortOrder: 1,
createdAt: 1
}
]
},
terminalLayoutsByTabId: {},
unifiedTabs: {
wt: [
{
id: 'tab1',
entityId: 'tab1',
groupId: 'group1',
worktreeId: 'wt',
contentType: 'terminal',
label: 'pnpm test',
quickCommandLabel: 'Run tests',
customLabel: null,
color: null,
sortOrder: 0,
createdAt: 0
}
]
}
})
expect(result.ok).toBe(true)
if (result.ok) {
expect(result.value.tabsByWorktree.wt[0].quickCommandLabel).toBe('Run tests')
expect(result.value.tabsByWorktree.wt[1].quickCommandLabel).toBeUndefined()
expect(result.value.unifiedTabs?.wt[0].quickCommandLabel).toBe('Run tests')
}
})
it('rejects a session with missing required top-level fields', () => {
const result = parseWorkspaceSession({
activeRepoId: null
})
expect(result.ok).toBe(false)
})
it('rejects a truncated JSON object', () => {
const result = parseWorkspaceSession({})
expect(result.ok).toBe(false)
})
it('rejects non-object input (e.g. corrupted file contents)', () => {
expect(parseWorkspaceSession(null).ok).toBe(false)
expect(parseWorkspaceSession('garbage').ok).toBe(false)
expect(parseWorkspaceSession(42).ok).toBe(false)
})
it('drops one truncated tab without discarding other persisted worktrees', () => {
const validTab = {
id: 'tab-good',
ptyId: null,
worktreeId: 'worktree-good',
title: 'Terminal',
customTitle: null,
color: null,
sortOrder: 0,
createdAt: 1_700_000_000_000
}
const result = parseWorkspaceSession({
activeRepoId: null,
activeWorktreeId: 'worktree-good',
activeTabId: 'tab-good',
tabsByWorktree: {
'worktree-good': [validTab],
'worktree-corrupt': [
{
id: 'tab-truncated',
ptyId: null,
worktreeId: 'worktree-corrupt',
title: 'Terminal',
sortOrder: 0,
generation: 3,
startupCwd: '/workspace'
}
]
},
terminalLayoutsByTabId: {}
})
expect(result.ok).toBe(true)
if (result.ok) {
expect(result.value.tabsByWorktree).toEqual({
'worktree-good': [validTab],
'worktree-corrupt': []
})
}
})
it('drops bad lastVisitedAtByWorktreeId entries rather than failing the session', () => {
const result = parseWorkspaceSession({
activeRepoId: null,
activeWorktreeId: null,
activeTabId: null,
tabsByWorktree: {},
terminalLayoutsByTabId: {},
lastVisitedAtByWorktreeId: {
good: 1_700_000_000_000,
nan: Number.NaN,
infinite: Number.POSITIVE_INFINITY,
negative: -5,
string: 'nope'
}
})
expect(result.ok).toBe(true)
if (result.ok) {
expect(result.value.lastVisitedAtByWorktreeId).toEqual({ good: 1_700_000_000_000 })
}
})
it('accepts default-tab idempotency markers', () => {
const result = parseWorkspaceSession({
activeRepoId: null,
activeWorktreeId: null,
activeTabId: null,
tabsByWorktree: {},
terminalLayoutsByTabId: {},
defaultTerminalTabsAppliedByWorktreeId: {
'repo1::/path/wt1': true
}
})
expect(result.ok).toBe(true)
if (result.ok) {
expect(result.value.defaultTerminalTabsAppliedByWorktreeId).toEqual({
'repo1::/path/wt1': true
})
}
})
it('caps oversized browser history while parsing legacy workspace sessions', () => {
const result = parseWorkspaceSession({
activeRepoId: null,
activeWorktreeId: null,
activeTabId: null,
tabsByWorktree: {},
terminalLayoutsByTabId: {},
browserUrlHistory: Array.from({ length: 500 }, (_, index) => ({
url: `https://example.com/${index}`,
normalizedUrl: `https://example.com/${index}`,
title: `Example ${index}`,
faviconUrl: index === 0 ? 'https://example.com/favicon.ico' : null,
lastVisitedAt: 1_700_000_000_000 - index,
visitCount: 1
}))
})
expect(result.ok).toBe(true)
if (result.ok) {
expect(result.value.browserUrlHistory).toHaveLength(MAX_BROWSER_HISTORY_ENTRIES)
expect(result.value.browserUrlHistory?.[0]?.faviconUrl).toBe(
'https://example.com/favicon.ico'
)
expect(result.value.browserUrlHistory?.at(-1)?.url).toBe('https://example.com/199')
}
})
it('preserves a known viewMode on a unified tab', () => {
const result = parseWorkspaceSession({
activeRepoId: null,
activeWorktreeId: 'wt',
activeTabId: null,
tabsByWorktree: {},
terminalLayoutsByTabId: {},
unifiedTabs: {
wt: [
{
id: 'tab1',
entityId: 'tab1',
groupId: 'group1',
worktreeId: 'wt',
contentType: 'terminal',
label: 'Claude',
customLabel: null,
color: null,
sortOrder: 0,
createdAt: 0,
viewMode: 'chat'
}
]
}
})
expect(result.ok).toBe(true)
if (result.ok) {
expect(result.value.unifiedTabs?.wt[0].viewMode).toBe('chat')
}
})
it('preserves a structured agent session tab and drops the retired adoption key', () => {
const result = parseWorkspaceSession({
activeRepoId: null,
activeWorktreeId: 'wt',
activeTabId: 'session-1',
tabsByWorktree: {},
terminalLayoutsByTabId: {},
unifiedTabs: {
wt: [
{
id: 'session-1',
entityId: 'session-1',
groupId: 'group1',
worktreeId: 'wt',
contentType: 'agent-session',
agentSessionAgent: 'codex',
label: 'Codex Chat',
customLabel: null,
color: null,
sortOrder: 0,
createdAt: 0
},
{
id: 'terminal-1',
entityId: 'terminal-1',
groupId: 'group1',
worktreeId: 'wt',
contentType: 'terminal',
viewMode: 'chat',
// Why: older builds could save this key on a chat-mode terminal; it must keep loading.
structuredSessionId: 'codex-session-1',
label: 'Terminal 1',
customLabel: null,
color: null,
sortOrder: 1,
createdAt: 0
}
]
},
activeTabTypeByWorktree: { wt: 'agent-session' }
})
expect(result.ok).toBe(true)
if (result.ok) {
expect(result.value.unifiedTabs?.wt[0]).toMatchObject({
contentType: 'agent-session',
agentSessionAgent: 'codex'
})
expect(result.value.unifiedTabs?.wt[1]).toMatchObject({
contentType: 'terminal',
viewMode: 'chat'
})
expect(result.value.unifiedTabs?.wt[1]).not.toHaveProperty('structuredSessionId')
expect(result.value.activeTabTypeByWorktree?.wt).toBe('agent-session')
}
})
it('keeps a chat tab of any agent id, and drops only a malformed id', () => {
const chatTab = (id: string, agentSessionAgent: unknown) => ({
id,
entityId: id,
groupId: 'group1',
worktreeId: 'wt',
contentType: 'agent-session',
agentSessionAgent,
label: 'Chat',
customLabel: null,
color: null,
sortOrder: 0,
createdAt: 0
})
const result = parseWorkspaceSession({
activeRepoId: null,
activeWorktreeId: 'wt',
activeTabId: null,
tabsByWorktree: {},
terminalLayoutsByTabId: {},
unifiedTabs: {
wt: [
chatTab('a', 'grok'),
chatTab('b', 'not an agent!'),
chatTab('c', 42),
chatTab('d', 'claude')
]
}
})
expect(result.ok).toBe(true)
if (result.ok) {
expect(result.value.unifiedTabs?.wt.map((tab) => [tab.id, tab.agentSessionAgent])).toEqual([
['a', 'grok'],
['b', undefined],
['c', undefined],
['d', 'claude']
])
}
})
it('degrades an unknown viewMode to the safe default instead of failing parse', () => {
const result = parseWorkspaceSession({
activeRepoId: null,
activeWorktreeId: 'wt',
activeTabId: null,
tabsByWorktree: {},
terminalLayoutsByTabId: {},
unifiedTabs: {
wt: [
{
id: 'tab1',
entityId: 'tab1',
groupId: 'group1',
worktreeId: 'wt',
contentType: 'terminal',
label: 'Claude',
customLabel: null,
color: null,
sortOrder: 0,
createdAt: 0,
// A newer build could persist a mode this version doesn't know.
viewMode: 'split-future-mode'
}
]
}
})
// The whole-session parse must still succeed; the unknown mode degrades.
expect(result.ok).toBe(true)
if (result.ok) {
expect(result.value.unifiedTabs?.wt[0].viewMode).toBe('terminal')
}
})
// Reclaiming client-hosted pages after relaunch requires both persisted identity fields; z.object drops unlisted fields.
it('preserves the remote page identity of a client-hosted browser page', () => {
const result = parseWorkspaceSession({
activeRepoId: null,
activeWorktreeId: 'wt',
activeTabId: null,
tabsByWorktree: {},
terminalLayoutsByTabId: {},
browserPagesByWorkspace: {
'workspace-1': [
{
id: 'page-1',
workspaceId: 'workspace-1',
worktreeId: 'wt',
url: 'https://example.com/',
title: 'Example',
loading: false,
faviconUrl: null,
canGoBack: false,
canGoForward: false,
loadError: null,
createdAt: 1,
browserRuntimeEnvironmentId: 'env-1',
remoteBrowserPageId: 'remote-page-1',
remoteBrowserPageClientHosted: true
}
]
}
})
expect(result.ok).toBe(true)
if (result.ok) {
expect(result.value.browserPagesByWorkspace?.['workspace-1']?.[0]).toMatchObject({
remoteBrowserPageId: 'remote-page-1',
remoteBrowserPageClientHosted: true
})
}
})
it('accepts a browser page persisted before the remote page identity existed', () => {
const result = parseWorkspaceSession({
activeRepoId: null,
activeWorktreeId: 'wt',
activeTabId: null,
tabsByWorktree: {},
terminalLayoutsByTabId: {},
browserPagesByWorkspace: {
'workspace-1': [
{
id: 'page-1',
workspaceId: 'workspace-1',
worktreeId: 'wt',
url: 'https://example.com/',
title: 'Example',
loading: false,
faviconUrl: null,
canGoBack: false,
canGoForward: false,
loadError: null,
createdAt: 1
}
]
}
})
expect(result.ok).toBe(true)
if (result.ok) {
const page = result.value.browserPagesByWorkspace?.['workspace-1']?.[0]
expect(page?.id).toBe('page-1')
expect(page?.remoteBrowserPageId).toBeUndefined()
expect(page?.remoteBrowserPageClientHosted).toBeUndefined()
}
})
})