개선관리 요구사항과 Google 로그인을 완료

This commit is contained in:
Yun Chan 2026-08-28 16:07:09 +09:00
parent cc0a15b7c6
commit 2a39636163
112 changed files with 10166 additions and 527 deletions

View file

@ -1,3 +1,4 @@
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
import { expect, test, type Page } from "@playwright/test";
import {
completeAlliancePreCheckpoint,
@ -10,6 +11,8 @@ import {
interface SessionStartResponse {
session_id: string;
case_id: string;
session_no: number;
degraded: boolean;
}
@ -173,6 +176,126 @@ interface PrepostMeasuresResponse {
measures: PrepostMeasureItem[];
}
interface ControlledFailureGateway {
url: string;
clientRequests: () => number;
evaluatorRequests: () => number;
close: () => Promise<void>;
}
async function startControlledEvaluationFailureGateway(
model: string,
fallbackProvider: string,
): Promise<ControlledFailureGateway> {
let clientRequestCount = 0;
let evaluatorRequestCount = 0;
const sendJson = (response: ServerResponse, status: number, payload: unknown) => {
response.writeHead(status, { "content-type": "application/json; charset=utf-8" });
response.end(JSON.stringify(payload));
};
const readJson = async (request: IncomingMessage) => {
const chunks: Buffer[] = [];
for await (const chunk of request) {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
}
const raw = Buffer.concat(chunks).toString("utf8");
return raw ? (JSON.parse(raw) as Record<string, unknown>) : {};
};
const server = createServer(async (request, response) => {
try {
const url = new URL(request.url ?? "/", "http://127.0.0.1");
const provider = url.searchParams.get("provider") || fallbackProvider;
if (request.method === "GET" && url.pathname === "/v1/capabilities") {
sendJson(response, 200, {
provider,
available: true,
source: "live_cli",
models: [
{
id: model,
label: "REQ-007 controlled gateway",
description: "client succeeds while evaluator returns HTTP 500",
reasoning_efforts: ["low", "medium", "high", "xhigh", "max", "ultra"],
default_reasoning_effort: "high",
is_default: true,
},
],
default_model: model,
default_reasoning_effort: "high",
detail: "controlled gateway ready",
fetched_at: Date.now() / 1000,
});
return;
}
if (request.method === "GET" && ["/ready", "/health"].includes(url.pathname)) {
sendJson(response, 200, {
ok: true,
status: "ok",
detail: "controlled gateway ready",
cached: false,
});
return;
}
if (request.method === "DELETE" && url.pathname.startsWith("/session/")) {
sendJson(response, 200, { closed: true });
return;
}
if (request.method === "POST" && url.pathname === "/v1/generate") {
const payload = await readJson(request);
if (payload.ai_role === "evaluator") {
evaluatorRequestCount += 1;
sendJson(response, 500, { detail: "controlled evaluator failure" });
return;
}
clientRequestCount += 1;
sendJson(response, 200, {
text: "조금 더 이야기해볼게요.",
model,
provider: String(payload.provider || fallbackProvider),
tokens_in: 7,
tokens_out: 9,
cost_usd: 0,
});
return;
}
sendJson(response, 404, { detail: `unexpected controlled gateway path: ${url.pathname}` });
} catch (error) {
sendJson(response, 500, {
detail: error instanceof Error ? error.message : String(error),
});
}
});
await new Promise<void>((resolve, reject) => {
const onError = (error: Error) => reject(error);
server.once("error", onError);
server.listen(0, "127.0.0.1", () => {
server.off("error", onError);
resolve();
});
});
const address = server.address();
if (!address || typeof address === "string") {
await new Promise<void>((resolve) => server.close(() => resolve()));
throw new Error("controlled gateway did not expose a TCP port");
}
return {
url: `http://127.0.0.1:${address.port}`,
clientRequests: () => clientRequestCount,
evaluatorRequests: () => evaluatorRequestCount,
close: () =>
new Promise<void>((resolve, reject) => {
server.close((error) => {
if (error) reject(error);
else resolve();
});
}),
};
}
async function expectResponseOk(response: { ok: () => boolean; text: () => Promise<string> }) {
if (!response.ok()) {
expect(response.ok(), await response.text()).toBeTruthy();
@ -1357,6 +1480,158 @@ test.describe("session persistence", () => {
}
});
test("keeps the failed review durable while the next persona session accepts a stored turn @single-run", async ({
page,
}) => {
test.setTimeout(180_000);
await withGlobalEngineConfigLock("req-007-evaluation-failure-continuity", async () => {
await signInAsAdmin(page);
const engineResponse = await page.request.get("/api/admin/engine-config");
await expectResponseOk(engineResponse);
const currentEngine = (await engineResponse.json()) as AdminEngineConfigResponse;
expect(currentEngine.source, "REQ-007 continuity proof requires the real DB lane").toBe(
"database",
);
expect(currentEngine.durable, "REQ-007 engine configuration must be durable").toBe(true);
const gateway = await startControlledEvaluationFailureGateway(
currentEngine.model,
currentEngine.engine_mode,
);
try {
const controlledPatch = await page.request.patch("/api/admin/engine-config", {
data: {
engine_mode: currentEngine.engine_mode,
engine_url: gateway.url,
model: currentEngine.model,
reasoning_effort: currentEngine.reasoning_effort,
},
});
await expectResponseOk(controlledPatch);
await signInAsLearner(page);
const persona = await fetchAvailablePersona(page);
const firstStartResponse = await page.request.post("/api/sessions", {
data: { persona_code: persona.code, theory_mode: "humanistic" },
});
await expectResponseOk(firstStartResponse);
expect(firstStartResponse.status()).toBe(201);
const first = (await firstStartResponse.json()) as SessionStartResponse;
expect(first.degraded).toBe(false);
const firstTurnResponse = await page.request.post(
`/api/sessions/${first.session_id}/turn`,
{
data: { text: "오늘 가장 버거웠던 순간부터 이야기해도 괜찮을까요?" },
},
);
await expectResponseOk(firstTurnResponse);
const endResponse = await page.request.post(`/api/sessions/${first.session_id}/end`);
await expectResponseOk(endResponse);
expect(endResponse.status()).toBe(200);
const endedDetailResponse = await page.request.get(`/api/sessions/${first.session_id}`);
await expectResponseOk(endedDetailResponse);
const endedDetail = (await endedDetailResponse.json()) as SessionDetailResponse;
expect(endedDetail.status).toBe("ended");
expect(endedDetail.ended_at).toBeTruthy();
expect(endedDetail.turns?.length ?? 0).toBeGreaterThanOrEqual(2);
const nextStartResponse = await page.request.post("/api/sessions", {
data: { persona_code: persona.code, theory_mode: "humanistic" },
});
await expectResponseOk(nextStartResponse);
expect(nextStartResponse.status()).toBe(201);
const next = (await nextStartResponse.json()) as SessionStartResponse;
expect(next.degraded).toBe(false);
expect(next.session_id).not.toBe(first.session_id);
expect(next.case_id).toBe(first.case_id);
expect(next.session_no).toBe(first.session_no + 1);
const nextTurnResponse = await page.request.post(
`/api/sessions/${next.session_id}/turn`,
{
data: { text: "지난 회기의 이야기를 이어서 천천히 살펴볼까요?" },
},
);
await expectResponseOk(nextTurnResponse);
const nextTurn = (await nextTurnResponse.json()) as {
client_reply?: string | null;
output_error?: string | null;
};
expect(nextTurn.client_reply).toBe("조금 더 이야기해볼게요.");
expect(nextTurn.output_error ?? null).toBeNull();
const nextDetailResponse = await page.request.get(`/api/sessions/${next.session_id}`);
await expectResponseOk(nextDetailResponse);
const nextDetail = (await nextDetailResponse.json()) as SessionDetailResponse;
expect(nextDetail.status).toBe("active");
expect(nextDetail.turns?.length ?? 0).toBeGreaterThanOrEqual(2);
expect(gateway.clientRequests()).toBeGreaterThanOrEqual(2);
await expect
.poll(() => gateway.evaluatorRequests(), {
timeout: 30_000,
message: "the ended session must reach the controlled evaluator failure",
})
.toBeGreaterThan(0);
await signInAsTeacher(page);
await expect
.poll(async () => evaluationState(page, first.session_id), {
timeout: 30_000,
intervals: [250, 500, 1_000],
message: "the evaluator HTTP 500 must become a durable error row",
})
.toMatch(/^error:.*:durable$/);
const reviewResponse = await page.request.get(`/api/sessions/${first.session_id}/review`);
await expectResponseOk(reviewResponse);
const review = (await reviewResponse.json()) as SessionReviewResponse;
expect(review.reviewReady).toBe(false);
expect(review.degraded).toBe(true);
expect(review.supervisorState).toBe("평가 실패");
expect(review.summary ?? "").toContain("AI 평가 재시도가 필요합니다");
await page.goto(`/teach/session/${first.session_id}/review`);
await expect(page.getByRole("button", { name: "AI 평가 재시도" })).toBeVisible({
timeout: 15_000,
});
const evaluatorRequestsBeforeRetry = gateway.evaluatorRequests();
const retryResponse = await page.request.post(
`/api/eval/sessions/${first.session_id}/reevaluate`,
{ data: { scope: "session_end" } },
);
expect(retryResponse.status()).toBe(503);
await expect
.poll(() => gateway.evaluatorRequests(), { timeout: 10_000 })
.toBeGreaterThan(evaluatorRequestsBeforeRetry);
await expect
.poll(async () => evaluationState(page, first.session_id), {
timeout: 10_000,
})
.toMatch(/^error:.*:durable$/);
} finally {
try {
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,
reasoning_effort: currentEngine.reasoning_effort,
},
});
await expectResponseOk(restoreResponse);
} finally {
await gateway.close();
}
}
});
});
test("finishes session end evaluation into a durable DB-backed teacher review @single-run", async ({
page,
}) => {