Files
fusion/packages/cli/src/__tests__/task-command-github-import-tracking.test.ts
gsxdsm a9e2baa9f8 FN-7367: link imported GitHub issues to tracking
Imported GitHub issues can now adopt their source issue as the tracking issue without changing ordinary task defaults.

- Add the project-scoped githubLinkImportedIssuesToTracking setting with docs, Settings UI, and defaults.
- Honor the import-only setting across dashboard, CLI, and extension GitHub issue import paths.
- Cover saved settings and import tracking behavior with CLI, dashboard, and parity tests.
- Add a minor changeset for the published CLI package.

Files changed:
 .changeset/fn-7367-github-import-tracking.md       |  7 +++
 docs/settings-reference.md                         |  3 +-
 packages/cli/src/__tests__/extension.test.ts       | 73 ++++++++++++++++++++++
 .../task-command-github-import-tracking.test.ts    | 16 +++++
 packages/cli/src/commands/__tests__/task.test.ts   | 28 +++++++++
 packages/cli/src/commands/task.ts                  | 45 +++++++------
 packages/cli/src/extension.ts                      | 45 +++++++------
 .../core/src/__tests__/settings-parity.test.ts     |  4 ++
 packages/core/src/settings-schema.ts               |  1 +
 packages/core/src/types.ts                         |  6 ++
 .../app/__tests__/settings-save-split.test.ts      | 19 ++++++
 .../dashboard/app/components/SettingsModal.tsx     |  6 ++
 .../__tests__/SettingsModal.general.test.tsx       | 59 +++++++++++++++++
 .../__tests__/SettingsModal.test-harness.tsx       |  1 +
 .../settings/sections/GeneralSection.tsx           |  9 +++
 .../dashboard/src/__tests__/routes-github.test.ts  | 45 +++++++++++++
 .../dashboard/src/routes/register-git-github.ts    |  7 +++
 17 files changed, 325 insertions(+), 49 deletions(-)

Fusion-Task-Id: FN-7367
Fusion-Task-Lineage: 43671cdf-7e0d-4b7a-9ba2-54c9a367dfec
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-01 09:50:57 -07:00

136 lines
4.5 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
const taskStoreCtorMock = vi.hoisted(() => vi.fn());
const runGhJsonAsyncMock = vi.hoisted(() => vi.fn());
const resolveProjectMock = vi.hoisted(() => vi.fn());
vi.mock("@fusion/core", async (importActual) => {
const actual = await importActual<typeof import("@fusion/core")>();
return {
...actual,
TaskStore: taskStoreCtorMock,
};
});
vi.mock("@fusion/core/gh-cli", () => ({
isGhAvailable: vi.fn(() => true),
isGhAuthenticated: vi.fn(() => true),
runGhJsonAsync: runGhJsonAsyncMock,
getGhErrorMessage: vi.fn((error: unknown) => (error instanceof Error ? error.message : String(error))),
}));
vi.mock("../project-context.js", () => ({
resolveProject: resolveProjectMock,
}));
vi.mock("@fusion/dashboard", () => ({
registerGithubTrackingHook: vi.fn(),
}));
vi.mock("@fusion/engine", () => ({
createFnAgent: vi.fn(),
runAiMerge: vi.fn(),
landWorkspaceTask: vi.fn(),
}));
vi.mock("@fusion/dashboard/planning", () => ({
createSession: vi.fn(),
submitResponse: vi.fn(),
RateLimitError: class RateLimitError extends Error {},
SessionNotFoundError: class SessionNotFoundError extends Error {},
InvalidSessionStateError: class InvalidSessionStateError extends Error {},
}));
import { runTaskImportFromGitHub } from "../commands/task.js";
describe("fn task import GitHub tracking defaults", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.spyOn(console, "log").mockImplementation(() => {});
vi.spyOn(console, "error").mockImplementation(() => {});
resolveProjectMock.mockRejectedValue(new Error("No project context"));
runGhJsonAsyncMock.mockResolvedValue([
{
number: 1,
title: "Imported Issue",
body: "Imported issue body",
html_url: "https://github.com/owner/repo/issues/1",
labels: [],
},
]);
});
function mockStore(options: { projectSettings?: Record<string, unknown>; globalSettings?: Record<string, unknown> } = {}) {
const createTask = vi.fn().mockImplementation((input) => Promise.resolve({
id: "FN-001",
title: input.title,
description: input.description,
column: "triage",
}));
taskStoreCtorMock.mockImplementation(function () {
return {
init: vi.fn().mockResolvedValue(undefined),
listTasks: vi.fn().mockResolvedValue([]),
createTask,
getSettings: vi.fn().mockResolvedValue(options.projectSettings ?? {}),
getGlobalSettingsStore: vi.fn().mockReturnValue({
getSettings: vi.fn().mockResolvedValue(options.globalSettings ?? {}),
}),
};
});
return { createTask };
}
it("sets githubTracking.enabled for fn task import when project tracking defaults are on", async () => {
const { createTask } = mockStore({ projectSettings: { githubTrackingEnabledByDefault: true } });
await runTaskImportFromGitHub("owner/repo", { limit: 1 });
expect(createTask).toHaveBeenCalledWith(expect.objectContaining({
githubTracking: { enabled: true },
sourceIssue: expect.objectContaining({
provider: "github",
repository: "owner/repo",
issueNumber: 1,
}),
}));
});
it("sets githubTracking.enabled for fn task import when global tracking defaults are on", async () => {
const { createTask } = mockStore({ globalSettings: { githubTrackingDefaultEnabledForNewTasks: true } });
await runTaskImportFromGitHub("owner/repo", { limit: 1 });
expect(createTask).toHaveBeenCalledWith(expect.objectContaining({
githubTracking: { enabled: true },
sourceIssue: expect.objectContaining({ issueNumber: 1 }),
}));
});
it("sets githubTracking.enabled for fn task import when import linking is on and new-task defaults are off", async () => {
const { createTask } = mockStore({
projectSettings: {
githubTrackingEnabledByDefault: false,
githubLinkImportedIssuesToTracking: true,
},
});
await runTaskImportFromGitHub("owner/repo", { limit: 1 });
expect(createTask).toHaveBeenCalledWith(expect.objectContaining({
githubTracking: { enabled: true },
sourceIssue: expect.objectContaining({ provider: "github", repository: "owner/repo", issueNumber: 1 }),
}));
});
it("does not force githubTracking for fn task import when tracking defaults are off", async () => {
const { createTask } = mockStore();
await runTaskImportFromGitHub("owner/repo", { limit: 1 });
expect(createTask).toHaveBeenCalledWith(expect.not.objectContaining({
githubTracking: expect.anything(),
}));
});
});