feat(FN-3963): clarify automation tracked-task creation semantics
Adds regression tests for automation tracked-task creation behavior across the dashboard automation and tasks routes, plus a documentation clarification on the semantics of how automation creates tracked tasks. Fusion-Task-Id: FN-3963
This commit is contained in:
@@ -94,6 +94,22 @@ describe("maybeCreateTrackingIssue", () => {
|
||||
expect(result).toEqual({ created: false, reason: "tracking_disabled" });
|
||||
});
|
||||
|
||||
it("returns issue_already_linked and does not create again", async () => {
|
||||
const result = await maybeCreateTrackingIssue(buildTask({
|
||||
githubTracking: {
|
||||
enabled: true,
|
||||
issue: { owner: "task", repo: "repo", number: 99, url: "https://github.com/task/repo/issues/99" },
|
||||
},
|
||||
}), {
|
||||
taskStore: {} as any,
|
||||
projectSettings: { githubTrackingDefaultRepo: "task/repo", githubAuthMode: "token", githubAuthToken: "tok" } as any,
|
||||
globalSettings: {},
|
||||
});
|
||||
|
||||
expect(result).toEqual({ created: false, reason: "issue_already_linked" });
|
||||
expect(createIssueMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns no_repo_configured and records activity", async () => {
|
||||
const recordActivity = vi.fn();
|
||||
const result = await maybeCreateTrackingIssue(buildTask({ githubTracking: { enabled: true } }), {
|
||||
|
||||
@@ -869,6 +869,131 @@ describe("Automation routes", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("create-task automation step attempts tracking issue creation and links metadata", async () => {
|
||||
const createIssueSpy = vi.spyOn(GitHubClient.prototype, "createIssue").mockResolvedValue({
|
||||
owner: "task",
|
||||
repo: "repo",
|
||||
number: 17,
|
||||
htmlUrl: "https://github.com/task/repo/issues/17",
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
});
|
||||
const mockStore = createMockAutomationStore();
|
||||
mockStore.getSchedule.mockResolvedValue({
|
||||
...FAKE_SCHEDULE,
|
||||
command: "",
|
||||
steps: [
|
||||
{
|
||||
id: "step-task",
|
||||
type: "create-task",
|
||||
name: "Create tracked task",
|
||||
taskTitle: "Tracked report",
|
||||
taskDescription: "Create tracked report",
|
||||
taskColumn: "todo",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const linkGithubIssue = vi.fn().mockResolvedValue(undefined);
|
||||
const recordActivity = vi.fn().mockResolvedValue(undefined);
|
||||
const { app, store } = buildApp(mockStore);
|
||||
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
githubTrackingDefaultRepo: "task/repo",
|
||||
githubAuthMode: "token",
|
||||
githubAuthToken: "tok",
|
||||
});
|
||||
(store.getGlobalSettingsStore as ReturnType<typeof vi.fn>).mockReturnValue({ getSettings: vi.fn().mockResolvedValue({}) });
|
||||
(store.createTask as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
id: "FN-9002",
|
||||
title: "Tracked report",
|
||||
description: "Create tracked report",
|
||||
githubTracking: { enabled: true },
|
||||
});
|
||||
(store as unknown as { linkGithubIssue: typeof linkGithubIssue }).linkGithubIssue = linkGithubIssue;
|
||||
(store as unknown as { recordActivity: typeof recordActivity }).recordActivity = recordActivity;
|
||||
|
||||
const res = await REQUEST(app, "POST", "/api/automations/sched-001/run");
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(createIssueSpy).toHaveBeenCalledWith(expect.objectContaining({ owner: "task", repo: "repo" }));
|
||||
expect(linkGithubIssue).toHaveBeenCalledWith("FN-9002", expect.objectContaining({ owner: "task", repo: "repo", number: 17 }));
|
||||
expect(recordActivity).toHaveBeenCalledWith(expect.objectContaining({ metadata: expect.objectContaining({ type: "github-issue-created" }) }));
|
||||
createIssueSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("create-task automation step keeps success when tracking issue creation fails", async () => {
|
||||
const createIssueSpy = vi.spyOn(GitHubClient.prototype, "createIssue").mockRejectedValue(new Error("github down"));
|
||||
const mockStore = createMockAutomationStore();
|
||||
mockStore.getSchedule.mockResolvedValue({
|
||||
...FAKE_SCHEDULE,
|
||||
command: "",
|
||||
steps: [
|
||||
{
|
||||
id: "step-task",
|
||||
type: "create-task",
|
||||
name: "Create tracked task",
|
||||
taskDescription: "Create tracked report",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { app, store } = buildApp(mockStore);
|
||||
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
githubTrackingDefaultRepo: "task/repo",
|
||||
githubAuthMode: "token",
|
||||
githubAuthToken: "tok",
|
||||
});
|
||||
(store.createTask as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
id: "FN-9003",
|
||||
description: "Create tracked report",
|
||||
githubTracking: { enabled: true },
|
||||
});
|
||||
|
||||
const res = await REQUEST(app, "POST", "/api/automations/sched-001/run");
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.result.stepResults[0]).toEqual(expect.objectContaining({ success: true }));
|
||||
expect(createIssueSpy).toHaveBeenCalledTimes(1);
|
||||
createIssueSpy.mockRestore();
|
||||
});
|
||||
it("does not create tracking issue in automation create-task when task already linked", async () => {
|
||||
const createIssueSpy = vi.spyOn(GitHubClient.prototype, "createIssue");
|
||||
const mockStore = createMockAutomationStore();
|
||||
mockStore.getSchedule.mockResolvedValue({
|
||||
...FAKE_SCHEDULE,
|
||||
command: "",
|
||||
steps: [
|
||||
{
|
||||
id: "step-task",
|
||||
type: "create-task",
|
||||
name: "Create tracked task",
|
||||
taskDescription: "Create tracked report",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { app, store } = buildApp(mockStore);
|
||||
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
githubTrackingDefaultRepo: "task/repo",
|
||||
githubAuthMode: "token",
|
||||
githubAuthToken: "tok",
|
||||
});
|
||||
(store.createTask as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
id: "FN-9004",
|
||||
description: "Create tracked report",
|
||||
githubTracking: {
|
||||
enabled: true,
|
||||
issue: { owner: "task", repo: "repo", number: 3, url: "https://github.com/task/repo/issues/3" },
|
||||
},
|
||||
});
|
||||
|
||||
const res = await REQUEST(app, "POST", "/api/automations/sched-001/run");
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.result.stepResults[0]).toEqual(expect.objectContaining({ success: true }));
|
||||
expect(createIssueSpy).not.toHaveBeenCalled();
|
||||
createIssueSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("respects continueOnFailure for create-task failures", async () => {
|
||||
const mockStore = createMockAutomationStore();
|
||||
mockStore.getSchedule.mockResolvedValue({
|
||||
|
||||
@@ -1946,6 +1946,129 @@ describe("POST /subtasks/*", () => {
|
||||
expect(store.updateTask).toHaveBeenCalledWith("FN-102", { dependencies: ["FN-101"] });
|
||||
});
|
||||
|
||||
it("subtask batch creation attempts tracking issue creation and links metadata", async () => {
|
||||
const createIssueSpy = vi.spyOn(GitHubClient.prototype, "createIssue").mockResolvedValue({
|
||||
owner: "task",
|
||||
repo: "repo",
|
||||
number: 55,
|
||||
htmlUrl: "https://github.com/task/repo/issues/55",
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
});
|
||||
|
||||
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
githubTrackingDefaultRepo: "task/repo",
|
||||
githubAuthMode: "token",
|
||||
githubAuthToken: "tok",
|
||||
});
|
||||
(store.createTask as ReturnType<typeof vi.fn>)
|
||||
.mockResolvedValueOnce({ ...FAKE_TASK_DETAIL, id: "FN-103", title: "First", column: "triage", githubTracking: { enabled: true } });
|
||||
|
||||
const start = await REQUEST(
|
||||
buildApp(),
|
||||
"POST",
|
||||
"/api/subtasks/start-streaming",
|
||||
JSON.stringify({ description: "Break this feature into subtasks" }),
|
||||
{ "Content-Type": "application/json" },
|
||||
);
|
||||
|
||||
const createRes = await REQUEST(
|
||||
buildApp(),
|
||||
"POST",
|
||||
"/api/subtasks/create-tasks",
|
||||
JSON.stringify({
|
||||
sessionId: start.body.sessionId,
|
||||
subtasks: [{ tempId: "subtask-1", title: "First", description: "Do first" }],
|
||||
}),
|
||||
{ "Content-Type": "application/json" },
|
||||
);
|
||||
|
||||
expect(createRes.status).toBe(201);
|
||||
expect(createIssueSpy).toHaveBeenCalledWith(expect.objectContaining({ owner: "task", repo: "repo" }));
|
||||
expect(store.linkGithubIssue).toHaveBeenCalledWith("FN-103", expect.objectContaining({ owner: "task", repo: "repo", number: 55 }));
|
||||
expect(store.recordActivity).toHaveBeenCalledWith(expect.objectContaining({ metadata: expect.objectContaining({ type: "github-issue-created" }) }));
|
||||
createIssueSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("subtask batch creation 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",
|
||||
});
|
||||
(store.createTask as ReturnType<typeof vi.fn>)
|
||||
.mockResolvedValueOnce({ ...FAKE_TASK_DETAIL, id: "FN-104", title: "First", column: "triage", githubTracking: { enabled: true } });
|
||||
|
||||
const start = await REQUEST(
|
||||
buildApp(),
|
||||
"POST",
|
||||
"/api/subtasks/start-streaming",
|
||||
JSON.stringify({ description: "Break this feature into subtasks" }),
|
||||
{ "Content-Type": "application/json" },
|
||||
);
|
||||
|
||||
const createRes = await REQUEST(
|
||||
buildApp(),
|
||||
"POST",
|
||||
"/api/subtasks/create-tasks",
|
||||
JSON.stringify({
|
||||
sessionId: start.body.sessionId,
|
||||
subtasks: [{ tempId: "subtask-1", title: "First", description: "Do first" }],
|
||||
}),
|
||||
{ "Content-Type": "application/json" },
|
||||
);
|
||||
|
||||
expect(createRes.status).toBe(201);
|
||||
expect(createRes.body.tasks).toHaveLength(1);
|
||||
expect(createIssueSpy).toHaveBeenCalledTimes(1);
|
||||
createIssueSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("subtask batch creation does not recreate tracking issue when task is already linked", async () => {
|
||||
const createIssueSpy = vi.spyOn(GitHubClient.prototype, "createIssue");
|
||||
|
||||
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
githubTrackingDefaultRepo: "task/repo",
|
||||
githubAuthMode: "token",
|
||||
githubAuthToken: "tok",
|
||||
});
|
||||
(store.createTask as ReturnType<typeof vi.fn>)
|
||||
.mockResolvedValueOnce({
|
||||
...FAKE_TASK_DETAIL,
|
||||
id: "FN-105",
|
||||
title: "First",
|
||||
column: "triage",
|
||||
githubTracking: {
|
||||
enabled: true,
|
||||
issue: { owner: "task", repo: "repo", number: 9, url: "https://github.com/task/repo/issues/9" },
|
||||
},
|
||||
});
|
||||
|
||||
const start = await REQUEST(
|
||||
buildApp(),
|
||||
"POST",
|
||||
"/api/subtasks/start-streaming",
|
||||
JSON.stringify({ description: "Break this feature into subtasks" }),
|
||||
{ "Content-Type": "application/json" },
|
||||
);
|
||||
|
||||
const createRes = await REQUEST(
|
||||
buildApp(),
|
||||
"POST",
|
||||
"/api/subtasks/create-tasks",
|
||||
JSON.stringify({
|
||||
sessionId: start.body.sessionId,
|
||||
subtasks: [{ tempId: "subtask-1", title: "First", description: "Do first" }],
|
||||
}),
|
||||
{ "Content-Type": "application/json" },
|
||||
);
|
||||
|
||||
expect(createRes.status).toBe(201);
|
||||
expect(createIssueSpy).not.toHaveBeenCalled();
|
||||
createIssueSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("applies explicit branch selection to created subtasks", async () => {
|
||||
(store.createTask as ReturnType<typeof vi.fn>)
|
||||
.mockResolvedValueOnce({ ...FAKE_TASK_DETAIL, id: "FN-201", title: "First", column: "triage" });
|
||||
|
||||
Reference in New Issue
Block a user