diff --git a/infra/deploy-runner/build-module-foundry-consumer-policy-artifact.mjs b/infra/deploy-runner/build-module-foundry-consumer-policy-artifact.mjs index 3ba301f..d244a06 100644 --- a/infra/deploy-runner/build-module-foundry-consumer-policy-artifact.mjs +++ b/infra/deploy-runner/build-module-foundry-consumer-policy-artifact.mjs @@ -127,8 +127,79 @@ async function assertConsumerPolicyBoundary() { || zoneV2Policy.removeMode !== "canonical-tombstone-or-snapshot-rebase" ) throw new Error("foundry_consumer_policy_zone_v2_contract_invalid"); - for (const [id, policy] of [["v4", movingObjectV4Policy], ["zone-v2", zoneV2Policy]]) { - if (/(provider|tenant|connection|endpoint|credential|token|secret|authorization)/i.test(JSON.stringify(policy))) { + const unitProfileV1Matches = registry.policies?.filter((policy) => ( + policy?.dataProductId === "fleet.units.profile.current.v1" && policy?.productVersion === "1.0.0" + )) || []; + if (unitProfileV1Matches.length !== 1) throw new Error("foundry_consumer_policy_unit_profile_v1_missing_or_ambiguous"); + const unitProfileV1Policy = unitProfileV1Matches[0]; + const expectedUnitProfileProjection = [ + "corrected_engine_hours_factor", + "corrected_mileage_factor", + "display_name", + "filter_by_satellite_count_enabled", + "filter_by_satellite_count_value", + "filter_emissions", + "hardware_manufacturer_name", + "hardware_port", + "hardware_type_class", + "hardware_type_name", + "limit_acceleration", + "lost_connection_enabled", + "lost_connection_time_value", + "maximum_permissible_speed", + "maximum_valid_height", + "maximum_valid_speed", + "mileage_by_ignition", + "minimum_movement_speed", + "minimum_movement_time", + "minimum_parking_time", + "minimum_stop_time", + "minimum_trip_distance", + "minimum_valid_height", + "speed_parameter", + "trip_detection_type", + "unit_type_class", + "unit_type_name", + "use_odometer", + ]; + if ( + unitProfileV1Policy.id !== "map-moving-object-unit-profile-current-v1" + || unitProfileV1Policy.version !== "1.0.0" + || unitProfileV1Policy.freshness !== "none" + || unitProfileV1Policy.staleAfterMs !== null + || JSON.stringify(unitProfileV1Policy.terminalStatuses) !== JSON.stringify([]) + || unitProfileV1Policy.consumerContract?.ontologyRevision !== "ontology.map.moving_object.v3" + || unitProfileV1Policy.consumerContract?.deliveryMode !== "snapshot+patch" + || JSON.stringify(unitProfileV1Policy.consumerContract?.semanticTypes) !== JSON.stringify(["map.moving_object"]) + || JSON.stringify(unitProfileV1Policy.consumerContract?.fieldProjection) !== JSON.stringify(expectedUnitProfileProjection) + || JSON.stringify(unitProfileV1Policy.consumerContract?.geometryTypes) !== JSON.stringify([]) + || unitProfileV1Policy.consumerContract?.subjectIdentity !== "semantic-type+source-id" + || unitProfileV1Policy.consumerContract?.snapshotMode !== "atomic-replace" + || unitProfileV1Policy.consumerContract?.patchMode !== "atomic" + || unitProfileV1Policy.removeMode !== "canonical-tombstone-or-snapshot-rebase" + ) throw new Error("foundry_consumer_policy_unit_profile_v1_contract_invalid"); + + for (const [id, policy] of [ + ["v4", movingObjectV4Policy], + ["zone-v2", zoneV2Policy], + ["unit-profile-v1", unitProfileV1Policy], + ]) { + const allowedPolicyKeys = new Set([ + "id", + "version", + "dataProductId", + "productVersion", + "freshness", + "staleAfterMs", + "terminalStatuses", + "statusContract", + "consumerContract", + "removeMode", + ]); + if ( + Object.keys(policy).some((key) => !allowedPolicyKeys.has(key)) + || /(provider|tenant|endpoint|credential|token|secret|authorization)/i.test(JSON.stringify(policy)) + ) { throw new Error(`foundry_consumer_policy_${id}_transport_boundary_violation`); } } diff --git a/infra/deploy-runner/test_module_foundry_consumer_policy_artifact.py b/infra/deploy-runner/test_module_foundry_consumer_policy_artifact.py index 25d9393..b8e3371 100644 --- a/infra/deploy-runner/test_module_foundry_consumer_policy_artifact.py +++ b/infra/deploy-runner/test_module_foundry_consumer_policy_artifact.py @@ -131,6 +131,27 @@ class ModuleFoundryConsumerPolicyArtifactTest(unittest.TestCase): for word in ("provider", "tenant", "connection", "endpoint", "credential", "token", "secret", "authorization") )) + profile_policies = [ + policy for policy in registry["policies"] + if policy["dataProductId"] == "fleet.units.profile.current.v1" + and policy["productVersion"] == "1.0.0" + ] + self.assertEqual(len(profile_policies), 1) + profile_policy = profile_policies[0] + self.assertEqual(profile_policy["id"], "map-moving-object-unit-profile-current-v1") + self.assertEqual(profile_policy["freshness"], "none") + self.assertIsNone(profile_policy["staleAfterMs"]) + self.assertEqual(profile_policy["terminalStatuses"], []) + self.assertEqual(profile_policy["consumerContract"]["ontologyRevision"], "ontology.map.moving_object.v3") + self.assertEqual(profile_policy["consumerContract"]["semanticTypes"], ["map.moving_object"]) + self.assertEqual(len(profile_policy["consumerContract"]["fieldProjection"]), 28) + self.assertEqual(profile_policy["consumerContract"]["geometryTypes"], []) + self.assertEqual(profile_policy["consumerContract"]["subjectIdentity"], "semantic-type+source-id") + self.assertFalse(any( + word in json.dumps(profile_policy).lower() + for word in ("provider", "tenant", "endpoint", "credential", "token", "secret", "authorization") + )) + with tarfile.open(first_artifact, "r:gz") as archive: members = archive.getmembers() names = [member.name for member in members]