1import type { AtomCommand } from "@t3tools/client-runtime/state/runtime";
2import {
3 AuthOrchestrationOperateScope,
4 EnvironmentAuthorizationError,
5 EnvironmentId,
6} from "@t3tools/contracts";
7import * as Cause from "effect/Cause";
8import { AsyncResult } from "effect/reactivity";
9import { assert, beforeEach, expect, it, vi } from "vite-plus/test";
10
11const state = vi.hoisted(() => ({
12 allowed: new Set<string>(),
13 organization: new Set<string>(),
14 readScope: vi.fn(),
15 run: vi.fn(),
16}));
17
18vi.mock("react", () => ({ useCallback: (callback: unknown) => callback }));
19vi.mock("./session", () => ({
20 readEnvironmentScope: state.readScope,
21 readThreadOrganizationPermission: (id: string) =>
22 state.organization.has(id) || state.allowed.has(id),
23}));
24vi.mock("./use-atom-command", () => ({ useAtomCommand: () => state.run }));
25
26import { useOrchestrationCommand } from "./use-orchestration-command";
27
28const first = EnvironmentId.make("primary");
29const second = EnvironmentId.make("secondary");
30type Target = { environmentId: EnvironmentId; input: { threadId: string } };
31const command: AtomCommand<Target, string, Error> = { label: "thread mutation", run: state.run };
32const target = (environmentId: EnvironmentId): Target => ({
33 environmentId,
34 input: { threadId: "thread-1" },
35});
36
37beforeEach(() => {
38 state.allowed.clear();
39 state.organization.clear();
40 state.run.mockReset().mockResolvedValue(AsyncResult.success("receipt"));
41 state.readScope
42 .mockReset()
43 .mockImplementation(
44 (environmentId, scope) =>
45 scope === AuthOrchestrationOperateScope && state.allowed.has(environmentId),
46 );
47});
48
49it("checks the command's environment instead of borrowing the primary grant", async () => {
50 state.allowed.add(first);
51 const mutate = useOrchestrationCommand(command, { reportFailure: false });
52 const result = await mutate(target(second));
53 assert(result._tag === "Failure");
54 const error = Cause.squash(result.cause);
55 expect(error).toBeInstanceOf(EnvironmentAuthorizationError);
56 expect(error).toMatchObject({ requiredScope: AuthOrchestrationOperateScope });
57 expect(state.run).not.toHaveBeenCalled();
58
59 expect(await mutate(target(first))).toMatchObject({ _tag: "Success", value: "receipt" });
60 expect(state.run).toHaveBeenCalledWith(target(first));
61});
62
63it("a retained callback follows both revocation and a new grant without remounting", async () => {
64 state.allowed.add(second);
65 const mutate = useOrchestrationCommand(command);
66 state.allowed.delete(second);
67 expect((await mutate(target(second)))._tag).toBe("Failure");
68 expect(state.run).not.toHaveBeenCalled();
69
70 state.allowed.add(second);
71 expect((await mutate(target(second)))._tag).toBe("Success");
72 expect(state.run).toHaveBeenCalledTimes(1);
73});
74
75it("does not dispatch a later mutation after access changes while awaiting a receipt", async () => {
76 state.allowed.add(second);
77 let finish: (() => void) | undefined;
78 state.run.mockImplementationOnce(
79 () =>
80 new Promise((resolve) => {
81 finish = () => resolve(AsyncResult.success("first receipt"));
82 }),
83 );
84 const mutate = useOrchestrationCommand(command);
85 const pending = mutate(target(second));
86 state.allowed.delete(second);
87 finish!();
88 expect((await pending)._tag).toBe("Success");
89 expect((await mutate(target(second)))._tag).toBe("Failure");
90 expect(state.run).toHaveBeenCalledTimes(1);
91});
92
93it("accepts the central organization grant only for organization commands and rechecks revocation", async () => {
94 state.organization.add(second);
95 const organize = useOrchestrationCommand(command, { organization: true, reportFailure: false });
96 const execute = useOrchestrationCommand(command, { reportFailure: false });
97 expect((await organize(target(second)))._tag).toBe("Success");
98 expect((await execute(target(second)))._tag).toBe("Failure");
99 state.organization.clear();
100 expect((await organize(target(second)))._tag).toBe("Failure");
101 expect(state.run).toHaveBeenCalledTimes(1);
102});