fix(coding-agent): suppressed credential disable tombstones for active accounts

- Update isActionableDisable in usage-cli.ts to accept active accounts and check identity matches.
- Suppress credential disable tombstones when an active account exists for the same provider and identity.
- Add test coverage for suppressing tombstones when active accounts share the same identity.
This commit is contained in:
can1357
2026-07-28 11:47:08 +02:00
parent b778617178
commit a10fe079ce
2 changed files with 58 additions and 4 deletions
+29 -4
View File
@@ -560,9 +560,34 @@ function formatReloginDeadline(
* automatically (refresh failure, upstream invalidation). Rows the user
* replaced or deleted deliberately are lifecycle noise, not lost capacity.
*/
function isActionableDisable(summary: DisabledCredentialSummary): boolean {
function isActionableDisable(summary: DisabledCredentialSummary, activeAccounts: UsageAccountIdentity[] = []): boolean {
if (summary.type !== "oauth") return false;
return !/^(replaced by|deleted by user)/i.test(summary.cause);
if (/^(replaced by|deleted by user)/i.test(summary.cause)) return false;
// Do not display tombstone if there is an active account for the same provider
// matching the same identity (email, accountId, or org).
const summaryEmail = summary.email?.toLowerCase();
const summaryAccountId = summary.accountId?.toLowerCase();
const summaryOrgId = summary.orgId?.toLowerCase();
const matchesActive = activeAccounts.some(account => {
if (account.provider !== summary.provider) return false;
const accountEmail = account.email?.toLowerCase();
const accountAccountId = account.accountId?.toLowerCase();
const accountOrgId = account.orgId?.toLowerCase();
// If email or accountId match, it's the same identity
if (summaryEmail && accountEmail && summaryEmail === accountEmail) return true;
if (summaryAccountId && accountAccountId && summaryAccountId === accountAccountId) return true;
// Fallback: if orgId matches and neither email nor accountId contradicts
if (summaryOrgId && accountOrgId && summaryOrgId === accountOrgId) return true;
return false;
});
return !matchesActive;
}
/** Human-sized disable cause: the upstream `error_description` when embedded, else the first clause. */
@@ -610,7 +635,7 @@ export function formatUsageBreakdown(
}
const disabledByProvider = new Map<string, DisabledCredentialSummary[]>();
for (const summary of disabled) {
if (!isActionableDisable(summary)) continue;
if (!isActionableDisable(summary, accounts)) continue;
const list = disabledByProvider.get(summary.provider) ?? [];
list.push(summary);
disabledByProvider.set(summary.provider, list);
@@ -1018,7 +1043,7 @@ export async function runUsageCommand(cmd: UsageCommandArgs): Promise<void> {
const stats = computeProviderWindowStats(filteredReports.filter(peer => peer.provider === report.provider));
if (stats.length > 0) capacity[report.provider] = stats;
}
let disabledForJson = disabled.filter(isActionableDisable);
let disabledForJson = disabled.filter(summary => isActionableDisable(summary, accounts));
if (redaction) {
disabledForJson = disabledForJson.map(summary => ({
...summary,
@@ -352,6 +352,35 @@ describe("formatUsageBreakdown", () => {
expect(text).not.toContain("rotated@example.test");
expect(text).not.toContain("Fireworks");
});
it("suppresses auto-disabled tombstones when an active account exists with the same identity", () => {
const now = Date.now();
const activeAccounts: UsageAccountIdentity[] = [
{
provider: "anthropic",
type: "oauth",
email: "active@example.test",
},
];
const disabled = [
{
id: 30,
provider: "anthropic",
type: "oauth" as const,
email: "active@example.test",
cause: "oauth refresh failed: Refresh token expired",
},
{
id: 31,
provider: "anthropic",
type: "oauth" as const,
email: "truly-dead@example.test",
cause: "oauth refresh failed: Refresh token expired",
},
];
const text = stripVTControlCharacters(formatUsageBreakdown([], activeAccounts, now, undefined, disabled));
expect(text).not.toContain("active@example.test — disabled");
expect(text).toContain("✗ truly-dead@example.test — disabled");
});
it("renders a tombstone-only provider section even when no active credential remains", () => {
const disabled = [