feat(device-core): add device ownership lifecycle

This commit is contained in:
Codex
2026-08-10 18:03:56 +03:00
parent 72db23c0e9
commit fceaca9546
17 changed files with 2218 additions and 54 deletions
@@ -0,0 +1,60 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";
const migrationUrl = new URL(
"../migrations/006_device_lifecycle_ownership.sql",
import.meta.url,
);
const repositoryUrl = new URL("../src/postgres-repository.mjs", import.meta.url);
test("lifecycle migration separates direct ownership from legacy contours", async () => {
const sql = await readFile(migrationUrl, "utf8");
assert.match(sql, /alter column contour_id drop not null/);
assert.match(sql, /add column if not exists owner_scope_id uuid/);
assert.match(sql, /device_instances_project_owner_fk/);
assert.match(sql, /device_instances_ownership_mode_check/);
assert.match(sql, /references device_projects\(id, owner_scope_id\)/);
assert.match(sql, /\) not valid;/);
});
test("route-bound discovery and enrollment evidence are DB constrained", async () => {
const sql = await readFile(migrationUrl, "utf8");
assert.match(sql, /device_discoveries_route_context_fk/);
assert.match(sql, /device_discoveries_enrollment_context_fk/);
assert.match(sql, /device_enrollment_observed_discovery_fk/);
assert.match(sql, /device_enrollment_intents_active_identity_idx/);
assert.match(sql, /where lifecycle_state in \('pending', 'observed', 'claimed'\)/);
});
test("ownership history supports transfer without rewriting session provenance", async () => {
const sql = await readFile(migrationUrl, "utf8");
assert.match(sql, /create table if not exists device_ownership_transitions/);
assert.match(sql, /transition_kind in \('claim', 'transfer'\)/);
assert.match(sql, /device_ownership_single_claim_idx/);
assert.match(sql, /device_assert_session_current_project/);
assert.match(sql, /device_assert_enrollment_current_project/);
assert.match(sql, /drop constraint if exists device_sessions_device_id_project_id_fkey/);
});
test("lifecycle migration contains no tenant, device, route or credential seed", async () => {
const sql = await readFile(migrationUrl, "utf8");
assert.doesNotMatch(sql, /insert\s+into/i);
assert.doesNotMatch(sql, /dcctouch|arusnavi|\bb2\b|imei|gelios/i);
assert.doesNotMatch(sql, /155\.212\.|device\.nodedc\.ru|synology/i);
assert.doesNotMatch(sql, /password|secret|private_key|credential_ref/i);
});
test("repository applies lifecycle migration after registry commands", async () => {
const source = await readFile(repositoryUrl, "utf8");
const commandsIndex = source.indexOf("005_device_registry_commands.sql");
const lifecycleIndex = source.indexOf("006_device_lifecycle_ownership.sql");
assert.notEqual(commandsIndex, -1);
assert.notEqual(lifecycleIndex, -1);
assert.ok(commandsIndex < lifecycleIndex);
});