feat: 운영 안정성과 세션 음성 경험 개선

This commit is contained in:
Yun Chan 2026-07-31 00:13:08 +09:00
parent facc4ad2d9
commit c788343467
95 changed files with 8431 additions and 1785 deletions

View file

@ -152,6 +152,7 @@ interface AdminEngineConfigResponse {
engine_mode: string;
engine_url: string;
model: string;
reasoning_effort?: string | null;
}
interface PrepostMeasureItem {
@ -1430,124 +1431,36 @@ test.describe("session persistence", () => {
}
});
test("runs manual AI evaluation retry from teacher review UI into durable DB state @single-run", async ({
test("rejects an unreachable AI gateway before it can poison the evaluation runtime @single-run", async ({
page,
}) => {
test.setTimeout(240_000);
test.setTimeout(120_000);
const healthResponse = await page.request.get("/api/health");
await expectResponseOk(healthResponse);
const health = (await healthResponse.json()) as HealthResponse;
test.skip(!health.db || !health.engine, "manual evaluation retry requires DB and engine");
test.skip(!health.db || !health.engine, "engine configuration validation requires DB and engine");
await signInAsLearner(page);
const sessionId = await createActiveSessionWithTurn(page);
const endedResponse = await page.request.post(`/api/sessions/${sessionId}/end`);
await expectResponseOk(endedResponse);
await withGlobalEngineConfigLock("manual-evaluation-retry-ui", async () => {
await withGlobalEngineConfigLock("engine-config-fail-closed", async () => {
await signInAsAdmin(page);
const engineResponse = await page.request.get("/api/admin/engine-config");
await expectResponseOk(engineResponse);
const currentEngine = (await engineResponse.json()) as AdminEngineConfigResponse;
try {
const brokenEnginePatch = await page.request.patch("/api/admin/engine-config", {
data: {
engine_mode: currentEngine.engine_mode,
engine_url: "http://127.0.0.1:9",
model: currentEngine.model,
},
});
await expectResponseOk(brokenEnginePatch);
const brokenEnginePatch = await page.request.patch("/api/admin/engine-config", {
data: {
engine_mode: currentEngine.engine_mode,
engine_url: "http://127.0.0.1:9",
model: currentEngine.model,
reasoning_effort: currentEngine.reasoning_effort,
},
});
expect(brokenEnginePatch.status()).toBe(422);
await signInAsTeacher(page);
const failedReevaluationResponse = await page.request.post(
`/api/eval/sessions/${sessionId}/reevaluate`,
{ data: { scope: "session_end" } },
);
expect(
failedReevaluationResponse.ok(),
"broken engine should create a failed durable evaluation seed",
).toBe(false);
await signInAsAdmin(page);
const restoreResponse = await page.request.patch("/api/admin/engine-config", {
data: {
engine_mode: currentEngine.engine_mode,
engine_url: currentEngine.engine_url,
model: currentEngine.model,
},
});
await expectResponseOk(restoreResponse);
await signInAsTeacher(page);
await page.goto(`/teach/session/${sessionId}/review`);
await expect(page.getByText("평가 실패")).toBeVisible({ timeout: 15_000 });
const retryButton = page.getByRole("button", { name: "AI 평가 재시도" });
await expect(retryButton).toBeVisible();
const reevaluateResponsePromise = page.waitForResponse((response) => {
const url = new URL(response.url());
return (
response.request().method() === "POST" &&
url.pathname.endsWith(`/eval/sessions/${sessionId}/reevaluate`)
);
});
await retryButton.click();
const reevaluateResponse = await reevaluateResponsePromise;
await expectResponseOk(reevaluateResponse);
const reevaluation = (await reevaluateResponse.json()) as {
scope?: string;
error?: string | null;
turns_evaluated?: number;
};
expect(reevaluation.scope).toBe("session_end");
expect(reevaluation.error ?? "").toBe("");
expect(reevaluation.turns_evaluated ?? 0).toBeGreaterThan(0);
await expect(page.locator(".sr-head__stats")).toContainText("평가 완료", {
timeout: 30_000,
});
await expect(page.locator('[aria-label="리뷰 생성 상태"]')).toContainText("준비됨");
await expect(page.getByRole("button", { name: "AI 평가 재시도" })).toHaveCount(0);
const evaluationResponse = await page.request.get(`/api/eval/sessions/${sessionId}/evaluation`);
await expectResponseOk(evaluationResponse);
const evaluation = (await evaluationResponse.json()) as EvaluationSummaryResponse;
expect(evaluation.status).toBe("ready");
expect(evaluation.durable).toBe(true);
expect(evaluation.deep?.scope).toBe("session_end");
expect(evaluation.deep?.turns_evaluated).toBe(reevaluation.turns_evaluated);
const reviewResponse = await page.request.get(`/api/sessions/${sessionId}/review`);
await expectResponseOk(reviewResponse);
const review = (await reviewResponse.json()) as SessionReviewResponse;
expect(review.reviewReady).toBe(true);
expect(review.supervisorState).toBe("평가 완료");
const dashboardResponse = await page.request.get("/api/teacher/dashboard");
await expectResponseOk(dashboardResponse);
const dashboard = (await dashboardResponse.json()) as TeacherDashboardResponse;
expect(dashboard.source).toBe("database");
const dashboardSession = [...(dashboard.pending_reviews ?? []), ...(dashboard.recent_sessions ?? [])].find(
(session) => session.session_id === sessionId,
);
expect(dashboardSession, "teacher dashboard should expose the retried session").toBeTruthy();
expect(dashboardSession?.evaluation_status).toBe("ready");
expect(dashboardSession?.review_ready).toBe(true);
expect(dashboardSession?.supervisor_state).toBe("평가 완료");
} finally {
await signInAsAdmin(page);
const restoreResponse = await page.request.patch("/api/admin/engine-config", {
data: {
engine_mode: currentEngine.engine_mode,
engine_url: currentEngine.engine_url,
model: currentEngine.model,
},
});
await expectResponseOk(restoreResponse);
}
const persistedResponse = await page.request.get("/api/admin/engine-config");
await expectResponseOk(persistedResponse);
const persisted = (await persistedResponse.json()) as AdminEngineConfigResponse;
expect(persisted).toMatchObject(currentEngine);
});
});
});