110 lines
6.0 KiB
TypeScript
110 lines
6.0 KiB
TypeScript
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 acts = read('src/inspection-acts/inspection-acts.service.ts');
|
|
const findings = read('src/inspection-findings/inspection-findings.service.ts');
|
|
const fieldFindings = read('src/inspection-visits/field-findings.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['"]/);
|
|
|
|
const createBody = acts.slice(acts.indexOf(' async create('), acts.indexOf(' async update('));
|
|
const singleDraftGuard = acts.slice(
|
|
acts.indexOf(' private async assertVisitHasNoDraftAct('),
|
|
acts.indexOf(' private async assertVisitAssets('),
|
|
);
|
|
assert.match(createBody, /this\.assertVisitOpen\(visit\)/);
|
|
assert.match(createBody, /this\.assertVisitHasNoDraftAct\(manager, visitId\)/);
|
|
assert.match(singleDraftGuard, /WHERE visit_id = \$1 AND status = 'DRAFT'/);
|
|
assert.doesNotMatch(singleDraftGuard, /LOCKED|SEALED/);
|
|
});
|
|
|
|
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 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(' private actSelect('));
|
|
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 close('));
|
|
assert.match(createBody, /this\.assertActEditable\(act, visit\)/);
|
|
assert.match(updateBody, /this\.assertActEditable\(act, visit\)/);
|
|
});
|
|
|
|
test('F4 field finding flow only targets a draft act of the same in-progress inspection', () => {
|
|
const createBody = fieldFindings.slice(fieldFindings.indexOf(' async create('), fieldFindings.indexOf(' private async requireGate('));
|
|
const draftGuard = fieldFindings.slice(
|
|
fieldFindings.indexOf(' private async requireDraftAct('),
|
|
fieldFindings.lastIndexOf('\n}'),
|
|
);
|
|
assert.match(createBody, /this\.requireDraftAct\(visitId, dto\.actId\)/);
|
|
assert.match(createBody, /this\.findings\.create\(act\.id, payload, principal, request\)/);
|
|
assert.match(fieldFindings, /row\.visitStatus !== 'IN_PROGRESS'/);
|
|
assert.match(draftGuard, /act\.status !== 'DRAFT'/);
|
|
assert.match(draftGuard, /FIELD_FINDING_ACT_NOT_DRAFT/);
|
|
assert.match(draftGuard, /WHERE visit_id = \$1::uuid[\s\S]*AND status = 'DRAFT'/);
|
|
});
|
|
|
|
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, /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 any sealed act', () => {
|
|
const cancelBody = lifecycle.slice(lifecycle.indexOf(' async cancel('), lifecycle.indexOf(' private async lockVisit('));
|
|
assert.match(cancelBody, /WHERE visit_id=\$1 AND status='SEALED'/);
|
|
assert.match(cancelBody, /INSPECTION_WITH_SEALED_ACT_CANNOT_CANCEL/);
|
|
});
|