@edge-base/server 0.1.1

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 (309) hide show
  1. package/admin-build/.gitkeep +0 -0
  2. package/admin-build/_app/env.js +1 -0
  3. package/admin-build/_app/immutable/assets/0.Bm6cF078.css +1 -0
  4. package/admin-build/_app/immutable/assets/1.BfW3pUNa.css +1 -0
  5. package/admin-build/_app/immutable/assets/11.CVmQOewb.css +1 -0
  6. package/admin-build/_app/immutable/assets/12.B1EhbRZT.css +1 -0
  7. package/admin-build/_app/immutable/assets/13.BvwYeuwE.css +1 -0
  8. package/admin-build/_app/immutable/assets/14.CdVfcO0R.css +1 -0
  9. package/admin-build/_app/immutable/assets/15.2yeZ66b-.css +1 -0
  10. package/admin-build/_app/immutable/assets/17.BVg0JEVu.css +1 -0
  11. package/admin-build/_app/immutable/assets/18.Rwnl3x_i.css +1 -0
  12. package/admin-build/_app/immutable/assets/20.DsPWA9AV.css +1 -0
  13. package/admin-build/_app/immutable/assets/21.Dz2RJ56c.css +1 -0
  14. package/admin-build/_app/immutable/assets/22.DwNLk5Ai.css +1 -0
  15. package/admin-build/_app/immutable/assets/23.CFpu0gOO.css +1 -0
  16. package/admin-build/_app/immutable/assets/24.Cy5LBeoJ.css +1 -0
  17. package/admin-build/_app/immutable/assets/25.pUyLVf-h.css +1 -0
  18. package/admin-build/_app/immutable/assets/26.DBcGrlXa.css +1 -0
  19. package/admin-build/_app/immutable/assets/27.BswYyAJD.css +1 -0
  20. package/admin-build/_app/immutable/assets/28.B4ueB1Kf.css +1 -0
  21. package/admin-build/_app/immutable/assets/29.B-qU6PdF.css +1 -0
  22. package/admin-build/_app/immutable/assets/3.Dg81Pgmd.css +1 -0
  23. package/admin-build/_app/immutable/assets/30.CsdWum94.css +1 -0
  24. package/admin-build/_app/immutable/assets/31.U6OwIp50.css +1 -0
  25. package/admin-build/_app/immutable/assets/4.CyawCCux.css +1 -0
  26. package/admin-build/_app/immutable/assets/5.C0YO2HTk.css +1 -0
  27. package/admin-build/_app/immutable/assets/8.Br5jd6kD.css +1 -0
  28. package/admin-build/_app/immutable/assets/Badge.EMYLHBxE.css +1 -0
  29. package/admin-build/_app/immutable/assets/Button.DpzMRTjK.css +1 -0
  30. package/admin-build/_app/immutable/assets/ConfirmDialog.DAnaWRRk.css +1 -0
  31. package/admin-build/_app/immutable/assets/EmptyState.CwKsu57Y.css +1 -0
  32. package/admin-build/_app/immutable/assets/Input.BDUSenmU.css +1 -0
  33. package/admin-build/_app/immutable/assets/Modal.Dm5B0Xie.css +1 -0
  34. package/admin-build/_app/immutable/assets/PageShell.CmU-Xh-b.css +1 -0
  35. package/admin-build/_app/immutable/assets/SchemaFieldEditor.g4NsCdno.css +1 -0
  36. package/admin-build/_app/immutable/assets/Select.BW4Keufm.css +1 -0
  37. package/admin-build/_app/immutable/assets/Skeleton.KWUulTKJ.css +1 -0
  38. package/admin-build/_app/immutable/assets/Tabs.CniGYb67.css +1 -0
  39. package/admin-build/_app/immutable/assets/TimeChart.BTCDAvmT.css +1 -0
  40. package/admin-build/_app/immutable/assets/Toggle.Cy_K12OM.css +1 -0
  41. package/admin-build/_app/immutable/assets/TopList.ClFzmPlA.css +1 -0
  42. package/admin-build/_app/immutable/chunks/7B47DvSx.js +1 -0
  43. package/admin-build/_app/immutable/chunks/7f08Id8e.js +1 -0
  44. package/admin-build/_app/immutable/chunks/8wJeQ7LN.js +1 -0
  45. package/admin-build/_app/immutable/chunks/B-h2afW5.js +1 -0
  46. package/admin-build/_app/immutable/chunks/B8vJP3wz.js +1 -0
  47. package/admin-build/_app/immutable/chunks/BR_fL5Yv.js +1 -0
  48. package/admin-build/_app/immutable/chunks/BY92tFS2.js +1 -0
  49. package/admin-build/_app/immutable/chunks/BcR-Rdj9.js +1 -0
  50. package/admin-build/_app/immutable/chunks/BdrwyZv8.js +1 -0
  51. package/admin-build/_app/immutable/chunks/Bh56EfQ_.js +1 -0
  52. package/admin-build/_app/immutable/chunks/BkrCkgYp.js +1 -0
  53. package/admin-build/_app/immutable/chunks/BmRjiP5k.js +1 -0
  54. package/admin-build/_app/immutable/chunks/BsokvhWC.js +1 -0
  55. package/admin-build/_app/immutable/chunks/C4D51vTW.js +1 -0
  56. package/admin-build/_app/immutable/chunks/C6puvcoR.js +2 -0
  57. package/admin-build/_app/immutable/chunks/CCKNu7m7.js +1 -0
  58. package/admin-build/_app/immutable/chunks/CWj6FrbW.js +1 -0
  59. package/admin-build/_app/immutable/chunks/Ce-ngf4p.js +5 -0
  60. package/admin-build/_app/immutable/chunks/Cs0GwzJA.js +1 -0
  61. package/admin-build/_app/immutable/chunks/CwROoZK0.js +1 -0
  62. package/admin-build/_app/immutable/chunks/CxCPv_Ut.js +1 -0
  63. package/admin-build/_app/immutable/chunks/CxbRue-5.js +1 -0
  64. package/admin-build/_app/immutable/chunks/CyqB6g-D.js +1 -0
  65. package/admin-build/_app/immutable/chunks/D5h5A1cc.js +2 -0
  66. package/admin-build/_app/immutable/chunks/DnyL7Zq-.js +1 -0
  67. package/admin-build/_app/immutable/chunks/DoPXzH7F.js +1 -0
  68. package/admin-build/_app/immutable/chunks/DrQSgw-f.js +1 -0
  69. package/admin-build/_app/immutable/chunks/DttM2zNO.js +1 -0
  70. package/admin-build/_app/immutable/chunks/DuXuUBWN.js +1 -0
  71. package/admin-build/_app/immutable/chunks/MdeqaOQx.js +10 -0
  72. package/admin-build/_app/immutable/chunks/NuUjtcO2.js +1 -0
  73. package/admin-build/_app/immutable/chunks/Q2nPFxS6.js +1 -0
  74. package/admin-build/_app/immutable/chunks/R6arueIl.js +1 -0
  75. package/admin-build/_app/immutable/chunks/UUazaC_N.js +1 -0
  76. package/admin-build/_app/immutable/chunks/cOYbrQxx.js +1 -0
  77. package/admin-build/_app/immutable/chunks/eFQHTGwA.js +1 -0
  78. package/admin-build/_app/immutable/chunks/ehbppgYb.js +1 -0
  79. package/admin-build/_app/immutable/chunks/glwixJlP.js +1 -0
  80. package/admin-build/_app/immutable/chunks/vApWTCBs.js +1 -0
  81. package/admin-build/_app/immutable/chunks/w89G9Xpi.js +1 -0
  82. package/admin-build/_app/immutable/chunks/wJsUhbfZ.js +1 -0
  83. package/admin-build/_app/immutable/chunks/zfauFM8P.js +1 -0
  84. package/admin-build/_app/immutable/entry/app.CcO-Uos3.js +2 -0
  85. package/admin-build/_app/immutable/entry/start.COebYq3I.js +1 -0
  86. package/admin-build/_app/immutable/nodes/0.CjtHKU-6.js +1 -0
  87. package/admin-build/_app/immutable/nodes/1.DEisjlM0.js +1 -0
  88. package/admin-build/_app/immutable/nodes/10.CvhdyWVB.js +1 -0
  89. package/admin-build/_app/immutable/nodes/11.DjHqcOvy.js +1 -0
  90. package/admin-build/_app/immutable/nodes/12.mQLz4Mj_.js +1 -0
  91. package/admin-build/_app/immutable/nodes/13.CBonZZyP.js +110 -0
  92. package/admin-build/_app/immutable/nodes/14.d-oiZL0j.js +3 -0
  93. package/admin-build/_app/immutable/nodes/15.CKPQsUYF.js +1 -0
  94. package/admin-build/_app/immutable/nodes/16.wPzAPQGx.js +1 -0
  95. package/admin-build/_app/immutable/nodes/17.DayhKyEZ.js +1 -0
  96. package/admin-build/_app/immutable/nodes/18.DKwS0Ir0.js +1 -0
  97. package/admin-build/_app/immutable/nodes/19.wPzAPQGx.js +1 -0
  98. package/admin-build/_app/immutable/nodes/2.BKoKrw1i.js +1 -0
  99. package/admin-build/_app/immutable/nodes/20.BvIkkkrW.js +1 -0
  100. package/admin-build/_app/immutable/nodes/21.DMaFhdHk.js +128 -0
  101. package/admin-build/_app/immutable/nodes/22.3xdgwuK1.js +1 -0
  102. package/admin-build/_app/immutable/nodes/23.8Bvgjbsl.js +112 -0
  103. package/admin-build/_app/immutable/nodes/24.DzSSzRhG.js +2 -0
  104. package/admin-build/_app/immutable/nodes/25.9KKYBnAE.js +2 -0
  105. package/admin-build/_app/immutable/nodes/26.Bhn9dfhY.js +1 -0
  106. package/admin-build/_app/immutable/nodes/27.kRLiC24G.js +1 -0
  107. package/admin-build/_app/immutable/nodes/28.BVIN1-7N.js +1 -0
  108. package/admin-build/_app/immutable/nodes/29.3yabZWj4.js +1 -0
  109. package/admin-build/_app/immutable/nodes/3.BFtSOkX7.js +2 -0
  110. package/admin-build/_app/immutable/nodes/30.CyCQlwaP.js +1 -0
  111. package/admin-build/_app/immutable/nodes/31.C4LDXjES.js +1 -0
  112. package/admin-build/_app/immutable/nodes/4.CvbiMlCa.js +1 -0
  113. package/admin-build/_app/immutable/nodes/5.C6BLv2eM.js +1 -0
  114. package/admin-build/_app/immutable/nodes/6.BcXvfl2P.js +1 -0
  115. package/admin-build/_app/immutable/nodes/7.CIuqhPiK.js +1 -0
  116. package/admin-build/_app/immutable/nodes/8.BQOR_JfO.js +1 -0
  117. package/admin-build/_app/immutable/nodes/9.NZqXQxPy.js +1 -0
  118. package/admin-build/_app/version.json +1 -0
  119. package/admin-build/favicon.svg +26 -0
  120. package/admin-build/index.html +45 -0
  121. package/openapi.json +19543 -0
  122. package/package.json +66 -0
  123. package/src/__tests__/admin-assets.test.ts +55 -0
  124. package/src/__tests__/admin-data-routes.test.ts +488 -0
  125. package/src/__tests__/admin-db-target.test.ts +103 -0
  126. package/src/__tests__/admin-routing.test.ts +31 -0
  127. package/src/__tests__/admin-user-management.test.ts +311 -0
  128. package/src/__tests__/analytics-query.test.ts +75 -0
  129. package/src/__tests__/auth-d1.test.ts +749 -0
  130. package/src/__tests__/auth-db-adapter.test.ts +73 -0
  131. package/src/__tests__/auth-jwt.test.ts +440 -0
  132. package/src/__tests__/auth-oauth.test.ts +389 -0
  133. package/src/__tests__/auth-password.test.ts +367 -0
  134. package/src/__tests__/auth-redirect.test.ts +87 -0
  135. package/src/__tests__/backup-restore.test.ts +711 -0
  136. package/src/__tests__/broadcast.test.ts +128 -0
  137. package/src/__tests__/cli.test.ts +178 -0
  138. package/src/__tests__/cloudflare-realtime.test.ts +113 -0
  139. package/src/__tests__/config.test.ts +469 -0
  140. package/src/__tests__/cors.test.ts +154 -0
  141. package/src/__tests__/cron.test.ts +302 -0
  142. package/src/__tests__/d1-handler.test.ts +402 -0
  143. package/src/__tests__/d1-sql.test.ts +120 -0
  144. package/src/__tests__/database-live-config.test.ts +42 -0
  145. package/src/__tests__/database-live-emitter.test.ts +56 -0
  146. package/src/__tests__/database-live-filters.test.ts +63 -0
  147. package/src/__tests__/database-live-route.test.ts +113 -0
  148. package/src/__tests__/db-sql.test.ts +163 -0
  149. package/src/__tests__/do-lifecycle.test.ts +263 -0
  150. package/src/__tests__/do-router.test.ts +729 -0
  151. package/src/__tests__/email-provider.test.ts +128 -0
  152. package/src/__tests__/email-templates.test.ts +528 -0
  153. package/src/__tests__/error-format.test.ts +250 -0
  154. package/src/__tests__/field-ops.test.ts +242 -0
  155. package/src/__tests__/functions-context.test.ts +334 -0
  156. package/src/__tests__/functions-d1-proxy.test.ts +229 -0
  157. package/src/__tests__/functions-registry-runtime-config.test.ts +17 -0
  158. package/src/__tests__/functions-route.test.ts +139 -0
  159. package/src/__tests__/internal-request.test.ts +77 -0
  160. package/src/__tests__/log-writer.test.ts +44 -0
  161. package/src/__tests__/logger.test.ts +58 -0
  162. package/src/__tests__/meta-admin-proxy.test.ts +48 -0
  163. package/src/__tests__/meta-export-coverage.test.ts +191 -0
  164. package/src/__tests__/meta-route-registration.test.ts +47 -0
  165. package/src/__tests__/namespace-dump.test.ts +28 -0
  166. package/src/__tests__/oauth-providers.test.ts +337 -0
  167. package/src/__tests__/openapi-coverage.test.ts +144 -0
  168. package/src/__tests__/pagination.test.ts +59 -0
  169. package/src/__tests__/password-policy.test.ts +191 -0
  170. package/src/__tests__/plugin-migrations.test.ts +379 -0
  171. package/src/__tests__/postgres-batch-compat.test.ts +133 -0
  172. package/src/__tests__/postgres-dialect.test.ts +328 -0
  173. package/src/__tests__/postgres-executor.test.ts +79 -0
  174. package/src/__tests__/postgres-field-ops-compat.test.ts +222 -0
  175. package/src/__tests__/postgres-schema-init.test.ts +105 -0
  176. package/src/__tests__/postgres-table-utils.test.ts +107 -0
  177. package/src/__tests__/presence.test.ts +199 -0
  178. package/src/__tests__/provider.test.ts +550 -0
  179. package/src/__tests__/public-user-profile.test.ts +339 -0
  180. package/src/__tests__/push-handlers.test.ts +179 -0
  181. package/src/__tests__/push-provider.test.ts +80 -0
  182. package/src/__tests__/push-token.test.ts +418 -0
  183. package/src/__tests__/query.test.ts +771 -0
  184. package/src/__tests__/rate-limit.test.ts +260 -0
  185. package/src/__tests__/room-access-policy.test.ts +101 -0
  186. package/src/__tests__/room-handler-context.test.ts +130 -0
  187. package/src/__tests__/room-monitoring.test.ts +138 -0
  188. package/src/__tests__/room-runtime-routing.test.ts +222 -0
  189. package/src/__tests__/room.test.ts +254 -0
  190. package/src/__tests__/route-parser.test.ts +490 -0
  191. package/src/__tests__/rules.test.ts +234 -0
  192. package/src/__tests__/runtime-surface-accounting.test.ts +120 -0
  193. package/src/__tests__/scheduled.test.ts +80 -0
  194. package/src/__tests__/schema.test.ts +1273 -0
  195. package/src/__tests__/security-hardening.test.ts +312 -0
  196. package/src/__tests__/server.unit.test.ts +333 -0
  197. package/src/__tests__/service-key-db-proxy.test.ts +650 -0
  198. package/src/__tests__/service-key-provider-bypass.test.ts +138 -0
  199. package/src/__tests__/service-key.test.ts +757 -0
  200. package/src/__tests__/smoke-skip-report.test.ts +72 -0
  201. package/src/__tests__/sms-provider.test.ts +39 -0
  202. package/src/__tests__/sql-route.test.ts +218 -0
  203. package/src/__tests__/storage-hook-context.test.ts +115 -0
  204. package/src/__tests__/totp.test.ts +200 -0
  205. package/src/__tests__/uuid.test.ts +144 -0
  206. package/src/__tests__/validation.test.ts +773 -0
  207. package/src/__tests__/websocket-pending.test.ts +163 -0
  208. package/src/_functions-registry.ts +51 -0
  209. package/src/bench-entry.ts +9 -0
  210. package/src/cloudflare-test.d.ts +1 -0
  211. package/src/durable-objects/auth-do.ts +49 -0
  212. package/src/durable-objects/database-do.ts +2240 -0
  213. package/src/durable-objects/database-live-do.ts +949 -0
  214. package/src/durable-objects/logs-do.ts +1200 -0
  215. package/src/durable-objects/room-runtime-base.ts +1604 -0
  216. package/src/durable-objects/rooms-do.ts +2191 -0
  217. package/src/generated-config.ts +6 -0
  218. package/src/index.ts +382 -0
  219. package/src/lib/admin-assets.ts +54 -0
  220. package/src/lib/admin-db-target.ts +301 -0
  221. package/src/lib/admin-routing.ts +35 -0
  222. package/src/lib/admin-user-management.ts +464 -0
  223. package/src/lib/analytics-adapter.ts +103 -0
  224. package/src/lib/analytics-query.ts +579 -0
  225. package/src/lib/auth-d1-service.ts +1193 -0
  226. package/src/lib/auth-d1.ts +1056 -0
  227. package/src/lib/auth-db-adapter.ts +289 -0
  228. package/src/lib/auth-redirect.ts +116 -0
  229. package/src/lib/cidr.ts +115 -0
  230. package/src/lib/client-ip.ts +51 -0
  231. package/src/lib/cloudflare-realtime.ts +251 -0
  232. package/src/lib/control-db.ts +36 -0
  233. package/src/lib/cron.ts +163 -0
  234. package/src/lib/d1-handler.ts +1425 -0
  235. package/src/lib/d1-schema-init.ts +255 -0
  236. package/src/lib/d1-sql.ts +33 -0
  237. package/src/lib/database-live-config.ts +24 -0
  238. package/src/lib/database-live-emitter.ts +111 -0
  239. package/src/lib/db-sql.ts +66 -0
  240. package/src/lib/do-retry.ts +36 -0
  241. package/src/lib/do-router.ts +270 -0
  242. package/src/lib/do-sql.ts +73 -0
  243. package/src/lib/email-provider.ts +379 -0
  244. package/src/lib/email-templates.ts +285 -0
  245. package/src/lib/email-translations.ts +422 -0
  246. package/src/lib/errors.ts +151 -0
  247. package/src/lib/functions.ts +2091 -0
  248. package/src/lib/hono.ts +56 -0
  249. package/src/lib/internal-request.ts +56 -0
  250. package/src/lib/jwt.ts +354 -0
  251. package/src/lib/log-writer.ts +272 -0
  252. package/src/lib/namespace-dump.ts +125 -0
  253. package/src/lib/oauth-providers.ts +1225 -0
  254. package/src/lib/op-parser.ts +99 -0
  255. package/src/lib/openapi.ts +146 -0
  256. package/src/lib/pagination.ts +19 -0
  257. package/src/lib/password-policy.ts +102 -0
  258. package/src/lib/password.ts +145 -0
  259. package/src/lib/plugin-migrations.ts +612 -0
  260. package/src/lib/postgres-executor.ts +203 -0
  261. package/src/lib/postgres-handler.ts +1102 -0
  262. package/src/lib/postgres-schema-init.ts +341 -0
  263. package/src/lib/postgres-table-utils.ts +87 -0
  264. package/src/lib/public-user-profile.ts +187 -0
  265. package/src/lib/push-provider.ts +409 -0
  266. package/src/lib/push-token.ts +294 -0
  267. package/src/lib/query-engine.ts +768 -0
  268. package/src/lib/room-monitoring.ts +97 -0
  269. package/src/lib/room-runtime.ts +14 -0
  270. package/src/lib/route-parser.ts +434 -0
  271. package/src/lib/schema.ts +538 -0
  272. package/src/lib/schemas.ts +152 -0
  273. package/src/lib/service-key.ts +419 -0
  274. package/src/lib/sms-provider.ts +230 -0
  275. package/src/lib/startup-config.ts +99 -0
  276. package/src/lib/totp.ts +242 -0
  277. package/src/lib/uuid.ts +87 -0
  278. package/src/lib/validation.ts +205 -0
  279. package/src/lib/version.ts +2 -0
  280. package/src/lib/websocket-pending.ts +40 -0
  281. package/src/middleware/auth.ts +169 -0
  282. package/src/middleware/captcha-verify.ts +217 -0
  283. package/src/middleware/cors.ts +159 -0
  284. package/src/middleware/error-handler.ts +54 -0
  285. package/src/middleware/internal-guard.ts +26 -0
  286. package/src/middleware/logger.ts +126 -0
  287. package/src/middleware/rate-limit.ts +283 -0
  288. package/src/middleware/rules.ts +475 -0
  289. package/src/routes/admin-auth.ts +447 -0
  290. package/src/routes/admin.ts +3501 -0
  291. package/src/routes/analytics-api.ts +290 -0
  292. package/src/routes/auth.ts +4222 -0
  293. package/src/routes/backup.ts +1466 -0
  294. package/src/routes/config.ts +53 -0
  295. package/src/routes/d1.ts +109 -0
  296. package/src/routes/database-live.ts +281 -0
  297. package/src/routes/functions.ts +155 -0
  298. package/src/routes/health.ts +32 -0
  299. package/src/routes/kv.ts +167 -0
  300. package/src/routes/oauth.ts +1055 -0
  301. package/src/routes/push.ts +1465 -0
  302. package/src/routes/room.ts +639 -0
  303. package/src/routes/schema-endpoint.ts +76 -0
  304. package/src/routes/sql.ts +176 -0
  305. package/src/routes/storage.ts +1674 -0
  306. package/src/routes/tables.ts +699 -0
  307. package/src/routes/users.ts +21 -0
  308. package/src/routes/vectorize.ts +372 -0
  309. package/src/types.ts +99 -0
@@ -0,0 +1,729 @@
1
+ /**
2
+ * 서버 단위 테스트 — lib/do-router.ts (config / DO 라우팅 유틸리티)
3
+ * + 1-20 rate-limit.test.ts 관련 (service-key/rate-limit 유틸리티 포함)
4
+ *
5
+ * 실행: cd packages/server && npx vitest run src/__tests__/do-router.test.ts
6
+ *
7
+ * 테스트 대상:
8
+ * getDbDoName / parseDbDoName
9
+ * setConfig / parseConfig
10
+ * getTablesInNamespace / findTableNamespace
11
+ */
12
+
13
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
14
+ import {
15
+ getDbDoName,
16
+ parseDbDoName,
17
+ setConfig,
18
+ parseConfig,
19
+ getTablesInNamespace,
20
+ findTableNamespace,
21
+ callDO,
22
+ callDOByHexId,
23
+ shouldRouteToD1,
24
+ getD1BindingName,
25
+ } from '../lib/do-router.js';
26
+
27
+ // ─── A. getDbDoName ──────────────────────────────────────────────────────────
28
+
29
+ describe('getDbDoName', () => {
30
+ it('static namespace (no id) → returns namespace only', () => {
31
+ expect(getDbDoName('shared')).toBe('shared');
32
+ });
33
+
34
+ it('dynamic namespace + id → namespace:id format', () => {
35
+ expect(getDbDoName('workspace', 'ws-456')).toBe('workspace:ws-456');
36
+ });
37
+
38
+ it('user namespace', () => {
39
+ expect(getDbDoName('user', 'user-123')).toBe('user:user-123');
40
+ });
41
+
42
+ it('id with colon → throws', () => {
43
+ expect(() => getDbDoName('workspace', 'ws:bad')).toThrow();
44
+ });
45
+
46
+ it('error message mentions colon', () => {
47
+ try {
48
+ getDbDoName('workspace', 'ws:bad');
49
+ } catch (err) {
50
+ expect((err as Error).message).toContain(':');
51
+ }
52
+ });
53
+
54
+ it('id with dash is fine', () => {
55
+ expect(() => getDbDoName('workspace', 'ws-123')).not.toThrow();
56
+ });
57
+
58
+ it('empty id → treated as no id (static)', () => {
59
+ // empty string is falsy
60
+ expect(getDbDoName('shared', '')).toBe('shared');
61
+ });
62
+
63
+ it('undefined id → static DO name', () => {
64
+ expect(getDbDoName('db')).toBe('db');
65
+ });
66
+ });
67
+
68
+ // ─── B. parseDbDoName ────────────────────────────────────────────────────────
69
+
70
+ describe('parseDbDoName', () => {
71
+ it('static name → { namespace, id: undefined }', () => {
72
+ const result = parseDbDoName('shared');
73
+ expect(result.namespace).toBe('shared');
74
+ expect(result.id).toBeUndefined();
75
+ });
76
+
77
+ it('dynamic name → { namespace, id }', () => {
78
+ const result = parseDbDoName('workspace:ws-456');
79
+ expect(result.namespace).toBe('workspace');
80
+ expect(result.id).toBe('ws-456');
81
+ });
82
+
83
+ it('only first colon separates (id may contain dash)', () => {
84
+ const result = parseDbDoName('ns:my-id-123');
85
+ expect(result.namespace).toBe('ns');
86
+ expect(result.id).toBe('my-id-123');
87
+ });
88
+
89
+ it('round-trip: getDbDoName → parseDbDoName', () => {
90
+ const doName = getDbDoName('user', 'user-abc');
91
+ const parsed = parseDbDoName(doName);
92
+ expect(parsed.namespace).toBe('user');
93
+ expect(parsed.id).toBe('user-abc');
94
+ });
95
+
96
+ it('system name: db:_system → namespace=db, id=_system', () => {
97
+ const result = parseDbDoName('db:_system');
98
+ expect(result.namespace).toBe('db');
99
+ expect(result.id).toBe('_system');
100
+ });
101
+ });
102
+
103
+ // ─── C. setConfig / parseConfig ──────────────────────────────────────────────
104
+
105
+ describe('setConfig / parseConfig', () => {
106
+ beforeEach(() => {
107
+ // Reset singleton by re-setting empty config
108
+ setConfig({});
109
+ delete (globalThis as Record<string, unknown>).__EDGEBASE_RUNTIME_CONFIG__;
110
+ });
111
+
112
+ it('setConfig and parseConfig return same config', () => {
113
+ const cfg = { databases: { shared: { tables: { posts: {} } } } } as any;
114
+ setConfig(cfg);
115
+ expect(parseConfig()).toEqual(cfg);
116
+ });
117
+
118
+ it('setConfig fails fast on normalization errors', () => {
119
+ expect(() =>
120
+ setConfig({
121
+ databases: {
122
+ shared: {
123
+ tables: {
124
+ 'plugin-a/events': {},
125
+ },
126
+ },
127
+ },
128
+ plugins: [
129
+ {
130
+ name: 'plugin-a',
131
+ pluginApiVersion: 1,
132
+ config: {},
133
+ tables: {
134
+ events: {},
135
+ },
136
+ },
137
+ ],
138
+ } as any),
139
+ ).toThrow('Plugin table collision');
140
+ });
141
+
142
+ it('singleton remains authoritative when runtime input is unrelated', () => {
143
+ setConfig({ databases: { from: 'singleton' } } as any);
144
+ const result = parseConfig({ ignored: true });
145
+ expect((result as any).databases?.from).toBe('singleton');
146
+ });
147
+
148
+ it('empty singleton remains authoritative even when extra input is passed', () => {
149
+ setConfig({} as any);
150
+ const result = parseConfig({ ignored: { databases: { shared: { tables: { posts: {} } } } } });
151
+ expect(result).toEqual({});
152
+ });
153
+
154
+ it('fresh module without startup config returns empty object', async () => {
155
+ vi.resetModules();
156
+ const fresh = await import('../lib/do-router.js');
157
+ expect(fresh.parseConfig({ ignored: true })).toEqual({});
158
+ });
159
+
160
+ it('fresh module without startup config returns empty object', async () => {
161
+ vi.resetModules();
162
+ const fresh = await import('../lib/do-router.js');
163
+ expect(fresh.parseConfig({ arbitrary: true })).toEqual({});
164
+ });
165
+
166
+ it('request-scoped EDGEBASE_CONFIG overrides singleton config', () => {
167
+ setConfig({ databases: { from: 'singleton' } } as any);
168
+ const result = parseConfig({
169
+ EDGEBASE_CONFIG: JSON.stringify({
170
+ databases: {
171
+ shared: {
172
+ tables: {
173
+ posts: {},
174
+ },
175
+ },
176
+ },
177
+ }),
178
+ });
179
+ expect((result as any).databases?.shared?.tables?.posts).toEqual({});
180
+ expect((result as any).databases?.from).toBeUndefined();
181
+ });
182
+
183
+ it('fresh module reads EDGEBASE_CONFIG when provided', async () => {
184
+ vi.resetModules();
185
+ const fresh = await import('../lib/do-router.js');
186
+ expect(fresh.parseConfig({
187
+ EDGEBASE_CONFIG: JSON.stringify({
188
+ databases: {
189
+ shared: {
190
+ tables: {
191
+ comments: {},
192
+ },
193
+ },
194
+ },
195
+ }),
196
+ })).toEqual({
197
+ databases: {
198
+ shared: {
199
+ tables: {
200
+ comments: {},
201
+ },
202
+ },
203
+ },
204
+ });
205
+ });
206
+
207
+ it('fresh module can recover config from global runtime storage', async () => {
208
+ const cfg = {
209
+ storage: {
210
+ buckets: {
211
+ lab: {},
212
+ },
213
+ },
214
+ serviceKeys: {
215
+ keys: [
216
+ {
217
+ kid: 'test',
218
+ tier: 'root',
219
+ scopes: ['*'],
220
+ secretSource: 'inline',
221
+ inlineSecret: 'sk-test',
222
+ },
223
+ ],
224
+ },
225
+ } as any;
226
+
227
+ setConfig(cfg);
228
+ vi.resetModules();
229
+ const fresh = await import('../lib/do-router.js');
230
+ const result = fresh.parseConfig();
231
+
232
+ expect((result as any).storage?.buckets?.lab).toEqual({});
233
+ expect((result as any).serviceKeys?.keys?.[0]?.kid).toBe('test');
234
+ });
235
+
236
+ it('no singleton, no env → returns {}', () => {
237
+ setConfig({} as any);
238
+ const result = parseConfig();
239
+ expect(result).toBeDefined();
240
+ });
241
+ });
242
+
243
+ // ─── D. getTablesInNamespace ─────────────────────────────────────────────────
244
+
245
+ describe('getTablesInNamespace', () => {
246
+ const config = {
247
+ databases: {
248
+ shared: {
249
+ tables: {
250
+ posts: {},
251
+ users: {},
252
+ },
253
+ },
254
+ },
255
+ } as any;
256
+
257
+ it('returns table names for namespace', () => {
258
+ const tables = getTablesInNamespace('shared', config);
259
+ expect(tables).toContain('posts');
260
+ expect(tables).toContain('users');
261
+ });
262
+
263
+ it('unknown namespace → empty array', () => {
264
+ const tables = getTablesInNamespace('unknown', config);
265
+ expect(tables).toEqual([]);
266
+ });
267
+
268
+ it('no databases in config → empty array', () => {
269
+ const tables = getTablesInNamespace('shared', {});
270
+ expect(tables).toEqual([]);
271
+ });
272
+
273
+ it('no tables in namespace → empty array', () => {
274
+ const tables = getTablesInNamespace('shared', {
275
+ databases: { shared: {} },
276
+ } as any);
277
+ expect(tables).toEqual([]);
278
+ });
279
+ });
280
+
281
+ // ─── E. findTableNamespace ────────────────────────────────────────────────────
282
+
283
+ describe('findTableNamespace', () => {
284
+ const config = {
285
+ databases: {
286
+ shared: { tables: { posts: {}, comments: {} } },
287
+ workspace: { tables: { workspaces: {}, members: {} } },
288
+ },
289
+ } as any;
290
+
291
+ it('finds namespace for table in shared', () => {
292
+ expect(findTableNamespace('posts', config)).toBe('shared');
293
+ });
294
+
295
+ it('finds namespace for table in workspace block', () => {
296
+ expect(findTableNamespace('workspaces', config)).toBe('workspace');
297
+ });
298
+
299
+ it('unknown table → undefined', () => {
300
+ expect(findTableNamespace('nonexistent', config)).toBeUndefined();
301
+ });
302
+
303
+ it('no databases → undefined', () => {
304
+ expect(findTableNamespace('posts', {})).toBeUndefined();
305
+ });
306
+
307
+ it('comments in shared block', () => {
308
+ expect(findTableNamespace('comments', config)).toBe('shared');
309
+ });
310
+ });
311
+
312
+ // ─── F. Edge cases (mutation coverage) ────────────────────────────────────────
313
+
314
+ describe('getDbDoName — edge cases', () => {
315
+ it('id with multiple colons → throws', () => {
316
+ expect(() => getDbDoName('ns', 'a:b:c')).toThrow();
317
+ });
318
+
319
+ it('colon-only id → throws', () => {
320
+ expect(() => getDbDoName('ns', ':')).toThrow();
321
+ });
322
+
323
+ it('namespace with special chars but no colon in id → ok', () => {
324
+ expect(getDbDoName('my-ns', 'id_123')).toBe('my-ns:id_123');
325
+ });
326
+ });
327
+
328
+ describe('parseDbDoName — edge cases', () => {
329
+ it('colon at start → namespace="", id="foo"', () => {
330
+ const result = parseDbDoName(':foo');
331
+ expect(result.namespace).toBe('');
332
+ expect(result.id).toBe('foo');
333
+ });
334
+
335
+ it('colon at end → namespace="foo", id=""', () => {
336
+ const result = parseDbDoName('foo:');
337
+ expect(result.namespace).toBe('foo');
338
+ expect(result.id).toBe('');
339
+ });
340
+
341
+ it('multiple colons → first colon only', () => {
342
+ const result = parseDbDoName('a:b:c');
343
+ expect(result.namespace).toBe('a');
344
+ expect(result.id).toBe('b:c');
345
+ });
346
+
347
+ it('single colon → namespace="", id=""', () => {
348
+ const result = parseDbDoName(':');
349
+ expect(result.namespace).toBe('');
350
+ expect(result.id).toBe('');
351
+ });
352
+
353
+ it('empty string → { namespace: "" }', () => {
354
+ const result = parseDbDoName('');
355
+ expect(result.namespace).toBe('');
356
+ expect(result.id).toBeUndefined();
357
+ });
358
+ });
359
+
360
+ describe('getTablesInNamespace — edge cases', () => {
361
+ it('namespace exists but tables is null → empty array', () => {
362
+ const tables = getTablesInNamespace('shared', {
363
+ databases: { shared: { tables: null } },
364
+ } as any);
365
+ expect(tables).toEqual([]);
366
+ });
367
+
368
+ it('returns exact keys (no prototype pollution)', () => {
369
+ const config = {
370
+ databases: {
371
+ shared: {
372
+ tables: Object.create(null, {
373
+ posts: { value: {}, enumerable: true },
374
+ }),
375
+ },
376
+ },
377
+ } as any;
378
+ const tables = getTablesInNamespace('shared', config);
379
+ expect(tables).toEqual(['posts']);
380
+ });
381
+ });
382
+
383
+ describe('findTableNamespace — edge cases', () => {
384
+ it('tables is undefined in dbBlock → skip', () => {
385
+ const config = {
386
+ databases: { shared: {} },
387
+ } as any;
388
+ expect(findTableNamespace('posts', config)).toBeUndefined();
389
+ });
390
+
391
+ it('first match wins when table in multiple namespaces', () => {
392
+ const config = {
393
+ databases: {
394
+ ns1: { tables: { shared_table: {} } },
395
+ ns2: { tables: { shared_table: {} } },
396
+ },
397
+ } as any;
398
+ const result = findTableNamespace('shared_table', config);
399
+ // Object.entries iteration order → first entry wins
400
+ expect(result).toBe('ns1');
401
+ });
402
+
403
+ it('__proto__ as table name → not found (prototype safety)', () => {
404
+ const config = {
405
+ databases: {
406
+ shared: { tables: { posts: {} } },
407
+ },
408
+ } as any;
409
+ expect(findTableNamespace('__proto__', config)).toBeUndefined();
410
+ });
411
+ });
412
+
413
+ describe('parseConfig — edge cases', () => {
414
+ it('fresh module with ignored runtime input still returns {}', async () => {
415
+ vi.resetModules();
416
+ const fresh = await import('../lib/do-router.js');
417
+ const result = fresh.parseConfig({ ignored: '' });
418
+ expect(result).toEqual({});
419
+ });
420
+ });
421
+
422
+ // ─── H. callDO — mutation-killing ───────────────────────────────────────────
423
+ // Covers 36 no-coverage + 1 survived mutant in callDO/callDOByHexId
424
+
425
+ function createMockNamespace() {
426
+ const fetchSpy = vi.fn(
427
+ async (_url: string, _init?: RequestInit) => new Response('ok', { status: 200 }),
428
+ );
429
+ const mockStub = { fetch: fetchSpy };
430
+ const mockId = { toString: () => 'mock-id' };
431
+ const ns = {
432
+ idFromName: vi.fn((_name: string) => mockId),
433
+ idFromString: vi.fn((_hex: string) => mockId),
434
+ get: vi.fn((_id: unknown) => mockStub),
435
+ } as unknown as DurableObjectNamespace;
436
+ return { ns, fetchSpy, mockStub, mockId };
437
+ }
438
+
439
+ /** Extract [url, init] from a fetch mock call with proper types. */
440
+ function mockCall(spy: ReturnType<typeof vi.fn>, index = 0) {
441
+ const [url, init] = spy.mock.calls[index] as [string, RequestInit];
442
+ const headers = init.headers as Record<string, string>;
443
+ return { url, init, headers };
444
+ }
445
+
446
+ describe('callDO', () => {
447
+ it('default GET request with correct URL and headers', async () => {
448
+ const { ns, fetchSpy } = createMockNamespace();
449
+ await callDO(ns, 'shared', '/api/tables');
450
+
451
+ expect(fetchSpy).toHaveBeenCalledOnce();
452
+ const { url, init, headers } = mockCall(fetchSpy);
453
+ expect(url).toBe('http://do/api/tables');
454
+ expect(init.method).toBe('GET');
455
+ expect(headers['Content-Type']).toBe('application/json');
456
+ expect(headers['X-DO-Name']).toBe('shared');
457
+ });
458
+
459
+ it('uses idFromName with the doName', async () => {
460
+ const { ns } = createMockNamespace();
461
+ await callDO(ns, 'workspace:ws-1', '/path');
462
+ expect(ns.idFromName).toHaveBeenCalledWith('workspace:ws-1');
463
+ });
464
+
465
+ it('POST with body → body is JSON-stringified', async () => {
466
+ const { ns, fetchSpy } = createMockNamespace();
467
+ await callDO(ns, 'db', '/insert', {
468
+ method: 'POST',
469
+ body: { name: 'test', value: 42 },
470
+ });
471
+
472
+ const { init } = mockCall(fetchSpy);
473
+ expect(init.method).toBe('POST');
474
+ expect(init.body).toBe(JSON.stringify({ name: 'test', value: 42 }));
475
+ });
476
+
477
+ it('GET with body → body is NOT attached (GET semantics)', async () => {
478
+ const { ns, fetchSpy } = createMockNamespace();
479
+ await callDO(ns, 'db', '/read', {
480
+ method: 'GET',
481
+ body: { ignored: true },
482
+ });
483
+
484
+ const { init } = mockCall(fetchSpy);
485
+ expect(init.method).toBe('GET');
486
+ expect(init.body).toBeUndefined();
487
+ });
488
+
489
+ it('custom headers are merged with defaults', async () => {
490
+ const { ns, fetchSpy } = createMockNamespace();
491
+ await callDO(ns, 'db', '/path', {
492
+ headers: { 'X-Custom': 'value', Authorization: 'Bearer tok' },
493
+ });
494
+
495
+ const { headers } = mockCall(fetchSpy);
496
+ // Default headers preserved
497
+ expect(headers['Content-Type']).toBe('application/json');
498
+ expect(headers['X-DO-Name']).toBe('db');
499
+ // Custom headers merged
500
+ expect(headers['X-Custom']).toBe('value');
501
+ expect(headers['Authorization']).toBe('Bearer tok');
502
+ });
503
+
504
+ it('custom header can override Content-Type', async () => {
505
+ const { ns, fetchSpy } = createMockNamespace();
506
+ await callDO(ns, 'db', '/path', {
507
+ headers: { 'Content-Type': 'text/plain' },
508
+ });
509
+
510
+ const { headers } = mockCall(fetchSpy);
511
+ // Spread puts custom header AFTER default → overrides
512
+ expect(headers['Content-Type']).toBe('text/plain');
513
+ });
514
+
515
+ it('body undefined + non-GET method → no body in init', async () => {
516
+ const { ns, fetchSpy } = createMockNamespace();
517
+ await callDO(ns, 'db', '/delete', { method: 'DELETE' });
518
+
519
+ const { init } = mockCall(fetchSpy);
520
+ expect(init.method).toBe('DELETE');
521
+ expect(init.body).toBeUndefined();
522
+ });
523
+
524
+ it('returns the Response from stub.fetch', async () => {
525
+ const { ns } = createMockNamespace();
526
+ const response = await callDO(ns, 'db', '/test');
527
+ expect(response).toBeInstanceOf(Response);
528
+ expect(response.status).toBe(200);
529
+ expect(await response.text()).toBe('ok');
530
+ });
531
+
532
+ it('no options → defaults to GET with default headers', async () => {
533
+ const { ns, fetchSpy } = createMockNamespace();
534
+ await callDO(ns, 'test', '/health');
535
+
536
+ const { url, init, headers } = mockCall(fetchSpy);
537
+ expect(url).toBe('http://do/health');
538
+ expect(init.method).toBe('GET');
539
+ expect(headers['X-DO-Name']).toBe('test');
540
+ });
541
+
542
+ it('PUT method with body → body included', async () => {
543
+ const { ns, fetchSpy } = createMockNamespace();
544
+ await callDO(ns, 'db', '/update', {
545
+ method: 'PUT',
546
+ body: { updated: true },
547
+ });
548
+
549
+ const { init } = mockCall(fetchSpy);
550
+ expect(init.method).toBe('PUT');
551
+ expect(init.body).toBe('{"updated":true}');
552
+ });
553
+ });
554
+
555
+ // ─── I. callDOByHexId — mutation-killing ────────────────────────────────────
556
+
557
+ describe('callDOByHexId', () => {
558
+ it('default GET request with correct URL', async () => {
559
+ const { ns, fetchSpy } = createMockNamespace();
560
+ await callDOByHexId(ns, 'abcd1234', '/backup');
561
+
562
+ expect(fetchSpy).toHaveBeenCalledOnce();
563
+ const { url, init } = mockCall(fetchSpy);
564
+ expect(url).toBe('http://do/backup');
565
+ expect(init.method).toBe('GET');
566
+ });
567
+
568
+ it('uses idFromString (not idFromName)', async () => {
569
+ const { ns } = createMockNamespace();
570
+ await callDOByHexId(ns, 'deadbeef', '/path');
571
+ expect(ns.idFromString).toHaveBeenCalledWith('deadbeef');
572
+ expect(ns.idFromName).not.toHaveBeenCalled();
573
+ });
574
+
575
+ it('headers include Content-Type but NOT X-DO-Name', async () => {
576
+ const { ns, fetchSpy } = createMockNamespace();
577
+ await callDOByHexId(ns, 'hex123', '/path');
578
+
579
+ const { headers } = mockCall(fetchSpy);
580
+ expect(headers['Content-Type']).toBe('application/json');
581
+ // callDOByHexId does NOT set X-DO-Name (unlike callDO)
582
+ expect(headers['X-DO-Name']).toBeUndefined();
583
+ });
584
+
585
+ it('POST with body → JSON stringified', async () => {
586
+ const { ns, fetchSpy } = createMockNamespace();
587
+ await callDOByHexId(ns, 'hex', '/restore', {
588
+ method: 'POST',
589
+ body: { data: [1, 2, 3] },
590
+ });
591
+
592
+ const { init } = mockCall(fetchSpy);
593
+ expect(init.method).toBe('POST');
594
+ expect(init.body).toBe(JSON.stringify({ data: [1, 2, 3] }));
595
+ });
596
+
597
+ it('GET with body → body not attached', async () => {
598
+ const { ns, fetchSpy } = createMockNamespace();
599
+ await callDOByHexId(ns, 'hex', '/read', {
600
+ method: 'GET',
601
+ body: { skip: true },
602
+ });
603
+
604
+ const { init } = mockCall(fetchSpy);
605
+ expect(init.body).toBeUndefined();
606
+ });
607
+
608
+ it('custom headers merged', async () => {
609
+ const { ns, fetchSpy } = createMockNamespace();
610
+ await callDOByHexId(ns, 'hex', '/path', {
611
+ headers: { 'X-Token': 'abc' },
612
+ });
613
+
614
+ const { headers } = mockCall(fetchSpy);
615
+ expect(headers['Content-Type']).toBe('application/json');
616
+ expect(headers['X-Token']).toBe('abc');
617
+ });
618
+
619
+ it('DELETE without body → no body', async () => {
620
+ const { ns, fetchSpy } = createMockNamespace();
621
+ await callDOByHexId(ns, 'hex', '/remove', { method: 'DELETE' });
622
+
623
+ const { init } = mockCall(fetchSpy);
624
+ expect(init.method).toBe('DELETE');
625
+ expect(init.body).toBeUndefined();
626
+ });
627
+
628
+ it('returns the Response from stub.fetch', async () => {
629
+ const { ns } = createMockNamespace();
630
+ const response = await callDOByHexId(ns, 'hex', '/test');
631
+ expect(response.status).toBe(200);
632
+ });
633
+ });
634
+
635
+ // ─── J. shouldRouteToD1 ───────────────────────────────────────────────────────
636
+
637
+ describe('shouldRouteToD1', () => {
638
+ it('namespace not in config → false', () => {
639
+ expect(shouldRouteToD1('unknown', {})).toBe(false);
640
+ });
641
+
642
+ it('explicit provider: "d1" → true', () => {
643
+ expect(
644
+ shouldRouteToD1('shared', {
645
+ databases: { shared: { provider: 'd1', tables: { posts: {} } } },
646
+ } as any),
647
+ ).toBe(true);
648
+ });
649
+
650
+ it('explicit provider: "do" → false', () => {
651
+ expect(
652
+ shouldRouteToD1('shared', {
653
+ databases: { shared: { provider: 'do', tables: { posts: {} } } },
654
+ } as any),
655
+ ).toBe(false);
656
+ });
657
+
658
+ it('explicit provider: "neon" → false', () => {
659
+ expect(
660
+ shouldRouteToD1('shared', {
661
+ databases: { shared: { provider: 'neon', tables: { posts: {} } } },
662
+ } as any),
663
+ ).toBe(false);
664
+ });
665
+
666
+ it('explicit provider: "postgres" → false', () => {
667
+ expect(
668
+ shouldRouteToD1('shared', {
669
+ databases: { shared: { provider: 'postgres', tables: { posts: {} } } },
670
+ } as any),
671
+ ).toBe(false);
672
+ });
673
+
674
+ it('instance: true → false (multi-tenant stays in DO)', () => {
675
+ expect(
676
+ shouldRouteToD1('workspace', {
677
+ databases: { workspace: { instance: true, tables: { members: {} } } },
678
+ } as any),
679
+ ).toBe(false);
680
+ });
681
+
682
+ it('access.canCreate → false (multi-tenant)', () => {
683
+ expect(
684
+ shouldRouteToD1('workspace', {
685
+ databases: { workspace: { access: { canCreate: 'auth.role == "admin"' }, tables: {} } },
686
+ } as any),
687
+ ).toBe(false);
688
+ });
689
+
690
+ it('access.access → false (multi-tenant)', () => {
691
+ expect(
692
+ shouldRouteToD1('workspace', {
693
+ databases: { workspace: { access: { access: 'auth.role == "admin"' }, tables: {} } },
694
+ } as any),
695
+ ).toBe(false);
696
+ });
697
+
698
+ it('no provider, no instance, no access → auto-detect as D1', () => {
699
+ expect(
700
+ shouldRouteToD1('shared', {
701
+ databases: { shared: { tables: { posts: {} } } },
702
+ } as any),
703
+ ).toBe(true);
704
+ });
705
+
706
+ it('empty databases block → false', () => {
707
+ expect(shouldRouteToD1('shared', { databases: {} } as any)).toBe(false);
708
+ });
709
+
710
+ it('no databases at all → false', () => {
711
+ expect(shouldRouteToD1('shared', {} as any)).toBe(false);
712
+ });
713
+ });
714
+
715
+ // ─── K. getD1BindingName ──────────────────────────────────────────────────────
716
+
717
+ describe('getD1BindingName', () => {
718
+ it('shared → DB_D1_SHARED', () => {
719
+ expect(getD1BindingName('shared')).toBe('DB_D1_SHARED');
720
+ });
721
+
722
+ it('myData → DB_D1_MYDATA', () => {
723
+ expect(getD1BindingName('myData')).toBe('DB_D1_MYDATA');
724
+ });
725
+
726
+ it('lowercase → uppercase', () => {
727
+ expect(getD1BindingName('analytics')).toBe('DB_D1_ANALYTICS');
728
+ });
729
+ });