feat(FN-3276): add task review tab with metadata persistence and desktop la
This merge lands three major features and a significant dashboard enhancement. FN-3276 adds a full Review tab to the task detail modal with multi-step lifecycle: review metadata persistence in the task store, new task workflow routes for refresh and same-task revision, and the review tab UI surface Fusion-Task-Id: FN-3276
This commit is contained in:
@@ -4,7 +4,10 @@ import type { TaskStore, Task } from "@fusion/core";
|
||||
|
||||
const mockStore = {
|
||||
addTaskComment: vi.fn<(id: string, text: string, author?: string) => Promise<Task>>(),
|
||||
getTask: vi.fn<(id: string) => Promise<Task>>().mockResolvedValue({ id: "FN-001", review: undefined } as Task),
|
||||
updateTask: vi.fn<(id: string, updates: Partial<Task>) => Promise<Task>>().mockResolvedValue({ id: "FN-001" } as Task),
|
||||
createTask: vi.fn<(input: Parameters<TaskStore["createTask"]>[0]) => Promise<Task>>().mockResolvedValue({ id: "FN-123" } as Task),
|
||||
moveTask: vi.fn<(id: string, column: Task["column"]) => Promise<Task>>().mockResolvedValue({ id: "FN-001", column: "in-progress" } as Task),
|
||||
} as unknown as TaskStore;
|
||||
|
||||
describe("PrCommentHandler", () => {
|
||||
@@ -195,6 +198,27 @@ describe("PrCommentHandler", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("handleChangesRequested", () => {
|
||||
it("persists review item feedback when changes are requested", async () => {
|
||||
(mockStore.getTask as ReturnType<typeof vi.fn>).mockResolvedValue({ id: "FN-001", column: "in-review", review: undefined } as Task);
|
||||
|
||||
await handler.handleChangesRequested("FN-001", mockPrInfo, "reviewer", "Please add tests");
|
||||
|
||||
expect(mockStore.updateTask).toHaveBeenCalledWith(
|
||||
"FN-001",
|
||||
expect.objectContaining({
|
||||
review: expect.objectContaining({
|
||||
mode: "pull-request",
|
||||
items: expect.arrayContaining([
|
||||
expect.objectContaining({ source: "github-pr", status: "queued" }),
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(mockStore.moveTask).toHaveBeenCalledWith("FN-001", "in-progress");
|
||||
});
|
||||
});
|
||||
|
||||
describe("createFollowUpTask", () => {
|
||||
it("creates follow-up task for unaddressed feedback", async () => {
|
||||
await handler.createFollowUpTask("FN-001", mockPrInfo, [
|
||||
|
||||
@@ -86,6 +86,7 @@ export class PrCommentHandler {
|
||||
|
||||
try {
|
||||
await this.store.addTaskComment(taskId, text, "agent");
|
||||
await this.upsertReviewItem(taskId, prInfo, comment, "queued");
|
||||
prMonitorLog.log(`Added comment for PR review #${comment.id}`);
|
||||
} catch (err) {
|
||||
prMonitorLog.error(`Failed to add comment for ${taskId}:`, err);
|
||||
@@ -179,6 +180,19 @@ export class PrCommentHandler {
|
||||
].join("\n");
|
||||
|
||||
await this.store.addTaskComment(taskId, feedbackText, "agent");
|
||||
await this.upsertReviewItem(
|
||||
taskId,
|
||||
prInfo,
|
||||
{
|
||||
id: Date.now(),
|
||||
body: reviewBody || "(no review body)",
|
||||
user: { login: reviewerLogin },
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
html_url: prInfo.url,
|
||||
},
|
||||
"queued",
|
||||
);
|
||||
await this.store.moveTask(taskId, "in-progress");
|
||||
await this.store.logEntry(
|
||||
taskId,
|
||||
@@ -231,4 +245,51 @@ Please review the PR comments and address any remaining issues.`;
|
||||
prMonitorLog.error(`Failed to create follow-up task:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
private async upsertReviewItem(
|
||||
taskId: string,
|
||||
prInfo: PrInfo,
|
||||
comment: PrComment,
|
||||
status: "queued" | "in-progress" | "addressed" | "failed",
|
||||
): Promise<void> {
|
||||
const task = await this.store.getTask(taskId);
|
||||
const now = new Date().toISOString();
|
||||
const current = task.review ?? {
|
||||
mode: "pull-request",
|
||||
source: "github-pr",
|
||||
decision: "pending",
|
||||
items: [],
|
||||
selectedItemIds: [],
|
||||
};
|
||||
const itemId = `gh-comment-${comment.id}`;
|
||||
const existingIndex = current.items.findIndex((item: { id: string }) => item.id === itemId);
|
||||
const nextItem = {
|
||||
id: itemId,
|
||||
source: "github-pr" as const,
|
||||
status,
|
||||
summary: comment.body.trim().slice(0, 160) || `Feedback from @${comment.user.login}`,
|
||||
body: comment.body,
|
||||
reviewer: comment.user.login,
|
||||
commentUrl: comment.html_url,
|
||||
createdAt: comment.created_at,
|
||||
updatedAt: now,
|
||||
};
|
||||
const nextItems = [...current.items];
|
||||
if (existingIndex >= 0) {
|
||||
nextItems[existingIndex] = { ...nextItems[existingIndex], ...nextItem };
|
||||
} else {
|
||||
nextItems.push(nextItem);
|
||||
}
|
||||
|
||||
await this.store.updateTask(taskId, {
|
||||
review: {
|
||||
...current,
|
||||
mode: "pull-request",
|
||||
source: "github-pr",
|
||||
summary: `PR #${prInfo.number} feedback items: ${nextItems.length}`,
|
||||
latestRefreshAt: now,
|
||||
items: nextItems,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user