blob: e26128062fe4bb491813ae07c7f5e144f410e261 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { _electron as electron, chromium } from '@playwright/test';
import { execFile, spawn } from 'node:child_process';
import { randomUUID } from 'node:crypto';
import { promisify } from 'node:util';
import { cp, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import { createServer } from 'node:net';
import { homedir, tmpdir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { evaluateCuE2eScenarioState, getCuE2eScenario } from './e2e-scenarios.mjs';
import { validateRealReport } from './provider-matrix.mjs';
import { sanitizeCuActionRecord, sanitizeCuReport, sanitizeCuTrace } from './report-sanitize.mjs';
import { createSqliteAgentRunStore } from '../../packages/storage/dist/agent-run-store.js';
import {
resolveStorageRoot,
tryAcquireInteractiveRootOwner,
} from '../../packages/storage/dist/root-authority.js';
import { openInteractiveRuntimePolicyStoresForWrite } from '../../packages/storage/dist/runtime-policy-stores.js';
const here = dirname(fileURLToPath(import.meta.url));
const repoRoot = join(here, '../..');
const execFileAsync = promisify(execFile);
const sourceWorkspace = join(
homedir(),
'Library',
'Application Support',
'Maka',
'workspaces',
'default',
);
const scenario = getCuE2eScenario(process.env.MAKA_CU_E2E_SCENARIO ?? 'l0-observe-only');
if (!scenario.realRunEnabled) {
throw new Error(`scenario ${scenario.id} is not enabled for real-model runs`);
}
if (scenario.runner) {
throw new Error(`scenario ${scenario.id} requires dedicated runner ${scenario.runner}`);
}
const availableCapabilities = new Set(
String(process.env.MAKA_CU_EXECUTION_CAPABILITIES ?? '')
.split(',')
.map((value) => value.trim())
.filter(Boolean),
);
for (const capability of scenario.requiresExecutionCapabilities) {
if (!availableCapabilities.has(capability)) {
throw new Error(`scenario ${scenario.id} requires unavailable capability ${capability}`);
}
}
const timeoutMs = Number(process.env.MAKA_CU_REAL_MODEL_TIMEOUT_MS ?? 180_000);
const keepProfile = process.env.MAKA_CU_KEEP_PROFILE === '1';
const providerOverride = process.env.MAKA_CU_PROVIDER;
const reportPath =
process.env.MAKA_CU_REAL_MODEL_REPORT ??
join(repoRoot, '.agents-workspace-data', 'cu-real-model', `report-${Date.now()}.json`);
const runPrompt = 'Use the maka_computer tool to complete this task. ' + scenario.prompt;
async function reportLineage() {
const { stdout } = await execFileAsync('git', ['rev-parse', 'HEAD'], {
cwd: repoRoot,
});
const generatedAt = new Date().toISOString();
const gitRevision = stdout.trim();
return {
runId: randomUUID(),
gitRevision,
generatedAt,
contentLineage: {
generator: 'scripts/computer-use/real-model.mjs',
gitRevision,
generatedAt,
},
};
}
async function reservePort() {
const server = createServer();
await new Promise((resolve, reject) => {
server.once('error', reject);
server.listen(0, '127.0.0.1', resolve);
});
const address = server.address();
const port = typeof address === 'object' && address ? address.port : 0;
await new Promise((resolve) => server.close(resolve));
if (!port) throw new Error('failed to reserve loopback port');
return port;
}
async function copyProfileFile(name, workspace) {
await cp(join(sourceWorkspace, name), join(workspace, name), {
errorOnExist: true,
});
}
async function copyProfileFileIfPresent(name, workspace) {
try {
await copyProfileFile(name, workspace);
} catch (error) {
if (error && error.code === 'ENOENT') return;
throw error;
}
}
async function prepareProviderProfile(workspace) {
if (!providerOverride) {
await copyProfileFile('connection-catalog.json', workspace);
await Promise.all([
copyProfileFileIfPresent('credential-vault.json', workspace),
copyProfileFileIfPresent('credentials.json', workspace),
copyProfileFile('settings.json', workspace),
]);
return;
}
if (providerOverride !== 'openai') {
throw new Error(`unsupported MAKA_CU_PROVIDER ${providerOverride}`);
}
await copyProfileFile('settings.json', workspace);
const modelId = process.env.MAKA_CU_OPENAI_MODEL ?? 'gpt-5.4';
const capability = await resolveStorageRoot({ path: workspace, kind: 'interactive' });
const owner = await tryAcquireInteractiveRootOwner(capability);
if (!owner) throw new Error('Unable to acquire the Computer Use catalog root');
try {
const stores = await openInteractiveRuntimePolicyStoresForWrite(owner.lease);
const created = await stores.connectionCatalog.create({
expectedCatalogRevision: 0,
connection: {
slug: 'cu-real-openai',
name: 'Computer Use real-model OpenAI',
providerType: 'openai',
baseUrl: process.env.MAKA_CU_OPENAI_BASE_URL ?? 'http://127.0.0.1:8538/v1',
enabled: true,
enabledModelIds: [modelId],
},
});
if (created.kind !== 'committed') {
throw new Error(`Computer Use catalog create failed: ${created.kind}`);
}
const connection = created.snapshot.connections.find((item) => item.slug === 'cu-real-openai');
if (!connection) throw new Error('Computer Use catalog create omitted cu-real-openai');
const vault = await stores.credentialVault.set({
locator: {
scope: 'connection',
connectionId: connection.connectionId,
kind: 'api_key',
},
expected: null,
secret: process.env.MAKA_CU_OPENAI_API_KEY ?? 'local-bridge',
});
if (vault.kind !== 'committed') {
throw new Error(`Computer Use vault set failed: ${vault.kind}`);
}
const defaulted = await stores.connectionCatalog.setDefaultTarget({
expectedCatalogRevision: created.snapshot.revision,
target: { connectionId: connection.connectionId, modelId },
});
if (defaulted.kind !== 'committed') {
throw new Error(`Computer Use default target failed: ${defaulted.kind}`);
}
} finally {
await owner.close();
}
}
async function waitForLine(child, marker, timeout) {
return new Promise((resolve, reject) => {
let stdout = '';
const timer = setTimeout(() => reject(new Error(`timed out waiting for ${marker}`)), timeout);
const onData = (chunk) => {
stdout += String(chunk);
process.stdout.write(chunk);
const matched =
marker === 'CU_FIXTURE_READY'
? /^CU_FIXTURE_READY\s+\d+\s*$/m.test(stdout)
: stdout.includes(marker);
if (matched) {
clearTimeout(timer);
child.stdout.off('data', onData);
resolve(stdout);
}
};
child.stdout.on('data', onData);
child.stderr.on('data', (chunk) => process.stderr.write(chunk));
child.once('error', reject);
child.once('exit', (code, signal) => {
clearTimeout(timer);
reject(new Error(`fixture exited before ready: ${signal ?? `code ${code}`}`));
});
});
}
export function parseFixtureReady(text, childPid) {
const matches =
typeof text === 'string' ? [...text.matchAll(/^CU_FIXTURE_READY\s+(\d+)\s*$/gm)] : [];
if (matches.length !== 1) {
throw new Error('fixture READY identity is missing or ambiguous');
}
const readyPid = Number(matches[0][1]);
if (!Number.isInteger(childPid) || childPid <= 0 || readyPid !== childPid) {
throw new Error(`fixture READY pid ${readyPid} does not match launcher child pid ${childPid}`);
}
return readyPid;
}
export function parseTargetEvidence(text) {
if (typeof text !== 'string') return undefined;
const candidates = [text];
for (const markerText of ['Fresh observation:\n', 'Fresh observation: ']) {
const marker = text.lastIndexOf(markerText);
if (marker >= 0) candidates.push(text.slice(marker + markerText.length));
}
for (const candidate of candidates.reverse()) {
try {
const value = JSON.parse(candidate);
if (
value &&
typeof value === 'object' &&
Number.isInteger(value.pid) &&
Number.isInteger(value.window_id)
) {
return {
...(typeof value.observation_id === 'string'
? { observationId: value.observation_id }
: {}),
pid: value.pid,
windowId: value.window_id,
};
}
} catch {
// Continue to the next privacy-safe projection candidate.
}
}
return undefined;
}
export function actionRecords(events) {
const starts = new Map();
const records = [];
for (const event of events) {
if (event.type === 'tool_start' && event.toolName === 'maka_computer') {
starts.set(event.toolUseId, event);
}
if (event.type === 'tool_result' && starts.has(event.toolUseId)) {
const start = starts.get(event.toolUseId);
const text = event.content?.kind === 'text' ? event.content.text : undefined;
const resultCode =
typeof text === 'string'
? text.match(/\bfailed:\s*([a-z][a-z0-9_]{1,63})\b/i)?.[1]
: undefined;
const target = parseTargetEvidence(text);
records.push(
sanitizeCuActionRecord({
action: start.args,
toolCallId: event.toolUseId,
sourceObservationId: start.args?.observation_id,
resultObservationId: target?.observationId,
targetPid: target?.pid,
targetWindowId: target?.windowId,
durationMs: event.durationMs,
text,
success: event.isError === false && !resultCode,
}),
);
}
}
return records;
}
export async function waitForTraceFlush(path, expectedToolCallIds, timeoutMs = 2_000) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const current = await readFile(path, 'utf8').catch((error) => {
if (error?.code === 'ENOENT') return '';
throw error;
});
const traces = [];
let incompleteTrace = false;
for (const line of current.split('\n').filter(Boolean)) {
try {
traces.push(JSON.parse(line));
} catch (error) {
if (!(error instanceof SyntaxError)) throw error;
incompleteTrace = true;
}
}
const observedToolCallIds = new Set(
traces.filter((trace) => trace.type === 'dispatch').map((trace) => trace.toolCallId),
);
if (
!incompleteTrace &&
expectedToolCallIds.every((toolCallId) => observedToolCallIds.has(toolCallId))
) {
return traces;
}
await new Promise((resolve) => setTimeout(resolve, 50));
}
throw new Error(
`timed out waiting for Computer Use dispatch traces: ${expectedToolCallIds.join(',')}`,
);
}
export async function discoverFixtureIdentity(
fixturePid,
windowSpecs,
{ listApps, timeoutMs = 2_000, pollIntervalMs = 50 } = {},
) {
if (!Number.isInteger(fixturePid) || fixturePid <= 0) {
throw new Error('fixture discovery requires a valid launcher-owned pid');
}
if (typeof listApps !== 'function') {
throw new Error('fixture discovery requires an independent window lister');
}
const expectedTitles = windowSpecs?.map((window) => window.title);
if (
!Array.isArray(expectedTitles) ||
expectedTitles.length === 0 ||
expectedTitles.some((title) => typeof title !== 'string' || title.length === 0) ||
new Set(expectedTitles).size !== expectedTitles.length
) {
throw new Error('fixture discovery requires unique expected window titles');
}
const deadline = Date.now() + timeoutMs;
let lastFailure = 'fixture app was not found';
while (Date.now() < deadline) {
const apps = await listApps();
const fixtureApps = Array.isArray(apps)
? apps.filter((app) => Number(app?.pid) === fixturePid)
: [];
if (fixtureApps.length === 1) {
const windows = Array.isArray(fixtureApps[0].windows) ? fixtureApps[0].windows : [];
const windowIds = [];
let complete = true;
for (const title of expectedTitles) {
const matches = windows.filter(
(window) =>
window?.title === title && Number.isInteger(window?.windowId) && window.windowId > 0,
);
if (matches.length !== 1) {
complete = false;
lastFailure = `expected one fixture window "${title}", got ${matches.length}`;
break;
}
windowIds.push(matches[0].windowId);
}
if (complete && new Set(windowIds).size === windowIds.length) {
return {
instances: [
{
pid: fixturePid,
windowIds,
},
],
};
}
if (complete) lastFailure = 'fixture discovery returned duplicate window ids';
} else {
lastFailure = `expected one fixture app for pid ${fixturePid}, got ${fixtureApps.length}`;
}
await new Promise((resolvePromise) => setTimeout(resolvePromise, pollIntervalMs));
}
throw new Error(`fixture identity discovery failed: ${lastFailure}`);
}
async function discoverLauncherFixtureIdentity(fixturePid, windowSpecs) {
const [{ createMakaCuBackend }, manifestText] = await Promise.all([
import('../../packages/computer-use/dist/index.js'),
readFile(join(repoRoot, 'apps', 'desktop', 'bundled-tools.json'), 'utf8'),
]);
const manifest = JSON.parse(manifestText);
const expectedBinarySha256 = manifest?.makaCu?.binarySha256;
if (typeof expectedBinarySha256 !== 'string' || !/^[a-f0-9]{64}$/.test(expectedBinarySha256)) {
throw new Error('fixture discovery cannot verify bundled maka-cu identity');
}
const backend = createMakaCuBackend({
binaryPath: join(repoRoot, 'apps', 'desktop', 'resources', 'bin', 'maka-cu'),
expectedBinarySha256,
timeoutMs: 10_000,
});
try {
return await discoverFixtureIdentity(fixturePid, windowSpecs, {
listApps: () => backend.listApps(new AbortController().signal),
timeoutMs: 5_000,
});
} finally {
backend.dispose();
}
}
function safeEvent(event) {
if (event.type === 'tool_start') {
const safeToolName =
event.toolName === 'tool_search' || event.toolName === 'maka_computer'
? event.toolName
: 'other';
return {
type: event.type,
toolName: safeToolName,
...(event.toolName === 'maka_computer'
? { actionType: event.args?.action ?? 'unknown' }
: {}),
ts: event.ts,
};
}
if (event.type === 'tool_result') {
return {
type: event.type,
isError: event.isError,
durationMs: event.durationMs,
ts: event.ts,
};
}
if (event.type === 'complete' || event.type === 'abort' || event.type === 'error') {
const safeCode = [event.code, event.reason].find(
(value) => typeof value === 'string' && /^[a-zA-Z][a-zA-Z0-9_.-]{0,63}$/.test(value),
);
return {
type: event.type,
...(event.stopReason ? { stopReason: event.stopReason } : {}),
...(safeCode ? { code: safeCode } : {}),
ts: event.ts,
};
}
return null;
}
function safeFailureMetadata(message) {
if (typeof message !== 'string') return undefined;
const status = message.match(/\b([45]\d\d)\b/)?.[1];
const errorName = message.match(/\b([A-Z][A-Za-z]+Error)\b/)?.[1];
const providerType = message
.match(
/\b(api_error|authentication_error|billing_error|invalid_request_error|overloaded_error|permission_error|rate_limit_error)\b/i,
)?.[1]
?.toLowerCase();
const result = {
...(status ? { httpStatus: Number(status) } : {}),
...(errorName ? { errorName } : {}),
...(providerType ? { providerErrorType: providerType } : {}),
};
return Object.keys(result).length > 0 ? result : undefined;
}
async function run() {
const lineage = await reportLineage();
const userData = await mkdtemp(join(tmpdir(), 'maka-cu-real-model-'));
const workspace = join(userData, 'workspaces', 'default');
const tracePath = join(userData, 'computer-use-trace.jsonl');
const fixturePort = await reservePort();
const electronBinary = join(repoRoot, 'node_modules', '.bin', 'electron');
let fixture;
let desktop;
let fixtureBrowser;
try {
await mkdir(workspace, { recursive: true });
await mkdir(dirname(reportPath), { recursive: true });
await prepareProviderProfile(workspace);
fixture = spawn(
electronBinary,
[
`--remote-debugging-port=${fixturePort}`,
'--remote-allow-origins=*',
join(here, 'real-model-fixture.mjs'),
],
{
cwd: repoRoot,
env: {
...process.env,
MAKA_CU_E2E_SCENARIO: scenario.id,
},
stdio: ['ignore', 'pipe', 'pipe'],
},
);
const readyOutput = await waitForLine(fixture, 'CU_FIXTURE_READY', 30_000);
const fixturePid = parseFixtureReady(readyOutput, fixture.pid);
const fixtureIdentity = await discoverLauncherFixtureIdentity(
fixturePid,
activeWindowSpecs(scenario),
);
desktop = await electron.launch({
args: ['apps/desktop'],
cwd: repoRoot,
env: {
...process.env,
MAKA_CU_REAL_MODEL_E2E: '1',
MAKA_E2E_USER_DATA_DIR: userData,
MAKA_CU_REAL_MODEL_POLICY: JSON.stringify({
allowedActions: scenario.allowedActions,
maxTotalActions: scenario.maxTotalActions,
maxActionCounts:
scenario.maxActionCounts ??
Object.fromEntries(
scenario.allowedActions.map((action) => [action, scenario.maxTotalActions]),
),
allowedApps: activeWindowSpecs(scenario).map((window) => window.title),
}),
MAKA_CU_REAL_MODEL_TRACE: tracePath,
},
timeout: 30_000,
});
const page = await desktop.firstWindow();
await page.waitForFunction(() => Boolean(window.maka?.sessions));
const runResult = await page.evaluate(
async ({ prompt, timeout }) => {
const connections = await window.maka.connections.list();
const defaultSlug = await window.maka.connections.getDefault();
const connection = connections.find((entry) => entry.slug === defaultSlug);
if (!connection?.defaultModel) {
throw new Error('isolated profile has no ready default model');
}
const session = await window.maka.sessions.create({
backend: 'ai-sdk',
llmConnectionSlug: connection.slug,
model: connection.defaultModel,
permissionMode: 'bypass',
name: 'Computer Use real-model E2E',
labels: ['computer-use', 'real-model-e2e'],
});
const events = [];
const terminal = new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error('real-model turn timed out')), timeout);
const unsubscribe = window.maka.sessions.subscribeEvents(session.id, (event) => {
events.push(event);
if (event.type === 'complete' || event.type === 'abort' || event.type === 'error') {
clearTimeout(timer);
unsubscribe();
resolve(event);
}
});
});
const turnId = crypto.randomUUID();
await window.maka.sessions.send(session.id, {
type: 'send',
turnId,
text: prompt,
});
const terminalEvent = await terminal;
return {
connection: {
slug: connection.slug,
providerType: connection.providerType,
model: connection.defaultModel,
},
sessionId: session.id,
turnId,
events,
terminalEvent,
};
},
{ prompt: runPrompt, timeout: timeoutMs },
);
fixtureBrowser = await chromium.connectOverCDP(`http://127.0.0.1:${fixturePort}`);
const fixturePages = fixtureBrowser.contexts().flatMap((context) => context.pages());
const fixtureState = {};
for (const windowSpec of activeWindowSpecs(scenario)) {
const pageForTitle = await Promise.all(
fixturePages.map(async (candidate) => ({
candidate,
title: await candidate.title(),
})),
).then((entries) => entries.find((entry) => entry.title === windowSpec.title)?.candidate);
if (!pageForTitle) throw new Error(`missing fixture page ${windowSpec.title}`);
fixtureState[windowSpec.id] = await pageForTitle.evaluate(
() => globalThis.__makaCuFixtureState?.() ?? null,
);
}
const evaluation = evaluateCuE2eScenarioState(scenario, fixtureState);
const events = runResult.events.map(safeEvent).filter(Boolean);
const rawActions = actionRecords(runResult.events);
const expectedDispatchToolCallIds = rawActions
.filter(
(action) =>
action.success === true &&
!['list_apps', 'observe', 'screenshot', 'cursor_position', 'wait'].includes(action.type),
)
.map((action) => action.toolCallId)
.filter((toolCallId) => typeof toolCallId === 'string');
const driverTraces = await waitForTraceFlush(tracePath, expectedDispatchToolCallIds);
const actions = bindActionTargets(rawActions, driverTraces, fixtureIdentity);
const runStore = createSqliteAgentRunStore(workspace);
await runStore.ready?.();
let runHeader;
try {
runHeader = await waitForRunHeader(runStore, runResult.sessionId, runResult.turnId);
} finally {
runStore.close?.();
}
const qualifyingActions = actions.filter(
(action) =>
action.success === true &&
action.targetOwned === true &&
scenario.allowedActions.includes(action.type),
);
const actionCounts = Object.fromEntries(
actions.reduce((counts, action) => {
counts.set(action.type, (counts.get(action.type) ?? 0) + 1);
return counts;
}, new Map()),
);
const qualifyingActionCounts = Object.fromEntries(
scenario.allowedActions.map((action) => [
action,
qualifyingActions.filter((record) => record.type === action).length,
]),
);
const minimumActionsPassed = Object.entries(scenario.minimumActionCounts ?? {}).every(
([action, minimum]) => (qualifyingActionCounts[action] ?? 0) >= minimum,
);
const terminalPassed =
runResult.terminalEvent.type === 'complete' &&
runResult.terminalEvent.stopReason === 'end_turn';
const actionsWithinBudget =
actions.length <= scenario.maxTotalActions &&
actions.every((action) => scenario.allowedActions.includes(action.type)) &&
Object.entries(scenario.maxActionCounts ?? {}).every(
([action, maximum]) => actions.filter((record) => record.type === action).length <= maximum,
);
const dispatchPathPassed = requiredDispatchPathPassed(scenario, driverTraces);
const ownershipPassed = allActionTargetsOwned(actions);
const localChecksPassed =
terminalPassed &&
minimumActionsPassed &&
actionsWithinBudget &&
dispatchPathPassed &&
ownershipPassed &&
evaluation.pass;
const reportInput = {
schemaVersion: 1,
...lineage,
evidenceClass: 'real-runtime',
policyMode: 'bypassed',
qualificationEligible: true,
toolExposure: 'direct-e2e',
scenarioId: scenario.id,
producer: 'computer-use/real-model',
transportClass: 'live-network',
provider: runResult.connection.providerType,
model: runResult.connection.model,
fixtureIdentity,
terminal: safeEvent(runResult.terminalEvent),
run: runHeader
? {
status: runHeader.status,
failureClass: runHeader.failureClass,
failure: safeFailureMetadata(runHeader.failureMessage),
durationMs:
runHeader.completedAt !== undefined
? Math.max(0, runHeader.completedAt - runHeader.createdAt)
: undefined,
}
: undefined,
actionAttempts: actions.length,
actionCount: actions.length,
actionCounts,
minimumActionsPassed,
actionsWithinBudget,
dispatchPathPassed,
actions,
fixtureState,
expectedState: evaluation.expected,
forbiddenEffects: {
status: evaluation.forbidden.every((entry) => entry.pass) ? 'pass' : 'fail',
violations: evaluation.forbidden.filter((entry) => !entry.pass),
},
status: localChecksPassed
? 'pass'
: terminalPassed && minimumActionsPassed
? 'fail'
: 'inconclusive',
traces: events.map(sanitizeCuTrace).filter(Boolean),
driverTraces: driverTraces.map(sanitizeCuTrace).filter(Boolean),
};
const provider = {
id: runResult.connection.providerType,
producer: 'computer-use/real-model',
model: runResult.connection.model,
};
const provisionalReport = sanitizeCuReport(reportInput);
const qualificationErrors = validateRealReport(
{ ...provisionalReport, status: 'pass' },
provider,
scenario,
);
const report = sanitizeCuReport({
...reportInput,
status: qualificationErrors.length === 0 ? 'pass' : 'fail',
});
const validationErrors = validateRealReport(report, provider, scenario);
await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`, {
flag: 'wx',
mode: 0o600,
});
process.stdout.write(`Real-model Computer Use report: ${reportPath}\n`);
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
if (validationErrors.length > 0) {
process.stderr.write(
`Real-model Computer Use qualification failed: ${validationErrors.join('; ')}\n`,
);
process.exitCode = 1;
}
} finally {
await fixtureBrowser?.close().catch(() => {});
await desktop?.close().catch(() => {});
if (fixture && fixture.exitCode === null) fixture.kill('SIGTERM');
await new Promise((resolve) => setTimeout(resolve, 250));
if (fixture && fixture.exitCode === null) fixture.kill('SIGKILL');
if (keepProfile) {
process.stderr.write(`Retained isolated debug profile: ${userData}\n`);
} else {
await rm(userData, { recursive: true, force: true });
}
}
}
async function handleRunFailure(error) {
const lineage = await reportLineage().catch(() => undefined);
const failure = sanitizeCuReport({
schemaVersion: 1,
...lineage,
evidenceClass: 'real-runtime',
scenarioId: scenario.id,
producer: 'computer-use/real-model',
status: 'inconclusive',
terminal: { type: 'error' },
run: {
status: 'failed',
failureClass:
typeof error?.code === 'string'
? error.code
: error instanceof Error
? error.name
: 'unknown',
},
});
await mkdir(dirname(reportPath), { recursive: true }).catch(() => {});
await writeFile(reportPath, `${JSON.stringify(failure, null, 2)}\n`, {
flag: 'wx',
mode: 0o600,
}).catch(() => {});
console.error('Real-model Computer Use E2E failed');
process.exitCode = 1;
}
function requiredDispatchPathPassed(scenario, traces) {
const mutationActions = scenario.allowedActions.filter(
(action) => !['list_apps', 'observe', 'screenshot', 'cursor_position', 'wait'].includes(action),
);
if (mutationActions.length === 0) return true;
return traces.some(
(trace) =>
trace.type === 'dispatch' && (trace.address === 'ax' || trace.address === 'semantic'),
);
}
function fixtureOwnsTarget(fixtureIdentity, pid, windowId) {
return (
fixtureIdentity?.instances?.some(
(instance) => instance.pid === pid && instance.windowIds?.includes(windowId),
) === true
);
}
export function bindActionTargets(actions, traces, fixtureIdentity) {
const dispatches = traces.filter(
(trace) =>
trace.type === 'dispatch' && (trace.address === 'ax' || trace.address === 'semantic'),
);
const observationTargets = new Map(
actions.flatMap((action) =>
(action.type === 'observe' || action.type === 'screenshot') &&
typeof action.resultObservationId === 'string' &&
Number.isInteger(action.targetPid) &&
Number.isInteger(action.targetWindowId)
? [
[
action.resultObservationId,
{
pid: action.targetPid,
windowId: action.targetWindowId,
},
],
]
: [],
),
);
const consumed = new Set();
return actions.map((action) => {
if (['list_apps', 'wait', 'cursor_position'].includes(action.type)) {
return { ...action, targetOwned: false };
}
const index = dispatches.findIndex(
(trace, traceIndex) =>
!consumed.has(traceIndex) &&
trace.toolCallId === action.toolCallId &&
trace.actionType === action.type,
);
if (index >= 0) consumed.add(index);
const dispatch = index >= 0 ? dispatches[index] : undefined;
const sourceTarget =
typeof action.sourceObservationId === 'string'
? observationTargets.get(action.sourceObservationId)
: undefined;
const directObservationTarget =
(action.type === 'observe' || action.type === 'screenshot') &&
Number.isInteger(action.targetPid) &&
Number.isInteger(action.targetWindowId)
? { pid: action.targetPid, windowId: action.targetWindowId }
: undefined;
const target = dispatch
? { pid: dispatch.pid, windowId: dispatch.windowId }
: sourceTarget
? sourceTarget
: directObservationTarget;
return {
...action,
targetOwned: target ? fixtureOwnsTarget(fixtureIdentity, target.pid, target.windowId) : false,
...(target
? {
targetPid: target.pid,
targetWindowId: target.windowId,
}
: {}),
};
});
}
export function allActionTargetsOwned(actions) {
return actions.every(
(action) =>
['list_apps', 'wait', 'cursor_position'].includes(action.type) || action.targetOwned === true,
);
}
function activeWindowSpecs(scenario) {
const specs = new Map(scenario.fixtureSetup.windows.map((window) => [window.id, window]));
for (const transition of scenario.fixtureSetup.transitions ?? []) {
specs.delete(transition.removeWindowId);
specs.set(transition.addWindow.id, transition.addWindow);
}
return [...specs.values()];
}
async function waitForRunHeader(store, sessionId, turnId) {
const deadline = Date.now() + 2_000;
let latest;
do {
latest = (await store.listSessionRuns(sessionId)).find((entry) => entry.turnId === turnId);
if (
latest &&
latest.status !== 'created' &&
latest.status !== 'running' &&
latest.status !== 'waiting_for_user'
)
return latest;
await new Promise((resolve) => setTimeout(resolve, 50));
} while (Date.now() < deadline);
return latest;
}
if (process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url))) {
run().catch(handleRunFailure);
}