import { expect, test, type Page } from "@playwright/test"; const sessionId = "33333333-3333-4333-8333-333333333333"; const learnerText = "요즘 많이 힘들었겠어요. 어떤 마음이 가장 크게 남아 있나요?"; const clientReply = "괜찮아요. 천천히 말해볼게요."; async function routeMvpApi(page: Page) { await page.route("**/api/auth/me", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ user_id: "00000000-0000-0000-0000-00000000e2e1", email: "mvp.learner@hs.ac.kr", display_name: "MVP Learner", role: "learner", cohort_ids: [], consent_at: Math.floor(Date.now() / 1000), onboarding_completed_at: Math.floor(Date.now() / 1000), nickname: "MVP Learner", self_introduction: "MVP 회기 흐름 검증용 학습자입니다.", avatar_url: "", }), }); }); await page.route("**/api/personas", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify([ { code: "P1", display_name: "민서(청소년 우울)", difficulty: "hard", theory_target: ["humanistic"], demographics: { age_band: "10대" }, presenting_summary: "자퇴와 무기력감을 둘러싼 상담 연습", voice_preset: "soft-young-fem", source: "database", degraded: false, }, ]), }); }); await page.route("**/api/sessions", async (route) => { if (route.request().method() !== "POST") { await route.fallback(); return; } await route.fulfill({ status: 201, contentType: "application/json", body: JSON.stringify({ session_id: sessionId, case_id: "mvp-case-001", session_no: 1, stage: "라포", effective_openness: 0.21, recall_summary: null, degraded: false, }), }); }); await page.route(`**/api/sessions/${sessionId}`, async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ session_id: sessionId, case_id: "mvp-case-001", persona_code: "P1", persona_name: "민서", session_no: 1, status: "active", stage: "라포", theory_mode: "humanistic", effective_openness: 0.21, started_at: new Date().toISOString(), ended_at: null, review_ready: false, turns: [], }), }); }); await page.route(`**/api/sessions/${sessionId}/stream`, async (route) => { await route.fulfill({ status: 200, contentType: "text/event-stream", body: [ "event: token", `data: ${clientReply}`, "", "event: done", `data: ${JSON.stringify({ session_id: sessionId, stage: "탐색", effective_openness: 0.42, turn_seq: 1, safety_flagged: false, })}`, "", ].join("\n"), }); }); await page.route(`**/api/sessions/${sessionId}/end`, async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ session_id: sessionId, session_no: 1, digest_pending: true, end_state: { stage: "탐색", turn_seq: 1, effective_openness: 0.42 }, }), }); }); await page.route(`**/api/sessions/${sessionId}/review`, async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ session_id: sessionId, client: { name: "민서", initial: "민", persona: "P1 · hard", }, date: "2026-06-26", durationLabel: "1분 02초", durationSeconds: 62, reachedPhase: "탐색", sessionSignal: "종료됨", supervisorState: "평가 완료", supervisorName: "AI", summary: "평가 AI가 저장된 축어록을 분석했습니다.", phases: [{ key: "explore", label: "탐색", weight: 1 }], phaseAxis: ["0:00", "1:02"], valenceAxis: ["0:00", "1:02"], clientValence: [], counselorBaseline: [], turns: [ { id: "t1", ts: "0:01", speaker: "learner", who: "학습자", text: learnerText, techniques: [], note: null, }, { id: "t2", ts: "0:04", speaker: "client", who: "민서", text: clientReply, techniques: [], note: null, }, ], rubric: [], goodMoments: [{ title: "반영", body: "학습자가 정서를 먼저 반영했습니다." }], growthPoints: [{ title: "탐색 확장", body: "다음 턴에서 구체 상황을 더 묻습니다." }], nextLine: "그 말을 꺼내는 것도 쉽지 않았을 것 같아요.", clientFeedback: clientReply, audioUrl: null, pdfExportUrl: null, degraded: false, reviewReady: true, }), }); }); } test.describe("P1 MVP core loop", () => { test("runs login, P1 text stream, session end, and review feedback @single-run", async ({ page, }) => { const diagnostics: string[] = []; page.on("pageerror", (error) => diagnostics.push(`pageerror: ${error.message}`)); page.on("console", (message) => { if (message.type() === "error") diagnostics.push(`console: ${message.text()}`); }); await routeMvpApi(page); await page.goto("/learn/session/P1"); await expect( page.getByRole("button", { name: "회기 시작" }), diagnostics.join("\n") || (await page.locator("#root").innerText().catch(() => "")), ).toBeVisible(); await page.getByRole("button", { name: "회기 시작" }).click(); await expect(page.locator(".sx-page.sx-page--active")).toBeVisible(); await page.getByLabel("학습자 발화 입력").fill(learnerText); await page.getByRole("button", { name: "보내기" }).click(); await expect(page.locator(".sx-utt").filter({ hasText: learnerText })).toBeVisible(); await expect(page.locator(".sx-utt").filter({ hasText: clientReply })).toBeVisible(); await page.getByRole("button", { name: "회기 종료" }).click(); await page.getByRole("button", { name: "종료하고 리뷰 보기" }).click(); await expect(page).toHaveURL(new RegExp(`/learn/session/${sessionId}/review$`)); await expect(page.getByText("내담자가 남긴 것")).toBeVisible(); await expect(page.locator(".sr-feedback").getByText(clientReply)).toBeVisible(); await expect(page.getByText("평가 AI가 저장된 축어록을 분석했습니다.")).toBeVisible(); }); });