verify-development-package.mjs 4.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465
  1. /** Exercise the exact release bytes in isolated local directories, never the production root. */
  2. import fs from 'node:fs';
  3. import path from 'node:path';
  4. import crypto from 'node:crypto';
  5. import assert from 'node:assert/strict';
  6. import {createServer} from 'node:http';
  7. import {spawn} from 'node:child_process';
  8. import {pathToFileURL} from 'node:url';
  9. const root=path.resolve('artifacts/ksf-development-references-20261108-r15/package');
  10. const work=path.resolve('work/ksf-development-references-20261108-r15');
  11. const target=fs.mkdtempSync(path.join(work,'publisher-check-'));
  12. const sha=b=>crypto.createHash('sha256').update(b).digest('hex');
  13. const manifest=JSON.parse(fs.readFileSync(path.join(root,'deploy/industry-ui-files.json')));
  14. const {publishFiles}=await import(pathToFileURL(path.join(root,'scripts/publish-industry-ui.mjs')));
  15. const {rollbackFiles}=await import(pathToFileURL(path.join(root,'scripts/rollback-ui.mjs')));
  16. const options={sourceDir:path.join(root,'payload'),manifestPath:path.join(root,'deploy/industry-ui-files.json'),targetDir:target};
  17. const receipt={ok:false,checks:[],testedFilesSha256:{},productionWrites:0,paidCalls:0,localFilesystemOnly:true};
  18. const check=(name,condition)=>{assert.ok(condition,name);receipt.checks.push(name);};
  19. for(const name of ['index.html',...manifest.files.filter(f=>!f.allowMissing).map(f=>f.path)]){
  20. const dest=path.join(target,name);fs.mkdirSync(path.dirname(dest),{recursive:true});fs.copyFileSync(path.join(work,'baseline',name),dest);
  21. }
  22. let server;
  23. try{
  24. const dry=publishFiles(options);check('dry-plan-six-no-write',dry.readOnly&&dry.changes.length===6&&dry.backup===null);
  25. for(const f of manifest.files.filter(f=>f.allowMissing))assert.ok(!fs.existsSync(path.join(target,f.path)));
  26. const apply=publishFiles({...options,apply:true});check('apply-six-files',apply.filesystemVerified&&apply.changes.length===6);
  27. const noop=publishFiles({...options,apply:true});check('idempotent-no-op',noop.changes.length===0&&noop.backup===null);
  28. server=createServer(async(req,res)=>{
  29. if(req.url==='/api/library'){
  30. let body='';for await(const chunk of req)body+=chunk;
  31. const parsed=JSON.parse(body);assert.equal(parsed.op,'summary');
  32. const upstream=await fetch('https://kangshifu.fmode.cn/api/library',{method:'POST',headers:{'content-type':'application/json'},body});
  33. res.writeHead(upstream.status,{'content-type':'application/json'});res.end(await upstream.text());return;
  34. }
  35. const name=new URL(req.url,'http://localhost').pathname.slice(1);
  36. const file=path.resolve(target,name);
  37. if(!file.startsWith(target+path.sep)||!fs.existsSync(file)){res.writeHead(404);res.end();return;}
  38. res.writeHead(200,{'content-type':file.endsWith('.json')?'application/json':file.endsWith('.js')?'text/javascript':'text/html'});res.end(fs.readFileSync(file));
  39. });
  40. await new Promise(resolve=>server.listen(0,'127.0.0.1',resolve));
  41. const origin='http://127.0.0.1:'+server.address().port;
  42. const verify=await new Promise((resolve,reject)=>{
  43. const child=spawn(process.execPath,[path.join(root,'scripts/verify-public.mjs'),'--origin',origin],{windowsHide:true});
  44. let out='',err='';child.stdout.on('data',b=>out+=b);child.stderr.on('data',b=>err+=b);child.on('error',reject);
  45. child.on('close',code=>resolve({code,out,err}));
  46. });
  47. const publicCheck=JSON.parse(verify.out);check('isolated-http-readback-with-live-library',verify.code===0&&publicCheck.ok);
  48. fs.writeFileSync(path.join(root,'evidence/isolated-public-check.json'),JSON.stringify(publicCheck,null,2)+'\n');
  49. const rdry=rollbackFiles({targetDir:target,backupDir:apply.backup});check('rollback-dry-six-files',rdry.readOnly&&rdry.changes.length===6);
  50. const restored=rollbackFiles({targetDir:target,backupDir:apply.backup,apply:true});check('rollback-restores-baseline',restored.filesystemVerified);
  51. check('rollback-noop',rollbackFiles({targetDir:target,backupDir:apply.backup}).changes.length===0);
  52. const corrupt=path.join(target,'src/pages/view-industry.js');fs.appendFileSync(corrupt,'\n// external change\n');
  53. assert.throws(()=>publishFiles(options),{code:'UNKNOWN_TARGET_CONTENT'});check('unknown-target-stops',true);
  54. receipt.ok=true;
  55. for(const dir of ['payload','scripts','test','test-support','deploy']){
  56. for(const entry of fs.readdirSync(path.join(root,dir),{recursive:true,withFileTypes:true})){
  57. if(entry.isFile()){const file=path.join(entry.parentPath,entry.name),name=path.relative(root,file).replaceAll('\\','/');receipt.testedFilesSha256[name]=sha(fs.readFileSync(file));}
  58. }
  59. }
  60. }finally{
  61. if(server)await new Promise(r=>server.close(r));
  62. fs.writeFileSync(path.join(root,'evidence/package-acceptance.json'),JSON.stringify(receipt,null,2)+'\n');
  63. console.log(JSON.stringify(receipt));
  64. }