mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
fix(remote-terminal): a headless host keeps a parked terminal at its viewer's size (#29349)
* fix(remote-terminal): a headless host keeps a parked terminal at its viewer's grid Parking a paired pane closes its stream, the PTY's last viewer. The floor then reclaimed the PTY to the host's geometry, which on a windowless orca serve/orcad host is the 120x40 spawn grid nobody sees. Every reveal then fetched a snapshot at 120x40 and reflowed it to the pane, and a TUI redrew twice. With no host pane to give the grid back to, the floor now keeps the last owner's grid; the parked owner's reserved seat resumes it without a resize. * test(remote-terminal): a phone or another viewer still takes a parked terminal's grid on a headless host * fix(remote-terminal): a phone leaving a parked headless terminal restores the parked grid * fix(remote-terminal): keep the parked desktop grid as the restore baseline even while a phone fits * test(remote-terminal): tie each PTY size probe to its own reply
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { OrcaRuntimeService } from './orca-runtime'
|
||||
import { publishHeadlessRuntimeGraph } from './headless-runtime-graph'
|
||||
|
||||
// A parked paired pane closes its stream, the PTY's last viewer. On a windowless host the PTY must
|
||||
// keep that viewer's grid, while other viewers and phones can still take it (#15997).
|
||||
|
||||
const PTY = 'pty-1'
|
||||
const SPAWN_GRID = { cols: 120, rows: 40 }
|
||||
|
||||
function createRuntime({ headless }: { headless: boolean }) {
|
||||
const store = {
|
||||
getRepos: () => [],
|
||||
getAllWorktreeMeta: () => ({}),
|
||||
getWorktreeMeta: () => undefined,
|
||||
getSettings: () => ({ workspaceDir: '/tmp/workspaces', mobileAutoRestoreFitMs: 5_000 })
|
||||
}
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the stub covers the settings and repo reads the viewer floor reaches; the rest of Store is unreached.
|
||||
const runtime = new OrcaRuntimeService(store as never)
|
||||
const sizes = new Map([[PTY, { ...SPAWN_GRID }]])
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: resize and getSize are the only controller members the layout queue drives here.
|
||||
runtime.setPtyController({
|
||||
write: () => true,
|
||||
kill: () => true,
|
||||
getForegroundProcess: async () => null,
|
||||
resize: (ptyId: string, cols: number, rows: number) => {
|
||||
sizes.set(ptyId, { cols, rows })
|
||||
return true
|
||||
},
|
||||
getSize: (ptyId: string) => sizes.get(ptyId) ?? null
|
||||
} as never)
|
||||
if (headless) {
|
||||
publishHeadlessRuntimeGraph(runtime)
|
||||
} else {
|
||||
runtime.attachWindow(1)
|
||||
}
|
||||
return { runtime, size: () => sizes.get(PTY) }
|
||||
}
|
||||
|
||||
describe('remote desktop viewers on a parked terminal', () => {
|
||||
beforeEach(() => vi.useFakeTimers())
|
||||
afterEach(() => vi.useRealTimers())
|
||||
|
||||
it('keeps the parked owner grid on a windowless host', async () => {
|
||||
const { runtime, size } = createRuntime({ headless: true })
|
||||
await runtime.updateRemoteDesktopViewer(PTY, 'multiplex:a:1', 'laptop', 99, 67)
|
||||
|
||||
await runtime.unregisterRemoteDesktopViewer(PTY, 'multiplex:a:1')
|
||||
|
||||
expect(size()).toEqual({ cols: 99, rows: 67 })
|
||||
})
|
||||
|
||||
it('lets a phone attaching after the park take the grid', async () => {
|
||||
const { runtime, size } = createRuntime({ headless: true })
|
||||
await runtime.updateRemoteDesktopViewer(PTY, 'multiplex:a:1', 'laptop', 99, 67)
|
||||
await runtime.unregisterRemoteDesktopViewer(PTY, 'multiplex:a:1')
|
||||
|
||||
await runtime.handleMobileSubscribe(PTY, 'phone', { cols: 45, rows: 20 })
|
||||
|
||||
expect(runtime.getDriver(PTY)).toEqual({ kind: 'mobile', clientId: 'phone' })
|
||||
expect(size()).toEqual({ cols: 45, rows: 20 })
|
||||
})
|
||||
|
||||
it('returns to the parked grid, not the spawn grid, after that phone leaves', async () => {
|
||||
const { runtime, size } = createRuntime({ headless: true })
|
||||
await runtime.updateRemoteDesktopViewer(PTY, 'multiplex:a:1', 'laptop', 99, 67)
|
||||
await runtime.unregisterRemoteDesktopViewer(PTY, 'multiplex:a:1')
|
||||
await runtime.handleMobileSubscribe(PTY, 'phone', { cols: 45, rows: 20 })
|
||||
|
||||
runtime.handleMobileUnsubscribe(PTY, 'phone')
|
||||
await vi.advanceTimersByTimeAsync(10_000)
|
||||
|
||||
expect(runtime.getDriver(PTY).kind).not.toBe('mobile')
|
||||
expect(size()).toEqual({ cols: 99, rows: 67 })
|
||||
})
|
||||
|
||||
it('restores the parked desktop grid when the park happened while a phone was fitting', async () => {
|
||||
const { runtime, size } = createRuntime({ headless: true })
|
||||
await runtime.updateRemoteDesktopViewer(PTY, 'multiplex:a:1', 'laptop', 99, 67)
|
||||
await runtime.handleMobileSubscribe(PTY, 'phone', { cols: 45, rows: 20 })
|
||||
await runtime.unregisterRemoteDesktopViewer(PTY, 'multiplex:a:1')
|
||||
|
||||
runtime.handleMobileUnsubscribe(PTY, 'phone')
|
||||
await vi.advanceTimersByTimeAsync(10_000)
|
||||
|
||||
expect(runtime.getDriver(PTY).kind).not.toBe('mobile')
|
||||
expect(size()).toEqual({ cols: 99, rows: 67 })
|
||||
})
|
||||
|
||||
it('lets a different desktop viewer that claims after the park take the grid', async () => {
|
||||
const { runtime, size } = createRuntime({ headless: true })
|
||||
await runtime.updateRemoteDesktopViewer(PTY, 'multiplex:a:1', 'laptop', 99, 67)
|
||||
await runtime.unregisterRemoteDesktopViewer(PTY, 'multiplex:a:1')
|
||||
|
||||
await runtime.updateRemoteDesktopViewer(PTY, 'multiplex:b:1', 'mini', 80, 30, false)
|
||||
expect(size()).toEqual({ cols: 99, rows: 67 })
|
||||
await runtime.claimRemoteDesktopViewer(PTY, 'multiplex:b:1')
|
||||
|
||||
expect(size()).toEqual({ cols: 80, rows: 30 })
|
||||
})
|
||||
|
||||
it('still gives a host with a window its grid back', async () => {
|
||||
const { runtime, size } = createRuntime({ headless: false })
|
||||
await runtime.updateRemoteDesktopViewer(PTY, 'multiplex:a:1', 'laptop', 99, 67)
|
||||
|
||||
await runtime.unregisterRemoteDesktopViewer(PTY, 'multiplex:a:1')
|
||||
|
||||
expect(size()).toEqual(SPAWN_GRID)
|
||||
})
|
||||
})
|
||||
@@ -5,13 +5,14 @@ const PTY = 'pty-1'
|
||||
// Spawn-size host geometry the floor reclaims to when no remote viewer owns the PTY.
|
||||
const HOST = { cols: 120, rows: 40 }
|
||||
|
||||
function createFloor() {
|
||||
function createFloor({ hostShowsTerminals = true }: { hostShowsTerminals?: boolean } = {}) {
|
||||
let size = { ...HOST }
|
||||
const layouts: { kind: string; cols: number; rows: number }[] = []
|
||||
const floor = new RemoteDesktopTerminalFloor({
|
||||
isMobileDriven: () => false,
|
||||
getTerminalSize: () => size,
|
||||
resolveHostTarget: () => HOST,
|
||||
hostShowsTerminals: () => hostShowsTerminals,
|
||||
applyLayout: async (_ptyId, target) => {
|
||||
layouts.push({ kind: target.kind, cols: target.cols, rows: target.rows })
|
||||
size = { cols: target.cols, rows: target.rows }
|
||||
@@ -99,3 +100,50 @@ describe('remote desktop floor across a client reconnect', () => {
|
||||
expect(size()).toEqual({ cols: 132, rows: 42 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('remote desktop floor on a windowless host', () => {
|
||||
it('keeps the last owner grid when that owner leaves', async () => {
|
||||
const { floor, layouts, size } = createFloor({ hostShowsTerminals: false })
|
||||
await floor.updateViewer(PTY, 'multiplex:old:1', 'pane-A', 159, 69)
|
||||
layouts.splice(0)
|
||||
|
||||
await floor.unregisterViewers(PTY, ['multiplex:old:1'])
|
||||
|
||||
expect(layouts).toEqual([])
|
||||
expect(size()).toEqual({ cols: 159, rows: 69 })
|
||||
expect(floor.isResizeDriven(PTY)).toBe(false)
|
||||
})
|
||||
|
||||
it('resumes the parked owner at its grid without a resize', async () => {
|
||||
const { floor, layouts, size } = createFloor({ hostShowsTerminals: false })
|
||||
await floor.updateViewer(PTY, 'multiplex:old:1', 'pane-A', 159, 69)
|
||||
await floor.unregisterViewers(PTY, ['multiplex:old:1'])
|
||||
layouts.splice(0)
|
||||
|
||||
await floor.updateViewer(PTY, 'multiplex:new:1', 'pane-A', 159, 69, false)
|
||||
|
||||
expect(floor.isViewerOwner(PTY, 'multiplex:new:1')).toBe(true)
|
||||
expect(size()).toEqual({ cols: 159, rows: 69 })
|
||||
expect(layouts.every((layout) => layout.cols === 159 && layout.rows === 69)).toBe(true)
|
||||
})
|
||||
|
||||
it('still hands the seat to an active peer when the owner leaves', async () => {
|
||||
const { floor, size } = createFloor({ hostShowsTerminals: false })
|
||||
await floor.updateViewer(PTY, 'multiplex:peer:1', 'pane-B', 100, 30)
|
||||
await floor.updateViewer(PTY, 'multiplex:old:1', 'pane-A', 159, 69)
|
||||
|
||||
await floor.unregisterViewers(PTY, ['multiplex:old:1'])
|
||||
|
||||
expect(floor.isViewerOwner(PTY, 'multiplex:peer:1')).toBe(true)
|
||||
expect(size()).toEqual({ cols: 100, rows: 30 })
|
||||
})
|
||||
|
||||
it('still gives a host with a window its grid back', async () => {
|
||||
const { floor, size } = createFloor()
|
||||
await floor.updateViewer(PTY, 'multiplex:old:1', 'pane-A', 159, 69)
|
||||
|
||||
await floor.unregisterViewers(PTY, ['multiplex:old:1'])
|
||||
|
||||
expect(size()).toEqual(HOST)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -10,6 +10,8 @@ export type RemoteDesktopTerminalFloorDependencies = {
|
||||
isMobileDriven: (ptyId: string) => boolean
|
||||
getTerminalSize: (ptyId: string) => Viewport | null
|
||||
resolveHostTarget: (ptyId: string) => Viewport
|
||||
/** False on a headless host (orca serve, orcad), which has no pane to give a PTY's grid back to. */
|
||||
hostShowsTerminals: () => boolean
|
||||
applyLayout: (ptyId: string, target: LayoutTarget) => Promise<{ ok: boolean }>
|
||||
}
|
||||
|
||||
@@ -202,9 +204,12 @@ export class RemoteDesktopTerminalFloor {
|
||||
}
|
||||
let changed = false
|
||||
let removedOwnerClientId: string | null = null
|
||||
let removedOwnerViewport: Viewport | null = null
|
||||
for (const subscriptionKey of subscriptionKeys) {
|
||||
if (this.owners.get(ptyId) === subscriptionKey) {
|
||||
removedOwnerClientId = viewers.get(subscriptionKey)?.clientId ?? null
|
||||
const owner = viewers.get(subscriptionKey)
|
||||
removedOwnerClientId = owner?.clientId ?? null
|
||||
removedOwnerViewport = owner ? { cols: owner.cols, rows: owner.rows } : null
|
||||
}
|
||||
changed = viewers.delete(subscriptionKey) || changed
|
||||
}
|
||||
@@ -236,7 +241,20 @@ export class RemoteDesktopTerminalFloor {
|
||||
}
|
||||
}
|
||||
this.bumpRevision(ptyId)
|
||||
return removedOwnerClientId !== null ? this.applyLayout(ptyId) : Promise.resolve(true)
|
||||
if (removedOwnerClientId === null) {
|
||||
return Promise.resolve(true)
|
||||
}
|
||||
if (!this.owners.has(ptyId) && !this.dependencies.hostShowsTerminals()) {
|
||||
// Why: reclaiming would restore the spawn grid nobody sees, so the next reveal replays a
|
||||
// snapshot at a foreign grid and TUIs redraw twice (#15997). The orphan seat resumes it.
|
||||
if (removedOwnerViewport) {
|
||||
// Why the viewer's grid, not the PTY's: a phone may be fitting the PTY, and its later
|
||||
// restore must return to the parked desktop grid rather than the spawn or phone grid.
|
||||
this.hostReclaimTargets.set(ptyId, removedOwnerViewport)
|
||||
}
|
||||
return Promise.resolve(true)
|
||||
}
|
||||
return this.applyLayout(ptyId)
|
||||
}
|
||||
|
||||
refreshViewer(
|
||||
|
||||
@@ -30,6 +30,7 @@ import { getRuntimeBrowserPageRegistry } from '../../runtime-browser-page-regist
|
||||
|
||||
import { RuntimeBrowserDriverController } from '../../runtime-browser-driver-controller'
|
||||
import { RemoteDesktopTerminalFloor } from '../../remote-desktop-terminal-floor'
|
||||
import { HEADLESS_RUNTIME_WINDOW_ID } from '../../../../shared/runtime-types'
|
||||
import type { StatsCollector } from '../../../stats/collector'
|
||||
import { RuntimeRemoteFetchController } from '../../runtime-remote-fetch-controller'
|
||||
import { RuntimeWorktreeBaseReconciliation } from '../../runtime-worktree-base-reconciliation'
|
||||
@@ -206,6 +207,7 @@ export abstract class OrcaRuntimeWithTerminalDrivers extends OrcaRuntimeWithFitO
|
||||
isMobileDriven: (ptyId) => this.getDriver(ptyId).kind === 'mobile',
|
||||
getTerminalSize: (ptyId) => this.getTerminalSize(ptyId) ?? null,
|
||||
resolveHostTarget: (ptyId) => this.resolveDesktopRestoreTarget(ptyId),
|
||||
hostShowsTerminals: () => this.authoritativeWindowId !== HEADLESS_RUNTIME_WINDOW_ID,
|
||||
applyLayout: async (ptyId, target) => {
|
||||
this.freshSubscribeGuard.add(ptyId)
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
import type { Page } from '@stablyai/playwright-test'
|
||||
import { toWebTerminalSurfaceTabId } from '../../src/shared/terminal-surface-id'
|
||||
import { expect, test } from './helpers/orca-app'
|
||||
import { launchHeadlessPairedRuntimeHost } from './helpers/headless-paired-runtime-host'
|
||||
import { launchPairedWebClient, type PairedWebClient } from './helpers/paired-electron-client'
|
||||
import { callColdActivationRuntime } from './helpers/paired-terminal-cold-activation-observation'
|
||||
import { createPairedTerminalParkingFixture } from './helpers/paired-terminal-parking-fixture'
|
||||
import { getTerminalContent, waitForActivePanePtyId } from './helpers/terminal'
|
||||
|
||||
// The most recent TERMINAL_WORKTREE_HOT_RETAIN_LIMIT worktrees stay warm, so park needs more.
|
||||
const WARM_WORKTREE_COUNT = 4
|
||||
|
||||
let probeCount = 0
|
||||
|
||||
/** Asks the PTY's own process for its grid; the token ties the reply to this request. */
|
||||
async function readHostPtySize(
|
||||
page: Page,
|
||||
terminal: string,
|
||||
readReportedSize: (token: string) => string | null
|
||||
): Promise<string | null> {
|
||||
const token = `probe${++probeCount}`
|
||||
await callColdActivationRuntime(page, 'terminal.send', {
|
||||
terminal,
|
||||
text: `SIZE:${token}`,
|
||||
enter: true,
|
||||
client: { id: 'viewer-size-e2e', type: 'desktop' }
|
||||
})
|
||||
let size: string | null = null
|
||||
await expect.poll(() => (size = readReportedSize(token)), { timeout: 10_000 }).not.toBeNull()
|
||||
return size
|
||||
}
|
||||
|
||||
// Parking closes the client's stream, the last viewer of the PTY. A windowless host has no pane
|
||||
// to hand the size back to, so the PTY must keep the viewer's grid instead of its spawn grid,
|
||||
// or every reveal replays a snapshot at a foreign grid (#15997).
|
||||
test('a headless host keeps a parked terminal at its last viewer grid', async ({
|
||||
testRepoPath
|
||||
}) => {
|
||||
test.setTimeout(240_000)
|
||||
const host = await launchHeadlessPairedRuntimeHost()
|
||||
const fixture = createPairedTerminalParkingFixture()
|
||||
let client: PairedWebClient | null = null
|
||||
try {
|
||||
const added = await host.client.call<{ repo: { id: string } }>('repo.add', {
|
||||
path: testRepoPath,
|
||||
kind: 'git'
|
||||
})
|
||||
client = await launchPairedWebClient(host.app, host.offer, { terminalParkingDelayMs: 100 })
|
||||
const page = client.page
|
||||
await expect
|
||||
.poll(() => page.evaluate(() => window.__store?.getState().allWorktrees()[0]?.id ?? null), {
|
||||
timeout: 30_000
|
||||
})
|
||||
.not.toBeNull()
|
||||
const fallbackWorktreeId = await page.evaluate(
|
||||
() => window.__store?.getState().allWorktrees()[0]?.id ?? ''
|
||||
)
|
||||
await page.evaluate(async () => {
|
||||
await window.__store?.getState().updateSettings({
|
||||
terminalHiddenViewParking: true,
|
||||
terminalHiddenWorktreeRetentionBudget: false
|
||||
})
|
||||
})
|
||||
const marker = `VIEWER_SIZE_${Date.now()}`
|
||||
const worktrees: { worktreeId: string; terminal: string; tabId: string }[] = []
|
||||
for (let index = 0; index <= WARM_WORKTREE_COUNT; index += 1) {
|
||||
const created = await callColdActivationRuntime<{
|
||||
startupTerminal?: { handle?: string; tabId?: string }
|
||||
worktree: { id: string }
|
||||
}>(page, 'worktree.create', {
|
||||
repo: added.result.repo.id,
|
||||
name: `viewer-size-${Date.now()}-${index}`,
|
||||
setupDecision: 'skip',
|
||||
activate: false,
|
||||
noParent: true,
|
||||
startupCommand: fixture.command(`${marker}_${index}`)
|
||||
})
|
||||
const handle = created.startupTerminal?.handle
|
||||
const hostTabId = created.startupTerminal?.tabId
|
||||
if (!handle || !hostTabId) {
|
||||
throw new Error(`Viewer-size startup terminal ${index} was not created`)
|
||||
}
|
||||
worktrees.push({
|
||||
worktreeId: created.worktree.id,
|
||||
terminal: handle,
|
||||
tabId: toWebTerminalSurfaceTabId(hostTabId)
|
||||
})
|
||||
}
|
||||
const openTab = async (entry: { worktreeId: string; tabId: string }): Promise<void> => {
|
||||
await page.evaluate((id) => {
|
||||
const state = window.__store?.getState()
|
||||
state?.setActiveView('terminal')
|
||||
state?.setActiveWorktree(id)
|
||||
}, entry.worktreeId)
|
||||
const tab = page.locator(`[data-testid="sortable-tab"][data-tab-id="${entry.tabId}"]`)
|
||||
await expect(tab).toBeVisible({ timeout: 30_000 })
|
||||
await tab.click()
|
||||
await waitForActivePanePtyId(page, 30_000)
|
||||
}
|
||||
const target = worktrees[0]
|
||||
await openTab(target)
|
||||
await expect
|
||||
.poll(() => getTerminalContent(page), { timeout: 30_000 })
|
||||
.toContain(`READY:${marker}_0`)
|
||||
const paneGrid = await page.evaluate((id) => {
|
||||
const terminal = window.__paneManagers?.get(id)?.getActivePane()?.terminal
|
||||
return terminal ? `${terminal.cols}x${terminal.rows}` : null
|
||||
}, target.tabId)
|
||||
const readTargetSize = (): Promise<string | null> =>
|
||||
readHostPtySize(page, target.terminal, (token) =>
|
||||
fixture.readReportedSize(`${marker}_0`, token)
|
||||
)
|
||||
await expect
|
||||
.poll(readTargetSize, { timeout: 30_000, intervals: [500, 1_000, 2_000] })
|
||||
.toBe(paneGrid)
|
||||
|
||||
for (const warm of worktrees.slice(1)) {
|
||||
await openTab(warm)
|
||||
}
|
||||
await page.evaluate(
|
||||
(id) => window.__store?.getState().setActiveWorktree(id),
|
||||
fallbackWorktreeId
|
||||
)
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
page.evaluate(
|
||||
(id) =>
|
||||
!window.__paneManagers?.has(id) &&
|
||||
(window.__terminalParkingDebug?.parkedTabIds() ?? []).includes(id),
|
||||
target.tabId
|
||||
),
|
||||
{ timeout: 30_000 }
|
||||
)
|
||||
.toBe(true)
|
||||
// Give a reclaim, if any, time to land before sampling the parked PTY.
|
||||
await page.waitForTimeout(1_000)
|
||||
expect(await readTargetSize()).toBe(paneGrid)
|
||||
} finally {
|
||||
await client?.dispose()
|
||||
await host.dispose()
|
||||
fixture.dispose()
|
||||
}
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
import { quotePosixShell } from '../../../src/shared/wsl-login-shell-command'
|
||||
@@ -18,6 +18,8 @@ function fixtureCommand(fixturePath: string, marker: string): string {
|
||||
|
||||
export function createPairedTerminalParkingFixture(): {
|
||||
command: (marker: string) => string
|
||||
/** PTY grid the fixture saw at `SIZE:<token>`, read off disk so a parked pane needs no client. */
|
||||
readReportedSize: (marker: string, token: string) => string | null
|
||||
dispose: () => void
|
||||
} {
|
||||
const scratch = mkdtempSync(path.join(os.tmpdir(), 'orca-paired-retention-memory-'))
|
||||
@@ -25,6 +27,7 @@ export function createPairedTerminalParkingFixture(): {
|
||||
writeFileSync(
|
||||
fixturePath,
|
||||
[
|
||||
"import { writeFileSync } from 'node:fs'",
|
||||
'const marker = process.argv[2]',
|
||||
'process.stdout.write(`READY:${marker}\\r\\n`)',
|
||||
'process.stdin.setRawMode?.(true)',
|
||||
@@ -65,6 +68,11 @@ export function createPairedTerminalParkingFixture(): {
|
||||
' process.stdout.write(`FILLED:${marker}\\r\\n`)',
|
||||
' continue',
|
||||
' }',
|
||||
" if (command.startsWith('SIZE:')) {",
|
||||
" const token = command.slice('SIZE:'.length)",
|
||||
' writeFileSync(`${process.argv[1]}.${marker}.${token}.size`, `${process.stdout.columns}x${process.stdout.rows}`)',
|
||||
' continue',
|
||||
' }',
|
||||
' process.stdout.write(`LIVE:${command}\\r\\n`)',
|
||||
' }',
|
||||
'})',
|
||||
@@ -73,6 +81,10 @@ export function createPairedTerminalParkingFixture(): {
|
||||
)
|
||||
return {
|
||||
command: (marker) => fixtureCommand(fixturePath, marker),
|
||||
readReportedSize: (marker, token) => {
|
||||
const sizePath = `${fixturePath}.${marker}.${token}.size`
|
||||
return existsSync(sizePath) ? readFileSync(sizePath, 'utf8') : null
|
||||
},
|
||||
dispose: () => rmSync(scratch, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user