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 { safePlanItems, summarizePlanItems, type AssetImportPlanDraftItem } from '../../src/asset-imports/asset-import-plan'; import { AssetImportsController } from '../../src/asset-imports/asset-imports.controller'; 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 item(overrides: Partial = {}): AssetImportPlanDraftItem { return { entityKey: 'technical:safe', entityKind: 'TECHNICAL_ASSET', action: 'CREATE', status: 'PLANNED', assetTypeCode: 'tanque', displayName: 'TK-1', generatedCode: 'IMP-TANQUE-1', parentEntityKey: null, matchedAssetId: null, payload: {}, sourceRowNumbers: [2], reviewCodes: [], ...overrides, }; } test('D5.3.11 exposes the review inbox and privileged safe application', () => { assert.deepEqual(permissionFor('reviews'), ['asset_imports.read']); assert.deepEqual(permissionFor('applySafePlan'), ['asset_imports.apply']); }); test('D5.3.11 imports only branches independent from pending reviews', () => { const decision = item({ entityKey: 'field:ambiguous', entityKind: 'FIELD', action: 'REVIEW', status: 'REVIEW', assetTypeCode: 'yacimiento', reviewCodes: ['PLAN_TERRITORY_AMBIGUOUS'] }); const blockedParent = item({ entityKey: 'structure:blocked', entityKind: 'LOCAL_STRUCTURE', assetTypeCode: 'estructura_local', parentEntityKey: decision.entityKey }); const blockedChild = item({ entityKey: 'technical:blocked', parentEntityKey: blockedParent.entityKey }); const safeField = item({ entityKey: 'field:safe', entityKind: 'FIELD', assetTypeCode: 'yacimiento' }); const safeChild = item({ entityKey: 'technical:safe', parentEntityKey: safeField.entityKey }); const planItems = [decision, blockedParent, blockedChild, safeField, safeChild]; const safe = safePlanItems(planItems); assert.deepEqual(safe.map((candidate) => candidate.entityKey), ['field:safe', 'technical:safe']); const summary = summarizePlanItems(planItems); assert.equal(summary.safeCreateItems, 2); assert.equal(summary.blockedCreateItems, 2); }); test('D5.3.11 treats already-applied parents as valid support for later safe branches', () => { const parent = item({ entityKey: 'field:applied', entityKind: 'FIELD', assetTypeCode: 'yacimiento', status: 'APPLIED' }); const child = item({ entityKey: 'technical:next', parentEntityKey: parent.entityKey }); assert.deepEqual(safePlanItems([parent, child]).map((candidate) => candidate.entityKey), ['field:applied', 'technical:next']); const summary = summarizePlanItems([parent, child]); assert.equal(summary.appliedCreateItems, 1); assert.equal(summary.pendingCreateItems, 1); assert.equal(summary.safeCreateItems, 1); assert.equal(summary.blockedCreateItems, 0); }); test('D5.3.11 keeps partial application transactional and rollback cumulative across plan revisions', () => { const service = source('src/asset-imports/asset-imports.service.ts'); const partial = service.slice(service.indexOf('async applySafePlan'), service.indexOf('async applyPlan')); const rollback = service.slice(service.indexOf('async rollbackPlan'), service.indexOf('async upload')); assert.match(partial, /this\.dataSource\.transaction/); assert.match(partial, /safePlanItems\(allItems\)/); assert.match(partial, /status: 'PARTIAL'/); assert.match(partial, /masterStateHash: nextMasterStateHash/); assert.match(rollback, /JOIN asset_import_plans history ON history\.id=item\.plan_id/); assert.match(rollback, /history\.batch_id=\$1 AND item\.action='CREATE' AND item\.status='APPLIED'/); assert.match(rollback, /UPDATE asset_import_rows SET imported_asset_id=NULL/); }); test('D5.3.11 prevents normal reconciliation from bypassing an existing partial import', () => { const service = source('src/asset-imports/asset-imports.service.ts'); assert.match(service, /ASSET_IMPORT_PARTIAL_RECONCILE_BLOCKED/); assert.match(service, /batchHasAppliedItems\(manager, id\)/); assert.match(service, /item\.status !== 'APPLIED'/); });