| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172 |
- import test from 'node:test';
- import assert from 'node:assert/strict';
- import fs from 'node:fs';
- import os from 'node:os';
- import path from 'node:path';
- import crypto from 'node:crypto';
- import { publish, rollback, FILES, RELEASE } from '../scripts/publish-five-views.mjs';
- const sha = (b) => crypto.createHash('sha256').update(b).digest('hex');
- function setup(t) {
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ksf-demo-publish-'));
- t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
- const sourceDir = path.join(dir, 'release'), targetDir = path.join(dir, 'site');
- const put = (file, text) => { fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, text); };
- const protectedFiles = ['src/services/board-service.js', 'src/components/kf-opportunity-card.js', 'src/pages/view-pool.js', 'src/core/report.js', 'src/core/evidence/chain.js'];
- put(path.join(targetDir, 'index.html'), 'homepage');
- for (const f of protectedFiles) put(path.join(targetDir, f), 'published-repair');
- const files = FILES.map((f, i) => {
- put(path.join(sourceDir, 'payload', f), 'new-' + i);
- if (i < 4) put(path.join(targetDir, f), 'old-' + i);
- return { path: f, sha256: sha('new-' + i), allowMissing: i >= 4, priorNormalizedSha256: i < 4 ? [sha('old-' + i)] : [] };
- });
- const manifestPath = path.join(sourceDir, 'manifest.json');
- fs.writeFileSync(manifestPath, JSON.stringify({ version: 'ksf.five-views.publish.v1', release: RELEASE, homepageSha256: sha('homepage'), files,
- preserved: protectedFiles.map((f) => ({ path: f, normalizedSha256: sha('published-repair') })) }));
- return { sourceDir, targetDir, manifestPath, put };
- }
- test('release defaults to a read-only plan, applies once and can restore all prior files', (t) => {
- const s = setup(t);
- assert.equal(publish(s).changes.length, FILES.length);
- assert.equal(fs.readFileSync(path.join(s.targetDir, FILES[0]), 'utf8'), 'old-0');
- const result = publish({ ...s, apply: true });
- assert.equal(result.filesystemVerified, true);
- assert.equal(publish({ ...s, apply: true }).changes.length, 0);
- const args = { targetDir: s.targetDir, backupDir: result.backup };
- assert.equal(rollback(args).readOnly, true);
- assert.equal(rollback({ ...args, apply: true }).filesystemVerified, true);
- assert.equal(fs.readFileSync(path.join(s.targetDir, FILES[0]), 'utf8'), 'old-0');
- assert.equal(fs.existsSync(path.join(s.targetDir, FILES[5])), false);
- assert.equal(fs.readFileSync(path.join(s.targetDir, 'index.html'), 'utf8'), 'homepage');
- });
- test('unknown versions, homepage changes, protected repair changes and overlapping directories stop before writes', (t) => {
- const s = setup(t);
- s.put(path.join(s.targetDir, FILES[0]), 'third-party');
- assert.throws(() => publish({ ...s, apply: true }), { code: 'UNKNOWN_TARGET_CONTENT' });
- s.put(path.join(s.targetDir, FILES[0]), 'old-0');
- s.put(path.join(s.targetDir, 'index.html'), 'other');
- assert.throws(() => publish(s), { code: 'HOMEPAGE_MISMATCH' });
- s.put(path.join(s.targetDir, 'index.html'), 'homepage');
- s.put(path.join(s.targetDir, 'src/services/board-service.js'), 'other');
- assert.throws(() => publish(s), { code: 'PUBLISHED_REPAIR_MISMATCH' });
- assert.throws(() => publish({ ...s, sourceDir: s.targetDir }), { code: 'SOURCE_TARGET_OVERLAP' });
- });
- test('an interrupted publication rolls back completed files without overwriting concurrent edits', (t) => {
- const s = setup(t); let caught;
- try { publish({ ...s, apply: true }, { beforeWrite(name) { if (name === FILES[2]) { s.put(path.join(s.targetDir, name), 'concurrent'); } } }); }
- catch (error) { caught = error; }
- assert.equal(caught.code, 'CONCURRENT_CHANGE'); assert.equal(caught.rollbackComplete, true);
- assert.equal(fs.readFileSync(path.join(s.targetDir, FILES[0]), 'utf8'), 'old-0');
- assert.equal(fs.readFileSync(path.join(s.targetDir, FILES[2]), 'utf8'), 'concurrent');
- assert.equal(JSON.parse(fs.readFileSync(path.join(caught.backup, 'journal.json'))).status, 'stopped');
- });
- test('rollback refuses a modified target or corrupted backup before restoring any file', (t) => {
- const s = setup(t), result = publish({ ...s, apply: true });
- const args = { targetDir: s.targetDir, backupDir: result.backup, apply: true };
- s.put(path.join(s.targetDir, FILES[1]), 'someone-else');
- assert.throws(() => rollback(args), { code: 'CONCURRENT_CHANGE' });
- assert.equal(fs.readFileSync(path.join(s.targetDir, FILES[0]), 'utf8'), 'new-0');
- s.put(path.join(s.targetDir, FILES[1]), 'new-1');
- s.put(path.join(result.backup, FILES[0]), 'broken-backup');
- assert.throws(() => rollback(args), { code: 'BACKUP_HASH_MISMATCH' });
- });
|