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:
@@ -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 = [
|
||||
|
||||
Reference in New Issue
Block a user