189 lines
6.3 KiB
TypeScript
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);
|
|
}
|
|
});
|
|
});
|