@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,399 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { renderHook, waitFor } from "@testing-library/react";
3
+ import type { ReactNode } from "react";
4
+ import { clone, create, equals } from "@bufbuild/protobuf";
5
+ import { ScheduleSchema, type Schedule } from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/api_pb";
6
+ import {
7
+ ScheduleListSchema,
8
+ ScheduleTriggerResultSchema,
9
+ ScheduleRunListSchema,
10
+ ScheduleRunSchema,
11
+ ScheduleRunOutcome,
12
+ ScheduleRunOrigin,
13
+ } from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/io_pb";
14
+ import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
15
+ import { StigmerError } from "@stigmer/sdk";
16
+ import { StigmerContext } from "../../context";
17
+ import { FetchCacheContext } from "../../internal/FetchCacheProvider";
18
+ import { useSchedule } from "../useSchedule";
19
+ import { useScheduleList } from "../useScheduleList";
20
+ import { useScheduleCount } from "../useScheduleCount";
21
+ import { useResumeSchedule } from "../useResumeSchedule";
22
+ import { useTriggerSchedule } from "../useTriggerSchedule";
23
+ import { useScheduleRuns } from "../useScheduleRuns";
24
+ import { useSetScheduleEnabled } from "../useSetScheduleEnabled";
25
+
26
+ vi.mock("../../feedback/toast", () => ({
27
+ toast: Object.assign(vi.fn(), { success: vi.fn(), error: vi.fn() }),
28
+ }));
29
+
30
+ import { toast } from "../../feedback/toast";
31
+
32
+ function wrapper(client: unknown) {
33
+ return function Wrapper({ children }: { children: ReactNode }) {
34
+ return (
35
+ <FetchCacheContext.Provider value={null}>
36
+ <StigmerContext.Provider value={client as never}>
37
+ {children}
38
+ </StigmerContext.Provider>
39
+ </FetchCacheContext.Provider>
40
+ );
41
+ };
42
+ }
43
+
44
+ // Shaped like a server-returned schedule, including metadata the curated
45
+ // ScheduleInput cannot express (tags) — the round-trip canary.
46
+ const SCHEDULE = create(ScheduleSchema, {
47
+ apiVersion: "agentic.stigmer.ai/v1",
48
+ kind: "Schedule",
49
+ metadata: {
50
+ id: "sch_01example",
51
+ name: "daily-fee-reminders",
52
+ slug: "daily-fee-reminders",
53
+ org: "isc",
54
+ tags: ["billing"],
55
+ labels: { team: "ops" },
56
+ },
57
+ spec: {
58
+ cron: "0 9 * * *",
59
+ timeZone: "Asia/Kolkata",
60
+ enabled: true,
61
+ target: {
62
+ case: "agent",
63
+ value: {
64
+ agentRef: { kind: ApiResourceKind.agent, org: "isc", slug: "fee-reminder" },
65
+ message: "Send today's fee reminders.",
66
+ },
67
+ },
68
+ },
69
+ status: { consecutiveFailures: 2 },
70
+ });
71
+
72
+ beforeEach(() => vi.clearAllMocks());
73
+
74
+ // ---------------------------------------------------------------------------
75
+ // Data hooks
76
+ // ---------------------------------------------------------------------------
77
+
78
+ describe("useSchedule", () => {
79
+ it("fetches a schedule by org/slug reference", async () => {
80
+ const getByReference = vi.fn().mockResolvedValue(SCHEDULE);
81
+ const client = { schedule: { getByReference } };
82
+
83
+ const { result } = renderHook(() => useSchedule("isc", "daily-fee-reminders"), {
84
+ wrapper: wrapper(client),
85
+ });
86
+
87
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
88
+ expect(result.current.schedule).toBe(SCHEDULE);
89
+ expect(result.current.error).toBeNull();
90
+ expect(getByReference).toHaveBeenCalledWith({
91
+ org: "isc",
92
+ slug: "daily-fee-reminders",
93
+ });
94
+ });
95
+
96
+ it("treats NOT_FOUND as null, not an error", async () => {
97
+ // Positional ctor: (code, message, connectCode) — 5 is Connect NotFound.
98
+ const notFound = new StigmerError("not-found", "Schedule 'x' not found", 5);
99
+ const client = {
100
+ schedule: { getByReference: vi.fn().mockRejectedValue(notFound) },
101
+ };
102
+
103
+ const { result } = renderHook(() => useSchedule("isc", "x"), {
104
+ wrapper: wrapper(client),
105
+ });
106
+
107
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
108
+ expect(result.current.schedule).toBeNull();
109
+ expect(result.current.error).toBeNull();
110
+ });
111
+
112
+ it("skips fetching when org or slug is null", () => {
113
+ const getByReference = vi.fn();
114
+ const client = { schedule: { getByReference } };
115
+
116
+ const { result } = renderHook(() => useSchedule(null, "x"), {
117
+ wrapper: wrapper(client),
118
+ });
119
+
120
+ expect(result.current.isLoading).toBe(false);
121
+ expect(getByReference).not.toHaveBeenCalled();
122
+ });
123
+ });
124
+
125
+ describe("useScheduleList", () => {
126
+ it("fetches a page of full Schedule protos", async () => {
127
+ const list = vi.fn().mockResolvedValue(
128
+ create(ScheduleListSchema, { items: [SCHEDULE], totalCount: 1 }),
129
+ );
130
+ const client = { schedule: { list } };
131
+
132
+ const { result } = renderHook(() => useScheduleList("isc"), {
133
+ wrapper: wrapper(client),
134
+ });
135
+
136
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
137
+ expect(result.current.schedules).toHaveLength(1);
138
+ expect(result.current.schedules[0].spec?.cron).toBe("0 9 * * *");
139
+ expect(result.current.totalCount).toBe(1);
140
+ expect(result.current.totalPages).toBe(1);
141
+ });
142
+
143
+ it("skips fetching when org is null", () => {
144
+ const list = vi.fn();
145
+ const client = { schedule: { list } };
146
+
147
+ const { result } = renderHook(() => useScheduleList(null), {
148
+ wrapper: wrapper(client),
149
+ });
150
+
151
+ expect(result.current.schedules).toEqual([]);
152
+ expect(list).not.toHaveBeenCalled();
153
+ });
154
+ });
155
+
156
+ describe("useScheduleCount", () => {
157
+ it("reads only the total count", async () => {
158
+ const list = vi.fn().mockResolvedValue(
159
+ create(ScheduleListSchema, { items: [SCHEDULE], totalCount: 7 }),
160
+ );
161
+ const client = { schedule: { list } };
162
+
163
+ const { result } = renderHook(() => useScheduleCount("isc"), {
164
+ wrapper: wrapper(client),
165
+ });
166
+
167
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
168
+ expect(result.current.count).toBe(7);
169
+ });
170
+ });
171
+
172
+ // ---------------------------------------------------------------------------
173
+ // Behavior hooks
174
+ // ---------------------------------------------------------------------------
175
+
176
+ describe("useResumeSchedule", () => {
177
+ it("resumes by id and toasts success", async () => {
178
+ const resume = vi.fn().mockResolvedValue(SCHEDULE);
179
+ const client = { schedule: { resume } };
180
+
181
+ const { result } = renderHook(() => useResumeSchedule(), {
182
+ wrapper: wrapper(client),
183
+ });
184
+
185
+ const resumed = await result.current.resumeSchedule("sch_01example");
186
+
187
+ expect(resumed).toBe(SCHEDULE);
188
+ expect(resume).toHaveBeenCalledWith("sch_01example");
189
+ expect(toast.success).toHaveBeenCalledWith("Schedule resumed");
190
+ });
191
+
192
+ it("relays a failure and sets error state", async () => {
193
+ const failure = new Error("boom");
194
+ const client = { schedule: { resume: vi.fn().mockRejectedValue(failure) } };
195
+
196
+ const { result } = renderHook(() => useResumeSchedule(), {
197
+ wrapper: wrapper(client),
198
+ });
199
+
200
+ await expect(result.current.resumeSchedule("sch_x")).rejects.toThrow("boom");
201
+ await waitFor(() => expect(result.current.error?.message).toBe("boom"));
202
+ expect(toast.error).toHaveBeenCalled();
203
+ });
204
+ });
205
+
206
+ describe("useTriggerSchedule", () => {
207
+ it("resolves a STARTED result and toasts success", async () => {
208
+ const started = create(ScheduleTriggerResultSchema, {
209
+ outcome: ScheduleRunOutcome.STARTED,
210
+ executionId: "aex_01run",
211
+ schedule: SCHEDULE,
212
+ });
213
+ const trigger = vi.fn().mockResolvedValue(started);
214
+ const client = { schedule: { trigger } };
215
+
216
+ const { result } = renderHook(() => useTriggerSchedule(), {
217
+ wrapper: wrapper(client),
218
+ });
219
+
220
+ const res = await result.current.triggerSchedule("sch_01example");
221
+
222
+ expect(trigger).toHaveBeenCalledWith("sch_01example");
223
+ expect(res.outcome).toBe(ScheduleRunOutcome.STARTED);
224
+ expect(res.executionId).toBe("aex_01run");
225
+ expect(toast.success).toHaveBeenCalledWith("Run started");
226
+ });
227
+
228
+ it("resolves a REFUSED result and toasts the gate's reason verbatim", async () => {
229
+ // A refused run is a SUCCESSFUL trigger honestly reported (DD-017
230
+ // D-6): the promise resolves, and the gate's own copy is surfaced,
231
+ // never paraphrased.
232
+ const refused = create(ScheduleTriggerResultSchema, {
233
+ outcome: ScheduleRunOutcome.REFUSED,
234
+ refusalReason:
235
+ "run refused: The organization cannot fund a scheduled run right now.",
236
+ schedule: SCHEDULE,
237
+ });
238
+ const trigger = vi.fn().mockResolvedValue(refused);
239
+ const client = { schedule: { trigger } };
240
+
241
+ const { result } = renderHook(() => useTriggerSchedule(), {
242
+ wrapper: wrapper(client),
243
+ });
244
+
245
+ const res = await result.current.triggerSchedule("sch_01example");
246
+
247
+ expect(res.outcome).toBe(ScheduleRunOutcome.REFUSED);
248
+ expect(toast.error).toHaveBeenCalledWith(
249
+ "run refused: The organization cannot fund a scheduled run right now.",
250
+ );
251
+ expect(toast.success).not.toHaveBeenCalled();
252
+ });
253
+
254
+ it("relays a trigger-level gRPC refusal (disabled) verbatim and rejects", async () => {
255
+ // A disabled schedule is refused at the trigger itself — a gRPC
256
+ // error, not a result. Copy is byte-identical across editions.
257
+ const refusal = new StigmerError(
258
+ "failed-precondition",
259
+ "schedule is disabled (spec.enabled=false) — enable it before triggering",
260
+ 9, // Connect FailedPrecondition
261
+ );
262
+ const client = { schedule: { trigger: vi.fn().mockRejectedValue(refusal) } };
263
+
264
+ const { result } = renderHook(() => useTriggerSchedule(), {
265
+ wrapper: wrapper(client),
266
+ });
267
+
268
+ await expect(result.current.triggerSchedule("sch_x")).rejects.toThrow();
269
+ expect(toast.error).toHaveBeenCalledWith(
270
+ "schedule is disabled (spec.enabled=false) — enable it before triggering",
271
+ );
272
+ });
273
+ });
274
+
275
+ describe("useScheduleRuns", () => {
276
+ it("fetches a schedule's run history, newest first", async () => {
277
+ const listRuns = vi.fn().mockResolvedValue(
278
+ create(ScheduleRunListSchema, {
279
+ totalCount: 2,
280
+ items: [
281
+ create(ScheduleRunSchema, {
282
+ scheduleId: "sch_01example",
283
+ origin: ScheduleRunOrigin.MANUAL,
284
+ outcome: ScheduleRunOutcome.STARTED,
285
+ executionId: "aex_01run",
286
+ }),
287
+ create(ScheduleRunSchema, {
288
+ scheduleId: "sch_01example",
289
+ origin: ScheduleRunOrigin.CRON,
290
+ outcome: ScheduleRunOutcome.REFUSED,
291
+ reason: "run refused: missing credential",
292
+ }),
293
+ ],
294
+ }),
295
+ );
296
+ const client = { schedule: { listRuns } };
297
+
298
+ const { result } = renderHook(() => useScheduleRuns("sch_01example"), {
299
+ wrapper: wrapper(client),
300
+ });
301
+
302
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
303
+ expect(result.current.runs).toHaveLength(2);
304
+ expect(result.current.totalCount).toBe(2);
305
+ expect(result.current.runs[1].reason).toBe("run refused: missing credential");
306
+ });
307
+
308
+ it("skips fetching when scheduleId is null", () => {
309
+ const listRuns = vi.fn();
310
+ const client = { schedule: { listRuns } };
311
+
312
+ const { result } = renderHook(() => useScheduleRuns(null), {
313
+ wrapper: wrapper(client),
314
+ });
315
+
316
+ expect(result.current.runs).toEqual([]);
317
+ expect(listRuns).not.toHaveBeenCalled();
318
+ });
319
+ });
320
+
321
+ describe("useSetScheduleEnabled", () => {
322
+ it("re-applies the full proto with only spec.enabled flipped", async () => {
323
+ const apply = vi.fn(async (doc: { message: unknown }) => ({
324
+ yamlKind: "Schedule",
325
+ displayName: "Schedule",
326
+ name: "daily-fee-reminders",
327
+ slug: "daily-fee-reminders",
328
+ org: "isc",
329
+ id: "sch_01example",
330
+ message: doc.message,
331
+ }));
332
+ const client = { manifest: { apply } };
333
+
334
+ const { result } = renderHook(() => useSetScheduleEnabled(), {
335
+ wrapper: wrapper(client),
336
+ });
337
+
338
+ const applied = await result.current.setEnabled(SCHEDULE, false);
339
+
340
+ // The apply went through the manifest engine with a real
341
+ // ManifestDocument (registry handler resolved from the proto type).
342
+ expect(apply).toHaveBeenCalledOnce();
343
+ const doc = apply.mock.calls[0][0] as {
344
+ handler: { yamlKind: string };
345
+ message: Schedule;
346
+ };
347
+ expect(doc.handler.yamlKind).toBe("Schedule");
348
+
349
+ // Round-trip field preservation: everything survives except the
350
+ // flipped flag. In particular metadata.tags — the field the curated
351
+ // ScheduleInput path would have silently wiped.
352
+ const expected = clone(ScheduleSchema, SCHEDULE);
353
+ expected.spec!.enabled = false;
354
+ expect(equals(ScheduleSchema, doc.message, expected)).toBe(true);
355
+ expect(doc.message.metadata?.tags).toEqual(["billing"]);
356
+
357
+ // The caller's copy is never mutated.
358
+ expect(SCHEDULE.spec?.enabled).toBe(true);
359
+
360
+ expect((applied as Schedule).spec?.enabled).toBe(false);
361
+ expect(toast.success).toHaveBeenCalledWith("Schedule disabled");
362
+ });
363
+
364
+ it("toasts 'enabled' when turning the schedule on", async () => {
365
+ const disabled = clone(ScheduleSchema, SCHEDULE);
366
+ disabled.spec!.enabled = false;
367
+ const apply = vi.fn(async (doc: { message: unknown }) => ({
368
+ yamlKind: "Schedule",
369
+ displayName: "Schedule",
370
+ name: "daily-fee-reminders",
371
+ slug: "daily-fee-reminders",
372
+ org: "isc",
373
+ id: "sch_01example",
374
+ message: doc.message,
375
+ }));
376
+ const client = { manifest: { apply } };
377
+
378
+ const { result } = renderHook(() => useSetScheduleEnabled(), {
379
+ wrapper: wrapper(client),
380
+ });
381
+
382
+ await result.current.setEnabled(disabled, true);
383
+ expect(toast.success).toHaveBeenCalledWith("Schedule enabled");
384
+ });
385
+
386
+ it("rejects a schedule without a spec", async () => {
387
+ const bare = create(ScheduleSchema, {
388
+ metadata: { name: "x", org: "isc" },
389
+ });
390
+ const client = { manifest: { apply: vi.fn() } };
391
+
392
+ const { result } = renderHook(() => useSetScheduleEnabled(), {
393
+ wrapper: wrapper(client),
394
+ });
395
+
396
+ await expect(result.current.setEnabled(bare, true)).rejects.toThrow(/no spec/);
397
+ expect(client.manifest.apply).not.toHaveBeenCalled();
398
+ });
399
+ });
@@ -0,0 +1,128 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { create } from "@bufbuild/protobuf";
3
+ import { ScheduleSchema, type Schedule } from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/api_pb";
4
+ import { ScheduleListSchema } from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/io_pb";
5
+ import { createScheduleListFn, listSchedulesPage } from "../scheduleListFn";
6
+
7
+ function schedules(count: number): Schedule[] {
8
+ return Array.from({ length: count }, (_, i) =>
9
+ create(ScheduleSchema, {
10
+ metadata: { id: `sch_${i}`, name: `sched-${i}`, slug: `sched-${i}`, org: "acme" },
11
+ }),
12
+ );
13
+ }
14
+
15
+ function clientReturning(items: Schedule[], totalCount: number) {
16
+ const list = vi
17
+ .fn()
18
+ .mockResolvedValue(create(ScheduleListSchema, { items, totalCount }));
19
+ return { client: { schedule: { list } }, list };
20
+ }
21
+
22
+ describe("listSchedulesPage", () => {
23
+ it("sends the org and requested page in the query", async () => {
24
+ const { client, list } = clientReturning([], 0);
25
+
26
+ await listSchedulesPage(client, "acme", { num: 2, size: 10 });
27
+
28
+ expect(list).toHaveBeenCalledOnce();
29
+ const request = list.mock.calls[0][0];
30
+ expect(request.org).toBe("acme");
31
+ expect(request.pageInfo?.num).toBe(2);
32
+ expect(request.pageInfo?.size).toBe(10);
33
+ });
34
+
35
+ it("defaults to page 1, size 20", async () => {
36
+ const { client, list } = clientReturning([], 0);
37
+
38
+ await listSchedulesPage(client, "acme");
39
+
40
+ const request = list.mock.calls[0][0];
41
+ expect(request.pageInfo?.num).toBe(1);
42
+ expect(request.pageInfo?.size).toBe(20);
43
+ });
44
+
45
+ // Cloud slices server-side: a response never exceeds the page size.
46
+ it("passes a cloud-shaped page through untouched", async () => {
47
+ const page = schedules(20);
48
+ const { client } = clientReturning(page, 50);
49
+
50
+ const result = await listSchedulesPage(client, "acme", { num: 2, size: 20 });
51
+
52
+ expect(result.items).toHaveLength(20);
53
+ expect(result.items[0].metadata?.slug).toBe("sched-0");
54
+ expect(result.totalCount).toBe(50);
55
+ expect(result.totalPages).toBe(3);
56
+ });
57
+
58
+ it("passes a cloud-shaped final short page through untouched", async () => {
59
+ const { client } = clientReturning(schedules(10), 50);
60
+
61
+ const result = await listSchedulesPage(client, "acme", { num: 3, size: 20 });
62
+
63
+ expect(result.items).toHaveLength(10);
64
+ expect(result.totalPages).toBe(3);
65
+ });
66
+
67
+ // OSS ignores page_info and returns the full org list — an overflowing
68
+ // response is the tell, and only then is the page sliced locally.
69
+ it("slices an OSS-shaped full-list response with absolute indices", async () => {
70
+ const { client } = clientReturning(schedules(50), 50);
71
+
72
+ const result = await listSchedulesPage(client, "acme", { num: 2, size: 20 });
73
+
74
+ expect(result.items).toHaveLength(20);
75
+ expect(result.items[0].metadata?.slug).toBe("sched-20");
76
+ expect(result.items[19].metadata?.slug).toBe("sched-39");
77
+ expect(result.totalCount).toBe(50);
78
+ expect(result.totalPages).toBe(3);
79
+ });
80
+
81
+ it("slices the OSS-shaped final short page correctly", async () => {
82
+ const { client } = clientReturning(schedules(50), 50);
83
+
84
+ const result = await listSchedulesPage(client, "acme", { num: 3, size: 20 });
85
+
86
+ expect(result.items).toHaveLength(10);
87
+ expect(result.items[0].metadata?.slug).toBe("sched-40");
88
+ expect(result.items[9].metadata?.slug).toBe("sched-49");
89
+ });
90
+
91
+ it("returns an empty page with zero totals", async () => {
92
+ const { client } = clientReturning([], 0);
93
+
94
+ const result = await listSchedulesPage(client, "acme");
95
+
96
+ expect(result.items).toEqual([]);
97
+ expect(result.totalCount).toBe(0);
98
+ expect(result.totalPages).toBe(0);
99
+ });
100
+ });
101
+
102
+ describe("createScheduleListFn", () => {
103
+ it("adapts ListParams to the direct query and back to ListResult", async () => {
104
+ const { client, list } = clientReturning(schedules(3), 3);
105
+ const listFn = createScheduleListFn(client);
106
+
107
+ const result = await listFn({
108
+ org: "acme",
109
+ // No server-side text search for schedules — ignored by contract.
110
+ query: "ignored",
111
+ excludePublic: false,
112
+ crossOrgPublic: false,
113
+ page: { num: 1, size: 20 },
114
+ });
115
+
116
+ expect(result.totalCount).toBe(3);
117
+ expect(result.totalPages).toBe(1);
118
+ expect(result.entries).toHaveLength(3);
119
+ // Entries are full Schedule protos occupying the SearchResult slot.
120
+ expect((result.entries[0] as unknown as Schedule).metadata?.slug).toBe("sched-0");
121
+
122
+ const request = list.mock.calls[0][0];
123
+ expect(request.org).toBe("acme");
124
+ // The query never reaches the wire: ListSchedulesRequest has no
125
+ // text-search field.
126
+ expect("query" in request && request.query).toBeFalsy();
127
+ });
128
+ });
@@ -0,0 +1,133 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { create } from "@bufbuild/protobuf";
3
+ import { ScheduleSpecSchema } from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/spec_pb";
4
+ import { ScheduleStatusSchema } from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/status_pb";
5
+ import { deriveScheduleState, formatNextFire } from "../scheduleState";
6
+
7
+ const enabledSpec = create(ScheduleSpecSchema, {
8
+ cron: "0 9 * * *",
9
+ timeZone: "Asia/Kolkata",
10
+ enabled: true,
11
+ });
12
+ const disabledSpec = create(ScheduleSpecSchema, {
13
+ cron: "0 9 * * *",
14
+ timeZone: "Asia/Kolkata",
15
+ enabled: false,
16
+ });
17
+ const pausedStatus = create(ScheduleStatusSchema, {
18
+ pausedReason: "5 consecutive failures",
19
+ consecutiveFailures: 5,
20
+ });
21
+ const healthyStatus = create(ScheduleStatusSchema, {});
22
+
23
+ describe("deriveScheduleState", () => {
24
+ // Every lever combination — the two levers must never collapse.
25
+ const cases = [
26
+ {
27
+ name: "enabled + not paused → active",
28
+ spec: enabledSpec,
29
+ status: healthyStatus,
30
+ state: "active",
31
+ phase: "ready",
32
+ label: "Active",
33
+ isPaused: false,
34
+ },
35
+ {
36
+ name: "enabled + paused → paused (platform latch)",
37
+ spec: enabledSpec,
38
+ status: pausedStatus,
39
+ state: "paused",
40
+ phase: "degraded",
41
+ label: "Paused",
42
+ isPaused: true,
43
+ },
44
+ {
45
+ name: "disabled + not paused → disabled (owner switch)",
46
+ spec: disabledSpec,
47
+ status: healthyStatus,
48
+ state: "disabled",
49
+ phase: "disabled",
50
+ label: "Disabled",
51
+ isPaused: false,
52
+ },
53
+ {
54
+ name: "disabled + paused → disabled wins, pause stays visible",
55
+ spec: disabledSpec,
56
+ status: pausedStatus,
57
+ state: "disabled",
58
+ phase: "disabled",
59
+ label: "Disabled",
60
+ isPaused: true,
61
+ },
62
+ {
63
+ name: "missing spec → disabled (fails closed, like the server)",
64
+ spec: undefined,
65
+ status: healthyStatus,
66
+ state: "disabled",
67
+ phase: "disabled",
68
+ label: "Disabled",
69
+ isPaused: false,
70
+ },
71
+ {
72
+ name: "missing status → not paused",
73
+ spec: enabledSpec,
74
+ status: undefined,
75
+ state: "active",
76
+ phase: "ready",
77
+ label: "Active",
78
+ isPaused: false,
79
+ },
80
+ ] as const;
81
+
82
+ for (const c of cases) {
83
+ it(c.name, () => {
84
+ const info = deriveScheduleState(c.spec, c.status);
85
+ expect(info.state).toBe(c.state);
86
+ expect(info.phase).toBe(c.phase);
87
+ expect(info.label).toBe(c.label);
88
+ expect(info.isPaused).toBe(c.isPaused);
89
+ });
90
+ }
91
+ });
92
+
93
+ describe("formatNextFire", () => {
94
+ const now = new Date("2026-08-04T10:00:00Z");
95
+ const at = (offsetMs: number) => new Date(now.getTime() + offsetMs);
96
+ const MINUTE = 60_000;
97
+ const HOUR = 60 * MINUTE;
98
+ const DAY = 24 * HOUR;
99
+
100
+ it("renders sub-minute and past instants as 'now' (tick mid-flight)", () => {
101
+ expect(formatNextFire(at(30_000), now)).toBe("now");
102
+ expect(formatNextFire(at(-5 * MINUTE), now)).toBe("now");
103
+ });
104
+
105
+ it("renders minutes, hours, and days compactly", () => {
106
+ expect(formatNextFire(at(5 * MINUTE), now)).toBe("in 5m");
107
+ expect(formatNextFire(at(59 * MINUTE), now)).toBe("in 59m");
108
+ expect(formatNextFire(at(3 * HOUR), now)).toBe("in 3h");
109
+ expect(formatNextFire(at(23 * HOUR), now)).toBe("in 23h");
110
+ expect(formatNextFire(at(DAY), now)).toBe("in 1d");
111
+ expect(formatNextFire(at(6 * DAY + 23 * HOUR), now)).toBe("in 6d");
112
+ });
113
+
114
+ it("falls back to a short date beyond a week", () => {
115
+ const target = at(10 * DAY);
116
+ // Locale-independent expectation: same formatting call, same options.
117
+ const expected = target.toLocaleDateString(undefined, {
118
+ month: "short",
119
+ day: "numeric",
120
+ });
121
+ expect(formatNextFire(target, now)).toBe(expected);
122
+ });
123
+
124
+ it("includes the year when the date crosses a year boundary", () => {
125
+ const target = new Date("2027-01-15T10:00:00Z");
126
+ const expected = target.toLocaleDateString(undefined, {
127
+ month: "short",
128
+ day: "numeric",
129
+ year: "numeric",
130
+ });
131
+ expect(formatNextFire(target, now)).toBe(expected);
132
+ });
133
+ });