import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import test from "node:test"; import { classifySectionErrorCode } from "../src/lib/personal-report-generation.ts"; import { summarizePersonalReportFailure } from "../src/lib/personal-report-failure-summary.ts"; import { PersonalReportAgentOutputError, classifyWriterRepairCategory, writerRepairPromptSuffix, } from "../src/mastra/personal-report.ts"; test("refs mismatch is not classified as evidence insufficient", () => { assert.equal( classifySectionErrorCode(new Error("report_writer_evidence_refs_mismatch")), "section_refs_mismatch", ); assert.equal( classifySectionErrorCode(new Error("report_writer_section_identity_mismatch")), "section_identity_mismatch", ); assert.equal( classifySectionErrorCode(new Error("Report evidence insufficient: planets_missing")), "section_evidence_insufficient", ); assert.equal( classifySectionErrorCode(new PersonalReportAgentOutputError("length")), "section_output_truncated", ); assert.equal(classifySectionErrorCode(new Error("section_output_invalid")), "section_output_invalid"); }); test("repair prompt names the failure category without carrying content", () => { const refs = writerRepairPromptSuffix({ error: new Error("report_writer_evidence_refs_mismatch") }); assert.match(refs, /失败类别:refs mismatch/); assert.match(refs, /evidenceRefs 必须逐字复制 plan\.evidenceRefs 数组,不得增删/); assert.doesNotMatch(refs, /ev-|Sun|Moon|宫/); const truncated = writerRepairPromptSuffix({ finishReason: "length", targetCharactersMin: 260 }); assert.equal(classifyWriterRepairCategory({ finishReason: "length" }), "output truncated"); assert.match(truncated, /失败类别:output truncated/); assert.match(truncated, /压缩到约 260 字/); const schema = writerRepairPromptSuffix({}); assert.match(schema, /失败类别:schema invalid/); }); test("failure summary is readable and stays at error-code granularity", () => { const truncated = summarizePersonalReportFailure({ themeCount: 4, failureCode: "report_schema_invalid", sections: [ { status: "blocked", lastErrorCode: "section_output_truncated" }, { status: "blocked", lastErrorCode: "section_output_truncated" }, { status: "blocked", lastErrorCode: "section_output_truncated" }, { status: "blocked", lastErrorCode: "section_output_truncated" }, ], }); assert.equal(truncated.summary, "4 个主题中 4 个写作失败:输出被截断"); assert.equal(truncated.innerReason, "all_sections_blocked"); const incomplete = summarizePersonalReportFailure({ themeCount: 4, failureCode: "report_schema_invalid", sections: [ { status: "blocked", lastErrorCode: "section_refs_mismatch" }, { status: "pending", lastErrorCode: null }, { status: "pending", lastErrorCode: null }, { status: "pending", lastErrorCode: null }, ], }); assert.equal(incomplete.summary, "4 个主题中 1 个写作失败:证据引用未对齐,3 个未开始"); assert.equal(incomplete.innerReason, "section_generation_incomplete"); assert.deepEqual(incomplete.lastErrorCodes, ["section_refs_mismatch"]); const assembledChapters = summarizePersonalReportFailure({ themeCount: 4, failureCode: "report_schema_invalid", sections: [ { status: "ready", lastErrorCode: null }, { status: "ready", lastErrorCode: null }, { status: "ready", lastErrorCode: null }, { status: "ready", lastErrorCode: null }, ], }); assert.equal(assembledChapters.summary, "4 个主题已写成,整份报告未完成装配"); assert.equal(assembledChapters.innerReason, "section_generation_incomplete"); }); test("writer identity and evidenceRefs assertions keep set-equality semantics", () => { const generationSource = readFileSync( new URL("../src/lib/personal-report-generation.ts", import.meta.url), "utf8", ); const agentSource = readFileSync( new URL("../src/mastra/personal-report.ts", import.meta.url), "utf8", ); assert.match(generationSource, /if \(candidate\.id !== entry\.id \|\| candidate\.theme !== entry\.theme\) throw new Error\("report_writer_section_identity_mismatch"\)/); assert.match(generationSource, /equalStringSets\(candidate\.evidenceRefs, entry\.evidenceRefs\)/); assert.match(generationSource, /throw new Error\("report_writer_evidence_refs_mismatch"\)/); assert.match(agentSource, /evidenceRefs 必须逐字复制 plan\.evidenceRefs 数组,不得增删/); assert.doesNotMatch(generationSource, /targetCharacters\.max\s*\/\s*2/); });