feat(FN-3867): add changeset for paused scope exclusion feature
Completes Step 4 by adding a patch changeset for `@runfusion/fusion`, signaling that the FN-3864 scope-exclusion work is ready for the next release. Fusion-Task-Id: FN-3867
This commit is contained in:
5
.changeset/fn-3864-paused-scope-exclusion.md
Normal file
5
.changeset/fn-3864-paused-scope-exclusion.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Scheduler: exclude paused in-review tasks from `activeScopes`. Paused failed-merge tasks no longer block dispatch of overlapping todo tasks via `blockedBy` re-stamping. (FN-3867)
|
||||||
@@ -793,6 +793,80 @@ describe("Scheduler", () => {
|
|||||||
expect(updateTask).not.toHaveBeenCalledWith("FN-002", { status: "queued", blockedBy: "FN-001" });
|
expect(updateTask).not.toHaveBeenCalledWith("FN-002", { status: "queued", blockedBy: "FN-001" });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("excludes paused in-review tasks from active scopes", async () => {
|
||||||
|
vi.mocked(existsSync).mockReturnValue(true);
|
||||||
|
vi.mocked(readFile).mockResolvedValue("# Task\nDo something");
|
||||||
|
|
||||||
|
const tasks = [
|
||||||
|
createMockTask({ id: "FN-001", column: "in-review", paused: true, worktree: "/test/project/.worktrees/fn-001" }),
|
||||||
|
createMockTask({ id: "FN-002", column: "todo" }),
|
||||||
|
];
|
||||||
|
|
||||||
|
const parseScopeMock = vi.fn(async (taskId: string): Promise<string[]> => {
|
||||||
|
if (taskId === "FN-001") return ["src/foo.ts"];
|
||||||
|
if (taskId === "FN-002") return ["src/foo.ts"];
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateTask = vi.fn().mockResolvedValue(undefined);
|
||||||
|
const moveTask = vi.fn().mockResolvedValue(undefined);
|
||||||
|
const store = createMockStore({
|
||||||
|
listTasks: vi.fn().mockResolvedValue(tasks),
|
||||||
|
getSettings: vi.fn().mockResolvedValue({
|
||||||
|
maxConcurrent: 2,
|
||||||
|
maxWorktrees: 4,
|
||||||
|
groupOverlappingFiles: true,
|
||||||
|
}),
|
||||||
|
parseFileScopeFromPrompt: parseScopeMock,
|
||||||
|
updateTask,
|
||||||
|
moveTask,
|
||||||
|
});
|
||||||
|
|
||||||
|
const scheduler = new Scheduler(store);
|
||||||
|
(scheduler as any).running = true;
|
||||||
|
await scheduler.schedule();
|
||||||
|
|
||||||
|
expect(moveTask).toHaveBeenCalledWith("FN-002", "in-progress", expect.objectContaining({ allocateWorktree: expect.any(Function) }));
|
||||||
|
expect(updateTask).not.toHaveBeenCalledWith("FN-002", { status: "queued", blockedBy: "FN-001" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("blocks todo when overlapping in-review task is not paused", async () => {
|
||||||
|
vi.mocked(existsSync).mockReturnValue(true);
|
||||||
|
vi.mocked(readFile).mockResolvedValue("# Task\nDo something");
|
||||||
|
|
||||||
|
const tasks = [
|
||||||
|
createMockTask({ id: "FN-001", column: "in-review", worktree: "/test/project/.worktrees/fn-001" }),
|
||||||
|
createMockTask({ id: "FN-002", column: "todo" }),
|
||||||
|
];
|
||||||
|
|
||||||
|
const parseScopeMock = vi.fn(async (taskId: string): Promise<string[]> => {
|
||||||
|
if (taskId === "FN-001") return ["src/foo.ts"];
|
||||||
|
if (taskId === "FN-002") return ["src/foo.ts"];
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateTask = vi.fn().mockResolvedValue(undefined);
|
||||||
|
const moveTask = vi.fn().mockResolvedValue(undefined);
|
||||||
|
const store = createMockStore({
|
||||||
|
listTasks: vi.fn().mockResolvedValue(tasks),
|
||||||
|
getSettings: vi.fn().mockResolvedValue({
|
||||||
|
maxConcurrent: 2,
|
||||||
|
maxWorktrees: 4,
|
||||||
|
groupOverlappingFiles: true,
|
||||||
|
}),
|
||||||
|
parseFileScopeFromPrompt: parseScopeMock,
|
||||||
|
updateTask,
|
||||||
|
moveTask,
|
||||||
|
});
|
||||||
|
|
||||||
|
const scheduler = new Scheduler(store);
|
||||||
|
(scheduler as any).running = true;
|
||||||
|
await scheduler.schedule();
|
||||||
|
|
||||||
|
expect(updateTask).toHaveBeenCalledWith("FN-002", { status: "queued", blockedBy: "FN-001" });
|
||||||
|
expect(moveTask).not.toHaveBeenCalledWith("FN-002", "in-progress");
|
||||||
|
});
|
||||||
|
|
||||||
it("still blocks overlap for non-ignored paths", async () => {
|
it("still blocks overlap for non-ignored paths", async () => {
|
||||||
vi.mocked(existsSync).mockReturnValue(true);
|
vi.mocked(existsSync).mockReturnValue(true);
|
||||||
vi.mocked(readFile).mockResolvedValue("# Task\nDo something");
|
vi.mocked(readFile).mockResolvedValue("# Task\nDo something");
|
||||||
|
|||||||
@@ -661,9 +661,12 @@ export class Scheduler {
|
|||||||
const filteredScope = filterPathsByIgnoreList(scope, overlapIgnorePaths);
|
const filteredScope = filterPathsByIgnoreList(scope, overlapIgnorePaths);
|
||||||
if (filteredScope.length > 0) activeScopes.set(t.id, filteredScope);
|
if (filteredScope.length > 0) activeScopes.set(t.id, filteredScope);
|
||||||
}
|
}
|
||||||
// In-review tasks with unmerged worktrees
|
// Paused in-review tasks (e.g., failed-merge tasks awaiting human triage) cannot
|
||||||
|
// make progress, so they must not contribute to activeScopes. Including them
|
||||||
|
// caused a deadlock pattern where a paused task indefinitely re-stamped
|
||||||
|
// `blockedBy` on overlapping todo tasks every scheduler tick. (FN-3867 / FN-3857)
|
||||||
const inReviewWithWorktree = tasks.filter(
|
const inReviewWithWorktree = tasks.filter(
|
||||||
(t) => t.column === "in-review" && t.worktree,
|
(t) => t.column === "in-review" && t.worktree && !t.paused,
|
||||||
);
|
);
|
||||||
for (const t of inReviewWithWorktree) {
|
for (const t of inReviewWithWorktree) {
|
||||||
const scope = await this.store.parseFileScopeFromPrompt(t.id);
|
const scope = await this.store.parseFileScopeFromPrompt(t.id);
|
||||||
|
|||||||
Reference in New Issue
Block a user