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

@ -44,6 +44,7 @@ interface AdminEngineConfigResponse {
engine_mode: string;
engine_url: string;
model: string;
reasoning_effort: string | null;
updated_by: string | null;
updated_at: number | null;
}
@ -117,11 +118,28 @@ async function waitForReactInputCommit(page: Page) {
);
}
function hasEngineConfigRequestBody(engineMode: string, model: string) {
interface EngineCapabilitiesResponse {
available: boolean;
models: Array<{
id: string;
reasoning_efforts?: string[];
default_reasoning_effort?: string | null;
}>;
}
function hasEngineConfigRequestBody(engineMode: string, model: string, reasoningEffort: string | null) {
return (response: Response) => {
try {
const body = response.request().postDataJSON() as { engine_mode?: string; model?: string };
return body.engine_mode === engineMode && body.model === model;
const body = response.request().postDataJSON() as {
engine_mode?: string;
model?: string;
reasoning_effort?: string | null;
};
return (
body.engine_mode === engineMode &&
body.model === model &&
body.reasoning_effort === reasoningEffort
);
} catch {
return false;
}
@ -143,7 +161,9 @@ async function expectNoEngineSegmentClipping(page: Page) {
height: number;
}> = [];
const segment = document.querySelector<HTMLElement>("#set-engine .vg-set__seg");
const segment = document.querySelector<HTMLElement>(
"#set-engine [aria-label='AI 엔진 공급자']",
);
if (!segment) {
return [
{
@ -666,19 +686,31 @@ test.describe("settings page", () => {
const engine = page.locator("#set-engine");
await expect(engine).toBeVisible();
await expect(engine.locator("input").nth(0)).toHaveValue(originalEngineConfig.engine_url);
await expect(engine.locator("input").nth(1)).toHaveValue(originalEngineConfig.model);
await expect(engine.getByLabel("AI 연결 주소")).toHaveValue(originalEngineConfig.engine_url);
await expect(engine.getByLabel("AI 엔진 공급자")).toHaveValue(
originalEngineConfig.engine_mode,
);
await expect(engine.getByLabel("AI 기본 모델")).toHaveValue(originalEngineConfig.model);
const nextMode =
originalEngineConfig.engine_mode === "claude_api" ? "claude_cli" : "claude_api";
const nextModel = `e2e-model-${slugFor(testInfo)}`;
const capabilityResponse = await page.request.get(
`/api/admin/engine-capabilities?engine_mode=${encodeURIComponent(originalEngineConfig.engine_mode)}`,
);
await expectResponseOk(capabilityResponse);
const capability = (await capabilityResponse.json()) as EngineCapabilitiesResponse;
const candidate =
capability.models.find((model) => model.id !== originalEngineConfig.model) ??
capability.models[0];
if (!candidate) throw new Error("engine capability returned no selectable model");
const nextMode = originalEngineConfig.engine_mode;
const nextModel = candidate.id;
const nextEffort = candidate.default_reasoning_effort ?? null;
try {
const nextModeButton = engine.locator(`[data-engine-mode="${nextMode}"]`);
await nextModeButton.click();
await expect(nextModeButton).toHaveAttribute("aria-checked", "true");
await engine.locator("input").nth(1).fill(nextModel);
await expect(engine.locator("input").nth(1)).toHaveValue(nextModel);
await engine.getByLabel("AI 기본 모델").selectOption(nextModel);
await expect(engine.getByLabel("AI 기본 모델")).toHaveValue(nextModel);
if (nextEffort) {
await engine.getByLabel("AI 추론 강도").selectOption(nextEffort);
}
await waitForReactInputCommit(page);
const enginePatchResponse = await runAndWaitForApiResponse(
@ -688,16 +720,17 @@ test.describe("settings page", () => {
async () => {
await engine.locator(".vg-set__foot .vg-btn").click();
},
hasEngineConfigRequestBody(nextMode, nextModel),
hasEngineConfigRequestBody(nextMode, nextModel, nextEffort),
);
await expectResponseOk(enginePatchResponse);
const updatedEngine = (await enginePatchResponse.json()) as AdminEngineConfigResponse;
expect(updatedEngine).toMatchObject({
engine_mode: nextMode,
model: nextModel,
reasoning_effort: nextEffort,
updated_by: email,
});
await expect(engine.locator("input").nth(1)).toHaveValue(nextModel);
await expect(engine.getByLabel("AI 기본 모델")).toHaveValue(nextModel);
const healthResponse = await page.request.get("/api/admin/health");
await expectResponseOk(healthResponse);
@ -709,6 +742,7 @@ test.describe("settings page", () => {
engine_mode: originalEngineConfig.engine_mode,
engine_url: originalEngineConfig.engine_url,
model: originalEngineConfig.model,
reasoning_effort: originalEngineConfig.reasoning_effort,
},
});
await expectResponseOk(restoreResponse);
@ -742,12 +776,13 @@ test.describe("settings page", () => {
const engine = page.locator("#set-engine");
await expect(engine).toBeVisible();
await expect(engine.locator("[data-engine-mode]")).toHaveCount(4);
await expect(engine.getByLabel("AI 엔진 공급자").locator("option")).toHaveCount(6);
await expectNoEngineSegmentClipping(page);
await expect(engine.locator("input").nth(0)).toBeVisible();
await expect(engine.locator("input").nth(0)).toHaveValue(engineConfig!.engine_url);
await expect(engine.locator("input").nth(1)).toBeVisible();
await expect(engine.locator("input").nth(1)).toHaveValue(engineConfig!.model);
await expect(engine.getByLabel("AI 연결 주소")).toBeVisible();
await expect(engine.getByLabel("AI 연결 주소")).toHaveValue(engineConfig!.engine_url);
await expect(engine.getByLabel("AI 기본 모델")).toBeVisible();
await expect(engine.getByLabel("AI 기본 모델")).toHaveValue(engineConfig!.model);
await expect(engine.getByLabel("AI 추론 강도")).toBeVisible();
await expectNoSettingsControlClipping(page);
await expectNoHorizontalOverflow(page);