refactor(test): 三份 helper 迁移共享

Task 6 审查遗留①(schema 再演进防漂移):makeWork / seedPublishedSite /
cleanupSiteRows 三件套自 build-snapshot / build-pipeline / build-gates /
preview-server 四文件(Task 6 审查时三份,Task 7 又落一份)迁
tests/integration/helpers.ts 共享——清场清单(21 语句 FK 反序 + 不可变
触发器开关)此后只改一处。

- makeWork 保持签名;媒体种子载荷改由 prefix 派生(bytes 仍随机唯一,
  魔数校验只看 PNG 签名——行为等价);shared Work 统一携带 pngA/pngB
- seedPublishedSite 增 keyPrefix 参数(各文件沿用原站点 key 派生前缀
  sfbuild/sfpipe/sfgate/sfpreview);succeeded_with_issues 与
  publishedCount=3 断言随迁(gallery 203/204 悬空语义文件无关)
- import 域短清单清场与 publish 域各自形状不同,不共用——保留原处

零行为变更由 408 测试守护(全绿:根 382 + 主题 26);lint/typecheck 五段过。

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-22 15:55:43 +08:00
parent 7e4cf11452
commit 5c2ee7dc5d
5 changed files with 117 additions and 310 deletions
+10 -87
View File
@@ -22,8 +22,7 @@
// 运行于 globalSetup 临时库(ADR-015);storage/artifacts 根 os.tmpdir,finally 清理。
import { createHash } from 'node:crypto'
import { existsSync } from 'node:fs'
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { readFile, rm, writeFile } from 'node:fs/promises'
import { join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
@@ -35,7 +34,6 @@ import {
} from '@sf/adapter-wp'
import { canonicalJson } from '../../apps/api/src/common/canonical.js'
import { createImportJob } from '../../apps/api/src/modules/import/import.service.js'
import { publishSiteContent } from '../../apps/api/src/modules/publish/publish.service.js'
import { registerSource } from '../../apps/api/src/modules/source/register.service.js'
import {
BuildError,
@@ -47,17 +45,10 @@ import {
materializeRenderSnapshot,
rewriteBodyHtml,
} from '../../apps/api/src/modules/build/render-snapshot.js'
import { rand, withImmutableCleanup, withTestDb, type Db } from './helpers.js'
import { cleanupSiteRows, makeWork, rand, seedPublishedSite, withTestDb, type Db } from './helpers.js'
const FIXTURES_ROOT = fileURLToPath(new URL('../fixtures', import.meta.url))
const WXR_FIXTURE = resolve(FIXTURES_ROOT, 'wxr', 'mini.xml')
const PNG_SIG = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])
/** 伪 PNG 体(签名 + 唯一种子载荷——魔数过检、内容唯一避免跨测试撞哈希) */
function pngBytes(seed: string): Buffer {
return Buffer.concat([PNG_SIG, Buffer.from(`png-body:${seed}`, 'latin1')])
}
function sha256of(buf: Buffer | string): string {
return createHash('sha256').update(buf).digest('hex')
}
@@ -71,74 +62,6 @@ async function countOf(db: Db, sql: string): Promise<number> {
return Number(rows[0].n)
}
/** 本文件写入的行清场:validation 域先行(FK 链 validation_issues→validation_runs→build_jobs;
// Task 6 起成功构建必落 validation_runs 行)+ 构建域(不可变快照先行)+ 全链反序 */
async function cleanupSiteRows(db: Db, siteId: string): Promise<void> {
await db.execute(`delete from validation_issues where validation_run_id in
(select id from validation_runs where site_id = '${siteId}')`)
await db.execute(`delete from validation_runs where site_id = '${siteId}'`)
await db.execute(`delete from build_artifacts where build_job_id in
(select id from build_jobs where site_id = '${siteId}')`)
await withImmutableCleanup(db, 'build_input_snapshots', 'build_input_snapshots_immutable', `site_id = '${siteId}'`)
await db.execute(`delete from build_jobs where site_id = '${siteId}'`)
await withImmutableCleanup(db, 'audit_logs', 'audit_logs_immutable', `site_id = '${siteId}'`)
await db.execute(`delete from site_config_revisions where site_id = '${siteId}'`)
await db.execute(`delete from route_registry where site_id = '${siteId}'`)
await db.execute(`delete from media_usages where site_id = '${siteId}'`)
await db.execute(`delete from media_assets where site_id = '${siteId}'`)
await db.execute(`delete from content_terms where site_id = '${siteId}'`)
await db.execute(`delete from terms where site_id = '${siteId}'`)
await db.execute(`delete from taxonomies where site_id = '${siteId}'`)
await db.execute(`delete from source_records where site_id = '${siteId}'`)
await db.execute(`delete from entry_locales where site_id = '${siteId}'`)
await withImmutableCleanup(db, 'content_revisions', 'content_revisions_immutable', `site_id = '${siteId}'`)
await db.execute(`delete from content_entries where site_id = '${siteId}'`)
await db.execute(`delete from import_jobs where site_id = '${siteId}'`)
await withImmutableCleanup(db, 'source_snapshots', 'source_snapshots_immutable', `site_id = '${siteId}'`)
await db.execute(`delete from source_connections where site_id = '${siteId}'`)
await db.execute(`delete from sites where id = '${siteId}'`)
}
interface Work {
work: string
storageRoot: string
allowedRoots: string[]
pngA: Buffer
pngB: Buffer
}
async function makeWork(prefix: string): Promise<Work> {
const work = await mkdtemp(join(tmpdir(), `${prefix}-`))
// 媒体目录:fixture-a/b 按旧 uploads 层级放(清单精确命中 → media_assets ready 两行)
const mediaDir = join(work, 'media')
await mkdir(join(mediaDir, '2026', '10'), { recursive: true })
const pngA = pngBytes(`build-a-${rand()}`)
const pngB = pngBytes(`build-b-${rand()}`)
await writeFile(join(mediaDir, '2026', '10', 'fixture-a.png'), pngA)
await writeFile(join(mediaDir, '2026', '10', 'fixture-b.png'), pngB)
return { work, storageRoot: join(work, 'storage'), allowedRoots: [FIXTURES_ROOT, work], pngA, pngB }
}
/** mini 全链(register wxr+media → import → publish):3 条 published + 2 ready 媒体 */
async function seedPublishedSite(db: Db, w: Work): Promise<{ key: string; siteId: string }> {
const key = `sfbuild-${rand()}`
const reg = await registerSource(db, {
siteKey: key,
wxrPath: WXR_FIXTURE,
mediaDir: join(w.work, 'media'),
allowedRoots: w.allowedRoots,
storageRoot: w.storageRoot,
reportRoot: join(w.work, 'artifacts', 'register'),
})
const result = await createImportJob(db, { siteKey: key, reportRoot: join(w.work, 'artifacts', 'import') })
// gallery 203/204 悬空 → issues 非空是预期(§7.6 禁静默)
expect(result.status).toBe('succeeded_with_issues')
const dry = await publishSiteContent(db, { siteKey: key, dryRun: true })
const run = await publishSiteContent(db, { siteKey: key, confirmToken: dry.confirmToken! })
expect(run.publishedCount).toBe(3)
return { key, siteId: reg.siteId }
}
/** 物化结果六文件串 → 解析对象(直调版——spawn 缩减后不走 job 落盘面) */
function parsedFiles(m: { files: Record<string, string> }): Record<string, any> {
const out: Record<string, any> = {}
@@ -153,7 +76,7 @@ describe('build job + Render Snapshot 物化', () => {
await withTestDb(async (db) => {
const w = await makeWork('sf-build-sm')
try {
const { key, siteId } = await seedPublishedSite(db, w)
const { key, siteId } = await seedPublishedSite(db, w, 'sfbuild')
try {
const statuses: string[] = []
const result = await createBuildJob(db, {
@@ -245,7 +168,7 @@ describe('build job + Render Snapshot 物化', () => {
await withTestDb(async (db) => {
const w = await makeWork('sf-build-files')
try {
const { key, siteId } = await seedPublishedSite(db, w)
const { key, siteId } = await seedPublishedSite(db, w, 'sfbuild')
try {
const m = await materializeRenderSnapshot(db, { siteId, generatedAt: '2026-10-22T00:00:00.000Z' })
// 六文件面齐备(RENDER_SNAPSHOT_FILES 冻结清单——物化产物键即清单)
@@ -384,7 +307,7 @@ describe('build job + Render Snapshot 物化', () => {
await withTestDb(async (db) => {
const w = await makeWork('sf-build-rewrite')
try {
const { siteId } = await seedPublishedSite(db, w)
const { siteId } = await seedPublishedSite(db, w, 'sfbuild')
try {
const files = parsedFiles(await materializeRenderSnapshot(db, { siteId, generatedAt: '2026-10-22T00:00:00.000Z' }))
const news = files.content.find((c: any) => c.slug === 'fixture-news-a')
@@ -420,7 +343,7 @@ describe('build job + Render Snapshot 物化', () => {
await withTestDb(async (db) => {
const w = await makeWork('sf-build-det')
try {
const { siteId } = await seedPublishedSite(db, w)
const { siteId } = await seedPublishedSite(db, w, 'sfbuild')
try {
const fixedAt = '2026-10-22T00:00:00.000Z'
const first = await materializeRenderSnapshot(db, { siteId, generatedAt: fixedAt })
@@ -491,7 +414,7 @@ describe('build job + Render Snapshot 物化', () => {
await withTestDb(async (db) => {
const w = await makeWork('sf-build-adr010')
try {
const { key, siteId } = await seedPublishedSite(db, w)
const { key, siteId } = await seedPublishedSite(db, w, 'sfbuild')
try {
// 第一次物化:published=A
const first = await materializeRenderSnapshot(db, { siteId, generatedAt: '2026-10-22T00:00:00.000Z' })
@@ -539,7 +462,7 @@ describe('build job + Render Snapshot 物化', () => {
await withTestDb(async (db) => {
const w = await makeWork('sf-build-dup')
try {
const { siteId } = await seedPublishedSite(db, w)
const { siteId } = await seedPublishedSite(db, w, 'sfbuild')
try {
const aPub = '/wp-content/uploads/2026/10/fixture-a.png'
const bPub = '/wp-content/uploads/2026/10/fixture-b.png'
@@ -579,7 +502,7 @@ describe('build job + Render Snapshot 物化', () => {
await withTestDb(async (db) => {
const w = await makeWork('sf-build-lineage')
try {
const { key, siteId } = await seedPublishedSite(db, w)
const { key, siteId } = await seedPublishedSite(db, w, 'sfbuild')
try {
// 手构崩溃残留:上个进程中断留下的 snapshotting 悬行(默认键、attempt=1)
const pubIds = plain(
@@ -641,7 +564,7 @@ describe('build job + Render Snapshot 物化', () => {
await withTestDb(async (db) => {
const w = await makeWork('sf-build-nonready')
try {
const { siteId } = await seedPublishedSite(db, w)
const { siteId } = await seedPublishedSite(db, w, 'sfbuild')
try {
// 登记后状态翻转:fixture-b(product 的 featured 引用)→ quarantined
await db.execute(`update media_assets set status = 'quarantined'