import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import path from 'node:path'; import crypto from 'node:crypto'; import {FILES,publishFiles} from '../scripts/publish-workshop-files.mjs'; const sha=b=>crypto.createHash('sha256').update(b).digest('hex'); function fixture(){ fs.mkdirSync('work',{recursive:true});const dir=fs.mkdtempSync(path.resolve('work/delta-publish-test-')); const sourceDir=path.join(dir,'bundle'),targetDir=path.join(dir,'site'),manifestPath=path.join(dir,'manifest.json'); fs.mkdirSync(sourceDir);fs.mkdirSync(targetDir);fs.writeFileSync(path.join(targetDir,'index.html'),'known public homepage'); const files=FILES.map((relative,i)=>{ const a=path.join(sourceDir,relative),b=path.join(targetDir,relative),bytes=Buffer.from('new-'+relative+'\n'),old=Buffer.from('old-'+relative+'\n'); fs.mkdirSync(path.dirname(a),{recursive:true});fs.writeFileSync(a,bytes); if(i!==0){fs.mkdirSync(path.dirname(b),{recursive:true});fs.writeFileSync(b,old);} return{path:relative,sha256:sha(bytes),priorNormalizedSha256:[sha(old)],allowMissing:i===0}; }); const manifest={version:'ksf.workshop.publish.v1',commit:'d14d02fd358c6c5dedfabc96c53fb29add8bd021',homepageSha256:sha(Buffer.from('known public homepage')),files}; fs.writeFileSync(manifestPath,JSON.stringify(manifest)); return{dir,sourceDir,targetDir,manifestPath,manifest}; } function snapshot(root){const rows=[];function walk(dir){for(const e of fs.readdirSync(dir,{withFileTypes:true})){const p=path.join(dir,e.name);if(e.isDirectory())walk(p);else rows.push([path.relative(root,p),sha(fs.readFileSync(p))]);}}walk(root);return rows.sort();} test('默认预检不创建文件;实际更新九文件,首页保持,重复运行不写入',()=>{ const f=fixture(),before=snapshot(f.targetDir),top=fs.readdirSync(f.dir); const plan=publishFiles(f);assert.equal(plan.readOnly,true);assert.equal(plan.filesChecked,9);assert.equal(plan.changes.length,9);assert.deepEqual(snapshot(f.targetDir),before);assert.deepEqual(fs.readdirSync(f.dir),top); const applied=publishFiles({...f,apply:true});assert.equal(applied.filesystemVerified,true);assert.equal(applied.publicSiteVerified,false);assert.ok(!applied.backup.startsWith(f.targetDir+path.sep)); assert.equal(fs.readFileSync(path.join(f.targetDir,'index.html'),'utf8'),'known public homepage'); for(const name of FILES)assert.equal(fs.readFileSync(path.join(f.targetDir,name),'utf8'),fs.readFileSync(path.join(f.sourceDir,name),'utf8')); const after=snapshot(f.targetDir),repeat=publishFiles({...f,apply:true});assert.deepEqual(repeat.changes,[]);assert.equal(repeat.backup,null);assert.deepEqual(snapshot(f.targetDir),after); }); test('未知目标内容、首页不符、坏包及超出白名单均在写入之前拒绝',()=>{ for(const kind of ['unknown','index','source','manifest']){ const f=fixture(); if(kind==='unknown')fs.writeFileSync(path.join(f.targetDir,FILES[2]),'another publisher changed this'); if(kind==='index')fs.writeFileSync(path.join(f.targetDir,'index.html'),'different website'); if(kind==='source')fs.appendFileSync(path.join(f.sourceDir,FILES[2]),'corrupt'); if(kind==='manifest'){f.manifest.files[0].path='index.html';fs.writeFileSync(f.manifestPath,JSON.stringify(f.manifest));} const before=snapshot(f.targetDir),top=fs.readdirSync(f.dir); assert.throws(()=>publishFiles({...f,apply:true}),{code:{unknown:'UNKNOWN_TARGET_CONTENT',index:'HOMEPAGE_MISMATCH',source:'PAYLOAD_HASH_MISMATCH',manifest:'UNEXPECTED_FILE_LIST'}[kind]}); assert.deepEqual(snapshot(f.targetDir),before);assert.deepEqual(fs.readdirSync(f.dir),top); } }); test('中途写入失败恢复已改文件,只清理本次新建文件,保留完整备份',()=>{ const f=fixture(),before=snapshot(f.targetDir);let attempts=0,error; try{publishFiles({...f,apply:true},{beforeWrite(){if(++attempts===4)throw Object.assign(Error('simulated write failure'),{code:'EACCES'});}});}catch(e){error=e;} assert.equal(attempts,4);assert.equal(error.rollbackComplete,true);assert.deepEqual(snapshot(f.targetDir),before); const journal=JSON.parse(fs.readFileSync(path.join(error.backup,'journal.json')));assert.equal(journal.status,'stopped');assert.equal(journal.files.length,9); }); test('已发布文件被其他人并发改动时,回滚保留外部版本并报告未完全恢复',()=>{ const f=fixture();let error; try{publishFiles({...f,apply:true},{beforeWrite(relative){if(relative===FILES[2]){fs.writeFileSync(path.join(f.targetDir,FILES[1]),'external concurrent edit');throw Error('stop');}}});}catch(e){error=e;} assert.equal(error.rollbackComplete,false);assert.equal(fs.readFileSync(path.join(f.targetDir,FILES[1]),'utf8'),'external concurrent edit');assert.equal(fs.existsSync(path.join(f.targetDir,FILES[0])),false); assert.equal(fs.readFileSync(path.join(f.targetDir,'index.html'),'utf8'),'known public homepage'); });