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'; const ownedBusinessReadEdges = [ ['model.OwnedBusinessModels', 'DispatchViews'], ['service.OwnedBusinessProjection', 'DispatchContracts'], ['service.OwnedBusinessQueryService', 'DispatchContracts'], ['service.OwnedBusinessQueryService', 'DispatchTypes'], ['service.OwnedBusinessQueryService', 'DispatchDetailReader'], ].map(([source, target]) => `com.baiyue.lastleg.fleet.ownedbusiness.${source}->com.baiyue.lastleg.dispatch.port.${target}`).sort(); const outsourcedBusinessReadEdges = ['com.baiyue.lastleg.fleet.outsourcedbusiness.model.OutsourcedBusinessModels->com.baiyue.lastleg.dispatch.port.DispatchViews']; 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.'))); const reviewed = JSON.parse(readFileSync(contract, 'utf8')).reviewedClassDependencies; const commercialEdges = report.classDependencies.filter(edge => edge.source.startsWith('com.baiyue.lastleg.fleet.') && /com\.baiyue\.lastleg\.(trucking|looseboard|procurement)\./.test(edge.target)); assert.ok(commercialEdges.every(edge => reviewed.some(allowed => allowed.source === edge.source && allowed.target === edge.target)), 'commercial integration only uses individually reviewed class references'); assert.deepEqual(report.sliceEdges.filter(edge => edge.source === 'fleet').map(edge => edge.target).sort(), ['dispatch-delivery', 'overseas-master-data', ...(commercialEdges.length ? ['procurement-trucking-looseboard'] : []), '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.deepEqual(report.classDependencies.filter(edge => edge.source.startsWith('com.baiyue.lastleg.fleet.') && /com\.baiyue\.lastleg\.(dispatch|delivery)\./.test(edge.target)).map(edge => `${edge.source}->${edge.target}`).sort(), [...ownedBusinessReadEdges, ...outsourcedBusinessReadEdges].sort()); 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' || edge.source === 'com.baiyue.lastleg.dispatch.service.DispatchAggregateService' && edge.target === 'com.baiyue.lastleg.fleet.ownedbusiness.port.OwnedBusinessOwnershipCapture' || ['com.baiyue.lastleg.dispatch.service.DispatchLoadingDocumentService', 'com.baiyue.lastleg.dispatch.service.DispatchLoadingDepartureService', 'com.baiyue.lastleg.dispatch.controller.DispatchLoadingDocumentController', 'com.baiyue.lastleg.dispatch.pda.controller.PdaDispatchLoadingDocumentController'].includes(edge.source) && edge.target === 'com.baiyue.lastleg.fleet.ownedbusiness.port.DispatchLoadingPhotoPort' || [ 'TransportTaskModels->pickup.FleetPickupModels', 'TransportTaskModels->trip.FleetTripModels', 'TransportTaskProjection->trip.FleetTripEntity', 'TransportTaskProjection->trip.TransportTripChanged', 'TransportTaskService->business.FleetPoolOrderQueries', 'TransportTaskService->driver.DriverService', 'TransportTaskService->reporting.TripOperationsEvidenceGateway', // 取货照片沿既有运输边界读取车次身份/时间,并复用司机资格与证照授权;不开放 Mapper。 'TransportReviewPhotos->driver.DriverService', 'TransportReviewPhotos->trip.FleetTripEntity', ].some(reference => `${edge.source}->${edge.target}` === `com.baiyue.lastleg.dispatch.transport.${reference.replace('->', '->com.baiyue.lastleg.fleet.')}`)), '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'); }); test('owned business reads only reviewed dispatch ports and cannot inherit persistence access', () => { const report = analyze(); const baseline = JSON.parse(readFileSync(contract, 'utf8')); const prefix = 'com.baiyue.lastleg.fleet.ownedbusiness.'; 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(), ownedBusinessReadEdges); assert.equal(baseline.allowedSliceEdges.includes('fleet->dispatch-delivery'), false); for (const target of ['entity.DispatchAggregate', 'mapper.DispatchAggregateMapper', 'service.DispatchAggregateService']) { const edge = { source: prefix + 'service.OwnedBusinessQueryService', target: 'com.baiyue.lastleg.dispatch.' + target }; report.classDependencies.push(edge); assert.ok(contractViolations(report, baseline).some(message => message.includes(`${edge.source}->${edge.target}`))); } const unreviewed = { source: 'com.baiyue.lastleg.fleet.trip.FleetTripService', target: 'com.baiyue.lastleg.dispatch.port.DispatchDetailReader' }; report.classDependencies.push(unreviewed); assert.ok(contractViolations(report, baseline).some(message => message.includes(`${unreviewed.source}->${unreviewed.target}`))); }); test('outsourced business reads only its reviewed original dispatch view and cannot reach persistence', () => { const report = analyze(); const baseline = JSON.parse(readFileSync(contract, 'utf8')); const prefix = 'com.baiyue.lastleg.fleet.outsourcedbusiness.'; 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(), outsourcedBusinessReadEdges); assert.equal(baseline.allowedSliceEdges.includes('fleet->dispatch-delivery'), false); for (const target of ['entity.DispatchAggregate', 'mapper.DispatchAggregateMapper', 'service.DispatchAggregateService']) { const edge = { source: prefix + 'service.OutsourcedBusinessService', target: 'com.baiyue.lastleg.dispatch.' + target }; report.classDependencies.push(edge); assert.ok(contractViolations(report, baseline).some(message => message.includes(`${edge.source}->${edge.target}`))); } }); 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'), /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:'))); });