import 'reflect-metadata'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { resolve } from 'node:path'; import test from 'node:test'; import { REQUIRED_PERMISSIONS_KEY } from '../../src/authorization/decorators/require-permissions.decorator'; import { AssetImportsController } from '../../src/asset-imports/asset-imports.controller'; import { departmentCode, normalizedLegalRightType, organizationImportKey } from '../../src/asset-imports/asset-import-plan'; function source(relative: string): string { return readFileSync(resolve(process.cwd(), relative), 'utf8'); } function permissionFor(method: string): string[] { const controller = AssetImportsController.prototype; return Reflect.getMetadata(REQUIRED_PERMISSIONS_KEY, controller[method as keyof typeof controller]) as string[]; } test('D5.3.7.2 normalizes Departments and legal-right source values', () => { assert.equal(departmentCode('Luján de Cuyo'), 'LUJAN-DE-CUYO'); assert.equal(departmentCode('San Rafael'), 'SAN-RAFAEL'); assert.equal(normalizedLegalRightType('Explotación'), 'EXPLOITATION_CONCESSION'); assert.equal(normalizedLegalRightType('Exploración'), 'EXPLORATION_PERMIT'); assert.equal(normalizedLegalRightType('Concesión de Transporte'), 'TRANSPORT_CONCESSION'); }); test('D5.3.7.2 exposes paginated plan details as read-only import information', () => { assert.deepEqual(permissionFor('planItems'), ['asset_imports.read']); const controller = source('src/asset-imports/asset-imports.controller.ts'); assert.match(controller, /@Get\(':id\/plan\/items'\)/); const service = source('src/asset-imports/asset-imports.service.ts'); assert.match(service, /async planItems\(/); assert.match(service, /entity_kind=\$\$\{params\.length\}/); }); test('D5.3.7.2 migration creates a normalized Department catalog and Area relation', () => { const migration = source('src/database/migrations/1787767200000-phase-d5-3-7-2-relational-territory-plan.ts'); assert.match(migration, /CREATE TABLE administrative_departments/); assert.match(migration, /CREATE TABLE area_department_relations/); assert.match(migration, /uq_administrative_department_name/); assert.match(migration, /uq_area_department_relation_active_pair/); assert.match(migration, /'DEPARTMENT'.*'AREA_DEPARTMENT_RELATION'.*'LEGAL_RIGHT'.*'LEGAL_RIGHT_ORGANIZATION'/s); assert.match(migration, /REVOKE DELETE ON TABLE administrative_departments, area_department_relations/); }); test('D5.3.7.2 territory planning deduplicates catalogs before relations and fields', () => { const service = source('src/asset-imports/asset-imports.service.ts'); const territory = service.slice(service.indexOf('private async buildTerritoryPlan'), service.indexOf('private async buildInventoryPlan')); assert.match(territory, /const departments = new Map/); assert.match(territory, /entityKind: 'DEPARTMENT'|entityKind:'DEPARTMENT'/); assert.match(territory, /entityKind:'AREA_DEPARTMENT_RELATION'|entityKind: 'AREA_DEPARTMENT_RELATION'/); assert.match(territory, /entityKind:'LEGAL_RIGHT'|entityKind: 'LEGAL_RIGHT'/); assert.match(territory, /entityKind:'LEGAL_RIGHT_ORGANIZATION'|entityKind: 'LEGAL_RIGHT_ORGANIZATION'/); assert.match(territory, /matchedDepartmentId/); assert.match(territory, /matchedLegalRightId/); assert.match(territory, /departmentEntityKeys/); assert.match(territory, /legalRightEntityKeys/); }); test('D5.3.7.2 applies relational objects transactionally without duplicating Department attributes', () => { const service = source('src/asset-imports/asset-imports.service.ts'); const apply = service.slice(service.indexOf('private async applyPlanItems'), service.indexOf('private async insertAssetItems')); assert.match(apply, /INSERT INTO administrative_departments/); assert.match(apply, /INSERT INTO area_department_relations/); assert.match(apply, /INSERT INTO area_legal_rights/); assert.match(apply, /INSERT INTO area_legal_right_organizations/); assert.match(apply, /'PENDING'/); assert.match(apply, /no se infiere titularidad/i); const attributes = service.slice(service.indexOf('private async insertKnownAttributes'), service.indexOf('private async insertImportVersions')); assert.doesNotMatch(attributes, /add\('departamento'/); assert.match(service, /return `\(\$\$\{base\+1\}::integer,\$\$\{base\+2\}::uuid\)`/); assert.match(service, /row\.row_number=v\.row_number/); }); test('D5.3.7.2 rollback is logical for relational territory objects', () => { const service = source('src/asset-imports/asset-imports.service.ts'); const rollback = service.slice(service.indexOf('async rollbackPlan'), service.indexOf('async upload')); assert.match(rollback, /area_department_relations SET valid_until=CURRENT_DATE/); assert.match(rollback, /area_legal_rights SET status='REVOKED'/); assert.match(rollback, /administrative_departments SET is_active=false/); assert.doesNotMatch(rollback, /DELETE FROM administrative_departments|DELETE FROM area_department_relations|DELETE FROM area_legal_rights/); }); test('D5.3.7.2.3 stores an applied asset id without PostgreSQL parameter type ambiguity', () => { const service = source('src/asset-imports/asset-imports.service.ts'); assert.match(service, /applied_asset_id=\$2::uuid,applied_object_id=\(\$2::uuid\)::text/); assert.doesNotMatch(service, /applied_asset_id=\$2,applied_object_id=\$2/); }); test('D5.3.7.2.4 snapshots the Maestro and blocks stale READY plans before any write', () => { const service = source('src/asset-imports/asset-imports.service.ts'); const generate = service.slice(service.indexOf('async generatePlan'), service.indexOf('async resolvePlanItem')); const apply = service.slice(service.indexOf('async applyPlan'), service.indexOf('async rollbackPlan')); assert.match(service, /private async masterStateFingerprint/); assert.match(generate, /masterStateBefore = await this\.masterStateFingerprint/); assert.match(generate, /masterStateHash = await this\.masterStateFingerprint/); assert.match(generate, /ASSET_IMPORT_MASTER_CHANGED_DURING_PLAN/); assert.match(generate, /masterStateHash,/); assert.match(apply, /plannedMasterStateHash/); assert.match(apply, /currentMasterStateHash/); assert.match(apply, /El Maestro cambió desde que se generó este plan/); }); test('D5.3.8 preserves operator-local hierarchy instead of inventing universal installation names', () => { const service = source('src/asset-imports/asset-imports.service.ts'); const planner = service.slice(service.indexOf('private async buildInventoryPlan'), service.indexOf('private async assetNameMatches')); assert.match(planner, /sourceLocalStructureSuggestion/); assert.match(planner, /entityKind:'LOCAL_STRUCTURE'/); assert.match(planner, /assetTypeCode:'estructura_local'/); assert.match(planner, /localNomenclature:true/); assert.match(planner, /classificationStatus:'SOURCE_PRESERVED'/); assert.doesNotMatch(planner, /\[A VALIDAR\]/); }); test('D5.3.8 collapses territory ambiguity into one explicit context decision', () => { const service = source('src/asset-imports/asset-imports.service.ts'); assert.match(service, /contextDecisionItems=new Map/); assert.match(service, /inventoryContextDecision:true/); assert.match(service, /candidateOptions/); assert.match(service, /PLAN_CONTEXT_DECISION_REQUIRED/); assert.match(service, /inventoryContextOverrides/); assert.match(service, /ASSET_IMPORT_CONTEXT_MATCH_INVALID/); }); test('D5.3.8 migration adds the provisional local structure type and hierarchy rules', () => { const migration = source('src/database/migrations/1787853600000-phase-d5-3-8-local-inventory-structure.ts'); assert.match(migration, /'LOCAL_STRUCTURE'/); assert.match(migration, /'estructura_local'/); assert.match(migration, /Estructura local \(fuente\)/); assert.match(migration, /nivel_fuente/); assert.match(migration, /clasificacion_fuente/); assert.match(migration, /ruta_fuente/); assert.match(migration, /asset_type_parent_rules/); });