mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-08-02 10:21:16 +08:00
feat: add session deletion with per-provider cleanup and path safety
Add delete_session Tauri command dispatching to provider-specific deletion logic for all 5 providers (Claude, Codex, Gemini, OpenCode, OpenClaw). Includes path traversal protection via canonicalize + starts_with validation, session ID verification against file contents, frontend confirmation dialog with optimistic cache updates, i18n keys (zh/en/ja), and component tests.
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import {
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
within,
|
||||
} from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { SessionManagerPage } from "@/components/sessions/SessionManagerPage";
|
||||
import type { SessionMessage, SessionMeta } from "@/types";
|
||||
import { setSessionFixtures } from "../msw/state";
|
||||
|
||||
const toastSuccessMock = vi.fn();
|
||||
const toastErrorMock = vi.fn();
|
||||
|
||||
vi.mock("sonner", () => ({
|
||||
toast: {
|
||||
success: (...args: unknown[]) => toastSuccessMock(...args),
|
||||
error: (...args: unknown[]) => toastErrorMock(...args),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/components/sessions/SessionToc", () => ({
|
||||
SessionTocSidebar: () => null,
|
||||
SessionTocDialog: () => null,
|
||||
}));
|
||||
|
||||
vi.mock("@/components/ConfirmDialog", () => ({
|
||||
ConfirmDialog: ({
|
||||
isOpen,
|
||||
title,
|
||||
message,
|
||||
confirmText,
|
||||
cancelText,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
}: {
|
||||
isOpen: boolean;
|
||||
title: string;
|
||||
message: string;
|
||||
confirmText: string;
|
||||
cancelText: string;
|
||||
onConfirm: () => void;
|
||||
onCancel: () => void;
|
||||
}) =>
|
||||
isOpen ? (
|
||||
<div data-testid="confirm-dialog">
|
||||
<div>{title}</div>
|
||||
<div>{message}</div>
|
||||
<button onClick={onConfirm}>{confirmText}</button>
|
||||
<button onClick={onCancel}>{cancelText}</button>
|
||||
</div>
|
||||
) : null,
|
||||
}));
|
||||
|
||||
const renderPage = () => {
|
||||
const client = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
|
||||
return render(
|
||||
<QueryClientProvider client={client}>
|
||||
<SessionManagerPage appId="codex" />
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
};
|
||||
|
||||
describe("SessionManagerPage", () => {
|
||||
beforeEach(() => {
|
||||
toastSuccessMock.mockReset();
|
||||
toastErrorMock.mockReset();
|
||||
|
||||
const sessions: SessionMeta[] = [
|
||||
{
|
||||
providerId: "codex",
|
||||
sessionId: "codex-session-1",
|
||||
title: "Alpha Session",
|
||||
summary: "Alpha summary",
|
||||
projectDir: "/mock/codex",
|
||||
createdAt: 2,
|
||||
lastActiveAt: 20,
|
||||
sourcePath: "/mock/codex/session-1.jsonl",
|
||||
resumeCommand: "codex resume codex-session-1",
|
||||
},
|
||||
{
|
||||
providerId: "codex",
|
||||
sessionId: "codex-session-2",
|
||||
title: "Beta Session",
|
||||
summary: "Beta summary",
|
||||
projectDir: "/mock/codex",
|
||||
createdAt: 1,
|
||||
lastActiveAt: 10,
|
||||
sourcePath: "/mock/codex/session-2.jsonl",
|
||||
resumeCommand: "codex resume codex-session-2",
|
||||
},
|
||||
];
|
||||
const messages: Record<string, SessionMessage[]> = {
|
||||
"codex:/mock/codex/session-1.jsonl": [
|
||||
{ role: "user", content: "alpha", ts: 20 },
|
||||
],
|
||||
"codex:/mock/codex/session-2.jsonl": [
|
||||
{ role: "user", content: "beta", ts: 10 },
|
||||
],
|
||||
};
|
||||
|
||||
setSessionFixtures(sessions, messages);
|
||||
});
|
||||
|
||||
it("deletes the selected session and selects the next visible session", async () => {
|
||||
renderPage();
|
||||
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.getByRole("heading", { name: "Alpha Session" }),
|
||||
).toBeInTheDocument(),
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /删除会话/i }));
|
||||
|
||||
const dialog = screen.getByTestId("confirm-dialog");
|
||||
expect(dialog).toBeInTheDocument();
|
||||
expect(within(dialog).getByText(/Alpha Session/)).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(within(dialog).getByRole("button", { name: /删除会话/i }));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.getByRole("heading", { name: "Beta Session" }),
|
||||
).toBeInTheDocument(),
|
||||
);
|
||||
|
||||
expect(screen.queryByText("Alpha Session")).not.toBeInTheDocument();
|
||||
expect(toastErrorMock).not.toHaveBeenCalled();
|
||||
expect(toastSuccessMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user