import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { STATISTICS_COLUMNS, statisticsRows, validateReportStatistics } from '../skills/ksf-workshop-runtime/scripts/report-statistics.mjs'; import { renderReportHtml } from '../skills/ksf-workshop-runtime/scripts/render-report-html.mjs'; import { prepareHtmlReport, saveAndVerifyReport } from '../skills/ksf-workshop-runtime/scripts/save-report.mjs'; import { statisticsRows as boardStatisticsRows } from '../src/core/report-statistics.js'; const section = () => ({ id: 'statistics-summary', kind: 'table', data: { cols: [...STATISTICS_COLUMNS], rows: [ ['支持A的记录', 4, 10, '合成测试|逐条判定10条有效记录;同主题去重;非真实资料', '部分支持,不等于结论成立;反证仍需比较', '先核对照样品,不自动改变产品', 'e'], ] } }); const task = () => ({ sessionId: 'test-person', taskKey: 'statistics-test', version: 1, artifactType: 'voc_report', title: '隔离合成统计测试', report: { executive: ['合成测试,不是研究结果'], delivery: { schema: 'ksf.workshop-delivery.v1', moduleId: 'D2-02', kind: 'research', contentStatus: 'draft', outputs: [] }, chapters: [{ title: '全体相关材料统计', sections: [section()] }], evidence: { e: { claim: '测试支持材料', quotes: [{ text: '合成原话,只用于测试', ref: 'https://example.org/test' }] } } } }); test('统计卡从同一n/N计算占比,保留范围、限制、产品含义和实际证据键', () => { const [row] = statisticsRows(section()); assert.equal(row.percent, '40.0%');assert.equal(row.ratio, 0.4); const html = renderReportHtml(task()); for (const value of ['40.0%', 'width:40%', '逐条判定10条有效记录', '部分支持,不等于结论成立', '先核对照样品', '合成原话,只用于测试']) assert.ok(html.includes(value), value); assert.match(html, /href="#evidence-1"/);assert.match(html, /查看统计汇总表与口径/); }); test('未核分母或无有效材料不编造百分比,已确认零提及保留0%', () => { for (const [n, N] of [[null, null], [4, null], [null, 10], [0, 0]]) { const t = task();t.report.chapters[0].sections[0].data.rows[0].splice(1, 2, n, N); assert.equal(statisticsRows(t.report.chapters[0].sections[0])[0].percent, null); assert.doesNotMatch(renderReportHtml(t), /role="img" aria-label="本样本内占比|NaN%|Infinity%/); } const s = section();s.data.rows[0][1] = 0;assert.equal(statisticsRows(s)[0].percent, '0.0%'); }); test('非法数值、口径缺失、错列和不存在的证据键在保存前拒绝', async () => { for (const [n, N] of [[11, 10], [-1, 10], [4.5, 10], ['4', 10], [1, 0], [1, -1]]) { const s = section();s.data.rows[0].splice(1, 2, n, N);assert.throws(() => statisticsRows(s), { code: 'REPORT_STATISTICS_INVALID' }); } const s = section();s.data.rows[0][3] = '';assert.throws(() => statisticsRows(s), { code: 'REPORT_STATISTICS_INVALID' }); const t = task();t.report.evidence = {};let called = false; assert.throws(() => renderReportHtml(t), { code: 'REPORT_STATISTICS_INVALID' }); await assert.rejects(saveAndVerifyReport(t, { requestId: 'test', invoke: async () => { called = true; } }), { code: 'REPORT_STATISTICS_INVALID' }); assert.equal(called, false); const wrong = section();wrong.data.cols.reverse();assert.throws(() => statisticsRows(wrong), { code: 'REPORT_STATISTICS_INVALID' }); assert.equal(statisticsRows({ kind: 'table', data: { rows: [] } }), null, '普通表不受此语义模板限制'); }); test('多标签主题可重叠,不要求各主题相加等于100%,原始数量不被修改', () => { const s = section();s.data.rows[0][1] = 8;s.data.rows.push(['主题B', 7, 10, '同10条合成测试记录;多标签重叠', '不能相加为总体比例', '比较场景差异', '']); const original = structuredClone(s), rows = statisticsRows(s); assert.deepEqual(rows.map(r => r.percent), ['80.0%', '70.0%']);assert.deepEqual(s, original); }); test('看板排版薄空格不改变统计识别,浏览器与HTML的统计校验保持同步', () => { const s = section();s.data.cols = s.data.cols.map(value => value.replaceAll(' ', '\u2009')); const original = structuredClone(s); assert.equal(statisticsRows(s)[0].percent, '40.0%'); assert.deepEqual(boardStatisticsRows(s), statisticsRows(s)); assert.deepEqual(s, original, '仅对列名匹配消除排版空白,不改写数据'); s.data.rows[0][1] = 11; for (const parse of [statisticsRows, boardStatisticsRows]) assert.throws(() => parse(s), { code: 'REPORT_STATISTICS_INVALID' }); assert.equal(fs.readFileSync(new URL('../src/core/report-statistics.js', import.meta.url), 'utf8'), '// Generated by scripts/build-workshop-skill.mjs; maintain the Skill report-statistics.mjs source.\n' + fs.readFileSync(new URL('../skills/ksf-workshop-runtime/scripts/report-statistics.mjs', import.meta.url), 'utf8')); }); test('统计表完整保留在默认HTML附件与同版report,生成器不从精选引文数量推断分子', t => { const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'ksf-statistics-test-')); t.after(() => fs.rmSync(directory, { recursive: true, force: true })); const original = task(), prepared = prepareHtmlReport(original, { directory, dataMode: 'sample' }); assert.deepEqual(prepared.task.report.chapters, original.report.chapters); const html = fs.readFileSync(prepared.htmlPath, 'utf8');assert.match(html, /40.0%/); assert.equal(original.report.evidence.e.quotes.length, 1);assert.equal(statisticsRows(original.report.chapters[0].sections[0])[0].numerator, 4); validateReportStatistics(original.report); });