// Captured deployed handlers in RAM only. No imports of the production client. import fs from 'node:fs'; import vm from 'node:vm'; import crypto from 'node:crypto'; import assert from 'node:assert/strict'; import {runWorkshop} from '../skills/ksf-workshop-runtime/scripts/ksf-workshop.mjs'; const output='work/S5-completion-isolated-simulation-20261103.json';assert.equal(fs.existsSync(output),false); const dir='work/S5-classroom-completion-20261103/'; const rb=JSON.parse(fs.readFileSync(dir+'runbook.json','utf8')); const template=JSON.parse(fs.readFileSync(dir+'01-project-complete.template.json','utf8')); const audit=JSON.parse(fs.readFileSync('work/S5-completion-live-preflight-20261103.json','utf8'));assert.equal(audit.ok,true); const sha=x=>crypto.createHash('sha256').update(x).digest('hex'),copy=structuredClone; function embedded(code,name){ const marker='runDatabaseFunction('+JSON.stringify(name)+',request,response,',at=code.lastIndexOf(marker)+marker.length;assert.ok(at>=marker.length); let depth=0,quoted=false,escaped=false; for(let i=at;i({__type:'Pointer',className,objectId}); const db=new Map(),rows=n=>{if(!db.has(n))db.set(n,[]);return db.get(n);};let serial=0; const equal=(a,b)=>JSON.stringify(a??null)===JSON.stringify(b??null); const store={ async transaction(fn){const before=copy(db);try{return await fn(this);}catch(e){db.clear();for(const [k,v]of before)db.set(k,v);throw e;}}, async lock(){},async get(name,id){return copy(rows(name).find(r=>r.objectId===id)??null);}, async list(name,{where={},afterId,limit=1000}={}){return copy(rows(name).filter(r=>Object.entries(where).every(([k,v])=>equal(r[k],v))).filter(r=>!afterId||r.objectId>afterId).sort((a,b)=>a.objectId.localeCompare(b.objectId)).slice(0,limit));}, async create(name,input){for(const k of Object.keys(input))assert.ok(data.tables[name][k],name+'.'+k);const r={...copy(input),objectId:'simComplete'+String(++serial).padStart(5,'0'),createdAt:new Date().toISOString(),updatedAt:new Date().toISOString()};rows(name).push(r);return copy(r);}, async update(name,id,input){for(const k of Object.keys(input))assert.ok(data.tables[name][k],name+'.'+k);const r=rows(name).find(x=>x.objectId===id);assert.ok(r);Object.assign(r,copy(input),{updatedAt:new Date().toISOString()});return copy(r);} }; rows('KfSession').push({objectId:sessionId,dataMode:'live',workshopKey:'isolated-completion-workshop',stageCode:'S5',groupId:'',meta:{ownerId:actor.id,attribution}}); rows('KfProject').push({objectId:projectId,dataMode:'live',workshopKey:'isolated-completion-workshop',stageCode:'S5',stage:'S5 场景与卖点设计',status:'workshop',progressState:'in_progress',ownerSession:sessionId,sessionRef:ptr('KfSession',sessionId),currentConceptRef:ptr('KfConcept',conceptId),artifactCount:3,groupId:'',meta:{ownerId:actor.id,participantKey:attribution.participantKey,attribution,pendingQuestions:copy(rb.pendingQuestions)}}); rows('KfConcept').push({objectId:conceptId,projectRef:ptr('KfProject',projectId),version:1,reviewState:'pending',specification:{netWeightG:null,noodleWeightG:null,packCount:null,currentSku:null},productionSteps:[]}); for(const [i,s]of ['S3','S4','S5'].entries())rows('KfArtifact').push({objectId:'simReport'+i,stageCode:s,projectRef:ptr('KfProject',projectId),version:1,rawText:'unchanged report '+s,meta:{workshop:{pendingQuestions:copy(i===2?rb.pendingQuestions:rb.pendingQuestions.slice(0,8)),formalConfirmation:false}},dashboardPublishedAt:null}); for(const [i,t]of ['project_created','task_submitted','stage_changed','task_submitted','stage_changed','concept_saved','task_submitted'].entries())rows('KfStageEvent').push({objectId:'simOldEvent'+i,eventType:t,sequence:i+1,projectRef:ptr('KfProject',projectId)}); const initial=copy(db),reset=()=>{db.clear();for(const [k,v]of copy(initial))db.set(k,v);}; const apps=new Map();for(const [name,impl]of modules){const app=impl.createApplication({store,crypto,Parse:{},data,settings:{},files:{}});impl.attachWorkshopHandlers(app,{store,data});apps.set(name,app);} const call=async(name,params)=>copy((await (apps.get(name)||apps.get('ksfTransitionProject')).dispatch(name,copy(params),actor)).data); const input={...copy(template),sessionId,projectId},statusInput={sessionId,projectId}; const checks=[],calls=[];const check=(name,fn)=>{step=name;fn();checks.push({name,passed:true});}; const execute=async(i,task)=>{const s=rb.steps[i];const result=await runWorkshop(s.command,copy(task),{requestId:s.requestId,agentProfile,call});calls.push({step:s.step,command:s.command,requestId:s.requestId,ok:true});return result;}; try{ for(const control of ['missing-S5-report','missing-concept','not-S5']){ reset();if(control==='missing-S5-report')rows('KfArtifact').splice(2,1);else if(control==='missing-concept')delete rows('KfProject')[0].currentConceptRef;else rows('KfProject')[0].stageCode='S4'; const p={...copy(input),requestId:'isolated-completion-negative-'+control,...(control==='not-S5'?{toStage:'S4'}:{})};const before=copy(db);let code; try{await call('ksfTransitionProject',p);}catch(e){code=e.code;} check(control+' refuses completion without any mutation',()=>{assert.equal(code,control==='not-S5'?'VALIDATION_FAILED':'STAGE_REQUIREMENTS_NOT_MET');assert.deepEqual(db,before);assert.equal((db.get('KsfOperation')||[]).length,0);}); } reset();const before=await execute(0,statusInput); check('precheck has S5 in progress, three reports, one concept, seven events and thirteen questions',()=>{assert.equal(before.project.stageCode,'S5');assert.equal(before.project.progressState,'in_progress');assert.equal(before.artifacts.length,3);assert.equal(before.concept.objectId,conceptId);assert.equal(before.events.length,7);assert.deepEqual(before.project.meta.pendingQuestions,rb.pendingQuestions);}); const completed=await execute(1,input),final=await execute(2,statusInput); check('one completion updates progress and emits exact eighth event without approval',()=>{ assert.equal(completed.projectId,projectId);assert.equal(completed.stageCode,'S5');assert.equal(completed.progressState,'completed');assert.equal(completed.enterpriseApproval,false); assert.equal(final.project.progressState,'completed');assert.equal(final.project.status,'workshop');assert.equal(final.events.length,8);const ev=final.events[7];assert.equal(ev.objectId,completed.eventId);assert.equal(ev.eventType,'completed');assert.equal(ev.sequence,8);assert.equal(ev.fromStage,'S5');assert.equal(ev.toStage,'S5');assert.equal(ev.note,template.note); }); check('reports, concept, formula state and thirteen questions remain unchanged',()=>{ assert.deepEqual(rows('KfArtifact'),initial.get('KfArtifact'));assert.deepEqual(rows('KfConcept'),initial.get('KfConcept'));assert.deepEqual(final.formulaItems,[]);assert.deepEqual(final.project.meta.pendingQuestions,rb.pendingQuestions);assert.equal(final.artifacts.length,3);assert.deepEqual(final.events.slice(0,7),initial.get('KfStageEvent'));assert.equal(rows('KfSource').length,0); }); const snapshot=copy(db);const replay=await call('ksfTransitionProject',{...copy(input),requestId:rb.steps[1].requestId}); check('RAM-only replay of exact key returns same receipt without a second event',()=>{assert.deepEqual(replay,completed);assert.deepEqual(db,snapshot);assert.equal(rows('KsfOperation').length,1);}); let duplicateCode;try{await call('ksfTransitionProject',{...copy(input),requestId:'isolated-completion-different-key'});}catch(e){duplicateCode=e.code;} check('different key after completed is refused without another write',()=>{assert.equal(duplicateCode,'VERSION_CONFLICT');assert.deepEqual(db,snapshot);assert.equal(rows('KsfOperation').length,1);}); check('three CLI steps, one committed operation and no new business entities',()=>{ assert.equal(calls.length,3);assert.deepEqual(calls.map(c=>c.requestId),rb.steps.map(s=>s.requestId));assert.equal(rows('KsfOperation').length,1);assert.equal(rows('KsfOperation')[0].requestId,rb.steps[1].requestId); for(const n of ['KfOpportunity','KfFormulaItem','KfProduct','KfSku','KfMaterial','KfProcess'])assert.equal(rows(n).length,0); }); const r={ok:true,at:new Date().toISOString(),simulatedOnly:true,productionCalls:0,productionWrites:0,checksPassed:checks.length,checks,calls,deployedCode:proof, negativeControls:['S5 report required','current concept required','only S5 may complete'],idempotencyControls:['Same-key replay in RAM only','Different key after completed refused'], expectedAfter:rb.expectedAfter,limits:['Synthetic identity and RAM store, not a production transaction or current-message identity test.','Exact-key replay here is an isolated test; production runbook still forbids automatic retries.']}; fs.writeFileSync(output,JSON.stringify(r,null,2)+'\n',{flag:'wx'});console.log(JSON.stringify({ok:true,checksPassed:checks.length,commandSteps:3,negativeControls:3,idempotencyControls:2,productionCalls:0,productionWrites:0})); }catch(error){const r={ok:false,step,checksPassed:checks.length,commandsCompleted:calls.length,errorCode:'S5_COMPLETION_SIMULATION_STOPPED',failureCode:error.code??null,checkLine:error.stack?.match(/simulate-S5-completion\.mjs:(\d+):/)?.[1]??null,productionCalls:0,productionWrites:0};fs.writeFileSync(output,JSON.stringify(r,null,2)+'\n',{flag:'wx'});console.error(JSON.stringify(r));process.exitCode=1;}