quality-rewrite.test.ts 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. /**
  2. * 质量校验 & 改写优化节点 - 单元测试
  3. * 测试消息构建、响应解析等纯函数逻辑
  4. */
  5. import { strict as assert } from 'assert';
  6. // ====== 模拟数据 ======
  7. // 模拟 quality-check 响应
  8. const mockQualityPassResponse = JSON.stringify({
  9. overallScore: 92,
  10. overallAssessment: "全书内容质量优秀,章节逻辑清晰,通顺性好",
  11. passedChapters: [1, 2, 3, 4, 5],
  12. failedChapters: []
  13. });
  14. const mockQualityFailResponse = JSON.stringify({
  15. overallScore: 68,
  16. overallAssessment: "部分章节存在逻辑和跑题问题,需要改写",
  17. passedChapters: [1, 4],
  18. failedChapters: [
  19. {
  20. chapterNumber: 2,
  21. chapterTitle: "变量与数据类型",
  22. scores: { fluency: 20, logic: 12, relevance: 15, completeness: 18 },
  23. totalScore: 65,
  24. issues: [
  25. {
  26. dimension: "logic",
  27. severity: "high",
  28. description: "变量作用域的解释与前面声明周期的概念矛盾",
  29. location: "第3段",
  30. suggestion: "统一变量作用域的定义,参考MDN标准术语"
  31. }
  32. ],
  33. rewriteInstructions: "修复逻辑矛盾,统一变量作用域概念"
  34. },
  35. {
  36. chapterNumber: 3,
  37. chapterTitle: "控制流",
  38. scores: { fluency: 15, logic: 14, relevance: 10, completeness: 14 },
  39. totalScore: 53,
  40. issues: [
  41. {
  42. dimension: "relevance",
  43. severity: "high",
  44. description: "大段内容在讲数据结构而非控制流,严重跑题",
  45. location: "第2段开始",
  46. suggestion: "裁减数据结构内容,聚焦if/switch/loop等控制流语句"
  47. },
  48. {
  49. dimension: "fluency",
  50. severity: "medium",
  51. description: "多处句子过长,可读性差",
  52. location: "全文多处",
  53. suggestion: "拆分长句,每句不超过30字"
  54. }
  55. ],
  56. rewriteInstructions: "去除跑题内容,拆分长句,精简文笔"
  57. }
  58. ]
  59. });
  60. // 模拟质量校验带 think 标签的响应
  61. const mockQualityWithThink = `<think>这个内容需要仔细评估...</think>
  62. ${JSON.stringify({
  63. overallScore: 75,
  64. overallAssessment: "中等水平,第3章有轻微问题",
  65. passedChapters: [1, 2, 4, 5],
  66. failedChapters: [
  67. {
  68. chapterNumber: 3,
  69. chapterTitle: "数组与集合",
  70. scores: { fluency: 18, logic: 16, relevance: 15, completeness: 17 },
  71. totalScore: 66,
  72. issues: [],
  73. rewriteInstructions: "需要增加代码示例"
  74. }
  75. ]
  76. })}`;
  77. // 模拟 rewrite 响应
  78. const mockRewriteResponse = JSON.stringify({
  79. chapters: [
  80. {
  81. number: 2,
  82. title: "变量与数据类型",
  83. rewrittenContent: "# 第2章 变量与数据类型\n\n修正后的内容...",
  84. changesSummary: "修复了变量作用域的矛盾表述"
  85. }
  86. ]
  87. });
  88. // ====== 解析函数(从源文件摘取核心逻辑用于测试)======
  89. function parseJSONResponse(raw: string) {
  90. let cleaned = raw.trim();
  91. const thinkEnd = cleaned.indexOf('</think>');
  92. if (thinkEnd !== -1) cleaned = cleaned.substring(thinkEnd + 8).trim();
  93. const match = cleaned.match(/\{[\s\S]*\}/);
  94. return match ? JSON.parse(match[0]) : null;
  95. }
  96. function parseQualityCheckResponse(response: string) {
  97. try {
  98. const data = parseJSONResponse(response);
  99. if (!data) return null;
  100. return {
  101. overallScore: data.overallScore || 0,
  102. overallAssessment: data.overallAssessment || '',
  103. passedChapters: data.passedChapters || [],
  104. failedChapters: (data.failedChapters || []).map((fc: any) => ({
  105. chapterNumber: fc.chapterNumber || 0,
  106. chapterTitle: fc.chapterTitle || '',
  107. scores: {
  108. fluency: fc.scores?.fluency ?? 0,
  109. logic: fc.scores?.logic ?? 0,
  110. relevance: fc.scores?.relevance ?? 0,
  111. completeness: fc.scores?.completeness ?? 0,
  112. },
  113. totalScore: fc.totalScore ?? 0,
  114. issues: fc.issues || [],
  115. rewriteInstructions: fc.rewriteInstructions || '请重新撰写本章',
  116. })),
  117. };
  118. } catch { return null; }
  119. }
  120. function parseRewriteResponse(response: string) {
  121. try {
  122. const data = parseJSONResponse(response);
  123. if (!data) return null;
  124. const chapters = data.chapters || data;
  125. if (Array.isArray(chapters) && chapters.length > 0) {
  126. return {
  127. title: chapters[0].title || '',
  128. rewrittenContent: chapters[0].rewrittenContent || chapters[0].content || '',
  129. changesSummary: chapters[0].changesSummary || '',
  130. };
  131. }
  132. return null;
  133. } catch { return null; }
  134. }
  135. // ====== 测试用例 ======
  136. let passed = 0, failed = 0;
  137. function test(name: string, fn: () => void) {
  138. try {
  139. fn();
  140. console.log(` ✅ ${name}`);
  141. passed++;
  142. } catch (e: any) {
  143. console.log(` ❌ ${name}: ${e.message}`);
  144. failed++;
  145. }
  146. }
  147. console.log('\n📋 质量校验 & 改写优化节点 - 单元测试\n');
  148. // ---- 质量校验响应解析 ----
  149. console.log('📝 质量校验响应解析:');
  150. test('解析全部通过的响应', () => {
  151. const result = parseQualityCheckResponse(mockQualityPassResponse);
  152. assert.ok(result, '解析结果不能为空');
  153. assert.strictEqual(result.overallScore, 92);
  154. assert.strictEqual(result.passedChapters.length, 5);
  155. assert.strictEqual(result.failedChapters.length, 0);
  156. assert.strictEqual(result.overallAssessment.includes('优秀'), true);
  157. });
  158. test('解析有不合格章节的响应', () => {
  159. const result = parseQualityCheckResponse(mockQualityFailResponse);
  160. assert.ok(result, '解析结果不能为空');
  161. assert.strictEqual(result.overallScore, 68);
  162. assert.strictEqual(result.failedChapters.length, 2);
  163. assert.strictEqual(result.failedChapters[0].chapterNumber, 2);
  164. assert.strictEqual(result.failedChapters[0].scores.logic, 12);
  165. assert.strictEqual(result.failedChapters[0].issues[0].severity, 'high');
  166. assert.strictEqual(result.failedChapters[1].issues.length, 2);
  167. });
  168. test('解析带 think 标签的响应', () => {
  169. const result = parseQualityCheckResponse(mockQualityWithThink);
  170. assert.ok(result, 'think标签不应影响解析');
  171. assert.strictEqual(result.overallScore, 75);
  172. assert.strictEqual(result.failedChapters.length, 1);
  173. });
  174. test('解析空字符串', () => {
  175. const result = parseQualityCheckResponse('');
  176. assert.strictEqual(result, null, '空字符串应返回null');
  177. });
  178. test('解析非法JSON', () => {
  179. const result = parseQualityCheckResponse('这不是JSON');
  180. assert.strictEqual(result, null, '非法JSON应返回null');
  181. });
  182. // ---- 改写响应解析 ----
  183. console.log('\n📝 改写响应解析:');
  184. test('解析正常改写响应', () => {
  185. const result = parseRewriteResponse(mockRewriteResponse);
  186. assert.ok(result, '解析结果不能为空');
  187. assert.strictEqual(result.title, '变量与数据类型');
  188. assert.ok(result.rewrittenContent.includes('修正后'), '应包含改写内容');
  189. assert.ok(result.changesSummary.length > 0, '应有改动摘要');
  190. });
  191. test('解析空响应', () => {
  192. const result = parseRewriteResponse('');
  193. assert.strictEqual(result, null, '空响应应返回null');
  194. });
  195. // ---- 质量评分阈值 ----
  196. console.log('\n📝 评分阈值逻辑:');
  197. test('总分≥80为通过', () => {
  198. const passResult = parseQualityCheckResponse(mockQualityPassResponse)!;
  199. const allPassed = passResult.overallScore >= 80 && passResult.failedChapters.length === 0;
  200. assert.strictEqual(allPassed, true);
  201. });
  202. test('总分<80且存在不合格章节为不通过', () => {
  203. const failResult = parseQualityCheckResponse(mockQualityFailResponse)!;
  204. const needsRewrite = failResult.overallScore < 80 || failResult.failedChapters.length > 0;
  205. assert.strictEqual(needsRewrite, true);
  206. });
  207. // ---- 改写指令完整性 ----
  208. console.log('\n📝 改写指令完整性:');
  209. test('不合格章节必须有改写指令', () => {
  210. const result = parseQualityCheckResponse(mockQualityFailResponse)!;
  211. for (const fc of result.failedChapters) {
  212. assert.ok(fc.rewriteInstructions.length > 0,
  213. `第${fc.chapterNumber}章缺少改写指令`);
  214. }
  215. });
  216. test('每条问题必须有四个维度之一', () => {
  217. const validDimensions = ['fluency', 'logic', 'relevance', 'completeness'];
  218. const result = parseQualityCheckResponse(mockQualityFailResponse)!;
  219. for (const fc of result.failedChapters) {
  220. for (const issue of fc.issues) {
  221. assert.ok(validDimensions.includes(issue.dimension),
  222. `无效维度: ${issue.dimension}`);
  223. }
  224. }
  225. });
  226. // ====== 结果汇总 ======
  227. console.log(`\n${'='.repeat(40)}`);
  228. console.log(`测试结果: ${passed} 通过, ${failed} 失败, ${passed + failed} 总计`);
  229. console.log(`${'='.repeat(40)}\n`);
  230. process.exit(failed > 0 ? 1 : 0);