Files
orca/tests/e2e/windows-project-runtime-smoke.spec.ts
T
Brennan Benson 29a5f89b8a Keep same-machine folders with matching Git remotes in separate projects (#32272)
* WIP: same-host same-remote checkouts keep their own project id

* WIP: claim the shared project id per checkout surface; drop the display-only clone split

* WIP: index derived ids by repo in the hydration normalizer

* Resolve workspace project affiliation from current checkout owners

* fix: preserve concrete project ownership across catalogs and operations

* fix: combine recent project selector imports

* fix: enforce canonical catalog reads and preserve search continuity

* fix: scope catalog lint exceptions to renderer files

* Resolve project search ownership before native candidate limits

* Combine duplicate session scope imports

* fix: derive returned history row ownership on the host

* Reconcile full session history replies by content

* Require authoritative project catalogs for terminal snapshots

* Preserve registered project claims and partial history results

* test: check orphaned setup cleanup state without assertions

* fix: align catalog callers and native search fixture contracts

* test: check full repo shapes in runtime routing fixtures

* Fix checked catalog inputs in compiler regression fixtures

* Keep checked PTY fixture repos mutable

* test: align catalog fixtures with canonical readers

* test: check catalog and runtime fixture inputs

* test: check catalog fixtures without outer assertions

* test: follow persistence harness relocation

* Adapt catalog test inputs to integrated main checks

* Test History hashing across cached depth revalidation

* test(settings): expect host-qualified setup removal

* test: capture host split listings and scope relay recovery

* ci: regenerate merge ref

* test: type host-split listing diagnostics
2026-10-26 19:07:06 -07:00

222 lines
8.6 KiB
TypeScript

import { execFileSync } from 'node:child_process'
import { mkdirSync, rmSync, writeFileSync } from 'node:fs'
import { mkdtemp } from 'node:fs/promises'
import os from 'node:os'
import path from 'node:path'
import type { Page } from '@stablyai/playwright-test'
import { selectCanonicalProjects } from '../../src/renderer/src/store/project-host-setup-selector'
import { test, expect } from './helpers/orca-app'
import { waitForSessionReady } from './helpers/store'
const tempRoots: string[] = []
const CLEANUP_RETRY_COUNT = 6
async function removeTempRoot(root: string): Promise<void> {
for (let attempt = 0; attempt < CLEANUP_RETRY_COUNT; attempt += 1) {
try {
rmSync(root, { recursive: true, force: true })
return
} catch (error) {
if (attempt === CLEANUP_RETRY_COUNT - 1) {
throw error
}
// Why: Windows and WSL-backed git probes can release repo handles shortly
// after the Electron fixture closes; retry to keep the smoke idempotent.
await new Promise((resolve) => setTimeout(resolve, 250 * (attempt + 1)))
}
}
}
async function createGitRepo(prefix: string, repoName: string): Promise<string> {
const rootPath = await mkdtemp(path.join(os.tmpdir(), prefix))
tempRoots.push(rootPath)
const repoPath = path.join(rootPath, repoName)
mkdirSync(repoPath, { recursive: true })
execFileSync('git', ['init'], { cwd: repoPath, stdio: 'pipe' })
execFileSync('git', ['config', 'user.email', 'e2e@test.local'], {
cwd: repoPath,
stdio: 'pipe'
})
execFileSync('git', ['config', 'user.name', 'E2E Test'], { cwd: repoPath, stdio: 'pipe' })
writeFileSync(path.join(repoPath, 'README.md'), `# ${repoName}\n`)
execFileSync('git', ['add', 'README.md'], { cwd: repoPath, stdio: 'pipe' })
execFileSync('git', ['commit', '-m', 'Initial commit'], { cwd: repoPath, stdio: 'pipe' })
execFileSync('git', ['branch', '-M', 'main'], { cwd: repoPath, stdio: 'pipe' })
return repoPath
}
async function openRepoSettings(page: Page, repoId: string): Promise<void> {
await page.evaluate((nextRepoId) => {
const state = window.__store!.getState()
state.setSettingsSearchQuery('')
state.openSettingsTarget({ pane: 'repo', repoId: nextRepoId })
state.openSettingsPage()
}, repoId)
await expect(page.getByPlaceholder('Search settings')).toBeVisible({ timeout: 10_000 })
const maybeLaterButton = page.getByRole('button', { name: 'Maybe Later' })
if (await maybeLaterButton.isVisible({ timeout: 1_000 }).catch(() => false)) {
await maybeLaterButton.click()
}
}
async function chooseProjectRuntime(
page: Page,
repoId: string,
runtimeLabel: 'Windows' | 'WSL'
): Promise<void> {
const section = page.locator(`[data-settings-section="repo-${repoId}"]`)
await section.getByRole('radio', { name: runtimeLabel, exact: true }).click()
const applyButton = section.getByRole('button', { name: 'Apply runtime change' })
if (await applyButton.isVisible({ timeout: 1_000 }).catch(() => false)) {
await applyButton.click()
}
}
test.afterAll(async () => {
for (const root of tempRoots.splice(0)) {
await removeTempRoot(root)
}
})
test.describe('Windows project runtime smoke', () => {
test('keeps a Windows-host project and WSL project available side by side', async ({
orcaPage,
testRepoPath
}) => {
test.skip(process.platform !== 'win32', 'Windows project runtime smoke requires Windows')
await waitForSessionReady(orcaPage)
const wsl = await orcaPage.evaluate(async () => ({
available: await window.api.wsl.isAvailable(),
distros: await window.api.wsl.listDistros()
}))
test.skip(!wsl.available || wsl.distros.length === 0, 'WSL distro is required for smoke')
const wslDistro = wsl.distros[0]!
const wslRepoPath = await createGitRepo('orca-e2e-project-runtime-', 'wsl-runtime-project')
const catalog = await orcaPage.evaluate(
async ({ hostRepoPath, wslRepoPath }) => {
const store = window.__store
if (!store) {
throw new Error('window.__store is not available')
}
await window.api.repos.add({ path: wslRepoPath })
await store.getState().fetchRepos({ runtimeEnvironmentId: null, throwOnError: true })
const state = store.getState()
const hostRepo = state.repos.find((repo) => repo.path === hostRepoPath)
const wslRepo = state.repos.find((repo) => repo.path === wslRepoPath)
if (!hostRepo || !wslRepo) {
throw new Error('Expected host and WSL smoke repos to be loaded')
}
return {
repos: state.repos,
endpointProjects: state.endpointProjects,
endpointProjectHostSetups: state.endpointProjectHostSetups,
hostRepo,
wslRepo
}
},
{ hostRepoPath: testRepoPath, wslRepoPath }
)
const projects = selectCanonicalProjects(catalog)
const hostProject = projects.find((project) =>
project.sourceRepoIds.includes(catalog.hostRepo.id)
)
const wslProject = projects.find((project) =>
project.sourceRepoIds.includes(catalog.wslRepo.id)
)
if (!hostProject || !wslProject) {
throw new Error('Expected host and WSL smoke projects to be loaded')
}
const smoke = await orcaPage.evaluate(
async ({ hostProjectId, wslProjectId, hostRepoId, wslRepoId, wslDistro }) => {
const state = window.__store!.getState()
await state.updateProject(
hostProjectId,
{
localWindowsRuntimePreference: { kind: 'windows-host' }
},
'local'
)
await state.updateProject(
wslProjectId,
{
localWindowsRuntimePreference: { kind: 'wsl', distro: wslDistro }
},
'local'
)
const hostWorktrees = await window.api.worktrees.listDetected({ repoId: hostRepoId })
const wslWorktrees = await window.api.worktrees.listDetected({ repoId: wslRepoId })
return {
hostRepoId,
wslRepoId,
wslDistro,
hostWorktreeCount: hostWorktrees.worktrees.length,
wslWorktreeCount: wslWorktrees.worktrees.length
}
},
{
hostProjectId: hostProject.id,
wslProjectId: wslProject.id,
hostRepoId: catalog.hostRepo.id,
wslRepoId: catalog.wslRepo.id,
wslDistro
}
)
expect(smoke.hostWorktreeCount).toBeGreaterThan(0)
expect(smoke.wslWorktreeCount).toBeGreaterThan(0)
await openRepoSettings(orcaPage, smoke.hostRepoId)
const hostSection = orcaPage.locator(`[data-settings-section="repo-${smoke.hostRepoId}"]`)
await expect(hostSection.getByText('Project Runtime')).toBeVisible()
await expect(hostSection.getByText('This project runs on Windows.')).toBeVisible()
await chooseProjectRuntime(orcaPage, smoke.hostRepoId, 'WSL')
await expect(
hostSection.getByText(`This project runs in ${smoke.wslDistro} via WSL.`)
).toBeVisible()
const afterWslCatalog = await orcaPage.evaluate(() => {
const state = window.__store!.getState()
return {
repos: state.repos,
endpointProjects: state.endpointProjects,
endpointProjectHostSetups: state.endpointProjectHostSetups
}
})
const hostAfterWslUiSwitch = selectCanonicalProjects(afterWslCatalog).find((project) =>
project.sourceRepoIds.includes(smoke.hostRepoId)
)?.localWindowsRuntimePreference
expect(hostAfterWslUiSwitch).toEqual({ kind: 'wsl', distro: smoke.wslDistro })
await chooseProjectRuntime(orcaPage, smoke.hostRepoId, 'Windows')
await expect(hostSection.getByText('This project runs on Windows.')).toBeVisible()
const afterWindowsCatalog = await orcaPage.evaluate(() => {
const state = window.__store!.getState()
return {
repos: state.repos,
endpointProjects: state.endpointProjects,
endpointProjectHostSetups: state.endpointProjectHostSetups
}
})
const hostAfterWindowsUiSwitch = selectCanonicalProjects(afterWindowsCatalog).find((project) =>
project.sourceRepoIds.includes(smoke.hostRepoId)
)?.localWindowsRuntimePreference
expect(hostAfterWindowsUiSwitch).toEqual({ kind: 'windows-host' })
await openRepoSettings(orcaPage, smoke.wslRepoId)
const wslSection = orcaPage.locator(`[data-settings-section="repo-${smoke.wslRepoId}"]`)
await expect(wslSection.getByText('Project Runtime')).toBeVisible()
await expect(
wslSection.getByText(`This project runs in ${smoke.wslDistro} via WSL.`)
).toBeVisible()
await orcaPage.evaluate(async (repoId) => {
const repo = window.__store!.getState().repos.find((entry) => entry.id === repoId)
await window.api.repos.removeForHost({ repoId, hostId: repo?.executionHostId ?? 'local' })
}, smoke.wslRepoId)
})
})