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>
192 lines
6.1 KiB
JavaScript
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);
|
|
}
|