mirror of
https://github.com/stablyai/orca.git
synced 2026-11-13 09:27:29 +00:00
* test: remove low-value fixture suites and custom scenarios * test: prune relay and recovery matrices and refresh retained host journey --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
238 lines
7.6 KiB
JavaScript
Executable File
238 lines
7.6 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
||
// Exercise packaged CLI paths under the hostile Linux conditions from #11609/#12530/#13719/#14229.
|
||
import { execFileSync } from 'node:child_process'
|
||
import { existsSync } from 'node:fs'
|
||
import { resolve } from 'node:path'
|
||
|
||
const commandArgs = process.argv.slice(2)
|
||
const appImageArg = valueAfter('--appimage')
|
||
const appImage = appImageArg ? resolve(appImageArg) : null
|
||
const platform = valueAfter('--platform')
|
||
const dockerPlatformArgs = platform ? ['--platform', platform] : []
|
||
|
||
const suffix = `${process.pid}-${Date.now()}`
|
||
const artifactVolume = `orca-cli-contract-artifact-${suffix}`
|
||
const tagArchitecture = platform?.split('/')[1] ?? process.arch
|
||
const tag = `orca-cli-launch-contract:ubuntu-24.04-${tagArchitecture}-${suffix}`
|
||
const containers = new Set()
|
||
let artifactVolumeCreated = false
|
||
const CASE_TIMEOUT_MS = 90_000
|
||
const BUILD_TIMEOUT_MS = 10 * 60_000
|
||
const STAGING_TIMEOUT_MS = 5 * 60_000
|
||
const DOCKER_TIMEOUT_MS = 2 * 60_000
|
||
const CLEANUP_TIMEOUT_MS = 30_000
|
||
|
||
// Exact statuses reject silent no-op launches as well as crashes.
|
||
const CASES = [
|
||
{
|
||
name: 'nofuse-userns-bundled-status',
|
||
// No runtime is running; the CLI must report that itself.
|
||
expectStatus: 1,
|
||
expectOutput: 'appRunning',
|
||
why: 'A command that needs the runtime must report its absence, not abort.'
|
||
},
|
||
{
|
||
name: 'nofuse-nosandbox-direct-binary-skills',
|
||
expectStatus: 0,
|
||
expectOutput: 'Usage: orca skills',
|
||
why: 'A direct binary launch that reaches JavaScript must run the command, not boot a GUI (#14229).'
|
||
},
|
||
{
|
||
name: 'stale-display-nosandbox-direct-binary-gui',
|
||
expectStatus: 1,
|
||
expectOutput: 'needs a usable display server',
|
||
why: 'A stale DISPLAY value must diagnose the unreachable endpoint instead of dying in uv_close (#13719).'
|
||
}
|
||
]
|
||
|
||
try {
|
||
if (!appImage) {
|
||
fail(
|
||
'Usage: run-linux-cli-launch-contract-docker.mjs --appimage /path/to/orca-linux.AppImage [--platform linux/amd64|linux/arm64]'
|
||
)
|
||
}
|
||
if (commandArgs.includes('--platform') && !platform) {
|
||
fail('Missing value for --platform')
|
||
}
|
||
if (platform !== null && platform !== 'linux/amd64' && platform !== 'linux/arm64') {
|
||
fail(`Unsupported --platform: ${platform}`)
|
||
}
|
||
if (!existsSync(appImage)) {
|
||
fail(`AppImage not found: ${appImage}`)
|
||
}
|
||
docker(['volume', 'create', artifactVolume], { timeoutMs: DOCKER_TIMEOUT_MS })
|
||
artifactVolumeCreated = true
|
||
buildImage()
|
||
stageArtifacts()
|
||
runContract()
|
||
console.log('\nLinux CLI launch contract passed.')
|
||
} catch (error) {
|
||
console.error(error instanceof Error ? error.message : String(error))
|
||
process.exitCode = 1
|
||
} finally {
|
||
for (const container of containers) {
|
||
docker(['rm', '-f', container], { allowFailure: true, timeoutMs: CLEANUP_TIMEOUT_MS })
|
||
}
|
||
if (artifactVolumeCreated) {
|
||
docker(['volume', 'rm', artifactVolume], {
|
||
allowFailure: true,
|
||
timeoutMs: CLEANUP_TIMEOUT_MS
|
||
})
|
||
}
|
||
docker(['image', 'rm', tag], { allowFailure: true, timeoutMs: CLEANUP_TIMEOUT_MS })
|
||
}
|
||
|
||
function runContract() {
|
||
const failures = []
|
||
for (const testCase of CASES) {
|
||
const output = runCase(testCase.name)
|
||
const statusMatch = /^RESULT status=(\d+)/m.exec(output)
|
||
if (!statusMatch) {
|
||
failures.push(`${testCase.name}: ${firstLine(output)}\n ${testCase.why}`)
|
||
console.log(` FAIL ${testCase.name} — ${firstLine(output)}`)
|
||
continue
|
||
}
|
||
const status = Number(statusMatch[1])
|
||
// Avoid asserting harness’s RESULT case-name echo: expected substring must come from CLI output.
|
||
const commandOutput = output
|
||
.split('\n')
|
||
.filter((line) => !/^(?:RESULT|CRASHED|PRECONDITION_FAILED) /.test(line))
|
||
.join('\n')
|
||
const matchesOutput =
|
||
typeof testCase.expectOutput === 'string'
|
||
? commandOutput.includes(testCase.expectOutput)
|
||
: testCase.expectOutput.test(commandOutput)
|
||
if (status !== testCase.expectStatus || !matchesOutput) {
|
||
failures.push(
|
||
`${testCase.name}: expected status ${testCase.expectStatus} and ${testCase.expectOutput}, ` +
|
||
`got status ${status}\n ${testCase.why}`
|
||
)
|
||
console.log(` FAIL ${testCase.name} — status ${status}`)
|
||
continue
|
||
}
|
||
console.log(` ok ${testCase.name} (status ${status})`)
|
||
}
|
||
if (failures.length > 0) {
|
||
fail(`Linux CLI launch contract failed:\n - ${failures.join('\n - ')}`)
|
||
}
|
||
}
|
||
|
||
function runCase(caseName) {
|
||
const container = `orca-cli-contract-${caseName}-${suffix}`
|
||
containers.add(container)
|
||
// FUSE and extra capabilities would invalidate the test conditions.
|
||
return docker(
|
||
[
|
||
'run',
|
||
...dockerPlatformArgs,
|
||
'--name',
|
||
container,
|
||
'--rm',
|
||
'-v',
|
||
`${artifactVolume}:/artifacts`,
|
||
tag,
|
||
caseName
|
||
],
|
||
{ allowFailure: true, capture: true, timeoutMs: CASE_TIMEOUT_MS }
|
||
)
|
||
}
|
||
|
||
function buildImage() {
|
||
console.log(`Building ${tag}…`)
|
||
const buildArgs = [
|
||
'build',
|
||
...dockerPlatformArgs,
|
||
...(process.env.ORCA_CLI_FIXTURE_CACHE_IMAGE
|
||
? ['--cache-from', process.env.ORCA_CLI_FIXTURE_CACHE_IMAGE]
|
||
: []),
|
||
'-f',
|
||
'config/docker/cli-launch-contract/Dockerfile',
|
||
'-t',
|
||
tag,
|
||
'config/docker/cli-launch-contract'
|
||
]
|
||
// Why: apt fetches from archive.ubuntu.com stall or fail mid-sync; a second build usually lands on a healthy index.
|
||
try {
|
||
docker(buildArgs, { timeoutMs: BUILD_TIMEOUT_MS })
|
||
} catch (error) {
|
||
console.error(
|
||
`${error instanceof Error ? error.message : String(error)}\nRetrying docker build once…`
|
||
)
|
||
docker(buildArgs, { timeoutMs: BUILD_TIMEOUT_MS })
|
||
}
|
||
}
|
||
|
||
// Extract unprivileged so chrome-sandbox is not root-owned setuid.
|
||
function stageArtifacts() {
|
||
console.log('Staging the AppImage payload…')
|
||
const container = `orca-cli-contract-stage-${suffix}`
|
||
containers.add(container)
|
||
docker(
|
||
[
|
||
'run',
|
||
...dockerPlatformArgs,
|
||
'--name',
|
||
container,
|
||
'--rm',
|
||
'-v',
|
||
`${artifactVolume}:/artifacts`,
|
||
'-v',
|
||
`${appImage}:/input/orca-linux.AppImage:ro`,
|
||
'--entrypoint',
|
||
'bash',
|
||
tag,
|
||
'-lc',
|
||
[
|
||
'set -euo pipefail',
|
||
'cp /input/orca-linux.AppImage /artifacts/orca-linux.AppImage',
|
||
'chmod +x /artifacts/orca-linux.AppImage',
|
||
'chown -R orca:orca /artifacts',
|
||
// Use the AppImage runtime's no-FUSE extraction path.
|
||
'cd /artifacts && runuser --user orca -- ./orca-linux.AppImage --appimage-extract >/dev/null',
|
||
'test -x /artifacts/squashfs-root/resources/bin/orca-ide'
|
||
].join(' && ')
|
||
],
|
||
{ timeoutMs: STAGING_TIMEOUT_MS }
|
||
)
|
||
}
|
||
|
||
function docker(args, options = {}) {
|
||
try {
|
||
const output = execFileSync('docker', args, {
|
||
encoding: 'utf8',
|
||
stdio: options.capture ? ['ignore', 'pipe', 'pipe'] : 'inherit',
|
||
timeout: options.timeoutMs ?? DOCKER_TIMEOUT_MS,
|
||
killSignal: 'SIGTERM'
|
||
})
|
||
return output ?? ''
|
||
} catch (error) {
|
||
const timedOut = error instanceof Error && 'code' in error && error.code === 'ETIMEDOUT'
|
||
if (timedOut) {
|
||
const message = `docker ${args.join(' ')} timed out after ${options.timeoutMs ?? DOCKER_TIMEOUT_MS}ms`
|
||
if (!options.allowFailure) {
|
||
fail(message)
|
||
}
|
||
return message
|
||
}
|
||
if (!options.allowFailure) {
|
||
fail(
|
||
`docker ${args.join(' ')} failed: ${error instanceof Error ? error.message : String(error)}`
|
||
)
|
||
}
|
||
return `${error?.stdout ?? ''}${error?.stderr ?? ''}`
|
||
}
|
||
}
|
||
|
||
function firstLine(value) {
|
||
return (value ?? '').trim().split('\n')[0] || '(no output)'
|
||
}
|
||
|
||
function valueAfter(flag) {
|
||
const index = commandArgs.indexOf(flag)
|
||
return index === -1 ? null : (commandArgs[index + 1] ?? null)
|
||
}
|
||
|
||
function fail(message) {
|
||
throw new Error(message)
|
||
}
|