1import {
2 AuthOrchestrationOperateScope,
3 EnvironmentAuthorizationError,
4 EnvironmentId,
5} from "@t3tools/contracts";
6import * as Cause from "effect/Cause";
7import { AsyncResult } from "effect/reactivity";
8import { assert, beforeEach, expect, it, vi } from "vite-plus/test";
9
10const state = vi.hoisted(() => ({
11 allowed: new Set<string>(),
12 run: vi.fn(),
13 getPreference: vi.fn(),
14 setPreference: vi.fn(),
15}));
16
17vi.mock("react", () => ({ useCallback: (callback: unknown) => callback }));
18vi.mock("./state/shell", () => ({ shellEnvironment: { openInEditor: "openInEditor" } }));
19vi.mock("./state/use-atom-command", () => ({ useAtomCommand: () => state.run }));
20vi.mock("./state/session", () => ({
21 readEnvironmentScope: (environmentId: string, scope: string) =>
22 scope === AuthOrchestrationOperateScope && state.allowed.has(environmentId),
23}));
24vi.mock("./hooks/useLocalStorage", () => ({
25 getLocalStorageItem: state.getPreference,
26 setLocalStorageItem: state.setPreference,
27 useLocalStorage: vi.fn(),
28}));
29
30import { useOpenInPreferredEditor } from "./editorPreferences";
31
32const primary = EnvironmentId.make("primary");
33const secondary = EnvironmentId.make("secondary");
34
35beforeEach(() => {
36 state.allowed.clear();
37 state.run.mockReset().mockResolvedValue(AsyncResult.success(undefined));
38 state.getPreference.mockReset().mockReturnValue(null);
39 state.setPreference.mockReset();
40});
41
42it("does not use another environment's grant or change preferences when denied", async () => {
43 state.allowed.add(primary);
44 const open = useOpenInPreferredEditor(secondary, ["vscode"]);
45 const result = await open("/work/readme.md");
46 assert(result._tag === "Failure");
47 const error = Cause.squash(result.cause);
48 expect(error).toBeInstanceOf(EnvironmentAuthorizationError);
49 expect(error).toMatchObject({ requiredScope: AuthOrchestrationOperateScope });
50 expect(state.run).not.toHaveBeenCalled();
51 expect(state.getPreference).not.toHaveBeenCalled();
52 expect(state.setPreference).not.toHaveBeenCalled();
53});
54
55it("checks revocation and regrant when a retained editor callback is invoked", async () => {
56 state.allowed.add(secondary);
57 const open = useOpenInPreferredEditor(secondary, ["vscode"]);
58 state.allowed.delete(secondary);
59 expect((await open("/work/readme.md:5"))._tag).toBe("Failure");
60 expect(state.run).not.toHaveBeenCalled();
61
62 state.allowed.add(secondary);
63 expect(await open("/work/readme.md:5")).toMatchObject({ _tag: "Success", value: "vscode" });
64 expect(state.run).toHaveBeenCalledExactlyOnceWith({
65 environmentId: secondary,
66 input: { cwd: "/work/readme.md:5", editor: "vscode" },
67 });
68 expect(state.setPreference).toHaveBeenCalledTimes(1);
69});
70
71it("keeps the stored available editor for an authorized launch", async () => {
72 state.allowed.add(secondary);
73 state.getPreference.mockReturnValue("cursor");
74 const open = useOpenInPreferredEditor(secondary, ["vscode", "cursor"]);
75 expect(await open("/work")).toMatchObject({ _tag: "Success", value: "cursor" });
76 expect(state.run).toHaveBeenCalledExactlyOnceWith({
77 environmentId: secondary,
78 input: { cwd: "/work", editor: "cursor" },
79 });
80 expect(state.setPreference).not.toHaveBeenCalled();
81});
82
83it("returns the existing failures for a missing environment or editor", async () => {
84 state.allowed.add(secondary);
85 expect((await useOpenInPreferredEditor(null, ["vscode"])("/work"))._tag).toBe("Failure");
86 expect((await useOpenInPreferredEditor(secondary, [])("/work"))._tag).toBe("Failure");
87 expect(state.run).not.toHaveBeenCalled();
88});