Generate checked navigation query pairs from closed React contracts before package checking. Retain per-type subscriptions under the logical PresentationMount and resolve cold component metadata independently of code. Keep scoped occurrence membership and idempotent local preparation in the presentation library; copy directly through state contracts without reading other PresentationMounts' source associations. Preserve normal transaction creation, pending references, portal synchronization and detach. Exercise automatic object refs in Reader queries, imported fragments and structured/aggregate results. Add heterogeneous owner-index, live dependency, cold metadata, browser ownership and latency coverage. Document measured improvements, supported query limits and qualification evidence.
497 lines
22 KiB
TypeScript
497 lines
22 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { test } from "node:test";
|
|
import { compileCapabilityResourceSource } from "../src/capability-language/index.js";
|
|
import { compileQuery } from "../src/query/compile.js";
|
|
import { QueryCompileError } from "../src/query/types.js";
|
|
import { checkQueryTemplate } from "../src/query/templates.js";
|
|
import { querySelectionToWire } from "../src/query/proto.js";
|
|
import type { InterfaceRevision } from "../src/capability-model/types.js";
|
|
|
|
const source = { repository: "https://example.test/queries.git", commit: "a".repeat(40) };
|
|
const iface = (text: string, dependencies: InterfaceRevision[] = []) => {
|
|
const compiled = compileCapabilityResourceSource(text, {
|
|
source,
|
|
environment: {
|
|
interfaces: new Map(dependencies.map((entry) => [entry.displayName, entry])),
|
|
interfaceClosure: dependencies,
|
|
},
|
|
});
|
|
assert.ok(compiled.ok, JSON.stringify(compiled.diagnostics));
|
|
assert.equal(compiled.resource.kind, "interface");
|
|
if (compiled.resource.kind !== "interface") throw new Error("interface required");
|
|
return compiled.resource.revision;
|
|
};
|
|
const facts = iface(`interface TaskFacts id "facts" revision "facts@1" {
|
|
queryable value title id "title" : string { get id "title:get"; }
|
|
queryable value done id "done" : bool { get id "done:get"; }
|
|
queryable value due id "due" : optional<int64> { get id "due:get"; }
|
|
queryable rpc value score id "score" : int32 { get id "score:get"; }
|
|
value hidden id "hidden" : string { get id "hidden:get"; }
|
|
}`);
|
|
const collection = iface(
|
|
`import interface TaskFacts;
|
|
interface Tasks id "tasks" revision "tasks@1" {
|
|
queryable relation items id "items" : many interface TaskFacts ordered { resolve id "items:resolve"; }
|
|
}`,
|
|
[facts],
|
|
);
|
|
|
|
test("generic query templates check declared bounds before closed specialization", async () => {
|
|
const generic = iface(
|
|
`import interface TaskFacts;
|
|
interface Collection<object Item implements TaskFacts> id "generic-collection" revision "generic-collection@1" {
|
|
queryable relation items id "items" : many object Item { resolve id "items:resolve"; }
|
|
}`,
|
|
[facts],
|
|
);
|
|
const build = (bound: string, workspace = false) =>
|
|
compileCapabilityResourceSource(
|
|
`import interface Collection; import interface TaskFacts;
|
|
package GenericQueries id "generic-queries" revision "generic-queries@1" {
|
|
query Rows<object Item ${bound}> id "rows-template" root ${workspace ? "workspace " : ""}Collection<Item>
|
|
document "rows.graphql" operation "Rows" { view object Item as TaskFacts; max rows 30; }
|
|
query TaskRows id "task-rows" specialize Rows<interface TaskFacts>;
|
|
}`,
|
|
{
|
|
source,
|
|
environment: {
|
|
interfaces: new Map([
|
|
["Collection", generic],
|
|
["TaskFacts", facts],
|
|
]),
|
|
interfaceClosure: [generic, facts],
|
|
},
|
|
},
|
|
);
|
|
const result = build("implements TaskFacts");
|
|
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
|
if (result.resource.kind !== "package") throw new Error("package expected");
|
|
const pkg = result.resource.revision;
|
|
assert.equal(pkg.queries?.length, 1);
|
|
assert.equal(pkg.queryTemplates?.length, 1);
|
|
assert.equal(pkg.queries![0]!.application!.templateId, "rows-template");
|
|
const read = async () => `query Rows { root { items(first: 3) { entries { node { title } } } } }`;
|
|
const universal = await checkQueryTemplate(pkg.queryTemplates![0]!, [generic, facts], read);
|
|
assert.equal(
|
|
universal.effects.some((effect) => effect.memberId === "title"),
|
|
true,
|
|
);
|
|
const specialized = await compileQuery(
|
|
pkg.queries![0]!,
|
|
[generic, facts, ...(result.resource.specializations ?? [])],
|
|
read,
|
|
);
|
|
assert.equal(
|
|
specialized.effects.some((effect) => effect.memberId === "title"),
|
|
true,
|
|
);
|
|
const invalid = build("");
|
|
assert.ok(invalid.ok, JSON.stringify(invalid.diagnostics));
|
|
if (invalid.resource.kind !== "package") throw new Error("package expected");
|
|
await assert.rejects(
|
|
checkQueryTemplate(invalid.resource.revision.queryTemplates![0]!, [generic, facts], read),
|
|
/bound|guarantee|implement/i,
|
|
);
|
|
const extent = build("implements TaskFacts", true);
|
|
assert.ok(extent.ok, JSON.stringify(extent.diagnostics));
|
|
if (extent.resource.kind !== "package") throw new Error("package expected");
|
|
const extentPackage = extent.resource.revision;
|
|
const readExtent = async () =>
|
|
`query Rows {root {objects(first: 2) {entries {node {items(first: 3) {entries {node {title}}}}}}}}`;
|
|
await checkQueryTemplate(extentPackage.queryTemplates![0]!, [generic, facts], readExtent);
|
|
const closed = await compileQuery(
|
|
extentPackage.queries![0]!,
|
|
[generic, facts, ...(extent.resource.specializations ?? [])],
|
|
readExtent,
|
|
);
|
|
assert.equal(closed.declaration.root.kind, "workspace-interface");
|
|
assert.equal(closed.declaration.root.interface, specialized.declaration.root.interface);
|
|
const invalidExtent = build("", true);
|
|
assert.ok(invalidExtent.ok, JSON.stringify(invalidExtent.diagnostics));
|
|
if (invalidExtent.resource.kind !== "package") throw new Error("package expected");
|
|
await assert.rejects(
|
|
checkQueryTemplate(invalidExtent.resource.revision.queryTemplates![0]!, [generic, facts], readExtent),
|
|
/bound|guarantee|implement/i,
|
|
);
|
|
});
|
|
function fixture(clauses = "", root = "Tasks") {
|
|
const result = compileCapabilityResourceSource(
|
|
`import interface Tasks; import interface TaskFacts;
|
|
package Queries id "queries" revision "queries@1" {
|
|
query Upcoming id "upcoming" root ${root} document "queries/upcoming.graphql" operation "Upcoming" {
|
|
fragments "queries/row.graphql"; max rows 30; ${clauses}
|
|
}
|
|
}`,
|
|
{
|
|
source,
|
|
environment: {
|
|
interfaces: new Map([
|
|
["Tasks", collection],
|
|
["TaskFacts", facts],
|
|
]),
|
|
interfaceClosure: [collection, facts],
|
|
},
|
|
},
|
|
);
|
|
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
|
assert.equal(result.resource.kind, "package");
|
|
if (result.resource.kind !== "package") throw new Error("package required");
|
|
assert.equal(result.resource.revision.exports.length, 0);
|
|
return result.resource.revision.queries![0]!;
|
|
}
|
|
test("workspace roots expose a bounded interface extent, not a root object or writable relationship", async () => {
|
|
const declaration = { ...fixture("watch;", "workspace TaskFacts"), fragments: [] };
|
|
assert.deepEqual(declaration.root, { kind: "workspace-interface", interface: "facts@1" });
|
|
const checked = await compileQuery(
|
|
declaration,
|
|
[facts, collection],
|
|
async () => `query Upcoming {
|
|
root {
|
|
objects(first: 3, where: {done: {eq: false}}, orderBy: {title: ASC}) {
|
|
entries {key node {_camino {ref} title}} pageInfo {hasNextPage endCursor}
|
|
}
|
|
_camino {relations {objects {aggregate {count sum {due}}}}}
|
|
}
|
|
}`,
|
|
);
|
|
assert.deepEqual(new Set(checked.effects.map((effect) => effect.memberId)), new Set(["done", "title", "due"]));
|
|
assert.ok(!checked.effects.some((effect) => effect.memberId.includes("workspace-objects")));
|
|
assert.ok(JSON.stringify(checked.selection).includes("qx-internal-workspace-objects"));
|
|
await assert.rejects(
|
|
compileQuery(declaration, [facts], async () => `query Upcoming {root {_camino {ref}}}`),
|
|
/QUERY_VALIDATION/,
|
|
);
|
|
await assert.rejects(
|
|
compileQuery(declaration, [facts], async () => `query Upcoming {root {objects {entries {key}}}}`),
|
|
/QUERY_ROW_LIMIT/,
|
|
);
|
|
});
|
|
const document = `query Upcoming($first: Int!, $before: Int64!) {
|
|
root { items(first: $first, where: {done: {eq: false}, due: {lt: $before}}, orderBy: [{due: ASC}]) {
|
|
entries { key node { _camino { ref } ...Row } } pageInfo { hasNextPage endCursor }
|
|
} }
|
|
}`;
|
|
const compile = (query = document, row = "fragment Row on TaskFacts { title due }", clauses = "") =>
|
|
compileQuery(fixture(clauses), [collection, facts], async (name) => (name.endsWith("row.graphql") ? row : query));
|
|
|
|
test("ordinary selections preserve aliases, fragment conditions and wire identity", async () => {
|
|
const checked = await compile(
|
|
`query Upcoming($show: Boolean!) {root {items(first: 3) {entries {node {plain: title ...Row @include(if: $show)}}}}}`,
|
|
`fragment Row on TaskFacts {label: title due}`,
|
|
);
|
|
const node = checked.selection[0]!.selection[0]!.selection[0]!.selection[0]!;
|
|
assert.deepEqual(
|
|
node.selection.map((s) => s.key),
|
|
["plain", "label", "due", "_camino"],
|
|
);
|
|
assert.equal(node.selection[1]!.conditions.length, 1);
|
|
assert.ok(JSON.stringify(checked.selection.map(querySelectionToWire)).includes("label"));
|
|
});
|
|
test("object references are unconditional, canonical and idempotent through fragments", async () => {
|
|
const checked = await compile(
|
|
`query Upcoming($show: Boolean!) {root {items(first: 3) {entries {node {...Row @include(if: $show)}}}}}`,
|
|
`fragment Row on TaskFacts {label: title identity: _camino {alias: ref} _camino @include(if: $show) {ref}}`,
|
|
);
|
|
const root = checked.selection[0]!;
|
|
const node = root.selection.find((s) => s.name === "items")!.selection[0]!.selection[0]!;
|
|
for (const object of [root, node]) {
|
|
const metadata = object.selection.filter((s) => s.key === "_camino");
|
|
assert.equal(metadata.length, 1);
|
|
assert.deepEqual(metadata[0]!.conditions, []);
|
|
assert.deepEqual(
|
|
metadata[0]!.selection.filter((s) => s.key === "ref").map((s) => s.conditions),
|
|
[[]],
|
|
);
|
|
}
|
|
assert.equal(node.selection.find((s) => s.key === "identity")!.conditions.length, 1);
|
|
const recompiled = await compileQuery(
|
|
{ ...checked.declaration, fragments: [] },
|
|
[collection, facts],
|
|
async () => checked.document,
|
|
);
|
|
assert.deepEqual(recompiled.selection, checked.selection);
|
|
assert.deepEqual(recompiled.output, checked.output);
|
|
});
|
|
test("retired live syntax fails with migration guidance", async () => {
|
|
for (const selected of ["score @live", "_camino @live {ref}", "title @live title", "title @live(unchecked: true)"])
|
|
await assert.rejects(
|
|
compile(
|
|
`query Upcoming {root {items(first: 3) {entries {node {...Row}}}}}`,
|
|
`fragment Row on TaskFacts {${selected}}`,
|
|
'allow TaskFacts.score select "bounded";',
|
|
),
|
|
(error: unknown) => error instanceof QueryCompileError && error.code === "QUERY_RETIRED_DIRECTIVE",
|
|
);
|
|
});
|
|
|
|
test("implicit references reject aliases which occupy the canonical metadata path", async () => {
|
|
for (const invalid of ["_camino: title", "_camino {ref: __typename}"])
|
|
await assert.rejects(
|
|
compile(
|
|
`query Upcoming {root {items(first: 3) {entries {node {...Row}}}}}`,
|
|
`fragment Row on TaskFacts {${invalid}}`,
|
|
),
|
|
/QUERY_VALIDATION|Introspection/,
|
|
);
|
|
});
|
|
|
|
test("implicit reference merges with conditional aggregate metadata", async () => {
|
|
const checked = await compileQuery(
|
|
{ ...fixture(""), fragments: [] },
|
|
[collection, facts],
|
|
async () =>
|
|
`query Upcoming($show: Boolean!) {root {_camino @include(if: $show) {relations {items {aggregate {count}}}}}}`,
|
|
);
|
|
const metadata = checked.selection[0]!.selection.filter((field) => field.key === "_camino");
|
|
assert.equal(metadata.length, 1);
|
|
assert.deepEqual(metadata[0]!.conditions, []);
|
|
assert.equal(metadata[0]!.selection.find((field) => field.name === "relations")!.conditions.length, 1);
|
|
assert.deepEqual(metadata[0]!.selection.find((field) => field.name === "ref")!.conditions, []);
|
|
assert.equal(
|
|
metadata[0]!.selection
|
|
.find((field) => field.name === "relations")!
|
|
.selection[0]!.selection[0]!.selection.some((field) => field.name === "_camino"),
|
|
false,
|
|
);
|
|
});
|
|
|
|
test("qualified interface predicates and guaranteed inline projections retain nominal reads", async () => {
|
|
const owner = iface(
|
|
`interface Owner id "owner" revision "owner@1" { queryable value name id "name" : string {get id "name:get";} }`,
|
|
);
|
|
const owned = iface(
|
|
`import interface Owner;
|
|
interface Owned id "owned" revision "owned@1" {
|
|
queryable relation owner id "owner" : optional-one interface Owner {resolve id "owner:resolve";}
|
|
queryable value label id "owned:label" : string {get id "owned:label:get";}
|
|
}`,
|
|
[owner],
|
|
);
|
|
const item = iface(
|
|
`import interface Owned;
|
|
interface Item id "item" revision "item@1" requires Owned {
|
|
queryable value label id "item:label" : string {get id "item:label:get";}
|
|
}`,
|
|
[owned, owner],
|
|
);
|
|
const declaration = {
|
|
...fixture(""),
|
|
root: { kind: "workspace-interface" as const, interface: item.revisionId },
|
|
fragments: [],
|
|
};
|
|
const document = `query Upcoming($owner: QxRef_Owner!) {root {objects(all: 10, where: {_camino: {as: {Owned: {owner: {eq: $owner}}}}}) {entries {node {label ... on Owned {owner {name} ownerLabel: label}}}}}}`;
|
|
const checked = await compileQuery(declaration, [item, owned, owner], async () => document);
|
|
assert.ok(
|
|
checked.effects.some(
|
|
(effect) =>
|
|
effect.interfaceRevisionId === owned.revisionId &&
|
|
effect.memberId === "owner" &&
|
|
effect.uses.includes("predicate"),
|
|
),
|
|
);
|
|
const objects = checked.selection[0]!.selection[0]!;
|
|
assert.equal(objects.predicate?.kind, "and");
|
|
assert.ok(JSON.stringify(objects.predicate).includes('"kind":"as"'));
|
|
const node = objects.selection[0]!.selection[0]!;
|
|
assert.equal(node.selection.find((s) => s.key === "ownerLabel")!.interfaceRevisionId, owned.revisionId);
|
|
assert.equal(node.selection.filter((s) => s.key === "_camino").length, 1);
|
|
await assert.rejects(
|
|
compileQuery(declaration, [item, owned, owner], async () =>
|
|
document.replace("ownerLabel: label", "ownerLabel: label _camino {ref}"),
|
|
),
|
|
/QUERY_VALIDATION/,
|
|
);
|
|
const explicitProjectionRef = await compileQuery(declaration, [item, owned, owner], async () =>
|
|
document.replace("ownerLabel: label", "ownerLabel: label ownedIdentity: _camino {ref}"),
|
|
);
|
|
const projectedNode = explicitProjectionRef.selection[0]!.selection[0]!.selection[0]!.selection[0]!;
|
|
assert.equal(
|
|
projectedNode.selection.find((field) => field.key === "_camino")!.selection.find((field) => field.key === "ref")!
|
|
.targetInterfaceRevisionId,
|
|
item.revisionId,
|
|
);
|
|
await assert.rejects(
|
|
compileQuery(declaration, [item, owned, owner], async () => document.replace("ownerLabel: label", "label")),
|
|
/QUERY_PROJECTION_CONFLICT|QUERY_VALIDATION/,
|
|
);
|
|
});
|
|
|
|
test("query dependency ports resolve exact exports without declaration ordering constraints", () => {
|
|
const result = compileCapabilityResourceSource(
|
|
`import interface Tasks;
|
|
package Queries id "queries" revision "queries@1" {
|
|
operation load id "read" : unit -> unit mode call receiver interfaces [Tasks]
|
|
requires { query upcoming id "upcoming-port" : Queries.Upcoming; };
|
|
query Upcoming id "upcoming" root Tasks document "upcoming.graphql" operation "Upcoming" { max rows 30; }
|
|
}`,
|
|
{ source, environment: { interfaces: new Map([["Tasks", collection]]), interfaceClosure: [collection, facts] } },
|
|
);
|
|
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
|
assert.equal(result.resource.kind, "package");
|
|
if (result.resource.kind !== "package") throw new Error("package expected");
|
|
assert.deepEqual(result.resource.revision.exports[0]!.dependencyPorts[0]!.requirement, {
|
|
kind: "query",
|
|
packageRevisionId: "queries@1",
|
|
queryId: "upcoming",
|
|
});
|
|
});
|
|
|
|
test("result types merge repeated selections and preserve conditional fragment fields", async () => {
|
|
const checked = await compile(
|
|
`query Upcoming($show: Boolean!) {
|
|
root { items(first: 3) { entries { node { title } } } }
|
|
root { items(first: 3) { entries { node { ...Row @include(if: $show) } } } }
|
|
}`,
|
|
"fragment Row on TaskFacts { due done }",
|
|
);
|
|
const output = JSON.stringify(checked.output);
|
|
assert.match(output, /\"title\":\{\"kind\":\"scalar\",\"name\":\"string\"\}/);
|
|
assert.match(output, /\"done\":\{\"kind\":\"optional\",\"value\":\{\"kind\":\"scalar\",\"name\":\"bool\"\}\}/);
|
|
assert.match(output, /\"due\"/);
|
|
});
|
|
|
|
test("fixed GraphQL yields exact effects, typed references and distinct query artifacts", async () => {
|
|
const checked = await compile();
|
|
assert.deepEqual(checked.variables, {
|
|
kind: "record",
|
|
fields: {
|
|
first: { kind: "scalar", name: "int32" },
|
|
before: { kind: "scalar", name: "int64" },
|
|
},
|
|
});
|
|
assert.match(JSON.stringify(checked.output), /"kind":"object-ref"/);
|
|
assert.ok(
|
|
checked.effects.some(
|
|
(effect) => effect.memberId === "due" && effect.uses.includes("order") && effect.uses.includes("predicate"),
|
|
),
|
|
);
|
|
assert.equal(checked.definitionDigest, (await compile()).definitionDigest);
|
|
assert.notEqual(
|
|
checked.definitionDigest,
|
|
(await compile(document, "fragment Row on TaskFacts { title }")).definitionDigest,
|
|
);
|
|
});
|
|
|
|
test("query allowlist and two-sided RPC effects fail during compilation", async () => {
|
|
const rejects = (promise: Promise<unknown>, code: string) =>
|
|
assert.rejects(promise, (error: unknown) => error instanceof QueryCompileError && error.code === code);
|
|
const substring = (field: string) =>
|
|
`query Upcoming {root {items(first: 3, where: {${field}: {containsInsensitive: "TASK"}}) {entries {node {...Row}}}}}`;
|
|
await compile(substring("title"));
|
|
await rejects(compile(substring("due")), "QUERY_VALIDATION");
|
|
await rejects(compile(document, "fragment Row on TaskFacts { hidden }"), "QUERY_VALIDATION");
|
|
await rejects(compile(document, "fragment Row on TaskFacts { score }"), "QUERY_RPC_CONSUMER_REASON");
|
|
await compile(
|
|
document,
|
|
"fragment Row on TaskFacts { score }",
|
|
'allow TaskFacts.score select "Only the visible page is enriched";',
|
|
);
|
|
await rejects(
|
|
compile(document, "fragment Row on TaskFacts { score }", 'watch; allow TaskFacts.score select "Visible rows";'),
|
|
"QUERY_WATCH_UNSUPPORTED",
|
|
);
|
|
await compile(
|
|
document,
|
|
"fragment Row on TaskFacts { score }",
|
|
'watch; poll 5000 "External score has no push API"; allow TaskFacts.score select "Visible rows";',
|
|
);
|
|
await rejects(compile(document.replace("first: $first", "first: 31")), "QUERY_VALIDATION");
|
|
await rejects(compile(document.replace("...Row", "__typename ...Row")), "QUERY_UNSUPPORTED_FEATURE");
|
|
await rejects(compile(document, "fragment Row on TaskFacts { ...Row }"), "QUERY_VALIDATION");
|
|
});
|
|
|
|
test("unselected atom relationships do not require views on reference-only targets", async () => {
|
|
const target = iface(`external atom Detail id "detail";
|
|
interface Referenced id "referenced" revision "referenced@1" {
|
|
queryable relation details id "details" : many atom Detail {resolve id "details:resolve";}
|
|
}`);
|
|
const subject = iface(
|
|
`external atom Detail id "detail"; import interface Referenced;
|
|
interface Subject id "subject" revision "subject@1" {
|
|
queryable relation selected id "selected" : optional-one interface Referenced {resolve id "selected:resolve";}
|
|
}`,
|
|
[target],
|
|
);
|
|
const declaration = {
|
|
...fixture(""),
|
|
root: { kind: "object" as const, interface: subject.revisionId },
|
|
fragments: [],
|
|
};
|
|
const document = "query Upcoming {root {selected {_camino {ref}}}}";
|
|
const checked = await compileQuery(declaration, [subject, target], async () => document);
|
|
assert.match(JSON.stringify(checked.output), /referenced@1/);
|
|
await assert.rejects(
|
|
compileQuery(
|
|
declaration,
|
|
[subject, target],
|
|
async () => "query Upcoming {root {selected {details(all: 3) {entries {node {_camino {ref}}}}}}}",
|
|
),
|
|
/QUERY_VIEW_REQUIRED/,
|
|
);
|
|
});
|
|
|
|
test("React tooling generates checked owner queries from imported closed navigation fields", async () => {
|
|
const { presentationQueries } = await import("../src/bindings/presentation-queries.js");
|
|
const mount = iface('interface PresentationMount id "mount" revision "mount@1" {}');
|
|
const subject = iface('interface Subject id "subject" revision "subject@1" {}');
|
|
const state = iface(
|
|
`import interface PresentationMount; import interface Subject;
|
|
interface PresentationState id "state" revision "state@1" {
|
|
queryable value occurrence id "occurrence" : string {get id "occurrence:get" execution snapshot targets [browser,server];}
|
|
queryable relation presentationMount id "mount" : optional-one interface PresentationMount {resolve id "mount:resolve" execution snapshot targets [browser,server];}
|
|
queryable relation parent id "parent" : optional-one interface PresentationState {resolve id "parent:resolve" execution snapshot targets [browser,server];}
|
|
queryable relation source id "source" : exactly-one interface Subject {resolve id "source:resolve" execution snapshot targets [browser,server];}
|
|
}`,
|
|
[mount, subject],
|
|
);
|
|
const navigation = iface(
|
|
`import interface PresentationState;
|
|
interface Navigation<value T> id "navigation" revision "navigation@1" requires PresentationState {
|
|
queryable value custom id "custom" : T {get id "custom:get" execution snapshot targets [browser,server];}
|
|
}`,
|
|
[mount, subject, state],
|
|
);
|
|
const react = iface(
|
|
`import interface PresentationState;
|
|
interface ReactComponent<object Subject, object Navigation implements PresentationState> id "react" revision "react@1" {
|
|
module render id "render" contract "org.quixos.react.component/2" subject object Subject;
|
|
}`,
|
|
[mount, subject, state],
|
|
);
|
|
const dependencies = [mount, subject, state, navigation, react];
|
|
const parsed = compileCapabilityResourceSource(
|
|
`import interface ReactComponent; import interface Navigation; import interface Subject;
|
|
package Example id "example" revision "example@1" {
|
|
module Screen id "screen" implements ReactComponent<interface Subject,interface Navigation<record {label:string;count:int32;}>>.render;
|
|
}`,
|
|
{
|
|
source,
|
|
environment: {
|
|
interfaces: new Map(dependencies.map((entry) => [entry.displayName, entry])),
|
|
interfaceClosure: dependencies,
|
|
},
|
|
},
|
|
);
|
|
assert.ok(parsed.ok, JSON.stringify(parsed.diagnostics));
|
|
if (parsed.resource.kind !== "package") throw Error("package required");
|
|
const contracts = [...dependencies, ...(parsed.resource.specializations ?? [])];
|
|
const documents = presentationQueries(parsed.resource.revision, contracts);
|
|
assert.equal(documents.size, 2);
|
|
const checked = await Promise.all(
|
|
parsed.resource.revision.queries!.map((query) =>
|
|
compileQuery(query, contracts, async (name) => documents.get(name)!),
|
|
),
|
|
);
|
|
assert.deepEqual(
|
|
checked.map((query) => query.declaration.placement?.kind),
|
|
["replica", "local"],
|
|
);
|
|
assert.match(JSON.stringify(checked[0]!.output), /"custom":\{"kind":"record","fields":\{"label"/);
|
|
assert.match(JSON.stringify(checked[0]!.selection), /"kind":"as"/);
|
|
assert(
|
|
checked.every((query) =>
|
|
query.effects.some((effect) => effect.memberId === "mount" && effect.uses.includes("predicate")),
|
|
),
|
|
);
|
|
});
|