orca/config/scripts/computer-use-smoke.mjs

130 lines
4.1 KiB
JavaScript

#!/usr/bin/env node
import { existsSync } from 'node:fs'
import { homedir } from 'node:os'
import { resolve } from 'node:path'
import { spawnSync } from 'node:child_process'
const repoRoot = resolve(import.meta.dirname, '..', '..')
const cliPath = resolve(repoRoot, 'out', 'cli', 'index.js')
const args = new Set(process.argv.slice(2))
const requestedApps = valueFlag('--apps')
const includeScreenshot = args.has('--screenshot')
const session = valueFlag('--session') ?? `computer-smoke-${process.pid}`
const preferredApps = (requestedApps ?? process.env.ORCA_COMPUTER_SMOKE_APPS ?? 'Finder,TextEdit,Spotify,Slack,Microsoft Edge')
.split(',')
.map((app) => app.trim())
.filter(Boolean)
if (!existsSync(cliPath)) {
fail(`Missing built CLI at ${cliPath}. Run pnpm build:cli first.`)
}
const list = unwrapResult(runCli(['computer', 'list-apps', '--json']))
const apps = Array.isArray(list.apps) ? list.apps : []
const availableNames = new Set(apps.map((app) => String(app.name ?? '').toLowerCase()))
const availableBundles = new Set(apps.map((app) => String(app.bundleId ?? '').toLowerCase()).filter(Boolean))
const targets = preferredApps.filter((app) => availableNames.has(app.toLowerCase()) || availableBundles.has(app.toLowerCase()))
console.log(`computer-use smoke: ${apps.length} apps listed`)
if (targets.length === 0) {
console.log(`computer-use smoke: no preferred apps are running (${preferredApps.join(', ')})`)
process.exit(0)
}
let failures = 0
for (const app of targets) {
const result = runCli([
'computer',
'get-app-state',
'--session',
session,
'--app',
app,
...(includeScreenshot ? [] : ['--no-screenshot']),
'--json'
], { allowFailure: true })
if (!result.ok) {
failures += 1
console.log(`computer-use smoke: ${app}: failed: ${result.error}`)
continue
}
const state = unwrapResult(result.value)
const snapshot = state.snapshot
const treeText = String(snapshot.treeText ?? '')
const lineCount = treeText.split('\n').filter(Boolean).length
const secondaryActions = (treeText.match(/Secondary Actions:/g) ?? []).length
const settable = (treeText.match(/\bsettable\b/g) ?? []).length
const screenshotState = state.screenshot ? `${state.screenshot.width}x${state.screenshot.height}` : 'missing'
console.log([
`computer-use smoke: ${snapshot.app.name}`,
`${snapshot.elementCount} elements`,
`${lineCount} lines`,
`${secondaryActions} secondary-action lines`,
`${settable} settable elements`,
`screenshot=${screenshotState}`
].join(' | '))
}
if (failures > 0) {
fail(`${failures} app snapshot smoke check(s) failed`)
}
function valueFlag(name) {
const index = process.argv.indexOf(name)
if (index === -1) {
return null
}
return process.argv[index + 1] ?? null
}
function runCli(cliArgs, options = {}) {
const child = spawnSync(process.execPath, [cliPath, ...cliArgs], {
cwd: repoRoot,
encoding: 'utf8',
env: {
...process.env,
ORCA_USER_DATA_PATH: process.env.ORCA_COMPUTER_SMOKE_USER_DATA_PATH ?? defaultDevUserDataPath()
}
})
if (child.status !== 0) {
const error = (child.stderr || child.stdout || `exit ${child.status}`).trim()
if (options.allowFailure) {
return { ok: false, error }
}
fail(error)
}
try {
return options.allowFailure
? { ok: true, value: JSON.parse(child.stdout) }
: JSON.parse(child.stdout)
} catch (error) {
const detail = error instanceof Error ? error.message : String(error)
fail(`Could not parse CLI JSON for ${cliArgs.join(' ')}: ${detail}\n${child.stdout}`)
}
}
function defaultDevUserDataPath() {
if (process.platform === 'darwin') {
return resolve(homedir(), 'Library', 'Application Support', 'orca-dev')
}
if (process.platform === 'win32') {
return resolve(process.env.APPDATA ?? resolve(homedir(), 'AppData', 'Roaming'), 'orca-dev')
}
return resolve(process.env.XDG_CONFIG_HOME ?? resolve(homedir(), '.config'), 'orca-dev')
}
function unwrapResult(value) {
if (value && typeof value === 'object' && 'result' in value) {
return value.result
}
return value
}
function fail(message) {
console.error(`computer-use smoke: ${message}`)
process.exit(1)
}