ci: trigga på master + formatfix inför Gitea Actions
CI / Typecheck, test & build (push) Failing after 2s

This commit is contained in:
Sven (AAMOS AI)
2026-08-13 17:25:14 +07:00
parent 61d60931ad
commit c32a7e33c7
141 changed files with 40555 additions and 45210 deletions
+126 -12
View File
@@ -212,16 +212,27 @@ export const EVAL_CASES: EvalCase[] = [
input: {
scope: "user",
scopeId: "user-1",
events: [{ id: "evt-1", type: "recipe_cooked", occurredAt: "2026-08-01T12:00:00Z", payload: {} }],
events: [
{ id: "evt-1", type: "recipe_cooked", occurredAt: "2026-08-01T12:00:00Z", payload: {} },
],
existingMemoryKeys: [],
},
verify(output) {
const o = output as TaskOutput<"UPDATE_USER_MEMORY">;
const eventIds = new Set(["evt-1"]);
return [
check("alla minnen har kända sourceEventIds", o.memoryUpdates.every((u) => u.sourceEventIds.every((id) => eventIds.has(id)))),
check("inga minnen utan sourceEventIds", o.memoryUpdates.every((u) => u.sourceEventIds.length > 0)),
check("origin är observed eller ai_inferred", o.memoryUpdates.every((u) => u.origin === "observed" || u.origin === "ai_inferred")),
check(
"alla minnen har kända sourceEventIds",
o.memoryUpdates.every((u) => u.sourceEventIds.every((id) => eventIds.has(id))),
),
check(
"inga minnen utan sourceEventIds",
o.memoryUpdates.every((u) => u.sourceEventIds.length > 0),
),
check(
"origin är observed eller ai_inferred",
o.memoryUpdates.every((u) => u.origin === "observed" || u.origin === "ai_inferred"),
),
];
},
},
@@ -232,14 +243,19 @@ export const EVAL_CASES: EvalCase[] = [
input: {
scope: "user",
scopeId: "user-1",
events: [{ id: "evt-sv", type: "recipe_cooked", occurredAt: "2026-08-01T12:00:00Z", payload: {} }],
events: [
{ id: "evt-sv", type: "recipe_cooked", occurredAt: "2026-08-01T12:00:00Z", payload: {} },
],
existingMemoryKeys: [],
},
verify(output) {
const o = output as TaskOutput<"UPDATE_USER_MEMORY">;
return [
check("minst ett minne returneras", o.memoryUpdates.length >= 1),
check("summarySv är på svenska", o.memoryUpdates.every((u) => /[åäöÅÄÖ]/.test(u.summarySv) || u.summarySv.length > 0)),
check(
"summarySv är på svenska",
o.memoryUpdates.every((u) => /[åäöÅÄÖ]/.test(u.summarySv) || u.summarySv.length > 0),
),
];
},
},
@@ -253,14 +269,40 @@ export const EVAL_CASES: EvalCase[] = [
cuisine: "swedish",
tags: [],
totalTimeMinutes: 30,
nutritionPerPortion: { kcal: 550, proteinG: 45, carbsG: 50, fatG: 18, saturatedFatG: 6, fiberG: 6, sugarG: 4, saltG: 1.5 },
nutritionPerPortion: {
kcal: 550,
proteinG: 45,
carbsG: 50,
fatG: 18,
saturatedFatG: 6,
fiberG: 6,
sugarG: 4,
saltG: 1.5,
},
estimatedCostMinorPerPortion: 2200,
ratingAverage: null,
ratingCount: 0,
peakSeasons: ["summer"],
holidayTags: [],
spiceLevel: 1,
coverage: { coverage: 0.85, matches: [], missing: [], expiringUsed: [{ canonicalIngredientId: "chicken", displayNameSv: "kycklingen", required: 400, unit: "GRAM", availableInUnit: 500, covered: true, optional: false, mostUrgentDaysLeft: 2, usesExpiringItem: true }] },
coverage: {
coverage: 0.85,
matches: [],
missing: [],
expiringUsed: [
{
canonicalIngredientId: "chicken",
displayNameSv: "kycklingen",
required: 400,
unit: "GRAM",
availableInUnit: 500,
covered: true,
optional: false,
mostUrgentDaysLeft: 2,
usesExpiringItem: true,
},
],
},
daysSinceLastCooked: 14,
householdRating: 4.8,
ingredientIds: ["chicken"],
@@ -275,12 +317,84 @@ export const EVAL_CASES: EvalCase[] = [
remainingProteinG: 60,
remainingKcal: 800,
personalizationEnabled: true,
memoryItems: [{ id: "m1", userId: "u1", kind: "structured_fact", key: "favorite_cuisine_swedish", summarySv: "Gillar svensk mat", value: { favoriteCuisine: "swedish" }, origin: "user_stated", confidence: 1, verifiedByUser: true, paused: false, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }],
memoryItems: [
{
id: "m1",
userId: "u1",
kind: "structured_fact",
key: "favorite_cuisine_swedish",
summarySv: "Gillar svensk mat",
value: { favoriteCuisine: "swedish" },
origin: "user_stated",
confidence: 1,
verifiedByUser: true,
paused: false,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
],
tasteSignals: [],
cookingAssumptions: [{ canonicalIngredientId: "chicken", averageEatenPortions: 4, averageLeftoverPortions: 0.5, observationCount: 5 }],
cookingAssumptions: [
{
canonicalIngredientId: "chicken",
averageEatenPortions: 4,
averageLeftoverPortions: 0.5,
observationCount: 5,
},
],
};
const whySv = buildWhy(candidate, context, { coverage: 0.85, expiry: 0.8, nutritionFit: 0.9, taste: 0.8, rating: 0.9, season: 1, holiday: 0, time: 1, budget: 0.6, variety: 1, weather: 0.5, craving: 0.5, memoryFit: 1, tasteFit: 0, cookingAssumptionFit: 0.8 }, [{ key: "favoriteCuisine", args: { cuisine: "svensk" } }, { key: "usesStapleYouFinish", args: { ingredient: "kyckling" } }], "sv-SE");
const whyEn = buildWhy(candidate, context, { coverage: 0.85, expiry: 0.8, nutritionFit: 0.9, taste: 0.8, rating: 0.9, season: 1, holiday: 0, time: 1, budget: 0.6, variety: 1, weather: 0.5, craving: 0.5, memoryFit: 1, tasteFit: 0, cookingAssumptionFit: 0.8 }, [{ key: "favoriteCuisine", args: { cuisine: "Swedish" } }, { key: "usesStapleYouFinish", args: { ingredient: "chicken" } }], "en-US");
const whySv = buildWhy(
candidate,
context,
{
coverage: 0.85,
expiry: 0.8,
nutritionFit: 0.9,
taste: 0.8,
rating: 0.9,
season: 1,
holiday: 0,
time: 1,
budget: 0.6,
variety: 1,
weather: 0.5,
craving: 0.5,
memoryFit: 1,
tasteFit: 0,
cookingAssumptionFit: 0.8,
},
[
{ key: "favoriteCuisine", args: { cuisine: "svensk" } },
{ key: "usesStapleYouFinish", args: { ingredient: "kyckling" } },
],
"sv-SE",
);
const whyEn = buildWhy(
candidate,
context,
{
coverage: 0.85,
expiry: 0.8,
nutritionFit: 0.9,
taste: 0.8,
rating: 0.9,
season: 1,
holiday: 0,
time: 1,
budget: 0.6,
variety: 1,
weather: 0.5,
craving: 0.5,
memoryFit: 1,
tasteFit: 0,
cookingAssumptionFit: 0.8,
},
[
{ key: "favoriteCuisine", args: { cuisine: "Swedish" } },
{ key: "usesStapleYouFinish", args: { ingredient: "chicken" } },
],
"en-US",
);
return [
check("whySv innehåller ingen förbjuden copy", !containsForbiddenCopy(whySv)),
check("whyEn innehåller ingen förbjuden copy", !containsForbiddenCopy(whyEn)),
+27 -13
View File
@@ -36,14 +36,16 @@ interface ScanEvalCase {
imageUrls: string[];
locationType: "fridge" | "pantry";
marketLocale: string;
checks: (items: Array<{
detectedName: string;
brand: string | null;
estimatedQuantity: number | null;
unit: string | null;
confidence: number;
requiresConfirmation: boolean;
}>) => { name: string; passed: boolean }[];
checks: (
items: Array<{
detectedName: string;
brand: string | null;
estimatedQuantity: number | null;
unit: string | null;
confidence: number;
requiresConfirmation: boolean;
}>,
) => { name: string; passed: boolean }[];
}
const CASES: ScanEvalCase[] = [
@@ -57,7 +59,10 @@ const CASES: ScanEvalCase[] = [
return [
{ name: "hittade mjölkprodukt", passed: !!milk },
{ name: "konfidens > 0.5", passed: !!milk && milk.confidence > 0.5 },
{ name: "kräver bekräftelse om låg konfidens", passed: !!milk && (milk.confidence >= 0.92 || milk.requiresConfirmation) },
{
name: "kräver bekräftelse om låg konfidens",
passed: !!milk && (milk.confidence >= 0.92 || milk.requiresConfirmation),
},
];
},
},
@@ -71,7 +76,10 @@ const CASES: ScanEvalCase[] = [
return [
{ name: "hittade konserverad produkt", passed: !!found },
{ name: "konfidens > 0.5", passed: !!found && found.confidence > 0.5 },
{ name: "minst ett item med confidence > 0.7", passed: items.some((i) => i.confidence > 0.7) },
{
name: "minst ett item med confidence > 0.7",
passed: items.some((i) => i.confidence > 0.7),
},
];
},
},
@@ -101,7 +109,9 @@ async function main() {
apiKey,
model: process.env.GEMINI_MODEL,
timeoutMs: process.env.GEMINI_TIMEOUT_MS ? Number(process.env.GEMINI_TIMEOUT_MS) : 60_000,
dailyBudgetUsd: process.env.GEMINI_DAILY_BUDGET_USD ? Number(process.env.GEMINI_DAILY_BUDGET_USD) : 10,
dailyBudgetUsd: process.env.GEMINI_DAILY_BUDGET_USD
? Number(process.env.GEMINI_DAILY_BUDGET_USD)
: 10,
budgetStore: new SilentBudgetStore(),
});
@@ -153,8 +163,12 @@ async function main() {
if (error) console.log(` fel: ${error}`);
}
console.log(`\n[eval:scan] ${CASES.length} fall, ${totalChecks} kontroller, ${failedChecks} fallerade.`);
console.log(`[eval:scan] Total latens: ${totalLatencyMs} ms, total kostnad: ~$${totalCostUsd.toFixed(6)}`);
console.log(
`\n[eval:scan] ${CASES.length} fall, ${totalChecks} kontroller, ${failedChecks} fallerade.`,
);
console.log(
`[eval:scan] Total latens: ${totalLatencyMs} ms, total kostnad: ~$${totalCostUsd.toFixed(6)}`,
);
if (failedChecks > 0) {
console.log("[eval:scan] UNDERKÄND åtgärda innan Skiva 1 går till fälttest.");
+1 -5
View File
@@ -9,11 +9,7 @@
import { randomUUID } from "node:crypto";
import { mkdir, writeFile } from "node:fs/promises";
import path from "node:path";
import {
PutObjectCommand,
S3Client,
type S3ClientConfig,
} from "@aws-sdk/client-s3";
import { PutObjectCommand, S3Client, type S3ClientConfig } from "@aws-sdk/client-s3";
import type { AamosTaskType } from "@app/ai-contracts";
export interface CaptureConsentFlags {
+6 -2
View File
@@ -338,14 +338,18 @@ export async function processMemorySync(ctx: WorkerContext): Promise<number> {
proposal.sourceEventIds.every((id) => eventIdSet.has(id));
if (!hasEventSupport) {
// eslint-disable-next-line no-console
console.log(`UPDATE_USER_MEMORY: avvisar ${proposal.key} för ${userId}: saknar event-stöd.`);
console.log(
`UPDATE_USER_MEMORY: avvisar ${proposal.key} för ${userId}: saknar event-stöd.`,
);
continue;
}
// R1: ai_inferred ska ha låg konfidens.
if (proposal.origin === "ai_inferred" && proposal.confidence > AI_INFERRED_MAX_CONFIDENCE) {
// eslint-disable-next-line no-console
console.log(`UPDATE_USER_MEMORY: avvisar ${proposal.key} för ${userId}: ai_inferred med för hög confidence.`);
console.log(
`UPDATE_USER_MEMORY: avvisar ${proposal.key} för ${userId}: ai_inferred med för hög confidence.`,
);
continue;
}
-1
View File
@@ -334,7 +334,6 @@ function tokenize(text: string): string[] {
.filter((t) => t.length > 1);
}
function classifyScanError(error?: string | null): string {
if (!error) return "unknown";
const lower = error.toLowerCase();
+7 -2
View File
@@ -11,7 +11,10 @@ describe("pushOpsSummaryToEoc", () => {
beforeAll(async () => {
await redis.set(
"ops:summary:cache",
JSON.stringify({ app: { app: "cibello", generated_at: new Date().toISOString() }, as_of: new Date().toISOString() }),
JSON.stringify({
app: { app: "cibello", generated_at: new Date().toISOString() },
as_of: new Date().toISOString(),
}),
"EX",
60,
);
@@ -131,7 +134,9 @@ describe("pushOpsSummaryToEoc", () => {
}
expect(board.tiles.find((t: { label: string }) => t.label === "MRR")?.tone).toBe("good");
expect(board.tiles.find((t: { label: string }) => t.label === "Workers")?.tone).toBe("crit");
expect(board.tiles.find((t: { label: string }) => t.label === "Allergen-brott")?.tone).toBe("crit");
expect(board.tiles.find((t: { label: string }) => t.label === "Allergen-brott")?.tone).toBe(
"crit",
);
} finally {
globalThis.fetch = originalFetch;
}
+7 -1
View File
@@ -244,7 +244,13 @@ describe("UPDATE_USER_MEMORY hardening", () => {
const aamos = {
async runTask() {
called = true;
return { status: "ok", output: { memoryUpdates: [] }, costUsd: 0, inputTokens: 0, outputTokens: 0 } as AamosResult<"UPDATE_USER_MEMORY">;
return {
status: "ok",
output: { memoryUpdates: [] },
costUsd: 0,
inputTokens: 0,
outputTokens: 0,
} as AamosResult<"UPDATE_USER_MEMORY">;
},
async healthCheck() {
return { ok: true };
+37 -15
View File
@@ -13,10 +13,7 @@ describe.sequential("SEND_PROACTIVE_TIPS", () => {
const emailBase = "proactive-tips-test";
async function cleanup() {
const emails = [
`${emailBase}-owner@example.invalid`,
`${emailBase}-member@example.invalid`,
];
const emails = [`${emailBase}-owner@example.invalid`, `${emailBase}-member@example.invalid`];
const users = await testDb.db
.select({ id: schema.users.id })
.from(schema.users)
@@ -24,7 +21,9 @@ describe.sequential("SEND_PROACTIVE_TIPS", () => {
const userIds = users.map((u) => u.id);
for (const userId of userIds) {
await testDb.db.delete(schema.userConsents).where(eq(schema.userConsents.userId, userId));
await testDb.db.delete(schema.householdMembers).where(eq(schema.householdMembers.userId, userId));
await testDb.db
.delete(schema.householdMembers)
.where(eq(schema.householdMembers.userId, userId));
await testDb.db.delete(schema.notifications).where(eq(schema.notifications.userId, userId));
}
const households = await testDb.db
@@ -32,20 +31,36 @@ describe.sequential("SEND_PROACTIVE_TIPS", () => {
.from(schema.households)
.where(eq(schema.households.name, "Proactive Tips Test"));
for (const h of households) {
await testDb.db.delete(schema.inventoryItems).where(eq(schema.inventoryItems.householdId, h.id));
await testDb.db.delete(schema.storageLocations).where(eq(schema.storageLocations.householdId, h.id));
await testDb.db
.delete(schema.inventoryItems)
.where(eq(schema.inventoryItems.householdId, h.id));
await testDb.db
.delete(schema.storageLocations)
.where(eq(schema.storageLocations.householdId, h.id));
await testDb.db.delete(schema.households).where(eq(schema.households.id, h.id));
}
for (const email of emails) {
await testDb.db.delete(schema.users).where(eq(schema.users.email, email));
}
// Clean up test recipes/ingredients if exists.
await testDb.db.delete(schema.recipeIngredients).where(eq(schema.recipeIngredients.recipeId, "00000000-0000-0000-0000-000000000abc"));
await testDb.db.delete(schema.recipeIngredients).where(eq(schema.recipeIngredients.recipeId, "00000000-0000-0000-0000-000000000def"));
await testDb.db.delete(schema.recipes).where(eq(schema.recipes.id, "00000000-0000-0000-0000-000000000abc"));
await testDb.db.delete(schema.recipes).where(eq(schema.recipes.id, "00000000-0000-0000-0000-000000000def"));
await testDb.db.delete(schema.canonicalIngredients).where(eq(schema.canonicalIngredients.id, "test_cucumber_001"));
await testDb.db.delete(schema.canonicalIngredients).where(eq(schema.canonicalIngredients.id, "test_tomato_001"));
await testDb.db
.delete(schema.recipeIngredients)
.where(eq(schema.recipeIngredients.recipeId, "00000000-0000-0000-0000-000000000abc"));
await testDb.db
.delete(schema.recipeIngredients)
.where(eq(schema.recipeIngredients.recipeId, "00000000-0000-0000-0000-000000000def"));
await testDb.db
.delete(schema.recipes)
.where(eq(schema.recipes.id, "00000000-0000-0000-0000-000000000abc"));
await testDb.db
.delete(schema.recipes)
.where(eq(schema.recipes.id, "00000000-0000-0000-0000-000000000def"));
await testDb.db
.delete(schema.canonicalIngredients)
.where(eq(schema.canonicalIngredients.id, "test_cucumber_001"));
await testDb.db
.delete(schema.canonicalIngredients)
.where(eq(schema.canonicalIngredients.id, "test_tomato_001"));
}
async function setup() {
@@ -68,7 +83,12 @@ describe.sequential("SEND_PROACTIVE_TIPS", () => {
const [household] = await testDb.db
.insert(schema.households)
.values({ name: "Proactive Tips Test", size: 2, locale: "sv-SE", inviteCode: "PROACTIVETEST" })
.values({
name: "Proactive Tips Test",
size: 2,
locale: "sv-SE",
inviteCode: "PROACTIVETEST",
})
.returning();
await testDb.db.insert(schema.householdMembers).values([
@@ -236,7 +256,9 @@ describe.sequential("SEND_PROACTIVE_TIPS", () => {
await testDb.db
.update(schema.userConsents)
.set({ status: "revoked" })
.where(and(eq(schema.userConsents.userId, ownerId), eq(schema.userConsents.kind, "notifications")));
.where(
and(eq(schema.userConsents.userId, ownerId), eq(schema.userConsents.kind, "notifications")),
);
// Rensa notisen för att simulera ny dag.
await testDb.db.delete(schema.notifications).where(eq(schema.notifications.userId, ownerId));
+2 -1
View File
@@ -8,5 +8,6 @@ process.env.EMAIL_MODE = "log";
process.env.S3_MODE = "mock";
process.env.LOG_LEVEL = "error";
process.env.TEST_DATABASE_URL ||= "postgres://app_user:app_dev_password@localhost:5432/cibello_test";
process.env.TEST_DATABASE_URL ||=
"postgres://app_user:app_dev_password@localhost:5432/cibello_test";
process.env.DATABASE_URL = process.env.TEST_DATABASE_URL;
+8 -2
View File
@@ -9,7 +9,10 @@ describe("BUILD_TRAINING_SAMPLE banks locally", () => {
const email = "training-export-test@example.invalid";
async function cleanup() {
const existing = await testDb.db.select({ id: schema.users.id }).from(schema.users).where(eq(schema.users.email, email));
const existing = await testDb.db
.select({ id: schema.users.id })
.from(schema.users)
.where(eq(schema.users.email, email));
for (const u of existing) {
await testDb.db.delete(schema.aiCorrections).where(eq(schema.aiCorrections.userId, u.id));
await testDb.db.delete(schema.userConsents).where(eq(schema.userConsents.userId, u.id));
@@ -63,7 +66,10 @@ describe("BUILD_TRAINING_SAMPLE banks locally", () => {
taskType: "ANALYZE_FRIDGE_IMAGE",
aiOutput: { raw: { items: [] } },
proposal: { detectedName: "Mellanmjölk", confidence: 0.98 },
userCorrection: { action: "accept", corrected: { displayName: "Mellanmjölk", quantity: 1, unit: "LITER" } },
userCorrection: {
action: "accept",
corrected: { displayName: "Mellanmjölk", quantity: 1, unit: "LITER" },
},
imageS3Key: "fridge-scans/train.jpg",
modelVersion: "gemini-2.5-flash",
promptVersion: "gemini-fridge-v1",