дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 03:05:40 +02:00
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]>
67 рядки
2.4 KiB
TypeScript
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 });
|
|
}
|
|
});
|