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 b91533ce43
commit 17a072c924
25 changed files with 819 additions and 205 deletions

View File

@@ -862,24 +862,46 @@ describe("InProcessRuntime", () => {
expect(scheduler!.getRegisteredAgents()).not.toContain(agent.id);
});
it("re-registers an existing agent when agent:updated event is emitted", async () => {
// Create a new agent
it("does not reset an armed timer on unrelated agent updates", async () => {
const store = getAgentStore(runtime);
const monitor = runtime.getHeartbeatMonitor();
expect(monitor).toBeDefined();
const executeHeartbeatSpy = vi
.spyOn(monitor!, "executeHeartbeat")
.mockResolvedValue({ id: "run-update-timer-stability" } as any);
const agent = await store.createAgent({
name: "test-agent-update",
role: "executor",
runtimeConfig: {
enabled: true,
heartbeatIntervalMs: 1000,
},
});
const scheduler = runtime.getTriggerScheduler();
expect(scheduler!.getRegisteredAgents()).toContain(agent.id);
// Update the agent
await vi.advanceTimersByTimeAsync(400);
await store.updateAgent(agent.id, {
name: "test-agent-update-renamed",
});
// Verify the agent is still registered (re-registration succeeded)
expect(scheduler!.getRegisteredAgents()).toContain(agent.id);
await vi.advanceTimersByTimeAsync(599);
expect(executeHeartbeatSpy).not.toHaveBeenCalled();
await vi.advanceTimersByTimeAsync(1);
await vi.waitFor(() => {
expect(executeHeartbeatSpy).toHaveBeenCalledTimes(1);
});
expect(executeHeartbeatSpy).toHaveBeenCalledWith(
expect.objectContaining({
agentId: agent.id,
source: "timer",
}),
);
});
it("unregisters an agent when enabled is set to false in update", async () => {
@@ -907,6 +929,41 @@ describe("InProcessRuntime", () => {
expect(scheduler!.getRegisteredAgents()).not.toContain(agent.id);
});
it("re-arms the timer when heartbeat interval changes", async () => {
const store = getAgentStore(runtime);
const monitor = runtime.getHeartbeatMonitor();
expect(monitor).toBeDefined();
const executeHeartbeatSpy = vi
.spyOn(monitor!, "executeHeartbeat")
.mockResolvedValue({ id: "run-interval-change" } as any);
const agent = await store.createAgent({
name: "interval-change-agent",
role: "executor",
runtimeConfig: {
enabled: true,
heartbeatIntervalMs: 1000,
},
});
await vi.advanceTimersByTimeAsync(400);
await store.updateAgent(agent.id, {
runtimeConfig: {
enabled: true,
heartbeatIntervalMs: 2000,
},
});
await vi.advanceTimersByTimeAsync(1599);
expect(executeHeartbeatSpy).not.toHaveBeenCalled();
await vi.advanceTimersByTimeAsync(401);
await vi.waitFor(() => {
expect(executeHeartbeatSpy).toHaveBeenCalledTimes(1);
});
});
it("clears timers on pause and re-arms from resume without stale pre-pause firing", async () => {
const store = getAgentStore(runtime);
const monitor = runtime.getHeartbeatMonitor();