@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.
- package/CHANGELOG.md +214 -0
- package/esm/cli/cli-commands.manager.mjs.map +1 -1
- package/esm/cli/commands/add.command.mjs +1 -1
- package/esm/cli/commands/add.command.mjs.map +1 -1
- package/esm/cli/commands/build.command.mjs +1 -1
- package/esm/cli/commands/build.command.mjs.map +1 -1
- package/esm/cli/commands/create-database.command.mjs +1 -1
- package/esm/cli/commands/create-database.command.mjs.map +1 -1
- package/esm/cli/commands/dev-server.command.mjs +1 -1
- package/esm/cli/commands/dev-server.command.mjs.map +1 -1
- package/esm/cli/commands/doctor/doctor-command.action.mjs.map +1 -1
- package/esm/cli/commands/doctor.command.mjs +1 -1
- package/esm/cli/commands/doctor.command.mjs.map +1 -1
- package/esm/cli/commands/drop-tables.command.mjs +1 -1
- package/esm/cli/commands/drop-tables.command.mjs.map +1 -1
- package/esm/cli/commands/generate/generate.command.mjs +1 -1
- package/esm/cli/commands/generate/generate.command.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/controller.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/migration.generator.mjs +1 -1
- package/esm/cli/commands/generate/generators/migration.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/model.generator.mjs +1 -1
- package/esm/cli/commands/generate/generators/model.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/module.generator.mjs +1 -1
- package/esm/cli/commands/generate/generators/module.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/repository.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/resource.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/service.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/utils/path-resolver.mjs +1 -1
- package/esm/cli/commands/generate/utils/writer.mjs +1 -1
- package/esm/cli/commands/migrate.command.mjs +1 -1
- package/esm/cli/commands/migrate.command.mjs.map +1 -1
- package/esm/cli/commands/routes/routes-command.action.mjs.map +1 -1
- package/esm/cli/commands/routes.command.mjs +1 -1
- package/esm/cli/commands/routes.command.mjs.map +1 -1
- package/esm/cli/commands/seed.command.mjs +1 -1
- package/esm/cli/commands/seed.command.mjs.map +1 -1
- package/esm/cli/commands/start-production.command.mjs +1 -1
- package/esm/cli/commands/start-production.command.mjs.map +1 -1
- package/esm/cli/commands/storage-put.action.mjs.map +1 -1
- package/esm/cli/commands/storage-put.command.mjs +1 -1
- package/esm/cli/commands/storage-put.command.mjs.map +1 -1
- package/esm/cli/commands/typings-generator.command.mjs +2 -2
- package/esm/cli/commands/typings-generator.command.mjs.map +1 -1
- package/esm/cli/commands/update.command.mjs +1 -1
- package/esm/cli/commands/update.command.mjs.map +1 -1
- package/esm/cli/commands-loader.mjs +1 -1
- package/esm/cli/commands-loader.mjs.map +1 -1
- package/esm/{cli → commands}/cli-command.d.mts +1 -1
- package/esm/commands/cli-command.d.mts.map +1 -0
- package/esm/{cli → commands}/cli-command.mjs +1 -1
- package/esm/commands/cli-command.mjs.map +1 -0
- package/esm/{cli → commands}/types.d.mts +1 -1
- package/esm/commands/types.d.mts.map +1 -0
- package/esm/config/load-config-files.mjs +1 -1
- package/esm/config/load-config-files.mjs.map +1 -1
- package/esm/connectors/base-connector.mjs +1 -1
- package/esm/connectors/base-connector.mjs.map +1 -1
- package/esm/connectors/connectors-manager.d.mts.map +1 -1
- package/esm/connectors/connectors-manager.mjs +5 -3
- package/esm/connectors/connectors-manager.mjs.map +1 -1
- package/esm/connectors/http-connector.d.mts.map +1 -1
- package/esm/connectors/http-connector.mjs +0 -2
- package/esm/connectors/http-connector.mjs.map +1 -1
- package/esm/database/create-database-action.mjs.map +1 -1
- package/esm/database/drop-tables-action.mjs.map +1 -1
- package/esm/database/migrate-action.mjs +1 -1
- package/esm/database/migrate-action.mjs.map +1 -1
- package/esm/database/seed-command-action.mjs +1 -1
- package/esm/database/seed-command-action.mjs.map +1 -1
- package/esm/dev-server/dev-logger.mjs +1 -1
- package/esm/dev-server/dev-logger.mjs.map +1 -1
- package/esm/dev-server/development-server.mjs +2 -2
- package/esm/dev-server/file-event-handler.mjs +1 -1
- package/esm/dev-server/file-event-handler.mjs.map +1 -1
- package/esm/dev-server/file-manager.mjs +1 -1
- package/esm/dev-server/file-manager.mjs.map +1 -1
- package/esm/dev-server/file-operations.mjs +1 -1
- package/esm/dev-server/file-operations.mjs.map +1 -1
- package/esm/dev-server/files-orchestrator.mjs +3 -18
- package/esm/dev-server/files-orchestrator.mjs.map +1 -1
- package/esm/dev-server/files-watcher.mjs +1 -1
- package/esm/dev-server/files-watcher.mjs.map +1 -1
- package/esm/dev-server/health-checker/file-health-checker.contract.d.mts +1 -1
- package/esm/dev-server/layer-executor.mjs +1 -1
- package/esm/dev-server/loader/register-loader.mjs +1 -1
- package/esm/dev-server/package-json-manager.mjs +1 -1
- package/esm/dev-server/package-json-manager.mjs.map +1 -1
- package/esm/dev-server/parse-imports.mjs +2 -2
- package/esm/dev-server/parse-imports.mjs.map +1 -1
- package/esm/dev-server/tsconfig-manager.mjs +1 -1
- package/esm/dev-server/tsconfig-manager.mjs.map +1 -1
- package/esm/dev-server/type-generator.mjs +1 -1
- package/esm/dev-server/type-generator.mjs.map +1 -1
- package/esm/dev-server/utils.mjs +1 -1
- package/esm/dev-server/utils.mjs.map +1 -1
- package/esm/generations/add-command.action.mjs +20 -8
- package/esm/generations/add-command.action.mjs.map +1 -1
- package/esm/http/build-cors-options.mjs +32 -0
- package/esm/http/build-cors-options.mjs.map +1 -0
- package/esm/http/middleware/max-body-size.middleware.d.mts +13 -5
- package/esm/http/middleware/max-body-size.middleware.d.mts.map +1 -1
- package/esm/http/middleware/max-body-size.middleware.mjs +13 -5
- package/esm/http/middleware/max-body-size.middleware.mjs.map +1 -1
- package/esm/http/plugins.d.mts.map +1 -1
- package/esm/http/plugins.mjs +2 -9
- package/esm/http/plugins.mjs.map +1 -1
- package/esm/http/server.d.mts.map +1 -1
- package/esm/http/server.mjs +3 -8
- package/esm/http/server.mjs.map +1 -1
- package/esm/index.d.mts +9 -17
- package/esm/index.mjs +4 -15
- package/esm/production/production-builder.mjs +51 -3
- package/esm/production/production-builder.mjs.map +1 -1
- package/esm/production/resolve-build-config.mjs +1 -1
- package/esm/production/resolve-build-config.mjs.map +1 -1
- package/esm/router/router.d.mts +9 -0
- package/esm/router/router.d.mts.map +1 -1
- package/esm/router/router.mjs +58 -5
- package/esm/router/router.mjs.map +1 -1
- package/esm/router/types.d.mts +24 -1
- package/esm/router/types.d.mts.map +1 -1
- package/esm/tests/index.d.mts +3 -2
- package/esm/tests/index.mjs +2 -3
- package/esm/tests/start-http-development-server.d.mts.map +1 -1
- package/esm/tests/start-http-development-server.mjs +38 -15
- package/esm/tests/start-http-development-server.mjs.map +1 -1
- package/esm/tests/test-connectors-selection.d.mts +14 -0
- package/esm/tests/test-connectors-selection.d.mts.map +1 -0
- package/esm/tests/test-connectors-selection.mjs +72 -0
- package/esm/tests/test-connectors-selection.mjs.map +1 -0
- package/esm/tests/test-helpers.d.mts.map +1 -1
- package/esm/tests/test-helpers.mjs +34 -13
- package/esm/tests/test-helpers.mjs.map +1 -1
- package/esm/tests/test-lifecycle-state.mjs +27 -0
- package/esm/tests/test-lifecycle-state.mjs.map +1 -0
- package/esm/tests/test-setup-timeout.mjs +53 -0
- package/esm/tests/test-setup-timeout.mjs.map +1 -0
- package/esm/tests/vitest-setup.d.mts +64 -8
- package/esm/tests/vitest-setup.d.mts.map +1 -1
- package/esm/tests/vitest-setup.mjs +317 -11
- package/esm/tests/vitest-setup.mjs.map +1 -1
- package/esm/utils/framework-vesion.mjs +1 -1
- package/esm/utils/index.d.mts +1 -0
- package/esm/utils/index.mjs +1 -0
- package/esm/utils/normalized-path.d.mts +55 -0
- package/esm/utils/normalized-path.d.mts.map +1 -0
- package/esm/{dev-server/path.mjs → utils/normalized-path.mjs} +15 -2
- package/esm/utils/normalized-path.mjs.map +1 -0
- package/esm/vite/index.d.mts +2 -0
- package/esm/vite/index.mjs +1 -1
- package/esm/vite/lower-stage3-decorators.d.mts +1 -1
- package/esm/vite/lower-stage3-decorators.mjs +1 -1
- package/esm/vite/lower-stage3-decorators.mjs.map +1 -1
- package/esm/warlock-config/index.mjs +1 -1
- package/esm/warlock-config/types.d.mts +34 -1
- package/esm/warlock-config/types.d.mts.map +1 -1
- package/esm/warlock-config/warlock-config.manager.d.mts.map +1 -1
- package/esm/warlock-config/warlock-config.manager.mjs +1 -2
- package/esm/warlock-config/warlock-config.manager.mjs.map +1 -1
- package/llms-full.txt +78 -23
- package/llms.txt +3 -3
- package/package.json +24 -12
- package/skills/lower-stage3-decorators/SKILL.md +4 -2
- package/skills/run-app/SKILL.md +6 -0
- package/skills/test-http/SKILL.md +37 -12
- package/skills/test-service/SKILL.md +167 -24
- package/skills/use-middleware/SKILL.md +5 -1
- package/esm/cli/cli-command.d.mts.map +0 -1
- package/esm/cli/cli-command.mjs.map +0 -1
- package/esm/cli/types.d.mts.map +0 -1
- package/esm/dev-server/files-orchestrator.d.mts +0 -89
- package/esm/dev-server/files-orchestrator.d.mts.map +0 -1
- package/esm/dev-server/files-watcher.d.mts +0 -69
- package/esm/dev-server/files-watcher.d.mts.map +0 -1
- package/esm/dev-server/health-checker/checkers/base-health-checker.d.mts +0 -55
- package/esm/dev-server/health-checker/checkers/base-health-checker.d.mts.map +0 -1
- package/esm/dev-server/health-checker/checkers/eslint-health-checker.d.mts +0 -44
- package/esm/dev-server/health-checker/checkers/eslint-health-checker.d.mts.map +0 -1
- package/esm/dev-server/health-checker/checkers/typescript-health-checker.d.mts +0 -60
- package/esm/dev-server/health-checker/checkers/typescript-health-checker.d.mts.map +0 -1
- package/esm/dev-server/health-checker/files-healthcare.manager.d.mts +0 -119
- package/esm/dev-server/health-checker/files-healthcare.manager.d.mts.map +0 -1
- package/esm/dev-server/health-checker/index.mjs +0 -7
- package/esm/dev-server/module-loader.d.mts +0 -63
- package/esm/dev-server/module-loader.d.mts.map +0 -1
- package/esm/dev-server/path.mjs.map +0 -1
- package/esm/tests/test-server-port-channel.d.mts +0 -27
- package/esm/tests/test-server-port-channel.d.mts.map +0 -1
- /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
|
|
67
|
+
return sendRequest(path, {
|
|
47
68
|
...options,
|
|
48
69
|
method: "POST",
|
|
49
|
-
body:
|
|
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
|
|
77
|
+
return sendRequest(path, {
|
|
57
78
|
...options,
|
|
58
79
|
method: "PUT",
|
|
59
|
-
body:
|
|
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
|
|
96
|
+
return sendRequest(path, {
|
|
76
97
|
...options,
|
|
77
98
|
method: "PATCH",
|
|
78
|
-
body:
|
|
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
|
|
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 {
|
|
1
|
+
import { TestConnectorsSelection } from "./test-connectors-selection.mjs";
|
|
2
|
+
|
|
2
3
|
//#region ../core/src/tests/vitest-setup.d.ts
|
|
3
|
-
type
|
|
4
|
-
|
|
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
|
-
*
|
|
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
|
|
10
|
-
|
|
11
|
-
}
|
|
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":"
|
|
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
|
-
*
|
|
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
|
-
|
|
17
|
-
|
|
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
|
|
25
|
-
if (
|
|
26
|
-
|
|
27
|
-
|
|
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
|