| 1 | import { scopeThreadRef } from "@t3tools/client-runtime/environment"; |
| 2 | import { EnvironmentId, ThreadId } from "@t3tools/contracts"; |
| 3 | import { beforeEach, describe, expect, it, vi } from "vite-plus/test"; |
| 4 | |
| 5 | import { openDiffFilePrimaryAction, resolveDiffPathForWorkspace } from "./diffFileActions"; |
| 6 | import { selectThreadRightPanelState, useRightPanelStore } from "./rightPanelStore"; |
| 7 | |
| 8 | const THREAD_REF = scopeThreadRef( |
| 9 | EnvironmentId.make("environment-local"), |
| 10 | ThreadId.make("thread-1"), |
| 11 | ); |
| 12 | |
| 13 | describe("openDiffFilePrimaryAction", () => { |
| 14 | beforeEach(() => { |
| 15 | useRightPanelStore.setState({ byThreadKey: {}, threadPanelVisibilityByThreadKey: {} }); |
| 16 | }); |
| 17 | |
| 18 | it("opens diff files in the thread file viewer", () => { |
| 19 | const openInEditor = vi.fn(); |
| 20 | |
| 21 | openDiffFilePrimaryAction({ |
| 22 | threadRef: THREAD_REF, |
| 23 | filePath: "apps/web/src/components/DiffPanel.tsx", |
| 24 | activeCwd: "/repo/project", |
| 25 | openInEditor, |
| 26 | }); |
| 27 | |
| 28 | expect( |
| 29 | selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, THREAD_REF), |
| 30 | ).toMatchObject({ |
| 31 | isOpen: true, |
| 32 | activeSurfaceId: "file:apps/web/src/components/DiffPanel.tsx", |
| 33 | }); |
| 34 | expect(openInEditor).not.toHaveBeenCalled(); |
| 35 | }); |
| 36 | |
| 37 | it("falls back to the editor without thread context", () => { |
| 38 | const openInEditor = vi.fn(); |
| 39 | |
| 40 | openDiffFilePrimaryAction({ |
| 41 | threadRef: null, |
| 42 | filePath: "apps/web/src/components/DiffPanel.tsx", |
| 43 | activeCwd: "/repo/project", |
| 44 | openInEditor, |
| 45 | }); |
| 46 | |
| 47 | expect(openInEditor).toHaveBeenCalledWith( |
| 48 | "/repo/project/apps/web/src/components/DiffPanel.tsx", |
| 49 | ); |
| 50 | }); |
| 51 | |
| 52 | it("opens repository-relative diff files from a nested project", () => { |
| 53 | const openInEditor = vi.fn(); |
| 54 | |
| 55 | openDiffFilePrimaryAction({ |
| 56 | threadRef: THREAD_REF, |
| 57 | filePath: "frontend/Dockerfile", |
| 58 | activeCwd: "/repo/frontend", |
| 59 | repositoryRoot: "/repo", |
| 60 | openInEditor, |
| 61 | }); |
| 62 | |
| 63 | expect( |
| 64 | selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, THREAD_REF), |
| 65 | ).toMatchObject({ |
| 66 | isOpen: true, |
| 67 | activeSurfaceId: "file:Dockerfile", |
| 68 | }); |
| 69 | expect(openInEditor).not.toHaveBeenCalled(); |
| 70 | }); |
| 71 | |
| 72 | it("preserves repository-relative paths in a separate worktree", () => { |
| 73 | expect( |
| 74 | resolveDiffPathForWorkspace({ |
| 75 | filePath: "frontend/Dockerfile", |
| 76 | workspaceRoot: "/worktrees/feature", |
| 77 | repositoryRoot: "/repo", |
| 78 | }), |
| 79 | ).toBe("frontend/Dockerfile"); |
| 80 | }); |
| 81 | |
| 82 | it("handles Windows roots and mixed diff separators", () => { |
| 83 | expect( |
| 84 | resolveDiffPathForWorkspace({ |
| 85 | filePath: "Frontend/src\\index.ts", |
| 86 | workspaceRoot: "C:\\repo\\frontend", |
| 87 | repositoryRoot: "C:\\repo", |
| 88 | }), |
| 89 | ).toBe("src/index.ts"); |
| 90 | }); |
| 91 | |
| 92 | it.each([ |
| 93 | { workspaceRoot: "/frontend", repositoryRoot: "/" }, |
| 94 | { workspaceRoot: "C:\\frontend", repositoryRoot: "C:\\" }, |
| 95 | ])("handles filesystem roots: $repositoryRoot", ({ workspaceRoot, repositoryRoot }) => { |
| 96 | expect( |
| 97 | resolveDiffPathForWorkspace({ |
| 98 | filePath: "frontend/index.ts", |
| 99 | workspaceRoot, |
| 100 | repositoryRoot, |
| 101 | }), |
| 102 | ).toBe("index.ts"); |
| 103 | }); |
| 104 | |
| 105 | it.each(["backend/server.ts", "frontend2/app.ts", "frontend/../secret.ts", "C:secret.ts"])( |
| 106 | "does not open an out-of-project diff path: %s", |
| 107 | (filePath) => { |
| 108 | const openInEditor = vi.fn(); |
| 109 | |
| 110 | openDiffFilePrimaryAction({ |
| 111 | threadRef: THREAD_REF, |
| 112 | filePath, |
| 113 | activeCwd: "/repo/frontend", |
| 114 | repositoryRoot: "/repo", |
| 115 | openInEditor, |
| 116 | }); |
| 117 | |
| 118 | expect( |
| 119 | selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, THREAD_REF), |
| 120 | ).toMatchObject({ isOpen: false }); |
| 121 | expect(openInEditor).not.toHaveBeenCalled(); |
| 122 | }, |
| 123 | ); |
| 124 | }); |