Files
quixos-protocol/test/generic-packages.test.ts
T
Timothy J. Aveni 52803dda05 Implement capability generics, checked package specializations and CRUD scaffolding
Add kinded parameters, capability bounds, Self, aliases and closed application identities. Check generic implementations universally and build candidate-specific codecs and descriptors from immutable schemas. Preserve lexical aliases and exact dispatch identities in package and host bindings.

Add an imperative CRUD+index domain scaffold with explicit soft-deletion semantics, source/codegen regression coverage, installed CLI tests and an authoring guide. Existing Web Studio opaque props and class-level create-menu migration are separate from the implemented language core.
2026-09-16 10:42:12 -07:00

200 lines
10 KiB
TypeScript

import assert from "node:assert/strict";
import test from "node:test";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { createRequire } from "node:module";
import { compileCapabilityResourceSource, compileCapabilitySource } from "../src/capability-language/parser.js";
import { generateTypeScriptBindings, generatePackageDescriptor } from "../src/bindings/index.js";
import { genericImplementationType } from "../src/bindings/generics.js";
import { compileWorkspaceRevision, runtimeContracts } from "../src/capability-model/index.js";
import { generateAppliedClientContracts } from "../src/bindings/client.js";
const source = { repository: "https://example.test/generic.git", commit: "a".repeat(40) };
test("generic port aliases retain their defining scope, including shadowed alias names", () => {
const iface = compileCapabilityResourceSource(
'type Box<value V> = list<V>; interface Data<value V> id "data" revision "data@1" {value payload id "payload" : Box<V> {get id "payload:get";}}',
{ source },
);
assert.ok(iface.ok, JSON.stringify(iface.diagnostics));
if (iface.resource.kind !== "interface") throw new Error("expected interface");
const pkg = compileCapabilityResourceSource(
'import interface Data; type Box<value V> = optional<V>; package P id "p" revision "p@1" {operation fetch<value V> id "data@1" : unit -> list<Box<V>> mode call receiver any requires {interface data id "data" : Data<Box<V>>;};}',
{ source, environment: { interfaces: new Map([["Data", iface.resource.revision]]) } },
);
assert.ok(pkg.ok, JSON.stringify(pkg.diagnostics));
if (pkg.resource.kind !== "package") throw new Error("expected package");
const generated = generateTypeScriptBindings(
{
format: "quixos-bindings",
version: 1,
interfaces: [],
interfaceTemplates: [iface.resource.revision],
packages: [pkg.resource.revision],
},
pkg.resource.revision.revisionId,
);
assert.match(generated, /"payload.get":\(\)=>Promise<Array<\(T0 \| null\)>>/);
});
const definition = () => {
const result = compileCapabilityResourceSource(
`package Generic id "generic" revision "generic@1" {
operation echo<value T> id "echo" : T -> T mode call receiver any;
}`,
{ source },
);
assert.ok(result.ok, JSON.stringify(result.diagnostics));
if (result.resource.kind !== "package") throw new Error("expected package");
return result.resource.revision;
};
test("generic package exports specialize per binding without changing immutable source identity", () => {
const iface = compileCapabilityResourceSource(
`interface Echo<value T> id "echo-interface" revision "echo-interface@1" {
operation echo id "echo-member" : T -> T {call id "echo-call";}
}`,
{ source },
);
assert.ok(iface.ok);
if (iface.resource.kind !== "interface") throw new Error("expected interface");
const pkg = definition();
const result = compileCapabilitySource(
`workspace W id "w" revision "w@1" commit "${source.commit}" {
import interface Echo; import package Generic; atom Thing id "thing";
conform Thing as Echo<string> id "string-echo" {bind echo.call to package Generic.echo<string>;}
conform Thing as Echo<list<int64>> id "list-echo" {bind echo.call to package Generic.echo<list<int64>>;}
}`,
"workspace.qx",
{ interfaces: new Map([["Echo", iface.resource.revision]]), packages: new Map([["Generic", pkg]]) },
);
assert.ok(result.ok, JSON.stringify(result.diagnostics));
assert.equal(pkg.exports.length, 0, "source definition was not mutated");
const closed = result.workspace.packageImports[0];
assert.equal(closed.revisionId, pkg.revisionId);
assert.equal(closed.exports.length, 2);
assert.notEqual(closed.exports[0].id, closed.exports[1].id);
const oneApplication = structuredClone(result.workspace);
oneApplication.packageImports[0].exports.splice(1);
oneApplication.conformances.splice(1);
assert.notDeepEqual(
runtimeContracts(oneApplication),
runtimeContracts(result.workspace),
"New specializations invalidate the executable contract even with unchanged package source",
);
const schema = {
format: "quixos-bindings" as const,
version: 1 as const,
interfaces: result.workspace.interfaceImports,
packages: [closed],
};
const generated = generateTypeScriptBindings(schema, pkg.revisionId);
assert.match(generated, /"echo":\s*\(<T0>/);
for (const entry of closed.exports) {
assert.ok(generated.includes(entry.id));
assert.ok(generatePackageDescriptor(schema, pkg.revisionId).includes(entry.id));
}
const forged = structuredClone(result.workspace);
forged.packageImports[0].exports[0].outputType = { kind: "scalar", name: "bool" };
const rejected = compileWorkspaceRevision(forged);
assert.equal(rejected.ok, false);
if (!rejected.ok) assert.ok(rejected.issues.some((issue) => issue.message.includes("Specialized export differs")));
});
test("generated universal implementations compile and cannot assume a concrete value type", async () => {
const pkg = definition();
const signature = genericImplementationType(pkg.genericExports![0], [], () => {
throw new Error("unexpected concrete type");
});
const directory = await fs.mkdtemp(path.join(os.tmpdir(), "qx-generic-ts-"));
const require = createRequire(import.meta.url);
const compiler = path.join(path.dirname(require.resolve("typescript/package.json")), "bin/tsc");
const run = promisify(execFile);
const preamble = `type QxObjectRef<T extends string>={readonly identity:T}; type QxContextLifecycle<C>={signal?:AbortSignal}; type Handler=${signature};\n`;
try {
const file = path.join(directory, "generic.ts");
await fs.writeFile(file, preamble + `const handler:Handler=({input})=>input;`);
await run(process.execPath, [compiler, "--ignoreConfig", "--noEmit", "--strict", "--target", "es2023", file]);
await fs.writeFile(file, preamble + `const handler:Handler=({input})=>"not universally T";`);
await assert.rejects(
run(process.execPath, [compiler, "--ignoreConfig", "--noEmit", "--strict", "--target", "es2023", file]),
(error) => {
assert.match(String((error as { stdout: string }).stdout), /not assignable/);
return true;
},
);
} finally {
await fs.rm(directory, { recursive: true, force: true });
}
});
test("generic Self follows the receiver parameter, never a preceding export", () => {
const compile = (declaration: string) =>
compileCapabilityResourceSource(`package P id "p" revision "p@1" {${declaration}}`, { source });
const valid = compile('operation identity<object T> id "identity" : unit -> ref<Self> mode call receiver object T;');
assert.ok(valid.ok, JSON.stringify(valid.diagnostics));
if (valid.resource.kind !== "package") throw new Error("expected package");
const definition = valid.resource.revision.genericExports![0];
assert.deepEqual(definition.outputType, {
kind: "object-ref",
expectation: { kind: "parameter", parameterId: definition.parameters[0].id },
});
assert.equal(compile('function identity<value T> id "identity" : T -> ref<Self>;').ok, false);
assert.equal(
compile('operation events<value T> id "events" : unit -> watch-handle mode watch-start receiver any;').ok,
false,
);
});
test("typed presentation and factory consumers preserve distinct closed applications and return targets", async () => {
const interfaces = new Map();
for (const text of [
'interface Presentation<value Props> id "presentation" revision "presentation@1" {operation props id "props" : unit -> Props {call id "props:get";}}',
'interface Factory<object Result> id "factory" revision "factory@1" {operation create id "create" : unit -> ref<Result> {call id "create:call";}}',
]) {
const result = compileCapabilityResourceSource(text, { source });
assert.ok(result.ok, JSON.stringify(result.diagnostics));
interfaces.set(result.resource.revision.displayName, result.resource.revision);
}
const pkg = compileCapabilityResourceSource(
`import interface Presentation; import interface Factory;
external atom Note id "note"; external atom Notebook id "notebook";
package Views id "views" revision "views@1" {
operation note id "note-view" : unit -> unit mode call receiver any requires {interface props id "props" : Presentation<record {title:string; note:atom-ref<Note>;}>;};
operation notebook id "notebook-view" : unit -> unit mode call receiver any requires {interface props id "props" : Presentation<record {count:int32;}>;};
operation factory id "factory-view" : unit -> unit mode call receiver any requires {interface factory id "factory" : Factory<atom Note>;};
}`,
{ source, environment: { interfaces } },
);
assert.ok(pkg.ok, JSON.stringify(pkg.diagnostics));
const closed = pkg.resource.specializations!;
const presentation = closed.find(
(contract) =>
contract.application?.definitionId === "presentation@1" && JSON.stringify(contract).includes('"title"'),
)!;
const factory = closed.find((contract) => contract.application?.definitionId === "factory@1")!;
const contracts = generateAppliedClientContracts(closed);
const directory = await fs.mkdtemp(path.join(os.tmpdir(), "qx-presentation-types-"));
const compiler = path.join(
path.dirname(createRequire(import.meta.url).resolve("typescript/package.json")),
"bin/tsc",
);
const run = promisify(execFile);
const usage = `type Props=CapabilityOutput<${JSON.stringify(presentation.revisionId)},"props:get">;
type Created=CapabilityOutput<${JSON.stringify(factory.revisionId)},"create:call">;
const render=({camino,render}:{camino:Props;render:{onSelect:(note:Created)=>void;compact:boolean}})=>{render.onSelect(camino.note);return camino.title;};\n`;
try {
const file = path.join(directory, "consumer.ts");
await fs.writeFile(file, contracts + usage);
await run(process.execPath, [compiler, "--ignoreConfig", "--noEmit", "--strict", "--target", "es2023", file]);
await fs.writeFile(file, contracts + usage + "const wrong=(props:Props)=>props.count;");
await assert.rejects(
run(process.execPath, [compiler, "--ignoreConfig", "--noEmit", "--strict", "--target", "es2023", file]),
(error) => {
assert.match(String((error as { stdout: string }).stdout), /count.*does not exist/);
return true;
},
);
} finally {
await fs.rm(directory, { recursive: true, force: true });
}
});