@gencow/core 0.1.26 → 0.1.28

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 (88) hide show
  1. package/dist/crud.d.ts +12 -0
  2. package/dist/crud.js +16 -0
  3. package/dist/db.d.ts +13 -0
  4. package/dist/db.js +16 -0
  5. package/dist/document-types.d.ts +65 -0
  6. package/dist/document-types.js +15 -0
  7. package/dist/grounded-answer-types.d.ts +62 -0
  8. package/dist/grounded-answer-types.js +6 -0
  9. package/dist/index.d.ts +12 -2
  10. package/dist/index.js +5 -1
  11. package/dist/rag-ingest-types.d.ts +39 -0
  12. package/dist/rag-ingest-types.js +1 -0
  13. package/dist/rag-operations-types.d.ts +81 -0
  14. package/dist/rag-operations-types.js +1 -0
  15. package/dist/rag-schema.d.ts +1557 -0
  16. package/dist/rag-schema.js +87 -0
  17. package/dist/reactive.d.ts +13 -0
  18. package/dist/rls-db.d.ts +9 -2
  19. package/dist/runtime-env-policy.d.ts +5 -0
  20. package/dist/runtime-env-policy.js +56 -0
  21. package/dist/search-types.d.ts +83 -0
  22. package/dist/search-types.js +1 -0
  23. package/dist/server.d.ts +1 -2
  24. package/dist/server.js +0 -1
  25. package/dist/storage-shared.d.ts +36 -0
  26. package/dist/storage-shared.js +39 -0
  27. package/dist/storage.d.ts +2 -26
  28. package/dist/storage.js +19 -15
  29. package/dist/workflow-types.d.ts +3 -1
  30. package/package.json +1 -1
  31. package/src/crud.ts +33 -0
  32. package/src/document-types.ts +95 -0
  33. package/src/grounded-answer-types.ts +78 -0
  34. package/src/index.ts +68 -2
  35. package/src/rag-ingest-types.ts +52 -0
  36. package/src/rag-operations-types.ts +90 -0
  37. package/src/rag-schema.ts +94 -0
  38. package/src/reactive.ts +13 -0
  39. package/src/rls-db.ts +9 -4
  40. package/src/runtime-env-policy.ts +66 -0
  41. package/src/search-types.ts +91 -0
  42. package/src/server.ts +1 -2
  43. package/src/storage-shared.ts +74 -0
  44. package/src/storage.ts +29 -46
  45. package/src/workflow-types.ts +3 -1
  46. package/src/__tests__/auth.test.ts +0 -118
  47. package/src/__tests__/crons.test.ts +0 -83
  48. package/src/__tests__/crud-codegen-integration.test.ts +0 -246
  49. package/src/__tests__/crud-owner-rls.test.ts +0 -387
  50. package/src/__tests__/crud.test.ts +0 -930
  51. package/src/__tests__/dist-exports.test.ts +0 -176
  52. package/src/__tests__/fixtures/basic/auth.ts +0 -32
  53. package/src/__tests__/fixtures/basic/drizzle.config.ts +0 -12
  54. package/src/__tests__/fixtures/basic/index.ts +0 -6
  55. package/src/__tests__/fixtures/basic/migrations/0000_last_warstar.sql +0 -75
  56. package/src/__tests__/fixtures/basic/migrations/meta/0000_snapshot.json +0 -497
  57. package/src/__tests__/fixtures/basic/migrations/meta/_journal.json +0 -13
  58. package/src/__tests__/fixtures/basic/schema.ts +0 -51
  59. package/src/__tests__/fixtures/basic/tasks.ts +0 -15
  60. package/src/__tests__/fixtures/common/auth-schema.ts +0 -67
  61. package/src/__tests__/helpers/basic-rls-fixture.ts +0 -135
  62. package/src/__tests__/helpers/pglite-migrations.ts +0 -32
  63. package/src/__tests__/helpers/pglite-rls-session.ts +0 -51
  64. package/src/__tests__/helpers/seed-like-fill.ts +0 -202
  65. package/src/__tests__/helpers/test-gencow-ctx-rls.ts +0 -50
  66. package/src/__tests__/httpaction.test.ts +0 -122
  67. package/src/__tests__/image-optimization.test.ts +0 -648
  68. package/src/__tests__/load.test.ts +0 -389
  69. package/src/__tests__/network-sim.test.ts +0 -319
  70. package/src/__tests__/reactive.test.ts +0 -479
  71. package/src/__tests__/retry.test.ts +0 -113
  72. package/src/__tests__/rls-crud-basic.test.ts +0 -317
  73. package/src/__tests__/rls-crud-no-owner-rls-pglite.test.ts +0 -117
  74. package/src/__tests__/rls-custom-mutation-handlers.test.ts +0 -142
  75. package/src/__tests__/rls-custom-query-handlers.test.ts +0 -128
  76. package/src/__tests__/rls-db-leased-connection.test.ts +0 -118
  77. package/src/__tests__/rls-session-and-policies.test.ts +0 -228
  78. package/src/__tests__/scheduler-durable-v2.test.ts +0 -288
  79. package/src/__tests__/scheduler-durable.test.ts +0 -173
  80. package/src/__tests__/scheduler-exec.test.ts +0 -328
  81. package/src/__tests__/scheduler.test.ts +0 -187
  82. package/src/__tests__/storage.test.ts +0 -334
  83. package/src/__tests__/tsconfig.json +0 -8
  84. package/src/__tests__/validator.test.ts +0 -323
  85. package/src/__tests__/workflow.test.ts +0 -606
  86. package/src/__tests__/ws-integration.test.ts +0 -309
  87. package/src/__tests__/ws-scale.test.ts +0 -241
  88. package/src/auth.ts +0 -155
@@ -1,479 +0,0 @@
1
- /**
2
- * packages/core/src/__tests__/reactive.test.ts
3
- *
4
- * Tests for ctx.realtime.emit() push model and refresh() API.
5
- *
6
- * Run: bun test packages/core/src/__tests__/reactive.test.ts
7
- */
8
-
9
- import { describe, it, expect, mock, beforeEach } from "bun:test";
10
- import { buildRealtimeCtx, subscribe, deregisterClient, registerClient } from "../reactive.js";
11
- import type { GencowCtx } from "../reactive.js";
12
-
13
- // ─── Mock WebSocket (Bun-style WSContext) ────────────────────────────────────
14
-
15
- function makeMockWs() {
16
- const sent: string[] = [];
17
- return {
18
- send: (msg: string) => sent.push(msg),
19
- readyState: 1,
20
- _sent: sent,
21
- } as any;
22
- }
23
-
24
- // ─── buildRealtimeCtx ────────────────────────────────────────────────────────
25
-
26
- describe("buildRealtimeCtx()", () => {
27
- it("각 mutation 호출마다 독립적인 인스턴스를 반환한다", () => {
28
- const a = buildRealtimeCtx();
29
- const b = buildRealtimeCtx();
30
- expect(a).not.toBe(b);
31
- });
32
-
33
- it("emit() 호출 시 해당 queryKey 구독자에게 query:updated 메시지를 push한다", async () => {
34
- const ws = makeMockWs();
35
- subscribe("test.list", ws);
36
-
37
- const rt = buildRealtimeCtx();
38
- rt.emit("test.list", [{ id: 1, title: "Task A" }]);
39
-
40
- // 50ms debounce 대기
41
- await new Promise((r) => setTimeout(r, 60));
42
-
43
- expect(ws._sent).toHaveLength(1);
44
- const msg = JSON.parse(ws._sent[0]);
45
- expect(msg.type).toBe("query:updated");
46
- expect(msg.query).toBe("test.list");
47
- expect(msg.data).toEqual([{ id: 1, title: "Task A" }]);
48
-
49
- deregisterClient(ws);
50
- });
51
-
52
- it("50ms 내 동일 queryKey에 대한 연속 emit은 마지막 데이터만 push한다 (debounce)", async () => {
53
- const ws = makeMockWs();
54
- subscribe("test.debounce", ws);
55
-
56
- const rt = buildRealtimeCtx();
57
- rt.emit("test.debounce", [{ id: 1 }]); // 무시됨
58
- rt.emit("test.debounce", [{ id: 2 }]); // 무시됨
59
- rt.emit("test.debounce", [{ id: 3 }]); // 최종 emit
60
-
61
- // 50ms debounce 대기
62
- await new Promise((r) => setTimeout(r, 80));
63
-
64
- expect(ws._sent).toHaveLength(1);
65
- const msg = JSON.parse(ws._sent[0]);
66
- expect(msg.data).toEqual([{ id: 3 }]); // 마지막 값만 전달
67
-
68
- deregisterClient(ws);
69
- });
70
-
71
- it("서로 다른 queryKey emit은 각각 독립적으로 처리된다", async () => {
72
- const ws = makeMockWs();
73
- subscribe("alpha.list", ws);
74
- subscribe("beta.list", ws);
75
-
76
- const rt = buildRealtimeCtx();
77
- rt.emit("alpha.list", [{ id: 10 }]);
78
- rt.emit("beta.list", [{ id: 20 }]);
79
-
80
- await new Promise((r) => setTimeout(r, 80));
81
-
82
- expect(ws._sent).toHaveLength(2);
83
- const queries = ws._sent.map((s: string) => JSON.parse(s).query);
84
- expect(queries).toContain("alpha.list");
85
- expect(queries).toContain("beta.list");
86
-
87
- deregisterClient(ws);
88
- });
89
-
90
- it("구독자가 없으면 아무것도 전송하지 않는다", async () => {
91
- const rt = buildRealtimeCtx();
92
- rt.emit("no.subscribers", [{ id: 99 }]);
93
-
94
- await new Promise((r) => setTimeout(r, 80));
95
- // no assertion needed — just must not throw
96
- });
97
- });
98
-
99
- // ─── refresh() API ──────────────────────────────────────────────────────────
100
-
101
- describe("refresh() — 서버 쿼리 재실행 요청 큐", () => {
102
- it("refresh()로 등록된 queryKey가 _pendingRefresh에 큐잉된다", () => {
103
- const rt = buildRealtimeCtx();
104
- rt.refresh("tasks.list");
105
-
106
- expect((rt as any)._pendingRefresh).toContain("tasks.list");
107
- });
108
-
109
- it("동일 queryKey 중복 refresh는 한 번만 큐잉된다", () => {
110
- const rt = buildRealtimeCtx();
111
- rt.refresh("tasks.list");
112
- rt.refresh("tasks.list");
113
- rt.refresh("tasks.list");
114
-
115
- const count = (rt as any)._pendingRefresh.filter((k: string) => k === "tasks.list").length;
116
- expect(count).toBe(1);
117
- });
118
-
119
- it("서로 다른 queryKey는 각각 큐잉된다", () => {
120
- const rt = buildRealtimeCtx();
121
- rt.refresh("tasks.list");
122
- rt.refresh("users.list");
123
-
124
- expect((rt as any)._pendingRefresh).toContain("tasks.list");
125
- expect((rt as any)._pendingRefresh).toContain("users.list");
126
- });
127
- });
128
-
129
- // ─── emit()과 refresh() 병행 시나리오 ───────────────────────────────────────
130
-
131
- describe("emit()과 refresh() 병행", () => {
132
- it("emit()은 query:updated를 구독자에게 즉시 push한다", async () => {
133
- const wsSubscribed = makeMockWs();
134
- subscribe("items.list", wsSubscribed);
135
-
136
- const rt = buildRealtimeCtx();
137
- rt.emit("items.list", [{ id: 1 }]);
138
- await new Promise((r) => setTimeout(r, 80));
139
-
140
- expect(wsSubscribed._sent.some((s: string) => JSON.parse(s).type === "query:updated")).toBe(true);
141
-
142
- deregisterClient(wsSubscribed);
143
- });
144
-
145
- it("emit() 호출 후 _hasEmitted가 true로 설정된다", () => {
146
- const rt = buildRealtimeCtx();
147
- expect((rt as any)._hasEmitted).toBe(false);
148
-
149
- const ws = makeMockWs();
150
- subscribe("flag.test", ws);
151
- rt.emit("flag.test", [{ id: 1 }]);
152
-
153
- expect((rt as any)._hasEmitted).toBe(true);
154
-
155
- deregisterClient(ws);
156
- });
157
- });
158
-
159
- // ─── Secure by Default: public 플래그 테스트 ─────────────────────────────────
160
-
161
- import { query, mutation, getQueryDef, getRegisteredMutations } from "../reactive.js";
162
-
163
- describe("Secure by Default — public 플래그", () => {
164
- it("query() 기본값은 isPublic === false (auth 필수)", () => {
165
- const q = query("sectest.private", {
166
- handler: async () => [],
167
- });
168
- expect(q.isPublic).toBe(false);
169
- });
170
-
171
- it("query({ public: true }) 시 isPublic === true", () => {
172
- const q = query("sectest.public", {
173
- public: true,
174
- handler: async () => [],
175
- });
176
- expect(q.isPublic).toBe(true);
177
- });
178
-
179
- it("query() legacy handler 형식도 isPublic === false", () => {
180
- const q = query("sectest.legacy", async () => []);
181
- expect(q.isPublic).toBe(false);
182
- });
183
-
184
- it("getQueryDef()로 조회해도 isPublic 정보가 유지된다", () => {
185
- query("sectest.lookup", { public: true, handler: async () => "ok" });
186
- const def = getQueryDef("sectest.lookup");
187
- expect(def).toBeDefined();
188
- expect(def!.isPublic).toBe(true);
189
- });
190
-
191
- it("mutation() 기본값은 isPublic === false", () => {
192
- const m = mutation({
193
- name: "sectest.mut.private",
194
- handler: async () => ({ ok: true }),
195
- });
196
- expect(m.isPublic).toBe(false);
197
- });
198
-
199
- it("mutation({ public: true }) 시 isPublic === true", () => {
200
- const m = mutation({
201
- name: "sectest.mut.public",
202
- public: true,
203
- handler: async () => ({ ok: true }),
204
- });
205
- expect(m.isPublic).toBe(true);
206
- });
207
-
208
- it("mutation() legacy array 형식도 isPublic === false", () => {
209
- const m = mutation(["some.key"], async () => ({ ok: true }), "sectest.mut.legacy");
210
- expect(m.isPublic).toBe(false);
211
- });
212
-
213
- it("getRegisteredMutations()에서 isPublic 정보가 노출된다", () => {
214
- const all = getRegisteredMutations();
215
- const pub = all.find((m) => m.name === "sectest.mut.public");
216
- const priv = all.find((m) => m.name === "sectest.mut.private");
217
- expect(pub?.isPublic).toBe(true);
218
- expect(priv?.isPublic).toBe(false);
219
- });
220
- });
221
-
222
- // ─── mutation("name", def) 새 시그니처 테스트 ────────────────────────────────
223
-
224
- describe("mutation(name, def) — query와 동일 패턴", () => {
225
- it("mutation('name', { handler })로 등록하면 name이 올바르게 설정된다", () => {
226
- const m = mutation("newsig.basic", {
227
- handler: async () => ({ ok: true }),
228
- });
229
- expect(
230
- (m as any).name ||
231
- (getRegisteredMutations().find((x) => x.handler === (m as any).handler) as any)?.name,
232
- ).toBeDefined();
233
- const all = getRegisteredMutations();
234
- const found = all.find((x) => x.name === "newsig.basic");
235
- expect(found).toBeDefined();
236
- expect(found!.isPublic).toBe(false);
237
- });
238
-
239
- it("mutation('name', { public: true })로 등록하면 isPublic === true", () => {
240
- const m = mutation("newsig.public", {
241
- public: true,
242
- handler: async () => ({ ok: true }),
243
- });
244
- expect(m.isPublic).toBe(true);
245
- });
246
-
247
- it("invalidates 미지정 시 빈 배열이 기본값 (하위호환)", () => {
248
- const m = mutation("newsig.noInvalidates", {
249
- handler: async () => ({ ok: true }),
250
- });
251
- const all = getRegisteredMutations();
252
- const found = all.find((x) => x.name === "newsig.noInvalidates");
253
- // invalidates는 deprecated이지만 빈 배열로 유지 (하위호환)
254
- expect(found).toBeDefined();
255
- });
256
-
257
- it("invalidates 지정해도 무시된다 (deprecated)", () => {
258
- const m = mutation("newsig.withInvalidates", {
259
- invalidates: ["tasks.list", "tasks.get"],
260
- handler: async () => ({ ok: true }),
261
- });
262
- const all = getRegisteredMutations();
263
- const found = all.find((x) => x.name === "newsig.withInvalidates");
264
- // invalidates는 deprecated — 전달해도 런타임에서 무시됨
265
- expect(found).toBeDefined();
266
- });
267
-
268
- it("기존 객체 스타일도 여전히 동작한다 (하위 호환)", () => {
269
- const m = mutation({
270
- name: "newsig.compat.object",
271
- handler: async () => ({ ok: true }),
272
- });
273
- const all = getRegisteredMutations();
274
- const found = all.find((x) => x.name === "newsig.compat.object");
275
- expect(found).toBeDefined();
276
- });
277
-
278
- it("기존 배열 스타일도 여전히 동작한다 (하위 호환)", () => {
279
- const m = mutation(["b.list"], async () => ({ ok: true }), "newsig.compat.array");
280
- const all = getRegisteredMutations();
281
- const found = all.find((x) => x.name === "newsig.compat.array");
282
- expect(found).toBeDefined();
283
- });
284
-
285
- it("이름 미지정 시 console.warn이 호출된다", () => {
286
- const warnSpy = mock(() => {});
287
- const originalWarn = console.warn;
288
- console.warn = warnSpy;
289
-
290
- mutation(["c.list"], async () => ({ ok: true }));
291
-
292
- expect(warnSpy).toHaveBeenCalled();
293
- const warnMsg = warnSpy.mock.calls[0][0] as string;
294
- expect(warnMsg).toContain("[gencow]");
295
- expect(warnMsg).toContain("without explicit name");
296
-
297
- console.warn = originalWarn;
298
- });
299
- });
300
-
301
- // ─── _flushRefresh — buildCtxForRefresh 통합 테스트 ──────────────────────────
302
-
303
- describe("_flushRefresh() — query re-run via buildCtxForRefresh", () => {
304
- it("buildCtxForRefresh 전달 시 query handler가 정상 ctx로 re-run된다", async () => {
305
- // 테스트용 query 등록
306
- const testQueryKey = "flush.test.rerun";
307
- const handler = mock(async (ctx: any) => {
308
- // ctx.db가 존재하는지 확인 (이전 버그: ctx = {} → crash)
309
- if (!ctx.db) throw new Error("ctx.db is undefined!");
310
- return [{ id: 1, count: ctx.db.mockValue }];
311
- });
312
-
313
- query(testQueryKey, { public: true, handler });
314
-
315
- // buildCtxForRefresh 콜백 제공
316
- const mockDb = { mockValue: 42 };
317
- const rt = buildRealtimeCtx({
318
- buildCtxForRefresh: () =>
319
- ({
320
- db: mockDb,
321
- auth: {
322
- getUserIdentity: () => null,
323
- requireAuth: () => {
324
- throw new Error();
325
- },
326
- },
327
- realtime: { emit: () => {}, refresh: () => {} },
328
- }) as any,
329
- });
330
-
331
- rt.refresh(testQueryKey);
332
- await rt._flushRefresh();
333
-
334
- // handler가 호출됐는지 확인
335
- expect(handler).toHaveBeenCalledTimes(1);
336
- // ctx.db가 올바르게 전달됐는지 확인
337
- const callCtx = handler.mock.calls[0][0];
338
- expect(callCtx.db).toBe(mockDb);
339
- });
340
-
341
- it("buildCtxForRefresh 미전달 시 ({} as ctx) 사용 + 경고 로그 출력", async () => {
342
- const testQueryKey = "flush.test.noCallback";
343
- const handler = mock(async (_ctx: any) => [{ id: 1 }]);
344
- query(testQueryKey, { public: true, handler });
345
-
346
- const warnSpy = mock(() => {});
347
- const originalWarn = console.warn;
348
- console.warn = warnSpy;
349
-
350
- // buildCtxForRefresh 없이 생성
351
- const rt = buildRealtimeCtx();
352
- rt.refresh(testQueryKey);
353
- await rt._flushRefresh();
354
-
355
- // 경고 출력 확인
356
- const warnCalls = warnSpy.mock.calls.map((c) => String(c[0]));
357
- const hasWarning = warnCalls.some((msg) => msg.includes("buildCtxForRefresh not provided"));
358
- expect(hasWarning).toBe(true);
359
-
360
- console.warn = originalWarn;
361
- });
362
-
363
- it("refresh 결과가 WS 구독자에게 query:updated로 push된다", async () => {
364
- const testQueryKey = "flush.test.push";
365
- const freshData = [{ id: 99, name: "Refreshed" }];
366
- query(testQueryKey, {
367
- public: true,
368
- handler: async () => freshData,
369
- });
370
-
371
- const ws = makeMockWs();
372
- subscribe(testQueryKey, ws);
373
-
374
- const rt = buildRealtimeCtx({
375
- buildCtxForRefresh: () =>
376
- ({
377
- db: {},
378
- auth: {
379
- getUserIdentity: () => null,
380
- requireAuth: () => {
381
- throw new Error();
382
- },
383
- },
384
- realtime: { emit: () => {}, refresh: () => {} },
385
- }) as any,
386
- });
387
-
388
- rt.refresh(testQueryKey);
389
- await rt._flushRefresh();
390
-
391
- // 50ms debounce 대기
392
- await new Promise((r) => setTimeout(r, 80));
393
-
394
- expect(ws._sent.length).toBeGreaterThanOrEqual(1);
395
- const msg = JSON.parse(ws._sent[ws._sent.length - 1]);
396
- expect(msg.type).toBe("query:updated");
397
- expect(msg.query).toBe(testQueryKey);
398
- expect(msg.data).toEqual(freshData);
399
-
400
- deregisterClient(ws);
401
- });
402
-
403
- it("httpCallback 모드에서 refresh 결과가 callback으로 전달된다", async () => {
404
- const testQueryKey = "flush.test.http";
405
- const freshData = [{ id: 77, title: "HTTP Push" }];
406
- query(testQueryKey, {
407
- public: true,
408
- handler: async () => freshData,
409
- });
410
-
411
- const httpCallback = mock((_event: any) => {});
412
-
413
- const rt = buildRealtimeCtx({
414
- httpCallback,
415
- buildCtxForRefresh: () =>
416
- ({
417
- db: {},
418
- auth: {
419
- getUserIdentity: () => null,
420
- requireAuth: () => {
421
- throw new Error();
422
- },
423
- },
424
- realtime: { emit: () => {}, refresh: () => {} },
425
- }) as any,
426
- });
427
-
428
- rt.refresh(testQueryKey);
429
- await rt._flushRefresh();
430
-
431
- expect(httpCallback).toHaveBeenCalledTimes(1);
432
- const event = httpCallback.mock.calls[0][0];
433
- expect(event.type).toBe("emit");
434
- expect(event.queryKey).toBe(testQueryKey);
435
- expect(event.data).toEqual(freshData);
436
- });
437
-
438
- it("flush 후 _pendingRefresh가 비워진다", async () => {
439
- const testQueryKey = "flush.test.clear";
440
- query(testQueryKey, { public: true, handler: async () => [] });
441
-
442
- const rt = buildRealtimeCtx({
443
- buildCtxForRefresh: () =>
444
- ({
445
- db: {},
446
- auth: {
447
- getUserIdentity: () => null,
448
- requireAuth: () => {
449
- throw new Error();
450
- },
451
- },
452
- realtime: { emit: () => {}, refresh: () => {} },
453
- }) as any,
454
- });
455
-
456
- rt.refresh(testQueryKey);
457
- expect(rt._pendingRefresh).toHaveLength(1);
458
-
459
- await rt._flushRefresh();
460
- expect(rt._pendingRefresh).toHaveLength(0);
461
- });
462
-
463
- it("미등록 queryKey refresh는 무시되고 경고 출력", async () => {
464
- const warnSpy = mock(() => {});
465
- const originalWarn = console.warn;
466
- console.warn = warnSpy;
467
-
468
- const rt = buildRealtimeCtx({
469
- buildCtxForRefresh: () => ({}) as any,
470
- });
471
- rt.refresh("nonexistent.query.key.xyz");
472
- await rt._flushRefresh();
473
-
474
- const hasWarning = warnSpy.mock.calls.some((c) => String(c[0]).includes("query not found in registry"));
475
- expect(hasWarning).toBe(true);
476
-
477
- console.warn = originalWarn;
478
- });
479
- });
@@ -1,113 +0,0 @@
1
- import { describe, test, expect, mock } from "bun:test";
2
- import { withRetry } from "../retry.js";
3
-
4
- describe("withRetry", () => {
5
- test("성공 시 즉시 반환", async () => {
6
- const result = await withRetry(async () => "success");
7
- expect(result).toBe("success");
8
- });
9
-
10
- test("첫 시도 실패 후 재시도 성공", async () => {
11
- let attempt = 0;
12
- const result = await withRetry(
13
- async () => {
14
- attempt++;
15
- if (attempt < 2) throw new Error("fail");
16
- return "recovered";
17
- },
18
- { initialBackoffMs: 10 },
19
- );
20
-
21
- expect(result).toBe("recovered");
22
- expect(attempt).toBe(2);
23
- });
24
-
25
- test("maxAttempts 초과 시 마지막 에러 throw", async () => {
26
- let attempt = 0;
27
- try {
28
- await withRetry(
29
- async () => {
30
- attempt++;
31
- throw new Error(`fail-${attempt}`);
32
- },
33
- { maxAttempts: 3, initialBackoffMs: 10 },
34
- );
35
- expect(true).toBe(false); // 여기에 도달하면 안됨
36
- } catch (err: unknown) {
37
- expect((err as Error).message).toBe("fail-3");
38
- expect(attempt).toBe(3);
39
- }
40
- });
41
-
42
- test("shouldRetry가 false 반환 시 즉시 throw", async () => {
43
- let attempt = 0;
44
- try {
45
- await withRetry(
46
- async () => {
47
- attempt++;
48
- throw new Error("non-retryable");
49
- },
50
- {
51
- maxAttempts: 5,
52
- initialBackoffMs: 10,
53
- shouldRetry: (err) => (err as Error).message !== "non-retryable",
54
- },
55
- );
56
- } catch (err: unknown) {
57
- expect((err as Error).message).toBe("non-retryable");
58
- expect(attempt).toBe(1); // 재시도 없이 즉시 실패
59
- }
60
- });
61
-
62
- test("onRetry 콜백 호출", async () => {
63
- let attempt = 0;
64
- const retries: number[] = [];
65
-
66
- await withRetry(
67
- async () => {
68
- attempt++;
69
- if (attempt < 3) throw new Error("fail");
70
- return "ok";
71
- },
72
- {
73
- initialBackoffMs: 10,
74
- onRetry: (_err, attemptNum, _delay) => {
75
- retries.push(attemptNum);
76
- },
77
- },
78
- );
79
-
80
- expect(retries).toEqual([1, 2]);
81
- });
82
-
83
- test("maxAttempts < 1이면 에러", async () => {
84
- try {
85
- await withRetry(async () => "ok", { maxAttempts: 0 });
86
- expect(true).toBe(false);
87
- } catch (err: unknown) {
88
- expect((err as Error).message).toContain("maxAttempts must be >= 1");
89
- }
90
- });
91
-
92
- test("maxAttempts = 1이면 재시도 없이 즉시 throw", async () => {
93
- let attempt = 0;
94
- try {
95
- await withRetry(
96
- async () => {
97
- attempt++;
98
- throw new Error("once");
99
- },
100
- { maxAttempts: 1 },
101
- );
102
- } catch (err: unknown) {
103
- expect((err as Error).message).toBe("once");
104
- expect(attempt).toBe(1);
105
- }
106
- });
107
-
108
- test("비동기 함수 결과 타입 보존", async () => {
109
- const result = await withRetry(async () => ({ id: 1, name: "test" }));
110
- expect(result.id).toBe(1);
111
- expect(result.name).toBe("test");
112
- });
113
- });