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