src/programs/snap-negative/validation.test.ts
Linked repository file from this build.
import { describe, expect, test } from "bun:test";
import { createSeed, completedReviewAnswers } from "./seed";
import { LEGACY_CHECK_VERSION, CURRENT_CHECK_VERSION } from "./rule-versions";
import {
activeStoredFindings,
requiredInputComplete,
runFullEditCheck,
validateRequiredInput,
validateReview,
} from "./validation";
import { normalizeReviewAnswers, reviewFieldDefinitions } from "./field-catalog";
import type { SnapReview } from "./types";
function withAnswers(review: SnapReview, patch: Parameters<typeof completedReviewAnswers>[0]) {
return {
...review,
facts: {
...review.facts,
answers: completedReviewAnswers(patch),
},
};
}
describe("field catalog", () => {
test("normalizeReviewAnswers fills legacy snapshots without crashing", () => {
const legacy = { disposition: "1", contactResponse: "03", actionInitiatedBy: "04" };
const normalized = normalizeReviewAnswers(legacy);
expect(normalized.disposition).toBe("1");
expect(normalized.caseClassification).toBe("1");
expect(normalized.nstrReason).toBe("");
expect(normalized.daysNoticeLate).toBe("");
});
test("exports field definitions for required-error references", () => {
expect(reviewFieldDefinitions.some((field) => field.key === "disposition")).toBe(true);
expect(reviewFieldDefinitions.some((field) => field.key === "narrative")).toBe(true);
});
});
describe("required input blockers", () => {
test("disposition 2 requires nstrReason only", () => {
const review = withAnswers(createSeed(), { disposition: "2", nstrReason: "" });
const blockers = validateRequiredInput(review);
expect(blockers.map((entry) => entry.field)).toEqual(["nstrReason"]);
expect(blockers[0]?.message).toContain("NSTR Reason");
});
test("disposition 1 requires completed review coded fields and items 19/20", () => {
const review = withAnswers(createSeed(), {
disposition: "1",
finding: "",
contactResponse: "",
actionInitiatedBy: "",
});
const blockers = validateRequiredInput(review);
expect(blockers.some((entry) => entry.field === "finding")).toBe(true);
expect(blockers.some((entry) => entry.field === "contactResponse")).toBe(true);
});
test("days fields apply when timeliness codes require them", () => {
const review = withAnswers(createSeed(), {
actionTimeliness: "3",
daysActionEarlyLate: "",
noticeTimeliness: "2",
daysNoticeLate: "",
});
const fields = validateRequiredInput(review).map((entry) => entry.field);
expect(fields).toContain("daysActionEarlyLate");
expect(fields).toContain("daysNoticeLate");
});
test("completed review passes required input", () => {
const review = withAnswers(createSeed(), {});
expect(requiredInputComplete(review)).toBe(true);
expect(validateRequiredInput(review)).toHaveLength(0);
});
});
describe("full edit check findings", () => {
test("12b=2 implies finding must be coded invalid", () => {
const review = withAnswers(createSeed(), {
recordedReasonAccuracy: "2",
finding: "1",
});
const findings = runFullEditCheck(review);
expect(findings.some((entry) => entry.code === "FINDING_MUST_BE_INVALID")).toBe(true);
expect(findings[0]?.field).toBe("finding");
});
test("14b=2 requires narrative explanation", () => {
const review = withAnswers(createSeed(), {
recordedReasonAccuracy: "1",
finding: "1",
noticeClearUnderstandable: "2",
narrative: "",
});
const findings = runFullEditCheck(review);
expect(findings.some((entry) => entry.code === "NARRATIVE_REQUIRED")).toBe(true);
expect(findings.find((entry) => entry.code === "NARRATIVE_REQUIRED")?.field).toBe("narrative");
});
test("cross-field findings do not duplicate required-field blockers", () => {
const review = withAnswers(createSeed(), {
recordedReasonAccuracy: "2",
finding: "2",
noticeClearUnderstandable: "1",
});
expect(runFullEditCheck(review)).toHaveLength(0);
});
});
describe("check version dispatch", () => {
test("legacy basic check keeps disposition and items 19/20 only", () => {
const review = {
...createSeed(),
checkVersion: LEGACY_CHECK_VERSION,
facts: {
...createSeed().facts,
answers: normalizeReviewAnswers({
disposition: "1",
contactResponse: "",
actionInitiatedBy: "",
}),
},
};
const findings = validateReview(review);
expect(findings.map((entry) => entry.field).sort()).toEqual([
"actionInitiatedBy",
"contactResponse",
]);
});
test("current full check combines required and cross-field findings in validate API", () => {
const review = {
...createSeed(),
checkVersion: CURRENT_CHECK_VERSION,
};
const findings = validateReview(review);
expect(findings.length).toBeGreaterThan(0);
expect(findings.every((entry) => entry.code && entry.id)).toBe(true);
});
});
describe("activeStoredFindings", () => {
test("excludes refused findings for ERC-style queries", () => {
const findings = runFullEditCheck(
withAnswers(createSeed(), { recordedReasonAccuracy: "2", finding: "1" }),
);
const refused = {
...findings[0]!,
refused: {
reason: "Verified against collateral",
actor: { name: "Sam", role: "supervisor" as const },
at: "2026-09-08T12:00:00Z",
},
};
expect(activeStoredFindings([refused])).toHaveLength(0);
expect(activeStoredFindings(findings)).toHaveLength(1);
});
});