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')}`) })