// Independent, read-only S3 acceptance. Never calls start/save/import. import fs from 'node:fs'; import crypto from 'node:crypto'; import assert from 'node:assert/strict'; import {invoke,classRows} from '../server/scripts/platform-client.mjs'; import {loadPublicBoard} from '../src/services/workshop-data.js'; const sessionId='btrFqkz0yw',artifactId='oiNxX7o27j',outputSourceId='HGMauzWO5k'; const s1Id='G8IrkkDG7X',s2Id='b7NOby5ys0'; const template=JSON.parse(fs.readFileSync('work/first-live-S3-template.json','utf8')); const s1Template=JSON.parse(fs.readFileSync('work/first-live-S1-v2-template.json','utf8')); const s2Template=JSON.parse(fs.readFileSync('work/first-live-S2-template.json','utf8')); const outputFile='work/live-S3-readback-20261102.json'; const saveRequestId='KSF-真实资料-20261102-01:S3:save:v1'; const pointer=(className,objectId)=>({__type:'Pointer',className,objectId}); 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); const sha=x=>crypto.createHash('sha256').update(x).digest('hex'); const read=async(className,objectId)=>{ const r=await invoke('ksfGetRecord',{className,objectId}); assert.equal(r.http,200);assert.equal(r.body?.result?.ok,true); const row=r.body.result.data.record;assert.equal(row?.objectId,objectId);return row; }; assert.equal(fs.existsSync(outputFile),false,'Preserve earlier acceptance'); let check='template fingerprint and fixed scope'; try{ assert.equal(template.sessionId,null);assert.equal(template.taskKey,'KSF-真实资料-20261102-01:S3'); assert.equal(template.stageCode,'S3');assert.equal(template.version,1);assert.equal(template.artifactType,'opportunity_card'); assert.equal('opportunity' in template,false);assert.equal('projectId' in template,false); assert.deepEqual(template.sourceIds,['2mzBadmvws','Pi04eTwTQ3','m1OQx9aynA','QdeMcyTTjX']); assert.equal(Object.keys(template.report.evidence).length,4);assert.equal(template.pendingQuestions.length,8); assert.equal(template.rawText.length,2554); check='read saved records and operation history'; const settled=await Promise.allSettled([ read('KfSession',sessionId),read('KfArtifact',artifactId),read('KfArtifact',s1Id),read('KfArtifact',s2Id), read('KfSource',outputSourceId), classRows('KfArtifact',{workshopKey:'KSF-真实资料-20261102-01'}), classRows('KsfOperation',{operation:'workshop-task',requestId:saveRequestId}), classRows('KfOpportunity',{sessionRef:pointer('KfSession',sessionId)}), classRows('KfProject',{sessionRef:pointer('KfSession',sessionId)}) ]); const failures=settled.flatMap((r,i)=>r.status==='rejected'?[i]:[]); assert.deepEqual(failures,[],'Read group failure indexes'); const [session,artifact,s1,s2,generated,history,operations,opportunities,projects]=settled.map(r=>r.value); check='artifact fields and template'; assert.equal(artifact.title,template.title);assert.equal(artifact.rawText,template.rawText); assert.equal(artifact.version,1);assert.equal(artifact.stageCode,'S3');assert.equal(artifact.artifactType,'opportunity_card'); assert.equal(artifact.reportSeriesKey,template.taskKey);assert.equal(artifact.reportType,'workshop_auxiliary'); assert.equal(artifact.schemaVersion,'ksf.v0.7');assert.equal(artifact.dataMode,'live');assert.equal(artifact.recordState,'active'); assert.equal(artifact.dashboardState,'workshop');assert.equal(artifact.ownerSession,sessionId); assert.deepEqual(artifact.sessionRef,pointer('KfSession',sessionId));assert.deepEqual(artifact.sourceRef,pointer('KfSource',outputSourceId)); assert.equal(artifact.projectRef??null,null);assert.equal(artifact.previousVersionRef??null,null); assert.equal(artifact.category??null,null); assert.deepEqual(artifact.tags,['拌面','课堂过程成果']); assert.equal(artifact.report.title,template.title);assert.ok(Number.isFinite(Date.parse(artifact.report.generatedAt))); assert.deepEqual(artifact.report.executive,[ '课堂过程成果,未经员工正式确认;待验证项不作为市场结论。',...template.report.executive ]); for(const field of ['chapters','evidence','subtitle','method','sources'])assert.deepEqual(artifact.report[field],template.report[field],field); assert.deepEqual(artifact.meta.workshop.sourceIds,template.sourceIds); assert.deepEqual(artifact.meta.workshop.pendingQuestions,template.pendingQuestions); assert.equal(artifact.meta.workshop.formalConfirmation,false); assert.equal(artifact.dashboardPublishedAt??null,null);assert.equal(artifact.contentConfirmedAt??null,null); assert.equal(generated.rawText,template.rawText);assert.equal(generated.dataMode,'live'); check='inherited source evidence and quotes'; assert.equal(artifact.evidenceCount,4);assert.equal(artifact.sources.length,4); for(const [key,entry]of Object.entries(template.report.evidence)){ const origin=await read('KfSource',entry.sourceId); assert.ok(origin.rawText.includes(entry.quotes[0].text),key+' quote exists in source'); assert.ok(template.rawText.includes(entry.quotes[0].text),key+' quote exists in report text'); assert.equal(entry.quotes.length,1,key+' quote count'); } check='session ownership and version history'; assert.equal(session.objectId,sessionId);assert.equal(session.stageCode,'S3');assert.equal(session.step,'submitted'); assert.deepEqual(session.meta.attribution,artifact.meta.attribution); assert.equal(artifact.agentName,'王刚001');assert.equal(artifact.agentId,session.meta.attribution.agentKey); for(const [row,prior]of [[s1,s1Template],[s2,s2Template]]){ assert.deepEqual(row.meta.attribution,session.meta.attribution); for(const field of ['title','rawText','version','stageCode'])assert.deepEqual(row[field],prior[field]); assert.deepEqual(row.report.chapters,prior.report.chapters);assert.deepEqual(row.report.evidence,prior.report.evidence); assert.deepEqual(row.meta.workshop.sourceIds,prior.sourceIds); } assert.equal(s1.dashboardState,'workshop');assert.equal(s2.dashboardState,'workshop'); const series=history.filter(x=>x.reportSeriesKey===template.taskKey); assert.equal(series.length,1);assert.equal(series[0].objectId,artifactId);assert.equal(series[0].version,1); assert.equal(history.filter(x=>x.stageCode==='S3'&&x.sessionRef?.objectId===sessionId).length,1); assert.equal(operations.length,1);assert.equal(operations[0].receipt.artifactId,artifactId); assert.equal(operations[0].receipt.sourceId,outputSourceId); assert.equal(operations[0].receipt.opportunityId,null);assert.equal(operations[0].receipt.projectId,null); assert.equal(opportunities.length,0);assert.equal(projects.length,0); check='anonymous public board projection'; const board=await loadPublicBoard({url:'https://kangshifu.fmode.cn/api/functions',appId:'FKieRbIoGt_main'}); const projected=board.artifact.find(x=>x.objectId===artifactId);assert.ok(projected); assert.equal(projected.dashboardState,'workshop');assert.equal(projected.dataMode,'live'); assert.equal(projected.category??null,null);assert.deepEqual(projected.tags,artifact.tags); assert.deepEqual(projected.report.evidence,template.report.evidence); assert.deepEqual(projected.report.chapters,template.report.chapters); assert.ok(board.artifact.some(x=>x.objectId===s1Id));assert.ok(board.artifact.some(x=>x.objectId===s2Id)); for(const value of [session.meta.attribution.participantAccount?.accountId,session.meta.attribution.agentAccount?.accountId,session.meta.attribution.conversationKey].filter(Boolean)){ assert.equal(JSON.stringify(board).includes(value),false); } const result={ ok:true,operation:'live-S3-independently-verified',at:new Date().toISOString(),readOnly:true,businessWrites:0, sessionId,artifactId,outputSourceId,reportUrl:'https://kangshifu.fmode.cn/report/'+artifactId, version:1,stageCode:'S3',dataMode:'live',dashboardState:'workshop',reportSeriesKey:template.taskKey, sourceIds:template.sourceIds,evidenceCount:artifact.evidenceCount,quoteCount:4,pendingQuestionCount:8, templateMatched:true,categoryPersistedAsTag:projected.tags.includes('拌面'),formalReportPublished:false, dashboardPublishedAt:null,employeeConfirmed:false,sessionOwnerMatched:true,s1AndS2Preserved:true, artifactSeriesCount:series.length,s3ArtifactCount:1,saveOperationCount:operations.length,opportunityCount:opportunities.length,projectCount:projects.length, publicBoardProjectionVerified:true,browserVerified:false }; fs.writeFileSync(outputFile,JSON.stringify(result,null,2)+'\n',{flag:'wx'}); console.log(JSON.stringify(result,null,2)); }catch(error){ const result={ok:false,operation:'live-S3-independent-readback-stopped',check, checkLine:error.stack?.match(/verify-live-S3-readback\.mjs:(\d+):/)?.[1]??null,readOnly:true,businessWrites:0}; fs.writeFileSync(outputFile,JSON.stringify(result,null,2)+'\n',{flag:'wx'}); console.error(JSON.stringify(result,null,2));process.exitCode=1; }