import {test} from 'node:test';import assert from 'node:assert/strict';import fs from 'node:fs';import vm from 'node:vm'; const source=fs.readFileSync(new URL('../cloud/payroll-integrity.js',import.meta.url),'utf8'); function setup(){const calls=[],c=vm.createContext({Date,Psql:{one:async(sql,args)=>{calls.push({sql,args});return sql.includes('_read')?{result:{stamp:'current',payload:null}}:{saved:true};}}});vm.runInContext(source,c);c.text=v=>String(v||'');c.number=v=>Number(v)||0;c.payrollTenant=()=> 'co';let count=0;c.payrollPreviewUncached=async()=>{count++;return{payableAmount:123,sourceStatus:{sync:{state:'batch_verified',settlementReady:false}}};};return{c,calls,count:()=>count};} test('commands and require-live callers cannot receive a cached approval',async()=>{const {c,calls,count}=setup();for(const [used,live,targetId]of[[false,false,''],[true,true,''],[true,false,'batch']])await c.payrollPreviewData({}, {targetId},'2026-11',used,live);assert.equal(count(),3);assert.equal(calls.length,0);}); test('cache miss computes authoritative output and publishes with original dependency stamp',async()=>{const {c,calls,count}=setup();const r=await c.payrollPreviewData({storeId:12},{storeId:99,search:'a'},'2026-11',true);assert.equal(r.payableAmount,123);assert.equal(count(),1);assert.equal(calls.length,2);assert.equal(calls[1].args[2],'current');assert.match(calls[0].args[1],/12,99,"a",false,false/);}); test('cached hit preserves amounts and denied settlement; roles are checked before reading cache',async()=>{const {c,count}=setup();c.Psql.one=async()=>({result:{stamp:'current',payload:{payableAmount:123,sourceStatus:{sync:{state:'batch_verified',settlementReady:false}}}}});const r=await c.payrollPreviewData({}, {},'2026-11',true);assert.equal(r.payableAmount,123);assert.equal(r.sourceStatus.sync.settlementReady,false);assert.equal(r.sourceStatus.cacheHit,true);assert.equal(count(),0);c.payrollTenant=()=>{throw Error('denied');};await assert.rejects(c.payrollPreviewData({}, {},'2026-11',true),/denied/);}); test('cache error falls back; failed verification is never cached',async()=>{const {c,calls,count}=setup();c.Psql.one=async()=>{throw Error('database missing helper');};assert.equal((await c.payrollPreviewData({}, {},'2026-11',true)).payableAmount,123);assert.equal(count(),1);const other=setup();other.c.payrollPreviewUncached=async()=>({sourceStatus:{sync:{state:'error'}}});await other.c.payrollPreviewData({}, {},'2026-11',true);assert.equal(other.calls.length,1);}); test('summary transport preserves every verified total; coach details preserve scope and exceptions',async()=>{ const {c}=setup(),lines=[{coachId:1,storeId:10,amount:20},{coachId:2,storeId:20,amount:30,exception:'needs review'}]; c.businessDate=()=> '2026-11-10';c.payrollPreviewData=async()=>({lines,sourceStatus:{sync:{state:'confirmed',settlementReady:true}}});c.payrollPage=async()=>({items:[]});c.storeNameOf=()=> 'store';c.Psql.query=async()=>[]; c.payrollResult=(month,rows,status,input)=>{const visible=rows.filter(l=>!input.storeId||l.storeId===input.storeId);return{month,lines:visible,exceptions:visible.filter(l=>l.exception),coachItems:visible.map(l=>({coachId:l.coachId,amount:l.amount})),payableAmount:visible.reduce((a,l)=>a+l.amount,0),exceptionCount:visible.filter(l=>l.exception).length,sourceStatus:status};}; const full=await c.payrollOverviewData({},{}),summary=await c.payrollOverviewData({},{summaryOnly:true}),detail=await c.payrollOverviewData({},{lineCoachId:2}); const withoutRows=r=>JSON.parse(JSON.stringify({...r,lines:[],exceptions:[]}));assert.deepEqual(withoutRows(full),withoutRows(summary));assert.deepEqual(withoutRows(full),withoutRows(detail));assert.equal(summary.lines.length,0);assert.equal(detail.lines.length,1);assert.equal(detail.exceptions.length,1);assert.equal(detail.lines[0].coachId,2); const scoped=await c.payrollOverviewData({storeId:10,store:{toJSON:()=>({})}},{storeId:20,lineCoachId:2});assert.equal(scoped.lines.length,0);assert.equal(scoped.payableAmount,20); }); test('export rejects presentation-only projections even when caller forwards overview flags',async()=>{ const {c}=setup();let seen;c.payrollOverviewData=async(_context,input)=>{seen=input;return{month:'2026-11',lines:[],sourceStatus:{}};};c.payrollVisibleLines=rows=>rows;c.payrollResult=()=>({coachItems:[]}); await c.payrollExportData({},{summaryOnly:true,lineCoachId:9,coachId:2});assert.equal(seen.summaryOnly,false);assert.equal(seen.lineCoachId,0);assert.equal(seen.coachId,2); });