feat(webdav): follow-up 补齐自动同步与大文件防护 (#1043)

* feat(webdav): add robust auto sync with failure feedback

(cherry picked from commit bb6760124a62a964b36902c004e173534910728f)

* fix(webdav): enforce bounded download and extraction size

(cherry picked from commit 7777d6ec2b9bba07c8bbba9b04fe3ea6b15e0e79)

* fix(webdav): only show auto-sync callout for auto-source errors

* refactor(webdav): remove services->commands auto-sync dependency
This commit is contained in:
SaladDay
2026-02-15 20:58:17 +08:00
committed by GitHub
parent 508aa6070c
commit 20f62bf4f8
19 changed files with 777 additions and 104 deletions

View File

@@ -34,6 +34,17 @@ vi.mock("@/components/ui/input", () => ({
Input: (props: any) => <input {...props} />,
}));
vi.mock("@/components/ui/switch", () => ({
Switch: ({ checked, onCheckedChange, ...props }: any) => (
<button
role="switch"
aria-checked={checked}
onClick={() => onCheckedChange?.(!checked)}
{...props}
/>
),
}));
vi.mock("@/components/ui/select", () => ({
Select: ({ value, onValueChange, children }: any) => (
<select
@@ -82,6 +93,7 @@ const baseConfig: WebDavSyncSettings = {
password: "secret",
remoteRoot: "cc-switch-sync",
profile: "default",
autoSync: false,
status: {},
};
@@ -128,6 +140,49 @@ describe("WebdavSyncSection", () => {
settingsApiMock.webdavSyncDownload.mockResolvedValue({ status: "downloaded" });
});
it("shows auto sync error callout when last auto sync failed", () => {
renderSection({
...baseConfig,
status: {
lastError: "network timeout",
lastErrorSource: "auto",
},
});
expect(
screen.getByText("settings.webdavSync.autoSyncLastErrorTitle"),
).toBeInTheDocument();
expect(screen.getByText("network timeout")).toBeInTheDocument();
});
it("does not show auto sync error callout for manual sync errors", () => {
renderSection({
...baseConfig,
status: {
lastError: "manual upload failed",
lastErrorSource: "manual",
},
});
expect(
screen.queryByText("settings.webdavSync.autoSyncLastErrorTitle"),
).not.toBeInTheDocument();
});
it("does not show auto sync error callout when source is missing", () => {
renderSection({
...baseConfig,
autoSync: true,
status: {
lastError: "legacy error without source",
},
});
expect(
screen.queryByText("settings.webdavSync.autoSyncLastErrorTitle"),
).not.toBeInTheDocument();
});
it("shows validation error when saving without base url", async () => {
renderSection({ ...baseConfig, baseUrl: "" });
@@ -150,6 +205,7 @@ describe("WebdavSyncSection", () => {
baseUrl: "https://dav.example.com/dav/",
username: "alice",
password: "secret",
autoSync: false,
}),
false,
);
@@ -166,6 +222,24 @@ describe("WebdavSyncSection", () => {
);
});
it("saves auto sync as true after toggle", async () => {
renderSection(baseConfig);
fireEvent.click(
screen.getByRole("switch", { name: "settings.webdavSync.autoSync" }),
);
fireEvent.click(screen.getByRole("button", { name: "settings.webdavSync.save" }));
await waitFor(() => {
expect(settingsApiMock.webdavSyncSaveSettings).toHaveBeenCalledWith(
expect.objectContaining({
autoSync: true,
}),
false,
);
});
});
it("blocks upload when there are unsaved changes", async () => {
renderSection(baseConfig);

View File

@@ -209,4 +209,25 @@ describe("App integration with MSW", () => {
expect(toastErrorMock).not.toHaveBeenCalled();
expect(toastSuccessMock).toHaveBeenCalled();
});
it("shows toast when auto sync fails in background", async () => {
const { default: App } = await import("@/App");
renderApp(App);
await waitFor(() =>
expect(screen.getByTestId("provider-list").textContent).toContain(
"claude-1",
),
);
emitTauriEvent("webdav-sync-status-updated", {
source: "auto",
status: "error",
error: "network timeout",
});
await waitFor(() => {
expect(toastErrorMock).toHaveBeenCalled();
});
});
});