verify-live-S3-v2-readback.mjs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. // Independent, read-only S3 v2 acceptance. Never calls start/save/import.
  2. import fs from 'node:fs';
  3. import crypto from 'node:crypto';
  4. import assert from 'node:assert/strict';
  5. import {invoke,classRows} from '../server/scripts/platform-client.mjs';
  6. import {loadPublicBoard} from '../src/services/workshop-data.js';
  7. const sessionId='btrFqkz0yw',artifactId='ZW1FB5zeHr',outputSourceId='G1Gzu0_sQV';
  8. const s1Id='G8IrkkDG7X',s2Id='b7NOby5ys0',s3v1Id='oiNxX7o27j';
  9. const templateBytes=fs.readFileSync('work/first-live-S3-v2-template.json');
  10. const template=JSON.parse(templateBytes);
  11. const s3v1Template=JSON.parse(fs.readFileSync('work/first-live-S3-template.json','utf8'));
  12. const s1Template=JSON.parse(fs.readFileSync('work/first-live-S1-v2-template.json','utf8'));
  13. const s2Template=JSON.parse(fs.readFileSync('work/first-live-S2-template.json','utf8'));
  14. const outputFile='work/live-S3-v2-readback-20261103.json';
  15. const saveRequestId='KSF-真实资料-20261102-01:S3:save:v2';
  16. const pointer=(className,objectId)=>({__type:'Pointer',className,objectId});
  17. const canonical=x=>x&&typeof x==='object'?Array.isArray(x)?'['+x.map(canonical).join(',')+']':'{'+Object.keys(x).sort().map(k=>JSON.stringify(k)+':'+canonical(x[k])).join(',')+'}':JSON.stringify(x);
  18. const sha=x=>crypto.createHash('sha256').update(x).digest('hex');
  19. const read=async(className,objectId)=>{
  20. const r=await invoke('ksfGetRecord',{className,objectId});
  21. assert.equal(r.http,200);assert.equal(r.body?.result?.ok,true);
  22. const row=r.body.result.data.record;assert.equal(row?.objectId,objectId);return row;
  23. };
  24. assert.equal(fs.existsSync(outputFile),false,'Preserve earlier acceptance');
  25. let check='template fingerprint and fixed scope';
  26. try{
  27. assert.equal(template.sessionId,null);assert.equal(template.taskKey,'KSF-真实资料-20261102-01:S3');
  28. assert.equal(sha(templateBytes),'41c32974519799103709fb124115ecd79a1ed2ef1b3b343c129e23cb2be9ca49');
  29. assert.equal(template.stageCode,'S3');assert.equal(template.version,2);assert.equal(template.artifactType,'opportunity_card');
  30. assert.equal('opportunity' in template,false);assert.equal('projectId' in template,false);
  31. assert.deepEqual(template.sourceIds,['2mzBadmvws','Pi04eTwTQ3','m1OQx9aynA','QdeMcyTTjX']);
  32. assert.equal(Object.keys(template.report.evidence).length,4);assert.equal(template.pendingQuestions.length,8);
  33. assert.equal(template.rawText.length,2554);
  34. assert.equal(template.rawText,s3v1Template.rawText);
  35. assert.deepEqual(template.report.chapters[2].sections[3],{kind:'table',title:'待补问题',data:{cols:['序号','待核实问题'],rows:template.pendingQuestions.map((q,i)=>[String(i+1),q])}});
  36. check='read saved records and operation history';
  37. const settled=await Promise.allSettled([
  38. read('KfSession',sessionId),read('KfArtifact',artifactId),read('KfArtifact',s1Id),read('KfArtifact',s2Id),
  39. read('KfSource',outputSourceId),
  40. classRows('KfArtifact',{workshopKey:'KSF-真实资料-20261102-01'}),
  41. classRows('KsfOperation',{operation:'workshop-task',requestId:saveRequestId}),
  42. classRows('KfOpportunity',{sessionRef:pointer('KfSession',sessionId)}),
  43. classRows('KfProject',{sessionRef:pointer('KfSession',sessionId)}),
  44. read('KfArtifact',s3v1Id)
  45. ]);
  46. const failures=settled.flatMap((r,i)=>r.status==='rejected'?[i]:[]);
  47. assert.deepEqual(failures,[],'Read group failure indexes');
  48. const [session,artifact,s1,s2,generated,history,operations,opportunities,projects,s3v1]=settled.map(r=>r.value);
  49. check='artifact fields and template';
  50. assert.equal(artifact.title,template.title);assert.equal(artifact.rawText,template.rawText);
  51. assert.equal(artifact.version,2);assert.equal(artifact.stageCode,'S3');assert.equal(artifact.artifactType,'opportunity_card');
  52. assert.equal(artifact.reportSeriesKey,template.taskKey);assert.equal(artifact.reportType,'workshop_auxiliary');
  53. assert.equal(artifact.schemaVersion,'ksf.v0.7');assert.equal(artifact.dataMode,'live');assert.equal(artifact.recordState,'active');
  54. assert.equal(artifact.dashboardState,'workshop');assert.equal(artifact.ownerSession,sessionId);
  55. assert.deepEqual(artifact.sessionRef,pointer('KfSession',sessionId));assert.deepEqual(artifact.sourceRef,pointer('KfSource',outputSourceId));
  56. assert.equal(artifact.projectRef??null,null);assert.equal(artifact.previousVersionRef??null,null);
  57. assert.equal(artifact.category??null,null);
  58. assert.deepEqual(artifact.tags,['拌面','课堂过程成果']);
  59. assert.equal(artifact.report.title,template.title);assert.ok(Number.isFinite(Date.parse(artifact.report.generatedAt)));
  60. assert.deepEqual(artifact.report.executive,[ '课堂过程成果,未经员工正式确认;待验证项不作为市场结论。',...template.report.executive ]);
  61. for(const field of ['chapters','evidence','subtitle','method','sources'])assert.deepEqual(artifact.report[field],template.report[field],field);
  62. assert.deepEqual(artifact.meta.workshop.sourceIds,template.sourceIds);
  63. assert.deepEqual(artifact.meta.workshop.pendingQuestions,template.pendingQuestions);
  64. assert.equal(artifact.meta.workshop.formalConfirmation,false);
  65. assert.equal(artifact.dashboardPublishedAt??null,null);assert.equal(artifact.contentConfirmedAt??null,null);
  66. assert.equal(generated.rawText,template.rawText);assert.equal(generated.dataMode,'live');
  67. check='inherited source evidence and quotes';
  68. assert.equal(artifact.evidenceCount,4);assert.equal(artifact.sources.length,4);
  69. const evidenceIds={'consumer-cq-01':'FO47R0yR8D','consumer-cq-02':'rD8XZgV1Xo','consumer-cq-03':'_9mUml0wmh','consumer-cq-04':'2bFGP7axMo'};
  70. for(const [key,entry]of Object.entries(template.report.evidence)){
  71. const [origin,evidence]=await Promise.all([read('KfSource',entry.sourceId),read('KfEvidence',evidenceIds[key])]);
  72. assert.equal(origin.dataMode,'live');assert.equal(origin.sourceKind,'social');assert.equal(origin.platform,'smzdm');
  73. assert.equal(evidence.quote,entry.quotes[0].text);
  74. assert.deepEqual(evidence.sourceRef,pointer('KfSource',entry.sourceId));assert.equal(evidence.productRef??null,null);
  75. assert.equal(evidence.evidenceKind,'public_user_experience_self_report');
  76. assert.equal(evidence.verificationState,'archived_text_and_screenshot_checked');
  77. assert.ok(origin.rawText.includes(entry.quotes[0].text),key+' quote exists in source');
  78. assert.ok(template.rawText.includes(entry.quotes[0].text),key+' quote exists in report text');
  79. assert.equal(entry.quotes.length,1,key+' quote count');
  80. }
  81. check='session ownership and version history';
  82. assert.equal(session.objectId,sessionId);assert.equal(session.stageCode,'S3');assert.equal(session.step,'submitted');
  83. assert.deepEqual(session.meta.attribution,artifact.meta.attribution);
  84. assert.equal(artifact.agentName,'王刚001');assert.equal(artifact.agentId,session.meta.attribution.agentKey);
  85. for(const [row,prior]of [[s1,s1Template],[s2,s2Template],[s3v1,s3v1Template]]){
  86. assert.deepEqual(row.meta.attribution,session.meta.attribution);
  87. for(const field of ['title','rawText','version','stageCode'])assert.deepEqual(row[field],prior[field]);
  88. assert.deepEqual(row.report.chapters,prior.report.chapters);assert.deepEqual(row.report.evidence,prior.report.evidence);
  89. assert.deepEqual(row.meta.workshop.sourceIds,prior.sourceIds);
  90. }
  91. assert.equal(s1.dashboardState,'workshop');assert.equal(s2.dashboardState,'workshop');assert.equal(s3v1.dashboardState,'workshop');
  92. const series=history.filter(x=>x.reportSeriesKey===template.taskKey);
  93. assert.equal(series.length,2);assert.deepEqual(series.map(x=>[x.objectId,x.version]).sort((a,b)=>a[1]-b[1]),[[s3v1Id,1],[artifactId,2]]);
  94. assert.equal(history.filter(x=>x.stageCode==='S3'&&x.sessionRef?.objectId===sessionId).length,2);
  95. assert.equal(operations.length,1);assert.equal(operations[0].receipt.artifactId,artifactId);
  96. assert.equal(operations[0].receipt.sourceId,outputSourceId);
  97. assert.equal(operations[0].receipt.opportunityId,null);assert.equal(operations[0].receipt.projectId,null);
  98. assert.equal(opportunities.length,0);assert.equal(projects.length,0);
  99. check='anonymous public board projection';
  100. const board=await loadPublicBoard({url:'https://kangshifu.fmode.cn/api/functions',appId:'FKieRbIoGt_main'});
  101. const projected=board.artifact.find(x=>x.objectId===artifactId);assert.ok(projected);
  102. assert.equal(projected.dashboardState,'workshop');assert.equal(projected.dataMode,'live');
  103. assert.equal(projected.category??null,null);assert.deepEqual(projected.tags,artifact.tags);
  104. assert.deepEqual(projected.report.evidence,template.report.evidence);
  105. assert.deepEqual(projected.report.chapters,template.report.chapters);
  106. assert.ok(board.artifact.some(x=>x.objectId===s1Id));assert.ok(board.artifact.some(x=>x.objectId===s2Id));
  107. assert.ok(board.artifact.some(x=>x.objectId===s3v1Id));
  108. for(const value of [session.meta.attribution.participantAccount?.accountId,session.meta.attribution.agentAccount?.accountId,session.meta.attribution.conversationKey].filter(Boolean)){
  109. assert.equal(JSON.stringify(board).includes(value),false);
  110. }
  111. const result={
  112. ok:true,operation:'live-S3-v2-independently-verified',at:new Date().toISOString(),readOnly:true,businessWrites:0,
  113. sessionId,artifactId,outputSourceId,reportUrl:'https://kangshifu.fmode.cn/report/'+artifactId,
  114. version:2,stageCode:'S3',dataMode:'live',dashboardState:'workshop',reportSeriesKey:template.taskKey,
  115. sourceIds:template.sourceIds,evidenceCount:artifact.evidenceCount,quoteCount:4,pendingQuestionCount:8,
  116. templateMatched:true,categoryPersistedAsTag:projected.tags.includes('拌面'),formalReportPublished:false,
  117. dashboardPublishedAt:null,employeeConfirmed:false,sessionOwnerMatched:true,s1AndS2Preserved:true,s3v1Preserved:true,
  118. pendingQuestionsInStructuredReport:8,templateSha256:sha(templateBytes),
  119. artifactSeriesCount:series.length,s3ArtifactCount:2,saveOperationCount:operations.length,opportunityCount:opportunities.length,projectCount:projects.length,
  120. publicBoardProjectionVerified:true,browserVerified:false
  121. };
  122. fs.writeFileSync(outputFile,JSON.stringify(result,null,2)+'\n',{flag:'wx'});
  123. console.log(JSON.stringify(result,null,2));
  124. }catch(error){
  125. const result={ok:false,operation:'live-S3-v2-independent-readback-stopped',check,
  126. checkLine:error.stack?.match(/verify-live-S3-v2-readback\.mjs:(\d+):/)?.[1]??null,readOnly:true,businessWrites:0};
  127. fs.writeFileSync(outputFile,JSON.stringify(result,null,2)+'\n',{flag:'wx'});
  128. console.error(JSON.stringify(result,null,2));process.exitCode=1;
  129. }