mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
test(serve): pin zero duplicate agent resumes across headless serve desktop promotion (#12666)
STA-1716 reported that a packaged `orca serve` could become the single-instance owner after the desktop app exits, leaving Dock/Finder unable to restore a window — and that forcing a reopen made the headless process hydrate a renderer that interrupted and DUPLICATED live agent sessions. Verification against main found every criterion already fixed (#8646 for desktop promotion and the fail-closed CLI, #12212 for duplicate serve activation, #12574 + #9729 for the resume/ownership guards). The genuine gap was criterion 6: the ticket's own automated regression never existed. An existing reliability gate asserted PTY identity survives promotion, but nothing asserted what the incident was actually about — how many agents the promoted renderer resumes. This adds that coverage: a unit/service-level journey that drives the real single-instance lock, activation gate, settle and focus paths, then runs the real resume logic against a store seeded as a renderer freshly mounted inside the serve process, asserting zero duplicate resumes. `settleServeDesktopActivation` moved from `index.ts` into its own module with identical semantics, so the test drives the real decision rather than re-implementing it — the earlier repro had to mirror that logic locally, which is the "test passes without running the scenario" failure mode. Proven to be a real oracle: breaking each guard individually turns it red, and reverting the pre-#12574 pane form reproduces the incident exactly (two duplicate `codex resume` tabs).
This commit is contained in:
@@ -858,16 +858,17 @@
|
||||
"providers": ["local", "daemon", "ssh"],
|
||||
"coveredPlatforms": ["macos"],
|
||||
"coveredProviders": ["local", "daemon", "ssh"],
|
||||
"coverageNotes": "Deterministic unit coverage exercises activation gating, single-instance ownership, quit policy, local/remote CLI status, headless binding persistence, local daemon identity, SSH identity transfer, and the macOS serve update handoff from staged installer through atomic bundle replacement and target-version readiness. A macOS Electron journey covers headless promotion and persistent PTY identity. A disposable locally signed Electron canary exercised real ShipIt and a temporary LaunchAgent with the compiled production supervisor; full packaged Orca and Linux/Windows serve updates remain uncollected.",
|
||||
"coverageNotes": "Deterministic unit coverage exercises activation gating, single-instance ownership, quit policy, local/remote CLI status, headless binding persistence, local daemon identity, SSH identity transfer, the promoted renderer's agent-resume accounting, and the macOS serve update handoff from staged installer through atomic bundle replacement and target-version readiness. A macOS Electron journey covers headless promotion and persistent PTY identity. A disposable locally signed Electron canary exercised real ShipIt and a temporary LaunchAgent with the compiled production supervisor; full packaged Orca and Linux/Windows serve updates remain uncollected.",
|
||||
"motivatingLinks": [
|
||||
"https://github.com/stablyai/orca/issues/8457",
|
||||
"https://github.com/stablyai/orca/issues/9563"
|
||||
],
|
||||
"invariant": "A safely promotable headless serve process is the single app owner. Desktop activation preserves its daemon-backed sessions. On macOS, a CLI-supervised serve update keeps the node-mode parent alive across ShipIt's atomic bundle swap, restarts with the original serve arguments only after the target bundle is present, and clears handoff state only after that target version reports runtime readiness. Unsupported or failed handoffs leave the current serving owner intact or recover it once without an install retry loop.",
|
||||
"oracle": "Unit tests coalesce early activation, preserve daemon and SSH identity, and reproduce the update race with a staged target, old serving child, persistent CLI parent, atomic .app replacement, and replacement readiness message. They assert the parent does not exit for launchd to respawn the old app, the native updater does not launch an interactive GUI, the replacement version is verified before handoff completion, mismatches become durable failures without retries, and unsupported/preflight-failed installs do not invoke native quit or PTY cleanup. The Electron journey independently verifies headless promotion retains owner/runtime/daemon/PTY identity and terminal I/O.",
|
||||
"oracle": "Unit tests coalesce early activation, preserve daemon and SSH identity, and reproduce the update race with a staged target, old serving child, persistent CLI parent, atomic .app replacement, and replacement readiness message. They assert the parent does not exit for launchd to respawn the old app, the native updater does not launch an interactive GUI, the replacement version is verified before handoff completion, mismatches become durable failures without retries, and unsupported/preflight-failed installs do not invoke native quit or PTY cleanup. A joined lock-owner/activation/hydration contract asserts that a forced relaunch opens exactly one window and that the renderer promoted inside the serve process launches zero agent resumes, creates no replacement tab or startup command, and leaves every surviving session record untouched. The Electron journey independently verifies headless promotion retains owner/runtime/daemon/PTY identity and terminal I/O.",
|
||||
"commands": [
|
||||
"pnpm exec vitest run --config config/vitest.config.ts src/cli/runtime/launch.test.ts src/main/serve-update-handoff.test.ts src/main/updater.headless-serve-install.test.ts src/main/updater.test.ts src/main/updater.mac-install.test.ts src/main/window/attach-main-window-services.test.ts src/main/startup/serve-desktop-activation-wiring.test.ts",
|
||||
"pnpm exec vitest run --config config/vitest.config.ts src/main/startup/serve-desktop-activation.test.ts src/main/startup/serve-desktop-activation-wiring.test.ts src/main/startup/single-instance-lock.test.ts src/main/startup/window-all-closed-quit-policy.test.ts src/cli/runtime-client.test.ts src/cli/runtime/websocket-transport.test.ts src/main/runtime/orca-runtime.test.ts",
|
||||
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/lib/serve-desktop-promotion-session-continuity.test.ts",
|
||||
"pnpm exec electron-vite build --mode e2e",
|
||||
"pnpm run test:e2e -- tests/e2e/headless-serve-desktop-activation.spec.ts --workers=1"
|
||||
],
|
||||
@@ -882,6 +883,7 @@
|
||||
"src/cli/runtime-client.test.ts",
|
||||
"src/cli/runtime/websocket-transport.test.ts",
|
||||
"src/main/runtime/orca-runtime.test.ts",
|
||||
"src/renderer/src/lib/serve-desktop-promotion-session-continuity.test.ts",
|
||||
"tests/e2e/headless-serve-desktop-activation.spec.ts"
|
||||
],
|
||||
"assertionRefs": [
|
||||
@@ -963,6 +965,15 @@
|
||||
"desktop-only bell, command, and link scanners remain disabled until a real renderer graph is ready"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "src/renderer/src/lib/serve-desktop-promotion-session-continuity.test.ts",
|
||||
"assertions": [
|
||||
"a forced desktop relaunch reaching the headless lock owner opens exactly one window, and only after the persistent provider settles",
|
||||
"a duplicate `orca serve` launch never promotes the headless owner",
|
||||
"the renderer promoted inside the serve process resumes zero agents while daemon panes survive, before and after those panes rebind their PTYs",
|
||||
"no replacement resume tab, startup command, or automatic-resume claim is created, and every surviving session record is left untouched"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "tests/e2e/headless-serve-desktop-activation.spec.ts",
|
||||
"assertions": [
|
||||
@@ -973,6 +984,15 @@
|
||||
}
|
||||
],
|
||||
"evidenceRuns": [
|
||||
{
|
||||
"date": "2026-08-05",
|
||||
"runner": "local",
|
||||
"platform": "macos",
|
||||
"command": "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/lib/serve-desktop-promotion-session-continuity.test.ts",
|
||||
"result": "passed",
|
||||
"durationSeconds": 2,
|
||||
"summary": "Six tests passed joining the serve lock owner, the activation gate, and the promoted renderer's resume accounting. Red evidence: reverting the hidden-pane ownership predicate launched two duplicate codex resume tabs; additionally zeroing the live-PTY check made both hydration passes red; removing the duplicate-serve argv guard opened a window for `--serve`; always marking the gate ready removed the fail-closed diagnostic; refusing to open a window dropped both promotion assertions."
|
||||
},
|
||||
{
|
||||
"date": "2026-07-21",
|
||||
"runner": "local",
|
||||
|
||||
@@ -14,7 +14,12 @@
|
||||
"../src/main/ipc/worktree-path-comparison.ts",
|
||||
"../src/main/wsl-distro-list-output.ts",
|
||||
"../src/main/wsl-distro-retry.ts",
|
||||
"../src/main/wsl.ts"
|
||||
"../src/main/wsl.ts",
|
||||
"../src/main/startup/serve-desktop-activation.ts",
|
||||
"../src/main/startup/single-instance-lock.ts",
|
||||
"../src/main/startup/startup-diagnostics.ts",
|
||||
"../src/main/window/focus-existing-window.ts",
|
||||
"../src/main/window/macos-app-activation.ts"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
|
||||
+7
-6
@@ -174,7 +174,10 @@ import {
|
||||
} from './startup/startup-diagnostics'
|
||||
import { ensureWindowsUserDataAclGrant } from './startup/windows-user-data-acl'
|
||||
import { shouldQuitWhenAllWindowsClosed } from './startup/window-all-closed-quit-policy'
|
||||
import { createServeDesktopActivationGate } from './startup/serve-desktop-activation'
|
||||
import {
|
||||
createServeDesktopActivationGate,
|
||||
settleServeDesktopActivation as settleServeDesktopActivationGate
|
||||
} from './startup/serve-desktop-activation'
|
||||
import { RateLimitService } from './rate-limits/service'
|
||||
import { readMiniMaxSessionCookie } from './minimax/minimax-cookie-store'
|
||||
import { getInitialClaudeRateLimitTarget } from './rate-limits/claude-rate-limit-target'
|
||||
@@ -630,11 +633,9 @@ function getDesktopWindowStatus(): RuntimeDesktopWindowStatus {
|
||||
}
|
||||
|
||||
function settleServeDesktopActivation(): void {
|
||||
if (getLocalPtyProvider() instanceof LocalPtyProvider) {
|
||||
desktopActivationGate.markBlocked('persistent PTY provider unavailable')
|
||||
return
|
||||
}
|
||||
desktopActivationGate.markReady()
|
||||
settleServeDesktopActivationGate(desktopActivationGate, {
|
||||
hasPersistentPtyProvider: !(getLocalPtyProvider() instanceof LocalPtyProvider)
|
||||
})
|
||||
}
|
||||
|
||||
// Why: webContents-scoped auto-expiring flag so an intent can't leak to a later renderer load; `consume` clears on match for one-shot signals.
|
||||
|
||||
@@ -9,6 +9,24 @@ export type ServeDesktopActivationGate = {
|
||||
markBlocked: (reason: string) => void
|
||||
}
|
||||
|
||||
export const SERVE_DESKTOP_ACTIVATION_BLOCKED_REASON = 'persistent PTY provider unavailable'
|
||||
|
||||
/**
|
||||
* Why: promotion is only safe when serve owns daemon-backed (persistent) PTYs —
|
||||
* the desktop renderer then reattaches the surviving sessions instead of
|
||||
* cold-restoring them. Without that provider, fail closed (#8457).
|
||||
*/
|
||||
export function settleServeDesktopActivation(
|
||||
gate: ServeDesktopActivationGate,
|
||||
options: { hasPersistentPtyProvider: boolean }
|
||||
): void {
|
||||
if (!options.hasPersistentPtyProvider) {
|
||||
gate.markBlocked(SERVE_DESKTOP_ACTIVATION_BLOCKED_REASON)
|
||||
return
|
||||
}
|
||||
gate.markReady()
|
||||
}
|
||||
|
||||
export function createServeDesktopActivationGate(options: {
|
||||
initialState: 'initializing' | 'ready'
|
||||
activateWindow: () => void
|
||||
|
||||
@@ -0,0 +1,291 @@
|
||||
/**
|
||||
* Regression for #8457 / STA-1716 acceptance criterion 6: desktop exit +
|
||||
* surviving daemon sessions + headless `serve` holding the single-instance lock
|
||||
* + a GUI activation attempt.
|
||||
*
|
||||
* The incident chain was: Cmd+Q leaves a packaged `orca serve` owning the
|
||||
* profile lock, a forced relaunch reaches that headless process, it mounts a
|
||||
* desktop renderer, and hydration cold-restores panes whose daemon PTYs are
|
||||
* still alive — launching duplicate `codex resume <session>` agents and
|
||||
* interrupting the live ones.
|
||||
*
|
||||
* Both halves are joined here because either alone passes vacuously: promotion
|
||||
* is only safe if hydration resumes nothing, and resuming nothing only matters
|
||||
* if promotion actually happens.
|
||||
*/
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { App, BrowserWindow } from 'electron'
|
||||
import { useAppStore } from '@/store'
|
||||
import { resumeSleepingAgentSessionsForWorktree } from '@/lib/resume-sleeping-agent-session'
|
||||
import type { SleepingAgentSessionRecord } from '../../../shared/agent-session-resume'
|
||||
import { makePaneKey } from '../../../shared/stable-pane-id'
|
||||
import { focusExistingMainWindow } from '../../../main/window/focus-existing-window'
|
||||
import { createMacAppActivationHandler } from '../../../main/window/macos-app-activation'
|
||||
import {
|
||||
createServeDesktopActivationGate,
|
||||
settleServeDesktopActivation
|
||||
} from '../../../main/startup/serve-desktop-activation'
|
||||
import {
|
||||
acquireSingleInstanceLock,
|
||||
shouldActivateDesktopForSecondInstance,
|
||||
shouldBypassSingleInstanceLock,
|
||||
shouldSkipSingleInstanceLock
|
||||
} from '../../../main/startup/single-instance-lock'
|
||||
|
||||
const WORKTREE_ID = 'wt-serve-promotion'
|
||||
const AGENT_PANES = [
|
||||
{ tabId: 'tab-codex-1', leafId: '11111111-1111-4111-8111-111111111111', ptyId: 'daemon-pty-1' },
|
||||
{ tabId: 'tab-codex-2', leafId: '22222222-2222-4222-8222-222222222222', ptyId: 'daemon-pty-2' },
|
||||
{ tabId: 'tab-codex-3', leafId: '33333333-3333-4333-8333-333333333333', ptyId: 'daemon-pty-3' }
|
||||
] as const
|
||||
/** Argv macOS delivers for `open -n -a Orca` / Finder / Dock relaunch. */
|
||||
const DESKTOP_RELAUNCH_ARGV = ['/Applications/Orca.app/Contents/MacOS/Orca'] as const
|
||||
const DUPLICATE_SERVE_ARGV = [
|
||||
'/Applications/Orca.app/Contents/MacOS/Orca',
|
||||
'--serve',
|
||||
'--serve-json',
|
||||
'--serve-port',
|
||||
'6769'
|
||||
] as const
|
||||
|
||||
const initialAppStoreState = useAppStore.getState()
|
||||
|
||||
afterEach(() => {
|
||||
useAppStore.setState(initialAppStoreState, true)
|
||||
})
|
||||
|
||||
function createFakeWindow(): BrowserWindow {
|
||||
return {
|
||||
isDestroyed: () => false,
|
||||
isMinimized: () => false,
|
||||
isAlwaysOnTop: () => false,
|
||||
restore: vi.fn(),
|
||||
show: vi.fn(),
|
||||
focus: vi.fn(),
|
||||
setAlwaysOnTop: vi.fn(),
|
||||
moveTop: vi.fn()
|
||||
} as unknown as BrowserWindow
|
||||
}
|
||||
|
||||
/**
|
||||
* Mirrors only the injection index.ts performs (gate -> focusExistingWindow ->
|
||||
* openMainWindow, second-instance -> requestDesktopActivation); every decision
|
||||
* under test comes from the production modules.
|
||||
*/
|
||||
function bootHeadlessServeOwner(): {
|
||||
openMainWindowCalls: () => number
|
||||
desktopWindowStatus: () => string
|
||||
blockedReasons: readonly string[]
|
||||
secondInstance: (argv: readonly string[]) => void
|
||||
dockActivate: () => void
|
||||
settle: (options: { hasPersistentPtyProvider: boolean }) => void
|
||||
} {
|
||||
let mainWindow: BrowserWindow | null = null
|
||||
const openMainWindow = vi.fn(() => {
|
||||
mainWindow = createFakeWindow()
|
||||
return mainWindow
|
||||
})
|
||||
const blockedReasons: string[] = []
|
||||
const gate = createServeDesktopActivationGate({
|
||||
initialState: 'initializing',
|
||||
activateWindow: () => {
|
||||
focusExistingMainWindow({
|
||||
app: { focus: vi.fn(), isReady: () => true } as unknown as App,
|
||||
getWindow: () => mainWindow,
|
||||
openWindow: openMainWindow,
|
||||
platform: 'darwin'
|
||||
})
|
||||
},
|
||||
onBlocked: (reason) => blockedReasons.push(reason)
|
||||
})
|
||||
const requestDesktopActivation = (argv: readonly string[] = []): void => {
|
||||
if (!shouldActivateDesktopForSecondInstance(argv)) {
|
||||
return
|
||||
}
|
||||
gate.requestActivation()
|
||||
}
|
||||
const secondInstanceHandlers: ((argv: readonly string[]) => void)[] = []
|
||||
const lockApp = {
|
||||
requestSingleInstanceLock: () => true,
|
||||
on: (event: string, listener: (event: unknown, argv: readonly string[]) => void) => {
|
||||
if (event === 'second-instance') {
|
||||
secondInstanceHandlers.push((argv) => listener({}, argv))
|
||||
}
|
||||
}
|
||||
} as unknown as App
|
||||
expect(acquireSingleInstanceLock(lockApp, requestDesktopActivation)).toBe(true)
|
||||
expect(secondInstanceHandlers).toHaveLength(1)
|
||||
|
||||
return {
|
||||
openMainWindowCalls: () => openMainWindow.mock.calls.length,
|
||||
desktopWindowStatus: () => {
|
||||
const state = gate.getState()
|
||||
return state === 'ready' ? 'openable' : state
|
||||
},
|
||||
blockedReasons,
|
||||
secondInstance: (argv) => {
|
||||
for (const handler of secondInstanceHandlers) {
|
||||
handler(argv)
|
||||
}
|
||||
},
|
||||
dockActivate: createMacAppActivationHandler({
|
||||
getWindow: () => mainWindow,
|
||||
requestActivation: () => requestDesktopActivation(DESKTOP_RELAUNCH_ARGV)
|
||||
}),
|
||||
settle: (options) => settleServeDesktopActivation(gate, options)
|
||||
}
|
||||
}
|
||||
|
||||
function makeTerminalTab(id: string): Record<string, unknown> {
|
||||
return {
|
||||
id,
|
||||
ptyId: null,
|
||||
worktreeId: WORKTREE_ID,
|
||||
title: 'codex',
|
||||
customTitle: null,
|
||||
color: null,
|
||||
sortOrder: 0,
|
||||
createdAt: 1
|
||||
}
|
||||
}
|
||||
|
||||
function makeSurvivingAgentRecord(index: number): SleepingAgentSessionRecord {
|
||||
const pane = AGENT_PANES[index]
|
||||
return {
|
||||
paneKey: makePaneKey(pane.tabId, pane.leafId),
|
||||
tabId: pane.tabId,
|
||||
worktreeId: WORKTREE_ID,
|
||||
agent: 'codex',
|
||||
providerSession: { key: 'session_id', id: `provider-session-${index + 1}` },
|
||||
prompt: 'keep working',
|
||||
state: 'working',
|
||||
capturedAt: 1,
|
||||
updatedAt: 1,
|
||||
// Why: Cmd+Q captures live agents as quit-origin records — the exact input
|
||||
// the promoted renderer replays.
|
||||
origin: 'quit'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Seeds the store the way a renderer freshly mounted inside the serve process
|
||||
* sees it: persisted tabs/layouts with their daemon PTY bindings, and no live
|
||||
* PTY map yet unless reattach has already run.
|
||||
*/
|
||||
function hydratePromotedRenderer(options: { reattached: boolean }): SleepingAgentSessionRecord[] {
|
||||
const records = AGENT_PANES.map((_, index) => makeSurvivingAgentRecord(index))
|
||||
useAppStore.setState(
|
||||
{
|
||||
activeWorktreeId: WORKTREE_ID,
|
||||
activeTabType: 'terminal',
|
||||
// Only the first tab is the visible one; the other two mount hidden.
|
||||
activeTabId: AGENT_PANES[0].tabId,
|
||||
activeTabIdByWorktree: { [WORKTREE_ID]: AGENT_PANES[0].tabId },
|
||||
tabsByWorktree: { [WORKTREE_ID]: AGENT_PANES.map((pane) => makeTerminalTab(pane.tabId)) },
|
||||
terminalLayoutsByTabId: Object.fromEntries(
|
||||
AGENT_PANES.map((pane) => [
|
||||
pane.tabId,
|
||||
{
|
||||
root: { type: 'leaf', leafId: pane.leafId },
|
||||
activeLeafId: pane.leafId,
|
||||
expandedLeafId: null,
|
||||
ptyIdsByLeafId: { [pane.leafId]: pane.ptyId }
|
||||
}
|
||||
])
|
||||
),
|
||||
ptyIdsByTabId: options.reattached
|
||||
? Object.fromEntries(AGENT_PANES.map((pane) => [pane.tabId, [pane.ptyId]]))
|
||||
: {},
|
||||
sleepingAgentSessionsByPaneKey: Object.fromEntries(
|
||||
records.map((record) => [record.paneKey, record])
|
||||
)
|
||||
} as never,
|
||||
false
|
||||
)
|
||||
return records
|
||||
}
|
||||
|
||||
function expectNoAgentWasResumed(records: readonly SleepingAgentSessionRecord[]): void {
|
||||
const state = useAppStore.getState()
|
||||
expect(state.tabsByWorktree[WORKTREE_ID]).toHaveLength(AGENT_PANES.length)
|
||||
expect(Object.keys(state.pendingStartupByTabId)).toEqual([])
|
||||
expect(Object.keys(state.automaticAgentResumeClaimsByTabId)).toEqual([])
|
||||
for (const record of records) {
|
||||
expect(state.sleepingAgentSessionsByPaneKey[record.paneKey]).toBe(record)
|
||||
}
|
||||
}
|
||||
|
||||
describe('#8457 headless serve promotion preserves surviving agent sessions', () => {
|
||||
it('leaves a packaged serve process owning the single-instance lock', () => {
|
||||
expect(shouldSkipSingleInstanceLock({ isDev: false, isServeMode: true, env: {} })).toBe(false)
|
||||
expect(
|
||||
shouldBypassSingleInstanceLock({
|
||||
platform: 'darwin',
|
||||
isDev: false,
|
||||
isServeMode: true,
|
||||
env: { ORCA_BYPASS_SINGLE_INSTANCE_LOCK: '1' }
|
||||
})
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('opens exactly one desktop window for a forced relaunch, and only after settle', () => {
|
||||
const serve = bootHeadlessServeOwner()
|
||||
|
||||
serve.secondInstance(DESKTOP_RELAUNCH_ARGV)
|
||||
expect(serve.desktopWindowStatus()).toBe('initializing')
|
||||
expect(serve.openMainWindowCalls()).toBe(0)
|
||||
|
||||
serve.settle({ hasPersistentPtyProvider: true })
|
||||
expect(serve.desktopWindowStatus()).toBe('openable')
|
||||
expect(serve.openMainWindowCalls()).toBe(1)
|
||||
|
||||
// Dock/Finder activation now finds the window and must not build another.
|
||||
serve.dockActivate()
|
||||
serve.secondInstance(DESKTOP_RELAUNCH_ARGV)
|
||||
expect(serve.openMainWindowCalls()).toBe(1)
|
||||
expect(serve.blockedReasons).toEqual([])
|
||||
})
|
||||
|
||||
it('ignores a duplicate `orca serve` launch instead of promoting the headless owner', () => {
|
||||
const serve = bootHeadlessServeOwner()
|
||||
|
||||
serve.secondInstance(DUPLICATE_SERVE_ARGV)
|
||||
serve.settle({ hasPersistentPtyProvider: true })
|
||||
|
||||
expect(serve.openMainWindowCalls()).toBe(0)
|
||||
})
|
||||
|
||||
it('fails closed with a diagnostic when the persistent PTY provider is unavailable', () => {
|
||||
const serve = bootHeadlessServeOwner()
|
||||
|
||||
serve.secondInstance(DESKTOP_RELAUNCH_ARGV)
|
||||
serve.settle({ hasPersistentPtyProvider: false })
|
||||
|
||||
expect(serve.openMainWindowCalls()).toBe(0)
|
||||
expect(serve.desktopWindowStatus()).toBe('blocked')
|
||||
expect(serve.blockedReasons).toEqual(['persistent PTY provider unavailable'])
|
||||
})
|
||||
|
||||
it('resumes zero agents when the promoted renderer hydrates surviving daemon panes', () => {
|
||||
const serve = bootHeadlessServeOwner()
|
||||
serve.secondInstance(DESKTOP_RELAUNCH_ARGV)
|
||||
serve.settle({ hasPersistentPtyProvider: true })
|
||||
expect(serve.openMainWindowCalls()).toBe(1)
|
||||
|
||||
const records = hydratePromotedRenderer({ reattached: false })
|
||||
|
||||
expect(resumeSleepingAgentSessionsForWorktree(WORKTREE_ID)).toBe(0)
|
||||
expectNoAgentWasResumed(records)
|
||||
})
|
||||
|
||||
it('resumes zero agents on the hydration pass after the panes rebind their PTYs', () => {
|
||||
const serve = bootHeadlessServeOwner()
|
||||
serve.secondInstance(DESKTOP_RELAUNCH_ARGV)
|
||||
serve.settle({ hasPersistentPtyProvider: true })
|
||||
|
||||
const records = hydratePromotedRenderer({ reattached: true })
|
||||
|
||||
expect(resumeSleepingAgentSessionsForWorktree(WORKTREE_ID)).toBe(0)
|
||||
expectNoAgentWasResumed(records)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user