fix(ai): require member identity within a shared org for report routing, overlays, and coverage

Codex review round 7 on e90a72bdd flagged that broker usage-report
matching, header-overlay keying, and omp-usage coverage all treated a
matching organization as a sufficient match. Two Team members share the
org id while drawing on per-user pools, so the first same-org report
(or a lone sibling report) was handed to the wrong member. The org is
now a gate: within the same-org subset the member's own base identity
(account/email/project) must still match, with org-only entities (no
base identifiers) matching on the org alone when unambiguous. The
overlay merger (findMatchingReportIndex) had the identical same-org
flaw and receives the symmetric fix. Org-presence-mismatch semantics
are unchanged: org-scoped vs org-less stays fall-through/unreported,
and both-org-less keeps the legacy base-identity fallback.
This commit is contained in:
chan1103
2026-07-11 19:01:57 +09:00
parent 45203a1b56
commit e095af3be0
6 changed files with 210 additions and 30 deletions
@@ -163,6 +163,34 @@ describe("collectUnreportedAccounts", () => {
const orglessAccounts: UsageAccountIdentity[] = [{ provider: "anthropic", type: "oauth", email: shared }];
expect(collectUnreportedAccounts([orglessReport], orglessAccounts)).toEqual([]);
});
it("gates same-org coverage on the member's own identity", () => {
const org = "org-team";
const alice: UsageAccountIdentity = {
provider: "anthropic",
type: "oauth",
email: "alice@example.test",
accountId: "account-alice",
orgId: org,
};
const bob: UsageAccountIdentity = {
provider: "anthropic",
type: "oauth",
email: "bob@example.test",
accountId: "account-bob",
orgId: org,
};
const orgOnly: UsageAccountIdentity = { provider: "anthropic", type: "oauth", orgId: org };
const aliceReport = {
...makeReport("anthropic", alice.email!, []),
metadata: { email: alice.email, accountId: alice.accountId, orgId: org },
};
// Alice reported, Bob not: the sibling's same-org report must not count
// as Bob's coverage — two Team members share the org id but draw on
// per-user pools. An org-only account (no base identifiers to gate on)
// stays covered by any same-org report.
expect(collectUnreportedAccounts([aliceReport], [alice, bob, orgOnly])).toEqual([bob]);
});
});
describe("formatUsageBreakdown", () => {