feat(FN-4966): complete Step 3 — add PR conflict diagnostics collection

Fusion-Task-Id: FN-4966
Fusion-Task-Lineage: 8f5ea300-feaa-469c-aeed-f7304a5a1b6d
This commit is contained in:
gsxdsm
2026-05-18 08:15:48 -07:00
parent b000b8dd66
commit d3297a8612
2 changed files with 232 additions and 1 deletions

View File

@@ -1,3 +1,7 @@
import { mkdtempSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { execSync } from "node:child_process";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { GitHubClient, CreatePrParams, PrComment, isPrMergeReady } from "../github.js";
@@ -1203,6 +1207,104 @@ describe("GitHubClient", () => {
});
});
describe("getPrConflictDiagnostics", () => {
function createConflictRepo(): string {
const repoRoot = mkdtempSync(join(tmpdir(), "fn-4966-conflict-"));
execSync("git init -b main", { cwd: repoRoot, stdio: "ignore" });
execSync("git config user.email 'test@example.com'", { cwd: repoRoot, stdio: "ignore" });
execSync("git config user.name 'Test User'", { cwd: repoRoot, stdio: "ignore" });
writeFileSync(join(repoRoot, "conflict.txt"), "base\n");
execSync("git add conflict.txt && git commit -m 'base'", { cwd: repoRoot, stdio: "ignore" });
execSync("git checkout -b feature", { cwd: repoRoot, stdio: "ignore" });
writeFileSync(join(repoRoot, "conflict.txt"), "feature\n");
execSync("git add conflict.txt && git commit -m 'feature'", { cwd: repoRoot, stdio: "ignore" });
execSync("git checkout main", { cwd: repoRoot, stdio: "ignore" });
writeFileSync(join(repoRoot, "conflict.txt"), "main\n");
execSync("git add conflict.txt && git commit -m 'main'", { cwd: repoRoot, stdio: "ignore" });
execSync("git update-ref refs/remotes/origin/main main", { cwd: repoRoot, stdio: "ignore" });
execSync("git update-ref refs/remotes/origin/feature feature", { cwd: repoRoot, stdio: "ignore" });
return repoRoot;
}
it("returns conflicting files from local git path", async () => {
const repoRoot = createConflictRepo();
const diagnostics = await client.getPrConflictDiagnostics("owner", "repo", 42, {
baseBranch: "main",
headBranch: "feature",
repoRoot,
});
expect(diagnostics.conflictingFiles).toContain("conflict.txt");
expect(diagnostics.suggestedCommands).toEqual([
"git fetch origin",
"git checkout feature",
"git rebase origin/main",
"# Resolve conflicts then: git add <files> && git rebase --continue",
]);
expect(mockRunGhJsonAsync).not.toHaveBeenCalled();
});
it("falls back to gh compare files when local repo path fails", async () => {
mockRunGhJsonAsync.mockResolvedValueOnce({ files: [{ filename: "a.ts" }, { filename: "b.ts" }] });
const diagnostics = await client.getPrConflictDiagnostics("owner", "repo", 42, {
baseBranch: "main",
headBranch: "feature",
repoRoot: "/does/not/exist",
});
expect(diagnostics.conflictingFiles).toEqual(["a.ts", "b.ts"]);
expect(diagnostics.suggestedCommands.at(-1)).toContain("file list reflects PR changes");
});
it("composes commands for each direct-merge strategy", async () => {
mockRunGhJsonAsync.mockResolvedValue({ files: [] });
const squash = await client.getPrConflictDiagnostics("owner", "repo", 1, {
baseBranch: "main",
headBranch: "feature",
directMergeCommitStrategy: "always-squash",
});
expect(squash.suggestedCommands).toEqual([
"git fetch origin",
"git checkout feature",
"git merge origin/main",
"# Resolve conflicts then: git add <files> && git commit",
]);
const rebase = await client.getPrConflictDiagnostics("owner", "repo", 1, {
baseBranch: "main",
headBranch: "feature",
directMergeCommitStrategy: "always-rebase",
});
expect(rebase.suggestedCommands[2]).toBe("git rebase origin/main");
const auto = await client.getPrConflictDiagnostics("owner", "repo", 1, {
baseBranch: "main",
headBranch: "feature",
directMergeCommitStrategy: "auto",
});
expect(auto.suggestedCommands[2]).toBe("git rebase origin/main");
});
it("returns non-throwing defaults when both detection paths fail", async () => {
mockRunGhJsonAsync.mockRejectedValueOnce(new Error("gh compare failed"));
const diagnostics = await client.getPrConflictDiagnostics("owner", "repo", 42, {
baseBranch: "main",
headBranch: "feature",
repoRoot: "/does/not/exist",
});
expect(diagnostics.conflictingFiles).toEqual([]);
expect(diagnostics.suggestedCommands).toEqual([
"git fetch origin",
"git checkout feature",
"git rebase origin/main",
"# Resolve conflicts then: git add <files> && git rebase --continue",
]);
expect(() => new Date(diagnostics.capturedAt)).not.toThrow();
});
});
describe("getPrMergeStatus", () => {
it("returns merge-ready status only when required checks pass and review is non-blocking", async () => {
mockRunGhJsonAsync