Files
fusion/packages/dashboard/app/components/__tests__/TaskDetailModal.gitlab-tracking.test.tsx
gsxdsm b8e126eeaf FN-7425: add GitLab tracking metadata to tasks
Persist GitLab tracking metadata and surface it across task APIs and dashboard views.

- add task-store schema, migration, and update helpers for linked GitLab items and stale state
- expose validated GitLab tracking updates through task workflow routes and legacy task payloads
- render GitLab badges, detail-panel metadata, open/unlink actions, styling, i18n strings, and docs
- cover persistence, route validation, task card badges, and task detail interactions with tests
- add a patch changeset for the published Fusion package

Files changed:
 .changeset/fn-7425-gitlab-tracking.md              |   7 ++
 docs/cli-reference.md                              |   2 +-
 docs/dashboard-guide.md                            |   3 +
 packages/core/src/__tests__/db-migrate.test.ts     |  29 +++++
 .../src/__tests__/store-gitlab-tracking.test.ts    | 138 +++++++++++++++++++++
 packages/core/src/db.ts                            |  10 +-
 packages/core/src/gitlab-tracking.ts               |  10 ++
 packages/core/src/index.ts                         |   3 +-
 packages/core/src/store.ts                         | 135 +++++++++++++++++++-
 packages/core/src/types.ts                         |  49 ++++++++
 packages/dashboard/app/api/legacy.ts               |   3 +
 packages/dashboard/app/components/GitLabBadge.tsx  |  33 +++++
 packages/dashboard/app/components/TaskCard.tsx     |   7 +-
 .../dashboard/app/components/TaskCardBadge.tsx     |  15 ++-
 .../dashboard/app/components/TaskDetailModal.css   |  47 +++++--
 .../dashboard/app/components/TaskDetailModal.tsx   | 134 +++++++++++++++++++-
 .../app/components/__tests__/TaskCard.test.tsx     |  49 ++++++++
 .../TaskDetailModal.gitlab-tracking.test.tsx       | 121 ++++++++++++++++++
 .../__tests__/TaskDetailModal.test-helpers.ts      |   1 +
 packages/dashboard/app/styles.css                  |   9 ++
 .../src/__tests__/routes-tasks-ops.test.ts         | 136 ++++++++++++++++++++
 packages/dashboard/src/gitlab.ts                   |  24 +++-
 packages/dashboard/src/routes/register-gitlab.ts   |   1 +
 .../src/routes/register-task-workflow-routes.ts    | 106 +++++++++++++++-
 packages/i18n/locales/en/app.json                  |  31 +++++
 packages/i18n/src/resources.d.ts                   |  31 +++++
 26 files changed, 1106 insertions(+), 28 deletions(-)

Fusion-Task-Id: FN-7425

Fusion-Task-Lineage: 9b5e6005-7284-402e-995c-553be2275eff

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-02 11:38:34 -07:00

122 lines
5.0 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { makeTask, mockConfirm, noop, noopDelete, noopMerge, noopMove, noopOpenDetail, setupTaskDetailModalHooks } from "./TaskDetailModal.test-helpers";
import { TaskDetailModal } from "../TaskDetailModal";
setupTaskDetailModalHooks();
const projectIssue = {
kind: "project_issue" as const,
url: "https://gitlab.com/acme/app/-/issues/42",
instanceUrl: "https://gitlab.com",
host: "gitlab.com",
iid: 42,
projectPath: "acme/app",
title: "Project issue",
state: "opened",
createdAt: "2026-07-02T00:00:00.000Z",
lastSyncedAt: "2026-07-02T00:01:00.000Z",
};
function renderModal(task = makeTask({ column: "todo", gitlabTracking: { item: projectIssue } }), onTaskUpdated = vi.fn()) {
return render(
<TaskDetailModal
initialTab="definition"
task={task}
onClose={noop}
onMoveTask={noopMove}
onDeleteTask={noopDelete}
onMergeTask={noopMerge}
onOpenDetail={noopOpenDetail}
onTaskUpdated={onTaskUpdated}
addToast={noop}
/>,
);
}
describe("TaskDetailModal GitLab tracking", () => {
it("renders linked project issue metadata with provider-correct labels and actions", async () => {
const user = userEvent.setup();
renderModal();
expect(screen.getByText("GitLab tracking")).toBeInTheDocument();
expect(screen.getByText("Linked")).toBeInTheDocument();
expect(screen.getByRole("link", { name: "Project issue #42" })).toHaveAttribute("href", projectIssue.url);
await user.click(screen.getByRole("button", { name: "Expand GitLab tracking details" }));
expect(screen.getByText("Kind")).toBeInTheDocument();
expect(screen.getByText("gitlab.com")).toBeInTheDocument();
expect(screen.getByRole("link", { name: "Open linked GitLab item" })).toHaveAttribute("href", projectIssue.url);
expect(screen.getByRole("button", { name: "Unlink GitLab item" })).toBeInTheDocument();
expect(screen.queryByText("GitHub tracking")).not.toBeInTheDocument();
});
it("renders group issues, merge requests, stale state, and GitHub coexistence", async () => {
const user = userEvent.setup();
const staleGroupIssue = {
kind: "group_issue" as const,
url: "https://git.example.test/groups/platform/-/issues/9",
instanceUrl: "https://git.example.test",
host: "git.example.test",
iid: 9,
groupPath: "platform",
title: "Group issue",
state: "opened",
createdAt: "2026-07-02T00:00:00.000Z",
staleAt: "2026-07-02T01:00:00.000Z",
staleReason: "GitLab sync failed",
};
const task = makeTask({
column: "todo",
gitlabTracking: { item: staleGroupIssue },
githubTracking: { enabled: true, repoOverride: "runfusion/fusion" },
});
const { rerender } = renderModal(task);
expect(screen.getByText("Stale")).toBeInTheDocument();
expect(screen.getByRole("link", { name: "Group issue #9" })).toBeInTheDocument();
expect(screen.getByText("GitHub tracking")).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Expand GitLab tracking details" }));
expect(screen.getByText("GitLab sync failed")).toBeInTheDocument();
rerender(
<TaskDetailModal
initialTab="definition"
task={makeTask({ column: "todo", gitlabTracking: { item: { ...projectIssue, kind: "merge_request", iid: 5, url: "https://gitlab.com/acme/app/-/merge_requests/5", title: "MR" } } })}
onClose={noop}
onMoveTask={noopMove}
onDeleteTask={noopDelete}
onMergeTask={noopMerge}
onOpenDetail={noopOpenDetail}
addToast={noop}
/>,
);
expect(screen.getByRole("link", { name: "Merge request !5" })).toBeInTheDocument();
});
it("unlinks after confirmation and does not render empty GitLab shells", async () => {
const user = userEvent.setup();
const onTaskUpdated = vi.fn();
const { updateTask } = await import("../../api");
vi.mocked(updateTask).mockResolvedValueOnce(makeTask({ column: "todo", gitlabTracking: { unlinkedAt: "2026-07-02T00:00:00.000Z" } }) as any);
mockConfirm.mockResolvedValueOnce(false).mockResolvedValueOnce(true);
renderModal(undefined, onTaskUpdated);
await user.click(screen.getByRole("button", { name: "Expand GitLab tracking details" }));
await user.click(screen.getByRole("button", { name: "Unlink GitLab item" }));
expect(updateTask).not.toHaveBeenCalled();
await user.click(screen.getByRole("button", { name: "Unlink GitLab item" }));
await waitFor(() => expect(updateTask).toHaveBeenCalledWith("FN-099", { gitlabTracking: { item: null } }, undefined));
expect(onTaskUpdated).toHaveBeenCalled();
});
it("omits GitLab tracking section when metadata is empty", () => {
renderModal(makeTask({ column: "todo", gitlabTracking: undefined }));
expect(screen.queryByTestId("detail-gitlab-tracking-section")).not.toBeInTheDocument();
expect(screen.queryByLabelText(/GitLab/i)).not.toBeInTheDocument();
});
});