f6eb39eb73
- switch takes changes: carry (plain git switch, 409 ECARRYCONFLICT with files) or shelve (shelf first, re-applied if the switch fails) - branch-create switch: true runs git switch -c, the changes come along - shelve / unshelve / shelf-drop + GET /git/shelves: only agenthub-shelf: stash entries, addressed by sha, repo root only, never mid-operation; a clean restore drops the shelf, a conflicted one keeps it - 409 EAGENTRUNNING until confirm: true while the workspace's agent is working - git bar: Bring my changes / Shelve and switch choice, Create & switch, restore offer after a switch; Changes panel: Shelve (n) and the Shelves list - fix: after a switch the Changes panel showed the old branch — the post-write re-read joined an in-flight GET started before the write (request() dedupe); writes now close in-flight GETs, and every git write fires agenthub:git-changed so every view of the workspace re-reads
107 lines
3.9 KiB
TypeScript
107 lines
3.9 KiB
TypeScript
/**
|
|
* @file LaneShelves.test.tsx
|
|
* @description Pins the Changes panel's Shelves list: hidden while there are
|
|
* none, one collapsed line otherwise, each shelf's branch, note and files,
|
|
* Restore sending `unshelve` by sha (and the conflict outcome told in a
|
|
* toast), Delete needing a second click before `shelf-drop` with
|
|
* `confirm: true`.
|
|
* @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 { LaneShelves } from "../LaneShelves";
|
|
import { api } from "../../../lib/api";
|
|
import { toast } from "../../../lib/toast";
|
|
|
|
vi.mock("../../../lib/confirm", () => ({ confirmDialog: vi.fn(async () => true) }));
|
|
vi.mock("../../../lib/toast", () => ({
|
|
toast: { success: vi.fn(), error: vi.fn(), info: vi.fn() },
|
|
}));
|
|
vi.mock("../../../lib/api", () => ({
|
|
api: { lanes: { gitAction: vi.fn(), gitShelves: vi.fn() } },
|
|
}));
|
|
|
|
const shelf = {
|
|
ref: "stash@{0}",
|
|
sha: "abc1234def5678",
|
|
branch: "fix/ac-515-a-rather-long-branch-name",
|
|
note: "half way through the login fix",
|
|
createdAt: "2026-11-02T03:04:05.000Z",
|
|
files: [
|
|
{ path: "src/login.ts", status: "M" },
|
|
{ path: "src/new.ts", status: "??" },
|
|
],
|
|
};
|
|
|
|
beforeEach(() => {
|
|
vi.mocked(api.lanes.gitShelves)
|
|
.mockReset()
|
|
.mockResolvedValue({
|
|
available: true,
|
|
shelves: [shelf],
|
|
});
|
|
vi.mocked(api.lanes.gitAction).mockReset().mockResolvedValue({
|
|
action: "unshelve",
|
|
ok: true,
|
|
output: "",
|
|
conflicts: [],
|
|
});
|
|
});
|
|
|
|
describe("LaneShelves", () => {
|
|
it("renders nothing while there are no shelves", async () => {
|
|
vi.mocked(api.lanes.gitShelves).mockResolvedValue({ available: true, shelves: [] });
|
|
const { container } = render(<LaneShelves target={3} reread={0} onChanged={vi.fn()} />);
|
|
await waitFor(() => expect(api.lanes.gitShelves).toHaveBeenCalledWith(3));
|
|
expect(container).toBeEmptyDOMElement();
|
|
});
|
|
|
|
it("lists a shelf with its branch, note and files", async () => {
|
|
const user = userEvent.setup();
|
|
render(<LaneShelves target={3} reread={0} onChanged={vi.fn()} />);
|
|
await user.click(await screen.findByRole("button", { name: /Shelves \(1\)/ }));
|
|
const row = screen.getByTestId("lane-shelf");
|
|
expect(row).toHaveTextContent(shelf.branch);
|
|
expect(row).toHaveTextContent(shelf.note);
|
|
await user.click(screen.getByRole("button", { name: "2 file(s)" }));
|
|
expect(screen.getByTestId("lane-shelf-files")).toHaveTextContent("src/new.ts");
|
|
});
|
|
|
|
it("restores by sha and reports a conflicted restore", async () => {
|
|
vi.mocked(api.lanes.gitAction).mockResolvedValueOnce({
|
|
action: "unshelve",
|
|
ok: true,
|
|
output: "",
|
|
conflicts: ["src/login.ts"],
|
|
kept: true,
|
|
});
|
|
const user = userEvent.setup();
|
|
const onChanged = vi.fn();
|
|
render(<LaneShelves target={3} reread={0} onChanged={onChanged} />);
|
|
await user.click(await screen.findByRole("button", { name: /Shelves/ }));
|
|
await user.click(screen.getByRole("button", { name: "Restore" }));
|
|
await waitFor(() =>
|
|
expect(api.lanes.gitAction).toHaveBeenCalledWith(3, "unshelve", { sha: shelf.sha })
|
|
);
|
|
expect(onChanged).toHaveBeenCalled();
|
|
expect(toast.info).toHaveBeenCalledWith(expect.stringContaining("1 conflicted file"));
|
|
});
|
|
|
|
it("deletes only on the second click, with confirm", async () => {
|
|
const user = userEvent.setup();
|
|
render(<LaneShelves target={3} reread={0} onChanged={vi.fn()} />);
|
|
await user.click(await screen.findByRole("button", { name: /Shelves/ }));
|
|
await user.click(screen.getByRole("button", { name: "Delete" }));
|
|
expect(api.lanes.gitAction).not.toHaveBeenCalled();
|
|
await user.click(screen.getByRole("button", { name: "Delete for good?" }));
|
|
await waitFor(() =>
|
|
expect(api.lanes.gitAction).toHaveBeenCalledWith(3, "shelf-drop", {
|
|
sha: shelf.sha,
|
|
confirm: true,
|
|
})
|
|
);
|
|
});
|
|
});
|