board-v3-deploy.test.mjs 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354
  1. import fs from 'node:fs';
  2. import os from 'node:os';
  3. import path from 'node:path';
  4. import crypto from 'node:crypto';
  5. import assert from 'node:assert/strict';
  6. import test from 'node:test';
  7. import { publishBoardV3, MANIFEST_PATH } from '../scripts/publish-board-v3-20261112.mjs';
  8. const sha = bytes => crypto.createHash('sha256').update(bytes).digest('hex');
  9. function fixture() {
  10. const base = fs.mkdtempSync(path.join(os.tmpdir(), 'ksf-board-v3-test-'));
  11. const targetDir = path.join(base, 'site'); fs.mkdirSync(targetDir);
  12. const manifest = JSON.parse(fs.readFileSync(MANIFEST_PATH)); manifest.homepageSha256 = sha('homepage');
  13. fs.writeFileSync(path.join(targetDir, 'index.html'), 'homepage');
  14. for (const file of manifest.files) {
  15. if (file.allowMissing) continue;
  16. const target = path.join(targetDir, file.path), bytes = Buffer.from('prior ' + file.path);
  17. fs.mkdirSync(path.dirname(target), { recursive: true }); fs.writeFileSync(target, bytes);
  18. file.priorNormalizedSha256 = [sha(bytes)];
  19. }
  20. const manifestPath = path.join(base, 'manifest.json'); fs.writeFileSync(manifestPath, JSON.stringify(manifest));
  21. return { base, targetDir, manifestPath, manifest };
  22. }
  23. test('v3发布检查完整浏览器依赖,原子更新后重复执行无变化,首页不变', () => {
  24. const f = fixture(); try {
  25. const plan = publishBoardV3(f); assert.equal(plan.readOnly, true); assert.equal(plan.filesChecked, f.manifest.files.length);
  26. assert.equal(plan.changes.length, f.manifest.files.length);
  27. assert.equal(fs.existsSync(path.join(f.targetDir, 'skills/ksf-workshop-runtime/scripts/course-deliverables.mjs')), false);
  28. const result = publishBoardV3({ ...f, apply: true }); assert.equal(result.filesystemVerified, true);
  29. assert.equal(result.homepageModified, false); assert.equal(result.businessRecordWrites, 0);
  30. assert.deepEqual(publishBoardV3(f).changes, []); assert.equal(fs.readFileSync(path.join(f.targetDir, 'index.html'), 'utf8'), 'homepage');
  31. } finally { fs.rmSync(f.base, { recursive: true, force: true }); }
  32. });
  33. test('v3发布拒绝未知现场、额外文件和首页变化,写入失败回退已改文件及新增模块', () => {
  34. const f = fixture(); try {
  35. const originalManifest = fs.readFileSync(f.manifestPath), prior = new Map();
  36. for (const file of f.manifest.files) if (!file.allowMissing) prior.set(file.path, fs.readFileSync(path.join(f.targetDir, file.path)));
  37. assert.throws(() => publishBoardV3({ ...f, apply: true }, { beforeWrite(relative) {
  38. if (relative === 'src/pages/view-report.js') throw Error('simulated write failure');
  39. } }), error => error.rollbackComplete === true);
  40. for (const file of f.manifest.files) {
  41. const target = path.join(f.targetDir, file.path);
  42. if (file.allowMissing) assert.equal(fs.existsSync(target), false); else assert.deepEqual(fs.readFileSync(target), prior.get(file.path));
  43. }
  44. f.manifest.files.push({ path: 'server/library-api.mjs' }); fs.writeFileSync(f.manifestPath, JSON.stringify(f.manifest));
  45. assert.throws(() => publishBoardV3({ ...f, apply: true }), { code: 'UNEXPECTED_FILE_LIST' });
  46. fs.writeFileSync(f.manifestPath, originalManifest);
  47. const unknown = f.manifest.files.find(file => !file.allowMissing).path;
  48. fs.writeFileSync(path.join(f.targetDir, unknown), 'concurrent content');
  49. assert.throws(() => publishBoardV3({ ...f, apply: true }), { code: 'UNKNOWN_TARGET_CONTENT' });
  50. fs.writeFileSync(path.join(f.targetDir, unknown), prior.get(unknown)); fs.writeFileSync(path.join(f.targetDir, 'index.html'), 'other homepage');
  51. assert.throws(() => publishBoardV3({ ...f, apply: true }), { code: 'HOMEPAGE_MISMATCH' });
  52. } finally { fs.rmSync(f.base, { recursive: true, force: true }); }
  53. });