feat(foundry): close the operational map data loop
This commit is contained in:
@@ -334,3 +334,283 @@ test("exact apply can ensure a missing target-scoped reader grant without exposi
|
||||
await rm(stateDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("Data Product replacement bootstraps a successor reader generation before revoking its predecessor", async () => {
|
||||
const stateDir = await mkdtemp(join(tmpdir(), "foundry-consumer-generation-"));
|
||||
const input = { applicationId: target.application.id, pageId: target.page.id, bindingId: target.binding.id };
|
||||
let currentTarget = structuredClone(target);
|
||||
let failV2Snapshot = true;
|
||||
let failPredecessorRevoke = true;
|
||||
const grants = new Map([[1, "fleet.positions.current.v1"]]);
|
||||
const events = [];
|
||||
const product = (id) => ({
|
||||
id,
|
||||
version: id.endsWith(".v2") ? "2.0.0" : "1.0.0",
|
||||
deliveryMode: "snapshot+patch",
|
||||
semanticTypes: ["map.moving_object"],
|
||||
active: true,
|
||||
});
|
||||
const manager = createFoundryDataProductConsumerManager({
|
||||
stateDir,
|
||||
dataPlaneUrl: "http://edp.test",
|
||||
resolveTarget: async () => structuredClone(currentTarget),
|
||||
readReaderToken: async (_applicationId, _pageId, _bindingId, generation = 1) => {
|
||||
return grants.has(generation) ? `ndc_edprb_generation-${generation}` : null;
|
||||
},
|
||||
inspectReaderGrant: async (resolvedTarget, { generation = 1 } = {}) => {
|
||||
const desiredProductId = resolvedTarget.binding.dataProductId;
|
||||
return {
|
||||
product: product(desiredProductId),
|
||||
readerGrantAction: grants.get(generation) === desiredProductId ? "reuse" : "ensure",
|
||||
readerGrantGeneration: generation,
|
||||
};
|
||||
},
|
||||
ensureReaderGrant: async (resolvedTarget, { generation }) => {
|
||||
grants.set(generation, resolvedTarget.binding.dataProductId);
|
||||
events.push(`ensure-g${generation}`);
|
||||
return { ensured: true, generation };
|
||||
},
|
||||
revokeReaderGrant: async (_resolvedTarget, { generation }) => {
|
||||
if (generation === 1 && failPredecessorRevoke) {
|
||||
failPredecessorRevoke = false;
|
||||
events.push("revoke-g1-failed");
|
||||
throw Object.assign(new Error("foundry_reader_grant_provisioner_unavailable"), { statusCode: 503 });
|
||||
}
|
||||
grants.delete(generation);
|
||||
events.push(`revoke-g${generation}`);
|
||||
return { revoked: true, generation };
|
||||
},
|
||||
resolvePolicy: (resolvedProduct) => ({
|
||||
id: resolvedProduct.id.endsWith(".v2") ? "map-moving-object-current-v2" : "map-moving-object-current-v1",
|
||||
version: resolvedProduct.version,
|
||||
dataProductId: resolvedProduct.id,
|
||||
productVersion: resolvedProduct.version,
|
||||
staleAfterMs: 60_000,
|
||||
terminalStatuses: ["inactive", "no-position", "no_position"],
|
||||
removeMode: "canonical-tombstone-or-snapshot-rebase",
|
||||
}),
|
||||
sanitizeSnapshot: (value) => value,
|
||||
sanitizePatch: (value) => value,
|
||||
fetchImpl: async (request, options = {}) => {
|
||||
const dataProductId = decodeURIComponent(new URL(request).pathname.split("/").at(-2));
|
||||
const generation = Number(String(options.headers.authorization).match(/generation-(\d+)/)?.[1]);
|
||||
assert.equal(grants.get(generation), dataProductId);
|
||||
events.push(`snapshot-${dataProductId}-g${generation}`);
|
||||
if (dataProductId.endsWith(".v2") && failV2Snapshot) {
|
||||
throw Object.assign(new Error("data_product_runtime_unavailable"), { statusCode: 503 });
|
||||
}
|
||||
return Response.json({
|
||||
...snapshot(dataProductId.endsWith(".v2") ? "20" : "10"),
|
||||
dataProduct: { id: dataProductId, version: dataProductId.endsWith(".v2") ? "2.0.0" : "1.0.0" },
|
||||
});
|
||||
},
|
||||
idleStopMs: 10,
|
||||
staleSweepMs: 10_000,
|
||||
});
|
||||
try {
|
||||
const createPlan = await manager.plan(input);
|
||||
assert.equal(createPlan.configuration.readerGrantGeneration, 1);
|
||||
const created = await manager.apply({ ...input, planId: createPlan.planId });
|
||||
assert.equal(created.consumer.product.id, "fleet.positions.current.v1");
|
||||
assert.equal(created.consumer.readerGrantGeneration, 1);
|
||||
|
||||
currentTarget.binding.dataProductId = "fleet.positions.current.v2";
|
||||
currentTarget.binding.fieldProjection = [
|
||||
"display_name", "availability_state", "motion_state", "position_state", "freshness_state",
|
||||
];
|
||||
const replacePlan = await manager.plan(input);
|
||||
assert.equal(replacePlan.action, "replace");
|
||||
assert.equal(replacePlan.configuration.readerGrantAction, "ensure");
|
||||
assert.equal(replacePlan.configuration.readerGrantGeneration, 2);
|
||||
assert.ok(replacePlan.effects.includes("ensure-successor-target-scoped-reader-grant"));
|
||||
await assert.rejects(
|
||||
manager.apply({ ...input, planId: replacePlan.planId }),
|
||||
/data_product_runtime_unavailable/,
|
||||
);
|
||||
const preserved = await manager.status(input);
|
||||
assert.equal(preserved.consumer.product.id, "fleet.positions.current.v1");
|
||||
assert.equal(preserved.consumer.readerGrantGeneration, 1);
|
||||
assert.equal(grants.get(1), "fleet.positions.current.v1");
|
||||
assert.equal(grants.get(2), "fleet.positions.current.v2");
|
||||
assert.equal(events.includes("revoke-g1"), false);
|
||||
|
||||
failV2Snapshot = false;
|
||||
const retryPlan = await manager.plan(input);
|
||||
assert.equal(retryPlan.configuration.readerGrantGeneration, 2);
|
||||
await assert.rejects(
|
||||
manager.apply({ ...input, planId: retryPlan.planId }),
|
||||
/foundry_reader_grant_provisioner_unavailable/,
|
||||
);
|
||||
const pendingFinalization = await manager.status(input);
|
||||
assert.equal(pendingFinalization.consumer.product.id, "fleet.positions.current.v2");
|
||||
assert.equal(pendingFinalization.consumer.readerGrantGeneration, 2);
|
||||
assert.equal(grants.has(1), true);
|
||||
const finalizePlan = await manager.plan(input);
|
||||
assert.equal(finalizePlan.action, "finalize-reader-grant-rotation");
|
||||
assert.equal(finalizePlan.effects.includes("bootstrap-scoped-snapshot"), false);
|
||||
assert.ok(finalizePlan.effects.includes("revoke-predecessor-reader-grant"));
|
||||
const replaced = await manager.apply({ ...input, planId: finalizePlan.planId });
|
||||
assert.equal(replaced.consumer.product.id, "fleet.positions.current.v2");
|
||||
assert.equal(replaced.consumer.readerGrantGeneration, 2);
|
||||
assert.equal(grants.has(1), false);
|
||||
assert.equal(grants.get(2), "fleet.positions.current.v2");
|
||||
assert.ok(events.lastIndexOf("snapshot-fleet.positions.current.v2-g2") < events.indexOf("revoke-g1"));
|
||||
} finally {
|
||||
await manager.shutdown();
|
||||
await rm(stateDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
const movingObjectV4Policy = {
|
||||
id: "map-moving-object-current-v4",
|
||||
version: "4.0.0",
|
||||
dataProductId: "fleet.positions.current.v4",
|
||||
productVersion: "4.0.0",
|
||||
staleAfterMs: null,
|
||||
terminalStatuses: ["inactive"],
|
||||
statusContract: {
|
||||
attribute: "signal_state",
|
||||
allowedValues: ["active", "inactive"],
|
||||
missing: "reject",
|
||||
freshness: "none",
|
||||
},
|
||||
removeMode: "canonical-tombstone-or-snapshot-rebase",
|
||||
};
|
||||
|
||||
const movingObjectV4Product = {
|
||||
id: "fleet.positions.current.v4",
|
||||
version: "4.0.0",
|
||||
deliveryMode: "snapshot+patch",
|
||||
semanticTypes: ["map.moving_object"],
|
||||
active: true,
|
||||
};
|
||||
|
||||
function movingObjectV4Target(fieldProjection = ["display_name", "signal_state", "movement_state", "speed_kph"]) {
|
||||
return {
|
||||
application: { id: "44444444-4444-4444-8444-444444444444" },
|
||||
page: { id: "map" },
|
||||
binding: {
|
||||
id: "fleet-current-v4",
|
||||
dataProductId: movingObjectV4Product.id,
|
||||
slotId: "points",
|
||||
delivery: "snapshot+patch",
|
||||
semanticTypes: ["map.moving_object"],
|
||||
fieldProjection,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function movingObjectV4Snapshot(signalState = "active") {
|
||||
return {
|
||||
schemaVersion: "nodedc.data-product.snapshot/v1",
|
||||
dataProduct: { id: movingObjectV4Product.id, version: movingObjectV4Product.version },
|
||||
generatedAt: "2026-07-20T12:00:01.000Z",
|
||||
cursor: "40",
|
||||
facts: [{
|
||||
sourceId: "gelios-unit-001",
|
||||
semanticType: "map.moving_object",
|
||||
observedAt: "2026-01-01T00:00:00.000Z",
|
||||
receivedAt: "2026-07-20T12:00:00.000Z",
|
||||
attributes: {
|
||||
display_name: "Unit 001",
|
||||
signal_state: signalState,
|
||||
movement_state: "stopped",
|
||||
speed_kph: 0,
|
||||
},
|
||||
geometry: { type: "Point", coordinates: [37.61, 55.75] },
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
test("v4 consumer preserves the closed signal and movement states without inventing freshness", async () => {
|
||||
const stateDir = await mkdtemp(join(tmpdir(), "foundry-consumer-v4-"));
|
||||
const currentTarget = movingObjectV4Target();
|
||||
let signalState = "active";
|
||||
const manager = createFoundryDataProductConsumerManager({
|
||||
stateDir,
|
||||
dataPlaneUrl: "http://edp.test",
|
||||
resolveTarget: async () => structuredClone(currentTarget),
|
||||
readReaderToken: async () => "ndc_edprb_v4-reader-capability",
|
||||
inspectReaderGrant: async () => ({ product: movingObjectV4Product, readerGrantAction: "reuse", readerGrantGeneration: 1 }),
|
||||
resolvePolicy: () => movingObjectV4Policy,
|
||||
sanitizeSnapshot: (value) => value,
|
||||
sanitizePatch: (value) => value,
|
||||
fetchImpl: async (request, options = {}) => {
|
||||
assert.equal(options.headers.authorization, "Bearer ndc_edprb_v4-reader-capability");
|
||||
assert.ok(new URL(request).pathname.endsWith("/snapshot"));
|
||||
return Response.json(movingObjectV4Snapshot(signalState));
|
||||
},
|
||||
now: () => Date.parse("2026-07-20T12:00:10.000Z"),
|
||||
});
|
||||
try {
|
||||
const input = { applicationId: currentTarget.application.id, pageId: currentTarget.page.id, bindingId: currentTarget.binding.id };
|
||||
const plan = await manager.plan(input);
|
||||
assert.deepEqual(plan.configuration.policy.statusContract.allowedValues, ["active", "inactive"]);
|
||||
assert.equal(plan.configuration.policy.staleAfterMs, null);
|
||||
const active = await manager.apply({ ...input, planId: plan.planId });
|
||||
assert.equal(active.consumer.subjects[0].status, "active");
|
||||
assert.equal(active.consumer.metrics.staleTransitions, 0);
|
||||
assert.equal((await manager.snapshot(currentTarget)).facts[0].attributes.movement_state, "stopped");
|
||||
|
||||
signalState = "inactive";
|
||||
const refreshPlan = await manager.plan(input);
|
||||
const inactive = await manager.apply({ ...input, planId: refreshPlan.planId });
|
||||
assert.equal(inactive.consumer.subjects[0].status, "inactive");
|
||||
assert.equal(inactive.consumer.metrics.staleTransitions, 0);
|
||||
} finally {
|
||||
await manager.shutdown();
|
||||
await rm(stateDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("v4 consumer rejects bindings that omit signal_state and snapshots outside the closed contract", async () => {
|
||||
const omittedStateDir = await mkdtemp(join(tmpdir(), "foundry-consumer-v4-omitted-"));
|
||||
const omittedTarget = movingObjectV4Target(["display_name", "movement_state"]);
|
||||
const omittedManager = createFoundryDataProductConsumerManager({
|
||||
stateDir: omittedStateDir,
|
||||
dataPlaneUrl: "http://edp.test",
|
||||
resolveTarget: async () => structuredClone(omittedTarget),
|
||||
readReaderToken: async () => "ndc_edprb_v4-reader-capability",
|
||||
inspectReaderGrant: async () => ({ product: movingObjectV4Product, readerGrantAction: "reuse", readerGrantGeneration: 1 }),
|
||||
resolvePolicy: () => movingObjectV4Policy,
|
||||
sanitizeSnapshot: (value) => value,
|
||||
sanitizePatch: (value) => value,
|
||||
});
|
||||
try {
|
||||
await assert.rejects(
|
||||
omittedManager.plan({ applicationId: omittedTarget.application.id, pageId: omittedTarget.page.id, bindingId: omittedTarget.binding.id }),
|
||||
/data_product_consumer_status_field_not_projected/,
|
||||
);
|
||||
} finally {
|
||||
await omittedManager.shutdown();
|
||||
await rm(omittedStateDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
const invalidStateDir = await mkdtemp(join(tmpdir(), "foundry-consumer-v4-invalid-"));
|
||||
const invalidTarget = movingObjectV4Target();
|
||||
const invalidManager = createFoundryDataProductConsumerManager({
|
||||
stateDir: invalidStateDir,
|
||||
dataPlaneUrl: "http://edp.test",
|
||||
resolveTarget: async () => structuredClone(invalidTarget),
|
||||
readReaderToken: async () => "ndc_edprb_v4-reader-capability",
|
||||
inspectReaderGrant: async () => ({ product: movingObjectV4Product, readerGrantAction: "reuse", readerGrantGeneration: 1 }),
|
||||
resolvePolicy: () => movingObjectV4Policy,
|
||||
sanitizeSnapshot: (value) => value,
|
||||
sanitizePatch: (value) => value,
|
||||
fetchImpl: async () => Response.json(movingObjectV4Snapshot("unknown")),
|
||||
});
|
||||
try {
|
||||
const input = { applicationId: invalidTarget.application.id, pageId: invalidTarget.page.id, bindingId: invalidTarget.binding.id };
|
||||
const plan = await invalidManager.plan(input);
|
||||
await assert.rejects(
|
||||
invalidManager.apply({ ...input, planId: plan.planId }),
|
||||
/data_product_consumer_fact_status_invalid/,
|
||||
);
|
||||
const failed = await invalidManager.status(input);
|
||||
assert.equal(failed.consumer.runtimeState, "error");
|
||||
assert.equal(failed.consumer.subjectCount, 0);
|
||||
} finally {
|
||||
await invalidManager.shutdown();
|
||||
await rm(invalidStateDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user