import { expect, test, type APIResponse } from "@playwright/test"; import { useRealApi, withGlobalEngineConfigLock } from "./support"; interface HealthResponse { db: boolean; } interface UserProfileResponse { user_id: string; email: string; display_name: string; role: string; cohort_ids: string[]; affiliation: string; } interface UserPreferencesResponse { theme: string; voice_preset_id: string; voice_rate: number; notifications: { session_done: boolean; safety_signal: boolean; learner_progress: boolean; product_news: boolean; }; } interface AdminUsersResponse { source: "database" | "server_session_registry"; durable: boolean; users: UserProfileResponse[]; } interface AdminEngineConfigResponse { engine_mode: string; engine_url: string; model: string; reasoning_effort: string | null; updated_by: string | null; updated_at: number | null; } async function expectResponseOk(response: APIResponse) { expect(response.ok(), await response.text()).toBeTruthy(); } function slugFor(testInfo: { project: { name: string }; workerIndex: number; retry: number }) { const project = testInfo.project.name.includes("mobile") ? "mob" : "desk"; return `${project}.${testInfo.workerIndex}.${testInfo.retry}.${Date.now()}`; } test.describe("database-backed runtime state", () => { test.beforeEach(async ({ page }) => { await useRealApi(page); }); test("persists user settings and admin AI configuration through DB-backed APIs @single-run", async ({ page, }, testInfo) => { const healthResponse = await page.request.get("/api/health"); await expectResponseOk(healthResponse); const health = (await healthResponse.json()) as HealthResponse; test.skip(!health.db, "PostgreSQL is not connected for this run"); const slug = slugFor(testInfo); const learnerEmail = `db-persist.${slug}@hs.ac.kr`.toLowerCase(); const learnerName = `DB Persist Learner ${testInfo.project.name}`; const nextName = `DB Persist Updated ${testInfo.project.name}`; const affiliation = "Hanshin University"; const learnerLogin = await page.request.post("/api/auth/dev-login", { data: { email: learnerEmail, role: "learner", display_name: learnerName, }, }); await expectResponseOk(learnerLogin); const profilePatch = await page.request.patch("/api/users/me", { data: { display_name: nextName, affiliation, }, }); await expectResponseOk(profilePatch); const profile = (await profilePatch.json()) as UserProfileResponse; expect(profile).toMatchObject({ email: learnerEmail, display_name: nextName, affiliation, }); const preferencesPatch = await page.request.patch("/api/users/me/preferences", { data: { theme: "dark", voice_rate: 1.1, notifications: { session_done: true, safety_signal: true, learner_progress: true, product_news: true, }, }, }); await expectResponseOk(preferencesPatch); const preferences = (await preferencesPatch.json()) as UserPreferencesResponse; expect(preferences.theme).toBe("dark"); expect(preferences.voice_rate).toBeCloseTo(1.1); expect(preferences.notifications.product_news).toBe(true); const adminEmail = `db-persist-admin.${slug}@twentyoz.kr`.toLowerCase(); const adminLogin = await page.request.post("/api/auth/dev-login", { data: { email: adminEmail, role: "admin", display_name: `DB Persist Admin ${testInfo.project.name}`, }, }); await expectResponseOk(adminLogin); const usersResponse = await page.request.get("/api/admin/users"); await expectResponseOk(usersResponse); const users = (await usersResponse.json()) as AdminUsersResponse; expect(users.source).toBe("database"); expect(users.durable).toBe(true); const learner = users.users.find((user) => user.email === learnerEmail); expect(learner, `Expected ${learnerEmail} in DB-backed admin list`).toBeTruthy(); expect(learner).toMatchObject({ display_name: nextName, affiliation, }); await withGlobalEngineConfigLock(`db-persistence-${slug}`, async () => { const engineResponse = await page.request.get("/api/admin/engine-config"); await expectResponseOk(engineResponse); const currentEngine = (await engineResponse.json()) as AdminEngineConfigResponse; const capabilityResponse = await page.request.get( `/api/admin/engine-capabilities?engine_mode=${encodeURIComponent(currentEngine.engine_mode)}`, ); await expectResponseOk(capabilityResponse); const capability = (await capabilityResponse.json()) as { models: Array<{ id: string; default_reasoning_effort?: string | null }>; }; const candidate = capability.models.find((model) => model.id !== currentEngine.model) ?? capability.models[0]; if (!candidate) throw new Error("engine capability returned no selectable model"); const nextModel = candidate.id; const nextEffort = candidate.default_reasoning_effort ?? null; try { const enginePatch = await page.request.patch("/api/admin/engine-config", { data: { engine_mode: currentEngine.engine_mode, engine_url: currentEngine.engine_url, model: nextModel, reasoning_effort: nextEffort, }, }); await expectResponseOk(enginePatch); const updatedEngine = (await enginePatch.json()) as AdminEngineConfigResponse; expect(updatedEngine).toMatchObject({ engine_mode: currentEngine.engine_mode, engine_url: currentEngine.engine_url, model: nextModel, reasoning_effort: nextEffort, updated_by: adminEmail, }); expect(updatedEngine.updated_at).toBeGreaterThan(0); const persistedEngineResponse = await page.request.get("/api/admin/engine-config"); await expectResponseOk(persistedEngineResponse); const persistedEngine = (await persistedEngineResponse.json()) as AdminEngineConfigResponse; expect(persistedEngine).toMatchObject({ model: nextModel, updated_by: adminEmail, }); } finally { const restoreResponse = await page.request.patch("/api/admin/engine-config", { data: { engine_mode: currentEngine.engine_mode, engine_url: currentEngine.engine_url, model: currentEngine.model, reasoning_effort: currentEngine.reasoning_effort, }, }); await expectResponseOk(restoreResponse); } }); }); });