@quave/agenda 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (216) hide show
  1. package/LICENSE.md +25 -0
  2. package/README.md +380 -0
  3. package/dist/Job.d.ts +190 -0
  4. package/dist/Job.js +689 -0
  5. package/dist/Job.js.map +1 -0
  6. package/dist/JobProcessingQueue.d.ts +47 -0
  7. package/dist/JobProcessingQueue.js +107 -0
  8. package/dist/JobProcessingQueue.js.map +1 -0
  9. package/dist/JobProcessor.d.ts +72 -0
  10. package/dist/JobProcessor.js +512 -0
  11. package/dist/JobProcessor.js.map +1 -0
  12. package/dist/backends/index.d.ts +18 -0
  13. package/dist/backends/index.js +20 -0
  14. package/dist/backends/index.js.map +1 -0
  15. package/dist/cjs/Job.d.ts +190 -0
  16. package/dist/cjs/Job.js +696 -0
  17. package/dist/cjs/Job.js.map +1 -0
  18. package/dist/cjs/JobProcessingQueue.d.ts +47 -0
  19. package/dist/cjs/JobProcessingQueue.js +111 -0
  20. package/dist/cjs/JobProcessingQueue.js.map +1 -0
  21. package/dist/cjs/JobProcessor.d.ts +72 -0
  22. package/dist/cjs/JobProcessor.js +519 -0
  23. package/dist/cjs/JobProcessor.js.map +1 -0
  24. package/dist/cjs/backends/index.d.ts +17 -0
  25. package/dist/cjs/backends/index.js +20 -0
  26. package/dist/cjs/backends/index.js.map +1 -0
  27. package/dist/cjs/decorators/Define.d.ts +33 -0
  28. package/dist/cjs/decorators/Define.js +47 -0
  29. package/dist/cjs/decorators/Define.js.map +1 -0
  30. package/dist/cjs/decorators/Every.d.ts +43 -0
  31. package/dist/cjs/decorators/Every.js +58 -0
  32. package/dist/cjs/decorators/Every.js.map +1 -0
  33. package/dist/cjs/decorators/JobsController.d.ts +43 -0
  34. package/dist/cjs/decorators/JobsController.js +40 -0
  35. package/dist/cjs/decorators/JobsController.js.map +1 -0
  36. package/dist/cjs/decorators/index.d.ts +41 -0
  37. package/dist/cjs/decorators/index.js +57 -0
  38. package/dist/cjs/decorators/index.js.map +1 -0
  39. package/dist/cjs/decorators/metadata.d.ts +50 -0
  40. package/dist/cjs/decorators/metadata.js +41 -0
  41. package/dist/cjs/decorators/metadata.js.map +1 -0
  42. package/dist/cjs/decorators/register.d.ts +52 -0
  43. package/dist/cjs/decorators/register.js +145 -0
  44. package/dist/cjs/decorators/register.js.map +1 -0
  45. package/dist/cjs/index.d.ts +498 -0
  46. package/dist/cjs/index.js +776 -0
  47. package/dist/cjs/index.js.map +1 -0
  48. package/dist/cjs/notifications/BaseNotificationChannel.d.ts +47 -0
  49. package/dist/cjs/notifications/BaseNotificationChannel.js +112 -0
  50. package/dist/cjs/notifications/BaseNotificationChannel.js.map +1 -0
  51. package/dist/cjs/notifications/InMemoryNotificationChannel.d.ts +13 -0
  52. package/dist/cjs/notifications/InMemoryNotificationChannel.js +37 -0
  53. package/dist/cjs/notifications/InMemoryNotificationChannel.js.map +1 -0
  54. package/dist/cjs/notifications/index.d.ts +2 -0
  55. package/dist/cjs/notifications/index.js +8 -0
  56. package/dist/cjs/notifications/index.js.map +1 -0
  57. package/dist/cjs/package.json +1 -0
  58. package/dist/cjs/types/AgendaBackend.d.ts +61 -0
  59. package/dist/cjs/types/AgendaBackend.js +3 -0
  60. package/dist/cjs/types/AgendaBackend.js.map +1 -0
  61. package/dist/cjs/types/AgendaConfig.d.ts +10 -0
  62. package/dist/cjs/types/AgendaConfig.js +3 -0
  63. package/dist/cjs/types/AgendaConfig.js.map +1 -0
  64. package/dist/cjs/types/AgendaStatus.d.ts +32 -0
  65. package/dist/cjs/types/AgendaStatus.js +3 -0
  66. package/dist/cjs/types/AgendaStatus.js.map +1 -0
  67. package/dist/cjs/types/DbOptions.d.ts +4 -0
  68. package/dist/cjs/types/DbOptions.js +3 -0
  69. package/dist/cjs/types/DbOptions.js.map +1 -0
  70. package/dist/cjs/types/DrainOptions.d.ts +37 -0
  71. package/dist/cjs/types/DrainOptions.js +3 -0
  72. package/dist/cjs/types/DrainOptions.js.map +1 -0
  73. package/dist/cjs/types/JobDefinition.d.ts +49 -0
  74. package/dist/cjs/types/JobDefinition.js +3 -0
  75. package/dist/cjs/types/JobDefinition.js.map +1 -0
  76. package/dist/cjs/types/JobLogger.d.ts +111 -0
  77. package/dist/cjs/types/JobLogger.js +3 -0
  78. package/dist/cjs/types/JobLogger.js.map +1 -0
  79. package/dist/cjs/types/JobParameters.d.ts +106 -0
  80. package/dist/cjs/types/JobParameters.js +21 -0
  81. package/dist/cjs/types/JobParameters.js.map +1 -0
  82. package/dist/cjs/types/JobQuery.d.ts +79 -0
  83. package/dist/cjs/types/JobQuery.js +36 -0
  84. package/dist/cjs/types/JobQuery.js.map +1 -0
  85. package/dist/cjs/types/JobRepository.d.ts +101 -0
  86. package/dist/cjs/types/JobRepository.js +3 -0
  87. package/dist/cjs/types/JobRepository.js.map +1 -0
  88. package/dist/cjs/types/NotificationChannel.d.ts +120 -0
  89. package/dist/cjs/types/NotificationChannel.js +3 -0
  90. package/dist/cjs/types/NotificationChannel.js.map +1 -0
  91. package/dist/cjs/utils/backoff.d.ts +152 -0
  92. package/dist/cjs/utils/backoff.js +175 -0
  93. package/dist/cjs/utils/backoff.js.map +1 -0
  94. package/dist/cjs/utils/dateConstraints.d.ts +48 -0
  95. package/dist/cjs/utils/dateConstraints.js +127 -0
  96. package/dist/cjs/utils/dateConstraints.js.map +1 -0
  97. package/dist/cjs/utils/isValidDate.d.ts +1 -0
  98. package/dist/cjs/utils/isValidDate.js +8 -0
  99. package/dist/cjs/utils/isValidDate.js.map +1 -0
  100. package/dist/cjs/utils/nextRunAt.d.ts +10 -0
  101. package/dist/cjs/utils/nextRunAt.js +119 -0
  102. package/dist/cjs/utils/nextRunAt.js.map +1 -0
  103. package/dist/cjs/utils/priority.d.ts +13 -0
  104. package/dist/cjs/utils/priority.js +23 -0
  105. package/dist/cjs/utils/priority.js.map +1 -0
  106. package/dist/cjs/utils/processEvery.d.ts +1 -0
  107. package/dist/cjs/utils/processEvery.js +13 -0
  108. package/dist/cjs/utils/processEvery.js.map +1 -0
  109. package/dist/cjs/utils/stack.d.ts +1 -0
  110. package/dist/cjs/utils/stack.js +21 -0
  111. package/dist/cjs/utils/stack.js.map +1 -0
  112. package/dist/cjs/version.d.ts +9 -0
  113. package/dist/cjs/version.js +13 -0
  114. package/dist/cjs/version.js.map +1 -0
  115. package/dist/decorators/Define.d.ts +33 -0
  116. package/dist/decorators/Define.js +44 -0
  117. package/dist/decorators/Define.js.map +1 -0
  118. package/dist/decorators/Every.d.ts +43 -0
  119. package/dist/decorators/Every.js +55 -0
  120. package/dist/decorators/Every.js.map +1 -0
  121. package/dist/decorators/JobsController.d.ts +43 -0
  122. package/dist/decorators/JobsController.js +37 -0
  123. package/dist/decorators/JobsController.js.map +1 -0
  124. package/dist/decorators/index.d.ts +41 -0
  125. package/dist/decorators/index.js +46 -0
  126. package/dist/decorators/index.js.map +1 -0
  127. package/dist/decorators/metadata.d.ts +50 -0
  128. package/dist/decorators/metadata.js +35 -0
  129. package/dist/decorators/metadata.js.map +1 -0
  130. package/dist/decorators/register.d.ts +52 -0
  131. package/dist/decorators/register.js +139 -0
  132. package/dist/decorators/register.js.map +1 -0
  133. package/dist/index.d.ts +498 -0
  134. package/dist/index.js +744 -0
  135. package/dist/index.js.map +1 -0
  136. package/dist/notifications/BaseNotificationChannel.d.ts +47 -0
  137. package/dist/notifications/BaseNotificationChannel.js +108 -0
  138. package/dist/notifications/BaseNotificationChannel.js.map +1 -0
  139. package/dist/notifications/InMemoryNotificationChannel.d.ts +13 -0
  140. package/dist/notifications/InMemoryNotificationChannel.js +33 -0
  141. package/dist/notifications/InMemoryNotificationChannel.js.map +1 -0
  142. package/dist/notifications/index.d.ts +2 -0
  143. package/dist/notifications/index.js +3 -0
  144. package/dist/notifications/index.js.map +1 -0
  145. package/dist/types/AgendaBackend.d.ts +61 -0
  146. package/dist/types/AgendaBackend.js +2 -0
  147. package/dist/types/AgendaBackend.js.map +1 -0
  148. package/dist/types/AgendaConfig.d.ts +10 -0
  149. package/dist/types/AgendaConfig.js +2 -0
  150. package/dist/types/AgendaConfig.js.map +1 -0
  151. package/dist/types/AgendaStatus.d.ts +32 -0
  152. package/dist/types/AgendaStatus.js +2 -0
  153. package/dist/types/AgendaStatus.js.map +1 -0
  154. package/dist/types/DbOptions.d.ts +4 -0
  155. package/dist/types/DbOptions.js +2 -0
  156. package/dist/types/DbOptions.js.map +1 -0
  157. package/dist/types/DrainOptions.d.ts +37 -0
  158. package/dist/types/DrainOptions.js +2 -0
  159. package/dist/types/DrainOptions.js.map +1 -0
  160. package/dist/types/JobDefinition.d.ts +49 -0
  161. package/dist/types/JobDefinition.js +2 -0
  162. package/dist/types/JobDefinition.js.map +1 -0
  163. package/dist/types/JobLogger.d.ts +111 -0
  164. package/dist/types/JobLogger.js +2 -0
  165. package/dist/types/JobLogger.js.map +1 -0
  166. package/dist/types/JobParameters.d.ts +106 -0
  167. package/dist/types/JobParameters.js +17 -0
  168. package/dist/types/JobParameters.js.map +1 -0
  169. package/dist/types/JobQuery.d.ts +79 -0
  170. package/dist/types/JobQuery.js +33 -0
  171. package/dist/types/JobQuery.js.map +1 -0
  172. package/dist/types/JobRepository.d.ts +101 -0
  173. package/dist/types/JobRepository.js +2 -0
  174. package/dist/types/JobRepository.js.map +1 -0
  175. package/dist/types/NotificationChannel.d.ts +120 -0
  176. package/dist/types/NotificationChannel.js +2 -0
  177. package/dist/types/NotificationChannel.js.map +1 -0
  178. package/dist/utils/backoff.d.ts +152 -0
  179. package/dist/utils/backoff.js +167 -0
  180. package/dist/utils/backoff.js.map +1 -0
  181. package/dist/utils/dateConstraints.d.ts +48 -0
  182. package/dist/utils/dateConstraints.js +117 -0
  183. package/dist/utils/dateConstraints.js.map +1 -0
  184. package/dist/utils/isValidDate.d.ts +1 -0
  185. package/dist/utils/isValidDate.js +5 -0
  186. package/dist/utils/isValidDate.js.map +1 -0
  187. package/dist/utils/nextRunAt.d.ts +10 -0
  188. package/dist/utils/nextRunAt.js +110 -0
  189. package/dist/utils/nextRunAt.js.map +1 -0
  190. package/dist/utils/priority.d.ts +13 -0
  191. package/dist/utils/priority.js +20 -0
  192. package/dist/utils/priority.js.map +1 -0
  193. package/dist/utils/processEvery.d.ts +1 -0
  194. package/dist/utils/processEvery.js +7 -0
  195. package/dist/utils/processEvery.js.map +1 -0
  196. package/dist/utils/stack.d.ts +1 -0
  197. package/dist/utils/stack.js +18 -0
  198. package/dist/utils/stack.js.map +1 -0
  199. package/dist/version.d.ts +9 -0
  200. package/dist/version.js +10 -0
  201. package/dist/version.js.map +1 -0
  202. package/package.json +79 -0
  203. package/test/fixtures/forkHelper.js +40 -0
  204. package/test/fixtures/someJobDefinition.ts +12 -0
  205. package/test/shared/agenda-test-suite.ts +1876 -0
  206. package/test/shared/backoff-test-suite.ts +408 -0
  207. package/test/shared/debounce-test-suite.ts +377 -0
  208. package/test/shared/full-test-suite.ts +161 -0
  209. package/test/shared/index.ts +50 -0
  210. package/test/shared/joblogger-test-suite.ts +305 -0
  211. package/test/shared/jobprocessor-test-suite.ts +266 -0
  212. package/test/shared/notification-channel-test-suite.ts +471 -0
  213. package/test/shared/removeoncomplete-test-suite.ts +228 -0
  214. package/test/shared/repository-test-suite.ts +708 -0
  215. package/test/shared/retry-test-suite.ts +174 -0
  216. package/test/shared/test-utils.ts +100 -0
@@ -0,0 +1,305 @@
1
+ /**
2
+ * Shared test suite for JobLogger implementations.
3
+ *
4
+ * Tests the JobLogger interface contract: log(), getLogs(), clearLogs()
5
+ * with filtering, pagination, and sorting. Each backend package runs this
6
+ * suite with its own logger implementation (MongoJobLogger, PostgresJobLogger,
7
+ * RedisJobLogger).
8
+ *
9
+ * Usage:
10
+ * ```typescript
11
+ * import { jobLoggerTestSuite } from 'agenda/testing';
12
+ *
13
+ * jobLoggerTestSuite({
14
+ * name: 'MongoJobLogger',
15
+ * createLogger: async () => new MongoJobLogger({ db }),
16
+ * cleanupLogger: async () => { await db.collection('agenda_logs').drop(); }
17
+ * });
18
+ * ```
19
+ */
20
+
21
+ import { describe, it, expect, beforeEach } from 'vitest';
22
+ import type { JobLogger, JobLogEntry } from '../../src/index.js';
23
+
24
+ export interface JobLoggerTestConfig {
25
+ /** Name for the test suite (e.g., 'MongoJobLogger') */
26
+ name: string;
27
+ /** Factory to create a fresh logger instance (already connected/initialized) */
28
+ createLogger: () => Promise<JobLogger>;
29
+ /** Cleanup between tests — clear all logs */
30
+ cleanupLogger: (logger: JobLogger) => Promise<void>;
31
+ }
32
+
33
+ function makeEntry(overrides: Partial<Omit<JobLogEntry, '_id'>> = {}): Omit<JobLogEntry, '_id'> {
34
+ return {
35
+ timestamp: new Date(),
36
+ level: 'info',
37
+ event: 'start',
38
+ jobName: 'test-job',
39
+ message: 'Job started',
40
+ ...overrides
41
+ };
42
+ }
43
+
44
+ export function jobLoggerTestSuite(config: JobLoggerTestConfig): void {
45
+ describe(`JobLogger: ${config.name}`, () => {
46
+ let logger: JobLogger;
47
+
48
+ beforeEach(async () => {
49
+ logger = await config.createLogger();
50
+ await config.cleanupLogger(logger);
51
+ });
52
+
53
+ describe('log()', () => {
54
+ it('should store a log entry', async () => {
55
+ await logger.log(makeEntry());
56
+
57
+ const result = await logger.getLogs();
58
+ expect(result.total).toBe(1);
59
+ expect(result.entries).toHaveLength(1);
60
+ expect(result.entries[0].level).toBe('info');
61
+ expect(result.entries[0].event).toBe('start');
62
+ expect(result.entries[0].jobName).toBe('test-job');
63
+ expect(result.entries[0].message).toBe('Job started');
64
+ });
65
+
66
+ it('should store all optional fields', async () => {
67
+ await logger.log(makeEntry({
68
+ jobId: 'job-123',
69
+ duration: 1500,
70
+ error: 'Something went wrong',
71
+ failCount: 3,
72
+ retryDelay: 5000,
73
+ retryAttempt: 2,
74
+ agendaName: 'worker-1',
75
+ meta: { customKey: 'customValue' }
76
+ }));
77
+
78
+ const result = await logger.getLogs();
79
+ expect(result.total).toBe(1);
80
+ const entry = result.entries[0];
81
+ expect(entry.jobId).toBe('job-123');
82
+ expect(entry.duration).toBe(1500);
83
+ expect(entry.error).toBe('Something went wrong');
84
+ expect(entry.failCount).toBe(3);
85
+ expect(entry.retryDelay).toBe(5000);
86
+ expect(entry.retryAttempt).toBe(2);
87
+ expect(entry.agendaName).toBe('worker-1');
88
+ expect(entry.meta).toEqual({ customKey: 'customValue' });
89
+ });
90
+
91
+ it('should store multiple entries', async () => {
92
+ await logger.log(makeEntry({ event: 'start', message: 'Started' }));
93
+ await logger.log(makeEntry({ event: 'success', message: 'Succeeded' }));
94
+ await logger.log(makeEntry({ event: 'complete', message: 'Completed' }));
95
+
96
+ const result = await logger.getLogs();
97
+ expect(result.total).toBe(3);
98
+ expect(result.entries).toHaveLength(3);
99
+ });
100
+ });
101
+
102
+ describe('getLogs()', () => {
103
+ beforeEach(async () => {
104
+ // Seed with a variety of entries
105
+ const baseTime = new Date('2025-06-01T12:00:00Z');
106
+ await logger.log(makeEntry({
107
+ timestamp: new Date(baseTime.getTime()),
108
+ event: 'start',
109
+ level: 'info',
110
+ jobName: 'job-a',
111
+ jobId: 'id-1',
112
+ message: 'Job A started'
113
+ }));
114
+ await logger.log(makeEntry({
115
+ timestamp: new Date(baseTime.getTime() + 1000),
116
+ event: 'success',
117
+ level: 'info',
118
+ jobName: 'job-a',
119
+ jobId: 'id-1',
120
+ message: 'Job A succeeded',
121
+ duration: 1000
122
+ }));
123
+ await logger.log(makeEntry({
124
+ timestamp: new Date(baseTime.getTime() + 2000),
125
+ event: 'start',
126
+ level: 'info',
127
+ jobName: 'job-b',
128
+ jobId: 'id-2',
129
+ message: 'Job B started'
130
+ }));
131
+ await logger.log(makeEntry({
132
+ timestamp: new Date(baseTime.getTime() + 3000),
133
+ event: 'fail',
134
+ level: 'error',
135
+ jobName: 'job-b',
136
+ jobId: 'id-2',
137
+ message: 'Job B failed',
138
+ error: 'timeout'
139
+ }));
140
+ await logger.log(makeEntry({
141
+ timestamp: new Date(baseTime.getTime() + 4000),
142
+ event: 'retry',
143
+ level: 'warn',
144
+ jobName: 'job-b',
145
+ jobId: 'id-2',
146
+ message: 'Job B retrying',
147
+ retryAttempt: 1,
148
+ retryDelay: 5000
149
+ }));
150
+ });
151
+
152
+ it('should return all entries when no filter is provided', async () => {
153
+ const result = await logger.getLogs();
154
+ expect(result.total).toBe(5);
155
+ expect(result.entries).toHaveLength(5);
156
+ });
157
+
158
+ it('should filter by jobName', async () => {
159
+ const result = await logger.getLogs({ jobName: 'job-a' });
160
+ expect(result.total).toBe(2);
161
+ for (const entry of result.entries) {
162
+ expect(entry.jobName).toBe('job-a');
163
+ }
164
+ });
165
+
166
+ it('should filter by jobId', async () => {
167
+ const result = await logger.getLogs({ jobId: 'id-2' });
168
+ expect(result.total).toBe(3);
169
+ for (const entry of result.entries) {
170
+ expect(entry.jobId).toBe('id-2');
171
+ }
172
+ });
173
+
174
+ it('should filter by single level', async () => {
175
+ const result = await logger.getLogs({ level: 'error' });
176
+ expect(result.total).toBe(1);
177
+ expect(result.entries[0].event).toBe('fail');
178
+ });
179
+
180
+ it('should filter by level array', async () => {
181
+ const result = await logger.getLogs({ level: ['error', 'warn'] });
182
+ expect(result.total).toBe(2);
183
+ for (const entry of result.entries) {
184
+ expect(['error', 'warn']).toContain(entry.level);
185
+ }
186
+ });
187
+
188
+ it('should filter by single event', async () => {
189
+ const result = await logger.getLogs({ event: 'start' });
190
+ expect(result.total).toBe(2);
191
+ for (const entry of result.entries) {
192
+ expect(entry.event).toBe('start');
193
+ }
194
+ });
195
+
196
+ it('should filter by event array', async () => {
197
+ const result = await logger.getLogs({ event: ['fail', 'retry'] });
198
+ expect(result.total).toBe(2);
199
+ for (const entry of result.entries) {
200
+ expect(['fail', 'retry']).toContain(entry.event);
201
+ }
202
+ });
203
+
204
+ it('should filter by date range (from)', async () => {
205
+ const from = new Date('2025-06-01T12:00:03Z');
206
+ const result = await logger.getLogs({ from });
207
+ expect(result.total).toBe(2); // fail + retry
208
+ for (const entry of result.entries) {
209
+ expect(entry.timestamp.getTime()).toBeGreaterThanOrEqual(from.getTime());
210
+ }
211
+ });
212
+
213
+ it('should filter by date range (to)', async () => {
214
+ const to = new Date('2025-06-01T12:00:01Z');
215
+ const result = await logger.getLogs({ to });
216
+ expect(result.total).toBe(2); // start + success
217
+ for (const entry of result.entries) {
218
+ expect(entry.timestamp.getTime()).toBeLessThanOrEqual(to.getTime());
219
+ }
220
+ });
221
+
222
+ it('should filter by date range (from + to)', async () => {
223
+ const from = new Date('2025-06-01T12:00:01Z');
224
+ const to = new Date('2025-06-01T12:00:03Z');
225
+ const result = await logger.getLogs({ from, to });
226
+ expect(result.total).toBe(3); // success, start, fail
227
+ for (const entry of result.entries) {
228
+ expect(entry.timestamp.getTime()).toBeGreaterThanOrEqual(from.getTime());
229
+ expect(entry.timestamp.getTime()).toBeLessThanOrEqual(to.getTime());
230
+ }
231
+ });
232
+
233
+ it('should support limit', async () => {
234
+ const result = await logger.getLogs({ limit: 2 });
235
+ expect(result.entries).toHaveLength(2);
236
+ expect(result.total).toBe(5); // total should reflect all matching, not limited
237
+ });
238
+
239
+ it('should support offset for pagination', async () => {
240
+ const page1 = await logger.getLogs({ limit: 2, offset: 0, sort: 'asc' });
241
+ const page2 = await logger.getLogs({ limit: 2, offset: 2, sort: 'asc' });
242
+
243
+ expect(page1.entries).toHaveLength(2);
244
+ expect(page2.entries).toHaveLength(2);
245
+ // Different entries
246
+ expect(page1.entries[0].message).not.toBe(page2.entries[0].message);
247
+ });
248
+
249
+ it('should sort descending by default', async () => {
250
+ const result = await logger.getLogs();
251
+ for (let i = 1; i < result.entries.length; i++) {
252
+ expect(result.entries[i - 1].timestamp.getTime())
253
+ .toBeGreaterThanOrEqual(result.entries[i].timestamp.getTime());
254
+ }
255
+ });
256
+
257
+ it('should sort ascending when requested', async () => {
258
+ const result = await logger.getLogs({ sort: 'asc' });
259
+ for (let i = 1; i < result.entries.length; i++) {
260
+ expect(result.entries[i - 1].timestamp.getTime())
261
+ .toBeLessThanOrEqual(result.entries[i].timestamp.getTime());
262
+ }
263
+ });
264
+
265
+ it('should return empty result when no entries match', async () => {
266
+ const result = await logger.getLogs({ jobName: 'nonexistent' });
267
+ expect(result.total).toBe(0);
268
+ expect(result.entries).toHaveLength(0);
269
+ });
270
+ });
271
+
272
+ describe('clearLogs()', () => {
273
+ beforeEach(async () => {
274
+ await logger.log(makeEntry({ jobName: 'job-a', event: 'start', level: 'info' }));
275
+ await logger.log(makeEntry({ jobName: 'job-a', event: 'success', level: 'info' }));
276
+ await logger.log(makeEntry({ jobName: 'job-b', event: 'fail', level: 'error' }));
277
+ });
278
+
279
+ it('should clear all logs when no filter is provided', async () => {
280
+ const deleted = await logger.clearLogs();
281
+ expect(deleted).toBe(3);
282
+
283
+ const result = await logger.getLogs();
284
+ expect(result.total).toBe(0);
285
+ });
286
+
287
+ it('should clear logs filtered by jobName', async () => {
288
+ const deleted = await logger.clearLogs({ jobName: 'job-a' });
289
+ expect(deleted).toBe(2);
290
+
291
+ const result = await logger.getLogs();
292
+ expect(result.total).toBe(1);
293
+ expect(result.entries[0].jobName).toBe('job-b');
294
+ });
295
+
296
+ it('should return 0 when no logs match the filter', async () => {
297
+ const deleted = await logger.clearLogs({ jobName: 'nonexistent' });
298
+ expect(deleted).toBe(0);
299
+
300
+ const result = await logger.getLogs();
301
+ expect(result.total).toBe(3);
302
+ });
303
+ });
304
+ });
305
+ }
@@ -0,0 +1,266 @@
1
+ /**
2
+ * JobProcessor test factory
3
+ *
4
+ * This file exports a test factory for JobProcessor-specific tests.
5
+ *
6
+ * Usage:
7
+ * ```typescript
8
+ * import { jobProcessorTestSuite } from 'agenda/test/jobprocessor.test';
9
+ *
10
+ * jobProcessorTestSuite({
11
+ * name: 'JobProcessor with MongoDB',
12
+ * createBackend: async () => new MongoBackend({ mongo: db }),
13
+ * cleanupBackend: async (backend) => await backend.disconnect(),
14
+ * clearJobs: async (backend) => await db.collection('agendaJobs').deleteMany({})
15
+ * });
16
+ * ```
17
+ */
18
+
19
+ import { describe, it, expect, beforeEach, afterEach, beforeAll, afterAll } from 'vitest';
20
+ import type { AgendaBackend } from '../../src/index.js';
21
+ import { Agenda } from '../../src/index.js';
22
+ import { delay } from './test-utils.js';
23
+
24
+ export interface JobProcessorTestConfig {
25
+ /** Name for the test suite */
26
+ name: string;
27
+ /** Factory to create a fresh backend instance */
28
+ createBackend: () => Promise<AgendaBackend>;
29
+ /** Cleanup function called after tests */
30
+ cleanupBackend: (backend: AgendaBackend) => Promise<void>;
31
+ /** Clear all jobs between tests */
32
+ clearJobs: (backend: AgendaBackend) => Promise<void>;
33
+ }
34
+
35
+ /**
36
+ * JobProcessor tests
37
+ */
38
+ export function jobProcessorTestSuite(config: JobProcessorTestConfig): void {
39
+ describe(`${config.name} - JobProcessor`, () => {
40
+ let backend: AgendaBackend;
41
+ let agenda: Agenda;
42
+
43
+ beforeAll(async () => {
44
+ backend = await config.createBackend();
45
+ });
46
+
47
+ afterAll(async () => {
48
+ await config.cleanupBackend(backend);
49
+ });
50
+
51
+ beforeEach(async () => {
52
+ await config.clearJobs(backend);
53
+ agenda = new Agenda({
54
+ backend,
55
+ maxConcurrency: 4,
56
+ defaultConcurrency: 1,
57
+ lockLimit: 15,
58
+ defaultLockLimit: 6,
59
+ processEvery: 100,
60
+ name: 'agendaTest'
61
+ });
62
+ await agenda.ready;
63
+ });
64
+
65
+ afterEach(async () => {
66
+ if (agenda) {
67
+ await agenda.stop();
68
+ }
69
+ await config.clearJobs(backend);
70
+ });
71
+
72
+ describe('getRunningStats', () => {
73
+ it('throws an error when agenda is not running', async () => {
74
+ await expect(agenda.getRunningStats()).rejects.toThrow('agenda not running!');
75
+ });
76
+
77
+ it('contains the agendaVersion', async () => {
78
+ await agenda.start();
79
+
80
+ const status = await agenda.getRunningStats();
81
+ expect(status).toHaveProperty('version');
82
+ expect(status.version).toMatch(/\d+\.\d+\.\d+/);
83
+ });
84
+
85
+ it('shows the correct job status', async () => {
86
+ agenda.define('test', async () => {
87
+ await delay(30000);
88
+ });
89
+
90
+ agenda.now('test');
91
+ await agenda.start();
92
+
93
+ await new Promise(resolve => {
94
+ agenda.on('start:test', resolve);
95
+ });
96
+
97
+ const status = await agenda.getRunningStats();
98
+ expect(status).toHaveProperty('jobStatus');
99
+ if (status.jobStatus) {
100
+ expect(status.jobStatus).toHaveProperty('test');
101
+ expect(status.jobStatus.test.locked).toBe(1);
102
+ expect(status.jobStatus.test.running).toBe(1);
103
+ expect(status.jobStatus.test.config.fn).toBeTypeOf('function');
104
+ expect(status.jobStatus.test.config.concurrency).toBe(1);
105
+ expect(status.jobStatus.test.config.lockLifetime).toBe(600000);
106
+ expect(status.jobStatus.test.config.priority).toBe(0);
107
+ expect(status.jobStatus.test.config.lockLimit).toBe(6);
108
+ }
109
+ });
110
+
111
+ it('shows queueName', async () => {
112
+ await agenda.start();
113
+
114
+ const status = await agenda.getRunningStats();
115
+ expect(status).toHaveProperty('queueName');
116
+ expect(status.queueName).toBeTypeOf('string');
117
+ expect(status.queueName).toBe('agendaTest');
118
+ });
119
+
120
+ it('shows totalQueueSizeDB', async () => {
121
+ await agenda.start();
122
+
123
+ const status = await agenda.getRunningStats();
124
+ expect(status).toHaveProperty('totalQueueSizeDB');
125
+ expect(status.totalQueueSizeDB).toBeTypeOf('number');
126
+ expect(status.totalQueueSizeDB).toBe(0);
127
+ });
128
+ });
129
+
130
+ it('ensure new jobs are always filling up running queue', async () => {
131
+ let shortOneFinished = false;
132
+
133
+ agenda.define('test long', async () => {
134
+ await delay(1000);
135
+ });
136
+ agenda.define('test short', async () => {
137
+ shortOneFinished = true;
138
+ await delay(5);
139
+ });
140
+
141
+ await agenda.start();
142
+
143
+ // queue up long ones
144
+ for (let i = 0; i < 100; i += 1) {
145
+ agenda.now('test long');
146
+ }
147
+
148
+ await delay(1500);
149
+
150
+ // queue more short ones (they will be picked up on next process interval)
151
+ for (let j = 0; j < 100; j += 1) {
152
+ agenda.now('test short');
153
+ }
154
+
155
+ // Wait for the next process interval to pick up and run the short jobs
156
+ await delay(2000);
157
+
158
+ expect(shortOneFinished).toBe(true);
159
+ });
160
+
161
+ it('ensure slow jobs time out', async () => {
162
+ let jobStarted = false;
163
+ agenda.define(
164
+ 'test long',
165
+ async () => {
166
+ jobStarted = true;
167
+ await delay(2500);
168
+ },
169
+ { lockLifetime: 500 }
170
+ );
171
+
172
+ agenda.now('test long');
173
+
174
+ await agenda.start();
175
+
176
+ const promiseResult = await new Promise<Error | void>(resolve => {
177
+ agenda.on('error', err => {
178
+ resolve(err);
179
+ });
180
+
181
+ agenda.on('success', () => {
182
+ resolve();
183
+ });
184
+ });
185
+
186
+ expect(jobStarted).toBe(true);
187
+ expect(promiseResult).toBeInstanceOf(Error);
188
+ });
189
+
190
+ it('ensure slow jobs do not time out when calling touch', async () => {
191
+ agenda.define(
192
+ 'test long',
193
+ async job => {
194
+ for (let i = 0; i < 10; i += 1) {
195
+ await delay(100);
196
+ await job.touch();
197
+ }
198
+ },
199
+ { lockLifetime: 500 }
200
+ );
201
+
202
+ await agenda.start();
203
+
204
+ agenda.now('test long');
205
+
206
+ const promiseResult = await new Promise<Error | void>(resolve => {
207
+ agenda.on('error', err => {
208
+ resolve(err);
209
+ });
210
+
211
+ agenda.on('success', () => {
212
+ resolve();
213
+ });
214
+ });
215
+
216
+ expect(promiseResult).toBeUndefined();
217
+ });
218
+
219
+ it('ensure concurrency is filled up', async () => {
220
+ agenda.maxConcurrency(300);
221
+ agenda.lockLimit(150);
222
+ agenda.defaultLockLimit(20);
223
+ agenda.defaultConcurrency(10);
224
+
225
+ for (let jobI = 0; jobI < 10; jobI += 1) {
226
+ agenda.define(
227
+ `test job ${jobI}`,
228
+ async () => {
229
+ await delay(5000);
230
+ },
231
+ { lockLifetime: 10000 }
232
+ );
233
+ }
234
+
235
+ for (let jobI = 0; jobI < 10; jobI += 1) {
236
+ for (let jobJ = 0; jobJ < 25; jobJ += 1) {
237
+ agenda.now(`test job ${jobI}`);
238
+ }
239
+ }
240
+
241
+ await agenda.start();
242
+
243
+ let runningJobs = 0;
244
+ const allJobsStarted = (async () => {
245
+ do {
246
+ runningJobs = (await agenda.getRunningStats()).runningJobs as number;
247
+ await delay(50);
248
+ } while (runningJobs < 90);
249
+ return 'all started';
250
+ })();
251
+
252
+ const result = await Promise.race([
253
+ allJobsStarted,
254
+ new Promise(resolve => {
255
+ setTimeout(
256
+ () => resolve(`not all jobs started, currently running: ${runningJobs}`),
257
+ 2500
258
+ );
259
+ })
260
+ ]);
261
+
262
+ expect(result).toBe('all started');
263
+ });
264
+ });
265
+ }
266
+