import assert from 'node:assert/strict'; import fs from 'node:fs'; import path from 'node:path'; import test from 'node:test'; const root = process.cwd(); const read = (relative: string) => fs.readFileSync(path.join(root, relative), 'utf8'); const migration = read('src/database/migrations/1790000600000-f4-align-document-lifecycle-constraints.ts'); const closing = read('src/inspection-closing/inspection-closing.service.ts'); const reports = read('src/inspection-reports/inspection-reports.service.ts'); const acts = read('src/inspection-acts/inspection-acts.service.ts'); const delivery = read('src/inspection-reports/inspection-document-delivery.service.ts'); test('F4 PostgreSQL accepts the active Act and report lifecycle states emitted by services', () => { assert.match(migration, /status IN \('WORKING','OFFICIALIZED','FROZEN','CANCELLED'\)/); assert.match(migration, /status IN \('DRAFT','LOCKED','SEALED','READY','CLOSED','CANCELLED','RECTIFIED'\)/); assert.match(reports, /'WORKING'/); assert.match(closing, /status='LOCKED'/); assert.match(closing, /status='SEALED'/); }); test('F4 PostgreSQL accepts the ACT-NNNNN-DD-MM-YY code generated by the active service', () => { assert.match(acts, /`ACT-\$\{String\(actNumber\)\.padStart\(5, '0'\)\}-\$\{dateRow\.date_part\}`/); assert.match(migration, /ACT-\[0-9\]\{5\}-\[0-9\]\{2\}-\[0-9\]\{2\}-\[0-9\]\{2\}/); assert.match(migration, /ACTA-\[0-9\]\{4\}-\[0-9\]\{6\}/); }); test('F4 PostgreSQL accepts LOCKED and SEALED in the immutable Act version ledger', () => { assert.match(migration, /event IN \('CREATED','UPDATED','LOCKED','SEALED','READY','REOPENED','CLOSED','CANCELLED'\)/); assert.match(closing, /InspectionActVersionEvent\.LOCKED/); assert.match(closing, /InspectionActVersionEvent\.SEALED/); }); test('F4 delivery constraint accepts INSPECTOR and retires DIRECTOR from the active contract', () => { const up = migration.slice(migration.indexOf(' public async up('), migration.indexOf(' public async down(')); assert.match(delivery, /'COMPANY' \| 'OFFICE' \| 'INSPECTOR'/); assert.match(delivery, /recipientKind:'INSPECTOR'/); assert.match(up, /recipient_kind IN \('COMPANY','OFFICE','INSPECTOR'\)/); assert.doesNotMatch(up, /recipient_kind IN \('COMPANY','OFFICE','DIRECTOR'\)/); }); test('F4 signature trigger binds signatures to the immutable LOCKED snapshot', () => { const up = migration.slice(migration.indexOf(' public async up('), migration.indexOf(' public async down(')); assert.match(up, /dhv2_guard_act_signature_ready/); assert.match(up, /current_status <> 'LOCKED'/); assert.match(up, /current_sha IS DISTINCT FROM NEW\.prepared_sha256/); assert.match(up, /OLD\.status IN \('SEALED','CLOSED','RECTIFIED'\)/); }); test('F4 SEALED rows require closure metadata at database level', () => { const up = migration.slice(migration.indexOf(' public async up('), migration.indexOf(' public async down(')); assert.match(up, /status NOT IN \('SEALED','CLOSED'\)/); assert.match(up, /closed_at IS NOT NULL/); assert.match(up, /closed_by IS NOT NULL/); assert.match(up, /closure_sha256 ~ '\^\[0-9a-f\]\{64\}\$'/); }); test('F4 lifecycle rollback translates new states and codes before restoring legacy CHECKs', () => { const down = migration.slice(migration.indexOf(' public async down(')); const guardRestore = down.indexOf("OLD.status IN ('CLOSED','RECTIFIED')"); const codeTranslation = down.indexOf("SET code='ACTA-'"); const actTranslation = down.indexOf('SET status=CASE'); assert.ok(guardRestore >= 0); assert.ok(codeTranslation > guardRestore, 'Debe restaurarse primero la guarda D5 para traducir códigos F4'); assert.ok(actTranslation > codeTranslation, 'El código debe traducirse antes de convertir SEALED a CLOSED'); assert.match(down, /WHEN status='LOCKED' THEN 'READY'/); assert.match(down, /WHEN status='SEALED' THEN 'CLOSED'/); assert.match(down, /WHEN event='LOCKED' THEN 'READY'/); assert.match(down, /WHEN event='SEALED' THEN 'CLOSED'/); assert.match(down, /SET status='FROZEN'/); assert.match(down, /recipient_kind IN \('COMPANY','OFFICE','DIRECTOR','INSPECTOR'\)/); });