fix(github): compared run_watch repo guard case-insensitively
GitHub owner/repo slugs are case-insensitive; `gh repo view` returns the canonical casing while callers may pass any casing. The new guard used strict equality, so a caller in the correct repo who typed `owner/repo` while the canonical form was `Owner/Repo` was forced to pass a redundant `branch`/`run` selector. Normalize both sides via toLowerCase() before deciding the cwd is a different repository. Regression test covers the casing-only match. Refs #1949 #1951
This commit is contained in:
@@ -3438,9 +3438,12 @@ async function executeRunWatch(
|
||||
// No branch/run selector — derive the commit from the current checkout,
|
||||
// but only when cwd actually points at `repo`. Otherwise we'd watch an
|
||||
// unrelated commit SHA against the explicit repo and silently stream a
|
||||
// confident wrong-repo status (issue #1949).
|
||||
// confident wrong-repo status (issue #1949). GitHub `owner/repo` slugs
|
||||
// are case-insensitive — `gh repo view` returns the canonical casing
|
||||
// while callers may pass any casing — so the equality check normalizes
|
||||
// both sides before deciding the cwd is a different repo (PR #1951).
|
||||
const cwdRepo = await tryResolveCurrentRepo(session.cwd, signal);
|
||||
if (cwdRepo !== repo) {
|
||||
if (cwdRepo?.toLowerCase() !== repo.toLowerCase()) {
|
||||
throw new ToolError(
|
||||
`Cannot infer the watched commit for ${repo}: current checkout is ${cwdRepo ?? "not a GitHub repository"}. Pass \`branch\` or \`run\` to scope the watch.`,
|
||||
);
|
||||
|
||||
@@ -1085,4 +1085,36 @@ describe("github tool", () => {
|
||||
);
|
||||
expect(jsonSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("treats explicit `repo` and the cwd repo as matching when only casing differs (PR #1951)", async () => {
|
||||
// `gh repo view --json nameWithOwner` returns the canonical GitHub casing.
|
||||
// A caller who types `cagedbird043/cxf` while the canonical form is
|
||||
// `CagedBird043/cxf` MUST be treated as the same repo — GitHub repository
|
||||
// paths are case-insensitive — and run_watch must NOT force them to pass
|
||||
// a redundant `branch`/`run` selector.
|
||||
const canonicalRepo = "CagedBird043/CXF";
|
||||
const userRepo = "cagedbird043/cxf";
|
||||
const cwd = `/tmp/run-watch-explicit-repo-casing-${Date.now()}`;
|
||||
vi.spyOn(git.github, "text").mockResolvedValue(canonicalRepo);
|
||||
// Past the case-insensitive guard, run_watch keeps using the caller's
|
||||
// `repo` (downstream `/repos/...` paths are case-insensitive on GitHub).
|
||||
// Stub the cwd's git HEAD/branch lookups so the watch proceeds to its
|
||||
// first poll, then trip an abort to terminate the loop deterministically.
|
||||
vi.spyOn(git.branch, "current").mockResolvedValue("main");
|
||||
vi.spyOn(git.head, "sha").mockResolvedValue("c215f3a91217c215f3a91217c215f3a91217c215");
|
||||
const abort = new AbortController();
|
||||
const jsonSpy = vi.spyOn(git.github, "json").mockImplementation((async () => {
|
||||
abort.abort();
|
||||
return { workflow_runs: [] };
|
||||
}) as unknown as typeof git.github.json);
|
||||
|
||||
const tool = new GithubTool(createSession(cwd));
|
||||
// We don't care about the outcome — just that the casing guard let us
|
||||
// reach the polling loop instead of throwing the mismatch ToolError.
|
||||
await tool.execute("run-watch", { op: "run_watch", repo: userRepo }, abort.signal).catch(() => {});
|
||||
|
||||
expect(jsonSpy).toHaveBeenCalled();
|
||||
const firstCall = jsonSpy.mock.calls[0]?.[1] as string[];
|
||||
expect(firstCall.some(arg => arg === `/repos/${userRepo}/actions/runs`)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user