Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions packages/cli/src/commands/event/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -703,6 +703,10 @@ async function tryEventFallbacks(
// Track whether the search completed so we can skip the org in cross-org
// only when we got a definitive "not found" (not a transient failure).
let sameOrgSearched = false;
// Track rate-limit separately: a 429 means the org is reachable but throttled.
// Retrying it immediately in the cross-org fallback would just hit the same
// limit again, producing consecutive identical HTTP requests (CLI-2Y1).
let orgRateLimited = false;
try {
const resolved = await resolveEventInOrg(org, eventId);
sameOrgSearched = true;
Expand All @@ -717,15 +721,20 @@ async function tryEventFallbacks(
if (sameOrgError instanceof AuthError) {
throw sameOrgError;
}
// Transient failure — don't mark org as searched so cross-org retries it
if (sameOrgError instanceof ApiError && sameOrgError.status === 429) {
// Rate-limited — exclude the org from cross-org search; an immediate
// retry against the same endpoint would hit the same limit.
orgRateLimited = true;
}
logger.debug("Same-org event lookup failed", sameOrgError);
}

// Cross-org fallback: the event may exist in a different organization.
// Only exclude the org if the same-org search completed successfully
// (returned null). If it threw a transient error, let cross-org retry it.
// Exclude the org when the same-org search completed (returned null) OR
// when it was rate-limited — either way, re-querying it immediately is futile.
try {
const crossOrg = await findEventAcrossOrgs(eventId, {
excludeOrgs: sameOrgSearched ? [org] : undefined,
excludeOrgs: sameOrgSearched || orgRateLimited ? [org] : undefined,
});
if (crossOrg) {
// Use project-scoped phrasing when found in same org (different project)
Expand Down
23 changes: 23 additions & 0 deletions packages/cli/test/commands/event/view.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1370,6 +1370,29 @@ describe("fetchEventWithContext", () => {
});
});

test("cross-org excludes org when same-org search was rate-limited (429)", async () => {
vi.spyOn(apiClient, "getEvent").mockRejectedValue(
new ApiError("Not found", 404)
);
// Same-org search hit a rate limit — retrying it immediately is futile (CLI-2Y1)
vi.spyOn(apiClient, "resolveEventInOrg").mockRejectedValue(
new ApiError("Too Many Requests", 429)
);
const findSpy = vi
.spyOn(apiClient, "findEventAcrossOrgs")
.mockResolvedValue(null);

await expect(
fetchEventWithContext(null, "my-org", "my-project", "abc123")
).rejects.toThrow(ResolutionError);

// org must be excluded — re-querying a rate-limited endpoint immediately
// produces consecutive identical HTTP requests (the Consecutive HTTP issue).
expect(findSpy).toHaveBeenCalledWith("abc123", {
excludeOrgs: ["my-org"],
});
});

test("swallows non-auth cross-org errors and throws ResolutionError", async () => {
vi.spyOn(apiClient, "getEvent").mockRejectedValue(
new ApiError("Not found", 404)
Expand Down
Loading