import { describe, expect, it } from "vitest"; import { promises as fs } from "node:fs"; import { join } from "node:path"; import { reconstructionGenerationJsonSchema, reconstructionGenerationSchema, reconstructionV2Schema, } from "@/lib/reconstruction/schema.js"; import { validateOpenQuestionCandidate, validateReconstructionOpenQuestions, } from "@/lib/reconstruction/open-question-presentation.js"; function makeAnalysisResponse(questionValue, includeQuestion = true) { const unknown = { id: "unknown-workforce", description: "Whether necessary skilled production workers can be recruited within the required project timeframe.", confidence: "high", }; if (includeQuestion) unknown.question = questionValue; return { inputClassification: { primaryType: "other", secondaryTypes: [], reasoningModes: [], classificationReason: "A delivery uncertainty is central to the situation.", confidence: "high", }, reconstruction: { summary: "Delivery depends on recruiting skilled production workers.", actors: [], systemsOrObjects: [], expectedStates: [], observedStates: [], differences: [], knownTransitions: [], unexplainedTransitions: [], contradictions: [], importantUnknowns: [unknown], plausibleInterpretations: [], relationships: [], }, evidence: [], nextQuestion: { id: "next-1", question: "What recruitment evidence is available?", targets: ["unknown-workforce"], reason: "Recruitment feasibility is unresolved.", expectedInformationValue: "high", reasoningMode: "other", }, }; } describe("new reconstruction generation contract", () => { it("requires a non-empty question for every generated important unknown", () => { expect( reconstructionGenerationSchema.safeParse( makeAnalysisResponse(undefined, false), ).success, ).toBe(false); expect( reconstructionGenerationSchema.safeParse(makeAnalysisResponse(" ")).success, ).toBe(false); expect( reconstructionGenerationSchema.safeParse( makeAnalysisResponse( "Can the necessary skilled production workers be recruited within the required project timeframe?", ), ).success, ).toBe(true); const unknownSchema = reconstructionGenerationJsonSchema.properties.reconstruction.properties .importantUnknowns.items; expect(unknownSchema.required).toContain("description"); expect(unknownSchema.required).toContain("question"); expect(unknownSchema.properties.question.minLength).toBe(1); }); it("keeps the durable compatibility schema tolerant of a missing question", () => { expect( reconstructionV2Schema.safeParse(makeAnalysisResponse(undefined, false)).success, ).toBe(true); }); }); describe("Open Question presentation validation", () => { it("accepts a trimmed direct interrogative", () => { expect( validateOpenQuestionCandidate( " Can the necessary workers be recruited in time? ", ), ).toEqual({ valid: true, question: "Can the necessary workers be recruited in time?", }); }); it("uses canonical UI fallback without a technical explanation", async () => { const source = await fs.readFile( join(process.cwd(), "components/reasoning-workspace.jsx"), "utf8", ); expect(source).toContain("const openQuestion = node.openQuestion || node.label;"); expect(source).not.toContain( "Canonical uncertainty — direct question wording unavailable.", ); expect(source).toContain("startFocused(n.id);"); }); it.each([ ["question_missing", undefined, false], ["question_empty", " ", true], ["question_indirect_proposition", "Whether workers can be recruited in time", true], ["question_indirect_proposition", "Whether workers can be recruited in time?", true], ["question_not_interrogative", "Workers can be recruited in time.", true], ])("classifies %s nonfatally", (category, candidate, includeQuestion) => { const input = makeAnalysisResponse(candidate, includeQuestion).reconstruction; const canonicalBefore = input.importantUnknowns[0].description; const idBefore = input.importantUnknowns[0].id; const result = validateReconstructionOpenQuestions(input); expect(result.reconstruction.importantUnknowns[0]).not.toHaveProperty("question"); expect(result.reconstruction.importantUnknowns[0].description).toBe( canonicalBefore, ); expect(result.reconstruction.importantUnknowns[0].id).toBe(idBefore); expect(result.issues).toEqual([ expect.objectContaining({ category, unknownId: idBefore, severity: "nonfatal", path: ["reconstruction", "importantUnknowns", 0, "question"], }), ]); }); });