S6: i18n-paritet, provenans + eval, copy-skanning, fabricerade-minnes-eval
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user