import test from 'node:test'; import assert from 'node:assert/strict'; import { COURSE_DELIVERABLES, courseModuleOf } from '../skills/ksf-workshop-runtime/scripts/course-deliverables.mjs'; import { DELIVERY_SCHEMA } from '../skills/ksf-workshop-runtime/scripts/report-contract.mjs'; import { workshopReview } from '../src/core/workshop-outputs.js'; import { REPORT_TYPES } from '../src/core/library/types.js'; const chapter = text => ({ id: 'analysis', title: '实际正文', sections: [{ kind: 'para', text }] }); const row = (moduleId, overrides = {}) => ({ id: moduleId, type: 'assessment_report', memberId: 'employee-a', workshopKey: 'topic-a', dataMode: 'live', version: 1, reportSeriesKey: `topic-a:${moduleId}:report`, report: { chapters: [chapter('该环节已保存的实质内容')] }, ...overrides }); const view = artifacts => workshopReview({ artifacts, members: [{ id: 'employee-a', name: '甲' }, { id: 'employee-b', name: '乙' }] }, { groups: [], assignments: {} }, { mode: 'all' }); const topics = result => result.hierarchy.flatMap(group => group.members.flatMap(member => member.topics)); test('13 个环节覆盖完整课程,11 个阅读分类和七类内容索引不能代替课程清单', () => { assert.deepEqual(COURSE_DELIVERABLES.map(output => output.moduleId), [ 'D1-01', 'D1-02', 'D1-03', 'D1-04', 'D1-N', 'D2-01', 'D2-02', 'D2-03', 'D2-04', 'D2-05', 'D2-06', 'D2-07', 'D2-08', ]); assert.equal(REPORT_TYPES.length, 11); const topic = topics(view(COURSE_DELIVERABLES.map(output => row(output.moduleId))))[0]; assert.equal(topic.courseOutputs.length, 13); assert.equal(topic.courseOutputs.filter(output => output.state === 'has-body').length, 13); assert.equal(topic.outputs.length, 7, '旧内容索引保留兼容,不影响13项环节导航'); }); test('同一环节多报告或附件不增加环节份数,配置卡、任务卡、验收与指标各有位置', () => { const artifacts = ['D1-02', 'D1-04', 'D2-01', 'D2-03', 'D2-04'].map(moduleId => row(moduleId)); artifacts.push(row('D2-04', { id: 'card-two', reportSeriesKey: 'topic-a:D2-04:card-two', report: { chapters: [chapter('第二张候选卡')], attachments: [{ name: 'report.html' }] } })); const ledger = topics(view(artifacts))[0].courseOutputs; assert.equal(ledger.filter(output => output.state === 'has-body').length, 5); assert.equal(ledger.find(output => output.moduleId === 'D2-04').reports.length, 2); }); test('旧系列接受冒号或斜线,标题猜测、非法交付标记和互相矛盾的环节不计数', () => { assert.equal(courseModuleOf(row('D1-01', { reportSeriesKey: 'topic-a/D1-01' })), 'D1-01'); assert.equal(courseModuleOf(row('D1-01', { reportSeriesKey: '', title: 'D1-01 品类全景图谱' })), ''); const delivery = { schema: DELIVERY_SCHEMA, moduleId: 'D1-02', kind: 'method', outputs: [], contentStatus: 'ready' }; assert.equal(courseModuleOf(row('D1-01', { report: { delivery, chapters: [chapter('冲突内容')] } })), ''); assert.equal(courseModuleOf(row('D1-02', { report: { delivery: { ...delivery, schema: 'invalid' }, chapters: [chapter('正文')] } })), ''); const topic = topics(view([row('D1-01', { reportSeriesKey: '', title: 'D1-01 品类全景图谱' })]))[0]; assert.equal(topic.courseUnlocatedCount, 1); assert.equal(topic.courseOutputs.every(output => output.state === 'missing'), true); }); test('仅原文、草稿、空章节、未保存分别显示,有正文不被称为质量通过或员工确认', () => { const artifacts = [row('D1-01', { report: null, rawText: '只有旧原文' }), row('D1-02', { report: { delivery: { schema: DELIVERY_SCHEMA, moduleId: 'D1-02', kind: 'method', contentStatus: 'draft', outputs: [] }, chapters: [chapter('配置草稿')] } }), row('D1-03', { report: { chapters: [chapter('')] } })]; const ledger = topics(view(artifacts))[0].courseOutputs; assert.deepEqual(ledger.slice(0, 4).map(output => output.state), ['text-only', 'draft', 'missing-body', 'missing']); assert.equal(ledger.some(output => output.state === 'completed' || output.state === 'accepted'), false); }); test('最新版缺正文不能借用历史版补齐,作者、课题与模式的报告不能互相补足', () => { const artifacts = [row('D1-01'), row('D1-01', { id: 'v2', version: 2, report: { chapters: [chapter('')] } }), row('D1-02', { memberId: 'employee-b' }), row('D1-03', { workshopKey: 'topic-b' }), row('D1-04', { dataMode: 'sample' })]; const result = view(artifacts); const employee = result.hierarchy.flatMap(group => group.members).find(member => member.id === 'employee-a'); const current = employee.topics.find(topic => topic.workshopKey === 'topic-a' && topic.mode === 'live'); assert.deepEqual(current.courseOutputs.slice(0, 4).map(output => output.state), ['missing-body', 'missing', 'missing', 'missing']); assert.equal(current.courseOutputs[0].reports.length, 1); assert.equal(current.courseOutputs[0].reports[0].id, 'v2'); });