дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-08 19:02:17 +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]>
78 рядки
3.1 KiB
TypeScript
78 рядки
3.1 KiB
TypeScript
import { afterEach, beforeEach, expect, it, vi } from "vitest";
|
|
import { act, cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
import type { StructuralFile, WorkspaceEdit } from "@/platform/ipc";
|
|
import { provideNotificationService } from "@/workbench/contributions";
|
|
import { AnalysisDialogHost } from "./dialogs";
|
|
import { showReplacePreview } from "./AnalysisDialogs";
|
|
|
|
const mocks = vi.hoisted(() => ({ prepare: vi.fn(), apply: vi.fn(), synced: vi.fn(), notify: vi.fn() }));
|
|
vi.mock("@/platform/ipc", async (original) => {
|
|
const actual = await original<typeof import("@/platform/ipc")>();
|
|
return {
|
|
...actual,
|
|
isTauri: () => true,
|
|
commands: { ...actual.commands, prepareWorkspaceEdit: mocks.prepare, applyWorkspaceEdit: mocks.apply },
|
|
};
|
|
});
|
|
vi.mock("@/editor/api", async (original) => ({
|
|
...(await original<typeof import("@/editor/api")>()),
|
|
whenAllSynced: mocks.synced,
|
|
}));
|
|
const files: StructuralFile[] = ["a.ts", "b.ts"].map((name) => ({
|
|
path: `/p/${name}`,
|
|
relPath: name,
|
|
matches: [
|
|
{
|
|
range: { start: { line: 0, col: 0 }, end: { line: 0, col: 3 } },
|
|
text: "old",
|
|
lineText: "old",
|
|
replacement: "new",
|
|
},
|
|
],
|
|
}));
|
|
const preconditions = files.map((f) => ({ path: f.path, hash: `preview:${f.path}` }));
|
|
beforeEach(() => {
|
|
mocks.prepare
|
|
.mockReset()
|
|
.mockImplementation(async (edit: WorkspaceEdit) => ({ status: "ok", data: { ...edit, preconditions } }));
|
|
mocks.apply.mockReset().mockResolvedValue({ status: "ok", data: null });
|
|
mocks.synced.mockReset().mockResolvedValue(undefined);
|
|
mocks.notify.mockReset();
|
|
provideNotificationService({ notify: mocks.notify });
|
|
});
|
|
afterEach(() => {
|
|
if (screen.queryByRole("button", { name: "Cancel" })) fireEvent.click(screen.getByRole("button", { name: "Cancel" }));
|
|
cleanup();
|
|
});
|
|
|
|
it("captures guards before preview and preserves them when the user excludes a file", async () => {
|
|
render(<AnalysisDialogHost />);
|
|
await act(() => showReplacePreview(files));
|
|
expect(mocks.prepare).toHaveBeenCalledOnce();
|
|
fireEvent.click(screen.getByRole("checkbox", { name: "Replace in b.ts:1" }));
|
|
fireEvent.click(screen.getByRole("button", { name: "Replace" }));
|
|
await waitFor(() => expect(mocks.apply).toHaveBeenCalledOnce());
|
|
expect(mocks.apply.mock.calls[0]![0]).toEqual({
|
|
files: [{ kind: "edit", path: "/p/a.ts", edits: [{ range: files[0]!.matches[0]!.range, new_text: "new" }] }],
|
|
preconditions,
|
|
});
|
|
expect(mocks.synced).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it("reports a stale preview without refreshing guards or retrying the edits", async () => {
|
|
mocks.apply.mockResolvedValue({
|
|
status: "error",
|
|
error: { kind: "outOfSync", message: "a.ts changed; request again" },
|
|
});
|
|
render(<AnalysisDialogHost />);
|
|
await act(() => showReplacePreview(files));
|
|
fireEvent.click(screen.getByRole("button", { name: "Replace" }));
|
|
await waitFor(() =>
|
|
expect(mocks.notify).toHaveBeenCalledWith(
|
|
expect.objectContaining({ level: "error", title: "Replace Structurally", body: "a.ts changed; request again" }),
|
|
),
|
|
);
|
|
expect(mocks.prepare).toHaveBeenCalledOnce();
|
|
expect(mocks.apply).toHaveBeenCalledOnce();
|
|
});
|