import assert from 'node:assert/strict'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { spawnSync } from 'node:child_process'; const repoRoot = process.cwd(); const dryRunScript = path.join(repoRoot, 'scripts', 'import-pocketbase', 'src', 'dry-run-report.ts'); function writeJson(filePath, value) { fs.writeFileSync(filePath, JSON.stringify(value, null, 2), 'utf8'); } function runDryRun(exportDir) { const result = spawnSync(process.execPath, ['--import', 'tsx', dryRunScript, '--json'], { cwd: repoRoot, encoding: 'utf8', env: { ...process.env, PB_EXPORT_DIR: exportDir, }, }); if (result.error) throw result.error; const stdout = result.stdout || ''; const jsonStart = stdout.indexOf('{'); const payload = JSON.parse(jsonStart >= 0 ? stdout.slice(jsonStart) : '{}'); return { ...result, payload }; } function createSafeExport() { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'tiku-pb-dry-safe-')); writeJson(path.join(dir, 'users.json'), [ { id: 'u1', phone: '13800000000', role: 'student', nickname: '学生A' }, ]); writeJson(path.join(dir, 'subjects.json'), [ { id: 's1', name: '英语' }, ]); writeJson(path.join(dir, 'categories.json'), [ { id: 'c1', name: '阅读理解', subjectId: 's1' }, ]); writeJson(path.join(dir, 'questions.json'), [ { id: 'q1', subjectId: 's1', categoryId: 'c1', type: 'choice', content: '题干', answer: 'A' }, ]); writeJson(path.join(dir, 'orders.json'), [ { id: 'o1', userId: 'u1', amount: 99, status: 'paid' }, ]); writeJson(path.join(dir, 'svip_plans.json'), [ { id: 'p1', name: '题库会员', price: 99 }, ]); writeJson(path.join(dir, 'codes.json'), [ { id: 'code1', code: 'SAFE-001', status: 'unused' }, ]); writeJson(path.join(dir, 'vocabulary_units.json'), [ { id: 'vu1', name: 'Unit 1' }, ]); writeJson(path.join(dir, 'vocabulary.json'), [ { id: 'vw1', unitId: 'vu1', word: 'abandon', meaning: '放弃' }, ]); writeJson(path.join(dir, 'handbook_subjects.json'), [ { id: 'hs1', name: '政治' }, ]); writeJson(path.join(dir, 'handbook_chapters.json'), [ { id: 'hc1', subjectId: 'hs1', title: '第一章' }, ]); writeJson(path.join(dir, 'handbook_entries.json'), [ { id: 'he1', chapterId: 'hc1', title: '知识点', content: '内容' }, ]); return dir; } function createUnsafeExport() { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'tiku-pb-dry-unsafe-')); writeJson(path.join(dir, 'users.json'), [ { id: 'u1', phone: '13800000000', password: 'plaintext-should-not-exist' }, { phone: '13900000000' }, ]); writeJson(path.join(dir, 'questions.json'), [ { id: 'q1', subjectId: 'missing-subject', content: '题干' }, { id: 'q1', subjectId: 'missing-subject', content: '重复题' }, ]); writeJson(path.join(dir, 'settings.json'), [ { id: 'settings1', appSecret: 'secret-in-public-export' }, ]); writeJson(path.join(dir, 'unknown_business.json'), [ { id: 'x1', name: '尚未映射集合' }, ]); return dir; } const safeDir = createSafeExport(); const safe = runDryRun(safeDir); assert.equal(safe.status, 0, `safe dry-run should pass: ${safe.stdout} ${safe.stderr}`); assert.equal(safe.payload.summary?.blockers, 0, 'safe dry-run should have no blockers'); assert.ok(safe.payload.summary?.records >= 12, 'safe dry-run should count fixture records'); assert.equal(safe.payload.businessCounts?.questions, 1, 'safe dry-run should report question count'); const unsafeDir = createUnsafeExport(); const unsafe = runDryRun(unsafeDir); assert.notEqual(unsafe.status, 0, 'unsafe dry-run should fail'); assert.ok(unsafe.payload.summary?.blockers > 0, 'unsafe dry-run should report blockers'); assert.ok( unsafe.payload.issues?.some(item => item.code === 'record_id_missing' && item.collection === 'users'), 'unsafe dry-run should detect missing user id', ); assert.ok( unsafe.payload.issues?.some(item => item.code === 'record_id_duplicate' && item.collection === 'questions'), 'unsafe dry-run should detect duplicate question id', ); assert.ok( unsafe.payload.issues?.some(item => item.code === 'sensitive_field_in_export'), 'unsafe dry-run should detect sensitive fields', ); assert.ok( unsafe.payload.issues?.some(item => item.code === 'unsupported_collection'), 'unsafe dry-run should warn about unsupported collections', ); fs.rmSync(safeDir, { recursive: true, force: true }); fs.rmSync(unsafeDir, { recursive: true, force: true }); console.log('[PASS] PocketBase dry-run report');