pi-mono/packages/chord/test/contracts/host-kind.test.ts
Mario Zechner e9c0ff4d84 feat(chord): Chord v2 is the package's root
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.
2026-11-02 22:36:55 +00:00

84 lines
3.5 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { BACKGROUND_CONTEXT as context } from "../../src/context/index.ts";
import { evolve, hostKind, int, oneOf, opt, remote, str } from "../../src/contracts/index.ts";
import { contractData, kindParams } from "../../src/contracts/internal.ts";
import { isChordError } from "../../src/data/index.ts";
import { checkAddress } from "../../src/data/internal.ts";
import { expectCollected } from "../support.ts";
const Server = hostKind("server");
const Session = hostKind("session", { id: str() });
const Shard = hostKind("shard", { region: oneOf("eu", "us"), n: int({ min: 0 }) });
describe("host kinds", () => {
it("make addresses: the kind, the params and a key built like an identity key", () => {
expect(Server()).toEqual({ kind: "server", params: {}, key: "server" });
expect(Session({ id: "S1" })).toEqual({ kind: "session", params: { id: "S1" }, key: "session|S1" });
expect(Shard({ region: "eu", n: 3 }).key).toBe("shard|eu|3");
// Escaped like identity keys: two different param lists never make one key.
expect(Session({ id: "a|b" }).key).not.toBe(Shard({ region: "a" as "eu", n: "b" as unknown as number }).key);
expect(Session({ id: "a|b" }).key).toBe("session|a\\|b");
});
it("follow the params policy of contracts at declaration", () => {
// @ts-expect-error: params fields are required
expect(() => hostKind("k", { id: opt(str()) })).toThrow("k.params.id: a param is");
expect(() => hostKind("")).toThrow(`A host kind's name "": expected lowercase [a-z0-9@/._~-]`);
expect(() => hostKind("Session")).toThrow("expected lowercase");
expect(() => hostKind("a:b")).toThrow("expected lowercase");
expect(() => hostKind("client")).toThrow('"client" is reserved for hosts without an address');
expect(hostKind("@pi/session.v2_~-").kind).toBe("@pi/session.v2_~-");
});
it("check an address's params where it is routed, with the kind's compiled check: invalid/host_address", () => {
const params = kindParams(Session);
expect(checkAddress("session", params, { id: "S1" }, context).key).toBe("session|S1");
for (const bad of [{ id: 1 }, {}, { id: "S1", extra: 1 }, null]) {
let thrown: unknown;
try {
checkAddress("session", params, bad, context);
} catch (error) {
thrown = error;
}
expect(isChordError(thrown, "invalid") && thrown.reason).toBe("host_address");
}
});
it("keep their params data only while the kind is referenced", async () => {
const ref = (() => {
const Temp = hostKind("temp", { id: str() });
kindParams(Temp);
return new WeakRef(Temp);
})();
await expectCollected(ref);
});
});
describe("hostAddress", () => {
const variants = { v: { state: { n: int() } } };
it("is not part of the fingerprint and is carried by evolve", () => {
const hostAddress = ({ session }: { session: string }) => Session({ id: session });
const onSession = remote("app.placed", { version: 1, params: { session: str() }, hostAddress, variants });
const onServer = remote("app.placed", {
version: 1,
params: { session: str() },
hostAddress: () => Server(),
variants,
});
expect(onSession.fingerprint).toBe(onServer.fingerprint);
expect(contractData(onSession).hostAddress).toBe(hostAddress);
const next = evolve(onSession, (v) => v.addState("v", "m", int()));
expect(contractData(next).hostAddress).toBe(hostAddress);
});
it("is typed by the contract's params", () => {
remote("app.typed", {
version: 1,
params: { session: str() },
// @ts-expect-error: the params have no `id`
hostAddress: ({ id }) => Session({ id }),
variants,
});
});
});