test(e2e): journeys for a reopened client and two clients on one host

Two gaps this suite had no coverage for, both driven end to end against a real
paired desktop client rather than at a seam.

A relaunched client holding a live remote terminal: every paired restart spec
here restarts around a browser pane, none around the terminal the user is
actually mid-work in. The host-side fixture's on-disk sink is the oracle — one
READY for the whole run proves the host never re-spawned the session, and a
recorded line for input sent after the relaunch proves the restored pane is
wired to that same process rather than painted with its scrollback.

Two clients on one host across an emptied workspace: the tombstone is
client-local on the runtime path, so a client that never held a row still seeds
into a workspace another client deliberately emptied. That asymmetry is by
design; a client falling out of step with the host and staying there is not.
Phase 0 is the control — without it a later divergence cannot be attributed to
the emptying rather than to mirroring never having worked.

The input probe goes through `pane.terminal.input`, not `window.api.pty.write`:
a mirrored pane's handle is a `remote:` id that no local PTY answers to, so a
direct write is swallowed and the assertion passes on nothing. The pre-restart
control exists to catch exactly that, and did.
This commit is contained in:
Neil
2026-09-11 01:19:27 -07:00
parent 56324233ba
commit 7358bc31f5
2 changed files with 725 additions and 0 deletions
@@ -0,0 +1,370 @@
/**
* JOURNEY: quit the desktop app while remote terminals are live on the host, then reopen it.
*
* TOPOLOGY: the `orcaPage` app is the host (orca server); a separate real Orca desktop client
* pairs to it, opens a host terminal, works in it, is force-quit, and relaunched on the same
* profile — the pairing credential and the persisted session survive, as they do for a real
* force-quit reopen.
*
* Why this exists: every paired restart spec in this suite restarts around a *browser* pane
* (paired-client-hosted-browser-*.spec.ts). None of them restarts a client holding a live remote
* *terminal*, which is the thing the user is actually mid-work in.
*
* The terminal is a fixture that appends one line per event to a file on disk. That sink is the
* oracle nothing on the client can fake:
* - exactly one `READY` for the whole run means the host never re-spawned the process, so the
* user came back to their session rather than a fresh shell wearing its name;
* - a `LINE:` for input sent after the relaunch means the restored pane is wired to that same
* process, not merely painted with its scrollback.
*
* Run:
* pnpm exec playwright test \
* tests/e2e/paired-remote-terminal-client-restart-survival.spec.ts \
* --config tests/playwright.config.ts --project electron-headless --workers=1
*/
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { randomUUID } from 'node:crypto'
import os from 'node:os'
import path from 'node:path'
import type { Page } from '@stablyai/playwright-test'
import {
HOST_TERMINAL_SURFACE_SEPARATOR,
toWebTerminalSurfaceTabId
} from '../../src/shared/terminal-surface-id'
import { closeElectronAppForE2E } from './helpers/electron-process-shutdown'
import { expect, test } from './helpers/orca-app'
import {
createRuntimeDesktopPairingOffer,
launchPairedElectronClient,
type PairedElectronClient
} from './helpers/paired-electron-client'
import { waitForActiveWorktree, waitForSessionReady } from './helpers/store'
/** What a user would accept for "my terminal is back" after reopening the app. */
const RESTORE_BUDGET_MS = 60_000
const scratch = mkdtempSync(path.join(os.tmpdir(), 'orca-client-restart-survival-'))
const fixturePath = path.join(scratch, 'restart-survival-terminal.mjs')
writeFileSync(
fixturePath,
[
"import { appendFileSync } from 'node:fs'",
'const sink = process.argv[2]',
'const record = (line) => appendFileSync(sink, `${line}\\n`)',
"record('READY')",
"process.stdout.write('RESTART_SURVIVAL_READY\\r\\n')",
"process.stdin.setEncoding('utf8')",
"let pending = ''",
"process.stdin.on('data', (data) => {",
' pending += data',
' const lines = pending.split(/\\r\\n|\\r|\\n/)',
" pending = lines.pop() ?? ''",
' for (const line of lines) {',
' record(`LINE:${line}`)',
' process.stdout.write(`LINE:${line}\\r\\n`)',
' }',
'})',
'process.stdin.resume()'
].join('\n')
)
test.afterAll(() => {
rmSync(scratch, { recursive: true, force: true })
})
function shellQuote(value: string): string {
return `'${value.replaceAll("'", `'\\''`)}'`
}
function fixtureCommand(sinkPath: string): string {
const command = [process.execPath, fixturePath, sinkPath]
return process.platform === 'win32'
? command.map((value) => `"${value.replaceAll('"', '""')}"`).join(' ')
: command.map(shellQuote).join(' ')
}
function readSinkLines(sinkPath: string): string[] {
try {
return readFileSync(sinkPath, 'utf8').split('\n').filter(Boolean)
} catch {
return []
}
}
async function callEnvironment<TResult>(
page: Page,
environmentId: string,
method: string,
params: unknown
): Promise<TResult> {
return page.evaluate(
async ({ environmentId, method, params }) => {
const response = await window.api.runtimeEnvironments.call({
selector: environmentId,
method,
params
})
if (!response.ok) {
throw new Error(`${response.error.code}: ${response.error.message}`)
}
return response.result
},
{ environmentId, method, params }
) as Promise<TResult>
}
async function focusWorkspace(page: Page, worktreeId: string): Promise<void> {
await page.evaluate((id) => {
const state = window.__store?.getState()
state?.setActiveView('terminal')
state?.setActiveWorktree(id)
}, worktreeId)
}
async function waitForClientWorkspace(page: Page, worktreeId: string): Promise<void> {
await expect
.poll(
() =>
page.evaluate(
(id) => (window.__store?.getState().allWorktrees() ?? []).some((w) => w.id === id),
worktreeId
),
{ timeout: 60_000, message: 'paired client never received the host workspace' }
)
.toBe(true)
}
/** Milliseconds until the tab is mirrored again, or null if it never was. */
async function waitForMirroredTab(
page: Page,
worktreeId: string,
webTabId: string,
budgetMs: number
): Promise<number | null> {
const startedAt = Date.now()
while (Date.now() - startedAt < budgetMs) {
const present = await page.evaluate(
({ id, worktreeId }) =>
(window.__store?.getState().tabsByWorktree[worktreeId] ?? []).some((tab) => tab.id === id),
{ id: webTabId, worktreeId }
)
if (present) {
return Date.now() - startedAt
}
await page.waitForTimeout(500)
}
return null
}
/** Milliseconds until the restored pane paints `marker`, or null if it never did. */
async function waitForPanePaint(
page: Page,
webTabId: string,
marker: string,
budgetMs: number
): Promise<number | null> {
const startedAt = Date.now()
while (Date.now() - startedAt < budgetMs) {
const content = await page.evaluate((id) => {
const manager = window.__paneManagers?.get(id)
const pane = manager?.getActivePane?.() ?? manager?.getPanes?.()[0] ?? null
return pane?.serializeAddon?.serialize?.() ?? ''
}, webTabId)
if (content.includes(marker)) {
return Date.now() - startedAt
}
await page.waitForTimeout(500)
}
return null
}
async function selectClientTab(page: Page, worktreeId: string, webTabId: string): Promise<void> {
await page.evaluate(
({ webTabId, worktreeId }) => {
const state = window.__store?.getState()
state?.setActiveView('terminal')
state?.setActiveWorktree(worktreeId)
state?.setActiveTab(webTabId)
state?.setActiveTabType('terminal')
},
{ webTabId, worktreeId }
)
}
/**
* Types `marker` into the pane until the host-side process records it, or the budget ends.
*
* Why through `pane.terminal.input` and not `window.api.pty.write`: a mirrored pane's handle is
* a `remote:` id that no local PTY answers to, so a direct write is silently swallowed. This is
* the path a keystroke actually takes, and it is retried because a pane still reattaching can
* replay-suppress a write (helpers/restored-terminal-input-readiness.ts polls for that reason).
*/
async function driveInputUntilProcessSees(
client: PairedElectronClient,
webTabId: string,
sinkPath: string,
marker: string,
budgetMs: number
): Promise<boolean> {
const startedAt = Date.now()
while (Date.now() - startedAt < budgetMs) {
await client.page.evaluate(
({ id, text }) => {
const manager = window.__paneManagers?.get(id)
const pane = manager?.getActivePane?.() ?? manager?.getPanes?.()[0] ?? null
pane?.terminal?.input?.(text, true)
},
{ id: webTabId, text: `${marker}\r` }
)
if (readSinkLines(sinkPath).some((line) => line.includes(marker))) {
return true
}
await client.page.waitForTimeout(1_000)
}
return false
}
async function readTabPtyIds(client: PairedElectronClient, webTabId: string): Promise<string[]> {
return client.page.evaluate((id) => window.__store?.getState().ptyIdsByTabId[id] ?? [], webTabId)
}
test('a relaunched client gets its live remote terminal back, still attached to the same process', async ({
orcaPage
}, testInfo) => {
test.setTimeout(900_000)
await waitForSessionReady(orcaPage)
await waitForActiveWorktree(orcaPage)
const worktreeId = await orcaPage.evaluate(() => {
const id = window.__store?.getState().activeWorktreeId
if (!id) {
throw new Error('host has no active worktree')
}
return id
})
const sinkPath = path.join(scratch, `sink-${randomUUID()}.log`)
const failures: string[] = []
let client: PairedElectronClient | null = null
const offer = await createRuntimeDesktopPairingOffer(orcaPage)
try {
client = await launchPairedElectronClient(offer, testInfo, 'remote-terminal-restart-survival')
const userDataDir = client.userDataDir
await waitForClientWorkspace(client.page, worktreeId)
await focusWorkspace(client.page, worktreeId)
const created = await callEnvironment<{ tab: { id: string; terminal: string | null } }>(
client.page,
client.environmentId,
'session.tabs.createTerminal',
{
worktree: `id:${worktreeId}`,
command: fixtureCommand(sinkPath),
activate: true,
select: true,
navigation: 'caller'
}
)
const hostTabId = created.tab.id.split(HOST_TERMINAL_SURFACE_SEPARATOR)[0]!
const webTabId = toWebTerminalSurfaceTabId(hostTabId)
expect(
await waitForMirroredTab(client.page, worktreeId, webTabId, RESTORE_BUDGET_MS),
'the client never mirrored the terminal it created'
).not.toBeNull()
await selectClientTab(client.page, worktreeId, webTabId)
await expect
.poll(() => readSinkLines(sinkPath), {
timeout: RESTORE_BUDGET_MS,
message: 'the host terminal fixture never started'
})
.toContain('READY')
expect(
await waitForPanePaint(client.page, webTabId, 'RESTART_SURVIVAL_READY', RESTORE_BUDGET_MS),
'the pane never painted the live terminal before the restart'
).not.toBeNull()
// The control. Without it, "input did not arrive after the restart" cannot be told apart
// from "this input path never worked in this topology".
const ptyIdsBefore = await readTabPtyIds(client, webTabId)
expect(ptyIdsBefore, 'the live pane had no PTY handle before the restart').not.toHaveLength(0)
expect(
await driveInputUntilProcessSees(
client,
webTabId,
sinkPath,
'PRE_RESTART_CONTROL',
RESTORE_BUDGET_MS
),
'input did not reach the host process even before the restart — the probe, not the product'
).toBe(true)
// ── The restart: force-quit and reopen on the same profile. ──
// Quit without disposing: the profile has to outlive the app, as it does for a real Cmd+Q.
const quitting = client.app
client = null
await closeElectronAppForE2E(quitting)
client = await launchPairedElectronClient(
offer,
testInfo,
'remote-terminal-restart-survival-relaunch',
{ reuseUserDataDir: userDataDir }
)
await waitForClientWorkspace(client.page, worktreeId)
await focusWorkspace(client.page, worktreeId)
const tabBackMs = await waitForMirroredTab(client.page, worktreeId, webTabId, RESTORE_BUDGET_MS)
console.error(`[client-restart] tabBackMs=${tabBackMs}`)
if (tabBackMs === null) {
failures.push('the remote terminal tab never came back after the app was reopened')
} else {
await selectClientTab(client.page, worktreeId, webTabId)
const paintedMs = await waitForPanePaint(
client.page,
webTabId,
'RESTART_SURVIVAL_READY',
RESTORE_BUDGET_MS
)
console.error(`[client-restart] paintedMs=${paintedMs}`)
if (paintedMs === null) {
failures.push(
'the remote terminal came back empty — the tab is there but the transcript is not'
)
}
}
// Is the restored pane actually wired to the live process, or only painted with its past?
const marker = `POST_RESTART_${randomUUID().slice(0, 8)}`
const ptyIds = await readTabPtyIds(client, webTabId)
console.error(`[client-restart] ptyBefore=${ptyIdsBefore[0]} ptyAfter=${ptyIds[0] ?? 'none'}`)
if (ptyIds.length === 0) {
failures.push(
'the restored tab has no PTY handle — nothing the user types can reach the host'
)
} else {
const echoed = await driveInputUntilProcessSees(
client,
webTabId,
sinkPath,
marker,
RESTORE_BUDGET_MS
)
console.error(`[client-restart] inputReachedProcess=${echoed}`)
if (!echoed) {
failures.push(
'input typed into the restored terminal never reached the process the host is running'
)
}
}
// The sink is the fork oracle: a second READY means the host re-spawned the user's work.
const readyCount = readSinkLines(sinkPath).filter((line) => line === 'READY').length
console.error(`[client-restart] readyCount=${readyCount}`)
if (readyCount !== 1) {
failures.push(
`the host process was re-spawned across the client restart (READY x${readyCount}) — the user's session was replaced, not restored`
)
}
} finally {
await client?.dispose()
}
expect(failures, failures.join('\n')).toEqual([])
})
@@ -0,0 +1,355 @@
/**
* JOURNEY: two desktop clients paired to one Orca server, working in the same workspace.
*
* TOPOLOGY: the `orcaPage` app is the host (orca server). Two separate real Orca desktop
* clients pair to it, exactly as two of the user's machines would. Nothing is faulted — this
* is the ordinary shape of using Orca from a laptop and a desktop at the same time.
*
* The emptied-workspace tombstone is an explicit `tabsByWorktree[worktreeId] = []` row and it
* is client-local on the runtime path: it never crosses the wire, so the second client cannot
* know the first emptied the workspace on purpose and still seeds into it. That asymmetry is
* by design. What is NOT by design is a client falling out of step with the host and staying
* there, which is what this spec measures.
*
* Phase 0 is the control: with both clients attached, does a terminal created on one reach the
* other at all? Without it a later divergence cannot be attributed to the emptying.
*
* Run:
* pnpm exec playwright test \
* tests/e2e/paired-two-client-emptied-workspace-reseed.spec.ts \
* --config tests/playwright.config.ts --project electron-headless --workers=1
*/
import type { Page } from '@stablyai/playwright-test'
import { expect, test } from './helpers/orca-app'
import {
createRuntimeDesktopPairingOffer,
launchPairedElectronClient,
type PairedElectronClient
} from './helpers/paired-electron-client'
import { waitForActiveWorktree, waitForSessionReady } from './helpers/store'
/** How long a client may lag the host before the user would call it broken. */
const MIRROR_BUDGET_MS = 30_000
/** A retraction may be slow; what matters is whether it arrives at all. */
const RETRACTION_BUDGET_MS = 90_000
type HostTabRow = { id: string; parentTabId?: string; terminal?: string | null }
async function callEnvironment<TResult>(
page: Page,
environmentId: string,
method: string,
params: unknown
): Promise<TResult> {
return page.evaluate(
async ({ environmentId, method, params }) => {
const response = await window.api.runtimeEnvironments.call({
selector: environmentId,
method,
params
})
if (!response.ok) {
throw new Error(`${response.error.code}: ${response.error.message}`)
}
return response.result
},
{ environmentId, method, params }
) as Promise<TResult>
}
/** The host's own tab inventory — the only oracle that is not a client re-derivation. */
async function readHostTerminalTabIds(
client: PairedElectronClient,
worktreeId: string
): Promise<string[]> {
const inventory = await callEnvironment<{ tabs: HostTabRow[] }>(
client.page,
client.environmentId,
'session.tabs.list',
{ worktree: `id:${worktreeId}` }
)
return [
...new Set(
inventory.tabs
.filter((tab) => tab.terminal !== undefined && tab.terminal !== null)
.map((tab) => tab.parentTabId ?? tab.id)
)
].sort()
}
async function readMirroredTabCount(page: Page, worktreeId: string): Promise<number> {
return page.evaluate(
(id) => (window.__store?.getState().tabsByWorktree[id] ?? []).length,
worktreeId
)
}
/** Whether the client holds an explicit empty row (the tombstone) versus no row at all. */
async function readWorkspaceRowState(
page: Page,
worktreeId: string
): Promise<'missing' | 'tombstoned' | 'populated'> {
return page.evaluate((id) => {
const tabs = window.__store?.getState().tabsByWorktree
if (!tabs || !Object.hasOwn(tabs, id)) {
return 'missing' as const
}
return (tabs[id] ?? []).length === 0 ? ('tombstoned' as const) : ('populated' as const)
}, worktreeId)
}
async function focusWorkspace(page: Page, worktreeId: string): Promise<void> {
await page.evaluate((id) => {
const state = window.__store?.getState()
state?.setActiveView('terminal')
state?.setActiveWorktree(id)
}, worktreeId)
}
/** Milliseconds until the client's mirrored count matches the host's, or null if it never did. */
async function waitForClientToMatchHost(
client: PairedElectronClient,
hostCount: number,
worktreeId: string,
budgetMs: number
): Promise<number | null> {
const startedAt = Date.now()
while (Date.now() - startedAt < budgetMs) {
if ((await readMirroredTabCount(client.page, worktreeId)) === hostCount) {
return Date.now() - startedAt
}
await client.page.waitForTimeout(500)
}
return null
}
async function waitForClientWorkspace(page: Page, worktreeId: string): Promise<void> {
await expect
.poll(
() =>
page.evaluate(
(id) => (window.__store?.getState().allWorktrees() ?? []).some((w) => w.id === id),
worktreeId
),
{ timeout: 60_000, message: 'paired client never received the host workspace' }
)
.toBe(true)
}
test('two paired clients stay in step with the host across an emptied workspace', async ({
orcaPage
}, testInfo) => {
test.setTimeout(600_000)
await waitForSessionReady(orcaPage)
await waitForActiveWorktree(orcaPage)
const worktreeId = await orcaPage.evaluate(() => {
const id = window.__store?.getState().activeWorktreeId
if (!id) {
throw new Error('host has no active worktree')
}
return id
})
let clientA: PairedElectronClient | null = null
let clientB: PairedElectronClient | null = null
const failures: string[] = []
try {
clientA = await launchPairedElectronClient(
await createRuntimeDesktopPairingOffer(orcaPage),
testInfo,
'emptied-workspace-client-a'
)
clientB = await launchPairedElectronClient(
await createRuntimeDesktopPairingOffer(orcaPage),
testInfo,
'emptied-workspace-client-b'
)
for (const client of [clientA, clientB]) {
await waitForClientWorkspace(client.page, worktreeId)
await focusWorkspace(client.page, worktreeId)
}
// ── Phase 0: the control. A creates a terminal; B must see it. ──
await callEnvironment(clientA.page, clientA.environmentId, 'session.tabs.createTerminal', {
worktree: `id:${worktreeId}`,
activate: true,
select: true,
navigation: 'caller'
})
const afterCreate = (await readHostTerminalTabIds(clientA, worktreeId)).length
const controlA = await waitForClientToMatchHost(
clientA,
afterCreate,
worktreeId,
MIRROR_BUDGET_MS
)
const controlB = await waitForClientToMatchHost(
clientB,
afterCreate,
worktreeId,
MIRROR_BUDGET_MS
)
console.error(`[two-client] phase0 host=${afterCreate} A=${controlA}ms B=${controlB}ms`)
if (controlA === null || controlB === null) {
failures.push(
`phase0: a terminal created on one client never reached the other (host=${afterCreate}, A=${controlA}, B=${controlB})`
)
}
// ── Phase 1: A empties the workspace by hand. ──
for (const hostTabId of await readHostTerminalTabIds(clientA, worktreeId)) {
await callEnvironment(clientA.page, clientA.environmentId, 'session.tabs.close', {
worktree: `id:${worktreeId}`,
tabId: hostTabId,
reason: 'user',
navigation: 'caller'
})
}
await expect
.poll(() => readHostTerminalTabIds(clientA, worktreeId).then((ids) => ids.length), {
timeout: MIRROR_BUDGET_MS,
message: 'host still held terminals after client A closed them all'
})
.toBe(0)
// Deliberately generous: the question is whether the retraction ever arrives, not whether
// it is prompt. A client still showing a terminal the host has destroyed is a dead pane the
// user will click.
const emptyA = await waitForClientToMatchHost(clientA, 0, worktreeId, RETRACTION_BUDGET_MS)
const emptyB = await waitForClientToMatchHost(clientB, 0, worktreeId, RETRACTION_BUDGET_MS)
const hostOwnView = await readMirroredTabCount(orcaPage, worktreeId)
console.error(
`[two-client] phase1 host=0 hostOwnView=${hostOwnView}` +
` A=${emptyA}ms(${await readWorkspaceRowState(clientA.page, worktreeId)})` +
` B=${emptyB}ms(${await readWorkspaceRowState(clientB.page, worktreeId)})`
)
if (emptyA === null || emptyB === null) {
failures.push(
`phase1: a client kept showing terminals the host no longer has (A=${emptyA}, B=${emptyB})`
)
}
// Neither client may seed a replacement into a workspace the user deliberately emptied:
// both hold a row for it, so both know it was emptied rather than never initialized.
await orcaPage.waitForTimeout(10_000)
const hostAfterSettle = (await readHostTerminalTabIds(clientA, worktreeId)).length
console.error(`[two-client] phase1-settled host=${hostAfterSettle}`)
if (hostAfterSettle !== 0) {
failures.push(
`phase1: the emptied workspace grew ${hostAfterSettle} terminal(s) back on its own`
)
}
// ── Phase 2: B creates a terminal again. Both clients must follow the host. ──
await callEnvironment(clientB.page, clientB.environmentId, 'session.tabs.createTerminal', {
worktree: `id:${worktreeId}`,
activate: true,
select: true,
navigation: 'caller'
})
const hostAfterB = (await readHostTerminalTabIds(clientB, worktreeId)).length
const rejoinB = await waitForClientToMatchHost(
clientB,
hostAfterB,
worktreeId,
MIRROR_BUDGET_MS
)
const rejoinA = await waitForClientToMatchHost(
clientA,
hostAfterB,
worktreeId,
MIRROR_BUDGET_MS
)
console.error(`[two-client] phase2 host=${hostAfterB} A=${rejoinA}ms B=${rejoinB}ms`)
if (rejoinA === null || rejoinB === null) {
failures.push(
`phase2: a client never adopted the terminal the host holds — the user sees an empty` +
` workspace while work runs on it (host=${hostAfterB}, A=${rejoinA}, B=${rejoinB})`
)
}
} finally {
await clientB?.dispose()
await clientA?.dispose()
}
expect(failures, failures.join('\n')).toEqual([])
})
/**
* The same workspace, driven by a client that starts working the moment it finishes pairing —
* which is what a user does on a machine they have just added.
*
* Isolated from the two-client test above because the failure it hunts is a startup race, not a
* multi-client one: the earlier form of that test drove the close seconds after the pairing
* completed and repeatedly left the client's mirror stuck — sometimes still showing the terminal
* the host had closed, sometimes stuck empty afterwards — with the link demonstrably alive.
*/
test('a client that works immediately after pairing stays in step with the host', async ({
orcaPage
}, testInfo) => {
test.setTimeout(600_000)
await waitForSessionReady(orcaPage)
await waitForActiveWorktree(orcaPage)
const worktreeId = await orcaPage.evaluate(() => {
const id = window.__store?.getState().activeWorktreeId
if (!id) {
throw new Error('host has no active worktree')
}
return id
})
let client: PairedElectronClient | null = null
const failures: string[] = []
try {
client = await launchPairedElectronClient(
await createRuntimeDesktopPairingOffer(orcaPage),
testInfo,
'fresh-pairing-immediate-work'
)
await waitForClientWorkspace(client.page, worktreeId)
await focusWorkspace(client.page, worktreeId)
await callEnvironment(client.page, client.environmentId, 'session.tabs.createTerminal', {
worktree: `id:${worktreeId}`,
activate: true,
select: true,
navigation: 'caller'
})
const afterCreate = (await readHostTerminalTabIds(client, worktreeId)).length
const sawCreate = await waitForClientToMatchHost(
client,
afterCreate,
worktreeId,
MIRROR_BUDGET_MS
)
console.error(`[fresh-pairing] create host=${afterCreate} client=${sawCreate}ms`)
if (sawCreate === null) {
failures.push(
`the client never mirrored the terminal it had just created (host=${afterCreate})`
)
}
for (const hostTabId of await readHostTerminalTabIds(client, worktreeId)) {
await callEnvironment(client.page, client.environmentId, 'session.tabs.close', {
worktree: `id:${worktreeId}`,
tabId: hostTabId,
reason: 'user',
navigation: 'caller'
})
}
await expect
.poll(() => readHostTerminalTabIds(client!, worktreeId).then((ids) => ids.length), {
timeout: MIRROR_BUDGET_MS,
message: 'host still held terminals after the client closed them all'
})
.toBe(0)
const sawClose = await waitForClientToMatchHost(client, 0, worktreeId, MIRROR_BUDGET_MS)
console.error(
`[fresh-pairing] close client=${sawClose}ms row=${await readWorkspaceRowState(client.page, worktreeId)}`
)
if (sawClose === null) {
failures.push('the client kept showing a terminal the host had already closed')
}
} finally {
await client?.dispose()
}
expect(failures, failures.join('\n')).toEqual([])
})