Make project initialization and registration produce Git-ready projects consistently.\n\n- Centralize repository readiness and initialization behavior.\n- Update CLI onboarding and dashboard registration flows to use the Git-ready setup.\n- Add regression coverage and document the new project preparation behavior.\n\nFiles changed:\n .changeset/fn-038-git-ready-projects.md | 7 +\n docs/cli-reference.md | 11 +-\n docs/getting-started.md | 17 +-\n docs/workspaces.md | 4 +-\n packages/cli/src/commands/__tests__/init.test.ts | 39 ++--\n packages/cli/src/commands/init.ts | 115 ++--------\n packages/core/src/__tests__/git-repository.test.ts | 125 +++++++++-\n packages/core/src/central/central-core.ts | 57 +++--\n packages/core/src/git/git-repository.ts | 254 +++++++++++++++++----\n packages/dashboard/app/api/projects/projects.ts | 2 -\n packages/dashboard/app/components/SetupWizardModal.tsx | 46 +---\n packages/dashboard/app/components/__tests__/SetupWizardModal.git-missing.test.tsx | 74 +++---\n packages/dashboard/src/routes/__tests__/register-project-git-readiness.test.ts | 165 +++++++++++++\n packages/dashboard/src/routes/register-project-routes.ts | 11 -\n 14 files changed, 647 insertions(+), 280 deletions(-) Fusion-Task-Id: FN-038 Fusion-Task-Lineage: 83634184-569b-4190-a88f-4a09eb832e9d Co-authored-by: Fusion <noreply@runfusion.ai>
101 lines
4.4 KiB
TypeScript
101 lines
4.4 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
import { SetupWizardModal } from "../SetupWizardModal";
|
|
|
|
/*
|
|
FNXC:ProjectSetup 2026-08-19-12:44:
|
|
Git-unavailable setup is fail-closed across every repository mode and viewport: the only
|
|
available actions are install/download or cancel/retry, and registration never receives a
|
|
bypass field for a project that cannot create task worktrees.
|
|
*/
|
|
|
|
const mockRegisterProject = vi.fn();
|
|
const mockFetchAuthStatus = vi.fn();
|
|
const mockConfirmWithChoice = vi.fn();
|
|
|
|
vi.mock("../../api", async (importOriginal) => ({
|
|
...(await importOriginal<Record<string, unknown>>()),
|
|
registerProject: (...args: unknown[]) => mockRegisterProject(...args),
|
|
fetchAuthStatus: (...args: unknown[]) => mockFetchAuthStatus(...args),
|
|
detectWorkspace: vi.fn().mockResolvedValue({ repos: [], isWorkspace: false }),
|
|
createAgent: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("../../hooks/useConfirm", () => ({
|
|
useConfirm: () => ({
|
|
confirm: vi.fn(),
|
|
confirmWithChoice: (...args: unknown[]) => mockConfirmWithChoice(...args),
|
|
}),
|
|
}));
|
|
|
|
vi.mock("../../hooks/useNodes", () => ({
|
|
useNodes: () => ({ nodes: [], loading: false }),
|
|
}));
|
|
|
|
function chooseMode(mode: "existing" | "init" | "clone"): void {
|
|
const labels = {
|
|
existing: /use existing directory/i,
|
|
init: /initialize new repository/i,
|
|
clone: /clone git repository/i,
|
|
};
|
|
fireEvent.click(screen.getByRole("radio", { name: labels[mode] }));
|
|
}
|
|
|
|
async function fillAndSubmit(mode: "existing" | "init" | "clone"): Promise<void> {
|
|
chooseMode(mode);
|
|
const pathPlaceholder = mode === "clone"
|
|
? "/path/for/new-clone"
|
|
: mode === "init" ? "/path/to/new-project" : "/path/to/your/project";
|
|
fireEvent.change(screen.getByPlaceholderText(pathPlaceholder), {
|
|
target: { value: "/tmp/demo-project" },
|
|
});
|
|
const nameInput = document.getElementById("project-name") as HTMLInputElement;
|
|
fireEvent.change(nameInput, { target: { value: "Demo" } });
|
|
if (mode === "clone") {
|
|
fireEvent.change(screen.getByPlaceholderText("https://github.com/owner/repo.git"), {
|
|
target: { value: "https://github.com/example/demo.git" },
|
|
});
|
|
}
|
|
fireEvent.click(screen.getByRole("button", { name: /register project|initialize and register|clone and register/i }));
|
|
}
|
|
|
|
describe.each(["desktop", "mobile"] as const)("SetupWizardModal git-missing warning (%s)", (viewport) => {
|
|
beforeEach(() => {
|
|
Object.defineProperty(window, "innerWidth", { configurable: true, value: viewport === "mobile" ? 390 : 1280 });
|
|
mockRegisterProject.mockReset().mockResolvedValue({ id: "proj_1", name: "Demo", path: "/tmp/demo-project" });
|
|
mockFetchAuthStatus.mockReset();
|
|
mockConfirmWithChoice.mockReset();
|
|
});
|
|
|
|
it.each(["existing", "init", "clone"] as const)("blocks %s registration when Git is unavailable", async (mode) => {
|
|
mockFetchAuthStatus.mockResolvedValue({ providers: [], gitCli: { available: false, installUrl: "https://git-scm.com/downloads" } });
|
|
mockConfirmWithChoice.mockResolvedValue("primary");
|
|
const windowOpen = vi.spyOn(window, "open").mockReturnValue(null);
|
|
|
|
render(<SetupWizardModal onProjectRegistered={vi.fn()} includeAgentStep={false} />);
|
|
await fillAndSubmit(mode);
|
|
|
|
await waitFor(() => expect(mockConfirmWithChoice).toHaveBeenCalledTimes(1));
|
|
expect(mockRegisterProject).not.toHaveBeenCalled();
|
|
expect(windowOpen).toHaveBeenCalledWith("https://git-scm.com/downloads", "_blank");
|
|
expect(mockConfirmWithChoice).toHaveBeenCalledWith(expect.objectContaining({
|
|
title: expect.stringMatching(/git is not installed/i),
|
|
confirmLabel: expect.stringMatching(/open git downloads/i),
|
|
cancelLabel: expect.stringMatching(/cancel/i),
|
|
}));
|
|
expect(mockConfirmWithChoice.mock.calls[0]?.[0]).not.toHaveProperty("tertiaryLabel");
|
|
windowOpen.mockRestore();
|
|
});
|
|
|
|
it("registers with Git available without the obsolete bypass payload", async () => {
|
|
mockFetchAuthStatus.mockResolvedValue({ providers: [], gitCli: { available: true, version: "2.50.0", installUrl: "https://git-scm.com/downloads" } });
|
|
|
|
render(<SetupWizardModal onProjectRegistered={vi.fn()} includeAgentStep={false} />);
|
|
await fillAndSubmit("existing");
|
|
|
|
await waitFor(() => expect(mockRegisterProject).toHaveBeenCalledTimes(1));
|
|
expect(mockRegisterProject.mock.calls[0]?.[0]).not.toHaveProperty("skipGitInit");
|
|
expect(mockConfirmWithChoice).not.toHaveBeenCalled();
|
|
});
|
|
});
|