From 222f9cba28916a4508303bd6805534f724339323 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 25 May 2026 12:10:44 +0000 Subject: [PATCH 1/3] fix(seer): extractRootCauses empty causes array blocks agent artifact fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit searchContainersForRootCauses returned early when container.causes was an empty array ([]) because [] is truthy in JavaScript. This prevented fallthrough to the agent artifact format (searchBlocksForAgentRootCause), causing 'no root causes found' errors when the API returned empty legacy causes alongside valid agent root_cause artifacts. Added length check so empty arrays fall through to agent format. Co-authored-by: Miguel Betegón --- src/types/seer.ts | 6 +++++- test/types/seer.test.ts | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/types/seer.ts b/src/types/seer.ts index 74fb3193b..a5a7da43b 100644 --- a/src/types/seer.ts +++ b/src/types/seer.ts @@ -253,7 +253,11 @@ function searchContainersForRootCauses( containers: WithCauses[] ): RootCause[] | null { for (const container of containers) { - if (container.key === "root_cause_analysis" && container.causes) { + if ( + container.key === "root_cause_analysis" && + container.causes && + container.causes.length > 0 + ) { return container.causes; } } diff --git a/test/types/seer.test.ts b/test/types/seer.test.ts index db049216f..b89dabd27 100644 --- a/test/types/seer.test.ts +++ b/test/types/seer.test.ts @@ -226,6 +226,44 @@ describe("extractRootCauses", () => { expect(causes[0]?.relevant_repos).toEqual(["org/backend"]); }); + test("falls through to agent artifacts when legacy causes array is empty", () => { + const state = { + run_id: 789, + status: "COMPLETED", + blocks: [ + { + key: "root_cause_analysis", + status: "COMPLETED", + causes: [], + artifacts: [], + }, + { + id: "block-2", + message: { role: "assistant", content: "Found it" }, + timestamp: "2025-01-01T00:00:00Z", + artifacts: [ + { + key: "root_cause", + data: { + one_line_description: "Race condition in auth middleware", + five_whys: ["Token refresh not atomic"], + relevant_repo: "org/api-server", + }, + reason: "", + }, + ], + }, + ], + } as unknown as AutofixState; + + const causes = extractRootCauses(state); + expect(causes).toHaveLength(1); + expect(causes[0]?.description).toBe( + "Race condition in auth middleware" + ); + expect(causes[0]?.relevant_repos).toEqual(["org/api-server"]); + }); + test("extracts root cause from agent artifact without relevant_repo", () => { const state = { run_id: 101, From 9f48b68be6ff6abb84472933e49c038d23694734 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 25 May 2026 12:13:07 +0000 Subject: [PATCH 2/3] fix(release): set-commits default mode masks unrelated 400 errors as missing repo integration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit setCommitsDefault() caught any ApiError with status 400 from setCommitsAuto() and treated it as 'no repository integration'. However, setCommitsAuto() internally calls setCommitsWithRefs() which can also return HTTP 400 for unrelated reasons (invalid commit refs, bad release state, etc.). This caused: 1. A false 1-hour negative cache (cacheNoRepoIntegration) 2. Silent fallback to local git history 3. The real API error being hidden from the user Narrowed the catch to only match the specific 'No repository integrations' error message from setCommitsAuto's own check. Unrelated 400 errors now propagate to the user. Co-authored-by: Miguel Betegón --- src/commands/release/set-commits.ts | 6 +- test/commands/release/set-commits.test.ts | 68 ++++++++++++++++++++++- 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/src/commands/release/set-commits.ts b/src/commands/release/set-commits.ts index f1917b506..44c2b650c 100644 --- a/src/commands/release/set-commits.ts +++ b/src/commands/release/set-commits.ts @@ -134,7 +134,11 @@ async function setCommitsDefault( clearRepoIntegrationCache(org); return release; } catch (error) { - if (error instanceof ApiError && error.status === 400) { + if ( + error instanceof ApiError && + error.status === 400 && + error.message.includes("No repository integrations") + ) { cacheNoRepoIntegration(org); log.warn( "Could not auto-discover commits (no repository integration). " + diff --git a/test/commands/release/set-commits.test.ts b/test/commands/release/set-commits.test.ts index 1d9f7efee..4a20f0d89 100644 --- a/test/commands/release/set-commits.test.ts +++ b/test/commands/release/set-commits.test.ts @@ -21,7 +21,7 @@ vi.mock("../../../src/lib/api-client.js", async (importOriginal) => { // biome-ignore lint/performance/noNamespaceImport: needed for spyOn mocking import * as apiClient from "../../../src/lib/api-client.js"; -import { ValidationError } from "../../../src/lib/errors.js"; +import { ApiError, ValidationError } from "../../../src/lib/errors.js"; vi.mock("../../../src/lib/resolve-target.js", async (importOriginal) => { const actual = @@ -265,6 +265,72 @@ describe("release set-commits (default mode)", () => { resolveOrgSpy.mockRestore(); }); + test("propagates unrelated 400 errors from setCommitsAuto", async () => { + resolveOrgSpy.mockResolvedValue({ org: "my-org" }); + setCommitsAutoSpy.mockRejectedValue( + new ApiError("Invalid commit SHA.", 400, undefined, "releases/1.0.0/") + ); + + const repoRoot = new URL("../../..", import.meta.url).pathname.replace( + /\/$/, + "" + ); + const { context } = createMockContext(repoRoot); + const func = await setCommitsCommand.loader(); + await expect( + func.call( + context, + { + auto: false, + local: false, + clear: false, + commit: undefined, + "initial-depth": 20, + json: true, + }, + "1.0.0" + ) + ).rejects.toThrow("Invalid commit SHA."); + + expect(setCommitsAutoSpy).toHaveBeenCalled(); + expect(setCommitsLocalSpy).not.toHaveBeenCalled(); + }); + + test("falls back to local only on 'No repository integrations' 400", async () => { + resolveOrgSpy.mockResolvedValue({ org: "my-org" }); + setCommitsAutoSpy.mockRejectedValue( + new ApiError( + "No repository integrations configured for this organization.", + 400, + undefined, + "releases/1.0.0/" + ) + ); + setCommitsLocalSpy.mockResolvedValue(sampleRelease); + + const repoRoot = new URL("../../..", import.meta.url).pathname.replace( + /\/$/, + "" + ); + const { context } = createMockContext(repoRoot); + const func = await setCommitsCommand.loader(); + await func.call( + context, + { + auto: false, + local: false, + clear: false, + commit: undefined, + "initial-depth": 20, + json: true, + }, + "1.0.0" + ); + + expect(setCommitsAutoSpy).toHaveBeenCalled(); + expect(setCommitsLocalSpy).toHaveBeenCalled(); + }); + test("falls back to local on ValidationError from auto", async () => { resolveOrgSpy.mockResolvedValue({ org: "my-org" }); setCommitsAutoSpy.mockRejectedValue( From 75a6733cd46310b96f327b2bc5651bf3999c596d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 25 May 2026 12:14:17 +0000 Subject: [PATCH 3/3] fix(issue): selector error hint suggests misleading is:resolved query MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When @latest or @most_frequent found no unresolved issues, the ResolutionError hint directed users to 'sentry issue list / -q "is:resolved"'. This was misleading because: 1. The selectors only work on unresolved issues 2. Listing resolved issues doesn't help find the issue they wanted 3. The suggested command can't be combined with the selector Changed the hint to 'sentry issue list /' (no filter) so users see all available issues and can determine what to do next. Co-authored-by: Miguel Betegón --- src/commands/issue/utils.ts | 2 +- test/commands/issue/utils.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/issue/utils.ts b/src/commands/issue/utils.ts index f2cbff001..07f035c6a 100644 --- a/src/commands/issue/utils.ts +++ b/src/commands/issue/utils.ts @@ -455,7 +455,7 @@ async function resolveSelector( throw new ResolutionError( `Selector '${selector}'`, "no unresolved issues found", - `sentry issue list ${orgSlug}/ -q "is:resolved"`, + `sentry issue list ${orgSlug}/`, [`The ${label} issue selector only matches unresolved issues.`] ); } diff --git a/test/commands/issue/utils.test.ts b/test/commands/issue/utils.test.ts index f105232f6..993044e03 100644 --- a/test/commands/issue/utils.test.ts +++ b/test/commands/issue/utils.test.ts @@ -1992,7 +1992,7 @@ describe("resolveOrgAndIssueId: magic @ selectors", () => { expect(String(err)).toContain("no unresolved issues found"); expect(String(err)).toContain("most recent"); expect(String(err)).toContain("sentry issue list"); - expect(String(err)).toContain('-q "is:resolved"'); + expect(String(err)).not.toContain("is:resolved"); }); test("throws ContextError when org cannot be resolved for bare @selector", async () => {