'use strict'; const {test}=require('node:test'); const assert=require('node:assert/strict'); const E=require('./engine.js'),D=require('./demo-data.js'); const opts={country:'CHL',controls:['ARG','PER','URY'],indicator:'SP.DYN.LE00.IN',start:2003,end:2024,breakYear:2016}; const approx=(actual,expected,tol=1e-6)=>assert.ok(Math.abs(actual-expected)[{country:'TRT',indicator:'X',year,value:treated[i],source:'fixture'},...Object.entries(donors).map(([country,values])=>({country,year,indicator:'X',value:values[i],source:'fixture'}))]);} const Y=Array.from({length:12},(_,i)=>2000+i),fixture={country:'TRT',controls:['A','B'],indicator:'X',start:2000,end:2011,breakYear:2006}; const controlA=Y.map((_,i)=>20+i),controlB=Y.map((_,i)=>20+i),treated=Y.map((_,i)=>25+i+(i>=6?4:0)); const R=rowsFromVectors(Y,treated,{A:controlA,B:controlB}); test('Demo has all 15 economies and 8 indicators',()=>{assert.equal(new Set(D.records.map(r=>r.country)).size,15);assert.equal(new Set(D.records.map(r=>r.indicator)).size,8);assert.equal(D.records.length,15*25*8);}); test('Demo observations clearly synthetic',()=>assert.ok(D.records.every(r=>r.source==='synthetic-demo'))); test('Demo target shift produces nonzero DiD',()=>{const d=E.estimateDid(D.records,opts);assert.equal(d.ok,true);assert.ok(d.effect>0.5);}); test('Equal-weight DiD recovers a known 4-unit shift',()=>approx(E.estimateDid(R,fixture).effect,4)); test('DiD records correct pre-post years',()=>{const d=E.estimateDid(R,fixture);assert.equal(d.preYears,6);assert.equal(d.postYears,6);}); test('DiD treated mean change is baseline trend plus 4',()=>approx(E.estimateDid(R,fixture).treatedChange,10)); test('DiD control mean change tracks baseline slope',()=>approx(E.estimateDid(R,fixture).controlChange,6)); test('DiD rejects no donors',()=>assert.throws(()=>E.estimateDid(R,{...fixture,controls:[]}))); test('DiD rejects treated country among donors',()=>assert.throws(()=>E.estimateDid(R,{...fixture,controls:['TRT','A']}))); test('DiD rejects too short calendar windows',()=>assert.throws(()=>E.estimateDid(R,{...fixture,end:2005}))); test('DiD rejects insufficient pre periods',()=>assert.throws(()=>E.estimateDid(R,{...fixture,breakYear:2002}))); test('DiD rejects insufficient post periods',()=>assert.throws(()=>E.estimateDid(R,{...fixture,breakYear:2010}))); test('Paired years require all donors present',()=>{const x=R.map(v=>({...v}));x.find(v=>v.country==='B'&&v.year===2001).value=null;const d=E.estimateDid(x,fixture);assert.equal(d.preYears,5);}); test('Missing post data returns explicit missing estimate if below threshold',()=>{const x=R.filter(v=>v.year<2008||v.country!=='TRT');const d=E.estimateDid(x,fixture);assert.equal(d.ok,false);}); test('Parallel pre-trends yield slope gap approximately zero',()=>approx(E.pretrend(E.estimateDid(R,fixture)).slopeDifference,0)); test('Pre-trends computed over pre years only',()=>assert.equal(E.pretrend(E.estimateDid(R,fixture)).preDifferences.length,6)); test('Bootstrap is reproducible with same seed',()=>{const d=E.estimateDid(R,fixture);assert.deepEqual(E.bootstrapDid(d,100,9).interval,E.bootstrapDid(d,100,9).interval);}); test('Bootstrap covers known constant DiD 4',()=>{const d=E.estimateDid(R,fixture);const b=E.bootstrapDid(d,400,99);assert.ok(b.interval[0]<6&&b.interval[1]>2);}); test('Bootstrap does not claim causal uncertainty',()=>assert.match(E.bootstrapDid(E.estimateDid(R,fixture)).method,/not causal/i)); test('Simplex projection nonnegative and sums to one',()=>{const w=E.projectSimplex([2,-1,0.3]);assert.ok(w.every(x=>x>=0));approx(E.sum(w),1);}); test('Simplex projection handles equal vectors',()=>{const w=E.projectSimplex([0,0,0,0]);w.forEach(x=>approx(x,.25));}); test('Synthetic control recovers perfect equal donors pre-break',()=>{const s=E.syntheticControl(R,fixture);assert.equal(s.ok,true);approx(s.preRmse,5,.05);approx(s.postMeanGap,9,.05);}); test('Synthetic donor weights stay on convex simplex',()=>{const s=E.syntheticControl(D.records,opts);assert.ok(s.ok);assert.ok(s.weights.every(w=>w.weight>=0&&w.weight<=1));approx(E.sum(s.weights.map(w=>w.weight)),1,1e-6);}); test('Synthetic control requires two donors',()=>{const s=E.syntheticControl(R,{...fixture,controls:['A']});assert.equal(s.ok,false);}); test('Synthetic control requires five paired pre years',()=>{const sparse=R.filter(r=>r.year>=2002||r.country!=='TRT');const s=E.syntheticControl(sparse,fixture);assert.equal(s.ok,false);}); test('Synthetic fitting excludes post years from weights',()=>{const x=R.map(r=>({...r}));const y=R.map(r=>({...r}));y.forEach(r=>{if(r.year>=2006&&r.country==='TRT')r.value+=999});assert.deepEqual(E.syntheticControl(x,fixture).weights,E.syntheticControl(y,fixture).weights);}); test('Unit placebos produce a real and placebo assignments',()=>{const p=E.unitPlacebos(D.records,opts);assert.equal(p.length,4);assert.equal(p.filter(v=>v.treated).length,1);}); test('Time placebos only run before specified break',()=>{const p=E.timePlacebos(D.records,opts);assert.ok(p.length>0);assert.ok(p.every(v=>v.year{const c=E.coverage(D.records,{...opts,indicator:'SE.SEC.ENRR',country:'BOL',controls:['ARG','PER']});assert.equal(c[0].missing,3);}); test('Coverage reports latest year',()=>assert.equal(E.coverage(D.records,opts)[0].latest,2024)); test('World Bank parser handles ISO3 and absent values',()=>{const payload=[{page:1},[{countryiso3code:'CHL',date:'2020',value:71.2},{countryiso3code:'ARG',date:'2020',value:null}]];const r=E.parseWorldBank(payload,'I');assert.deepEqual(r.map(v=>v.value),[71.2,null]);}); test('World Bank parser rejects invalid shapes',()=>assert.throws(()=>E.parseWorldBank({},'I'))); test('World Bank parser ignores invalid years',()=>assert.equal(E.parseWorldBank([{},[{countryiso3code:'ARG',date:'foo',value:4}]],'I').length,0)); test('Data validator drops invalid years and NaNs',()=>{const x=E.validateRows([{country:'A',indicator:'Y',year:2020,value:4},{country:'B',indicator:'Y',year:1800,value:3},{country:'C',indicator:'Y',year:2020,value:'foo'}]);assert.equal(x.length,1);}); test('Data validator de-duplicates years',()=>{const x=E.validateRows([{country:'A',indicator:'Y',year:2020,value:4},{country:'A',indicator:'Y',year:2020,value:5}]);assert.equal(x.length,1);assert.equal(x[0].value,4);}); test('Data validator retains null as missing',()=>assert.equal(E.validateRows([{country:'A',indicator:'Y',year:2020,value:null}])[0].value,null)); test('CSV formula values are quoted to preserve structure',()=>{const csv=E.toCSV([{name:'a,"b"',year:2020}],['name','year']);assert.match(csv,/"a,""b"""/);}); test('Year-by-year control mean uses all donors',()=>{const t=E.timeline(R,fixture);assert.equal(t.find(p=>p.year===2000).controls,20);}); test('Invalid options reject duplicate treated and donor',()=>assert.throws(()=>E.checkOptions({...fixture,controls:['TRT']}))); test('Every display indicator has a documented unit',()=>assert.ok(Object.values(E.indicators).every(x=>x.name&&x.unit&&x.domain))); test('CSV export neutralizes spreadsheet formulas in user supplied strings',()=>{const csv=E.toCSV([{value:'=HYPERLINK("https://x")'}],['value']);assert.ok(csv.includes("'"+"=HYPERLINK"));});