Files
baiyue-firstleg/scripts/script-documentation-contract.test.mjs
2026-09-23 17:47:50 +08:00

103 lines
4.6 KiB
JavaScript

import assert from 'node:assert/strict'
import { readdir, readFile } from 'node:fs/promises'
import path from 'node:path'
import test from 'node:test'
import { fileURLToPath } from 'node:url'
const repositoryRoot = path.resolve(fileURLToPath(new URL('..', import.meta.url)))
const scriptsReadme = path.join(repositoryRoot, 'scripts', 'README.md')
const adminModulesReadme = path.join(repositoryRoot, 'scripts', 'admin-test-env-modules', 'README.md')
const adminAcceptanceEntry = path.join(repositoryRoot, 'scripts', 'admin-test-env-acceptance.mjs')
const excludedDirectoryNames = new Set([
'.git', '.logs', 'dist', 'node_modules', 'output', 'outputs', 'reports', 'target', 'uni_modules',
])
const allowedDirectoryPrefixes = [
'scripts/',
'deploy/docker/',
'deploy/scripts/',
'deploy/sql/scripts/',
'deploy/rocketmq/',
'firstleg/scripts/',
'lastleg-service/scripts/',
'warehouse/scripts/',
]
async function executableFiles(directory, extensions, { excludeTests = true } = {}) {
const entries = await readdir(path.join(repositoryRoot, directory), { withFileTypes: true })
return entries
.filter((entry) => entry.isFile())
.filter((entry) => extensions.some((extension) => entry.name.endsWith(extension)))
.filter((entry) => !excludeTests || !entry.name.endsWith('.test.mjs'))
.map((entry) => `${directory}/${entry.name}`.replaceAll('\\', '/'))
}
async function discoverScriptFiles(directory = repositoryRoot) {
const discovered = []
const entries = await readdir(directory, { withFileTypes: true })
for (const entry of entries) {
if (entry.isDirectory() && (
excludedDirectoryNames.has(entry.name)
|| entry.name.startsWith('node_modules.')
|| entry.name.startsWith('.codex-')
|| entry.name.startsWith('.runtime-')
)) continue
const absolute = path.join(directory, entry.name)
if (entry.isDirectory()) {
discovered.push(...await discoverScriptFiles(absolute))
continue
}
if (!['.mjs', '.ps1', '.sh', '.groovy'].some((extension) => entry.name.endsWith(extension))) continue
discovered.push(path.relative(repositoryRoot, absolute).replaceAll('\\', '/'))
}
return discovered
}
test('executable scripts only live in documented directories', async () => {
const files = await discoverScriptFiles()
const misplaced = files.filter((file) => !allowedDirectoryPrefixes.some((prefix) => file.startsWith(prefix)))
assert.deepEqual(misplaced, [], `以下脚本位于未登记目录:\n${misplaced.join('\n')}`)
})
test('all directly runnable scripts are documented in scripts/README.md', async () => {
const readme = await readFile(scriptsReadme, 'utf8')
const files = [
...await executableFiles('scripts', ['.mjs', '.ps1', '.groovy']),
...await executableFiles('scripts/openapi-validation', ['.mjs']),
...await executableFiles('deploy/scripts', ['.sh']),
...await executableFiles('deploy/sql/scripts', ['.mjs']),
...await executableFiles('deploy/rocketmq', ['.sh']),
...await executableFiles('firstleg/scripts', ['.mjs']),
...await executableFiles('lastleg-service/scripts', ['.mjs']),
]
const undocumented = files.filter((file) => {
const relativeToScripts = file.startsWith('scripts/') ? file.slice('scripts/'.length) : file
return !readme.includes(file) && !readme.includes(relativeToScripts)
})
assert.deepEqual(undocumented, [], `以下可执行脚本未登记用途与边界:\n${undocumented.join('\n')}`)
})
test('README explains internal modules and test scripts are not standalone entries', async () => {
const readme = await readFile(scriptsReadme, 'utf8')
assert.match(readme, /\*\.test\.mjs.*离线 Node 静态合同/)
assert.match(readme, /admin-test-env-modules\/\*\.mjs.*逐模块说明/)
assert.match(readme, /warehouse\/scripts\/warehouse-public-master-data-contract\.test\.mjs/)
})
test('all Admin acceptance modules are documented in the module index', async () => {
const [entrySource, readme] = await Promise.all([
readFile(adminAcceptanceEntry, 'utf8'),
readFile(adminModulesReadme, 'utf8'),
])
const registeredModules = []
for (const list of entrySource.matchAll(/const (?:CONFIGURED|OPTIONAL)_MODULES = \[([\s\S]*?)\]/g)) {
for (const moduleName of list[1].matchAll(/'([^']+)'/g)) registeredModules.push(moduleName[1])
}
assert.notEqual(registeredModules.length, 0, '未从 Admin 全量入口读取到模块注册表')
const undocumented = [...new Set(registeredModules)]
.filter((moduleName) => !readme.includes(`\`${moduleName}.mjs\``))
assert.deepEqual(undocumented, [], `以下 Admin 验收模块未登记用途与边界:\n${undocumented.join('\n')}`)
})