@gfdelarue/testkit 0.4.3 → 0.4.5
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/README.md +109 -22
- package/bin/testkit-windows-owner-arm64.exe +0 -0
- package/bin/testkit-windows-owner-x64.exe +0 -0
- package/dist/app/bugs/categories.js +1 -1
- package/dist/app/bugs/draft.js +3 -3
- package/dist/app/bugs/facts.d.ts +3 -2
- package/dist/app/bugs/facts.js +2 -2
- package/dist/app/bugs/guide.js +1 -1
- package/dist/app/bugs/run-reading.js +1 -1
- package/dist/app/db-schema/prepare.js +1 -1
- package/dist/app/destroy/index.js +1 -1
- package/dist/app/doctor/index.js +1 -1
- package/dist/app/environments/config-driven.d.ts +5 -0
- package/dist/app/environments/config-driven.js +1 -0
- package/dist/app/environments/routing.d.ts +6 -0
- package/dist/app/environments/routing.js +1 -0
- package/dist/app/environments/supplied.d.ts +3 -0
- package/dist/app/environments/supplied.js +1 -0
- package/dist/app/file-selection.js +1 -1
- package/dist/app/lint/native.js +1 -1
- package/dist/app/lint/options.d.ts +0 -1
- package/dist/app/lint/options.js +1 -1
- package/dist/app/lint/shared.d.ts +0 -2
- package/dist/app/lint/shared.js +1 -1
- package/dist/app/package-inspection.js +1 -1
- package/dist/app/reports/files.d.ts +13 -0
- package/dist/app/reports/files.js +1 -0
- package/dist/app/reports/prepare.d.ts +20 -0
- package/dist/app/reports/prepare.js +2 -0
- package/dist/app/reports/publish.d.ts +21 -0
- package/dist/app/reports/publish.js +1 -0
- package/dist/app/run/engine-evidence-persistence.js +1 -1
- package/dist/app/run/engine-execution-cleanup.js +1 -1
- package/dist/app/run/engine-execution-failures.js +1 -1
- package/dist/app/run/engine-execution.d.ts +10 -8
- package/dist/app/run/engine-execution.js +1 -1
- package/dist/app/run/engine-orchestration.d.ts +15 -34
- package/dist/app/run/engine-orchestration.js +1 -1
- package/dist/app/run/environment.d.ts +33 -0
- package/dist/app/run/environment.js +1 -0
- package/dist/app/run/failure-details.js +1 -1
- package/dist/app/run/live-run.d.ts +3 -1
- package/dist/app/run/live-run.js +1 -1
- package/dist/app/run/orchestrator.d.ts +8 -1
- package/dist/app/run/orchestrator.js +1 -1
- package/dist/app/run/planning.d.ts +1 -2
- package/dist/app/run/planning.js +1 -1
- package/dist/app/run/results.d.ts +1 -1
- package/dist/app/run/results.js +1 -1
- package/dist/app/run/run-finalization.d.ts +6 -2
- package/dist/app/run/run-finalization.js +1 -1
- package/dist/app/run/runtime-fixtures.js +1 -1
- package/dist/app/run/runtime-task-types.d.ts +1 -1
- package/dist/app/run/scheduler/index.js +1 -1
- package/dist/app/run/scheduler/locks.d.ts +6 -0
- package/dist/app/run/scheduler/locks.js +1 -0
- package/dist/app/run/selection.d.ts +0 -1
- package/dist/app/run/selection.js +1 -1
- package/dist/app/run/setup/runtime-node-setup-protocol.js +1 -1
- package/dist/app/run/setup/runtime-node-setup-worker.js +1 -1
- package/dist/app/run/task-leases.d.ts +15 -0
- package/dist/app/run/task-leases.js +1 -0
- package/dist/app/run/worker-loop.d.ts +13 -4
- package/dist/app/run/worker-loop.js +1 -1
- package/dist/app/tracker/index.d.ts +6 -1
- package/dist/app/tracker/index.js +1 -1
- package/dist/app/typecheck/index.js +4 -4
- package/dist/cli/command-catalog.d.ts +19 -0
- package/dist/cli/command-catalog.js +1 -1
- package/dist/cli/command-registry.js +1 -1
- package/dist/cli/commands/report/prepare.d.ts +12 -0
- package/dist/cli/commands/report/prepare.js +5 -0
- package/dist/cli/commands/report/publish.d.ts +13 -0
- package/dist/cli/commands/report/publish.js +2 -0
- package/dist/cli/json-errors.js +1 -1
- package/dist/cli/operations/run/operation.d.ts +3 -1
- package/dist/cli/operations/run/operation.js +2 -2
- package/dist/cli/renderers/doctor/text.js +1 -1
- package/dist/cli/renderers/run/formatting.js +1 -1
- package/dist/config/authoring/index.d.ts +4 -13
- package/dist/config/authoring/index.js +1 -1
- package/dist/config/authoring/preset-env.js +1 -1
- package/dist/config/loader/config-loader.js +1 -1
- package/dist/config/loader/database.js +1 -1
- package/dist/config/loader/decode-execution.d.ts +0 -2
- package/dist/config/loader/decode-execution.js +1 -1
- package/dist/config/loader/discover-tests.js +1 -1
- package/dist/config/loader/discovery.d.ts +0 -1
- package/dist/config/loader/discovery.js +2 -2
- package/dist/config/loader/env.js +1 -1
- package/dist/config/loader/execution-model.d.ts +0 -1
- package/dist/config/loader/execution-model.js +1 -1
- package/dist/config/loader/file-metadata.d.ts +1 -1
- package/dist/config/loader/file-metadata.js +1 -1
- package/dist/config/loader/groups.d.ts +33 -0
- package/dist/config/loader/groups.js +1 -0
- package/dist/config/loader/load.d.ts +0 -2
- package/dist/config/loader/load.js +1 -1
- package/dist/config/loader/runtime-isolation.js +1 -1
- package/dist/config/loader/runtime.d.ts +0 -9
- package/dist/config/loader/runtime.js +1 -1
- package/dist/config/loader/skip-config.js +1 -1
- package/dist/config/loader/validation.d.ts +1 -2
- package/dist/config/loader/validation.js +1 -1
- package/dist/config/loader/values.d.ts +0 -1
- package/dist/config/loader/values.js +1 -1
- package/dist/database/activity-gate.js +1 -1
- package/dist/database/admin.js +2 -2
- package/dist/database/lifecycle-cleanup.d.ts +1 -0
- package/dist/database/lifecycle-cleanup.js +1 -1
- package/dist/database/local-postgres.js +1 -1
- package/dist/database/naming.js +1 -1
- package/dist/database/schema-operations.d.ts +1 -1
- package/dist/database/schema-operations.js +1 -1
- package/dist/database/schema-source.js +3 -3
- package/dist/database/source-url.d.ts +0 -1
- package/dist/database/source-url.js +2 -2
- package/dist/database/state.js +1 -1
- package/dist/database/steps.js +3 -3
- package/dist/domain/file-timeout.d.ts +0 -3
- package/dist/domain/file-timeout.js +1 -1
- package/dist/domain/operation-display.js +1 -1
- package/dist/domain/test-outcomes.d.ts +0 -1
- package/dist/domain/test-outcomes.js +1 -1
- package/dist/engines/adapters/evidence-identity.js +1 -1
- package/dist/engines/adapters/execution.d.ts +0 -1
- package/dist/engines/adapters/execution.js +1 -1
- package/dist/engines/api/capabilities.d.ts +0 -10
- package/dist/engines/api/capabilities.js +1 -1
- package/dist/engines/api/execution-scope.js +1 -1
- package/dist/engines/api/k6-runtime-input.js +1 -1
- package/dist/engines/bundle-cache.js +1 -1
- package/dist/engines/contracts/fixture-manifest.js +1 -1
- package/dist/engines/definitions.d.ts +1 -0
- package/dist/engines/definitions.js +1 -1
- package/dist/engines/installations/playwright-resolution-child.js +1 -1
- package/dist/engines/installations/playwright.js +1 -1
- package/dist/engines/k6/adapter/evidence.js +4 -4
- package/dist/engines/k6/adapter/executor.js +1 -1
- package/dist/engines/k6/adapter/native-summary.js +1 -1
- package/dist/engines/k6/runtime/checks.js +1 -1
- package/dist/engines/k6/runtime/dal-fixtures.js +1 -1
- package/dist/engines/k6/runtime/dal-suite.js +1 -1
- package/dist/engines/k6/runtime/http-suite-runtime.js +1 -1
- package/dist/engines/k6/runtime/http.js +1 -1
- package/dist/engines/k6/runtime/shared/db-normalization.js +1 -1
- package/dist/engines/k6/runtime/shared/fixture-engine.js +2 -2
- package/dist/engines/k6/runtime/shared/http-parsing.js +3 -3
- package/dist/engines/managed-module-resolution-policy-input.d.ts +0 -1
- package/dist/engines/managed-module-resolution-policy-input.js +1 -1
- package/dist/engines/managed-module-resolution-policy.js +1 -1
- package/dist/engines/node-vitest/adapter/adapter.js +1 -1
- package/dist/engines/node-vitest/adapter/evidence.js +2 -2
- package/dist/engines/node-vitest/adapter/workspace.js +22 -21
- package/dist/engines/node-vitest/runtime/contract/index.js +1 -1
- package/dist/engines/node-vitest/runtime/data/http.js +1 -1
- package/dist/engines/node-vitest/runtime/data/index.d.ts +2 -2
- package/dist/engines/node-vitest/runtime/data/index.js +1 -1
- package/dist/engines/node-vitest/runtime/data/report.js +5 -5
- package/dist/engines/node-vitest/runtime/data/scenario.d.ts +0 -1
- package/dist/engines/node-vitest/runtime/data/scenario.js +2 -2
- package/dist/engines/node-vitest/runtime/data/types.d.ts +1 -29
- package/dist/engines/node-vitest/runtime/runtime-input.js +1 -1
- package/dist/engines/playwright/adapter/adapter.js +1 -1
- package/dist/engines/playwright/adapter/config.d.ts +1 -1
- package/dist/engines/playwright/adapter/config.js +4 -4
- package/dist/engines/playwright/adapter/evidence-validation.d.ts +0 -3
- package/dist/engines/playwright/adapter/evidence-validation.js +1 -1
- package/dist/engines/playwright/adapter/evidence.js +1 -1
- package/dist/engines/playwright/runtime/config-file.js +1 -1
- package/dist/engines/playwright/runtime/config.js +1 -1
- package/dist/engines/playwright/runtime/runtime-input.d.ts +2 -2
- package/dist/engines/playwright/runtime/runtime-input.js +16 -12
- package/dist/environment/lifecycle-artifacts.js +1 -1
- package/dist/environment/lifecycle-controller.d.ts +1 -11
- package/dist/environment/lifecycle-controller.js +1 -1
- package/dist/environment/lifecycle.d.ts +14 -11
- package/dist/environment/lifecycle.js +1 -1
- package/dist/environment/planning-types.d.ts +0 -2
- package/dist/environment/ports.d.ts +10 -0
- package/dist/environment/ports.js +1 -0
- package/dist/environment/ready-environment.d.ts +2 -5
- package/dist/environment/ready-environment.js +1 -1
- package/dist/environment/run-environment.d.ts +1 -0
- package/dist/environment/run-environment.js +1 -1
- package/dist/environment/runtime-contexts.d.ts +11 -7
- package/dist/environment/runtime-contexts.js +1 -1
- package/dist/environment/runtime-manager.d.ts +1 -9
- package/dist/environment/runtime-manager.js +1 -1
- package/dist/environment/runtime-planning.js +1 -1
- package/dist/environment/runtime-preparation.js +1 -1
- package/dist/environment/runtime-service.d.ts +0 -6
- package/dist/environment/services.d.ts +11 -3
- package/dist/environment/services.js +3 -1
- package/dist/environment/state.d.ts +1 -4
- package/dist/environment/state.js +1 -1
- package/dist/environment/template.d.ts +6 -22
- package/dist/environment/template.js +1 -1
- package/dist/evidence/artifact-config.js +1 -1
- package/dist/evidence/artifact-validation.d.ts +1 -2
- package/dist/evidence/artifact-validation.js +1 -1
- package/dist/evidence/artifacts.js +2 -2
- package/dist/evidence/bundles.js +1 -1
- package/dist/evidence/environment-record.d.ts +22 -0
- package/dist/evidence/environment-record.js +1 -0
- package/dist/evidence/index.d.ts +1 -1
- package/dist/evidence/logs.js +2 -2
- package/dist/evidence/operation-timings.js +1 -1
- package/dist/evidence/read-fields.d.ts +0 -1
- package/dist/evidence/read-fields.js +1 -1
- package/dist/evidence/read-run.js +1 -1
- package/dist/evidence/reading.js +1 -1
- package/dist/evidence/reporting-types.d.ts +19 -0
- package/dist/evidence/reporting.d.ts +2 -2
- package/dist/evidence/reporting.js +1 -1
- package/dist/evidence/storage.d.ts +0 -2
- package/dist/evidence/storage.js +2 -2
- package/dist/evidence/timing.js +1 -1
- package/dist/kernel/error-serialization.d.ts +2 -0
- package/dist/kernel/error-serialization.js +1 -1
- package/dist/kernel/unknown-values.d.ts +5 -0
- package/dist/kernel/unknown-values.js +1 -1
- package/dist/process/environment.js +1 -1
- package/dist/process/execa-bundle.json +3 -3
- package/dist/process/files/json-file.d.ts +0 -1
- package/dist/process/files/json-file.js +1 -1
- package/dist/process/files/paths.d.ts +2 -0
- package/dist/process/files/paths.js +1 -1
- package/dist/process/host/metadata.js +1 -1
- package/dist/process/host/platform.js +1 -1
- package/dist/process/host-process.js +1 -1
- package/dist/process/lifecycle-fence.d.ts +1 -2
- package/dist/process/lifecycle-fence.js +1 -1
- package/dist/process/locks.js +2 -2
- package/dist/process/process-group.js +1 -1
- package/dist/process/process-identity.js +1 -1
- package/dist/process/process.js +30 -30
- package/dist/process/readiness.d.ts +7 -1
- package/dist/process/readiness.js +1 -1
- package/dist/process/recovery.js +1 -1
- package/dist/process/runs/manifest.js +1 -1
- package/dist/process/runs/ownership.js +1 -1
- package/dist/process/runs/product-state.js +1 -1
- package/dist/process/runs/stop.js +1 -1
- package/dist/process/stoppable-settlement.d.ts +26 -0
- package/dist/process/stoppable-settlement.js +1 -0
- package/dist/process/url-redaction.d.ts +1 -0
- package/dist/process/url-redaction.js +1 -0
- package/dist/process/windows-native.d.ts +6 -0
- package/dist/process/windows-native.js +1 -1
- package/dist/repo/ci-environment.js +1 -1
- package/dist/repo/fingerprint-policy.d.ts +0 -2
- package/dist/repo/fingerprint-policy.js +1 -1
- package/dist/repo/physical-tree.js +1 -1
- package/dist/repo/publish-directory.d.ts +20 -0
- package/dist/repo/publish-directory.js +3 -0
- package/dist/repo/state.js +4 -4
- package/dist/steps/build-config.d.ts +0 -1
- package/dist/steps/build-config.js +1 -1
- package/dist/steps/configured-steps.js +1 -1
- package/dist/steps/environment.d.ts +0 -2
- package/dist/steps/environment.js +1 -1
- package/dist/steps/index.d.ts +1 -1
- package/dist/steps/run.js +2 -2
- package/dist/toolchains/archive.js +1 -1
- package/dist/toolchains/index.js +3 -3
- package/dist/toolchains/node-provisioning.js +2 -2
- package/dist/toolchains/vitest/index.js +1 -1
- package/docs/bug-records.md +54 -52
- package/docs/engine-architecture.md +39 -11
- package/package.json +1 -1
- package/testkit-config-schema.md +43 -5
- package/dist/app/run/playwright-configuration.d.ts +0 -2
- package/dist/app/run/playwright-configuration.js +0 -1
- package/dist/environment/readiness.d.ts +0 -12
- package/dist/environment/readiness.js +0 -1
|
@@ -172,7 +172,6 @@ export interface ServiceLocalConfig {
|
|
|
172
172
|
baseUrl: string;
|
|
173
173
|
cwd?: string;
|
|
174
174
|
env?: Record<string, string>;
|
|
175
|
-
port?: number;
|
|
176
175
|
readyTimeoutMs?: number;
|
|
177
176
|
shutdownTimeoutMs?: number;
|
|
178
177
|
readyUrl?: string | false | null;
|
|
@@ -213,7 +212,7 @@ export interface AppRuntimeOptions {
|
|
|
213
212
|
prepare?: RuntimePrepareConfig;
|
|
214
213
|
toolchain?: string | NodeToolchainConfig;
|
|
215
214
|
}
|
|
216
|
-
interface
|
|
215
|
+
export interface NodeAppOptions extends PresetServiceOptions {
|
|
217
216
|
baseUrl?: string;
|
|
218
217
|
build?: BuildConfigInput | null;
|
|
219
218
|
buildInputs?: string[];
|
|
@@ -221,7 +220,6 @@ interface NodeAppBoundaryOptions extends PresetServiceOptions {
|
|
|
221
220
|
entry?: string;
|
|
222
221
|
env?: PresetEnvConfig;
|
|
223
222
|
outDir?: string;
|
|
224
|
-
port?: number;
|
|
225
223
|
readyPath?: string;
|
|
226
224
|
readyTimeoutMs?: number;
|
|
227
225
|
shutdownTimeoutMs?: number;
|
|
@@ -231,10 +229,7 @@ interface NodeAppBoundaryOptions extends PresetServiceOptions {
|
|
|
231
229
|
toolchain?: string | NodeToolchainConfig;
|
|
232
230
|
tsconfig?: string;
|
|
233
231
|
}
|
|
234
|
-
export interface
|
|
235
|
-
port: number;
|
|
236
|
-
}
|
|
237
|
-
interface NextAppBoundaryOptions extends PresetServiceOptions {
|
|
232
|
+
export interface NextAppOptions extends PresetServiceOptions {
|
|
238
233
|
api?: string | {
|
|
239
234
|
baseUrl: string;
|
|
240
235
|
};
|
|
@@ -244,7 +239,6 @@ interface NextAppBoundaryOptions extends PresetServiceOptions {
|
|
|
244
239
|
cwd?: string;
|
|
245
240
|
env?: PresetEnvConfig;
|
|
246
241
|
mode?: "dev" | "start";
|
|
247
|
-
port?: number;
|
|
248
242
|
readyTimeoutMs?: number;
|
|
249
243
|
shutdownTimeoutMs?: number;
|
|
250
244
|
readyUrl?: string | false | null;
|
|
@@ -252,9 +246,6 @@ interface NextAppBoundaryOptions extends PresetServiceOptions {
|
|
|
252
246
|
start?: string;
|
|
253
247
|
toolchain?: string | NodeToolchainConfig;
|
|
254
248
|
}
|
|
255
|
-
export interface NextAppOptions extends NextAppBoundaryOptions {
|
|
256
|
-
port: number;
|
|
257
|
-
}
|
|
258
249
|
export interface NodeNextPresetOptions {
|
|
259
250
|
fileTimeoutSeconds?: number;
|
|
260
251
|
install?: "require-host" | "download";
|
|
@@ -325,8 +316,8 @@ declare function postgresConnectionFromEnvValue(prefix: string): {
|
|
|
325
316
|
};
|
|
326
317
|
declare function postgresFixture(options?: DatabaseFixtureOptions): ServiceConfig;
|
|
327
318
|
declare function nodeToolchain(options?: NodeToolchainConfig): NodeToolchainConfig;
|
|
328
|
-
declare function nodeApp(options
|
|
329
|
-
declare function nextApp(options
|
|
319
|
+
declare function nodeApp(options?: NodeAppOptions): AppServiceConfig;
|
|
320
|
+
declare function nextApp(options?: NextAppOptions): AppServiceConfig;
|
|
330
321
|
export declare const app: {
|
|
331
322
|
node: typeof nodeApp;
|
|
332
323
|
next: typeof nextApp;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{clearRepoConfig as
|
|
1
|
+
import{clearRepoConfig as I,getRepoConfig as O,registerRepoConfig as $}from"./runtime.js";import{normalizeDatabaseEnvToken as b,normalizeNextPresetEnv as N,normalizePresetEnv as A}from"./preset-env.js";const P=String;export function defineConfig(e){return e||{}}function F(e={}){return{...e,...e.maxConnections?{maxConnections:e.maxConnections}:{},...e.reservedConnections?{reservedConnections:e.reservedConnections}:{},...e.runtimeConnections?{runtimeConnections:e.runtimeConnections}:{}}}function v(e,n="baseUrl"){const t=b(e,"service name",!1);return`{${b(n,"endpoint field",!1)}:${t}}`}function R(e={}){const n=E(e.migrate),t=E(e.seed),r=E(e.verify);return{inputs:Array.isArray(e.inputs)?[...e.inputs]:void 0,migrate:n,seed:t,verify:r}}function z(e,n={}){return{kind:"command",run:e,...U(n)}}function _(e,n={}){return{kind:"sql-file",path:e,...U(n)}}function u(e,n={}){return{kind:"module",target:e,...U(n)}}function U(e={}){return{...e.cwd!==void 0?{cwd:e.cwd}:{},...e.inputs!==void 0?{inputs:[...e.inputs]}:{},...e.args!==void 0?{args:e.args}:{}}}function q(e,n={}){if(typeof e!="string"||e.trim().length===0)throw new Error("database.schema.fromEnv(...) requires a non-empty env var name");return{kind:"env",env:e.trim(),...n}}function f(e){return`@gfdelarue/testkit/config/database-steps#${e}`}function M(e={}){return u(f("verifySeed"),{args:e})}function V(e={}){return u(f("verifyRows"),{args:e})}function X(e={}){return u(f("runSql"),{args:e})}function G(e={}){return u(f("seedRows"),{args:e})}function H(e={}){return u(f("materializePostgresBinding"),{args:e})}function L(e={}){return u(f("updateRows"),{args:e})}function W(){return{kind:"now"}}function J(e){return{kind:"env",name:b(e,"database.values.env(...) name",!1)}}function K(e){return{kind:"json",value:e}}function Q(e){return{kind:"postgres-connection-from-env",prefix:b(e,"database.values.postgresConnectionFromEnv(...) prefix",!1)}}function Y(e={}){const{discovery:n,envFiles:t,template:r,inputs:s,migrate:o,seed:a,verify:d,...c}=e,i=r||D(e)?R({inputs:s,migrate:o,seed:a,verify:d,...r||{}}):void 0;return{discovery:n||{roots:[".testkit-fixture"]},envFiles:t,local:!1,database:F(i?{...c,template:i}:c)}}function D(e={}){return["inputs","migrate","seed","verify"].some(n=>Object.prototype.hasOwnProperty.call(e,n))}function B(e={}){return{...e}}function Z(e={}){return{kind:"tsc",cwd:e.cwd,entry:e.entry||"src/index.ts",tsconfig:e.tsconfig||"tsconfig.json",outDir:e.outDir||"dist",inputs:Array.isArray(e.inputs)?[...e.inputs]:void 0}}function ee(e={}){return{kind:"next",cwd:e.cwd,distDir:e.distDir||"dist",tsconfig:e.tsconfig||"tsconfig.json",inputs:Array.isArray(e.inputs)?[...e.inputs]:void 0}}function ne(e={}){const{baseUrl:n,build:t,buildInputs:r,cwd:s=".",entry:o="src/index.ts",env:a,envFiles:d,outDir:c="dist",port:i,readyPath:g="/health",readyTimeoutMs:S,shutdownTimeoutMs:p,readyUrl:h,runtime:m,start:C,toolchain:k,tsconfig:T="tsconfig.json",...l}=e,y=A(a),x=n||"http://127.0.0.1:{port}",w=t===void 0?Z({cwd:s,entry:o,tsconfig:T,outDir:c,inputs:r}):t,j=C||re(w,o);return{...l,envFiles:d,runtime:{...m||{},build:w,toolchain:k??m?.toolchain},local:{cwd:s,start:j,...i===void 0?{}:{port:i},baseUrl:x,readyUrl:h??`${x}${g}`,readyTimeoutMs:S,...p===void 0?{}:{shutdownTimeoutMs:p},env:y}}}function te(e={}){const{api:n,baseUrl:t,build:r,buildInputs:s,cwd:o=".",dependsOn:a,env:d,envFiles:c,mode:i="dev",port:g,readyTimeoutMs:S,shutdownTimeoutMs:p,readyUrl:h,runtime:m,start:C,toolchain:k,...T}=e,l={...N({api:n}),...A(d)},y=t||"http://127.0.0.1:{port}",x=r===void 0?i==="start"?ee({cwd:o,inputs:s}):null:r,w=C||(i==="start"?"{bin:next} start --port {port}":"{bin:next} dev -p {port}");return{...T,envFiles:c,dependsOn:Array.isArray(a)?[...a]:a,runtime:{...m||{},build:x,toolchain:k??m?.toolchain},local:{cwd:o,start:w,...g===void 0?{}:{port:g},baseUrl:y,readyUrl:h??y,readyTimeoutMs:S,...p===void 0?{}:{shutdownTimeoutMs:p},env:i==="start"?{NEXT_DIST_DIR:l.NEXT_DIST_DIR||".next-testkit/{runtimeId}/dist",NEXT_TSCONFIG_PATH:l.NEXT_TSCONFIG_PATH||".next-testkit/{runtimeId}/tsconfig.json",...l}:l}}}export const app={node:ne,next:te},database={schema:{fromEnv:q},steps:{materializePostgresBinding:H,runSql:X,seedRows:G,updateRows:L,verifyRows:V,verifySeed:M},values:{env:J,json:K,now:W,postgresConnectionFromEnv:Q},postgres(e={}){const{template:n,sourceSchema:t,inputs:r,migrate:s,seed:o,verify:a,...d}=e,c=t===void 0?d:{...d,sourceSchema:t},i=n||D(e)?R({inputs:r,migrate:s,seed:o,verify:a,...n||{}}):void 0;return F(i?{...c,template:i}:c)},fixture:Y},endpoint={baseUrl(e){return v(e,"baseUrl")},readyUrl(e){return v(e,"readyUrl")},publicBaseUrl(e){return v(e,"publicBaseUrl")},privateBaseUrl(e){return v(e,"privateBaseUrl")},serviceContextUrl(e){return v(e,"serviceContextUrl")}},step={command:z,module:u,sqlFile:_},presets={nodeNext(e={}){const n=e.workers;if(e.taskConcurrency!=null&&n!=null&&e.taskConcurrency!==n)throw new Error("presets.nodeNext taskConcurrency and workers must match when both are provided.");return{execution:{taskConcurrency:e.taskConcurrency??n??1,fileTimeoutSeconds:e.fileTimeoutSeconds??120},toolchains:{node:B({install:e.install??"download",...e.node?{node:e.node}:{},...e.npm?{npm:e.npm}:{}})}}}},toolchain={node:B};export{I as clearRepoConfig,O as getRepoConfig,$ as registerRepoConfig};function re(e,n){return e?.kind==="tsc"?`node {prepareDir}/${ie(n||e.entry||"src/index.ts",e.outDir||"dist")}`:`{bin:tsx} ${n||"src/index.ts"}`}function E(e){return e==null?[]:(Array.isArray(e)?e:[e]).map(t=>typeof t=="string"?z(t):t)}function ie(e,n){const r=P(e||"src/index.ts").replaceAll("\\","/").replace(/\.[cm]?[jt]sx?$/i,".js"),s=r.startsWith("src/")?r.slice(4):r;return`${n}/${s}`.replace(/\/+/g,"/")}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
const i=String;export function normalizePresetEnv(e){if(!e)return{};if(!o(e))throw new Error("Preset env must be an object");const t=new Set(["values","databases"]),n=Object.keys(e).filter(a=>!t.has(a));if(n.length>0)throw new Error(`Preset env only supports "values" and "databases". Received unexpected key(s): ${n.join(", ")}`);const
|
|
1
|
+
import{isUnknownRecord as o}from"../../kernel/unknown-values.js";const i=String;export function normalizePresetEnv(e){if(!e)return{};if(!o(e))throw new Error("Preset env must be an object");const t=new Set(["values","databases"]),n=Object.keys(e).filter(a=>!t.has(a));if(n.length>0)throw new Error(`Preset env only supports "values" and "databases". Received unexpected key(s): ${n.join(", ")}`);const r=o(e.values)?e.values:{},s=o(e.databases)?e.databases:{};return{...c(s),...r}}export function normalizeDatabaseEnvToken(e,t,n=!0){const r=i(e||"").trim(),s=n?r.replace(/[^A-Za-z0-9]+/g,"_").replace(/^_+|_+$/g,""):r;if(!s)throw new Error(`${t} must be a non-empty string`);return s}export function normalizeNextPresetEnv({api:e}={}){const t={};if(e){const n=typeof e=="string"?e:e.baseUrl;n&&(t.NEXT_PUBLIC_API_URL=n)}return t}function c(e){const t={};for(const[n,r]of Object.entries(e)){if(!o(r))throw new Error(`env.databases.${n} must be an object`);const s=normalizeDatabaseEnvToken(r.prefix,`env.databases.${n}.prefix`),a=normalizeDatabaseEnvToken(r.service,`env.databases.${n}.service`,!1);t[`${s}_DATABASE_HOST`]=`{dbHost:${a}}`,t[`${s}_DATABASE_PORT`]=`{dbPort:${a}}`,t[`${s}_DATABASE_NAME`]=`{dbName:${a}}`,t[`${s}_DATABASE_USER`]=`{dbUser:${a}}`,t[`${s}_DATABASE_PASSWORD`]=`{dbPassword:${a}}`,t[`${s}_DATABASE_SSL`]=`{dbSsl:${a}}`}return t}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import m from"node:crypto";import o from"node:fs";import
|
|
1
|
+
import m from"node:crypto";import{isUnknownRecord as c}from"../../kernel/unknown-values.js";import o from"node:fs";import e from"node:path";import{fileURLToPath as d,pathToFileURL as p}from"node:url";import{buildWithEsbuildRetry as h}from"../../toolchains/esbuild.js";const u=e.resolve(e.dirname(d(import.meta.url)),"..","..",".."),k=e.join(u,"dist","index.js"),j=e.join(u,"dist","config","authoring","index.js"),w=new Set(["/k6","/node-vitest/data","/node-vitest/contract","/playwright"]),y=["testkit.config.ts","testkit.config.mts","testkit.config.mjs","testkit.config.js"];export function findConfigFile(t){for(const n of y){const i=e.join(t,n);if(o.existsSync(i))return i}return null}export async function loadTestkitConfig(t,n={}){const i=findConfigFile(t);if(!i)return{config:{},configFile:null};const r=e.join(t,".testkit","_config");o.mkdirSync(r,{recursive:!0});const s=b(i),a=e.join(r,`config-${s.slice(0,12)}.mjs`);await h({absWorkingDir:t,bundle:!0,entryPoints:[i],format:"esm",legalComments:"none",outfile:a,platform:"node",sourcemap:"inline",target:"es2020",plugins:[C()]});const l=[s,n.importCacheSalt||""].filter(Boolean).join("-"),g=await import(`${p(a).href}?v=${l}`),f=S(g);if(!c(f))throw new Error(`testkit config file must export an object: ${e.basename(i)}`);return{config:f,configFile:i}}function b(t){const n=o.readFileSync(t,"utf8");return m.createHash("sha256").update(t).update("\0").update(n).digest("hex")}function C(){return{name:"testkit-config-alias",setup(t){t.onResolve({filter:/^@gfdelarue\/testkit(?:\/.*)?$/},n=>({namespace:"file",path:E(n.path)}))}}}function E(t){const n=t.slice(18);if(!n)return k;if(n==="/config")return j;throw w.has(n)?new Error(`testkit.config.ts may not import engine authoring module "${t}". Use repository config helpers from @gfdelarue/testkit/config instead.`):new Error(`Unsupported @gfdelarue/testkit import "${t}" while loading testkit config`)}function S(t){if(c(t))return t.default||t.config||{}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{normalizeDatabaseBinding as u}from"./decode-execution.js";import{normalizeDatabaseRoles as m}from"./database-roles.js";import{materializeDatabaseConfig as b}from"./database-materialization.js";import"./execution-model.js";import{normalizeConfiguredInputs as d,normalizeConfiguredSteps as i}from"../../steps/configured-steps.js";export function normalizeDatabaseConfig(e,r){if(s(e.databaseFrom))return;const t=e.database;if(!s(t))return;const o=n(t,"user"),a=b({user:typeof o=="string"?o:void 0}).user;return{...g(t),binding:u(f(n(t,"binding"),"per-runtime"),`Service "${r}" database.binding`),maxConnections:c(n(t,"maxConnections"),`Service "${r}" database.maxConnections`),reservedConnections:p(n(t,"reservedConnections"),`Service "${r}" database.reservedConnections`),runtimeConnections:c(n(t,"runtimeConnections"),`Service "${r}" database.runtimeConnections`),reset:n(t,"reset")!==!1,roles:m(n(t,"roles"),r,a),sourceSchema:l(n(t,"sourceSchema"),r),template:normalizeDatabaseTemplateConfig(n(t,"template"),r),serviceName:r}}function c(e,r){if(e==null)return;const t=Number(e);if(!Number.isInteger(t)||t<=0)throw new Error(`${r} must be a positive integer`);return t}function p(e,r){if(e==null)return;const t=Number(e);if(!Number.isInteger(t)||t<0)throw new Error(`${r} must be a non-negative integer`);return t}export function normalizeDatabaseTemplateConfig(e,r){if(e==null)return{inputs:[],migrate:[],seed:[],verify:[]};if(typeof e!="object")throw new Error(`Service "${r}" database.template must be an object`);if(Object.prototype.hasOwnProperty.call(e,"schema"))throw new Error(`Service "${r}" database.template.schema has been removed. Configure database.sourceSchema instead.`);return{inputs:d(n(e,"inputs"),`Service "${r}" database.template`),migrate:i(n(e,"migrate"),`Service "${r}" database.template.migrate`),seed:i(n(e,"seed"),`Service "${r}" database.template.seed`),verify:i(n(e,"verify"),`Service "${r}" database.template.verify`)}}function l(e,r){if(e===!1||e===null)return null;if(e===void 0)return{kind:"auto",refresh:{mode:"auto"},unavailable:"auto",verify:!0};if(typeof e!="object"||Array.isArray(e))throw new Error(`Service "${r}" database.sourceSchema must be an object or false`);const t=f(n(e,"kind"),"env");if(t!=="env")throw new Error(`Service "${r}" database.sourceSchema.kind must be "env"`);if(Object.prototype.hasOwnProperty.call(e,"cachePath"))throw new Error(`Service "${r}" database.sourceSchema.cachePath has been removed. Testkit now manages commit-scoped source schema caches automatically.`);const o=n(e,"env");if(typeof o!="string"||o.trim().length===0)throw new Error(`Service "${r}" database.sourceSchema.env must be a non-empty string`);return{kind:t,env:o.trim(),refresh:h(n(e,"refresh"),r),unavailable:y(n(e,"unavailable"),r),verify:n(e,"verify")!==!1}}function h(e,r){if(e==null||e==="auto")return{mode:"auto"};if(e==="always")return{mode:"always"};if(typeof e=="object"&&!Array.isArray(e)){const t=n(e,"ttlSeconds");if(typeof t!="number"||!Number.isInteger(t)||t<0)throw new Error(`Service "${r}" database.sourceSchema.refresh.ttlSeconds must be a non-negative integer`);return{mode:"ttl",ttlSeconds:t}}throw new Error(`Service "${r}" database.sourceSchema.refresh must be "auto", "always", or { ttlSeconds }`)}function y(e,r){if(e==null||e==="auto")return"auto";if(e==="fail"||e==="warn-cache")return e;throw new Error(`Service "${r}" database.sourceSchema.unavailable must be "auto", "fail", or "warn-cache"`)}function s(e){return!!e}function f(e,r){return s(e)?e:r}function
|
|
1
|
+
import{normalizeDatabaseBinding as u}from"./decode-execution.js";import{normalizeDatabaseRoles as m}from"./database-roles.js";import{materializeDatabaseConfig as b}from"./database-materialization.js";import"./execution-model.js";import{normalizeConfiguredInputs as d,normalizeConfiguredSteps as i}from"../../steps/configured-steps.js";import{readDynamicProperty as n}from"../../kernel/unknown-values.js";export function normalizeDatabaseConfig(e,r){if(s(e.databaseFrom))return;const t=e.database;if(!s(t))return;const o=n(t,"user"),a=b({user:typeof o=="string"?o:void 0}).user;return{...g(t),binding:u(f(n(t,"binding"),"per-runtime"),`Service "${r}" database.binding`),maxConnections:c(n(t,"maxConnections"),`Service "${r}" database.maxConnections`),reservedConnections:p(n(t,"reservedConnections"),`Service "${r}" database.reservedConnections`),runtimeConnections:c(n(t,"runtimeConnections"),`Service "${r}" database.runtimeConnections`),reset:n(t,"reset")!==!1,roles:m(n(t,"roles"),r,a),sourceSchema:l(n(t,"sourceSchema"),r),template:normalizeDatabaseTemplateConfig(n(t,"template"),r),serviceName:r}}function c(e,r){if(e==null)return;const t=Number(e);if(!Number.isInteger(t)||t<=0)throw new Error(`${r} must be a positive integer`);return t}function p(e,r){if(e==null)return;const t=Number(e);if(!Number.isInteger(t)||t<0)throw new Error(`${r} must be a non-negative integer`);return t}export function normalizeDatabaseTemplateConfig(e,r){if(e==null)return{inputs:[],migrate:[],seed:[],verify:[]};if(typeof e!="object")throw new Error(`Service "${r}" database.template must be an object`);if(Object.prototype.hasOwnProperty.call(e,"schema"))throw new Error(`Service "${r}" database.template.schema has been removed. Configure database.sourceSchema instead.`);return{inputs:d(n(e,"inputs"),`Service "${r}" database.template`),migrate:i(n(e,"migrate"),`Service "${r}" database.template.migrate`),seed:i(n(e,"seed"),`Service "${r}" database.template.seed`),verify:i(n(e,"verify"),`Service "${r}" database.template.verify`)}}function l(e,r){if(e===!1||e===null)return null;if(e===void 0)return{kind:"auto",refresh:{mode:"auto"},unavailable:"auto",verify:!0};if(typeof e!="object"||Array.isArray(e))throw new Error(`Service "${r}" database.sourceSchema must be an object or false`);const t=f(n(e,"kind"),"env");if(t!=="env")throw new Error(`Service "${r}" database.sourceSchema.kind must be "env"`);if(Object.prototype.hasOwnProperty.call(e,"cachePath"))throw new Error(`Service "${r}" database.sourceSchema.cachePath has been removed. Testkit now manages commit-scoped source schema caches automatically.`);const o=n(e,"env");if(typeof o!="string"||o.trim().length===0)throw new Error(`Service "${r}" database.sourceSchema.env must be a non-empty string`);return{kind:t,env:o.trim(),refresh:h(n(e,"refresh"),r),unavailable:y(n(e,"unavailable"),r),verify:n(e,"verify")!==!1}}function h(e,r){if(e==null||e==="auto")return{mode:"auto"};if(e==="always")return{mode:"always"};if(typeof e=="object"&&!Array.isArray(e)){const t=n(e,"ttlSeconds");if(typeof t!="number"||!Number.isInteger(t)||t<0)throw new Error(`Service "${r}" database.sourceSchema.refresh.ttlSeconds must be a non-negative integer`);return{mode:"ttl",ttlSeconds:t}}throw new Error(`Service "${r}" database.sourceSchema.refresh must be "auto", "always", or { ttlSeconds }`)}function y(e,r){if(e==null||e==="auto")return"auto";if(e==="fail"||e==="warn-cache")return e;throw new Error(`Service "${r}" database.sourceSchema.unavailable must be "auto", "fail", or "warn-cache"`)}function s(e){return!!e}function f(e,r){return s(e)?e:r}function g(e){const r={};if(typeof e=="string"){for(let t=0;t<e.length;t+=1)Object.defineProperty(r,String(t),{configurable:!0,enumerable:!0,value:e[t],writable:!0});return r}if(e===null||typeof e!="object"&&typeof e!="function")return r;for(const t of Reflect.ownKeys(e))if(Object.getOwnPropertyDescriptor(e,t)?.enumerable){const a=Reflect.get(e,t);Object.defineProperty(r,t,{configurable:!0,enumerable:!0,value:a,writable:!0})}return r}
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import type { DatabaseBinding, ExecutionConfigInput, ResolveExecutionConfigInput, NormalizedExecutionConfig } from "./execution-model.js";
|
|
2
2
|
export declare function parseTaskConcurrencyOptions(taskConcurrency: unknown, workers?: unknown): number | null;
|
|
3
|
-
export declare function parseRuntimeInstancesOption(value: unknown, label?: string): number;
|
|
4
3
|
export declare function normalizeRuntimeInstances(value: unknown, label?: string): number;
|
|
5
|
-
export declare function parseRuntimeMaxConcurrentTasksOption(value: unknown, label?: string): number;
|
|
6
4
|
export declare function normalizeRuntimeMaxConcurrentTasks(value: unknown, label?: string): number;
|
|
7
5
|
export declare function normalizeDatabaseBinding(value: unknown, label?: string): DatabaseBinding;
|
|
8
6
|
export declare function resolveExecutionConfig({ cli, repo, }?: ResolveExecutionConfigInput): NormalizedExecutionConfig;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{DEFAULT_FILE_TIMEOUT_SECONDS as
|
|
1
|
+
import{DEFAULT_FILE_TIMEOUT_SECONDS as u,normalizeFileTimeoutSeconds as a}from"../../domain/file-timeout.js";export function parseTaskConcurrencyOptions(e,n){return i(e,n,"--task-concurrency","--workers")??null}export function normalizeRuntimeInstances(e,n="runtime.instances"){return c(e,n)}export function normalizeRuntimeMaxConcurrentTasks(e,n="runtime.maxConcurrentTasks"){return e==null?Number.POSITIVE_INFINITY:c(e,n)}export function normalizeDatabaseBinding(e,n="database.binding"){const r=String(e).trim();if(r!=="shared"&&r!=="per-runtime")throw new Error(`Invalid ${n} value "${String(e)}". Expected one of: shared, per-runtime.`);return r}export function resolveExecutionConfig({cli:e={},repo:n={}}={}){const r=i(n.taskConcurrency,n.workers);return{taskConcurrency:i(e.taskConcurrency,e.workers,"--task-concurrency","--workers")??r??1,fileTimeoutSeconds:a(e.fileTimeoutSeconds??n.fileTimeoutSeconds??u)}}export function normalizeExecutionConfig(e={}){const n=new Set(["workers","taskConcurrency","fileTimeoutSeconds"]),r=Object.keys(e).filter(o=>!n.has(o));if(r.length>0)throw new Error(`execution received unexpected key(s): ${r.join(", ")}`);return{taskConcurrency:i(e.taskConcurrency,e.workers)??1,fileTimeoutSeconds:a(e.fileTimeoutSeconds??u)}}function i(e,n,r="execution.taskConcurrency",t="execution.workers"){const o=e==null?void 0:c(e,r),s=n==null?void 0:c(n,t);if(o!==void 0&&s!==void 0&&o!==s)throw new Error(`${r} and ${t} must match when both are provided.`);return o??s}function c(e,n){const r=Number(e);if(!Number.isInteger(r)||r<=0)throw new Error(`${n} must be a positive integer.`);return r}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import g from"path";import{randomUUID as z}from"node:crypto";import{loadConfigContext as D,resolveProductDir as A}from"./load.js";import{discoverProject as B,hasDiscoveryErrors as H,buildDiscoveryErrorFromDiagnostics as L}from"./discovery.js";import{loadTestkitConfig as V}from"./config-loader.js";import{TEST_TYPE_ORDER as _,isRunType as U}from"../../domain/test-types.js";import"../../domain/identifiers.js";import{DEFAULT_RUNTIME_ISOLATION_POLICY as q,resolveRuntimeIsolationPolicy as G}from"./runtime-isolation.js";import{resolveEngineProfile as Y}from"../../engines/catalog.js";import{readTestFileMetadata as K}from"./file-metadata.js";import{normalizeDiscoveryConfig as J}from"./decode-discovery.js";import"./path-policy.js";import{buildDiscoverySummary as Q,emptyDiscoverySummary as W}from"./discovery-summary.js";import{matchesSelectedTypes as X,matchesSuiteSelectors as k,normalizeTypeValues as Z,parseSuiteSelectors as ee}from"../../domain/suite-selection.js";import{errorMessage as P}from"../../kernel/error-serialization.js";import{isObjectRecord as E,readObjectRecord as I,readProperty as f}from"./values.js";const ie=9,C=String;export async function discoverTests(e={}){return(await loadDiscoveryContext(e)).discovery}export async function loadDiscoveredConfigContext(e={}){const i=await D({dir:e.dir,configImportCacheSalt:z()}),{discovery:t}=await loadDiscoveryContext({dir:i.productDir,diagnostics:"error"},i);return{context:i,discovery:t}}export async function loadDiscoveryContext(e={},i){const t=i?.productDir??A(process.cwd(),e.dir),o=le(e);if(i&&o.diagnosticsMode==="error"&&H(i.discovery.diagnostics))throw L(i.discovery.diagnostics);const r=i?{config:i.config,configFile:i.configFile,diagnostics:[]}:await te(t,o.diagnosticsMode),s={schemaVersion:ie,source:"testkit-discovery",product:{name:g.basename(t),directory:t},configFile:r.configFile?g.relative(t,r.configFile)||g.basename(r.configFile):null,filters:{service:o.serviceFilter,types:o.typeValues,suiteSelectors:o.suiteSelectors.map(n=>n.raw),fileNames:o.fileNames,runnableOnly:o.runnableOnly,diagnostics:o.diagnosticsMode},services:[],suites:[],files:[],diagnostics:[...r.diagnostics],summary:W()};if(!r.config)return{discovery:R(s),context:null};const a=ue(r.config),l=fe(a.services),p=i?.discovery??B(t,l,{strict:o.diagnosticsMode==="error",discovery:a.discovery}),v={project:p,fileMetadataByPath:new Map(i?.configs.flatMap(n=>[...n.testkit.fileMetadataByPath])??[])};s.diagnostics.push(...p.diagnostics),ae(o.serviceFilter,a.services,p);let y=i??null;try{y=i??await D({dir:t,preparedDiscovery:v,configContext:{config:r.config,configFile:r.configFile},discoveryOptions:{strict:o.diagnosticsMode==="error"}})}catch(n){if(o.diagnosticsMode==="error")throw n;s.diagnostics.push({code:"config_invalid",severity:"error",message:P(n)})}const h=ne({productDir:t,rawDiscovery:p,explicitServices:a.services,configs:y?.configs??[],fileMetadataByPath:v.fileMetadataByPath,filters:o});return{discovery:R({...s,...h}),context:y}}export function formatDisplayName(e){return C(e||"").split(/[-_/]+/g).filter(Boolean).map(i=>i.replace(/^\w/,t=>t.toUpperCase())).join(" ")}export function testTypeContract(e,i,t){const o=Y(e,i);if(o.id!==t)throw new Error(`Discovery profile mismatch for ${e}: metadata resolved ${t}, catalog resolved ${o.id}`);return e==="data"?{authoringApi:"@gfdelarue/testkit/node-vitest/data",executionHost:o.capabilities.executionHost,summary:"Data suites use the node-vitest engine and record Testkit data evidence; they are not raw Vitest or service-namespace tests."}:e==="contract"?{authoringApi:"@gfdelarue/testkit/node-vitest/contract",executionHost:o.capabilities.executionHost,summary:"Contract suites use the node-vitest engine for Node-backed service/runtime contracts without data-flow evidence requirements."}:i==="k6"?{authoringApi:"@gfdelarue/testkit/k6",executionHost:o.capabilities.executionHost}:i==="playwright"?{authoringApi:"@gfdelarue/testkit/playwright",executionHost:o.capabilities.executionHost}:{executionHost:o.capabilities.executionHost}}async function te(e,i){try{const t=await V(e),{config:o,configFile:r}=oe(t);return{config:o,configFile:r,diagnostics:[]}}catch(t){if(i==="error")throw t;return{config:null,configFile:null,diagnostics:[{code:"config_invalid",severity:"error",message:P(t)}]}}}function oe(e){if(!E(e))throw new Error("testkit config loader must return an object");const i=f(e,"config"),t=f(e,"configFile");if(!E(i)){const o=typeof t=="string"?g.basename(t):"testkit config file";throw new Error(`testkit config file must export an object: ${o}`)}if(t!==null&&typeof t!="string")throw new Error("testkit config loader returned an invalid config path");return{config:i,configFile:t}}function ne({configs:e,fileMetadataByPath:i,productDir:t,rawDiscovery:o,explicitServices:r,filters:s}){const a=new Map(e.map(n=>[n.name,n])),l=new Map,p=new Map,v=[],y=new Set([...Object.keys(r),...Object.keys(o.services),...a.keys()]);for(const n of y){if(s.serviceFilter&&n!==s.serviceFilter)continue;const c=a.get(n),u=o.services[n],d=r[n];l.set(n,{name:n,discovered:c!==void 0||!!u,localCwd:c?c.testkit.local?.cwd||".":d?.localCwd||u?.inferredLocalCwd||".",dependsOn:[...c?.testkit.dependsOn??d?.dependsOn??[]],suiteCount:0,fileCount:0,activeFileCount:0,skippedFileCount:0})}for(const n of o.files){const c=n.testType;if(!X(c,s.typeValues)||!k(c,n.suiteName,s.suiteSelectors)||s.serviceFilter&&n.serviceName!==s.serviceFilter||s.fileNameSet.size>0&&!s.fileNameSet.has(n.filePath))continue;const u=i.get(n.filePath)??K(t,n.filePath);i.set(n.filePath,u);const d=a.get(n.serviceName),m=d?.suites[c]?.find($=>$.name===n.suiteName);if(d&&(!m||m.testType!==c||m.engineId!==u.engineId||m.profileId!==u.profileId))throw new Error(`Normalized suite ${n.serviceName}/${c}:${n.suiteName} conflicts with file ${n.filePath}`);const S=d&&(u.skipReason||d.testkit.skip?.fileReasonByPath.get(n.filePath)||me(d,c,n.suiteName))||null;if(s.runnableOnly&&S)continue;const j=d?[...new Set([...ge(d,c,n.suiteName),...d.testkit.requirements.fileLocksByPath.get(n.filePath)||[],...u.locks])].sort():[],x=l.get(n.serviceName)?.dependsOn||[],w=re({serviceName:n.serviceName,suiteName:n.suiteName,testType:c,filePath:n.filePath,engineId:u.engineId,profileId:u.profileId,skipReason:S,locks:j,runtimeIsolation:d&&m?G(d,m,n.filePath):q,dependsOn:x});v.push(w);const F=[n.serviceName,c,n.suiteName].join("|"),O=p.get(F);O?O.files.push(w):p.set(F,{serviceName:n.serviceName,testType:c,suiteName:n.suiteName,files:[w]})}const h=[...p.values()].map(n=>se(n.serviceName,n.testType,n.suiteName,n.files,l.get(n.serviceName)?.dependsOn||[]));for(const n of h){const c=l.get(n.service)||{name:n.service,discovered:!0,localCwd:o.services[n.service]?.inferredLocalCwd||".",dependsOn:[...r[n.service]?.dependsOn||[]],suiteCount:0,fileCount:0,activeFileCount:0,skippedFileCount:0};c.suiteCount+=1,c.fileCount+=n.fileCount,c.activeFileCount+=n.activeFileCount,c.skippedFileCount+=n.skippedFileCount,l.set(n.service,c)}return{services:[...l.values()].sort((n,c)=>n.name.localeCompare(c.name)),suites:h.sort(he),files:v.sort(Ce)}}function re(e){const i=testTypeContract(e.testType,e.engineId,e.profileId);return{id:ye(e.serviceName,e.testType,e.engineId,e.profileId,e.filePath),path:e.filePath,displayName:fileDisplayName(e.filePath),service:e.serviceName,suiteName:e.suiteName,groupLabel:formatDisplayName(e.suiteName),testType:e.testType,engineId:e.engineId,profileId:e.profileId,skipped:!!e.skipReason,skipReason:e.skipReason,locks:e.locks,runtimeIsolation:e.runtimeIsolation,dependsOn:[...e.dependsOn],contract:i}}function se(e,i,t,o,r){const s=ce(e,i,t,o),a=o.filter(l=>l.skipped).length;return{id:ve(e,i,s.engineId,s.profileId,t),service:e,name:t,displayName:formatDisplayName(t),testType:i,engineId:s.engineId,profileId:s.profileId,groupLabel:formatDisplayName(t),fileCount:o.length,activeFileCount:o.length-a,skippedFileCount:a,dependsOn:[...r],locks:[...new Set(o.flatMap(l=>l.locks))].sort(),filePaths:o.map(l=>l.path),contract:s.contract}}function ce(e,i,t,o){const r=o[0];for(const s of o.slice(1))if(s.engineId!==r.engineId||s.profileId!==r.profileId)throw new Error(`Discovery suite ${e}/${i}:${t} mixes engine profiles ${r.engineId}/${r.profileId} and ${s.engineId}/${s.profileId}`);return{contract:r.contract,engineId:r.engineId,profileId:r.profileId}}function R(e){return{...e,diagnostics:e.diagnostics.sort(we),summary:Q({diagnostics:e.diagnostics,files:e.files,serviceCount:e.services.length,suiteCount:e.suites.length})}}function ae(e,i,t){if(!e)return;const o=new Set([...Object.keys(i),...Object.keys(t.services),...Object.keys(t.suitesByService)]);if(o.has(e))return;const r=[...o].sort((s,a)=>s.localeCompare(a)).join(", ");throw new Error(r.length>0?`Service "${e}" not found. Available: ${r}`:`Service "${e}" not found.`)}function le(e){const i=de(Z(b(e.type))),t=ee(b(e.suite)),o=b(e.file).map(N);return{typeValues:i,suiteSelectors:t,fileNames:o,fileNameSet:new Set(o),serviceFilter:e.service||null,runnableOnly:!!e.runnableOnly,diagnosticsMode:e.diagnostics==="error"?"error":"report"}}function de(e){const i=[];for(const t of e){if(!U(t))throw new Error(`Discovery produced unsupported test type "${t}".`);i.push(t)}return i}function ue(e){const i=I(f(e,"services")),t={};for(const[o,r]of Object.entries(i)){const s=I(r),a=I(f(s,"local"));t[o]={dependsOn:pe(f(s,"dependsOn")),discovery:M(f(s,"discovery")),localCwd:N(f(a,"cwd")||"."),localDisabled:f(s,"local")===!1}}return{discovery:M(f(e,"discovery")),services:t}}function fe(e){const i={};for(const[t,o]of Object.entries(e))o&&(i[t]={discovery:o.discovery,local:o.localDisabled?!1:{baseUrl:"",cwd:o.localCwd,env:{},start:""}});return i}function M(e){try{return J(e,{allowRoots:!0})}catch{return{}}}function pe(e){return Array.isArray(e)?e.filter(t=>typeof t=="string"):[]}function me(e,i,t){for(const o of e.testkit.skip?.suites||[])if(k(i,t,[o.selector]))return o.reason;return null}function ge(e,i,t){const o=new Set;for(const r of e.testkit.requirements.suites)if(k(i,t,[r.selector]))for(const s of r.locks)o.add(s);return[...o].sort()}function b(e){return Array.isArray(e)?e.filter(t=>t!=null).map(t=>C(t)):e==null||e===""?[]:[C(e)]}function N(e){return C(e).split(g.sep).join("/").replace(/^\.\/+/,"")}export function fileDisplayName(e){const i=new RegExp(`\\.(${_.join("|")})\\.testkit\\.ts$`),t=g.posix.basename(e).replace(i,"");return formatDisplayName(t)}function ve(e,i,t,o,r){return[e,i,t,o,r].join("|")}function ye(e,i,t,o,r){return[e,i,t,o,N(r)].join("|")}function he(e,i){return e.service.localeCompare(i.service)||e.testType.localeCompare(i.testType)||e.name.localeCompare(i.name)}function Ce(e,i){return e.service.localeCompare(i.service)||e.testType.localeCompare(i.testType)||e.suiteName.localeCompare(i.suiteName)||e.path.localeCompare(i.path)}function we(e,i){return T(e.severity)-T(i.severity)||e.code.localeCompare(i.code)||(e.path||"").localeCompare(i.path||"")}function T(e){return e==="error"?1:2}
|
|
1
|
+
import g from"path";import{randomUUID as z}from"node:crypto";import{isUnknownRecord as D}from"../../kernel/unknown-values.js";import{loadConfigContext as P,resolveProductDir as A}from"./load.js";import{discoverProject as B,hasDiscoveryErrors as H,buildDiscoveryErrorFromDiagnostics as L}from"./discovery.js";import{loadTestkitConfig as V}from"./config-loader.js";import{TEST_TYPE_ORDER as _,isRunType as U}from"../../domain/test-types.js";import"../../domain/identifiers.js";import{DEFAULT_RUNTIME_ISOLATION_POLICY as q,resolveRuntimeIsolationPolicy as G}from"./runtime-isolation.js";import{resolveEngineProfile as Y}from"../../engines/catalog.js";import{readTestFileMetadata as K}from"./file-metadata.js";import{normalizeDiscoveryConfig as J}from"./decode-discovery.js";import"./path-policy.js";import{buildDiscoverySummary as Q,emptyDiscoverySummary as W}from"./discovery-summary.js";import{matchesSelectedTypes as X,matchesSuiteSelectors as k,normalizeTypeValues as Z,parseSuiteSelectors as ee}from"../../domain/suite-selection.js";import{errorMessage as E}from"../../kernel/error-serialization.js";import{readObjectRecord as I,readProperty as f}from"./values.js";const ie=9,C=String;export async function discoverTests(e={}){return(await loadDiscoveryContext(e)).discovery}export async function loadDiscoveredConfigContext(e={}){const i=await P({dir:e.dir,configImportCacheSalt:z()}),{discovery:t}=await loadDiscoveryContext({dir:i.productDir,diagnostics:"error"},i);return{context:i,discovery:t}}export async function loadDiscoveryContext(e={},i){const t=i?.productDir??A(process.cwd(),e.dir),o=le(e);if(i&&o.diagnosticsMode==="error"&&H(i.discovery.diagnostics))throw L(i.discovery.diagnostics);const r=i?{config:i.config,configFile:i.configFile,diagnostics:[]}:await te(t,o.diagnosticsMode),s={schemaVersion:ie,source:"testkit-discovery",product:{name:g.basename(t),directory:t},configFile:r.configFile?g.relative(t,r.configFile)||g.basename(r.configFile):null,filters:{service:o.serviceFilter,types:o.typeValues,suiteSelectors:o.suiteSelectors.map(n=>n.raw),fileNames:o.fileNames,runnableOnly:o.runnableOnly,diagnostics:o.diagnosticsMode},services:[],suites:[],files:[],diagnostics:[...r.diagnostics],summary:W()};if(!r.config)return{discovery:R(s),context:null};const a=ue(r.config),l=fe(a.services),p=i?.discovery??B(t,l,{strict:o.diagnosticsMode==="error",discovery:a.discovery}),v={project:p,fileMetadataByPath:new Map(i?.configs.flatMap(n=>[...n.testkit.fileMetadataByPath])??[])};s.diagnostics.push(...p.diagnostics),ae(o.serviceFilter,a.services,p);let y=i??null;try{y=i??await P({dir:t,preparedDiscovery:v,configContext:{config:r.config,configFile:r.configFile},discoveryOptions:{strict:o.diagnosticsMode==="error"}})}catch(n){if(o.diagnosticsMode==="error")throw n;s.diagnostics.push({code:"config_invalid",severity:"error",message:E(n)})}const h=ne({productDir:t,rawDiscovery:p,explicitServices:a.services,configs:y?.configs??[],fileMetadataByPath:v.fileMetadataByPath,filters:o});return{discovery:R({...s,...h}),context:y}}export function formatDisplayName(e){return C(e||"").split(/[-_/]+/g).filter(Boolean).map(i=>i.replace(/^\w/,t=>t.toUpperCase())).join(" ")}export function testTypeContract(e,i,t){const o=Y(e,i);if(o.id!==t)throw new Error(`Discovery profile mismatch for ${e}: metadata resolved ${t}, catalog resolved ${o.id}`);return e==="data"?{authoringApi:"@gfdelarue/testkit/node-vitest/data",executionHost:o.capabilities.executionHost,summary:"Data suites use the node-vitest engine and record Testkit data evidence; they are not raw Vitest or service-namespace tests."}:e==="contract"?{authoringApi:"@gfdelarue/testkit/node-vitest/contract",executionHost:o.capabilities.executionHost,summary:"Contract suites use the node-vitest engine for Node-backed service/runtime contracts without data-flow evidence requirements."}:i==="k6"?{authoringApi:"@gfdelarue/testkit/k6",executionHost:o.capabilities.executionHost}:i==="playwright"?{authoringApi:"@gfdelarue/testkit/playwright",executionHost:o.capabilities.executionHost}:{executionHost:o.capabilities.executionHost}}async function te(e,i){try{const t=await V(e),{config:o,configFile:r}=oe(t);return{config:o,configFile:r,diagnostics:[]}}catch(t){if(i==="error")throw t;return{config:null,configFile:null,diagnostics:[{code:"config_invalid",severity:"error",message:E(t)}]}}}function oe(e){if(!D(e))throw new Error("testkit config loader must return an object");const i=f(e,"config"),t=f(e,"configFile");if(!D(i)){const o=typeof t=="string"?g.basename(t):"testkit config file";throw new Error(`testkit config file must export an object: ${o}`)}if(t!==null&&typeof t!="string")throw new Error("testkit config loader returned an invalid config path");return{config:i,configFile:t}}function ne({configs:e,fileMetadataByPath:i,productDir:t,rawDiscovery:o,explicitServices:r,filters:s}){const a=new Map(e.map(n=>[n.name,n])),l=new Map,p=new Map,v=[],y=new Set([...Object.keys(r),...Object.keys(o.services),...a.keys()]);for(const n of y){if(s.serviceFilter&&n!==s.serviceFilter)continue;const c=a.get(n),u=o.services[n],d=r[n];l.set(n,{name:n,discovered:c!==void 0||!!u,localCwd:c?c.testkit.local?.cwd||".":d?.localCwd||u?.inferredLocalCwd||".",dependsOn:[...c?.testkit.dependsOn??d?.dependsOn??[]],suiteCount:0,fileCount:0,activeFileCount:0,skippedFileCount:0})}for(const n of o.files){const c=n.testType;if(!X(c,s.typeValues)||!k(c,n.suiteName,s.suiteSelectors)||s.serviceFilter&&n.serviceName!==s.serviceFilter||s.fileNameSet.size>0&&!s.fileNameSet.has(n.filePath))continue;const u=i.get(n.filePath)??K(t,n.filePath);i.set(n.filePath,u);const d=a.get(n.serviceName),m=d?.suites[c]?.find(j=>j.name===n.suiteName);if(d&&(!m||m.testType!==c||m.engineId!==u.engineId||m.profileId!==u.profileId))throw new Error(`Normalized suite ${n.serviceName}/${c}:${n.suiteName} conflicts with file ${n.filePath}`);const b=d&&(u.skipReason||d.testkit.skip?.fileReasonByPath.get(n.filePath)||me(d,c,n.suiteName))||null;if(s.runnableOnly&&b)continue;const x=d?[...new Set([...ge(d,c,n.suiteName),...d.testkit.requirements.fileLocksByPath.get(n.filePath)||[],...u.locks])].sort():[],$=l.get(n.serviceName)?.dependsOn||[],w=re({serviceName:n.serviceName,suiteName:n.suiteName,testType:c,filePath:n.filePath,engineId:u.engineId,profileId:u.profileId,skipReason:b,locks:x,runtimeIsolation:d&&m?G(d,m,n.filePath):q,dependsOn:$});v.push(w);const F=[n.serviceName,c,n.suiteName].join("|"),O=p.get(F);O?O.files.push(w):p.set(F,{serviceName:n.serviceName,testType:c,suiteName:n.suiteName,files:[w]})}const h=[...p.values()].map(n=>se(n.serviceName,n.testType,n.suiteName,n.files,l.get(n.serviceName)?.dependsOn||[]));for(const n of h){const c=l.get(n.service)||{name:n.service,discovered:!0,localCwd:o.services[n.service]?.inferredLocalCwd||".",dependsOn:[...r[n.service]?.dependsOn||[]],suiteCount:0,fileCount:0,activeFileCount:0,skippedFileCount:0};c.suiteCount+=1,c.fileCount+=n.fileCount,c.activeFileCount+=n.activeFileCount,c.skippedFileCount+=n.skippedFileCount,l.set(n.service,c)}return{services:[...l.values()].sort((n,c)=>n.name.localeCompare(c.name)),suites:h.sort(he),files:v.sort(Ce)}}function re(e){const i=testTypeContract(e.testType,e.engineId,e.profileId);return{id:ye(e.serviceName,e.testType,e.engineId,e.profileId,e.filePath),path:e.filePath,displayName:fileDisplayName(e.filePath),service:e.serviceName,suiteName:e.suiteName,groupLabel:formatDisplayName(e.suiteName),testType:e.testType,engineId:e.engineId,profileId:e.profileId,skipped:!!e.skipReason,skipReason:e.skipReason,locks:e.locks,runtimeIsolation:e.runtimeIsolation,dependsOn:[...e.dependsOn],contract:i}}function se(e,i,t,o,r){const s=ce(e,i,t,o),a=o.filter(l=>l.skipped).length;return{id:ve(e,i,s.engineId,s.profileId,t),service:e,name:t,displayName:formatDisplayName(t),testType:i,engineId:s.engineId,profileId:s.profileId,groupLabel:formatDisplayName(t),fileCount:o.length,activeFileCount:o.length-a,skippedFileCount:a,dependsOn:[...r],locks:[...new Set(o.flatMap(l=>l.locks))].sort(),filePaths:o.map(l=>l.path),contract:s.contract}}function ce(e,i,t,o){const r=o[0];for(const s of o.slice(1))if(s.engineId!==r.engineId||s.profileId!==r.profileId)throw new Error(`Discovery suite ${e}/${i}:${t} mixes engine profiles ${r.engineId}/${r.profileId} and ${s.engineId}/${s.profileId}`);return{contract:r.contract,engineId:r.engineId,profileId:r.profileId}}function R(e){return{...e,diagnostics:e.diagnostics.sort(we),summary:Q({diagnostics:e.diagnostics,files:e.files,serviceCount:e.services.length,suiteCount:e.suites.length})}}function ae(e,i,t){if(!e)return;const o=new Set([...Object.keys(i),...Object.keys(t.services),...Object.keys(t.suitesByService)]);if(o.has(e))return;const r=[...o].sort((s,a)=>s.localeCompare(a)).join(", ");throw new Error(r.length>0?`Service "${e}" not found. Available: ${r}`:`Service "${e}" not found.`)}function le(e){const i=de(Z(N(e.type))),t=ee(N(e.suite)),o=N(e.file).map(S);return{typeValues:i,suiteSelectors:t,fileNames:o,fileNameSet:new Set(o),serviceFilter:e.service||null,runnableOnly:!!e.runnableOnly,diagnosticsMode:e.diagnostics==="error"?"error":"report"}}function de(e){const i=[];for(const t of e){if(!U(t))throw new Error(`Discovery produced unsupported test type "${t}".`);i.push(t)}return i}function ue(e){const i=I(f(e,"services")),t={};for(const[o,r]of Object.entries(i)){const s=I(r),a=I(f(s,"local"));t[o]={dependsOn:pe(f(s,"dependsOn")),discovery:M(f(s,"discovery")),localCwd:S(f(a,"cwd")||"."),localDisabled:f(s,"local")===!1}}return{discovery:M(f(e,"discovery")),services:t}}function fe(e){const i={};for(const[t,o]of Object.entries(e))o&&(i[t]={discovery:o.discovery,local:o.localDisabled?!1:{baseUrl:"",cwd:o.localCwd,env:{},start:""}});return i}function M(e){try{return J(e,{allowRoots:!0})}catch{return{}}}function pe(e){return Array.isArray(e)?e.filter(t=>typeof t=="string"):[]}function me(e,i,t){for(const o of e.testkit.skip?.suites||[])if(k(i,t,[o.selector]))return o.reason;return null}function ge(e,i,t){const o=new Set;for(const r of e.testkit.requirements.suites)if(k(i,t,[r.selector]))for(const s of r.locks)o.add(s);return[...o].sort()}function N(e){return Array.isArray(e)?e.filter(t=>t!=null).map(t=>C(t)):e==null||e===""?[]:[C(e)]}function S(e){return C(e).split(g.sep).join("/").replace(/^\.\/+/,"")}export function fileDisplayName(e){const i=new RegExp(`\\.(${_.join("|")})\\.testkit\\.ts$`),t=g.posix.basename(e).replace(i,"");return formatDisplayName(t)}function ve(e,i,t,o,r){return[e,i,t,o,r].join("|")}function ye(e,i,t,o,r){return[e,i,t,o,S(r)].join("|")}function he(e,i){return e.service.localeCompare(i.service)||e.testType.localeCompare(i.testType)||e.name.localeCompare(i.name)}function Ce(e,i){return e.service.localeCompare(i.service)||e.testType.localeCompare(i.testType)||e.suiteName.localeCompare(i.suiteName)||e.path.localeCompare(i.path)}function we(e,i){return T(e.severity)-T(i.severity)||e.code.localeCompare(i.code)||(e.path||"").localeCompare(i.path||"")}function T(e){return e==="error"?1:2}
|
|
@@ -46,7 +46,6 @@ export interface DiscoveryProject {
|
|
|
46
46
|
suitesByService: DiscoveredSuitesByService;
|
|
47
47
|
}
|
|
48
48
|
export declare function discoverProject(productDir: string, explicitServices?: DiscoveryServices, options?: DiscoveryOptions): DiscoveryProject;
|
|
49
|
-
export declare function discoverSuites(productDir: string, explicitServices?: DiscoveryServices, options?: DiscoveryOptions): DiscoveredSuitesByService;
|
|
50
49
|
export declare function hasDiscoveryErrors(diagnostics?: readonly ({
|
|
51
50
|
severity?: unknown;
|
|
52
51
|
} | null | undefined)[]): boolean;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import L from"fs";import h from"path";import{recordWork as k}from"../../kernel/work-metrics.js";import{DEFAULT_DISCOVERY_EXCLUDES as F,normalizeDiscoveryPath as K,resolveDiscoveryRoots as z,shouldExcludeDiscoveryPath as D}from"./path-policy.js";import{normalizeDiscoveryConfig as C}from"./decode-discovery.js";const E=String,j="__testkit__",O=[{suffix:".int.testkit.ts",testType:"int"},{suffix:".e2e.testkit.ts",testType:"e2e"},{suffix:".contract.testkit.ts",testType:"contract"},{suffix:".data.testkit.ts",testType:"data"},{suffix:".dal.testkit.ts",testType:"dal"},{suffix:".load.testkit.ts",testType:"load"},{suffix:".ui.testkit.ts",testType:"ui"}];export function discoverProject(t,e={},
|
|
2
|
-
`))}function A(t,e={}){const
|
|
1
|
+
import L from"fs";import h from"path";import{recordWork as k}from"../../kernel/work-metrics.js";import{DEFAULT_DISCOVERY_EXCLUDES as F,normalizeDiscoveryPath as K,resolveDiscoveryRoots as z,shouldExcludeDiscoveryPath as D}from"./path-policy.js";import{normalizeDiscoveryConfig as C}from"./decode-discovery.js";const E=String,j="__testkit__",O=[{suffix:".int.testkit.ts",testType:"int"},{suffix:".e2e.testkit.ts",testType:"e2e"},{suffix:".contract.testkit.ts",testType:"contract"},{suffix:".data.testkit.ts",testType:"data"},{suffix:".dal.testkit.ts",testType:"dal"},{suffix:".load.testkit.ts",testType:"load"},{suffix:".ui.testkit.ts",testType:"ui"}];export function discoverProject(t,e={},r={}){const n=r.strict!==!1,o=C(r.discovery,{allowRoots:!0}),{suiteFiles:s,legacyFiles:a}=A(t,o),i={},p={},u=X(a),g=[];for(const c of s){const l=R(c);if(!l)continue;const f=M(c,e,o);if(f===null)continue;if(f.length===0){u.push({code:"unowned_test",severity:"error",message:`Unowned test file: ${c}`,path:c});continue}if(f.length>1){u.push({code:"ambiguous_test",severity:"error",message:`Ambiguous test file: ${c} -> ${f.map(v=>v.name).sort((v,m)=>v.localeCompare(m)).join(", ")}`,path:c,serviceNames:f.map(v=>v.name).sort((v,m)=>v.localeCompare(m))});continue}const d=f[0];p[d.name]=Y(p[d.name],d);const y=W(d,c),w=q(y,l.suffix);g.push({serviceName:d.name,testType:l.testType,filePath:c,suitePath:w.suitePath})}if(n&&hasDiscoveryErrors(u))throw buildDiscoveryErrorFromDiagnostics(u);for(const c of g){const l=i[c.serviceName]||{},f=l[c.testType]||[],d=c.suitePath.join("/");let y=f.find(w=>w._suiteKey===d);y||(y={files:[],_suiteKey:d,_suitePath:c.suitePath},f.push(y),l[c.testType]=f,i[c.serviceName]=l),y.files.push(c.filePath)}const _=[],T={};for(const[c,l]of Object.entries(i)){if(!l)continue;const f={};for(const[d,y]of Object.entries(l)){if(!y)continue;const w=B(y),v=[];for(const m of y){const P=I(w,m._suiteKey);m.files.sort((S,$)=>S.localeCompare($));for(const S of m.files)_.push({serviceName:c,testType:d,suiteName:P,suitePath:[...m._suitePath],filePath:S});v.push({name:P,files:m.files})}f[d]=v.sort((m,P)=>m.name.localeCompare(P.name))}T[c]=f}return{services:p,suitesByService:T,files:_.sort((c,l)=>c.serviceName.localeCompare(l.serviceName)||c.testType.localeCompare(l.testType)||c.suiteName.localeCompare(l.suiteName)||c.filePath.localeCompare(l.filePath)),diagnostics:u}}export function hasDiscoveryErrors(t=[]){return t.some(e=>e?.severity==="error")}export function buildDiscoveryErrorFromDiagnostics(t=[]){const e=["Filesystem discovery failed for one or more .testkit.ts files."],r=t.filter(s=>s.code==="legacy_path").map(s=>s.path),n=t.filter(s=>s.code==="unowned_test").map(s=>s.path),o=t.filter(s=>s.code==="ambiguous_test");if(r.length>0){e.push(""),e.push("Legacy test files outside __testkit__:");for(const s of r)e.push(`- ${s}`)}if(n.length>0){e.push(""),e.push("Unowned test files:");for(const s of n)e.push(`- ${s}`)}if(o.length>0){e.push(""),e.push("Ambiguous test files:");for(const s of o)e.push(`- ${s.path} -> ${s.serviceNames.join(", ")}`)}return e.push(""),e.push('Expected test files to live under a "__testkit__" directory within a service root.'),new Error(e.join(`
|
|
2
|
+
`))}function A(t,e={}){const r=[],n=[],o=[...new Set([...F,...(e.exclude||[]).map(x)])],s=z(t,e.roots,".");for(;s.length>0;){const a=s.pop();if(a!==void 0){k("discovery.directoriesRead");for(const i of L.readdirSync(a,{withFileTypes:!0})){k("discovery.entriesVisited");const p=h.join(a,i.name);if(i.isSymbolicLink())continue;if(i.isDirectory()){const g=K(h.relative(t,p));if(D(g,o))continue;s.push(p);continue}if(!i.isFile())continue;const u=x(h.relative(t,p));R(u)&&(u.split("/").includes(j)?r.push(u):n.push(u))}}}return{suiteFiles:r.sort((a,i)=>a.localeCompare(i)),legacyFiles:n.sort((a,i)=>a.localeCompare(i))}}function M(t,e,r={}){const n=Object.entries(e).flatMap(([i,p])=>U(i,p,r));if(n.length===0)return[{name:"app",source:"implicit-root",cwdPrefix:"."}];const o=n.filter(i=>i.depth===0||i.matchPrefix&&b(t,i.matchPrefix)),s=o.filter(i=>V(i,t));if(s.length===0)return o.length>0&&o.every(i=>N(i,t))?null:[];const a=Math.max(...s.map(i=>i.depth));return s.filter(i=>i.depth===a)}function B(t){const e=new Map,r=new Map;for(const n of t){const o=n._suitePath;let s="";for(let a=1;a<=o.length;a+=1){s=o.slice(-a).join("-");const i=e.get(s);if(!i){e.set(s,n._suiteKey);break}if(i===n._suiteKey)break;s=""}s||(s=o.join("-")),r.set(n._suiteKey,s)}if(r.size!==t.length)for(const n of t)r.has(n._suiteKey)||r.set(n._suiteKey,n._suitePath.join("-"));if(new Set(r.values()).size!==t.length){const n=new Map;for(const o of t){const s=o._suitePath.join("-"),a=n.get(s)||0;n.set(s,a+1),r.set(o._suiteKey,a===0?s:`${s}-${E(a+1)}`)}}return r}function I(t,e){const r=t.get(e);if(r===void 0)throw new Error(`Discovery suite name was not resolved for "${e}"`);return r}function U(t,e,r={}){const n=e?.local,o=x(n&&n.cwd||"."),s=C(e?.discovery,{allowRoots:!0}),a=Array.isArray(s.roots)&&s.roots.length>0?s.roots:["."],i=(s.exclude||[]).map(u=>x(o==="."?u:h.posix.join(o,u))),p=[...new Set([...F,...(r.exclude||[]).map(x),...i])];return a.map(u=>{const g=x(u||"."),_=o==="."?g:g==="."?o:x(h.posix.join(o,g));return{name:t,source:"cwd",cwdPrefix:o==="."?null:o,matchPrefix:_==="."?null:_,depth:_==="."?0:_.split("/").filter(Boolean).length,exclude:p}})}function V(t,e){return N(t,e)?!1:!!(t.depth===0||t.matchPrefix&&b(e,t.matchPrefix))}function W(t,e){return t.matchPrefix&&b(e,t.matchPrefix)?h.posix.relative(t.matchPrefix,e):t.cwdPrefix&&b(e,t.cwdPrefix)?h.posix.relative(t.cwdPrefix,e):e}function N(t,e){return D(x(e),t.exclude)}function q(t,e){const r=t.split("/").filter(Boolean),n=r.indexOf(j);if(n===-1)return{suitePath:[h.posix.basename(t,e)]};const o=r.slice(0,n),s=r.slice(n+1);return s.length===0?{suitePath:[h.posix.basename(t,e)]}:s.length===1?o.length===0?{suitePath:[h.posix.basename(s[0],e)]}:{suitePath:o}:{suitePath:[...o,s[0]]}}function Y(t,e){return t?{...t,inferredLocalCwd:t.inferredLocalCwd==="."?e.cwdPrefix||".":t.inferredLocalCwd}:{name:e.name,inferredLocalCwd:e.cwdPrefix||".",source:e.source}}function R(t){return O.find(e=>t.endsWith(e.suffix))||null}function b(t,e){return t===e||t.startsWith(`${e}/`)}function x(t){const e=E(t).split(h.sep).join("/");return e==="."||e==="./"?".":e.replace(/^\.\/+/,"").replace(/\/+$/,"")||"."}function X(t){return t.map(e=>({code:"legacy_path",severity:"error",message:`Legacy test file outside __testkit__: ${e}`,path:e}))}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import
|
|
1
|
+
import d from"fs";import p from"path";import{parseDotenv as m}from"../../process/files/dotenv.js";import{resolveServiceCwd as l}from"./paths.js";import{isUnknownArray as h,isUnknownRecord as w}from"../../kernel/unknown-values.js";export{m as parseDotenv};export function inferEnvFiles(n,e,r){const o=u(e,"service config"),v=u(r,"local config"),s=a(o?.envFile,"service.envFile"),f=g(o?.envFiles,"service.envFiles");if(s||f){const i=[];return s&&i.push(s),f&&i.push(...f),i}const t=[],c=a(v?.cwd,"service.local.cwd")??".";return c!=="."&&(t.push(p.posix.join(c,".env.testkit")),t.push(p.posix.join(c,".env"))),t.push(".env.testkit"),t.push(".env"),[...new Set(t)].map(i=>i.split(p.sep).join("/")).filter(i=>d.existsSync(l(n,i)))}export function loadServiceEnv(n,e){const r={};for(const o of e)Object.assign(r,m(l(n,o)));return r}function u(n,e){if(n!=null){if(!w(n))throw new Error(`${e} must be an object`);return n}}function a(n,e){if(n!=null){if(typeof n!="string"||n.trim().length===0)throw new Error(`${e} must be a non-empty string`);return n.trim()}}function g(n,e){if(n!=null){if(!h(n))throw new Error(`${e} must be an array of non-empty strings`);return n.map(r=>{if(typeof r!="string"||r.trim().length===0)throw new Error(`${e} must be an array of non-empty strings`);return r.trim()})}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export
|
|
1
|
+
export{};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type ts from "typescript";
|
|
2
2
|
import { type EngineId, type RunnerProfileId } from "../../domain/identifiers.js";
|
|
3
|
-
import { type RuntimeIsolationPolicy } from "
|
|
3
|
+
import { type RuntimeIsolationPolicy } from "../../domain/runtime-isolation-contract.js";
|
|
4
4
|
import { type EngineAuthoringProfile } from "../../engines/catalog.js";
|
|
5
5
|
export interface TestFileMetadata {
|
|
6
6
|
engineId: EngineId;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{isWorkObserved as M,recordWork as L}from"../../kernel/work-metrics.js";import{isPathWithin as h}from"../../process/files/paths.js";import d from"fs";import E from"path";import{createRequire as A}from"node:module";import{fileURLToPath as O}from"node:url";import{ENGINE_IDS as j,PUBLIC_TEST_TYPES as B,isEngineId as K}from"../../domain/identifiers.js";import{normalizeRuntimeIsolationPolicy as V}from"./runtime-isolation.js";import{resolveEngineProfile as _,expectedEngineAuthoringModule as W,invalidEngineAuthoringImport as z,requireMatchingHostImport as U}from"../../engines/catalog.js";const G=A(import.meta.url),J=O(import.meta.resolve("typescript"));let H;function a(){return H??=G(J)}const g="@gfdelarue/testkit",X="@gfdelarue/testkit-engine-";export function loadTestFileMetadataMap(e,t={},i=new Map){for(const r of Object.values(t||{}))for(const s of Object.values(r||{}))for(const n of s||[])for(const o of n.files||[])i.has(o)||i.set(o,readTestFileMetadata(e,o));return i}export function readTestFileMetadata(e,t){const i=a(),r=Ee(e,t),s=ke(t);let n;if(M()){const m=d.readFileSync(r);L("metadata.filesRead",1),L("metadata.bytesRead",m.byteLength),n=m.toString("utf8")}else n=d.readFileSync(r,"utf8");const o=i.createSourceFile(r,n,i.ScriptTarget.Latest,!0,i.ScriptKind.TS),u=Ie(o);if(u.length>0){const m=u.map(f=>i.flattenDiagnosticMessageText(f.messageText," ")).join("; ");throw c(t,`TypeScript syntax is invalid: ${m}`)}let l=null,p=null;for(const m of o.statements)if(!(!i.isVariableStatement(m)||!ie(m))){for(const f of m.declarationList.declarations)if(!(!i.isIdentifier(f.name)||f.name.text!=="testkit")){if(l)throw c(t,'exported "testkit" metadata must be declared exactly once');if(!re(m))throw c(t,'exported "testkit" metadata must be const');l=f,p=m}}if(!l||!p)throw c(t,'export const testkit = defineFile({ engine: "..." }) is required');se(o,p,t);const S=Y(l.initializer,t,s);return requireMatchingEngineAuthoringImports(o,t,S),S}function Y(e,t,i){const r=a();if(!e)throw c(t,'exported "testkit" metadata has no value');if(!r.isCallExpression(e))throw c(t,"testkit metadata must use defineFile({ ... })");if(te(e.expression)!=="defineFile")throw c(t,"testkit metadata must call defineFile");if(e.arguments.length!==1)throw c(t,"defineFile metadata must receive exactly one object literal");const s=e.arguments[0];if(!r.isObjectLiteralExpression(s))throw c(t,"defineFile metadata must receive an object literal");return Q(s,t,i)}function Q(e,t,i){const r=a(),s={locks:[],runtimeIsolation:null,skipReason:null};let n=null;const o=new Set;for(const l of e.properties){if(r.isSpreadAssignment(l))throw c(t,"testkit metadata may not contain spread properties");if(!r.isPropertyAssignment(l))throw c(t,"testkit metadata properties must use key: value assignments");const p=b(l.name,t,"testkit metadata");if(!ne(p))throw c(t,`unknown metadata property "${p}"`);if(o.has(p))throw c(t,`duplicate metadata property "${p}"`);if(o.add(p),p==="engine"){n=I(l.initializer,t,'metadata property "engine"');continue}if(p==="locks"){s.locks=Z(l.initializer,t);continue}if(p==="skip"){s.skipReason=P(l.initializer,t);continue}s.runtimeIsolation=ee(l.initializer,t)}if(!n)throw c(t,'a literal "engine" property is required');if(!K(n))throw c(t,`unsupported engine "${n}"; expected one of: ${j.join(", ")}`);let u;try{u=_(i,n)}catch(l){throw c(t,$(l),l)}return{engineId:n,locks:s.locks,profileId:u.id,runtimeIsolation:s.runtimeIsolation,skipReason:s.skipReason}}function Z(e,t){const i=a();if(!i.isArrayLiteralExpression(e))throw c(t,'metadata property "locks" must be an array');const r=[];for(const s of e.elements){if(i.isSpreadElement(s)||i.isOmittedExpression(s))throw c(t,'metadata property "locks" must contain only string literals');r.push(T(s,t,'metadata property "locks" entry'))}return[...new Set(r)].sort()}function P(e,t){const i=a(),r=x(e);if(r!==null)return D(r,t,'metadata property "skip"').trim();if(!i.isObjectLiteralExpression(e))throw c(t,'metadata property "skip" must be a string literal or { reason: string literal }');if(e.properties.length!==1)throw c(t,'metadata property "skip" object must contain exactly one reason property');const s=e.properties[0];if(!i.isPropertyAssignment(s))throw c(t,'metadata property "skip" reason must use a string-literal assignment');if(b(s.name,t,'metadata property "skip"')!=="reason")throw c(t,'metadata property "skip" object only accepts "reason"');return T(s.initializer,t,'metadata property "skip.reason"')}function ee(e,t){const i=I(e,t,'metadata property "runtimeIsolation"');try{return V(i,"defineFile runtimeIsolation")}catch(r){throw c(t,$(r),r)}}function te(e){return a().isIdentifier(e)?e.text:null}function b(e,t,i){const r=a();if(r.isIdentifier(e)||r.isStringLiteral(e))return e.text;throw c(t,`${i} property names must be plain identifiers or string literals`)}function x(e){const t=a();return t.isStringLiteral(e)||t.isNoSubstitutionTemplateLiteral(e)?e.text:null}function I(e,t,i){const r=x(e);if(r===null)throw c(t,`${i} must be a string literal`);return D(r,t,i)}function T(e,t,i){return I(e,t,i).trim()}function D(e,t,i){if(e.trim().length===0)throw c(t,`${i} must not be empty`);return e}function ie(e){const t=a();return(e.modifiers||[]).some(i=>i.kind===t.SyntaxKind.ExportKeyword)}function re(e){const t=a();return(e.declarationList.flags&t.NodeFlags.Const)!==0}function ne(e){return["engine","locks","runtimeIsolation","skip"].includes(e)}function se(e,t,i){const r=a();let s=null,n=0;for(const o of e.statements){if(r.isImportDeclaration(o)){const u=o.importClause;if(!u||u.phaseModifier===r.SyntaxKind.TypeKeyword)continue;u.name?.text==="defineFile"&&(n+=1);const l=u.namedBindings;if(l&&r.isNamespaceImport(l)){l.name.text==="defineFile"&&(n+=1);continue}if(!l||!r.isNamedImports(l))continue;for(const p of l.elements){if(p.isTypeOnly||p.name.text!=="defineFile")continue;if(n+=1,(p.propertyName?.text??p.name.text)==="defineFile"&&r.isStringLiteral(o.moduleSpecifier)&&o.moduleSpecifier.text===g){if(s)throw c(i,`defineFile must have exactly one named value import from "${g}"`);s=o}}continue}n+=Se(o)}if(!s)throw c(i,`defineFile must be a named value import from "${g}"`);if(n!==1)throw c(i,"the imported defineFile binding may not be shadowed or redeclared");if(s.getStart(e)>t.getStart(e))throw c(i,"the defineFile import must precede metadata")}export function requireMatchingEngineAuthoringImports(e,t,i){oe(e,t,i);const r=W(i.profileId);for(const s of collectModuleSpecifiers(e))if(U(t,i,s),!(s===g||s===r||!s.startsWith(`${g}/`)&&!s.startsWith(X)))throw z(t,i,s,r)}function oe(e,t,i){const r=a(),s=N(e),n=o=>{if(ae(o,t,i),ce(o,s,t,i),r.isCallExpression(o)&&F(o,s)){const u=o.arguments.at(0);if(!u||!r.isStringLiteralLike(u)){const l=o.expression.kind===r.SyntaxKind.ImportKeyword?"import()":"require()";throw new Error(`Invalid Testkit authoring in "${t}": ${i.engineId}/${i.profileId} may not use non-literal ${l}; module specifiers must be string literals so engine boundaries can be verified`)}}for(const u of C(o))n(u);r.forEachChild(o,n)};n(e)}function F(e,t){const i=a();return e.expression.kind===i.SyntaxKind.ImportKeyword||i.isIdentifier(e.expression)&&e.expression.text==="require"&&!k(e.expression,t)}function ae(e,t,i){const r=a();if(r.isImportDeclaration(e)){const n=e.importClause?.namedBindings;if(n&&r.isNamedImports(n)&&n.elements.find(u=>!u.isTypeOnly&&(u.propertyName?.text??u.name.text)==="createRequire"))throw y(t,i,"createRequire")}if(r.isPropertyAccessExpression(e)&&e.name.text==="createRequire"||r.isElementAccessExpression(e)&&x(e.argumentExpression)==="createRequire"||r.isBindingElement(e)&&(me(e)==="createRequire"||r.isIdentifier(e.name)&&e.name.text==="createRequire"))throw y(t,i,"createRequire")}function ce(e,t,i,r){const s=a();if(s.isPropertyAccessExpression(e)&&e.name.text==="require"&&q(e.expression,t)||s.isElementAccessExpression(e)&&x(e.argumentExpression)==="require"&&q(e.expression,t))throw y(i,r,e.getText());if(s.isIdentifier(e)&&e.text==="require"&&!k(e,t)&&!ue(e)&&!le(e))throw y(i,r,"global require")}function y(e,t,i){return new Error(`Invalid Testkit authoring in "${e}": ${t.engineId}/${t.profileId} may not alias or indirectly access the module loader through ${i}; use literal import and require specifiers so engine boundaries can be verified`)}function q(e,t){return a().isIdentifier(e)&&(e.text==="globalThis"||e.text==="module")&&!k(e,t)}function ue(e){return a().isCallExpression(e.parent)&&e.parent.expression===e}function le(e){const t=a(),i=e.parent;return pe(e)||t.isPropertyAccessExpression(i)&&i.name===e||t.isPropertyAssignment(i)&&i.name===e||t.isImportSpecifier(i)&&i.propertyName===e||t.isExportSpecifier(i)&&i.parent.parent.moduleSpecifier!==void 0}function pe(e){const t=a(),i=e.parent;return i.name===e&&!t.isShorthandPropertyAssignment(i)&&!t.isExportSpecifier(i)}function me(e){const t=a(),i=e.propertyName;return i&&(t.isIdentifier(i)||t.isStringLiteralLike(i))?i.text:null}function N(e){const t=a(),i=new WeakMap,r=(n,o)=>{let u=i.get(n);u||(u=new Set,i.set(n,u)),v(u,o)},s=n=>{if(t.isImportDeclaration(n))fe(n,e,r);else if(t.isImportEqualsDeclaration(n)&&!n.isTypeOnly)r(e,n.name);else if(t.isVariableDeclaration(n)){const o=n.parent,u=t.isVariableDeclarationList(o)&&(o.flags&t.NodeFlags.BlockScoped)===0?de(n):w(n);r(u,n.name)}else t.isParameter(n)?r(ge(n),n.name):t.isFunctionDeclaration(n)&&n.name?r(w(n),n.name):t.isFunctionExpression(n)&&n.name?r(n,n.name):(t.isClassDeclaration(n)||t.isEnumDeclaration(n)||t.isModuleDeclaration(n))&&n.name&&t.isIdentifier(n.name)?r(w(n),n.name):t.isClassExpression(n)&&n.name?r(n,n.name):t.isCatchClause(n)&&n.variableDeclaration&&r(n,n.variableDeclaration.name);t.forEachChild(n,s)};return s(e),i}function fe(e,t,i){const r=a(),s=e.importClause;if(!s||s.phaseModifier===r.SyntaxKind.TypeKeyword)return;s.name&&i(t,s.name);const n=s.namedBindings;if(n&&r.isNamespaceImport(n))i(t,n.name);else if(n&&r.isNamedImports(n))for(const o of n.elements)o.isTypeOnly||i(t,o.name)}function v(e,t){const i=a();if(i.isIdentifier(t)){e.add(t.text);return}for(const r of t.elements)i.isOmittedExpression(r)||v(e,r.name)}function k(e,t){const i=a();let r=e;for(;;){if(t.get(r)?.has(e.text))return!0;if(i.isSourceFile(r))return!1;r=r.parent}}function w(e){let t=e.parent;for(;;){if(ye(t))return t;t=t.parent}}function de(e){const t=a();let i=e.parent;for(;;){if(t.isFunctionLike(i)||t.isSourceFile(i))return i;i=i.parent}}function ge(e){const t=a();let i=e.parent;for(;;){if(t.isFunctionLike(i)||t.isSourceFile(i))return i;i=i.parent}}function ye(e){const t=a();return t.isSourceFile(e)||t.isBlock(e)||t.isCaseBlock(e)||t.isCatchClause(e)||t.isForStatement(e)||t.isForInStatement(e)||t.isForOfStatement(e)||t.isFunctionLike(e)||t.isClassStaticBlockDeclaration(e)}export function collectModuleSpecifiers(e){const t=a(),i=new Set,r=N(e),s=n=>{const o=xe(n,r);o&&i.add(o);for(const u of C(n))s(u);t.forEachChild(n,s)};return s(e),i}function C(e){return e.jsDoc??[]}function xe(e,t){const i=a();if(i.isJSDocImportTag(e)&&i.isStringLiteralLike(e.moduleSpecifier)||(i.isImportDeclaration(e)||i.isExportDeclaration(e))&&e.moduleSpecifier&&i.isStringLiteralLike(e.moduleSpecifier))return e.moduleSpecifier.text;if(i.isImportEqualsDeclaration(e)&&i.isExternalModuleReference(e.moduleReference)&&i.isStringLiteralLike(e.moduleReference.expression))return e.moduleReference.expression.text;if(i.isImportTypeNode(e)){const s=e.argument;if(i.isLiteralTypeNode(s)&&i.isStringLiteralLike(s.literal))return s.literal.text}if(!i.isCallExpression(e)||!F(e,t))return null;const r=e.arguments[0];return i.isStringLiteralLike(r)?r.text:null}function Se(e){const t=a();return t.isVariableStatement(e)?e.declarationList.declarations.reduce((i,r)=>i+R(r.name,"defineFile"),0):(t.isClassDeclaration(e)||t.isEnumDeclaration(e)||t.isFunctionDeclaration(e))&&e.name?.text==="defineFile"?1:0}function R(e,t){const i=a();return i.isIdentifier(e)?e.text===t?1:0:e.elements.reduce((r,s)=>i.isOmittedExpression(s)?r:r+R(s.name,t),0)}function Ee(e,t){const i=E.resolve(e);if(E.isAbsolute(t))throw new Error(`Test file metadata path must be product-relative: ${t}`);const r=E.resolve(i,t);if(!h(i,r))throw new Error(`Test file metadata path escapes the product root: ${t}`);if(!d.existsSync(r))throw new Error(`Test file metadata source does not exist: ${t}`);const s=d.realpathSync(i),n=d.realpathSync(r);if(!h(s,n))throw new Error(`Test file metadata path resolves outside the product root: ${t}`);return n}function Ie(e){return e.parseDiagnostics??[]}function ke(e){const t=B.find(i=>e.endsWith(`.${i}.testkit.ts`));if(t)return t;throw new Error(`Cannot read Testkit file metadata from noncanonical path: ${e}`)}function c(e,t,i){return new Error(`Invalid testkit metadata in "${e}": ${t}.`,{cause:i})}function $(e){return e instanceof Error?e.message.replace(/\.$/u,""):String(e)}
|
|
1
|
+
import{isWorkObserved as M,recordWork as L}from"../../kernel/work-metrics.js";import{isPathWithin as h}from"../../process/files/paths.js";import d from"fs";import E from"path";import{createRequire as A}from"node:module";import{fileURLToPath as O}from"node:url";import{ENGINE_IDS as j,PUBLIC_TEST_TYPES as B,isEngineId as K}from"../../domain/identifiers.js";import{normalizeRuntimeIsolationPolicy as V}from"../../domain/runtime-isolation-contract.js";import{resolveEngineProfile as _,expectedEngineAuthoringModule as W,invalidEngineAuthoringImport as z,requireMatchingHostImport as U}from"../../engines/catalog.js";const G=A(import.meta.url),J=O(import.meta.resolve("typescript"));let H;function a(){return H??=G(J)}const g="@gfdelarue/testkit",X="@gfdelarue/testkit-engine-";export function loadTestFileMetadataMap(e,t={},i=new Map){for(const r of Object.values(t||{}))for(const s of Object.values(r||{}))for(const n of s||[])for(const o of n.files||[])i.has(o)||i.set(o,readTestFileMetadata(e,o));return i}export function readTestFileMetadata(e,t){const i=a(),r=Ee(e,t),s=ke(t);let n;if(M()){const m=d.readFileSync(r);L("metadata.filesRead",1),L("metadata.bytesRead",m.byteLength),n=m.toString("utf8")}else n=d.readFileSync(r,"utf8");const o=i.createSourceFile(r,n,i.ScriptTarget.Latest,!0,i.ScriptKind.TS),u=Ie(o);if(u.length>0){const m=u.map(f=>i.flattenDiagnosticMessageText(f.messageText," ")).join("; ");throw c(t,`TypeScript syntax is invalid: ${m}`)}let l=null,p=null;for(const m of o.statements)if(!(!i.isVariableStatement(m)||!ie(m))){for(const f of m.declarationList.declarations)if(!(!i.isIdentifier(f.name)||f.name.text!=="testkit")){if(l)throw c(t,'exported "testkit" metadata must be declared exactly once');if(!re(m))throw c(t,'exported "testkit" metadata must be const');l=f,p=m}}if(!l||!p)throw c(t,'export const testkit = defineFile({ engine: "..." }) is required');se(o,p,t);const S=Y(l.initializer,t,s);return requireMatchingEngineAuthoringImports(o,t,S),S}function Y(e,t,i){const r=a();if(!e)throw c(t,'exported "testkit" metadata has no value');if(!r.isCallExpression(e))throw c(t,"testkit metadata must use defineFile({ ... })");if(te(e.expression)!=="defineFile")throw c(t,"testkit metadata must call defineFile");if(e.arguments.length!==1)throw c(t,"defineFile metadata must receive exactly one object literal");const s=e.arguments[0];if(!r.isObjectLiteralExpression(s))throw c(t,"defineFile metadata must receive an object literal");return Q(s,t,i)}function Q(e,t,i){const r=a(),s={locks:[],runtimeIsolation:null,skipReason:null};let n=null;const o=new Set;for(const l of e.properties){if(r.isSpreadAssignment(l))throw c(t,"testkit metadata may not contain spread properties");if(!r.isPropertyAssignment(l))throw c(t,"testkit metadata properties must use key: value assignments");const p=b(l.name,t,"testkit metadata");if(!ne(p))throw c(t,`unknown metadata property "${p}"`);if(o.has(p))throw c(t,`duplicate metadata property "${p}"`);if(o.add(p),p==="engine"){n=I(l.initializer,t,'metadata property "engine"');continue}if(p==="locks"){s.locks=Z(l.initializer,t);continue}if(p==="skip"){s.skipReason=P(l.initializer,t);continue}s.runtimeIsolation=ee(l.initializer,t)}if(!n)throw c(t,'a literal "engine" property is required');if(!K(n))throw c(t,`unsupported engine "${n}"; expected one of: ${j.join(", ")}`);let u;try{u=_(i,n)}catch(l){throw c(t,$(l),l)}return{engineId:n,locks:s.locks,profileId:u.id,runtimeIsolation:s.runtimeIsolation,skipReason:s.skipReason}}function Z(e,t){const i=a();if(!i.isArrayLiteralExpression(e))throw c(t,'metadata property "locks" must be an array');const r=[];for(const s of e.elements){if(i.isSpreadElement(s)||i.isOmittedExpression(s))throw c(t,'metadata property "locks" must contain only string literals');r.push(T(s,t,'metadata property "locks" entry'))}return[...new Set(r)].sort()}function P(e,t){const i=a(),r=x(e);if(r!==null)return D(r,t,'metadata property "skip"').trim();if(!i.isObjectLiteralExpression(e))throw c(t,'metadata property "skip" must be a string literal or { reason: string literal }');if(e.properties.length!==1)throw c(t,'metadata property "skip" object must contain exactly one reason property');const s=e.properties[0];if(!i.isPropertyAssignment(s))throw c(t,'metadata property "skip" reason must use a string-literal assignment');if(b(s.name,t,'metadata property "skip"')!=="reason")throw c(t,'metadata property "skip" object only accepts "reason"');return T(s.initializer,t,'metadata property "skip.reason"')}function ee(e,t){const i=I(e,t,'metadata property "runtimeIsolation"');try{return V(i,"defineFile runtimeIsolation")}catch(r){throw c(t,$(r),r)}}function te(e){return a().isIdentifier(e)?e.text:null}function b(e,t,i){const r=a();if(r.isIdentifier(e)||r.isStringLiteral(e))return e.text;throw c(t,`${i} property names must be plain identifiers or string literals`)}function x(e){const t=a();return t.isStringLiteral(e)||t.isNoSubstitutionTemplateLiteral(e)?e.text:null}function I(e,t,i){const r=x(e);if(r===null)throw c(t,`${i} must be a string literal`);return D(r,t,i)}function T(e,t,i){return I(e,t,i).trim()}function D(e,t,i){if(e.trim().length===0)throw c(t,`${i} must not be empty`);return e}function ie(e){const t=a();return(e.modifiers||[]).some(i=>i.kind===t.SyntaxKind.ExportKeyword)}function re(e){const t=a();return(e.declarationList.flags&t.NodeFlags.Const)!==0}function ne(e){return["engine","locks","runtimeIsolation","skip"].includes(e)}function se(e,t,i){const r=a();let s=null,n=0;for(const o of e.statements){if(r.isImportDeclaration(o)){const u=o.importClause;if(!u||u.phaseModifier===r.SyntaxKind.TypeKeyword)continue;u.name?.text==="defineFile"&&(n+=1);const l=u.namedBindings;if(l&&r.isNamespaceImport(l)){l.name.text==="defineFile"&&(n+=1);continue}if(!l||!r.isNamedImports(l))continue;for(const p of l.elements){if(p.isTypeOnly||p.name.text!=="defineFile")continue;if(n+=1,(p.propertyName?.text??p.name.text)==="defineFile"&&r.isStringLiteral(o.moduleSpecifier)&&o.moduleSpecifier.text===g){if(s)throw c(i,`defineFile must have exactly one named value import from "${g}"`);s=o}}continue}n+=Se(o)}if(!s)throw c(i,`defineFile must be a named value import from "${g}"`);if(n!==1)throw c(i,"the imported defineFile binding may not be shadowed or redeclared");if(s.getStart(e)>t.getStart(e))throw c(i,"the defineFile import must precede metadata")}export function requireMatchingEngineAuthoringImports(e,t,i){oe(e,t,i);const r=W(i.profileId);for(const s of collectModuleSpecifiers(e))if(U(t,i,s),!(s===g||s===r||!s.startsWith(`${g}/`)&&!s.startsWith(X)))throw z(t,i,s,r)}function oe(e,t,i){const r=a(),s=N(e),n=o=>{if(ae(o,t,i),ce(o,s,t,i),r.isCallExpression(o)&&F(o,s)){const u=o.arguments.at(0);if(!u||!r.isStringLiteralLike(u)){const l=o.expression.kind===r.SyntaxKind.ImportKeyword?"import()":"require()";throw new Error(`Invalid Testkit authoring in "${t}": ${i.engineId}/${i.profileId} may not use non-literal ${l}; module specifiers must be string literals so engine boundaries can be verified`)}}for(const u of C(o))n(u);r.forEachChild(o,n)};n(e)}function F(e,t){const i=a();return e.expression.kind===i.SyntaxKind.ImportKeyword||i.isIdentifier(e.expression)&&e.expression.text==="require"&&!k(e.expression,t)}function ae(e,t,i){const r=a();if(r.isImportDeclaration(e)){const n=e.importClause?.namedBindings;if(n&&r.isNamedImports(n)&&n.elements.find(u=>!u.isTypeOnly&&(u.propertyName?.text??u.name.text)==="createRequire"))throw y(t,i,"createRequire")}if(r.isPropertyAccessExpression(e)&&e.name.text==="createRequire"||r.isElementAccessExpression(e)&&x(e.argumentExpression)==="createRequire"||r.isBindingElement(e)&&(me(e)==="createRequire"||r.isIdentifier(e.name)&&e.name.text==="createRequire"))throw y(t,i,"createRequire")}function ce(e,t,i,r){const s=a();if(s.isPropertyAccessExpression(e)&&e.name.text==="require"&&q(e.expression,t)||s.isElementAccessExpression(e)&&x(e.argumentExpression)==="require"&&q(e.expression,t))throw y(i,r,e.getText());if(s.isIdentifier(e)&&e.text==="require"&&!k(e,t)&&!ue(e)&&!le(e))throw y(i,r,"global require")}function y(e,t,i){return new Error(`Invalid Testkit authoring in "${e}": ${t.engineId}/${t.profileId} may not alias or indirectly access the module loader through ${i}; use literal import and require specifiers so engine boundaries can be verified`)}function q(e,t){return a().isIdentifier(e)&&(e.text==="globalThis"||e.text==="module")&&!k(e,t)}function ue(e){return a().isCallExpression(e.parent)&&e.parent.expression===e}function le(e){const t=a(),i=e.parent;return pe(e)||t.isPropertyAccessExpression(i)&&i.name===e||t.isPropertyAssignment(i)&&i.name===e||t.isImportSpecifier(i)&&i.propertyName===e||t.isExportSpecifier(i)&&i.parent.parent.moduleSpecifier!==void 0}function pe(e){const t=a(),i=e.parent;return i.name===e&&!t.isShorthandPropertyAssignment(i)&&!t.isExportSpecifier(i)}function me(e){const t=a(),i=e.propertyName;return i&&(t.isIdentifier(i)||t.isStringLiteralLike(i))?i.text:null}function N(e){const t=a(),i=new WeakMap,r=(n,o)=>{let u=i.get(n);u||(u=new Set,i.set(n,u)),v(u,o)},s=n=>{if(t.isImportDeclaration(n))fe(n,e,r);else if(t.isImportEqualsDeclaration(n)&&!n.isTypeOnly)r(e,n.name);else if(t.isVariableDeclaration(n)){const o=n.parent,u=t.isVariableDeclarationList(o)&&(o.flags&t.NodeFlags.BlockScoped)===0?de(n):w(n);r(u,n.name)}else t.isParameter(n)?r(ge(n),n.name):t.isFunctionDeclaration(n)&&n.name?r(w(n),n.name):t.isFunctionExpression(n)&&n.name?r(n,n.name):(t.isClassDeclaration(n)||t.isEnumDeclaration(n)||t.isModuleDeclaration(n))&&n.name&&t.isIdentifier(n.name)?r(w(n),n.name):t.isClassExpression(n)&&n.name?r(n,n.name):t.isCatchClause(n)&&n.variableDeclaration&&r(n,n.variableDeclaration.name);t.forEachChild(n,s)};return s(e),i}function fe(e,t,i){const r=a(),s=e.importClause;if(!s||s.phaseModifier===r.SyntaxKind.TypeKeyword)return;s.name&&i(t,s.name);const n=s.namedBindings;if(n&&r.isNamespaceImport(n))i(t,n.name);else if(n&&r.isNamedImports(n))for(const o of n.elements)o.isTypeOnly||i(t,o.name)}function v(e,t){const i=a();if(i.isIdentifier(t)){e.add(t.text);return}for(const r of t.elements)i.isOmittedExpression(r)||v(e,r.name)}function k(e,t){const i=a();let r=e;for(;;){if(t.get(r)?.has(e.text))return!0;if(i.isSourceFile(r))return!1;r=r.parent}}function w(e){let t=e.parent;for(;;){if(ye(t))return t;t=t.parent}}function de(e){const t=a();let i=e.parent;for(;;){if(t.isFunctionLike(i)||t.isSourceFile(i))return i;i=i.parent}}function ge(e){const t=a();let i=e.parent;for(;;){if(t.isFunctionLike(i)||t.isSourceFile(i))return i;i=i.parent}}function ye(e){const t=a();return t.isSourceFile(e)||t.isBlock(e)||t.isCaseBlock(e)||t.isCatchClause(e)||t.isForStatement(e)||t.isForInStatement(e)||t.isForOfStatement(e)||t.isFunctionLike(e)||t.isClassStaticBlockDeclaration(e)}export function collectModuleSpecifiers(e){const t=a(),i=new Set,r=N(e),s=n=>{const o=xe(n,r);o&&i.add(o);for(const u of C(n))s(u);t.forEachChild(n,s)};return s(e),i}function C(e){return e.jsDoc??[]}function xe(e,t){const i=a();if(i.isJSDocImportTag(e)&&i.isStringLiteralLike(e.moduleSpecifier)||(i.isImportDeclaration(e)||i.isExportDeclaration(e))&&e.moduleSpecifier&&i.isStringLiteralLike(e.moduleSpecifier))return e.moduleSpecifier.text;if(i.isImportEqualsDeclaration(e)&&i.isExternalModuleReference(e.moduleReference)&&i.isStringLiteralLike(e.moduleReference.expression))return e.moduleReference.expression.text;if(i.isImportTypeNode(e)){const s=e.argument;if(i.isLiteralTypeNode(s)&&i.isStringLiteralLike(s.literal))return s.literal.text}if(!i.isCallExpression(e)||!F(e,t))return null;const r=e.arguments[0];return i.isStringLiteralLike(r)?r.text:null}function Se(e){const t=a();return t.isVariableStatement(e)?e.declarationList.declarations.reduce((i,r)=>i+R(r.name,"defineFile"),0):(t.isClassDeclaration(e)||t.isEnumDeclaration(e)||t.isFunctionDeclaration(e))&&e.name?.text==="defineFile"?1:0}function R(e,t){const i=a();return i.isIdentifier(e)?e.text===t?1:0:e.elements.reduce((r,s)=>i.isOmittedExpression(s)?r:r+R(s.name,t),0)}function Ee(e,t){const i=E.resolve(e);if(E.isAbsolute(t))throw new Error(`Test file metadata path must be product-relative: ${t}`);const r=E.resolve(i,t);if(!h(i,r))throw new Error(`Test file metadata path escapes the product root: ${t}`);if(!d.existsSync(r))throw new Error(`Test file metadata source does not exist: ${t}`);const s=d.realpathSync(i),n=d.realpathSync(r);if(!h(s,n))throw new Error(`Test file metadata path resolves outside the product root: ${t}`);return n}function Ie(e){return e.parseDiagnostics??[]}function ke(e){const t=B.find(i=>e.endsWith(`.${i}.testkit.ts`));if(t)return t;throw new Error(`Cannot read Testkit file metadata from noncanonical path: ${e}`)}function c(e,t,i){return new Error(`Invalid testkit metadata in "${e}": ${t}.`,{cause:i})}function $(e){return e instanceof Error?e.message.replace(/\.$/u,""):String(e)}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export declare const ROOT_GROUP = "root";
|
|
2
|
+
export declare const LEGACY_OWNERSHIP_NOTICE = "testkit.config.ts has services but no groups, so each service owns the tests under its directory as a group of the same name. Legacy ownership will be removed; declare groups with roots and uses instead.";
|
|
3
|
+
export type TestGroupSource = "groups" | "implicit" | "services";
|
|
4
|
+
export interface TestGroup {
|
|
5
|
+
name: string;
|
|
6
|
+
roots: string[];
|
|
7
|
+
source: TestGroupSource;
|
|
8
|
+
uses: string[];
|
|
9
|
+
}
|
|
10
|
+
export interface GroupCandidate {
|
|
11
|
+
kind: "plain" | "typed";
|
|
12
|
+
path: string;
|
|
13
|
+
}
|
|
14
|
+
export type GroupAssignment = {
|
|
15
|
+
group: string;
|
|
16
|
+
path: string;
|
|
17
|
+
relativePath: string;
|
|
18
|
+
status: "owned";
|
|
19
|
+
} | {
|
|
20
|
+
groups: string[];
|
|
21
|
+
path: string;
|
|
22
|
+
status: "ambiguous";
|
|
23
|
+
} | {
|
|
24
|
+
path: string;
|
|
25
|
+
status: "excluded";
|
|
26
|
+
};
|
|
27
|
+
export interface GroupResolution {
|
|
28
|
+
assignments: GroupAssignment[];
|
|
29
|
+
groups: TestGroup[];
|
|
30
|
+
notice: null | string;
|
|
31
|
+
problems: string[];
|
|
32
|
+
}
|
|
33
|
+
export declare function resolveGroups(config: null | object | undefined, candidates: readonly GroupCandidate[]): GroupResolution;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import p from"node:path";import{requireServiceName as O}from"../../domain/service-name.js";import{errorMessage as m}from"../../kernel/error-serialization.js";import{isUnknownRecord as y}from"../../kernel/unknown-values.js";import{normalizeDiscoveryConfig as S}from"./decode-discovery.js";import{DEFAULT_DISCOVERY_EXCLUDES as C,shouldExcludeDiscoveryPath as w}from"./path-policy.js";import{readProperty as f}from"./values.js";export const ROOT_GROUP="root",LEGACY_OWNERSHIP_NOTICE="testkit.config.ts has services but no groups, so each service owns the tests under its directory as a group of the same name. Legacy ownership will be removed; declare groups with roots and uses instead.";export function resolveGroups(e,t){const s=[],r=G(e,s),i=e&&Object.hasOwn(e,"groups")?b(f(e,"groups"),r,s):z(r,s),o=$(f(e,"discovery"),null,s),c=[...C,...(o.exclude??[]).map(h)],a=t.map(n=>A(n,i.rules,c)),l=new Set(i.groups.filter(n=>n.source==="services").map(n=>n.name));return{assignments:a,groups:[...i.groups,{name:ROOT_GROUP,roots:["."],source:"implicit",uses:[]}],notice:a.some(n=>n.status==="owned"&&l.has(n.group))?LEGACY_OWNERSHIP_NOTICE:null,problems:s}}function A(e,t,s){const r=L(e.path);if(w(r,s))return{path:r,status:"excluded"};const i=t.filter(u=>u.prefix===null||v(r,u.prefix)),o=i.filter(u=>!w(r,u.exclude));if(i.length>0&&o.length===0)return{path:r,status:"excluded"};const c=o.filter(u=>e.kind==="typed"||!u.typedOnly);if(c.length===0)return{group:ROOT_GROUP,path:r,relativePath:r,status:"owned"};const a=Math.max(...c.map(u=>u.depth)),l=c.filter(u=>u.depth===a),n=[...new Set(l.map(u=>u.group))].sort((u,d)=>u.localeCompare(d));return n.length>1?{groups:n,path:r,status:"ambiguous"}:{group:n[0],path:r,relativePath:R(l[0],r),status:"owned"}}function b(e,t,s){const r={groups:[],rules:[]};if(!y(e))return s.push("testkit.config.ts groups must be an object"),r;for(const[i,o]of Object.entries(e)){const c=`groups.${i}`;if(i===ROOT_GROUP){s.push(`${c} is reserved for the implicit group that owns files no other group claims; rename it`);continue}try{O(i,`Group name "${i}"`)}catch(n){s.push(m(n));continue}if(!y(o)){s.push(`${c} must be an object with roots`);continue}const a=x(o.roots,`${c}.roots`,!0,s),l=x(o.exclude,`${c}.exclude`,!1,s);r.groups.push({name:i,roots:a,source:"groups",uses:j(o.uses,c,t,s)});for(const n of a)r.rules.push({depth:n==="."?0:n.split("/").length,exclude:l,group:i,legacyCwd:null,prefix:n==="."?null:n,typedOnly:!1})}return r}function x(e,t,s,r){if(e===void 0&&!s)return[];if(!Array.isArray(e)||s&&e.length===0)return r.push(`${t} must be ${s?"a non-empty array":"an array"} of repo-relative paths`),[];const i=e,o=new Set;for(const c of i){const a=P(c);a===null?r.push(`${t} entry ${JSON.stringify(c)} must be a relative path inside the product directory`):o.add(a)}return[...o]}function j(e,t,s,r){if(e===void 0)return[];if(!D(e))return r.push(`${t}.uses must be an array of service names`),[];const i=[];for(const o of new Set(e))Object.hasOwn(s,o)?i.push(o):r.push(`${t}.uses names "${o}", which is not a service; declare services.${o} or remove it from ${t}.uses`);return i}function z(e,t){const s={groups:[],rules:[]};for(const[r,i]of Object.entries(e)){if(r===ROOT_GROUP){t.push("services.root is reserved for the implicit group that owns files no other group claims; rename the service");continue}const o=E(i,r,t),c=$(f(i,"discovery"),r,t),a=c.roots&&c.roots.length>0?c.roots:["."],l=(c.exclude??[]).map(u=>h(o==="."?u:p.posix.join(o,u))),n=a.map(u=>{const d=h(u),g=o==="."?d:d==="."?o:h(p.posix.join(o,d));return{depth:g==="."?0:g.split("/").filter(Boolean).length,exclude:l,group:r,legacyCwd:o==="."?null:o,prefix:g==="."?null:g,typedOnly:!0}});s.rules.push(...n),s.groups.push({name:r,roots:[...new Set(n.map(u=>u.prefix??"."))],source:"services",uses:[r]})}return s}function E(e,t,s){const r=f(f(e,"local"),"cwd");return r===void 0?".":typeof r!="string"?(s.push(`Service "${t}" local.cwd must be a string`),"."):h(r||".")}function G(e,t){const s=f(e,"services");return s==null?{}:y(s)?s:(t.push("testkit.config.ts services must be an object"),{})}function $(e,t,s){try{return S(e,{allowRoots:!0})}catch(r){return s.push(t===null?m(r):`Service "${t}" has invalid discovery config: ${m(r)}`),{}}}function R(e,t){return e.prefix!==null&&v(t,e.prefix)?p.posix.relative(e.prefix,t):e.legacyCwd!==null&&v(t,e.legacyCwd)?p.posix.relative(e.legacyCwd,t):t}function D(e){return Array.isArray(e)&&e.every(t=>typeof t=="string")}function v(e,t){return e===t||e.startsWith(`${t}/`)}function P(e){if(typeof e!="string")return null;const t=e.trim(),s=t.replaceAll("\\","/");if(s===""||p.posix.isAbsolute(s)||p.win32.isAbsolute(t))return null;const r=p.posix.normalize(s).replace(/\/+$/u,"")||".";return r===".."||r.startsWith("../")?null:r}function L(e){return p.posix.normalize(e.replaceAll("\\","/")).replace(/\/+$/u,"")}function h(e){const t=e.split(p.sep).join("/");return t==="."||t==="./"?".":t.replace(/^\.\/+/u,"").replace(/\/+$/u,"")||"."}
|
|
@@ -25,7 +25,6 @@ export interface NormalizedLocalServiceConfig {
|
|
|
25
25
|
baseUrl: string;
|
|
26
26
|
cwd: string;
|
|
27
27
|
env: Record<string, string>;
|
|
28
|
-
port?: number;
|
|
29
28
|
readyTimeoutMs?: number;
|
|
30
29
|
shutdownTimeoutMs?: number;
|
|
31
30
|
readyUrl?: false | null | string;
|
|
@@ -70,7 +69,6 @@ export interface LoadConfigContextOptions {
|
|
|
70
69
|
configImportCacheSalt?: string;
|
|
71
70
|
dir?: null | string;
|
|
72
71
|
discoveryOptions?: Omit<DiscoveryOptions, "discovery">;
|
|
73
|
-
requireLocalRuntime?: boolean;
|
|
74
72
|
}
|
|
75
73
|
export interface LoadConfigsOptions extends LoadConfigContextOptions {
|
|
76
74
|
service?: null | string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{selectDiscoveredSuites as K,discoveredRuleSuites as G}from"./test-selection.js";import H from"path";import{buildEngineConfigurations as J}from"../../engines/catalog.js";import{isPublicTestType as Q}from"../../domain/identifiers.js";import{discoverProject as V}from"./discovery.js";import{loadTestkitConfig as W}from"./config-loader.js";import{normalizeToolchainRegistry as X}from"../../toolchains/index.js";import{loadTestFileMetadataMap as Y}from"./file-metadata.js";import{mergeDiscoveryConfigs as Z}from"./path-policy.js";import{normalizeDiscoveryConfig as T}from"./decode-discovery.js";import{normalizeFingerprintPolicy as _}from"../../repo/fingerprint-policy.js";import{normalizeDatabaseConfig as N}from"./database.js";import{inferEnvFiles as ee,loadServiceEnv as re,parseDotenv as te}from"./env.js";import{normalizeRuntimeIsolationConfig as oe}from"./runtime-isolation.js";import{resolveProductDir as k,resolveServiceCwd as ne}from"./paths.js";import{inferLocalRuntime as ie,normalizeRepoExecution as se,normalizeRuntimeConfig as ce}from"./runtime.js";import{normalizeServiceRequirements as ae,normalizeSkipConfig as fe}from"./skip-config.js";import{normalizeArtifactArchiveConfig as de}from"../../evidence/index.js";import{validateConfigCoverage as le,validateServiceConfig as ue}from"./validation.js";import{isObjectRecord as w,readProperty as i}from"./values.js";import{assertPortableServiceNameSet as me,requireServiceName as ge}from"../../domain/service-name.js";import{resolveEngineProfile as pe}from"../../engines/catalog.js";export{te as parseDotenv,k as resolveProductDir,ne as resolveServiceCwd};export async function loadConfigContext(e={}){const r=k(process.cwd(),e.dir),t=e.requireLocalRuntime!==!1,o=e.configContext||await W(r,{importCacheSalt:e.configImportCacheSalt??""}),{config:n,configFile:c}=Ee(o),d=se(Me(i(n,"execution"))),l=de(i(n,"artifacts")),a=X(i(n,"toolchains")),s=_(i(n,"fingerprints")),g=T(i(n,"discovery"),{allowRoots:!0}),f=ve(i(n,"services")),u=e.preparedDiscovery?.project??V(r,$e(f),{...e.discoveryOptions||{},discovery:g}),v=e.testSelection?K(u.suitesByService,e.testSelection):u.suitesByService,$=Y(r,v,e.preparedDiscovery?.fileMetadataByPath),y=we(v,$),b=new Set([...Object.keys(f),...Object.keys(y)]),p=[];for(const m of[...b].sort((C,E)=>C.localeCompare(E)))p.push(await be({name:m,productDir:r,config:n,configFile:c,execution:d,discovery:g,artifacts:l,toolchains:a,fingerprints:s,explicitService:f[m]||{},discoveredService:u.services[m]||null,suites:y[m]||{},discoveredSuites:u.suitesByService[m]||{},fileMetadataByPath:$,requireLocalRuntime:t}));return le(p),{productDir:r,config:n,configFile:c,execution:d,toolchains:a,fingerprints:s,explicitServices:f,discovery:u,configs:p}}function we(e,r){return Object.fromEntries(Object.entries(e).map(([t,o])=>[t,ye(t,o||{},r)]))}function ye(e,r,t){const o={};for(const[n,c]of Object.entries(r)){if(!Q(n))throw new Error(`Discovered service "${e}" contains unknown test type "${n}"`);o[n]=c.map(d=>{const l=d.files[0];if(!l)throw new Error(`Discovered suite ${e}/${n}:${d.name} contains no files`);const a=P(t,l),s=pe(n,a.engineId);if(s.id!==a.profileId)throw new Error(`Discovered file ${l} resolved conflicting engine profile ${a.profileId}; expected ${s.id}`);for(const g of d.files.slice(1)){const f=P(t,g);if(f.engineId!==a.engineId||f.profileId!==a.profileId)throw new Error(`Discovered suite ${e}/${n}:${d.name} mixes engine profiles ${a.engineId}/${a.profileId} and ${f.engineId}/${f.profileId}`)}return{...d,testType:n,engineId:a.engineId,profileId:a.profileId}})}return o}function P(e,r){const t=e.get(r);if(!t)throw new Error(`Missing validated testkit metadata for discovered file: ${r}`);return t}export async function loadConfigs(e={}){const r=await loadConfigContext(e),{configs:t}=r,o=e.service?t.filter(n=>n.name===e.service):t;if(e.service&&o.length===0){const n=t.map(c=>c.name).join(", ");throw new Error(`Service "${e.service}" not found. Available: ${n}`)}return o}async function be({name:e,productDir:r,config:t,configFile:o,execution:n,discovery:c,artifacts:d,toolchains:l,fingerprints:a,explicitService:s,discoveredService:g,suites:f,discoveredSuites:u,fileMetadataByPath:v,requireLocalRuntime:$}){const y=await J(t,new Set(Object.values(f).flatMap(h=>h.map(O=>O.engineId)))),b=he(e,s,g,r),p=ee(r,s,b),m={...re(r,p),...F(i(s,"env"),`Service "${e}" env`)},C=Z(c,D(i(s,"discovery"),e));if(i(s,"migrate")||i(s,"seed"))throw new Error(`Service "${e}" uses removed migrate/seed hooks. Move template lifecycle to database.template.{migrate,seed,verify}.`);const E=Se(s,e),z=ce(i(s,"runtime"),e,l),j=G(u),q=fe(i(s,"skip"),{name:e,suites:j}),x=ae(i(s,"requirements"),{name:e,suites:j}),U=oe(i(s,"runtimeIsolation"),{name:e,suites:j}),A=new Set(Object.values(f).flatMap(h=>h.flatMap(O=>O.files))),B=new Map([...v.entries()].filter(([h])=>A.has(h))),I=Ce(i(s,"dependsOn"),`Service "${e}" dependsOn`),M=S(i(s,"databaseFrom"),`Service "${e}" databaseFrom`);return ue({name:e,local:b,database:E,databaseFrom:M,runtime:z,dependsOn:I,suites:f,productDir:r,requireLocalRuntime:$}),{name:e,productDir:r,configFile:o,...y===void 0?{}:{engineConfigurations:y},stateDir:H.join(r,".testkit",e),artifacts:d,suites:f,testkit:{execution:n,dependsOn:I,discovery:C,database:E,databaseFrom:M,envFiles:p,requirements:x,serviceEnv:m,skip:q,runtime:z,runtimeIsolation:U,ui:i(t,"ui")||null,fingerprints:a,fileMetadataByPath:B,local:b}}}function he(e,r,t,o){if(Object.prototype.hasOwnProperty.call(r,"local"))return R(i(r,"local"),e);const n=t?.inferredLocalCwd||".",c=ie(o,n);if(c)return c}function R(e,r){if(e===!1)return;if(!w(e))throw new Error(`Service "${r}" local must be an object or false`);const t=L(e.baseUrl,`Service "${r}" local.baseUrl`),o=L(e.start,`Service "${r}" local.start`),{shutdownTimeoutMs:n,...c}=e;return{...c,...ze(n,r),baseUrl:t,start:o,cwd:S(e.cwd,`Service "${r}" local.cwd`)||".",env:F(e.env,`Service "${r}" local.env`),...je(e.port,`Service "${r}" local.port`),...Oe(e.readyTimeoutMs,`Service "${r}" local.readyTimeoutMs`),...Ie(e.readyUrl,r)}}function Se(e,r){const t=N(e,r);if(!t)return;const o=S(i(t,"image"),`Service "${r}" database.image`),n=S(i(t,"password"),`Service "${r}" database.password`),c=S(i(t,"user"),`Service "${r}" database.user`);return{...t,...Object.prototype.hasOwnProperty.call(t,"image")?{image:o}:{},...Object.prototype.hasOwnProperty.call(t,"password")?{password:n}:{},...Object.prototype.hasOwnProperty.call(t,"user")?{user:c}:{}}}function D(e,r){try{return T(e,{allowRoots:!0})}catch(t){const o=t instanceof Error?t.message:String(t);throw new Error(`Service "${r}" has invalid discovery config: ${o}`,{cause:t})}}function ve(e){if(e==null)return{};if(!w(e))throw new Error("testkit.config.ts services must be an object");const r=Object.fromEntries(Object.entries(e).map(([t,o])=>{if(ge(t,`Service name "${t}"`),o==null)return[t,{}];if(!w(o))throw new Error(`Service "${t}" config must be an object`);return[t,o]}));return me(Object.keys(r)),r}function $e(e){return Object.fromEntries(Object.entries(e).map(([r,t])=>{const o=t||{},n={discovery:D(i(o,"discovery"),r),local:Object.prototype.hasOwnProperty.call(o,"local")?R(i(o,"local"),r)||!1:void 0};return[r,n]}))}function Ee(e){if(!w(e))throw new Error("Loaded Testkit config context must be an object");const r=e.config;if(typeof r!="object"||r===null)throw new Error("Loaded Testkit config must be an object");const t=e.configFile;if(t!==null&&typeof t!="string")throw new Error("Loaded Testkit config file must be a string or null");return{config:r,configFile:t}}function Ce(e,r){if(e==null)return[];if(!Array.isArray(e))throw new Error(`${r} must be an array of strings`);return e.map((t,o)=>{if(typeof t!="string")throw new Error(`${r}[${String(o)}] must be a string`);return t})}function F(e,r){if(e==null)return{};if(!w(e))throw new Error(`${r} must be an object of strings`);return Object.fromEntries(Object.entries(e).map(([t,o])=>{if(typeof o!="string")throw new Error(`${r}.${t} must be a string`);return[t,o]}))}function je(e,r){if(e===void 0)return{};if(typeof e!="number")throw new Error(`${r} must be a number`);return{port:e}}function Oe(e,r){if(e===void 0)return{};if(typeof e!="number")throw new Error(`${r} must be a number`);return{readyTimeoutMs:e}}function ze(e,r){if(e===void 0)return{};if(typeof e!="number"||!Number.isInteger(e)||e<=0)throw new Error(`Service "${r}" local.shutdownTimeoutMs must be a positive finite integer in milliseconds`);return{shutdownTimeoutMs:e}}function Ie(e,r){if(e===void 0)return{};if(e===!1||e===null||typeof e=="string")return{readyUrl:e};throw new Error(`Service "${r}" local.readyUrl must be a string, false, or null`)}function S(e,r){if(e!==void 0){if(typeof e!="string")throw new Error(`${r} must be a string`);return e}}function L(e,r){if(typeof e!="string")throw new Error(`${r} must be a string`);return e}function Me(e){if(e){if(!w(e))throw new Error("testkit.config.ts execution must be an object");return e}}
|
|
1
|
+
import{selectDiscoveredSuites as J,discoveredRuleSuites as Q}from"./test-selection.js";import V from"path";import{isUnknownRecord as w}from"../../kernel/unknown-values.js";import{buildEngineConfigurations as W}from"../../engines/catalog.js";import{isPublicTestType as X}from"../../domain/identifiers.js";import{discoverProject as Y}from"./discovery.js";import{loadTestkitConfig as Z}from"./config-loader.js";import{normalizeToolchainRegistry as N}from"../../toolchains/index.js";import{loadTestFileMetadataMap as ee}from"./file-metadata.js";import{mergeDiscoveryConfigs as re}from"./path-policy.js";import{normalizeDiscoveryConfig as I}from"./decode-discovery.js";import{normalizeFingerprintPolicy as te}from"../../repo/fingerprint-policy.js";import{normalizeDatabaseConfig as oe}from"./database.js";import{inferEnvFiles as ie,loadServiceEnv as ne,parseDotenv as se}from"./env.js";import{normalizeRuntimeIsolationConfig as ce}from"./runtime-isolation.js";import{resolveProductDir as U,resolveServiceCwd as ae}from"./paths.js";import{normalizeRepoExecution as fe,normalizeRuntimeConfig as le}from"./runtime.js";import{normalizeServiceRequirements as de,normalizeSkipConfig as ue}from"./skip-config.js";import{normalizeArtifactArchiveConfig as pe}from"../../evidence/index.js";import{validateConfigCoverage as me,validateServiceConfig as ge}from"./validation.js";import{readProperty as n}from"./values.js";import{assertPortableServiceNameSet as we,requireServiceName as ye}from"../../domain/service-name.js";import{resolveEngineProfile as he}from"../../engines/catalog.js";const k=["65533","65534"],be=/\{(?:baseUrl|readyUrl|publicBaseUrl|privateBaseUrl|serviceContextUrl)(?::[^}]+)?\}/gu;export{se as parseDotenv,U as resolveProductDir,ae as resolveServiceCwd};export async function loadConfigContext(e={}){const r=U(process.cwd(),e.dir),t=e.configContext||await Z(r,{importCacheSalt:e.configImportCacheSalt??""}),{config:o,configFile:i}=xe(t),c=fe(ke(n(o,"execution"))),f=pe(n(o,"artifacts")),l=N(n(o,"toolchains")),a=te(n(o,"fingerprints")),p=I(n(o,"discovery"),{allowRoots:!0}),d=Ce(n(o,"services")),u=e.preparedDiscovery?.project??Y(r,je(d),{...e.discoveryOptions||{},discovery:p}),v=e.testSelection?J(u.suitesByService,e.testSelection):u.suitesByService,y=ee(r,v,e.preparedDiscovery?.fileMetadataByPath),s=Se(v,y),h=new Set([...Object.keys(d),...Object.keys(s)]),m=[];for(const g of[...h].sort((E,$)=>E.localeCompare($)))m.push(await $e({name:g,productDir:r,config:o,configFile:i,execution:c,discovery:p,artifacts:f,toolchains:l,fingerprints:a,explicitService:d[g],suites:s[g]||{},discoveredSuites:u.suitesByService[g]||{},fileMetadataByPath:y}));return me(m),{productDir:r,config:o,configFile:i,execution:c,toolchains:l,fingerprints:a,explicitServices:d,discovery:u,configs:m}}function Se(e,r){return Object.fromEntries(Object.entries(e).map(([t,o])=>[t,ve(t,o||{},r)]))}function ve(e,r,t){const o={};for(const[i,c]of Object.entries(r)){if(!X(i))throw new Error(`Discovered service "${e}" contains unknown test type "${i}"`);o[i]=c.map(f=>{const l=f.files[0];if(!l)throw new Error(`Discovered suite ${e}/${i}:${f.name} contains no files`);const a=P(t,l),p=he(i,a.engineId);if(p.id!==a.profileId)throw new Error(`Discovered file ${l} resolved conflicting engine profile ${a.profileId}; expected ${p.id}`);for(const d of f.files.slice(1)){const u=P(t,d);if(u.engineId!==a.engineId||u.profileId!==a.profileId)throw new Error(`Discovered suite ${e}/${i}:${f.name} mixes engine profiles ${a.engineId}/${a.profileId} and ${u.engineId}/${u.profileId}`)}return{...f,testType:i,engineId:a.engineId,profileId:a.profileId}})}return o}function P(e,r){const t=e.get(r);if(!t)throw new Error(`Missing validated testkit metadata for discovered file: ${r}`);return t}export async function loadConfigs(e={}){const r=await loadConfigContext(e),{configs:t}=r,o=e.service?t.filter(i=>i.name===e.service):t;if(e.service&&o.length===0){const i=t.map(c=>c.name).join(", ");throw new Error(`Service "${e.service}" not found. Available: ${i}`)}return o}async function $e({name:e,productDir:r,config:t,configFile:o,execution:i,discovery:c,artifacts:f,toolchains:l,fingerprints:a,explicitService:p,suites:d,discoveredSuites:u,fileMetadataByPath:v}){const y=await W(t,new Set(Object.values(d).flatMap(b=>b.map(C=>C.engineId)))),s=p??{},h=Object.hasOwn(s,"local")?M(n(s,"local"),e):void 0,m=p===void 0?[]:ie(r,s,h),g={...ne(r,m),...A(n(s,"env"),`Service "${e}" env`)},E=re(c,L(n(s,"discovery"),e));if(n(s,"migrate")||n(s,"seed"))throw new Error(`Service "${e}" uses removed migrate/seed hooks. Move template lifecycle to database.template.{migrate,seed,verify}.`);const $=Oe(s,e),x=le(n(s,"runtime"),e,l),O=Q(u),q=ue(n(s,"skip"),{name:e,suites:O}),_=de(n(s,"requirements"),{name:e,suites:O}),K=ce(n(s,"runtimeIsolation"),{name:e,suites:O}),G=new Set(Object.values(d).flatMap(b=>b.flatMap(C=>C.files))),H=new Map([...v.entries()].filter(([b])=>G.has(b))),z=ze(n(s,"dependsOn"),`Service "${e}" dependsOn`),T=S(n(s,"databaseFrom"),`Service "${e}" databaseFrom`);return ge({name:e,local:h,database:$,databaseFrom:T,runtime:x,dependsOn:z,suites:d,productDir:r}),{name:e,productDir:r,configFile:o,...y===void 0?{}:{engineConfigurations:y},stateDir:V.join(r,".testkit",e),artifacts:f,suites:d,testkit:{execution:i,dependsOn:z,discovery:E,database:$,databaseFrom:T,envFiles:m,requirements:_,serviceEnv:g,skip:q,runtime:x,runtimeIsolation:K,ui:n(t,"ui")||null,fingerprints:a,fileMetadataByPath:H,local:h}}}function M(e,r){if(e===!1)return;if(!w(e))throw new Error(`Service "${r}" local must be an object or false`);if(Object.prototype.hasOwnProperty.call(e,"port"))throw new Error(`Service "${r}" sets local.port. Testkit assigns every service a free port from the OS. Delete the line, or the port option of app.node or app.next, and use {port} in start, baseUrl, and readyUrl.`);const t=B(e.baseUrl,`Service "${r}" local.baseUrl`);F(t,`Service "${r}" local.baseUrl`);const o=new RegExp(`\\{port(?::${Ee(r)})?\\}`,"gu");if(!k.every(l=>D(t,o,l,!1)===l))throw new Error(`Service "${r}" local.baseUrl must use {port} for its port, for example http://127.0.0.1:{port}. Testkit assigns every service a free port from the OS.`);const i=B(e.start,`Service "${r}" local.start`),{shutdownTimeoutMs:c,...f}=e;return{...f,...Ie(c,r),baseUrl:t,start:i,cwd:S(e.cwd,`Service "${r}" local.cwd`)||".",env:A(e.env,`Service "${r}" local.env`),...Te(e.readyTimeoutMs,`Service "${r}" local.readyTimeoutMs`),...Ue(e.readyUrl,r)}}function j(e){return e.replace(/[\t\n\r]/gu,"").trim()}function R(e){return/^[a-z][a-z0-9+.-]*:[\\/]{2}(?![\\/])([^/\\?#]*)/iu.exec(j(e))?.[1]}function D(e,r,t,o){const i=j(e).replace(r,t).replace(be,o?`http://template.invalid:${t}`:"http://template.invalid").replace(/\{[^}]+\}/gu,"template");try{return new URL(i).port}catch{return}}function Ee(e){return e.replace(/[.*+?^${}()|[\]\\]/gu,"\\$&")}function F(e,r){if(/^[a-z][a-z0-9+.-]*:/iu.test(j(e))&&R(e)===void 0)throw new Error(`${r} must be an absolute URL with a host, for example http://127.0.0.1:{port}/health.`);const[t,o]=k.map(c=>D(e,/\{port(?::[^}]+)?\}/gu,c,!0)),i=/:(\d+)$/u.exec(R(e)??"")?.[1]??(t&&t===o?t:void 0);if(i!==void 0)throw new Error(`${r} has a literal port (${i}). Testkit assigns every service a free port from the OS. Replace the port with {port}.`)}function Oe(e,r){const t=oe(e,r);if(!t)return;const o=S(n(t,"image"),`Service "${r}" database.image`),i=S(n(t,"password"),`Service "${r}" database.password`),c=S(n(t,"user"),`Service "${r}" database.user`);return{...t,...Object.prototype.hasOwnProperty.call(t,"image")?{image:o}:{},...Object.prototype.hasOwnProperty.call(t,"password")?{password:i}:{},...Object.prototype.hasOwnProperty.call(t,"user")?{user:c}:{}}}function L(e,r){try{return I(e,{allowRoots:!0})}catch(t){const o=t instanceof Error?t.message:String(t);throw new Error(`Service "${r}" has invalid discovery config: ${o}`,{cause:t})}}function Ce(e){if(e==null)return{};if(!w(e))throw new Error("testkit.config.ts services must be an object");const r=Object.fromEntries(Object.entries(e).map(([t,o])=>{if(ye(t,`Service name "${t}"`),o==null)return[t,{}];if(!w(o))throw new Error(`Service "${t}" config must be an object`);return[t,o]}));return we(Object.keys(r)),r}function je(e){return Object.fromEntries(Object.entries(e).map(([r,t])=>{const o=t||{},i={discovery:L(n(o,"discovery"),r),local:Object.prototype.hasOwnProperty.call(o,"local")?M(n(o,"local"),r)||!1:void 0};return[r,i]}))}function xe(e){if(!w(e))throw new Error("Loaded Testkit config context must be an object");const r=e.config;if(typeof r!="object"||r===null)throw new Error("Loaded Testkit config must be an object");const t=e.configFile;if(t!==null&&typeof t!="string")throw new Error("Loaded Testkit config file must be a string or null");return{config:r,configFile:t}}function ze(e,r){if(e==null)return[];if(!Array.isArray(e))throw new Error(`${r} must be an array of strings`);return e.map((t,o)=>{if(typeof t!="string")throw new Error(`${r}[${String(o)}] must be a string`);return t})}function A(e,r){if(e==null)return{};if(!w(e))throw new Error(`${r} must be an object of strings`);return Object.fromEntries(Object.entries(e).map(([t,o])=>{if(typeof o!="string")throw new Error(`${r}.${t} must be a string`);return[t,o]}))}function Te(e,r){if(e===void 0)return{};if(typeof e!="number")throw new Error(`${r} must be a number`);return{readyTimeoutMs:e}}function Ie(e,r){if(e===void 0)return{};if(typeof e!="number"||!Number.isInteger(e)||e<=0)throw new Error(`Service "${r}" local.shutdownTimeoutMs must be a positive finite integer in milliseconds`);return{shutdownTimeoutMs:e}}function Ue(e,r){if(e===void 0)return{};if(typeof e=="string")return F(e,`Service "${r}" local.readyUrl`),{readyUrl:e};if(e===!1||e===null)return{readyUrl:e};throw new Error(`Service "${r}" local.readyUrl must be a string, false, or null`)}function S(e,r){if(e!==void 0){if(typeof e!="string")throw new Error(`${r} must be a string`);return e}}function B(e,r){if(typeof e!="string")throw new Error(`${r} must be a string`);return e}function ke(e){if(e){if(!w(e))throw new Error("testkit.config.ts execution must be an object");return e}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{matchesSuiteSelectors as P,parseSuiteSelectors as v}from"../../domain/suite-selection.js";import{
|
|
1
|
+
import{matchesSuiteSelectors as P,parseSuiteSelectors as v}from"../../domain/suite-selection.js";import{isUnknownRecord as y}from"../../kernel/unknown-values.js";import{DEFAULT_RUNTIME_ISOLATION_POLICY as E,normalizeRuntimeIsolationPolicy as m}from"../../domain/runtime-isolation-contract.js";import{normalizePath as I}from"./paths.js";export{DEFAULT_RUNTIME_ISOLATION_POLICY,isRuntimeIsolationPolicy,normalizeRuntimeIsolationPolicy,RUNTIME_ISOLATION_POLICIES}from"../../domain/runtime-isolation-contract.js";const R=String;export function normalizeRuntimeIsolationConfig(e,{name:t,suites:d}){if(!e)return h();if(typeof e=="string")return{...h(),defaultPolicy:m(e,`Service "${t}" runtimeIsolation`)};if(!y(e))throw new Error(`Service "${t}" runtimeIsolation must be a policy string or object`);const l=[],f=new Set;for(const o of Object.values(d||{}))for(const r of o||[]){l.push({testType:r.testType,name:r.name});for(const s of r.files||[])f.add(I(s))}const c=e.default==null?E:m(e.default,`Service "${t}" runtimeIsolation.default`),u=[],a=new Set;for(const[o,r]of g(e.suites,`Service "${t}" runtimeIsolation.suites`).entries()){if(!y(r))throw new Error(`Service "${t}" runtimeIsolation.suites entries must be objects`);const s=r,i=R(s.selector||"").trim();if(!i)throw new Error(`Service "${t}" runtimeIsolation.suites[${o.toString()}] requires a selector`);const S=v([i]);if(S.length!==1)throw new Error(`Service "${t}" runtimeIsolation.suites["${i}"] is invalid`);const n=S[0];if(n.kind!=="typed")throw new Error(`Service "${t}" runtimeIsolation.suites["${i}"] must use a typed selector like ui:auth`);if(a.has(n.raw))throw new Error(`Service "${t}" defines duplicate runtimeIsolation.suites selector "${n.raw}"`);if(!l.some($=>P($.testType,$.name,[n])))throw new Error(`Service "${t}" runtimeIsolation.suites selector "${n.raw}" did not match any discovered suite`);a.add(n.raw),u.push({selector:n,policy:m(s.policy||s.runtimeIsolation,`Service "${t}" runtimeIsolation.suites["${n.raw}"].policy`)})}const p=[],w=new Set;for(const[o,r]of g(e.files,`Service "${t}" runtimeIsolation.files`).entries()){if(!y(r))throw new Error(`Service "${t}" runtimeIsolation.files entries must be objects`);const s=r,i=I(T(s.path,`Service "${t}" runtimeIsolation.files[${o.toString()}].path`));if(!i)throw new Error(`Service "${t}" runtimeIsolation.files[${o.toString()}] requires a path`);if(!f.has(i))throw new Error(`Service "${t}" runtimeIsolation.files["${i}"] did not match any discovered test file`);if(w.has(i))throw new Error(`Service "${t}" defines duplicate runtimeIsolation.files path "${i}"`);w.add(i),p.push({path:i,policy:m(s.policy||s.runtimeIsolation,`Service "${t}" runtimeIsolation.files["${i}"].policy`)})}return{defaultPolicy:c,suites:u,files:p,filePolicyByPath:new Map(p.map(o=>[o.path,o.policy]))}}export function resolveRuntimeIsolationPolicy(e,t,d){const l=I(d),f=e.testkit?.fileMetadataByPath?.get(l)||null,c=e.testkit?.runtimeIsolation||h(),u=f?.runtimeIsolation||c.filePolicyByPath.get(l);if(u)return u;for(const a of c.suites)if(P(t.testType,t.name,[a.selector]))return a.policy;return c.defaultPolicy}function h(){return{defaultPolicy:E,suites:[],files:[],filePolicyByPath:new Map}}function g(e,t){if(e==null)return[];if(!Array.isArray(e))throw new Error(`${t} must be an array`);return e}function T(e,t){if(e==null||typeof e=="string")return e;throw new Error(`${t} must be a string`)}
|
|
@@ -9,15 +9,6 @@ export interface NormalizedRuntimeConfig {
|
|
|
9
9
|
prepare: NormalizedBuildPrepareConfig;
|
|
10
10
|
toolchain: NormalizedRuntimeToolchain | null;
|
|
11
11
|
}
|
|
12
|
-
export interface InferredLocalRuntimeConfig {
|
|
13
|
-
baseUrl: string;
|
|
14
|
-
cwd: string;
|
|
15
|
-
env: Record<string, string>;
|
|
16
|
-
port: number;
|
|
17
|
-
readyUrl: string;
|
|
18
|
-
start: string;
|
|
19
|
-
}
|
|
20
|
-
export declare function inferLocalRuntime(productDir: string, cwd: string): InferredLocalRuntimeConfig | undefined;
|
|
21
12
|
export declare function normalizeRuntimeConfig(value: unknown, serviceName: string, toolchains: NormalizedToolchainRegistry | null | undefined): NormalizedRuntimeConfig;
|
|
22
13
|
export declare function normalizeRuntimePrepareConfig(value: unknown, serviceName: string): NormalizedBuildPrepareConfig;
|
|
23
14
|
export declare function normalizeRepoExecution(execution: ExecutionConfigInput | null | undefined): NormalizedExecutionConfig;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import
|
|
1
|
+
import t from"fs";import i from"path";import{DEFAULT_FILE_TIMEOUT_SECONDS as a}from"../../domain/file-timeout.js";import{normalizeExecutionConfig as u,normalizeRuntimeMaxConcurrentTasks as f,normalizeRuntimeInstances as l}from"./decode-execution.js";import"./execution-model.js";import{normalizeConfiguredInputs as S,normalizeConfiguredSteps as x}from"../../steps/configured-steps.js";import{buildConfigToPrepare as z,normalizeBuildConfig as C}from"../../steps/build-config.js";import{normalizeRuntimeToolchain as T}from"../../toolchains/index.js";export function normalizeRuntimeConfig(n,e,c){if(!n)return{build:null,instances:1,maxConcurrentTasks:Number.POSITIVE_INFINITY,prepare:{inputs:[],steps:[]},toolchain:null};const r=m(n)?n:{},o=C(r.build,`Service "${e}" runtime.build`),s=normalizeRuntimePrepareConfig(r.prepare,e),p=z(o);return{build:o,instances:l(r.instances??1,`Service "${e}" runtime.instances`),maxConcurrentTasks:f(r.maxConcurrentTasks,`Service "${e}" runtime.maxConcurrentTasks`),prepare:{inputs:[...new Set([...p.inputs,...s.inputs])],steps:[...p.steps,...s.steps]},toolchain:T(r.toolchain,`Service "${e}" runtime.toolchain`,c||{})}}export function normalizeRuntimePrepareConfig(n,e){if(n==null)return{inputs:[],steps:[]};if(!m(n))throw new Error(`Service "${e}" runtime.prepare must be an object`);return{inputs:S(n.inputs,`Service "${e}" runtime.prepare`),steps:x(n.steps,`Service "${e}" runtime.prepare.steps`)}}export function normalizeRepoExecution(n){return n?u(n):u({workers:1,fileTimeoutSeconds:a})}export function detectNextApp(n){return t.existsSync(i.join(n,"next.config.js"))||t.existsSync(i.join(n,"next.config.mjs"))||t.existsSync(i.join(n,"next.config.ts"))}function m(n){return n!==null&&typeof n=="object"}export{normalizeConfiguredStep as normalizeTemplateLifecycleStep,normalizeConfiguredSteps as normalizeTemplateLifecycleSteps,normalizeOptionalString,parseModuleSpecifier}from"../../steps/configured-steps.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{matchesSuiteSelectors as k,parseSuiteSelectors as y}from"../../domain/suite-selection.js";import{normalizePath as q}from"./paths.js";import{isUnknownRecord as E}from"../../kernel/unknown-values.js";const S=String;export function normalizeSkipReason(s,e){const u=S(s||"").trim();if(!u)throw new Error(`${e} requires a non-empty reason`);return u}export function normalizeSkipConfig(s,{name:e,suites:u}){if(!s)return;const f=b(s,`Service "${e}" skip`),l=new Set,a=[];for(const t of Object.values(u))for(const i of t||[]){a.push({testType:i.testType,name:i.name});for(const r of i.files)l.add(q(r))}const c=new Set,p=[];for(const t of m(f.files,`Service "${e}" skip.files`)){const i=$(t,`Service "${e}" skip.files entries must be objects`),r=q(typeof i.path=="string"?i.path:void 0),o=normalizeSkipReason(i.reason,`Service "${e}" skip.files["${r}"]`);if(!r)throw new Error(`Service "${e}" skip.files entries require a non-empty path`);if(c.has(r))throw new Error(`Service "${e}" defines duplicate skip.files path "${r}"`);if(!l.has(r))throw new Error(`Service "${e}" skip.files path "${r}" did not match any discovered suite file`);c.add(r),p.push({path:r,reason:o})}const d=m(f.suites,`Service "${e}" skip.suites`).map((t,i)=>{const r=$(t,`Service "${e}" skip.suites entries must be objects`),o=S(r.selector||"").trim();if(!o)throw new Error(`Service "${e}" skip.suites[${String(i)}] requires a non-empty selector`);const v=normalizeSkipReason(r.reason,`Service "${e}" skip.suites["${o}"]`),w=y([o]),g=w[0];if(w.length!==1)throw new Error(`Service "${e}" skip.suites["${o}"] is invalid`);return{selector:g,reason:v}}),h=new Set,n=[];for(const t of d){if(h.has(t.selector.raw))throw new Error(`Service "${e}" defines duplicate skip.suites selector "${t.selector.raw}"`);if(!a.some(r=>k(r.testType,r.name,[t.selector])))throw new Error(`Service "${e}" skip.suites selector "${t.selector.raw}" did not match any discovered suite`);h.add(t.selector.raw),n.push(t)}if(!(p.length===0&&n.length===0))return{files:p,fileReasonByPath:new Map(p.map(t=>[t.path,t.reason])),suites:n}}export function normalizeServiceRequirements(s,{name:e,suites:u}){if(!s)return R();const f=b(s,`Service "${e}" requirements`),l=[],a=new Set;for(const n of Object.values(u))for(const t of n||[]){l.push({testType:t.testType,name:t.name});for(const i of t.files)a.add(i)}const c=[],p=new Set;for(const n of m(f.suites,`Service "${e}" requirements.suites`)){const t=$(n,`Service "${e}" requirements.suites entries must be objects`),i=S(t.selector||"").trim();if(!i)throw new Error(`Service "${e}" requirements.suites entries require a selector`);const r=y([i]),o=r[0];if(r.length!==1)throw new Error(`Service "${e}" requirements.suites["${i}"] is invalid`);if(o.kind!=="typed")throw new Error(`Service "${e}" requirements.suites["${i}"] must use a typed selector like int:health`);if(p.has(o.raw))throw new Error(`Service "${e}" defines duplicate requirements.suites selector "${o.raw}"`);if(!l.some(w=>k(w.testType,w.name,[o])))throw new Error(`Service "${e}" requirements.suites selector "${o.raw}" did not match any discovered suite`);p.add(o.raw),c.push({selector:o,locks:normalizeRequirementLocks(t.locks,`Service "${e}" requirements.suites["${o.raw}"].locks`)})}const d=[],h=new Set;for(const[n,t]of m(f.files,`Service "${e}" requirements.files`).entries()){const i=$(t,`Service "${e}" requirements.files entries must be objects`),r=S(i.path||"").trim();if(!r)throw new Error(`Service "${e}" requirements.files[${String(n)}] requires a path`);if(!a.has(r))throw new Error(`Service "${e}" requirements.files["${r}"] did not match any discovered test file`);if(h.has(r))throw new Error(`Service "${e}" defines duplicate requirements.files path "${r}"`);h.add(r),d.push({path:r,locks:normalizeRequirementLocks(i.locks,`Service "${e}" requirements.files["${r}"].locks`)})}return{suites:c,files:d,fileLocksByPath:new Map(d.map(n=>[n.path,n.locks]))}}export function normalizeRequirementLocks(s,e){const u=Array.isArray(s)?s:s==null?[]:[s],f=new Set,l=[];for(const a of u){const c=S(a||"").trim();if(!c)throw new Error(`${e} entries must be non-empty strings`);f.has(c)||(f.add(c),l.push(c))}return l.sort()}function R(){return{suites:[],files:[],fileLocksByPath:new Map}}function b(s,e){if(!E(s))throw new Error(`${e} must be an object`);return s}function $(s,e){if(!E(s))throw new Error(e);return s}function m(s,e){if(s==null)return[];if(!Array.isArray(s))throw new Error(`${e} must be an array`);return s}
|