Add changeset

This commit is contained in:
gsxdsm
2026-05-04 09:54:22 -07:00
parent 2a4fba505e
commit 89401cd2c9
10 changed files with 249 additions and 4 deletions

View File

@@ -1095,6 +1095,34 @@ describe("runDashboard — PR-first auto-merge queue", () => {
});
expect(aiMergeTask).not.toHaveBeenCalled();
});
it("manual onMerge still uses PR lifecycle when autoMerge is disabled", async () => {
const { aiMergeTask } = await import("@fusion/engine");
const { createServer } = await import("@fusion/dashboard");
mockStore.getSettings.mockResolvedValue({
maxConcurrent: 1,
maxWorktrees: 2,
autoMerge: false,
mergeStrategy: "pull-request",
pollIntervalMs: 60_000,
enginePaused: false,
globalPause: false,
});
await runDashboard(0, { open: false, dev: true });
const createServerCall = (createServer as ReturnType<typeof vi.fn>).mock.calls[0];
const serverOpts = createServerCall[1] as { onMerge: (taskId: string) => Promise<unknown> };
await serverOpts.onMerge("FN-093");
expect(mockCreatePr).toHaveBeenCalledWith({
title: "FN-093: Task",
body: "Automated PR for FN-093.\n\nDescription",
head: "fusion/fn-093",
});
expect(aiMergeTask).not.toHaveBeenCalled();
});
});
describe("runDashboard — WorktreePool wiring", () => {

View File

@@ -189,6 +189,57 @@ describe("processPullRequestMergeTask", () => {
expect(github.createPr).not.toHaveBeenCalled();
});
it("finalizes task cleanup when PR is already merged on status refresh", async () => {
const task: MockTask = {
id: "FN-9004",
title: "test",
description: "desc",
column: "in-review",
worktree: "/tmp/worktree-fn-9004",
prInfo: {
number: 88,
url: "https://github.com/x/y/pull/88",
status: "open",
headBranch: "fusion/fn-9004",
baseBranch: "main",
},
};
const store = makeStore(task);
execMock.mockImplementation(() => "");
const github = {
findPrForBranch: vi.fn(),
createPr: vi.fn(),
getPrMergeStatus: vi.fn(async () => ({
prInfo: {
number: 88,
url: "https://github.com/x/y/pull/88",
status: "merged" as const,
headBranch: "fusion/fn-9004",
baseBranch: "main",
},
reviewDecision: "APPROVED",
checks: [],
mergeReady: true,
blockingReasons: [],
})),
mergePr: vi.fn(),
};
const result = await processPullRequestMergeTask(
store as never,
"/repo",
task.id,
github as never,
() => undefined,
);
expect(result).toBe("merged");
expect(github.mergePr).not.toHaveBeenCalled();
expect(store.updateTask).toHaveBeenCalledWith("FN-9004", { status: null, mergeRetries: 0 });
expect(store.moveTask).toHaveBeenCalledWith("FN-9004", "done");
});
describe("requirePrApproval", () => {
function makeReadyMergeStatus(reviewDecision: string | null) {
const prInfo = {

View File

@@ -31,6 +31,7 @@ import { aiMergeTask, MissionAutopilot, MissionExecutionLoop, HeartbeatMonitor,
import { AuthStorage, DefaultPackageManager, ModelRegistry, SettingsManager, discoverAndLoadExtensions, createExtensionRuntime } from "@mariozechner/pi-coding-agent";
import {
getMergeStrategy,
getTaskBranchName,
processPullRequestMergeTask,
} from "./task-lifecycle.js";
import { promptForPort } from "./port-prompt.js";
@@ -1148,6 +1149,21 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
// (semaphore-gated via the engine's InProcessRuntime).
//
const onMergeImpl = async (taskId: string) => {
const settings = await store.getSettings();
if (getMergeStrategy(settings) === "pull-request") {
const githubClient = new GitHubClient();
const outcome = await processPullRequestMergeTask(store, cwd, taskId, githubClient, getTaskMergeBlocker);
const task = await store.getTask(taskId);
return {
task,
branch: getTaskBranchName(taskId),
merged: outcome === "merged",
worktreeRemoved: false,
branchDeleted: false,
error: outcome === "waiting" ? "pull request not ready" : undefined,
};
}
const streamedMergeLog = new StreamedLogBuffer(
(line) => logSink.log(line, "merge"),
STREAM_LOG_FLUSH_IDLE_MS,