| 1 | import { EnvironmentId, ThreadId, ScheduledTaskId, type ScheduledTask } from "@t3tools/contracts"; |
| 2 | import { act, create, type ReactTestRenderer } from "react-test-renderer"; |
| 3 | import { afterEach, beforeEach, expect, it, vi } from "vite-plus/test"; |
| 4 | |
| 5 | const state = vi.hoisted(() => ({ allowed: true, toggle: vi.fn(), run: vi.fn() })); |
| 6 | vi.mock("../../state/session", () => ({ |
| 7 | useEnvironmentScope: () => state.allowed, |
| 8 | readEnvironmentScope: () => state.allowed, |
| 9 | })); |
| 10 | vi.mock("@effect/atom-react", () => ({ useAtomValue: () => state.allowed })); |
| 11 | vi.mock("@tanstack/react-router", () => ({ useNavigate: () => vi.fn() })); |
| 12 | vi.mock("../settings/ScheduledTasksSettings", () => ({ |
| 13 | relativeLabel: () => "later", |
| 14 | scheduleLabel: () => "daily", |
| 15 | })); |
| 16 | vi.mock("../../state/server", () => ({ |
| 17 | serverEnvironment: { |
| 18 | scheduledTasksLive: () => null, |
| 19 | setScheduledTaskEnabled: "toggle", |
| 20 | runScheduledTaskNow: { permissionAtom: () => null }, |
| 21 | }, |
| 22 | })); |
| 23 | vi.mock("../../state/use-atom-command", () => ({ |
| 24 | useAtomCommand: (command: string) => (command === "toggle" ? state.toggle : state.run), |
| 25 | })); |
| 26 | vi.mock("../../state/query", () => ({ |
| 27 | useEnvironmentQuery: () => ({ data: { tasks: [task] }, error: null }), |
| 28 | })); |
| 29 | vi.mock("../ui/tooltip", () => ({ |
| 30 | Tooltip: ({ children }: { children: React.ReactNode }) => children, |
| 31 | TooltipTrigger: ({ render }: { render: React.ReactNode }) => render, |
| 32 | TooltipPopup: () => null, |
| 33 | })); |
| 34 | vi.mock("../ui/toast", () => ({ |
| 35 | toastManager: { add: vi.fn() }, |
| 36 | stackedThreadToast: (x: unknown) => x, |
| 37 | })); |
| 38 | |
| 39 | import { ThreadAutomationsPanel } from "./ThreadAutomationsPanel"; |
| 40 | const environmentId = EnvironmentId.make("remote"); |
| 41 | const threadId = ThreadId.make("thread"); |
| 42 | const task = { |
| 43 | id: ScheduledTaskId.make("task"), |
| 44 | threadId, |
| 45 | title: "Daily review", |
| 46 | enabled: true, |
| 47 | schedule: { type: "interval", everyMs: 60000 }, |
| 48 | lastRunStatus: "never", |
| 49 | } as ScheduledTask; |
| 50 | let renderer: ReactTestRenderer; |
| 51 | beforeEach(() => { |
| 52 | state.allowed = true; |
| 53 | state.toggle.mockReset().mockResolvedValue({ _tag: "Success" }); |
| 54 | state.run.mockReset().mockResolvedValue({ _tag: "Success" }); |
| 55 | }); |
| 56 | afterEach(async () => { |
| 57 | if (renderer) await act(async () => renderer.unmount()); |
| 58 | }); |
| 59 | |
| 60 | it.each([true, false])("runs scheduled work only with task permission (%s)", async (allowed) => { |
| 61 | state.allowed = allowed; |
| 62 | await act(async () => { |
| 63 | renderer = create(<ThreadAutomationsPanel environmentId={environmentId} threadId={threadId} />); |
| 64 | }); |
| 65 | const run = renderer.root.findAllByProps({ "aria-label": "Run Daily review now" })[0]!; |
| 66 | await act(async () => { |
| 67 | await run.props.onClick(); |
| 68 | }); |
| 69 | expect(state.run).toHaveBeenCalledTimes(allowed ? 1 : 0); |
| 70 | }); |
| 71 | |
| 72 | it("rejects a retained toggle after task permission is revoked", async () => { |
| 73 | await act(async () => { |
| 74 | renderer = create(<ThreadAutomationsPanel environmentId={environmentId} threadId={threadId} />); |
| 75 | }); |
| 76 | const toggle = renderer.root |
| 77 | .findAllByProps({ "aria-label": "Pause Daily review" }) |
| 78 | .find((node) => node.props.onCheckedChange)!; |
| 79 | state.allowed = false; |
| 80 | await act(async () => { |
| 81 | await toggle.props.onCheckedChange(false); |
| 82 | }); |
| 83 | expect(state.toggle).not.toHaveBeenCalled(); |
| 84 | }); |