From 784fc827de52eb6119346039dba8a76297c96ec1 Mon Sep 17 00:00:00 2001 From: davion-knight <298846663+davion-knight@users.noreply.github.com> Date: Wed, 15 Jul 2026 04:31:05 -0500 Subject: [PATCH] test(mcp): cover find-opportunities searchQuery, goal-spec, and token-fallback paths runFindOpportunities was at 79.34% branch coverage (96/121) -- several real paths were never exercised by test/unit/find-opportunities.test.ts, which only ever called it with targets. Add cases for: the searchQuery path plus its post-search canAccessRepo re-filter, buildGoalSpecsByRepo's lane/languages arms, appliedLane/appliedMinRankScore inclusion vs omission, the validated.ok===false path through runFindOpportunities, and resolveDiscoveryGithubToken's installation-token fallback loop (skip-uninstalled, success, and the swallowed createInstallationToken failure). find-opportunities.ts is now 100% branch and statement covered. Test-only change; no src modified. Closes #5847 --- test/unit/find-opportunities.test.ts | 162 +++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) diff --git a/test/unit/find-opportunities.test.ts b/test/unit/find-opportunities.test.ts index 4ed866611f..0e652306ea 100644 --- a/test/unit/find-opportunities.test.ts +++ b/test/unit/find-opportunities.test.ts @@ -20,6 +20,14 @@ vi.mock("@loopover/engine", async () => { return import("../../packages/loopover-engine/src/index"); }); +// Stub only createInstallationToken so the installation-token fallback path in resolveDiscoveryGithubToken is +// exercised without real GitHub App JWT signing; every other src/github/app export stays real. +const createInstallationTokenMock = vi.hoisted(() => vi.fn(async () => "installation-token")); +vi.mock("../../src/github/app", async (importActual) => ({ + ...(await importActual()), + createInstallationToken: createInstallationTokenMock, +})); + const fixtureDir = join(dirname(fileURLToPath(import.meta.url)), "../fixtures/ai-policy"); function readFixture(name: string): string { @@ -57,6 +65,8 @@ const issue = (number: number) => ({ afterEach(() => { vi.unstubAllGlobals(); + createInstallationTokenMock.mockClear(); + createInstallationTokenMock.mockResolvedValue("installation-token"); }); describe("validateFindOpportunitiesInput", () => { @@ -275,4 +285,156 @@ describe("runFindOpportunities", () => { expect(allowed.status).toBe("ok"); expect(allowed.ranked).toHaveLength(1); }); + + it("returns invalid_request (via runFindOpportunities) when neither targets nor searchQuery is given", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "test-token" }); + const result = await runFindOpportunities(env, {}); + expect(result).toEqual({ + status: "invalid_request", + ranked: [], + totalCandidates: 0, + reason: "targets_or_search_query_required", + }); + }); + + it("omits appliedLane / appliedMinRankScore when neither a lane nor a min-rank-score is supplied", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "test-token" }); + const allowedPolicy = readFixture("allowed-silent.md"); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = String(input); + if (url.includes("/repos/acme/allowed/contents/AI-USAGE.md")) return jsonResponse({}, { status: 404 }); + if (url.includes("/repos/acme/allowed/contents/CONTRIBUTING.md")) return contentResponse(allowedPolicy); + if (url.includes("/repos/acme/allowed/issues?")) return jsonResponse([issue(11)]); + return jsonResponse({}, { status: 404 }); + }); + + const result = await runFindOpportunities(env, { targets: [{ owner: "acme", repo: "allowed" }] }); + expect(result.status).toBe("ok"); + expect("appliedLane" in result).toBe(false); + expect("appliedMinRankScore" in result).toBe(false); + }); + + it("surfaces appliedLane + appliedMinRankScore when a goalSpec lane and min-rank-score are supplied", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "test-token" }); + const allowedPolicy = readFixture("allowed-silent.md"); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = String(input); + if (url.includes("/repos/acme/allowed/contents/AI-USAGE.md")) return jsonResponse({}, { status: 404 }); + if (url.includes("/repos/acme/allowed/contents/CONTRIBUTING.md")) return contentResponse(allowedPolicy); + if (url.includes("/repos/acme/allowed/issues?")) return jsonResponse([issue(13)]); + return jsonResponse({}, { status: 404 }); + }); + + // minRankScore 1 is low enough that the single candidate still passes the >= filter, so we still get a + // ranked result to read appliedMinRankScore off of; lane "docs" narrows preferredLabels in the goal spec. + // Lane only (no languages) so buildGoalSpecsByRepo takes the preferredLabels arm but not the wantedPaths arm. + const result = await runFindOpportunities(env, { + targets: [{ owner: "acme", repo: "allowed" }], + goalSpec: { lane: "docs", minRankScore: 1 }, + }); + expect(result.status).toBe("ok"); + expect(result.appliedLane).toBe("docs"); + expect(result.appliedMinRankScore).toBe(1); + }); + + it("takes the searchQuery path and filters returned repos through canAccessRepo post-search", async () => { + // No GITHUB_PUBLIC_TOKEN and no targets -> resolveDiscoveryGithubToken returns a null token, so the search + // path runs with the empty-token fallback (`token ?? ""`); the fetch stub serves the search regardless. + const env = createTestEnv(); + const allowedPolicy = readFixture("allowed-silent.md"); + const searchItem = (owner: string, repo: string, number: number) => ({ + ...issue(number), + repository_url: `https://api.github.com/repos/${owner}/${repo}`, + html_url: `https://github.com/${owner}/${repo}/issues/${number}`, + }); + + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = String(input); + if (url.includes("/search/issues")) { + return jsonResponse({ items: [searchItem("acme", "found", 21), searchItem("acme", "blocked", 22)] }); + } + // AI policy is fetched per discovered repo; leave both allowed so the post-search canAccessRepo re-filter + // is what actually removes acme/blocked, not the policy gate. + if (url.includes("/contents/AI-USAGE.md")) return jsonResponse({}, { status: 404 }); + if (url.includes("/contents/CONTRIBUTING.md")) return contentResponse(allowedPolicy); + return jsonResponse({}, { status: 404 }); + }); + + const result = await runFindOpportunities( + env, + { searchQuery: "good first issue in:title" }, + { canAccessRepo: async (repoFullName) => repoFullName !== "acme/blocked" }, + ); + + expect(result.status).toBe("ok"); + const repos = result.ranked.map((entry) => `${entry.owner}/${entry.repo}`); + expect(repos).toContain("acme/found"); + expect(repos).not.toContain("acme/blocked"); // removed by the post-search canAccessRepo re-filter + }); + + it("resolves a GitHub token from an installed repo's installation when GITHUB_PUBLIC_TOKEN is unset", async () => { + const env = createTestEnv(); // no GITHUB_PUBLIC_TOKEN -> installation-token fallback loop + // First target has no DB row (installationId undefined -> the loop's `continue`); second is installed. + await upsertRepositoryFromGitHub(env, { name: "installed", full_name: "acme/installed" }, 4242); + const allowedPolicy = readFixture("allowed-silent.md"); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = String(input); + if (url.includes("/repos/acme/installed/contents/AI-USAGE.md")) return jsonResponse({}, { status: 404 }); + if (url.includes("/repos/acme/installed/contents/CONTRIBUTING.md")) return contentResponse(allowedPolicy); + if (url.includes("/repos/acme/installed/issues?")) return jsonResponse([issue(31)]); + return jsonResponse({}, { status: 404 }); + }); + + const result = await runFindOpportunities(env, { + targets: [ + { owner: "acme", repo: "uninstalled" }, + { owner: "acme", repo: "installed" }, + ], + }); + + expect(result.status).toBe("ok"); + expect(createInstallationTokenMock).toHaveBeenCalledWith(expect.anything(), 4242); + expect(result.ranked.map((entry) => `${entry.owner}/${entry.repo}`)).toContain("acme/installed"); + }); + + it("swallows a failing createInstallationToken and still proceeds when the repo is installed", async () => { + const env = createTestEnv(); + await upsertRepositoryFromGitHub(env, { name: "installed", full_name: "acme/installed" }, 4244); + createInstallationTokenMock.mockRejectedValueOnce(new Error("jwt signing failed")); + const allowedPolicy = readFixture("allowed-silent.md"); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = String(input); + if (url.includes("/repos/acme/installed/contents/AI-USAGE.md")) return jsonResponse({}, { status: 404 }); + if (url.includes("/repos/acme/installed/contents/CONTRIBUTING.md")) return contentResponse(allowedPolicy); + if (url.includes("/repos/acme/installed/issues?")) return jsonResponse([issue(41)]); + return jsonResponse({}, { status: 404 }); + }); + + // Token resolution failed (createInstallationToken threw and was swallowed), but the repo IS installed, so + // it does NOT short-circuit to github_token_unavailable -- the fetch proceeds with the empty-token fallback. + const result = await runFindOpportunities(env, { targets: [{ owner: "acme", repo: "installed" }] }); + expect(result.status).toBe("ok"); + expect(createInstallationTokenMock).toHaveBeenCalled(); + }); + + it("applies a languages-only goalSpec (no lane) without setting appliedLane", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "test-token" }); + const allowedPolicy = readFixture("allowed-silent.md"); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = String(input); + if (url.includes("/repos/acme/allowed/contents/AI-USAGE.md")) return jsonResponse({}, { status: 404 }); + if (url.includes("/repos/acme/allowed/contents/CONTRIBUTING.md")) return contentResponse(allowedPolicy); + if (url.includes("/repos/acme/allowed/issues?")) return jsonResponse([issue(51)]); + return jsonResponse({}, { status: 404 }); + }); + + // languages present, lane absent -> buildGoalSpecsByRepo takes the wantedPaths arm but not the + // preferredLabels arm, and appliedLane stays omitted. + const result = await runFindOpportunities(env, { + targets: [{ owner: "acme", repo: "allowed" }], + goalSpec: { languages: ["go"] }, + }); + expect(result.status).toBe("ok"); + expect("appliedLane" in result).toBe(false); + }); });