feat(FN-3953): enable tracking issue creation on task edit and document the
Implements GitHub tracking issue creation on PATCH operations for FN-3953, with accompanying test coverage and documentation updates for edit-time tracking. Fusion-Task-Id: FN-3953
This commit is contained in:
@@ -17,6 +17,7 @@ describe("github tracking documentation contract", () => {
|
||||
expect(taskManagement).toContain("## GitHub Tracking Issues");
|
||||
expect(taskManagement).toContain("They are **not** the same as imported source issues (`issueInfo` / `sourceIssue`)");
|
||||
expect(taskManagement).toContain("task creation flows (including quick create, planning output, and subtask creation paths that create tasks)");
|
||||
expect(taskManagement).toContain("Fusion also attempts issue creation on existing-task edits that update `githubTracking`");
|
||||
expect(taskManagement).toContain("task.githubTracking.enabled");
|
||||
expect(taskManagement).toContain("task.githubTracking.repoOverride");
|
||||
expect(taskManagement).toContain("Repository resolution order");
|
||||
@@ -24,6 +25,7 @@ describe("github tracking documentation contract", () => {
|
||||
expect(taskManagement).toContain("2. Project default: `githubTrackingDefaultRepo`");
|
||||
expect(taskManagement).toContain("3. Global default: `githubTrackingDefaultRepo`");
|
||||
expect(taskManagement).toContain("Creation is best-effort and non-blocking");
|
||||
expect(taskManagement).toContain("Explicit manual unlink (`githubTracking.issue: null`) does not recreate a tracking issue in that same update request");
|
||||
expect(taskManagement).toContain("Title: `[FN-XXXX] Task title`");
|
||||
expect(taskManagement).toContain("Body prefix: `Fusion task: FN-XXXX`");
|
||||
});
|
||||
|
||||
@@ -204,6 +204,8 @@ function createMockStore(overrides: Partial<TaskStore> = {}): TaskStore {
|
||||
deleteTaskDocument: vi.fn().mockResolvedValue(undefined),
|
||||
updatePrInfo: vi.fn().mockResolvedValue(undefined),
|
||||
updateIssueInfo: vi.fn().mockResolvedValue(undefined),
|
||||
linkGithubIssue: vi.fn().mockResolvedValue(undefined),
|
||||
recordActivity: vi.fn().mockResolvedValue(undefined),
|
||||
getRootDir: vi.fn().mockReturnValue("/fake/root"),
|
||||
listWorkflowSteps: vi.fn().mockResolvedValue([]),
|
||||
createWorkflowStep: vi.fn(),
|
||||
@@ -1679,6 +1681,97 @@ describe("PATCH /tasks/:id", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("creates and links a tracking issue when enabling tracking on an existing task with resolvable repo", async () => {
|
||||
const createIssueSpy = vi.spyOn(GitHubClient.prototype, "createIssue").mockResolvedValue({
|
||||
owner: "runfusion",
|
||||
repo: "fusion",
|
||||
number: 73,
|
||||
htmlUrl: "https://github.com/runfusion/fusion/issues/73",
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
});
|
||||
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({ githubAuthMode: "token", githubAuthToken: "tok" });
|
||||
(store.updateTask as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
...FAKE_TASK_DETAIL,
|
||||
id: "KB-001",
|
||||
githubTracking: { enabled: true, repoOverride: "runfusion/fusion" },
|
||||
});
|
||||
(store.getTask as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
...FAKE_TASK_DETAIL,
|
||||
id: "KB-001",
|
||||
githubTracking: {
|
||||
enabled: true,
|
||||
repoOverride: "runfusion/fusion",
|
||||
issue: { owner: "runfusion", repo: "fusion", number: 73, url: "https://github.com/runfusion/fusion/issues/73", createdAt: "2026-01-01T00:00:00.000Z" },
|
||||
},
|
||||
});
|
||||
|
||||
const res = await REQUEST(buildApp(), "PATCH", "/api/tasks/KB-001", JSON.stringify({
|
||||
githubTracking: {
|
||||
enabled: true,
|
||||
repoOverride: "runfusion/fusion",
|
||||
},
|
||||
}), {
|
||||
"Content-Type": "application/json",
|
||||
});
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(createIssueSpy).toHaveBeenCalledWith(expect.objectContaining({ owner: "runfusion", repo: "fusion" }));
|
||||
expect(store.linkGithubIssue).toHaveBeenCalledWith("KB-001", expect.objectContaining({ owner: "runfusion", repo: "fusion", number: 73 }));
|
||||
createIssueSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("does not recreate tracking issue during explicit manual unlink patch", async () => {
|
||||
const createIssueSpy = vi.spyOn(GitHubClient.prototype, "createIssue").mockResolvedValue({
|
||||
owner: "runfusion",
|
||||
repo: "fusion",
|
||||
number: 99,
|
||||
htmlUrl: "https://github.com/runfusion/fusion/issues/99",
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
});
|
||||
(store.updateTask as ReturnType<typeof vi.fn>).mockResolvedValue({ ...FAKE_TASK_DETAIL });
|
||||
|
||||
const res = await REQUEST(buildApp(), "PATCH", "/api/tasks/KB-001", JSON.stringify({
|
||||
githubTracking: { issue: null },
|
||||
}), {
|
||||
"Content-Type": "application/json",
|
||||
});
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(createIssueSpy).not.toHaveBeenCalled();
|
||||
expect(store.getTask).not.toHaveBeenCalled();
|
||||
createIssueSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("does not create tracking issue when disabling tracking", async () => {
|
||||
const createIssueSpy = vi.spyOn(GitHubClient.prototype, "createIssue").mockResolvedValue({
|
||||
owner: "runfusion",
|
||||
repo: "fusion",
|
||||
number: 100,
|
||||
htmlUrl: "https://github.com/runfusion/fusion/issues/100",
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
});
|
||||
(store.updateTask as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
...FAKE_TASK_DETAIL,
|
||||
id: "KB-001",
|
||||
githubTracking: { enabled: false, repoOverride: "runfusion/fusion" },
|
||||
});
|
||||
(store.getTask as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
...FAKE_TASK_DETAIL,
|
||||
id: "KB-001",
|
||||
githubTracking: { enabled: false, repoOverride: "runfusion/fusion" },
|
||||
});
|
||||
|
||||
const res = await REQUEST(buildApp(), "PATCH", "/api/tasks/KB-001", JSON.stringify({
|
||||
githubTracking: { enabled: false },
|
||||
}), {
|
||||
"Content-Type": "application/json",
|
||||
});
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(createIssueSpy).not.toHaveBeenCalled();
|
||||
createIssueSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("returns 400 for invalid githubTracking repo override format", async () => {
|
||||
const res = await REQUEST(buildApp(), "PATCH", "/api/tasks/KB-001", JSON.stringify({
|
||||
githubTracking: {
|
||||
|
||||
Reference in New Issue
Block a user