src/lib/import/snap-negative.test.ts
Linked repository file from this build.
import { describe, expect, test } from "bun:test";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { createSeed } from "@/programs/snap-negative/seed";
import {
BIS_COLUMN_COUNT,
BIS_HEADERS,
formatSnapNegativeReviewNumber,
isValidReviewNumberFormat,
parseSnapNegativeBatch,
SNAP_NEGATIVE_FIXTURE_FILES,
SYNTHETIC_ATTESTATION,
} from "./snap-negative";
import { demoActor } from "@/lib/auth/roster";
const fixtureDir = join(import.meta.dir, "../../../public/fixtures/snap-negative");
function publicFixture(name: string): string {
return readFileSync(join(fixtureDir, name), "utf8");
}
function publicBatch() {
return {
closedSuspended: publicFixture(SNAP_NEGATIVE_FIXTURE_FILES.closedSuspended),
rejected: publicFixture(SNAP_NEGATIVE_FIXTURE_FILES.rejected),
};
}
function attested(rows: string[]): string {
return [`# ${SYNTHETIC_ATTESTATION}`, ...rows].join("\n") + "\n";
}
describe("SNAP Negative synthetic batch ingest", () => {
test("review numbers follow prefix length rules", () => {
expect(isValidReviewNumberFormat("60001")).toBe(true);
expect(isValidReviewNumberFormat("61999")).toBe(true);
expect(isValidReviewNumberFormat("650001")).toBe(true);
expect(isValidReviewNumberFormat("669999")).toBe(true);
expect(isValidReviewNumberFormat("600001")).toBe(false);
expect(isValidReviewNumberFormat("620001")).toBe(false);
expect(formatSnapNegativeReviewNumber("60", 1)).toBe("60001");
expect(formatSnapNegativeReviewNumber("61", 999)).toBe("61999");
expect(formatSnapNegativeReviewNumber("65", 1)).toBe("650001");
});
test("public fixtures carry the synthetic attestation and 42 A:AP columns", () => {
for (const name of Object.values(SNAP_NEGATIVE_FIXTURE_FILES)) {
const text = publicFixture(name);
expect(text.includes(SYNTHETIC_ATTESTATION)).toBe(true);
const data = text
.split(/\r?\n/)
.filter((line) => line.trim() && !line.trim().startsWith("#"));
expect(data.length).toBeGreaterThan(0);
for (const line of data) expect(line.split("|")).toHaveLength(BIS_COLUMN_COUNT);
}
});
test("matched public fixtures become new_active cases with identity and action facts only", () => {
const { cases, findings } = parseSnapNegativeBatch(publicBatch());
expect(findings).toEqual([]);
expect(cases.map((review) => review.reviewNumber)).toEqual([
"60002",
"61001",
"650003",
"660004",
]);
const denial = cases.find((review) => review.reviewNumber === "61001");
expect(denial).toMatchObject({
caseNumber: "SYN200445566",
program: "SNAP",
subtype: "negative",
sampleMonth: "2026-04",
clientName: "Taylor Brooks",
address: "100 Example Lane, Harrisburg, PA 17101",
county: "22",
district: "01",
localAgency: "",
examiner: "101",
supervisor: "",
state: "new_active",
outcome: "",
});
expect(denial?.facts).toMatchObject({
negativeAction: "Denial",
actionDate: "2026-04-01",
noticeDate: "2026-03-20",
members: [],
inputsRevision: 0,
});
expect(denial?.facts.final).toBeUndefined();
expect(denial?.validation).toBeUndefined();
expect(denial?.facts.answers).toMatchObject({
disposition: "",
contactResponse: "",
actionInitiatedBy: "",
timelinessResponse: "",
finding: "",
narrative:
"Synthetic intake: Denial on 2026-04-01. Examiner must complete household, income, and review facts.",
variances: [],
});
for (const scenario of Object.values(denial?.facts.scenarios ?? {})) {
expect(scenario.populated).toBe(false);
expect(scenario.householdSize).toBe(0);
expect(scenario.income).toEqual([]);
}
expect(denial?.lineage).toEqual([
{
sourceSystem: "sample_delim",
role: "rejected",
fileName: "rejected.txt",
rowNumber: 4,
rawText: expect.stringMatching(/^61001\|/),
},
]);
const suspension = cases.find((review) => review.reviewNumber === "650003");
expect(suspension?.facts.negativeAction).toBe("Suspension");
expect(suspension?.facts.actionDate).toBe("2026-04-15");
expect(suspension?.facts.noticeDate).toBe("");
const rejected = cases.find((review) => review.reviewNumber === "660004");
expect(rejected?.facts.negativeAction).toBe("Denial");
expect(rejected?.lineage[0]).toMatchObject({
role: "rejected",
fileName: "rejected.txt",
});
});
test("createSeed household and income are not copied onto imported cases", () => {
const seed = createSeed();
expect(seed.facts.members.length).toBeGreaterThan(0);
expect(seed.facts.scenarios.compI.income.length).toBeGreaterThan(0);
const { cases } = parseSnapNegativeBatch(publicBatch());
for (const review of cases) {
expect(review.facts.members).toEqual([]);
expect(review.clientName).not.toBe(seed.clientName);
expect(review.facts.paCap).toBeUndefined();
expect(review.facts.answers.finding).toBe("");
expect(review.outcome).toBe("");
for (const scenario of Object.values(review.facts.scenarios)) {
expect(scenario.income).toEqual([]);
expect(scenario.rentCents).toBe(0);
}
}
});
test("invalid input and duplicate rows block intake", () => {
const batch = publicBatch();
for (const bad of [
"",
attested([]),
attested(["60002|C"]),
batch.closedSuspended.replace("60002", "600001"),
batch.closedSuspended.replace("60002", "62002"),
]) {
expect(
parseSnapNegativeBatch({ ...batch, closedSuspended: bad }).findings.length,
).toBeGreaterThan(0);
}
const duplicate = batch.closedSuspended + batch.closedSuspended;
expect(
parseSnapNegativeBatch({ ...batch, closedSuspended: duplicate }).findings.some((f) =>
f.message.includes("duplicated"),
),
).toBe(true);
// A row supplies both identity and action maps; invalid identity cannot be supplied by another file.
const rows = batch.closedSuspended
.split("\n")
.map((line) => {
if (line.startsWith("60002|")) {
const fields = line.split("|");
fields[8] = "";
return fields.join("|");
}
return line;
})
.join("\n");
expect(
parseSnapNegativeBatch({ ...batch, closedSuspended: rows }).findings.some((f) =>
f.field.endsWith("caseNumber"),
),
).toBe(true);
});
});
test("attestation must occupy its own comment line and typed lineage survives commands", async () => {
const { execute } = await import("@/features/reviews/commands");
const files = publicBatch();
const bad = {
...files,
closedSuspended: files.closedSuspended.replace(
`# ${SYNTHETIC_ATTESTATION}`,
`not a comment ${SYNTHETIC_ATTESTATION}`,
),
};
expect(
parseSnapNegativeBatch(bad).findings.some((f) => f.field === "closedSuspended.attestation"),
).toBe(true);
const imported = parseSnapNegativeBatch(files).cases[0];
imported.intake = { batchId: "synthetic-batch", sources: files };
const changed = execute(
{ ...imported, id: crypto.randomUUID() },
{ type: "identity", value: { clientName: "Updated synthetic client" } },
{ ...demoActor("examiner"), staffId: imported.examiner },
{ id: "edit", at: new Date().toISOString() },
);
expect(changed.lineage).toEqual(imported.lineage);
expect(changed.intake?.sources).toEqual(files);
});
test("VBA pipe text qualifiers preserve separators and reject individual records", () => {
const batch = publicBatch();
const quoted = batch.closedSuspended.replace("Walker|Casey", '"Walker|Jr"|Casey');
expect(
parseSnapNegativeBatch({ ...batch, closedSuspended: quoted }).cases.find(
(r) => r.reviewNumber === "60002",
)?.clientName,
).toBe("Casey Walker|Jr");
expect(
parseSnapNegativeBatch({
...batch,
closedSuspended: batch.closedSuspended.replace("60002||", "60002|I|"),
}).findings.some((f) => f.field.endsWith("recordType")),
).toBe(true);
});
test("intake allows repeated review numbers across FFYs and blocks repeats inside an FFY", () => {
const batch = publicBatch();
const currentRow = batch.closedSuspended.split("\n").find((row) => row.startsWith("60002|"))!;
const previous = currentRow.split("|");
previous[5] = "2025";
const parsed = parseSnapNegativeBatch({
...batch,
closedSuspended: attested([currentRow, previous.join("|")]),
});
expect(parsed.findings).toEqual([]);
const repeats = parsed.cases.filter((review) => review.reviewNumber === "60002");
expect(repeats.map((review) => review.sampleMonth)).toEqual(["2025-04", "2026-04"]);
expect(repeats.every((review) => !("id" in review))).toBe(true);
previous[4] = "10";
const duplicate = parseSnapNegativeBatch({
...batch,
closedSuspended: attested([currentRow, previous.join("|")]),
});
expect(
duplicate.findings.some((finding) => finding.message.includes("60002, FFY 2026 is duplicated")),
).toBe(true);
});
test("fully populated 42-field rows preserve every value and map identity without shifts", () => {
const batch = publicBatch();
const fields = Array.from({ length: 42 }, (_, i) => `source-${i + 1}`);
Object.assign(fields, {
0: "60002",
1: "C",
2: "C",
4: "04",
5: "2026",
6: "02",
7: "01",
8: "000123456",
10: "20260415",
18: "20260401",
22: '12 "Example" | Lane',
23: "Apt 02",
24: "Test City",
25: "PA",
26: "01234-5678",
27: "000-555-0100",
28: "Example",
29: "Alex",
30: "Q",
40: "001",
41: "HBG",
});
const raw = fields.map((value) => '"' + value.replaceAll('"', '""') + '"').join("|");
const parsed = parseSnapNegativeBatch({
...batch,
closedSuspended: attested([BIS_HEADERS.join("|"), raw]),
});
expect(parsed.findings).toEqual([]);
expect(parsed.cases.find((review) => review.reviewNumber === "60002")).toMatchObject({
caseNumber: "000123456",
clientName: "Alex Q Example",
examiner: "001",
address: '12 "Example" | Lane, Apt 02, Test City, PA 01234',
facts: {
actionDate: "2026-04-15",
noticeDate: "2026-04-01",
answers: { telephone: "000-555-0100" },
},
lineage: [{ rawText: raw, rowNumber: 3 }],
});
});
test("empty final fields are retained; missing or extra columns and reordered headers fail", () => {
const batch = publicBatch();
const row = batch.closedSuspended.split("\n").find((line) => line.startsWith("60002|"))!;
const fields = row.split("|");
fields[41] = "";
expect(
parseSnapNegativeBatch({ ...batch, closedSuspended: attested([fields.join("|")]) }).findings,
).toEqual([]);
for (const invalid of [fields.slice(0, 41).join("|"), fields.join("|") + "|extra"]) {
expect(
parseSnapNegativeBatch({ ...batch, closedSuspended: attested([invalid]) }).findings.some(
(f) => f.field.endsWith("columns"),
),
).toBe(true);
}
const headers = [...BIS_HEADERS];
[headers[22], headers[23]] = [headers[23], headers[22]];
expect(
parseSnapNegativeBatch({
...batch,
closedSuspended: attested([headers.join("|"), row]),
}).findings.some((f) => f.field.endsWith("headers")),
).toBe(true);
});