Файли
7fdf1d62c3 fix(refactor): preserve versions and recover multi-file workspace edits (#488)
Multi-file refactorings could drop LSP document versions, overwrite content changed after a preview, or leave earlier files saved after a later write failed. Workspace edits now keep server versions and preview fingerprints, are validated on a staging copy, snapshot disk and open buffers, and roll back on commit failure (retaining recovery files if rollback fails). The whole edit is one Undo/Redo step in editors and in the notification. Version reads and transaction work run off the async workers. Closes #461.

Co-authored-by: shohruh-genius <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-02 22:18:16 +08:00

67 рядки
2.4 KiB
TypeScript

import { afterEach, beforeEach, expect, it, vi } from "vitest";
import { emitLocalIdeEvent } from "@/platform/ipc";
import { provideNotificationService, type NotificationService } from "@/workbench/contributions";
import { useProject } from "@/platform/project";
import { installWorkspaceEditUndo } from "./workspaceUndo";
const mocks = vi.hoisted(() => ({ undo: vi.fn(), synced: vi.fn(), history: vi.fn(() => () => {}) }));
vi.mock("@/editor/api", () => ({
whenAllSynced: mocks.synced,
workspaceEditHistory: mocks.undo,
onWorkspaceEditHistory: mocks.history,
}));
let stop: () => void;
let notices: Parameters<NotificationService["notify"]>[0][];
beforeEach(() => {
mocks.undo.mockReset().mockResolvedValue(undefined);
mocks.synced.mockReset().mockResolvedValue(undefined);
notices = [];
provideNotificationService({
notify: (notice) => {
notices.push(notice);
},
});
stop = installWorkspaceEditUndo();
});
afterEach(() => stop());
it("waits for pending typing before undoing the entire backend transaction", async () => {
let release: () => void = () => {};
mocks.synced.mockReturnValue(
new Promise<void>((resolve) => {
release = resolve;
}),
);
emitLocalIdeEvent({ type: "workspaceEditApplied", id: 17, paths: [] });
const action = notices[0]!.actions![0]!;
expect(action.label).toBe("Undo");
const pending = action.run();
expect(mocks.undo).not.toHaveBeenCalled();
release();
await pending;
expect(mocks.undo).toHaveBeenCalledExactlyOnceWith(17);
});
it("reports a refused undo instead of discarding newer work", async () => {
mocks.undo.mockRejectedValue(new Error("a.txt changed"));
emitLocalIdeEvent({ type: "workspaceEditApplied", id: 18, paths: [] });
await notices[0]!.actions![0]!.run();
expect(notices[1]).toMatchObject({ level: "error", title: "Cannot undo refactoring", body: "a.txt changed" });
});
it("does not apply an old notification's id to a different project", async () => {
emitLocalIdeEvent({ type: "workspaceEditApplied", id: 19, paths: [] });
const previous = useProject.getState().project;
useProject.setState({ project: { ...previous, root: "/different-project" } as NonNullable<typeof previous> });
try {
await notices[0]!.actions![0]!.run();
expect(mocks.undo).not.toHaveBeenCalled();
expect(notices[1]).toMatchObject({ level: "error", body: "The project for this refactoring is no longer open" });
} finally {
useProject.setState({ project: previous });
}
});