import { expect, test, type APIResponse, type Page } from "@playwright/test"; import { completeOnboarding, expectNoHorizontalOverflow } from "./support"; async function expectResponseOk(response: APIResponse) { if (!response.ok()) { expect(response.ok(), await response.text()).toBeTruthy(); } } async function signIn( page: Page, role: "learner" | "admin", email: string, displayName: string, ) { const res = await page.request.post("/api/auth/dev-login", { data: { email, role, display_name: displayName }, }); await expectResponseOk(res); await completeOnboarding(page, { legal_name: displayName, affiliation: "한신대학교", department: role === "admin" ? "운영팀" : "상담심리학과", grade_level: role === "admin" ? "관리자" : "3학년", phone: "010-7777-7777", contact_address: "경기도 오산시 한신대학교", nickname: displayName, self_introduction: `${displayName} readiness E2E 사용자입니다.`, }); } test.describe("production readiness gates", () => { test("learner-facing data comes from server persistence, not browser or seed mocks", async ({ page, }, testInfo) => { const email = `readiness.${testInfo.project.name}.${testInfo.workerIndex}.${Date.now()}@hs.ac.kr`; await signIn(page, "learner", email.toLowerCase(), "Readiness Learner"); const personaResponse = await page.request.get("/api/personas"); await expectResponseOk(personaResponse); expect(personaResponse.headers()["x-vignette-catalog-source"]).toBe("database"); const personas = (await personaResponse.json()) as Array<{ source: string; degraded: boolean; }>; expect(personas.length).toBeGreaterThan(0); expect(personas.every((persona) => persona.source === "database" && !persona.degraded)).toBe( true, ); const sessionsResponse = await page.request.get("/api/sessions"); await expectResponseOk(sessionsResponse); const sessions = (await sessionsResponse.json()) as { source: string; sessions: unknown[] }; expect(sessions).toMatchObject({ source: "database", sessions: [] }); await page.goto("/learn"); await expect(page.getByText("지금까지 12회 연습했어요")).toHaveCount(0); await expect(page.getByText("최근 8회")).toHaveCount(0); await expect(page.getByRole("heading", { name: "오늘 이어갈 회기를 먼저 봅니다." })).toBeVisible(); const statusRegion = page.getByRole("region", { name: "학습 현황" }); await expect(statusRegion.getByRole("button", { name: /진행 회기 0회/ })).toBeVisible(); await expect(statusRegion.getByRole("button", { name: /리뷰 대기 0건/ })).toBeVisible(); await expect(page.getByText("최근 기록이 없습니다.")).toBeVisible(); await expect( page.locator(".lh-head").getByRole("button", { name: "학습 대상 선택" }), ).toBeInViewport(); await expectNoHorizontalOverflow(page); }); test("admin-owned runtime controls are durable database state", async ({ page }) => { await signIn(page, "admin", "readiness-admin@twentyoz.kr", "Readiness Admin"); const usersResponse = await page.request.get("/api/admin/users"); await expectResponseOk(usersResponse); const users = (await usersResponse.json()) as { source: string; durable: boolean }; expect(users).toMatchObject({ source: "database", durable: true }); const engineResponse = await page.request.get("/api/admin/engine-config"); await expectResponseOk(engineResponse); const engine = (await engineResponse.json()) as { durable: boolean; source: string; engine_mode: string; engine_url: string; model: string; }; expect(engine).toMatchObject({ durable: true, source: "database" }); expect(["claude_cli", "claude_api", "openai", "solar"]).toContain(engine.engine_mode); expect(engine.engine_url).toMatch(/^https?:\/\//); expect(engine.model.trim().length).toBeGreaterThan(0); }); });