fix(device-core): activate adopted model profiles safely
This commit is contained in:
@@ -271,7 +271,18 @@ async function registerModelProfile(client, actor, command) {
|
||||
and device_model_profiles.protocol = excluded.protocol
|
||||
and (
|
||||
(
|
||||
device_model_profiles.profile = excluded.profile
|
||||
(
|
||||
device_model_profiles.profile = excluded.profile
|
||||
or (
|
||||
jsonb_typeof(device_model_profiles.profile) = 'object'
|
||||
and device_model_profiles.profile ->> 'schemaVersion' = excluded.schema_version
|
||||
and device_model_profiles.profile ->> 'profileRef' = excluded.profile_ref
|
||||
and device_model_profiles.profile ->> 'vendor' = excluded.vendor
|
||||
and device_model_profiles.profile ->> 'model' = excluded.model
|
||||
and device_model_profiles.profile ->> 'deviceType' = excluded.device_type
|
||||
and device_model_profiles.profile ->> 'protocol' = excluded.protocol
|
||||
)
|
||||
)
|
||||
and device_model_profiles.adapter_version_id = excluded.adapter_version_id
|
||||
and device_model_profiles.schema_artifact_ref = excluded.schema_artifact_ref
|
||||
and device_model_profiles.profile_digest = excluded.profile_digest
|
||||
|
||||
+10
@@ -31,3 +31,13 @@ test("legacy model adoption is a one-way exact-identity registry transition", as
|
||||
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'/);
|
||||
});
|
||||
|
||||
test("adopted rich profile lifecycle changes preserve exact registry identity", async () => {
|
||||
const source = await readFile(
|
||||
new URL("../src/infrastructure-repository.mjs", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
assert.match(source, /profile = excluded\.profile[\s\S]*or \([\s\S]*profile ->> 'schemaVersion' = excluded\.schema_version[\s\S]*profile ->> 'profileRef' = excluded\.profile_ref[\s\S]*profile ->> 'vendor' = excluded\.vendor[\s\S]*profile ->> 'model' = excluded\.model[\s\S]*profile ->> 'deviceType' = excluded\.device_type[\s\S]*profile ->> 'protocol' = excluded\.protocol/);
|
||||
assert.match(source, /adapter_version_id = excluded\.adapter_version_id[\s\S]*schema_artifact_ref = excluded\.schema_artifact_ref[\s\S]*profile_digest = excluded\.profile_digest[\s\S]*capabilities = excluded\.capabilities/);
|
||||
});
|
||||
|
||||
@@ -198,6 +198,88 @@ test("keeps non-legacy model profile identity conflicts fail-closed", async () =
|
||||
assert.equal(client.released, true);
|
||||
});
|
||||
|
||||
test("activates an adopted rich legacy profile without replacing its immutable JSON", 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"],
|
||||
lifecycleState: "active",
|
||||
});
|
||||
const client = scriptedClient([
|
||||
step("begin"),
|
||||
step("insert into device_management_command_receipts", {
|
||||
rows: [{ id: "receipt-profile-activation" }],
|
||||
}),
|
||||
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: adapterVersionId,
|
||||
schema_artifact_ref: command.schemaArtifactRef,
|
||||
profile_digest: command.profileDigest,
|
||||
capabilities: command.capabilities,
|
||||
lifecycle_state: "draft",
|
||||
}],
|
||||
}),
|
||||
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: "active",
|
||||
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: "d",
|
||||
}));
|
||||
|
||||
assert.equal(result.result.created, false);
|
||||
assert.equal(result.result.modelProfile.lifecycleState, "active");
|
||||
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", {
|
||||
|
||||
Reference in New Issue
Block a user