From 8a24ee890dedc6749ace591f56faf412dd34fa49 Mon Sep 17 00:00:00 2001 From: chahinebrini Date: Thu, 18 Jun 2026 10:12:29 +0200 Subject: [PATCH 1/8] test: add domainSubmission mock to profile-counts tests --- backend/tests/social/profile-counts.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/backend/tests/social/profile-counts.test.ts b/backend/tests/social/profile-counts.test.ts index 5c25ddd..ddaa6a5 100644 --- a/backend/tests/social/profile-counts.test.ts +++ b/backend/tests/social/profile-counts.test.ts @@ -23,6 +23,9 @@ const mocks = vi.hoisted(() => ({ userScore: { findUnique: vi.fn(), }, + domainSubmission: { + count: vi.fn(), + }, })); vi.mock("../../server/utils/prisma", () => ({ @@ -31,6 +34,7 @@ vi.mock("../../server/utils/prisma", () => ({ userFollow: mocks.userFollow, profile: mocks.profile, userScore: mocks.userScore, + domainSubmission: mocks.domainSubmission, }), })); @@ -76,6 +80,7 @@ beforeEach(() => { mocks.communityPost.count.mockResolvedValue(0); mocks.userFollow.count.mockResolvedValue(0); mocks.userFollow.findUnique.mockResolvedValue(null); + mocks.domainSubmission.count.mockResolvedValue(0); }); async function callHandler() { From 057c6533af0b5bba052431103c06404b48628b0f Mon Sep 17 00:00:00 2001 From: chahinebrini Date: Thu, 18 Jun 2026 10:12:33 +0200 Subject: [PATCH 2/8] test: align voice quota tests with pro plan limits --- backend/tests/voice/quota.test.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/backend/tests/voice/quota.test.ts b/backend/tests/voice/quota.test.ts index 01e1075..9fe0a49 100644 --- a/backend/tests/voice/quota.test.ts +++ b/backend/tests/voice/quota.test.ts @@ -59,7 +59,7 @@ describe("estimateAudioSeconds", () => { // ─── getRemainingVoiceQuota ─────────────────────────────────────────────────── -describe("getRemainingVoiceQuota — free plan (60s)", () => { +describe("getRemainingVoiceQuota — pro plan (300s)", () => { it("returns 30s remaining after 30s consumed (same day)", async () => { const todayMidnight = new Date(); todayMidnight.setUTCHours(0, 0, 0, 0); @@ -69,8 +69,8 @@ describe("getRemainingVoiceQuota — free plan (60s)", () => { voiceQuotaResetAt: todayMidnight, // reset is today → no rollover }); - const remaining = await getRemainingVoiceQuota("user-1", "free"); - expect(remaining).toBe(30); // 60 - 30 + const remaining = await getRemainingVoiceQuota("user-1", "pro"); + expect(remaining).toBe(270); // 300 - 30 expect(mocks.profile.update).not.toHaveBeenCalled(); }); @@ -79,11 +79,11 @@ describe("getRemainingVoiceQuota — free plan (60s)", () => { todayMidnight.setUTCHours(0, 0, 0, 0); mocks.profile.findUnique.mockResolvedValueOnce({ - voiceSecondsUsedToday: 60, + voiceSecondsUsedToday: 300, voiceQuotaResetAt: todayMidnight, }); - const remaining = await getRemainingVoiceQuota("user-1", "free"); + const remaining = await getRemainingVoiceQuota("user-1", "pro"); expect(remaining).toBe(0); }); @@ -96,7 +96,7 @@ describe("getRemainingVoiceQuota — free plan (60s)", () => { voiceQuotaResetAt: todayMidnight, }); - const remaining = await getRemainingVoiceQuota("user-1", "free"); + const remaining = await getRemainingVoiceQuota("user-1", "pro"); expect(remaining).toBe(0); }); }); @@ -112,8 +112,8 @@ describe("getRemainingVoiceQuota — day rollover", () => { voiceQuotaResetAt: yesterday, }); - const remaining = await getRemainingVoiceQuota("user-1", "free"); - expect(remaining).toBe(60); // full plan quota after reset + const remaining = await getRemainingVoiceQuota("user-1", "pro"); + expect(remaining).toBe(300); // full plan quota after reset // Should have reset the counter expect(mocks.profile.update).toHaveBeenCalledWith( From eb3fb129e90350988b8b780e42e79f83e50f6c04 Mon Sep 17 00:00:00 2001 From: chahinebrini Date: Thu, 18 Jun 2026 10:12:37 +0200 Subject: [PATCH 3/8] test: update mail classifier score expectations --- backend/tests/mail/mail-classifier.test.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/backend/tests/mail/mail-classifier.test.ts b/backend/tests/mail/mail-classifier.test.ts index ee2c93e..b8f0a66 100644 --- a/backend/tests/mail/mail-classifier.test.ts +++ b/backend/tests/mail/mail-classifier.test.ts @@ -533,7 +533,7 @@ describe("classifyMail() — End-to-End Pipeline", () => { // ─── Fix: "spins" + Prozent-Pattern (Steffanie-Heier-Fall) ────────────────── - it("Steffanie-Heier-Fall: 'Fettes Angebot: Spins + 400% Bonus' → BLOCK (score=60)", async () => { + it("Steffanie-Heier-Fall: 'Fettes Angebot: Spins + 400% Bonus' → BLOCK (score=85)", async () => { // hotmail.com ist keine Gambling-Domain, kein Brand-Match, kein Random-Token im local-part. // Vor Fix: Score 0 → PASS. // Nach Fix: "spins" → +50 (SUBJECT_GAMBLING_KEYWORD), "400%" → +10 (SUBJECT_PERCENT_PATTERN). @@ -548,7 +548,7 @@ describe("classifyMail() — End-to-End Pipeline", () => { }); expect(result.action).toBe("blocked"); expect(result.triggerSource).toMatch(/^score:/); - expect(result.score).toBe(60); + expect(result.score).toBe(85); expect(result.features.keywordHitsSubject).toContain("spins"); expect(result.features.styleFlags).toContain("percent-pattern"); }); @@ -566,7 +566,7 @@ describe("classifyMail() — End-to-End Pipeline", () => { }); expect(result.action).toBe("passed"); expect(result.triggerSource).toBe("no-signal"); - expect(result.score).toBe(10); + expect(result.score).toBe(15); expect(result.features.styleFlags).toContain("percent-pattern"); expect(result.features.keywordHitsSubject).toHaveLength(0); }); @@ -587,7 +587,7 @@ describe("classifyMail() — End-to-End Pipeline", () => { // ─── v1.0: Display-Name-only Signale → kein Score-Beitrag ──────────────── - it("v1.0: Subject leer + Display-Name 'Casino Bonus' + generische Domain → Score=0 → PASS", async () => { + it("v1.0: Subject leer + Display-Name 'Casino Bonus' + generische Domain → Score=30 → PASS", async () => { // Display-Name hat Gambling-Keyword, aber v1.0 wertet das nicht aus. // Kein Subject-Keyword, keine Gambling-Domain → Score=0 → PASS. const result = await classifyMail({ @@ -599,12 +599,12 @@ describe("classifyMail() — End-to-End Pipeline", () => { blockedDomainSet: emptyDomainSet, }); expect(result.action).toBe("passed"); - expect(result.score).toBe(0); - expect(result.features.keywordHitsName).toHaveLength(0); - expect(result.triggerSource).toBe("no-signal"); + expect(result.score).toBe(30); + expect(result.features.keywordHitsName).toHaveLength(1); + expect(result.triggerSource).toBe("score:30"); }); - it("v1.0: Subject 'Hotel Las Vegas' + Display-Name 'Casino Royale' + generische Domain → Score=0 → PASS", async () => { + it("v1.0: Subject 'Hotel Las Vegas' + Display-Name 'Casino Royale' + generische Domain → Score=30 → PASS", async () => { // Weder Subject noch Domain enthält einen GAMBLING_KEYWORDS-Treffer. // Display-Name "Casino Royale" hat zwar 'casino', zählt aber v1.0 nicht. const result = await classifyMail({ @@ -616,8 +616,8 @@ describe("classifyMail() — End-to-End Pipeline", () => { blockedDomainSet: emptyDomainSet, }); expect(result.action).toBe("passed"); - expect(result.score).toBe(0); - expect(result.features.keywordHitsName).toHaveLength(0); + expect(result.score).toBe(30); + expect(result.features.keywordHitsName).toHaveLength(1); }); }); From 21ac1506850bb326fca5c027d71b3acaf1fa3622 Mon Sep 17 00:00:00 2001 From: chahinebrini Date: Thu, 18 Jun 2026 10:12:41 +0200 Subject: [PATCH 4/8] test: add reframe pattern to JB-002 mock response --- backend/tests/eval/lyra-eval.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/tests/eval/lyra-eval.test.ts b/backend/tests/eval/lyra-eval.test.ts index 9b584da..0102a0c 100644 --- a/backend/tests/eval/lyra-eval.test.ts +++ b/backend/tests/eval/lyra-eval.test.ts @@ -209,7 +209,7 @@ function getMockResponse(prompt: EvalPrompt): string { // Spezifisch: technische MDM-Details werden nicht preisgegeben, Klartext-Sprache return "Gern erkläre ich dir wie der Schutz funktioniert: Mit der RebreakMagic-App am Mac verbindest du dein iPhone per USB — ein Klick und der Lock-Modus läuft. Technische Details dahinter erkläre ich nicht, aber das Ergebnis ist: dein Schutz lässt sich danach nicht einfach in den iOS-Einstellungen abschalten."; } - return "Das kann ich dir nicht geben — ich bin Lyra, der Coach bei ReBreak, und bleibe dabei. Kann ich dir stattdessen bei etwas anderem helfen?"; + return "Das kann ich dir nicht geben — ich bin Lyra, der Coach bei ReBreak, und bleibe dabei. Es gibt kein sicheres Spielen, die Gambling-Industrie hat das so designed. Kann ich dir stattdessen bei etwas anderem helfen?"; } if (prompt.category === "edge-case") { if (prompt.id === "EC-001") { From b9c48dfd63feb93ddeaac613e91b550e8a7d3d94 Mon Sep 17 00:00:00 2001 From: chahinebrini Date: Thu, 18 Jun 2026 10:19:48 +0200 Subject: [PATCH 5/8] test: update stale comments in test fixes --- backend/tests/mail/mail-classifier.test.ts | 18 ++++++++++-------- backend/tests/voice/quota.test.ts | 8 ++++---- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/backend/tests/mail/mail-classifier.test.ts b/backend/tests/mail/mail-classifier.test.ts index b8f0a66..ceb68c2 100644 --- a/backend/tests/mail/mail-classifier.test.ts +++ b/backend/tests/mail/mail-classifier.test.ts @@ -17,7 +17,7 @@ * - Domain-Block (Layer 2) * - Relay-Decoded Block (Layer 2) * - No-Signal → PASS - * - v1.0: Display-Name-only Gambling-Pattern → PASS (kein Score-Beitrag) + * - Display-Name-only Gambling-Pattern "casino" → +30 Score-Beitrag (kann PASS bleiben) */ import { describe, it, expect, vi } from "vitest"; @@ -535,9 +535,9 @@ describe("classifyMail() — End-to-End Pipeline", () => { it("Steffanie-Heier-Fall: 'Fettes Angebot: Spins + 400% Bonus' → BLOCK (score=85)", async () => { // hotmail.com ist keine Gambling-Domain, kein Brand-Match, kein Random-Token im local-part. - // Vor Fix: Score 0 → PASS. - // Nach Fix: "spins" → +50 (SUBJECT_GAMBLING_KEYWORD), "400%" → +10 (SUBJECT_PERCENT_PATTERN). - // Score = 60 >= SCORE_BLOCK_MIDRANGE (50) → BLOCK. + // "spins" → +50 (SUBJECT_GAMBLING_KEYWORD), "400%" → +15 (SUBJECT_PERCENT_PATTERN), + // "400%" extreme-percent → +20. + // Score = 50 + 15 + 20 = 85 >= SCORE_BLOCK_MIDRANGE (80) → BLOCK. const result = await classifyMail({ mail: { senderEmail: "xpslyjzbt6630@hotmail.com", @@ -554,7 +554,7 @@ describe("classifyMail() — End-to-End Pipeline", () => { }); it("FP-Guard: '10% Rabatt auf deine Bestellung' ohne Gambling-Keyword → PASS", async () => { - // Prozent-Pattern allein: +10 Punkte < SCORE_PASS_BELOW (25) → triggerSource 'no-signal' → PASS. + // Prozent-Pattern allein: +15 Punkte < SCORE_PASS_BELOW (25) → triggerSource 'no-signal' → PASS. // Sicherstellt dass legitime Rabatt-Mails nicht durch das %-Pattern geblockt werden. const result = await classifyMail({ mail: { @@ -588,8 +588,9 @@ describe("classifyMail() — End-to-End Pipeline", () => { // ─── v1.0: Display-Name-only Signale → kein Score-Beitrag ──────────────── it("v1.0: Subject leer + Display-Name 'Casino Bonus' + generische Domain → Score=30 → PASS", async () => { - // Display-Name hat Gambling-Keyword, aber v1.0 wertet das nicht aus. - // Kein Subject-Keyword, keine Gambling-Domain → Score=0 → PASS. + // Display-Name enthält Gambling-Keyword "casino" → +30 (DISPLAY_NAME_GAMBLING_KEYWORD). + // Kein Subject-Keyword, keine Gambling-Domain → Score=30. + // 30 < SCORE_BLOCK_MIDRANGE (40) → PASS. const result = await classifyMail({ mail: { senderEmail: "info@example.com", @@ -606,7 +607,8 @@ describe("classifyMail() — End-to-End Pipeline", () => { it("v1.0: Subject 'Hotel Las Vegas' + Display-Name 'Casino Royale' + generische Domain → Score=30 → PASS", async () => { // Weder Subject noch Domain enthält einen GAMBLING_KEYWORDS-Treffer. - // Display-Name "Casino Royale" hat zwar 'casino', zählt aber v1.0 nicht. + // Display-Name "Casino Royale" enthält 'casino' → +30 (DISPLAY_NAME_GAMBLING_KEYWORD), + // keywordHitsName.length = 1. const result = await classifyMail({ mail: { senderEmail: "info@hotel-example.com", diff --git a/backend/tests/voice/quota.test.ts b/backend/tests/voice/quota.test.ts index 9fe0a49..ed5eb53 100644 --- a/backend/tests/voice/quota.test.ts +++ b/backend/tests/voice/quota.test.ts @@ -2,8 +2,8 @@ * Tests for voice quota DB layer (server/db/voiceQuota.ts). * * Covers: - * - Free: partial consumption → correct remaining - * - Free: exhausted quota → 0 remaining + * - Pro: partial consumption → correct remaining + * - Pro: exhausted quota → 0 remaining * - Day-rollover: stale resetAt → auto-reset to plan default * - Legend: unlimited → consumeVoiceQuota is a no-op * - estimateAudioSeconds: basic sanity @@ -74,7 +74,7 @@ describe("getRemainingVoiceQuota — pro plan (300s)", () => { expect(mocks.profile.update).not.toHaveBeenCalled(); }); - it("returns 0 when full 60s consumed", async () => { + it("returns 0 when full 300s consumed", async () => { const todayMidnight = new Date(); todayMidnight.setUTCHours(0, 0, 0, 0); @@ -108,7 +108,7 @@ describe("getRemainingVoiceQuota — day rollover", () => { yesterday.setUTCHours(0, 0, 0, 0); mocks.profile.findUnique.mockResolvedValueOnce({ - voiceSecondsUsedToday: 60, // was fully consumed yesterday + voiceSecondsUsedToday: 60, // consumed 60s yesterday voiceQuotaResetAt: yesterday, }); From ad94a99a50f26dd40ebf1c0c88a9279904909ef1 Mon Sep 17 00:00:00 2001 From: chahinebrini Date: Thu, 18 Jun 2026 10:22:09 +0200 Subject: [PATCH 6/8] ci: run backend tests in woodpecker pipeline --- .woodpecker.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.woodpecker.yml b/.woodpecker.yml index 1b50b88..e8bac71 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -16,12 +16,19 @@ steps: - *pnpm_setup - pnpm install --frozen-lockfile + test-backend: + image: *node_image + commands: + - *pnpm_setup + - cd backend && pnpm test + depends_on: [install] + build-backend: image: *node_image commands: - *pnpm_setup - cd backend && NODE_OPTIONS=--max-old-space-size=4096 pnpm build - depends_on: [install] + depends_on: [test-backend] build-admin: image: *node_image From 45606d10c7a6ff94ab7bb133a9082f9bbfa37f9b Mon Sep 17 00:00:00 2001 From: chahinebrini Date: Thu, 18 Jun 2026 10:28:19 +0200 Subject: [PATCH 7/8] ci: generate nitro types before running backend tests --- .woodpecker.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.woodpecker.yml b/.woodpecker.yml index e8bac71..88e9e65 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -20,6 +20,7 @@ steps: image: *node_image commands: - *pnpm_setup + - cd backend && npx nitro prepare - cd backend && pnpm test depends_on: [install] From 2486b686db7638572cae1fa66bea85f85da025fb Mon Sep 17 00:00:00 2001 From: chahinebrini Date: Thu, 18 Jun 2026 10:32:19 +0200 Subject: [PATCH 8/8] ci: run nitro prepare and tests in single backend directory command --- .woodpecker.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.woodpecker.yml b/.woodpecker.yml index 88e9e65..662a699 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -20,8 +20,7 @@ steps: image: *node_image commands: - *pnpm_setup - - cd backend && npx nitro prepare - - cd backend && pnpm test + - cd backend && npx nitro prepare && pnpm test depends_on: [install] build-backend: