| 1 | import type { AtomCommand } from "@t3tools/client-runtime/state/runtime"; |
| 2 | import { |
| 3 | AuthOrchestrationOperateScope, |
| 4 | EnvironmentAuthorizationError, |
| 5 | EnvironmentId, |
| 6 | } from "@t3tools/contracts"; |
| 7 | import * as Cause from "effect/Cause"; |
| 8 | import { AsyncResult } from "effect/reactivity"; |
| 9 | import { assert, beforeEach, expect, it, vi } from "vite-plus/test"; |
| 10 | |
| 11 | const state = vi.hoisted(() => ({ |
| 12 | allowed: new Set<string>(), |
| 13 | organization: new Set<string>(), |
| 14 | readScope: vi.fn(), |
| 15 | run: vi.fn(), |
| 16 | })); |
| 17 | |
| 18 | vi.mock("react", () => ({ useCallback: (callback: unknown) => callback })); |
| 19 | vi.mock("./session", () => ({ |
| 20 | readEnvironmentScope: state.readScope, |
| 21 | readThreadOrganizationPermission: (id: string) => |
| 22 | state.organization.has(id) || state.allowed.has(id), |
| 23 | })); |
| 24 | vi.mock("./use-atom-command", () => ({ useAtomCommand: () => state.run })); |
| 25 | |
| 26 | import { useOrchestrationCommand } from "./use-orchestration-command"; |
| 27 | |
| 28 | const first = EnvironmentId.make("primary"); |
| 29 | const second = EnvironmentId.make("secondary"); |
| 30 | type Target = { environmentId: EnvironmentId; input: { threadId: string } }; |
| 31 | const command: AtomCommand<Target, string, Error> = { label: "thread mutation", run: state.run }; |
| 32 | const target = (environmentId: EnvironmentId): Target => ({ |
| 33 | environmentId, |
| 34 | input: { threadId: "thread-1" }, |
| 35 | }); |
| 36 | |
| 37 | beforeEach(() => { |
| 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 | |
| 49 | it("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 | |
| 63 | it("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 | |
| 75 | it("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 | |
| 93 | it("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 | }); |