@gencow/core 0.1.24 → 0.1.26

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 (75) hide show
  1. package/dist/crud.d.ts +2 -2
  2. package/dist/crud.js +225 -208
  3. package/dist/index.d.ts +5 -5
  4. package/dist/index.js +2 -2
  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.js +4 -11
  16. package/dist/workflows-api.js +5 -12
  17. package/package.json +45 -42
  18. package/src/__tests__/auth.test.ts +90 -86
  19. package/src/__tests__/crons.test.ts +69 -67
  20. package/src/__tests__/crud-codegen-integration.test.ts +164 -170
  21. package/src/__tests__/crud-owner-rls.test.ts +308 -301
  22. package/src/__tests__/crud.test.ts +694 -711
  23. package/src/__tests__/dist-exports.test.ts +120 -120
  24. package/src/__tests__/fixtures/basic/auth.ts +16 -16
  25. package/src/__tests__/fixtures/basic/drizzle.config.ts +1 -4
  26. package/src/__tests__/fixtures/basic/index.ts +1 -1
  27. package/src/__tests__/fixtures/basic/schema.ts +1 -1
  28. package/src/__tests__/fixtures/basic/tasks.ts +4 -4
  29. package/src/__tests__/fixtures/common/auth-schema.ts +38 -34
  30. package/src/__tests__/helpers/basic-rls-fixture.ts +80 -78
  31. package/src/__tests__/helpers/pglite-migrations.ts +2 -5
  32. package/src/__tests__/helpers/pglite-rls-session.ts +13 -16
  33. package/src/__tests__/helpers/seed-like-fill.ts +47 -41
  34. package/src/__tests__/helpers/test-gencow-ctx-rls.ts +4 -7
  35. package/src/__tests__/httpaction.test.ts +91 -91
  36. package/src/__tests__/image-optimization.test.ts +570 -574
  37. package/src/__tests__/load.test.ts +321 -308
  38. package/src/__tests__/network-sim.test.ts +238 -215
  39. package/src/__tests__/reactive.test.ts +380 -358
  40. package/src/__tests__/retry.test.ts +99 -84
  41. package/src/__tests__/rls-crud-basic.test.ts +172 -245
  42. package/src/__tests__/rls-crud-no-owner-rls-pglite.test.ts +81 -81
  43. package/src/__tests__/rls-custom-mutation-handlers.test.ts +47 -94
  44. package/src/__tests__/rls-custom-query-handlers.test.ts +92 -92
  45. package/src/__tests__/rls-db-leased-connection.test.ts +2 -6
  46. package/src/__tests__/rls-session-and-policies.test.ts +181 -199
  47. package/src/__tests__/scheduler-durable-v2.test.ts +199 -181
  48. package/src/__tests__/scheduler-durable.test.ts +117 -117
  49. package/src/__tests__/scheduler-exec.test.ts +258 -246
  50. package/src/__tests__/scheduler.test.ts +129 -111
  51. package/src/__tests__/storage.test.ts +282 -269
  52. package/src/__tests__/tsconfig.json +6 -6
  53. package/src/__tests__/validator.test.ts +236 -232
  54. package/src/__tests__/workflow.test.ts +309 -286
  55. package/src/__tests__/ws-integration.test.ts +223 -218
  56. package/src/__tests__/ws-scale.test.ts +168 -159
  57. package/src/auth-config.ts +18 -18
  58. package/src/auth.ts +106 -106
  59. package/src/crons.ts +77 -77
  60. package/src/crud.ts +523 -479
  61. package/src/index.ts +69 -5
  62. package/src/reactive.ts +357 -331
  63. package/src/retry.ts +51 -54
  64. package/src/rls-db.ts +195 -205
  65. package/src/rls.ts +33 -36
  66. package/src/scheduler.ts +237 -211
  67. package/src/server.ts +0 -1
  68. package/src/storage.ts +632 -593
  69. package/src/v.ts +119 -114
  70. package/src/workflow-types.ts +67 -70
  71. package/src/workflow.ts +99 -116
  72. package/src/workflows-api.ts +231 -241
  73. package/dist/db.d.ts +0 -13
  74. package/dist/db.js +0 -16
  75. 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
  }