414 lines
16 KiB
TypeScript
414 lines
16 KiB
TypeScript
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);
|
||
});
|
||
});
|