117 lines
6.5 KiB
TypeScript
117 lines
6.5 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { test } from "node:test";
|
|
import { generatePortableTypeScriptBindings } from "../src/bindings/portable.js";
|
|
import { compileExecutionPlan } from "../src/execution-world/plan.js";
|
|
import { loadReaderWorkspace } from "./reader-fixture.js";
|
|
|
|
test("service bindings depend on their consumed contract, not workspace or source provenance", async () => {
|
|
const schema = compileExecutionPlan(await loadReaderWorkspace()).definition.bindingSchema;
|
|
const pkg = schema.packages[0]!;
|
|
const before = generatePortableTypeScriptBindings(schema, pkg.revisionId, { target: "server" });
|
|
const other = structuredClone(pkg);
|
|
other.revisionId = "unrelated@1" as typeof other.revisionId;
|
|
other.exports = [];
|
|
schema.packages.push(other);
|
|
pkg.source.commit = "b".repeat(40);
|
|
pkg.revisionId = "same-service-new-source@2" as typeof pkg.revisionId;
|
|
assert.equal(generatePortableTypeScriptBindings(schema, pkg.revisionId, { target: "server" }), before);
|
|
assert.doesNotMatch(before, /export const (bindingDigest|packageRevision)/);
|
|
pkg.exports[0]!.inputType = { kind: "scalar", name: "bool" };
|
|
assert.notEqual(generatePortableTypeScriptBindings(schema, pkg.revisionId, { target: "server" }), before);
|
|
});
|
|
|
|
test("reader portable bindings have selected facets, creation inputs and stable keyed calls", async () => {
|
|
const schema = compileExecutionPlan(await loadReaderWorkspace()).definition.bindingSchema;
|
|
const source = generatePortableTypeScriptBindings(schema, "package:reader-runtime@1", { target: "browser" });
|
|
assert.match(source, /readonly "sourceRevision.get": \(\) => Promise<string>/);
|
|
assert.match(
|
|
source,
|
|
/readonly "highlights.connect": \(input: ManagedReference<"atom", "atom:reader:highlight">, options: MutationKey\)/,
|
|
);
|
|
assert.match(source, /readonly "createDocument"/);
|
|
assert.match(source, /readonly "create":/);
|
|
assert.match(source, /readonly "comments": ReadonlyArray/);
|
|
assert.doesNotMatch(source, /"highlights.resolve"|context\.conform|objectId|"live"/);
|
|
assert.equal(source, generatePortableTypeScriptBindings(schema, "package:reader-runtime@1", { target: "browser" }));
|
|
const server = generatePortableTypeScriptBindings(schema, "package:reader-runtime@1", { target: "server" });
|
|
assert.equal(source.replace('target = "browser"', 'target = "server"'), server);
|
|
});
|
|
|
|
test("generation rejects remote dependencies, wrong effects and unsupported shapes explicitly", async () => {
|
|
const schema = compileExecutionPlan(await loadReaderWorkspace()).definition.bindingSchema;
|
|
const generate = () => generatePortableTypeScriptBindings(schema, "package:reader-runtime@1", { target: "browser" });
|
|
const entry = schema.packages[0]!.exports.find((e) => e.displayName === "addHighlight")!;
|
|
entry.execution = { kind: "snapshot", targets: ["browser"] };
|
|
assert.throws(generate, /Snapshot context cannot expose mutation ports/);
|
|
entry.execution = { kind: "transaction", targets: ["browser"] };
|
|
const iface = schema.interfaces.find((i) => i.revisionId === "interface:reader:document-data@1")!;
|
|
const getter = iface.members[0]!.operations[0]!;
|
|
getter.execution = { kind: "remote" };
|
|
assert.throws(generate, /not an object call available in portable target browser/);
|
|
getter.execution = { kind: "snapshot", targets: ["browser", "server"] };
|
|
entry.dependencyPorts[0]!.requirement = { kind: "reflection", selectors: ["active"] };
|
|
assert.throws(generate, /Only checked presentation metadata is portable/);
|
|
});
|
|
|
|
test("closed generic portable handlers have distinct concrete codecs and no universal casts", async () => {
|
|
const { portableGenericPlan } = await import("./portable-generic-fixture.js");
|
|
const plan = portableGenericPlan();
|
|
const schema = plan.definition.bindingSchema;
|
|
const pkg = schema.packages[0]!;
|
|
assert.equal(pkg.exports.length, 2);
|
|
const generated = generatePortableTypeScriptBindings(schema, pkg.revisionId, { target: "browser" });
|
|
assert.match(generated, /readonly input: string/);
|
|
assert.match(generated, /readonly input: ReadonlyArray<bigint>/);
|
|
for (const entry of pkg.exports) {
|
|
assert.ok(entry.application);
|
|
assert.ok(generated.includes(JSON.stringify(entry.id)));
|
|
assert.ok(generated.includes(JSON.stringify(entry.displayName)));
|
|
}
|
|
assert.notEqual(pkg.exports[0]!.displayName, pkg.exports[1]!.displayName);
|
|
assert.equal(
|
|
generated.replace('target = "browser"', 'target = "server"'),
|
|
generatePortableTypeScriptBindings(schema, pkg.revisionId, { target: "server" }),
|
|
);
|
|
});
|
|
|
|
test("a portable subset of a mixed interface excludes remote dependencies", async () => {
|
|
const schema = compileExecutionPlan(await loadReaderWorkspace()).definition.bindingSchema;
|
|
const entry = schema.packages.flatMap((p) => p.exports).find((e) => e.displayName === "addHighlight")!;
|
|
const iface = schema.interfaces.find((i) => i.revisionId === "interface:reader:document-data@1")!;
|
|
const selected = entry.dependencyPorts.find((p) => p.requirement.kind === "interface")!.requirement;
|
|
assert.equal(selected.kind, "interface");
|
|
if (selected.kind !== "interface") throw Error("interface required");
|
|
const operation = structuredClone(iface.members[0]!.operations[0]!);
|
|
operation.id = "document:remote:get" as typeof operation.id;
|
|
operation.execution = { kind: "remote" };
|
|
iface.members.push({
|
|
...structuredClone(iface.members[0]!),
|
|
id: "document:remote" as (typeof iface.members)[0]["id"],
|
|
displayName: "remote",
|
|
operations: [operation],
|
|
});
|
|
const generate = () => generatePortableTypeScriptBindings(schema, "package:reader-runtime@1", { target: "browser" });
|
|
assert.doesNotMatch(generate(), /readonly "remote.get"/);
|
|
selected.operations!.push(operation.id);
|
|
assert.throws(generate, /select a portable subset/);
|
|
});
|
|
|
|
test("server projection descriptors are distinct from canonical inputs and have result decoders", async () => {
|
|
const { generatePortableClientBindings } = await import("../src/bindings/portable-client.js");
|
|
const plan = compileExecutionPlan(await loadReaderWorkspace());
|
|
const source = generatePortableClientBindings(plan, { target: "browser" });
|
|
const projections = source.slice(
|
|
source.indexOf("export const serverProjections"),
|
|
source.indexOf("export const results"),
|
|
);
|
|
assert.match(projections, /ConfirmedColor/);
|
|
assert.match(projections, /satisfies ServerProjection/);
|
|
assert.match(source.slice(source.indexOf("export const results")), /ConfirmedColor/);
|
|
await assert.rejects(
|
|
loadReaderWorkspace((name, text) =>
|
|
name === "queries/confirmed-color.graphql" ? text.replace("color", "color @live") : text,
|
|
),
|
|
/@live is retired/,
|
|
);
|
|
});
|