Files
reverse-engineering-anything/tests/boundary/process/nativeUiVerifierActions.test.ts
T
67ce671d66 fix(verifiers): release fixture roots only after their owners finish (#10491)
* fix(verifiers): release fixture roots only after their owners finish

Several real-tool verifiers released a fixture root even when a cleanup
that still used it had failed, so a surviving MCP server, session or
descendant kept running against a deleted directory and the failure
looked like ordinary cleanup.

- scripts/lib/verifier-cleanup.mjs: new releaseAfter(dependents,
  release, retained) task. It attempts every dependent cleanup and
  releases the resource only if all succeed; otherwise it keeps the
  resource and reports it with the dependent failures.
- verify-signal-recording: the root is released after the MCP client
  and transport close, and the host-limits read moves before the root
  is acquired so its failure cannot leak the root.
- verify-recorded-crash: sessions, the sentinel and the process-lineage
  check must all succeed before the root is released. A transport close
  failure after a failed connect keeps the connect failure.
- verify-wasm-artifact: both roots are released after the MCP client
  and transport close; the residue check and the two directories stay
  independent after that.
- verify-native-ui: `open -W` exits only after the app exits, so the
  root is removed only once that exit is observed; otherwise it is
  retained and reported.

Fixes #8561
Fixes #8562
Fixes #8564
Refs #8559

Co-Authored-By: gensokyo-dev <gensokyo-dev@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(verifiers): treat a signaled open as unproven fixture exit

A terminated `open` launcher does not prove the LaunchServices app it
started has exited, so only an `open -W` that exits on its own counts.
Also clear the grace timer so a prompt exit does not hold the process
for the rest of the grace period.

Refs #8559

Co-Authored-By: gensokyo-dev <gensokyo-dev@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(verifiers): model open -W exiting after the fixture is signaled

The native UI verifier now removes its root only after `open -W` exits
on its own. The authored recording's `open` never exited when the
fixture was signaled and had no signalCode, so the complete case waited
out the grace period and kept the root. The double now behaves like
`open -W`: it exits once the fixture process is signaled.

Refs #8559

Co-Authored-By: gensokyo-dev <gensokyo-dev@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: gensokyo-dev <gensokyo-dev@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-14 09:56:18 +01:00

90 lines
4.4 KiB
TypeScript

import { execFile } from "node:child_process";
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { expect, it } from "vitest";
const execute = promisify(execFile);
it.each(["complete", "drop CLI", "drop MCP"])(
"requires each native UI adapter's own action (%s)",
async (mode) => {
const directory = await mkdtemp(join(tmpdir(), "rea-ui-action-oracle-"));
try {
// Authored AX recordings qualify adapter action assertions, not macOS
// permissions, native capture, Swift compilation or process ownership.
await writeFile(
join(directory, "recording.mjs"),
`
import {EventEmitter} from 'node:events';import {writeFile} from 'node:fs/promises';import {promisify} from 'node:util';
let count=0;
const nodes=()=>[{role:'AXButton',path:[0],title:count===0?'Increment REA fixture':'REA fixture incremented '+count}];
const captured=()=>({steps:[{outcome:'completed'},{outcome:'completed',after:{nodes:nodes()}}]});
export const execFile=Object.assign(()=>{}, {[promisify.custom]:async (file,args)=>{
if(args?.includes('capture-native-ui-scenario')){if(process.env.MODE!=='drop CLI')count++;return {stdout:JSON.stringify({normalized_result:captured()})};}
return {stdout:'{"v":"slash/producer"}\\n{"v":1.0}\\n'};
}});
export const spawn=(_command,args)=>{const child=new EventEmitter();child.exitCode=null;child.signalCode=null;globalThis.reaOpenChild=child;child.kill=()=>{child.exitCode=0;child.emit('exit',0);return true;};void writeFile(args[args.indexOf('--stdout')+1],JSON.stringify({pid:424242,window_id:7})+'\\n');return child;};
export const parseBinaryTarget=async path=>({ok:true,value:{path,sha256:'a'.repeat(64)}});
export const observeProcessStartIdentity=async()=>({state:'readable',identity:'authored-process-identity'});
export const observeNativeUi=async(target,operation)=>{
if(target.sha256==='0'.repeat(64))return {ok:false,error:{issues:[{message:'target-mismatch: authored changed digest'}]}};
if(operation==='capture_native_ui_scenario'){count++;return {ok:true,value:captured()};}
return {ok:true,value:{initial:{nodes:nodes()}}};
};
export const withArtifactMcp=async(_path,run)=>run({});
export const artifactMcpResult=async(_client,name)=>{if(name==='observe_native_ui')return {initial:{nodes:nodes()}};if(process.env.MODE!=='drop MCP')count++;return captured();};
`,
);
await writeFile(
join(directory, "hook.mjs"),
`
import {registerHooks} from 'node:module';import {pathToFileURL} from 'node:url';
Object.defineProperty(process,'platform',{value:'darwin'});process.kill=()=>{const open=globalThis.reaOpenChild;if(open?.exitCode===null){open.exitCode=0;queueMicrotask(()=>open.emit('exit',0,null));}return true;};
registerHooks({resolve(specifier,context,next){if(context.parentURL?.endsWith('/scripts/verify-native-ui.mjs')&&['node:child_process','../dist/application/BinaryTargetResolver.js','../dist/native/NativeUiObservation.js','./lib/artifact-e2e.mjs','../dist/process/ProcessOwnershipObservation.js'].includes(specifier))return {shortCircuit:true,url:pathToFileURL(process.env.RECORDING).href};return next(specifier,context);}});
`,
);
const result = execute(
process.execPath,
[
"--import",
join(directory, "hook.mjs"),
fileURLToPath(
new URL("../../../scripts/verify-native-ui.mjs", import.meta.url),
),
],
{
timeout: 10_000,
env: {
...process.env,
TMPDIR: directory,
TMP: directory,
TEMP: directory,
RECORDING: join(directory, "recording.mjs"),
MODE: mode,
},
},
);
if (mode === "complete") {
expect(JSON.parse((await result).stdout)).toMatchObject({
ok: true,
positive_e2e: true,
scenario: "selected-AX-button-action-and-CLI-MCP-parity-observed",
});
} else {
await expect(result).rejects.toMatchObject({
stderr: expect.stringContaining(
mode === "drop CLI"
? "Native UI CLI did not preserve the selected scenario"
: "MCP did not observe the selected fixture's AX action",
),
});
}
} finally {
await rm(directory, { recursive: true, force: true });
}
},
);