| import { readFile, writeFile } from 'node:fs/promises'; |
| import { tmpdir } from 'node:os'; |
| import { join, relative, resolve } from 'node:path'; |
| import { createCuaDriverBackend } from '../packages/computer-use/dist/index.js'; |
| import { buildComputerUseTools } from '../packages/runtime/dist/index.js'; |
| |
| const repoRoot = new URL('..', import.meta.url).pathname; |
| const binaryPath = join(repoRoot, 'apps/desktop/resources/bin/cua-driver'); |
| const labRoot = '/Users/haoqing/Documents/Learning/codex-computer-use-lab'; |
| const expectedAppPath = join(labRoot, 'test-app/build/Codex CUA Lab.app'); |
| const statePath = join(labRoot, 'test-app/runtime/state.json'); |
| const temporaryDirectory = process.env.MAKA_CU_RESTART_TEMP_DIR; |
| const oldPID = Number(process.env.MAKA_CU_RESTART_OLD_PID); |
| const soakRounds = Number(process.env.MAKA_CU_RESTART_SOAK_ROUNDS); |
| const expectedBinarySha256 = '683dad5cccb47dd0a8bb5d534d62fbb9e6edfb1cded232509cf4c2b190066040'; |
| |
| const resolvedTemporaryDirectory = resolve(temporaryDirectory ?? ''); |
| const temporaryRelativePath = relative(resolve(tmpdir()), resolvedTemporaryDirectory); |
| if ( |
| !temporaryDirectory || |
| temporaryRelativePath.startsWith('..') || |
| temporaryRelativePath === '' || |
| !Number.isInteger(oldPID) || |
| oldPID <= 0 || |
| !Number.isInteger(soakRounds) || |
| soakRounds < 1 || |
| soakRounds > 20 |
| ) { |
| throw new Error('process restart E2E requires launcher-owned inputs'); |
| } |
| |
| const reportPath = join(resolvedTemporaryDirectory, 'report.json'); |
| const restartRequestPath = (round) => |
| join(resolvedTemporaryDirectory, `restart-request-${round}.json`); |
| const restartCompletePath = (round) => |
| join(resolvedTemporaryDirectory, `restart-complete-${round}.json`); |
| const delay = (milliseconds) => |
| new Promise((resolvePromise) => setTimeout(resolvePromise, milliseconds)); |
| async function waitForJson(path, label, timeoutMs = 15_000) { |
| const deadline = Date.now() + timeoutMs; |
| while (Date.now() < deadline) { |
| try { |
| return JSON.parse(await readFile(path, 'utf8')); |
| } catch (error) { |
| if (error?.code !== 'ENOENT' && !(error instanceof SyntaxError)) throw error; |
| } |
| await delay(50); |
| } |
| throw new Error(`${label} timeout`); |
| } |
| |
| const traces = []; |
| const idFlow = []; |
| let latestObservationGeometry; |
| const backend = createCuaDriverBackend({ |
| binaryPath, |
| hostBundleId: 'com.maka.desktop', |
| expectedBinarySha256, |
| expectedServerName: 'cua-driver', |
| expectedServerVersion: '0.7.1', |
| expectedProtocolVersion: '2025-06-18', |
| timeoutMs: 10_000, |
| onTrace(event) { |
| traces.push(event); |
| }, |
| }); |
| const instrumentedBackend = { |
| ...backend, |
| async observeApp(input, signal, context) { |
| const observation = await backend.observeApp(input, signal, context); |
| latestObservationGeometry = { |
| windowBounds: observation.windowBounds, |
| sourceBoundsPx: observation.sourceBoundsPx, |
| }; |
| idFlow.push({ |
| phase: 'observe', |
| observationId: observation.observationId, |
| pid: observation.pid, |
| sessionId: context.sessionId, |
| turnId: context.turnId, |
| }); |
| return observation; |
| }, |
| async runSemantic(action, signal, context) { |
| const result = await backend.runSemantic(action, signal, context); |
| idFlow.push({ |
| phase: 'runSemanticResult', |
| toolCallId: context.toolCallId, |
| ok: result.outcome.ok, |
| ...(!result.outcome.ok |
| ? { error: result.outcome.error, message: result.outcome.message } |
| : {}), |
| }); |
| return result; |
| }, |
| async run(action, signal, context) { |
| const result = await backend.run(action, signal, context); |
| idFlow.push({ |
| phase: 'runResult', |
| toolCallId: context.toolCallId, |
| ok: result.outcome.ok, |
| ...(!result.outcome.ok |
| ? { error: result.outcome.error, message: result.outcome.message } |
| : {}), |
| }); |
| return result; |
| }, |
| }; |
| const tools = buildComputerUseTools({ backend: instrumentedBackend }); |
| const [tool] = tools; |
| const context = (toolCallId, turnId) => ({ |
| sessionId: 'process-restart-e2e', |
| turnId, |
| toolCallId, |
| cwd: repoRoot, |
| abortSignal: new AbortController().signal, |
| emitOutput() {}, |
| }); |
| const call = (input, toolCallId, turnId) => tool.impl(input, context(toolCallId, turnId)); |
| const parseModel = (result) => JSON.parse(result.modelText ?? '{}'); |
| const readState = async () => JSON.parse(await readFile(statePath, 'utf8')); |
| |
| function freshObservationFrom(result) { |
| const marker = '\nFresh observation:\n'; |
| const markerIndex = result.modelText?.indexOf(marker) ?? -1; |
| if (markerIndex < 0) return undefined; |
| return JSON.parse(result.modelText.slice(markerIndex + marker.length)); |
| } |
| |
| async function observe(pid, toolCallId, turnId) { |
| const result = await call( |
| { |
| action: 'observe', |
| app: `pid:${pid}`, |
| include_screenshot: true, |
| }, |
| toolCallId, |
| turnId, |
| ); |
| if (result.error || !result.modelText) { |
| throw new Error(`observe failed: ${result.error ?? result.text}`); |
| } |
| return { |
| result, |
| model: parseModel(result), |
| persisted: JSON.parse(result.text), |
| geometry: latestObservationGeometry, |
| }; |
| } |
| |
| async function observeUntilElement(pid, label, toolCallPrefix, turnId, timeoutMs = 8_000) { |
| const deadline = Date.now() + timeoutMs; |
| let lastObserved; |
| let lastObservationError; |
| while (Date.now() < deadline) { |
| try { |
| lastObserved = await observe(pid, `${toolCallPrefix}-${Date.now()}`, turnId); |
| lastObservationError = undefined; |
| } catch (error) { |
| const message = error instanceof Error ? error.message : String(error); |
| if (!/invalidApp: no visible window matched/.test(message)) throw error; |
| lastObservationError = message; |
| await delay(200); |
| continue; |
| } |
| const matches = lastObserved.model.elements |
| .filter((element) => element.label === label) |
| .sort((left, right) => Number(left.element_id) - Number(right.element_id)); |
| if (matches.length > 0) { |
| return { |
| observed: lastObserved, |
| element: matches[0], |
| candidateCount: matches.length, |
| }; |
| } |
| await delay(200); |
| } |
| const labels = |
| lastObserved?.model.elements |
| ?.map((element) => element.label) |
| .filter(Boolean) |
| .slice(0, 30) ?? []; |
| throw new Error( |
| `${label} did not appear before timeout; ` + |
| `lastObservationError=${lastObservationError ?? 'none'}; ` + |
| `labels=${JSON.stringify(labels)}`, |
| ); |
| } |
| |
| async function writeReport(report) { |
| await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`, { |
| flag: 'wx', |
| mode: 0o600, |
| }); |
| } |
| |
| const report = { |
| schemaVersion: 1, |
| soakRounds, |
| fixture: { |
| bundleIdentifier: 'com.openai.codex.cualab', |
| canonicalAppPath: expectedAppPath, |
| oldPID, |
| }, |
| cases: [], |
| }; |
| |
| try { |
| const seenHostPIDs = new Set(); |
| const seenWebContentPIDs = new Set(); |
| let currentPID = oldPID; |
| let serviceGenerations; |
| for (let round = 1; round <= soakRounds; round += 1) { |
| const currentState = await readState(); |
| if ( |
| currentState.synthetic !== true || |
| currentState.syntheticMarker !== 'CUA Lab Synthetic Surface' || |
| currentState.appPath !== expectedAppPath || |
| currentState.oop.hostPID !== currentPID || |
| !Number.isInteger(currentState.oop.webContentPID) || |
| currentState.oop.webContentPID <= 0 |
| ) { |
| throw new Error(`round ${round} fixture provenance mismatch`); |
| } |
| const currentWebContentPID = currentState.oop.webContentPID; |
| if (seenHostPIDs.has(currentPID) || seenWebContentPIDs.has(currentWebContentPID)) { |
| throw new Error(`round ${round} reused a prior process identity`); |
| } |
| seenHostPIDs.add(currentPID); |
| seenWebContentPIDs.add(currentWebContentPID); |
| |
| const oldReady = await observeUntilElement( |
| currentPID, |
| 'CUA Lab Set Value Field', |
| `observe-old-process-r${round}`, |
| `turn-old-r${round}`, |
| ); |
| const oldObserved = oldReady.observed; |
| await writeFile( |
| restartRequestPath(round), |
| `${JSON.stringify({ |
| round, |
| oldPID: currentPID, |
| oldWebContentPID: currentWebContentPID, |
| observationId: oldObserved.model.observation_id, |
| candidateCount: oldReady.candidateCount, |
| })}\n`, |
| { flag: 'wx', mode: 0o600 }, |
| ); |
| |
| const restart = await waitForJson(restartCompletePath(round), `fixture restart ${round}`); |
| const newPID = restart.newPID; |
| const newWebContentPID = restart.newWebContentPID; |
| if ( |
| restart.round !== round || |
| restart.oldPID !== currentPID || |
| !Number.isInteger(newPID) || |
| newPID <= 0 || |
| currentPID === newPID || |
| !Number.isInteger(newWebContentPID) || |
| newWebContentPID <= 0 || |
| currentWebContentPID === newWebContentPID || |
| seenHostPIDs.has(newPID) || |
| seenWebContentPIDs.has(newWebContentPID) |
| ) { |
| throw new Error(`round ${round} restart identity did not advance`); |
| } |
| |
| const staleToolCallId = `old-observation-after-restart-r${round}`; |
| const staleAttempt = await call( |
| { |
| action: 'set_value', |
| observation_id: oldObserved.model.observation_id, |
| element_id: oldReady.element.element_id, |
| value: `stale-value-r${round}`, |
| }, |
| staleToolCallId, |
| `turn-old-r${round}`, |
| ); |
| await delay(150); |
| const newState = await readState(); |
| const oldRunResult = idFlow.find( |
| (event) => event.phase === 'runSemanticResult' && event.toolCallId === staleToolCallId, |
| ); |
| const staleDispatch = traces.find( |
| (event) => event.type === 'dispatch' && event.toolCallId === staleToolCallId, |
| ); |
| if ( |
| oldRunResult?.error !== 'target_missing' || |
| !/target_missing/.test(staleAttempt.modelText ?? '') || |
| staleDispatch || |
| newState.controls.setValue !== '' |
| ) { |
| throw new Error(`round ${round} old observation crossed process restart`); |
| } |
| |
| tools.clearSession('process-restart-e2e'); |
| const freshReady = await observeUntilElement( |
| newPID, |
| 'CUA Lab Set Value Field', |
| `observe-new-process-r${round}`, |
| `turn-new-r${round}`, |
| ); |
| const freshValue = `fresh-value-r${round}`; |
| const freshToolCallId = `fresh-process-set-value-r${round}`; |
| const freshAction = await call( |
| { |
| action: 'set_value', |
| observation_id: freshReady.observed.model.observation_id, |
| element_id: freshReady.element.element_id, |
| value: freshValue, |
| }, |
| freshToolCallId, |
| `turn-new-r${round}`, |
| ); |
| await delay(150); |
| const newStateAfterAction = await readState(); |
| const dispatch = traces.find( |
| (event) => event.type === 'dispatch' && event.toolCallId === freshToolCallId, |
| ); |
| const freshRunResult = idFlow.find( |
| (event) => event.phase === 'runSemanticResult' && event.toolCallId === freshToolCallId, |
| ); |
| const freshObservation = freshObservationFrom(freshAction); |
| const freshField = freshObservation?.elements?.find( |
| (element) => element.label === 'CUA Lab Set Value Field', |
| ); |
| const freshOccluded = freshRunResult?.error === 'target_occluded'; |
| const freshSucceeded = !freshAction.error && freshField?.value === freshValue; |
| if ( |
| (!freshSucceeded && !freshOccluded) || |
| (freshOccluded && (dispatch || freshField?.value === freshValue)) || |
| newStateAfterAction.oop.hostPID !== newPID || |
| newStateAfterAction.oop.webContentPID !== newWebContentPID |
| ) { |
| throw new Error(`round ${round} fresh process action violated its oracle`); |
| } |
| |
| const serviceState = backend.serviceState(); |
| for (const role of ['action', 'capture']) { |
| if (serviceState[role].restartAttempts !== 0) { |
| throw new Error(`round ${round} ${role} service consumed restart budget`); |
| } |
| } |
| const generations = { |
| action: serviceState.action.generation, |
| capture: serviceState.capture.generation, |
| }; |
| if (!serviceGenerations) { |
| serviceGenerations = generations; |
| } else if ( |
| generations.action !== serviceGenerations.action || |
| generations.capture !== serviceGenerations.capture |
| ) { |
| throw new Error(`round ${round} unexpectedly restarted cua-driver services`); |
| } |
| |
| report.cases.push({ |
| round, |
| oldPID: currentPID, |
| oldWebContentPID: currentWebContentPID, |
| newPID, |
| newWebContentPID, |
| stale: { |
| error: oldRunResult.error, |
| mutation: ['', newState.controls.setValue], |
| dispatch: false, |
| }, |
| fresh: { |
| outcome: freshOccluded ? 'fail_closed_occluded' : 'ax_set_value_succeeded', |
| mutation: ['', freshField?.value], |
| }, |
| serviceGenerations: generations, |
| }); |
| currentPID = newPID; |
| tools.clearSession('process-restart-e2e'); |
| } |
| |
| report.ok = true; |
| report.claim = |
| 'old observations never cross repeated real app-process restarts; fresh AX set_value actions target only the new process or fail occluded without side effects'; |
| report.evidence = { |
| seenHostPIDs: [...seenHostPIDs], |
| seenWebContentPIDs: [...seenWebContentPIDs], |
| serviceGenerations, |
| idFlow, |
| traceTypes: traces.map((event) => event.type), |
| }; |
| await writeReport(report); |
| process.stdout.write(`${JSON.stringify(report, null, 2)}\n`); |
| } catch (error) { |
| const failureReport = { |
| ...report, |
| ok: false, |
| failure: error instanceof Error ? error.message : String(error), |
| evidence: { idFlow, traces }, |
| }; |
| await writeReport(failureReport).catch(() => {}); |
| process.stderr.write(`${JSON.stringify(failureReport, null, 2)}\n`); |
| throw error; |
| } finally { |
| tools.clearSession('process-restart-e2e'); |
| backend.dispose(); |
| } |