@stigmer/react 3.5.3 → 3.6.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.
- package/agent/AgentPicker.d.ts +10 -1
- package/agent/AgentPicker.d.ts.map +1 -1
- package/agent/AgentPicker.js +2 -2
- package/agent/AgentPicker.js.map +1 -1
- package/channel/AgentChannelsPanel.d.ts.map +1 -1
- package/channel/AgentChannelsPanel.js +22 -5
- package/channel/AgentChannelsPanel.js.map +1 -1
- package/channel/ChannelTemplatesDialog.d.ts +53 -0
- package/channel/ChannelTemplatesDialog.d.ts.map +1 -0
- package/channel/ChannelTemplatesDialog.js +150 -0
- package/channel/ChannelTemplatesDialog.js.map +1 -0
- package/channel/index.d.ts +8 -0
- package/channel/index.d.ts.map +1 -1
- package/channel/index.js +4 -0
- package/channel/index.js.map +1 -1
- package/channel/providers.d.ts +12 -0
- package/channel/providers.d.ts.map +1 -1
- package/channel/providers.js +14 -2
- package/channel/providers.js.map +1 -1
- package/channel/templatePresentation.d.ts +37 -0
- package/channel/templatePresentation.d.ts.map +1 -0
- package/channel/templatePresentation.js +69 -0
- package/channel/templatePresentation.js.map +1 -0
- package/channel/useChannelTemplateList.d.ts +64 -0
- package/channel/useChannelTemplateList.d.ts.map +1 -0
- package/channel/useChannelTemplateList.js +69 -0
- package/channel/useChannelTemplateList.js.map +1 -0
- package/channel/useChannelTemplateReadiness.d.ts +53 -0
- package/channel/useChannelTemplateReadiness.d.ts.map +1 -0
- package/channel/useChannelTemplateReadiness.js +42 -0
- package/channel/useChannelTemplateReadiness.js.map +1 -0
- package/dashboard/useDashboardFailedRuns.d.ts.map +1 -1
- package/dashboard/useDashboardFailedRuns.js +2 -0
- package/dashboard/useDashboardFailedRuns.js.map +1 -1
- package/index.d.ts +4 -2
- package/index.d.ts.map +1 -1
- package/index.js +9 -1
- package/index.js.map +1 -1
- package/library/useExportResource.d.ts +4 -3
- package/library/useExportResource.d.ts.map +1 -1
- package/library/useExportResource.js +1 -1
- package/library/useExportResource.js.map +1 -1
- package/organization/OrgProvider.d.ts +11 -4
- package/organization/OrgProvider.d.ts.map +1 -1
- package/organization/OrgProvider.js +28 -4
- package/organization/OrgProvider.js.map +1 -1
- package/package.json +4 -4
- package/resource-detail/useDeleteResource.d.ts +1 -1
- package/resource-detail/useDeleteResource.d.ts.map +1 -1
- package/resource-detail/useDeleteResource.js +5 -0
- package/resource-detail/useDeleteResource.js.map +1 -1
- package/resource-workbench/components/ResourceWorkbench.d.ts +10 -1
- package/resource-workbench/components/ResourceWorkbench.d.ts.map +1 -1
- package/resource-workbench/components/ResourceWorkbench.js +6 -2
- package/resource-workbench/components/ResourceWorkbench.js.map +1 -1
- package/resource-workbench/hooks/useResourceCollection.d.ts +8 -0
- package/resource-workbench/hooks/useResourceCollection.d.ts.map +1 -1
- package/resource-workbench/hooks/useResourceCollection.js +2 -2
- package/resource-workbench/hooks/useResourceCollection.js.map +1 -1
- package/schedule/CadenceField.d.ts +31 -0
- package/schedule/CadenceField.d.ts.map +1 -0
- package/schedule/CadenceField.js +116 -0
- package/schedule/CadenceField.js.map +1 -0
- package/schedule/ScheduleDetailView.d.ts +58 -0
- package/schedule/ScheduleDetailView.d.ts.map +1 -0
- package/schedule/ScheduleDetailView.js +236 -0
- package/schedule/ScheduleDetailView.js.map +1 -0
- package/schedule/ScheduleForm.d.ts +43 -0
- package/schedule/ScheduleForm.d.ts.map +1 -0
- package/schedule/ScheduleForm.js +129 -0
- package/schedule/ScheduleForm.js.map +1 -0
- package/schedule/ScheduleRowActions.d.ts +20 -0
- package/schedule/ScheduleRowActions.d.ts.map +1 -0
- package/schedule/ScheduleRowActions.js +54 -0
- package/schedule/ScheduleRowActions.js.map +1 -0
- package/schedule/TimeZoneField.d.ts +32 -0
- package/schedule/TimeZoneField.d.ts.map +1 -0
- package/schedule/TimeZoneField.js +104 -0
- package/schedule/TimeZoneField.js.map +1 -0
- package/schedule/cadence.d.ts +95 -0
- package/schedule/cadence.d.ts.map +1 -0
- package/schedule/cadence.js +242 -0
- package/schedule/cadence.js.map +1 -0
- package/schedule/index.d.ts +31 -0
- package/schedule/index.d.ts.map +1 -0
- package/schedule/index.js +16 -0
- package/schedule/index.js.map +1 -0
- package/schedule/scheduleColumns.d.ts +26 -0
- package/schedule/scheduleColumns.d.ts.map +1 -0
- package/schedule/scheduleColumns.js +77 -0
- package/schedule/scheduleColumns.js.map +1 -0
- package/schedule/scheduleListFn.d.ts +50 -0
- package/schedule/scheduleListFn.d.ts.map +1 -0
- package/schedule/scheduleListFn.js +65 -0
- package/schedule/scheduleListFn.js.map +1 -0
- package/schedule/scheduleState.d.ts +43 -0
- package/schedule/scheduleState.d.ts.map +1 -0
- package/schedule/scheduleState.js +67 -0
- package/schedule/scheduleState.js.map +1 -0
- package/schedule/useCreateSchedule.d.ts +57 -0
- package/schedule/useCreateSchedule.d.ts.map +1 -0
- package/schedule/useCreateSchedule.js +61 -0
- package/schedule/useCreateSchedule.js.map +1 -0
- package/schedule/useResumeSchedule.d.ts +30 -0
- package/schedule/useResumeSchedule.d.ts.map +1 -0
- package/schedule/useResumeSchedule.js +45 -0
- package/schedule/useResumeSchedule.js.map +1 -0
- package/schedule/useSchedule.d.ts +39 -0
- package/schedule/useSchedule.d.ts.map +1 -0
- package/schedule/useSchedule.js +44 -0
- package/schedule/useSchedule.js.map +1 -0
- package/schedule/useScheduleCount.d.ts +38 -0
- package/schedule/useScheduleCount.d.ts.map +1 -0
- package/schedule/useScheduleCount.js +30 -0
- package/schedule/useScheduleCount.js.map +1 -0
- package/schedule/useScheduleList.d.ts +48 -0
- package/schedule/useScheduleList.d.ts.map +1 -0
- package/schedule/useScheduleList.js +43 -0
- package/schedule/useScheduleList.js.map +1 -0
- package/schedule/useSetScheduleEnabled.d.ts +31 -0
- package/schedule/useSetScheduleEnabled.d.ts.map +1 -0
- package/schedule/useSetScheduleEnabled.js +53 -0
- package/schedule/useSetScheduleEnabled.js.map +1 -0
- package/schedule/useTriggerSchedule.d.ts +28 -0
- package/schedule/useTriggerSchedule.d.ts.map +1 -0
- package/schedule/useTriggerSchedule.js +43 -0
- package/schedule/useTriggerSchedule.js.map +1 -0
- package/search/useResourceCount.d.ts +2 -1
- package/search/useResourceCount.d.ts.map +1 -1
- package/search/useResourceCount.js +6 -1
- package/search/useResourceCount.js.map +1 -1
- package/src/agent/AgentPicker.tsx +13 -1
- package/src/channel/AgentChannelsPanel.tsx +43 -1
- package/src/channel/ChannelTemplatesDialog.tsx +433 -0
- package/src/channel/__tests__/AgentChannelsPanel.test.tsx +73 -0
- package/src/channel/__tests__/ChannelTemplatesDialog.test.tsx +292 -0
- package/src/channel/__tests__/providers.test.ts +9 -0
- package/src/channel/__tests__/templatePresentation.test.ts +96 -0
- package/src/channel/__tests__/useChannelTemplateList.test.tsx +216 -0
- package/src/channel/__tests__/useChannelTemplateReadiness.test.tsx +124 -0
- package/src/channel/index.ts +15 -0
- package/src/channel/providers.ts +26 -2
- package/src/channel/templatePresentation.ts +82 -0
- package/src/channel/useChannelTemplateList.ts +123 -0
- package/src/channel/useChannelTemplateReadiness.ts +69 -0
- package/src/dashboard/__tests__/useDashboardFailedRuns.test.tsx +106 -0
- package/src/dashboard/useDashboardFailedRuns.ts +2 -0
- package/src/index.ts +65 -0
- package/src/library/useExportResource.ts +4 -3
- package/src/organization/OrgProvider.tsx +29 -4
- package/src/organization/__tests__/OrgProvider.orgSwitch.test.tsx +89 -0
- package/src/resource-detail/useDeleteResource.ts +11 -1
- package/src/resource-workbench/__tests__/ResourceWorkbench.test.tsx +112 -0
- package/src/resource-workbench/components/ResourceWorkbench.tsx +32 -15
- package/src/resource-workbench/hooks/useResourceCollection.ts +11 -1
- package/src/schedule/CadenceField.tsx +365 -0
- package/src/schedule/ScheduleDetailView.tsx +583 -0
- package/src/schedule/ScheduleForm.tsx +405 -0
- package/src/schedule/ScheduleRowActions.tsx +124 -0
- package/src/schedule/TimeZoneField.tsx +199 -0
- package/src/schedule/__tests__/ScheduleDetailView.test.tsx +260 -0
- package/src/schedule/__tests__/cadence.test.ts +278 -0
- package/src/schedule/__tests__/scheduleColumnsRowActions.test.tsx +176 -0
- package/src/schedule/__tests__/scheduleCreation.test.tsx +340 -0
- package/src/schedule/__tests__/scheduleHooks.test.tsx +312 -0
- package/src/schedule/__tests__/scheduleListFn.test.ts +128 -0
- package/src/schedule/__tests__/scheduleState.test.ts +133 -0
- package/src/schedule/cadence.ts +298 -0
- package/src/schedule/index.ts +57 -0
- package/src/schedule/scheduleColumns.tsx +118 -0
- package/src/schedule/scheduleListFn.ts +108 -0
- package/src/schedule/scheduleState.ts +96 -0
- package/src/schedule/useCreateSchedule.ts +88 -0
- package/src/schedule/useResumeSchedule.ts +67 -0
- package/src/schedule/useSchedule.ts +67 -0
- package/src/schedule/useScheduleCount.ts +57 -0
- package/src/schedule/useScheduleList.ts +83 -0
- package/src/schedule/useSetScheduleEnabled.ts +84 -0
- package/src/schedule/useTriggerSchedule.ts +65 -0
- package/src/search/__tests__/useResourceCount.test.tsx +89 -0
- package/src/search/useResourceCount.ts +8 -2
- package/src/workflow/WorkflowDashboard.tsx +1 -0
- package/src/workflow/useWorkflowExecutionList.ts +13 -2
- package/styles.css +1 -1
- package/workflow/CanvasTransitionEdge.d.ts +1 -1
- package/workflow/WorkflowDashboard.d.ts.map +1 -1
- package/workflow/WorkflowDashboard.js +1 -0
- package/workflow/WorkflowDashboard.js.map +1 -1
- package/workflow/useWorkflowExecutionList.d.ts +10 -0
- package/workflow/useWorkflowExecutionList.d.ts.map +1 -1
- package/workflow/useWorkflowExecutionList.js +3 -2
- package/workflow/useWorkflowExecutionList.js.map +1 -1
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
cadenceToCron,
|
|
4
|
+
cronToCadence,
|
|
5
|
+
describeCadence,
|
|
6
|
+
formatTime,
|
|
7
|
+
validateCron,
|
|
8
|
+
validateTimeZone,
|
|
9
|
+
type CadencePreset,
|
|
10
|
+
} from "../cadence";
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Preset → cron → preset round-trips
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
describe("cadenceToCron / cronToCadence round-trips", () => {
|
|
17
|
+
const cases: readonly { preset: CadencePreset; cron: string }[] = [
|
|
18
|
+
{ preset: { kind: "hourly", minute: 0 }, cron: "0 * * * *" },
|
|
19
|
+
{ preset: { kind: "hourly", minute: 45 }, cron: "45 * * * *" },
|
|
20
|
+
{ preset: { kind: "daily", hour: 9, minute: 0 }, cron: "0 9 * * *" },
|
|
21
|
+
{ preset: { kind: "daily", hour: 0, minute: 0 }, cron: "0 0 * * *" },
|
|
22
|
+
{ preset: { kind: "daily", hour: 23, minute: 59 }, cron: "59 23 * * *" },
|
|
23
|
+
{
|
|
24
|
+
preset: { kind: "weekly", days: [1], hour: 9, minute: 30 },
|
|
25
|
+
cron: "30 9 * * 1",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
preset: { kind: "weekly", days: [1, 3, 5], hour: 8, minute: 15 },
|
|
29
|
+
cron: "15 8 * * 1,3,5",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
preset: { kind: "weekly", days: [0, 6], hour: 10, minute: 0 },
|
|
33
|
+
cron: "0 10 * * 0,6",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
preset: { kind: "monthly", day: 1, hour: 9, minute: 0 },
|
|
37
|
+
cron: "0 9 1 * *",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
preset: { kind: "monthly", day: 31, hour: 6, minute: 30 },
|
|
41
|
+
cron: "30 6 31 * *",
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
for (const { preset, cron } of cases) {
|
|
46
|
+
it(`${JSON.stringify(preset)} ⇄ "${cron}"`, () => {
|
|
47
|
+
expect(cadenceToCron(preset)).toBe(cron);
|
|
48
|
+
expect(cronToCadence(cron)).toEqual(preset);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
it("generated cron always passes the platform's lexical validation", () => {
|
|
53
|
+
for (const { preset } of cases) {
|
|
54
|
+
expect(validateCron(cadenceToCron(preset))).toBeNull();
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("canonicalizes weekly day order and duplicates", () => {
|
|
59
|
+
expect(
|
|
60
|
+
cadenceToCron({ kind: "weekly", days: [5, 1, 3, 1], hour: 9, minute: 0 }),
|
|
61
|
+
).toBe("0 9 * * 1,3,5");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("returns custom cron verbatim", () => {
|
|
65
|
+
expect(cadenceToCron({ kind: "custom", cron: "0 9 * * MON-FRI" })).toBe(
|
|
66
|
+
"0 9 * * MON-FRI",
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("cronToCadence recognition boundaries", () => {
|
|
72
|
+
it("maps calendar shorthands to their preset equivalents", () => {
|
|
73
|
+
expect(cronToCadence("@hourly")).toEqual({ kind: "hourly", minute: 0 });
|
|
74
|
+
expect(cronToCadence("@daily")).toEqual({ kind: "daily", hour: 0, minute: 0 });
|
|
75
|
+
expect(cronToCadence("@weekly")).toEqual({
|
|
76
|
+
kind: "weekly",
|
|
77
|
+
days: [0],
|
|
78
|
+
hour: 0,
|
|
79
|
+
minute: 0,
|
|
80
|
+
});
|
|
81
|
+
expect(cronToCadence("@monthly")).toEqual({
|
|
82
|
+
kind: "monthly",
|
|
83
|
+
day: 1,
|
|
84
|
+
hour: 0,
|
|
85
|
+
minute: 0,
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("sorts unordered day lists into canonical form", () => {
|
|
90
|
+
// The builder emits sorted lists, but a hand-written spec may not.
|
|
91
|
+
expect(cronToCadence("0 9 * * 5,1")).toEqual({
|
|
92
|
+
kind: "weekly",
|
|
93
|
+
days: [1, 5],
|
|
94
|
+
hour: 9,
|
|
95
|
+
minute: 0,
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("tolerates surrounding and duplicate whitespace", () => {
|
|
100
|
+
expect(cronToCadence(" 0 9 * * * ")).toEqual({
|
|
101
|
+
kind: "daily",
|
|
102
|
+
hour: 9,
|
|
103
|
+
minute: 0,
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const fallsBackToCustom = [
|
|
108
|
+
"@yearly", // valid, but no preset arm can express it
|
|
109
|
+
"*/5 * * * *", // steps
|
|
110
|
+
"0 9 * * MON-FRI", // names and ranges
|
|
111
|
+
"0 9 1,15 * *", // multiple days of month
|
|
112
|
+
"0 9-17 * * *", // hour ranges
|
|
113
|
+
"60 * * * *", // out-of-range minute
|
|
114
|
+
"0 24 * * *", // out-of-range hour
|
|
115
|
+
"0 9 0 * *", // out-of-range day of month
|
|
116
|
+
"0 9 * * 7", // out-of-range day of week
|
|
117
|
+
"0 9 * * 1,1", // duplicate days
|
|
118
|
+
"not a cron",
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
for (const cron of fallsBackToCustom) {
|
|
122
|
+
it(`"${cron}" round-trips as custom with the raw string intact`, () => {
|
|
123
|
+
expect(cronToCadence(cron)).toEqual({ kind: "custom", cron });
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// Plain-English summaries
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
describe("describeCadence", () => {
|
|
133
|
+
it("describes each preset", () => {
|
|
134
|
+
expect(describeCadence({ kind: "hourly", minute: 5 })).toBe(
|
|
135
|
+
"Every hour at minute 5",
|
|
136
|
+
);
|
|
137
|
+
expect(describeCadence({ kind: "daily", hour: 9, minute: 0 })).toBe(
|
|
138
|
+
"Every day at 09:00",
|
|
139
|
+
);
|
|
140
|
+
expect(
|
|
141
|
+
describeCadence({ kind: "weekly", days: [1], hour: 9, minute: 30 }),
|
|
142
|
+
).toBe("Every Monday at 09:30");
|
|
143
|
+
expect(
|
|
144
|
+
describeCadence({ kind: "weekly", days: [1, 3, 5], hour: 9, minute: 0 }),
|
|
145
|
+
).toBe("Every Monday, Wednesday and Friday at 09:00");
|
|
146
|
+
expect(
|
|
147
|
+
describeCadence({
|
|
148
|
+
kind: "weekly",
|
|
149
|
+
days: [0, 1, 2, 3, 4, 5, 6],
|
|
150
|
+
hour: 9,
|
|
151
|
+
minute: 0,
|
|
152
|
+
}),
|
|
153
|
+
).toBe("Every day at 09:00");
|
|
154
|
+
expect(
|
|
155
|
+
describeCadence({ kind: "monthly", day: 15, hour: 9, minute: 0 }),
|
|
156
|
+
).toBe("On day 15 of every month at 09:00");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("appends the time zone when provided", () => {
|
|
160
|
+
expect(
|
|
161
|
+
describeCadence({ kind: "daily", hour: 9, minute: 0 }, "Asia/Kolkata"),
|
|
162
|
+
).toBe("Every day at 09:00 (Asia/Kolkata)");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("formats times zero-padded on a 24-hour clock", () => {
|
|
166
|
+
expect(formatTime(0, 0)).toBe("00:00");
|
|
167
|
+
expect(formatTime(9, 5)).toBe("09:05");
|
|
168
|
+
expect(formatTime(23, 59)).toBe("23:59");
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// Lexical cron validation — mirrors cron_test.go's rejection matrix.
|
|
174
|
+
// The wording is the user-facing contract, pinned byte-identically in
|
|
175
|
+
// the Go and Java editions; a change on any side must change all three.
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
describe("validateCron", () => {
|
|
179
|
+
const accepted = [
|
|
180
|
+
"0 9 * * *",
|
|
181
|
+
"*/5 * * * *",
|
|
182
|
+
"0 0 1,15 * *",
|
|
183
|
+
"30 8 * * MON-FRI",
|
|
184
|
+
"0 12 * Jan,Apr *",
|
|
185
|
+
"15 8-17 * * 0-6/2",
|
|
186
|
+
"@hourly",
|
|
187
|
+
"@daily",
|
|
188
|
+
"@weekly",
|
|
189
|
+
"@monthly",
|
|
190
|
+
"@yearly",
|
|
191
|
+
" 0 9 * * * ",
|
|
192
|
+
];
|
|
193
|
+
|
|
194
|
+
for (const cron of accepted) {
|
|
195
|
+
it(`accepts "${cron}"`, () => {
|
|
196
|
+
expect(validateCron(cron)).toBeNull();
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const rejected: readonly { cron: string; message: string }[] = [
|
|
201
|
+
{
|
|
202
|
+
cron: "CRON_TZ=America/New_York 0 9 * * *",
|
|
203
|
+
message:
|
|
204
|
+
"spec.cron must not carry a timezone prefix (CRON_TZ=/TZ=) — spec.time_zone is the single timezone authority",
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
cron: "TZ=UTC 0 9 * * *",
|
|
208
|
+
message:
|
|
209
|
+
"spec.cron must not carry a timezone prefix (CRON_TZ=/TZ=) — spec.time_zone is the single timezone authority",
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
cron: "@every 30s",
|
|
213
|
+
message:
|
|
214
|
+
"spec.cron must be a calendar expression — @every intervals are not supported",
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
cron: "@annually",
|
|
218
|
+
message:
|
|
219
|
+
'spec.cron shorthand "@annually" is not supported — use @hourly, @daily, @weekly, @monthly, or @yearly, or a 5-field cron expression',
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
cron: "@midnight",
|
|
223
|
+
message:
|
|
224
|
+
'spec.cron shorthand "@midnight" is not supported — use @hourly, @daily, @weekly, @monthly, or @yearly, or a 5-field cron expression',
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
cron: "0 9 * * * # fee reminders",
|
|
228
|
+
message: "spec.cron must not contain a comment (#)",
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
cron: "0 9 * * * 2027",
|
|
232
|
+
message:
|
|
233
|
+
"spec.cron must have exactly 5 fields (minute hour day-of-month month day-of-week); got 6",
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
cron: "* * * * * * *",
|
|
237
|
+
message:
|
|
238
|
+
"spec.cron must have exactly 5 fields (minute hour day-of-month month day-of-week); got 7",
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
cron: "0 9 *",
|
|
242
|
+
message:
|
|
243
|
+
"spec.cron must have exactly 5 fields (minute hour day-of-month month day-of-week); got 3",
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
cron: " ",
|
|
247
|
+
message:
|
|
248
|
+
"spec.cron must have exactly 5 fields (minute hour day-of-month month day-of-week); got 0",
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
cron: "0 9 ? * *",
|
|
252
|
+
message:
|
|
253
|
+
`spec.cron field "?" contains unsupported characters — allowed: digits, names, '*', ',', '-', '/'`,
|
|
254
|
+
},
|
|
255
|
+
];
|
|
256
|
+
|
|
257
|
+
for (const { cron, message } of rejected) {
|
|
258
|
+
it(`rejects "${cron}" with the server's exact wording`, () => {
|
|
259
|
+
expect(validateCron(cron)).toBe(message);
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
describe("validateTimeZone", () => {
|
|
265
|
+
for (const zone of ["UTC", "Asia/Kolkata", "America/New_York", "Europe/Berlin"]) {
|
|
266
|
+
it(`accepts "${zone}"`, () => {
|
|
267
|
+
expect(validateTimeZone(zone)).toBeNull();
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
for (const zone of ["Not/AZone", "Local"]) {
|
|
272
|
+
it(`rejects "${zone}" with the server's exact wording`, () => {
|
|
273
|
+
expect(validateTimeZone(zone)).toBe(
|
|
274
|
+
`spec.time_zone "${zone}" is not a valid IANA time zone (e.g. "Asia/Kolkata")`,
|
|
275
|
+
);
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
});
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeAll, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { render, screen, waitFor, fireEvent, cleanup } from "@testing-library/react";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import { create } from "@bufbuild/protobuf";
|
|
5
|
+
import { timestampFromDate } from "@bufbuild/protobuf/wkt";
|
|
6
|
+
import { ScheduleSchema, type Schedule } from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/api_pb";
|
|
7
|
+
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
8
|
+
import { StigmerContext } from "../../context";
|
|
9
|
+
import { createScheduleColumns } from "../scheduleColumns";
|
|
10
|
+
import { ScheduleRowActions } from "../ScheduleRowActions";
|
|
11
|
+
|
|
12
|
+
vi.mock("../../feedback/toast", () => ({
|
|
13
|
+
toast: Object.assign(vi.fn(), { success: vi.fn(), error: vi.fn() }),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
beforeAll(() => {
|
|
17
|
+
// happy-dom does not implement the native dialog show/close methods.
|
|
18
|
+
HTMLDialogElement.prototype.showModal = function showModal() {
|
|
19
|
+
this.open = true;
|
|
20
|
+
};
|
|
21
|
+
HTMLDialogElement.prototype.close = function close() {
|
|
22
|
+
this.open = false;
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
beforeEach(() => vi.clearAllMocks());
|
|
27
|
+
afterEach(cleanup);
|
|
28
|
+
|
|
29
|
+
const NOW = new Date("2026-08-04T10:00:00Z");
|
|
30
|
+
|
|
31
|
+
function makeSchedule(overrides?: {
|
|
32
|
+
enabled?: boolean;
|
|
33
|
+
pausedReason?: string;
|
|
34
|
+
}): Schedule {
|
|
35
|
+
return create(ScheduleSchema, {
|
|
36
|
+
metadata: {
|
|
37
|
+
id: "sch_01example",
|
|
38
|
+
name: "daily-fee-reminders",
|
|
39
|
+
slug: "daily-fee-reminders",
|
|
40
|
+
org: "isc",
|
|
41
|
+
},
|
|
42
|
+
spec: {
|
|
43
|
+
cron: "0 9 * * *",
|
|
44
|
+
timeZone: "Asia/Kolkata",
|
|
45
|
+
enabled: overrides?.enabled ?? true,
|
|
46
|
+
target: {
|
|
47
|
+
case: "agent",
|
|
48
|
+
value: {
|
|
49
|
+
agentRef: { kind: ApiResourceKind.agent, org: "isc", slug: "fee-reminder" },
|
|
50
|
+
message: "Send reminders.",
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
status: {
|
|
55
|
+
nextFireAt: timestampFromDate(new Date(NOW.getTime() + 2 * 3_600_000)),
|
|
56
|
+
lastFireAt: timestampFromDate(new Date(NOW.getTime() - 10 * 60_000)),
|
|
57
|
+
pausedReason: overrides?.pausedReason ?? "",
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
describe("createScheduleColumns", () => {
|
|
63
|
+
const columns = createScheduleColumns({ now: () => NOW });
|
|
64
|
+
const byId = new Map(columns.map((c) => [c.id, c]));
|
|
65
|
+
|
|
66
|
+
it("declares the operational column set the issue calls for", () => {
|
|
67
|
+
expect(columns.map((c) => c.id)).toEqual([
|
|
68
|
+
"name",
|
|
69
|
+
"state",
|
|
70
|
+
"cron",
|
|
71
|
+
"next-fire",
|
|
72
|
+
"last-run",
|
|
73
|
+
"target",
|
|
74
|
+
]);
|
|
75
|
+
// No fake sort affordances: the direct query has one fixed order.
|
|
76
|
+
expect(columns.every((c) => !c.sortable)).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("renders live status straight from the Schedule proto", () => {
|
|
80
|
+
const schedule = makeSchedule();
|
|
81
|
+
render(
|
|
82
|
+
<>
|
|
83
|
+
{byId.get("state")!.cell(schedule)}
|
|
84
|
+
{byId.get("next-fire")!.cell(schedule)}
|
|
85
|
+
{byId.get("last-run")!.cell(schedule)}
|
|
86
|
+
{byId.get("target")!.cell(schedule)}
|
|
87
|
+
</>,
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
expect(screen.getByText("Active")).toBeTruthy();
|
|
91
|
+
expect(screen.getByText("in 2h")).toBeTruthy();
|
|
92
|
+
expect(screen.getByText("10m")).toBeTruthy();
|
|
93
|
+
expect(screen.getByText("isc/fee-reminder")).toBeTruthy();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("shows no countdown for a disabled schedule", () => {
|
|
97
|
+
const schedule = makeSchedule({ enabled: false });
|
|
98
|
+
render(
|
|
99
|
+
<>
|
|
100
|
+
{byId.get("state")!.cell(schedule)}
|
|
101
|
+
{byId.get("next-fire")!.cell(schedule)}
|
|
102
|
+
</>,
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
expect(screen.getByText("Disabled")).toBeTruthy();
|
|
106
|
+
expect(screen.queryByText("in 2h")).toBeNull();
|
|
107
|
+
expect(screen.getByText("—")).toBeTruthy();
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe("ScheduleRowActions", () => {
|
|
112
|
+
function renderRow(
|
|
113
|
+
schedule: Schedule,
|
|
114
|
+
client: unknown,
|
|
115
|
+
onChanged?: () => void,
|
|
116
|
+
) {
|
|
117
|
+
function Wrapper({ children }: { children: ReactNode }) {
|
|
118
|
+
return (
|
|
119
|
+
<StigmerContext.Provider value={client as never}>
|
|
120
|
+
{children}
|
|
121
|
+
</StigmerContext.Provider>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
return render(
|
|
125
|
+
<ScheduleRowActions schedule={schedule} onChanged={onChanged} />,
|
|
126
|
+
{ wrapper: Wrapper },
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
it("gates Run now behind a confirmation, then triggers and notifies", async () => {
|
|
131
|
+
const trigger = vi.fn().mockResolvedValue(makeSchedule());
|
|
132
|
+
const onChanged = vi.fn();
|
|
133
|
+
renderRow(makeSchedule(), { schedule: { trigger } }, onChanged);
|
|
134
|
+
|
|
135
|
+
fireEvent.click(screen.getByRole("button", { name: /Run .* now/ }));
|
|
136
|
+
expect(trigger).not.toHaveBeenCalled();
|
|
137
|
+
|
|
138
|
+
fireEvent.click(screen.getByText("Start run"));
|
|
139
|
+
await waitFor(() => expect(trigger).toHaveBeenCalledWith("sch_01example"));
|
|
140
|
+
await waitFor(() => expect(onChanged).toHaveBeenCalled());
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("offers Resume on a paused schedule, one click", async () => {
|
|
144
|
+
const resume = vi.fn().mockResolvedValue(makeSchedule());
|
|
145
|
+
const onChanged = vi.fn();
|
|
146
|
+
renderRow(
|
|
147
|
+
makeSchedule({ pausedReason: "5 consecutive failures" }),
|
|
148
|
+
{ schedule: { resume } },
|
|
149
|
+
onChanged,
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
fireEvent.click(screen.getByRole("button", { name: /Resume/ }));
|
|
153
|
+
await waitFor(() => expect(resume).toHaveBeenCalledWith("sch_01example"));
|
|
154
|
+
await waitFor(() => expect(onChanged).toHaveBeenCalled());
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("renders nothing for an owner-disabled schedule", () => {
|
|
158
|
+
const { container } = renderRow(makeSchedule({ enabled: false }), {});
|
|
159
|
+
expect(container.textContent).toBe("");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("never bubbles clicks into the row's navigation handler", () => {
|
|
163
|
+
const onRowClick = vi.fn();
|
|
164
|
+
const trigger = vi.fn();
|
|
165
|
+
render(
|
|
166
|
+
<StigmerContext.Provider value={{ schedule: { trigger } } as never}>
|
|
167
|
+
<div onClick={onRowClick}>
|
|
168
|
+
<ScheduleRowActions schedule={makeSchedule()} />
|
|
169
|
+
</div>
|
|
170
|
+
</StigmerContext.Provider>,
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
fireEvent.click(screen.getByRole("button", { name: /Run .* now/ }));
|
|
174
|
+
expect(onRowClick).not.toHaveBeenCalled();
|
|
175
|
+
});
|
|
176
|
+
});
|