import assert from 'node:assert/strict' import { access, readFile } from 'node:fs/promises' import test from 'node:test' const read = async (path) => (await readFile(new URL(`../${path}`, import.meta.url), 'utf8')) .replaceAll('\r\n', '\n') const sorted = (values) => [...values].sort() const parseRootModules = (xml) => { const body = xml.match(/([\s\S]*?)<\/modules>/)?.[1] ?? '' return [...body.matchAll(/([^<]+)<\/module>/g)].map((match) => match[1].trim()) } const parsePom = (xml) => { const project = xml.replace(/[\s\S]*?<\/parent>/, '') return { artifactId: project.match(/([^<]+)<\/artifactId>/)?.[1]?.trim(), springBoot: /spring-boot-maven-plugin<\/artifactId>/.test(project) } } const parseServiceMap = (yaml) => { const services = new Map() let current for (const line of yaml.split('\n')) { const service = line.match(/^ ([a-z][a-z0-9-]+):$/) if (service) { current = {} services.set(service[1], current) continue } const property = line.match(/^ ([a-zA-Z]+): (.+)$/) if (!current || !property) continue const [, key, raw] = property current[key] = key === 'port' ? Number(raw) : key === 'regions' ? raw.slice(1, -1).split(',').map((value) => value.trim()) : raw } return services } const parseGroovyList = (source, variable) => { const body = source.match(new RegExp(`def ${variable} = \\[([\\s\\S]*?)\\n\\]`))?.[1] ?? '' return [...body.matchAll(/'([^']+)'/g)].map((match) => match[1]) } const parseGroovyMap = (source, variable) => { const body = source.match(new RegExp(`def ${variable} = \\[([\\s\\S]*?)\\n\\]`))?.[1] ?? '' return new Map([...body.matchAll(/'([^']+)':\s*'([^']+)'/g)] .map((match) => [match[1], match[2]])) } const parseCompose = (yaml) => { const body = yaml.match(/^services:\n([\s\S]*?)(?=^networks:)/m)?.[1] ?? '' const matches = [...body.matchAll(/^ ([a-z][a-z0-9-]+):\s*$/gm)] return new Map(matches.map((match, index) => { const block = body.slice(match.index, matches[index + 1]?.index ?? body.length) return [match[1], Number(block.match(/^ SERVER_PORT: (\d+)$/m)?.[1])] })) } test('root POM freezes 20 modules and service-map freezes 13 executable services', async () => { const modules = parseRootModules(await read('pom.xml')) const services = parseServiceMap(await read('deploy/service-map.yml')) assert.equal(modules.length, 20) assert.equal(new Set(modules).size, 20) assert.equal(services.size, 13) const bootModules = [] for (const module of modules) { const pom = parsePom(await read(`${module}/pom.xml`)) if (pom.springBoot) bootModules.push(module) if (!services.has(module)) continue const service = services.get(module) assert.equal(service.artifactId, pom.artifactId, `${module} artifactId drifted`) assert.equal(service.jar, `${module}/target/${pom.artifactId}-1.0.0-SNAPSHOT.jar`) } assert.deepEqual(sorted(bootModules), sorted(services.keys())) assert.equal(new Set([...services.values()].map(({ port }) => port)).size, services.size) }) test('Jenkins and GitLab package exactly the service-map modules', async () => { const services = parseServiceMap(await read('deploy/service-map.yml')) const expected = sorted(services.keys()) const coordinator = await read('Jenkinsfile') const servicePipeline = await read('Jenkinsfile.service') const serviceJars = parseGroovyMap(servicePipeline, 'serviceJars') const gitlab = await read('.gitlab-ci.yml') const gitlabArtifacts = [...gitlab.matchAll(/^ - ([a-z][a-z0-9-]+)\/target\/\*\.jar$/gm)] .map((match) => match[1]) assert.deepEqual(sorted(parseGroovyList(coordinator, 'serviceModules')), expected) assert.deepEqual(sorted(parseGroovyList(servicePipeline, 'serviceModules')), expected) assert.deepEqual(sorted(serviceJars.keys()), expected) assert.deepEqual(sorted(gitlabArtifacts), expected) for (const [module, service] of services) assert.equal(serviceJars.get(module), service.jar) }) test('GitLab runs the topology contract in a dedicated Node job', async () => { const gitlab = await read('.gitlab-ci.yml') const topologyJob = gitlab.match(/^Java拓扑合同:\n([\s\S]*?)(?=^[^\s#].*:\n)/m)?.[1] ?? '' const packageJob = gitlab.match(/^打包服务:\n([\s\S]*)/m)?.[1] ?? '' assert.match(topologyJob, /^ stage: verify$/m) assert.match(topologyJob, /^ image: node:[^\s]+$/m) assert.match(topologyJob, /^ - node --test scripts\/java-topology-contract\.test\.mjs$/m) assert.match(packageJob, /^ - Java拓扑合同$/m) }) test('GitLab validates every static OpenAPI document before packaging', async () => { const gitlab = await read('.gitlab-ci.yml') const openapiJob = gitlab.match(/^OpenAPI有效性:\n([\s\S]*?)(?=^[^\s#].*:\n)/m)?.[1] ?? '' const packageJob = gitlab.match(/^打包服务:\n([\s\S]*)/m)?.[1] ?? '' assert.match(openapiJob, /^ stage: verify$/m) assert.match(openapiJob, /^ image: node:[^\s]+$/m) assert.match(openapiJob, /^ - npm ci --ignore-scripts --prefix scripts\/openapi-validation$/m) assert.match(openapiJob, /^ - npm test --prefix scripts\/openapi-validation$/m) assert.match(openapiJob, /^ - npm run validate --prefix scripts\/openapi-validation$/m) assert.doesNotMatch(openapiJob, /\bnpx\b|@latest/) assert.match(packageJob, /^ - Java拓扑合同$/m) assert.match(packageJob, /^ - OpenAPI有效性$/m) assert.match(packageJob, /^ - 校验与测试$/m) }) test('Jenkins cannot invoke the Node contract without declaring a Node runtime', async () => { for (const path of ['Jenkinsfile', 'Jenkinsfile.service']) { const pipeline = await read(path) if (!pipeline.includes('java-topology-contract.test.mjs')) continue assert.match( pipeline, /(?:nodejs\s*\(|NodeJSInstallation|docker(?:\.image\([^)]*node:|\s+run[^\n]*\bnode:))/, `${path} must declare how Node is provided before invoking the topology contract` ) } }) test('Maven verify publishes JaCoCo reports without enforcing a threshold', async () => { const pom = await read('pom.xml') const gitlab = await read('.gitlab-ci.yml') const jacoco = pom.match(/\s*org\.jacoco<\/groupId>[\s\S]*?<\/plugin>/)?.[0] ?? '' assert.match(jacoco, /jacoco-maven-plugin<\/artifactId>/) assert.match(jacoco, /prepare-agent<\/goal>/) assert.match(jacoco, /verify<\/phase>/) assert.match(jacoco, /report<\/goal>/) assert.match(jacoco, /HTML<\/format>/) assert.match(jacoco, /XML<\/format>/) assert.doesNotMatch(jacoco, /check<\/goal>|/) assert.match(gitlab, /^ - "\*\*\/target\/site\/jacoco\/"$/m) assert.match(gitlab, /^ - "\*\*\/target\/jacoco\.exec"$/m) }) test('SpotBugs remains an opt-in non-blocking report without quality thresholds', async () => { const pom = await read('pom.xml') const gitlab = await read('.gitlab-ci.yml') const profile = pom.match(/\s*static-analysis-report<\/id>[\s\S]*?<\/profile>/)?.[0] ?? '' const spotbugs = profile.match(/\s*com\.github\.spotbugs<\/groupId>[\s\S]*?<\/plugin>/)?.[0] ?? '' const reportJob = gitlab.match(/^静态分析报告:\n([\s\S]*?)(?=^[^\s#].*:\n)/m)?.[1] ?? '' const verifyJob = gitlab.match(/^校验与测试:\n([\s\S]*?)(?=^[^\s#].*:\n)/m)?.[1] ?? '' const packageJob = gitlab.match(/^打包服务:\n([\s\S]*)/m)?.[1] ?? '' assert.match(pom, /4\.9\.8\.5<\/spotbugs-maven-plugin\.version>/) assert.match(spotbugs, /spotbugs-maven-plugin<\/artifactId>/) assert.match(spotbugs, /verify<\/phase>/) assert.match(spotbugs, /spotbugs<\/goal>/) assert.match(spotbugs, /true<\/xmlOutput>/) assert.match(spotbugs, /true<\/htmlOutput>/) assert.match(spotbugs, /false<\/failOnError>/) assert.doesNotMatch(spotbugs, /check<\/goal>||/) assert.doesNotMatch(pom.replace(profile, ''), /spotbugs-maven-plugin<\/artifactId>/) assert.match(reportJob, /^ allow_failure: true$/m) assert.match(reportJob, /-Pstatic-analysis-report/) assert.match(reportJob, /^ - "\*\*\/target\/spotbugs\*\.xml"$/m) assert.match(reportJob, /^ - "\*\*\/target\/site\/spotbugs\.html"$/m) assert.doesNotMatch(verifyJob, /static-analysis-report|spotbugs/i) assert.doesNotMatch(packageJob, /^ - 静态分析报告$/m) }) test('one TEST Compose and Nacos manifest cover every retained service', async () => { const services = parseServiceMap(await read('deploy/service-map.yml')) const compose = parseCompose(await read('deploy/docker-compose.yml')) assert.deepEqual(sorted(compose.keys()), sorted(services.keys())) assert.equal(services.size, 13) for (const [module, service] of services) { assert.equal(compose.get(module), service.port, module) await access(new URL(`../config/nacos/test/${service.artifactId}.yml`, import.meta.url)) } })