vignette/apps/web/e2e/supervision-research.spec.ts
Yun Chan 16e791e044 G0~G8 성과·동맹 측정 OS 작업 일괄 고정
8월 7일까지 워킹트리에만 남아 있던 미커밋 작업을 커밋한다. 여러 사본
폴더(worktree·clone)에 흩어져 있던 중간 스냅샷을 정리하기 전에 원본을
git 이력으로 고정하는 것이 목적이다.

- contracts/routes/services: measurement, outcome_trajectory, rupture_repair,
  deliberate_practice, calibration_transfer, supervision_research,
  multimodal_alliance, continuous_improvement 계열 신규 모듈과 테스트
- infra/db/init: 07~16 마이그레이션(측정 기반~calibration transfer 실행)
- apps/web: 세션 리뷰 카드·관리 화면·E2E 스펙 추가
- docs/ops: G0~G8 라이브 통합·배포·롤백 증거 문서와 evidence JSON/PNG
- scripts: smoke·ledger·릴리스 에이전트·NAS 프리뷰 운영 스크립트

engine.public 로그 .bak과 apps/web/test-results 산출물은 커밋에서 제외했다.
2026-08-08 01:30:53 +09:00

386 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.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")).toContainText("계약 상한 3번");
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("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 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 provenance 연결", { 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("provenance 저하", { exact: true })).toBeVisible();
await expect(page.getByText("매니페스트 요약만 도착했어")).toBeVisible();
await expect(page.getByText("원본 provenance 미제공").first()).toBeVisible();
await expectNoHorizontalOverflow(page);
});
});