import assert from 'node:assert/strict'; import test from 'node:test'; import { readFileSync } from 'node:fs'; import { resolve } from 'node:path'; const read = (path: string) => readFileSync(resolve(process.cwd(), path), 'utf8'); const migration = read('src/database/migrations/1788976800000-phase-d5-4-contextual-finding-catalog.ts'); const catalogService = read('src/inspection-findings/finding-catalog.service.ts'); const catalogController = read('src/inspection-findings/finding-catalog.controller.ts'); const findingsService = read('src/inspection-findings/inspection-findings.service.ts'); const createFindingDto = read('src/inspection-findings/dto/create-inspection-finding.dto.ts'); const catalogItemDto = read('src/inspection-findings/dto/create-finding-catalog-item.dto.ts'); const closingService = read('src/inspection-closing/inspection-closing.service.ts'); const wordBuilder = read('src/inspection-reports/inspection-report-word-builder.ts'); test('D5.4 models type applicability and concrete Inventory exceptions separately', () => { assert.match(migration, /CREATE TABLE finding_catalog_asset_type_profiles/); assert.match(migration, /CREATE TABLE finding_catalog_item_asset_types/); assert.match(migration, /CREATE TABLE finding_catalog_asset_overrides/); assert.match(catalogService, /replaceAssetTypeSelection/); assert.match(catalogService, /replaceAssetSelection/); assert.match(catalogService, /typeDefaultEnabled/); }); test('D5.4 exposes only applicable catalog items and enforces the same rule on field creation', () => { assert.match(catalogController, /@Get\('applicable\/:assetId'\)/); assert.match(catalogService, /listApplicableForAsset/); assert.match(findingsService, /FINDING_CATALOG_ITEM_NOT_APPLICABLE/); assert.match(findingsService, /finding_catalog_asset_type_profiles profile/); assert.match(findingsService, /finding_catalog_asset_overrides override/); assert.match(findingsService, /await this\.requireCatalogItem\(manager, finding\.catalogItemId, dto\.assetId\)/); }); test('D5.4 stores suggested and actual severity independently on the 1 to 10 scale', () => { assert.match(migration, /suggested_severity smallint/); assert.match(migration, /severity smallint/); assert.match(migration, /BETWEEN 1 AND 10/); assert.match(catalogItemDto, /@Min\(1\)/); assert.match(catalogItemDto, /@Max\(10\)/); assert.match(createFindingDto, /severity\?: number/); assert.match(findingsService, /suggestedSeverity: catalog\?\.suggestedSeverity/); assert.match(findingsService, /severity: dto\.severity \?\? catalog\?\.suggestedSeverity/); assert.match(closingService, /finding\.suggested_severity AS "suggestedSeverity"/); assert.match(closingService, /finding\.severity/); assert.match(wordBuilder, /'Gravedad'/); }); test('D5.4 turns OTHER into an office proposal without rewriting the original finding', () => { assert.match(migration, /CREATE TABLE finding_catalog_proposals/); assert.match(findingsService, /INSERT INTO finding_catalog_proposals/); assert.match(catalogService, /FINDING_CATALOG_PROPOSAL_ALREADY_REVIEWED/); assert.match(catalogService, /status = 'MATCHED'/); assert.match(catalogService, /status = 'REJECTED'/); assert.doesNotMatch(catalogService, /UPDATE inspection_findings SET catalog_item_id/); }); test('D5.4 keeps contextual catalog administration in office permissions and audited routes', () => { assert.match(catalogController, /@Put\('asset-types\/:assetTypeId\/selection'\)/); assert.match(catalogController, /@Put\('assets\/:assetId\/selection'\)/); assert.match(catalogController, /@Get\('proposals'\)/); assert.match(catalogController, /finding_catalog\.manage/); assert.match(catalogService, /FINDING_CATALOG_TYPE_APPLICABILITY_UPDATED/); assert.match(catalogService, /FINDING_CATALOG_ASSET_SELECTION_UPDATED/); assert.match(catalogService, /FINDING_CATALOG_PROPOSAL_MATCHED/); });