diff --git a/src/app/api/auth/me/coach-prefs/__tests__/route.test.ts b/src/app/api/auth/me/coach-prefs/__tests__/route.test.ts index 0eec03dd1..0a5592c0f 100644 --- a/src/app/api/auth/me/coach-prefs/__tests__/route.test.ts +++ b/src/app/api/auth/me/coach-prefs/__tests__/route.test.ts @@ -64,7 +64,24 @@ beforeEach(() => { }); describe("GET /api/auth/me/coach-prefs", () => { - it("returns the prefs plus the optimistic-concurrency token", async () => { + it("returns a SAVED user's prefs plus the optimistic-concurrency token", async () => { + vi.mocked(prisma.user.findUnique).mockResolvedValue({ + coachPrefsJson: { tone: "concise" }, + updatedAt: new Date("2026-07-24T10:00:00.000Z"), + } as never); + + const res = await (GET as () => Promise)(); + expect(res.status).toBe(200); + const env = (await res.json()) as { + data: { tone: string; updatedAt?: string }; + }; + expect(env.data.tone).toBe("concise"); + expect(env.data.updatedAt).toBe("2026-07-24T10:00:00.000Z"); + }); + + it("returns the PURE default with NO token for a never-saved user", async () => { + // Mirrors the insights-layout never-saved GET: nothing to guard yet, and + // the first save is the tokenless unconditional write (backward-compat). vi.mocked(prisma.user.findUnique).mockResolvedValue({ coachPrefsJson: null, updatedAt: new Date("2026-07-24T10:00:00.000Z"), @@ -76,7 +93,7 @@ describe("GET /api/auth/me/coach-prefs", () => { data: { tone: string; updatedAt?: string }; }; expect(env.data.tone).toBe("warm"); - expect(env.data.updatedAt).toBe("2026-07-24T10:00:00.000Z"); + expect(env.data.updatedAt).toBeUndefined(); }); }); diff --git a/src/app/api/auth/me/coach-prefs/route.ts b/src/app/api/auth/me/coach-prefs/route.ts index e40ff6c13..d76c3fdb2 100644 --- a/src/app/api/auth/me/coach-prefs/route.ts +++ b/src/app/api/auth/me/coach-prefs/route.ts @@ -39,12 +39,16 @@ export const GET = apiHandler(async () => { where: { id: user.id }, select: { coachPrefsJson: true, updatedAt: true }, }); - return apiSuccess({ - ...parseCoachPrefs(row?.coachPrefsJson), - // v1.32.22 (M4) — optimistic-concurrency token (additive sibling key; the - // prefs shape is a fixed key set, so `updatedAt` cannot collide). - updatedAt: row?.updatedAt?.toISOString(), - }); + const prefs = parseCoachPrefs(row?.coachPrefsJson); + // v1.32.22 (M4) — a never-saved user gets the PURE default with NO token + // (mirrors the insights-layout never-saved GET). There is nothing to guard + // against yet, and the first save is just the tokenless unconditional write. + // A saved row carries `updatedAt` (additive sibling key; the prefs shape is a + // fixed key set, so it cannot collide) so the client can echo it. + if (row?.coachPrefsJson == null) { + return apiSuccess(prefs); + } + return apiSuccess({ ...prefs, updatedAt: row.updatedAt?.toISOString() }); }); export const PUT = apiHandler(async (req: Request) => { diff --git a/tests/integration/coach-prefs.test.ts b/tests/integration/coach-prefs.test.ts index 5da6b5531..6a1a6e3f7 100644 --- a/tests/integration/coach-prefs.test.ts +++ b/tests/integration/coach-prefs.test.ts @@ -104,23 +104,30 @@ describe("PUT /api/auth/me/coach-prefs", () => { }), ); expect(putRes.status).toBe(200); - const putEnv = (await putRes.json()) as { data: typeof expected }; - expect(putEnv.data).toEqual(expected); + // v1.32.22 (M4) — the write echoes the fresh optimistic-concurrency + // `updatedAt` token alongside the canonical prefs. + const putEnv = (await putRes.json()) as { + data: typeof expected & { updatedAt?: string }; + }; + expect(putEnv.data).toEqual({ ...expected, updatedAt: expect.any(String) }); - // DB row reflects the canonical defaulted form. + // DB row reflects the canonical defaulted form. `coachPrefsJson` must NOT + // carry `updatedAt` — that token is the separate `User.updatedAt` column. const row = await prisma.user.findUnique({ where: { id: user.id }, select: { coachPrefsJson: true }, }); expect(row?.coachPrefsJson).toEqual(expected); - // GET reads the saved values back. + // GET reads the saved values back, now carrying the token (saved row). const getRes = await (GET as (r: Request) => Promise)( new Request("http://localhost/api/auth/me/coach-prefs"), ); expect(getRes.status).toBe(200); - const getEnv = (await getRes.json()) as { data: typeof expected }; - expect(getEnv.data).toEqual(expected); + const getEnv = (await getRes.json()) as { + data: typeof expected & { updatedAt?: string }; + }; + expect(getEnv.data).toEqual({ ...expected, updatedAt: expect.any(String) }); }); it("rejects unknown tone values with 422", async () => {