@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
@@ -7,313 +7,317 @@
7
7
  */
8
8
 
9
9
  import { describe, it, expect } from "bun:test";
10
- import { v, parseArgs, GencowValidationError } from "../v";
10
+ import { v, parseArgs, GencowValidationError } from "../v.js";
11
11
 
12
12
  // ─── v.string() ─────────────────────────────────────────
13
13
 
14
14
  describe("v.string()", () => {
15
- const validator = v.string();
15
+ const validator = v.string();
16
16
 
17
- it("문자열 통과", () => {
18
- expect(validator.parse("hello")).toBe("hello");
19
- });
17
+ it("문자열 통과", () => {
18
+ expect(validator.parse("hello")).toBe("hello");
19
+ });
20
20
 
21
- it("빈 문자열 통과", () => {
22
- expect(validator.parse("")).toBe("");
23
- });
21
+ it("빈 문자열 통과", () => {
22
+ expect(validator.parse("")).toBe("");
23
+ });
24
24
 
25
- it("숫자 → 에러", () => {
26
- expect(() => validator.parse(42)).toThrow("Expected string");
27
- });
25
+ it("숫자 → 에러", () => {
26
+ expect(() => validator.parse(42)).toThrow("Expected string");
27
+ });
28
28
 
29
- it("불리언 → 에러", () => {
30
- expect(() => validator.parse(true)).toThrow("Expected string");
31
- });
29
+ it("불리언 → 에러", () => {
30
+ expect(() => validator.parse(true)).toThrow("Expected string");
31
+ });
32
32
 
33
- it("null → 에러", () => {
34
- expect(() => validator.parse(null)).toThrow("Expected string");
35
- });
33
+ it("null → 에러", () => {
34
+ expect(() => validator.parse(null)).toThrow("Expected string");
35
+ });
36
36
 
37
- it("undefined → 에러", () => {
38
- expect(() => validator.parse(undefined)).toThrow("Expected string");
39
- });
37
+ it("undefined → 에러", () => {
38
+ expect(() => validator.parse(undefined)).toThrow("Expected string");
39
+ });
40
40
  });
41
41
 
42
42
  // ─── v.number() ─────────────────────────────────────────
43
43
 
44
44
  describe("v.number()", () => {
45
- const validator = v.number();
45
+ const validator = v.number();
46
46
 
47
- it("정수 통과", () => {
48
- expect(validator.parse(42)).toBe(42);
49
- });
47
+ it("정수 통과", () => {
48
+ expect(validator.parse(42)).toBe(42);
49
+ });
50
50
 
51
- it("실수 통과", () => {
52
- expect(validator.parse(3.14)).toBe(3.14);
53
- });
51
+ it("실수 통과", () => {
52
+ expect(validator.parse(3.14)).toBe(3.14);
53
+ });
54
54
 
55
- it("0 통과", () => {
56
- expect(validator.parse(0)).toBe(0);
57
- });
55
+ it("0 통과", () => {
56
+ expect(validator.parse(0)).toBe(0);
57
+ });
58
58
 
59
- it("음수 통과", () => {
60
- expect(validator.parse(-10)).toBe(-10);
61
- });
59
+ it("음수 통과", () => {
60
+ expect(validator.parse(-10)).toBe(-10);
61
+ });
62
62
 
63
- it("숫자 문자열 → 자동 변환", () => {
64
- expect(validator.parse("42")).toBe(42);
65
- });
63
+ it("숫자 문자열 → 자동 변환", () => {
64
+ expect(validator.parse("42")).toBe(42);
65
+ });
66
66
 
67
- it("비숫자 문자열 → 에러", () => {
68
- expect(() => validator.parse("abc")).toThrow("Expected number");
69
- });
67
+ it("비숫자 문자열 → 에러", () => {
68
+ expect(() => validator.parse("abc")).toThrow("Expected number");
69
+ });
70
70
 
71
- it("불리언 → 에러", () => {
72
- expect(() => validator.parse(true)).toThrow("Expected number");
73
- });
71
+ it("불리언 → 에러", () => {
72
+ expect(() => validator.parse(true)).toThrow("Expected number");
73
+ });
74
74
 
75
- it("NaN 입력 → 에러", () => {
76
- expect(() => validator.parse(NaN)).toThrow("Expected number");
77
- });
75
+ it("NaN 입력 → 에러", () => {
76
+ expect(() => validator.parse(NaN)).toThrow("Expected number");
77
+ });
78
78
  });
79
79
 
80
80
  // ─── v.boolean() ────────────────────────────────────────
81
81
 
82
82
  describe("v.boolean()", () => {
83
- const validator = v.boolean();
83
+ const validator = v.boolean();
84
84
 
85
- it("true 통과", () => {
86
- expect(validator.parse(true)).toBe(true);
87
- });
85
+ it("true 통과", () => {
86
+ expect(validator.parse(true)).toBe(true);
87
+ });
88
88
 
89
- it("false 통과", () => {
90
- expect(validator.parse(false)).toBe(false);
91
- });
89
+ it("false 통과", () => {
90
+ expect(validator.parse(false)).toBe(false);
91
+ });
92
92
 
93
- it("문자열 → 에러", () => {
94
- expect(() => validator.parse("true")).toThrow("Expected boolean");
95
- });
93
+ it("문자열 → 에러", () => {
94
+ expect(() => validator.parse("true")).toThrow("Expected boolean");
95
+ });
96
96
 
97
- it("숫자 → 에러", () => {
98
- expect(() => validator.parse(1)).toThrow("Expected boolean");
99
- });
97
+ it("숫자 → 에러", () => {
98
+ expect(() => validator.parse(1)).toThrow("Expected boolean");
99
+ });
100
100
  });
101
101
 
102
102
  // ─── v.any() ────────────────────────────────────────────
103
103
 
104
104
  describe("v.any()", () => {
105
- const validator = v.any();
105
+ const validator = v.any();
106
106
 
107
- it("모든 값 통과 — 문자열", () => {
108
- expect(validator.parse("hello")).toBe("hello");
109
- });
107
+ it("모든 값 통과 — 문자열", () => {
108
+ expect(validator.parse("hello")).toBe("hello");
109
+ });
110
110
 
111
- it("모든 값 통과 — 숫자", () => {
112
- expect(validator.parse(42)).toBe(42);
113
- });
111
+ it("모든 값 통과 — 숫자", () => {
112
+ expect(validator.parse(42)).toBe(42);
113
+ });
114
114
 
115
- it("모든 값 통과 — null", () => {
116
- expect(validator.parse(null)).toBe(null);
117
- });
115
+ it("모든 값 통과 — null", () => {
116
+ expect(validator.parse(null)).toBe(null);
117
+ });
118
118
 
119
- it("모든 값 통과 — undefined", () => {
120
- expect(validator.parse(undefined)).toBe(undefined);
121
- });
119
+ it("모든 값 통과 — undefined", () => {
120
+ expect(validator.parse(undefined)).toBe(undefined);
121
+ });
122
122
 
123
- it("모든 값 통과 — 객체", () => {
124
- const obj = { key: "value" };
125
- expect(validator.parse(obj)).toBe(obj);
126
- });
123
+ it("모든 값 통과 — 객체", () => {
124
+ const obj = { key: "value" };
125
+ expect(validator.parse(obj)).toBe(obj);
126
+ });
127
127
  });
128
128
 
129
129
  // ─── v.optional() ───────────────────────────────────────
130
130
 
131
131
  describe("v.optional()", () => {
132
- const validator = v.optional(v.string());
132
+ const validator = v.optional(v.string());
133
133
 
134
- it("문자열 통과", () => {
135
- expect(validator.parse("hello")).toBe("hello");
136
- });
134
+ it("문자열 통과", () => {
135
+ expect(validator.parse("hello")).toBe("hello");
136
+ });
137
137
 
138
- it("undefined → undefined 반환", () => {
139
- expect(validator.parse(undefined)).toBe(undefined);
140
- });
138
+ it("undefined → undefined 반환", () => {
139
+ expect(validator.parse(undefined)).toBe(undefined);
140
+ });
141
141
 
142
- it("null → undefined 반환", () => {
143
- expect(validator.parse(null)).toBe(undefined);
144
- });
142
+ it("null → undefined 반환", () => {
143
+ expect(validator.parse(null)).toBe(undefined);
144
+ });
145
145
 
146
- it("숫자 → 에러 (내부 validator 적용)", () => {
147
- expect(() => validator.parse(42)).toThrow("Expected string");
148
- });
146
+ it("숫자 → 에러 (내부 validator 적용)", () => {
147
+ expect(() => validator.parse(42)).toThrow("Expected string");
148
+ });
149
149
  });
150
150
 
151
151
  // ─── v.array() ──────────────────────────────────────────
152
152
 
153
153
  describe("v.array()", () => {
154
- const validator = v.array(v.string());
154
+ const validator = v.array(v.string());
155
155
 
156
- it("문자열 배열 통과", () => {
157
- expect(validator.parse(["a", "b", "c"])).toEqual(["a", "b", "c"]);
158
- });
156
+ it("문자열 배열 통과", () => {
157
+ expect(validator.parse(["a", "b", "c"])).toEqual(["a", "b", "c"]);
158
+ });
159
159
 
160
- it("빈 배열 통과", () => {
161
- expect(validator.parse([])).toEqual([]);
162
- });
160
+ it("빈 배열 통과", () => {
161
+ expect(validator.parse([])).toEqual([]);
162
+ });
163
163
 
164
- it("비배열 → 에러", () => {
165
- expect(() => validator.parse("not array")).toThrow("Expected array");
166
- });
164
+ it("비배열 → 에러", () => {
165
+ expect(() => validator.parse("not array")).toThrow("Expected array");
166
+ });
167
167
 
168
- it("원소 타입 불일치 → 에러", () => {
169
- expect(() => validator.parse([1, 2, 3])).toThrow("Expected string");
170
- });
168
+ it("원소 타입 불일치 → 에러", () => {
169
+ expect(() => validator.parse([1, 2, 3])).toThrow("Expected string");
170
+ });
171
171
 
172
- it("혼합 배열 → 에러", () => {
173
- expect(() => validator.parse(["ok", 42])).toThrow("Expected string");
174
- });
172
+ it("혼합 배열 → 에러", () => {
173
+ expect(() => validator.parse(["ok", 42])).toThrow("Expected string");
174
+ });
175
175
  });
176
176
 
177
177
  // ─── v.object() ─────────────────────────────────────────
178
178
 
179
179
  describe("v.object()", () => {
180
- const validator = v.object({
181
- name: v.string(),
182
- age: v.number(),
183
- });
184
-
185
- it("유효한 객체 통과", () => {
186
- expect(validator.parse({ name: "Alice", age: 30 })).toEqual({ name: "Alice", age: 30 });
187
- });
188
-
189
- it("비객체 → 에러", () => {
190
- expect(() => validator.parse("string")).toThrow("Expected object");
191
- });
192
-
193
- it("null → 에러", () => {
194
- expect(() => validator.parse(null)).toThrow("Expected object");
195
- });
196
-
197
- it("필드 타입 불일치 → 에러", () => {
198
- expect(() => validator.parse({ name: 42, age: 30 })).toThrow("Expected string");
199
- });
200
-
201
- it("필드 누락 → inner validator 에러", () => {
202
- expect(() => validator.parse({ name: "Alice" })).toThrow("Expected number");
203
- });
180
+ const validator = v.object({
181
+ name: v.string(),
182
+ age: v.number(),
183
+ });
184
+
185
+ it("유효한 객체 통과", () => {
186
+ expect(validator.parse({ name: "Alice", age: 30 })).toEqual({ name: "Alice", age: 30 });
187
+ });
188
+
189
+ it("비객체 → 에러", () => {
190
+ expect(() => validator.parse("string")).toThrow("Expected object");
191
+ });
192
+
193
+ it("null → 에러", () => {
194
+ expect(() => validator.parse(null)).toThrow("Expected object");
195
+ });
196
+
197
+ it("필드 타입 불일치 → 에러", () => {
198
+ expect(() => validator.parse({ name: 42, age: 30 })).toThrow("Expected string");
199
+ });
200
+
201
+ it("필드 누락 → inner validator 에러", () => {
202
+ expect(() => validator.parse({ name: "Alice" })).toThrow("Expected number");
203
+ });
204
204
  });
205
205
 
206
206
  // ─── 복합 중첩 검증 ────────────────────────────────────
207
207
 
208
208
  describe("복합 중첩 검증", () => {
209
- it("v.optional(v.array(v.string()))", () => {
210
- const validator = v.optional(v.array(v.string()));
211
- expect(validator.parse(undefined)).toBe(undefined);
212
- expect(validator.parse(["a", "b"])).toEqual(["a", "b"]);
213
- expect(() => validator.parse([1])).toThrow("Expected string");
214
- });
215
-
216
- it("v.array(v.object(...))", () => {
217
- const validator = v.array(v.object({
218
- id: v.number(),
219
- label: v.string(),
220
- }));
221
- expect(validator.parse([
222
- { id: 1, label: "A" },
223
- { id: 2, label: "B" },
224
- ])).toEqual([
225
- { id: 1, label: "A" },
226
- { id: 2, label: "B" },
227
- ]);
228
- });
229
-
230
- it("v.object({ tags: v.array(v.string()), meta: v.optional(v.object(...)) })", () => {
231
- const validator = v.object({
232
- tags: v.array(v.string()),
233
- meta: v.optional(v.object({ key: v.string() })),
234
- });
235
- expect(validator.parse({ tags: ["a"], meta: undefined })).toEqual({ tags: ["a"], meta: undefined });
236
- expect(validator.parse({ tags: [], meta: { key: "val" } })).toEqual({ tags: [], meta: { key: "val" } });
209
+ it("v.optional(v.array(v.string()))", () => {
210
+ const validator = v.optional(v.array(v.string()));
211
+ expect(validator.parse(undefined)).toBe(undefined);
212
+ expect(validator.parse(["a", "b"])).toEqual(["a", "b"]);
213
+ expect(() => validator.parse([1])).toThrow("Expected string");
214
+ });
215
+
216
+ it("v.array(v.object(...))", () => {
217
+ const validator = v.array(
218
+ v.object({
219
+ id: v.number(),
220
+ label: v.string(),
221
+ }),
222
+ );
223
+ expect(
224
+ validator.parse([
225
+ { id: 1, label: "A" },
226
+ { id: 2, label: "B" },
227
+ ]),
228
+ ).toEqual([
229
+ { id: 1, label: "A" },
230
+ { id: 2, label: "B" },
231
+ ]);
232
+ });
233
+
234
+ it("v.object({ tags: v.array(v.string()), meta: v.optional(v.object(...)) })", () => {
235
+ const validator = v.object({
236
+ tags: v.array(v.string()),
237
+ meta: v.optional(v.object({ key: v.string() })),
237
238
  });
239
+ expect(validator.parse({ tags: ["a"], meta: undefined })).toEqual({ tags: ["a"], meta: undefined });
240
+ expect(validator.parse({ tags: [], meta: { key: "val" } })).toEqual({ tags: [], meta: { key: "val" } });
241
+ });
238
242
  });
239
243
 
240
244
  // ─── parseArgs() ────────────────────────────────────────
241
245
 
242
246
  describe("parseArgs()", () => {
243
- it("schema 없으면 args 그대로 반환", () => {
244
- expect(parseArgs(null, { foo: 1 })).toEqual({ foo: 1 });
245
- expect(parseArgs(undefined, "hello")).toBe("hello");
246
- });
247
-
248
- it("Validator 스키마로 직접 파싱", () => {
249
- const schema = v.string();
250
- expect(parseArgs(schema, "hello")).toBe("hello");
251
- });
252
-
253
- it("Validator 스키마 검증 실패 → GencowValidationError", () => {
254
- const schema = v.string();
255
- expect(() => parseArgs(schema, 42)).toThrow(GencowValidationError);
256
- });
257
-
258
- it("Shorthand record 스키마 (Convex 스타일)", () => {
259
- const schema = { title: v.string(), count: v.number() };
260
- expect(parseArgs(schema, { title: "Hello", count: 5 })).toEqual({ title: "Hello", count: 5 });
261
- });
262
-
263
- it("Shorthand record 검증 실패 → GencowValidationError + 필드명 포함", () => {
264
- const schema = { title: v.string(), count: v.number() };
265
- try {
266
- parseArgs(schema, { title: 42, count: 5 });
267
- expect(true).toBe(false); // 여기 도달하면 안 됨
268
- } catch (e) {
269
- expect(e).toBeInstanceOf(GencowValidationError);
270
- expect((e as Error).message).toContain("title");
271
- }
272
- });
273
-
274
- it("Shorthand record에 비객체 전달 → GencowValidationError", () => {
275
- const schema = { title: v.string() };
276
- expect(() => parseArgs(schema, "not an object")).toThrow(GencowValidationError);
277
- });
278
-
279
- it("GencowValidationError.statusCode === 400", () => {
280
- const err = new GencowValidationError("test");
281
- expect(err.statusCode).toBe(400);
282
- expect(err.name).toBe("GencowValidationError");
283
- });
284
-
285
- // ─── 빈 스키마 passthrough (FormData 업로드 버그 회귀 방지) ────
286
-
287
- it("빈 스키마 {} → args 전체 passthrough (FormData file 필드 보존)", () => {
288
- const schema = {};
289
- const args = { file: new File(["hello"], "test.txt"), _mutation: "upload.store" };
290
- const result = parseArgs(schema, args);
291
- // 빈 스키마이므로 args가 그대로 반환되어야 함 (file 포함)
292
- expect(result).toBe(args); // 참조 동일
293
- expect(result.file).toBeInstanceOf(File);
294
- expect(result._mutation).toBe("upload.store");
295
- });
296
-
297
- it("빈 스키마 {} + 일반 객체 → passthrough", () => {
298
- const schema = {};
299
- const args = { name: "test", count: 42, nested: { a: 1 } };
300
- const result = parseArgs(schema, args);
301
- expect(result).toBe(args);
302
- expect(result.name).toBe("test");
303
- expect(result.count).toBe(42);
304
- });
305
-
306
- it("빈 스키마 {} + 빈 args {} → 빈 객체 반환", () => {
307
- const result = parseArgs({}, {});
308
- expect(result).toEqual({});
309
- });
310
-
311
- it("키가 있는 스키마는 여전히 지정된 키만 추출 (file 제거됨)", () => {
312
- const schema = { title: v.string() };
313
- const args = { title: "hello", file: "should-be-stripped", extra: 123 };
314
- const result = parseArgs(schema, args);
315
- expect(result).toEqual({ title: "hello" });
316
- expect(result.file).toBeUndefined();
317
- expect(result.extra).toBeUndefined();
318
- });
247
+ it("schema 없으면 args 그대로 반환", () => {
248
+ expect(parseArgs(null, { foo: 1 })).toEqual({ foo: 1 });
249
+ expect(parseArgs(undefined, "hello")).toBe("hello");
250
+ });
251
+
252
+ it("Validator 스키마로 직접 파싱", () => {
253
+ const schema = v.string();
254
+ expect(parseArgs(schema, "hello")).toBe("hello");
255
+ });
256
+
257
+ it("Validator 스키마 검증 실패 → GencowValidationError", () => {
258
+ const schema = v.string();
259
+ expect(() => parseArgs(schema, 42)).toThrow(GencowValidationError);
260
+ });
261
+
262
+ it("Shorthand record 스키마 (Convex 스타일)", () => {
263
+ const schema = { title: v.string(), count: v.number() };
264
+ expect(parseArgs(schema, { title: "Hello", count: 5 })).toEqual({ title: "Hello", count: 5 });
265
+ });
266
+
267
+ it("Shorthand record 검증 실패 → GencowValidationError + 필드명 포함", () => {
268
+ const schema = { title: v.string(), count: v.number() };
269
+ try {
270
+ parseArgs(schema, { title: 42, count: 5 });
271
+ expect(true).toBe(false); // 여기 도달하면 안 됨
272
+ } catch (e) {
273
+ expect(e).toBeInstanceOf(GencowValidationError);
274
+ expect((e as Error).message).toContain("title");
275
+ }
276
+ });
277
+
278
+ it("Shorthand record에 비객체 전달 → GencowValidationError", () => {
279
+ const schema = { title: v.string() };
280
+ expect(() => parseArgs(schema, "not an object")).toThrow(GencowValidationError);
281
+ });
282
+
283
+ it("GencowValidationError.statusCode === 400", () => {
284
+ const err = new GencowValidationError("test");
285
+ expect(err.statusCode).toBe(400);
286
+ expect(err.name).toBe("GencowValidationError");
287
+ });
288
+
289
+ // ─── 빈 스키마 passthrough (FormData 업로드 버그 회귀 방지) ────
290
+
291
+ it("빈 스키마 {} → args 전체 passthrough (FormData file 필드 보존)", () => {
292
+ const schema = {};
293
+ const args = { file: new File(["hello"], "test.txt"), _mutation: "upload.store" };
294
+ const result = parseArgs(schema, args);
295
+ // 빈 스키마이므로 args가 그대로 반환되어야 함 (file 포함)
296
+ expect(result).toBe(args); // 참조 동일
297
+ expect(result.file).toBeInstanceOf(File);
298
+ expect(result._mutation).toBe("upload.store");
299
+ });
300
+
301
+ it("빈 스키마 {} + 일반 객체 → passthrough", () => {
302
+ const schema = {};
303
+ const args = { name: "test", count: 42, nested: { a: 1 } };
304
+ const result = parseArgs(schema, args);
305
+ expect(result).toBe(args);
306
+ expect(result.name).toBe("test");
307
+ expect(result.count).toBe(42);
308
+ });
309
+
310
+ it("빈 스키마 {} + 빈 args {} → 빈 객체 반환", () => {
311
+ const result = parseArgs({}, {});
312
+ expect(result).toEqual({});
313
+ });
314
+
315
+ it("키가 있는 스키마는 여전히 지정된 키만 추출 (file 제거됨)", () => {
316
+ const schema = { title: v.string() };
317
+ const args = { title: "hello", file: "should-be-stripped", extra: 123 };
318
+ const result = parseArgs(schema, args);
319
+ expect(result).toEqual({ title: "hello" });
320
+ expect(result.file).toBeUndefined();
321
+ expect(result.extra).toBeUndefined();
322
+ });
319
323
  });