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

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 (56) hide show
  1. package/output/app/index.d.ts +2 -1
  2. package/output/app/index.d.ts.map +1 -1
  3. package/output/app/index.js +47 -14
  4. package/output/auth/index.d.ts +3 -3
  5. package/output/auth/index.d.ts.map +1 -1
  6. package/output/auth/index.js +3 -2
  7. package/output/core/boot/context.d.ts +54 -34
  8. package/output/core/boot/context.d.ts.map +1 -1
  9. package/output/core/boot/context.js +42 -4
  10. package/output/core/boot/lifecycle.d.ts +39 -7
  11. package/output/core/boot/lifecycle.d.ts.map +1 -1
  12. package/output/core/boot/lifecycle.js +59 -19
  13. package/output/core/boot/shutdown.d.ts +9 -5
  14. package/output/core/boot/shutdown.d.ts.map +1 -1
  15. package/output/core/boot/shutdown.js +9 -4
  16. package/output/core/boot/signal.d.ts +1 -1
  17. package/output/core/boot/signal.js +1 -1
  18. package/output/core/boot/steps.d.ts +60 -29
  19. package/output/core/boot/steps.d.ts.map +1 -1
  20. package/output/core/boot/steps.js +72 -13
  21. package/output/core/entries.d.ts +49 -22
  22. package/output/core/entries.d.ts.map +1 -1
  23. package/output/core/entries.js +64 -30
  24. package/output/core/entries.test.js +251 -10
  25. package/output/core/env.d.ts +21 -0
  26. package/output/core/env.d.ts.map +1 -1
  27. package/output/core/env.js +28 -0
  28. package/output/core/resource.d.ts +56 -0
  29. package/output/core/resource.d.ts.map +1 -0
  30. package/output/core/resource.js +56 -0
  31. package/output/core/test-support/database.d.ts +2 -9
  32. package/output/core/test-support/database.d.ts.map +1 -1
  33. package/output/core/test-support/database.js +3 -16
  34. package/output/core/test-support/database.test.js +2 -1
  35. package/output/core/test-support/environment.d.ts +8 -3
  36. package/output/core/test-support/environment.d.ts.map +1 -1
  37. package/output/core/test-support/environment.js +10 -7
  38. package/output/core/test-support/test-server.d.ts +49 -19
  39. package/output/core/test-support/test-server.d.ts.map +1 -1
  40. package/output/core/test-support/test-server.js +22 -6
  41. package/output/core/test-support/test-server.test.js +7 -5
  42. package/output/core/test-support/testdata.d.ts +148 -0
  43. package/output/core/test-support/testdata.d.ts.map +1 -0
  44. package/output/core/test-support/testdata.js +202 -0
  45. package/output/core/test-support/testdata.test.d.ts +2 -0
  46. package/output/core/test-support/testdata.test.d.ts.map +1 -0
  47. package/output/core/test-support/testdata.test.js +220 -0
  48. package/output/core/types.d.ts +235 -71
  49. package/output/core/types.d.ts.map +1 -1
  50. package/output/cors/index.d.ts +2 -2
  51. package/output/cors/index.d.ts.map +1 -1
  52. package/output/cors/index.js +2 -1
  53. package/output/rate-limit/index.d.ts +2 -2
  54. package/output/rate-limit/index.d.ts.map +1 -1
  55. package/output/rate-limit/index.js +2 -1
  56. package/package.json +3 -3
@@ -0,0 +1,56 @@
1
+ /**
2
+ * 构造资源单元(唯一的正当方式)。
3
+ *
4
+ * ## 为什么需要外壳
5
+ *
6
+ * 资源多是**第三方对象**:`PrismaClient`、Redis client、HTTP 连接池⋯⋯它们的形状改不了。
7
+ * service 可以直接在对象上挂 `init()`(因为 service 是本仓自己写的),资源不行——
8
+ * `prisma.setup = …` 是在改别人的类型,运行期也未必允许。
9
+ *
10
+ * 所以 `setup()` 挂在**框架私有外壳**上,`value` 原样保留:
11
+ *
12
+ * ```ts
13
+ * resources: (ctx) => ({
14
+ * database: defineResource(createPrisma(ctx.env.DATABASE_URL), {
15
+ * async setup() { await pushSchema(ctx.env.DATABASE_URL) },
16
+ * }),
17
+ * })
18
+ * ```
19
+ *
20
+ * 消费方读 `ctx.resources.database` 拿到的是 `PrismaClient` 本身
21
+ * (`ResourcesView` 会解包),不是 `ResourceUnit<PrismaClient>`。
22
+ *
23
+ * ## 单元是可选的
24
+ *
25
+ * 裸对象照旧可用(无 `setup`、无解包开销):`resources: () => ({ database: prisma })`。
26
+ * 只有需要「装配期准备」的资源才包一层。
27
+ *
28
+ * ## `setup` 何时跑、谁跑
29
+ *
30
+ * - **何时**:`resources` 槽位构造完成后、`config` 槽位之前;按**对象声明序**逐项 `await`。
31
+ * 排在 `config` 前是硬要求——`config` 常从数据库读(如 `SystemConfig` 表),库没准备好
32
+ * 它就连不上。
33
+ * - **谁**:框架。`startServer` / `createTestServer` 都调;`generateOpenApi` 不调
34
+ * (出 spec 不构造资源)。应用**永不手调**。
35
+ * - **失败**:立即中断装配并把错误抛给入口(已构造的资源照常释放)。资源准备失败后
36
+ * `config` / `services` 都没有意义,所以不吞。
37
+ *
38
+ * ## 不要在里面写 `if (ENV === 'test')`
39
+ *
40
+ * 准备动作本身应当是**状态驱动**的:`prisma db push` / `migrate deploy` 都会自己比对现状、
41
+ * 只补差量,生产与测试跑同一份代码。需要区分环境的细节应当从 `ctx.env` 取,而不是让
42
+ * 「跑不跑这个函数」变成环境分支。
43
+ *
44
+ * ## 用例间重置不在本文件
45
+ *
46
+ * 「清表 / 清缓存 / 重新插入样本数据」不适合挂在资源上:资源只知道有哪些表、哪些 key,
47
+ * 不知道**谁往里写了什么**。写在资源上就变成了「新增一个 model 就得回去改一次清库清单」,
48
+ * 漏改只会安静地变脏。
49
+ *
50
+ * 所以它单独成机制:**`packages/server/testdata/*.ts|js`,每个脚本自带 `load()` / `cleanup()`**,
51
+ * 由框架在 `development` / `test` 下自动装载并执行(见 `core/test-support/testdata.ts`)。
52
+ * 数据脚本自己知道要插入什么,也就自己知道怎么删干净。
53
+ */
54
+ export function defineResource(value, lifecycle) {
55
+ return lifecycle ? { value, ...lifecycle } : { value };
56
+ }
@@ -30,13 +30,6 @@
30
30
  */
31
31
  /** 测试 schema 的强制后缀(`account` → `account_test`) */
32
32
  export declare const TEST_SCHEMA_SUFFIX = "_test";
33
- /**
34
- * 允许运行「会清理数据库」的测试的环境(`ENV` 取值)。
35
- *
36
- * `development` 是本地开发,`test` 是 vitest 入口显式声明的环境开关。除此之外一律拒绝——
37
- * 只看 `NODE_ENV`、CI 标志或变量名都不构成证明:`NODE_ENV=test` 完全可能连着生产库。
38
- */
39
- export declare const CLEANABLE_ENVS: readonly string[];
40
33
  /**
41
34
  * 一个连接串的**全部**连接维度。
42
35
  *
@@ -71,8 +64,8 @@ export declare function maskDatabaseUrl(connectionString: string): string;
71
64
  * 调用方额外传应用连接串、也不需要另开一道闸门。
72
65
  */
73
66
  export declare function deriveTestDatabaseUrl(appDatabaseUrl: string): string;
74
- /** 环境闸门:非可清理环境立即失败(不在分支里「跳过清理」——那会让结果不可重复) */
75
- export declare function assertCleanableEnvironment(env: string): void;
67
+ /** 环境闸门(实现在 `core/env.ts`,这里**只转出**:判定只有一份) */
68
+ export { CLEANABLE_ENVS, assertCleanableEnvironment } from '../env.js';
76
69
  /** schema 闸门:目标 schema 必须以 `_test` 结尾(在**任何**同步 / 迁移 / 清理之前执行) */
77
70
  export declare function assertTestDatabaseSchema(testDatabaseUrl: string): void;
78
71
  /** `resolveTestDatabase()` 入参 */
@@ -1 +1 @@
1
- {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../../source/core/test-support/database.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,kDAAkD;AAClD,eAAO,MAAM,kBAAkB,UAAU,CAAA;AAEzC;;;;;GAKG;AACH,eAAO,MAAM,cAAc,EAAE,SAAS,MAAM,EAA4B,CAAA;AAQxE;;;;;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,8CAA8C;AAC9C,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAU5D;AAED,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"}
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"}
@@ -28,15 +28,9 @@
28
28
  * 抛错在两处都能得到「立刻失败」,而 `process.exit(1)` 在 worker 内会杀掉整个进程池、
29
29
  * 丢掉其余文件的报告。错误信息本身必须足够定位,不能靠退出码表达。
30
30
  */
31
+ import { assertCleanableEnvironment } from '../env.js';
31
32
  /** 测试 schema 的强制后缀(`account` → `account_test`) */
32
33
  export const TEST_SCHEMA_SUFFIX = '_test';
33
- /**
34
- * 允许运行「会清理数据库」的测试的环境(`ENV` 取值)。
35
- *
36
- * `development` 是本地开发,`test` 是 vitest 入口显式声明的环境开关。除此之外一律拒绝——
37
- * 只看 `NODE_ENV`、CI 标志或变量名都不构成证明:`NODE_ENV=test` 完全可能连着生产库。
38
- */
39
- export const CLEANABLE_ENVS = ['development', 'test'];
40
34
  /** 连接串未写端口时按协议默认端口补齐后再比较(PostgreSQL 默认 5432) */
41
35
  const DEFAULT_POSTGRES_PORT = '5432';
42
36
  /** 连接串未写 `?schema=` 时的实际 schema(PostgreSQL 默认 `public`) */
@@ -87,15 +81,8 @@ export function deriveTestDatabaseUrl(appDatabaseUrl) {
87
81
  parsed.searchParams.set('schema', `${schema}${TEST_SCHEMA_SUFFIX}`);
88
82
  return parsed.toString();
89
83
  }
90
- /** 环境闸门:非可清理环境立即失败(不在分支里「跳过清理」——那会让结果不可重复) */
91
- export function assertCleanableEnvironment(env) {
92
- if (CLEANABLE_ENVS.includes(env)) {
93
- return;
94
- }
95
- throw new Error(`[test-database] 拒绝在 ENV="${env || '<未设置>'}" 下运行会清理数据库的测试:` +
96
- `只接受 ${CLEANABLE_ENVS.join(' / ')}。` +
97
- '会写入 / 清理 / truncate / reset 数据库的测试绝不能指向非开发环境。');
98
- }
84
+ /** 环境闸门(实现在 `core/env.ts`,这里**只转出**:判定只有一份) */
85
+ export { CLEANABLE_ENVS, assertCleanableEnvironment } from '../env.js';
99
86
  /** schema 闸门:目标 schema 必须以 `_test` 结尾(在**任何**同步 / 迁移 / 清理之前执行) */
100
87
  export function assertTestDatabaseSchema(testDatabaseUrl) {
101
88
  const { schema } = resolveDatabaseTarget(testDatabaseUrl);
@@ -1,5 +1,6 @@
1
1
  import { afterEach, describe, expect, it } from 'vitest';
2
- import { CLEANABLE_ENVS, assertCleanableEnvironment, assertTestDatabaseSchema, deriveTestDatabaseUrl, maskDatabaseUrl, resolveDatabaseTarget, resolveTestDatabase, } from './database.js';
2
+ import { CLEANABLE_ENVS, assertCleanableEnvironment } from '../env.js';
3
+ import { assertTestDatabaseSchema, deriveTestDatabaseUrl, maskDatabaseUrl, resolveDatabaseTarget, resolveTestDatabase, } from './database.js';
3
4
  /** 稳定的测试夹具(含密码,用于验证脱敏) */
4
5
  const APP_URL = 'postgresql://postgres:secret@db.internal:5432/taicode-labs?schema=account';
5
6
  /** 每个用例自行还原 env(`resolveTestDatabase` 会读 `process.env` 作为兜底) */
@@ -11,7 +11,7 @@ import type { ConfigEnv } from '../env.js';
11
11
  */
12
12
  export declare function loadRuntimeEnvironment(): ConfigEnv;
13
13
  /**
14
- * 结算**测试**数据库目标:加载测试层 → 闸门 → 结算 → 回写 `process.env.DATABASE_URL`。
14
+ * 结算**测试**数据库目标:加载测试层 → 闸门 → 结算 → 把测试连接串注入返回的 env 快照。
15
15
  *
16
16
  * **`createTestServer` 的私有步骤,不对外导出。** 导出它等于让调用方自己决定「什么时候跑
17
17
  * 闸门、按什么顺序」,而顺序本身就是安全前提。
@@ -22,10 +22,15 @@ export declare function loadRuntimeEnvironment(): ConfigEnv;
22
22
  * → 环境闸门(ENV 必须可清理)
23
23
  * → 结算(显式 TEST_DATABASE_URL 优先,否则由 DATABASE_URL 派生 +_test,幂等)
24
24
  * → schema 闸门(必须以 _test 结尾)
25
- * → 回写 process.env.DATABASE_URL
25
+ * → 把测试连接串写进**返回的 env 快照**(不碰全局 process.env)
26
26
  * ```
27
27
  *
28
- * 隔离比较不在这里:本进程读到的 `DATABASE_URL` 此时就是测试目标,比对恒等于「自比自」。
28
+ * **为什么不再改写 `process.env.DATABASE_URL`**:那是进程级副作用——同一进程里并行跑多个
29
+ * `createTestServer`(或测试与被测代码共享进程)时会互相覆写,谁最后写谁赢。测试目标应当
30
+ * 只属于**本次装配**:框架把它注入到返回的 `env` 快照里,`resources` 槽位从 `ctx.env.DATABASE_URL`
31
+ * 读取(框架契约:资源只读 `ctx.env`,不读 `process.env`),因此每个实例各连各的,互不干扰。
32
+ *
33
+ * 隔离比较不在这里:返回的 `env.DATABASE_URL` 此时就是测试目标,比对恒等于「自比自」。
29
34
  * 防止测试清空应用数据靠的是**派生 + `_test` schema 闸门**——派生只会追加后缀,schema 闸门
30
35
  * 只放行 `*_test`,所以测试目标结构上不可能是应用目标。
31
36
  */
@@ -1 +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;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,mBAAmB,IAAI,SAAS,CAU/C"}
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"}
@@ -14,7 +14,7 @@ export function loadRuntimeEnvironment() {
14
14
  return loadEnvironment();
15
15
  }
16
16
  /**
17
- * 结算**测试**数据库目标:加载测试层 → 闸门 → 结算 → 回写 `process.env.DATABASE_URL`。
17
+ * 结算**测试**数据库目标:加载测试层 → 闸门 → 结算 → 把测试连接串注入返回的 env 快照。
18
18
  *
19
19
  * **`createTestServer` 的私有步骤,不对外导出。** 导出它等于让调用方自己决定「什么时候跑
20
20
  * 闸门、按什么顺序」,而顺序本身就是安全前提。
@@ -25,18 +25,21 @@ export function loadRuntimeEnvironment() {
25
25
  * → 环境闸门(ENV 必须可清理)
26
26
  * → 结算(显式 TEST_DATABASE_URL 优先,否则由 DATABASE_URL 派生 +_test,幂等)
27
27
  * → schema 闸门(必须以 _test 结尾)
28
- * → 回写 process.env.DATABASE_URL
28
+ * → 把测试连接串写进**返回的 env 快照**(不碰全局 process.env)
29
29
  * ```
30
30
  *
31
- * 隔离比较不在这里:本进程读到的 `DATABASE_URL` 此时就是测试目标,比对恒等于「自比自」。
31
+ * **为什么不再改写 `process.env.DATABASE_URL`**:那是进程级副作用——同一进程里并行跑多个
32
+ * `createTestServer`(或测试与被测代码共享进程)时会互相覆写,谁最后写谁赢。测试目标应当
33
+ * 只属于**本次装配**:框架把它注入到返回的 `env` 快照里,`resources` 槽位从 `ctx.env.DATABASE_URL`
34
+ * 读取(框架契约:资源只读 `ctx.env`,不读 `process.env`),因此每个实例各连各的,互不干扰。
35
+ *
36
+ * 隔离比较不在这里:返回的 `env.DATABASE_URL` 此时就是测试目标,比对恒等于「自比自」。
32
37
  * 防止测试清空应用数据靠的是**派生 + `_test` schema 闸门**——派生只会追加后缀,schema 闸门
33
38
  * 只放行 `*_test`,所以测试目标结构上不可能是应用目标。
34
39
  */
35
40
  export function prepareTestDatabase() {
36
41
  const loaded = loadEnvironment({ mode: 'test' });
37
42
  const databaseUrl = resolveTestDatabase({ env: loaded.ENV });
38
- // 强制改写:之后任何读 `process.env.DATABASE_URL` 的代码都指向测试目标
39
- process.env.DATABASE_URL = databaseUrl;
40
- // 快照必须在回写之后:调用方读到的 `env.DATABASE_URL` 就是测试目标
41
- return resolveConfigEnv();
43
+ // 只注入到本次装配的 env 快照,**不改写全局 process.env.DATABASE_URL**(避免进程级串扰)
44
+ return { ...resolveConfigEnv(), DATABASE_URL: databaseUrl };
42
45
  }
@@ -1,5 +1,5 @@
1
- import type { ConfigEnv } from '../env.js';
2
- import type { ClientsView, LoggerLike, ResourcesView, Server } from '../types.js';
1
+ import type { AppEnv, ClientsView, LoggerLike, ResourcesView, Server } from '../types.js';
2
+ import type { TestDataUnit } from './testdata.js';
3
3
  import type { FastifyInstance } from 'fastify';
4
4
  /**
5
5
  * `createTestServer()` ——**测试对象的唯一构造方式**。
@@ -29,7 +29,7 @@ import type { FastifyInstance } from 'fastify';
29
29
  *
30
30
  * `startServer` 会 listen 并装信号处理器;测试用 `app.inject()`,且 vitest worker 收不到
31
31
  * SIGTERM。除此之外(env 分层 → resources → config → clients → services → `init()` →
32
- * middlewares → 系统路由 → routers、七步关闭)**逐行相同**。
32
+ * middlewares → 系统路由 → routers、八步关闭)**逐行相同**。
33
33
  *
34
34
  * ## 参数就是那个 `Server`,没有第二个参数
35
35
  *
@@ -44,32 +44,56 @@ import type { FastifyInstance } from 'fastify';
44
44
  * 因此返回装配期**已经构造完成**的各槽位产物,而不是让调用方重新跑一遍工厂——重新构造就等于
45
45
  * 测试里又拼了一份,与生产接线脱钩。
46
46
  *
47
- * **不额外包方法**:没有 `close()`、没有 `getDatabase()`、没有 `reset()`。`app` 就是原生
48
- * `FastifyInstance`,关闭就是 `await app.close()`,走同一套七步流程;连接串就是
49
- * `env.DATABASE_URL`。多包一层 `close()` 只是在已有接口上再加一个同义名,反而让「用哪个」变成
50
- * 需要记的事。
47
+ * **不包同义方法**:没有 `close()`、没有 `getDatabase()`。`app` 就是原生 `FastifyInstance`,
48
+ * 关闭就是 `await app.close()`,走同一套八步流程;连接串就是 `env.DATABASE_URL`。多包一层
49
+ * `close()` 只是在已有接口上再加一个同义名,反而让「用哪个」变成需要记的事。
50
+ *
51
+ * **唯一的例外是 `resetTestData()`**:它不是现有接口的同义名,生产入口没有对应物。测试数据的
52
+ * 装载 / 清理只能在测试里被驱动,而只有测试入口知道「什么时候要一份干净基线」。所以这里提供
53
+ * 一个方法把 `testdata/` 下那批脚本串起来——调用方仍然不需要知道有哪些脚本、怎么清。
51
54
  */
52
- export interface TestServer<R, C, CL, S> {
55
+ export interface TestServer<Resources, Config, Clients, Services, Env> {
53
56
  /** 原生 Fastify 实例(已 `ready()`,用 `inject` 发请求;关闭用 `app.close()`) */
54
57
  readonly app: FastifyInstance;
55
58
  /**
56
- * 装配后的固定约定 env。
59
+ * 装配后的 env 快照(框架固定约定 + 应用声明的扩展字段 `Env`,`AppEnv<Env>`)。
57
60
  *
58
- * **测试断言「连的是哪个库」就看这里**:`env.DATABASE_URL` 在装配前已被改写成
59
- * `*_test` 目标,它就是本次实际连的连接串。不额外给一个 `databaseUrl` 字段——
60
- * 同一份事实出现两次,迟早会有人读到另一个。
61
+ * **测试断言「连的是哪个库」就看这里**:`env.DATABASE_URL` 在装配前已结算成 `*_test`
62
+ * 目标,它就是本次实际连的连接串。不额外给一个 `databaseUrl` 字段——同一份事实出现两次,
63
+ * 迟早会有人读到另一个。应用扩展的 env(如 `REDIS_KEY_PREFIX`)也在这里、且有类型。
61
64
  */
62
- readonly env: ConfigEnv;
63
- /** `resources` 槽位产物(`false` 键已按 `Live<T>` 剔除) */
64
- readonly resources: ResourcesView<R>;
65
+ readonly env: AppEnv<Env>;
66
+ /** `resources` 槽位产物(`false` 键已按 `Live<Value>` 剔除) */
67
+ readonly resources: ResourcesView<Resources>;
65
68
  /** `config` 槽位产物(`Awaited<>` 归一) */
66
- readonly config: Awaited<C>;
69
+ readonly config: Awaited<Config>;
67
70
  /** `clients` 槽位产物 */
68
- readonly clients: ClientsView<CL>;
71
+ readonly clients: ClientsView<Clients>;
69
72
  /** `services` 槽位产物(已 `init()` 过;测试直接拿它调方法,不 mock) */
70
- readonly services: Awaited<S>;
73
+ readonly services: Awaited<Services>;
71
74
  /** 日志器(与 `app.log` 同一个;关闭阶段仍可用) */
72
75
  readonly log: LoggerLike;
76
+ /**
77
+ * `testdata/` 目录下已装载的脚本(**执行序**:`dependsOn` 拓扑序,同层按文件名升序;
78
+ * 目录不存在则为空数组)。
79
+ *
80
+ * 装配时已经自动 `load()` 过一次。需要单独的细粒度控制时用这里列的脚本自跑
81
+ * `loadTestData({ env: testServer.env.ENV, units })`;绝大多数用例只需 `resetTestData()`。
82
+ */
83
+ readonly testData: readonly TestDataUnit[];
84
+ /**
85
+ * 重置测试数据:先整体 `cleanup`(文件名**逆序**),再整体 `load`(**正序**)。
86
+ *
87
+ * 惯例是在 `beforeEach` 调:用例前重置,上一条中途失败也不会污染下一条。
88
+ * 没写 `testdata/` 的服务调它没有任何副作用(空脚本集)。
89
+ *
90
+ * **环境闸门**:本实例装配时的 `ENV` 必须 ∈ `CLEANABLE_ENVS`(`development` / `test`),
91
+ * 否则**直接抛错**、一个脚本都不跑(不是静默跳过)。判据是装配快照,事后改
92
+ * `process.env.ENV` 不会改变结论——闸门守的是「这个实例实际是在什么环境里装起来的」。
93
+ *
94
+ * 失败会**直接抛出**(数据没清干净时静默继续只会制造伪绿)。
95
+ */
96
+ resetTestData(): Promise<void>;
73
97
  }
74
98
  /**
75
99
  * 构造测试对象。
@@ -84,11 +108,17 @@ export interface TestServer<R, C, CL, S> {
84
108
  * → 回写 process.env.DATABASE_URL
85
109
  * → resources → core → config → clients → services → init() → middlewares
86
110
  * → 系统路由 → routers → 关闭钩子 → ready()
111
+ * → 装载 testdata/ 脚本并整体 load()
87
112
  * ```
88
113
  *
114
+ * `testdata/` 的 `load()` 排在 `ready()` 之后有一个硬性理由:脚本用的是被装配出来的真实资源
115
+ * (资源模块自己建连接、自己连库),而仓库里不提供「从这里拿资源」的通道——数据脚本应当
116
+ * 自己建自己那份客户端,所以它只依赖 env。排在最后也让失败语义最干净:装载失败时
117
+ * 装配已完成、可正常 `app.close()` 释放。
118
+ *
89
119
  * 不会再拿应用目标跟测试目标比对:本进程读到的 `DATABASE_URL` 此时已经是测试目标,比对恒等于
90
120
  * 「自比自」。真正拦住「测试清空应用库」的是派生 + schema 闸门——派生只会给 schema 追加
91
121
  * `_test`,schema 闸门只放行 `*_test`,所以测试目标结构上不可能是应用目标。
92
122
  */
93
- export declare function createTestServer<R, C, CL, S>(server: Server<R, C, CL, S>): Promise<TestServer<R, C, CL, S>>;
123
+ export declare function createTestServer<Resources, Config, Clients, Services, Env>(server: Server<Resources, Config, Clients, Services, Env>): Promise<TestServer<Resources, Config, Clients, Services, Env>>;
94
124
  //# sourceMappingURL=test-server.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"test-server.d.ts","sourceRoot":"","sources":["../../../source/core/test-support/test-server.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAC1C,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACjF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAE9C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,MAAM,WAAW,UAAU,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC;IACrC,kEAAkE;IAClE,QAAQ,CAAC,GAAG,EAAE,eAAe,CAAA;IAC7B;;;;;;OAMG;IACH,QAAQ,CAAC,GAAG,EAAE,SAAS,CAAA;IACvB,iDAAiD;IACjD,QAAQ,CAAC,SAAS,EAAE,aAAa,CAAC,CAAC,CAAC,CAAA;IACpC,oCAAoC;IACpC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,CAAA;IAC3B,qBAAqB;IACrB,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,EAAE,CAAC,CAAA;IACjC,qDAAqD;IACrD,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC,CAAA;IAC7B,mCAAmC;IACnC,QAAQ,CAAC,GAAG,EAAE,UAAU,CAAA;CACzB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,gBAAgB,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,CA8CjH"}
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,7 +1,8 @@
1
1
  import { initServiceUnits } from '../boot/lifecycle.js';
2
- import { buildClients, buildConfig, buildResources, buildServices, createBootContext, installShutdownHook, registerCoreStep, registerMiddlewares, registerRouters, registerSystemRoutesStep, } from '../boot/steps.js';
2
+ import { buildClients, buildConfig, buildResources, buildServices, createBootContext, installShutdownHook, registerCoreStep, registerMiddlewares, registerRouters, registerSystemRoutesStep, resolveAppEnv, setupResources, } from '../boot/steps.js';
3
3
  import { maskDatabaseUrl } from './database.js';
4
4
  import { prepareTestDatabase } from './environment.js';
5
+ import { discoverTestDataModules, loadTestData, resetTestData } from './testdata.js';
5
6
  /**
6
7
  * 构造测试对象。
7
8
  *
@@ -15,18 +16,27 @@ import { prepareTestDatabase } from './environment.js';
15
16
  * → 回写 process.env.DATABASE_URL
16
17
  * → resources → core → config → clients → services → init() → middlewares
17
18
  * → 系统路由 → routers → 关闭钩子 → ready()
19
+ * → 装载 testdata/ 脚本并整体 load()
18
20
  * ```
19
21
  *
22
+ * `testdata/` 的 `load()` 排在 `ready()` 之后有一个硬性理由:脚本用的是被装配出来的真实资源
23
+ * (资源模块自己建连接、自己连库),而仓库里不提供「从这里拿资源」的通道——数据脚本应当
24
+ * 自己建自己那份客户端,所以它只依赖 env。排在最后也让失败语义最干净:装载失败时
25
+ * 装配已完成、可正常 `app.close()` 释放。
26
+ *
20
27
  * 不会再拿应用目标跟测试目标比对:本进程读到的 `DATABASE_URL` 此时已经是测试目标,比对恒等于
21
28
  * 「自比自」。真正拦住「测试清空应用库」的是派生 + schema 闸门——派生只会给 schema 追加
22
29
  * `_test`,schema 闸门只放行 `*_test`,所以测试目标结构上不可能是应用目标。
23
30
  */
24
31
  export async function createTestServer(server) {
25
- // 闸门 + 结算 + 改写全在框架内部,顺序不由调用方决定
26
- const env = prepareTestDatabase();
32
+ // 闸门 + 结算测试连接串全在框架内部,顺序不由调用方决定;**不改写全局 process.env**(见
33
+ // `prepareTestDatabase`)——测试目标只注入到本次装配的 `env` 快照里,`resources` 从
34
+ // `ctx.env.DATABASE_URL` 读取,同进程并行多个实例互不污染。
35
+ const env = resolveAppEnv(prepareTestDatabase());
27
36
  // 以下步序与 `startServer` 逐行对应,只去掉 listen 与信号处理器
28
37
  const ctx = createBootContext(server, env);
29
- const { resources, classifyResourceError } = await buildResources(ctx);
38
+ const { resources, units: resourceUnits, classifyResourceError } = await buildResources(ctx);
39
+ await setupResources(ctx, resourceUnits);
30
40
  await registerCoreStep(ctx, classifyResourceError);
31
41
  await buildConfig(ctx, resources);
32
42
  const clients = await buildClients(ctx, resources);
@@ -39,7 +49,7 @@ export async function createTestServer(server) {
39
49
  await registerMiddlewares(ctx, services, clients);
40
50
  registerSystemRoutesStep(ctx, services);
41
51
  await registerRouters(ctx, services, clients);
42
- installShutdownHook(ctx, services);
52
+ installShutdownHook(ctx, services, resourceUnits);
43
53
  await ctx.app.ready();
44
54
  ctx.log.info({ service: ctx.service, database: maskDatabaseUrl(env.DATABASE_URL), env: env.ENV }, '[test-server] ready(测试实例不会监听端口)');
45
55
  // `buildConfig` 一定跑过(本路径不跳过 config),缺了就按「没有 config」的语义
@@ -47,6 +57,10 @@ export async function createTestServer(server) {
47
57
  if (!ctx.configAccess.available) {
48
58
  throw new Error('[test-server] config 槽位没有产出:装配顺序被改坏了(应当紧跟 resources 之后)');
49
59
  }
60
+ // 装载`testdata/` 脚本并整体 `load()`:闸门(ENV 必须可清理)在 `loadTestData` 内部,
61
+ // 这里不重复判断,也不降级成「非测试就不算」——失败直接冒泡。
62
+ const testData = await discoverTestDataModules();
63
+ await loadTestData({ env: env.ENV, units: testData, service: ctx.service, log: ctx.log });
50
64
  const app = ctx.app;
51
65
  return {
52
66
  app,
@@ -55,6 +69,8 @@ export async function createTestServer(server) {
55
69
  config: ctx.configAccess.read(),
56
70
  clients,
57
71
  services,
58
- log: ctx.log
72
+ log: ctx.log,
73
+ testData,
74
+ resetTestData: () => resetTestData({ env: env.ENV, units: testData, service: ctx.service, log: ctx.log }),
59
75
  };
60
76
  }
@@ -26,7 +26,7 @@ function buildServer(seen = {}) {
26
26
  config: () => ({ server: { cookieNames: ['token'] } }),
27
27
  clients: () => ({}),
28
28
  services: () => ({}),
29
- middlewares: () => ({}),
29
+ middlewares: () => [],
30
30
  routers: ({ app }) => {
31
31
  app.get('/probe', { schema: ROUTE_SCHEMA }, async () => ({ ok: true }));
32
32
  },
@@ -60,14 +60,16 @@ describe('createTestServer', () => {
60
60
  expect((await testServer.app.inject({ method: 'GET', url: '/health' })).statusCode).toBe(200);
61
61
  await testServer.app.close();
62
62
  });
63
- it('数据层被强制指向 *_test(resources 拿到的 DATABASE_URL 已改写)', async () => {
63
+ it('数据层被结算到 *_test(resources 从 ctx.env 拿到的就是测试目标),且不改写全局 process.env', async () => {
64
64
  useEnv({ DATABASE_URL: APP_URL });
65
65
  const seen = {};
66
66
  const testServer = await createTestServer(buildServer(seen));
67
+ // resources 从 `ctx.env.DATABASE_URL` 读到的是结算后的测试目标
67
68
  expect(seen.databaseUrl).toContain('schema=probe_test');
68
- expect(process.env.DATABASE_URL).toContain('schema=probe_test');
69
69
  // 交出来的连接串就是「测试实际连的那个」,不是从 process.env 再推一遍
70
70
  expect(testServer.env.DATABASE_URL).toBe(seen.databaseUrl);
71
+ // **不改写全局 process.env.DATABASE_URL**:测试目标只属于本次装配,避免同进程并行串扰
72
+ expect(process.env.DATABASE_URL).toBe(APP_URL);
71
73
  await testServer.app.close();
72
74
  });
73
75
  it('显式 TEST_DATABASE_URL 优先于派生', async () => {
@@ -107,7 +109,7 @@ describe('createTestServer', () => {
107
109
  await expect(createTestServer(server)).rejects.toThrow(/必须以 _test 结尾/);
108
110
  expect(onResources).not.toHaveBeenCalled();
109
111
  });
110
- it('关闭时走同一套七步流程(services 的 dispose 被调用)', async () => {
112
+ it('关闭时走同一套八步流程(services 的 dispose 被调用)', async () => {
111
113
  useEnv({ DATABASE_URL: APP_URL });
112
114
  const onDispose = vi.fn();
113
115
  const server = defineServer({
@@ -140,7 +142,7 @@ describe('createTestServer', () => {
140
142
  config: () => config,
141
143
  clients: () => clients,
142
144
  services: () => services,
143
- middlewares: () => ({}),
145
+ middlewares: () => [],
144
146
  routers: () => { },
145
147
  }));
146
148
  // 装配产物直接交出来:测试能断言/操作它们本身,而不是再构造一份副本。
@@ -0,0 +1,148 @@
1
+ import type { LoggerLike } from '../types.js';
2
+ /**
3
+ * **测试数据(testdata)机制**:`packages/server/testdata/*.ts|js` 下的脚本,在 `development` /
4
+ * `test` 启动时自动加载、在测试用例之间自动清理。
5
+ *
6
+ * ## 为什么是「目录 + 两个约定方法」,而不是资源上的生命周期
7
+ *
8
+ * 「这条用例需要哪些样本数据」与「这份数据怎么删掉」是**同一件事的正反面**,写在同一个文件里
9
+ * 才不容易漏。把 `cleanup` 挂到资源单元上做不到这一点:资源只知道「有哪些表 / 哪些 key」,
10
+ * 不知道「谁往里写了什么」——新增一个 model 就要回去改一次清库清单,漏改只会安静地变脏。
11
+ *
12
+ * 所以机制倒过来:**数据脚本自己负责自己**。
13
+ *
14
+ * ```ts
15
+ * // packages/server/testdata/01-base.ts
16
+ * import { PrismaClient } from '@prisma/generated/client'
17
+ *
18
+ * export async function load() { …插入样本数据… }
19
+ * export async function cleanup() { …删掉自己插入的数据… }
20
+ * ```
21
+ *
22
+ * ## 契约(只有三条)
23
+ *
24
+ * 1. **目录固定**:`<cwd>/testdata`(`tsx watch ./source/index.ts` 与 `vitest` 的 cwd 都是
25
+ * `packages/server`)。目录不存在 = 没有脚本。
26
+ * 2. **两个方法都必需**:`load()` 与 `cleanup()` 缺一个就**报错**(不是跳过)。只写 `load`
27
+ * 的脚本意味着「这份数据删不掉」,那正是本机制要消灭的东西。
28
+ * 3. **顺序**:文件名(字典序升序)是默认序;需要「我必须排在某脚本之后」时用
29
+ * `dependsOn` 显式声明,见下节。
30
+ *
31
+ * ## 顺序:`dependsOn` 是权威,文件名只做同层破平
32
+ *
33
+ * ```ts
34
+ * // packages/server/testdata/02-organization.ts
35
+ * export const dependsOn = ['01-database']
36
+ * ```
37
+ *
38
+ * - **引用名 = 去扩展名的文件名**(`01-base.ts` → `'01-base'`),与 `TestDataUnit.name` 一致。
39
+ * - **只影响顺序,不注入任何东西**:声明依赖**不会**把前一个脚本的客户端/数据递给你。脚本仍然
40
+ * 自己建自己的连接(只依赖 env),否则 `testdata/` 就不再是可独立运行的样本数据。
41
+ * - **`cleanup` 不用声明**:它是装配序的逆序,依赖自动反向成立(后建的先拆)。
42
+ * - **同层按文件名升序**:没有依赖关系的脚本之间的相对顺序由文件名决定,结果确定可复现。
43
+ * - **缺依赖 / 自依赖 / 环一律报错**(不是跳过、不是猜测顺序):顺序是安全属性,猜错只会
44
+ * 变成偶发的外键冲突。
45
+ *
46
+ * 为什么要有这个字段:数字前缀是「顺序的替身」,中间插一个脚本就得重命名后面全部文件——
47
+ * 纯噪音改动,还会切断 git 历史。依赖声明让「谁必须在谁之后」贴着原因写。
48
+ *
49
+ * > 字段名与已删除的**模块装配期** `dependsOn`(连同 `validateLifecycleDependencies`,§11-2)
50
+ * > 无关:那个是资源/模块之间的构造依赖,这个是数据脚本之间的**执行顺序**。
51
+ *
52
+ * ## 两条与「安全」有关的约定
53
+ *
54
+ * - **`_` 开头的文件不是脚本**:它们是脚本之间共享的 helper(`_client.ts`),不会被 `load` /
55
+ * `cleanup`。否则扫描目录就会把工具文件也当成脚本执行。
56
+ * - **只在可清理环境执行**:`load` / `cleanup` / `reset` 三个执行入口的**第 0 步**都是
57
+ * `assertCleanableEnvironment(env)`。`env` 是**必填参数**而不是从 `process.env` 现读——
58
+ * 漏传即编译错误,且判据是装配期的快照(事后改写 `process.env` 不影响结论)。
59
+ *
60
+ * ## 执行顺序与失败语义
61
+ *
62
+ * | 入口 | 顺序 | 失败 |
63
+ * | --- | --- | --- |
64
+ * | `loadTestData` | **执行序**(`dependsOn` 拓扑序,同层按文件名升序) | 立即冒泡 |
65
+ * | `cleanupTestData` | **执行序逆序** | 立即冒泡 |
66
+ * | `resetTestData` | 先整体 `cleanup`(逆序),再整体 `load`(正序) | 立即冒泡 |
67
+ *
68
+ * `cleanup` 逆序是「后建的先拆」,依赖因此不必反向声明;`reset` 合成一个入口是给
69
+ * `beforeEach` 用的——一条语句就能拿到干净基线,不需要调用方自己记「先清后载」。
70
+ *
71
+ * 失败**一律冒泡**,不降级成「跳过这条脚本」:数据没准备好时静默继续只会让后续用例读到脏状态,
72
+ * 制造伪绿。
73
+ */
74
+ export interface TestDataScript {
75
+ /** 加载本脚本负责的测试数据(可重复执行:`reset` 会反复调它) */
76
+ load(): void | Promise<void>;
77
+ /** 清理本脚本负责的测试数据(必须能把 `load()` 的效果完全撤销) */
78
+ cleanup(): void | Promise<void>;
79
+ /**
80
+ * 必须在哪些脚本**之后**执行(去扩展名的文件名;缺省 = 无要求)。
81
+ *
82
+ * 只影响排序:`load` 按依赖拓扑序(同层按文件名升序),`cleanup` 取其逆序。
83
+ * 校验严格:引用不存在的脚本、引用自己、循环依赖都会让装载阶段**抛错**。
84
+ */
85
+ readonly dependsOn?: readonly string[];
86
+ }
87
+ /** 一个已从磁盘装载好的脚本单元(含名字,用于日志与报错定位) */
88
+ export interface TestDataUnit extends TestDataScript {
89
+ /** 脚本文件名去掉扩展名(`01-base.ts` → `01-base`) */
90
+ readonly name: string;
91
+ }
92
+ /** 三个执行入口的共同入参 */
93
+ export interface TestDataRunParams {
94
+ /**
95
+ * 本次装配的 `env.ENV`(`ctx.env.ENV`)。**必填**:破坏性动作没有环境就没法判断该不该跑。
96
+ *
97
+ * 非 `CLEANABLE_ENVS`(`development` / `test`)一律抛错,一个脚本都不跑。
98
+ */
99
+ env: string;
100
+ /** 已装载的脚本(`discoverTestDataModules()` 的产物);缺省 = 没有脚本 */
101
+ units?: readonly TestDataUnit[];
102
+ /** 日志标签(`defineServer({ app: { service } })`) */
103
+ service?: string;
104
+ /** 记录型 logger(`ctx.log`);缺省 = 不打日志 */
105
+ log?: LoggerLike;
106
+ }
107
+ /** 默认脚本目录(相对 `process.cwd()`) */
108
+ export declare function resolveTestDataDirectory(): string;
109
+ /**
110
+ * 扫描目录并**装载**全部脚本(`import()` 一次,模块本身由 ESM 缓存复用)。
111
+ *
112
+ * 目录不存在 → 空数组(不是错误:没写测试数据的服务是合法的)。
113
+ * 文件存在但 `load` / `cleanup` 缺一个 → **抛错**,并指出是哪个文件缺哪个方法。
114
+ *
115
+ * 返回的顺序就是**执行序**:`dependsOn` 拓扑序 + 同层文件名升序(`load` 直接正序跑,
116
+ * `cleanup` 直接逆序跑)。
117
+ */
118
+ export declare function discoverTestDataModules(directory?: string): Promise<TestDataUnit[]>;
119
+ /**
120
+ * 加载测试数据:按**执行序**逐项 `await load()`(依赖在前,同层按文件名升序)。
121
+ *
122
+ * 在 `createTestServer()` 装配完成(`ready()` 之后)自动执行一次;也可以由测试自己再调。
123
+ */
124
+ export declare function loadTestData(params: TestDataRunParams): Promise<void>;
125
+ /**
126
+ * 清理测试数据:按执行序**逆序**逐项 `await cleanup()`(后建的先拆)。
127
+ *
128
+ * 依赖不必反向声明:`load` 的拓扑序取逆就是安全的拆除序。
129
+ *
130
+ * 单独调用它的场景很少(多半是要 `resetTestData`):它与 `loadTestData` 成对,
131
+ * 语义是「把脚本插入的数据撤掉」,而不是「把整个库清空」。
132
+ */
133
+ export declare function cleanupTestData(params: TestDataRunParams): Promise<void>;
134
+ /**
135
+ * 重置测试数据:先**整体** `cleanup`(逆序),再**整体** `load`(正序)。
136
+ *
137
+ * 这是 `beforeEach` 的标准动作(`TestServer.resetTestData()` 的底层实现):
138
+ * 先全部撤掉再全部重建,而不是「逐脚本 cleanup + load」——后者会让前一个脚本重新插入的数据
139
+ * 被后一个脚本的 `cleanup` 顺手删掉(顺序依赖)。分两趟就没有这个问题。
140
+ *
141
+ * ## 两点与「安全」有关的说明
142
+ *
143
+ * - **先在清理阶段**:万一 `load` 失败,库里留下的是「干净」而不是「半套数据 + 半套旧数据」。
144
+ * - **闸门只判一次**(三个入口各自开头),不做逐脚本判定:同一批脚本在同一环境里跑,
145
+ * 逐个判只是把一次判定复制 N 次。
146
+ */
147
+ export declare function resetTestData(params: TestDataRunParams): Promise<void>;
148
+ //# sourceMappingURL=testdata.d.ts.map
@@ -0,0 +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"}