mirror of
https://github.com/stablyai/orca.git
synced 2026-10-24 00:04:01 +00:00
test(orchestration): stabilize agent prompt submit e2e
This commit is contained in:
@@ -1,46 +1,88 @@
|
||||
import { execFile } from 'node:child_process'
|
||||
import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { chmodSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
import { promisify } from 'node:util'
|
||||
import { expect, test } from './helpers/orca-app'
|
||||
import { buildFakeAgentCommandOverride } from './helpers/fake-agent-command-override'
|
||||
import { waitForSessionReady } from './helpers/store'
|
||||
import { RuntimeClient } from '../../src/cli/runtime-client'
|
||||
import { recognizeAgentProcess } from '../../src/shared/agent-process-recognition'
|
||||
import { SWALLOWED_ENTER_FIXTURE_TIMEOUT_MS } from '../../src/shared/orchestration-timing-budgets'
|
||||
|
||||
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 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')
|
||||
const fakeCodex = path.join(fixtureRoot, process.platform === 'win32' ? 'codex.cmd' : 'codex')
|
||||
const fakeCodexCommand = buildFakeAgentCommandOverride(fakeCodex)
|
||||
const swallowedEnterFixtureTimeoutMs = SWALLOWED_ENTER_FIXTURE_TIMEOUT_MS
|
||||
|
||||
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`,
|
||||
: `#!/usr/bin/env sh\n"${process.execPath}" "${fixtureScript}" --fake-agent --report "$ORCA_FAKE_AGENT_REPORT" --marker "$ORCA_FAKE_AGENT_MARKER" "$@"\n`,
|
||||
'utf8'
|
||||
)
|
||||
if (process.platform !== 'win32') {
|
||||
chmodSync(fakeCodex, 0o755)
|
||||
}
|
||||
|
||||
test.use({
|
||||
seedTestRepo: false,
|
||||
orcaAppExtraEnv: {
|
||||
PATH: `${fixtureBin}${path.delimiter}${process.env.PATH ?? ''}`,
|
||||
ORCA_FAKE_AGENT_REPORT: fixtureReport,
|
||||
ORCA_FAKE_AGENT_MARKER: fixtureMarker
|
||||
}
|
||||
})
|
||||
|
||||
test.afterAll(() => {
|
||||
rmSync(fixtureRoot, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
async function createFakeCodexTerminal(
|
||||
userDataDir: string,
|
||||
testRepoPath: string,
|
||||
args: string[] = []
|
||||
): Promise<string> {
|
||||
const client = new RuntimeClient(userDataDir, 30_000, null, null)
|
||||
const expectedPath = path.resolve(testRepoPath)
|
||||
const findWorktree = async (): Promise<{ id: string } | undefined> => {
|
||||
const listed = await client.call<{ worktrees: { id: string; path: string }[] }>(
|
||||
'worktree.list',
|
||||
{}
|
||||
)
|
||||
return listed.result.worktrees.find((worktree) => {
|
||||
const candidatePath = path.resolve(worktree.path)
|
||||
return process.platform === 'win32'
|
||||
? candidatePath.toLowerCase() === expectedPath.toLowerCase()
|
||||
: candidatePath === expectedPath
|
||||
})
|
||||
}
|
||||
await expect.poll(async () => Boolean(await findWorktree()), { timeout: 60_000 }).toBe(true)
|
||||
const worktree = await findWorktree()
|
||||
if (!worktree) {
|
||||
throw new Error(`runtime did not register ${testRepoPath}`)
|
||||
}
|
||||
const created = await client.call<{ terminal: { handle: string } }>('terminal.create', {
|
||||
worktree: `id:${worktree.id}`,
|
||||
command: [fakeCodexCommand, ...args].join(' '),
|
||||
launchAgent: 'codex',
|
||||
env: {
|
||||
ORCA_FAKE_AGENT_REPORT: fixtureReport,
|
||||
ORCA_FAKE_AGENT_MARKER: fixtureMarker
|
||||
},
|
||||
title: 'terminal send submit repro'
|
||||
})
|
||||
const handle = created.result.terminal.handle
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const inspected = await client.call<{
|
||||
process: { foregroundProcess: string | null }
|
||||
}>('terminal.inspectProcess', { terminal: handle })
|
||||
return recognizeAgentProcess(inspected.result.process.foregroundProcess)?.agent ?? null
|
||||
},
|
||||
{ timeout: 30_000 }
|
||||
)
|
||||
.toBe('codex')
|
||||
return handle
|
||||
}
|
||||
|
||||
test('CLI text plus Enter waits for a slow agent composer before submitting', async ({
|
||||
electronApp,
|
||||
orcaPage,
|
||||
@@ -49,6 +91,7 @@ test('CLI text plus Enter waits for a slow agent composer before submitting', as
|
||||
test.setTimeout(110_000)
|
||||
await waitForSessionReady(orcaPage)
|
||||
const userDataDir = await electronApp.evaluate(({ app }) => app.getPath('userData'))
|
||||
const terminal = await createFakeCodexTerminal(userDataDir, testRepoPath)
|
||||
const repoRoot = process.cwd()
|
||||
let stdout = ''
|
||||
try {
|
||||
@@ -60,8 +103,8 @@ test('CLI text plus Enter waits for a slow agent composer before submitting', as
|
||||
path.join(repoRoot, 'config', 'scripts', 'orca-dev.mjs'),
|
||||
'--worktree',
|
||||
testRepoPath,
|
||||
'--agent-command',
|
||||
'codex',
|
||||
'--terminal',
|
||||
terminal,
|
||||
'--report',
|
||||
fixtureReport,
|
||||
'--marker',
|
||||
@@ -99,6 +142,11 @@ test('CLI reports a swallowed Enter without submitting a second Enter', async ({
|
||||
test.setTimeout(110_000)
|
||||
await waitForSessionReady(orcaPage)
|
||||
const userDataDir = await electronApp.evaluate(({ app }) => app.getPath('userData'))
|
||||
const terminal = await createFakeCodexTerminal(userDataDir, testRepoPath, [
|
||||
'--swallow-first-enter',
|
||||
'--timeout-ms',
|
||||
String(swallowedEnterFixtureTimeoutMs)
|
||||
])
|
||||
const repoRoot = process.cwd()
|
||||
let stdout = ''
|
||||
try {
|
||||
@@ -110,8 +158,10 @@ test('CLI reports a swallowed Enter without submitting a second Enter', async ({
|
||||
path.join(repoRoot, 'config', 'scripts', 'orca-dev.mjs'),
|
||||
'--worktree',
|
||||
testRepoPath,
|
||||
'--agent-command',
|
||||
`codex --swallow-first-enter --timeout-ms ${swallowedEnterFixtureTimeoutMs}`,
|
||||
'--terminal',
|
||||
terminal,
|
||||
'--timeout-ms',
|
||||
String(swallowedEnterFixtureTimeoutMs),
|
||||
'--expect-stalled',
|
||||
'--report',
|
||||
fixtureReport,
|
||||
@@ -152,6 +202,9 @@ test('CLI does not write prompt bytes into an active permission dialog', async (
|
||||
test.setTimeout(110_000)
|
||||
await waitForSessionReady(orcaPage)
|
||||
const userDataDir = await electronApp.evaluate(({ app }) => app.getPath('userData'))
|
||||
const terminal = await createFakeCodexTerminal(userDataDir, testRepoPath, [
|
||||
'--permission-before-send'
|
||||
])
|
||||
const repoRoot = process.cwd()
|
||||
let stdout = ''
|
||||
try {
|
||||
@@ -163,6 +216,8 @@ test('CLI does not write prompt bytes into an active permission dialog', async (
|
||||
path.join(repoRoot, 'config', 'scripts', 'orca-dev.mjs'),
|
||||
'--worktree',
|
||||
testRepoPath,
|
||||
'--terminal',
|
||||
terminal,
|
||||
'--expect-blocked',
|
||||
'--report',
|
||||
fixtureReport,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
#!/usr/bin/env node
|
||||
import { spawn } from 'node:child_process'
|
||||
import { mkdir, readFile, rm, symlink, writeFile } from 'node:fs/promises'
|
||||
import { chmod, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import path from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
|
||||
const SCRIPT_PATH = import.meta.filename
|
||||
const BEGIN = '\x1b[200~'
|
||||
@@ -118,6 +119,20 @@ async function closeTerminal(cli, handle, cwd) {
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForPermissionPrompt(cli, handle, cwd) {
|
||||
const deadline = Date.now() + 10_000
|
||||
let lastTerminal = null
|
||||
while (Date.now() < deadline) {
|
||||
const shown = await callOrca(cli, ['terminal', 'show', '--terminal', handle], cwd)
|
||||
lastTerminal = shown.terminal ?? null
|
||||
if (lastTerminal?.agentWait) {
|
||||
return
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
}
|
||||
throw new Error(`terminal permission prompt was not observed: ${JSON.stringify(lastTerminal)}`)
|
||||
}
|
||||
|
||||
async function waitForWorktreeSelector(cli, repoId, cwd) {
|
||||
const deadline = Date.now() + 10_000
|
||||
const expectedPath = path.resolve(cwd)
|
||||
@@ -138,18 +153,25 @@ async function waitForWorktreeSelector(cli, repoId, cwd) {
|
||||
}
|
||||
|
||||
async function createFakeCodexCommand(tempDir, args) {
|
||||
const launcherPath = path.join(tempDir, 'codex')
|
||||
const runnerPath = path.join(tempDir, 'fake-agent.mjs')
|
||||
await writeFile(runnerPath, `import ${JSON.stringify(pathToFileURL(SCRIPT_PATH).href)}\n`, 'utf8')
|
||||
if (process.platform === 'win32') {
|
||||
const launcherPath = path.join(tempDir, 'codex.cmd')
|
||||
const commandPath = path.join(tempDir, 'codex.cmd')
|
||||
await writeFile(
|
||||
launcherPath,
|
||||
`@echo off\r\n"${process.execPath}" "${SCRIPT_PATH}" %*\r\n`,
|
||||
commandPath,
|
||||
`@echo off\r\n"${process.execPath}" "%~dp0fake-agent.mjs" %*\r\n`,
|
||||
'utf8'
|
||||
)
|
||||
return [shellQuote(launcherPath), ...args].join(' ')
|
||||
return [shellQuote(commandPath), ...args].join(' ')
|
||||
}
|
||||
const launcherPath = path.join(tempDir, 'codex')
|
||||
await symlink(process.execPath, launcherPath)
|
||||
return [shellQuote(launcherPath), shellQuote(SCRIPT_PATH), ...args].join(' ')
|
||||
await writeFile(
|
||||
launcherPath,
|
||||
`#!/usr/bin/env sh\n"${process.execPath}" "${runnerPath}" "$@"\n`,
|
||||
'utf8'
|
||||
)
|
||||
await chmod(launcherPath, 0o755)
|
||||
return [shellQuote(launcherPath), ...args].join(' ')
|
||||
}
|
||||
|
||||
async function parentMain() {
|
||||
@@ -162,43 +184,48 @@ async function parentMain() {
|
||||
const prompt = `${marker} ${'slow composer payload '.repeat(24)}`
|
||||
const expectStalled = hasFlag('expect-stalled')
|
||||
const expectBlocked = hasFlag('expect-blocked')
|
||||
const providedHandle = argValue('terminal')
|
||||
await mkdir(tempDir, { recursive: true })
|
||||
await rm(reportPath, { force: true })
|
||||
|
||||
const command =
|
||||
argValue('agent-command') ??
|
||||
(await createFakeCodexCommand(tempDir, [
|
||||
'--fake-agent',
|
||||
'--report',
|
||||
shellQuote(reportPath),
|
||||
'--marker',
|
||||
shellQuote(marker),
|
||||
'--timeout-ms',
|
||||
String(timeoutMs),
|
||||
...(expectBlocked ? ['--permission-before-send'] : []),
|
||||
...(process.platform === 'win32' ? ['--allow-unframed-paste'] : [])
|
||||
]))
|
||||
const added = await callOrca(cli, ['repo', 'add', '--path', cwd], cwd)
|
||||
const repoId = added.repo?.id
|
||||
if (!repoId) {
|
||||
throw new Error('repo add returned no id')
|
||||
let handle = providedHandle
|
||||
if (!handle) {
|
||||
const command =
|
||||
argValue('agent-command') ??
|
||||
(await createFakeCodexCommand(tempDir, [
|
||||
'--fake-agent',
|
||||
'--report',
|
||||
shellQuote(reportPath),
|
||||
'--marker',
|
||||
shellQuote(marker),
|
||||
'--timeout-ms',
|
||||
String(timeoutMs),
|
||||
...(expectStalled ? ['--swallow-first-enter'] : []),
|
||||
...(expectBlocked ? ['--permission-before-send'] : []),
|
||||
...(process.platform === 'win32' ? ['--allow-unframed-paste'] : [])
|
||||
]))
|
||||
const added = await callOrca(cli, ['repo', 'add', '--path', cwd], cwd)
|
||||
const repoId = added.repo?.id
|
||||
if (!repoId) {
|
||||
throw new Error('repo add returned no id')
|
||||
}
|
||||
const worktreeSelector = await waitForWorktreeSelector(cli, repoId, cwd)
|
||||
const created = await callOrca(
|
||||
cli,
|
||||
[
|
||||
'terminal',
|
||||
'create',
|
||||
'--worktree',
|
||||
worktreeSelector,
|
||||
'--title',
|
||||
'terminal send submit repro',
|
||||
'--command',
|
||||
command
|
||||
],
|
||||
cwd
|
||||
)
|
||||
handle = created.terminal?.handle
|
||||
}
|
||||
const worktreeSelector = await waitForWorktreeSelector(cli, repoId, cwd)
|
||||
const created = await callOrca(
|
||||
cli,
|
||||
[
|
||||
'terminal',
|
||||
'create',
|
||||
'--worktree',
|
||||
worktreeSelector,
|
||||
'--title',
|
||||
'terminal send submit repro',
|
||||
'--command',
|
||||
command
|
||||
],
|
||||
cwd
|
||||
)
|
||||
const handle = created.terminal?.handle
|
||||
if (!handle) {
|
||||
throw new Error('terminal create returned no handle')
|
||||
}
|
||||
@@ -209,6 +236,7 @@ async function parentMain() {
|
||||
if (!setupReport) {
|
||||
throw new Error('terminal permission prompt did not materialize')
|
||||
}
|
||||
await waitForPermissionPrompt(cli, handle, cwd)
|
||||
} else {
|
||||
await callOrca(
|
||||
cli,
|
||||
@@ -278,6 +306,7 @@ async function parentMain() {
|
||||
}
|
||||
|
||||
async function fakeAgentMain() {
|
||||
process.title = 'codex'
|
||||
const reportPath = argValue('report')
|
||||
const marker = argValue('marker')
|
||||
const timeoutMs = parsePositiveInteger('timeout-ms', DEFAULT_TIMEOUT_MS)
|
||||
@@ -291,10 +320,14 @@ async function fakeAgentMain() {
|
||||
process.stdin.setRawMode(true)
|
||||
}
|
||||
process.stdin.resume()
|
||||
process.stdout.write('OpenAI Codex\nmodel: fake\ndirectory: fixture\n> ')
|
||||
if (permissionBeforeSend) {
|
||||
process.stdout.write('\nPermission required\nAllow once\nAllow always\nReject\n')
|
||||
}
|
||||
process.stdout.write('\x1b]0;Codex working\x07')
|
||||
setTimeout(() => {
|
||||
const title = permissionBeforeSend ? 'Codex permission' : 'Codex Ready'
|
||||
process.stdout.write(`\x1b]0;${title}\x07OpenAI Codex\nmodel: fake\ndirectory: fixture\n> `)
|
||||
if (permissionBeforeSend) {
|
||||
process.stdout.write('\nPermission required\nAllow once\nAllow always\nReject\n')
|
||||
}
|
||||
}, 100)
|
||||
|
||||
let input = ''
|
||||
let countedCarriages = 0
|
||||
|
||||
Reference in New Issue
Block a user