Repository: cli/cli

Issue:
`gh pr view 0` panic
### Describe the bug

Running `gh pr view 0` panic with the following output:

```sh
$ gh pr view 0

panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x2 addr=0x28 pc=0x1010c99e8]

goroutine 1 [running]:
github.com/cli/cli/v2/internal/ghrepo.IsSame({0x0, 0x0}, {0x0, 0x0})
        github.com/cli/cli/v2/internal/ghrepo/repo.go:80 +0x28
github.com/cli/cli/v2/pkg/cmd/pr/shared.(*PullRequestRefs).GetPRHeadLabel(0x140007c16b8)
        github.com/cli/cli/v2/pkg/cmd/pr/shared/finder.go:116 +0x34
github.com/cli/cli/v2/pkg/cmd/pr/shared.(*finder).Find(0x140008a8900, {{0x16f0f72f3, 0x1}, {0x103629240, 0x1c, 0x1c}, {0x0, 0x0}, {0x0, 0x0, ...}})
        github.com/cli/cli/v2/pkg/cmd/pr/shared/finder.go:258 +0x107c
github.com/cli/cli/v2/pkg/cmd/pr/view.viewRun(0x14000680780)
        github.com/cli/cli/v2/pkg/cmd/pr/view/view.go:98 +0x118
github.com/cli/cli/v2/pkg/cmd/pr/view.NewCmdView.func1(0x14000993b08, {0x1400086a030, 0x1, 0x101bfcfd1?})
        github.com/cli/cli/v2/pkg/cmd/pr/view/view.go:69 +0x198
github.com/spf13/cobra.(*Command).execute(0x14000993b08, {0x1400086a010, 0x1, 0x1})
        github.com/spf13/cobra@v1.8.1/command.go:985 +0x830
github.com/spf13/cobra.(*Command).ExecuteC(0x140005ea908)
        github.com/spf13/cobra@v1.8.1/command.go:1117 +0x344
github.com/spf13/cobra.(*Command).ExecuteContextC(...)
        github.com/spf13/cobra@v1.8.1/command.go:1050
github.com/cli/cli/v2/internal/ghcmd.Main()
        github.com/cli/cli/v2/internal/ghcmd/cmd.go:119 +0x4b0
main.main()
        github.com/cli/cli/v2/cmd/gh/main.go:10 +0x1c
```

### Affected version

gh version 2.69.0 (2025-03-19)
https://github.com/cli/cli/releases/tag/v2.69.0

### Steps to reproduce the behavior

1. `gh pr view 0`

### Expected vs actual behavior

Expected to fail to lookup PR or fail on invalid PR value.

### Logs

```
[git remote -v]
[git config --get-regexp ^remote\..*\.gh-resolved$]
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x2 addr=0x28 pc=0x1049ad9e8]

goroutine 1 [running]:
github.com/cli/cli/v2/internal/ghrepo.IsSame({0x0, 0x0}, {0x0, 0x0})
	github.com/cli/cli/v2/internal/ghrepo/repo.go:80 +0x28
github.com/cli/cli/v2/pkg/cmd/pr/shared.(*PullRequestRefs).GetPRHeadLabel(0x1400098f6b8)
	github.com/cli/cli/v2/pkg/cmd/pr/shared/finder.go:116 +0x34
github.com/cli/cli/v2/pkg/cmd/pr/shared.(*finder).Find(0x140006dc380, {{0x16b8132e3, 0x1}, {0x106f0d240, 0x1c, 0x1c}, {0x0, 0x0}, {0x0, 0x0, ...}})
	github.com/cli/cli/v2/pkg/cmd/pr/shared/finder.go:258 +0x107c
github.com/cli/cli/v2/pkg/cmd/pr/view.viewRun(0x14000133b60)
	github.com/cli/cli/v2/pkg/cmd/pr/view/view.go:98 +0x118
github.com/cli/cli/v2/pkg/cmd/pr/view.NewCmdView.func1(0x14000788f08, {0x1400059e450, 0x1, 0x1054e0fd1?})
	github.com/cli/cli/v2/pkg/cmd/pr/view/view.go:69 +0x198
github.com/spf13/cobra.(*Command).execute(0x14000788f08, {0x1400059e430, 0x1, 0x1})
	github.com/spf13/cobra@v1.8.1/command.go:985 +0x830
github.com/spf13/cobra.(*Command).ExecuteC(0x1400077e908)
	github.com/spf13/cobra@v1.8.1/command.go:1117 +0x344
github.com/spf13/cobra.(*Command).ExecuteContextC(...)
	github.com/spf13/cobra@v1.8.1/command.go:1050
github.com/cli/cli/v2/internal/ghcmd.Main()
	github.com/cli/cli/v2/internal/ghcmd/cmd.go:119 +0x4b0
main.main()
	github.com/cli/cli/v2/cmd/gh/main.go:10 +0x1c
```

Discussion:
@williammartin: Haha so obviously we shouldn't panic here but I have to ask, what were you doing when you ran into this?@nopcoder: Typing error

Solution patch:
diff --git a/api/queries_repo.go b/api/queries_repo.go
index 27e21eb32..93a32d80c 100644
--- a/api/queries_repo.go
+++ b/api/queries_repo.go
@@ -738,34 +738,37 @@ func (m *RepoMetadataResult) LabelsToIDs(names []string) ([]string, error) {
 	return ids, nil
 }
 
-// ProjectsToIDs returns two arrays:
+// ProjectsTitlesToIDs returns two arrays:
 // - the first contains IDs of projects V1
 // - the second contains IDs of projects V2
 // - if neither project V1 or project V2 can be found with a given name, then an error is returned
-func (m *RepoMetadataResult) ProjectsToIDs(names []string) ([]string, []string, error) {
+func (m *RepoMetadataResult) ProjectsTitlesToIDs(titles []string) ([]string, []string, error) {
 	var ids []string
 	var idsV2 []string
-	for _, projectName := range names {
-		id, found := m.projectNameToID(projectName)
+	for _, title := range titles {
+		id, found := m.v1ProjectNameToID(title)
 		if found {
 			ids = append(ids, id)
 			continue
 		}
 
-		idV2, found := m.projectV2TitleToID(projectName)
+		idV2, found := m.v2ProjectTitleToID(title)
 		if found {
 			idsV2 = append(idsV2, idV2)
 			continue
 		}
 
-		return nil, nil, fmt.Errorf("'%s' not found", projectName)
+		return nil, nil, fmt.Errorf("'%s' not found", title)
 	}
 	return ids, idsV2, nil
 }
 
-func (m *RepoMetadataResult) projectNameToID(projectName string) (string, bool) {
+// We use the word "titles" when referring to v1 and v2 projects.
+// In reality, v1 projects really have "names", so there is a bit of a
+// mismatch we just need to gloss over.
+func (m *RepoMetadataResult) v1ProjectNameToID(name string) (string, bool) {
 	for _, p := range m.Projects {
-		if strings.EqualFold(projectName, p.Name) {
+		if strings.EqualFold(name, p.Name) {
 			return p.ID, true
 		}
 	}
@@ -773,9 +776,9 @@ func (m *RepoMetadataResult) projectNameToID(projectName string) (string, bool)
 	return "", false
 }
 
-func (m *RepoMetadataResult) projectV2TitleToID(projectTitle string) (string, bool) {
+func (m *RepoMetadataResult) v2ProjectTitleToID(title string) (string, bool) {
 	for _, p := range m.ProjectsV2 {
-		if strings.EqualFold(projectTitle, p.Title) {
+		if strings.EqualFold(title, p.Title) {
 			return p.ID, true
 		}
 	}
@@ -783,8 +786,8 @@ func (m *RepoMetadataResult) projectV2TitleToID(projectTitle string) (string, bo
 	return "", false
 }
 
-func ProjectNamesToPaths(client *Client, repo ghrepo.Interface, projectNames []string, projectsV1Support gh.ProjectsV1Support) ([]string, error) {
-	paths := make([]string, 0, len(projectNames))
+func ProjectTitlesToPaths(client *Client, repo ghrepo.Interface, titles []string, projectsV1Support gh.ProjectsV1Support) ([]string, error) {
+	paths := make([]string, 0, len(titles))
 	matchedPaths := map[string]struct{}{}
 
 	// TODO: ProjectsV1Cleanup
@@ -796,9 +799,9 @@ func ProjectNamesToPaths(client *Client, repo ghrepo.Interface, projectNames []s
 			return nil, err
 		}
 
-		for _, projectName := range projectNames {
+		for _, title := range titles {
 			for _, p := range v1Projects {
-				if strings.EqualFold(projectName, p.Name) {
+				if strings.EqualFold(title, p.Name) {
 					pathParts := strings.Split(p.ResourcePath, "/")
 					var path string
 					if pathParts[1] == "orgs" || pathParts[1] == "users" {
@@ -807,7 +810,7 @@ func ProjectNamesToPaths(client *Client, repo ghrepo.Interface, projectNames []s
 						path = fmt.Sprintf("%s/%s/%s", pathParts[1], pathParts[2], pathParts[4])
 					}
 					paths = append(paths, path)
-					matchedPaths[projectName] = struct{}{}
+					matchedPaths[title] = struct{}{}
 					break
 				}
 			}
@@ -820,15 +823,15 @@ func ProjectNamesToPaths(client *Client, repo ghrepo.Interface, projectNames []s
 		return nil, err
 	}
 
-	for _, projectName := range projectNames {
+	for _, title := range titles {
 		// If we already found a v1 project with this name, skip it
-		if _, ok := matchedPaths[projectName]; ok {
+		if _, ok := matchedPaths[title]; ok {
 			continue
 		}
 
 		found := false
 		for _, p := range v2Projects {
-			if strings.EqualFold(projectName, p.Title) {
+			if strings.EqualFold(title, p.Title) {
 				pathParts := strings.Split(p.ResourcePath, "/")
 				var path string
 				if pathParts[1] == "orgs" || pathParts[1] == "users" {
@@ -843,7 +846,7 @@ func ProjectNamesToPaths(client *Client, repo ghrepo.Interface, projectNames []s
 		}
 
 		if !found {
-			return nil, fmt.Errorf("'%s' not found", projectName)
+			return nil, fmt.Errorf("'%s' not found", title)
 		}
 	}
 
diff --git a/pkg/cmd/pr/create/create.go b/pkg/cmd/pr/create/create.go
index 7f960bce4..483b8246b 100644
--- a/pkg/cmd/pr/create/create.go
+++ b/pkg/cmd/pr/create/create.go
@@ -18,6 +18,7 @@ import (
 	ghContext "github.com/cli/cli/v2/context"
 	"github.com/cli/cli/v2/git"
 	"github.com/cli/cli/v2/internal/browser"
+	fd "github.com/cli/cli/v2/internal/featuredetection"
 	"github.com/cli/cli/v2/internal/gh"
 	"github.com/cli/cli/v2/internal/ghrepo"
 	"github.com/cli/cli/v2/internal/text"
@@ -31,6 +32,7 @@ import (
 
 type CreateOptions struct {
 	// This struct stores user input and factory functions
+	Detector         fd.Detector
 	HttpClient       func() (*http.Client, error)
 	GitClient        *git.Client
 	Config           func() (gh.Config, error)
@@ -363,6 +365,20 @@ func createRun(opts *CreateOptions) error {
 		return err
 	}
 
+	httpClient, err := opts.HttpClient()
+	if err != nil {
+		return err
+	}
+
+	// TODO projectsV1Deprecation
+	// Remove this section as we should no longer need to detect
+	if opts.Detector == nil {
+		cachedClient := api.NewCachedHTTPClient(httpClient, time.Hour*24)
+		opts.Detector = fd.NewDetector(cachedClient, ctx.PRRefs.BaseRepo().RepoHost())
+	}
+
+	projectsV1Support := opts.Detector.ProjectsV1()
+
 	client := ctx.Client
 
 	state, err := NewIssueState(*ctx, *opts)
@@ -384,7 +400,7 @@ func createRun(opts *CreateOptions) error {
 		if err != nil {
 			return err
 		}
-		openURL, err = generateCompareURL(*ctx, *state)
+		openURL, err = generateCompareURL(*ctx, *state, gh.ProjectsV1Supported)
 		if err != nil {
 			return err
 		}
@@ -441,7 +457,7 @@ func createRun(opts *CreateOptions) error {
 			return err
 		}
 		// TODO wm: revisit project support
-		return submitPR(*opts, *ctx, *state, gh.ProjectsV1Supported)
+		return submitPR(*opts, *ctx, *state, projectsV1Support)
 	}
 
 	if opts.RecoverFile != "" {
@@ -518,7 +534,7 @@ func createRun(opts *CreateOptions) error {
 			}
 		}
 
-		openURL, err = generateCompareURL(*ctx, *state)
+		openURL, err = generateCompareURL(*ctx, *state, gh.ProjectsV1Supported)
 		if err != nil {
 			return err
 		}
@@ -568,12 +584,12 @@ func createRun(opts *CreateOptions) error {
 	if action == shared.SubmitDraftAction {
 		state.Draft = true
 		// TODO wm: revisit project support
-		return submitPR(*opts, *ctx, *state, gh.ProjectsV1Supported)
+		return submitPR(*opts, *ctx, *state, projectsV1Support)
 	}
 
 	if action == shared.SubmitAction {
 		// TODO wm: revisit project support
-		return submitPR(*opts, *ctx, *state, gh.ProjectsV1Supported)
+		return submitPR(*opts, *ctx, *state, projectsV1Support)
 	}
 
 	err = errors.New("expected to cancel, preview, or submit")
@@ -1216,13 +1232,13 @@ func handlePush(opts CreateOptions, ctx CreateContext) error {
 	return pushBranch()
 }
 
-func generateCompareURL(ctx CreateContext, state shared.IssueMetadataState) (string, error) {
+func generateCompareURL(ctx CreateContext, state shared.IssueMetadataState, projectsV1Support gh.ProjectsV1Support) (string, error) {
 	u := ghrepo.GenerateRepoURL(
 		ctx.PRRefs.BaseRepo(),
 		"compare/%s...%s?expand=1",
 		url.PathEscape(ctx.PRRefs.BaseRef()), url.PathEscape(ctx.PRRefs.QualifiedHeadRef()))
 	// TODO wm: revisit project support
-	url, err := shared.WithPrAndIssueQueryParams(ctx.Client, ctx.PRRefs.BaseRepo(), u, state, gh.ProjectsV1Supported)
+	url, err := shared.WithPrAndIssueQueryParams(ctx.Client, ctx.PRRefs.BaseRepo(), u, state, projectsV1Support)
 	if err != nil {
 		return "", err
 	}
diff --git a/pkg/cmd/pr/shared/editable.go b/pkg/cmd/pr/shared/editable.go
index 0bebb999a..e73b3c294 100644
--- a/pkg/cmd/pr/shared/editable.go
+++ b/pkg/cmd/pr/shared/editable.go
@@ -137,7 +137,7 @@ func (e Editable) ProjectIds() (*[]string, error) {
 		s.RemoveValues(e.Projects.Remove)
 		e.Projects.Value = s.ToSlice()
 	}
-	p, _, err := e.Metadata.ProjectsToIDs(e.Projects.Value)
+	p, _, err := e.Metadata.ProjectsTitlesToIDs(e.Projects.Value)
 	return &p, err
 }
 
@@ -171,14 +171,14 @@ func (e Editable) ProjectV2Ids() (*[]string, *[]string, error) {
 	var err error
 
 	if addTitles.Len() > 0 {
-		_, addIds, err = e.Metadata.ProjectsToIDs(addTitles.ToSlice())
+		_, addIds, err = e.Metadata.ProjectsTitlesToIDs(addTitles.ToSlice())
 		if err != nil {
 			return nil, nil, err
 		}
 	}
 
 	if removeTitles.Len() > 0 {
-		_, removeIds, err = e.Metadata.ProjectsToIDs(removeTitles.ToSlice())
+		_, removeIds, err = e.Metadata.ProjectsTitlesToIDs(removeTitles.ToSlice())
 		if err != nil {
 			return nil, nil, err
 		}
diff --git a/pkg/cmd/pr/shared/finder.go b/pkg/cmd/pr/shared/finder.go
index e6bb7d66a..b8d23c978 100644
--- a/pkg/cmd/pr/shared/finder.go
+++ b/pkg/cmd/pr/shared/finder.go
@@ -10,12 +10,14 @@ import (
 	"sort"
 	"strconv"
 	"strings"
+	"testing"
 	"time"
 
 	"github.com/cli/cli/v2/api"
 	ghContext "github.com/cli/cli/v2/context"
 	"github.com/cli/cli/v2/git"
 	fd "github.com/cli/cli/v2/internal/featuredetection"
+	"github.com/cli/cli/v2/internal/gh"
 	"github.com/cli/cli/v2/internal/ghrepo"
 	"github.com/cli/cli/v2/pkg/cmdutil"
 	o "github.com/cli/cli/v2/pkg/option"
@@ -54,9 +56,9 @@ type finder struct {
 }
 
 func NewFinder(factory *cmdutil.Factory) PRFinder {
-	if runCommandFinder != nil {
-		f := runCommandFinder
-		runCommandFinder = &mockFinder{err: errors.New("you must use a RunCommandFinder to stub PR lookups")}
+	if finderForRunCommandStyleTests != nil {
+		f := finderForRunCommandStyleTests
+		finderForRunCommandStyleTests = &mockFinder{err: errors.New("you must use StubFinderForRunCommandStyleTests to stub PR lookups")}
 		return f
 	}
 
@@ -70,12 +72,23 @@ func NewFinder(factory *cmdutil.Factory) PRFinder {
 	}
 }
 
-var runCommandFinder PRFinder
+var finderForRunCommandStyleTests PRFinder
 
-// RunCommandFinder is the NewMockFinder substitute to be used ONLY in runCommand-style tests.
-func RunCommandFinder(selector string, pr *api.PullRequest, repo ghrepo.Interface) *mockFinder {
+// StubFinderForRunCommandStyleTests is the NewMockFinder substitute to be used ONLY in runCommand-style tests.
+func StubFinderForRunCommandStyleTests(t *testing.T, selector string, pr *api.PullRequest, repo ghrepo.Interface) *mockFinder {
+	// Create a new mock finder and override the "runCommandFinder" variable so that calls to
+	// NewFinder() will return this mock. This is a bad pattern, and a result of old style runCommand
+	// tests that would ideally be replaced. The reason we need to do this is that the runCommand style tests
+	// construct the cobra command via NewCmd* functions, and then Execute them directly, providing no opportunity
+	// to inject a test double unless it's on the factory, which finder never is, because only PR commands need it.
 	finder := NewMockFinder(selector, pr, repo)
-	runCommandFinder = finder
+	finderForRunCommandStyleTests = finder
+
+	// Ensure that at the end of the test, we reset the "runCommandFinder" variable so that tests are isolated,
+	// at least if they are run sequentially.
+	t.Cleanup(func() {
+		finderForRunCommandStyleTests = nil
+	})
 	return finder
 }
 
@@ -89,6 +102,8 @@ type FindOptions struct {
 	BaseBranch string
 	// States lists the possible PR states to scope the PR-for-branch lookup to.
 	States []string
+
+	Detector fd.Detector
 }
 
 func (f *finder) Find(opts FindOptions) (*api.PullRequest, ghrepo.Interface, error) {
@@ -193,9 +208,11 @@ func (f *finder) Find(opts FindOptions) (*api.PullRequest, ghrepo.Interface, err
 	fields.AddValues([]string{"id", "number"}) // for additional preload queries below
 
 	if fields.Contains("isInMergeQueue") || fields.Contains("isMergeQueueEnabled") {
-		cachedClient := api.NewCachedHTTPClient(httpClient, time.Hour*24)
-		detector := fd.NewDetector(cachedClient, f.baseRefRepo.RepoHost())
-		prFeatures, err := detector.PullRequestFeatures()
+		if opts.Detector == nil {
+			cachedClient := api.NewCachedHTTPClient(httpClient, time.Hour*24)
+			opts.Detector = fd.NewDetector(cachedClient, f.baseRefRepo.RepoHost())
+		}
+		prFeatures, err := opts.Detector.PullRequestFeatures()
 		if err != nil {
 			return nil, nil, err
 		}
@@ -211,8 +228,23 @@ func (f *finder) Find(opts FindOptions) (*api.PullRequest, ghrepo.Interface, err
 		fields.Remove("projectItems")
 	}
 
+	// TODO projectsV1Deprecation
+	// Remove this block
+	// When removing this, remember to remove `projectCards` from the list of default fields in pr/view.go
+	if fields.Contains("projectCards") {
+		if opts.Detector == nil {
+			cachedClient := api.NewCachedHTTPClient(httpClient, time.Hour*24)
+			opts.Detector = fd.NewDetector(cachedClient, f.baseRefRepo.RepoHost())
+		}
+
+		if opts.Detector.ProjectsV1() == gh.ProjectsV1Unsupported {
+			fields.Remove("projectCards")
+		}
+	}
+
 	var pr *api.PullRequest
 	if f.prNumber > 0 {
+		// If we have a PR number, let's look it up
 		if numberFieldOnly {
 			// avoid hitting the API if we already have all the information
 			return &api.PullRequest{Number: f.prNumber}, f.baseRefRepo, nil
@@ -221,11 +253,16 @@ func (f *finder) Find(opts FindOptions) (*api.PullRequest, ghrepo.Interface, err
 		if err != nil {
 			return pr, f.baseRefRepo, err
 		}
-	} else {
+	} else if prRefs.BaseRepo() != nil && f.branchName != "" {
+		// No PR number, but we have a base repo and branch name.
 		pr, err = findForRefs(httpClient, prRefs, opts.States, fields.ToSlice())
 		if err != nil {
 			return pr, f.baseRefRepo, err
 		}
+	} else {
+		// If we don't have a PR number or a base repo and branch name,
+		// we can't do anything
+		return nil, f.baseRefRepo, &NotFoundError{fmt.Errorf("no pull requests found")}
 	}
 
 	g, _ := errgroup.WithContext(context.Background())
diff --git a/pkg/cmd/pr/shared/params.go b/pkg/cmd/pr/shared/params.go
index 4f36a80aa..08968939d 100644
--- a/pkg/cmd/pr/shared/params.go
+++ b/pkg/cmd/pr/shared/params.go
@@ -36,7 +36,7 @@ func WithPrAndIssueQueryParams(client *api.Client, baseRepo ghrepo.Interface, ba
 		q.Set("labels", strings.Join(state.Labels, ","))
 	}
 	if len(state.ProjectTitles) > 0 {
-		projectPaths, err := api.ProjectNamesToPaths(client, baseRepo, state.ProjectTitles, projectsV1Support)
+		projectPaths, err := api.ProjectTitlesToPaths(client, baseRepo, state.ProjectTitles, projectsV1Support)
 		if err != nil {
 			return "", fmt.Errorf("could not add to project: %w", err)
 		}
@@ -119,7 +119,7 @@ func AddMetadataToIssueParams(client *api.Client, baseRepo ghrepo.Interface, par
 	}
 	params["labelIds"] = labelIDs
 
-	projectIDs, projectV2IDs, err := tb.MetadataResult.ProjectsToIDs(tb.ProjectTitles)
+	projectIDs, projectV2IDs, err := tb.MetadataResult.ProjectsTitlesToIDs(tb.ProjectTitles)
 	if err != nil {
 		return fmt.Errorf("could not add to project: %w", err)
 	}
diff --git a/pkg/cmd/pr/view/view.go b/pkg/cmd/pr/view/view.go
index 997f74d87..8a39d1134 100644
--- a/pkg/cmd/pr/view/view.go
+++ b/pkg/cmd/pr/view/view.go
@@ -10,6 +10,7 @@ import (
 	"github.com/MakeNowJust/heredoc"
 	"github.com/cli/cli/v2/api"
 	"github.com/cli/cli/v2/internal/browser"
+	fd "github.com/cli/cli/v2/internal/featuredetection"
 	"github.com/cli/cli/v2/internal/ghrepo"
 	"github.com/cli/cli/v2/internal/text"
 	"github.com/cli/cli/v2/pkg/cmd/pr/shared"
@@ -22,6 +23,9 @@ import (
 type ViewOptions struct {
 	IO      *iostreams.IOStreams
 	Browser browser.Browser
+	// TODO projectsV1Deprecation
+	// Remove this detector since it is only used for test validation.
+	Detector fd.Detector
 
 	Finder   shared.PRFinder
 	Exporter cmdutil.Exporter
@@ -89,6 +93,7 @@ func viewRun(opts *ViewOptions) error {
 	findOptions := shared.FindOptions{
 		Selector: opts.SelectorArg,
 		Fields:   defaultFields,
+		Detector: opts.Detector,
 	}
 	if opts.BrowserMode {
 		findOptions.Fields = []string{"url"}
diff --git a/pkg/httpmock/stub.go b/pkg/httpmock/stub.go
index 745c12417..3b03ae718 100644
--- a/pkg/httpmock/stub.go
+++ b/pkg/httpmock/stub.go
@@ -9,6 +9,8 @@ import (
 	"os"
 	"regexp"
 	"strings"
+
+	"github.com/cli/go-gh/v2/pkg/api"
 )
 
 type Matcher func(req *http.Request) bool
@@ -161,6 +163,9 @@ func JSONResponse(body interface{}) Responder {
 	}
 }
 
+// StatusJSONResponse turns the given argument into a JSON response.
+//
+// The argument is not meant to be a JSON string, unless it's intentional.
 func StatusJSONResponse(status int, body interface{}) Responder {
 	return func(req *http.Request) (*http.Response, error) {
 		b, _ := json.Marshal(body)
@@ -171,6 +176,12 @@ func StatusJSONResponse(status int, body interface{}) Responder {
 	}
 }
 
+// JSONErrorResponse is a type-safe helper to avoid confusion around the
+// provided argument.
+func JSONErrorResponse(status int, err api.HTTPError) Responder {
+	return StatusJSONResponse(status, err)
+}
+
 func FileResponse(filename string) Responder {
 	return func(req *http.Request) (*http.Response, error) {
 		f, err := os.Open(filename)



Test patch:
diff --git a/api/queries_repo_test.go b/api/queries_repo_test.go
index 72ed35776..01fc7a4c7 100644
--- a/api/queries_repo_test.go
+++ b/api/queries_repo_test.go
@@ -187,7 +187,7 @@ func Test_RepoMetadata(t *testing.T) {
 
 	expectedProjectIDs := []string{"TRIAGEID", "ROADMAPID"}
 	expectedProjectV2IDs := []string{"TRIAGEV2ID", "ROADMAPV2ID", "MONALISAV2ID"}
-	projectIDs, projectV2IDs, err := result.ProjectsToIDs([]string{"triage", "roadmap", "triagev2", "roadmapv2", "monalisav2"})
+	projectIDs, projectV2IDs, err := result.ProjectsTitlesToIDs([]string{"triage", "roadmap", "triagev2", "roadmapv2", "monalisav2"})
 	if err != nil {
 		t.Errorf("error resolving projects: %v", err)
 	}
@@ -273,7 +273,7 @@ func Test_ProjectNamesToPaths(t *testing.T) {
 		} } } }
 		`))
 
-		projectPaths, err := ProjectNamesToPaths(client, repo, []string{"Triage", "Roadmap", "TriageV2", "RoadmapV2", "MonalisaV2"}, gh.ProjectsV1Supported)
+		projectPaths, err := ProjectTitlesToPaths(client, repo, []string{"Triage", "Roadmap", "TriageV2", "RoadmapV2", "MonalisaV2"}, gh.ProjectsV1Supported)
 		if err != nil {
 			t.Fatalf("unexpected error: %v", err)
 		}
@@ -331,7 +331,7 @@ func Test_ProjectNamesToPaths(t *testing.T) {
 		} } } }
 		`))
 
-		projectPaths, err := ProjectNamesToPaths(client, repo, []string{"TriageV2", "RoadmapV2", "MonalisaV2"}, gh.ProjectsV1Unsupported)
+		projectPaths, err := ProjectTitlesToPaths(client, repo, []string{"TriageV2", "RoadmapV2", "MonalisaV2"}, gh.ProjectsV1Unsupported)
 		if err != nil {
 			t.Fatalf("unexpected error: %v", err)
 		}
@@ -374,7 +374,7 @@ func Test_ProjectNamesToPaths(t *testing.T) {
 		} } } }
 		`))
 
-		_, err := ProjectNamesToPaths(client, repo, []string{"TriageV2"}, gh.ProjectsV1Unsupported)
+		_, err := ProjectTitlesToPaths(client, repo, []string{"TriageV2"}, gh.ProjectsV1Unsupported)
 		require.Equal(t, err, fmt.Errorf("'TriageV2' not found"))
 	})
 }
diff --git a/pkg/cmd/gist/delete/delete_test.go b/pkg/cmd/gist/delete/delete_test.go
index 24ca2bb33..2c4df8d8d 100644
--- a/pkg/cmd/gist/delete/delete_test.go
+++ b/pkg/cmd/gist/delete/delete_test.go
@@ -18,6 +18,7 @@ import (
 	ghAPI "github.com/cli/go-gh/v2/pkg/api"
 	"github.com/google/shlex"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 func TestNewCmdDelete(t *testing.T) {
@@ -327,11 +328,12 @@ func Test_deleteRun(t *testing.T) {
 
 func Test_gistDelete(t *testing.T) {
 	tests := []struct {
-		name      string
-		httpStubs func(*httpmock.Registry)
-		hostname  string
-		gistID    string
-		wantErr   error
+		name          string
+		httpStubs     func(*httpmock.Registry)
+		hostname      string
+		gistID        string
+		wantErr       error
+		wantErrString string
 	}{
 		{
 			name: "successful delete",
@@ -343,36 +345,34 @@ func Test_gistDelete(t *testing.T) {
 			},
 			hostname: "github.com",
 			gistID:   "1234",
-			wantErr:  nil,
 		},
 		{
-			name: "when an gist is not found, it returns a NotFoundError",
+			name: "when a gist is not found, it returns a NotFoundError",
 			httpStubs: func(reg *httpmock.Registry) {
 				reg.Register(
 					httpmock.REST("DELETE", "gists/1234"),
 					httpmock.StatusStringResponse(404, "{}"),
 				)
 			},
-			hostname: "github.com",
-			gistID:   "1234",
-			wantErr:  shared.NotFoundErr,
+			hostname:      "github.com",
+			gistID:        "1234",
+			wantErr:       shared.NotFoundErr, // To make sure we return the pre-defined error instance.
+			wantErrString: "not found",
 		},
 		{
 			name: "when there is a non-404 error deleting the gist, that error is returned",
 			httpStubs: func(reg *httpmock.Registry) {
 				reg.Register(
 					httpmock.REST("DELETE", "gists/1234"),
-					httpmock.StatusJSONResponse(500, `{"message": "arbitrary error"}`),
+					httpmock.JSONErrorResponse(500, ghAPI.HTTPError{
+						StatusCode: 500,
+						Message:    "arbitrary error",
+					}),
 				)
 			},
-			hostname: "github.com",
-			gistID:   "1234",
-			wantErr: api.HTTPError{
-				HTTPError: &ghAPI.HTTPError{
-					StatusCode: 500,
-					Message:    "arbitrary error",
-				},
-			},
+			hostname:      "github.com",
+			gistID:        "1234",
+			wantErrString: "HTTP 500: arbitrary error (https://api.github.com/gists/1234)",
 		},
 	}
 
@@ -383,12 +383,16 @@ func Test_gistDelete(t *testing.T) {
 			client := api.NewClientFromHTTP(&http.Client{Transport: reg})
 
 			err := deleteGist(client, tt.hostname, tt.gistID)
-			if tt.wantErr != nil {
-				assert.ErrorAs(t, err, &tt.wantErr)
+			if tt.wantErrString == "" && tt.wantErr == nil {
+				require.NoError(t, err)
 			} else {
-				assert.NoError(t, err)
+				if tt.wantErrString != "" {
+					require.EqualError(t, err, tt.wantErrString)
+				}
+				if tt.wantErr != nil {
+					require.ErrorIs(t, err, tt.wantErr)
+				}
 			}
-
 		})
 	}
 }
diff --git a/pkg/cmd/gpg-key/delete/delete_test.go b/pkg/cmd/gpg-key/delete/delete_test.go
index 115e72db2..dc730b100 100644
--- a/pkg/cmd/gpg-key/delete/delete_test.go
+++ b/pkg/cmd/gpg-key/delete/delete_test.go
@@ -177,7 +177,7 @@ func Test_deleteRun(t *testing.T) {
 			opts: DeleteOptions{KeyID: "ABC123", Confirmed: true},
 			httpStubs: func(reg *httpmock.Registry) {
 				reg.Register(httpmock.REST("GET", "user/gpg_keys"), httpmock.StatusStringResponse(200, keysResp))
-				reg.Register(httpmock.REST("DELETE", "user/gpg_keys/123"), httpmock.StatusJSONResponse(404, api.HTTPError{
+				reg.Register(httpmock.REST("DELETE", "user/gpg_keys/123"), httpmock.JSONErrorResponse(404, api.HTTPError{
 					StatusCode: 404,
 					Message:    "GPG key 123 not found",
 				}))
diff --git a/pkg/cmd/pr/checkout/checkout_test.go b/pkg/cmd/pr/checkout/checkout_test.go
index 40917fd76..496139423 100644
--- a/pkg/cmd/pr/checkout/checkout_test.go
+++ b/pkg/cmd/pr/checkout/checkout_test.go
@@ -518,7 +518,7 @@ func TestPRCheckout_sameRepo(t *testing.T) {
 	defer http.Verify(t)
 
 	baseRepo, pr := stubPR("OWNER/REPO", "OWNER/REPO:feature")
-	finder := shared.RunCommandFinder("123", pr, baseRepo)
+	finder := shared.StubFinderForRunCommandStyleTests(t, "123", pr, baseRepo)
 	finder.ExpectFields([]string{"number", "headRefName", "headRepository", "headRepositoryOwner", "isCrossRepository", "maintainerCanModify"})
 
 	cs, cmdTeardown := run.Stub()
@@ -539,7 +539,7 @@ func TestPRCheckout_existingBranch(t *testing.T) {
 	defer http.Verify(t)
 
 	baseRepo, pr := stubPR("OWNER/REPO", "OWNER/REPO:feature")
-	shared.RunCommandFinder("123", pr, baseRepo)
+	shared.StubFinderForRunCommandStyleTests(t, "123", pr, baseRepo)
 
 	cs, cmdTeardown := run.Stub()
 	defer cmdTeardown(t)
@@ -570,7 +570,7 @@ func TestPRCheckout_differentRepo_remoteExists(t *testing.T) {
 	defer http.Verify(t)
 
 	baseRepo, pr := stubPR("OWNER/REPO", "hubot/REPO:feature")
-	finder := shared.RunCommandFinder("123", pr, baseRepo)
+	finder := shared.StubFinderForRunCommandStyleTests(t, "123", pr, baseRepo)
 	finder.ExpectFields([]string{"number", "headRefName", "headRepository", "headRepositoryOwner", "isCrossRepository", "maintainerCanModify"})
 
 	cs, cmdTeardown := run.Stub()
@@ -590,7 +590,7 @@ func TestPRCheckout_differentRepo(t *testing.T) {
 	defer http.Verify(t)
 
 	baseRepo, pr := stubPR("OWNER/REPO:master", "hubot/REPO:feature")
-	finder := shared.RunCommandFinder("123", pr, baseRepo)
+	finder := shared.StubFinderForRunCommandStyleTests(t, "123", pr, baseRepo)
 	finder.ExpectFields([]string{"number", "headRefName", "headRepository", "headRepositoryOwner", "isCrossRepository", "maintainerCanModify"})
 
 	cs, cmdTeardown := run.Stub()
@@ -613,7 +613,7 @@ func TestPRCheckout_differentRepoForce(t *testing.T) {
 	defer http.Verify(t)
 
 	baseRepo, pr := stubPR("OWNER/REPO:master", "hubot/REPO:feature")
-	finder := shared.RunCommandFinder("123", pr, baseRepo)
+	finder := shared.StubFinderForRunCommandStyleTests(t, "123", pr, baseRepo)
 	finder.ExpectFields([]string{"number", "headRefName", "headRepository", "headRepositoryOwner", "isCrossRepository", "maintainerCanModify"})
 
 	cs, cmdTeardown := run.Stub()
@@ -636,7 +636,7 @@ func TestPRCheckout_differentRepo_existingBranch(t *testing.T) {
 	defer http.Verify(t)
 
 	baseRepo, pr := stubPR("OWNER/REPO:master", "hubot/REPO:feature")
-	shared.RunCommandFinder("123", pr, baseRepo)
+	shared.StubFinderForRunCommandStyleTests(t, "123", pr, baseRepo)
 
 	cs, cmdTeardown := run.Stub()
 	defer cmdTeardown(t)
@@ -655,7 +655,7 @@ func TestPRCheckout_detachedHead(t *testing.T) {
 	defer http.Verify(t)
 
 	baseRepo, pr := stubPR("OWNER/REPO:master", "hubot/REPO:feature")
-	shared.RunCommandFinder("123", pr, baseRepo)
+	shared.StubFinderForRunCommandStyleTests(t, "123", pr, baseRepo)
 
 	cs, cmdTeardown := run.Stub()
 	defer cmdTeardown(t)
@@ -674,7 +674,7 @@ func TestPRCheckout_differentRepo_currentBranch(t *testing.T) {
 	defer http.Verify(t)
 
 	baseRepo, pr := stubPR("OWNER/REPO:master", "hubot/REPO:feature")
-	shared.RunCommandFinder("123", pr, baseRepo)
+	shared.StubFinderForRunCommandStyleTests(t, "123", pr, baseRepo)
 
 	cs, cmdTeardown := run.Stub()
 	defer cmdTeardown(t)
@@ -693,7 +693,7 @@ func TestPRCheckout_differentRepo_invalidBranchName(t *testing.T) {
 	defer http.Verify(t)
 
 	baseRepo, pr := stubPR("OWNER/REPO", "hubot/REPO:-foo")
-	shared.RunCommandFinder("123", pr, baseRepo)
+	shared.StubFinderForRunCommandStyleTests(t, "123", pr, baseRepo)
 
 	_, cmdTeardown := run.Stub()
 	defer cmdTeardown(t)
@@ -711,7 +711,7 @@ func TestPRCheckout_maintainerCanModify(t *testing.T) {
 
 	baseRepo, pr := stubPR("OWNER/REPO:master", "hubot/REPO:feature")
 	pr.MaintainerCanModify = true
-	shared.RunCommandFinder("123", pr, baseRepo)
+	shared.StubFinderForRunCommandStyleTests(t, "123", pr, baseRepo)
 
 	cs, cmdTeardown := run.Stub()
 	defer cmdTeardown(t)
@@ -732,7 +732,7 @@ func TestPRCheckout_recurseSubmodules(t *testing.T) {
 	http := &httpmock.Registry{}
 
 	baseRepo, pr := stubPR("OWNER/REPO", "OWNER/REPO:feature")
-	shared.RunCommandFinder("123", pr, baseRepo)
+	shared.StubFinderForRunCommandStyleTests(t, "123", pr, baseRepo)
 
 	cs, cmdTeardown := run.Stub()
 	defer cmdTeardown(t)
@@ -753,7 +753,7 @@ func TestPRCheckout_force(t *testing.T) {
 	http := &httpmock.Registry{}
 
 	baseRepo, pr := stubPR("OWNER/REPO", "OWNER/REPO:feature")
-	shared.RunCommandFinder("123", pr, baseRepo)
+	shared.StubFinderForRunCommandStyleTests(t, "123", pr, baseRepo)
 
 	cs, cmdTeardown := run.Stub()
 	defer cmdTeardown(t)
@@ -774,7 +774,7 @@ func TestPRCheckout_detach(t *testing.T) {
 	defer http.Verify(t)
 
 	baseRepo, pr := stubPR("OWNER/REPO:master", "hubot/REPO:feature")
-	shared.RunCommandFinder("123", pr, baseRepo)
+	shared.StubFinderForRunCommandStyleTests(t, "123", pr, baseRepo)
 
 	cs, cmdTeardown := run.Stub()
 	defer cmdTeardown(t)
diff --git a/pkg/cmd/pr/close/close_test.go b/pkg/cmd/pr/close/close_test.go
index 959af0e04..57ee0f0e6 100644
--- a/pkg/cmd/pr/close/close_test.go
+++ b/pkg/cmd/pr/close/close_test.go
@@ -110,7 +110,7 @@ func TestPrClose(t *testing.T) {
 
 	baseRepo, pr := stubPR("OWNER/REPO", "OWNER/REPO:feature")
 	pr.Title = "The title of the PR"
-	shared.RunCommandFinder("96", pr, baseRepo)
+	shared.StubFinderForRunCommandStyleTests(t, "96", pr, baseRepo)
 
 	http.Register(
 		httpmock.GraphQL(`mutation PullRequestClose\b`),
@@ -133,7 +133,7 @@ func TestPrClose_alreadyClosed(t *testing.T) {
 	baseRepo, pr := stubPR("OWNER/REPO", "OWNER/REPO:feature")
 	pr.State = "CLOSED"
 	pr.Title = "The title of the PR"
-	shared.RunCommandFinder("96", pr, baseRepo)
+	shared.StubFinderForRunCommandStyleTests(t, "96", pr, baseRepo)
 
 	output, err := runCommand(http, true, "96")
 	assert.NoError(t, err)
@@ -147,7 +147,7 @@ func TestPrClose_deleteBranch_sameRepo(t *testing.T) {
 
 	baseRepo, pr := stubPR("OWNER/REPO", "OWNER/REPO:blueberries")
 	pr.Title = "The title of the PR"
-	shared.RunCommandFinder("96", pr, baseRepo)
+	shared.StubFinderForRunCommandStyleTests(t, "96", pr, baseRepo)
 
 	http.Register(
 		httpmock.GraphQL(`mutation PullRequestClose\b`),
@@ -181,7 +181,7 @@ func TestPrClose_deleteBranch_crossRepo(t *testing.T) {
 
 	baseRepo, pr := stubPR("OWNER/REPO", "hubot/REPO:blueberries")
 	pr.Title = "The title of the PR"
-	shared.RunCommandFinder("96", pr, baseRepo)
+	shared.StubFinderForRunCommandStyleTests(t, "96", pr, baseRepo)
 
 	http.Register(
 		httpmock.GraphQL(`mutation PullRequestClose\b`),
@@ -213,7 +213,7 @@ func TestPrClose_deleteBranch_sameBranch(t *testing.T) {
 
 	baseRepo, pr := stubPR("OWNER/REPO:main", "OWNER/REPO:trunk")
 	pr.Title = "The title of the PR"
-	shared.RunCommandFinder("96", pr, baseRepo)
+	shared.StubFinderForRunCommandStyleTests(t, "96", pr, baseRepo)
 
 	http.Register(
 		httpmock.GraphQL(`mutation PullRequestClose\b`),
@@ -248,7 +248,7 @@ func TestPrClose_deleteBranch_notInGitRepo(t *testing.T) {
 
 	baseRepo, pr := stubPR("OWNER/REPO:main", "OWNER/REPO:trunk")
 	pr.Title = "The title of the PR"
-	shared.RunCommandFinder("96", pr, baseRepo)
+	shared.StubFinderForRunCommandStyleTests(t, "96", pr, baseRepo)
 
 	http.Register(
 		httpmock.GraphQL(`mutation PullRequestClose\b`),
@@ -282,7 +282,7 @@ func TestPrClose_withComment(t *testing.T) {
 
 	baseRepo, pr := stubPR("OWNER/REPO", "OWNER/REPO:feature")
 	pr.Title = "The title of the PR"
-	shared.RunCommandFinder("96", pr, baseRepo)
+	shared.StubFinderForRunCommandStyleTests(t, "96", pr, baseRepo)
 
 	http.Register(
 		httpmock.GraphQL(`mutation CommentCreate\b`),
diff --git a/pkg/cmd/pr/create/create_test.go b/pkg/cmd/pr/create/create_test.go
index 2a88b5eee..f3b99bc89 100644
--- a/pkg/cmd/pr/create/create_test.go
+++ b/pkg/cmd/pr/create/create_test.go
@@ -15,6 +15,7 @@ import (
 	"github.com/cli/cli/v2/git"
 	"github.com/cli/cli/v2/internal/browser"
 	"github.com/cli/cli/v2/internal/config"
+	fd "github.com/cli/cli/v2/internal/featuredetection"
 	"github.com/cli/cli/v2/internal/gh"
 	"github.com/cli/cli/v2/internal/ghrepo"
 	"github.com/cli/cli/v2/internal/prompter"
@@ -1618,6 +1619,7 @@ func Test_createRun(t *testing.T) {
 			}
 
 			opts := CreateOptions{}
+			opts.Detector = &fd.EnabledDetectorMock{}
 			opts.Prompter = pm
 
 			ios, _, stdout, stderr := iostreams.Test()
@@ -1941,7 +1943,8 @@ func Test_generateCompareURL(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			got, err := generateCompareURL(tt.ctx, tt.state)
+			// TODO wm: projects v1 support?
+			got, err := generateCompareURL(tt.ctx, tt.state, gh.ProjectsV1Supported)
 			if (err != nil) != tt.wantErr {
 				t.Errorf("generateCompareURL() error = %v, wantErr %v", err, tt.wantErr)
 				return
@@ -2009,3 +2012,119 @@ func mockRetrieveProjects(_ *testing.T, reg *httpmock.Registry) {
 }
 
 // TODO interactive metadata tests once: 1) we have test utils for Prompter and 2) metadata questions use Prompter
+
+// TODO projectsV1Deprecation
+// Remove this test.
+func TestProjectsV1Deprecation(t *testing.T) {
+
+	t.Run("non-interactive submission", func(t *testing.T) {
+		t.Run("when projects v1 is supported, queries for it", func(t *testing.T) {
+			ios, _, _, _ := iostreams.Test()
+
+			reg := &httpmock.Registry{}
+			reg.StubRepoInfoResponse("OWNER", "REPO", "main")
+			reg.Register(
+				// ( is required to avoid matching projectsV2
+				httpmock.GraphQL(`projects\(`),
+				// Simulate a GraphQL error to early exit the test.
+				httpmock.StatusStringResponse(500, ""),
+			)
+
+			cs, cmdTeardown := run.Stub()
+			defer cmdTeardown(t)
+
+			cs.Register(`git config --get-regexp \^branch\\\..+\\\.\(remote\|merge\|pushremote\|gh-merge-base\)\$`, 0, "")
+
+			// Ignore the error because we have no way to really stub it without
+			// fully stubbing a GQL error structure in the request body.
+			_ = createRun(&CreateOptions{
+				Detector: &fd.EnabledDetectorMock{},
+				IO:       ios,
+				HttpClient: func() (*http.Client, error) {
+					return &http.Client{Transport: reg}, nil
+				},
+				GitClient: &git.Client{
+					GhPath:  "some/path/gh",
+					GitPath: "some/path/git",
+				},
+				Remotes: func() (context.Remotes, error) {
+					return context.Remotes{
+						{
+							Remote: &git.Remote{
+								Name:     "upstream",
+								Resolved: "base",
+							},
+							Repo: ghrepo.New("OWNER", "REPO"),
+						},
+					}, nil
+				},
+				Finder: shared.NewMockFinder("feature", nil, nil),
+
+				HeadBranch: "feature",
+
+				TitleProvided: true,
+				BodyProvided:  true,
+				Title:         "Test Title",
+				Body:          "Test Body",
+
+				// Required to force a lookup of projects
+				Projects: []string{"Project"},
+			})
+
+			// Verify that our request contained projects
+			reg.Verify(t)
+		})
+
+		t.Run("when projects v1 is not supported, does not query for it", func(t *testing.T) {
+			ios, _, _, _ := iostreams.Test()
+
+			reg := &httpmock.Registry{}
+			reg.StubRepoInfoResponse("OWNER", "REPO", "main")
+			// ( is required to avoid matching projectsV2
+			reg.Exclude(t, httpmock.GraphQL(`projects\(`))
+
+			cs, cmdTeardown := run.Stub()
+			defer cmdTeardown(t)
+
+			cs.Register(`git config --get-regexp \^branch\\\..+\\\.\(remote\|merge\|pushremote\|gh-merge-base\)\$`, 0, "")
+
+			// Ignore the error because we're not really interested in it.
+			_ = createRun(&CreateOptions{
+				Detector: &fd.DisabledDetectorMock{},
+				IO:       ios,
+				HttpClient: func() (*http.Client, error) {
+					return &http.Client{Transport: reg}, nil
+				},
+				GitClient: &git.Client{
+					GhPath:  "some/path/gh",
+					GitPath: "some/path/git",
+				},
+				Remotes: func() (context.Remotes, error) {
+					return context.Remotes{
+						{
+							Remote: &git.Remote{
+								Name:     "upstream",
+								Resolved: "base",
+							},
+							Repo: ghrepo.New("OWNER", "REPO"),
+						},
+					}, nil
+				},
+				Finder: shared.NewMockFinder("feature", nil, nil),
+
+				HeadBranch: "feature",
+
+				TitleProvided: true,
+				BodyProvided:  true,
+				Title:         "Test Title",
+				Body:          "Test Body",
+
+				// Required to force a lookup of projects
+				Projects: []string{"Project"},
+			})
+
+			// Verify that our request contained projectCards
+			reg.Verify(t)
+		})
+	})
+}
diff --git a/pkg/cmd/pr/merge/merge_test.go b/pkg/cmd/pr/merge/merge_test.go
index f1c2e37fe..4ca8c5d06 100644
--- a/pkg/cmd/pr/merge/merge_test.go
+++ b/pkg/cmd/pr/merge/merge_test.go
@@ -307,7 +307,7 @@ func TestPrMerge(t *testing.T) {
 	http := initFakeHTTP()
 	defer http.Verify(t)
 
-	shared.RunCommandFinder(
+	shared.StubFinderForRunCommandStyleTests(t,
 		"1",
 		&api.PullRequest{
 			ID:               "THE-ID",
@@ -348,7 +348,7 @@ func TestPrMerge_blocked(t *testing.T) {
 	http := initFakeHTTP()
 	defer http.Verify(t)
 
-	shared.RunCommandFinder(
+	shared.StubFinderForRunCommandStyleTests(t,
 		"1",
 		&api.PullRequest{
 			ID:               "THE-ID",
@@ -379,7 +379,7 @@ func TestPrMerge_dirty(t *testing.T) {
 	http := initFakeHTTP()
 	defer http.Verify(t)
 
-	shared.RunCommandFinder(
+	shared.StubFinderForRunCommandStyleTests(t,
 		"1",
 		&api.PullRequest{
 			ID:               "THE-ID",
@@ -413,7 +413,7 @@ func TestPrMerge_nontty(t *testing.T) {
 	http := initFakeHTTP()
 	defer http.Verify(t)
 
-	shared.RunCommandFinder(
+	shared.StubFinderForRunCommandStyleTests(t,
 		"1",
 		&api.PullRequest{
 			ID:               "THE-ID",
@@ -451,7 +451,7 @@ func TestPrMerge_editMessage_nontty(t *testing.T) {
 	http := initFakeHTTP()
 	defer http.Verify(t)
 
-	shared.RunCommandFinder(
+	shared.StubFinderForRunCommandStyleTests(t,
 		"1",
 		&api.PullRequest{
 			ID:               "THE-ID",
@@ -490,7 +490,7 @@ func TestPrMerge_withRepoFlag(t *testing.T) {
 	http := initFakeHTTP()
 	defer http.Verify(t)
 
-	shared.RunCommandFinder(
+	shared.StubFinderForRunCommandStyleTests(t,
 		"1",
 		&api.PullRequest{
 			ID:               "THE-ID",
@@ -529,7 +529,7 @@ func TestPrMerge_withMatchCommitHeadFlag(t *testing.T) {
 	http := initFakeHTTP()
 	defer http.Verify(t)
 
-	shared.RunCommandFinder(
+	shared.StubFinderForRunCommandStyleTests(t,
 		"1",
 		&api.PullRequest{
 			ID:               "THE-ID",
@@ -570,7 +570,7 @@ func TestPrMerge_withAuthorFlag(t *testing.T) {
 	http := initFakeHTTP()
 	defer http.Verify(t)
 
-	shared.RunCommandFinder(
+	shared.StubFinderForRunCommandStyleTests(t,
 		"1",
 		&api.PullRequest{
 			ID:               "THE-ID",
@@ -612,7 +612,7 @@ func TestPrMerge_deleteBranch(t *testing.T) {
 	http := initFakeHTTP()
 	defer http.Verify(t)
 
-	shared.RunCommandFinder(
+	shared.StubFinderForRunCommandStyleTests(t,
 		"",
 		&api.PullRequest{
 			ID:               "PR_10",
@@ -663,7 +663,7 @@ func TestPrMerge_deleteBranch_mergeQueue(t *testing.T) {
 	http := initFakeHTTP()
 	defer http.Verify(t)
 
-	shared.RunCommandFinder(
+	shared.StubFinderForRunCommandStyleTests(t,
 		"",
 		&api.PullRequest{
 			ID:                  "PR_10",
@@ -686,7 +686,7 @@ func TestPrMerge_deleteBranch_nonDefault(t *testing.T) {
 	http := initFakeHTTP()
 	defer http.Verify(t)
 
-	shared.RunCommandFinder(
+	shared.StubFinderForRunCommandStyleTests(t,
 		"",
 		&api.PullRequest{
 			ID:               "PR_10",
@@ -737,7 +737,7 @@ func TestPrMerge_deleteBranch_onlyLocally(t *testing.T) {
 	http := initFakeHTTP()
 	defer http.Verify(t)
 
-	shared.RunCommandFinder(
+	shared.StubFinderForRunCommandStyleTests(t,
 		"",
 		&api.PullRequest{
 			ID:                  "PR_10",
@@ -785,7 +785,7 @@ func TestPrMerge_deleteBranch_checkoutNewBranch(t *testing.T) {
 	http := initFakeHTTP()
 	defer http.Verify(t)
 
-	shared.RunCommandFinder(
+	shared.StubFinderForRunCommandStyleTests(t,
 		"",
 		&api.PullRequest{
 			ID:               "PR_10",
@@ -836,7 +836,7 @@ func TestPrMerge_deleteNonCurrentBranch(t *testing.T) {
 	http := initFakeHTTP()
 	defer http.Verify(t)
 
-	shared.RunCommandFinder(
+	shared.StubFinderForRunCommandStyleTests(t,
 		"blueberries",
 		&api.PullRequest{
 			ID:               "PR_10",
@@ -893,7 +893,7 @@ func Test_nonDivergingPullRequest(t *testing.T) {
 	}
 	stubCommit(pr, "COMMITSHA1")
 
-	shared.RunCommandFinder("", pr, baseRepo("OWNER", "REPO", "main"))
+	shared.StubFinderForRunCommandStyleTests(t, "", pr, baseRepo("OWNER", "REPO", "main"))
 
 	http.Register(
 		httpmock.GraphQL(`mutation PullRequestMerge\b`),
@@ -933,7 +933,7 @@ func Test_divergingPullRequestWarning(t *testing.T) {
 	}
 	stubCommit(pr, "COMMITSHA1")
 
-	shared.RunCommandFinder("", pr, baseRepo("OWNER", "REPO", "main"))
+	shared.StubFinderForRunCommandStyleTests(t, "", pr, baseRepo("OWNER", "REPO", "main"))
 
 	http.Register(
 		httpmock.GraphQL(`mutation PullRequestMerge\b`),
@@ -964,7 +964,7 @@ func Test_pullRequestWithoutCommits(t *testing.T) {
 	http := initFakeHTTP()
 	defer http.Verify(t)
 
-	shared.RunCommandFinder(
+	shared.StubFinderForRunCommandStyleTests(t,
 		"",
 		&api.PullRequest{
 			ID:               "PR_10",
@@ -1003,7 +1003,7 @@ func TestPrMerge_rebase(t *testing.T) {
 	http := initFakeHTTP()
 	defer http.Verify(t)
 
-	shared.RunCommandFinder(
+	shared.StubFinderForRunCommandStyleTests(t,
 		"2",
 		&api.PullRequest{
 			ID:               "THE-ID",
@@ -1044,7 +1044,7 @@ func TestPrMerge_squash(t *testing.T) {
 	http := initFakeHTTP()
 	defer http.Verify(t)
 
-	shared.RunCommandFinder(
+	shared.StubFinderForRunCommandStyleTests(t,
 		"3",
 		&api.PullRequest{
 			ID:               "THE-ID",
@@ -1084,7 +1084,7 @@ func TestPrMerge_alreadyMerged(t *testing.T) {
 	http := initFakeHTTP()
 	defer http.Verify(t)
 
-	shared.RunCommandFinder(
+	shared.StubFinderForRunCommandStyleTests(t,
 		"4",
 		&api.PullRequest{
 			ID:               "THE-ID",
@@ -1129,7 +1129,7 @@ func TestPrMerge_alreadyMerged_withMergeStrategy(t *testing.T) {
 	http := initFakeHTTP()
 	defer http.Verify(t)
 
-	shared.RunCommandFinder(
+	shared.StubFinderForRunCommandStyleTests(t,
 		"4",
 		&api.PullRequest{
 			ID:                  "THE-ID",
@@ -1159,7 +1159,7 @@ func TestPrMerge_alreadyMerged_withMergeStrategy_TTY(t *testing.T) {
 	http := initFakeHTTP()
 	defer http.Verify(t)
 
-	shared.RunCommandFinder(
+	shared.StubFinderForRunCommandStyleTests(t,
 		"4",
 		&api.PullRequest{
 			ID:                  "THE-ID",
@@ -1200,7 +1200,7 @@ func TestPrMerge_alreadyMerged_withMergeStrategy_crossRepo(t *testing.T) {
 	http := initFakeHTTP()
 	defer http.Verify(t)
 
-	shared.RunCommandFinder(
+	shared.StubFinderForRunCommandStyleTests(t,
 		"4",
 		&api.PullRequest{
 			ID:                  "THE-ID",
@@ -1239,7 +1239,7 @@ func TestPRMergeTTY(t *testing.T) {
 	http := initFakeHTTP()
 	defer http.Verify(t)
 
-	shared.RunCommandFinder(
+	shared.StubFinderForRunCommandStyleTests(t,
 		"",
 		&api.PullRequest{
 			ID:               "THE-ID",
@@ -1305,7 +1305,7 @@ func TestPRMergeTTY_withDeleteBranch(t *testing.T) {
 	http := initFakeHTTP()
 	defer http.Verify(t)
 
-	shared.RunCommandFinder(
+	shared.StubFinderForRunCommandStyleTests(t,
 		"",
 		&api.PullRequest{
 			ID:               "THE-ID",
@@ -1468,7 +1468,7 @@ func TestPRMergeEmptyStrategyNonTTY(t *testing.T) {
 	http := initFakeHTTP()
 	defer http.Verify(t)
 
-	shared.RunCommandFinder(
+	shared.StubFinderForRunCommandStyleTests(t,
 		"1",
 		&api.PullRequest{
 			ID:               "THE-ID",
@@ -1495,7 +1495,7 @@ func TestPRTTY_cancelled(t *testing.T) {
 	http := initFakeHTTP()
 	defer http.Verify(t)
 
-	shared.RunCommandFinder(
+	shared.StubFinderForRunCommandStyleTests(t,
 		"",
 		&api.PullRequest{ID: "THE-ID", Number: 123, Title: "title", MergeStateStatus: "CLEAN"},
 		ghrepo.New("OWNER", "REPO"),
@@ -1679,7 +1679,7 @@ func TestPrInMergeQueue(t *testing.T) {
 	http := initFakeHTTP()
 	defer http.Verify(t)
 
-	shared.RunCommandFinder(
+	shared.StubFinderForRunCommandStyleTests(t,
 		"1",
 		&api.PullRequest{
 			ID:                  "THE-ID",
@@ -1710,7 +1710,7 @@ func TestPrAddToMergeQueueWithMergeMethod(t *testing.T) {
 	http := initFakeHTTP()
 	defer http.Verify(t)
 
-	shared.RunCommandFinder(
+	shared.StubFinderForRunCommandStyleTests(t,
 		"1",
 		&api.PullRequest{
 			ID:                  "THE-ID",
@@ -1748,7 +1748,7 @@ func TestPrAddToMergeQueueClean(t *testing.T) {
 	http := initFakeHTTP()
 	defer http.Verify(t)
 
-	shared.RunCommandFinder(
+	shared.StubFinderForRunCommandStyleTests(t,
 		"1",
 		&api.PullRequest{
 			ID:                  "THE-ID",
@@ -1788,7 +1788,7 @@ func TestPrAddToMergeQueueBlocked(t *testing.T) {
 	http := initFakeHTTP()
 	defer http.Verify(t)
 
-	shared.RunCommandFinder(
+	shared.StubFinderForRunCommandStyleTests(t,
 		"1",
 		&api.PullRequest{
 			ID:                  "THE-ID",
@@ -1828,7 +1828,7 @@ func TestPrAddToMergeQueueAdmin(t *testing.T) {
 	http := initFakeHTTP()
 	defer http.Verify(t)
 
-	shared.RunCommandFinder(
+	shared.StubFinderForRunCommandStyleTests(t,
 		"1",
 		&api.PullRequest{
 			ID:                  "THE-ID",
@@ -1897,7 +1897,7 @@ func TestPrAddToMergeQueueAdminWithMergeStrategy(t *testing.T) {
 	http := initFakeHTTP()
 	defer http.Verify(t)
 
-	shared.RunCommandFinder(
+	shared.StubFinderForRunCommandStyleTests(t,
 		"1",
 		&api.PullRequest{
 			ID:               "THE-ID",
diff --git a/pkg/cmd/pr/ready/ready_test.go b/pkg/cmd/pr/ready/ready_test.go
index 9046ab3ac..5a6053a17 100644
--- a/pkg/cmd/pr/ready/ready_test.go
+++ b/pkg/cmd/pr/ready/ready_test.go
@@ -124,7 +124,7 @@ func TestPRReady(t *testing.T) {
 	http := &httpmock.Registry{}
 	defer http.Verify(t)
 
-	shared.RunCommandFinder("123", &api.PullRequest{
+	shared.StubFinderForRunCommandStyleTests(t, "123", &api.PullRequest{
 		ID:      "THE-ID",
 		Number:  123,
 		State:   "OPEN",
@@ -149,7 +149,7 @@ func TestPRReady_alreadyReady(t *testing.T) {
 	http := &httpmock.Registry{}
 	defer http.Verify(t)
 
-	shared.RunCommandFinder("123", &api.PullRequest{
+	shared.StubFinderForRunCommandStyleTests(t, "123", &api.PullRequest{
 		ID:      "THE-ID",
 		Number:  123,
 		State:   "OPEN",
@@ -166,7 +166,7 @@ func TestPRReadyUndo(t *testing.T) {
 	http := &httpmock.Registry{}
 	defer http.Verify(t)
 
-	shared.RunCommandFinder("123", &api.PullRequest{
+	shared.StubFinderForRunCommandStyleTests(t, "123", &api.PullRequest{
 		ID:      "THE-ID",
 		Number:  123,
 		State:   "OPEN",
@@ -191,7 +191,7 @@ func TestPRReadyUndo_alreadyDraft(t *testing.T) {
 	http := &httpmock.Registry{}
 	defer http.Verify(t)
 
-	shared.RunCommandFinder("123", &api.PullRequest{
+	shared.StubFinderForRunCommandStyleTests(t, "123", &api.PullRequest{
 		ID:      "THE-ID",
 		Number:  123,
 		State:   "OPEN",
@@ -208,7 +208,7 @@ func TestPRReady_closed(t *testing.T) {
 	http := &httpmock.Registry{}
 	defer http.Verify(t)
 
-	shared.RunCommandFinder("123", &api.PullRequest{
+	shared.StubFinderForRunCommandStyleTests(t, "123", &api.PullRequest{
 		ID:      "THE-ID",
 		Number:  123,
 		State:   "CLOSED",
diff --git a/pkg/cmd/pr/reopen/reopen_test.go b/pkg/cmd/pr/reopen/reopen_test.go
index 856e19172..9fb3702c0 100644
--- a/pkg/cmd/pr/reopen/reopen_test.go
+++ b/pkg/cmd/pr/reopen/reopen_test.go
@@ -53,7 +53,7 @@ func TestPRReopen(t *testing.T) {
 	http := &httpmock.Registry{}
 	defer http.Verify(t)
 
-	shared.RunCommandFinder("123", &api.PullRequest{
+	shared.StubFinderForRunCommandStyleTests(t, "123", &api.PullRequest{
 		ID:     "THE-ID",
 		Number: 123,
 		State:  "CLOSED",
@@ -78,7 +78,7 @@ func TestPRReopen_alreadyOpen(t *testing.T) {
 	http := &httpmock.Registry{}
 	defer http.Verify(t)
 
-	shared.RunCommandFinder("123", &api.PullRequest{
+	shared.StubFinderForRunCommandStyleTests(t, "123", &api.PullRequest{
 		ID:     "THE-ID",
 		Number: 123,
 		State:  "OPEN",
@@ -95,7 +95,7 @@ func TestPRReopen_alreadyMerged(t *testing.T) {
 	http := &httpmock.Registry{}
 	defer http.Verify(t)
 
-	shared.RunCommandFinder("123", &api.PullRequest{
+	shared.StubFinderForRunCommandStyleTests(t, "123", &api.PullRequest{
 		ID:     "THE-ID",
 		Number: 123,
 		State:  "MERGED",
@@ -112,7 +112,7 @@ func TestPRReopen_withComment(t *testing.T) {
 	http := &httpmock.Registry{}
 	defer http.Verify(t)
 
-	shared.RunCommandFinder("123", &api.PullRequest{
+	shared.StubFinderForRunCommandStyleTests(t, "123", &api.PullRequest{
 		ID:     "THE-ID",
 		Number: 123,
 		State:  "CLOSED",
diff --git a/pkg/cmd/pr/review/review_test.go b/pkg/cmd/pr/review/review_test.go
index f9e00c3b8..684617ca9 100644
--- a/pkg/cmd/pr/review/review_test.go
+++ b/pkg/cmd/pr/review/review_test.go
@@ -235,7 +235,7 @@ func TestPRReview(t *testing.T) {
 			http := &httpmock.Registry{}
 			defer http.Verify(t)
 
-			shared.RunCommandFinder("", &api.PullRequest{ID: "THE-ID"}, ghrepo.New("OWNER", "REPO"))
+			shared.StubFinderForRunCommandStyleTests(t, "", &api.PullRequest{ID: "THE-ID"}, ghrepo.New("OWNER", "REPO"))
 
 			http.Register(
 				httpmock.GraphQL(`mutation PullRequestReviewAdd\b`),
@@ -261,7 +261,7 @@ func TestPRReview_interactive(t *testing.T) {
 	http := &httpmock.Registry{}
 	defer http.Verify(t)
 
-	shared.RunCommandFinder("", &api.PullRequest{ID: "THE-ID", Number: 123}, ghrepo.New("OWNER", "REPO"))
+	shared.StubFinderForRunCommandStyleTests(t, "", &api.PullRequest{ID: "THE-ID", Number: 123}, ghrepo.New("OWNER", "REPO"))
 
 	http.Register(
 		httpmock.GraphQL(`mutation PullRequestReviewAdd\b`),
@@ -293,7 +293,7 @@ func TestPRReview_interactive_no_body(t *testing.T) {
 	http := &httpmock.Registry{}
 	defer http.Verify(t)
 
-	shared.RunCommandFinder("", &api.PullRequest{ID: "THE-ID", Number: 123}, ghrepo.New("OWNER", "REPO"))
+	shared.StubFinderForRunCommandStyleTests(t, "", &api.PullRequest{ID: "THE-ID", Number: 123}, ghrepo.New("OWNER", "REPO"))
 
 	pm := &prompter.PrompterMock{
 		SelectFunc:         func(_, _ string, _ []string) (int, error) { return 2, nil },
@@ -308,7 +308,7 @@ func TestPRReview_interactive_blank_approve(t *testing.T) {
 	http := &httpmock.Registry{}
 	defer http.Verify(t)
 
-	shared.RunCommandFinder("", &api.PullRequest{ID: "THE-ID", Number: 123}, ghrepo.New("OWNER", "REPO"))
+	shared.StubFinderForRunCommandStyleTests(t, "", &api.PullRequest{ID: "THE-ID", Number: 123}, ghrepo.New("OWNER", "REPO"))
 
 	http.Register(
 		httpmock.GraphQL(`mutation PullRequestReviewAdd\b`),
diff --git a/pkg/cmd/pr/shared/finder_test.go b/pkg/cmd/pr/shared/finder_test.go
index e1aae16b1..abc754d1a 100644
--- a/pkg/cmd/pr/shared/finder_test.go
+++ b/pkg/cmd/pr/shared/finder_test.go
@@ -165,6 +165,23 @@ func TestFind(t *testing.T) {
 			wantPR:   13,
 			wantRepo: "https://github.com/ORIGINOWNER/REPO",
 		},
+		{
+			name: "pr number zero",
+			args: args{
+				selector:   "0",
+				fields:     []string{"number"},
+				baseRepoFn: stubBaseRepoFn(ghrepo.New("ORIGINOWNER", "REPO"), nil),
+				branchFn: func() (string, error) {
+					return "blueberries", nil
+				},
+				gitConfigClient: stubGitConfigClient{
+					readBranchConfigFn:  stubBranchConfig(git.BranchConfig{}, nil),
+					pushDefaultFn:       stubPushDefault(git.PushDefaultSimple, nil),
+					remotePushDefaultFn: stubRemotePushDefault("", nil),
+				},
+			},
+			wantErr: true,
+		},
 		{
 			name: "number with hash argument",
 			args: args{
diff --git a/pkg/cmd/pr/view/view_test.go b/pkg/cmd/pr/view/view_test.go
index 2cd4066b8..35f7fa513 100644
--- a/pkg/cmd/pr/view/view_test.go
+++ b/pkg/cmd/pr/view/view_test.go
@@ -12,6 +12,7 @@ import (
 
 	"github.com/cli/cli/v2/api"
 	"github.com/cli/cli/v2/internal/browser"
+	fd "github.com/cli/cli/v2/internal/featuredetection"
 	"github.com/cli/cli/v2/internal/ghrepo"
 	"github.com/cli/cli/v2/internal/run"
 	"github.com/cli/cli/v2/pkg/cmd/pr/shared"
@@ -176,6 +177,9 @@ func runCommand(rt http.RoundTripper, branch string, isTTY bool, cli string) (*t
 	factory := &cmdutil.Factory{
 		IOStreams: ios,
 		Browser:   browser,
+		HttpClient: func() (*http.Client, error) {
+			return &http.Client{Transport: rt}, nil
+		},
 	}
 
 	cmd := NewCmdView(factory, nil)
@@ -404,7 +408,7 @@ func TestPRView_Preview_nontty(t *testing.T) {
 
 			pr, err := prFromFixtures(tc.fixtures)
 			require.NoError(t, err)
-			shared.RunCommandFinder("12", pr, ghrepo.New("OWNER", "REPO"))
+			shared.StubFinderForRunCommandStyleTests(t, "12", pr, ghrepo.New("OWNER", "REPO"))
 
 			output, err := runCommand(http, tc.branch, false, tc.args)
 			if err != nil {
@@ -608,7 +612,7 @@ func TestPRView_Preview(t *testing.T) {
 
 			pr, err := prFromFixtures(tc.fixtures)
 			require.NoError(t, err)
-			shared.RunCommandFinder("12", pr, ghrepo.New("OWNER", "REPO"))
+			shared.StubFinderForRunCommandStyleTests(t, "12", pr, ghrepo.New("OWNER", "REPO"))
 
 			output, err := runCommand(http, tc.branch, true, tc.args)
 			if err != nil {
@@ -631,7 +635,7 @@ func TestPRView_web_currentBranch(t *testing.T) {
 	http := &httpmock.Registry{}
 	defer http.Verify(t)
 
-	shared.RunCommandFinder("", &api.PullRequest{URL: "https://github.com/OWNER/REPO/pull/10"}, ghrepo.New("OWNER", "REPO"))
+	shared.StubFinderForRunCommandStyleTests(t, "", &api.PullRequest{URL: "https://github.com/OWNER/REPO/pull/10"}, ghrepo.New("OWNER", "REPO"))
 
 	_, cmdTeardown := run.Stub()
 	defer cmdTeardown(t)
@@ -650,7 +654,7 @@ func TestPRView_web_noResultsForBranch(t *testing.T) {
 	http := &httpmock.Registry{}
 	defer http.Verify(t)
 
-	shared.RunCommandFinder("", nil, nil)
+	shared.StubFinderForRunCommandStyleTests(t, "", nil, nil)
 
 	_, cmdTeardown := run.Stub()
 	defer cmdTeardown(t)
@@ -742,9 +746,9 @@ func TestPRView_tty_Comments(t *testing.T) {
 			if len(tt.fixtures) > 0 {
 				pr, err := prFromFixtures(tt.fixtures)
 				require.NoError(t, err)
-				shared.RunCommandFinder("123", pr, ghrepo.New("OWNER", "REPO"))
+				shared.StubFinderForRunCommandStyleTests(t, "123", pr, ghrepo.New("OWNER", "REPO"))
 			} else {
-				shared.RunCommandFinder("123", nil, nil)
+				shared.StubFinderForRunCommandStyleTests(t, "123", nil, nil)
 			}
 
 			output, err := runCommand(http, tt.branch, true, tt.cli)
@@ -853,9 +857,9 @@ func TestPRView_nontty_Comments(t *testing.T) {
 			if len(tt.fixtures) > 0 {
 				pr, err := prFromFixtures(tt.fixtures)
 				require.NoError(t, err)
-				shared.RunCommandFinder("123", pr, ghrepo.New("OWNER", "REPO"))
+				shared.StubFinderForRunCommandStyleTests(t, "123", pr, ghrepo.New("OWNER", "REPO"))
 			} else {
-				shared.RunCommandFinder("123", nil, nil)
+				shared.StubFinderForRunCommandStyleTests(t, "123", nil, nil)
 			}
 
 			output, err := runCommand(http, tt.branch, false, tt.cli)
@@ -870,3 +874,74 @@ func TestPRView_nontty_Comments(t *testing.T) {
 		})
 	}
 }
+
+// TODO projectsV1Deprecation
+// Remove this test.
+func TestProjectsV1Deprecation(t *testing.T) {
+	t.Run("when projects v1 is supported, is included in query", func(t *testing.T) {
+		ios, _, _, _ := iostreams.Test()
+
+		reg := &httpmock.Registry{}
+		reg.Register(
+			httpmock.GraphQL(`projectCards`),
+			// Simulate a GraphQL error to early exit the test.
+			httpmock.StatusStringResponse(500, ""),
+		)
+
+		f := &cmdutil.Factory{
+			IOStreams: ios,
+			HttpClient: func() (*http.Client, error) {
+				return &http.Client{Transport: reg}, nil
+			},
+		}
+
+		_, cmdTeardown := run.Stub()
+		defer cmdTeardown(t)
+
+		// Ignore the error because we have no way to really stub it without
+		// fully stubbing a GQL error structure in the request body.
+		_ = viewRun(&ViewOptions{
+			IO:       ios,
+			Finder:   shared.NewFinder(f),
+			Detector: &fd.EnabledDetectorMock{},
+
+			SelectorArg: "https://github.com/cli/cli/pull/123",
+		})
+
+		// Verify that our request contained projectCards
+		reg.Verify(t)
+	})
+
+	t.Run("when projects v1 is not supported, is not included in query", func(t *testing.T) {
+		ios, _, _, _ := iostreams.Test()
+
+		reg := &httpmock.Registry{}
+		reg.Exclude(
+			t,
+			httpmock.GraphQL(`projectCards`),
+		)
+
+		f := &cmdutil.Factory{
+			IOStreams: ios,
+			HttpClient: func() (*http.Client, error) {
+				return &http.Client{Transport: reg}, nil
+			},
+		}
+
+		_, cmdTeardown := run.Stub()
+		defer cmdTeardown(t)
+
+		// Ignore the error because we have no way to really stub it without
+		// fully stubbing a GQL error structure in the request body.
+		_ = viewRun(&ViewOptions{
+			IO:       ios,
+			Finder:   shared.NewFinder(f),
+			Detector: &fd.DisabledDetectorMock{},
+
+			SelectorArg: "https://github.com/cli/cli/pull/123",
+		})
+
+		// Verify that our request contained projectCards
+		reg.Verify(t)
+	})
+}
diff --git a/pkg/cmd/repo/autolink/delete/http_test.go b/pkg/cmd/repo/autolink/delete/http_test.go
index a2676178d..a0aec5e13 100644
--- a/pkg/cmd/repo/autolink/delete/http_test.go
+++ b/pkg/cmd/repo/autolink/delete/http_test.go
@@ -7,6 +7,7 @@ import (
 
 	"github.com/cli/cli/v2/internal/ghrepo"
 	"github.com/cli/cli/v2/pkg/httpmock"
+	"github.com/cli/go-gh/v2/pkg/api"
 	"github.com/stretchr/testify/require"
 )
 
@@ -14,10 +15,10 @@ func TestAutolinkDeleter_Delete(t *testing.T) {
 	repo := ghrepo.New("OWNER", "REPO")
 
 	tests := []struct {
-		name         string
-		id           string
-		stubStatus   int
-		stubRespJSON string
+		name       string
+		id         string
+		stubStatus int
+		stubResp   any
 
 		expectErr      bool
 		expectedErrMsg string
@@ -31,17 +32,18 @@ func TestAutolinkDeleter_Delete(t *testing.T) {
 			name:           "404 repo or autolink not found",
 			id:             "123",
 			stubStatus:     http.StatusNotFound,
-			stubRespJSON:   `{}`, // API response not used in output
 			expectErr:      true,
 			expectedErrMsg: "error deleting autolink: HTTP 404: Perhaps you are missing admin rights to the repository? (https://api.github.com/repos/OWNER/REPO/autolinks/123)",
 		},
 		{
-			name:           "500 unexpected error",
-			id:             "123",
-			stubRespJSON:   `{"messsage": "arbitrary error"}`,
+			name: "500 unexpected error",
+			id:   "123",
+			stubResp: api.HTTPError{
+				Message: "arbitrary error",
+			},
 			stubStatus:     http.StatusInternalServerError,
 			expectErr:      true,
-			expectedErrMsg: "HTTP 500 (https://api.github.com/repos/OWNER/REPO/autolinks/123)",
+			expectedErrMsg: "HTTP 500: arbitrary error (https://api.github.com/repos/OWNER/REPO/autolinks/123)",
 		},
 	}
 
@@ -53,7 +55,7 @@ func TestAutolinkDeleter_Delete(t *testing.T) {
 					http.MethodDelete,
 					fmt.Sprintf("repos/%s/%s/autolinks/%s", repo.RepoOwner(), repo.RepoName(), tt.id),
 				),
-				httpmock.StatusJSONResponse(tt.stubStatus, tt.stubRespJSON),
+				httpmock.StatusJSONResponse(tt.stubStatus, tt.stubResp),
 			)
 			defer reg.Verify(t)
 
diff --git a/pkg/cmd/run/watch/watch_test.go b/pkg/cmd/run/watch/watch_test.go
index d42e8d3d8..49e56217b 100644
--- a/pkg/cmd/run/watch/watch_test.go
+++ b/pkg/cmd/run/watch/watch_test.go
@@ -316,7 +316,7 @@ func TestWatchRun(t *testing.T) {
 				)
 				reg.Register(
 					httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"),
-					httpmock.StatusJSONResponse(404, api.HTTPError{
+					httpmock.JSONErrorResponse(404, api.HTTPError{
 						StatusCode: 404,
 						Message:    "run 1234 not found",
 					}),