@warlock.js/core 4.12.0 → 4.14.0

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 (189) hide show
  1. package/CHANGELOG.md +214 -0
  2. package/esm/cli/cli-commands.manager.mjs.map +1 -1
  3. package/esm/cli/commands/add.command.mjs +1 -1
  4. package/esm/cli/commands/add.command.mjs.map +1 -1
  5. package/esm/cli/commands/build.command.mjs +1 -1
  6. package/esm/cli/commands/build.command.mjs.map +1 -1
  7. package/esm/cli/commands/create-database.command.mjs +1 -1
  8. package/esm/cli/commands/create-database.command.mjs.map +1 -1
  9. package/esm/cli/commands/dev-server.command.mjs +1 -1
  10. package/esm/cli/commands/dev-server.command.mjs.map +1 -1
  11. package/esm/cli/commands/doctor/doctor-command.action.mjs.map +1 -1
  12. package/esm/cli/commands/doctor.command.mjs +1 -1
  13. package/esm/cli/commands/doctor.command.mjs.map +1 -1
  14. package/esm/cli/commands/drop-tables.command.mjs +1 -1
  15. package/esm/cli/commands/drop-tables.command.mjs.map +1 -1
  16. package/esm/cli/commands/generate/generate.command.mjs +1 -1
  17. package/esm/cli/commands/generate/generate.command.mjs.map +1 -1
  18. package/esm/cli/commands/generate/generators/controller.generator.mjs.map +1 -1
  19. package/esm/cli/commands/generate/generators/migration.generator.mjs +1 -1
  20. package/esm/cli/commands/generate/generators/migration.generator.mjs.map +1 -1
  21. package/esm/cli/commands/generate/generators/model.generator.mjs +1 -1
  22. package/esm/cli/commands/generate/generators/model.generator.mjs.map +1 -1
  23. package/esm/cli/commands/generate/generators/module.generator.mjs +1 -1
  24. package/esm/cli/commands/generate/generators/module.generator.mjs.map +1 -1
  25. package/esm/cli/commands/generate/generators/repository.generator.mjs.map +1 -1
  26. package/esm/cli/commands/generate/generators/resource.generator.mjs.map +1 -1
  27. package/esm/cli/commands/generate/generators/service.generator.mjs.map +1 -1
  28. package/esm/cli/commands/generate/utils/path-resolver.mjs +1 -1
  29. package/esm/cli/commands/generate/utils/writer.mjs +1 -1
  30. package/esm/cli/commands/migrate.command.mjs +1 -1
  31. package/esm/cli/commands/migrate.command.mjs.map +1 -1
  32. package/esm/cli/commands/routes/routes-command.action.mjs.map +1 -1
  33. package/esm/cli/commands/routes.command.mjs +1 -1
  34. package/esm/cli/commands/routes.command.mjs.map +1 -1
  35. package/esm/cli/commands/seed.command.mjs +1 -1
  36. package/esm/cli/commands/seed.command.mjs.map +1 -1
  37. package/esm/cli/commands/start-production.command.mjs +1 -1
  38. package/esm/cli/commands/start-production.command.mjs.map +1 -1
  39. package/esm/cli/commands/storage-put.action.mjs.map +1 -1
  40. package/esm/cli/commands/storage-put.command.mjs +1 -1
  41. package/esm/cli/commands/storage-put.command.mjs.map +1 -1
  42. package/esm/cli/commands/typings-generator.command.mjs +2 -2
  43. package/esm/cli/commands/typings-generator.command.mjs.map +1 -1
  44. package/esm/cli/commands/update.command.mjs +1 -1
  45. package/esm/cli/commands/update.command.mjs.map +1 -1
  46. package/esm/cli/commands-loader.mjs +1 -1
  47. package/esm/cli/commands-loader.mjs.map +1 -1
  48. package/esm/{cli → commands}/cli-command.d.mts +1 -1
  49. package/esm/commands/cli-command.d.mts.map +1 -0
  50. package/esm/{cli → commands}/cli-command.mjs +1 -1
  51. package/esm/commands/cli-command.mjs.map +1 -0
  52. package/esm/{cli → commands}/types.d.mts +1 -1
  53. package/esm/commands/types.d.mts.map +1 -0
  54. package/esm/config/load-config-files.mjs +1 -1
  55. package/esm/config/load-config-files.mjs.map +1 -1
  56. package/esm/connectors/base-connector.mjs +1 -1
  57. package/esm/connectors/base-connector.mjs.map +1 -1
  58. package/esm/connectors/connectors-manager.d.mts.map +1 -1
  59. package/esm/connectors/connectors-manager.mjs +5 -3
  60. package/esm/connectors/connectors-manager.mjs.map +1 -1
  61. package/esm/connectors/http-connector.d.mts.map +1 -1
  62. package/esm/connectors/http-connector.mjs +0 -2
  63. package/esm/connectors/http-connector.mjs.map +1 -1
  64. package/esm/database/create-database-action.mjs.map +1 -1
  65. package/esm/database/drop-tables-action.mjs.map +1 -1
  66. package/esm/database/migrate-action.mjs +1 -1
  67. package/esm/database/migrate-action.mjs.map +1 -1
  68. package/esm/database/seed-command-action.mjs +1 -1
  69. package/esm/database/seed-command-action.mjs.map +1 -1
  70. package/esm/dev-server/dev-logger.mjs +1 -1
  71. package/esm/dev-server/dev-logger.mjs.map +1 -1
  72. package/esm/dev-server/development-server.mjs +2 -2
  73. package/esm/dev-server/file-event-handler.mjs +1 -1
  74. package/esm/dev-server/file-event-handler.mjs.map +1 -1
  75. package/esm/dev-server/file-manager.mjs +1 -1
  76. package/esm/dev-server/file-manager.mjs.map +1 -1
  77. package/esm/dev-server/file-operations.mjs +1 -1
  78. package/esm/dev-server/file-operations.mjs.map +1 -1
  79. package/esm/dev-server/files-orchestrator.mjs +3 -18
  80. package/esm/dev-server/files-orchestrator.mjs.map +1 -1
  81. package/esm/dev-server/files-watcher.mjs +1 -1
  82. package/esm/dev-server/files-watcher.mjs.map +1 -1
  83. package/esm/dev-server/health-checker/file-health-checker.contract.d.mts +1 -1
  84. package/esm/dev-server/layer-executor.mjs +1 -1
  85. package/esm/dev-server/loader/register-loader.mjs +1 -1
  86. package/esm/dev-server/package-json-manager.mjs +1 -1
  87. package/esm/dev-server/package-json-manager.mjs.map +1 -1
  88. package/esm/dev-server/parse-imports.mjs +2 -2
  89. package/esm/dev-server/parse-imports.mjs.map +1 -1
  90. package/esm/dev-server/tsconfig-manager.mjs +1 -1
  91. package/esm/dev-server/tsconfig-manager.mjs.map +1 -1
  92. package/esm/dev-server/type-generator.mjs +1 -1
  93. package/esm/dev-server/type-generator.mjs.map +1 -1
  94. package/esm/dev-server/utils.mjs +1 -1
  95. package/esm/dev-server/utils.mjs.map +1 -1
  96. package/esm/generations/add-command.action.mjs +20 -8
  97. package/esm/generations/add-command.action.mjs.map +1 -1
  98. package/esm/http/build-cors-options.mjs +32 -0
  99. package/esm/http/build-cors-options.mjs.map +1 -0
  100. package/esm/http/middleware/max-body-size.middleware.d.mts +13 -5
  101. package/esm/http/middleware/max-body-size.middleware.d.mts.map +1 -1
  102. package/esm/http/middleware/max-body-size.middleware.mjs +13 -5
  103. package/esm/http/middleware/max-body-size.middleware.mjs.map +1 -1
  104. package/esm/http/plugins.d.mts.map +1 -1
  105. package/esm/http/plugins.mjs +2 -9
  106. package/esm/http/plugins.mjs.map +1 -1
  107. package/esm/http/server.d.mts.map +1 -1
  108. package/esm/http/server.mjs +3 -8
  109. package/esm/http/server.mjs.map +1 -1
  110. package/esm/index.d.mts +9 -17
  111. package/esm/index.mjs +4 -15
  112. package/esm/production/production-builder.mjs +51 -3
  113. package/esm/production/production-builder.mjs.map +1 -1
  114. package/esm/production/resolve-build-config.mjs +1 -1
  115. package/esm/production/resolve-build-config.mjs.map +1 -1
  116. package/esm/router/router.d.mts +9 -0
  117. package/esm/router/router.d.mts.map +1 -1
  118. package/esm/router/router.mjs +58 -5
  119. package/esm/router/router.mjs.map +1 -1
  120. package/esm/router/types.d.mts +24 -1
  121. package/esm/router/types.d.mts.map +1 -1
  122. package/esm/tests/index.d.mts +3 -2
  123. package/esm/tests/index.mjs +2 -3
  124. package/esm/tests/start-http-development-server.d.mts.map +1 -1
  125. package/esm/tests/start-http-development-server.mjs +38 -15
  126. package/esm/tests/start-http-development-server.mjs.map +1 -1
  127. package/esm/tests/test-connectors-selection.d.mts +14 -0
  128. package/esm/tests/test-connectors-selection.d.mts.map +1 -0
  129. package/esm/tests/test-connectors-selection.mjs +72 -0
  130. package/esm/tests/test-connectors-selection.mjs.map +1 -0
  131. package/esm/tests/test-helpers.d.mts.map +1 -1
  132. package/esm/tests/test-helpers.mjs +34 -13
  133. package/esm/tests/test-helpers.mjs.map +1 -1
  134. package/esm/tests/test-lifecycle-state.mjs +27 -0
  135. package/esm/tests/test-lifecycle-state.mjs.map +1 -0
  136. package/esm/tests/test-setup-timeout.mjs +53 -0
  137. package/esm/tests/test-setup-timeout.mjs.map +1 -0
  138. package/esm/tests/vitest-setup.d.mts +64 -8
  139. package/esm/tests/vitest-setup.d.mts.map +1 -1
  140. package/esm/tests/vitest-setup.mjs +317 -11
  141. package/esm/tests/vitest-setup.mjs.map +1 -1
  142. package/esm/utils/framework-vesion.mjs +1 -1
  143. package/esm/utils/index.d.mts +1 -0
  144. package/esm/utils/index.mjs +1 -0
  145. package/esm/utils/normalized-path.d.mts +55 -0
  146. package/esm/utils/normalized-path.d.mts.map +1 -0
  147. package/esm/{dev-server/path.mjs → utils/normalized-path.mjs} +15 -2
  148. package/esm/utils/normalized-path.mjs.map +1 -0
  149. package/esm/vite/index.d.mts +2 -0
  150. package/esm/vite/index.mjs +1 -1
  151. package/esm/vite/lower-stage3-decorators.d.mts +1 -1
  152. package/esm/vite/lower-stage3-decorators.mjs +1 -1
  153. package/esm/vite/lower-stage3-decorators.mjs.map +1 -1
  154. package/esm/warlock-config/index.mjs +1 -1
  155. package/esm/warlock-config/types.d.mts +34 -1
  156. package/esm/warlock-config/types.d.mts.map +1 -1
  157. package/esm/warlock-config/warlock-config.manager.d.mts.map +1 -1
  158. package/esm/warlock-config/warlock-config.manager.mjs +1 -2
  159. package/esm/warlock-config/warlock-config.manager.mjs.map +1 -1
  160. package/llms-full.txt +78 -23
  161. package/llms.txt +3 -3
  162. package/package.json +24 -12
  163. package/skills/lower-stage3-decorators/SKILL.md +4 -2
  164. package/skills/run-app/SKILL.md +6 -0
  165. package/skills/test-http/SKILL.md +37 -12
  166. package/skills/test-service/SKILL.md +167 -24
  167. package/skills/use-middleware/SKILL.md +5 -1
  168. package/esm/cli/cli-command.d.mts.map +0 -1
  169. package/esm/cli/cli-command.mjs.map +0 -1
  170. package/esm/cli/types.d.mts.map +0 -1
  171. package/esm/dev-server/files-orchestrator.d.mts +0 -89
  172. package/esm/dev-server/files-orchestrator.d.mts.map +0 -1
  173. package/esm/dev-server/files-watcher.d.mts +0 -69
  174. package/esm/dev-server/files-watcher.d.mts.map +0 -1
  175. package/esm/dev-server/health-checker/checkers/base-health-checker.d.mts +0 -55
  176. package/esm/dev-server/health-checker/checkers/base-health-checker.d.mts.map +0 -1
  177. package/esm/dev-server/health-checker/checkers/eslint-health-checker.d.mts +0 -44
  178. package/esm/dev-server/health-checker/checkers/eslint-health-checker.d.mts.map +0 -1
  179. package/esm/dev-server/health-checker/checkers/typescript-health-checker.d.mts +0 -60
  180. package/esm/dev-server/health-checker/checkers/typescript-health-checker.d.mts.map +0 -1
  181. package/esm/dev-server/health-checker/files-healthcare.manager.d.mts +0 -119
  182. package/esm/dev-server/health-checker/files-healthcare.manager.d.mts.map +0 -1
  183. package/esm/dev-server/health-checker/index.mjs +0 -7
  184. package/esm/dev-server/module-loader.d.mts +0 -63
  185. package/esm/dev-server/module-loader.d.mts.map +0 -1
  186. package/esm/dev-server/path.mjs.map +0 -1
  187. package/esm/tests/test-server-port-channel.d.mts +0 -27
  188. package/esm/tests/test-server-port-channel.d.mts.map +0 -1
  189. /package/esm/{cli → commands}/index.d.mts +0 -0
@@ -20,17 +20,38 @@ function getTestServerUrl() {
20
20
  * Uses native fetch - lightweight, no extra dependencies
21
21
  */
22
22
  async function testRequest(path, options = {}) {
23
+ return sendRequest(path, options, false);
24
+ }
25
+ /**
26
+ * The single place a request is actually built.
27
+ *
28
+ * `serializedJson` says whether THIS module turned the caller's value into a
29
+ * JSON string. It is the only thing that justifies setting a JSON content type:
30
+ * a `FormData` body carries a multipart boundary the runtime generates, and
31
+ * labelling it `application/json` produces a request no server can parse.
32
+ */
33
+ async function sendRequest(path, options, serializedJson) {
23
34
  const baseUrl = getTestServerUrl();
24
35
  const url = path.startsWith("/") ? `${baseUrl}${path}` : `${baseUrl}/${path}`;
36
+ const headers = new Headers(options.headers);
37
+ if (serializedJson && !headers.has("content-type")) headers.set("content-type", "application/json");
25
38
  return fetch(url, {
26
39
  ...options,
27
- headers: {
28
- "Content-Type": "application/json",
29
- ...options.headers
30
- }
40
+ headers
31
41
  });
32
42
  }
33
43
  /**
44
+ * Serialize a body for a JSON request.
45
+ *
46
+ * `undefined` means the caller omitted the argument; every other value —
47
+ * including `false`, `0`, `""` and `null` — is a legal JSON document and must
48
+ * be sent. The old check was `body ? JSON.stringify(body) : undefined`, which
49
+ * dropped all four.
50
+ */
51
+ function jsonBody(body) {
52
+ return body === void 0 ? void 0 : JSON.stringify(body);
53
+ }
54
+ /**
34
55
  * GET request helper
35
56
  */
36
57
  async function testGet(path, options = {}) {
@@ -43,21 +64,21 @@ async function testGet(path, options = {}) {
43
64
  * POST request helper
44
65
  */
45
66
  async function testPost(path, body, options = {}) {
46
- return testRequest(path, {
67
+ return sendRequest(path, {
47
68
  ...options,
48
69
  method: "POST",
49
- body: body ? JSON.stringify(body) : void 0
50
- });
70
+ body: jsonBody(body)
71
+ }, body !== void 0);
51
72
  }
52
73
  /**
53
74
  * PUT request helper
54
75
  */
55
76
  async function testPut(path, body, options = {}) {
56
- return testRequest(path, {
77
+ return sendRequest(path, {
57
78
  ...options,
58
79
  method: "PUT",
59
- body: body ? JSON.stringify(body) : void 0
60
- });
80
+ body: jsonBody(body)
81
+ }, body !== void 0);
61
82
  }
62
83
  /**
63
84
  * DELETE request helper
@@ -72,11 +93,11 @@ async function testDelete(path, options = {}) {
72
93
  * PATCH request helper
73
94
  */
74
95
  async function testPatch(path, body, options = {}) {
75
- return testRequest(path, {
96
+ return sendRequest(path, {
76
97
  ...options,
77
98
  method: "PATCH",
78
- body: body ? JSON.stringify(body) : void 0
79
- });
99
+ body: jsonBody(body)
100
+ }, body !== void 0);
80
101
  }
81
102
  /**
82
103
  * Parse JSON response with type safety
@@ -1 +1 @@
1
- {"version":3,"file":"test-helpers.mjs","names":[],"sources":["../../../../../../../core/src/tests/test-helpers.ts"],"sourcesContent":["/**\n * Warlock.js Test Helpers\n *\n * Utilities for testing Warlock.js applications.\n */\n\nimport { config } from \"../config\";\nimport { TEST_SERVER_PORT_ENV_KEY } from \"./test-server-port-channel\";\n\n/**\n * Get the test server base URL\n */\nexport function getTestServerUrl(): string {\n // `startHttpTestServer` publishes the port it actually bound. Test workers are\n // separate processes whose own config resolves `http.port` from `.env`, so\n // without this a suite started on an explicit port would send every request to\n // the `.env` port instead of the one the server is listening on.\n const publishedPort = process.env[TEST_SERVER_PORT_ENV_KEY];\n const port = publishedPort || config.key(\"http.port\", 2031);\n const host = config.key(\"http.host\", \"localhost\");\n return `http://${host}:${port}`;\n}\n\n/**\n * Simple HTTP request helper for test server\n * Uses native fetch - lightweight, no extra dependencies\n */\nexport async function testRequest(path: string, options: RequestInit = {}): Promise<Response> {\n const baseUrl = getTestServerUrl();\n const url = path.startsWith(\"/\") ? `${baseUrl}${path}` : `${baseUrl}/${path}`;\n\n return fetch(url, {\n ...options,\n headers: {\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n}\n\n/**\n * GET request helper\n */\nexport async function testGet(path: string, options: RequestInit = {}): Promise<Response> {\n return testRequest(path, { ...options, method: \"GET\" });\n}\n\n/**\n * POST request helper\n */\nexport async function testPost(\n path: string,\n body?: unknown,\n options: RequestInit = {},\n): Promise<Response> {\n return testRequest(path, {\n ...options,\n method: \"POST\",\n body: body ? JSON.stringify(body) : undefined,\n });\n}\n\n/**\n * PUT request helper\n */\nexport async function testPut(\n path: string,\n body?: unknown,\n options: RequestInit = {},\n): Promise<Response> {\n return testRequest(path, {\n ...options,\n method: \"PUT\",\n body: body ? JSON.stringify(body) : undefined,\n });\n}\n\n/**\n * DELETE request helper\n */\nexport async function testDelete(path: string, options: RequestInit = {}): Promise<Response> {\n return testRequest(path, { ...options, method: \"DELETE\" });\n}\n\n/**\n * PATCH request helper\n */\nexport async function testPatch(\n path: string,\n body?: unknown,\n options: RequestInit = {},\n): Promise<Response> {\n return testRequest(path, {\n ...options,\n method: \"PATCH\",\n body: body ? JSON.stringify(body) : undefined,\n });\n}\n\n/**\n * Parse JSON response with type safety\n */\nexport async function parseJsonResponse<T>(response: Response): Promise<T> {\n return response.json() as Promise<T>;\n}\n\n/**\n * Assert response status and return parsed JSON\n */\nexport async function expectJson<T>(response: Response, expectedStatus = 200): Promise<T> {\n if (response.status !== expectedStatus) {\n const text = await response.text();\n throw new Error(`Expected status ${expectedStatus}, got ${response.status}. Body: ${text}`);\n }\n return parseJsonResponse<T>(response);\n}\n"],"mappings":";;;;;;;;;;;;;AAYA,SAAgB,mBAA2B;CAMzC,MAAM,OADgB,QAAQ,mCACA,OAAO,IAAI,aAAa,IAAI;CAE1D,OAAO,UADM,OAAO,IAAI,aAAa,WACjB,EAAE,GAAG;AAC3B;;;;;AAMA,eAAsB,YAAY,MAAc,UAAuB,CAAC,GAAsB;CAC5F,MAAM,UAAU,iBAAiB;CACjC,MAAM,MAAM,KAAK,WAAW,GAAG,IAAI,GAAG,UAAU,SAAS,GAAG,QAAQ,GAAG;CAEvE,OAAO,MAAM,KAAK;EAChB,GAAG;EACH,SAAS;GACP,gBAAgB;GAChB,GAAG,QAAQ;EACb;CACF,CAAC;AACH;;;;AAKA,eAAsB,QAAQ,MAAc,UAAuB,CAAC,GAAsB;CACxF,OAAO,YAAY,MAAM;EAAE,GAAG;EAAS,QAAQ;CAAM,CAAC;AACxD;;;;AAKA,eAAsB,SACpB,MACA,MACA,UAAuB,CAAC,GACL;CACnB,OAAO,YAAY,MAAM;EACvB,GAAG;EACH,QAAQ;EACR,MAAM,OAAO,KAAK,UAAU,IAAI,IAAI;CACtC,CAAC;AACH;;;;AAKA,eAAsB,QACpB,MACA,MACA,UAAuB,CAAC,GACL;CACnB,OAAO,YAAY,MAAM;EACvB,GAAG;EACH,QAAQ;EACR,MAAM,OAAO,KAAK,UAAU,IAAI,IAAI;CACtC,CAAC;AACH;;;;AAKA,eAAsB,WAAW,MAAc,UAAuB,CAAC,GAAsB;CAC3F,OAAO,YAAY,MAAM;EAAE,GAAG;EAAS,QAAQ;CAAS,CAAC;AAC3D;;;;AAKA,eAAsB,UACpB,MACA,MACA,UAAuB,CAAC,GACL;CACnB,OAAO,YAAY,MAAM;EACvB,GAAG;EACH,QAAQ;EACR,MAAM,OAAO,KAAK,UAAU,IAAI,IAAI;CACtC,CAAC;AACH;;;;AAKA,eAAsB,kBAAqB,UAAgC;CACzE,OAAO,SAAS,KAAK;AACvB;;;;AAKA,eAAsB,WAAc,UAAoB,iBAAiB,KAAiB;CACxF,IAAI,SAAS,WAAW,gBAAgB;EACtC,MAAM,OAAO,MAAM,SAAS,KAAK;EACjC,MAAM,IAAI,MAAM,mBAAmB,eAAe,QAAQ,SAAS,OAAO,UAAU,MAAM;CAC5F;CACA,OAAO,kBAAqB,QAAQ;AACtC"}
1
+ {"version":3,"file":"test-helpers.mjs","names":[],"sources":["../../../../../../../core/src/tests/test-helpers.ts"],"sourcesContent":["/**\n * Warlock.js Test Helpers\n *\n * Utilities for testing Warlock.js applications.\n */\n\nimport { config } from \"../config\";\nimport { TEST_SERVER_PORT_ENV_KEY } from \"./test-server-port-channel\";\n\n/**\n * Get the test server base URL\n */\nexport function getTestServerUrl(): string {\n // `startHttpTestServer` publishes the port it actually bound. Test workers are\n // separate processes whose own config resolves `http.port` from `.env`, so\n // without this a suite started on an explicit port would send every request to\n // the `.env` port instead of the one the server is listening on.\n const publishedPort = process.env[TEST_SERVER_PORT_ENV_KEY];\n const port = publishedPort || config.key(\"http.port\", 2031);\n const host = config.key(\"http.host\", \"localhost\");\n return `http://${host}:${port}`;\n}\n\n/**\n * Simple HTTP request helper for test server\n * Uses native fetch - lightweight, no extra dependencies\n */\nexport async function testRequest(path: string, options: RequestInit = {}): Promise<Response> {\n return sendRequest(path, options, false);\n}\n\n/**\n * The single place a request is actually built.\n *\n * `serializedJson` says whether THIS module turned the caller's value into a\n * JSON string. It is the only thing that justifies setting a JSON content type:\n * a `FormData` body carries a multipart boundary the runtime generates, and\n * labelling it `application/json` produces a request no server can parse.\n */\nasync function sendRequest(\n path: string,\n options: RequestInit,\n serializedJson: boolean,\n): Promise<Response> {\n const baseUrl = getTestServerUrl();\n const url = path.startsWith(\"/\") ? `${baseUrl}${path}` : `${baseUrl}/${path}`;\n\n // Normalised through `Headers` rather than object-spread: `RequestInit.headers`\n // may be a record, a `Headers` instance, or a list of `[name, value]` tuples,\n // and spreading the last two silently produces an object with numeric keys —\n // the header is simply lost, with no error anywhere.\n const headers = new Headers(options.headers);\n\n if (serializedJson && !headers.has(\"content-type\")) {\n headers.set(\"content-type\", \"application/json\");\n }\n\n return fetch(url, { ...options, headers });\n}\n\n/**\n * Serialize a body for a JSON request.\n *\n * `undefined` means the caller omitted the argument; every other value —\n * including `false`, `0`, `\"\"` and `null` — is a legal JSON document and must\n * be sent. The old check was `body ? JSON.stringify(body) : undefined`, which\n * dropped all four.\n */\nfunction jsonBody(body: unknown): string | undefined {\n return body === undefined ? undefined : JSON.stringify(body);\n}\n\n/**\n * GET request helper\n */\nexport async function testGet(path: string, options: RequestInit = {}): Promise<Response> {\n return testRequest(path, { ...options, method: \"GET\" });\n}\n\n/**\n * POST request helper\n */\nexport async function testPost(\n path: string,\n body?: unknown,\n options: RequestInit = {},\n): Promise<Response> {\n return sendRequest(path, { ...options, method: \"POST\", body: jsonBody(body) }, body !== undefined);\n}\n\n/**\n * PUT request helper\n */\nexport async function testPut(\n path: string,\n body?: unknown,\n options: RequestInit = {},\n): Promise<Response> {\n return sendRequest(path, { ...options, method: \"PUT\", body: jsonBody(body) }, body !== undefined);\n}\n\n/**\n * DELETE request helper\n */\nexport async function testDelete(path: string, options: RequestInit = {}): Promise<Response> {\n return testRequest(path, { ...options, method: \"DELETE\" });\n}\n\n/**\n * PATCH request helper\n */\nexport async function testPatch(\n path: string,\n body?: unknown,\n options: RequestInit = {},\n): Promise<Response> {\n return sendRequest(path, { ...options, method: \"PATCH\", body: jsonBody(body) }, body !== undefined);\n}\n\n/**\n * Parse JSON response with type safety\n */\nexport async function parseJsonResponse<T>(response: Response): Promise<T> {\n return response.json() as Promise<T>;\n}\n\n/**\n * Assert response status and return parsed JSON\n */\nexport async function expectJson<T>(response: Response, expectedStatus = 200): Promise<T> {\n if (response.status !== expectedStatus) {\n const text = await response.text();\n throw new Error(`Expected status ${expectedStatus}, got ${response.status}. Body: ${text}`);\n }\n return parseJsonResponse<T>(response);\n}\n"],"mappings":";;;;;;;;;;;;;AAYA,SAAgB,mBAA2B;CAMzC,MAAM,OADgB,QAAQ,mCACA,OAAO,IAAI,aAAa,IAAI;CAE1D,OAAO,UADM,OAAO,IAAI,aAAa,WACjB,EAAE,GAAG;AAC3B;;;;;AAMA,eAAsB,YAAY,MAAc,UAAuB,CAAC,GAAsB;CAC5F,OAAO,YAAY,MAAM,SAAS,KAAK;AACzC;;;;;;;;;AAUA,eAAe,YACb,MACA,SACA,gBACmB;CACnB,MAAM,UAAU,iBAAiB;CACjC,MAAM,MAAM,KAAK,WAAW,GAAG,IAAI,GAAG,UAAU,SAAS,GAAG,QAAQ,GAAG;CAMvE,MAAM,UAAU,IAAI,QAAQ,QAAQ,OAAO;CAE3C,IAAI,kBAAkB,CAAC,QAAQ,IAAI,cAAc,GAC/C,QAAQ,IAAI,gBAAgB,kBAAkB;CAGhD,OAAO,MAAM,KAAK;EAAE,GAAG;EAAS;CAAQ,CAAC;AAC3C;;;;;;;;;AAUA,SAAS,SAAS,MAAmC;CACnD,OAAO,SAAS,SAAY,SAAY,KAAK,UAAU,IAAI;AAC7D;;;;AAKA,eAAsB,QAAQ,MAAc,UAAuB,CAAC,GAAsB;CACxF,OAAO,YAAY,MAAM;EAAE,GAAG;EAAS,QAAQ;CAAM,CAAC;AACxD;;;;AAKA,eAAsB,SACpB,MACA,MACA,UAAuB,CAAC,GACL;CACnB,OAAO,YAAY,MAAM;EAAE,GAAG;EAAS,QAAQ;EAAQ,MAAM,SAAS,IAAI;CAAE,GAAG,SAAS,MAAS;AACnG;;;;AAKA,eAAsB,QACpB,MACA,MACA,UAAuB,CAAC,GACL;CACnB,OAAO,YAAY,MAAM;EAAE,GAAG;EAAS,QAAQ;EAAO,MAAM,SAAS,IAAI;CAAE,GAAG,SAAS,MAAS;AAClG;;;;AAKA,eAAsB,WAAW,MAAc,UAAuB,CAAC,GAAsB;CAC3F,OAAO,YAAY,MAAM;EAAE,GAAG;EAAS,QAAQ;CAAS,CAAC;AAC3D;;;;AAKA,eAAsB,UACpB,MACA,MACA,UAAuB,CAAC,GACL;CACnB,OAAO,YAAY,MAAM;EAAE,GAAG;EAAS,QAAQ;EAAS,MAAM,SAAS,IAAI;CAAE,GAAG,SAAS,MAAS;AACpG;;;;AAKA,eAAsB,kBAAqB,UAAgC;CACzE,OAAO,SAAS,KAAK;AACvB;;;;AAKA,eAAsB,WAAc,UAAoB,iBAAiB,KAAiB;CACxF,IAAI,SAAS,WAAW,gBAAgB;EACtC,MAAM,OAAO,MAAM,SAAS,KAAK;EACjC,MAAM,IAAI,MAAM,mBAAmB,eAAe,QAAQ,SAAS,OAAO,UAAU,MAAM;CAC5F;CACA,OAAO,kBAAqB,QAAQ;AACtC"}
@@ -0,0 +1,27 @@
1
+ //#region ../core/src/tests/test-lifecycle-state.ts
2
+ /**
3
+ * Where the registry hangs off the runtime context.
4
+ *
5
+ * @internal Exported for specs, which must be able to reach the same slot the
6
+ * implementation uses — a spec that could only reset module state would be
7
+ * testing the very assumption this module rejects. Deliberately not re-exported
8
+ * from `src/tests/index.ts`.
9
+ */
10
+ const TEST_LIFECYCLE_REGISTRY_KEY = Symbol.for("@warlock.js/core:tests:lifecycle");
11
+ /**
12
+ * Read the runtime context's lifecycle registry, creating it on first use.
13
+ *
14
+ * @internal
15
+ */
16
+ function getTestLifecycleRegistry() {
17
+ const host = globalThis;
18
+ const registry = host[TEST_LIFECYCLE_REGISTRY_KEY];
19
+ if (registry) return registry;
20
+ const freshRegistry = { state: "idle" };
21
+ host[TEST_LIFECYCLE_REGISTRY_KEY] = freshRegistry;
22
+ return freshRegistry;
23
+ }
24
+
25
+ //#endregion
26
+ export { getTestLifecycleRegistry };
27
+ //# sourceMappingURL=test-lifecycle-state.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-lifecycle-state.mjs","names":[],"sources":["../../../../../../../core/src/tests/test-lifecycle-state.ts"],"sourcesContent":["/**\n * Lifecycle bookkeeping for the worker test runtime.\n *\n * Owns one thing: WHERE the state lives. `setupTest` / `teardownTest` own what\n * the transitions mean.\n *\n * The storage rule is the defect this module exists to fix. A module-scoped\n * `let` is rebuilt for every test file — measured across `forks|threads` ×\n * `isolate true|false` in `proofs/2026-08-12-nova-vitest-setupfiles-lifetime.md`\n * — while the worker itself, and every socket, pool and timer it holds, can\n * survive that rebuild. State that resets precisely where live resources do not\n * is not a guard; it is a lie about what is running.\n *\n * `globalThis` is the right scope for it: one per process for a fork worker, one\n * per thread for a thread worker, never shared between distinct workers — which\n * is exactly the boundary the resources themselves live inside.\n */\nimport type {\n RequestedTestConnectors,\n TestConnectorsSelection,\n} from \"./test-connectors-selection\";\nimport type { TestTimeoutScheduler } from \"./test-setup-timeout\";\n\n/**\n * The transitions the lifecycle serializes through.\n *\n * ```\n * idle -> starting -> ready -> stopping -> idle\n * \\-> poisoned\n * ```\n *\n * `poisoned` is reached when the shutdown layer reports a failure: the runtime\n * is neither up nor provably down, so the next `setupTest` must refuse rather\n * than stack a second runtime on top of leaked resources.\n *\n * @internal No state API is public.\n */\nexport type TestLifecycleState = \"idle\" | \"starting\" | \"ready\" | \"stopping\" | \"poisoned\";\n\n/**\n * The single in-flight setup attempt that concurrent `setupTest` callers share.\n *\n * @internal\n */\nexport type TestSetupAttempt = {\n /**\n * What the caller that opened the attempt asked for, before config was read.\n */\n readonly requested: RequestedTestConnectors;\n\n /**\n * Settles once the attempt has consulted project config, and therefore doubles\n * as the \"config is readable now\" barrier a later caller needs before it can\n * resolve its own config-derived selection and compare the two. Rejects with\n * the startup error when the attempt fails before reaching that point.\n */\n readonly effectiveSelection: Promise<TestConnectorsSelection>;\n\n /**\n * The attempt itself. Resolves when the runtime is ready, rejects with the\n * original startup error.\n */\n readonly completion: Promise<void>;\n};\n\n/**\n * @internal\n */\nexport type TestLifecycleRegistry = {\n state: TestLifecycleState;\n activeSelection?: TestConnectorsSelection;\n setupAttempt?: TestSetupAttempt;\n teardownAttempt?: Promise<void>;\n\n /**\n * How the setup bound is scheduled. Specs replace it to prove the guard in\n * milliseconds rather than outliving a real two-minute timeout; production\n * leaves it unset and gets {@link scheduleRealTimeout}.\n *\n * It lives here, on the runtime-context registry, for the same reason the\n * state does: a spec that could only reach a module-level slot would be\n * relying on the very rebuild this module exists to survive.\n */\n scheduleTimeout?: TestTimeoutScheduler;\n\n /**\n * Overrides `tests.setupTimeout` and the default. Internal, for specs — the\n * public surface stays the `setupTest` / `teardownTest` pair.\n */\n setupTimeoutOverride?: number;\n};\n\n/**\n * Where the registry hangs off the runtime context.\n *\n * @internal Exported for specs, which must be able to reach the same slot the\n * implementation uses — a spec that could only reset module state would be\n * testing the very assumption this module rejects. Deliberately not re-exported\n * from `src/tests/index.ts`.\n */\nexport const TEST_LIFECYCLE_REGISTRY_KEY = Symbol.for(\"@warlock.js/core:tests:lifecycle\");\n\ntype TestLifecycleHost = typeof globalThis & {\n [TEST_LIFECYCLE_REGISTRY_KEY]?: TestLifecycleRegistry;\n};\n\n/**\n * Read the runtime context's lifecycle registry, creating it on first use.\n *\n * @internal\n */\nexport function getTestLifecycleRegistry(): TestLifecycleRegistry {\n const host = globalThis as TestLifecycleHost;\n const registry = host[TEST_LIFECYCLE_REGISTRY_KEY];\n\n if (registry) {\n return registry;\n }\n\n const freshRegistry: TestLifecycleRegistry = { state: \"idle\" };\n\n host[TEST_LIFECYCLE_REGISTRY_KEY] = freshRegistry;\n\n return freshRegistry;\n}\n"],"mappings":";;;;;;;;;AAoGA,MAAa,8BAA8B,OAAO,IAAI,kCAAkC;;;;;;AAWxF,SAAgB,2BAAkD;CAChE,MAAM,OAAO;CACb,MAAM,WAAW,KAAK;CAEtB,IAAI,UACF,OAAO;CAGT,MAAM,gBAAuC,EAAE,OAAO,OAAO;CAE7D,KAAK,+BAA+B;CAEpC,OAAO;AACT"}
@@ -0,0 +1,53 @@
1
+ import { config } from "../config/config-getter.mjs";
2
+ import "../config/index.mjs";
3
+
4
+ //#region ../core/src/tests/test-setup-timeout.ts
5
+ /**
6
+ * Two minutes. Chosen to sit far above any healthy bootstrap and far below the
7
+ * point where a human stops watching — a stuck worker should announce itself
8
+ * while someone is still looking at the terminal.
9
+ */
10
+ const DEFAULT_TEST_SETUP_TIMEOUT = 12e4;
11
+ /**
12
+ * The real scheduler.
13
+ *
14
+ * `unref` matters: a pending hang guard must never be the reason a worker stays
15
+ * alive after its tests have finished.
16
+ *
17
+ * @internal
18
+ */
19
+ const scheduleRealTimeout = (timeout, onExpiry) => {
20
+ const timer = setTimeout(onExpiry, timeout);
21
+ timer.unref?.();
22
+ return { cancel: () => clearTimeout(timer) };
23
+ };
24
+ /**
25
+ * Read `tests.setupTimeout`, or `undefined` when the project has not set one.
26
+ *
27
+ * Only meaningful once config files are loaded — before that, every project
28
+ * looks like a project without a `tests` config.
29
+ *
30
+ * @throws when the key is present but is not a positive finite number of
31
+ * milliseconds. Falling back to the default would silently erase a project's
32
+ * configuration, which is the same defect the connector precedence rules out.
33
+ */
34
+ function readConfiguredSetupTimeout() {
35
+ const configured = config.get("tests", {})?.setupTimeout;
36
+ if (configured === void 0) return;
37
+ if (typeof configured !== "number" || !Number.isFinite(configured) || configured <= 0) throw new Error(`tests.setupTimeout must be a positive number of milliseconds, but it is ${JSON.stringify(configured)}. Remove the key to use the default of ${DEFAULT_TEST_SETUP_TIMEOUT}ms.`);
38
+ return configured;
39
+ }
40
+ /**
41
+ * The message a caller sees when the bound expires.
42
+ *
43
+ * Names three things, because a hang guard that only says "timed out" sends the
44
+ * reader to the framework's source: what state the lifecycle is stuck in, what
45
+ * bound it exceeded, and how to raise that bound.
46
+ */
47
+ function describeExpiredSetup(timeout) {
48
+ return `setupTest() did not finish within ${timeout}ms and is stuck in the "starting" state. The lifecycle is now poisoned: whatever that attempt had already started is not known to be closed, so later setupTest() calls refuse until the Vitest worker is recycled. If your cold start is legitimately slower than this, raise the bound with \`tests.setupTimeout\` in \`src/config/tests.ts\` — milliseconds, default ${DEFAULT_TEST_SETUP_TIMEOUT}.`;
49
+ }
50
+
51
+ //#endregion
52
+ export { DEFAULT_TEST_SETUP_TIMEOUT, describeExpiredSetup, readConfiguredSetupTimeout, scheduleRealTimeout };
53
+ //# sourceMappingURL=test-setup-timeout.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-setup-timeout.mjs","names":[],"sources":["../../../../../../../core/src/tests/test-setup-timeout.ts"],"sourcesContent":["/**\n * The setup bound for the worker test lifecycle.\n *\n * Owns one decision: how long a `setupTest` attempt may run before the\n * lifecycle gives up on it. `vitest-setup.ts` owns what giving up MEANS.\n *\n * This is a HANG GUARD, not a startup-performance deadline. A cold database, a\n * container that has just been started, a first-run migration — all of those are\n * legitimately slow and none of them are a stuck lifecycle. The default is\n * therefore generous on purpose: it exists so that a setup which will never\n * finish fails with a sentence a caller can act on, instead of stranding the\n * lifecycle in `starting` where `teardownTest`'s wait-then-re-enter path spins\n * until the worker dies of heap exhaustion.\n */\nimport type { GenericObject } from \"@mongez/reinforcements\";\nimport { config } from \"../config\";\n\n/**\n * Two minutes. Chosen to sit far above any healthy bootstrap and far below the\n * point where a human stops watching — a stuck worker should announce itself\n * while someone is still looking at the terminal.\n */\nexport const DEFAULT_TEST_SETUP_TIMEOUT = 120_000;\n\n/**\n * A scheduled expiry the lifecycle can cancel when the attempt settles first.\n *\n * @internal\n */\nexport type TestTimeoutHandle = {\n cancel: () => void;\n};\n\n/**\n * How the lifecycle schedules its expiry.\n *\n * Injectable so a spec can prove the bound in milliseconds instead of outliving\n * a real one — see {@link TestLifecycleRegistry.scheduleTimeout}. Deliberately\n * internal: the public API stays the `setupTest` / `teardownTest` pair.\n *\n * @internal\n */\nexport type TestTimeoutScheduler = (\n timeout: number,\n onExpiry: () => void,\n) => TestTimeoutHandle;\n\n/**\n * The real scheduler.\n *\n * `unref` matters: a pending hang guard must never be the reason a worker stays\n * alive after its tests have finished.\n *\n * @internal\n */\nexport const scheduleRealTimeout: TestTimeoutScheduler = (timeout, onExpiry) => {\n const timer = setTimeout(onExpiry, timeout);\n\n timer.unref?.();\n\n return { cancel: () => clearTimeout(timer) };\n};\n\n/**\n * Read `tests.setupTimeout`, or `undefined` when the project has not set one.\n *\n * Only meaningful once config files are loaded — before that, every project\n * looks like a project without a `tests` config.\n *\n * @throws when the key is present but is not a positive finite number of\n * milliseconds. Falling back to the default would silently erase a project's\n * configuration, which is the same defect the connector precedence rules out.\n */\nexport function readConfiguredSetupTimeout(): number | undefined {\n // `config.get` resolves an absent key to its default, and ITS default is\n // `null` — not `{}` — so the explicit `{}` is what keeps this from throwing\n // \"Cannot read properties of null\" on a project with no `src/config/tests.ts`.\n const testsConfig = config.get<GenericObject>(\"tests\", {});\n const configured = testsConfig?.setupTimeout as unknown;\n\n if (configured === undefined) {\n return undefined;\n }\n\n if (typeof configured !== \"number\" || !Number.isFinite(configured) || configured <= 0) {\n throw new Error(\n `tests.setupTimeout must be a positive number of milliseconds, but it is ${JSON.stringify(configured)}. Remove the key to use the default of ${DEFAULT_TEST_SETUP_TIMEOUT}ms.`,\n );\n }\n\n return configured;\n}\n\n/**\n * The message a caller sees when the bound expires.\n *\n * Names three things, because a hang guard that only says \"timed out\" sends the\n * reader to the framework's source: what state the lifecycle is stuck in, what\n * bound it exceeded, and how to raise that bound.\n */\nexport function describeExpiredSetup(timeout: number): string {\n return (\n `setupTest() did not finish within ${timeout}ms and is stuck in the \"starting\" state. ` +\n \"The lifecycle is now poisoned: whatever that attempt had already started is not known to be \" +\n \"closed, so later setupTest() calls refuse until the Vitest worker is recycled. \" +\n `If your cold start is legitimately slower than this, raise the bound with \\`tests.setupTimeout\\` ` +\n `in \\`src/config/tests.ts\\` — milliseconds, default ${DEFAULT_TEST_SETUP_TIMEOUT}.`\n );\n}\n"],"mappings":";;;;;;;;;AAsBA,MAAa,6BAA6B;;;;;;;;;AAiC1C,MAAa,uBAA6C,SAAS,aAAa;CAC9E,MAAM,QAAQ,WAAW,UAAU,OAAO;CAE1C,MAAM,QAAQ;CAEd,OAAO,EAAE,cAAc,aAAa,KAAK,EAAE;AAC7C;;;;;;;;;;;AAYA,SAAgB,6BAAiD;CAK/D,MAAM,aADc,OAAO,IAAmB,SAAS,CAAC,CAC3B,CAAC,EAAE;CAEhC,IAAI,eAAe,QACjB;CAGF,IAAI,OAAO,eAAe,YAAY,CAAC,OAAO,SAAS,UAAU,KAAK,cAAc,GAClF,MAAM,IAAI,MACR,2EAA2E,KAAK,UAAU,UAAU,EAAE,yCAAyC,2BAA2B,IAC5K;CAGF,OAAO;AACT;;;;;;;;AASA,SAAgB,qBAAqB,SAAyB;CAC5D,OACE,qCAAqC,QAAQ,0WAIS,2BAA2B;AAErF"}
@@ -1,14 +1,70 @@
1
- import { ConnectorName } from "../connectors/types.mjs";
1
+ import { TestConnectorsSelection } from "./test-connectors-selection.mjs";
2
+
2
3
  //#region ../core/src/tests/vitest-setup.d.ts
3
- type TestSetup = {
4
- connectors?: boolean | ConnectorName[];
4
+ type TestSetupOptions = {
5
+ /**
6
+ * Which connectors to start.
7
+ *
8
+ * Precedence is `explicit non-undefined option > tests.connectors config >
9
+ * true`. Omitting the property, or passing `undefined`, lets project config
10
+ * apply — so an optional variable that happens to be `undefined` cannot erase
11
+ * it.
12
+ */
13
+ connectors?: TestConnectorsSelection;
5
14
  };
6
15
  /**
7
- * Setup function that runs once per worker thread
16
+ * Raised by the lifecycle itself, never by the runtime it manages — a conflict
17
+ * between two calls, or a refusal to reuse a runtime that did not close.
8
18
  */
9
- declare function setupTest({
10
- connectors
11
- }: TestSetup): Promise<void>;
19
+ declare class TestLifecycleError extends Error {
20
+ constructor(message: string);
21
+ }
22
+ /**
23
+ * Bootstrap the test runtime and start the selected connectors.
24
+ *
25
+ * ⚠ Runs once per TEST FILE, not once per worker: `setupFiles` is executed for
26
+ * every file and its module registry is rebuilt with it, measured across both
27
+ * pools with isolation on and off. The lifecycle state lives on the runtime
28
+ * context rather than in this module precisely so that a rebuild cannot hide a
29
+ * runtime that is still up.
30
+ *
31
+ * Repeated calls:
32
+ *
33
+ * - concurrent calls with the same effective selection share one startup;
34
+ * - while ready, the same effective selection is a no-op;
35
+ * - while starting or ready, a DIFFERENT effective selection rejects and leaves
36
+ * the live runtime untouched;
37
+ * - a failed setup unwinds and returns to idle, so a clean retry is allowed;
38
+ * - after a successful `teardownTest`, a later setup may use a different
39
+ * selection.
40
+ *
41
+ * @example
42
+ * // let project config decide, falling back to the default set
43
+ * await setupTest();
44
+ *
45
+ * @example
46
+ * // bootstrap only — no connectors, whatever config says
47
+ * await setupTest({ connectors: false });
48
+ */
49
+ declare function setupTest(options?: TestSetupOptions): Promise<void>;
50
+ /**
51
+ * Shut the test runtime down and release the lifecycle.
52
+ *
53
+ * - idle is a no-op;
54
+ * - concurrent calls share one attempt;
55
+ * - a call made while setup is still running waits for that attempt to settle,
56
+ * then closes the runtime if it succeeded;
57
+ * - the local "ready" state is always cleared, including when shutdown rejects;
58
+ * - a shutdown rejection is surfaced, never swallowed, and poisons the
59
+ * lifecycle — a reported close failure is not proof that anything closed, so
60
+ * later `setupTest` calls refuse until the worker is restarted or a retry
61
+ * fully succeeds.
62
+ *
63
+ * ⚠ Individual connector shutdown failures are caught and logged inside
64
+ * `connectorsManager.shutdown()`, so they never reach this function and never
65
+ * poison anything. This lifecycle can only surface what that layer reports.
66
+ */
67
+ declare function teardownTest(): Promise<void>;
12
68
  //#endregion
13
- export { setupTest };
69
+ export { TestLifecycleError, TestSetupOptions, setupTest, teardownTest };
14
70
  //# sourceMappingURL=vitest-setup.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"vitest-setup.d.mts","names":[],"sources":["../../../../../../../core/src/tests/vitest-setup.ts"],"mappings":";;KAmBK,SAAA;EACH,UAAA,aAAuB,aAAa;AAAA;AAR2B;;;AAAA,iBAc3C,SAAA;EAAY;AAAA,GAAqB,SAAA,GAAS,OAAA"}
1
+ {"version":3,"file":"vitest-setup.d.mts","names":[],"sources":["../../../../../../../core/src/tests/vitest-setup.ts"],"mappings":";;;KAoDY,gBAAA;EAAA;;;;AAS0B;AAOtC;;;EAPE,UAAA,GAAa,uBAAuB;AAAA;;;;AAQF;cADvB,kBAAA,SAA2B,KAAK;cACxB,OAAA;AAAA;;;;;;AAqC+C;AAgDpE;;;;AAA6C;;;;;;;;;;;;;;;;;iBAhDvB,SAAA,CAAU,OAAA,GAAU,gBAAA,GAAmB,OAAO;;;;;;;;;;;;;;;;;;iBAgD9C,YAAA,IAAgB,OAAO"}
@@ -1,5 +1,3 @@
1
- import { config } from "../config/config-getter.mjs";
2
- import "../config/index.mjs";
3
1
  import { Application } from "../application/application.mjs";
4
2
  import { bootstrap } from "../bootstrap.mjs";
5
3
  import { connectorsManager } from "../connectors/connectors-manager.mjs";
@@ -7,30 +5,338 @@ import "../connectors/index.mjs";
7
5
  import { warlockConfigManager } from "../warlock-config/warlock-config.manager.mjs";
8
6
  import { filesOrchestrator } from "../dev-server/files-orchestrator.mjs";
9
7
  import { loadConfigFiles } from "../config/load-config-files.mjs";
8
+ import { describeConnectorsSelection, isSameConnectorsSelection, normalizeConnectorNames, readRequestedConnectors, resolveEffectiveConnectors } from "./test-connectors-selection.mjs";
9
+ import { getTestLifecycleRegistry } from "./test-lifecycle-state.mjs";
10
+ import { DEFAULT_TEST_SETUP_TIMEOUT, describeExpiredSetup, readConfiguredSetupTimeout, scheduleRealTimeout } from "./test-setup-timeout.mjs";
10
11
 
11
12
  //#region ../core/src/tests/vitest-setup.ts
12
- let isSetupComplete = false;
13
13
  /**
14
- * Setup function that runs once per worker thread
14
+ * Worker test lifecycle `setupTest` / `teardownTest`.
15
+ *
16
+ * Implements `contracts/2026-08-12-test-worker-lifecycle.md`. The pair is
17
+ * context-scoped, not "once per worker": Vitest runs `setupFiles` before EVERY
18
+ * test file and rebuilds the module registry each time, so the harness that
19
+ * calls `setupTest` owns pairing it with `teardownTest` in the same runtime
20
+ * context.
21
+ *
22
+ * Ownership, stated once because teardown is wider than it looks:
23
+ *
24
+ * - the runtime assumes exclusive ownership of the process-local
25
+ * `connectorsManager` for its lifetime;
26
+ * - it owns the framework bootstrap, the application shutdown hooks, and the
27
+ * connectors it starts — including the `connectors: false` case, where
28
+ * bootstrap still registers hooks;
29
+ * - teardown is MANAGER-WIDE. `connectorsManager.shutdown()` has no selective
30
+ * ownership handle, so a lifecycle that started two connectors still closes
31
+ * every connector the manager holds. Mixing `setupTest` with manual connector
32
+ * startup in the same process is unsupported for exactly that reason.
33
+ * - HTTP global setup stays separately owned by `startHttpTestServer` /
34
+ * `stopHttpTestServer`.
15
35
  */
16
- async function setupTest({ connectors = true }) {
17
- if (isSetupComplete) return;
36
+ /**
37
+ * Raised by the lifecycle itself, never by the runtime it manages — a conflict
38
+ * between two calls, or a refusal to reuse a runtime that did not close.
39
+ */
40
+ var TestLifecycleError = class extends Error {
41
+ constructor(message) {
42
+ super(message);
43
+ this.name = "TestLifecycleError";
44
+ }
45
+ };
46
+ const POISONED_MESSAGE = "setupTest() refuses to start a test runtime: an earlier teardownTest() reported a shutdown failure, so the connectors, ports and pools it owned are not known to be closed. Restart the Vitest worker before setting up again, or call teardownTest() to retry the shutdown — the lifecycle only returns to idle on a fully successful retry.";
47
+ /**
48
+ * Bootstrap the test runtime and start the selected connectors.
49
+ *
50
+ * ⚠ Runs once per TEST FILE, not once per worker: `setupFiles` is executed for
51
+ * every file and its module registry is rebuilt with it, measured across both
52
+ * pools with isolation on and off. The lifecycle state lives on the runtime
53
+ * context rather than in this module precisely so that a rebuild cannot hide a
54
+ * runtime that is still up.
55
+ *
56
+ * Repeated calls:
57
+ *
58
+ * - concurrent calls with the same effective selection share one startup;
59
+ * - while ready, the same effective selection is a no-op;
60
+ * - while starting or ready, a DIFFERENT effective selection rejects and leaves
61
+ * the live runtime untouched;
62
+ * - a failed setup unwinds and returns to idle, so a clean retry is allowed;
63
+ * - after a successful `teardownTest`, a later setup may use a different
64
+ * selection.
65
+ *
66
+ * @example
67
+ * // let project config decide, falling back to the default set
68
+ * await setupTest();
69
+ *
70
+ * @example
71
+ * // bootstrap only — no connectors, whatever config says
72
+ * await setupTest({ connectors: false });
73
+ */
74
+ async function setupTest(options) {
75
+ const registry = getTestLifecycleRegistry();
76
+ const requested = readRequestedConnectors(options?.connectors);
77
+ if (registry.state === "poisoned") throw new TestLifecycleError(POISONED_MESSAGE);
78
+ if (registry.state === "stopping" && registry.teardownAttempt) {
79
+ await settled(registry.teardownAttempt);
80
+ return setupTest(options);
81
+ }
82
+ if (registry.state === "ready") {
83
+ assertReadySelectionMatches(registry.activeSelection, requested);
84
+ return;
85
+ }
86
+ if (registry.state === "starting" && registry.setupAttempt) {
87
+ const attempt = registry.setupAttempt;
88
+ await assertPendingSetupMatches(attempt, requested);
89
+ return attempt.completion;
90
+ }
91
+ return startTestRuntime(requested);
92
+ }
93
+ /**
94
+ * Shut the test runtime down and release the lifecycle.
95
+ *
96
+ * - idle is a no-op;
97
+ * - concurrent calls share one attempt;
98
+ * - a call made while setup is still running waits for that attempt to settle,
99
+ * then closes the runtime if it succeeded;
100
+ * - the local "ready" state is always cleared, including when shutdown rejects;
101
+ * - a shutdown rejection is surfaced, never swallowed, and poisons the
102
+ * lifecycle — a reported close failure is not proof that anything closed, so
103
+ * later `setupTest` calls refuse until the worker is restarted or a retry
104
+ * fully succeeds.
105
+ *
106
+ * ⚠ Individual connector shutdown failures are caught and logged inside
107
+ * `connectorsManager.shutdown()`, so they never reach this function and never
108
+ * poison anything. This lifecycle can only surface what that layer reports.
109
+ */
110
+ async function teardownTest() {
111
+ const registry = getTestLifecycleRegistry();
112
+ if (registry.state === "stopping" && registry.teardownAttempt) return registry.teardownAttempt;
113
+ if (registry.state === "starting" && registry.setupAttempt) {
114
+ await settled(registry.setupAttempt.completion);
115
+ return teardownTest();
116
+ }
117
+ if (registry.state === "idle") return;
118
+ registry.state = "stopping";
119
+ const attempt = Promise.resolve().then(() => runTestRuntimeShutdown());
120
+ registry.teardownAttempt = attempt;
121
+ return attempt;
122
+ }
123
+ /**
124
+ * Arm the hang guard for one setup attempt.
125
+ *
126
+ * Armed at the default BEFORE the attempt runs, because `tests.setupTimeout` is
127
+ * not readable until the attempt has loaded config — and loading config is
128
+ * itself one of the steps that can hang. `rearm` then narrows or widens it the
129
+ * moment config arrives.
130
+ */
131
+ function armSetupBound(registry) {
132
+ const schedule = registry.scheduleTimeout ?? scheduleRealTimeout;
133
+ const armedAt = Date.now();
134
+ let handle;
135
+ let expired = false;
136
+ let closed = false;
137
+ let expire = () => void 0;
138
+ const expiry = new Promise((_resolve, reject) => {
139
+ expire = reject;
140
+ });
141
+ expiry.catch(() => void 0);
142
+ const expireNow = (bound) => {
143
+ if (closed) return;
144
+ closed = true;
145
+ expired = true;
146
+ handle?.cancel();
147
+ handle = void 0;
148
+ expire(new TestLifecycleError(describeExpiredSetup(bound)));
149
+ };
150
+ const armFor = (bound, delay) => {
151
+ if (closed) return;
152
+ handle?.cancel();
153
+ if (delay <= 0) {
154
+ expireNow(bound);
155
+ return;
156
+ }
157
+ handle = schedule(delay, () => expireNow(bound));
158
+ };
159
+ const initialBound = registry.setupTimeoutOverride ?? 12e4;
160
+ armFor(initialBound, initialBound);
161
+ return {
162
+ expiry,
163
+ rearm: (next) => armFor(next, next - (Date.now() - armedAt)),
164
+ settle: () => {
165
+ closed = true;
166
+ handle?.cancel();
167
+ handle = void 0;
168
+ },
169
+ hasExpired: () => expired
170
+ };
171
+ }
172
+ /**
173
+ * Race one setup attempt against its bound.
174
+ *
175
+ * The losing attempt is NOT cancellable — nothing here can interrupt a bootstrap
176
+ * stuck in a socket connect — so when the bound wins, the lifecycle is poisoned
177
+ * and every later state write from that abandoned attempt is dropped. That
178
+ * dropping happens in `runTestRuntimeStartup`, guarded on `hasExpired()`;
179
+ * without it the still-running attempt would quietly overwrite `poisoned` with
180
+ * `ready` or `idle` and hand the next caller a runtime nobody owns.
181
+ */
182
+ async function awaitSetupWithinBound(attempt, bound, registry) {
183
+ try {
184
+ await Promise.race([attempt, bound.expiry]);
185
+ } catch (error) {
186
+ if (bound.hasExpired()) {
187
+ registry.state = "poisoned";
188
+ registry.activeSelection = void 0;
189
+ registry.setupAttempt = void 0;
190
+ }
191
+ throw error;
192
+ } finally {
193
+ bound.settle();
194
+ }
195
+ }
196
+ /**
197
+ * Open a new setup attempt and publish it before it runs.
198
+ */
199
+ function startTestRuntime(requested) {
200
+ const registry = getTestLifecycleRegistry();
201
+ let publishEffectiveSelection = () => void 0;
202
+ let failEffectiveSelection = () => void 0;
203
+ const effectiveSelection = new Promise((resolve, reject) => {
204
+ publishEffectiveSelection = resolve;
205
+ failEffectiveSelection = reject;
206
+ });
207
+ effectiveSelection.catch(() => void 0);
208
+ const bound = armSetupBound(registry);
209
+ const attempt = Promise.resolve().then(() => runTestRuntimeStartup(requested, publishEffectiveSelection, failEffectiveSelection, bound));
210
+ attempt.catch(() => void 0);
211
+ const completion = awaitSetupWithinBound(attempt, bound, registry);
212
+ registry.state = "starting";
213
+ registry.setupAttempt = {
214
+ requested,
215
+ effectiveSelection,
216
+ completion
217
+ };
218
+ return completion;
219
+ }
220
+ /**
221
+ * The startup attempt itself.
222
+ *
223
+ * On failure: best-effort unwind, reset bookkeeping, rethrow the ORIGINAL error
224
+ * unchanged. A cleanup failure is a secondary diagnostic and never replaces the
225
+ * cause the caller needs to read.
226
+ */
227
+ async function runTestRuntimeStartup(requested, publishEffectiveSelection, failEffectiveSelection, bound) {
228
+ const registry = getTestLifecycleRegistry();
18
229
  try {
19
230
  Application.setEnvironment("test");
20
231
  await warlockConfigManager.load();
21
232
  await bootstrap();
22
233
  await filesOrchestrator.init();
23
234
  await loadConfigFiles(true);
24
- const connectorsToStart = config.get("tests").connectors || connectors;
25
- if (Array.isArray(connectorsToStart)) await connectorsManager.start(connectorsToStart);
26
- else await connectorsManager.startWithout(["http"]);
27
- isSetupComplete = true;
235
+ const configuredTimeout = readConfiguredSetupTimeout();
236
+ if (configuredTimeout !== void 0) bound.rearm(configuredTimeout);
237
+ const effectiveSelection = resolveEffectiveConnectors(requested);
238
+ if (!bound.hasExpired()) registry.activeSelection = effectiveSelection;
239
+ publishEffectiveSelection(effectiveSelection);
240
+ await startSelectedConnectors(effectiveSelection);
241
+ if (!bound.hasExpired()) {
242
+ registry.state = "ready";
243
+ registry.setupAttempt = void 0;
244
+ }
28
245
  } catch (error) {
246
+ failEffectiveSelection(error);
247
+ await unwindPartialStartup();
248
+ if (!bound.hasExpired()) {
249
+ registry.state = "idle";
250
+ registry.activeSelection = void 0;
251
+ registry.setupAttempt = void 0;
252
+ }
29
253
  console.error("[vitest-setup] Failed to setup test environment:", error);
30
254
  throw error;
31
255
  }
32
256
  }
257
+ /**
258
+ * `false` means none at all. Everything else starts something: an array starts
259
+ * exactly those, and `true` starts the default set — all but http, which is the
260
+ * global setup's job and shared across every worker.
261
+ */
262
+ async function startSelectedConnectors(selection) {
263
+ if (selection === false) return;
264
+ if (Array.isArray(selection)) {
265
+ await connectorsManager.start(normalizeConnectorNames(selection));
266
+ return;
267
+ }
268
+ await connectorsManager.startWithout(["http"]);
269
+ }
270
+ /**
271
+ * Best-effort teardown of whatever a failed startup managed to start.
272
+ *
273
+ * Runs while an error is already in flight, so no step here may throw and every
274
+ * step is attempted independently.
275
+ */
276
+ async function unwindPartialStartup() {
277
+ await attemptCleanupStep("application shutdown hooks", () => Application.runShutdownHooks());
278
+ await attemptCleanupStep("connectors shutdown", () => connectorsManager.shutdown());
279
+ }
280
+ /**
281
+ * Run one cleanup step, isolating its failure so the next step still runs.
282
+ */
283
+ async function attemptCleanupStep(step, run) {
284
+ try {
285
+ await run();
286
+ } catch (error) {
287
+ console.error(`[vitest-setup] Cleanup after a failed setup did not complete (${step}):`, error);
288
+ }
289
+ }
290
+ /**
291
+ * The teardown attempt itself.
292
+ */
293
+ async function runTestRuntimeShutdown() {
294
+ const registry = getTestLifecycleRegistry();
295
+ let shutdownSucceeded = false;
296
+ try {
297
+ await connectorsManager.shutdown();
298
+ shutdownSucceeded = true;
299
+ } finally {
300
+ registry.activeSelection = void 0;
301
+ registry.setupAttempt = void 0;
302
+ registry.teardownAttempt = void 0;
303
+ registry.state = shutdownSucceeded ? "idle" : "poisoned";
304
+ }
305
+ }
306
+ /**
307
+ * Reject when a ready runtime was set up with a different selection.
308
+ */
309
+ function assertReadySelectionMatches(activeSelection, requested) {
310
+ if (activeSelection === void 0) return;
311
+ assertSelectionsMatch(activeSelection, resolveEffectiveConnectors(requested));
312
+ }
313
+ /**
314
+ * Reject when an in-flight setup attempt is starting a different selection.
315
+ */
316
+ async function assertPendingSetupMatches(attempt, requested) {
317
+ const pending = attempt.requested;
318
+ if (!pending.isExplicit && !requested.isExplicit) return;
319
+ if (pending.isExplicit && requested.isExplicit) {
320
+ assertSelectionsMatch(pending.selection, requested.selection);
321
+ return;
322
+ }
323
+ assertSelectionsMatch(await attempt.effectiveSelection, resolveEffectiveConnectors(requested));
324
+ }
325
+ /**
326
+ * Name both selections: a caller looking at this message has to be able to tell
327
+ * which call to change without reading the framework's source.
328
+ */
329
+ function assertSelectionsMatch(activeSelection, requestedSelection) {
330
+ if (isSameConnectorsSelection(activeSelection, requestedSelection)) return;
331
+ throw new TestLifecycleError(`setupTest() is already using ${describeConnectorsSelection(activeSelection)}, and this call asked for ${describeConnectorsSelection(requestedSelection)}. One test runtime serves one connector selection — call teardownTest() before setting up a different one.`);
332
+ }
333
+ /**
334
+ * Await an attempt for its state transition only, not its result.
335
+ */
336
+ function settled(attempt) {
337
+ return attempt.catch(() => void 0);
338
+ }
33
339
 
34
340
  //#endregion
35
- export { setupTest };
341
+ export { TestLifecycleError, setupTest, teardownTest };
36
342
  //# sourceMappingURL=vitest-setup.mjs.map