import { readFileSync } from "node:fs"; import { join } from "node:path"; import { describe, expect, it } from "vitest"; import type { Runner } from "../exec.js"; import { main, mineGithub } from "../mine_tickets.js"; const fx = (name: string): string => readFileSync(join(import.meta.dirname, "fixtures", name), "utf8"); type Json = Record; const fxJson = (name: string): T => JSON.parse(fx(name)) as T; /** Build a fake gh Runner from a map of arg-substring -> JSON payload. First matching key wins. */ const makeGh = (routes: Record): Runner => async (cmd, args) => { expect(cmd).toBe("gh"); const joined = args.join(" "); for (const [needle, payload] of Object.entries(routes)) { if (joined.includes(needle)) return { code: 0, stdout: JSON.stringify(payload), stderr: "" }; } return { code: 1, stdout: "", stderr: `unexpected: ${joined}` }; }; /** Happy-path routes derived from the canonical fixtures; tests override entries per-case. */ const baseRoutes = (): Record => ({ "pr list": fxJson("gh_pr_list.json"), "pr view 42": fxJson("gh_pr_view_42.json"), "issues/17": fxJson("gh_issue_17.json"), "commits/aaaa": fxJson("gh_commit.json"), }); const CFG = { id: "demo", github: "acme/demo", clone_url: "x", language: "ts", ticket_source: "github" as const, install: [], test_command: "t {test_files}", test_patterns: ["test/**"], test_retries: 0, ticket_after: "2025-06-01", wiki_commit: "", toolchain: [], services: [], }; const mine = (routes: Record, target = 10) => mineGithub(CFG, { target, limit: 200, runner: makeGh(routes) }); describe("mineGithub", () => { it("produces a sanitized, eligible ticket record", async () => { const out = await mine(baseRoutes()); expect(out.tickets).toHaveLength(1); const t = out.tickets[0]!; expect(t).toMatchObject({ issue: 17, fix_pr: 42, base_commit: "bbbb111122223333444455556666777788880000", fix_commit: "aaaa111122223333444455556666777788889999", test_files: ["test/config.test.ts"], src_files: ["src/config.ts"], changed_lines: 22, merge_parents: 1, }); expect(t.body_sanitized.length).toBeGreaterThan(0); expect(out.schema_version).toBe(1); }); it("tags issue-linked tickets with source 'issue'", async () => { const out = await mine(baseRoutes()); expect(out.tickets).toHaveLength(1); expect(out.tickets[0]!.source).toBe("issue"); }); it("falls back to a PR-as-ticket when no issue is linked", async () => { const routes = baseRoutes(); routes["pr view 42"] = fxJson("gh_pr_view_99_unlinked.json"); const out = await mine(routes); expect(out.tickets).toHaveLength(1); const t = out.tickets[0]!; expect(t.source).toBe("pr"); expect(t.issue).toBe(99); // PR number serves as the ticket id expect(t.fix_pr).toBe(99); expect(t.issue_url).toBe("https://github.com/acme/demo/pull/99"); expect(t.base_commit).toBe("bbbb111122223333444455556666777788880000"); // PR's first parent expect(t.fix_commit).toBe("aaaa111122223333444455556666777788889999"); // PR merge commit expect(t.test_files).toEqual(["test/checkout.test.ts"]); expect(t.src_files).toEqual(["src/checkout.ts"]); // body_sanitized has the template boilerplate stripped: expect(t.body_sanitized).not.toContain("