diff --git a/.changeset/quiet-pr-sha-match.md b/.changeset/quiet-pr-sha-match.md new file mode 100644 index 000000000000..e48468e9c0e9 --- /dev/null +++ b/.changeset/quiet-pr-sha-match.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Prevent new worktrees from showing unrelated merged or closed pull requests that share the same commit. diff --git a/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts b/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts index 9d3aa717e8e5..ca874c146279 100644 --- a/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts +++ b/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts @@ -400,7 +400,8 @@ export class PRStatusPoller { if (!head) return null const stdout = await this.query( - ["pr", "list", "--state", "all", "--search", `${head} is:pr`, "--limit", "5"], + // New branches can share HEAD with a merged PR without belonging to it. + ["pr", "list", "--state", "open", "--search", `${head} is:pr`, "--limit", "5"], cwd, ) const items = JSON.parse(stdout) as unknown[] diff --git a/packages/kilo-vscode/tests/unit/am-pr-status-bridge.test.ts b/packages/kilo-vscode/tests/unit/am-pr-status-bridge.test.ts index a946f239e5b7..e074671ce0bc 100644 --- a/packages/kilo-vscode/tests/unit/am-pr-status-bridge.test.ts +++ b/packages/kilo-vscode/tests/unit/am-pr-status-bridge.test.ts @@ -66,6 +66,83 @@ function harness(opts: { hasPersisted?: boolean; projectId?: string } = {}) { } describe("PRStatusPoller batched GitHub queries", () => { + it.each([ + { lookup: "sha", state: "MERGED", exact: true, expected: null }, + { lookup: "sha", state: "CLOSED", exact: true, expected: null }, + { lookup: "sha", state: "OPEN", exact: true, expected: "open" }, + { lookup: "sha", state: "OPEN", exact: false, expected: null }, + { lookup: "tracking", state: "MERGED", exact: true, expected: "merged" }, + { lookup: "tracking", state: "CLOSED", exact: true, expected: "closed" }, + { lookup: "branch", state: "MERGED", exact: true, expected: "merged" }, + { lookup: "branch", state: "CLOSED", exact: true, expected: "closed" }, + ])("resolves $lookup PRs in state $state (exact SHA: $exact)", async ({ lookup, state, exact, expected }) => { + const poller = new PRStatusPoller({ + getWorktrees: () => [], + getWorkspaceRoot: () => "/repo", + onStatus: () => undefined, + log: () => undefined, + }) + const calls: string[][] = [] + const internal = poller as unknown as { + fetchPRForBranch: (branch: string, cwd: string) => Promise<{ state: string } | null> + gh: (args: string[]) => Promise<{ stdout: string; stderr: string }> + shell: (command: string, args: string[]) => Promise<{ stdout: string; stderr: string }> + } + internal.shell = async (command, args) => { + expect(command).toBe("git") + expect(args).toEqual(["rev-parse", "HEAD"]) + return { stdout: refs.headRefOid, stderr: "" } + } + internal.gh = async (args) => { + calls.push(args) + const data = { + number: 1, + title: "Example change", + url: "https://github.com/example/project/pull/1", + state, + headRefName: lookup === "sha" ? "contributor-change" : "feature", + headRefOid: exact ? refs.headRefOid : refs.baseRefOid, + } + if (args.at(1) === "view") { + if (lookup === "tracking" || (lookup === "branch" && args.at(2) === "feature")) + return { stdout: JSON.stringify(data), stderr: "" } + throw new Error("no pull requests found for branch") + } + const filter = args.at(args.indexOf("--state") + 1) + return { stdout: JSON.stringify(filter === "all" || filter === state.toLowerCase() ? [data] : []), stderr: "" } + } + + const result = await internal.fetchPRForBranch("feature", "/repo") + expect(result?.state ?? null).toBe(expected) + expect(calls.map((args) => args.slice(0, 3))).toEqual( + lookup === "tracking" + ? [["pr", "view", "--json"]] + : lookup === "branch" + ? [ + ["pr", "view", "--json"], + ["pr", "view", "feature"], + ] + : [ + ["pr", "view", "--json"], + ["pr", "view", "feature"], + ["pr", "list", "--state"], + ], + ) + if (lookup === "sha") { + expect(calls.at(-1)?.slice(0, 8)).toEqual([ + "pr", + "list", + "--state", + "open", + "--search", + `${refs.headRefOid} is:pr`, + "--limit", + "5", + ]) + } + poller.stop() + }) + it("forwards the actual branch for null PR results", async () => { const values: Array<{ pr: PRStatus | null; branch?: string }> = [] const branches: string[] = []