simulate-S4-classroom-next.mjs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. // Executes the captured production handler definitions against local in-memory
  2. // storage only. No platform-client import and no network or production writes.
  3. import fs from 'node:fs';
  4. import vm from 'node:vm';
  5. import crypto from 'node:crypto';
  6. import assert from 'node:assert/strict';
  7. import {runWorkshop} from '../skills/ksf-workshop-runtime/scripts/ksf-workshop.mjs';
  8. const output='work/S4-classroom-isolated-simulation-20261103-02.json';
  9. assert.equal(fs.existsSync(output),false,'Preserve prior simulation');
  10. const base='work/S4-classroom-next-20261103/';
  11. const audit=JSON.parse(fs.readFileSync('work/S4-classroom-contract-check-20261103-03.json','utf8'));
  12. assert.equal(audit.ok,true);
  13. const sha=value=>crypto.createHash('sha256').update(value).digest('hex');
  14. const copy=structuredClone;
  15. const proof=[],calls=[],checks=[];
  16. const okay=(name,fn)=>{fn();checks.push({name,passed:true});};
  17. function readEmbeddedData(code,name){
  18. const marker='runDatabaseFunction('+JSON.stringify(name)+',request,response,';
  19. const at=code.lastIndexOf(marker)+marker.length;assert.ok(at>=marker.length);
  20. let depth=0,quoted=false,escaped=false;
  21. for(let i=at;i<code.length;i++){
  22. const ch=code[i];
  23. if(quoted){if(escaped)escaped=false;else if(ch==='\\')escaped=true;else if(ch==='"')quoted=false;continue;}
  24. if(ch==='"')quoted=true;
  25. else if(ch==='{')depth++;
  26. else if(ch==='}'&&--depth===0)return JSON.parse(code.slice(at,i+1));
  27. }
  28. throw Error('Embedded data missing');
  29. }
  30. const modules=new Map();
  31. let data;
  32. for(const name of ['ksfSaveWorkshopTask','ksfCreateProjectFromOpportunity','ksfTransitionProject','ksfGetWorkshopProject']){
  33. const code=fs.readFileSync('work/S4-deployed-'+name+'-20261103.js','utf8');
  34. const expected=audit.deployedCodeChecks.find(x=>x.name===name);assert.equal(sha(code),expected.sha256);
  35. const runtime=readEmbeddedData(code,name);
  36. if(data)assert.deepEqual(runtime,data);else data=runtime;
  37. const handlers=vm.runInNewContext(code+'\n;({createApplication,attachWorkshopHandlers});',{
  38. Buffer,structuredClone,URL,URLSearchParams,TextEncoder,TextDecoder
  39. },{timeout:5000});
  40. modules.set(name,handlers);proof.push({name,sha256:sha(code)});
  41. }
  42. const actor={id:'isolated-service-only',kind:'service',scopes:['ingest:write','data:read','source:write'],projectIds:['*']};
  43. const attribution={agentKey:'agent-registration:11111111-1111-4111-8111-111111111111',agentName:'隔离验证助手',agentKeyOrigin:'runtime_config',
  44. participantKey:'isolated-participant',participantName:'隔离验证参与者',channel:'wecom',agentRuntimePlatform:'hermes',agentChannel:'wecom'};
  45. const agentProfile={key:attribution.agentKey,name:attribution.agentName,identityBasis:'runtime_config',role:'workshop',runtimePlatform:'hermes',channel:'wecom'};
  46. const db=new Map(),copyRow=value=>value==null?null:copy(value);
  47. let sequence=0;
  48. const rows=name=>{if(!db.has(name))db.set(name,[]);return db.get(name);};
  49. const equal=(a,b)=>JSON.stringify(a??null)===JSON.stringify(b??null);
  50. const store={
  51. async transaction(fn){const before=copy(db);try{return await fn(this);}catch(error){db.clear();for(const [k,v]of before)db.set(k,v);throw error;}},
  52. async lock(){},
  53. async get(name,id){return copyRow(rows(name).find(x=>x.objectId===id));},
  54. async list(name,{where={},afterId,limit=1000}={}){return copy(rows(name).filter(row=>Object.entries(where).every(([k,v])=>equal(row[k],v))).filter(row=>!afterId||row.objectId>afterId).sort((a,b)=>a.objectId.localeCompare(b.objectId)).slice(0,limit));},
  55. async create(name,input){for(const key of Object.keys(input))assert.ok(data.tables[name][key],name+'.'+key);
  56. const row={...copy(input),objectId:'sim'+String(++sequence).padStart(7,'0'),createdAt:new Date().toISOString(),updatedAt:new Date().toISOString()};rows(name).push(row);return copy(row);},
  57. async update(name,id,input){for(const key of Object.keys(input))assert.ok(data.tables[name][key],name+'.'+key);
  58. const row=rows(name).find(x=>x.objectId===id);assert.ok(row);Object.assign(row,copy(input),{updatedAt:new Date().toISOString()});return copy(row);}
  59. };
  60. // Seed a synthetic actor/session and accepted original source text only in RAM.
  61. // The null ID fields of the delivered templates are the only mutated inputs.
  62. rows('KfSession').push({objectId:'simSession01',dataMode:'live',workshopKey:'KSF-真实资料-20261102-01',stageCode:'S3',studentName:attribution.participantName,groupId:'',meta:{ownerId:actor.id,attribution}});
  63. const publicRows=JSON.parse(fs.readFileSync('work/S4-public-record-inspection-20261103.json','utf8')).records;
  64. for(const row of publicRows.filter(x=>x.className==='KfSource'))rows('KfSource').push({...copy(row.record),meta:{ownerId:actor.id}});
  65. const previous=JSON.parse(fs.readFileSync('work/first-live-S3-v2-template.json','utf8'));
  66. const originalIds={'consumer-cq-03':'cq-03','consumer-cq-04':'cq-04'};
  67. for(const [key,id]of Object.entries(originalIds)){
  68. const input=JSON.parse(fs.readFileSync('work/consumer-quote-inputs-20261102/'+id+'.input.json','utf8'));
  69. rows('KfSource').push({...copy(input.source),objectId:previous.report.evidence[key].sourceId,meta:{ownerId:actor.id}});
  70. }
  71. const beforeSourceRows=copy(rows('KfSource'));
  72. const apps=new Map();
  73. for(const [name,impl]of modules){
  74. const app=impl.createApplication({store,crypto,Parse:{},data,settings:{signingSecret:'isolated-simulation-only'},files:{}});
  75. impl.attachWorkshopHandlers(app,{store,data});apps.set(name,app);
  76. }
  77. const general=apps.get('ksfSaveWorkshopTask');
  78. async function call(name,params){
  79. const app=apps.get(name)||general;
  80. const result=await app.dispatch(name,copy(params),actor);
  81. return copy(result.data);
  82. }
  83. async function execute(command,input,requestId){
  84. const result=await runWorkshop(command,copy(input),{requestId,agentProfile,call});
  85. calls.push({command,requestId,ok:true});return result;
  86. }
  87. const getTemplate=name=>JSON.parse(fs.readFileSync(base+name,'utf8'));
  88. const bound=(name,ids)=>{const input=getTemplate(name),result={...copy(input),...ids};for(const key of Object.keys(ids))assert.equal(input[key],null);return result;};
  89. const prefix='KSF-真实资料-20261102-01',sessionId='simSession01';
  90. function verifyReport(row,template,projectId=null){
  91. assert.equal(row.reportType,'workshop_auxiliary');assert.equal(row.artifactSubtype,'classroom_task');assert.equal(row.artifactType,template.artifactType);
  92. for(const field of ['rawText','version','stageCode','reportSeriesKey'])assert.equal(row[field],field==='reportSeriesKey'?template.taskKey:template[field]);
  93. assert.deepEqual(row.meta.workshop.sourceIds,template.sourceIds);assert.deepEqual(row.meta.workshop.pendingQuestions,template.pendingQuestions);
  94. assert.equal(row.evidenceCount,4);assert.equal(row.ownerSession,sessionId);assert.equal(row.projectId??null,projectId);
  95. for(const field of ['chapters','evidence','method','sources','subtitle'])assert.deepEqual(row.report[field],template.report[field]);
  96. assert.deepEqual(row.report.executive.slice(1),template.report.executive);assert.equal(row.report.executive[0],'课堂过程成果,未经员工正式确认;待验证项不作为市场结论。');
  97. }
  98. let step='candidate save/read';
  99. try{
  100. const candidate=bound('01-candidate-save.template.json',{sessionId});
  101. const c=await execute('workshop-save',candidate,prefix+':S3-份量候选:save:v1');
  102. const artifact=(await execute('workshop-read',{className:'KfArtifact',objectId:c.artifactId},prefix+':S3-份量候选:read:v1')).record;
  103. okay('candidate report stored as workshop_auxiliary/classroom_task with opportunity_card artifactType',()=>verifyReport(artifact,candidate));
  104. const opp=(await execute('workshop-read',{className:'KfOpportunity',objectId:c.opportunityId},prefix+':portion-fit:read:v1')).record;
  105. okay('opportunity handler supplies hypothesis/candidate/version/series and report link',()=>{
  106. assert.equal(opp.claimKind,'hypothesis');assert.equal(opp.status,'candidate');assert.equal(opp.version,1);
  107. assert.equal(opp.opportunitySeriesKey,candidate.opportunity.key);assert.deepEqual(opp.linkedArtifacts,[c.artifactId]);
  108. });
  109. step='project create/status';
  110. const p=await execute('project-create',bound('02-project-create.template.json',{sessionId,opportunityId:c.opportunityId}),prefix+':portion-fit-project:create:v1');
  111. const projectId=p.projectId,statusInput={sessionId,projectId};
  112. const created=await execute('project-status',statusInput,prefix+':portion-fit-project:read-created:v1');
  113. okay('projectKey stored in meta, stage S3, no project artifacts, no enterprise approval',()=>{
  114. assert.equal(created.project.meta.projectKey,getTemplate('02-project-create.template.json').projectKey);
  115. assert.equal(created.project.stageCode,'S3');assert.equal(created.artifacts.length,0);assert.equal(p.enterpriseApproval,false);
  116. });
  117. okay('classroom project changes opportunity to approved with linked project only',()=>{
  118. const saved=rows('KfOpportunity').find(x=>x.objectId===c.opportunityId);assert.equal(saved.status,'approved');assert.equal(saved.linkedProject,projectId);
  119. });
  120. // Negative branch is local-only and uses a simulation-specific request ID.
  121. step='negative control: refuse premature transition';
  122. let premature;
  123. try{await call('ksfTransitionProject',{sessionId,projectId,toStage:'S4',state:'in_progress',note:'negative control only',requestId:'isolated-premature-S4'});}catch(error){premature=error.code;}
  124. okay('S3 project artifact is necessary before entering S4',()=>assert.equal(premature,'STAGE_REQUIREMENTS_NOT_MET'));
  125. step='project S3 baseline/save/read';
  126. const s3input=bound('03-project-S3-save.template.json',{sessionId,projectId});
  127. const s3=await execute('workshop-save',s3input,prefix+':S3-项目基线:save:v1');
  128. const s3row=(await execute('workshop-read',{className:'KfArtifact',objectId:s3.artifactId},prefix+':S3-项目基线:read:v1')).record;
  129. okay('project S3 baseline keeps four quotes/eight questions and actual project reference',()=>verifyReport(s3row,s3input,projectId));
  130. step='transition/status';
  131. const moved=await execute('project-progress',bound('04-project-S4-progress.template.json',{sessionId,projectId}),prefix+':portion-fit-project:to-S4:v1');
  132. const underway=await execute('project-status',statusInput,prefix+':portion-fit-project:read-S4:v1');
  133. okay('project transition returns S4/in_progress and preserves S3 baseline',()=>{
  134. assert.equal(moved.stageCode,'S4');assert.equal(moved.progressState,'in_progress');assert.equal(moved.enterpriseApproval,false);
  135. assert.equal(underway.project.stageCode,'S4');assert.equal(underway.artifacts.length,1);
  136. });
  137. step='project S4 assessment/save/read';
  138. const s4input=bound('05-project-S4-save.template.json',{sessionId,projectId});
  139. const s4=await execute('workshop-save',s4input,prefix+':S4:save:v1');
  140. const s4row=(await execute('workshop-read',{className:'KfArtifact',objectId:s4.artifactId},prefix+':S4:read:v1')).record;
  141. okay('S4 assessment_report persists as artifactType alongside workshop_auxiliary reportType',()=>verifyReport(s4row,s4input,projectId));
  142. const final=await execute('project-status',statusInput,prefix+':portion-fit-project:read-final:v1');
  143. okay('final project remains S4/in_progress with S3/S4 reports and no concept/formula',()=>{
  144. assert.equal(final.project.stageCode,'S4');assert.equal(final.project.progressState,'in_progress');assert.equal(final.artifacts.length,2);
  145. assert.equal(final.concept,null);assert.equal(final.formulaItems.length,0);assert.equal(final.events.length,4);
  146. });
  147. okay('12 ordered commands use each delivered request ID exactly once',()=>{
  148. assert.equal(calls.length,12);assert.deepEqual(calls.map(x=>x.requestId),audit.checkedRequestIds);
  149. });
  150. okay('only three reports/sources, one candidate/project and five write audit records created in RAM',()=>{
  151. assert.equal(rows('KfArtifact').length,3);assert.equal(rows('KfOpportunity').length,1);assert.equal(rows('KfProject').length,1);assert.equal(rows('KsfOperation').length,5);
  152. assert.equal(rows('KfSource').length-beforeSourceRows.length,3);
  153. assert.equal(rows('KfProduct').length,0);assert.equal(rows('KfCategory').length,0);assert.equal(rows('KfConcept').length,0);
  154. assert.deepEqual(rows('KfSource').filter(x=>beforeSourceRows.some(y=>x.objectId===y.objectId)),beforeSourceRows);
  155. });
  156. const result={ok:true,at:new Date().toISOString(),scope:'Local RAM simulation using captured deployed command source and runtime schema; synthetic execution identity/session',
  157. productionCalls:0,productionWrites:0,browserVerified:false,templatesModified:false,checksPassed:checks.length,checks,calls,
  158. deployedCode:proof,simulatedOnly:true,negativeControl:'Premature S4 transition rejected before project S3 baseline',
  159. threeReportedDifferencesResolved:true,expectedFinal:{stageCode:'S4',progressState:'in_progress',reports:3,projectReports:2,opportunities:1,projects:1},
  160. limitations:['Does not exercise real platform database transaction/transport or current-message identity integration','Production writes/readbacks remain necessary; stop on uncertainty']};
  161. fs.writeFileSync(output,JSON.stringify(result,null,2)+'\n',{flag:'wx'});
  162. console.log(JSON.stringify({ok:true,checksPassed:checks.length,orderedCommandCount:calls.length,productionCalls:0,productionWrites:0,simulatedOnly:true,negativeControl:result.negativeControl,expectedFinal:result.expectedFinal},null,2));
  163. }catch(error){
  164. const result={ok:false,step,errorCode:'S4_CLASSROOM_SIMULATION_STOPPED',failureCode:error.code??null,checkLine:error.stack?.match(/simulate-S4-classroom-next\.mjs:(\d+):/)?.[1]??null,
  165. commandsCompleted:calls.length,checksPassed:checks.length,productionCalls:0,productionWrites:0,simulatedOnly:true};
  166. fs.writeFileSync(output,JSON.stringify(result,null,2)+'\n',{flag:'wx'});console.error(JSON.stringify(result,null,2));process.exitCode=1;
  167. }