mirror of
https://github.com/stablyai/orca.git
synced 2026-10-23 00:03:52 +00:00
fix(terminal): refuse stale settled prompt proof
This commit is contained in:
@@ -9421,7 +9421,7 @@
|
||||
"https://linear.app/stably/issue/STA-4488",
|
||||
"https://github.com/stablyai/orca/issues/14525"
|
||||
],
|
||||
"invariant": "A public CLI send containing non-empty text plus Enter and no interrupt identifies agent-prompt intent. The owning runtime uses bracketed-paste and settled submit only when a bounded foreground read positively identifies the exact target as Claude or Codex on a desktop call; known transient wrappers may resolve within the existing bound, and proven current foreground identity outranks historical launch identity for render settlement. The exact PTY binding and desktop input-floor authority are rechecked before every paste chunk and delayed Enter. Every other agent, stale agent metadata over a shell, text-only input, bare Enter, interrupts, mobile input, query replies, and older clients keep the direct terminal contract.",
|
||||
"invariant": "A public CLI send containing non-empty text plus Enter and no interrupt identifies agent-prompt intent. The owning runtime uses bracketed-paste and settled submit only when a bounded foreground read positively identifies the exact target as Claude or Codex on a desktop call; known transient wrappers may resolve within the existing bound, and proven current foreground identity outranks historical launch identity for render settlement. Once target proof exists, a changed or unavailable foreground refuses the request without legacy fallback; foreground identity is rechecked after the pre-write guard and before delayed Enter, while exact PTY binding and desktop input-floor authority are rechecked before every paste chunk and delayed Enter. Every other positively identified agent or shell, text-only input, bare Enter, interrupts, mobile input, query replies, and older clients keep the direct terminal contract.",
|
||||
"oracle": "Launch a bare fake Codex through the real Electron PTY and delay its composer render for 1,200 ms, beyond the legacy generic 500 ms gap. Send a 557-byte prompt with the public CLI and require the marker, zero premature Enter, no rescue Enter, and one submission. POSIX child stdin must retain one bracketed-paste frame; Windows ConPTY consumes that control frame, so runtime tests require the frame at the PTY write boundary while the Windows child requires the exact prompt. The same harness against the installed pre-fix runtime must record one premature Enter and require one bare rescue Enter. Focused units drive a node-to-Codex wrapper transition with fake time, prove fresh foreground Codex outranks stale non-target launch identity at the render gate, and transfer the paired-mobile floor between paste and suffix while recording request, PTY generation, owner, exact writes, and ordering. The baseline must fall back or write Enter in all three cases; the candidate must settle once or reject before cross-owner Enter, with no direct fallback or duplicate input.",
|
||||
"commands": [
|
||||
"pnpm exec vitest run --config config/vitest.config.ts src/cli/handlers/terminal.test.ts src/main/runtime/rpc/terminal-agent-prompt-send.test.ts src/main/runtime/rpc/terminal-send.test.ts src/main/runtime/orca-runtime.test.ts src/shared/agent-prompt-injection.test.ts",
|
||||
@@ -9464,7 +9464,8 @@
|
||||
"a speculative CLI prompt check does not poll wrapper foregrounds",
|
||||
"a known transient wrapper resolves to Codex within the bounded foreground retry",
|
||||
"current foreground proof stays request-local inside the PTY generation queue and outranks stale launch identity",
|
||||
"identity transition, cancellation, and concurrent classification cannot write or fan out",
|
||||
"identity transition, unavailable inspection, cancellation, and concurrent classification cannot write, fall back, or fan out",
|
||||
"foreground authority is rechecked after an awaited pre-write guard and before delayed Enter",
|
||||
"the PTY write boundary retains bracketed framing and one delayed submit"
|
||||
]
|
||||
},
|
||||
@@ -9530,8 +9531,8 @@
|
||||
"platform": "macos",
|
||||
"command": "pnpm exec vitest run --config config/vitest.config.ts src/cli/handlers/terminal.test.ts src/main/runtime/rpc/terminal-agent-prompt-send.test.ts src/main/runtime/rpc/terminal-send.test.ts src/main/runtime/orca-runtime.test.ts src/shared/agent-prompt-injection.test.ts",
|
||||
"result": "passed",
|
||||
"durationSeconds": 10.8,
|
||||
"summary": "The 1,262-test candidate gate passed with one existing skip. Its three ticket oracles fail on origin/main and with only the candidate production hunks disabled, then pass together on the candidate. Follow-up adversarial contracts also prove request-local identity, cancellation after one wrapper read, serialized concurrent classification, and a non-yielding floor check beside each PTY write."
|
||||
"durationSeconds": 11.6,
|
||||
"summary": "The final 1,235-test candidate gate passed with one existing skip in 11.6s. Its three ticket oracles fail on origin/main and with only the candidate production hunks disabled, then pass together on the candidate. Follow-up adversarial contracts also prove request-local identity, prompt refusal after foreground loss or unavailable inspection, abort-aware foreground reads, serialized classification, and a non-yielding floor check beside each PTY write."
|
||||
},
|
||||
{
|
||||
"date": "2026-08-20",
|
||||
@@ -9539,8 +9540,8 @@
|
||||
"platform": "macos",
|
||||
"command": "pnpm run build:cli && pnpm exec playwright test tests/e2e/terminal-send-agent-prompt-submit.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1",
|
||||
"result": "passed",
|
||||
"durationSeconds": 34.6,
|
||||
"summary": "All four source-built Electron prompt journeys passed. The encrypted paired-mobile case recorded desktop then mobile authority, the exact bracketed prompt frame plus mobile bytes at fake-agent stdin, no carriage return, and an accepted=false desktop result after takeover."
|
||||
"durationSeconds": 35.2,
|
||||
"summary": "All four source-built Electron prompt journeys passed. The encrypted paired-mobile case recorded desktop then mobile authority, the exact and only bracketed prompt frame followed by mobile bytes at fake-agent stdin, no carriage return, and an accepted=false desktop result after takeover."
|
||||
},
|
||||
{
|
||||
"date": "2026-08-20",
|
||||
@@ -9548,8 +9549,8 @@
|
||||
"platform": "linux",
|
||||
"command": "pnpm run build:relay && ORCA_E2E_SSH_DOCKER=1 pnpm exec playwright test tests/e2e/ssh-docker-agent-prompt-submit.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1",
|
||||
"result": "passed",
|
||||
"durationSeconds": 39.4,
|
||||
"summary": "The candidate passed against an ephemeral Debian SSH target and real relay with two controlled half-open process reads, an asserted upper retry bound, one post-render remote Enter, SSH execution-host proof, unconditional target cleanup, and an unrelated live remote PTY. The byte-identical oracle fails when only settled-wrapper delivery is disabled because legacy delivery settles before the retry barrier."
|
||||
"durationSeconds": 37.2,
|
||||
"summary": "The candidate passed against an ephemeral Debian SSH target and real relay with two controlled half-open process reads, an asserted upper retry bound, one post-render remote Enter, SSH execution-host proof, bounded best-effort terminal close followed by unconditional target cleanup, and an unrelated live remote PTY. The byte-identical oracle fails when only settled-wrapper delivery is disabled because legacy delivery settles before the retry barrier."
|
||||
}
|
||||
],
|
||||
"runtimeBudget": {
|
||||
|
||||
@@ -24236,10 +24236,107 @@ describe('OrcaRuntimeService', () => {
|
||||
runtime.sendTerminalAgentPrompt(handle, 'review this change', {
|
||||
requireSettledForeground: true
|
||||
})
|
||||
).resolves.toMatchObject({ accepted: false, bytesWritten: 0 })
|
||||
).rejects.toThrow('terminal_guard_not_writable')
|
||||
expect(writes).toEqual([])
|
||||
})
|
||||
|
||||
it('refuses settled delivery when foreground inspection becomes unavailable', async () => {
|
||||
const writes: string[] = []
|
||||
const getForegroundProcess = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce('codex')
|
||||
.mockRejectedValueOnce(new Error('foreground unavailable'))
|
||||
const runtime = new OrcaRuntimeService(store)
|
||||
runtime.setPtyController({
|
||||
spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }),
|
||||
write: (_ptyId, data) => {
|
||||
writes.push(data)
|
||||
return true
|
||||
},
|
||||
kill: () => true,
|
||||
getForegroundProcess
|
||||
})
|
||||
const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, {
|
||||
command: 'bash',
|
||||
title: 'bash'
|
||||
})
|
||||
|
||||
await expect(
|
||||
runtime.sendTerminalAgentPrompt(handle, 'review this change', {
|
||||
requireSettledForeground: true
|
||||
})
|
||||
).rejects.toThrow('terminal_guard_not_writable')
|
||||
expect(writes).toEqual([])
|
||||
})
|
||||
|
||||
it('revalidates settled foreground after the pre-write guard yields', async () => {
|
||||
const writes: string[] = []
|
||||
const getForegroundProcess = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce('codex')
|
||||
.mockResolvedValueOnce('codex')
|
||||
.mockResolvedValueOnce('codex')
|
||||
.mockResolvedValue('zsh')
|
||||
const runtime = new OrcaRuntimeService(store)
|
||||
runtime.setPtyController({
|
||||
spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }),
|
||||
write: (_ptyId, data) => {
|
||||
writes.push(data)
|
||||
return true
|
||||
},
|
||||
kill: () => true,
|
||||
getForegroundProcess
|
||||
})
|
||||
const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, {
|
||||
command: 'bash',
|
||||
title: 'bash'
|
||||
})
|
||||
|
||||
await expect(
|
||||
runtime.sendTerminalAgentPrompt(handle, 'review this change', {
|
||||
beforeWrite: async () => undefined,
|
||||
requireSettledForeground: true
|
||||
})
|
||||
).rejects.toThrow('terminal_guard_not_writable')
|
||||
expect(writes).toEqual([])
|
||||
})
|
||||
|
||||
it('revalidates settled foreground before delayed Enter', async () => {
|
||||
vi.useFakeTimers()
|
||||
const writes: string[] = []
|
||||
const getForegroundProcess = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce('codex')
|
||||
.mockResolvedValueOnce('codex')
|
||||
.mockResolvedValueOnce('codex')
|
||||
.mockResolvedValueOnce('codex')
|
||||
.mockResolvedValue('zsh')
|
||||
const runtime = new OrcaRuntimeService(store)
|
||||
runtime.setPtyController({
|
||||
spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }),
|
||||
write: (_ptyId, data) => {
|
||||
writes.push(data)
|
||||
return true
|
||||
},
|
||||
kill: () => true,
|
||||
getForegroundProcess
|
||||
})
|
||||
const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, {
|
||||
command: 'bash',
|
||||
title: 'bash'
|
||||
})
|
||||
|
||||
const send = runtime.sendTerminalAgentPrompt(handle, 'review this change', {
|
||||
requireSettledForeground: true
|
||||
})
|
||||
const rejected = expect(send).rejects.toThrow('terminal_guard_not_writable')
|
||||
await vi.runAllTimersAsync()
|
||||
|
||||
await rejected
|
||||
expect(writes.some((data) => data.includes('review this change'))).toBe(true)
|
||||
expect(writes.filter((data) => data === '\r')).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('cancels bounded wrapper recognition without writing or retrying', async () => {
|
||||
vi.useFakeTimers()
|
||||
const controller = new AbortController()
|
||||
@@ -24275,6 +24372,43 @@ describe('OrcaRuntimeService', () => {
|
||||
expect(writes).toEqual([])
|
||||
})
|
||||
|
||||
it('cancels an in-flight settled foreground inspection without writing', async () => {
|
||||
const controller = new AbortController()
|
||||
let inspectionStarted!: () => void
|
||||
const started = new Promise<void>((resolve) => {
|
||||
inspectionStarted = resolve
|
||||
})
|
||||
const runtime = new OrcaRuntimeService(store)
|
||||
const writes: string[] = []
|
||||
runtime.setPtyController({
|
||||
spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }),
|
||||
write: (_ptyId, data) => {
|
||||
writes.push(data)
|
||||
return true
|
||||
},
|
||||
kill: () => true,
|
||||
getForegroundProcess: async () => {
|
||||
inspectionStarted()
|
||||
return await new Promise<string | null>(() => undefined)
|
||||
}
|
||||
})
|
||||
const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, {
|
||||
command: 'bash',
|
||||
title: 'bash'
|
||||
})
|
||||
const send = runtime.sendTerminalAgentPrompt(handle, 'review this change', {
|
||||
requireSettledForeground: true,
|
||||
signal: controller.signal
|
||||
})
|
||||
const rejected = expect(send).rejects.toThrow('request_aborted')
|
||||
|
||||
await started
|
||||
controller.abort()
|
||||
|
||||
await rejected
|
||||
expect(writes).toEqual([])
|
||||
})
|
||||
|
||||
it('serializes concurrent settled foreground classification per PTY', async () => {
|
||||
let releaseFirst!: () => void
|
||||
const firstInspection = new Promise<void>((resolve) => {
|
||||
|
||||
@@ -19391,6 +19391,11 @@ export class OrcaRuntimeService {
|
||||
await options.beforeWrite?.(ptyId)
|
||||
assertAgentPromptRequestActive(options.signal)
|
||||
this.assertAgentPromptGeneration(ptyId, generation)
|
||||
if (!wrotePasteBytes && options.settlementAgent) {
|
||||
await this.assertSettledPromptForeground(ptyId, options.settlementAgent, options.signal)
|
||||
assertAgentPromptRequestActive(options.signal)
|
||||
this.assertAgentPromptGeneration(ptyId, generation)
|
||||
}
|
||||
this.assertAgentPromptPermissionSafe(
|
||||
permissionBaseline,
|
||||
this.getAgentPromptActivity(handle, ptyId)
|
||||
@@ -19444,6 +19449,11 @@ export class OrcaRuntimeService {
|
||||
}
|
||||
assertAgentPromptRequestActive(options.signal)
|
||||
this.assertAgentPromptGeneration(ptyId, generation)
|
||||
if (options.settlementAgent) {
|
||||
await this.assertSettledPromptForeground(ptyId, options.settlementAgent, options.signal)
|
||||
assertAgentPromptRequestActive(options.signal)
|
||||
this.assertAgentPromptGeneration(ptyId, generation)
|
||||
}
|
||||
const baseline = this.getAgentPromptActivity(handle, ptyId)
|
||||
this.assertAgentPromptPermissionSafe(permissionBaseline, baseline)
|
||||
options.assertWriteAuthority?.(ptyId)
|
||||
@@ -19532,6 +19542,24 @@ export class OrcaRuntimeService {
|
||||
}
|
||||
}
|
||||
|
||||
private async assertSettledPromptForeground(
|
||||
ptyId: string,
|
||||
expectedAgent: TuiAgent,
|
||||
signal?: AbortSignal
|
||||
): Promise<void> {
|
||||
if (!this.ptyController) {
|
||||
throw new Error('terminal_guard_not_writable')
|
||||
}
|
||||
const foregroundProcess = await waitForAgentPromptPromise(
|
||||
this.ptyController.getForegroundProcess(ptyId),
|
||||
signal
|
||||
)
|
||||
assertAgentPromptRequestActive(signal)
|
||||
if (recognizeAgentProcess(foregroundProcess)?.agent !== expectedAgent) {
|
||||
throw new Error('terminal_guard_not_writable')
|
||||
}
|
||||
}
|
||||
|
||||
private createAgentPromptRenderGate(
|
||||
ptyId: string,
|
||||
provenAgent?: TuiAgent
|
||||
@@ -34604,7 +34632,7 @@ export class OrcaRuntimeService {
|
||||
// Why: title is the tightest agent-presence signal, but a Claude management title is negative evidence for task activity.
|
||||
async isTerminalRunningAgent(
|
||||
handle: string,
|
||||
options: { retryForegroundWrappers?: boolean } = {}
|
||||
options: { retryForegroundWrappers?: boolean; signal?: AbortSignal } = {}
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const pty = this.getLivePtyForHandle(handle)
|
||||
@@ -34645,7 +34673,10 @@ export class OrcaRuntimeService {
|
||||
if (!leaf.ptyId || !this.ptyController) {
|
||||
return false
|
||||
}
|
||||
const fg = await this.ptyController.getForegroundProcess(leaf.ptyId)
|
||||
const fg = await waitForAgentPromptPromise(
|
||||
this.ptyController.getForegroundProcess(leaf.ptyId),
|
||||
options.signal
|
||||
)
|
||||
// Why: a bare `Cursor Agent` title is identity, not liveness — it reads the same
|
||||
// whether cursor-agent is parked or long exited with the shell back. A null
|
||||
// foreground is untracked, not alive, so no-evidence must stay a refusal. A live
|
||||
@@ -34662,9 +34693,11 @@ export class OrcaRuntimeService {
|
||||
// Why: review-note delivery auto-submits with Enter, so only known agent processes are safe (not arbitrary focused TUIs).
|
||||
return await this.isRecognizedForegroundAgentProcess(leaf.ptyId, fg, {
|
||||
suppressClaude: shouldSuppressClaudeForeground,
|
||||
retryWrappers: options.retryForegroundWrappers !== false
|
||||
retryWrappers: options.retryForegroundWrappers !== false,
|
||||
signal: options.signal
|
||||
})
|
||||
} catch {
|
||||
assertAgentPromptRequestActive(options.signal)
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -34696,27 +34729,43 @@ export class OrcaRuntimeService {
|
||||
return null
|
||||
}
|
||||
assertAgentPromptRequestActive(signal)
|
||||
const foregroundProcess = await this.ptyController.getForegroundProcess(ptyId)
|
||||
const foregroundProcess = await waitForAgentPromptPromise(
|
||||
this.ptyController.getForegroundProcess(ptyId),
|
||||
signal
|
||||
)
|
||||
const recognized = await this.recognizeForegroundAgentProcess(ptyId, foregroundProcess, {
|
||||
signal
|
||||
})
|
||||
if (!isTerminalSendSettlementAgent(recognized?.agent)) {
|
||||
if (!foregroundProcess || this.isAgentWrapperForegroundProcess(foregroundProcess)) {
|
||||
throw new Error('terminal_guard_not_writable')
|
||||
}
|
||||
return null
|
||||
}
|
||||
assertAgentPromptRequestActive(signal)
|
||||
if (!(await this.isTerminalRunningAgent(handle, { retryForegroundWrappers: false }))) {
|
||||
return null
|
||||
if (
|
||||
!(await this.isTerminalRunningAgent(handle, {
|
||||
retryForegroundWrappers: false,
|
||||
signal
|
||||
}))
|
||||
) {
|
||||
throw new Error('terminal_guard_not_writable')
|
||||
}
|
||||
assertAgentPromptRequestActive(signal)
|
||||
const confirmed = recognizeAgentProcess(await this.ptyController.getForegroundProcess(ptyId))
|
||||
const confirmed = recognizeAgentProcess(
|
||||
await waitForAgentPromptPromise(this.ptyController.getForegroundProcess(ptyId), signal)
|
||||
)
|
||||
assertAgentPromptRequestActive(signal)
|
||||
return confirmed?.agent === recognized.agent ? recognized.agent : null
|
||||
if (confirmed?.agent !== recognized.agent) {
|
||||
throw new Error('terminal_guard_not_writable')
|
||||
}
|
||||
return recognized.agent
|
||||
}
|
||||
|
||||
private async isPtyRunningAgent(
|
||||
pty: RuntimePtyWorktreeRecord,
|
||||
leaf: RuntimeLeafRecord | null = null,
|
||||
options: { retryForegroundWrappers?: boolean } = {}
|
||||
options: { retryForegroundWrappers?: boolean; signal?: AbortSignal } = {}
|
||||
): Promise<boolean> {
|
||||
const leafTitle = leaf
|
||||
? getLatestAgentCandidateTitle(
|
||||
@@ -34760,7 +34809,10 @@ export class OrcaRuntimeService {
|
||||
if (!this.ptyController) {
|
||||
return false
|
||||
}
|
||||
const fg = await this.ptyController.getForegroundProcess(pty.ptyId)
|
||||
const fg = await waitForAgentPromptPromise(
|
||||
this.ptyController.getForegroundProcess(pty.ptyId),
|
||||
options.signal
|
||||
)
|
||||
// Why: mirrors the leaf path — an unreadable foreground is indistinguishable from an
|
||||
// exited one, so a bare Cursor identity title never substitutes for corroboration.
|
||||
if (!fg) {
|
||||
@@ -34776,14 +34828,15 @@ export class OrcaRuntimeService {
|
||||
// Why: review-note delivery auto-submits with Enter, so only known agent processes are safe (not arbitrary focused TUIs).
|
||||
return await this.isRecognizedForegroundAgentProcess(pty.ptyId, fg, {
|
||||
suppressClaude: shouldSuppressClaudeForeground,
|
||||
retryWrappers: options.retryForegroundWrappers !== false
|
||||
retryWrappers: options.retryForegroundWrappers !== false,
|
||||
signal: options.signal
|
||||
})
|
||||
}
|
||||
|
||||
private async isRecognizedForegroundAgentProcess(
|
||||
ptyId: string,
|
||||
foregroundProcess: string,
|
||||
options: { suppressClaude?: boolean; retryWrappers?: boolean } = {}
|
||||
options: { suppressClaude?: boolean; retryWrappers?: boolean; signal?: AbortSignal } = {}
|
||||
): Promise<boolean> {
|
||||
return (await this.recognizeForegroundAgentProcess(ptyId, foregroundProcess, options)) !== null
|
||||
}
|
||||
@@ -34812,7 +34865,10 @@ export class OrcaRuntimeService {
|
||||
while (Date.now() - startedAt < FOREGROUND_AGENT_WRAPPER_RETRY_TIMEOUT_MS) {
|
||||
await waitForAgentPromptDelay(FOREGROUND_AGENT_WRAPPER_RETRY_INTERVAL_MS, options.signal)
|
||||
assertAgentPromptRequestActive(options.signal)
|
||||
const refreshedProcess = await this.ptyController.getForegroundProcess(ptyId)
|
||||
const refreshedProcess = await waitForAgentPromptPromise(
|
||||
this.ptyController.getForegroundProcess(ptyId),
|
||||
options.signal
|
||||
)
|
||||
assertAgentPromptRequestActive(options.signal)
|
||||
const refreshedRecognition = recognizeAgentProcess(refreshedProcess)
|
||||
if (refreshedRecognition !== null) {
|
||||
|
||||
@@ -89,6 +89,36 @@ describe('terminal agent prompt send RPC', () => {
|
||||
expect(sendTerminalAgentPrompt).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('does not fall back after settled foreground authority is lost', async () => {
|
||||
const sendTerminal = vi.fn()
|
||||
const sendTerminalAgentPrompt = vi
|
||||
.fn()
|
||||
.mockRejectedValue(new Error('terminal_guard_not_writable'))
|
||||
const runtime = makeRuntime({
|
||||
resolveLiveLeafForHandle: vi.fn().mockReturnValue({ ptyId: 'pty-1' }),
|
||||
getDriver: vi.fn().mockReturnValue({ kind: 'idle' }),
|
||||
sendTerminal,
|
||||
sendTerminalAgentPrompt
|
||||
})
|
||||
const dispatcher = new RpcDispatcher({ runtime, methods: TERMINAL_METHODS })
|
||||
|
||||
const response = await dispatcher.dispatch(
|
||||
makeRequest({
|
||||
terminal: 'terminal-1',
|
||||
text: 'review this change',
|
||||
enter: true,
|
||||
agentPrompt: true,
|
||||
client: { id: 'orca-cli', type: 'desktop' }
|
||||
})
|
||||
)
|
||||
|
||||
expect(response).toMatchObject({
|
||||
ok: true,
|
||||
result: { send: { accepted: false, bytesWritten: 0 } }
|
||||
})
|
||||
expect(sendTerminal).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not send delayed Enter after a paired mobile client takes the floor', async () => {
|
||||
const writes: string[] = []
|
||||
const ordering: string[] = []
|
||||
|
||||
@@ -27,6 +27,7 @@ const PS_RETRY = '/tmp/orca-prompt-ps-retry'
|
||||
const PS_RETRY_ENTERED = '/tmp/orca-prompt-ps-retry-entered'
|
||||
const PS_RETRY_RELEASE = '/tmp/orca-prompt-ps-retry-release'
|
||||
const PS_COUNT = '/tmp/orca-prompt-ps-count'
|
||||
const TERMINAL_CLOSE_CLEANUP_TIMEOUT_MS = 5_000
|
||||
const execFileAsync = promisify(execFile)
|
||||
|
||||
type RuntimeTerminalSummary = {
|
||||
@@ -48,6 +49,26 @@ async function callRuntime<TResult>(page: Page, method: string, params: unknown)
|
||||
) as Promise<TResult>
|
||||
}
|
||||
|
||||
async function closeRemoteTerminalsBestEffort(page: Page, handles: string[]): Promise<void> {
|
||||
let timeout: NodeJS.Timeout | null = null
|
||||
try {
|
||||
await Promise.race([
|
||||
Promise.all(
|
||||
handles.map((terminal) =>
|
||||
callRuntime(page, 'terminal.closeTab', { terminal }).catch(() => undefined)
|
||||
)
|
||||
),
|
||||
new Promise<void>((resolve) => {
|
||||
timeout = setTimeout(resolve, TERMINAL_CLOSE_CLEANUP_TIMEOUT_MS)
|
||||
})
|
||||
])
|
||||
} finally {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function createRemoteTerminal(
|
||||
page: Page,
|
||||
worktreeId: string,
|
||||
@@ -263,22 +284,21 @@ test.describe('Docker SSH settled prompt submission', () => {
|
||||
description: `target=${remote.targetId} worktree=${remote.worktreeId} agentPty=${terminal.ptyId} survivorPty=${survivor.ptyId} psCalls=${remoteFile(target, PS_COUNT)}`
|
||||
})
|
||||
} finally {
|
||||
await Promise.all(
|
||||
terminalHandles.map((terminal) =>
|
||||
callRuntime(orcaPage, 'terminal.closeTab', { terminal }).catch(() => undefined)
|
||||
)
|
||||
)
|
||||
if (target) {
|
||||
try {
|
||||
execDockerSshRelayTargetControlCommand(
|
||||
target,
|
||||
`rm -f ${PS_OBSERVE} ${PS_FAIL} ${PS_RELEASE} ${PS_RETRY} ${PS_RETRY_RELEASE}`
|
||||
)
|
||||
} finally {
|
||||
try {
|
||||
await closeRemoteTerminalsBestEffort(orcaPage, terminalHandles)
|
||||
} finally {
|
||||
if (target) {
|
||||
try {
|
||||
execDockerSshRelayTargetControlCommand(
|
||||
target,
|
||||
`rm -f ${PS_OBSERVE} ${PS_FAIL} ${PS_RELEASE} ${PS_RETRY} ${PS_RETRY_RELEASE}`
|
||||
)
|
||||
} finally {
|
||||
cleanupDockerSshRelayTarget(target)
|
||||
}
|
||||
} else {
|
||||
cleanupDockerSshRelayTarget(target)
|
||||
}
|
||||
} else {
|
||||
cleanupDockerSshRelayTarget(target)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -356,14 +356,8 @@ test('paired mobile floor takeover blocks the CLI delayed Enter', async ({
|
||||
send: { accepted: false, bytesWritten: 0 }
|
||||
})
|
||||
const observedInput = readObservedInput(fixtureInputObservation)
|
||||
expect(observedInput).toContain(marker)
|
||||
expect(observedInput).toContain('mobile-owner-input')
|
||||
if (process.platform === 'win32') {
|
||||
expect(observedInput).toContain(prompt)
|
||||
} else {
|
||||
expect(observedInput).toContain(`\x1b[200~${prompt}\x1b[201~`)
|
||||
}
|
||||
expect(observedInput).not.toContain('\r')
|
||||
const desktopFrame = process.platform === 'win32' ? prompt : `\x1b[200~${prompt}\x1b[201~`
|
||||
expect(observedInput).toBe(`${desktopFrame}mobile-owner-input`)
|
||||
} finally {
|
||||
subscription.close()
|
||||
await callDevCli(
|
||||
|
||||
Reference in New Issue
Block a user