142 lines
4.8 KiB
JavaScript
142 lines
4.8 KiB
JavaScript
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"],
|
|
}),
|
|
]);
|
|
});
|
|
}); |