1import { requestGuarded, runStreamGuarded } from "../rpc/client.ts";
2import { EnvironmentSupervisor } from "../connection/supervisor.ts";
3import * as SubscriptionRef from "effect/SubscriptionRef";
4import * as Option from "effect/Option";
5import * as Stream from "effect/Stream";
6import type { RpcSession } from "../rpc/session.ts";
7import { describe, expect, it } from "@effect/vitest";
8import { vi } from "vite-plus/test";
9import {
10 AuthAccessReadScope,
11 AuthAccessWriteScope,
12 AuthEnvironmentMaintainScope,
13 AuthOrchestrationOperateScope,
14 AuthOrchestrationReadScope,
15 AuthPreviewOperateScope,
16 AuthProvidersManageScope,
17 AuthSettingsWriteScope,
18 AuthSourceControlWriteScope,
19 ThreadId,
20 EnvironmentId,
21 ORCHESTRATION_V2_WS_METHODS,
22 ScheduledTaskId,
23 WS_METHODS,
24 type AuthSessionState,
25 type PeerLinkSummary,
26} from "@t3tools/contracts";
27import * as Cause from "effect/Cause";
28import * as DateTime from "effect/DateTime";
29import * as Effect from "effect/Effect";
30import * as Fiber from "effect/Fiber";
31import * as Layer from "effect/Layer";
32import { Atom, AtomRegistry, AsyncResult } from "effect/reactivity";
33import { EnvironmentRegistry } from "../connection/registry.ts";
34import { createCommandPermissions } from "./commandPermissions.ts";
35import { createEnvironmentRpcCommand } from "./runtime.ts";
36import { AVAILABLE_CONNECTION_STATE, BearerConnectionTarget } from "../connection/model.ts";
37import { createPeerLinkEnvironmentAtoms } from "./peerLinks.ts";
38
39vi.mock("./session.ts", () => ({
40 createEnvironmentSessionAtoms: () => ({ sessionStateAtom: sessions }),
41}));
42const sessions = Atom.family((_id: EnvironmentId) =>
43 Atom.make<AsyncResult.AsyncResult<AuthSessionState, string>>(AsyncResult.initial()),
44);
45const env = EnvironmentId.make("target");
46const other = EnvironmentId.make("other");
47const grant = (allowed: boolean): AuthSessionState => ({
48 authenticated: true,
49 auth: {
50 policy: "remote-reachable",
51 bootstrapMethods: [],
52 sessionMethods: [],
53 sessionCookieName: "test",
54 },
55 scopes: allowed ? [AuthOrchestrationOperateScope] : [],
56 permissions: allowed ? [AuthOrchestrationOperateScope] : [],
57});
58const runtime = Atom.runtime(
59 Layer.succeed(EnvironmentRegistry, {
60 run: (_id: EnvironmentId, effect: Effect.Effect<unknown>) => effect,
61 } as unknown as EnvironmentRegistry["Service"]),
62);
63const permissions = createCommandPermissions(runtime, WS_METHODS.scheduledTasksDelete);
64const setup = Effect.gen(function* () {
65 const registry = AtomRegistry.make();
66 registry.mount(sessions(env));
67 yield* Effect.addFinalizer(() => Effect.sync(() => registry.dispose()));
68 return registry;
69});
70
71describe("command permissions", () => {
72 it.effect("requires the destination read grant to refresh providers", () =>
73 Effect.scoped(
74 Effect.gen(function* () {
75 const registry = yield* setup;
76 const refresh = createCommandPermissions(runtime, WS_METHODS.serverRefreshProviders);
77 registry.set(sessions(env), AsyncResult.success(grant(true)));
78 expect(registry.get(refresh.permissionAtom(env))).toBe(false);
79 expect((yield* refresh.authorize(registry, env).pipe(Effect.flip))._tag).toBe(
80 "EnvironmentAuthorizationError",
81 );
82 registry.set(
83 sessions(env),
84 AsyncResult.success({
85 ...grant(false),
86 scopes: [AuthOrchestrationReadScope],
87 permissions: [AuthOrchestrationReadScope],
88 }),
89 );
90 expect(registry.get(refresh.permissionAtom(env))).toBe(true);
91 yield* refresh.authorize(registry, env);
92 registry.set(sessions(other), AsyncResult.success(grant(false)));
93 expect(registry.get(refresh.permissionAtom(other))).toBe(false);
94 expect((yield* refresh.authorize(registry, other).pipe(Effect.flip))._tag).toBe(
95 "EnvironmentAuthorizationError",
96 );
97 registry.set(sessions(env), AsyncResult.success(grant(false)));
98 expect(registry.get(refresh.permissionAtom(env))).toBe(false);
99 expect((yield* refresh.authorize(registry, env).pipe(Effect.flip))._tag).toBe(
100 "EnvironmentAuthorizationError",
101 );
102 }),
103 ),
104 );
105
106 it.effect.each([WS_METHODS.agentSessionsImport, WS_METHODS.agentSessionsImportWithProgress])(
107 "requires the destination operate grant for %s",
108 (method) =>
109 Effect.scoped(
110 Effect.gen(function* () {
111 const registry = yield* setup;
112 const imports = createCommandPermissions(runtime, method);
113 registry.set(sessions(env), AsyncResult.success(grant(false)));
114 expect(registry.get(imports.permissionAtom(env))).toBe(false);
115 expect((yield* imports.authorize(registry, env).pipe(Effect.flip))._tag).toBe(
116 "EnvironmentAuthorizationError",
117 );
118 registry.set(sessions(env), AsyncResult.success(grant(true)));
119 expect(registry.get(imports.permissionAtom(env))).toBe(true);
120 yield* imports.authorize(registry, env);
121 registry.set(sessions(other), AsyncResult.success(grant(false)));
122 expect(registry.get(imports.permissionAtom(other))).toBe(false);
123 expect((yield* imports.authorize(registry, other).pipe(Effect.flip))._tag).toBe(
124 "EnvironmentAuthorizationError",
125 );
126 }),
127 ),
128 );
129 it.effect("requires the destination preview grant to close a browser tab", () =>
130 Effect.scoped(
131 Effect.gen(function* () {
132 const registry = yield* setup;
133 const close = createCommandPermissions(runtime, WS_METHODS.previewClose);
134 registry.set(sessions(env), AsyncResult.success(grant(true)));
135 expect(registry.get(close.permissionAtom(env))).toBe(false);
136 expect((yield* close.authorize(registry, env).pipe(Effect.flip))._tag).toBe(
137 "EnvironmentAuthorizationError",
138 );
139 registry.set(
140 sessions(env),
141 AsyncResult.success({
142 ...grant(false),
143 scopes: [AuthPreviewOperateScope],
144 permissions: [AuthPreviewOperateScope],
145 }),
146 );
147 expect(registry.get(close.permissionAtom(env))).toBe(true);
148 yield* close.authorize(registry, env);
149 registry.set(sessions(other), AsyncResult.success(grant(false)));
150 expect(registry.get(close.permissionAtom(other))).toBe(false);
151 expect((yield* close.authorize(registry, other).pipe(Effect.flip))._tag).toBe(
152 "EnvironmentAuthorizationError",
153 );
154 }),
155 ),
156 );
157
158 it.effect("requires the destination provider grant to remove an incomplete connection", () =>
159 Effect.scoped(
160 Effect.gen(function* () {
161 const registry = yield* setup;
162 const remove = createCommandPermissions(runtime, WS_METHODS.providerAuthRemoveMethod);
163 registry.set(sessions(env), AsyncResult.success(grant(true)));
164 expect(registry.get(remove.permissionAtom(env))).toBe(false);
165 expect((yield* remove.authorize(registry, env).pipe(Effect.flip))._tag).toBe(
166 "EnvironmentAuthorizationError",
167 );
168 registry.set(
169 sessions(env),
170 AsyncResult.success({
171 ...grant(false),
172 scopes: [AuthProvidersManageScope],
173 permissions: [AuthProvidersManageScope],
174 }),
175 );
176 expect(registry.get(remove.permissionAtom(env))).toBe(true);
177 yield* remove.authorize(registry, env);
178 registry.set(sessions(other), AsyncResult.success(grant(false)));
179 expect(registry.get(remove.permissionAtom(other))).toBe(false);
180 }),
181 ),
182 );
183
184 it.effect.each([
185 { method: WS_METHODS.serverRunStorageCleanup, scope: AuthSettingsWriteScope },
186 { method: WS_METHODS.serverReclaimDatabaseSpace, scope: AuthEnvironmentMaintainScope },
187 ])("requires the destination $scope grant for $method", ({ method, scope }) =>
188 Effect.scoped(
189 Effect.gen(function* () {
190 const registry = yield* setup;
191 const command = createCommandPermissions(runtime, method);
192 registry.set(sessions(env), AsyncResult.success(grant(true)));
193 expect(registry.get(command.permissionAtom(env))).toBe(false);
194 expect((yield* command.authorize(registry, env).pipe(Effect.flip))._tag).toBe(
195 "EnvironmentAuthorizationError",
196 );
197 registry.set(
198 sessions(env),
199 AsyncResult.success({ ...grant(true), scopes: [scope], permissions: [scope] }),
200 );
201 expect(registry.get(command.permissionAtom(env))).toBe(true);
202 yield* command.authorize(registry, env);
203 registry.set(sessions(other), AsyncResult.success(grant(false)));
204 expect(registry.get(command.permissionAtom(other))).toBe(false);
205 }),
206 ),
207 );
208
209 it.effect("requires the destination settings grant to check OTLP endpoints", () =>
210 Effect.scoped(
211 Effect.gen(function* () {
212 const registry = yield* setup;
213 const check = createCommandPermissions(runtime, WS_METHODS.serverCheckOtlpEndpoint);
214 registry.set(sessions(env), AsyncResult.success(grant(true)));
215 expect(registry.get(check.permissionAtom(env))).toBe(false);
216 registry.set(
217 sessions(env),
218 AsyncResult.success({
219 ...grant(false),
220 scopes: [AuthSettingsWriteScope],
221 permissions: [AuthSettingsWriteScope],
222 }),
223 );
224 expect(registry.get(check.permissionAtom(env))).toBe(true);
225 yield* check.authorize(registry, env);
226 }),
227 ),
228 );
229
230 it.effect.each([WS_METHODS.scheduledTasksDelete, ORCHESTRATION_V2_WS_METHODS.stopWorkflow])(
231 "uses the target grant for both availability and execution of %s",
232 (method) =>
233 Effect.scoped(
234 Effect.gen(function* () {
235 const registry = yield* setup;
236 const permissions = createCommandPermissions(runtime, method);
237 registry.set(sessions(env), AsyncResult.success(grant(true)));
238 expect(registry.get(permissions.permissionAtom(env))).toBe(true);
239 expect(permissions.permissionAtom(env)).toBe(permissions.permissionAtom(env));
240 yield* permissions.authorize(registry, env);
241 registry.set(sessions(other), AsyncResult.success(grant(false)));
242 expect(registry.get(permissions.permissionAtom(other))).toBe(false);
243 const denied = yield* permissions.authorize(registry, other).pipe(Effect.flip);
244 expect(denied._tag).toBe("EnvironmentAuthorizationError");
245 }),
246 ),
247 );
248 it.effect("waits for an initial grant", () =>
249 Effect.scoped(
250 Effect.gen(function* () {
251 const registry = yield* setup;
252 const fiber = yield* permissions.authorize(registry, env).pipe(Effect.forkChild);
253 yield* Effect.yieldNow;
254 registry.set(sessions(env), AsyncResult.success(grant(true)));
255 yield* Fiber.join(fiber);
256 }),
257 ),
258 );
259 it.effect(
260 "denies failed and unauthenticated sessions, but accepts a cached refreshing grant",
261 () =>
262 Effect.scoped(
263 Effect.gen(function* () {
264 const registry = yield* setup;
265 registry.set(sessions(env), AsyncResult.failure(Cause.fail("offline")));
266 expect((yield* permissions.authorize(registry, env).pipe(Effect.flip))._tag).toBe(
267 "EnvironmentAuthorizationError",
268 );
269 registry.set(
270 sessions(env),
271 AsyncResult.success({ ...grant(true), authenticated: false }),
272 );
273 expect((yield* permissions.authorize(registry, env).pipe(Effect.flip))._tag).toBe(
274 "EnvironmentAuthorizationError",
275 );
276 registry.set(sessions(env), AsyncResult.waiting(AsyncResult.success(grant(true))));
277 yield* permissions.authorize(registry, env);
278 }),
279 ),
280 );
281 it("rechecks permission after waiting in a serial command lane", async () => {
282 const registry = AtomRegistry.make();
283 const unmount = registry.mount(sessions(env));
284 registry.set(sessions(env), AsyncResult.success(grant(true)));
285 let release!: () => void;
286 let started!: () => void;
287 const entered = new Promise<void>((resolve) => {
288 started = resolve;
289 });
290 const gate = new Promise<void>((resolve) => {
291 release = resolve;
292 });
293 let calls = 0;
294 const command = createEnvironmentRpcCommand(runtime, {
295 label: "test.delete",
296 tag: WS_METHODS.scheduledTasksDelete,
297 concurrency: { mode: "serial", key: () => "target" },
298 execute: () =>
299 Effect.promise(async () => {
300 calls++;
301 started();
302 await gate;
303 return { id: ScheduledTaskId.make("task") };
304 }),
305 });
306 const target = { environmentId: env, input: { id: ScheduledTaskId.make("task") } };
307 try {
308 const first = command.run(registry, target);
309 await entered;
310 const second = command.run(registry, target);
311 registry.set(sessions(env), AsyncResult.success(grant(false)));
312 release();
313 expect((await first)._tag).toBe("Success");
314 expect((await second)._tag).toBe("Failure");
315 expect(calls).toBe(1);
316 } finally {
317 unmount();
318 registry.dispose();
319 }
320 });
321});
322
323it.effect(
324 "requires source control alone for git, and both grants when attaching a worktree to a thread",
325 () =>
326 Effect.scoped(
327 Effect.gen(function* () {
328 const registry = yield* setup;
329 registry.set(sessions(env), AsyncResult.success(grant(true)));
330 const git = createCommandPermissions(runtime, WS_METHODS.vcsInit);
331 expect((yield* git.authorize(registry, env).pipe(Effect.flip)).requiredPermission).toBe(
332 AuthSourceControlWriteScope,
333 );
334 registry.set(
335 sessions(env),
336 AsyncResult.success({
337 ...grant(false),
338 scopes: [AuthSourceControlWriteScope],
339 permissions: [AuthSourceControlWriteScope],
340 }),
341 );
342 yield* git.authorize(registry, env);
343 const prepare = createCommandPermissions(runtime, WS_METHODS.gitPreparePullRequestThread);
344 const input = {
345 cwd: "/repo",
346 reference: "123",
347 mode: "worktree",
348 threadId: ThreadId.make("thread"),
349 };
350 expect(registry.get(prepare.permissionAtom(env, input))).toBe(false);
351 expect(
352 (yield* prepare.authorize(registry, env, input).pipe(Effect.flip)).requiredScope,
353 ).toBe(AuthOrchestrationOperateScope);
354 yield* prepare.authorize(registry, env, { ...input, threadId: undefined });
355 registry.set(
356 sessions(env),
357 AsyncResult.success({
358 ...grant(true),
359 scopes: [AuthSourceControlWriteScope, AuthOrchestrationOperateScope],
360 permissions: [AuthSourceControlWriteScope, AuthOrchestrationOperateScope],
361 }),
362 );
363 expect(registry.get(prepare.permissionAtom(env, input))).toBe(true);
364 yield* prepare.authorize(registry, env, input);
365 }),
366 ),
367);
368
369it.effect.each(["commit", "push", "commit_push", "create_pr", "commit_push_pr"] as const)(
370 "%s needs thread read permission only when generating a PR from a thread",
371 (action) =>
372 Effect.scoped(
373 Effect.gen(function* () {
374 const registry = yield* setup;
375 const command = createCommandPermissions(runtime, WS_METHODS.gitRunStackedAction);
376 const input = { actionId: "action", cwd: "/repo", action };
377 const withThread = { ...input, threadId: ThreadId.make("thread") };
378 const session = {
379 ...grant(false),
380 scopes: [AuthSourceControlWriteScope],
381 permissions: [AuthSourceControlWriteScope],
382 };
383 registry.set(sessions(env), AsyncResult.success(session));
384 expect(registry.get(command.permissionAtom(env, input))).toBe(true);
385 yield* command.authorize(registry, env, input);
386 const readsThread = action === "create_pr" || action === "commit_push_pr";
387 expect(registry.get(command.permissionAtom(env, withThread))).toBe(!readsThread);
388 if (readsThread) {
389 expect(
390 yield* command.authorize(registry, env, withThread).pipe(Effect.flip),
391 ).toMatchObject({ requiredPermission: AuthOrchestrationReadScope });
392 } else {
393 yield* command.authorize(registry, env, withThread);
394 }
395 registry.set(
396 sessions(env),
397 AsyncResult.success({
398 ...session,
399 scopes: [AuthSourceControlWriteScope, AuthOrchestrationReadScope],
400 permissions: [AuthSourceControlWriteScope, AuthOrchestrationReadScope],
401 }),
402 );
403 expect(registry.get(command.permissionAtom(env, withThread))).toBe(true);
404 yield* command.authorize(registry, env, withThread);
405 }),
406 ),
407);
408
409it.effect(
410 "refreshes linked environments after permitted writes and blocks a read-only session",
411 () =>
412 Effect.scoped(
413 Effect.gen(function* () {
414 const registry = yield* setup;
415 const peer: PeerLinkSummary = {
416 environmentId: EnvironmentId.make("peer"),
417 label: "Box",
418 urls: ["https://box.example.test"],
419 access: "auto",
420 linkedAt: DateTime.makeUnsafe("2026-10-01T12:00:00Z"),
421 expiresAt: DateTime.makeUnsafe("2026-10-31T12:00:00Z"),
422 lastReachedAt: null,
423 lastError: null,
424 status: "reachable",
425 };
426 let links: ReadonlyArray<PeerLinkSummary> = [];
427 const client = {
428 [WS_METHODS.peerLinksList]: () => Effect.sync(() => ({ links })),
429 [WS_METHODS.peerLinksLink]: vi.fn(() =>
430 Effect.sync(() => {
431 links = [peer];
432 return peer;
433 }),
434 ),
435 [WS_METHODS.peerLinksUnlink]: vi.fn(() =>
436 Effect.sync(() => {
437 links = [];
438 return { removed: true };
439 }),
440 ),
441 };
442 const supervisor = {
443 target: new BearerConnectionTarget({
444 environmentId: env,
445 label: "Test",
446 connectionId: "test",
447 }),
448 session: yield* SubscriptionRef.make(Option.some({ client } as unknown as RpcSession)),
449 state: yield* SubscriptionRef.make({
450 ...AVAILABLE_CONNECTION_STATE,
451 phase: "connected" as const,
452 }),
453 } as EnvironmentSupervisor["Service"];
454 const peerRuntime = Atom.runtime(
455 Layer.succeed(EnvironmentRegistry, {
456 run: (id, effect) => {
457 expect(id).toBe(env);
458 return Effect.provideService(effect, EnvironmentSupervisor, supervisor);
459 },
460 followStream: (id, stream) => {
461 expect(id).toBe(env);
462 return Stream.provideService(stream, EnvironmentSupervisor, supervisor);
463 },
464 } as EnvironmentRegistry["Service"]),
465 );
466 const peerLinks = createPeerLinkEnvironmentAtoms(peerRuntime);
467 const list = peerLinks.list({ environmentId: env, input: {} });
468 registry.mount(list);
469 const readLinks = AtomRegistry.getResult(registry, list, { suspendOnWaiting: true });
470 const setAccess = (writable: boolean) =>
471 registry.set(
472 sessions(env),
473 AsyncResult.success({
474 ...grant(false),
475 scopes: [AuthAccessReadScope, ...(writable ? [AuthAccessWriteScope] : [])],
476 permissions: [AuthAccessReadScope, ...(writable ? [AuthAccessWriteScope] : [])],
477 }),
478 );
479 registry.mount(sessions(other));
480 registry.set(
481 sessions(other),
482 AsyncResult.success({
483 ...grant(false),
484 scopes: [AuthAccessWriteScope],
485 permissions: [AuthAccessWriteScope],
486 }),
487 );
488 setAccess(false);
489 expect(yield* readLinks).toEqual({ links: [] });
490 const linkInput = { url: peer.urls[0]!, pairingCode: "test-code", access: peer.access };
491 const link = Effect.promise(() =>
492 peerLinks.link.run(registry, { environmentId: env, input: linkInput }),
493 );
494 const unlink = Effect.promise(() =>
495 peerLinks.unlink.run(registry, {
496 environmentId: env,
497 input: { environmentId: peer.environmentId },
498 }),
499 );
500 expect(registry.get(peerLinks.link.permissionAtom(env))).toBe(false);
501 const deniedLink = yield* link;
502 expect(deniedLink._tag).toBe("Failure");
503 if (deniedLink._tag === "Failure") {
504 expect(Cause.squash(deniedLink.cause)).toMatchObject({
505 _tag: "EnvironmentAuthorizationError",
506 requiredPermission: AuthAccessWriteScope,
507 });
508 }
509 expect(client[WS_METHODS.peerLinksLink]).not.toHaveBeenCalled();
510 setAccess(true);
511 expect(registry.get(peerLinks.link.permissionAtom(env))).toBe(true);
512 expect((yield* link)._tag).toBe("Success");
513 expect(client[WS_METHODS.peerLinksLink]).toHaveBeenCalledExactlyOnceWith(linkInput);
514 expect(yield* readLinks).toEqual({ links: [peer] });
515
516 setAccess(false);
517 expect(registry.get(peerLinks.unlink.permissionAtom(env))).toBe(false);
518 const deniedUnlink = yield* unlink;
519 expect(deniedUnlink._tag).toBe("Failure");
520 if (deniedUnlink._tag === "Failure") {
521 expect(Cause.squash(deniedUnlink.cause)).toMatchObject({
522 _tag: "EnvironmentAuthorizationError",
523 requiredPermission: AuthAccessWriteScope,
524 });
525 }
526 expect(client[WS_METHODS.peerLinksUnlink]).not.toHaveBeenCalled();
527 expect(yield* readLinks).toEqual({ links: [peer] });
528 setAccess(true);
529 expect(registry.get(peerLinks.unlink.permissionAtom(env))).toBe(true);
530 expect((yield* unlink)._tag).toBe("Success");
531 expect(client[WS_METHODS.peerLinksUnlink]).toHaveBeenCalledExactlyOnceWith({
532 environmentId: peer.environmentId,
533 });
534 expect(yield* readLinks).toEqual({ links: [] });
535 }),
536 ),
537);
538
539it.effect("requires access management to answer a link request", () =>
540 Effect.scoped(
541 Effect.gen(function* () {
542 const registry = yield* setup;
543 registry.set(sessions(env), AsyncResult.success(grant(true)));
544 const answer = createCommandPermissions(runtime, WS_METHODS.peerLinksAnswerRequest);
545 expect(registry.get(answer.permissionAtom(env))).toBe(false);
546 expect((yield* answer.authorize(registry, env).pipe(Effect.flip)).requiredPermission).toBe(
547 AuthAccessWriteScope,
548 );
549 registry.set(
550 sessions(env),
551 AsyncResult.success({
552 ...grant(false),
553 scopes: [AuthAccessWriteScope],
554 permissions: [AuthAccessWriteScope],
555 }),
556 );
557 expect(registry.get(answer.permissionAtom(env))).toBe(true);
558 yield* answer.authorize(registry, env);
559 }),
560 ),
561);
562
563it.effect("honors exact empty permissions and preserves legacy parent grants", () =>
564 Effect.scoped(
565 Effect.gen(function* () {
566 const registry = yield* setup;
567 const git = createCommandPermissions(runtime, WS_METHODS.vcsInit);
568 registry.set(sessions(env), AsyncResult.success({ ...grant(true), permissions: [] }));
569 expect(registry.get(git.permissionAtom(env))).toBe(false);
570 const denied = yield* git.authorize(registry, env).pipe(Effect.flip);
571 expect(denied).toMatchObject({
572 requiredPermission: AuthSourceControlWriteScope,
573 requiredScope: AuthOrchestrationOperateScope,
574 });
575 const { permissions: _exact, ...legacy } = grant(true);
576 registry.set(sessions(env), AsyncResult.success(legacy));
577 expect(registry.get(git.permissionAtom(env))).toBe(true);
578 yield* git.authorize(registry, env);
579 }),
580 ),
581);
582
583it.effect("rejects protected unary and streamed RPCs outside a guarded command", () =>
584 Effect.gen(function* () {
585 let writes = 0;
586 const session = {
587 client: {
588 [WS_METHODS.scheduledTasksDelete]: () =>
589 Effect.sync(() => {
590 writes++;
591 return { id: ScheduledTaskId.make("task") };
592 }),
593 [WS_METHODS.gitRunStackedAction]: () =>
594 Stream.fromEffect(
595 Effect.sync(() => {
596 writes++;
597 }),
598 ),
599 },
600 } as unknown as RpcSession;
601 const supervisor = {
602 target: { environmentId: env, label: "target" },
603 session: yield* SubscriptionRef.make(Option.some(session)),
604 } as unknown as EnvironmentSupervisor["Service"];
605 const unary = yield* requestGuarded(WS_METHODS.scheduledTasksDelete, {
606 id: ScheduledTaskId.make("task"),
607 }).pipe(Effect.provideService(EnvironmentSupervisor, supervisor), Effect.flip);
608 expect(unary._tag).toBe("EnvironmentAuthorizationError");
609 const streamed = yield* runStreamGuarded(WS_METHODS.gitRunStackedAction, {
610 actionId: "test-action",
611 cwd: "/repo",
612 action: "commit",
613 }).pipe(Stream.runDrain, Effect.provideService(EnvironmentSupervisor, supervisor), Effect.flip);
614 expect(streamed._tag).toBe("EnvironmentAuthorizationError");
615 expect(writes).toBe(0);
616 }),
617);