Move React navigation query generation behind an explicit build frontend. Add checked result-dependent query bindings and pure composition evaluation, including one-snapshot server execution. Preserve the existing rendering path while the React manifest, worker preparation, and replica admission integration remain pending; see docs/PREPARED_PRESENTATIONS_PROGRESS.md.
118 lines
4.9 KiB
TypeScript
118 lines
4.9 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { test } from "node:test";
|
|
import { compileCapabilityResourceSource } from "../src/capability-language/index.js";
|
|
import type { InterfaceRevision } from "../src/capability-model/types.js";
|
|
import { valueType } from "../src/capability-model/types.js";
|
|
import { compileQuery } from "../src/query/compile.js";
|
|
import { compileQueryComposition, type QueryComposition } from "../src/query/composition.js";
|
|
import { defaultQueryBudgets } from "../src/query/types.js";
|
|
|
|
const iface = (text: string, dependencies: InterfaceRevision[] = []) => {
|
|
const result = compileCapabilityResourceSource(text, {
|
|
source: { repository: "https://example.test/devices", commit: "a".repeat(40) },
|
|
environment: {
|
|
interfaces: new Map(dependencies.map((entry) => [entry.displayName, entry])),
|
|
interfaceClosure: dependencies,
|
|
},
|
|
});
|
|
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
|
if (result.resource.kind !== "interface") throw Error("Expected interface");
|
|
return result.resource.revision;
|
|
};
|
|
|
|
test("dependent query roots and arguments are checked without a presentation library", async () => {
|
|
const sensor = iface(`interface Sensor id "sensor" revision "sensor@1" {
|
|
queryable value reading id "reading" : int64 { get id "reading:get"; }
|
|
}`);
|
|
const control = iface(
|
|
`import interface Sensor;
|
|
interface Control id "control" revision "control@1" {
|
|
queryable value enabled id "enabled" : bool { get id "enabled:get"; }
|
|
queryable relation selected id "selected" : optional-one interface Sensor { resolve id "selected:get"; }
|
|
}`,
|
|
[sensor],
|
|
);
|
|
const query = (id: string, root: InterfaceRevision["revisionId"], text: string) =>
|
|
compileQuery(
|
|
{
|
|
id,
|
|
displayName: id,
|
|
document: `${id}.graphql`,
|
|
operation: id,
|
|
root: { kind: "object", interface: root },
|
|
fragments: [],
|
|
views: [],
|
|
allowances: [],
|
|
budgets: { ...defaultQueryBudgets },
|
|
watch: false,
|
|
placement: { kind: "replica" },
|
|
},
|
|
[control, sensor],
|
|
async () => text,
|
|
);
|
|
const selection = await query(
|
|
"Selection",
|
|
control.revisionId,
|
|
"query Selection { root { enabled selected { _camino { ref atom } } } }",
|
|
);
|
|
const reading = await query(
|
|
"Reading",
|
|
sensor.revisionId,
|
|
"query Reading($enabled: Boolean!) { root { reading @include(if: $enabled) } }",
|
|
);
|
|
const queries = new Map([
|
|
["Selection", selection],
|
|
["Reading", reading],
|
|
]);
|
|
const source: QueryComposition = {
|
|
id: "device-display",
|
|
input: { kind: "record", fields: { controller: valueType.interfaceRef(control.revisionId) } },
|
|
maxQueries: 2,
|
|
steps: [
|
|
{
|
|
name: "selection",
|
|
query: "Selection",
|
|
root: { from: "input", path: ["controller"] },
|
|
arguments: { record: {} },
|
|
},
|
|
{
|
|
name: "reading",
|
|
query: "Reading",
|
|
with: { from: "result", name: "selection", path: ["root", "selected"] },
|
|
root: { from: "item", path: ["_camino", "ref"] },
|
|
arguments: { record: { enabled: { from: "result", name: "selection", path: ["root", "enabled"] } } },
|
|
},
|
|
],
|
|
};
|
|
const checked = compileQueryComposition(source, queries);
|
|
assert.equal(checked.output.kind, "record");
|
|
assert.equal(checked.dependencies.length, 2);
|
|
assert.ok(checked.definitionDigest);
|
|
const changed = new Map(queries);
|
|
changed.set("Reading", { ...reading, definitionDigest: "different-source" });
|
|
assert.notEqual(compileQueryComposition(source, changed).definitionDigest, checked.definitionDigest);
|
|
assert.throws(() => compileQueryComposition({ ...source, maxQueries: 1 }, queries), /exceed maxQueries/);
|
|
assert.throws(
|
|
() => compileQueryComposition({ ...source, steps: [...source.steps].reverse() }, queries),
|
|
/earlier step/,
|
|
);
|
|
const wrongArguments = structuredClone(source);
|
|
wrongArguments.steps[1]!.arguments = { record: { enabled: { from: "input", path: ["controller"] } } };
|
|
assert.throws(() => compileQueryComposition(wrongArguments, queries), /arguments do not match/);
|
|
const wrongRoot = structuredClone(source);
|
|
wrongRoot.steps[1]!.root = { from: "input", path: ["controller"] };
|
|
assert.throws(() => compileQueryComposition(wrongRoot, queries), /reference to sensor@1/);
|
|
const nullableTraversal = structuredClone(source);
|
|
nullableTraversal.steps[1]!.root = {
|
|
from: "result",
|
|
name: "selection",
|
|
path: ["root", "selected", "_camino", "ref"],
|
|
};
|
|
assert.throws(() => compileQueryComposition(nullableTraversal, queries), /explicit boundary/);
|
|
const branch = structuredClone(source);
|
|
branch.steps[1]!.when = { equal: [{ from: "item", path: ["_camino", "atom"] }, { literal: "thermometer" }] };
|
|
assert.ok(compileQueryComposition(branch, queries).definitionDigest);
|
|
branch.steps[1]!.when = { literal: "not a boolean" };
|
|
assert.throws(() => compileQueryComposition(branch, queries), /when requires a boolean/);
|
|
});
|