test: remove wrapper replays and test fixture inventories (#33745)

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
OrcaWin
2026-10-25 20:09:25 -07:00
committed by GitHub
co-authored by OrcaWin
parent 19fecada22
commit f01a97201b
85 changed files with 21 additions and 3351 deletions
+1 -6
View File
@@ -3,7 +3,6 @@
# Prune after a fix: pnpm check:flaky-test-pattern-ratchet --prune
1 test-no-polling-loop:config/scripts/happy-dom-mutation-observer-retention.test.ts
1 test-no-polling-loop:config/scripts/mobile-web-app/mobile-web-app-frame-budget-sweep.test.ts
1 test-no-polling-loop:mobile/src/transport/cellular-handshake-stall-real-socket.test.ts
1 test-no-polling-loop:mobile/src/transport/rpc-client-live-recovery.test.ts
1 test-no-polling-loop:src/main/browser/cdp-ws-proxy.test.ts
1 test-no-polling-loop:src/main/browser/cookies/browser-cookie-import-concurrency.test.ts
@@ -164,7 +163,6 @@
1 test-no-real-time-wait:mobile/src/tasks/worktree-create-retry.test.ts
1 test-no-real-time-wait:mobile/src/terminal/terminal-web-document-mount.test.ts
1 test-no-real-time-wait:mobile/src/terminal/terminal-webview-tap-routing.test.ts
1 test-no-real-time-wait:mobile/src/transport/cellular-handshake-stall-real-socket.test.ts
1 test-no-real-time-wait:mobile/src/transport/mobile-relay-runtime-failover.test.ts
4 test-no-real-time-wait:mobile/src/transport/rpc-client-live-recovery.test.ts
1 test-no-real-time-wait:mobile/src/transport/rpc-operation-barrier.test.ts
@@ -481,7 +479,6 @@
5 test-no-real-time-wait:src/shared/fish-query-reply-child-stdin.node-pty.test.ts
1 test-no-real-time-wait:src/shared/git-branch-line-total-soft-deadline.test.ts
1 test-no-real-time-wait:src/shared/osc52-clipboard.test.ts
1 test-no-real-time-wait:src/shared/priority-semaphore.test.ts
4 test-no-real-time-wait:src/shared/pty-reply-echo-shapes.node-pty.test.ts
1 test-no-real-time-wait:src/shared/remote-runtime-outbound-admission.test.ts
2 test-no-real-time-wait:src/shared/remote-runtime/control/remote-runtime-shared-control-auth-refusal.test.ts
@@ -704,7 +701,6 @@
1 test-no-wall-clock:mobile/src/session/use-mobile-native-chat-message-send.test.ts
1 test-no-wall-clock:mobile/src/source-control/MobileGitHistoryList.test.tsx
1 test-no-wall-clock:mobile/src/tasks/worktree-create-launch-operation.test.ts
2 test-no-wall-clock:mobile/src/transport/cellular-handshake-stall-real-socket.test.ts
1 test-no-wall-clock:mobile/src/transport/host-store.test.ts
1 test-no-wall-clock:mobile/src/transport/mobile-relay-invite-director.test.ts
3 test-no-wall-clock:mobile/src/transport/mobile-relay-resume-director.test.ts
@@ -1179,10 +1175,9 @@
7 test-timeout-needs-reason:mobile/src/tasks/worktree-create-retry.test.ts
2 test-timeout-needs-reason:mobile/src/terminal/document/native-document-bundle.test.ts
1 test-timeout-needs-reason:mobile/src/test-support/rpc-recording/family-recordings.test.ts
2 test-timeout-needs-reason:mobile/src/test-support/rpc-recording/mutants/family-mutants.test.ts
1 test-timeout-needs-reason:mobile/src/test-support/rpc-recording/mutants/family-mutants.test.ts
1 test-timeout-needs-reason:mobile/src/test-support/rpc-recording/recorded-request-params.test.ts
1 test-timeout-needs-reason:mobile/src/test-support/rpc-recording/rpc-recording-through-bridge.test.ts
1 test-timeout-needs-reason:mobile/src/transport/cellular-handshake-stall-real-socket.test.ts
1 test-timeout-needs-reason:mobile/src/transport/mobile-web-bundle-host-slow-link.test.ts
1 test-timeout-needs-reason:mobile/src/transport/rpc-client-live-recovery.test.ts
3 test-timeout-needs-reason:mobile/src/transport/rpc-client-reconnect-wait.test.ts
@@ -193,11 +193,6 @@ describe('resolveMobileNativeChat', () => {
})
).toBeNull()
})
it('canShowMobileNativeChat mirrors resolution', () => {
expect(canShowMobileNativeChat({ type: 'terminal', launchAgent: 'claude' })).toBe(true)
expect(canShowMobileNativeChat(null)).toBe(false)
})
})
it('resolves folder readability from the serving host catalog and rejects Model-A SSH', () => {
@@ -80,19 +80,6 @@ describe('appendMobileNativeChatPending ordinals for repeated sends', () => {
)
).toEqual(['p2'])
})
it('still counts a single-line repeat, which never regressed', () => {
const baseline = [userTurn('m1', 'unrelated')]
const both = appendMobileNativeChatPending(
appendMobileNativeChatPending({}, KEY, 'p1', sendOrigin('ping', baseline), 'ping'),
KEY,
'p2',
sendOrigin('ping', baseline),
'ping'
)
expect(both[KEY]?.map((item) => item.expectedOccurrence)).toEqual([1, 2])
})
})
describe('mobile pending echoes whose text normalizes to nothing', () => {
@@ -129,13 +116,4 @@ describe('mobile pending echoes whose text normalizes to nothing', () => {
expect(both[KEY]?.map((item) => item.expectedOccurrence)).toEqual([1, 2])
})
it('records an image echo, whose text is empty by design', () => {
const pending = appendMobileNativeChatPending({}, KEY, 'p1', sendOrigin('', []), '', [
'file:///photo.jpg'
])
expect(pending[KEY]).toHaveLength(1)
expect(pending[KEY]?.[0]?.expectedOccurrence).toBe(1)
})
})
@@ -114,16 +114,6 @@ describe('selectGluedPendingIds', () => {
expect(retiredIds(messages, pending)).toEqual([])
})
it('retires three collapsed prompts in one row', () => {
const messages = [assistantTurn('m1', 'ready', 1000), userTurn('m2', 'one two three', 5000)]
const pending = [
pendingSend('p1', 'one', 'm1'),
pendingSend('p2', 'two', 'm1'),
pendingSend('p3', 'three', 'm1')
]
expect(retiredIds(messages, pending)).toEqual(['p1', 'p2', 'p3'])
})
it('matches across tab, newline and repeated-space separators', () => {
const messages = [
assistantTurn('m1', 'ready', 1000),
@@ -136,18 +126,6 @@ describe('selectGluedPendingIds', () => {
expect(retiredIds(messages, pending)).toEqual(['p1', 'p2'])
})
it('matches prompts that themselves contain the separator', () => {
const messages = [
assistantTurn('m1', 'ready', 1000),
userTurn('m2', 'fix the bug and run the tests', 5000)
]
const pending = [
pendingSend('p1', 'fix the bug', 'm1'),
pendingSend('p2', 'and run the tests', 'm1')
]
expect(retiredIds(messages, pending)).toEqual(['p1', 'p2'])
})
it('matches when one pending is a strict prefix of the next', () => {
const messages = [assistantTurn('m1', 'ready', 1000), userTurn('m2', 'run run the tests', 5000)]
const pending = [pendingSend('p1', 'run', 'm1'), pendingSend('p2', 'run the tests', 'm1')]
@@ -195,12 +173,6 @@ describe('selectGluedPendingIds', () => {
expect(retiredIds(messages, pending)).toEqual([])
})
it('never lets an older turn retire sends captured after it', () => {
const messages = [userTurn('m1', 'fix the bug', 1000), assistantTurn('m2', 'fixed', 1100)]
const pending = [pendingSend('p1', 'fix the', 'm2'), pendingSend('p2', 'bug', 'm2')]
expect(retiredIds(messages, pending)).toEqual([])
})
it('rejects a row the pending run only prefixes', () => {
const messages = [
assistantTurn('m1', 'ready', 1000),
@@ -403,11 +375,4 @@ describe('retireLandedMobileNativeChatPending on a PTY-typed send', () => {
expect(retireLandedMobileNativeChatPending(messages, pending, NO_IMAGE_ECHOES)).toEqual([])
})
it('still holds the echo when no row carries the text at all', () => {
const messages = [assistantTurn('m1', 'ready', 1000)]
const pending = [pendingSend('p1', COMPOSER_TEXT, 'm1')]
expect(retireLandedMobileNativeChatPending(messages, pending, NO_IMAGE_ECHOES)).toEqual(pending)
})
})
@@ -1,44 +0,0 @@
import { describe, expect, it } from 'vitest'
import { compiledOperationModule } from './compiled-operation-module'
describe('compiled operation code', () => {
it('reuses identical code but separates source, exposure and diagnostic filename', () => {
const source = 'const value: number = 1; export const read = () => value'
const original = compiledOperationModule('original.ts', source, '')
expect(compiledOperationModule('original.ts', source, '')).toBe(original)
expect(compiledOperationModule('original.ts', source.replace('= 1', '= 2'), '')).not.toBe(
original
)
expect(compiledOperationModule('original.ts', source, '\nexports.extra = value')).not.toBe(
original
)
expect(compiledOperationModule('different.ts', source, '')).not.toBe(original)
expect(compiledOperationModule('original.ts', source, '')).toBe(original)
})
it('bounds retained code and still evaluates an evicted module correctly', () => {
const source = 'export const value = 42'
const original = compiledOperationModule('evicted.ts', source, '')
for (let index = 0; index < 512; index++) {
compiledOperationModule(`bounded-${index}.ts`, 'export {}', '')
}
const recompiled = compiledOperationModule('evicted.ts', source, '')
expect(recompiled).not.toBe(original)
const exports = {}
recompiled(() => undefined, exports)
expect(exports).toEqual({ value: 42 })
})
it('bounds retained variants of one file, not only the number of filenames', () => {
const source = 'export const value = 42'
const original = compiledOperationModule('variants.ts', source, '')
for (let index = 0; index < 512; index++) {
compiledOperationModule('variants.ts', `${source}; export const variant = ${index}`, '')
}
const recompiled = compiledOperationModule('variants.ts', source, '')
expect(recompiled).not.toBe(original)
const exports = {}
recompiled(() => undefined, exports)
expect(exports).toEqual({ value: 42 })
})
})
@@ -1,7 +1,5 @@
import { resolve } from 'node:path'
import { describe, expect, it } from 'vitest'
import { declaredDeviceSubstitutes } from './declared-device-state'
import { nativeMountingSubstitutes } from './native-mounting-substitutes'
import { pilotMountAdapters } from './pilot-mount-adapters'
import { runRecording } from './run-recording'
import { readScenarios } from './scenario-input'
@@ -15,23 +13,6 @@ const manifest = readScenarios(
resolve(root, 'mobile/rpc-foundation/pilot-scenarios.json')
).scenarios
const STORE = '@react-native-async-storage/async-storage'
function store(declared: Record<string, string>): {
module: Record<string, (...args: unknown[]) => Promise<unknown>>
effects: { name: string; value: unknown }[]
} {
const effects: { name: string; value: unknown }[] = []
const device = declaredDeviceSubstitutes({ deviceStore: declared })
device.bind((name, value) => effects.push({ name, value }))
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the declared store answers every member; the test asserts what each one does.
const module = device.substitutes.get(STORE) as Record<
string,
(...args: unknown[]) => Promise<unknown>
>
return { module, effects }
}
function scenario(id: string): RecordingScenario {
const found = manifest.find((candidate) => candidate.id === id)
if (!found) {
@@ -65,57 +46,6 @@ function sentParams(recording: Recording, index = 0): unknown {
}
describe('a scenario that declares its device', () => {
it('leaves the refusing store in place when nothing is declared', () => {
const undeclared = declaredDeviceSubstitutes({})
expect(undeclared.substitutes.size).toBe(0)
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the table's default store answers every member with a throwing call.
const fallback = nativeMountingSubstitutes().get(STORE) as { getItem: () => unknown }
expect(() => fallback.getItem()).toThrow('Native store reached during recording')
})
it('reads the declared entry, and null for everything else', async () => {
const { module } = store({ 'orca:key': 'declared' })
await expect(module.getItem!('orca:key')).resolves.toBe('declared')
await expect(module.getItem!('orca:other')).resolves.toBeNull()
})
it('records a write as an effect without letting a read see it', async () => {
const { module, effects } = store({})
await module.setItem!('orca:key', 'written')
await module.removeItem!('orca:key')
await expect(module.getItem!('orca:key')).resolves.toBeNull()
expect(effects).toEqual([
{ name: 'device-store.setItem', value: { key: 'orca:key', value: 'written' } },
{ name: 'device-store.removeItem', value: { key: 'orca:key' } }
])
})
it('refuses a member the declaration does not back', () => {
const { module } = store({})
expect(() => module.multiGet!('orca:key')).toThrow(
`Native store reached during recording: ${STORE}.multiGet`
)
})
it('answers a declared tray, and records a dismissal as an effect', async () => {
const entry = { request: { identifier: 'tray-1', content: { data: { a: 1 } } } }
const effects: { name: string; value: unknown }[] = []
const device = declaredDeviceSubstitutes({ deviceState: { notificationTray: [entry] } })
device.bind((name, value) => effects.push({ name, value }))
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the declared tray lists exactly these two members.
const module = device.substitutes.get('expo-notifications') as {
getPresentedNotificationsAsync: () => Promise<unknown[]>
dismissNotificationAsync: (identifier: string) => Promise<void>
}
const presented = await module.getPresentedNotificationsAsync()
expect(presented).toEqual([entry])
expect(presented[0]).not.toBe(entry)
await module.dismissNotificationAsync('tray-1')
expect(effects).toEqual([
{ name: 'notification-tray.dismiss', value: { identifier: 'tray-1' } }
])
})
it('backs a recorded read: the declared last-visited repo is the one the dialog selects', async () => {
const recording = await record('new-workspace-repositories-fulfilled')
expect(sentParams(recording)).toEqual({ name: 'repo.list#1', params: { $rpc: 'absent' } })
@@ -2,7 +2,6 @@ import { resolve } from 'node:path'
import { describe, expect, it } from 'vitest'
import { readScenarios } from './scenario-input'
import { familyGoldens } from './derived-goldens'
import { REPLY_MATRIX_NORMAL_RESULT_INVENTORY } from './reply-matrix-normal-result'
import { runRecording } from './run-recording'
import { pilotMountAdapters } from './pilot-mount-adapters'
import { vitestRecordingScheduler } from './vitest-recording-scheduler'
@@ -47,20 +46,6 @@ async function certify(id: string, scenarios: RecordingScenario[]) {
describe('family reply partitions and owned schedules', () => {
const goldens = familyGoldens(input.scenarios)
const families = [...new Set(input.scenarios.map((scenario) => scenario.family))]
// Count sites that actually generate tests so skipped families cannot hide behind the matrix-site reader.
const sites = goldens.flatMap((golden) => (golden.site ? [golden] : []))
it('matrices every family in the manifest', () => {
expect([...new Set(sites.map((golden) => golden.family))]).toEqual(families)
})
// The inventory is only consulted for a live site, so a stale entry would retire silently.
it('lists only live matrix sites in the normal-result inventory', () => {
const live = new Set(sites.map((golden) => `${golden.family}\0${golden.site}`))
const stale = REPLY_MATRIX_NORMAL_RESULT_INVENTORY.filter(
(entry) => !live.has(`${entry.family}\0${entry.request}`)
).map((entry) => `${entry.family} ${entry.request}`)
expect(stale).toEqual([])
})
for (const golden of goldens) {
it(
golden.title,
@@ -1,65 +0,0 @@
import { resolve } from 'node:path'
import { expect, it } from 'vitest'
import { familyGoldens } from './derived-goldens'
import { hoistPreludeCheckpoints } from './prelude-checkpoints'
import { readScenarios } from './scenario-input'
import { bindCompletions, lifecycleSchedules } from './schedule-driver'
it('omits only inventory admission following a refusal to a retired owner', () => {
const manifest = readScenarios(
resolve(import.meta.dirname, '../../../rpc-foundation/pilot-scenarios.json')
)
const base = manifest.scenarios.find((scenario) => scenario.id === 'inventory-lifecycle')
if (!base) {
throw new Error('missing inventory base scenario')
}
const golden = familyGoldens(manifest.scenarios).find(
(item) => item.id === 'lifecycle-inventory-lifecycle'
)
if (!golden) {
throw new Error('missing inventory lifecycle golden')
}
const original = hoistPreludeCheckpoints(
{ ...base, steps: bindCompletions(base.steps) },
(['reset', 'unmount', 'blur'] as const).flatMap((action) => lifecycleSchedules(base, action))
)
const derived = golden.scenarios()
expect(derived.map((scenario) => scenario.id)).toEqual(original.map((scenario) => scenario.id))
expect(derived).toHaveLength(12)
for (const [index, scenario] of original.entries()) {
const unmounted = scenario.id === 'inventory-lifecycle.unmount-before-1'
if (!unmounted && scenario.id !== 'inventory-lifecycle.reset-before-1') {
expect(derived[index]).toEqual(scenario)
expect(JSON.stringify(derived[index])).toBe(JSON.stringify(scenario))
continue
}
expect(scenario.steps).toHaveLength(unmounted ? 13 : 11)
expect(scenario.steps.slice(7, 10)).toEqual([
{ bind: 'old-inventory', request: 'files.list#1', params: { worktree: 'id:A' } },
{ bind: 'lifecycle-old-inventory', request: 'old-inventory', params: { worktree: 'id:A' } },
{
complete: 'lifecycle-old-inventory',
params: { worktree: 'id:A' },
reply: { ok: true, result: { files: [{ relativePath: 'old.ts' }] } }
}
])
const expected = {
...scenario,
steps: [...scenario.steps.slice(0, 7), ...scenario.steps.slice(10)]
}
expect(derived[index]).toEqual(expected)
expect(JSON.stringify(derived[index])).toBe(JSON.stringify(expected))
expect(expected.steps.filter((step) => 'checkpoint' in step)).toEqual([
{ checkpoint: 'lifecycle-boundary' },
{ checkpoint: 'settled' },
...(unmounted ? [{ checkpoint: 'remounted' }] : [])
])
expect(expected.steps.filter((step) => 'bind' in step)).toEqual([
{
bind: 'lifecycle-files.searchPaths#1',
request: 'files.searchPaths#1',
params: { worktree: 'id:A', query: 'old', limit: 16 }
}
])
}
})
@@ -1,12 +1,7 @@
import { resolve } from 'node:path'
import { createElement } from 'react'
import { describe, expect, it } from 'vitest'
import {
hookScreenMount,
projectMountedScreen,
renderedElementProps,
screenMount
} from './mounted-screen-tree'
import { hookScreenMount, projectMountedScreen, screenMount } from './mounted-screen-tree'
import { pilotMountAdapters } from './pilot-mount-adapters'
import { runRecording } from './run-recording'
import { readScenarios } from './scenario-input'
@@ -42,36 +37,6 @@ describe('a mounted screen', () => {
})
})
it('projects the elements, copy and labels of what rendered', () => {
const screen = screenMount(
() =>
createElement(
'View',
null,
createElement('Text', { accessibilityLabel: 'title' }, 'Files'),
createElement('Text', null, 'orca-files')
),
effectSink().effect
)
screen.mount()
expect(projectMountedScreen(screen)).toEqual({
elements: { View: 1, Text: 2 },
text: ['Files', 'orca-files'],
labels: ['title'],
crash: null
})
})
it('reads the props an inert element was handed, which is all a list ever renders', () => {
const screen = screenMount(
() => createElement('View', null, createElement('FlatList', { data: [{ id: 'row-1' }] })),
effectSink().effect
)
screen.mount()
expect(renderedElementProps(screen.tree(), 'FlatList')).toEqual([{ data: [{ id: 'row-1' }] }])
expect(renderedElementProps(screen.tree(), 'SectionList')).toEqual([])
})
// Hook projections omit crashes; the effect sink must record them independently.
it('reports a crash through the effect sink with no adapter cooperation', () => {
const sink = effectSink()
@@ -17,20 +17,18 @@ const input = readScenarios(
const goldens = process.env.RPC_FOUNDATION_GOLDENS ?? resolve(root, 'mobile/rpc-foundation/goldens')
// Compare the whole variant: malformed-container mutants may differ only at settlement and recover to identical final state.
// Pin both a killing and a blind partition; row-shape requirements remain unit coverage because matrices vary envelopes only.
// Row-shape requirements remain unit coverage because matrices vary envelopes only.
const MATRIX_MUTANTS: readonly {
mutation: Mutation
family: string
site: string
killedBy: string
blindTo: string
}[] = [
{
mutation: 'linear-status-nullable',
family: 'tasks.provider-load',
site: 'linear.status#1',
killedBy: 'result-null',
blindTo: 'normal'
killedBy: 'result-null'
}
]
@@ -78,8 +76,5 @@ describe('reply-matrix partitions hold the container requirements', () => {
it(`${entry.family} ${entry.site}: ${entry.killedBy} kills ${entry.mutation}`, async () => {
expect(await verdict(entry, entry.killedBy)).toBe('killed')
}, 30_000)
it(`${entry.family} ${entry.site}: ${entry.blindTo} cannot see ${entry.mutation}`, async () => {
expect(await verdict(entry, entry.blindTo)).toBe('survived')
}, 30_000)
}
})
@@ -1,82 +0,0 @@
import { existsSync, readFileSync, readdirSync } from 'node:fs'
import { dirname, join, resolve, sep } from 'node:path'
import { describe, expect, it } from 'vitest'
import { RECORDING_DRIVERS } from '../recording-drivers'
const root = resolve(import.meta.dirname, '../../../../..')
const recorder = join(root, 'mobile/src/test-support/rpc-recording')
const mutants = join(recorder, 'mutants')
/** Names the drivers for the record script and the suites that check them; no recording loads it. */
const OFF_RECORDING_PATH = ['recording-drivers.ts']
function sources(directory: string): string[] {
return readdirSync(directory, { withFileTypes: true, recursive: true })
.filter((entry) => entry.isFile() && entry.name.endsWith('.ts'))
.map((entry) => join(entry.parentPath, entry.name))
}
function resolved(from: string, specifier: string): string | undefined {
const base = resolve(dirname(from), specifier)
return ['', '.ts', '.tsx', '/index.ts']
.map((suffix) => base + suffix)
.find((candidate) => existsSync(candidate) && /\.tsx?$/.test(candidate))
}
function relative(file: string): string {
return file.slice(recorder.length + 1)
}
/** A suite that records nothing: excluded below, since no driver has reason to reach it. */
function suite(file: string): boolean {
return file.endsWith('.test.ts') && !RECORDING_DRIVERS.some((driver) => file.endsWith(driver))
}
// Include type-position imports too; over-approximating reachability only strengthens the forbidden mutant boundary.
function reachable(entries: readonly string[]): Set<string> {
const seen = new Set<string>()
const pending = [...entries]
while (pending.length > 0) {
const file = pending.pop()!
if (seen.has(file)) {
continue
}
seen.add(file)
for (const match of readFileSync(file, 'utf8').matchAll(/(?:from|import\()\s*'(\.[^']*)'/g)) {
const target = resolved(file, match[1]!)
if (target) {
pending.push(target)
}
}
}
return seen
}
// Walk outward from recording drivers so no recorded byte can depend on mutant code; scan names for non-import reads too.
describe('the mutant seam', () => {
const outside = sources(recorder).filter((file) => !file.startsWith(`${mutants}${sep}`))
it('is unreachable from every recording driver', () => {
const graph = reachable(RECORDING_DRIVERS.map((driver) => join(recorder, driver)))
const reached = [...graph]
.filter((file) => file.startsWith(`${mutants}${sep}`))
.map(relative)
.sort()
expect(reached).toEqual([])
// Require every recorder module to be reachable so an empty graph cannot pass.
const missed = outside
.filter((file) => !suite(file) && !graph.has(file))
.filter((file) => !OFF_RECORDING_PATH.includes(relative(file)))
.map(relative)
.sort()
expect(missed).toEqual([])
expect(sources(mutants).length).toBeGreaterThan(1)
})
// Apply the engine boundary to recording drivers; other tests cannot alter recorded bytes.
it('is named in no recording file', () => {
const naming = outside
.filter((file) => !suite(file) && readFileSync(file, 'utf8').includes('mutants'))
.map(relative)
expect(naming).toEqual([])
})
})
@@ -3,7 +3,7 @@ import { describe, expect, it } from 'vitest'
import { pilotGoldens } from '../derived-goldens'
import { readGolden } from '../golden-recording'
import { pilotMountAdapters } from '../pilot-mount-adapters'
import { runRecording, runRecordingMutant } from '../run-recording'
import { runRecordingMutant } from '../run-recording'
import { readScenarios } from '../scenario-input'
import { vitestRecordingScheduler } from '../vitest-recording-scheduler'
import { operationMutation, type Mutation } from './operation-mutations'
@@ -42,26 +42,6 @@ const mutants: Record<string, Mutation> = {
'worktree-catalog-snapshot-unreadable': 'worktree-catalog-unchecked-reader',
'mobile-web-bundle-fetch-paged': 'mobile-web-bundle-chunk-placement'
}
// Pin archived defect states so unrelated reference refactors cannot pass merely by differing.
const referenceStates: Record<string, RecordedValue> = {
b1: { files: [] },
b2: {
error: '',
mutating: false,
row: {
content: {
assignees: [],
labels: [{ color: '808080', name: 'recorded' }],
number: 1,
repository: 'owner/repo'
},
id: 'item-1',
itemType: 'ISSUE'
}
},
b3: { error: 'issue refused', loading: false, payload: { $rpc: 'null' } }
}
function visibleState(recording: Recording): RecordedValue {
return recording.checkpoints.at(-1)!.observation.state
}
@@ -71,11 +51,6 @@ const mutantPilots = pilots.flatMap((pilot) => {
const mutation = mutants[pilot.id]
return mutation ? [{ ...pilot, mutation }] : []
})
const referencePilots = pilots.flatMap((pilot) => {
const reference = referenceStates[pilot.id]
return reference ? [{ ...pilot, reference }] : []
})
describe('RPC main recording mutants', () => {
for (const { id, scenario, mutation } of mutantPilots) {
it(`${id}: kills ${mutation}`, async () => {
@@ -94,19 +69,4 @@ describe('RPC main recording mutants', () => {
expect(result.verdict).toBe('killed')
})
}
for (const { id, scenario, reference } of referencePilots) {
it.skipIf(!process.env.RPC_FOUNDATION_REFERENCE_ROOT)(`${id}: rejects bcba08b3e4`, async () => {
const { adapters } = pilotMountAdapters(process.env.RPC_FOUNDATION_REFERENCE_ROOT!, {
device: scenario,
reference: true
})
const result = await runRecording(
scenario,
adapters[scenario.operation],
vitestRecordingScheduler()
)
expect(visibleState(result)).toEqual(reference)
expect(reference).not.toEqual(visibleState(readGolden(goldens, id).recording))
})
}
})
@@ -14,7 +14,7 @@ const input = readScenarios(
)
const goldens = process.env.RPC_FOUNDATION_GOLDENS ?? resolve(root, 'mobile/rpc-foundation/goldens')
// Prove each probe closes a surviving mutation and that earlier scenarios stayed blind.
// Prove each probe closes a surviving mutation.
// Empty session inputs exercise omission guards that always-filled fixtures could not reach.
const HOLES: readonly { mutation: Mutation; operation: string; closedBy: readonly string[] }[] = [
{
@@ -96,38 +96,11 @@ async function verdict(id: string, mutation: Mutation): Promise<string> {
}
describe('probe scenarios close holes the pre-probe recordings left open', () => {
// Every completion needs a later checkpoint so a params-mismatch abort always suppresses an observation the golden actually holds.
it('never completes a request after the last checkpoint of a scenario', () => {
const trailing = input.scenarios
.filter((scenario) => {
let lastComplete = -1
let lastCheckpoint = -1
scenario.steps.forEach((step, index) => {
if ('complete' in step) {
lastComplete = index
}
// A step may carry both, and then the checkpoint records what the completion produced.
if ('checkpoint' in step) {
lastCheckpoint = index
}
})
return lastComplete > lastCheckpoint
})
.map((scenario) => scenario.id)
expect(trailing).toEqual([])
})
for (const hole of HOLES) {
const family = input.scenarios.filter((scenario) => scenario.operation === hole.operation)
for (const id of hole.closedBy) {
it(`${id} kills ${hole.mutation}`, async () => {
expect(await verdict(id, hole.mutation)).toBe('killed')
})
}
for (const scenario of family.filter(({ id }) => !hole.closedBy.includes(id))) {
it(`${scenario.id} cannot see ${hole.mutation}`, async () => {
expect(await verdict(scenario.id, hole.mutation)).toBe('survived')
})
}
}
})
@@ -26,13 +26,6 @@ describe('nativeMountingSubstitutes', () => {
)
})
it('leaves the interop marker undefined so a default import binds the module, not the trap', () => {
for (const name of ['@react-native-async-storage/async-storage', 'expo-crypto']) {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: reads the marker the emit reads, off a proxy with no declared shape.
expect((substitute(name) as { __esModule?: unknown }).__esModule).toBeUndefined()
}
})
it('throws on a member nobody substituted rather than recording an undefined native API', () => {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the read itself is the assertion; the proxy has no declared shape.
expect(() => (substitute('expo-crypto') as { randomUUID?: unknown }).randomUUID).toThrow(
@@ -1,5 +1,4 @@
import { mkdtempSync, mkdirSync, rmSync, writeFileSync, existsSync } from 'node:fs'
import type * as FsModule from 'node:fs'
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, expect, it, vi } from 'vitest'
@@ -7,11 +6,6 @@ import { nativeMountingSubstitutes } from './native-mounting-substitutes'
import type * as NativeSubstitutesModule from './native-mounting-substitutes'
import { operationModuleLoader } from './operation-module-loader'
vi.mock('node:fs', async (original) => {
const fs = await original<typeof FsModule>()
return { ...fs, existsSync: vi.fn(fs.existsSync) }
})
vi.mock('./native-mounting-substitutes', async (original) => {
const module = await original<typeof NativeSubstitutesModule>()
return { ...module, nativeMountingSubstitutes: vi.fn(module.nativeMountingSubstitutes) }
@@ -36,22 +30,6 @@ afterEach(() => {
vi.clearAllMocks()
})
it('resolves a relative import once instead of probing disk on every exported member read', () => {
const root = sourceTree({
'value.ts': 'export const value = 42',
'operation.ts': `import { value } from './value'; export const read = () => value`
})
const operation = operationModuleLoader(root).load<{ read: () => number }>(
'mobile/src/operation.ts'
)
expect(operation.read()).toBe(42)
vi.mocked(existsSync).mockClear()
for (let index = 0; index < 1_000; index++) {
expect(operation.read()).toBe(42)
}
expect(existsSync).not.toHaveBeenCalled()
})
it('builds device substitutes only for a used loader and keeps their state per loader', async () => {
const root = sourceTree({
'operation.ts': `
@@ -21,7 +21,6 @@ import {
import { C5_PAGE_CLOSURE } from '../bridged-parity/c5-page-closure'
import { C6_BROWSER_CLOSURE_FAMILIES } from '../bridged-parity/c6-browser-closure-families'
import { C2_PAGE_CLOSURE } from '../bridged-parity/c2-page-closure'
import { C1_PAGE_CLOSURE } from '../bridged-parity/c1-page-closure'
import { C3_PAGE_CLOSURE } from '../bridged-parity/c3-page-closure'
import {
pageClosureDrift,
@@ -303,16 +302,6 @@ describe.skipIf(process.env[BRIDGED_PARITY_FLAG] === BRIDGED_PARITY_OFF)(
expect({ tally: bridgedParityTallyDrift({ identical, counts }) }).toEqual({ tally: [] })
})
it('gives every golden the C1 page closure records the verdict it is pinned to', () => {
process.stdout.write(readPageClosure('C1', C1_PAGE_CLOSURE, observed))
// Pin every closure id; aggregate corpus totals can trade a domain regression for an unrelated improvement.
expect({ closure: pageClosureDrift(C1_PAGE_CLOSURE, observed) }).toEqual({ closure: [] })
// Check run totals independently because C1’s table has no separate class-count pins.
expect(pageClosureRunTotals(C1_PAGE_CLOSURE, observed)).toEqual(
pageClosureTotals(C1_PAGE_CLOSURE)
)
})
it('gives every golden the C5 page closure records the verdict it is pinned to', () => {
process.stdout.write(readPageClosure('C5', C5_PAGE_CLOSURE, observed))
// Recheck inherited C1 families alongside C5’s additions so shared regressions fail both closures.
@@ -1,9 +1,8 @@
import { createElement, type ElementType } from 'react'
import { act, create } from 'react-test-renderer'
import { describe, expect, it } from 'vitest'
import { inertIconModule } from './inert-native-elements'
import { nativeMountingSubstitutes } from './native-mounting-substitutes'
import { reactNativeScreenMembers, screenNativeSubstitutes } from './screen-native-substitutes'
import { reactNativeScreenMembers } from './screen-native-substitutes'
function member(module: string, name: string): unknown {
const substitute = nativeMountingSubstitutes().get(module)
@@ -68,44 +67,4 @@ describe('the inert screen substitutes', () => {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a rendered host node carries exactly these fields.
expect((tree as { children: unknown }).children).toBeNull()
})
it('pins the device inputs a screen reads instead of measuring them', () => {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the member is a hook by construction.
expect((member('react-native', 'useWindowDimensions') as () => unknown)()).toEqual({
width: 390,
height: 844
})
expect(member('react-native', 'StyleSheet')).toMatchObject({ hairlineWidth: 1 })
})
it('keeps a style sheet readable so a screen that reads one style sees it', () => {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the member is the substituted StyleSheet by construction.
const sheet = member('react-native', 'StyleSheet') as {
create: (value: unknown) => unknown
flatten: (value: unknown) => unknown
}
expect(sheet.create({ row: { flex: 1 } })).toEqual({ row: { flex: 1 } })
expect(sheet.flatten([{ flex: 1 }, [{ gap: 2 }]])).toEqual({ flex: 1, gap: 2 })
})
it('answers any icon name with its own element, because every export here is an icon', () => {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the icon module answers every string key.
const icons = inertIconModule() as Record<string, ElementType>
expect(icons.ChevronLeft).toBe(icons.ChevronLeft)
act(() => {
create(createElement(icons.ChevronLeft!, { size: 20 }))
})
})
it('throws on a member nobody listed, in every screen package', () => {
for (const [module, substitute] of screenNativeSubstitutes()) {
if (module === 'lucide-react-native') {
continue
}
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the read is the assertion; a substitute proxy has no declared shape.
expect(() => (substitute as Record<string, unknown>).notASubstitutedMember).toThrow(
`Unsubstituted native member: ${module}.notASubstitutedMember`
)
}
})
})
@@ -32,13 +32,4 @@ describe('a wrapper between the operation and the scripted transport', () => {
expect(inner).not.toBeNull()
expect(() => inner?.('alpha.one')).toThrow(/cannot take the name \(no logical request\)/)
})
it('names each physical send after the logical call that made it', async () => {
const transport = new ScriptedRpcTransport()
void transport.client.sendRequest('alpha.one')
void transport.client.sendRequest('beta.two')
await Promise.resolve()
await Promise.resolve()
expect(transport.payloads.map((payload) => payload.name)).toEqual(['alpha.one#1', 'beta.two#1'])
})
})
@@ -2,8 +2,6 @@ import { describe, expect, it } from 'vitest'
import { RpcClientStreamRegistry } from '../../transport/rpc-client-stream-registry'
import { ScriptedRpcTransport } from './scripted-rpc-transport'
import { vitestRecordingScheduler } from './vitest-recording-scheduler'
import { driveReplyMatrix, replyMatrixGoldenId, replyMatrixSites } from './reply-matrix'
import { replyMatrixNormalResult } from './reply-matrix-normal-result'
import { runRecording } from './run-recording'
import type { RpcClient } from '../../transport/rpc-client'
import type { RecordingScenario } from './recording-scenario'
@@ -229,56 +227,6 @@ describe('subscription recordings', () => {
// Due-timer cleanup must match synchronous cleanup rather than appear as an uncancelled stream.
expect(await drive('timer')).toEqual([])
})
it('drives the reply matrix over frames, and matrixes a family that only subscribes', () => {
const changed = { ok: true, streaming: true, result: { type: 'worktreesChanged' } }
const base: RecordingScenario = {
id: 'stream',
operation: 'op',
family: 'op',
sites: [],
schedules: [],
steps: [
{ action: 'mount', id: 'mount' },
{ frame: `${CLIENT_EVENTS}#1`, params: null, reply: readyFrame('sub-1') },
{ checkpoint: 'ready' },
{ frame: `${CLIENT_EVENTS}#1`, params: null, reply: changed },
{ checkpoint: 'changed' }
]
}
expect(replyMatrixSites(base)).toEqual([`${CLIENT_EVENTS}#1@1`, `${CLIENT_EVENTS}#1@2`])
expect(replyMatrixGoldenId('op', `${CLIENT_EVENTS}#1@2`)).toBe(
'matrix-op-runtime.clientevents.subscribe-1-2'
)
expect(replyMatrixNormalResult('op', [base], `${CLIENT_EVENTS}#1@2`)).toEqual(changed.result)
const variants = driveReplyMatrix(base, `${CLIENT_EVENTS}#1@1`, readyFrame('sub-1').result)
const partitions = variants.map((variant) => variant.id.replace('stream.', ''))
// Frames have no promise, so the two request-rejection partitions do not apply.
expect(partitions).toEqual([
'normal',
'result-absent',
'result-null',
'inner-ok-missing',
'inner-false-string-error',
'inner-false-object-error',
'outer-refused',
'outer-refused-no-message',
'method-not-found'
])
const replies = new Map(variants.map((variant) => [variant.id, variant.steps[1]]))
// Keep streaming:true on success controls so they route to the same stream.
expect(replies.get('stream.normal')).toEqual(base.steps[1])
expect(replies.get('stream.result-absent')).toEqual({
frame: `${CLIENT_EVENTS}#1`,
params: null,
reply: { ok: true, streaming: true }
})
expect(replies.get('stream.outer-refused')).toMatchObject({
reply: { ok: false, error: { code: 'refused' } }
})
// Frames stay required after divergence because the registry still routes them to the opening id.
expect(variants[0]!.steps[3]).toEqual(base.steps[3])
})
})
const CLIENT_EVENTS = 'runtime.clientEvents.subscribe'
@@ -1,111 +0,0 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { WebSocketServer, type WebSocket as ServerWebSocket } from 'ws'
import { connect } from './rpc-client'
import { classifyConnection, verdictDisplayLabel } from './connection-health'
// Guard fake-timer mock drift with real WebSocket upgrades and delayed E2EE replies (#10119).
// Opt in with ORCA_MOBILE_LIVE_REPRO=1; this needs real timers and roughly 22 seconds.
vi.mock('./e2ee', () => ({
generateKeyPair: () => ({ publicKey: new Uint8Array(32), secretKey: new Uint8Array(32) }),
deriveSharedKey: () => new Uint8Array(32),
publicKeyFromBase64: () => new Uint8Array(32),
publicKeyToBase64: () => 'client-public-key',
encrypt: (plaintext: string) => plaintext,
decrypt: (raw: string) => raw,
decryptBytes: (bytes: Uint8Array) => bytes
}))
const RUN_LIVE =
process.env.ORCA_MOBILE_LIVE_REPRO === '1' || !!process.env.ORCA_MOBILE_LIVE_REPRO_FULL
// Allow three handshake timeout cycles plus CI scheduling slack.
const OBSERVE_MS = 22_000
const SAMPLE_MS = 200
let server: WebSocketServer | null = null
const serverSockets: ServerWebSocket[] = []
// Upgrade succeeds but e2ee_ready arrives after the handshake budget.
async function startSlowHandshakeDesktop(replyAfterMs: number): Promise<number> {
const wss = new WebSocketServer({ host: '127.0.0.1', port: 0 })
server = wss
wss.on('connection', (socket) => {
serverSockets.push(socket)
socket.on('message', (data) => {
const text = data.toString()
if (!text.includes('e2ee_hello')) {
return
}
setTimeout(() => {
if (socket.readyState === socket.OPEN) {
socket.send(JSON.stringify({ type: 'e2ee_ready', publicKeyB64: 'server-public-key' }))
}
}, replyAfterMs)
})
})
await new Promise<void>((resolve) => wss.once('listening', resolve))
const address = wss.address()
if (typeof address === 'string' || address === null) {
throw new Error('expected a TCP address')
}
return address.port
}
describe.runIf(RUN_LIVE)('issue #10119 — real socket, handshake slower than the 5s budget', () => {
beforeEach(() => {
serverSockets.length = 0
vi.spyOn(console, 'log').mockImplementation(() => {})
})
afterEach(async () => {
vi.restoreAllMocks()
for (const socket of serverSockets) {
socket.terminate()
}
const wss = server
server = null
if (wss) {
await new Promise<void>((resolve) => wss.close(() => resolve()))
}
})
it('escalates and latches against a live desktop that answers 1s too late', async () => {
const port = await startSlowHandshakeDesktop(6_000)
const endpoint = `ws://127.0.0.1:${port}`
const client = connect(endpoint, 'device-token', 'server-public-key')
const labels: string[] = []
const states: string[] = []
let maxAttempts = 0
const started = Date.now()
while (Date.now() - started < OBSERVE_MS) {
const state = client.getState()
const attempts = client.getReconnectAttempt()
maxAttempts = Math.max(maxAttempts, attempts)
states.push(state)
labels.push(
verdictDisplayLabel(
classifyConnection({
state,
reconnectAttempts: attempts,
lastConnectedAt: client.getLastConnectedAt(),
endpoint
})
)
)
await new Promise((resolve) => setTimeout(resolve, SAMPLE_MS))
}
client.close()
expect(serverSockets.length).toBeGreaterThanOrEqual(2)
expect(states).toContain('handshaking')
expect(states).not.toContain('connected')
expect(maxAttempts).toBeGreaterThanOrEqual(3)
const firstEscalated = labels.findIndex((l) => l !== 'Connecting…' && l !== 'Reconnecting…')
expect(firstEscalated).toBeGreaterThan(0)
expect(labels.slice(firstEscalated).every((l) => l !== 'Connecting…')).toBe(true)
}, 60_000)
})
@@ -1,9 +1,4 @@
import { readFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { extname, join, relative, resolve } from 'node:path'
import ts from 'typescript-api'
import { describe, expect, it } from 'vitest'
import { censusSourceFiles } from '../test-support/census-source-files'
import {
GenerationScopedRequestOwner,
type LoadedRequest,
@@ -83,23 +78,6 @@ describe('key-reset-cleanup', () => {
})
})
describe('blur', () => {
it('refuses a reply that settles after a blur, without the scope having moved', async () => {
const owner: Owner = new GenerationScopedRequestOwner()
const scope = scopeAt('w1', 1)
const request = pending()
const loaded = owner.load(scope, QUERY, request.start)
// Blur retires the surface generation without changing host/workspace identity.
owner.reset()
request.resolve(['stale.ts'])
const lease = await settled(loaded)
expect(owner.commit(lease.lease, lease.value)).toBe('retired-generation')
expect(owner.read(scope, QUERY)).toBeUndefined()
})
})
describe('cutover', () => {
it('retires the whole scope when the logical authority epoch advances', async () => {
const owner: Owner = new GenerationScopedRequestOwner()
@@ -340,178 +318,3 @@ describe('currency probe', () => {
expect(owner.read(scope, QUERY)).toEqual(['sent.ts'])
})
})
const mobileRoot = fileURLToPath(new URL('../..', import.meta.url))
const ownerModule = join(mobileRoot, 'src', 'transport', 'generation-scoped-request-owner')
function importsOwner(path: string, source: string): boolean {
const parsed = ts.createSourceFile(path, source, ts.ScriptTarget.Latest, true)
return parsed.statements.some((statement) => {
const specifier = ts.isImportDeclaration(statement) ? statement.moduleSpecifier : undefined
return (
specifier !== undefined &&
ts.isStringLiteral(specifier) &&
specifier.text.startsWith('.') &&
resolve(path, '..', specifier.text) === ownerModule
)
})
}
function declaredInside(callback: ts.Node): Set<string> {
const names = new Set<string>()
const bind = (name: ts.BindingName): void => {
if (ts.isIdentifier(name)) {
names.add(name.text)
return
}
for (const element of name.elements) {
if (ts.isBindingElement(element)) {
bind(element.name)
}
}
}
const visit = (node: ts.Node): void => {
if (ts.isVariableDeclaration(node) || ts.isParameter(node)) {
bind(node.name)
}
if ((ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node)) && node.name) {
names.add(node.name.text)
}
if (ts.isCatchClause(node) && node.variableDeclaration) {
bind(node.variableDeclaration.name)
}
ts.forEachChild(node, visit)
}
ts.forEachChild(callback, visit)
return names
}
function assignmentRoot(target: ts.Expression): string | null {
let node: ts.Expression = target
for (;;) {
if (ts.isIdentifier(node)) {
return node.text
}
if (node.kind === ts.SyntaxKind.ThisKeyword) {
return 'this'
}
if (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) {
node = node.expression
continue
}
if (ts.isParenthesizedExpression(node) || ts.isNonNullExpression(node)) {
node = node.expression
continue
}
return null
}
}
function externalWrites(callback: ts.Node): string[] {
const declared = declaredInside(callback)
const offenders: string[] = []
const record = (target: ts.Expression): void => {
const root = assignmentRoot(target)
if (root !== null && !declared.has(root)) {
offenders.push(root)
}
}
const visit = (node: ts.Node): void => {
if (ts.isBinaryExpression(node) && node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment) {
if (node.operatorToken.kind <= ts.SyntaxKind.LastAssignment) {
record(node.left)
}
}
if (
(ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node)) &&
(node.operator === ts.SyntaxKind.PlusPlusToken ||
node.operator === ts.SyntaxKind.MinusMinusToken)
) {
record(node.operand)
}
ts.forEachChild(node, visit)
}
ts.forEachChild(callback, visit)
return offenders
}
function loaders(path: string, source: string): ts.Node[] {
const parsed = ts.createSourceFile(
path,
source,
ts.ScriptTarget.Latest,
true,
extname(path) === '.tsx' ? ts.ScriptKind.TSX : ts.ScriptKind.TS
)
const found: ts.Node[] = []
const visit = (node: ts.Node): void => {
if (
ts.isCallExpression(node) &&
ts.isPropertyAccessExpression(node.expression) &&
node.expression.name.text === 'load'
) {
const loader = node.arguments[2]
if (loader && (ts.isArrowFunction(loader) || ts.isFunctionExpression(loader))) {
found.push(loader)
}
}
ts.forEachChild(node, visit)
}
ts.forEachChild(parsed, visit)
return found
}
function loaderWrites(path: string, source: string): string[] {
return loaders(path, source).flatMap((loader) => externalWrites(loader))
}
describe('loader write fence', () => {
const holders = ['app', 'src']
.map((directory) => join(mobileRoot, directory))
.flatMap(censusSourceFiles)
.filter((path) => ['.ts', '.tsx'].includes(extname(path)))
.filter((path) => !/\.test\.tsx?$/.test(path))
.map((path) => ({ path, source: readFileSync(path, 'utf8') }))
.filter(({ path, source }) => importsOwner(path, source))
it('recognizes a loader that writes outside itself and leaves an honest one alone', () => {
const probe = join(mobileRoot, 'src', 'transport', 'probe.ts')
expect(
loaderWrites(probe, 'owner.load(scope, {}, async () => { cacheRef.current = await read() })')
).toEqual(['cacheRef'])
expect(loaderWrites(probe, 'owner.load(scope, {}, async () => { sequence++ })')).toEqual([
'sequence'
])
expect(
loaderWrites(probe, 'owner.load(scope, {}, async () => { this.paths = await read() })')
).toEqual(['this'])
expect(
loaderWrites(
probe,
'owner.load(scope, {}, async () => { const rows = await read(); return rows })'
)
).toEqual([])
})
it('has every owner holder loading without writing external state', () => {
// Require discovered owners/loaders so an empty scan cannot pass vacuously.
expect(
holders.map(({ path }) => relative(mobileRoot, path).split(/[/\\]/).join('/'))
).toContain('src/session/native-chat/composer/use-mobile-native-chat-file-search.ts')
expect(
holders.reduce((total, { path, source }) => total + loaders(path, source).length, 0)
).toBeGreaterThan(0)
const offenders = holders
.map(({ path, source }) => ({
file: relative(mobileRoot, path).split(/[/\\]/).join('/'),
writes: loaderWrites(path, source)
}))
.filter((entry) => entry.writes.length > 0)
.map((entry) => `${entry.file}: ${[...new Set(entry.writes)].sort().join(', ')}`)
.sort()
expect(
offenders,
'Return the value from the loader and publish it with commit(lease, value) instead.'
).toEqual([])
})
})
@@ -1,61 +0,0 @@
import { createElement } from 'react'
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
import { describe, expect, it, vi } from 'vitest'
import type { MobileRelayHost } from '../../../src/shared/mobile-relay-hosts-contract'
import type { RpcClient } from './rpc-client'
import type { ConnectionState } from './types'
import { stubRpcClient } from './rpc-client-test-stub'
const feed = vi.hoisted((): { hosts: readonly MobileRelayHost[] | null } => ({ hosts: null }))
const gatesConnStates = vi.hoisted((): ConnectionState[] => [])
vi.mock('../worktree/relay-hosts-feed', () => ({ useRelayHosts: () => feed.hosts }))
vi.mock('./execution-host-scoped-rpc-client', () => ({
workspaceRouteExecutionHost: (_client: unknown, _caps: unknown, host: string) => host,
scopeRpcClientToExecutionHost: (client: RpcClient) => client
}))
vi.mock('./host-status-gates', () => ({
useHostStatusGates: (args: { connState: ConnectionState }) => {
gatesConnStates.push(args.connState)
return {}
}
}))
const { useWorkspaceServer } = await import('./workspace-server')
const HOST = 'runtime:server-1' as const
function server(relay: MobileRelayHost['relay']): MobileRelayHost {
return { hostId: HOST, label: 'Server', health: 'available', relay }
}
describe('useWorkspaceServer', () => {
it("re-reads the server's status when the server reconnects under a connected desktop", async () => {
const client = stubRpcClient()
function Probe(): null {
useWorkspaceServer({
client,
connState: 'connected',
desktopCapabilities: [],
executionHost: HOST
})
return null
}
feed.hosts = [server('ready')]
let renderer: ReactTestRenderer | undefined
await act(async () => {
renderer = create(createElement(Probe))
})
feed.hosts = [server('unavailable')]
await act(async () => {
renderer?.update(createElement(Probe))
})
feed.hosts = [server('ready')]
await act(async () => {
renderer?.update(createElement(Probe))
})
// The gates' status read is keyed on this state, so the restart must reach it.
expect(gatesConnStates).toEqual(['connected', 'connecting', 'connected'])
renderer?.unmount()
})
})
@@ -1,111 +0,0 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { Automation, AutomationRun } from '../../shared/automations-types'
import type { AutomationRunWriter } from './automation-run-writer'
import { UNEVALUABLE_SCHEDULE, recordUnevaluableAutomation } from './dispatch-refusal'
const brokenAutomation: Automation = {
id: 'a1',
name: 'Broken schedule',
prompt: 'Check the repo',
precheck: null,
agentId: 'claude',
projectId: 'r1',
executionTargetType: 'local',
executionTargetId: 'local',
schedulerOwner: 'local_host_service',
workspaceMode: 'existing',
workspaceId: 'wt1',
baseBranch: null,
reuseSession: false,
timezone: 'UTC',
rrule: '0 9 32 * *',
dtstart: 0,
enabled: true,
nextRunAt: 1000,
missedRunPolicy: 'run_once_within_grace',
missedRunGraceMinutes: 720,
createdAt: 0,
updatedAt: 0
}
const makeRun = (id: string): AutomationRun => ({
id,
automationId: brokenAutomation.id,
title: 'Broken schedule run',
scheduledFor: brokenAutomation.nextRunAt,
status: 'pending',
trigger: 'scheduled',
workspaceId: brokenAutomation.workspaceId,
sessionKind: 'terminal',
chatSessionId: null,
terminalSessionId: null,
terminalPaneKey: null,
terminalPtyId: null,
outputSnapshot: null,
precheckResult: null,
usage: null,
error: null,
startedAt: null,
dispatchedAt: null,
createdAt: 0
})
function makeRunWriter(foldsRepeat: boolean): {
writer: AutomationRunWriter
created: string[]
updated: { status: string; error?: string | null }[]
} {
const created: string[] = []
const updated: { status: string; error?: string | null }[] = []
const writer: AutomationRunWriter = {
advanceNextRun: async () => brokenAutomation,
repeatSkip: async () => (foldsRepeat ? makeRun('folded') : null),
createRun: async () => {
const run = makeRun(`run-${created.length + 1}`)
created.push(run.id)
return run
},
updateRun: async (args) => {
updated.push({ status: args.status, error: args.error })
return { ...makeRun(args.runId), status: args.status, error: args.error ?? null }
}
}
return { writer, created, updated }
}
describe('recordUnevaluableAutomation', () => {
afterEach(() => {
vi.restoreAllMocks()
})
it('writes one run and logs once when the record is newly broken', async () => {
const logged = vi.spyOn(console, 'error').mockImplementation(() => {})
const { writer, created, updated } = makeRunWriter(false)
await recordUnevaluableAutomation({
runs: writer,
automation: brokenAutomation,
error: new Error('Invalid cron day of month.')
})
expect(created).toEqual(['run-1'])
expect(updated).toEqual([{ status: 'skipped_unavailable', error: UNEVALUABLE_SCHEDULE }])
expect(logged).toHaveBeenCalledTimes(1)
})
it('stays silent on a record it has already reported', async () => {
const logged = vi.spyOn(console, 'error').mockImplementation(() => {})
const { writer, created } = makeRunWriter(true)
for (let tick = 0; tick < 5; tick += 1) {
await recordUnevaluableAutomation({
runs: writer,
automation: brokenAutomation,
error: new Error('Invalid cron day of month.')
})
}
expect(created).toEqual([])
expect(logged).not.toHaveBeenCalled()
})
})
@@ -1,8 +1,7 @@
import { describe, expect, it, vi } from 'vitest'
import {
ExternalAutomationProbeCancelledError,
ExternalAutomationProbeScheduler,
isExternalAutomationProbeCancelled
ExternalAutomationProbeScheduler
} from './external-automation-probe-scheduler'
function deferred<T>(): { promise: Promise<T>; resolve: (value: T) => void } {
@@ -71,58 +70,6 @@ describe('ExternalAutomationProbeScheduler', () => {
expect(run).toHaveBeenCalledTimes(1)
})
it('cancels queued and in-flight probes whose scope is no longer selected', async () => {
const scheduler = new ExternalAutomationProbeScheduler({ concurrency: 1 })
const never = deferred<string>()
const queuedRun = vi.fn(() => Promise.resolve('queued'))
const keptRun = vi.fn(() => Promise.resolve('local'))
const inFlight = scheduler.schedule({
key: 'openclaw@ssh',
scopeKey: 'owner:desktop:ssh:target-a:3',
run: () => never.promise
})
const queued = scheduler.schedule({
key: 'openclaw@ssh',
scopeKey: 'owner:desktop:ssh:target-a:3',
run: queuedRun
})
const kept = scheduler.schedule({
key: 'openclaw@self',
scopeKey: 'owner:desktop:self',
run: keptRun
})
scheduler.retainScopes(['owner:desktop:self'])
await expect(inFlight).rejects.toBeInstanceOf(ExternalAutomationProbeCancelledError)
await expect(queued).rejects.toSatisfy(isExternalAutomationProbeCancelled)
expect(queuedRun).not.toHaveBeenCalled()
expect(keptRun).not.toHaveBeenCalled()
expect(scheduler.inFlight).toBe(1)
never.resolve('ignored')
await expect(kept).resolves.toBe('local')
expect(scheduler.inFlight).toBe(0)
})
it('cancels everything on demand', async () => {
const scheduler = new ExternalAutomationProbeScheduler({ concurrency: 1 })
const never = deferred<string>()
const probe = scheduler.schedule({
key: 'openclaw@self',
scopeKey: 'owner:desktop:self',
run: () => never.promise
})
scheduler.cancelAll()
await expect(probe).rejects.toBeInstanceOf(ExternalAutomationProbeCancelledError)
expect(scheduler.inFlight).toBe(1)
never.resolve('ignored')
await vi.waitFor(() => expect(scheduler.inFlight).toBe(0))
})
it('does not start a duplicate key while cancelled provider work is still settling', async () => {
const scheduler = new ExternalAutomationProbeScheduler({ concurrency: 2 })
const never = deferred<string>()
@@ -53,10 +53,6 @@ function desktopSsh(targetId: string, targetGeneration: number): AutomationOwner
return { authority: { kind: 'desktop' }, selector: { kind: 'ssh', targetId, targetGeneration } }
}
function orcaStoreHealth() {
return { read: vi.fn<() => void>(), markUnavailable: vi.fn<() => void>() }
}
function buildEngine(targets: SshTarget[]) {
return createScopedExternalAutomations({
registry: { getSshTargets: () => targets },
@@ -125,39 +121,6 @@ describe('scoped external automations', () => {
expect([...files]).toEqual(before)
})
it('settles when a local command lookup hangs', async () => {
vi.useFakeTimers()
runProcessMock.mockResolvedValue({
code: null,
signal: null,
stdout: '',
stderr: '',
timedOut: true
})
const promise = buildEngine([]).listManager({ owner: desktopSelf, provider: 'openclaw' })
let settled = false
void promise.finally(() => {
settled = true
})
await expect(promise).resolves.toMatchObject({ manager: null })
expect(settled).toBe(true)
})
it('probes only local managers when Local is selected', async () => {
const engine = buildEngine([sshTarget(), sshTarget({ id: 'target-b', generation: 1 })])
const entry = await engine.listManager({ owner: desktopSelf, provider: 'openclaw' })
expect(entry.manager?.id).toBe('openclaw:local')
expect(runProcessMock).toHaveBeenCalledTimes(1)
expect(runProcessMock.mock.calls[0]?.[0]).toEqual({
program: process.platform === 'win32' ? 'where' : 'which',
args: ['openclaw'],
timeoutMs: 5_000
})
})
it('reports an SSH host this process does not serve without probing this computer', async () => {
const engine = buildEngine([sshTarget(), sshTarget({ id: 'target-b', generation: 1 })])
@@ -233,28 +196,4 @@ describe('scoped external automations', () => {
).rejects.toBeInstanceOf(AutomationOwnerConflictError)
expect(runProcessMock).not.toHaveBeenCalled()
})
it('confines a manager failure to its own scope and never to Orca store health', async () => {
const health = orcaStoreHealth()
const engine = createScopedExternalAutomations({
registry: {
getSshTargets: () => {
health.read()
return [sshTarget()]
}
},
scheduler: new ExternalAutomationProbeScheduler(),
cache: new ExternalAutomationManagerCache({ ttlMs: 0 })
})
const failed = await engine.listManager({
owner: desktopSsh('target-a', 3),
provider: 'openclaw'
})
const healthy = await engine.listManager({ owner: desktopSelf, provider: 'openclaw' })
expect(failed).toMatchObject({ manager: null, error: SSH_HOST_NOT_SERVED_MESSAGE })
expect(healthy.manager?.status).toBe('available')
expect(health.markUnavailable).not.toHaveBeenCalled()
})
})
@@ -37,22 +37,6 @@ afterEach(() => {
})
describe('runExternalAutomationAction', () => {
it('runs local OpenClaw lifecycle actions through the CLI', async () => {
await runExternalAutomationAction({
managerId: 'openclaw:local',
provider: 'openclaw',
target: { type: 'local' },
jobId: 'job-1',
action: 'run'
})
expect(runProcessMock).toHaveBeenCalledWith({
program: 'openclaw',
args: ['cron', 'run', 'job-1'],
timeoutMs: 30_000
})
})
it('rejects job IDs that could be parsed as CLI options', async () => {
await expect(
runExternalAutomationAction({
@@ -109,22 +93,6 @@ describe('runExternalAutomationAction', () => {
expect(runProcessMock).not.toHaveBeenCalled()
})
it('maps OpenClaw lifecycle actions through its cron CLI names', async () => {
await runExternalAutomationAction({
managerId: 'openclaw:local',
provider: 'openclaw',
target: { type: 'local' },
jobId: 'job-1',
action: 'pause'
})
expect(runProcessMock).toHaveBeenCalledWith({
program: 'openclaw',
args: ['cron', 'disable', 'job-1'],
timeoutMs: 30_000
})
})
it('refuses actions on an SSH host without launching locally', async () => {
await expect(
runExternalAutomationAction({
@@ -117,45 +117,6 @@ function launch(
}
describe('headless automation agent launch', () => {
it('launches Hermes through the ordinary agent contract', async () => {
const runtime = harness()
const launched = await launch(runtime, { agentId: 'hermes' })
expect(runtime.launchAgentTerminal).toHaveBeenCalledWith('id:wt-1', {
agent: 'hermes',
prompt: 'fix the flaky test',
title: 'Nightly',
suppressStartupUpdatePrompt: true
})
expect(runtime.deliverStartupFollowup).not.toHaveBeenCalled()
expect(launched.terminalHandle).toBe('term_existing')
})
it('starts an argv agent in an existing workspace with the prompt on its command', async () => {
const runtime = harness()
const launched = await launch(runtime, { extraAgentArgs: '--model opus' })
expect(runtime.launchAgentTerminal).toHaveBeenCalledWith('id:wt-1', {
agent: 'claude',
prompt: 'fix the flaky test',
title: 'Nightly',
suppressStartupUpdatePrompt: true,
extraAgentArgs: '--model opus'
})
expect(runtime.deliverStartupFollowup).not.toHaveBeenCalled()
expect(runtime.getStructuredAgentSessionCreateSupport).not.toHaveBeenCalled()
expect(launched).toEqual({
workspaceId: 'wt-1',
workspaceDisplayName: 'repo',
terminalHandle: 'term_existing',
terminalSessionId: 'tab-1',
terminalPaneKey: 'tab-1:leaf-1',
terminalPtyId: 'pty-1'
})
})
it('starts a post-start agent bare and delivers its prompt once, after the terminal is recorded', async () => {
const runtime = harness()
let finishDelivery: (delivered: boolean) => void = () => {}
@@ -178,54 +139,6 @@ describe('headless automation agent launch', () => {
finishDelivery(true)
})
it('creates a new-per-run workspace agent-first with the argv prompt', async () => {
const runtime = harness()
const launched = await launch(runtime, {
workspaceMode: 'new_per_run',
workspaceId: null,
extraAgentArgs: '--model opus'
})
expect(runtime.createManagedWorktree).toHaveBeenCalledTimes(1)
const args = runtime.createManagedWorktree.mock.calls[0]![0]
expect(args).toMatchObject({
repoSelector: 'repo-1',
activate: false,
setupDecision: 'skip',
startupAgent: 'claude',
startupPrompt: 'fix the flaky test',
startupExtraAgentArgs: '--model opus',
createdWithAgent: 'claude',
telemetrySource: 'unknown'
})
expect(args).not.toHaveProperty('awaitTerminalProvisioning')
expect(args).not.toHaveProperty('observeSetupCompletion')
expect(runtime.launchAgentTerminal).not.toHaveBeenCalled()
expect(runtime.deliverStartupFollowup).not.toHaveBeenCalled()
expect(launched).toEqual({
workspaceId: 'repo-1::/wt/auto',
workspaceDisplayName: 'auto-nightly',
terminalHandle: 'term_new',
terminalSessionId: 'tab-new',
terminalPaneKey: 'tab-new:leaf-1',
terminalPtyId: 'pty-new'
})
})
it('creates a new-per-run workspace for a post-start agent and delivers its prompt once', async () => {
const runtime = harness()
await launch(runtime, { agentId: 'goose', workspaceMode: 'new_per_run', workspaceId: null })
expect(runtime.createManagedWorktree.mock.calls[0]![0]).not.toHaveProperty('startupPrompt')
expect(runtime.deliverStartupFollowup).toHaveBeenCalledTimes(1)
expect(runtime.deliverStartupFollowup).toHaveBeenCalledWith('term_new', {
agent: 'goose',
prompt: 'fix the flaky test'
})
})
it('fails the run with the create warning when no agent terminal started, without a second one', async () => {
const runtime = harness({ startupTerminal: null })
@@ -235,18 +148,6 @@ describe('headless automation agent launch', () => {
expect(runtime.launchAgentTerminal).not.toHaveBeenCalled()
})
it('launches bare and delivers nothing for a blank prompt', async () => {
const runtime = harness()
await launch(runtime, { agentId: 'aider', prompt: ' ' })
expect(runtime.launchAgentTerminal).toHaveBeenCalledWith(
'id:wt-1',
expect.objectContaining({ prompt: '' })
)
expect(runtime.deliverStartupFollowup).not.toHaveBeenCalled()
})
it('logs a failure after the terminal was recorded instead of dropping it', async () => {
const runtime = harness()
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
@@ -203,18 +203,6 @@ describe('headless run terminal retention', () => {
expect(h.forgotten[0]).toMatchObject({ id: 'missing', status: 'dispatch_failed' })
})
it('keeps a timed-out run whose agent still runs in its shell', async () => {
const runs = [
makeRun('timed-out', 0, 'dispatch_failed'),
...[1, 2, 3].map((n) => makeRun(`done${n}`, n))
]
const h = harness(runs)
await h.retention.sweep()
vi.advanceTimersByTime(RUN_TERMINAL_GRACE_MS * 10)
await h.retention.sweep()
expect(h.closed).toEqual([])
})
it('closes a still-dispatched run whose agent exited, leaving its shell idle', async () => {
const runs = [
makeRun('exited', 0, 'dispatched'),
@@ -354,17 +342,6 @@ describe('headless run terminal retention', () => {
expect(h.forgotten.map((run) => run.id)).not.toContain('r0')
})
it('closes a completed run whose workspace is shared', async () => {
const runs = [0, 1, 2, 3].map((n) => makeRun(`r${n}`, n))
const h = harness(runs)
await h.retention.sweep()
vi.advanceTimersByTime(RUN_TERMINAL_GRACE_MS)
await h.retention.sweep()
expect(h.slept).toEqual([])
expect(h.closed).toEqual(['tab-r0:1'])
expect(h.forgotten.map((run) => run.id)).toEqual(['r0'])
})
it('closes a failed run in its own workspace, and never sleeps or closes a used one', async () => {
const runs = [
makeRun('failed', 0, 'dispatch_failed'),
@@ -89,29 +89,4 @@ describe('headless automation workspace create args', () => {
}
})
})
it('falls back to skip for legacy automations without a saved setup decision', () => {
const args = buildHeadlessAutomationWorktreeCreateArgs({
automation: { ...automation, setupDecision: undefined },
run: {
id: 'run-1',
title: 'Nightly review run',
scheduledFor: Date.UTC(2026, 0, 2, 3, 4, 5)
},
repo
})
expect(args.setupDecision).toBe('skip')
expect(Object.hasOwn(args, 'startupExtraAgentArgs')).toBe(false)
})
it('carries saved extras into the startup agent launch', () => {
const args = buildHeadlessAutomationWorktreeCreateArgs({
automation: { ...automation, extraAgentArgs: '--model gpt-5.5' },
run: { id: 'run-1', title: 'Nightly review run', scheduledFor: 0 },
repo
})
expect(args.startupExtraAgentArgs).toBe('--model gpt-5.5')
})
})
@@ -4,11 +4,7 @@ import { mkdtempSync, rmSync } from 'node:fs'
import { join } from 'node:path'
import { tmpdir } from 'node:os'
import type { Repo } from '../../shared/repo-types'
import {
isFinalAutomationRunStatus,
type Automation,
type AutomationRun
} from '../../shared/automations-types'
import type { Automation, AutomationRun } from '../../shared/automations-types'
import { AutomationService } from './service'
import type {
AutomationRunCompletionObservation,
@@ -156,44 +152,6 @@ describe('reconciling retained runs against a graph that has not published yet',
service.stop()
})
it('persists the terminal status exactly once when the restored pane finishes the run', async () => {
const store = await createStore()
const automation = createAutomation(store)
const retained = retainDispatchedRun(store, automation)
const surface = createPaneSurface()
const service = new AutomationService(store, { terminalObserver: surface.observer })
const transitions: string[] = []
const persist = store.updateAutomationRun.bind(store)
vi.spyOn(store, 'updateAutomationRun').mockImplementation((update) => {
const before = store.listAutomationRuns(automation.id).find((r) => r.id === update.runId)
if (before && !isFinalAutomationRunStatus(before.status)) {
if (isFinalAutomationRunStatus(update.status)) {
transitions.push(update.status)
}
}
return persist(update)
})
service.start()
service.setRendererReady()
await vi.advanceTimersByTimeAsync(5_000)
surface.mountPane()
await service.markDispatchResult({
runId: retained.id,
status: 'completed',
...LAUNCH_TARGET,
error: null
})
await vi.advanceTimersByTimeAsync(AFTER_SETTLE_MS)
expect(transitions).toEqual(['completed'])
expect(readRun(store, automation.id, retained.id).status).toBe('completed')
expect(readRun(store, automation.id, retained.id).error).toBeNull()
service.stop()
})
it('does not fail a serve-startup retained run whose host pane has not reattached', async () => {
const store = await createStore()
const automation = createAutomation(store)
@@ -344,23 +344,4 @@ describe('automationsChanged publication', () => {
expect(seen.every((entry) => entry.payload.selector?.kind === 'self')).toBe(true)
service.stop()
})
it('changes nothing for a consumer that ignores the unknown event type', async () => {
const store = await createStore()
const automation = createAutomation(store)
const oldClientState = { repoRefreshes: 0 }
const service = new AutomationService(store, {
headlessDispatcher: async () => ({ ...LAUNCH_TARGET }),
onAutomationsChanged: (payload) => {
if ((payload as { type?: string }).type === 'reposChanged') {
oldClientState.repoRefreshes += 1
}
}
})
const run = await service.runNow(automation.id)
expect(oldClientState.repoRefreshes).toBe(0)
expect(readRun(store, automation.id, run.id).status).toBe('dispatched')
service.stop()
})
})
@@ -147,15 +147,6 @@ describe('resolveAutomationRunTarget projectId drift', () => {
expect(result).toMatchObject({ ok: true, cwd: '/repo' })
})
it('resolves when the snapshot is at the git: tier and the setup climbed to github:', () => {
const store = makeStore([makeSetup()], [makeRepo()])
const automation = makeAutomation(makeRunContext({ projectId: 'git:github.com/o/r' }))
const result = resolveAutomationRunTarget(store, automation)
expect(result).toMatchObject({ ok: true, cwd: '/repo' })
})
it('still blocks when the repoId no longer matches the setup', () => {
const store = makeStore([makeSetup({ repoId: 'repo-2' })], [makeRepo()])
const automation = makeAutomation(makeRunContext({ repoId: 'repo-1' }))
@@ -195,22 +195,4 @@ describe('headless automation service run terminal retention', () => {
expect(runtime.closeTerminal).not.toHaveBeenCalled()
expect(service.markDispatchResult).not.toHaveBeenCalled()
})
it('treats a retry that finds the run PTY already stopped by a failed sleep as slept', async () => {
const { runtime, service, store } = build(true)
store.getWorktreeMeta.mockImplementation(firstRunOwnsWorkspace)
store.listAutomationRuns.mockReturnValue(finishedRuns(1))
// Simulate retrying a sleep that stopped the PTY before throwing.
runtime.sleepTerminalsForWorktree.mockResolvedValue({ livePtyIds: [], stoppedPtyIds: [] })
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the mocked service exposes the drain hook it was given.
const drain = (service as unknown as { releaseFinishedRunTerminals: () => Promise<number> })
.releaseFinishedRunTerminals
await expect(drain()).resolves.toBe(1)
expect(runtime.closeTerminal).not.toHaveBeenCalled()
expect(service.markDispatchResult).toHaveBeenCalledWith({
runId: 'r0',
status: 'completed',
terminalPtyId: null
})
})
})
@@ -1,5 +1,4 @@
import { describe, expect, it, vi } from 'vitest'
import type { AutomationRun } from '../../shared/automations-types'
import type { HeadlessAutomationDispatcher } from './headless-dispatch'
import type { AutomationRunTerminalObserver } from './run-completion-watcher'
@@ -93,118 +92,3 @@ describe('headless automation dispatch', () => {
)
})
})
describe('headless automation dispatch with extra agent args', () => {
it('passes saved extras to an existing-workspace launch', async () => {
const runtime = {
setAutomationService: vi.fn(),
notifyAutomationsChanged: vi.fn(),
launchAgentTerminal: vi.fn(async () => ({
handle: 'terminal-1',
tabId: 'tab-1',
paneKey: 'tab-1:pane-1',
ptyId: 'pty-1',
worktreeId: 'wt-1'
})),
showManagedWorktree: vi.fn(async () => ({ displayName: 'repo' })),
getTerminalHandleForPaneKey: vi.fn(() => null),
getAgentStatusRowsForPane: vi.fn(() => [])
}
createRuntimeAutomationService({
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the mocked service never reads the store; the dispatcher reads only past runs.
store: { listAutomationRuns: () => [] } as never,
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the dispatcher reads only the members faked above.
runtime: runtime as never,
headless: true
})
const dispatcher = capturedOption('headlessDispatcher')
const automation = {
workspaceMode: 'existing',
workspaceId: 'wt-1',
agentId: 'claude',
prompt: 'go',
extraAgentArgs: '--model opus'
}
await dispatcher({
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: only these fields are read.
automation: automation as never,
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: only the title is read.
run: { title: 'Nightly' } as never,
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: unused for an existing workspace.
target: {} as never
})
expect(runtime.launchAgentTerminal).toHaveBeenCalledWith('id:wt-1', {
agent: 'claude',
prompt: 'go',
title: 'Nightly',
suppressStartupUpdatePrompt: true,
extraAgentArgs: '--model opus'
})
})
})
describe('headless reuse-session dispatch', () => {
it('sends the second run into the agent the first run started, on one PTY', async () => {
const runs: AutomationRun[] = []
const runtime = {
setAutomationService: vi.fn(),
notifyAutomationsChanged: vi.fn(),
launchAgentTerminal: vi.fn(async () => ({
handle: 'terminal-1',
tabId: 'tab-1',
paneKey: 'tab-1:pane-1',
ptyId: 'pty-1',
worktreeId: 'wt-1'
})),
showManagedWorktree: vi.fn(async () => ({ displayName: 'repo' })),
getTerminalHandleForPaneKey: vi.fn(() => 'terminal-1'),
getTerminalPtyIdForHandle: vi.fn(() => 'pty-1'),
getAgentStatusRowsForPane: vi.fn(() => [
{ state: 'done', agentType: 'claude', receivedAt: 1 }
])
}
createRuntimeAutomationService({
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the mocked service never reads the store; the dispatcher reads only past runs.
store: { listAutomationRuns: () => runs } as never,
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the dispatcher reads only the members faked above.
runtime: runtime as never,
headless: true
})
const dispatcher = capturedOption('headlessDispatcher')
const dispatch = (id: string) =>
dispatcher({
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: only these fields are read.
automation: {
id: 'auto-1',
workspaceMode: 'existing',
workspaceId: 'wt-1',
agentId: 'claude',
prompt: 'triage',
reuseSession: true
} as never,
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: only the id and title are read.
run: { id, title: 'Nightly' } as never,
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: unused for an existing workspace.
target: {} as never
})
const first = await dispatch('run-1')
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: selection reads only these run fields.
runs.push({
...first,
id: 'run-1',
automationId: 'auto-1',
status: 'completed',
createdAt: 1
} as AutomationRun)
const second = await dispatch('run-2')
expect(runtime.launchAgentTerminal).toHaveBeenCalledTimes(1)
expect(deliverTerminalAgentLaunchPrompt).toHaveBeenCalledWith(
expect.objectContaining({ handle: 'terminal-1', freshLaunch: false, text: 'triage' })
)
expect(second.terminalPtyId).toBe(first.terminalPtyId)
expect(second.terminalPaneKey).toBe('tab-1:pane-1')
})
})
@@ -226,16 +226,6 @@ describe('observing a run with agent evidence', () => {
expect(run.settled).toHaveBeenCalledWith(expect.objectContaining({ status: 'completed' }))
})
it('keeps idle-means-done for an agent that never reports, after its start window', async () => {
const pane = createPane({ idle: true, tail: ['summary written', '$'] })
const run = observe(pane)
await vi.advanceTimersByTimeAsync(10_000)
expect(run.settled).not.toHaveBeenCalled()
await vi.advanceTimersByTimeAsync(AGENT_START_GRACE_MS)
await run.promise
expect(run.settled).toHaveBeenCalledWith(expect.objectContaining({ status: 'completed' }))
})
it("never reads an agent's own tool output as its command missing", async () => {
const tail = ['running tests', 'bash: pytest: command not found', 'fell back to unittest', '$']
const run = observe(createPane({ idle: true, tail }))
@@ -118,40 +118,6 @@ describe('createRuntimeAutomationRunTerminalObserver', () => {
await run.promise
})
it('does not complete from a stale idle pane title (leaf branch shape)', async () => {
const runtime = createFakeRuntime({ lastAgentStatus: null, paneTitle: '✳ Claude — idle' })
const run = observe(runtime)
await vi.advanceTimersByTimeAsync(10_000)
expect(run.settled).toEqual([])
runtime.setPane({ paneTitle: '✳ Claude — working' })
await vi.advanceTimersByTimeAsync(1_000)
expect(run.settled).toEqual([])
runtime.setPane({ paneTitle: '✳ Claude — idle' })
await vi.advanceTimersByTimeAsync(1_000)
expect(run.settled[0]?.status).toBe('completed')
await run.promise
})
it('does not complete from a ready shell prompt left over at dispatch', async () => {
const runtime = createFakeRuntime({ preview: 'user@host repo %\n$' })
const run = observe(runtime)
await vi.advanceTimersByTimeAsync(10_000)
expect(run.settled).toEqual([])
runtime.setPane({ preview: 'claude is thinking…' })
await vi.advanceTimersByTimeAsync(1_000)
expect(run.settled).toEqual([])
runtime.setPane({ lastAgentStatus: 'idle' })
await vi.advanceTimersByTimeAsync(1_000)
expect(run.settled[0]?.status).toBe('completed')
await run.promise
})
it('fails the run truthfully when the pane never leaves its pre-dispatch state', async () => {
const runtime = createFakeRuntime({ lastAgentStatus: 'idle' })
const run = observe(runtime)
@@ -86,56 +86,6 @@ describe('AutomationService prechecks', () => {
rmSync(testState.dir, { recursive: true, force: true })
})
it('runs scheduled prechecks in the target repo before dispatch', async () => {
vi.setSystemTime(new Date('2026-05-13T08:00:00Z'))
const store = await createStore()
store.addRepo(makeRepo({ path: '/repo/path' }))
const automation = store.createAutomation({
name: 'Conditional check',
prompt: 'Check the repo',
precheck: {
command: 'test -f ready',
timeoutSeconds: 30
},
agentId: 'claude',
projectId: 'r1',
workspaceMode: 'new_per_run',
timezone: 'UTC',
rrule: 'FREQ=DAILY;BYHOUR=9;BYMINUTE=0',
dtstart: new Date('2026-05-14T00:00:00Z').getTime()
})
const run = store.createAutomationRun(automation, Date.now(), 'scheduled')
const precheckResult = {
command: 'test -f ready',
exitCode: 0,
timedOut: false,
durationMs: 5,
stdout: '',
stderr: '',
stdoutTruncated: false,
stderrTruncated: false,
error: null,
startedAt: Date.now(),
completedAt: Date.now()
}
runAutomationPrecheckMock.mockResolvedValue(precheckResult)
const service = new AutomationService(store, { tickMs: 60_000 })
const result = await service.runPrecheck(automation.id, run.id)
expect(result).toEqual(precheckResult)
expect(runAutomationPrecheckMock).toHaveBeenCalledWith({
precheck: {
command: 'test -f ready',
timeoutSeconds: 30
},
target: {
type: 'local',
cwd: '/repo/path'
}
})
})
it('does not run scheduled prechecks when the selected host setup is stale', async () => {
vi.setSystemTime(new Date('2026-05-13T08:00:00Z'))
const store = await createStore()
@@ -45,23 +45,6 @@ describe('Azure DevOps API request (STA-3494)', () => {
globalThis.fetch = OLD_FETCH
})
it('retries with -preview when Azure DevOps Server rejects the api-version', async () => {
const versions: (string | null)[] = []
globalThis.fetch = vi.fn(async (input: string | URL | Request) => {
const url = new URL(String(input))
versions.push(url.searchParams.get('api-version'))
if (!url.searchParams.get('api-version')?.endsWith('-preview')) {
return previewRejection()
}
return Response.json({ authenticatedUser: { providerDisplayName: 'Server User' } })
}) as never
await expect(
requestAzureDevOpsJsonAtBase(SERVER_BASE, '/_apis/connectionData')
).resolves.toEqual({ authenticatedUser: { providerDisplayName: 'Server User' } })
expect(versions).toEqual(['7.1', '7.1-preview'])
})
it('remembers the -preview requirement per origin after the first rejection', async () => {
const versions: (string | null)[] = []
globalThis.fetch = vi.fn(async (input: string | URL | Request) => {
+1 -61
View File
@@ -3,8 +3,7 @@ import { cancelTrackingResponse } from '../lib/unread-response-body.test-fixture
import {
getAzureDevOpsAuthStatus,
getAzureDevOpsPullRequestForBranch,
getAzureDevOpsPullRequestForBranchOrThrow,
normalizeAzureDevOpsApiBaseUrl
getAzureDevOpsPullRequestForBranchOrThrow
} from './client'
import { _resetAzureDevOpsPreviewApiVersionCache } from './azure-devops-api-request'
import { _resetAzureDevOpsRepoRefCache } from './repository-ref'
@@ -46,23 +45,6 @@ describe('Azure DevOps client', () => {
_resetAzureDevOpsRepoRefCache()
})
it('normalizes configured API base URLs', () => {
expect(normalizeAzureDevOpsApiBaseUrl('https://dev.azure.com/acme/Project/_apis/')).toBe(
'https://dev.azure.com/acme/Project'
)
})
it('marks token-only auth as configured but unverified because repository remotes supply the API base URL', async () => {
delete process.env.ORCA_AZURE_DEVOPS_API_BASE_URL
await expect(getAzureDevOpsAuthStatus()).resolves.toEqual({
configured: true,
authenticated: false,
account: null,
baseUrl: null,
tokenConfigured: true
})
})
it('authenticates against an on-prem Server that requires -preview api-versions (STA-3494)', async () => {
process.env.ORCA_AZURE_DEVOPS_API_BASE_URL = 'https://ado.example.com:8443/tfs/MyCollection'
const versions: (string | null)[] = []
@@ -334,48 +316,6 @@ describe('Azure DevOps client', () => {
).resolves.toMatchObject({ number: 40, state: 'open' })
})
it('keeps an active PR whose source branch is the repo default branch', async () => {
gitExecFileAsyncMock.mockResolvedValue({
stdout: 'https://dev.azure.com/acme/Project/_git/repo\n'
})
globalThis.fetch = vi.fn(async (input: string | URL | Request) => {
const url = new URL(String(input))
const response = (body: unknown): Response =>
new Response(JSON.stringify(body), {
status: 200,
headers: { 'Content-Type': 'application/json' }
})
if (url.pathname === '/acme/Project/_apis/git/repositories/repo') {
return response({ id: 'repo-guid' })
}
if (url.pathname === '/acme/Project/_apis/git/repositories/repo-guid/pullRequests') {
return response({
value: [
{
pullRequestId: 31,
title: 'main → release',
status: 'active',
creationDate: '2026-05-10T00:00:00Z',
mergeStatus: 'succeeded',
lastMergeSourceCommit: { commitId: 'main-head-oid' }
}
]
})
}
if (
url.pathname === '/acme/Project/_apis/git/repositories/repo-guid/pullRequests/31/statuses'
) {
return response({ value: [{ state: 'succeeded' }] })
}
return new Response(JSON.stringify({ message: 'not found' }), { status: 404 })
}) as never
await expect(
getAzureDevOpsPullRequestForBranch('/repo', 'refs/heads/main')
).resolves.toMatchObject({ number: 31, state: 'open' })
})
it('cancels unread error-response bodies so bundled undici cannot crash on socket close', async () => {
gitExecFileAsyncMock.mockResolvedValue({
stdout: 'https://dev.azure.com/acme/Project/_git/repo\n'
@@ -1,8 +1,5 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import {
createAzureDevOpsPullRequest,
isAzureDevOpsReviewCreationAuthenticated
} from './pull-request-creation'
import { createAzureDevOpsPullRequest } from './pull-request-creation'
import { _resetAzureDevOpsPreviewApiVersionCache } from './azure-devops-api-request'
import { _resetAzureDevOpsRepoRefCache } from './repository-ref'
import { REMOTE_URL_PROBE_TIMEOUT_MS } from '../git/remote-url-probe'
@@ -40,11 +37,6 @@ describe('Azure DevOps pull request creation', () => {
_resetAzureDevOpsRepoRefCache()
})
it('treats token-only auth as sufficient for repo-scoped creation', () => {
delete process.env.ORCA_AZURE_DEVOPS_API_BASE_URL
expect(isAzureDevOpsReviewCreationAuthenticated()).toBe(true)
})
it('posts a pull request create body to the repository REST endpoint', async () => {
const fetchMock = vi.fn(async (input: string | URL | Request, init?: RequestInit) => {
const url = new URL(String(input))
@@ -1,9 +1,5 @@
import { describe, expect, it } from 'vitest'
import {
deriveAzureDevOpsStatus,
mapAzureDevOpsPullRequest,
mapAzureDevOpsPullRequestState
} from './pull-request-mappers'
import { deriveAzureDevOpsStatus, mapAzureDevOpsPullRequestState } from './pull-request-mappers'
describe('Azure DevOps pull request mappers', () => {
it('maps Azure DevOps PR states into the hosted review state set', () => {
@@ -19,30 +15,4 @@ describe('Azure DevOps pull request mappers', () => {
expect(deriveAzureDevOpsStatus([{ state: 'succeeded' }, { state: 'failed' }])).toBe('failure')
expect(deriveAzureDevOpsStatus([])).toBe('neutral')
})
it('maps a raw PR into hosted review info', () => {
expect(
mapAzureDevOpsPullRequest(
{
pullRequestId: 42,
title: 'Add Azure support',
status: 'active',
creationDate: '2026-05-15T12:00:00Z',
mergeStatus: 'succeeded',
lastMergeSourceCommit: { commitId: 'abc123' }
},
'success',
'https://dev.azure.com/acme/Project/_git/repo'
)
).toEqual({
number: 42,
title: 'Add Azure support',
state: 'open',
url: 'https://dev.azure.com/acme/Project/_git/repo/pullrequest/42',
status: 'success',
updatedAt: '2026-05-15T12:00:00Z',
mergeable: 'MERGEABLE',
headSha: 'abc123'
})
})
})
@@ -136,17 +136,4 @@ describe('parseAzureDevOpsRepoRef', () => {
timeout: REMOTE_URL_PROBE_TIMEOUT_MS
})
})
it('bounds cached repository refs for distinct repo paths', async () => {
gitExecFileAsyncMock.mockResolvedValue({
stdout: 'git@ssh.dev.azure.com:v3/acme/Project/repo\n',
stderr: ''
})
for (let i = 0; i < 513; i += 1) {
await getAzureDevOpsRepoRefForRemote(`/repo-${i}`, 'origin')
}
expect(_getAzureDevOpsRepoRefCacheSize()).toBe(512)
})
})
-69
View File
@@ -82,44 +82,6 @@ describe('Bitbucket client', () => {
await expect(getBitbucketPullRequestForBranch('/repo', 'refs/heads/main')).resolves.toBeNull()
})
it('keeps an OPEN PR whose source branch is the repo default branch', async () => {
const fetchMock = vi.fn(async (url: string) => {
if (url.includes('/statuses/build')) {
return Response.json({ values: [{ state: 'SUCCESSFUL' }] })
}
return Response.json({
values: [
{ ...bitbucketPr(8), source: { branch: { name: 'main' }, commit: { hash: 'abc123' } } }
]
})
})
vi.stubGlobal('fetch', fetchMock)
await expect(
getBitbucketPullRequestForBranch('/repo', 'refs/heads/main')
).resolves.toMatchObject({ number: 8, state: 'open' })
})
it('discards a MERGED default-branch shadow and refetches the linked PR via the fallback (#9171)', async () => {
primeGitExecWithDefaultBranch()
const fetchMock = vi.fn(async (url: string) => {
if (url.includes('/statuses/build')) {
return Response.json({ values: [] })
}
if (url.endsWith('/pullrequests/42')) {
return Response.json(bitbucketPr(42))
}
return Response.json({
values: [{ ...bitbucketPr(7), state: 'MERGED', source: { branch: { name: 'main' } } }]
})
})
vi.stubGlobal('fetch', fetchMock)
await expect(
getBitbucketPullRequestForBranch('/repo', 'refs/heads/main', 42)
).resolves.toMatchObject({ number: 42 })
})
it('hides a MERGED PR matched only by feature-branch name so a new PR can be created', async () => {
const fetchMock = vi.fn(async (url: string) => {
if (url.includes('/statuses/build')) {
@@ -289,37 +251,6 @@ describe('Bitbucket client', () => {
).rejects.toThrow(/Bitbucket request failed/)
})
it('falls back to a linked PR number when branch lookup misses', async () => {
const fetchMock = vi.fn(async (url: string, _init?: RequestInit) => {
if (url.includes('/statuses/build')) {
return Response.json({ values: [] })
}
if (url.endsWith('/pullrequests/42')) {
return Response.json(bitbucketPr(42))
}
return Response.json({ values: [] })
})
vi.stubGlobal('fetch', fetchMock)
await expect(getBitbucketPullRequestForBranch('/repo', 'different', 42)).resolves.toMatchObject(
{
number: 42,
status: 'neutral'
}
)
})
it('reports env-token auth status through the Bitbucket /user endpoint', async () => {
const fetchMock = vi.fn(async () => Response.json({ username: 'bitbucket-user' }))
vi.stubGlobal('fetch', fetchMock)
await expect(getBitbucketAuthStatus()).resolves.toEqual({
configured: true,
authenticated: true,
account: 'bitbucket-user'
})
})
it('cancels unread error-response bodies so bundled undici cannot crash on socket close', async () => {
let cancelledBodies = 0
const fetchMock = vi.fn(async () =>
@@ -150,36 +150,4 @@ describe('Bitbucket credential connection', () => {
authMode: 'token'
})
})
it('lets an env API base URL override the stored one without env credentials', async () => {
const conn = await loadModule()
globalThis.fetch = vi.fn(async () =>
Response.json({ username: 'ada' })
) as unknown as typeof fetch
await conn.connectBitbucket({
authMode: 'basic',
email: 'ada@example.com',
apiToken: 'tok',
baseUrl: 'https://stored.example.com/2.0'
})
process.env.ORCA_BITBUCKET_API_BASE_URL = 'https://env.example.com/2.0'
const { resolveBitbucketAuthConfig } = await import('./resolve-auth')
expect(resolveBitbucketAuthConfig().baseUrl).toBe('https://env.example.com/2.0')
})
it('clears the stored connection on disconnect', async () => {
const conn = await loadModule()
globalThis.fetch = vi.fn(async () =>
Response.json({ username: 'ada' })
) as unknown as typeof fetch
await conn.connectBitbucket({
authMode: 'basic',
email: 'ada@example.com',
apiToken: 'tok'
})
conn.disconnectBitbucket()
expect(conn.getBitbucketConnectionStatus().source).toBe('none')
})
})
@@ -82,31 +82,6 @@ beforeEach(() => {
})
describe('Bitbucket credential store', () => {
it('persists plaintext metadata and an encrypted secret, then reads them back', async () => {
const store = await loadStore()
store.saveBitbucketCredential({
authMode: 'basic',
email: 'ada@example.com',
baseUrl: null,
account: 'ada',
accessToken: null,
apiToken: 'secret-token'
})
expect(store.hasStoredBitbucketCredential()).toBe(true)
expect(store.getStoredBitbucketMetadata()).toMatchObject({
authMode: 'basic',
email: 'ada@example.com',
account: 'ada'
})
expect(store.loadStoredBitbucketSecret()).toMatchObject({
accessToken: null,
apiToken: 'secret-token',
authMode: 'basic',
email: 'ada@example.com'
})
})
it('writes both credential files 0600', async () => {
const store = await loadStore()
store.saveBitbucketCredential({
@@ -1,8 +1,5 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import {
createBitbucketPullRequest,
isBitbucketReviewCreationAuthenticated
} from './pull-request-creation'
import { createBitbucketPullRequest } from './pull-request-creation'
import { _resetBitbucketRepoRefCache } from './repository-ref'
const { gitExecFileAsyncMock } = vi.hoisted(() => ({
@@ -82,18 +79,6 @@ describe('Bitbucket pull request creation', () => {
expect(fetchMock).toHaveBeenCalledTimes(1)
})
it('uses the stored credential when no environment variable is set', async () => {
delete process.env.ORCA_BITBUCKET_ACCESS_TOKEN
const fetchMock = vi.fn(async () => createdPullRequestResponse())
globalThis.fetch = fetchMock as unknown as typeof fetch
const result = await createBitbucketPullRequest('/repo', CREATE_INPUT, 'local')
// This fixture has no stored credential after the environment token is removed.
expect(result).toMatchObject({ ok: false, code: 'auth_required' })
expect(fetchMock).not.toHaveBeenCalled()
})
it('ignores a draft request rather than dead-ending a hidden persisted default', async () => {
const fetchMock = vi.fn(async () => createdPullRequestResponse())
globalThis.fetch = fetchMock as unknown as typeof fetch
@@ -106,12 +91,6 @@ describe('Bitbucket pull request creation', () => {
)
})
it('reports authenticated only when a credential resolves', async () => {
expect(isBitbucketReviewCreationAuthenticated()).toBe(true)
delete process.env.ORCA_BITBUCKET_ACCESS_TOKEN
expect(isBitbucketReviewCreationAuthenticated()).toBe(false)
})
it('maps a duplicate-branch rejection to already_exists with the existing review', async () => {
let call = 0
const fetchMock = vi.fn(async () => {
@@ -1,9 +1,5 @@
import { describe, expect, it } from 'vitest'
import {
deriveBitbucketBuildStatus,
mapBitbucketPullRequest,
mapBitbucketPullRequestState
} from './pull-request-mappers'
import { deriveBitbucketBuildStatus, mapBitbucketPullRequestState } from './pull-request-mappers'
describe('Bitbucket pull request mappers', () => {
it('normalizes Bitbucket pull request states', () => {
@@ -19,30 +15,4 @@ describe('Bitbucket pull request mappers', () => {
expect(deriveBitbucketBuildStatus([{ state: 'INPROGRESS' }])).toBe('pending')
expect(deriveBitbucketBuildStatus([{ state: 'FAILED' }])).toBe('failure')
})
it('maps raw pull request JSON into the shared PR-like shape', () => {
expect(
mapBitbucketPullRequest(
{
id: 42,
title: 'Add Bitbucket',
state: 'MERGED',
updated_on: '2026-05-10T00:00:00.000Z',
links: { html: { href: 'https://bitbucket.org/team/repo/pull-requests/42' } },
source: { branch: { name: 'feature' }, commit: { hash: 'abc123' } },
destination: { branch: { name: 'main' } }
},
'success'
)
).toEqual({
number: 42,
title: 'Add Bitbucket',
state: 'merged',
url: 'https://bitbucket.org/team/repo/pull-requests/42',
status: 'success',
updatedAt: '2026-05-10T00:00:00.000Z',
mergeable: 'UNKNOWN',
headSha: 'abc123'
})
})
})
-13
View File
@@ -108,17 +108,4 @@ describe('Bitbucket repository refs', () => {
timeout: REMOTE_URL_PROBE_TIMEOUT_MS
})
})
it('bounds cached repository refs for distinct repo paths', async () => {
gitExecFileAsyncMock.mockResolvedValue({
stdout: 'git@bitbucket.org:team/project.git\n',
stderr: ''
})
for (let i = 0; i < 513; i += 1) {
await getBitbucketRepoRef(`/repo-${i}`)
}
expect(_getBitbucketRepoRefCacheSize()).toBe(512)
})
})
-5
View File
@@ -40,8 +40,3 @@ it('removes the listener on disposal', () => {
parent.emit('disconnect')
expect(quit).not.toHaveBeenCalled()
})
it('does not couple a desktop launch to a sidecar owner marker', () => {
installServeSupervisorDisconnectQuit(false, new EventEmitter())
expect(quit).not.toHaveBeenCalled()
})
@@ -100,23 +100,6 @@ describe('AgentBrowserBridge', () => {
expect(holdPaintForCapture).not.toHaveBeenCalled()
})
it('gives the helper proxy a paint hold scoped to its page', async () => {
const release = vi.fn()
const holdPaintForCapture = vi.fn(() => release)
const b = new AgentBrowserBridge(
mockBrowserManager(undefined, undefined, { holdPaintForCapture })
)
b.setActiveTab(100)
webContentsFromIdMock.mockReturnValue(mockWebContents(100))
succeedWith({ snapshot: 'tree' })
await b.snapshot()
const holdPaint = CdpWsProxyMock.mock.calls[0]?.[1]
expect(holdPaint()).toBe(release)
expect(holdPaintForCapture).toHaveBeenCalledWith(100)
})
it('captures full-page screenshots directly through CDP using CSS layout bounds', async () => {
const wc = mockWebContents(100)
wc.debugger.sendCommand.mockImplementation((method: string) => {
@@ -53,7 +53,6 @@ vi.mock('./cdp-bridge', () => ({
}))
import { AgentBrowserBridge } from './agent-browser-bridge'
import { AGENT_BROWSER_IDLE_TIMEOUT_MS } from './agent-browser-process-environment'
import {
createFakeAgentBrowserChild,
createSucceedWith,
@@ -94,64 +93,6 @@ describe('AgentBrowserBridge', () => {
bridge.setActiveTab(100)
})
it('uses browserPageId as session name', async () => {
succeedWith({ snapshot: '...' })
await bridge.snapshot()
const args = execFileMock.mock.calls[0][1] as string[]
expect(args).toContain('--session')
expect(args[args.indexOf('--session') + 1]).toBe('orca-tab-tab-1')
})
it('passes --cdp on every helper command family so a restarted daemon cannot launch Chrome', async () => {
succeedWith({ snapshot: '...' })
await bridge.snapshot()
const snapshotCall = execFileMock.mock.calls.find((c: unknown[]) =>
(c[1] as string[]).includes('snapshot')
)
expect(snapshotCall![1]).toContain('--cdp')
const cdpIdx = (snapshotCall![1] as string[]).indexOf('--cdp')
expect((snapshotCall![1] as string[])[cdpIdx + 1]).toBe('9222')
await bridge.click('@e1')
await bridge.scroll('down')
await bridge.setOffline('on')
await bridge.consoleLog()
await bridge.exec('get title')
for (const command of ['click', 'scroll', 'set', 'console', 'get']) {
const call = execFileMock.mock.calls.find((candidate: unknown[]) =>
(candidate[1] as string[]).includes(command)
)
expect(call).toBeDefined()
const args = call![1] as string[]
expect(args).toContain('--cdp')
expect(args[args.indexOf('--cdp') + 1]).toBe('9222')
}
})
it('always appends --json to commands', async () => {
succeedWith({ snapshot: '...' })
await bridge.snapshot()
const snapshotCall = execFileMock.mock.calls.find((c: unknown[]) =>
(c[1] as string[]).includes('snapshot')
)
expect((snapshotCall![1] as string[]).at(-1)).toBe('--json')
})
it('translates success response to result', async () => {
succeedWith({ snapshot: 'tree output' })
const result = await bridge.snapshot()
expect(result).toEqual({ browserPageId: 'tab-1', snapshot: 'tree output' })
})
it('translates error response to BrowserError', async () => {
failWith('Element not found')
await expect(bridge.click('@e1')).rejects.toThrow('Element not found')
})
it('keeps CDP discovery failures generic while the tab session is still live', async () => {
failWith(CDP_DISCOVERY_FAILURE)
await expect(bridge.snapshot()).rejects.toMatchObject({
@@ -218,102 +159,6 @@ describe('AgentBrowserBridge', () => {
expect(args).toContain('@e3')
})
it('serializes concurrent commands per session', async () => {
const commandCalls: string[][] = []
execFileMock.mockImplementation(
(_bin: string, args: string[], _opts: unknown, cb: ExecFileCallback) => {
commandCalls.push(args)
cb(null, JSON.stringify({ success: true, data: { ok: true } }), '')
}
)
const [r1, r2] = await Promise.all([bridge.snapshot(), bridge.click('@e1')])
expect(r1).toEqual({ browserPageId: 'tab-1', ok: true })
expect(r2).toEqual({ ok: true })
const snapshotIdx = commandCalls.findIndex((a) => a.includes('snapshot'))
const clickIdx = commandCalls.findIndex((a) => a.includes('click'))
expect(snapshotIdx).toBeLessThan(clickIdx)
})
it('builds cookie set args with all options', async () => {
succeedWith({ success: true })
await bridge.cookieSet({
name: 'sid',
value: 'abc',
domain: '.example.com',
path: '/',
secure: true,
httpOnly: true,
sameSite: 'Lax',
expires: 1700000000
})
const args = execFileMock.mock.calls.at(-1)![1] as string[]
expect(args).toContain('cookies')
expect(args).toContain('set')
expect(args).toContain('sid')
expect(args).toContain('abc')
expect(args).toContain('--domain')
expect(args).toContain('.example.com')
expect(args).toContain('--path')
expect(args).toContain('/')
expect(args).toContain('--secure')
expect(args).toContain('--httpOnly')
expect(args).toContain('--sameSite')
expect(args).toContain('Lax')
expect(args).toContain('--expires')
expect(args).toContain('1700000000')
})
it('applies viewport emulation through CDP so mobile mode is preserved', async () => {
const wc = mockWebContents(100)
webContentsFromIdMock.mockReturnValue(wc)
await bridge.setViewport(375, 812, 2, true)
expect(wc.debugger.sendCommand).toHaveBeenCalledWith('Emulation.setDeviceMetricsOverride', {
width: 375,
height: 812,
deviceScaleFactor: 2,
mobile: true
})
expect(wc.debugger.sendCommand).toHaveBeenCalledWith('Emulation.setVisibleSize', {
width: 375,
height: 812
})
const viewportCall = execFileMock.mock.calls.find((call: unknown[]) =>
(call[1] as string[]).includes('viewport')
)
expect(viewportCall).toBeUndefined()
})
it('normalizes selector wait state=visible to the default supported semantics', async () => {
succeedWith({ selector: 'h1', waited: 'selector' })
await bridge.wait({ selector: 'h1', state: 'visible' })
const args = execFileMock.mock.calls.at(-1)![1] as string[]
expect(args).toContain('wait')
expect(args).toContain('h1')
expect(args).not.toContain('--state')
})
it('enforces conditional wait timeouts at the bridge layer', async () => {
succeedWith({ selector: '#ready', waited: 'selector' })
await bridge.wait({ selector: '#ready', timeout: 1200 })
const args = execFileMock.mock.calls.at(-1)![1] as string[]
const options = execFileMock.mock.calls.at(-1)![2] as { timeout?: number; env?: unknown }
expect(args).toContain('wait')
expect(args).toContain('#ready')
expect(options.timeout).toBe(2200)
const env = options.env as NodeJS.ProcessEnv
expect(env.PATH).toBe(process.env.PATH)
expect(env.AGENT_BROWSER_IDLE_TIMEOUT_MS).toBe(String(AGENT_BROWSER_IDLE_TIMEOUT_MS))
})
it('returns browser_timeout for timed conditional waits without recycling the session', async () => {
const killedError = Object.assign(new Error('timeout'), { killed: true })
execFileMock.mockImplementation(
@@ -218,14 +218,6 @@ describe('AgentBrowserBridge keypress input', () => {
expect(keyEventCalls(wc)[1]).toMatchObject({ type: 'keyUp', windowsVirtualKeyCode: 229 })
})
it('keeps the helper for a surrogate-pair character', async () => {
succeedWith({ pressed: '👍' })
await expect(bridge.keypress('👍', undefined, 'tab-1')).resolves.toEqual({ pressed: '👍' })
expect(keyEventCalls(wc)).toHaveLength(0)
})
it('falls back to agent-browser for a key name the table cannot express', async () => {
succeedWith({ pressed: 'MediaPlayPause' })
@@ -198,17 +198,4 @@ describe('AgentBrowserBridge', () => {
expect(mouseCalls[0]?.[1]).toMatchObject({ type: 'mousePressed', x: 12, y: 34, modifiers: 4 })
expect(mouseCalls[1]?.[1]).toMatchObject({ type: 'mouseReleased', x: 12, y: 34, modifiers: 4 })
})
it('drops empty command queues after direct CDP commands finish', async () => {
const wc = mockWebContents(100)
wc.debugger.sendCommand.mockResolvedValue({})
webContentsFromIdMock.mockReturnValue(wc)
await bridge.mouseClick(10, 20, 'right', undefined, 'tab-1')
expect(
(bridge as unknown as { commandQueues: Map<string, unknown[]> }).commandQueues.size
).toBe(0)
expect((bridge as unknown as { processingQueues: Set<string> }).processingQueues.size).toBe(0)
})
})
@@ -82,65 +82,6 @@ describe('AgentBrowserBridge', () => {
bridge.setActiveTab(100)
})
it('clears reload fallback timer after the load event settles', async () => {
vi.useFakeTimers()
try {
succeedWith(null)
const wc = {
...mockWebContents(100, 'https://reloaded.example', 'Reloaded'),
reload: vi.fn(),
on: vi.fn(),
removeListener: vi.fn()
}
webContentsFromIdMock.mockReturnValue(wc)
const result = bridge.reload()
await vi.waitFor(() => {
expect(wc.on).toHaveBeenCalledWith('did-finish-load', expect.any(Function))
})
const finishListener = wc.on.mock.calls.find(
([event]) => event === 'did-finish-load'
)?.[1] as (() => void) | undefined
const failListener = wc.on.mock.calls.find(([event]) => event === 'did-fail-load')?.[1] as
| (() => void)
| undefined
expect(finishListener).toBeDefined()
expect(failListener).toBeDefined()
expect(vi.getTimerCount()).toBe(1)
finishListener!()
await expect(result).resolves.toEqual({
url: 'https://reloaded.example',
title: 'Reloaded'
})
expect(wc.removeListener).toHaveBeenCalledWith('did-finish-load', finishListener)
expect(wc.removeListener).toHaveBeenCalledWith('did-fail-load', failListener)
expect(vi.getTimerCount()).toBe(0)
await vi.advanceTimersByTimeAsync(10_000)
expect(wc.removeListener).toHaveBeenCalledTimes(2)
} finally {
vi.useRealTimers()
}
})
it('navigates the registered webContents without spawning agent-browser', async () => {
const wc = mockWebContents(100, 'https://example.com/start', 'Example')
webContentsFromIdMock.mockReturnValue(wc)
await expect(bridge.goto('https://example.com/next')).resolves.toEqual({
url: 'https://example.com/next',
title: 'Example'
})
expect(wc.loadURL).toHaveBeenCalledWith('https://example.com/next')
expect(wc.isLoading).not.toHaveBeenCalled()
expect(execFileMock).not.toHaveBeenCalled()
})
it('preserves scheme-less navigation semantics at the direct WebContents boundary', async () => {
const wc = mockWebContents(100)
webContentsFromIdMock.mockReturnValue(wc)
@@ -101,20 +101,6 @@ describe('AgentBrowserBridge coordinate pointer input', () => {
webContentsFromIdMock.mockReturnValue(wc)
})
it('dispatches move, down, up and wheel over CDP without spawning the helper', async () => {
await expect(bridge.mouseMove(10, 20)).resolves.toEqual({ moved: true })
await expect(bridge.mouseDown('left')).resolves.toEqual({ pressed: true })
await expect(bridge.mouseUp('left')).resolves.toEqual({ released: true })
await expect(bridge.mouseWheel(120, 30)).resolves.toEqual({
scrolled: true,
deltaX: 30,
deltaY: 120
})
expect(execFileMock).not.toHaveBeenCalled()
expect(dispatched()).toHaveLength(4)
})
it('sends CDP payloads matching a real pointer press and release', async () => {
await bridge.mouseMove(10, 20)
await bridge.mouseDown('left')
@@ -343,14 +329,6 @@ describe('AgentBrowserBridge coordinate pointer input', () => {
expect(dispatched()).toHaveLength(0)
})
it('reports a dispatch failure as browser_error on the call that failed', async () => {
wc.debugger.sendCommand.mockRejectedValueOnce(new Error('boom'))
await expect(bridge.mouseMove(10, 20)).rejects.toMatchObject({
code: 'browser_error'
})
})
it('reports a page that dies mid-dispatch as browser_tab_not_found', async () => {
wc.debugger.sendCommand.mockImplementation(async () => {
webContentsFromIdMock.mockReturnValue(null)
@@ -415,12 +393,6 @@ describe('AgentBrowserBridge coordinate pointer input', () => {
expect(wc.debugger.detach).not.toHaveBeenCalled()
})
it('focuses an offscreen page on press, as mouseClick does', async () => {
await bridge.mouseDown('left')
expect(wc.focus).toHaveBeenCalled()
})
it('guards the user’s focus instead of focusing an embedded guest on press', async () => {
const release = vi.fn()
const guardUserFocusForAgentInput = vi.fn(async () => release)
@@ -21,7 +21,6 @@ vi.mock('node:child_process', async (importOriginal) => {
import { AgentBrowserBridge } from './agent-browser-bridge'
import { mockBrowserManager } from './agent-browser-bridge-test-harness'
import { AGENT_BROWSER_EXIT_DRAIN_MS } from './agent-browser-bridge-types'
let bridge: AgentBrowserBridge
@@ -71,47 +70,3 @@ it('releases the completed helper before the fallback deadline while its bridge
expect(child.deref()).toBeUndefined()
expect(await run("process.stdout.write('next')")).toBe('next')
})
it.each(['error', 'cancel'] as const)(
'retires the unused drain timer after %s',
async (outcome) => {
const timers = vi.spyOn(globalThis, 'setTimeout')
const clear = vi.spyOn(globalThis, 'clearTimeout')
const spawned = vi.mocked(childProcess.execFile)
const pending = run(
outcome === 'error'
? "process.stderr.write('fixture-error');process.exitCode=1"
: 'setInterval(() => {}, 1000)'
)
if (outcome === 'cancel') {
const result = spawned.mock.results[0]
if (result?.type !== 'return') {
throw new Error('Expected a real helper process')
}
bridge['cancelledProcesses'].add(result.value)
result.value.kill()
}
await expect(pending).rejects.toThrow(outcome === 'error' ? 'fixture-error' : 'Tab was closed')
const completed = spawned.mock.results[0]
if (completed?.type !== 'return') {
throw new Error('Expected a real helper process')
}
expect(bridge['cancelledProcesses'].has(completed.value)).toBe(false)
const drainIndex = timers.mock.calls.findIndex(
([, delay]) => delay === AGENT_BROWSER_EXIT_DRAIN_MS
)
expect(drainIndex).toBeGreaterThanOrEqual(0)
const drain = timers.mock.results[drainIndex]
if (drain?.type !== 'return') {
throw new Error('Expected a scheduled drain timer')
}
expect(clear).toHaveBeenCalledWith(drain.value)
}
)
it('preserves spawn failure without scheduling an exit drain', async () => {
Object.defineProperty(bridge, 'agentBrowserBin', { value: join(userDataPath.current, 'missing') })
const timers = vi.spyOn(globalThis, 'setTimeout')
await expect(run('')).rejects.toThrow('ENOENT')
expect(timers.mock.calls.some(([, delay]) => delay === AGENT_BROWSER_EXIT_DRAIN_MS)).toBe(false)
})
@@ -1,142 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
const { execFileMock, webContentsFromIdMock, existsSyncMock, readFileSyncMock, stdinWrites } =
vi.hoisted(() => ({
execFileMock: vi.fn(),
webContentsFromIdMock: vi.fn(),
existsSyncMock: vi.fn(() => false),
readFileSyncMock: vi.fn(() => Buffer.from('')),
stdinWrites: new Array<string>()
}))
vi.mock('child_process', () => ({ execFile: execFileMock }))
vi.mock('fs', () => ({
existsSync: existsSyncMock,
readFileSync: readFileSyncMock,
accessSync: vi.fn(),
chmodSync: vi.fn(),
constants: { X_OK: 1 }
}))
vi.mock('os', () => ({ platform: () => 'darwin', arch: () => 'arm64' }))
vi.mock('electron', () => ({
app: { getPath: vi.fn(() => '/app'), getAppPath: vi.fn(() => '/project'), isPackaged: false },
webContents: { fromId: webContentsFromIdMock }
}))
const { CdpWsProxyMock } = vi.hoisted(() => {
class MockCdpWsProxy {
static instances: unknown[] = []
start = vi.fn(async () => 'ws://127.0.0.1:9222')
stop = vi.fn(async () => {})
getPort = vi.fn(() => 9222)
constructor(_wc: unknown) {
MockCdpWsProxy.instances.push(this)
}
}
return { CdpWsProxyMock: MockCdpWsProxy }
})
vi.mock('./cdp-ws-proxy', () => ({ CdpWsProxy: CdpWsProxyMock }))
vi.mock('./cdp-bridge', () => ({
BrowserError: class BrowserError extends Error {
code: string
constructor(code: string, message: string) {
super(message)
this.code = code
}
}
}))
import { AgentBrowserBridge } from './agent-browser-bridge'
import {
createSucceedWith,
mockBrowserManager,
overrideBridgeWebContentsLookup,
resetAgentBrowserBridgeMocks
} from './agent-browser-bridge-test-harness'
overrideBridgeWebContentsLookup(AgentBrowserBridge.prototype, webContentsFromIdMock)
const succeedWith = createSucceedWith(execFileMock, stdinWrites)
function isStringArray(value: unknown): value is string[] {
return Array.isArray(value) && value.every((item) => typeof item === 'string')
}
function screenshotArgs(): string[] {
const screenshotCall = execFileMock.mock.calls.find(
(call) => Array.isArray(call) && isStringArray(call[1]) && call[1].includes('screenshot')
)
if (!screenshotCall || !isStringArray(screenshotCall[1])) {
throw new Error('Expected agent-browser screenshot arguments')
}
return screenshotCall[1]
}
describe('AgentBrowserBridge viewport screenshot formats', () => {
let bridge: AgentBrowserBridge
beforeEach(() => {
resetAgentBrowserBridgeMocks({
webContentsFromIdMock,
existsSyncMock,
readFileSyncMock,
stdinWrites,
cdpWsProxyInstances: CdpWsProxyMock.instances
})
bridge = new AgentBrowserBridge(mockBrowserManager())
bridge.setActiveTab(100)
})
it('forwards JPEG encoding and returns matching JPEG bytes and metadata', async () => {
const jpeg = Buffer.from([0xff, 0xd8, 0xff, 0xe0])
existsSyncMock.mockReturnValue(true)
readFileSyncMock.mockReturnValue(jpeg)
succeedWith({ path: '/tmp/capture.jpeg' })
const result = await bridge.screenshot('jpeg')
expect(result).toEqual({
data: jpeg.toString('base64'),
format: 'jpeg'
})
expect(Buffer.from(result.data, 'base64')).toEqual(jpeg)
expect(screenshotArgs()).toEqual(
expect.arrayContaining(['screenshot', '--screenshot-format', 'jpeg'])
)
expect(screenshotArgs().slice(-4)).toEqual([
'screenshot',
'--screenshot-format',
'jpeg',
'--json'
])
expect(screenshotArgs()).not.toContain('--screenshot-quality')
})
it.each([
['png', 'explicit PNG'],
[undefined, 'an omitted format'],
['webp', 'an unsupported format']
])('normalizes %s to PNG for %s', async (requestedFormat, _description) => {
const png = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])
existsSyncMock.mockReturnValue(true)
readFileSyncMock.mockReturnValue(png)
succeedWith({ path: '/tmp/capture.png' })
const result = await bridge.screenshot(requestedFormat)
expect(result).toEqual({
data: png.toString('base64'),
format: 'png'
})
expect(Buffer.from(result.data, 'base64')).toEqual(png)
expect(screenshotArgs().slice(-4)).toEqual([
'screenshot',
'--screenshot-format',
'png',
'--json'
])
})
})
@@ -155,44 +155,6 @@ describe('AgentBrowserBridge', () => {
}
})
it('destroys session after 3 consecutive timeouts', async () => {
const killedError = Object.assign(new Error('timeout'), { killed: true })
execFileMock.mockImplementation(
(_bin: string, args: string[], _opts: unknown, cb: ExecFileCallback) => {
if (args.includes('close')) {
cb(null, JSON.stringify({ success: true, data: null }), '')
return
}
cb(killedError, '', '')
}
)
for (let i = 0; i < 3; i++) {
await expect(bridge.snapshot()).rejects.toThrow('timed out')
}
succeedWith({ snapshot: 'fresh' })
await bridge.snapshot()
const lastArgs = execFileMock.mock.calls.at(-1)![1] as string[]
expect(lastArgs).toContain('--cdp')
})
it('bounds owner cleanup independently from command execution timeouts', async () => {
succeedWith({ snapshot: 'initial' })
await bridge.snapshot()
execFileMock.mockClear()
succeedWith(null)
await bridge.onPageClosed('tab-1')
const closeCall = execFileMock.mock.calls.find((call: unknown[]) =>
(call[1] as string[]).includes('close')
)
expect(closeCall?.[2]).toMatchObject({ timeout: 5_000 })
})
it('waits for pending session destruction before recreating the same session', async () => {
succeedWith({ snapshot: 'initial' })
await bridge.snapshot()
@@ -496,33 +458,6 @@ describe('AgentBrowserBridge', () => {
expect(routeCalls[0]).toContain('--cdp')
})
it('clears pending intercept restore state when a swapped tab closes before reuse', async () => {
const tabs = new Map([['tab-1', 100]])
const mgr = mockBrowserManager(tabs)
const b = new AgentBrowserBridge(mgr)
b.setActiveTab(100)
succeedWith({ ok: true })
await b.interceptEnable(['https://old.example/**'])
tabs.set('tab-1', 200)
webContentsFromIdMock.mockReturnValue(mockWebContents(200))
succeedWith(null)
await b.onProcessSwap('tab-1', 200)
expect(
(b as unknown as { pendingInterceptRestore: Map<string, string[]> }).pendingInterceptRestore
.size
).toBe(1)
await b.onTabClosed(200)
expect(
(b as unknown as { pendingInterceptRestore: Map<string, string[]> }).pendingInterceptRestore
.size
).toBe(0)
})
it('replays intercept routes after the daemon idles out', async () => {
succeedWith({ ok: true })
await bridge.interceptEnable(['https://api.example/**'])
@@ -620,42 +555,4 @@ describe('AgentBrowserBridge', () => {
expect(peakRetirements).toBe(4)
})
it('bounds every teardown close well inside the quit barrier', async () => {
succeedWith({ snapshot: 'tree' })
await bridge.snapshot()
succeedWith(null)
await bridge.destroyAllSessions()
const closeCall = execFileMock.mock.calls.findLast((c: unknown[]) =>
(c[1] as string[]).includes('close')
)
expect((closeCall![2] as { timeout: number }).timeout).toBeLessThanOrEqual(5_000)
})
it('closes a session still being created when everything is torn down', async () => {
let releaseProxyStart: (() => void) | undefined
CdpWsProxyMock.mockImplementationOnce(function (this: Record<string, unknown>) {
this.start = vi.fn(
() =>
new Promise<string>((resolve) => {
releaseProxyStart = () => resolve('ws://127.0.0.1:9222')
})
)
this.stop = vi.fn(async () => {})
this.getPort = vi.fn(() => 9222)
})
succeedWith({ snapshot: 'tree' })
const inFlight = bridge.snapshot()
await vi.waitFor(() => expect(releaseProxyStart).toBeDefined())
const closesBeforeTeardown = closeCallCount()
const teardown = bridge.destroyAllSessions()
releaseProxyStart!()
await Promise.allSettled([inFlight, teardown])
expect(closeCallCount()).toBeGreaterThan(closesBeforeTeardown)
})
})
@@ -217,66 +217,6 @@ describe('AgentBrowserBridge', () => {
await expect(b.snapshot()).rejects.toThrow('No browser tab open')
})
it('clears activeWebContentsId on tab close', async () => {
succeedWith({ snapshot: 'tree' })
await bridge.snapshot()
await bridge.onTabClosed(100)
expect(bridge.getActiveWebContentsId()).toBeNull()
})
it('resolves repeated guest lookups without enumerating registered tabs', () => {
const tabCount = 1_000
const tabs = new Map<string, number>()
const tabIdByWebContentsId = new Map<number, string>()
for (let index = 0; index < tabCount; index += 1) {
const tabId = `tab-${index}`
const webContentsId = 20_000 + index
tabs.set(tabId, webContentsId)
tabIdByWebContentsId.set(webContentsId, tabId)
}
const getRegisteredTabs = vi.fn(() => {
throw new Error('unexpected registered-tab enumeration')
})
const getTabIdForWebContentsId = vi.fn(
(webContentsId: number) => tabIdByWebContentsId.get(webContentsId) ?? null
)
const b = new AgentBrowserBridge(
mockBrowserManager(tabs, new Map(), {
getWebContentsIdByTabId: getRegisteredTabs,
getTabIdForWebContentsId
})
)
const resolveTabIdSafe = (
b as unknown as { resolveTabIdSafe: (webContentsId: number) => string | null }
).resolveTabIdSafe.bind(b)
const targetWebContentsId = 20_000 + tabCount - 1
for (let lookup = 0; lookup < tabCount; lookup += 1) {
expect(resolveTabIdSafe(targetWebContentsId)).toBe(`tab-${tabCount - 1}`)
}
expect(getTabIdForWebContentsId).toHaveBeenCalledTimes(tabCount)
expect(getRegisteredTabs).not.toHaveBeenCalled()
expect(resolveTabIdSafe(99_999)).toBeNull()
})
it('closes the named agent-browser session when a tab closes', async () => {
succeedWith({ snapshot: 'tree' })
await bridge.snapshot()
execFileMock.mockClear()
succeedWith(null)
await bridge.onTabClosed(100)
const closeCall = execFileMock.mock.calls.find((call: unknown[]) =>
(call[1] as string[]).includes('close')
)
expect(closeCall).toBeTruthy()
expect(closeCall![1]).toEqual(['--session', 'orca-tab-tab-1', 'close'])
})
it('repairs per-worktree active routing when the active tab closes', async () => {
const tabs = new Map([
['tab-a', 1],
@@ -313,40 +253,6 @@ describe('AgentBrowserBridge', () => {
expect(b.tabList('wt-1').tabs).toMatchObject([{ browserPageId: 'tab-a', active: true }])
})
it('switches active tab and returns switched index', async () => {
const tabs = new Map([
['tab-a', 1],
['tab-b', 2]
])
const wc1 = mockWebContents(1)
const wc2 = mockWebContents(2)
webContentsFromIdMock.mockImplementation((id: number) => (id === 1 ? wc1 : wc2))
const b = new AgentBrowserBridge(mockBrowserManager(tabs))
b.setActiveTab(1)
const result = await b.tabSwitch(1)
expect(result).toEqual({ switched: 1, browserPageId: 'tab-b' })
expect(b.getActiveWebContentsId()).toBe(2)
})
it('switches tabs by explicit browser page id', async () => {
const tabs = new Map([
['tab-a', 1],
['tab-b', 2]
])
const wc1 = mockWebContents(1)
const wc2 = mockWebContents(2)
webContentsFromIdMock.mockImplementation((id: number) => (id === 1 ? wc1 : wc2))
const b = new AgentBrowserBridge(mockBrowserManager(tabs))
b.setActiveTab(1)
const result = await b.tabSwitch(undefined, undefined, 'tab-b')
expect(result).toEqual({ switched: 1, browserPageId: 'tab-b' })
expect(b.getActiveWebContentsId()).toBe(2)
})
it('updates the owning worktree active tab when switching by browser page id', async () => {
const tabs = new Map([
['tab-a', 1],
@@ -318,19 +318,6 @@ describe('AgentBrowserBridge', () => {
expect(environment.editor.textContent).toBe(`${firstChunk}tail`)
})
it('uses target-aware agent-browser fill when clearing a non-rich target', async () => {
succeedWith({ filled: '@disabled' })
const result = await bridge.clear('@disabled')
const commandArgs = execFileMock.mock.calls.map((call: unknown[]) => call[1] as string[])
expect(commandArgs.some((args) => args.includes('eval'))).toBe(false)
expect(commandArgs.some((args) => args.includes('fill') && args.includes('@disabled'))).toBe(
true
)
expect(result).toEqual({ cleared: '@disabled' })
})
it('routes focused spinbutton wrappers to editable descendants before filling', async () => {
succeedWith({ ok: true })
@@ -437,24 +424,6 @@ describe('AgentBrowserBridge', () => {
expect(wrapper.events).toHaveLength(0)
})
it('keeps plain focused inputs as fill targets', async () => {
succeedWith({ ok: true })
await bridge.fill('@input', '200')
const expressions = stdinWrites
const input = createFillEvalNode({ tagName: 'INPUT' })
runFillEvalExpressions(expressions, {
activeElement: input.node,
getElementById: () => null
})
expect(input.value).toBe('200')
expect(input.events.map((event) => event.type)).toEqual(['input', 'change'])
})
it('fills large plain fields with one stdin edit and one event pair', async () => {
const text = `${'é\n'.repeat(512 * 1024)}tail'\\`
succeedWith({ ok: true })
@@ -41,18 +41,6 @@ describe('agent-browser orphan sweep', () => {
runProcessMock.mockReset()
})
it('closes tab daemons left by a previous run', async () => {
respond(['orca-tab-aaa', 'orca-tab-bbb'])
const closed = await sweepOrphanedAgentBrowserSessions({ binaryPath: BIN, ...SCOPED })
expect(closed).toEqual(['orca-tab-aaa', 'orca-tab-bbb'])
expect(closedArgs()).toEqual([
['--session', 'orca-tab-aaa', 'close'],
['--session', 'orca-tab-bbb', 'close']
])
})
it('never closes a daemon outside Orca tab naming', async () => {
respond(['default', 'agent1', 'orca-orcad-deadbeef', 'orca-tab-aaa'])
@@ -104,14 +92,6 @@ describe('agent-browser orphan sweep', () => {
expect(closedArgs()).toEqual([])
})
it('survives a listing that never returns', async () => {
runProcessMock.mockRejectedValue(new Error('ENOENT'))
await expect(
sweepOrphanedAgentBrowserSessions({ binaryPath: BIN, ...SCOPED })
).resolves.toEqual([])
})
it('keeps sweeping after one close fails', async () => {
runProcessMock.mockImplementation((spec: Spec) => {
if (spec.args?.[0] === 'session') {
@@ -133,16 +113,6 @@ describe('agent-browser orphan sweep', () => {
expect(closed).toEqual(['orca-tab-bbb'])
})
it('bounds every child it starts', async () => {
respond(['orca-tab-aaa'])
await sweepOrphanedAgentBrowserSessions({ binaryPath: BIN, ...SCOPED })
for (const call of runProcessMock.mock.calls) {
expect((call[0] as { timeoutMs?: number | null }).timeoutMs).toBeGreaterThan(0)
}
})
})
describe('sweep kill switch', () => {
@@ -169,18 +139,4 @@ describe('sweep kill switch', () => {
expect(closed).toEqual([])
expect(runProcessMock).not.toHaveBeenCalled()
})
it('still sweeps when the flag holds any other value', async () => {
process.env.ORCA_DISABLE_AGENT_BROWSER_SWEEP = '0'
runProcessMock.mockClear()
runProcessMock.mockResolvedValue({ code: 0, stdout: '{"data":{"sessions":[]}}', stderr: '' })
await sweepOrphanedAgentBrowserSessions({
binaryPath: BIN,
env: {},
ownsSocketDirectory: true
})
expect(runProcessMock).toHaveBeenCalled()
})
})
@@ -11,7 +11,6 @@ import {
MOBILE_WEB_BUNDLE_MAX_ROUTES,
MOBILE_WEB_BUNDLE_MAX_TOTAL_BYTES,
MOBILE_WEB_BUNDLE_SCHEMA_VERSION,
MOBILE_WEB_PAGE_VERSION,
type MobileWebBundleAsset
} from './manifest-contract'
@@ -117,10 +116,6 @@ describe('the page version a manifest carries', () => {
).toBe(false)
}
})
it('is at least 1, so a manifest without one reads as older than every page that has one', () => {
expect(MOBILE_WEB_PAGE_VERSION).toBeGreaterThanOrEqual(1)
})
})
describe('the page routes a manifest declares', () => {
@@ -352,31 +347,6 @@ describe('refinement short-circuit', () => {
const withinCeiling = assetsTotalling(MOBILE_WEB_BUNDLE_MAX_ASSETS, 64)
expect(issuePaths(manifestOf(withinCeiling, { buildId: WRONG_BUILD_ID }))).toEqual(['buildId'])
})
it('does not hash an oversized asset list', () => {
const overCeiling = [
...assetsTotalling(MOBILE_WEB_BUNDLE_MAX_ASSETS, 64),
asset('assets/overflow.js', 0)
]
const paths = issuePaths(manifestOf(overCeiling, { buildId: WRONG_BUILD_ID }))
expect(paths).toEqual(['assets'])
expect(paths).not.toContain('buildId')
})
it('does not hash once a cheaper invariant has failed', () => {
const twoAssets = [ENTRY, asset('assets/a.js', 10)]
expect(issuePaths(manifestOf(twoAssets, { totalBytes: 0, buildId: WRONG_BUILD_ID }))).toEqual([
'totalBytes'
])
expect(
issuePaths(
manifestOf(twoAssets, {
minCompatibleRuntimeProtocolVersion: 4,
buildId: WRONG_BUILD_ID
})
)
).toEqual(['minCompatibleRuntimeProtocolVersion'])
})
})
describe('asset paths', () => {
@@ -1,10 +1,8 @@
import { describe, expect, it } from 'vitest'
import {
buildInjectRejectionMessage,
taskNotFoundRefusal,
taskNotStartableRefusal
} from './orchestration-dispatch-refusal-contract'
import { TUI_AGENT_CONFIG } from './tui-agent-config'
import { recognizeAgentProcess } from './agent-process-recognition'
describe('buildInjectRejectionMessage', () => {
@@ -15,13 +13,6 @@ describe('buildInjectRejectionMessage', () => {
expect(message).toContain('no recognized agent detected')
})
it('names every agent Orca recognizes, including agy', () => {
expect(message).toMatch(/\bagy\b/)
for (const config of Object.values(TUI_AGENT_CONFIG)) {
expect(message).toContain(config.expectedProcess)
}
})
it('lists only names detection actually resolves, deduped and sorted', () => {
const listed = (/\(([^)]+)\)/.exec(message)?.[1] ?? '').split(', ')
@@ -36,19 +27,6 @@ describe('buildInjectRejectionMessage', () => {
// Pin published receipt literals independently so refactors cannot change builders and expectations together.
describe('dispatch refusal receipts keep their published messages', () => {
it('leaves the message exactly as each call site supplies it', () => {
expect(taskNotFoundRefusal('Task not found: task_1', { taskId: 'task_1' }).message).toBe(
'Task not found: task_1'
)
expect(
taskNotStartableRefusal('Task task_1 is pending; only a ready Task can start.', {
taskId: 'task_1',
status: 'pending',
unmetDependencies: []
}).message
).toBe('Task task_1 is pending; only a ready Task can start.')
})
it('tailors nextSteps to retry, dependency, occupancy, and terminal-status refusals', () => {
const base = { taskId: 'task_1', status: 'failed', unmetDependencies: [] }
expect(taskNotStartableRefusal('m', { ...base, retryOf: 'ctx_1' }).data.nextSteps[0]).toMatch(
@@ -50,28 +50,4 @@ describe('orchestration fleet attention', () => {
expect(child.categories).toEqual([])
expect(root).toEqual({ categories: ['root_completion'], requiresAction: false })
})
it('measures a five-worker wave without choosing one alert policy', () => {
const wave = [
{ isRoot: true, outcome: 'succeeded' as const },
{ isRoot: false, outcome: 'succeeded' as const },
{ isRoot: false, outcome: 'in_progress' as const, pendingInput: true },
{ isRoot: false, outcome: 'failed' as const },
{ isRoot: false, outcome: 'in_progress' as const, interrupted: true }
].map((facts) =>
projectOrchestrationFleetAttention({
...facts,
liveness: { verdict: facts.outcome === 'in_progress' ? 'live' : 'exited' }
})
)
const counts = wave
.flatMap((entry) => entry.categories)
.reduce<Record<string, number>>(
(result, category) => ({ ...result, [category]: (result[category] ?? 0) + 1 }),
{}
)
expect(counts).toEqual({ root_completion: 1, input: 1, failure: 1, interruption: 1 })
expect(wave.filter((entry) => entry.requiresAction)).toHaveLength(3)
})
})
@@ -42,14 +42,4 @@ describe('buildOrchestrationTaskDisplayMetadata', () => {
expect(metadata.displayName).toHaveLength(ORCHESTRATION_DISPLAY_NAME_MAX_LENGTH)
expect(metadata.displayName.endsWith('...')).toBe(true)
})
it('does not leave a dangling high surrogate when truncating', () => {
const metadata = buildOrchestrationTaskDisplayMetadata({
spec: `x${'😀'.repeat(ORCHESTRATION_TASK_TITLE_MAX_LENGTH)}`
})
const last = metadata.taskTitle.charCodeAt(metadata.taskTitle.length - 1)
expect(metadata.taskTitle.length).toBeLessThanOrEqual(ORCHESTRATION_TASK_TITLE_MAX_LENGTH)
expect(last < 0xd800 || last > 0xdbff).toBe(true)
})
})
@@ -1,49 +1,7 @@
import { describe, expect, it, vi } from 'vitest'
import * as ownership from './own-retained-string'
import { describe, expect, it } from 'vitest'
import { extractOscTitleScanTail } from './osc-title-scan-tail'
const INCOMPLETE_TITLE = '\x1b]2;Working on a terminal title'
describe('OSC title scan tail storage', () => {
it('routes every retained title tail through ownRetainedString', () => {
const own = vi.spyOn(ownership, 'ownRetainedString')
try {
const tail = extractOscTitleScanTail('x'.repeat(32 * 1024) + INCOMPLETE_TITLE)
expect(own).toHaveBeenCalledTimes(1)
expect(own).toHaveBeenLastCalledWith(INCOMPLETE_TITLE)
expect(tail).toBe(INCOMPLETE_TITLE)
// Ownership is unconditional: a growing title is re-owned on every chunk.
let pending = tail
for (let index = 0; index < 8; index += 1) {
pending = extractOscTitleScanTail(pending + 'x'.repeat(512))
}
expect(own).toHaveBeenCalledTimes(9)
expect(own).toHaveBeenLastCalledWith(pending)
// An unrelated incomplete OSC still yields nothing to retain.
expect(extractOscTitleScanTail(`${'x'.repeat(32 * 1024)}\x1b]9999;incomplete`)).toBe('')
} finally {
own.mockRestore()
}
})
it.each([
[16 * 1024, 256, INCOMPLETE_TITLE],
[64 * 1024, 64, INCOMPLETE_TITLE],
[1024 * 1024, 16, INCOMPLETE_TITLE],
[16 * 1024, 128, INCOMPLETE_TITLE + 'x'.repeat(16 * 1024)]
])('keeps %i-character chunks with %i incomplete titles byte-exact', (size, count, title) => {
const tails: string[] = []
for (let index = 0; index < count; index++) {
tails.push(
extractOscTitleScanTail(String.fromCharCode(65 + (index % 26)).repeat(size) + title)
)
}
const expected = title.length <= 4096 ? title : title.slice(0, 4) + title.slice(-4092)
expect(tails).toEqual(Array.from({ length: count }, () => expected))
})
it.each(['0', '1', '2'])('preserves title %s introducer and exact UTF-16 at the cap', (code) => {
const prefix = `\x1b]${code};`
for (const length of [4095, 4096, 4097, 16 * 1024]) {
-22
View File
@@ -368,28 +368,6 @@ describe('createOsc52OscHandler', () => {
})
describe('resolveOsc52ClipboardGate', () => {
it('allows a live write once the setting is on', () => {
expect(resolveOsc52ClipboardGate({ settingEnabled: true, replaying: false })).toEqual({
allowClipboardWrite: true,
shouldSurfaceBlockedWrite: false
})
})
it('drops replayed writes so restore cannot clobber the clipboard', () => {
// Restore replays old PTY copies through the parser and could overwrite newer clipboard content (#10588).
expect(resolveOsc52ClipboardGate({ settingEnabled: true, replaying: true })).toEqual({
allowClipboardWrite: false,
shouldSurfaceBlockedWrite: false
})
})
it('surfaces the blocked toast only for a real opt-out', () => {
expect(resolveOsc52ClipboardGate({ settingEnabled: false, replaying: false })).toEqual({
allowClipboardWrite: false,
shouldSurfaceBlockedWrite: true
})
})
it('stays quiet when a write races settings hydration', () => {
// Pre-hydration reads look blocked despite default-on; consuming the once-per-session toast hides real refusals.
for (const settingEnabled of [null, undefined]) {
+1 -16
View File
@@ -1,6 +1,6 @@
import nacl from 'tweetnacl'
import { describe, expect, it } from 'vitest'
import { encodeDeviceProofMessage, verifyDeviceProof } from './device-key-proof'
import { verifyDeviceProof } from './device-key-proof'
import { createMobileE2EEV2Fixture, MOBILE_E2EE_V2_VECTOR } from './mobile-e2ee-v2-fixtures'
import { openMobileE2EEV2Frame } from './mobile-e2ee-v2-framing'
import { createPairedE2EEClientHandshake } from './paired-e2ee-client-handshake'
@@ -99,19 +99,4 @@ describe('paired E2EE device-key auth', () => {
expect(early.transcriptHashB64()).toBeNull()
await expect(early.sealAuth(keyCredential)).rejects.toThrow()
})
it('signs the documented proof message', async () => {
const handshake = v2Handshake()
const seen: Uint8Array[] = []
await handshake.sealAuth({
...keyCredential,
sign: async (message) => {
seen.push(message)
return nacl.sign.detached(message, key.secretKey)
}
})
expect(seen[0]).toEqual(
encodeDeviceProofMessage('auth', Buffer.from(handshake.transcriptHashB64()!, 'base64'))
)
})
})
@@ -70,15 +70,4 @@ describe('selectAutoAdvertisedPairingAddress', () => {
])('does not treat the route-backed near-match %s as a known host-local label', (name) => {
expect(isVirtualBridgeInterface(name, true)).toBe(false)
})
it.each([
['external', EXTERNAL_SWITCH, false],
['Default Switch', DEFAULT_SWITCH, true],
['WSL', WSL_SWITCH, true],
['physical', PHYSICAL_LAN, false],
['host-local', HOST_LOCAL_BRIDGE, true],
['ambiguous', AMBIGUOUS_SWITCH, true]
] as const)('classifies the %s fixture', (_label, fixture, expected) => {
expect(isVirtualBridgeInterface(fixture.name, fixture.hasDefaultRoute)).toBe(expected)
})
})
+1 -24
View File
@@ -1,30 +1,7 @@
import { describe, expect, it } from 'vitest'
import { omitPairingLocalUiFields, PAIRING_LOCAL_UI_FIELDS } from './pairing-local-ui-fields'
import { omitPairingLocalUiFields } from './pairing-local-ui-fields'
describe('pairing-local UI fields', () => {
// Host RPC and web-preload seams are spread out; a census catches new fields without complete boundary wiring.
it('census: the set is exactly the fields no pairing may exchange', () => {
expect([...PAIRING_LOCAL_UI_FIELDS]).toEqual([
'automationHostFilter',
'hideWorkspacesFromOtherDevices',
'manualRepoOrder',
'workspaceHostOrder',
'sidebarOpen',
'agentsVisibleHostIds',
'agentsFilterRepoIds',
'agentsHideWorkspacesFromOtherDevices',
'agentsHideAutomationGeneratedWorkspaces',
'agentsHideCliCreatedWorkspaces',
'agentsShowChildAgents',
'agentsCompactMode',
'agentsShowSearch',
'agentsReadFilter',
'agentsGroupBy',
'activityClearedAtByPaneKey',
'manuallyUnreadTurnsByPaneKey'
])
})
it('omits every member and keeps everything else', () => {
const state = {
hideWorkspacesFromOtherDevices: true,
-11
View File
@@ -100,12 +100,6 @@ describe('pairing offer', () => {
expect(decodePairingOffer(encodePairingOffer(proxiedOffer))).toEqual(proxiedOffer)
})
it('encoded URL uses base64url (no +, /, or = characters)', () => {
const url = encodePairingOffer(offer)
const code = new URLSearchParams(url.slice(url.indexOf('?') + 1)).get('code')!
expect(code).not.toMatch(/[+/=]/)
})
it('rejects URLs with wrong scheme', () => {
expect(() => decodePairingOffer('https://example.com#abc')).toThrow('Invalid pairing URL')
})
@@ -165,11 +159,6 @@ describe('parsePairingCode', () => {
publicKeyB64: 'pubkey-xyz'
}
it('parses a full orca://pair# URL', () => {
const url = encodePairingOffer(offer)
expect(parsePairingCode(url)).toEqual(offer)
})
it('parses a bare base64url payload (without scheme prefix)', () => {
const url = encodePairingOffer(offer)
const base64url = new URLSearchParams(url.slice(url.indexOf('?') + 1)).get('code')!
@@ -37,12 +37,6 @@ describe('per-pane coverage gate', () => {
'uncovered'
)
})
it('is computed from evidence, never from a platform or remote flag', () => {
// No platform/isRemote input: host-proven hooks must resolve identically on every execution host.
const identity = resolveCanonicalPaneAgentIdentity({ hookAgent: 'claude', hookIsLive: true })
expect(identity).toMatchObject({ agent: 'claude', source: 'live-hook', coverage: 'covered' })
})
})
describe('process rung requires a host-stamped proof', () => {
@@ -158,21 +152,6 @@ describe('canonical ladder inside the covered lane', () => {
expect(identity).toMatchObject({ agent: 'claude', source: 'launch', titleOnly: false })
})
it('sibling evidence needs the explicit tab-scope opt-in', () => {
const withoutOptIn = resolveCanonicalPaneAgentIdentity({
launchAgent: 'claude',
siblingAgent: 'codex'
})
expect(withoutOptIn.agent).toBe('claude')
const optedIn = resolveCanonicalPaneAgentIdentity({
hookAgent: 'claude',
hookIsLive: true,
siblingAgent: 'codex',
allowSibling: true
})
expect(optedIn).toMatchObject({ agent: 'claude', source: 'live-hook' })
})
it('surfaces ambiguity instead of picking by array order', () => {
const identity = resolveCanonicalPaneAgentIdentity({
hookAgent: 'claude',
@@ -1,5 +1,4 @@
import { describe, expect, it } from 'vitest'
import { PANE_AGENT_SOURCE_RANK } from './pane-agent-identity-adapter'
import {
PANE_AGENT_EVIDENCE_SOURCES,
type PaneAgentEvidence,
@@ -12,14 +11,6 @@ const resolve = (evidence: PaneAgentEvidence[], extra = {}) =>
const H = 'authority-a'
describe('resolvePaneAgentIdentity', () => {
it('keeps every evidence source ranked exactly once', () => {
expect(PANE_AGENT_SOURCE_RANK).toBe(PANE_AGENT_EVIDENCE_SOURCES)
expect(new Set(PANE_AGENT_SOURCE_RANK).size).toBe(PANE_AGENT_SOURCE_RANK.length)
for (const source of PANE_AGENT_EVIDENCE_SOURCES) {
expect(PANE_AGENT_SOURCE_RANK.indexOf(source)).toBeGreaterThanOrEqual(0)
}
})
describe('a display title is the last thing consulted', () => {
it.each(PANE_AGENT_EVIDENCE_SOURCES.filter((s) => s !== 'title' && s !== 'sibling'))(
'lets %s outrank a conflicting title',
@@ -39,16 +30,6 @@ describe('resolvePaneAgentIdentity', () => {
source: 'title'
})
})
it('outranks the launch record with nothing weaker than the launch record', () => {
// The tab ladder currently puts the parsed title ABOVE activeLaunchAgent, so a string Orca
// parsed beats a fact Orca owns. That inversion cannot be expressed here.
const result = resolve([
{ source: 'launch', agent: 'claude' },
{ source: 'title', agent: 'gemini' }
])
expect(result.agent).toBe('claude')
})
})
describe('run generation separates the bug from the legitimate reclaim', () => {
@@ -78,18 +59,6 @@ describe('resolvePaneAgentIdentity', () => {
expect(result).toMatchObject({ agent: 'codex', source: 'title' })
expect(result.supersededSources).toEqual(['completed-hook'])
})
it('produces opposite answers from identical evidence, given only the run ids', () => {
const bug = resolvePaneAgentIdentity({
evidence: evidenceFor(7, 7),
currentRun: { authorityId: H, incarnation: 7 }
})
const reclaim = resolvePaneAgentIdentity({
evidence: evidenceFor(7, 8),
currentRun: { authorityId: H, incarnation: 8 }
})
expect(bug.agent).not.toBe(reclaim.agent)
})
})
describe('mixed-version peers', () => {
@@ -288,9 +257,5 @@ describe('resolvePaneAgentIdentity', () => {
)
expect(result).toMatchObject({ agent: 'claude', source: 'launch' })
})
it('leaves display consumers unrestricted when no floor is given', () => {
expect(resolve([{ source: 'title', agent: 'codex' }]).agent).toBe('codex')
})
})
})
-10
View File
@@ -70,14 +70,4 @@ describe('Pi/OMP native state-title markers', () => {
}
)
})
// Every table row must reach detection without a second parser change (#13890).
it('routes every table marker through title detection', () => {
for (const marker of PI_STATE_MARKERS) {
const status = getPiStateTitleStatus(`π ${marker} my-project`)
expect(status).not.toBeNull()
expect(detectAgentStatusFromTitle(`π ${marker} my-project`)).toBe(status)
expect(detectAgentStatusFromTitle(`zsh | π ${marker} my-project`)).toBe(status)
}
})
})
@@ -1,6 +1,5 @@
import { createHash } from 'node:crypto'
import { describe, expect, it } from 'vitest'
import { canonicalizeCapabilitySet, type PluginCapability } from './plugin-capabilities'
import type { PluginCapability } from './plugin-capabilities'
import { fingerprintPluginConsent } from './plugin-consent-fingerprint'
import {
getPluginActivationState,
@@ -55,15 +54,6 @@ describe('fingerprintPluginConsent', () => {
expect(needsReconsent('orca-samples.demo', withWorker, lists)).toBe(true)
})
it('preserves capability-only fingerprints for existing panel plugins', () => {
const capabilities = [workspaceRead, storage]
const legacy = `sha256-${createHash('sha256')
.update(canonicalizeCapabilitySet(capabilities))
.digest('base64')}`
expect(fingerprintPluginConsent({ main: undefined, capabilities })).toBe(legacy)
})
it('requires re-consent when instructional content bytes change', () => {
const subject = {
main: undefined,
@@ -1,11 +1,9 @@
import { describe, expect, it } from 'vitest'
import {
isPluginLanguagePackRegistration,
parsePluginLanguagePackArtifact,
PLUGIN_LANGUAGE_CATALOG_MAX_DEPTH,
PLUGIN_LANGUAGE_CATALOG_MAX_ENTRIES,
validatePluginLanguagePackCatalog,
checkPluginLanguagePackCatalog,
pluginLanguageResourceId
} from './plugin-language-pack-artifact'
@@ -120,23 +118,6 @@ describe('plugin language-pack artifacts', () => {
).toMatchObject({ ok: false, error: expect.stringContaining('protected security copy') })
})
it('forges no trust badge: the community→Official swap is refused', () => {
expect(
parsePluginLanguagePackArtifact(
JSON.stringify({
auto: {
components: {
settings: { PluginConsentProvenance: { community: 'Official' } }
}
}
})
)
).toMatchObject({
ok: false,
error: expect.stringContaining('auto.components.settings.PluginConsentProvenance')
})
})
it('still lets language packs translate non-plugin settings copy', () => {
expect(
parsePluginLanguagePackArtifact(
@@ -185,29 +166,4 @@ describe('plugin language-pack artifacts', () => {
error: expect.stringContaining(`${PLUGIN_LANGUAGE_CATALOG_MAX_ENTRIES} entries`)
})
})
it('shape-validates 16 maximum-entry registrations without returning catalog copies', () => {
const catalog = Object.fromEntries(
Array.from({ length: PLUGIN_LANGUAGE_CATALOG_MAX_ENTRIES }, (_, index) => [
`key${index}`,
'value'
])
)
const packs = Array.from({ length: 16 }, (_, index) => {
const id = `plugin:maximum-${index}` as const
return {
id,
resourceLanguage: pluginLanguageResourceId(id),
pluginKey: `maximum-${index}`,
locale: 'en',
catalog
}
})
expect(checkPluginLanguagePackCatalog(catalog)).toEqual({
ok: true,
entries: PLUGIN_LANGUAGE_CATALOG_MAX_ENTRIES
})
expect(packs.filter(isPluginLanguagePackRegistration)).toHaveLength(16)
})
})
@@ -17,13 +17,6 @@ describe('structuredCloneMessageBytes', () => {
expect(structuredCloneMessageBytes(tinyView, 256)).toBeGreaterThanOrEqual(128)
expect(structuredCloneMessageBytes(new Blob([new Uint8Array(80)]), 256)).toBe(80)
})
it('handles cycles and stops walking once the cap is exceeded', () => {
const cyclic: { self?: unknown; text: string } = { text: 'x'.repeat(100) }
cyclic.self = cyclic
expect(structuredCloneMessageBytes(cyclic, 32)).toBe(33)
})
})
describe('createPanelMessageBudget', () => {
+1 -13
View File
@@ -1,21 +1,9 @@
// @vitest-environment happy-dom
import { describe, expect, it } from 'vitest'
import { buildPluginPanelShellHtml, PLUGIN_PANEL_CSP } from './plugin-panel-shell'
import { buildPluginPanelShellHtml } from './plugin-panel-shell'
describe('buildPluginPanelShellHtml', () => {
it('places CSP and navigation guards before plugin content', () => {
const html = buildPluginPanelShellHtml('<main id="plugin-content">Plugin</main>')
const pluginOffset = html.indexOf('plugin-content')
expect(PLUGIN_PANEL_CSP).toContain("form-action 'none'")
expect(html.indexOf('Content-Security-Policy')).toBeLessThan(pluginOffset)
expect(html.indexOf("window.navigation.addEventListener('navigate'")).toBeLessThan(pluginOffset)
expect(html.indexOf("window.addEventListener('click'")).toBeLessThan(pluginOffset)
expect(html.indexOf("window.addEventListener('submit'")).toBeLessThan(pluginOffset)
expect(html.indexOf("Object.defineProperty(window, 'open'")).toBeLessThan(pluginOffset)
})
it('cancels anchor and form default navigation in the fallback path', () => {
const html = buildPluginPanelShellHtml('<main>Plugin</main>')
const script = html.match(/<script>\n([\s\S]*?)<\/script>/)?.[1]
@@ -7,24 +7,6 @@ const check = (name: string, conclusion: string | null) =>
({ name, conclusion }) as Pick<PRCheckDetail, 'name' | 'conclusion'>
describe('PR check severity order', () => {
it('keeps passing checks above the skipped and neutral noise', () => {
const sorted = sortChecksBySeverity([
check('daily-cleanup', 'skipped'),
check('Test Results', 'success'),
check('build-admin', 'skipped'),
check('advisory', 'neutral'),
check('Validate Configs', 'success')
])
expect(sorted.map((c) => c.name)).toEqual([
'Test Results',
'Validate Configs',
'advisory',
'daily-cleanup',
'build-admin'
])
})
it('orders failures, then in-flight work, then successes', () => {
const sorted = sortChecksBySeverity([
check('success', 'success'),
@@ -73,12 +55,6 @@ describe('PR check severity order', () => {
])
})
it('sinks unknown conclusions below every known state', () => {
expect(getCheckSeverityRank('stale_from_a_future_github')).toBeGreaterThan(
getCheckSeverityRank('skipped')
)
})
// Object rank tables expose prototype names as functions, producing NaN comparisons and unstable ordering.
it('treats prototype property names as unknown conclusions', () => {
for (const inherited of ['constructor', 'toString', 'hasOwnProperty', '__proto__']) {
-19
View File
@@ -2,25 +2,6 @@ import { describe, expect, it } from 'vitest'
import { PrioritySemaphore } from './priority-semaphore'
describe('PrioritySemaphore', () => {
it('blocks when concurrency limit is reached', async () => {
const sem = new PrioritySemaphore(1)
const r1 = await sem.acquire(0)
let acquired = false
const p2 = sem.acquire(0).then((r) => {
acquired = true
return r
})
await new Promise((r) => setTimeout(r, 10))
expect(acquired).toBe(false)
r1()
const r2 = await p2
expect(acquired).toBe(true)
r2()
})
it('serves higher priority (lower number) first', async () => {
const sem = new PrioritySemaphore(1)
const r1 = await sem.acquire(0)
@@ -1,41 +0,0 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
const { execFileMock } = vi.hoisted(() => ({ execFileMock: vi.fn() }))
vi.mock('node:child_process', () => ({ execFile: execFileMock }))
import * as processTableFormat from './process-table-snapshot'
import {
getProcessTableSnapshot,
getStrictProcessTableSnapshot,
resetProcessTableSnapshotForTests
} from './process-table-snapshot-reader'
const platform = Object.getOwnPropertyDescriptor(process, 'platform')!
afterEach(() => {
Object.defineProperty(process, 'platform', platform)
vi.restoreAllMocks()
resetProcessTableSnapshotForTests()
})
describe('process table parsing on non-Linux hosts', () => {
it.each(['darwin', 'win32'])('only parses the requested view on %s', async (hostPlatform) => {
Object.defineProperty(process, 'platform', { value: hostPlatform })
resetProcessTableSnapshotForTests()
const lenient = vi.spyOn(processTableFormat, 'parseProcessTableRows')
const strict = vi.spyOn(processTableFormat, 'parseStrictProcessTableRows')
execFileMock.mockImplementation((_program, _args, _options, callback) => {
callback(null, {
stdout: '100 1 100 100 Ss+ ?? Fri Oct 9 12:34:56 2026 /bin/zsh\n',
stderr: ''
})
})
expect((await getStrictProcessTableSnapshot())[0]?.pid).toBe(100)
expect(lenient).not.toHaveBeenCalled()
expect(strict).toHaveBeenCalledTimes(1)
await getStrictProcessTableSnapshot()
expect(strict).toHaveBeenCalledTimes(1)
expect((await getProcessTableSnapshot())[0]?.pid).toBe(100)
expect(lenient).toHaveBeenCalledTimes(1)
})
})
-19
View File
@@ -4,7 +4,6 @@ import {
collectDescendantsFromIndex,
type ProcessIdentityRow
} from './process-table-index'
import { parseStrictProcessTableRows } from './process-table-snapshot'
function boundedWalk<Row extends ProcessIdentityRow>(rows: readonly Row[], rootPid: number) {
const index = buildProcessTableIndex(rows)
@@ -139,22 +138,4 @@ describe('collectDescendantsFromIndex', () => {
const rows = Array.from({ length: 10_000 }, (_, index) => ({ pid: index + 2, ppid: index + 1 }))
expect(boundedWalk(rows, 1)).toEqual(rows.map((row, index) => ({ ...row, depth: index + 1 })))
})
it('preserves reverse capture order for a broad valid tree', () => {
const rows = Array.from({ length: 10_000 }, (_, index) => ({ pid: index + 2, ppid: 1 }))
expect(boundedWalk(rows, 1)).toEqual(rows.toReversed().map((row) => ({ ...row, depth: 1 })))
})
it('retains synthetic POSIX host identity fields through a cycle', () => {
const rows = parseStrictProcessTableRows('5 6 5 6 S pts/1 100 bash\n6 5 6 6 S+ pts/1 200 codex')
expect(boundedWalk(rows, 5)).toEqual([{ ...rows[1], depth: 1 }])
})
it('retains synthetic Windows creation markers without treating a PID as an incarnation', () => {
const rows = [
{ pid: 5, ppid: 6, creationTimeMs: 100, name: 'pwsh.exe' },
{ pid: 6, ppid: 5, creationTimeMs: 200, name: 'node.exe' }
]
expect(boundedWalk(rows, 5)).toEqual([{ ...rows[1], depth: 1 }])
})
})
+1 -51
View File
@@ -21,11 +21,7 @@ import {
parseStrictProcessTableRows,
ProcessTableCaptureError
} from './process-table-snapshot'
import {
buildProcessTableIndex,
getProcessTableIndex,
type ProcessTableIndexStats
} from './process-table-index'
import { buildProcessTableIndex, getProcessTableIndex } from './process-table-index'
function deferred<T>(): {
promise: Promise<T>
@@ -215,29 +211,6 @@ describe('process-table-snapshot reader', () => {
await expect(Promise.all([fresh, ordinary])).resolves.toEqual(['scan-1', 'scan-1'])
expect(scans).toBe(1)
})
it('shares one parsed-rows array across a burst so panes do not each re-parse', async () => {
// Parse inside the shared scan so callers share the same rows array instead of retokenizing stdout per pane.
let parses = 0
const gate = deferred<ReturnType<typeof parseProcessTableRows>>()
const reader = createProcessTableSnapshotReader<ReturnType<typeof parseProcessTableRows>>({
runPs: () => {
parses += 1
return gate.promise
},
now: () => 0
})
const a = reader.getSnapshot()
const b = reader.getSnapshot()
gate.resolve(parseProcessTableRows('100 1 Ss+ /bin/zsh'))
const rowsA = await a
const rowsB = await b
expect(parses).toBe(1)
// Reference identity: the burst reuses one parse, not one-per-caller.
expect(rowsA).toBe(rowsB)
})
})
describe('shared process-table capture', () => {
@@ -493,24 +466,6 @@ describe('getProcessTableIndex', () => {
expect(getProcessTableIndex(rows).byPid.get(100)).toBe(rows[0])
expect(buildProcessTableIndex(rows).byPid.get(100)).toBe(rows[0])
})
it('keeps the memo out of measured builds so a cache hit cannot satisfy a perf gate', () => {
const rows = parseProcessTableRows('100 1 Ss bash')
const stats: ProcessTableIndexStats = {
indexBuilds: 0,
rowVisits: 0,
indexLookups: 0
}
const memoized = getProcessTableIndex(rows)
const measured = buildProcessTableIndex(rows, stats)
expect(memoized.stats).toBeUndefined()
expect(measured).not.toBe(memoized)
expect(stats).toEqual({ indexBuilds: 1, rowVisits: 1, indexLookups: 0 })
// The measured build must not evict or replace the shared memo.
expect(getProcessTableIndex(rows)).toBe(memoized)
})
})
describe('process-table capture completeness', () => {
@@ -698,9 +653,4 @@ describe('evidence-publishing capture budget', () => {
})
expect(execFileMock).toHaveBeenCalledTimes(1)
})
it('keeps the identity budget far above its own', () => {
// Only current-evidence consumers give up early; slow identity reads must not become absent processes.
expect(PROCESS_TABLE_EVIDENCE_BUDGET_MS).toBeLessThan(PS_TIMEOUT_MS)
})
})
@@ -1,49 +0,0 @@
import { describe, expect, it } from 'vitest'
import { normalizeAccumulatedWorkspaceFixtureOptions } from './accumulated-workspace-profile'
import { buildAccumulatedWorkspaceSeed } from './accumulated-workspace-state-builder'
describe('accumulated workspace state builder', () => {
it('matches the captured production scale with canonical terminal identities', () => {
const seed = buildAccumulatedWorkspaceSeed(
normalizeAccumulatedWorkspaceFixtureOptions({}),
1_700_000_000_000
)
const worktrees = Object.values(seed.worktreesByRepo).flat()
const terminalTabs = Object.values(seed.tabsByWorktree).flat()
const unifiedTabs = Object.values(seed.unifiedTabsByWorktree).flat()
const unifiedTerminalTabs = unifiedTabs.filter(({ contentType }) => contentType === 'terminal')
expect(seed.repos).toHaveLength(27)
expect(worktrees).toHaveLength(870)
expect(terminalTabs).toHaveLength(1_410)
expect(unifiedTabs).toHaveLength(2_000)
expect(Object.keys(seed.terminalLayoutsByTabId)).toHaveLength(1_410)
expect(Object.keys(seed.sleepingAgentSessionsByPaneKey)).toHaveLength(857)
expect(seed.liveStatuses).toHaveLength(177)
expect(unifiedTerminalTabs.map(({ id }) => id)).toEqual(terminalTabs.map(({ id }) => id))
expect(unifiedTerminalTabs.every(({ id, entityId }) => id === entityId)).toBe(true)
})
it('uses shallow independent lineage pairs instead of one synthetic chain', () => {
const seed = buildAccumulatedWorkspaceSeed(normalizeAccumulatedWorkspaceFixtureOptions({}))
const lineages = Object.values(seed.worktreeLineageById)
const childIds = new Set(lineages.map(({ worktreeId }) => worktreeId))
expect(lineages.length).toBeGreaterThan(0)
expect(lineages.every(({ parentWorktreeId }) => !childIds.has(parentWorktreeId))).toBe(true)
})
// Interval 1 must include every non-main row; ordinal % interval === 1 would silently create none.
it('builds lineage for every non-main worktree at an interval of 1', () => {
const options = normalizeAccumulatedWorkspaceFixtureOptions({
worktrees: 9,
repositories: 3,
lineageEvery: 1
})
const seed = buildAccumulatedWorkspaceSeed(options)
const lineages = Object.values(seed.worktreeLineageById)
// 9 worktrees over 3 repos gives ordinals 0..2 each; ordinal 0 is the main row.
expect(lineages).toHaveLength(6)
})
})
@@ -1,54 +0,0 @@
import { describe, expect, it } from 'vitest'
import {
HOLD_ALTERNATE_SCREEN_OPEN,
alternateScreenFixtureScript
} from './alternate-screen-fixture-script'
import {
type HiddenPressureOutputMode,
pressureOutputScript
} from './artificial-opencode-hidden-pressure-script'
// The escape as it appears in generated source, where it is still a JS string escape.
const ALTERNATE_SCREEN_ENTER_SOURCE = '\\x1b[?1049h'
const PRESSURE_MODES: HiddenPressureOutputMode[] = ['tui', 'plain', 'title', 'latin', 'rich-model']
describe('alternateScreenFixtureScript', () => {
it('holds the painting process open so the alternate screen survives the assertions', () => {
const source = alternateScreenFixtureScript('\x1b[?1049hFRAME')
expect(source).toContain(HOLD_ALTERNATE_SCREEN_OPEN)
expect(source).toContain('process.stdout.write("\\u001b[?1049hFRAME")')
expect(source).not.toContain('setTimeout')
})
it('defers the paint by the requested delay and still holds open', () => {
const source = alternateScreenFixtureScript('FRAME', 750)
expect(source).toContain('setTimeout(() => process.stdout.write("FRAME"), 750)')
expect(source).toContain(HOLD_ALTERNATE_SCREEN_OPEN)
})
})
describe('hidden pressure fixture', () => {
// Entering alternate screen then exiting triggers dead-TUI recovery, silently erasing fixture content.
it.each(PRESSURE_MODES)(
'keeps mode %s alive exactly when it enters the alternate screen',
(mode) => {
const source = pressureOutputScript('run-id', mode)
expect(source.includes(HOLD_ALTERNATE_SCREEN_OPEN)).toBe(
source.includes(ALTERNATE_SCREEN_ENTER_SOURCE)
)
}
)
it('holds the rich-model alternate screen open past its done marker', () => {
const source = pressureOutputScript('run-id', 'rich-model')
expect(source).toContain(ALTERNATE_SCREEN_ENTER_SOURCE)
expect(source.indexOf(HOLD_ALTERNATE_SCREEN_OPEN)).toBeGreaterThan(
source.indexOf('OPENCODE_PRESSURE_DONE_')
)
})
})