diff --git a/api-v3/test/unit/f4-document-lifecycle-database-contract.test.ts b/api-v3/test/unit/f4-document-lifecycle-database-contract.test.ts new file mode 100644 index 0000000..08de105 --- /dev/null +++ b/api-v3/test/unit/f4-document-lifecycle-database-contract.test.ts @@ -0,0 +1,54 @@ +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'); + +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 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 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 before restoring legacy CHECKs', () => { + const down = migration.slice(migration.indexOf(' public async down(')); + const guardRestore = down.indexOf("OLD.status IN ('CLOSED','RECTIFIED')"); + const actTranslation = down.indexOf('UPDATE inspection_acts'); + assert.ok(guardRestore >= 0); + assert.ok(actTranslation > guardRestore, 'Debe restaurarse primero la guarda D5 para poder traducir 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'/); +});