72 lines
4.4 KiB
TypeScript
72 lines
4.4 KiB
TypeScript
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> = {}): 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'/);
|
|
});
|