@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.
- package/output/app/index.d.ts +1 -0
- package/output/app/index.d.ts.map +1 -1
- package/output/app/index.js +1 -1
- package/output/core/entries.d.ts +6 -5
- package/output/core/entries.d.ts.map +1 -1
- package/output/core/entries.js +20 -15
- package/output/core/test-support/test-server.d.ts +6 -6
- package/output/core/test-support/test-server.d.ts.map +1 -1
- package/output/core/test-support/test-server.js +10 -7
- package/output/core/test-support/testdata.d.ts +48 -25
- package/output/core/test-support/testdata.d.ts.map +1 -1
- package/output/core/test-support/testdata.js +9 -9
- package/output/core/test-support/testdata.test.js +35 -15
- package/package.json +3 -3
package/output/app/index.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/output/app/index.js
CHANGED
|
@@ -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';
|
package/output/core/entries.d.ts
CHANGED
|
@@ -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
|
|
79
|
-
*
|
|
80
|
-
*
|
|
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
|
|
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"}
|
package/output/core/entries.js
CHANGED
|
@@ -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
|
|
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
|
|
99
|
-
*
|
|
100
|
-
*
|
|
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({
|
|
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
|
-
*
|
|
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,
|
|
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
|
-
*
|
|
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({
|
|
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({
|
|
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 {
|
|
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
|
-
*
|
|
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)
|
|
58
|
-
*
|
|
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
|
|
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
|
-
* 本次装配的
|
|
119
|
+
* 本次装配的 ctx(`env` + `resources` + `log`)。**必填**:
|
|
96
120
|
*
|
|
97
|
-
*
|
|
121
|
+
* - 闸门读 `ctx.env.ENV`:非 `CLEANABLE_ENVS`(`development` / `test`)一律抛错,一个脚本都不跑;
|
|
122
|
+
* - 脚本拿到的就是这份 ctx,所以它看到的环境 / 资源与本次装配完全一致。
|
|
98
123
|
*/
|
|
99
|
-
|
|
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()`
|
|
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;
|
|
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
|
|
144
|
+
params.ctx.log.debug(fields, `[testdata] ${phase} "${unit.name}" start`);
|
|
145
145
|
try {
|
|
146
|
-
await unit[phase]();
|
|
147
|
-
params.log
|
|
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
|
|
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()`
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
183
|
+
await loadTestData({ ctx: makeCtx('test'), units });
|
|
164
184
|
expect(order).toEqual(['load:early', 'load:late']);
|
|
165
185
|
order.length = 0;
|
|
166
|
-
await cleanupTestData({
|
|
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({
|
|
171
|
-
await expect(cleanupTestData({
|
|
172
|
-
await expect(resetTestData({
|
|
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({
|
|
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({
|
|
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({
|
|
227
|
+
await expect(resetTestData({ ctx: makeCtx('production') })).rejects.toThrow(/ENV="production"/);
|
|
208
228
|
});
|
|
209
|
-
it('
|
|
229
|
+
it('判据是 ctx.env 快照,不是 process.env(事后改写不影响结论)', async () => {
|
|
210
230
|
const order = [];
|
|
211
231
|
process.env.ENV = 'production';
|
|
212
|
-
await loadTestData({
|
|
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.
|
|
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.
|
|
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.
|
|
52
|
+
"@taicode/common-base": "^4.0.0-beta.7"
|
|
53
53
|
},
|
|
54
54
|
"scripts": {
|
|
55
55
|
"test": "vitest run",
|