Merge remote-tracking branch 'origin/main' into aug12-blocker-sta4067-live-readiness

This commit is contained in:
Jinwoo-H
2026-08-13 00:35:04 -04:00
16 changed files with 266 additions and 45 deletions
+1 -1
View File
@@ -30,7 +30,7 @@ Fixes #
## AI Disclosure
<!-- For all external contributors only: Which AI model if anyone was used, please state the details -->
<!-- DO NOT FILL IN IF !INTERNAL! CONTRIBUTOR: Which AI model if anyone was used, please state the details -->
## Review
+11 -4
View File
@@ -23,10 +23,9 @@ jobs:
platform: linux
- os: macos-15
platform: mac
# Why: Windows golden E2E is temporarily disabled on CI while its
# flaky runner-only failures are investigated.
# - os: windows-latest
# platform: windows
# Match the release gate: Windows runs the fresh-startup golden only.
- os: windows-2022
platform: windows
steps:
- name: Checkout
@@ -74,6 +73,14 @@ jobs:
env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run test:e2e -- tests/e2e/golden-core-flows.spec.ts
env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run test:e2e:terminal-rendering-golden
- name: Run golden E2E tests on Windows
if: runner.os == 'Windows'
shell: pwsh
run: |
$env:SKIP_BUILD = '1'
$env:ORCA_E2E_FORWARD_APP_LOGS = '1'
pnpm run test:e2e:windows-fresh-startup-golden
- name: Upload Playwright traces
if: failure()
uses: actions/upload-artifact@v7
+16 -8
View File
@@ -783,7 +783,7 @@ jobs:
{
echo "## Release E2E Signal"
echo ""
echo "- Terminal rendering golden is release-blocking."
echo "- Platform golden E2E is release-blocking: terminal rendering on Linux/macOS and fresh startup on Windows."
echo "- Full E2E runs separately after publication and cannot change the release result."
echo "- Terminal rendering release evidence is diagnostic/non-blocking."
echo ""
@@ -817,7 +817,7 @@ jobs:
terminal-rendering-golden:
needs: cut
if: needs.cut.outputs.should_release == 'true'
name: terminal rendering golden ${{ matrix.platform }}
name: golden e2e ${{ matrix.platform }}
runs-on: ${{ matrix.os }}
timeout-minutes: 30
env:
@@ -830,10 +830,10 @@ jobs:
platform: linux
- os: macos-15
platform: mac
# Why: Windows terminal rendering golden is temporarily disabled on
# CI while its flaky runner-only failures are investigated.
# - os: windows-latest
# platform: windows
# Windows terminal rendering remains flaky; keep its blocking signal
# scoped to the fresh-profile startup regression from #14130.
- os: windows-2022
platform: windows
steps:
- name: Checkout
@@ -866,7 +866,7 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build Electron app for terminal rendering golden
- name: Build Electron app for platform golden
run: npx electron-vite build --mode e2e
- name: Run terminal rendering golden on Linux
@@ -877,11 +877,19 @@ jobs:
if: runner.os == 'macOS'
run: env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run test:e2e:terminal-rendering-golden
- name: Run fresh-startup golden on Windows
if: runner.os == 'Windows'
shell: pwsh
run: |
$env:SKIP_BUILD = '1'
$env:ORCA_E2E_FORWARD_APP_LOGS = '1'
pnpm run test:e2e:windows-fresh-startup-golden
- name: Upload Playwright traces
if: failure()
uses: actions/upload-artifact@v7
with:
name: terminal-rendering-golden-${{ matrix.platform }}-playwright-traces
name: golden-e2e-${{ matrix.platform }}-playwright-traces
path: test-results/
retention-days: 7
if-no-files-found: ignore
+4
View File
@@ -126,6 +126,10 @@ playwright-report/
/.agents/skills/
/skills-lock.json
# Generated Clawpatch state includes machine-local paths and review records.
/.clawpatch/
/mobile/.clawpatch/
# Agent hook runtime endpoints (machine-local secrets)
/agent-hooks/
validation-screenshots/
@@ -492,7 +492,7 @@ describe('Electron runtime package contract', () => {
expect(uploadStep.with.path).toBe('${{ env.ORCA_E2E_TERMINAL_PERF_REPORT_PATH }}')
})
it('keeps terminal rendering regressions in the manual golden E2E workflow', () => {
it('keeps platform golden regressions in the manual and release workflows', () => {
const packageScripts = packageJson.scripts
const goldenWorkflow = parse(
readFileSync(join(projectDir, '.github/workflows/golden-e2e-experiment.yml'), 'utf8')
@@ -506,17 +506,19 @@ describe('Electron runtime package contract', () => {
['mac', 'macOS'],
['windows', 'Windows']
])
const goldenPlatforms = goldenWorkflow.jobs['golden-e2e'].strategy.matrix.include
.map(({ platform }) => platform)
.sort()
const goldenRunSteps = goldenPlatforms.map((platform) => {
const label = goldenPlatformLabels.get(platform)
const goldenMatrix = goldenWorkflow.jobs['golden-e2e'].strategy.matrix.include
const goldenPlatforms = goldenMatrix.map(({ platform }) => platform).sort()
const goldenRunSteps = new Map(
goldenPlatforms.map((platform) => {
const label = goldenPlatformLabels.get(platform)
expect(label, platform).toBeDefined()
expect(label, platform).toBeDefined()
return steps.find((step) => step.name === `Run golden E2E tests on ${label}`)
})
return [platform, steps.find((step) => step.name === `Run golden E2E tests on ${label}`)]
})
)
const releaseGoldenJob = releaseWorkflow.jobs['terminal-rendering-golden']
const releaseGoldenMatrix = releaseGoldenJob.strategy.matrix.include
const releaseEvidenceJob = releaseWorkflow.jobs['terminal-rendering-release-evidence']
const releaseBuildNeeds = releaseWorkflow.jobs.build.needs
const publishReleaseNeeds = releaseWorkflow.jobs['publish-release'].needs
@@ -535,15 +537,34 @@ describe('Electron runtime package contract', () => {
expect(packageScripts['test:e2e:terminal-rendering-golden']).not.toContain(
'terminal-long-table-scroll-restore.spec.ts'
)
expect(packageScripts['test:e2e:windows-fresh-startup-golden']).toContain(
'golden-windows-fresh-startup.spec.ts'
)
expect(packageScripts['test:e2e:windows-fresh-startup-golden']).toContain(
'@windows-fresh-startup-golden'
)
expect(packageScripts['test:e2e:terminal-rendering-release-evidence']).toContain(
'terminal-opencode-emoji-table-rendering.spec.ts'
)
expect(packageScripts['test:e2e:terminal-rendering-release-evidence']).toContain(
'terminal-long-table-scroll-restore.spec.ts'
)
for (const runStep of goldenRunSteps) {
expect(runStep?.run).toContain('pnpm run test:e2e:terminal-rendering-golden')
}
expect(goldenMatrix).toEqual([
{ os: 'ubuntu-latest', platform: 'linux' },
{ os: 'macos-15', platform: 'mac' },
{ os: 'windows-2022', platform: 'windows' }
])
expect(goldenRunSteps.get('linux')?.run).toContain(
'pnpm run test:e2e:terminal-rendering-golden'
)
expect(goldenRunSteps.get('mac')?.run).toContain('pnpm run test:e2e:terminal-rendering-golden')
expect(goldenRunSteps.get('windows')).toMatchObject({
if: "runner.os == 'Windows'",
shell: 'pwsh'
})
expect(goldenRunSteps.get('windows').run).toContain(
'pnpm run test:e2e:windows-fresh-startup-golden'
)
expect(goldenWorkflow.on.pull_request).toBeUndefined()
expect(goldenWorkflow.on.workflow_dispatch).toBeDefined()
expect(releaseBuildNeeds).not.toContain('terminal-rendering-golden')
@@ -552,12 +573,18 @@ describe('Electron runtime package contract', () => {
expect(publishReleaseNeeds).toContain('build')
expect(publishReleaseNeeds).not.toContain('terminal-rendering-release-evidence')
expect(releaseGoldenJob['continue-on-error']).toBeUndefined()
expect(releaseGoldenJob.strategy.matrix.include.map(({ platform }) => platform).sort()).toEqual(
goldenPlatforms
)
expect(releaseGoldenMatrix).toEqual(goldenMatrix)
expect(releaseGoldenJob.steps.map((step) => step.run ?? '')).toContain(
'xvfb-run --auto-servernum env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run test:e2e:terminal-rendering-golden'
)
const releaseWindowsRunStep = releaseGoldenJob.steps.find(
(step) => step.name === 'Run fresh-startup golden on Windows'
)
expect(releaseWindowsRunStep).toMatchObject({
if: "runner.os == 'Windows'",
shell: 'pwsh'
})
expect(releaseWindowsRunStep.run).toContain('pnpm run test:e2e:windows-fresh-startup-golden')
expect(releaseEvidenceJob['continue-on-error']).toBe(true)
expect(
releaseEvidenceJob.strategy.matrix.include.map(({ platform }) => platform).sort()
+1
View File
@@ -87,6 +87,7 @@
"test:e2e:multi-client-navigation": "node config/scripts/run-multi-client-navigation-e2e.mjs",
"test:e2e:floating-mobile-emulator": "pnpm run ensure:electron-runtime && npx playwright test tests/e2e/floating-mobile-emulator-tab.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1",
"test:e2e:terminal-rendering-golden": "pnpm run ensure:electron-runtime && npx playwright test tests/e2e/terminal-raw-emoji-table-scroll-restore.spec.ts tests/e2e/terminal-webgl-atlas-budget.spec.ts --grep @terminal-rendering-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: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,7 +2,10 @@ import { mkdtemp, mkdir, readFile, readdir, rm, stat, writeFile } from 'node:fs/
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { installRemoteManagedAgentHooks } from './remote-managed-hook-installers'
import {
installRemoteManagedAgentHooks,
REMOTE_MANAGED_HOOK_INSTALLER_AGENTS
} from './remote-managed-hook-installers'
import { createManagedHookLocalFilesystem } from './managed-hook-local-filesystem'
const tempHomes: string[] = []
@@ -34,7 +37,10 @@ describe('managed-hook local filesystem', () => {
it('supports cold and warm aggregate installs without SFTP or temp-file residue', async () => {
const home = await createTempHome()
const filesystem = createManagedHookLocalFilesystem()
const options = { grokHomeDir: join(home, '.grok') }
const options = {
grokHomeDir: join(home, '.grok'),
agents: REMOTE_MANAGED_HOOK_INSTALLER_AGENTS
}
const cold = await installRemoteManagedAgentHooks(filesystem, home, options)
const warm = await installRemoteManagedAgentHooks(filesystem, home, options)
@@ -61,7 +67,8 @@ describe('managed-hook local filesystem', () => {
await writeFile(claudeConfig, '{"hooks": }', 'utf8')
const results = await installRemoteManagedAgentHooks(createManagedHookLocalFilesystem(), home, {
grokHomeDir: join(home, '.grok')
grokHomeDir: join(home, '.grok'),
agents: REMOTE_MANAGED_HOOK_INSTALLER_AGENTS
})
expect(results).toHaveLength(14)
@@ -1,3 +1,6 @@
import { chmod, mkdtemp, readdir, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type * as NodeChildProcess from 'node:child_process'
@@ -12,7 +15,9 @@ vi.mock('node:child_process', async (importOriginal) => {
const { execFile } = await import('node:child_process')
const execFileMock = vi.mocked(execFile)
const { resolveRelayGrokHome } = await import('./managed-hook-runtime')
const { execFile: actualExecFile } =
await vi.importActual<typeof NodeChildProcess>('node:child_process')
const { installManagedHooks, resolveRelayGrokHome } = await import('./managed-hook-runtime')
type ExecFileCallback = (error: Error | null, result?: { stdout: string; stderr: string }) => void
@@ -32,10 +37,46 @@ function stubProbeFailure(error: Error): void {
beforeEach(() => {
execFileMock.mockReset()
// Why: `installManagedHooks` proves a skipped probe through the login-shell run log, so the
// probe must really spawn unless a case above stubs it. The mock loses `promisify.custom`,
// so the real `(error, stdout, stderr)` callback is reshaped into the `{ stdout, stderr }`
// that `promisify(execFile)` resolves in production.
execFileMock.mockImplementation(((...args: unknown[]) => {
const callback = args.at(-1) as ExecFileCallback
return (actualExecFile as (...callArgs: unknown[]) => unknown)(
...args.slice(0, -1),
(error: Error | null, stdout: string, stderr: string) => callback(error, { stdout, stderr })
)
}) as unknown as typeof execFile)
})
afterEach(() => {
const tempHomes: string[] = []
const tempRoot = process.platform === 'win32' ? tmpdir() : '/tmp'
const SHELL_NAME = 'login-shell'
const SHELL_RUNS_NAME = 'login-shell-runs'
async function createTempHome(): Promise<string> {
const home = await mkdtemp(join(tempRoot, 'orca-managed-hook-runtime-'))
tempHomes.push(home)
return home
}
/** Login shell that records each invocation, so a skipped GROK_HOME probe is observable. */
async function stubLoginShell(home: string): Promise<void> {
const shell = join(home, SHELL_NAME)
await writeFile(
shell,
`#!/bin/sh\necho ran >> "${join(home, SHELL_RUNS_NAME)}"\nexit 0\n`,
'utf8'
)
await chmod(shell, 0o755)
vi.stubEnv('HOME', home)
vi.stubEnv('SHELL', shell)
}
afterEach(async () => {
vi.unstubAllEnvs()
await Promise.all(tempHomes.splice(0).map((home) => rm(home, { recursive: true, force: true })))
})
describe.runIf(process.platform !== 'win32')('resolveRelayGrokHome', () => {
@@ -78,3 +119,42 @@ describe.runIf(process.platform !== 'win32')('resolveRelayGrokHome', () => {
await expect(resolveRelayGrokHome('/home/orca')).resolves.toBe('/home/orca/.grok')
})
})
describe.runIf(process.platform !== 'win32')('installManagedHooks', () => {
it.each([
['omitted', undefined],
['empty', { agents: [] }]
])(
'writes nothing and runs no probe when the allowlist is %s (issue #11641)',
async (_label, options) => {
const home = await createTempHome()
await stubLoginShell(home)
await expect(installManagedHooks(options)).resolves.toEqual({ installers: 0, errors: 0 })
// Why: no agent config home, no ~/.orca install lock, and no GROK_HOME login-shell probe.
expect(await readdir(home)).toEqual([SHELL_NAME])
}
)
it('still rejects an aborted request rather than resolving an empty summary', async () => {
const controller = new AbortController()
controller.abort()
await expect(
installManagedHooks({ signal: controller.signal, agents: [] })
).rejects.toMatchObject({ name: 'AbortError' })
})
it('installs only the allowlisted agent, under the install lock', async () => {
const home = await createTempHome()
await stubLoginShell(home)
await expect(installManagedHooks({ agents: ['claude'] })).resolves.toEqual({
installers: 1,
errors: 0
})
expect((await readdir(home)).sort()).toEqual(['.claude', '.orca', SHELL_NAME, SHELL_RUNS_NAME])
})
})
+6 -1
View File
@@ -79,6 +79,11 @@ export async function installManagedHooks(options?: {
agents?: readonly AgentHookTarget[]
}): Promise<ManagedHookInstallSummary> {
options?.signal?.throwIfAborted()
// Why: empty/omitted allowlist fails closed before any home/host probes.
const agents = options?.agents ?? []
if (agents.length === 0) {
return { installers: 0, errors: 0 }
}
const home = homedir()
const grokHomeDir = await resolveRelayGrokHome(home, options?.signal)
options?.signal?.throwIfAborted()
@@ -96,7 +101,7 @@ export async function installManagedHooks(options?: {
{
grokHomeDir,
signal: options?.signal,
agents: options?.agents
agents
}
)
return {
@@ -707,7 +707,9 @@ describe('remote hook service installers', () => {
it('installs Droid and Copilot when running the aggregate remote installer (issue #7253)', async () => {
const { sftp } = createFakeSftp()
const results = await installRemoteManagedAgentHooks(sftp, '/home/dev')
const results = await installRemoteManagedAgentHooks(sftp, '/home/dev', {
agents: REMOTE_MANAGED_HOOK_INSTALLER_AGENTS
})
const byAgent = new Map(results.map((r) => [r.agent, r.state]))
expect(byAgent.get('droid')).toBe('installed')
expect(byAgent.get('copilot')).toBe('installed')
@@ -727,6 +729,19 @@ describe('remote hook service installers', () => {
}
})
it('fails closed when the agent allowlist is omitted or empty (issue #11641)', async () => {
const { sftp, fs } = createFakeSftp()
await expect(installRemoteManagedAgentHooks(sftp, '/home/dev')).resolves.toEqual([])
await expect(
installRemoteManagedAgentHooks(sftp, '/home/dev', { agents: [] })
).resolves.toEqual([])
// Why: fake SFTP seeds '/' only; no agent config homes or files may appear.
expect([...fs.files.keys()]).toEqual([])
expect([...fs.dirs]).toEqual(['/'])
})
it('stops before the next installer when its relay request is cancelled', async () => {
const controller = new AbortController()
const claudeInstall = vi
@@ -746,7 +761,10 @@ describe('remote hook service installers', () => {
const { sftp } = createFakeSftp()
await expect(
installRemoteManagedAgentHooks(sftp, '/home/dev', { signal: controller.signal })
installRemoteManagedAgentHooks(sftp, '/home/dev', {
signal: controller.signal,
agents: REMOTE_MANAGED_HOOK_INSTALLER_AGENTS
})
).rejects.toMatchObject({ name: 'AbortError' })
expect(claudeInstall).toHaveBeenCalledTimes(1)
expect(openClaudeInstall).not.toHaveBeenCalled()
@@ -26,7 +26,8 @@ export type RemoteManagedHookInstallOptions = {
/** Stops before starting the next installer when the owning relay request
* is cancelled. Individual filesystem mutations remain atomic. */
signal?: AbortSignal
/** Positively detected and enabled agents allowed to mutate config. */
/** Positively detected and enabled agents allowed to mutate config.
* Required for any install: omit/empty fails closed (no config mutation). */
agents?: readonly AgentHookTarget[]
}
@@ -81,10 +82,15 @@ export async function installRemoteManagedAgentHooks(
remoteHome: string,
options?: RemoteManagedHookInstallOptions
): Promise<AgentHookInstallStatus[]> {
// Why: omit/empty allowlist must never mean "install every agent" — that
// recreates config homes for CLIs the user never installed (issue #11641).
const allowedAgents = new Set(options?.agents ?? [])
if (allowedAgents.size === 0) {
return []
}
const results: AgentHookInstallStatus[] = []
const allowedAgents = options?.agents ? new Set(options.agents) : null
for (const [agent, install] of REMOTE_MANAGED_HOOK_INSTALLERS) {
if (allowedAgents && !allowedAgents.has(agent)) {
if (!allowedAgents.has(agent)) {
continue
}
// Why: relay requests can disappear during reconnect; do not start more
@@ -11,7 +11,10 @@ import { RelayDispatcher } from '../../relay/dispatcher'
import { registerWslHookFsHandlers } from '../../relay/wsl-hook-fs-bridge'
import { SshChannelMultiplexer, type MultiplexerTransport } from '../ssh/ssh-channel-multiplexer'
import { createWslHookSftpAdapter } from './wsl-hook-fs-adapter'
import { installRemoteManagedAgentHooks } from './remote-managed-hook-installers'
import {
installRemoteManagedAgentHooks,
REMOTE_MANAGED_HOOK_INSTALLER_AGENTS
} from './remote-managed-hook-installers'
import { WslHookRelayManager } from './wsl-hook-relay-manager'
import { FAILURE_COOLDOWN_BASE_MS, type WslHookRelayManagerDeps } from './wsl-hook-relay-deps'
import {
@@ -122,7 +125,9 @@ describe.skipIf(process.platform === 'win32')(
it('runs the unchanged remote managed hook installers against a WSL guest home', async () => {
const adapter = createWslHookSftpAdapter(harness.mux)
const results = await installRemoteManagedAgentHooks(adapter, home)
const results = await installRemoteManagedAgentHooks(adapter, home, {
agents: REMOTE_MANAGED_HOOK_INSTALLER_AGENTS
})
expect(results.length).toBeGreaterThan(0)
expect(results.every((r) => r.state !== 'error')).toBe(true)
@@ -86,6 +86,40 @@ describe('BrowserDownloadDestinationReservations', () => {
expect(macReservations.reserve('report.csv').filename).toBe('report (1).csv')
})
it('rewrites Windows reserved device basenames', () => {
const reservations = new BrowserDownloadDestinationReservations({
downloadsPath,
pathExists: vi.fn(() => false),
platform: 'win32'
})
expect(reservations.reserve('CON').filename).toBe('_CON')
expect(reservations.reserve('con.txt').filename).toBe('_con.txt')
expect(reservations.reserve('NUL.tar.gz').filename).toBe('_NUL.tar.gz')
expect(reservations.reserve('AUX.json').filename).toBe('_AUX.json')
expect(reservations.reserve('PRN').filename).toBe('_PRN')
expect(reservations.reserve('CLOCK$.csv').filename).toBe('_CLOCK$.csv')
expect(reservations.reserve('CONIN$.txt').filename).toBe('_CONIN$.txt')
expect(reservations.reserve('CONOUT$.log').filename).toBe('_CONOUT$.log')
expect(reservations.reserve('COM1.log').filename).toBe('_COM1.log')
expect(reservations.reserve('COM¹.log').filename).toBe('_COM¹.log')
expect(reservations.reserve('LPT9.csv').filename).toBe('_LPT9.csv')
expect(reservations.reserve('LPT³.csv').filename).toBe('_LPT³.csv')
expect(reservations.reserve('COM10.log').filename).toBe('COM10.log')
expect(reservations.reserve('LPT0.csv').filename).toBe('LPT0.csv')
expect(reservations.reserve('CONSOLE.txt').filename).toBe('CONSOLE.txt')
})
it('preserves Windows device-like names on other platforms', () => {
const reservations = new BrowserDownloadDestinationReservations({
downloadsPath,
pathExists: vi.fn(() => false),
platform: 'linux'
})
expect(reservations.reserve('CON.txt').filename).toBe('CON.txt')
})
it('fails after bounded collision attempts', () => {
const reservations = new BrowserDownloadDestinationReservations({
downloadsPath,
@@ -5,6 +5,8 @@ import { app } from 'electron'
const MAX_BROWSER_DOWNLOAD_COLLISION_ATTEMPTS = 1_000
const WINDOWS_RESERVED_FILENAME_CHARS = new Set(['<', '>', ':', '"', '|', '?', '*'])
const WINDOWS_RESERVED_FILENAME =
/^(?:con|prn|aux|nul|clock\$|conin\$|conout\$|com[1-9¹²³]|lpt[1-9¹²³])(?:\.|$)/iu
export type BrowserDownloadDestination = {
filename: string
@@ -18,7 +20,7 @@ type BrowserDownloadDestinationOptions = {
platform?: NodeJS.Platform
}
function normalizeFilename(filename: string): string {
function normalizeFilename(filename: string, platform: NodeJS.Platform): string {
// Normalize separators first so basename strips paths from any platform.
const normalizedSeparators = filename.replace(/\\/g, '/')
const rawBasename = path.posix.basename(normalizedSeparators).trim()
@@ -32,7 +34,12 @@ function normalizeFilename(filename: string): string {
.join('')
.replace(/[. ]+$/g, '')
.trim()
return safeName || 'download'
if (!safeName) {
return 'download'
}
return platform === 'win32' && WINDOWS_RESERVED_FILENAME.test(safeName)
? `_${safeName}`
: safeName
}
function buildCollisionCandidate(filename: string, suffix: number): string {
@@ -65,7 +72,7 @@ export class BrowserDownloadDestinationReservations {
}
reserve(filename: string): BrowserDownloadDestination {
const safeFilename = normalizeFilename(filename)
const safeFilename = normalizeFilename(filename, this.platform)
const downloadsPath = this.downloadsPath()
for (let attempt = 0; attempt < MAX_BROWSER_DOWNLOAD_COLLISION_ATTEMPTS; attempt += 1) {
@@ -0,0 +1,12 @@
import { expect, test } from './helpers/orca-app'
test.use({ dismissOnboarding: false, seedTestRepo: false })
test.skip(process.platform !== 'win32', 'Fresh-profile fsync regression is Windows-only')
test('fresh Windows profile reaches onboarding @windows-fresh-startup-golden', async ({
orcaPage
}) => {
await expect(orcaPage.getByRole('heading', { name: /Pick your default agent/i })).toBeVisible({
timeout: 30_000
})
})
+1 -1
View File
@@ -252,6 +252,7 @@ export const test = base.extend<OrcaTestFixtures, OrcaWorkerFixtures>({
...(headful ? { ORCA_E2E_HEADFUL: '1' } : { ORCA_E2E_HEADLESS: '1' })
}
})
forwardElectronProcessLogs(app, testInfo)
try {
const resolvedHome = await app.evaluate(({ app }) => app.getPath('home'))
assertElectronResolvedIsolatedHome(resolvedHome, homeIsolation)
@@ -261,7 +262,6 @@ export const test = base.extend<OrcaTestFixtures, OrcaWorkerFixtures>({
await removeUserDataDirAfterShutdown(userDataDir)
throw error
}
forwardElectronProcessLogs(app, testInfo)
await provideFixture(app)
// Why: the Playwright close promise can settle before all Electron and PTY
// descendants are gone in CI; worker teardown then hangs on open handles.