@gencow/core 0.1.23 → 0.1.25

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 (77) hide show
  1. package/dist/crud.d.ts +2 -2
  2. package/dist/crud.js +225 -208
  3. package/dist/index.d.ts +7 -3
  4. package/dist/index.js +4 -1
  5. package/dist/reactive.js +10 -3
  6. package/dist/retry.js +1 -1
  7. package/dist/rls-db.d.ts +2 -2
  8. package/dist/rls-db.js +1 -5
  9. package/dist/scheduler.d.ts +2 -0
  10. package/dist/scheduler.js +16 -6
  11. package/dist/server.d.ts +0 -1
  12. package/dist/server.js +0 -1
  13. package/dist/storage.js +29 -22
  14. package/dist/v.d.ts +2 -2
  15. package/dist/workflow-types.d.ts +81 -0
  16. package/dist/workflow-types.js +12 -0
  17. package/dist/workflow.d.ts +30 -0
  18. package/dist/workflow.js +150 -0
  19. package/dist/workflows-api.d.ts +13 -0
  20. package/dist/workflows-api.js +321 -0
  21. package/package.json +46 -42
  22. package/src/__tests__/auth.test.ts +90 -86
  23. package/src/__tests__/crons.test.ts +69 -67
  24. package/src/__tests__/crud-codegen-integration.test.ts +164 -170
  25. package/src/__tests__/crud-owner-rls.test.ts +308 -301
  26. package/src/__tests__/crud.test.ts +694 -711
  27. package/src/__tests__/dist-exports.test.ts +120 -114
  28. package/src/__tests__/fixtures/basic/auth.ts +16 -16
  29. package/src/__tests__/fixtures/basic/drizzle.config.ts +1 -4
  30. package/src/__tests__/fixtures/basic/index.ts +1 -1
  31. package/src/__tests__/fixtures/basic/schema.ts +1 -1
  32. package/src/__tests__/fixtures/basic/tasks.ts +4 -4
  33. package/src/__tests__/fixtures/common/auth-schema.ts +38 -34
  34. package/src/__tests__/helpers/basic-rls-fixture.ts +80 -78
  35. package/src/__tests__/helpers/pglite-migrations.ts +2 -5
  36. package/src/__tests__/helpers/pglite-rls-session.ts +13 -16
  37. package/src/__tests__/helpers/seed-like-fill.ts +50 -44
  38. package/src/__tests__/helpers/test-gencow-ctx-rls.ts +4 -7
  39. package/src/__tests__/httpaction.test.ts +91 -91
  40. package/src/__tests__/image-optimization.test.ts +570 -574
  41. package/src/__tests__/load.test.ts +321 -308
  42. package/src/__tests__/network-sim.test.ts +238 -215
  43. package/src/__tests__/reactive.test.ts +380 -358
  44. package/src/__tests__/retry.test.ts +99 -84
  45. package/src/__tests__/rls-crud-basic.test.ts +172 -245
  46. package/src/__tests__/rls-crud-no-owner-rls-pglite.test.ts +81 -81
  47. package/src/__tests__/rls-custom-mutation-handlers.test.ts +47 -94
  48. package/src/__tests__/rls-custom-query-handlers.test.ts +92 -92
  49. package/src/__tests__/rls-db-leased-connection.test.ts +2 -6
  50. package/src/__tests__/rls-session-and-policies.test.ts +181 -199
  51. package/src/__tests__/scheduler-durable-v2.test.ts +199 -181
  52. package/src/__tests__/scheduler-durable.test.ts +117 -117
  53. package/src/__tests__/scheduler-exec.test.ts +258 -246
  54. package/src/__tests__/scheduler.test.ts +129 -111
  55. package/src/__tests__/storage.test.ts +282 -269
  56. package/src/__tests__/tsconfig.json +6 -6
  57. package/src/__tests__/validator.test.ts +236 -232
  58. package/src/__tests__/workflow.test.ts +606 -0
  59. package/src/__tests__/ws-integration.test.ts +223 -218
  60. package/src/__tests__/ws-scale.test.ts +168 -159
  61. package/src/auth-config.ts +18 -18
  62. package/src/auth.ts +106 -106
  63. package/src/crons.ts +77 -77
  64. package/src/crud.ts +523 -479
  65. package/src/index.ts +71 -6
  66. package/src/reactive.ts +357 -331
  67. package/src/retry.ts +51 -54
  68. package/src/rls-db.ts +195 -205
  69. package/src/rls.ts +33 -36
  70. package/src/scheduler.ts +237 -211
  71. package/src/server.ts +0 -1
  72. package/src/storage.ts +632 -593
  73. package/src/v.ts +119 -114
  74. package/src/workflow-types.ts +108 -0
  75. package/src/workflow.ts +188 -0
  76. package/src/workflows-api.ts +415 -0
  77. package/src/db.ts +0 -18
@@ -11,306 +11,318 @@ import { describe, it, expect, beforeEach } from "bun:test";
11
11
  import { createScheduler, getSchedulerInfo } from "../scheduler";
12
12
 
13
13
  describe("Scheduler 실행 — runAfter", () => {
14
- it("runAfter → 지정 시간 후 action 실행", async () => {
15
- const scheduler = createScheduler();
16
- let executed = false;
14
+ it("runAfter → 지정 시간 후 action 실행", async () => {
15
+ const scheduler = createScheduler();
16
+ let executed = false;
17
17
 
18
- scheduler.registerAction("test.delayed", async () => {
19
- executed = true;
20
- });
21
-
22
- scheduler.runAfter(100, "test.delayed");
23
-
24
- // 100ms 전에는 실행 안 됨
25
- expect(executed).toBe(false);
26
-
27
- // 200ms 후 실행 확인
28
- await new Promise((r) => setTimeout(r, 200));
29
- expect(executed).toBe(true);
18
+ scheduler.registerAction("test.delayed", async () => {
19
+ executed = true;
30
20
  });
31
21
 
32
- it("runAfter → args가 action에 전달된다", async () => {
33
- const scheduler = createScheduler();
34
- let receivedArgs: any = null;
35
-
36
- scheduler.registerAction("test.withArgs", async (args) => {
37
- receivedArgs = args;
38
- });
39
-
40
- scheduler.runAfter(50, "test.withArgs", { taskId: 42 });
41
- await new Promise((r) => setTimeout(r, 150));
22
+ scheduler.runAfter(100, "test.delayed");
42
23
 
43
- expect(receivedArgs).toEqual({ taskId: 42 });
44
- });
45
-
46
- it("runAfter → 반환된 jobId가 유효하다", () => {
47
- const scheduler = createScheduler();
48
- scheduler.registerAction("noop", async () => {});
49
- const id = scheduler.runAfter(10000, "noop");
50
- expect(typeof id).toBe("string");
51
- expect(id).toMatch(/^job_/);
52
-
53
- // cleanup
54
- scheduler.cancel(id);
55
- });
24
+ // 100ms 전에는 실행 안 됨
25
+ expect(executed).toBe(false);
56
26
 
57
- it("runAfter pendingJobs에 등록된다", () => {
58
- const scheduler = createScheduler();
59
- scheduler.registerAction("noop", async () => {});
60
- const id = scheduler.runAfter(10000, "noop");
27
+ // 200ms 실행 확인
28
+ await new Promise((r) => setTimeout(r, 200));
29
+ expect(executed).toBe(true);
30
+ });
61
31
 
62
- const info = getSchedulerInfo();
63
- const job = info.pendingJobs.find((j: any) => j.id === id);
64
- expect(job).toBeDefined();
65
- expect(job!.action).toBe("noop");
32
+ it("runAfter args가 action에 전달된다", async () => {
33
+ const scheduler = createScheduler();
34
+ let receivedArgs: any = null;
66
35
 
67
- // cleanup
68
- scheduler.cancel(id);
36
+ scheduler.registerAction("test.withArgs", async (args) => {
37
+ receivedArgs = args;
69
38
  });
70
39
 
71
- it("runAfter → 실행 후 pendingJobs에서 제거된다", async () => {
72
- const scheduler = createScheduler();
73
- let actionDone = false;
74
- scheduler.registerAction("fast-track", async () => {
75
- actionDone = true;
76
- });
77
- const id = scheduler.runAfter(50, "fast-track");
78
-
79
- // 실행 전: 등록 확인
80
- const infoBefore = getSchedulerInfo();
81
- expect(infoBefore.pendingJobs.some((j: any) => j.id === id)).toBe(true);
82
-
83
- // 실행 완료 대기 (setTimeout + await executeAction + splice)
84
- // setTimeout 50ms + executeAction async + splice → 최소 300ms 필요
85
- await new Promise((r) => setTimeout(r, 500));
86
- expect(actionDone).toBe(true);
87
-
88
- // 실행 후: 해당 ID가 제거됨 (splice는 executeAction 후 동기적으로 실행)
89
- const infoAfter = getSchedulerInfo();
90
- const stillPending = infoAfter.pendingJobs.some((j: any) => j.id === id);
91
-
92
- // 만약 아직 남아있다면 내부 구현의 비동기 splice 이슈이므로 경고만 출력
93
- if (stillPending) {
94
- console.warn("[test] pendingJobs splice is async skipping strict check");
95
- }
96
- // action이 실행된 것만 확인 (핵심 기능 검증)
97
- expect(actionDone).toBe(true);
40
+ scheduler.runAfter(50, "test.withArgs", { taskId: 42 });
41
+ await new Promise((r) => setTimeout(r, 150));
42
+
43
+ expect(receivedArgs).toEqual({ taskId: 42 });
44
+ });
45
+
46
+ it("runAfter 반환된 jobId가 유효하다", () => {
47
+ const scheduler = createScheduler();
48
+ scheduler.registerAction("noop", async () => {});
49
+ const id = scheduler.runAfter(10000, "noop");
50
+ expect(typeof id).toBe("string");
51
+ expect(id).toMatch(/^job_/);
52
+
53
+ // cleanup
54
+ scheduler.cancel(id);
55
+ });
56
+
57
+ it("runAfter pendingJobs에 등록된다", () => {
58
+ const scheduler = createScheduler();
59
+ scheduler.registerAction("noop", async () => {});
60
+ const id = scheduler.runAfter(10000, "noop");
61
+
62
+ const info = getSchedulerInfo();
63
+ const job = info.pendingJobs.find((j: any) => j.id === id);
64
+ expect(job).toBeDefined();
65
+ expect(job!.action).toBe("noop");
66
+
67
+ // cleanup
68
+ scheduler.cancel(id);
69
+ });
70
+
71
+ it("runAfter → 실행 후 pendingJobs에서 제거된다", async () => {
72
+ const scheduler = createScheduler();
73
+ let actionDone = false;
74
+ scheduler.registerAction("fast-track", async () => {
75
+ actionDone = true;
98
76
  });
77
+ const id = scheduler.runAfter(50, "fast-track");
78
+
79
+ // 실행 전: 등록 확인
80
+ const infoBefore = getSchedulerInfo();
81
+ expect(infoBefore.pendingJobs.some((j: any) => j.id === id)).toBe(true);
82
+
83
+ // 실행 완료 대기 (setTimeout + await executeAction + splice)
84
+ // setTimeout 50ms + executeAction async + splice → 최소 300ms 필요
85
+ await new Promise((r) => setTimeout(r, 500));
86
+ expect(actionDone).toBe(true);
87
+
88
+ // 실행 후: 해당 ID가 제거됨 (splice는 executeAction 후 동기적으로 실행)
89
+ const infoAfter = getSchedulerInfo();
90
+ const stillPending = infoAfter.pendingJobs.some((j: any) => j.id === id);
91
+
92
+ // 만약 아직 남아있다면 내부 구현의 비동기 splice 이슈이므로 경고만 출력
93
+ if (stillPending) {
94
+ console.warn("[test] pendingJobs splice is async — skipping strict check");
95
+ }
96
+ // action이 실행된 것만 확인 (핵심 기능 검증)
97
+ expect(actionDone).toBe(true);
98
+ });
99
99
  });
100
100
 
101
101
  describe("Scheduler 실행 — cancel", () => {
102
- it("cancel → runAfter를 취소할 수 있다", async () => {
103
- const scheduler = createScheduler();
104
- let executed = false;
102
+ it("cancel → runAfter를 취소할 수 있다", async () => {
103
+ const scheduler = createScheduler();
104
+ let executed = false;
105
105
 
106
- scheduler.registerAction("cancel.test", async () => {
107
- executed = true;
108
- });
109
-
110
- const id = scheduler.runAfter(100, "cancel.test");
111
- const cancelled = scheduler.cancel(id);
112
- expect(cancelled).toBe(true);
113
-
114
- await new Promise((r) => setTimeout(r, 200));
115
- expect(executed).toBe(false); // 취소되어 실행 안 됨
106
+ scheduler.registerAction("cancel.test", async () => {
107
+ executed = true;
116
108
  });
117
109
 
118
- it("cancel 이미 취소된 job → false 반환", () => {
119
- const scheduler = createScheduler();
120
- scheduler.registerAction("noop", async () => {});
121
- const id = scheduler.runAfter(10000, "noop");
122
- scheduler.cancel(id);
123
- expect(scheduler.cancel(id)).toBe(false); // 이미 취소됨
124
- });
125
-
126
- it("cancel → 존재하지 않는 ID → false 반환", () => {
127
- const scheduler = createScheduler();
128
- expect(scheduler.cancel("nonexistent_job")).toBe(false);
129
- });
110
+ const id = scheduler.runAfter(100, "cancel.test");
111
+ const cancelled = scheduler.cancel(id);
112
+ expect(cancelled).toBe(true);
113
+
114
+ await new Promise((r) => setTimeout(r, 200));
115
+ expect(executed).toBe(false); // 취소되어 실행 안 됨
116
+ });
117
+
118
+ it("cancel → 이미 취소된 job → false 반환", () => {
119
+ const scheduler = createScheduler();
120
+ scheduler.registerAction("noop", async () => {});
121
+ const id = scheduler.runAfter(10000, "noop");
122
+ scheduler.cancel(id);
123
+ expect(scheduler.cancel(id)).toBe(false); // 이미 취소됨
124
+ });
125
+
126
+ it("cancel → 존재하지 않는 ID → false 반환", () => {
127
+ const scheduler = createScheduler();
128
+ expect(scheduler.cancel("nonexistent_job")).toBe(false);
129
+ });
130
130
  });
131
131
 
132
132
  describe("Scheduler 실행 — registerAction + executeAction", () => {
133
- it("registerAction 후 executeAction으로 즉시 실행", async () => {
134
- const scheduler = createScheduler();
135
- let result = "";
133
+ it("registerAction 후 executeAction으로 즉시 실행", async () => {
134
+ const scheduler = createScheduler();
135
+ let result = "";
136
136
 
137
- scheduler.registerAction("greet", async (args) => {
138
- result = `Hello ${args.name}`;
139
- });
140
-
141
- await scheduler.executeAction("greet", { name: "World" });
142
- expect(result).toBe("Hello World");
137
+ scheduler.registerAction("greet", async (args) => {
138
+ result = `Hello ${args.name}`;
143
139
  });
144
140
 
145
- it("미등록 action executeAction → throw 안 함, console.error 출력", async () => {
146
- const scheduler = createScheduler();
147
- // 공개 API는 에러를 삼기고 console.error만 출력
148
- const errors: string[] = [];
149
- const origError = console.error;
150
- console.error = (...args: any[]) => { errors.push(args.map(String).join(" ")); };
141
+ await scheduler.executeAction("greet", { name: "World" });
142
+ expect(result).toBe("Hello World");
143
+ });
151
144
 
152
- await scheduler.executeAction("nonexistent"); // throw하지 않음
145
+ it("미등록 action executeAction → throw 안 함, console.error 출력", async () => {
146
+ const scheduler = createScheduler();
147
+ // 공개 API는 에러를 삼기고 console.error만 출력
148
+ const errors: string[] = [];
149
+ const origError = console.error;
150
+ console.error = (...args: any[]) => {
151
+ errors.push(args.map(String).join(" "));
152
+ };
153
153
 
154
- console.error = origError;
155
- expect(errors.some(e => e.includes("nonexistent"))).toBe(true);
156
- });
154
+ await scheduler.executeAction("nonexistent"); // throw하지 않음
155
+
156
+ console.error = origError;
157
+ expect(errors.some((e) => e.includes("nonexistent"))).toBe(true);
158
+ });
159
+
160
+ it("action 에러 시 다른 action에 영향 없음", async () => {
161
+ const scheduler = createScheduler();
162
+ let secondRan = false;
157
163
 
158
- it("action 에러 시 다른 action에 영향 없음", async () => {
159
- const scheduler = createScheduler();
160
- let secondRan = false;
161
-
162
- scheduler.registerAction("failing", async () => {
163
- throw new Error("Intentional failure");
164
- });
165
- scheduler.registerAction("healthy", async () => {
166
- secondRan = true;
167
- });
168
-
169
- // 실패하는 action — 공개 API는 throw하지 않음 (에러를 삼김)
170
- const origError = console.error;
171
- console.error = () => {}; // suppress expected error log
172
- await scheduler.executeAction("failing"); // throw하지 않음
173
- console.error = origError;
174
-
175
- // 정상 action은 여전히 동작
176
- await scheduler.executeAction("healthy");
177
- expect(secondRan).toBe(true);
164
+ scheduler.registerAction("failing", async () => {
165
+ throw new Error("Intentional failure");
178
166
  });
167
+ scheduler.registerAction("healthy", async () => {
168
+ secondRan = true;
169
+ });
170
+
171
+ // 실패하는 action — 공개 API는 throw하지 않음 (에러를 삼김)
172
+ const origError = console.error;
173
+ console.error = () => {}; // suppress expected error log
174
+ await scheduler.executeAction("failing"); // throw하지 않음
175
+ console.error = origError;
176
+
177
+ // 정상 action은 여전히 동작
178
+ await scheduler.executeAction("healthy");
179
+ expect(secondRan).toBe(true);
180
+ });
181
+
182
+ it("executeActionStrict는 미등록 action에서 throw한다", async () => {
183
+ const scheduler = createScheduler();
184
+ await expect(scheduler.executeActionStrict("nonexistent")).rejects.toThrow(
185
+ 'Action "nonexistent" not registered',
186
+ );
187
+ });
179
188
  });
180
189
 
181
190
  describe("Scheduler 실행 — runAt", () => {
182
- it("runAt(과거 시점) → 즉시 실행", async () => {
183
- const scheduler = createScheduler();
184
- let executed = false;
185
-
186
- scheduler.registerAction("past", async () => {
187
- executed = true;
188
- });
189
-
190
- // 이미 지난 시점
191
- scheduler.runAt(Date.now() - 1000, "past");
192
- await new Promise((r) => setTimeout(r, 100));
193
- expect(executed).toBe(true);
191
+ it("runAt(과거 시점) → 즉시 실행", async () => {
192
+ const scheduler = createScheduler();
193
+ let executed = false;
194
+
195
+ scheduler.registerAction("past", async () => {
196
+ executed = true;
194
197
  });
195
198
 
196
- it("runAt(Date 객체) 지원됨", async () => {
197
- const scheduler = createScheduler();
198
- let executed = false;
199
+ // 이미 지난 시점
200
+ scheduler.runAt(Date.now() - 1000, "past");
201
+ await new Promise((r) => setTimeout(r, 100));
202
+ expect(executed).toBe(true);
203
+ });
199
204
 
200
- scheduler.registerAction("dateObj", async () => {
201
- executed = true;
202
- });
205
+ it("runAt(Date 객체) → 지원됨", async () => {
206
+ const scheduler = createScheduler();
207
+ let executed = false;
203
208
 
204
- scheduler.runAt(new Date(Date.now() + 50), "dateObj");
205
- await new Promise((r) => setTimeout(r, 150));
206
- expect(executed).toBe(true);
209
+ scheduler.registerAction("dateObj", async () => {
210
+ executed = true;
207
211
  });
212
+
213
+ scheduler.runAt(new Date(Date.now() + 50), "dateObj");
214
+ await new Promise((r) => setTimeout(r, 150));
215
+ expect(executed).toBe(true);
216
+ });
208
217
  });
209
218
 
210
219
  describe("Scheduler 실행 — cron", () => {
211
- it("cron 등록 → cronInfo에 기록된다", () => {
212
- const scheduler = createScheduler();
213
- scheduler.cron("test-cron", "*/5 * * * *", async () => {});
214
-
215
- const info = getSchedulerInfo();
216
- const cronEntry = info.crons.find((c: any) => c.name === "test-cron");
217
- expect(cronEntry).toBeDefined();
218
- expect(cronEntry!.pattern).toBe("*/5 * * * *");
220
+ it("cron 등록 → cronInfo에 기록된다", () => {
221
+ const scheduler = createScheduler();
222
+ scheduler.cron("test-cron", "*/5 * * * *", async () => {});
223
+
224
+ const info = getSchedulerInfo();
225
+ const cronEntry = info.crons.find((c: any) => c.name === "test-cron");
226
+ expect(cronEntry).toBeDefined();
227
+ expect(cronEntry!.pattern).toBe("*/5 * * * *");
228
+ });
229
+
230
+ it("cron 등록 시 name과 pattern이 올바르게 저장된다", () => {
231
+ const scheduler = createScheduler();
232
+ scheduler.cron("hourly-cleanup", "0 * * * *", async () => {});
233
+
234
+ const info = getSchedulerInfo();
235
+ const entry = info.crons.find((c: any) => c.name === "hourly-cleanup");
236
+ expect(entry).toBeDefined();
237
+ expect(entry!.name).toBe("hourly-cleanup");
238
+ expect(entry!.registeredAt).toBeDefined();
239
+ });
240
+
241
+ it("동일 이름 cron 재등록 → 이전 것 중지 후 교체", () => {
242
+ const scheduler = createScheduler();
243
+ scheduler.cron("dup", "*/5 * * * *", async () => {});
244
+ scheduler.cron("dup", "*/10 * * * *", async () => {});
245
+
246
+ // cronInfo에 2개 다 기록되어 있지만,
247
+ // 내부적으로 첫 번째 task는 stop()됨
248
+ const info = getSchedulerInfo();
249
+ const dups = info.crons.filter((c: any) => c.name === "dup");
250
+ expect(dups.length).toBeGreaterThanOrEqual(1);
251
+ });
252
+
253
+ it("초 단위 cron — 1초 간격 실행 확인 (6자리 패턴)", async () => {
254
+ const scheduler = createScheduler();
255
+ let count = 0;
256
+
257
+ // 매 초 실행 (6자리 cron)
258
+ scheduler.cron("every-second", "* * * * * *", async () => {
259
+ count++;
219
260
  });
220
261
 
221
- it("cron 등록 name과 pattern이 올바르게 저장된다", () => {
222
- const scheduler = createScheduler();
223
- scheduler.cron("hourly-cleanup", "0 * * * *", async () => {});
224
-
225
- const info = getSchedulerInfo();
226
- const entry = info.crons.find((c: any) => c.name === "hourly-cleanup");
227
- expect(entry).toBeDefined();
228
- expect(entry!.name).toBe("hourly-cleanup");
229
- expect(entry!.registeredAt).toBeDefined();
230
- });
262
+ // 2.5초 대기 최소 2회 실행 기대
263
+ await new Promise((r) => setTimeout(r, 2500));
264
+ expect(count).toBeGreaterThanOrEqual(2);
265
+ });
266
+ });
231
267
 
232
- it("동일 이름 cron 재등록 → 이전 것 중지 후 교체", () => {
233
- const scheduler = createScheduler();
234
- scheduler.cron("dup", "*/5 * * * *", async () => {});
235
- scheduler.cron("dup", "*/10 * * * *", async () => {});
268
+ describe("Scheduler 실행 onError dead-letter", () => {
269
+ it("runAfter 실패 onError action이 호출된다", async () => {
270
+ const scheduler = createScheduler();
271
+ let errorHandlerArgs: any = null;
236
272
 
237
- // cronInfo에 2개 기록되어 있지만,
238
- // 내부적으로 번째 task는 stop()
239
- const info = getSchedulerInfo();
240
- const dups = info.crons.filter((c: any) => c.name === "dup");
241
- expect(dups.length).toBeGreaterThanOrEqual(1);
273
+ scheduler.registerAction("failing.step", async () => {
274
+ throw new Error("Step failed!");
242
275
  });
243
276
 
244
- it("초 단위 cron — 1초 간격 실행 확인 (6자리 패턴)", async () => {
245
- const scheduler = createScheduler();
246
- let count = 0;
247
-
248
- // 매 초 실행 (6자리 cron)
249
- scheduler.cron("every-second", "* * * * * *", async () => {
250
- count++;
251
- });
252
-
253
- // 2.5초 대기 → 최소 2회 실행 기대
254
- await new Promise((r) => setTimeout(r, 2500));
255
- expect(count).toBeGreaterThanOrEqual(2);
277
+ scheduler.registerAction("pipeline.onError", async (args) => {
278
+ errorHandlerArgs = args;
256
279
  });
257
- });
258
-
259
- describe("Scheduler 실행 — onError dead-letter", () => {
260
- it("runAfter 실패 시 onError action이 호출된다", async () => {
261
- const scheduler = createScheduler();
262
- let errorHandlerArgs: any = null;
263
280
 
264
- scheduler.registerAction("failing.step", async () => {
265
- throw new Error("Step failed!");
266
- });
281
+ scheduler.runAfter(
282
+ 50,
283
+ "failing.step",
284
+ { input: "test" },
285
+ {
286
+ onError: "pipeline.onError",
287
+ },
288
+ );
267
289
 
268
- scheduler.registerAction("pipeline.onError", async (args) => {
269
- errorHandlerArgs = args;
270
- });
290
+ await new Promise((r) => setTimeout(r, 300));
271
291
 
272
- scheduler.runAfter(50, "failing.step", { input: "test" }, {
273
- onError: "pipeline.onError",
274
- });
292
+ expect(errorHandlerArgs).not.toBeNull();
293
+ expect(errorHandlerArgs.failedAction).toBe("failing.step");
294
+ expect(errorHandlerArgs.error).toBe("Step failed!");
295
+ expect(errorHandlerArgs.originalArgs).toEqual({ input: "test" });
296
+ });
275
297
 
276
- await new Promise((r) => setTimeout(r, 300));
298
+ it("실패한 작업이 failedJobs에 기록된다", async () => {
299
+ const scheduler = createScheduler();
277
300
 
278
- expect(errorHandlerArgs).not.toBeNull();
279
- expect(errorHandlerArgs.failedAction).toBe("failing.step");
280
- expect(errorHandlerArgs.error).toBe("Step failed!");
281
- expect(errorHandlerArgs.originalArgs).toEqual({ input: "test" });
301
+ scheduler.registerAction("record.fail", async () => {
302
+ throw new Error("Recorded failure");
282
303
  });
283
304
 
284
- it("실패한 작업이 failedJobs에 기록된다", async () => {
285
- const scheduler = createScheduler();
305
+ scheduler.runAfter(50, "record.fail", { id: 1 });
286
306
 
287
- scheduler.registerAction("record.fail", async () => {
288
- throw new Error("Recorded failure");
289
- });
307
+ await new Promise((r) => setTimeout(r, 300));
290
308
 
291
- scheduler.runAfter(50, "record.fail", { id: 1 });
309
+ const info = getSchedulerInfo();
310
+ const failed = info.failedJobs.find((j: any) => j.action === "record.fail");
311
+ expect(failed).toBeDefined();
312
+ expect(failed!.error).toBe("Recorded failure");
313
+ });
292
314
 
293
- await new Promise((r) => setTimeout(r, 300));
315
+ it("pendingJobs에 status 필드가 포함된다", () => {
316
+ const scheduler = createScheduler();
317
+ scheduler.registerAction("noop", async () => {});
318
+ const id = scheduler.runAfter(10000, "noop");
294
319
 
295
- const info = getSchedulerInfo();
296
- const failed = info.failedJobs.find(
297
- (j: any) => j.action === "record.fail"
298
- );
299
- expect(failed).toBeDefined();
300
- expect(failed!.error).toBe("Recorded failure");
301
- });
302
-
303
- it("pendingJobs에 status 필드가 포함된다", () => {
304
- const scheduler = createScheduler();
305
- scheduler.registerAction("noop", async () => {});
306
- const id = scheduler.runAfter(10000, "noop");
320
+ const info = getSchedulerInfo();
321
+ const job = info.pendingJobs.find((j: any) => j.id === id);
322
+ expect(job).toBeDefined();
323
+ expect(job!.status).toBe("pending");
307
324
 
308
- const info = getSchedulerInfo();
309
- const job = info.pendingJobs.find((j: any) => j.id === id);
310
- expect(job).toBeDefined();
311
- expect(job!.status).toBe("pending");
312
-
313
- // cleanup
314
- scheduler.cancel(id);
315
- });
325
+ // cleanup
326
+ scheduler.cancel(id);
327
+ });
316
328
  });