feat(FN-4690): add tracking coverage for register-task-workflow API routes
Adds test coverage for task operation API routes including workflow tracking endpoints, with minor doc updates and a changeset for the `@runfusion/fusion` package. Fusion-Task-Id: FN-4690
This commit is contained in:
committed by
gsxdsm
parent
82273a47bb
commit
f7a7038582
@@ -702,8 +702,26 @@ describe("POST /tasks/:id/duplicate", () => {
|
||||
return app;
|
||||
}
|
||||
|
||||
it("duplicates a task and returns 201 with new task", async () => {
|
||||
const newTask = { ...FAKE_TASK_DETAIL, id: "FN-002", column: "triage" };
|
||||
it("duplicates a task, returns 201, and attempts tracking issue creation", async () => {
|
||||
const createIssueSpy = vi.spyOn(GitHubClient.prototype, "createIssue").mockResolvedValue({
|
||||
owner: "task",
|
||||
repo: "repo",
|
||||
number: 91,
|
||||
htmlUrl: "https://github.com/task/repo/issues/91",
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
});
|
||||
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
githubTrackingDefaultRepo: "task/repo",
|
||||
githubAuthMode: "token",
|
||||
githubAuthToken: "tok",
|
||||
});
|
||||
|
||||
const newTask = {
|
||||
...FAKE_TASK_DETAIL,
|
||||
id: "FN-002",
|
||||
column: "triage",
|
||||
githubTracking: { enabled: true, repoOverride: "task/repo" },
|
||||
};
|
||||
(store.duplicateTask as ReturnType<typeof vi.fn>).mockResolvedValue(newTask);
|
||||
|
||||
const res = await REQUEST(buildApp(), "POST", "/api/tasks/KB-001/duplicate", JSON.stringify({}), {
|
||||
@@ -712,8 +730,36 @@ describe("POST /tasks/:id/duplicate", () => {
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
expect(res.body.id).toBe("FN-002");
|
||||
expect(res.body.column).toBe("triage");
|
||||
expect(store.duplicateTask).toHaveBeenCalledWith("KB-001");
|
||||
expect(createIssueSpy).toHaveBeenCalledWith(expect.objectContaining({ owner: "task", repo: "repo" }));
|
||||
expect(store.linkGithubIssue).toHaveBeenCalledWith("FN-002", expect.objectContaining({ owner: "task", repo: "repo", number: 91 }));
|
||||
createIssueSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("duplicate remains successful when tracking issue creation fails", async () => {
|
||||
const createIssueSpy = vi.spyOn(GitHubClient.prototype, "createIssue").mockRejectedValue(new Error("boom"));
|
||||
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
githubTrackingDefaultRepo: "task/repo",
|
||||
githubAuthMode: "token",
|
||||
githubAuthToken: "tok",
|
||||
});
|
||||
|
||||
const newTask = {
|
||||
...FAKE_TASK_DETAIL,
|
||||
id: "FN-002",
|
||||
column: "triage",
|
||||
githubTracking: { enabled: true, repoOverride: "task/repo" },
|
||||
};
|
||||
(store.duplicateTask as ReturnType<typeof vi.fn>).mockResolvedValue(newTask);
|
||||
|
||||
const res = await REQUEST(buildApp(), "POST", "/api/tasks/KB-001/duplicate", JSON.stringify({}), {
|
||||
"Content-Type": "application/json",
|
||||
});
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
expect(res.body.id).toBe("FN-002");
|
||||
expect(createIssueSpy).toHaveBeenCalledTimes(1);
|
||||
createIssueSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("returns 404 when source task not found", async () => {
|
||||
@@ -758,8 +804,27 @@ describe("POST /tasks/:id/refine", () => {
|
||||
return app;
|
||||
}
|
||||
|
||||
it("creates refinement task from done task and returns 201", async () => {
|
||||
const refinedTask = { ...FAKE_TASK_DETAIL, id: "FN-002", column: "triage", title: "Refinement: KB-001" };
|
||||
it("creates refinement task from done task, returns 201, and attempts tracking issue creation", async () => {
|
||||
const createIssueSpy = vi.spyOn(GitHubClient.prototype, "createIssue").mockResolvedValue({
|
||||
owner: "task",
|
||||
repo: "repo",
|
||||
number: 92,
|
||||
htmlUrl: "https://github.com/task/repo/issues/92",
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
});
|
||||
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
githubTrackingDefaultRepo: "task/repo",
|
||||
githubAuthMode: "token",
|
||||
githubAuthToken: "tok",
|
||||
});
|
||||
|
||||
const refinedTask = {
|
||||
...FAKE_TASK_DETAIL,
|
||||
id: "FN-002",
|
||||
column: "triage",
|
||||
title: "Refinement: KB-001",
|
||||
githubTracking: { enabled: true, repoOverride: "task/repo" },
|
||||
};
|
||||
(store.refineTask as ReturnType<typeof vi.fn>).mockResolvedValue(refinedTask);
|
||||
(store.logEntry as ReturnType<typeof vi.fn>).mockResolvedValue(FAKE_TASK_DETAIL);
|
||||
|
||||
@@ -769,9 +834,39 @@ describe("POST /tasks/:id/refine", () => {
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
expect(res.body.id).toBe("FN-002");
|
||||
expect(res.body.column).toBe("triage");
|
||||
expect(store.refineTask).toHaveBeenCalledWith("KB-001", "Need improvements");
|
||||
expect(store.logEntry).toHaveBeenCalledWith("KB-001", "Refinement requested", "Need improvements");
|
||||
expect(createIssueSpy).toHaveBeenCalledWith(expect.objectContaining({ owner: "task", repo: "repo" }));
|
||||
expect(store.linkGithubIssue).toHaveBeenCalledWith("FN-002", expect.objectContaining({ owner: "task", repo: "repo", number: 92 }));
|
||||
createIssueSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("refine remains successful when tracking issue creation fails", async () => {
|
||||
const createIssueSpy = vi.spyOn(GitHubClient.prototype, "createIssue").mockRejectedValue(new Error("boom"));
|
||||
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
githubTrackingDefaultRepo: "task/repo",
|
||||
githubAuthMode: "token",
|
||||
githubAuthToken: "tok",
|
||||
});
|
||||
|
||||
const refinedTask = {
|
||||
...FAKE_TASK_DETAIL,
|
||||
id: "FN-002",
|
||||
column: "triage",
|
||||
title: "Refinement: KB-001",
|
||||
githubTracking: { enabled: true, repoOverride: "task/repo" },
|
||||
};
|
||||
(store.refineTask as ReturnType<typeof vi.fn>).mockResolvedValue(refinedTask);
|
||||
(store.logEntry as ReturnType<typeof vi.fn>).mockResolvedValue(FAKE_TASK_DETAIL);
|
||||
|
||||
const res = await REQUEST(buildApp(), "POST", "/api/tasks/KB-001/refine", JSON.stringify({ feedback: "Need improvements" }), {
|
||||
"Content-Type": "application/json",
|
||||
});
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
expect(res.body.id).toBe("FN-002");
|
||||
expect(createIssueSpy).toHaveBeenCalledTimes(1);
|
||||
createIssueSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("creates refinement task from in-review task and returns 201", async () => {
|
||||
|
||||
@@ -717,6 +717,67 @@ describe("POST /tasks", () => {
|
||||
createIssueSpy.mockRestore();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "project default when task override is omitted",
|
||||
projectSettings: { githubTrackingEnabledByDefault: true, githubTrackingDefaultRepo: "task/repo", githubAuthMode: "token", githubAuthToken: "tok" },
|
||||
globalSettings: {},
|
||||
expectedCreatesIssue: true,
|
||||
},
|
||||
{
|
||||
name: "global default when project default is omitted",
|
||||
projectSettings: { githubTrackingDefaultRepo: "task/repo", githubAuthMode: "token", githubAuthToken: "tok" },
|
||||
globalSettings: { githubTrackingEnabledByDefault: true },
|
||||
expectedCreatesIssue: true,
|
||||
},
|
||||
{
|
||||
name: "disabled when defaults are off at every level",
|
||||
projectSettings: { githubTrackingDefaultRepo: "task/repo", githubAuthMode: "token", githubAuthToken: "tok" },
|
||||
globalSettings: { githubTrackingEnabledByDefault: false },
|
||||
expectedCreatesIssue: false,
|
||||
},
|
||||
])("honors tracking precedence: $name", async ({ projectSettings, globalSettings, expectedCreatesIssue }) => {
|
||||
const createIssueSpy = vi.spyOn(GitHubClient.prototype, "createIssue").mockResolvedValue({
|
||||
owner: "task",
|
||||
repo: "repo",
|
||||
number: 43,
|
||||
htmlUrl: "https://github.com/task/repo/issues/43",
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
});
|
||||
|
||||
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue(projectSettings);
|
||||
const mockGlobalSettingsStore = {
|
||||
getSettings: vi.fn().mockResolvedValue(globalSettings),
|
||||
updateSettings: vi.fn().mockResolvedValue({}),
|
||||
getSettingsPath: vi.fn().mockReturnValue("/fake/home/.fusion/settings.json"),
|
||||
init: vi.fn().mockResolvedValue(false),
|
||||
invalidateCache: vi.fn(),
|
||||
};
|
||||
(store.getGlobalSettingsStore as ReturnType<typeof vi.fn>).mockReturnValue(mockGlobalSettingsStore);
|
||||
(store.createTask as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
...FAKE_TASK_DETAIL,
|
||||
githubTracking: expectedCreatesIssue ? { enabled: true, repoOverride: "task/repo" } : undefined,
|
||||
});
|
||||
|
||||
const res = await REQUEST(
|
||||
buildApp(),
|
||||
"POST",
|
||||
"/api/tasks",
|
||||
JSON.stringify({ description: "Track settings precedence" }),
|
||||
{ "Content-Type": "application/json" },
|
||||
);
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
if (expectedCreatesIssue) {
|
||||
expect(createIssueSpy).toHaveBeenCalledTimes(1);
|
||||
expect(store.linkGithubIssue).toHaveBeenCalledWith("FN-001", expect.objectContaining({ owner: "task", repo: "repo", number: 43 }));
|
||||
} else {
|
||||
expect(createIssueSpy).not.toHaveBeenCalled();
|
||||
}
|
||||
|
||||
createIssueSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("uses store.createTask for local task creation", async () => {
|
||||
const createTask = vi.fn().mockResolvedValue({
|
||||
...FAKE_TASK_DETAIL,
|
||||
|
||||
@@ -605,6 +605,7 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
|
||||
try {
|
||||
const { store: scopedStore } = await getProjectContext(req);
|
||||
const newTask = await scopedStore.duplicateTask(req.params.id);
|
||||
await maybeCreateTaskTrackingIssue(scopedStore, newTask, options?.githubToken);
|
||||
res.status(201).json(newTask);
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof ApiError) {
|
||||
@@ -631,6 +632,7 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
|
||||
}
|
||||
|
||||
const refinedTask = await scopedStore.refineTask(req.params.id, trimmedFeedback);
|
||||
await maybeCreateTaskTrackingIssue(scopedStore, refinedTask, options?.githubToken);
|
||||
await scopedStore.logEntry(req.params.id, "Refinement requested", trimmedFeedback);
|
||||
res.status(201).json(refinedTask);
|
||||
} catch (err: unknown) {
|
||||
|
||||
Reference in New Issue
Block a user