From 0e2b524a26f5aee6dd36bf7c22e0c8bdf0d560aa Mon Sep 17 00:00:00 2001 From: enlineawork Date: Tue, 8 Sep 2026 09:08:58 -0300 Subject: [PATCH] test: lock F4 multi-act lifecycle invariants --- .../test/unit/f4-multi-act-lifecycle.test.ts | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 api-v3/test/unit/f4-multi-act-lifecycle.test.ts diff --git a/api-v3/test/unit/f4-multi-act-lifecycle.test.ts b/api-v3/test/unit/f4-multi-act-lifecycle.test.ts new file mode 100644 index 0000000..29594ce --- /dev/null +++ b/api-v3/test/unit/f4-multi-act-lifecycle.test.ts @@ -0,0 +1,47 @@ +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 closing = read('src/inspection-closing/inspection-closing.service.ts'); +const lifecycle = read('src/inspection-visits/inspection-visit-lifecycle.service.ts'); +const actEntity = read('src/database/entities/inspection-act.entity.ts'); + +test('F4 keeps finalized acts irreversible', () => { + const reopenBody = closing.slice(closing.indexOf(' async reopen('), closing.indexOf(' async signInspector(')); + assert.match(reopenBody, /INSPECTION_ACT_LOCK_IS_IMMUTABLE/); + assert.match(reopenBody, /throw new ConflictException/); + assert.doesNotMatch(reopenBody, /UPDATE inspection_acts/); + assert.doesNotMatch(reopenBody, /InspectionActStatus\.DRAFT/); +}); + +test('F4 permits multiple acts per inspection but only one simultaneous draft', () => { + assert.match(actEntity, /uq_inspection_acts_one_draft_per_visit/); + assert.match(actEntity, /where: "status = 'DRAFT'"/); + assert.doesNotMatch(actEntity, /uq_inspection_acts_visit['"]/); +}); + +test('F4 act locking is independent from inspection-wide verification completion', () => { + const prepareBody = closing.slice(closing.indexOf(' async prepare('), closing.indexOf(' async reopen(')); + assert.doesNotMatch(prepareBody, /inspection_finding_verification_visits/); + assert.doesNotMatch(prepareBody, /INSPECTION_VERIFICATION_RESULTS_REQUIRED/); + assert.match(prepareBody, /status='LOCKED'/); +}); + +test('F4 inspection close is the aggregate gate for sealed acts and verification results', () => { + const closeBody = lifecycle.slice(lifecycle.indexOf(' async close('), lifecycle.indexOf(' async cancel(')); + assert.match(closeBody, /status<>'CANCELLED'/); + assert.match(closeBody, /act\.status !== 'SEALED'/); + assert.match(closeBody, /INSPECTION_ACTS_NOT_SEALED/); + assert.match(closeBody, /assertVerificationResultsComplete\(manager, id\)/); + assert.match(closeBody, /InspectionVisitStatus\.CLOSED/); +}); + +test('F4 prevents cancelling an inspection that already has a sealed act', () => { + const cancelBody = lifecycle.slice(lifecycle.indexOf(' async cancel('), lifecycle.indexOf(' private async lockVisit(')); + assert.match(cancelBody, /status='SEALED'/); + assert.match(cancelBody, /INSPECTION_WITH_SEALED_ACT_CANNOT_CANCEL/); +});