feat(ai): added usage history tracking and trend reporting for account limits

- Added usage snapshot persistence in sqlite with hour-bucket upsert behavior.
- Added listUsageHistory query support with optional provider and sinceMs filters.
- Added usage CLI history mode with `--history` and `--days` and trend rendering.
- Added changelog documentation for usage trend inspection and no-history exit behavior.
This commit is contained in:
can1357
2026-06-12 05:26:03 +02:00
parent be31ce3098
commit 579a005a04
8 changed files with 669 additions and 19 deletions
@@ -6,6 +6,7 @@ import {
collectUnreportedAccounts,
computeProviderWindowStats,
formatUsageBreakdown,
formatUsageHistory,
type UsageAccountIdentity,
} from "@oh-my-pi/pi-coding-agent/cli/usage-cli";
@@ -184,3 +185,47 @@ describe("formatUsageBreakdown", () => {
for (const mask of redaction.values()) expect(text).toContain(mask);
});
});
describe("formatUsageHistory", () => {
const NOW = Date.now();
const SINCE = NOW - 7 * 24 * HOUR;
function historyEntry(recordedAt: number, usedFraction: number | undefined, overrides?: Record<string, unknown>) {
return {
recordedAt,
provider: "anthropic",
accountKey: "oauth|email:dummy.primary@example.test",
email: "dummy.primary@example.test",
limitId: "anthropic:5h",
label: "Session",
windowLabel: "5 Hour",
usedFraction,
status: "ok" as const,
...overrides,
};
}
const entries = [
historyEntry(SINCE + HOUR, 0.2),
historyEntry(SINCE + 30 * HOUR, 0.95),
historyEntry(NOW - HOUR, 0.4),
];
it("renders one series per account window with latest and peak percentages", () => {
const text = stripVTControlCharacters(formatUsageHistory(entries, SINCE, NOW));
expect(text).toContain("Anthropic");
expect(text).toContain("dummy.primary@example.test");
// Window label is appended when the limit label doesn't carry it.
expect(text).toContain("Session (5 Hour)");
expect(text).toContain("latest 40.0%");
expect(text).toContain("peak 95.0%");
expect(text).toContain("3 snapshots");
});
it("redacts account labels through the provided map", () => {
const redaction = buildRedactionMap(["dummy.primary@example.test"]);
const text = stripVTControlCharacters(formatUsageHistory(entries, SINCE, NOW, redaction));
expect(text).not.toContain("dummy.primary@example.test");
expect(text).toContain("du*");
});
});