@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
@@ -8,314 +8,327 @@
8
8
  */
9
9
 
10
10
  import { describe, it, expect, beforeEach, afterEach } from "bun:test";
11
- import { createStorage } from "../storage";
11
+ import { createStorage } from "../storage.js";
12
12
  import * as fs from "fs/promises";
13
13
  import * as path from "path";
14
14
  import * as os from "os";
15
15
 
16
16
  describe("createStorage()", () => {
17
- let tmpDir: string;
17
+ let tmpDir: string;
18
18
 
19
- beforeEach(async () => {
20
- tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "gencow-storage-test-"));
21
- });
19
+ beforeEach(async () => {
20
+ tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "gencow-storage-test-"));
21
+ });
22
22
 
23
- afterEach(async () => {
24
- await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
25
- });
23
+ afterEach(async () => {
24
+ await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
25
+ });
26
26
 
27
- // ─── store + getUrl ─────────────────────────────────
28
-
29
- describe("store()", () => {
30
- it("File 객체를 저장하고 storageId를 반환한다", async () => {
31
- const storage = createStorage(tmpDir);
32
- const file = new File(["hello world"], "test.txt", { type: "text/plain" });
33
-
34
- const id = await storage.store(file);
35
- expect(typeof id).toBe("string");
36
- expect(id.length).toBeGreaterThan(0);
37
- });
38
-
39
- it("저장된 파일이 디스크에 존재한다", async () => {
40
- const storage = createStorage(tmpDir);
41
- const file = new File(["content"], "doc.txt", { type: "text/plain" });
42
-
43
- const id = await storage.store(file);
44
- const filePath = path.join(tmpDir, id);
45
- const stat = await fs.stat(filePath);
46
- expect(stat.isFile()).toBe(true);
47
- });
48
-
49
- it("Blob 객체도 저장 가능하다", async () => {
50
- const storage = createStorage(tmpDir);
51
- const blob = new Blob(["blob content"], { type: "application/octet-stream" });
52
-
53
- const id = await storage.store(blob);
54
- expect(typeof id).toBe("string");
55
- });
56
-
57
- it("50MB 초과 파일 → 에러", async () => {
58
- const storage = createStorage(tmpDir);
59
- // 51MB File mock — File constructor에서 size를 직접 제어
60
- const bigContent = new Uint8Array(51 * 1024 * 1024);
61
- const file = new File([bigContent], "big.bin");
62
-
63
- await expect(storage.store(file)).rejects.toThrow("File too large");
64
- });
65
-
66
- it("커스텀 filename 적용", async () => {
67
- const storage = createStorage(tmpDir);
68
- const file = new File(["data"], "original.txt");
69
-
70
- const id = await storage.store(file, "custom-name.txt");
71
- const meta = await storage.getMeta(id);
72
- expect(meta?.name).toBe("custom-name.txt");
73
- });
74
- });
27
+ // ─── store + getUrl ─────────────────────────────────
75
28
 
76
- // ─── storeBuffer ────────────────────────────────────
29
+ describe("store()", () => {
30
+ it("File 객체를 저장하고 storageId를 반환한다", async () => {
31
+ const storage = createStorage(tmpDir);
32
+ const file = new File(["hello world"], "test.txt", { type: "text/plain" });
77
33
 
78
- describe("storeBuffer()", () => {
79
- it("Buffer를 저장하고 storageId를 반환한다", async () => {
80
- const storage = createStorage(tmpDir);
81
- const buffer = Buffer.from("buffer content");
34
+ const id = await storage.store(file);
35
+ expect(typeof id).toBe("string");
36
+ expect(id.length).toBeGreaterThan(0);
37
+ });
82
38
 
83
- const id = await storage.storeBuffer(buffer, "buffer.txt", "text/plain");
84
- expect(typeof id).toBe("string");
85
- });
39
+ it("저장된 파일이 디스크에 존재한다", async () => {
40
+ const storage = createStorage(tmpDir);
41
+ const file = new File(["content"], "doc.txt", { type: "text/plain" });
86
42
 
87
- it("저장된 파일의 메타데이터가 올바르다", async () => {
88
- const storage = createStorage(tmpDir);
89
- const buffer = Buffer.from("meta test");
43
+ const id = await storage.store(file);
44
+ const filePath = path.join(tmpDir, id);
45
+ const stat = await fs.stat(filePath);
46
+ expect(stat.isFile()).toBe(true);
47
+ });
48
+
49
+ it("Blob 객체도 저장 가능하다", async () => {
50
+ const storage = createStorage(tmpDir);
51
+ const blob = new Blob(["blob content"], { type: "application/octet-stream" });
90
52
 
91
- const id = await storage.storeBuffer(buffer, "meta.txt", "text/plain");
92
- const meta = await storage.getMeta(id);
93
- expect(meta).not.toBeNull();
94
- expect(meta!.name).toBe("meta.txt");
95
- expect(meta!.type).toBe("text/plain");
96
- expect(meta!.size).toBe(buffer.length);
97
- });
53
+ const id = await storage.store(blob);
54
+ expect(typeof id).toBe("string");
98
55
  });
99
56
 
100
- // ─── getUrl ─────────────────────────────────────────
101
-
102
- describe("getUrl()", () => {
103
- it("storageId로 URL 반환", () => {
104
- const storage = createStorage(tmpDir);
105
- const url = storage.getUrl("some-uuid-123");
106
- expect(url).toBe("/api/storage/some-uuid-123");
107
- });
108
-
109
- it("URL 패턴이 /api/storage/{id} 형식이다", async () => {
110
- const storage = createStorage(tmpDir);
111
- const file = new File(["test"], "test.txt");
112
- const id = await storage.store(file);
113
- const url = storage.getUrl(id);
114
- expect(url).toMatch(/^\/api\/storage\/.+$/);
115
- });
57
+ it("50MB 초과 파일 → 에러", async () => {
58
+ const storage = createStorage(tmpDir);
59
+ // 51MB File mock — File constructor에서 size를 직접 제어
60
+ const bigContent = new Uint8Array(51 * 1024 * 1024);
61
+ const file = new File([bigContent], "big.bin");
62
+
63
+ await expect(storage.store(file)).rejects.toThrow("File too large");
116
64
  });
117
65
 
118
- // ─── getMeta ────────────────────────────────────────
119
-
120
- describe("getMeta()", () => {
121
- it("저장된 파일의 메타데이터를 반환한다", async () => {
122
- const storage = createStorage(tmpDir);
123
- const file = new File(["hello"], "hello.txt", { type: "text/plain" });
124
- const id = await storage.store(file);
125
-
126
- const meta = await storage.getMeta(id);
127
- expect(meta).not.toBeNull();
128
- expect(meta!.id).toBe(id);
129
- expect(meta!.name).toBe("hello.txt");
130
- expect(meta!.type).toContain("text/plain");
131
- });
132
-
133
- it("존재하지 않는 storageId → null 반환", async () => {
134
- const storage = createStorage(tmpDir);
135
- const meta = await storage.getMeta("nonexistent-id");
136
- expect(meta).toBeNull();
137
- });
66
+ it("커스텀 filename 적용", async () => {
67
+ const storage = createStorage(tmpDir);
68
+ const file = new File(["data"], "original.txt");
69
+
70
+ const id = await storage.store(file, "custom-name.txt");
71
+ const meta = await storage.getMeta(id);
72
+ expect(meta?.name).toBe("custom-name.txt");
138
73
  });
74
+ });
139
75
 
140
- // ─── delete ─────────────────────────────────────────
76
+ // ─── storeBuffer ────────────────────────────────────
141
77
 
142
- describe("delete()", () => {
143
- it("파일을 삭제한다", async () => {
144
- const storage = createStorage(tmpDir);
145
- const file = new File(["to delete"], "delete-me.txt");
146
- const id = await storage.store(file);
78
+ describe("storeBuffer()", () => {
79
+ it("Buffer를 저장하고 storageId를 반환한다", async () => {
80
+ const storage = createStorage(tmpDir);
81
+ const buffer = Buffer.from("buffer content");
147
82
 
148
- // 파일 존재 확인
149
- const filePath = path.join(tmpDir, id);
150
- const statBefore = await fs.stat(filePath);
151
- expect(statBefore.isFile()).toBe(true);
83
+ const id = await storage.storeBuffer(buffer, "buffer.txt", "text/plain");
84
+ expect(typeof id).toBe("string");
85
+ });
152
86
 
153
- // 삭제
154
- await storage.delete(id);
87
+ it("저장된 파일의 메타데이터가 올바르다", async () => {
88
+ const storage = createStorage(tmpDir);
89
+ const buffer = Buffer.from("meta test");
155
90
 
156
- // 파일 삭제 확인
157
- const exists = await fs.access(filePath).then(() => true).catch(() => false);
158
- expect(exists).toBe(false);
91
+ const id = await storage.storeBuffer(buffer, "meta.txt", "text/plain");
92
+ const meta = await storage.getMeta(id);
93
+ expect(meta).not.toBeNull();
94
+ expect(meta!.name).toBe("meta.txt");
95
+ expect(meta!.type).toBe("text/plain");
96
+ expect(meta!.size).toBe(buffer.length);
97
+ });
98
+ });
159
99
 
160
- // 메타데이터 삭제 확인
161
- const meta = await storage.getMeta(id);
162
- expect(meta).toBeNull();
163
- });
100
+ // ─── getUrl ─────────────────────────────────────────
164
101
 
165
- it("존재하지 않는 storageId 삭제 시 에러 없음", async () => {
166
- const storage = createStorage(tmpDir);
167
- await expect(storage.delete("nonexistent-id")).resolves.toBeUndefined();
168
- });
102
+ describe("getUrl()", () => {
103
+ it("storageId로 URL 반환", () => {
104
+ const storage = createStorage(tmpDir);
105
+ const url = storage.getUrl("some-uuid-123");
106
+ expect(url).toBe("/api/storage/some-uuid-123");
169
107
  });
170
108
 
171
- // ─── Storage Quota ──────────────────────────────────
172
-
173
- describe("스토리지 쿼터", () => {
174
- it("쿼터 초과 에러 (rawSql 있을 때)", async () => {
175
- const mockRawSql = async (sql: string, _params?: unknown[]) => {
176
- if (sql.includes("information_schema")) return []; // ensureFilesTable
177
- if (sql.includes("SUM")) return [{ total: "999999999" }]; // 쿼터에 근접
178
- return [];
179
- };
180
-
181
- const storage = createStorage(tmpDir, {
182
- rawSql: mockRawSql,
183
- storageQuota: 1000000000, // 1GB
184
- });
185
-
186
- const file = new File(["x".repeat(1024)], "test.txt");
187
- await expect(storage.store(file)).rejects.toThrow("Storage quota exceeded");
188
- });
189
-
190
- it("쿼터 0 = 무제한 (에러 없음)", async () => {
191
- const mockRawSql = async (sql: string, _params?: unknown[]) => {
192
- if (sql.includes("information_schema")) return [];
193
- if (sql.includes("SUM")) return [{ total: "999999999999" }];
194
- if (sql.includes("INSERT")) return [];
195
- return [];
196
- };
197
-
198
- const storage = createStorage(tmpDir, {
199
- rawSql: mockRawSql,
200
- storageQuota: 0, // 무제한
201
- });
202
-
203
- const file = new File(["small"], "small.txt");
204
- const id = await storage.store(file);
205
- expect(typeof id).toBe("string");
206
- });
109
+ it("URL 패턴이 /api/storage/{id} 형식이다", async () => {
110
+ const storage = createStorage(tmpDir);
111
+ const file = new File(["test"], "test.txt");
112
+ const id = await storage.store(file);
113
+ const url = storage.getUrl(id);
114
+ expect(url).toMatch(/^\/api\/storage\/.+$/);
207
115
  });
208
- });
116
+ });
209
117
 
210
- // ─── _system_files 테이블 리네이밍 관련 테스트 ────────────
211
- // 2026-04-10 WSOD 사고 후 추가: files → _system_files 리네이밍 검증
212
- // 📄 참고: docs/analysis/analysis-files-page-wsod.md
118
+ // ─── getMeta ────────────────────────────────────────
213
119
 
214
- describe("_system_files 시스템 테이블 네이밍", () => {
215
- it("rawSql로 실행되는 모든 SQL에 old 'files' 테이블 참조가 없다", async () => {
216
- const executedSql: string[] = [];
217
- const mockRawSql = async (sql: string) => {
218
- executedSql.push(sql);
219
- if (sql.includes("SUM")) return [{ total: "0" }];
220
- if (sql.includes("INSERT")) return [];
221
- return [];
222
- };
223
-
224
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "gencow-naming-"));
225
- const storage = createStorage(tmpDir, { rawSql: mockRawSql });
226
-
227
- // store 트리거 (ensureFilesTable + checkQuota + recordFileToDb)
228
- const file = new File(["test"], "test.txt", { type: "text/plain" });
229
- try { await storage.store(file); } catch {}
230
-
231
- // 모든 실행된 SQL에서 old 테이블명 'files'가 아닌 '_system_files'만 참조하는지 확인
232
- // (files 단독 참조를 찾되, _system_files는 통과)
233
- for (const sql of executedSql) {
234
- // "FROM files", "INTO files", "TABLE files" 같은 old 패턴이 없어야 함
235
- expect(sql).not.toMatch(/\bFROM\s+files\b(?!_)/i);
236
- expect(sql).not.toMatch(/\bINTO\s+files\b(?!_)/i);
237
- expect(sql).not.toMatch(/\bTABLE\s+files\b(?!_)/i);
238
- }
239
-
240
- await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
120
+ describe("getMeta()", () => {
121
+ it("저장된 파일의 메타데이터를 반환한다", async () => {
122
+ const storage = createStorage(tmpDir);
123
+ const file = new File(["hello"], "hello.txt", { type: "text/plain" });
124
+ const id = await storage.store(file);
125
+
126
+ const meta = await storage.getMeta(id);
127
+ expect(meta).not.toBeNull();
128
+ expect(meta!.id).toBe(id);
129
+ expect(meta!.name).toBe("hello.txt");
130
+ expect(meta!.type).toContain("text/plain");
241
131
  });
242
132
 
243
- it("쿼터 검증 SQL이 _system_files 테이블을 참조한다", async () => {
244
- const executedSql: string[] = [];
245
- const mockRawSql = async (sql: string) => {
246
- executedSql.push(sql);
247
- if (sql.includes("SUM")) return [{ total: "0" }];
248
- if (sql.includes("INSERT")) return [];
249
- return [];
250
- };
251
-
252
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "gencow-quota-"));
253
- const storage = createStorage(tmpDir, {
254
- rawSql: mockRawSql,
255
- storageQuota: 1000000000,
256
- });
257
-
258
- const file = new File(["small"], "small.txt");
259
- try { await storage.store(file); } catch {}
260
-
261
- // SUM 쿼리가 _system_files 테이블을 참조하는지 확인
262
- const sumSql = executedSql.find(s => s.includes("SUM"));
263
- if (sumSql) {
264
- expect(sumSql).toContain("_system_files");
265
- expect(sumSql).not.toMatch(/FROM\s+files[^_]/);
266
- }
267
-
268
- await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
133
+ it("존재하지 않는 storageId null 반환", async () => {
134
+ const storage = createStorage(tmpDir);
135
+ const meta = await storage.getMeta("nonexistent-id");
136
+ expect(meta).toBeNull();
137
+ });
138
+ });
139
+
140
+ // ─── delete ─────────────────────────────────────────
141
+
142
+ describe("delete()", () => {
143
+ it("파일을 삭제한다", async () => {
144
+ const storage = createStorage(tmpDir);
145
+ const file = new File(["to delete"], "delete-me.txt");
146
+ const id = await storage.store(file);
147
+
148
+ // 파일 존재 확인
149
+ const filePath = path.join(tmpDir, id);
150
+ const statBefore = await fs.stat(filePath);
151
+ expect(statBefore.isFile()).toBe(true);
152
+
153
+ // 삭제
154
+ await storage.delete(id);
155
+
156
+ // 파일 삭제 확인
157
+ const exists = await fs
158
+ .access(filePath)
159
+ .then(() => true)
160
+ .catch(() => false);
161
+ expect(exists).toBe(false);
162
+
163
+ // 메타데이터 삭제 확인
164
+ const meta = await storage.getMeta(id);
165
+ expect(meta).toBeNull();
269
166
  });
270
167
 
271
- it("recordFileToDb SQL이 _system_files 테이블에 INSERT한다", async () => {
272
- const executedSql: string[] = [];
273
- const mockRawSql = async (sql: string) => {
274
- executedSql.push(sql);
275
- if (sql.includes("SUM")) return [{ total: "0" }];
276
- return [];
277
- };
168
+ it("존재하지 않는 storageId 삭제 시 에러 없음", async () => {
169
+ const storage = createStorage(tmpDir);
170
+ await expect(storage.delete("nonexistent-id")).resolves.toBeUndefined();
171
+ });
172
+ });
278
173
 
279
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "gencow-insert-"));
280
- const storage = createStorage(tmpDir, { rawSql: mockRawSql });
174
+ // ─── Storage Quota ──────────────────────────────────
281
175
 
282
- const file = new File(["data"], "data.txt", { type: "text/plain" });
283
- try { await storage.store(file); } catch {}
176
+ describe("스토리지 쿼터", () => {
177
+ it("쿼터 초과 에러 (rawSql 있을 때)", async () => {
178
+ const mockRawSql = async (sql: string, _params?: unknown[]) => {
179
+ if (sql.includes("information_schema")) return []; // ensureFilesTable
180
+ if (sql.includes("SUM")) return [{ total: "999999999" }]; // 쿼터에 근접
181
+ return [];
182
+ };
284
183
 
285
- const insertSql = executedSql.find(s => s.includes("INSERT"));
286
- if (insertSql) {
287
- expect(insertSql).toContain("_system_files");
288
- expect(insertSql).not.toMatch(/INTO\s+files[^_]/);
289
- }
184
+ const storage = createStorage(tmpDir, {
185
+ rawSql: mockRawSql,
186
+ storageQuota: 1000000000, // 1GB
187
+ });
290
188
 
291
- await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
189
+ const file = new File(["x".repeat(1024)], "test.txt");
190
+ await expect(storage.store(file)).rejects.toThrow("Storage quota exceeded");
292
191
  });
293
192
 
294
- it("delete SQL이 _system_files 테이블에서 삭제한다", async () => {
295
- const executedSql: string[] = [];
296
- const mockRawSql = async (sql: string) => {
297
- executedSql.push(sql);
298
- if (sql.includes("SUM")) return [{ total: "0" }];
299
- return [];
300
- };
301
-
302
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "gencow-delete-"));
303
- const storage = createStorage(tmpDir, { rawSql: mockRawSql });
304
-
305
- // store 후 delete
306
- const file = new File(["delete-me"], "del.txt");
307
- let storageId: string | undefined;
308
- try { storageId = await storage.store(file); } catch {}
309
- if (storageId) {
310
- try { await storage.delete(storageId); } catch {}
311
- }
312
-
313
- const deleteSql = executedSql.find(s => s.includes("DELETE"));
314
- if (deleteSql) {
315
- expect(deleteSql).toContain("_system_files");
316
- expect(deleteSql).not.toMatch(/FROM\s+files[^_]/);
317
- }
318
-
319
- await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
193
+ it("쿼터 0 = 무제한 (에러 없음)", async () => {
194
+ const mockRawSql = async (sql: string, _params?: unknown[]) => {
195
+ if (sql.includes("information_schema")) return [];
196
+ if (sql.includes("SUM")) return [{ total: "999999999999" }];
197
+ if (sql.includes("INSERT")) return [];
198
+ return [];
199
+ };
200
+
201
+ const storage = createStorage(tmpDir, {
202
+ rawSql: mockRawSql,
203
+ storageQuota: 0, // 무제한
204
+ });
205
+
206
+ const file = new File(["small"], "small.txt");
207
+ const id = await storage.store(file);
208
+ expect(typeof id).toBe("string");
320
209
  });
210
+ });
211
+ });
212
+
213
+ // ─── _system_files 테이블 리네이밍 관련 테스트 ────────────
214
+ // 2026-04-10 WSOD 사고 후 추가: files → _system_files 리네이밍 검증
215
+ // 📄 참고: docs/analysis/analysis-files-page-wsod.md
216
+
217
+ describe("_system_files 시스템 테이블 네이밍", () => {
218
+ it("rawSql로 실행되는 모든 SQL에 old 'files' 테이블 참조가 없다", async () => {
219
+ const executedSql: string[] = [];
220
+ const mockRawSql = async (sql: string) => {
221
+ executedSql.push(sql);
222
+ if (sql.includes("SUM")) return [{ total: "0" }];
223
+ if (sql.includes("INSERT")) return [];
224
+ return [];
225
+ };
226
+
227
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "gencow-naming-"));
228
+ const storage = createStorage(tmpDir, { rawSql: mockRawSql });
229
+
230
+ // store 트리거 (ensureFilesTable + checkQuota + recordFileToDb)
231
+ const file = new File(["test"], "test.txt", { type: "text/plain" });
232
+ try {
233
+ await storage.store(file);
234
+ } catch {}
235
+
236
+ // 모든 실행된 SQL에서 old 테이블명 'files'가 아닌 '_system_files'만 참조하는지 확인
237
+ // (files 단독 참조를 찾되, _system_files는 통과)
238
+ for (const sql of executedSql) {
239
+ // "FROM files", "INTO files", "TABLE files" 같은 old 패턴이 없어야 함
240
+ expect(sql).not.toMatch(/\bFROM\s+files\b(?!_)/i);
241
+ expect(sql).not.toMatch(/\bINTO\s+files\b(?!_)/i);
242
+ expect(sql).not.toMatch(/\bTABLE\s+files\b(?!_)/i);
243
+ }
244
+
245
+ await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
246
+ });
247
+
248
+ it("쿼터 검증 SQL이 _system_files 테이블을 참조한다", async () => {
249
+ const executedSql: string[] = [];
250
+ const mockRawSql = async (sql: string) => {
251
+ executedSql.push(sql);
252
+ if (sql.includes("SUM")) return [{ total: "0" }];
253
+ if (sql.includes("INSERT")) return [];
254
+ return [];
255
+ };
256
+
257
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "gencow-quota-"));
258
+ const storage = createStorage(tmpDir, {
259
+ rawSql: mockRawSql,
260
+ storageQuota: 1000000000,
261
+ });
262
+
263
+ const file = new File(["small"], "small.txt");
264
+ try {
265
+ await storage.store(file);
266
+ } catch {}
267
+
268
+ // SUM 쿼리가 _system_files 테이블을 참조하는지 확인
269
+ const sumSql = executedSql.find((s) => s.includes("SUM"));
270
+ if (sumSql) {
271
+ expect(sumSql).toContain("_system_files");
272
+ expect(sumSql).not.toMatch(/FROM\s+files[^_]/);
273
+ }
274
+
275
+ await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
276
+ });
277
+
278
+ it("recordFileToDb SQL이 _system_files 테이블에 INSERT한다", async () => {
279
+ const executedSql: string[] = [];
280
+ const mockRawSql = async (sql: string) => {
281
+ executedSql.push(sql);
282
+ if (sql.includes("SUM")) return [{ total: "0" }];
283
+ return [];
284
+ };
285
+
286
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "gencow-insert-"));
287
+ const storage = createStorage(tmpDir, { rawSql: mockRawSql });
288
+
289
+ const file = new File(["data"], "data.txt", { type: "text/plain" });
290
+ try {
291
+ await storage.store(file);
292
+ } catch {}
293
+
294
+ const insertSql = executedSql.find((s) => s.includes("INSERT"));
295
+ if (insertSql) {
296
+ expect(insertSql).toContain("_system_files");
297
+ expect(insertSql).not.toMatch(/INTO\s+files[^_]/);
298
+ }
299
+
300
+ await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
301
+ });
302
+
303
+ it("delete SQL이 _system_files 테이블에서 삭제한다", async () => {
304
+ const executedSql: string[] = [];
305
+ const mockRawSql = async (sql: string) => {
306
+ executedSql.push(sql);
307
+ if (sql.includes("SUM")) return [{ total: "0" }];
308
+ return [];
309
+ };
310
+
311
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "gencow-delete-"));
312
+ const storage = createStorage(tmpDir, { rawSql: mockRawSql });
313
+
314
+ // store 후 delete
315
+ const file = new File(["delete-me"], "del.txt");
316
+ let storageId: string | undefined;
317
+ try {
318
+ storageId = await storage.store(file);
319
+ } catch {}
320
+ if (storageId) {
321
+ try {
322
+ await storage.delete(storageId);
323
+ } catch {}
324
+ }
325
+
326
+ const deleteSql = executedSql.find((s) => s.includes("DELETE"));
327
+ if (deleteSql) {
328
+ expect(deleteSql).toContain("_system_files");
329
+ expect(deleteSql).not.toMatch(/FROM\s+files[^_]/);
330
+ }
331
+
332
+ await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
333
+ });
321
334
  });
@@ -1,8 +1,8 @@
1
1
  {
2
- "extends": "../../tsconfig.json",
3
- "compilerOptions": {
4
- "noEmit": true
5
- },
6
- "include": ["./**/*.ts"],
7
- "exclude": []
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "noEmit": true
5
+ },
6
+ "include": ["./**/*.ts"],
7
+ "exclude": []
8
8
  }