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 {deployIdentity} from '../scripts/deploy-identity.mjs'; const sha=x=>crypto.createHash('sha256').update(x).digest('hex'); function fixture(t){ const root=fs.mkdtempSync(path.join(os.tmpdir(),'ksf-identity-deploy-'));t.after(()=>{assert.equal(path.dirname(root),fs.realpathSync(os.tmpdir()));assert.ok(path.basename(root).startsWith('ksf-identity-deploy-'));fs.rmSync(root,{recursive:true});}); const functions=[{name:'ksfStartWorkshopSession',objectId:'yKpUay6RDG'},{name:'ksfGetIngestContract',objectId:'dWSrHp4FfQ'}],db={},puts=[];fs.mkdirSync(path.join(root,'dist')); const manifest={version:'ksf.identity-deployment.v1',appId:'FKieRbIoGt_main',functions:functions.map((x,i)=>{const prior='function previous'+i+'(){}',code='function next'+i+'(){}';db[x.objectId]={...x,path:'/'+x.name,code:prior};const file='dist/'+x.name+'.js';fs.writeFileSync(path.join(root,file),code);return{...x,path:'/'+x.name,file,beforeSha256:sha(prior),sha256:sha(code)};})}; fs.writeFileSync(path.join(root,'dist/identity-manifest.json'),JSON.stringify(manifest)); const request=async(method,route,body)=>{if(route==='/schemas')return{results:[{className:'KfSession',fields:{meta:{type:'Object'}},classLevelPermissions:{find:{}}}]};const id=route.split('/').at(-1);if(method==='PUT'){puts.push(id);db[id].code=body.code;}return structuredClone(db[id]);};return{root,db,puts,request,manifest}; } test('身份发布默认只读;仅更新固定两函数,复验结构权限且重放不写',async t=>{ const f=fixture(t),plan=await deployIdentity(f);assert.equal(plan.readOnly,true);assert.equal(f.puts.length,0);assert.equal(fs.existsSync(path.join(f.root,'evidence')),false); const result=await deployIdentity({...f,apply:true});assert.equal(result.ok,true);assert.deepEqual(f.puts,['yKpUay6RDG','dWSrHp4FfQ']);assert.equal(result.schemaAndPermissionsPreserved,true);assert.ok(fs.existsSync(result.backup)); f.puts.length=0;assert.ok((await deployIdentity({...f,apply:true})).functions.every(x=>x.action==='unchanged'));assert.equal(f.puts.length,0); }); test('未知线上版本、损坏包在写前停止;中途失败恢复已更新函数',async t=>{ const f=fixture(t),original=structuredClone(f.db);f.db.yKpUay6RDG.code='function externalChange(){}';await assert.rejects(()=>deployIdentity({...f,apply:true}),{code:'UNKNOWN_FUNCTION_VERSION'});assert.equal(f.puts.length,0);f.db.yKpUay6RDG=structuredClone(original.yKpUay6RDG); const file=path.join(f.root,f.manifest.functions[0].file),bytes=fs.readFileSync(file);fs.appendFileSync(file,'// broken');await assert.rejects(()=>deployIdentity({...f,apply:true}),{code:'PACKAGE_HASH_MISMATCH'});assert.equal(f.puts.length,0);fs.writeFileSync(file,bytes); let failed=false;const request=async(m,r,b)=>{if(m==='PUT'&&r.endsWith('/dWSrHp4FfQ')&&!failed){failed=true;throw Object.assign(Error('fixture'),{code:'REMOTE_UNCERTAIN'});}return f.request(m,r,b);}; await assert.rejects(()=>deployIdentity({...f,request,apply:true}),e=>{assert.equal(e.receipt.rollbackComplete,true);return true;});assert.deepEqual(f.db,original); }); test('失败回滚不覆盖其他维护者并发改动',async t=>{ const f=fixture(t);let failed=false;const request=async(m,r,b)=>{if(m==='PUT'&&r.endsWith('/dWSrHp4FfQ')&&!failed){failed=true;f.db.yKpUay6RDG.code='function concurrentOwnerChange(){}';throw Object.assign(Error('fixture'),{code:'REMOTE_UNCERTAIN'});}return f.request(m,r,b);}; await assert.rejects(()=>deployIdentity({...f,request,apply:true}),e=>{assert.equal(e.receipt.rollbackComplete,false);return true;});assert.equal(f.db.yKpUay6RDG.code,'function concurrentOwnerChange(){}'); });