Files
oh-my-pi/packages/coding-agent/test/eval/runtime-global-dispose.test.ts
T
can1357 db58a163b2 fix(coding-agent): isolated JS eval runtime globals across runtime lifetimes
- Made `JsRuntime` track the realm globals it installs (`#globalOwner`, `#ownedGlobalKeys`, `PRELUDE_GLOBAL_KEYS`) and reclaim them in `dispose()`, re-binding ownership through `#activateGlobals()` on cwd/run-scope/run; disposing an older inline/direct runtime no longer deletes a newer runtime's helper globals, and overlapping same-realm runs are serialized via `enterGlobalRun`.
- Added `WorkerCore.dispose()` (rejects pending tool calls with `ToolError` and disposes the runtime) and invoked it from `spawnInlineWorker` teardown in `context-manager.ts`.
- Updated the `js-static-import-rewrite` test to snapshot and restore any pre-existing `__omp_import__` global instead of unconditionally deleting it.
- Added `runtime-global-dispose.test.ts` covering newer-runtime global survival, older-runtime reactivation, and cross-runtime mutation rejection.
- Clarified the `runWorkerEntrypoint` comment in `cli.ts` about `parentPort` sync-prefix buffering for tab/eval workers.
2026-06-17 01:21:33 +02:00

135 lines
4.4 KiB
TypeScript

import { describe, expect, it } from "bun:test";
import { JsRuntime, type RuntimeHooks } from "@oh-my-pi/pi-coding-agent/eval/js/shared/runtime";
const GLOBAL_KEYS = ["__omp_import__", "read"] as const;
type GlobalKey = (typeof GLOBAL_KEYS)[number];
interface GlobalSnapshot {
exists: boolean;
value: unknown;
}
function snapshotGlobals(): Record<GlobalKey, GlobalSnapshot> {
const globals = globalThis as Record<string, unknown>;
return {
__omp_import__: { exists: "__omp_import__" in globals, value: globals.__omp_import__ },
read: { exists: "read" in globals, value: globals.read },
};
}
function restoreGlobals(snapshot: Record<GlobalKey, GlobalSnapshot>): void {
const globals = globalThis as Record<string, unknown>;
for (const key of GLOBAL_KEYS) {
const state = snapshot[key];
if (state.exists) globals[key] = state.value;
else delete globals[key];
}
}
function expectGlobalsRestored(snapshot: Record<GlobalKey, GlobalSnapshot>): void {
const globals = globalThis as Record<string, unknown>;
for (const key of GLOBAL_KEYS) {
const state = snapshot[key];
if (state.exists) expect(globals[key]).toBe(state.value);
else expect(key in globals).toBe(false);
}
}
const hooks: RuntimeHooks = {
onText: () => {},
onDisplay: () => {},
callTool: async () => undefined,
};
describe("JsRuntime global disposal", () => {
it("keeps newer same-realm runtime globals after disposing an older runtime", () => {
const globals = globalThis as Record<string, unknown>;
const before = snapshotGlobals();
const first = new JsRuntime({ initialCwd: process.cwd(), sessionId: "first" });
const firstImport = globals.__omp_import__;
const firstRead = globals.read;
const second = new JsRuntime({ initialCwd: process.cwd(), sessionId: "second" });
const secondImport = globals.__omp_import__;
try {
expect(typeof firstImport).toBe("function");
expect(typeof firstRead).toBe("function");
expect(secondImport).not.toBe(firstImport);
expect(typeof globals.read).toBe("function");
first.dispose();
expect(globals.__omp_import__).toBe(secondImport);
expect(globals.__omp_helpers__).toBe(second.helpers);
expect(typeof globals.read).toBe("function");
second.dispose();
expectGlobalsRestored(before);
} finally {
first.dispose();
second.dispose();
restoreGlobals(before);
}
});
it("reactivates older same-realm runtime globals when no other run is active", async () => {
const globals = globalThis as Record<string, unknown>;
const before = snapshotGlobals();
const first = new JsRuntime({ initialCwd: process.cwd(), sessionId: "first-reactivated" });
const second = new JsRuntime({ initialCwd: process.cwd(), sessionId: "second-reactivated" });
try {
expect(globals.__omp_helpers__).toBe(second.helpers);
first.setCwd(process.cwd());
expect(globals.__omp_helpers__).toBe(first.helpers);
first.setRunScope({ reactivatedProbe: 7 });
expect(globals.reactivatedProbe).toBe(7);
expect(await first.run("1 + 6;", undefined, hooks)).toBe(7);
second.setCwd(process.cwd());
expect(globals.__omp_helpers__).toBe(second.helpers);
} finally {
delete globals.reactivatedProbe;
first.dispose();
second.dispose();
restoreGlobals(before);
}
});
it("rejects cross-runtime mutations while another same-realm runtime is running", async () => {
const before = snapshotGlobals();
const globals = globalThis as Record<string, unknown>;
const first = new JsRuntime({ initialCwd: process.cwd(), sessionId: "first-overlap" });
const second = new JsRuntime({ initialCwd: process.cwd(), sessionId: "second-overlap" });
const gate = Promise.withResolvers<void>();
let activeSecond: Promise<unknown> | undefined;
try {
second.setRunScope({ gate: gate.promise });
activeSecond = second.run("await gate;", undefined, hooks);
expect(() => first.setCwd(process.cwd())).toThrow("another same-realm JS runtime is running");
await first.run("1", undefined, hooks).then(
() => {
throw new Error("expected active runtime rejection");
},
error =>
expect(error).toHaveProperty(
"message",
"Cannot run code while another same-realm JS runtime is running",
),
);
gate.resolve();
await activeSecond;
first.setCwd(process.cwd());
expect(globals.__omp_helpers__).toBe(first.helpers);
} finally {
gate.resolve();
if (activeSecond) await activeSecond.catch(() => undefined);
delete globals.gate;
first.dispose();
second.dispose();
restoreGlobals(before);
}
});
});