현재 작업 전체 반영
This commit is contained in:
parent
5560638e54
commit
c0dddab594
85 changed files with 11322 additions and 539 deletions
|
|
@ -39,6 +39,35 @@ interface AdminUsersResponse {
|
|||
users: AdminManagedUser[];
|
||||
}
|
||||
|
||||
interface AdminUsageBreakdown {
|
||||
provider: string;
|
||||
model: string;
|
||||
turns: number;
|
||||
tokens_in: number;
|
||||
tokens_out: number;
|
||||
cost_usd: number;
|
||||
}
|
||||
|
||||
interface AdminUsageBudget {
|
||||
limit_usd: number;
|
||||
used_ratio: number;
|
||||
remaining_usd: number | null;
|
||||
status: "disabled" | "ok" | "warn" | "exceeded";
|
||||
}
|
||||
|
||||
interface AdminUsageResponse {
|
||||
source: "database" | "server_session_registry";
|
||||
durable: boolean;
|
||||
window_days: number;
|
||||
total_turns: number;
|
||||
metered_turns: number;
|
||||
tokens_in: number;
|
||||
tokens_out: number;
|
||||
cost_usd: number;
|
||||
budget: AdminUsageBudget;
|
||||
by_provider: AdminUsageBreakdown[];
|
||||
}
|
||||
|
||||
async function expectResponseOk(response: APIResponse | Response) {
|
||||
if (!response.ok()) {
|
||||
expect(response.ok(), await response.text()).toBeTruthy();
|
||||
|
|
@ -66,6 +95,11 @@ function isAdminUsersResponse(response: Response) {
|
|||
return response.request().method() === "GET" && url.pathname.endsWith("/admin/users");
|
||||
}
|
||||
|
||||
function isAdminUsageResponse(response: Response) {
|
||||
const url = new URL(response.url());
|
||||
return response.request().method() === "GET" && url.pathname.endsWith("/admin/usage");
|
||||
}
|
||||
|
||||
function isAdminUserCreate(response: Response) {
|
||||
const url = new URL(response.url());
|
||||
return response.request().method() === "POST" && url.pathname.endsWith("/admin/users");
|
||||
|
|
@ -106,17 +140,33 @@ function engineModeLabel(value: string) {
|
|||
return value;
|
||||
}
|
||||
|
||||
function countLabel(value: number) {
|
||||
if (!Number.isFinite(value)) return "0";
|
||||
return Math.round(value).toLocaleString("ko-KR");
|
||||
}
|
||||
|
||||
function costLabel(value: number) {
|
||||
if (!Number.isFinite(value) || value <= 0) return "$0";
|
||||
return `$${value.toFixed(value < 0.01 ? 6 : 4)}`;
|
||||
}
|
||||
|
||||
async function openAdminAndReadHealth(page: Page) {
|
||||
const healthResponsePromise = page.waitForResponse(isAdminHealthResponse);
|
||||
const usageResponsePromise = page.waitForResponse(isAdminUsageResponse);
|
||||
|
||||
await page.goto("/admin");
|
||||
|
||||
const healthResponse = await healthResponsePromise;
|
||||
const [healthResponse, usageResponse] = await Promise.all([
|
||||
healthResponsePromise,
|
||||
usageResponsePromise,
|
||||
]);
|
||||
await expectResponseOk(healthResponse);
|
||||
await expectResponseOk(usageResponse);
|
||||
|
||||
const health = (await healthResponse.json()) as AdminHealthResponse;
|
||||
const usage = (await usageResponse.json()) as AdminUsageResponse;
|
||||
expect(health.services.length).toBeGreaterThan(0);
|
||||
return health;
|
||||
return { health, usage };
|
||||
}
|
||||
|
||||
async function openAdminAndReadUsers(page: Page) {
|
||||
|
|
@ -225,7 +275,7 @@ test.describe("admin route", () => {
|
|||
await signInAsAdmin(page);
|
||||
|
||||
await withGlobalEngineConfigLock("admin-health-dashboard", async () => {
|
||||
const health = await openAdminAndReadHealth(page);
|
||||
const { health, usage } = await openAdminAndReadHealth(page);
|
||||
const serviceCards = page.locator(".ad-service:not(.ad-service--skeleton)");
|
||||
const counts = {
|
||||
ok: health.services.filter((service) => service.status === "ok").length,
|
||||
|
|
@ -242,6 +292,34 @@ test.describe("admin route", () => {
|
|||
String(counts.degraded),
|
||||
String(counts.down),
|
||||
]);
|
||||
await expect(page.getByRole("heading", { name: "AI 비용 관측" })).toBeVisible();
|
||||
await expect(page.locator(".ad-usage-kpi b")).toHaveText([
|
||||
costLabel(usage.cost_usd),
|
||||
countLabel(usage.tokens_in),
|
||||
countLabel(usage.tokens_out),
|
||||
usage.total_turns > 0
|
||||
? `${Math.round((usage.metered_turns / usage.total_turns) * 100)}%`
|
||||
: "0%",
|
||||
]);
|
||||
await expect(page.locator(".ad-usage-budget")).toContainText(
|
||||
usage.budget.status === "disabled"
|
||||
? "예산 경고 비활성"
|
||||
: usage.budget.status === "exceeded"
|
||||
? "예산 초과"
|
||||
: usage.budget.status === "warn"
|
||||
? "예산 주의"
|
||||
: "예산 정상",
|
||||
);
|
||||
if (usage.by_provider.length > 0) {
|
||||
await expect(page.locator(".ad-usage-row")).toHaveCount(usage.by_provider.length);
|
||||
await expect(page.locator(".ad-usage-row").first()).toContainText(
|
||||
usage.by_provider[0].provider,
|
||||
);
|
||||
} else {
|
||||
await expect(page.locator(".ad-usage-breakdown")).toContainText(
|
||||
"최근 윈도우에 계량된 AI 턴이 없습니다.",
|
||||
);
|
||||
}
|
||||
await expect(serviceCards).toHaveCount(health.services.length);
|
||||
|
||||
for (const service of health.services) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue