237 lines
15 KiB
JavaScript
237 lines
15 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { execFileSync } from 'node:child_process';
|
|
import { readFileSync } from 'node:fs';
|
|
import path from 'node:path';
|
|
import test from 'node:test';
|
|
import { contractViolations } from './analyze-lastleg-dependencies.mjs';
|
|
|
|
const moduleRoot = path.resolve(import.meta.dirname, '..');
|
|
const analyzer = path.join(moduleRoot, 'scripts', 'analyze-lastleg-dependencies.mjs');
|
|
const contract = path.join(moduleRoot, 'src', 'test', 'resources', 'architecture', 'lastleg-slice-dependencies.json');
|
|
const vehiclePrefix = 'com.baiyue.lastleg.masterdata.vehicle';
|
|
|
|
test('selfoperated analytics is independent of customer outbound and inventory slices', () => {
|
|
const report = analyze();
|
|
const prefix = 'com.baiyue.lastleg.selfoperated.analytics.';
|
|
const classes = report.classes.filter(entry => entry.fqcn.startsWith(prefix));
|
|
assert.ok(classes.length > 0);
|
|
assert.ok(classes.some(entry => entry.packageName.endsWith('.inventory')));
|
|
assert.ok(classes.some(entry => entry.packageName.endsWith('.outbound')));
|
|
assert.ok(classes.every(entry => entry.slice === 'selfoperated-analytics'));
|
|
assert.deepEqual(report.sliceEdges.filter(edge => edge.source === 'selfoperated-analytics'
|
|
|| edge.target === 'selfoperated-analytics'), [{ source: 'selfoperated-analytics', target: 'shared-technical', count: 2 }, { source: 'shared-technical', target: 'selfoperated-analytics', count: 1 }]);
|
|
assert.deepEqual(report.classDependencies.filter(edge => edge.source.startsWith(prefix) && !edge.target.startsWith(prefix)), [{
|
|
source: prefix + 'diagnostic.QuantityDiagnosticController', target: 'com.baiyue.lastleg.security.InternalServiceTokenGuard',
|
|
}, {
|
|
source: prefix + 'request.SelfopEgressController', target: 'com.baiyue.lastleg.security.InternalServiceTokenGuard',
|
|
}]);
|
|
assert.deepEqual(report.packageSccs.filter(cycle => cycle.some(pkg => pkg.startsWith(prefix))), []);
|
|
const baseline = JSON.parse(readFileSync(contract, 'utf8'));
|
|
const source = classes.find(entry => entry.packageName.endsWith('.inventory')).fqcn;
|
|
const target = 'com.baiyue.lastleg.inventory.query.service.InventoryQueryService';
|
|
report.classDependencies.push({ source, target });
|
|
assert.ok(contractViolations(report, baseline).some(message => message.includes(`${source}->${target}`)));
|
|
});
|
|
|
|
test('fleet stays a business slice and its policies have no infrastructure dependencies', () => {
|
|
const report = analyze();
|
|
const classes = report.classes.filter(entry => entry.fqcn.startsWith('com.baiyue.lastleg.fleet.'));
|
|
assert.ok(classes.some(entry => entry.fqcn.endsWith('.DriverService')));
|
|
assert.ok(classes.every(entry => entry.slice === 'fleet'));
|
|
assert.equal(report.classes.find(entry => entry.fqcn === 'com.baiyue.lastleg.dispatch.fleet.DispatchFleetAdapter').slice, 'dispatch-delivery');
|
|
const policyClasses = classes.filter(entry => entry.packageName === 'com.baiyue.lastleg.fleet.domain');
|
|
assert.equal(policyClasses.length, 3);
|
|
for (const entry of policyClasses) assert.ok(entry.imports.every(imported => imported.startsWith('java.')));
|
|
assert.deepEqual(report.sliceEdges.filter(edge => edge.source === 'fleet').map(edge => edge.target).sort(), ['overseas-master-data', 'shared-technical']);
|
|
for (const edge of report.classDependencies.filter(edge => edge.source.startsWith('com.baiyue.lastleg.fleet.') && edge.target.startsWith(vehiclePrefix))) {
|
|
assert.ok(edge.target.startsWith(vehiclePrefix + '.application.'), 'fleet must use vehicle application ports, not persistence adapters');
|
|
}
|
|
assert.equal(report.packageSccs.some(component => component.some(name => name.startsWith('com.baiyue.lastleg.fleet.'))), false);
|
|
assert.equal(report.classDependencies.some(edge => edge.source.startsWith('com.baiyue.lastleg.fleet.')
|
|
&& /com\.baiyue\.lastleg\.(dispatch|delivery)\./.test(edge.target)), false);
|
|
const dispatchEdges = report.classDependencies.filter(edge => edge.source.startsWith('com.baiyue.lastleg.dispatch.')
|
|
&& edge.target.startsWith('com.baiyue.lastleg.fleet.'));
|
|
assert.ok(dispatchEdges.length > 0);
|
|
assert.ok(dispatchEdges.every(edge => /\.(FleetDispatchGateway|FleetDispatchBindingGuard|FleetTripExecutionService|FleetExecutionModels|FleetCompletionModels)$/.test(edge.target)
|
|
|| edge.source === 'com.baiyue.lastleg.dispatch.fleet.FleetDispatchPlanningAdapter'
|
|
&& /\.(FleetBookingIncomingGateway|FleetDispatchPlanningGateway)$/.test(edge.target)
|
|
|| edge.source === 'com.baiyue.lastleg.dispatch.fleet.FleetDispatchPlanningMapper'
|
|
&& edge.target === 'com.baiyue.lastleg.fleet.trip.FleetDispatchPlanningGateway'
|
|
|| edge.source === 'com.baiyue.lastleg.dispatch.fleet.PdaFleetHandoverController'
|
|
&& edge.target === 'com.baiyue.lastleg.fleet.exception.FleetHandoverPdaQuery'),
|
|
'dispatch must use fleet ports and guards, not its persistence mappers');
|
|
const incomingEdges = report.classDependencies.filter(edge => edge.source.startsWith('com.baiyue.lastleg.transfer.') && edge.target.startsWith('com.baiyue.lastleg.fleet.'));
|
|
assert.ok(incomingEdges.length > 0);
|
|
assert.ok(incomingEdges.every(edge => edge.source === 'com.baiyue.lastleg.transfer.fleet.FleetIncomingAdapter' && /\.(FleetBookingIncomingGateway|FleetDispatchGateway)$/.test(edge.target)), 'incoming adapter uses fleet ports only');
|
|
});
|
|
|
|
function analyze(sourceSet = 'main') {
|
|
return JSON.parse(execFileSync(process.execPath, [analyzer, '--source-set', sourceSet, '--format', 'json'], {
|
|
cwd: moduleRoot,
|
|
encoding: 'utf8',
|
|
maxBuffer: 8 * 1024 * 1024,
|
|
}));
|
|
}
|
|
|
|
test('original appointment adapters use only the reviewed dispatch port types', () => {
|
|
const report = analyze();
|
|
const baseline = JSON.parse(readFileSync(contract, 'utf8'));
|
|
const prefix = 'com.baiyue.lastleg.inboundplan.work.';
|
|
const port = 'com.baiyue.lastleg.dispatch.port.';
|
|
const expected = [
|
|
['FbaAppointmentExecutionAdapter', 'DispatchAppointmentAmendmentPort'],
|
|
['FbaAppointmentExecutionAdapter', 'DispatchContracts'],
|
|
['FbaAppointmentExecutionAdapter', 'DispatchTypes'],
|
|
['FbaAppointmentTransportAdapter', 'DispatchAppointmentAmendmentPort'],
|
|
['FbaAppointmentTransportAdapter', 'DispatchCommands'],
|
|
['FbaAppointmentTransportAdapter', 'DispatchContracts'],
|
|
['FbaAppointmentTransportAdapter', 'DispatchTypes'],
|
|
].map(([source, target]) => `${prefix}${source}->${port}${target}`).sort();
|
|
const reviewed = baseline.reviewedClassDependencies.filter(edge => edge.source.startsWith(prefix)
|
|
&& edge.target.startsWith('com.baiyue.lastleg.dispatch.'));
|
|
assert.deepEqual(reviewed.map(edge => `${edge.source}->${edge.target}`).sort(), expected);
|
|
assert.equal(baseline.allowedSliceEdges.includes('inbound-pda->dispatch-delivery'), false);
|
|
const violations = contractViolations(report, baseline);
|
|
for (const edge of expected) assert.equal(violations.some(message => message.includes(edge)), false);
|
|
|
|
// 精确端口边不允许原适配器直连整车实现,也不授权其他预约消费者借用此许可。
|
|
const forbidden = [
|
|
{ source: prefix + 'FbaAppointmentExecutionAdapter', target: 'com.baiyue.lastleg.dispatch.service.DispatchAggregateService' },
|
|
{ source: prefix + 'AppointmentWorkDispatchBinding', target: port + 'DispatchAppointmentAmendmentPort' },
|
|
];
|
|
for (const edge of forbidden) {
|
|
assert.ok(report.classes.some(entry => entry.fqcn === edge.source));
|
|
assert.ok(report.classes.some(entry => entry.fqcn === edge.target));
|
|
report.classDependencies.push(edge);
|
|
assert.ok(contractViolations(report, baseline).some(message => message.includes(`${edge.source}->${edge.target}`)));
|
|
}
|
|
});
|
|
|
|
test('current source does not add slice dependencies beyond the Lastleg baseline', () => {
|
|
execFileSync(process.execPath, [analyzer, '--source-set', 'main', '--format', 'dot', '--check', contract], {
|
|
cwd: moduleRoot,
|
|
encoding: 'utf8',
|
|
});
|
|
});
|
|
|
|
test('main, test, and combined source sets expose package dependency graphs', () => {
|
|
for (const sourceSet of ['main', 'test', 'all']) {
|
|
const dot = execFileSync(process.execPath, [analyzer, '--source-set', sourceSet, '--format', 'dot', '--graph', 'package'], {
|
|
cwd: moduleRoot,
|
|
encoding: 'utf8',
|
|
});
|
|
assert.match(dot, /^digraph lastleg_packages \{/);
|
|
assert.match(dot, /com\.baiyue\.lastleg/);
|
|
}
|
|
});
|
|
|
|
test('vehicle master data remains the reviewed low-coupling first-slice candidate', () => {
|
|
const report = analyze();
|
|
const classes = report.classes.filter(entry => entry.fqcn.startsWith(vehiclePrefix));
|
|
assert.deepEqual(classes.map(entry => entry.fqcn).sort(), [
|
|
'com.baiyue.lastleg.masterdata.vehicle.adapter.in.web.PlatformVehicleController',
|
|
'com.baiyue.lastleg.masterdata.vehicle.adapter.out.persistence.VehicleMapper',
|
|
'com.baiyue.lastleg.masterdata.vehicle.adapter.out.persistence.VehiclePersistenceAdapter',
|
|
'com.baiyue.lastleg.masterdata.vehicle.adapter.out.persistence.VehicleRecord',
|
|
'com.baiyue.lastleg.masterdata.vehicle.application.VehicleApplicationService',
|
|
'com.baiyue.lastleg.masterdata.vehicle.application.model.VehicleModels',
|
|
'com.baiyue.lastleg.masterdata.vehicle.application.port.out.VehicleAssignmentGuard',
|
|
'com.baiyue.lastleg.masterdata.vehicle.application.port.out.VehicleRepository',
|
|
]);
|
|
const outgoing = report.classDependencies.filter(edge => edge.source.startsWith(vehiclePrefix)
|
|
&& !edge.target.startsWith(vehiclePrefix));
|
|
const incoming = report.classDependencies.filter(edge => edge.target.startsWith(vehiclePrefix)
|
|
&& !edge.source.startsWith(vehiclePrefix));
|
|
assert.deepEqual(outgoing, [{
|
|
source: 'com.baiyue.lastleg.masterdata.vehicle.adapter.in.web.PlatformVehicleController',
|
|
target: 'com.baiyue.lastleg.security.OverseasPermissions',
|
|
}]);
|
|
assert.ok(incoming.length > 0);
|
|
assert.ok(incoming.every(edge => edge.source.startsWith('com.baiyue.lastleg.fleet.') && edge.target.startsWith(vehiclePrefix + '.application.')
|
|
|| edge.source === 'com.baiyue.lastleg.export.LastlegExportProviderService'
|
|
&& [vehiclePrefix + '.application.VehicleApplicationService', vehiclePrefix + '.application.model.VehicleModels'].includes(edge.target)),
|
|
'vehicle consumers use application contracts; the export composition has two exact read-only references');
|
|
assert.equal(report.packageSccs.some(component => component.some(name => name.startsWith(vehiclePrefix))), false);
|
|
assert.equal(classes.some(entry => entry.kind === 'cross-service-adapter'), false);
|
|
assert.equal(classes.some(entry => entry.imports.some(imported => /RestClient|\.client\./.test(imported))), false);
|
|
});
|
|
|
|
test('vehicle application only depends inward and stays free of adapter frameworks', () => {
|
|
const report = analyze();
|
|
const prefix = `${vehiclePrefix}.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);
|
|
assert.equal(entry.imports.some(imported => /RestClient|\.client\./.test(imported)), false);
|
|
}
|
|
const outwardDependencies = report.classDependencies.filter(edge => edge.source.startsWith(prefix)
|
|
&& edge.target.startsWith('com.baiyue.lastleg.') && !edge.target.startsWith(prefix));
|
|
assert.deepEqual(outwardDependencies, []);
|
|
});
|
|
|
|
test('retired Lastleg security projection stays absent under the unified Auth contract', () => {
|
|
const report = analyze();
|
|
const projectionClasses = report.classes.filter(entry => entry.slice === 'security-projection');
|
|
assert.deepEqual(projectionClasses, []);
|
|
assert.deepEqual(report.sliceEdges.filter(edge => edge.source === 'security-projection'), []);
|
|
assert.deepEqual(report.sliceEdges.filter(edge => edge.target === 'security-projection'), []);
|
|
assert.match(readFileSync(path.join(moduleRoot, 'pom.xml'), 'utf8'), /<artifactId>baiyue-common-security<\/artifactId>/);
|
|
});
|
|
|
|
test('reviewed references do not allow another consumer into the same slice', () => {
|
|
const report = analyze();
|
|
const baseline = JSON.parse(readFileSync(contract, 'utf8'));
|
|
assert.deepEqual(contractViolations(report, baseline), []);
|
|
const source = 'com.baiyue.lastleg.controller.LastlegOrderController';
|
|
const target = 'com.baiyue.lastleg.masterdata.exchange.mapper.MonthlyExchangeRateMapper';
|
|
report.classDependencies.push({ source, target });
|
|
assert.ok(contractViolations(report, baseline).some(message => message.includes(`${source}->${target}`)));
|
|
});
|
|
|
|
test('export composition approvals do not grant persistence access or new consumers', () => {
|
|
const baseline = JSON.parse(readFileSync(contract, 'utf8'));
|
|
const source = 'com.baiyue.lastleg.export.LastlegExportProviderService';
|
|
const cases = [
|
|
{ source, target: vehiclePrefix + '.adapter.out.persistence.VehicleMapper' },
|
|
{ source, target: 'com.baiyue.lastleg.masterdata.exchange.mapper.MonthlyExchangeRateMapper' },
|
|
{ source: 'com.baiyue.lastleg.export.LastlegExportColumns', target: 'com.baiyue.lastleg.fleet.quote.FleetQuoteService' },
|
|
];
|
|
for (const edge of cases) {
|
|
const report = analyze();
|
|
report.classDependencies.push(edge);
|
|
assert.ok(contractViolations(report, baseline).some(message => message.includes(`${edge.source}->${edge.target}`)),
|
|
'each unreviewed consumer/target pair must still fail');
|
|
}
|
|
});
|
|
|
|
test('restoring a retired projection or importing its missing type still fails', () => {
|
|
const baseline = JSON.parse(readFileSync(contract, 'utf8'));
|
|
const report = analyze();
|
|
const service = report.classes.find(entry => entry.fqcn.endsWith('.FleetTripService'));
|
|
service.imports.push('com.baiyue.lastleg.security.service.LastlegSecurityValidationService');
|
|
assert.ok(contractViolations(report, baseline).some(message => message.includes('retired security type referenced')));
|
|
service.imports.pop();
|
|
report.classes.push({ ...service, fqcn: 'com.baiyue.lastleg.security.service.LastlegSecurityValidationService', slice: 'security-projection' });
|
|
assert.ok(contractViolations(report, baseline).some(message => message.includes('retired slice returned')));
|
|
});
|
|
|
|
test('reviewed references cannot bypass the vehicle application boundary or use wildcard grants', () => {
|
|
const report = analyze(), baseline = JSON.parse(readFileSync(contract, 'utf8'));
|
|
const source = `${vehiclePrefix}.application.VehicleApplicationService`, target = `${vehiclePrefix}.adapter.out.persistence.VehicleMapper`;
|
|
report.classDependencies.push({ source, target });
|
|
baseline.reviewedClassDependencies.push({ source, target, reason: 'invalid attempted waiver' });
|
|
assert.ok(contractViolations(report, baseline).some(message => message.includes('forbidden application dependency')));
|
|
baseline.reviewedClassDependencies.push({ source: 'com.baiyue.lastleg.*', target, reason: 'invalid wildcard' });
|
|
assert.ok(contractViolations(report, baseline).some(message => message.includes('requires exact classes')));
|
|
});
|
|
|
|
test('package cycles are rejected even when every slice edge is otherwise allowed', () => {
|
|
const report = analyze(), baseline = JSON.parse(readFileSync(contract, 'utf8'));
|
|
assert.deepEqual(report.packageSccs, []);
|
|
report.packageSccs.push(['com.baiyue.lastleg.delivery.dto', 'com.baiyue.lastleg.delivery.model']);
|
|
assert.ok(contractViolations(report, baseline).some(message => message.startsWith('package cycle:')));
|
|
});
|