From c869891b3749b1808dc99b083a118dce4aff3e56 Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Fri, 10 Jul 2026 22:43:12 -0700 Subject: [PATCH] test(e2e): defer large repo cleanup until shutdown (#8228) --- tests/e2e/helpers/orca-app.ts | 27 +++++++- .../source-control-large-file-count.spec.ts | 63 ++++++++++++++----- 2 files changed, 74 insertions(+), 16 deletions(-) diff --git a/tests/e2e/helpers/orca-app.ts b/tests/e2e/helpers/orca-app.ts index eb49dfeba..4d7f1b9ef 100644 --- a/tests/e2e/helpers/orca-app.ts +++ b/tests/e2e/helpers/orca-app.ts @@ -32,6 +32,7 @@ import { createSeededTestRepo, isValidGitRepo } from './seeded-test-repo' type OrcaTestFixtures = { electronApp: ElectronApplication + registerPostElectronShutdownCleanup: (cleanup: () => Promise) => void sharedPage: Page orcaPage: Page // Why: every fresh userData dir paints the first-launch onboarding overlay @@ -145,12 +146,36 @@ export const test = base.extend({ { scope: 'worker' } ], + // Why: Windows keeps watched worktrees locked until Electron and its + // detached test daemons exit. Tests register fixture cleanup here so it runs + // after electronApp teardown instead of masking the real assertion failure. + registerPostElectronShutdownCleanup: [ + // oxlint-disable-next-line no-empty-pattern -- Playwright fixture callbacks require object destructuring here. + async ({}, provideFixture) => { + const cleanups: (() => Promise)[] = [] + await provideFixture((cleanup) => cleanups.push(cleanup)) + for (const cleanup of cleanups.toReversed()) { + await cleanup() + } + }, + { scope: 'test' } + ], + // Test-scoped: one Electron app per test electronApp: async ( - { dismissOnboarding, launchEnv, orcaAppExtraEnv, orcaAppExtraArgs }, + { + dismissOnboarding, + launchEnv, + orcaAppExtraEnv, + orcaAppExtraArgs, + registerPostElectronShutdownCleanup + }, provideFixture, testInfo ) => { + // Establish fixture ordering: registered path cleanup must run only after + // this Electron fixture has released watchers, terminals, and daemons. + void registerPostElectronShutdownCleanup const mainPath = path.join(process.cwd(), 'out', 'main', 'index.js') const userDataDir = mkdtempSync(path.join(os.tmpdir(), 'orca-e2e-userdata-')) diff --git a/tests/e2e/source-control-large-file-count.spec.ts b/tests/e2e/source-control-large-file-count.spec.ts index 2eff3d284..c54a9651c 100644 --- a/tests/e2e/source-control-large-file-count.spec.ts +++ b/tests/e2e/source-control-large-file-count.spec.ts @@ -102,17 +102,36 @@ async function addAndActivateRepo(orcaPage: Page, repoPath: string): Promise orcaPage.evaluate(() => window.__store?.getState().rightSidebarTab)) .toBe('source-control') + await expect(orcaPage.getByRole('button', { name: 'Filter files by name' })).toBeVisible() return worktreeId } +async function unregisterLargeFileCountRepos( + orcaPage: Page, + repoPaths: readonly string[] +): Promise { + // Why: remove disposable projects through the product so their terminals + // and watcher subscriptions begin shutting down before Electron teardown. + for (const repoPath of repoPaths) { + await orcaPage.evaluate(async (pathToRepo) => { + const store = window.__store + const repo = store?.getState().repos.find((entry) => entry.path === pathToRepo) + if (repo) { + await store?.getState().removeProject(repo.id) + } + }, repoPath) + } +} + /** * Drives the exact pipeline the panel's poll uses (api.git.status → * setGitStatus) so results are deterministic even while the built-in 3s poll @@ -242,7 +261,8 @@ test.describe('Source Control large file count (#8013)', () => { test('thousands of untracked files under the status cap stay responsive', async ({ orcaPage, - electronApp + electronApp, + registerPostElectronShutdownCleanup }) => { test.setTimeout(600_000) const untrackedFiles = Number(process.env.ORCA_LARGE_FILE_COUNT ?? '9500') @@ -255,6 +275,7 @@ test.describe('Source Control large file count (#8013)', () => { untrackedFiles, untrackedFileBytes }) + registerPostElectronShutdownCleanup(() => removeLargeFileCountRepo(fixture.repoPath)) try { await waitForSessionReady(orcaPage) const worktreeId = await addAndActivateRepo(orcaPage, fixture.repoPath) @@ -286,17 +307,19 @@ test.describe('Source Control large file count (#8013)', () => { ) } } finally { - await removeLargeFileCountRepo(fixture.repoPath) + await unregisterLargeFileCountRepos(orcaPage, [fixture.repoPath]) } }) test('thousands of modified tracked files under the status cap stay responsive', async ({ orcaPage, - electronApp + electronApp, + registerPostElectronShutdownCleanup }) => { test.setTimeout(600_000) const modifiedFiles = Number(process.env.ORCA_LARGE_FILE_COUNT ?? '5000') const fixture = createLargeFileCountRepo({ trackedFiles: modifiedFiles, modifiedFiles }) + registerPostElectronShutdownCleanup(() => removeLargeFileCountRepo(fixture.repoPath)) try { await waitForSessionReady(orcaPage) const worktreeId = await addAndActivateRepo(orcaPage, fixture.repoPath) @@ -319,17 +342,19 @@ test.describe('Source Control large file count (#8013)', () => { expect(measurement.renderedRows).toBeLessThan(MAX_MOUNTED_ROWS) expect(measurement.maxLagMs).toBeLessThan(MAX_EVENT_LOOP_LAG_MS) } finally { - await removeLargeFileCountRepo(fixture.repoPath) + await unregisterLargeFileCountRepos(orcaPage, [fixture.repoPath]) } }) test('a change set over the status cap degrades to the too-many-changes state', async ({ orcaPage, - electronApp + electronApp, + registerPostElectronShutdownCleanup }) => { test.setTimeout(600_000) const untrackedFiles = DEFAULT_GIT_STATUS_LIMIT + 1_000 const fixture = createLargeFileCountRepo({ trackedFiles: 100, untrackedFiles }) + registerPostElectronShutdownCleanup(() => removeLargeFileCountRepo(fixture.repoPath)) try { await waitForSessionReady(orcaPage) const worktreeId = await addAndActivateRepo(orcaPage, fixture.repoPath) @@ -361,11 +386,14 @@ test.describe('Source Control large file count (#8013)', () => { ) expect(hugeState).not.toBeNull() } finally { - await removeLargeFileCountRepo(fixture.repoPath) + await unregisterLargeFileCountRepos(orcaPage, [fixture.repoPath]) } }) - test('untracked line-stat cache stays effective above 2,048 files', async ({ orcaPage }) => { + test('untracked line-stat cache stays effective above 2,048 files', async ({ + orcaPage, + registerPostElectronShutdownCleanup + }) => { test.setTimeout(600_000) // Why: the untracked line-stat cache historically capped at 2,048 entries // with FIFO eviction, so a sequential scan over more files evicted every @@ -380,6 +408,7 @@ test.describe('Source Control large file count (#8013)', () => { untrackedFiles: 2_000, untrackedFileBytes: fileBytes }) + registerPostElectronShutdownCleanup(() => removeLargeFileCountRepo(smallRepo.repoPath)) let largeRepo: ReturnType | null = null try { largeRepo = createLargeFileCountRepo({ @@ -387,6 +416,8 @@ test.describe('Source Control large file count (#8013)', () => { untrackedFiles: 4_000, untrackedFileBytes: fileBytes }) + const largeRepoPath = largeRepo.repoPath + registerPostElectronShutdownCleanup(() => removeLargeFileCountRepo(largeRepoPath)) await waitForSessionReady(orcaPage) const warmRescanPerFileMs = async (repoPath: string, files: number): Promise => { @@ -407,20 +438,22 @@ test.describe('Source Control large file count (#8013)', () => { ) expect(largePerFileMs).toBeLessThan(smallPerFileMs * 2) } finally { - await removeLargeFileCountRepo(smallRepo.repoPath) - if (largeRepo) { - await removeLargeFileCountRepo(largeRepo.repoPath) - } + await unregisterLargeFileCountRepos(orcaPage, [ + smallRepo.repoPath, + ...(largeRepo ? [largeRepo.repoPath] : []) + ]) } }) test('a large clean repo (tracked files only) loads instantly', async ({ orcaPage, - electronApp + electronApp, + registerPostElectronShutdownCleanup }) => { test.setTimeout(600_000) const trackedFiles = Number(process.env.ORCA_LARGE_FILE_COUNT ?? '15000') const fixture = createLargeFileCountRepo({ trackedFiles }) + registerPostElectronShutdownCleanup(() => removeLargeFileCountRepo(fixture.repoPath)) try { await waitForSessionReady(orcaPage) const worktreeId = await addAndActivateRepo(orcaPage, fixture.repoPath) @@ -440,7 +473,7 @@ test.describe('Source Control large file count (#8013)', () => { expect(measurement.entryCount).toBe(0) expect(measurement.maxLagMs).toBeLessThan(MAX_EVENT_LOOP_LAG_MS) } finally { - await removeLargeFileCountRepo(fixture.repoPath) + await unregisterLargeFileCountRepos(orcaPage, [fixture.repoPath]) } }) })