feat(foundry): close the operational map data loop
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import test from "node:test";
|
||||
import ts from "typescript";
|
||||
|
||||
const source = await readFile(new URL("../apps/catalog/src/mapPresentationProfile.ts", import.meta.url), "utf8");
|
||||
const transpiled = ts.transpileModule(source, {
|
||||
compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 },
|
||||
}).outputText;
|
||||
const moduleUrl = `data:text/javascript;base64,${Buffer.from(transpiled).toString("base64")}`;
|
||||
const {
|
||||
mapFactMatchesFilters,
|
||||
mapPresentationBindingIsAll,
|
||||
mapPresentationFacetCounts,
|
||||
toggleMapPresentationFacetSelection,
|
||||
} = await import(moduleUrl);
|
||||
|
||||
const profile = {
|
||||
id: "map.moving-object.operational.default",
|
||||
facets: [
|
||||
{
|
||||
field: "signal_state",
|
||||
values: [{ value: "active" }, { value: "inactive" }],
|
||||
},
|
||||
{
|
||||
field: "movement_state",
|
||||
values: [{ value: "moving" }, { value: "stopped" }],
|
||||
},
|
||||
],
|
||||
};
|
||||
const onlineMoving = {
|
||||
attributes: { signal_state: "active", movement_state: "moving" },
|
||||
};
|
||||
|
||||
test("missing subject state is explicit all for that binding", () => {
|
||||
assert.equal(mapPresentationBindingIsAll("fleet", {}), true);
|
||||
assert.equal(mapFactMatchesFilters(onlineMoving, profile, {}, "fleet"), true);
|
||||
});
|
||||
|
||||
test("a selected chip restricts only its own facet", () => {
|
||||
const filters = { fleet: { visible: true, facets: { signal_state: ["active"] } } };
|
||||
assert.equal(mapFactMatchesFilters(onlineMoving, profile, filters, "fleet"), true);
|
||||
assert.equal(mapFactMatchesFilters({ attributes: { signal_state: "inactive", movement_state: "moving" } }, profile, filters, "fleet"), false);
|
||||
});
|
||||
|
||||
test("deselecting the last chip remains empty and never normalizes to all", () => {
|
||||
const filters = { fleet: { visible: true, facets: { signal_state: [] } } };
|
||||
assert.equal(mapPresentationBindingIsAll("fleet", filters), false);
|
||||
assert.equal(mapFactMatchesFilters(onlineMoving, profile, filters, "fleet"), false);
|
||||
});
|
||||
|
||||
test("interactive deselect of the last chip removes the facet constraint", () => {
|
||||
const selected = toggleMapPresentationFacetSelection({}, "signal_state", "active");
|
||||
assert.deepEqual(selected, { signal_state: ["active"] });
|
||||
|
||||
const unconstrained = toggleMapPresentationFacetSelection(selected, "signal_state", "active");
|
||||
assert.deepEqual(unconstrained, {});
|
||||
assert.equal(mapPresentationBindingIsAll("fleet", { fleet: { visible: true, facets: unconstrained } }), true);
|
||||
});
|
||||
|
||||
test("interactive deselect preserves other values and facet constraints", () => {
|
||||
const facets = {
|
||||
signal_state: ["active", "inactive"],
|
||||
movement_state: ["moving"],
|
||||
};
|
||||
assert.deepEqual(toggleMapPresentationFacetSelection(facets, "signal_state", "active"), {
|
||||
signal_state: ["inactive"],
|
||||
movement_state: ["moving"],
|
||||
});
|
||||
});
|
||||
|
||||
test("chips from different facets form one global union", () => {
|
||||
const filters = {
|
||||
fleet: {
|
||||
visible: true,
|
||||
facets: {
|
||||
signal_state: ["active"],
|
||||
movement_state: ["moving"],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
assert.equal(mapFactMatchesFilters(onlineMoving, profile, filters, "fleet"), true);
|
||||
assert.equal(mapFactMatchesFilters({ attributes: { signal_state: "active", movement_state: "stopped" } }, profile, filters, "fleet"), true);
|
||||
assert.equal(mapFactMatchesFilters({ attributes: { signal_state: "inactive", movement_state: "moving" } }, profile, filters, "fleet"), false);
|
||||
});
|
||||
|
||||
test("movement filters and counters ignore stale movement on inactive subjects", () => {
|
||||
const activeStopped = { attributes: { signal_state: "active", movement_state: "stopped" } };
|
||||
const inactiveMoving = { attributes: { signal_state: "inactive", movement_state: "moving" } };
|
||||
const inactiveStopped = { attributes: { signal_state: "inactive", movement_state: "stopped" } };
|
||||
const filters = { fleet: { visible: true, facets: { movement_state: ["moving"] } } };
|
||||
|
||||
assert.equal(mapFactMatchesFilters(onlineMoving, profile, filters, "fleet"), true);
|
||||
assert.equal(mapFactMatchesFilters(inactiveMoving, profile, filters, "fleet"), false);
|
||||
|
||||
assert.deepEqual(mapPresentationFacetCounts([
|
||||
onlineMoving,
|
||||
activeStopped,
|
||||
inactiveMoving,
|
||||
inactiveStopped,
|
||||
], profile), {
|
||||
signal_state: { active: 2, inactive: 2 },
|
||||
movement_state: { moving: 1, stopped: 1 },
|
||||
});
|
||||
});
|
||||
|
||||
test("explicitly disabled All hides only the selected binding", () => {
|
||||
const filters = {
|
||||
fleet: { visible: false, facets: {} },
|
||||
service: { visible: true, facets: {} },
|
||||
};
|
||||
assert.equal(mapFactMatchesFilters(onlineMoving, profile, filters, "fleet"), false);
|
||||
assert.equal(mapFactMatchesFilters(onlineMoving, profile, filters, "service"), true);
|
||||
});
|
||||
@@ -323,10 +323,12 @@ try {
|
||||
return payload.result;
|
||||
};
|
||||
const initialized = await mcpRequest("initialize", { protocolVersion: "2025-06-18", capabilities: {}, clientInfo: { name: "runtime-smoke", version: "1" } });
|
||||
assert.equal(initialized.serverInfo.version, "0.2.0");
|
||||
assert.equal(initialized.serverInfo.version, "0.5.0");
|
||||
const listedTools = await mcpRequest("tools/list");
|
||||
assert.equal(listedTools.tools.length, 13);
|
||||
assert.equal(listedTools.tools.length, 17);
|
||||
assert.ok(listedTools.tools.some((tool) => tool.name === "foundry_apply_map_data_product_consumer"));
|
||||
assert.ok(listedTools.tools.some((tool) => tool.name === "foundry_save_map_page_view_state"));
|
||||
assert.ok(listedTools.tools.some((tool) => tool.name === "foundry_remove_map_pin_binding"));
|
||||
|
||||
const workloadHeaders = {
|
||||
authorization: `Bearer ${bindingGrantToken}`,
|
||||
|
||||
@@ -15,6 +15,8 @@ const applicationManifestSchema = await parse("registry/schemas/application-mani
|
||||
const designProfileSchema = await parse("registry/schemas/design-profile-v0.1.schema.json");
|
||||
const mapSceneFixtureSchema = await parse("registry/schemas/map-scene-fixture-v0.1.schema.json");
|
||||
const mapLodPolicySchema = await parse("registry/schemas/map-lod-policy-v0.1.schema.json");
|
||||
const mapPresentationProfiles = await parse("registry/map-presentation-profiles.json");
|
||||
const dataProductConsumerPolicies = await parse("registry/data-product-consumer-policies.json");
|
||||
|
||||
const failures = [];
|
||||
const requireValue = (condition, message) => {
|
||||
@@ -26,7 +28,13 @@ requireValue(registry.libraryVersion, "registry libraryVersion is required");
|
||||
requireValue(Array.isArray(registry.packages) && registry.packages.length >= 4, "registry must list all packages");
|
||||
requireValue(Array.isArray(sources.sources) && sources.sources.length >= 6, "source audit is incomplete");
|
||||
requireValue(Array.isArray(components.components) && components.components.length >= 10, "component registry is incomplete");
|
||||
requireValue(Array.isArray(candidates.candidates) && candidates.candidates.length >= 10, "candidate registry is incomplete");
|
||||
requireValue(Array.isArray(candidates.candidates) && candidates.candidates.length >= 1, "candidate registry is incomplete");
|
||||
requireValue(
|
||||
Array.isArray(components.components)
|
||||
&& Array.isArray(candidates.candidates)
|
||||
&& components.components.length + candidates.candidates.length >= 20,
|
||||
"component inventory is incomplete",
|
||||
);
|
||||
requireValue(Array.isArray(icons.groups) && icons.groups.length >= 5, "icon registry is incomplete");
|
||||
requireValue(pages.schemaVersion === "0.1.0", "page registry schemaVersion must be 0.1.0");
|
||||
requireValue(Array.isArray(pages.templates) && pages.templates.length >= 1, "page registry must contain at least one template");
|
||||
@@ -34,10 +42,48 @@ requireValue(registry.applicationManifestSchema === "schemas/application-manifes
|
||||
requireValue(registry.designProfileSchema === "schemas/design-profile-v0.1.schema.json", "design profile schema must be registered");
|
||||
requireValue(registry.mapSceneFixtureSchema === "schemas/map-scene-fixture-v0.1.schema.json", "map scene fixture schema must be registered");
|
||||
requireValue(registry.mapLodPolicySchema === "schemas/map-lod-policy-v0.1.schema.json", "map LOD policy schema must be registered");
|
||||
requireValue(registry.mapPresentationProfileRegistry === "map-presentation-profiles.json", "map presentation profile registry must be registered");
|
||||
requireValue(applicationManifestSchema.properties?.schemaVersion?.const === "0.1.0", "application manifest schema version must be 0.1.0");
|
||||
requireValue(designProfileSchema.properties?.schemaVersion?.const === "0.1.0", "design profile schema version must be 0.1.0");
|
||||
requireValue(mapSceneFixtureSchema.properties?.schemaVersion?.const === "0.1.0", "map scene fixture schema version must be 0.1.0");
|
||||
requireValue(mapLodPolicySchema.properties?.schemaVersion?.const === "0.1.0", "map LOD policy schema version must be 0.1.0");
|
||||
requireValue(mapPresentationProfiles.schemaVersion === "nodedc.map-presentation-profiles/v1", "map presentation profile registry schema is unsupported");
|
||||
requireValue(Array.isArray(mapPresentationProfiles.profiles) && mapPresentationProfiles.profiles.length >= 1, "map presentation profiles are missing");
|
||||
requireValue(dataProductConsumerPolicies.schemaVersion === "nodedc.foundry.data-product-consumer-policies/v1", "data product consumer policy registry schema is unsupported");
|
||||
requireValue(Array.isArray(dataProductConsumerPolicies.policies) && dataProductConsumerPolicies.policies.length >= 1, "data product consumer policies are missing");
|
||||
|
||||
const consumerPolicies = dataProductConsumerPolicies.policies ?? [];
|
||||
const consumerPolicyIds = consumerPolicies.map((policy) => `${policy.id}@${policy.version}`);
|
||||
const consumerPolicyProducts = consumerPolicies.map((policy) => `${policy.dataProductId}@${policy.productVersion}`);
|
||||
requireValue(new Set(consumerPolicyIds).size === consumerPolicyIds.length, "data product consumer policy id/version pairs must be unique");
|
||||
requireValue(new Set(consumerPolicyProducts).size === consumerPolicyProducts.length, "data product consumer policy product/version pairs must be unique");
|
||||
for (const policy of consumerPolicies) {
|
||||
const statusContract = policy.statusContract;
|
||||
requireValue(typeof policy.id === "string" && /^[a-z0-9._-]{1,160}$/.test(policy.id), `invalid data product consumer policy id: ${policy.id ?? "unknown"}`);
|
||||
requireValue(typeof policy.version === "string" && /^\d+\.\d+\.\d+$/.test(policy.version), `invalid data product consumer policy version: ${policy.id ?? "unknown"}`);
|
||||
requireValue(typeof policy.dataProductId === "string" && /^[A-Za-z0-9._:-]{1,160}$/.test(policy.dataProductId), `invalid data product consumer policy product: ${policy.id ?? "unknown"}`);
|
||||
requireValue(typeof policy.productVersion === "string" && /^\d+\.\d+\.\d+$/.test(policy.productVersion), `invalid data product consumer product version: ${policy.id ?? "unknown"}`);
|
||||
requireValue(Array.isArray(policy.terminalStatuses) && policy.terminalStatuses.every((value) => typeof value === "string" && /^[a-z0-9_-]{1,64}$/.test(value)), `invalid terminal statuses: ${policy.id ?? "unknown"}`);
|
||||
requireValue(policy.removeMode === "canonical-tombstone-or-snapshot-rebase", `invalid remove mode: ${policy.id ?? "unknown"}`);
|
||||
if (statusContract === undefined) {
|
||||
requireValue(Number.isInteger(policy.staleAfterMs) && policy.staleAfterMs >= 1_000 && policy.staleAfterMs <= 7 * 24 * 60 * 60 * 1000, `invalid stale window: ${policy.id ?? "unknown"}`);
|
||||
continue;
|
||||
}
|
||||
requireValue(statusContract && typeof statusContract === "object" && !Array.isArray(statusContract), `invalid status contract: ${policy.id ?? "unknown"}`);
|
||||
requireValue(typeof statusContract?.attribute === "string" && /^[A-Za-z][A-Za-z0-9_.-]{0,127}$/.test(statusContract.attribute), `invalid status attribute: ${policy.id ?? "unknown"}`);
|
||||
requireValue(Array.isArray(statusContract?.allowedValues) && statusContract.allowedValues.length >= 1 && new Set(statusContract.allowedValues).size === statusContract.allowedValues.length, `invalid allowed status values: ${policy.id ?? "unknown"}`);
|
||||
requireValue(statusContract?.allowedValues?.every((value) => typeof value === "string" && /^[a-z0-9_-]{1,64}$/.test(value)), `invalid allowed status value: ${policy.id ?? "unknown"}`);
|
||||
requireValue(statusContract?.missing === "reject", `status contract must reject missing values: ${policy.id ?? "unknown"}`);
|
||||
requireValue(["none", "observed-at"].includes(statusContract?.freshness), `invalid status freshness mode: ${policy.id ?? "unknown"}`);
|
||||
requireValue(statusContract?.freshness === "none" ? policy.staleAfterMs === null : Number.isInteger(policy.staleAfterMs), `status freshness/stale window mismatch: ${policy.id ?? "unknown"}`);
|
||||
requireValue(policy.terminalStatuses.every((value) => statusContract.allowedValues.includes(value)), `terminal status is outside the closed value contract: ${policy.id ?? "unknown"}`);
|
||||
}
|
||||
|
||||
const movingObjectV4Policy = consumerPolicies.find((policy) => policy.dataProductId === "fleet.positions.current.v4" && policy.productVersion === "4.0.0");
|
||||
requireValue(movingObjectV4Policy?.id === "map-moving-object-current-v4", "fleet.positions.current.v4 consumer policy is missing");
|
||||
requireValue(movingObjectV4Policy?.statusContract?.attribute === "signal_state", "fleet.positions.current.v4 status source must be signal_state");
|
||||
requireValue(JSON.stringify(movingObjectV4Policy?.statusContract?.allowedValues) === JSON.stringify(["active", "inactive"]), "fleet.positions.current.v4 signal states must remain closed active/inactive");
|
||||
requireValue(movingObjectV4Policy?.statusContract?.freshness === "none" && movingObjectV4Policy?.staleAfterMs === null, "fleet.positions.current.v4 must not invent freshness states");
|
||||
|
||||
const ids = components.components.map((component) => component.id);
|
||||
requireValue(new Set(ids).size === ids.length, "component ids must be unique");
|
||||
|
||||
Reference in New Issue
Block a user