From afb6feb4c1cda5f9189fbc8d101380272fbad11a Mon Sep 17 00:00:00 2001 From: enlineawork Date: Tue, 8 Sep 2026 09:58:12 -0300 Subject: [PATCH] test(f4): harden multi-act immutability gates --- .../test/unit/f4-multi-act-lifecycle.test.ts | 47 +++++++++++++++++-- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/api-v3/test/unit/f4-multi-act-lifecycle.test.ts b/api-v3/test/unit/f4-multi-act-lifecycle.test.ts index 29594ce..1e697e8 100644 --- a/api-v3/test/unit/f4-multi-act-lifecycle.test.ts +++ b/api-v3/test/unit/f4-multi-act-lifecycle.test.ts @@ -8,6 +8,8 @@ const read = (relative: string) => fs.readFileSync(path.join(root, relative), 'u const closing = read('src/inspection-closing/inspection-closing.service.ts'); const lifecycle = read('src/inspection-visits/inspection-visit-lifecycle.service.ts'); +const acts = read('src/inspection-acts/inspection-acts.service.ts'); +const findings = read('src/inspection-findings/inspection-findings.service.ts'); const actEntity = read('src/database/entities/inspection-act.entity.ts'); test('F4 keeps finalized acts irreversible', () => { @@ -31,17 +33,52 @@ test('F4 act locking is independent from inspection-wide verification completion assert.match(prepareBody, /status='LOCKED'/); }); -test('F4 inspection close is the aggregate gate for sealed acts and verification results', () => { +test('F4 blocks act header and inventory mutations once the act leaves draft', () => { + const updateBody = acts.slice(acts.indexOf(' async update('), acts.indexOf(' async cancel(')); + const cancelBody = acts.slice(acts.indexOf(' async cancel('), acts.indexOf(' async getById(')); + assert.match(updateBody, /this\.assertActDraft\(act\)/); + assert.match(cancelBody, /this\.assertActDraft\(act\)/); +}); + +test('F4 blocks finding creation and editing once the act is locked', () => { + const editableGuard = findings.slice( + findings.indexOf(' private assertActEditable('), + findings.indexOf(' private assertFindingOpen('), + ); + assert.match(editableGuard, /act\.status !== InspectionActStatus\.DRAFT/); + assert.match(editableGuard, /visit\.status !== InspectionVisitStatus\.IN_PROGRESS/); + assert.match(editableGuard, /INSPECTION_FINDING_ACT_NOT_EDITABLE/); + + const createBody = findings.slice(findings.indexOf(' async create('), findings.indexOf(' async update(')); + const updateBody = findings.slice(findings.indexOf(' async update('), findings.indexOf(' async updateFollowUp(')); + assert.match(createBody, /this\.assertActEditable\(act, visit\)/); + assert.match(updateBody, /this\.assertActEditable\(act, visit\)/); +}); + +test('F4 sealing requires inspector signature and exactly one resolved company manifestation', () => { + const closeBody = closing.slice(closing.indexOf(' async close('), closing.indexOf(' async signatureContent(')); + assert.match(closeBody, /INSPECTION_ACT_INSPECTOR_SIGNATURE_REQUIRED/); + assert.match(closeBody, /companyOutcomes\.length !== 1/); + assert.match(closeBody, /INSPECTION_ACT_COMPANY_OUTCOME_REQUIRED/); + assert.match(closeBody, /InspectionActSignatureStatus\.ABSENT/); + assert.match(closeBody, /INSPECTION_ACT_COMPANY_MANIFESTATION_PENDING/); + assert.match(closeBody, /status='SEALED'/); +}); + +test('F4 inspection close is the aggregate gate for every non-cancelled act and verification result', () => { 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, /WHERE visit_id=\$1 AND status<>'CANCELLED'/); + assert.match(closeBody, /acts\.length === 0/); + assert.match(closeBody, /INSPECTION_REQUIRES_ACT/); + assert.match(closeBody, /acts\.filter\(\(act\) => act\.status !== 'SEALED'\)/); assert.match(closeBody, /INSPECTION_ACTS_NOT_SEALED/); + assert.match(closeBody, /pendingActs: pending\.map/); assert.match(closeBody, /assertVerificationResultsComplete\(manager, id\)/); assert.match(closeBody, /InspectionVisitStatus\.CLOSED/); }); -test('F4 prevents cancelling an inspection that already has a sealed act', () => { +test('F4 prevents cancelling an inspection that already has any sealed act', () => { const cancelBody = lifecycle.slice(lifecycle.indexOf(' async cancel('), lifecycle.indexOf(' private async lockVisit(')); - assert.match(cancelBody, /status='SEALED'/); + assert.match(cancelBody, /WHERE visit_id=\$1 AND status='SEALED'/); assert.match(cancelBody, /INSPECTION_WITH_SEALED_ACT_CANNOT_CANCEL/); });