Files
orca/config/scripts/node-server-runtime-verifier.ts
T

233 lines
8.6 KiB
TypeScript

import { mkdirSync, mkdtempSync, realpathSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { removeHostTree } from '../../src/main/host-tree-removal'
import { decodePairingOffer, type PairingOffer } from '../../src/shared/pairing'
import { sendRemoteRuntimeRequest } from '../../src/shared/remote-runtime-client'
import type { RuntimeStatus, RuntimeTerminalRead } from '../../src/shared/runtime-types'
import { verifyInstalledCliProfileBehavior } from './node-server-installed-cli-oracle'
import { stopNodeServerVerifierDaemons } from './node-server-verifier-daemon-cleanup'
import { initializeNodeServerVerifierGitWorkspace } from './node-server-verifier-git-fixture'
import { isSameExistingHostPath } from './node-server-verifier-host-path'
import {
startNodeServerVerifierProcess,
stopNodeServerVerifierProcess,
terminateNodeServerVerifierProcess,
type NodeServerVerifierProcess
} from './node-server-verifier-server-process'
const cliPath = resolve(readArgument('--cli') ?? 'resources/npm-server/dist/cli.js')
const expectedVersion = readArgument('--expected-version') ?? packageVersionFromEnvironment()
// Keep Unix daemon socket paths below platform limits while preserving Windows-native temp paths.
const shortTemporaryRoot = process.platform === 'win32' ? tmpdir() : '/tmp'
const ownedRoot = realpathSync.native(mkdtempSync(join(shortTemporaryRoot, 'orca-nsv-')))
const dataPath = join(ownedRoot, 'state')
const gitPath = join(ownedRoot, 'git-workspace')
const folderPath = join(ownedRoot, 'folder-workspace')
async function main(): Promise<void> {
let activeServer: NodeServerVerifierProcess | null = null
try {
initializeNodeServerVerifierGitWorkspace(gitPath)
mkdirSync(folderPath)
const first = await startNodeServerVerifierProcess({ cliPath, dataPath })
activeServer = first
const pairing = decodePairingOffer(first.ready.pairing.url)
await verifyWebClient(first.ready.pairing.webClientUrl)
await verifyStatus(pairing, first.ready.runtimeId, expectedVersion)
await verifyBrowserUnavailable(pairing)
await verifyGitWorkspace(pairing)
const terminal = await verifyFolderWorkspaceAndTerminal(pairing)
await verifyInstalledCliProfileBehavior({
cliPath,
dataPath,
expectedRuntimeId: first.ready.runtimeId,
expectedVersion,
pairingUrl: first.ready.pairing.url,
terminalHandle: terminal.handle,
temporaryRoot: ownedRoot
})
await stopNodeServerVerifierProcess(first)
activeServer = null
const second = await startNodeServerVerifierProcess({ cliPath, dataPath })
activeServer = second
assert(
second.ready.runtimeId !== first.ready.runtimeId,
'runtime process identity did not rotate'
)
const reconnectedPairing = { ...pairing, endpoint: second.ready.boundEndpoint }
await verifyStatus(reconnectedPairing, second.ready.runtimeId, expectedVersion)
const restored = await call<{ terminals: { handle: string; ptyId?: string }[] }>(
reconnectedPairing,
'terminal.list',
{ worktree: terminal.workspaceSelector }
)
assert(terminal.ptyId, 'created terminal did not expose its daemon identity')
const reattached = restored.terminals.find((candidate) => candidate.ptyId === terminal.ptyId)
assert(reattached, 'terminal daemon session was not adopted')
await waitForTerminalMarker(reconnectedPairing, reattached.handle, terminal.marker)
await stopNodeServerVerifierProcess(second)
activeServer = null
process.stdout.write('Node server runtime verification passed\n')
} finally {
if (activeServer?.child.exitCode === null && activeServer.child.signalCode === null) {
await stopNodeServerVerifierProcess(activeServer).catch(() =>
terminateNodeServerVerifierProcess(activeServer.child)
)
}
await stopNodeServerVerifierDaemons(dataPath)
await removeHostTree(ownedRoot)
}
}
void main().catch((error) => {
process.stderr.write(
`${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`
)
process.exitCode = 1
})
function readArgument(flag: string): string | undefined {
const index = process.argv.indexOf(flag)
return index === -1 ? undefined : process.argv[index + 1]
}
function packageVersionFromEnvironment(): string {
return process.env.npm_package_version ?? '0.0.0-dev'
}
async function verifyWebClient(url: string): Promise<void> {
const response = await fetch(url, { signal: AbortSignal.timeout(15_000) })
assert(response.ok, `web client returned ${response.status}`)
assert((await response.text()).includes('<!doctype html>'), 'web client HTML was not served')
}
async function verifyStatus(
pairing: PairingOffer,
expectedRuntimeId: string,
expectedAppVersion: string
): Promise<void> {
const status = await call<RuntimeStatus>(pairing, 'status.get')
assert(status.runtimeId === expectedRuntimeId, 'status returned the wrong runtime')
assert(status.appVersion === expectedAppVersion, 'status returned the wrong package version')
assert(status.desktopWindowStatus === 'blocked', 'desktop status is not blocked')
const capabilities = status.capabilities ?? []
for (const capability of [
'browser.screencast.v1',
'browser.headless.v1',
'browser.certificate-trust.v1'
]) {
assert(
!capabilities.some((candidate) => candidate === capability),
`browser capability leaked: ${capability}`
)
}
}
async function verifyBrowserUnavailable(pairing: PairingOffer): Promise<void> {
try {
await call(pairing, 'browser.tabCreate', { url: 'about:blank' })
} catch (error) {
assert(
error instanceof Error && error.message.includes('does not support browser panes'),
'browser request did not fail with an unsupported-host error'
)
return
}
throw new Error('browser request unexpectedly succeeded')
}
async function verifyGitWorkspace(pairing: PairingOffer): Promise<void> {
const added = await call<{ repo: { id: string; path: string } }>(pairing, 'repo.add', {
path: gitPath,
kind: 'git'
})
assert(
isSameExistingHostPath(added.repo.path, gitPath),
`repo.add returned an unexpected path: ${JSON.stringify(added.repo.path)}`
)
const listed = await call<{ worktrees: { id: string; path: string }[] }>(
pairing,
'worktree.list',
{ repo: `id:${added.repo.id}` }
)
assert(
listed.worktrees.length > 0,
`Git worktree unavailable: ${JSON.stringify(listed.worktrees.map((worktree) => worktree.path))}`
)
}
async function verifyFolderWorkspaceAndTerminal(pairing: PairingOffer): Promise<{
handle: string
marker: string
ptyId: string | undefined
workspaceSelector: string
}> {
const group = await call<{ group: { id: string } }>(pairing, 'projectGroup.create', {
name: 'Package verification',
parentPath: folderPath,
createdFrom: 'manual'
})
const workspace = await call<{ folderWorkspace: { id: string } }>(
pairing,
'folderWorkspace.create',
{ projectGroupId: group.group.id, name: 'Folder workspace', folderPath }
)
const marker = `orca-node-server-${Date.now()}`
const workspaceSelector = `id:folder:${workspace.folderWorkspace.id}`
const created = await call<{ terminal: { handle: string; ptyId?: string } }>(
pairing,
'terminal.create',
{
worktree: workspaceSelector,
command: `node -p "'${marker}:'+process.env.TERM_PROGRAM_VERSION"`
}
)
const versionedMarker = `${marker}:${expectedVersion}`
await waitForTerminalMarker(pairing, created.terminal.handle, versionedMarker)
return {
handle: created.terminal.handle,
marker: versionedMarker,
ptyId: created.terminal.ptyId,
workspaceSelector
}
}
async function waitForTerminalMarker(
pairing: PairingOffer,
handle: string,
marker: string
): Promise<void> {
const deadline = Date.now() + 20_000
while (Date.now() < deadline) {
const result = await call<{ terminal: RuntimeTerminalRead }>(pairing, 'terminal.read', {
terminal: handle,
limit: 200
})
if (result.terminal.tail.join('\n').includes(marker)) {
return
}
await new Promise((resolveWait) => setTimeout(resolveWait, 100))
}
throw new Error('terminal marker was not observed')
}
async function call<TResult>(
pairing: PairingOffer,
method: string,
params?: unknown
): Promise<TResult> {
const response = await sendRemoteRuntimeRequest<TResult>(pairing, method, params, 15_000)
if (!response.ok) {
throw new Error(`${method} failed: ${response.error.message}`)
}
return response.result
}
function assert(condition: unknown, message: string): asserts condition {
if (!condition) {
throw new Error(message)
}
}