# PR #23660 fixture matrix (audit pass 1, head 33a1b866ae, base 60b35a1ee0)

Only one call site was added: `runPrStep` (GitManager.ts:2403). It runs after `findOpenPr`
returns nothing and after `generatePrContent`, and only when `provider.kind === "github"`. Both
`create_pr` and `commit_push_pr` reach it. `status`, `findOpenPr`, `findLatestPr` and
`resolvePrBase` never call it.

The provider (`packages/source-control-github/src/server/GitHubSourceControlProvider.ts:930`)
calls `resolveRepository()`, which uses GH_REPO first, then `git remote -v` plus
`config --get-regexp ^remote\..*\.gh-resolved$`, then `resolveGitHubRepository`. It then sends
`POST https://api.github.com/repos/<owner>/<name>/pulls` with body
`{base, head, title, body, maintainer_can_modify}`. The request goes through Effect
`HttpClient` (`FetchHttpClient`, so `globalThis.fetch`), not a `gh` subprocess. `gh` runs only
for `gh auth token` when GH_TOKEN is unset.

Notation: F = `juliusmarminge/t3code` (fork), U = `pingdotgg/t3code` (source), branch `fix`.

| # | Layout | Staging | Affected? | Expected before (POST target / head) | Expected after |
|---|---|---|---|---|---|
| 1 | origin=F, upstream=U, `remote.upstream.gh-resolved base`, branch tracks `origin/fix` | driver | **Yes** (the bug) | `repos/U/pulls` head `fix` | `repos/U/pulls` head `juliusmarminge:fix` |
| 1b | Same as 1 but **no** gh-resolved mark (gh's own ranking: upstream beats origin) | driver | **Yes** | `repos/U/pulls` head `fix` | `repos/U/pulls` head `juliusmarminge:fix` |
| 1c | Same as 1 but the mark is `remote.origin.gh-resolved pingdotgg/t3code` (explicit owner/name mark) | driver | **Yes** | `repos/U/pulls` head `fix` | `repos/U/pulls` head `juliusmarminge:fix` |
| 1d | origin=F, upstream=U, `remote.origin.gh-resolved base` (set-default to the fork) | driver | No | `repos/F/pulls` head `fix` | `repos/F/pulls` head `fix` |
| 2 | GH_REPO=`pingdotgg/t3code`, origin=F only, branch tracks `origin/fix` | driver (env in `HostProcess.Environment`) | **Yes** | `repos/U/pulls` head `fix` | `repos/U/pulls` head `juliusmarminge:fix` |
| 2b | GH_REPO=`juliusmarminge/t3code`, layout of row 1 (GH_REPO beats the mark) | driver | No | `repos/F/pulls` head `fix` | `repos/F/pulls` head `fix` |
| 2c | GH_REPO=`not-a-repo` (junk), layout of row 1 | driver | **Yes** (falls through to the remotes) | `repos/U/pulls` head `fix` | `repos/U/pulls` head `juliusmarminge:fix` |
| 3 | Non-fork clone: origin=U only, branch tracks `origin/fix` | driver | No | `repos/U/pulls` head `fix` | `repos/U/pulls` head `fix` |
| 4 | origin=F, upstream=U + `gh-resolved base`, branch tracks **`upstream/fix`** | driver | No (head repo == target, so `preferredHeadSelector` is used, which is already cross-repo) | `repos/U/pulls` head `pingdotgg:fix` | `repos/U/pulls` head `pingdotgg:fix` |
| 4b | origin=F, upstream=U, `remote.origin.gh-resolved base`, branch tracks `upstream/fix` | driver | No (head != target, but the owner-qualified value is identical) | `repos/F/pulls` head `pingdotgg:fix` | `repos/F/pulls` head `pingdotgg:fix` |
| 5 | Row-1 layout: `manager.status` and a PR lookup (`findOpenPr` through `listChangeRequests`) | driver, spying on spawned git argv | No | no `config --get-regexp ^remote\..*\.(url\|gh-resolved)$` spawned; GraphQL `PullRequestsByHead` reads only | same: zero spawns of that regexp (the provider's own `...\.gh-resolved$` regexp is a different string) |
| 5b | Row-1 layout, but an open PR already exists (GraphQL returns a node) | driver | No (returns `opened_existing` before the new code) | no POST; `opened_existing` | no POST; no `(url\|gh-resolved)` spawn |
| 6 | origin non-GitHub (`https://gitlab.com/x/y.git`), upstream=U | driver (or unit) | No (`provider.kind !== "github"`, or `resolveGitHubTargetRepository` returns null because origin is not GitHub) | provider-dependent, no GitHub POST if GitLab is resolved | same |
| 7 | **No origin**: remotes `fork`=F, `upstream`=U + mark, branch tracks `fork/fix` | driver | No (the target is null without origin; the existing `isForeignRemote` path already qualifies) | `repos/U/pulls` head `juliusmarminge:fix` | same |
| 8 | Head repo unresolvable: branch tracks remote `local` with a bare path URL, origin=F, upstream=U + mark | driver | No (head owner/name is null, so `preferredHeadSelector` is used) | head `fix` | head `fix` |
| 9 | Locator null although origin is GitHub (origin URL `https://github.com/a/b/c.git`, 3-segment path) | unit only (practically unreachable) | No | provider fails "No GitHub repository…" or uses the context fallback | same |
| 10 | Divergence trap: row 1 with `url.<local>.insteadOf https://github.com/...` | driver (negative control) | Yes (the fix can be silently wrong here) | `git remote -v` shows the local paths, so the provider fails to resolve a GitHub repo | GitManager reads raw config URLs (target U) while the provider sees no GitHub remote. The new comment acknowledges this; real users with `insteadOf` pointing to the same repo over SSH stay consistent |

Rows 1, 1b, 1c, 2 and 2c show the fix. Rows 1d, 2b, 3, 4, 4b, 7 and 8 are unchanged-head
controls. Rows 5 and 5b are the "status/lookup untouched" claim. Row 10 is a staging
pitfall, not a product row. Rows 6 and 9 are optional.

## Staging recipe (driver; no dev server needed)

Use a node script (`node /tmp/ev-23660/driver.ts <worktree> <row>`; Node 24 strips types, the
same way `apps/server` runs `node src/bin.ts`). Run it once against `/home/ubuntu/wt/pr-23660-before`
and once against `/home/ubuntu/wt/pr-23660`. Import the worktree's `.ts` files by absolute path so
each run gets its own `effect` from that worktree's node_modules.

1. **fetch stub first.** Before any import, replace `globalThis.fetch`. The FetchHttpClient
   `Fetch` reference defaults to `() => globalThis.fetch`. Log `method url body` to
   `/tmp/ev-23660/<side>/<row>.log`. Answer as follows:
   - `POST */graphql`: return `{"data":{"repository":{"pullRequests":{"nodes":[]}}}}`, or one
     node for row 5b.
   - `POST */repos/*/pulls`: return 422 with
     `{"message":"Validation Failed","errors":[{"resource":"PullRequest","field":"head","code":"invalid"}]}`.
   - Other GETs (quota/rate_limit, templates): 200 `{}` or 404.
   - Any other write: 422.
   - Any host other than `api.github.com`: throw.
2. **No real gh.** Put a fake `gh` first on PATH (`/tmp/ev-23660/bin/gh`) that appends argv and
   stdin to a log and exits 1. Also pass `GH_TOKEN=stub` in `HostProcess.Environment` so
   `GitHubCredentials` never shells out to `gh auth token`. Set GH_REPO only in that Environment
   (rows 2/2b/2c). Leave `process.env` alone, because GitManager and the provider both read
   `HostProcess.Environment`.
3. **Repository layout** (per row, under `/tmp/ev-23660/<side>/<row>/`): `git init` a work repo
   and two bare repos (`fork.git`, `src.git`).
   - Set `remote.origin.url` / `remote.upstream.url` to the **visible GitHub URLs**.
   - Route pushes with `url./tmp/.../fork.git.pushInsteadOf https://github.com/juliusmarminge/t3code.git`.
     Use **pushInsteadOf**, not insteadOf: with insteadOf, `git remote -v` prints local paths, the
     provider then finds no GitHub repo, and the result diverges from GitManager's raw-config read
     (row 10). This is verified locally: pushInsteadOf leaves the `(fetch)` line as the GitHub URL.
   - The fork case needs remote-tracking refs without fetching from GitHub. Push `main` to both bare
     repos, then `git push -u origin fix`. The push creates `refs/remotes/origin/fix` and the upstream
     config. For `upstream/main`, run `git update-ref refs/remotes/upstream/main main`.
   - Add the gh-resolved mark per row (`git config remote.upstream.gh-resolved base`).
4. **Layers.** Mirror `server.ts` `layerGitManager`, using the real GitHub provider:
   `GitManager.layer`, then the following, each with `Layer.provideMerge`:
   - `SourceControlProviderRegistry.layer` with `sourceControl/builtInDrivers.ts` `layer`,
     `GitVcsDriver.layer` and `VcsDriverRegistry.layer` (+`VcsProjectConfig.layer`)
   - `SourceControlRateLimit.layer`
   - `FetchHttpClient.layer`
   - `ServerConfig.layerTest(tmp, {prefix})`
   - `ServerSettings.ServerSettingsService.layerTest()`
   - `Layer.succeed(TextGeneration.TextGeneration, {generatePrContent: () => Effect.succeed({title:"t", body:"b"}), …})`
   - `Layer.mock(ProviderRegistry.ProviderRegistry)({getProviders: Effect.succeed([])})`
   - `ProjectSetupScriptRunner` returning `no-script`
   - `ProjectionStore.layer` + `ProjectStore.layer` over `SqlitePersistence.layerMemory`
   - `NodeServices.layer`

   Copy the shape from `GitManager.test.ts` `makeManager` (around lines 743–880) and replace the fake
   registry with the real one. Provide `HostProcess.Environment` with `{GH_TOKEN:"stub", PATH, HOME, GH_REPO?}`.
5. **Entry points.**
   - Rows 1–4, 6–9: `manager.runStackedAction({cwd, action:"create_pr"})`. The branch already has
     its upstream, so no push phase runs. Expected result: the 422 surfaces as `createChangeRequest: …
     head invalid`. The evidence is the logged `POST repos/<o>/<n>/pulls` body `head`.
   - Row 5: `manager.status({cwd})`. Wrap the `ChildProcessSpawner` the way `makeManager`'s
     `gitCommands` does, and assert that no argv contains `^remote\..*\.(url|gh-resolved)$`.
     Do the same for 5b (`create_pr` with an existing PR, so the result is `opened_existing` and
     no POST is logged).
6. Compare the before and after logs per row against the matrix columns.
