mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
src/next is src (the main export), src/next-node is src/node with short subpaths (/http, /workers, /plugins, /plugins/runtime, /app, /surfaces, /web, /react, /tui, /tui/launcher); the foundations get /json and /watch; Chord v1 moves to src/old (/old, /old/node) until p2 runs on v2; tests and docs move likewise (test/, docs/, test/old, docs/old). Durable and env import the foundations from their subpaths.
219 lines
11 KiB
TypeScript
219 lines
11 KiB
TypeScript
// Tests of testing a plugin alone (slice 12 §6): the plugin built as production builds it against the app's API
|
|
// modules, loaded on an in-memory host beside a stub serving what it uses; the registries it contributes to that nobody
|
|
// serves collected; the contracts it waits for unserved listed; a principal for the host and for a root scope.
|
|
|
|
import { join } from "node:path";
|
|
import { describe, expect, it } from "vitest";
|
|
import { BACKGROUND_CONTEXT as context } from "../../../src/context/index.ts";
|
|
import * as chord from "../../../src/index.ts";
|
|
import { defineFacet, int, local, str } from "../../../src/index.ts";
|
|
import { testPlugin } from "../../../src/node/plugins/index.ts";
|
|
import { rejection } from "../../support.ts";
|
|
import { tempRoot, writeFiles, writePlugin } from "./support.ts";
|
|
|
|
/** The app's API v3 for the tests: Chord's surface re-exported, and a worker module with the app's `Panels`. */
|
|
const Panels = chord.defineRegistry<{ readonly title: string }, unknown>("acme.panels", { version: 2 });
|
|
const MODULES = { "@acme/app/v3": { ...chord }, "@acme/app/v3/worker": { Panels } };
|
|
const API = { name: "@acme/app", versions: ["v3"] } as const;
|
|
|
|
/** The notes contract the stats plugin uses, as the notes plugin declares it. */
|
|
const Notes = local("acme.notes", { version: 1, params: { project: str() }, members: { words: int() } });
|
|
|
|
/** The stats plugin's contract, as a test of it declares it again. */
|
|
const Stats = local("acme.stats", { version: 1, params: { project: str() }, members: { words: int() } });
|
|
|
|
/** A root with the stats plugin: serves `Stats` over `Notes`, contributes a panel, and a tag cloud over `Tags`. */
|
|
async function statsPlugin(): Promise<string> {
|
|
const root = await tempRoot();
|
|
return writePlugin(root, "stats", "@acme/stats", {
|
|
"worker.ts": [
|
|
'import { constant, defineFacet, int, local, str } from "@acme/app/v3";',
|
|
'import { Panels } from "@acme/app/v3/worker";',
|
|
'const Notes = local("acme.notes", { version: 1, params: { project: str() }, members: { words: int() } });',
|
|
'const Tags = local("acme.tags", { version: 1, params: {}, members: { list: str() } });',
|
|
'const Stats = local("acme.stats", { version: 1, params: { project: str() }, members: { words: int() } });',
|
|
'const Cloud = local("acme.cloud", { version: 1, params: {}, members: { list: str() } });',
|
|
"export default defineFacet((host, self) => {",
|
|
' self.contribute(Panels, { title: "Word count" });',
|
|
" host.serve(Stats, ({ project }, self) => ({ words: self.get(Notes, { project }).words }));",
|
|
" host.serve(Cloud, (_params, self) => ({ list: self.get(Tags, {}).list }));",
|
|
"});",
|
|
].join("\n"),
|
|
});
|
|
}
|
|
|
|
/** A stub of the notes plugin: every project has 42 words. */
|
|
const notesStub = defineFacet((host) => host.serve(Notes, () => ({ words: chord.constant(42) })));
|
|
|
|
describe("testPlugin", () => {
|
|
it("loads the plugin built against the API beside a stub; collects its contributions and lists what it waits for", async () => {
|
|
const dir = await statsPlugin();
|
|
const t = await testPlugin(
|
|
dir,
|
|
{
|
|
kind: "worker",
|
|
api: API,
|
|
modules: MODULES,
|
|
stubs: [{ name: "@acme/notes", facet: notesStub }],
|
|
principal: { kind: "user", id: "u1" },
|
|
},
|
|
context,
|
|
);
|
|
const stats = await t.host.get(Stats, { project: "p1" }, context);
|
|
expect(stats.words.value).toBe(42);
|
|
expect(t.contributions(Panels).map(({ plugin, value }) => [plugin, value])).toEqual([
|
|
["@acme/stats", { title: "Word count" }],
|
|
]);
|
|
expect(t.unserved()).toEqual(["acme.tags"]);
|
|
expect(await t.root(async (ctx) => ctx.principal, context)).toEqual({ kind: "user", id: "u1" });
|
|
expect(await t.root(async (ctx) => ctx.principal, context, { principal: { kind: "user", id: "u2" } })).toEqual({
|
|
kind: "user",
|
|
id: "u2",
|
|
});
|
|
expect(() => t.contributions(chord.defineRegistry("acme.other", { version: 1 }))).toThrow(
|
|
"testPlugin: registry acme.other is not collected",
|
|
);
|
|
await t.close(context);
|
|
});
|
|
|
|
it("rejects a build production would refuse, and a setup that throws", async () => {
|
|
const root = await tempRoot();
|
|
const old = await writePlugin(root, "old", "@acme/old", {
|
|
"worker.ts": 'import { defineFacet } from "@acme/app/v1";\nexport default defineFacet(() => {});',
|
|
});
|
|
expect(await rejection(testPlugin(old, { kind: "worker", api: API, modules: MODULES }, context))).toMatchObject({
|
|
message: expect.stringContaining('"@acme/app/v1": @acme/app/v1 is not available: this app provides v3'),
|
|
});
|
|
const throwing = await writePlugin(root, "throwing", "@acme/throwing", {
|
|
"worker.ts":
|
|
'import { defineFacet } from "@acme/app/v3";\nexport default defineFacet(() => { throw new Error("no"); });',
|
|
});
|
|
expect(
|
|
await rejection(testPlugin(throwing, { kind: "worker", api: API, modules: MODULES }, context)),
|
|
).toMatchObject({ message: expect.stringMatching(/^testPlugin: setup failed: plugin "@acme\/throwing": no$/) });
|
|
});
|
|
|
|
it("builds as production by default: the per-kind check active, api required with versioned modules (Opus M5)", async () => {
|
|
const root = await tempRoot();
|
|
const dir = await writePlugin(root, "peek", "@acme/peek", {
|
|
"worker.ts":
|
|
'import { defineFacet } from "@acme/app/v3";\nimport { panel } from "./contract.web.ts";\nexport default defineFacet(() => console.log(panel));',
|
|
"contract.web.ts": 'export const panel = "panel";',
|
|
});
|
|
expect(await rejection(testPlugin(dir, { kind: "worker", api: API, modules: MODULES }, context))).toMatchObject({
|
|
message: expect.stringContaining('"./contract.web.ts": a web file, imported into a worker facet'),
|
|
});
|
|
expect(await rejection(testPlugin(dir, { kind: "worker", modules: MODULES }, context))).toMatchObject({
|
|
message: 'testPlugin: modules hold the app\'s versioned API ("@acme/app/v3"): pass api, as production does',
|
|
});
|
|
});
|
|
|
|
it("tests a single-file plugin and a plugin in a checkout of several (Opus L4)", async () => {
|
|
const root = await tempRoot();
|
|
await writeFiles(root, {
|
|
"hello.worker.ts": [
|
|
'import { constant, defineFacet, local, str } from "@acme/app/v3";',
|
|
'const Hello = local("acme.hello", { version: 1, params: {}, members: { text: str() } });',
|
|
'export default defineFacet((host) => host.serve(Hello, () => ({ text: constant("hello") })));',
|
|
].join("\n"),
|
|
});
|
|
const single = await testPlugin(
|
|
join(root, "hello.worker.ts"),
|
|
{ kind: "worker", api: API, modules: MODULES },
|
|
context,
|
|
);
|
|
const Hello = local("acme.hello", { version: 1, params: {}, members: { text: str() } });
|
|
expect((await single.host.get(Hello, {}, context)).text.value).toBe("hello");
|
|
await single.close(context);
|
|
// A checkout of several (its own package.json) beside another plugin of the root it copies from.
|
|
await writePlugin(root, "words", "@acme/words", { "contract.ts": 'export const WORDS = "words";' });
|
|
await writeFiles(join(root, "repo"), { "package.json": JSON.stringify({ name: "@bob/repo" }) });
|
|
const inner = await writePlugin(join(root, "repo"), "counter", "@bob/counter", {
|
|
"worker.ts": [
|
|
'import { constant, defineFacet, local, str } from "@acme/app/v3";',
|
|
'import { WORDS } from "../../words/contract.ts";',
|
|
'const Count = local("bob.count", { version: 1, params: {}, members: { text: str() } });',
|
|
"export default defineFacet((host) => host.serve(Count, () => ({ text: constant(WORDS) })));",
|
|
].join("\n"),
|
|
});
|
|
const checkout = await testPlugin(inner, { kind: "worker", api: API, modules: MODULES }, context);
|
|
const Count = local("bob.count", { version: 1, params: {}, members: { text: str() } });
|
|
expect((await checkout.host.get(Count, {}, context)).text.value).toBe("words");
|
|
await checkout.close(context);
|
|
});
|
|
|
|
it("collects until no new registry appears: an object live after the first collection contributes too (Sol)", async () => {
|
|
const root = await tempRoot();
|
|
const dir = await writePlugin(root, "chain", "@acme/chain", {
|
|
"worker.ts": [
|
|
'import { constant, defineFacet, defineRegistry, local, str } from "@acme/app/v3";',
|
|
'const A = defineRegistry("acme.a", { version: 1 });',
|
|
'const B = defineRegistry("acme.b", { version: 1 });',
|
|
'const Reader = local("acme.reader", { version: 1, params: {}, members: { s: str() } });',
|
|
"export default defineFacet((host, self) => {",
|
|
'\tself.contribute(A, "a");',
|
|
'\thost.serve(Reader, (_params, self) => { self.registry(A); self.contribute(B, "b"); return { s: constant("x") }; });',
|
|
"});",
|
|
].join("\n"),
|
|
});
|
|
const t = await testPlugin(dir, { kind: "worker", api: API, modules: MODULES }, context);
|
|
const B = chord.defineRegistry<string, unknown>("acme.b", { version: 1 });
|
|
expect(t.contributions(B).map(({ value }) => value)).toEqual(["b"]);
|
|
await t.close(context);
|
|
});
|
|
|
|
it("waits for slow activations a collection woke before it looks again (Opus R1)", async () => {
|
|
const root = await tempRoot();
|
|
const dir = await writePlugin(root, "slow", "@acme/slow", {
|
|
"worker.ts": [
|
|
'import { constant, defineFacet, defineRegistry, local, str } from "@acme/app/v3";',
|
|
'const A = defineRegistry("acme.a", { version: 1 });',
|
|
'const B = defineRegistry("acme.b", { version: 1 });',
|
|
'const Reader = local("acme.reader", { version: 1, params: {}, members: { s: str() } });',
|
|
"export default defineFacet((host, self) => {",
|
|
'\tself.contribute(A, "a");',
|
|
"\thost.serve(Reader, (_params, self) => {",
|
|
"\t\tself.registry(A);",
|
|
"\t\tself.open(() => new Promise((resolve) => setTimeout(resolve, 50)), () => {});",
|
|
'\t\tself.contribute(B, "b");',
|
|
'\t\treturn { s: constant("x") };',
|
|
"\t});",
|
|
"});",
|
|
].join("\n"),
|
|
});
|
|
const t = await testPlugin(dir, { kind: "worker", api: API, modules: MODULES }, context);
|
|
const B = chord.defineRegistry<string, unknown>("acme.b", { version: 1 });
|
|
expect(t.contributions(B).map(({ value }) => value)).toEqual(["b"]);
|
|
await t.close(context);
|
|
});
|
|
|
|
it("serves a registry contributed at a high version without an adapter per older version (Sol)", async () => {
|
|
const root = await tempRoot();
|
|
const dir = await writePlugin(root, "high", "@acme/high", {
|
|
"worker.ts":
|
|
'import { defineFacet, defineRegistry } from "@acme/app/v3";\nconst R = defineRegistry("acme.high", { version: 2000000 });\nexport default defineFacet((_host, self) => self.contribute(R, 1));',
|
|
});
|
|
const started = performance.now();
|
|
const t = await testPlugin(dir, { kind: "worker", api: API, modules: MODULES }, context);
|
|
expect(performance.now() - started).toBeLessThan(1_000);
|
|
const R = chord.defineRegistry<number, unknown>("acme.high", { version: 1 });
|
|
expect(t.contributions(R).map(({ value }) => value)).toEqual([1]);
|
|
await t.close(context);
|
|
});
|
|
|
|
it("takes no kind from test and declaration files (Opus N6)", async () => {
|
|
const root = await tempRoot();
|
|
await writeFiles(root, {
|
|
"hello.worker.ts": 'import { defineFacet } from "@acme/app/v3";\nexport default defineFacet(() => {});',
|
|
"hello.test.ts": "",
|
|
"env.d.ts": "",
|
|
});
|
|
const t = await testPlugin(
|
|
join(root, "hello.worker.ts"),
|
|
{ kind: "worker", api: API, modules: MODULES },
|
|
context,
|
|
);
|
|
expect(Object.keys(t.plugin.facets)).toEqual(["worker"]);
|
|
await t.close(context);
|
|
});
|
|
});
|