Fix stale and race-prone E2E test failures (#7468)

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Brennan Benson 2026-07-05 19:46:49 -07:00 committed by GitHub
parent 0fa8589abf
commit b58c0a42da
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 315 additions and 178 deletions

View File

@ -21,6 +21,10 @@ if (args[0] === 'api' && args.includes('rate_limit')) {
console.log(JSON.stringify({ resources: { core: { limit: 5000, remaining: 5000, reset: 0 }, graphql: { limit: 5000, remaining: 5000, reset: 0 }, search: { limit: 30, remaining: 30, reset: 0 } } }))
process.exit(0)
}
if (args[0] === 'api' && joined.includes('search/issues')) {
console.log(JSON.stringify({ total_count: 0, incomplete_results: false, items: [] }))
process.exit(0)
}
if (args[0] === 'issue' && args[1] === 'list') {
console.log('[]')
process.exit(0)
@ -113,20 +117,18 @@ test('GitHub Tasks drawer recovers when gh stalls on issue details', async ({
return { repoId: repo.id }
}, testRepoPath)
const drawer = orcaPage
.getByRole('dialog')
.filter({ hasText: 'Issue detail fetch that hangs in gh' })
.last()
await expect(drawer).toBeVisible()
// The item detail opens as an inline task-detail page (no longer a modal
// dialog): the item title heading proves it mounted.
const detailHeading = orcaPage.getByRole('heading', {
name: /Issue detail fetch that hangs in gh/
})
await expect(detailHeading).toBeVisible({ timeout: 10_000 })
// Why: this is the user-visible regression signal. Before ghExecFileAsync had
// a default timeout, the drawer's pending details promise never settled and
// the conversation pane stayed stuck in its loading shell. The main GitHub
// details service degrades failed detail fetches to an empty shell, so the
// stable visible proof is that the drawer becomes usable and stops spinning.
await expect(drawer.getByText('No description provided.')).toBeVisible({ timeout: 5_000 })
await expect(drawer.getByText('No comments yet.')).toBeVisible()
await expect(drawer.locator('.animate-spin')).toHaveCount(0)
// Why: the bounded gh timeout rejects instead of hanging, so this terminal
// error text (replacing the body, not a spinner) proves the stall recovered.
await expect(orcaPage.getByText('Unable to load details for this GitHub item.')).toBeVisible({
timeout: 5_000
})
expect(repoId).toBeTruthy()
})

View File

@ -0,0 +1,151 @@
import type { Page } from '@stablyai/playwright-test'
import { expect } from './orca-app'
import type { CommitMessageAiSettings } from '../../../src/shared/types'
// Why: these specs create worktrees via raw `git worktree add`, which bypasses
// Orca's own add/remove path — the one thing that invalidates the main-process
// worktrees.list scan cache (DETECTED_WORKTREE_SCAN_CACHE_TTL_MS = 5_000). So a
// read inside the TTL window can serve a stale miss. No renderer-reachable
// cache-invalidation seam exists without adding product surface, so poll past
// the deterministic 5s boundary. Budget 10s (not 6s, which sits dangerously
// close to a 5s TTL if one scan is slow); the informative message keeps a
// genuinely never-loading worktree failing loudly.
const WORKTREE_CACHE_TTL_POLL_MS = 10_000
/**
* Loads the repo's worktrees into the renderer store and resolves the id of the
* worktree at `targetWorktreePath`, polling past the 5s scan-cache TTL so a
* raw `git worktree add` that Orca never observed still becomes visible.
*/
async function resolveE2eWorktreeId(
page: Page,
repoPath: string,
targetWorktreePath: string
): Promise<string> {
let worktreeId: string | null = null
await expect
.poll(
async () => {
worktreeId = await page.evaluate(
async ({ repoPath, targetWorktreePath }) => {
const store = window.__store
if (!store) {
throw new Error('window.__store is not available')
}
await store.getState().fetchRepos()
const repo = store.getState().repos.find((entry) => entry.path === repoPath)
if (!repo) {
throw new Error(`Seeded E2E repo was not registered: ${repoPath}`)
}
// Why: use the store's own fetch (like loadWorktreesUntilPathsPresent)
// so both TTL workarounds stay behaviorally identical.
await store.getState().fetchWorktrees(repo.id)
const listedWorktrees = store.getState().worktreesByRepo[repo.id] ?? []
const normalize = (value: string): string =>
value.startsWith('/private/var/') ? value.slice('/private'.length) : value
const worktree = listedWorktrees.find(
(entry) => normalize(entry.path) === normalize(targetWorktreePath)
)
return worktree?.id ?? null
},
{ repoPath, targetWorktreePath }
)
return worktreeId
},
{
timeout: WORKTREE_CACHE_TTL_POLL_MS,
message: `E2E worktree was not loaded within the worktree-cache TTL window: ${targetWorktreePath}`
}
)
.not.toBeNull()
if (!worktreeId) {
throw new Error(`E2E worktree was not loaded: ${targetWorktreePath}`)
}
return worktreeId
}
/**
* Shared setup for the Source Control specs: resolves the target worktree
* (surviving the scan-cache TTL race), activates it, optionally applies AI
* commit-message settings, loads its git status, and opens the Source Control
* sidebar tab.
*/
export async function openSourceControlForWorktree(
page: Page,
repoPath: string,
targetWorktreePath: string,
options: { commitMessageAi?: CommitMessageAiSettings } = {}
): Promise<void> {
const worktreeId = await resolveE2eWorktreeId(page, repoPath, targetWorktreePath)
await page.evaluate(
async ({ worktreeId, commitMessageAi }) => {
const store = window.__store
if (!store) {
throw new Error('window.__store is not available')
}
const worktree = Object.values(store.getState().worktreesByRepo)
.flat()
.find((entry) => entry.id === worktreeId)
if (!worktree) {
throw new Error(`E2E worktree disappeared from the store: ${worktreeId}`)
}
store.getState().setActiveWorktree(worktree.id)
if (commitMessageAi) {
await store.getState().updateSettings({ commitMessageAi })
}
const status = await window.api.git.status({ worktreePath: worktree.path })
store.getState().setGitStatus(worktree.id, status)
store.getState().setRightSidebarTab('source-control')
store.getState().setRightSidebarOpen(true)
},
{ worktreeId, commitMessageAi: options.commitMessageAi ?? null }
)
await expect
.poll(
async () =>
page.evaluate(() => {
const state = window.__store?.getState()
return Boolean(state?.rightSidebarOpen && state?.rightSidebarTab === 'source-control')
}),
{ timeout: 5_000 }
)
.toBe(true)
}
/**
* Polls `fetchWorktrees` past the scan-cache TTL until every expected path is
* registered in the store. Used by the Workspace Space git-status spec, which
* adds 60 worktrees via raw git and would otherwise read a stale partial scan.
*/
export async function loadWorktreesUntilPathsPresent(
page: Page,
repoId: string,
expectedPaths: string[]
): Promise<void> {
await expect
.poll(
async () =>
page.evaluate(
async ({ repoId, expectedPaths }) => {
const store = window.__store
if (!store) {
throw new Error('window.__store is not available')
}
await store.getState().fetchWorktrees(repoId)
const registered = new Set(
(store.getState().worktreesByRepo[repoId] ?? []).map((entry) => entry.path)
)
return expectedPaths.every((entry) => registered.has(entry))
},
{ repoId, expectedPaths }
),
{
timeout: WORKTREE_CACHE_TTL_POLL_MS,
message: `Not all worktrees registered within the worktree-cache TTL window (${expectedPaths.length} expected)`
}
)
.toBe(true)
}

View File

@ -241,12 +241,17 @@ test.describe('Onboarding flow', () => {
.toBe(oppositeTheme)
await continueOnboarding(orcaPage)
// Why: the theme Continue persists step 2, then persists *through* any
// skipped optional steps (integrations is skipped when gh is installed,
// windows_terminal off macOS), so lastCompletedStep can land at 2, 3, or 4.
// Key off the settled "theme step committed" lower bound rather than a fixed
// window that assumed integrations always renders.
await expect
.poll(async () => [2, 3].includes((await getOnboardingState(orcaPage)).lastCompletedStep), {
.poll(async () => (await getOnboardingState(orcaPage)).lastCompletedStep, {
timeout: 5_000,
message: 'lastCompletedStep did not advance after second Continue'
message: 'lastCompletedStep did not advance past the theme step after second Continue'
})
.toBe(true)
.toBeGreaterThanOrEqual(2)
await expect
.poll(async () => (await getSettings(orcaPage)).theme, { timeout: 5_000 })
.toBe(oppositeTheme)
@ -414,7 +419,53 @@ test.describe('Onboarding flow', () => {
timeout: 15_000
})
await orcaPage.evaluate(async () => {
await window.__store?.getState().updateSettings({ activeRuntimeEnvironmentId: 'env-e2e' })
const store = window.__store
if (!store) {
throw new Error('window.__store is not available')
}
// Why: after #5071 the server-path add step gates on the registered
// runtime-environment list (store.runtimeEnvironments), not just the
// activeRuntimeEnvironmentId setting. Seed a redacted environment so the
// host option exists and the "on host" add UI renders.
const now = Date.now()
store.getState().setRuntimeEnvironments([
{
id: 'env-e2e',
name: 'E2E Server',
createdAt: now,
updatedAt: now,
lastUsedAt: null,
runtimeId: null,
source: 'manual',
endpoints: [
{
id: 'ws-env-e2e',
kind: 'websocket',
label: 'WebSocket',
endpoint: 'wss://e2e.invalid/ws'
}
],
preferredEndpointId: 'ws-env-e2e'
}
])
// Why: a runtime host is only auto-selectable (health 'available') when it
// has a live, protocol-compatible status; without one it reads
// 'disconnected' and the Add Project dialog falls back to Local Mac.
// runtimeProtocolVersion 3 clears MIN_COMPATIBLE_RUNTIME_SERVER_VERSION.
store.getState().setRuntimeEnvironmentStatus('env-e2e', {
status: {
runtimeId: 'env-e2e-runtime',
rendererGraphEpoch: 0,
graphStatus: 'ready',
authoritativeWindowId: null,
liveTabCount: 0,
liveLeafCount: 0,
runtimeProtocolVersion: 3,
minCompatibleRuntimeClientVersion: 1
},
checkedAt: now
})
await store.getState().updateSettings({ activeRuntimeEnvironmentId: 'env-e2e' })
})
await expect
.poll(async () => (await getSettings(orcaPage)).activeRuntimeEnvironmentId, {
@ -425,10 +476,12 @@ test.describe('Onboarding flow', () => {
await onboardingFooterButton(orcaPage, SKIP_TO_PROJECT_SETUP_BUTTON).click()
await expectAddProjectDialog(orcaPage)
await expect(orcaPage.getByRole('button', { name: /Browse server/i })).toBeVisible()
// The runtime env is selected as the Add Project host and the browse action
// is host-scoped, proving the server project-setup UI is preserved on skip.
await expect(orcaPage.getByText('Existing Git repository or folder on this host')).toBeVisible()
await expect(orcaPage.getByRole('button', { name: /Browse folder/i })).toBeVisible()
await expect(orcaPage.getByRole('button', { name: /Clone from URL/i })).toBeVisible()
await expect(orcaPage.getByRole('button', { name: /Create on server/i })).toBeVisible()
await expect(orcaPage.getByText(/Or enter a server path manually/i)).toBeVisible()
await expect(orcaPage.getByRole('button', { name: /Create new project/i })).toBeVisible()
await expect(onboardingFooterButton(orcaPage, SKIP_TO_PROJECT_SETUP_BUTTON)).toHaveCount(0)
expect((await getOnboardingState(orcaPage)).closedAt).not.toBeNull()
})

View File

@ -58,7 +58,6 @@ test.describe('PR comments sidebar cards view', () => {
await expect(orcaPage.getByText('Needs review · 1')).toBeVisible()
await expect(orcaPage.getByText('Please update this handler before merge.')).toBeVisible()
await expect(orcaPage.getByText('alice')).toBeVisible()
await expect(orcaPage.getByText('Open', { exact: true })).toBeVisible()
await expect(orcaPage.getByText('LGTM on the overall approach.')).toBeVisible()
const openThreadCard = orcaPage.getByTestId('pr-comment-group').filter({

View File

@ -112,6 +112,13 @@ async function typeHangulGanadaSlowly(
// Slow typing: let the async store echo land before the next key.
await page.waitForTimeout(200)
}
// Why: a real IME commits the pending syllable (space/enter) at the end of a
// word, firing compositionend. Without this final commit the controlled
// input stays in composing state, so the component's defer-until-compositionend
// persist never runs. insertText replaces the composing region in place, so it
// finalizes to the same text rather than double-committing.
await session.send('Input.insertText', { text: committed + pending })
}
test.describe('Repository Display Name IME composition', () => {

View File

@ -4,6 +4,7 @@ import os from 'node:os'
import path from 'node:path'
import { test, expect } from './helpers/orca-app'
import { waitForSessionReady } from './helpers/store'
import { openSourceControlForWorktree } from './helpers/worktree-registration'
type E2eWorktree = {
branchName: string
@ -46,67 +47,6 @@ function cleanupWorktree(repoPath: string, worktreePath: string, branchName: str
}
}
async function openSourceControlForWorktree(
page: Parameters<typeof waitForSessionReady>[0],
repoPath: string,
targetWorktreePath: string
): Promise<void> {
await page.evaluate(
async ({ repoPath, targetWorktreePath }) => {
const store = window.__store
if (!store) {
throw new Error('window.__store is not available')
}
const state = store.getState()
await state.fetchRepos()
const repo = store.getState().repos.find((entry) => entry.path === repoPath)
if (!repo) {
throw new Error(`Seeded E2E repo was not registered: ${repoPath}`)
}
const listedWorktrees = await window.api.worktrees.list({ repoId: repo.id })
store.setState((current) => ({
worktreesByRepo: {
...current.worktreesByRepo,
[repo.id]: listedWorktrees
}
}))
const normalizeMacTmpPath = (value: string): string =>
value.startsWith('/private/var/') ? value.slice('/private'.length) : value
const worktree = listedWorktrees.find(
(entry) => normalizeMacTmpPath(entry.path) === normalizeMacTmpPath(targetWorktreePath)
)
if (!worktree) {
throw new Error(
`E2E worktree was not loaded: ${targetWorktreePath}; listed=${listedWorktrees
.map((entry) => entry.path)
.join(', ')}`
)
}
store.getState().setActiveWorktree(worktree.id)
const status = await window.api.git.status({ worktreePath: worktree.path })
store.getState().setGitStatus(worktree.id, status)
store.getState().setRightSidebarOpen(true)
store.getState().setRightSidebarTab('source-control')
},
{ repoPath, targetWorktreePath }
)
await expect
.poll(
async () =>
page.evaluate(() => {
const state = window.__store?.getState()
return Boolean(state?.rightSidebarOpen && state?.rightSidebarTab === 'source-control')
}),
{ timeout: 5_000 }
)
.toBe(true)
}
test.describe('Source Control commit draft persistence', () => {
test('preserves a typed draft when the sidebar tab remounts', async ({
orcaPage,

View File

@ -4,6 +4,7 @@ import os from 'node:os'
import path from 'node:path'
import { test, expect } from './helpers/orca-app'
import { waitForSessionReady } from './helpers/store'
import { openSourceControlForWorktree } from './helpers/worktree-registration'
function createWorktreeWithStagedChange(repoPath: string): {
branchName: string
@ -50,67 +51,16 @@ test.describe('Source Control AI commit messages', () => {
try {
await waitForSessionReady(orcaPage)
await orcaPage.evaluate(
async ({ repoPath, worktreePath: targetWorktreePath, agentCommand: command }) => {
const store = window.__store
if (!store) {
throw new Error('window.__store is not available')
}
const state = store.getState()
await state.fetchRepos()
const repo = store.getState().repos.find((entry) => entry.path === repoPath)
if (!repo) {
throw new Error(`Seeded E2E repo was not registered: ${repoPath}`)
}
const listedWorktrees = await window.api.worktrees.list({ repoId: repo.id })
store.setState((current) => ({
worktreesByRepo: {
...current.worktreesByRepo,
[repo.id]: listedWorktrees
}
}))
const normalizeMacTmpPath = (value: string): string =>
value.startsWith('/private/var/') ? value.slice('/private'.length) : value
const worktree = listedWorktrees.find(
(entry) => normalizeMacTmpPath(entry.path) === normalizeMacTmpPath(targetWorktreePath)
)
if (!worktree) {
throw new Error(
`E2E worktree was not loaded: ${targetWorktreePath}; listed=${listedWorktrees
.map((entry) => entry.path)
.join(', ')}`
)
}
store.getState().setActiveWorktree(worktree.id)
await store.getState().updateSettings({
commitMessageAi: {
enabled: true,
agentId: 'custom',
selectedModelByAgent: {},
selectedThinkingByModel: {},
customPrompt: '',
customAgentCommand: command
}
})
const status = await window.api.git.status({ worktreePath: worktree.path })
store.getState().setGitStatus(worktree.id, status)
store.getState().setRightSidebarTab('source-control')
store.getState().setRightSidebarOpen(true)
},
{ repoPath: testRepoPath, worktreePath, agentCommand }
)
await expect
.poll(
async () =>
orcaPage.evaluate(() => {
const state = window.__store?.getState()
return Boolean(state?.rightSidebarOpen && state?.rightSidebarTab === 'source-control')
}),
{ timeout: 5_000 }
)
.toBe(true)
await openSourceControlForWorktree(orcaPage, testRepoPath, worktreePath, {
commitMessageAi: {
enabled: true,
agentId: 'custom',
selectedModelByAgent: {},
selectedThinkingByModel: {},
customPrompt: '',
customAgentCommand: agentCommand
}
})
const textarea = orcaPage.getByRole('textbox', { name: 'Commit message' })
await expect(textarea).toBeVisible({ timeout: 10_000 })

View File

@ -413,21 +413,6 @@ async function pressShiftedRussianLayoutKey(page: Page): Promise<{
})
}
// Why: handleRequestClosePane pops a "Close Terminal?" dialog when the pane
// reports a running child process. Under E2E, a freshly split pane's
// proc.process is briefly unset so the check returns true spuriously. Click
// Close when the dialog appears so the test's chord-routing assertion stays
// deterministic; no-op when it doesn't.
async function confirmCloseDialogIfShown(page: Page): Promise<void> {
const confirmButton = page.getByRole('button', { name: 'Close', exact: true })
try {
await confirmButton.waitFor({ state: 'visible', timeout: 500 })
await confirmButton.click()
} catch {
// Dialog did not appear — pane closed directly.
}
}
async function pressAndExpectWrite(
page: Page,
app: ElectronApplication,
@ -460,6 +445,34 @@ const mod = isMac ? 'Meta' : 'Control'
const splitVerticalChord = isMac ? `${mod}+d` : `${mod}+Shift+d`
const splitHorizontalChord = isMac ? `${mod}+Shift+d` : 'Alt+Shift+d'
// Why: a freshly split pane can transiently still report a running child, so
// poll for the confirm dialog and pane-count settling instead of a fixed wait.
async function closeActivePaneAndSettle(page: Page, expectedCount: number): Promise<void> {
await focusActiveTerminal(page)
await page.keyboard.press(`${mod}+w`)
// The "Stop running command?" confirm surfaces a "Stop and Close" action when
// the pane still reports a running child.
const confirmButton = page.getByRole('button', { name: /Stop and Close/i })
await expect
.poll(
async () => {
if (await confirmButton.isVisible().catch(() => false)) {
// Why: surface a click failure so a real actionability/strict-mode
// error isn't hidden behind the generic pane-count timeout.
await confirmButton.click().catch((err) => {
console.warn('closeActivePaneAndSettle: confirm click failed', err)
})
}
return countVisibleTerminalPanes(page)
},
{
timeout: 10_000,
message: `Expected ${expectedCount} visible terminal panes after close`
}
)
.toBe(expectedCount)
}
// Why: serial mode is load-bearing. Tests mutate shared Electron app state
// (pane layout, terminal buffer, expand toggle) and the pty:write spy log is
// a single main-process singleton. Parallel execution would interleave chord
@ -749,6 +762,9 @@ test.describe('Terminal Shortcuts', () => {
await focusActiveTerminal(orcaPage)
await orcaPage.keyboard.press(splitVerticalChord)
await waitForPaneCount(orcaPage, panesBeforeSplit + 1)
// Why: ensure the new split pane's PTY is actually bound before we later
// close it, so the close cycle can't race an in-progress split.
await waitForActivePanePtyId(orcaPage)
// Cmd/Ctrl+] and Cmd/Ctrl+[ cycle focus (no pane-count change).
await focusActiveTerminal(orcaPage)
@ -781,25 +797,15 @@ test.describe('Terminal Shortcuts', () => {
.toBe(false)
// Cmd/Ctrl+W closes the active split pane (not the whole tab: >1 pane).
// Why: the close handler checks hasChildProcesses async; a freshly
// spawned pane can transiently report a running child (node-pty's
// proc.process lags the spawn), which surfaces a confirmation dialog
// instead of closing immediately. Confirm it if it appears — the test
// only needs to prove the chord routed to the close handler.
await focusActiveTerminal(orcaPage)
await orcaPage.keyboard.press(`${mod}+w`)
await confirmCloseDialogIfShown(orcaPage)
await waitForPaneCount(orcaPage, panesBeforeSplit)
await closeActivePaneAndSettle(orcaPage, panesBeforeSplit)
// Split horizontally (chord varies by platform — see splitHorizontalChord).
const panesBeforeHSplit = await countVisibleTerminalPanes(orcaPage)
await focusActiveTerminal(orcaPage)
await orcaPage.keyboard.press(splitHorizontalChord)
await waitForPaneCount(orcaPage, panesBeforeHSplit + 1)
await focusActiveTerminal(orcaPage)
await orcaPage.keyboard.press(`${mod}+w`)
await confirmCloseDialogIfShown(orcaPage)
await waitForPaneCount(orcaPage, panesBeforeHSplit)
await waitForActivePanePtyId(orcaPage)
await closeActivePaneAndSettle(orcaPage, panesBeforeHSplit)
// Cmd/Ctrl+F toggles the search overlay.
await focusActiveTerminal(orcaPage)

View File

@ -3,13 +3,19 @@ import { mkdtempSync, realpathSync, rmSync } from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { test, expect } from './helpers/orca-app'
import { loadWorktreesUntilPathsPresent } from './helpers/worktree-registration'
test.describe('Workspace Space git status checks', () => {
test('checks every scanned deletable row, including rows after the first 50', async ({
orcaPage,
testRepoPath
}) => {
const worktreeParent = mkdtempSync(path.join(os.tmpdir(), 'orca-space-git-status-'))
// Why: on symlinked tmpdirs (/var→/private/var on macOS, /tmp→… on CI) Orca
// registers worktrees under their realpath, so the parent must be canonical
// before `git worktree add` or the recorded paths won't match and rows drop.
const worktreeParent = realpathSync(
mkdtempSync(path.join(os.tmpdir(), 'orca-space-git-status-'))
)
const worktreePaths = Array.from({ length: 60 }, (_, index) =>
path.join(worktreeParent, `worktree-${index}`)
)
@ -25,6 +31,22 @@ test.describe('Workspace Space git status checks', () => {
realpathSync(worktreePath)
)
const repoId = await orcaPage.evaluate((testRepoPath) => {
const store = window.__store
if (!store) {
throw new Error('Expected e2e store to be exposed')
}
const repo = store.getState().repos.find((item) => item.path === testRepoPath)
if (!repo) {
throw new Error('Expected test repo to be loaded')
}
return repo.id
}, testRepoPath)
// Why: the 60 worktrees were added via raw git, so poll past the 5s scan
// cache TTL until every path registers before deriving the space rows.
await loadWorktreesUntilPathsPresent(orcaPage, repoId, registeredWorktreePaths)
await orcaPage.evaluate(
async ({ testRepoPath, worktreePaths }) => {
const store = window.__store
@ -37,7 +59,6 @@ test.describe('Workspace Space git status checks', () => {
if (!repo) {
throw new Error('Expected test repo to be loaded')
}
await initialState.fetchWorktrees(repo.id)
await window.api.git.status({ worktreePath: worktreePaths[0] })
const state = store.getState()

View File

@ -263,9 +263,12 @@ test.describe('Create Workspace', () => {
}
)
ipcMain.removeHandler('worktrees:resolvePrBase')
// Why: the fixture repo has no remote and its default branch name
// depends on the host's git init.defaultBranch (main vs master), so
// resolve the PR base to HEAD, which always exists regardless.
ipcMain.handle('worktrees:resolvePrBase', () => {
counters.__smartResolvePrBaseCount += 1
return { baseBranch: 'origin/main' }
return { baseBranch: 'HEAD' }
})
},
{ title, url }
@ -299,6 +302,10 @@ test.describe('Create Workspace', () => {
})
await expect(orcaPage.getByRole('option', { name: url })).toHaveCount(0)
await expect(orcaPage.getByText('Linked PR #2049')).toBeVisible()
// Why: quick create reuses the single GitHub lookup from typing (no
// redundant re-fetch), and since #5733 ("Create PR worktrees from the PR
// head") it resolves the PR start point exactly once at submit time — so
// the base resolves once here rather than being skipped.
await expect
.poll(() =>
electronApp.evaluate(() => {
@ -312,7 +319,7 @@ test.describe('Create Workspace', () => {
}
})
)
.toEqual({ githubLookupCount: 1, resolvePrBaseCount: 0 })
.toEqual({ githubLookupCount: 1, resolvePrBaseCount: 1 })
} finally {
await orcaPage
.evaluate(() => {
@ -358,9 +365,10 @@ test.describe('Create Workspace', () => {
})
)
ipcMain.removeHandler('worktrees:resolvePrBase')
// Why: the fixture repo's default branch is master and has no
// remote; resolve the PR base to a ref that actually exists.
ipcMain.handle('worktrees:resolvePrBase', () => ({ baseBranch: 'master' }))
// Why: the fixture repo has no remote and its default branch name
// depends on the host's git init.defaultBranch (main vs master), so
// resolve the PR base to HEAD, which always exists regardless.
ipcMain.handle('worktrees:resolvePrBase', () => ({ baseBranch: 'HEAD' }))
},
{ title, url }
)