test: remove 158 low-value tests (#39639)

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
OrcaWin
2026-11-07 17:10:38 -08:00
committed by GitHub
co-authored by OrcaWin
parent a238176954
commit 126cf690ae
37 changed files with 31 additions and 5453 deletions
-2
View File
@@ -584,7 +584,6 @@
3 test-no-wall-clock:src/main/github/pr-refresh-coordinator-visible-follow-up.test.ts
4 test-no-wall-clock:src/main/github/pr-refresh-queue-growth-bound.test.ts
2 test-no-wall-clock:src/main/grok/grok-hook-stdin-no-eof.test.ts
1 test-no-wall-clock:src/main/ipc/ai-vault/ai-vault.test.ts
3 test-no-wall-clock:src/main/ipc/runtime-environments/runtime-environment-request-connections.test.ts
1 test-no-wall-clock:src/main/ipc/terminal-render-desync-evidence.test.ts
5 test-no-wall-clock:src/main/ipc/worktrees-orphan-directory-cleanup.test.ts
@@ -806,7 +805,6 @@
1 test-timeout-needs-reason:src/main/codex/structured/launch/codex-structured-real-cli-command-env.test.ts
1 test-timeout-needs-reason:src/main/codex/structured/launch/codex-structured-real-cli-permissions.test.ts
1 test-timeout-needs-reason:src/main/codex/structured/launch/codex-structured-real-cli-visuals.test.ts
1 test-timeout-needs-reason:src/main/codex/trust/toml/codex-trust-config-concurrent-launch.test.ts
1 test-timeout-needs-reason:src/main/cursor/hook-service.test.ts
1 test-timeout-needs-reason:src/main/daemon/executable-host/linux/testing/daemon-appimage-copy-host.electron.test.ts
2 test-timeout-needs-reason:src/main/daemon/health/testing/issue-6814-daemon-failure-classification.test.ts
File diff suppressed because one or more lines are too long
@@ -952,7 +952,6 @@
"src/main/browser/client-pages/lifecycle/browser-client-page-reconciliation-adapters.test.ts": 252,
"src/main/browser/client-pages/renderer/browser-client-page-renderer-bridge.test.ts": 163,
"src/main/browser/client-pages/lifecycle/browser-client-page-upload-routing.test.ts": 303,
"src/main/browser/cookies/client-route/browser-client-route-cookie-import-partition-parity.test.ts": 374,
"src/main/browser/cookies/client-route/browser-client-route-cookie-import.test.ts": 101,
"src/main/browser/client-hosts/file-transfers/browser-client-upload-buffer-retention.test.ts": 736,
"src/main/browser/client-hosts/file-transfers/browser-client-upload-staging.test.ts": 140,
@@ -979,7 +978,6 @@
"src/main/browser/guest-lifecycle/manager/browser-manager-grab-capture.test.ts": 516,
"src/main/browser/guest-lifecycle/manager/browser-manager-grab-mode.test.ts": 567,
"src/main/browser/guest-lifecycle/manager/browser-manager-grab-selection.test.ts": 584,
"src/main/browser/guest-lifecycle/manager/browser-manager-guest-lifecycle.test.ts": 554,
"src/main/browser/guest-lifecycle/manager/browser-manager-guest-unload.test.ts": 534,
"src/main/browser/guest-lifecycle/manager/browser-manager-guest-visibility.test.ts": 543,
"src/main/browser/guest-lifecycle/manager/browser-manager-load-failure-replay.test.ts": 549,
@@ -1066,7 +1064,6 @@
"src/main/browser/cookies/browser-cookie-import-write.test.ts": 68,
"src/main/browser/cookies/browser-cookie-import.comet.test.ts": 1501,
"src/main/browser/cookies/browser-cookie-import.test.ts": 1304,
"src/main/browser/cookies/browser-cookie-public-suffix-scope.test.ts": 92,
"src/main/browser/cookies/browser-cookie-samesite.electron.test.ts": 1882,
"src/main/browser/cookies/sources/browser-cookie-decryption.test.ts": 74,
"src/main/browser/cookies/sources/browser-cookie-import-undecryptable.test.ts": 541,
@@ -1481,7 +1478,6 @@
"src/main/codex/configuration/shared-server/codex-shared-server-probe.test.ts": 181,
"src/main/codex/configuration/homes/codex-stale-pane-accounts.test.ts": 146,
"src/main/codex/history/index/codex-state-db-backfill-recovery-supervision.test.ts": 4660,
"src/main/codex/history/index/codex-state-db-backfill-recovery.test.ts": 444,
"src/main/codex/history/index/codex-state-db.test.ts": 127,
"src/main/codex/structured/acquisition/codex-structured-acquisition-exit-proof.test.ts": 580,
"src/main/codex/structured/launch/codex-structured-child-environment.test.ts": 887,
@@ -1535,7 +1531,6 @@
"src/main/codex/structured/launch/codex-structured-visuals.test.ts": 783,
"src/main/codex/structured/background-work/codex-subagent-interrupts.test.ts": 587,
"src/main/codex/structured/threads/codex-thread-catch-up-rewind.test.ts": 1479,
"src/main/codex/trust/toml/codex-trust-config-concurrent-launch.test.ts": 254,
"src/main/codex/trust/toml/codex-trust-config-mutation-queue.test.ts": 51,
"src/main/codex/structured/journal/codex-turn-lifecycle-completion.test.ts": 374,
"src/main/codex/structured/journal/codex-turn-lifecycle-history.test.ts": 374,
@@ -1572,7 +1567,6 @@
"src/main/codex/trust/hooks/sta-4737-unreadable-is-not-absent.test.ts": 182,
"src/main/codex/configuration/homes/sta-4823-shared-state-survives-a-failed-read.test.ts": 242,
"src/main/codex/hooks/installation/windows-hook-command.test.ts": 129,
"src/main/codex/hooks/installation/windows-hook-upgrade.test.ts": 312,
"src/main/codex/history/bridges/wsl-codex-session-bridge-script.test.ts": 190,
"src/main/codex/history/bridges/wsl-codex-session-bridge.test.ts": 346,
"src/main/command-code/hook-service.test.ts": 271,
@@ -1894,7 +1888,6 @@
"src/main/foreign-sqlite-readers/readers/cursor-profile.test.ts": 403,
"src/main/foreign-sqlite-readers/readers/opencode-binder-sessions.test.ts": 227,
"src/main/foreign-sqlite-readers/readers/opencode-prompt-failure.test.ts": 346,
"src/main/gemini/hook-service.test.ts": 108,
"src/main/ghostty/index.test.ts": 118,
"src/main/ghostty/theme-import.test.ts": 100,
"src/main/git/base-ref-search-selector.test.ts": 257,
@@ -2202,13 +2195,10 @@
"src/main/ipc/ai-vault/ai-vault-search-merged-cursor.test.ts": 103,
"src/main/ipc/ai-vault/ai-vault-search.test.ts": 1424,
"src/main/ipc/ai-vault/ai-vault-unresolved-owner.test.ts": 1436,
"src/main/ipc/ai-vault/ai-vault.test.ts": 1453,
"src/main/ipc/app.test.ts": 319,
"src/main/ipc/automations-external-scope.test.ts": 359,
"src/main/ipc/browser/browser-client-page-metadata-ipc.test.ts": 143,
"src/main/ipc/browser/browser-preview-tool-authorization.test.ts": 1105,
"src/main/ipc/browser/browser-session-profile-ipc.test.ts": 699,
"src/main/ipc/browser/browser-tab-registration-wait.test.ts": 234,
"src/main/ipc/browser/browser.test.ts": 1209,
"src/main/ipc/codex-config-sync.test.ts": 101,
"src/main/ipc/command-path/command-path-directory-worker.test.ts": 640,
@@ -2217,7 +2207,6 @@
"src/main/ipc/created-worktree-root-prune.test.ts": 494,
"src/main/ipc/developer-permissions.test.ts": 147,
"src/main/ipc/diagnostics.test.ts": 124,
"src/main/ipc/doc-preview-grant-ipc.test.ts": 58,
"src/main/ipc/dropped-path-resolution.test.ts": 92,
"src/main/ipc/emulator-frame-stream-socket-lifetime.test.ts": 166,
"src/main/ipc/emulator-renderer-lifetime.test.ts": 116,
@@ -4726,7 +4715,6 @@
"src/main/window/macos-tahoe-release.test.ts": 48,
"src/main/window/main-window-state-lifecycle.test.ts": 60,
"src/main/window/main-window-updater-deferred-setup.test.ts": 215,
"src/main/window/main-window-webview-security.test.ts": 296,
"src/main/window/native-chat-paste-files.test.ts": 666,
"src/main/window/native-chat-pasted-text-file.test.ts": 94,
"src/main/window/privileged-window-navigation.test.ts": 246,
@@ -184,7 +184,6 @@ export const WINDOWS_BOUNDARY_TESTS = [
'src/main/agent-hooks/windows-powershell-hook-launcher.test.ts',
'src/main/claude/claude-status-line-windows.test.ts',
'src/main/codex/hooks/installation/windows-hook-command.test.ts',
'src/main/codex/hooks/installation/windows-hook-upgrade.test.ts',
'src/main/codex/hooks/installation/hook-service-managed-install.test.ts',
'src/main/windows/windows-pty-job.win32.test.ts',
'src/main/windows/windows-msys-job.win32.test.ts',
@@ -20,7 +20,6 @@ import {
resetMobileNativeChatDraftStoreForTests,
setMobileNativeChatDraftText
} from './native-chat/composer/mobile-native-chat-draft-store'
import './native-chat/mobile-native-chat-scope-key'
import { resolveActiveSessionTab } from './active-session-tab'
const initial: Terminal[] = ['a', 'b', 'c'].map((handle) => ({
@@ -1,6 +1,5 @@
import type * as WorktreeScanCacheModule from '../../../git/worktrees/catalog/worktree-scan-cache'
import type * as WorktreeListingModule from '../../../git/worktrees/catalog/worktree-listing'
import '../../../runtime/desktop-browser-bridge/desktop-browser-bridge-allowlist.spec'
import { mkdtempSync } from 'node:fs'
import { RuntimeBrowserCommands } from '../../../runtime/browser/commands/browser-commands'
import { setRuntimeBrowserCommandsFactory } from '../../../runtime/browser/installation/runtime-browser-commands-factory'
@@ -1,12 +0,0 @@
import { describe, expect, it } from 'vitest'
import { normalizeCookieImportDomain } from './browser-cookie-import-policy'
// Suffix-list changes can widen removal scopes and destroy unrelated sessions.
describe('registrable family across public-suffix sections', () => {
it('rejects a bare suffix as an import domain but keeps real hosts', () => {
expect(normalizeCookieImportDomain('co.uk')).toBeNull()
expect(normalizeCookieImportDomain('api.br')).toBeNull()
expect(normalizeCookieImportDomain('.example.com')).toBe('example.com')
expect(normalizeCookieImportDomain('foo.github.io')).toBe('foo.github.io')
})
})
@@ -1,182 +0,0 @@
import { describe, expect, it, vi } from 'vitest'
import type { BrowserHostLeaseAuthority } from '../../../../shared/browser/client-hosting/browser-client-host-protocol'
import type { BrowserNetworkExecutionHost } from '../../../../shared/browser/client-hosting/browser-network-execution-host'
const {
bindingStore,
detectInstalledBrowsersMock,
getProfileMock,
getRouteIdentityMock,
importCookiesFromBrowserMock,
requireRouteBrowserProfileMock,
updateProfileSourceMock
} = vi.hoisted(() => ({
bindingStore: {
get: vi.fn(() => null as string | null),
set: vi.fn(() => [] as readonly string[]),
touch: vi.fn(),
findPartitionByFingerprint: vi.fn(() => null as string | null),
rebind: vi.fn()
},
detectInstalledBrowsersMock: vi.fn(),
getProfileMock: vi.fn(),
getRouteIdentityMock: vi.fn(),
importCookiesFromBrowserMock: vi.fn(),
requireRouteBrowserProfileMock: vi.fn(),
updateProfileSourceMock: vi.fn()
}))
vi.mock('../browser-cookie-import', () => ({
detectInstalledBrowsers: detectInstalledBrowsersMock,
importCookiesFromBrowser: importCookiesFromBrowserMock,
selectBrowserProfile: vi.fn()
}))
vi.mock('../../routing/partitions/browser-route-partition-binding-runtime', () => ({
currentBrowserRoutePartitionBindingStore: () => bindingStore
}))
vi.mock('../../sessions/browser-session-registry', () => ({
browserSessionRegistry: {
getProfile: getProfileMock,
requireRouteBrowserProfile: requireRouteBrowserProfileMock,
updateProfileSource: updateProfileSourceMock
}
}))
vi.mock('../../client-hosts/lifecycle/paired-runtime-browser-client-host-runtime', () => ({
getPairedRuntimeBrowserClientRouteIdentity: getRouteIdentityMock
}))
import { importCookiesIntoClientRoutePartition } from './browser-client-route-cookie-import'
import { BrowserClientNetworkRouteRegistry } from '../../network/client-routes/browser-client-network-route-registry'
import {
browserAuthorityExecutionHostStorageIdentity,
legacyBrowserNativeExecutionHostStorageIdentity
} from '../../routing/identity/browser-execution-host-storage-identity'
import { createBrowserRoutePartitionBindingStoreFake } from '../../routing/partitions/browser-route-partition-binding-store-fake'
import { browserNetworkExecutionHostKey } from '../../network/execution-routes/browser-network-execution-route'
import { BrowserRouteSessionRegistry } from '../../routing/sessions/browser-route-session-registry'
import type { BrowserRouteElectronSession } from '../../routing/sessions/browser-route-session-policy'
const orcaProfileId = 'orca-profile-a'
const browserProfileId = 'default'
const authorityConnectionIdentity = 'paired-runtime:authority-a'
const authorityRuntimeId = 'runtime-a'
const storageScope = 'e'.repeat(64)
const authority: BrowserHostLeaseAuthority = {
authorityRuntimeId,
authorityEpoch: 'epoch-a',
browserHostClientId: 'client-a',
browserHostGeneration: 1
}
async function pagePartition(host: BrowserNetworkExecutionHost): Promise<string> {
const routes = new BrowserClientNetworkRouteRegistry({
// Native and WSL routes require their own runtime authority.
authority:
host.kind === 'ssh' ? authority : { ...authority, authorityRuntimeId: host.runtimeId },
authorityStorageKey: storageScope,
createRoute: () => ({
start: vi.fn(async () => ({ host: '127.0.0.1', port: 43123 })),
reconnect: vi.fn(async () => ({ host: '127.0.0.1', port: 43123 })),
suspend: vi.fn(),
close: vi.fn(async () => {})
})
})
const route = await routes.retain(
browserNetworkExecutionHostKey(host),
new AbortController().signal
)
const session: BrowserRouteElectronSession = {
setProxy: vi.fn(async () => {}),
closeAllConnections: vi.fn(async () => {}),
resolveProxy: vi.fn(async () => 'SOCKS5 127.0.0.1:43123')
}
// Exercise the default partition derivation used by real pages.
const sessions = new BrowserRouteSessionRegistry({
validateProfile: vi.fn(),
getSession: () => session,
setupPolicies: vi.fn(),
clearPolicies: vi.fn(),
retirePageAuthority: vi.fn(() => true),
bindingStore: createBrowserRoutePartitionBindingStoreFake()
})
const handle = await sessions.preparePage({
identity: {
orcaProfileId,
browserProfileId,
authorityConnectionIdentity,
executionHostIdentity: route.executionHostIdentity
},
storageScope,
browserPageId: 'page-a',
pageHostGeneration: 1,
rendererWebContentsId: 11,
proxyEndpoint: route.proxyEndpoint
})
const partition = handle.partition
handle.release()
await route.release()
await routes.close()
return partition
}
async function importPartition(): Promise<string> {
bindingStore.get.mockReturnValue(null)
getProfileMock.mockReturnValue({ id: browserProfileId, partition: 'persist:legacy-server' })
detectInstalledBrowsersMock.mockReturnValue([
{ family: 'chrome', label: 'Chrome', profiles: [], selectedProfile: 'Default' }
])
importCookiesFromBrowserMock.mockResolvedValue({ ok: true, summary: { importedCookies: 1 } })
getRouteIdentityMock.mockReturnValue({
orcaProfileId,
authorityConnectionIdentity,
// Settings imports target the server machine, using the host startup identity.
executionHostIdentity: browserAuthorityExecutionHostStorageIdentity(storageScope),
pairingAuthorityConnectionIdentity: 'paired-runtime:pairing-authority-a',
legacyAuthorityConnectionIdentity: 'paired-runtime:legacy-authority-a',
legacyExecutionHostIdentity:
legacyBrowserNativeExecutionHostStorageIdentity(authorityRuntimeId),
storageScope
})
const result = await importCookiesIntoClientRoutePartition({
environmentId: 'env-a',
browserProfileId,
browserFamily: 'chrome'
})
expect(result).toMatchObject({ ok: true })
return importCookiesFromBrowserMock.mock.calls.at(-1)?.[1] as string
}
describe('cookie-import target vs. client-hosted page partition', () => {
it('imports into the same partition a page on the server’s own machine uses', async () => {
const imported = await importPartition()
expect(imported).toBe(
await pagePartition({ kind: 'native', runtimeId: authorityRuntimeId, revision: 1 })
)
})
// Settings imports exclude nested SSH and WSL workspace partitions.
it('does not reach pages hosted on a nested SSH target or WSL distro', async () => {
const imported = await importPartition()
expect(imported).not.toBe(
await pagePartition({
kind: 'ssh',
targetId: 'ssh-1700000000-aaa111',
providerEpoch: 'epoch-1',
connectionGeneration: 1
})
)
expect(imported).not.toBe(
await pagePartition({
kind: 'wsl',
runtimeId: authorityRuntimeId,
revision: 1,
distro: 'Ubuntu'
})
)
})
})
@@ -102,13 +102,6 @@ describe('importCookiesIntoClientRoutePartition', () => {
expect(importCookiesFromBrowserMock).not.toHaveBeenCalled()
})
it('rejects a traversing browser profile name before touching the filesystem', async () => {
expect(
await importCookiesIntoClientRoutePartition({ ...request, browserProfile: '../../etc' })
).toMatchObject({ ok: false, reason: 'Invalid browser profile name.' })
expect(detectInstalledBrowsersMock).not.toHaveBeenCalled()
})
// A replacement during import must not report success for an abandoned partition.
it('fails the import when the server is re-paired mid-import', async () => {
getRouteIdentityMock.mockReturnValueOnce(routeIdentity).mockReturnValue(repairedRouteIdentity)
@@ -122,25 +115,4 @@ describe('importCookiesIntoClientRoutePartition', () => {
expect(importCookiesFromBrowserMock).toHaveBeenCalledOnce()
expect(recordClientRouteImportSourceMock).not.toHaveBeenCalled()
})
it('fails the import when the client host is retired mid-import', async () => {
getRouteIdentityMock.mockReturnValueOnce(routeIdentity).mockReturnValue(null)
const result = await importCookiesIntoClientRoutePartition(request)
expect(result).toMatchObject({
ok: false,
reason: 'The connection to this server ended during the import. Reconnect and try again.'
})
expect(recordClientRouteImportSourceMock).not.toHaveBeenCalled()
})
it('rejects an unknown browser session profile', async () => {
getProfileMock.mockReturnValue(null)
expect(await importCookiesIntoClientRoutePartition(request)).toMatchObject({
ok: false,
reason: 'Session profile not found.'
})
})
})
@@ -35,18 +35,4 @@ describe('decryptCookieValueRaw host-key hash prefix', () => {
const encrypted = encryptGcm(hostKeyHash(HOST_KEY))
expect(decryptCookieValueRaw(encrypted, gcmKeyResult, HOST_KEY)).toEqual(Buffer.alloc(0))
})
it('keeps a pre-schema-24 value that has no prefix', () => {
const encrypted = encryptCbc(Buffer.from('plain-legacy-value'))
expect(decryptCookieValueRaw(encrypted, cbcKeyResult, HOST_KEY)).toEqual(
Buffer.from('plain-legacy-value')
)
})
it('still strips a binary prefix that is not this host key hash', () => {
const plaintext = Buffer.concat([hostKeyHash('.other.com'), Buffer.from('value')])
expect(decryptCookieValueRaw(encryptCbc(plaintext), cbcKeyResult, HOST_KEY)).toEqual(
Buffer.from('value')
)
})
})
@@ -38,29 +38,6 @@ describe('resolveDocPreviewTargetPath', () => {
expect(resolveDocPreviewTargetPath(atWorkspaceRoot, 'docs/styles.css')).toBeNull()
})
it('authorizes only the requested sibling directory', () => {
const grant = mintDocPreviewGrant({
owner: sshOwner,
requestBase: '/srv/repo',
root: '/srv/repo/docs',
entryRelativePath: 'docs/report.html',
browserPageId: 'page-1'
})
expect(resolveDocPreviewTargetPath(grant, 'docs/report.html')).toBe(
'/srv/repo/docs/report.html'
)
expect(resolveDocPreviewTargetPath(grant, 'assets/chart.js')).toBeNull()
expect(resolveDocPreviewTargetPath(grant, '.env')).toBeNull()
expect(authorizeDocPreviewDirectory(grant.id, 'assets/chart.js')).toBe(true)
expect(resolveDocPreviewTargetPath(grant, 'assets/chart.js')).toBe('/srv/repo/assets/chart.js')
expect(resolveDocPreviewTargetPath(grant, 'assets/theme.css')).toBe(
'/srv/repo/assets/theme.css'
)
expect(resolveDocPreviewTargetPath(grant, '.env')).toBeNull()
})
it('uses Windows semantics when authorizing a sibling directory', () => {
const grant = mintDocPreviewGrant({
owner: sshOwner,
@@ -98,26 +98,6 @@ describe('outside-workspace HTML preview', () => {
})
})
// A macOS host spells /tmp as /private/tmp and returns a case-insensitive volume's stored name.
it('reads paths the host spells differently once canonicalized', async () => {
const page = grant()
authorizeDocPreviewDirectory(page.id, 'assets/styles.css')
host(
{
'/tmp/reports': { canonical: '/private/tmp/reports' },
'/tmp/reports/report.html': { canonical: '/private/tmp/reports/report.html' },
'/tmp/reports/assets': { canonical: '/private/tmp/reports/assets' },
'/tmp/reports/assets/styles.css': { canonical: '/private/tmp/reports/assets/Styles.css' }
},
{ ok: true, result: { content: 'h1{}', isBinary: false } }
)
await expect(readDocPreviewFile(page, 'report.html')).resolves.toMatchObject({ ok: true })
await expect(readDocPreviewFile(page, 'assets/styles.css')).resolves.toMatchObject({
ok: true
})
})
it('refuses symlink redirection outside an approved asset directory', async () => {
const page = grant()
authorizeDocPreviewDirectory(page.id, 'assets/data.json')
@@ -67,19 +67,6 @@ function makeRawPayload(overrides: Record<string, unknown> = {}) {
}
describe('clampGrabPayload', () => {
it('redacts secret-bearing cssClasses outside the attribute path', () => {
const payload = clampGrabPayload(
makeRawPayload({
target: {
...makeRawPayload().target,
cssClasses: 'primary access_token=secret'
}
})
)
expect(payload?.target.cssClasses).toBe('[redacted]')
})
it('redacts secret-bearing browser annotation metadata fields', () => {
const payload = clampGrabPayload(
makeRawPayload({
@@ -161,60 +148,4 @@ describe('clampGrabPayload', () => {
revokeDocPreviewGrant(grant.id)
expect(clampGrabPayload(raw, guestUrl)?.page.sanitizedUrl).toBe('')
})
it('keeps safe and ARIA attributes while stripping unsafe names', () => {
const payload = clampGrabPayload(
makeRawPayload({
target: {
...makeRawPayload().target,
attributes: {
ID: 'checkout',
class: 'source-code stateful auth token',
role: 'button',
type: 'button',
'aria-label': 'Submit',
'aria-labelledby': 'checkout-label',
'aria-hidden': 'false',
onclick: 'alert(1)',
style: 'display:none',
'data-secret': 'hidden',
'notaria-label': 'unexpected'
}
}
})
)
expect(payload?.target.attributes).toEqual({
id: 'checkout',
class: 'source-code stateful auth token',
role: 'button',
type: 'button',
'aria-label': 'Submit',
'aria-labelledby': 'checkout-label',
'aria-hidden': 'false'
})
})
it.each([
'access_token=credential',
'API_KEY=credential',
'password=credential',
'secret=credential',
'session_id=credential',
'csrf=credential'
])('redacts %s in allowed attribute values', (value) => {
const payload = clampGrabPayload(
makeRawPayload({
target: {
...makeRawPayload().target,
attributes: { title: value, 'aria-label': value }
}
})
)
expect(payload?.target.attributes).toEqual({
title: '[redacted]',
'aria-label': '[redacted]'
})
})
})
@@ -1,241 +0,0 @@
import { EventEmitter } from 'node:events'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const browserMocks = vi.hoisted(() => ({
appGetPathMock: vi.fn(() => '/downloads'),
shellOpenExternalMock: vi.fn(),
browserWindowFromWebContentsMock: vi.fn(),
menuBuildFromTemplateMock: vi.fn(),
guestOffMock: vi.fn(),
guestOnMock: vi.fn(),
guestSetBackgroundThrottlingMock: vi.fn(),
guestSetWindowOpenHandlerMock: vi.fn(),
guestOpenDevToolsMock: vi.fn(),
webContentsFromIdMock: vi.fn(),
screenGetCursorScreenPointMock: vi.fn(() => ({ x: 0, y: 0 })),
openPopupWithOriginBarMock: vi.fn()
}))
vi.mock('electron', () => ({
WebContentsView: class {
onDestroyed: (() => void) | null = null
webContents = {
id: 9001,
isDestroyed: () => false,
close: () => this.onDestroyed?.(),
once: (_event: string, listener: () => void) => {
this.onDestroyed = listener
},
focus: vi.fn()
}
setVisible(): void {}
},
app: {
getPath: browserMocks.appGetPathMock
},
BrowserWindow: {
fromWebContents: browserMocks.browserWindowFromWebContentsMock
},
clipboard: { writeText: vi.fn() },
shell: { openExternal: browserMocks.shellOpenExternalMock },
Menu: {
buildFromTemplate: browserMocks.menuBuildFromTemplateMock
},
screen: {
getCursorScreenPoint: browserMocks.screenGetCursorScreenPointMock
},
webContents: {
fromId: browserMocks.webContentsFromIdMock
}
}))
vi.mock('../popups/popup-origin-bar-window', () => ({
openPopupWithOriginBar: browserMocks.openPopupWithOriginBarMock
}))
import { browserManager } from '../../browser-manager'
import {
rendererWebContentsId,
resetBrowserManagerMocks,
resetBrowserManagerState
} from './browser-manager-test-harness'
import { revokeAllDocPreviewGrants } from '../../document-preview/doc-preview-grant-registry'
const {
guestSetBackgroundThrottlingMock,
guestSetWindowOpenHandlerMock,
guestOpenDevToolsMock,
webContentsFromIdMock
} = browserMocks
describe('browserManager', () => {
beforeEach(() => {
resetBrowserManagerMocks(browserMocks)
resetBrowserManagerState()
revokeAllDocPreviewGrants()
})
afterEach(() => {
vi.useRealTimers()
})
// The headless backend destroys its window when registration returns false.
it("publishes an offscreen page's top-level address changes, not its subframes'", () => {
const stateChanged = vi.fn()
browserManager.setBrowserGuestStateChangedListener(stateChanged)
let address = 'https://example.com/'
const guest = Object.assign(new EventEmitter(), {
id: 515,
isDestroyed: vi.fn(() => false),
getType: vi.fn(() => 'window'),
setBackgroundThrottling: guestSetBackgroundThrottlingMock,
isCurrentlyAudible: vi.fn(() => false),
setWindowOpenHandler: guestSetWindowOpenHandlerMock,
getURL: vi.fn(() => address)
})
webContentsFromIdMock.mockReturnValue(guest)
browserManager.registerOffscreenGuest({
browserPageId: 'offscreen-page',
worktreeId: 'remote-worktree',
webContentsId: guest.id
})
const navigateInPage = (url: string): void => {
address = url
guest.emit('did-navigate-in-page', {}, url, true, 1, 1)
}
guest.emit('did-navigate-in-page', {}, 'https://frame.example/#x', false, 1, 2)
expect(stateChanged).not.toHaveBeenCalled()
navigateInPage('https://example.com/#top')
expect(stateChanged).toHaveBeenCalledTimes(1)
expect(stateChanged).toHaveBeenLastCalledWith('remote-worktree')
// replaceState without a new address.
navigateInPage('https://example.com/#top')
expect(stateChanged).toHaveBeenCalledTimes(1)
address = 'https://example.com/next'
guest.emit('did-navigate', {}, address, 200, 'OK')
expect(stateChanged).toHaveBeenCalledTimes(2)
// A full navigation also counts as the address to compare with.
navigateInPage('https://example.com/next')
expect(stateChanged).toHaveBeenCalledTimes(2)
browserManager.unregisterGuest('offscreen-page')
expect(guest.listenerCount('did-navigate-in-page')).toBe(0)
})
it('retires stale guest mappings when a page re-registers after a process swap', () => {
const rendererSendMock = vi.fn()
const oldGuestOnMock = vi.fn()
const oldGuestOffMock = vi.fn()
const newGuestOnMock = vi.fn()
const newGuestOffMock = vi.fn()
const oldGuest = {
id: 501,
isDestroyed: vi.fn(() => false),
getType: vi.fn(() => 'webview'),
isCurrentlyAudible: vi.fn(() => false),
send: vi.fn(),
setBackgroundThrottling: guestSetBackgroundThrottlingMock,
setWindowOpenHandler: guestSetWindowOpenHandlerMock,
on: oldGuestOnMock,
off: oldGuestOffMock,
openDevTools: guestOpenDevToolsMock,
getURL: vi.fn(() => 'https://old.example')
}
const newGuest = {
id: 502,
isDestroyed: vi.fn(() => false),
getType: vi.fn(() => 'webview'),
isCurrentlyAudible: vi.fn(() => false),
send: vi.fn(),
setBackgroundThrottling: guestSetBackgroundThrottlingMock,
setWindowOpenHandler: guestSetWindowOpenHandlerMock,
on: newGuestOnMock,
off: newGuestOffMock,
openDevTools: guestOpenDevToolsMock,
getURL: vi.fn(() => 'https://new.example')
}
webContentsFromIdMock.mockImplementation((id: number) => {
if (id === oldGuest.id) {
return oldGuest
}
if (id === newGuest.id) {
return newGuest
}
if (id === rendererWebContentsId) {
return { isDestroyed: vi.fn(() => false), send: rendererSendMock }
}
return null
})
browserManager.attachGuestPolicies(oldGuest as never)
browserManager.registerGuest({
browserPageId: 'browser-1',
webContentsId: oldGuest.id,
rendererWebContentsId
})
browserManager.attachGuestPolicies(newGuest as never)
browserManager.registerGuest({
browserPageId: 'browser-1',
webContentsId: newGuest.id,
rendererWebContentsId
})
// Registration also reports audio state; this test is about load-failure routing.
rendererSendMock.mockClear()
const oldDidFailLoadHandler = oldGuestOnMock.mock.calls.find(
([event]) => event === 'did-fail-load'
)?.[1] as
| ((
event: unknown,
errorCode: number,
errorDescription: string,
validatedUrl: string,
isMainFrame: boolean
) => void)
| undefined
const newDidFailLoadHandler = newGuestOnMock.mock.calls.find(
([event]) => event === 'did-fail-load'
)?.[1] as
| ((
event: unknown,
errorCode: number,
errorDescription: string,
validatedUrl: string,
isMainFrame: boolean
) => void)
| undefined
oldDidFailLoadHandler?.(null, -105, 'Old guest failed', 'https://old.example', true)
expect(rendererSendMock).not.toHaveBeenCalled()
newDidFailLoadHandler?.(null, -106, 'New guest failed', 'https://new.example', true)
expect(rendererSendMock).toHaveBeenCalledWith('browser:guest-load-failed', {
browserPageId: 'browser-1',
loadError: {
code: -106,
description: 'New guest failed',
validatedUrl: 'https://new.example'
}
})
expect(oldGuestOffMock).toHaveBeenCalled()
expect(browserManager.getGuestWebContentsId('browser-1')).toBe(newGuest.id)
expect(browserManager.getTabIdForWebContentsId(oldGuest.id)).toBeNull()
expect(browserManager.getTabIdForWebContentsId(newGuest.id)).toBe('browser-1')
browserManager.unregisterGuest('browser-1')
expect(browserManager.getTabIdForWebContentsId(newGuest.id)).toBeNull()
})
// Browsing guests track override invalidation without attaching a debugger.
// The renderer re-applies a tab's mute on each registration report and clears the
// speaker on the unregister report, so neither may be skipped.
})
@@ -1,210 +0,0 @@
import { EventEmitter } from 'node:events'
import { link, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import * as ownerIdentity from '../../../agent-hooks/managed-hook-owner-identity'
import {
_internals,
resolveCodexBackfillSupervisorLockRoot,
runCodexStateDbBackfillRecovery,
startCodexStateDbBackfillRecoveryInBackground,
withCodexBackfillSupervisorLock
} from './codex-state-db-backfill-recovery'
import type { CodexStateDbBackfillStatus } from './codex-state-db'
const temporaryRoots: string[] = []
const originalPlatform = process.platform
function createFakeChild(): EventEmitter & {
stdin: { end: ReturnType<typeof vi.fn> }
exitCode: number | null
signalCode: NodeJS.Signals | null
kill: ReturnType<typeof vi.fn>
} {
return Object.assign(new EventEmitter(), {
stdin: { end: vi.fn() },
exitCode: null,
signalCode: null,
kill: vi.fn(() => true)
})
}
async function createTemporaryRoot(): Promise<string> {
const root = await mkdtemp(join(tmpdir(), 'orca-codex-backfill-recovery-'))
temporaryRoots.push(root)
return root
}
afterEach(async () => {
_internals.resetForTests()
Object.defineProperty(process, 'platform', { configurable: true, value: originalPlatform })
vi.restoreAllMocks()
vi.unstubAllEnvs()
await Promise.all(
temporaryRoots.splice(0).map((root) => rm(root, { recursive: true, force: true }))
)
})
describe('Codex state DB backfill recovery', () => {
it('does not start a supervisor for an unreadable index', async () => {
const home = await createTemporaryRoot()
await writeFile(join(home, 'state_5.sqlite'), 'not a sqlite database')
const run = vi.fn()
const withLock = vi.fn()
await expect(
startCodexStateDbBackfillRecoveryInBackground(home, {
run,
withLock: withLock as never
})
).resolves.toBeNull()
expect(withLock).not.toHaveBeenCalled()
expect(run).not.toHaveBeenCalled()
})
it('bounds permanent claimant exits just beyond the fast-failure window', async () => {
const controller = new AbortController()
const children = new Map<ReturnType<typeof createFakeChild>, number>()
const timerDurations: number[] = []
let now = 0
let exitCount = 0
let pendingTimers = 0
let maxPendingTimers = 0
let maxLiveChildren = 0
let safetyFuseTripped = false
const terminate = vi.fn(async () => {})
const spawnProcess = vi.fn(() => {
const child = createFakeChild()
children.set(child, now + 10_001)
maxLiveChildren = Math.max(maxLiveChildren, children.size)
return child
})
const sleep = vi.fn(async (ms: number) => {
timerDurations.push(ms)
pendingTimers += 1
maxPendingTimers = Math.max(maxPendingTimers, pendingTimers)
const wakeAt = now + ms
for (const [child, exitAt] of children) {
if (exitAt <= wakeAt) {
now = exitAt
children.delete(child)
exitCount += 1
child.emit('exit', 1, null)
}
}
now = wakeAt
pendingTimers -= 1
if (ms === 2_000 && spawnProcess.mock.calls.length === 6) {
safetyFuseTripped = true
controller.abort()
throw Object.assign(new Error('reproduction safety fuse'), { name: 'AbortError' })
}
await Promise.resolve()
})
const summary = await runCodexStateDbBackfillRecovery('/managed-home', controller.signal, {
spawnProcess: spawnProcess as never,
readStatus: vi.fn((): CodexStateDbBackfillStatus => ({
kind: 'incomplete',
stateDbPath: '/state.sqlite',
status: 'running'
})),
terminate,
sleep,
now: () => now
})
expect({
summary,
spawnCount: spawnProcess.mock.calls.length,
exitCount,
timerCount: timerDurations.length,
pollCount: timerDurations.filter((ms) => ms === 5_000).length,
backoffCount: timerDurations.filter((ms) => ms === 2_000).length,
liveChildren: children.size,
pendingTimers,
maxLiveChildren,
maxPendingTimers,
terminateCount: terminate.mock.calls.length,
safetyFuseTripped
}).toEqual({
summary: { outcome: 'gave-up', spawnCount: 5 },
spawnCount: 5,
exitCount: 5,
timerCount: 19,
pollCount: 15,
backoffCount: 4,
liveChildren: 0,
pendingTimers: 0,
maxLiveChildren: 1,
maxPendingTimers: 1,
terminateCount: 0,
safetyFuseTripped: false
})
})
})
describe.skipIf(process.platform === 'win32')('Codex backfill supervisor owner lock', () => {
it('recovers a dead owner whose PID was reused with a different start identity', async () => {
const userData = await createTemporaryRoot()
vi.stubEnv('ORCA_USER_DATA_PATH', userData)
const home = join(userData, 'managed-home')
const lockRoot = resolveCodexBackfillSupervisorLockRoot(home)
const lockParent = join(lockRoot, '.orca')
const token = '00000000-0000-4000-8000-000000000000'
const ownerPath = join(lockParent, `managed-hook-install.owner-${token}.json`)
const lockPath = join(lockParent, 'managed-hook-install.lock')
await mkdir(lockParent, { recursive: true })
await writeFile(
ownerPath,
JSON.stringify({
token,
pid: process.pid,
hostIdentity: await ownerIdentity.readManagedHookHostIdentity(),
processIdentity: 'stale-process-start-time'
})
)
await link(ownerPath, lockPath)
await expect(
withCodexBackfillSupervisorLock(home, undefined, async () => 'recovered')
).resolves.toBe('recovered')
})
it('does not interfere with a live supervisor from another Orca instance', async () => {
const userData = await createTemporaryRoot()
vi.stubEnv('ORCA_USER_DATA_PATH', userData)
const home = join(userData, 'managed-home')
let releaseFirst!: () => void
const first = withCodexBackfillSupervisorLock(
home,
undefined,
async () => await new Promise<void>((resolve) => (releaseFirst = resolve))
)
await vi.waitFor(async () => {
await expect(
import('node:fs/promises').then(({ readFile }) =>
readFile(
join(
resolveCodexBackfillSupervisorLockRoot(home),
'.orca',
'managed-hook-install.lock'
),
'utf8'
)
)
).resolves.toContain('processIdentity')
})
const controller = new AbortController()
const secondRun = vi.fn(async () => {})
setTimeout(() => controller.abort(), 40)
await expect(
withCodexBackfillSupervisorLock(home, controller.signal, secondRun)
).rejects.toMatchObject({ name: 'AbortError' })
expect(secondRun).not.toHaveBeenCalled()
releaseFirst()
await first
})
})
@@ -1,23 +1,11 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import {
existsSync,
linkSync,
mkdtempSync,
mkdirSync,
readFileSync,
rmSync,
statSync,
writeFileSync
} from 'node:fs'
import { mkdtempSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { basename, dirname, join } from 'node:path'
import { basename, join } from 'node:path'
import type * as NodeFsPromises from 'node:fs/promises'
import {
codexRolloutHardlinkIdentity,
dedupeCodexRolloutFileAliases
} from '../../../ai-vault/scanners/codex/codex-session-root-dedup'
import '../../../ai-vault/scanners/codex/codex-session-root-dedup'
import { prepareLegacySharedCodexSessionResume } from './codex-legacy-session-resume'
import { ManagedCodexHomeTemporarilyUnavailableError } from '../../../codex-accounts/host-codex-managed-home-ownership'
import '../../../codex-accounts/host-codex-managed-home-ownership'
const lstatFaults = vi.hoisted(() => ({
path: null as string | null,
@@ -89,21 +77,6 @@ describe('prepareLegacySharedCodexSessionResume', () => {
expect(statSync(targetPath).ino).toBe(statSync(rolloutPath).ino)
})
it('coalesces concurrent resumes of the same legacy rollout', async () => {
const [first, second] = await Promise.all([prepare(), prepare()])
expect(first.useRealCodexHome).toBe(true)
expect(second.useRealCodexHome).toBe(true)
expect(readFileSync(targetRolloutPath(), 'utf-8')).toContain('session_meta')
})
it('fails clearly and retryably instead of approving a missing rollout', async () => {
rmSync(rolloutPath)
await expect(prepare()).rejects.toThrow(/Retry resume/)
expect(existsSync(targetRolloutPath())).toBe(false)
})
it('fails closed when a different rollout already owns the target path', async () => {
mkdirSync(join(targetRolloutPath(), '..'), { recursive: true })
writeFileSync(targetRolloutPath(), '{"different":true}\n', 'utf-8')
@@ -137,125 +110,3 @@ describe('prepareLegacySharedCodexSessionResume', () => {
return join(systemHome, 'sessions', '2026', '07', '20', basename(rolloutPath))
}
})
// Vault dedup's smallest bridge alias may name a peer account instead of the selected one.
describe('per-account resume repin', () => {
let root: string
// Make the selected account lose the path tie-break.
let peerHome: string
let selectedHome: string
let recordedRolloutPath: string
let bridgedRolloutPath: string
const rolloutRelativePath = join(
'sessions',
'2026',
'07',
'20',
'rollout-2026-07-20T10-00-00-abcdef00-1234-4321-9999-cafecafecafe.jsonl'
)
beforeEach(() => {
root = mkdtempSync(join(tmpdir(), 'orca-codex-account-repin-'))
peerHome = join(root, 'codex-accounts', '11111111-aaaa-4aaa-8aaa-111111111111', 'home')
selectedHome = join(root, 'codex-accounts', '99999999-bbbb-4bbb-8bbb-999999999999', 'home')
recordedRolloutPath = join(selectedHome, rolloutRelativePath)
bridgedRolloutPath = join(peerHome, rolloutRelativePath)
mkdirSync(dirname(recordedRolloutPath), { recursive: true })
writeFileSync(recordedRolloutPath, '{"type":"session_meta"}\n', 'utf-8')
mkdirSync(dirname(bridgedRolloutPath), { recursive: true })
linkSync(recordedRolloutPath, bridgedRolloutPath)
})
afterEach(() => {
rmSync(root, { recursive: true, force: true })
})
it.each([['recorded rollout first'], ['bridged alias first']])(
'repins the surviving vault row to the selected account, discovered %s',
async (order) => {
const recorded = rolloutCandidate(recordedRolloutPath, selectedHome)
const bridged = rolloutCandidate(bridgedRolloutPath, peerHome)
const survivors = dedupeCodexRolloutFileAliases(
order === 'recorded rollout first' ? [recorded, bridged] : [bridged, recorded],
rolloutCandidateAccessors
)
expect(survivors).toEqual([bridged])
const result = await prepareLegacySharedCodexSessionResume(
{
agent: 'codex',
filePath: bridged.filePath,
codexHome: peerHome,
executionHostId: 'local'
},
repinOptions()
)
expect(result).toEqual({ useRealCodexHome: false, substituteCodexHome: selectedHome })
}
)
it('declines while the bridge has not yet linked the rollout into the selected home', async () => {
const unbridgedSelectedHome = join(
root,
'codex-accounts',
'ffffffff-cccc-4ccc-8ccc-ffffffffffff',
'home'
)
mkdirSync(unbridgedSelectedHome, { recursive: true })
const result = await prepareLegacySharedCodexSessionResume(
{
agent: 'codex',
filePath: bridgedRolloutPath,
codexHome: peerHome,
executionHostId: 'local'
},
{ ...repinOptions(), getSelectedHostAccountCodexHomePath: () => unbridgedSelectedHome }
)
expect(result).toEqual({ useRealCodexHome: false })
})
it('refuses when the selected rollout is temporarily unreadable', async () => {
lstatFaults.path = recordedRolloutPath
await expect(
prepareLegacySharedCodexSessionResume(
{
agent: 'codex',
filePath: bridgedRolloutPath,
codexHome: peerHome,
executionHostId: 'local'
},
repinOptions()
)
).rejects.toBeInstanceOf(ManagedCodexHomeTemporarilyUnavailableError)
})
function repinOptions() {
return {
isHostSystemDefaultRealHomeSelected: () => false,
getSelectedHostAccountCodexHomePath: () => selectedHome
}
}
function rolloutCandidate(filePath: string, codexHome: string) {
const stat = statSync(filePath)
return {
filePath,
codexHome,
file: { dev: stat.dev, ino: stat.ino, nlink: stat.nlink }
}
}
})
const rolloutCandidateAccessors = {
isCodex: () => true,
getFilePath: (candidate: { filePath: string }) => candidate.filePath,
getCodexHome: (candidate: { codexHome: string }) => candidate.codexHome,
getHardlinkIdentity: (candidate: { file: { dev: number; ino: number; nlink: number } }) =>
codexRolloutHardlinkIdentity(candidate.file)
}
@@ -1,101 +0,0 @@
import { describe, expect, it, vi } from 'vitest'
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
import type * as Os from 'node:os'
import { setupCodexHookHomes } from './hook-service-test-harness'
const { getPathMock, homedirMock } = vi.hoisted(() => ({
getPathMock: vi.fn<(name: string) => string>(),
homedirMock: vi.fn<() => string>()
}))
vi.mock('electron', () => ({ app: { getPath: getPathMock } }))
vi.mock('os', async (importOriginal) => ({
...(await importOriginal<typeof Os>()),
homedir: homedirMock
}))
import './hook-service'
import { CodexHookService } from './codex-hook-service-implementation'
import { CODEX_EVENTS, CODEX_EVENT_LABEL } from '../definitions/codex-hook-definition'
import { buildCodexHookCommand as getManagedCommand } from '../definitions/codex-hook-command-form'
import '../../../agent-hooks/installer-utils'
import { readHooksJson } from '../../../agent-hooks/hooks-json-read'
import { wrapWindowsHookCommand } from '../../../agent-hooks/windows-hook-command'
import { upsertHookTrustEntries } from '../../trust/toml/config-toml-trust'
import {
computeCodexTrustedHash as computeTrustedHash,
getExplicitHomeCodexHookSourcePath as getCodexExplicitHomeHookSourcePath
} from '../../trust/identity/codex-trust-identity'
const homes = setupCodexHookHomes(homedirMock, getPathMock)
describe.skipIf(process.platform !== 'win32')('Unicode Windows hook upgrade', () => {
it('replaces all encoded commands and trust hashes while preserving user hooks on reinstall', async () => {
const home = join(homes.tmpHome, '测试 用户')
mkdirSync(home)
homedirMock.mockReturnValue(home)
const runtimeHome = join(homes.userDataDir, 'codex-runtime-home', 'home')
const configPath = join(runtimeHome, 'hooks.json')
const tomlPath = join(runtimeHome, 'config.toml')
const scriptPath = join(home, '.orca', 'agent-hooks', 'codex-hook.cmd')
const oldCommand = wrapWindowsHookCommand(scriptPath)
const userHome = join(home, '.codex')
mkdirSync(userHome)
const userConfig = JSON.stringify({
hooks: { Stop: [{ hooks: [{ type: 'command', command: 'user-hook' }] }] }
})
writeFileSync(join(userHome, 'hooks.json'), userConfig)
mkdirSync(runtimeHome, { recursive: true })
writeFileSync(
configPath,
JSON.stringify({
hooks: Object.fromEntries(
CODEX_EVENTS.map((event) => [
event,
[{ hooks: [{ type: 'command', command: oldCommand, timeout: 10 }] }]
])
)
})
)
upsertHookTrustEntries(
tomlPath,
CODEX_EVENTS.map((event) => ({
sourcePath: getCodexExplicitHomeHookSourcePath(configPath),
eventLabel: CODEX_EVENT_LABEL[event],
groupIndex: 0,
handlerIndex: 0,
command: oldCommand,
timeoutSec: 10
}))
)
const service = new CodexHookService()
expect(service.getStatus(runtimeHome).state).not.toBe('installed')
for (let pass = 0; pass < 2; pass++) {
expect((await service.install()).state).toBe('installed')
expect(service.getStatus(runtimeHome).state).toBe('installed')
const hooks = readHooksJson(configPath)?.hooks
const trust = readFileSync(tomlPath, 'utf8')
for (const event of CODEX_EVENTS) {
const commands = hooks?.[event]?.flatMap((group) => group.hooks ?? []) ?? []
expect(
commands.filter((hook) => hook.command === getManagedCommand(scriptPath))
).toHaveLength(1)
expect(commands.some((hook) => hook.command === oldCommand)).toBe(false)
const entry = {
sourcePath: getCodexExplicitHomeHookSourcePath(configPath),
eventLabel: CODEX_EVENT_LABEL[event],
groupIndex: 0,
handlerIndex: 0,
command: getManagedCommand(scriptPath),
timeoutSec: 10
}
expect(trust).toContain(computeTrustedHash(entry))
expect(trust).not.toContain(computeTrustedHash({ ...entry, command: oldCommand }))
}
expect(
hooks?.Stop?.some((group) => group.hooks?.some((hook) => hook.command === 'user-hook'))
).toBe(true)
expect(readFileSync(join(userHome, 'hooks.json'), 'utf8')).toBe(userConfig)
}
})
})
@@ -1,286 +0,0 @@
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type {
CodexHookTrustGrantRequest,
CodexHookTrustGrantSessionResult
} from '../../app-server/codex-app-server-client'
import type { CodexManagedTrustGrantPlan } from '../grants/codex-managed-trust-grant-plan'
import type { CodexTrustEntry } from './config-toml-trust'
// Keep the guest identity probe unstamped without requiring wsl.exe.
vi.mock('@orca/process-host', () => ({
runProcess: vi.fn(async () => ({
code: 1,
signal: null,
timedOut: false,
stdout: '',
stderr: ''
}))
}))
const testState = {
fakeHomeDir: '',
userDataDir: '',
previousUserDataPath: undefined as string | undefined
}
vi.mock(import('node:os'), async (importOriginal) => {
const actual = await importOriginal()
return { ...actual, homedir: () => testState.fakeHomeDir }
})
const { CodexAppServerUnsupportedError } = await import('../../app-server/codex-app-server-session')
const { codexAppServerCapabilityCache } =
await import('../../app-server/codex-app-server-capability-cache')
const { _internals, grantManagedCodexHookTrust } = await import('../grants/codex-hook-trust-grant')
const { markCodexProjectTrusted } = await import('../../../agent-launch/trust/agent-trust-presets')
const { getLocalCodexTrustConfigFiles } = await import('../../configuration/homes/codex-home-paths')
const { setCodexTrustGrantTelemetry } = await import('../grants/codex-trust-grant-telemetry')
const [
{ readHookTrustEntries, upsertHookTrustEntries },
{
computeCodexTrustKey: computeTrustKey,
computeCodexTrustedHash: computeTrustedHash,
normalizeCodexHookTrustLookupKey: normalizeHookTrustKeyForLookup
}
] = await Promise.all([import('./config-toml-trust'), import('../identity/codex-trust-identity')])
let runtimeHomeDir: string
beforeEach(() => {
testState.fakeHomeDir = mkdtempSync(join(tmpdir(), 'orca-concurrent-home-'))
testState.userDataDir = mkdtempSync(join(tmpdir(), 'orca-concurrent-userdata-'))
testState.previousUserDataPath = process.env.ORCA_USER_DATA_PATH
process.env.ORCA_USER_DATA_PATH = testState.userDataDir
runtimeHomeDir = join(testState.userDataDir, 'codex-runtime-home', 'home')
mkdirSync(runtimeHomeDir, { recursive: true })
writeFileSync(join(runtimeHomeDir, 'hooks.json'), '{"hooks":{}}\n', 'utf-8')
mkdirSync(join(testState.fakeHomeDir, '.codex'), { recursive: true })
codexAppServerCapabilityCache.clear()
_internals.resetDiagnostics()
})
afterEach(() => {
_internals.setGrantSessionRunner(null)
setCodexTrustGrantTelemetry(() => {})
codexAppServerCapabilityCache.clear()
if (testState.previousUserDataPath === undefined) {
delete process.env.ORCA_USER_DATA_PATH
} else {
process.env.ORCA_USER_DATA_PATH = testState.previousUserDataPath
}
delete process.env.ORCA_DISABLE_CODEX_TRUST_RPC
rmSync(testState.fakeHomeDir, { recursive: true, force: true })
rmSync(testState.userDataDir, { recursive: true, force: true })
})
const MANAGED_COMMAND = "/bin/sh '/tmp/orca/codex-hook.sh'"
function managedEntry(eventLabel: CodexTrustEntry['eventLabel']): CodexTrustEntry {
return {
sourcePath: join(runtimeHomeDir, 'hooks.json'),
eventLabel,
groupIndex: 0,
handlerIndex: 0,
command: MANAGED_COMMAND,
timeoutSec: 10
}
}
function buildPlan(
entries: CodexTrustEntry[],
overrides: Partial<CodexManagedTrustGrantPlan> = {}
): CodexManagedTrustGrantPlan {
return {
runtimeHomePath: runtimeHomeDir,
tomlPath: join(runtimeHomeDir, 'config.toml'),
managedCommand: MANAGED_COMMAND,
managedEntries: entries,
host: {
kind: 'wsl',
distro: 'Ubuntu',
linuxRuntimeHome: overrides.runtimeHomePath ?? runtimeHomeDir
},
...overrides
}
}
const tick = (): Promise<void> => new Promise((resolve) => setTimeout(resolve, 0))
/** Write across an await to model concurrent app-server mutations. */
function writingSessionRunner(args: {
tomlPath: string
entries: CodexTrustEntry[]
hashPrefix: string
gate?: Promise<void>
outcome?: 'granted' | 'verify-failed'
}) {
return async (
_request: CodexHookTrustGrantRequest
): Promise<CodexHookTrustGrantSessionResult> => {
const granted = args.entries.map((entry) => {
const key = computeTrustKey(entry)
return {
key,
normalizedKey: normalizeHookTrustKeyForLookup(key),
trustedHash: `${args.hashPrefix}${entry.eventLabel}`
}
})
await tick()
upsertHookTrustEntries(
args.tomlPath,
args.entries.map((entry, index) => ({ ...entry, trustedHash: granted[index].trustedHash }))
)
if (args.gate) {
await args.gate
}
if (args.outcome === 'verify-failed') {
return {
outcome: 'verify-failed',
reason: 'listed hash mismatch',
reasonClass: 'post-grant-mismatch'
}
}
return { outcome: 'granted', wroteTrust: true, entries: granted }
}
}
describe('two Codex pane launches against one config.toml', () => {
it("leaves a concurrent grant's records in place when a sibling grant fails", async () => {
// Warm the capability cache so both grant sessions can overlap.
codexAppServerCapabilityCache.rememberSupported('wsl:Ubuntu')
const tomlPath = join(runtimeHomeDir, 'config.toml')
const entries = [managedEntry('session_start')]
let call = 0
let releaseFirst!: () => void
const firstGate = new Promise<void>((resolve) => {
releaseFirst = resolve
})
_internals.setGrantSessionRunner(async (request) => {
call += 1
const isFirst = call === 1
return writingSessionRunner({
tomlPath,
entries,
hashPrefix: isFirst ? 'sha256:doomed-' : 'sha256:survivor-',
gate: isFirst ? firstGate : undefined,
outcome: isFirst ? 'verify-failed' : 'granted'
})(request)
})
const doomed = grantManagedCodexHookTrust(buildPlan(entries))
const survivor = grantManagedCodexHookTrust(buildPlan(entries))
await tick()
await tick()
releaseFirst()
expect(await doomed).toMatchObject({ lane: 'fallback', reason: 'verify-failed' })
expect(await survivor).toMatchObject({ lane: 'rpc' })
const trust = readHookTrustEntries(tomlPath)
const key = normalizeHookTrustKeyForLookup(computeTrustKey(entries[0]))
expect(trust.get(key)?.trustedHash).toBe('sha256:survivor-session_start')
})
it('writes project trust while a grant session is still running, and keeps it', async () => {
codexAppServerCapabilityCache.rememberSupported('wsl:Ubuntu')
const tomlPath = join(runtimeHomeDir, 'config.toml')
const entries = [managedEntry('session_start')]
const workspace = mkdtempSync(join(tmpdir(), 'orca-concurrent-ws-'))
let releaseSession!: () => void
const sessionGate = new Promise<void>((resolve) => {
releaseSession = resolve
})
_internals.setGrantSessionRunner(
writingSessionRunner({
tomlPath,
entries,
hashPrefix: 'sha256:doomed-',
gate: sessionGate,
outcome: 'verify-failed'
})
)
try {
const grant = grantManagedCodexHookTrust(buildPlan(entries))
await tick()
await tick()
await markCodexProjectTrusted(workspace, getLocalCodexTrustConfigFiles(testState.fakeHomeDir))
expect(readFileSync(tomlPath, 'utf-8')).toContain('trust_level = "trusted"')
releaseSession()
expect(await grant).toMatchObject({ lane: 'fallback', reason: 'verify-failed' })
expect(readFileSync(tomlPath, 'utf-8')).toContain('trust_level = "trusted"')
} finally {
rmSync(workspace, { recursive: true, force: true })
}
})
})
describe('concurrent capability probes against a cold host', () => {
it('leaves the waiter config.toml untouched when the shared probe reports unsupported', async () => {
const secondHome = join(testState.userDataDir, 'second-runtime-home')
mkdirSync(secondHome, { recursive: true })
writeFileSync(join(secondHome, 'hooks.json'), '{"hooks":{}}\n', 'utf-8')
const waiterToml = join(secondHome, 'config.toml')
const waiterEntry = {
...managedEntry('session_start'),
sourcePath: join(secondHome, 'hooks.json')
}
upsertHookTrustEntries(waiterToml, [
{ ...waiterEntry, trustedHash: computeTrustedHash(waiterEntry) }
])
const before = readFileSync(waiterToml, 'utf-8')
let releaseProbe!: () => void
const probeGate = new Promise<void>((resolve) => {
releaseProbe = resolve
})
_internals.setGrantSessionRunner(async () => {
await probeGate
throw new CodexAppServerUnsupportedError('hooks/grantTrust: method not found')
})
const first = grantManagedCodexHookTrust(buildPlan([managedEntry('session_start')]))
const waiter = grantManagedCodexHookTrust(
buildPlan([waiterEntry], { runtimeHomePath: secondHome, tomlPath: waiterToml })
)
await tick()
releaseProbe()
await first
expect(await waiter).toMatchObject({ lane: 'fallback', reason: 'unsupported-cached' })
expect(readFileSync(waiterToml, 'utf-8')).toBe(before)
})
})
describe('reentrancy under concurrency', () => {
it('completes a grant nested inside an installer that already holds both lanes', async () => {
const { runExclusivelyForCodexTrustConfig } =
await import('./codex-trust-config-mutation-queue')
const entries = [managedEntry('session_start')]
const tomlPath = join(runtimeHomeDir, 'config.toml')
const systemToml = join(testState.fakeHomeDir, '.codex', 'config.toml')
_internals.setGrantSessionRunner(
writingSessionRunner({ tomlPath, entries, hashPrefix: 'sha256:nested-' })
)
const workspace = mkdtempSync(join(tmpdir(), 'orca-nested-ws-'))
try {
const outcome = await runExclusivelyForCodexTrustConfig(tomlPath, () =>
runExclusivelyForCodexTrustConfig(systemToml, async () => {
await markCodexProjectTrusted(
workspace,
getLocalCodexTrustConfigFiles(testState.fakeHomeDir)
)
return grantManagedCodexHookTrust(buildPlan(entries))
})
)
expect(outcome).toMatchObject({ lane: 'rpc' })
expect(readFileSync(tomlPath, 'utf-8')).toContain('trust_level = "trusted"')
} finally {
rmSync(workspace, { recursive: true, force: true })
}
}, 5000)
})
@@ -49,13 +49,12 @@ vi.mock('../../../agent-hooks/installer-utils-remote', async (importOriginal) =>
import {
isCodexConfigTomlRefusedError,
moveHookTrustEntries,
readHookTrustEntries,
upsertHookTrustEntries,
type CodexTrustEntry
} from './config-toml-trust'
import { computeCodexTrustKey as computeTrustKey } from '../identity/codex-trust-identity'
import { escapeTomlBasicString as escapeTomlString } from '../../configuration/toml/config-toml-syntax'
import { installCodexHooksRemote } from '../../hooks/installation/codex-hook-remote-install'
import '../../hooks/installation/codex-hook-remote-install'
let dir: string
let tomlPath: string
@@ -128,43 +127,4 @@ describe('hook approval writes never break a config.toml Codex can load', () =>
expect(readFileSync(tomlPath, 'utf-8')).toBe(original)
})
it('the SSH installer reports the refusal and leaves the remote config.toml untouched', async () => {
const remoteToml = '/home/u/.codex/config.toml'
const original = 'model = "m"\nhooks = { state = {} }\n'
mocks.remoteFiles.set(remoteToml, original)
mocks.remoteFiles.set('/home/u/.codex/hooks.json', '{"hooks":{}}')
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the mocked remote helpers never touch the SFTP handle.
const status = await installCodexHooksRemote({} as SFTPWrapper, '/home/u')
expect(status).toMatchObject({
state: 'error',
detail: expect.stringContaining('defines hook approvals in a form Orca cannot add to')
})
expect(mocks.remoteFiles.get(remoteToml)).toBe(original)
})
})
describe('reading approvals Codex or the user wrote without a table header', () => {
it('reads an approval written as dotted keys', () => {
writeFileSync(tomlPath, `hooks.state."${tomlKey(stopEntry())}".trusted_hash = "sha256:user"\n`)
expect(readHookTrustEntries(tomlPath).get(computeTrustKey(stopEntry()))).toEqual({
trustedHash: 'sha256:user',
enabled: undefined
})
})
it('reads an approval written as an inline table', () => {
writeFileSync(
tomlPath,
`[hooks]\nstate = { "${tomlKey(stopEntry())}" = { trusted_hash = "sha256:user", enabled = false } }\n`
)
expect(readHookTrustEntries(tomlPath).get(computeTrustKey(stopEntry()))).toEqual({
trustedHash: 'sha256:user',
enabled: false
})
})
})
-97
View File
@@ -1,97 +0,0 @@
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import type * as osModule from 'node:os'
const { getPathMock, homedirMock } = vi.hoisted(() => ({
getPathMock: vi.fn<(name: string) => string>(),
homedirMock: vi.fn<() => string>()
}))
vi.mock('electron', () => ({
app: {
getPath: getPathMock
}
}))
vi.mock('os', async (importOriginal) => {
const actual = await importOriginal<typeof osModule>()
return {
...actual,
homedir: homedirMock
}
})
import { GeminiHookService } from './hook-service'
import { wrapWindowsBatchHookCommand } from '../agent-hooks/windows-direct-cmd-hook-command'
describe('GeminiHookService', () => {
let homeDir: string
let userDataDir: string
beforeAll(() => {
homeDir = mkdtempSync(join(tmpdir(), 'orca-gemini-home-'))
userDataDir = mkdtempSync(join(tmpdir(), 'orca-gemini-userdata-'))
homedirMock.mockReturnValue(homeDir)
getPathMock.mockImplementation((name: string) => {
if (name === 'userData') {
return userDataDir
}
throw new Error(`unexpected getPath(${name})`)
})
})
afterAll(() => {
rmSync(homeDir, { recursive: true, force: true })
rmSync(userDataDir, { recursive: true, force: true })
})
it('preserves user-authored PreToolUse hooks while sweeping stale managed Gemini hooks', () => {
const managedHookFileName = process.platform === 'win32' ? 'gemini-hook.cmd' : 'gemini-hook.sh'
const staleManagedHookPath =
process.platform === 'win32'
? `C:\\Users\\ramzi\\.orca\\agent-hooks\\${managedHookFileName}`
: `/Users/ramzi/.orca/agent-hooks/${managedHookFileName}`
const staleManagedCommand =
process.platform === 'win32'
? staleManagedHookPath
: `if [ -x '${staleManagedHookPath}' ]; then /bin/sh '${staleManagedHookPath}'; fi`
const configDir = join(homeDir, '.gemini')
mkdirSync(configDir, { recursive: true })
writeFileSync(
join(configDir, 'settings.json'),
JSON.stringify(
{
hooks: {
PreToolUse: [
{
hooks: [{ type: 'command', command: staleManagedCommand }]
},
{
hooks: [{ type: 'command', command: 'echo user-authored' }]
}
]
}
},
null,
2
)
)
const status = new GeminiHookService().install()
const config = JSON.parse(readFileSync(join(configDir, 'settings.json'), 'utf8'))
const preToolCommands = config.hooks.PreToolUse.flatMap(
(definition: { hooks?: { command: string }[] }) =>
(definition.hooks ?? []).map((hook) => hook.command)
)
expect(status.state).toBe('installed')
expect(preToolCommands).toEqual(['echo user-authored'])
expect(config.hooks.BeforeTool[0].hooks[0].command).toMatch(
process.platform === 'win32'
? wrapWindowsBatchHookCommand(join(homeDir, '.orca', 'agent-hooks', managedHookFileName))
: new RegExp(managedHookFileName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
)
})
})
-235
View File
@@ -1,235 +0,0 @@
import { homedir } from 'node:os'
import { join, sep } from 'node:path'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type {
AiVaultListResult,
AiVaultSession
} from '../../../shared/ai-vault/contracts/ai-vault-types'
import type * as CachedSessionListModule from '../../ai-vault/cached-session-list'
import type * as SessionParseCacheModule from '../../ai-vault/parse-cache/session-parse-cache-store'
const mocks = vi.hoisted(() => ({
scanLocalSessions: vi.fn(),
resolveAiVaultSessionTitlesInService: vi.fn(),
listSubagentsInService: vi.fn(),
scanRuntimeAiVaultSessions: vi.fn(),
getAiVaultWslHomeDirs: vi.fn(),
ipcHandle: vi.fn(),
trashItem: vi.fn(),
deleteAiVaultSessionFile: vi.fn(),
invalidateAiVaultSessionListCache: vi.fn(),
invalidateSessionParseCacheEntry: vi.fn()
}))
vi.mock('electron', () => ({
app: { on: vi.fn() },
ipcMain: { handle: mocks.ipcHandle },
shell: { trashItem: mocks.trashItem }
}))
vi.mock('../../ai-vault/scan-service/session-scanner-service-spawn', async (importOriginal) => ({
...(await importOriginal<object>()),
scanAiVaultSessionsInService: mocks.scanLocalSessions,
resolveAiVaultSessionTitlesInService: mocks.resolveAiVaultSessionTitlesInService,
listAiVaultSubagentSessionsInService: mocks.listSubagentsInService
}))
vi.mock('../../ai-vault/session-delete', () => ({
deleteAiVaultSessionFile: mocks.deleteAiVaultSessionFile
}))
// Mock only invalidation so routing and cache tests retain the real list implementation.
vi.mock('../../ai-vault/cached-session-list', async (importOriginal) => {
const actual = await importOriginal<typeof CachedSessionListModule>()
return {
...actual,
invalidateAiVaultSessionListCache: mocks.invalidateAiVaultSessionListCache
}
})
vi.mock('../../ai-vault/parse-cache/session-parse-cache-store', async (importOriginal) => {
const actual = await importOriginal<typeof SessionParseCacheModule>()
return {
...actual,
invalidateSessionParseCacheEntry: mocks.invalidateSessionParseCacheEntry
}
})
vi.mock('../../wsl/discovery/distro-catalog', () => ({
listRunningWslDistrosAsync: vi.fn().mockResolvedValue([]),
listRunningWslHomeDirsAsync: vi.fn().mockResolvedValue([]),
hasCachedWslDistros: vi.fn(() => false)
}))
const { resolveOmpSessionsDir } = await import('../../ai-vault/scanners/omp/omp-session-root')
const { _internals, registerAiVaultHandlers } = await import('./ai-vault')
const { deleteAiVaultSession: deleteAiVaultSessionWithDeps } = await import('./ai-vault-delete')
await import('../../ai-vault/host-session-delete')
beforeEach(() => {
vi.clearAllMocks()
_internals.resetAiVaultCacheForTests()
mocks.scanLocalSessions.mockResolvedValue(result([session('local', 'local-session')]))
mocks.resolveAiVaultSessionTitlesInService.mockResolvedValue({ titles: [] })
mocks.listSubagentsInService.mockResolvedValue({ sessions: [], issues: [] })
mocks.scanRuntimeAiVaultSessions.mockResolvedValue(
result([session('runtime:remote-server', 'runtime-session')])
)
})
describe('prepareSessionResume IPC', () => {
it('preserves SSH session homes without reading their paths locally', async () => {
const prepareSessionResume = vi.fn()
const prepareRuntimeSessionResume = vi.fn()
registerAiVaultHandlers({ prepareSessionResume, prepareRuntimeSessionResume })
await expect(
getPrepareSessionResumeHandler()(
{},
{
agent: 'codex',
filePath: '/managed/sessions/2026/07/20/rollout-a.jsonl',
codexHome: '/managed',
executionHostId: 'ssh:dev-box'
}
)
).resolves.toEqual({ useRealCodexHome: false })
expect(prepareSessionResume).not.toHaveBeenCalled()
expect(prepareRuntimeSessionResume).not.toHaveBeenCalled()
})
})
function getPrepareSessionResumeHandler(): (
event: unknown,
args: unknown
) => Promise<{ useRealCodexHome: boolean }> {
const registration = mocks.ipcHandle.mock.calls.find(
([channel]) => channel === 'aiVault:prepareSessionResume'
)
if (!registration) {
throw new Error('aiVault:prepareSessionResume was not registered')
}
return registration[1]
}
describe('listAiVaultSubagentSessions gating', () => {
const claudeRoot = join(homedir(), '.claude', 'projects')
it('rejects a dot-segment traversal out of the Claude projects root', async () => {
// Built with sep (not join) so the `..` segments survive into the arg.
const traversal = [claudeRoot, '..', '..', '..', 'etc', 'passwd.jsonl'].join(sep)
const result = await _internals.listAiVaultSubagentSessions({
agent: 'claude',
parentFilePath: traversal,
executionHostId: 'local'
})
expect(result).toEqual({ sessions: [], issues: [] })
expect(mocks.listSubagentsInService).not.toHaveBeenCalled()
})
it('rejects an OMP path that only sits inside another agent root', async () => {
// Each agent's allowlist is its own root: a Claude path must not be
// readable through the OMP branch (or vice versa).
const crossAgent = await _internals.listAiVaultSubagentSessions({
agent: 'omp',
parentFilePath: join(claudeRoot, 'proj', 'sess.jsonl'),
executionHostId: 'local'
})
const traversal = await _internals.listAiVaultSubagentSessions({
agent: 'omp',
// Built with sep (not join) so the `..` segments survive into the arg.
parentFilePath: [resolveOmpSessionsDir(), '..', '..', '..', 'etc', 'passwd.jsonl'].join(sep),
executionHostId: 'local'
})
expect(crossAgent).toEqual({ sessions: [], issues: [] })
expect(traversal).toEqual({ sessions: [], issues: [] })
expect(mocks.listSubagentsInService).not.toHaveBeenCalled()
})
})
describe('deleteAiVaultSession', () => {
const args = {
agent: 'gemini' as const,
sessionId: 'session-1',
filePath: '/home/ada/.gemini/tmp/sess.json',
executionHostId: 'local' as const
}
it("routes a server-hosted session to its owning host and drops that host's cached list", async () => {
const deleteRuntimeAiVaultSession = vi.fn().mockResolvedValue({ outcome: 'deleted' })
const invalidateMultiHostListCache = vi.fn()
const result = await deleteAiVaultSessionWithDeps(
{ ...args, executionHostId: 'runtime:gpu%20box' },
{ invalidateMultiHostListCache, deleteRuntimeSession: deleteRuntimeAiVaultSession }
)
expect(result).toEqual({ outcome: 'deleted' })
expect(deleteRuntimeAiVaultSession).toHaveBeenCalledWith(
'gpu box',
expect.objectContaining({ agent: 'gemini', filePath: args.filePath })
)
expect(invalidateMultiHostListCache).toHaveBeenCalledTimes(1)
// This computer never touches a path that belongs to another host.
expect(mocks.deleteAiVaultSessionFile).not.toHaveBeenCalled()
})
it('never routes the unresolved-owner sentinel to a host', async () => {
const deleteRuntimeAiVaultSession = vi.fn()
mocks.deleteAiVaultSessionFile.mockResolvedValue({
outcome: 'rejected',
agent: 'gemini',
reason: 'non-local-host'
})
await deleteAiVaultSessionWithDeps(
{ ...args, executionHostId: 'runtime:unresolved-owner' },
{
invalidateMultiHostListCache: vi.fn(),
deleteRuntimeSession: deleteRuntimeAiVaultSession
}
)
expect(deleteRuntimeAiVaultSession).not.toHaveBeenCalled()
})
})
function result(sessions: AiVaultSession[]): AiVaultListResult {
return { sessions, issues: [], scannedAt: new Date().toISOString() }
}
function session(
executionHostId: AiVaultSession['executionHostId'],
sessionId: string
): AiVaultSession {
return {
id: `${executionHostId}:codex:${sessionId}:/tmp/${sessionId}.jsonl`,
executionHostId,
agent: 'codex',
sessionId,
title: sessionId,
cwd: '/repo',
branch: null,
model: null,
filePath: `/tmp/${sessionId}.jsonl`,
codexHome: null,
createdAt: null,
updatedAt:
sessionId === 'runtime-session'
? '2026-07-04T03:00:00.000Z'
: sessionId === 'remote-session'
? '2026-07-04T02:00:00.000Z'
: '2026-07-04T01:00:00.000Z',
modifiedAt: '2026-07-04T00:00:00.000Z',
messageCount: 1,
totalTokens: 0,
previewMessages: [],
queuedMessageCount: 0,
subagentTranscriptCount: 0,
resumeCommand: `codex resume ${sessionId}`,
subagent: null
}
}
@@ -1,371 +0,0 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type * as DocPreviewGuestPolicyModule from '../../browser/document-preview/doc-preview-guest-policy'
import type * as TabRegistrationWaitModule from './browser-tab-registration-wait'
const {
handleMock,
removeHandlerMock,
getAuthorizedGuestMock,
setGrabModeMock,
awaitGrabSelectionMock,
cancelGrabOpMock,
captureSelectionScreenshotMock,
extractHoverPayloadMock,
setAnnotationViewportBridgeMock,
openDevToolsMock,
setViewportOverrideMock,
previewAuthoritySpy,
registrationWaitSpy
} = vi.hoisted(() => ({
handleMock: vi.fn(),
removeHandlerMock: vi.fn(),
getAuthorizedGuestMock: vi.fn(),
setGrabModeMock: vi.fn().mockResolvedValue(true),
awaitGrabSelectionMock: vi.fn().mockResolvedValue({ opId: 'op', kind: 'cancelled' }),
cancelGrabOpMock: vi.fn(),
captureSelectionScreenshotMock: vi.fn().mockResolvedValue(null),
extractHoverPayloadMock: vi.fn().mockResolvedValue(null),
setAnnotationViewportBridgeMock: vi.fn().mockResolvedValue(true),
openDevToolsMock: vi.fn().mockResolvedValue(true),
setViewportOverrideMock: vi.fn().mockResolvedValue(true),
previewAuthoritySpy: vi.fn(),
registrationWaitSpy: vi.fn()
}))
vi.mock('./browser-passwords-ipc', () => ({ registerBrowserPasswordsHandlers: vi.fn() }))
vi.mock('electron', () => ({
BrowserWindow: { fromWebContents: vi.fn() },
ipcMain: { removeHandler: removeHandlerMock, handle: handleMock },
webContents: { fromId: vi.fn(() => ({ isDestroyed: () => false })) }
}))
vi.mock('../../browser/browser-manager', () => ({
browserCertificateTrustController: { proceed: vi.fn(() => ({ ok: true })) },
browserManager: {
registerGuest: vi.fn(() => true),
attachGuestPolicies: vi.fn(),
unregisterGuest: vi.fn(),
getGuestWebContentsId: vi.fn(),
getWebContentsIdByTabId: vi.fn(() => new Map()),
getWorktreeIdForTab: vi.fn(),
getAuthorizedGuest: getAuthorizedGuestMock,
setGrabMode: setGrabModeMock,
awaitGrabSelection: awaitGrabSelectionMock,
cancelGrabOp: cancelGrabOpMock,
captureSelectionScreenshot: captureSelectionScreenshotMock,
extractHoverPayload: extractHoverPayloadMock,
setAnnotationViewportBridge: setAnnotationViewportBridgeMock,
openDevTools: openDevToolsMock,
setViewportOverride: setViewportOverrideMock,
cancelDownload: vi.fn()
}
}))
// Spy on the real policy so registry selection remains observable.
vi.mock('../../browser/document-preview/doc-preview-guest-policy', async (importOriginal) => {
const actual = await importOriginal<typeof DocPreviewGuestPolicyModule>()
return {
...actual,
getWorkspaceDocPageGuest: (browserPageId: string, senderWebContentsId: number) => {
previewAuthoritySpy(browserPageId, senderWebContentsId)
return actual.getWorkspaceDocPageGuest(browserPageId, senderWebContentsId)
}
}
})
// Keep registration waits real; observe only which targets enter them.
vi.mock('./browser-tab-registration-wait', async (importOriginal) => {
const actual = await importOriginal<typeof TabRegistrationWaitModule>()
return {
...actual,
waitForNextTabRegistration: (...args: Parameters<typeof actual.waitForNextTabRegistration>) => {
registrationWaitSpy(args[0])
return actual.waitForNextTabRegistration(...args)
}
}
})
import { registerBrowserHandlers } from './browser'
import { setTrustedBrowserRendererWebContentsId } from './browser-renderer-trust'
import { browserManager } from '../../browser/browser-manager'
import {
getWorkspaceDocPageGuest,
installDocPreviewGuestPolicy
} from '../../browser/document-preview/doc-preview-guest-policy'
import {
mintDocPreviewGrant,
revokeAllDocPreviewGrants
} from '../../browser/document-preview/doc-preview-grant-registry'
import { buildDocPreviewUrl } from '../../../shared/doc-preview-scheme'
const HOST_RENDERER_ID = 91
const OTHER_RENDERER_ID = 92
// Match browser-grab-ipc's registration timeout.
const GRAB_REGISTRATION_WAIT_MS = 1_000
/** Exhaustively classify invoke channels as guest tools or browser/session/profile management. */
const TOOL_CHANNELS = [
'browser:setGrabMode',
'browser:awaitGrabSelection',
'browser:cancelGrab',
'browser:captureSelectionScreenshot',
'browser:extractHoverPayload',
'browser:setAnnotationViewportBridge'
]
type Handler = (event: { sender: Electron.WebContents }, args: unknown) => unknown
function registeredHandlers(): Map<string, Handler> {
const handlers = new Map<string, Handler>()
for (const [channel, handler] of handleMock.mock.calls as [string, Handler][]) {
handlers.set(channel, handler)
}
return handlers
}
function trustedSender(id: number): { sender: Electron.WebContents } {
return {
sender: {
id,
isDestroyed: () => false,
getType: () => 'window',
getURL: () => 'file:///renderer/index.html'
} as unknown as Electron.WebContents
}
}
let nextDocPageOrdinal = 0
function grantForNewDocPage(): { id: string; browserPageId: string } {
nextDocPageOrdinal += 1
const browserPageId = `doc-page-${nextDocPageOrdinal}`
const grant = mintDocPreviewGrant({
owner: { kind: 'ssh', connectionId: 'ssh-1' },
root: '/home/alice/docs',
entryRelativePath: 'index.html',
browserPageId
})
return { id: grant.id, browserPageId }
}
type PreviewGuestContents = {
isDestroyed: () => boolean
getURL: () => string
}
function renderPreviewForGrant(
grant: { id: string; browserPageId: string },
hostId: number = HOST_RENDERER_ID
): {
grantId: string
browserPageId: string
contents: PreviewGuestContents
markContentsDestroyed: () => void
} {
const browserPageId = grant.browserPageId
const handlers: Record<string, (...args: never[]) => void> = {}
const register = (event: string, handler: (...args: never[]) => void): void => {
handlers[event] = handler
}
// Policy attaches mid-load, when the guest already reports its document URL.
const documentUrl = buildDocPreviewUrl(grant.id, 'index.html')
let contentsDestroyed = false
const guest = {
isFocused: () => true,
isDestroyed: () => contentsDestroyed,
getURL: () => documentUrl,
on: vi.fn(register),
once: vi.fn(register),
setWindowOpenHandler: vi.fn(),
setWebRTCIPHandlingPolicy: vi.fn()
}
installDocPreviewGuestPolicy(guest as never, { id: hostId, send: vi.fn() })
handlers['did-start-navigation']?.({ url: documentUrl, isMainFrame: true } as never)
return {
grantId: grant.id,
browserPageId,
contents: guest,
// Contents can be destroyed before Chromium delivers the destroyed event.
markContentsDestroyed: () => {
contentsDestroyed = true
}
}
}
function liveRenderedPreview(
hostId: number = HOST_RENDERER_ID
): ReturnType<typeof renderPreviewForGrant> {
return renderPreviewForGrant(grantForNewDocPage(), hostId)
}
/** Minimal well-formed args per channel, so a refusal is authorization and not shape validation. */
function toolArgs(channel: string, browserPageId: string): Record<string, unknown> {
switch (channel) {
case 'browser:setGrabMode':
return { browserPageId, enabled: true }
case 'browser:awaitGrabSelection':
return { browserPageId, opId: 'op-1' }
case 'browser:captureSelectionScreenshot':
return { browserPageId, rect: { x: 0, y: 0, width: 10, height: 10 } }
case 'browser:setAnnotationViewportBridge':
return {
browserPageId,
enabled: true,
emitViewport: true,
markers: [],
token: 'annotation-bridge-token-1'
}
default:
return { browserPageId }
}
}
// Unwrap viewport resolvers as well as direct guest arguments.
function resolvesToGuest(argument: unknown, guest: PreviewGuestContents): boolean {
return argument === guest || (typeof argument === 'function' && argument() === guest)
}
const GUEST_RECEIVING_MOCKS = [
setGrabModeMock,
awaitGrabSelectionMock,
captureSelectionScreenshotMock,
extractHoverPayloadMock,
setAnnotationViewportBridgeMock
]
beforeEach(() => {
vi.stubEnv('ELECTRON_RENDERER_URL', '')
setTrustedBrowserRendererWebContentsId(HOST_RENDERER_ID)
// Revoke before clearing mocks so disposal calls belong to the previous test.
revokeAllDocPreviewGrants()
vi.clearAllMocks()
// This suite checks channel authorization; real manager registry splitting is covered separately.
getAuthorizedGuestMock.mockImplementation((browserPageId: string, senderWebContentsId: number) =>
getWorkspaceDocPageGuest(browserPageId, senderWebContentsId)
)
setGrabModeMock.mockResolvedValue(true)
awaitGrabSelectionMock.mockResolvedValue({ opId: 'op-1', kind: 'cancelled' })
captureSelectionScreenshotMock.mockResolvedValue(null)
extractHoverPayloadMock.mockResolvedValue(null)
setAnnotationViewportBridgeMock.mockResolvedValue(true)
registerBrowserHandlers({ getSettings: vi.fn() })
// Fake timers settle registration waits for guests that never attach.
vi.useFakeTimers()
})
afterEach(() => {
vi.useRealTimers()
})
async function settle<T>(pending: Promise<T> | T | undefined): Promise<T | undefined> {
await vi.advanceTimersByTimeAsync(GRAB_REGISTRATION_WAIT_MS)
return pending
}
describe('doc preview tool authorization', () => {
it.each(TOOL_CHANNELS)('drives the preview guest from %s', async (channel) => {
const preview = liveRenderedPreview()
const handler = registeredHandlers().get(channel)
await handler?.(trustedSender(HOST_RENDERER_ID), toolArgs(channel, preview.browserPageId))
// Assert the actual WebContents receiving the tool operation.
const receivedGuest = GUEST_RECEIVING_MOCKS.some((mock) =>
mock.mock.calls.some((args) => args.some((arg) => resolvesToGuest(arg, preview.contents)))
)
expect(receivedGuest || cancelGrabOpMock.mock.calls.length > 0).toBe(true)
})
// Browser/session management must never consult the document registry.
it.each(TOOL_CHANNELS)(
'refuses %s from a renderer that does not host the preview',
async (channel) => {
const preview = liveRenderedPreview()
const handler = registeredHandlers().get(channel)
await settle(
handler?.(trustedSender(OTHER_RENDERER_ID), toolArgs(channel, preview.browserPageId))
)
for (const mock of [...GUEST_RECEIVING_MOCKS, cancelGrabOpMock]) {
expect(mock).not.toHaveBeenCalled()
}
}
)
// A live grant and trusted sender are insufficient once contents are destroyed.
it.each(TOOL_CHANNELS)('refuses %s once the preview contents are destroyed', async (channel) => {
const preview = liveRenderedPreview()
preview.markContentsDestroyed()
const handler = registeredHandlers().get(channel)
await settle(
handler?.(trustedSender(HOST_RENDERER_ID), toolArgs(channel, preview.browserPageId))
)
for (const mock of [...GUEST_RECEIVING_MOCKS, cancelGrabOpMock]) {
expect(mock).not.toHaveBeenCalled()
}
})
// Document tools wait for their own attaching guest instead of returning not-ready immediately.
it('waits for a document page whose guest has not attached yet, and arms when it does', async () => {
const grant = grantForNewDocPage()
const handler = registeredHandlers().get('browser:setGrabMode')
const pending = handler?.(trustedSender(HOST_RENDERER_ID), {
browserPageId: grant.browserPageId,
enabled: true
})
await vi.advanceTimersByTimeAsync(0)
expect(registrationWaitSpy).toHaveBeenCalledWith(grant.browserPageId)
const preview = renderPreviewForGrant(grant)
await expect(pending).resolves.toEqual({ ok: true })
expect(setGrabModeMock).toHaveBeenCalledWith(grant.browserPageId, true, preview.contents)
})
// Unrendered documents must eventually answer not-ready.
// Revocation, including re-minting, is how previews withdraw and release retained tool state.
// unregisterGuest must not drop preview intent and silently settle its grab without arming.
it('leaves an in-flight preview grab armed when unregisterGuest names its target', async () => {
const preview = liveRenderedPreview()
const handlers = registeredHandlers()
const pending = handlers.get('browser:setGrabMode')?.(trustedSender(HOST_RENDERER_ID), {
browserPageId: preview.browserPageId,
enabled: true
})
// Intent exists before the queued operation runs; test disposal in that gap.
const unregistered = handlers.get('browser:unregisterGuest')?.(
trustedSender(HOST_RENDERER_ID),
{ browserPageId: preview.browserPageId }
)
// A dropped intent also returns ok; only the driven guest proves the grab armed.
await expect(pending).resolves.toEqual({ ok: true })
expect(setGrabModeMock).toHaveBeenCalledWith(preview.browserPageId, true, preview.contents)
expect(unregistered).toBe(false)
expect(vi.mocked(browserManager.unregisterGuest)).not.toHaveBeenCalled()
})
// The same guard must still allow browser tabs to dispose their grabs.
// An open document must not answer for a browsing page ID.
it('never hands a browsing page id the guest of a document that is open', async () => {
const preview = liveRenderedPreview()
const handler = registeredHandlers().get('browser:extractHoverPayload')
await handler?.(trustedSender(HOST_RENDERER_ID), { browserPageId: 'browser-page-1' })
expect(getAuthorizedGuestMock).toHaveBeenCalledWith('browser-page-1', HOST_RENDERER_ID)
expect(previewAuthoritySpy).toHaveBeenCalledWith('browser-page-1', HOST_RENDERER_ID)
expect(extractHoverPayloadMock).not.toHaveBeenCalled()
await handler?.(trustedSender(HOST_RENDERER_ID), { browserPageId: preview.browserPageId })
expect(extractHoverPayloadMock).toHaveBeenCalledWith(preview.browserPageId, preview.contents)
})
})
@@ -1,103 +0,0 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const mocks = vi.hoisted(() => ({
getWebContentsIdByTabId: vi.fn(() => new Map<string, number>()),
getWorktreeIdForTab: vi.fn(() => undefined as string | undefined),
getGuestWebContentsId: vi.fn(() => null as number | null),
webContentsFromId: vi.fn(() => null as unknown)
}))
vi.mock('electron', () => ({ webContents: { fromId: mocks.webContentsFromId } }))
vi.mock('../../browser/browser-manager', () => ({
browserManager: {
getWebContentsIdByTabId: mocks.getWebContentsIdByTabId,
getWorktreeIdForTab: mocks.getWorktreeIdForTab,
getGuestWebContentsId: mocks.getGuestWebContentsId
}
}))
import {
waitForAnyTabRegistration,
waitForNextTabRegistration,
waitForWorktreeTabRegistration
} from './browser-tab-registration-wait'
import { installDocPreviewGuestPolicy } from '../../browser/document-preview/doc-preview-guest-policy'
import {
mintDocPreviewGrant,
revokeAllDocPreviewGrants
} from '../../browser/document-preview/doc-preview-grant-registry'
import { buildDocPreviewUrl } from '../../../shared/doc-preview-scheme'
const WAIT_MS = 1_000
function attachDocumentGuest(browserPageId: string): void {
const grant = mintDocPreviewGrant({
owner: { kind: 'ssh', connectionId: 'ssh-1' },
root: '/home/alice/docs',
entryRelativePath: 'index.html',
browserPageId
})
const guest = {
isFocused: () => false,
isDestroyed: () => false,
getURL: () => buildDocPreviewUrl(grant.id, grant.entryRelativePath),
on: vi.fn(),
once: vi.fn(),
setWindowOpenHandler: vi.fn(),
setWebRTCIPHandlingPolicy: vi.fn()
}
installDocPreviewGuestPolicy(guest as never, { id: 91, send: vi.fn() })
}
function track(pending: Promise<void>): () => 'pending' | 'resolved' | 'rejected' {
let state: 'pending' | 'resolved' | 'rejected' = 'pending'
pending.then(
() => {
state = 'resolved'
},
() => {
state = 'rejected'
}
)
return () => state
}
beforeEach(() => {
revokeAllDocPreviewGrants()
vi.clearAllMocks()
mocks.getWebContentsIdByTabId.mockReturnValue(new Map())
mocks.getGuestWebContentsId.mockReturnValue(null)
vi.useFakeTimers()
})
afterEach(() => {
vi.useRealTimers()
})
describe('a workspace document registering', () => {
it('settles a wait that already names its page', async () => {
const settled = track(waitForNextTabRegistration('doc-page-1', WAIT_MS))
attachDocumentGuest('doc-page-1')
await vi.advanceTimersByTimeAsync(0)
expect(settled()).toBe('resolved')
})
// CLI/agent any-tab waits need drivable browser tabs; documents satisfy only their own page wait.
it('leaves the waits that ask for any browser tab still waiting', async () => {
const worktreeWait = track(waitForWorktreeTabRegistration('wt-1', WAIT_MS))
const anyWait = track(waitForAnyTabRegistration(WAIT_MS))
attachDocumentGuest('doc-page-2')
await vi.advanceTimersByTimeAsync(0)
expect(worktreeWait()).toBe('pending')
expect(anyWait()).toBe('pending')
// Verify unsatisfied waits actually time out.
await vi.advanceTimersByTimeAsync(WAIT_MS)
expect(worktreeWait()).toBe('rejected')
expect(anyWait()).toBe('rejected')
})
})
-137
View File
@@ -1,137 +0,0 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
const mocks = vi.hoisted(() => ({
handlers: new Map<string, (event: unknown, ...args: unknown[]) => unknown>(),
listeners: new Map<string, (event: unknown, ...args: unknown[]) => unknown>(),
isTrustedBrowserRenderer: vi.fn(),
reportDocPreviewLinkClick: vi.fn(),
getGuestWebContentsId: vi.fn()
}))
vi.mock('electron', () => ({
ipcMain: {
handle: (channel: string, handler: (event: unknown, ...args: unknown[]) => unknown) => {
mocks.handlers.set(channel, handler)
},
on: (channel: string, listener: (event: unknown, ...args: unknown[]) => unknown) => {
mocks.listeners.set(channel, listener)
}
}
}))
vi.mock('./browser/browser-renderer-trust', () => ({
isTrustedBrowserRenderer: mocks.isTrustedBrowserRenderer
}))
vi.mock('../browser/document-preview/doc-preview-guest-policy', () => ({
reportDocPreviewLinkClick: mocks.reportDocPreviewLinkClick
}))
vi.mock('../browser/browser-manager', () => ({
browserManager: { getGuestWebContentsId: mocks.getGuestWebContentsId }
}))
import {
registerDocPreviewGrantHandlers,
type DocPreviewGrantRequest
} from './doc-preview-grant-ipc'
import {
getDocPreviewGrant,
revokeAllDocPreviewGrants
} from '../browser/document-preview/doc-preview-grant-registry'
import {
DOC_PREVIEW_AUTHORIZE_DIRECTORY_CHANNEL,
DOC_PREVIEW_MINT_GRANT_CHANNEL,
DOC_PREVIEW_REVOKE_GRANT_CHANNEL
} from '../../shared/doc-preview-scheme'
const REQUEST: DocPreviewGrantRequest = {
owner: { kind: 'ssh', connectionId: 'ssh-1' },
requestBase: '/home/alice/docs',
root: '/home/alice/docs',
entryRelativePath: 'index.html',
browserPageId: 'doc-page-1'
}
const sender = { id: 7 }
function mint(request: DocPreviewGrantRequest = REQUEST): { grantId: string; url: string } {
const handler = mocks.handlers.get(DOC_PREVIEW_MINT_GRANT_CHANNEL)
if (!handler) {
throw new Error('mint handler not registered')
}
return handler({ sender }, request) as { grantId: string; url: string }
}
function revoke(grantId: string): boolean {
const handler = mocks.handlers.get(DOC_PREVIEW_REVOKE_GRANT_CHANNEL)
if (!handler) {
throw new Error('revoke handler not registered')
}
return handler({ sender }, grantId) as boolean
}
function authorize(grantId: unknown, relativePath: unknown): boolean {
const handler = mocks.handlers.get(DOC_PREVIEW_AUTHORIZE_DIRECTORY_CHANNEL)
if (!handler) {
throw new Error('authorize handler not registered')
}
return handler({ sender }, grantId, relativePath) as boolean
}
beforeEach(() => {
mocks.handlers.clear()
mocks.listeners.clear()
vi.clearAllMocks()
revokeAllDocPreviewGrants()
mocks.isTrustedBrowserRenderer.mockReturnValue(true)
mocks.getGuestWebContentsId.mockReturnValue(null)
registerDocPreviewGrantHandlers()
})
describe('document preview grant handlers', () => {
// Keep browser/document registries disjoint when minting the document identity.
it('refuses to make a page that already hosts a browsing guest into a document page', () => {
mocks.getGuestWebContentsId.mockReturnValue(42)
expect(() => mint()).toThrow(/browsing page/)
expect(mocks.getGuestWebContentsId).toHaveBeenCalledWith('doc-page-1')
})
// Untrusted senders must receive no filesystem-read grant.
it('mints nothing for a sender that is not the trusted renderer', () => {
mocks.isTrustedBrowserRenderer.mockReturnValue(false)
expect(() => mint()).toThrow(/Untrusted/)
})
it('refuses to revoke on behalf of a sender that is not the trusted renderer', () => {
const result = mint()
mocks.isTrustedBrowserRenderer.mockReturnValue(false)
expect(revoke(result.grantId)).toBe(false)
expect(getDocPreviewGrant(result.grantId)).not.toBeNull()
})
it('lets only the trusted renderer authorize a valid requested directory', () => {
const result = mint({
...REQUEST,
requestBase: '/home/alice',
root: '/home/alice/docs',
entryRelativePath: 'docs/index.html'
})
expect(authorize(result.grantId, 'assets/app.js')).toBe(true)
expect(getDocPreviewGrant(result.grantId)?.authorizedRoots).toEqual(['/home/alice/assets'])
mocks.isTrustedBrowserRenderer.mockReturnValue(false)
expect(authorize(result.grantId, 'secrets/token.txt')).toBe(false)
expect(getDocPreviewGrant(result.grantId)?.authorizedRoots).toEqual(['/home/alice/assets'])
})
it('refuses malformed directory authorization arguments', () => {
const result = mint()
expect(authorize(result.grantId, '../secret.txt')).toBe(false)
expect(authorize(result.grantId, null)).toBe(false)
expect(authorize(null, 'assets/app.js')).toBe(false)
expect(authorize('0'.repeat(32), 'assets/app.js')).toBe(false)
})
})
@@ -1,453 +0,0 @@
import { getEventListeners } from 'node:events'
import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { PassThrough } from 'node:stream'
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
import { createAcpAgentConnection } from '../../../../acp/acp-agent-connection'
import { openAcpAdapterRig } from '../../../../acp/acp-structured-adapter.test-support'
import { readAntigravitySignInLine } from '../../../../antigravity/antigravity-sign-in-url'
import { findInErrorChain } from '../../../../provider-process/provider-error-chain'
import { createFakePipedChild } from '../../../../../shared/__fixtures__/fake-spawned-child'
import { closeProviderTimelineRigs } from '../../../agent-session-timeline/provider-timeline-assembler-test-support'
import { computeAgentSessionPayloadFingerprint } from '../../../../../shared/agent-session/lifecycle/agent-session-mutation-envelope'
import { AgentSessionStartFailure } from '../../startup/structured-agent-session-start-failure'
import { CodexAppServerExitError } from '../../../../codex/app-server/codex-app-server-exit-error'
import { CodexAppServerRequestError } from '../../../../codex/app-server/codex-app-server-request-error'
import {
adapterFor,
fakeCodex,
THREAD_ID
} from '../../../../codex/structured/session-lifecycle/codex-structured-session-adapter-fixture'
import { openTestAgentSessionRecordStore } from '../../../../runtime/agent-session/records/agent-session-record-store-test-harness'
import {
closeTestJournalHostDatabases,
openTestJournalHostDatabase
} from '../../../agent-session-journal/store-lifecycle/journal-host-database-test-support'
import {
claudeAndCodexAgents,
claudeAndCodexRouter
} from '../../providers/testing/structured-agent-session-adapter-router-test-support'
import type * as ExitObservation from '../../termination/structured-agent-session-exit-observation'
import { StructuredAgentSessionHost } from '../structured-agent-session-host'
import {
HOST_TEST_NOW as NOW,
HOST_TEST_SESSION as SESSION,
hostTestAttachParams,
hostTestOperationId,
resetHostTestOperationIds
} from './structured-agent-session-host-test-data'
import { createStructuredAgentSessionLogger } from '../structured-agent-session-logger'
import { restTestSend } from './structured-agent-session-rest-test-rig'
import type * as StartFailure from '../../startup/structured-agent-session-start-failure'
// Every adapter's startup receipts and every acquisition's root-exit receipt, to count what they
// still hold after each start, whichever adapter or sink created them.
const created = vi.hoisted(() => ({
endings: new Array<StartFailure.AgentSessionStartEndings>(),
rootExits: new Array<ExitObservation.StructuredAgentSessionRootExitReceipt>()
}))
const retainedReceipts = () =>
created.endings.reduce((sum, endings) => sum + endings['endings'].size, 0)
vi.mock('../../startup/structured-agent-session-start-failure', async (original) => {
const actual = await original<typeof StartFailure>()
class CountedEndings extends actual.AgentSessionStartEndings {
constructor() {
super()
created.endings.push(this)
}
}
return { ...actual, AgentSessionStartEndings: CountedEndings }
})
vi.mock('../../termination/structured-agent-session-exit-observation', async (original) => {
const actual = await original<typeof ExitObservation>()
class CountedRootExit extends actual.StructuredAgentSessionRootExitReceipt {
constructor() {
super()
created.rootExits.push(this)
}
}
return { ...actual, StructuredAgentSessionRootExitReceipt: CountedRootExit }
})
const CALLER = { callerKey: 'client-1' }
const CEILING_MS = 20_000
type Cycle = 'ready' | 'final' | 'retried' | 'expired' | 'stopped'
const CYCLES: readonly Cycle[] = ['ready', 'final', 'retried', 'expired', 'stopped']
const SCRIPTS: Record<Cycle, ('ok' | 'refuse' | 'hang')[]> = {
ready: ['ok'],
final: ['refuse'],
retried: ['hang', 'ok'],
expired: ['hang'],
stopped: ['hang']
}
let root: string
beforeEach(async () => {
vi.useFakeTimers({
toFake: ['Date', 'setTimeout', 'clearTimeout', 'setInterval', 'clearInterval']
})
root = await mkdtemp(join(tmpdir(), 'orca-start-lifecycle-'))
resetHostTestOperationIds()
created.endings.length = 0
created.rootExits.length = 0
})
afterEach(async () => {
try {
await closeTestJournalHostDatabases()
await rm(root, { recursive: true, force: true })
} finally {
vi.useRealTimers()
vi.restoreAllMocks()
}
})
/** Runs `count` rounds of each start outcome through the production host, router and Codex adapter. */
async function runStarts(count: number) {
// Each thread/start or thread/resume takes the next scripted answer.
const answers: ('ok' | 'refuse' | 'hang')[] = []
const threadAnswer = () => {
const answer = answers.shift()
if (answer === 'refuse') {
throw new CodexAppServerRequestError('thread/start', -32600, 'invalid', 'invalid')
}
return answer === 'hang'
? new Promise(() => {})
: { thread: { id: THREAD_ID }, model: 'gpt-live', reasoningEffort: 'medium' }
}
const codex = fakeCodex({ 'thread/start': threadAnswer, 'thread/resume': threadAnswer })
let host: StructuredAgentSessionHost | undefined
const store = await openTestAgentSessionRecordStore(join(root, `starts-${count}`))
const adapter = adapterFor(codex, {}, [], {
// Once a thread exists, each new child resumes it.
resolveLaunch: async () => ({
command: 'codex',
args: ['app-server'],
cwd: '/work/repo',
codexHome: null,
resumeThreadId: store.getRecord(SESSION)?.providerHandleChain.length ? THREAD_ID : null
}),
onEvent: (event) => {
if (event.type === 'ended' && 'cause' in event) {
void host?.handleAdapterEvent(event)
}
}
})
const other = adapterFor(fakeCodex())
const router = claudeAndCodexRouter({ claude: other, codex: adapter }, async () => {
await adapter.closeAll()
await other.closeAll()
})
const dispatch = vi.fn(async () => ({
state: 'accepted' as const,
providerIdentity: {
provider: 'codex' as const,
threadId: THREAD_ID,
turnId: `turn-${dispatch.mock.calls.length}`,
ordinal: dispatch.mock.calls.length
}
}))
// Turn work is not under test; every start-path call reaches the production router.
Object.assign(router, { dispatch })
host = new StructuredAgentSessionHost({
agents: claudeAndCodexAgents({ claude: other, codex: adapter }),
logger: createStructuredAgentSessionLogger(),
store,
adapter: router,
journalDatabase: openTestJournalHostDatabase(join(root, `starts-${count}`)),
claimKeyId: 'key-1',
mintSpawnToken: () => `spawn-${codex.connections.length}`,
startupLimits: { ceilingMs: CEILING_MS },
idleSweep: { intervalMs: 60 * 60_000, idleMs: () => 60 * 60_000 },
now: () => NOW
})
const { runtimeState } = host.collaboratorsForTests()
const spies = {
opens: vi.spyOn(router, 'open'),
// The router reaches every registered adapter by exact generation.
codexEndingReads: vi.spyOn(adapter, 'startupEnding'),
otherEndingReads: vi.spyOn(other, 'startupEnding'),
codexRetirements: vi.spyOn(adapter, 'retireStartup'),
otherRetirements: vi.spyOn(other, 'retireStartup'),
timersScheduled: vi.spyOn(globalThis, 'setTimeout')
}
// Abort handles the host still holds, and the listeners on their signals.
const abortSignals = () =>
[...runtimeState.acquireAborts['inFlight'].values()].flatMap((waits) =>
[...waits].map(({ controller }) => controller.signal)
)
const abortListeners = () =>
abortSignals().reduce((sum, signal) => sum + getEventListeners(signal, 'abort').length, 0)
const held = () => ({
timers: vi.getTimerCount(),
abortListeners: abortListeners(),
abortHandles: abortSignals().length,
trackedStarts: runtimeState.startupAttempts['open'].size,
owners: runtimeState.startupAttempts['owners'].size,
flights: runtimeState.startFlights['flying'].size,
bufferedExits: runtimeState.startFlights['exits'].size,
receipts: retainedReceipts(),
rootExitListeners: created.rootExits.reduce((sum, exit) => sum + exit['listeners'].size, 0),
liveChildren: codex.connections.filter((connection) => !connection.closed).length,
// Host work still tracked or queued after its caller returned.
trackedTasks: host!['tasks']['attaching'].size,
queuedTasks: host!['tasks']['chains'].size
})
const eventually = (assertion: () => unknown) =>
vi.waitFor(assertion, { timeout: 10_000, interval: 0 })
const submission = async (id: string) =>
(await host!.journalSnapshot(SESSION)).submissions.find((row) => row.clientMessageId === id)
const failureRows = async () =>
(await host!.journalSnapshot(SESSION)).items.filter(
(item) => item.body.kind === 'status' && item.body.tone === 'error'
).length
const phase = () => host!.collaboratorsForTests().sessions.get(SESSION)?.child?.phase
const exitChild = (connection = codex.connections.at(-1)!) => {
connection.closed = true
const exit = new CodexAppServerExitError('codex app-server connection ended')
// A duplicate terminal report must change nothing.
connection.handlers.onExit?.(exit, { expected: false })
connection.handlers.onExit?.(exit, { expected: false })
}
// Work the starts did, counted from a ready chat.
const work = async (): Promise<Record<string, number>> => ({
...Object.fromEntries(
Object.entries(spies).map(([field, spy]) => [field, spy.mock.calls.length])
),
spawns: codex.connections.length,
failureRows: await failureRows()
})
answers.push('ok')
expect(await host.attach(CALLER, hostTestAttachParams(null))).toMatchObject({ ok: true })
await eventually(() => expect(phase()).toBe('ready'))
const begun = await work()
const after: Record<Cycle, ReturnType<typeof held>[]> = {
ready: [],
final: [],
retried: [],
expired: [],
stopped: []
}
let rows = 0
// What a Stopped start holds while its child's cleanup is still running.
const stopping: ReturnType<typeof held>[] = []
// What one start holds while its child is still answering.
const starting: ReturnType<typeof held>[] = []
for (let round = 0; round < count; round++) {
for (const cycle of CYCLES) {
// The chat's ready child dies, so the next message starts a new one as the script says.
if (phase() === 'ready') {
exitChild()
await eventually(() => expect(phase()).toBeUndefined())
}
await host.flushStreamedEvents(SESSION)
rows = await failureRows()
const spawned = codex.connections.length
answers.push(...SCRIPTS[cycle])
const send = restTestSend(cycle, store.getRecord(SESSION)!.lease.runtimeFence)
expect(await host.send(CALLER, send), `${cycle} ${round}`).toMatchObject({ ok: true })
if (cycle === 'retried') {
await eventually(() => expect(answers).toEqual(['ok']))
starting.push(held())
exitChild()
}
if (cycle === 'expired') {
await eventually(() => expect(answers).toEqual([]))
await vi.advanceTimersByTimeAsync(CEILING_MS)
}
if (cycle === 'stopped') {
await eventually(() => expect(answers).toEqual([]))
// The user stops the start while its open waits; the child's cleanup is held, then ends.
const connection = codex.connections.at(-1)!
const close = connection.close
const cleanup = Promise.withResolvers<void>()
connection.close = async () => {
await cleanup.promise
return close()
}
const fence = store.getRecord(SESSION)!.lease.runtimeFence
const stop = {
sessionId: SESSION,
clientOperationId: hostTestOperationId(),
expectedRuntimeFence: fence,
payloadFingerprint: computeAgentSessionPayloadFingerprint({
method: 'agentSession.cancel',
sessionId: SESSION,
fields: {}
})
}
const stopped = host.cancel(CALLER, { envelope: stop })
await vi.advanceTimersByTimeAsync(100)
stopping.push(held())
cleanup.resolve()
expect(await stopped).toMatchObject({ ok: true })
}
// The start's owner decides it: the message runs on a ready child, or one row says why not.
const started = cycle === 'ready' || cycle === 'retried'
await eventually(async () => {
await vi.advanceTimersByTimeAsync(100)
expect(runtimeState.startFlights.has(SESSION)).toBe(false)
expect(phase(), `${cycle} ${round}`).toBe(started ? 'ready' : undefined)
expect((await submission(send.envelope.clientOperationId))?.dispatchState).toBe(
started ? 'accepted' : 'rejected'
)
expect(await failureRows()).toBe(rows + (cycle === 'final' || cycle === 'expired' ? 1 : 0))
})
expect(codex.connections).toHaveLength(spawned + (cycle === 'retried' ? 2 : 1))
await host.flushStreamedEvents(SESSION)
await vi.advanceTimersByTimeAsync(2_000)
after[cycle].push(held())
}
}
const done = await work()
await host.close(SESSION, 'evict')
await host.flushAllStreamedEvents()
await router.closeAll()
vi.restoreAllMocks()
const counts = Object.fromEntries(
Object.entries(done).map(([field, count]) => [field, count - begun[field]])
)
return { after, starting, stopping, counts }
}
it('returns every start resource to baseline and does constant work per start, at N and 10N', async () => {
const n = await runStarts(1)
const tenN = await runStarts(10)
for (const cycle of CYCLES) {
const [first] = n.after[cycle]
// Nothing a finished start owned survives it: no owner, abort handle, tracked attempt, flight,
// buffered exit, adapter receipt, root-exit listener or tracked task, and no exited child left
// running.
expect(first, cycle).toMatchObject({
abortHandles: 0,
trackedStarts: 0,
owners: 0,
flights: 0,
bufferedExits: 0,
receipts: 0,
rootExitListeners: 0,
liveChildren: cycle === 'ready' || cycle === 'retried' ? 1 : 0,
trackedTasks: 0,
queuedTasks: 0
})
// Timers and abort listeners held after each start do not grow with the starts before it.
for (const held of tenN.after[cycle]) {
expect(held, cycle).toEqual(first)
}
}
// While a start waits on its child it holds a fixed set, however many starts came before it:
// one deadline timer, the flight's and the owner's abort handles, one tracked attempt, owner,
// receipt and root-exit subscription.
expect(n.starting[0]).toMatchObject({
timers: n.after.ready[0].timers + 1,
abortHandles: 2,
trackedStarts: 1,
owners: 1,
flights: 1,
receipts: 1,
rootExitListeners: 1
})
for (const held of tenN.starting) {
expect(held).toEqual(n.starting[0])
}
// A Stop releases the start's owner at once; while its child's cleanup is held, that cleanup is
// the one task still tracked and queued, however many Stops came before it.
expect(n.stopping[0]).toMatchObject({
owners: 0,
flights: 0,
abortHandles: 0,
receipts: 0,
trackedTasks: 1,
queuedTasks: 1,
liveChildren: 1
})
for (const held of tenN.stopping) {
expect(held).toEqual(n.stopping[0])
}
// Per round of five starts: six spawns and opens (the retried start opens twice) and a failure
// row for the final and the expired start; a Stopped start reads no ending. Router reads and
// retirements reach the registered adapters a fixed number of times per open, whatever the
// starts before it.
expect(n.counts).toMatchObject({
spawns: 6,
opens: 6,
codexEndingReads: 4,
otherEndingReads: 4,
codexRetirements: 6,
otherRetirements: 6,
failureRows: 2
})
for (const [field, count] of Object.entries(n.counts)) {
expect(tenN.counts[field], field).toBe(count * 10)
}
})
/** Starts an ACP agent that prints a sign-in line Orca cannot read, `count` times. */
async function refuseAcpStarts(count: number) {
const connections: ReturnType<typeof createAcpAgentConnection>[] = []
const refusals = new Map<ReturnType<typeof createAcpAgentConnection>, Error>()
const rig = await openAcpAdapterRig({
deps: {
connect: (launch, options) => {
const child = Object.assign(createFakePipedChild(), {
pid: 9_999_999,
stdout: new PassThrough(),
stdin: new PassThrough(),
stderr: new PassThrough(),
kill: () => true
})
// Orca's cleanup ends stdin; the agent then exits.
child.stdin.once('finish', () => child.emit('exit', 0, null))
const connection: ReturnType<typeof createAcpAgentConnection> = createAcpAgentConnection(
launch,
{
...options,
onClose: (error) => {
if (!refusals.has(connection)) {
refusals.set(connection, error)
}
options.onClose?.(error)
},
signIn: { read: readAntigravitySignInLine }
},
() => child
)
connections.push(connection)
child.stdout.write('Open the following link to authenticate the ACP server: not-a-url\n')
return connection
}
}
})
const retained: number[] = []
try {
for (let index = 0; index < count; index++) {
const error = await rig.acquire({ waitForStart: false }).catch((caught: unknown) => caught)
const connection = connections[index]
// The verdict is final and carries this connection's own first refusal, not a later close.
const verdict = findInErrorChain(error, (cause) =>
cause instanceof AgentSessionStartFailure ? cause : undefined
)
expect(verdict?.retry).toBe('never')
expect(verdict?.cause).toBe(refusals.get(connection))
expect(connection.lostBeforeExit).toBe(refusals.get(connection))
expect(connection.exited).toBe(true)
retained.push(retainedReceipts())
}
return { spawns: connections.length, retained }
} finally {
await rig.adapter.closeAll()
await closeProviderTimelineRigs()
}
}
it('ACP: each refused start keeps its own cause and leaves no receipt or child, at N and 10N', async () => {
vi.useRealTimers()
for (const count of [10, 100]) {
const { spawns, retained } = await refuseAcpStarts(count)
expect(spawns).toBe(count)
expect(retained).toEqual(Array(count).fill(0))
}
})
@@ -1,52 +0,0 @@
import { describe, expect, it } from 'vitest'
import { DESKTOP_BROWSER_BRIDGE_METHODS } from '../../../shared/desktop-browser-bridge-protocol'
import { ALL_RPC_METHODS } from '../rpc/methods/rpc-method-registry'
describe('desktop browser bridge allowlist', () => {
const registered = new Map<string, (typeof ALL_RPC_METHODS)[number]>(
ALL_RPC_METHODS.map((method) => [method.name, method])
)
it('names only real one-shot browser methods in the workspace tier', () => {
for (const name of DESKTOP_BROWSER_BRIDGE_METHODS) {
const method = registered.get(name)
expect(method, name).toBeDefined()
expect(name.startsWith('browser.'), name).toBe(true)
expect(method?.permission, name).toBe('workspace')
expect('stream' in (method ?? {}), name).toBe(false)
}
})
it('leaves out every browser method that touches desktop files, secrets or browser identity', () => {
const excluded = [...registered.keys()].filter(
(name) => name.startsWith('browser.') && !DESKTOP_BROWSER_BRIDGE_METHODS.has(name)
)
expect(excluded).toEqual(
expect.arrayContaining([
'browser.upload',
'browser.download',
'browser.pdf',
'browser.exec',
'browser.cookie.get',
'browser.cookie.set',
'browser.cookie.delete',
'browser.network',
'browser.clipboardRead',
'browser.clipboardWrite',
'browser.storage.local.get',
'browser.profileImportFromBrowser',
'browser.profileCreate',
'browser.profileDelete',
'browser.tabSetProfile',
'browser.identity.set',
'browser.setCredentials',
'browser.setHeaders',
'browser.intercept.enable',
'browser.certificate.proceed',
'browser.screencast',
'browser.openUrl',
'browser.clientHost.attach'
])
)
})
})
@@ -174,113 +174,6 @@ describe('mobile subscribe integration', () => {
}
})
it('serializeMainTerminalBuffer returns an empty snapshot for an empty headless buffer', async () => {
const { runtime } = createRuntime()
type HeadlessStateForTest = {
emulator: {
isAlternateScreen: boolean
getSnapshot: (opts: { scrollbackRows?: number }) => {
rehydrateSequences: string
snapshotAnsi: string
cols: number
rows: number
}
}
outputSequence: number
writeChain: Promise<void>
ownership: { settle: () => Promise<void>; owner: undefined }
}
const runtimePrivate = runtime as unknown as {
headlessTerminals: Map<string, HeadlessStateForTest>
}
runtimePrivate.headlessTerminals.set('pty-empty', {
emulator: {
isAlternateScreen: false,
getSnapshot: () => ({ rehydrateSequences: '', snapshotAnsi: '', cols: 90, rows: 30 })
},
outputSequence: 17,
writeChain: Promise.resolve(),
ownership: { settle: async () => {}, owner: undefined }
})
await expect(runtime.serializeMainTerminalBuffer('pty-empty')).resolves.toEqual({
data: '',
cols: 90,
rows: 30,
seq: 17,
source: 'headless',
// Preserve destructive scrollback clear on non-alt-screen restore.
alternateScreen: false
})
await expect(runtime.serializeTerminalBuffer('pty-empty')).resolves.toBeNull()
})
it('handleMobileSubscribe resizes PTY to phone dims', async () => {
const { runtime, ptySizes, resizes, notifications } = createRuntime()
const result = await runtime.handleMobileSubscribe('pty-1', 'client-a', {
cols: 45,
rows: 20
})
expect(result).toBe(true)
expect(ptySizes.get('pty-1')).toEqual({ cols: 45, rows: 20 })
expect(resizes).toEqual([{ ptyId: 'pty-1', cols: 45, rows: 20 }])
expect(notifications).toEqual([{ ptyId: 'pty-1', mode: 'mobile-fit', cols: 45, rows: 20 }])
expect(runtime.isMobileSubscriberActive('pty-1')).toBe(true)
})
it('handleMobileSubscribe skips resize when mode is desktop', async () => {
const { runtime, ptySizes, resizes } = createRuntime()
runtime.setMobileDisplayMode('pty-1', 'desktop')
const result = await runtime.handleMobileSubscribe('pty-1', 'client-a', {
cols: 45,
rows: 20
})
expect(result).toBe(false)
expect(ptySizes.get('pty-1')).toEqual({ cols: 150, rows: 40 })
expect(resizes).toEqual([])
})
it('handleMobileSubscribe skips resize when no viewport provided', async () => {
const { runtime, ptySizes, resizes } = createRuntime()
const result = await runtime.handleMobileSubscribe('pty-1', 'client-a')
expect(result).toBe(false)
expect(ptySizes.get('pty-1')).toEqual({ cols: 150, rows: 40 })
expect(resizes).toEqual([])
})
it('handleMobileUnsubscribe restores PTY after debounce in auto mode', async () => {
const { runtime, ptySizes } = createRuntime()
await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 })
expect(ptySizes.get('pty-1')).toEqual({ cols: 45, rows: 20 })
runtime.handleMobileUnsubscribe('pty-1', 'client-a')
expect(ptySizes.get('pty-1')).toEqual({ cols: 45, rows: 20 })
await vi.advanceTimersByTimeAsync(LEGACY_RESTORE_MS)
expect(ptySizes.get('pty-1')).toEqual({ cols: 150, rows: 40 })
})
it('preserves previousDims across re-subscribes to same terminal', async () => {
const { runtime, ptySizes } = createRuntime()
await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 })
expect(ptySizes.get('pty-1')).toEqual({ cols: 45, rows: 20 })
// Re-subscribe sees phone-fitted PTY dims but must retain the original desktop baseline.
await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 })
runtime.handleMobileUnsubscribe('pty-1', 'client-a')
await vi.advanceTimersByTimeAsync(LEGACY_RESTORE_MS)
expect(ptySizes.get('pty-1')).toEqual({ cols: 150, rows: 40 })
})
it('preserves held-override baseline across resubscribe-after-indefinite-hold', async () => {
// Indefinite-hold re-subscribe must inherit the retained desktop baseline for later Restore.
settingsState.mobileAutoRestoreFitMs = null
@@ -300,184 +193,7 @@ describe('mobile subscribe integration', () => {
expect(ptySizes.get('pty-1')).toEqual({ cols: 150, rows: 40 })
})
it('clamps viewport to valid range', async () => {
const { runtime, ptySizes } = createRuntime()
await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 10, rows: 3 })
expect(ptySizes.get('pty-1')).toEqual({ cols: 20, rows: 8 })
})
describe('display mode', () => {
it('defaults to auto', () => {
const { runtime } = createRuntime()
expect(runtime.getMobileDisplayMode('pty-1')).toBe('auto')
})
it('set/get round-trip', () => {
const { runtime } = createRuntime()
runtime.setMobileDisplayMode('pty-1', 'desktop')
expect(runtime.getMobileDisplayMode('pty-1')).toBe('desktop')
runtime.setMobileDisplayMode('pty-1', 'auto')
expect(runtime.getMobileDisplayMode('pty-1')).toBe('auto')
})
})
describe('applyMobileDisplayMode', () => {
it('desktop mode restores PTY when currently phone-fitted', async () => {
const { runtime, ptySizes } = createRuntime()
await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 })
expect(ptySizes.get('pty-1')).toEqual({ cols: 45, rows: 20 })
const resizeEvents: unknown[] = []
runtime.subscribeToTerminalResize('pty-1', (event) => resizeEvents.push(event))
runtime.setMobileDisplayMode('pty-1', 'desktop')
await runtime.applyMobileDisplayMode('pty-1')
expect(ptySizes.get('pty-1')).toEqual({ cols: 150, rows: 40 })
expect(resizeEvents).toHaveLength(1)
expect(resizeEvents[0]).toMatchObject({
cols: 150,
rows: 40,
displayMode: 'desktop',
reason: 'apply-layout'
})
})
it('auto mode re-fits PTY when subscriber exists and not phone-fitted', async () => {
const { runtime, ptySizes } = createRuntime()
await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 })
runtime.setMobileDisplayMode('pty-1', 'desktop')
await runtime.applyMobileDisplayMode('pty-1')
expect(ptySizes.get('pty-1')).toEqual({ cols: 150, rows: 40 })
const resizeEvents: unknown[] = []
runtime.subscribeToTerminalResize('pty-1', (event) => resizeEvents.push(event))
runtime.setMobileDisplayMode('pty-1', 'auto')
await runtime.applyMobileDisplayMode('pty-1')
expect(ptySizes.get('pty-1')).toEqual({ cols: 45, rows: 20 })
expect(resizeEvents).toHaveLength(1)
expect(resizeEvents[0]).toMatchObject({
displayMode: 'phone',
reason: 'apply-layout'
})
})
})
describe('cleanup paths', () => {
it('onClientDisconnected restores all PTYs immediately (no debounce)', async () => {
const { runtime, ptySizes } = createRuntime()
await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 })
await runtime.handleMobileSubscribe('pty-2', 'client-a', { cols: 45, rows: 20 })
runtime.onClientDisconnected('client-a')
// Disconnect queues work; flush microtasks and zero-delay timers.
await vi.advanceTimersByTimeAsync(0)
expect(ptySizes.get('pty-1')).toEqual({ cols: 150, rows: 40 })
expect(ptySizes.get('pty-2')).toEqual({ cols: 120, rows: 35 })
expect(runtime.isMobileSubscriberActive('pty-1')).toBe(false)
expect(runtime.isMobileSubscriberActive('pty-2')).toBe(false)
})
it('onClientDisconnected cancels pending restore timers', async () => {
const { runtime, ptySizes } = createRuntime()
await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 })
runtime.handleMobileUnsubscribe('pty-1', 'client-a')
runtime.onClientDisconnected('client-a')
await vi.advanceTimersByTimeAsync(LEGACY_RESTORE_MS)
expect(ptySizes.get('pty-1')).toEqual({ cols: 150, rows: 40 })
})
it('onPtyExit cleans up mobileSubscribers and pending timers', async () => {
const { runtime } = createRuntime()
await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 })
runtime.handleMobileUnsubscribe('pty-1', 'client-a')
runtime.onPtyExit('pty-1', 0)
expect(runtime.isMobileSubscriberActive('pty-1')).toBe(false)
expect(runtime.getMobileDisplayMode('pty-1')).toBe('auto')
await vi.advanceTimersByTimeAsync(LEGACY_RESTORE_MS)
})
it('onPtyExit does not cancel timers for other PTYs', async () => {
const { runtime, ptySizes } = createRuntime()
await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 })
runtime.handleMobileUnsubscribe('pty-1', 'client-a')
runtime.onPtyExit('pty-2', 0)
await vi.advanceTimersByTimeAsync(LEGACY_RESTORE_MS)
expect(ptySizes.get('pty-1')).toEqual({ cols: 150, rows: 40 })
})
})
describe('resize listener system', () => {
it('subscribe/unsubscribe lifecycle', async () => {
const { runtime } = createRuntime()
const events: unknown[] = []
const unsubscribe = runtime.subscribeToTerminalResize('pty-1', (e) => events.push(e))
await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 })
runtime.setMobileDisplayMode('pty-1', 'desktop')
await runtime.applyMobileDisplayMode('pty-1')
expect(events.length).toBeGreaterThan(0)
const countBefore = events.length
unsubscribe()
runtime.setMobileDisplayMode('pty-1', 'auto')
await runtime.applyMobileDisplayMode('pty-1')
expect(events.length).toBe(countBefore)
})
})
describe('onExternalPtyResize', () => {
it('updates previousCols when desktop renderer resizes PTY after desktop restore', async () => {
const { runtime, ptySizes } = createRuntime()
await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 })
runtime.setMobileDisplayMode('pty-1', 'desktop')
await runtime.applyMobileDisplayMode('pty-1')
expect(ptySizes.get('pty-1')).toEqual({ cols: 150, rows: 40 })
// Wait out the 500ms phone-to-desktop resize cascade suppression before reporting renderer geometry.
await vi.advanceTimersByTimeAsync(500)
runtime.onExternalPtyResize('pty-1', 105, 40)
runtime.setMobileDisplayMode('pty-1', 'auto')
await runtime.applyMobileDisplayMode('pty-1')
expect(ptySizes.get('pty-1')).toEqual({ cols: 45, rows: 20 })
runtime.setMobileDisplayMode('pty-1', 'desktop')
await runtime.applyMobileDisplayMode('pty-1')
expect(ptySizes.get('pty-1')).toEqual({ cols: 105, rows: 40 })
})
it('uses lastRendererSize for previousCols on first subscribe', async () => {
const { runtime, ptySizes } = createRuntime()
ptySizes.set('pty-1', { cols: 214, rows: 72 })
runtime.onExternalPtyResize('pty-1', 105, 40)
await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 })
expect(ptySizes.get('pty-1')).toEqual({ cols: 45, rows: 20 })
runtime.setMobileDisplayMode('pty-1', 'desktop')
await runtime.applyMobileDisplayMode('pty-1')
expect(ptySizes.get('pty-1')).toEqual({ cols: 105, rows: 40 })
})
it('ignores reports while a mobile-fit override is in place', async () => {
// Phone-fit echoes must not poison the desktop restore baseline; see docs/mobile-fit-hold.md.
const { runtime, ptySizes } = createRuntime()
@@ -490,28 +206,6 @@ describe('mobile subscribe integration', () => {
expect(ptySizes.get('pty-1')).toEqual({ cols: 150, rows: 40 })
})
it('still updates baseline once the override is cleared (real desktop resize)', async () => {
// Once the override clears, legitimate renderer reports must update the baseline.
const { runtime, ptySizes } = createRuntime()
await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 })
runtime.setMobileDisplayMode('pty-1', 'desktop')
await runtime.applyMobileDisplayMode('pty-1')
expect(ptySizes.get('pty-1')).toEqual({ cols: 150, rows: 40 })
await vi.advanceTimersByTimeAsync(500)
runtime.onExternalPtyResize('pty-1', 130, 35)
runtime.setMobileDisplayMode('pty-1', 'auto')
await runtime.applyMobileDisplayMode('pty-1')
expect(ptySizes.get('pty-1')).toEqual({ cols: 45, rows: 20 })
runtime.setMobileDisplayMode('pty-1', 'desktop')
await runtime.applyMobileDisplayMode('pty-1')
expect(ptySizes.get('pty-1')).toEqual({ cols: 130, rows: 35 })
})
it('updates baseline mid-fit when desktop reports dims that differ from the override', async () => {
// A newly visible desktop tab can report real geometry during phone fit; retain it for restore.
const { runtime, ptySizes } = createRuntime()
@@ -528,45 +222,6 @@ describe('mobile subscribe integration', () => {
})
describe('recordRendererGeometry (pty:reportGeometry IPC)', () => {
it('refreshes subscriber baseline while a mobile-fit override is active', async () => {
// Never-active terminals need measurement-only geometry to replace spawn-default restore sizes.
const { runtime, ptySizes } = createRuntime()
ptySizes.set('pty-1', { cols: 80, rows: 24 })
runtime.onExternalPtyResize('pty-1', 80, 24)
await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 })
runtime.recordRendererGeometry('pty-1', 214, 72)
runtime.setMobileDisplayMode('pty-1', 'desktop')
await runtime.applyMobileDisplayMode('pty-1')
expect(ptySizes.get('pty-1')).toEqual({ cols: 214, rows: 72 })
})
it('restores reported geometry captured before the first mobile subscribe', async () => {
const { runtime, ptySizes, resizes } = createRuntime()
runtime.recordRendererGeometry('pty-1', 180, 50)
expect(ptySizes.get('pty-1')).toEqual({ cols: 150, rows: 40 })
expect(resizes).toEqual([])
await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 })
expect(await runtime.reclaimTerminalForDesktop('pty-1')).toBe(true)
expect(ptySizes.get('pty-1')).toEqual({ cols: 180, rows: 50 })
})
it('ignores non-positive dims when capturing restore geometry', async () => {
const { runtime, ptySizes, resizes } = createRuntime()
runtime.recordRendererGeometry('pty-1', 180, 50)
runtime.recordRendererGeometry('pty-1', 0, 0)
runtime.recordRendererGeometry('pty-1', -5, 10)
runtime.recordRendererGeometry('pty-1', 10, -5)
expect(ptySizes.get('pty-1')).toEqual({ cols: 150, rows: 40 })
expect(resizes).toEqual([])
await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 })
expect(await runtime.reclaimTerminalForDesktop('pty-1')).toBe(true)
expect(ptySizes.get('pty-1')).toEqual({ cols: 180, rows: 50 })
})
it('bypasses the cascade-suppress window (it is measurement-only)', async () => {
const { runtime, ptySizes, resizes } = createRuntime()
await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 })
@@ -589,78 +244,8 @@ describe('mobile subscribe integration', () => {
})
})
describe('backward compatibility', () => {
it('old resizeForClient still works alongside new system', async () => {
const { runtime, ptySizes } = createRuntime()
const fitResult = await runtime.resizeForClient('pty-1', 'mobile-fit', 'client-old', 45, 20)
expect(ptySizes.get('pty-1')).toEqual({ cols: 45, rows: 20 })
expect(fitResult.mode).toBe('mobile-fit')
const restoreResult = await runtime.resizeForClient('pty-1', 'restore', 'client-old')
expect(ptySizes.get('pty-1')).toEqual({ cols: 150, rows: 40 })
expect(restoreResult.mode).toBe('desktop-fit')
})
})
// See docs/mobile-fit-hold.md.
describe('mobileAutoRestoreFitMs (fit hold)', () => {
it('null (indefinite) keeps PTY at phone dims after last unsubscribe', async () => {
settingsState.mobileAutoRestoreFitMs = null
const { runtime, ptySizes } = createRuntime()
await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 })
runtime.handleMobileUnsubscribe('pty-1', 'client-a')
await vi.advanceTimersByTimeAsync(60_000)
expect(ptySizes.get('pty-1')).toEqual({ cols: 45, rows: 20 })
})
it('finite ms restores after the configured delay', async () => {
settingsState.mobileAutoRestoreFitMs = 60_000
const { runtime, ptySizes } = createRuntime()
await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 })
runtime.handleMobileUnsubscribe('pty-1', 'client-a')
await vi.advanceTimersByTimeAsync(30_000)
expect(ptySizes.get('pty-1')).toEqual({ cols: 45, rows: 20 })
await vi.advanceTimersByTimeAsync(30_000)
expect(ptySizes.get('pty-1')).toEqual({ cols: 150, rows: 40 })
})
it('re-subscribe before timer fires cancels pending restore', async () => {
settingsState.mobileAutoRestoreFitMs = 60_000
const { runtime, ptySizes } = createRuntime()
await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 })
runtime.handleMobileUnsubscribe('pty-1', 'client-a')
await vi.advanceTimersByTimeAsync(30_000)
await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 })
await vi.advanceTimersByTimeAsync(120_000)
expect(ptySizes.get('pty-1')).toEqual({ cols: 45, rows: 20 })
})
it('reclaimTerminalForDesktop with active subscriber keeps desktop mode until the phone acts', async () => {
// #28487: reconnect/resume re-subscribe stays passive; only typing takes the floor.
const { runtime, ptySizes } = createRuntime()
await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 })
expect(ptySizes.get('pty-1')).toEqual({ cols: 45, rows: 20 })
const ok = await runtime.reclaimTerminalForDesktop('pty-1')
expect(ok).toBe(true)
expect(ptySizes.get('pty-1')).toEqual({ cols: 150, rows: 40 })
expect(runtime.getMobileDisplayMode('pty-1')).toBe('desktop')
await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 })
expect(ptySizes.get('pty-1')).toEqual({ cols: 150, rows: 40 })
expect(runtime.getDriver('pty-1').kind).not.toBe('mobile')
await runtime.mobileTookFloor('pty-1', 'client-a')
expect(ptySizes.get('pty-1')).toEqual({ cols: 45, rows: 20 })
expect(runtime.getMobileDisplayMode('pty-1')).toBe('auto')
})
it('reclaim with WS disconnect between cycles — held branch restores correctly each time', async () => {
// Repeated held-override reclaim must restore desktop geometry, never the current phone fit.
settingsState.mobileAutoRestoreFitMs = null
@@ -690,176 +275,5 @@ describe('mobile subscribe integration', () => {
expect(runtime.getTerminalFitOverride('pty-1'), `iter ${i}: override cleared`).toBeNull()
}
})
it('reclaim → re-subscribe → reclaim cycle works repeatedly', async () => {
const { runtime, ptySizes } = createRuntime()
for (let i = 0; i < 3; i++) {
await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 })
await runtime.mobileTookFloor('pty-1', 'client-a')
expect(ptySizes.get('pty-1'), `iter ${i}: phone-fit`).toEqual({ cols: 45, rows: 20 })
const ok = await runtime.reclaimTerminalForDesktop('pty-1')
expect(ok, `iter ${i}: reclaim ok`).toBe(true)
expect(ptySizes.get('pty-1'), `iter ${i}: reclaimed to desktop`).toEqual({
cols: 150,
rows: 40
})
expect(runtime.getTerminalFitOverride('pty-1'), `iter ${i}: override cleared`).toBeNull()
}
})
it('null (indefinite) keeps PTY at phone dims when WS connection closes (onClientDisconnected)', async () => {
// WS close bypasses handleMobileUnsubscribe but must still honor indefinite hold.
settingsState.mobileAutoRestoreFitMs = null
const { runtime, ptySizes } = createRuntime()
await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 })
runtime.onClientDisconnected('client-a')
await vi.advanceTimersByTimeAsync(0)
await vi.advanceTimersByTimeAsync(60_000)
expect(ptySizes.get('pty-1')).toEqual({ cols: 45, rows: 20 })
expect(runtime.getTerminalFitOverride('pty-1')).not.toBeNull()
})
it('reclaimTerminalForDesktop returns held PTY to desktop dims (no subscriber)', async () => {
settingsState.mobileAutoRestoreFitMs = null
const { runtime, ptySizes } = createRuntime()
await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 })
runtime.handleMobileUnsubscribe('pty-1', 'client-a')
await runtime.reclaimTerminalForDesktop('pty-1')
expect(ptySizes.get('pty-1')).toEqual({ cols: 150, rows: 40 })
await vi.advanceTimersByTimeAsync(250)
expect(runtime.getDriver('pty-1')).toEqual({ kind: 'desktop' })
})
it('reclaim cancels the soft-leave timer in the remote-layout branch', async () => {
const { runtime } = createRuntime()
await runtime.updateRemoteDesktopViewer('pty-1', 'sub-a', 'viewer-a', 120, 32)
await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 })
runtime.handleMobileUnsubscribe('pty-1', 'client-a')
expect(await runtime.reclaimTerminalForDesktop('pty-1')).toBe(true)
await vi.advanceTimersByTimeAsync(250)
expect(runtime.getDriver('pty-1')).toEqual({ kind: 'desktop' })
})
it('reclaim cancels pending restore timers on the active-subscriber branch', async () => {
const { runtime } = createRuntime()
await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 })
runtime.handleMobileUnsubscribe('pty-1', 'client-a')
await runtime.handleMobileSubscribe('pty-1', 'client-b', { cols: 40, rows: 18 })
const pendingRestore = runtime.restoreTimers
pendingRestore.set('pty-1', { timer: setTimeout(() => {}, 60_000), clientId: 'client-b' })
const pendingSoft = runtime.softLeavers
expect(pendingSoft.has('pty-1')).toBe(true)
await runtime.reclaimTerminalForDesktop('pty-1')
expect(pendingRestore.has('pty-1')).toBe(false)
expect(pendingSoft.has('pty-1')).toBe(false)
})
it('reclaim cancels pending restore timers on the orphan-driver branch', async () => {
const { runtime } = createRuntime()
await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 })
runtime.handleMobileUnsubscribe('pty-1', 'client-a')
runtime.fitOverrides.delete('pty-1')
const pendingRestore = runtime.restoreTimers
const pendingSoft = runtime.softLeavers
await runtime.reclaimTerminalForDesktop('pty-1')
expect(pendingRestore.has('pty-1')).toBe(false)
expect(pendingSoft.has('pty-1')).toBe(false)
})
it('reclaim cancels pending restore timers when no driver lock remains', async () => {
const { runtime } = createRuntime()
await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 })
runtime.handleMobileUnsubscribe('pty-1', 'client-a')
runtime.fitOverrides.delete('pty-1')
runtime.drivers.set('pty-1', { kind: 'idle' })
const pendingRestore = runtime.restoreTimers
const pendingSoft = runtime.softLeavers
expect(await runtime.reclaimTerminalForDesktop('pty-1')).toBe(false)
expect(pendingRestore.has('pty-1')).toBe(false)
expect(pendingSoft.has('pty-1')).toBe(false)
})
it('reclaim revokes soft-leave grace admission for mobile input', async () => {
const { runtime } = createRuntime()
await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 })
runtime.handleMobileUnsubscribe('pty-1', 'client-a')
const claim = runtime.beginMobileInputFloor('pty-1', 'client-a')
expect(claim).not.toBeNull()
claim?.rollback()
await runtime.reclaimTerminalForDesktop('pty-1')
expect(runtime.getDriver('pty-1')).toEqual({ kind: 'desktop' })
expect(runtime.beginMobileInputFloor('pty-1', 'client-a')).toBeNull()
})
it('reclaimTerminalForDesktop prefers fresh desktop geometry for a held PTY', async () => {
settingsState.mobileAutoRestoreFitMs = null
const { runtime, ptySizes } = createRuntime()
await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 })
runtime.handleMobileUnsubscribe('pty-1', 'client-a')
await vi.advanceTimersByTimeAsync(60_000)
expect(ptySizes.get('pty-1')).toEqual({ cols: 45, rows: 20 })
runtime.recordRendererGeometry('pty-1', 180, 50)
const ok = await runtime.reclaimTerminalForDesktop('pty-1')
expect(ok).toBe(true)
expect(ptySizes.get('pty-1')).toEqual({ cols: 180, rows: 50 })
})
it('setMobileAutoRestoreFitMs(null) clears all pending timers', async () => {
settingsState.mobileAutoRestoreFitMs = 60_000
const { runtime, ptySizes } = createRuntime()
await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 })
runtime.handleMobileUnsubscribe('pty-1', 'client-a')
runtime.setMobileAutoRestoreFitMs(null)
await vi.advanceTimersByTimeAsync(120_000)
expect(ptySizes.get('pty-1')).toEqual({ cols: 45, rows: 20 })
})
it('setMobileAutoRestoreFitMs to a finite value does not retroactively schedule', async () => {
settingsState.mobileAutoRestoreFitMs = null
const { runtime, ptySizes } = createRuntime()
await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 })
runtime.handleMobileUnsubscribe('pty-1', 'client-a')
// Finite preferences apply on the next unsubscribe, never retroactively to an existing hold.
runtime.setMobileAutoRestoreFitMs(60_000)
await vi.advanceTimersByTimeAsync(120_000)
expect(ptySizes.get('pty-1')).toEqual({ cols: 45, rows: 20 })
})
it('setMobileAutoRestoreFitMs clamps below MIN to MIN', () => {
const { runtime } = createRuntime()
const result = runtime.setMobileAutoRestoreFitMs(100)
expect(result).toBe(5_000)
})
it('setMobileAutoRestoreFitMs clamps above MAX to MAX', () => {
const { runtime } = createRuntime()
const result = runtime.setMobileAutoRestoreFitMs(99 * 60 * 60 * 1000)
expect(result).toBe(60 * 60 * 1000)
})
it('reclaimTerminalForDesktop on already-restored PTY returns false', async () => {
const { runtime } = createRuntime()
await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 })
runtime.handleMobileUnsubscribe('pty-1', 'client-a')
await vi.advanceTimersByTimeAsync(LEGACY_RESTORE_MS)
expect(await runtime.reclaimTerminalForDesktop('pty-1')).toBe(false)
})
})
})
@@ -1,4 +1,3 @@
import type { RuntimeAutomationController } from './runtime-automation-controller'
import { describe, expect, it, vi } from 'vitest'
import { OrcaRuntimeService } from './orca-runtime'
import type { Automation } from '../../shared/automations-types'
@@ -73,38 +72,6 @@ export function registerOrcaRuntimeAutomationsTests(): void {
} satisfies Automation
describe('OrcaRuntimeService automation methods', () => {
it('creates repo-scoped automations through the shared store', async () => {
const store = makeStore()
const runtime = new OrcaRuntimeService(store as never)
const automation = await runtime.createAutomation({
name: 'Daily review',
prompt: 'Review changes',
precheck: { command: 'test -f ready', timeoutSeconds: 30 },
agentId: 'codex',
repo: 'repo-1',
workspaceMode: 'new_per_run',
setupDecision: 'skip',
rrule: 'FREQ=DAILY;BYHOUR=9;BYMINUTE=0',
dtstart: 1
})
expect(store.createAutomation).toHaveBeenCalledWith(
expect.objectContaining({
name: 'Daily review',
prompt: 'Review changes',
precheck: { command: 'test -f ready', timeoutSeconds: 30 },
agentId: 'codex',
projectId: 'repo-1',
workspaceMode: 'new_per_run',
workspaceId: null,
setupDecision: 'skip'
}),
{ destination: undefined, resolvedRepo: repo }
)
expect(automation.id).toBe('auto-1')
})
it('rejects a run context that names a different repo path', async () => {
const store = makeStore()
const runtime = new OrcaRuntimeService(store as never)
@@ -132,38 +99,6 @@ export function registerOrcaRuntimeAutomationsTests(): void {
expect(store.createAutomation).not.toHaveBeenCalled()
})
it('rejects a mismatched run context when the workspace is the machine selector', async () => {
const store = makeStore()
const runtime = new OrcaRuntimeService(store as never)
vi.spyOn(runtime, 'showManagedWorktree').mockResolvedValue({
id: 'repo-1::/tmp/orca',
repoId: 'repo-1',
path: '/tmp/orca'
} as never)
await expect(
runtime.createAutomation({
name: 'Workspace review',
prompt: 'Review changes',
agentId: 'codex',
workspace: 'id:repo-1::/tmp/orca',
runContext: {
kind: 'workspace-run',
projectId: 'project-1',
hostId: 'local',
projectHostSetupId: 'setup-1',
repoId: 'repo-1',
path: '/other/repo'
},
workspaceMode: 'existing',
setupDecision: 'skip',
rrule: 'FREQ=DAILY;BYHOUR=9;BYMINUTE=0',
dtstart: 1
})
).rejects.toThrow('Automation project does not match its run context.')
expect(store.createAutomation).not.toHaveBeenCalled()
})
it('rejects a run-context-only update that names a different repo path', async () => {
const store = makeStore([existingAutomation])
const runtime = new OrcaRuntimeService(store as never)
@@ -182,169 +117,5 @@ export function registerOrcaRuntimeAutomationsTests(): void {
).rejects.toThrow('Automation project does not match its run context.')
expect(store.updateAutomation).not.toHaveBeenCalled()
})
it('updates and deletes existing automations through the shared store', async () => {
const store = makeStore([existingAutomation])
const runtime = new OrcaRuntimeService(store as never)
const updated = await runtime.updateAutomation('auto-1', { enabled: false })
const removed = await runtime.deleteAutomation('auto-1')
expect(store.updateAutomation).toHaveBeenCalledWith(
'auto-1',
{ enabled: false },
{ resolvedRepo: undefined }
)
expect(updated).toMatchObject({
prompt: existingAutomation.prompt,
baseBranch: existingAutomation.baseBranch,
workspaceMode: existingAutomation.workspaceMode,
enabled: false
})
expect(store.deleteAutomation).toHaveBeenCalledWith('auto-1', undefined)
expect(removed).toEqual({ removed: true, id: 'auto-1' })
})
it('preserves explicit nullable fields in sparse automation updates', async () => {
const store = makeStore([existingAutomation])
const runtime = new OrcaRuntimeService(store as never)
await runtime.updateAutomation('auto-1', { baseBranch: null })
expect(store.updateAutomation).toHaveBeenCalledWith(
'auto-1',
{ baseBranch: null },
{ resolvedRepo: undefined }
)
})
it('passes setup decision updates through the shared store', async () => {
const store = makeStore([existingAutomation])
const runtime = new OrcaRuntimeService(store as never)
await runtime.updateAutomation('auto-1', { setupDecision: 'run' })
expect(store.updateAutomation).toHaveBeenCalledWith(
'auto-1',
{ setupDecision: 'run' },
{ resolvedRepo: undefined }
)
})
it('passes session reuse updates for existing-workspace automations', async () => {
const existing = {
...existingAutomation,
workspaceMode: 'existing',
workspaceId: 'repo-1::/tmp/orca',
baseBranch: null
} satisfies Automation
const store = makeStore([existing])
const runtime = new OrcaRuntimeService(store as never)
await runtime.updateAutomation('auto-1', { reuseSession: true })
expect(store.updateAutomation).toHaveBeenCalledWith(
'auto-1',
{ reuseSession: true },
{ resolvedRepo: undefined }
)
})
it('clears session reuse when retargeting to new-per-run', async () => {
const existing = {
...existingAutomation,
workspaceMode: 'existing',
workspaceId: 'repo-1::/tmp/orca',
baseBranch: null,
reuseSession: true
} satisfies Automation
const store = makeStore([existing])
const runtime = new OrcaRuntimeService(store as never)
await runtime.updateAutomation('auto-1', {
repo: 'repo-1',
workspaceMode: 'new_per_run'
})
expect(store.updateAutomation).toHaveBeenCalledWith(
'auto-1',
expect.objectContaining({
projectId: 'repo-1',
workspaceMode: 'new_per_run',
workspaceId: null,
reuseSession: false
}),
{ resolvedRepo: repo }
)
})
it('rejects session reuse for new-per-run automations', async () => {
const store = makeStore()
const runtime = new OrcaRuntimeService(store as never)
await expect(
runtime.createAutomation({
name: 'Fresh',
prompt: 'Run checks',
agentId: 'codex',
repo: 'repo-1',
workspaceMode: 'new_per_run',
reuseSession: true,
rrule: 'FREQ=DAILY;BYHOUR=9;BYMINUTE=0',
dtstart: 1
})
).rejects.toThrow('Session reuse requires an existing workspace target.')
expect(store.createAutomation).not.toHaveBeenCalled()
})
it('rejects repo-only updates for existing-workspace automations', async () => {
const existing = {
...existingAutomation,
workspaceMode: 'existing',
workspaceId: 'repo-1::/tmp/orca-worktree',
baseBranch: null
} satisfies Automation
const store = makeStore([existing])
const runtime = new OrcaRuntimeService(store as never)
await expect(runtime.updateAutomation('auto-1', { repo: 'repo-2' })).rejects.toThrow(
'Repo updates for existing-workspace automation require workspaceMode new_per_run.'
)
expect(store.updateAutomation).not.toHaveBeenCalled()
})
})
it('keeps generic automation reads on the current owner before invoking the reader', () => {
const runtime = new OrcaRuntimeService(null)
const owner = runtime['automation']
const result = { rows: ['run-1'] }
const events: string[] = []
const read = vi.fn(() => {
events.push('read')
return result
})
const priority = vi.spyOn(owner, 'withExternalProbePriority').mockImplementation(function <T>(
this: RuntimeAutomationController,
operation: () => T
): T {
expect(this).toBe(owner)
expect(arguments.length).toBe(1)
events.push('priority')
return operation()
})
Object.defineProperty(runtime, 'automation', {
get() {
events.push('owner')
return owner
}
})
try {
expect(runtime['readAutomationRuns'](undefined, undefined, read)).toBe(result)
expect(events).toEqual(['owner', 'priority', 'read'])
expect(priority).toHaveBeenCalledOnce()
expect(read).toHaveBeenCalledOnce()
} finally {
priority.mockRestore()
}
})
}
@@ -1,475 +1,12 @@
import { describe, expect, it, vi } from 'vitest'
import './runtime-test-module-registration.spec'
import { OrcaRuntimeService } from '../orca-runtime'
import { inspectPtyProviderProcess } from '../../providers/pty-process-inspection'
import { makePaneKey } from '../../../shared/workspace-layout/stable-pane-id'
import type { IPtyProvider } from '../../providers/pty-provider-contract'
import {
TEST_WORKTREE_ID,
createExplicitAgentStatusHarness,
deferred,
store
} from './orca-runtime-test-fixtures.spec'
import '../orca-runtime'
import '../../providers/pty-process-inspection'
import '../../../shared/workspace-layout/stable-pane-id'
import { createExplicitAgentStatusHarness, deferred } from './orca-runtime-test-fixtures.spec'
describe('OrcaRuntimeService', () => {
it('prefers newer explicit working state over older explicit permission state', async () => {
const leafId = '11111111-1111-4111-8111-111111111111'
const paneKey = makePaneKey('tab-1', leafId)
const now = Date.now()
const runtime = new OrcaRuntimeService(store, undefined, {
getAgentStatusSnapshot: () => [
{
paneKey,
state: 'waiting',
prompt: '',
agentType: 'codex',
connectionId: null,
receivedAt: now - 1000,
stateStartedAt: now - 1000,
tabId: 'tab-1',
worktreeId: TEST_WORKTREE_ID
},
{
paneKey,
state: 'working',
prompt: '',
agentType: 'codex',
connectionId: null,
receivedAt: now,
stateStartedAt: now,
tabId: 'tab-1',
worktreeId: TEST_WORKTREE_ID
}
]
})
runtime.setPtyController({
spawn: vi.fn().mockResolvedValue({ id: 'pty-1' }),
write: () => true,
kill: () => true,
getForegroundProcess: async () => null
})
runtime.attachWindow(1)
runtime.syncWindowGraph(1, {
tabs: [
{
tabId: 'tab-1',
worktreeId: TEST_WORKTREE_ID,
title: 'repo terminal',
activeLeafId: leafId,
layout: null
}
],
leaves: [
{
tabId: 'tab-1',
worktreeId: TEST_WORKTREE_ID,
leafId,
paneRuntimeId: 1,
ptyId: 'pty-1'
}
]
})
const [terminal] = (await runtime.listTerminals()).terminals
await expect(runtime.getTerminalAgentStatus(terminal.handle)).resolves.toEqual({
handle: terminal.handle,
isRunningAgent: true,
status: 'working'
})
})
it('prefers fresh explicit working state over a stale permission title', async () => {
const leafId = '11111111-1111-4111-8111-111111111111'
const paneKey = makePaneKey('tab-1', leafId)
const runtime = new OrcaRuntimeService(store, undefined, {
getAgentStatusSnapshot: () => [
{
paneKey,
state: 'working',
prompt: '',
agentType: 'codex',
connectionId: null,
receivedAt: Date.now(),
stateStartedAt: Date.now(),
tabId: 'tab-1',
worktreeId: TEST_WORKTREE_ID
}
]
})
runtime.setPtyController({
spawn: vi.fn().mockResolvedValue({ id: 'pty-1' }),
write: () => true,
kill: () => true,
getForegroundProcess: async () => null
})
runtime.attachWindow(1)
runtime.syncWindowGraph(1, {
tabs: [
{
tabId: 'tab-1',
worktreeId: TEST_WORKTREE_ID,
title: 'Codex - action required',
activeLeafId: leafId,
layout: null
}
],
leaves: [
{
tabId: 'tab-1',
worktreeId: TEST_WORKTREE_ID,
leafId,
paneRuntimeId: 1,
ptyId: 'pty-1'
}
]
})
const [terminal] = (await runtime.listTerminals()).terminals
await expect(runtime.getTerminalAgentStatus(terminal.handle)).resolves.toEqual({
handle: terminal.handle,
isRunningAgent: true,
status: 'working'
})
})
it('reports permission from a live title over fresh explicit working state', async () => {
const leafId = '11111111-1111-4111-8111-111111111111'
const paneKey = makePaneKey('tab-1', leafId)
const runtime = new OrcaRuntimeService(store, undefined, {
getAgentStatusSnapshot: () => [
{
paneKey,
state: 'working',
prompt: '',
agentType: 'codex',
connectionId: null,
receivedAt: Date.now(),
stateStartedAt: Date.now(),
tabId: 'tab-1',
worktreeId: TEST_WORKTREE_ID
}
]
})
runtime.setPtyController({
spawn: vi.fn().mockResolvedValue({ id: 'pty-1' }),
write: () => true,
kill: () => true,
getForegroundProcess: async () => null
})
runtime.attachWindow(1)
runtime.syncWindowGraph(1, {
tabs: [
{
tabId: 'tab-1',
worktreeId: TEST_WORKTREE_ID,
title: 'repo terminal',
activeLeafId: leafId,
layout: null
}
],
leaves: [
{
tabId: 'tab-1',
worktreeId: TEST_WORKTREE_ID,
leafId,
paneRuntimeId: 1,
ptyId: 'pty-1',
paneTitle: 'Codex - action required'
}
]
})
const [terminal] = (await runtime.listTerminals()).terminals
await expect(runtime.getTerminalAgentStatus(terminal.handle)).resolves.toEqual({
handle: terminal.handle,
isRunningAgent: true,
status: 'permission'
})
})
it('does not let fresh explicit hook state authorize a current shell terminal', async () => {
const leafId = '11111111-1111-4111-8111-111111111111'
const paneKey = makePaneKey('tab-1', leafId)
const runtime = new OrcaRuntimeService(store, undefined, {
getAgentStatusSnapshot: () => [
{
paneKey,
state: 'working',
prompt: '',
agentType: 'codex',
connectionId: null,
receivedAt: Date.now(),
stateStartedAt: Date.now(),
tabId: 'tab-1',
worktreeId: TEST_WORKTREE_ID
}
]
})
runtime.setPtyController({
spawn: vi.fn().mockResolvedValue({ id: 'pty-1' }),
write: () => true,
kill: () => true,
getForegroundProcess: async () => null
})
runtime.attachWindow(1)
runtime.syncWindowGraph(1, {
tabs: [
{
tabId: 'tab-1',
worktreeId: TEST_WORKTREE_ID,
title: 'zsh',
activeLeafId: leafId,
layout: null
}
],
leaves: [
{
tabId: 'tab-1',
worktreeId: TEST_WORKTREE_ID,
leafId,
paneRuntimeId: 1,
ptyId: 'pty-1'
}
]
})
const [terminal] = (await runtime.listTerminals()).terminals
await expect(runtime.getTerminalAgentStatus(terminal.handle)).resolves.toEqual({
handle: terminal.handle,
isRunningAgent: false,
status: null
})
})
it('does not let fresh explicit hook state authorize a shell foreground process', async () => {
const leafId = '11111111-1111-4111-8111-111111111111'
const paneKey = makePaneKey('tab-1', leafId)
const runtime = new OrcaRuntimeService(store, undefined, {
getAgentStatusSnapshot: () => [
{
paneKey,
state: 'working',
prompt: '',
agentType: 'codex',
connectionId: null,
receivedAt: Date.now(),
stateStartedAt: Date.now(),
tabId: 'tab-1',
worktreeId: TEST_WORKTREE_ID
}
]
})
runtime.setPtyController({
spawn: vi.fn().mockResolvedValue({ id: 'pty-1' }),
write: () => true,
kill: () => true,
getForegroundProcess: async () => 'zsh'
})
runtime.attachWindow(1)
runtime.syncWindowGraph(1, {
tabs: [
{
tabId: 'tab-1',
worktreeId: TEST_WORKTREE_ID,
title: 'repo terminal',
activeLeafId: leafId,
layout: null
}
],
leaves: [
{
tabId: 'tab-1',
worktreeId: TEST_WORKTREE_ID,
leafId,
paneRuntimeId: 1,
ptyId: 'pty-1'
}
]
})
const [terminal] = (await runtime.listTerminals()).terminals
await expect(runtime.getTerminalAgentStatus(terminal.handle)).resolves.toEqual({
handle: terminal.handle,
isRunningAgent: false,
status: null
})
})
it('uses strong provider confirmation to authorize fresh hook state over a shell foreground', async () => {
const getForegroundProcess = vi.fn(async () => 'powershell.exe')
const confirmForegroundProcess = vi.fn(async () => 'claude')
const { runtime, handle } = await createExplicitAgentStatusHarness({
getForegroundProcess,
confirmForegroundProcess
})
await expect(runtime.getTerminalAgentStatus(handle)).resolves.toEqual({
handle,
isRunningAgent: true,
status: 'working'
})
expect(getForegroundProcess).toHaveBeenCalledOnce()
expect(getForegroundProcess).toHaveBeenCalledWith('pty-1')
expect(confirmForegroundProcess).toHaveBeenCalledOnce()
expect(confirmForegroundProcess).toHaveBeenCalledWith('pty-1')
})
it('preserves provider failure during completion-sensitive process inspection', async () => {
const failure = new Error('daemon unavailable')
const providerInspectProcess = vi.fn().mockRejectedValue(failure)
const provider = { inspectProcess: providerInspectProcess } as unknown as IPtyProvider
const inspectProcess = vi.fn((ptyId: string) => inspectPtyProviderProcess(provider, ptyId))
const getForegroundProcess = vi.fn(async () => null)
const { runtime, handle } = await createExplicitAgentStatusHarness({
getForegroundProcess,
inspectProcess
})
await expect(runtime.inspectTerminalProcess(handle)).rejects.toBe(failure)
expect(inspectProcess).toHaveBeenCalledExactlyOnceWith('pty-1')
expect(providerInspectProcess).toHaveBeenCalledExactlyOnceWith('pty-1')
expect(getForegroundProcess).not.toHaveBeenCalled()
})
it('preserves provider unavailable results during process inspection', async () => {
const inspection = {
foregroundProcess: null,
hasChildProcesses: true,
unavailable: true as const
}
const inspectProcess = vi.fn(async () => inspection)
const getForegroundProcess = vi.fn(async () => null)
const { runtime, handle } = await createExplicitAgentStatusHarness({
getForegroundProcess,
inspectProcess
})
await expect(runtime.inspectTerminalProcess(handle)).resolves.toEqual(inspection)
expect(inspectProcess).toHaveBeenCalledExactlyOnceWith('pty-1')
expect(getForegroundProcess).not.toHaveBeenCalled()
})
it('calls foreground confirmation with its controller receiver', async () => {
const getForegroundProcess = vi.fn(async () => 'powershell.exe')
const confirmForegroundProcess = vi.fn(async function (this: {
getForegroundProcess: typeof getForegroundProcess
}) {
return this.getForegroundProcess === getForegroundProcess ? 'codex' : null
})
const { runtime, handle } = await createExplicitAgentStatusHarness({
getForegroundProcess,
confirmForegroundProcess
})
await expect(runtime.getTerminalAgentStatus(handle)).resolves.toMatchObject({
isRunningAgent: true,
status: 'working'
})
expect(confirmForegroundProcess).toHaveBeenCalledOnce()
})
it.each([
['shell', async () => 'pwsh.exe'],
['non-agent', async () => 'vim'],
['unavailable', async () => null],
[
'failure',
async () => {
throw new Error('provider unavailable')
}
]
])('fails closed when shell-conflict confirmation returns %s', async (_case, confirm) => {
const confirmForegroundProcess = vi.fn(confirm)
const { runtime, handle } = await createExplicitAgentStatusHarness({
getForegroundProcess: async () => 'zsh',
confirmForegroundProcess
})
await expect(runtime.getTerminalAgentStatus(handle)).resolves.toEqual({
handle,
isRunningAgent: false,
status: null
})
expect(confirmForegroundProcess).toHaveBeenCalledOnce()
})
it('fails closed on a shell conflict when the controller cannot confirm it', async () => {
const { runtime, handle } = await createExplicitAgentStatusHarness({
getForegroundProcess: async () => 'zsh'
})
await expect(runtime.getTerminalAgentStatus(handle)).resolves.toEqual({
handle,
isRunningAgent: false,
status: null
})
})
it('skips strong confirmation when ordinary foreground evidence recognizes an agent', async () => {
const getForegroundProcess = vi.fn(async () => 'codex')
const confirmForegroundProcess = vi.fn(async () => 'codex')
const { runtime, handle } = await createExplicitAgentStatusHarness({
getForegroundProcess,
confirmForegroundProcess
})
await expect(runtime.getTerminalAgentStatus(handle)).resolves.toMatchObject({
isRunningAgent: true,
status: 'working'
})
expect(getForegroundProcess).toHaveBeenCalledOnce()
expect(confirmForegroundProcess).not.toHaveBeenCalled()
})
it('skips both foreground reads when current title evidence blocks explicit hook state', async () => {
const getForegroundProcess = vi.fn(async () => 'zsh')
const confirmForegroundProcess = vi.fn(async () => 'codex')
const { runtime, handle } = await createExplicitAgentStatusHarness({
getForegroundProcess,
confirmForegroundProcess,
title: 'zsh'
})
await expect(runtime.getTerminalAgentStatus(handle)).resolves.toMatchObject({
isRunningAgent: false,
status: null
})
expect(getForegroundProcess).not.toHaveBeenCalled()
expect(confirmForegroundProcess).not.toHaveBeenCalled()
})
it('skips foreground reads for permission title and blocked wait evidence', async () => {
for (const blocked of ['title', 'wait'] as const) {
const getForegroundProcess = vi.fn(async () => 'zsh')
const confirmForegroundProcess = vi.fn(async () => 'codex')
const { runtime, handle } = await createExplicitAgentStatusHarness({
getForegroundProcess,
confirmForegroundProcess
})
runtime.onPtyData(
'pty-1',
blocked === 'title'
? '\x1b]0;Codex waiting for permission\x07'
: 'Hooks need review. Press enter to confirm\n',
Date.now() + 1000
)
getForegroundProcess.mockClear()
confirmForegroundProcess.mockClear()
await expect(runtime.getTerminalAgentStatus(handle)).resolves.toMatchObject({
isRunningAgent: true,
status: 'permission'
})
expect(getForegroundProcess).not.toHaveBeenCalled()
expect(confirmForegroundProcess).not.toHaveBeenCalled()
}
})
it('rejects foreground evidence when the handle rebinds during the ordinary read', async () => {
const foreground = deferred<string | null>()
const getForegroundProcess = vi.fn(() => foreground.promise)
@@ -531,99 +68,4 @@ describe('OrcaRuntimeService', () => {
await expect(status).rejects.toThrow('terminal_exited')
})
it('reports permission from a title-derived action-required agent state', async () => {
const runtime = new OrcaRuntimeService(store)
runtime.setPtyController({
spawn: vi.fn().mockResolvedValue({ id: 'pty-1' }),
write: () => true,
kill: () => true,
getForegroundProcess: async () => null
})
const leafId = '11111111-1111-4111-8111-111111111111'
runtime.attachWindow(1)
runtime.syncWindowGraph(1, {
tabs: [
{
tabId: 'tab-1',
worktreeId: TEST_WORKTREE_ID,
title: 'Codex waiting for permission',
activeLeafId: leafId,
layout: null
}
],
leaves: [
{
tabId: 'tab-1',
worktreeId: TEST_WORKTREE_ID,
leafId,
paneRuntimeId: 1,
ptyId: 'pty-1'
}
]
})
const [terminal] = (await runtime.listTerminals()).terminals
await expect(runtime.getTerminalAgentStatus(terminal.handle)).resolves.toEqual({
handle: terminal.handle,
isRunningAgent: true,
status: 'permission'
})
})
it('maps fresh explicit done hook state to idle for send readiness', async () => {
const leafId = '11111111-1111-4111-8111-111111111111'
const paneKey = makePaneKey('tab-1', leafId)
const runtime = new OrcaRuntimeService(store, undefined, {
getAgentStatusSnapshot: () => [
{
paneKey,
state: 'done',
prompt: '',
agentType: 'codex',
connectionId: null,
receivedAt: Date.now(),
stateStartedAt: Date.now(),
tabId: 'tab-1',
worktreeId: TEST_WORKTREE_ID
}
]
})
runtime.setPtyController({
spawn: vi.fn().mockResolvedValue({ id: 'pty-1' }),
write: () => true,
kill: () => true,
getForegroundProcess: async () => null
})
runtime.attachWindow(1)
runtime.syncWindowGraph(1, {
tabs: [
{
tabId: 'tab-1',
worktreeId: TEST_WORKTREE_ID,
title: 'repo terminal',
activeLeafId: leafId,
layout: null
}
],
leaves: [
{
tabId: 'tab-1',
worktreeId: TEST_WORKTREE_ID,
leafId,
paneRuntimeId: 1,
ptyId: 'pty-1'
}
]
})
const [terminal] = (await runtime.listTerminals()).terminals
await expect(runtime.getTerminalAgentStatus(terminal.handle)).resolves.toEqual({
handle: terminal.handle,
isRunningAgent: true,
status: 'idle'
})
})
})
@@ -2,7 +2,7 @@ import { describe, expect, it, vi } from 'vitest'
import { committingSpawn } from './bind-first-pane.test-fixture'
import './runtime-test-module-registration.spec'
import { OrcaRuntimeService } from '../orca-runtime'
import { makePaneKey } from '../../../shared/workspace-layout/stable-pane-id'
import '../../../shared/workspace-layout/stable-pane-id'
import type { RuntimeTerminalAgentStatusEvent } from '../terminal/output/terminal-agent-status-events'
import {
TEST_WORKTREE_ID,
@@ -107,116 +107,6 @@ describe('OrcaRuntimeService', () => {
)
})
it('reads bounded terminal output and writes through the PTY controller', async () => {
const writes: string[] = []
const runtime = new OrcaRuntimeService(store)
runtime.setPtyController({
write: (_ptyId, data) => {
writes.push(data)
return true
},
kill: () => true,
getForegroundProcess: async () => null
})
runtime.attachWindow(1)
runtime.syncWindowGraph(1, {
tabs: [
{
tabId: 'tab-1',
worktreeId: 'repo-1::/tmp/worktree-a',
title: 'Claude',
activeLeafId: 'pane:1',
layout: null
}
],
leaves: [
{
tabId: 'tab-1',
worktreeId: 'repo-1::/tmp/worktree-a',
leafId: 'pane:1',
paneRuntimeId: 1,
ptyId: 'pty-1'
}
]
})
runtime.onPtyData('pty-1', '\u001b[32mhello\u001b[0m\nworld\n', 123)
const [terminal] = (await runtime.listTerminals()).terminals
const read = await runtime.readTerminal(terminal.handle)
expect(read).toMatchObject({
handle: terminal.handle,
status: 'running',
tail: ['hello', 'world'],
truncated: false,
nextCursor: expect.any(String)
})
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'], shouldClearNativeTimers: true })
try {
const [send] = await Promise.all([
runtime.sendTerminal(
terminal.handle,
{
text: 'continue',
enter: true
},
{ inputKind: 'driving' }
),
vi.runAllTimersAsync()
])
expect(send).toMatchObject({
handle: terminal.handle,
accepted: true
})
expect(writes).toEqual(['continue', '\r'])
} finally {
vi.clearAllTimers()
vi.useRealTimers()
}
})
it('reports permission from blocked terminal wait text', async () => {
const runtime = new OrcaRuntimeService(store)
runtime.setPtyController({
spawn: vi.fn().mockResolvedValue({ id: 'pty-1' }),
write: () => true,
kill: () => true,
getForegroundProcess: async () => null
})
const leafId = '11111111-1111-4111-8111-111111111111'
runtime.attachWindow(1)
runtime.syncWindowGraph(1, {
tabs: [
{
tabId: 'tab-1',
worktreeId: TEST_WORKTREE_ID,
title: 'repo terminal',
activeLeafId: leafId,
layout: null
}
],
leaves: [
{
tabId: 'tab-1',
worktreeId: TEST_WORKTREE_ID,
leafId,
paneRuntimeId: 1,
ptyId: 'pty-1'
}
]
})
runtime.onPtyData('pty-1', 'Hooks need review. Press enter to confirm\n', 123)
const [terminal] = (await runtime.listTerminals()).terminals
await expect(runtime.getTerminalAgentStatus(terminal.handle)).resolves.toEqual({
handle: terminal.handle,
isRunningAgent: true,
status: 'permission'
})
})
it('keeps blocked prompt text authoritative over an OpenCode marker', async () => {
const runtime = new OrcaRuntimeService(store)
runtime.setPtyController({
@@ -267,370 +157,4 @@ describe('OrcaRuntimeService', () => {
blockedReason: 'agent-interactive-prompt'
})
})
it('reports permission from blocked wait text over title-only working state', async () => {
const runtime = new OrcaRuntimeService(store)
runtime.setPtyController({
spawn: vi.fn().mockResolvedValue({ id: 'pty-1' }),
write: () => true,
kill: () => true,
getForegroundProcess: async () => null
})
const leafId = '11111111-1111-4111-8111-111111111111'
runtime.attachWindow(1)
runtime.syncWindowGraph(1, {
tabs: [
{
tabId: 'tab-1',
worktreeId: TEST_WORKTREE_ID,
title: 'Codex working',
activeLeafId: leafId,
layout: null
}
],
leaves: [
{
tabId: 'tab-1',
worktreeId: TEST_WORKTREE_ID,
leafId,
paneRuntimeId: 1,
ptyId: 'pty-1'
}
]
})
runtime.onPtyData('pty-1', 'Hooks need review. Press enter to confirm\n', 123)
const [terminal] = (await runtime.listTerminals()).terminals
await expect(runtime.getTerminalAgentStatus(terminal.handle)).resolves.toEqual({
handle: terminal.handle,
isRunningAgent: true,
status: 'permission'
})
})
it('lets a live non-permission title supersede stale blocked wait text', async () => {
const runtime = new OrcaRuntimeService(store)
runtime.setPtyController({
spawn: vi.fn().mockResolvedValue({ id: 'pty-1' }),
write: () => true,
kill: () => true,
getForegroundProcess: async () => null
})
const leafId = '11111111-1111-4111-8111-111111111111'
runtime.attachWindow(1)
runtime.syncWindowGraph(1, {
tabs: [
{
tabId: 'tab-1',
worktreeId: TEST_WORKTREE_ID,
title: 'repo terminal',
activeLeafId: leafId,
layout: null
}
],
leaves: [
{
tabId: 'tab-1',
worktreeId: TEST_WORKTREE_ID,
leafId,
paneRuntimeId: 1,
ptyId: 'pty-1'
}
]
})
runtime.onPtyData('pty-1', 'Hooks need review. Press enter to confirm\n', 123)
runtime.onPtyData('pty-1', '\x1b]0;Codex working\x07', 124)
const [terminal] = (await runtime.listTerminals()).terminals
await expect(runtime.getTerminalAgentStatus(terminal.handle)).resolves.toEqual({
handle: terminal.handle,
isRunningAgent: true,
status: 'working'
})
})
it('maps fresh explicit waiting hook state to permission over a working title', async () => {
const leafId = '11111111-1111-4111-8111-111111111111'
const paneKey = makePaneKey('tab-1', leafId)
const runtime = new OrcaRuntimeService(store, undefined, {
getAgentStatusSnapshot: () => [
{
paneKey,
state: 'waiting',
prompt: '',
agentType: 'codex',
connectionId: null,
receivedAt: Date.now(),
stateStartedAt: Date.now(),
tabId: 'tab-1',
worktreeId: TEST_WORKTREE_ID
}
]
})
runtime.setPtyController({
spawn: vi.fn().mockResolvedValue({ id: 'pty-1' }),
write: () => true,
kill: () => true,
getForegroundProcess: async () => null
})
runtime.attachWindow(1)
runtime.syncWindowGraph(1, {
tabs: [
{
tabId: 'tab-1',
worktreeId: TEST_WORKTREE_ID,
title: 'Codex working',
activeLeafId: leafId,
layout: null
}
],
leaves: [
{
tabId: 'tab-1',
worktreeId: TEST_WORKTREE_ID,
leafId,
paneRuntimeId: 1,
ptyId: 'pty-1'
}
]
})
const [terminal] = (await runtime.listTerminals()).terminals
await expect(runtime.getTerminalAgentStatus(terminal.handle)).resolves.toEqual({
handle: terminal.handle,
isRunningAgent: true,
status: 'permission'
})
})
// A restored permission wait is the exception; agent-prompt-restored-permission.test.ts covers it (#35962).
it('does not treat a restored-unconfirmed hook row as live terminal status', async () => {
const leafId = '11111111-1111-4111-8111-111111111111'
const paneKey = makePaneKey('tab-1', leafId)
const runtime = new OrcaRuntimeService(store, undefined, {
getAgentStatusSnapshot: () => [
{
paneKey,
state: 'working',
prompt: '',
agentType: 'codex',
connectionId: null,
receivedAt: Date.now(),
stateStartedAt: Date.now(),
tabId: 'tab-1',
worktreeId: TEST_WORKTREE_ID,
restoredUnconfirmed: true
}
]
})
runtime.setPtyController({
spawn: vi.fn().mockResolvedValue({ id: 'pty-1' }),
write: () => true,
kill: () => true,
getForegroundProcess: async () => null
})
runtime.attachWindow(1)
runtime.syncWindowGraph(1, {
tabs: [
{
tabId: 'tab-1',
worktreeId: TEST_WORKTREE_ID,
title: 'repo terminal',
activeLeafId: leafId,
layout: null
}
],
leaves: [
{
tabId: 'tab-1',
worktreeId: TEST_WORKTREE_ID,
leafId,
paneRuntimeId: 1,
ptyId: 'pty-1'
}
]
})
const [terminal] = (await runtime.listTerminals()).terminals
await expect(runtime.getTerminalAgentStatus(terminal.handle)).resolves.toEqual({
handle: terminal.handle,
isRunningAgent: false,
status: null
})
})
it('does not let stale wait text override a fresh explicit working state', async () => {
const leafId = '11111111-1111-4111-8111-111111111111'
const paneKey = makePaneKey('tab-1', leafId)
const runtime = new OrcaRuntimeService(store, undefined, {
getAgentStatusSnapshot: () => [
{
paneKey,
state: 'working',
prompt: '',
agentType: 'codex',
connectionId: null,
receivedAt: Date.now(),
stateStartedAt: Date.now(),
tabId: 'tab-1',
worktreeId: TEST_WORKTREE_ID
}
]
})
runtime.setPtyController({
spawn: vi.fn().mockResolvedValue({ id: 'pty-1' }),
write: () => true,
kill: () => true,
getForegroundProcess: async () => null
})
runtime.attachWindow(1)
runtime.syncWindowGraph(1, {
tabs: [
{
tabId: 'tab-1',
worktreeId: TEST_WORKTREE_ID,
title: 'Codex working',
activeLeafId: leafId,
layout: null
}
],
leaves: [
{
tabId: 'tab-1',
worktreeId: TEST_WORKTREE_ID,
leafId,
paneRuntimeId: 1,
ptyId: 'pty-1'
}
]
})
runtime.onPtyData('pty-1', 'Hooks need review. Press enter to confirm\n', 123)
const [terminal] = (await runtime.listTerminals()).terminals
await expect(runtime.getTerminalAgentStatus(terminal.handle)).resolves.toEqual({
handle: terminal.handle,
isRunningAgent: true,
status: 'working'
})
})
it('reports permission when blocked wait text is newer than explicit working state', async () => {
const leafId = '11111111-1111-4111-8111-111111111111'
const paneKey = makePaneKey('tab-1', leafId)
const now = Date.now()
const runtime = new OrcaRuntimeService(store, undefined, {
getAgentStatusSnapshot: () => [
{
paneKey,
state: 'working',
prompt: '',
agentType: 'codex',
connectionId: null,
receivedAt: now - 1000,
stateStartedAt: now - 1000,
tabId: 'tab-1',
worktreeId: TEST_WORKTREE_ID
}
]
})
runtime.setPtyController({
spawn: vi.fn().mockResolvedValue({ id: 'pty-1' }),
write: () => true,
kill: () => true,
getForegroundProcess: async () => null
})
runtime.attachWindow(1)
runtime.syncWindowGraph(1, {
tabs: [
{
tabId: 'tab-1',
worktreeId: TEST_WORKTREE_ID,
title: 'Codex working',
activeLeafId: leafId,
layout: null
}
],
leaves: [
{
tabId: 'tab-1',
worktreeId: TEST_WORKTREE_ID,
leafId,
paneRuntimeId: 1,
ptyId: 'pty-1'
}
]
})
runtime.onPtyData('pty-1', 'Hooks need review. Press enter to confirm\n', now)
const [terminal] = (await runtime.listTerminals()).terminals
await expect(runtime.getTerminalAgentStatus(terminal.handle)).resolves.toEqual({
handle: terminal.handle,
isRunningAgent: true,
status: 'permission'
})
})
it('timestamps blocked wait text when the prompt arrives across PTY chunks', async () => {
const leafId = '11111111-1111-4111-8111-111111111111'
const paneKey = makePaneKey('tab-1', leafId)
const now = Date.now()
const runtime = new OrcaRuntimeService(store, undefined, {
getAgentStatusSnapshot: () => [
{
paneKey,
state: 'working',
prompt: '',
agentType: 'codex',
connectionId: null,
receivedAt: now,
stateStartedAt: now,
tabId: 'tab-1',
worktreeId: TEST_WORKTREE_ID
}
]
})
runtime.setPtyController({
spawn: vi.fn().mockResolvedValue({ id: 'pty-1' }),
write: () => true,
kill: () => true,
getForegroundProcess: async () => null
})
runtime.attachWindow(1)
runtime.syncWindowGraph(1, {
tabs: [
{
tabId: 'tab-1',
worktreeId: TEST_WORKTREE_ID,
title: 'Codex working',
activeLeafId: leafId,
layout: null
}
],
leaves: [
{
tabId: 'tab-1',
worktreeId: TEST_WORKTREE_ID,
leafId,
paneRuntimeId: 1,
ptyId: 'pty-1'
}
]
})
runtime.onPtyData('pty-1', 'Hooks need review. ', now + 1000)
runtime.onPtyData('pty-1', 'Press enter to confirm\n', now + 1001)
const [terminal] = (await runtime.listTerminals()).terminals
await expect(runtime.getTerminalAgentStatus(terminal.handle)).resolves.toEqual({
handle: terminal.handle,
isRunningAgent: true,
status: 'permission'
})
})
})
@@ -1,39 +0,0 @@
import { describe, expect, it, vi } from 'vitest'
import './runtime-test-module-registration.spec'
import { OrcaRuntimeService } from '../orca-runtime'
import {
TEST_REPO_ID,
TEST_WORKTREE_ID,
TEST_WORKTREE_PATH,
store
} from './orca-runtime-test-fixtures.spec'
describe('OrcaRuntimeService', () => {
it('rejects a qualified removal when only another host has persisted ownership', async () => {
const runtimeStore = {
...store,
getWorktreeMeta: () => ({ ...store.getWorktreeMeta(TEST_WORKTREE_ID), hostId: 'local' })
}
const runtime = new OrcaRuntimeService(runtimeStore as never)
const internals = runtime as unknown as {
listResolvedWorktrees: () => Promise<unknown[]>
resolveWorktreeRemovalTarget: (
worktreeSelector: string,
requiredHostId?: string
) => Promise<unknown>
}
internals.listResolvedWorktrees = vi.fn().mockResolvedValue([
{
id: TEST_WORKTREE_ID,
repoId: TEST_REPO_ID,
path: TEST_WORKTREE_PATH,
hostId: 'local'
}
])
await expect(
internals.resolveWorktreeRemovalTarget(TEST_WORKTREE_ID, 'runtime:env-b')
).rejects.toThrow('selector_not_found')
expect(internals.listResolvedWorktrees).not.toHaveBeenCalled()
})
})
@@ -1,7 +1,7 @@
import { describe, expect, it, vi } from 'vitest'
import './runtime-test-module-registration.spec'
import { OrcaRuntimeService } from '../orca-runtime'
import { deleteWorktreeHistoryDirMock } from '../orca-runtime-test-mocks/setup.spec'
import '../orca-runtime-test-mocks/setup.spec'
import { getBrowserHostLeaseRegistry } from '../browser-host/leases/browser-host-lease-registry-instance'
import { getRuntimeBrowserPageRegistry } from '../browser/client-pages/runtime-browser-page-registry'
import {
@@ -14,7 +14,7 @@ import {
attachClientBrowserHost,
publishClientHostedPage
} from './orca-runtime-test-scenario-builders.spec'
import { hostWorktreeTarget } from '../../../shared/worktree/host-qualified-identity'
import '../../../shared/worktree/host-qualified-identity'
describe('OrcaRuntimeService', () => {
it('preserves a live client page across headed renderer graph updates and prunes it after retirement', async () => {
@@ -164,71 +164,4 @@ describe('OrcaRuntimeService', () => {
placement
)
})
it('preserves bare-id runtime state when removing a different qualified owner', () => {
const removeWorktreeMeta = vi.fn()
const runtimeStore = {
...store,
getWorktreeMeta: () => ({ ...store.getWorktreeMeta(TEST_WORKTREE_ID), hostId: 'local' }),
removeWorktreeMeta
}
const runtime = new OrcaRuntimeService(runtimeStore as never)
const internals = runtime as unknown as {
mobileSessionTabsByWorktree: Map<string, unknown>
removeWorktreeMetadataAndHistory: (
runtimeStore: typeof store,
worktreeId: string,
hostId: string
) => void
}
const localSession = { tabs: [{ id: 'local-tab' }] }
internals.mobileSessionTabsByWorktree.set(TEST_WORKTREE_ID, localSession)
internals.removeWorktreeMetadataAndHistory(
runtimeStore as typeof store,
TEST_WORKTREE_ID,
'runtime:env-b'
)
expect(removeWorktreeMeta).toHaveBeenCalledWith(
hostWorktreeTarget({ id: TEST_WORKTREE_ID, hostId: 'runtime:env-b' })
)
expect(internals.mobileSessionTabsByWorktree.get(TEST_WORKTREE_ID)).toBe(localSession)
})
it('preserves bare-id runtime state when removed-host metadata masks another owner', () => {
const localRepo = store.getRepo(TEST_REPO_ID)!
const remoteRepo = { ...localRepo, connectionId: 'ssh-1' }
const removeWorktreeMeta = vi.fn()
const runtimeStore = {
...store,
getRepos: () => [localRepo, remoteRepo],
getWorktreeMeta: () => ({ ...store.getWorktreeMeta(TEST_WORKTREE_ID), hostId: 'ssh:ssh-1' }),
removeWorktreeMeta
}
const runtime = new OrcaRuntimeService(runtimeStore as never)
const internals = runtime as unknown as {
mobileSessionTabsByWorktree: Map<string, unknown>
removeWorktreeMetadataAndHistory: (
runtimeStore: typeof store,
worktreeId: string,
hostId: string
) => void
}
const survivingSession = { tabs: [{ id: 'same-id-local-tab' }] }
internals.mobileSessionTabsByWorktree.set(TEST_WORKTREE_ID, survivingSession)
deleteWorktreeHistoryDirMock.mockClear()
internals.removeWorktreeMetadataAndHistory(
runtimeStore as typeof store,
TEST_WORKTREE_ID,
'ssh:ssh-1'
)
expect(removeWorktreeMeta).toHaveBeenCalledWith(
hostWorktreeTarget({ id: TEST_WORKTREE_ID, hostId: 'ssh:ssh-1' })
)
expect(internals.mobileSessionTabsByWorktree.get(TEST_WORKTREE_ID)).toBe(survivingSession)
expect(deleteWorktreeHistoryDirMock).not.toHaveBeenCalled()
})
})
@@ -63,16 +63,4 @@ describe('OrcaRuntimeService ConPTY stream reads', () => {
clearNativeWindowsConptyPty(ptyId)
}
})
it('keeps accumulated-stream reads for terminals that are not ConPTY', async () => {
const ptyId = 'pty-posix-prediction'
const runtime = startConptyRuntime(ptyId)
const [terminal] = (await runtime.listTerminals()).terminals
runtime.onPtyData(ptyId, 'one\r\ntwo\rTWO\r\n', 100)
const read = await runtime.readTerminal(terminal.handle)
expect(read.tail).toEqual(['one', 'TWO'])
expect(read.source).toBe('stream')
})
})
-1
View File
@@ -17,7 +17,6 @@ await import('./orca-runtime-tests/dedupe-worktree-create.spec')
await import('./orca-runtime-tests/paired-settings.spec')
await import('./orca-runtime-tests/runtime-availability.spec')
await import('./orca-runtime-tests/browser-capabilities.spec')
await import('./orca-runtime-tests/browser-capabilities-part-02.spec')
await import('./orca-runtime-tests/managed-server-capability.spec')
await import('./orca-runtime-tests/window-authority.spec')
await import('./orca-runtime-tests/terminal-handles.spec')
@@ -49,12 +49,6 @@ describe('worktree removal drops the workspace trust Orca wrote', () => {
}
})
it('removes the recorded entry before the purge resolves', async () => {
await purgeWithMeta()(WORKTREE_ID)
expect(readFileSync(configPath, 'utf-8')).not.toContain(`[projects."${PROJECT}"]`)
})
it('removes only the Antigravity trustedWorkspaces entry Orca appended', async () => {
const agyHome = join(userDataDir, 'agy-home')
const settingsPath = join(agyHome, '.gemini', 'antigravity-cli', 'settings.json')
@@ -73,19 +67,6 @@ describe('worktree removal drops the workspace trust Orca wrote', () => {
})
})
it('removes a WSL guest entry recorded under the host workspace path', async () => {
const agyHome = join(userDataDir, 'agy-home')
const settingsPath = join(agyHome, '.gemini', 'antigravity-cli', 'settings.json')
markAntigravityWorkspaceTrusted(PROJECT, agyHome, '/home/u/feature-r1')
expect(JSON.parse(readFileSync(settingsPath, 'utf-8')).trustedWorkspaces).toEqual([
'/home/u/feature-r1'
])
await purgeWithMeta()(WORKTREE_ID)
expect(JSON.parse(readFileSync(settingsPath, 'utf-8')).trustedWorkspaces).toEqual([])
})
it('keeps an Antigravity entry that was trusted before Orca saw the path', async () => {
const agyHome = join(userDataDir, 'agy-home')
const settingsPath = join(agyHome, '.gemini', 'antigravity-cli', 'settings.json')
@@ -99,18 +80,4 @@ describe('worktree removal drops the workspace trust Orca wrote', () => {
trustedWorkspaces: [PROJECT]
})
})
it('keeps the folder trust when one folder-workspace session is removed', async () => {
const session = `${WORKTREE_ID}::workspace:5e8cc6b2-1d3f-4a5b-9c2d-7f6e5a4b3c2d`
await purgeWithMeta()(session, undefined, false)
await purgeWithMeta()(session)
expect(readFileSync(configPath, 'utf-8')).toContain(`[projects."${PROJECT}"]`)
})
it('keeps the entry while another host still owns the same path-derived id', async () => {
await purgeWithMeta({ hostId: 'ssh:host-b' })(WORKTREE_ID, 'ssh:host-a')
expect(readFileSync(configPath, 'utf-8')).toContain(`[projects."${PROJECT}"]`)
})
})
@@ -149,117 +149,4 @@ describe('Azure DevOps hosted review integration', () => {
}
}
)
it('prefers an active Azure Repos PR over a newer abandoned PR for the same branch', async () => {
const seen: SeenRequest[] = []
const server = createServer((req: IncomingMessage, res: ServerResponse) => {
const url = new URL(req.url ?? '/', `http://${req.headers.host ?? '127.0.0.1'}`)
seen.push({
pathname: url.pathname,
search: url.search,
authorization: req.headers.authorization
})
if (url.pathname === '/acme/Project/_apis/git/repositories/repo') {
sendJson(res, {
id: 'repo-guid',
webUrl: 'https://dev.azure.com/acme/Project/_git/repo'
})
return
}
if (url.pathname === '/acme/Project/_apis/git/repositories/repo-guid/pullRequests') {
sendJson(res, {
value: [
{
pullRequestId: 40,
title: 'Abandoned branch',
status: 'abandoned',
creationDate: '2026-05-10T00:00:00Z',
closedDate: '2026-05-20T00:00:00Z',
mergeStatus: 'conflicts',
lastMergeSourceCommit: { commitId: 'old123' }
},
{
pullRequestId: 41,
title: 'Active branch',
status: 'active',
creationDate: '2026-05-01T00:00:00Z',
mergeStatus: 'succeeded',
lastMergeSourceCommit: { commitId: 'active123' }
}
]
})
return
}
if (
url.pathname === '/acme/Project/_apis/git/repositories/repo-guid/pullRequests/40/statuses'
) {
sendJson(res, { value: [{ state: 'failed' }] })
return
}
if (
url.pathname === '/acme/Project/_apis/git/repositories/repo-guid/pullRequests/41/statuses'
) {
sendJson(res, { value: [{ state: 'succeeded' }] })
return
}
res.writeHead(404, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ message: 'not found' }))
})
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve))
const repoPath = await mkdtemp(join(tmpdir(), 'orca-azure-review-active-'))
try {
const address = server.address()
if (!address || typeof address === 'string') {
throw new Error('expected TCP server address')
}
process.env.ORCA_AZURE_DEVOPS_API_BASE_URL = `http://127.0.0.1:${address.port}/acme/Project`
await execFileAsync('git', ['init'], { cwd: repoPath })
await execFileAsync(
'git',
['remote', 'add', 'origin', 'https://dev.azure.com/acme/Project/_git/repo'],
{ cwd: repoPath }
)
await expect(
getHostedReviewForBranch({
executionHostId: 'local',
repoPath,
branch: 'refs/heads/feature/azure'
})
).resolves.toMatchObject({
provider: 'azure-devops',
number: 41,
title: 'Active branch',
state: 'open',
status: 'success',
checksSummary: {
state: 'success',
total: 1,
passed: 1,
failed: 0,
cancelled: 0,
pending: 0,
neutral: 0
},
mergeable: 'MERGEABLE',
headSha: 'active123'
})
expect(seen.map((request) => request.pathname)).toContain(
'/acme/Project/_apis/git/repositories/repo-guid/pullRequests/41/statuses'
)
} finally {
await rm(repoPath, { recursive: true, force: true })
await new Promise<void>((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()))
})
}
})
})
@@ -1,451 +0,0 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ORCA_BROWSER_GUEST_WEB_PREFERENCES } from '../../shared/browser-guest-web-preferences'
vi.mock('../host/native-chat-visual-guest-theme', () => ({
installNativeChatVisualGuestTheme: vi.fn()
}))
const mocks = vi.hoisted(() => ({
attachGuestPolicies: vi.fn(),
installNavigationPolicy: vi.fn(),
isAllowedPartition: vi.fn(),
attachRouteGuest: vi.fn(),
installJavaScriptDialogs: vi.fn(),
registerPluginGuard: vi.fn(),
admitPluginFrame: vi.fn(),
installPluginFramePolicies: vi.fn()
}))
vi.mock('../browser/browser-manager', () => ({
browserManager: { attachGuestPolicies: mocks.attachGuestPolicies }
}))
vi.mock('../browser/sessions/browser-session-registry', () => ({
browserSessionRegistry: { isAllowedPartition: mocks.isAllowedPartition }
}))
vi.mock('./host-frame-navigation-guard', () => ({
registerHostFrameNavigationGuard: mocks.registerPluginGuard
}))
vi.mock('./privileged-window-navigation', () => ({
installPrivilegedWindowNavigationPolicy: mocks.installNavigationPolicy
}))
vi.mock('../browser/routing/sessions/browser-route-session-runtime', () => ({
browserRouteSessionRegistry: { isAllowedPartition: () => false },
browserRouteWebContentsRegistry: { attachGuest: mocks.attachRouteGuest }
}))
vi.mock('../browser/routing/local-host-partitions/local-ssh-browser-partitions', () => ({
isLocalSshBrowserPartition: () => false,
enforceLocalSshWebRtcPolicyForGuest: vi.fn()
}))
vi.mock('../browser/extensions/profile-sessions/browser-extension-session-loader', () => ({
getBrowserExtensionSessionForProfile: () => null
}))
vi.mock('../browser/guest-lifecycle/prompts/browser-javascript-dialogs', () => ({
installBrowserJavaScriptDialogs: mocks.installJavaScriptDialogs
}))
vi.mock(import('../plugins/panels/plugin-frame-guest-registry'), async (importOriginal) => ({
PLUGIN_FRAME_GUEST_RENDERER_ARGUMENTS: (await importOriginal())
.PLUGIN_FRAME_GUEST_RENDERER_ARGUMENTS,
admitPluginFrameAttach: mocks.admitPluginFrame,
isPluginFramePartition: (partition: string) => partition.startsWith('persist:orca-plugin-'),
attachPluginFrameGuest: (guest: { session?: unknown }, host: unknown) => {
if (guest.session !== 'plugin-session') {
return false
}
mocks.installPluginFramePolicies(guest, host)
return true
}
}))
vi.mock('../browser-passwords/browser-password-guest-service', () => ({
attachBrowserPasswordGuest: vi.fn()
}))
vi.mock('../browser/document-preview/doc-preview-protocol', () => ({
isDocPreviewSession: (candidate: unknown) => candidate === 'doc-preview-session'
}))
import { installMainWindowWebviewSecurity } from './main-window-webview-security'
import {
getDocPreviewGrant,
mintDocPreviewGrant,
revokeAllDocPreviewGrants
} from '../browser/document-preview/doc-preview-grant-registry'
import {
publishDocPreviewFailure,
setDocPreviewFailureSink
} from '../browser/document-preview/doc-preview-failure-notice'
import { buildDocPreviewUrl, DOC_PREVIEW_PARTITION } from '../../shared/doc-preview-scheme'
function installOnFakeWindow(): {
handlers: Record<string, (...args: never[]) => void>
webContents: { on: ReturnType<typeof vi.fn>; setWebRTCIPHandlingPolicy: ReturnType<typeof vi.fn> }
} {
const handlers: Record<string, (...args: never[]) => void> = {}
const webContents = {
on: vi.fn((event: string, handler: (...args: never[]) => void) => {
handlers[event] = handler
}),
setWebRTCIPHandlingPolicy: vi.fn()
}
installMainWindowWebviewSecurity({ webContents } as never)
return { handlers, webContents }
}
function mintPreviewGrant(): ReturnType<typeof mintDocPreviewGrant> {
return mintDocPreviewGrant({
owner: { kind: 'ssh', connectionId: 'ssh-1' },
root: '/home/alice/docs',
entryRelativePath: 'index.html',
browserPageId: 'page-1'
})
}
describe('main window webview security', () => {
beforeEach(() => {
vi.clearAllMocks()
revokeAllDocPreviewGrants()
})
it('fails closed before applying hardened guest preferences', () => {
const { handlers, webContents } = installOnFakeWindow()
mocks.isAllowedPartition.mockReturnValue(false)
const preventDefault = vi.fn()
handlers['will-attach-webview']?.(
{ preventDefault } as never,
{ partition: 'persist:untrusted', preload: 'attacker.js' } as never,
{ src: 'https://example.com', preload: 'attacker.js' } as never
)
expect(preventDefault).toHaveBeenCalledOnce()
expect(mocks.installNavigationPolicy).toHaveBeenCalledWith(webContents)
expect(mocks.registerPluginGuard).toHaveBeenCalledWith(webContents)
expect(webContents.setWebRTCIPHandlingPolicy).toHaveBeenCalledWith('disable_non_proxied_udp')
})
it('removes renderer preload input and restores every hardened preference', () => {
const { handlers } = installOnFakeWindow()
mocks.isAllowedPartition.mockReturnValue(true)
const params = { src: 'https://example.com', preload: 'attacker.js' }
const preferences: Record<string, unknown> = {
partition: 'persist:orca-browser',
preload: 'attacker.js',
preloadURL: 'attacker.js',
sandbox: false
}
handlers['will-attach-webview']?.(
{ preventDefault: vi.fn() } as never,
preferences as never,
params as never
)
expect(params).not.toHaveProperty('preload')
expect(preferences).toMatchObject({
...ORCA_BROWSER_GUEST_WEB_PREFERENCES,
partition: 'persist:orca-browser',
contextIsolation: true,
nodeIntegration: false,
nodeIntegrationInSubFrames: true,
sandbox: true,
webSecurity: true
})
expect(preferences).not.toHaveProperty('preloadURL')
expect(String(preferences.preload)).toMatch(/browser-guest-preload\.js$/)
})
it('runs preloads in subframes of every local profile tab, so extensions switched on later reach it', () => {
const { handlers } = installOnFakeWindow()
mocks.isAllowedPartition.mockReturnValue(true)
const preferences: Record<string, unknown> = {
partition: 'persist:orca-browser-work',
nodeIntegrationInSubFrames: false
}
handlers['will-attach-webview']?.(
{ preventDefault: vi.fn() } as never,
preferences as never,
{ src: 'https://example.com' } as never
)
expect(preferences.nodeIntegrationInSubFrames).toBe(true)
})
})
describe('orca-preview scheme admission', () => {
beforeEach(() => {
vi.clearAllMocks()
revokeAllDocPreviewGrants()
})
it('admits a preview URL only on the preview partition and only with a live grant', () => {
// Install first, as the window does: installation itself clears the registry.
const { handlers } = installOnFakeWindow()
const grant = mintPreviewGrant()
mocks.isAllowedPartition.mockReturnValue(false)
const preventDefault = vi.fn()
const preferences: Record<string, unknown> = {
partition: DOC_PREVIEW_PARTITION,
preload: 'attacker.js',
sandbox: false
}
handlers['will-attach-webview']?.(
{ preventDefault } as never,
preferences as never,
{ src: buildDocPreviewUrl(grant.id, 'index.html'), preload: 'attacker.js' } as never
)
expect(preventDefault).not.toHaveBeenCalled()
expect(preferences).toMatchObject({
partition: DOC_PREVIEW_PARTITION,
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
webSecurity: true
})
expect(preferences).not.toHaveProperty('preloadURL')
})
// Only preview guests get the pinned link preload; browsing guests must never acquire it and renderer values must never replace it.
it('pins the preview preload onto a preview attach, replacing whatever the renderer asked for', () => {
const { handlers } = installOnFakeWindow()
const grant = mintPreviewGrant()
mocks.isAllowedPartition.mockReturnValue(false)
const params = { src: buildDocPreviewUrl(grant.id, 'index.html'), preload: 'attacker.js' }
const preferences: Record<string, unknown> = {
partition: DOC_PREVIEW_PARTITION,
preload: 'attacker.js'
}
handlers['will-attach-webview']?.(
{ preventDefault: vi.fn() } as never,
preferences as never,
params as never
)
expect(params).not.toHaveProperty('preload')
expect(String(preferences.preload)).toMatch(/doc-preview-link-preload\.js$/)
})
it('keeps the preview preload off a browsing attach', () => {
const { handlers } = installOnFakeWindow()
mocks.isAllowedPartition.mockReturnValue(true)
const preferences: Record<string, unknown> = { partition: 'persist:orca-browser' }
handlers['will-attach-webview']?.(
{ preventDefault: vi.fn() } as never,
preferences as never,
{ src: 'https://example.com' } as never
)
expect(String(preferences.preload)).toMatch(/browser-guest-preload\.js$/)
})
it('denies a preview URL whose grant is unknown or revoked', () => {
const { handlers } = installOnFakeWindow()
mocks.isAllowedPartition.mockReturnValue(false)
const preventDefault = vi.fn()
handlers['will-attach-webview']?.(
{ preventDefault } as never,
{ partition: DOC_PREVIEW_PARTITION } as never,
{ src: `orca-preview://${'0'.repeat(32)}/index.html` } as never
)
expect(preventDefault).toHaveBeenCalledOnce()
})
it('denies a preview URL smuggled onto a browsing partition', () => {
const { handlers } = installOnFakeWindow()
const grant = mintPreviewGrant()
// Even an allowlisted browsing partition must not load the preview scheme.
mocks.isAllowedPartition.mockReturnValue(true)
const preventDefault = vi.fn()
handlers['will-attach-webview']?.(
{ preventDefault } as never,
{ partition: 'persist:orca-browser' } as never,
{ src: buildDocPreviewUrl(grant.id, 'index.html') } as never
)
expect(preventDefault).toHaveBeenCalledOnce()
})
it('denies a web URL on the preview partition', () => {
const { handlers } = installOnFakeWindow()
mocks.isAllowedPartition.mockReturnValue(false)
const preventDefault = vi.fn()
handlers['will-attach-webview']?.(
{ preventDefault } as never,
{ partition: DOC_PREVIEW_PARTITION } as never,
{ src: 'https://example.com' } as never
)
expect(preventDefault).toHaveBeenCalledOnce()
})
// The granting renderer is the only valid click sink; verify this window hosts the preview, not merely its profile.
it('attaches a preview guest under the workspace-doc profile, hosted by this window', () => {
const { handlers, webContents } = installOnFakeWindow()
handlers['did-attach-webview']?.({} as never, { session: 'doc-preview-session' } as never)
expect(mocks.attachGuestPolicies).toHaveBeenCalledWith(
{ session: 'doc-preview-session' },
null,
{
profile: 'workspace-doc',
host: webContents
}
)
expect(mocks.attachRouteGuest).not.toHaveBeenCalled()
expect(mocks.installJavaScriptDialogs).not.toHaveBeenCalled()
})
it('keeps browser guests on the browsing profile and its route registration', () => {
const { handlers } = installOnFakeWindow()
handlers['did-attach-webview']?.({} as never, { session: 'browser-session' } as never)
expect(mocks.attachGuestPolicies).toHaveBeenCalledOnce()
expect(mocks.attachGuestPolicies.mock.calls[0]?.[2]).toBeUndefined()
expect(mocks.attachRouteGuest).toHaveBeenCalledOnce()
expect(mocks.installJavaScriptDialogs).toHaveBeenCalledOnce()
})
// WebContents teardown leaves no preview readers; clear grants and the dead failure sink.
it('drops the failure sink and every grant when the window contents are destroyed', () => {
const { handlers } = installOnFakeWindow()
const grant = mintPreviewGrant()
const send = vi.fn()
setDocPreviewFailureSink({ send })
expect(getDocPreviewGrant(grant.id)).not.toBeNull()
handlers['destroyed']?.()
publishDocPreviewFailure({
grantId: grant.id,
relativePath: 'index.html',
reason: 'unreadable'
})
expect(getDocPreviewGrant(grant.id)).toBeNull()
expect(send).not.toHaveBeenCalled()
})
// A renderer owns grant-to-preview bookkeeping; replacing it must revoke otherwise unreleasable read authority.
it('clears grants a previous renderer left behind when the window is created', () => {
const stranded = mintPreviewGrant()
installOnFakeWindow()
expect(getDocPreviewGrant(stranded.id)).toBeNull()
})
it('clears grants the renderer forgot across a reload', () => {
const { handlers } = installOnFakeWindow()
const grant = mintPreviewGrant()
handlers['did-start-navigation']?.({ isMainFrame: true, isSameDocument: false } as never)
expect(getDocPreviewGrant(grant.id)).toBeNull()
})
it('keeps grants across an in-document or subframe navigation, which keeps the renderer', () => {
const { handlers } = installOnFakeWindow()
const grant = mintPreviewGrant()
handlers['did-start-navigation']?.({ isMainFrame: true, isSameDocument: true } as never)
handlers['did-start-navigation']?.({ isMainFrame: false, isSameDocument: false } as never)
expect(getDocPreviewGrant(grant.id)).not.toBeNull()
})
})
describe('plugin frame guests', () => {
const partition = 'persist:orca-plugin-0123456789abcdef'
const src = 'orca-plugin-frame://0123456789abcdef/panel.html'
beforeEach(() => vi.clearAllMocks())
it('refuses a plugin partition main did not prepare for that origin', () => {
const { handlers } = installOnFakeWindow()
mocks.admitPluginFrame.mockReturnValue(false)
const preventDefault = vi.fn()
const preferences: Record<string, unknown> = { partition, preload: 'attacker.js' }
handlers['will-attach-webview']?.(
{ preventDefault } as never,
preferences as never,
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the handler reads only params.src.
{ src } as never
)
expect(mocks.admitPluginFrame).toHaveBeenCalledWith(partition, src)
expect(preventDefault).toHaveBeenCalledOnce()
expect(preferences.preload).toBe('attacker.js')
})
it('pins the guest preload and every secure preference, whatever the markup asked', () => {
const { handlers } = installOnFakeWindow()
mocks.admitPluginFrame.mockReturnValue(true)
const params: Record<string, unknown> = { src, preload: 'attacker.js', allowpopups: 'true' }
const preferences: Record<string, unknown> = {
partition,
preload: 'attacker.js',
preloadURL: 'attacker.js',
additionalArguments: ['--x'],
sandbox: false,
contextIsolation: false,
nodeIntegration: true,
webSecurity: false,
webviewTag: true,
disablePopups: false,
experimentalFeatures: true,
enableWebSQL: true,
zoomFactor: 1.25
}
handlers['will-attach-webview']?.(
{ preventDefault: vi.fn() } as never,
preferences as never,
params as never
)
expect(params).not.toHaveProperty('preload')
expect(params).not.toHaveProperty('allowpopups')
expect(preferences).not.toHaveProperty('preloadURL')
// Anything main did not pin is dropped; zoom still follows the host window.
expect(preferences).not.toHaveProperty('experimentalFeatures')
expect(preferences).not.toHaveProperty('enableWebSQL')
expect(preferences.zoomFactor).toBe(1.25)
// Markup's arguments are replaced by main's renderer pins, never merged.
expect(preferences.additionalArguments).toEqual([
'--blink-settings=dnsPrefetchingEnabled=false'
])
expect(String(preferences.preload)).toMatch(/plugin-frame-guest-preload\.js$/)
expect(preferences).toMatchObject({
partition,
sandbox: true,
contextIsolation: true,
nodeIntegration: false,
nodeIntegrationInSubFrames: false,
webSecurity: true,
allowRunningInsecureContent: false,
webviewTag: false,
disablePopups: true,
disableDialogs: true
})
expect(mocks.isAllowedPartition).not.toHaveBeenCalled()
})
it('gives attached plugin guests their own policy, never the browser guest one', () => {
const { handlers } = installOnFakeWindow()
const guest = { session: 'plugin-session' }
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the handler reads only guest.session.
handlers['did-attach-webview']?.({} as never, guest as never)
expect(mocks.installPluginFramePolicies).toHaveBeenCalledWith(guest, expect.anything())
expect(mocks.attachGuestPolicies).not.toHaveBeenCalled()
expect(mocks.installJavaScriptDialogs).not.toHaveBeenCalled()
})
})