Files
fusion/packages/cli/src/__tests__/task-command-gitlab-import.test.ts
gsxdsm 478f226a54 test: green full-suite CI after main drift (#2229)
## Summary
Restores green **Full Suite (non-blocking)** runs on `main`. Recent main
merges left i18n key parity, schema baseline bookkeeping (0011→0012),
heartbeat tool inventory (FN-8058 `fn_task_logs_read`), and merger
whitespace-classification mocks (execFile `git diff -p -w :2: :3:`) out
of date, so all four test shards failed.

## Root causes observed on main
- **Shard 4 / `@fusion/i18n`**: missing `skipConfirmationDialogs*` +
`reviewBudgetExhausted` in non-en locales; orphan
`awaitingApprovalPlanReviewReplanCap`
- **Shard 3 / `@fusion/core`**: `SCHEMA_BASELINE_VERSION` advanced to
`0012` while tests still equated it with
`OWNER_PROJECT_ID_SPLIT_VERSION` (`0011`) and omitted `0012` from
applied-migration lists
- **Shards 1–2 / `@fusion/engine`**: tool count/snapshot drift for
`fn_task_logs_read`; merger tests still mocked `git diff-tree` for
trivial classification after the execFile `:2:`/`:3:` cutover; mock
provider `updateTask` arity drift

## Changes
- Locale catalogs: add missing keys, drop orphan key
- Schema applier tests: immutable 0011 identity + baseline 0012 lists
- Heartbeat + gating snapshots: include `fn_task_logs_read`
- Merger unit mocks: recognize `git diff -p -w :2:path :3:path`
- Mock provider: accept optional third `updateTask` arg

## Test plan
- [x] `pnpm --filter @fusion/i18n exec vitest run` — 23/23
- [x] `pnpm --filter @fusion/core exec vitest run
src/__tests__/postgres/schema-applier.test.ts` (immutable + automation
upgrade) — pass
- [x] `pnpm --filter @fusion/core exec vitest run` project-identity +
satellite-fusiondir — pass
- [x] Engine suites from failed CI shards (file-scoped,
hermes/openclaw/paperclip/grok, reliability post-finalize/mission,
heartbeat, gating, merger recovery/prompt, mock-provider, etc.) — pass
- [ ] Full Suite workflow green on merge to main

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
  - Improved project data isolation across backend operations.
- Added safer optional toast handling when UI components render outside
the full application shell.
  - Added support for reading task logs during agent heartbeat sessions.

- **Bug Fixes**
- Prevented runtime probes from hanging and avoided scanning large
binary files.
  - Improved path handling for workspaces with missing descendants.
- Corrected task retry state resets and GitHub import/issue-close
behavior.

- **Style**
  - Improved chat, terminal, and settings spacing.
  - Added clearer accessibility labeling for the auto-merge control.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-16 16:01:29 -07:00

129 lines
7.0 KiB
TypeScript

import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
const mocks = vi.hoisted(() => ({
createTask: vi.fn(),
logEntry: vi.fn(),
listTasks: vi.fn(),
getSettings: vi.fn(),
getGlobalSettings: vi.fn(),
}));
vi.mock("@fusion/core", async (importOriginal) => {
const actual = await importOriginal<typeof import("@fusion/core")>();
return {
...actual,
createTaskStoreForBackend: vi.fn(async () => null),
TaskStore: vi.fn(function TaskStore() {
return {
init: vi.fn(),
getSettings: mocks.getSettings,
getGlobalSettingsStore: () => ({ getSettings: mocks.getGlobalSettings }),
listTasks: mocks.listTasks,
createTask: mocks.createTask,
logEntry: mocks.logEntry,
};
}),
};
});
vi.mock("@fusion/dashboard", () => {
class GitLabClient {
auth: any;
constructor(auth: any) { this.auth = auth; }
async listProjectIssues(project: string, options: any = {}) {
const response = await fetch(`https://gitlab.example.com/api/v4/projects/${encodeURIComponent(project)}/issues?per_page=${options.limit ?? 30}&page=1&state=opened`);
return response.json();
}
async listGroupIssues(group: string, options: any = {}) {
const response = await fetch(`https://gitlab.example.com/api/v4/groups/${encodeURIComponent(group)}/issues?per_page=${options.limit ?? 30}&page=1&state=opened`);
return response.json();
}
async listMergeRequests(project: string, options: any = {}) {
const response = await fetch(`https://gitlab.example.com/api/v4/projects/${encodeURIComponent(project)}/merge_requests?per_page=${options.limit ?? 30}&page=1&state=opened`);
return response.json();
}
}
return {
registerGithubTrackingHook: vi.fn(),
resolveGitlabAuth: vi.fn(() => ({ ok: true, auth: { apiBaseUrl: "https://gitlab.example.com/api/v4", webBaseUrl: "https://gitlab.example.com", token: "token", tokenType: "personal", headerName: "PRIVATE-TOKEN" } })),
GitLabClient,
buildGitLabTaskDescription: (item: any) => `${item.description?.trim() || "(no description)"}\n\nSource: ${item.webUrl ?? item.web_url}`,
buildGitLabTaskProvenance: ({ resourceType, item }: any) => ({ sourceIssue: { provider: "gitlab", repository: String(item.projectPath ?? item.project_id ?? "unknown"), externalIssueId: resourceType === "merge_request" ? `gitlab:mr:${item.project_id}:${item.id}` : String(item.id), issueNumber: item.iid, url: item.webUrl ?? item.web_url }, gitlabTracking: { item: { kind: resourceType, iid: item.iid, url: item.webUrl ?? item.web_url, host: "gitlab.example.com", instanceUrl: "https://gitlab.example.com", projectId: item.project_id, projectPath: item.projectPath, title: item.title, state: item.state } }, sourceMetadata: { provider: "gitlab", resourceType, iid: item.iid, webUrl: item.webUrl ?? item.web_url } }),
isGitLabAlreadyImported: (task: any, provenance: any) => task.description?.includes(provenance.sourceIssue.url) || task.sourceIssue?.externalIssueId === provenance.sourceIssue.externalIssueId,
};
});
vi.mock("../project-context.js", () => ({
// FNXC:CliTests 2026-07-16-08:56: FN-8102 keeps GitLab import on the
// controllable command-context seam, preventing fallback startup from
// masking provenance assertions behind an embedded PostgreSQL boot.
resolveProject: vi.fn(async () => ({
store: {
getSettings: mocks.getSettings,
getGlobalSettingsStore: () => ({ getSettings: mocks.getGlobalSettings }),
listTasks: mocks.listTasks,
createTask: mocks.createTask,
logEntry: mocks.logEntry,
},
projectId: "test-project",
projectPath: process.cwd(),
projectName: "test-project",
isRegistered: false,
})),
closeProjectStore: vi.fn().mockResolvedValue(undefined),
}));
import { runTaskImportFromGitLab } from "../commands/task.js";
/*
FNXC:GitLabCLI 2026-07-02-00:00:
GitLab CLI import support is HTTP API based through configured tokens and must not require `glab`, a downloaded binary, or real GitLab network calls. These tests mock the GitLab client seam and assert source/tracking metadata is created locally.
*/
describe("fn task import-gitlab", () => {
let fetchSpy: ReturnType<typeof vi.fn>;
beforeEach(() => {
vi.clearAllMocks();
vi.spyOn(console, "log").mockImplementation(() => {});
mocks.getSettings.mockResolvedValue({ gitlabAuthToken: "token", gitlabInstanceUrl: "https://gitlab.example.com" });
mocks.getGlobalSettings.mockResolvedValue({});
mocks.listTasks.mockResolvedValue([]);
mocks.createTask.mockImplementation(async (input) => ({ id: "FN-001", ...input }));
fetchSpy = vi.fn().mockImplementation(() => Promise.resolve(new Response(JSON.stringify([{ id: 1, iid: 2, project_id: 3, title: "GitLab issue", description: null, webUrl: "https://gitlab.example.com/g/p/-/issues/2", web_url: "https://gitlab.example.com/g/p/-/issues/2", state: "opened", labels: [] }]), { status: 200 })));
vi.stubGlobal("fetch", fetchSpy);
});
afterEach(() => {
vi.restoreAllMocks();
vi.unstubAllGlobals();
});
it("imports GitLab project issues with gitlab provenance", async () => {
await runTaskImportFromGitLab("g/p", { resource: "project-issues", limit: 1 });
expect(fetchSpy.mock.calls[0][0]).toContain("/projects/g%2Fp/issues?");
expect(mocks.createTask).toHaveBeenCalledWith(expect.objectContaining({
sourceIssue: expect.objectContaining({ provider: "gitlab", issueNumber: 2 }),
gitlabTracking: expect.objectContaining({ item: expect.objectContaining({ kind: "project_issue", iid: 2 }) }),
source: expect.objectContaining({ sourceType: "gitlab_import", sourceMetadata: expect.objectContaining({ resourceType: "project_issue" }) }),
}));
});
it("skips duplicates by GitLab source URL", async () => {
mocks.listTasks.mockResolvedValue([{ id: "FN-OLD", description: "Source: https://gitlab.example.com/g/p/-/issues/2" }]);
await runTaskImportFromGitLab("g/p", { resource: "project-issues", limit: 1 });
expect(mocks.createTask).not.toHaveBeenCalled();
});
it("uses group issue and merge request endpoints with filters and review-task metadata", async () => {
await runTaskImportFromGitLab("g", { resource: "group-issues", limit: 1, labels: ["ops"] });
expect(fetchSpy.mock.calls.at(-1)?.[0]).toContain("/groups/g/issues?");
fetchSpy.mockResolvedValueOnce(new Response(JSON.stringify([{ id: 9, iid: 2, project_id: 3, title: "GitLab MR", description: "MR", webUrl: "https://gitlab.example.com/g/p/-/merge_requests/2", web_url: "https://gitlab.example.com/g/p/-/merge_requests/2", state: "opened", labels: [], source_branch: "feat", target_branch: "main" }]), { status: 200 }));
await runTaskImportFromGitLab("g/p", { resource: "merge-requests", limit: 1, labels: ["review"] });
expect(fetchSpy.mock.calls.at(-1)?.[0]).toContain("/projects/g%2Fp/merge_requests?");
expect(mocks.createTask.mock.calls.at(-1)?.[0]).toEqual(expect.objectContaining({
title: expect.stringMatching(/^Review MR !2:/),
gitlabTracking: expect.objectContaining({ item: expect.objectContaining({ kind: "merge_request" }) }),
}));
});
});