Files
dev/third_party/pi/packages/chord/test/placement.test.ts
alexion 6081e3e325 chore(vendor): import pi 1.3.0
Imported as a linear vendor commit so forge rebases do not replay the upstream root tree.

git-subtree-dir: third_party/pi

git-subtree-split: 71023aec1546d6d677a65669c5665c0927ce0dd0
2026-10-15 17:58:48 -04:00

199 lines
8.3 KiB
TypeScript

import assert from "node:assert/strict";
import { Type } from "typebox";
import { describe, it } from "vitest";
import {
bind,
createHost,
type Facet,
type Frame,
type Host,
hostFamily,
hostNamed,
isChordError,
type Link,
memoryLinkPair,
method,
PROTOCOL_VERSION,
remote,
} from "../src/index.ts";
const tick = () => new Promise<void>((resolve) => setImmediate(resolve));
/** Lives on `session:` + its input's `session`. */
const Note = remote("t.note", {
version: 1,
input: Type.Object({ session: Type.String(), id: Type.String() }),
host: hostFamily("session:", "session"),
variants: { main: { methods: { where: method(undefined, Type.String()) } } },
});
/** Lives on `server`. */
const Catalog = remote("t.catalog", {
version: 1,
input: Type.Object({}),
host: hostNamed("server"),
variants: { main: { methods: { where: method(undefined, Type.String()) } } },
});
/** A server with two session hosts as children; every host serves what it may and reports its name. */
function cluster() {
const server = createHost({ name: "server", self: "host", onError: () => {} });
server.serve(Catalog, () => ({ variant: "main", where: () => "server" }));
const children = new Map<string, Host>();
const links = new Map<string, Link>();
const child = (name: string) => {
const [down, up] = memoryLinkPair();
links.set(name, down);
server.accept(down, { kind: "host", name, principal: "server-side" });
const host = createHost({ name, self: "host", upstream: { link: up, principal: "server" }, onError: () => {} });
host.serve(Note, ({ id }) => ({ variant: "main", where: () => `${host.name} ${id}` }));
children.set(name, host);
return { host, down };
};
const s1 = child("session:S1");
const s2 = child("session:S2");
const close = async () => {
for (const host of children.values()) await host.close();
await server.close();
};
// Relays need a route (where authorization lives); it finds the connected child.
server.route("session:", {
authorize: () => true,
resolve: async (name) => links.get(name)!,
principal: (name) => name,
});
return { server, s1: s1.host, s2: s2.host, close };
}
describe("placement", () => {
it("a get goes where the contract places the input: here, a child, or the parent, never named by the caller", async () => {
const { server, s1, close } = cluster();
await tick();
assert.equal(await server.hold(Note, { session: "S1", id: "a" }).where(), "session:S1 a");
assert.equal(await server.hold(Note, { session: "S2", id: "b" }).where(), "session:S2 b");
// From a session host: its own notes locally, the other session's through the server, the catalog upstream.
assert.equal(await s1.hold(Note, { session: "S1", id: "c" }).where(), "session:S1 c");
assert.equal(await s1.hold(Catalog, {}).where(), "server");
await close();
});
it("serve refuses a type the host is not the place of, also at a reload", async () => {
const host = createHost({ name: "session:S1", self: "host", onError: () => {} });
assert.throws(
() => host.serve(Catalog, () => ({ variant: "main", where: () => "" })),
/t\.catalog is declared for server, but session:S1 serves it/,
);
const server = createHost({ name: "server", self: "host", onError: () => {} });
assert.throws(
() => server.serve(Note, () => ({ variant: "main", where: () => "" })),
/t\.note is declared for session:<session>, but server serves it/,
);
const misplaced: Facet = {
name: "misplaced",
setup: (api) => api.serve(Note, () => ({ variant: "main", where: () => "" })),
};
await assert.rejects(server.reload([misplaced]), /declared for session:<session>, but server serves it/);
await host.close();
await server.close();
});
it("a host refuses a hold its own contract places elsewhere, and a relay one whose target disagrees", async () => {
const { server, close } = cluster();
await tick();
const holds = async (target: string, input: unknown): Promise<Frame | undefined> => {
const [down, raw] = memoryLinkPair();
server.accept(down, { principal: "guest" });
const frames: Frame[] = [];
raw.receive((frame) => frames.push(frame));
raw.send({ t: "hello", protocol: PROTOCOL_VERSION, name: "raw", kind: "client", codeVersion: 0 });
raw.send({ t: "hold", id: 1, at: target, token: Note.description, input });
for (let i = 0; i < 50 && !frames.some((frame) => frame.t === "shape" || frame.t === "gone"); i++)
await tick();
return frames.find((frame) => frame.t === "shape" || frame.t === "gone");
};
// A client picks session:S2 for a note of S1: the relay refuses, nothing reaches S2.
const relayed = await holds("session:S2", { session: "S1", id: "x" });
assert.equal(relayed?.t, "gone");
assert.match(relayed?.t === "gone" ? relayed.error.message : "", /placement|does not live on session:S2/);
// The right target is served.
assert.equal((await holds("session:S1", { session: "S1", id: "x" }))?.t, "shape");
// Addressed to the server itself, a note is refused by the server's own contract check.
const direct = await holds("server", { session: "S1", id: "x" });
assert.equal(direct?.t, "gone");
await close();
});
it("the host comes from the normalized input: unknown fields and defaults do not change it", async () => {
const Doc = remote("t.doc-default", {
version: 1,
input: Type.Object({ session: Type.String({ default: "S1" }) }),
host: hostFamily("session:", "session"),
variants: { main: { methods: { where: method(undefined, Type.String()) } } },
});
const { server, s1, close } = cluster();
s1.serve(Doc, () => ({ variant: "main", where: () => "session:S1" }));
await tick();
// An untyped caller leaves the field out: the default places it.
assert.equal(await server.hold(Doc, {} as never).where(), "session:S1");
assert.throws(() => server.hold(Note, { session: "", id: "x" }), /input field session names no host/);
await close();
});
it("placement is part of the contract: a moved type is a breaking change, never bridged by an adapter", async () => {
const at = (host: string) =>
remote("t.moved", {
version: 1,
input: Type.Object({}),
host: hostNamed(host),
variants: { main: { methods: { ping: method() } } },
});
assert.notEqual(at("server").fingerprint, at("session:S1").fingerprint);
const host = createHost({ name: "server", self: "host", onError: () => {} });
const facet = (version: number, place: string): Facet => ({
name: "moved",
setup: (api) =>
api.serve(
remote("t.moved", {
version,
input: Type.Object({}),
host: hostNamed(place),
variants: { main: { methods: { ping: method() } } },
}),
() => ({ variant: "main", ping: () => {} }),
{ accepts: { 1: (current) => current } },
),
});
await host.install(facet(1, "server"));
await host.get(at("server"), {});
// Same version, other placement: a forgotten bump.
await assert.rejects(host.reload([facet(1, "session:S1")]), /declared for session:S1|without a version bump/);
// A new version that `accepts` version 1 but lives elsewhere: no adapter makes version 1 consumers bind.
const moved = remote("t.moved", {
version: 2,
input: Type.Object({}),
host: hostNamed("session:S1"),
variants: { main: { methods: { ping: method() } } },
});
assert.throws(
() => bind(moved, { 1: (current) => current }, at("server")),
(error) => isChordError(error, "contract_mismatch") && /lives on session:S1/.test(error.message),
);
await host.close();
});
it("types: the family's field is a required string of the input, and remote contracts need a host", () => {
const input = Type.Object({ session: Type.String(), count: Type.Number(), note: Type.Optional(Type.String()) });
const variants = { main: {} };
remote("t.ok", { version: 1, input, host: hostFamily("session:", "session"), variants });
// @ts-expect-error: no such field
remote("t.typo", { version: 1, input, host: hostFamily("session:", "sesion"), variants });
// @ts-expect-error: not a string
remote("t.number", { version: 1, input, host: hostFamily("session:", "count"), variants });
// @ts-expect-error: optional
remote("t.optional", { version: 1, input, host: hostFamily("session:", "note"), variants });
// @ts-expect-error: a remote contract says where it lives
assert.throws(() => remote("t.nowhere", { version: 1, input, variants }), /needs `host`/);
assert.throws(() => hostFamily("", "session"), /prefix/);
});
});