'use strict'; const assert = require('node:assert/strict'); const SS = require('../js/skin-score'), UI = require('../js/report-ui'); const keys = ['closed_comedones','acne','acne_pustule','acne_nodule','acne_mark']; const node = counts => Object.fromEntries(keys.map((key,i)=>[key,counts[i]===null?{}:{count:counts[i]}])); function run(front,left,right,score=100){ const r=structuredClone(SS.MOCK_RESULT);Object.assign(r,node(front));r.score_info.acne_score=score; const sides={left:{left_acne_info:{...node(left),score:0}},right:{right_acne_info:{...node(right),score:0}}}; const before=JSON.stringify({r,sides});const vm=SS.buildViewModel(r,sides,{}); assert.equal(JSON.stringify({r,sides}),before,'raw inputs unchanged'); return {vm,cat:vm.cats.acne,view:UI.build(vm,r).views.find(v=>v.key==='acne')}; } const result=run([2,0,0,0,3],[5,1,0,0,1],[3,1,0,0,4]); assert.deepEqual(Object.values(result.cat.vars.acne_counts),[5,1,0,0,4]); assert.equal(result.cat.vars.acne_scoring.deduction,13);assert.equal(result.cat.P,87); assert.equal(result.cat.vars.acne_cnt,10);assert.equal(result.cat.vars.acne_detail,'闭口粉刺5处、丘疹1处、痘印4处'); assert.deepEqual(result.cat.vars.acne_scoring.types[1].sources,['left','right']); assert.equal(result.vm.dims.stab.S,9.61);assert(result.view.text.includes('丘疹1处')); assert.equal(run([2,0,0,0,3],[5,1,0,0,1],[3,1,0,0,4],80).cat.P,70,'front score mapping still limits score'); const heavy=run([0,0,0,0,0],[0,0,1,0,0],[0,0,0,1,0]); assert.equal(heavy.cat.P,90);assert.equal(heavy.view.level,'优秀'); assert(heavy.view.text.includes('脓包1处')&&heavy.view.text.includes('结节1处')); assert(heavy.view.text.includes('含脓包、结节')); const empty=run([null,null,null,null,null],[null,null,null,null,null],[null,null,null,null,null]); assert.equal(empty.cat.P,100);assert.equal(empty.cat.vars.acne_cnt,null);assert(empty.view.text.includes('未获取完整')); assert.equal(empty.cat.vars.acne_scoring.deduction,null); const partial=run([null,0,null,null,null],[null,null,null,null,null],[null,null,null,null,null]); assert.equal(partial.cat.vars.acne_counts_complete,false);assert(partial.view.text.includes('部分类型数据缺失')); assert.equal(run([null,null,null,null,null],[null,null,null,null,null],[null,null,null,null,null],null).cat.P,null); const capped=run([100,100,100,100,100],[0,0,0,0,0],[0,0,0,0,0]); assert.equal(capped.cat.P,50);assert.equal(capped.cat.vars.acne_scoring.raw_deduction,1450);assert.equal(capped.cat.vars.acne_scoring.deduction,50); const r=structuredClone(SS.MOCK_RESULT);r.acne={rectangle:[{},{}]}; let vm=SS.buildViewModel(r,null,{});assert.equal(vm.cats.acne.vars.acne_counts.acne,2,'rectangle fallback'); for(const n of [-1,1.5,NaN,Infinity]){r.acne={count:n};vm=SS.buildViewModel(r,null,{});assert.equal(vm.cats.acne.vars.acne_counts.acne,null);} const zero=run([0,0,0,0,0],[null,null,null,null,null],[null,null,null,null,null]); assert.equal(zero.cat.P,100);assert.equal(zero.cat.vars.acne_counts_complete,true);assert(zero.view.text.includes('未检出明显皮损')); console.log('PASS acne maxima per type, no sum/view selection, front-score mapping, heavy side lesions/copy, missing vs zero, cap50, source audit, invalid counts and unchanged inputs');