Files
software-mansion__argent/packages/tool-server/test/flows/script/flow-script-protocol.test.ts
2026-09-11 11:40:54 +02:00

766 lines
27 KiB
TypeScript

import { fork } from "node:child_process";
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import net from "node:net";
import { pathToFileURL } from "node:url";
import { afterEach, describe, expect, it } from "vitest";
import {
FlowScriptExecutor,
type FlowScriptFailure,
type FlowScriptRequest,
} from "../../../src/tools/flows/script/flow-script-executor";
import {
parseScriptResponse,
type ScriptFailureType,
type ScriptResponse,
} from "../../../src/tools/flows/script/flow-script-protocol";
import {
createScriptWorkspace,
SOURCE_RUNNER_DIR,
type ScriptWorkspace,
} from "../../helpers/flow-script-workspace";
import { resolveHostBash } from "../../helpers/host-bash";
const workspaces: ScriptWorkspace[] = [];
const cleanups: Array<() => void> = [];
function workspace(): ScriptWorkspace {
const ws = createScriptWorkspace("proto");
workspaces.push(ws);
return ws;
}
afterEach(() => {
while (cleanups.length) cleanups.pop()!();
while (workspaces.length) workspaces.pop()!.cleanup();
});
function executor() {
return new FlowScriptExecutor({ concurrency: 4, maxTimeoutMs: 60_000 });
}
/**
* The fake runner parks rather than returns: Node awaits an `--import` module
* before it loads the entry, so the script never runs — the real runner's own
* shape on a request it cannot honour.
*/
async function withFakeRunner(source: string, extra: Partial<FlowScriptRequest> = {}) {
const ws = workspace();
const runnerDir = ws.resolve("runner");
fs.mkdirSync(runnerDir, { recursive: true });
fs.writeFileSync(
path.join(runnerDir, "flow-script-runner.mjs"),
`${source}\nawait new Promise(() => {});\n`
);
const script = ws.write("script.mjs", `output.ok = true;`);
return executor().execute({
scriptPath: script,
projectRoot: ws.dir,
runnerDir,
timeoutMs: 5_000,
...extra,
});
}
describe("script response parsing", () => {
it("accepts the three valid shapes", () => {
const started: ScriptResponse | null = parseScriptResponse({ type: "started" }, "node");
expect(started).toEqual({ type: "started" });
expect(parseScriptResponse({ type: "result", outputJson: "{}" }, "node")).toEqual({
type: "result",
outputJson: "{}",
});
const runtime: ScriptFailureType = "runtime";
expect(
parseScriptResponse({ type: "failure", failureType: runtime, message: "x" }, "node")
).toEqual({
type: "failure",
failureType: runtime,
message: "x",
});
});
it("carries a stack when there is one, and drops one that is not a string", () => {
expect(
parseScriptResponse(
{ type: "failure", failureType: "runtime", message: "x", stack: "at y" },
"node"
)
).toEqual({ type: "failure", failureType: "runtime", message: "x", stack: "at y" });
expect(
parseScriptResponse(
{ type: "failure", failureType: "runtime", message: "x", stack: 7 },
"node"
)
).toEqual({ type: "failure", failureType: "runtime", message: "x" });
});
it.each(["spawn", "signal"] as const)("accepts the bash-mode failure type %s", (failureType) => {
expect(parseScriptResponse({ type: "failure", failureType, message: "x" }, "bash")).toEqual({
type: "failure",
failureType,
message: "x",
});
});
it.each(["spawn", "signal"] as const)(
"refuses the bash-mode failure type %s in node mode",
(failureType) => {
expect(
parseScriptResponse({ type: "failure", failureType, message: "x" }, "node")
).toBeNull();
}
);
it.each([
["a non-object", "started"],
["null", null],
["an unknown type", { type: "progress" }],
["a result with no output", { type: "result" }],
["a result whose output is not a string", { type: "result", outputJson: {} }],
["a failure with no message", { type: "failure", failureType: "runtime" }],
[
"a failure with an unknown failure type",
{ type: "failure", failureType: "weird", message: "x" },
],
])("rejects %s", (_label, raw) => {
expect(parseScriptResponse(raw, "node")).toBeNull();
expect(parseScriptResponse(raw, "bash")).toBeNull();
});
});
describe("flow script executor — a runner that misbehaves", () => {
it("reports an unrecognized message as a protocol failure", async () => {
const result = await withFakeRunner(
`process.on("message", () => { process.send({ type: "progress", at: 1 }); });`
);
expect(result.failure?.kind).toBe("protocol");
expect(result.failure?.message).toContain("does not recognise");
});
it("reports a runner that exits before starting the script", async () => {
const result = await withFakeRunner(`process.on("message", () => process.exit(9));`);
expect(result.failure?.kind).toBe("protocol");
expect(result.failure?.message).toContain("exited before it started the script");
expect(result.failure?.message).toContain("exit code 9");
});
it("reports a runner that starts the script then exits without a verdict", async () => {
const result = await withFakeRunner(
`process.on("message", () => { process.send({ type: "started" }); process.exit(0); });`
);
expect(result.failure?.kind).toBe("exit");
expect(result.failure?.message).toContain("exit code 0");
});
it("rejects a result that is not an object", async () => {
const result = await withFakeRunner(
`process.on("message", () => {
process.send({ type: "started" });
process.send({ type: "result", outputJson: "[1,2,3]" }, () => process.exit(0));
});`
);
expect(result.failure?.kind).toBe("output");
expect(result.failure?.message).toContain("not an object");
});
it("rejects a result whose output does not parse", async () => {
const result = await withFakeRunner(
`process.on("message", () => {
process.send({ type: "started" });
process.send({ type: "result", outputJson: "{not json" }, () => process.exit(0));
});`
);
expect(result.failure?.kind).toBe("output");
expect(result.failure?.message).toContain("did not parse");
});
it("keeps the first terminal response and ignores a second", async () => {
const result = await withFakeRunner(
`process.on("message", () => {
process.send({ type: "started" });
process.send({ type: "result", outputJson: JSON.stringify({ first: true }) });
process.send({ type: "failure", failureType: "runtime", message: "second" }, () =>
process.exit(0)
);
});`
);
expect(result.ok).toBe(true);
expect(result.output).toEqual({ first: true });
});
it("re-checks the output size even though a compliant child already did", async () => {
const result = await withFakeRunner(
`process.on("message", () => {
process.send({ type: "started" });
const big = JSON.stringify({ blob: "x".repeat(1024 * 1024 + 32) });
process.send({ type: "result", outputJson: big }, () => process.exit(0));
});`
);
expect(result.failure?.kind).toBe("output");
expect(result.failure?.message).toContain("the limit is 1.0 MiB");
});
it("re-checks the failure text bounds even though a compliant child already did", async () => {
const result = await withFakeRunner(
`process.on("message", () => {
process.send({ type: "started" });
process.send(
{ type: "failure", failureType: "runtime", message: "m".repeat(20000), stack: "s".repeat(40000) },
() => process.exit(0)
);
});`
);
expect(result.failure?.kind).toBe("runtime");
expect(result.failure?.message.length).toBeLessThan(20000);
expect(result.failure?.message).toMatch(/… \[\d+ more characters omitted]$/);
expect(result.failure?.stack?.length).toBeLessThan(40000);
expect(result.failure?.stack).toMatch(/… \[\d+ more characters omitted]$/);
});
it("re-checks an own __proto__ key even though a compliant child already did", async () => {
const result = await withFakeRunner(
`process.on("message", () => {
process.send({ type: "started" });
const doc = '{"settings":{"__proto__":{"injected":"yes"}}}';
process.send({ type: "result", outputJson: doc }, () => process.exit(0));
});`
);
expect(result.failure?.kind).toBe("output");
expect(result.failure?.message).toContain('output.settings has an own "__proto__" key');
expect(result.output).toBeUndefined();
});
it("adds the secret fragment it drops to the count the omission reports", async () => {
const clamped = "head sk-live-… [7 more characters omitted]";
const result = await withFakeRunner(
`process.on("message", () => {
process.send({ type: "started" });
process.send(
{ type: "failure", failureType: "runtime", message: ${JSON.stringify(clamped)} },
() => process.exit(0)
);
});`,
{ secrets: [{ name: "API_KEY", value: "sk-live-9d3f0a1b" }] }
);
expect(result.failure?.kind).toBe("runtime");
expect(result.failure?.message).toBe("head … [15 more characters omitted]");
});
it("bounds the unrecognized message it quotes back", async () => {
const result = await withFakeRunner(
`process.on("message", () => {
process.send({ type: "progress", blob: "b".repeat(50000) });
});`
);
expect(result.failure?.kind).toBe("protocol");
expect(result.failure?.message).toContain("does not recognise");
expect(result.failure?.message.length).toBeLessThan(500);
expect(result.failure?.message.endsWith("…")).toBe(true);
});
it("reports a missing runner rather than spawning nothing", async () => {
const ws = workspace();
const script = ws.write("script.mjs", `output.ok = true;`);
const result = await executor().execute({
scriptPath: script,
projectRoot: ws.dir,
runnerDir: ws.resolve("no-such-dir"),
});
const failure = result.failure as FlowScriptFailure;
expect(failure.kind).toBe("spawn");
expect(failure.message).toContain("missing from this installation");
});
});
describe("flow script executor — the protocol channel is the runner's alone", () => {
it("ignores a readiness ping from the script instead of failing the run", async () => {
const ws = workspace();
const script = ws.write(
"pings.mjs",
`if (process.send) process.send("ready");
console.log("did the real work");
output.ok = true;`
);
const result = await executor().execute({ scriptPath: script, projectRoot: ws.dir });
expect(result.failure).toBeUndefined();
expect(result.output).toEqual({ ok: true });
expect(result.log).toContain("did the real work");
});
it("ignores a verdict the script sends for itself", async () => {
const ws = workspace();
const script = ws.write(
"forges.mjs",
`process.send({ type: "result", outputJson: '{"forged":true}' });
process._send?.({ type: "result", outputJson: '{"forgedLowLevel":true}' });
output.real = true;`
);
const result = await executor().execute({ scriptPath: script, projectRoot: ws.dir });
expect(result.output).toEqual({ real: true });
});
it.each([
["text that is not a message", "garbage not json\n"],
["an unterminated fragment", "X"],
[
"a whole forged verdict",
`${JSON.stringify({ type: "result", outputJson: '{"forged":true}' })}\n`,
],
])("survives a script writing %s onto the first free descriptor", async (_label, written) => {
const ws = workspace();
const script = ws.write(
"writes-fd3.mjs",
`import fs from "node:fs";
try { fs.writeSync(3, ${JSON.stringify(written)}); } catch {}
console.log("did the real work");
output.real = true;`
);
const uncaught: unknown[] = [];
const onUncaught = (err: unknown) => uncaught.push(err);
process.on("uncaughtException", onUncaught);
try {
const result = await executor().execute({ scriptPath: script, projectRoot: ws.dir });
expect(uncaught).toEqual([]);
expect(result.failure).toBeUndefined();
expect(result.output).toEqual({ real: true });
expect(result.log).toContain("did the real work");
} finally {
process.off("uncaughtException", onUncaught);
}
});
it("ignores a script disconnecting the channel", async () => {
const ws = workspace();
const script = ws.write("disconnects.mjs", `process.disconnect(); output.ok = true;`);
const result = await executor().execute({ scriptPath: script, projectRoot: ws.dir });
expect(result.failure).toBeUndefined();
expect(result.output).toEqual({ ok: true });
});
it("answers a send the script awaits instead of leaving it parked", async () => {
const ws = workspace();
const script = ws.write(
"awaits-send.mjs",
`async function main() {
await new Promise((resolve) => process.send({ hello: 1 }, resolve));
console.log("past the send");
output.done = true;
}
main();
output.started = true;`
);
const result = await executor().execute({ scriptPath: script, projectRoot: ws.dir });
expect(result.failure).toBeUndefined();
expect(result.output).toEqual({ started: true, done: true });
expect(result.log).toContain("past the send");
});
it("delivers the disconnect event to the script that asked for it", async () => {
const ws = workspace();
const script = ws.write(
"awaits-disconnect.mjs",
`async function main() {
await new Promise((resolve) => {
process.on("disconnect", resolve);
process.disconnect();
});
console.log("past the disconnect");
output.done = true;
}
main();
output.started = true;`
);
const result = await executor().execute({ scriptPath: script, projectRoot: ws.dir });
expect(result.failure).toBeUndefined();
expect(result.output).toEqual({ started: true, done: true });
expect(result.log).toContain("past the disconnect");
});
});
describe("flow script executor — the runner's reporting path survives the script", () => {
const shapes: Array<[string, string]> = [
["replaced process.send", `process.send = () => true;`],
["deleted process.send", `delete process.send;`],
["removed every process listener", `process.removeAllListeners();`],
["removed the beforeExit listeners", `process.removeAllListeners("beforeExit");`],
];
for (const [what, prelude] of shapes) {
it(`still reports a script that ${what}`, async () => {
const ws = workspace();
const script = ws.write(
"disturbs.mjs",
`${prelude}\nconsole.log("did the work");\noutput.ok = true;`
);
const result = await executor().execute({ scriptPath: script, projectRoot: ws.dir });
expect(result.failure).toBeUndefined();
expect(result.output).toEqual({ ok: true });
expect(result.log).toContain("did the work");
});
}
it("puts its listeners back in front of the ones a script registers after", async () => {
const ws = workspace();
// Order, not just presence: the runner re-registers inside
// `removeAllListeners`, and its `uncaughtException` handler decides whether
// the script has one of its own by asking whether there is more than one.
// Put back after the script's, it would see itself alone and claim an
// exception the script recovered from.
const script = ws.write(
"reclaims.mjs",
`process.removeAllListeners();
process.on("uncaughtException", (err) => {
console.log("script handled", err.message);
output.recovered = true;
});
setTimeout(() => { throw new Error("late boom"); }, 10);`
);
const result = await executor().execute({ scriptPath: script, projectRoot: ws.dir });
expect(result.failure).toBeUndefined();
expect(result.output).toEqual({ recovered: true });
expect(result.log).toContain("script handled late boom");
});
});
function nestedDocument(depth: number, leaf: string): string {
let json = JSON.stringify(leaf);
for (let i = 0; i < depth; i++) json = `{"nested":${json}}`;
return json;
}
describe("flow script executor — redacting a document from a runner", () => {
// Deeper than a recursive walk survives - the runner's own `walk` gives out
// between about 3450 and 3925 across Node 20 to 26 - and inside the depth the
// parent admits, so the scrub is what has to hold here.
it("scrubs a document too deep for a recursive walk", async () => {
const depth = 4_000;
const result = await withFakeRunner(
`process.on("message", () => {
process.send({ type: "started" });
process.send({ type: "result", outputJson: ${JSON.stringify(nestedDocument(depth, "token sk-live-9d3f0a1b2c3d4e5f"))} }, () => process.exit(0));
});`,
{ secrets: [{ name: "TOKEN", value: "sk-live-9d3f0a1b2c3d4e5f" }] }
);
expect(result.failure).toBeUndefined();
let node: unknown = result.output;
for (let i = 0; i < depth; i++) node = (node as Record<string, unknown>).nested;
expect(node).toBe("token {{secret:TOKEN}}");
}, 30_000);
it("answers a document past the depth bound with a verdict, not a throw", async () => {
const result = await withFakeRunner(
`process.on("message", () => {
process.send({ type: "started" });
process.send({ type: "result", outputJson: ${JSON.stringify(nestedDocument(20_000, "deep"))} }, () => process.exit(0));
});`
);
expect(result.failure?.kind).toBe("output");
expect(result.failure?.message).toContain("nests deeper than");
}, 30_000);
});
describe("flow script executor — the published layout", () => {
it("runs from a directory holding only the three .mjs files", async () => {
// In the published bundle the runner sits flat in dist/ beside
// tool-server.cjs. It resolves both watchdogs against its own module URL,
// so nothing else has to be there.
const dist = fs.mkdtempSync(path.join(os.tmpdir(), "argent-script-dist-"));
cleanups.push(() => fs.rmSync(dist, { recursive: true, force: true }));
for (const name of fs.readdirSync(SOURCE_RUNNER_DIR).filter((f) => f.endsWith(".mjs"))) {
fs.copyFileSync(path.join(SOURCE_RUNNER_DIR, name), path.join(dist, name));
}
expect(fs.readdirSync(dist).sort()).toEqual([
"flow-script-runner.mjs",
"flow-script-watchdog-deadline.mjs",
"flow-script-watchdog-lifeline.mjs",
]);
const ws = workspace();
const script = ws.write("seed.mjs", `console.log("bundled"); output.ok = true;`);
const shared = executor();
await shared.execute({ scriptPath: script, projectRoot: ws.dir, runnerDir: dist });
const started = Date.now();
const result = await shared.execute({
scriptPath: script,
projectRoot: ws.dir,
runnerDir: dist,
});
const roundTripMs = Date.now() - started;
expect(result.ok).toBe(true);
expect(result.log).toContain("bundled");
expect(roundTripMs, `process start cost: ${roundTripMs}ms`).toBeLessThan(3_000);
}, 30_000);
});
describe("flow script runner — the watchdogs, driven directly", () => {
function forkRunner(scriptPath: string, deadlineMs: number, request?: unknown) {
const child = fork(scriptPath, [], {
cwd: path.dirname(scriptPath),
env: {
PATH: process.env.PATH ?? "",
HOME: process.env.HOME ?? "",
ARGENT_FLOW_SCRIPT_RUNNER: "1",
},
execArgv: [
"--max-old-space-size=512",
"--import",
pathToFileURL(path.join(SOURCE_RUNNER_DIR, "flow-script-runner.mjs")).href,
],
stdio: ["ignore", "pipe", "pipe", "ignore", "pipe", "ipc"],
detached: process.platform !== "win32",
});
cleanups.push(() => {
if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
});
child.send(
request ?? {
type: "execute",
scriptUrl: pathToFileURL(fs.realpathSync(scriptPath)).href,
outputJson: "{}",
deadlineMs,
maxOutputBytes: 1024 * 1024,
}
);
return child;
}
function exitOf(child: ReturnType<typeof forkRunner>) {
return new Promise<{ code: number | null; signal: NodeJS.Signals | null; at: number }>(
(resolve) => {
const started = Date.now();
child.once("exit", (code, signal) => resolve({ code, signal, at: Date.now() - started }));
}
);
}
it.each([
["a request that is not an execute", { type: "run", scriptUrl: "file:///x", outputJson: "{}" }],
[
"a request with no script",
{ type: "execute", outputJson: "{}", deadlineMs: 1, maxOutputBytes: 1 },
],
[
"a request with a deadline that is not a number",
{
type: "execute",
scriptUrl: "file:///x",
outputJson: "{}",
deadlineMs: "soon",
maxOutputBytes: 1,
},
],
[
"a request naming an interpreter the runner does not know",
{
type: "execute",
interpreter: "zsh",
scriptUrl: "file:///x",
outputJson: "{}",
deadlineMs: 1,
maxOutputBytes: 1,
},
],
[
"a bash request with no interpreter path",
{
type: "execute",
interpreter: "bash",
scriptPath: "/tmp/x.sh",
outputFile: "/tmp/o.json",
outputJson: "{}",
deadlineMs: 1,
maxOutputBytes: 1,
},
],
[
"a bash request with no exchange file",
{
type: "execute",
interpreter: "bash",
interpreterPath: "/bin/bash",
scriptPath: "/tmp/x.sh",
deadlineMs: 1,
maxOutputBytes: 1,
},
],
])(
"refuses %s, from the child's side of the protocol",
async (_label, message) => {
const ws = workspace();
const script = ws.write("never.mjs", `output.ran = true;`);
const child = forkRunner(script, 20_000, message);
const failure = await new Promise<{ failureType?: string; message?: string } | null>(
(resolve) => {
child.on("message", (raw) => {
const m = raw as { type?: string; failureType?: string; message?: string };
if (m.type === "failure") resolve(m);
});
child.once("exit", () => resolve(null));
}
);
expect(failure?.failureType).toBe("protocol");
expect(failure?.message).toContain("malformed request");
},
30_000
);
it("reads a request that names no interpreter as a node one", async () => {
const ws = workspace();
const script = ws.write("legacy.mjs", `output.ran = true;`);
const child = forkRunner(script, 20_000);
const result = await new Promise<{ outputJson?: string } | null>((resolve) => {
child.on("message", (raw) => {
const m = raw as { type?: string; outputJson?: string };
if (m.type === "result") resolve(m);
});
child.once("exit", () => resolve(null));
});
expect(result?.outputJson).toBe('{"ran":true}');
}, 30_000);
it("runs a bash request that names no reason file", async (ctx) => {
const found = await resolveHostBash();
if (!("path" in found)) {
ctx.skip(`this host has no bash to run a .sh step with: ${found.problem}`);
return;
}
const ws = workspace();
const slashed = (file: string) => file.split(path.sep).join("/");
const outputFile = ws.resolve("output.json");
fs.writeFileSync(outputFile, "{}");
const script = ws.write("ran.sh", `printf '{"ran":true}' > "$ARGENT_OUTPUT"`);
const child = forkRunner(ws.write("entry.mjs", ""), 20_000, {
type: "execute",
interpreter: "bash",
interpreterPath: found.path,
scriptPath: slashed(script),
outputFile: slashed(outputFile),
outputJson: "{}",
timeoutMs: 20_000,
deadlineMs: 20_000,
maxOutputBytes: 1024 * 1024,
});
const answer = await new Promise<Record<string, unknown> | null>((resolve) => {
child.on("message", (raw) => {
const m = raw as { type?: string };
if (m.type === "result" || m.type === "failure") resolve(m);
});
child.once("exit", () => resolve(null));
});
expect(answer).toMatchObject({ type: "result", outputJson: '{"ran":true}' });
}, 30_000);
it("obeys the first request and ignores a second", async () => {
const ws = workspace();
const script = ws.write("once.mjs", `output.runs = (output.runs ?? 0) + 1;`);
const child = forkRunner(script, 20_000);
child.send({
type: "execute",
scriptUrl: pathToFileURL(fs.realpathSync(script)).href,
outputJson: JSON.stringify({ runs: 40 }),
deadlineMs: 20_000,
maxOutputBytes: 1024 * 1024,
});
const results: string[] = [];
await new Promise<void>((resolve) => {
child.on("message", (raw) => {
const m = raw as { type?: string; outputJson?: string };
if (m.type === "result") results.push(m.outputJson!);
});
child.once("exit", () => resolve());
});
expect(results).toEqual([JSON.stringify({ runs: 1 })]);
}, 30_000);
it("does not run the script after refusing a malformed request", async () => {
const ws = workspace();
const marker = ws.resolve("ran.txt");
const script = ws.write(
"never.mjs",
`import fs from "node:fs";
fs.writeFileSync(${JSON.stringify(marker)}, "ran");`
);
const child = forkRunner(script, 20_000, { type: "run" });
await exitOf(child);
expect(fs.existsSync(marker)).toBe(false);
}, 30_000);
it("leaves when the parent closes the protocol channel", async () => {
const ws = workspace();
const script = ws.write("waits.mjs", `await new Promise(() => {});`);
const child = forkRunner(script, 120_000);
await new Promise((resolve) => setTimeout(resolve, 300));
child.disconnect();
const exit = await exitOf(child);
expect(exit.code).toBe(0);
expect(exit.signal).toBeNull();
expect(exit.at).toBeLessThan(5_000);
}, 30_000);
it("stops a spinning script on its own deadline, with no help from the parent", async () => {
const ws = workspace();
const script = ws.write("spin.mjs", `for (;;) {}`);
const child = forkRunner(script, 1_200);
const exit = await exitOf(child);
expect(exit.signal).toBe("SIGKILL");
expect(exit.at).toBeGreaterThan(1_000);
expect(exit.at).toBeLessThan(6_000);
}, 30_000);
it("stops a spinning script when the parent's lifeline end closes", async () => {
const ws = workspace();
const script = ws.write("spin.mjs", `for (;;) {}`);
// A deadline far past the assertion window, so only the lifeline can be
// what stops it.
const child = forkRunner(script, 120_000);
const exit = exitOf(child);
await new Promise((resolve) => setTimeout(resolve, 400));
(child.stdio[4] as net.Socket | null)?.destroy();
expect((await exit).signal).toBe("SIGKILL");
}, 30_000);
});
describe("flow script executor — the lifeline end in the parent", () => {
it("unrefs it so one script step cannot hold the tool server past idle shutdown", async () => {
const original = net.Socket.prototype.unref;
const unreffed: net.Socket[] = [];
net.Socket.prototype.unref = function (this: net.Socket) {
unreffed.push(this);
return original.call(this);
};
cleanups.push(() => {
net.Socket.prototype.unref = original;
});
const ws = workspace();
const script = ws.write("quick.mjs", `output.ok = true;`);
const result = await executor().execute({ scriptPath: script, projectRoot: ws.dir });
expect(result.ok).toBe(true);
expect(unreffed.length).toBeGreaterThanOrEqual(1);
});
});