From f4782e8db60ec2d8f4b1bb23cb0b3fe20601431e Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Thu, 13 Aug 2026 01:53:17 -0700 Subject: [PATCH] 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 --- tests/e2e/fixtures/golden-stub-agent/codex | 2 + .../e2e/fixtures/golden-stub-agent/codex.cmd | 2 + .../golden-stub-agent/golden-stub-agent | 2 + .../golden-stub-agent/golden-stub-agent.cmd | 2 + .../golden-stub-agent/golden-stub-agent.js | 98 +++++++++++++++++++ tests/e2e/golden-agent-tui-launch.spec.ts | 32 ++++++ .../e2e/golden-shell-after-agent-exit.spec.ts | 47 +++++++++ tests/e2e/helpers/golden-stub-agent.ts | 39 ++++++++ 8 files changed, 224 insertions(+) create mode 100755 tests/e2e/fixtures/golden-stub-agent/codex create mode 100644 tests/e2e/fixtures/golden-stub-agent/codex.cmd create mode 100755 tests/e2e/fixtures/golden-stub-agent/golden-stub-agent create mode 100644 tests/e2e/fixtures/golden-stub-agent/golden-stub-agent.cmd create mode 100755 tests/e2e/fixtures/golden-stub-agent/golden-stub-agent.js create mode 100644 tests/e2e/golden-agent-tui-launch.spec.ts create mode 100644 tests/e2e/golden-shell-after-agent-exit.spec.ts create mode 100644 tests/e2e/helpers/golden-stub-agent.ts diff --git a/tests/e2e/fixtures/golden-stub-agent/codex b/tests/e2e/fixtures/golden-stub-agent/codex new file mode 100755 index 00000000000..d16a99de266 --- /dev/null +++ b/tests/e2e/fixtures/golden-stub-agent/codex @@ -0,0 +1,2 @@ +#!/bin/sh +exec "$(dirname "$0")/golden-stub-agent" "$@" diff --git a/tests/e2e/fixtures/golden-stub-agent/codex.cmd b/tests/e2e/fixtures/golden-stub-agent/codex.cmd new file mode 100644 index 00000000000..894a9216aa2 --- /dev/null +++ b/tests/e2e/fixtures/golden-stub-agent/codex.cmd @@ -0,0 +1,2 @@ +@echo off +call "%~dp0golden-stub-agent.cmd" %* diff --git a/tests/e2e/fixtures/golden-stub-agent/golden-stub-agent b/tests/e2e/fixtures/golden-stub-agent/golden-stub-agent new file mode 100755 index 00000000000..9a9bf91bce4 --- /dev/null +++ b/tests/e2e/fixtures/golden-stub-agent/golden-stub-agent @@ -0,0 +1,2 @@ +#!/bin/sh +exec node "$(dirname "$0")/golden-stub-agent.js" "$@" diff --git a/tests/e2e/fixtures/golden-stub-agent/golden-stub-agent.cmd b/tests/e2e/fixtures/golden-stub-agent/golden-stub-agent.cmd new file mode 100644 index 00000000000..ccf5ced005e --- /dev/null +++ b/tests/e2e/fixtures/golden-stub-agent/golden-stub-agent.cmd @@ -0,0 +1,2 @@ +@echo off +node "%~dp0golden-stub-agent.js" %* diff --git a/tests/e2e/fixtures/golden-stub-agent/golden-stub-agent.js b/tests/e2e/fixtures/golden-stub-agent/golden-stub-agent.js new file mode 100755 index 00000000000..38558866cee --- /dev/null +++ b/tests/e2e/fixtures/golden-stub-agent/golden-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() diff --git a/tests/e2e/golden-agent-tui-launch.spec.ts b/tests/e2e/golden-agent-tui-launch.spec.ts new file mode 100644 index 00000000000..c7591bf9be5 --- /dev/null +++ b/tests/e2e/golden-agent-tui-launch.spec.ts @@ -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') +}) diff --git a/tests/e2e/golden-shell-after-agent-exit.spec.ts b/tests/e2e/golden-shell-after-agent-exit.spec.ts new file mode 100644 index 00000000000..45b2e0bc1a9 --- /dev/null +++ b/tests/e2e/golden-shell-after-agent-exit.spec.ts @@ -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) +}) diff --git a/tests/e2e/helpers/golden-stub-agent.ts b/tests/e2e/helpers/golden-stub-agent.ts new file mode 100644 index 00000000000..62e44c9b091 --- /dev/null +++ b/tests/e2e/helpers/golden-stub-agent.ts @@ -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 { + 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 { + 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) +}