Skip to content
Draft
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
2 changes: 1 addition & 1 deletion src/commands/issue/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.`]
);
}
Expand Down
6 changes: 5 additions & 1 deletion src/commands/release/set-commits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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). " +
Expand Down
6 changes: 5 additions & 1 deletion src/types/seer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Expand Down
2 changes: 1 addition & 1 deletion test/commands/issue/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
68 changes: 67 additions & 1 deletion test/commands/release/set-commits.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -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(
Expand Down
38 changes: 38 additions & 0 deletions test/types/seer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading