Merge branch 'refs/heads/main' into fix/openclaw-serialize-panic

This commit is contained in:
YoVinchen
2026-04-01 09:03:34 +08:00
34 changed files with 1829 additions and 399 deletions
+151 -8
View File
@@ -1,5 +1,6 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import {
act,
fireEvent,
render,
screen,
@@ -8,6 +9,7 @@ import {
} from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { SessionManagerPage } from "@/components/sessions/SessionManagerPage";
import { sessionsApi } from "@/lib/api/sessions";
import type { SessionMessage, SessionMeta } from "@/types";
import { setSessionFixtures } from "../msw/state";
@@ -62,16 +64,19 @@ const renderPage = () => {
},
});
return render(
<QueryClientProvider client={client}>
<SessionManagerPage appId="codex" />
</QueryClientProvider>,
);
return {
client,
...render(
<QueryClientProvider client={client}>
<SessionManagerPage appId="codex" />
</QueryClientProvider>,
),
};
};
const openSearch = () => {
const searchButton = Array.from(screen.getAllByRole("button")).find((button) =>
button.querySelector(".lucide-search"),
const searchButton = Array.from(screen.getAllByRole("button")).find(
(button) => button.querySelector(".lucide-search"),
);
if (!searchButton) {
@@ -81,10 +86,23 @@ const openSearch = () => {
fireEvent.click(searchButton);
};
const closeSearch = () => {
const closeButton = Array.from(screen.getAllByRole("button")).find(
(button) => button.querySelector(".lucide-x"),
);
if (!closeButton) {
throw new Error("Search close button not found");
}
fireEvent.click(closeButton);
};
describe("SessionManagerPage", () => {
beforeEach(() => {
toastSuccessMock.mockReset();
toastErrorMock.mockReset();
Element.prototype.scrollIntoView = vi.fn();
const sessions: SessionMeta[] = [
{
@@ -178,11 +196,136 @@ describe("SessionManagerPage", () => {
expect(screen.queryByText("Alpha Session")).not.toBeInTheDocument(),
);
expect(screen.getByText("sessionManager.selectSession")).toBeInTheDocument();
expect(
screen.getByText("sessionManager.selectSession"),
).toBeInTheDocument();
expect(
screen.queryByText("sessionManager.emptySession"),
).not.toBeInTheDocument();
expect(toastErrorMock).not.toHaveBeenCalled();
expect(toastSuccessMock).toHaveBeenCalled();
});
it("restores batch delete controls when deleteMany rejects", async () => {
const deleteManySpy = vi
.spyOn(sessionsApi, "deleteMany")
.mockRejectedValueOnce(new Error("network error"));
renderPage();
await waitFor(() =>
expect(
screen.getByRole("heading", { name: "Alpha Session" }),
).toBeInTheDocument(),
);
fireEvent.click(screen.getByRole("button", { name: /批量管理/i }));
fireEvent.click(screen.getByRole("button", { name: /全选当前/i }));
fireEvent.click(screen.getByRole("button", { name: /批量删除/i }));
const dialog = screen.getByTestId("confirm-dialog");
fireEvent.click(
within(dialog).getByRole("button", { name: /删除所选会话/i }),
);
await waitFor(() =>
expect(toastErrorMock).toHaveBeenCalledWith("network error"),
);
await waitFor(() =>
expect(
screen.getByRole("button", { name: /批量删除/i }),
).not.toBeDisabled(),
);
deleteManySpy.mockRestore();
});
it("keeps the exit batch mode button visible when search hides all sessions", async () => {
renderPage();
await waitFor(() =>
expect(
screen.getByRole("heading", { name: "Alpha Session" }),
).toBeInTheDocument(),
);
fireEvent.click(screen.getByRole("button", { name: /批量管理/i }));
openSearch();
fireEvent.change(screen.getByRole("textbox"), {
target: { value: "NoSuchSession" },
});
await waitFor(() => expect(screen.queryByText("Alpha Session")).toBeNull());
expect(screen.getByRole("button", { name: /退出批量管理/i })).toBeVisible();
});
it("drops hidden selections when search narrows the result set", async () => {
renderPage();
await waitFor(() =>
expect(
screen.getByRole("heading", { name: "Alpha Session" }),
).toBeInTheDocument(),
);
fireEvent.click(screen.getByRole("button", { name: /批量管理/i }));
fireEvent.click(screen.getByRole("button", { name: /全选当前/i }));
expect(screen.getByText("已选 2 项")).toBeInTheDocument();
openSearch();
fireEvent.change(screen.getByRole("textbox"), {
target: { value: "Alpha" },
});
await waitFor(() =>
expect(screen.queryByText("Beta Session")).not.toBeInTheDocument(),
);
closeSearch();
await waitFor(() =>
expect(screen.getByText("已选 1 项")).toBeInTheDocument(),
);
});
it("removes successfully deleted sessions from the UI before refetch completes", async () => {
const view = renderPage();
let resolveInvalidate!: () => void;
const invalidateSpy = vi
.spyOn(view.client, "invalidateQueries")
.mockImplementation(
() =>
new Promise((resolve) => {
resolveInvalidate = () => resolve(undefined);
}),
);
await waitFor(() =>
expect(
screen.getByRole("heading", { name: "Alpha Session" }),
).toBeInTheDocument(),
);
fireEvent.click(screen.getByRole("button", { name: /批量管理/i }));
fireEvent.click(screen.getByRole("button", { name: /全选当前/i }));
fireEvent.click(screen.getByRole("button", { name: /批量删除/i }));
const dialog = screen.getByTestId("confirm-dialog");
fireEvent.click(
within(dialog).getByRole("button", { name: /删除所选会话/i }),
);
await waitFor(() => {
expect(screen.queryByText("Alpha Session")).not.toBeInTheDocument();
expect(screen.queryByText("Beta Session")).not.toBeInTheDocument();
});
await act(async () => {
resolveInvalidate();
});
invalidateSpy.mockRestore();
});
});
+108 -2
View File
@@ -104,11 +104,12 @@ function renderSection(config?: WebDavSyncSettings) {
mutations: { retry: false },
},
});
return render(
const view = render(
<QueryClientProvider client={client}>
<WebdavSyncSection config={config} />
</QueryClientProvider>,
);
return { ...view, client };
}
describe("WebdavSyncSection", () => {
@@ -204,7 +205,7 @@ describe("WebdavSyncSection", () => {
expect.objectContaining({
baseUrl: "https://dav.example.com/dav/",
username: "alice",
password: "secret",
password: "",
autoSync: false,
}),
false,
@@ -222,6 +223,111 @@ describe("WebdavSyncSection", () => {
);
});
it("preserves password only for the single post-save refresh", async () => {
const view = renderSection(baseConfig);
fireEvent.click(screen.getByRole("button", { name: "settings.webdavSync.save" }));
await waitFor(() => {
expect(settingsApiMock.webdavSyncSaveSettings).toHaveBeenCalledTimes(1);
});
view.rerender(
<QueryClientProvider client={view.client}>
<WebdavSyncSection config={{ ...baseConfig, password: "" }} />
</QueryClientProvider>,
);
expect(
(
screen.getByPlaceholderText(
"settings.webdavSync.passwordPlaceholder",
) as HTMLInputElement
).value,
).toBe("secret");
view.rerender(
<QueryClientProvider client={view.client}>
<WebdavSyncSection config={{ ...baseConfig, password: "" }} />
</QueryClientProvider>,
);
expect(
(
screen.getByPlaceholderText(
"settings.webdavSync.passwordPlaceholder",
) as HTMLInputElement
).value,
).toBe("");
});
it("does not preserve password after a later external config refresh", async () => {
const view = renderSection(baseConfig);
fireEvent.click(screen.getByRole("button", { name: "settings.webdavSync.save" }));
await waitFor(() => {
expect(settingsApiMock.webdavSyncSaveSettings).toHaveBeenCalledTimes(1);
});
view.rerender(
<QueryClientProvider client={view.client}>
<WebdavSyncSection config={{ ...baseConfig, password: "" }} />
</QueryClientProvider>,
);
expect(
(
screen.getByPlaceholderText(
"settings.webdavSync.passwordPlaceholder",
) as HTMLInputElement
).value,
).toBe("secret");
view.rerender(
<QueryClientProvider client={view.client}>
<WebdavSyncSection
config={{ ...baseConfig, username: "bob", password: "" }}
/>
</QueryClientProvider>,
);
expect(
(
screen.getByPlaceholderText(
"settings.webdavSync.passwordPlaceholder",
) as HTMLInputElement
).value,
).toBe("");
});
it("does not submit a preserved password again when testing without touching it", async () => {
const view = renderSection(baseConfig);
fireEvent.click(screen.getByRole("button", { name: "settings.webdavSync.save" }));
await waitFor(() => {
expect(settingsApiMock.webdavSyncSaveSettings).toHaveBeenCalledTimes(1);
});
view.rerender(
<QueryClientProvider client={view.client}>
<WebdavSyncSection config={{ ...baseConfig, password: "" }} />
</QueryClientProvider>,
);
fireEvent.click(screen.getByRole("button", { name: "settings.webdavSync.test" }));
await waitFor(() => {
expect(settingsApiMock.webdavTestConnection).toHaveBeenLastCalledWith(
expect.objectContaining({
password: "",
}),
true,
);
});
});
it("saves auto sync as true after toggle", async () => {
renderSection(baseConfig);
+23
View File
@@ -144,6 +144,29 @@ export const handlers = [
return success(deleteSession(providerId, sessionId, sourcePath));
}),
http.post(`${TAURI_ENDPOINT}/delete_sessions`, async ({ request }) => {
const { items = [] } = await withJson<{
items?: {
providerId: string;
sessionId: string;
sourcePath: string;
}[];
}>(request);
return success(
items.map((item) => ({
providerId: item.providerId,
sessionId: item.sessionId,
sourcePath: item.sourcePath,
success: deleteSession(
item.providerId,
item.sessionId,
item.sourcePath,
),
})),
);
}),
// MCP APIs
http.post(`${TAURI_ENDPOINT}/get_mcp_config`, async ({ request }) => {
const { app } = await withJson<{ app: AppId }>(request);