49f87c4da3
Whole tree from parallel sessions, plus the version bump: - Clone: a queue with live progress (Add workspace and extra folders), clones run under nice/ionice so the machine stays responsive. - Settings → AI tools → Session defaults: a new session's default permission mode / bypass per account. - Chat lists the background subagents that have not reported back yet. - A file pasted or dropped into a claude pane goes in as an @ mention. - Changes panel: say why amend/undo is refused on a pushed commit. - openapi.yaml regenerated (Artifacts, Host, resumeById).
121 lines
5.3 KiB
TypeScript
121 lines
5.3 KiB
TypeScript
/**
|
|
* @file Tests for CloneSourcesForm: every URL in the box is cloned in turn as
|
|
* an extra folder, a host that wants a login opens the login row, and Retry
|
|
* re-runs only the failed URLs with that login. An SSH link while the
|
|
* account has no key is never sent — the user is pointed at Settings → Git —
|
|
* and an HTTPS host with no saved login is flagged before Clone. The
|
|
* partial-clone box adds `partial: true` to every clone.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
import { render, screen, waitFor } from "@testing-library/react";
|
|
import userEvent from "@testing-library/user-event";
|
|
import "../../../i18n";
|
|
|
|
vi.mock("../../../lib/api", () => ({
|
|
api: { lanes: { cloneFolder: vi.fn() }, gitIdentity: { get: vi.fn() } },
|
|
}));
|
|
|
|
import { api, type GitIdentity, type LaneSourcesView } from "../../../lib/api";
|
|
import { CloneSourcesForm, cloneTarget } from "../CloneSourcesForm";
|
|
|
|
const view = { sources: [] } as unknown as LaneSourcesView & { path: string };
|
|
|
|
describe("CloneSourcesForm", () => {
|
|
beforeEach(() => {
|
|
vi.mocked(api.lanes.cloneFolder).mockReset();
|
|
// Unknown identity: nothing is checked up front.
|
|
vi.mocked(api.gitIdentity.get).mockReset().mockRejectedValue(new Error("old server"));
|
|
});
|
|
|
|
it("clones each URL, asks for a login where needed, and retries only that one", async () => {
|
|
const user = userEvent.setup();
|
|
const onAdded = vi.fn();
|
|
vi.mocked(api.lanes.cloneFolder).mockImplementation(async (_id, body) => {
|
|
if (body?.url.includes("private") && !body.credentials)
|
|
throw Object.assign(new Error("login"), {
|
|
code: "ECREDENTIALS",
|
|
auth: { protocol: "https", host: "private.test" },
|
|
});
|
|
return view;
|
|
});
|
|
render(<CloneSourcesForm laneId={3} parent="/w" onAdded={onAdded} />);
|
|
|
|
await user.type(
|
|
screen.getByTestId("workspace-clone-input"),
|
|
"https://h/a.git\nhttps://private.test/b.git\nhttps://h/a.git"
|
|
);
|
|
await user.click(screen.getByTestId("workspace-clone-run"));
|
|
|
|
await waitFor(() => expect(api.lanes.cloneFolder).toHaveBeenCalledTimes(2));
|
|
expect(onAdded).toHaveBeenCalledTimes(1);
|
|
expect(await screen.findByText(/private\.test needs a login/)).toBeInTheDocument();
|
|
// Only what is left stays in the box.
|
|
expect(screen.getByTestId("workspace-clone-input")).toHaveValue("https://private.test/b.git");
|
|
|
|
await user.type(screen.getByLabelText("Username"), "dev");
|
|
await user.type(screen.getByLabelText("Password or access token"), "tok");
|
|
await user.click(screen.getByRole("button", { name: "Retry the failed ones" }));
|
|
|
|
await waitFor(() => expect(onAdded).toHaveBeenCalledTimes(2));
|
|
expect(api.lanes.cloneFolder).toHaveBeenLastCalledWith(3, {
|
|
progressToken: expect.any(String),
|
|
url: "https://private.test/b.git",
|
|
credentials: { username: "dev", password: "tok", remember: true },
|
|
});
|
|
expect(screen.getByTestId("workspace-clone-input")).toHaveValue("");
|
|
});
|
|
|
|
it("asks for partial clones when the box is ticked", async () => {
|
|
const user = userEvent.setup();
|
|
vi.mocked(api.lanes.cloneFolder).mockResolvedValue(view);
|
|
render(<CloneSourcesForm laneId={3} parent="/w" onAdded={vi.fn()} />);
|
|
await user.type(screen.getByTestId("workspace-clone-input"), "https://h/big.git");
|
|
await user.click(screen.getByTestId("workspace-clone-partial"));
|
|
await user.click(screen.getByTestId("workspace-clone-run"));
|
|
await waitFor(() =>
|
|
expect(api.lanes.cloneFolder).toHaveBeenCalledWith(3, {
|
|
progressToken: expect.any(String),
|
|
url: "https://h/big.git",
|
|
partial: true,
|
|
})
|
|
);
|
|
});
|
|
|
|
it("tells an SSH link from an HTTPS one", () => {
|
|
expect(cloneTarget("git@github.com:o/r.git")).toEqual({ kind: "ssh", host: "github.com" });
|
|
expect(cloneTarget("ssh://git@h.test:2222/o/r")).toEqual({ kind: "ssh", host: "h.test" });
|
|
expect(cloneTarget("https://h.test/o/r.git")).toEqual({ kind: "https", host: "h.test" });
|
|
expect(cloneTarget("not a url")).toBeNull();
|
|
});
|
|
|
|
it("does not clone an SSH link before the account has a key, and flags an HTTPS host with no login", async () => {
|
|
const user = userEvent.setup();
|
|
vi.mocked(api.gitIdentity.get).mockResolvedValue({
|
|
ssh: null,
|
|
sshKeys: [],
|
|
https: [],
|
|
} as unknown as GitIdentity);
|
|
vi.mocked(api.lanes.cloneFolder).mockResolvedValue(view);
|
|
render(<CloneSourcesForm laneId={3} parent="/w" onAdded={vi.fn()} />);
|
|
await waitFor(() => expect(api.gitIdentity.get).toHaveBeenCalled());
|
|
|
|
await user.type(
|
|
screen.getByTestId("workspace-clone-input"),
|
|
"git@github.com:o/r.git\nhttps://h.test/a.git"
|
|
);
|
|
expect(screen.getByTestId("workspace-clone-need-ssh")).toHaveTextContent("github.com");
|
|
expect(screen.getByTestId("workspace-clone-need-login")).toHaveTextContent("h.test");
|
|
|
|
await user.click(screen.getByTestId("workspace-clone-run"));
|
|
await waitFor(() => expect(api.lanes.cloneFolder).toHaveBeenCalledTimes(1));
|
|
expect(api.lanes.cloneFolder).toHaveBeenCalledWith(3, {
|
|
url: "https://h.test/a.git",
|
|
progressToken: expect.any(String),
|
|
});
|
|
expect(await screen.findByText(/has no SSH key yet/)).toBeInTheDocument();
|
|
expect(screen.getByTestId("workspace-clone-input")).toHaveValue("git@github.com:o/r.git");
|
|
});
|
|
});
|