| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131 |
- import test from 'node:test';
- import assert from 'node:assert/strict';
- import fs from 'node:fs';
- import path from 'node:path';
- import os from 'node:os';
- import crypto from 'node:crypto';
- import {fileURLToPath} from 'node:url';
- import {planUpdate,publicPlan,applyUpdate,verifyUpdate,rollbackUpdate} from '../scripts/update-workshop-docs.mjs';
- const root=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..');
- const releaseDir=path.join(root,'docs/workshop-skill-release-20261109-r3');
- const hash=b=>crypto.createHash('sha256').update(b).digest('hex');
- const digest=p=>hash(fs.readFileSync(p));
- function fileMap(dir){const out={};for(const e of fs.readdirSync(dir,{withFileTypes:true})){const p=path.join(dir,e.name);if(e.isDirectory())for(const[n,h]of Object.entries(fileMap(p)))out[e.name+'/'+n]=h;else out[e.name]=digest(p);}return out;}
- function seal(dir){fs.writeFileSync(path.join(dir,'FILES.sha256'),Object.entries(fileMap(dir)).filter(([n])=>n!=='FILES.sha256').map(([n,h])=>h+' '+n+'\n').join(''));}
- function fixture(t){
- const dir=fs.mkdtempSync(path.join(os.tmpdir(),'ksf-docs-update-test-'));
- t.after(()=>{const abs=fs.realpathSync(dir);assert.ok(abs.startsWith(fs.realpathSync(os.tmpdir())+path.sep)&&path.basename(abs).startsWith('ksf-docs-update-test-'));fs.rmSync(abs,{recursive:true});});
- const dataRoot=path.join(dir,'data'),targetDir=path.join(dataRoot,'skills/ksf-workshop-runtime'),libraryDir=path.join(dataRoot,'skills/ksf-voc-library'),packageDir=path.join(dir,'package');
- fs.mkdirSync(path.dirname(targetDir),{recursive:true});
- fs.cpSync(path.join(releaseDir,'rehearsal-baseline'),targetDir,{recursive:true});
- fs.cpSync(path.join(releaseDir,'package'),packageDir,{recursive:true});
- // This research fixture is self-consistent and uses the four pinned scripts.
- // It is not presented as an exact remote research-directory export.
- fs.cpSync(path.join(root,'skills/ksf-voc-library'),libraryDir,{recursive:true});
- fs.unlinkSync(path.join(libraryDir,'SKILL.md'));seal(libraryDir);
- fs.writeFileSync(path.join(targetDir,'credential-binding.json'),'{"syntheticBinding":true}',{mode:0o600});
- fs.writeFileSync(path.join(targetDir,'fixture-private-envelope.json'),'{"fixtureOnly":true}',{mode:0o600});
- fs.mkdirSync(path.join(dataRoot,'ksf-workshop-state/agents'),{recursive:true});
- fs.writeFileSync(path.join(dataRoot,'ksf-workshop-state/agents/fixture.json'),'{"fixtureOnly":true}');
- return{dir,dataRoot,targetDir,libraryDir,packageDir,before:fileMap(dataRoot)};
- }
- function unchanged(f,before=f.before){assert.deepEqual(fileMap(f.dataRoot),before);}
- function restored(f){const after=fileMap(f.dataRoot);for(const[n,h]of Object.entries(f.before))assert.equal(after[n],h,n);for(const n of Object.keys(after).filter(n=>!n.startsWith('ksf-workshop-docs-backup-')))assert.ok(Object.hasOwn(f.before,n),n);}
- function editRelease(f,edit){const p=path.join(f.packageDir,'release.json'),r=JSON.parse(fs.readFileSync(p));edit(r);fs.writeFileSync(p,JSON.stringify(r));seal(f.packageDir);}
- test('dry against exact accepted r2 workshop bytes proposes only eight updates and one creation, zero writes',t=>{
- const f=fixture(t),p=publicPlan(planUpdate(f));assert.equal(p.changedFiles,9);assert.equal(p.readOnly,true);assert.equal(p.alreadyApplied,false);
- assert.equal(p.changes.filter(c=>c.action==='create').length,1);assert.equal(p.changes.filter(c=>c.action==='update').length,8);
- assert.ok(p.changes.every(c=>!c.path.includes('scripts')&&!c.path.includes('ksf-voc-library')));unchanged(f);
- });
- test('apply verifies all 39 public files, preserves private/state/research files, and retires no further entry',t=>{
- const f=fixture(t),r=applyUpdate(f),v=verifyUpdate({backup:r.backup});
- assert.equal(r.changedFiles,9);assert.equal(r.readOnly,false);assert.equal(v.installedFilesVerified,39);assert.equal(v.changedFilesVerified,9);
- assert.equal(v.researchFilesUnchanged,true);assert.equal(v.runtimeAndBindingPreserved,true);assert.equal(v.legacyEntrypointRetired,true);
- assert.equal(v.agentLoaded,false);assert.equal(v.currentReadVerified,false);
- assert.equal(fs.existsSync(path.join(f.libraryDir,'SKILL.md')),false);
- const release=JSON.parse(fs.readFileSync(path.join(f.packageDir,'release.json')));
- for(const[n,h]of Object.entries(release.candidate))assert.equal(digest(path.join(f.targetDir,n)),h,n);
- for(const[n,h]of Object.entries(f.before).filter(([n])=>!Object.keys(release.candidate).some(p=>n==='skills/ksf-workshop-runtime/'+p)))assert.equal(fileMap(f.dataRoot)[n],h,n);
- const backups=fs.readdirSync(r.backup).filter(n=>n.endsWith('.before'));assert.equal(backups.length,8);
- assert.ok(!fs.readFileSync(path.join(r.backup,'journal.json'),'utf8').includes('syntheticBinding'));
- });
- test('repeat apply is read-only with no new backup; rollback restores exactly and keeps retired entry absent',t=>{
- const f=fixture(t),r=applyUpdate(f),after=fileMap(f.dataRoot),repeat=applyUpdate(f);
- assert.equal(repeat.alreadyApplied,true);assert.equal(repeat.readOnly,true);assert.equal(repeat.changedFiles,0);unchanged(f,after);
- const rollback=rollbackUpdate({backup:r.backup});assert.equal(rollback.legacyEntrypointRetired,true);restored(f);
- assert.equal(rollbackUpdate({backup:r.backup}).alreadyRolledBack,true);
- });
- test('tampered payload and unknown target entry stop before any installed writes',t=>{
- for(const scenario of ['payload','skill']){
- const f=fixture(t),p=scenario==='payload'?path.join(f.packageDir,'payload/SKILL.md'):path.join(f.targetDir,'SKILL.md');fs.appendFileSync(p,'unknown');const before=fileMap(f.dataRoot);
- assert.throws(()=>applyUpdate(f),{code:scenario==='payload'?'PACKAGE_HASH_MISMATCH':'UNKNOWN_TARGET_SKILL'});unchanged(f,before);
- }
- });
- test('unknown installed reference, manifest or runtime is never silently replaced',t=>{
- for(const name of ['references/workshop-analysis.md','FILES.sha256','scripts/ksf-secure.mjs']){
- const f=fixture(t);fs.appendFileSync(path.join(f.targetDir,name),'unknown');const before=fileMap(f.dataRoot);
- assert.throws(()=>applyUpdate(f),{code:name.startsWith('scripts/')?'RUNTIME_BASELINE_MISMATCH':'TARGET_BASELINE_MISMATCH'});unchanged(f,before);
- }
- });
- test('an existing new feedback file is protected even if its bytes happen to match the candidate',t=>{
- const f=fixture(t);fs.copyFileSync(path.join(f.packageDir,'payload/references/workshop-feedback.md'),path.join(f.targetDir,'references/workshop-feedback.md'));const before=fileMap(f.dataRoot);
- assert.throws(()=>applyUpdate(f),{code:'UNEXPECTED_EXISTING_FILE'});unchanged(f,before);
- });
- test('reactivated legacy entry, inconsistent research files and rewritten pinned runtime all stop',t=>{
- for(const scenario of ['entry','reference','runtime']){
- const f=fixture(t);
- if(scenario==='entry')fs.writeFileSync(path.join(f.libraryDir,'SKILL.md'),'new entry');
- if(scenario==='reference')fs.appendFileSync(path.join(f.libraryDir,'references/query-rules.md'),'unknown');
- if(scenario==='runtime'){fs.appendFileSync(path.join(f.libraryDir,'scripts/query_voc.py'),'unknown');seal(f.libraryDir);}
- const before=fileMap(f.dataRoot),code=scenario==='entry'?'LEGACY_ENTRY_REACTIVATED':scenario==='reference'?'RESEARCH_MANIFEST_MISMATCH':'RESEARCH_RUNTIME_MISMATCH';
- assert.throws(()=>applyUpdate(f),{code});unchanged(f,before);
- }
- });
- test('a resealed package cannot add an executable to the write set',t=>{
- const f=fixture(t);editRelease(f,r=>{r.candidate['scripts/ksf.mjs']='0'.repeat(64);});
- assert.throws(()=>applyUpdate(f),{code:'UNEXPECTED_WRITE_SET'});unchanged(f);
- });
- test('a directory junction or symbolic link in installed paths is rejected',t=>{
- const f=fixture(t),saved=path.join(f.dir,'saved-references');fs.renameSync(path.join(f.targetDir,'references'),saved);
- try{fs.symlinkSync(saved,path.join(f.targetDir,'references'),process.platform==='win32'?'junction':'dir');}catch(e){if(['EPERM','EACCES'].includes(e.code)){t.skip('symlinks unavailable');return;}throw e;}
- assert.throws(()=>applyUpdate(f),{code:'SYMLINK_PATH'});
- });
- test('injected partial failure restores previous bytes and removes only its new file',t=>{
- const f=fixture(t);let error;try{applyUpdate(f,{afterMutation:i=>{if(i===5)throw Object.assign(Error('injected'),{code:'INJECTED_FAILURE'});}});}catch(e){error=e;}
- assert.equal(error.code,'INJECTED_FAILURE');assert.equal(error.recoveryStatus,'FAILED_RESTORED');restored(f);
- assert.equal(fs.existsSync(path.join(f.targetDir,'references/workshop-feedback.md')),false);
- });
- test('a payload changed during apply is rejected and all earlier changes restored',t=>{
- const f=fixture(t);let error;try{applyUpdate(f,{afterMutation:i=>{if(i===0)fs.appendFileSync(path.join(f.packageDir,'payload/references/workshop-feedback.md'),'unknown');}});}catch(e){error=e;}
- assert.equal(error.code,'PACKAGE_CHANGED_DURING_APPLY');assert.equal(error.recoveryStatus,'FAILED_RESTORED');restored(f);
- });
- test('binding changes during apply are detected, not overwritten, and recovery is flagged for review',t=>{
- const f=fixture(t);let error;try{applyUpdate(f,{afterMutation:i=>{if(i===0)fs.appendFileSync(path.join(f.targetDir,'credential-binding.json'),'concurrent');}});}catch(e){error=e;}
- assert.equal(error.code,'BINDING_CHANGED');assert.equal(error.recoveryStatus,'FAILED_REQUIRES_REVIEW');
- assert.ok(fs.readFileSync(path.join(f.targetDir,'credential-binding.json'),'utf8').endsWith('concurrent'));
- assert.equal(digest(path.join(f.targetDir,'SKILL.md')),f.before['skills/ksf-workshop-runtime/SKILL.md']);
- });
- test('a concurrent edit to an owned updated file is not overwritten by automatic recovery',t=>{
- const f=fixture(t);let error;try{applyUpdate(f,{afterMutation:i=>{if(i===0){fs.appendFileSync(path.join(f.targetDir,'references/workshop-analysis.md'),'concurrent');throw Object.assign(Error('injected'),{code:'INJECTED_FAILURE'});}}});}catch(e){error=e;}
- assert.equal(error.recoveryStatus,'FAILED_REQUIRES_REVIEW');assert.ok(fs.readFileSync(path.join(f.targetDir,'references/workshop-analysis.md'),'utf8').endsWith('concurrent'));
- });
- test('rollback refuses concurrent changes before restoring anything',t=>{
- const f=fixture(t),r=applyUpdate(f);fs.appendFileSync(path.join(f.targetDir,'SKILL.md'),'concurrent');const before=fileMap(f.dataRoot);
- assert.throws(()=>rollbackUpdate({backup:r.backup}),{code:'CONCURRENT_FILE_CHANGE'});unchanged(f,before);
- });
- test('verify detects changes to unchanged public files and preserved research files',t=>{
- for(const name of ['identity','research']){
- const f=fixture(t),r=applyUpdate(f);fs.appendFileSync(name==='identity'?path.join(f.targetDir,'references/identity.md'):path.join(f.libraryDir,'references/query-rules.md'),'concurrent');
- const before=fileMap(f.dataRoot);assert.throws(()=>verifyUpdate({backup:r.backup}),{code:name==='identity'?'CONCURRENT_FILE_CHANGE':'PRESERVED_RESEARCH_CHANGED'});unchanged(f,before);
- }
- });
- test('corrupt backups and escaped journal paths block rollback with no writes',t=>{
- for(const scenario of ['backup','path']){
- const f=fixture(t),r=applyUpdate(f),p=path.join(r.backup,'journal.json'),j=JSON.parse(fs.readFileSync(p));
- if(scenario==='backup')fs.appendFileSync(path.join(r.backup,j.rows[0].backupFile),'corrupt');
- else{j.rows[0].path=path.join(f.dir,'outside.md');fs.writeFileSync(p,JSON.stringify(j));}
- const before=fileMap(f.dataRoot);assert.throws(()=>rollbackUpdate({backup:r.backup}),{code:scenario==='backup'?'BACKUP_HASH_MISMATCH':'JOURNAL_PATH_ESCAPE'});unchanged(f,before);
- }
- });
|