fix(ai): org-qualify account/project fallback identities; org-decisive routing on either side

Addresses the fourth review round (Codex no-email finding on d37e3992c,
confirmed and scoped by internal review):

- resolveProviderCredentialIdentityKey: the anthropic org qualifier now
  rides on whichever base identity exists (email > account > project),
  not only email. The account UUID is identical across the orgs of one
  login account, so the bare account fallback let a second subscription
  replace the first whenever the email could not be recovered (token
  response omits it AND bootstrap fails). Org-only credentials key on
  the org alone instead of losing identity entirely.
- matchesReplacementCredential: the one-way legacy claim strips a
  trailing |org: from ANY anthropic base key (account/project included);
  only anthropic keys carry the qualifier, so other providers are
  unaffected.
- Usage-report dedupe falls back to the org-qualified account for
  no-email anthropic reports instead of returning no identifiers.
- Broker report/overlay routing (matchUsageReport/findMatchingReportIndex)
  is org-decisive on EITHER side: an org-less legacy credential no longer
  receives an org-attributed sibling's pool via the lone-candidate or
  email/account fallback, and an org-less overlay only merges into
  org-less reports.
- omp usage unreported-account attribution follows the same either-side
  rule, so a legacy row whose fetch failed surfaces as 'no usage data'
  instead of being hidden by a sibling's report.
- Regression tests: no-email identity coexistence/replace/claim, no-email
  report dedupe, org-less broker routing, either-side unreported
  attribution.
This commit is contained in:
chan1103
2026-07-11 18:27:34 +09:00
parent 3df97d5097
commit c2456d882f
8 changed files with 189 additions and 48 deletions
@@ -139,6 +139,30 @@ describe("collectUnreportedAccounts", () => {
const unreported = collectUnreportedAccounts(anonymous, accounts);
expect(unreported).toEqual([{ provider: "cerebras", type: "api_key" }]);
});
it("attributes org-decisively when either side carries an org", () => {
const shared = "shared@example.test";
const orgAccounts: UsageAccountIdentity[] = [
{ provider: "anthropic", type: "oauth", email: shared, orgId: "org-team" },
{ provider: "anthropic", type: "oauth", email: shared, orgId: "org-max" },
{ provider: "anthropic", type: "oauth", email: shared },
];
const teamReport = {
...makeReport("anthropic", shared, []),
metadata: { email: shared, orgId: "org-team" },
};
// Only the Team org reported: Max and the org-less legacy row must both
// surface as unreported despite the shared email.
const unreported = collectUnreportedAccounts([teamReport], orgAccounts);
expect(unreported).toEqual([
{ provider: "anthropic", type: "oauth", email: shared, orgId: "org-max" },
{ provider: "anthropic", type: "oauth", email: shared },
]);
// Both sides org-less: the email fallback still covers the account.
const orglessReport = { ...makeReport("anthropic", shared, []), metadata: { email: shared } };
const orglessAccounts: UsageAccountIdentity[] = [{ provider: "anthropic", type: "oauth", email: shared }];
expect(collectUnreportedAccounts([orglessReport], orglessAccounts)).toEqual([]);
});
});
describe("formatUsageBreakdown", () => {