Files
orca/tests/e2e/worktree-lineage-state.ts
T
Jinwoo HongandClaude 78bad24d33 test(e2e): nine sidebar specs create workspaces and links through main so they pass with the switch on (#30667)
* test(e2e): create reveal-spec workspaces through main so they pass with the sidebar switch on

The agent-send picker and host-section reveal specs injected fake repos and
workspaces into the window store. With ORCA_SIDEBAR_INPUTS=main the sidebar
lists main's rows, so those workspaces never appeared. The SSH host is now an
unreachable target seeded into the stopped profile, and the paired-server host
is a real headless orca serve host; pins, lineage and notes go through main.

Co-Authored-By: Claude <noreply@anthropic.com>

* test(e2e): type the seeded hosts as execution host ids

Co-Authored-By: Claude <noreply@anthropic.com>

* test(e2e): poll the reveal specs' page setup instead of awaiting it across CDP

An evaluate that awaits a preload promise sometimes fails with "Resulting
promise was garbage collected" in switch-on runs. evaluatePolled runs the work
in the page, polls its outcome, and still fails on a rejected or failed write.

Co-Authored-By: Claude <noreply@anthropic.com>

* test(e2e): parent reorder spec creates its workspaces and links through main

Co-Authored-By: Claude <noreply@anthropic.com>

* test(e2e): lineage specs create their workspaces and links through main

seedLineageScenario names and orders the two seeded workspaces and links them
with assignWorktreeParent. Extra families and surrounding rows come from a
fresh repo with real worktrees; pins and stale instances are main writes.

Co-Authored-By: Claude <noreply@anthropic.com>

* test(e2e): default-branch and downward-drag specs create their workspaces through main

Co-Authored-By: Claude <noreply@anthropic.com>

* test(e2e): reveal and active-delete specs create workspaces through main

The 60-row reveal, the folder-workspace reveal and both active-delete tests
now use real workspaces and folder workspaces added through main, ordered with
metadata writes. The deletes go through the real removal path, so the row
removal sampler waits by time rather than a frame count.

Co-Authored-By: Claude <noreply@anthropic.com>

* test(e2e): address review on the switch-on spec follow-up

writeWorkspaceMeta now fails unless main lists the written values; the sleep
test's parent is a real worktree instead of a store-faked one; the legacy
inline-lineage case stays window-only and is skipped with the switch on;
the parent-reorder collapse key is built from the row's host.

Co-Authored-By: Claude <noreply@anthropic.com>

* test(e2e): read-back reports an unlisted workspace and compares values structurally

Co-Authored-By: Claude <noreply@anthropic.com>

* test(e2e): wait for the added repo before showing its worktrees

updateRepo returns false without writing when the store does not list the repo
yet. On CI the add's own refresh superseded the helper's fetch, so the visibility
write was skipped and every discovered worktree stayed hidden. The helper now
waits until the store lists the repo and fails if the write is refused.

Co-Authored-By: Claude <noreply@anthropic.com>

* test(e2e): name the repo wait in its failure message

Co-Authored-By: Claude <noreply@anthropic.com>

* test(e2e): size the setup poll budget to the number of main writes

evaluatePolled waited a fixed 10 s, sized for one write. Linking 53 children takes
about 8 s locally and 36 folder workspaces about 4.5 s, so CI ran past it ("page
work never settled"). Bulk setup now passes a budget of 10 s plus 500 ms per write.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-19 06:29:17 -04:00

197 lines
6.1 KiB
TypeScript

import type { Page } from '@stablyai/playwright-test'
import { linkWorkspaces, writeWorkspaceMeta } from './helpers/repo-with-workspaces'
export type LineageScenario = {
parentId: string
childId: string
}
export async function seedLineageScenario(
page: Page,
options: { inlineOnly?: boolean; preserveGrouping?: boolean } = {}
): Promise<LineageScenario> {
const family = await page.evaluate((preserveGrouping) => {
const store = window.__store
if (!store) {
throw new Error('window.__store is not available')
}
const state = store.getState()
state.setActiveView('terminal')
state.setSidebarOpen(true)
if (!preserveGrouping) {
state.setGroupBy('none')
}
state.setSortBy('recent')
// Why: these specs assert lineage structure, not the user's persisted
// sidebar filters. Make the seeded child render even when it has no live PTY.
state.setShowActiveOnly(false)
state.setShowSleepingWorkspaces(true)
state.setHideDefaultBranchWorkspace(false)
state.setFilterRepoIds([])
const worktrees = Object.values(state.worktreesByRepo)
.flat()
.filter((worktree) => !worktree.isArchived)
if (worktrees.length < 2) {
throw new Error('Worktree lineage E2E needs at least two worktrees')
}
const [parent, child] = worktrees
if (!parent.instanceId || !child.instanceId) {
throw new Error('Worktree lineage E2E needs instance-stamped worktrees')
}
return {
parentId: parent.id,
childId: child.id,
parentInstanceId: parent.instanceId,
childInstanceId: child.instanceId
}
}, options.preserveGrouping ?? false)
const { parentId, childId } = family
await writeWorkspaceMeta(page, [
{ worktreeId: parentId, updates: { displayName: 'E2E lineage parent', sortOrder: 0 } },
{ worktreeId: childId, updates: { displayName: 'E2E lineage child', sortOrder: 1 } }
])
if (!options.inlineOnly) {
await linkWorkspaces(page, [{ childId, parentId }])
}
if (options.inlineOnly) {
// Window-only on purpose: the records' inline link with no side map, which main cannot produce.
await page.evaluate(({ parentId, childId, parentInstanceId, childInstanceId }) => {
const lineage = {
worktreeId: childId,
worktreeInstanceId: childInstanceId,
parentWorktreeId: parentId,
parentWorktreeInstanceId: parentInstanceId,
origin: 'manual' as const,
capture: { source: 'manual-action' as const, confidence: 'explicit' as const },
createdAt: Date.now()
}
window.__store!.setState((current) => ({
worktreesByRepo: Object.fromEntries(
Object.entries(current.worktreesByRepo).map(([repoId, repoWorktrees]) => [
repoId,
repoWorktrees.map((worktree) =>
worktree.id === parentId
? {
...worktree,
parentWorktreeId: null,
childWorktreeIds: [childId],
lineage: null
}
: worktree.id === childId
? { ...worktree, parentWorktreeId: parentId, childWorktreeIds: [], lineage }
: worktree
)
])
),
worktreeLineageById: {}
}))
}, family)
}
await page.evaluate(
(parentId) => window.__store!.getState().setActiveWorktree(parentId),
parentId
)
return { parentId, childId }
}
export async function seedWorkspaceAgentStatus(
page: Page,
worktreeId: string,
label: string
): Promise<string> {
return page.evaluate(
({ worktreeId, label }) => {
const store = window.__store
if (!store) {
throw new Error('window.__store is not available')
}
const state = store.getState()
if (!state.worktreeCardProperties.includes('inline-agents')) {
state.setWorktreeCardProperties([...state.worktreeCardProperties, 'inline-agents'])
}
if ((state.tabsByWorktree[worktreeId] ?? []).length === 0) {
state.createTab(worktreeId)
}
const next = store.getState()
const tab = next.tabsByWorktree[worktreeId]?.[0]
if (!tab) {
throw new Error(`Worktree lineage E2E failed to create a ${label} workspace tab`)
}
const prompt = `LINEAGE_${label}_AGENT_${Date.now()}`
const leafId = crypto.randomUUID()
const now = Date.now()
next.setAgentStatus(
`${tab.id}:${leafId}`,
{ state: 'working', prompt, agentType: 'codex' },
'codex',
{ updatedAt: now, stateStartedAt: now }
)
return prompt
},
{ worktreeId, label }
)
}
export async function seedWorkspaceLiveTerminal(page: Page, worktreeId: string): Promise<string> {
return page.evaluate((worktreeId) => {
const store = window.__store
if (!store) {
throw new Error('window.__store is not available')
}
const state = store.getState()
if ((state.tabsByWorktree[worktreeId] ?? []).length === 0) {
state.createTab(worktreeId)
}
const next = store.getState()
const tab = next.tabsByWorktree[worktreeId]?.[0]
if (!tab) {
throw new Error('Worktree lineage E2E failed to create a live terminal tab')
}
next.dropAgentStatusByWorktree(worktreeId)
store.setState((current) => ({
ptyIdsByTabId: {
...current.ptyIdsByTabId,
[tab.id]: [`e2e-live-pty-${Date.now()}`]
},
browserTabsByWorktree: {
...current.browserTabsByWorktree,
[worktreeId]: []
}
}))
return tab.id
}, worktreeId)
}
export async function markWorkspaceTerminalSlept(
page: Page,
args: { worktreeId: string; tabId: string }
): Promise<void> {
await page.evaluate(({ worktreeId, tabId }) => {
const store = window.__store
if (!store) {
throw new Error('window.__store is not available')
}
store.getState().dropAgentStatusByWorktree(worktreeId)
store.setState((current) => ({
ptyIdsByTabId: {
...current.ptyIdsByTabId,
[tabId]: []
},
browserTabsByWorktree: {
...current.browserTabsByWorktree,
[worktreeId]: []
}
}))
}, args)
}