jest.mock("fs"); import * as fs from "fs"; import * as path from "path"; import { classifyAttemptType, loadRubric, buildFeedbackContext, buildAttemptInjection, getRubricSummary, } from "../src/calibration-engine"; const mockFs = fs as jest.Mocked; const VALID_RUBRIC = { version: 1, summary: { name: "attempt", domain: "generic", description: "Evaluates submitted work across two test dimensions.", attemptTypes: ["code", "essay"], }, domain: "generic", attemptTypes: ["code", "essay"], criteria: [ { id: "correctness", label: "Correctness", weight: 60, description: "Achieves the goal" }, { id: "quality", label: "Quality", weight: 40, description: "Clean and organized" }, ], integrityProfile: { safeSkills: [], gradedSkills: ["attempt"] }, }; beforeEach(() => { jest.clearAllMocks(); mockFs.existsSync.mockReturnValue(false); }); describe("classifyAttemptType()", () => { it("returns the hint when provided", () => { expect(classifyAttemptType(undefined, "essay")).toBe("essay"); expect(classifyAttemptType("/some/file.py", "custom")).toBe("custom"); }); it("infers type from file extension", () => { expect(classifyAttemptType("/src/main.py")).toBe("code"); expect(classifyAttemptType("/docs/report.md")).toBe("essay"); expect(classifyAttemptType("/src/app.ts")).toBe("code"); expect(classifyAttemptType("/src/App.tsx")).toBe("code"); expect(classifyAttemptType("/src/Main.java")).toBe("code"); }); it("returns 'generic' for unknown extensions", () => { expect(classifyAttemptType("/file.unknown")).toBe("generic"); expect(classifyAttemptType("/file.docx")).toBe("generic"); }); it("returns 'generic' when no path or hint given", () => { expect(classifyAttemptType()).toBe("generic"); expect(classifyAttemptType(undefined, undefined)).toBe("generic"); }); it("normalises hint to lowercase", () => { expect(classifyAttemptType(undefined, "CODE")).toBe("code"); expect(classifyAttemptType(undefined, " Essay ")).toBe("essay"); }); }); describe("loadRubric()", () => { it("returns null when no rubric file is found", () => { mockFs.existsSync.mockReturnValue(false); expect(loadRubric("/some/cwd")).toBeNull(); }); it("loads and returns a valid rubric from the .pisces directory", () => { const rubricPath = path.join("/some/cwd", ".pisces", "rubric.json"); mockFs.existsSync.mockImplementation((p) => p === rubricPath); mockFs.readFileSync.mockReturnValue(JSON.stringify(VALID_RUBRIC)); const result = loadRubric("/some/cwd"); expect(result).not.toBeNull(); expect(result?.spec.domain).toBe("generic"); expect(result?.source).toBe(rubricPath); }); it("skips an invalid rubric file and continues to next path", () => { mockFs.existsSync.mockReturnValue(true); mockFs.readFileSync.mockReturnValue(JSON.stringify({ version: 99, bad: true })); expect(loadRubric("/some/cwd")).toBeNull(); }); it("handles JSON parse errors gracefully", () => { mockFs.existsSync.mockReturnValue(true); mockFs.readFileSync.mockReturnValue("not valid json{{{"); expect(loadRubric("/some/cwd")).toBeNull(); }); }); describe("buildFeedbackContext()", () => { it("includes all criteria in the rubric markdown", () => { const { rubricMarkdown } = buildFeedbackContext(VALID_RUBRIC as never, ""); expect(rubricMarkdown).toContain("Correctness"); expect(rubricMarkdown).toContain("Quality"); expect(rubricMarkdown).toContain("60%"); expect(rubricMarkdown).toContain("40%"); }); it("includes the domain in the heading", () => { const { rubricMarkdown } = buildFeedbackContext(VALID_RUBRIC as never, ""); expect(rubricMarkdown).toContain("generic"); }); it("passes gap summary through unchanged", () => { const gaps = "Recent gaps:\n- missed null check"; const { gapSummary } = buildFeedbackContext(VALID_RUBRIC as never, gaps); expect(gapSummary).toBe(gaps); }); }); describe("getRubricSummary()", () => { const rubricResult = { spec: VALID_RUBRIC as never, source: "/path/rubric.json", }; it("returns the public summary fields", () => { const summary = getRubricSummary(rubricResult); expect(summary.name).toBe("attempt"); expect(summary.domain).toBe("generic"); expect(summary.description).toBeTruthy(); expect(summary.attemptTypes).toContain("code"); }); it("does not expose criteria on the returned summary", () => { const summary = getRubricSummary(rubricResult); expect((summary as unknown as Record)["criteria"]).toBeUndefined(); }); it("does not expose integrityProfile on the returned summary", () => { const summary = getRubricSummary(rubricResult); expect((summary as unknown as Record)["integrityProfile"]).toBeUndefined(); }); }); describe("buildAttemptInjection()", () => { const rubricResult = { spec: VALID_RUBRIC as never, source: "/path/rubric.json", }; it("includes submitted content", () => { const injection = buildAttemptInjection(rubricResult, "", "def foo(): pass"); expect(injection).toContain("def foo(): pass"); }); it("includes rubric when available", () => { const injection = buildAttemptInjection(rubricResult, "", "my code"); expect(injection).toContain("Correctness"); expect(injection).toContain("Active Rubric"); }); it("includes gap summary when non-empty", () => { const injection = buildAttemptInjection(rubricResult, "Recent gaps:\n- null check", "code"); expect(injection).toContain("null check"); }); it("falls back to a generic header when no rubric is loaded", () => { const injection = buildAttemptInjection(null, "", "my essay"); expect(injection).toContain("No domain rubric loaded"); expect(injection).toContain("my essay"); }); it("omits the gap section when gap summary is empty", () => { const injection = buildAttemptInjection(rubricResult, "", "code here"); expect(injection).not.toContain("Recent gaps"); }); });