@taicode/common-server 4.0.0-beta.1 → 4.0.0-beta.10

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 (292) hide show
  1. package/output/app/index.d.ts +18 -15
  2. package/output/app/index.d.ts.map +1 -1
  3. package/output/app/index.js +330 -18
  4. package/output/auth/index.d.ts +73 -0
  5. package/output/auth/index.d.ts.map +1 -0
  6. package/output/auth/index.js +196 -0
  7. package/output/core/assemble.d.ts +64 -0
  8. package/output/core/assemble.d.ts.map +1 -0
  9. package/output/core/assemble.js +88 -0
  10. package/output/core/boot/context.d.ts +109 -0
  11. package/output/core/boot/context.d.ts.map +1 -0
  12. package/output/core/boot/context.js +145 -0
  13. package/output/core/boot/lifecycle.d.ts +206 -0
  14. package/output/core/boot/lifecycle.d.ts.map +1 -0
  15. package/output/core/boot/lifecycle.js +255 -0
  16. package/output/core/boot/shutdown.d.ts +34 -0
  17. package/output/core/boot/shutdown.d.ts.map +1 -0
  18. package/output/core/boot/shutdown.js +50 -0
  19. package/output/core/boot/signal.d.ts +13 -0
  20. package/output/core/boot/signal.d.ts.map +1 -0
  21. package/output/{app → core/boot}/signal.js +3 -2
  22. package/output/core/boot/steps.d.ts +138 -0
  23. package/output/core/boot/steps.d.ts.map +1 -0
  24. package/output/core/boot/steps.js +259 -0
  25. package/output/core/entries.d.ts +151 -0
  26. package/output/core/entries.d.ts.map +1 -0
  27. package/output/core/entries.js +242 -0
  28. package/output/core/entries.test.d.ts +2 -0
  29. package/output/core/entries.test.d.ts.map +1 -0
  30. package/output/core/entries.test.js +458 -0
  31. package/output/core/env.d.ts +83 -0
  32. package/output/core/env.d.ts.map +1 -0
  33. package/output/core/env.js +124 -0
  34. package/output/{app → core}/env.test.d.ts.map +1 -1
  35. package/output/core/env.test.js +105 -0
  36. package/output/core/modules/auth-provider/index.d.ts +10 -0
  37. package/output/core/modules/auth-provider/index.d.ts.map +1 -0
  38. package/output/core/modules/auth-provider/index.js +9 -0
  39. package/output/core/modules/auth-provider/types.d.ts +71 -0
  40. package/output/core/modules/auth-provider/types.d.ts.map +1 -0
  41. package/output/core/modules/context/index.d.ts +18 -0
  42. package/output/core/modules/context/index.d.ts.map +1 -0
  43. package/output/core/modules/context/index.js +23 -0
  44. package/output/core/modules/context/index.test.d.ts.map +1 -0
  45. package/output/core/modules/cookie/index.d.ts +21 -0
  46. package/output/core/modules/cookie/index.d.ts.map +1 -0
  47. package/output/{app/core/cookie.js → core/modules/cookie/index.js} +1 -6
  48. package/output/core/modules/identity/index.d.ts +4 -0
  49. package/output/core/modules/identity/index.d.ts.map +1 -0
  50. package/output/{app/core/identity.js → core/modules/identity/index.js} +1 -1
  51. package/output/core/modules/logging/index.d.ts +5 -0
  52. package/output/core/modules/logging/index.d.ts.map +1 -0
  53. package/output/{app/core/logging.js → core/modules/logging/index.js} +2 -2
  54. package/output/core/modules/openapi/index.d.ts +7 -0
  55. package/output/core/modules/openapi/index.d.ts.map +1 -0
  56. package/output/{app/core/openapi.js → core/modules/openapi/index.js} +11 -12
  57. package/output/core/modules/permission-operations/index.d.ts +26 -0
  58. package/output/core/modules/permission-operations/index.d.ts.map +1 -0
  59. package/output/core/modules/permission-operations/index.js +36 -0
  60. package/output/core/modules/permission-operations/index.test.d.ts +2 -0
  61. package/output/core/modules/permission-operations/index.test.d.ts.map +1 -0
  62. package/output/core/modules/permission-operations/index.test.js +45 -0
  63. package/output/{app/core/request-id.d.ts → core/modules/request-id/index.d.ts} +1 -4
  64. package/output/core/modules/request-id/index.d.ts.map +1 -0
  65. package/output/{app/core/request-id.js → core/modules/request-id/index.js} +2 -2
  66. package/output/core/modules/response/index.d.ts +50 -0
  67. package/output/core/modules/response/index.d.ts.map +1 -0
  68. package/output/{app/core/response.js → core/modules/response/index.js} +31 -18
  69. package/output/{schema/index.d.ts → core/modules/schema/builders.d.ts} +8 -1
  70. package/output/core/modules/schema/builders.d.ts.map +1 -0
  71. package/output/{schema/index.js → core/modules/schema/builders.js} +7 -4
  72. package/output/core/modules/schema/index.d.ts +34 -0
  73. package/output/core/modules/schema/index.d.ts.map +1 -0
  74. package/output/core/modules/schema/index.js +62 -0
  75. package/output/core/modules/schema/index.test.d.ts +2 -0
  76. package/output/core/modules/schema/index.test.d.ts.map +1 -0
  77. package/output/core/modules/schema/index.test.js +55 -0
  78. package/output/core/modules/system-routes/index.d.ts +15 -0
  79. package/output/core/modules/system-routes/index.d.ts.map +1 -0
  80. package/output/core/modules/system-routes/index.js +205 -0
  81. package/output/core/resource.d.ts +56 -0
  82. package/output/core/resource.d.ts.map +1 -0
  83. package/output/core/resource.js +56 -0
  84. package/output/core/test-support/database.d.ts +89 -0
  85. package/output/core/test-support/database.d.ts.map +1 -0
  86. package/output/core/test-support/database.js +131 -0
  87. package/output/core/test-support/database.test.d.ts +2 -0
  88. package/output/core/test-support/database.test.d.ts.map +1 -0
  89. package/output/core/test-support/database.test.js +111 -0
  90. package/output/core/test-support/environment.d.ts +38 -0
  91. package/output/core/test-support/environment.d.ts.map +1 -0
  92. package/output/core/test-support/environment.js +45 -0
  93. package/output/core/test-support/service-context.d.ts +64 -0
  94. package/output/core/test-support/service-context.d.ts.map +1 -0
  95. package/output/core/test-support/service-context.js +63 -0
  96. package/output/core/test-support/service-context.test.d.ts +2 -0
  97. package/output/core/test-support/service-context.test.d.ts.map +1 -0
  98. package/output/core/test-support/service-context.test.js +55 -0
  99. package/output/core/test-support/test-server.d.ts +154 -0
  100. package/output/core/test-support/test-server.d.ts.map +1 -0
  101. package/output/core/test-support/test-server.js +114 -0
  102. package/output/core/test-support/test-server.test.d.ts +2 -0
  103. package/output/core/test-support/test-server.test.d.ts.map +1 -0
  104. package/output/core/test-support/test-server.test.js +296 -0
  105. package/output/core/test-support/testdata.d.ts +255 -0
  106. package/output/core/test-support/testdata.d.ts.map +1 -0
  107. package/output/core/test-support/testdata.js +271 -0
  108. package/output/core/test-support/testdata.test.d.ts +2 -0
  109. package/output/core/test-support/testdata.test.d.ts.map +1 -0
  110. package/output/core/test-support/testdata.test.js +334 -0
  111. package/output/{app → core}/type-augment.d.ts +2 -2
  112. package/output/core/type-augment.d.ts.map +1 -0
  113. package/output/core/types.d.ts +582 -0
  114. package/output/core/types.d.ts.map +1 -0
  115. package/output/cors/index.d.ts +3 -0
  116. package/output/cors/index.d.ts.map +1 -0
  117. package/output/cors/index.js +28 -0
  118. package/output/index.d.ts +10 -4
  119. package/output/index.d.ts.map +1 -1
  120. package/output/index.js +25 -5
  121. package/output/rate-limit/index.d.ts +23 -0
  122. package/output/rate-limit/index.d.ts.map +1 -0
  123. package/output/rate-limit/index.js +43 -0
  124. package/output/{rate-limiter/rate-limiter.d.ts → rate-limit/limiter.d.ts} +1 -1
  125. package/output/rate-limit/limiter.d.ts.map +1 -0
  126. package/output/{rate-limiter → rate-limit}/memory-store.d.ts +6 -0
  127. package/output/rate-limit/memory-store.d.ts.map +1 -0
  128. package/output/{rate-limiter → rate-limit}/memory-store.js +8 -0
  129. package/output/{rate-limiter → rate-limit}/redis-store.d.ts +18 -0
  130. package/output/rate-limit/redis-store.d.ts.map +1 -0
  131. package/output/{rate-limiter → rate-limit}/redis-store.js +35 -0
  132. package/output/rate-limit/redis-store.test.d.ts.map +1 -0
  133. package/output/{rate-limiter → rate-limit}/redis-store.test.js +2 -2
  134. package/output/rate-limit/types.d.ts +45 -0
  135. package/output/rate-limit/types.d.ts.map +1 -0
  136. package/output/rate-limit/types.js +1 -0
  137. package/output/{redis-cache/redis-cache.d.ts → redis/cache.d.ts} +1 -1
  138. package/output/redis/cache.d.ts.map +1 -0
  139. package/output/redis/cache.test.d.ts +2 -0
  140. package/output/redis/cache.test.d.ts.map +1 -0
  141. package/output/{redis-cache/redis-cache.test.js → redis/cache.test.js} +1 -1
  142. package/output/{app/redis.d.ts → redis/index.d.ts} +12 -7
  143. package/output/redis/index.d.ts.map +1 -0
  144. package/output/{app/redis.js → redis/index.js} +15 -5
  145. package/output/redis/index.test.d.ts +2 -0
  146. package/output/{context → redis}/index.test.d.ts.map +1 -1
  147. package/output/redis/index.test.js +55 -0
  148. package/output/redis-queue/index.js +1 -1
  149. package/output/redis-queue/test-helpers.d.ts +1 -1
  150. package/output/redis-queue/test-helpers.js +1 -1
  151. package/output/redis-queue/types.d.ts +3 -12
  152. package/output/redis-queue/types.d.ts.map +1 -1
  153. package/package.json +5 -5
  154. package/output/app/auth.test.d.ts +0 -2
  155. package/output/app/auth.test.d.ts.map +0 -1
  156. package/output/app/auth.test.js +0 -148
  157. package/output/app/boot.d.ts +0 -48
  158. package/output/app/boot.d.ts.map +0 -1
  159. package/output/app/boot.js +0 -123
  160. package/output/app/config.test.d.ts +0 -2
  161. package/output/app/config.test.d.ts.map +0 -1
  162. package/output/app/config.test.js +0 -132
  163. package/output/app/context.d.ts +0 -68
  164. package/output/app/context.d.ts.map +0 -1
  165. package/output/app/context.js +0 -73
  166. package/output/app/core/cookie.d.ts +0 -25
  167. package/output/app/core/cookie.d.ts.map +0 -1
  168. package/output/app/core/identity.d.ts +0 -12
  169. package/output/app/core/identity.d.ts.map +0 -1
  170. package/output/app/core/index.d.ts +0 -44
  171. package/output/app/core/index.d.ts.map +0 -1
  172. package/output/app/core/index.js +0 -57
  173. package/output/app/core/logging.d.ts +0 -13
  174. package/output/app/core/logging.d.ts.map +0 -1
  175. package/output/app/core/openapi.d.ts +0 -17
  176. package/output/app/core/openapi.d.ts.map +0 -1
  177. package/output/app/core/request-id.d.ts.map +0 -1
  178. package/output/app/core/response.d.ts +0 -41
  179. package/output/app/core/response.d.ts.map +0 -1
  180. package/output/app/define-server.negative-cases.d.ts +0 -32
  181. package/output/app/define-server.negative-cases.d.ts.map +0 -1
  182. package/output/app/define-server.negative-cases.js +0 -209
  183. package/output/app/define-server.test.d.ts +0 -2
  184. package/output/app/define-server.test.d.ts.map +0 -1
  185. package/output/app/define-server.test.js +0 -97
  186. package/output/app/entries.d.ts +0 -29
  187. package/output/app/entries.d.ts.map +0 -1
  188. package/output/app/entries.js +0 -64
  189. package/output/app/env.d.ts +0 -59
  190. package/output/app/env.d.ts.map +0 -1
  191. package/output/app/env.js +0 -66
  192. package/output/app/env.test.js +0 -124
  193. package/output/app/error-contract.test.d.ts +0 -2
  194. package/output/app/error-contract.test.d.ts.map +0 -1
  195. package/output/app/error-contract.test.js +0 -125
  196. package/output/app/errors.d.ts +0 -20
  197. package/output/app/errors.d.ts.map +0 -1
  198. package/output/app/errors.js +0 -51
  199. package/output/app/generate-openapi.test.d.ts +0 -2
  200. package/output/app/generate-openapi.test.d.ts.map +0 -1
  201. package/output/app/generate-openapi.test.js +0 -123
  202. package/output/app/lifecycle-logging.test.d.ts +0 -2
  203. package/output/app/lifecycle-logging.test.d.ts.map +0 -1
  204. package/output/app/lifecycle-logging.test.js +0 -155
  205. package/output/app/lifecycle.d.ts +0 -70
  206. package/output/app/lifecycle.d.ts.map +0 -1
  207. package/output/app/lifecycle.js +0 -99
  208. package/output/app/lifecycle.test.d.ts +0 -2
  209. package/output/app/lifecycle.test.d.ts.map +0 -1
  210. package/output/app/lifecycle.test.js +0 -152
  211. package/output/app/middleware/auth.d.ts +0 -71
  212. package/output/app/middleware/auth.d.ts.map +0 -1
  213. package/output/app/middleware/auth.js +0 -190
  214. package/output/app/middleware/context.d.ts +0 -13
  215. package/output/app/middleware/context.d.ts.map +0 -1
  216. package/output/app/middleware/context.js +0 -15
  217. package/output/app/middleware/cors.d.ts +0 -14
  218. package/output/app/middleware/cors.d.ts.map +0 -1
  219. package/output/app/middleware/cors.js +0 -30
  220. package/output/app/middleware/index.d.ts +0 -17
  221. package/output/app/middleware/index.d.ts.map +0 -1
  222. package/output/app/middleware/index.js +0 -16
  223. package/output/app/middleware/rate-limit.d.ts +0 -24
  224. package/output/app/middleware/rate-limit.d.ts.map +0 -1
  225. package/output/app/middleware/rate-limit.js +0 -46
  226. package/output/app/middleware-order.test.d.ts +0 -2
  227. package/output/app/middleware-order.test.d.ts.map +0 -1
  228. package/output/app/middleware-order.test.js +0 -148
  229. package/output/app/permission-operations.d.ts +0 -48
  230. package/output/app/permission-operations.d.ts.map +0 -1
  231. package/output/app/permission-operations.js +0 -77
  232. package/output/app/permission-operations.test.d.ts +0 -2
  233. package/output/app/permission-operations.test.d.ts.map +0 -1
  234. package/output/app/permission-operations.test.js +0 -72
  235. package/output/app/permission-registration.test.d.ts +0 -2
  236. package/output/app/permission-registration.test.d.ts.map +0 -1
  237. package/output/app/permission-registration.test.js +0 -138
  238. package/output/app/redis.d.ts.map +0 -1
  239. package/output/app/registered-operations.test.d.ts +0 -2
  240. package/output/app/registered-operations.test.d.ts.map +0 -1
  241. package/output/app/registered-operations.test.js +0 -57
  242. package/output/app/request-id.test.d.ts +0 -2
  243. package/output/app/request-id.test.d.ts.map +0 -1
  244. package/output/app/request-id.test.js +0 -130
  245. package/output/app/resources.test.d.ts +0 -2
  246. package/output/app/resources.test.d.ts.map +0 -1
  247. package/output/app/resources.test.js +0 -197
  248. package/output/app/shutdown.d.ts +0 -22
  249. package/output/app/shutdown.d.ts.map +0 -1
  250. package/output/app/shutdown.js +0 -30
  251. package/output/app/shutdown.test.d.ts +0 -2
  252. package/output/app/shutdown.test.d.ts.map +0 -1
  253. package/output/app/shutdown.test.js +0 -103
  254. package/output/app/signal.d.ts +0 -12
  255. package/output/app/signal.d.ts.map +0 -1
  256. package/output/app/smoke.test.d.ts +0 -2
  257. package/output/app/smoke.test.d.ts.map +0 -1
  258. package/output/app/smoke.test.js +0 -210
  259. package/output/app/test-support.d.ts +0 -21
  260. package/output/app/test-support.d.ts.map +0 -1
  261. package/output/app/test-support.js +0 -24
  262. package/output/app/type-augment.d.ts.map +0 -1
  263. package/output/app/types.d.ts +0 -211
  264. package/output/app/types.d.ts.map +0 -1
  265. package/output/context/index.d.ts +0 -6
  266. package/output/context/index.d.ts.map +0 -1
  267. package/output/context/index.js +0 -14
  268. package/output/rate-limiter/index.d.ts +0 -5
  269. package/output/rate-limiter/index.d.ts.map +0 -1
  270. package/output/rate-limiter/index.js +0 -4
  271. package/output/rate-limiter/memory-store.d.ts.map +0 -1
  272. package/output/rate-limiter/rate-limiter.d.ts.map +0 -1
  273. package/output/rate-limiter/redis-store.d.ts.map +0 -1
  274. package/output/rate-limiter/redis-store.test.d.ts.map +0 -1
  275. package/output/rate-limiter/types.d.ts +0 -27
  276. package/output/rate-limiter/types.d.ts.map +0 -1
  277. package/output/redis-cache/index.d.ts +0 -4
  278. package/output/redis-cache/index.d.ts.map +0 -1
  279. package/output/redis-cache/index.js +0 -1
  280. package/output/redis-cache/redis-cache.d.ts.map +0 -1
  281. package/output/redis-cache/redis-cache.test.d.ts +0 -2
  282. package/output/redis-cache/redis-cache.test.d.ts.map +0 -1
  283. package/output/schema/index.d.ts.map +0 -1
  284. /package/output/{app → core}/env.test.d.ts +0 -0
  285. /package/output/{rate-limiter → core/modules/auth-provider}/types.js +0 -0
  286. /package/output/{context → core/modules/context}/index.test.d.ts +0 -0
  287. /package/output/{context → core/modules/context}/index.test.js +0 -0
  288. /package/output/{app → core}/type-augment.js +0 -0
  289. /package/output/{app → core}/types.js +0 -0
  290. /package/output/{rate-limiter/rate-limiter.js → rate-limit/limiter.js} +0 -0
  291. /package/output/{rate-limiter → rate-limit}/redis-store.test.d.ts +0 -0
  292. /package/output/{redis-cache/redis-cache.js → redis/cache.js} +0 -0
@@ -0,0 +1,89 @@
1
+ /**
2
+ * 测试数据库安全检查(强制规则见 `@taicode/skills` 的 `test-database-isolation`)。
3
+ *
4
+ * 两道闸门,缺一不可:
5
+ *
6
+ * | 闸门 | 拒绝条件 | 缺失后的后果 |
7
+ * | --- | --- | --- |
8
+ * | 环境闸门 | `ENV` 不是可清理环境 | 生产 runner 上误跑破坏性测试 |
9
+ * | schema 闸门 | 目标 schema 不以 `_test` 结尾 | 绕过命名约定后仍能同步 / 清理应用 schema |
10
+ *
11
+ * 不需要「应用目标 vs 测试目标」比对:测试目标只由 `DATABASE_URL` **派生**得到(schema
12
+ * 追加 `_test`,幂等),再由 schema 闸门只放行 `*_test`。两条合起来,测试目标结构上不可能
13
+ * 是应用目标;再拿两个连接串互相比较,比的也只是同一份派生结果。
14
+ *
15
+ * ## 为什么在框架里
16
+ *
17
+ * 这套逻辑此前在 `account` / `expense` / `release` / `observai` 各写一遍,互相抄、互相漂移。
18
+ * 它保护的是**数据不丢**,属于「项目可以决定怎么实现,但不能决定要不要实现」的那类规则——
19
+ * 所以实现只能有一份,由框架提供;各仓的 `vitest.config.ts` / `global-setup.ts` 只调用,不复制。
20
+ *
21
+ * ## 为什么用 `throw` 而不是 `process.exit`
22
+ *
23
+ * 本模块被两类进程共用:
24
+ *
25
+ * - `createTestServer`(vitest worker 内):退出码由 vitest 决定,抛错即该文件失败;
26
+ * - `vitest.config.ts` / `global-setup.ts`(配置与全局初始化):抛错同样以非零状态结束进程。
27
+ *
28
+ * 抛错在两处都能得到「立刻失败」,而 `process.exit(1)` 在 worker 内会杀掉整个进程池、
29
+ * 丢掉其余文件的报告。错误信息本身必须足够定位,不能靠退出码表达。
30
+ */
31
+ /** 测试 schema 的强制后缀(`account` → `account_test`) */
32
+ export declare const TEST_SCHEMA_SUFFIX = "_test";
33
+ /**
34
+ * 一个连接串的**全部**连接维度。
35
+ *
36
+ * 比较必须覆盖四个维度:只比字符串会误判(`localhost:5432` 与 `localhost:5432/` 不同、
37
+ * `?schema=` 与默认端口都能让「不同字符串」指向同一 schema)。
38
+ */
39
+ export interface DatabaseTarget {
40
+ hostname: string;
41
+ /** 已按默认端口补齐 */
42
+ port: string;
43
+ /** 路径部分(不含开头的 `/`) */
44
+ database: string;
45
+ /** `?schema=`,缺省 `public` */
46
+ schema: string;
47
+ }
48
+ /** 解析连接串的连接维度(解析失败即抛错:配置错误不该被当成「不同的目标」放过去) */
49
+ export declare function resolveDatabaseTarget(connectionString: string): DatabaseTarget;
50
+ /**
51
+ * 打印用的连接串(**密码一律替换为 `***`**)。
52
+ *
53
+ * 报错要能定位「连的是哪儿」,但不能把密码写进 CI 日志。
54
+ */
55
+ export declare function maskDatabaseUrl(connectionString: string): string;
56
+ /**
57
+ * 由应用连接串派生测试连接串:只改 schema,`host` / `port` / `database` 保持与应用一致。
58
+ *
59
+ * 派生是**幂等**的(`account` → `account_test`,`account_test` → `account_test`),
60
+ * 这样重复调用不会产生 `account_test_test`。
61
+ *
62
+ * 应用 schema 本身就以 `_test` 结尾时**直接拒绝**:此时派生结果与应用目标完全相同,测试会
63
+ * 清空应用数据。这是唯一需要「应用目标」才能发现的情况,所以把它固化成派生的一步——不需要
64
+ * 调用方额外传应用连接串、也不需要另开一道闸门。
65
+ */
66
+ export declare function deriveTestDatabaseUrl(appDatabaseUrl: string): string;
67
+ /** 环境闸门(实现在 `core/env.ts`,这里**只转出**:判定只有一份) */
68
+ export { CLEANABLE_ENVS, assertCleanableEnvironment } from '../env.js';
69
+ /** schema 闸门:目标 schema 必须以 `_test` 结尾(在**任何**同步 / 迁移 / 清理之前执行) */
70
+ export declare function assertTestDatabaseSchema(testDatabaseUrl: string): void;
71
+ /** `resolveTestDatabase()` 入参 */
72
+ export interface TestDatabaseResolutionOptions {
73
+ /** 环境标记(`ENV`);非可清理环境直接拒绝 */
74
+ env: string;
75
+ /** 显式测试连接串(`TEST_DATABASE_URL`);优先级最高 */
76
+ testDatabaseUrl?: string;
77
+ }
78
+ /**
79
+ * 结算测试连接串,并**顺序**跑两道闸门:环境 → schema 命名。
80
+ *
81
+ * 顺序是刻意的:环境闸门决定「这台机器能不能做破坏性操作」,schema 闸门决定「目标名字是否
82
+ * 表示测试库」。
83
+ *
84
+ * 派生规则:显式 `testDatabaseUrl` 优先;否则由 `process.env.DATABASE_URL` 派生(schema
85
+ * 追加 `_test`,**幂等**);两者都没有即拒绝——「猜一个连接串」在会清理数据的场景里不可接受。
86
+ */
87
+ export declare function resolveTestDatabase(options: TestDatabaseResolutionOptions): string;
88
+ export declare function parseConnectionString(connectionString: string): URL;
89
+ //# sourceMappingURL=database.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../../source/core/test-support/database.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAIH,kDAAkD;AAClD,eAAO,MAAM,kBAAkB,UAAU,CAAA;AAQzC;;;;;GAKG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAA;IAChB,eAAe;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,sBAAsB;IACtB,QAAQ,EAAE,MAAM,CAAA;IAChB,6BAA6B;IAC7B,MAAM,EAAE,MAAM,CAAA;CACf;AAED,8CAA8C;AAC9C,wBAAgB,qBAAqB,CAAC,gBAAgB,EAAE,MAAM,GAAG,cAAc,CAS9E;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,gBAAgB,EAAE,MAAM,GAAG,MAAM,CAShE;AAED;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM,CAcpE;AAED,+CAA+C;AAC/C,OAAO,EAAE,cAAc,EAAE,0BAA0B,EAAE,MAAM,WAAW,CAAA;AACtE,kEAAkE;AAClE,wBAAgB,wBAAwB,CAAC,eAAe,EAAE,MAAM,GAAG,IAAI,CAUtE;AAED,iCAAiC;AACjC,MAAM,WAAW,6BAA6B;IAC5C,6BAA6B;IAC7B,GAAG,EAAE,MAAM,CAAA;IACX,yCAAyC;IACzC,eAAe,CAAC,EAAE,MAAM,CAAA;CACzB;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,6BAA6B,GAAG,MAAM,CAiBlF;AAED,wBAAgB,qBAAqB,CAAC,gBAAgB,EAAE,MAAM,GAAG,GAAG,CASnE"}
@@ -0,0 +1,131 @@
1
+ /**
2
+ * 测试数据库安全检查(强制规则见 `@taicode/skills` 的 `test-database-isolation`)。
3
+ *
4
+ * 两道闸门,缺一不可:
5
+ *
6
+ * | 闸门 | 拒绝条件 | 缺失后的后果 |
7
+ * | --- | --- | --- |
8
+ * | 环境闸门 | `ENV` 不是可清理环境 | 生产 runner 上误跑破坏性测试 |
9
+ * | schema 闸门 | 目标 schema 不以 `_test` 结尾 | 绕过命名约定后仍能同步 / 清理应用 schema |
10
+ *
11
+ * 不需要「应用目标 vs 测试目标」比对:测试目标只由 `DATABASE_URL` **派生**得到(schema
12
+ * 追加 `_test`,幂等),再由 schema 闸门只放行 `*_test`。两条合起来,测试目标结构上不可能
13
+ * 是应用目标;再拿两个连接串互相比较,比的也只是同一份派生结果。
14
+ *
15
+ * ## 为什么在框架里
16
+ *
17
+ * 这套逻辑此前在 `account` / `expense` / `release` / `observai` 各写一遍,互相抄、互相漂移。
18
+ * 它保护的是**数据不丢**,属于「项目可以决定怎么实现,但不能决定要不要实现」的那类规则——
19
+ * 所以实现只能有一份,由框架提供;各仓的 `vitest.config.ts` / `global-setup.ts` 只调用,不复制。
20
+ *
21
+ * ## 为什么用 `throw` 而不是 `process.exit`
22
+ *
23
+ * 本模块被两类进程共用:
24
+ *
25
+ * - `createTestServer`(vitest worker 内):退出码由 vitest 决定,抛错即该文件失败;
26
+ * - `vitest.config.ts` / `global-setup.ts`(配置与全局初始化):抛错同样以非零状态结束进程。
27
+ *
28
+ * 抛错在两处都能得到「立刻失败」,而 `process.exit(1)` 在 worker 内会杀掉整个进程池、
29
+ * 丢掉其余文件的报告。错误信息本身必须足够定位,不能靠退出码表达。
30
+ */
31
+ import { assertCleanableEnvironment } from '../env.js';
32
+ /** 测试 schema 的强制后缀(`account` → `account_test`) */
33
+ export const TEST_SCHEMA_SUFFIX = '_test';
34
+ /** 连接串未写端口时按协议默认端口补齐后再比较(PostgreSQL 默认 5432) */
35
+ const DEFAULT_POSTGRES_PORT = '5432';
36
+ /** 连接串未写 `?schema=` 时的实际 schema(PostgreSQL 默认 `public`) */
37
+ const DEFAULT_DATABASE_SCHEMA = 'public';
38
+ /** 解析连接串的连接维度(解析失败即抛错:配置错误不该被当成「不同的目标」放过去) */
39
+ export function resolveDatabaseTarget(connectionString) {
40
+ const parsed = parseConnectionString(connectionString);
41
+ return {
42
+ hostname: parsed.hostname,
43
+ port: parsed.port || DEFAULT_POSTGRES_PORT,
44
+ database: parsed.pathname.replace(/^\//, ''),
45
+ schema: parsed.searchParams.get('schema') ?? DEFAULT_DATABASE_SCHEMA,
46
+ };
47
+ }
48
+ /**
49
+ * 打印用的连接串(**密码一律替换为 `***`**)。
50
+ *
51
+ * 报错要能定位「连的是哪儿」,但不能把密码写进 CI 日志。
52
+ */
53
+ export function maskDatabaseUrl(connectionString) {
54
+ const parsed = tryParseConnectionString(connectionString);
55
+ if (!parsed) {
56
+ return '<无法解析的连接串>';
57
+ }
58
+ if (parsed.password) {
59
+ parsed.password = '***';
60
+ }
61
+ return parsed.toString();
62
+ }
63
+ /**
64
+ * 由应用连接串派生测试连接串:只改 schema,`host` / `port` / `database` 保持与应用一致。
65
+ *
66
+ * 派生是**幂等**的(`account` → `account_test`,`account_test` → `account_test`),
67
+ * 这样重复调用不会产生 `account_test_test`。
68
+ *
69
+ * 应用 schema 本身就以 `_test` 结尾时**直接拒绝**:此时派生结果与应用目标完全相同,测试会
70
+ * 清空应用数据。这是唯一需要「应用目标」才能发现的情况,所以把它固化成派生的一步——不需要
71
+ * 调用方额外传应用连接串、也不需要另开一道闸门。
72
+ */
73
+ export function deriveTestDatabaseUrl(appDatabaseUrl) {
74
+ const parsed = parseConnectionString(appDatabaseUrl);
75
+ const schema = parsed.searchParams.get('schema') ?? DEFAULT_DATABASE_SCHEMA;
76
+ if (schema.endsWith(TEST_SCHEMA_SUFFIX)) {
77
+ throw new Error(`[test-database] 应用连接串的 schema 已是 "${schema}"(以 ${TEST_SCHEMA_SUFFIX} 结尾),` +
78
+ `派生出的测试目标会与应用目标完全相同(${maskDatabaseUrl(appDatabaseUrl)})。` +
79
+ '应用运行时 schema 绝不能以 _test 结尾:请修正 DATABASE_URL,而不是放宽守卫。');
80
+ }
81
+ parsed.searchParams.set('schema', `${schema}${TEST_SCHEMA_SUFFIX}`);
82
+ return parsed.toString();
83
+ }
84
+ /** 环境闸门(实现在 `core/env.ts`,这里**只转出**:判定只有一份) */
85
+ export { CLEANABLE_ENVS, assertCleanableEnvironment } from '../env.js';
86
+ /** schema 闸门:目标 schema 必须以 `_test` 结尾(在**任何**同步 / 迁移 / 清理之前执行) */
87
+ export function assertTestDatabaseSchema(testDatabaseUrl) {
88
+ const { schema } = resolveDatabaseTarget(testDatabaseUrl);
89
+ if (schema.endsWith(TEST_SCHEMA_SUFFIX)) {
90
+ return;
91
+ }
92
+ throw new Error(`[test-database] 拒绝使用 schema "${schema}":测试 schema 必须以 ${TEST_SCHEMA_SUFFIX} 结尾` +
93
+ `(当前目标 ${maskDatabaseUrl(testDatabaseUrl)})。`);
94
+ }
95
+ /**
96
+ * 结算测试连接串,并**顺序**跑两道闸门:环境 → schema 命名。
97
+ *
98
+ * 顺序是刻意的:环境闸门决定「这台机器能不能做破坏性操作」,schema 闸门决定「目标名字是否
99
+ * 表示测试库」。
100
+ *
101
+ * 派生规则:显式 `testDatabaseUrl` 优先;否则由 `process.env.DATABASE_URL` 派生(schema
102
+ * 追加 `_test`,**幂等**);两者都没有即拒绝——「猜一个连接串」在会清理数据的场景里不可接受。
103
+ */
104
+ export function resolveTestDatabase(options) {
105
+ assertCleanableEnvironment(options.env);
106
+ const explicit = options.testDatabaseUrl || process.env.TEST_DATABASE_URL;
107
+ const base = process.env.DATABASE_URL;
108
+ const testDatabaseUrl = explicit || (base ? deriveTestDatabaseUrl(base) : '');
109
+ if (!testDatabaseUrl) {
110
+ throw new Error('[test-database] 无法确定测试连接串:TEST_DATABASE_URL 与 DATABASE_URL 都为空。' +
111
+ '会清理数据库的测试必须显式指向一个独立的 *_test schema。');
112
+ }
113
+ assertTestDatabaseSchema(testDatabaseUrl);
114
+ return testDatabaseUrl;
115
+ }
116
+ export function parseConnectionString(connectionString) {
117
+ const parsed = tryParseConnectionString(connectionString);
118
+ if (!parsed) {
119
+ throw new Error(`[test-database] 无法解析连接串 "${connectionString}":测试连接目标必须可读,` +
120
+ '否则无法安全地派生 / 校验。检查 DATABASE_URL / TEST_DATABASE_URL 是否为合法 URL。');
121
+ }
122
+ return parsed;
123
+ }
124
+ function tryParseConnectionString(connectionString) {
125
+ try {
126
+ return new URL(connectionString);
127
+ }
128
+ catch {
129
+ return null;
130
+ }
131
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=database.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"database.test.d.ts","sourceRoot":"","sources":["../../../source/core/test-support/database.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,111 @@
1
+ import { afterEach, describe, expect, it } from 'vitest';
2
+ import { CLEANABLE_ENVS, assertCleanableEnvironment } from '../env.js';
3
+ import { assertTestDatabaseSchema, deriveTestDatabaseUrl, maskDatabaseUrl, resolveDatabaseTarget, resolveTestDatabase, } from './database.js';
4
+ /** 稳定的测试夹具(含密码,用于验证脱敏) */
5
+ const APP_URL = 'postgresql://postgres:secret@db.internal:5432/taicode-labs?schema=account';
6
+ /** 每个用例自行还原 env(`resolveTestDatabase` 会读 `process.env` 作为兜底) */
7
+ const ORIGINAL_ENV = { ...process.env };
8
+ afterEach(() => {
9
+ process.env = { ...ORIGINAL_ENV };
10
+ });
11
+ describe('resolveDatabaseTarget', () => {
12
+ it('解析出四个连接维度(端口与 schema 按协议默认值补齐)', () => {
13
+ expect(resolveDatabaseTarget(APP_URL)).toEqual({
14
+ hostname: 'db.internal',
15
+ port: '5432',
16
+ database: 'taicode-labs',
17
+ schema: 'account',
18
+ });
19
+ });
20
+ it('未写端口时按 5432 补齐(否则 localhost 与 localhost:5432 会被误判为不同目标)', () => {
21
+ expect(resolveDatabaseTarget('postgresql://postgres@localhost/db').port).toBe('5432');
22
+ });
23
+ it('未写 schema 时按 public 补齐', () => {
24
+ expect(resolveDatabaseTarget('postgresql://postgres@localhost/db').schema).toBe('public');
25
+ });
26
+ it('连接串无法解析即抛错(不能把解析失败当成「不同的目标」放过去)', () => {
27
+ expect(() => resolveDatabaseTarget('not-a-url')).toThrow(/无法解析连接串/);
28
+ });
29
+ });
30
+ describe('deriveTestDatabaseUrl', () => {
31
+ it('只改 schema,host / port / database 保持与应用完全一致', () => {
32
+ const derived = resolveDatabaseTarget(deriveTestDatabaseUrl(APP_URL));
33
+ expect(derived).toEqual({
34
+ hostname: 'db.internal',
35
+ port: '5432',
36
+ database: 'taicode-labs',
37
+ schema: 'account_test',
38
+ });
39
+ });
40
+ it('应用 schema 已以 _test 结尾时拒绝:派生结果会与应用目标完全相同', () => {
41
+ // 这是唯一需要「应用目标」才能发现的情况,因此固化成派生的一步。
42
+ expect(() => deriveTestDatabaseUrl('postgresql://postgres:secret@db.internal:5432/taicode-labs?schema=account_test')).toThrow(/派生出的测试目标会与应用目标完全相同/);
43
+ });
44
+ it('保留密码与查询参数里的其它键', () => {
45
+ const derived = new URL(deriveTestDatabaseUrl(`${APP_URL}&sslmode=require`));
46
+ expect(derived.password).toBe('secret');
47
+ expect(derived.searchParams.get('sslmode')).toBe('require');
48
+ });
49
+ });
50
+ describe('maskDatabaseUrl', () => {
51
+ it('隐藏密码(报错要能定位目标,但不能把密码写进 CI 日志)', () => {
52
+ const masked = maskDatabaseUrl(APP_URL);
53
+ expect(masked).not.toContain('secret');
54
+ expect(masked).toContain('***');
55
+ expect(masked).toContain('db.internal');
56
+ });
57
+ it('无法解析时给出占位而不是抛错(它只用于报错信息)', () => {
58
+ expect(maskDatabaseUrl('not-a-url')).toBe('<无法解析的连接串>');
59
+ });
60
+ });
61
+ describe('assertCleanableEnvironment(环境闸门)', () => {
62
+ it.each(CLEANABLE_ENVS)('放行可清理环境 %s', (env) => {
63
+ expect(() => assertCleanableEnvironment(env)).not.toThrow();
64
+ });
65
+ it('拒绝生产环境,并在错误里写明当前值与可接受值', () => {
66
+ expect(() => assertCleanableEnvironment('production')).toThrow(/ENV="production"/);
67
+ });
68
+ it('拒绝未设置的空值(默认放行等于把守卫关掉)', () => {
69
+ expect(() => assertCleanableEnvironment('')).toThrow(/<未设置>/);
70
+ });
71
+ });
72
+ describe('assertTestDatabaseSchema(schema 闸门)', () => {
73
+ it('放行 *_test', () => {
74
+ expect(() => assertTestDatabaseSchema('postgresql://postgres@localhost/db?schema=account_test')).not.toThrow();
75
+ });
76
+ it('拒绝非 *_test(在同步 / 迁移 / 清理之前就失败)', () => {
77
+ expect(() => assertTestDatabaseSchema(APP_URL)).toThrow(/account/);
78
+ });
79
+ });
80
+ describe('resolveTestDatabase(两道闸门串联 + 派生)', () => {
81
+ it('无任何变量时拒绝(猜测连接串在会清理数据的场景里不可接受)', () => {
82
+ delete process.env.DATABASE_URL;
83
+ delete process.env.TEST_DATABASE_URL;
84
+ expect(() => resolveTestDatabase({ env: 'development' })).toThrow(/无法确定测试连接串/);
85
+ });
86
+ it('显式 TEST_DATABASE_URL 优先于派生', () => {
87
+ process.env.DATABASE_URL = APP_URL;
88
+ process.env.TEST_DATABASE_URL = 'postgresql://postgres:secret@db.internal:5432/taicode-labs?schema=explicit_test';
89
+ expect(resolveTestDatabase({ env: 'development' })).toContain('explicit_test');
90
+ });
91
+ it('没有显式变量时由 DATABASE_URL 派生', () => {
92
+ process.env.DATABASE_URL = APP_URL;
93
+ delete process.env.TEST_DATABASE_URL;
94
+ expect(resolveDatabaseTarget(resolveTestDatabase({ env: 'development' })).schema).toBe('account_test');
95
+ });
96
+ it('顺序:环境闸门先于 schema 闸门(生产环境下不先抱怨 schema 名)', () => {
97
+ process.env.TEST_DATABASE_URL = APP_URL;
98
+ expect(() => resolveTestDatabase({ env: 'production' })).toThrow(/拒绝在 ENV="production"/);
99
+ });
100
+ it('通过闸门后返回可用的测试连接串', () => {
101
+ process.env.DATABASE_URL = APP_URL;
102
+ delete process.env.TEST_DATABASE_URL;
103
+ const resolved = resolveTestDatabase({ env: 'development' });
104
+ expect(resolveDatabaseTarget(resolved)).toEqual({
105
+ hostname: 'db.internal',
106
+ port: '5432',
107
+ database: 'taicode-labs',
108
+ schema: 'account_test',
109
+ });
110
+ });
111
+ });
@@ -0,0 +1,38 @@
1
+ import type { ConfigEnv } from '../env.js';
2
+ /**
3
+ * **工具进程**的环境出口。
4
+ *
5
+ * `startServer` / `createTestServer` 覆盖不到的进程(`prisma.config.ts`、`scripts/*.ts`、以及被
6
+ * 它们静态 import 的配置模块)只能自己开口要 env:ES 模块顶层求值早于入口函数执行,所以
7
+ * 「入口里加载 env」救不了这些文件。
8
+ *
9
+ * 只加载、不做任何闸门——运行期配置不该带上测试闸门,那会给正常启动平白加一道无关检查。
10
+ * 幂等:重复调用只是重复读 `dotenv`。
11
+ */
12
+ export declare function loadRuntimeEnvironment(): ConfigEnv;
13
+ /**
14
+ * 结算**测试**数据库目标:加载测试层 → 闸门 → 结算 → 把测试连接串注入返回的 env 快照。
15
+ *
16
+ * **`createTestServer` 的私有步骤,不对外导出。** 导出它等于让调用方自己决定「什么时候跑
17
+ * 闸门、按什么顺序」,而顺序本身就是安全前提。
18
+ *
19
+ * 顺序(固定,不开放重排):
20
+ * ```
21
+ * 加载 .env.dev < .env < .env.local < .env.test < .env.test.local(ENV 未给时兜底 test)
22
+ * → 环境闸门(ENV 必须可清理)
23
+ * → 结算(显式 TEST_DATABASE_URL 优先,否则由 DATABASE_URL 派生 +_test,幂等)
24
+ * → schema 闸门(必须以 _test 结尾)
25
+ * → 把测试连接串写进**返回的 env 快照**(不碰全局 process.env)
26
+ * ```
27
+ *
28
+ * **为什么不再改写 `process.env.DATABASE_URL`**:那是进程级副作用——同一进程里并行跑多个
29
+ * `createTestServer`(或测试与被测代码共享进程)时会互相覆写,谁最后写谁赢。测试目标应当
30
+ * 只属于**本次装配**:框架把它注入到返回的 `env` 快照里,`resources` 槽位从 `ctx.env.DATABASE_URL`
31
+ * 读取(框架契约:资源只读 `ctx.env`,不读 `process.env`),因此每个实例各连各的,互不干扰。
32
+ *
33
+ * 隔离比较不在这里:返回的 `env.DATABASE_URL` 此时就是测试目标,比对恒等于「自比自」。
34
+ * 防止测试清空应用数据靠的是**派生 + `_test` schema 闸门**——派生只会追加后缀,schema 闸门
35
+ * 只放行 `*_test`,所以测试目标结构上不可能是应用目标。
36
+ */
37
+ export declare function prepareTestDatabase(): ConfigEnv;
38
+ //# sourceMappingURL=environment.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"environment.d.ts","sourceRoot":"","sources":["../../../source/core/test-support/environment.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAE1C;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,IAAI,SAAS,CAElD;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,mBAAmB,IAAI,SAAS,CAO/C"}
@@ -0,0 +1,45 @@
1
+ import { loadEnvironment, resolveConfigEnv } from '../env.js';
2
+ import { resolveTestDatabase } from './database.js';
3
+ /**
4
+ * **工具进程**的环境出口。
5
+ *
6
+ * `startServer` / `createTestServer` 覆盖不到的进程(`prisma.config.ts`、`scripts/*.ts`、以及被
7
+ * 它们静态 import 的配置模块)只能自己开口要 env:ES 模块顶层求值早于入口函数执行,所以
8
+ * 「入口里加载 env」救不了这些文件。
9
+ *
10
+ * 只加载、不做任何闸门——运行期配置不该带上测试闸门,那会给正常启动平白加一道无关检查。
11
+ * 幂等:重复调用只是重复读 `dotenv`。
12
+ */
13
+ export function loadRuntimeEnvironment() {
14
+ return loadEnvironment();
15
+ }
16
+ /**
17
+ * 结算**测试**数据库目标:加载测试层 → 闸门 → 结算 → 把测试连接串注入返回的 env 快照。
18
+ *
19
+ * **`createTestServer` 的私有步骤,不对外导出。** 导出它等于让调用方自己决定「什么时候跑
20
+ * 闸门、按什么顺序」,而顺序本身就是安全前提。
21
+ *
22
+ * 顺序(固定,不开放重排):
23
+ * ```
24
+ * 加载 .env.dev < .env < .env.local < .env.test < .env.test.local(ENV 未给时兜底 test)
25
+ * → 环境闸门(ENV 必须可清理)
26
+ * → 结算(显式 TEST_DATABASE_URL 优先,否则由 DATABASE_URL 派生 +_test,幂等)
27
+ * → schema 闸门(必须以 _test 结尾)
28
+ * → 把测试连接串写进**返回的 env 快照**(不碰全局 process.env)
29
+ * ```
30
+ *
31
+ * **为什么不再改写 `process.env.DATABASE_URL`**:那是进程级副作用——同一进程里并行跑多个
32
+ * `createTestServer`(或测试与被测代码共享进程)时会互相覆写,谁最后写谁赢。测试目标应当
33
+ * 只属于**本次装配**:框架把它注入到返回的 `env` 快照里,`resources` 槽位从 `ctx.env.DATABASE_URL`
34
+ * 读取(框架契约:资源只读 `ctx.env`,不读 `process.env`),因此每个实例各连各的,互不干扰。
35
+ *
36
+ * 隔离比较不在这里:返回的 `env.DATABASE_URL` 此时就是测试目标,比对恒等于「自比自」。
37
+ * 防止测试清空应用数据靠的是**派生 + `_test` schema 闸门**——派生只会追加后缀,schema 闸门
38
+ * 只放行 `*_test`,所以测试目标结构上不可能是应用目标。
39
+ */
40
+ export function prepareTestDatabase() {
41
+ const loaded = loadEnvironment({ mode: 'test' });
42
+ const databaseUrl = resolveTestDatabase({ env: loaded.ENV });
43
+ // 只注入到本次装配的 env 快照,**不改写全局 process.env.DATABASE_URL**(避免进程级串扰)
44
+ return { ...resolveConfigEnv(), DATABASE_URL: databaseUrl };
45
+ }
@@ -0,0 +1,64 @@
1
+ import type { ClientsView, LoggerLike, ResourcesView, ServiceContext } from '../types.js';
2
+ /**
3
+ * `createTestServiceContext()` —— **单元级**测试构造 `services` 槽位入参的唯一方式。
4
+ *
5
+ * ## 为什么框架要给这个出口
6
+ *
7
+ * `createTestServer()` 是**集成**测试的唯一入口:它跑全量装配(resources → config →
8
+ * clients → services → routers),代价是「每条用例都要连真库、装全部路由」。只想验一个
9
+ * service 函数的判断逻辑时,这个代价与收益不成比例——于是各仓各自写了一份
10
+ * `createServiceContext(fakeDb, { ... })`,还都带着同一句
11
+ * `as unknown as AccountServiceContext`:**同一个绕过动作被抄了四遍**,说明缺的是
12
+ * 框架能力,不是各仓的疏忽。
13
+ *
14
+ * 这里的取舍是明确的:**不伪造能力,只允许缺省能力**。
15
+ * - 传进来的 `resources` / `clients` 是**真实对象**(真 reader、真 db 客户端、注入式
16
+ * 替身),不是 mock 框架的代理——本函数不生成任何行为,只做装配。
17
+ * - 没传的能力被访问时**抛错**(错误信息点名是哪个键),而不是返回 `undefined` 让调用方
18
+ * 的 `undefined.xxx` 在更远处炸开。这区分了「这个单元不该碰数据库」与「忘了给数据库」
19
+ * 两种完全不同的失败。
20
+ *
21
+ * ## 为什么不干脆把 `ctx` 收窄成「这个服务用到的字段」
22
+ *
23
+ * 那会让「服务工厂第一参数恒为 ctx」这条约定退化:每个工厂的入参类型各不相同,装配层
24
+ * 就无法把一份 ctx 原样传下去(`createServices` 得为每个服务裁一次类型)。约定要留,
25
+ * 代价就由本函数承担——它给出**形状完整、能力可缺省**的 ctx。
26
+ *
27
+ * ## `resources.<key>` 为什么用 Proxy 而不是浅合并空对象
28
+ *
29
+ * 浅合并只能发现问题的一半:`resources.database` 变成了 `undefined`,错误发生在
30
+ * `undefined.user.findMany()` 上,栈里全都是业务代码,看不出根因是「这份 ctx 没给
31
+ * database」。Proxy 让错误在**取字段的那一行**抛出并点名,测试写错时的定位成本从
32
+ * 「翻业务代码」降到「看一行错误」。
33
+ */
34
+ export interface TestServiceContextOptions<Resources, Config, Clients> {
35
+ /**
36
+ * 提供被测单元**实际读到**的资源能力(其余键访问即抛错)。
37
+ *
38
+ * 传真实实例:真 `PrismaClient`、`createSystemConfigReader(db)`,或对**外部 I/O
39
+ * 边界**(第三方 HTTP、邮件网关)注入的替身。不要替身框架自己的装配。
40
+ */
41
+ resources?: Partial<ResourcesView<Resources>>;
42
+ /** 提供被测单元**实际读到**的客户端(其余键访问即抛错) */
43
+ clients?: Partial<ClientsView<Clients>>;
44
+ /** `config` 产物;被测单元按契约不读它时省略(读取会抛错,不会拿到 `undefined`) */
45
+ config?: Awaited<Config>;
46
+ /** 日志出口覆盖(缺省为静默 no-op;断言日志时传 `vi.fn()` 组成的实现) */
47
+ log?: Partial<LoggerLike>;
48
+ }
49
+ /**
50
+ * 构造 `services` 槽位的测试入参(`ServiceContext`)。
51
+ *
52
+ * ```ts
53
+ * const ctx = createTestServiceContext<AccountResources, AccountConfig>({
54
+ * resources: { systemConfig: fakeReader },
55
+ * log: { warn: warnSpy },
56
+ * })
57
+ * const service = createRuntimeConfigService(ctx)
58
+ * ```
59
+ *
60
+ * 返回的 `disposer` 是**真实** `Disposer`:被测单元登记的释放函数会真的被收集,
61
+ * 测试可以用 `await ctx.disposer.dispose()` 验证「服务自己登记了释放」这条契约。
62
+ */
63
+ export declare function createTestServiceContext<Resources, Config, Clients = unknown>(options?: TestServiceContextOptions<Resources, Config, Clients>): ServiceContext<Resources, Config, Clients>;
64
+ //# sourceMappingURL=service-context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-context.d.ts","sourceRoot":"","sources":["../../../source/core/test-support/service-context.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAEzF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,MAAM,WAAW,yBAAyB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO;IACnE;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC,CAAA;IAC7C,mCAAmC;IACnC,OAAO,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,CAAA;IACvC,wDAAwD;IACxD,MAAM,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,CAAA;IACxB,iDAAiD;IACjD,GAAG,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,CAAA;CAC1B;AAUD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,wBAAwB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,GAAG,OAAO,EAC3E,OAAO,GAAE,yBAAyB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAM,GAClE,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,CAoB5C"}
@@ -0,0 +1,63 @@
1
+ import { Disposer } from '@taicode/common-base';
2
+ /** 缺省日志:静默。测试不该因为「服务打了一行 info」而需要准备 logger。 */
3
+ const SILENT_LOG = {
4
+ debug: () => undefined,
5
+ info: () => undefined,
6
+ warn: () => undefined,
7
+ error: () => undefined,
8
+ };
9
+ /**
10
+ * 构造 `services` 槽位的测试入参(`ServiceContext`)。
11
+ *
12
+ * ```ts
13
+ * const ctx = createTestServiceContext<AccountResources, AccountConfig>({
14
+ * resources: { systemConfig: fakeReader },
15
+ * log: { warn: warnSpy },
16
+ * })
17
+ * const service = createRuntimeConfigService(ctx)
18
+ * ```
19
+ *
20
+ * 返回的 `disposer` 是**真实** `Disposer`:被测单元登记的释放函数会真的被收集,
21
+ * 测试可以用 `await ctx.disposer.dispose()` 验证「服务自己登记了释放」这条契约。
22
+ */
23
+ export function createTestServiceContext(options = {}) {
24
+ // 释放器是真实的:登记 / 逆序执行都是被测契约的一部分,不能替身掉
25
+ const disposer = new Disposer({ label: 'test-service-context', log: SILENT_LOG });
26
+ return {
27
+ log: { ...SILENT_LOG, ...options.log },
28
+ disposer,
29
+ resources: requireProvided(options.resources, 'resources'),
30
+ clients: requireProvided(options.clients, 'clients'),
31
+ get config() {
32
+ if (options.config === undefined) {
33
+ throw new Error('[test-service-context] 这份 ctx 没有提供 config:' +
34
+ '被测单元按契约不读 config(运行期配置一律经 resources.systemConfig)。' +
35
+ '若确实要读,请通过 createTestServiceContext({ config }) 显式给出。');
36
+ }
37
+ return options.config;
38
+ },
39
+ };
40
+ }
41
+ /**
42
+ * 把「部分提供」包装成「形状完整、缺省即抛错」的视图。
43
+ *
44
+ * 只在**读取被缺省的键**时抛错:传了的键行为与真实视图完全一致(同一个对象、同一份
45
+ * 引用),所以注入式替身与真实例都照常工作。
46
+ */
47
+ function requireProvided(provided, label) {
48
+ const source = provided ?? {};
49
+ return new Proxy(source, {
50
+ get(target, key, receiver) {
51
+ // Symbol(`Symbol.toStringTag`、`then` 探测等)与 `in` 都按普通对象处理
52
+ if (typeof key === 'symbol') {
53
+ return Reflect.get(target, key, receiver);
54
+ }
55
+ if (!(key in target)) {
56
+ throw new Error(`[test-service-context] 这份 ctx 没有提供 ${label}.${key}:` +
57
+ `被测单元读到了它,说明它确实依赖这项能力——请通过 createTestServiceContext({ ${label}: { ${key}: … } }) 给出` +
58
+ '(真实实例或对外部 I/O 边界的替身,不要替身框架自身的装配)。');
59
+ }
60
+ return Reflect.get(target, key, receiver);
61
+ },
62
+ });
63
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=service-context.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-context.test.d.ts","sourceRoot":"","sources":["../../../source/core/test-support/service-context.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,55 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { createTestServiceContext } from './service-context.js';
3
+ describe('createTestServiceContext', () => {
4
+ it('提供的资源与客户端原样可用(同一个引用,不做拷贝)', () => {
5
+ const database = { user: { count: async () => 3 } };
6
+ const cache = { get: () => undefined };
7
+ const ctx = createTestServiceContext({
8
+ resources: { database },
9
+ clients: { cache },
10
+ });
11
+ expect(ctx.resources.database).toBe(database);
12
+ expect(ctx.clients.cache).toBe(cache);
13
+ });
14
+ it('读取被缺省的能力时当场抛错,错误点名是哪个键', () => {
15
+ const ctx = createTestServiceContext({
16
+ resources: { database: { user: { count: async () => 0 } } },
17
+ });
18
+ // 提供的键正常
19
+ expect(ctx.resources.database).toBeDefined();
20
+ // 未提供的键:取字段那一步就抛,而不是返回 undefined 留到业务代码里炸
21
+ expect(() => ctx.resources.systemConfig).toThrowError(/resources\.systemConfig/);
22
+ });
23
+ it('缺省能力抛错信息里给出补法(createTestServiceContext({ resources: { … } }))', () => {
24
+ const ctx = createTestServiceContext();
25
+ expect(() => ctx.clients.anything).toThrowError(/createTestServiceContext\(\{ clients: \{ anything: … \} \}\)/);
26
+ });
27
+ it('未给 config 时读取抛错(不会拿到 undefined)', () => {
28
+ const ctx = createTestServiceContext();
29
+ expect(() => ctx.config).toThrowError(/没有提供 config/);
30
+ });
31
+ it('给了 config 时按传入值返回', () => {
32
+ const ctx = createTestServiceContext({
33
+ config: { featureEnabled: true },
34
+ });
35
+ expect(ctx.config).toEqual({ featureEnabled: true });
36
+ });
37
+ it('disposer 是真实的:登记的清理会被收集并按逆序执行', async () => {
38
+ const ctx = createTestServiceContext();
39
+ const order = [];
40
+ // 模拟「服务在构造期自己登记释放」这条契约
41
+ ctx.disposer.add(() => order.push('first'));
42
+ ctx.disposer.add(() => order.push('second'));
43
+ await ctx.disposer.dispose();
44
+ expect(order).toEqual(['second', 'first']);
45
+ });
46
+ it('日志缺省静默;覆盖后能断言调用', () => {
47
+ const warn = vi.fn();
48
+ const silent = createTestServiceContext();
49
+ const spying = createTestServiceContext({ log: { warn } });
50
+ // 缺省实现不抛错(pino 之外不要强制测试准备 logger)
51
+ expect(() => silent.log.warn({ a: 1 }, 'hello')).not.toThrow();
52
+ spying.log.warn({ a: 1 }, 'hello');
53
+ expect(warn).toHaveBeenCalledWith({ a: 1 }, 'hello');
54
+ });
55
+ });