diff --git a/command/issues/issues.go b/command/issues/issues.go index d2a27956..378cd073 100644 --- a/command/issues/issues.go +++ b/command/issues/issues.go @@ -314,7 +314,11 @@ func (opts *IssuesOptions) resolveIssues(ctx context.Context, client *deepsource case ab.PRNumber > 0: opts.PRNumber = ab.PRNumber opts.CommitOid = ab.CommitOid - issuesList, err = client.GetPRIssues(ctx, remote.Owner, remote.RepoName, remote.VCSProvider, ab.PRNumber, prFilters) + if ab.Fallback { + issuesList, err = client.GetRunIssuesFlat(ctx, ab.CommitOid, serverFilters) + } else { + issuesList, err = client.GetPRIssues(ctx, remote.Owner, remote.RepoName, remote.VCSProvider, ab.PRNumber, prFilters) + } case ab.UseRepo: issuesList, err = client.GetIssues(ctx, remote.Owner, remote.RepoName, remote.VCSProvider) default: diff --git a/command/issues/tests/golden_files/get_analysis_runs_pr_fallback_completed_response.json b/command/issues/tests/golden_files/get_analysis_runs_pr_fallback_completed_response.json new file mode 100644 index 00000000..7abccc30 --- /dev/null +++ b/command/issues/tests/golden_files/get_analysis_runs_pr_fallback_completed_response.json @@ -0,0 +1,46 @@ +{ + "repository": { + "analysisRuns": { + "edges": [ + { + "node": { + "runUid": "run-uid-running-01", + "commitOid": "831701c7a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6", + "branchName": "feature/new-auth", + "status": "RUNNING", + "createdAt": "2025-03-12T14:00:00Z", + "finishedAt": null, + "updatedAt": "2025-03-12T14:01:00Z", + "summary": { + "occurrencesIntroduced": 0, + "occurrencesResolved": 0, + "occurrencesSuppressed": 0 + }, + "reportCard": null + } + }, + { + "node": { + "runUid": "run-uid-completed-01", + "commitOid": "862df9f2e3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8", + "branchName": "feature/new-auth", + "status": "SUCCESS", + "createdAt": "2025-03-12T12:00:00Z", + "finishedAt": "2025-03-12T12:05:00Z", + "updatedAt": "2025-03-12T12:05:00Z", + "summary": { + "occurrencesIntroduced": 2, + "occurrencesResolved": 0, + "occurrencesSuppressed": 0 + }, + "reportCard": null + } + } + ], + "pageInfo": { + "hasNextPage": false, + "endCursor": null + } + } + } +} diff --git a/command/issues/tests/golden_files/get_analysis_runs_pr_fallback_first_response.json b/command/issues/tests/golden_files/get_analysis_runs_pr_fallback_first_response.json new file mode 100644 index 00000000..a3ce4549 --- /dev/null +++ b/command/issues/tests/golden_files/get_analysis_runs_pr_fallback_first_response.json @@ -0,0 +1,29 @@ +{ + "repository": { + "analysisRuns": { + "edges": [ + { + "node": { + "runUid": "run-uid-running-01", + "commitOid": "831701c7a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6", + "branchName": "feature/new-auth", + "status": "RUNNING", + "createdAt": "2025-03-12T14:00:00Z", + "finishedAt": null, + "updatedAt": "2025-03-12T14:01:00Z", + "summary": { + "occurrencesIntroduced": 0, + "occurrencesResolved": 0, + "occurrencesSuppressed": 0 + }, + "reportCard": null + } + } + ], + "pageInfo": { + "hasNextPage": false, + "endCursor": null + } + } + } +} diff --git a/command/issues/tests/issues_test.go b/command/issues/tests/issues_test.go index 483ec226..a609ec3b 100644 --- a/command/issues/tests/issues_test.go +++ b/command/issues/tests/issues_test.go @@ -420,6 +420,104 @@ func TestIssuesMultipleFilters(t *testing.T) { } } +// TestIssuesPRFallbackUsesRunIssues verifies that when auto-branch resolution +// detects a PR AND the current run is in-progress (fallback), the code fetches +// issues via GetRunIssuesFlat (commit-scoped) instead of GetPRIssues. +// Regression test for ticket #6884175. +func TestIssuesPRFallbackUsesRunIssues(t *testing.T) { + cfgMgr := testutil.CreateTestConfigManager(t, "test-token", "deepsource.com", "test@example.com") + + // Load golden files for the multi-step mock. + prFoundData, err := os.ReadFile(goldenPath("get_pr_by_branch_found_response.json")) + if err != nil { + t.Fatalf("failed to read PR golden file: %v", err) + } + runsFirstData, err := os.ReadFile(goldenPath("get_analysis_runs_pr_fallback_first_response.json")) + if err != nil { + t.Fatalf("failed to read first runs golden file: %v", err) + } + runsCompletedData, err := os.ReadFile(goldenPath("get_analysis_runs_pr_fallback_completed_response.json")) + if err != nil { + t.Fatalf("failed to read completed runs golden file: %v", err) + } + commitScopeData, err := os.ReadFile(goldenPath("commit_scope_response.json")) + if err != nil { + t.Fatalf("failed to read commit scope golden file: %v", err) + } + + mock := graphqlclient.NewMockClient() + analysisRunsCalls := 0 + mock.QueryFunc = func(_ context.Context, query string, _ map[string]any, result any) error { + switch { + case strings.Contains(query, "pullRequests("): + return json.Unmarshal(prFoundData, result) + case strings.Contains(query, "query GetAnalysisRuns("): + analysisRunsCalls++ + if analysisRunsCalls == 1 { + // First call (ResolveLatestRunForBranch, limit=1): RUNNING run + return json.Unmarshal(runsFirstData, result) + } + // Second call (ResolveLatestCompletedRun, limit=10): RUNNING + SUCCESS + return json.Unmarshal(runsCompletedData, result) + case strings.Contains(query, "checks {"): + // GetRunIssuesFlat for the fallback commit + return json.Unmarshal(commitScopeData, result) + default: + t.Fatalf("unexpected query: %s", query) + return nil + } + } + client := deepsource.NewWithGraphQLClient(mock) + + var buf bytes.Buffer + deps := &cmddeps.Deps{ + Client: client, + ConfigMgr: cfgMgr, + Stdout: &buf, + BranchNameFunc: func() (string, error) { + return "feature/new-auth", nil + }, + HasUnpushedCommitsFunc: func() bool { return false }, + HasUncommittedChangesFunc: func() bool { return false }, + } + + cmd := issuesCmd.NewCmdIssuesWithDeps(deps) + cmd.SetArgs([]string{"--repo", "gh/testowner/testrepo", "--output", "json"}) + + if err := cmd.Execute(); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + got := buf.String() + + // Verify the fallback info message is present. + if !strings.Contains(got, "Analysis is running on commit") { + t.Errorf("expected fallback info message, got: %q", got) + } + + // Extract the JSON array from the output (after the info message line). + // The JSON starts at the first '[' character. + jsonStart := strings.Index(got, "[") + if jsonStart < 0 { + t.Fatalf("no JSON array found in output: %s", got) + } + + var issues []map[string]any + if err := json.Unmarshal([]byte(got[jsonStart:]), &issues); err != nil { + t.Fatalf("failed to parse JSON output: %v\nraw output: %s", err, got) + } + + // Must NOT be empty — this was the bug. + if len(issues) == 0 { + t.Fatal("expected non-empty issues from fallback commit, got empty array") + } + + // Verify we got the expected issues from commit_scope_response.json. + if issues[0]["issue_code"] != "GO-W1007" { + t.Errorf("expected first issue GO-W1007, got %v", issues[0]["issue_code"]) + } +} + func TestIssuesRunInProgress(t *testing.T) { cfgMgr := testutil.CreateTestConfigManager(t, "test-token", "deepsource.com", "test@example.com") mock := testutil.MockQueryFunc(t, map[string]string{