Files
fusion/packages/dashboard/app/utils/__tests__/taskTokenCost.test.ts
gsxdsm d4aa79b66c FN-8598: preserve legacy task cost badges
Restore cost badges for tasks with valid legacy token totals.

- Preserve usage records when optional timestamps and cache-write totals are absent
- Use task creation time to satisfy legacy usage timestamp requirements
- Cover card badge rendering, unpriced mixed usage, and mobile visibility
- Add a patch changeset for the restored badge behavior

Files changed:
 .changeset/fn-8598-cost-badge-fix.md               |   7 +
 .../task-token-usage-serialization.test.ts         |  45 +++++++
 packages/core/src/task-store/serialization.ts      |  16 ++-
 .../__tests__/TaskCard.cost-badge.test.tsx         | 146 +++++++++++++++++++++
 .../app/utils/__tests__/taskTokenCost.test.ts      |  11 ++
 5 files changed, 221 insertions(+), 4 deletions(-)

Fusion-Task-Id: FN-8598

Fusion-Task-Lineage: 83fb4051-8e0f-4ee9-9f00-0e4d5cb8661e

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-26 09:06:41 -07:00

74 lines
3.7 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type { TaskDetail, TaskTokenUsage } from "@fusion/core";
import { buildTokenCostRows, formatCost, hasTaskCost, taskTotalCost, totalCostForRows } from "../taskTokenCost";
function usage(overrides: Partial<TaskTokenUsage> = {}): TaskTokenUsage {
return {
inputTokens: 0,
outputTokens: 0,
cachedTokens: 0,
cacheWriteTokens: 0,
totalTokens: 0,
firstUsedAt: "2026-01-01T00:00:00Z",
lastUsedAt: "2026-01-01T00:00:00Z",
...overrides,
};
}
function task(tokenUsage?: TaskTokenUsage): TaskDetail {
return { id: "FN-7820", title: "Cost", column: "done", steps: [], dependencies: [], tokenUsage } as TaskDetail;
}
describe("taskTokenCost", () => {
it("prices and merges per-model buckets", () => {
const rows = buildTokenCostRows(task(usage({
perModel: [
{ modelProvider: "openai", modelId: "gpt-5-mini", inputTokens: 1_000_000, outputTokens: 1_000_000, cachedTokens: 0, cacheWriteTokens: 0, totalTokens: 2_000_000 },
{ modelProvider: "openai", modelId: "gpt-5-mini", inputTokens: 500_000, outputTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, totalTokens: 500_000 },
],
})), "(unknown)");
expect(rows).toHaveLength(1);
expect(rows[0]).toMatchObject({ modelProvider: "openai", modelId: "gpt-5-mini", inputTokens: 1_500_000, outputTokens: 1_000_000, totalTokens: 2_500_000 });
expect(formatCost(rows[0].cost.usd, rows[0].cost.unavailable)).toBe("$2.38");
expect(formatCost(totalCostForRows(rows).usd, totalCostForRows(rows).unavailable)).toBe("$2.38");
});
it("prices aggregate-only fallback usage", () => {
const result = taskTotalCost(task(usage({ modelProvider: "openai", modelId: "gpt-5-mini", inputTokens: 1_000_000, outputTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, totalTokens: 1_000_000 })));
expect(formatCost(result.usd, result.unavailable)).toBe("$0.25");
});
it("marks unpriceable positive-token rows and totals unavailable", () => {
const rows = buildTokenCostRows(task(usage({ modelProvider: "unknown", modelId: "no-price", inputTokens: 10, outputTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, totalTokens: 10 })), "(unknown)");
expect(rows[0].cost.unavailable).toBe(true);
expect(formatCost(rows[0].cost.usd, rows[0].cost.unavailable)).toBe("—");
expect(formatCost(totalCostForRows(rows).usd, totalCostForRows(rows).unavailable)).toBe("—");
});
it("keeps mixed priced and unpriced per-model usage unavailable", () => {
const total = taskTotalCost(task(usage({
perModel: [
{ modelProvider: "openai", modelId: "gpt-5-mini", inputTokens: 1_000_000, outputTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, totalTokens: 1_000_000 },
{ modelProvider: "unknown", modelId: "no-price", inputTokens: 1, outputTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, totalTokens: 1 },
],
})));
expect(formatCost(total.usd, total.unavailable)).toBe("—");
});
it("does not fabricate cost for zero usage", () => {
const zeroTask = task(usage({ modelProvider: "unknown", modelId: "no-price" }));
const total = taskTotalCost(zeroTask);
expect(hasTaskCost(zeroTask)).toBe(false);
expect(formatCost(total.usd, total.unavailable)).toBe("—");
});
it("detects positive task cost availability without requiring pricing", () => {
expect(hasTaskCost(task())).toBe(false);
expect(hasTaskCost(task(usage()))).toBe(false);
expect(hasTaskCost(task(usage({ totalTokens: 1 })))).toBe(true);
expect(hasTaskCost(task(usage({ perModel: [{ modelProvider: "x", modelId: "y", inputTokens: 1, outputTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, totalTokens: 1 }] })))).toBe(true);
});
});