@warlock.js/core 5.34.2 → 5.35.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 +62 -0
- package/esm/cli/cli-commands.manager.mjs +1 -1
- package/esm/cli/cli-commands.manager.mjs.map +1 -1
- package/esm/cli/commands/add.command.mjs +1 -0
- package/esm/cli/commands/add.command.mjs.map +1 -1
- package/esm/cli/commands/build.command.mjs +30 -3
- package/esm/cli/commands/build.command.mjs.map +1 -1
- package/esm/cli/commands/check/check-command.action.mjs +1 -0
- package/esm/cli/commands/check/check-command.action.mjs.map +1 -1
- package/esm/cli/commands/check.command.mjs +5 -0
- package/esm/cli/commands/check.command.mjs.map +1 -1
- package/esm/cli/commands/explain/explain-command.action.mjs +21 -0
- package/esm/cli/commands/explain/explain-command.action.mjs.map +1 -1
- package/esm/cli/commands/explain.command.mjs +14 -4
- package/esm/cli/commands/explain.command.mjs.map +1 -1
- package/esm/cli/commands/generate/generate.command.mjs +19 -1
- package/esm/cli/commands/generate/generate.command.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/endpoint.generator.mjs +1 -1
- package/esm/cli/commands/generate/generators/endpoint.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/web.generator.mjs +169 -0
- package/esm/cli/commands/generate/generators/web.generator.mjs.map +1 -0
- package/esm/cli/commands/generate/templates/web-stubs.mjs +231 -0
- package/esm/cli/commands/generate/templates/web-stubs.mjs.map +1 -0
- package/esm/cli/commands/inspect/inspect-command.action.mjs +4 -3
- package/esm/cli/commands/inspect/inspect-command.action.mjs.map +1 -1
- package/esm/cli/commands/inspect.command.mjs +13 -8
- package/esm/cli/commands/inspect.command.mjs.map +1 -1
- package/esm/cli/commands/mcp/mcp-command.action.mjs +1 -1
- package/esm/cli/commands/mcp/mcp-command.action.mjs.map +1 -1
- package/esm/config/config-loader.d.mts.map +1 -1
- package/esm/config/config-loader.mjs +2 -0
- package/esm/config/config-loader.mjs.map +1 -1
- package/esm/config/load-config-files.mjs +4 -0
- package/esm/config/load-config-files.mjs.map +1 -1
- package/esm/dev-server/file-event-handler.mjs +11 -1
- package/esm/dev-server/file-event-handler.mjs.map +1 -1
- package/esm/dev-server/file-manager.d.mts +1 -1
- package/esm/dev-server/file-manager.d.mts.map +1 -1
- package/esm/dev-server/file-manager.mjs.map +1 -1
- package/esm/dev-server/files-orchestrator.mjs +16 -1
- package/esm/dev-server/files-orchestrator.mjs.map +1 -1
- package/esm/dev-server/files-watcher.mjs +13 -4
- package/esm/dev-server/files-watcher.mjs.map +1 -1
- package/esm/dev-server/loader/loader-port-acks.mjs +21 -9
- package/esm/dev-server/loader/loader-port-acks.mjs.map +1 -1
- package/esm/dev-server/loader/register-loader.mjs +31 -12
- package/esm/dev-server/loader/register-loader.mjs.map +1 -1
- package/esm/dev-server/module-loader.mjs +14 -0
- package/esm/dev-server/module-loader.mjs.map +1 -1
- package/esm/dev-server/special-file-patterns.mjs +4 -1
- package/esm/dev-server/special-file-patterns.mjs.map +1 -1
- package/esm/e2e/browser.d.mts.map +1 -1
- package/esm/e2e/browser.mjs +3 -1
- package/esm/e2e/browser.mjs.map +1 -1
- package/esm/e2e/index.d.mts +2 -2
- package/esm/e2e/page.d.mts +6 -1
- package/esm/e2e/page.d.mts.map +1 -1
- package/esm/e2e/page.mjs +36 -1
- package/esm/e2e/page.mjs.map +1 -1
- package/esm/e2e/types.d.mts +37 -0
- package/esm/e2e/types.d.mts.map +1 -1
- package/esm/e2e/types.mjs.map +1 -1
- package/esm/errors/error-codes.d.mts +48 -0
- package/esm/errors/error-codes.d.mts.map +1 -1
- package/esm/errors/error-codes.mjs +48 -0
- package/esm/errors/error-codes.mjs.map +1 -1
- package/esm/errors/index.d.mts +2 -2
- package/esm/errors/index.mjs +2 -2
- package/esm/generations/add-command.action.mjs +2 -0
- package/esm/generations/add-command.action.mjs.map +1 -1
- package/esm/generations/design-system-payload.mjs +53 -0
- package/esm/generations/design-system-payload.mjs.map +1 -0
- package/esm/generations/features/design-system.feature.mjs +38 -0
- package/esm/generations/features/design-system.feature.mjs.map +1 -0
- package/esm/generations/features/index.mjs +3 -1
- package/esm/generations/features/index.mjs.map +1 -1
- package/esm/generations/features/notifications.feature.mjs +13 -2
- package/esm/generations/features/notifications.feature.mjs.map +1 -1
- package/esm/generations/features/test.feature.mjs +48 -13
- package/esm/generations/features/test.feature.mjs.map +1 -1
- package/esm/generations/stubs.mjs +9 -5
- package/esm/generations/stubs.mjs.map +1 -1
- package/esm/http/error-mapper.d.mts +1 -1
- package/esm/http/error-mapper.mjs +3 -3
- package/esm/http/error-mapper.mjs.map +1 -1
- package/esm/http/error-response.d.mts +15 -0
- package/esm/http/error-response.d.mts.map +1 -0
- package/esm/http/error-response.mjs +47 -0
- package/esm/http/error-response.mjs.map +1 -0
- package/esm/http/errors/errors.d.mts +8 -0
- package/esm/http/errors/errors.d.mts.map +1 -1
- package/esm/http/errors/errors.mjs +13 -1
- package/esm/http/errors/errors.mjs.map +1 -1
- package/esm/http/index.d.mts +5 -3
- package/esm/http/index.mjs +3 -2
- package/esm/http/list-endpoint.d.mts +35 -0
- package/esm/http/list-endpoint.d.mts.map +1 -0
- package/esm/http/list-endpoint.mjs +28 -0
- package/esm/http/list-endpoint.mjs.map +1 -0
- package/esm/http/middleware/inject-request-context.d.mts.map +1 -1
- package/esm/http/middleware/inject-request-context.mjs +12 -2
- package/esm/http/middleware/inject-request-context.mjs.map +1 -1
- package/esm/http/response.d.mts +2 -0
- package/esm/http/response.d.mts.map +1 -1
- package/esm/http/response.mjs +2 -0
- package/esm/http/response.mjs.map +1 -1
- package/esm/http/server.d.mts.map +1 -1
- package/esm/http/server.mjs +2 -0
- package/esm/http/server.mjs.map +1 -1
- package/esm/http/types.d.mts +7 -0
- package/esm/http/types.d.mts.map +1 -1
- package/esm/index.d.mts +10 -7
- package/esm/index.mjs +3 -2
- package/esm/openapi/security.mjs +12 -4
- package/esm/openapi/security.mjs.map +1 -1
- package/esm/openapi/types.d.mts +2 -0
- package/esm/openapi/types.d.mts.map +1 -1
- package/esm/production/build-budgets.d.mts +46 -0
- package/esm/production/build-budgets.d.mts.map +1 -0
- package/esm/production/build-budgets.mjs +109 -0
- package/esm/production/build-budgets.mjs.map +1 -0
- package/esm/production/production-builder.mjs +13 -1
- package/esm/production/production-builder.mjs.map +1 -1
- package/esm/production/resolve-build-config.d.mts +1 -1
- package/esm/production/resolve-build-config.d.mts.map +1 -1
- package/esm/production/resolve-build-config.mjs.map +1 -1
- package/esm/restful/restful.d.mts +1 -1
- package/esm/tests/fakes/storage-fake.d.mts +3 -1
- package/esm/tests/fakes/storage-fake.d.mts.map +1 -1
- package/esm/tests/fakes/storage-fake.mjs +3 -3
- package/esm/tests/fakes/storage-fake.mjs.map +1 -1
- package/esm/tests/fakes/test-fakes.d.mts +3 -1
- package/esm/tests/fakes/test-fakes.d.mts.map +1 -1
- package/esm/tests/fakes/test-fakes.mjs +3 -2
- package/esm/tests/fakes/test-fakes.mjs.map +1 -1
- package/esm/tests/index.d.mts +4 -2
- package/esm/tests/index.mjs +3 -1
- package/esm/tests/load-unit-config.mjs +24 -0
- package/esm/tests/load-unit-config.mjs.map +1 -0
- package/esm/tests/response-contract.d.mts +32 -0
- package/esm/tests/response-contract.d.mts.map +1 -0
- package/esm/tests/response-contract.mjs +116 -0
- package/esm/tests/response-contract.mjs.map +1 -0
- package/esm/tests/start-http-development-server.d.mts.map +1 -1
- package/esm/tests/start-http-development-server.mjs +10 -2
- package/esm/tests/start-http-development-server.mjs.map +1 -1
- package/esm/tests/test-lifecycle-state.mjs.map +1 -1
- package/esm/tests/test-locales.mjs +33 -0
- package/esm/tests/test-locales.mjs.map +1 -0
- package/esm/tests/test-runtime-scope.d.mts +15 -0
- package/esm/tests/test-runtime-scope.d.mts.map +1 -0
- package/esm/tests/test-runtime-scope.mjs +80 -0
- package/esm/tests/test-runtime-scope.mjs.map +1 -0
- package/esm/tests/vitest-setup.d.mts +13 -3
- package/esm/tests/vitest-setup.d.mts.map +1 -1
- package/esm/tests/vitest-setup.mjs +146 -18
- package/esm/tests/vitest-setup.mjs.map +1 -1
- package/esm/tooling/check/affected-files.d.mts +5 -0
- package/esm/tooling/check/affected-files.d.mts.map +1 -0
- package/esm/tooling/check/affected-files.mjs +56 -0
- package/esm/tooling/check/affected-files.mjs.map +1 -0
- package/esm/tooling/check/changed-files.d.mts +2 -0
- package/esm/tooling/check/changed-files.d.mts.map +1 -1
- package/esm/tooling/check/changed-files.mjs +2 -2
- package/esm/tooling/check/changed-files.mjs.map +1 -1
- package/esm/tooling/check/conventions-step.d.mts +2 -1
- package/esm/tooling/check/conventions-step.d.mts.map +1 -1
- package/esm/tooling/check/conventions-step.mjs +48 -6
- package/esm/tooling/check/conventions-step.mjs.map +1 -1
- package/esm/tooling/check/fixes.d.mts +8 -0
- package/esm/tooling/check/fixes.d.mts.map +1 -0
- package/esm/tooling/check/fixes.mjs +31 -0
- package/esm/tooling/check/fixes.mjs.map +1 -0
- package/esm/tooling/check/index.d.mts +12 -0
- package/esm/tooling/check/index.mjs +3 -2
- package/esm/tooling/check/run-check.d.mts +2 -0
- package/esm/tooling/check/run-check.d.mts.map +1 -1
- package/esm/tooling/check/run-check.mjs +20 -4
- package/esm/tooling/check/run-check.mjs.map +1 -1
- package/esm/tooling/check/run-tests.mjs +1 -1
- package/esm/tooling/check/source-ast.d.mts +1 -0
- package/esm/tooling/check/web-conventions.d.mts +10 -0
- package/esm/tooling/check/web-conventions.d.mts.map +1 -0
- package/esm/tooling/check/web-conventions.mjs +126 -0
- package/esm/tooling/check/web-conventions.mjs.map +1 -0
- package/esm/tooling/index.d.mts +7 -2
- package/esm/tooling/index.mjs +4 -2
- package/esm/tooling/inspect/collect-files.mjs +172 -0
- package/esm/tooling/inspect/collect-files.mjs.map +1 -0
- package/esm/tooling/inspect/collect-pages.mjs +3 -2
- package/esm/tooling/inspect/collect-pages.mjs.map +1 -1
- package/esm/tooling/inspect/collect-routes.mjs +1 -1
- package/esm/tooling/inspect/collect-routes.mjs.map +1 -1
- package/esm/tooling/inspect/explain-file.d.mts +12 -0
- package/esm/tooling/inspect/explain-file.d.mts.map +1 -0
- package/esm/tooling/inspect/explain-file.mjs +45 -0
- package/esm/tooling/inspect/explain-file.mjs.map +1 -0
- package/esm/tooling/inspect/index.d.mts +3 -2
- package/esm/tooling/inspect/index.mjs +2 -1
- package/esm/tooling/inspect/inspect-app.d.mts.map +1 -1
- package/esm/tooling/inspect/inspect-app.mjs +9 -3
- package/esm/tooling/inspect/inspect-app.mjs.map +1 -1
- package/esm/tooling/inspect/inspect-report.schema.mjs +34 -1
- package/esm/tooling/inspect/inspect-report.schema.mjs.map +1 -1
- package/esm/tooling/inspect/inspect-report.types.d.mts +17 -1
- package/esm/tooling/inspect/inspect-report.types.d.mts.map +1 -1
- package/esm/tooling/inspect/inspect-report.types.mjs +2 -1
- package/esm/tooling/inspect/inspect-report.types.mjs.map +1 -1
- package/esm/tooling/mcp/mcp-tools.d.mts.map +1 -1
- package/esm/tooling/mcp/mcp-tools.mjs +8 -2
- package/esm/tooling/mcp/mcp-tools.mjs.map +1 -1
- package/esm/tooling/problem.d.mts +4 -0
- package/esm/tooling/problem.d.mts.map +1 -1
- package/esm/tooling/problem.mjs +1 -0
- package/esm/tooling/problem.mjs.map +1 -1
- package/esm/tooling/syntax/ast.d.mts +2 -0
- package/esm/tooling/typecheck/run-typecheck.mjs +5 -1
- package/esm/tooling/typecheck/run-typecheck.mjs.map +1 -1
- package/esm/upgrade/codemods/toolchain-templates.mjs +3 -1
- package/esm/upgrade/codemods/toolchain-templates.mjs.map +1 -1
- package/esm/vite/index.d.mts +3 -1
- package/esm/vite/index.mjs +3 -1
- package/esm/vite/test-setup-reporter.d.mts +16 -0
- package/esm/vite/test-setup-reporter.d.mts.map +1 -0
- package/esm/vite/test-setup-reporter.mjs +55 -0
- package/esm/vite/test-setup-reporter.mjs.map +1 -0
- package/esm/vite/test-workers.d.mts +5 -0
- package/esm/vite/test-workers.d.mts.map +1 -0
- package/esm/vite/test-workers.mjs +38 -0
- package/esm/vite/test-workers.mjs.map +1 -0
- package/esm/warlock-config/index.d.mts +2 -1
- package/esm/warlock-config/types.d.mts +3 -0
- package/esm/warlock-config/types.d.mts.map +1 -1
- package/llms-full.txt +883 -70
- package/llms.txt +58 -56
- package/package.json +15 -14
- package/schemas/inspect-report.schema.json +47 -1
- package/skills/build-budgets/SKILL.md +51 -0
- package/skills/build-restful/SKILL.md +56 -0
- package/skills/configure-app/SKILL.md +34 -0
- package/skills/create-module/SKILL.md +9 -0
- package/skills/generate-openapi/SKILL.md +1 -1
- package/skills/generate-web-ui/SKILL.md +71 -0
- package/skills/index.md +3 -3
- package/skills/run-app/SKILL.md +1 -1
- package/skills/send-mail/SKILL.md +25 -0
- package/skills/send-response/SKILL.md +77 -0
- package/skills/test-browser/SKILL.md +54 -0
- package/skills/test-http/SKILL.md +91 -0
- package/skills/test-service/SKILL.md +161 -3
- package/skills/upgrade-from-5-29/SKILL.md +22 -0
- package/skills/use-localization/SKILL.md +21 -0
- package/skills/warlock-check/SKILL.md +72 -1
- package/skills/warlock-inspect/SKILL.md +45 -2
- package/skills/warlock-mcp/SKILL.md +4 -0
- package/skills/warlock-trace/SKILL.md +2 -2
- package/skills/write-cli-command/SKILL.md +20 -2
- package/esm/production/build-app-production.mjs +0 -14
- package/esm/production/build-app-production.mjs.map +0 -1
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { router } from "../router/router.mjs";
|
|
2
|
+
import { buildDeclaredResponses } from "../openapi/response-schemas.mjs";
|
|
3
|
+
import { SchemaRegistry } from "../openapi/schema-registry.mjs";
|
|
4
|
+
import { testRequest } from "./test-helpers.mjs";
|
|
5
|
+
|
|
6
|
+
//#region ../../packages/core/src/tests/response-contract.ts
|
|
7
|
+
function record(value) {
|
|
8
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
9
|
+
}
|
|
10
|
+
/** Validate the JSON Schema subset emitted by Core's response-schema translator. */
|
|
11
|
+
function matches(value, schema, refs) {
|
|
12
|
+
if (typeof schema.$ref === "string") {
|
|
13
|
+
const target = refs[schema.$ref.replace("#/components/schemas/", "")];
|
|
14
|
+
if (!target) throw new Error(`Unresolved response schema reference: ${schema.$ref}`);
|
|
15
|
+
return matches(value, target, refs);
|
|
16
|
+
}
|
|
17
|
+
for (const key of ["oneOf", "anyOf"]) {
|
|
18
|
+
const alternatives = schema[key];
|
|
19
|
+
if (Array.isArray(alternatives)) {
|
|
20
|
+
const count = alternatives.filter((entry) => record(entry) && matches(value, entry, refs)).length;
|
|
21
|
+
return key === "oneOf" ? count === 1 : count > 0;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (Array.isArray(schema.type)) return schema.type.some((type) => matches(value, {
|
|
25
|
+
...schema,
|
|
26
|
+
type
|
|
27
|
+
}, refs));
|
|
28
|
+
switch (schema.type) {
|
|
29
|
+
case "null": return value === null;
|
|
30
|
+
case "string": return typeof value === "string";
|
|
31
|
+
case "number": return typeof value === "number" && Number.isFinite(value);
|
|
32
|
+
case "integer": return typeof value === "number" && Number.isInteger(value);
|
|
33
|
+
case "boolean": return typeof value === "boolean";
|
|
34
|
+
case "array": {
|
|
35
|
+
const items = schema.items;
|
|
36
|
+
return Array.isArray(value) && (!record(items) || value.every((item) => matches(item, items, refs)));
|
|
37
|
+
}
|
|
38
|
+
case "object": {
|
|
39
|
+
if (!record(value)) return false;
|
|
40
|
+
if (Array.isArray(schema.required) && schema.required.some((key) => typeof key === "string" && !Object.hasOwn(value, key))) return false;
|
|
41
|
+
const properties = schema.properties;
|
|
42
|
+
return !record(properties) || Object.entries(properties).every(([key, child]) => !Object.hasOwn(value, key) || record(child) && matches(value[key], child, refs));
|
|
43
|
+
}
|
|
44
|
+
default: throw new Error("Response schema has no verifiable static shape.");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Call real HTTP routes and check their serialized bodies against responseSchema.
|
|
49
|
+
* Throws on undeclared statuses, mismatches, empty discovery or unsupported shapes.
|
|
50
|
+
* Additional response fields are allowed, as in Core's OpenAPI declarations.
|
|
51
|
+
*/
|
|
52
|
+
async function assertResponseContracts(options = {}) {
|
|
53
|
+
const routes = options.routes ?? router.list().filter((route) => route.handler.responseSchema);
|
|
54
|
+
if (routes.length === 0) throw new Error("No response contracts found. Load routes in this process first.");
|
|
55
|
+
const plans = routes.map((route) => {
|
|
56
|
+
const label = `${route.method} ${route.path}`;
|
|
57
|
+
if (!route.handler.responseSchema) throw new Error(`${label}: missing responseSchema.`);
|
|
58
|
+
const registry = new SchemaRegistry(void 0, (warning) => {
|
|
59
|
+
throw new Error(`${label}: ${warning}`);
|
|
60
|
+
});
|
|
61
|
+
const responses = buildDeclaredResponses(route.handler.responseSchema, registry, (warning) => {
|
|
62
|
+
throw new Error(`${label}: ${warning}`);
|
|
63
|
+
});
|
|
64
|
+
if (Object.keys(responses).length === 0) throw new Error(`${label}: empty responseSchema.`);
|
|
65
|
+
const cases = options.cases?.(route) ?? (route.method.toUpperCase() === "GET" && !/[:*]/.test(route.path) ? [{}] : []);
|
|
66
|
+
if (cases.length === 0) throw new Error(`${label}: provide contract request fixtures.`);
|
|
67
|
+
for (const fixture of cases) {
|
|
68
|
+
if (route.method === "all" && !fixture.method) throw new Error(`${label}: provide a concrete fixture method.`);
|
|
69
|
+
if (/[:*]/.test((fixture.path ?? route.path).split("?")[0] ?? "")) throw new Error(`${label}: provide a concrete fixture path.`);
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
route,
|
|
73
|
+
label,
|
|
74
|
+
registry,
|
|
75
|
+
responses,
|
|
76
|
+
cases
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
const client = options.client ?? { request: testRequest };
|
|
80
|
+
for (const { route, label, registry, responses, cases } of plans) {
|
|
81
|
+
const covered = /* @__PURE__ */ new Set();
|
|
82
|
+
for (const fixture of cases) {
|
|
83
|
+
const response = await client.request(fixture.path ?? route.path, {
|
|
84
|
+
...fixture.init,
|
|
85
|
+
method: route.method === "all" ? fixture.method : route.method,
|
|
86
|
+
redirect: "manual"
|
|
87
|
+
});
|
|
88
|
+
const status = String(response.status);
|
|
89
|
+
if (fixture.status !== void 0 && fixture.status !== response.status) throw new Error(`${label}: expected status ${fixture.status}, received ${status}.`);
|
|
90
|
+
const declared = responses[status];
|
|
91
|
+
if (!declared) throw new Error(`${label}: undeclared response status ${status}.`);
|
|
92
|
+
const schema = declared.content?.["application/json"]?.schema;
|
|
93
|
+
if (route.method === "HEAD" || route.method === "all" && fixture.method === "HEAD" || response.status === 204 || response.status === 304) {
|
|
94
|
+
if (await response.text()) throw new Error(`${label}: status ${status} must have no body.`);
|
|
95
|
+
} else {
|
|
96
|
+
if (!schema) throw new Error(`${label}: status ${status} has no body schema.`);
|
|
97
|
+
let body;
|
|
98
|
+
try {
|
|
99
|
+
body = await response.json();
|
|
100
|
+
} catch {
|
|
101
|
+
throw new Error(`${label}: status ${status} did not return JSON.`);
|
|
102
|
+
}
|
|
103
|
+
if (!matches(body, schema, registry.schemas)) throw new Error(`${label}: response body does not match responseSchema for status ${status}.`);
|
|
104
|
+
}
|
|
105
|
+
covered.add(status);
|
|
106
|
+
}
|
|
107
|
+
if (options.requireAllStatuses) {
|
|
108
|
+
const missing = Object.keys(responses).filter((status) => !covered.has(status));
|
|
109
|
+
if (missing.length) throw new Error(`${label}: untested declared statuses: ${missing.join(", ")}.`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
//#endregion
|
|
115
|
+
export { assertResponseContracts };
|
|
116
|
+
//# sourceMappingURL=response-contract.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"response-contract.mjs","names":[],"sources":["../../../../../../../../packages/core/src/tests/response-contract.ts"],"sourcesContent":["import { buildDeclaredResponses } from \"../openapi/response-schemas\";\nimport { SchemaRegistry } from \"../openapi/schema-registry\";\nimport type { OpenApiSchema } from \"../openapi/types\";\nimport { router } from \"../router/router\";\nimport type { RequestMethod, Route } from \"../router/types\";\nimport { testRequest } from \"./test-helpers\";\n\nexport type ResponseContractRoute = Pick<Route, \"method\" | \"path\" | \"handler\">;\nexport type ResponseContractCase = {\n /** Concrete HTTP method for an `all` route. */\n method?: Exclude<RequestMethod, \"all\">;\n /** Concrete path, including query values, for parameterized routes. */\n path?: string;\n init?: Omit<RequestInit, \"method\">;\n /** Assert the intended scenario's status as well as its body. */\n status?: number;\n};\nexport type ResponseContractOptions = {\n /** Defaults to registered routes with responseSchema in this process. */\n routes?: readonly ResponseContractRoute[];\n /** Required for parameterized routes and methods other than GET. No cases are skipped. */\n cases?: (route: ResponseContractRoute) => readonly ResponseContractCase[];\n /** Use an in-process server or an authenticated client; defaults to testRequest. */\n client?: { request(path: string, init?: RequestInit): Promise<Response> };\n /** Require fixtures to exercise every declared status. Defaults to false. */\n requireAllStatuses?: boolean;\n};\n\nfunction record(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === \"object\" && !Array.isArray(value);\n}\n\n/** Validate the JSON Schema subset emitted by Core's response-schema translator. */\nfunction matches(\n value: unknown,\n schema: OpenApiSchema,\n refs: Record<string, OpenApiSchema>,\n): boolean {\n if (typeof schema.$ref === \"string\") {\n const target = refs[schema.$ref.replace(\"#/components/schemas/\", \"\")];\n if (!target) throw new Error(`Unresolved response schema reference: ${schema.$ref}`);\n return matches(value, target, refs);\n }\n for (const key of [\"oneOf\", \"anyOf\"]) {\n const alternatives = schema[key];\n if (Array.isArray(alternatives)) {\n const count = alternatives.filter(\n (entry: unknown) => record(entry) && matches(value, entry, refs),\n ).length;\n return key === \"oneOf\" ? count === 1 : count > 0;\n }\n }\n if (Array.isArray(schema.type)) {\n return schema.type.some((type: unknown) => matches(value, { ...schema, type }, refs));\n }\n switch (schema.type) {\n case \"null\":\n return value === null;\n case \"string\":\n return typeof value === \"string\";\n case \"number\":\n return typeof value === \"number\" && Number.isFinite(value);\n case \"integer\":\n return typeof value === \"number\" && Number.isInteger(value);\n case \"boolean\":\n return typeof value === \"boolean\";\n case \"array\": {\n const items = schema.items;\n return (\n Array.isArray(value) &&\n (!record(items) || value.every((item: unknown) => matches(item, items, refs)))\n );\n }\n case \"object\": {\n if (!record(value)) return false;\n if (\n Array.isArray(schema.required) &&\n schema.required.some(\n (key: unknown) => typeof key === \"string\" && !Object.hasOwn(value, key),\n )\n )\n return false;\n const properties = schema.properties;\n return (\n !record(properties) ||\n Object.entries(properties).every(\n ([key, child]) =>\n !Object.hasOwn(value, key) || (record(child) && matches(value[key], child, refs)),\n )\n );\n }\n default:\n throw new Error(\"Response schema has no verifiable static shape.\");\n }\n}\n\n/**\n * Call real HTTP routes and check their serialized bodies against responseSchema.\n * Throws on undeclared statuses, mismatches, empty discovery or unsupported shapes.\n * Additional response fields are allowed, as in Core's OpenAPI declarations.\n */\nexport async function assertResponseContracts(\n options: ResponseContractOptions = {},\n): Promise<void> {\n const routes = options.routes ?? router.list().filter((route) => route.handler.responseSchema);\n if (routes.length === 0)\n throw new Error(\"No response contracts found. Load routes in this process first.\");\n const plans = routes.map((route) => {\n const label = `${route.method} ${route.path}`;\n if (!route.handler.responseSchema) throw new Error(`${label}: missing responseSchema.`);\n const registry = new SchemaRegistry(undefined, (warning) => {\n throw new Error(`${label}: ${warning}`);\n });\n const responses = buildDeclaredResponses(route.handler.responseSchema, registry, (warning) => {\n throw new Error(`${label}: ${warning}`);\n });\n if (Object.keys(responses).length === 0) throw new Error(`${label}: empty responseSchema.`);\n const cases: readonly ResponseContractCase[] =\n options.cases?.(route) ??\n (route.method.toUpperCase() === \"GET\" && !/[:*]/.test(route.path) ? [{}] : []);\n if (cases.length === 0) throw new Error(`${label}: provide contract request fixtures.`);\n for (const fixture of cases) {\n if (route.method === \"all\" && !fixture.method) {\n throw new Error(`${label}: provide a concrete fixture method.`);\n }\n if (/[:*]/.test((fixture.path ?? route.path).split(\"?\")[0] ?? \"\"))\n throw new Error(`${label}: provide a concrete fixture path.`);\n }\n return { route, label, registry, responses, cases };\n });\n const client = options.client ?? { request: testRequest };\n for (const { route, label, registry, responses, cases } of plans) {\n const covered = new Set<string>();\n for (const fixture of cases) {\n const response = await client.request(fixture.path ?? route.path, {\n ...fixture.init,\n method: route.method === \"all\" ? fixture.method : route.method,\n redirect: \"manual\",\n });\n const status = String(response.status);\n if (fixture.status !== undefined && fixture.status !== response.status) {\n throw new Error(`${label}: expected status ${fixture.status}, received ${status}.`);\n }\n const declared = responses[status];\n if (!declared) throw new Error(`${label}: undeclared response status ${status}.`);\n const schema = declared.content?.[\"application/json\"]?.schema;\n if (\n route.method === \"HEAD\" ||\n (route.method === \"all\" && fixture.method === \"HEAD\") ||\n response.status === 204 ||\n response.status === 304\n ) {\n if (await response.text()) throw new Error(`${label}: status ${status} must have no body.`);\n } else {\n if (!schema) throw new Error(`${label}: status ${status} has no body schema.`);\n let body: unknown;\n try {\n body = await response.json();\n } catch {\n throw new Error(`${label}: status ${status} did not return JSON.`);\n }\n if (!matches(body, schema, registry.schemas)) {\n throw new Error(\n `${label}: response body does not match responseSchema for status ${status}.`,\n );\n }\n }\n covered.add(status);\n }\n if (options.requireAllStatuses) {\n const missing = Object.keys(responses).filter((status) => !covered.has(status));\n if (missing.length)\n throw new Error(`${label}: untested declared statuses: ${missing.join(\", \")}.`);\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"start-http-development-server.d.mts","names":[],"sources":["../../../../../../../../packages/core/src/tests/start-http-development-server.ts"],"mappings":";;YA4BY;;;;;;;;;;;EAWV;;;;;;;;;;EAWA;;;;;;;;;;;;EAaA,QAAQ;;;;;;;;;;;;;;wBA6HY,oBAAoB,UAAS,6BAAkC;;;;;
|
|
1
|
+
{"version":3,"file":"start-http-development-server.d.mts","names":[],"sources":["../../../../../../../../packages/core/src/tests/start-http-development-server.ts"],"mappings":";;YA4BY;;;;;;;;;;;EAWV;;;;;;;;;;EAWA;;;;;;;;;;;;EAaA,QAAQ;;;;;;;;;;;;;;wBA6HY,oBAAoB,UAAS,6BAAkC;;;;;wBA+H/D,sBAAsB;;;;wBAuC5B"}
|
|
@@ -135,7 +135,11 @@ async function unwindPartialStartup() {
|
|
|
135
135
|
console.error("[test-server] Restoring the test fakes after a failed start did not complete:", fakesError);
|
|
136
136
|
}
|
|
137
137
|
try {
|
|
138
|
-
|
|
138
|
+
try {
|
|
139
|
+
await connectorsManager.shutdown();
|
|
140
|
+
} finally {
|
|
141
|
+
await filesOrchestrator.dispose();
|
|
142
|
+
}
|
|
139
143
|
} catch (shutdownError) {
|
|
140
144
|
console.error("[test-server] Cleanup after a failed start did not complete:", shutdownError);
|
|
141
145
|
}
|
|
@@ -157,7 +161,11 @@ async function stopHttpTestServer() {
|
|
|
157
161
|
} catch (fakesError) {
|
|
158
162
|
console.error("[test-server] Restoring the test fakes did not complete:", fakesError);
|
|
159
163
|
}
|
|
160
|
-
|
|
164
|
+
try {
|
|
165
|
+
await connectorsManager.shutdown();
|
|
166
|
+
} finally {
|
|
167
|
+
await filesOrchestrator.dispose();
|
|
168
|
+
}
|
|
161
169
|
console.log("[test-server] HTTP test server stopped");
|
|
162
170
|
} catch (error) {
|
|
163
171
|
console.error("[test-server] Error stopping HTTP server:", error);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"start-http-development-server.mjs","names":["config"],"sources":["../../../../../../../../packages/core/src/tests/start-http-development-server.ts"],"sourcesContent":["/**\n * HTTP Test Server\n *\n * Starts a minimal HTTP server for testing.\n * Unlike the full DevelopmentServer, this:\n * - Does NOT watch files\n * - Does NOT do HMR\n * - Only starts connectors needed for HTTP requests\n *\n * Used in Vitest's globalSetup to start server once for all test workers.\n */\nimport config from \"@mongez/config\";\nimport { Application } from \"../application\";\nimport { bootstrap } from \"../bootstrap\";\nimport { loadConfigFiles } from \"../config/load-config-files\";\nimport { connectorsManager } from \"../connectors/connectors-manager\";\nimport { ConnectorLifecyclePhase } from \"../connectors/types\";\nimport { filesOrchestrator } from \"../dev-server/files-orchestrator\";\nimport { assertPortIsAvailable } from \"../http/port-preflight\";\nimport { getHttpReadyReport } from \"../http/ready-report\";\nimport { loadEnvironmentFiles } from \"../utils/load-environment\";\nimport { warlockConfigManager } from \"../warlock-config/warlock-config.manager\";\nimport { installTestFakes, restoreTestFakes } from \"./fakes/test-fakes\";\nimport type { TestFakesSelection } from \"./fakes/test-fakes\";\nimport { publishTestServerPort, withdrawTestServerPort } from \"./test-server-port-channel\";\n\nlet isServerRunning = false;\n\nexport type StartHttpTestServerOptions = {\n /**\n * Port the test server binds to, overriding `http.port`.\n *\n * Needed because the server cannot be moved from outside: this function\n * bootstraps the app itself, and that bootstrap re-reads `.env`.\n *\n * `0` asks the OS for a free port. The port actually bound is published to\n * the test workers, so `testGet()`, `actingAs()` and friends follow it, and\n * a suite never collides with `warlock dev` or another suite.\n */\n port?: number;\n\n /**\n * Whether to boot the optional web connector.\n *\n * Defaults to `true` to preserve the full-stack test server. Pass `false`\n * for API-only suites: the web connector's lazy delegate is skipped before\n * `boot()`, so it never constructs its Vite-backed implementation.\n *\n * @default true\n */\n web?: boolean;\n\n /**\n * Which test fakes the server process installs (mail, queue, cache,\n * storage, clock, ids). Same values and precedence as `setupTest({ fakes })`:\n * this option > `tests.fakes` config > all on.\n *\n * A fake records what happens in ITS process: mail sent while this server\n * handles a request is captured here, not in the test worker that sent the\n * request. To assert on what an HTTP request did, serve it in the worker\n * with `createTestHttpServer()`. The storage fake is the exception: its\n * temp root is shared with the workers.\n */\n fakes?: TestFakesSelection;\n};\n\n/**\n * Start one lifecycle phase while omitting the web connector.\n *\n * Kept here rather than changing the manager's public API: this is a\n * test-server-only selection, and retaining the manager's boot-all / start-all\n * ordering is essential for HTTP and socket connector wiring.\n */\nasync function startTestServerPhase(phase: ConnectorLifecyclePhase): Promise<void> {\n const connectors = connectorsManager\n .list()\n .filter((connector) => connector.lifecyclePhase === phase)\n .filter((connector) => connector.name !== \"web\");\n\n for (const connector of connectors) {\n await connector.boot();\n }\n\n for (const connector of connectors) {\n await connector.start();\n }\n}\n\n/**\n * Apply the caller's port and preflight the bind.\n *\n * Called at the last moment before the HTTP connector binds, and that timing is\n * the fix: `bootstrap()` re-reads `.env` through `loadEnv()` (dotenv overrides\n * by default), and `src/config/http.ts` then resolves its port from that store,\n * so anything the caller set earlier is gone by the time the port is read.\n * Writing into `config` here is also the only channel that works — assigning\n * `process.env.HTTP_PORT` has no effect, since `env()` reads dotenv's own store\n * and never falls back to `process.env`.\n */\nasync function applyTestServerPort(port?: number): Promise<\"published\" | \"ephemeral\" | \"none\"> {\n const httpConfig = config.get(\"http\");\n\n if (!httpConfig) {\n if (port !== undefined) {\n throw new Error(\n `startHttpTestServer({ port: ${port} }) was given a port, but this app has no \\`http\\` config, so no HTTP server is started. Add \\`src/config/http.ts\\` first.`,\n );\n }\n\n return \"none\";\n }\n\n if (port !== undefined) {\n config.set(\"http.port\", port);\n }\n\n const resolvedPort = config.get(\"http.port\");\n\n // No configured port at all is fine: Fastify falls back to its own default and\n // the workers resolve the same one from their own config, so both ends agree\n // without the channel.\n //\n // A non-numeric port is NOT fine, and must fail loudly rather than skip the\n // preflight silently. `env()` coerces a value only when it round-trips exactly\n // (`String(Number(v)) === v`), so `HTTP_PORT=3999` and `HTTP_PORT=0` arrive as\n // numbers, but `03999`, `+3999`, `1e3` and anything with stray whitespace stay\n // strings. Letting those through used to reach `HttpConnector.start()`'s\n // `listen({ port })` unchanged — no preflight, no published port, and every\n // test worker resolving a URL from a channel nothing ever wrote to.\n if (typeof resolvedPort !== \"number\") {\n throw new Error(\n `startHttpTestServer() cannot use \\`http.port\\` value ${JSON.stringify(resolvedPort)} — it did not round-trip through Number() (env() only coerces a value when \\`String(Number(v)) === v\\`, so \"03999\", \"+3999\", \"1e3\", and values with stray whitespace stay strings). Fix \\`HTTP_PORT\\` in .env, or pass an explicit numeric port: startHttpTestServer({ port: 3999 }).`,\n );\n }\n\n // `0` is the OS's \"pick a free one for me\". There is nothing to preflight —\n // probing 0 would bind some unrelated port and prove nothing — and nothing to\n // publish yet: the port is only known once the HTTP connector has bound, and\n // `HttpConnector.start()` records the port `listen()` actually returned (see\n // `ready-report.ts`). It is published right after the late phase instead.\n if (resolvedPort === 0) {\n return \"ephemeral\";\n }\n\n if (resolvedPort < 0) {\n throw new Error(\n `startHttpTestServer() cannot run on port ${resolvedPort}. Pass a port number, or 0 to let the OS pick a free one — e.g. startHttpTestServer({ port: 0 }).`,\n );\n }\n\n await assertPortIsAvailable(resolvedPort, config.get(\"http.host\") || \"localhost\");\n\n publishTestServerPort(resolvedPort);\n\n return \"published\";\n}\n\n/**\n * Publish the port an ephemeral (`port: 0`) bind actually took.\n *\n * `before` is the ready report from before the late phase: the report is a\n * module-level record, so an unchanged one means THIS boot bound nothing and an\n * older report must not be mistaken for it.\n */\nfunction publishEphemeralPort(before: ReturnType<typeof getHttpReadyReport>): void {\n const report = getHttpReadyReport();\n\n if (!report || report === before || report.port <= 0) {\n throw new Error(\n \"startHttpTestServer({ port: 0 }) booted, but the HTTP connector did not bind a port, so there is no address to give the test workers. Check that `src/config/http.ts` exists and that this process serves HTTP (its role is not worker-only).\",\n );\n }\n\n publishTestServerPort(report.port);\n}\n\n/**\n * Start the HTTP test server (minimal - no file watching)\n * Call this in Vitest's globalSetup\n *\n * @example\n * // run this suite on its own port, whatever `.env` says\n * await startHttpTestServer({ port: 3999 });\n *\n * @example\n * // let the OS pick a free port; the request helpers follow it\n * await startHttpTestServer({ port: 0 });\n */\nexport async function startHttpTestServer(options: StartHttpTestServerOptions = {}): Promise<void> {\n const nodeEnvironment = process.env.NODE_ENV;\n\n if (nodeEnvironment === undefined) {\n process.env.NODE_ENV = \"test\";\n } else if (nodeEnvironment !== \"test\") {\n throw new Error(\n `startHttpTestServer() requires NODE_ENV=\"test\", but received NODE_ENV=${JSON.stringify(nodeEnvironment)}. Set NODE_ENV=test before starting the test server.`,\n );\n }\n\n if (isServerRunning) {\n console.log(\"[test-server] Server already running, skipping start\");\n return;\n }\n\n console.log(\"[test-server] Starting HTTP test server...\");\n\n try {\n // `globalSetup` runs in Vitest's main process, where Vitest's worker env\n // setup has not run. Load `.env.test` here, before any config is read.\n await loadEnvironmentFiles();\n\n // Set environment\n Application.setRuntimeStrategy(\"development\");\n Application.setEnvironment(\"test\");\n\n // Bootstrap (env, etc.)\n await warlockConfigManager.load();\n await bootstrap();\n\n // Initialize file orchestrator (but don't watch)\n await filesOrchestrator.init();\n await filesOrchestrator.initializeAll();\n\n // Load config files\n await loadConfigFiles(true);\n\n // Early-phase connectors (database, cache, logger, storage, mailer,\n // herald) must start BEFORE app modules load: a module's `main.ts` boot\n // side-effect can query the DB at import time, so the data source has to\n // be registered first. This mirrors the dev/prod boot order (see\n // `cli-commands.manager`, `production-builder`, and `DevelopmentServer`).\n if (options.web === false) {\n await startTestServerPhase(ConnectorLifecyclePhase.Early);\n } else {\n await connectorsManager.startPhase(ConnectorLifecyclePhase.Early);\n }\n\n // Fakes before app modules load, so module boot code already sees them.\n await installTestFakes(options.fakes);\n\n // Load application modules (their boot side-effects now see a live DB).\n await filesOrchestrator.moduleLoader.loadAll();\n\n const portMode = await applyTestServerPort(options.port);\n const readyReportBefore = getHttpReadyReport();\n\n // A rejecting validator (Application.onValidateBoot) must abort boot\n // before late-phase connectors bind a port — same slot as\n // `DevelopmentServer.start()` and the generated production `app.ts`.\n await Application.runStartupValidators();\n\n // Late-phase connectors (http, socket) bind after app code has\n // registered its routes and listeners.\n if (options.web === false) {\n await startTestServerPhase(ConnectorLifecyclePhase.Late);\n } else {\n await connectorsManager.startPhase(ConnectorLifecyclePhase.Late);\n }\n\n if (portMode === \"ephemeral\") {\n publishEphemeralPort(readyReportBefore);\n }\n\n isServerRunning = true;\n } catch (error) {\n // This function owns the connectors it starts, so a failure part-way\n // through has to leave nothing behind. Without this, `isServerRunning` was\n // still `false` — it is only set on the last line — so `globalTeardown`'s\n // `stopHttpTestServer()` reported \"No server to stop\" and walked away from\n // live early-phase connectors and a published port that outlived them.\n await unwindPartialStartup();\n\n // Rethrown unchanged: the startup failure is the one the caller needs to\n // read. A cleanup failure that replaced it would hide the actual cause.\n throw error;\n }\n}\n\n/**\n * Best-effort teardown of whatever a failed {@link startHttpTestServer} managed\n * to start.\n *\n * Every step is independent and none may throw: this runs while an error is\n * already in flight, and the caller is about to rethrow it.\n */\nasync function unwindPartialStartup(): Promise<void> {\n try {\n await restoreTestFakes();\n } catch (fakesError) {\n console.error(\n \"[test-server] Restoring the test fakes after a failed start did not complete:\",\n fakesError,\n );\n }\n\n try {\n await connectorsManager.shutdown();\n } catch (shutdownError) {\n // Reported, not thrown — it is secondary to the startup error being\n // rethrown, and losing that one to this would be the worse outcome.\n console.error(\"[test-server] Cleanup after a failed start did not complete:\", shutdownError);\n }\n\n withdrawTestServerPort();\n isServerRunning = false;\n}\n\n/**\n * Stop the HTTP test server\n * Call this in Vitest's globalTeardown\n */\nexport async function stopHttpTestServer(): Promise<void> {\n if (!isServerRunning) {\n console.log(\"[test-server] No server to stop\");\n return;\n }\n\n try {\n try {\n await restoreTestFakes();\n } catch (fakesError) {\n console.error(\"[test-server] Restoring the test fakes did not complete:\", fakesError);\n }\n\n await connectorsManager.shutdown();\n\n console.log(\"[test-server] HTTP test server stopped\");\n } catch (error) {\n // Surfaced, never swallowed — cleaning up is not the same as the shutdown\n // having succeeded, and a suite whose teardown failed should say so.\n console.error(\"[test-server] Error stopping HTTP server:\", error);\n\n throw error;\n } finally {\n // In a `finally` rather than after the `await`: when `shutdown()` threw,\n // the published port survived the server it pointed at, so the next run in\n // the same process inherited a stale one and sent every request to it.\n withdrawTestServerPort();\n\n isServerRunning = false;\n }\n}\n\n/**\n * Check if test server is running\n */\nexport function isTestServerRunning(): boolean {\n return isServerRunning;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"start-http-development-server.mjs","names":["config"],"sources":["../../../../../../../../packages/core/src/tests/start-http-development-server.ts"],"sourcesContent":["/**\n * HTTP Test Server\n *\n * Starts a minimal HTTP server for testing.\n * Unlike the full DevelopmentServer, this:\n * - Does NOT watch files\n * - Does NOT do HMR\n * - Only starts connectors needed for HTTP requests\n *\n * Used in Vitest's globalSetup to start server once for all test workers.\n */\nimport config from \"@mongez/config\";\nimport { Application } from \"../application\";\nimport { bootstrap } from \"../bootstrap\";\nimport { loadConfigFiles } from \"../config/load-config-files\";\nimport { connectorsManager } from \"../connectors/connectors-manager\";\nimport { ConnectorLifecyclePhase } from \"../connectors/types\";\nimport { filesOrchestrator } from \"../dev-server/files-orchestrator\";\nimport { assertPortIsAvailable } from \"../http/port-preflight\";\nimport { getHttpReadyReport } from \"../http/ready-report\";\nimport { loadEnvironmentFiles } from \"../utils/load-environment\";\nimport { warlockConfigManager } from \"../warlock-config/warlock-config.manager\";\nimport { installTestFakes, restoreTestFakes } from \"./fakes/test-fakes\";\nimport type { TestFakesSelection } from \"./fakes/test-fakes\";\nimport { publishTestServerPort, withdrawTestServerPort } from \"./test-server-port-channel\";\n\nlet isServerRunning = false;\n\nexport type StartHttpTestServerOptions = {\n /**\n * Port the test server binds to, overriding `http.port`.\n *\n * Needed because the server cannot be moved from outside: this function\n * bootstraps the app itself, and that bootstrap re-reads `.env`.\n *\n * `0` asks the OS for a free port. The port actually bound is published to\n * the test workers, so `testGet()`, `actingAs()` and friends follow it, and\n * a suite never collides with `warlock dev` or another suite.\n */\n port?: number;\n\n /**\n * Whether to boot the optional web connector.\n *\n * Defaults to `true` to preserve the full-stack test server. Pass `false`\n * for API-only suites: the web connector's lazy delegate is skipped before\n * `boot()`, so it never constructs its Vite-backed implementation.\n *\n * @default true\n */\n web?: boolean;\n\n /**\n * Which test fakes the server process installs (mail, queue, cache,\n * storage, clock, ids). Same values and precedence as `setupTest({ fakes })`:\n * this option > `tests.fakes` config > all on.\n *\n * A fake records what happens in ITS process: mail sent while this server\n * handles a request is captured here, not in the test worker that sent the\n * request. To assert on what an HTTP request did, serve it in the worker\n * with `createTestHttpServer()`. The storage fake is the exception: its\n * temp root is shared with the workers.\n */\n fakes?: TestFakesSelection;\n};\n\n/**\n * Start one lifecycle phase while omitting the web connector.\n *\n * Kept here rather than changing the manager's public API: this is a\n * test-server-only selection, and retaining the manager's boot-all / start-all\n * ordering is essential for HTTP and socket connector wiring.\n */\nasync function startTestServerPhase(phase: ConnectorLifecyclePhase): Promise<void> {\n const connectors = connectorsManager\n .list()\n .filter((connector) => connector.lifecyclePhase === phase)\n .filter((connector) => connector.name !== \"web\");\n\n for (const connector of connectors) {\n await connector.boot();\n }\n\n for (const connector of connectors) {\n await connector.start();\n }\n}\n\n/**\n * Apply the caller's port and preflight the bind.\n *\n * Called at the last moment before the HTTP connector binds, and that timing is\n * the fix: `bootstrap()` re-reads `.env` through `loadEnv()` (dotenv overrides\n * by default), and `src/config/http.ts` then resolves its port from that store,\n * so anything the caller set earlier is gone by the time the port is read.\n * Writing into `config` here is also the only channel that works — assigning\n * `process.env.HTTP_PORT` has no effect, since `env()` reads dotenv's own store\n * and never falls back to `process.env`.\n */\nasync function applyTestServerPort(port?: number): Promise<\"published\" | \"ephemeral\" | \"none\"> {\n const httpConfig = config.get(\"http\");\n\n if (!httpConfig) {\n if (port !== undefined) {\n throw new Error(\n `startHttpTestServer({ port: ${port} }) was given a port, but this app has no \\`http\\` config, so no HTTP server is started. Add \\`src/config/http.ts\\` first.`,\n );\n }\n\n return \"none\";\n }\n\n if (port !== undefined) {\n config.set(\"http.port\", port);\n }\n\n const resolvedPort = config.get(\"http.port\");\n\n // No configured port at all is fine: Fastify falls back to its own default and\n // the workers resolve the same one from their own config, so both ends agree\n // without the channel.\n //\n // A non-numeric port is NOT fine, and must fail loudly rather than skip the\n // preflight silently. `env()` coerces a value only when it round-trips exactly\n // (`String(Number(v)) === v`), so `HTTP_PORT=3999` and `HTTP_PORT=0` arrive as\n // numbers, but `03999`, `+3999`, `1e3` and anything with stray whitespace stay\n // strings. Letting those through used to reach `HttpConnector.start()`'s\n // `listen({ port })` unchanged — no preflight, no published port, and every\n // test worker resolving a URL from a channel nothing ever wrote to.\n if (typeof resolvedPort !== \"number\") {\n throw new Error(\n `startHttpTestServer() cannot use \\`http.port\\` value ${JSON.stringify(resolvedPort)} — it did not round-trip through Number() (env() only coerces a value when \\`String(Number(v)) === v\\`, so \"03999\", \"+3999\", \"1e3\", and values with stray whitespace stay strings). Fix \\`HTTP_PORT\\` in .env, or pass an explicit numeric port: startHttpTestServer({ port: 3999 }).`,\n );\n }\n\n // `0` is the OS's \"pick a free one for me\". There is nothing to preflight —\n // probing 0 would bind some unrelated port and prove nothing — and nothing to\n // publish yet: the port is only known once the HTTP connector has bound, and\n // `HttpConnector.start()` records the port `listen()` actually returned (see\n // `ready-report.ts`). It is published right after the late phase instead.\n if (resolvedPort === 0) {\n return \"ephemeral\";\n }\n\n if (resolvedPort < 0) {\n throw new Error(\n `startHttpTestServer() cannot run on port ${resolvedPort}. Pass a port number, or 0 to let the OS pick a free one — e.g. startHttpTestServer({ port: 0 }).`,\n );\n }\n\n await assertPortIsAvailable(resolvedPort, config.get(\"http.host\") || \"localhost\");\n\n publishTestServerPort(resolvedPort);\n\n return \"published\";\n}\n\n/**\n * Publish the port an ephemeral (`port: 0`) bind actually took.\n *\n * `before` is the ready report from before the late phase: the report is a\n * module-level record, so an unchanged one means THIS boot bound nothing and an\n * older report must not be mistaken for it.\n */\nfunction publishEphemeralPort(before: ReturnType<typeof getHttpReadyReport>): void {\n const report = getHttpReadyReport();\n\n if (!report || report === before || report.port <= 0) {\n throw new Error(\n \"startHttpTestServer({ port: 0 }) booted, but the HTTP connector did not bind a port, so there is no address to give the test workers. Check that `src/config/http.ts` exists and that this process serves HTTP (its role is not worker-only).\",\n );\n }\n\n publishTestServerPort(report.port);\n}\n\n/**\n * Start the HTTP test server (minimal - no file watching)\n * Call this in Vitest's globalSetup\n *\n * @example\n * // run this suite on its own port, whatever `.env` says\n * await startHttpTestServer({ port: 3999 });\n *\n * @example\n * // let the OS pick a free port; the request helpers follow it\n * await startHttpTestServer({ port: 0 });\n */\nexport async function startHttpTestServer(options: StartHttpTestServerOptions = {}): Promise<void> {\n const nodeEnvironment = process.env.NODE_ENV;\n\n if (nodeEnvironment === undefined) {\n process.env.NODE_ENV = \"test\";\n } else if (nodeEnvironment !== \"test\") {\n throw new Error(\n `startHttpTestServer() requires NODE_ENV=\"test\", but received NODE_ENV=${JSON.stringify(nodeEnvironment)}. Set NODE_ENV=test before starting the test server.`,\n );\n }\n\n if (isServerRunning) {\n console.log(\"[test-server] Server already running, skipping start\");\n return;\n }\n\n console.log(\"[test-server] Starting HTTP test server...\");\n\n try {\n // `globalSetup` runs in Vitest's main process, where Vitest's worker env\n // setup has not run. Load `.env.test` here, before any config is read.\n await loadEnvironmentFiles();\n\n // Set environment\n Application.setRuntimeStrategy(\"development\");\n Application.setEnvironment(\"test\");\n\n // Bootstrap (env, etc.)\n await warlockConfigManager.load();\n await bootstrap();\n\n // Initialize file orchestrator (but don't watch)\n await filesOrchestrator.init();\n await filesOrchestrator.initializeAll();\n\n // Load config files\n await loadConfigFiles(true);\n\n // Early-phase connectors (database, cache, logger, storage, mailer,\n // herald) must start BEFORE app modules load: a module's `main.ts` boot\n // side-effect can query the DB at import time, so the data source has to\n // be registered first. This mirrors the dev/prod boot order (see\n // `cli-commands.manager`, `production-builder`, and `DevelopmentServer`).\n if (options.web === false) {\n await startTestServerPhase(ConnectorLifecyclePhase.Early);\n } else {\n await connectorsManager.startPhase(ConnectorLifecyclePhase.Early);\n }\n\n // Fakes before app modules load, so module boot code already sees them.\n await installTestFakes(options.fakes);\n\n // Load application modules (their boot side-effects now see a live DB).\n await filesOrchestrator.moduleLoader.loadAll();\n\n const portMode = await applyTestServerPort(options.port);\n const readyReportBefore = getHttpReadyReport();\n\n // A rejecting validator (Application.onValidateBoot) must abort boot\n // before late-phase connectors bind a port — same slot as\n // `DevelopmentServer.start()` and the generated production `app.ts`.\n await Application.runStartupValidators();\n\n // Late-phase connectors (http, socket) bind after app code has\n // registered its routes and listeners.\n if (options.web === false) {\n await startTestServerPhase(ConnectorLifecyclePhase.Late);\n } else {\n await connectorsManager.startPhase(ConnectorLifecyclePhase.Late);\n }\n\n if (portMode === \"ephemeral\") {\n publishEphemeralPort(readyReportBefore);\n }\n\n isServerRunning = true;\n } catch (error) {\n // This function owns the connectors it starts, so a failure part-way\n // through has to leave nothing behind. Without this, `isServerRunning` was\n // still `false` — it is only set on the last line — so `globalTeardown`'s\n // `stopHttpTestServer()` reported \"No server to stop\" and walked away from\n // live early-phase connectors and a published port that outlived them.\n await unwindPartialStartup();\n\n // Rethrown unchanged: the startup failure is the one the caller needs to\n // read. A cleanup failure that replaced it would hide the actual cause.\n throw error;\n }\n}\n\n/**\n * Best-effort teardown of whatever a failed {@link startHttpTestServer} managed\n * to start.\n *\n * Every step is independent and none may throw: this runs while an error is\n * already in flight, and the caller is about to rethrow it.\n */\nasync function unwindPartialStartup(): Promise<void> {\n try {\n await restoreTestFakes();\n } catch (fakesError) {\n console.error(\n \"[test-server] Restoring the test fakes after a failed start did not complete:\",\n fakesError,\n );\n }\n\n try {\n try {\n await connectorsManager.shutdown();\n } finally {\n await filesOrchestrator.dispose();\n }\n } catch (shutdownError) {\n // Reported, not thrown — it is secondary to the startup error being\n // rethrown, and losing that one to this would be the worse outcome.\n console.error(\"[test-server] Cleanup after a failed start did not complete:\", shutdownError);\n }\n\n withdrawTestServerPort();\n isServerRunning = false;\n}\n\n/**\n * Stop the HTTP test server\n * Call this in Vitest's globalTeardown\n */\nexport async function stopHttpTestServer(): Promise<void> {\n if (!isServerRunning) {\n console.log(\"[test-server] No server to stop\");\n return;\n }\n\n try {\n try {\n await restoreTestFakes();\n } catch (fakesError) {\n console.error(\"[test-server] Restoring the test fakes did not complete:\", fakesError);\n }\n\n try {\n await connectorsManager.shutdown();\n } finally {\n await filesOrchestrator.dispose();\n }\n\n console.log(\"[test-server] HTTP test server stopped\");\n } catch (error) {\n // Surfaced, never swallowed — cleaning up is not the same as the shutdown\n // having succeeded, and a suite whose teardown failed should say so.\n console.error(\"[test-server] Error stopping HTTP server:\", error);\n\n throw error;\n } finally {\n // In a `finally` rather than after the `await`: when `shutdown()` threw,\n // the published port survived the server it pointed at, so the next run in\n // the same process inherited a stale one and sent every request to it.\n withdrawTestServerPort();\n\n isServerRunning = false;\n }\n}\n\n/**\n * Check if test server is running\n */\nexport function isTestServerRunning(): boolean {\n return isServerRunning;\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test-lifecycle-state.mjs","names":[],"sources":["../../../../../../../../packages/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 { RequestedTestConnectors, TestConnectorsSelection } 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":";;;;;;;;;
|
|
1
|
+
{"version":3,"file":"test-lifecycle-state.mjs","names":[],"sources":["../../../../../../../../packages/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 { RequestedTestConnectors, TestConnectorsSelection } 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 cleanupLocales?: () => 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":";;;;;;;;;AAkGA,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,33 @@
|
|
|
1
|
+
import { registerLocaleDictionary } from "../localization/register-locale-dictionary.mjs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import glob from "fast-glob";
|
|
5
|
+
|
|
6
|
+
//#region ../../packages/core/src/tests/test-locales.ts
|
|
7
|
+
/** Register data-only module dictionaries in the calling worker, without app side effects. */
|
|
8
|
+
async function loadTestLocales(root = process.cwd()) {
|
|
9
|
+
const cleanups = [];
|
|
10
|
+
const cleanup = () => {
|
|
11
|
+
for (const undo of [...cleanups.splice(0)].reverse()) undo();
|
|
12
|
+
};
|
|
13
|
+
try {
|
|
14
|
+
const files = await glob("src/app/*/utils/locales.json", { cwd: root });
|
|
15
|
+
for (const sourceFile of [...files].sort()) {
|
|
16
|
+
const namespace = sourceFile.split("/")[2];
|
|
17
|
+
if (!namespace) continue;
|
|
18
|
+
cleanups.push(registerLocaleDictionary({
|
|
19
|
+
sourceFile,
|
|
20
|
+
source: await readFile(resolve(root, sourceFile), "utf8"),
|
|
21
|
+
defaultNamespace: namespace
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
return cleanup;
|
|
25
|
+
} catch (error) {
|
|
26
|
+
cleanup();
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
//#endregion
|
|
32
|
+
export { loadTestLocales };
|
|
33
|
+
//# sourceMappingURL=test-locales.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-locales.mjs","names":[],"sources":["../../../../../../../../packages/core/src/tests/test-locales.ts"],"sourcesContent":["import glob from \"fast-glob\";\nimport { readFile } from \"node:fs/promises\";\nimport { resolve } from \"node:path\";\nimport { registerLocaleDictionary } from \"../localization/register-locale-dictionary\";\n\n/** Register data-only module dictionaries in the calling worker, without app side effects. */\nexport async function loadTestLocales(root = process.cwd()): Promise<() => void> {\n const cleanups: Array<() => void> = [];\n const cleanup = () => {\n for (const undo of [...cleanups.splice(0)].reverse()) undo();\n };\n try {\n const files = await glob(\"src/app/*/utils/locales.json\", { cwd: root });\n for (const sourceFile of [...files].sort()) {\n const namespace = sourceFile.split(\"/\")[2];\n if (!namespace) continue;\n cleanups.push(\n registerLocaleDictionary({\n sourceFile,\n source: await readFile(resolve(root, sourceFile), \"utf8\"),\n defaultNamespace: namespace,\n }),\n );\n }\n return cleanup;\n } catch (error) {\n cleanup();\n throw error;\n }\n}\n"],"mappings":";;;;;;;AAMA,eAAsB,gBAAgB,OAAO,QAAQ,IAAI,GAAwB;CAC/E,MAAM,WAA8B,CAAC;CACrC,MAAM,gBAAgB;EACpB,KAAK,MAAM,QAAQ,CAAC,GAAG,SAAS,OAAO,CAAC,CAAC,CAAC,CAAC,QAAQ,GAAG,KAAK;CAC7D;CACA,IAAI;EACF,MAAM,QAAQ,MAAM,KAAK,gCAAgC,EAAE,KAAK,KAAK,CAAC;EACtE,KAAK,MAAM,cAAc,CAAC,GAAG,KAAK,CAAC,CAAC,KAAK,GAAG;GAC1C,MAAM,YAAY,WAAW,MAAM,GAAG,CAAC,CAAC;GACxC,IAAI,CAAC,WAAW;GAChB,SAAS,KACP,yBAAyB;IACvB;IACA,QAAQ,MAAM,SAAS,QAAQ,MAAM,UAAU,GAAG,MAAM;IACxD,kBAAkB;GACpB,CAAC,CACH;EACF;EACA,OAAO;CACT,SAAS,OAAO;EACd,QAAQ;EACR,MAAM;CACR;AACF"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//#region ../../packages/core/src/tests/test-runtime-scope.d.ts
|
|
2
|
+
export type TestWorkerConfiguration = {
|
|
3
|
+
pool: string;
|
|
4
|
+
watch: boolean;
|
|
5
|
+
isolate: boolean;
|
|
6
|
+
};
|
|
7
|
+
/** Called by a Vitest runner, whose cleanup callback is awaited before worker exit. */
|
|
8
|
+
export declare function configureTestWorker(configuration: TestWorkerConfiguration, onCleanup: (cleanup: () => Promise<void>) => void): void;
|
|
9
|
+
/** Process-local counts; aggregate bootstraps across workers when measuring a run. */
|
|
10
|
+
export declare function getTestRuntimeStats(): {
|
|
11
|
+
bootstraps: number;
|
|
12
|
+
files: number;
|
|
13
|
+
};
|
|
14
|
+
//#endregion
|
|
15
|
+
//# sourceMappingURL=test-runtime-scope.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-runtime-scope.d.mts","names":[],"sources":["../../../../../../../../packages/core/src/tests/test-runtime-scope.ts"],"mappings":";YAEY;EAA4B;EAAc;EAAgB;;;wBAqCtD,oBACd,eAAe,yBACf,YAAY,eAAe;;wBA8Bb;EAAyB;EAAoB"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { performance } from "node:perf_hooks";
|
|
2
|
+
|
|
3
|
+
//#region ../../packages/core/src/tests/test-runtime-scope.ts
|
|
4
|
+
const KEY = Symbol.for("@warlock.js/core:tests:runtime-scope");
|
|
5
|
+
function testRuntimeScope() {
|
|
6
|
+
const host = globalThis;
|
|
7
|
+
if (host[KEY]) return host[KEY];
|
|
8
|
+
const scope = {
|
|
9
|
+
lifetime: "file",
|
|
10
|
+
fileOpen: false,
|
|
11
|
+
bootstraps: 0,
|
|
12
|
+
files: 0,
|
|
13
|
+
profiles: []
|
|
14
|
+
};
|
|
15
|
+
host[KEY] = scope;
|
|
16
|
+
process.once("exit", () => printTestSummary(scope));
|
|
17
|
+
return scope;
|
|
18
|
+
}
|
|
19
|
+
/** Called by a Vitest runner, whose cleanup callback is awaited before worker exit. */
|
|
20
|
+
function configureTestWorker(configuration, onCleanup) {
|
|
21
|
+
const scope = testRuntimeScope();
|
|
22
|
+
scope.worker = configuration;
|
|
23
|
+
if (scope.cleanupRegistered) return;
|
|
24
|
+
scope.cleanupRegistered = true;
|
|
25
|
+
onCleanup(async () => {
|
|
26
|
+
try {
|
|
27
|
+
const shutdown = scope.shutdown;
|
|
28
|
+
scope.shutdown = void 0;
|
|
29
|
+
await shutdown?.();
|
|
30
|
+
} finally {
|
|
31
|
+
printTestSummary(scope);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
function selectTestLifetime(value) {
|
|
36
|
+
if (value === void 0 || value === null || value === "file") return "file";
|
|
37
|
+
if (value !== "worker") throw new Error("tests.lifetime must be \"file\" or \"worker\".");
|
|
38
|
+
const worker = testRuntimeScope().worker;
|
|
39
|
+
if (!worker || worker.pool !== "forks" || worker.watch || worker.isolate) throw new Error("tests.lifetime: \"worker\" requires the Warlock test runner, pool: \"forks\", isolate: false and vitest run (watch is unsupported).");
|
|
40
|
+
return "worker";
|
|
41
|
+
}
|
|
42
|
+
/** Process-local counts; aggregate bootstraps across workers when measuring a run. */
|
|
43
|
+
function getTestRuntimeStats() {
|
|
44
|
+
const scope = testRuntimeScope();
|
|
45
|
+
return {
|
|
46
|
+
bootstraps: scope.bootstraps,
|
|
47
|
+
files: scope.files
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
async function timeTestStep(step, run) {
|
|
51
|
+
const started = performance.now();
|
|
52
|
+
try {
|
|
53
|
+
return await run();
|
|
54
|
+
} finally {
|
|
55
|
+
const profile = testRuntimeScope().profile;
|
|
56
|
+
if (profile) profile.steps[step] = (profile.steps[step] ?? 0) + performance.now() - started;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function formatSteps(steps) {
|
|
60
|
+
return Object.entries(steps).map(([step, ms]) => `${step}=${ms.toFixed(2)}ms`).join(" ");
|
|
61
|
+
}
|
|
62
|
+
function finishTestProfile() {
|
|
63
|
+
const scope = testRuntimeScope();
|
|
64
|
+
if (!scope.profile) return;
|
|
65
|
+
console.info(`[warlock:test] file=${JSON.stringify(scope.profile.file)} pid=${process.pid} ${formatSteps(scope.profile.steps)}`);
|
|
66
|
+
scope.profiles.push(scope.profile);
|
|
67
|
+
scope.profile = void 0;
|
|
68
|
+
}
|
|
69
|
+
function printTestSummary(scope) {
|
|
70
|
+
if (!scope.profiles.length || scope.summaryPrinted) return;
|
|
71
|
+
scope.summaryPrinted = true;
|
|
72
|
+
const totals = {};
|
|
73
|
+
for (const profile of scope.profiles) for (const [step, ms] of Object.entries(profile.steps)) totals[step] = (totals[step] ?? 0) + ms;
|
|
74
|
+
if (scope.shutdownMs !== void 0) totals.shutdown = scope.shutdownMs;
|
|
75
|
+
console.info(`[warlock:test] summary pid=${process.pid} files=${scope.files} bootstraps=${scope.bootstraps} ${formatSteps(totals)}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
//#endregion
|
|
79
|
+
export { configureTestWorker, finishTestProfile, getTestRuntimeStats, selectTestLifetime, testRuntimeScope, timeTestStep };
|
|
80
|
+
//# sourceMappingURL=test-runtime-scope.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-runtime-scope.mjs","names":[],"sources":["../../../../../../../../packages/core/src/tests/test-runtime-scope.ts"],"sourcesContent":["import { performance } from \"node:perf_hooks\";\n\nexport type TestWorkerConfiguration = { pool: string; watch: boolean; isolate: boolean };\nexport type TestSetupProfile = { file: string; steps: Record<string, number> };\n\ntype RuntimeScope = {\n worker?: TestWorkerConfiguration;\n cleanupRegistered?: boolean;\n lifetime: \"file\" | \"worker\";\n fileOpen: boolean;\n boundary?: Promise<void>;\n bootstraps: number;\n files: number;\n summaryPrinted?: boolean;\n shutdownMs?: number;\n shutdown?: () => Promise<void>;\n profiles: TestSetupProfile[];\n profile?: TestSetupProfile;\n unitConfig?: Promise<void>;\n mode?: \"unit\" | \"integration\";\n};\nconst KEY = Symbol.for(\"@warlock.js/core:tests:runtime-scope\");\n\nexport function testRuntimeScope(): RuntimeScope {\n const host = globalThis as typeof globalThis & { [KEY]?: RuntimeScope };\n if (host[KEY]) return host[KEY];\n const scope: RuntimeScope = {\n lifetime: \"file\",\n fileOpen: false,\n bootstraps: 0,\n files: 0,\n profiles: [],\n };\n host[KEY] = scope;\n process.once(\"exit\", () => printTestSummary(scope));\n return scope;\n}\n\n/** Called by a Vitest runner, whose cleanup callback is awaited before worker exit. */\nexport function configureTestWorker(\n configuration: TestWorkerConfiguration,\n onCleanup: (cleanup: () => Promise<void>) => void,\n): void {\n const scope = testRuntimeScope();\n scope.worker = configuration;\n if (scope.cleanupRegistered) return;\n scope.cleanupRegistered = true;\n onCleanup(async () => {\n try {\n const shutdown = scope.shutdown;\n scope.shutdown = undefined;\n await shutdown?.();\n } finally {\n printTestSummary(scope);\n }\n });\n}\n\nexport function selectTestLifetime(value: unknown): \"file\" | \"worker\" {\n if (value === undefined || value === null || value === \"file\") return \"file\";\n if (value !== \"worker\") throw new Error('tests.lifetime must be \"file\" or \"worker\".');\n const worker = testRuntimeScope().worker;\n if (!worker || worker.pool !== \"forks\" || worker.watch || worker.isolate) {\n throw new Error(\n 'tests.lifetime: \"worker\" requires the Warlock test runner, pool: \"forks\", isolate: false and vitest run (watch is unsupported).',\n );\n }\n return \"worker\";\n}\n\n/** Process-local counts; aggregate bootstraps across workers when measuring a run. */\nexport function getTestRuntimeStats(): { bootstraps: number; files: number } {\n const scope = testRuntimeScope();\n return { bootstraps: scope.bootstraps, files: scope.files };\n}\n\nexport async function timeTestStep<T>(step: string, run: () => T | Promise<T>): Promise<T> {\n const started = performance.now();\n try {\n return await run();\n } finally {\n const profile = testRuntimeScope().profile;\n if (profile) profile.steps[step] = (profile.steps[step] ?? 0) + performance.now() - started;\n }\n}\n\nfunction formatSteps(steps: Record<string, number>): string {\n return Object.entries(steps)\n .map(([step, ms]) => `${step}=${ms.toFixed(2)}ms`)\n .join(\" \");\n}\n\nexport function finishTestProfile(): void {\n const scope = testRuntimeScope();\n if (!scope.profile) return;\n console.info(\n `[warlock:test] file=${JSON.stringify(scope.profile.file)} pid=${process.pid} ${formatSteps(scope.profile.steps)}`,\n );\n scope.profiles.push(scope.profile);\n scope.profile = undefined;\n}\n\nfunction printTestSummary(scope: RuntimeScope): void {\n if (!scope.profiles.length || scope.summaryPrinted) return;\n scope.summaryPrinted = true;\n const totals: Record<string, number> = {};\n for (const profile of scope.profiles) {\n for (const [step, ms] of Object.entries(profile.steps)) totals[step] = (totals[step] ?? 0) + ms;\n }\n if (scope.shutdownMs !== undefined) totals.shutdown = scope.shutdownMs;\n console.info(\n `[warlock:test] summary pid=${process.pid} files=${scope.files} bootstraps=${scope.bootstraps} ${formatSteps(totals)}`,\n );\n}\n"],"mappings":";;;AAqBA,MAAM,MAAM,OAAO,IAAI,sCAAsC;AAE7D,SAAgB,mBAAiC;CAC/C,MAAM,OAAO;CACb,IAAI,KAAK,MAAM,OAAO,KAAK;CAC3B,MAAM,QAAsB;EAC1B,UAAU;EACV,UAAU;EACV,YAAY;EACZ,OAAO;EACP,UAAU,CAAC;CACb;CACA,KAAK,OAAO;CACZ,QAAQ,KAAK,cAAc,iBAAiB,KAAK,CAAC;CAClD,OAAO;AACT;;AAGA,SAAgB,oBACd,eACA,WACM;CACN,MAAM,QAAQ,iBAAiB;CAC/B,MAAM,SAAS;CACf,IAAI,MAAM,mBAAmB;CAC7B,MAAM,oBAAoB;CAC1B,UAAU,YAAY;EACpB,IAAI;GACF,MAAM,WAAW,MAAM;GACvB,MAAM,WAAW;GACjB,MAAM,WAAW;EACnB,UAAU;GACR,iBAAiB,KAAK;EACxB;CACF,CAAC;AACH;AAEA,SAAgB,mBAAmB,OAAmC;CACpE,IAAI,UAAU,UAAa,UAAU,QAAQ,UAAU,QAAQ,OAAO;CACtE,IAAI,UAAU,UAAU,MAAM,IAAI,MAAM,gDAA4C;CACpF,MAAM,SAAS,iBAAiB,CAAC,CAAC;CAClC,IAAI,CAAC,UAAU,OAAO,SAAS,WAAW,OAAO,SAAS,OAAO,SAC/D,MAAM,IAAI,MACR,qIACF;CAEF,OAAO;AACT;;AAGA,SAAgB,sBAA6D;CAC3E,MAAM,QAAQ,iBAAiB;CAC/B,OAAO;EAAE,YAAY,MAAM;EAAY,OAAO,MAAM;CAAM;AAC5D;AAEA,eAAsB,aAAgB,MAAc,KAAuC;CACzF,MAAM,UAAU,YAAY,IAAI;CAChC,IAAI;EACF,OAAO,MAAM,IAAI;CACnB,UAAU;EACR,MAAM,UAAU,iBAAiB,CAAC,CAAC;EACnC,IAAI,SAAS,QAAQ,MAAM,SAAS,QAAQ,MAAM,SAAS,KAAK,YAAY,IAAI,IAAI;CACtF;AACF;AAEA,SAAS,YAAY,OAAuC;CAC1D,OAAO,OAAO,QAAQ,KAAK,CAAC,CACzB,KAAK,CAAC,MAAM,QAAQ,GAAG,KAAK,GAAG,GAAG,QAAQ,CAAC,EAAE,GAAG,CAAC,CACjD,KAAK,GAAG;AACb;AAEA,SAAgB,oBAA0B;CACxC,MAAM,QAAQ,iBAAiB;CAC/B,IAAI,CAAC,MAAM,SAAS;CACpB,QAAQ,KACN,uBAAuB,KAAK,UAAU,MAAM,QAAQ,IAAI,EAAE,OAAO,QAAQ,IAAI,GAAG,YAAY,MAAM,QAAQ,KAAK,GACjH;CACA,MAAM,SAAS,KAAK,MAAM,OAAO;CACjC,MAAM,UAAU;AAClB;AAEA,SAAS,iBAAiB,OAA2B;CACnD,IAAI,CAAC,MAAM,SAAS,UAAU,MAAM,gBAAgB;CACpD,MAAM,iBAAiB;CACvB,MAAM,SAAiC,CAAC;CACxC,KAAK,MAAM,WAAW,MAAM,UAC1B,KAAK,MAAM,CAAC,MAAM,OAAO,OAAO,QAAQ,QAAQ,KAAK,GAAG,OAAO,SAAS,OAAO,SAAS,KAAK;CAE/F,IAAI,MAAM,eAAe,QAAW,OAAO,WAAW,MAAM;CAC5D,QAAQ,KACN,8BAA8B,QAAQ,IAAI,SAAS,MAAM,MAAM,cAAc,MAAM,WAAW,GAAG,YAAY,MAAM,GACrH;AACF"}
|
|
@@ -2,7 +2,17 @@ import { TestFakesSelection } from "./fakes/test-fakes.mjs";
|
|
|
2
2
|
import { AppModuleImporter } from "../loader/app-module-importer.mjs";
|
|
3
3
|
import { TestConnectorsSelection } from "./test-connectors-selection.mjs";
|
|
4
4
|
//#region ../../packages/core/src/tests/vitest-setup.d.ts
|
|
5
|
+
export type TestRuntimeConfiguration = {
|
|
6
|
+
lifetime?: "file" | "worker";
|
|
7
|
+
profileSetup?: boolean;
|
|
8
|
+
/** Runs before fixtures in every worker-lifetime file, including the first. Use a worker-specific database. */
|
|
9
|
+
resetBetweenFiles?: () => void | Promise<void>;
|
|
10
|
+
};
|
|
5
11
|
export type TestSetupOptions = {
|
|
12
|
+
/** Unit mode loads config values without hooks, connectors or native loaders. Requires importModule. */
|
|
13
|
+
mode?: "unit" | "integration";
|
|
14
|
+
/** File label for setup profiling. Pass expect.getState().testPath from Vitest. */
|
|
15
|
+
file?: string;
|
|
6
16
|
/**
|
|
7
17
|
* Which connectors to start.
|
|
8
18
|
*
|
|
@@ -47,9 +57,9 @@ export declare class TestLifecycleError extends Error {
|
|
|
47
57
|
/**
|
|
48
58
|
* Bootstrap the test runtime and start the selected connectors.
|
|
49
59
|
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
60
|
+
* By default boots once per TEST FILE. With tests.lifetime: "worker", the runner
|
|
61
|
+
* owns shutdown and requires a shared module graph. `setupFiles` executes for
|
|
62
|
+
* every file. The lifecycle state lives on the runtime
|
|
53
63
|
* context rather than in this module precisely so that a rebuild cannot hide a
|
|
54
64
|
* runtime that is still up.
|
|
55
65
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vitest-setup.d.mts","names":[],"sources":["../../../../../../../../packages/core/src/tests/vitest-setup.ts"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"vitest-setup.d.mts","names":[],"sources":["../../../../../../../../packages/core/src/tests/vitest-setup.ts"],"mappings":";;;;YAgEY;EACV;EACA;;EAEA,iCAAiC;;YAGvB;;EAEV;;EAEA;;;;;;;;;EASA,aAAa;;;;;;;;;;EAUb,eAAe;;;;;;;;;;EAUf,QAAQ;;;;;wBAQM,wBAAwB,cAAc;;;;;qBAkBzC,2BAA2B;EACtC,YAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wBAqCC,UAAU,UAAU,mBAAmB;;;;;;;;;;;;;;;;;;wBAgHvC,gBAAgB"}
|