@rebasepro/server-core 0.0.1-canary.4d4fb3e → 0.0.1-canary.ca2cb6e
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/common/src/collections/CollectionRegistry.d.ts +8 -0
- package/dist/common/src/util/entities.d.ts +22 -0
- package/dist/common/src/util/relations.d.ts +14 -4
- package/dist/common/src/util/resolutions.d.ts +1 -1
- package/dist/index-DXVBFp5V.js +37 -0
- package/dist/index-DXVBFp5V.js.map +1 -0
- package/dist/index.es.js +49931 -55
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +28475 -34569
- package/dist/index.umd.js.map +1 -1
- package/dist/server-core/src/api/openapi-generator.d.ts +15 -1
- package/dist/server-core/src/api/types.d.ts +1 -1
- package/dist/server-core/src/auth/admin-routes.d.ts +10 -1
- package/dist/server-core/src/auth/apple-oauth.d.ts +30 -0
- package/dist/server-core/src/auth/bitbucket-oauth.d.ts +11 -0
- package/dist/server-core/src/auth/discord-oauth.d.ts +14 -0
- package/dist/server-core/src/auth/facebook-oauth.d.ts +14 -0
- package/dist/server-core/src/auth/github-oauth.d.ts +15 -0
- package/dist/server-core/src/auth/gitlab-oauth.d.ts +13 -0
- package/dist/server-core/src/auth/google-oauth.d.ts +5 -11
- package/dist/server-core/src/auth/index.d.ts +13 -2
- package/dist/server-core/src/auth/interfaces.d.ts +45 -6
- package/dist/server-core/src/auth/jwt.d.ts +1 -0
- package/dist/server-core/src/auth/linkedin-oauth.d.ts +18 -0
- package/dist/server-core/src/auth/microsoft-oauth.d.ts +16 -0
- package/dist/server-core/src/auth/middleware.d.ts +36 -11
- package/dist/server-core/src/auth/rate-limiter.d.ts +2 -2
- package/dist/server-core/src/auth/routes.d.ts +13 -3
- package/dist/server-core/src/auth/slack-oauth.d.ts +12 -0
- package/dist/server-core/src/auth/spotify-oauth.d.ts +12 -0
- package/dist/server-core/src/auth/twitter-oauth.d.ts +18 -0
- package/dist/server-core/src/cron/cron-loader.d.ts +17 -0
- package/dist/server-core/src/cron/cron-routes.d.ts +14 -0
- package/dist/server-core/src/cron/cron-scheduler.d.ts +61 -0
- package/dist/server-core/src/cron/cron-store.d.ts +32 -0
- package/dist/server-core/src/cron/index.d.ts +6 -0
- package/dist/server-core/src/email/index.d.ts +2 -2
- package/dist/server-core/src/email/templates.d.ts +9 -0
- package/dist/server-core/src/email/types.d.ts +20 -23
- package/dist/server-core/src/history/history-routes.d.ts +2 -2
- package/dist/server-core/src/index.d.ts +5 -0
- package/dist/server-core/src/init.d.ts +112 -2
- package/dist/server-core/src/singleton.d.ts +35 -0
- package/dist/server-core/src/storage/LocalStorageController.d.ts +9 -9
- package/dist/server-core/src/storage/S3StorageController.d.ts +8 -8
- package/dist/server-core/src/storage/index.d.ts +18 -11
- package/dist/server-core/src/storage/routes.d.ts +3 -3
- package/dist/server-core/src/storage/types.d.ts +28 -16
- package/dist/server-core/src/utils/dev-port.d.ts +35 -0
- package/dist/server-core/src/utils/logger.d.ts +31 -0
- package/dist/server-core/src/utils/request-logger.d.ts +19 -0
- package/dist/types/src/controllers/auth.d.ts +2 -0
- package/dist/types/src/controllers/client.d.ts +119 -7
- package/dist/types/src/controllers/collection_registry.d.ts +4 -3
- package/dist/types/src/controllers/customization_controller.d.ts +7 -1
- package/dist/types/src/controllers/data.d.ts +34 -7
- package/dist/types/src/controllers/data_driver.d.ts +20 -28
- package/dist/types/src/controllers/database_admin.d.ts +2 -2
- package/dist/types/src/controllers/email.d.ts +34 -0
- package/dist/types/src/controllers/index.d.ts +1 -0
- package/dist/types/src/controllers/local_config_persistence.d.ts +4 -4
- package/dist/types/src/controllers/navigation.d.ts +5 -5
- package/dist/types/src/controllers/registry.d.ts +6 -3
- package/dist/types/src/controllers/side_entity_controller.d.ts +7 -6
- package/dist/types/src/controllers/storage.d.ts +24 -26
- package/dist/types/src/rebase_context.d.ts +8 -4
- package/dist/types/src/types/backend.d.ts +4 -1
- package/dist/types/src/types/builders.d.ts +5 -4
- package/dist/types/src/types/chips.d.ts +1 -1
- package/dist/types/src/types/collections.d.ts +169 -125
- package/dist/types/src/types/cron.d.ts +102 -0
- package/dist/types/src/types/data_source.d.ts +1 -1
- package/dist/types/src/types/entity_actions.d.ts +8 -8
- package/dist/types/src/types/entity_callbacks.d.ts +15 -15
- package/dist/types/src/types/entity_link_builder.d.ts +1 -1
- package/dist/types/src/types/entity_overrides.d.ts +2 -1
- package/dist/types/src/types/entity_views.d.ts +8 -8
- package/dist/types/src/types/export_import.d.ts +3 -3
- package/dist/types/src/types/index.d.ts +1 -0
- package/dist/types/src/types/plugins.d.ts +72 -18
- package/dist/types/src/types/properties.d.ts +118 -33
- package/dist/types/src/types/relations.d.ts +1 -1
- package/dist/types/src/types/slots.d.ts +30 -6
- package/dist/types/src/types/translations.d.ts +44 -0
- package/dist/types/src/types/user_management_delegate.d.ts +1 -0
- package/package.json +84 -84
- package/scratch.ts +6 -5
- package/src/api/ast-schema-editor.ts +11 -11
- package/src/api/collections_for_test/callbacks_test_collection.ts +5 -2
- package/src/api/errors.ts +38 -14
- package/src/api/graphql/graphql-schema-generator.ts +5 -3
- package/src/api/openapi-generator.ts +613 -58
- package/src/api/rest/api-generator.ts +17 -11
- package/src/api/rest/query-parser.ts +1 -1
- package/src/api/schema-editor-routes.ts +2 -0
- package/src/api/server.ts +8 -5
- package/src/api/types.ts +1 -1
- package/src/auth/admin-routes.ts +58 -17
- package/src/auth/apple-oauth.ts +130 -0
- package/src/auth/bitbucket-oauth.ts +82 -0
- package/src/auth/discord-oauth.ts +83 -0
- package/src/auth/facebook-oauth.ts +72 -0
- package/src/auth/github-oauth.ts +110 -0
- package/src/auth/gitlab-oauth.ts +70 -0
- package/src/auth/google-oauth.ts +34 -46
- package/src/auth/index.ts +15 -2
- package/src/auth/interfaces.ts +54 -7
- package/src/auth/jwt.ts +22 -5
- package/src/auth/linkedin-oauth.ts +81 -0
- package/src/auth/microsoft-oauth.ts +88 -0
- package/src/auth/middleware.ts +194 -45
- package/src/auth/password.ts +3 -1
- package/src/auth/rate-limiter.ts +4 -4
- package/src/auth/routes.ts +197 -139
- package/src/auth/slack-oauth.ts +71 -0
- package/src/auth/spotify-oauth.ts +67 -0
- package/src/auth/twitter-oauth.ts +120 -0
- package/src/collections/loader.ts +6 -6
- package/src/cron/cron-loader.ts +89 -0
- package/src/cron/cron-routes.test.ts +265 -0
- package/src/cron/cron-routes.ts +85 -0
- package/src/cron/cron-scheduler.test.ts +421 -0
- package/src/cron/cron-scheduler.ts +413 -0
- package/src/cron/cron-store.ts +163 -0
- package/src/cron/index.ts +6 -0
- package/src/db/interfaces.ts +2 -2
- package/src/email/index.ts +3 -2
- package/src/email/smtp-email-service.ts +5 -2
- package/src/email/templates.ts +93 -6
- package/src/email/types.ts +17 -24
- package/src/functions/function-loader.ts +34 -6
- package/src/functions/function-routes.ts +2 -2
- package/src/history/history-routes.ts +4 -3
- package/src/index.ts +10 -0
- package/src/init.ts +475 -57
- package/src/services/driver-registry.ts +1 -1
- package/src/singleton.test.ts +28 -0
- package/src/singleton.ts +70 -0
- package/src/storage/LocalStorageController.ts +50 -53
- package/src/storage/S3StorageController.ts +49 -46
- package/src/storage/index.ts +27 -16
- package/src/storage/routes.ts +76 -59
- package/src/storage/storage-registry.ts +5 -5
- package/src/storage/types.ts +44 -32
- package/src/types/index.ts +4 -4
- package/src/utils/dev-port.ts +176 -0
- package/src/utils/logger.ts +143 -0
- package/src/utils/logging.ts +5 -2
- package/src/utils/request-logger.ts +66 -0
- package/test/admin-routes.test.ts +114 -65
- package/test/api-generator.test.ts +73 -30
- package/test/ast-schema-editor.test.ts +5 -3
- package/test/auth-middleware-hono.test.ts +268 -33
- package/test/auth-routes.test.ts +325 -146
- package/test/driver-registry.test.ts +4 -2
- package/test/error-propagation.test.ts +226 -0
- package/test/errors.test.ts +12 -7
- package/test/jwt-security.test.ts +18 -9
- package/test/jwt.test.ts +26 -13
- package/test/middleware.test.ts +13 -8
- package/test/query-parser.test.ts +13 -8
- package/test/rate-limiter.test.ts +7 -7
- package/test/safe-compare.test.ts +66 -0
- package/test/singleton.test.ts +59 -0
- package/test/storage-local.test.ts +36 -43
- package/test/storage-registry.test.ts +38 -36
- package/test/storage-routes.test.ts +9 -5
- package/test/storage-s3.test.ts +51 -48
- package/test.ts +6 -0
- package/tsconfig.json +1 -1
- package/vite.config.ts +7 -5
- package/dist/index-BeMqpmfQ.js +0 -239
- package/dist/index-BeMqpmfQ.js.map +0 -1
- package/dist/index-bl4J3lNb.js +0 -55823
- package/dist/index-bl4J3lNb.js.map +0 -1
- package/vite.config.ts.timestamp-1775065397568-8a853255edf6e.mjs +0 -46
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, jest } from "@jest/globals";
|
|
2
|
+
import { CronScheduler } from "./cron-scheduler";
|
|
3
|
+
import type { CronJobDefinition } from "@rebasepro/types";
|
|
4
|
+
import type { LoadedCronJob } from "./cron-loader";
|
|
5
|
+
|
|
6
|
+
// ─── Helpers ────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
function makeJob(
|
|
9
|
+
id: string,
|
|
10
|
+
overrides: Partial<CronJobDefinition> = {}
|
|
11
|
+
): LoadedCronJob {
|
|
12
|
+
return {
|
|
13
|
+
id,
|
|
14
|
+
definition: {
|
|
15
|
+
schedule: "0 * * * *", // every hour
|
|
16
|
+
name: `Job ${id}`,
|
|
17
|
+
description: `Description for ${id}`,
|
|
18
|
+
enabled: true,
|
|
19
|
+
timeoutSeconds: 5,
|
|
20
|
+
handler: async (ctx) => {
|
|
21
|
+
ctx.log("hello from", id);
|
|
22
|
+
return { ok: true };
|
|
23
|
+
},
|
|
24
|
+
...overrides
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function makeFailingJob(id: string, errorMsg = "boom"): LoadedCronJob {
|
|
30
|
+
return makeJob(id, {
|
|
31
|
+
handler: async () => {
|
|
32
|
+
throw new Error(errorMsg);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function makeSlowJob(id: string, delayMs: number): LoadedCronJob {
|
|
38
|
+
return makeJob(id, {
|
|
39
|
+
timeoutSeconds: 1, // 1s timeout
|
|
40
|
+
handler: () =>
|
|
41
|
+
new Promise((resolve) => setTimeout(() => resolve({ done: true }), delayMs))
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ─── Tests ──────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
describe("CronScheduler", () => {
|
|
48
|
+
let scheduler: CronScheduler;
|
|
49
|
+
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
scheduler = new CronScheduler();
|
|
52
|
+
jest.useFakeTimers();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
afterEach(() => {
|
|
56
|
+
scheduler.stop();
|
|
57
|
+
jest.useRealTimers();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// ── Registration ────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
describe("registerJobs", () => {
|
|
63
|
+
it("registers jobs and they appear in listJobs", () => {
|
|
64
|
+
scheduler.registerJobs([makeJob("alpha"), makeJob("beta")]);
|
|
65
|
+
|
|
66
|
+
const jobs = scheduler.listJobs();
|
|
67
|
+
expect(jobs).toHaveLength(2);
|
|
68
|
+
expect(jobs.map((j) => j.id).sort()).toEqual(["alpha", "beta"]);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("sets initial state to idle for enabled jobs", () => {
|
|
72
|
+
scheduler.registerJobs([makeJob("enabled-job")]);
|
|
73
|
+
const job = scheduler.getJob("enabled-job");
|
|
74
|
+
expect(job?.state).toBe("idle");
|
|
75
|
+
expect(job?.enabled).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("sets initial state to disabled for disabled jobs", () => {
|
|
79
|
+
scheduler.registerJobs([makeJob("disabled-job", { enabled: false })]);
|
|
80
|
+
const job = scheduler.getJob("disabled-job");
|
|
81
|
+
expect(job?.state).toBe("disabled");
|
|
82
|
+
expect(job?.enabled).toBe(false);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("initializes counters to zero", () => {
|
|
86
|
+
scheduler.registerJobs([makeJob("fresh")]);
|
|
87
|
+
const job = scheduler.getJob("fresh");
|
|
88
|
+
expect(job?.totalRuns).toBe(0);
|
|
89
|
+
expect(job?.totalFailures).toBe(0);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("overwrites duplicate job IDs", () => {
|
|
93
|
+
scheduler.registerJobs([makeJob("dup", { name: "First" })]);
|
|
94
|
+
scheduler.registerJobs([makeJob("dup", { name: "Second" })]);
|
|
95
|
+
|
|
96
|
+
const jobs = scheduler.listJobs();
|
|
97
|
+
expect(jobs).toHaveLength(1);
|
|
98
|
+
expect(jobs[0].name).toBe("Second");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("preserves definition metadata", () => {
|
|
102
|
+
scheduler.registerJobs([
|
|
103
|
+
makeJob("meta", {
|
|
104
|
+
name: "My Job",
|
|
105
|
+
description: "Does stuff",
|
|
106
|
+
schedule: "30 2 * * 1"
|
|
107
|
+
})
|
|
108
|
+
]);
|
|
109
|
+
|
|
110
|
+
const job = scheduler.getJob("meta");
|
|
111
|
+
expect(job?.name).toBe("My Job");
|
|
112
|
+
expect(job?.description).toBe("Does stuff");
|
|
113
|
+
expect(job?.schedule).toBe("30 2 * * 1");
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// ── getJob ───────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
describe("getJob", () => {
|
|
120
|
+
it("returns undefined for nonexistent ID", () => {
|
|
121
|
+
expect(scheduler.getJob("nope")).toBeUndefined();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("returns the correct job by ID", () => {
|
|
125
|
+
scheduler.registerJobs([makeJob("a"), makeJob("b")]);
|
|
126
|
+
const job = scheduler.getJob("b");
|
|
127
|
+
expect(job?.id).toBe("b");
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// ── triggerJob (manual execution) ───────────────────────────────
|
|
132
|
+
|
|
133
|
+
describe("triggerJob", () => {
|
|
134
|
+
beforeEach(() => {
|
|
135
|
+
jest.useRealTimers(); // triggerJob uses real async
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("returns undefined for nonexistent job", async () => {
|
|
139
|
+
const result = await scheduler.triggerJob("ghost");
|
|
140
|
+
expect(result).toBeUndefined();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("executes the handler and returns a log entry", async () => {
|
|
144
|
+
scheduler.registerJobs([makeJob("trigger-me")]);
|
|
145
|
+
const log = await scheduler.triggerJob("trigger-me");
|
|
146
|
+
|
|
147
|
+
expect(log).toBeDefined();
|
|
148
|
+
expect(log!.jobId).toBe("trigger-me");
|
|
149
|
+
expect(log!.success).toBe(true);
|
|
150
|
+
expect(log!.manual).toBe(true);
|
|
151
|
+
expect(log!.durationMs).toBeGreaterThanOrEqual(0);
|
|
152
|
+
expect(log!.logs).toContain("hello from trigger-me");
|
|
153
|
+
expect(log!.result).toEqual({ ok: true });
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("increments totalRuns after trigger", async () => {
|
|
157
|
+
scheduler.registerJobs([makeJob("count-me")]);
|
|
158
|
+
|
|
159
|
+
await scheduler.triggerJob("count-me");
|
|
160
|
+
expect(scheduler.getJob("count-me")?.totalRuns).toBe(1);
|
|
161
|
+
|
|
162
|
+
await scheduler.triggerJob("count-me");
|
|
163
|
+
expect(scheduler.getJob("count-me")?.totalRuns).toBe(2);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("records failure and increments totalFailures", async () => {
|
|
167
|
+
scheduler.registerJobs([makeFailingJob("fail-me", "something broke")]);
|
|
168
|
+
|
|
169
|
+
const log = await scheduler.triggerJob("fail-me");
|
|
170
|
+
|
|
171
|
+
expect(log!.success).toBe(false);
|
|
172
|
+
expect(log!.error).toBe("something broke");
|
|
173
|
+
|
|
174
|
+
const status = scheduler.getJob("fail-me");
|
|
175
|
+
expect(status?.totalFailures).toBe(1);
|
|
176
|
+
expect(status?.state).toBe("error");
|
|
177
|
+
expect(status?.lastError).toBe("something broke");
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("captures ctx.log output", async () => {
|
|
181
|
+
scheduler.registerJobs([
|
|
182
|
+
makeJob("logger", {
|
|
183
|
+
handler: async (ctx) => {
|
|
184
|
+
ctx.log("line 1");
|
|
185
|
+
ctx.log("line 2", { nested: true });
|
|
186
|
+
ctx.log(42);
|
|
187
|
+
}
|
|
188
|
+
})
|
|
189
|
+
]);
|
|
190
|
+
|
|
191
|
+
const log = await scheduler.triggerJob("logger");
|
|
192
|
+
expect(log!.logs).toEqual([
|
|
193
|
+
"line 1",
|
|
194
|
+
'line 2 {"nested":true}',
|
|
195
|
+
"42"
|
|
196
|
+
]);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("sets lastRunAt after execution", async () => {
|
|
200
|
+
scheduler.registerJobs([makeJob("timed")]);
|
|
201
|
+
|
|
202
|
+
const before = new Date();
|
|
203
|
+
await scheduler.triggerJob("timed");
|
|
204
|
+
const after = new Date();
|
|
205
|
+
|
|
206
|
+
const job = scheduler.getJob("timed");
|
|
207
|
+
expect(job?.lastRunAt).toBeDefined();
|
|
208
|
+
const lastRun = new Date(job!.lastRunAt!);
|
|
209
|
+
expect(lastRun.getTime()).toBeGreaterThanOrEqual(before.getTime());
|
|
210
|
+
expect(lastRun.getTime()).toBeLessThanOrEqual(after.getTime());
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("sets lastDurationMs after execution", async () => {
|
|
214
|
+
scheduler.registerJobs([makeJob("duration-check")]);
|
|
215
|
+
await scheduler.triggerJob("duration-check");
|
|
216
|
+
|
|
217
|
+
const job = scheduler.getJob("duration-check");
|
|
218
|
+
expect(job?.lastDurationMs).toBeDefined();
|
|
219
|
+
expect(job!.lastDurationMs!).toBeGreaterThanOrEqual(0);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("handles handler that returns undefined (void)", async () => {
|
|
223
|
+
scheduler.registerJobs([
|
|
224
|
+
makeJob("void-handler", {
|
|
225
|
+
handler: async () => {
|
|
226
|
+
// returns void
|
|
227
|
+
}
|
|
228
|
+
})
|
|
229
|
+
]);
|
|
230
|
+
|
|
231
|
+
const log = await scheduler.triggerJob("void-handler");
|
|
232
|
+
expect(log!.success).toBe(true);
|
|
233
|
+
expect(log!.result).toBeUndefined();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("handles synchronous handler", async () => {
|
|
237
|
+
scheduler.registerJobs([
|
|
238
|
+
makeJob("sync-handler", {
|
|
239
|
+
handler: (ctx) => {
|
|
240
|
+
ctx.log("sync");
|
|
241
|
+
return { sync: true };
|
|
242
|
+
}
|
|
243
|
+
})
|
|
244
|
+
]);
|
|
245
|
+
|
|
246
|
+
const log = await scheduler.triggerJob("sync-handler");
|
|
247
|
+
expect(log!.success).toBe(true);
|
|
248
|
+
expect(log!.result).toEqual({ sync: true });
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// ── Timeout ─────────────────────────────────────────────────────
|
|
253
|
+
|
|
254
|
+
describe("timeout", () => {
|
|
255
|
+
it("times out a slow handler", async () => {
|
|
256
|
+
jest.useRealTimers();
|
|
257
|
+
scheduler.registerJobs([
|
|
258
|
+
makeJob("slow", {
|
|
259
|
+
timeoutSeconds: 1,
|
|
260
|
+
handler: () =>
|
|
261
|
+
new Promise((resolve) =>
|
|
262
|
+
setTimeout(() => resolve("late"), 3000)
|
|
263
|
+
)
|
|
264
|
+
})
|
|
265
|
+
]);
|
|
266
|
+
|
|
267
|
+
const log = await scheduler.triggerJob("slow");
|
|
268
|
+
expect(log!.success).toBe(false);
|
|
269
|
+
expect(log!.error).toContain("timed out");
|
|
270
|
+
}, 10000);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// ── Logs ring buffer ────────────────────────────────────────────
|
|
274
|
+
|
|
275
|
+
describe("getJobLogs", () => {
|
|
276
|
+
beforeEach(() => {
|
|
277
|
+
jest.useRealTimers();
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("returns empty array for nonexistent job", () => {
|
|
281
|
+
expect(scheduler.getJobLogs("nope")).toEqual([]);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("returns logs in reverse order (newest first)", async () => {
|
|
285
|
+
scheduler.registerJobs([
|
|
286
|
+
makeJob("ordered", {
|
|
287
|
+
handler: async (ctx) => {
|
|
288
|
+
return { run: ctx.jobId };
|
|
289
|
+
}
|
|
290
|
+
})
|
|
291
|
+
]);
|
|
292
|
+
|
|
293
|
+
await scheduler.triggerJob("ordered");
|
|
294
|
+
await scheduler.triggerJob("ordered");
|
|
295
|
+
await scheduler.triggerJob("ordered");
|
|
296
|
+
|
|
297
|
+
const logs = scheduler.getJobLogs("ordered");
|
|
298
|
+
expect(logs).toHaveLength(3);
|
|
299
|
+
// newest first means the last entry's startedAt >= first entry's startedAt
|
|
300
|
+
const t0 = new Date(logs[0].startedAt).getTime();
|
|
301
|
+
const t2 = new Date(logs[2].startedAt).getTime();
|
|
302
|
+
expect(t0).toBeGreaterThanOrEqual(t2);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("respects limit parameter", async () => {
|
|
306
|
+
scheduler.registerJobs([makeJob("limited")]);
|
|
307
|
+
|
|
308
|
+
for (let i = 0; i < 5; i++) {
|
|
309
|
+
await scheduler.triggerJob("limited");
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
expect(scheduler.getJobLogs("limited", 2)).toHaveLength(2);
|
|
313
|
+
expect(scheduler.getJobLogs("limited", 10)).toHaveLength(5);
|
|
314
|
+
expect(scheduler.getJobLogs("limited")).toHaveLength(5);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it("caps at 50 entries (ring buffer)", async () => {
|
|
318
|
+
scheduler.registerJobs([makeJob("ring")]);
|
|
319
|
+
|
|
320
|
+
for (let i = 0; i < 60; i++) {
|
|
321
|
+
await scheduler.triggerJob("ring");
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
expect(scheduler.getJobLogs("ring")).toHaveLength(50);
|
|
325
|
+
expect(scheduler.getJob("ring")?.totalRuns).toBe(60);
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// ── Enable / Disable ────────────────────────────────────────────
|
|
330
|
+
|
|
331
|
+
describe("setJobEnabled", () => {
|
|
332
|
+
it("returns undefined for nonexistent job", () => {
|
|
333
|
+
expect(scheduler.setJobEnabled("nope", true)).toBeUndefined();
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it("disables a job", () => {
|
|
337
|
+
scheduler.registerJobs([makeJob("togglable")]);
|
|
338
|
+
const result = scheduler.setJobEnabled("togglable", false);
|
|
339
|
+
|
|
340
|
+
expect(result?.enabled).toBe(false);
|
|
341
|
+
expect(result?.state).toBe("disabled");
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it("re-enables a disabled job", () => {
|
|
345
|
+
scheduler.registerJobs([makeJob("togglable")]);
|
|
346
|
+
scheduler.setJobEnabled("togglable", false);
|
|
347
|
+
scheduler.start();
|
|
348
|
+
|
|
349
|
+
const result = scheduler.setJobEnabled("togglable", true);
|
|
350
|
+
expect(result?.enabled).toBe(true);
|
|
351
|
+
expect(result?.state).toBe("idle");
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// ── start / stop ────────────────────────────────────────────────
|
|
356
|
+
|
|
357
|
+
describe("start / stop", () => {
|
|
358
|
+
it("start is idempotent", () => {
|
|
359
|
+
scheduler.registerJobs([makeJob("idem")]);
|
|
360
|
+
scheduler.start();
|
|
361
|
+
scheduler.start(); // should not throw
|
|
362
|
+
expect(scheduler.listJobs()).toHaveLength(1);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it("stop clears nextRunAt", () => {
|
|
366
|
+
scheduler.registerJobs([makeJob("stoppable")]);
|
|
367
|
+
scheduler.start();
|
|
368
|
+
|
|
369
|
+
// After start, nextRunAt should be set
|
|
370
|
+
expect(scheduler.getJob("stoppable")?.nextRunAt).toBeDefined();
|
|
371
|
+
|
|
372
|
+
scheduler.stop();
|
|
373
|
+
expect(scheduler.getJob("stoppable")?.nextRunAt).toBeUndefined();
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it("does not schedule disabled jobs on start", () => {
|
|
377
|
+
scheduler.registerJobs([makeJob("off", { enabled: false })]);
|
|
378
|
+
scheduler.start();
|
|
379
|
+
|
|
380
|
+
expect(scheduler.getJob("off")?.nextRunAt).toBeUndefined();
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// ── toStatus shape ──────────────────────────────────────────────
|
|
385
|
+
|
|
386
|
+
describe("status shape", () => {
|
|
387
|
+
it("returns all expected fields", () => {
|
|
388
|
+
scheduler.registerJobs([
|
|
389
|
+
makeJob("shape", {
|
|
390
|
+
name: "Shape Test",
|
|
391
|
+
description: "Desc",
|
|
392
|
+
schedule: "15 3 * * *"
|
|
393
|
+
})
|
|
394
|
+
]);
|
|
395
|
+
|
|
396
|
+
const job = scheduler.getJob("shape")!;
|
|
397
|
+
expect(job).toMatchObject({
|
|
398
|
+
id: "shape",
|
|
399
|
+
name: "Shape Test",
|
|
400
|
+
description: "Desc",
|
|
401
|
+
schedule: "15 3 * * *",
|
|
402
|
+
enabled: true,
|
|
403
|
+
state: "idle",
|
|
404
|
+
totalRuns: 0,
|
|
405
|
+
totalFailures: 0
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it("lastRunAt and nextRunAt are ISO strings or undefined", () => {
|
|
410
|
+
scheduler.registerJobs([makeJob("iso-check")]);
|
|
411
|
+
const before = scheduler.getJob("iso-check")!;
|
|
412
|
+
expect(before.lastRunAt).toBeUndefined();
|
|
413
|
+
|
|
414
|
+
scheduler.start();
|
|
415
|
+
const after = scheduler.getJob("iso-check")!;
|
|
416
|
+
// nextRunAt should be set as an ISO string after start
|
|
417
|
+
expect(after.nextRunAt).toBeDefined();
|
|
418
|
+
expect(() => new Date(after.nextRunAt!)).not.toThrow();
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
});
|