File size: 5,986 Bytes
7c9e8d9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// Local test suite (Node >=18). No third-party packages. Run: node --test test_engine.js
'use strict';
const test=require('node:test'),assert=require('node:assert/strict'),fs=require('node:fs'),path=require('node:path');
const E=require('./engine.js');const data=n=>JSON.parse(fs.readFileSync(path.join(__dirname,n),'utf8'));
const train=data('train.json'),iid=data('iid_test.json'),challenge=data('challenge.json');
const model=E.train(train);
const preds={};for(const split of ['iid','challenge']){preds[split]={};for(const method of ['nb','word','char','hybrid'])preds[split][method]=E.evaluate(model,split==='iid'?iid:challenge,method);}
test('source counts and partitions match original',()=>{assert.equal(train.length,842);assert.equal(iid.length,130);assert.equal(challenge.length,54);});
test('balanced six class universe',()=>{assert.equal(E.LABELS.length,6);for(const label of E.LABELS)assert.ok(train.some(r=>r.label===label));});
test('three source languages represented',()=>{for(const language of ['en','es','pt'])assert.ok(train.some(r=>r.language===language));});
test('all records have stable unique IDs',()=>{const all=[...train,...iid,...challenge];assert.equal(new Set(all.map(r=>r.id)).size,1026);});
test('no missing text and labels across partitions',()=>{for(const r of [...train,...iid,...challenge]){assert.ok(r.text?.trim());assert.ok(E.LABELS.includes(r.label));}});
test('words normalize Unicode accents',()=>{const f=E.features('Educación MÉTODO educação','word');assert.ok(f.has('educacion'));assert.ok(f.has('metodo'));assert.ok(f.has('educacao'));});
test('character trigram counts are deterministic',()=>{assert.deepEqual([...E.features('abc abc','char')],[...E.features('abc abc','char')]);});
test('invalid training label is rejected',()=>{assert.throws(()=>E.train([{text:'works',label:'broken'}]));});
test('empty train rejected',()=>{assert.throws(()=>E.train([]));});
test('classifier rejects unknown model',()=>{assert.throws(()=>E.predict(model,'hello','bogus'));});
test('four predictions are valid labels',()=>{for(const method of ['nb','word','char','hybrid'])assert.ok(E.LABELS.includes(E.predict(model,'Our results suggest progress.',method).label));});
test('all predictions have correct row metadata',()=>{for(const split of ['iid','challenge'])for(const method of ['nb','word','char','hybrid']){assert.equal(preds[split][method].length,split==='iid'?130:54);assert.ok(preds[split][method].every(p=>p.id&&p.language&&p.domain&&p.text));}});
test('benchmark results are reproducible byte-for-byte',()=>{assert.deepEqual(E.evaluate(model,challenge,'hybrid'),preds.challenge.hybrid);});
test('NB IID accuracy matches measured baseline',()=>{assert.equal(E.evaluation(preds.iid.nb).accuracy,1);});
test('hybrid challenge accuracy is 42/54',()=>{assert.equal(E.evaluation(preds.challenge.hybrid).correct,42);});
test('all models show challenge degradation',()=>{for(const method of ['nb','word','char','hybrid'])assert.ok(E.evaluation(preds.iid[method]).accuracy>E.evaluation(preds.challenge[method]).accuracy);});
test('macro F1 is within 0..1',()=>{for(const split of ['iid','challenge'])for(const method of ['nb','word','char','hybrid'])assert.ok(E.evaluation(preds[split][method]).macroF1>=0&&E.evaluation(preds[split][method]).macroF1<=1);});
test('confusion entries sum to sample size',()=>{const m=E.evaluation(preds.challenge.hybrid);assert.equal(E.LABELS.reduce((s,x)=>s+E.LABELS.reduce((t,y)=>t+m.confusion[x][y],0),0),54);});
test('macro F1 of perfect predictions equals 1',()=>{const rows=E.LABELS.map(label=>({truth:label,pred:label}));assert.equal(E.evaluation(rows).macroF1,1);});
test('macro F1 zero for two mismatches with unsupported labels',()=>{const rows=[{truth:'background',pred:'method'},{truth:'objective',pred:'method'}];assert.equal(E.evaluation(rows).macroF1,0);});
test('invalid evaluation label rejected',()=>{assert.throws(()=>E.evaluation([{truth:'not-label',pred:'method'}]));});
test('evaluation of no rows rejected',()=>{assert.throws(()=>E.evaluation([]));});
test('by-language slice totals cover challenge',()=>{const slices=E.summarizeSlices(preds.challenge.hybrid,'language');assert.equal(slices.reduce((s,x)=>s+x.n,0),54);assert.equal(slices.length,3);});
test('by-domain slices cover IID',()=>{const slices=E.summarizeSlices(preds.iid.word,'domain');assert.equal(slices.reduce((s,x)=>s+x.n,0),130);});
test('clean original test split has no exact duplicate',()=>{assert.equal(E.audit(train,iid).clean,true);});
test('clean novel challenge has no exact duplicate',()=>{assert.equal(E.audit(train,challenge).clean,true);});
test('duplicated ID and text detected',()=>{const r=E.audit(train,[train[0]]);assert.equal(r.exactTextOverlap,1);assert.equal(r.duplicateIds,1);assert.equal(r.clean,false);});
test('bootstrap is seeded and reproducible',()=>{assert.deepEqual(E.bootstrapAccuracy(preds.challenge.hybrid,2026,200),E.bootstrapAccuracy(preds.challenge.hybrid,2026,200));});
test('bootstrap confidence range within valid values',()=>{const c=E.bootstrapAccuracy(preds.challenge.hybrid,2026,200);assert.ok(c.low<=c.high&&c.low>=0&&c.high<=1);});
test('invalid bootstrap repetitions rejected',()=>{assert.throws(()=>E.bootstrapAccuracy(preds.challenge.hybrid,2026,0));assert.throws(()=>E.bootstrapAccuracy(preds.challenge.hybrid,2026,5001));});
test('different model predictions can be compared',()=>{const x=E.compareRuns(preds.challenge.nb,preds.challenge.hybrid);assert.ok(x.changed>=0&&x.bWins>=0&&x.aWins>=0);});
test('CSV escape commas and quoted fields',()=>{const csv=E.toCSV([{a:'"x,y"',b:'ok'}],['a','b']);assert.equal(csv,'a,b\r\n"""x,y""",ok');});
test('test labels are not features in model input',()=>{const one=challenge[0];const relabeled={...one,label:one.label==='result'?'background':'result'};assert.equal(E.evaluate(model,[one],'word')[0].pred,E.evaluate(model,[relabeled],'word')[0].pred);});
test('model version tracks release',()=>{assert.match(E.version(),/^2\.0\.0$/);});