workshop-quality-update.test.mjs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. import test from 'node:test';
  2. import assert from 'node:assert/strict';
  3. import fs from 'node:fs';
  4. import path from 'node:path';
  5. import os from 'node:os';
  6. import crypto from 'node:crypto';
  7. import {fileURLToPath} from 'node:url';
  8. import {planUpdate,publicPlan,applyUpdate,verifyUpdate,rollbackUpdate} from '../scripts/update-workshop-quality.mjs';
  9. const root=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..');
  10. const releaseDir=path.join(root,'docs/workshop-skill-release-20261109-r5');
  11. const digest=p=>crypto.createHash('sha256').update(fs.readFileSync(p)).digest('hex');
  12. const counts={r2:[17,14,3],r3:[16,14,2],r4:[9,8,1]};
  13. 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;}
  14. 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(''));}
  15. function fixture(t,version='r4'){
  16. const dir=fs.mkdtempSync(path.join(os.tmpdir(),'ksf-quality-update-test-'));
  17. t.after(()=>{const abs=fs.realpathSync(dir);assert.ok(abs.startsWith(fs.realpathSync(os.tmpdir())+path.sep)&&path.basename(abs).startsWith('ksf-quality-update-test-'));fs.rmSync(abs,{recursive:true});});
  18. 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');
  19. fs.mkdirSync(path.dirname(targetDir),{recursive:true});
  20. fs.cpSync(path.join(releaseDir,'rehearsal-baseline-'+version),targetDir,{recursive:true});
  21. fs.cpSync(path.join(releaseDir,'package'),packageDir,{recursive:true});
  22. // Public self-consistent fixture, not a claim of exact remote research state.
  23. fs.cpSync(path.join(root,'skills/ksf-voc-library'),libraryDir,{recursive:true});fs.unlinkSync(path.join(libraryDir,'SKILL.md'));seal(libraryDir);
  24. fs.writeFileSync(path.join(targetDir,'credential-binding.json'),'{"syntheticBinding":true}',{mode:0o600});
  25. fs.writeFileSync(path.join(targetDir,'fixture-private-envelope.json'),'{"fixtureOnly":true}',{mode:0o600});
  26. fs.mkdirSync(path.join(dataRoot,'ksf-workshop-state/agents'),{recursive:true});
  27. fs.writeFileSync(path.join(dataRoot,'ksf-workshop-state/agents/fixture.json'),'{"fixtureOnly":true}');
  28. return{dir,dataRoot,targetDir,libraryDir,packageDir,before:fileMap(dataRoot)};
  29. }
  30. function unchanged(f,before=f.before){assert.deepEqual(fileMap(f.dataRoot),before);}
  31. 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-research-backup-')))assert.ok(Object.hasOwn(f.before,n),n);}
  32. 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);}
  33. for(const version of Object.keys(counts)){
  34. test(version+' exact baseline dry/apply/verify/idempotence/rollback preserves private and runtime state',t=>{
  35. const f=fixture(t,version),[total,updates,creates]=counts[version];
  36. const dry=publicPlan(planUpdate(f));assert.equal(dry.baselineId,version);assert.equal(dry.readOnly,true);assert.equal(dry.changedFiles,total);
  37. assert.equal(dry.changes.filter(e=>e.action==='update').length,updates);assert.equal(dry.changes.filter(e=>e.action==='create').length,creates);unchanged(f);
  38. const result=applyUpdate(f),v=verifyUpdate({backup:result.backup});
  39. assert.equal(v.installedFilesVerified,41);assert.equal(v.changedFilesVerified,total);assert.equal(v.runtimeAndBindingPreserved,true);assert.equal(v.researchFilesUnchanged,true);
  40. assert.equal(v.agentLoaded,false);assert.equal(v.currentReadVerified,false);assert.equal(fs.readdirSync(result.backup).filter(n=>n.endsWith('.before')).length,updates);
  41. const release=JSON.parse(fs.readFileSync(path.join(f.packageDir,'release.json')));
  42. for(const[n,h]of Object.entries(release.candidate))assert.equal(digest(path.join(f.targetDir,n)),h,n);
  43. 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);
  44. assert.ok(!fs.readFileSync(path.join(result.backup,'journal.json'),'utf8').includes('syntheticBinding'));
  45. const after=fileMap(f.dataRoot),again=applyUpdate(f);assert.equal(again.alreadyApplied,true);assert.equal(again.changedFiles,0);unchanged(f,after);
  46. rollbackUpdate({backup:result.backup});restored(f);assert.equal(fs.existsSync(path.join(f.libraryDir,'SKILL.md')),false);
  47. assert.equal(rollbackUpdate({backup:result.backup}).alreadyRolledBack,true);
  48. });
  49. test(version+' partial failure after creating innovation reference restores prior state',t=>{
  50. const f=fixture(t,version),p=planUpdate(f),index=p.changes.findIndex(c=>c.name==='references/workshop-innovation-reasoning.md');assert.ok(index>=0);
  51. let error;try{applyUpdate(f,{afterMutation:i=>{if(i===index)throw Object.assign(Error('injected'),{code:'INJECTED_FAILURE'});}});}catch(e){error=e;}
  52. assert.equal(error.code,'INJECTED_FAILURE');assert.equal(error.recoveryStatus,'FAILED_RESTORED');restored(f);
  53. });
  54. }
  55. test('tampered payload is rejected before writing',t=>{const f=fixture(t);fs.appendFileSync(path.join(f.packageDir,'payload/SKILL.md'),'unknown');assert.throws(()=>applyUpdate(f),{code:'PACKAGE_HASH_MISMATCH'});unchanged(f);});
  56. test('unknown installed entry/reference/manifest/runtime are not overwritten',t=>{
  57. for(const name of ['SKILL.md','references/workshop-analysis.md','FILES.sha256','scripts/ksf-secure.mjs']){
  58. const f=fixture(t);fs.appendFileSync(path.join(f.targetDir,name),'unknown');const before=fileMap(f.dataRoot);
  59. assert.throws(()=>applyUpdate(f),{code:name==='SKILL.md'?'UNKNOWN_TARGET_SKILL':name.startsWith('scripts/')?'RUNTIME_BASELINE_MISMATCH':'TARGET_BASELINE_MISMATCH'});unchanged(f,before);
  60. }
  61. });
  62. test('existing untracked new reference is protected even when target bytes match',t=>{
  63. const f=fixture(t);fs.copyFileSync(path.join(f.packageDir,'payload/references/workshop-innovation-reasoning.md'),path.join(f.targetDir,'references/workshop-innovation-reasoning.md'));const before=fileMap(f.dataRoot);
  64. assert.throws(()=>applyUpdate(f),{code:'UNEXPECTED_EXISTING_FILE'});unchanged(f,before);
  65. });
  66. test('mixed baseline states are refused',t=>{
  67. const f=fixture(t,'r3');fs.copyFileSync(path.join(releaseDir,'rehearsal-baseline-r4/references/workshop-analysis.md'),path.join(f.targetDir,'references/workshop-analysis.md'));const before=fileMap(f.dataRoot);
  68. assert.throws(()=>applyUpdate(f),{code:'TARGET_BASELINE_MISMATCH'});unchanged(f,before);
  69. });
  70. test('legacy reactivation, inconsistent research reference and changed pinned query code stop',t=>{
  71. for(const scenario of ['entry','reference','runtime']){
  72. const f=fixture(t);
  73. if(scenario==='entry')fs.writeFileSync(path.join(f.libraryDir,'SKILL.md'),'entry');
  74. if(scenario==='reference')fs.appendFileSync(path.join(f.libraryDir,'references/query-rules.md'),'unknown');
  75. if(scenario==='runtime'){fs.appendFileSync(path.join(f.libraryDir,'scripts/query_voc.py'),'unknown');seal(f.libraryDir);}
  76. const before=fileMap(f.dataRoot);assert.throws(()=>applyUpdate(f),{code:scenario==='entry'?'LEGACY_ENTRY_REACTIVATED':scenario==='reference'?'RESEARCH_MANIFEST_MISMATCH':'RESEARCH_RUNTIME_MISMATCH'});unchanged(f,before);
  77. }
  78. });
  79. test('resealed malicious write set or unknown target is refused',t=>{
  80. for(const scenario of ['runtime','target']){
  81. const f=fixture(t);editRelease(f,r=>{r.candidate[scenario==='runtime'?'scripts/ksf.mjs':'SKILL.md']='0'.repeat(64);});
  82. assert.throws(()=>applyUpdate(f),{code:scenario==='runtime'?'UNEXPECTED_WRITE_SET':'UNKNOWN_RELEASE_TARGET'});unchanged(f);
  83. }
  84. });
  85. test('junction or symlink in installed tree is rejected',t=>{
  86. const f=fixture(t),saved=path.join(f.dir,'saved-references');fs.renameSync(path.join(f.targetDir,'references'),saved);
  87. 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;}
  88. assert.throws(()=>applyUpdate(f),{code:'SYMLINK_PATH'});
  89. });
  90. test('payload changed after preflight restores earlier mutations',t=>{
  91. 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'),'concurrent');}});}catch(e){error=e;}
  92. assert.equal(error.code,'PACKAGE_CHANGED_DURING_APPLY');assert.equal(error.recoveryStatus,'FAILED_RESTORED');restored(f);
  93. });
  94. test('binding change is preserved and recovery flagged for review',t=>{
  95. 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;}
  96. 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'));
  97. });
  98. test('concurrent edit to a newly updated file is not clobbered during failure restoration',t=>{
  99. const f=fixture(t),first=planUpdate(f).changes[0];let error;
  100. try{applyUpdate(f,{afterMutation:i=>{if(i===0){fs.appendFileSync(first.path,'concurrent');throw Object.assign(Error('injected'),{code:'INJECTED_FAILURE'});}}});}catch(e){error=e;}
  101. assert.equal(error.recoveryStatus,'FAILED_REQUIRES_REVIEW');assert.ok(fs.readFileSync(first.path,'utf8').endsWith('concurrent'));
  102. });
  103. test('rollback refuses later target edits without writes',t=>{
  104. const f=fixture(t),r=applyUpdate(f);fs.appendFileSync(path.join(f.targetDir,'SKILL.md'),'later');const before=fileMap(f.dataRoot);
  105. assert.throws(()=>rollbackUpdate({backup:r.backup}),{code:'CONCURRENT_FILE_CHANGE'});unchanged(f,before);
  106. });
  107. test('verify checks unchanged public and research content',t=>{
  108. for(const scenario of ['public','research']){const f=fixture(t),r=applyUpdate(f);fs.appendFileSync(scenario==='public'?path.join(f.targetDir,'references/identity.md'):path.join(f.libraryDir,'references/query-rules.md'),'later');const before=fileMap(f.dataRoot);
  109. assert.throws(()=>verifyUpdate({backup:r.backup}),{code:scenario==='public'?'CONCURRENT_FILE_CHANGE':'PRESERVED_RESEARCH_CHANGED'});unchanged(f,before);}
  110. });
  111. test('corrupt backup or escaped journal path blocks restoration',t=>{
  112. 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));
  113. if(scenario==='backup')fs.appendFileSync(path.join(r.backup,j.rows.find(e=>e.backupFile).backupFile),'corrupt');else{j.rows[0].path=path.join(f.dir,'outside.md');fs.writeFileSync(p,JSON.stringify(j));}
  114. const before=fileMap(f.dataRoot);assert.throws(()=>rollbackUpdate({backup:r.backup}),{code:scenario==='backup'?'BACKUP_HASH_MISMATCH':'JOURNAL_PATH_ESCAPE'});unchanged(f,before);}
  115. });