test(e2e): cover tab-bar agent launches on Windows and WSL (#16110)

* test(e2e): gate the tab-bar agent launcher on Windows shells and WSL

The `+` menu agent launcher had no golden coverage in the Windows lane, so a
Windows-only break anywhere in its chain (detection row, startup-plan build,
tab create, PTY spawn, startup-command injection) could ship unnoticed.

Adds a golden spec that launches a stub agent from the menu and asserts the
agent's own banner reached the pane — a tab that spawned a bare shell instead
is indistinguishable at the store/tab layer. Runs two agents everywhere, and
on Windows also PowerShell, cmd, Git Bash and a WSL project runtime.

* test(e2e): track WSL stub agent staging state for precise cleanup

Refactor `stageWslGoldenStubAgent` to track which artifacts it creates
during setup, then only remove those artifacts during cleanup. This
prevents the test from destructively removing pre-existing symlinks or
state from previous runs, improving test isolation and idempotency.

* test(e2e): track WSL stub agent staging state for precise cleanup

- Back up and restore pre-existing stub agents to avoid destroying them
- Simplify verbose test comments to match project style guidelines

* test(e2e): serialize WSL stub agent setup with distributed lock

- Add mkdir-based lock to prevent concurrent staging invocations
- Reclaim stale locks after 10 minutes to recover from crashes
- Track lock ownership in stage state for safe cleanup

* test(e2e): track WSL stub agent staging state for precise cleanup

Track which stubs this test helper stages by writing a marker file, then
only remove stubs during stale-lock recovery if we created them. Prevents
cleanup from removing stubs left by other processes.
This commit is contained in:
Jinjing
2026-08-24 08:58:19 -07:00
committed by GitHub
parent afd76a4df9
commit f5fd7303ab
7 changed files with 293 additions and 14 deletions
@@ -74,6 +74,7 @@ jobs:
xvfb-run --auto-servernum env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run test:e2e:terminal-rendering-golden
xvfb-run --auto-servernum env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run --if-present test:e2e:posix-profile-index-golden
xvfb-run --auto-servernum env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run --if-present test:e2e:agent-tui-golden
xvfb-run --auto-servernum env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run --if-present test:e2e:tab-bar-agent-launch-golden
xvfb-run --auto-servernum env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run --if-present test:e2e:source-control-golden
- name: Run golden E2E tests on macOS
@@ -87,6 +88,7 @@ jobs:
env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run test:e2e:terminal-rendering-golden
env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run --if-present test:e2e:posix-profile-index-golden
env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run --if-present test:e2e:agent-tui-golden
env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run --if-present test:e2e:tab-bar-agent-launch-golden
env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run --if-present test:e2e:source-control-golden
- name: Run golden E2E tests on Windows
@@ -97,6 +99,7 @@ jobs:
$env:ORCA_E2E_FORWARD_APP_LOGS = '1'
pnpm run --if-present test:e2e:workspace-session-golden
pnpm run --if-present test:e2e:windows-fresh-startup-golden
pnpm run --if-present test:e2e:tab-bar-agent-launch-golden
if (Test-Path tests/e2e/golden-fresh-profile-terminal.spec.ts) {
pnpm run test:e2e -- tests/e2e/golden-fresh-profile-terminal.spec.ts tests/e2e/golden-shell-command.spec.ts
}
+1
View File
@@ -100,6 +100,7 @@
"test:e2e:posix-profile-index-golden": "pnpm run ensure:electron-runtime && npx playwright test tests/e2e/golden-posix-fresh-startup.spec.ts tests/e2e/golden-posix-profile-index-fsync.spec.ts --grep @posix-profile-index-golden --config tests/playwright.config.ts --project electron-headless --workers=1",
"test:e2e:windows-fresh-startup-golden": "pnpm run ensure:electron-runtime && npx playwright test tests/e2e/golden-windows-fresh-startup.spec.ts --grep @windows-fresh-startup-golden --config tests/playwright.config.ts --project electron-headless --workers=1",
"test:e2e:agent-tui-golden": "pnpm run ensure:electron-runtime && npx playwright test tests/e2e/golden-agent-tui-launch.spec.ts tests/e2e/golden-shell-after-agent-exit.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1",
"test:e2e:tab-bar-agent-launch-golden": "pnpm run ensure:electron-runtime && npx playwright test tests/e2e/golden-tab-bar-agent-launch.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1",
"test:e2e:terminal-rendering-release-evidence": "pnpm run ensure:electron-runtime && npx playwright test tests/e2e/terminal-opencode-emoji-table-rendering.spec.ts tests/e2e/terminal-long-table-scroll-restore.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=2",
"test:e2e:terminal-perf": "pnpm run ensure:electron-runtime && npx playwright test tests/e2e/terminal-typing-latency.spec.ts tests/e2e/terminal-foreground-redraw-freeze.spec.ts tests/e2e/terminal-output-scheduler.spec.ts tests/e2e/terminal-hidden-tui-visual-restore.spec.ts tests/e2e/artificial-opencode-terminal-load.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=2",
"test:e2e:terminal-perf:scale": "pnpm run ensure:electron-runtime && node config/scripts/run-terminal-scale-perf-e2e.mjs",
+2
View File
@@ -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,90 @@
import type { Page } from '@stablyai/playwright-test'
import { expect, test } from './helpers/orca-app'
import {
configureGoldenStubAgent,
getGoldenStubAgentLaunchEnv,
GOLDEN_STUB_AGENTS,
GOLDEN_STUB_READY_MARKER,
launchGoldenStubAgentFromNewTab
} from './helpers/golden-stub-agent'
import {
getFirstWslDistro,
removeWslGoldenStubAgent,
stageWslGoldenStubAgent,
useWslRuntimeForActiveProject
} from './helpers/wsl-golden-stub-agent'
import { ensureTerminalVisible, waitForActiveWorktree, waitForSessionReady } from './helpers/store'
import { getTerminalContent } from './helpers/terminal'
import type { BuiltInWindowsTerminalShell } from '../../src/shared/windows-terminal-shell'
// Covers the Windows-only tab-bar launch path that the default-shell test misses.
test.use({ launchEnv: getGoldenStubAgentLaunchEnv() })
const WINDOWS_SHELLS: readonly BuiltInWindowsTerminalShell[] = [
'powershell.exe',
'cmd.exe',
'git-bash'
]
async function openWorkspaceTerminal(page: Page): Promise<void> {
await waitForSessionReady(page)
await waitForActiveWorktree(page)
await ensureTerminalVisible(page)
}
for (const { id, menuItemName } of GOLDEN_STUB_AGENTS) {
test(`tab-bar + menu launches ${id} into a live TUI @tab-bar-agent-launch-golden`, async ({
orcaPage
}) => {
await openWorkspaceTerminal(orcaPage)
await configureGoldenStubAgent(orcaPage, { agent: id })
await launchGoldenStubAgentFromNewTab(orcaPage, menuItemName)
const activeTab = orcaPage.locator('[data-testid="sortable-tab"][data-active="true"]')
await expect(activeTab).toHaveAttribute('data-tab-title', /Golden Stub Agent|Codex|Claude/i)
// The marker distinguishes an agent launch from an identical bare-shell tab.
expect(await getTerminalContent(orcaPage)).toContain(GOLDEN_STUB_READY_MARKER)
})
}
// Suite-level skipping avoids launching Electron for each unsupported case.
test.describe('Windows runtimes', () => {
test.skip(process.platform !== 'win32', 'Windows agent launch matrix is Windows-only')
for (const shell of WINDOWS_SHELLS) {
test(`tab-bar + menu launches an agent under ${shell} @tab-bar-agent-launch-golden`, async ({
orcaPage
}) => {
await openWorkspaceTerminal(orcaPage)
// Each shell family requires different launch-command quoting.
await configureGoldenStubAgent(orcaPage, { agent: 'codex', windowsShell: shell })
await launchGoldenStubAgentFromNewTab(orcaPage)
expect(await getTerminalContent(orcaPage)).toContain(GOLDEN_STUB_READY_MARKER)
})
}
test('tab-bar + menu launches an agent inside WSL @tab-bar-agent-launch-golden', async ({
orcaPage
}) => {
await openWorkspaceTerminal(orcaPage)
const distro = await getFirstWslDistro(orcaPage)
test.skip(!distro, 'No WSL distro is available on this Windows host')
const stage = stageWslGoldenStubAgent(distro!)
test.skip(!stage, 'WSL distro would not accept the staged stub agent')
try {
// WSL must retarget both agent detection and the PTY.
await useWslRuntimeForActiveProject(orcaPage, distro!)
await configureGoldenStubAgent(orcaPage, { agent: 'codex' })
await launchGoldenStubAgentFromNewTab(orcaPage)
// The distro-only marker proves the agent ran in WSL.
expect(await getTerminalContent(orcaPage)).toContain(GOLDEN_STUB_READY_MARKER)
} finally {
removeWslGoldenStubAgent(distro!, stage!)
}
})
})
+36 -14
View File
@@ -2,10 +2,17 @@ import path from 'node:path'
import type { Page } from '@stablyai/playwright-test'
import { expect } from '@stablyai/playwright-test'
import { focusActiveTerminalInput, waitForTerminalOutput } from './terminal'
import type { BuiltInWindowsTerminalShell } from '../../../src/shared/windows-terminal-shell'
export const GOLDEN_STUB_READY_MARKER = 'GOLDEN_STUB_AGENT_READY'
export const GOLDEN_STUB_EXIT_MARKER = 'GOLDEN_STUB_AGENT_EXITED'
/** Agents exposed by the fixture directory for tab-bar detection. */
export const GOLDEN_STUB_AGENTS = [
{ id: 'codex', menuItemName: /^Codex(?:\s|$)/i },
{ id: 'claude', menuItemName: /^Claude(?:\s|$)/i }
] as const
const fixtureDir = path.join(process.cwd(), 'tests', 'e2e', 'fixtures', 'golden-stub-agent')
export function getGoldenStubAgentLaunchEnv(): NodeJS.ProcessEnv {
@@ -15,23 +22,38 @@ export function getGoldenStubAgentLaunchEnv(): NodeJS.ProcessEnv {
}
}
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 configureGoldenStubAgent(
page: Page,
options: {
agent?: (typeof GOLDEN_STUB_AGENTS)[number]['id']
/** Windows default shell the launch command must survive; ignored elsewhere. */
windowsShell?: BuiltInWindowsTerminalShell
} = {}
): Promise<void> {
const agent = options.agent ?? 'codex'
await page.evaluate(
async ({ agent, windowsShell }) => {
const store = window.__store
if (!store) {
throw new Error('Orca store is unavailable')
}
await store.getState().updateSettings({
defaultTuiAgent: agent,
agentCmdOverrides: { [agent]: 'golden-stub-agent' },
agentDefaultArgs: { [agent]: '' },
...(windowsShell ? { terminalWindowsShell: windowsShell } : {})
})
},
{ agent, windowsShell: options.windowsShell ?? null }
)
}
export async function launchGoldenStubAgentFromNewTab(page: Page): Promise<void> {
export async function launchGoldenStubAgentFromNewTab(
page: Page,
menuItemName: RegExp = /^Codex(?:\s|$)/i
): Promise<void> {
await page.getByRole('button', { name: 'New tab' }).click({ force: true })
const launchOption = page.getByRole('menuitem', { name: /^Codex(?:\s|$)/i }).first()
const launchOption = page.getByRole('menuitem', { name: menuItemName }).first()
await expect(launchOption).toBeVisible({ timeout: 15_000 })
await launchOption.click({ force: true })
await focusActiveTerminalInput(page)
+159
View File
@@ -0,0 +1,159 @@
import { execFileSync } from 'node:child_process'
import type { Page } from '@stablyai/playwright-test'
import { buildWslExecArgs } from '../../../src/shared/wsl-login-shell-command'
/** A WSL-only path makes the stub marker proof that the pane ran in the distro. */
const WSL_STUB_PATH = '/usr/local/bin/golden-stub-agent'
const WSL_STUB_AGENT_LINK = '/usr/local/bin/codex'
const WSL_STUB_BACKUP_PATH = '/usr/local/bin/golden-stub-agent.orca-e2e-backup'
/** mkdir is atomic in the distro, so the lock dir serializes overlapping invocations. */
const WSL_STUB_LOCK_PATH = '/usr/local/bin/golden-stub-agent.orca-e2e-lock'
const WSL_STUB_LINK_MARKER = `${WSL_STUB_LOCK_PATH}/created-codex-link`
const WSL_STUB_STAGED_MARKER = `${WSL_STUB_LOCK_PATH}/staged-stub`
const WSL_STUB_LOCK_STALE_MINUTES = 10
const WSL_STUB_LOCK_WAIT_SECONDS = 60
// Undoes a lock holder that died mid-run, so its leftovers cannot poison later invocations.
const RECLAIM_STALE_LOCK_SCRIPT =
`if [ -e ${WSL_STUB_LINK_MARKER} ]; then rm -f ${WSL_STUB_AGENT_LINK}; fi; ` +
`if [ -e ${WSL_STUB_STAGED_MARKER} ]; then rm -f ${WSL_STUB_PATH}; fi; ` +
`if [ -e ${WSL_STUB_BACKUP_PATH} ] || [ -L ${WSL_STUB_BACKUP_PATH} ]; then ` +
`mv ${WSL_STUB_BACKUP_PATH} ${WSL_STUB_PATH}; fi; ` +
`rm -rf ${WSL_STUB_LOCK_PATH}`
const ACQUIRE_LOCK_SCRIPT =
`mkdir -p /usr/local/bin || exit 1; i=0; ` +
`while [ $i -lt ${WSL_STUB_LOCK_WAIT_SECONDS} ]; do ` +
`if mkdir ${WSL_STUB_LOCK_PATH} 2>/dev/null; then printf acquired; exit 0; fi; ` +
`if [ -n "$(find ${WSL_STUB_LOCK_PATH} -maxdepth 0 -mmin +${WSL_STUB_LOCK_STALE_MINUTES} ` +
`2>/dev/null)" ]; then ${RECLAIM_STALE_LOCK_SCRIPT}; else sleep 1; fi; i=$((i+1)); ` +
`done; printf timeout`
// Keep the cross-boundary script newline-free to avoid Windows argv-encoding surprises.
// Moving the entry avoids following and overwriting a pre-existing symlink.
const BACKUP_EXISTING_STUB_SCRIPT =
`mkdir -p /usr/local/bin && ` +
`if [ -e ${WSL_STUB_BACKUP_PATH} ] || [ -L ${WSL_STUB_BACKUP_PATH} ]; then exit 1; fi && ` +
`if [ -e ${WSL_STUB_PATH} ] || [ -L ${WSL_STUB_PATH} ]; then ` +
`mv ${WSL_STUB_PATH} ${WSL_STUB_BACKUP_PATH} && ` +
`printf backed-up; else printf none; fi`
// The marker is written first so stale-lock recovery only removes a stub this helper wrote.
const STAGE_SCRIPT =
`mkdir -p /usr/local/bin && : > ${WSL_STUB_STAGED_MARKER} && ` +
`printf '#!/bin/sh\\necho GOLDEN_STUB_AGENT_READY\\nexec sleep 3600\\n' > ${WSL_STUB_PATH} && ` +
`chmod 0755 ${WSL_STUB_PATH}`
// The marker is written before the link so a crashed run over-reports rather than leaks a link.
const STAGE_CODEX_LINK_IF_MISSING_SCRIPT =
`if [ -e ${WSL_STUB_AGENT_LINK} ] || [ -L ${WSL_STUB_AGENT_LINK} ]; then ` +
`printf existing; else : > ${WSL_STUB_LINK_MARKER} && ` +
`ln -s ${WSL_STUB_PATH} ${WSL_STUB_AGENT_LINK} && printf created; fi`
// `;` between steps so the lock is released even when a restore step fails.
function buildRestoreScript(stage: WslGoldenStubAgentStage): string {
const steps: string[] = []
if (stage.ownsStubPath) {
const removed = stage.createdCodexLink
? `${WSL_STUB_AGENT_LINK} ${WSL_STUB_PATH}`
: WSL_STUB_PATH
steps.push(`rm -f ${removed}`)
if (stage.backedUpStub) {
steps.push(`mv ${WSL_STUB_BACKUP_PATH} ${WSL_STUB_PATH}`)
}
}
if (stage.heldLock) {
steps.push(`rm -rf ${WSL_STUB_LOCK_PATH}`)
}
return steps.join(' ; ')
}
// --exec prevents wsl.exe from expanding shell variables in argv.
function runInWslAsRoot(distro: string, script: string): string {
return execFileSync(
'wsl.exe',
['-u', 'root', ...buildWslExecArgs(distro, ['sh', '-c', script])],
{ encoding: 'utf8', stdio: 'pipe', windowsHide: true }
)
}
export async function getFirstWslDistro(page: Page): Promise<string | null> {
const wsl = await page.evaluate(async () => ({
available: await window.api.wsl.isAvailable(),
distros: await window.api.wsl.listDistros()
}))
return wsl.available ? (wsl.distros[0] ?? null) : null
}
export type WslGoldenStubAgentStage = {
createdCodexLink: boolean
backedUpStub: boolean
ownsStubPath: boolean
heldLock: boolean
}
/** Returns null when the distro cannot stage the stub. Holds a distro lock until cleanup. */
export function stageWslGoldenStubAgent(distro: string): WslGoldenStubAgentStage | null {
const stage: WslGoldenStubAgentStage = {
createdCodexLink: false,
backedUpStub: false,
ownsStubPath: false,
heldLock: false
}
try {
if (runInWslAsRoot(distro, ACQUIRE_LOCK_SCRIPT).trim() !== 'acquired') {
return null
}
stage.heldLock = true
stage.backedUpStub = runInWslAsRoot(distro, BACKUP_EXISTING_STUB_SCRIPT).trim() === 'backed-up'
stage.ownsStubPath = true
runInWslAsRoot(distro, STAGE_SCRIPT)
stage.createdCodexLink =
runInWslAsRoot(distro, STAGE_CODEX_LINK_IF_MISSING_SCRIPT).trim() === 'created'
return stage
} catch {
removeWslGoldenStubAgent(distro, stage)
return null
}
}
export function removeWslGoldenStubAgent(distro: string, stage: WslGoldenStubAgentStage): void {
const script = buildRestoreScript(stage)
if (!script) {
return
}
try {
runInWslAsRoot(distro, script)
stage.heldLock = false
stage.ownsStubPath = false
} catch {
// Best-effort cleanup; a leftover stub only affects this fixture's own name.
}
}
/** Retargets project agent detection and terminal spawning to WSL. */
export async function useWslRuntimeForActiveProject(page: Page, distro: string): Promise<void> {
await page.evaluate(async (wslDistro) => {
const store = window.__store
if (!store) {
throw new Error('Orca store is unavailable')
}
const state = store.getState()
const worktreeId = state.activeWorktreeId
if (!worktreeId) {
throw new Error('No active worktree')
}
const activeWorktree = Object.values(state.worktreesByRepo)
.flat()
.find((worktree) => worktree.id === worktreeId)
const activeProject = state.projects.find((project) =>
activeWorktree ? project.sourceRepoIds.includes(activeWorktree.repoId) : false
)
if (!activeProject) {
throw new Error('No active project')
}
await state.updateProject(activeProject.id, {
localWindowsRuntimePreference: { kind: 'wsl', distro: wslDistro }
})
}, distro)
}