workshop-outputs.test.js 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. import test from 'node:test';
  2. import assert from 'node:assert/strict';
  3. import { buildModel } from '../src/core/kfs/build.js';
  4. import { workshopReview, workshopOutputRefs, readWorkshopQuery, OUTPUT_GROUP_NONE } from '../src/core/workshop-outputs.js';
  5. import { reportCenter, entriesForType, stageOutcome } from '../src/core/library/reports.js';
  6. import { DELIVERY_SCHEMA } from '../skills/ksf-workshop-runtime/scripts/report-contract.mjs';
  7. const chapter = (id, text = '产品判断与支持依据') => ({ id, title: id, sections: [{ kind: 'para', text }] });
  8. const artifact = (id, other = {}) => ({ id, memberId: 'member-a', title: '测试报告', workshopKey: 'topic-a',
  9. reportSeriesKey: 'topic-a:D1-01:report', version: 1, dataMode: 'live', report: { chapters: [chapter('market-analysis')] }, ...other });
  10. const groups = { groups: [{ id: 'g1', name: '一组' }, { id: 'g2', name: '二组' }], assignments: { 'member-a': 'g1', 'member-b': 'g2' } };
  11. const review = (artifacts, q = {}, state = groups, members = []) => workshopReview({ artifacts, members }, state, q);
  12. test('归一化保留真实任务系列、版本、课题和模式,非法版本保持未知', () => {
  13. const raw = { artifact: [{ objectId: 'one', title: '报告', reportSeriesKey: 't:D1-01:r', workshopKey: 't',
  14. dataMode: 'sample', version: 2, sessionRef: { objectId: 's' } }, { objectId: 'two', version: '3' }] };
  15. const db = buildModel(raw), a = db.artifactById.one;
  16. assert.equal(a.reportSeriesKey, 't:D1-01:r'); assert.equal(a.workshopKey, 't');
  17. assert.equal(a.version, 2); assert.equal(a.dataMode, 'sample'); assert.equal(a.sessionId, 's');
  18. assert.equal(db.artifactById.two.version, null);
  19. });
  20. test('只按有正文的明确章节或任务定位;标题和成果类型不会把运行记录当提案', () => {
  21. assert.equal(workshopOutputRefs(artifact('a', { reportSeriesKey: '', report: null, title: '市场分析概念提案', type: 'concept' })).refs.length, 0);
  22. assert.equal(workshopOutputRefs(artifact('a', { report: { chapters: [chapter('market-analysis', '')] } })).refs.length, 0);
  23. assert.equal(workshopOutputRefs(artifact('a', { reportSeriesKey: '', report: { chapters: [chapter('market-analysis'), chapter('social-needs')] } })).refs.length, 2);
  24. assert.equal(workshopOutputRefs(artifact('a', { report: null, rawText: '真实的比较正文' })).refs[0].basis, 'task');
  25. assert.equal(workshopOutputRefs(artifact('a', { reportSeriesKey: 't:D2-08:proposal', report: { chapters: [chapter('conclusion')] } })).refs[0].key, 'proposal');
  26. });
  27. test('明确标为方法材料的配置卡不因课表环节被计作核心市场报告', () => {
  28. const a = artifact('method', { type: 'category_report', report: { executive: [],
  29. delivery: { schema: DELIVERY_SCHEMA, moduleId: 'D1-01', kind: 'method', contentStatus: 'ready', outputs: [] },
  30. chapters: [chapter('configuration')] } });
  31. const view = review([a]);
  32. assert.equal(view.rows[0].reportType, 'method');assert.deepEqual(view.rows[0].refs, []);
  33. });
  34. test('默认区分真实资料、样例、人工和未知,不根据标题猜模式', () => {
  35. const rows = ['live', 'sample', 'manual', ''].map((dataMode, i) => artifact(String(i), { dataMode }));
  36. assert.deepEqual(review(rows).rows.map(r => r.id), ['0']);
  37. assert.equal(review(rows, { mode: 'all' }).rows.length, 4);
  38. assert.deepEqual(review(rows, { mode: 'unknown' }).rows.map(r => r.id), ['3']);
  39. assert.deepEqual(readWorkshopQuery({ mode: 'bogus', stage: 'D1', reportType: 'bogus' }), { group: '', workshop: '', mode: 'live', stage: '', reportType: '', search: '', history: false });
  40. });
  41. test('共享稳定身份决定分组;同名学员各自归属,失效组不按原快照猜', () => {
  42. const rows = [artifact('a', { ownerName: '同名' }), artifact('b', { ownerName: '同名', memberId: 'member-b' }), artifact('c', { memberId: 'unknown', groupId: 'g1' })];
  43. assert.deepEqual(review(rows, { group: 'g2' }).rows.map(r => r.id), ['b']);
  44. assert.equal(review(rows, { group: OUTPUT_GROUP_NONE }).rows[0].id, 'c');
  45. assert.equal(review(rows, {}, { ...groups, assignments: { 'member-a': 'deleted-group' } }).ungrouped, 3);
  46. });
  47. test('同一稳定课题、作者、系列选择最新版本;同版本冲突及未知版本不隐藏', () => {
  48. const rows = [artifact('v1'), artifact('v2', { version: 2 }), artifact('conflict', { version: 2 }), artifact('unknown', { version: null })];
  49. assert.deepEqual(review(rows).rows.map(r => r.id).sort(), ['conflict', 'unknown', 'v2']);
  50. assert.equal(review(rows).rows[0].historyCount, 4);
  51. assert.equal(review(rows, { history: '1' }).rows.length, 4);
  52. assert.equal(review([artifact('a'), artifact('b', { memberId: 'member-b', version: 2 }), artifact('c', { workshopKey: 'topic-b', version: 3 })]).rows.length, 3);
  53. assert.equal(review([artifact('live'), artifact('sample', { version: 5, dataMode: 'sample' })], { mode: 'all' }).rows.length, 2);
  54. });
  55. test('缺系列或稳定身份时保留每条记录,不按相同标题合并', () => {
  56. for (const missing of [{ reportSeriesKey: '' }, { memberId: '' }, { workshopKey: '' }]) {
  57. const out = review([artifact('one', missing), artifact('two', { ...missing, version: 2 })]);
  58. assert.equal(out.rows.length, 2); assert.equal(out.missingSeries, 2);
  59. }
  60. });
  61. test('课题、小组、阶段、报告分类和搜索可共同筛选,分类统计落在个人报告上', () => {
  62. const rows = [artifact('target', { type: 'category_report', stageObj: { code: 'S1' } }), artifact('other-topic', { workshopKey: 't2' }), artifact('other-person', { type: 'competitor_report', memberId: 'member-b', stageObj: { code: 'S1' } })];
  63. const q = { workshop: 'topic-a', group: 'g1', stage: 'S1', reportType: 'category', search: 'target' };
  64. const out = review(rows, q);
  65. assert.deepEqual(out.rows.map(r => r.id), ['target']);
  66. assert.equal(out.categories.find(category => category.key === 'category').count, 1);
  67. assert.equal(out.categories.find(category => category.key === 'competitor').count, 0);
  68. assert.equal(review(rows, {}, null).groupStateAvailable, false);
  69. });
  70. test('成果目录按小组、成员、报告展开,并保留当前筛选下没有报告的成员', () => {
  71. const members = [
  72. { id: 'member-a', name: '甲' },
  73. { id: 'member-b', name: '乙' },
  74. { id: 'member-c', name: '丙' },
  75. ];
  76. const rows = [
  77. artifact('a1', { memberId: 'member-a', ownerName: '甲', reportSeriesKey: 'topic-a:D1-01:report' }),
  78. artifact('a2', { memberId: 'member-a', ownerName: '甲', reportSeriesKey: 'topic-a:D1-02:report' }),
  79. artifact('b1', { memberId: 'member-b', ownerName: '乙', reportSeriesKey: 'topic-a:D1-03:report' }),
  80. ];
  81. const out = review(rows, { mode: 'all' }, groups, members);
  82. const first = out.hierarchy.find(group => group.id === 'g1');
  83. const second = out.hierarchy.find(group => group.id === 'g2');
  84. const ungrouped = out.hierarchy.find(group => group.id === OUTPUT_GROUP_NONE);
  85. assert.deepEqual(first.members.map(member => [member.name, member.reportCount]), [['甲', 2]]);
  86. assert.deepEqual(first.members[0].reports.map(row => row.id), ['a1', 'a2']);
  87. assert.deepEqual(first.members[0].reports[0].refs.map(ref => ref.key), ['market']);
  88. assert.equal(first.members.length, 1);
  89. assert.equal(first.memberCount, 1);
  90. assert.deepEqual(second.members.map(member => [member.name, member.reportCount]), [['乙', 1]]);
  91. assert.deepEqual(ungrouped.members.map(member => [member.name, member.reportCount]), [['丙', 0]]);
  92. assert.equal(review(rows, { mode: 'all', reportType: 'unclassified' }, groups, members).rows.length, 3);
  93. });
  94. test('分类按钮筛选的是组员自己的报告,分类缺失的报告仍归入未归类', () => {
  95. const rows = [
  96. artifact('market', { type: 'category_report' }),
  97. artifact('competitor', { type: 'competitor_report', reportSeriesKey: 'topic-a:D1-02:report' }),
  98. artifact('unknown', { type: '', reportSeriesKey: 'topic-a:D1-03:report' }),
  99. ];
  100. const out = review(rows, { mode: 'all', reportType: 'competitor' }, groups, [{ id: 'member-a', name: '甲' }]);
  101. assert.deepEqual(out.rows.map(row => [row.id, row.reportType]), [['competitor', 'competitor']]);
  102. assert.equal(out.categories.find(category => category.key === 'category').count, 1);
  103. assert.equal(out.categories.find(category => category.key === 'competitor').count, 1);
  104. assert.equal(out.categories.find(category => category.key === 'unclassified').count, 1);
  105. assert.deepEqual(out.hierarchy.flatMap(group => group.members.flatMap(member => member.reports.map(row => row.id))), ['competitor']);
  106. });
  107. test('成员姓名搜索保留零报告成员,报告搜索只显示命中的作者和小组', () => {
  108. const members = [{ id: 'member-a', name: '甲' }, { id: 'member-c', name: '丙' }];
  109. const rows = [artifact('a1', { memberId: 'member-a', ownerName: '甲' })];
  110. const noReport = review(rows, { mode: 'all', search: '丙' }, groups, members);
  111. assert.equal(noReport.rows.length, 0);
  112. assert.deepEqual(noReport.hierarchy.map(group => [group.name, group.members.map(member => member.name)]), [['未分组', ['丙']]]);
  113. const report = review(rows, { mode: 'all', search: '甲' }, groups, members);
  114. assert.deepEqual(report.hierarchy.map(group => [group.name, group.members.map(member => member.name)]), [['一组', ['甲']]]);
  115. assert.deepEqual(report.hierarchy[0].members[0].reports.map(row => row.id), ['a1']);
  116. });
  117. test('评估、运行记录与概念提案分开,同时都可从总清单打开', () => {
  118. const db = buildModel({ artifact: [{ objectId: 'accept', artifactType: 'assessment_report', title: '运行验收', stageCode: 'S3' }, { objectId: 'concept', artifactType: 'concept', stageCode: 'S3' }] });
  119. assert.deepEqual(entriesForType(db, 'concept').map(r => r.id), ['concept']);
  120. assert.deepEqual(entriesForType(db, 'assessment').map(r => r.id), ['accept']);
  121. assert.equal(reportCenter(db).total, 2);
  122. assert.equal(stageOutcome(db, 'S3').total, 2);
  123. assert.equal(stageOutcome(db, 'S3').groups.find(g => g.key === 'assessment').items[0].id, 'accept');
  124. });