import 'reflect-metadata'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { join, 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 { analyzeImportWorkbook } from '../../src/asset-imports/asset-import-profiles'; import type { ParsedWorkbook } from '../../src/asset-imports/asset-import-parser'; 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[]; } function workbook(rows: string[][]): ParsedWorkbook { return { kind: 'XLSX', sheets: [{ name: 'Inventario', rows }] }; } test('D5.3.6 protects reconciliation as an import-management operation', () => { assert.deepEqual(permissionFor('reconcile'), ['asset_imports.manage']); }); test('D5.3.6 normalizes technical family and subtype while preserving source classification', () => { const analysis = analyzeImportWorkbook(workbook([ ['Item','Area/ Yacimiento','Instalacion','Sub instalacion','Equipo','Denominación del equipo','Ubicación del equipo o instalación','N° ID/ Inventario','Cantidad','Especificaciones técnicas','Estado'], ['1','REFUGIO TUPUNGATO','TUP-PIAS','ZONA BOMBAS','Bomba Centrífuga','Clase: BBA; Tipo: CEN','N/A','BBA-050','1','Fabricante; GOULD; Modelo: 3196','NO'], ])); assert.equal(analysis.rows[0]?.normalized.sourceClass, 'BBA'); assert.equal(analysis.rows[0]?.normalized.sourceSubtype, 'CEN'); assert.equal(analysis.rows[0]?.normalized.normalizedFamily, 'bomba'); assert.equal(analysis.rows[0]?.normalized.normalizedSubtype, 'centrifuga'); assert.equal(analysis.rows[0]?.normalized.sourceStatus, 'NO'); }); test('D5.3.6 reconciliation is conservative and does not write Maestro assets', () => { const service = source('src/asset-imports/asset-imports.service.ts'); const reconcile = service.slice(service.indexOf('async reconcile'), service.indexOf('async cancel')); assert.match(reconcile, /INVENTORY_ID_OR_CODE_EXACT/); assert.match(reconcile, /AREA_FIELD_EXACT/); assert.match(reconcile, /MATCH_MULTIPLE_ASSETS/); assert.match(reconcile, /masterModified: false/); assert.doesNotMatch(reconcile, /INSERT INTO assets\s*\(/); assert.doesNotMatch(reconcile, /UPDATE assets\s+SET/); }); test('D5.3.6.3 audit summary is exposed as a plain record', () => { const source = readFileSync(join(process.cwd(), 'src/asset-imports/asset-imports.service.ts'), 'utf8'); assert.match(source, /afterData:\s*\{\s*\.\.\.summary\s*\}/); });