feat: Stop archives a session, Sleep keeps it; SSH keys per type (#82); update Auto fix
- Stop (a tab's or pane's X, Close tab) now archives the session it held (DELETE /api/run/:id?archive=1); the new Sleep (moon left of the X, and in the tab, pane and session right-click menus) stops the pane but keeps the session listed to resume later. Shared endRuns in client/src/lib/stopSession.ts. - Git identity (#82): an account keeps an ED25519 AND an RSA key (AWS CodeCommit takes only RSA), every ssh git offers both, and a member's ~/.gitconfig gets core.sshCommand so a git typed in a terminal uses them. - Update dialog: Auto fix starts a Claude Code session on the install folder with the failed update's error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,18 @@ import type { UpdateStatusPayload } from "../../lib/types";
|
||||
const statusMock = vi.fn();
|
||||
const checkMock = vi.fn();
|
||||
const applyMock = vi.fn();
|
||||
const ensureMock = vi.fn();
|
||||
const startMock = vi.fn();
|
||||
const navigateMock = vi.fn();
|
||||
|
||||
vi.mock("react-router-dom", async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
useNavigate: () => navigateMock,
|
||||
}));
|
||||
|
||||
vi.mock("../lanes/startSession", () => ({
|
||||
startInFreeTab: (...args: unknown[]) => startMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock("../../lib/api", async (importOriginal) => {
|
||||
const actual = await importOriginal<Record<string, unknown>>();
|
||||
@@ -30,6 +42,7 @@ vi.mock("../../lib/api", async (importOriginal) => {
|
||||
check: (...args: unknown[]) => checkMock(...args),
|
||||
apply: (...args: unknown[]) => applyMock(...args),
|
||||
},
|
||||
lanes: { ensure: (...args: unknown[]) => ensureMock(...args) },
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -56,6 +69,9 @@ beforeEach(() => {
|
||||
statusMock.mockReset().mockResolvedValue(BASE_STATUS);
|
||||
checkMock.mockReset().mockResolvedValue(BASE_STATUS);
|
||||
applyMock.mockReset();
|
||||
ensureMock.mockReset().mockResolvedValue({ lane: { id: 7 }, created: true });
|
||||
startMock.mockReset().mockResolvedValue("agenthub-lane-7-1");
|
||||
navigateMock.mockReset();
|
||||
try {
|
||||
localStorage.clear();
|
||||
} catch {
|
||||
@@ -274,7 +290,7 @@ describe("UpdateNotifier — closing vs skipping", () => {
|
||||
|
||||
it("Check for updates re-opens a modal closed with Later", async () => {
|
||||
render(<UpdateNotifier />);
|
||||
await userEvent.click(await screen.findByText(i18n.t("updates:later")));
|
||||
await userEvent.click(await screen.findByRole("button", { name: i18n.t("updates:later") }));
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByText(i18n.t("updates:title"))).not.toBeInTheDocument()
|
||||
);
|
||||
@@ -290,3 +306,26 @@ describe("UpdateNotifier — closing vs skipping", () => {
|
||||
expect(screen.getByText(i18n.t("updates:title"))).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("UpdateNotifier — Auto fix", () => {
|
||||
it("offers no Not now button, and Auto fix only once an update failed", async () => {
|
||||
applyMock.mockRejectedValue(new Error("Command failed: git merge --ff-only v2.8.0"));
|
||||
render(<UpdateNotifier />);
|
||||
await screen.findByText(i18n.t("updates:updateNow"));
|
||||
expect(screen.queryByText(i18n.t("updates:later"))).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(i18n.t("updates:autoFix"))).not.toBeInTheDocument();
|
||||
|
||||
await userEvent.click(screen.getByText(i18n.t("updates:updateNow")));
|
||||
await userEvent.click(await screen.findByText(i18n.t("updates:autoFix")));
|
||||
|
||||
await waitFor(() => expect(navigateMock).toHaveBeenCalled());
|
||||
expect(ensureMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ cwd: "/repo", exact: true, tool: "claude" })
|
||||
);
|
||||
const [laneId, body] = startMock.mock.calls[0] ?? [];
|
||||
expect(laneId).toBe(7);
|
||||
expect(body.prompt).toContain("git merge --ff-only v2.8.0");
|
||||
expect(body.prompt).toContain(BASE_STATUS.manual_command);
|
||||
expect(navigateMock).toHaveBeenCalledWith("/workspace?lane=7&run=agenthub-lane-7-1");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -254,7 +254,7 @@ describe("WorkspaceTree", () => {
|
||||
let menu = screen.getByTestId("session-menu-s-old");
|
||||
// Ended: no pane to split or close.
|
||||
expect(within(menu).queryByText("Open in split")).toBeNull();
|
||||
expect(within(menu).queryByText("Close tab (stops its session)")).toBeNull();
|
||||
expect(within(menu).queryByText("Close tab (stops and archives its session)")).toBeNull();
|
||||
await user.click(within(menu).getByText("Rename"));
|
||||
expect(screen.queryByTestId("session-menu-s-old")).toBeNull();
|
||||
expect(screen.getByTestId("workspace-session-rename-s-old")).toBeInTheDocument();
|
||||
@@ -271,11 +271,17 @@ describe("WorkspaceTree", () => {
|
||||
await user.click(within(screen.getByTestId("session-menu-s-live")).getByText("Archive"));
|
||||
expect(api.sessions.patch).toHaveBeenCalledWith("s-live", { archived: true });
|
||||
|
||||
await right(screen.getByTestId("workspace-session-s-live"));
|
||||
await user.click(screen.getByTestId("session-menu-sleep"));
|
||||
expect(api.run.kill).toHaveBeenCalledWith("agenthub-lane-1-2", { archive: false });
|
||||
|
||||
await right(screen.getByTestId("workspace-session-s-live"));
|
||||
await user.click(
|
||||
within(screen.getByTestId("session-menu-s-live")).getByText("Close tab (stops its session)")
|
||||
within(screen.getByTestId("session-menu-s-live")).getByText(
|
||||
"Close tab (stops and archives its session)"
|
||||
)
|
||||
);
|
||||
expect(api.run.kill).toHaveBeenCalledWith("agenthub-lane-1-2");
|
||||
expect(api.run.kill).toHaveBeenCalledWith("agenthub-lane-1-2", { archive: true });
|
||||
});
|
||||
|
||||
it("archiving a worktree session removes its worktree first, after asking", async () => {
|
||||
|
||||
Reference in New Issue
Block a user