board-ui-deploy.test.mjs 3.8 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374
  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 { publishBoardUi, MANIFEST_PATH, UI_FILES } from '../scripts/publish-board-ui-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-ui-test-'));
  11. const targetDir = path.join(base, 'site');
  12. fs.mkdirSync(targetDir);
  13. fs.writeFileSync(path.join(targetDir, 'index.html'), 'fixture homepage');
  14. const manifest = JSON.parse(fs.readFileSync(MANIFEST_PATH));
  15. manifest.homepageSha256 = sha('fixture homepage');
  16. for (const file of manifest.files) {
  17. if (file.allowMissing) continue;
  18. const target = path.join(targetDir, file.path), bytes = Buffer.from('fixture prior: ' + file.path);
  19. fs.mkdirSync(path.dirname(target), { recursive: true });
  20. fs.writeFileSync(target, bytes);
  21. file.priorNormalizedSha256 = [sha(bytes)];
  22. }
  23. const reportPath = path.join(targetDir, 'src/pages/view-report.js');
  24. fs.writeFileSync(reportPath, 'independent report update');
  25. const manifestPath = path.join(base, 'manifest.json');
  26. fs.writeFileSync(manifestPath, JSON.stringify(manifest));
  27. return { base, targetDir, manifestPath, reportPath, manifest };
  28. }
  29. test('six-file UI release applies and is idempotent while preserving an independent report update', () => {
  30. const f = fixture();
  31. try {
  32. const plan = publishBoardUi(f);
  33. assert.equal(plan.readOnly, true);
  34. assert.deepEqual(plan.changes, UI_FILES);
  35. assert.equal(fs.existsSync(path.join(f.targetDir, 'src/components/kf-member-picker.js')), false);
  36. const result = publishBoardUi({ ...f, apply: true });
  37. assert.equal(result.filesystemVerified, true);
  38. assert.equal(result.filesChecked, 6);
  39. assert.equal(result.homepageModified, false);
  40. assert.deepEqual(publishBoardUi(f).changes, []);
  41. assert.equal(fs.readFileSync(f.reportPath, 'utf8'), 'independent report update');
  42. assert.equal(fs.readFileSync(path.join(f.targetDir, 'index.html'), 'utf8'), 'fixture homepage');
  43. } finally { fs.rmSync(f.base, { recursive: true, force: true }); }
  44. });
  45. test('UI release refuses an unexpected file list and an unknown baseline before writing', () => {
  46. const f = fixture();
  47. try {
  48. const original = fs.readFileSync(f.manifestPath);
  49. f.manifest.files.push({ path: 'src/pages/view-report.js' });
  50. fs.writeFileSync(f.manifestPath, JSON.stringify(f.manifest));
  51. assert.throws(() => publishBoardUi({ ...f, apply: true }), { code: 'UNEXPECTED_FILE_LIST' });
  52. fs.writeFileSync(f.manifestPath, original);
  53. fs.writeFileSync(path.join(f.targetDir, UI_FILES[0]), 'unknown concurrent update');
  54. assert.throws(() => publishBoardUi({ ...f, apply: true }), { code: 'UNKNOWN_TARGET_CONTENT' });
  55. assert.equal(fs.existsSync(path.join(f.targetDir, 'src/components/kf-member-picker.js')), false);
  56. assert.equal(fs.readFileSync(f.reportPath, 'utf8'), 'independent report update');
  57. } finally { fs.rmSync(f.base, { recursive: true, force: true }); }
  58. });
  59. test('UI release rolls back both replaced files and its newly added component on a write failure', () => {
  60. const f = fixture();
  61. try {
  62. const original = fs.readFileSync(path.join(f.targetDir, UI_FILES[0]));
  63. assert.throws(() => publishBoardUi({ ...f, apply: true }, { beforeWrite(file) {
  64. if (file === 'src/pages/view-workbench.js') throw Error('fixture failure');
  65. } }), error => error.rollbackComplete === true);
  66. assert.deepEqual(fs.readFileSync(path.join(f.targetDir, UI_FILES[0])), original);
  67. assert.equal(fs.existsSync(path.join(f.targetDir, 'src/components/kf-member-picker.js')), false);
  68. assert.equal(fs.readFileSync(f.reportPath, 'utf8'), 'independent report update');
  69. } finally { fs.rmSync(f.base, { recursive: true, force: true }); }
  70. });