fix(labs): keep the printed range on a marker minted from a text result

A report that prints a text result such as "SEE NOTE" with a numeric
range beside it minted the marker without bounds: the document commit,
the scan commit, the free-text lab write and the CSV import each passed
null bounds for a qualitative reading, and the CSV row reader and the
scan review payload dropped the range before that. The marker's first
number then had nothing to be judged against.

Every mint path now passes the printed window. A range that is text
itself yields no bounds, an existing marker keeps its own, a transposed
window is not minted, and the reading itself still records no window.
The scan review shows the range on a text row that mints a new marker.

Refs #1313
This commit is contained in:
Marc-André Bombeck committed 2026-11-05 10:59:52 +01:00
1 parent e9886ee554
commit ef61c7d5d1
13 files changed
+520 -39

No files matched your search

+8
View File
@@ -62,6 +62,14 @@
on the phone to an account pending deletion now carries the date the
account is erased, as the web sign-in already did, so the app can say
until when an admin can still undo it. (#1341)
- **A marker started from a text result keeps its range.** When a report
printed a text result such as "SEE NOTE" with a numeric range beside it,
the new marker was saved without that range, so its first number later
had nothing to be judged against. The range now goes onto the new marker,
from a document, a lab scan, a CSV import or a typed reading, and the
scan review shows it for such a row. A range that is text itself, like
"NEGATIVE", is not turned into bounds, and a marker that already exists
keeps its own. (#1313)
### Changed
@@ -1,7 +1,7 @@
{
"$comment": "Production functions over 200 lines, keyed \"file :: name\", and their length. Maintained by src/__tests__/source-size-ratchet.test.ts.",
"count": 345,
"total": 111902,
"total": 111889,
"entries": {
"app/achievements/page.tsx :: AchievementsPage": 206,
"app/api/admin/backups/upload/route.ts :: apiHandler(arg)": 275,
@@ -24,7 +24,7 @@
"app/api/integrations/status/route.ts :: apiHandler(arg)": 476,
"app/api/labs/[id]/route.ts :: apiHandler(arg) #2": 239,
"app/api/labs/ocr/commit/route.ts :: commitOcrRows": 280,
"app/api/labs/route.ts :: postLabResult": 242,
"app/api/labs/route.ts :: postLabResult": 241,
"app/api/mcp/oauth/token/route.ts :: POST": 250,
"app/api/mcp/oauth/token/route.ts :: POST > withBackgroundEvent(arg)": 248,
"app/api/measurement-reminders/[id]/route.ts :: apiHandler(arg) #2": 239,
@@ -139,7 +139,7 @@
"components/labs/lab-history-list.tsx :: LabHistoryList": 534,
"components/labs/lab-list.tsx :: LabList": 308,
"components/labs/ocr-review-dialog.tsx :: OcrReviewDialog": 359,
"components/labs/ocr-row-editor.tsx :: OcrRowEditor": 386,
"components/labs/ocr-row-editor.tsx :: OcrRowEditor": 378,
"components/layout/auth-shell.tsx :: AuthShell": 518,
"components/layout/bottom-nav.tsx :: BottomNav": 262,
"components/layout/capture-picker.tsx :: CapturePicker": 235,
@@ -286,8 +286,8 @@
"lib/google-health/data-points.ts :: forEachDataPointPage > walk": 223,
"lib/google-health/sync-core.ts :: upsertGoogleHealthMeasurements": 452,
"lib/illness/correlation.ts :: computeIllnessCorrelation": 233,
"lib/import/labs-csv.ts :: importLabsCsv": 309,
"lib/import/labs-csv.ts :: importLabsCsv > flush": 231,
"lib/import/labs-csv.ts :: importLabsCsv": 307,
"lib/import/labs-csv.ts :: importLabsCsv > flush": 229,
"lib/insights/comprehensive-aggregator.ts :: buildFromRollups": 217,
"lib/insights/comprehensive-generate.ts :: generateComprehensiveInsight": 876,
"lib/insights/features.ts :: extractFeatures": 796,
+3 -3
View File
@@ -249,10 +249,10 @@ async function commitOcrRows(request: NextRequest) {
user.id,
{
analyte: row.analyte,
// A qualitative reading has no numeric unit / range.
// A text result has no unit; its printed window still seeds.
unit: isQualitative ? (row.unit ?? "") : (row.unit as string),
referenceLow: isQualitative ? null : sourceLow,
referenceHigh: isQualitative ? null : sourceHigh,
referenceLow: sourceLow,
referenceHigh: sourceHigh,
panel: row.panel ?? null,
},
{ index: markerIndex },
+4 -5
View File
@@ -234,12 +234,11 @@ async function postLabResult(request: NextRequest) {
} else {
biomarker = await resolveOrMintBiomarker(user.id, {
analyte: analyte as string,
// A qualitative reading has no numeric unit / range — mint the catalog
// marker with an empty unit label and no bounds. A numeric reading
// supplies its unit (Zod requires it on the free-text numeric path).
// A qualitative reading mints with an empty unit but the printed range;
// a numeric one supplies its unit (Zod requires it on this path).
unit: isQualitative ? (unit ?? "") : (unit as string),
referenceLow: isQualitative ? null : (referenceLow ?? null),
referenceHigh: isQualitative ? null : (referenceHigh ?? null),
referenceLow: referenceLow ?? null,
referenceHigh: referenceHigh ?? null,
panel: panel ?? null,
});
}
@@ -115,6 +115,27 @@ describe("toCommitRow agrees with validateReviewRow", () => {
it("anchors the date at noon UTC", () => {
expect(toCommitRow(row())?.takenAt).toBe("2026-06-10T12:00:00.000Z");
});
it("sends the range printed beside a text result", () => {
expect(
toCommitRow(
row({
value: null,
valueText: "SEE NOTE",
unit: null,
referenceLow: 10,
referenceHigh: 20,
referenceText: "10 - 20",
biomarkerMatch: "new",
}),
),
).toMatchObject({
valueText: "SEE NOTE",
referenceLow: 10,
referenceHigh: 20,
referenceText: "10 - 20",
});
});
});
describe("collectRowErrors", () => {
@@ -125,6 +125,44 @@ describe("<OcrRowEditor> layout", () => {
});
});
describe("<OcrRowEditor> range beside a text result", () => {
const text: OcrReviewRow = {
...baseRow,
value: null,
valueText: "SEE NOTE",
unit: null,
referenceLow: 10,
referenceHigh: 20,
referenceText: "10 - 20",
};
it("offers the range for review when the row mints a new marker", () => {
const html = render(undefined, "en", { ...text, biomarkerMatch: "new" });
for (const id of ["-lo", "-hi", "-refText"]) {
expect(html, `field ${id}`).toContain(`${id}"`);
}
expect(html).toContain("Reference range taken from the scan");
});
it("does not ask to verify a range a new text marker never had", () => {
const html = render(undefined, "en", {
...text,
biomarkerMatch: "new",
referenceLow: null,
referenceHigh: null,
referenceText: null,
});
expect(html).not.toContain("Verify the reference range");
});
it("leaves it out when the row goes to an existing marker", () => {
const html = render(undefined, "en", text);
for (const id of ["-lo", "-hi", "-refText"]) {
expect(html, `field ${id}`).not.toContain(`${id}"`);
}
});
});
describe("<OcrRowEditor> unit differs from the marker's", () => {
const mismatched: OcrReviewRow = {
...baseRow,
+13 -1
View File
@@ -157,6 +157,9 @@ export function toCommitRow(row: OcrReviewRow): OcrCommitRowInput | null {
analyte,
valueText,
takenAt: takenAt.toISOString(),
// A text result has no window of its own, but a new marker minted from
// it takes the one printed beside it.
...printedRange(row),
...chosen(row),
};
}
@@ -170,10 +173,19 @@ export function toCommitRow(row: OcrReviewRow): OcrCommitRowInput | null {
value: row.value,
unit,
takenAt: takenAt.toISOString(),
...printedRange(row),
...chosen(row),
};
}
/** The reviewed window: the two bounds and the string they were read from. */
function printedRange(
row: OcrReviewRow,
): Pick<OcrCommitRowInput, "referenceLow" | "referenceHigh" | "referenceText"> {
return {
...(row.referenceLow !== null ? { referenceLow: row.referenceLow } : {}),
...(row.referenceHigh !== null ? { referenceHigh: row.referenceHigh } : {}),
...(row.referenceText ? { referenceText: row.referenceText } : {}),
...chosen(row),
};
}
+32 -13
View File
@@ -75,6 +75,33 @@ function unitMismatchMessage(
: ["labs.ocr.errorUnitMismatch", { markerUnit: row.markerUnit ?? "" }];
}
/**
* v1.18.10 (#5) — a NEW biomarker mints its catalog reference range from THIS
* extracted row, so the range is load-bearing and worth a second look. Surface
* the range confidence for a new numeric marker, and call it out when the
* model was unsure (or set no bounds at all) so the user verifies the range it
* will be judged against. A text result ("SEE NOTE") records no window of its
* own, but a new marker minted from it takes the one printed beside it, so the
* range is shown there too, and only called out when there is one: a text
* result often has no numeric range, and that is not a gap.
*/
function rangeReview(row: OcrReviewRow): {
showRange: boolean;
showRangeBadge: boolean;
flagRange: boolean;
} {
const isQualitative = row.valueText !== null && row.valueText !== undefined;
const isNew = row.biomarkerMatch === "new";
const lowRangeConfidence = row.confidence.range < CONFIDENCE_THRESHOLD;
const hasBounds = row.referenceLow !== null || row.referenceHigh !== null;
const showRangeBadge = isNew && (!isQualitative || hasBounds);
return {
showRange: !isQualitative || isNew,
showRangeBadge,
flagRange: showRangeBadge && (lowRangeConfidence || !hasBounds),
};
}
export function OcrRowEditor({
row,
onChange,
@@ -138,15 +165,7 @@ export function OcrRowEditor({
? row.confidence.value < CONFIDENCE_THRESHOLD
: lowValueConfidence);
// v1.18.10 (#5) — a NEW numeric biomarker mints its catalog reference range
// from THIS extracted row. The range is therefore load-bearing and worth a
// second look. Surface the range confidence prominently for new numeric
// markers, and call it out explicitly when the model was unsure (or set no
// bounds at all) so the user verifies the range it will be judged against.
const isNewNumericMarker = !isQualitative && row.biomarkerMatch === "new";
const lowRangeConfidence = row.confidence.range < CONFIDENCE_THRESHOLD;
const hasBounds = row.referenceLow !== null || row.referenceHigh !== null;
const flagRange = isNewNumericMarker && (lowRangeConfidence || !hasBounds);
const { showRange, showRangeBadge, flagRange } = rangeReview(row);
return (
// `@container` makes the row follow the width of the dialog, not the
@@ -353,11 +372,11 @@ export function OcrRowEditor({
</div>
</div>
{/* v1.18.10 (#5) — for a NEW numeric biomarker the reference range below
{/* v1.18.10 (#5) — for a NEW biomarker the reference range below
becomes the marker's catalog range. Make that consequence visible, and
flag low range-confidence / a missing range so the user verifies it
before saving. A calm `secondary` badge, not an alarm. */}
{isNewNumericMarker ? (
{showRangeBadge ? (
<div className="flex flex-wrap items-center gap-1.5">
<Badge variant={flagRange ? "secondary" : "outline"}>
<Ruler aria-hidden />
@@ -368,7 +387,7 @@ export function OcrRowEditor({
</div>
) : null}
{!isQualitative ? (
{showRange ? (
<div className="grid grid-cols-2 gap-2">
<div className="space-y-1">
<Label htmlFor={`${fieldId}-lo`} className="text-xs">
@@ -424,7 +443,7 @@ export function OcrRowEditor({
reading of this string, and correcting the string is how a range the
parser could not read ("bis 5,0", "negativ") still reaches the row
instead of being dropped. */}
{!isQualitative ? (
{showRange ? (
<div className="space-y-1">
<Label htmlFor={`${fieldId}-refText`} className="text-xs">
{t("labs.ocr.refTextLabel")}
+4 -2
View File
@@ -197,8 +197,10 @@ async function commitObservation(
biomarker = await resolveOrMintBiomarker(userId, {
analyte: data.label,
unit: isQualitative ? (data.unit ?? "") : (data.unit as string),
referenceLow: isQualitative ? null : sourceLow,
referenceHigh: isQualitative ? null : sourceHigh,
// A text result mints with the window printed beside it as well: the
// marker's next number is judged against it.
referenceLow: sourceLow,
referenceHigh: sourceHigh,
panel: null,
});
}
@@ -106,6 +106,26 @@ describe("lab rows", () => {
});
});
it("keeps the numeric range beside a text result, and no text range", () => {
expect(
lab(["BUN/Creatinine ratio", "SEE NOTE", "", "2025-03-01", "10 - 20"]),
).toMatchObject({
ok: true,
row: {
valueText: "SEE NOTE",
sourceReferenceLow: 10,
sourceReferenceHigh: 20,
sourceReferenceText: null,
},
});
expect(
lab(["Urine nitrite", "neg", "", "2025-03-01", "NEGATIVE"]),
).toMatchObject({
ok: true,
row: { sourceReferenceLow: null, sourceReferenceHigh: null },
});
});
it.each([
[["", "5", "%", "2025-03-01", ""], "missing_value", "analyte"],
[["HbA1c", "", "%", "2025-03-01", ""], "missing_value", "value"],
+8 -8
View File
@@ -74,6 +74,8 @@ export interface LabCsvRow {
valueText: string | null;
unit: string | null;
takenAt: Date;
/** The file's window. On a text result it only seeds a new marker: the
* reading itself records no window. */
sourceReferenceLow: number | null;
sourceReferenceHigh: number | null;
sourceReferenceText: string | null;
@@ -200,8 +202,8 @@ export function normaliseLabRow(
valueText,
unit,
takenAt: date.instant,
sourceReferenceLow: valueText === null ? low : null,
sourceReferenceHigh: valueText === null ? high : null,
sourceReferenceLow: low,
sourceReferenceHigh: high,
sourceReferenceText: valueText === null ? rangeText : null,
panel,
note,
@@ -473,10 +475,8 @@ export async function importLabsCsv(args: {
{
analyte: row.analyte,
unit: row.unit ?? "",
referenceLow:
row.valueText === null ? row.sourceReferenceLow : null,
referenceHigh:
row.valueText === null ? row.sourceReferenceHigh : null,
referenceLow: row.sourceReferenceLow,
referenceHigh: row.sourceReferenceHigh,
panel: row.panel,
},
{ index: working },
@@ -484,8 +484,8 @@ export async function importLabsCsv(args: {
if (!marker) created.add(row.analyte);
let storedValue = row.value;
let storedLow = row.sourceReferenceLow;
let storedHigh = row.sourceReferenceHigh;
let storedLow = row.valueText === null ? row.sourceReferenceLow : null;
let storedHigh = row.valueText === null ? row.sourceReferenceHigh : null;
let convertedFrom: { sourceValue: number; sourceUnit: string } | null =
null;
if (row.value !== null) {
+10 -2
View File
@@ -86,6 +86,10 @@ export interface MintedBiomarker {
* reused as-is: its unit / range are authoritative and the incoming free-text
* unit / bounds are NOT allowed to silently rewrite it. When nothing resolves,
* a marker is minted from the reading's own spelling / unit / bounds. The
* bounds are the report's printed window whether the reading is a number or a
* text result ("SEE NOTE" beside "10 - 20"): the marker outlives the reading,
* and its next number is judged against them. A window that cannot be true
* (low above high) is not minted; nothing says which of the two is wrong. The
* `@@unique([userId, name])` index is the structural backstop; a concurrent
* double-write converges via the catch-and-refetch.
*
@@ -109,14 +113,18 @@ export async function resolveOrMintBiomarker(
const hit = resolveInIndex(index, name, input.unit || null);
if (hit) return toMinted(hit.marker);
const transposed =
input.referenceLow !== null &&
input.referenceHigh !== null &&
input.referenceLow > input.referenceHigh;
try {
const created = await prisma.biomarker.create({
data: {
userId,
name,
unit: input.unit,
lowerBound: input.referenceLow,
upperBound: input.referenceHigh,
lowerBound: transposed ? null : input.referenceLow,
upperBound: transposed ? null : input.referenceHigh,
panel: input.panel,
analyteKey: matchAnalyteKey({ name, unit: input.unit }),
},
@@ -0,0 +1,354 @@
/**
* A marker minted from a text result keeps the range the report printed.
*
* A report that prints "SEE NOTE" for a ratio, with "10 - 20" in the range
* column beside it, minted a marker with no bounds: every import path read the
* range only for a numeric reading. The next report's number then had nothing
* to be judged against. A numeric range now seeds the new marker the way a
* numeric first reading does; a range that is text itself ("NEGATIVE") is not
* a bound and stays out, and a marker that already exists keeps its own.
*
* Every path that mints markers from an import, against Postgres: a document's
* confirmed facts, the lab scan commit, a free-text reading through
* `POST /api/labs` (the on-device scan posts there), and the CSV import.
*/
import { NextRequest } from "next/server";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { cookieJar } from "./mock-next-headers";
import { getPrismaClient, truncateAllTables } from "./setup";
import { textToStream } from "@/lib/import/csv-stream";
import { importLabsCsv } from "@/lib/import/labs-csv";
import { labsCsvQuerySchema } from "@/lib/validations/record-import";
vi.mock("@/lib/jobs/boss-instance", () => ({
getGlobalBoss: vi.fn(() => null),
}));
vi.mock("next/headers", async () => {
const { cookieJar, headerJar } = await import("./mock-next-headers");
return {
headers: vi.fn(async () => ({
get: (name: string) => headerJar.get(name.toLowerCase()) ?? null,
})),
cookies: vi.fn(async () => ({
get: (name: string) => {
const value = cookieJar.get(name);
return value ? { name, value } : undefined;
},
set: (name: string, value: string) => cookieJar.set(name, value),
delete: (name: string) => cookieJar.delete(name),
})),
};
});
const prisma = getPrismaClient();
const USER = "text-marker-range-user";
const TAKEN_AT = "2026-09-01T12:00:00.000Z";
beforeEach(async () => {
await truncateAllTables(prisma);
await prisma.user.create({
data: {
id: USER,
username: USER,
timezone: "UTC",
modulePreferencesJson: { inboundDocuments: true, labs: true },
},
});
const s = await prisma.session.create({
data: { userId: USER, expiresAt: new Date(Date.now() + 3_600_000) },
});
cookieJar.clear();
cookieJar.set("healthlog_session", s.id);
});
function jsonReq(path: string, body: unknown): NextRequest {
return new NextRequest(`http://localhost${path}`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
});
}
async function bounds(name: string) {
const marker = await prisma.biomarker.findFirstOrThrow({
where: { userId: USER, name },
});
return { low: marker.lowerBound, high: marker.upperBound, id: marker.id };
}
async function seedMarker(name: string) {
return prisma.biomarker.create({
data: { userId: USER, name, unit: "", lowerBound: 1, upperBound: 2 },
});
}
// --- document facts ---------------------------------------------------------
async function confirmTextFact(
label: string,
referenceText: string,
referenceLow: number | null,
referenceHigh: number | null,
) {
const { encryptFactData, encryptFactProvenance } =
await import("@/lib/documents/store");
const doc = await prisma.inboundDocument.create({
data: {
userId: USER,
filename: "report.pdf",
mimeType: "application/pdf",
byteSize: 1024,
contentEncrypted: Buffer.from("stand-in for the stored bytes"),
status: "EXTRACTED",
reportDate: new Date(TAKEN_AT),
},
});
const fact = await prisma.extractedFact.create({
data: {
documentId: doc.id,
userId: USER,
factType: "OBSERVATION",
status: "PENDING",
confidence: 0.95,
needsReview: false,
dataEncrypted: encryptFactData({
label,
code: null,
codeSystem: null,
value: null,
valueText: "SEE NOTE",
unit: null,
referenceLow,
referenceHigh,
referenceText,
effectiveDate: null,
}),
provenanceEncrypted: encryptFactProvenance({
sourceText: `${label} SEE NOTE ${referenceText}`,
anchored: true,
sourceOffset: 0,
page: 0,
confidence: 0.95,
}),
},
});
const { POST } =
await import("@/app/api/documents/inbound/[id]/confirm/route");
const res = await POST(
jsonReq(`/api/documents/inbound/${doc.id}/confirm`, {
decisions: [{ factId: fact.id, action: "approve" }],
}),
{ params: Promise.resolve({ id: doc.id }) },
);
expect(res.status).toBe(200);
const body = await res.json();
expect(body.data.failed).toEqual([]);
}
describe("a document's text result", () => {
it("gives a new marker the numeric range printed beside it", async () => {
await confirmTextFact("BUN/Creatinine ratio", "10 - 20", null, null);
expect(await bounds("BUN/Creatinine ratio")).toMatchObject({
low: 10,
high: 20,
});
});
it("does not turn a text range into bounds", async () => {
await confirmTextFact("Urine nitrite", "NEGATIVE", null, null);
expect(await bounds("Urine nitrite")).toMatchObject({
low: null,
high: null,
});
});
it("leaves an existing marker's range alone", async () => {
await seedMarker("BUN/Creatinine ratio");
await confirmTextFact("BUN/Creatinine ratio", "10 - 20", 10, 20);
expect(await bounds("BUN/Creatinine ratio")).toMatchObject({
low: 1,
high: 2,
});
});
});
// --- lab scan commit --------------------------------------------------------
async function commitScanRows(rows: Record<string, unknown>[]) {
const { POST } = await import("@/app/api/labs/ocr/commit/route");
const res = await POST(
jsonReq("/api/labs/ocr/commit", {
rows: rows.map((row) => ({ takenAt: TAKEN_AT, ...row })),
}),
);
expect(res.status).toBe(200);
}
describe("a scanned text result", () => {
it("gives a new marker the confirmed bounds", async () => {
await commitScanRows([
{
analyte: "BUN/Creatinine ratio",
valueText: "SEE NOTE",
referenceLow: 10,
referenceHigh: 20,
referenceText: "10 - 20",
},
]);
expect(await bounds("BUN/Creatinine ratio")).toMatchObject({
low: 10,
high: 20,
});
});
it("derives them from the printed range when no bounds were sent", async () => {
await commitScanRows([
{
analyte: "BUN/Creatinine ratio",
valueText: "SEE NOTE",
referenceText: "10 - 20",
},
{ analyte: "Urine nitrite", valueText: "neg", referenceText: "NEGATIVE" },
]);
expect(await bounds("BUN/Creatinine ratio")).toMatchObject({
low: 10,
high: 20,
});
expect(await bounds("Urine nitrite")).toMatchObject({
low: null,
high: null,
});
});
it("drops a transposed window instead of minting it", async () => {
await commitScanRows([
{
analyte: "BUN/Creatinine ratio",
valueText: "SEE NOTE",
referenceLow: 20,
referenceHigh: 10,
},
]);
expect(await bounds("BUN/Creatinine ratio")).toMatchObject({
low: null,
high: null,
});
});
it("leaves an existing marker's range alone", async () => {
await seedMarker("BUN/Creatinine ratio");
await commitScanRows([
{
analyte: "BUN/Creatinine ratio",
valueText: "SEE NOTE",
referenceLow: 10,
referenceHigh: 20,
},
]);
expect(await bounds("BUN/Creatinine ratio")).toMatchObject({
low: 1,
high: 2,
});
});
});
// --- POST /api/labs ---------------------------------------------------------
async function postLab(body: Record<string, unknown>) {
const { POST } = await import("@/app/api/labs/route");
const res = await POST(jsonReq("/api/labs", { takenAt: TAKEN_AT, ...body }));
expect(res.status).toBe(201);
return (await res.json()).data;
}
describe("a free-text reading that is a text result", () => {
it("seeds the new marker, and the next number is judged against it", async () => {
await postLab({
analyte: "BUN/Creatinine ratio",
valueText: "SEE NOTE",
referenceLow: 10,
referenceHigh: 20,
});
const { id } = await bounds("BUN/Creatinine ratio");
expect(await bounds("BUN/Creatinine ratio")).toMatchObject({
low: 10,
high: 20,
});
await postLab({
biomarkerId: id,
value: 18,
unit: "1",
takenAt: "2026-10-01T12:00:00.000Z",
});
const reading = await prisma.labResult.findFirstOrThrow({
where: { biomarkerId: id, value: { not: null } },
});
expect(reading).toMatchObject({ referenceLow: 10, referenceHigh: 20 });
});
});
// --- CSV import -------------------------------------------------------------
describe("a CSV text result", () => {
const query = () =>
labsCsvQuerySchema.parse({
analyte: "0",
value: "1",
unit: "2",
date: "3",
referenceRange: "4",
});
it("gives a new marker a numeric range and leaves a text range out", async () => {
const result = await importLabsCsv({
userId: USER,
tz: "UTC",
stream: textToStream(
[
"Analyte;Value;Unit;Date;Range",
"BUN/Creatinine ratio;SEE NOTE;;2026-09-01;10 - 20",
"Urine nitrite;neg;;2026-09-01;NEGATIVE",
].join("\n"),
),
maxBytes: 1_000_000,
query: query(),
});
expect(result).toMatchObject({ inserted: 2 });
expect(await bounds("BUN/Creatinine ratio")).toMatchObject({
low: 10,
high: 20,
});
expect(await bounds("Urine nitrite")).toMatchObject({
low: null,
high: null,
});
// The text reading itself still carries no window of its own.
const reading = await prisma.labResult.findFirstOrThrow({
where: { userId: USER, analyte: "BUN/Creatinine ratio" },
});
expect(reading).toMatchObject({
valueText: "SEE NOTE",
sourceReferenceLow: null,
sourceReferenceHigh: null,
});
});
it("leaves an existing marker's range alone", async () => {
await seedMarker("BUN/Creatinine ratio");
await importLabsCsv({
userId: USER,
tz: "UTC",
stream: textToStream(
"Analyte;Value;Unit;Date;Range\nBUN/Creatinine ratio;SEE NOTE;;2026-09-01;10 - 20\n",
),
maxBytes: 1_000_000,
query: query(),
});
expect(await bounds("BUN/Creatinine ratio")).toMatchObject({
low: 1,
high: 2,
});
});
});