verify-first-live-S1-readback.mjs 5.3 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950
  1. // Exact read-only lookup; never calls start/save/search.
  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='cOm6c8gg7z',sourceId='CI8dFPUdTz',workshopKey='KSF-真实资料-20261102-01';
  8. const template=JSON.parse(fs.readFileSync(new URL('../server/test/fixtures/first-live-S1-20261102.json',import.meta.url)));
  9. const call=async(name,p)=>{const r=await invoke(name,p);assert.equal(r.http,200);assert.equal(r.body?.result?.ok,true);return r.body.result.data;};
  10. try{
  11. const [a,s,output,origins,history]=await Promise.all([
  12. call('ksfGetRecord',{className:'KfArtifact',objectId:artifactId}),call('ksfGetRecord',{className:'KfSession',objectId:sessionId}),
  13. call('ksfGetRecord',{className:'KfSource',objectId:sourceId}),Promise.all(template.sourceIds.map(objectId=>call('ksfGetRecord',{className:'KfSource',objectId}))),
  14. classRows('KfArtifact',{workshopKey})
  15. ]);
  16. const artifact=a.record,session=s.record;
  17. assert.equal(session.workshopKey,workshopKey);assert.equal(session.dataMode,'live');
  18. assert.equal(artifact.sessionRef?.objectId,sessionId);assert.equal(artifact.sourceRef?.objectId,sourceId);
  19. for(const k of ['title','rawText','version','stageCode','artifactType'])assert.deepEqual(artifact[k],template[k]);
  20. assert.equal(artifact.report.title,template.title);assert.ok(Number.isFinite(Date.parse(artifact.report.generatedAt)));
  21. assert.equal(artifact.report.executive[0],'课堂过程成果,未经员工正式确认;待验证项不作为市场结论。');
  22. assert.deepEqual(artifact.report.executive.slice(1),template.report.executive);
  23. for(const k of ['chapters','evidence'])assert.deepEqual(artifact.report[k],template.report[k]);
  24. assert.equal(output.record.rawText,template.rawText);assert.equal(output.record.sourceKind,'dataset');assert.equal(output.record.dataMode,'live');
  25. assert.equal(artifact.dataMode,'live');assert.equal(artifact.dashboardState,'workshop');
  26. assert.equal(artifact.projectRef??null,null);assert.equal(artifact.projectId??null,null);
  27. assert.equal(artifact.contentConfirmedAt??null,null);assert.equal(artifact.dashboardPublishedAt??null,null);
  28. assert.deepEqual(artifact.meta.workshop.sourceIds,template.sourceIds);assert.deepEqual(artifact.meta.workshop.pendingQuestions,template.pendingQuestions);
  29. assert.equal(artifact.meta.workshop.formalConfirmation,false);assert.equal(artifact.evidenceCount,2);assert.equal(artifact.sources.length,2);
  30. assert.equal(artifact.agentName,'王刚001');assert.equal(artifact.agentId,'agent-account:7c9f85363ca4c5a529a5b363308b065998924e2394e846145a5c62c904506d2e');
  31. assert.deepEqual(artifact.meta.attribution,session.meta.attribution);
  32. const attribution=session.meta.attribution;
  33. assert.equal(attribution.identityBasis,'channel_context');
  34. const expectedParticipant='participant:'+crypto.createHash('sha256').update(JSON.stringify([workshopKey,'participant-account:f83edd27dda7e0989a0d313c158314043fd129efb86b8f6a07db8140033c5708'])).digest('hex');
  35. assert.equal(attribution.participantKey,expectedParticipant);
  36. assert.equal(attribution.participantAccount.scopeId,attribution.agentAccount.accountId);
  37. for(const entry of Object.values(artifact.report.evidence)){
  38. const origin=origins.find(x=>x.record.objectId===entry.sourceId)?.record;assert.ok(origin);assert.equal(origin.dataMode,'live');
  39. for(const q of entry.quotes)assert.ok(origin.rawText.includes(q.text));
  40. }
  41. assert.equal(history.length,1);assert.equal(history[0].objectId,artifactId);
  42. const sessions=await classRows('KfSession',{workshopKey});assert.equal(sessions.length,1);assert.equal(sessions[0].objectId,sessionId);
  43. const raw=await loadPublicBoard({url:'https://kangshifu.fmode.cn/api/functions',appId:'FKieRbIoGt_main'});
  44. assert.ok(raw.artifact.some(x=>x.objectId===artifactId&&x.dataMode==='live'));
  45. // The public session projection omits dataMode; it was checked on the private record above.
  46. assert.ok(raw.session.some(x=>x.objectId===sessionId&&x.workshopKey===workshopKey));
  47. for(const v of [attribution.participantAccount.accountId,attribution.participantAccount.scopeId,session.meta.conversationKey].filter(Boolean))assert.equal(JSON.stringify(raw).includes(v),false);
  48. const result={ok:true,operation:'first-live-S1-independently-verified',at:new Date().toISOString(),readOnly:true,businessWrites:0,sessionId,artifactId,outputSourceId:sourceId,originalSourceIds:template.sourceIds,reportUrl:'https://kangshifu.fmode.cn/report/'+artifactId,stageCode:'S1',dataMode:'live',dashboardState:'workshop',productFlavorEntries:9,linkedProductEvidenceEntities:9,independentSourcePages:2,reportQuoteCount:2,independentManufacturerCount:1,pendingQuestionCount:6,privateAttributionMatched:true,templateMatched:true,publicBoardDataVerified:true,matchingSessionCount:1,matchingArtifactCount:1,formalReportPublished:false,employeeConfirmed:false,browserVerified:false};
  49. fs.writeFileSync('work/first-live-S1-readback-20261102.json',JSON.stringify(result,null,2));console.log(JSON.stringify(result,null,2));
  50. }catch(e){console.error(JSON.stringify({ok:false,error:'FIRST_LIVE_S1_READBACK_FAILED',checkLine:e.stack?.match(/verify-first-live-S1-readback\.mjs:(\d+):/)?.[1],message:'独立只读核验停止,未输出原始异常或私有身份。'}));process.exitCode=1;}