feat(FN-3282): add durable review addressing snapshots
This merge adds two substantial features: a rooms and chat sidebar system (FN-3806) with a new `CreateRoomModal` and enriched `ChatView`, wired through plugin routes and a new `listTasksModifiedSince` core API; and a durable review addressing workflow (FN-3282) that persists snapshots, renders addre Fusion-Task-Id: FN-3282
This commit is contained in:
@@ -151,6 +151,58 @@ describe("buildExecutionPrompt", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("TaskExecutor review addressing transitions", () => {
|
||||
beforeEach(() => {
|
||||
resetExecutorMocks();
|
||||
});
|
||||
|
||||
it("moves queued addressing records to in-progress", async () => {
|
||||
const store = createMockStore();
|
||||
store.getTask.mockResolvedValue({
|
||||
id: "FN-001",
|
||||
column: "in-progress",
|
||||
status: null,
|
||||
reviewState: {
|
||||
source: "pull-request",
|
||||
items: [],
|
||||
addressing: [{ itemId: "ri-1", status: "queued", selectedAt: new Date().toISOString() }],
|
||||
},
|
||||
});
|
||||
|
||||
const executor = new TaskExecutor(store, "/tmp/test");
|
||||
await (executor as any).transitionReviewAddressing("FN-001", ["queued"], "in-progress");
|
||||
|
||||
expect(store.updateTask).toHaveBeenCalledWith("FN-001", {
|
||||
reviewState: expect.objectContaining({
|
||||
addressing: [expect.objectContaining({ status: "in-progress", startedAt: expect.any(String) })],
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it("marks in-progress addressing records as failed", async () => {
|
||||
const store = createMockStore();
|
||||
store.getTask.mockResolvedValue({
|
||||
id: "FN-001",
|
||||
column: "in-review",
|
||||
status: "failed",
|
||||
reviewState: {
|
||||
source: "reviewer-agent",
|
||||
items: [],
|
||||
addressing: [{ itemId: "ri-1", status: "in-progress", selectedAt: new Date().toISOString() }],
|
||||
},
|
||||
});
|
||||
|
||||
const executor = new TaskExecutor(store, "/tmp/test");
|
||||
await (executor as any).transitionReviewAddressing("FN-001", ["in-progress"], "failed");
|
||||
|
||||
expect(store.updateTask).toHaveBeenCalledWith("FN-001", {
|
||||
reviewState: expect.objectContaining({
|
||||
addressing: [expect.objectContaining({ status: "failed", completedAt: expect.any(String) })],
|
||||
}),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("TaskExecutor action gate context", () => {
|
||||
it("pauses task and agent for approval and marks completion", async () => {
|
||||
const store = createMockStore();
|
||||
|
||||
@@ -2266,8 +2266,11 @@ export class TaskExecutor {
|
||||
// true = requeue to todo, false = budget exhausted (already marked failed).
|
||||
let stuckRequeue: boolean | null = null;
|
||||
let taskDone = false;
|
||||
let reviewAddressingActivated = false;
|
||||
|
||||
try {
|
||||
await this.transitionReviewAddressing(task.id, ["queued"], "in-progress");
|
||||
reviewAddressingActivated = true;
|
||||
// Check dependencies
|
||||
const allTasks = await this.store.listTasks({ slim: true, includeArchived: false });
|
||||
const unmetDeps = task.dependencies.filter((depId) => {
|
||||
@@ -3823,6 +3826,15 @@ export class TaskExecutor {
|
||||
this.options.onError?.(task, err instanceof Error ? err : new Error(errorMessage));
|
||||
}
|
||||
} finally {
|
||||
if (reviewAddressingActivated) {
|
||||
const latestTask = await this.store.getTask(task.id);
|
||||
if (taskDone) {
|
||||
await this.transitionReviewAddressing(task.id, ["in-progress", "queued"], "addressed");
|
||||
} else if (latestTask.status === "failed") {
|
||||
await this.transitionReviewAddressing(task.id, ["in-progress", "queued"], "failed");
|
||||
}
|
||||
}
|
||||
|
||||
this.executing.delete(task.id);
|
||||
// Clear run context at end of execute() lifecycle
|
||||
this.currentRunContext = undefined;
|
||||
@@ -4130,6 +4142,41 @@ export class TaskExecutor {
|
||||
};
|
||||
}
|
||||
|
||||
private async transitionReviewAddressing(taskId: string, from: Array<"queued" | "in-progress" | "addressed" | "failed">, to: "queued" | "in-progress" | "addressed" | "failed"): Promise<void> {
|
||||
const task = await this.store.getTask(taskId);
|
||||
const reviewState = task.reviewState;
|
||||
if (!reviewState || reviewState.addressing.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = new Date().toISOString();
|
||||
let changed = false;
|
||||
const addressing = reviewState.addressing.map((record) => {
|
||||
if (!from.includes(record.status)) {
|
||||
return record;
|
||||
}
|
||||
changed = true;
|
||||
return {
|
||||
...record,
|
||||
status: to,
|
||||
startedAt: to === "in-progress" ? now : record.startedAt,
|
||||
completedAt: to === "addressed" || to === "failed" ? now : record.completedAt,
|
||||
error: to === "addressed" ? undefined : record.error,
|
||||
};
|
||||
});
|
||||
|
||||
if (!changed) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.store.updateTask(taskId, {
|
||||
reviewState: {
|
||||
...reviewState,
|
||||
addressing,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private createTaskDoneTool(taskId: string, onDone: () => void): ToolDefinition {
|
||||
const store = this.store;
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user