import assert from 'node:assert/strict'; import { execFileSync } from 'node:child_process'; import path from 'node:path'; import test from 'node:test'; const moduleRoot = path.resolve(import.meta.dirname, '..'); const analyzer = path.join(moduleRoot, 'scripts', 'analyze-firstleg-dependencies.mjs'); const contract = path.join(moduleRoot, 'src', 'test', 'resources', 'architecture', 'firstleg-slice-dependencies.json'); function analyze() { return JSON.parse(execFileSync(process.execPath, [analyzer, '--source-set', 'main', '--format', 'json'], { cwd: moduleRoot, encoding: 'utf8', })); } test('current source does not add slice dependencies beyond the baseline', () => { execFileSync(process.execPath, [analyzer, '--source-set', 'main', '--format', 'dot', '--check', contract], { cwd: moduleRoot, encoding: 'utf8', }); }); test('customer statistics remains an independently packaged low-coupling slice', () => { const report = analyze(); const classes = report.classes.filter(entry => entry.slice === 'customer-statistics'); assert.deepEqual(classes.map(entry => entry.fqcn).sort(), [ 'com.baiyue.firstleg.customerstatistics.adapter.in.web.PlatformCustomerTransitStatisticsController', 'com.baiyue.firstleg.customerstatistics.adapter.out.persistence.CustomerTransitStatisticsMapper', 'com.baiyue.firstleg.customerstatistics.application.CustomerTransitStatisticsService', 'com.baiyue.firstleg.customerstatistics.application.model.CustomerTransitStatisticsDtos', 'com.baiyue.firstleg.customerstatistics.application.port.out.CustomerTransitStatisticsReader', ]); const outgoing = report.sliceEdges.filter(edge => edge.source === 'customer-statistics'); const incoming = report.sliceEdges.filter(edge => edge.target === 'customer-statistics'); assert.deepEqual(outgoing, [{ source: 'customer-statistics', target: 'shared-technical', count: 1 }]); assert.deepEqual(incoming, []); assert.equal(report.sliceSccs.some(component => component.includes('customer-statistics')), false); }); test('customer statistics application layer only depends inward', () => { const report = analyze(); const prefix = 'com.baiyue.firstleg.customerstatistics.application'; const applicationClasses = report.classes.filter(entry => entry.packageName.startsWith(prefix)); assert.equal(applicationClasses.length, 3); for (const entry of applicationClasses) { assert.equal(entry.imports.some(imported => imported.startsWith('org.springframework.web')), false); assert.equal(entry.imports.some(imported => imported.startsWith('org.apache.ibatis')), false); assert.equal(entry.imports.some(imported => imported.startsWith('com.baomidou.mybatisplus')), false); } const outwardDependencies = report.classDependencies.filter(edge => edge.source.startsWith(prefix) && edge.target.startsWith('com.baiyue.firstleg.') && !edge.target.startsWith(prefix)); assert.deepEqual(outwardDependencies, []); }); test('price book query is a leaf slice and removes the package cycle', () => { const report = analyze(); const classes = report.classes.filter(entry => entry.slice === 'pricebook-query'); assert.deepEqual(classes.map(entry => entry.fqcn).sort(), [ 'com.baiyue.firstleg.pricebookquery.adapter.out.persistence.PriceBookQueryMapper', 'com.baiyue.firstleg.pricebookquery.application.PriceBookQueryService', 'com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows', 'com.baiyue.firstleg.pricebookquery.application.port.in.PriceBookQueryUseCase', 'com.baiyue.firstleg.pricebookquery.application.port.out.PriceBookQueryReader', ]); assert.deepEqual(report.sliceEdges.filter(edge => edge.source === 'pricebook-query'), []); assert.deepEqual(report.sliceEdges.filter(edge => edge.target === 'pricebook-query') .map(edge => edge.source).sort(), ['booking-customs-trucking', 'measurement-pricing', 'split-loading']); assert.equal(report.sliceSccs.some(component => component.includes('pricebook-query')), false); assert.deepEqual(report.packageSccs, []); }); test('price book query application and consumers respect ports', () => { const report = analyze(); const prefix = 'com.baiyue.firstleg.pricebookquery.application'; const applicationClasses = report.classes.filter(entry => entry.packageName.startsWith(prefix)); assert.equal(applicationClasses.length, 4); for (const entry of applicationClasses) { assert.equal(entry.imports.some(imported => imported.startsWith('org.springframework.web')), false); assert.equal(entry.imports.some(imported => imported.startsWith('org.apache.ibatis')), false); assert.equal(entry.imports.some(imported => imported.startsWith('com.baomidou.mybatisplus')), false); } const outwardDependencies = report.classDependencies.filter(edge => edge.source.startsWith(prefix) && edge.target.startsWith('com.baiyue.firstleg.') && !edge.target.startsWith(prefix)); assert.deepEqual(outwardDependencies, []); const allowedTargets = [ 'com.baiyue.firstleg.pricebookquery.application.model.', 'com.baiyue.firstleg.pricebookquery.application.port.in.', ]; const implementationLeaks = report.classDependencies.filter(edge => !edge.source.startsWith('com.baiyue.firstleg.pricebookquery.') && edge.target.startsWith('com.baiyue.firstleg.pricebookquery.') && !allowedTargets.some(prefixValue => edge.target.startsWith(prefixValue))); assert.deepEqual(implementationLeaks, []); });