Files
orca/tests/e2e/cross-version-wire/runtime-source-fence-downgrade.unit.test.ts
Neil 18bab90efa refactor(main): group client RPC pairing and host runtime owners (#38584)
* refactor(main): group client RPC pairing and host runtime owners

* test: target concrete mobile notification owner in headless fixture

* test: retarget dynamic quit mocks to runtime metadata owner
2026-11-05 09:16:47 -08:00

173 lines
5.7 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Pair the runtime-source fence over real sockets: old clients omit expectedRuntimeSource; old hosts ignore it.
// New hosts must reject mismatched expectations before dispatch; old hosts rely on the CLI pre-send probe.
import { createServer, type Server } from 'node:net'
import { randomUUID } from 'node:crypto'
import { mkdtempSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { z } from 'zod'
import {
RUNTIME_CAPABILITIES,
RUNTIME_SOURCE_FENCE_RUNTIME_CAPABILITY
} from '../../../src/shared/protocol-version'
import { publishHostDescriptor } from '../../../src/main/runtime/hosts/platform-probe/host-descriptor'
import type { RuntimeMetadata } from '../../../src/shared/runtime-bootstrap'
import type { ExpectedRuntimeSource } from '../../../src/shared/runtime-rpc-envelope'
import { sendRequest } from '../../../src/cli/runtime/transport'
import {
importReleaseCheckoutModule,
materializeReleaseCheckout,
releaseModuleExport
} from './release-checkout'
const SUITE_TIMEOUT_MS = 180_000
// The last release without the fence; a newer baseline would refuse like the current host.
const PRE_FENCE_REF = 'v1.4.224'
const RUNTIME_ID = 'runtime-a'
const SOURCE_A = '11111111-1111-4111-8111-111111111111'
const SOURCE_B = '22222222-2222-4222-8222-222222222222'
type Dispatcher = { dispatch(request: unknown): Promise<unknown> }
type HostBuild = (effect: () => unknown) => Dispatcher
function hostBuildOf(
dispatcher: Record<string, unknown>,
core: Record<string, unknown>
): HostBuild {
const RpcDispatcher = releaseModuleExport<
new (options: { runtime: unknown; methods: unknown[] }) => Dispatcher
>(dispatcher, 'RpcDispatcher')
const defineMethod = releaseModuleExport<(declaration: Record<string, unknown>) => unknown>(
core,
'defineMethod'
)
return (effect) =>
new RpcDispatcher({
runtime: { getRuntimeId: () => RUNTIME_ID },
methods: [
defineMethod({
name: 'terminal.send',
permission: 'workspace',
params: z.object({ text: z.string() }),
handler: effect
})
]
})
}
const servers: Server[] = []
async function serve(host: Dispatcher): Promise<RuntimeMetadata> {
const dir = mkdtempSync(join(tmpdir(), 'orca-source-fence-wire-'))
const isWindows = process.platform === 'win32'
const endpoint = isWindows
? `\\\\.\\pipe\\orca-source-fence-wire-${randomUUID()}`
: join(dir, 'runtime.sock')
const server = createServer((socket) => {
let buffer = ''
socket.setEncoding('utf8')
socket.on('data', (chunk: string) => {
buffer += chunk
const newline = buffer.indexOf('\n')
if (newline !== -1) {
void host
.dispatch(JSON.parse(buffer.slice(0, newline)))
.then((response) => socket.end(`${JSON.stringify(response)}\n`))
}
})
})
servers.push(server)
await new Promise<void>((resolve) => server.listen(endpoint, resolve))
return {
runtimeId: RUNTIME_ID,
pid: 1,
transports: [{ kind: isWindows ? 'named-pipe' : 'unix', endpoint }],
authToken: 'token',
startedAt: 1
}
}
let releasedHost: HostBuild
let currentHost: HostBuild
beforeAll(async () => {
const checkout = await materializeReleaseCheckout(PRE_FENCE_REF)
const load = (path: string) => importReleaseCheckoutModule(checkout, path)
const [oldDispatcher, oldCore, newDispatcher, newCore] = await Promise.all([
load('src/main/runtime/rpc/dispatcher.ts'),
load('src/main/runtime/rpc/core.ts'),
import('../../../src/main/runtime/rpc/dispatcher'),
import('../../../src/main/runtime/rpc/core')
])
releasedHost = hostBuildOf(oldDispatcher, oldCore)
currentHost = hostBuildOf(newDispatcher, newCore)
publishHostDescriptor(RUNTIME_ID, { installationId: SOURCE_A })
}, SUITE_TIMEOUT_MS)
afterAll(async () => {
publishHostDescriptor(RUNTIME_ID, null)
await Promise.all(
servers.map((server) => new Promise<void>((resolve) => server.close(() => resolve())))
)
})
async function call(
host: HostBuild,
expectedRuntimeSource?: ExpectedRuntimeSource
): Promise<{ response: { ok: boolean; error?: { code: string } }; ran: number }> {
let ran = 0
const metadata = await serve(
host(() => {
ran += 1
return { sent: true }
})
)
const response = await sendRequest(
metadata,
'terminal.send',
{ text: 'hi' },
5_000,
undefined,
expectedRuntimeSource
)
return { response, ran }
}
describe('runtime-source fence across versions', () => {
it('an old CLI sends no expectation, and a new host runs its request', async () => {
const { response, ran } = await call(currentHost)
expect(response).toMatchObject({ ok: true })
expect(ran).toBe(1)
})
it('an old host ignores a new CLI’s expectation rather than refusing', async () => {
const { response, ran } = await call(releasedHost, {
sourceId: SOURCE_A,
runtimeId: RUNTIME_ID
})
expect(response).toMatchObject({ ok: true })
expect(ran).toBe(1)
})
it('a new host refuses a new CLI’s request meant for another runtime before running it', async () => {
const { response, ran } = await call(currentHost, {
sourceId: SOURCE_B,
runtimeId: RUNTIME_ID
})
expect(response).toMatchObject({ ok: false, error: { code: 'runtime_source_mismatch' } })
expect(ran).toBe(0)
})
it('a new host runs a new CLI’s request meant for it and advertises the fence', async () => {
const { response, ran } = await call(currentHost, {
sourceId: SOURCE_A,
runtimeId: RUNTIME_ID
})
expect(response).toMatchObject({ ok: true })
expect(ran).toBe(1)
expect(RUNTIME_CAPABILITIES).toContain(RUNTIME_SOURCE_FENCE_RUNTIME_CAPABILITY)
})
})