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:
Fusion
2026-05-10 14:48:52 -07:00
committed by gsxdsm
parent 5f4fd222da
commit cedf07c1fe
5 changed files with 117 additions and 5 deletions

View File

@@ -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`");
});

View File

@@ -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: {