feat(FN-4993): wire pr create flags router and AI metadata parity

Fusion-Task-Id: FN-4993
Fusion-Task-Lineage: 20ce6d5f-ef33-49e6-8519-648d769cc473
This commit is contained in:
Fusion (runfusion.ai)
2026-05-18 05:35:00 -07:00
committed by gsxdsm
parent 09491671ea
commit 60d3da7394
6 changed files with 239 additions and 41 deletions

View File

@@ -0,0 +1,75 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("@fusion/core", async () => {
const actual = await vi.importActual<typeof import("@fusion/core")>("@fusion/core");
return {
...actual,
isGhAvailable: vi.fn(() => true),
isGhAuthenticated: vi.fn(() => true),
runGh: vi.fn(),
runGhAsync: vi.fn(),
runGhJson: vi.fn(),
runGhJsonAsync: vi.fn(),
getGhErrorMessage: vi.fn((err) => (err instanceof Error ? err.message : String(err))),
getCurrentRepo: vi.fn(() => ({ owner: "owner", repo: "repo" })),
};
});
import { runGh } from "@fusion/core";
import { GitHubClient } from "../github.js";
const mockRunGh = vi.mocked(runGh);
describe("GitHubClient.createPr draft/reviewer", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it.each([
{ draft: true, reviewers: ["alice", "bob"], expectsDraft: true, expectsReviewer: true },
{ draft: undefined, reviewers: [], expectsDraft: false, expectsReviewer: false },
])("passes gh flags %#", async ({ draft, reviewers, expectsDraft, expectsReviewer }) => {
mockRunGh.mockReturnValue("https://github.com/owner/repo/pull/42\n");
const client = new GitHubClient({ forceMode: "gh-cli" });
await client.createPr({ title: "T", head: "fusion/fn-001", base: "main", draft, reviewers });
const args = mockRunGh.mock.calls[0][0];
expect(args.includes("--draft")).toBe(expectsDraft);
expect(args.includes("--reviewer")).toBe(expectsReviewer);
});
it("sends draft and reviewers through REST path", async () => {
const client = new GitHubClient({ token: "ghp_token", forceMode: "token" });
const fetchSpy = vi.spyOn(global, "fetch" as any)
.mockResolvedValueOnce({
ok: true,
json: async () => ({ number: 7, html_url: "https://github.com/owner/repo/pull/7", title: "T", state: "open", draft: true, head: { ref: "fusion/fn-001" }, base: { ref: "main" }, comments: 0 }),
} as any)
.mockResolvedValueOnce({ ok: true, json: async () => ({}) } as any);
await client.createPr({ title: "T", head: "fusion/fn-001", base: "main", draft: true, reviewers: ["alice", "bob"] });
expect(String(fetchSpy.mock.calls[0][1]?.body)).toContain('"draft":true');
expect(fetchSpy.mock.calls[1][0]).toContain("/requested_reviewers");
fetchSpy.mockRestore();
});
it("continues when reviewer request fails on REST path", async () => {
const stderrSpy = vi.spyOn(process.stderr, "write").mockReturnValue(true);
const client = new GitHubClient({ token: "ghp_token", forceMode: "token" });
const fetchSpy = vi.spyOn(global, "fetch" as any)
.mockResolvedValueOnce({
ok: true,
json: async () => ({ number: 8, html_url: "https://github.com/owner/repo/pull/8", title: "T", state: "open", head: { ref: "fusion/fn-001" }, base: { ref: "main" }, comments: 0 }),
} as any)
.mockResolvedValueOnce({ ok: false, status: 422, statusText: "Unprocessable", json: async () => ({ message: "invalid reviewers" }) } as any);
const pr = await client.createPr({ title: "T", head: "fusion/fn-001", reviewers: ["alice"] });
expect(pr.number).toBe(8);
expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining("failed to request reviewers"));
fetchSpy.mockRestore();
stderrSpy.mockRestore();
});
});

View File

@@ -12,6 +12,7 @@ export {
} from "./runtime-logger.js";
export { createSkillsAdapter, getProjectSettingsPath, type SkillsAdapter, type DiscoveredSkill, type CatalogEntry, type CatalogFetchResult, type ToggleSkillResult, type UpstreamError, type UpstreamErrorCode, type SkillContent, type SkillFileEntry } from "./skills-adapter.js";
export { GitHubClient, isPrMergeReady, type GitHubClientOptions, type PrMergeStatus, type PrCheckStatus, type ReviewDecision, type MergePrParams, type FindPrParams, type CreateIssueParams, type CreatedIssue } from "./github.js";
export { generatePrMetadata, type GeneratedPrMetadata } from "./pr-metadata-generator.js";
export { maybeCreateTrackingIssue, type MaybeCreateTrackingIssueDeps } from "./github-tracking.js";
export {
buildIssueSearchQueries,