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); });