feat(FN-2008): merge fusion/fn-2008
This commit is contained in:
@@ -1758,15 +1758,13 @@ export function TaskDetailModal({
|
||||
)}
|
||||
|
||||
{/* Respecify */}
|
||||
{task.column !== "triage" && (
|
||||
<button
|
||||
className="detail-actions-menu-item"
|
||||
role="menuitem"
|
||||
onClick={() => handleActionsMenuItemClick(handleRespecify)}
|
||||
>
|
||||
Respecify
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="detail-actions-menu-item"
|
||||
role="menuitem"
|
||||
onClick={() => handleActionsMenuItemClick(handleRespecify)}
|
||||
>
|
||||
Respecify
|
||||
</button>
|
||||
|
||||
{/* Retry */}
|
||||
{canRetryTask && onRetryTask && (
|
||||
|
||||
@@ -6075,21 +6075,41 @@ describe("POST /tasks/:id/spec/revise", () => {
|
||||
expect(store.moveTask).toHaveBeenCalledWith("FN-001", "triage");
|
||||
});
|
||||
|
||||
it("returns 400 when task is already in triage", async () => {
|
||||
it("allows spec revision for task already in triage", async () => {
|
||||
const triageTask = { ...FAKE_TASK_DETAIL, column: "triage" as const };
|
||||
(store.getTask as ReturnType<typeof vi.fn>).mockResolvedValue(triageTask);
|
||||
const updatedTask = { ...FAKE_TASK_DETAIL, column: "triage" as const, status: "needs-respecify" as const };
|
||||
const tempRoot = mkdtempSync(join(tmpdir(), "kb-spec-revise-triage-"));
|
||||
const taskDir = join(tempRoot, ".fusion", "tasks", "FN-001");
|
||||
mkdirSync(taskDir, { recursive: true });
|
||||
writeFileSync(join(taskDir, "PROMPT.md"), "# stale spec\n");
|
||||
|
||||
const res = await REQUEST(
|
||||
buildApp(),
|
||||
"POST",
|
||||
"/api/tasks/KB-001/spec/revise",
|
||||
JSON.stringify({ feedback: "Some feedback" }),
|
||||
{ "Content-Type": "application/json" }
|
||||
);
|
||||
(store.getTask as ReturnType<typeof vi.fn>)
|
||||
.mockResolvedValueOnce(triageTask)
|
||||
.mockResolvedValueOnce(updatedTask);
|
||||
(store.updateTask as ReturnType<typeof vi.fn>).mockResolvedValue(updatedTask);
|
||||
(store.getRootDir as ReturnType<typeof vi.fn>).mockReturnValue(tempRoot);
|
||||
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.body.error).toContain("Cannot request spec revision");
|
||||
expect(store.moveTask).not.toHaveBeenCalled();
|
||||
try {
|
||||
const res = await REQUEST(
|
||||
buildApp(),
|
||||
"POST",
|
||||
"/api/tasks/KB-001/spec/revise",
|
||||
JSON.stringify({ feedback: "Some feedback" }),
|
||||
{ "Content-Type": "application/json" }
|
||||
);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(store.logEntry).toHaveBeenCalledWith(
|
||||
"FN-001",
|
||||
"AI spec revision requested",
|
||||
"Some feedback"
|
||||
);
|
||||
expect(store.moveTask).not.toHaveBeenCalled();
|
||||
expect(existsSync(join(taskDir, "PROMPT.md"))).toBe(false);
|
||||
expect(store.updateTask).toHaveBeenCalledWith("FN-001", { status: "needs-respecify" });
|
||||
} finally {
|
||||
rmSync(tempRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("returns 400 when task is in in-review", async () => {
|
||||
@@ -6297,15 +6317,34 @@ describe("POST /tasks/:id/spec/rebuild", () => {
|
||||
expect(store.moveTask).toHaveBeenCalledWith("FN-001", "triage");
|
||||
});
|
||||
|
||||
it("returns 400 when task is already in triage", async () => {
|
||||
it("allows rebuild for task already in triage", async () => {
|
||||
const triageTask = { ...FAKE_TASK_DETAIL, column: "triage" as const };
|
||||
(store.getTask as ReturnType<typeof vi.fn>).mockResolvedValue(triageTask);
|
||||
const updatedTask = { ...FAKE_TASK_DETAIL, column: "triage" as const, status: "needs-respecify" as const };
|
||||
const tempRoot = mkdtempSync(join(tmpdir(), "kb-spec-rebuild-triage-"));
|
||||
const taskDir = join(tempRoot, ".fusion", "tasks", "FN-001");
|
||||
mkdirSync(taskDir, { recursive: true });
|
||||
writeFileSync(join(taskDir, "PROMPT.md"), "# stale spec\n");
|
||||
|
||||
const res = await REQUEST(buildApp(), "POST", "/api/tasks/KB-001/spec/rebuild");
|
||||
(store.getTask as ReturnType<typeof vi.fn>)
|
||||
.mockResolvedValueOnce(triageTask)
|
||||
.mockResolvedValueOnce(updatedTask);
|
||||
(store.updateTask as ReturnType<typeof vi.fn>).mockResolvedValue(updatedTask);
|
||||
(store.getRootDir as ReturnType<typeof vi.fn>).mockReturnValue(tempRoot);
|
||||
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.body.error).toContain("Cannot rebuild spec");
|
||||
expect(store.moveTask).not.toHaveBeenCalled();
|
||||
try {
|
||||
const res = await REQUEST(buildApp(), "POST", "/api/tasks/KB-001/spec/rebuild");
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(store.logEntry).toHaveBeenCalledWith(
|
||||
"FN-001",
|
||||
"Specification rebuild requested by user"
|
||||
);
|
||||
expect(store.moveTask).not.toHaveBeenCalled();
|
||||
expect(existsSync(join(taskDir, "PROMPT.md"))).toBe(false);
|
||||
expect(store.updateTask).toHaveBeenCalledWith("FN-001", { status: "needs-respecify" });
|
||||
} finally {
|
||||
rmSync(tempRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("returns 400 when task is in in-review (cannot transition to triage)", async () => {
|
||||
|
||||
@@ -4533,6 +4533,27 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
||||
// Get current task state
|
||||
const task = await scopedStore.getTask(req.params.id);
|
||||
|
||||
// If task is already in triage, skip the transition check and moveTask.
|
||||
// Just reset for re-specification in place.
|
||||
if (task.column === "triage") {
|
||||
// Log the revision request
|
||||
await scopedStore.logEntry(task.id, "AI spec revision requested", feedback);
|
||||
|
||||
// Remove the existing spec so re-specification starts from the task
|
||||
// description and feedback rather than revising stale PROMPT.md content.
|
||||
const { rm } = await import("node:fs/promises");
|
||||
const { join } = await import("node:path");
|
||||
const promptPath = join(scopedStore.getRootDir(), ".fusion", "tasks", task.id, "PROMPT.md");
|
||||
await rm(promptPath, { force: true });
|
||||
|
||||
// Update status to indicate needs re-specification
|
||||
await scopedStore.updateTask(task.id, { status: "needs-respecify" });
|
||||
|
||||
const updated = await scopedStore.getTask(task.id);
|
||||
res.json(updated);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if task can transition to triage
|
||||
const canTransition = VALID_TRANSITIONS[task.column]?.includes("triage");
|
||||
if (!canTransition) {
|
||||
@@ -4544,7 +4565,7 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
||||
// Log the revision request
|
||||
await scopedStore.logEntry(task.id, "AI spec revision requested", feedback);
|
||||
|
||||
// Move to triage for re-specification (only valid for todo/in-progress)
|
||||
// Move to triage for re-specification
|
||||
const updated = await scopedStore.moveTask(task.id, "triage");
|
||||
|
||||
// Remove the existing spec so re-specification starts from the task
|
||||
@@ -4578,6 +4599,27 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
||||
// Get current task state
|
||||
const task = await scopedStore.getTask(req.params.id);
|
||||
|
||||
// If task is already in triage, skip the transition check and moveTask.
|
||||
// Just reset for re-specification in place.
|
||||
if (task.column === "triage") {
|
||||
// Log the rebuild request
|
||||
await scopedStore.logEntry(task.id, "Specification rebuild requested by user");
|
||||
|
||||
// Remove the existing spec so rebuilds produce a fresh PROMPT.md instead
|
||||
// of asking triage to revise whatever was already on disk.
|
||||
const { rm } = await import("node:fs/promises");
|
||||
const { join } = await import("node:path");
|
||||
const promptPath = join(scopedStore.getRootDir(), ".fusion", "tasks", task.id, "PROMPT.md");
|
||||
await rm(promptPath, { force: true });
|
||||
|
||||
// Update status to indicate needs re-specification
|
||||
await scopedStore.updateTask(task.id, { status: "needs-respecify" });
|
||||
|
||||
const updated = await scopedStore.getTask(task.id);
|
||||
res.json(updated);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if task can transition to triage
|
||||
const canTransition = VALID_TRANSITIONS[task.column]?.includes("triage");
|
||||
if (!canTransition) {
|
||||
|
||||
Reference in New Issue
Block a user