import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import crypto from 'node:crypto'; import assert from 'node:assert/strict'; import test from 'node:test'; import { publishBoardUi, MANIFEST_PATH, UI_FILES } from '../scripts/publish-board-ui-20261112.mjs'; const sha = bytes => crypto.createHash('sha256').update(bytes).digest('hex'); function fixture() { const base = fs.mkdtempSync(path.join(os.tmpdir(), 'ksf-board-ui-test-')); const targetDir = path.join(base, 'site'); fs.mkdirSync(targetDir); fs.writeFileSync(path.join(targetDir, 'index.html'), 'fixture homepage'); const manifest = JSON.parse(fs.readFileSync(MANIFEST_PATH)); manifest.homepageSha256 = sha('fixture homepage'); for (const file of manifest.files) { // Exercise publisher behavior with current source, independently of the archived release hashes. const source = fs.readFileSync(file.path); file.sha256 = sha(Buffer.from(source.toString('utf8').replaceAll('\r\n', '\n'))); if (file.allowMissing) continue; const target = path.join(targetDir, file.path), bytes = Buffer.from('fixture prior: ' + file.path); fs.mkdirSync(path.dirname(target), { recursive: true }); fs.writeFileSync(target, bytes); file.priorNormalizedSha256 = [sha(bytes)]; } const reportPath = path.join(targetDir, 'src/pages/view-report.js'); fs.writeFileSync(reportPath, 'independent report update'); const manifestPath = path.join(base, 'manifest.json'); fs.writeFileSync(manifestPath, JSON.stringify(manifest)); return { base, targetDir, manifestPath, reportPath, manifest }; } test('six-file UI release applies and is idempotent while preserving an independent report update', () => { const f = fixture(); try { const plan = publishBoardUi(f); assert.equal(plan.readOnly, true); assert.deepEqual(plan.changes, UI_FILES); assert.equal(fs.existsSync(path.join(f.targetDir, 'src/components/kf-member-picker.js')), false); const result = publishBoardUi({ ...f, apply: true }); assert.equal(result.filesystemVerified, true); assert.equal(result.filesChecked, 6); assert.equal(result.homepageModified, false); assert.deepEqual(publishBoardUi(f).changes, []); assert.equal(fs.readFileSync(f.reportPath, 'utf8'), 'independent report update'); assert.equal(fs.readFileSync(path.join(f.targetDir, 'index.html'), 'utf8'), 'fixture homepage'); } finally { fs.rmSync(f.base, { recursive: true, force: true }); } }); test('UI release refuses an unexpected file list and an unknown baseline before writing', () => { const f = fixture(); try { const original = fs.readFileSync(f.manifestPath); f.manifest.files.push({ path: 'src/pages/view-report.js' }); fs.writeFileSync(f.manifestPath, JSON.stringify(f.manifest)); assert.throws(() => publishBoardUi({ ...f, apply: true }), { code: 'UNEXPECTED_FILE_LIST' }); fs.writeFileSync(f.manifestPath, original); fs.writeFileSync(path.join(f.targetDir, UI_FILES[0]), 'unknown concurrent update'); assert.throws(() => publishBoardUi({ ...f, apply: true }), { code: 'UNKNOWN_TARGET_CONTENT' }); assert.equal(fs.existsSync(path.join(f.targetDir, 'src/components/kf-member-picker.js')), false); assert.equal(fs.readFileSync(f.reportPath, 'utf8'), 'independent report update'); } finally { fs.rmSync(f.base, { recursive: true, force: true }); } }); test('UI release rolls back both replaced files and its newly added component on a write failure', () => { const f = fixture(); try { const original = fs.readFileSync(path.join(f.targetDir, UI_FILES[0])); assert.throws(() => publishBoardUi({ ...f, apply: true }, { beforeWrite(file) { if (file === 'src/pages/view-workbench.js') throw Error('fixture failure'); } }), error => error.rollbackComplete === true); assert.deepEqual(fs.readFileSync(path.join(f.targetDir, UI_FILES[0])), original); assert.equal(fs.existsSync(path.join(f.targetDir, 'src/components/kf-member-picker.js')), false); assert.equal(fs.readFileSync(f.reportPath, 'utf8'), 'independent report update'); } finally { fs.rmSync(f.base, { recursive: true, force: true }); } });