184 lines
8.9 KiB
TypeScript
184 lines
8.9 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { test } from "node:test";
|
|
import { compileCapabilityResourceSource, compileCapabilitySource } from "../src/capability-language/index.js";
|
|
import type { InterfaceRevision, PackageRevision } from "../src/capability-model/types.js";
|
|
import { compileQuery } from "../src/query/compile.js";
|
|
import { compileQueryComposition } from "../src/query/composition.js";
|
|
import { defaultQueryBudgets, type CheckedQuery } from "../src/query/types.js";
|
|
import {
|
|
presentationDeclarations,
|
|
preparationArtifact,
|
|
type ReactPreparationArtifact,
|
|
} from "../src/react-build/declarations.js";
|
|
import { completeWorkspace } from "../src/react-build/workspace.js";
|
|
|
|
const source = { repository: "https://example.test/prepared", commit: "a".repeat(40) };
|
|
async function fixture(replacement = "First", change?: (artifact: ReactPreparationArtifact) => void) {
|
|
const interfaces = new Map<string, InterfaceRevision>();
|
|
const resource = (text: string) => {
|
|
const result = compileCapabilityResourceSource(text, {
|
|
source,
|
|
environment: { interfaces, interfaceClosure: [...interfaces.values()] },
|
|
});
|
|
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
|
return result.resource;
|
|
};
|
|
for (const text of [
|
|
'interface Row id "row" revision "row@1" { queryable value title id "title" : string {get id "title:get" execution snapshot;} queryable value detail id "detail" : string {get id "detail:get" execution snapshot;} }',
|
|
'import interface Row; interface Library id "library" revision "library@1" { queryable relation selected id "selected" : optional-one interface Row {resolve id "selected:get" execution snapshot;} }',
|
|
'import interface Row; interface RowRole id "row-role" revision "row-role@1" { module render id "row-render" contract "org.quixos.react.subject-only/1" subject interface Row; }',
|
|
'import interface Library; interface LibraryRole id "library-role" revision "library-role@1" { module render id "library-render" contract "org.quixos.react.subject-only/1" subject interface Library; }',
|
|
]) {
|
|
const result = resource(text);
|
|
assert.equal(result.kind, "interface");
|
|
if (result.kind === "interface") interfaces.set(result.revision.displayName, result.revision);
|
|
}
|
|
const result = resource(
|
|
'import interface Row; import interface Library; import interface RowRole; import interface LibraryRole; package UI id "ui" revision "ui@1" { module LibraryCard id "library-card" implements LibraryRole.render; module First id "first" implements RowRole.render; module Second id "second" implements RowRole.render; }',
|
|
);
|
|
assert.equal(result.kind, "package");
|
|
if (result.kind !== "package") throw Error("package expected");
|
|
const pkg: PackageRevision = result.revision;
|
|
const checked: CheckedQuery[] = [];
|
|
for (const [name, iface, fields] of [
|
|
["LibraryData", "library@1", "selected {_camino {ref atom}}"],
|
|
["Title", "row@1", "title"],
|
|
["Detail", "row@1", "detail"],
|
|
]) {
|
|
for (const local of [false, true]) {
|
|
const id = name + (local ? "" : "Inputs");
|
|
checked.push(
|
|
await compileQuery(
|
|
{
|
|
id,
|
|
displayName: id,
|
|
root: {
|
|
kind: "object",
|
|
interface: [...interfaces.values()].find((entry) => entry.revisionId === iface)!.revisionId,
|
|
},
|
|
document: id + ".graphql",
|
|
operation: id,
|
|
fragments: [],
|
|
views: [],
|
|
allowances: [],
|
|
budgets: { ...defaultQueryBudgets },
|
|
watch: local,
|
|
placement: local ? { kind: "local", sourceQueryId: name + "Inputs" } : { kind: "replica" },
|
|
},
|
|
[...interfaces.values()],
|
|
async () => `query ${id} {root {${fields}}}`,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
pkg.checkedQueries = checked;
|
|
pkg.queries = checked.map((query) => query.declaration);
|
|
const input = (query: string) => ({
|
|
name: "data",
|
|
query,
|
|
root: { from: "input", path: ["subject"] },
|
|
arguments: { record: {} },
|
|
});
|
|
const authored = {
|
|
modules: {
|
|
LibraryCard: {
|
|
inputs: [input("LibraryData")],
|
|
children: [
|
|
{
|
|
name: "selected",
|
|
subject: "Row",
|
|
slot: "selected",
|
|
with: { from: "result", name: "data", path: ["root", "selected"] },
|
|
root: { from: "item", path: ["_camino", "ref"] },
|
|
role: { interface: "RowRole", member: "render" },
|
|
},
|
|
],
|
|
},
|
|
First: { inputs: [input("Title")] },
|
|
Second: { inputs: [input("Detail")] },
|
|
},
|
|
};
|
|
const declarations = presentationDeclarations(JSON.stringify(authored), pkg, [...interfaces.values()]);
|
|
change?.(declarations.artifact);
|
|
pkg.buildArtifacts = { [preparationArtifact]: declarations.artifact };
|
|
pkg.queryCompositions = declarations.compositions.map((composition) =>
|
|
compileQueryComposition(composition, new Map(checked.map((query) => [query.declaration.id, query]))),
|
|
);
|
|
const workspace = compileCapabilitySource(
|
|
`workspace Example id "example" revision "example@1" commit "${source.commit}" {
|
|
import interface Row; import interface Library; import interface RowRole; import interface LibraryRole; import package UI;
|
|
atom A id "a"; atom B id "b";
|
|
conform A as Row id "a-row" {private state TitleA id "title-A" on A : string policy register default "title"; private state DetailA id "detail-A" on A : string policy register default "detail"; bind title.get to state TitleA.read; bind detail.get to state DetailA.read;}
|
|
conform B as Row id "b-row" {private state TitleB id "title-B" on B : string policy register default "title"; private state DetailB id "detail-B" on B : string policy register default "detail"; bind title.get to state TitleB.read; bind detail.get to state DetailB.read;}
|
|
conform A as RowRole id "a-role" {bind render to UI.${replacement};}
|
|
conform B as RowRole id "b-role" {bind render to UI.Second;}
|
|
}`,
|
|
"workspace.qx",
|
|
{ interfaces, interfaceClosure: [...interfaces.values()], packages: new Map([["UI", pkg]]) },
|
|
);
|
|
assert.ok(workspace.ok, JSON.stringify(workspace.diagnostics));
|
|
await completeWorkspace(workspace.workspace);
|
|
return { workspace: workspace.workspace, pkg: workspace.workspace.packageImports[0]!, authored, interfaces };
|
|
}
|
|
|
|
test("workspace expands distinct providers' private inputs without parent field declarations", async () => {
|
|
const { pkg } = await fixture();
|
|
const parent = pkg.checkedQueries!.find((query) => query.declaration.id === "react:input:library-card:data")!;
|
|
const acquisition = pkg.checkedQueries!.find(
|
|
(query) =>
|
|
query.declaration.id ===
|
|
(parent.declaration.placement?.kind === "local" ? parent.declaration.placement.sourceQueryId : ""),
|
|
)!;
|
|
assert.match(acquisition.document, /title/);
|
|
assert.match(acquisition.document, /detail/);
|
|
const plan = pkg.queryCompositions!.find((composition) => composition.id === "react:inputs:library-card")!;
|
|
assert.equal(plan.steps[0]!.query, parent.declaration.id);
|
|
assert.equal(Object.keys(parent.fragmentContracts ?? {}).length, 2);
|
|
assert(!pkg.queryCompositions!.some((composition) => composition.id.endsWith(":discovery")));
|
|
const original = pkg.checkedQueries!.find((query) => query.declaration.id === "LibraryData")!;
|
|
assert.deepEqual(parent.output, original.output, "Acquisition-only child fields are not parent requirements");
|
|
assert(Object.values(parent.fragmentContracts!).every((entry) => entry.mode === "reference"));
|
|
});
|
|
|
|
test("eager cycles and wrongly typed child bindings fail before runtime", async () => {
|
|
await assert.rejects(
|
|
fixture("First", (artifact) => {
|
|
artifact.modules
|
|
.find((module) => module.module === "first")!
|
|
.children.push({
|
|
name: "again",
|
|
subject: "row@1",
|
|
slot: "again",
|
|
root: { from: "input", path: ["subject"] },
|
|
role: { interface: "row-role@1", member: "render" },
|
|
});
|
|
}),
|
|
/Eager React preparation cycle/,
|
|
);
|
|
await assert.rejects(
|
|
fixture("First", (artifact) => {
|
|
artifact.modules.find((module) => module.module === "library-card")!.children[0]!.root = {
|
|
literal: "not a reference",
|
|
};
|
|
}),
|
|
/child root must be/,
|
|
);
|
|
});
|
|
|
|
test("workspace replacement changes acquisition requirements without changing the parent", async () => {
|
|
const { pkg } = await fixture("Second");
|
|
const parent = pkg.checkedQueries!.find((query) => query.declaration.id === "react:input:library-card:data")!;
|
|
const acquisition = pkg.checkedQueries!.find(
|
|
(query) =>
|
|
query.declaration.id ===
|
|
(parent.declaration.placement?.kind === "local" ? parent.declaration.placement.sourceQueryId : ""),
|
|
)!;
|
|
assert.doesNotMatch(acquisition.document, /\btitle\b/);
|
|
assert.match(acquisition.document, /detail/);
|
|
assert.equal(Object.keys(parent.fragmentContracts ?? {}).length, 1);
|
|
const previous = (await fixture()).pkg.checkedQueries!.find(
|
|
(query) => query.declaration.id === parent.declaration.id,
|
|
)!;
|
|
assert.deepEqual(parent.output, previous.output);
|
|
});
|