fix(recommendation-engine): S4-FIX — opersonliga vyer fungerar utan samtycke via depersonalize(weights)

This commit is contained in:
Sven (AAMOS AI)
2026-08-11 04:02:46 +07:00
parent 05bba93b90
commit 1e18146665
4 changed files with 160 additions and 16 deletions
+4 -6
View File
@@ -11,8 +11,8 @@ import {
type PantryItem,
} from "@app/recipe-engine";
import {
depersonalize,
isEventActive,
NON_PERSONALIZED_WEIGHTS,
parseCraving,
rankAll,
seasonForDate,
@@ -389,10 +389,8 @@ export async function recommendationRoutes(app: FastifyInstance) {
}
const weights = personalizationEnabled
? q.view === "default"
? undefined
: viewWeights(q.view)
: NON_PERSONALIZED_WEIGHTS;
? viewWeights(q.view)
: depersonalize(viewWeights(q.view));
let recommendations = rankAll(scoredCandidates, ctx, weights, q.limit);
// --- 9. AAMOS-omrankning bakom feature flag (aldrig obligatorisk) ---
@@ -457,7 +455,7 @@ export async function recommendationRoutes(app: FastifyInstance) {
remainingKcal: ctx.remainingKcal,
remainingProteinG: ctx.remainingProteinG,
craving: craving ?? null,
view: personalizationEnabled ? q.view : "default",
view: q.view,
},
mealBoxSuggestions,
recommendations,
+85 -10
View File
@@ -310,7 +310,7 @@ describe("S4 recommendation views", () => {
}
});
it("ignores view and falls back to default without personalization consent", async () => {
it("honoreras vy och nollade personliga axlar utan samtycke", async () => {
// Revoke consent.
await testDb.db
.insert(schema.userConsents)
@@ -322,30 +322,105 @@ describe("S4 recommendation views", () => {
const defaultRes = await app.inject({
method: "GET",
url: "/v1/recommendations/what-to-eat?limit=1",
url: "/v1/recommendations/what-to-eat?limit=5",
headers: { authorization: `Bearer ${accessToken}` },
});
const viewRes = await app.inject({
const pantryRes = await app.inject({
method: "GET",
url: "/v1/recommendations/what-to-eat?view=taste&limit=1",
url: "/v1/recommendations/what-to-eat?view=pantry&limit=5",
headers: { authorization: `Bearer ${accessToken}` },
});
const healthRes = await app.inject({
method: "GET",
url: "/v1/recommendations/what-to-eat?view=health&limit=5",
headers: { authorization: `Bearer ${accessToken}` },
});
const tasteRes = await app.inject({
method: "GET",
url: "/v1/recommendations/what-to-eat?view=taste&limit=5",
headers: { authorization: `Bearer ${accessToken}` },
});
expect(defaultRes.statusCode).toBe(200);
expect(viewRes.statusCode).toBe(200);
expect(pantryRes.statusCode).toBe(200);
expect(healthRes.statusCode).toBe(200);
expect(tasteRes.statusCode).toBe(200);
const defaultBody = JSON.parse(defaultRes.body) as {
context: { view: string };
recommendations: Array<{ recipeId: string; parts: Record<string, number> }>;
};
const pantryBody = JSON.parse(pantryRes.body) as {
context: { view: string };
recommendations: Array<{ recipeId: string; parts: Record<string, number> }>;
};
const healthBody = JSON.parse(healthRes.body) as {
context: { view: string };
recommendations: Array<{ recipeId: string; parts: Record<string, number> }>;
};
const tasteBody = JSON.parse(tasteRes.body) as {
context: { view: string };
recommendations: Array<{ recipeId: string; parts: Record<string, number> }>;
};
// Vyn honoreras i svaret även utan samtycke.
expect(defaultBody.context.view).toBe("default");
expect(pantryBody.context.view).toBe("pantry");
expect(healthBody.context.view).toBe("health");
expect(tasteBody.context.view).toBe("taste");
// Personliga axlar är nollade i alla vyer utan samtycke.
for (const body of [defaultBody, pantryBody, healthBody, tasteBody]) {
for (const rec of body.recommendations) {
expect(rec.parts.memoryFit ?? 0).toBe(0);
expect(rec.parts.tasteFit ?? 0).toBe(0);
expect(rec.parts.cookingAssumptionFit ?? 0).toBe(0);
}
}
});
it("default-vyn är oförändrad med och utan samtycke", async () => {
// Se till att samtycke är revoked.
await testDb.db
.insert(schema.userConsents)
.values({ userId, kind: "personalization", status: "revoked" })
.onConflictDoUpdate({
target: [schema.userConsents.userId, schema.userConsents.kind],
set: { status: "revoked" },
});
const withoutConsent = await app.inject({
method: "GET",
url: "/v1/recommendations/what-to-eat?view=default&limit=5",
headers: { authorization: `Bearer ${accessToken}` },
});
await testDb.db
.insert(schema.userConsents)
.values({ userId, kind: "personalization", status: "granted" })
.onConflictDoUpdate({
target: [schema.userConsents.userId, schema.userConsents.kind],
set: { status: "granted" },
});
const withConsent = await app.inject({
method: "GET",
url: "/v1/recommendations/what-to-eat?view=default&limit=5",
headers: { authorization: `Bearer ${accessToken}` },
});
expect(withoutConsent.statusCode).toBe(200);
expect(withConsent.statusCode).toBe(200);
const a = JSON.parse(withoutConsent.body) as {
recommendations: Array<{ recipeId: string }>;
};
const viewBody = JSON.parse(viewRes.body) as {
context: { view: string };
const b = JSON.parse(withConsent.body) as {
recommendations: Array<{ recipeId: string }>;
};
expect(viewBody.context.view).toBe("default");
expect(viewBody.recommendations.map((r) => r.recipeId)).toEqual(
defaultBody.recommendations.map((r) => r.recipeId),
expect(a.recommendations.map((r) => r.recipeId)).toEqual(
b.recommendations.map((r) => r.recipeId),
);
});
});