verify-first-live-readback.mjs 5.1 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556
  1. // Independent read-only acceptance of the actual first live source imports.
  2. import fs from 'node:fs';
  3. import crypto from 'node:crypto';
  4. import assert from 'node:assert/strict';
  5. import {request,classRows} from '../server/scripts/platform-client.mjs';
  6. const bundle=JSON.parse(fs.readFileSync(new URL('../server/test/fixtures/first-live-sources-20261102.json',import.meta.url)));
  7. const categoryId='Mc9FJpryn4';
  8. const imported=[
  9. {key:'uni-jiangban',sourceId:'BgUB6dnxN1',products:['OH93MpSAur','1hhmH3P2jO','RyVDxfutN7','43cZQbN1s_'],evidence:['kHU2d7KCRd','4F0odlTmp7','OMPoQHwLtM','u-M-YlSAp5']},
  10. {key:'uni-qiaomianguan',sourceId:'SFiKA0D7n9',products:['Sq74PECPh_','jSsWsk83J0','JojU9_ZB-p','WjigWybqon','rbgvCXmBwo'],evidence:['KebZUsjXjB','t1fLXcQoth','cPbDPYTJnl','Twk_M76Irf','zthpfSI4v-']}
  11. ];
  12. 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);
  13. const get=(table,id)=>request('GET','/classes/'+table+'/'+encodeURIComponent(id));
  14. const ptr=(className,objectId)=>({__type:'Pointer',className,objectId});
  15. try{
  16. const category=await get('KfCategory',categoryId),reports=[];
  17. for(const row of imported){
  18. const batch=bundle.batches.find(b=>b.key===row.key),task=batch.task;
  19. const source=await get('KfSource',row.sourceId);
  20. for(const field of ['title','sourceKind','sourceUrl','platform','dataMode','rawText'])assert.equal(source[field],task.source[field]);
  21. assert.equal(Date.parse(source.collectedAt?.iso||source.collectedAt),Date.parse(task.source.collectedAt));
  22. assert.equal(source.contentHash,crypto.createHash('sha256').update(task.source.rawText).digest('hex'));
  23. const sourceMatches=await classRows('KfSource',{contentHash:source.contentHash,sourceUrl:source.sourceUrl,dataMode:'live'});assert.equal(sourceMatches.length,1);assert.equal(sourceMatches[0].objectId,row.sourceId);
  24. const [products,evidence,jobs]=await Promise.all([
  25. classRows('KfProduct',{sourceRef:ptr('KfSource',row.sourceId)}),classRows('KfEvidence',{sourceRef:ptr('KfSource',row.sourceId)}),
  26. classRows('KfImportJob',{inputSourceRef:ptr('KfSource',row.sourceId)})
  27. ]);
  28. assert.deepEqual(products.map(p=>p.objectId).sort(),row.products.slice().sort());assert.deepEqual(evidence.map(e=>e.objectId).sort(),row.evidence.slice().sort());
  29. const lookup=new Map([['category',category]]);
  30. for(let i=0;i<row.products.length;i++){lookup.set('product-'+(i+1),products.find(p=>p.objectId===row.products[i]));lookup.set('evidence-'+(i+1),evidence.find(e=>e.objectId===row.evidence[i]));}
  31. for(const item of task.records){
  32. const saved=lookup.get(item.itemKey);assert.ok(saved);assert.equal(saved.dataMode,'live');
  33. for(const [field,v]of Object.entries(item.data)){
  34. if(v?.$item)assert.equal(saved[field]?.objectId,lookup.get(v.$item)?.objectId);
  35. else if(field==='collectedAt')assert.equal(Date.parse(saved[field]?.iso||saved[field]),Date.parse(v));
  36. else assert.equal(canonical(saved[field]),canonical(v));
  37. }
  38. if(item.className==='KfEvidence')assert.ok(source.rawText.includes(saved.quote));
  39. if(item.className==='KfProduct'){assert.equal(saved.netWeightG??null,null);assert.equal(saved.observedRetailPrice??null,null);assert.equal(saved.categoryRef?.objectId,categoryId);}
  40. }
  41. assert.equal(jobs.length,task.records.length);
  42. const operations=[];
  43. for(const job of jobs){
  44. assert.equal(job.status,'succeeded');assert.equal(job.expectedCount,1);assert.equal(job.successCount,1);assert.equal(job.failureCount,0);
  45. assert.equal(job.producer.agent.key,'agent-account:7c9f85363ca4c5a529a5b363308b065998924e2394e846145a5c62c904506d2e');
  46. const items=await classRows('KfImportItem',{jobRef:ptr('KfImportJob',job.objectId)});assert.equal(items.length,1);
  47. const item=items[0];assert.equal(item.status,'succeeded');assert.equal(item.targetId,lookup.get(item.itemKey)?.objectId);
  48. operations.push(item.operation);
  49. }
  50. reports.push({key:row.key,sourceId:row.sourceId,productIds:row.products,evidenceIds:row.evidence,importJobs:jobs.length,created:operations.filter(x=>x==='created').length,unchanged:operations.filter(x=>x==='unchanged').length});
  51. }
  52. assert.equal(reports.reduce((s,b)=>s+b.created,0),19);assert.equal(reports.reduce((s,b)=>s+b.unchanged,0),1);
  53. const existingSessions=await classRows('KfSession',{workshopKey:bundle.taskKey});
  54. const result={ok:true,operation:'first-live-sources-independently-verified',at:new Date().toISOString(),readOnly:true,businessWrites:0,taskKey:bundle.taskKey,dataMode:'live',categoryId,sourcePages:2,productFlavorEntries:9,evidenceEntries:9,independentManufacturerCount:1,sourceAndQuoteVerified:true,sameAgentAttribution:true,imports:reports,matchingSessionsBeforeS1:existingSessions.length,formalReportPublished:false,consumerResearchPerformed:false,collectionPerformedBy:'Codex',importExecutedBy:'workshop Agent; execution receipt relayed by user'};
  55. fs.writeFileSync('work/first-live-readback-20261102.json',JSON.stringify(result,null,2));console.log(JSON.stringify(result,null,2));
  56. }catch{console.error(JSON.stringify({ok:false,error:'FIRST_LIVE_READBACK_FAILED',message:'只读核验未通过;未输出原始异常、账号或凭据。'}));process.exitCode=1;}