feat(FN-4401): complete remaining auto-claim snapshot integration
Fusion-Task-Id: FN-4401 Fusion-Task-Lineage: c1b6c497-b22c-48d5-b1c8-299877bf09ac
This commit is contained in:
@@ -12,15 +12,15 @@ function makeTask(overrides: Partial<Task> & Pick<Task, "id">): Task {
|
||||
createdAt: overrides.createdAt ?? "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: overrides.updatedAt ?? "2026-01-01T00:00:00.000Z",
|
||||
dependencies: overrides.dependencies ?? [],
|
||||
labels: overrides.labels ?? [],
|
||||
comments: overrides.comments ?? [],
|
||||
activityLog: overrides.activityLog ?? [],
|
||||
metadata: overrides.metadata ?? {},
|
||||
steps: overrides.steps ?? [],
|
||||
currentStep: overrides.currentStep ?? 0,
|
||||
log: overrides.log ?? [],
|
||||
assignedAgentId: overrides.assignedAgentId,
|
||||
checkedOutBy: overrides.checkedOutBy,
|
||||
paused: overrides.paused,
|
||||
columnMovedAt: overrides.columnMovedAt,
|
||||
} as Task;
|
||||
} as unknown as Task;
|
||||
}
|
||||
|
||||
describe("AutoClaimSnapshotManager", () => {
|
||||
|
||||
@@ -896,6 +896,51 @@ describe("executeHeartbeat", () => {
|
||||
expect(store.claimTaskForAgent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("reuses one snapshot rebuild across concurrent no-task heartbeats", async () => {
|
||||
const listTasks = vi.fn().mockResolvedValue([
|
||||
{
|
||||
id: "FN-CANDIDATE",
|
||||
description: "executor reliability follow-up",
|
||||
title: "Executor reliability",
|
||||
prompt: "",
|
||||
steps: [],
|
||||
column: "todo",
|
||||
dependencies: [],
|
||||
log: [],
|
||||
attachments: [],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
} as unknown as TaskDetail,
|
||||
]);
|
||||
mockTaskStore = createMockTaskStore({ listTasks });
|
||||
const store = createStoreWithAgentForExec({ taskId: undefined, soul: "executor reliability owner" });
|
||||
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
|
||||
|
||||
await Promise.all([
|
||||
monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" }),
|
||||
monitor.executeHeartbeat({ agentId: "agent-001", source: "on_demand" }),
|
||||
]);
|
||||
|
||||
expect(listTasks).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("omits candidate section when autoClaimCandidatesInPrompt resolves to zero", async () => {
|
||||
const store = createStoreWithAgentForExec({
|
||||
taskId: undefined,
|
||||
soul: "executor reliability owner",
|
||||
runtimeConfig: { autoClaimRelevantTasks: true, autoClaimCandidatesInPrompt: 0 },
|
||||
});
|
||||
const mockSession = createMockAgentSession();
|
||||
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
|
||||
|
||||
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
|
||||
await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
|
||||
|
||||
const executionPrompt = mockSession.prompt.mock.calls.at(-1)?.[0] as string;
|
||||
expect(executionPrompt).toContain("auto-claim relevant tasks: disabled (prompt-suppressed)");
|
||||
expect(executionPrompt).not.toContain("Open unowned tasks you may auto-claim");
|
||||
});
|
||||
|
||||
it("agent WITH instructionsText but no task creates session and completes successfully", async () => {
|
||||
const store = createStoreWithAgentForExec({ taskId: undefined, instructionsText: "Monitor task board and create follow-up tasks" });
|
||||
const mockSession = createMockAgentSession();
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { TaskStore } from "@fusion/core";
|
||||
import { Scheduler } from "../scheduler.js";
|
||||
|
||||
function createStore() {
|
||||
const listeners = new Map<string, ((payload: unknown) => void)[]>();
|
||||
const on = vi.fn((event: string, listener: (payload: unknown) => void) => {
|
||||
const existing = listeners.get(event) ?? [];
|
||||
existing.push(listener);
|
||||
listeners.set(event, existing);
|
||||
});
|
||||
|
||||
const store = {
|
||||
on,
|
||||
off: vi.fn(),
|
||||
} as unknown as TaskStore;
|
||||
|
||||
const emit = (event: string, payload: unknown) => {
|
||||
for (const listener of listeners.get(event) ?? []) {
|
||||
listener(payload);
|
||||
}
|
||||
};
|
||||
|
||||
return { store, emit };
|
||||
}
|
||||
|
||||
describe("Scheduler auto-claim snapshot invalidation", () => {
|
||||
it("invalidates on task:created and task:updated", () => {
|
||||
const invalidate = vi.fn();
|
||||
const { store, emit } = createStore();
|
||||
new Scheduler(store, { snapshotManager: { invalidate } as any });
|
||||
|
||||
emit("task:created", { task: { id: "FN-1" } });
|
||||
emit("task:updated", { id: "FN-1" });
|
||||
|
||||
expect(invalidate).toHaveBeenCalledWith("task:created");
|
||||
expect(invalidate).toHaveBeenCalledWith("task:updated");
|
||||
});
|
||||
|
||||
it("invalidates task:moved only when todo is source or destination", () => {
|
||||
const invalidate = vi.fn();
|
||||
const { store, emit } = createStore();
|
||||
new Scheduler(store, { snapshotManager: { invalidate } as any });
|
||||
|
||||
emit("task:moved", { task: { id: "FN-1" }, from: "todo", to: "in-progress" });
|
||||
emit("task:moved", { task: { id: "FN-2" }, from: "in-progress", to: "todo" });
|
||||
emit("task:moved", { task: { id: "FN-3" }, from: "in-review", to: "done" });
|
||||
|
||||
expect(invalidate).toHaveBeenCalledWith("task:moved:todo->in-progress");
|
||||
expect(invalidate).toHaveBeenCalledWith("task:moved:in-progress->todo");
|
||||
expect(invalidate).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user