feat(FN-4378): complete Step 4 — plumb github issue action through API

Fusion-Task-Id: FN-4378
Fusion-Task-Lineage: d3867ba8-f852-41e3-9db0-584c0ffc23b1
This commit is contained in:
Fusion
2026-05-13 13:37:14 -07:00
committed by gsxdsm
parent c4baef9a4a
commit 97b2321f7e
4 changed files with 57 additions and 5 deletions

View File

@@ -564,6 +564,18 @@ describe("Git Management API", () => {
});
});
it("sends githubIssueAction when requested", async () => {
const deletedTask: Task = { ...FAKE_DETAIL, column: "done" };
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, deletedTask));
await deleteTask("FN-001", undefined, { githubIssueAction: "delete" });
expect(globalThis.fetch).toHaveBeenCalledWith("/api/tasks/FN-001?githubIssueAction=delete", {
headers: { "Content-Type": "application/json" },
method: "DELETE",
});
});
it("throws ApiRequestError on error and preserves details payload", async () => {
globalThis.fetch = vi.fn().mockReturnValue(
mockFetchResponse(

View File

@@ -77,7 +77,7 @@ import type {
TaskIdIntegrityReport,
} from "@fusion/core";
import type { PlanningQuestion, PlanningSummary } from "@fusion/core";
import type { ScheduledTask, ScheduledTaskCreateInput, ScheduledTaskUpdateInput, AutomationRunResult, Routine, RoutineCreateInput, RoutineUpdateInput, RoutineExecutionResult } from "@fusion/core";
import type { GithubIssueAction, ScheduledTask, ScheduledTaskCreateInput, ScheduledTaskUpdateInput, AutomationRunResult, Routine, RoutineCreateInput, RoutineUpdateInput, RoutineExecutionResult } from "@fusion/core";
import type { DiscoveredSkill, CatalogEntry, CatalogFetchResult, ToggleSkillResult, SkillContent, SkillFileEntry } from "@fusion/dashboard";
import type { MilestoneValidationTelemetry, MissionInterviewDraftSummary } from "../components/mission-types";
import type {
@@ -106,6 +106,7 @@ export class ApiRequestError extends Error {
export interface DeleteTaskOptions {
removeDependencyReferences?: boolean;
githubIssueAction?: GithubIssueAction;
}
function looksLikeHtml(body: string): boolean {
@@ -455,6 +456,9 @@ export function deleteTask(id: string, projectId?: string, options?: DeleteTaskO
if (options?.removeDependencyReferences) {
search.set("removeDependencyReferences", "true");
}
if (options?.githubIssueAction) {
search.set("githubIssueAction", options.githubIssueAction);
}
const suffix = search.size > 0 ? `?${search.toString()}` : "";
return api<Task>(withProjectId(`/tasks/${id}${suffix}`, projectId), { method: "DELETE" });

View File

@@ -890,7 +890,10 @@ describe("DELETE /tasks/:id", () => {
expect(res.status).toBe(200);
expect(res.body.id).toBe("KB-001");
expect(store.deleteTask).toHaveBeenCalledWith("KB-001", { removeDependencyReferences: false });
expect(store.deleteTask).toHaveBeenCalledWith("KB-001", {
removeDependencyReferences: false,
githubIssueAction: undefined,
});
});
it("returns structured 409 conflict when delete is blocked by dependents", async () => {
@@ -917,7 +920,31 @@ describe("DELETE /tasks/:id", () => {
const res = await REQUEST(buildApp(), "DELETE", "/api/tasks/KB-001?removeDependencyReferences=true");
expect(res.status).toBe(200);
expect(store.deleteTask).toHaveBeenCalledWith("KB-001", { removeDependencyReferences: true });
expect(store.deleteTask).toHaveBeenCalledWith("KB-001", {
removeDependencyReferences: true,
githubIssueAction: undefined,
});
});
it.each(["close", "delete", "leave", "auto"] as const)("forwards githubIssueAction=%s", async (githubIssueAction) => {
const deletedTask = { ...FAKE_TASK_DETAIL, id: "KB-001" };
(store.deleteTask as ReturnType<typeof vi.fn>).mockResolvedValue(deletedTask);
const res = await REQUEST(buildApp(), "DELETE", `/api/tasks/KB-001?githubIssueAction=${githubIssueAction}`);
expect(res.status).toBe(200);
expect(store.deleteTask).toHaveBeenCalledWith("KB-001", {
removeDependencyReferences: false,
githubIssueAction,
});
});
it("rejects invalid githubIssueAction values", async () => {
const res = await REQUEST(buildApp(), "DELETE", "/api/tasks/KB-001?githubIssueAction=bad-value");
expect(res.status).toBe(400);
expect(res.body.error).toContain("githubIssueAction must be one of: close, delete, leave, auto");
expect(store.deleteTask).not.toHaveBeenCalled();
});
});

View File

@@ -1,5 +1,5 @@
import { createReadStream } from "node:fs";
import type { TaskStore, Task, TaskDetail, Column, TaskReviewData, TaskReviewItem, TaskReviewSummary } from "@fusion/core";
import type { TaskStore, Task, TaskDetail, Column, TaskReviewData, TaskReviewItem, TaskReviewSummary, GithubIssueAction } from "@fusion/core";
import {
COLUMNS,
TASK_PRIORITIES,
@@ -2229,7 +2229,16 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
const { store: scopedStore } = await getProjectContext(req);
const removeDependencyReferences = req.query.removeDependencyReferences === "1"
|| req.query.removeDependencyReferences === "true";
const task = await scopedStore.deleteTask(req.params.id, { removeDependencyReferences });
const githubIssueActionRaw = req.query.githubIssueAction;
const githubIssueActionValues: readonly GithubIssueAction[] = ["close", "delete", "leave", "auto"];
let githubIssueAction: GithubIssueAction | undefined;
if (typeof githubIssueActionRaw === "string") {
if (!githubIssueActionValues.includes(githubIssueActionRaw as GithubIssueAction)) {
throw badRequest("githubIssueAction must be one of: close, delete, leave, auto");
}
githubIssueAction = githubIssueActionRaw as GithubIssueAction;
}
const task = await scopedStore.deleteTask(req.params.id, { removeDependencyReferences, githubIssueAction });
res.json(task);
} catch (err: unknown) {
if (err instanceof ApiError) {