mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
test(platform): add unit tests for platform cost helpers and data layer
Extract pure helper functions (formatMicrodollars, formatTokens, formatDuration, estimateCostForRow, trackingValue, toDateOrUndefined) from PlatformCostContent.tsx into helpers.ts for testability. Add 26 vitest cases covering all formatting and cost-estimation branches. Add backend tests for _build_where and _json_or_none in platform_cost.py (11 pytest cases covering filter combinations).
This commit is contained in:
71
autogpt_platform/backend/backend/data/platform_cost_test.py
Normal file
71
autogpt_platform/backend/backend/data/platform_cost_test.py
Normal file
@@ -0,0 +1,71 @@
|
||||
"""Unit tests for pure helpers in platform_cost module."""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from .platform_cost import _build_where, _json_or_none
|
||||
|
||||
|
||||
class TestJsonOrNone:
|
||||
def test_returns_none_for_none(self):
|
||||
assert _json_or_none(None) is None
|
||||
|
||||
def test_returns_json_string_for_dict(self):
|
||||
result = _json_or_none({"key": "value", "num": 42})
|
||||
assert result is not None
|
||||
assert '"key"' in result
|
||||
assert '"value"' in result
|
||||
|
||||
def test_returns_json_for_empty_dict(self):
|
||||
assert _json_or_none({}) == "{}"
|
||||
|
||||
|
||||
class TestBuildWhere:
|
||||
def test_no_filters_returns_true(self):
|
||||
sql, params = _build_where(None, None, None, None)
|
||||
assert sql == "TRUE"
|
||||
assert params == []
|
||||
|
||||
def test_start_only(self):
|
||||
dt = datetime(2026, 1, 1, tzinfo=timezone.utc)
|
||||
sql, params = _build_where(dt, None, None, None)
|
||||
assert '"createdAt" >= $1::timestamptz' in sql
|
||||
assert params == [dt]
|
||||
|
||||
def test_end_only(self):
|
||||
dt = datetime(2026, 6, 1, tzinfo=timezone.utc)
|
||||
sql, params = _build_where(None, dt, None, None)
|
||||
assert '"createdAt" <= $1::timestamptz' in sql
|
||||
assert params == [dt]
|
||||
|
||||
def test_provider_only(self):
|
||||
sql, params = _build_where(None, None, "openai", None)
|
||||
assert 'LOWER("provider") = LOWER($1)' in sql
|
||||
assert params == ["openai"]
|
||||
|
||||
def test_user_id_only(self):
|
||||
sql, params = _build_where(None, None, None, "user-123")
|
||||
assert '"userId" = $1' in sql
|
||||
assert params == ["user-123"]
|
||||
|
||||
def test_all_filters(self):
|
||||
start = datetime(2026, 1, 1, tzinfo=timezone.utc)
|
||||
end = datetime(2026, 6, 1, tzinfo=timezone.utc)
|
||||
sql, params = _build_where(start, end, "anthropic", "u1")
|
||||
assert "$1" in sql
|
||||
assert "$2" in sql
|
||||
assert "$3" in sql
|
||||
assert "$4" in sql
|
||||
assert len(params) == 4
|
||||
assert params == [start, end, "anthropic", "u1"]
|
||||
|
||||
def test_table_alias(self):
|
||||
dt = datetime(2026, 1, 1, tzinfo=timezone.utc)
|
||||
sql, params = _build_where(dt, None, None, None, table_alias="p")
|
||||
assert 'p."createdAt"' in sql
|
||||
assert params == [dt]
|
||||
|
||||
def test_clauses_joined_with_and(self):
|
||||
start = datetime(2026, 1, 1, tzinfo=timezone.utc)
|
||||
end = datetime(2026, 6, 1, tzinfo=timezone.utc)
|
||||
sql, _ = _build_where(start, end, None, None)
|
||||
assert " AND " in sql
|
||||
@@ -0,0 +1,206 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { ProviderCostSummary } from "@/app/api/__generated__/models/providerCostSummary";
|
||||
import {
|
||||
toDateOrUndefined,
|
||||
formatMicrodollars,
|
||||
formatTokens,
|
||||
formatDuration,
|
||||
estimateCostForRow,
|
||||
trackingValue,
|
||||
} from "../helpers";
|
||||
|
||||
function makeRow(overrides: Partial<ProviderCostSummary>): ProviderCostSummary {
|
||||
return {
|
||||
provider: "openai",
|
||||
tracking_type: null,
|
||||
total_cost_microdollars: 0,
|
||||
total_input_tokens: 0,
|
||||
total_output_tokens: 0,
|
||||
total_duration_seconds: 0,
|
||||
request_count: 0,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("toDateOrUndefined", () => {
|
||||
it("returns undefined for empty string", () => {
|
||||
expect(toDateOrUndefined("")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined for undefined", () => {
|
||||
expect(toDateOrUndefined(undefined)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined for invalid date string", () => {
|
||||
expect(toDateOrUndefined("not-a-date")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns a Date for a valid ISO string", () => {
|
||||
const result = toDateOrUndefined("2026-01-15T00:00:00Z");
|
||||
expect(result).toBeInstanceOf(Date);
|
||||
expect(result!.toISOString()).toBe("2026-01-15T00:00:00.000Z");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatMicrodollars", () => {
|
||||
it("formats zero", () => {
|
||||
expect(formatMicrodollars(0)).toBe("$0.0000");
|
||||
});
|
||||
|
||||
it("formats a small amount", () => {
|
||||
expect(formatMicrodollars(50_000)).toBe("$0.0500");
|
||||
});
|
||||
|
||||
it("formats one dollar", () => {
|
||||
expect(formatMicrodollars(1_000_000)).toBe("$1.0000");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatTokens", () => {
|
||||
it("formats small numbers as-is", () => {
|
||||
expect(formatTokens(500)).toBe("500");
|
||||
});
|
||||
|
||||
it("formats thousands with K suffix", () => {
|
||||
expect(formatTokens(1_500)).toBe("1.5K");
|
||||
});
|
||||
|
||||
it("formats millions with M suffix", () => {
|
||||
expect(formatTokens(2_500_000)).toBe("2.5M");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatDuration", () => {
|
||||
it("formats seconds", () => {
|
||||
expect(formatDuration(30)).toBe("30.0s");
|
||||
});
|
||||
|
||||
it("formats minutes", () => {
|
||||
expect(formatDuration(90)).toBe("1.5m");
|
||||
});
|
||||
|
||||
it("formats hours", () => {
|
||||
expect(formatDuration(5400)).toBe("1.5h");
|
||||
});
|
||||
});
|
||||
|
||||
describe("estimateCostForRow", () => {
|
||||
it("returns microdollars directly for cost_usd tracking", () => {
|
||||
const row = makeRow({
|
||||
tracking_type: "cost_usd",
|
||||
total_cost_microdollars: 500_000,
|
||||
});
|
||||
expect(estimateCostForRow(row, {})).toBe(500_000);
|
||||
});
|
||||
|
||||
it("returns reported cost for token tracking when cost > 0", () => {
|
||||
const row = makeRow({
|
||||
tracking_type: "tokens",
|
||||
total_cost_microdollars: 100_000,
|
||||
total_input_tokens: 1000,
|
||||
total_output_tokens: 500,
|
||||
});
|
||||
expect(estimateCostForRow(row, {})).toBe(100_000);
|
||||
});
|
||||
|
||||
it("estimates cost from default rate for token tracking with zero cost", () => {
|
||||
const row = makeRow({
|
||||
provider: "openai",
|
||||
tracking_type: "tokens",
|
||||
total_cost_microdollars: 0,
|
||||
total_input_tokens: 500,
|
||||
total_output_tokens: 500,
|
||||
});
|
||||
// 1000 tokens / 1000 * 0.005 USD * 1_000_000 = 5000
|
||||
expect(estimateCostForRow(row, {})).toBe(5000);
|
||||
});
|
||||
|
||||
it("returns null for unknown token provider with zero cost", () => {
|
||||
const row = makeRow({
|
||||
provider: "unknown_provider",
|
||||
tracking_type: "tokens",
|
||||
total_cost_microdollars: 0,
|
||||
});
|
||||
expect(estimateCostForRow(row, {})).toBeNull();
|
||||
});
|
||||
|
||||
it("uses per-run override when provided", () => {
|
||||
const row = makeRow({
|
||||
provider: "google_maps",
|
||||
tracking_type: "per_run",
|
||||
request_count: 10,
|
||||
});
|
||||
// override = 0.05 * 10 * 1_000_000 = 500_000
|
||||
expect(estimateCostForRow(row, { google_maps: 0.05 })).toBe(500_000);
|
||||
});
|
||||
|
||||
it("uses default per-run cost when no override", () => {
|
||||
const row = makeRow({
|
||||
provider: "google_maps",
|
||||
tracking_type: null,
|
||||
request_count: 5,
|
||||
});
|
||||
// 0.032 * 5 * 1_000_000 = 160_000
|
||||
expect(estimateCostForRow(row, {})).toBe(160_000);
|
||||
});
|
||||
|
||||
it("returns null for unknown per_run provider", () => {
|
||||
const row = makeRow({
|
||||
provider: "totally_unknown",
|
||||
tracking_type: "per_run",
|
||||
request_count: 3,
|
||||
});
|
||||
expect(estimateCostForRow(row, {})).toBeNull();
|
||||
});
|
||||
|
||||
it("returns cost for other tracking types when cost > 0", () => {
|
||||
const row = makeRow({
|
||||
tracking_type: "duration_seconds",
|
||||
total_cost_microdollars: 42_000,
|
||||
});
|
||||
expect(estimateCostForRow(row, {})).toBe(42_000);
|
||||
});
|
||||
|
||||
it("returns null for other tracking types when cost is 0", () => {
|
||||
const row = makeRow({
|
||||
tracking_type: "duration_seconds",
|
||||
total_cost_microdollars: 0,
|
||||
});
|
||||
expect(estimateCostForRow(row, {})).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("trackingValue", () => {
|
||||
it("returns formatted microdollars for cost_usd", () => {
|
||||
const row = makeRow({
|
||||
tracking_type: "cost_usd",
|
||||
total_cost_microdollars: 1_000_000,
|
||||
});
|
||||
expect(trackingValue(row)).toBe("$1.0000");
|
||||
});
|
||||
|
||||
it("returns formatted token count for tokens", () => {
|
||||
const row = makeRow({
|
||||
tracking_type: "tokens",
|
||||
total_input_tokens: 500,
|
||||
total_output_tokens: 500,
|
||||
});
|
||||
expect(trackingValue(row)).toBe("1.0K");
|
||||
});
|
||||
|
||||
it("returns formatted duration for duration_seconds", () => {
|
||||
const row = makeRow({
|
||||
tracking_type: "duration_seconds",
|
||||
total_duration_seconds: 120,
|
||||
});
|
||||
expect(trackingValue(row)).toBe("2.0m");
|
||||
});
|
||||
|
||||
it("returns run count for per_run (default tracking)", () => {
|
||||
const row = makeRow({
|
||||
tracking_type: null,
|
||||
request_count: 42,
|
||||
});
|
||||
expect(trackingValue(row)).toBe("42 runs");
|
||||
});
|
||||
});
|
||||
@@ -5,12 +5,7 @@ import {
|
||||
getV2GetPlatformCostLogs,
|
||||
} from "@/app/api/__generated__/endpoints/admin/admin";
|
||||
import { okData } from "@/app/api/helpers";
|
||||
|
||||
function toDateOrUndefined(val?: string): Date | undefined {
|
||||
if (!val) return undefined;
|
||||
const d = new Date(val);
|
||||
return isNaN(d.getTime()) ? undefined : d;
|
||||
}
|
||||
import { toDateOrUndefined } from "./helpers";
|
||||
|
||||
export async function getPlatformCostDashboard(params?: {
|
||||
start?: string;
|
||||
|
||||
@@ -7,6 +7,14 @@ import type { CostLogRow } from "@/app/api/__generated__/models/costLogRow";
|
||||
import type { Pagination } from "@/app/api/__generated__/models/pagination";
|
||||
import type { PlatformCostLogsResponse } from "@/app/api/__generated__/models/platformCostLogsResponse";
|
||||
import { getPlatformCostDashboard, getPlatformCostLogs } from "../actions";
|
||||
import {
|
||||
DEFAULT_COST_PER_RUN,
|
||||
estimateCostForRow,
|
||||
formatDuration,
|
||||
formatMicrodollars,
|
||||
formatTokens,
|
||||
trackingValue,
|
||||
} from "../helpers";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
|
||||
interface Props {
|
||||
@@ -20,42 +28,6 @@ interface Props {
|
||||
};
|
||||
}
|
||||
|
||||
// Default per-run costs in USD (checked 2026-04-02)
|
||||
const DEFAULT_COST_PER_RUN: Record<string, number> = {
|
||||
google_maps: 0.032,
|
||||
ideogram: 0.08,
|
||||
nvidia: 0.0,
|
||||
screenshotone: 0.01,
|
||||
zerobounce: 0.008,
|
||||
mem0: 0.01,
|
||||
openweathermap: 0.0,
|
||||
webshare_proxy: 0.0,
|
||||
};
|
||||
|
||||
// Default cost per 1K tokens in USD for token-based providers without actual cost
|
||||
const DEFAULT_COST_PER_1K_TOKENS: Record<string, number> = {
|
||||
openai: 0.005,
|
||||
anthropic: 0.008,
|
||||
groq: 0.0003,
|
||||
ollama: 0.0,
|
||||
};
|
||||
|
||||
function formatMicrodollars(microdollars: number) {
|
||||
return `$${(microdollars / 1_000_000).toFixed(4)}`;
|
||||
}
|
||||
|
||||
function formatTokens(tokens: number) {
|
||||
if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(1)}M`;
|
||||
if (tokens >= 1_000) return `${(tokens / 1_000).toFixed(1)}K`;
|
||||
return tokens.toString();
|
||||
}
|
||||
|
||||
function formatDuration(seconds: number) {
|
||||
if (seconds >= 3600) return `${(seconds / 3600).toFixed(1)}h`;
|
||||
if (seconds >= 60) return `${(seconds / 60).toFixed(1)}m`;
|
||||
return `${seconds.toFixed(1)}s`;
|
||||
}
|
||||
|
||||
function trackingBadge(trackingType: string | null | undefined) {
|
||||
const colors: Record<string, string> = {
|
||||
cost_usd:
|
||||
@@ -81,48 +53,6 @@ function trackingBadge(trackingType: string | null | undefined) {
|
||||
);
|
||||
}
|
||||
|
||||
function estimateCostForRow(
|
||||
row: ProviderCostSummary,
|
||||
costPerRunOverrides: Record<string, number>,
|
||||
) {
|
||||
const tt = row.tracking_type || "per_run";
|
||||
if (tt === "cost_usd") return row.total_cost_microdollars;
|
||||
if (tt === "tokens") {
|
||||
if (row.total_cost_microdollars > 0) return row.total_cost_microdollars;
|
||||
const rate = DEFAULT_COST_PER_1K_TOKENS[row.provider] ?? null;
|
||||
if (rate !== null) {
|
||||
const totalTokens = row.total_input_tokens + row.total_output_tokens;
|
||||
return Math.round((totalTokens / 1000) * rate * 1_000_000);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (tt === "per_run") {
|
||||
const rate =
|
||||
costPerRunOverrides[row.provider] ??
|
||||
DEFAULT_COST_PER_RUN[row.provider] ??
|
||||
null;
|
||||
if (rate !== null) return Math.round(rate * row.request_count * 1_000_000);
|
||||
return null;
|
||||
}
|
||||
return row.total_cost_microdollars > 0 ? row.total_cost_microdollars : null;
|
||||
}
|
||||
|
||||
function trackingValue(row: ProviderCostSummary) {
|
||||
const tt = row.tracking_type || "per_run";
|
||||
if (tt === "cost_usd") return formatMicrodollars(row.total_cost_microdollars);
|
||||
if (tt === "tokens")
|
||||
return formatTokens(row.total_input_tokens + row.total_output_tokens);
|
||||
if (
|
||||
tt === "duration_seconds" ||
|
||||
tt === "sandbox_seconds" ||
|
||||
tt === "walltime_seconds"
|
||||
)
|
||||
return formatDuration(row.total_duration_seconds || 0);
|
||||
if (tt === "characters")
|
||||
return formatTokens(row.total_input_tokens + row.total_output_tokens);
|
||||
return row.request_count.toLocaleString() + " runs";
|
||||
}
|
||||
|
||||
function PlatformCostContent({ searchParams }: Props) {
|
||||
const router = useRouter();
|
||||
const urlParams = useSearchParams();
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import type { ProviderCostSummary } from "@/app/api/__generated__/models/providerCostSummary";
|
||||
|
||||
export const DEFAULT_COST_PER_RUN: Record<string, number> = {
|
||||
google_maps: 0.032,
|
||||
ideogram: 0.08,
|
||||
nvidia: 0.0,
|
||||
screenshotone: 0.01,
|
||||
zerobounce: 0.008,
|
||||
mem0: 0.01,
|
||||
openweathermap: 0.0,
|
||||
webshare_proxy: 0.0,
|
||||
};
|
||||
|
||||
export const DEFAULT_COST_PER_1K_TOKENS: Record<string, number> = {
|
||||
openai: 0.005,
|
||||
anthropic: 0.008,
|
||||
groq: 0.0003,
|
||||
ollama: 0.0,
|
||||
};
|
||||
|
||||
export function toDateOrUndefined(val?: string): Date | undefined {
|
||||
if (!val) return undefined;
|
||||
const d = new Date(val);
|
||||
return isNaN(d.getTime()) ? undefined : d;
|
||||
}
|
||||
|
||||
export function formatMicrodollars(microdollars: number) {
|
||||
return `$${(microdollars / 1_000_000).toFixed(4)}`;
|
||||
}
|
||||
|
||||
export function formatTokens(tokens: number) {
|
||||
if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(1)}M`;
|
||||
if (tokens >= 1_000) return `${(tokens / 1_000).toFixed(1)}K`;
|
||||
return tokens.toString();
|
||||
}
|
||||
|
||||
export function formatDuration(seconds: number) {
|
||||
if (seconds >= 3600) return `${(seconds / 3600).toFixed(1)}h`;
|
||||
if (seconds >= 60) return `${(seconds / 60).toFixed(1)}m`;
|
||||
return `${seconds.toFixed(1)}s`;
|
||||
}
|
||||
|
||||
export function estimateCostForRow(
|
||||
row: ProviderCostSummary,
|
||||
costPerRunOverrides: Record<string, number>,
|
||||
) {
|
||||
const tt = row.tracking_type || "per_run";
|
||||
if (tt === "cost_usd") return row.total_cost_microdollars;
|
||||
if (tt === "tokens") {
|
||||
if (row.total_cost_microdollars > 0) return row.total_cost_microdollars;
|
||||
const rate = DEFAULT_COST_PER_1K_TOKENS[row.provider] ?? null;
|
||||
if (rate !== null) {
|
||||
const totalTokens = row.total_input_tokens + row.total_output_tokens;
|
||||
return Math.round((totalTokens / 1000) * rate * 1_000_000);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (tt === "per_run") {
|
||||
const rate =
|
||||
costPerRunOverrides[row.provider] ??
|
||||
DEFAULT_COST_PER_RUN[row.provider] ??
|
||||
null;
|
||||
if (rate !== null) return Math.round(rate * row.request_count * 1_000_000);
|
||||
return null;
|
||||
}
|
||||
return row.total_cost_microdollars > 0 ? row.total_cost_microdollars : null;
|
||||
}
|
||||
|
||||
export function trackingValue(row: ProviderCostSummary) {
|
||||
const tt = row.tracking_type || "per_run";
|
||||
if (tt === "cost_usd") return formatMicrodollars(row.total_cost_microdollars);
|
||||
if (tt === "tokens")
|
||||
return formatTokens(row.total_input_tokens + row.total_output_tokens);
|
||||
if (
|
||||
tt === "duration_seconds" ||
|
||||
tt === "sandbox_seconds" ||
|
||||
tt === "walltime_seconds"
|
||||
)
|
||||
return formatDuration(row.total_duration_seconds || 0);
|
||||
if (tt === "characters")
|
||||
return formatTokens(row.total_input_tokens + row.total_output_tokens);
|
||||
return row.request_count.toLocaleString() + " runs";
|
||||
}
|
||||
Reference in New Issue
Block a user