diff --git a/api-v3/test/unit/f6-1-project-invariants.test.ts b/api-v3/test/unit/f6-1-project-invariants.test.ts new file mode 100644 index 0000000..d824b85 --- /dev/null +++ b/api-v3/test/unit/f6-1-project-invariants.test.ts @@ -0,0 +1,71 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import test from 'node:test'; + +function source(path: string): string { + return readFileSync(resolve(process.cwd(), path), 'utf8'); +} + +test('F6.1 conserva una jerarquía territorial única y Empresa fuera del árbol físico', () => { + const structure = source('src/asset-master/inventory-structure.service.ts'); + const dto = source('src/asset-master/dto/create-inventory-structure.dto.ts'); + + assert.match(dto, /'EMPRESA',[\s\S]*'DEPARTAMENTO',[\s\S]*'AREA',[\s\S]*'YACIMIENTO',[\s\S]*'INSTALACION',[\s\S]*'SUBINSTALACION'/); + assert.match(structure, /EMPRESA:\s*null/); + assert.match(structure, /DEPARTAMENTO:\s*null/); + assert.match(structure, /AREA:\s*'departamento'/); + assert.match(structure, /YACIMIENTO:\s*'area'/); + assert.match(structure, /INSTALACION:\s*'yacimiento'/); + assert.match(structure, /SUBINSTALACION:\s*'instalacion'/); + assert.match(structure, /Departamento → Área → Yacimiento → Instalación → Subinstalación/); +}); + +test('F6.1 usa Área + Operadora como contexto de planificación y no mezcla Yacimiento con Área', () => { + const lifecycle = source('src/inspection-visits/inspection-visit-lifecycle.service.ts'); + const visits = source('src/inspection-visits/inspection-visits.service.ts'); + + assert.match(lifecycle, /El contexto operativo de una Inspección es Área \+ Operadora vigente/); + assert.doesNotMatch(lifecycle, /Área\/Yacimiento/); + assert.match(visits, /FROM area_company_relations/); + assert.match(visits, /relation\.relation_role = 'OPERATOR'/); +}); + +test('F6.1 mantiene múltiples Actas por Inspección pero un solo borrador simultáneo', () => { + const act = source('src/database/entities/inspection-act.entity.ts'); + const lifecycle = source('src/inspection-visits/inspection-visit-lifecycle.service.ts'); + + assert.match(act, /uq_inspection_acts_one_draft_per_visit/); + assert.match(act, /where: "status = 'DRAFT'"/); + assert.match(act, /Una Inspección puede acumular múltiples Actas/); + assert.match(lifecycle, /Todas las Actas deben estar SELLADAS antes de cerrar la Inspección/); +}); + +test('F6.1 permite Hallazgos directos sólo en Yacimiento, Instalación y Subinstalación y conserva OTROS', () => { + const field = source('src/inspection-visits/field-findings.service.ts'); + const resolver = source('src/inspection-findings/f3-finding-catalog-resolver.service.ts'); + + assert.match(field, /\['yacimiento', 'instalacion', 'subinstalacion'\]\.includes\(assetTypeCode\)/); + assert.match(field, /assetTypeCode !== 'yacimiento' && !row\.inventoryFamilyId/); + assert.match(field, /canAddAnother: true/); + assert.match(resolver, /code: 'OTHER'/); + assert.match(resolver, /label: 'OTROS'/); +}); + +test('F6.1 abre Inspecciones desde Android usando el lifecycle canónico', () => { + const controller = source('src/inspection-visits/inspection-visits.controller.ts'); + + assert.match(controller, /@Post\('mobile\/open'\)/); + assert.match(controller, /leadInspectorUserId: principal\.userId/); + assert.match(controller, /this\.lifecycle\.plan\(created\.id/); + assert.match(controller, /this\.lifecycle\.start\(created\.id/); +}); + +test('F6.1 separa oficialización GEDO de la activación del vencimiento no urgente', () => { + const report = source('src/inspection-reports/inspection-report-workflow.service.ts'); + const act = source('src/database/entities/inspection-act.entity.ts'); + + assert.match(report, /GEDO oficializa el INF, pero no equivale por sí solo a la notificación/); + assert.match(report, /deadlineActivation: 'PENDING_NOTIFICATION_EVENT'/); + assert.match(act, /NOTIFICATION_DATE = 'GEDO_DATE'/); +});