import { afterAll, beforeAll, describe, expect, it } from '@jest/globals'; import fs from 'node:fs'; import path from 'node:path'; import { prisma, resetDb, disconnect, unique, seedModel } from './helpers/harness'; const DEV_DB = path.resolve(__dirname, '..', 'prisma', 'dev.db'); interface DbFingerprint { exists: boolean; ino: number | null; size: number | null; mtimeMs: number | null; } interface Manifest { datasourceUrl: string; baseline: Record; } /** globalSetup 落盘的运行期清单(与临时库同目录,见 config/test/itest/env.js) */ function readManifest(): Manifest { const dbFile = process.env.NOVALON_ITEST_DB; if (!dbFile) throw new Error('NOVALON_ITEST_DB 未设置:worker 未经过 config/test/itest/setup-files.js'); const file = path.join(path.dirname(dbFile), 'novalon-itest-manifest.json'); return JSON.parse(fs.readFileSync(file, 'utf8')) as Manifest; } /** * 安全边界规格:这个文件不测业务,它测的是「其余所有 itest 到底连在哪个库上」。 * 它是 A-8 修复的前置条件 —— 集成层一旦误连 prisma/dev.db,这里必须变红。 */ describe('集成层数据源边界', () => { beforeAll(async () => { await resetDb(); }); afterAll(async () => { await disconnect(); }); it('打开的是临时目录里的一次性 SQLite 文件,项目数据源指纹未变', async () => { // 1) 引擎自报打开的文件 —— 不是「我们以为的路径」,而是 SQLite 实际 attach 的路径 const attached = await prisma.$queryRawUnsafe>('PRAGMA database_list'); const file = attached[0]?.file ?? ''; expect(file).toMatch(/novalon-itest\.db$/); expect(file).not.toMatch(/(^|[/\\])(dev|data|test|prod)\.db$/); // 2) 真的在跟 SQLite 说话:引擎版本号只有真实引擎给得出,任何 jest mock 都造不出来 const version = await prisma.$queryRawUnsafe>('SELECT sqlite_version() AS v'); expect(version[0]?.v).toMatch(/^\d+\.\d+\.\d+$/); // 3) worker 的 DATABASE_URL 与 globalSetup 落盘的 manifest 一致,且是绝对 file: 路径 const envUrl = process.env.DATABASE_URL ?? ''; const manifest = readManifest(); expect(envUrl.startsWith('file:/')).toBe(true); expect(manifest.datasourceUrl).toBe(envUrl); expect(envUrl).toBe(`file:${process.env.NOVALON_ITEST_DB}`); // 4) 项目数据源在集成测试运行前后是同一个文件且完全未写(inode + size + mtime 三重比对) const now = fs.statSync(DEV_DB); const before = manifest.baseline.prismaDevDb; if (!before) throw new Error('manifest 缺少 prismaDevDb 基线,守卫无法比对'); expect([now.ino, now.size, Math.round(now.mtimeMs)]).toEqual([ before.ino, before.size, before.mtimeMs === null ? null : Math.round(before.mtimeMs), ]); }); it('应用的 schema 带真实约束(唯一索引与外键都生效,不是空表壳)', async () => { const code = unique('it-model'); const { id } = await seedModel(code, []); await prisma.contentItem.create({ data: { modelId: id, modelCode: code, title: '第一版', slug: 'dup', data: '{}' }, }); // 同一 (modelCode, slug, locale) 的第二次写入必须被真实唯一索引拒绝 await expect( prisma.contentItem.create({ data: { modelId: id, modelCode: code, title: '第二版', slug: 'dup', data: '{}' }, }), ).rejects.toMatchObject({ code: 'P2002' }); // modelId 指向不存在的模型必须被真实外键(ContentItem_modelId_fkey)拒绝 await expect( prisma.contentItem.create({ data: { modelId: 'no-such-model', modelCode: code, title: '孤儿', slug: unique('orphan'), data: '{}' }, }), ).rejects.toThrow(); }); });