Initial commit (unpacked platform)

This commit is contained in:
Sven (AAMOS AI)
2026-08-05 19:21:11 +07:00
commit ac5340195a
314 changed files with 57584 additions and 0 deletions
@@ -0,0 +1,214 @@
import { describe, expect, it } from "vitest";
import type { NutritionDeclaration } from "@app/shared-types";
import {
bmrMifflinStJeor,
computeDailyTargets,
computeItemNutrition,
computeRecipeNutrition,
convert,
scaleNutrition,
summarizeDay,
sumNutrition,
toGrams,
} from "../src/index.js";
const chicken: NutritionDeclaration = {
basis: "per_100_g",
values: {
kcal: 106,
proteinG: 22,
carbsG: 0,
fatG: 2,
saturatedFatG: 0.6,
fiberG: 0,
sugarG: 0,
saltG: 0.2,
},
};
const milk: NutritionDeclaration = {
basis: "per_100_ml",
values: {
kcal: 60,
proteinG: 3.4,
carbsG: 4.7,
fatG: 3,
saturatedFatG: 1.9,
fiberG: 0,
sugarG: 4.7,
saltG: 0.1,
},
};
describe("enhetskonvertering", () => {
it("konverterar massa", () => {
expect(toGrams(1, "KILOGRAM")).toBe(1000);
expect(toGrams(250, "GRAM")).toBe(250);
});
it("konverterar volym via densitet", () => {
expect(toGrams(1, "LITER", { densityGPerMl: 1.03 })).toBeCloseTo(1030);
expect(toGrams(1, "DECILITER", { densityGPerMl: 1 })).toBe(100);
expect(toGrams(1, "TABLESPOON", { densityGPerMl: 0.92 })).toBeCloseTo(13.8);
});
it("konverterar styck via styckvikt", () => {
expect(toGrams(2, "COUNT", { gramsPerPiece: 58 })).toBe(116);
});
it("vägrar gissa: null utan densitet/styckvikt (spec §61.4)", () => {
expect(toGrams(1, "DECILITER")).toBeNull();
expect(toGrams(1, "COUNT")).toBeNull();
expect(convert(1, "DECILITER", "GRAM")).toBeNull();
});
});
describe("näringsberäkning", () => {
it("räknar per 100 g", () => {
const result = computeItemNutrition(300, "GRAM", chicken);
expect(result?.kcal).toBeCloseTo(318);
expect(result?.proteinG).toBeCloseTo(66);
});
it("räknar per 100 ml med volymenhet", () => {
const result = computeItemNutrition(2, "DECILITER", milk, { densityGPerMl: 1.03 });
expect(result?.kcal).toBeCloseTo(120);
});
it("returnerar null när data saknas aldrig påhitt", () => {
expect(computeItemNutrition(1, "COUNT", chicken)).toBeNull();
});
it("summerar och skalar", () => {
const a = computeItemNutrition(100, "GRAM", chicken)!;
const total = sumNutrition([a, a]);
expect(total.kcal).toBeCloseTo(212);
expect(scaleNutrition(total, 0.5).kcal).toBeCloseTo(106);
});
});
describe("receptnäring", () => {
it("beräknar per portion och redovisar oberäkneliga", () => {
const sources = new Map([
["chicken", { nutritionPer100: chicken }],
["milk", { nutritionPer100: milk, densityGPerMl: 1.03 }],
]);
const result = computeRecipeNutrition(
[
{ canonicalIngredientId: "chicken", quantity: 400, unit: "GRAM" },
{ canonicalIngredientId: "milk", quantity: 2, unit: "DECILITER" },
{ canonicalIngredientId: "unknown", quantity: 1, unit: "COUNT" },
],
4,
sources,
);
expect(result.uncomputableIngredientIds).toEqual(["unknown"]);
expect(result.perPortion.kcal).toBeGreaterThan(100);
expect(result.perPortion.kcal * 4).toBeCloseTo(result.total.kcal, 0);
});
it("exkluderar valfria ingredienser", () => {
const sources = new Map([["chicken", { nutritionPer100: chicken }]]);
const withOptional = computeRecipeNutrition(
[
{ canonicalIngredientId: "chicken", quantity: 400, unit: "GRAM" },
{ canonicalIngredientId: "chicken", quantity: 400, unit: "GRAM", optional: true },
],
4,
sources,
);
expect(withOptional.total.kcal).toBeCloseTo(424);
});
});
describe("energi & dagsmål", () => {
it("MifflinSt Jeor referensvärde (man 80 kg, 180 cm, 30 år)", () => {
const bmr = bmrMifflinStJeor({ sex: "male", weightKg: 80, heightCm: 180, age: 30 });
expect(bmr).toBeCloseTo(10 * 80 + 6.25 * 180 - 5 * 30 + 5); // 1780
});
it("viktnedgång ger underskott men aldrig under golvet", () => {
const normal = computeDailyTargets({
sex: "female",
age: 35,
heightCm: 165,
weightKg: 60,
activityLevel: "moderate",
primaryGoal: "lose_weight",
});
expect(normal.targets.kcal).toBeLessThan(normal.basis.tdeeKcal);
const extreme = computeDailyTargets({
sex: "female",
age: 80,
heightCm: 145,
weightKg: 40,
activityLevel: "sedentary",
primaryGoal: "lose_weight",
});
expect(extreme.targets.kcal).toBeGreaterThanOrEqual(1200);
});
it("proteinmål skalar med kroppsvikt och mål", () => {
const base = computeDailyTargets({
sex: "male",
age: 30,
heightCm: 180,
weightKg: 80,
activityLevel: "moderate",
});
const muscle = computeDailyTargets({
sex: "male",
age: 30,
heightCm: 180,
weightKg: 80,
activityLevel: "moderate",
primaryGoal: "build_muscle",
});
expect(muscle.targets.proteinG).toBeGreaterThan(base.targets.proteinG);
expect(muscle.targets.proteinG).toBe(Math.round(80 * 1.8));
});
});
describe("dagsöversikt", () => {
it("summerar måltider och flaggar salt", () => {
const meal = {
kcal: 700,
proteinG: 40,
carbsG: 60,
fatG: 25,
saturatedFatG: 8,
fiberG: 6,
sugarG: 5,
saltG: 4,
};
const summary = summarizeDay([meal, meal], {
kcal: 2000,
proteinG: 100,
carbsG: 220,
fatG: 65,
fiberG: 25,
saltMaxG: 6,
});
expect(summary.consumed.kcal).toBe(1400);
expect(summary.remaining.kcal).toBe(600);
expect(summary.saltWarning).toBe(true);
expect(summary.progress.proteinG).toBeCloseTo(0.8);
});
});
describe("i18n-enheter (i18n-spec §9–§11)", () => {
it("amerikanska volymer konverterar enligt dokumenterad standard", () => {
expect(toGrams(1, "CUP_US", { densityGPerMl: 1 })).toBeCloseTo(236.59);
expect(toGrams(2, "FLUID_OUNCE_US", { densityGPerMl: 1 })).toBeCloseTo(59.14);
expect(toGrams(1, "TEASPOON", { densityGPerMl: 1 })).toBe(5);
expect(toGrams(1, "TABLESPOON", { densityGPerMl: 1 })).toBe(15);
expect(toGrams(2, "PINCH", { densityGPerMl: 1 })).toBe(1);
});
it("amerikanska massor är massa ingen densitet behövs", () => {
expect(toGrams(1, "OUNCE")).toBeCloseTo(28.35);
expect(toGrams(1, "POUND")).toBeCloseTo(453.59);
expect(convert(1, "POUND", "OUNCE")).toBeCloseTo(16, 1);
});
it("DECILITER och cup↔dl-konvertering utan förlust", () => {
expect(convert(1, "CUP_US", "DECILITER")).toBeCloseTo(2.3659);
expect(convert(2.3659, "DECILITER", "CUP_US")).toBeCloseTo(1, 4);
});
it("volym↔massa vägrar fortfarande utan ingrediensdensitet (i18n-spec §10)", () => {
expect(toGrams(1, "CUP_US")).toBeNull();
expect(convert(100, "GRAM", "CUP_US")).toBeNull();
});
it("count-enheter (CLOVE, CAN …) kräver styckvikt", () => {
expect(toGrams(2, "CLOVE", { gramsPerPiece: 5 })).toBe(10);
expect(toGrams(1, "CAN")).toBeNull();
});
});