Skip to content
Merged
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
162 changes: 162 additions & 0 deletions test/unit/find-opportunities.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof import("../../src/github/app")>()),
createInstallationToken: createInstallationTokenMock,
}));

const fixtureDir = join(dirname(fileURLToPath(import.meta.url)), "../fixtures/ai-policy");

function readFixture(name: string): string {
Expand Down Expand Up @@ -57,6 +65,8 @@ const issue = (number: number) => ({

afterEach(() => {
vi.unstubAllGlobals();
createInstallationTokenMock.mockClear();
createInstallationTokenMock.mockResolvedValue("installation-token");
});

describe("validateFindOpportunitiesInput", () => {
Expand Down Expand Up @@ -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);
});
});