mirror of
https://github.com/stablyai/orca.git
synced 2026-10-09 16:02:46 +00:00
* fix(terminal): mount one surface per workspace id in the workbench (STA-4846) * test(terminal): pin the workbench projection against under-selecting Losing a surface unmounts live terminals, which is worse than the duplicate mount STA-4846 fixes, so cover every catalog shape that reaches the workbench: local-only rows that name no host, an unqualified row colliding with a host-qualified one, two SSH hosts on one id, folder rows across three hosts, folder ids alongside git worktree ids, and a whole-catalog assertion that the emitted id set equals the distinct input id set. Also pin the `useAllWorktrees` -> `useWorktreeMap` swap: both read the same WeakMap-cached snapshot, so the zustand compare is unchanged. Harden the folder tie-break to require the row to name its own host. `getCatalogOwnerHostId` defaults an unstamped row to `local`, which would let a row that never named a host win the `local` tie and mount another host's path; it now keeps first-wins instead of guessing. * fix(terminal): surface the unresolvable folder-surface collision When two hosts publish the same folder-workspace id and the active workspace's host cannot be resolved, the projection drops one row's folderPath first-wins. That path is the PTY cwd for any tab without a startupCwd, so the drop was silent. Warn on it, and pin the two tie-break branches the unit tests missed: a colliding row that is not the active workspace, and the same collision with the rows in swapped order (a host reconnect re-appends its rows, flipping which row is first mid-session). * test(e2e): ride out Playwright's spurious main-process evaluate rejection `e2e / changed e2e specs` failed on `pr11346-selected-runtime-add.spec.ts` with "Execution context was destroyed, most likely because of a navigation" from the paired client's first `app.evaluate` — the isolated-HOME assert that runs one millisecond after `electron.launch()` resolves, which is before the app is `ready`. Nothing navigates there: Playwright raises that message for any main-process CDP failure that is neither a JS error nor a closed session, and `ElectronApplication.evaluate` is unreliable on Electron 27+ (microsoft/playwright#33737). Reproduced locally, and a plain re-run of the same commit went green. Extract the retry `installTerminalPtyWriteSpy` already carried for this exact message into `retryTransientMainEvaluate`, and use it for the launch-time home read in all three launchers. The read is idempotent and a real boundary escape still throws on the first successful read. Also forward the paired client's process logs before the assert instead of after: this failure reached CI with none of the client's own output, because forwarding had not started yet. * test(e2e): wait on the owning group before asserting a Cmd-J browser tab is active `changed e2e specs` then failed at the remote browser-page step: the store poll had already seen `activeBrowserTabId` land on the mirrored workspace, but `[data-tab-id=...][data-active="true"]` never appeared. `data-active` on a `BrowserTab` is the strip's active tab, which comes from the owning group's `activeTabId` — not from `activeBrowserTabId` — so the DOM assert was racing an activation the poll never waited for. The simulator rows in the same spec already poll the group; the two browser-page rows did not. Poll the same triple for them, so a genuinely stuck group fails with the ids it ended on instead of a bare "element(s) not found".
82 lines
3.1 KiB
TypeScript
82 lines
3.1 KiB
TypeScript
import type { ElectronApplication } from '@stablyai/playwright-test'
|
|
|
|
import { retryTransientMainEvaluate } from './electron-main-evaluate-retry'
|
|
|
|
export type PtyWriteLogEntry = { id: string; data: string }
|
|
|
|
export async function installTerminalPtyWriteSpy(app: ElectronApplication): Promise<void> {
|
|
await retryTransientMainEvaluate(() =>
|
|
app.evaluate(({ ipcMain }) => {
|
|
const global = globalThis as unknown as {
|
|
__terminalPtyWriteLog?: PtyWriteLogEntry[]
|
|
__terminalPtyWriteSpyInstalled?: boolean
|
|
__terminalPtyWriteAcceptedSpyInstalled?: boolean
|
|
__terminalPtyWriteDelayMs?: number
|
|
}
|
|
if (global.__terminalPtyWriteSpyInstalled) {
|
|
return
|
|
}
|
|
global.__terminalPtyWriteLog = []
|
|
global.__terminalPtyWriteSpyInstalled = true
|
|
ipcMain.prependListener('pty:write', (_event: unknown, args: PtyWriteLogEntry) => {
|
|
global.__terminalPtyWriteLog!.push({ id: args.id, data: args.data })
|
|
})
|
|
|
|
// Playwright cannot observe ipcRenderer.invoke payloads, so this e2e spy wraps main's handler.
|
|
const invokeHandlers = (
|
|
ipcMain as unknown as {
|
|
_invokeHandlers?: Map<string, (event: unknown, args: PtyWriteLogEntry) => unknown>
|
|
}
|
|
)._invokeHandlers
|
|
const writeAcceptedHandler = invokeHandlers?.get('pty:writeAccepted')
|
|
if (!writeAcceptedHandler || global.__terminalPtyWriteAcceptedSpyInstalled) {
|
|
return
|
|
}
|
|
global.__terminalPtyWriteAcceptedSpyInstalled = true
|
|
invokeHandlers?.set('pty:writeAccepted', async (event, args) => {
|
|
global.__terminalPtyWriteLog!.push({ id: args.id, data: args.data })
|
|
const delayMs = Math.max(0, global.__terminalPtyWriteDelayMs ?? 0)
|
|
if (delayMs > 0) {
|
|
await new Promise((resolve) => setTimeout(resolve, delayMs))
|
|
}
|
|
return writeAcceptedHandler(event, args)
|
|
})
|
|
})
|
|
)
|
|
}
|
|
|
|
export async function clearTerminalPtyWriteLog(app: ElectronApplication): Promise<void> {
|
|
await app.evaluate(() => {
|
|
const global = globalThis as unknown as { __terminalPtyWriteLog?: PtyWriteLogEntry[] }
|
|
if (global.__terminalPtyWriteLog) {
|
|
global.__terminalPtyWriteLog.length = 0
|
|
}
|
|
})
|
|
}
|
|
|
|
export async function readTerminalPtyWrites(app: ElectronApplication): Promise<string[]> {
|
|
return app.evaluate(() => {
|
|
const global = globalThis as unknown as { __terminalPtyWriteLog?: PtyWriteLogEntry[] }
|
|
return (global.__terminalPtyWriteLog ?? []).map((entry) => entry.data)
|
|
})
|
|
}
|
|
|
|
export async function readTerminalPtyWriteEntries(
|
|
app: ElectronApplication
|
|
): Promise<PtyWriteLogEntry[]> {
|
|
return app.evaluate(() => {
|
|
const global = globalThis as unknown as { __terminalPtyWriteLog?: PtyWriteLogEntry[] }
|
|
return [...(global.__terminalPtyWriteLog ?? [])]
|
|
})
|
|
}
|
|
|
|
export async function setTerminalPtyWriteDelay(
|
|
app: ElectronApplication,
|
|
delayMs: number
|
|
): Promise<void> {
|
|
await app.evaluate((nextDelayMs) => {
|
|
const global = globalThis as unknown as { __terminalPtyWriteDelayMs?: number }
|
|
global.__terminalPtyWriteDelayMs = Math.max(0, nextDelayMs)
|
|
}, delayMs)
|
|
}
|