feat(FN-2920): improve remote tunnel setup and heartbeat scheduling

- Add cloudflared install/detection support in remote settings API, UI, and route tests
- Surface Cloudflare tunnel prerequisites in Settings modal with remote access docs updates
- Harden heartbeat runtime scheduling by avoiding stale timeout state and simplifying runtime timeout handling
- Expand CLI/core/dashboard/engine coverage for task lifecycle, agent health, and runtime heartbeat behavior
- Add changesets for heartbeat scheduling fixes and PR approval setting updates

Fusion-Task-Id: FN-2920
This commit is contained in:
Fusion
2026-04-29 13:49:05 -07:00
committed by gsxdsm
parent 8fdadb0795
commit 01cf62b002
25 changed files with 819 additions and 205 deletions

View File

@@ -26,11 +26,12 @@ interface MockTask {
column: string;
}
function makeStore(task: MockTask) {
function makeStore(task: MockTask, settings: Record<string, unknown> = {}) {
const emitter = new EventEmitter();
const updates: Array<{ id: string; patch: Record<string, unknown> }> = [];
return Object.assign(emitter, {
getTask: vi.fn().mockResolvedValue(task),
getSettings: vi.fn().mockResolvedValue({ requirePrApproval: false, ...settings }),
updateTask: vi.fn(async (id: string, patch: Record<string, unknown>) => {
updates.push({ id, patch });
}),
@@ -187,4 +188,150 @@ describe("processPullRequestMergeTask", () => {
expect(github.createPr).not.toHaveBeenCalled();
});
describe("requirePrApproval", () => {
function makeReadyMergeStatus(reviewDecision: string | null) {
const prInfo = {
number: 100,
url: "https://github.com/x/y/pull/100",
status: "open" as const,
headBranch: "fusion/fn-9100",
baseBranch: "main",
};
// Simulate the "free private repo" case: GitHub reports no required
// checks and no blocking review state, so isPrMergeReady returns
// mergeReady: true. Without the gate this would auto-merge.
return {
prInfo,
reviewDecision,
checks: [],
mergeReady: true,
blockingReasons: [],
};
}
it("holds the merge when requirePrApproval is true and reviewDecision is not APPROVED", async () => {
const task: MockTask = {
id: "FN-9100",
title: "test",
description: "desc",
column: "in-review",
prInfo: {
number: 100,
url: "https://github.com/x/y/pull/100",
status: "open",
headBranch: "fusion/fn-9100",
baseBranch: "main",
},
};
const store = makeStore(task, { requirePrApproval: true });
const github = {
findPrForBranch: vi.fn(),
createPr: vi.fn(),
getPrMergeStatus: vi.fn(async () => makeReadyMergeStatus(null)),
mergePr: vi.fn(),
};
const result = await processPullRequestMergeTask(
store as never,
"/repo",
task.id,
github as never,
() => undefined,
);
expect(result).toBe("waiting");
expect(github.mergePr).not.toHaveBeenCalled();
const lastUpdate = (store as { _updates: Array<{ patch: Record<string, unknown> }> })._updates.at(-1);
expect(lastUpdate?.patch).toEqual({ status: "awaiting-pr-checks" });
});
it("merges when requirePrApproval is true and reviewDecision is APPROVED", async () => {
const task: MockTask = {
id: "FN-9101",
title: "test",
description: "desc",
column: "in-review",
prInfo: {
number: 100,
url: "https://github.com/x/y/pull/100",
status: "open",
headBranch: "fusion/fn-9101",
baseBranch: "main",
},
};
const store = makeStore(task, { requirePrApproval: true });
const merged = {
number: 100,
url: "https://github.com/x/y/pull/100",
status: "merged" as const,
headBranch: "fusion/fn-9101",
baseBranch: "main",
};
const github = {
findPrForBranch: vi.fn(),
createPr: vi.fn(),
getPrMergeStatus: vi.fn(async () => makeReadyMergeStatus("APPROVED")),
mergePr: vi.fn(async () => merged),
};
const result = await processPullRequestMergeTask(
store as never,
"/repo",
task.id,
github as never,
() => undefined,
);
expect(result).toBe("merged");
expect(github.mergePr).toHaveBeenCalledWith({ number: 100, method: "squash" });
});
it("preserves existing behavior when requirePrApproval is false", async () => {
const task: MockTask = {
id: "FN-9102",
title: "test",
description: "desc",
column: "in-review",
prInfo: {
number: 100,
url: "https://github.com/x/y/pull/100",
status: "open",
headBranch: "fusion/fn-9102",
baseBranch: "main",
},
};
const store = makeStore(task, { requirePrApproval: false });
const merged = {
number: 100,
url: "https://github.com/x/y/pull/100",
status: "merged" as const,
headBranch: "fusion/fn-9102",
baseBranch: "main",
};
const github = {
findPrForBranch: vi.fn(),
createPr: vi.fn(),
// reviewDecision: null but mergeReady: true — without the gate,
// this should still merge (the buggy default that #21's reviewer
// flagged as too aggressive on free private repos).
getPrMergeStatus: vi.fn(async () => makeReadyMergeStatus(null)),
mergePr: vi.fn(async () => merged),
};
const result = await processPullRequestMergeTask(
store as never,
"/repo",
task.id,
github as never,
() => undefined,
);
expect(result).toBe("merged");
expect(github.mergePr).toHaveBeenCalled();
});
});
});