1import { EnvironmentId, ThreadId, ScheduledTaskId, type ScheduledTask } from "@t3tools/contracts";
2import { act, create, type ReactTestRenderer } from "react-test-renderer";
3import { afterEach, beforeEach, expect, it, vi } from "vite-plus/test";
4
5const state = vi.hoisted(() => ({ allowed: true, toggle: vi.fn(), run: vi.fn() }));
6vi.mock("../../state/session", () => ({
7 useEnvironmentScope: () => state.allowed,
8 readEnvironmentScope: () => state.allowed,
9}));
10vi.mock("@effect/atom-react", () => ({ useAtomValue: () => state.allowed }));
11vi.mock("@tanstack/react-router", () => ({ useNavigate: () => vi.fn() }));
12vi.mock("../settings/ScheduledTasksSettings", () => ({
13 relativeLabel: () => "later",
14 scheduleLabel: () => "daily",
15}));
16vi.mock("../../state/server", () => ({
17 serverEnvironment: {
18 scheduledTasksLive: () => null,
19 setScheduledTaskEnabled: "toggle",
20 runScheduledTaskNow: { permissionAtom: () => null },
21 },
22}));
23vi.mock("../../state/use-atom-command", () => ({
24 useAtomCommand: (command: string) => (command === "toggle" ? state.toggle : state.run),
25}));
26vi.mock("../../state/query", () => ({
27 useEnvironmentQuery: () => ({ data: { tasks: [task] }, error: null }),
28}));
29vi.mock("../ui/tooltip", () => ({
30 Tooltip: ({ children }: { children: React.ReactNode }) => children,
31 TooltipTrigger: ({ render }: { render: React.ReactNode }) => render,
32 TooltipPopup: () => null,
33}));
34vi.mock("../ui/toast", () => ({
35 toastManager: { add: vi.fn() },
36 stackedThreadToast: (x: unknown) => x,
37}));
38
39import { ThreadAutomationsPanel } from "./ThreadAutomationsPanel";
40const environmentId = EnvironmentId.make("remote");
41const threadId = ThreadId.make("thread");
42const 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;
50let renderer: ReactTestRenderer;
51beforeEach(() => {
52 state.allowed = true;
53 state.toggle.mockReset().mockResolvedValue({ _tag: "Success" });
54 state.run.mockReset().mockResolvedValue({ _tag: "Success" });
55});
56afterEach(async () => {
57 if (renderer) await act(async () => renderer.unmount());
58});
59
60it.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
72it("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});