verify-live-S2-template.mjs 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869
  1. // Validate the real S2 template through the save/read path in isolated memory.
  2. import fs from 'node:fs';
  3. import crypto from 'node:crypto';
  4. import assert from 'node:assert/strict';
  5. import {createApplication} from '../server/src/core.mjs';
  6. import {attachWorkshopHandlers} from '../server/src/workshop.mjs';
  7. import {runWorkshop} from '../skills/ksf-workshop-runtime/scripts/ksf-workshop.mjs';
  8. const data=JSON.parse(fs.readFileSync('server/references/runtime-data.json','utf8'));
  9. const templateBytes=fs.readFileSync('work/first-live-S2-template.json'),template=JSON.parse(templateBytes);
  10. const sha=x=>crypto.createHash('sha256').update(x).digest('hex');
  11. assert.equal(template.sessionId,null);assert.equal(template.stageCode,'S2');assert.equal(template.version,1);
  12. assert.equal(template.artifactType,'pain_list');assert.equal(template.taskKey,'KSF-真实资料-20261102-01:S2');
  13. assert.equal(template.sourceIds.length,4);assert.equal(template.pendingQuestions.length,6);
  14. assert.equal(template.projectId,undefined);assert.equal(template.opportunity,undefined);
  15. const rows={},copy=structuredClone;let seq=0;
  16. const store={
  17. async transaction(fn){const before=copy(rows);try{return await fn(this);}catch(e){for(const k of Object.keys(rows))delete rows[k];Object.assign(rows,before);throw e;}},
  18. async lock(){},
  19. async get(name,id){return copy(rows[name]?.find(r=>r.objectId===id)||null);},
  20. async list(name,{where={},afterId,limit=200}={}){return copy((rows[name]||[]).filter(r=>Object.entries(where).every(([k,v])=>JSON.stringify(r[k]??null)===JSON.stringify(v))).filter(r=>!afterId||r.objectId>afterId).sort((a,b)=>a.objectId.localeCompare(b.objectId)).slice(0,limit));},
  21. async create(name,fields){for(const k of Object.keys(fields))assert.ok(data.tables[name][k],name+'.'+k);const row={...copy(fields),objectId:'fixture'+String(++seq).padStart(7,'0'),createdAt:new Date().toISOString(),updatedAt:new Date().toISOString()};(rows[name]??=[]).push(row);return copy(row);},
  22. async update(name,id,fields){for(const k of Object.keys(fields))assert.ok(data.tables[name][k],name+'.'+k);const row=rows[name]?.find(r=>r.objectId===id);assert.ok(row);Object.assign(row,copy(fields));return copy(row);}
  23. };
  24. const actor={id:'local-s2-validation',kind:'service',scopes:['ingest:write','data:read'],projectIds:['*']};
  25. // Local fixtures only: no production identities or endpoint credentials enter this script.
  26. const profile={key:'agent-account:'+'a'.repeat(64),name:'本地S2校验夹具',identityBasis:'runtime_config',role:'workshop',runtimePlatform:'hermes',channel:'wecom',account:{platform:'wecom',accountId:'fixture-only'}};
  27. rows.KfSource=template.sourceIds.map((id,index)=>{
  28. const key='cq-0'+(index+1),input=JSON.parse(fs.readFileSync('work/consumer-quote-inputs-20261102/'+key+'.input.json','utf8'));
  29. return {...copy(input.source),objectId:id,contentHash:sha(input.source.rawText),schemaVersion:'ksf.v0.7',externalKey:'fixture-source-'+key,reviewState:'pending',recordState:'active',ingestedAt:'2026-11-02T00:00:00Z',meta:{ownerId:actor.id}};
  30. });
  31. const sourcesBefore=copy(rows.KfSource);
  32. const app=createApplication({store,crypto,Parse:{},data,settings:{signingSecret:'local-verification-only'},files:{}});
  33. attachWorkshopHandlers(app,{store,data});
  34. const call=async(name,p)=>(await app.dispatch(name,copy(p),actor)).data;
  35. const session=await call('ksfStartWorkshopSession',{requestId:'fixture-s2-start',workshopKey:'fixture-s2-validation',dataMode:'live',groupId:'',
  36. participant:{key:'fixture-participant',name:'本地校验',identityBasis:'participant_registration'},
  37. agent:{key:profile.key,name:profile.name,identityBasis:profile.identityBasis,role:profile.role,runtimePlatform:profile.runtimePlatform,channel:profile.channel,account:profile.account},
  38. conversation:{key:'fixture-conversation',channel:'wecom'}});
  39. const task={...copy(template),sessionId:session.sessionId};
  40. assert.deepEqual(Object.keys(task).filter(k=>JSON.stringify(task[k])!==JSON.stringify(template[k])),['sessionId']);
  41. const result=await runWorkshop('workshop-save',task,{requestId:'fixture-s2-save',agentProfile:profile,call});
  42. assert.equal(result.dataMode,'live');assert.equal(result.stageCode,'S2');assert.equal(result.version,1);
  43. assert.equal(result.boardVisible,true);assert.equal(result.formalReportPublished,false);assert.equal(result.employeeConfirmed,false);
  44. assert.equal(result.opportunityId,null);assert.equal(result.projectId,null);
  45. const read=(await runWorkshop('workshop-read',{className:'KfArtifact',objectId:result.artifactId},{requestId:'fixture-s2-read',call})).record;
  46. for(const field of ['rawText','title','version','stageCode','artifactType'])assert.deepEqual(read[field],task[field]);
  47. assert.equal(read.dashboardState,'workshop');assert.equal(read.ownerSession,session.sessionId);
  48. assert.deepEqual(read.meta.workshop.sourceIds,template.sourceIds);assert.deepEqual(read.meta.workshop.pendingQuestions,template.pendingQuestions);
  49. assert.deepEqual(read.report.executive.slice(1),template.report.executive);
  50. assert.deepEqual(read.report.chapters,template.report.chapters);assert.deepEqual(read.report.evidence,template.report.evidence);
  51. assert.equal(read.evidenceCount,4);assert.equal(read.sources.length,4);
  52. for(const entry of Object.values(read.report.evidence)){
  53. const origin=rows.KfSource.find(x=>x.objectId===entry.sourceId);assert.ok(origin);
  54. for(const quote of entry.quotes)assert.ok(origin.rawText.includes(quote.text));
  55. }
  56. assert.deepEqual(rows.KfSource.slice(0,4),sourcesBefore);
  57. assert.equal(rows.KfArtifact.length,1);assert.equal(rows.KfSource.length,5);
  58. assert.equal((await store.get('KfSession',session.sessionId)).stageCode,'S2');
  59. for(const name of ['KfOpportunity','KfProject','KfProduct','KfCategory','KfConcept','KfEvidence'])assert.equal((rows[name]??[]).length,0,name);
  60. const publicBoard=await app.dispatch('ksfGetPublicBoard',{},null);
  61. assert.ok(publicBoard.data.artifact.some(x=>x.objectId===result.artifactId&&x.stageCode==='S2'&&x.dataMode==='live'));
  62. const verified={ok:true,operation:'live-S2-template-validated-locally',at:new Date().toISOString(),templateSha256:sha(templateBytes),
  63. scope:'real backend/workshop wrapper, isolated memory with explicit local fixtures',quoteCount:4,sourceCount:4,pendingQuestionCount:6,
  64. saveAndReadbackMatched:true,publicBoardDataVisible:true,sourceContentPreserved:true,onlySessionIdChanged:true,
  65. newLocalArtifacts:1,newLocalReportSources:1,newLocalOpportunities:0,newLocalProjects:0,
  66. networkRequests:0,productionBusinessWrites:0,targetSaved:false,browserVerified:false};
  67. const output='work/first-live-S2-local-check.json';assert.equal(fs.existsSync(output),false,'Retain previous check');
  68. fs.writeFileSync(output,JSON.stringify(verified,null,2)+'\n',{flag:'wx'});console.log(JSON.stringify(verified,null,2));