#!/usr/bin/env node import assert from 'node:assert/strict'; import test from 'node:test'; import { parseCsv, verifyPilotExportContract } from '../lib/pilot-export-contract.mjs'; const startDate = '2026-07-07'; const endDate = '2026-07-10'; function validCsv() { const metrics = { pilot_start_date: startDate, pilot_end_date: endDate, data_scope: 'active_org_unique_identity', scenario_count: '12', scenario_count_target: '12', formal_org_identity_count: '20', formal_pilot_people_count: '20', formal_pilot_people_target: '20', formal_pilot_project_count: '1', formal_pilot_project_target: '1-2', formal_pilot_scope_ready: 'true', training_count: '1', completion_definition: 'at_least_10_sessions_per_person', completion_people_count: '1', completion_people_total: '20', completion_rate: '5.0%', completion_rate_target: '80.0%', completion_rate_ready: 'false', average_score: '86.0', alert_count: '0', review_rate: '100.0%', calibration_sample_count: '1', ai_human_band_matched_count: '1', calibration_sample_target: '20', calibration_sample_ready: 'false', ai_human_band_consistency_rate: '100.0%', ai_human_band_consistency_target: '70.0%', ai_human_band_consistency_ready: 'false', sop_review_sample_count: '2', sop_answer_usable_count: '2', sop_answer_usable_rate: '100.0%', sop_answer_usable_target: '80.0%', sop_answer_usable_ready: 'true', pilot_metric_ready: 'false' }; const lines = ['metric,value', ...Object.entries(metrics).map(([key, value]) => `${key},${value}`)]; lines.push('', 'formal_ext_party_id,project_codes,session_id,trainee,scene,score,status,finished_time,summary,review_advice,incentive_point,ai_score,calibration_original_score,human_score,calibration_reviewer,calibration_time,final_adopted_score'); lines.push('FORMAL-1,P1,session-1,员工甲,投诉处理,86,已复盘,2026-07-08 10:00:00,正式记录,保持回访,3,86,86,78,主管甲,2026-07-08 11:00:00,78'); return lines.join('\n'); } test('accepts a complete formal export whose training summary matches its details', () => { const result = verifyPilotExportContract({ csv: validCsv(), startDate, endDate, contentDisposition: 'attachment; filename=aihr-practice-pilot-2026-07-07-2026-07-10.csv' }); assert.equal(result.metrics.get('training_count'), '1'); assert.equal(result.detailRows.length, 1); }); test('rejects a missing metric and a detail row outside the requested date window', () => { assert.throws(() => verifyPilotExportContract({ csv: validCsv().replace('calibration_sample_count,1\n', ''), startDate, endDate, contentDisposition: 'attachment; filename=aihr-practice-pilot-2026-07-07-2026-07-10.csv' }), /missing metric calibration_sample_count/); assert.throws(() => verifyPilotExportContract({ csv: validCsv().replace('2026-07-08 10:00:00', '2026-07-11 10:00:00'), startDate, endDate, contentDisposition: 'attachment; filename=aihr-practice-pilot-2026-07-07-2026-07-10.csv' }), /outside requested window/); }); test('rejects contradictory pilot metrics and incomplete formal details', () => { assert.throws(() => verifyPilotExportContract({ csv: validCsv().replace('completion_people_total,20', 'completion_people_total,19'), startDate, endDate, contentDisposition: 'attachment; filename=aihr-practice-pilot-2026-07-07-2026-07-10.csv' }), /completion_people_total/); assert.throws(() => verifyPilotExportContract({ csv: validCsv().replace('training_count,1', 'training_count,2'), startDate, endDate, contentDisposition: 'attachment; filename=aihr-practice-pilot-2026-07-07-2026-07-10.csv' }), /does not match detail rows/); assert.throws(() => verifyPilotExportContract({ csv: validCsv().replace('FORMAL-1,P1,session-1', ',P1,session-1'), startDate, endDate, contentDisposition: 'attachment; filename=wrong.csv' }), /missing formal identity/); assert.throws(() => verifyPilotExportContract({ csv: validCsv().replace('ai_human_band_consistency_rate,100.0%', 'ai_human_band_consistency_rate,0.0%'), startDate, endDate, contentDisposition: 'attachment; filename=aihr-practice-pilot-2026-07-07-2026-07-10.csv' }), /ai_human_band_consistency_rate/); }); test('parses valid quoted CSV fields and rejects malformed quoting', () => { assert.deepEqual(parseCsv('metric,value\nsummary,"逗号,转义""引号""\n第二行"\n'), [ ['metric', 'value'], ['summary', '逗号,转义"引号"\n第二行'] ]); assert.deepEqual(parseCsv('""'), [['']]); assert.throws(() => parseCsv('metric,value\nsummary,"未闭合'), /unclosed quoted field/); assert.throws(() => parseCsv('metric,value\nsummary,值"错误'), /quote must start a field/); assert.throws(() => parseCsv('metric,value\nsummary,"值"错误'), /unexpected character after closing quote/); assert.throws(() => verifyPilotExportContract({ csv: validCsv().replace('metric,value', '"metric,value"'), startDate, endDate, contentDisposition: 'attachment; filename=aihr-practice-pilot-2026-07-07-2026-07-10.csv' }), /missing metric,value header/); }); test('uses raw numerators for threshold readiness instead of rounded percentages', () => { const csv = validCsv() .replace('calibration_sample_count,1', 'calibration_sample_count,203') .replace('ai_human_band_matched_count,1', 'ai_human_band_matched_count,142') .replace('ai_human_band_consistency_rate,100.0%', 'ai_human_band_consistency_rate,70.0%') .replace('calibration_sample_ready,false', 'calibration_sample_ready,true') .replace('sop_review_sample_count,2', 'sop_review_sample_count,2049') .replace('sop_answer_usable_count,2', 'sop_answer_usable_count,1639') .replace('sop_answer_usable_rate,100.0%', 'sop_answer_usable_rate,80.0%') .replace('sop_answer_usable_ready,true', 'sop_answer_usable_ready,false'); assert.doesNotThrow(() => verifyPilotExportContract({ csv, startDate, endDate, contentDisposition: 'attachment; filename=aihr-practice-pilot-2026-07-07-2026-07-10.csv' })); });