mirror of
https://github.com/stablyai/orca.git
synced 2026-10-23 00:03:52 +00:00
test(terminal): cover settled prompt ownership topologies
This commit is contained in:
@@ -9411,9 +9411,9 @@
|
||||
],
|
||||
"platforms": ["macos", "linux", "windows"],
|
||||
"providers": ["local", "daemon", "ssh", "remote-runtime"],
|
||||
"coveredPlatforms": ["macos", "windows"],
|
||||
"coveredProviders": ["local"],
|
||||
"coverageNotes": "Source-built macOS and physical Windows Electron journeys cover the public CLI through RPC, runtime identity, real PTY/ConPTY writes, and a delayed fake Codex composer. Focused units cover CLI intent, transient wrapper resolution, fresh foreground identity over stale launch metadata, paired-mobile floor takeover before delayed Enter, framing bytes, stale-agent shell fallback, and exact legacy behavior for every other configured agent. SSH, daemon, and remote-runtime live evidence remain explicit gaps.",
|
||||
"coveredPlatforms": ["macos", "linux", "windows"],
|
||||
"coveredProviders": ["local", "ssh"],
|
||||
"coverageNotes": "Source-built macOS and physical Windows Electron journeys cover the public CLI through RPC, runtime identity, real PTY/ConPTY writes, and a delayed fake Codex composer. A real encrypted mobile subscription proves desktop-to-mobile floor takeover with exact fake-agent stdin, and an ephemeral Linux Docker SSH target proves half-open relay process inspection, transient wrapper retry, exact remote input, SSH execution ownership, and unrelated-PTY survival. Daemon and paired remote-runtime live evidence remain explicit gaps.",
|
||||
"motivatingLinks": [
|
||||
"https://linear.app/stably/issue/STA-4328",
|
||||
"https://linear.app/stably/issue/STA-4486",
|
||||
@@ -9426,6 +9426,7 @@
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"pnpm.cmd exec playwright test tests/e2e/terminal-send-agent-prompt-submit.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1",
|
||||
"node tests/tools/repro-terminal-send-submit.mjs --cli orca --worktree <registered-worktree> --discard-report"
|
||||
],
|
||||
@@ -9436,6 +9437,7 @@
|
||||
"src/main/runtime/orca-runtime.test.ts",
|
||||
"src/shared/agent-prompt-injection.test.ts",
|
||||
"tests/e2e/terminal-send-agent-prompt-submit.spec.ts",
|
||||
"tests/e2e/ssh-docker-agent-prompt-submit.spec.ts",
|
||||
"tests/tools/repro-terminal-send-submit.mjs"
|
||||
],
|
||||
"assertionRefs": [
|
||||
@@ -9470,7 +9472,16 @@
|
||||
"assertions": [
|
||||
"the public source-built CLI submits after a delayed composer render without rescue",
|
||||
"the fake agent receives the marker and zero premature Enter on macOS and Windows",
|
||||
"POSIX preserves one bracketed-paste frame while Windows validates the ConPTY-observable payload"
|
||||
"POSIX preserves one bracketed-paste frame while Windows validates the ConPTY-observable payload",
|
||||
"a real paired mobile floor takeover blocks delayed desktop Enter with exact PTY input"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "tests/e2e/ssh-docker-agent-prompt-submit.spec.ts",
|
||||
"assertions": [
|
||||
"two half-open SSH relay wrapper inspections precede a bounded retry that identifies Codex",
|
||||
"the remote fake agent receives one post-render Enter and no premature Enter",
|
||||
"execution stays on the SSH host and an unrelated remote PTY remains writable"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -9519,19 +9530,37 @@
|
||||
"result": "passed",
|
||||
"durationSeconds": 12.9,
|
||||
"summary": "The 1,227-test candidate gate passed with one existing skip. Its three controlled oracles also failed on origin/main and with only the candidate production hunks disabled, then passed together on the candidate: node-to-Codex wrapper settlement, current foreground over stale launch identity, and paired-mobile floor takeover before delayed Enter."
|
||||
},
|
||||
{
|
||||
"date": "2026-08-20",
|
||||
"runner": "local",
|
||||
"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.0,
|
||||
"summary": "All four source-built Electron prompt journeys passed. The encrypted paired-mobile case recorded desktop then mobile authority, prompt and mobile bytes at fake-agent stdin, no carriage return, and an accepted=false desktop result after takeover."
|
||||
},
|
||||
{
|
||||
"date": "2026-08-20",
|
||||
"runner": "local",
|
||||
"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": 31.3,
|
||||
"summary": "The candidate passed against an ephemeral Debian SSH target and real relay with two controlled half-open process reads, bounded wrapper retry, one post-render remote Enter, SSH execution-host proof, and an unrelated live remote PTY. The byte-identical oracle failed when only the settled-wrapper fix was disabled because the send settled through legacy delivery before the retry barrier."
|
||||
}
|
||||
],
|
||||
"runtimeBudget": {
|
||||
"p95Seconds": 90,
|
||||
"scope": "focused units plus one isolated Electron CLI/PTY journey"
|
||||
"p95Seconds": 180,
|
||||
"scope": "focused units, isolated Electron CLI/paired-mobile journeys, and one Docker SSH relay journey"
|
||||
},
|
||||
"flakeHistory": {
|
||||
"status": "unknown",
|
||||
"evidence": "The final oracle passed twice in independent macOS app launches and twice on physical Windows. A shared --repeat-each fixture mode failed before test execution because only one seeded worktree loaded; supported isolated invocations were used for product evidence. CI history is not yet collected."
|
||||
},
|
||||
"redGreenEvidence": {
|
||||
"status": "partial",
|
||||
"evidence": "The fake-composer oracle was red against the installed pre-fix runtime with prematureEnters=1 and rescueSent=true. It also caught a source-built late-identity race with a complete paste frame but one premature Enter before the render gate began resolving foreground identity. The final candidate is green on macOS and Windows with prematureEnters=0 and rescueSent=false."
|
||||
"status": "complete",
|
||||
"evidence": "The same controlled unit oracles are red on origin/main and with each production fix disabled, then green together on the candidate. The paired-mobile Electron oracle is green on the candidate, while its byte-identical controlled floor-takeover oracle is red without the delayed authority recheck. The Docker SSH oracle is green on the candidate and red with only settled-wrapper retry disabled, where legacy delivery settles before the retry barrier. Existing macOS and Windows fake-composer evidence remains green with prematureEnters=0 and rescueSent=false."
|
||||
},
|
||||
"performanceBudget": {
|
||||
"required": true,
|
||||
@@ -9542,7 +9571,7 @@
|
||||
"Retain mixed-version evidence that old hosts strip the optional intent and new hosts accept old clients without a protocol bump."
|
||||
],
|
||||
"knownGaps": [
|
||||
"Live SSH/daemon/remote-runtime evidence is not yet attached.",
|
||||
"Live daemon and paired remote-runtime evidence is not yet attached; SSH relay and paired mobile-to-local-runtime evidence are covered.",
|
||||
"Only Claude and Codex opt into settlement; every other agent retains the legacy behavior until independently validated.",
|
||||
"A new CLI talking to an old host safely loses the optional field and therefore keeps the old timing behavior until the host updates.",
|
||||
"The E2E fake agent models the submission race without requiring external agent authentication; live account-backed agents are covered separately."
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
import { execFile } from 'node:child_process'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { promisify } from 'node:util'
|
||||
import type { Page } from '@stablyai/playwright-test'
|
||||
import { expect, test } from './helpers/orca-app'
|
||||
import { waitForActiveWorktree, waitForSessionReady } from './helpers/store'
|
||||
import {
|
||||
cleanupDockerSshRelayTarget,
|
||||
execDockerSshRelayTargetControlCommand,
|
||||
shellQuote,
|
||||
startDockerSshRelayTarget,
|
||||
type DockerSshRelayTarget,
|
||||
writeDockerSshRelayTargetFile
|
||||
} from './helpers/docker-ssh-relay-target'
|
||||
import { connectDockerSshRelayTarget } from './helpers/docker-ssh-relay-connection'
|
||||
|
||||
const RUN_DOCKER_SSH = process.env.ORCA_E2E_SSH_DOCKER === '1'
|
||||
const REMOTE_AGENT = '/tmp/codex'
|
||||
const REMOTE_INPUT = '/tmp/orca-codex-prompt-input.bin'
|
||||
const REMOTE_REPORT = '/tmp/orca-codex-prompt-report.json'
|
||||
const PS_OBSERVE = '/tmp/orca-prompt-ps-observe'
|
||||
const PS_FAIL = '/tmp/orca-prompt-ps-fail-once'
|
||||
const PS_ENTERED = '/tmp/orca-prompt-ps-entered'
|
||||
const PS_RELEASE = '/tmp/orca-prompt-ps-release'
|
||||
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 execFileAsync = promisify(execFile)
|
||||
|
||||
type RuntimeTerminalSummary = {
|
||||
handle: string
|
||||
ptyId: string | null
|
||||
executionHostId?: string
|
||||
}
|
||||
|
||||
async function callRuntime<TResult>(page: Page, method: string, params: unknown): Promise<TResult> {
|
||||
return page.evaluate(
|
||||
async ({ method, params }) => {
|
||||
const response = await window.api.runtime.call({ method, params })
|
||||
if (!response.ok) {
|
||||
throw new Error(`${response.error.code}: ${response.error.message}`)
|
||||
}
|
||||
return response.result
|
||||
},
|
||||
{ method, params }
|
||||
) as Promise<TResult>
|
||||
}
|
||||
|
||||
async function createRemoteTerminal(
|
||||
page: Page,
|
||||
worktreeId: string,
|
||||
command: string,
|
||||
title: string
|
||||
): Promise<RuntimeTerminalSummary> {
|
||||
const result = await callRuntime<{ terminal: RuntimeTerminalSummary }>(page, 'terminal.create', {
|
||||
worktree: `id:${worktreeId}`,
|
||||
command,
|
||||
title,
|
||||
presentation: 'background'
|
||||
})
|
||||
if (!result.terminal.ptyId) {
|
||||
throw new Error(`Remote terminal ${result.terminal.handle} has no PTY`)
|
||||
}
|
||||
return result.terminal
|
||||
}
|
||||
|
||||
function installControlledPs(target: DockerSshRelayTarget): void {
|
||||
const script = `#!/usr/bin/env bash
|
||||
if [ -e ${PS_OBSERVE} ]; then
|
||||
count=0
|
||||
[ ! -r ${PS_COUNT} ] || count="$(cat ${PS_COUNT})"
|
||||
printf '%s' "$((count + 1))" > ${PS_COUNT}
|
||||
if rm -f ${PS_FAIL} 2>/dev/null && [ ! -e ${PS_ENTERED} ]; then
|
||||
printf entered > ${PS_ENTERED}
|
||||
while [ ! -e ${PS_RELEASE} ]; do sleep 0.02; done
|
||||
exit 1
|
||||
fi
|
||||
if rm -f ${PS_RETRY} 2>/dev/null && [ ! -e ${PS_RETRY_ENTERED} ]; then
|
||||
printf retry > ${PS_RETRY_ENTERED}
|
||||
while [ ! -e ${PS_RETRY_RELEASE} ]; do sleep 0.02; done
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
exec /usr/bin/ps "$@"
|
||||
`
|
||||
writeDockerSshRelayTargetFile(target, '/usr/local/bin/ps', script)
|
||||
execDockerSshRelayTargetControlCommand(target, 'chmod 755 /usr/local/bin/ps')
|
||||
}
|
||||
|
||||
function prepareRemoteAgent(target: DockerSshRelayTarget): void {
|
||||
const fixture = readFileSync(
|
||||
path.join(process.cwd(), 'tests', 'tools', 'repro-terminal-send-submit.mjs'),
|
||||
'utf8'
|
||||
)
|
||||
writeDockerSshRelayTargetFile(target, REMOTE_AGENT, fixture)
|
||||
installControlledPs(target)
|
||||
}
|
||||
|
||||
function remoteFile(target: DockerSshRelayTarget, filePath: string): string {
|
||||
try {
|
||||
return execDockerSshRelayTargetControlCommand(
|
||||
target,
|
||||
`test -r ${shellQuote(filePath)} && cat ${shellQuote(filePath)}`
|
||||
)
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
function remoteInput(target: DockerSshRelayTarget): string {
|
||||
try {
|
||||
const encoded = execDockerSshRelayTargetControlCommand(
|
||||
target,
|
||||
`test -r ${shellQuote(REMOTE_INPUT)} && base64 -w0 ${shellQuote(REMOTE_INPUT)}`
|
||||
)
|
||||
return Buffer.from(encoded, 'base64').toString('utf8')
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
function armHalfOpenInspection(target: DockerSshRelayTarget): void {
|
||||
execDockerSshRelayTargetControlCommand(
|
||||
target,
|
||||
`rm -f ${REMOTE_INPUT} ${REMOTE_REPORT} ${PS_ENTERED} ${PS_RELEASE} ${PS_RETRY_ENTERED} ${PS_RETRY_RELEASE} ${PS_COUNT}; ` +
|
||||
`: > ${PS_OBSERVE}; : > ${PS_FAIL}; : > ${PS_RETRY}`
|
||||
)
|
||||
}
|
||||
|
||||
async function waitForRemoteSignal(
|
||||
target: DockerSshRelayTarget,
|
||||
filePath: string
|
||||
): Promise<string> {
|
||||
const result = await execFileAsync(
|
||||
'docker',
|
||||
[
|
||||
'exec',
|
||||
target.containerName,
|
||||
'bash',
|
||||
'--noprofile',
|
||||
'--norc',
|
||||
'-c',
|
||||
`while [ ! -r ${shellQuote(filePath)} ]; do sleep 0.02; done; cat ${shellQuote(filePath)}`
|
||||
],
|
||||
{ encoding: 'utf8', timeout: 10_000 }
|
||||
)
|
||||
return result.stdout.trim()
|
||||
}
|
||||
|
||||
test.describe('Docker SSH settled prompt submission', () => {
|
||||
test.skip(!RUN_DOCKER_SSH, 'Set ORCA_E2E_SSH_DOCKER=1 to run Docker SSH relay tests.')
|
||||
test.skip(process.platform === 'win32', 'Docker SSH relay tests use POSIX ssh tooling.')
|
||||
|
||||
test('waits through a half-open wrapper inspection before one remote Enter', async ({
|
||||
orcaPage
|
||||
}, testInfo) => {
|
||||
test.setTimeout(180_000)
|
||||
let target: DockerSshRelayTarget | null = null
|
||||
const terminalHandles: string[] = []
|
||||
try {
|
||||
target = startDockerSshRelayTarget(testInfo)
|
||||
prepareRemoteAgent(target)
|
||||
await waitForSessionReady(orcaPage)
|
||||
await waitForActiveWorktree(orcaPage)
|
||||
const remote = await connectDockerSshRelayTarget(orcaPage, target)
|
||||
const marker = `SSH_SETTLED_PROMPT_${Date.now()}`
|
||||
const survivor = await createRemoteTerminal(
|
||||
orcaPage,
|
||||
remote.worktreeId,
|
||||
'bash',
|
||||
'unrelated remote shell'
|
||||
)
|
||||
terminalHandles.push(survivor.handle)
|
||||
const terminal = await createRemoteTerminal(
|
||||
orcaPage,
|
||||
remote.worktreeId,
|
||||
`node ${REMOTE_AGENT} --fake-agent --report ${REMOTE_REPORT} --marker ${marker} --input-observation ${REMOTE_INPUT} --timeout-ms 60000`,
|
||||
'remote prompt fixture'
|
||||
)
|
||||
terminalHandles.push(terminal.handle)
|
||||
expect(terminal.ptyId).not.toBe(survivor.ptyId)
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const read = await callRuntime<{ terminal: { tail: string[] } }>(
|
||||
orcaPage,
|
||||
'terminal.read',
|
||||
{ terminal: terminal.handle }
|
||||
)
|
||||
return read.terminal.tail.join('\n')
|
||||
})
|
||||
.toContain('OpenAI Codex')
|
||||
expect(terminal.executionHostId).toBe(`ssh:${encodeURIComponent(remote.targetId)}`)
|
||||
|
||||
armHalfOpenInspection(target)
|
||||
const inspectionEntered = waitForRemoteSignal(target, PS_ENTERED)
|
||||
const inspection = callRuntime(orcaPage, 'terminal.inspectProcess', {
|
||||
terminal: terminal.handle
|
||||
})
|
||||
await expect(inspectionEntered).resolves.toBe('entered')
|
||||
const prompt = `${marker} remote wrapper payload`
|
||||
let sendSettled = false
|
||||
const send = callRuntime<{ send: { accepted: boolean; bytesWritten: number } }>(
|
||||
orcaPage,
|
||||
'terminal.send',
|
||||
{
|
||||
terminal: terminal.handle,
|
||||
text: prompt,
|
||||
enter: true,
|
||||
agentPrompt: true,
|
||||
client: { id: 'docker-ssh-e2e', type: 'desktop' }
|
||||
}
|
||||
).finally(() => {
|
||||
sendSettled = true
|
||||
})
|
||||
|
||||
expect(sendSettled).toBe(false)
|
||||
expect(remoteInput(target)).toBe('')
|
||||
execDockerSshRelayTargetControlCommand(target, `: > ${PS_RELEASE}`)
|
||||
await expect(inspection).resolves.toBeTruthy()
|
||||
await expect(waitForRemoteSignal(target, PS_RETRY_ENTERED)).resolves.toBe('retry')
|
||||
expect(remoteInput(target)).toBe('')
|
||||
expect(sendSettled).toBe(false)
|
||||
execDockerSshRelayTargetControlCommand(target, `: > ${PS_RETRY_RELEASE}`)
|
||||
|
||||
await expect(send).resolves.toMatchObject({ send: { accepted: true } })
|
||||
await expect.poll(() => remoteFile(target!, REMOTE_REPORT)).not.toBe('')
|
||||
const observedInput = remoteInput(target)
|
||||
expect(observedInput).toContain(prompt)
|
||||
expect(observedInput.match(/\r/g)).toHaveLength(1)
|
||||
expect(JSON.parse(remoteFile(target, REMOTE_REPORT))).toMatchObject({
|
||||
contractOk: true,
|
||||
submitted: true,
|
||||
prematureEnters: 0,
|
||||
receivedEnters: 1,
|
||||
markerReceived: true
|
||||
})
|
||||
expect(Number(remoteFile(target, PS_COUNT))).toBeGreaterThanOrEqual(2)
|
||||
|
||||
const survivorMarker = `SSH_UNRELATED_SURVIVES_${Date.now()}`
|
||||
await callRuntime(orcaPage, 'terminal.send', {
|
||||
terminal: survivor.handle,
|
||||
text: `printf '${survivorMarker}\\n'`,
|
||||
enter: true,
|
||||
client: { id: 'docker-ssh-e2e', type: 'desktop' }
|
||||
})
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const read = await callRuntime<{ terminal: { tail: string[] } }>(
|
||||
orcaPage,
|
||||
'terminal.read',
|
||||
{ terminal: survivor.handle }
|
||||
)
|
||||
return read.terminal.tail.join('\n')
|
||||
})
|
||||
.toContain(survivorMarker)
|
||||
|
||||
testInfo.annotations.push({
|
||||
type: 'docker-ssh-settled-prompt',
|
||||
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) {
|
||||
execDockerSshRelayTargetControlCommand(
|
||||
target,
|
||||
`rm -f ${PS_OBSERVE} ${PS_FAIL} ${PS_RELEASE} ${PS_RETRY} ${PS_RETRY_RELEASE}`
|
||||
)
|
||||
}
|
||||
cleanupDockerSshRelayTarget(target)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1,37 +1,48 @@
|
||||
import { execFile } from 'node:child_process'
|
||||
import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
import { promisify } from 'node:util'
|
||||
import type { Page } from '@stablyai/playwright-test'
|
||||
import { expect, test } from './helpers/orca-app'
|
||||
import { waitForSessionReady } from './helpers/store'
|
||||
import { parsePairingCode } from '../../src/shared/pairing'
|
||||
import {
|
||||
sendRemoteRuntimeRequest,
|
||||
subscribeRemoteRuntimeRequest
|
||||
} from '../../src/shared/remote-runtime-client'
|
||||
|
||||
const execFileAsync = promisify(execFile)
|
||||
const fixtureRoot = mkdtempSync(path.join(os.tmpdir(), 'orca-terminal-send-agent-prompt-'))
|
||||
const fixtureBin = path.join(fixtureRoot, 'bin')
|
||||
const fixtureReport = path.join(fixtureRoot, 'report.json')
|
||||
const fixtureInputObservation = path.join(fixtureRoot, 'input.bin')
|
||||
const fixtureMarker = `ORCA_TERMINAL_SEND_E2E_${process.pid}`
|
||||
const fixtureScript = path.join(process.cwd(), 'tests', 'tools', 'repro-terminal-send-submit.mjs')
|
||||
const fakeCodex = path.join(fixtureBin, process.platform === 'win32' ? 'codex.cmd' : 'codex')
|
||||
|
||||
mkdirSync(fixtureBin)
|
||||
writeFileSync(
|
||||
fakeCodex,
|
||||
process.platform === 'win32'
|
||||
? `@echo off\r\n"${process.execPath}" "${fixtureScript}" --fake-agent --report "%ORCA_FAKE_AGENT_REPORT%" --marker "%ORCA_FAKE_AGENT_MARKER%" --allow-unframed-paste %*\r\n`
|
||||
: `#!/usr/bin/env sh\nexec "${process.execPath}" "${fixtureScript}" --fake-agent --report "$ORCA_FAKE_AGENT_REPORT" --marker "$ORCA_FAKE_AGENT_MARKER" "$@"\n`,
|
||||
'utf8'
|
||||
)
|
||||
if (process.platform !== 'win32') {
|
||||
chmodSync(fakeCodex, 0o755)
|
||||
if (process.platform === 'win32') {
|
||||
writeFileSync(
|
||||
fakeCodex,
|
||||
`@echo off\r\n"${process.execPath}" "${fixtureScript}" --fake-agent --report "%ORCA_FAKE_AGENT_REPORT%" --marker "%ORCA_FAKE_AGENT_MARKER%" --input-observation "%ORCA_FAKE_AGENT_INPUT_OBSERVATION%" --allow-unframed-paste %*\r\n`,
|
||||
'utf8'
|
||||
)
|
||||
} else {
|
||||
symlinkSync(process.execPath, fakeCodex)
|
||||
}
|
||||
const fakeCodexCommand =
|
||||
process.platform === 'win32'
|
||||
? `"${fakeCodex}"`
|
||||
: `${shellQuote(fakeCodex)} ${shellQuote(fixtureScript)} --fake-agent --report ${shellQuote(fixtureReport)} --marker ${shellQuote(fixtureMarker)} --input-observation ${shellQuote(fixtureInputObservation)}`
|
||||
|
||||
test.use({
|
||||
seedTestRepo: false,
|
||||
orcaAppExtraEnv: {
|
||||
PATH: `${fixtureBin}${path.delimiter}${process.env.PATH ?? ''}`,
|
||||
ORCA_FAKE_AGENT_REPORT: fixtureReport,
|
||||
ORCA_FAKE_AGENT_MARKER: fixtureMarker
|
||||
ORCA_FAKE_AGENT_MARKER: fixtureMarker,
|
||||
ORCA_FAKE_AGENT_INPUT_OBSERVATION: fixtureInputObservation
|
||||
}
|
||||
})
|
||||
|
||||
@@ -59,7 +70,7 @@ test('CLI text plus Enter waits for a slow agent composer before submitting', as
|
||||
'--worktree',
|
||||
testRepoPath,
|
||||
'--agent-command',
|
||||
'codex',
|
||||
fakeCodexCommand,
|
||||
'--report',
|
||||
fixtureReport,
|
||||
'--marker',
|
||||
@@ -109,7 +120,7 @@ test('CLI reports a swallowed Enter without submitting a second Enter', async ({
|
||||
'--worktree',
|
||||
testRepoPath,
|
||||
'--agent-command',
|
||||
'codex --swallow-first-enter',
|
||||
`${fakeCodexCommand} --swallow-first-enter`,
|
||||
'--expect-stalled',
|
||||
'--report',
|
||||
fixtureReport,
|
||||
@@ -189,3 +200,251 @@ test('CLI does not write prompt bytes into an active permission dialog', async (
|
||||
markerReceived: false
|
||||
})
|
||||
})
|
||||
|
||||
test('paired mobile floor takeover blocks the CLI delayed Enter', async ({
|
||||
electronApp,
|
||||
orcaPage,
|
||||
testRepoPath
|
||||
}) => {
|
||||
test.setTimeout(90_000)
|
||||
await waitForSessionReady(orcaPage)
|
||||
const userDataDir = await electronApp.evaluate(({ app }) => app.getPath('userData'))
|
||||
const marker = fixtureMarker
|
||||
rmSync(fixtureReport, { force: true })
|
||||
rmSync(fixtureInputObservation, { force: true })
|
||||
const worktreeSelector = await resolveTestWorktreeSelector(testRepoPath, userDataDir)
|
||||
const created = await callDevCli(
|
||||
[
|
||||
'terminal',
|
||||
'create',
|
||||
'--worktree',
|
||||
worktreeSelector,
|
||||
'--title',
|
||||
'paired mobile prompt authority',
|
||||
'--command',
|
||||
`${fakeCodexCommand} --timeout-ms 60000`
|
||||
],
|
||||
testRepoPath,
|
||||
userDataDir
|
||||
)
|
||||
const terminal = created.terminal as { handle: string }
|
||||
const shown = await callDevCli(
|
||||
['terminal', 'show', '--terminal', terminal.handle],
|
||||
testRepoPath,
|
||||
userDataDir
|
||||
)
|
||||
const ptyId = (shown.terminal as { ptyId: string }).ptyId
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const ready = await callDevCli(
|
||||
['terminal', 'show', '--terminal', terminal.handle],
|
||||
testRepoPath,
|
||||
userDataDir
|
||||
)
|
||||
return (ready.terminal as { preview?: string }).preview
|
||||
})
|
||||
.toContain('OpenAI Codex')
|
||||
|
||||
const pairingResult = await orcaPage.evaluate(() =>
|
||||
window.api.mobile.getPairingQR({
|
||||
address: '127.0.0.1',
|
||||
connectionMode: 'local-only',
|
||||
rotate: true
|
||||
})
|
||||
)
|
||||
expect(pairingResult.available).toBe(true)
|
||||
if (!pairingResult.available) {
|
||||
throw new Error('Mobile pairing unavailable')
|
||||
}
|
||||
const pairing = parsePairingCode(pairingResult.pairingUrl)
|
||||
expect(pairing).toBeTruthy()
|
||||
if (!pairing) {
|
||||
throw new Error('Mobile pairing URL did not parse')
|
||||
}
|
||||
|
||||
const responses: Record<string, unknown>[] = []
|
||||
const subscription = await subscribeRemoteRuntimeRequest(
|
||||
pairing,
|
||||
'terminal.subscribe',
|
||||
{
|
||||
terminal: terminal.handle,
|
||||
client: { id: pairing.deviceToken, type: 'mobile' },
|
||||
viewport: { cols: 45, rows: 20 },
|
||||
capabilities: { terminalBinaryStream: 1 }
|
||||
},
|
||||
15_000,
|
||||
{
|
||||
onResponse: (response) => responses.push(response as unknown as Record<string, unknown>),
|
||||
onError: (error) => {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
try {
|
||||
await expect
|
||||
.poll(() =>
|
||||
responses.some(
|
||||
(response) => (response.result as { type?: string } | undefined)?.type === 'subscribed'
|
||||
)
|
||||
)
|
||||
.toBe(true)
|
||||
await expectTerminalDriver(orcaPage, ptyId, 'mobile')
|
||||
await expect(
|
||||
orcaPage.evaluate((id) => window.api.runtime.restoreTerminalFit(id), ptyId)
|
||||
).resolves.toEqual({ restored: true })
|
||||
await expectTerminalDriver(orcaPage, ptyId, 'desktop')
|
||||
const beforeSend = await callDevCli(
|
||||
['terminal', 'show', '--terminal', terminal.handle],
|
||||
testRepoPath,
|
||||
userDataDir
|
||||
)
|
||||
expect(beforeSend.terminal).toMatchObject({ connected: true, writable: true, ptyId })
|
||||
|
||||
const prompt = `${marker} paired owner payload`
|
||||
let desktopSendOutcome: unknown
|
||||
const desktopSend = callDevCli(
|
||||
['terminal', 'send', '--terminal', terminal.handle, '--text', prompt, '--enter'],
|
||||
testRepoPath,
|
||||
userDataDir
|
||||
)
|
||||
void desktopSend.then(
|
||||
(value) => {
|
||||
desktopSendOutcome = value
|
||||
},
|
||||
(error) => {
|
||||
desktopSendOutcome = error
|
||||
}
|
||||
)
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
readObservedInput(fixtureInputObservation).includes(marker) ||
|
||||
desktopSendOutcome !== undefined
|
||||
)
|
||||
.toBe(true)
|
||||
if (!readObservedInput(fixtureInputObservation).includes(marker)) {
|
||||
const drivers = await orcaPage.evaluate(() => window.api.runtime.getTerminalDrivers())
|
||||
throw new Error(
|
||||
`Desktop send settled before paste: ${JSON.stringify(desktopSendOutcome)}; drivers=${JSON.stringify(drivers)}`
|
||||
)
|
||||
}
|
||||
|
||||
const mobileSend = await sendRemoteRuntimeRequest<{
|
||||
send: { accepted: boolean; bytesWritten: number }
|
||||
}>(
|
||||
pairing,
|
||||
'terminal.send',
|
||||
{
|
||||
terminal: terminal.handle,
|
||||
text: 'mobile-owner-input',
|
||||
client: { id: pairing.deviceToken, type: 'mobile' }
|
||||
},
|
||||
15_000
|
||||
)
|
||||
expect(mobileSend.ok).toBe(true)
|
||||
if (!mobileSend.ok) {
|
||||
throw new Error(mobileSend.error.message)
|
||||
}
|
||||
expect(mobileSend).toMatchObject({
|
||||
ok: true,
|
||||
result: { send: { accepted: true } }
|
||||
})
|
||||
await expectTerminalDriver(orcaPage, ptyId, 'mobile')
|
||||
|
||||
await expect(desktopSend).resolves.toMatchObject({
|
||||
send: { accepted: false, bytesWritten: 0 }
|
||||
})
|
||||
const observedInput = readObservedInput(fixtureInputObservation)
|
||||
expect(observedInput).toContain(marker)
|
||||
expect(observedInput).toContain('mobile-owner-input')
|
||||
expect(observedInput).not.toContain('\r')
|
||||
} finally {
|
||||
subscription.close()
|
||||
await callDevCli(
|
||||
['terminal', 'close', '--terminal', terminal.handle],
|
||||
testRepoPath,
|
||||
userDataDir
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
async function resolveTestWorktreeSelector(
|
||||
testRepoPath: string,
|
||||
userDataDir: string
|
||||
): Promise<string> {
|
||||
const added = await callDevCli(['repo', 'add', '--path', testRepoPath], testRepoPath, userDataDir)
|
||||
const repoId = (added.repo as { id: string }).id
|
||||
const deadline = Date.now() + 10_000
|
||||
while (Date.now() < deadline) {
|
||||
const listed = await callDevCli(
|
||||
['worktree', 'list', '--repo', `id:${repoId}`],
|
||||
testRepoPath,
|
||||
userDataDir
|
||||
)
|
||||
const worktree = (listed.worktrees as { id: string; path: string }[]).find(
|
||||
(candidate) => path.resolve(candidate.path) === path.resolve(testRepoPath)
|
||||
)
|
||||
if (worktree) {
|
||||
return `id:${worktree.id}`
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
}
|
||||
throw new Error(`Test worktree did not materialize for ${repoId}`)
|
||||
}
|
||||
|
||||
async function callDevCli(
|
||||
args: string[],
|
||||
cwd: string,
|
||||
userDataDir: string
|
||||
): Promise<Record<string, unknown>> {
|
||||
let result: { stdout: string; stderr: string }
|
||||
try {
|
||||
result = await execFileAsync(
|
||||
process.execPath,
|
||||
[path.join(process.cwd(), 'config', 'scripts', 'orca-dev.mjs'), ...args, '--json'],
|
||||
{
|
||||
cwd,
|
||||
env: { ...process.env, ORCA_DEV_USER_DATA_PATH: userDataDir },
|
||||
timeout: 60_000
|
||||
}
|
||||
)
|
||||
} catch (error) {
|
||||
const failed = error as Error & { stdout?: string; stderr?: string }
|
||||
throw new Error([failed.message, failed.stdout, failed.stderr].filter(Boolean).join('\n'))
|
||||
}
|
||||
const response = JSON.parse(result.stdout) as {
|
||||
ok: boolean
|
||||
result?: Record<string, unknown>
|
||||
error?: { message?: string }
|
||||
}
|
||||
if (!response.ok || !response.result) {
|
||||
throw new Error(response.error?.message ?? 'Orca CLI request failed')
|
||||
}
|
||||
return response.result
|
||||
}
|
||||
|
||||
async function expectTerminalDriver(
|
||||
page: Page,
|
||||
ptyId: string,
|
||||
kind: 'desktop' | 'mobile'
|
||||
): Promise<void> {
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const drivers = await page.evaluate(() => window.api.runtime.getTerminalDrivers())
|
||||
return drivers.find((entry) => entry.ptyId === ptyId)?.driver.kind
|
||||
})
|
||||
.toBe(kind)
|
||||
}
|
||||
|
||||
function shellQuote(value: string): string {
|
||||
return `'${value.replaceAll("'", "'\\''")}'`
|
||||
}
|
||||
|
||||
function readObservedInput(filePath: string): string {
|
||||
try {
|
||||
return readFileSync(filePath, 'utf8')
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
import { spawn } from 'node:child_process'
|
||||
import { appendFileSync } from 'node:fs'
|
||||
import { mkdir, readFile, rm, symlink, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import path from 'node:path'
|
||||
@@ -280,6 +281,7 @@ async function parentMain() {
|
||||
async function fakeAgentMain() {
|
||||
const reportPath = argValue('report')
|
||||
const marker = argValue('marker')
|
||||
const inputObservationPath = argValue('input-observation')
|
||||
const timeoutMs = parsePositiveInteger('timeout-ms', DEFAULT_TIMEOUT_MS)
|
||||
const pasteFramingRequired = !hasFlag('allow-unframed-paste')
|
||||
const swallowFirstEnter = hasFlag('swallow-first-enter')
|
||||
@@ -339,6 +341,9 @@ async function fakeAgentMain() {
|
||||
setTimeout(() => void writeReport(false), 250)
|
||||
}
|
||||
process.stdin.on('data', (chunk) => {
|
||||
if (inputObservationPath) {
|
||||
appendFileSync(inputObservationPath, chunk)
|
||||
}
|
||||
input += chunk.toString('utf8')
|
||||
if (!renderScheduled && input.includes(marker)) {
|
||||
renderScheduled = true
|
||||
@@ -370,6 +375,7 @@ async function fakeAgentMain() {
|
||||
return
|
||||
}
|
||||
prematureEnters += 1
|
||||
void writeReport(false)
|
||||
nextCarriage = input.indexOf('\r', countedCarriages)
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user