@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.
- package/dist/crud.d.ts +2 -2
- package/dist/crud.js +225 -208
- package/dist/index.d.ts +7 -3
- package/dist/index.js +4 -1
- package/dist/reactive.js +10 -3
- package/dist/retry.js +1 -1
- package/dist/rls-db.d.ts +2 -2
- package/dist/rls-db.js +1 -5
- package/dist/scheduler.d.ts +2 -0
- package/dist/scheduler.js +16 -6
- package/dist/server.d.ts +0 -1
- package/dist/server.js +0 -1
- package/dist/storage.js +29 -22
- package/dist/v.d.ts +2 -2
- package/dist/workflow-types.d.ts +81 -0
- package/dist/workflow-types.js +12 -0
- package/dist/workflow.d.ts +30 -0
- package/dist/workflow.js +150 -0
- package/dist/workflows-api.d.ts +13 -0
- package/dist/workflows-api.js +321 -0
- package/package.json +46 -42
- package/src/__tests__/auth.test.ts +90 -86
- package/src/__tests__/crons.test.ts +69 -67
- package/src/__tests__/crud-codegen-integration.test.ts +164 -170
- package/src/__tests__/crud-owner-rls.test.ts +308 -301
- package/src/__tests__/crud.test.ts +694 -711
- package/src/__tests__/dist-exports.test.ts +120 -114
- package/src/__tests__/fixtures/basic/auth.ts +16 -16
- package/src/__tests__/fixtures/basic/drizzle.config.ts +1 -4
- package/src/__tests__/fixtures/basic/index.ts +1 -1
- package/src/__tests__/fixtures/basic/schema.ts +1 -1
- package/src/__tests__/fixtures/basic/tasks.ts +4 -4
- package/src/__tests__/fixtures/common/auth-schema.ts +38 -34
- package/src/__tests__/helpers/basic-rls-fixture.ts +80 -78
- package/src/__tests__/helpers/pglite-migrations.ts +2 -5
- package/src/__tests__/helpers/pglite-rls-session.ts +13 -16
- package/src/__tests__/helpers/seed-like-fill.ts +50 -44
- package/src/__tests__/helpers/test-gencow-ctx-rls.ts +4 -7
- package/src/__tests__/httpaction.test.ts +91 -91
- package/src/__tests__/image-optimization.test.ts +570 -574
- package/src/__tests__/load.test.ts +321 -308
- package/src/__tests__/network-sim.test.ts +238 -215
- package/src/__tests__/reactive.test.ts +380 -358
- package/src/__tests__/retry.test.ts +99 -84
- package/src/__tests__/rls-crud-basic.test.ts +172 -245
- package/src/__tests__/rls-crud-no-owner-rls-pglite.test.ts +81 -81
- package/src/__tests__/rls-custom-mutation-handlers.test.ts +47 -94
- package/src/__tests__/rls-custom-query-handlers.test.ts +92 -92
- package/src/__tests__/rls-db-leased-connection.test.ts +2 -6
- package/src/__tests__/rls-session-and-policies.test.ts +181 -199
- package/src/__tests__/scheduler-durable-v2.test.ts +199 -181
- package/src/__tests__/scheduler-durable.test.ts +117 -117
- package/src/__tests__/scheduler-exec.test.ts +258 -246
- package/src/__tests__/scheduler.test.ts +129 -111
- package/src/__tests__/storage.test.ts +282 -269
- package/src/__tests__/tsconfig.json +6 -6
- package/src/__tests__/validator.test.ts +236 -232
- package/src/__tests__/workflow.test.ts +606 -0
- package/src/__tests__/ws-integration.test.ts +223 -218
- package/src/__tests__/ws-scale.test.ts +168 -159
- package/src/auth-config.ts +18 -18
- package/src/auth.ts +106 -106
- package/src/crons.ts +77 -77
- package/src/crud.ts +523 -479
- package/src/index.ts +71 -6
- package/src/reactive.ts +357 -331
- package/src/retry.ts +51 -54
- package/src/rls-db.ts +195 -205
- package/src/rls.ts +33 -36
- package/src/scheduler.ts +237 -211
- package/src/server.ts +0 -1
- package/src/storage.ts +632 -593
- package/src/v.ts +119 -114
- package/src/workflow-types.ts +108 -0
- package/src/workflow.ts +188 -0
- package/src/workflows-api.ts +415 -0
- package/src/db.ts +0 -18
|
@@ -15,256 +15,274 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { describe, it, expect } from "bun:test";
|
|
18
|
-
import { createScheduler } from "../scheduler";
|
|
19
|
-
import type { ScheduledJobRecord } from "../scheduler";
|
|
18
|
+
import { createScheduler } from "../scheduler.js";
|
|
19
|
+
import type { ScheduledJobRecord } from "../scheduler.js";
|
|
20
20
|
|
|
21
21
|
describe("Scheduler Durable v2 — Platform DB 중앙화", () => {
|
|
22
|
-
|
|
22
|
+
// ── args 전달 정확성 ──
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
it("persistJob에 args가 정확히 전달된다 (중첩 객체)", async () => {
|
|
25
|
+
const persisted: ScheduledJobRecord[] = [];
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const complexArgs = {
|
|
35
|
-
nested: { deeply: { value: 42 } },
|
|
36
|
-
array: [1, 2, { x: "y" }],
|
|
37
|
-
unicode: "한글테스트 🎯",
|
|
38
|
-
nullValue: null,
|
|
39
|
-
boolValue: false,
|
|
40
|
-
zero: 0,
|
|
41
|
-
emptyString: "",
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
scheduler.runAfter(1000, "deep.args", complexArgs);
|
|
45
|
-
await new Promise((r) => setTimeout(r, 50));
|
|
27
|
+
const scheduler = createScheduler({
|
|
28
|
+
persistJob: async (job) => {
|
|
29
|
+
persisted.push(job);
|
|
30
|
+
},
|
|
31
|
+
removeJob: async () => true,
|
|
32
|
+
});
|
|
46
33
|
|
|
47
|
-
|
|
48
|
-
|
|
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,
|
|
49
61
|
});
|
|
50
62
|
|
|
51
|
-
|
|
52
|
-
|
|
63
|
+
scheduler.registerAction("no.args", async () => {});
|
|
64
|
+
scheduler.runAfter(1000, "no.args"); // args 생략
|
|
65
|
+
|
|
66
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
53
67
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
+
});
|
|
58
76
|
|
|
59
|
-
|
|
60
|
-
scheduler.runAfter(1000, "no.args"); // args 생략
|
|
77
|
+
// ── 동시 등록 id 고유성 ──
|
|
61
78
|
|
|
62
|
-
|
|
79
|
+
it("10개 job 동시 등록 시 모든 id가 고유하다", async () => {
|
|
80
|
+
const persisted: ScheduledJobRecord[] = [];
|
|
63
81
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
82
|
+
const scheduler = createScheduler({
|
|
83
|
+
persistJob: async (job) => {
|
|
84
|
+
persisted.push(job);
|
|
85
|
+
},
|
|
86
|
+
removeJob: async () => true,
|
|
67
87
|
});
|
|
68
88
|
|
|
69
|
-
|
|
89
|
+
for (let i = 0; i < 10; i++) {
|
|
90
|
+
scheduler.registerAction(`batch.${i}`, async () => {});
|
|
91
|
+
}
|
|
70
92
|
|
|
71
|
-
|
|
72
|
-
|
|
93
|
+
const ids: string[] = [];
|
|
94
|
+
for (let i = 0; i < 10; i++) {
|
|
95
|
+
ids.push(scheduler.runAfter(1000, `batch.${i}`, { idx: i }));
|
|
96
|
+
}
|
|
73
97
|
|
|
74
|
-
|
|
75
|
-
persistJob: async (job) => { persisted.push(job); },
|
|
76
|
-
removeJob: async () => true,
|
|
77
|
-
});
|
|
98
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
78
99
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
100
|
+
// 모든 id 고유
|
|
101
|
+
const uniqueIds = new Set(ids);
|
|
102
|
+
expect(uniqueIds.size).toBe(10);
|
|
82
103
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
104
|
+
// 모든 persistJob 호출됨
|
|
105
|
+
expect(persisted.length).toBe(10);
|
|
106
|
+
});
|
|
87
107
|
|
|
88
|
-
|
|
108
|
+
// ── runAfter(0) — 즉시 실행 ──
|
|
89
109
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
expect(uniqueIds.size).toBe(10);
|
|
110
|
+
it("runAfter(0) — durable mode에서도 즉시 persistJob 호출", async () => {
|
|
111
|
+
const persisted: ScheduledJobRecord[] = [];
|
|
93
112
|
|
|
94
|
-
|
|
95
|
-
|
|
113
|
+
const scheduler = createScheduler({
|
|
114
|
+
persistJob: async (job) => {
|
|
115
|
+
persisted.push(job);
|
|
116
|
+
},
|
|
117
|
+
removeJob: async () => true,
|
|
96
118
|
});
|
|
97
119
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
it("runAfter(0) — durable mode에서도 즉시 persistJob 호출", async () => {
|
|
101
|
-
const persisted: ScheduledJobRecord[] = [];
|
|
120
|
+
scheduler.registerAction("instant", async () => {});
|
|
102
121
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
removeJob: async () => true,
|
|
106
|
-
});
|
|
122
|
+
const beforeTime = Date.now();
|
|
123
|
+
scheduler.runAfter(0, "instant", { immediate: true });
|
|
107
124
|
|
|
108
|
-
|
|
125
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
109
126
|
|
|
110
|
-
|
|
111
|
-
|
|
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
|
+
});
|
|
112
132
|
|
|
113
|
-
|
|
133
|
+
// ── removeJob 실패 시 graceful 처리 ──
|
|
114
134
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
+
},
|
|
119
141
|
});
|
|
120
142
|
|
|
121
|
-
|
|
143
|
+
scheduler.registerAction("fail.cancel", async () => {});
|
|
144
|
+
const id = scheduler.runAfter(10000, "fail.cancel");
|
|
122
145
|
|
|
123
|
-
|
|
124
|
-
const scheduler = createScheduler({
|
|
125
|
-
persistJob: async () => {},
|
|
126
|
-
removeJob: async () => {
|
|
127
|
-
throw new Error("DB connection lost");
|
|
128
|
-
},
|
|
129
|
-
});
|
|
146
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
130
147
|
|
|
131
|
-
|
|
132
|
-
|
|
148
|
+
// suppress expected error
|
|
149
|
+
const orig = console.error;
|
|
150
|
+
console.error = () => {};
|
|
151
|
+
const result = scheduler.cancel(id);
|
|
152
|
+
console.error = orig;
|
|
133
153
|
|
|
134
|
-
|
|
154
|
+
// removeJob 실패 시 return false, throw 안 함
|
|
155
|
+
expect(typeof result).toBe("boolean");
|
|
156
|
+
});
|
|
135
157
|
|
|
136
|
-
|
|
137
|
-
const orig = console.error;
|
|
138
|
-
console.error = () => {};
|
|
139
|
-
const result = scheduler.cancel(id);
|
|
140
|
-
console.error = orig;
|
|
158
|
+
// ── 대형 args 안정성 ──
|
|
141
159
|
|
|
142
|
-
|
|
143
|
-
|
|
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,
|
|
144
168
|
});
|
|
145
169
|
|
|
146
|
-
|
|
170
|
+
scheduler.registerAction("large.payload", async () => {});
|
|
147
171
|
|
|
148
|
-
|
|
149
|
-
|
|
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
|
+
};
|
|
150
177
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
removeJob: async () => true,
|
|
154
|
-
});
|
|
178
|
+
scheduler.runAfter(1000, "large.payload", largeArgs);
|
|
179
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
155
180
|
|
|
156
|
-
|
|
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
|
+
});
|
|
157
185
|
|
|
158
|
-
|
|
159
|
-
const largeArgs = {
|
|
160
|
-
data: "x".repeat(10_000),
|
|
161
|
-
items: Array.from({ length: 100 }, (_, i) => ({ id: i, name: `item-${i}` })),
|
|
162
|
-
};
|
|
186
|
+
// ── onError + args 조합 ──
|
|
163
187
|
|
|
164
|
-
|
|
165
|
-
|
|
188
|
+
it("onError와 args가 함께 persistJob에 전달된다", async () => {
|
|
189
|
+
const persisted: ScheduledJobRecord[] = [];
|
|
166
190
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
191
|
+
const scheduler = createScheduler({
|
|
192
|
+
persistJob: async (job) => {
|
|
193
|
+
persisted.push(job);
|
|
194
|
+
},
|
|
195
|
+
removeJob: async () => true,
|
|
170
196
|
});
|
|
171
197
|
|
|
172
|
-
|
|
198
|
+
scheduler.registerAction("pipeline.step1", async () => {});
|
|
199
|
+
scheduler.registerAction("pipeline.onError", async () => {});
|
|
173
200
|
|
|
174
|
-
|
|
175
|
-
const persisted: ScheduledJobRecord[] = [];
|
|
201
|
+
scheduler.runAfter(5000, "pipeline.step1", { step: 1, data: "test" }, { onError: "pipeline.onError" });
|
|
176
202
|
|
|
177
|
-
|
|
178
|
-
persistJob: async (job) => { persisted.push(job); },
|
|
179
|
-
removeJob: async () => true,
|
|
180
|
-
});
|
|
203
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
181
204
|
|
|
182
|
-
|
|
183
|
-
|
|
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
|
+
});
|
|
184
210
|
|
|
185
|
-
|
|
211
|
+
// ── runAt + durable 조합 ──
|
|
186
212
|
|
|
187
|
-
|
|
213
|
+
it("runAt(과거 시점) — durable mode에서도 persistJob 호출 (즉시 실행 대상)", async () => {
|
|
214
|
+
const persisted: ScheduledJobRecord[] = [];
|
|
188
215
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
216
|
+
const scheduler = createScheduler({
|
|
217
|
+
persistJob: async (job) => {
|
|
218
|
+
persisted.push(job);
|
|
219
|
+
},
|
|
220
|
+
removeJob: async () => true,
|
|
193
221
|
});
|
|
194
222
|
|
|
195
|
-
|
|
223
|
+
scheduler.registerAction("past.task", async () => {});
|
|
196
224
|
|
|
197
|
-
|
|
198
|
-
|
|
225
|
+
const pastTime = new Date(Date.now() - 60_000); // 1분 전
|
|
226
|
+
scheduler.runAt(pastTime, "past.task", { expired: true });
|
|
199
227
|
|
|
200
|
-
|
|
201
|
-
persistJob: async (job) => { persisted.push(job); },
|
|
202
|
-
removeJob: async () => true,
|
|
203
|
-
});
|
|
228
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
204
229
|
|
|
205
|
-
|
|
230
|
+
expect(persisted.length).toBe(1);
|
|
231
|
+
expect(persisted[0].args).toEqual({ expired: true });
|
|
232
|
+
// runAt이 과거여도 persistJob은 호출되어야 함 (폴러가 즉시 픽업)
|
|
233
|
+
});
|
|
206
234
|
|
|
207
|
-
|
|
208
|
-
scheduler.runAt(pastTime, "past.task", { expired: true });
|
|
235
|
+
// ── 다중 cancel ──
|
|
209
236
|
|
|
210
|
-
|
|
237
|
+
it("같은 job을 2번 cancel — 첫 번째만 removeJob 성공", async () => {
|
|
238
|
+
let removeCount = 0;
|
|
211
239
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
240
|
+
const scheduler = createScheduler({
|
|
241
|
+
persistJob: async () => {},
|
|
242
|
+
removeJob: async () => {
|
|
243
|
+
removeCount++;
|
|
244
|
+
return removeCount === 1; // 첫 번째만 true
|
|
245
|
+
},
|
|
215
246
|
});
|
|
216
247
|
|
|
217
|
-
|
|
248
|
+
scheduler.registerAction("double.cancel", async () => {});
|
|
249
|
+
const id = scheduler.runAfter(10000, "double.cancel");
|
|
218
250
|
|
|
219
|
-
|
|
220
|
-
let removeCount = 0;
|
|
251
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
221
252
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
removeCount++;
|
|
226
|
-
return removeCount === 1; // 첫 번째만 true
|
|
227
|
-
},
|
|
228
|
-
});
|
|
253
|
+
const r1 = scheduler.cancel(id);
|
|
254
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
255
|
+
const r2 = scheduler.cancel(id);
|
|
229
256
|
|
|
230
|
-
|
|
231
|
-
|
|
257
|
+
// 첫 번째 cancel은 성공 (pendingJobs에서 제거)
|
|
258
|
+
expect(r1).toBe(true);
|
|
259
|
+
// 두 번째 cancel은 이미 제거되었으므로 false
|
|
260
|
+
expect(r2).toBe(false);
|
|
261
|
+
});
|
|
232
262
|
|
|
233
|
-
|
|
263
|
+
// ── persistJob 순서 보장 ──
|
|
234
264
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
const r2 = scheduler.cancel(id);
|
|
265
|
+
it("연속 runAfter 호출 시 persistJob 순서 보장", async () => {
|
|
266
|
+
const order: string[] = [];
|
|
238
267
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
268
|
+
const scheduler = createScheduler({
|
|
269
|
+
persistJob: async (job) => {
|
|
270
|
+
order.push(job.action);
|
|
271
|
+
},
|
|
272
|
+
removeJob: async () => true,
|
|
243
273
|
});
|
|
244
274
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
const order: string[] = [];
|
|
249
|
-
|
|
250
|
-
const scheduler = createScheduler({
|
|
251
|
-
persistJob: async (job) => {
|
|
252
|
-
order.push(job.action);
|
|
253
|
-
},
|
|
254
|
-
removeJob: async () => true,
|
|
255
|
-
});
|
|
275
|
+
scheduler.registerAction("a", async () => {});
|
|
276
|
+
scheduler.registerAction("b", async () => {});
|
|
277
|
+
scheduler.registerAction("c", async () => {});
|
|
256
278
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
279
|
+
scheduler.runAfter(1000, "a");
|
|
280
|
+
scheduler.runAfter(2000, "b");
|
|
281
|
+
scheduler.runAfter(3000, "c");
|
|
260
282
|
|
|
261
|
-
|
|
262
|
-
scheduler.runAfter(2000, "b");
|
|
263
|
-
scheduler.runAfter(3000, "c");
|
|
283
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
264
284
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
expect(order).toEqual(["a", "b", "c"]);
|
|
269
|
-
});
|
|
285
|
+
// persistJob은 호출 순서대로 실행되어야 함
|
|
286
|
+
expect(order).toEqual(["a", "b", "c"]);
|
|
287
|
+
});
|
|
270
288
|
});
|