세션 평가와 교수자 분석 보강
This commit is contained in:
parent
5c4ac04e06
commit
fe2796f05a
51 changed files with 4928 additions and 240 deletions
|
|
@ -1,10 +1,15 @@
|
|||
import { expect, test, type Page, type Response } from "@playwright/test";
|
||||
import {
|
||||
FILLED_REVIEW_SESSION_ID,
|
||||
filledReviewResponse,
|
||||
routeFilledSessionReview,
|
||||
routePrepostMeasures,
|
||||
} from "./session-review-fixture";
|
||||
import { expectNoHorizontalOverflow, fetchAvailablePersona, signInAsLearner } from "./support";
|
||||
import {
|
||||
expectNoHorizontalOverflow,
|
||||
fetchAvailablePersona,
|
||||
signInAsLearner,
|
||||
} from "./support";
|
||||
|
||||
interface SessionStartResponse {
|
||||
session_id: string;
|
||||
|
|
@ -16,6 +21,31 @@ async function expectResponseOk(response: { ok: () => boolean; text: () => Promi
|
|||
}
|
||||
}
|
||||
|
||||
async function routeSupervisorCapableLearner(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-000000000303",
|
||||
email: "yunchan@twentyoz.kr",
|
||||
role: "learner",
|
||||
display_name: "Yun Chan",
|
||||
admin_access: true,
|
||||
super_admin: true,
|
||||
account_status: "approved",
|
||||
approval_required: false,
|
||||
cohort_ids: [],
|
||||
consent_at: 1782820000,
|
||||
onboarding_completed_at: 1782820001,
|
||||
nickname: "Yun Chan",
|
||||
self_introduction: "",
|
||||
avatar_url: "",
|
||||
}),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function isReviewResponse(sessionId: string) {
|
||||
return (response: Response) => {
|
||||
const url = new URL(response.url());
|
||||
|
|
@ -102,6 +132,243 @@ async function expectReviewRegionsReachable(page: Page) {
|
|||
}
|
||||
|
||||
test.describe("session review", () => {
|
||||
test("redirects supervisor-capable users from learner 403 into the teacher review route", async ({
|
||||
page,
|
||||
}) => {
|
||||
await routeSupervisorCapableLearner(page);
|
||||
await routePrepostMeasures(page);
|
||||
|
||||
const sessionId = "supervisor-redirect-review";
|
||||
let reviewAttempts = 0;
|
||||
await page.route(`**/api/sessions/${sessionId}/review`, async (route) => {
|
||||
reviewAttempts += 1;
|
||||
const referer = route.request().headers().referer ?? page.url();
|
||||
if (!referer.includes(`/teach/session/${sessionId}/review`)) {
|
||||
await route.fulfill({
|
||||
status: 403,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ detail: "session does not belong to user" }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(filledReviewResponse(sessionId)),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto(`/learn/session/${sessionId}/review`);
|
||||
|
||||
await expect(page).toHaveURL(new RegExp(`/teach/session/${sessionId}/review$`));
|
||||
await expect(page.getByText("교수자 검토", { exact: true })).toBeVisible();
|
||||
expect(reviewAttempts).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
test("normalizes a successful supervisor review response away from the learner route", async ({
|
||||
page,
|
||||
}) => {
|
||||
await routeSupervisorCapableLearner(page);
|
||||
|
||||
const sessionId = "supervisor-success-review";
|
||||
let reviewAttempts = 0;
|
||||
await page.route(`**/api/sessions/${sessionId}/review`, async (route) => {
|
||||
reviewAttempts += 1;
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
...filledReviewResponse(sessionId),
|
||||
teacherReview: {
|
||||
status: "pending",
|
||||
note: "",
|
||||
reviewedAt: null,
|
||||
reviewerId: null,
|
||||
updatedAt: null,
|
||||
worksheetStatus: "pending",
|
||||
worksheetNote: "",
|
||||
worksheetReviewedAt: null,
|
||||
},
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto(`/learn/session/${sessionId}/review`);
|
||||
|
||||
await expect(page).toHaveURL(new RegExp(`/teach/session/${sessionId}/review$`));
|
||||
await expect(page.getByText("교수자 검토", { exact: true })).toBeVisible();
|
||||
await expect(page.locator(".sr-card--prepost")).toHaveCount(0);
|
||||
expect(reviewAttempts).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
test("refreshes an ended review from evaluation pending to ready", async ({ page }) => {
|
||||
await routeSupervisorCapableLearner(page);
|
||||
await routePrepostMeasures(page);
|
||||
|
||||
const sessionId = "review-pending-then-ready";
|
||||
let reviewAttempts = 0;
|
||||
let firstResponseWasPending = false;
|
||||
await page.route(`**/api/sessions/${sessionId}/review`, async (route) => {
|
||||
reviewAttempts += 1;
|
||||
const ready = {
|
||||
...filledReviewResponse(sessionId),
|
||||
summary: "평가 AI가 저장된 축어록을 분석했습니다.",
|
||||
};
|
||||
const pending = {
|
||||
...ready,
|
||||
supervisorState: "평가 대기",
|
||||
summary: "평가 AI가 저장된 축어록을 분석 중입니다.",
|
||||
rubric: [],
|
||||
goodMoments: [],
|
||||
growthPoints: [],
|
||||
degraded: true,
|
||||
reviewReady: false,
|
||||
};
|
||||
if (reviewAttempts === 1) firstResponseWasPending = true;
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(reviewAttempts === 1 ? pending : ready),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto(`/learn/session/${sessionId}/review`);
|
||||
|
||||
await expect(page.getByText("평가 AI가 저장된 축어록을 분석했습니다.")).toBeVisible({
|
||||
timeout: 5000,
|
||||
});
|
||||
await expect(page.getByText("평가 완료")).toBeVisible();
|
||||
expect(firstResponseWasPending).toBe(true);
|
||||
expect(reviewAttempts).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
test("lets supervisor retry a failed AI session evaluation", async ({ page }) => {
|
||||
await routeSupervisorCapableLearner(page);
|
||||
await routePrepostMeasures(page);
|
||||
|
||||
const sessionId = "review-failed-retry";
|
||||
let reevaluateRequests = 0;
|
||||
let retrySucceeded = false;
|
||||
await page.route(`**/api/sessions/${sessionId}/review`, async (route) => {
|
||||
const ready = filledReviewResponse(sessionId);
|
||||
const failed = {
|
||||
...ready,
|
||||
supervisorState: "평가 실패",
|
||||
summary:
|
||||
"저장된 축어록은 확인했지만 평가 AI 산출물이 아직 준비되지 않았습니다. 사유: engine_error: evaluator timeout",
|
||||
rubric: [],
|
||||
goodMoments: [],
|
||||
growthPoints: [],
|
||||
degraded: true,
|
||||
reviewReady: false,
|
||||
teacherReview: {
|
||||
status: "pending",
|
||||
note: "",
|
||||
reviewedAt: null,
|
||||
reviewerId: null,
|
||||
updatedAt: null,
|
||||
worksheetStatus: "pending",
|
||||
worksheetNote: "",
|
||||
worksheetReviewedAt: null,
|
||||
},
|
||||
};
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(retrySucceeded ? ready : failed),
|
||||
});
|
||||
});
|
||||
await page.route(`**/api/eval/sessions/${sessionId}/reevaluate`, async (route) => {
|
||||
reevaluateRequests += 1;
|
||||
retrySucceeded = true;
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
loop: "deep",
|
||||
session_id: sessionId,
|
||||
stage: "정리",
|
||||
scope: "session_end",
|
||||
turns_evaluated: 2,
|
||||
distribution: {},
|
||||
theory_mode: "humanistic",
|
||||
strengths: [],
|
||||
improvements: [],
|
||||
intent_deviations: [],
|
||||
error: null,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto(`/teach/session/${sessionId}/review`);
|
||||
await expect(page.getByText("평가 실패")).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "AI 평가 재시도" })).toBeVisible();
|
||||
|
||||
await page.getByRole("button", { name: "AI 평가 재시도" }).click();
|
||||
|
||||
await expect(page.getByText("평가 완료")).toBeVisible();
|
||||
await expect(page.getByText("평가 실패")).toHaveCount(0);
|
||||
expect(reevaluateRequests).toBe(1);
|
||||
});
|
||||
|
||||
test("keeps the failed review visible when AI evaluation retry fails", async ({ page }) => {
|
||||
await routeSupervisorCapableLearner(page);
|
||||
await routePrepostMeasures(page);
|
||||
|
||||
const sessionId = "review-failed-retry-error";
|
||||
let reviewRequests = 0;
|
||||
let reevaluateRequests = 0;
|
||||
await page.route(`**/api/sessions/${sessionId}/review`, async (route) => {
|
||||
reviewRequests += 1;
|
||||
const ready = filledReviewResponse(sessionId);
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
...ready,
|
||||
supervisorState: "평가 실패",
|
||||
summary:
|
||||
"저장된 축어록은 확인했지만 평가 AI 산출물이 아직 준비되지 않았습니다. 사유: session evaluation timeout after 45s",
|
||||
rubric: [],
|
||||
goodMoments: [],
|
||||
growthPoints: [],
|
||||
degraded: true,
|
||||
reviewReady: false,
|
||||
teacherReview: {
|
||||
status: "pending",
|
||||
note: "",
|
||||
reviewedAt: null,
|
||||
reviewerId: null,
|
||||
updatedAt: null,
|
||||
worksheetStatus: "pending",
|
||||
worksheetNote: "",
|
||||
worksheetReviewedAt: null,
|
||||
},
|
||||
}),
|
||||
});
|
||||
});
|
||||
await page.route(`**/api/eval/sessions/${sessionId}/reevaluate`, async (route) => {
|
||||
reevaluateRequests += 1;
|
||||
await route.fulfill({
|
||||
status: 503,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ detail: "engine_error: retry timeout" }),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto(`/teach/session/${sessionId}/review`);
|
||||
await expect(page.getByText("평가 실패")).toBeVisible();
|
||||
const reviewRequestsBeforeRetry = reviewRequests;
|
||||
|
||||
await page.getByRole("button", { name: "AI 평가 재시도" }).click();
|
||||
|
||||
await expect(page.getByText("engine_error: retry timeout")).toBeVisible();
|
||||
await expect(page.getByText("평가 실패")).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "AI 평가 재시도" })).toBeEnabled();
|
||||
expect(reevaluateRequests).toBe(1);
|
||||
expect(reviewRequests).toBe(reviewRequestsBeforeRetry);
|
||||
});
|
||||
|
||||
test("renders server review data without legacy transcript fixtures", async ({ page }) => {
|
||||
await signInAsLearner(page);
|
||||
await routePrepostMeasures(page);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue