fix(device-core): adopt legacy model profiles safely

This commit is contained in:
Codex
2026-08-13 10:23:38 +03:00
parent 9c359111ea
commit 185c54ced2
3 changed files with 237 additions and 12 deletions
@@ -17,3 +17,17 @@ test("catalog, Edge and route upserts enforce irreversible lifecycle transitions
assert.doesNotMatch(source, /device_(?:adapter_versions|model_profiles|edges|routes)\.lifecycle_state = 'retired'[\s\S]{0,160}excluded\.lifecycle_state = 'active'/);
});
test("legacy model adoption is a one-way exact-identity registry transition", async () => {
const source = await readFile(
new URL("../src/infrastructure-repository.mjs", import.meta.url),
"utf8",
);
assert.match(source, /profile ->> 'schemaVersion' = excluded\.schema_version/);
assert.match(source, /profile ->> 'profileRef' = excluded\.profile_ref/);
assert.match(source, /profile ->> 'vendor' = excluded\.vendor/);
assert.match(source, /profile ->> 'model' = excluded\.model/);
assert.match(source, /profile ->> 'deviceType' = excluded\.device_type/);
assert.match(source, /profile ->> 'protocol' = excluded\.protocol/);
assert.match(source, /adapter_version_id is null[\s\S]*schema_artifact_ref is null[\s\S]*profile_digest is null[\s\S]*cardinality\(device_model_profiles\.capabilities\) = 0[\s\S]*lifecycle_state = 'active'[\s\S]*excluded\.lifecycle_state = 'draft'/);
});
@@ -10,6 +10,8 @@ const now = new Date("2026-08-10T00:00:00.000Z");
const projectId = "11111111-1111-4111-8111-111111111111";
const edgeId = "22222222-2222-4222-8222-222222222222";
const routeId = "33333333-3333-4333-8333-333333333333";
const adapterPackageId = "44444444-4444-4444-8444-444444444444";
const adapterVersionId = "55555555-5555-4555-8555-555555555555";
test("commits an owner-authorized generic Edge registration", async () => {
const actor = managementActor("owner");
@@ -55,6 +57,147 @@ test("commits an owner-authorized generic Edge registration", async () => {
assert.equal(client.released, true);
});
test("adopts a legacy metadata-only model profile into the versioned registry", async () => {
const actor = managementActor("owner");
const command = normalizeDeviceManagementCommand("model_profile.register", {
adapterVersionRef: `adapter-version:${adapterVersionId}`,
profileRef: "vendor.model.protocol.v1",
schemaVersion: "nodedc.device-model-profile.v1",
vendor: "Example Vendor",
model: "Model One",
deviceType: "tracker",
protocol: "GENERIC_TCP",
schemaArtifactRef: "artifact:model-profiles/vendor-model-v1",
profileDigest: `sha256:${"e".repeat(64)}`,
capabilities: ["telemetry.observe"],
});
const client = scriptedClient([
step("begin"),
step("insert into device_management_command_receipts", {
rows: [{ id: "receipt-profile" }],
}),
step("from device_adapter_versions av", {
rows: [{
id: adapterVersionId,
adapter_package_id: adapterPackageId,
version: "1.0.0",
runtime_package_ref: "artifact:device-adapters/vendor-model-1.0.0",
content_digest: `sha256:${"f".repeat(64)}`,
contract_version: "nodedc.device-adapter.v1",
capabilities: ["telemetry.observe"],
lifecycle_state: "active",
package_lifecycle_state: "active",
created_at: now,
updated_at: now,
}],
}),
step("from device_model_profiles", {
rows: [{
profile_ref: command.profileRef,
adapter_version_id: null,
schema_artifact_ref: null,
profile_digest: null,
capabilities: [],
lifecycle_state: "active",
}],
}),
step("insert into device_model_profiles", {
rows: [{
profile_ref: command.profileRef,
schema_version: command.schemaVersion,
vendor: command.vendor,
model: command.model,
device_type: command.deviceType,
protocol: command.protocol,
adapter_version_id: adapterVersionId,
schema_artifact_ref: command.schemaArtifactRef,
profile_digest: command.profileDigest,
capabilities: command.capabilities,
lifecycle_state: "draft",
created_at: now,
updated_at: now,
created: false,
}],
}),
step("insert into device_audit_events"),
step("update device_management_command_receipts"),
step("commit"),
]);
const repository = repositoryWithClient(client);
const result = await repository.executeManagementCommand(commandInput({
actor,
commandKind: "model_profile.register",
command,
digestCharacter: "e",
}));
assert.equal(result.replayed, false);
assert.equal(result.result.created, false);
assert.equal(
result.result.modelProfile.adapterVersionRef,
`adapter-version:${adapterVersionId}`,
);
assert.equal(result.result.modelProfile.lifecycleState, "draft");
assert.equal(client.remaining(), 0);
assert.equal(client.released, true);
});
test("keeps non-legacy model profile identity conflicts fail-closed", async () => {
const actor = managementActor("owner");
const command = normalizeDeviceManagementCommand("model_profile.register", {
adapterVersionRef: `adapter-version:${adapterVersionId}`,
profileRef: "vendor.model.protocol.v1",
schemaVersion: "nodedc.device-model-profile.v1",
vendor: "Example Vendor",
model: "Model One",
deviceType: "tracker",
protocol: "GENERIC_TCP",
schemaArtifactRef: "artifact:model-profiles/vendor-model-v1",
profileDigest: `sha256:${"e".repeat(64)}`,
capabilities: ["telemetry.observe"],
});
const client = scriptedClient([
step("begin"),
step("insert into device_management_command_receipts", {
rows: [{ id: "receipt-profile-conflict" }],
}),
step("from device_adapter_versions av", {
rows: [{
id: adapterVersionId,
adapter_package_id: adapterPackageId,
lifecycle_state: "active",
package_lifecycle_state: "active",
}],
}),
step("from device_model_profiles", {
rows: [{
profile_ref: command.profileRef,
adapter_version_id: null,
schema_artifact_ref: "artifact:legacy-but-partial",
profile_digest: null,
capabilities: [],
lifecycle_state: "active",
}],
}),
step("insert into device_model_profiles"),
step("rollback"),
]);
const repository = repositoryWithClient(client);
await assert.rejects(
repository.executeManagementCommand(commandInput({
actor,
commandKind: "model_profile.register",
command,
digestCharacter: "f",
})),
/device_model_profile_identity_conflict/,
);
assert.equal(client.remaining(), 0);
assert.equal(client.released, true);
});
test("denies project route mutation without an explicit project grant", async () => {
const actor = managementActor("owner");
const command = normalizeDeviceManagementCommand("route.ensure", {