Skip to content
Source
Sign inSign up

sourceControlActions.test.ts

203 lines6.8 KB

1import {
2 AuthOrchestrationOperateScope,
3 AuthSourceControlWriteScope,
4 EnvironmentId,
5 ThreadId,
6 SourceControlProviderKind,
7} from "@t3tools/contracts";
8import * as Cause from "effect/Cause";
9import { AsyncResult } from "effect/reactivity";
10import { beforeEach, describe, expect, it, vi } from "vite-plus/test";
11​
12const state = vi.hoisted(() => ({
13 scopes: new Map<string, Set<string>>(),
14 requests: [] as { action: string; environmentId: string | null }[],
15}));
16​
17vi.mock("react", () => ({ useCallback: (callback: unknown) => callback }));
18vi.mock("@effect/atom-react", () => ({
19 useAtomValue: () => ({ operation: null, error: null, isRunning: false }),
20}));
21vi.mock("../rpc/atomRegistry", () => ({ appAtomRegistry: {} }));
22vi.mock("../components/sourceControlAppAccessToast", () => ({
23 suggestAppAccessForRemote: () => Promise.resolve(),
24}));
25vi.mock("./session", () => ({
26 useEnvironmentScope: (environmentId: string, scope: string) =>
27 state.scopes.get(environmentId)?.has(scope) === true,
28 readEnvironmentScope: (environmentId: string, scope: string) =>
29 state.scopes.get(environmentId)?.has(scope) === true,
30}));
31vi.mock("./query", () => ({ useEnvironmentQuery: () => ({ refresh: () => {} }) }));
32vi.mock("./use-atom-command", () => ({
33 useAtomCommand:
34 (command: { action: string; environmentId?: string }) =>
35 async (input: { environmentId?: string }) => {
36 state.requests.push({
37 action: command.action,
38 environmentId: input.environmentId ?? command.environmentId ?? null,
39 });
40 return AsyncResult.success({});
41 },
42}));
43vi.mock("./vcs", () => ({
44 vcsEnvironment: { init: { action: "init" }, pull: { action: "pull" }, status: () => "status" },
45 vcsActionManager: {
46 stateAtom: () => "state",
47 resetError: () => {},
48 track: (_registry: unknown, _target: unknown, _input: unknown, execute: () => unknown) =>
49 execute(),
50 runStackedAction: (scope: { environmentId: string }) => ({ action: "stack", ...scope }),
51 },
52}));
53vi.mock("./sourceControl", () => ({
54 sourceControlEnvironment: { publishRepository: { action: "publish" } },
55}));
56vi.mock("./git", () => ({
57 gitEnvironment: { preparePullRequestThread: { action: "prepare" } },
58}));
59​
60import {
61 useGitStackedAction,
62 usePreparePullRequestThreadAction,
63 useSourceControlPublishRepositoryAction,
64 useVcsInitAction,
65 useVcsPullAction,
66} from "./sourceControlActions";
67​
68const primary = EnvironmentId.make("primary");
69const secondary = EnvironmentId.make("secondary");
70const scope = { environmentId: secondary, cwd: "/repo" };
71const threadId = ThreadId.make("thread");
72const cases = [
73 {
74 name: "init",
75 create: () => {
76 const action = useVcsInitAction(scope);
77 return { isAllowed: action.isAllowed, run: () => action.run() };
78 },
79 },
80 {
81 name: "pull",
82 create: () => {
83 const action = useVcsPullAction(scope);
84 return { isAllowed: action.isAllowed, run: () => action.run() };
85 },
86 },
87 {
88 name: "stack",
89 create: () => {
90 const action = useGitStackedAction(scope);
91 return {
92 isAllowed: action.isAllowed,
93 run: () => action.run({ actionId: "action", action: "commit" }),
94 };
95 },
96 },
97 {
98 name: "publish",
99 create: () => {
100 const action = useSourceControlPublishRepositoryAction(scope);
101 return {
102 isAllowed: action.isAllowed,
103 run: () =>
104 action.run({
105 provider: SourceControlProviderKind.make("github"),
106 repository: "owner/repo",
107 visibility: "private",
108 remoteName: "origin",
109 protocol: "ssh",
110 }),
111 };
112 },
113 },
114 {
115 name: "prepare",
116 create: () => {
117 const action = usePreparePullRequestThreadAction(scope);
118 return {
119 isAllowed: action.isAllowed,
120 run: () => action.run({ reference: "1", mode: "local" }),
121 };
122 },
123 },
124] as const;
125​
126beforeEach(() => {
127 state.scopes = new Map([
128 [primary, new Set([AuthSourceControlWriteScope, AuthOrchestrationOperateScope])],
129 [secondary, new Set<string>()],
130 ]);
131 state.requests = [];
132});
133​
134describe("source control callback grants", () => {
135 it.each(cases)("$name uses the selected environment's permission", async ({ create }) => {
136 const action = create();
137 expect(action.isAllowed).toBe(false);
138 expect((await action.run())._tag).toBe("Failure");
139 expect(state.requests).toEqual([]);
140 });
141​
142 it.each(cases)("$name rejects a retained callback after revocation", async ({ create }) => {
143 state.scopes.get(secondary)!.add(AuthSourceControlWriteScope);
144 const action = create();
145 expect(action.isAllowed).toBe(true);
146 state.scopes.get(secondary)!.clear();
147 expect((await action.run())._tag).toBe("Failure");
148 expect(state.requests).toEqual([]);
149 expect(create().isAllowed).toBe(false);
150 });
151});
152​
153describe("pull request worktree attachment permission", () => {
154 it.each([
155 ["local", threadId, false, true],
156 ["worktree", undefined, false, true],
157 ["worktree", threadId, false, false],
158 ["worktree", threadId, true, true],
159 ] as const)(
160 "mode %s / thread %s / task grant %s",
161 async (mode, attachedThreadId, operate, allowed) => {
162 state.scopes.get(secondary)!.add(AuthSourceControlWriteScope);
163 if (operate) state.scopes.get(secondary)!.add(AuthOrchestrationOperateScope);
164 const result = await usePreparePullRequestThreadAction(scope).run({
165 reference: "1",
166 mode,
167 ...(attachedThreadId !== undefined ? { threadId: attachedThreadId } : {}),
168 });
169 expect(result._tag).toBe(allowed ? "Success" : "Failure");
170 expect(state.requests).toHaveLength(allowed ? 1 : 0);
171 if (result._tag === "Failure") {
172 expect(Cause.squash(result.cause)).toMatchObject({
173 requiredScope: AuthOrchestrationOperateScope,
174 });
175 }
176 },
177 );
178​
179 it.each([false, true])(
180 "reads the fresh task grant before attaching a worktree: %s",
181 async (allowed) => {
182 state.scopes.get(secondary)!.add(AuthSourceControlWriteScope);
183 if (!allowed) state.scopes.get(secondary)!.add(AuthOrchestrationOperateScope);
184 const action = usePreparePullRequestThreadAction(scope);
185 if (allowed) state.scopes.get(secondary)!.add(AuthOrchestrationOperateScope);
186 else state.scopes.get(secondary)!.delete(AuthOrchestrationOperateScope);
187 const result = await action.run({ reference: "1", mode: "worktree", threadId });
188 expect(result._tag).toBe(allowed ? "Success" : "Failure");
189 expect(state.requests).toHaveLength(allowed ? 1 : 0);
190 },
191 );
192​
193 it("still requires source-control permission when the task grant is present", async () => {
194 state.scopes.get(secondary)!.add(AuthOrchestrationOperateScope);
195 const result = await usePreparePullRequestThreadAction(scope).run({
196 reference: "1",
197 mode: "worktree",
198 threadId,
199 });
200 expect(result._tag).toBe("Failure");
201 expect(state.requests).toEqual([]);
202 });
203});