ci: trigga på master + formatfix inför Gitea Actions
CI / Typecheck, test & build (push) Failing after 2s

This commit is contained in:
Sven (AAMOS AI)
2026-08-13 17:25:14 +07:00
parent 61d60931ad
commit c32a7e33c7
141 changed files with 40555 additions and 45210 deletions
+465 -110
View File
@@ -18,7 +18,12 @@ describe("cooking sessions", () => {
let recipeId: string;
const email = "cooking-session-test@example.invalid";
async function createItemWithPurchase(canonicalIngredientId: string, displayName: string, quantity: number, unit: Unit) {
async function createItemWithPurchase(
canonicalIngredientId: string,
displayName: string,
quantity: number,
unit: Unit,
) {
const [location] = await testDb.db
.select({ id: schema.storageLocations.id })
.from(schema.storageLocations)
@@ -64,26 +69,46 @@ describe("cooking sessions", () => {
.where(eq(schema.inventoryTransactions.cookingSessionId, s.id));
await testDb.db.delete(schema.meals).where(eq(schema.meals.cookingSessionId, s.id));
await testDb.db.delete(schema.mealBoxes).where(eq(schema.mealBoxes.cookingSessionId, s.id));
await testDb.db.delete(schema.recipeCooks).where(eq(schema.recipeCooks.cookingSessionId, s.id));
await testDb.db
.delete(schema.recipeCooks)
.where(eq(schema.recipeCooks.cookingSessionId, s.id));
}
await testDb.db.delete(schema.cookingSessions).where(eq(schema.cookingSessions.startedByUserId, u.id));
await testDb.db
.delete(schema.cookingSessions)
.where(eq(schema.cookingSessions.startedByUserId, u.id));
const memberships = await testDb.db
.select({ householdId: schema.householdMembers.householdId })
.from(schema.householdMembers)
.where(eq(schema.householdMembers.userId, u.id));
for (const m of memberships) {
await testDb.db.delete(schema.inventoryItems).where(eq(schema.inventoryItems.householdId, m.householdId));
await testDb.db.delete(schema.mealBoxes).where(eq(schema.mealBoxes.householdId, m.householdId));
await testDb.db
.delete(schema.inventoryItems)
.where(eq(schema.inventoryItems.householdId, m.householdId));
await testDb.db
.delete(schema.mealBoxes)
.where(eq(schema.mealBoxes.householdId, m.householdId));
await testDb.db.delete(schema.meals).where(eq(schema.meals.householdId, m.householdId));
await testDb.db.delete(schema.recipeCooks).where(eq(schema.recipeCooks.householdId, m.householdId));
await testDb.db.delete(schema.cookingSessions).where(eq(schema.cookingSessions.householdId, m.householdId));
await testDb.db.delete(schema.storageLocations).where(eq(schema.storageLocations.householdId, m.householdId));
await testDb.db.delete(schema.cookingAssumptionProfiles).where(eq(schema.cookingAssumptionProfiles.householdId, m.householdId));
await testDb.db.delete(schema.householdMembers).where(eq(schema.householdMembers.householdId, m.householdId));
await testDb.db
.delete(schema.recipeCooks)
.where(eq(schema.recipeCooks.householdId, m.householdId));
await testDb.db
.delete(schema.cookingSessions)
.where(eq(schema.cookingSessions.householdId, m.householdId));
await testDb.db
.delete(schema.storageLocations)
.where(eq(schema.storageLocations.householdId, m.householdId));
await testDb.db
.delete(schema.cookingAssumptionProfiles)
.where(eq(schema.cookingAssumptionProfiles.householdId, m.householdId));
await testDb.db
.delete(schema.householdMembers)
.where(eq(schema.householdMembers.householdId, m.householdId));
await testDb.db.delete(schema.households).where(eq(schema.households.id, m.householdId));
}
await testDb.db.delete(schema.userPreferences).where(eq(schema.userPreferences.userId, u.id));
await testDb.db.delete(schema.productAnalyticsEvents).where(eq(schema.productAnalyticsEvents.userId, u.id));
await testDb.db
.delete(schema.productAnalyticsEvents)
.where(eq(schema.productAnalyticsEvents.userId, u.id));
await testDb.db.delete(schema.users).where(eq(schema.users.id, u.id));
}
}
@@ -99,7 +124,11 @@ describe("cooking sessions", () => {
payload: { email, password: "Password123!", displayName: "Cooking Test" },
});
token = (JSON.parse(res.body) as { accessToken: string }).accessToken;
const profile = await app.inject({ method: "GET", url: "/v1/me", headers: { authorization: `Bearer ${token}` } });
const profile = await app.inject({
method: "GET",
url: "/v1/me",
headers: { authorization: `Bearer ${token}` },
});
userId = (JSON.parse(profile.body) as { id: string }).id;
const quick = await app.inject({
@@ -168,7 +197,9 @@ describe("cooking sessions", () => {
.from(schema.productAnalyticsEvents)
.where(eq(schema.productAnalyticsEvents.userId, userId))
.orderBy(schema.productAnalyticsEvents.occurredAt);
expect(started.filter((e) => e.eventName === "cooking_session_started").length).toBeGreaterThanOrEqual(1);
expect(
started.filter((e) => e.eventName === "cooking_session_started").length,
).toBeGreaterThanOrEqual(1);
});
it("cancels a session without touching inventory", async () => {
@@ -183,7 +214,10 @@ describe("cooking sessions", () => {
const before = await testDb.db
.select({ count: count(schema.inventoryTransactions.id) })
.from(schema.inventoryTransactions)
.innerJoin(schema.inventoryItems, eq(schema.inventoryTransactions.inventoryItemId, schema.inventoryItems.id))
.innerJoin(
schema.inventoryItems,
eq(schema.inventoryTransactions.inventoryItemId, schema.inventoryItems.id),
)
.where(eq(schema.inventoryItems.householdId, householdId));
const res = await app.inject({
@@ -199,7 +233,10 @@ describe("cooking sessions", () => {
const after = await testDb.db
.select({ count: count(schema.inventoryTransactions.id) })
.from(schema.inventoryTransactions)
.innerJoin(schema.inventoryItems, eq(schema.inventoryTransactions.inventoryItemId, schema.inventoryItems.id))
.innerJoin(
schema.inventoryItems,
eq(schema.inventoryTransactions.inventoryItemId, schema.inventoryItems.id),
)
.where(eq(schema.inventoryItems.householdId, householdId));
expect(after[0]!.count).toBe(before[0]!.count);
});
@@ -248,7 +285,11 @@ describe("cooking sessions", () => {
.where(eq(schema.inventoryItems.id, d.itemId))
.limit(1);
const txs = await testDb.db
.select({ type: schema.inventoryTransactions.type, quantityDelta: schema.inventoryTransactions.quantityDelta, unit: schema.inventoryTransactions.unit })
.select({
type: schema.inventoryTransactions.type,
quantityDelta: schema.inventoryTransactions.quantityDelta,
unit: schema.inventoryTransactions.unit,
})
.from(schema.inventoryTransactions)
.where(eq(schema.inventoryTransactions.inventoryItemId, d.itemId));
const balance = computeBalance(txs);
@@ -264,7 +305,11 @@ describe("cooking sessions", () => {
payload: { portionsCooked: 4, mealBoxPortions: 2, deductInventory: true },
});
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body) as { ok: boolean; sessionId: string; mealBoxId: string | null };
const body = JSON.parse(res.body) as {
ok: boolean;
sessionId: string;
mealBoxId: string | null;
};
expect(body.ok).toBe(true);
expect(body.sessionId).toBeDefined();
expect(body.mealBoxId).toBeDefined();
@@ -298,14 +343,20 @@ describe("cooking sessions", () => {
});
it("legacy /cook updates cooking assumption profiles", async () => {
await testDb.db.delete(schema.cookingAssumptionProfiles).where(eq(schema.cookingAssumptionProfiles.householdId, householdId));
await testDb.db
.delete(schema.cookingAssumptionProfiles)
.where(eq(schema.cookingAssumptionProfiles.householdId, householdId));
const recipe = (await app.inject({
method: "GET",
url: `/v1/recipes/${recipeId}`,
headers: { authorization: `Bearer ${token}` },
})).json() as { ingredients: Array<{ canonicalIngredientId: string; optional?: boolean }> };
const firstNonOptionalIngredientId = recipe.ingredients.find((i) => i.canonicalIngredientId && !i.optional)?.canonicalIngredientId;
const recipe = (
await app.inject({
method: "GET",
url: `/v1/recipes/${recipeId}`,
headers: { authorization: `Bearer ${token}` },
})
).json() as { ingredients: Array<{ canonicalIngredientId: string; optional?: boolean }> };
const firstNonOptionalIngredientId = recipe.ingredients.find(
(i) => i.canonicalIngredientId && !i.optional,
)?.canonicalIngredientId;
expect(firstNonOptionalIngredientId).toBeDefined();
await app.inject({
@@ -338,7 +389,10 @@ describe("cooking sessions", () => {
it("legacy /cook writes cooking_session_started and cooking_session_completed analytics", async () => {
const before = await testDb.db
.select({ id: schema.productAnalyticsEvents.id, eventName: schema.productAnalyticsEvents.eventName })
.select({
id: schema.productAnalyticsEvents.id,
eventName: schema.productAnalyticsEvents.eventName,
})
.from(schema.productAnalyticsEvents)
.where(eq(schema.productAnalyticsEvents.userId, userId));
const beforeIds = new Set(before.map((e) => e.id));
@@ -357,9 +411,14 @@ describe("cooking sessions", () => {
.from(schema.productAnalyticsEvents)
.where(eq(schema.productAnalyticsEvents.userId, userId));
const newEvents = after.filter((e) => !beforeIds.has(e.id));
const props = (e: (typeof after)[number]) => (e.properties ?? {}) as { cookingSessionId?: string };
const started = newEvents.filter((e) => e.eventName === "cooking_session_started" && props(e).cookingSessionId === sessionId);
const completed = newEvents.filter((e) => e.eventName === "cooking_session_completed" && props(e).cookingSessionId === sessionId);
const props = (e: (typeof after)[number]) =>
(e.properties ?? {}) as { cookingSessionId?: string };
const started = newEvents.filter(
(e) => e.eventName === "cooking_session_started" && props(e).cookingSessionId === sessionId,
);
const completed = newEvents.filter(
(e) => e.eventName === "cooking_session_completed" && props(e).cookingSessionId === sessionId,
);
expect(started.length).toBe(1);
expect(completed.length).toBe(1);
});
@@ -395,7 +454,9 @@ describe("cooking sessions", () => {
payload: { mealBoxPortions: 1, actualPortionsEaten: 2, leftoverEstimatePortions: 1 },
});
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body) as { session: { actualPortionsEaten: number; leftoverEstimatePortions: number } };
const body = JSON.parse(res.body) as {
session: { actualPortionsEaten: number; leftoverEstimatePortions: number };
};
expect(body.session.actualPortionsEaten).toBe(2);
expect(body.session.leftoverEstimatePortions).toBe(1);
});
@@ -419,7 +480,9 @@ describe("cooking sessions", () => {
});
it("updates cooking assumption profiles per household and ingredient", async () => {
await testDb.db.delete(schema.cookingAssumptionProfiles).where(eq(schema.cookingAssumptionProfiles.householdId, householdId));
await testDb.db
.delete(schema.cookingAssumptionProfiles)
.where(eq(schema.cookingAssumptionProfiles.householdId, householdId));
const start = await app.inject({
method: "POST",
@@ -429,12 +492,16 @@ describe("cooking sessions", () => {
});
const sessionId = (JSON.parse(start.body) as { session: { id: string } }).session.id;
const recipe = (await app.inject({
method: "GET",
url: `/v1/recipes/${recipeId}`,
headers: { authorization: `Bearer ${token}` },
})).json() as { ingredients: Array<{ canonicalIngredientId: string; optional?: boolean }> };
const firstNonOptionalIngredientId = recipe.ingredients.find((i) => i.canonicalIngredientId && !i.optional)?.canonicalIngredientId;
const recipe = (
await app.inject({
method: "GET",
url: `/v1/recipes/${recipeId}`,
headers: { authorization: `Bearer ${token}` },
})
).json() as { ingredients: Array<{ canonicalIngredientId: string; optional?: boolean }> };
const firstNonOptionalIngredientId = recipe.ingredients.find(
(i) => i.canonicalIngredientId && !i.optional,
)?.canonicalIngredientId;
expect(firstNonOptionalIngredientId).toBeDefined();
await app.inject({
@@ -461,7 +528,9 @@ describe("cooking sessions", () => {
});
it("cooking-assumptions ignores optional first ingredient and returns defaults from a non-optional one", async () => {
await testDb.db.delete(schema.cookingAssumptionProfiles).where(eq(schema.cookingAssumptionProfiles.householdId, householdId));
await testDb.db
.delete(schema.cookingAssumptionProfiles)
.where(eq(schema.cookingAssumptionProfiles.householdId, householdId));
// Skapa ett recept där den första ingrediensen är valfri.
const [recipe] = await testDb.db
@@ -480,7 +549,16 @@ describe("cooking sessions", () => {
cookTimeMinutes: 10,
totalTimeMinutes: 15,
portions: 4,
nutritionPerPortion: { kcal: 100, proteinG: 5, fatG: 3, carbsG: 12, saturatedFatG: 1, fiberG: 1, sugarG: 2, saltG: 0.1 },
nutritionPerPortion: {
kcal: 100,
proteinG: 5,
fatG: 3,
carbsG: 12,
saturatedFatG: 1,
fiberG: 1,
sugarG: 2,
saltG: 0.1,
},
allergens: [],
spiceLevel: 0,
dna: {
@@ -554,7 +632,9 @@ describe("cooking sessions", () => {
expect(body.defaultLeftoverEstimatePortions).toBe(1);
// Städa upp testreceptet.
await testDb.db.delete(schema.recipeIngredients).where(eq(schema.recipeIngredients.recipeId, testRecipeId));
await testDb.db
.delete(schema.recipeIngredients)
.where(eq(schema.recipeIngredients.recipeId, testRecipeId));
await testDb.db.delete(schema.recipes).where(eq(schema.recipes.id, testRecipeId));
});
@@ -619,12 +699,16 @@ describe("cooking sessions", () => {
});
it("undo restores inventory, removes meals, discards meal boxes and deletes recipe_cooks", async () => {
const recipe = (await app.inject({
method: "GET",
url: `/v1/recipes/${recipeId}`,
headers: { authorization: `Bearer ${token}` },
})).json() as { ingredients: Array<{ canonicalIngredientId: string; optional?: boolean }> };
const firstNonOptional = recipe.ingredients.find((i) => i.canonicalIngredientId && !i.optional)?.canonicalIngredientId;
const recipe = (
await app.inject({
method: "GET",
url: `/v1/recipes/${recipeId}`,
headers: { authorization: `Bearer ${token}` },
})
).json() as { ingredients: Array<{ canonicalIngredientId: string; optional?: boolean }> };
const firstNonOptional = recipe.ingredients.find(
(i) => i.canonicalIngredientId && !i.optional,
)?.canonicalIngredientId;
// Sätt upp ett känt lager om receptet har en icke-valfri ingrediens.
let itemId: string | undefined;
@@ -660,7 +744,11 @@ describe("cooking sessions", () => {
payload: {},
});
expect(undo.statusCode).toBe(200);
const undoBody = JSON.parse(undo.body) as { ok: boolean; removedMealIds: string[]; discardedMealBoxIds: string[] };
const undoBody = JSON.parse(undo.body) as {
ok: boolean;
removedMealIds: string[];
discardedMealBoxIds: string[];
};
expect(undoBody.ok).toBe(true);
// Sessionstatus = undone.
@@ -679,16 +767,25 @@ describe("cooking sessions", () => {
expect(Number(txsAfter[0]!.count)).toBeGreaterThan(Number(txsBefore[0]!.count));
// Meals borttagna.
const meals = await testDb.db.select({ count: count(schema.meals.id) }).from(schema.meals).where(eq(schema.meals.cookingSessionId, sessionId));
const meals = await testDb.db
.select({ count: count(schema.meals.id) })
.from(schema.meals)
.where(eq(schema.meals.cookingSessionId, sessionId));
expect(Number(meals[0]!.count)).toBe(0);
// Matlådor markerade discarded.
const boxes = await testDb.db.select().from(schema.mealBoxes).where(eq(schema.mealBoxes.cookingSessionId, sessionId));
const boxes = await testDb.db
.select()
.from(schema.mealBoxes)
.where(eq(schema.mealBoxes.cookingSessionId, sessionId));
expect(boxes.length).toBe(undoBody.discardedMealBoxIds.length);
for (const box of boxes) expect(box.status).toBe("discarded");
// recipe_cooks borttagen och cookCount backad.
const cooks = await testDb.db.select().from(schema.recipeCooks).where(eq(schema.recipeCooks.cookingSessionId, sessionId));
const cooks = await testDb.db
.select()
.from(schema.recipeCooks)
.where(eq(schema.recipeCooks.cookingSessionId, sessionId));
expect(cooks.length).toBe(0);
// Inventory-transaktionsinvariant.
@@ -699,7 +796,11 @@ describe("cooking sessions", () => {
.where(eq(schema.inventoryItems.id, itemId))
.limit(1);
const itemTxs = await testDb.db
.select({ type: schema.inventoryTransactions.type, quantityDelta: schema.inventoryTransactions.quantityDelta, unit: schema.inventoryTransactions.unit })
.select({
type: schema.inventoryTransactions.type,
quantityDelta: schema.inventoryTransactions.quantityDelta,
unit: schema.inventoryTransactions.unit,
})
.from(schema.inventoryTransactions)
.where(eq(schema.inventoryTransactions.inventoryItemId, itemId));
const balance = computeBalance(itemTxs);
@@ -712,13 +813,16 @@ describe("cooking sessions", () => {
.from(schema.productAnalyticsEvents)
.where(eq(schema.productAnalyticsEvents.userId, userId))
.orderBy(schema.productAnalyticsEvents.occurredAt);
const props = (e: (typeof analytics)[number]) => (e.properties ?? {}) as { cookingSessionId?: string };
expect(analytics.some((e) => e.eventName === "cooking_session_undone" && props(e).cookingSessionId === sessionId)).toBe(true);
const props = (e: (typeof analytics)[number]) =>
(e.properties ?? {}) as { cookingSessionId?: string };
expect(
analytics.some(
(e) => e.eventName === "cooking_session_undone" && props(e).cookingSessionId === sessionId,
),
).toBe(true);
if (itemId) {
await testDb.db
.delete(schema.inventoryItems)
.where(eq(schema.inventoryItems.id, itemId));
await testDb.db.delete(schema.inventoryItems).where(eq(schema.inventoryItems.id, itemId));
}
});
@@ -771,10 +875,28 @@ describe("cooking sessions", () => {
cookTimeMinutes: 10,
totalTimeMinutes: 15,
portions: 4,
nutritionPerPortion: { kcal: 100, proteinG: 5, fatG: 3, carbsG: 12, saturatedFatG: 1, fiberG: 1, sugarG: 2, saltG: 0.1 },
nutritionPerPortion: {
kcal: 100,
proteinG: 5,
fatG: 3,
carbsG: 12,
saturatedFatG: 1,
fiberG: 1,
sugarG: 2,
saltG: 0.1,
},
allergens: [],
spiceLevel: 0,
dna: { cuisine: "international", vegetables: [], flavorProfile: [], spiceLevel: 0, method: "stovetop", timeMinutes: 15, calories: 100, proteinGrams: 5 },
dna: {
cuisine: "international",
vegetables: [],
flavorProfile: [],
spiceLevel: 0,
method: "stovetop",
timeMinutes: 15,
calories: 100,
proteinGrams: 5,
},
status: "published",
verificationStatus: "unverified",
sourceType: "own_editorial",
@@ -794,7 +916,12 @@ describe("cooking sessions", () => {
await testDb.db
.delete(schema.inventoryItems)
.where(and(eq(schema.inventoryItems.householdId, householdId), eq(schema.inventoryItems.canonicalIngredientId, "pasta_dry")));
.where(
and(
eq(schema.inventoryItems.householdId, householdId),
eq(schema.inventoryItems.canonicalIngredientId, "pasta_dry"),
),
);
const itemId = await createItemWithPurchase("pasta_dry", "Pasta", 400, "GRAM");
const start = await app.inject({
@@ -838,8 +965,15 @@ describe("cooking sessions", () => {
// Städa testreceptet och lagerposten.
await testDb.db
.delete(schema.inventoryItems)
.where(and(eq(schema.inventoryItems.householdId, householdId), eq(schema.inventoryItems.canonicalIngredientId, "pasta_dry")));
await testDb.db.delete(schema.recipeIngredients).where(eq(schema.recipeIngredients.recipeId, testRecipeId));
.where(
and(
eq(schema.inventoryItems.householdId, householdId),
eq(schema.inventoryItems.canonicalIngredientId, "pasta_dry"),
),
);
await testDb.db
.delete(schema.recipeIngredients)
.where(eq(schema.recipeIngredients.recipeId, testRecipeId));
await testDb.db.delete(schema.recipes).where(eq(schema.recipes.id, testRecipeId));
});
@@ -879,10 +1013,28 @@ describe("cooking sessions", () => {
cookTimeMinutes: 10,
totalTimeMinutes: 15,
portions: 4,
nutritionPerPortion: { kcal: 100, proteinG: 5, fatG: 3, carbsG: 12, saturatedFatG: 1, fiberG: 1, sugarG: 2, saltG: 0.1 },
nutritionPerPortion: {
kcal: 100,
proteinG: 5,
fatG: 3,
carbsG: 12,
saturatedFatG: 1,
fiberG: 1,
sugarG: 2,
saltG: 0.1,
},
allergens: [],
spiceLevel: 0,
dna: { cuisine: "international", vegetables: [], flavorProfile: [], spiceLevel: 0, method: "stovetop", timeMinutes: 15, calories: 100, proteinGrams: 5 },
dna: {
cuisine: "international",
vegetables: [],
flavorProfile: [],
spiceLevel: 0,
method: "stovetop",
timeMinutes: 15,
calories: 100,
proteinGrams: 5,
},
status: "published",
verificationStatus: "unverified",
sourceType: "own_editorial",
@@ -902,7 +1054,12 @@ describe("cooking sessions", () => {
await testDb.db
.delete(schema.inventoryItems)
.where(and(eq(schema.inventoryItems.householdId, householdId), eq(schema.inventoryItems.canonicalIngredientId, "chicken_breast")));
.where(
and(
eq(schema.inventoryItems.householdId, householdId),
eq(schema.inventoryItems.canonicalIngredientId, "chicken_breast"),
),
);
const itemId = await createItemWithPurchase("chicken_breast", "Kyckling", 400, "GRAM");
const start = await app.inject({
@@ -930,13 +1087,20 @@ describe("cooking sessions", () => {
expect(item[0]!.quantity).toBeCloseTo(0, 1);
// Matlådan ska ha 2 portioner.
const boxes = await testDb.db.select().from(schema.mealBoxes).where(eq(schema.mealBoxes.cookingSessionId, sessionId));
const boxes = await testDb.db
.select()
.from(schema.mealBoxes)
.where(eq(schema.mealBoxes.cookingSessionId, sessionId));
expect(boxes.length).toBe(1);
expect(boxes[0]!.portions).toBe(2);
// Invariant.
const txs = await testDb.db
.select({ type: schema.inventoryTransactions.type, quantityDelta: schema.inventoryTransactions.quantityDelta, unit: schema.inventoryTransactions.unit })
.select({
type: schema.inventoryTransactions.type,
quantityDelta: schema.inventoryTransactions.quantityDelta,
unit: schema.inventoryTransactions.unit,
})
.from(schema.inventoryTransactions)
.where(eq(schema.inventoryTransactions.inventoryItemId, itemId));
const balance = computeBalance(txs);
@@ -944,8 +1108,15 @@ describe("cooking sessions", () => {
await testDb.db
.delete(schema.inventoryItems)
.where(and(eq(schema.inventoryItems.householdId, householdId), eq(schema.inventoryItems.canonicalIngredientId, "chicken_breast")));
await testDb.db.delete(schema.recipeIngredients).where(eq(schema.recipeIngredients.recipeId, testRecipeId));
.where(
and(
eq(schema.inventoryItems.householdId, householdId),
eq(schema.inventoryItems.canonicalIngredientId, "chicken_breast"),
),
);
await testDb.db
.delete(schema.recipeIngredients)
.where(eq(schema.recipeIngredients.recipeId, testRecipeId));
await testDb.db.delete(schema.recipes).where(eq(schema.recipes.id, testRecipeId));
});
@@ -966,10 +1137,28 @@ describe("cooking sessions", () => {
cookTimeMinutes: 10,
totalTimeMinutes: 15,
portions: 4,
nutritionPerPortion: { kcal: 100, proteinG: 5, fatG: 3, carbsG: 12, saturatedFatG: 1, fiberG: 1, sugarG: 2, saltG: 0.1 },
nutritionPerPortion: {
kcal: 100,
proteinG: 5,
fatG: 3,
carbsG: 12,
saturatedFatG: 1,
fiberG: 1,
sugarG: 2,
saltG: 0.1,
},
allergens: [],
spiceLevel: 0,
dna: { cuisine: "international", vegetables: [], flavorProfile: [], spiceLevel: 0, method: "stovetop", timeMinutes: 15, calories: 100, proteinGrams: 5 },
dna: {
cuisine: "international",
vegetables: [],
flavorProfile: [],
spiceLevel: 0,
method: "stovetop",
timeMinutes: 15,
calories: 100,
proteinGrams: 5,
},
status: "published",
verificationStatus: "unverified",
sourceType: "own_editorial",
@@ -989,7 +1178,12 @@ describe("cooking sessions", () => {
await testDb.db
.delete(schema.inventoryItems)
.where(and(eq(schema.inventoryItems.householdId, householdId), eq(schema.inventoryItems.canonicalIngredientId, "carrot")));
.where(
and(
eq(schema.inventoryItems.householdId, householdId),
eq(schema.inventoryItems.canonicalIngredientId, "carrot"),
),
);
const itemId = await createItemWithPurchase("carrot", "Morötter", 400, "GRAM");
const start = await app.inject({
@@ -1017,7 +1211,11 @@ describe("cooking sessions", () => {
expect(item[0]!.quantity).toBeCloseTo(100, 1);
const txs = await testDb.db
.select({ type: schema.inventoryTransactions.type, quantityDelta: schema.inventoryTransactions.quantityDelta, unit: schema.inventoryTransactions.unit })
.select({
type: schema.inventoryTransactions.type,
quantityDelta: schema.inventoryTransactions.quantityDelta,
unit: schema.inventoryTransactions.unit,
})
.from(schema.inventoryTransactions)
.where(eq(schema.inventoryTransactions.inventoryItemId, itemId));
const balance = computeBalance(txs);
@@ -1025,8 +1223,15 @@ describe("cooking sessions", () => {
await testDb.db
.delete(schema.inventoryItems)
.where(and(eq(schema.inventoryItems.householdId, householdId), eq(schema.inventoryItems.canonicalIngredientId, "carrot")));
await testDb.db.delete(schema.recipeIngredients).where(eq(schema.recipeIngredients.recipeId, testRecipeId));
.where(
and(
eq(schema.inventoryItems.householdId, householdId),
eq(schema.inventoryItems.canonicalIngredientId, "carrot"),
),
);
await testDb.db
.delete(schema.recipeIngredients)
.where(eq(schema.recipeIngredients.recipeId, testRecipeId));
await testDb.db.delete(schema.recipes).where(eq(schema.recipes.id, testRecipeId));
});
@@ -1047,10 +1252,28 @@ describe("cooking sessions", () => {
cookTimeMinutes: 10,
totalTimeMinutes: 15,
portions: 4,
nutritionPerPortion: { kcal: 100, proteinG: 5, fatG: 3, carbsG: 12, saturatedFatG: 1, fiberG: 1, sugarG: 2, saltG: 0.1 },
nutritionPerPortion: {
kcal: 100,
proteinG: 5,
fatG: 3,
carbsG: 12,
saturatedFatG: 1,
fiberG: 1,
sugarG: 2,
saltG: 0.1,
},
allergens: [],
spiceLevel: 0,
dna: { cuisine: "international", vegetables: [], flavorProfile: [], spiceLevel: 0, method: "stovetop", timeMinutes: 15, calories: 100, proteinGrams: 5 },
dna: {
cuisine: "international",
vegetables: [],
flavorProfile: [],
spiceLevel: 0,
method: "stovetop",
timeMinutes: 15,
calories: 100,
proteinGrams: 5,
},
status: "published",
verificationStatus: "unverified",
sourceType: "own_editorial",
@@ -1070,7 +1293,12 @@ describe("cooking sessions", () => {
await testDb.db
.delete(schema.inventoryItems)
.where(and(eq(schema.inventoryItems.householdId, householdId), eq(schema.inventoryItems.canonicalIngredientId, "potato")));
.where(
and(
eq(schema.inventoryItems.householdId, householdId),
eq(schema.inventoryItems.canonicalIngredientId, "potato"),
),
);
const itemId = await createItemWithPurchase("potato", "Potatis", 400, "GRAM");
const start = await app.inject({
@@ -1099,13 +1327,20 @@ describe("cooking sessions", () => {
expect(item[0]!.quantity).toBeCloseTo(0, 1);
// Lådan ska ha 1 portion.
const boxes = await testDb.db.select().from(schema.mealBoxes).where(eq(schema.mealBoxes.cookingSessionId, sessionId));
const boxes = await testDb.db
.select()
.from(schema.mealBoxes)
.where(eq(schema.mealBoxes.cookingSessionId, sessionId));
expect(boxes.length).toBe(1);
expect(boxes[0]!.portions).toBe(1);
// Invariant.
const txs = await testDb.db
.select({ type: schema.inventoryTransactions.type, quantityDelta: schema.inventoryTransactions.quantityDelta, unit: schema.inventoryTransactions.unit })
.select({
type: schema.inventoryTransactions.type,
quantityDelta: schema.inventoryTransactions.quantityDelta,
unit: schema.inventoryTransactions.unit,
})
.from(schema.inventoryTransactions)
.where(eq(schema.inventoryTransactions.inventoryItemId, itemId));
const balance = computeBalance(txs);
@@ -1113,8 +1348,15 @@ describe("cooking sessions", () => {
await testDb.db
.delete(schema.inventoryItems)
.where(and(eq(schema.inventoryItems.householdId, householdId), eq(schema.inventoryItems.canonicalIngredientId, "potato")));
await testDb.db.delete(schema.recipeIngredients).where(eq(schema.recipeIngredients.recipeId, testRecipeId));
.where(
and(
eq(schema.inventoryItems.householdId, householdId),
eq(schema.inventoryItems.canonicalIngredientId, "potato"),
),
);
await testDb.db
.delete(schema.recipeIngredients)
.where(eq(schema.recipeIngredients.recipeId, testRecipeId));
await testDb.db.delete(schema.recipes).where(eq(schema.recipes.id, testRecipeId));
});
@@ -1135,10 +1377,28 @@ describe("cooking sessions", () => {
cookTimeMinutes: 10,
totalTimeMinutes: 15,
portions: 4,
nutritionPerPortion: { kcal: 100, proteinG: 5, fatG: 3, carbsG: 12, saturatedFatG: 1, fiberG: 1, sugarG: 2, saltG: 0.1 },
nutritionPerPortion: {
kcal: 100,
proteinG: 5,
fatG: 3,
carbsG: 12,
saturatedFatG: 1,
fiberG: 1,
sugarG: 2,
saltG: 0.1,
},
allergens: [],
spiceLevel: 0,
dna: { cuisine: "international", vegetables: [], flavorProfile: [], spiceLevel: 0, method: "stovetop", timeMinutes: 15, calories: 100, proteinGrams: 5 },
dna: {
cuisine: "international",
vegetables: [],
flavorProfile: [],
spiceLevel: 0,
method: "stovetop",
timeMinutes: 15,
calories: 100,
proteinGrams: 5,
},
status: "published",
verificationStatus: "unverified",
sourceType: "own_editorial",
@@ -1158,7 +1418,12 @@ describe("cooking sessions", () => {
await testDb.db
.delete(schema.inventoryItems)
.where(and(eq(schema.inventoryItems.householdId, householdId), eq(schema.inventoryItems.canonicalIngredientId, "rice_white")));
.where(
and(
eq(schema.inventoryItems.householdId, householdId),
eq(schema.inventoryItems.canonicalIngredientId, "rice_white"),
),
);
const itemId = await createItemWithPurchase("rice_white", "Ris", 400, "GRAM");
async function assertInvariant() {
@@ -1168,7 +1433,11 @@ describe("cooking sessions", () => {
.where(eq(schema.inventoryItems.id, itemId))
.limit(1);
const txs = await testDb.db
.select({ type: schema.inventoryTransactions.type, quantityDelta: schema.inventoryTransactions.quantityDelta, unit: schema.inventoryTransactions.unit })
.select({
type: schema.inventoryTransactions.type,
quantityDelta: schema.inventoryTransactions.quantityDelta,
unit: schema.inventoryTransactions.unit,
})
.from(schema.inventoryTransactions)
.where(eq(schema.inventoryTransactions.inventoryItemId, itemId));
const balance = computeBalance(txs);
@@ -1217,8 +1486,15 @@ describe("cooking sessions", () => {
await testDb.db
.delete(schema.inventoryItems)
.where(and(eq(schema.inventoryItems.householdId, householdId), eq(schema.inventoryItems.canonicalIngredientId, "rice_white")));
await testDb.db.delete(schema.recipeIngredients).where(eq(schema.recipeIngredients.recipeId, testRecipeId));
.where(
and(
eq(schema.inventoryItems.householdId, householdId),
eq(schema.inventoryItems.canonicalIngredientId, "rice_white"),
),
);
await testDb.db
.delete(schema.recipeIngredients)
.where(eq(schema.recipeIngredients.recipeId, testRecipeId));
await testDb.db.delete(schema.recipes).where(eq(schema.recipes.id, testRecipeId));
});
@@ -1247,26 +1523,40 @@ describe("cooking sessions", () => {
.limit(1);
expect(session[0]!.status).toBe("undone");
const boxes = await testDb.db.select().from(schema.mealBoxes).where(eq(schema.mealBoxes.cookingSessionId, sessionId));
const boxes = await testDb.db
.select()
.from(schema.mealBoxes)
.where(eq(schema.mealBoxes.cookingSessionId, sessionId));
expect(boxes.every((b) => b.status === "discarded")).toBe(true);
});
it("undo rolls back cooking assumption profiles", async () => {
await testDb.db.delete(schema.cookingAssumptionProfiles).where(eq(schema.cookingAssumptionProfiles.householdId, householdId));
await testDb.db
.delete(schema.cookingAssumptionProfiles)
.where(eq(schema.cookingAssumptionProfiles.householdId, householdId));
const recipe = (await app.inject({
method: "GET",
url: `/v1/recipes/${recipeId}`,
headers: { authorization: `Bearer ${token}` },
})).json() as { ingredients: Array<{ canonicalIngredientId: string; optional?: boolean }> };
const firstNonOptionalIngredientId = recipe.ingredients.find((i) => i.canonicalIngredientId && !i.optional)?.canonicalIngredientId;
const recipe = (
await app.inject({
method: "GET",
url: `/v1/recipes/${recipeId}`,
headers: { authorization: `Bearer ${token}` },
})
).json() as { ingredients: Array<{ canonicalIngredientId: string; optional?: boolean }> };
const firstNonOptionalIngredientId = recipe.ingredients.find(
(i) => i.canonicalIngredientId && !i.optional,
)?.canonicalIngredientId;
expect(firstNonOptionalIngredientId).toBeDefined();
const cook = await app.inject({
method: "POST",
url: `/v1/recipes/${recipeId}/cook`,
headers: { authorization: `Bearer ${token}` },
payload: { portionsCooked: 4, actualPortionsEaten: 3, leftoverEstimatePortions: 1, deductInventory: true },
payload: {
portionsCooked: 4,
actualPortionsEaten: 3,
leftoverEstimatePortions: 1,
deductInventory: true,
},
});
const { sessionId } = JSON.parse(cook.body) as { sessionId: string };
@@ -1315,12 +1605,20 @@ describe("cooking sessions", () => {
method: "POST",
url: `/v1/recipes/${recipeId}/cook`,
headers: { authorization: `Bearer ${token}` },
payload: { portionsCooked: 4, mealBoxPortions: 1, mealBoxFrozen: false, deductInventory: true },
payload: {
portionsCooked: 4,
mealBoxPortions: 1,
mealBoxFrozen: false,
deductInventory: true,
},
});
expect(cook1.statusCode).toBe(200);
const { sessionId: sessionId1 } = JSON.parse(cook1.body) as { sessionId: string };
const boxes1 = await testDb.db.select().from(schema.mealBoxes).where(eq(schema.mealBoxes.cookingSessionId, sessionId1));
const boxes1 = await testDb.db
.select()
.from(schema.mealBoxes)
.where(eq(schema.mealBoxes.cookingSessionId, sessionId1));
expect(boxes1.length).toBe(1);
const boxId = boxes1[0]!.id;
@@ -1328,12 +1626,20 @@ describe("cooking sessions", () => {
method: "POST",
url: `/v1/recipes/${recipeId}/cook`,
headers: { authorization: `Bearer ${token}` },
payload: { portionsCooked: 4, mealBoxPortions: 2, mealBoxFrozen: false, deductInventory: true },
payload: {
portionsCooked: 4,
mealBoxPortions: 2,
mealBoxFrozen: false,
deductInventory: true,
},
});
expect(cook2.statusCode).toBe(200);
const { sessionId: sessionId2 } = JSON.parse(cook2.body) as { sessionId: string };
const boxes2 = await testDb.db.select().from(schema.mealBoxes).where(eq(schema.mealBoxes.id, boxId));
const boxes2 = await testDb.db
.select()
.from(schema.mealBoxes)
.where(eq(schema.mealBoxes.id, boxId));
expect(boxes2[0]!.portions).toBe(3);
expect(boxes2[0]!.portionsRemaining).toBe(3);
@@ -1352,7 +1658,13 @@ describe("cooking sessions", () => {
method: "POST",
url: `/v1/recipes/${recipeId}/cook`,
headers: { authorization: `Bearer ${token}` },
payload: { portionsCooked: 4, mealBoxPortions: 1, mealBoxFrozen: false, date: yesterday, deductInventory: true },
payload: {
portionsCooked: 4,
mealBoxPortions: 1,
mealBoxFrozen: false,
date: yesterday,
deductInventory: true,
},
});
expect(cook1.statusCode).toBe(200);
const { sessionId: sessionId1 } = JSON.parse(cook1.body) as { sessionId: string };
@@ -1361,7 +1673,12 @@ describe("cooking sessions", () => {
method: "POST",
url: `/v1/recipes/${recipeId}/cook`,
headers: { authorization: `Bearer ${token}` },
payload: { portionsCooked: 4, mealBoxPortions: 1, mealBoxFrozen: false, deductInventory: true },
payload: {
portionsCooked: 4,
mealBoxPortions: 1,
mealBoxFrozen: false,
deductInventory: true,
},
});
expect(cook2.statusCode).toBe(200);
const { sessionId: sessionId2 } = JSON.parse(cook2.body) as { sessionId: string };
@@ -1385,7 +1702,12 @@ describe("cooking sessions", () => {
method: "POST",
url: `/v1/recipes/${recipeId}/cook`,
headers: { authorization: `Bearer ${token}` },
payload: { portionsCooked: 4, mealBoxPortions: 1, mealBoxFrozen: false, deductInventory: true },
payload: {
portionsCooked: 4,
mealBoxPortions: 1,
mealBoxFrozen: false,
deductInventory: true,
},
});
expect(cook1.statusCode).toBe(200);
const { sessionId: sessionId1 } = JSON.parse(cook1.body) as { sessionId: string };
@@ -1394,7 +1716,12 @@ describe("cooking sessions", () => {
method: "POST",
url: `/v1/recipes/${recipeId}/cook`,
headers: { authorization: `Bearer ${token}` },
payload: { portionsCooked: 4, mealBoxPortions: 1, mealBoxFrozen: true, deductInventory: true },
payload: {
portionsCooked: 4,
mealBoxPortions: 1,
mealBoxFrozen: true,
deductInventory: true,
},
});
expect(cook2.statusCode).toBe(200);
const { sessionId: sessionId2 } = JSON.parse(cook2.body) as { sessionId: string };
@@ -1420,20 +1747,38 @@ describe("cooking sessions", () => {
method: "POST",
url: `/v1/recipes/${recipeId}/cook`,
headers: { authorization: `Bearer ${token}` },
payload: { portionsCooked: 4, mealBoxPortions: 1, mealBoxFrozen: false, deductInventory: true },
payload: {
portionsCooked: 4,
mealBoxPortions: 1,
mealBoxFrozen: false,
deductInventory: true,
},
});
const { sessionId: sessionId1 } = JSON.parse(cook1.body) as { sessionId: string };
const boxId = (await testDb.db.select().from(schema.mealBoxes).where(eq(schema.mealBoxes.cookingSessionId, sessionId1)))[0]!.id;
const boxId = (
await testDb.db
.select()
.from(schema.mealBoxes)
.where(eq(schema.mealBoxes.cookingSessionId, sessionId1))
)[0]!.id;
const cook2 = await app.inject({
method: "POST",
url: `/v1/recipes/${recipeId}/cook`,
headers: { authorization: `Bearer ${token}` },
payload: { portionsCooked: 4, mealBoxPortions: 2, mealBoxFrozen: false, deductInventory: true },
payload: {
portionsCooked: 4,
mealBoxPortions: 2,
mealBoxFrozen: false,
deductInventory: true,
},
});
const { sessionId: sessionId2 } = JSON.parse(cook2.body) as { sessionId: string };
const beforeUndo = await testDb.db.select().from(schema.mealBoxes).where(eq(schema.mealBoxes.id, boxId));
const beforeUndo = await testDb.db
.select()
.from(schema.mealBoxes)
.where(eq(schema.mealBoxes.id, boxId));
expect(beforeUndo[0]!.portions).toBe(3);
const undo = await app.inject({
@@ -1444,7 +1789,10 @@ describe("cooking sessions", () => {
});
expect(undo.statusCode).toBe(200);
const afterUndo = await testDb.db.select().from(schema.mealBoxes).where(eq(schema.mealBoxes.id, boxId));
const afterUndo = await testDb.db
.select()
.from(schema.mealBoxes)
.where(eq(schema.mealBoxes.id, boxId));
expect(afterUndo[0]!.portions).toBe(1);
expect(afterUndo[0]!.portionsRemaining).toBe(1);
expect(afterUndo[0]!.status).toBe("available");
@@ -1469,7 +1817,10 @@ describe("cooking sessions", () => {
});
expect(undo.statusCode).toBe(200);
const boxes = await testDb.db.select().from(schema.mealBoxes).where(eq(schema.mealBoxes.cookingSessionId, sessionId));
const boxes = await testDb.db
.select()
.from(schema.mealBoxes)
.where(eq(schema.mealBoxes.cookingSessionId, sessionId));
expect(boxes[0]!.status).toBe("discarded");
expect(boxes[0]!.portionsRemaining).toBe(0);
});
@@ -1504,7 +1855,11 @@ describe("cooking sessions", () => {
.from(schema.inventoryTransactions)
.where(eq(schema.inventoryTransactions.inventoryItemId, itemId));
const balance = computeBalance(txs);
const item = await testDb.db.select().from(schema.inventoryItems).where(eq(schema.inventoryItems.id, itemId)).limit(1);
const item = await testDb.db
.select()
.from(schema.inventoryItems)
.where(eq(schema.inventoryItems.id, itemId))
.limit(1);
expect(item[0]!.quantity).toBeCloseTo(balance.balance, 6);
};