Files
dh-inspeccion-v2/api-v3/test/unit/f5-inventory-authoritative-contract.test.ts
T
admin 7be5d89204 F6.1 hotfix · jerarquía de Inspecciones y scope Yacimiento (#37)
Corrige creación de Inspección, separa Departamento→Área→Yacimiento, elimina el UPDATE oculto de checklist en lectura, integra Preparación para campo como etapa de Inspección, limita operaciones Android al Yacimiento, preserva scope en edición, auto-incluye Inventarios accionables, valida Operadora a plannedStartAt y recompila Android 0.15.2/vc24 con el ícono exacto de presentación.
2026-09-10 16:06:48 -03:00

145 lines
7.3 KiB
TypeScript

import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';
import { loadF5InventoryAuthoritativeSource } from '../../src/reference-data/f5-authoritative-inventory-source';
function key(value: string): string {
return value.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.toLowerCase()
.replace(/[^a-z0-9]+/g, ' ')
.trim()
.replace(/\s+/g, ' ');
}
test('F5 territory preload is exactly the authorized Tablas de yacimiento y areas workbook', () => {
const source = loadF5InventoryAuthoritativeSource().areaSource;
assert.equal(source.file, 'Tablas de yacimiento y areas.xlsx');
assert.equal(source.sheet, 'cr26e_tabla1');
assert.equal(source.sha256, '8260fcadebbcd631a4c95260d0a67c3ecb28d497b32decb02a1c0847be5afa78');
assert.equal(source.rows.length, 230);
const areas = new Set(source.rows.map((row) => key(row.area)));
const literalYacimientoNames = new Set(source.rows.map((row) => row.yacimiento.trim()));
const yacimientos = new Set(source.rows.map((row) => key(row.yacimiento)));
const areaYacimiento = new Set(source.rows.map((row) => `${key(row.area)}|${key(row.yacimiento)}`));
const departments = new Set(source.rows.map((row) => key(row.departamento)));
const concessionTypes = new Set(source.rows.map((row) => key(row.tipoConcesion)));
const operators = new Set(source.rows.map((row) => key(row.empresaOperadora)));
assert.equal(areas.size, 64);
// The authorized workbook contains 220 literal Yacimiento names. The F5 key
// intentionally folds accents/punctuation/case, so one pair of literal names
// collides into the same normalized key. Yacimiento identity is Area+Yacimiento,
// never a globally-unique normalized name.
assert.equal(literalYacimientoNames.size, 220);
assert.equal(yacimientos.size, 219);
assert.equal(areaYacimiento.size, 230);
assert.equal(departments.size, 7);
assert.equal(concessionTypes.size, 2);
assert.equal(operators.size, 13);
const literalNamesByKey = new Map<string, Set<string>>();
for (const row of source.rows) {
const normalized = key(row.yacimiento);
const literals = literalNamesByKey.get(normalized) ?? new Set<string>();
literals.add(row.yacimiento.trim());
literalNamesByKey.set(normalized, literals);
}
assert.equal([...literalNamesByKey.values()].filter((items) => items.size > 1).length, 1);
const literalAreasByYacimiento = new Map<string, Set<string>>();
const areasByYacimiento = new Map<string, Set<string>>();
for (const row of source.rows) {
const literal = row.yacimiento.trim();
const literalMemberships = literalAreasByYacimiento.get(literal) ?? new Set<string>();
literalMemberships.add(key(row.area));
literalAreasByYacimiento.set(literal, literalMemberships);
const normalized = key(row.yacimiento);
const memberships = areasByYacimiento.get(normalized) ?? new Set<string>();
memberships.add(key(row.area));
areasByYacimiento.set(normalized, memberships);
}
const literalMembershipSurplus = [...literalAreasByYacimiento.values()]
.reduce((total, memberships) => total + Math.max(0, memberships.size - 1), 0);
const normalizedMembershipSurplus = [...areasByYacimiento.values()]
.reduce((total, memberships) => total + Math.max(0, memberships.size - 1), 0);
assert.equal(literalMembershipSurplus, 10);
assert.equal(normalizedMembershipSurplus, 11);
for (const area of areas) {
const rows = source.rows.filter((row) => key(row.area) === area);
assert.equal(new Set(rows.map((row) => key(row.departamento))).size, 1, `${area}: departamento`);
assert.equal(new Set(rows.map((row) => key(row.tipoConcesion))).size, 1, `${area}: concesión`);
assert.equal(new Set(rows.map((row) => key(row.empresaOperadora))).size, 1, `${area}: operadora`);
}
});
test('F5 technical catalog is exactly final_modelov2 and resolves internal IDEM references', () => {
const source = loadF5InventoryAuthoritativeSource().catalogSource;
assert.equal(source.file, 'final_modelov2.xlsx');
assert.equal(source.sheet, 'Hoja1');
assert.equal(source.sha256, 'c9a2d1db59fff2157162c41009b8c9042a3c7a3001649239a07732a3b8fca155');
assert.equal(source.installations.length, 14);
assert.equal(source.subinstallations.length, 109);
const installationKeys = new Set(source.installations.map((item) => key(item.name)));
assert.equal(installationKeys.size, 14);
for (const subinstallation of source.subinstallations) {
assert.ok(installationKeys.has(key(subinstallation.installation)), `${subinstallation.name} parent`);
assert.ok(subinstallation.findings.every((finding) => !/^idem\b/i.test(finding.trim())), `${subinstallation.name} has unresolved IDEM`);
}
for (const installation of source.installations) {
assert.ok(installation.findings.every((finding) => !/^idem\b/i.test(finding.trim())), `${installation.name} has unresolved IDEM`);
}
const allFindings = new Map<string, string>();
for (const finding of source.universalFindings) allFindings.set(key(finding), finding);
for (const installation of source.installations) {
for (const finding of installation.findings) allFindings.set(key(finding), finding);
}
for (const subinstallation of source.subinstallations) {
for (const finding of subinstallation.findings) allFindings.set(key(finding), finding);
}
assert.equal(allFindings.size, 177);
assert.deepEqual(
new Set(source.universalFindings.map(key)),
new Set([
'ORDEN Y LIMPIEZA',
'CARTELERIA PREVENTIVA / INFORMATIVA',
'EXTINTORES',
].map(key)),
);
});
test('F5/F6.1 source contracts keep Empresa out of physical ownership and preserve sealed documents on merge', () => {
const mergeSource = readFileSync('src/asset-master/inventory-merge.service.ts', 'utf8');
const fieldSource = readFileSync('src/inspection-visits/field-inventory.service.ts', 'utf8');
const contextMigration = readFileSync('src/database/migrations/1790087250000-f5-operational-context-compatibility.ts', 'utf8');
const catalogMigration = readFileSync('src/database/migrations/1790087300000-f5-authoritative-inventory-catalog.ts', 'utf8');
assert.match(mergeSource, /resolvePhysicalAreaId/);
assert.match(mergeSource, /documentInvariantsBefore/);
assert.match(mergeSource, /locked_sha256/);
assert.match(mergeSource, /closure_sha256/);
assert.match(mergeSource, /frozen_sha256/);
assert.match(mergeSource, /gedo_pdf_sha256/);
assert.match(mergeSource, /historicalReferencesRewritten:\s*false/);
assert.doesNotMatch(mergeSource, /source\.operatorCompanyId\s*!==\s*canonical\.operatorCompanyId/);
assert.doesNotMatch(mergeSource, /misma Área y Operadora/);
assert.match(fieldSource, /Yacimiento definido como alcance de esta inspección/);
assert.match(fieldSource, /scopeAssetId/);
assert.match(fieldSource, /area_company_relations/);
assert.doesNotMatch(fieldSource, /asset\.operator_company_id\s*=\s*\$2::uuid/);
assert.match(contextMigration, /public async up[\s\S]*installAreaOwnedGuard/);
assert.match(contextMigration, /public async down[\s\S]*installLegacyPairedGuard/);
assert.match(contextMigration, /La Empresa se cambia en la relación temporal del Área/);
assert.doesNotMatch(catalogMigration, /unaccent\s*\(/i);
assert.doesNotMatch(catalogMigration, /source_reference\s+IS\s+NULL/i);
assert.match(catalogMigration, /F5_SOURCE_FAMILY_COUNT\s*=\s*123/);
});