Files
rea/experiments/analysis-efficiency/drawio-electron-lifecycle.mjs
bf3475867f fix(electron): preserve native IPC listener behavior (#4389)
Preserve original-function removal, native IPC dispatch and argument identity, with explicit metadata ownership and Evidence verification. Runtime cleanup limitations remain documented.

Fixes #4390.

Co-authored-by: Adolanium <94890352+Adolanium@users.noreply.github.com>
Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-17 04:17:11 +08:00

192 lines
6.1 KiB
JavaScript

#!/usr/bin/env node
// Real draw.io ASAR, renderer and declared actions; DOM reads are independent
// verification probes. No fabricated IPC channels or replacement app callbacks.
import assert from "node:assert/strict";
import { mkdtemp, writeFile } from "node:fs/promises";
import { join, resolve } from "node:path";
import { pathToFileURL } from "node:url";
import { parseArgs } from "node:util";
import { _electron } from "playwright-core";
const { values } = parseArgs({
options: {
"runtime-root": { type: "string" },
executable: { type: "string" },
application: { type: "string" },
"profile-root": { type: "string" },
selector: { type: "string" },
"dismiss-error": { type: "boolean", default: false },
},
});
for (const key of ["runtime-root", "executable", "application", "profile-root"])
assert.ok(values[key], `Missing --${key}`);
const runtime = resolve(values["runtime-root"]);
const { PlaywrightElectronActiveProvider } = await import(
pathToFileURL(
join(runtime, "dist/browser/PlaywrightElectronActiveProvider.js"),
)
);
const { captureElectronScenario } = await import(
pathToFileURL(
join(
runtime,
"dist/application/javascript/ElectronActiveObservationService.js",
),
)
);
const { runWithExecutionLimits } = await import(
pathToFileURL(
join(runtime, "dist/browser/PlaywrightElectronActiveActions.js"),
)
);
const profile = await mkdtemp(
join(resolve(values["profile-root"]), "drawio-ui-"),
);
const controller = new AbortController();
const timer = setTimeout(
() => controller.abort(new Error("Verifier deadline")),
40_000,
);
let application;
let child;
let page;
let before;
let after;
let checkedFinalState = false;
const inspect = () =>
page.evaluate(() => {
const diagram = document.querySelector(".geDiagramContainer svg");
return {
title: document.title,
invalid_file_dialog:
document.body.innerText.includes("Invalid file data"),
unsaved_changes: document.body.innerText.includes("Unsaved changes"),
diagram_svg_elements:
diagram?.querySelectorAll("rect,path,ellipse,text").length ?? null,
cells: document.querySelectorAll(".geDiagramContainer [data-cell-id]")
.length,
};
});
const started = performance.now();
try {
const actions = values["dismiss-error"]
? [
{
step_id: "dismiss-invalid-file",
kind: "click",
window_index: 0,
selector: 'button:has-text("OK"):visible',
},
]
: [];
if (values.selector !== undefined)
actions.push({
step_id: "insert-shape",
kind: "click",
window_index: 0,
selector: values.selector,
});
actions.push({
step_id: "settle-renderer",
kind: "wait",
window_index: 0,
duration_ms: 500,
});
const result = await captureElectronScenario(
new PlaywrightElectronActiveProvider(process.env, async (options) => {
application = await _electron.launch(options);
child = application.process();
page = await application.firstWindow({ timeout: 15_000 });
await page
.locator(".geDiagramContainer")
.waitFor({ state: "visible", timeout: 15_000 });
if (values["dismiss-error"])
await page
.getByText("Invalid file data", { exact: true })
.waitFor({ state: "visible", timeout: 15_000 });
before = await inspect();
return new Proxy(application, {
get(target, key) {
if (key === "close")
return async () => {
if (!checkedFinalState) {
checkedFinalState = true;
after = await inspect();
}
return target.close();
};
const value = Reflect.get(target, key, target);
return typeof value === "function" ? value.bind(target) : value;
},
});
}),
{
executable_path: resolve(values.executable),
application_path: resolve(values.application),
args: ["--no-sandbox", "--disable-gpu", `--user-data-dir=${profile}`],
actions,
},
{ signal: controller.signal },
);
const capture = result.ok
? result.value.normalized_result
: result.error.partialObservation?.capture;
await writeFile(join(profile, "result.json"), JSON.stringify(result));
const actionsCompleted =
capture?.actions.every((action) => action.status === "completed") === true;
const inserted =
values.selector === undefined
? null
: after?.diagram_svg_elements > before?.diagram_svg_elements;
console.log(
JSON.stringify({
profile,
runtime,
elapsed_ms: performance.now() - started,
deadline_triggered: controller.signal.aborted,
selected_shape: values.selector ?? null,
before,
after,
actions_completed: actionsCompleted,
shape_inserted: inserted,
provider_ok: result.ok,
cleanup_verified: result.ok,
reason: result.ok ? null : result.error.reason,
coverage: capture?.coverage,
retention: capture?.retention,
ipc: capture?.ipc,
actions: capture?.actions,
workflow_status:
result.ok && actionsCompleted && inserted !== false
? "verified"
: "partial-or-failed",
}),
);
assert.ok(actionsCompleted, "Declared real-app actions did not complete");
assert.ok(before && after, "Real renderer state was not verified");
assert.notEqual(inserted, false, "Click did not insert a diagram shape");
if (values["dismiss-error"]) assert.equal(after.invalid_file_dialog, false);
assert.equal(
controller.signal.aborted,
false,
"Verifier deadline was reached",
);
assert.ok(result.ok, "Provider cleanup remains unverified");
} finally {
clearTimeout(timer);
if (
application !== undefined &&
child?.exitCode === null &&
child?.signalCode === null
)
await runWithExecutionLimits(
application.close(),
undefined,
performance.now() + 5_000,
).catch(() => {
process.exitCode = 1;
});
// The resource scope owns any unresolved descendants; no PID/path-only kill.
if (child?.exitCode === null && child?.signalCode === null) process.exit(1);
}