pi-mono/packages/chord/test/next-node/plugins/stack.test.ts
Mario Zechner a14cdf90f0 feat(chord): v2 slice 7c, plugins component and per-plugin load and reload
Plugin discovery, esbuild builds (optional peer dependency), vm evaluation with
pluginFile and mapStack, dependency installs, a watcher with dirty marks and
rollout across hosts; host.load and host.reload per plugin with the closure rule;
examples 35-39 as folders with real plugin directories.
2026-10-30 18:18:23 +00:00

123 lines
5.2 KiB
TypeScript

// Tests of stack traces (slice 7c §3, decision Q1 (b)): `mapStack` names plugin files and lines through each
// evaluated script's source map; nothing global; a script's map goes with its facet.
import { describe, expect, it } from "vitest";
import { BACKGROUND_CONTEXT as context } from "../../../src/context/index.ts";
import {
type BuiltScript,
buildFacet,
discoverPlugins,
evaluateFacet,
mapStack,
type Plugin,
} from "../../../src/next-node/plugins/index.ts";
import { expectCollected, settledHeap } from "../../next/support.ts";
import { HOST_PACKAGES, KINDS, tempRoot, writePlugin } from "./support.ts";
/** A plugin `notes` whose worker facet's `fail` throws from `helper.ts` line 3. */
async function failing(): Promise<{ script: BuiltScript; plugin: Plugin }> {
const root = await tempRoot();
await writePlugin(root, "notes", "@test/notes", {
"worker.ts": 'import { helper } from "./helper.ts";\nexport default { setup: () => {}, fail: () => helper() };',
"helper.ts":
'// A helper.\nexport function helper(): never {\n\tthrow new Error("helper failed at " + Date.now());\n}',
});
const { plugins } = await discoverPlugins([root], { kinds: KINDS }, context);
const plugin = plugins[0]!;
const built = await buildFacet(
plugin,
"worker",
{ hostPackages: Object.keys(HOST_PACKAGES), platform: "node" },
context,
);
if (built.kind === "failed") throw new Error(built.errors.join("\n"));
return { script: built, plugin };
}
/** The error `facet`'s `fail` throws. */
function thrownBy(facet: object): unknown {
try {
(facet as { readonly fail: () => void }).fail();
} catch (error) {
return error;
}
throw new Error("fail did not throw");
}
describe("mapStack", () => {
it("maps a plugin script's positions to the plugin's files and lines; other lines stay; nothing global", async () => {
const { script, plugin } = await failing();
const prepare = Error.prepareStackTrace;
const facet = evaluateFacet(script, { hostPackages: HOST_PACKAGES, plugin });
const error = thrownBy(facet);
const mapped = mapStack(error);
expect(mapped).toMatch(/at \S+ \(notes\/helper\.ts:3:8\)/);
expect(mapped).toMatch(/notes\/worker\.ts:2:\d+/);
expect(mapped).not.toContain("/chord-plugins/");
// The raw stack is untouched: no `Error.prepareStackTrace` of the component's.
expect((error as Error).stack).toContain("/chord-plugins/@test/notes/worker.ts?");
expect(Error.prepareStackTrace).toBe(prepare);
expect(mapStack("not an error")).toBe("not an error");
});
it("lets a script's source map go with its facet", async () => {
const { script, plugin } = await failing();
const error = await (async () => {
const facet = evaluateFacet(script, { hostPackages: HOST_PACKAGES, plugin });
return { error: thrownBy(facet), ref: new WeakRef(facet) };
})();
expect(mapStack(error.error)).toContain("notes/helper.ts:3:8");
await expectCollected(error.ref);
// The finalization runs after the collection.
for (let round = 0; round < 10 && mapStack(error.error).includes("notes/helper.ts"); round++) await settledHeap();
expect(mapStack(error.error)).toContain("/chord-plugins/@test/notes/worker.ts?");
});
});
describe("mapStack, fix round 1", () => {
it("maps an import-time throw (the map is registered before the script runs) (Sol)", async () => {
const root = await tempRoot();
await writePlugin(root, "evaluation", "@test/evaluation", {
"worker.ts":
'// An import-time throw.\n\nthrow new Error("at import");\nexport default { setup: () => {} };\n',
});
const { plugins } = await discoverPlugins([root], { kinds: KINDS }, context);
const plugin = plugins[0]!;
const built = await buildFacet(plugin, "worker", { hostPackages: [], platform: "node" }, context);
if (built.kind === "failed") throw new Error(built.errors.join("\n"));
let failure: unknown;
try {
evaluateFacet(built, { hostPackages: {}, plugin });
} catch (error) {
failure = error;
}
expect(mapStack(failure)).toContain("evaluation/worker.ts:3:");
});
it("maps a throw while its default export is read (a getter) (Sol)", async () => {
const root = await tempRoot();
await writePlugin(root, "getter", "@test/getter", {
"worker.ts": '// A getter failure.\nexport default {\n\tget setup() { throw new Error("getter boom"); } };\n',
});
const { plugins } = await discoverPlugins([root], { kinds: KINDS }, context);
const plugin = plugins[0]!;
const built = await buildFacet(plugin, "worker", { hostPackages: [], platform: "node" }, context);
if (built.kind === "failed") throw new Error(built.errors.join("\n"));
let failure: unknown;
try {
evaluateFacet(built, { hostPackages: {}, plugin });
} catch (error) {
failure = error;
}
expect(mapStack(failure)).toContain("getter/worker.ts:3:");
});
it("keeps a script's map while any evaluation of it lives: two hosts share it (Sol, Opus N3)", async () => {
const { script, plugin } = await failing();
const first = evaluateFacet(script, { hostPackages: HOST_PACKAGES, plugin });
const second = (() => new WeakRef(evaluateFacet(script, { hostPackages: HOST_PACKAGES, plugin })))();
await expectCollected(second);
for (let round = 0; round < 5; round++) await settledHeap();
expect(mapStack(thrownBy(first))).toContain("notes/helper.ts:3:8");
});
});