@stigmer/react 3.5.3 → 3.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (283) hide show
  1. package/agent/AgentPicker.d.ts +10 -1
  2. package/agent/AgentPicker.d.ts.map +1 -1
  3. package/agent/AgentPicker.js +2 -2
  4. package/agent/AgentPicker.js.map +1 -1
  5. package/billing/CreditLedgerTable.d.ts.map +1 -1
  6. package/billing/CreditLedgerTable.js +2 -7
  7. package/billing/CreditLedgerTable.js.map +1 -1
  8. package/channel/AgentChannelsPanel.d.ts.map +1 -1
  9. package/channel/AgentChannelsPanel.js +22 -5
  10. package/channel/AgentChannelsPanel.js.map +1 -1
  11. package/channel/ChannelTemplatesDialog.d.ts +53 -0
  12. package/channel/ChannelTemplatesDialog.d.ts.map +1 -0
  13. package/channel/ChannelTemplatesDialog.js +150 -0
  14. package/channel/ChannelTemplatesDialog.js.map +1 -0
  15. package/channel/index.d.ts +8 -0
  16. package/channel/index.d.ts.map +1 -1
  17. package/channel/index.js +4 -0
  18. package/channel/index.js.map +1 -1
  19. package/channel/providers.d.ts +12 -0
  20. package/channel/providers.d.ts.map +1 -1
  21. package/channel/providers.js +14 -2
  22. package/channel/providers.js.map +1 -1
  23. package/channel/templatePresentation.d.ts +37 -0
  24. package/channel/templatePresentation.d.ts.map +1 -0
  25. package/channel/templatePresentation.js +69 -0
  26. package/channel/templatePresentation.js.map +1 -0
  27. package/channel/useChannelTemplateList.d.ts +64 -0
  28. package/channel/useChannelTemplateList.d.ts.map +1 -0
  29. package/channel/useChannelTemplateList.js +69 -0
  30. package/channel/useChannelTemplateList.js.map +1 -0
  31. package/channel/useChannelTemplateReadiness.d.ts +53 -0
  32. package/channel/useChannelTemplateReadiness.d.ts.map +1 -0
  33. package/channel/useChannelTemplateReadiness.js +42 -0
  34. package/channel/useChannelTemplateReadiness.js.map +1 -0
  35. package/composer/ComposerToolbar.d.ts +6 -1
  36. package/composer/ComposerToolbar.d.ts.map +1 -1
  37. package/composer/ComposerToolbar.js +2 -2
  38. package/composer/ComposerToolbar.js.map +1 -1
  39. package/composer/SessionComposer.d.ts +9 -0
  40. package/composer/SessionComposer.d.ts.map +1 -1
  41. package/composer/SessionComposer.js +13 -2
  42. package/composer/SessionComposer.js.map +1 -1
  43. package/dashboard/useDashboardFailedRuns.d.ts.map +1 -1
  44. package/dashboard/useDashboardFailedRuns.js +2 -0
  45. package/dashboard/useDashboardFailedRuns.js.map +1 -1
  46. package/execution/useCreateAgentExecution.d.ts +14 -0
  47. package/execution/useCreateAgentExecution.d.ts.map +1 -1
  48. package/execution/useCreateAgentExecution.js +5 -0
  49. package/execution/useCreateAgentExecution.js.map +1 -1
  50. package/index.d.ts +4 -2
  51. package/index.d.ts.map +1 -1
  52. package/index.js +10 -1
  53. package/index.js.map +1 -1
  54. package/internal/Pagination.d.ts +15 -0
  55. package/internal/Pagination.d.ts.map +1 -0
  56. package/internal/Pagination.js +20 -0
  57. package/internal/Pagination.js.map +1 -0
  58. package/library/useExportResource.d.ts +4 -3
  59. package/library/useExportResource.d.ts.map +1 -1
  60. package/library/useExportResource.js +1 -1
  61. package/library/useExportResource.js.map +1 -1
  62. package/models/ModelSelector.d.ts +26 -1
  63. package/models/ModelSelector.d.ts.map +1 -1
  64. package/models/ModelSelector.js +27 -6
  65. package/models/ModelSelector.js.map +1 -1
  66. package/models/index.d.ts +2 -0
  67. package/models/index.d.ts.map +1 -1
  68. package/models/index.js +1 -0
  69. package/models/index.js.map +1 -1
  70. package/models/registry.d.ts +10 -0
  71. package/models/registry.d.ts.map +1 -1
  72. package/models/registry.js +3 -0
  73. package/models/registry.js.map +1 -1
  74. package/models/service-tier.d.ts +27 -0
  75. package/models/service-tier.d.ts.map +1 -0
  76. package/models/service-tier.js +34 -0
  77. package/models/service-tier.js.map +1 -0
  78. package/organization/OrgProvider.d.ts +11 -4
  79. package/organization/OrgProvider.d.ts.map +1 -1
  80. package/organization/OrgProvider.js +28 -4
  81. package/organization/OrgProvider.js.map +1 -1
  82. package/package.json +4 -4
  83. package/resource-detail/useDeleteResource.d.ts +1 -1
  84. package/resource-detail/useDeleteResource.d.ts.map +1 -1
  85. package/resource-detail/useDeleteResource.js +5 -0
  86. package/resource-detail/useDeleteResource.js.map +1 -1
  87. package/resource-workbench/components/ResourceWorkbench.d.ts +10 -1
  88. package/resource-workbench/components/ResourceWorkbench.d.ts.map +1 -1
  89. package/resource-workbench/components/ResourceWorkbench.js +6 -2
  90. package/resource-workbench/components/ResourceWorkbench.js.map +1 -1
  91. package/resource-workbench/hooks/useResourceCollection.d.ts +8 -0
  92. package/resource-workbench/hooks/useResourceCollection.d.ts.map +1 -1
  93. package/resource-workbench/hooks/useResourceCollection.js +2 -2
  94. package/resource-workbench/hooks/useResourceCollection.js.map +1 -1
  95. package/schedule/CadenceField.d.ts +31 -0
  96. package/schedule/CadenceField.d.ts.map +1 -0
  97. package/schedule/CadenceField.js +116 -0
  98. package/schedule/CadenceField.js.map +1 -0
  99. package/schedule/ScheduleDetailView.d.ts +81 -0
  100. package/schedule/ScheduleDetailView.d.ts.map +1 -0
  101. package/schedule/ScheduleDetailView.js +648 -0
  102. package/schedule/ScheduleDetailView.js.map +1 -0
  103. package/schedule/ScheduleForm.d.ts +46 -0
  104. package/schedule/ScheduleForm.d.ts.map +1 -0
  105. package/schedule/ScheduleForm.js +214 -0
  106. package/schedule/ScheduleForm.js.map +1 -0
  107. package/schedule/ScheduleRowActions.d.ts +20 -0
  108. package/schedule/ScheduleRowActions.d.ts.map +1 -0
  109. package/schedule/ScheduleRowActions.js +54 -0
  110. package/schedule/ScheduleRowActions.js.map +1 -0
  111. package/schedule/ScheduleRunsTable.d.ts +72 -0
  112. package/schedule/ScheduleRunsTable.d.ts.map +1 -0
  113. package/schedule/ScheduleRunsTable.js +161 -0
  114. package/schedule/ScheduleRunsTable.js.map +1 -0
  115. package/schedule/TimeZoneField.d.ts +32 -0
  116. package/schedule/TimeZoneField.d.ts.map +1 -0
  117. package/schedule/TimeZoneField.js +104 -0
  118. package/schedule/TimeZoneField.js.map +1 -0
  119. package/schedule/cadence.d.ts +95 -0
  120. package/schedule/cadence.d.ts.map +1 -0
  121. package/schedule/cadence.js +242 -0
  122. package/schedule/cadence.js.map +1 -0
  123. package/schedule/index.d.ts +37 -0
  124. package/schedule/index.d.ts.map +1 -0
  125. package/schedule/index.js +19 -0
  126. package/schedule/index.js.map +1 -0
  127. package/schedule/scheduleColumns.d.ts +26 -0
  128. package/schedule/scheduleColumns.d.ts.map +1 -0
  129. package/schedule/scheduleColumns.js +77 -0
  130. package/schedule/scheduleColumns.js.map +1 -0
  131. package/schedule/scheduleListFn.d.ts +50 -0
  132. package/schedule/scheduleListFn.d.ts.map +1 -0
  133. package/schedule/scheduleListFn.js +65 -0
  134. package/schedule/scheduleListFn.js.map +1 -0
  135. package/schedule/scheduleState.d.ts +43 -0
  136. package/schedule/scheduleState.d.ts.map +1 -0
  137. package/schedule/scheduleState.js +67 -0
  138. package/schedule/scheduleState.js.map +1 -0
  139. package/schedule/useCreateSchedule.d.ts +57 -0
  140. package/schedule/useCreateSchedule.d.ts.map +1 -0
  141. package/schedule/useCreateSchedule.js +61 -0
  142. package/schedule/useCreateSchedule.js.map +1 -0
  143. package/schedule/useResumeSchedule.d.ts +30 -0
  144. package/schedule/useResumeSchedule.d.ts.map +1 -0
  145. package/schedule/useResumeSchedule.js +45 -0
  146. package/schedule/useResumeSchedule.js.map +1 -0
  147. package/schedule/useSchedule.d.ts +39 -0
  148. package/schedule/useSchedule.d.ts.map +1 -0
  149. package/schedule/useSchedule.js +44 -0
  150. package/schedule/useSchedule.js.map +1 -0
  151. package/schedule/useScheduleCount.d.ts +38 -0
  152. package/schedule/useScheduleCount.d.ts.map +1 -0
  153. package/schedule/useScheduleCount.js +30 -0
  154. package/schedule/useScheduleCount.js.map +1 -0
  155. package/schedule/useScheduleList.d.ts +48 -0
  156. package/schedule/useScheduleList.d.ts.map +1 -0
  157. package/schedule/useScheduleList.js +43 -0
  158. package/schedule/useScheduleList.js.map +1 -0
  159. package/schedule/useScheduleRuns.d.ts +44 -0
  160. package/schedule/useScheduleRuns.d.ts.map +1 -0
  161. package/schedule/useScheduleRuns.js +48 -0
  162. package/schedule/useScheduleRuns.js.map +1 -0
  163. package/schedule/useSetScheduleEnabled.d.ts +31 -0
  164. package/schedule/useSetScheduleEnabled.d.ts.map +1 -0
  165. package/schedule/useSetScheduleEnabled.js +53 -0
  166. package/schedule/useSetScheduleEnabled.js.map +1 -0
  167. package/schedule/useTriggerSchedule.d.ts +41 -0
  168. package/schedule/useTriggerSchedule.d.ts.map +1 -0
  169. package/schedule/useTriggerSchedule.js +62 -0
  170. package/schedule/useTriggerSchedule.js.map +1 -0
  171. package/schedule/useUpdateScheduleSpec.d.ts +49 -0
  172. package/schedule/useUpdateScheduleSpec.d.ts.map +1 -0
  173. package/schedule/useUpdateScheduleSpec.js +70 -0
  174. package/schedule/useUpdateScheduleSpec.js.map +1 -0
  175. package/search/useResourceCount.d.ts +2 -1
  176. package/search/useResourceCount.d.ts.map +1 -1
  177. package/search/useResourceCount.js +6 -1
  178. package/search/useResourceCount.js.map +1 -1
  179. package/session/useNewSessionFlow.d.ts.map +1 -1
  180. package/session/useNewSessionFlow.js +1 -0
  181. package/session/useNewSessionFlow.js.map +1 -1
  182. package/session/useSessionConversation.d.ts +11 -0
  183. package/session/useSessionConversation.d.ts.map +1 -1
  184. package/session/useSessionConversation.js +1 -0
  185. package/session/useSessionConversation.js.map +1 -1
  186. package/session/useSessionPageFlow.d.ts.map +1 -1
  187. package/session/useSessionPageFlow.js +1 -0
  188. package/session/useSessionPageFlow.js.map +1 -1
  189. package/src/agent/AgentPicker.tsx +13 -1
  190. package/src/billing/CreditLedgerTable.tsx +3 -42
  191. package/src/channel/AgentChannelsPanel.tsx +43 -1
  192. package/src/channel/ChannelTemplatesDialog.tsx +433 -0
  193. package/src/channel/__tests__/AgentChannelsPanel.test.tsx +73 -0
  194. package/src/channel/__tests__/ChannelTemplatesDialog.test.tsx +292 -0
  195. package/src/channel/__tests__/providers.test.ts +9 -0
  196. package/src/channel/__tests__/templatePresentation.test.ts +96 -0
  197. package/src/channel/__tests__/useChannelTemplateList.test.tsx +216 -0
  198. package/src/channel/__tests__/useChannelTemplateReadiness.test.tsx +124 -0
  199. package/src/channel/index.ts +15 -0
  200. package/src/channel/providers.ts +26 -2
  201. package/src/channel/templatePresentation.ts +82 -0
  202. package/src/channel/useChannelTemplateList.ts +123 -0
  203. package/src/channel/useChannelTemplateReadiness.ts +69 -0
  204. package/src/composer/ComposerToolbar.tsx +9 -0
  205. package/src/composer/SessionComposer.tsx +24 -1
  206. package/src/composer/__tests__/SessionComposer-serviceTier.test.tsx +156 -0
  207. package/src/dashboard/__tests__/useDashboardFailedRuns.test.tsx +106 -0
  208. package/src/dashboard/useDashboardFailedRuns.ts +2 -0
  209. package/src/execution/useCreateAgentExecution.ts +18 -0
  210. package/src/index.ts +70 -0
  211. package/src/internal/Pagination.tsx +72 -0
  212. package/src/library/useExportResource.ts +4 -3
  213. package/src/models/ModelSelector.tsx +100 -3
  214. package/src/models/__tests__/ModelSelector-serviceTier.test.tsx +172 -0
  215. package/src/models/__tests__/useModelRegistry.test.tsx +15 -0
  216. package/src/models/index.ts +2 -0
  217. package/src/models/registry.ts +16 -0
  218. package/src/models/service-tier.ts +49 -0
  219. package/src/organization/OrgProvider.tsx +29 -4
  220. package/src/organization/__tests__/OrgProvider.orgSwitch.test.tsx +89 -0
  221. package/src/resource-detail/useDeleteResource.ts +11 -1
  222. package/src/resource-workbench/__tests__/ResourceWorkbench.test.tsx +112 -0
  223. package/src/resource-workbench/components/ResourceWorkbench.tsx +32 -15
  224. package/src/resource-workbench/hooks/useResourceCollection.ts +11 -1
  225. package/src/schedule/CadenceField.tsx +365 -0
  226. package/src/schedule/ScheduleDetailView.tsx +1651 -0
  227. package/src/schedule/ScheduleForm.tsx +615 -0
  228. package/src/schedule/ScheduleRowActions.tsx +124 -0
  229. package/src/schedule/ScheduleRunsTable.tsx +424 -0
  230. package/src/schedule/TimeZoneField.tsx +199 -0
  231. package/src/schedule/__tests__/ScheduleDetailView.test.tsx +683 -0
  232. package/src/schedule/__tests__/cadence.test.ts +278 -0
  233. package/src/schedule/__tests__/scheduleColumnsRowActions.test.tsx +176 -0
  234. package/src/schedule/__tests__/scheduleCreation.test.tsx +537 -0
  235. package/src/schedule/__tests__/scheduleHooks.test.tsx +399 -0
  236. package/src/schedule/__tests__/scheduleListFn.test.ts +128 -0
  237. package/src/schedule/__tests__/scheduleState.test.ts +133 -0
  238. package/src/schedule/cadence.ts +298 -0
  239. package/src/schedule/index.ts +69 -0
  240. package/src/schedule/scheduleColumns.tsx +118 -0
  241. package/src/schedule/scheduleListFn.ts +108 -0
  242. package/src/schedule/scheduleState.ts +96 -0
  243. package/src/schedule/useCreateSchedule.ts +88 -0
  244. package/src/schedule/useResumeSchedule.ts +67 -0
  245. package/src/schedule/useSchedule.ts +67 -0
  246. package/src/schedule/useScheduleCount.ts +57 -0
  247. package/src/schedule/useScheduleList.ts +83 -0
  248. package/src/schedule/useScheduleRuns.ts +93 -0
  249. package/src/schedule/useSetScheduleEnabled.ts +84 -0
  250. package/src/schedule/useTriggerSchedule.ts +90 -0
  251. package/src/schedule/useUpdateScheduleSpec.ts +108 -0
  252. package/src/search/__tests__/useResourceCount.test.tsx +89 -0
  253. package/src/search/useResourceCount.ts +8 -2
  254. package/src/session/useNewSessionFlow.ts +1 -0
  255. package/src/session/useSessionConversation.ts +12 -0
  256. package/src/session/useSessionPageFlow.ts +1 -0
  257. package/src/workflow/WorkflowDashboard.tsx +1 -0
  258. package/src/workflow/__tests__/inspector-forms.test.tsx +119 -2
  259. package/src/workflow/inspector/forms/AgentCallForm.tsx +260 -37
  260. package/src/workflow/inspector/tabs/RuntimeTab.tsx +72 -33
  261. package/src/workflow/starter-workflow-yaml.ts +2 -1
  262. package/src/workflow/useWorkflowExecutionList.ts +13 -2
  263. package/styles.css +1 -1
  264. package/workflow/CanvasTransitionEdge.d.ts +1 -1
  265. package/workflow/WorkflowDashboard.d.ts.map +1 -1
  266. package/workflow/WorkflowDashboard.js +1 -0
  267. package/workflow/WorkflowDashboard.js.map +1 -1
  268. package/workflow/inspector/forms/AgentCallForm.d.ts +18 -2
  269. package/workflow/inspector/forms/AgentCallForm.d.ts.map +1 -1
  270. package/workflow/inspector/forms/AgentCallForm.js +87 -9
  271. package/workflow/inspector/forms/AgentCallForm.js.map +1 -1
  272. package/workflow/inspector/tabs/RuntimeTab.d.ts +2 -1
  273. package/workflow/inspector/tabs/RuntimeTab.d.ts.map +1 -1
  274. package/workflow/inspector/tabs/RuntimeTab.js +31 -5
  275. package/workflow/inspector/tabs/RuntimeTab.js.map +1 -1
  276. package/workflow/starter-workflow-yaml.d.ts +1 -1
  277. package/workflow/starter-workflow-yaml.d.ts.map +1 -1
  278. package/workflow/starter-workflow-yaml.js +2 -1
  279. package/workflow/starter-workflow-yaml.js.map +1 -1
  280. package/workflow/useWorkflowExecutionList.d.ts +10 -0
  281. package/workflow/useWorkflowExecutionList.d.ts.map +1 -1
  282. package/workflow/useWorkflowExecutionList.js +3 -2
  283. package/workflow/useWorkflowExecutionList.js.map +1 -1
@@ -0,0 +1,537 @@
1
+ import { describe, it, expect, vi, beforeAll, afterEach, beforeEach } from "vitest";
2
+ import {
3
+ render,
4
+ screen,
5
+ cleanup,
6
+ fireEvent,
7
+ waitFor,
8
+ } from "@testing-library/react";
9
+ import { renderHook, act } from "@testing-library/react";
10
+ import { useState, type ReactNode } from "react";
11
+ import { create } from "@bufbuild/protobuf";
12
+ import { ScheduleSchema } from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/api_pb";
13
+ import { Harness } from "@stigmer/protos/ai/stigmer/agentic/session/v1/enum_pb";
14
+ import { ServiceTier } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
15
+ import { StigmerContext } from "../../context";
16
+ import { FetchCacheContext } from "../../internal/FetchCacheProvider";
17
+ import { ModelRegistryContext } from "../../models/ModelRegistryContext";
18
+ import type { ModelInfo } from "../../models/registry";
19
+ import { useCreateSchedule } from "../useCreateSchedule";
20
+ import { CadenceField } from "../CadenceField";
21
+ import { ScheduleForm } from "../ScheduleForm";
22
+ import type { CadencePreset } from "../cadence";
23
+
24
+ // Without a StigmerProvider the portal container is null, and Base UI's
25
+ // Portal renders nothing — pin it to document.body so the agent-picker
26
+ // popover mounts.
27
+ vi.mock("../../portal-container", () => ({
28
+ useStigmerPortalContainer: () => document.body,
29
+ }));
30
+
31
+ // Base UI's Popover positioner observes its anchor; happy-dom lacks
32
+ // ResizeObserver, so provide a no-op shim.
33
+ beforeAll(() => {
34
+ if (!("ResizeObserver" in globalThis)) {
35
+ (globalThis as unknown as { ResizeObserver: unknown }).ResizeObserver =
36
+ class {
37
+ observe() {}
38
+ unobserve() {}
39
+ disconnect() {}
40
+ };
41
+ }
42
+ });
43
+
44
+ function wrapper(client: unknown) {
45
+ return function Wrapper({ children }: { children: ReactNode }) {
46
+ return (
47
+ <FetchCacheContext.Provider value={null}>
48
+ <StigmerContext.Provider value={client as never}>
49
+ {children}
50
+ </StigmerContext.Provider>
51
+ </FetchCacheContext.Provider>
52
+ );
53
+ };
54
+ }
55
+
56
+ const CREATED = create(ScheduleSchema, {
57
+ metadata: {
58
+ id: "sch_01new",
59
+ name: "daily-fee-reminders",
60
+ slug: "daily-fee-reminders",
61
+ org: "isc",
62
+ },
63
+ spec: { cron: "0 9 * * *", timeZone: "Asia/Kolkata", enabled: false },
64
+ });
65
+
66
+ const AGENT_RESULT = {
67
+ id: "agt_01example",
68
+ org: "isc",
69
+ slug: "fee-reminder",
70
+ name: "Fee Reminder",
71
+ description: "Sends fee reminders.",
72
+ };
73
+
74
+ beforeEach(() => vi.clearAllMocks());
75
+ afterEach(cleanup);
76
+
77
+ // ---------------------------------------------------------------------------
78
+ // useCreateSchedule
79
+ // ---------------------------------------------------------------------------
80
+
81
+ describe("useCreateSchedule", () => {
82
+ it("applies the input and returns the created schedule", async () => {
83
+ const apply = vi.fn().mockResolvedValue(CREATED);
84
+ const client = { schedule: { apply } };
85
+
86
+ const { result } = renderHook(() => useCreateSchedule(), {
87
+ wrapper: wrapper(client),
88
+ });
89
+
90
+ const input = {
91
+ name: "daily-fee-reminders",
92
+ org: "isc",
93
+ cron: "0 9 * * *",
94
+ timeZone: "Asia/Kolkata",
95
+ enabled: false,
96
+ agent: {
97
+ agentRef: { org: "isc", slug: "fee-reminder" },
98
+ message: "Send today's reminders.",
99
+ },
100
+ };
101
+
102
+ let returned: unknown;
103
+ await act(async () => {
104
+ returned = await result.current.create(input);
105
+ });
106
+
107
+ expect(apply).toHaveBeenCalledWith(input);
108
+ expect(returned).toBe(CREATED);
109
+ expect(result.current.isCreating).toBe(false);
110
+ expect(result.current.error).toBeNull();
111
+ });
112
+
113
+ it("captures the error, rethrows, and clears via clearError", async () => {
114
+ const failure = new Error("agent 'isc/fee-reminder' not found");
115
+ const apply = vi.fn().mockRejectedValue(failure);
116
+ const client = { schedule: { apply } };
117
+
118
+ const { result } = renderHook(() => useCreateSchedule(), {
119
+ wrapper: wrapper(client),
120
+ });
121
+
122
+ await act(async () => {
123
+ await expect(
124
+ result.current.create({ name: "x", org: "isc" }),
125
+ ).rejects.toThrow(failure);
126
+ });
127
+
128
+ expect(result.current.error).toBe(failure);
129
+
130
+ act(() => result.current.clearError());
131
+ expect(result.current.error).toBeNull();
132
+ });
133
+ });
134
+
135
+ // ---------------------------------------------------------------------------
136
+ // CadenceField
137
+ // ---------------------------------------------------------------------------
138
+
139
+ function ControlledCadenceField({ initial }: { initial: CadencePreset }) {
140
+ const [value, setValue] = useState<CadencePreset>(initial);
141
+ return <CadenceField value={value} onChange={setValue} timeZone="UTC" />;
142
+ }
143
+
144
+ describe("CadenceField", () => {
145
+ it("summarizes the selected preset in plain English", () => {
146
+ render(
147
+ <ControlledCadenceField initial={{ kind: "daily", hour: 9, minute: 0 }} />,
148
+ );
149
+ expect(screen.getByTestId("cadence-summary").textContent).toBe(
150
+ "Every day at 09:00 (UTC)",
151
+ );
152
+ });
153
+
154
+ it("carries the time of day across preset switches", () => {
155
+ render(
156
+ <ControlledCadenceField initial={{ kind: "daily", hour: 9, minute: 30 }} />,
157
+ );
158
+ fireEvent.click(screen.getByRole("radio", { name: "Weekly" }));
159
+ expect(screen.getByTestId("cadence-summary").textContent).toBe(
160
+ "Every Monday at 09:30 (UTC)",
161
+ );
162
+ });
163
+
164
+ it("prefills the custom escape hatch with the generated cron", () => {
165
+ render(
166
+ <ControlledCadenceField initial={{ kind: "daily", hour: 9, minute: 0 }} />,
167
+ );
168
+ fireEvent.click(screen.getByRole("radio", { name: "Custom cron" }));
169
+ expect(
170
+ (screen.getByRole("textbox", { name: "Cron expression" }) as HTMLInputElement)
171
+ .value,
172
+ ).toBe("0 9 * * *");
173
+ });
174
+
175
+ it("rejects invalid custom cron with the server's wording", () => {
176
+ render(
177
+ <ControlledCadenceField initial={{ kind: "custom", cron: "@every 30s" }} />,
178
+ );
179
+ expect(screen.getByRole("alert").textContent).toBe(
180
+ "spec.cron must be a calendar expression — @every intervals are not supported",
181
+ );
182
+ expect(screen.queryByTestId("cadence-summary")).toBeNull();
183
+ });
184
+
185
+ it("keeps the last selected weekly day locked", () => {
186
+ render(
187
+ <ControlledCadenceField
188
+ initial={{ kind: "weekly", days: [1], hour: 9, minute: 0 }}
189
+ />,
190
+ );
191
+ const monday = screen.getByRole("button", { name: "Monday" });
192
+ expect((monday as HTMLButtonElement).disabled).toBe(true);
193
+
194
+ fireEvent.click(screen.getByRole("button", { name: "Friday" }));
195
+ expect(
196
+ (screen.getByRole("button", { name: "Monday" }) as HTMLButtonElement)
197
+ .disabled,
198
+ ).toBe(false);
199
+ });
200
+
201
+ it("warns that days 29-31 skip shorter months", () => {
202
+ render(
203
+ <ControlledCadenceField
204
+ initial={{ kind: "monthly", day: 31, hour: 9, minute: 0 }}
205
+ />,
206
+ );
207
+ expect(screen.getByText(/Months without day 31 are skipped/)).toBeTruthy();
208
+ });
209
+ });
210
+
211
+ // ---------------------------------------------------------------------------
212
+ // ScheduleForm
213
+ // ---------------------------------------------------------------------------
214
+
215
+ function createClient(overrides?: {
216
+ apply?: ReturnType<typeof vi.fn>;
217
+ list?: ReturnType<typeof vi.fn>;
218
+ }) {
219
+ return {
220
+ schedule: { apply: overrides?.apply ?? vi.fn().mockResolvedValue(CREATED) },
221
+ agent: {
222
+ list:
223
+ overrides?.list ?? vi.fn().mockResolvedValue({ entries: [AGENT_RESULT] }),
224
+ },
225
+ };
226
+ }
227
+
228
+ async function pickAgent() {
229
+ fireEvent.click(screen.getByText("Choose an agent…"));
230
+ const result = await screen.findByRole("option", { name: /Fee Reminder/ });
231
+ fireEvent.click(result);
232
+ }
233
+
234
+ describe("ScheduleForm", () => {
235
+ it("disables submit until name, agent, and message are provided", async () => {
236
+ const client = createClient();
237
+ render(<ScheduleForm org="isc" />, { wrapper: wrapper(client) });
238
+
239
+ const submit = screen.getByRole("button", { name: "Create schedule" });
240
+ expect((submit as HTMLButtonElement).disabled).toBe(true);
241
+
242
+ fireEvent.change(screen.getByLabelText("Name"), {
243
+ target: { value: "daily-fee-reminders" },
244
+ });
245
+ fireEvent.change(screen.getByLabelText("Message"), {
246
+ target: { value: "Send today's reminders." },
247
+ });
248
+ expect((submit as HTMLButtonElement).disabled).toBe(true);
249
+
250
+ await pickAgent();
251
+ expect((submit as HTMLButtonElement).disabled).toBe(false);
252
+ });
253
+
254
+ it("locks the agent picker to org scope (no Org/All toggle)", async () => {
255
+ const client = createClient();
256
+ render(<ScheduleForm org="isc" />, { wrapper: wrapper(client) });
257
+
258
+ fireEvent.click(screen.getByText("Choose an agent…"));
259
+ await screen.findByRole("option", { name: /Fee Reminder/ });
260
+
261
+ expect(
262
+ screen.queryByRole("radiogroup", { name: "Resource scope" }),
263
+ ).toBeNull();
264
+ });
265
+
266
+ it("submits the generated cron, browser time zone, and staged-disabled default", async () => {
267
+ const apply = vi.fn().mockResolvedValue(CREATED);
268
+ const client = createClient({ apply });
269
+ const onComplete = vi.fn();
270
+
271
+ render(<ScheduleForm org="isc" onComplete={onComplete} />, {
272
+ wrapper: wrapper(client),
273
+ });
274
+
275
+ fireEvent.change(screen.getByLabelText("Name"), {
276
+ target: { value: "daily-fee-reminders" },
277
+ });
278
+ fireEvent.change(screen.getByLabelText("Message"), {
279
+ target: { value: " Send today's reminders. " },
280
+ });
281
+ await pickAgent();
282
+
283
+ fireEvent.click(screen.getByRole("button", { name: "Create schedule" }));
284
+
285
+ await waitFor(() => expect(onComplete).toHaveBeenCalledWith(CREATED));
286
+ expect(apply).toHaveBeenCalledWith({
287
+ name: "daily-fee-reminders",
288
+ org: "isc",
289
+ // The default cadence: daily at 09:00.
290
+ cron: "0 9 * * *",
291
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC",
292
+ // Staged-disabled by default — validate with Run now, then enable.
293
+ enabled: false,
294
+ agent: {
295
+ agentRef: { org: "isc", slug: "fee-reminder", kind: 40 },
296
+ message: "Send today's reminders.",
297
+ },
298
+ });
299
+ });
300
+
301
+ it("submits the budget as runConfig.maxCostUsd, dropping the blank model", async () => {
302
+ const apply = vi.fn().mockResolvedValue(CREATED);
303
+ const client = createClient({ apply });
304
+ const onComplete = vi.fn();
305
+
306
+ render(<ScheduleForm org="isc" onComplete={onComplete} />, {
307
+ wrapper: wrapper(client),
308
+ });
309
+
310
+ fireEvent.change(screen.getByLabelText("Name"), {
311
+ target: { value: "daily-fee-reminders" },
312
+ });
313
+ fireEvent.change(screen.getByLabelText("Message"), {
314
+ target: { value: "Send today's reminders." },
315
+ });
316
+ await pickAgent();
317
+ fireEvent.change(screen.getByLabelText(/Budget per run/), {
318
+ target: { value: "0.50" },
319
+ });
320
+
321
+ fireEvent.click(screen.getByRole("button", { name: "Create schedule" }));
322
+
323
+ await waitFor(() => expect(onComplete).toHaveBeenCalledWith(CREATED));
324
+ const input = apply.mock.calls[0][0];
325
+ expect(input.agent.runConfig).toEqual({ maxCostUsd: 0.5 });
326
+ // No model picked: neither a model nor a harness is pinned.
327
+ expect(input.agent.harness).toBeUndefined();
328
+ });
329
+
330
+ it("submits git workspace entries added through the manual URL input", async () => {
331
+ const apply = vi.fn().mockResolvedValue(CREATED);
332
+ const client = createClient({ apply });
333
+ const onComplete = vi.fn();
334
+
335
+ render(<ScheduleForm org="isc" onComplete={onComplete} />, {
336
+ wrapper: wrapper(client),
337
+ });
338
+
339
+ fireEvent.change(screen.getByLabelText("Name"), {
340
+ target: { value: "daily-fee-reminders" },
341
+ });
342
+ fireEvent.change(screen.getByLabelText("Message"), {
343
+ target: { value: "Send today's reminders." },
344
+ });
345
+ await pickAgent();
346
+
347
+ // No GitHub connection in this client, so the workspace editor's
348
+ // GitHub action drills into the manual URL panel — no OAuth flow.
349
+ fireEvent.click(screen.getByRole("button", { name: /Connect GitHub/ }));
350
+ fireEvent.change(screen.getByLabelText("Git repository URL"), {
351
+ target: { value: "https://github.com/isc/fee-data.git" },
352
+ });
353
+ fireEvent.change(screen.getByLabelText("Branch (optional)"), {
354
+ target: { value: "main" },
355
+ });
356
+ fireEvent.click(screen.getByRole("button", { name: "Add" }));
357
+
358
+ fireEvent.click(screen.getByRole("button", { name: "Create schedule" }));
359
+
360
+ await waitFor(() => expect(onComplete).toHaveBeenCalledWith(CREATED));
361
+ const input = apply.mock.calls[0][0];
362
+ expect(input.agent.workspaceEntries).toEqual([
363
+ {
364
+ name: "isc/fee-data",
365
+ source: {
366
+ gitRepo: { url: "https://github.com/isc/fee-data.git", branch: "main" },
367
+ },
368
+ },
369
+ ]);
370
+ });
371
+
372
+ it("picking a model pins the engine it belongs to (harness + model travel together)", async () => {
373
+ const apply = vi.fn().mockResolvedValue(CREATED);
374
+ const client = createClient({ apply });
375
+ const onComplete = vi.fn();
376
+
377
+ const registryModel: ModelInfo = {
378
+ modelId: "claude-sonnet-4-6",
379
+ provider: "anthropic",
380
+ displayName: "Claude Sonnet 4.6",
381
+ shortDescription: "Balanced default",
382
+ speedTier: "balanced",
383
+ costTier: "standard",
384
+ harness: "cursor",
385
+ featured: true,
386
+ serviceTiers: [],
387
+ };
388
+
389
+ function RegistryWrapper({ children }: { children: ReactNode }) {
390
+ const Base = wrapper(client);
391
+ return (
392
+ <ModelRegistryContext.Provider
393
+ value={{
394
+ models: [registryModel],
395
+ isLoading: false,
396
+ error: null,
397
+ refetch: () => {},
398
+ }}
399
+ >
400
+ <Base>{children}</Base>
401
+ </ModelRegistryContext.Provider>
402
+ );
403
+ }
404
+
405
+ render(<ScheduleForm org="isc" onComplete={onComplete} />, {
406
+ wrapper: RegistryWrapper,
407
+ });
408
+
409
+ fireEvent.change(screen.getByLabelText("Name"), {
410
+ target: { value: "daily-fee-reminders" },
411
+ });
412
+ fireEvent.change(screen.getByLabelText("Message"), {
413
+ target: { value: "Send today's reminders." },
414
+ });
415
+ await pickAgent();
416
+
417
+ // Nothing pinned yet: the trigger shows the placeholder.
418
+ fireEvent.click(screen.getByRole("button", { name: /Platform default/ }));
419
+ fireEvent.click(
420
+ await screen.findByRole("option", { name: /Claude Sonnet 4\.6/ }),
421
+ );
422
+
423
+ fireEvent.click(screen.getByRole("button", { name: "Create schedule" }));
424
+
425
+ await waitFor(() => expect(onComplete).toHaveBeenCalledWith(CREATED));
426
+ const input = apply.mock.calls[0][0];
427
+ expect(input.agent.runConfig).toEqual({ modelName: "claude-sonnet-4-6" });
428
+ expect(input.agent.harness).toBe(Harness.CURSOR);
429
+ });
430
+
431
+ it("toggling the fast tier on a capable model lands service_tier on run_config (#357)", async () => {
432
+ const apply = vi.fn().mockResolvedValue(CREATED);
433
+ const client = createClient({ apply });
434
+ const onComplete = vi.fn();
435
+
436
+ const fastCapable: ModelInfo = {
437
+ modelId: "composer-2.5",
438
+ provider: "cursor",
439
+ displayName: "Composer 2.5",
440
+ shortDescription: "Fast agentic model",
441
+ speedTier: "fastest",
442
+ costTier: "economy",
443
+ harness: "cursor",
444
+ featured: true,
445
+ serviceTiers: ["fast"],
446
+ };
447
+
448
+ function RegistryWrapper({ children }: { children: ReactNode }) {
449
+ const Base = wrapper(client);
450
+ return (
451
+ <ModelRegistryContext.Provider
452
+ value={{
453
+ models: [fastCapable],
454
+ isLoading: false,
455
+ error: null,
456
+ refetch: () => {},
457
+ }}
458
+ >
459
+ <Base>{children}</Base>
460
+ </ModelRegistryContext.Provider>
461
+ );
462
+ }
463
+
464
+ render(<ScheduleForm org="isc" onComplete={onComplete} />, {
465
+ wrapper: RegistryWrapper,
466
+ });
467
+
468
+ fireEvent.change(screen.getByLabelText("Name"), {
469
+ target: { value: "daily-fee-reminders" },
470
+ });
471
+ fireEvent.change(screen.getByLabelText("Message"), {
472
+ target: { value: "Send today's reminders." },
473
+ });
474
+ await pickAgent();
475
+
476
+ fireEvent.click(screen.getByRole("button", { name: /Platform default/ }));
477
+ fireEvent.click(await screen.findByRole("option", { name: /Composer 2\.5/ }));
478
+
479
+ // The popover closed on selection; reopen it — the toggle renders only
480
+ // for the now-selected fast-capable model.
481
+ fireEvent.click(screen.getByRole("button", { name: /Composer 2\.5/ }));
482
+ fireEvent.click(await screen.findByRole("switch", { name: "Fast tier" }));
483
+
484
+ fireEvent.click(screen.getByRole("button", { name: "Create schedule" }));
485
+
486
+ await waitFor(() => expect(onComplete).toHaveBeenCalledWith(CREATED));
487
+ const input = apply.mock.calls[0][0];
488
+ expect(input.agent.runConfig).toEqual({
489
+ modelName: "composer-2.5",
490
+ serviceTier: ServiceTier.FAST,
491
+ });
492
+ });
493
+
494
+ it("surfaces the server error verbatim and stays editable", async () => {
495
+ const apply = vi
496
+ .fn()
497
+ .mockRejectedValue(
498
+ new Error(
499
+ "spec.cron must be a calendar expression — @every intervals are not supported",
500
+ ),
501
+ );
502
+ const client = createClient({ apply });
503
+ const onComplete = vi.fn();
504
+
505
+ render(<ScheduleForm org="isc" onComplete={onComplete} />, {
506
+ wrapper: wrapper(client),
507
+ });
508
+
509
+ fireEvent.change(screen.getByLabelText("Name"), {
510
+ target: { value: "daily-fee-reminders" },
511
+ });
512
+ fireEvent.change(screen.getByLabelText("Message"), {
513
+ target: { value: "Send today's reminders." },
514
+ });
515
+ await pickAgent();
516
+ fireEvent.click(screen.getByRole("button", { name: "Create schedule" }));
517
+
518
+ const alert = await screen.findByRole("alert");
519
+ expect(alert.textContent).toContain("@every intervals are not supported");
520
+ expect(onComplete).not.toHaveBeenCalled();
521
+ expect(
522
+ (screen.getByRole("button", { name: "Create schedule" }) as HTMLButtonElement)
523
+ .disabled,
524
+ ).toBe(false);
525
+ });
526
+
527
+ it("fires onCancel", () => {
528
+ const client = createClient();
529
+ const onCancel = vi.fn();
530
+ render(<ScheduleForm org="isc" onCancel={onCancel} />, {
531
+ wrapper: wrapper(client),
532
+ });
533
+
534
+ fireEvent.click(screen.getByRole("button", { name: "Cancel" }));
535
+ expect(onCancel).toHaveBeenCalled();
536
+ });
537
+ });