import { test } from 'node:test' import assert from 'node:assert/strict' import { execSync } from 'node:child_process' import { readFileSync, existsSync, readdirSync } from 'node:fs' import { fileURLToPath } from 'node:url' import { resolve, dirname } from 'node:path' const here = dirname(fileURLToPath(import.meta.url)) const repoRoot = resolve(here, '..') const testsDir = resolve(here) function run(cmd: string): string { const env = { ...process.env } delete env.NODE_TEST_CONTEXT return execSync(cmd, { cwd: repoRoot, encoding: 'utf-8', env }) } function otherTestFiles(): string[] { return readdirSync(testsDir) .filter((f) => f.endsWith('.test.ts') && f !== 'dead-code-cleanup.test.ts') .map((f) => `tests/${f}`) } test('test_no_unexported_duplicates', () => { const source = readFileSync(resolve(repoRoot, 'src/shared/api/java-modules.ts'), 'utf-8') const lines = source.split('\n').filter((line) => line.includes('export * from')) const targets = lines .map((line) => line.match(/export \* from "\.\/([^"]+)"/)?.[1]) .filter(Boolean) assert.equal(new Set(targets).size, targets.length, '不应有重复的 re-export') }) test('test_build_typecheck', () => { const output = run('npx vue-tsc --noEmit') assert.equal(output.trim(), '', `vue-tsc 应零错误,实际: ${output.slice(0, 500)}`) }) test('test_full_vitest_green', () => { const output = run(`node --test ${otherTestFiles().join(' ')} 2>&1`) assert.match(output, /pass \d+/, '应产出通过统计') assert.match(output, /fail 0/, '全量测试应零失败') }) test('test_no_console_debug_left', () => { const apiDir = resolve(repoRoot, 'src/shared/api') for (const file of readdirSync(apiDir, { recursive: true }) as string[]) { if (!file.endsWith('.ts')) continue const source = readFileSync(resolve(apiDir, file), 'utf-8') assert.ok( !/console\.(log|debug)\(/.test(source), `${file} 不应残留调试日志`, ) } }) test('test_no_unused_import', () => { const source = readFileSync(resolve(repoRoot, 'src/shared/api/java-modules.ts'), 'utf-8') // 聚合器除 re-export 外不应有其它 import/函数体 const deadMarkers = [ 'function postTaskProgressBatch', 'const taskProgressResponseCache', 'function getCurrentUserId', 'function normalizeTaskIds', 'function buildTaskProgressRequestKey', 'async function uploadTempFileToJava', 'function getJavaDownloadUrl', ] for (const marker of deadMarkers) { assert.ok(!source.includes(marker), `java-modules.ts 不应残留: ${marker}`) } }) test('test_entry_points_compile', () => { const viteConfig = readFileSync(resolve(repoRoot, 'vite.config.ts'), 'utf-8') const entries = [...viteConfig.matchAll(/['"]([^'"]+\.html)['"]/g)].map((m) => m[1]) // SPA(2026-09-08):单入口 index.html,页面由 vue-router 渲染 assert.deepEqual(entries, ['index.html'], `应仅 index.html 单入口,实际 ${entries.join(', ')}`) for (const entry of entries) { assert.ok(existsSync(resolve(repoRoot, entry)), `入口 ${entry} 应存在`) } }) test('test_bundle_build', () => { const outDir = resolve(repoRoot, 'new_web_source') assert.ok(existsSync(outDir), '构建产物目录应存在') assert.ok(existsSync(resolve(outDir, 'index.html')), 'SPA 产物应包含 index.html') assert.ok(existsSync(resolve(outDir, 'assets')), 'SPA 产物应包含 assets/') }) test('test_git_diff_scope', () => { const diff = run('git diff --name-only HEAD~1 HEAD') const changed = diff.split('\n').filter(Boolean) assert.ok(changed.length > 0, '应检出本次提交的变更') const frontendTouched = changed.some((file) => file.startsWith('frontend-vue/')) if (!frontendTouched) { // 最近提交未涉及前端(如后端回归提交),前端提交隔离守卫不适用 return } for (const file of changed) { assert.ok( file.startsWith('frontend-vue/'), `前端提交变更应仅限 frontend-vue 目录,越界: ${file}`, ) } })