import { expect, test, type Page } from "@playwright/test"; import type { ResearchViewResponse, SupervisionViewResponse, } from "../src/pages/supervisionResearchApi"; import { expectNoHorizontalOverflow } from "./support"; const FORBIDDEN_VERBATIM = "내담자가 실제로 말한 비공개 원문"; function routeUnmockedApi(page: Page) { return page.route("**/api/**", (route) => route.fulfill({ status: 404, contentType: "application/json", body: JSON.stringify({ detail: "not part of the focused G6 fixture" }), }), ); } function routeAuth(page: Page, role: "teacher" | "admin") { return page.route("**/auth/me", (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ user_id: role === "teacher" ? "62000000-0000-0000-0000-000000000002" : "62000000-0000-0000-0000-000000000003", email: `${role}@hs.ac.kr`, role, display_name: role === "teacher" ? "E2E Teacher" : "E2E Research Admin", admin_access: role === "admin", super_admin: false, account_status: "approved", approval_required: false, cohort_ids: ["g6-cohort"], consent_at: 1782820000, onboarding_completed_at: 1782820001, nickname: role === "teacher" ? "E2E Teacher" : "E2E Research Admin", self_introduction: "", avatar_url: "", }), }), ); } function supervisionFixture(): SupervisionViewResponse { return { attention_items: [ { item_id: "g6-attention-risk", snapshot_id: "g6-snapshot-001", learner_id: "62000000-0000-0000-0000-000000000101", learner_ref: "learner-risk", cohort_id: "g6-cohort", queue_position: 1, primary_signal: "deterioration", oldest_active_sequence: 4, drilldown_routes: [ "/teach/analysis?learner=learner-risk&tab=outcome", "/teach/analysis?learner=learner-risk&tab=safety", "/teach/session/g6-session-risk/review", ], evidence_pointer_ids: [ "pointer-outcome-risk", "pointer-safety-risk", "pointer-alliance-risk", "pointer-hidden-by-cap", ], created_at: "2026-08-06T03:00:00Z", }, { item_id: "g6-attention-stagnation", snapshot_id: "g6-snapshot-001", learner_id: "62000000-0000-0000-0000-000000000102", learner_ref: "learner-stagnation", cohort_id: "g6-cohort", queue_position: 2, primary_signal: "growth_stagnation", oldest_active_sequence: 7, drilldown_routes: ["/teach/analysis?learner=learner-stagnation&tab=practice"], evidence_pointer_ids: ["pointer-practice-stagnation"], created_at: "2026-08-06T03:00:00Z", }, { item_id: "g6-attention-rupture", snapshot_id: "g6-snapshot-001", learner_id: "62000000-0000-0000-0000-000000000103", learner_ref: "learner-rupture", cohort_id: "g6-cohort", queue_position: 3, primary_signal: "unresolved_rupture", oldest_active_sequence: 9, drilldown_routes: ["/teach/analysis?learner=learner-rupture&tab=rupture"], evidence_pointer_ids: ["pointer-rupture-open"], created_at: "2026-08-06T03:00:00Z", }, ], curriculum_gaps: [ { gap_snapshot_id: "g6-gap-001", cohort_id: "g6-cohort", competency_id: "competency.rupture-repair", gap_kind: "rupture_repair", status: "observed", uncertainty: 0.18, affected_learner_count: 4, evidence_pointer_ids: ["pointer-gap-1", "pointer-gap-2"], created_at: "2026-08-06T03:02:00Z", }, { gap_snapshot_id: "g6-gap-002", cohort_id: "g6-cohort", competency_id: "competency.transfer-context", gap_kind: "transfer", status: "insufficient_evidence", uncertainty: 1, affected_learner_count: 0, evidence_pointer_ids: [], created_at: "2026-08-06T03:02:00Z", }, ], clinical_claim_allowed: false, }; } function researchFixture(): ResearchViewResponse { return { calibration_dataset: [ { dataset_row_id: "g6-dataset-row-001", row_hash: "a".repeat(64), disagreement_record_id: "g6-disagreement-001", case_ref: "synthetic-case-001", competency_id: "competency.rupture-repair", ai_label: "resolved", teacher_label: "partial", ai_model: "evaluator-v2", prompt_version: "2.3.0", instrument_id: "rupture-repair-evaluator", instrument_version: "1.4.0", correction_reason_code: "repair_impact_not_confirmed", evidence_pointer_ids: ["pointer-ai-001", "pointer-teacher-001"], raw_transcript_included: false, created_at: "2026-08-06T03:04:00Z", }, ], drift_reports: [ { drift_report_id: "g6-drift-001", cohort_id: "g6-cohort", matched_count: 12, status: "drift_flagged", baseline_accuracy: 0.83, candidate_accuracy: 0.67, accuracy_delta: -0.16, disagreement_case_refs: ["synthetic-case-b2", "synthetic-case-b3"], alerts: [ "overall_accuracy_regression", "synthetic_subgroup_regression:synthetic-low-disclosure", ], evidence_pointer_ids: ["pointer-drift-baseline", "pointer-drift-candidate"], created_at: "2026-08-06T03:06:00Z", baseline_model: "evaluator-v1", candidate_model: "evaluator-v2", baseline_prompt_version: "1.8.0", candidate_prompt_version: "2.3.0", instrument_id: "alliance-evaluation-suite", baseline_instrument_version: "1.1.0", candidate_instrument_version: "1.4.0", subgroup_metrics: [ { subgroup: "synthetic-low-disclosure", matched_count: 4, baseline_accuracy: 0.75, candidate_accuracy: 0.5, accuracy_delta: -0.25, }, { subgroup: "synthetic-high-resistance", matched_count: 4, baseline_accuracy: 0.75, candidate_accuracy: 0.75, accuracy_delta: 0, }, ], }, ], phase3_manifests: [ { manifest_id: "g6-manifest-001", cohort_id: "g6-cohort", schema_version: "vignette.phase3-outcome-evidence-manifest.v1", artifact_count: 4, created_at: "2026-08-06T03:08:00Z", artifacts: [ { domain: "alliance", artifact_id: "artifact-alliance-v1", schema_version: "alliance.v1", content_sha256: "1".repeat(64), record_count: 24, provenance_uri: "db://measurement/alliance", clinical_claim_allowed: false, }, { domain: "rupture", artifact_id: "artifact-rupture-v1", schema_version: "rupture.v1", content_sha256: "2".repeat(64), record_count: 18, provenance_uri: "db://measurement/rupture", clinical_claim_allowed: false, }, { domain: "transfer", artifact_id: "artifact-transfer-v1", schema_version: "transfer.v1", content_sha256: "3".repeat(64), record_count: 16, provenance_uri: "audit://transfer/suite-v1", clinical_claim_allowed: false, }, { domain: "calibration", artifact_id: "artifact-calibration-v1", schema_version: "calibration.v1", content_sha256: "4".repeat(64), record_count: 20, provenance_uri: "repo://evidence/calibration-v1", clinical_claim_allowed: false, }, ], }, ], raw_transcript_included: false, clinical_claim_allowed: false, }; } async function routeG6( page: Page, role: "teacher" | "admin", supervision = supervisionFixture(), research = researchFixture(), ) { await routeUnmockedApi(page); await routeAuth(page, role); await page.route("**/api/supervision-research/supervision-view", (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(supervision), }), ); await page.route("**/api/supervision-research/research-view", (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(research), }), ); } test.describe("G6 supervision and research OS", () => { test("teacher follows a three-click-bounded evidence queue without research controls", async ({ page, }) => { await routeG6(page, "teacher"); await page.goto("/teach/supervision"); await page.evaluate(() => document.documentElement.setAttribute("data-theme", "dark")); await expect( page.getByRole("heading", { name: "감독·연구", }), ).toBeVisible(); await expect(page.getByText("학습 검토", { exact: true })).toBeVisible(); await expect(page.locator(".g6-hero p")).toHaveText( "검토가 필요한 학습 회기와 평가 근거를 확인합니다.", ); await expect(page.getByRole("tab", { name: "교수 감독" })).toHaveAttribute( "aria-selected", "true", ); await expect(page.getByRole("tab", { name: "연구 품질" })).toHaveCount(0); await expect(page.getByText("위험", { exact: true })).toBeVisible(); await expect(page.getByText("정체", { exact: true })).toBeVisible(); await expect(page.getByText("미해결 관계 사건", { exact: true }).first()).toBeVisible(); const stagnation = page.getByRole("button", { name: /learner-stagnation/ }); await stagnation.focus(); await page.keyboard.press("Enter"); await expect(stagnation).toHaveAttribute("aria-pressed", "true"); await page.getByRole("button", { name: /learner-risk/ }).click(); await expect(page.locator(".g6-ledger li")).toHaveCount(3); await expect(page.locator(".g6-drilldown a")).toHaveCount(3); await expect(page.getByTestId("drilldown-depth")).toHaveText("선택한 항목의 근거를 확인합니다."); await expect(page.getByRole("heading", { name: "반복되는 학습 보완점" })).toBeVisible(); await expect(page.getByText("competency.rupture-repair").first()).toBeVisible(); await expect(page.getByText("근거가 부족한 상태를 유지하고 해석하지 않습니다.")).toBeVisible(); await expect(page.getByText("resolved", { exact: true })).toBeVisible(); await expect(page.getByText("partial", { exact: true })).toBeVisible(); await expect(page.getByText("원문 축어록 제외")).toBeVisible(); await expect(page.getByText(/XP|총점/i)).toHaveCount(0); await expect(page.getByRole("button", { name: /확정|승인|판정 저장/ })).toHaveCount(0); await expect(page.getByText(FORBIDDEN_VERBATIM)).toHaveCount(0); await expectNoHorizontalOverflow(page); }); test("wraps a long learner reference without clipping at 320px", async ({ page }) => { const longLearnerRef = "learner-bc2e3fe5b35e46a8b1c9d2e4f6a7b8c9d0e1f2a3b4c5d6e7f8"; const supervision = supervisionFixture(); supervision.attention_items[0] = { ...supervision.attention_items[0], learner_ref: longLearnerRef, }; await routeG6(page, "teacher", supervision); await page.setViewportSize({ width: 320, height: 900 }); await page.goto("/teach/supervision"); const detailReference = page.locator(".g6-case h3"); await expect(detailReference).toHaveText(longLearnerRef); await expect(detailReference).toHaveCSS("overflow-wrap", "anywhere"); expect( await detailReference.evaluate((element) => element.scrollWidth <= element.clientWidth + 1), ).toBeTruthy(); await expectNoHorizontalOverflow(page); }); test("research role traces model, prompt, instrument, subgroup, and four-domain provenance", async ({ page, }) => { await page.emulateMedia({ reducedMotion: "reduce" }); await routeG6(page, "admin"); await page.goto("/teach/supervision"); await expect(page.locator(".g6-hero p")).toHaveText( "검토가 필요한 학습 회기와 평가 근거를 확인합니다. 연구 품질 탭에서는 평가 버전과 출처를 확인할 수 있습니다.", ); await page.getByRole("tab", { name: "연구 품질" }).click(); await expect(page.getByRole("heading", { name: "모델·프롬프트·도구·하위집단 드리프트" })).toBeVisible(); await expect(page.getByText("evaluator-v1", { exact: true })).toBeVisible(); await expect( page.getByLabel("모델 프롬프트 도구 버전 출처").getByText("evaluator-v2", { exact: true }), ).toBeVisible(); await expect(page.getByText("prompt 1.8.0", { exact: true })).toBeVisible(); await expect(page.getByText("prompt 2.3.0", { exact: true })).toBeVisible(); await expect(page.getByText("alliance-evaluation-suite", { exact: true })).toBeVisible(); await expect(page.getByText("synthetic-low-disclosure", { exact: true })).toBeVisible(); await expect(page.getByText("-25%p", { exact: true })).toBeVisible(); await expect(page.getByText("4/4 출처 연결", { exact: true })).toBeVisible(); for (const label of ["동맹", "파열·수선", "전이", "보정"]) { await expect(page.getByText(label, { exact: true }).first()).toBeVisible(); } await expect(page.getByText("db://measurement/alliance", { exact: true })).toBeVisible(); await expect(page.getByText("audit://transfer/suite-v1", { exact: true })).toBeVisible(); await expect(page.getByText("repo://evidence/calibration-v1", { exact: true })).toBeVisible(); await expect(page.getByText(FORBIDDEN_VERBATIM)).toHaveCount(0); await expect(page.getByText(/XP|총점/i)).toHaveCount(0); const activeTab = page.getByRole("tab", { name: "연구 품질" }); await expect(activeTab).toHaveAttribute("tabindex", "0"); expect( await page.evaluate(() => window.matchMedia("(prefers-reduced-motion: reduce)").matches), ).toBe(true); const reducedTransitionSeconds = await activeTab.evaluate((element) => Number.parseFloat(window.getComputedStyle(element).transitionDuration), ); expect(reducedTransitionSeconds).toBeLessThanOrEqual(0.00001); await expectNoHorizontalOverflow(page); }); test("empty and summary-only contracts stay explicit instead of inventing evidence", async ({ page, }) => { const emptySupervision: SupervisionViewResponse = { attention_items: [], curriculum_gaps: [], clinical_claim_allowed: false, }; const summaryOnlyResearch: ResearchViewResponse = { calibration_dataset: [], drift_reports: [], phase3_manifests: [ { manifest_id: "g6-manifest-summary-only", cohort_id: "g6-cohort", schema_version: "vignette.phase3-outcome-evidence-manifest.v1", artifact_count: 4, created_at: "2026-08-06T03:08:00Z", }, ], raw_transcript_included: false, clinical_claim_allowed: false, }; await routeG6(page, "admin", emptySupervision, summaryOnlyResearch); await page.goto("/teach/supervision"); await expect(page.getByText("현재 우선 검토 항목이 없습니다")).toBeVisible(); await expect(page.getByText("현재 교육과정 공백이 없습니다")).toBeVisible(); await expect(page.getByText("교수자–AI 불일치 메타데이터가 없습니다")).toBeVisible(); await page.getByRole("tab", { name: "연구 품질" }).click(); await expect(page.getByText("버전 드리프트 비교가 없습니다")).toBeVisible(); await expect(page.getByText("출처 연결 저하", { exact: true })).toBeVisible(); await expect(page.getByText("매니페스트 요약만 있습니다")).toBeVisible(); await expect(page.getByText("원본 출처 미제공").first()).toBeVisible(); await expectNoHorizontalOverflow(page); }); });