evOo-local/packages/adapters/src/coach-rhythm.postgres.test.ts
Claude e1ce6f4090
test(coach): repository, streak and rhythm suites
Covers what the coach interfaces promise before anything builds on them:
goal and habit ownership and limits, one check-in per habit, goal and day
dated in the user's time zone, streaks across weekends and weeks, and a
rhythm that keeps exactly one main-bot routine per check-in and is
idempotent when set again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
2026-10-03 13:15:06 +00:00

183 lines
6.7 KiB
TypeScript

import { randomUUID } from "node:crypto";
import { routineJobKey } from "@rakazo/adapter-kit";
import type { PrismaClient } from "@rakazo/db";
import { bootstrapUserSpace, createDb, ensureMainConversation } from "@rakazo/db";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
import { COACH_ROUTINES, getCoachRhythm, setCoachRhythm } from "./coach-rhythm.js";
const databaseUrl = process.env.DATABASE_URL;
const describePostgres =
process.env.VERIFY_DATABASE && databaseUrl ? describe.sequential : describe.skip;
const signupEnv = { signupsEnabled: "true", signupAllowlist: "" };
describePostgres("coach rhythm (PostgreSQL)", () => {
let prisma: PrismaClient;
let close: () => Promise<void>;
const userIds: string[] = [];
async function newUser(timeZone: string | null = "Europe/Vienna") {
const id = `cr${randomUUID().replaceAll("-", "")}`;
userIds.push(id);
await prisma.user.create({ data: { id, name: "Ada", email: `${id}@rakazo.test` } });
await bootstrapUserSpace(prisma, { id }, signupEnv, { claimDeploymentOwner: false });
if (timeZone) await prisma.userProfile.create({ data: { userId: id, timeZone } });
return id;
}
function fakeJobs() {
return { enqueue: vi.fn(async () => undefined), cancel: vi.fn(async () => undefined) };
}
function coachRoutines(userId: string) {
return prisma.routine.findMany({
where: { userId, coachKey: { not: null } },
orderBy: { coachKey: "asc" },
});
}
beforeAll(() => {
const db = createDb(databaseUrl!);
prisma = db.prisma;
close = async () => {
await db.prisma.$disconnect();
await db.pool.end();
};
});
afterAll(async () => {
if (!prisma) return;
const memberships = await prisma.member.findMany({
where: { userId: { in: userIds } },
select: { organizationId: true },
});
await prisma.organization.deleteMany({
where: { id: { in: memberships.map((member) => member.organizationId) } },
});
await prisma.user.deleteMany({ where: { id: { in: userIds } } });
await close();
});
it("keeps one main-bot routine per check-in in the main conversation and user's time zone", async () => {
const userId = await newUser("Europe/Vienna");
const jobs = fakeJobs();
const rhythm = { morning: "07:30", evening: null, weekly: { day: 0, time: "18:00" } };
expect(await setCoachRhythm({ prisma, jobs }, { userId, rhythm })).toEqual(rhythm);
const conversation = await ensureMainConversation(prisma, userId);
const created = await coachRoutines(userId);
expect(
created.map(
({ coachKey, botId, threadId, spaceId, crons, timezone, active, notify, name }) => ({
coachKey,
botId,
threadId,
spaceId,
crons,
timezone,
active,
notify,
name,
}),
),
).toEqual([
{
coachKey: "morning",
botId: conversation.leadBotId,
threadId: conversation.threadId,
spaceId: conversation.spaceId,
crons: ["30 7 * * *"],
timezone: "Europe/Vienna",
active: true,
notify: true,
name: COACH_ROUTINES.morning.name,
},
{
coachKey: "weekly",
botId: conversation.leadBotId,
threadId: conversation.threadId,
spaceId: conversation.spaceId,
crons: ["0 18 * * 0"],
timezone: "Europe/Vienna",
active: true,
notify: true,
name: COACH_ROUTINES.weekly.name,
},
]);
for (const routine of created) {
expect(routine.nextRunAt!.getTime()).toBeGreaterThan(Date.now());
expect(jobs.enqueue).toHaveBeenCalledWith(
expect.objectContaining({
name: "routine.wakeup",
replaceKey: routineJobKey(routine.id),
availableAt: routine.nextRunAt,
}),
);
}
expect(created[0]?.prompt).toBe(COACH_ROUTINES.morning.prompt);
// The same rhythm again changes nothing.
jobs.enqueue.mockClear();
expect(await setCoachRhythm({ prisma, jobs }, { userId, rhythm })).toEqual(rhythm);
expect(jobs.enqueue).not.toHaveBeenCalled();
expect(await coachRoutines(userId)).toEqual(created);
// Moving, adding and turning off check-ins.
const next = { morning: "06:45", evening: "21:00", weekly: null };
expect(await setCoachRhythm({ prisma, jobs }, { userId, rhythm: next })).toEqual(next);
const updated = await coachRoutines(userId);
expect(updated.map((routine) => [routine.coachKey, routine.crons[0]])).toEqual([
["evening", "0 21 * * *"],
["morning", "45 6 * * *"],
]);
expect(updated.find((routine) => routine.coachKey === "morning")?.id).toBe(created[0]?.id);
expect(jobs.cancel).toHaveBeenCalledWith(routineJobKey(created[1]!.id));
expect(await getCoachRhythm(prisma, userId)).toEqual(next);
});
it("reads paused or reshaped routines as off and turns them back on", async () => {
const userId = await newUser("UTC");
const jobs = fakeJobs();
await setCoachRhythm(
{ prisma, jobs },
{ userId, rhythm: { morning: "08:00", evening: "20:00", weekly: null } },
);
const routines = await coachRoutines(userId);
await prisma.routine.update({ where: { id: routines[0]!.id }, data: { active: false } });
await prisma.routine.update({
where: { id: routines[1]!.id },
data: { crons: ["0 8 * * 1-5"] },
});
expect(await getCoachRhythm(prisma, userId)).toEqual({
morning: null,
evening: null,
weekly: null,
});
const rhythm = { morning: "08:00", evening: "20:00", weekly: null };
expect(await setCoachRhythm({ prisma, jobs }, { userId, rhythm })).toEqual(rhythm);
expect((await coachRoutines(userId)).map((routine) => routine.id)).toEqual(
routines.map((routine) => routine.id),
);
});
it("turning everything off removes the routines and never creates a main bot", async () => {
const userId = await newUser(null);
const jobs = fakeJobs();
const off = { morning: null, evening: null, weekly: null };
expect(await setCoachRhythm({ prisma, jobs }, { userId, rhythm: off })).toEqual(off);
expect(await prisma.bot.count({ where: { userId, kind: "main" } })).toBe(0);
expect(await getCoachRhythm(prisma, userId)).toEqual(off);
await setCoachRhythm(
{ prisma, jobs },
{ userId, rhythm: { morning: "09:15", evening: null, weekly: null } },
);
const [routine] = await coachRoutines(userId);
expect(routine).toMatchObject({ timezone: "UTC", crons: ["15 9 * * *"] });
expect(await setCoachRhythm({ prisma, jobs }, { userId, rhythm: off })).toEqual(off);
expect(await coachRoutines(userId)).toEqual([]);
expect(jobs.cancel).toHaveBeenCalledWith(routineJobKey(routine!.id));
});
});