Files
orca/tests/e2e/plugin-panel-containment.spec.ts
Neil ec0d3cedd1 Plugin panels talk to their worker; panel storage, settings and agent events (#37013)
* feat(plugins): panels talk to their own worker, read storage, settings and events

- plugin.request (host API 1.2): a panel or editor asks its plugin's worker, which answers with
  orca.panels.handle(name); the worker pushes with orca.panels.post. Same plugin only, JSON
  checked, 1 MiB answers, 256 KiB/200-per-10s posts, a deadline and the panel's lifetime.
- Panels and editors get storage.*, settings.* and secrets.* behind the same capabilities, and
  storage/settings onDidChange on every surface; events.subscribe delivers workspace and agent
  events to panels that hold the read capability.
- contributes.settings: typed settings (a JSON Schema subset) with defaults and value checks.
- Frames get orca.plugin/storage/settings/secrets; components panels import plugin, storage,
  settings, secrets from 'orca', and atom(key, init, { persist: 'storage' }) shares a value with
  the worker. Workers get orca.panels/storage/settings/secrets sugar.
- One typed contract: declare module 'orca' { interface PluginRequests / PluginPanelEvents }.
- Desktop push channel per open panel session (plugins:openPanelPush).

* feat(plugins): Orca Settings shows and edits a plugin's contributes.settings

The plugin page lists declared settings with Orca's own controls (switch, picker, text and
number fields, Reset to default). Edits are checked against the declaration in main, written to
the plugin's settings, and reach open panels and the running worker through onDidChange.

* docs(plugins): hello-tsx talks to its worker; authoring guide covers panels, workers and data

The example now uses one of each: plugin.request answered by orca.panels.handle, a worker post
heard with plugin.on, a persist: 'storage' atom the worker watches, two contributes.settings and
agent.status.changed in the panel, all typed by one src/protocol.ts. The build-less TSX e2e
drives the whole loop.

* fix(plugins): typed bridge overloads, panel call budget, settings IPC and catalog keys

- PluginBridgeApi/PanelsApi use overloads: declared names are typed (and complete), any other
  name carries JSON; no never-unions for the type-aware lint.
- The per-plugin panel call budget is 120 per 10 s, since a panel loading storage atoms,
  settings and its first requests makes a burst; the hostile fixture floods past it.
- Settings IPC builds its host per call; the settings section's strings are in en.json.

* test(plugins): the example's declared settings show under the plugin with the person's value

* refactor(plugins): keep hello-tsx the minimal starter; the bridge demo is hello-worker-panel

hello-tsx goes back to a sandboxed-ui panel with no worker or capabilities. The worker, protocol,
storage atom, settings and events move to examples/plugins/hello-worker-panel, with their own
e2e (plugin-worker-panel) and PR route.

* feat(plugins): test kit covers the panel bridge and plugin data

mockHost keeps the plugin's storage, settings and secrets in memory (seeded with storage,
settings, secrets; read with host.data()) and fakes the worker with requests, so plugin.request,
storage/settings/secrets and persist: 'storage' atoms work under mount(), writes reach
onDidChange, and panel.post(event, payload) is what plugin.on hears. Typed by PluginRequests and
PluginPanelEvents.

* test(plugins): focusInGuest waits for the guest and retries focusing as one step

Under load the custom-editor guest could still be attaching, so executeJavaScript threw
'dom-ready' before the focus poll began. Wait for waitForGuestReady, then retry focusing the
webview and the element together, tolerating a guest that is still attaching or remounts.

* docs(plugins): the authoring skill covers panels, workers and data; persistence leaves the renderer

- skill-guides/orca-plugin-authoring.md gains Panels, workers and data (plugin.request/on,
  orca.panels.handle/post, persist: 'storage', contributes.settings, panel events, test kit), and
  its generated references read orca-services.d.ts for shell, files, on and the bridge types.
- After the dev-loop rebase, persisted-atom wiring (queue plus storage sync) moves to
  createDeclarativePersistence so the renderer stays under max-lines.

* feat(plugins): panels show agents: agents.list, terminal ids on agent events, workspace.changed

- agents.list (agent:read, every surface): the focused workspace's agents with terminalId,
  title, agent kind, state and stateStartedAt, from the hook server's store.
- agent.status.changed carries terminalId and title: paneKey mapped through the runtime's
  terminal list (tabId:leafId) or the status's terminal handle; looked up only while a plugin
  listens, in arrival order.
- workspace.readContext terminals carry title; workspace.changed (workspace:read) fires after
  workspace session writes when the focused workspace or its terminals change.
- Event subscriptions are kept per panel session, so one made before the push channel opens
  still counts.
- shell.openExternal never prompts on a hidden or minimized window: it settles
  { opened: false, reason: 'consent-unavailable' } and says so in the plugin log, which also
  notes 'waiting for the person to confirm' while a prompt is up; the rate budget is spent only
  when Orca acts. e2e launches never open a real browser or mail client.
- A worker may export activate as default or by name; docs, typings and the authoring skill
  document the entry, the agent events and a live agent status recipe.

* fix(plugins): the plugin page names the workspace.changed subscription

* fix(plugins): workers activate only by default export, and say so when it is named

Orca calls the worker module's default export. A module with a named `activate` and no default
now fails to start with an error naming the fix (`export default function activate(orca)`),
instead of being called by a fallback; docs, typings and the skill say default export.
hello-worker-panel gains a picker setting (suggestionOrder) the worker reads.
2026-11-01 04:40:17 -08:00

543 lines
21 KiB
TypeScript

// Require zero loopback requests plus isolated-guest CSP/navigation containment and bounded bridge refusal.
// Chromium is required; keep experimental maturity until CI soak covers every desktop platform.
import { cp, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import { createServer, type Server } from 'node:http'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import type { AddressInfo } from 'node:net'
import type { ElectronApplication, Locator, Page, TestInfo } from '@stablyai/playwright-test'
import { expect, test } from './helpers/orca-app'
import {
clickInGuest,
evalInGuest,
guestWebContentsId,
pluginGuest,
readGuestCsp,
readInGuest,
waitForGuestReady,
ISOLATED_FRAMES_E2E
} from './helpers/plugin-frame-guest'
// Isolated frames are off by default; these journeys exercise the guest path.
test.use(ISOLATED_FRAMES_E2E)
type InstalledPanel = {
pluginKey: string
tabKey: string
title: string
}
type ProbeServer = {
origin: string
requests: string[]
close: () => Promise<void>
}
type GuestNavigationObservation = {
willFrameNavigations: { defaultPrevented: boolean; isMainFrame: boolean; url: string }[]
didFrameNavigations: string[]
externalUrls: string[]
/** Orca's "wants to open" prompts, answered from `promptAnswers` (default Cancel). */
prompts: { message: string; detail: string; checkboxLabel: string }[]
promptAnswers: { response: number; checkboxChecked: boolean }[]
}
type GuestDocumentSnapshot = {
url: string
marker: string | null
fixture: boolean
}
async function closeServer(server: Server): Promise<void> {
await new Promise<void>((resolve, reject) => {
server.close((error) => {
if (error) {
reject(error)
return
}
resolve()
})
})
}
async function startPermissiveProbeServer(): Promise<ProbeServer> {
const requests: string[] = []
const gif = Buffer.from('R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=', 'base64')
const server = createServer((request, response) => {
requests.push(request.url ?? '/')
response.setHeader('Access-Control-Allow-Origin', '*')
if (request.url?.includes('beacon.gif')) {
response.writeHead(200, { 'Content-Type': 'image/gif', 'Content-Length': gif.byteLength })
response.end(gif)
return
}
response.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' })
response.end('permissive probe response')
})
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve))
const port = (server.address() as AddressInfo).port
return {
origin: `http://127.0.0.1:${port}`,
requests,
close: () => closeServer(server)
}
}
async function materializeHostilePlugin(origin: string): Promise<string> {
const tempRoot = await mkdtemp(join(tmpdir(), 'orca-hostile-panel-e2e-'))
const pluginRoot = join(tempRoot, 'hostile-panel')
await cp(
join(process.cwd(), 'examples', 'plugins', 'test-fixtures', 'hostile-panel'),
pluginRoot,
{
recursive: true
}
)
const panelPath = join(pluginRoot, 'panel.html')
const panelHtml = await readFile(panelPath, 'utf8')
await writeFile(panelPath, panelHtml.replaceAll('https://example.com', origin))
return pluginRoot
}
async function installApprovedPanel(page: Page, sourcePath: string): Promise<InstalledPanel> {
return page.evaluate(async (pluginPath) => {
const settings = await window.api.settings.set({ pluginSystemEnabled: true })
window.__store?.setState({ settings })
await window.api.plugins.refresh()
const installed = await window.api.plugins.install({ kind: 'local-path', path: pluginPath })
if (!installed.ok) {
throw new Error(installed.error)
}
const listed = await window.api.plugins.refresh()
const plugin = listed.find((entry) => entry.pluginKey === installed.pluginKey)
if (!plugin?.consentFingerprint || !plugin.panels[0]) {
throw new Error(`installed plugin ${installed.pluginKey} has no reviewable panel`)
}
const approved = await window.api.plugins.consent({
pluginKey: plugin.pluginKey,
reviewedFingerprint: plugin.consentFingerprint,
decision: 'approve'
})
const approvedPlugin = approved.find((entry) => entry.pluginKey === plugin.pluginKey)
const panel = approvedPlugin?.panels[0]
if (!panel) {
throw new Error(`approved plugin ${plugin.pluginKey} has no panel`)
}
return { pluginKey: plugin.pluginKey, tabKey: panel.tabKey, title: panel.title }
}, sourcePath)
}
async function openPanel(page: Page, panel: InstalledPanel): Promise<Locator> {
await page.evaluate(async () => {
const store = window.__store?.getState()
if (!store) {
throw new Error('window.__store is unavailable')
}
if (!store.rightSidebarOpen) {
store.toggleRightSidebar()
}
// Refresh after the sidebar subscription exists so this isolated profile
// cannot miss the install/consent change events emitted just before mount.
await window.api.plugins.refresh()
})
const panelButton = page.getByRole('button', { name: panel.title })
await expect(panelButton).toBeVisible({ timeout: 15_000 })
await panelButton.click()
const guest = pluginGuest(page, panel.title)
await expect(guest).toBeVisible({ timeout: 15_000 })
await waitForGuestReady(guest)
return guest
}
async function attachProbeRequests(testInfo: TestInfo, requests: readonly string[]): Promise<void> {
await testInfo.attach('hostile-panel-loopback-requests', {
body: Buffer.from(JSON.stringify(requests, null, 2)),
contentType: 'application/json'
})
}
// Watches the guest WebContents itself: main's own will-frame-navigate guard runs first.
async function startGuestObserver(
electronApp: ElectronApplication,
guestId: number
): Promise<void> {
await electronApp.evaluate(({ dialog, shell, webContents }, id) => {
// By id: earlier specs in this worker may leave other plugins' guests alive.
const guest = webContents.fromId(id)
if (!guest) {
throw new Error('plugin guest WebContents not found')
}
const observation: GuestNavigationObservation = {
willFrameNavigations: [],
didFrameNavigations: [],
externalUrls: [],
prompts: [],
promptAnswers: []
}
const onWill = (event: Electron.Event<Electron.WebContentsWillFrameNavigateEventParams>) => {
observation.willFrameNavigations.push({
defaultPrevented: event.defaultPrevented,
isMainFrame: event.isMainFrame,
url: event.url
})
}
const onDid = (_event: Electron.Event, url: string) => {
observation.didFrameNavigations.push(url)
}
const originalOpenExternal = shell.openExternal
const originalShowMessageBox = dialog.showMessageBox
// Never a native prompt on the test machine: record it and answer from the queue.
const recordPrompt = async (...args: unknown[]): Promise<Electron.MessageBoxReturnValue> => {
const options = args.at(-1)
const field = (name: string): string =>
typeof options === 'object' && options !== null && name in options
? String(Object.getOwnPropertyDescriptor(options, name)?.value)
: ''
observation.prompts.push({
message: field('message'),
detail: field('detail'),
checkboxLabel: field('checkboxLabel')
})
return { ...(observation.promptAnswers.shift() ?? { response: 1, checkboxChecked: false }) }
}
dialog.showMessageBox = recordPrompt
const recordOpenExternal = async (url: string): Promise<void> => {
observation.externalUrls.push(url)
}
guest.on('will-frame-navigate', onWill)
guest.on('did-frame-navigate', onDid)
shell.openExternal = recordOpenExternal
if (shell.openExternal !== recordOpenExternal) {
throw new Error('shell.openExternal could not be stubbed')
}
Reflect.set(globalThis, '__orcaGuestProbe', {
observation,
dispose: () => {
guest.off('will-frame-navigate', onWill)
guest.off('did-frame-navigate', onDid)
shell.openExternal = originalOpenExternal
dialog.showMessageBox = originalShowMessageBox
}
})
}, guestId)
}
async function readGuestObserver(
electronApp: ElectronApplication
): Promise<GuestNavigationObservation> {
const observation: GuestNavigationObservation = await electronApp.evaluate(
() =>
Object.getOwnPropertyDescriptor(globalThis, '__orcaGuestProbe')?.value?.observation ?? null
)
return observation
}
async function stopGuestObserver(
electronApp: ElectronApplication
): Promise<GuestNavigationObservation | null> {
const observation: GuestNavigationObservation | null = await electronApp.evaluate(() => {
const probe = Object.getOwnPropertyDescriptor(globalThis, '__orcaGuestProbe')?.value
probe?.dispose()
Reflect.deleteProperty(globalThis, '__orcaGuestProbe')
return probe?.observation ?? null
})
return observation
}
async function readGuestDocument(guest: Locator): Promise<GuestDocumentSnapshot> {
const url = await guest.evaluate((webview: Electron.WebviewTag) => webview.getURL())
const content = await evalInGuest<{ marker: string | null; fixture: boolean }>(
guest,
`({ marker: document.documentElement.dataset.navigationProbeDocument ?? null,
fixture: document.querySelector('h1')?.textContent === 'Hostile panel fixture' })`
)
return { url, ...content }
}
function readProbe(guest: Locator, probe: string): Promise<string | null> {
return readInGuest<string | null>(
guest,
`document.querySelector('[data-probe="${probe}"]')?.dataset.contained ?? null`
)
}
test('contains hostile panel network and navigation probes', async ({
electronApp,
orcaPage
}, testInfo) => {
testInfo.annotations.push({ type: 'maturity', description: 'experimental' })
const server = await startPermissiveProbeServer()
const pluginRoot = await materializeHostilePlugin(server.origin)
const tempRoot = join(pluginRoot, '..')
const appUrl = orcaPage.url()
const guestDocuments: GuestDocumentSnapshot[] = []
let observation: GuestNavigationObservation | null = null
let observing = false
try {
const panel = await installApprovedPanel(orcaPage, pluginRoot)
const guest = await openPanel(orcaPage, panel)
await expect(guest).toHaveAttribute('partition', /^persist:orca-plugin-[0-9a-f]{16}$/)
await expect(orcaPage.locator(`iframe[title="${panel.title}"]`)).toHaveCount(0)
expect(await readGuestCsp(guest)).toMatch(/connect-src 'self' data: blob:.*frame-src 'none'/)
for (const probe of ['fetch-exfil', 'img-beacon']) {
await expect.poll(() => readProbe(guest, probe), { timeout: 5_000 }).toBe('true')
}
const bridgeErrorCode = await evalInGuest<string>(
guest,
`window.orca.host.call('invalid.hostileAction', {}).then(
() => 'unexpected_success',
(error) => (typeof error?.code === 'string' ? error.code : 'missing_error_code'))`
)
// Why the host's code: the renderer checks only the shape, the host gates the method.
expect(bridgeErrorCode).toBe('unknown_method')
expect(server.requests).toEqual([])
expect(orcaPage.url()).toBe(appUrl)
await startGuestObserver(electronApp, await guestWebContentsId(guest))
observing = true
const initial = await readGuestDocument(guest)
guestDocuments.push(initial)
expect(initial.url).toMatch(/^orca-plugin-frame:\/\/[0-9a-f]{16}\//)
for (const navigation of [
{ button: 'navigate-top', probe: 'top-navigation' },
{ button: 'navigate-self', probe: 'self-navigation' },
{ button: 'navigate-anchor-form', probe: 'anchor-form-navigation' },
{ button: 'navigate-meta', probe: 'meta-refresh-navigation' }
]) {
const documentId = `source:${navigation.probe}`
await evalInGuest(
guest,
`document.documentElement.dataset.navigationProbeDocument = ${JSON.stringify(documentId)};
document.getElementById(${JSON.stringify(navigation.button)}).click()`
)
// Each probe reports after its attempt; a guest is its own top, so top-navigation's
// `window.top !== window` verdict is meaningless here and the URL check decides.
await expect.poll(() => readProbe(guest, navigation.probe)).not.toBeNull()
const current = await readGuestDocument(guest)
guestDocuments.push(current)
expect(current).toEqual({ url: initial.url, marker: documentId, fixture: true })
expect(server.requests).toEqual([])
expect(orcaPage.url()).toBe(appUrl)
}
const guardDestination = `${server.origin}/frame-guard-navigation`
await evalInGuest(guest, `location.assign(${JSON.stringify(guardDestination)})`)
await expect
.poll(async () => {
observation = await readGuestObserver(electronApp)
const attempt = observation.willFrameNavigations.find(({ url }) => url === guardDestination)
return attempt?.defaultPrevented === true && attempt.isMainFrame
})
.toBe(true)
const guarded = await readGuestDocument(guest)
guestDocuments.push(guarded)
expect(guarded).toEqual({
url: initial.url,
marker: 'source:meta-refresh-navigation',
fixture: true
})
observation = await readGuestObserver(electronApp)
const attempted = observation.willFrameNavigations.filter(({ url }) =>
url.startsWith(server.origin)
)
expect(attempted.every(({ defaultPrevented }) => defaultPrevented)).toBe(true)
expect(observation.didFrameNavigations.filter((url) => url.startsWith(server.origin))).toEqual(
[]
)
expect(observation.externalUrls).toEqual([])
expect(server.requests).toEqual([])
expect(orcaPage.url()).toBe(appUrl)
// Bridge budgets on the guest path: oversized and flooded calls get real refusals.
await clickInGuest(guest, '#bridge')
for (const probe of ['oversized-message', 'message-flood']) {
await expect
.poll(() =>
readInGuest<string | null>(
guest,
`document.querySelector('[data-probe="${probe}"]')?.dataset.contained ?? null`
)
)
.toBe('true')
}
} finally {
try {
if (observing) {
observation = await stopGuestObserver(electronApp)
}
} finally {
await attachProbeRequests(testInfo, server.requests)
await testInfo.attach('hostile-panel-guest-documents', {
body: Buffer.from(JSON.stringify(guestDocuments, null, 2)),
contentType: 'application/json'
})
await testInfo.attach('hostile-panel-guest-navigation-observation', {
body: Buffer.from(JSON.stringify(observation, null, 2)),
contentType: 'application/json'
})
await server.close()
await rm(tempRoot, { recursive: true, force: true })
}
}
})
test('shell.openExternal from a panel needs a real gesture in the guest', async ({
electronApp,
orcaPage
}, testInfo) => {
testInfo.annotations.push({ type: 'maturity', description: 'experimental' })
const server = await startPermissiveProbeServer()
const pluginRoot = await materializeHostilePlugin(server.origin)
const tempRoot = join(pluginRoot, '..')
const target = 'https://example.com/orca-open-external-probe'
let observing = false
try {
const panel = await installApprovedPanel(orcaPage, pluginRoot)
const guest = await openPanel(orcaPage, panel)
await startGuestObserver(electronApp, await guestWebContentsId(guest))
observing = true
const ungated = await evalInGuest<string>(
guest,
`window.orca.shell.openExternal(${JSON.stringify(target)}).then(
() => 'opened', (error) => String(error?.code))`
)
expect(ungated).toBe('not_permitted')
expect((await readGuestObserver(electronApp)).externalUrls).toEqual([])
await evalInGuest(
guest,
`(() => {
const button = document.createElement('button')
button.id = 'open-external'
button.textContent = 'Open external'
button.style.cssText = 'position:fixed;top:0;left:0;width:160px;height:40px;z-index:9'
button.onclick = () => window.orca.shell.openExternal(${JSON.stringify(target)}).then(
(result) => { window.__openExternal = result.opened ? 'opened' : result.reason },
(error) => { window.__openExternal = String(error?.code) })
document.body.appendChild(button)
})()`
)
// A scripted click is not a gesture: the same handler must be refused.
await evalInGuest(guest, `document.getElementById('open-external').click()`)
await expect
.poll(() => readInGuest<string | null>(guest, 'window.__openExternal ?? null'))
.toBe('not_permitted')
expect((await readGuestObserver(electronApp)).externalUrls).toEqual([])
expect((await readGuestObserver(electronApp)).prompts).toEqual([])
const clickAndRead = async (): Promise<string | null> => {
await evalInGuest(guest, 'window.__openExternal = null')
await clickInGuest(guest, '#open-external')
await expect
.poll(() => readInGuest<string | null>(guest, 'window.__openExternal ?? null'))
.not.toBeNull()
return readInGuest<string | null>(guest, 'window.__openExternal ?? null')
}
// This background launch's window is hidden, so nobody could answer a prompt: it settles at
// once, unopened, instead of waiting forever.
expect(await clickAndRead()).toBe('consent-unavailable')
expect((await readGuestObserver(electronApp)).prompts).toEqual([])
// Report the window as on screen (it is never shown) so the prompt path runs.
await electronApp.evaluate(({ BrowserWindow }) => {
for (const window of BrowserWindow.getAllWindows()) {
window.isVisible = () => true
window.isMinimized = () => false
}
})
// A real click on a host this plugin was never allowed asks first; Cancel opens nothing.
expect(await clickAndRead()).toBe('cancelled')
let observed = await readGuestObserver(electronApp)
expect(observed.externalUrls).toEqual([])
expect(observed.prompts).toEqual([
{
message: expect.stringContaining('wants to open example.com'),
detail: target,
checkboxLabel: expect.stringContaining('example.com')
}
])
// Open with "Always allow": opened now, and the next click opens without asking.
await electronApp.evaluate(() => {
Object.getOwnPropertyDescriptor(
globalThis,
'__orcaGuestProbe'
)?.value?.observation.promptAnswers.push({ response: 0, checkboxChecked: true })
})
expect(await clickAndRead()).toBe('opened')
expect(await clickAndRead()).toBe('opened')
observed = await readGuestObserver(electronApp)
expect(observed.externalUrls).toEqual([target, target])
expect(observed.prompts).toHaveLength(2)
} finally {
if (observing) {
await stopGuestObserver(electronApp)
}
await server.close()
await rm(tempRoot, { recursive: true, force: true })
}
})
test('detects and suspends a busy-looping panel in an isolated renderer', async ({
electronApp,
orcaPage
}, testInfo) => {
testInfo.annotations.push({ type: 'maturity', description: 'experimental' })
const server = await startPermissiveProbeServer()
const pluginRoot = await materializeHostilePlugin(server.origin)
const tempRoot = join(pluginRoot, '..')
const appUrl = orcaPage.url()
let processes: { main: number | null; guest: number | null; guestUrl: string | null } | null =
null
try {
const panel = await installApprovedPanel(orcaPage, pluginRoot)
const guest = await openPanel(orcaPage, panel)
const guestId = await guestWebContentsId(guest)
processes = await electronApp.evaluate(
({ BrowserWindow, webContents }, { expectedUrl, id }) => {
const mainContents = (
BrowserWindow.getAllWindows().find(
(candidate) => candidate.webContents.getURL() === expectedUrl
) ?? BrowserWindow.getAllWindows()[0]
)?.webContents
const guestContents = webContents.fromId(id)
return {
main: mainContents?.getOSProcessId() ?? null,
guest: guestContents?.getOSProcessId() ?? null,
guestUrl: guestContents?.getURL() ?? null
}
},
{ expectedUrl: appUrl, id: guestId }
)
expect(processes.main).toBeGreaterThan(0)
expect(processes.guest).toBeGreaterThan(0)
expect(processes.guest).not.toBe(processes.main)
// Deferred so executeJavaScript resolves before the guest's main thread spins.
await evalInGuest(guest, `setTimeout(() => document.getElementById('busy').click(), 0)`)
await expect(
orcaPage.getByText('This plugin panel stopped responding and was suspended.')
).toBeVisible({ timeout: 20_000 })
await expect(
orcaPage.getByRole('button', { name: new RegExp(`${panel.title}.*Error`) })
).toBeVisible()
expect(orcaPage.url()).toBe(appUrl)
expect(server.requests).toEqual([])
} finally {
await testInfo.attach('hostile-panel-processes', {
body: Buffer.from(JSON.stringify(processes, null, 2)),
contentType: 'application/json'
})
await attachProbeRequests(testInfo, server.requests)
await server.close()
await rm(tempRoot, { recursive: true, force: true })
}
})