// @ts-nocheck /** * 集成测试 globalSetup:创建 /tmp 一次性 SQLite 库并应用 Prisma schema。 * * 步骤(每一步都可审计): * 1. 记录项目数据源(prisma/dev.db、/dev.db、/data.db)指纹基线。 * 2. 用 `prisma migrate diff --from-empty --to-schema-datamodel prisma/schema.prisma --script` * 离线生成最终 DDL —— 该命令只输出 SQL 文本,不连接任何数据库。作为第二道保险, * 子进程的 DATABASE_URL 同样被强制指向临时库,并在执行后复验项目库指纹未变。 * 3. 用 `npx tsx config/test/itest/apply-schema.mts` 在进程内以真实 Prisma 客户端把 DDL * 应用到临时库,并由 SQLite 的 PRAGMA database_list 自证它打开的是哪个文件。 * 4. 复验项目库指纹,写入 manifest(worker 的 setupFiles 与 globalTeardown 都读它)。 */ const fs = require('fs'); const path = require('path'); const { execFileSync } = require('child_process'); const guard = require('./env.js'); function stamp(msg) { console.log(`[itest:setup] ${msg}`); } function assertProjectDbUntouched(baseline, where) { const now = guard.devDbBaselines(); const drift = Object.keys(baseline).filter((k) => !guard.sameFingerprint(baseline[k], now[k])); if (drift.length > 0) { throw new Error(`[itest:setup] ${where} 之后项目数据源指纹发生变化:${JSON.stringify(drift)}`); } return now; } module.exports = async function globalSetup() { // 0) URL 合法性先于任何文件操作 guard.assertSafeDatasourceUrl(guard.DATASOURCE_URL, 'globalSetup'); const baseline = guard.devDbBaselines(); stamp(`tmp realpath = ${guard.TMP_REALPATH}`); stamp(`integration db = ${guard.DB_PATH}`); stamp(`prisma/dev.db fp = ${JSON.stringify(baseline.prismaDevDb)}`); // 1) 干净起步:只删除 /tmp 下属于本次运行的文件 for (const f of [ guard.DB_PATH, `${guard.DB_PATH}-journal`, `${guard.DB_PATH}-wal`, `${guard.DB_PATH}-shm`, guard.DDL_PATH, ]) { if (!f.startsWith(`${guard.TMP_REALPATH}${path.sep}`)) { throw new Error(`[itest:setup] 拒绝删除非临时目录文件 ${f}`); } try { fs.rmSync(f); } catch { /* 不存在即正常 */ } } // 2) 离线生成 DDL(migrate diff 不连接数据库;DATABASE_URL 仍指向临时库作为第二道保险) const diffOut = execFileSync( 'npx', [ 'prisma', 'migrate', 'diff', '--from-empty', '--to-schema-datamodel', 'prisma/schema.prisma', '--script', ], { cwd: guard.REPO_ROOT, encoding: 'utf8', env: { ...process.env, DATABASE_URL: guard.DATASOURCE_URL }, stdio: ['ignore', 'pipe', 'pipe'], }, ); const statements = guard.splitStatements(diffOut); if (statements.length < 5) { throw new Error(`[itest:setup] migrate diff 产出的语句数量异常(${statements.length}),拒绝继续`); } fs.writeFileSync(guard.DDL_PATH, diffOut); stamp(`ddl statements = ${statements.length}`); assertProjectDbUntouched(baseline, 'migrate diff'); // 3) 进程内应用 schema const resultFile = `${guard.MANIFEST_PATH}.apply.json`; const baselineFile = `${guard.MANIFEST_PATH}.baseline.json`; try { fs.rmSync(resultFile); } catch { /* ignore */ } fs.writeFileSync(baselineFile, JSON.stringify(baseline)); execFileSync('npx', ['tsx', path.join(__dirname, 'apply-schema.mts')], { cwd: guard.REPO_ROOT, encoding: 'utf8', env: { ...process.env, DATABASE_URL: guard.DATASOURCE_URL, ITEST_DDL_FILE: guard.DDL_PATH, ITEST_RESULT_FILE: resultFile, ITEST_BASELINE_FILE: baselineFile, }, stdio: ['ignore', 'inherit', 'inherit'], }); const applyResult = JSON.parse(fs.readFileSync(resultFile, 'utf8')); if (!applyResult.ok) throw new Error('[itest:setup] apply-schema 未报告成功'); stamp(`sqlite opened file= ${applyResult.openedFile}`); stamp(`tables = ${applyResult.tables.join(',')}`); // 4) 复验 + manifest 落盘 const afterSetup = assertProjectDbUntouched(baseline, 'apply-schema'); fs.writeFileSync( guard.MANIFEST_PATH, JSON.stringify( { datasourceUrl: guard.DATASOURCE_URL, baseline, afterSetup, applyResult, startedAt: new Date().toISOString() }, null, 2, ), ); process.env.DATABASE_URL = guard.DATASOURCE_URL; process.env.NOVALON_ITEST_MANIFEST = guard.MANIFEST_PATH; };