Skip to content
Open
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
251 changes: 207 additions & 44 deletions apps/server/src/git/GitManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,13 +371,22 @@ function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): {
headSelectorIndex >= 0 && headSelectorIndex < args.length - 1
? args[headSelectorIndex + 1]
: undefined;
const mappedQueue =
const mappedHeadSelector =
typeof headSelector === "string"
? prListQueueByHeadSelector.get(headSelector)?.shift()
? ([
...prListQueueByHeadSelector.keys(),
...Object.keys(scenario.prListByHeadSelector ?? {}),
].find(
(candidate) => candidate === headSelector || candidate.endsWith(`:${headSelector}`),
) ?? headSelector)
: undefined;
const mappedQueue =
typeof mappedHeadSelector === "string"
? prListQueueByHeadSelector.get(mappedHeadSelector)?.shift()
: undefined;
const mappedStdout =
typeof headSelector === "string"
? scenario.prListByHeadSelector?.[headSelector]
typeof mappedHeadSelector === "string"
? scenario.prListByHeadSelector?.[mappedHeadSelector]
: undefined;
const stdout = (mappedQueue ?? mappedStdout ?? prListQueue.shift() ?? "[]") + "\n";
return Effect.succeed(fakeGhOutput(stdout));
Expand All @@ -392,6 +401,18 @@ function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): {
}

if (args[0] === "pr" && args[1] === "view") {
const selector = args[2];
const queuedPullRequests =
typeof selector === "string"
? (prListQueueByHeadSelector.get(selector)?.shift() ??
scenario.prListByHeadSelector?.[selector] ??
prListQueue.shift())
: undefined;
if (queuedPullRequests !== undefined) {
const parsed = JSON.parse(queuedPullRequests) as unknown;
const candidate = Array.isArray(parsed) ? parsed[0] : parsed;
return Effect.succeed(fakeGhOutput(candidate ? `${JSON.stringify(candidate)}\n` : ""));
}
const pullRequest: FakePullRequest = scenario.pullRequest ?? {
number: 101,
title: "Pull request",
Expand Down Expand Up @@ -493,29 +514,57 @@ function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): {
return {
service: {
execute,
listOpenPullRequests: (input) =>
execute({
cwd: input.cwd,
args: [
"pr",
"list",
"--head",
input.headSelector,
"--state",
"open",
"--limit",
String(input.limit ?? 1),
"--json",
"number,title,url,baseRefName,headRefName,state,mergedAt,isCrossRepository,headRepository,headRepositoryOwner",
],
}).pipe(
Effect.map((result) => JSON.parse(result.stdout) as unknown[]),
listOpenPullRequests: (input) => {
const qualifiedHead = /^([^:/\s]+):(.+)$/u.exec(input.headSelector);
const requestedLimit = input.limit ?? 1;
const args = qualifiedHead
? [
"pr",
"view",
input.headSelector,
...(input.repository ? ["--repo", input.repository] : []),
"--json",
"number,title,url,baseRefName,headRefName,state,mergedAt,isCrossRepository,headRepository,headRepositoryOwner",
]
: [
"pr",
"list",
"--head",
input.headSelector,
"--state",
"open",
"--limit",
String(requestedLimit),
...(input.repository ? ["--repo", input.repository] : []),
"--json",
"number,title,url,baseRefName,headRefName,state,mergedAt,isCrossRepository,headRepository,headRepositoryOwner",
];
ghCalls.push(args.join(" "));
if (scenario.failWith && ghCalls.length > (scenario.failAfterCalls ?? 0)) {
return Effect.fail(scenario.failWith);
}
const stdout =
prListQueueByHeadSelector.get(input.headSelector)?.shift() ??
scenario.prListByHeadSelector?.[input.headSelector] ??
prListQueue.shift() ??
"[]";
// @effect-diagnostics-next-line preferSchemaOverJson:off
return Effect.sync(() => JSON.parse(stdout) as unknown).pipe(
Effect.map((raw) =>
raw
(Array.isArray(raw) ? raw : [raw])
.map((entry) => normalizeFakePullRequestSummary(entry))
.filter((entry): entry is GitHubCli.GitHubPullRequestSummary => entry !== null),
.filter((entry): entry is GitHubCli.GitHubPullRequestSummary => entry !== null)
.filter(
(entry) =>
!qualifiedHead ||
(entry.headRefName === qualifiedHead[2] &&
entry.headRepositoryOwnerLogin?.toLowerCase() ===
qualifiedHead[1]?.toLowerCase()),
)
.slice(0, requestedLimit),
),
),
);
},
createPullRequest: (input) =>
execute({
cwd: input.cwd,
Expand Down Expand Up @@ -1007,6 +1056,62 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
}),
);

it.effect("uses the origin fork identity for a branch pushed without tracking", () =>
Effect.gen(function* () {
const repoDir = yield* makeTempDir("t3code-git-manager-");
yield* initRepo(repoDir);
const originDir = yield* createBareRemote();
const upstreamDir = yield* createBareRemote();
yield* runGit(repoDir, ["remote", "add", "origin", originDir]);
yield* runGit(repoDir, ["remote", "add", "upstream", upstreamDir]);
yield* runGit(repoDir, ["push", "-u", "origin", "main"]);
yield* runGit(repoDir, ["push", "upstream", "main"]);
yield* configureVisibleRemoteUrlWithLocalRewrite(
repoDir,
"origin",
"git@github.com:octocat/codething-mvp.git",
originDir,
);
yield* runGit(repoDir, ["config", "remote.origin.pushurl", originDir]);
yield* configureVisibleRemoteUrlWithLocalRewrite(
repoDir,
"upstream",
"git@github.com:pingdotgg/codething-mvp.git",
upstreamDir,
);
yield* runGit(repoDir, ["checkout", "-b", "feature/pushed-no-tracking"]);
yield* runGit(repoDir, ["push", "origin", "feature/pushed-no-tracking"]);

const { manager, ghCalls } = yield* makeManager({
ghScenario: {
prListByHeadSelector: {
// @effect-diagnostics-next-line preferSchemaOverJson:off
"octocat:feature/pushed-no-tracking": JSON.stringify([
{
number: 215,
title: "Fork PR without tracking",
url: "https://github.com/pingdotgg/codething-mvp/pull/215",
baseRefName: "main",
headRefName: "feature/pushed-no-tracking",
state: "OPEN",
isCrossRepository: true,
headRepository: { nameWithOwner: "octocat/codething-mvp" },
headRepositoryOwner: { login: "octocat" },
},
]),
},
},
});

const status = yield* manager.status({ cwd: repoDir });

expect(status.pr?.number).toBe(215);
expect(
ghCalls.some((call) => call.startsWith("pr view octocat:feature/pushed-no-tracking ")),
).toBe(true);
}),
);

it("backs off repeated PR lookup failures past the healthy refresh cadence", () => {
expect(Duration.toMillis(GitManager.prLookupFailureTtl(1))).toBe(20_000);
expect(Duration.toMillis(GitManager.prLookupFailureTtl(2))).toBe(40_000);
Expand Down Expand Up @@ -1121,7 +1226,71 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
state: "open",
});
expect(ghCalls).toContain(
"pr list --head jasonLaster:statemachine --state all --limit 20 --json number,title,url,baseRefName,headRefName,state,mergedAt,updatedAt,isCrossRepository,headRepository,headRepositoryOwner",
"pr view jasonLaster:statemachine --json number,title,url,baseRefName,headRefName,state,mergedAt,updatedAt,isCrossRepository,headRepository,headRepositoryOwner",
);
}),
20_000,
);

it.effect(
"status detects fork PRs opened against the conventional upstream remote",
() =>
Effect.gen(function* () {
const repoDir = yield* makeTempDir("t3code-git-manager-");
yield* initRepo(repoDir);
const forkDir = yield* createBareRemote();
const upstreamDir = yield* createBareRemote();
yield* runGit(repoDir, ["remote", "add", "origin", forkDir]);
yield* runGit(repoDir, ["remote", "add", "upstream", upstreamDir]);
yield* runGit(repoDir, ["checkout", "-b", "feature/upstream-pr"]);
yield* runGit(repoDir, ["push", "-u", "origin", "feature/upstream-pr"]);
yield* configureVisibleRemoteUrlWithLocalRewrite(
repoDir,
"origin",
"git@github.com:contributor/t3code.git",
forkDir,
);
yield* configureVisibleRemoteUrlWithLocalRewrite(
repoDir,
"upstream",
"git@github.com:T3Tools/t3code.git",
upstreamDir,
);

const { manager, ghCalls } = yield* makeManager({
ghScenario: {
prListSequence: [
// @effect-diagnostics-next-line preferSchemaOverJson:off
JSON.stringify([
{
number: 1701,
title: "Fix fork PR detection",
url: "https://github.com/T3Tools/t3code/pull/1701",
baseRefName: "main",
headRefName: "feature/upstream-pr",
state: "OPEN",
updatedAt: "2026-07-11T12:00:00Z",
isCrossRepository: true,
headRepository: { nameWithOwner: "contributor/t3code" },
headRepositoryOwner: { login: "contributor" },
},
]),
],
},
});

const status = yield* manager.status({ cwd: repoDir });

expect(status.pr).toEqual({
number: 1701,
title: "Fix fork PR detection",
url: "https://github.com/T3Tools/t3code/pull/1701",
baseRef: "main",
headRef: "feature/upstream-pr",
state: "open",
});
expect(ghCalls).toContain(
"pr view contributor:feature/upstream-pr --json number,title,url,baseRefName,headRefName,state,mergedAt,updatedAt,isCrossRepository,headRepository,headRepositoryOwner",
);
}),
20_000,
Expand Down Expand Up @@ -2305,8 +2474,6 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
const { manager, ghCalls } = yield* makeManager({
ghScenario: {
prListSequence: [
// @effect-diagnostics-next-line preferSchemaOverJson:off
JSON.stringify([]),
// @effect-diagnostics-next-line preferSchemaOverJson:off
JSON.stringify([
{
Expand Down Expand Up @@ -2336,11 +2503,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {

expect(result.pr.status).toBe("opened_existing");
expect(result.pr.number).toBe(142);
expect(
ghCalls.some((call) =>
call.includes("pr list --head octocat:statemachine --state open --limit 1"),
),
).toBe(true);
expect(ghCalls.some((call) => call.startsWith("pr view octocat:statemachine "))).toBe(true);
expect(ghCalls.some((call) => call.startsWith("pr create "))).toBe(false);
}),
12_000,
Expand Down Expand Up @@ -2513,7 +2676,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
expect(result.pr.number).toBe(142);

const ownerSelectorCallIndex = ghCalls.findIndex((call) =>
call.includes("pr list --head octocat:statemachine --state open --limit 1"),
call.startsWith("pr view octocat:statemachine "),
);
expect(ownerSelectorCallIndex).toBeGreaterThanOrEqual(0);
expect(ghCalls.some((call) => call.startsWith("pr create "))).toBe(false);
Expand Down Expand Up @@ -2579,11 +2742,11 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
expect(result.pr.status).toBe("opened_existing");
expect(result.pr.number).toBe(142);

const openLookupCalls = ghCalls.filter((call) => call.includes("--state open --limit 1"));
expect(openLookupCalls).toHaveLength(1);
expect(openLookupCalls[0]).toContain(
"pr list --head octocat:statemachine --state open --limit 1",
const openLookupCalls = ghCalls.filter((call) =>
call.startsWith("pr view octocat:statemachine "),
);
expect(openLookupCalls).toHaveLength(1);
expect(openLookupCalls[0]).toContain("pr view octocat:statemachine");
}),
12_000,
);
Expand Down Expand Up @@ -2780,13 +2943,13 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
}),
);

it.effect("generates PR content against the remote base when the local base is stale", () =>
it.effect("generates PR content against a non-origin remote base when local base is stale", () =>
Effect.gen(function* () {
const repoDir = yield* makeTempDir("t3code-git-manager-");
yield* initRepo(repoDir);
const remoteDir = yield* createBareRemote();
yield* runGit(repoDir, ["remote", "add", "origin", remoteDir]);
yield* runGit(repoDir, ["push", "-u", "origin", "main"]);
yield* runGit(repoDir, ["remote", "add", "primary", remoteDir]);
yield* runGit(repoDir, ["push", "-u", "primary", "main"]);
yield* runGit(remoteDir, ["symbolic-ref", "HEAD", "refs/heads/main"]);

const peerDir = yield* makeTempDir("t3code-git-peer-");
Expand All @@ -2798,18 +2961,18 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
yield* runGit(peerDir, ["commit", "-m", "Remote base commit"]);
yield* runGit(peerDir, ["push", "origin", "main"]);

yield* runGit(repoDir, ["fetch", "origin"]);
yield* runGit(repoDir, ["fetch", "primary"]);
yield* runGit(repoDir, [
"checkout",
"--no-track",
"-b",
"feature/remote-base",
"origin/main",
"primary/main",
]);
NodeFS.writeFileSync(NodePath.join(repoDir, "feature.txt"), "feature\n");
yield* runGit(repoDir, ["add", "feature.txt"]);
yield* runGit(repoDir, ["commit", "-m", "Feature commit"]);
yield* runGit(repoDir, ["push", "-u", "origin", "feature/remote-base"]);
yield* runGit(repoDir, ["push", "-u", "primary", "feature/remote-base"]);
yield* runGit(repoDir, ["config", "branch.feature/remote-base.gh-merge-base", "main"]);

let generatedCommitSummary = "";
Expand Down Expand Up @@ -2926,7 +3089,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
yield* configureVisibleRemoteUrlWithLocalRewrite(
repoDir,
"fork-seed",
"git@github.com:octocat/codething-mvp.git",
"https://github.com/octocat/codething-mvp.git/",
forkDir,
);

Expand Down
Loading
Loading