test(db): 空库一键迁移验收 + globalSetup 临时库 + 公共夹具收敛(阶段1验收)

- vitest globalSetup:每次运行 CREATE sf_test_<ts> 临时库 → drizzle-orm 程序化迁移器
  全量迁移(apps/api/src/db/migrate.ts,drizzle-kit 无法指向运行时建库)→ 设
  DATABASE_URL_TEST 让 getDb() 自动回退到临时库;teardown DROP(WITH FORCE)。
  有 default 导出时 vitest 不捕获具名 teardown,故用双具名导出。
- db.full-migrate.test.ts:21 表齐 + 四张不可变触发器在库 + sf_test_* 护栏断言
  (current_database 不符即红,杜绝静默连开发库)。
- helpers.ts 收敛:rand/rejectionMessage/createSiteFixture/withImmutableCleanup/
  withTestDb,Task 6–12 七文件内联版替换为 import(断言、对照组、清场顺序不变)。
- scripts/migrate-test.sh:docker compose up --wait + pnpm test 一键验收。
- 实证:75 测试全绿(72+3);开发库 21 表行数总和 0(零写入);无 sf_test_* 残留。

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-20 20:43:13 +08:00
parent 9e0b49885b
commit f1c181de02
13 changed files with 338 additions and 220 deletions
+42
View File
@@ -0,0 +1,42 @@
// vitest globalSetup(Task 13):每次测试运行用完好的空临时库,结束即删——
// 彻底消除"测试碰开发库"与"清场残留"两类问题。
// 机制:用开发库凭据连维护库(postgres)CREATE DATABASE sf_test_<ts> →
// 用 drizzle-orm 程序化迁移器对临时库跑 apps/api/drizzle 全量迁移 →
// 设 process.env.DATABASE_URL_TEST(getDb() 的 DATABASE_URL_TEST ?? DATABASE_URL
// 回退链让所有测试自动连临时库;worker 由主进程在 setup 之后派生,env 继承生效)。
// 导出形式注意:vitest 的 globalSetup 加载器在有 default 导出时不捕获具名 teardown
// (loadGlobalSetupFile 的 if/default 分支只取 default 作 setup),故这里必须
// 用具名 setup + teardown 双导出、不写 default——teardown 才会被注册并在收尾执行。
// teardown DROP DATABASE 用 WITH (FORCE) 先断开残余业务连接再删;setup 中途失败也自清。
import { requireDatabaseUrl } from '../../apps/api/src/db/env.js'
import { createDatabase, dropDatabase, migrateDatabase } from '../../apps/api/src/db/migrate.js'
let created: { adminUrl: string; name: string } | undefined
export async function setup(): Promise<void> {
// 只认 DATABASE_URL(开发库凭据)——即便环境里残留 DATABASE_URL_TEST 也不受影响
const devUrl = requireDatabaseUrl({ allowTestUrl: false })
const adminUrl = new URL(devUrl)
adminUrl.pathname = '/postgres' // CREATE/DROP DATABASE 须在维护库会话里执行
const name = `sf_test_${Date.now()}`
const testUrl = new URL(devUrl)
testUrl.pathname = `/${name}`
await createDatabase(adminUrl.toString(), name)
try {
await migrateDatabase(testUrl.toString())
} catch (e) {
// 迁移半途而废:立即清掉本次临时库再抛出,失败运行也不留残留
await dropDatabase(adminUrl.toString(), name)
throw e
}
process.env.DATABASE_URL_TEST = testUrl.toString()
created = { adminUrl: adminUrl.toString(), name }
}
export async function teardown(): Promise<void> {
if (!created) return
const { adminUrl, name } = created
created = undefined
await dropDatabase(adminUrl, name)
}