mirror of
https://github.com/stablyai/orca.git
synced 2026-10-21 16:03:26 +00:00
233 lines
8.6 KiB
TypeScript
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)
|
|
}
|
|
}
|