fix(recommendation-engine): S4-FIX — opersonliga vyer fungerar utan samtycke via depersonalize(weights)
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user