From 1d3ed5e16eaf7bc0c32de64796afc585882eac0b Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Fri, 21 Aug 2026 18:51:08 +0300 Subject: [PATCH] fix: make command constraint replay restart-safe --- .../005_device_registry_commands.sql | 2 +- .../007_device_lifecycle_commands.sql | 2 +- ...09_device_sensitive_reference_commands.sql | 2 +- .../011_device_control_resource_commands.sql | 2 +- .../management-command-migration.test.mjs | 60 +++++++++++++++++++ 5 files changed, 64 insertions(+), 4 deletions(-) diff --git a/services/device-control-core/migrations/005_device_registry_commands.sql b/services/device-control-core/migrations/005_device_registry_commands.sql index 062ce52..96c8671 100644 --- a/services/device-control-core/migrations/005_device_registry_commands.sql +++ b/services/device-control-core/migrations/005_device_registry_commands.sql @@ -16,6 +16,6 @@ alter table device_management_command_receipts 'edge.ensure', 'route.ensure', 'enrollment_intent.ensure' - )); + )) not valid; commit; diff --git a/services/device-control-core/migrations/007_device_lifecycle_commands.sql b/services/device-control-core/migrations/007_device_lifecycle_commands.sql index 90881e0..81d8926 100644 --- a/services/device-control-core/migrations/007_device_lifecycle_commands.sql +++ b/services/device-control-core/migrations/007_device_lifecycle_commands.sql @@ -20,6 +20,6 @@ alter table device_management_command_receipts 'device.transfer', 'discovery.reject', 'discovery.expire' - )); + )) not valid; commit; diff --git a/services/device-control-core/migrations/009_device_sensitive_reference_commands.sql b/services/device-control-core/migrations/009_device_sensitive_reference_commands.sql index 2902a15..00df9ed 100644 --- a/services/device-control-core/migrations/009_device_sensitive_reference_commands.sql +++ b/services/device-control-core/migrations/009_device_sensitive_reference_commands.sql @@ -22,6 +22,6 @@ alter table device_management_command_receipts 'discovery.expire', 'device_credential_binding.upsert', 'device_credential_binding.revoke' - )); + )) not valid; commit; diff --git a/services/device-control-core/migrations/011_device_control_resource_commands.sql b/services/device-control-core/migrations/011_device_control_resource_commands.sql index cc456e7..3433b10 100644 --- a/services/device-control-core/migrations/011_device_control_resource_commands.sql +++ b/services/device-control-core/migrations/011_device_control_resource_commands.sql @@ -26,6 +26,6 @@ alter table device_management_command_receipts 'device_binding.revoke', 'device_configuration_revision.create', 'device_configuration_desired.set' - )); + )) not valid; commit; diff --git a/services/device-control-core/test/management-command-migration.test.mjs b/services/device-control-core/test/management-command-migration.test.mjs index d868a32..41021c5 100644 --- a/services/device-control-core/test/management-command-migration.test.mjs +++ b/services/device-control-core/test/management-command-migration.test.mjs @@ -2,6 +2,36 @@ import assert from "node:assert/strict"; import { readFile } from "node:fs/promises"; import test from "node:test"; +const replayedIntermediateConstraintMigrations = Object.freeze([ + "005_device_registry_commands.sql", + "007_device_lifecycle_commands.sql", + "009_device_sensitive_reference_commands.sql", + "011_device_control_resource_commands.sql", +]); +const finalCommandKindMigration = "014_device_registry_profile_commands.sql"; +const finalCommandKinds = Object.freeze([ + "owner_scope.ensure", + "project.ensure", + "collection.ensure", + "project_grant.upsert", + "adapter_package.ensure", + "adapter_version.register", + "model_profile.register", + "edge.ensure", + "route.ensure", + "enrollment_intent.ensure", + "device.claim", + "device.update", + "device.transfer", + "discovery.reject", + "discovery.expire", + "device_credential_binding.upsert", + "device_credential_binding.revoke", + "device_binding.ensure", + "device_binding.revoke", + "device_configuration_revision.create", + "device_configuration_desired.set", +]); const migrationUrl = new URL( "../migrations/003_device_management_commands.sql", import.meta.url, @@ -37,3 +67,33 @@ test("repository applies management migration after project access", async () => assert.notEqual(managementIndex, -1); assert.ok(projectAccessIndex < managementIndex); }); + +test("replayed intermediate command constraints do not revalidate historical receipts", async () => { + for (const migrationFile of replayedIntermediateConstraintMigrations) { + const sql = await readFile( + new URL(`../migrations/${migrationFile}`, import.meta.url), + "utf8", + ); + + assert.match( + sql, + /add constraint device_management_command_receipts_command_kind_check[\s\S]+not valid;/, + `${migrationFile} must not reject receipts introduced by a later migration`, + ); + } +}); + +test("final replayed command constraint validates every current command kind", async () => { + const sql = await readFile( + new URL(`../migrations/${finalCommandKindMigration}`, import.meta.url), + "utf8", + ); + + assert.doesNotMatch(sql, /not valid;/); + for (const commandKind of finalCommandKinds) { + assert.ok( + sql.includes(`'${commandKind}'`), + `${finalCommandKindMigration} must validate ${commandKind}`, + ); + } +});