mirror of
https://github.com/stablyai/orca.git
synced 2026-10-24 08:03:58 +00:00
#14397 split `shared/types.ts` into 46 per-domain modules but kept the path as a re-export barrel so the import sites did not have to change. This removes the barrel: every consumer now imports from the module that actually declares the type, and `src/shared/types.ts` is deleted. Barrels hide where a type lives, make every consumer look like it depends on the whole domain, and let an unrelated edit invalidate a module that ~2,000 files transitively import. 2,323 import declarations across 2,321 files. Rewritten mechanically: each specifier was resolved to an absolute path via the TypeScript AST and recomputed, rather than string-substituted, so alias forms (`@/../../shared/ types`) and per-specifier `type` modifiers survive. Four cases the mechanical pass had to handle, each found by a gate rather than by reading the diff: - Modules inside `src/shared` import the barrel as `./types`, not `shared/types`. A pre-filter on the latter string skipped 176 of them and left imports dangling at a deleted file, which surfaced as confusing `Property 'x' is optional in type 'Repo' but required in Pick<Repo, ...>` errors rather than "module not found". - The barrel RENAMED one type on the way through (`WorkspaceSource as WorkspaceCreateTelemetrySource`), so the original name in the owning module has to be re-aliased at each consumer. - Three test files put `;(globalThis as ...)` on the line after the import. TypeScript parses that `;` as the import statement's terminator, so replacing through `statement.getEnd()` deletes it and breaks ASI. The rewrite now stops at the module specifier. - A file that already imported directly from a module got a SECOND import from it, because the barrel re-exported those same names — which trips `import/no-duplicates` under `--deny-warnings`. A post-pass merges declarations sharing a specifier and type-only-ness; the `import type` plus `import` pair from one module is left alone, since that form is allowed. Splitting one barrel import into several genuinely adds lines, which pushed `terminal-layout-pty-ownership.ts` to 301 counted lines: its 107-character import must wrap, and neither local type collapses onto one line (101 and 116 characters). Rather than contort a type declaration to fit a line budget, `collectLeafIds` and `pruneLeaves` move to `terminal-pane-layout-tree.ts` — they are pure structural operations on the layout tree and independent of PTY ownership. `visible-worktrees.ts` similarly loses its own mini-barrel re-export of `isDefaultBranchWorkspace`, with the four real consumers repointed at the declaring module. No `max-lines` bypass added. Verified: cold `tsc --noEmit` green on node, cli, and web (buildinfo deleted first — these projects are `composite: true` and reuse stale caches); the full `pnpm lint` green, not just bare oxlint — the narrower local check is what let the duplicate imports reach CI; max-lines ratchet OK at 344.
265 lines
9.5 KiB
TypeScript
265 lines
9.5 KiB
TypeScript
import { randomUUID } from 'node:crypto'
|
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
import path from 'node:path'
|
|
import type { ElectronApplication, Page } from '@stablyai/playwright-test'
|
|
import type { TerminalLayoutSnapshot } from '../../src/shared/terminal-tab-types'
|
|
import { DEFAULT_LOCAL_ORCA_PROFILE_ID } from '../../src/shared/orca-profiles'
|
|
import { test, expect } from './helpers/orca-app'
|
|
import { attachRepoAndOpenTerminal, createRestartSession } from './helpers/orca-restart'
|
|
import { stageNodeScriptForTerminal } from './helpers/run-node-script-in-terminal'
|
|
import {
|
|
execInTerminal,
|
|
waitForActivePanePtyId,
|
|
waitForActiveTerminalManager,
|
|
waitForPaneCount,
|
|
waitForTerminalOutput
|
|
} from './helpers/terminal'
|
|
import {
|
|
ensureTerminalVisible,
|
|
getActiveTabId,
|
|
getActiveWorktreeId,
|
|
waitForSessionReady
|
|
} from './helpers/store'
|
|
import { TEST_REPO_PATH_FILE } from './global-setup'
|
|
|
|
type PersistedData = {
|
|
workspaceSession?: {
|
|
activeTabId?: string | null
|
|
terminalLayoutsByTabId?: Record<string, TerminalLayoutSnapshot>
|
|
}
|
|
}
|
|
|
|
type RendererOwnershipSnapshot = {
|
|
paneCount: number
|
|
xtermCount: number
|
|
rootLeafCount: number
|
|
ptyBindingCount: number
|
|
uniquePtyCount: number
|
|
}
|
|
|
|
function streamingTuiSource(marker: string): string {
|
|
return `
|
|
let frame = 0
|
|
process.stdout.write('\\x1b[?1049h\\x1b[?25l')
|
|
setInterval(() => {
|
|
frame += 1
|
|
const lines = [${JSON.stringify(marker)} + ' frame ' + String(frame).padStart(6, '0')]
|
|
for (let row = 1; row <= 32; row += 1) {
|
|
const width = 8 + ((frame + row * 7) % 48)
|
|
lines.push(String(row).padStart(2, '0') + ' OpenCode tool output ' + '#'.repeat(width))
|
|
}
|
|
process.stdout.write('\\x1b[?2026h\\x1b[H' + lines.join('\\r\\n') + '\\x1b[J\\x1b[?2026l')
|
|
}, 32)
|
|
`.trim()
|
|
}
|
|
|
|
function persistedDataPath(userDataDir: string): string {
|
|
return path.join(userDataDir, 'profiles', DEFAULT_LOCAL_ORCA_PROFILE_ID, 'orca-data.json')
|
|
}
|
|
|
|
function seedDuplicatePtyOwnership(userDataDir: string): void {
|
|
const dataPath = persistedDataPath(userDataDir)
|
|
const data = JSON.parse(readFileSync(dataPath, 'utf8')) as PersistedData
|
|
const session = data.workspaceSession
|
|
const tabId = session?.activeTabId
|
|
const layout = tabId ? session?.terminalLayoutsByTabId?.[tabId] : undefined
|
|
const retainedLeafId = layout?.activeLeafId
|
|
const ptyId = retainedLeafId ? layout?.ptyIdsByLeafId?.[retainedLeafId] : undefined
|
|
if (!session?.terminalLayoutsByTabId || !tabId || !layout || !retainedLeafId || !ptyId) {
|
|
throw new Error('Persisted terminal ownership was unavailable for duplicate-layout seeding')
|
|
}
|
|
|
|
const duplicateLeafId = randomUUID()
|
|
session.terminalLayoutsByTabId[tabId] = {
|
|
...layout,
|
|
root: {
|
|
type: 'split',
|
|
direction: 'vertical',
|
|
first: { type: 'leaf', leafId: retainedLeafId },
|
|
second: { type: 'leaf', leafId: duplicateLeafId }
|
|
},
|
|
activeLeafId: retainedLeafId,
|
|
expandedLeafId: null,
|
|
ptyIdsByLeafId: {
|
|
[retainedLeafId]: ptyId,
|
|
[duplicateLeafId]: ptyId
|
|
}
|
|
}
|
|
writeFileSync(dataPath, `${JSON.stringify(data, null, 2)}\n`)
|
|
}
|
|
|
|
async function waitForRestoredTerminal(page: Page, worktreeId: string): Promise<string> {
|
|
await waitForSessionReady(page)
|
|
await expect.poll(() => getActiveWorktreeId(page), { timeout: 15_000 }).toBe(worktreeId)
|
|
await ensureTerminalVisible(page)
|
|
await waitForActiveTerminalManager(page, 30_000)
|
|
await waitForPaneCount(page, 1, 30_000)
|
|
const tabId = await getActiveTabId(page)
|
|
if (!tabId) {
|
|
throw new Error('Restored terminal tab was unavailable')
|
|
}
|
|
return tabId
|
|
}
|
|
|
|
async function readRendererOwnership(
|
|
page: Page,
|
|
tabId: string
|
|
): Promise<RendererOwnershipSnapshot> {
|
|
return page.evaluate((tabId) => {
|
|
const layout = window.__store?.getState().terminalLayoutsByTabId[tabId]
|
|
const manager = window.__paneManagers?.get(tabId)
|
|
const surface = document.querySelector(
|
|
`[data-terminal-tab-id="${CSS.escape(tabId)}"][data-terminal-layout-leaf-ids]`
|
|
)
|
|
const countLeaves = (node: TerminalLayoutSnapshot['root']): number =>
|
|
!node ? 0 : node.type === 'leaf' ? 1 : countLeaves(node.first) + countLeaves(node.second)
|
|
const ptyIds = Object.values(layout?.ptyIdsByLeafId ?? {})
|
|
return {
|
|
paneCount: manager?.getPanes?.().length ?? 0,
|
|
xtermCount: surface?.querySelectorAll('.xterm').length ?? 0,
|
|
rootLeafCount: countLeaves(layout?.root ?? null),
|
|
ptyBindingCount: ptyIds.length,
|
|
uniquePtyCount: new Set(ptyIds).size
|
|
}
|
|
}, tabId)
|
|
}
|
|
|
|
async function readStreamingFrame(
|
|
page: Page,
|
|
tabId: string,
|
|
marker: string
|
|
): Promise<number | null> {
|
|
const content = await page.evaluate((tabId) => {
|
|
const pane = window.__paneManagers?.get(tabId)?.getPanes?.()[0]
|
|
return pane?.serializeAddon?.serialize?.() ?? null
|
|
}, tabId)
|
|
return parseStreamingFrame(content, marker)
|
|
}
|
|
|
|
async function readMainStreamingFrame(
|
|
page: Page,
|
|
ptyId: string,
|
|
marker: string
|
|
): Promise<number | null> {
|
|
const content = await page.evaluate(async (ptyId) => {
|
|
const snapshot = await window.api.pty.getMainBufferSnapshot(ptyId, { scrollbackRows: 0 })
|
|
return snapshot?.data ?? null
|
|
}, ptyId)
|
|
return parseStreamingFrame(content, marker)
|
|
}
|
|
|
|
function parseStreamingFrame(content: string | null, marker: string): number | null {
|
|
const prefix = `${marker} frame `
|
|
const start = content?.indexOf(prefix) ?? -1
|
|
if (!content || start < 0) {
|
|
return null
|
|
}
|
|
const digits = content.slice(start + prefix.length).match(/^\d+/)?.[0]
|
|
return digits ? Number(digits) : null
|
|
}
|
|
|
|
test('repairs duplicate persisted PTY renderers before streaming tab reveal', async (// oxlint-disable-next-line no-empty-pattern -- this restart test owns its Electron launches.
|
|
{}, testInfo) => {
|
|
const repoPath = existsSync(TEST_REPO_PATH_FILE)
|
|
? readFileSync(TEST_REPO_PATH_FILE, 'utf8').trim()
|
|
: ''
|
|
test.skip(!repoPath || !existsSync(repoPath), 'Seeded E2E repository is unavailable')
|
|
|
|
const session = createRestartSession(testInfo)
|
|
const marker = `DUPLICATE_PTY_REVEAL_${randomUUID()}`
|
|
const tui = stageNodeScriptForTerminal(streamingTuiSource(marker))
|
|
let firstApp: ElectronApplication | null = null
|
|
let secondApp: ElectronApplication | null = null
|
|
|
|
try {
|
|
const firstLaunch = await session.launch()
|
|
firstApp = firstLaunch.app
|
|
const worktreeId = await attachRepoAndOpenTerminal(firstLaunch.page, repoPath)
|
|
await waitForSessionReady(firstLaunch.page)
|
|
await ensureTerminalVisible(firstLaunch.page)
|
|
await waitForActiveTerminalManager(firstLaunch.page, 30_000)
|
|
const firstPtyId = await waitForActivePanePtyId(firstLaunch.page)
|
|
await execInTerminal(firstLaunch.page, firstPtyId, tui.command)
|
|
await waitForTerminalOutput(firstLaunch.page, marker, 20_000)
|
|
tui.cleanup()
|
|
|
|
await session.close(firstApp)
|
|
firstApp = null
|
|
seedDuplicatePtyOwnership(session.userDataDir)
|
|
|
|
const secondLaunch = await session.launch()
|
|
secondApp = secondLaunch.app
|
|
const restoredTabId = await waitForRestoredTerminal(secondLaunch.page, worktreeId)
|
|
await waitForTerminalOutput(secondLaunch.page, marker, 20_000)
|
|
const frameBeforeHide = await readMainStreamingFrame(secondLaunch.page, firstPtyId, marker)
|
|
if (frameBeforeHide === null) {
|
|
throw new Error('Authoritative TUI frame was unavailable before hiding the restored tab')
|
|
}
|
|
|
|
const siblingTabId = await secondLaunch.page.evaluate((worktreeId) => {
|
|
const store = window.__store
|
|
if (!store) {
|
|
throw new Error('Renderer store unavailable')
|
|
}
|
|
return store.getState().createTab(worktreeId, undefined, undefined, { activate: false }).id
|
|
}, worktreeId)
|
|
await secondLaunch.page.evaluate(
|
|
(tabId) => window.__store?.getState().setActiveTab(tabId),
|
|
siblingTabId
|
|
)
|
|
await expect
|
|
.poll(() => getActiveTabId(secondLaunch.page), { timeout: 10_000 })
|
|
.toBe(siblingTabId)
|
|
const restoredSurface = secondLaunch.page.locator(
|
|
`[data-terminal-tab-id=${JSON.stringify(restoredTabId)}]`
|
|
)
|
|
await expect(restoredSurface).toBeHidden()
|
|
await expect
|
|
.poll(() => readMainStreamingFrame(secondLaunch.page, firstPtyId, marker), {
|
|
timeout: 10_000,
|
|
message: 'Authoritative TUI output did not advance while the restored tab was hidden'
|
|
})
|
|
.toBeGreaterThan(frameBeforeHide)
|
|
const hiddenFrame = await readMainStreamingFrame(secondLaunch.page, firstPtyId, marker)
|
|
if (hiddenFrame === null || hiddenFrame <= frameBeforeHide) {
|
|
throw new Error('Authoritative TUI output did not remain advanced while the tab was hidden')
|
|
}
|
|
await secondLaunch.page.evaluate(
|
|
(tabId) => window.__store?.getState().setActiveTab(tabId),
|
|
restoredTabId
|
|
)
|
|
await expect
|
|
.poll(() => getActiveTabId(secondLaunch.page), { timeout: 10_000 })
|
|
.toBe(restoredTabId)
|
|
await expect
|
|
.poll(() => readStreamingFrame(secondLaunch.page, restoredTabId, marker), {
|
|
timeout: 20_000,
|
|
message: 'Revealed renderer did not catch up to hidden authoritative output'
|
|
})
|
|
.toBeGreaterThanOrEqual(hiddenFrame)
|
|
|
|
await expect
|
|
.poll(() => readRendererOwnership(secondLaunch.page, restoredTabId), { timeout: 10_000 })
|
|
.toEqual({
|
|
paneCount: 1,
|
|
xtermCount: 1,
|
|
rootLeafCount: 1,
|
|
ptyBindingCount: 1,
|
|
uniquePtyCount: 1
|
|
})
|
|
await testInfo.attach('duplicate-pty-renderer-after-reveal.png', {
|
|
body: await secondLaunch.page.screenshot(),
|
|
contentType: 'image/png'
|
|
})
|
|
} finally {
|
|
tui.cleanup()
|
|
if (secondApp) {
|
|
await session.close(secondApp)
|
|
}
|
|
if (firstApp) {
|
|
await session.close(firstApp)
|
|
}
|
|
await session.dispose()
|
|
}
|
|
})
|