diff --git a/src/cli/access.ts b/src/cli/access.ts index 9cc0ec091a2..dc541db5ce2 100644 --- a/src/cli/access.ts +++ b/src/cli/access.ts @@ -38,6 +38,13 @@ const USAGE = `Usage: */ function formatKeyRows(payload: Record, keys: Array>): string[] { const cells: string[][] = [["ID", "NAME", "PREFIX", "REQ 7D", "TOTAL", "LAST USED"]]; + // A string that does not parse is not attribution data: treat it like an absent + // field so malformed payloads still render "unavailable" instead of usage values. + const attributionSince = typeof payload.attributionSince === "string" + && !Number.isNaN(Date.parse(payload.attributionSince)) + ? payload.attributionSince + : undefined; + const usageAvailable = attributionSince !== undefined; for (const entry of keys) { const usage = (entry.usage ?? {}) as Record; const ambiguous = usage.ambiguous === true; @@ -47,16 +54,16 @@ function formatKeyRows(payload: Record, keys: Array Math.max(...cells.map(row => (row[column] ?? "").length))); const lines = cells.map(row => row.map((cell, i) => (cell ?? "").padEnd(widths[i]!)).join(" ").trimEnd()); const footer: string[] = []; - if (typeof payload.attributionSince === "string") { - footer.push(`attribution since ${payload.attributionSince}`); + if (attributionSince !== undefined) { + footer.push(`attribution since ${attributionSince}`); } if (payload.historyTruncated === true) { footer.push("older history truncated"); diff --git a/tests/cli/cli-dto-fidelity.test.ts b/tests/cli/cli-dto-fidelity.test.ts index 67cc148795a..9cfca772c69 100644 --- a/tests/cli/cli-dto-fidelity.test.ts +++ b/tests/cli/cli-dto-fidelity.test.ts @@ -216,6 +216,7 @@ describe("#2705 access key usage columns", () => { id: "k_9f2a", name: "ci-runner", prefix: "ocx_data_abc...", usage: { requests7d: 1204, totalRequests: 18330, lastUsedAt: "2026-08-27T04:11:00Z" }, }], + attributionSince: "2026-07-29T00:00:00Z", }); expect(out).toContain("REQ 7D"); expect(out).toContain("1,204"); @@ -229,6 +230,7 @@ describe("#2705 access key usage columns", () => { // use is the dangerous answer for someone deciding what to delete. const out = await listOutput({ keys: [{ id: "k_11bd", name: "laptop", prefix: "ocx_data_def...", usage: { ambiguous: true } }], + attributionSince: "2026-07-29T00:00:00Z", }); expect(out).toContain("ambiguous"); expect(out).not.toMatch(/\b0\b/); @@ -237,10 +239,30 @@ describe("#2705 access key usage columns", () => { test("a never-used key says never rather than showing an empty cell", async () => { const out = await listOutput({ keys: [{ id: "k_new", name: "fresh", prefix: "ocx_data_ghi...", usage: { requests7d: 0, totalRequests: 0 } }], + attributionSince: "2026-08-29T00:00:00Z", }); expect(out).toContain("never"); }); + test("unavailable attribution does not report zero usage or never used", async () => { + const out = await listOutput({ + keys: [{ id: "k_unknown", name: "unknown", prefix: "ocx_data_jkl...", usage: { requests7d: 0, totalRequests: 0 } }], + }); + expect(out).toContain("unavailable"); + expect(out).not.toMatch(/\b0\b/); + expect(out).not.toContain("never"); + }); + + test("a malformed attributionSince string stays unavailable", async () => { + const out = await listOutput({ + keys: [{ id: "k_bad", name: "bad", prefix: "ocx_data_mno...", usage: { requests7d: 0, totalRequests: 0 } }], + attributionSince: "not-a-timestamp", + }); + expect(out).toContain("unavailable"); + expect(out).not.toContain("attribution since"); + expect(out).not.toMatch(/\b0\b/); + }); + test("dataset-level attribution and truncation print ONCE as a footer", async () => { // They describe the usage log, not a key. Without attributionSince an absent lastUsedAt is // unreadable: "never used" and "nothing attributable yet" look identical.