106 lines
3.5 KiB
TypeScript
106 lines
3.5 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { buildReconciliationCandidates } from "../src/reconciliation.js";
|
|
|
|
const PINNED = new Date("2026-08-07T00:00:00.000Z");
|
|
|
|
function baseItem(overrides: Partial<Parameters<typeof buildReconciliationCandidates>[0][number]> = {}) {
|
|
return {
|
|
id: "itm-1",
|
|
displayName: "Mjölk",
|
|
quantity: 1,
|
|
unit: "l",
|
|
locationName: "Kylen",
|
|
confidence: 1,
|
|
verifiedByUser: true,
|
|
lastVerifiedAt: PINNED,
|
|
updatedAt: PINNED,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("buildReconciliationCandidates", () => {
|
|
it("returns empty when no items match", () => {
|
|
const result = buildReconciliationCandidates(
|
|
[baseItem({ quantity: 0, depletedAt: PINNED })],
|
|
{ plannedRecipeIngredientIds: new Map(), daysLeftByItemId: new Map(), dailyConsumptionRate: new Map(), priceMinorByItemId: new Map() },
|
|
PINNED,
|
|
);
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it("prioritises planned recipe ingredients", () => {
|
|
const a = baseItem({ id: "a", displayName: "Grädde", confidence: 1, verifiedByUser: true });
|
|
const b = baseItem({ id: "b", displayName: "Ost", confidence: 1, verifiedByUser: true });
|
|
const result = buildReconciliationCandidates(
|
|
[b, a],
|
|
{
|
|
plannedRecipeIngredientIds: new Map([["a", ["rec-1"]]]),
|
|
daysLeftByItemId: new Map(),
|
|
dailyConsumptionRate: new Map(),
|
|
priceMinorByItemId: new Map(),
|
|
},
|
|
PINNED,
|
|
15,
|
|
);
|
|
expect(result.map((r) => r.itemId)).toEqual(["a"]);
|
|
expect(result[0]?.reasons).toContainEqual({ kind: "planned_recipe", recipeIds: ["rec-1"] });
|
|
});
|
|
|
|
it("flags expiring soon and sorts by urgency", () => {
|
|
const a = baseItem({ id: "a", displayName: "Yoghurt", confidence: 0.5 });
|
|
const b = baseItem({ id: "b", displayName: "Juice", confidence: 0.5 });
|
|
const result = buildReconciliationCandidates(
|
|
[b, a],
|
|
{
|
|
plannedRecipeIngredientIds: new Map(),
|
|
daysLeftByItemId: new Map([
|
|
["a", 1],
|
|
["b", 6],
|
|
]),
|
|
dailyConsumptionRate: new Map(),
|
|
priceMinorByItemId: new Map(),
|
|
},
|
|
PINNED,
|
|
);
|
|
expect(result.map((r) => r.itemId)).toEqual(["a", "b"]);
|
|
});
|
|
|
|
it("flags low confidence", () => {
|
|
const item = baseItem({ confidence: 0.3, verifiedByUser: false });
|
|
const result = buildReconciliationCandidates(
|
|
[item],
|
|
{ plannedRecipeIngredientIds: new Map(), daysLeftByItemId: new Map(), dailyConsumptionRate: new Map(), priceMinorByItemId: new Map() },
|
|
PINNED,
|
|
);
|
|
expect(result[0]?.reasons).toContainEqual({ kind: "low_confidence", confidence: 0.3 });
|
|
});
|
|
|
|
it("suggests depleted when remaining days < 0.5", () => {
|
|
const item = baseItem({ quantity: 0.1 });
|
|
const result = buildReconciliationCandidates(
|
|
[item],
|
|
{
|
|
plannedRecipeIngredientIds: new Map(),
|
|
daysLeftByItemId: new Map(),
|
|
dailyConsumptionRate: new Map([["itm-1", 1]]),
|
|
priceMinorByItemId: new Map(),
|
|
},
|
|
PINNED,
|
|
);
|
|
expect(result[0]?.suggestedAction).toBe("depleted");
|
|
});
|
|
|
|
it("caps results at maxItems", () => {
|
|
const items = Array.from({ length: 20 }, (_, i) =>
|
|
baseItem({ id: `itm-${i}`, displayName: `Vara ${String(i).padStart(2, "0")}`, confidence: 0.1 }),
|
|
);
|
|
const result = buildReconciliationCandidates(
|
|
items,
|
|
{ plannedRecipeIngredientIds: new Map(), daysLeftByItemId: new Map(), dailyConsumptionRate: new Map(), priceMinorByItemId: new Map() },
|
|
PINNED,
|
|
5,
|
|
);
|
|
expect(result).toHaveLength(5);
|
|
});
|
|
});
|