mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
* fix(orchestration): point idle headless terminals at their mail A headless orca serve publishes no window leaves, so the mailbox lookups (getLiveLeafForHandle, leaves, handleByLeafKey, the title idle edge) found no target and the pointer never reached a live coordinator. When no window publishes a terminal's pane, its connected PTY record now stands in as the mailbox target, built from the runtime's existing PTY-lane lookups. A window leaf stays authoritative, and the idle and settled gates are unchanged. Fixes #33228 Co-authored-by: Fede654 <buzondefede@gmail.com> * fix(orchestration): keep the PTY mail lane to a ready headless graph A window host's missing leaf means a reload, crash or unmounted pane, so the lane must not type there. It now runs only under ready headless graph authority, keeps assertGraphReady, and defers to any leaf that holds the PTY. The parked submit target reuses the same PTY leaf builder. Co-authored-by: Fede654 <buzondefede@gmail.com> * fix(orchestration): type no headless pointer while two PTYs claim one pane A retained predecessor and its replacement can both be connected at the same recorded pane; the lane now finds no owner then, so mail waits for an explicit check instead of reaching the wrong process. Co-authored-by: Fede654 <buzondefede@gmail.com> * fix(orchestration): one pane-owner check for every headless pointer step The headless lane now also refuses a pane a window retracted before it closed, and the delayed Enter re-runs the same owner check, so a claimant that appears after the pointer is typed withholds the submit. Co-authored-by: Fede654 <buzondefede@gmail.com> * fix(orchestration): a cancelled desktop promotion keeps headless mail flowing Restoring headless authority clears handle records the PTY lane still owns; mailbox handle checks now re-issue them under headless authority, as other PTY-owned handle reads do. Co-authored-by: Fede654 <buzondefede@gmail.com> --------- Co-authored-by: Fede654 <buzondefede@gmail.com>
192 lines
6.4 KiB
TypeScript
192 lines
6.4 KiB
TypeScript
import './rpc/unused-default-rpc-methods.test-fixture'
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { OrchestrationDb } from './orchestration/db'
|
|
import type { OrcaRuntimeService } from './orca-runtime'
|
|
import { HEADLESS_RUNTIME_WINDOW_ID } from '../../shared/runtime-types'
|
|
import {
|
|
createRuntime,
|
|
PANE_KEY,
|
|
pointerCount,
|
|
PTY_ID,
|
|
TAB_ID,
|
|
LEAF_ID,
|
|
WORKTREE_ID
|
|
} from './orchestration-mailbox-notification-test-harness'
|
|
|
|
type Host = 'headless' | 'window-empty-graph' | 'window-graph-unavailable'
|
|
|
|
// A headless serve publishes no window leaves, so its terminals live only in the PTY lane.
|
|
async function coordinator(host: Host = 'headless') {
|
|
const db = new OrchestrationDb(':memory:')
|
|
const { runtime, write } = createRuntime(db, { headless: host === 'headless' })
|
|
if (host !== 'headless') {
|
|
runtime.syncWindowGraph(1, { tabs: [], leaves: [] })
|
|
}
|
|
const [terminal] = (await runtime.listTerminals()).terminals
|
|
const run = db.createRun({
|
|
objective: 'Headless Run',
|
|
coordinatorHandle: terminal.handle,
|
|
coordinatorPaneKey: PANE_KEY
|
|
})
|
|
if (host === 'window-graph-unavailable') {
|
|
runtime.markGraphUnavailable(1)
|
|
}
|
|
const sendRunMail = () => {
|
|
const message = db.insertMessage({
|
|
from: 'term_worker',
|
|
to: `run:${run.id}`,
|
|
subject: 'Worker complete',
|
|
type: 'worker_done',
|
|
runId: run.id
|
|
})
|
|
runtime.notifyMessageArrived(`run:${run.id}`, 'worker_done')
|
|
return message
|
|
}
|
|
return { db, runtime, write, sendRunMail }
|
|
}
|
|
|
|
async function goIdle(runtime: OrcaRuntimeService): Promise<void> {
|
|
runtime.onPtyData(PTY_ID, '\x1b]0;Codex working\x07', 1)
|
|
runtime.onPtyData(PTY_ID, '\x1b]0;Codex done\x07', 2)
|
|
await runtime.serializeMainTerminalBuffer(PTY_ID)
|
|
}
|
|
|
|
describe('headless orchestration mailbox pointer', () => {
|
|
beforeEach(() => {
|
|
vi.useFakeTimers()
|
|
})
|
|
afterEach(() => {
|
|
vi.useRealTimers()
|
|
})
|
|
|
|
it('points an already idle coordinator at Run mail', async () => {
|
|
const { db, runtime, write, sendRunMail } = await coordinator()
|
|
await goIdle(runtime)
|
|
|
|
const message = sendRunMail()
|
|
await vi.advanceTimersByTimeAsync(0)
|
|
expect(pointerCount(write)).toBe(1)
|
|
await vi.advanceTimersByTimeAsync(500)
|
|
expect(write).toHaveBeenCalledWith(PTY_ID, '\r')
|
|
expect(db.getMessageById(message.id)?.delivered_at).toEqual(expect.any(String))
|
|
db.close()
|
|
})
|
|
|
|
it('points a coordinator at mail that arrived while it worked once it goes idle', async () => {
|
|
const { db, runtime, write, sendRunMail } = await coordinator()
|
|
runtime.onPtyData(PTY_ID, '\x1b]0;Codex working\x07', 1)
|
|
await runtime.serializeMainTerminalBuffer(PTY_ID)
|
|
const message = sendRunMail()
|
|
await vi.advanceTimersByTimeAsync(0)
|
|
expect(pointerCount(write)).toBe(0)
|
|
|
|
runtime.onPtyData(PTY_ID, '\x1b]0;Codex done\x07', 2)
|
|
await runtime.serializeMainTerminalBuffer(PTY_ID)
|
|
await vi.advanceTimersByTimeAsync(0)
|
|
expect(pointerCount(write)).toBe(1)
|
|
await vi.advanceTimersByTimeAsync(500)
|
|
expect(db.getMessageById(message.id)?.delivered_at).toEqual(expect.any(String))
|
|
db.close()
|
|
})
|
|
|
|
it('waits for a name-only idle pane to go quiet before typing the pointer', async () => {
|
|
const { db, runtime, write, sendRunMail } = await coordinator()
|
|
sendRunMail()
|
|
runtime.onPtyData(PTY_ID, '\x1b]0;⠋ Codex\x07working\n', Date.now())
|
|
runtime.onPtyData(PTY_ID, '\x1b]0;Codex\x07still working\n', Date.now())
|
|
await runtime.serializeMainTerminalBuffer(PTY_ID)
|
|
expect(pointerCount(write)).toBe(0)
|
|
|
|
await vi.advanceTimersByTimeAsync(3_050)
|
|
expect(pointerCount(write)).toBe(1)
|
|
db.close()
|
|
})
|
|
|
|
it('types into no PTY while two live PTYs claim the coordinator pane', async () => {
|
|
const { db, runtime, write, sendRunMail } = await coordinator()
|
|
runtime.registerPty('pty-mailbox-replacement', WORKTREE_ID, null, {
|
|
tabId: TAB_ID,
|
|
leafId: LEAF_ID,
|
|
incarnationId: 'mailbox-replacement-incarnation'
|
|
})
|
|
await runtime.listTerminals()
|
|
await goIdle(runtime)
|
|
sendRunMail()
|
|
await vi.advanceTimersByTimeAsync(3_500)
|
|
expect(pointerCount(write)).toBe(0)
|
|
db.close()
|
|
})
|
|
|
|
it('withholds Enter when a second PTY claims the pane after the pointer was typed', async () => {
|
|
const { db, runtime, write, sendRunMail } = await coordinator()
|
|
await goIdle(runtime)
|
|
sendRunMail()
|
|
await vi.advanceTimersByTimeAsync(0)
|
|
expect(pointerCount(write)).toBe(1)
|
|
|
|
runtime.registerPty('pty-mailbox-replacement', WORKTREE_ID, null, {
|
|
tabId: TAB_ID,
|
|
leafId: LEAF_ID,
|
|
incarnationId: 'mailbox-replacement-incarnation'
|
|
})
|
|
await vi.advanceTimersByTimeAsync(500)
|
|
expect(write).not.toHaveBeenCalledWith(PTY_ID, '\r')
|
|
db.close()
|
|
})
|
|
|
|
it('still points an idle coordinator after a desktop promotion is cancelled', async () => {
|
|
const { db, runtime, write, sendRunMail } = await coordinator()
|
|
await goIdle(runtime)
|
|
runtime.attachWindow(1)
|
|
runtime.markGraphUnavailable(1)
|
|
|
|
sendRunMail()
|
|
await vi.advanceTimersByTimeAsync(0)
|
|
expect(pointerCount(write)).toBe(1)
|
|
db.close()
|
|
})
|
|
|
|
it('does not revive a pane a serve window retracted before it closed', async () => {
|
|
const { db, runtime, write, sendRunMail } = await coordinator()
|
|
runtime.attachWindow(1)
|
|
runtime.syncWindowGraph(1, {
|
|
tabs: [
|
|
{
|
|
tabId: TAB_ID,
|
|
worktreeId: WORKTREE_ID,
|
|
title: 'Codex',
|
|
activeLeafId: LEAF_ID,
|
|
layout: null
|
|
}
|
|
],
|
|
leaves: [
|
|
{ tabId: TAB_ID, worktreeId: WORKTREE_ID, leafId: LEAF_ID, paneRuntimeId: 1, ptyId: PTY_ID }
|
|
]
|
|
})
|
|
runtime.syncWindowGraph(1, { tabs: [], leaves: [] })
|
|
runtime.markGraphUnavailable(1)
|
|
expect([runtime['authoritativeWindowId'], runtime['graphStatus']]).toEqual([
|
|
HEADLESS_RUNTIME_WINDOW_ID,
|
|
'ready'
|
|
])
|
|
await goIdle(runtime)
|
|
sendRunMail()
|
|
await vi.advanceTimersByTimeAsync(3_500)
|
|
expect(pointerCount(write)).toBe(0)
|
|
db.close()
|
|
})
|
|
|
|
// A window's missing leaf means reload, crash or an unmounted pane, not a headless terminal.
|
|
it.each(['window-empty-graph', 'window-graph-unavailable'] as const)(
|
|
'does not type into a leafless PTY on a %s host',
|
|
async (host) => {
|
|
const { db, runtime, write, sendRunMail } = await coordinator(host)
|
|
await goIdle(runtime)
|
|
sendRunMail()
|
|
await vi.advanceTimersByTimeAsync(3_500)
|
|
expect(pointerCount(write)).toBe(0)
|
|
db.close()
|
|
}
|
|
)
|
|
})
|