mirror of
https://github.com/stablyai/orca.git
synced 2026-10-22 08:03:37 +00:00
test: add golden e2e tests for agent TUI launch and shell recovery
Add test fixtures and E2E tests to verify agent TUI functionality: - Stub agent implementation supports cross-platform execution (Unix/Windows) - Test verifies multiline composer with Shift+Enter support in agent TUI - Test verifies clean shell resumes after agent exit without state leakage
This commit is contained in:
Executable
+2
@@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
exec "$(dirname "$0")/golden-stub-agent" "$@"
|
||||
@@ -0,0 +1,2 @@
|
||||
@echo off
|
||||
call "%~dp0golden-stub-agent.cmd" %*
|
||||
@@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
exec node "$(dirname "$0")/golden-stub-agent.js" "$@"
|
||||
@@ -0,0 +1,2 @@
|
||||
@echo off
|
||||
node "%~dp0golden-stub-agent.js" %*
|
||||
@@ -0,0 +1,98 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const READY_MARKER = 'GOLDEN_STUB_AGENT_READY'
|
||||
const EXIT_MARKER = 'GOLDEN_STUB_AGENT_EXITED'
|
||||
|
||||
let composer = ''
|
||||
let lastSubmission = ''
|
||||
let pendingInput = ''
|
||||
let exiting = false
|
||||
|
||||
function render() {
|
||||
const lines = composer.split('\n')
|
||||
const renderedComposer = lines.map((line, index) => `${index === 0 ? '> ' : ' '}${line}`)
|
||||
process.stdout.write(
|
||||
`\x1b]0;Golden Stub Agent\x07${[
|
||||
'\x1b[H\x1b[2JGolden Stub Agent',
|
||||
`[${READY_MARKER}]`,
|
||||
'',
|
||||
...renderedComposer,
|
||||
'',
|
||||
'Shift+Enter inserts a newline. Type exit then Enter to quit.',
|
||||
...(lastSubmission ? [`[GOLDEN_STUB_AGENT_SUBMITTED] ${lastSubmission}`] : [])
|
||||
].join('\r\n')}`
|
||||
)
|
||||
}
|
||||
|
||||
function exitCleanly() {
|
||||
if (exiting) {
|
||||
return
|
||||
}
|
||||
exiting = true
|
||||
if (process.stdin.isTTY) {
|
||||
process.stdin.setRawMode(false)
|
||||
}
|
||||
process.stdout.write(`\x1b[?1049l[${EXIT_MARKER}]\r\n`, () => process.exit(0))
|
||||
}
|
||||
|
||||
function submit() {
|
||||
if (composer.trim() === 'exit') {
|
||||
exitCleanly()
|
||||
return
|
||||
}
|
||||
lastSubmission = composer
|
||||
composer = ''
|
||||
render()
|
||||
}
|
||||
|
||||
function consumeInput() {
|
||||
while (pendingInput.length > 0 && !exiting) {
|
||||
const shiftEnter = ['\x1b[13;2u', '\x1b[13;2~', '\x1b\r'].find((sequence) =>
|
||||
pendingInput.startsWith(sequence)
|
||||
)
|
||||
if (shiftEnter) {
|
||||
pendingInput = pendingInput.slice(shiftEnter.length)
|
||||
composer += '\n'
|
||||
render()
|
||||
continue
|
||||
}
|
||||
if (
|
||||
pendingInput === '\x1b' ||
|
||||
('\x1b[13;2u'.startsWith(pendingInput) && pendingInput.startsWith('\x1b[')) ||
|
||||
('\x1b[13;2~'.startsWith(pendingInput) && pendingInput.startsWith('\x1b['))
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const char = pendingInput[0]
|
||||
pendingInput = pendingInput.slice(1)
|
||||
if (char === '\r' || char === '\n') {
|
||||
submit()
|
||||
} else if (char === '\x04') {
|
||||
exitCleanly()
|
||||
} else if (char === '\x7f' || char === '\b') {
|
||||
composer = composer.slice(0, -1)
|
||||
render()
|
||||
} else if (char >= ' ') {
|
||||
composer += char
|
||||
render()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (process.stdin.isTTY) {
|
||||
process.stdin.setRawMode(true)
|
||||
}
|
||||
process.stdin.setEncoding('utf8')
|
||||
process.stdin.on('data', (chunk) => {
|
||||
pendingInput += chunk
|
||||
consumeInput()
|
||||
})
|
||||
process.stdin.resume()
|
||||
|
||||
for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
|
||||
process.on(signal, exitCleanly)
|
||||
}
|
||||
|
||||
process.stdout.write('\x1b[?1049h')
|
||||
render()
|
||||
@@ -0,0 +1,32 @@
|
||||
import { expect, test } from './helpers/orca-app'
|
||||
import {
|
||||
configureGoldenStubAgent,
|
||||
getGoldenStubAgentLaunchEnv,
|
||||
launchGoldenStubAgentFromNewTab
|
||||
} from './helpers/golden-stub-agent'
|
||||
import { ensureTerminalVisible, waitForActiveWorktree, waitForSessionReady } from './helpers/store'
|
||||
import { focusActiveTerminalInput, getTerminalContent } from './helpers/terminal'
|
||||
|
||||
test.use({ launchEnv: getGoldenStubAgentLaunchEnv() })
|
||||
|
||||
test('@golden launches an agent TUI with a live multiline composer', async ({ orcaPage }) => {
|
||||
await waitForSessionReady(orcaPage)
|
||||
await waitForActiveWorktree(orcaPage)
|
||||
await ensureTerminalVisible(orcaPage)
|
||||
await configureGoldenStubAgent(orcaPage)
|
||||
await launchGoldenStubAgentFromNewTab(orcaPage)
|
||||
|
||||
const activeTab = orcaPage.locator('[data-testid="sortable-tab"][data-active="true"]')
|
||||
await expect(activeTab).toHaveAttribute('data-tab-title', /Codex|Golden Stub Agent/i)
|
||||
await expect(activeTab).not.toHaveAttribute('data-tab-title', /^Terminal \d+$/)
|
||||
|
||||
await focusActiveTerminalInput(orcaPage)
|
||||
await orcaPage.keyboard.type('hello from e2e')
|
||||
await orcaPage.keyboard.press('Shift+Enter')
|
||||
await orcaPage.keyboard.type('second line')
|
||||
|
||||
await expect
|
||||
.poll(() => getTerminalContent(orcaPage), { timeout: 10_000 })
|
||||
.toContain('> hello from e2e\r\n second line')
|
||||
expect(await getTerminalContent(orcaPage)).not.toContain('GOLDEN_STUB_AGENT_SUBMITTED')
|
||||
})
|
||||
@@ -0,0 +1,47 @@
|
||||
import { expect, test } from './helpers/orca-app'
|
||||
import {
|
||||
configureGoldenStubAgent,
|
||||
getGoldenStubAgentLaunchEnv,
|
||||
GOLDEN_STUB_EXIT_MARKER,
|
||||
launchGoldenStubAgentFromNewTab
|
||||
} from './helpers/golden-stub-agent'
|
||||
import { ensureTerminalVisible, waitForActiveWorktree, waitForSessionReady } from './helpers/store'
|
||||
import {
|
||||
focusActiveTerminalInput,
|
||||
waitForActivePanePtyId,
|
||||
waitForTerminalOutput
|
||||
} from './helpers/terminal'
|
||||
|
||||
test.use({ launchEnv: getGoldenStubAgentLaunchEnv() })
|
||||
|
||||
test('@golden opens a clean live shell after an agent exits', async ({ orcaPage }) => {
|
||||
await waitForSessionReady(orcaPage)
|
||||
await waitForActiveWorktree(orcaPage)
|
||||
await ensureTerminalVisible(orcaPage)
|
||||
await configureGoldenStubAgent(orcaPage)
|
||||
await launchGoldenStubAgentFromNewTab(orcaPage)
|
||||
|
||||
await orcaPage.keyboard.type('exit')
|
||||
await orcaPage.keyboard.press('Enter')
|
||||
await waitForTerminalOutput(orcaPage, GOLDEN_STUB_EXIT_MARKER, 15_000)
|
||||
|
||||
const tabsBeforeShell = await orcaPage.locator('[data-testid="sortable-tab"]').count()
|
||||
await orcaPage.getByRole('button', { name: 'New tab' }).click({ force: true })
|
||||
await orcaPage
|
||||
.getByRole('menuitem', { name: /New Terminal/i })
|
||||
.first()
|
||||
.click({ force: true })
|
||||
await expect(orcaPage.locator('[data-testid="sortable-tab"]')).toHaveCount(tabsBeforeShell + 1)
|
||||
await waitForActivePanePtyId(orcaPage)
|
||||
|
||||
await focusActiveTerminalInput(orcaPage)
|
||||
await orcaPage.keyboard.type('echo after-agent')
|
||||
await orcaPage.keyboard.press('Enter')
|
||||
await waitForTerminalOutput(orcaPage, 'after-agent', 15_000)
|
||||
|
||||
await orcaPage.keyboard.press('Shift+Enter')
|
||||
await orcaPage.keyboard.type('echo after-shift-enter')
|
||||
await orcaPage.keyboard.press('Enter')
|
||||
await waitForTerminalOutput(orcaPage, 'after-shift-enter', 15_000)
|
||||
await expect(orcaPage.locator('[data-testid="sortable-tab"]')).toHaveCount(tabsBeforeShell + 1)
|
||||
})
|
||||
@@ -0,0 +1,39 @@
|
||||
import path from 'node:path'
|
||||
import type { Page } from '@stablyai/playwright-test'
|
||||
import { expect } from '@stablyai/playwright-test'
|
||||
import { focusActiveTerminalInput, waitForTerminalOutput } from './terminal'
|
||||
|
||||
export const GOLDEN_STUB_READY_MARKER = 'GOLDEN_STUB_AGENT_READY'
|
||||
export const GOLDEN_STUB_EXIT_MARKER = 'GOLDEN_STUB_AGENT_EXITED'
|
||||
|
||||
const fixtureDir = path.join(process.cwd(), 'tests', 'e2e', 'fixtures', 'golden-stub-agent')
|
||||
|
||||
export function getGoldenStubAgentLaunchEnv(): NodeJS.ProcessEnv {
|
||||
const pathKey = Object.keys(process.env).find((key) => key.toLowerCase() === 'path') ?? 'PATH'
|
||||
return {
|
||||
[pathKey]: [fixtureDir, process.env[pathKey] ?? ''].filter(Boolean).join(path.delimiter)
|
||||
}
|
||||
}
|
||||
|
||||
export async function configureGoldenStubAgent(page: Page): Promise<void> {
|
||||
await page.evaluate(async () => {
|
||||
const store = window.__store
|
||||
if (!store) {
|
||||
throw new Error('Orca store is unavailable')
|
||||
}
|
||||
await store.getState().updateSettings({
|
||||
defaultTuiAgent: 'codex',
|
||||
agentCmdOverrides: { codex: 'golden-stub-agent' },
|
||||
agentDefaultArgs: { codex: '' }
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export async function launchGoldenStubAgentFromNewTab(page: Page): Promise<void> {
|
||||
await page.getByRole('button', { name: 'New tab' }).click({ force: true })
|
||||
const launchOption = page.getByRole('menuitem', { name: /^Codex(?:\s|$)/i }).first()
|
||||
await expect(launchOption).toBeVisible({ timeout: 15_000 })
|
||||
await launchOption.click({ force: true })
|
||||
await focusActiveTerminalInput(page)
|
||||
await waitForTerminalOutput(page, GOLDEN_STUB_READY_MARKER, 20_000)
|
||||
}
|
||||
Reference in New Issue
Block a user