From f10b6c8476a3f834a3ad0456e3fa215a675ede54 Mon Sep 17 00:00:00 2001 From: sonwr Date: Tue, 3 Mar 2026 11:08:46 +0900 Subject: [PATCH] fix: normalize timezone-aware usage date labels --- plugins/claude/plugin.js | 5 ++ plugins/claude/plugin.test.js | 57 +++++++++++++++++++++++ plugins/codex/plugin.js | 5 ++ plugins/codex/plugin.test.js | 87 +++++++++++++++++++++++++++++++++++ 4 files changed, 154 insertions(+) diff --git a/plugins/claude/plugin.js b/plugins/claude/plugin.js index 991d233c..9d0b8fd3 100644 --- a/plugins/claude/plugin.js +++ b/plugins/claude/plugin.js @@ -332,6 +332,11 @@ return isoMatch[1] + "-" + isoMatch[2] + "-" + isoMatch[3] } + const isoDatePrefixMatch = value.match(/^(\d{4})-(\d{2})-(\d{2})(?:[Tt\s]|$)/) + if (isoDatePrefixMatch) { + return isoDatePrefixMatch[1] + "-" + isoDatePrefixMatch[2] + "-" + isoDatePrefixMatch[3] + } + const compactMatch = value.match(/^(\d{4})(\d{2})(\d{2})$/) if (compactMatch) { return compactMatch[1] + "-" + compactMatch[2] + "-" + compactMatch[3] diff --git a/plugins/claude/plugin.test.js b/plugins/claude/plugin.test.js index c57f3168..de46aad2 100644 --- a/plugins/claude/plugin.test.js +++ b/plugins/claude/plugin.test.js @@ -865,6 +865,63 @@ describe("claude plugin", () => { expect(yesterdayLine.value).toContain("$0.60") }) + it("matches UTC timestamp day keys at month boundary (regression)", async () => { + vi.useFakeTimers() + vi.setSystemTime(new Date(2026, 2, 1, 12, 0, 0)) + try { + const ctx = makeProbeCtx({ + ccusageResult: okUsage([ + { date: "2026-03-01T12:00:00Z", inputTokens: 10, outputTokens: 0, cacheCreationTokens: 0, cacheReadTokens: 0, totalTokens: 10, totalCost: 0.1 }, + ]), + }) + const plugin = await loadPlugin() + const result = plugin.probe(ctx) + const todayLine = result.lines.find((l) => l.label === "Today") + expect(todayLine).toBeTruthy() + expect(todayLine.value).toContain("10 tokens") + } finally { + vi.useRealTimers() + } + }) + + it("matches UTC+9 timestamp day keys at month boundary (regression)", async () => { + vi.useFakeTimers() + vi.setSystemTime(new Date(2026, 2, 1, 12, 0, 0)) + try { + const ctx = makeProbeCtx({ + ccusageResult: okUsage([ + { date: "2026-03-01T00:30:00+09:00", inputTokens: 20, outputTokens: 0, cacheCreationTokens: 0, cacheReadTokens: 0, totalTokens: 20, totalCost: 0.2 }, + ]), + }) + const plugin = await loadPlugin() + const result = plugin.probe(ctx) + const todayLine = result.lines.find((l) => l.label === "Today") + expect(todayLine).toBeTruthy() + expect(todayLine.value).toContain("20 tokens") + } finally { + vi.useRealTimers() + } + }) + + it("matches UTC-8 timestamp day keys at day boundary (regression)", async () => { + vi.useFakeTimers() + vi.setSystemTime(new Date(2026, 2, 1, 12, 0, 0)) + try { + const ctx = makeProbeCtx({ + ccusageResult: okUsage([ + { date: "2026-03-01T23:30:00-08:00", inputTokens: 30, outputTokens: 0, cacheCreationTokens: 0, cacheReadTokens: 0, totalTokens: 30, totalCost: 0.3 }, + ]), + }) + const plugin = await loadPlugin() + const result = plugin.probe(ctx) + const todayLine = result.lines.find((l) => l.label === "Today") + expect(todayLine).toBeTruthy() + expect(todayLine.value).toContain("30 tokens") + } finally { + vi.useRealTimers() + } + }) + it("adds Last 30 Days line summing all daily entries", async () => { const todayKey = localDayKey(new Date()) const ctx = makeProbeCtx({ diff --git a/plugins/codex/plugin.js b/plugins/codex/plugin.js index bfa39aab..99d75c8b 100644 --- a/plugins/codex/plugin.js +++ b/plugins/codex/plugin.js @@ -356,6 +356,11 @@ return isoMatch[1] + "-" + isoMatch[2] + "-" + isoMatch[3] } + const isoDatePrefixMatch = value.match(/^(\d{4})-(\d{2})-(\d{2})(?:[Tt\s]|$)/) + if (isoDatePrefixMatch) { + return isoDatePrefixMatch[1] + "-" + isoDatePrefixMatch[2] + "-" + isoDatePrefixMatch[3] + } + const compactMatch = value.match(/^(\d{4})(\d{2})(\d{2})$/) if (compactMatch) { return compactMatch[1] + "-" + compactMatch[2] + "-" + compactMatch[3] diff --git a/plugins/codex/plugin.test.js b/plugins/codex/plugin.test.js index 0f42a20d..621e319a 100644 --- a/plugins/codex/plugin.test.js +++ b/plugins/codex/plugin.test.js @@ -450,6 +450,93 @@ describe("codex plugin", () => { expect(yesterdayLine.value).toContain("$1.10") }) + it("matches UTC timestamp day keys at month boundary (regression)", async () => { + vi.useFakeTimers() + vi.setSystemTime(new Date(2026, 2, 1, 12, 0, 0)) + try { + const ctx = makeCtx() + ctx.host.fs.writeText("~/.codex/auth.json", JSON.stringify({ + tokens: { access_token: "token" }, + last_refresh: new Date().toISOString(), + })) + ctx.host.http.request.mockReturnValue({ + status: 200, + headers: { "x-codex-primary-used-percent": "10" }, + bodyText: JSON.stringify({}), + }) + ctx.host.ccusage.query.mockReturnValue({ + status: "ok", + data: { daily: [{ date: "2026-03-01T12:00:00Z", totalTokens: 10, costUSD: 0.1 }] }, + }) + + const plugin = await loadPlugin() + const result = plugin.probe(ctx) + const todayLine = result.lines.find((line) => line.label === "Today") + expect(todayLine).toBeTruthy() + expect(todayLine.value).toContain("10 tokens") + } finally { + vi.useRealTimers() + } + }) + + it("matches UTC+9 timestamp day keys at month boundary (regression)", async () => { + vi.useFakeTimers() + vi.setSystemTime(new Date(2026, 2, 1, 12, 0, 0)) + try { + const ctx = makeCtx() + ctx.host.fs.writeText("~/.codex/auth.json", JSON.stringify({ + tokens: { access_token: "token" }, + last_refresh: new Date().toISOString(), + })) + ctx.host.http.request.mockReturnValue({ + status: 200, + headers: { "x-codex-primary-used-percent": "10" }, + bodyText: JSON.stringify({}), + }) + ctx.host.ccusage.query.mockReturnValue({ + status: "ok", + data: { daily: [{ date: "2026-03-01T00:30:00+09:00", totalTokens: 20, costUSD: 0.2 }] }, + }) + + const plugin = await loadPlugin() + const result = plugin.probe(ctx) + const todayLine = result.lines.find((line) => line.label === "Today") + expect(todayLine).toBeTruthy() + expect(todayLine.value).toContain("20 tokens") + } finally { + vi.useRealTimers() + } + }) + + it("matches UTC-8 timestamp day keys at day boundary (regression)", async () => { + vi.useFakeTimers() + vi.setSystemTime(new Date(2026, 2, 1, 12, 0, 0)) + try { + const ctx = makeCtx() + ctx.host.fs.writeText("~/.codex/auth.json", JSON.stringify({ + tokens: { access_token: "token" }, + last_refresh: new Date().toISOString(), + })) + ctx.host.http.request.mockReturnValue({ + status: 200, + headers: { "x-codex-primary-used-percent": "10" }, + bodyText: JSON.stringify({}), + }) + ctx.host.ccusage.query.mockReturnValue({ + status: "ok", + data: { daily: [{ date: "2026-03-01T23:30:00-08:00", totalTokens: 30, costUSD: 0.3 }] }, + }) + + const plugin = await loadPlugin() + const result = plugin.probe(ctx) + const todayLine = result.lines.find((line) => line.label === "Today") + expect(todayLine).toBeTruthy() + expect(todayLine.value).toContain("30 tokens") + } finally { + vi.useRealTimers() + } + }) + it("throws token expired when refresh fails", async () => { const ctx = makeCtx() ctx.host.fs.writeText("~/.codex/auth.json", JSON.stringify({