@herdctl/core 0.0.1 → 0.0.2
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/dist/config/__tests__/agent.test.js +31 -13
- package/dist/config/__tests__/agent.test.js.map +1 -1
- package/dist/config/__tests__/merge.test.js +9 -2
- package/dist/config/__tests__/merge.test.js.map +1 -1
- package/dist/config/__tests__/schema.test.js +350 -1
- package/dist/config/__tests__/schema.test.js.map +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +3 -1
- package/dist/config/index.js.map +1 -1
- package/dist/config/schema.d.ts +828 -24
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +118 -6
- package/dist/config/schema.js.map +1 -1
- package/dist/fleet-manager/__tests__/coverage.test.js +11 -332
- package/dist/fleet-manager/__tests__/coverage.test.js.map +1 -1
- package/dist/fleet-manager/__tests__/errors.test.js +1 -49
- package/dist/fleet-manager/__tests__/errors.test.js.map +1 -1
- package/dist/fleet-manager/__tests__/integration.test.js +109 -0
- package/dist/fleet-manager/__tests__/integration.test.js.map +1 -1
- package/dist/fleet-manager/__tests__/reload.test.js +1 -1
- package/dist/fleet-manager/__tests__/reload.test.js.map +1 -1
- package/dist/fleet-manager/config-reload.d.ts +164 -0
- package/dist/fleet-manager/config-reload.d.ts.map +1 -0
- package/dist/fleet-manager/config-reload.js +445 -0
- package/dist/fleet-manager/config-reload.js.map +1 -0
- package/dist/fleet-manager/context.d.ts +76 -0
- package/dist/fleet-manager/context.d.ts.map +1 -0
- package/dist/fleet-manager/context.js +11 -0
- package/dist/fleet-manager/context.js.map +1 -0
- package/dist/fleet-manager/errors.d.ts +0 -25
- package/dist/fleet-manager/errors.d.ts.map +1 -1
- package/dist/fleet-manager/errors.js +0 -38
- package/dist/fleet-manager/errors.js.map +1 -1
- package/dist/fleet-manager/event-emitters.d.ts +123 -0
- package/dist/fleet-manager/event-emitters.d.ts.map +1 -0
- package/dist/fleet-manager/event-emitters.js +136 -0
- package/dist/fleet-manager/event-emitters.js.map +1 -0
- package/dist/fleet-manager/event-types.d.ts +0 -15
- package/dist/fleet-manager/event-types.d.ts.map +1 -1
- package/dist/fleet-manager/fleet-manager.d.ts +40 -653
- package/dist/fleet-manager/fleet-manager.d.ts.map +1 -1
- package/dist/fleet-manager/fleet-manager.js +95 -1720
- package/dist/fleet-manager/fleet-manager.js.map +1 -1
- package/dist/fleet-manager/index.d.ts +13 -2
- package/dist/fleet-manager/index.d.ts.map +1 -1
- package/dist/fleet-manager/index.js +19 -6
- package/dist/fleet-manager/index.js.map +1 -1
- package/dist/fleet-manager/job-control.d.ts +64 -0
- package/dist/fleet-manager/job-control.d.ts.map +1 -0
- package/dist/fleet-manager/job-control.js +296 -0
- package/dist/fleet-manager/job-control.js.map +1 -0
- package/dist/fleet-manager/log-streaming.d.ts +171 -0
- package/dist/fleet-manager/log-streaming.d.ts.map +1 -0
- package/dist/fleet-manager/log-streaming.js +503 -0
- package/dist/fleet-manager/log-streaming.js.map +1 -0
- package/dist/fleet-manager/schedule-executor.d.ts +63 -0
- package/dist/fleet-manager/schedule-executor.d.ts.map +1 -0
- package/dist/fleet-manager/schedule-executor.js +209 -0
- package/dist/fleet-manager/schedule-executor.js.map +1 -0
- package/dist/fleet-manager/schedule-management.d.ts +71 -0
- package/dist/fleet-manager/schedule-management.d.ts.map +1 -0
- package/dist/fleet-manager/schedule-management.js +171 -0
- package/dist/fleet-manager/schedule-management.js.map +1 -0
- package/dist/fleet-manager/status-queries.d.ts +105 -0
- package/dist/fleet-manager/status-queries.d.ts.map +1 -0
- package/dist/fleet-manager/status-queries.js +247 -0
- package/dist/fleet-manager/status-queries.js.map +1 -0
- package/dist/fleet-manager/types.d.ts +0 -39
- package/dist/fleet-manager/types.d.ts.map +1 -1
- package/dist/runner/__tests__/job-executor.test.js +206 -1
- package/dist/runner/__tests__/job-executor.test.js.map +1 -1
- package/dist/runner/job-executor.d.ts +9 -0
- package/dist/runner/job-executor.d.ts.map +1 -1
- package/dist/runner/job-executor.js +78 -4
- package/dist/runner/job-executor.js.map +1 -1
- package/dist/runner/types.d.ts +2 -0
- package/dist/runner/types.d.ts.map +1 -1
- package/dist/scheduler/__tests__/cron.test.d.ts +2 -0
- package/dist/scheduler/__tests__/cron.test.d.ts.map +1 -0
- package/dist/scheduler/__tests__/cron.test.js +867 -0
- package/dist/scheduler/__tests__/cron.test.js.map +1 -0
- package/dist/scheduler/__tests__/scheduler.test.js +164 -5
- package/dist/scheduler/__tests__/scheduler.test.js.map +1 -1
- package/dist/scheduler/cron.d.ts +126 -0
- package/dist/scheduler/cron.d.ts.map +1 -0
- package/dist/scheduler/cron.js +390 -0
- package/dist/scheduler/cron.js.map +1 -0
- package/dist/scheduler/errors.d.ts +81 -1
- package/dist/scheduler/errors.d.ts.map +1 -1
- package/dist/scheduler/errors.js +81 -6
- package/dist/scheduler/errors.js.map +1 -1
- package/dist/scheduler/index.d.ts +1 -0
- package/dist/scheduler/index.d.ts.map +1 -1
- package/dist/scheduler/index.js +2 -0
- package/dist/scheduler/index.js.map +1 -1
- package/dist/scheduler/schedule-runner.d.ts +2 -2
- package/dist/scheduler/schedule-runner.d.ts.map +1 -1
- package/dist/scheduler/schedule-runner.js +20 -8
- package/dist/scheduler/schedule-runner.js.map +1 -1
- package/dist/scheduler/scheduler.d.ts +4 -4
- package/dist/scheduler/scheduler.d.ts.map +1 -1
- package/dist/scheduler/scheduler.js +86 -20
- package/dist/scheduler/scheduler.js.map +1 -1
- package/dist/scheduler/types.d.ts +1 -1
- package/dist/scheduler/types.d.ts.map +1 -1
- package/dist/state/schemas/job-metadata.d.ts +2 -2
- package/package.json +33 -8
- package/.turbo/turbo-build.log +0 -4
- package/.turbo/turbo-test.log +0 -219
- package/.turbo/turbo-typecheck.log +0 -4
- package/coverage/base.css +0 -224
- package/coverage/block-navigation.js +0 -87
- package/coverage/coverage-final.json +0 -51
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +0 -251
- package/coverage/prettify.css +0 -1
- package/coverage/prettify.js +0 -2
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +0 -210
- package/coverage/src/config/index.html +0 -191
- package/coverage/src/config/index.ts.html +0 -442
- package/coverage/src/config/interpolate.ts.html +0 -652
- package/coverage/src/config/loader.ts.html +0 -1501
- package/coverage/src/config/merge.ts.html +0 -823
- package/coverage/src/config/parser.ts.html +0 -1213
- package/coverage/src/config/schema.ts.html +0 -1123
- package/coverage/src/fleet-manager/errors.ts.html +0 -2326
- package/coverage/src/fleet-manager/event-types.ts.html +0 -1219
- package/coverage/src/fleet-manager/fleet-manager.ts.html +0 -7030
- package/coverage/src/fleet-manager/index.html +0 -206
- package/coverage/src/fleet-manager/index.ts.html +0 -469
- package/coverage/src/fleet-manager/job-manager.ts.html +0 -2074
- package/coverage/src/fleet-manager/job-queue.ts.html +0 -2479
- package/coverage/src/fleet-manager/types.ts.html +0 -2602
- package/coverage/src/index.html +0 -116
- package/coverage/src/index.ts.html +0 -181
- package/coverage/src/runner/errors.ts.html +0 -1006
- package/coverage/src/runner/index.html +0 -191
- package/coverage/src/runner/index.ts.html +0 -256
- package/coverage/src/runner/job-executor.ts.html +0 -1429
- package/coverage/src/runner/message-processor.ts.html +0 -1150
- package/coverage/src/runner/sdk-adapter.ts.html +0 -658
- package/coverage/src/runner/types.ts.html +0 -559
- package/coverage/src/scheduler/errors.ts.html +0 -388
- package/coverage/src/scheduler/index.html +0 -206
- package/coverage/src/scheduler/index.ts.html +0 -244
- package/coverage/src/scheduler/interval.ts.html +0 -652
- package/coverage/src/scheduler/schedule-runner.ts.html +0 -1411
- package/coverage/src/scheduler/schedule-state.ts.html +0 -718
- package/coverage/src/scheduler/scheduler.ts.html +0 -1795
- package/coverage/src/scheduler/types.ts.html +0 -733
- package/coverage/src/state/directory.ts.html +0 -736
- package/coverage/src/state/errors.ts.html +0 -376
- package/coverage/src/state/fleet-state.ts.html +0 -937
- package/coverage/src/state/index.html +0 -221
- package/coverage/src/state/index.ts.html +0 -322
- package/coverage/src/state/job-metadata.ts.html +0 -1420
- package/coverage/src/state/job-output.ts.html +0 -1033
- package/coverage/src/state/schemas/fleet-state.ts.html +0 -445
- package/coverage/src/state/schemas/index.html +0 -176
- package/coverage/src/state/schemas/index.ts.html +0 -286
- package/coverage/src/state/schemas/job-metadata.ts.html +0 -628
- package/coverage/src/state/schemas/job-output.ts.html +0 -616
- package/coverage/src/state/schemas/session-info.ts.html +0 -361
- package/coverage/src/state/session.ts.html +0 -844
- package/coverage/src/state/types.ts.html +0 -262
- package/coverage/src/state/utils/atomic.ts.html +0 -748
- package/coverage/src/state/utils/index.html +0 -146
- package/coverage/src/state/utils/index.ts.html +0 -103
- package/coverage/src/state/utils/reads.ts.html +0 -1621
- package/coverage/src/work-sources/adapters/github.ts.html +0 -3583
- package/coverage/src/work-sources/adapters/index.html +0 -131
- package/coverage/src/work-sources/adapters/index.ts.html +0 -277
- package/coverage/src/work-sources/errors.ts.html +0 -298
- package/coverage/src/work-sources/index.html +0 -176
- package/coverage/src/work-sources/index.ts.html +0 -529
- package/coverage/src/work-sources/manager.ts.html +0 -1324
- package/coverage/src/work-sources/registry.ts.html +0 -619
- package/coverage/src/work-sources/types.ts.html +0 -568
- package/dist/fleet-manager/__tests__/event-helpers.test.d.ts +0 -7
- package/dist/fleet-manager/__tests__/event-helpers.test.d.ts.map +0 -1
- package/dist/fleet-manager/__tests__/event-helpers.test.js +0 -368
- package/dist/fleet-manager/__tests__/event-helpers.test.js.map +0 -1
- package/src/config/__tests__/agent.test.ts +0 -864
- package/src/config/__tests__/interpolate.test.ts +0 -644
- package/src/config/__tests__/loader.test.ts +0 -784
- package/src/config/__tests__/merge.test.ts +0 -751
- package/src/config/__tests__/parser.test.ts +0 -533
- package/src/config/__tests__/schema.test.ts +0 -873
- package/src/config/index.ts +0 -119
- package/src/config/interpolate.ts +0 -189
- package/src/config/loader.ts +0 -472
- package/src/config/merge.ts +0 -246
- package/src/config/parser.ts +0 -376
- package/src/config/schema.ts +0 -346
- package/src/fleet-manager/__tests__/coverage.test.ts +0 -2869
- package/src/fleet-manager/__tests__/errors.test.ts +0 -660
- package/src/fleet-manager/__tests__/event-helpers.test.ts +0 -448
- package/src/fleet-manager/__tests__/integration.test.ts +0 -1209
- package/src/fleet-manager/__tests__/job-control.test.ts +0 -283
- package/src/fleet-manager/__tests__/job-manager.test.ts +0 -869
- package/src/fleet-manager/__tests__/job-queue.test.ts +0 -401
- package/src/fleet-manager/__tests__/reload.test.ts +0 -751
- package/src/fleet-manager/__tests__/status-queries.test.ts +0 -595
- package/src/fleet-manager/__tests__/trigger.test.ts +0 -601
- package/src/fleet-manager/errors.ts +0 -747
- package/src/fleet-manager/event-types.ts +0 -378
- package/src/fleet-manager/fleet-manager.ts +0 -2315
- package/src/fleet-manager/index.ts +0 -128
- package/src/fleet-manager/job-manager.ts +0 -663
- package/src/fleet-manager/job-queue.ts +0 -798
- package/src/fleet-manager/types.ts +0 -839
- package/src/index.ts +0 -32
- package/src/runner/__tests__/errors.test.ts +0 -382
- package/src/runner/__tests__/job-executor.test.ts +0 -1708
- package/src/runner/__tests__/message-processor.test.ts +0 -960
- package/src/runner/__tests__/sdk-adapter.test.ts +0 -626
- package/src/runner/errors.ts +0 -307
- package/src/runner/index.ts +0 -57
- package/src/runner/job-executor.ts +0 -448
- package/src/runner/message-processor.ts +0 -355
- package/src/runner/sdk-adapter.ts +0 -191
- package/src/runner/types.ts +0 -158
- package/src/scheduler/__tests__/errors.test.ts +0 -159
- package/src/scheduler/__tests__/interval.test.ts +0 -515
- package/src/scheduler/__tests__/schedule-runner.test.ts +0 -798
- package/src/scheduler/__tests__/schedule-state.test.ts +0 -671
- package/src/scheduler/__tests__/scheduler.test.ts +0 -1280
- package/src/scheduler/errors.ts +0 -101
- package/src/scheduler/index.ts +0 -53
- package/src/scheduler/interval.ts +0 -189
- package/src/scheduler/schedule-runner.ts +0 -442
- package/src/scheduler/schedule-state.ts +0 -211
- package/src/scheduler/scheduler.ts +0 -570
- package/src/scheduler/types.ts +0 -216
- package/src/state/__tests__/directory.test.ts +0 -595
- package/src/state/__tests__/fleet-state.test.ts +0 -868
- package/src/state/__tests__/job-metadata-schema.test.ts +0 -414
- package/src/state/__tests__/job-metadata.test.ts +0 -831
- package/src/state/__tests__/job-output.test.ts +0 -856
- package/src/state/__tests__/session-schema.test.ts +0 -378
- package/src/state/__tests__/session.test.ts +0 -604
- package/src/state/directory.ts +0 -217
- package/src/state/errors.ts +0 -97
- package/src/state/fleet-state.ts +0 -284
- package/src/state/index.ts +0 -79
- package/src/state/job-metadata.ts +0 -445
- package/src/state/job-output.ts +0 -316
- package/src/state/schemas/__tests__/job-output.test.ts +0 -338
- package/src/state/schemas/fleet-state.ts +0 -120
- package/src/state/schemas/index.ts +0 -67
- package/src/state/schemas/job-metadata.ts +0 -181
- package/src/state/schemas/job-output.ts +0 -177
- package/src/state/schemas/session-info.ts +0 -92
- package/src/state/session.ts +0 -253
- package/src/state/types.ts +0 -59
- package/src/state/utils/__tests__/atomic.test.ts +0 -723
- package/src/state/utils/__tests__/reads.test.ts +0 -1071
- package/src/state/utils/atomic.ts +0 -221
- package/src/state/utils/index.ts +0 -6
- package/src/state/utils/reads.ts +0 -512
- package/src/work-sources/__tests__/github.test.ts +0 -1800
- package/src/work-sources/__tests__/manager.test.ts +0 -529
- package/src/work-sources/__tests__/registry.test.ts +0 -477
- package/src/work-sources/__tests__/types.test.ts +0 -479
- package/src/work-sources/adapters/github.ts +0 -1166
- package/src/work-sources/adapters/index.ts +0 -64
- package/src/work-sources/errors.ts +0 -71
- package/src/work-sources/index.ts +0 -148
- package/src/work-sources/manager.ts +0 -413
- package/src/work-sources/registry.ts +0 -178
- package/src/work-sources/types.ts +0 -161
- package/tsconfig.json +0 -9
- package/vitest.config.ts +0 -19
|
@@ -0,0 +1,867 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { parseCronExpression, getNextCronTrigger, calculateNextCronTrigger, isValidCronExpression, } from "../cron.js";
|
|
3
|
+
import { CronParseError, SchedulerErrorCode } from "../errors.js";
|
|
4
|
+
import { FleetManagerError } from "../../fleet-manager/errors.js";
|
|
5
|
+
// =============================================================================
|
|
6
|
+
// parseCronExpression - Standard 5-field expressions
|
|
7
|
+
// =============================================================================
|
|
8
|
+
describe("parseCronExpression", () => {
|
|
9
|
+
describe("standard 5-field cron expressions", () => {
|
|
10
|
+
it("parses basic cron expressions", () => {
|
|
11
|
+
const result = parseCronExpression("0 9 * * *");
|
|
12
|
+
expect(result.expression).toBe("0 9 * * *");
|
|
13
|
+
expect(result.isShorthand).toBe(false);
|
|
14
|
+
expect(result.cronExpression).toBeDefined();
|
|
15
|
+
});
|
|
16
|
+
it("parses expressions with all wildcards", () => {
|
|
17
|
+
const result = parseCronExpression("* * * * *");
|
|
18
|
+
expect(result.expression).toBe("* * * * *");
|
|
19
|
+
expect(result.isShorthand).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
it("parses expressions with specific values", () => {
|
|
22
|
+
const result = parseCronExpression("30 14 1 6 3");
|
|
23
|
+
expect(result.expression).toBe("30 14 1 6 3");
|
|
24
|
+
expect(result.isShorthand).toBe(false);
|
|
25
|
+
});
|
|
26
|
+
it("parses expressions with ranges", () => {
|
|
27
|
+
const result = parseCronExpression("0 9 * * 1-5");
|
|
28
|
+
expect(result.expression).toBe("0 9 * * 1-5");
|
|
29
|
+
expect(result.isShorthand).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
it("parses expressions with steps", () => {
|
|
32
|
+
const result = parseCronExpression("*/15 * * * *");
|
|
33
|
+
expect(result.expression).toBe("*/15 * * * *");
|
|
34
|
+
expect(result.isShorthand).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
it("parses expressions with lists", () => {
|
|
37
|
+
const result = parseCronExpression("0 9,12,18 * * *");
|
|
38
|
+
expect(result.expression).toBe("0 9,12,18 * * *");
|
|
39
|
+
expect(result.isShorthand).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
it("parses complex expressions with combined syntax", () => {
|
|
42
|
+
const result = parseCronExpression("0,30 9-17 1-15 1,6 1-5");
|
|
43
|
+
expect(result.expression).toBe("0,30 9-17 1-15 1,6 1-5");
|
|
44
|
+
expect(result.isShorthand).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
it("handles whitespace around the expression", () => {
|
|
47
|
+
const result = parseCronExpression(" 0 9 * * * ");
|
|
48
|
+
expect(result.expression).toBe("0 9 * * *");
|
|
49
|
+
expect(result.isShorthand).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
it("handles multiple spaces between fields", () => {
|
|
52
|
+
const result = parseCronExpression("0 9 * * *");
|
|
53
|
+
expect(result.expression).toBe("0 9 * * *");
|
|
54
|
+
expect(result.isShorthand).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
// =============================================================================
|
|
58
|
+
// parseCronExpression - Shorthands
|
|
59
|
+
// =============================================================================
|
|
60
|
+
describe("cron shorthands", () => {
|
|
61
|
+
it("parses @yearly shorthand", () => {
|
|
62
|
+
const result = parseCronExpression("@yearly");
|
|
63
|
+
expect(result.expression).toBe("0 0 1 1 *");
|
|
64
|
+
expect(result.isShorthand).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
it("parses @annually shorthand (alias for @yearly)", () => {
|
|
67
|
+
const result = parseCronExpression("@annually");
|
|
68
|
+
expect(result.expression).toBe("0 0 1 1 *");
|
|
69
|
+
expect(result.isShorthand).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
it("parses @monthly shorthand", () => {
|
|
72
|
+
const result = parseCronExpression("@monthly");
|
|
73
|
+
expect(result.expression).toBe("0 0 1 * *");
|
|
74
|
+
expect(result.isShorthand).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
it("parses @weekly shorthand", () => {
|
|
77
|
+
const result = parseCronExpression("@weekly");
|
|
78
|
+
expect(result.expression).toBe("0 0 * * 0");
|
|
79
|
+
expect(result.isShorthand).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
it("parses @daily shorthand", () => {
|
|
82
|
+
const result = parseCronExpression("@daily");
|
|
83
|
+
expect(result.expression).toBe("0 0 * * *");
|
|
84
|
+
expect(result.isShorthand).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
it("parses @midnight shorthand (alias for @daily)", () => {
|
|
87
|
+
const result = parseCronExpression("@midnight");
|
|
88
|
+
expect(result.expression).toBe("0 0 * * *");
|
|
89
|
+
expect(result.isShorthand).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
it("parses @hourly shorthand", () => {
|
|
92
|
+
const result = parseCronExpression("@hourly");
|
|
93
|
+
expect(result.expression).toBe("0 * * * *");
|
|
94
|
+
expect(result.isShorthand).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
it("handles case-insensitive shorthands", () => {
|
|
97
|
+
expect(parseCronExpression("@DAILY").expression).toBe("0 0 * * *");
|
|
98
|
+
expect(parseCronExpression("@Daily").expression).toBe("0 0 * * *");
|
|
99
|
+
expect(parseCronExpression("@HOURLY").expression).toBe("0 * * * *");
|
|
100
|
+
expect(parseCronExpression("@Weekly").expression).toBe("0 0 * * 0");
|
|
101
|
+
});
|
|
102
|
+
it("handles whitespace around shorthands", () => {
|
|
103
|
+
const result = parseCronExpression(" @daily ");
|
|
104
|
+
expect(result.expression).toBe("0 0 * * *");
|
|
105
|
+
expect(result.isShorthand).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
// =============================================================================
|
|
109
|
+
// parseCronExpression - Empty string
|
|
110
|
+
// =============================================================================
|
|
111
|
+
describe("empty string handling", () => {
|
|
112
|
+
it("throws CronParseError for empty string", () => {
|
|
113
|
+
expect(() => parseCronExpression("")).toThrow(CronParseError);
|
|
114
|
+
expect(() => parseCronExpression("")).toThrow(/cannot be empty/);
|
|
115
|
+
});
|
|
116
|
+
it("throws CronParseError for whitespace-only string", () => {
|
|
117
|
+
expect(() => parseCronExpression(" ")).toThrow(CronParseError);
|
|
118
|
+
expect(() => parseCronExpression("\t")).toThrow(CronParseError);
|
|
119
|
+
expect(() => parseCronExpression("\n")).toThrow(CronParseError);
|
|
120
|
+
});
|
|
121
|
+
it("includes the empty expression in the error", () => {
|
|
122
|
+
try {
|
|
123
|
+
parseCronExpression("");
|
|
124
|
+
}
|
|
125
|
+
catch (e) {
|
|
126
|
+
expect(e).toBeInstanceOf(CronParseError);
|
|
127
|
+
expect(e.expression).toBe("");
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
// =============================================================================
|
|
132
|
+
// parseCronExpression - Invalid expressions
|
|
133
|
+
// =============================================================================
|
|
134
|
+
describe("invalid expression handling", () => {
|
|
135
|
+
it("throws CronParseError for unknown shorthand", () => {
|
|
136
|
+
expect(() => parseCronExpression("@every5m")).toThrow(CronParseError);
|
|
137
|
+
expect(() => parseCronExpression("@every5m")).toThrow(/Unknown cron shorthand/);
|
|
138
|
+
});
|
|
139
|
+
it("suggests valid shorthands in error message", () => {
|
|
140
|
+
try {
|
|
141
|
+
parseCronExpression("@invalid");
|
|
142
|
+
}
|
|
143
|
+
catch (e) {
|
|
144
|
+
expect(e.message).toContain("@daily");
|
|
145
|
+
expect(e.message).toContain("@hourly");
|
|
146
|
+
expect(e.message).toContain("@weekly");
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
it("throws CronParseError for invalid minute value", () => {
|
|
150
|
+
expect(() => parseCronExpression("60 * * * *")).toThrow(CronParseError);
|
|
151
|
+
});
|
|
152
|
+
it("throws CronParseError for invalid hour value", () => {
|
|
153
|
+
expect(() => parseCronExpression("0 24 * * *")).toThrow(CronParseError);
|
|
154
|
+
});
|
|
155
|
+
it("throws CronParseError for invalid day of month value", () => {
|
|
156
|
+
expect(() => parseCronExpression("0 0 32 * *")).toThrow(CronParseError);
|
|
157
|
+
});
|
|
158
|
+
it("throws CronParseError for invalid month value", () => {
|
|
159
|
+
expect(() => parseCronExpression("0 0 * 13 *")).toThrow(CronParseError);
|
|
160
|
+
});
|
|
161
|
+
it("throws CronParseError for invalid day of week value", () => {
|
|
162
|
+
expect(() => parseCronExpression("0 0 * * 8")).toThrow(CronParseError);
|
|
163
|
+
});
|
|
164
|
+
it("throws CronParseError for negative values", () => {
|
|
165
|
+
// Negative values are not valid in cron expressions
|
|
166
|
+
expect(() => parseCronExpression("-1 * * * *")).toThrow(CronParseError);
|
|
167
|
+
});
|
|
168
|
+
it("throws CronParseError for random invalid input", () => {
|
|
169
|
+
expect(() => parseCronExpression("invalid")).toThrow(CronParseError);
|
|
170
|
+
expect(() => parseCronExpression("not a cron")).toThrow(CronParseError);
|
|
171
|
+
});
|
|
172
|
+
it("includes the original expression in the error", () => {
|
|
173
|
+
try {
|
|
174
|
+
parseCronExpression("60 * * * *");
|
|
175
|
+
}
|
|
176
|
+
catch (e) {
|
|
177
|
+
expect(e).toBeInstanceOf(CronParseError);
|
|
178
|
+
expect(e.expression).toBe("60 * * * *");
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
it("preserves the underlying cause when cron-parser throws", () => {
|
|
182
|
+
// Note: Some errors are now caught by our custom validation before cron-parser,
|
|
183
|
+
// so we need to use an expression that passes our validation but fails cron-parser.
|
|
184
|
+
// Currently, our validation catches most common errors, so we test that
|
|
185
|
+
// the cause is either defined (if cron-parser threw) or undefined (if we caught it early).
|
|
186
|
+
try {
|
|
187
|
+
// Use an expression that's syntactically valid but will cause cron-parser issues
|
|
188
|
+
parseCronExpression("invalid syntax here");
|
|
189
|
+
}
|
|
190
|
+
catch (e) {
|
|
191
|
+
expect(e).toBeInstanceOf(CronParseError);
|
|
192
|
+
// The error should have a cause from cron-parser if it wasn't caught by our validation
|
|
193
|
+
// It's okay if cause is undefined when we catch the error early
|
|
194
|
+
expect(e).toBeInstanceOf(CronParseError);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
// =============================================================================
|
|
199
|
+
// CronParseError properties
|
|
200
|
+
// =============================================================================
|
|
201
|
+
describe("CronParseError", () => {
|
|
202
|
+
it("has correct name property", () => {
|
|
203
|
+
try {
|
|
204
|
+
parseCronExpression("invalid");
|
|
205
|
+
}
|
|
206
|
+
catch (e) {
|
|
207
|
+
expect(e).toBeInstanceOf(CronParseError);
|
|
208
|
+
expect(e.name).toBe("CronParseError");
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
it("preserves the expression string", () => {
|
|
212
|
+
const testInputs = ["", "@invalid", "60 * * * *", "not valid"];
|
|
213
|
+
for (const input of testInputs) {
|
|
214
|
+
try {
|
|
215
|
+
parseCronExpression(input);
|
|
216
|
+
}
|
|
217
|
+
catch (e) {
|
|
218
|
+
expect(e.expression).toBe(input);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
it("has descriptive error messages", () => {
|
|
223
|
+
try {
|
|
224
|
+
parseCronExpression("60 * * * *");
|
|
225
|
+
}
|
|
226
|
+
catch (e) {
|
|
227
|
+
expect(e).toBeInstanceOf(CronParseError);
|
|
228
|
+
expect(e.message).toContain("60 * * * *");
|
|
229
|
+
expect(e.message.length).toBeGreaterThan(20);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
it("extends FleetManagerError", () => {
|
|
233
|
+
try {
|
|
234
|
+
parseCronExpression("invalid");
|
|
235
|
+
}
|
|
236
|
+
catch (e) {
|
|
237
|
+
expect(e).toBeInstanceOf(FleetManagerError);
|
|
238
|
+
expect(e).toBeInstanceOf(CronParseError);
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
it("has correct error code", () => {
|
|
242
|
+
try {
|
|
243
|
+
parseCronExpression("invalid");
|
|
244
|
+
}
|
|
245
|
+
catch (e) {
|
|
246
|
+
expect(e.code).toBe(SchedulerErrorCode.CRON_PARSE_ERROR);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
// =============================================================================
|
|
251
|
+
// Error message content tests (US-4 acceptance criteria)
|
|
252
|
+
// =============================================================================
|
|
253
|
+
describe("error message content", () => {
|
|
254
|
+
it("includes what's wrong and a valid example for invalid hour", () => {
|
|
255
|
+
try {
|
|
256
|
+
parseCronExpression("0 25 * * *");
|
|
257
|
+
expect.fail("Should have thrown CronParseError");
|
|
258
|
+
}
|
|
259
|
+
catch (e) {
|
|
260
|
+
expect(e).toBeInstanceOf(CronParseError);
|
|
261
|
+
const error = e;
|
|
262
|
+
// Should mention the invalid expression
|
|
263
|
+
expect(error.message).toContain("0 25 * * *");
|
|
264
|
+
// Should mention hour constraint
|
|
265
|
+
expect(error.message).toContain("hour");
|
|
266
|
+
expect(error.message).toContain("0-23");
|
|
267
|
+
// Should include an example
|
|
268
|
+
expect(error.message).toMatch(/Example valid expression:/i);
|
|
269
|
+
// Error should have field property
|
|
270
|
+
expect(error.field).toBe("hour");
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
it("includes what's wrong and a valid example for wrong field count", () => {
|
|
274
|
+
try {
|
|
275
|
+
parseCronExpression("* * *");
|
|
276
|
+
expect.fail("Should have thrown CronParseError");
|
|
277
|
+
}
|
|
278
|
+
catch (e) {
|
|
279
|
+
expect(e).toBeInstanceOf(CronParseError);
|
|
280
|
+
const error = e;
|
|
281
|
+
// Should mention the invalid expression
|
|
282
|
+
expect(error.message).toContain("* * *");
|
|
283
|
+
// Should mention field count
|
|
284
|
+
expect(error.message).toContain("expected 5 fields");
|
|
285
|
+
expect(error.message).toContain("got 3");
|
|
286
|
+
// Should include an example
|
|
287
|
+
expect(error.message).toMatch(/Example valid expression:/i);
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
it("includes what's wrong and a valid example for invalid day-of-week", () => {
|
|
291
|
+
try {
|
|
292
|
+
parseCronExpression("0 9 * * 8");
|
|
293
|
+
expect.fail("Should have thrown CronParseError");
|
|
294
|
+
}
|
|
295
|
+
catch (e) {
|
|
296
|
+
expect(e).toBeInstanceOf(CronParseError);
|
|
297
|
+
const error = e;
|
|
298
|
+
// Should mention the invalid expression
|
|
299
|
+
expect(error.message).toContain("0 9 * * 8");
|
|
300
|
+
// Should mention day-of-week constraint
|
|
301
|
+
expect(error.message).toContain("day-of-week");
|
|
302
|
+
expect(error.message).toContain("0-7");
|
|
303
|
+
// Should include an example
|
|
304
|
+
expect(error.message).toMatch(/Example valid expression:/i);
|
|
305
|
+
// Error should have field property
|
|
306
|
+
expect(error.field).toBe("day-of-week");
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
it("includes what's wrong and a valid example for invalid minute", () => {
|
|
310
|
+
try {
|
|
311
|
+
parseCronExpression("60 * * * *");
|
|
312
|
+
expect.fail("Should have thrown CronParseError");
|
|
313
|
+
}
|
|
314
|
+
catch (e) {
|
|
315
|
+
expect(e).toBeInstanceOf(CronParseError);
|
|
316
|
+
const error = e;
|
|
317
|
+
// Should mention minute constraint
|
|
318
|
+
expect(error.message).toContain("minute");
|
|
319
|
+
expect(error.message).toContain("0-59");
|
|
320
|
+
// Should include an example
|
|
321
|
+
expect(error.message).toMatch(/Example valid expression:/i);
|
|
322
|
+
// Error should have field property
|
|
323
|
+
expect(error.field).toBe("minute");
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
it("includes what's wrong and a valid example for invalid day-of-month", () => {
|
|
327
|
+
try {
|
|
328
|
+
parseCronExpression("0 0 32 * *");
|
|
329
|
+
expect.fail("Should have thrown CronParseError");
|
|
330
|
+
}
|
|
331
|
+
catch (e) {
|
|
332
|
+
expect(e).toBeInstanceOf(CronParseError);
|
|
333
|
+
const error = e;
|
|
334
|
+
// Should mention day-of-month constraint
|
|
335
|
+
expect(error.message).toContain("day-of-month");
|
|
336
|
+
expect(error.message).toContain("1-31");
|
|
337
|
+
// Should include an example
|
|
338
|
+
expect(error.message).toMatch(/Example valid expression:/i);
|
|
339
|
+
// Error should have field property
|
|
340
|
+
expect(error.field).toBe("day-of-month");
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
it("includes what's wrong and a valid example for invalid month", () => {
|
|
344
|
+
try {
|
|
345
|
+
parseCronExpression("0 0 * 13 *");
|
|
346
|
+
expect.fail("Should have thrown CronParseError");
|
|
347
|
+
}
|
|
348
|
+
catch (e) {
|
|
349
|
+
expect(e).toBeInstanceOf(CronParseError);
|
|
350
|
+
const error = e;
|
|
351
|
+
// Should mention month constraint
|
|
352
|
+
expect(error.message).toContain("month");
|
|
353
|
+
expect(error.message).toContain("1-12");
|
|
354
|
+
// Should include an example
|
|
355
|
+
expect(error.message).toMatch(/Example valid expression:/i);
|
|
356
|
+
// Error should have field property
|
|
357
|
+
expect(error.field).toBe("month");
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
it("includes what's wrong for too many fields", () => {
|
|
361
|
+
try {
|
|
362
|
+
parseCronExpression("0 9 * * * *");
|
|
363
|
+
expect.fail("Should have thrown CronParseError");
|
|
364
|
+
}
|
|
365
|
+
catch (e) {
|
|
366
|
+
expect(e).toBeInstanceOf(CronParseError);
|
|
367
|
+
const error = e;
|
|
368
|
+
// Should mention field count
|
|
369
|
+
expect(error.message).toContain("expected 5 fields");
|
|
370
|
+
expect(error.message).toContain("got 6");
|
|
371
|
+
// Should include an example
|
|
372
|
+
expect(error.message).toMatch(/Example valid expression:/i);
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
it("includes what's wrong for invalid values in ranges", () => {
|
|
376
|
+
try {
|
|
377
|
+
parseCronExpression("0 9 * * 1-8");
|
|
378
|
+
expect.fail("Should have thrown CronParseError");
|
|
379
|
+
}
|
|
380
|
+
catch (e) {
|
|
381
|
+
expect(e).toBeInstanceOf(CronParseError);
|
|
382
|
+
const error = e;
|
|
383
|
+
// Should mention day-of-week constraint
|
|
384
|
+
expect(error.message).toContain("day-of-week");
|
|
385
|
+
expect(error.message).toContain("0-7");
|
|
386
|
+
// Should include an example
|
|
387
|
+
expect(error.message).toMatch(/Example valid expression:/i);
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
it("includes what's wrong for invalid values in lists", () => {
|
|
391
|
+
try {
|
|
392
|
+
parseCronExpression("0 9,25 * * *");
|
|
393
|
+
expect.fail("Should have thrown CronParseError");
|
|
394
|
+
}
|
|
395
|
+
catch (e) {
|
|
396
|
+
expect(e).toBeInstanceOf(CronParseError);
|
|
397
|
+
const error = e;
|
|
398
|
+
// Should mention hour constraint
|
|
399
|
+
expect(error.message).toContain("hour");
|
|
400
|
+
expect(error.message).toContain("0-23");
|
|
401
|
+
// Should include an example
|
|
402
|
+
expect(error.message).toMatch(/Example valid expression:/i);
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
it("provides helpful example for hour field errors", () => {
|
|
406
|
+
try {
|
|
407
|
+
parseCronExpression("0 24 * * *");
|
|
408
|
+
expect.fail("Should have thrown CronParseError");
|
|
409
|
+
}
|
|
410
|
+
catch (e) {
|
|
411
|
+
expect(e).toBeInstanceOf(CronParseError);
|
|
412
|
+
const error = e;
|
|
413
|
+
// Should include a daily at 9 AM example for hour errors
|
|
414
|
+
expect(error.message).toContain("0 9 * * *");
|
|
415
|
+
expect(error.message.toLowerCase()).toMatch(/9.*am|daily/i);
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
it("provides helpful example for day-of-week field errors", () => {
|
|
419
|
+
try {
|
|
420
|
+
parseCronExpression("0 9 * * 8");
|
|
421
|
+
expect.fail("Should have thrown CronParseError");
|
|
422
|
+
}
|
|
423
|
+
catch (e) {
|
|
424
|
+
expect(e).toBeInstanceOf(CronParseError);
|
|
425
|
+
const error = e;
|
|
426
|
+
// Should include a weekday example
|
|
427
|
+
expect(error.message).toContain("1-5");
|
|
428
|
+
expect(error.message.toLowerCase()).toMatch(/weekday/i);
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
// =============================================================================
|
|
434
|
+
// getNextCronTrigger
|
|
435
|
+
// =============================================================================
|
|
436
|
+
describe("getNextCronTrigger", () => {
|
|
437
|
+
beforeEach(() => {
|
|
438
|
+
vi.useFakeTimers();
|
|
439
|
+
vi.setSystemTime(new Date("2024-01-15T12:00:00.000Z"));
|
|
440
|
+
});
|
|
441
|
+
afterEach(() => {
|
|
442
|
+
vi.useRealTimers();
|
|
443
|
+
});
|
|
444
|
+
describe("without fromDate", () => {
|
|
445
|
+
it("calculates next trigger from now", () => {
|
|
446
|
+
// @hourly triggers at the start of each hour
|
|
447
|
+
const result = getNextCronTrigger("@hourly");
|
|
448
|
+
// Current time is 12:00, so next trigger is 13:00
|
|
449
|
+
expect(result.getTime()).toBe(new Date("2024-01-15T13:00:00.000Z").getTime());
|
|
450
|
+
});
|
|
451
|
+
it("calculates next trigger for @daily", () => {
|
|
452
|
+
const result = getNextCronTrigger("@daily");
|
|
453
|
+
// @daily is 0 0 * * * - midnight every day
|
|
454
|
+
// Current time is Jan 15 12:00, next midnight is Jan 16 00:00
|
|
455
|
+
expect(result.getTime()).toBe(new Date("2024-01-16T00:00:00.000Z").getTime());
|
|
456
|
+
});
|
|
457
|
+
it("calculates next trigger for specific time", () => {
|
|
458
|
+
// 9 AM every day
|
|
459
|
+
const result = getNextCronTrigger("0 9 * * *");
|
|
460
|
+
// Current time is 12:00, so next 9 AM is tomorrow
|
|
461
|
+
expect(result.getTime()).toBe(new Date("2024-01-16T09:00:00.000Z").getTime());
|
|
462
|
+
});
|
|
463
|
+
it("returns same day if time hasn't passed", () => {
|
|
464
|
+
// 6 PM every day
|
|
465
|
+
const result = getNextCronTrigger("0 18 * * *");
|
|
466
|
+
// Current time is 12:00, 6 PM is later today
|
|
467
|
+
expect(result.getTime()).toBe(new Date("2024-01-15T18:00:00.000Z").getTime());
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
describe("with fromDate", () => {
|
|
471
|
+
it("calculates next trigger from specified date", () => {
|
|
472
|
+
const fromDate = new Date("2024-01-10T08:00:00.000Z");
|
|
473
|
+
const result = getNextCronTrigger("@hourly", fromDate);
|
|
474
|
+
// Next hour after 8 AM is 9 AM
|
|
475
|
+
expect(result.getTime()).toBe(new Date("2024-01-10T09:00:00.000Z").getTime());
|
|
476
|
+
});
|
|
477
|
+
it("calculates next @daily from specified date", () => {
|
|
478
|
+
const fromDate = new Date("2024-01-10T15:30:00.000Z");
|
|
479
|
+
const result = getNextCronTrigger("@daily", fromDate);
|
|
480
|
+
// Next midnight after Jan 10 15:30 is Jan 11 00:00
|
|
481
|
+
expect(result.getTime()).toBe(new Date("2024-01-11T00:00:00.000Z").getTime());
|
|
482
|
+
});
|
|
483
|
+
it("calculates next @weekly from specified date", () => {
|
|
484
|
+
const fromDate = new Date("2024-01-15T12:00:00.000Z"); // Monday
|
|
485
|
+
const result = getNextCronTrigger("@weekly", fromDate);
|
|
486
|
+
// @weekly is Sunday at midnight (day 0)
|
|
487
|
+
// Next Sunday after Monday Jan 15 is Jan 21
|
|
488
|
+
expect(result.getTime()).toBe(new Date("2024-01-21T00:00:00.000Z").getTime());
|
|
489
|
+
});
|
|
490
|
+
it("calculates next @monthly from specified date", () => {
|
|
491
|
+
const fromDate = new Date("2024-01-15T12:00:00.000Z");
|
|
492
|
+
const result = getNextCronTrigger("@monthly", fromDate);
|
|
493
|
+
// @monthly is first of month at midnight
|
|
494
|
+
// Next first after Jan 15 is Feb 1
|
|
495
|
+
expect(result.getTime()).toBe(new Date("2024-02-01T00:00:00.000Z").getTime());
|
|
496
|
+
});
|
|
497
|
+
it("calculates next @yearly from specified date", () => {
|
|
498
|
+
const fromDate = new Date("2024-01-15T12:00:00.000Z");
|
|
499
|
+
const result = getNextCronTrigger("@yearly", fromDate);
|
|
500
|
+
// @yearly is Jan 1 at midnight
|
|
501
|
+
// Next Jan 1 after Jan 15 2024 is Jan 1 2025
|
|
502
|
+
expect(result.getTime()).toBe(new Date("2025-01-01T00:00:00.000Z").getTime());
|
|
503
|
+
});
|
|
504
|
+
it("handles weekday-only schedules", () => {
|
|
505
|
+
// Friday Jan 19, 2024
|
|
506
|
+
const fromDate = new Date("2024-01-19T12:00:00.000Z");
|
|
507
|
+
// 9 AM on weekdays (Mon-Fri)
|
|
508
|
+
const result = getNextCronTrigger("0 9 * * 1-5", fromDate);
|
|
509
|
+
// Next weekday 9 AM after Friday 12:00 is Monday Jan 22 9 AM
|
|
510
|
+
expect(result.getTime()).toBe(new Date("2024-01-22T09:00:00.000Z").getTime());
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
describe("error handling", () => {
|
|
514
|
+
it("throws CronParseError for invalid expression", () => {
|
|
515
|
+
expect(() => getNextCronTrigger("invalid")).toThrow(CronParseError);
|
|
516
|
+
});
|
|
517
|
+
it("throws CronParseError for empty expression", () => {
|
|
518
|
+
expect(() => getNextCronTrigger("")).toThrow(CronParseError);
|
|
519
|
+
});
|
|
520
|
+
it("throws CronParseError for unknown shorthand", () => {
|
|
521
|
+
expect(() => getNextCronTrigger("@invalid")).toThrow(CronParseError);
|
|
522
|
+
});
|
|
523
|
+
});
|
|
524
|
+
});
|
|
525
|
+
// =============================================================================
|
|
526
|
+
// isValidCronExpression
|
|
527
|
+
// =============================================================================
|
|
528
|
+
describe("isValidCronExpression", () => {
|
|
529
|
+
describe("valid expressions", () => {
|
|
530
|
+
it("returns true for standard 5-field expressions", () => {
|
|
531
|
+
expect(isValidCronExpression("* * * * *")).toBe(true);
|
|
532
|
+
expect(isValidCronExpression("0 9 * * *")).toBe(true);
|
|
533
|
+
expect(isValidCronExpression("*/15 * * * *")).toBe(true);
|
|
534
|
+
expect(isValidCronExpression("0 9 * * 1-5")).toBe(true);
|
|
535
|
+
expect(isValidCronExpression("0,30 9-17 1-15 1,6 1-5")).toBe(true);
|
|
536
|
+
});
|
|
537
|
+
it("returns true for valid shorthands", () => {
|
|
538
|
+
expect(isValidCronExpression("@yearly")).toBe(true);
|
|
539
|
+
expect(isValidCronExpression("@annually")).toBe(true);
|
|
540
|
+
expect(isValidCronExpression("@monthly")).toBe(true);
|
|
541
|
+
expect(isValidCronExpression("@weekly")).toBe(true);
|
|
542
|
+
expect(isValidCronExpression("@daily")).toBe(true);
|
|
543
|
+
expect(isValidCronExpression("@midnight")).toBe(true);
|
|
544
|
+
expect(isValidCronExpression("@hourly")).toBe(true);
|
|
545
|
+
});
|
|
546
|
+
it("returns true for case-insensitive shorthands", () => {
|
|
547
|
+
expect(isValidCronExpression("@DAILY")).toBe(true);
|
|
548
|
+
expect(isValidCronExpression("@Daily")).toBe(true);
|
|
549
|
+
});
|
|
550
|
+
});
|
|
551
|
+
describe("invalid expressions", () => {
|
|
552
|
+
it("returns false for empty string", () => {
|
|
553
|
+
expect(isValidCronExpression("")).toBe(false);
|
|
554
|
+
expect(isValidCronExpression(" ")).toBe(false);
|
|
555
|
+
});
|
|
556
|
+
it("returns false for unknown shorthands", () => {
|
|
557
|
+
expect(isValidCronExpression("@invalid")).toBe(false);
|
|
558
|
+
expect(isValidCronExpression("@every5m")).toBe(false);
|
|
559
|
+
});
|
|
560
|
+
it("returns false for invalid field values", () => {
|
|
561
|
+
expect(isValidCronExpression("60 * * * *")).toBe(false);
|
|
562
|
+
expect(isValidCronExpression("0 24 * * *")).toBe(false);
|
|
563
|
+
expect(isValidCronExpression("0 0 32 * *")).toBe(false);
|
|
564
|
+
expect(isValidCronExpression("0 0 * 13 *")).toBe(false);
|
|
565
|
+
expect(isValidCronExpression("0 0 * * 8")).toBe(false);
|
|
566
|
+
});
|
|
567
|
+
it("returns false for negative values", () => {
|
|
568
|
+
// Negative values are not valid in cron expressions
|
|
569
|
+
expect(isValidCronExpression("-1 * * * *")).toBe(false);
|
|
570
|
+
});
|
|
571
|
+
it("returns false for random invalid input", () => {
|
|
572
|
+
expect(isValidCronExpression("invalid")).toBe(false);
|
|
573
|
+
expect(isValidCronExpression("not a cron")).toBe(false);
|
|
574
|
+
});
|
|
575
|
+
});
|
|
576
|
+
});
|
|
577
|
+
// =============================================================================
|
|
578
|
+
// calculateNextCronTrigger - System timezone calculations
|
|
579
|
+
// =============================================================================
|
|
580
|
+
describe("calculateNextCronTrigger", () => {
|
|
581
|
+
// Note: These tests use specific dates without timezone suffixes to test
|
|
582
|
+
// system timezone behavior. The function uses Intl.DateTimeFormat to get
|
|
583
|
+
// the system timezone.
|
|
584
|
+
describe("same-day future trigger", () => {
|
|
585
|
+
it("returns same-day trigger when time has not passed", () => {
|
|
586
|
+
// Daily at 9:00 AM, called at 8:00 AM
|
|
587
|
+
const expr = "0 9 * * *";
|
|
588
|
+
const morning = new Date("2024-01-15T08:00:00");
|
|
589
|
+
const result = calculateNextCronTrigger(expr, morning);
|
|
590
|
+
// Should trigger at 9:00 AM same day
|
|
591
|
+
expect(result.getDate()).toBe(15);
|
|
592
|
+
expect(result.getMonth()).toBe(0); // January
|
|
593
|
+
expect(result.getFullYear()).toBe(2024);
|
|
594
|
+
expect(result.getHours()).toBe(9);
|
|
595
|
+
expect(result.getMinutes()).toBe(0);
|
|
596
|
+
});
|
|
597
|
+
it("returns same-day trigger for frequent schedule", () => {
|
|
598
|
+
// Every 15 minutes
|
|
599
|
+
const expr = "*/15 * * * *";
|
|
600
|
+
const midHour = new Date("2024-01-15T10:07:00");
|
|
601
|
+
const result = calculateNextCronTrigger(expr, midHour);
|
|
602
|
+
// Should trigger at 10:15
|
|
603
|
+
expect(result.getHours()).toBe(10);
|
|
604
|
+
expect(result.getMinutes()).toBe(15);
|
|
605
|
+
});
|
|
606
|
+
it("returns next 15-minute interval from various times", () => {
|
|
607
|
+
const expr = "*/15 * * * *";
|
|
608
|
+
// At :00, next is :15
|
|
609
|
+
let result = calculateNextCronTrigger(expr, new Date("2024-01-15T10:00:00"));
|
|
610
|
+
expect(result.getMinutes()).toBe(15);
|
|
611
|
+
// At :14, next is :15
|
|
612
|
+
result = calculateNextCronTrigger(expr, new Date("2024-01-15T10:14:00"));
|
|
613
|
+
expect(result.getMinutes()).toBe(15);
|
|
614
|
+
// At :15, next is :30
|
|
615
|
+
result = calculateNextCronTrigger(expr, new Date("2024-01-15T10:15:00"));
|
|
616
|
+
expect(result.getMinutes()).toBe(30);
|
|
617
|
+
// At :45, next is :00 of next hour
|
|
618
|
+
result = calculateNextCronTrigger(expr, new Date("2024-01-15T10:45:00"));
|
|
619
|
+
expect(result.getHours()).toBe(11);
|
|
620
|
+
expect(result.getMinutes()).toBe(0);
|
|
621
|
+
});
|
|
622
|
+
});
|
|
623
|
+
describe("next-day rollover", () => {
|
|
624
|
+
it("rolls over to next day when time has passed", () => {
|
|
625
|
+
// Daily at 9:00 AM, called at 9:00 AM (on the exact trigger time)
|
|
626
|
+
const expr = "0 9 * * *";
|
|
627
|
+
const afterRun = new Date("2024-01-15T09:00:00");
|
|
628
|
+
const result = calculateNextCronTrigger(expr, afterRun);
|
|
629
|
+
// Should trigger next day at 9:00 AM
|
|
630
|
+
expect(result.getDate()).toBe(16);
|
|
631
|
+
expect(result.getMonth()).toBe(0);
|
|
632
|
+
expect(result.getHours()).toBe(9);
|
|
633
|
+
expect(result.getMinutes()).toBe(0);
|
|
634
|
+
});
|
|
635
|
+
it("rolls over to next day when past the trigger time", () => {
|
|
636
|
+
// Daily at 9:00 AM, called at 10:00 AM
|
|
637
|
+
const expr = "0 9 * * *";
|
|
638
|
+
const afterNine = new Date("2024-01-15T10:00:00");
|
|
639
|
+
const result = calculateNextCronTrigger(expr, afterNine);
|
|
640
|
+
expect(result.getDate()).toBe(16);
|
|
641
|
+
expect(result.getHours()).toBe(9);
|
|
642
|
+
});
|
|
643
|
+
it("handles midnight rollover", () => {
|
|
644
|
+
// Daily at midnight
|
|
645
|
+
const expr = "0 0 * * *";
|
|
646
|
+
const lateNight = new Date("2024-01-15T23:30:00");
|
|
647
|
+
const result = calculateNextCronTrigger(expr, lateNight);
|
|
648
|
+
// Next midnight is Jan 16
|
|
649
|
+
expect(result.getDate()).toBe(16);
|
|
650
|
+
expect(result.getHours()).toBe(0);
|
|
651
|
+
expect(result.getMinutes()).toBe(0);
|
|
652
|
+
});
|
|
653
|
+
});
|
|
654
|
+
describe("month boundary crossing", () => {
|
|
655
|
+
it("crosses month boundary from end of January", () => {
|
|
656
|
+
// Daily at 9:00 AM
|
|
657
|
+
const expr = "0 9 * * *";
|
|
658
|
+
const lastDayJan = new Date("2024-01-31T10:00:00");
|
|
659
|
+
const result = calculateNextCronTrigger(expr, lastDayJan);
|
|
660
|
+
// Should go to Feb 1
|
|
661
|
+
expect(result.getDate()).toBe(1);
|
|
662
|
+
expect(result.getMonth()).toBe(1); // February
|
|
663
|
+
expect(result.getHours()).toBe(9);
|
|
664
|
+
});
|
|
665
|
+
it("handles monthly schedule crossing year boundary", () => {
|
|
666
|
+
// First of month at midnight
|
|
667
|
+
const expr = "0 0 1 * *";
|
|
668
|
+
const midDecember = new Date("2024-12-15T12:00:00");
|
|
669
|
+
const result = calculateNextCronTrigger(expr, midDecember);
|
|
670
|
+
// Should go to Jan 1, 2025
|
|
671
|
+
expect(result.getDate()).toBe(1);
|
|
672
|
+
expect(result.getMonth()).toBe(0); // January
|
|
673
|
+
expect(result.getFullYear()).toBe(2025);
|
|
674
|
+
});
|
|
675
|
+
it("handles 30-day month boundary", () => {
|
|
676
|
+
// Daily at 9:00 AM
|
|
677
|
+
const expr = "0 9 * * *";
|
|
678
|
+
const lastDayApril = new Date("2024-04-30T10:00:00");
|
|
679
|
+
const result = calculateNextCronTrigger(expr, lastDayApril);
|
|
680
|
+
// Should go to May 1
|
|
681
|
+
expect(result.getDate()).toBe(1);
|
|
682
|
+
expect(result.getMonth()).toBe(4); // May
|
|
683
|
+
});
|
|
684
|
+
});
|
|
685
|
+
describe("year boundary crossing", () => {
|
|
686
|
+
it("crosses year boundary from December 31", () => {
|
|
687
|
+
// Daily at 9:00 AM
|
|
688
|
+
const expr = "0 9 * * *";
|
|
689
|
+
const newYearsEve = new Date("2024-12-31T10:00:00");
|
|
690
|
+
const result = calculateNextCronTrigger(expr, newYearsEve);
|
|
691
|
+
// Should go to Jan 1, 2025
|
|
692
|
+
expect(result.getDate()).toBe(1);
|
|
693
|
+
expect(result.getMonth()).toBe(0);
|
|
694
|
+
expect(result.getFullYear()).toBe(2025);
|
|
695
|
+
});
|
|
696
|
+
it("handles yearly schedule", () => {
|
|
697
|
+
// Jan 1 at midnight (@yearly)
|
|
698
|
+
const expr = "0 0 1 1 *";
|
|
699
|
+
const midYear = new Date("2024-06-15T12:00:00");
|
|
700
|
+
const result = calculateNextCronTrigger(expr, midYear);
|
|
701
|
+
// Should go to Jan 1, 2025
|
|
702
|
+
expect(result.getDate()).toBe(1);
|
|
703
|
+
expect(result.getMonth()).toBe(0);
|
|
704
|
+
expect(result.getFullYear()).toBe(2025);
|
|
705
|
+
});
|
|
706
|
+
});
|
|
707
|
+
describe("leap year handling", () => {
|
|
708
|
+
it("handles February 29 in leap year", () => {
|
|
709
|
+
// Daily at 9:00 AM
|
|
710
|
+
const expr = "0 9 * * *";
|
|
711
|
+
const feb28LeapYear = new Date("2024-02-28T10:00:00");
|
|
712
|
+
const result = calculateNextCronTrigger(expr, feb28LeapYear);
|
|
713
|
+
// 2024 is a leap year, so Feb 29 exists
|
|
714
|
+
expect(result.getDate()).toBe(29);
|
|
715
|
+
expect(result.getMonth()).toBe(1); // February
|
|
716
|
+
});
|
|
717
|
+
it("skips Feb 29 in non-leap year", () => {
|
|
718
|
+
// Daily at 9:00 AM
|
|
719
|
+
const expr = "0 9 * * *";
|
|
720
|
+
const feb28NonLeap = new Date("2023-02-28T10:00:00");
|
|
721
|
+
const result = calculateNextCronTrigger(expr, feb28NonLeap);
|
|
722
|
+
// 2023 is not a leap year, so goes to March 1
|
|
723
|
+
expect(result.getDate()).toBe(1);
|
|
724
|
+
expect(result.getMonth()).toBe(2); // March
|
|
725
|
+
});
|
|
726
|
+
it("handles monthly schedule on Feb 29", () => {
|
|
727
|
+
// 29th of month at midnight
|
|
728
|
+
const expr = "0 0 29 * *";
|
|
729
|
+
const janMidMonth = new Date("2024-01-15T12:00:00");
|
|
730
|
+
const result = calculateNextCronTrigger(expr, janMidMonth);
|
|
731
|
+
// Should go to Jan 29
|
|
732
|
+
expect(result.getDate()).toBe(29);
|
|
733
|
+
expect(result.getMonth()).toBe(0); // January
|
|
734
|
+
});
|
|
735
|
+
});
|
|
736
|
+
describe("day-of-week calculations", () => {
|
|
737
|
+
it("calculates next Monday correctly", () => {
|
|
738
|
+
// Monday at 9 AM (day 1)
|
|
739
|
+
const expr = "0 9 * * 1";
|
|
740
|
+
// Sunday Jan 14, 2024
|
|
741
|
+
const sunday = new Date("2024-01-14T12:00:00");
|
|
742
|
+
const result = calculateNextCronTrigger(expr, sunday);
|
|
743
|
+
// Next Monday is Jan 15
|
|
744
|
+
expect(result.getDate()).toBe(15);
|
|
745
|
+
expect(result.getDay()).toBe(1); // Monday
|
|
746
|
+
expect(result.getHours()).toBe(9);
|
|
747
|
+
});
|
|
748
|
+
it("calculates next occurrence of same day when time passed", () => {
|
|
749
|
+
// Monday at 9 AM
|
|
750
|
+
const expr = "0 9 * * 1";
|
|
751
|
+
// Monday Jan 15, 2024 at 10 AM (after 9 AM)
|
|
752
|
+
const mondayAfter = new Date("2024-01-15T10:00:00");
|
|
753
|
+
const result = calculateNextCronTrigger(expr, mondayAfter);
|
|
754
|
+
// Next Monday is Jan 22
|
|
755
|
+
expect(result.getDate()).toBe(22);
|
|
756
|
+
expect(result.getDay()).toBe(1);
|
|
757
|
+
});
|
|
758
|
+
it("handles weekday-only schedules (Mon-Fri)", () => {
|
|
759
|
+
// 9 AM on weekdays
|
|
760
|
+
const expr = "0 9 * * 1-5";
|
|
761
|
+
// Friday Jan 19, 2024 at 10 AM
|
|
762
|
+
const fridayAfter = new Date("2024-01-19T10:00:00");
|
|
763
|
+
const result = calculateNextCronTrigger(expr, fridayAfter);
|
|
764
|
+
// Next weekday is Monday Jan 22
|
|
765
|
+
expect(result.getDate()).toBe(22);
|
|
766
|
+
expect(result.getDay()).toBe(1);
|
|
767
|
+
});
|
|
768
|
+
it("handles weekend-only schedules (Sat-Sun)", () => {
|
|
769
|
+
// 9 AM on weekends
|
|
770
|
+
const expr = "0 9 * * 0,6";
|
|
771
|
+
// Wednesday Jan 17, 2024
|
|
772
|
+
const wednesday = new Date("2024-01-17T12:00:00");
|
|
773
|
+
const result = calculateNextCronTrigger(expr, wednesday);
|
|
774
|
+
// Next weekend day is Saturday Jan 20
|
|
775
|
+
expect(result.getDate()).toBe(20);
|
|
776
|
+
expect(result.getDay()).toBe(6); // Saturday
|
|
777
|
+
});
|
|
778
|
+
it("handles Sunday as day 0", () => {
|
|
779
|
+
// Sunday at noon
|
|
780
|
+
const expr = "0 12 * * 0";
|
|
781
|
+
// Monday Jan 15
|
|
782
|
+
const monday = new Date("2024-01-15T12:00:00");
|
|
783
|
+
const result = calculateNextCronTrigger(expr, monday);
|
|
784
|
+
// Next Sunday is Jan 21
|
|
785
|
+
expect(result.getDate()).toBe(21);
|
|
786
|
+
expect(result.getDay()).toBe(0);
|
|
787
|
+
});
|
|
788
|
+
});
|
|
789
|
+
describe("defaults to now when no after date provided", () => {
|
|
790
|
+
beforeEach(() => {
|
|
791
|
+
vi.useFakeTimers();
|
|
792
|
+
vi.setSystemTime(new Date("2024-01-15T12:00:00"));
|
|
793
|
+
});
|
|
794
|
+
afterEach(() => {
|
|
795
|
+
vi.useRealTimers();
|
|
796
|
+
});
|
|
797
|
+
it("uses current time when after is not provided", () => {
|
|
798
|
+
// Every hour at :00
|
|
799
|
+
const result = calculateNextCronTrigger("@hourly");
|
|
800
|
+
// Current fake time is 12:00, next is 13:00
|
|
801
|
+
expect(result.getHours()).toBe(13);
|
|
802
|
+
expect(result.getMinutes()).toBe(0);
|
|
803
|
+
});
|
|
804
|
+
it("calculates next daily trigger from now", () => {
|
|
805
|
+
// Daily at 9 AM
|
|
806
|
+
const result = calculateNextCronTrigger("0 9 * * *");
|
|
807
|
+
// Current fake time is 12:00, so next 9 AM is tomorrow
|
|
808
|
+
expect(result.getDate()).toBe(16);
|
|
809
|
+
expect(result.getHours()).toBe(9);
|
|
810
|
+
});
|
|
811
|
+
});
|
|
812
|
+
describe("error handling", () => {
|
|
813
|
+
it("throws CronParseError for invalid expression", () => {
|
|
814
|
+
expect(() => calculateNextCronTrigger("invalid")).toThrow(CronParseError);
|
|
815
|
+
});
|
|
816
|
+
it("throws CronParseError for empty expression", () => {
|
|
817
|
+
expect(() => calculateNextCronTrigger("")).toThrow(CronParseError);
|
|
818
|
+
});
|
|
819
|
+
it("throws CronParseError for unknown shorthand", () => {
|
|
820
|
+
expect(() => calculateNextCronTrigger("@invalid")).toThrow(CronParseError);
|
|
821
|
+
});
|
|
822
|
+
it("throws CronParseError for invalid field values", () => {
|
|
823
|
+
expect(() => calculateNextCronTrigger("60 * * * *")).toThrow(CronParseError);
|
|
824
|
+
expect(() => calculateNextCronTrigger("0 24 * * *")).toThrow(CronParseError);
|
|
825
|
+
});
|
|
826
|
+
});
|
|
827
|
+
describe("shorthand expressions", () => {
|
|
828
|
+
it("handles @daily shorthand", () => {
|
|
829
|
+
const afternoon = new Date("2024-01-15T14:00:00");
|
|
830
|
+
const result = calculateNextCronTrigger("@daily", afternoon);
|
|
831
|
+
// @daily is midnight, so next is Jan 16 00:00
|
|
832
|
+
expect(result.getDate()).toBe(16);
|
|
833
|
+
expect(result.getHours()).toBe(0);
|
|
834
|
+
expect(result.getMinutes()).toBe(0);
|
|
835
|
+
});
|
|
836
|
+
it("handles @hourly shorthand", () => {
|
|
837
|
+
const midHour = new Date("2024-01-15T10:30:00");
|
|
838
|
+
const result = calculateNextCronTrigger("@hourly", midHour);
|
|
839
|
+
// @hourly is start of hour, so next is 11:00
|
|
840
|
+
expect(result.getHours()).toBe(11);
|
|
841
|
+
expect(result.getMinutes()).toBe(0);
|
|
842
|
+
});
|
|
843
|
+
it("handles @weekly shorthand", () => {
|
|
844
|
+
const wednesday = new Date("2024-01-17T12:00:00");
|
|
845
|
+
const result = calculateNextCronTrigger("@weekly", wednesday);
|
|
846
|
+
// @weekly is Sunday at midnight, next Sunday is Jan 21
|
|
847
|
+
expect(result.getDate()).toBe(21);
|
|
848
|
+
expect(result.getDay()).toBe(0);
|
|
849
|
+
});
|
|
850
|
+
it("handles @monthly shorthand", () => {
|
|
851
|
+
const midMonth = new Date("2024-01-15T12:00:00");
|
|
852
|
+
const result = calculateNextCronTrigger("@monthly", midMonth);
|
|
853
|
+
// @monthly is 1st of month at midnight, so Feb 1
|
|
854
|
+
expect(result.getDate()).toBe(1);
|
|
855
|
+
expect(result.getMonth()).toBe(1);
|
|
856
|
+
});
|
|
857
|
+
it("handles @yearly shorthand", () => {
|
|
858
|
+
const midYear = new Date("2024-06-15T12:00:00");
|
|
859
|
+
const result = calculateNextCronTrigger("@yearly", midYear);
|
|
860
|
+
// @yearly is Jan 1 at midnight, so Jan 1 2025
|
|
861
|
+
expect(result.getDate()).toBe(1);
|
|
862
|
+
expect(result.getMonth()).toBe(0);
|
|
863
|
+
expect(result.getFullYear()).toBe(2025);
|
|
864
|
+
});
|
|
865
|
+
});
|
|
866
|
+
});
|
|
867
|
+
//# sourceMappingURL=cron.test.js.map
|