@gencow/core 0.1.27 → 0.1.29

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 (130) hide show
  1. package/dist/auth-config.d.ts +92 -5
  2. package/dist/config.d.ts +107 -0
  3. package/dist/config.js +12 -0
  4. package/dist/context.d.ts +139 -0
  5. package/dist/context.js +3 -0
  6. package/dist/crud.d.ts +5 -5
  7. package/dist/crud.js +19 -35
  8. package/dist/document-types.d.ts +65 -0
  9. package/dist/document-types.js +15 -0
  10. package/dist/grounded-answer-types.d.ts +62 -0
  11. package/dist/grounded-answer-types.js +6 -0
  12. package/dist/http-action.d.ts +77 -0
  13. package/dist/http-action.js +41 -0
  14. package/dist/index.d.ts +30 -5
  15. package/dist/index.js +15 -2
  16. package/dist/platform-capacity-profile.d.ts +19 -0
  17. package/dist/platform-capacity-profile.js +94 -0
  18. package/dist/procedure.d.ts +58 -0
  19. package/dist/procedure.js +115 -0
  20. package/dist/rag-ingest-types.d.ts +39 -0
  21. package/dist/rag-ingest-types.js +1 -0
  22. package/dist/rag-operations-types.d.ts +81 -0
  23. package/dist/rag-operations-types.js +1 -0
  24. package/dist/rag-schema.d.ts +1466 -0
  25. package/dist/rag-schema.js +87 -0
  26. package/dist/reactive-mutation-types.d.ts +11 -0
  27. package/dist/reactive-mutation-types.js +1 -0
  28. package/dist/reactive-mutation.d.ts +51 -0
  29. package/dist/reactive-mutation.js +75 -0
  30. package/dist/reactive-query-types.d.ts +12 -0
  31. package/dist/reactive-query-types.js +1 -0
  32. package/dist/reactive-query.d.ts +14 -0
  33. package/dist/reactive-query.js +28 -0
  34. package/dist/reactive-realtime.d.ts +48 -0
  35. package/dist/reactive-realtime.js +236 -0
  36. package/dist/reactive.d.ts +29 -5
  37. package/dist/reactive.js +65 -0
  38. package/dist/rls-db.d.ts +9 -2
  39. package/dist/runtime-env-policy.d.ts +5 -0
  40. package/dist/runtime-env-policy.js +56 -0
  41. package/dist/search-types.d.ts +83 -0
  42. package/dist/search-types.js +1 -0
  43. package/dist/server.d.ts +1 -2
  44. package/dist/server.js +0 -1
  45. package/dist/storage-metering.d.ts +13 -0
  46. package/dist/storage-metering.js +18 -0
  47. package/dist/storage-shared.d.ts +36 -0
  48. package/dist/storage-shared.js +39 -0
  49. package/dist/storage.d.ts +5 -27
  50. package/dist/storage.js +30 -22
  51. package/dist/wake-app-result.d.ts +22 -0
  52. package/dist/wake-app-result.js +11 -0
  53. package/dist/workflow-types.d.ts +16 -2
  54. package/dist/workflow.d.ts +1 -1
  55. package/dist/workflow.js +136 -11
  56. package/dist/workflows-api.js +71 -3
  57. package/package.json +11 -7
  58. package/src/auth-config.ts +104 -3
  59. package/src/config.ts +119 -0
  60. package/src/context.ts +152 -0
  61. package/src/crud.ts +18 -35
  62. package/src/document-types.ts +102 -0
  63. package/src/grounded-answer-types.ts +78 -0
  64. package/src/http-action.ts +101 -0
  65. package/src/index.ts +142 -19
  66. package/src/platform-capacity-profile.ts +114 -0
  67. package/src/procedure.ts +283 -0
  68. package/src/rag-ingest-types.ts +52 -0
  69. package/src/rag-operations-types.ts +90 -0
  70. package/src/rag-schema.ts +94 -0
  71. package/src/reactive-mutation-types.ts +13 -0
  72. package/src/reactive-mutation.ts +115 -0
  73. package/src/reactive-query-types.ts +14 -0
  74. package/src/reactive-query.ts +48 -0
  75. package/src/reactive-realtime.ts +267 -0
  76. package/src/rls-db.ts +9 -4
  77. package/src/runtime-env-policy.ts +66 -0
  78. package/src/search-types.ts +91 -0
  79. package/src/server.ts +6 -2
  80. package/src/storage-metering.ts +35 -0
  81. package/src/storage-shared.ts +74 -0
  82. package/src/storage.ts +44 -53
  83. package/src/wake-app-result.ts +37 -0
  84. package/src/workflow-types.ts +16 -2
  85. package/src/workflow.ts +166 -12
  86. package/src/workflows-api.ts +82 -3
  87. package/src/__tests__/auth.test.ts +0 -118
  88. package/src/__tests__/crons.test.ts +0 -83
  89. package/src/__tests__/crud-codegen-integration.test.ts +0 -246
  90. package/src/__tests__/crud-owner-rls.test.ts +0 -387
  91. package/src/__tests__/crud.test.ts +0 -930
  92. package/src/__tests__/dist-exports.test.ts +0 -176
  93. package/src/__tests__/fixtures/basic/auth.ts +0 -32
  94. package/src/__tests__/fixtures/basic/drizzle.config.ts +0 -12
  95. package/src/__tests__/fixtures/basic/index.ts +0 -6
  96. package/src/__tests__/fixtures/basic/migrations/0000_last_warstar.sql +0 -75
  97. package/src/__tests__/fixtures/basic/migrations/meta/0000_snapshot.json +0 -497
  98. package/src/__tests__/fixtures/basic/migrations/meta/_journal.json +0 -13
  99. package/src/__tests__/fixtures/basic/schema.ts +0 -51
  100. package/src/__tests__/fixtures/basic/tasks.ts +0 -15
  101. package/src/__tests__/fixtures/common/auth-schema.ts +0 -67
  102. package/src/__tests__/helpers/basic-rls-fixture.ts +0 -135
  103. package/src/__tests__/helpers/pglite-migrations.ts +0 -32
  104. package/src/__tests__/helpers/pglite-rls-session.ts +0 -51
  105. package/src/__tests__/helpers/seed-like-fill.ts +0 -202
  106. package/src/__tests__/helpers/test-gencow-ctx-rls.ts +0 -50
  107. package/src/__tests__/httpaction.test.ts +0 -122
  108. package/src/__tests__/image-optimization.test.ts +0 -648
  109. package/src/__tests__/load.test.ts +0 -389
  110. package/src/__tests__/network-sim.test.ts +0 -319
  111. package/src/__tests__/reactive.test.ts +0 -479
  112. package/src/__tests__/retry.test.ts +0 -113
  113. package/src/__tests__/rls-crud-basic.test.ts +0 -317
  114. package/src/__tests__/rls-crud-no-owner-rls-pglite.test.ts +0 -117
  115. package/src/__tests__/rls-custom-mutation-handlers.test.ts +0 -142
  116. package/src/__tests__/rls-custom-query-handlers.test.ts +0 -128
  117. package/src/__tests__/rls-db-leased-connection.test.ts +0 -118
  118. package/src/__tests__/rls-session-and-policies.test.ts +0 -228
  119. package/src/__tests__/scheduler-durable-v2.test.ts +0 -288
  120. package/src/__tests__/scheduler-durable.test.ts +0 -173
  121. package/src/__tests__/scheduler-exec.test.ts +0 -328
  122. package/src/__tests__/scheduler.test.ts +0 -187
  123. package/src/__tests__/storage.test.ts +0 -334
  124. package/src/__tests__/tsconfig.json +0 -8
  125. package/src/__tests__/validator.test.ts +0 -323
  126. package/src/__tests__/workflow.test.ts +0 -606
  127. package/src/__tests__/ws-integration.test.ts +0 -309
  128. package/src/__tests__/ws-scale.test.ts +0 -241
  129. package/src/auth.ts +0 -155
  130. package/src/reactive.ts +0 -580
@@ -1,288 +0,0 @@
1
- /**
2
- * packages/core/src/__tests__/scheduler-durable-v2.test.ts
3
- *
4
- * Scheduler Durable v2 테스트 — Platform DB 중앙화 구현 검증.
5
- *
6
- * DB/HTTP 의존 없이 검증 가능한 핵심 로직:
7
- * - persistJob에 올바른 필드가 전달되는지
8
- * - args가 persistJob 콜백까지 정확히 전달되는지
9
- * - 다수의 job 동시 등록 시 id 충돌 없음
10
- * - removeJob 실패 시 graceful 처리
11
- * - runAfter(0) 즉시 실행 케이스
12
- * - 대형 args 직렬화 안정성
13
- *
14
- * Run: bun test packages/core/src/__tests__/scheduler-durable-v2.test.ts
15
- */
16
-
17
- import { describe, it, expect } from "bun:test";
18
- import { createScheduler } from "../scheduler.js";
19
- import type { ScheduledJobRecord } from "../scheduler.js";
20
-
21
- describe("Scheduler Durable v2 — Platform DB 중앙화", () => {
22
- // ── args 전달 정확성 ──
23
-
24
- it("persistJob에 args가 정확히 전달된다 (중첩 객체)", async () => {
25
- const persisted: ScheduledJobRecord[] = [];
26
-
27
- const scheduler = createScheduler({
28
- persistJob: async (job) => {
29
- persisted.push(job);
30
- },
31
- removeJob: async () => true,
32
- });
33
-
34
- scheduler.registerAction("deep.args", async () => {});
35
-
36
- const complexArgs = {
37
- nested: { deeply: { value: 42 } },
38
- array: [1, 2, { x: "y" }],
39
- unicode: "한글테스트 🎯",
40
- nullValue: null,
41
- boolValue: false,
42
- zero: 0,
43
- emptyString: "",
44
- };
45
-
46
- scheduler.runAfter(1000, "deep.args", complexArgs);
47
- await new Promise((r) => setTimeout(r, 50));
48
-
49
- expect(persisted.length).toBe(1);
50
- expect(persisted[0].args).toEqual(complexArgs);
51
- });
52
-
53
- it("persistJob에 args 미전달 시 undefined로 전달", async () => {
54
- const persisted: ScheduledJobRecord[] = [];
55
-
56
- const scheduler = createScheduler({
57
- persistJob: async (job) => {
58
- persisted.push(job);
59
- },
60
- removeJob: async () => true,
61
- });
62
-
63
- scheduler.registerAction("no.args", async () => {});
64
- scheduler.runAfter(1000, "no.args"); // args 생략
65
-
66
- await new Promise((r) => setTimeout(r, 50));
67
-
68
- expect(persisted.length).toBe(1);
69
- // args는 undefined 또는 빈 값이어야 함 (Platform에서 {} default 처리)
70
- expect(
71
- persisted[0].args === undefined ||
72
- persisted[0].args === null ||
73
- JSON.stringify(persisted[0].args) === "{}",
74
- ).toBe(true);
75
- });
76
-
77
- // ── 동시 등록 id 고유성 ──
78
-
79
- it("10개 job 동시 등록 시 모든 id가 고유하다", async () => {
80
- const persisted: ScheduledJobRecord[] = [];
81
-
82
- const scheduler = createScheduler({
83
- persistJob: async (job) => {
84
- persisted.push(job);
85
- },
86
- removeJob: async () => true,
87
- });
88
-
89
- for (let i = 0; i < 10; i++) {
90
- scheduler.registerAction(`batch.${i}`, async () => {});
91
- }
92
-
93
- const ids: string[] = [];
94
- for (let i = 0; i < 10; i++) {
95
- ids.push(scheduler.runAfter(1000, `batch.${i}`, { idx: i }));
96
- }
97
-
98
- await new Promise((r) => setTimeout(r, 100));
99
-
100
- // 모든 id 고유
101
- const uniqueIds = new Set(ids);
102
- expect(uniqueIds.size).toBe(10);
103
-
104
- // 모든 persistJob 호출됨
105
- expect(persisted.length).toBe(10);
106
- });
107
-
108
- // ── runAfter(0) — 즉시 실행 ──
109
-
110
- it("runAfter(0) — durable mode에서도 즉시 persistJob 호출", async () => {
111
- const persisted: ScheduledJobRecord[] = [];
112
-
113
- const scheduler = createScheduler({
114
- persistJob: async (job) => {
115
- persisted.push(job);
116
- },
117
- removeJob: async () => true,
118
- });
119
-
120
- scheduler.registerAction("instant", async () => {});
121
-
122
- const beforeTime = Date.now();
123
- scheduler.runAfter(0, "instant", { immediate: true });
124
-
125
- await new Promise((r) => setTimeout(r, 50));
126
-
127
- expect(persisted.length).toBe(1);
128
- expect(persisted[0].args).toEqual({ immediate: true });
129
- // runAt은 현재 시간과 거의 동일해야 함 (±2초 오차 허용)
130
- expect(Math.abs(persisted[0].runAt.getTime() - beforeTime)).toBeLessThan(2000);
131
- });
132
-
133
- // ── removeJob 실패 시 graceful 처리 ──
134
-
135
- it("removeJob 실패 시 cancel()이 false 반환 (throw 안 함)", async () => {
136
- const scheduler = createScheduler({
137
- persistJob: async () => {},
138
- removeJob: async () => {
139
- throw new Error("DB connection lost");
140
- },
141
- });
142
-
143
- scheduler.registerAction("fail.cancel", async () => {});
144
- const id = scheduler.runAfter(10000, "fail.cancel");
145
-
146
- await new Promise((r) => setTimeout(r, 50));
147
-
148
- // suppress expected error
149
- const orig = console.error;
150
- console.error = () => {};
151
- const result = scheduler.cancel(id);
152
- console.error = orig;
153
-
154
- // removeJob 실패 시 return false, throw 안 함
155
- expect(typeof result).toBe("boolean");
156
- });
157
-
158
- // ── 대형 args 안정성 ──
159
-
160
- it("대형 args (10KB) 직렬화 안정성", async () => {
161
- const persisted: ScheduledJobRecord[] = [];
162
-
163
- const scheduler = createScheduler({
164
- persistJob: async (job) => {
165
- persisted.push(job);
166
- },
167
- removeJob: async () => true,
168
- });
169
-
170
- scheduler.registerAction("large.payload", async () => {});
171
-
172
- // ~10KB args
173
- const largeArgs = {
174
- data: "x".repeat(10_000),
175
- items: Array.from({ length: 100 }, (_, i) => ({ id: i, name: `item-${i}` })),
176
- };
177
-
178
- scheduler.runAfter(1000, "large.payload", largeArgs);
179
- await new Promise((r) => setTimeout(r, 50));
180
-
181
- expect(persisted.length).toBe(1);
182
- expect((persisted[0].args as any).data.length).toBe(10_000);
183
- expect((persisted[0].args as any).items.length).toBe(100);
184
- });
185
-
186
- // ── onError + args 조합 ──
187
-
188
- it("onError와 args가 함께 persistJob에 전달된다", async () => {
189
- const persisted: ScheduledJobRecord[] = [];
190
-
191
- const scheduler = createScheduler({
192
- persistJob: async (job) => {
193
- persisted.push(job);
194
- },
195
- removeJob: async () => true,
196
- });
197
-
198
- scheduler.registerAction("pipeline.step1", async () => {});
199
- scheduler.registerAction("pipeline.onError", async () => {});
200
-
201
- scheduler.runAfter(5000, "pipeline.step1", { step: 1, data: "test" }, { onError: "pipeline.onError" });
202
-
203
- await new Promise((r) => setTimeout(r, 50));
204
-
205
- expect(persisted.length).toBe(1);
206
- expect(persisted[0].action).toBe("pipeline.step1");
207
- expect(persisted[0].args).toEqual({ step: 1, data: "test" });
208
- expect(persisted[0].onErrorAction).toBe("pipeline.onError");
209
- });
210
-
211
- // ── runAt + durable 조합 ──
212
-
213
- it("runAt(과거 시점) — durable mode에서도 persistJob 호출 (즉시 실행 대상)", async () => {
214
- const persisted: ScheduledJobRecord[] = [];
215
-
216
- const scheduler = createScheduler({
217
- persistJob: async (job) => {
218
- persisted.push(job);
219
- },
220
- removeJob: async () => true,
221
- });
222
-
223
- scheduler.registerAction("past.task", async () => {});
224
-
225
- const pastTime = new Date(Date.now() - 60_000); // 1분 전
226
- scheduler.runAt(pastTime, "past.task", { expired: true });
227
-
228
- await new Promise((r) => setTimeout(r, 50));
229
-
230
- expect(persisted.length).toBe(1);
231
- expect(persisted[0].args).toEqual({ expired: true });
232
- // runAt이 과거여도 persistJob은 호출되어야 함 (폴러가 즉시 픽업)
233
- });
234
-
235
- // ── 다중 cancel ──
236
-
237
- it("같은 job을 2번 cancel — 첫 번째만 removeJob 성공", async () => {
238
- let removeCount = 0;
239
-
240
- const scheduler = createScheduler({
241
- persistJob: async () => {},
242
- removeJob: async () => {
243
- removeCount++;
244
- return removeCount === 1; // 첫 번째만 true
245
- },
246
- });
247
-
248
- scheduler.registerAction("double.cancel", async () => {});
249
- const id = scheduler.runAfter(10000, "double.cancel");
250
-
251
- await new Promise((r) => setTimeout(r, 50));
252
-
253
- const r1 = scheduler.cancel(id);
254
- await new Promise((r) => setTimeout(r, 20));
255
- const r2 = scheduler.cancel(id);
256
-
257
- // 첫 번째 cancel은 성공 (pendingJobs에서 제거)
258
- expect(r1).toBe(true);
259
- // 두 번째 cancel은 이미 제거되었으므로 false
260
- expect(r2).toBe(false);
261
- });
262
-
263
- // ── persistJob 순서 보장 ──
264
-
265
- it("연속 runAfter 호출 시 persistJob 순서 보장", async () => {
266
- const order: string[] = [];
267
-
268
- const scheduler = createScheduler({
269
- persistJob: async (job) => {
270
- order.push(job.action);
271
- },
272
- removeJob: async () => true,
273
- });
274
-
275
- scheduler.registerAction("a", async () => {});
276
- scheduler.registerAction("b", async () => {});
277
- scheduler.registerAction("c", async () => {});
278
-
279
- scheduler.runAfter(1000, "a");
280
- scheduler.runAfter(2000, "b");
281
- scheduler.runAfter(3000, "c");
282
-
283
- await new Promise((r) => setTimeout(r, 100));
284
-
285
- // persistJob은 호출 순서대로 실행되어야 함
286
- expect(order).toEqual(["a", "b", "c"]);
287
- });
288
- });
@@ -1,173 +0,0 @@
1
- /**
2
- * packages/core/src/__tests__/scheduler-durable.test.ts
3
- *
4
- * Scheduler Durable Mode 테스트 — createScheduler({ persistJob, removeJob })
5
- * DB 콜백이 제공되면 runAfter()가 setTimeout 대신 persistJob을 호출하고,
6
- * cancel()이 removeJob을 호출하는지 검증.
7
- *
8
- * Run: bun test packages/core/src/__tests__/scheduler-durable.test.ts
9
- */
10
-
11
- import { describe, it, expect } from "bun:test";
12
- import { createScheduler } from "../scheduler.js";
13
- import type { ScheduledJobRecord } from "../scheduler.js";
14
-
15
- describe("Scheduler Durable Mode — persistJob/removeJob", () => {
16
- it("durable mode에서 runAfter → persistJob 콜백 호출", async () => {
17
- const persisted: ScheduledJobRecord[] = [];
18
-
19
- const scheduler = createScheduler({
20
- persistJob: async (job) => {
21
- persisted.push(job);
22
- },
23
- removeJob: async () => true,
24
- });
25
-
26
- scheduler.registerAction("test.durable", async () => {});
27
- const id = scheduler.runAfter(5000, "test.durable", { key: "value" });
28
-
29
- // persistJob은 비동기이므로 약간 대기
30
- await new Promise((r) => setTimeout(r, 50));
31
-
32
- expect(persisted.length).toBe(1);
33
- expect(persisted[0].id).toBe(id);
34
- expect(persisted[0].action).toBe("test.durable");
35
- expect(persisted[0].args).toEqual({ key: "value" });
36
- expect(persisted[0].runAt).toBeInstanceOf(Date);
37
- expect(persisted[0].runAt.getTime()).toBeGreaterThan(Date.now() + 4000); // ~5초 후
38
- });
39
-
40
- it("durable mode에서 runAfter → setTimeout 실행 안 함 (외부 폴러에 위임)", async () => {
41
- let actionExecuted = false;
42
-
43
- const scheduler = createScheduler({
44
- persistJob: async () => {},
45
- removeJob: async () => true,
46
- });
47
-
48
- scheduler.registerAction("no.exec", async () => {
49
- actionExecuted = true;
50
- });
51
-
52
- scheduler.runAfter(50, "no.exec");
53
-
54
- // 100ms 대기 — durable mode에서는 action이 실행되지 않아야 함
55
- await new Promise((r) => setTimeout(r, 150));
56
-
57
- expect(actionExecuted).toBe(false);
58
- });
59
-
60
- it("durable mode에서 cancel → removeJob 콜백 호출", async () => {
61
- const removed: string[] = [];
62
-
63
- const scheduler = createScheduler({
64
- persistJob: async () => {},
65
- removeJob: async (jobId) => {
66
- removed.push(jobId);
67
- return true;
68
- },
69
- });
70
-
71
- scheduler.registerAction("noop", async () => {});
72
- const id = scheduler.runAfter(10000, "noop");
73
-
74
- // persistJob 완료 대기
75
- await new Promise((r) => setTimeout(r, 50));
76
-
77
- scheduler.cancel(id);
78
-
79
- // removeJob은 비동기이므로 대기
80
- await new Promise((r) => setTimeout(r, 50));
81
-
82
- expect(removed.length).toBeGreaterThanOrEqual(1);
83
- expect(removed).toContain(id);
84
- });
85
-
86
- it("durable mode에서 onError action 이름이 persistJob에 전달", async () => {
87
- const persisted: ScheduledJobRecord[] = [];
88
-
89
- const scheduler = createScheduler({
90
- persistJob: async (job) => {
91
- persisted.push(job);
92
- },
93
- removeJob: async () => true,
94
- });
95
-
96
- scheduler.registerAction("step1", async () => {});
97
- scheduler.registerAction("onFail", async () => {});
98
-
99
- scheduler.runAfter(1000, "step1", {}, { onError: "onFail" });
100
-
101
- await new Promise((r) => setTimeout(r, 50));
102
-
103
- expect(persisted.length).toBe(1);
104
- expect(persisted[0].onErrorAction).toBe("onFail");
105
- });
106
-
107
- it("persistJob 실패 시 인메모리 fallback으로 실행", async () => {
108
- let actionExecuted = false;
109
-
110
- const scheduler = createScheduler({
111
- persistJob: async () => {
112
- throw new Error("DB connection failed");
113
- },
114
- removeJob: async () => true,
115
- });
116
-
117
- scheduler.registerAction("fallback.test", async () => {
118
- actionExecuted = true;
119
- });
120
-
121
- // suppress expected error log
122
- const origError = console.error;
123
- console.error = () => {};
124
-
125
- scheduler.runAfter(50, "fallback.test");
126
-
127
- // persistJob 실패 → fallback → setTimeout(50ms) → 실행
128
- await new Promise((r) => setTimeout(r, 300));
129
-
130
- console.error = origError;
131
-
132
- expect(actionExecuted).toBe(true);
133
- });
134
-
135
- it("콜백 미설정 시 기존 인메모리 mode 유지", async () => {
136
- let executed = false;
137
-
138
- const scheduler = createScheduler(); // no options
139
-
140
- scheduler.registerAction("mem.test", async () => {
141
- executed = true;
142
- });
143
-
144
- scheduler.runAfter(50, "mem.test");
145
-
146
- await new Promise((r) => setTimeout(r, 150));
147
-
148
- expect(executed).toBe(true);
149
- });
150
-
151
- it("runAt도 durable mode에서 persistJob 호출", async () => {
152
- const persisted: ScheduledJobRecord[] = [];
153
-
154
- const scheduler = createScheduler({
155
- persistJob: async (job) => {
156
- persisted.push(job);
157
- },
158
- removeJob: async () => true,
159
- });
160
-
161
- scheduler.registerAction("future.task", async () => {});
162
-
163
- const futureTime = new Date(Date.now() + 60_000); // 1분 후
164
- scheduler.runAt(futureTime, "future.task", { x: 1 });
165
-
166
- await new Promise((r) => setTimeout(r, 50));
167
-
168
- expect(persisted.length).toBe(1);
169
- expect(persisted[0].action).toBe("future.task");
170
- // runAt → runAfter 변환이므로 runAt 시간과 근사해야 함
171
- expect(Math.abs(persisted[0].runAt.getTime() - futureTime.getTime())).toBeLessThan(1000);
172
- });
173
- });