Files

189 lines
6.3 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
buildWhy,
containsForbiddenCopy,
FORBIDDEN_COPY_PATTERNS,
type ProvenanceEntry,
type RecommendationCandidate,
type RecommendationContext,
} from "../src/index.js";
const nutrition = {
kcal: 550,
proteinG: 45,
carbsG: 50,
fatG: 18,
saturatedFatG: 6,
fiberG: 6,
sugarG: 4,
saltG: 1.5,
};
function candidate(overrides: Partial<RecommendationCandidate> = {}): RecommendationCandidate {
return {
recipeId: "r1",
titleSv: "Kycklinggryta",
cuisine: "swedish",
tags: [],
totalTimeMinutes: 30,
nutritionPerPortion: nutrition,
estimatedCostMinorPerPortion: 2200,
ratingAverage: null,
ratingCount: 0,
peakSeasons: ["summer"],
holidayTags: [],
spiceLevel: 1,
coverage: { coverage: 0.75, matches: [], missing: [], expiringUsed: [] },
...overrides,
};
}
const baseCtx: RecommendationContext = {
mealType: "dinner",
persons: 4,
currentSeason: "summer",
activeHolidayTags: [],
isWeekday: true,
favoriteCuisines: ["swedish"],
remainingProteinG: 60,
remainingKcal: 800,
personalizationEnabled: true,
};
const provenanceSet: ProvenanceEntry[][] = [
[],
[{ key: "favoriteCuisine", args: { cuisine: "svensk" } }],
[{ key: "cookedOften", args: { recipe: "pannkakor", count: 3 } }],
[{ key: "expiringIngredient", args: { count: 2, ingredient: "gurkan", days: 1 } }],
[{ key: "fitsProteinGoal", args: {} }],
[{ key: "fitsVegetableGoal", args: {} }],
[{ key: "tastePreference", args: { axis: "spice" } }],
[{ key: "usesStapleYouFinish", args: { ingredient: "ris" } }],
];
describe("S6 förbjuden-copy-skanning av genererade förklaringar", () => {
it("100+ varierade förklaringar innehåller ingen förbjuden copy", () => {
const explanations: string[] = [];
const langs = ["sv-SE", "en-US", "de-DE", "es-ES", "fr-FR"];
const coverages = [0.25, 0.55, 0.75, 0.92, 1];
const proteins = [15, 30, 45, 58];
const ratings = [null, 4.5, 4.9];
const costs = [1200, 2200, 4500];
const daysSince = [null, 2, 14];
for (const lang of langs) {
for (const cov of coverages) {
for (const protein of proteins) {
for (const rating of ratings) {
for (const cost of costs) {
for (const days of daysSince) {
for (const prov of provenanceSet) {
const ctx: RecommendationContext = {
...baseCtx,
isTrainingDay: protein >= 35,
activeHolidayTags: lang === "sv-SE" ? ["midsummer"] : [],
};
const c = candidate({
coverage: {
coverage: cov,
matches: [],
missing: [],
expiringUsed:
cov < 1
? [
{
canonicalIngredientId: "chicken",
displayNameSv: "kycklingen",
required: 400,
unit: "GRAM",
availableInUnit: 500,
covered: true,
optional: false,
mostUrgentDaysLeft: 2,
usesExpiringItem: true,
},
]
: [],
},
nutritionPerPortion: { ...nutrition, proteinG: protein },
estimatedCostMinorPerPortion: cost,
householdRating: rating,
daysSinceLastCooked: days,
});
const parts = {
coverage: cov,
expiry: cov < 1 ? 0.8 : 0,
nutritionFit: protein >= 25 ? 0.85 : 0.4,
taste: 0.6,
rating: rating != null ? 0.9 : 0.5,
season: 1,
holiday: ctx.activeHolidayTags.length > 0 ? 1 : 0,
time: 1,
budget: cost <= 2500 ? 1 : 0.4,
variety: days == null ? 0.8 : days < 7 ? 0 : 1,
weather: 0.5,
craving: 0.5,
memoryFit: 1,
tasteFit: 0,
cookingAssumptionFit: 0,
};
explanations.push(buildWhy(c, ctx, parts, prov, lang));
}
}
}
}
}
}
}
expect(explanations.length).toBeGreaterThanOrEqual(100);
const hits = explanations.filter((e) => containsForbiddenCopy(e));
if (hits.length > 0) {
// eslint-disable-next-line no-console
console.error("Förbjuden copy hittades:", hits.slice(0, 5));
}
expect(hits).toHaveLength(0);
});
it("skam-exempel-korpus flaggas av förbjuden-copy-listan", () => {
const shameExamples = [
"Du har överskridit ditt kalorimål.",
"Bara 200 kcal kvar idag.",
"Begränsa dig nu.",
"Undvik kolhydrater.",
"Du borde inte äta så mycket kött.",
"Skäms över dina matvanor.",
"Din kost är dålig.",
"Du har ätit för mycket socker.",
"Måste sluta med snacks.",
"Överdriv inte.",
"Du får inte äta det där.",
];
const flagged = shameExamples.filter((s) => containsForbiddenCopy(s));
expect(flagged.length).toBeGreaterThanOrEqual(shameExamples.length - 1);
});
it("varje förbjudet mönster matchar minst ett skam-exempel", () => {
const shameExamples = [
"Du har överskridit ditt kalorimål.",
"Bara 200 kcal kvar idag.",
"Begränsa dig nu.",
"Undvik kolhydrater.",
"Du borde inte äta så mycket kött.",
"Skäms över dina matvanor.",
"Din kost är dålig.",
"Du har ätit för mycket socker.",
"Måste sluta med snacks.",
"Överdriv inte.",
"Du får inte äta det där.",
"För lite grönsaker idag.",
"För mycket fett.",
"Skärp dig.",
];
for (const pattern of FORBIDDEN_COPY_PATTERNS) {
const matches = shameExamples.filter((s) => pattern.test(s));
expect(matches.length, `mönster ${pattern.source} matchade inget exempel`).toBeGreaterThan(0);
}
});
});