From f112d36f4a145bbd25aa6f587125e4c112f18a4b Mon Sep 17 00:00:00 2001 From: neodk2004 Date: Sat, 3 Oct 2026 19:49:46 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20neues=20Tier=20folgt=20dem=20Kurator-Ti?= =?UTF-8?q?er=20zur=C3=BCck?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bisher folgte nur "PawFeed Team" dem neuen Tier, der Feed des neuen Nutzers startete leer. welcomeNewPet legt jetzt auch den Follow in Gegenrichtung an und befüllt den Redis-Feed per backfillOnFollow mit den letzten Team-Posts. Gleicher Admin-Schalter (autoFollowEnabled), Beschriftung im Kurator-Panel angepasst. Co-Authored-By: Claude Opus 5.5 --- src/__tests__/new-pet-welcome.test.ts | 26 ++++++++++++++++++++++++++ src/app/p/[secret]/curator/page.tsx | 4 ++-- src/lib/new-pet-welcome.ts | 15 ++++++++++++++- 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/__tests__/new-pet-welcome.test.ts b/src/__tests__/new-pet-welcome.test.ts index b8e0014..9f4509e 100644 --- a/src/__tests__/new-pet-welcome.test.ts +++ b/src/__tests__/new-pet-welcome.test.ts @@ -13,6 +13,9 @@ vi.mock("@/lib/team-pet", () => ({ ensureTeamPet: vi.fn().mockResolvedValue({ id: "pet-team" }), sendTeamMessage: (...args: unknown[]) => mockSendTeamMessage(...args), })); +vi.mock("@/lib/redis", () => ({ redis: {} })); +const mockBackfillOnFollow = vi.fn().mockResolvedValue(undefined); +vi.mock("@/lib/feed-helpers", () => ({ backfillOnFollow: (...args: unknown[]) => mockBackfillOnFollow(...args) })); const mockAssertAdmin = vi.fn(); vi.mock("@/trpc/routers/admin", () => ({ assertAdmin: (...args: unknown[]) => mockAssertAdmin(...args) })); @@ -67,6 +70,28 @@ describe("welcomeNewPet", () => { expect(mockSendTeamMessage).toHaveBeenCalledWith(expect.anything(), "pet-luna", expect.stringContaining("Hallo Luna")); }); + it("makes the new pet follow the team back and seeds its feed with the team's posts", async () => { + const prisma = createMockPrisma(); + prisma.follow.upsert.mockResolvedValue({}); + + await welcomeNewPet(asDb(prisma), { id: "pet-luna", name: "Luna" }, "de"); + + expect(prisma.follow.upsert).toHaveBeenCalledWith( + expect.objectContaining({ create: { followerPetId: "pet-luna", followeePetId: "pet-team" } }) + ); + expect(mockBackfillOnFollow).toHaveBeenCalledWith(expect.anything(), "pet-luna", "pet-team", expect.anything()); + }); + + it("still sends the welcome when following the team back fails", async () => { + const prisma = createMockPrisma(); + prisma.follow.upsert.mockResolvedValueOnce({}).mockRejectedValueOnce(new Error("db down")); + + await welcomeNewPet(asDb(prisma), { id: "pet-luna", name: "Luna" }, "de"); + + expect(mockBackfillOnFollow).not.toHaveBeenCalled(); + expect(mockSendTeamMessage).toHaveBeenCalled(); + }); + it("uses the English text for English users", async () => { const prisma = createMockPrisma(); prisma.follow.upsert.mockResolvedValue({}); @@ -86,6 +111,7 @@ describe("welcomeNewPet", () => { await welcomeNewPet(asDb(prisma), { id: "pet-kiki", name: "Kiki" }, "de"); expect(prisma.follow.upsert).not.toHaveBeenCalled(); + expect(mockBackfillOnFollow).not.toHaveBeenCalled(); expect(mockSendTeamMessage).toHaveBeenCalledWith(expect.anything(), "pet-kiki", "Servus Kiki!"); }); diff --git a/src/app/p/[secret]/curator/page.tsx b/src/app/p/[secret]/curator/page.tsx index 2a31eb4..e836e94 100644 --- a/src/app/p/[secret]/curator/page.tsx +++ b/src/app/p/[secret]/curator/page.tsx @@ -80,8 +80,8 @@ function CuratorForm({ initial }: { initial: Form }) { return (
set("autoFollowEnabled", v)} /> diff --git a/src/lib/new-pet-welcome.ts b/src/lib/new-pet-welcome.ts index dcea4ac..1090f45 100644 --- a/src/lib/new-pet-welcome.ts +++ b/src/lib/new-pet-welcome.ts @@ -2,12 +2,15 @@ import "server-only"; import type { PrismaClient } from "@prisma/client"; import { ensureTeamPet, sendTeamMessage } from "@/lib/team-pet"; import { notify } from "@/lib/notify"; +import { redis } from "@/lib/redis"; +import { backfillOnFollow } from "@/lib/feed-helpers"; /** * Was beim Anlegen eines neuen Tiers automatisch passiert (aufgerufen aus * pets.create, nie blockierend): * 1. Gründungsmitglied, wenn es eines der ersten FOUNDING_MEMBER_LIMIT Tiere ist. - * 2. Das Kurator-Tier „PawFeed Team“ folgt dem neuen Tier. + * 2. Das Kurator-Tier „PawFeed Team“ und das neue Tier folgen sich gegenseitig + * (der Feed des neuen Tiers wird mit den Team-Posts befüllt). * 3. Das Kurator-Tier schickt eine Begrüßungs-Direktnachricht. * 2 und 3 sind im Admin-Panel abschaltbar, der Text ist dort bearbeitbar * (CuratorSettings). {name} im Text wird durch den Tiernamen ersetzt. @@ -84,6 +87,16 @@ export async function welcomeNewPet( }) .then(() => notify(prisma, { recipientPetId: pet.id, type: "FOLLOW", actorPetId: teamPet.id })) .catch((err) => console.error("[welcome] curator follow failed", err)); + + // Gegenrichtung: das neue Tier folgt dem Team, damit sein Feed nicht leer startet. + await prisma.follow + .upsert({ + where: { followerPetId_followeePetId: { followerPetId: pet.id, followeePetId: teamPet.id } }, + create: { followerPetId: pet.id, followeePetId: teamPet.id }, + update: {}, + }) + .then(() => backfillOnFollow(redis, pet.id, teamPet.id, prisma)) + .catch((err) => console.error("[welcome] follow-back of curator failed", err)); } if (settings.welcomeEnabled) {