board-ui-deploy.test.mjs 4.1 KB

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