@taicode/common-server 4.0.0-beta.6 → 4.0.0-beta.7

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.
@@ -7,6 +7,7 @@ export { createAuthMiddleware } from '../auth/index.js';
7
7
  export { createRateLimitMiddleware } from '../rate-limit/index.js';
8
8
  export { createRedis, createCache } from '../redis/index.js';
9
9
  export { defineResource } from '../core/resource.js';
10
+ export type { TestDataContext, TestDataRunParams, TestDataScript, TestDataUnit } from '../core/test-support/testdata.js';
10
11
  export type { AppEnv, AppInstance, ClientsContext, ClientsFactory, ClientsView, ConfigContext, LoggerLike, Middleware, MiddlewareContext, MiddlewareEntry, MiddlewareUnit, ResourceUnit, ResourcesContext, ResourcesView, RouterContext, ServiceContext, } from '../core/types.js';
11
12
  export type { AuthVerifyInput, AuthVerifyMetadata, AuthVerifyResult } from '../auth/index.js';
12
13
  export type { AuthMiddlewareOptions } from '../auth/index.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../source/app/index.ts"],"names":[],"mappings":"AA8HA,OAAO,yBAAyB,CAAA;AAGhC,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AAiE/E,OAAO,EAAE,gBAAgB,EAAE,MAAM,qCAAqC,CAAA;AAyBtE,OAAO,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAA;AAiC5E,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAA;AACvD,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAA;AACvD,OAAO,EAAE,yBAAyB,EAAE,MAAM,wBAAwB,CAAA;AAiBlE,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAsB5D,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AAGpD,YAAY,EACV,MAAM,EACN,WAAW,EACX,cAAc,EACd,cAAc,EACd,WAAW,EACX,aAAa,EACb,UAAU,EACV,UAAU,EACV,iBAAiB,EACjB,eAAe,EACf,cAAc,EACd,YAAY,EACZ,gBAAgB,EAChB,aAAa,EACb,aAAa,EACb,cAAc,GACf,MAAM,kBAAkB,CAAA;AAEzB,YAAY,EAAE,eAAe,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AAC7F,YAAY,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAA;AAC7D,YAAY,EACV,UAAU,EACV,iBAAiB,EACjB,mCAAmC,GACpC,MAAM,wCAAwC,CAAA;AAC/C,YAAY,EAAE,0BAA0B,EAAE,MAAM,wBAAwB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../source/app/index.ts"],"names":[],"mappings":"AA8HA,OAAO,yBAAyB,CAAA;AAGhC,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AAiE/E,OAAO,EAAE,gBAAgB,EAAE,MAAM,qCAAqC,CAAA;AAyBtE,OAAO,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAA;AAiC5E,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAA;AACvD,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAA;AACvD,OAAO,EAAE,yBAAyB,EAAE,MAAM,wBAAwB,CAAA;AAiBlE,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAsB5D,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AAiBpD,YAAY,EAAE,eAAe,EAAE,iBAAiB,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAA;AAGxH,YAAY,EACV,MAAM,EACN,WAAW,EACX,cAAc,EACd,cAAc,EACd,WAAW,EACX,aAAa,EACb,UAAU,EACV,UAAU,EACV,iBAAiB,EACjB,eAAe,EACf,cAAc,EACd,YAAY,EACZ,gBAAgB,EAChB,aAAa,EACb,aAAa,EACb,cAAc,GACf,MAAM,kBAAkB,CAAA;AAEzB,YAAY,EAAE,eAAe,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AAC7F,YAAY,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAA;AAC7D,YAAY,EACV,UAAU,EACV,iBAAiB,EACjB,mCAAmC,GACpC,MAAM,wCAAwC,CAAA;AAC/C,YAAY,EAAE,0BAA0B,EAAE,MAAM,wBAAwB,CAAA"}
@@ -284,6 +284,6 @@ export { createRedis, createCache } from '../redis/index.js';
284
284
  //
285
285
  // 「用例之间重置数据」(清表、清缓存、重新插入样本数据)**不在资源上**:资源只知道有哪些表 /
286
286
  // 哪些 key,不知道谁往里写了什么。它由一套独立机制承担:`packages/server/testdata/*.ts|js`,
287
- // 每个脚本自带 `load()` / `cleanup()`,框架在 `development` / `test` 下自动装载并执行。
287
+ // 每个脚本自带 `load(ctx)` / `cleanup(ctx)`,框架在 `development` / `test` 下自动装载并执行。
288
288
  // 详见 `createTestServer` 的 `resetTestData()`。
289
289
  export { defineResource } from '../core/resource.js';
@@ -63,7 +63,7 @@ export declare function defineServer<Env = object, Resources = object, Config =
63
63
  * 步序**就写在本函数里**,不抽到共享的「按模式分支」的装配器:
64
64
  * ```
65
65
  * env → resources → core → config → clients → services → services.init() → middlewares
66
- * → systemRoutes → routers → onClose → ready → listen/signal
66
+ * → systemRoutes → routers → onClose → ready → testdata → listen/signal
67
67
  * ```
68
68
  *
69
69
  * - 最早阶段**固定加载 env**(`.env.dev < .env < .env.local`;`ENV=test` 追加 `.env.test.local`)。
@@ -75,10 +75,11 @@ export declare function defineServer<Env = object, Resources = object, Config =
75
75
  *
76
76
  * ## `development` / `test` 下会自动装载 `testdata/`
77
77
  *
78
- * 真启动(本地 `pnpm dev`)在 listen 成功之后、装信号处理器之前,装载 `<cwd>/testdata` 下的
79
- * 脚本并整体 `load()`(见 `test-support/testdata.ts`)。`ENV` 不在 `CLEANABLE_ENVS` 里
80
- * (即生产)时**整段跳过**——这是 `startServer` 的唯一一处环境分支,且是安静的:生产不需要
81
- * 测试数据,也没有什么东西需要失败。
78
+ * 真启动(本地 `pnpm dev`)在 `ready()` 之后、**listen 之前**装载 `<cwd>/testdata` 下的脚本并整体
79
+ * `load()`(见 `test-support/testdata.ts`):端口一旦对外,数据必须已经在位。脚本拿到的是装配好的
80
+ * ctx(`env` + `resources` + `log`),所以它们写数据用的就是被测服务读数据的那份连接。
81
+ * `ENV` 不在 `CLEANABLE_ENVS` 里(即生产)时**整段跳过**——这是 `startServer` 的唯一一处环境
82
+ * 分支,且是安静的:生产不需要测试数据,也没有什么东西需要失败。
82
83
  *
83
84
  * `cleanup()` 在 `startServer` 路径上不会被调用:真启动要的就是「数据在」,不是「数据不在」。
84
85
  *
@@ -1 +1 @@
1
- {"version":3,"file":"entries.d.ts","sourceRoot":"","sources":["../../source/core/entries.ts"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAAU,eAAe,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAC/E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAc9C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AACH,wBAAgB,YAAY,CAAC,GAAG,GAAG,MAAM,EAAE,SAAS,GAAG,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,OAAO,GAAG,MAAM,EAAE,QAAQ,GAAG,MAAM,EACjH,MAAM,EAAE,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,CAAC,GAC9D,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,CAAC,CAMnD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,wBAAsB,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,CAAC,GAAG,OAAO,CAAC,eAAe,CAAC,CAgDhK;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,wBAAsB,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,CAAC,GAAG,OAAO,CAAC,eAAe,CAAC,CAsBpK"}
1
+ {"version":3,"file":"entries.d.ts","sourceRoot":"","sources":["../../source/core/entries.ts"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAAU,eAAe,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAC/E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAc9C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AACH,wBAAgB,YAAY,CAAC,GAAG,GAAG,MAAM,EAAE,SAAS,GAAG,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,OAAO,GAAG,MAAM,EAAE,QAAQ,GAAG,MAAM,EACjH,MAAM,EAAE,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,CAAC,GAC9D,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,CAAC,CAMnD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,wBAAsB,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,CAAC,GAAG,OAAO,CAAC,eAAe,CAAC,CAoDhK;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,wBAAsB,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,CAAC,GAAG,OAAO,CAAC,eAAe,CAAC,CAsBpK"}
@@ -3,7 +3,7 @@ import { attachSignalHandlers } from './boot/signal.js';
3
3
  import { buildClients, buildConfig, buildResources, buildServices, createBootContext, createDocumentContext, installShutdownHook, registerCoreStep, registerDocumentCoreStep, registerDocumentRoutersStep, registerDocumentSystemRoutesStep, registerMiddlewares, registerRouters, registerSystemRoutesStep, resolveAppEnv, setupResources, } from './boot/steps.js';
4
4
  import { loadEnvironment, resolveConfigEnv, CLEANABLE_ENVS } from './env.js';
5
5
  import { SERVER_BRAND } from './types.js';
6
- import { discoverTestDataModules, loadTestData, resolveTestDataDirectory } from './test-support/testdata.js';
6
+ import { discoverTestDataModules, loadTestData } from './test-support/testdata.js';
7
7
  /** 默认监听地址 */
8
8
  const DEFAULT_HOST = '0.0.0.0';
9
9
  function resolvePort(env) {
@@ -83,7 +83,7 @@ export function defineServer(config) {
83
83
  * 步序**就写在本函数里**,不抽到共享的「按模式分支」的装配器:
84
84
  * ```
85
85
  * env → resources → core → config → clients → services → services.init() → middlewares
86
- * → systemRoutes → routers → onClose → ready → listen/signal
86
+ * → systemRoutes → routers → onClose → ready → testdata → listen/signal
87
87
  * ```
88
88
  *
89
89
  * - 最早阶段**固定加载 env**(`.env.dev < .env < .env.local`;`ENV=test` 追加 `.env.test.local`)。
@@ -95,10 +95,11 @@ export function defineServer(config) {
95
95
  *
96
96
  * ## `development` / `test` 下会自动装载 `testdata/`
97
97
  *
98
- * 真启动(本地 `pnpm dev`)在 listen 成功之后、装信号处理器之前,装载 `<cwd>/testdata` 下的
99
- * 脚本并整体 `load()`(见 `test-support/testdata.ts`)。`ENV` 不在 `CLEANABLE_ENVS` 里
100
- * (即生产)时**整段跳过**——这是 `startServer` 的唯一一处环境分支,且是安静的:生产不需要
101
- * 测试数据,也没有什么东西需要失败。
98
+ * 真启动(本地 `pnpm dev`)在 `ready()` 之后、**listen 之前**装载 `<cwd>/testdata` 下的脚本并整体
99
+ * `load()`(见 `test-support/testdata.ts`):端口一旦对外,数据必须已经在位。脚本拿到的是装配好的
100
+ * ctx(`env` + `resources` + `log`),所以它们写数据用的就是被测服务读数据的那份连接。
101
+ * `ENV` 不在 `CLEANABLE_ENVS` 里(即生产)时**整段跳过**——这是 `startServer` 的唯一一处环境
102
+ * 分支,且是安静的:生产不需要测试数据,也没有什么东西需要失败。
102
103
  *
103
104
  * `cleanup()` 在 `startServer` 路径上不会被调用:真启动要的就是「数据在」,不是「数据不在」。
104
105
  *
@@ -142,6 +143,19 @@ export async function startServer(server) {
142
143
  detachSignalHandlers();
143
144
  });
144
145
  await ctx.app.ready();
146
+ // 服务已经完全就绪(路由 / 中间件 / 关闭钩子都装好),现在把开发用样本数据写进去,然后才开门。
147
+ // 顺序是**装配 → 数据 → listen**:端口一旦对外,第一条请求就必须能看到完整数据;反过来
148
+ // (先 listen 再 load)会留下一段「端口在响应、数据还没就位」的窗口——本地前端 / 集成脚本
149
+ // 恰好在这时请求,就是偶发的 404 / 空列表。
150
+ //
151
+ // 失败直接冒泡给调用方:`testdata/` 的脚本是服务能正常提供数据的前提,它失败时**不该**把端口
152
+ // 占着等人发现。非可清理环境(生产)整段跳过——这是本入口唯一的环境分支。
153
+ if (CLEANABLE_ENVS.includes(env.ENV)) {
154
+ const units = await discoverTestDataModules();
155
+ if (units.length > 0) {
156
+ await loadTestData({ ctx: { env, resources, log: ctx.log }, units, service: ctx.service });
157
+ }
158
+ }
145
159
  try {
146
160
  await ctx.app.listen({ port: resolvePort(env), host: DEFAULT_HOST });
147
161
  }
@@ -151,15 +165,6 @@ export async function startServer(server) {
151
165
  throw error;
152
166
  }
153
167
  detachSignalHandlers = attachSignalHandlers(ctx.app, ctx.log);
154
- // 本地开发(非测试入口)也拿同一批测试数据:`load()` 排在 listen 之后、信号处理器之后,
155
- // 失败也不会把已就绪的服务卡在「端口占着、数据没就绪」的状态。
156
- // 非可清理环境(生产)整段跳过——这是本入口唯一的环境分支。
157
- if (CLEANABLE_ENVS.includes(env.ENV)) {
158
- const units = await discoverTestDataModules(resolveTestDataDirectory());
159
- if (units.length > 0) {
160
- await loadTestData({ env: env.ENV, units, service: ctx.service, log: ctx.log });
161
- }
162
- }
163
168
  return ctx.app;
164
169
  }
165
170
  /**
@@ -78,9 +78,9 @@ export interface TestServer<Resources, Config, Clients, Services, Env> {
78
78
  * 目录不存在则为空数组)。
79
79
  *
80
80
  * 装配时已经自动 `load()` 过一次。需要单独的细粒度控制时用这里列的脚本自跑
81
- * `loadTestData({ env: testServer.env.ENV, units })`;绝大多数用例只需 `resetTestData()`。
81
+ * `loadTestData({ ctx, units })`(`ctx` 见 `TestDataContext`);绝大多数用例只需 `resetTestData()`。
82
82
  */
83
- readonly testData: readonly TestDataUnit[];
83
+ readonly testData: readonly TestDataUnit<Resources, Env>[];
84
84
  /**
85
85
  * 重置测试数据:先整体 `cleanup`(文件名**逆序**),再整体 `load`(**正序**)。
86
86
  *
@@ -108,12 +108,12 @@ export interface TestServer<Resources, Config, Clients, Services, Env> {
108
108
  * → 回写 process.env.DATABASE_URL
109
109
  * → resources → core → config → clients → services → init() → middlewares
110
110
  * → 系统路由 → routers → 关闭钩子 → ready()
111
- * → 装载 testdata/ 脚本并整体 load()
111
+ * → 装载 testdata/ 脚本并整体 load(ctx)
112
112
  * ```
113
113
  *
114
- * `testdata/` 的 `load()` 排在 `ready()` 之后有一个硬性理由:脚本用的是被装配出来的真实资源
115
- * (资源模块自己建连接、自己连库),而仓库里不提供「从这里拿资源」的通道——数据脚本应当
116
- * 自己建自己那份客户端,所以它只依赖 env。排在最后也让失败语义最干净:装载失败时
114
+ * `testdata/` 的 `load()` 排在 `ready()` 之后:脚本拿到的是装配好的 `ctx`(`env` + `resources` +
115
+ * `log`),所以它写数据用的就是被测服务读数据的那份连接——两套连接写/读同一张表而看不到对方
116
+ * 写入(缓存 / 连接级状态)这类问题从结构上不会发生。排在最后也让失败语义最干净:装载失败时
117
117
  * 装配已完成、可正常 `app.close()` 释放。
118
118
  *
119
119
  * 不会再拿应用目标跟测试目标比对:本进程读到的 `DATABASE_URL` 此时已经是测试目标,比对恒等于
@@ -1 +1 @@
1
- {"version":3,"file":"test-server.d.ts","sourceRoot":"","sources":["../../../source/core/test-support/test-server.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AACjD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAE9C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AACH,MAAM,WAAW,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG;IACnE,kEAAkE;IAClE,QAAQ,CAAC,GAAG,EAAE,eAAe,CAAA;IAC7B;;;;;;OAMG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,CAAA;IACzB,qDAAqD;IACrD,QAAQ,CAAC,SAAS,EAAE,aAAa,CAAC,SAAS,CAAC,CAAA;IAC5C,oCAAoC;IACpC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,CAAA;IAChC,qBAAqB;IACrB,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,OAAO,CAAC,CAAA;IACtC,qDAAqD;IACrD,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAA;IACpC,mCAAmC;IACnC,QAAQ,CAAC,GAAG,EAAE,UAAU,CAAA;IACxB;;;;;;OAMG;IACH,QAAQ,CAAC,QAAQ,EAAE,SAAS,YAAY,EAAE,CAAA;IAC1C;;;;;;;;;;;OAWG;IACH,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CAC/B;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAsB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,CAAC,GAAG,OAAO,CAAC,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,CAAC,CAAC,CAwD3M"}
1
+ {"version":3,"file":"test-server.d.ts","sourceRoot":"","sources":["../../../source/core/test-support/test-server.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzF,OAAO,KAAK,EAAmB,YAAY,EAAE,MAAM,eAAe,CAAA;AAClE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAE9C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AACH,MAAM,WAAW,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG;IACnE,kEAAkE;IAClE,QAAQ,CAAC,GAAG,EAAE,eAAe,CAAA;IAC7B;;;;;;OAMG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,CAAA;IACzB,qDAAqD;IACrD,QAAQ,CAAC,SAAS,EAAE,aAAa,CAAC,SAAS,CAAC,CAAA;IAC5C,oCAAoC;IACpC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,CAAA;IAChC,qBAAqB;IACrB,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,OAAO,CAAC,CAAA;IACtC,qDAAqD;IACrD,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAA;IACpC,mCAAmC;IACnC,QAAQ,CAAC,GAAG,EAAE,UAAU,CAAA;IACxB;;;;;;OAMG;IACH,QAAQ,CAAC,QAAQ,EAAE,SAAS,YAAY,CAAC,SAAS,EAAE,GAAG,CAAC,EAAE,CAAA;IAC1D;;;;;;;;;;;OAWG;IACH,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CAC/B;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAsB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,CAAC,GAAG,OAAO,CAAC,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,CAAC,CAAC,CA2D3M"}
@@ -16,12 +16,12 @@ import { discoverTestDataModules, loadTestData, resetTestData } from './testdata
16
16
  * → 回写 process.env.DATABASE_URL
17
17
  * → resources → core → config → clients → services → init() → middlewares
18
18
  * → 系统路由 → routers → 关闭钩子 → ready()
19
- * → 装载 testdata/ 脚本并整体 load()
19
+ * → 装载 testdata/ 脚本并整体 load(ctx)
20
20
  * ```
21
21
  *
22
- * `testdata/` 的 `load()` 排在 `ready()` 之后有一个硬性理由:脚本用的是被装配出来的真实资源
23
- * (资源模块自己建连接、自己连库),而仓库里不提供「从这里拿资源」的通道——数据脚本应当
24
- * 自己建自己那份客户端,所以它只依赖 env。排在最后也让失败语义最干净:装载失败时
22
+ * `testdata/` 的 `load()` 排在 `ready()` 之后:脚本拿到的是装配好的 `ctx`(`env` + `resources` +
23
+ * `log`),所以它写数据用的就是被测服务读数据的那份连接——两套连接写/读同一张表而看不到对方
24
+ * 写入(缓存 / 连接级状态)这类问题从结构上不会发生。排在最后也让失败语义最干净:装载失败时
25
25
  * 装配已完成、可正常 `app.close()` 释放。
26
26
  *
27
27
  * 不会再拿应用目标跟测试目标比对:本进程读到的 `DATABASE_URL` 此时已经是测试目标,比对恒等于
@@ -57,10 +57,13 @@ export async function createTestServer(server) {
57
57
  if (!ctx.configAccess.available) {
58
58
  throw new Error('[test-server] config 槽位没有产出:装配顺序被改坏了(应当紧跟 resources 之后)');
59
59
  }
60
- // 装载`testdata/` 脚本并整体 `load()`:闸门(ENV 必须可清理)在 `loadTestData` 内部,
60
+ // 装载`testdata/` 脚本并整体 `load(ctx)`:闸门(ENV 必须可清理)在 `loadTestData` 内部,
61
61
  // 这里不重复判断,也不降级成「非测试就不算」——失败直接冒泡。
62
+ //
63
+ // `ctx` 与 `resources` 槽位同一个口径:脚本写数据 / 清缓存用的就是被测服务的那份实例。
64
+ const testDataCtx = { env, resources, log: ctx.log };
62
65
  const testData = await discoverTestDataModules();
63
- await loadTestData({ env: env.ENV, units: testData, service: ctx.service, log: ctx.log });
66
+ await loadTestData({ ctx: testDataCtx, units: testData, service: ctx.service });
64
67
  const app = ctx.app;
65
68
  return {
66
69
  app,
@@ -71,6 +74,6 @@ export async function createTestServer(server) {
71
74
  services,
72
75
  log: ctx.log,
73
76
  testData,
74
- resetTestData: () => resetTestData({ env: env.ENV, units: testData, service: ctx.service, log: ctx.log }),
77
+ resetTestData: () => resetTestData({ ctx: testDataCtx, units: testData, service: ctx.service }),
75
78
  };
76
79
  }
@@ -1,4 +1,4 @@
1
- import type { LoggerLike } from '../types.js';
1
+ import type { AppEnv, LoggerLike, ResourcesView } from '../types.js';
2
2
  /**
3
3
  * **测试数据(testdata)机制**:`packages/server/testdata/*.ts|js` 下的脚本,在 `development` /
4
4
  * `test` 启动时自动加载、在测试用例之间自动清理。
@@ -13,10 +13,11 @@ import type { LoggerLike } from '../types.js';
13
13
  *
14
14
  * ```ts
15
15
  * // packages/server/testdata/01-base.ts
16
- * import { PrismaClient } from '@prisma/generated/client'
16
+ * import type { TestDataContext } from '@taicode/common-server/app'
17
+ * import type { AppResources } from '../source/resources'
17
18
  *
18
- * export async function load() { …插入样本数据… }
19
- * export async function cleanup() { …删掉自己插入的数据… }
19
+ * export async function load(ctx: TestDataContext<AppResources>) { …插入样本数据… }
20
+ * export async function cleanup(ctx: TestDataContext<AppResources>) { …删掉自己插入的数据… }
20
21
  * ```
21
22
  *
22
23
  * ## 契约(只有三条)
@@ -36,8 +37,8 @@ import type { LoggerLike } from '../types.js';
36
37
  * ```
37
38
  *
38
39
  * - **引用名 = 去扩展名的文件名**(`01-base.ts` → `'01-base'`),与 `TestDataUnit.name` 一致。
39
- * - **只影响顺序,不注入任何东西**:声明依赖**不会**把前一个脚本的客户端/数据递给你。脚本仍然
40
- * 自己建自己的连接(只依赖 env),否则 `testdata/` 就不再是可独立运行的样本数据。
40
+ * - **只影响顺序,不注入任何东西**:声明依赖**不会**把前一个脚本的客户端/数据递给你——需要共享
41
+ * 资源时用 `ctx.resources`(框架注入的装配产物),而不是依赖另一个脚本。
41
42
  * - **`cleanup` 不用声明**:它是装配序的逆序,依赖自动反向成立(后建的先拆)。
42
43
  * - **同层按文件名升序**:没有依赖关系的脚本之间的相对顺序由文件名决定,结果确定可复现。
43
44
  * - **缺依赖 / 自依赖 / 环一律报错**(不是跳过、不是猜测顺序):顺序是安全属性,猜错只会
@@ -54,8 +55,8 @@ import type { LoggerLike } from '../types.js';
54
55
  * - **`_` 开头的文件不是脚本**:它们是脚本之间共享的 helper(`_client.ts`),不会被 `load` /
55
56
  * `cleanup`。否则扫描目录就会把工具文件也当成脚本执行。
56
57
  * - **只在可清理环境执行**:`load` / `cleanup` / `reset` 三个执行入口的**第 0 步**都是
57
- * `assertCleanableEnvironment(env)`。`env` 是**必填参数**而不是从 `process.env` 现读——
58
- * 漏传即编译错误,且判据是装配期的快照(事后改写 `process.env` 不影响结论)。
58
+ * `assertCleanableEnvironment(ctx.env.ENV)`。判据是装配期的 `ctx.env` 快照(事后改写
59
+ * `process.env` 不影响结论)。
59
60
  *
60
61
  * ## 执行顺序与失败语义
61
62
  *
@@ -70,12 +71,35 @@ import type { LoggerLike } from '../types.js';
70
71
  *
71
72
  * 失败**一律冒泡**,不降级成「跳过这条脚本」:数据没准备好时静默继续只会让后续用例读到脏状态,
72
73
  * 制造伪绿。
74
+ *
75
+ * ## 脚本拿到什么:`ctx`(与 `resources` 槽位同一个口径)
76
+ *
77
+ * 脚本不再自己读 env、不再自己建连接——框架把装配好的东西注入进来:
78
+ *
79
+ * | 字段 | 是什么 | 为什么给 |
80
+ * | --- | --- | --- |
81
+ * | `env` | 装配快照 `AppEnv<Env>` | 脚本不碰 `process.env`;看到的就是应用看到的那份 |
82
+ * | `resources` | `ResourcesView<Resources>`(单元已解包) | 脚本写数据的地方就是被测服务读数据的地方 |
83
+ * | `log` | `ctx.log` | 日志与装配期同一路输出 |
84
+ *
85
+ * 「脚本自己建一份客户端」是**错的**:同一份数据经两条连接写/读,缓存与连接级状态对不上
86
+ * (account 曾经的 `globalThis` 重置注册表就是这个坑的产物)。所以注入 `resources` 是结构性的,
87
+ * 不是便利。
73
88
  */
74
- export interface TestDataScript {
89
+ export interface TestDataContext<Resources = object, Env = object> {
90
+ /** 本次装配的 env 快照(框架固定约定 + 应用扩展字段);脚本**不必也不该**再读 `process.env` */
91
+ readonly env: AppEnv<Env>;
92
+ /** 装配好的资源(单元已解包):写数据 / 清缓存都用它,而不是另建一份客户端 */
93
+ readonly resources: ResourcesView<Resources>;
94
+ /** 记录型 logger(与装配期同一个) */
95
+ readonly log: LoggerLike;
96
+ }
97
+ /** 一个测试数据脚本(`testdata/*.ts` 的两个约定导出) */
98
+ export interface TestDataScript<Resources = object, Env = object> {
75
99
  /** 加载本脚本负责的测试数据(可重复执行:`reset` 会反复调它) */
76
- load(): void | Promise<void>;
100
+ load(ctx: TestDataContext<Resources, Env>): void | Promise<void>;
77
101
  /** 清理本脚本负责的测试数据(必须能把 `load()` 的效果完全撤销) */
78
- cleanup(): void | Promise<void>;
102
+ cleanup(ctx: TestDataContext<Resources, Env>): void | Promise<void>;
79
103
  /**
80
104
  * 必须在哪些脚本**之后**执行(去扩展名的文件名;缺省 = 无要求)。
81
105
  *
@@ -85,24 +109,23 @@ export interface TestDataScript {
85
109
  readonly dependsOn?: readonly string[];
86
110
  }
87
111
  /** 一个已从磁盘装载好的脚本单元(含名字,用于日志与报错定位) */
88
- export interface TestDataUnit extends TestDataScript {
112
+ export interface TestDataUnit<Resources = object, Env = object> extends TestDataScript<Resources, Env> {
89
113
  /** 脚本文件名去掉扩展名(`01-base.ts` → `01-base`) */
90
114
  readonly name: string;
91
115
  }
92
116
  /** 三个执行入口的共同入参 */
93
- export interface TestDataRunParams {
117
+ export interface TestDataRunParams<Resources = object, Env = object> {
94
118
  /**
95
- * 本次装配的 `env.ENV`(`ctx.env.ENV`)。**必填**:破坏性动作没有环境就没法判断该不该跑。
119
+ * 本次装配的 ctx(`env` + `resources` + `log`)。**必填**:
96
120
  *
97
- * 非 `CLEANABLE_ENVS`(`development` / `test`)一律抛错,一个脚本都不跑。
121
+ * - 闸门读 `ctx.env.ENV`:非 `CLEANABLE_ENVS`(`development` / `test`)一律抛错,一个脚本都不跑;
122
+ * - 脚本拿到的就是这份 ctx,所以它看到的环境 / 资源与本次装配完全一致。
98
123
  */
99
- env: string;
124
+ ctx: TestDataContext<Resources, Env>;
100
125
  /** 已装载的脚本(`discoverTestDataModules()` 的产物);缺省 = 没有脚本 */
101
- units?: readonly TestDataUnit[];
126
+ units?: readonly TestDataUnit<Resources, Env>[];
102
127
  /** 日志标签(`defineServer({ app: { service } })`) */
103
128
  service?: string;
104
- /** 记录型 logger(`ctx.log`);缺省 = 不打日志 */
105
- log?: LoggerLike;
106
129
  }
107
130
  /** 默认脚本目录(相对 `process.cwd()`) */
108
131
  export declare function resolveTestDataDirectory(): string;
@@ -115,13 +138,13 @@ export declare function resolveTestDataDirectory(): string;
115
138
  * 返回的顺序就是**执行序**:`dependsOn` 拓扑序 + 同层文件名升序(`load` 直接正序跑,
116
139
  * `cleanup` 直接逆序跑)。
117
140
  */
118
- export declare function discoverTestDataModules(directory?: string): Promise<TestDataUnit[]>;
141
+ export declare function discoverTestDataModules<Resources = object, Env = object>(directory?: string): Promise<TestDataUnit<Resources, Env>[]>;
119
142
  /**
120
- * 加载测试数据:按**执行序**逐项 `await load()`(依赖在前,同层按文件名升序)。
143
+ * 加载测试数据:按**执行序**逐项 `await load(ctx)`(依赖在前,同层按文件名升序)。
121
144
  *
122
- * 在 `createTestServer()` 装配完成(`ready()` 之后)自动执行一次;也可以由测试自己再调。
145
+ * 在 `createTestServer()` / `startServer()` 装配完成后自动执行一次;也可以由测试自己再调。
123
146
  */
124
- export declare function loadTestData(params: TestDataRunParams): Promise<void>;
147
+ export declare function loadTestData<Resources, Env>(params: TestDataRunParams<Resources, Env>): Promise<void>;
125
148
  /**
126
149
  * 清理测试数据:按执行序**逆序**逐项 `await cleanup()`(后建的先拆)。
127
150
  *
@@ -130,7 +153,7 @@ export declare function loadTestData(params: TestDataRunParams): Promise<void>;
130
153
  * 单独调用它的场景很少(多半是要 `resetTestData`):它与 `loadTestData` 成对,
131
154
  * 语义是「把脚本插入的数据撤掉」,而不是「把整个库清空」。
132
155
  */
133
- export declare function cleanupTestData(params: TestDataRunParams): Promise<void>;
156
+ export declare function cleanupTestData<Resources, Env>(params: TestDataRunParams<Resources, Env>): Promise<void>;
134
157
  /**
135
158
  * 重置测试数据:先**整体** `cleanup`(逆序),再**整体** `load`(正序)。
136
159
  *
@@ -144,5 +167,5 @@ export declare function cleanupTestData(params: TestDataRunParams): Promise<void
144
167
  * - **闸门只判一次**(三个入口各自开头),不做逐脚本判定:同一批脚本在同一环境里跑,
145
168
  * 逐个判只是把一次判定复制 N 次。
146
169
  */
147
- export declare function resetTestData(params: TestDataRunParams): Promise<void>;
170
+ export declare function resetTestData<Resources, Env>(params: TestDataRunParams<Resources, Env>): Promise<void>;
148
171
  //# sourceMappingURL=testdata.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"testdata.d.ts","sourceRoot":"","sources":["../../../source/core/test-support/testdata.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAE7C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuEG;AACH,MAAM,WAAW,cAAc;IAC7B,wCAAwC;IACxC,IAAI,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC5B,0CAA0C;IAC1C,OAAO,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/B;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CACvC;AAED,oCAAoC;AACpC,MAAM,WAAW,YAAa,SAAQ,cAAc;IAClD,2CAA2C;IAC3C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CACtB;AAED,kBAAkB;AAClB,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,GAAG,EAAE,MAAM,CAAA;IACX,wDAAwD;IACxD,KAAK,CAAC,EAAE,SAAS,YAAY,EAAE,CAAA;IAC/B,iDAAiD;IACjD,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,sCAAsC;IACtC,GAAG,CAAC,EAAE,UAAU,CAAA;CACjB;AAED,iCAAiC;AACjC,wBAAgB,wBAAwB,IAAI,MAAM,CAEjD;AA8FD;;;;;;;;GAQG;AACH,wBAAsB,uBAAuB,CAAC,SAAS,GAAE,MAAmC,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAgDrH;AAeD;;;;GAIG;AACH,wBAAsB,YAAY,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAO3E;AAED;;;;;;;GAOG;AACH,wBAAsB,eAAe,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAM9E;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,aAAa,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAW5E"}
1
+ {"version":3,"file":"testdata.d.ts","sourceRoot":"","sources":["../../../source/core/test-support/testdata.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAEpE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsFG;AACH,MAAM,WAAW,eAAe,CAAC,SAAS,GAAG,MAAM,EAAE,GAAG,GAAG,MAAM;IAC/D,gEAAgE;IAChE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,CAAA;IACzB,4CAA4C;IAC5C,QAAQ,CAAC,SAAS,EAAE,aAAa,CAAC,SAAS,CAAC,CAAA;IAC5C,0BAA0B;IAC1B,QAAQ,CAAC,GAAG,EAAE,UAAU,CAAA;CACzB;AAED,wCAAwC;AACxC,MAAM,WAAW,cAAc,CAAC,SAAS,GAAG,MAAM,EAAE,GAAG,GAAG,MAAM;IAC9D,wCAAwC;IACxC,IAAI,CAAC,GAAG,EAAE,eAAe,CAAC,SAAS,EAAE,GAAG,CAAC,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAChE,0CAA0C;IAC1C,OAAO,CAAC,GAAG,EAAE,eAAe,CAAC,SAAS,EAAE,GAAG,CAAC,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACnE;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CACvC;AAED,oCAAoC;AACpC,MAAM,WAAW,YAAY,CAAC,SAAS,GAAG,MAAM,EAAE,GAAG,GAAG,MAAM,CAAE,SAAQ,cAAc,CAAC,SAAS,EAAE,GAAG,CAAC;IACpG,2CAA2C;IAC3C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CACtB;AAED,kBAAkB;AAClB,MAAM,WAAW,iBAAiB,CAAC,SAAS,GAAG,MAAM,EAAE,GAAG,GAAG,MAAM;IACjE;;;;;OAKG;IACH,GAAG,EAAE,eAAe,CAAC,SAAS,EAAE,GAAG,CAAC,CAAA;IACpC,wDAAwD;IACxD,KAAK,CAAC,EAAE,SAAS,YAAY,CAAC,SAAS,EAAE,GAAG,CAAC,EAAE,CAAA;IAC/C,iDAAiD;IACjD,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,iCAAiC;AACjC,wBAAgB,wBAAwB,IAAI,MAAM,CAEjD;AA8FD;;;;;;;;GAQG;AACH,wBAAsB,uBAAuB,CAAC,SAAS,GAAG,MAAM,EAAE,GAAG,GAAG,MAAM,EAAE,SAAS,GAAE,MAAmC,GAAG,OAAO,CAAC,YAAY,CAAC,SAAS,EAAE,GAAG,CAAC,EAAE,CAAC,CAgDvK;AAeD;;;;GAIG;AACH,wBAAsB,YAAY,CAAC,SAAS,EAAE,GAAG,EAAE,MAAM,EAAE,iBAAiB,CAAC,SAAS,EAAE,GAAG,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAO3G;AAED;;;;;;;GAOG;AACH,wBAAsB,eAAe,CAAC,SAAS,EAAE,GAAG,EAAE,MAAM,EAAE,iBAAiB,CAAC,SAAS,EAAE,GAAG,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAM9G;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,aAAa,CAAC,SAAS,EAAE,GAAG,EAAE,MAAM,EAAE,iBAAiB,CAAC,SAAS,EAAE,GAAG,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAW5G"}
@@ -141,24 +141,24 @@ export async function discoverTestDataModules(directory = resolveTestDataDirecto
141
141
  /** 跑一个脚本步骤(统一日志,失败冒泡给调用方决定) */
142
142
  async function runTestDataUnit(params, unit, phase) {
143
143
  const fields = { service: params.service ?? '', unit: unit.name, stage: 'testdata', phase };
144
- params.log?.debug(fields, `[testdata] ${phase} "${unit.name}" start`);
144
+ params.ctx.log.debug(fields, `[testdata] ${phase} "${unit.name}" start`);
145
145
  try {
146
- await unit[phase]();
147
- params.log?.debug(fields, `[testdata] ${phase} "${unit.name}" done`);
146
+ await unit[phase](params.ctx);
147
+ params.ctx.log.debug(fields, `[testdata] ${phase} "${unit.name}" done`);
148
148
  }
149
149
  catch (error) {
150
- params.log?.error({ ...fields, error }, `[testdata] ${phase} "${unit.name}" failed`);
150
+ params.ctx.log.error({ ...fields, error }, `[testdata] ${phase} "${unit.name}" failed`);
151
151
  throw error;
152
152
  }
153
153
  }
154
154
  /**
155
- * 加载测试数据:按**执行序**逐项 `await load()`(依赖在前,同层按文件名升序)。
155
+ * 加载测试数据:按**执行序**逐项 `await load(ctx)`(依赖在前,同层按文件名升序)。
156
156
  *
157
- * 在 `createTestServer()` 装配完成(`ready()` 之后)自动执行一次;也可以由测试自己再调。
157
+ * 在 `createTestServer()` / `startServer()` 装配完成后自动执行一次;也可以由测试自己再调。
158
158
  */
159
159
  export async function loadTestData(params) {
160
160
  // 闸门先于一切:非可清理环境下一个脚本都不许跑
161
- assertCleanableEnvironment(params.env);
161
+ assertCleanableEnvironment(params.ctx.env.ENV);
162
162
  for (const unit of sortByDependencies(params.units ?? [])) {
163
163
  await runTestDataUnit(params, unit, 'load');
164
164
  }
@@ -172,7 +172,7 @@ export async function loadTestData(params) {
172
172
  * 语义是「把脚本插入的数据撤掉」,而不是「把整个库清空」。
173
173
  */
174
174
  export async function cleanupTestData(params) {
175
- assertCleanableEnvironment(params.env);
175
+ assertCleanableEnvironment(params.ctx.env.ENV);
176
176
  for (const unit of sortByDependencies(params.units ?? []).reverse()) {
177
177
  await runTestDataUnit(params, unit, 'cleanup');
178
178
  }
@@ -191,7 +191,7 @@ export async function cleanupTestData(params) {
191
191
  * 逐个判只是把一次判定复制 N 次。
192
192
  */
193
193
  export async function resetTestData(params) {
194
- assertCleanableEnvironment(params.env);
194
+ assertCleanableEnvironment(params.ctx.env.ENV);
195
195
  const units = sortByDependencies(params.units ?? []);
196
196
  for (const unit of [...units].reverse()) {
197
197
  await runTestDataUnit(params, unit, 'cleanup');
@@ -124,6 +124,14 @@ describe('discoverTestDataModules', () => {
124
124
  await expect(discoverTestDataModules(dir)).rejects.toThrow(/dependsOn 必须是字符串数组/);
125
125
  });
126
126
  });
127
+ /** 造一个最小 ctx(`resources` 用不着时给空对象;`log` 静默) */
128
+ function makeCtx(env, resources = {}) {
129
+ return {
130
+ env: { ENV: env },
131
+ resources: resources,
132
+ log: { debug: () => { }, info: () => { }, warn: () => { }, error: () => { } },
133
+ };
134
+ }
127
135
  /** 造一批内存脚本单元(不经磁盘,专测执行语义) */
128
136
  function unitsOf(order, names) {
129
137
  return names.map((name) => ({
@@ -140,36 +148,48 @@ describe('testdata 执行语义', () => {
140
148
  it('load 正序、cleanup 逆序', async () => {
141
149
  const order = [];
142
150
  const units = unitsOf(order, ['a', 'b', 'c']);
143
- await loadTestData({ env: 'test', units });
151
+ await loadTestData({ ctx: makeCtx('test'), units });
144
152
  expect(order).toEqual(['load:a', 'load:b', 'load:c']);
145
153
  order.length = 0;
146
- await cleanupTestData({ env: 'test', units });
154
+ await cleanupTestData({ ctx: makeCtx('test'), units });
147
155
  expect(order).toEqual(['cleanup:c', 'cleanup:b', 'cleanup:a']);
148
156
  });
149
157
  it('reset 先整体 cleanup(逆序)再整体 load(正序)', async () => {
150
158
  const order = [];
151
159
  const units = unitsOf(order, ['a', 'b']);
152
- await resetTestData({ env: 'test', units });
160
+ await resetTestData({ ctx: makeCtx('test'), units });
153
161
  // 分两趟而不是「逐脚本 cleanup + load」:后者会让前一个脚本刚插入的数据
154
162
  // 被后一个脚本的 cleanup 顺手删掉(顺序依赖)。
155
163
  expect(order).toEqual(['cleanup:b', 'cleanup:a', 'load:a', 'load:b']);
156
164
  });
165
+ it('每个脚本收到的就是同一个 ctx(env / resources / log 都是装配产物)', async () => {
166
+ const seen = [];
167
+ const resources = { database: { marker: 'live-db' } };
168
+ const ctx = makeCtx('test', resources);
169
+ const units = [
170
+ { name: 'a', load: (received) => { seen.push(received); }, cleanup: () => { } },
171
+ ];
172
+ await loadTestData({ ctx, units });
173
+ // 同一个对象:脚本里 `ctx.resources.database` 就是被测服务用的那个实例,不是副本
174
+ expect(seen[0]).toBe(ctx);
175
+ expect(seen[0].resources.database).toBe(resources.database);
176
+ });
157
177
  it('执行器自己按 dependsOn 定序,不信任调用方传入的顺序', async () => {
158
178
  const order = [];
159
179
  const units = [
160
180
  { name: '01-late', dependsOn: ['99-early'], load: () => { order.push('load:late'); }, cleanup: () => { order.push('cleanup:late'); } },
161
181
  { name: '99-early', load: () => { order.push('load:early'); }, cleanup: () => { order.push('cleanup:early'); } },
162
182
  ];
163
- await loadTestData({ env: 'test', units });
183
+ await loadTestData({ ctx: makeCtx('test'), units });
164
184
  expect(order).toEqual(['load:early', 'load:late']);
165
185
  order.length = 0;
166
- await cleanupTestData({ env: 'test', units });
186
+ await cleanupTestData({ ctx: makeCtx('test'), units });
167
187
  expect(order).toEqual(['cleanup:late', 'cleanup:early']);
168
188
  });
169
189
  it('没有脚本时三个入口都是空操作(但闸门照样生效)', async () => {
170
- await expect(loadTestData({ env: 'test' })).resolves.toBeUndefined();
171
- await expect(cleanupTestData({ env: 'development' })).resolves.toBeUndefined();
172
- await expect(resetTestData({ env: 'test' })).resolves.toBeUndefined();
190
+ await expect(loadTestData({ ctx: makeCtx('test') })).resolves.toBeUndefined();
191
+ await expect(cleanupTestData({ ctx: makeCtx('development') })).resolves.toBeUndefined();
192
+ await expect(resetTestData({ ctx: makeCtx('test') })).resolves.toBeUndefined();
173
193
  });
174
194
  it.each([
175
195
  ['loadTestData', loadTestData],
@@ -181,7 +201,7 @@ describe('testdata 执行语义', () => {
181
201
  { name: 'ok', load: () => { order.push('load:ok'); }, cleanup: () => { order.push('cleanup:ok'); } },
182
202
  { name: 'broken', load: () => { throw new Error('insert failed'); }, cleanup: () => { throw new Error('delete failed'); } },
183
203
  ];
184
- await expect(run({ env: 'test', units })).rejects.toThrow(/failed/);
204
+ await expect(run({ ctx: makeCtx('test'), units })).rejects.toThrow(/failed/);
185
205
  });
186
206
  });
187
207
  describe('testdata 的环境闸门', () => {
@@ -192,24 +212,24 @@ describe('testdata 的环境闸门', () => {
192
212
  ['resetTestData', resetTestData],
193
213
  ])('%s:非可清理环境一个脚本都不跑', async (_name, run) => {
194
214
  const order = [];
195
- await expect(run({ env: 'production', units: units(order) })).rejects.toThrow(/拒绝在 ENV=/);
215
+ await expect(run({ ctx: makeCtx('production'), units: units(order) })).rejects.toThrow(/拒绝在 ENV=/);
196
216
  expect(order).toEqual([]);
197
217
  });
198
218
  it.each(['production', 'staging', ''])('ENV=%s 时拒绝,报错带上当前值', async (env) => {
199
- await expect(loadTestData({ env })).rejects.toThrow(/ENV="/);
219
+ await expect(loadTestData({ ctx: makeCtx(env) })).rejects.toThrow(/ENV="/);
200
220
  });
201
221
  it.each(['development', 'test'])('ENV=%s 时放行', async (env) => {
202
222
  const order = [];
203
- await loadTestData({ env, units: units(order) });
223
+ await loadTestData({ ctx: makeCtx(env), units: units(order) });
204
224
  expect(order).toEqual(['load:a']);
205
225
  });
206
226
  it('闸门先于一切:没有脚本也要在非可清理环境下失败', async () => {
207
- await expect(resetTestData({ env: 'production' })).rejects.toThrow(/ENV="production"/);
227
+ await expect(resetTestData({ ctx: makeCtx('production') })).rejects.toThrow(/ENV="production"/);
208
228
  });
209
- it('判据是传入的 env 参数,不是 process.env(事后改写不影响结论)', async () => {
229
+ it('判据是 ctx.env 快照,不是 process.env(事后改写不影响结论)', async () => {
210
230
  const order = [];
211
231
  process.env.ENV = 'production';
212
- await loadTestData({ env: 'test', units: units(order) });
232
+ await loadTestData({ ctx: makeCtx('test'), units: units(order) });
213
233
  expect(order).toEqual(['load:a']);
214
234
  });
215
235
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taicode/common-server",
3
- "version": "4.0.0-beta.6",
3
+ "version": "4.0.0-beta.7",
4
4
  "author": "Alain",
5
5
  "license": "ISC",
6
6
  "description": "",
@@ -35,7 +35,7 @@
35
35
  "peerDependencies": {
36
36
  "fastify": "^5.5.0",
37
37
  "redis": "^5.11.0",
38
- "@taicode/common-base": "^4.0.0-beta.6"
38
+ "@taicode/common-base": "^4.0.0-beta.7"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@eslint/js": "^9.39.3",
@@ -49,7 +49,7 @@
49
49
  "redis": "^5.11.0",
50
50
  "typescript-eslint": "^8.56.1",
51
51
  "vitest": "^3.2.4",
52
- "@taicode/common-base": "^4.0.0-beta.6"
52
+ "@taicode/common-base": "^4.0.0-beta.7"
53
53
  },
54
54
  "scripts": {
55
55
  "test": "vitest run",