관리자 증거와 계정 흐름을 정돈

This commit is contained in:
Yun Chan 2026-08-30 00:01:17 +09:00
parent 21bbe3f98d
commit 34aff65cb0
9 changed files with 1100 additions and 193 deletions

View file

@ -3,6 +3,15 @@
// route fixture로 상태를 고정해 검증한다. 실제 AI 엔진 턴 생성은 하지 않는다.
import { expect, test, type Page } from "@playwright/test";
type UsageCostBasis =
| "provider_estimate"
| "provider_reported"
| "reference_rate"
| "reference_upper_bound"
| "partial"
| "partial_upper_bound"
| "unavailable";
interface AdminUsageBreakdown {
provider: string;
model: string;
@ -14,7 +23,7 @@ interface AdminUsageBreakdown {
estimated_cost_usd: number;
token_metered_turns: number;
token_unmetered_turns: number;
cost_basis: "provider_estimate" | "provider_reported" | "reference_rate" | "unavailable";
cost_basis: UsageCostBasis;
rate_label?: string | null;
rate_source_url?: string | null;
}
@ -25,6 +34,7 @@ interface AdminUsageDailyCost {
tokens_in: number;
tokens_out: number;
cost_usd: number;
cost_basis?: UsageCostBasis;
}
interface AdminUsageEvaluatorCache {
@ -52,11 +62,13 @@ interface AdminUsageResponse {
cost_usd: number;
recorded_cost_usd: number;
estimated_cost_usd: number;
cost_basis?: UsageCostBasis;
budget: {
limit_usd: number;
used_ratio: number;
remaining_usd: number | null;
status: "disabled" | "ok" | "warn" | "exceeded";
status: "disabled" | "ok" | "warn" | "exceeded" | "indeterminate";
cost_basis?: UsageCostBasis;
};
evaluator_cache?: AdminUsageEvaluatorCache;
by_provider: AdminUsageBreakdown[];
@ -90,10 +102,17 @@ function usageFixture(
tokens_in: 0,
tokens_out: 0,
cost_usd: 0,
cost_basis: "provider_reported",
// 실 API는 cost_usd = recorded + estimated 로 집계한다. 기본은 전액 기록값으로 둔다.
recorded_cost_usd: overrides.cost_usd ?? 0,
estimated_cost_usd: 0,
budget: { limit_usd: 0, used_ratio: 0, remaining_usd: null, status: "disabled" },
budget: {
limit_usd: 0,
used_ratio: 0,
remaining_usd: null,
status: "disabled",
cost_basis: "provider_reported",
},
evaluator_cache: {
enabled: false,
entries: 0,
@ -482,7 +501,7 @@ test.describe("full sweep: admin ai operations", () => {
await expect(chart.locator(".aic-chart__day")).toHaveCount(3);
await expect(chart.locator(".aic-chart__day").first()).toHaveAttribute(
"title",
"2026-07-13 $1.42",
"2026-07-13 $1.42 · Provider 보고",
);
await expect(chart.locator(".aic-chart__day").first()).toContainText("8턴");
@ -521,6 +540,70 @@ test.describe("full sweep: admin ai operations", () => {
await expect(page.locator(".aic-table")).toHaveCount(0);
});
test("does not present a partial upper bound as an exact cost", async ({ page }) => {
const state = createMockState({
usageForWindow: (days) =>
usageFixture(days, {
total_turns: 2,
metered_turns: 2,
token_metered_turns: 2,
tokens_in: 2_000_000,
tokens_out: 2_000_000,
cost_usd: 4.5,
recorded_cost_usd: 0,
estimated_cost_usd: 4.5,
cost_basis: "partial_upper_bound",
budget: {
limit_usd: 20,
used_ratio: 0.225,
remaining_usd: null,
status: "indeterminate",
cost_basis: "partial_upper_bound",
},
by_provider: [
{
provider: "agy_cli",
model: "gemini-3.7-flash-high",
turns: 2,
token_metered_turns: 2,
token_unmetered_turns: 0,
tokens_in: 2_000_000,
tokens_out: 2_000_000,
cost_usd: 4.5,
recorded_cost_usd: 0,
estimated_cost_usd: 4.5,
cost_basis: "partial_upper_bound",
rate_label: "일부 호출 미산정 · 산정된 부분도 상한 추정",
},
],
daily_cost: [
{
day: "2026-08-13",
turns: 2,
tokens_in: 2_000_000,
tokens_out: 2_000_000,
cost_usd: 4.5,
cost_basis: "partial_upper_bound",
},
],
}),
});
await mockAdminAiSession(page, state);
await page.goto("/admin/ai");
await expect(page.locator(".aic-ledger b").first()).toHaveText("미산정");
await expect(page.locator(".aic-ledger article").last().locator("b")).toHaveText("—");
await expect(page.locator(".aic-budget")).toContainText("예산 미확정");
await expect(page.locator(".aic-budget")).toContainText("잔여 미산정 · 사용률 미산정");
await expect(page.locator(".aic-chart__value")).toHaveText("미산정");
const row = page.locator(".aic-table tbody tr");
await expect(row.locator("td").nth(4)).toContainText("미산정");
await expect(row.locator("td").nth(4)).toContainText("일부 상한");
await expect(row.locator("td").nth(5)).toHaveText("—");
await expect(row.locator("td").nth(6)).toHaveText("—");
});
// checklist: admin-ai-engine-config-meta
test("shows engine config metadata for durable DB and runtime-only sources", async ({ page }) => {
const state = createMockState({