@gfdelarue/testkit 0.3.4 → 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/README.md +11 -73
  2. package/dist/app/doctor/index.js +1 -1
  3. package/dist/app/inspect/artifacts.d.ts +1 -0
  4. package/dist/app/inspect/artifacts.js +1 -1
  5. package/dist/app/inspect/index.js +1 -1
  6. package/dist/app/lint/index.js +1 -1
  7. package/dist/app/lint/native.js +1 -1
  8. package/dist/app/lint/policy.js +1 -1
  9. package/dist/app/lint/rules/data.js +1 -1
  10. package/dist/app/lint/rules/engine-imports.js +1 -1
  11. package/dist/app/lint/rules/syntax.js +1 -1
  12. package/dist/app/lint/rules/unit.js +1 -1
  13. package/dist/app/lint/shared.d.ts +1 -1
  14. package/dist/app/lint/shared.js +1 -1
  15. package/dist/app/run/orchestrator.js +1 -1
  16. package/dist/app/run/request.js +1 -1
  17. package/dist/app/setup/playwright.js +1 -1
  18. package/dist/app/status/index.d.ts +2 -0
  19. package/dist/app/status/index.js +1 -1
  20. package/dist/app/status/runs.d.ts +2 -2
  21. package/dist/app/status/runs.js +1 -1
  22. package/dist/app/stop/index.js +1 -1
  23. package/dist/app/typecheck/index.d.ts +2 -0
  24. package/dist/app/typecheck/index.js +4 -4
  25. package/dist/cli/commands/artifacts.js +2 -2
  26. package/dist/cli/commands/db/schema/refresh.js +1 -1
  27. package/dist/cli/commands/db/schema/verify.js +1 -1
  28. package/dist/cli/commands/doctor.js +1 -1
  29. package/dist/cli/operations/db/schema/refresh/operation.d.ts +1 -0
  30. package/dist/cli/operations/db/schema/refresh/operation.js +1 -1
  31. package/dist/cli/operations/db/schema/verify/operation.d.ts +1 -0
  32. package/dist/cli/operations/db/schema/verify/operation.js +1 -1
  33. package/dist/cli/operations/run/operation.js +1 -1
  34. package/dist/config/loader/discover-tests.d.ts +5 -0
  35. package/dist/config/loader/discover-tests.js +1 -1
  36. package/dist/config/loader/discovery.js +2 -2
  37. package/dist/config/loader/file-metadata.js +1 -1
  38. package/dist/config/loader/index.d.ts +2 -1
  39. package/dist/config/loader/index.js +1 -1
  40. package/dist/config/loader/load.d.ts +2 -0
  41. package/dist/config/loader/load.js +1 -1
  42. package/dist/config/loader/managed-configs.d.ts +2 -0
  43. package/dist/config/loader/managed-configs.js +1 -1
  44. package/dist/config/loader/paths.d.ts +1 -1
  45. package/dist/config/loader/paths.js +1 -1
  46. package/dist/config/loader/skip-config.d.ts +1 -1
  47. package/dist/config/loader/test-selection.d.ts +15 -0
  48. package/dist/config/loader/test-selection.js +1 -0
  49. package/dist/database/index.d.ts +1 -3
  50. package/dist/database/index.js +1 -1
  51. package/dist/database/lifecycle-cleanup.d.ts +7 -0
  52. package/dist/database/lifecycle-cleanup.js +1 -0
  53. package/dist/database/local-postgres.js +1 -1
  54. package/dist/database/schema-source.js +3 -3
  55. package/dist/engines/adapters/evidence-files.js +1 -1
  56. package/dist/engines/catalog.d.ts +3 -8
  57. package/dist/engines/catalog.js +1 -1
  58. package/dist/engines/definitions.d.ts +8 -1
  59. package/dist/engines/definitions.js +1 -1
  60. package/dist/engines/generated-performance.d.ts +1 -0
  61. package/dist/engines/generated-performance.js +12 -0
  62. package/dist/engines/installations/node-vitest.d.ts +8 -22
  63. package/dist/engines/installations/node-vitest.js +1 -1
  64. package/dist/engines/installations/package-requirements.d.ts +28 -0
  65. package/dist/engines/installations/package-requirements.js +1 -0
  66. package/dist/engines/installations/playwright.d.ts +1 -11
  67. package/dist/engines/installations/playwright.js +1 -1
  68. package/dist/engines/k6/adapter/repository-configuration.d.ts +1 -1
  69. package/dist/engines/k6/adapter/repository-configuration.js +1 -1
  70. package/dist/engines/managed-module-resolution.js +111 -21
  71. package/dist/engines/manager.d.ts +1 -0
  72. package/dist/engines/manager.js +1 -1
  73. package/dist/engines/node-vitest/adapter/adapter.d.ts +1 -1
  74. package/dist/engines/node-vitest/adapter/adapter.js +1 -1
  75. package/dist/engines/node-vitest/adapter/consumer-typescript-paths.js +1 -1
  76. package/dist/engines/node-vitest/adapter/resolution.d.ts +1 -1
  77. package/dist/engines/node-vitest/adapter/resolution.js +1 -1
  78. package/dist/engines/node-vitest/adapter/workspace.js +33 -9
  79. package/dist/engines/node-vitest/runtime/runtime-input.d.ts +1 -0
  80. package/dist/engines/node-vitest/runtime/runtime-input.js +1 -1
  81. package/dist/engines/runtime-input-file.d.ts +3 -1
  82. package/dist/engines/runtime-input-file.js +1 -1
  83. package/dist/environment/lifecycle-controller.d.ts +1 -2
  84. package/dist/environment/lifecycle-controller.js +1 -1
  85. package/dist/environment/lifecycle.js +1 -1
  86. package/dist/environment/maintenance.d.ts +1 -1
  87. package/dist/environment/maintenance.js +1 -1
  88. package/dist/environment/ready-environment.d.ts +1 -1
  89. package/dist/environment/ready-environment.js +1 -1
  90. package/dist/environment/runtime-contexts.d.ts +1 -1
  91. package/dist/environment/runtime-contexts.js +1 -1
  92. package/dist/environment/runtime-preparation.js +1 -1
  93. package/dist/environment/services.js +1 -1
  94. package/dist/environment/template.d.ts +1 -1
  95. package/dist/environment/template.js +1 -1
  96. package/dist/evidence/bundles.js +1 -1
  97. package/dist/evidence/read-run.js +1 -1
  98. package/dist/evidence/reading.d.ts +1 -1
  99. package/dist/evidence/reading.js +1 -1
  100. package/dist/evidence/storage.js +2 -2
  101. package/dist/kernel/error-serialization.js +1 -1
  102. package/dist/kernel/work-metrics.d.ts +6 -0
  103. package/dist/kernel/work-metrics.js +1 -0
  104. package/dist/process/capture-output.js +1 -1
  105. package/dist/process/execa-bundle.json +1 -1
  106. package/dist/process/files/product-dir.d.ts +1 -0
  107. package/dist/process/files/product-dir.js +1 -0
  108. package/dist/process/locks.js +4 -4
  109. package/dist/process/readiness.js +1 -1
  110. package/dist/process/runs/inspection.d.ts +5 -0
  111. package/dist/process/runs/inspection.js +1 -0
  112. package/dist/process/runs/manifest.d.ts +1 -0
  113. package/dist/process/runs/manifest.js +1 -1
  114. package/dist/process/service-processes.d.ts +1 -1
  115. package/dist/process/service-processes.js +1 -1
  116. package/dist/toolchains/index.js +2 -2
  117. package/docs/cloud-control.md +3 -0
  118. package/docs/engine-architecture.md +4 -1
  119. package/docs/test-types.md +9 -0
  120. package/docs/testkit-cloud-boundary.md +12 -24
  121. package/package.json +2 -1
  122. package/testkit-config-schema.md +2 -0
  123. package/dist/cli/renderers/artifact-preview.d.ts +0 -4
  124. package/dist/cli/renderers/artifact-preview.js +0 -1
@@ -1,3 +1,3 @@
1
- import{asUnknownRecord as c}from"../../kernel/unknown-values.js";import s from"node:fs";import{inspectArtifacts as d}from"../../app/inspect/artifacts.js";import{formatArtifactPreview as p}from"../renderers/artifact-preview.js";export default function l({args:i,flags:e,json:n,log:r}){const o=d({dir:e.dir,mode:e.list?"runs":e.verify?"verify":"artifacts",file:i.file||null,runId:e.run||null,service:e.service||null,kind:e.kind});if(o.mode==="runs"){const{runs:t}=o;if(n)return{ok:!0,runs:t};t.length===0&&r("No immutable Testkit runs found.");for(const u of t)r(`${u.runId} ${u.status||"unknown"} ${u.finishedAt||u.startedAt||"unknown-time"}`);return{ok:!0,count:t.length}}const f=o.runId;if(o.mode==="verify"){const{verification:t}=o;return t.ok||(process.exitCode=1),n?{ok:t.ok,verification:t}:(r(t.ok?`Verified ${f??"unknown"}: ${String(t.checkedFiles)} files (${t.manifestDigest??"unknown-digest"})`:`Integrity verification failed for ${f??"unknown"}: ${t.errors.join("; ")}`),{ok:t.ok,verification:t})}const{entries:a}=o;if(n)return{ok:!0,runId:f,artifacts:a.map(t=>({file:t.file.path,service:t.service.name,suite:{engineId:t.suite.engineId,name:t.suite.name,profileId:t.suite.profileId,testType:t.suite.testType},ref:t.artifactRef,payload:t.payload}))};if(e.format==="text")r(k(a));else for(const t of m(a))r(t);return{ok:!0,runId:f,count:a.length}}function m(i){if(i.length===0)return["No artifacts matched."];const e=[];for(const n of i){e.push(`${n.artifactRef.name}${n.artifactRef.kind?` [${n.artifactRef.kind}]`:""}`),n.artifactRef.summary&&e.push(` ${n.artifactRef.summary}`);for(const r of p(n.payload,8))e.push(` ${r}`);e.push(` ${n.artifactRef.path}`)}return e}function k(i){return i.length===0?"No artifacts matched.":i.map(e=>h(e)).join(`
1
+ import{asUnknownRecord as c}from"../../kernel/unknown-values.js";import s from"node:fs";import{inspectArtifacts as d}from"../../app/inspect/artifacts.js";export default function l({args:i,flags:e,json:n,log:o}){const r=d({dir:e.dir,mode:e.list?"runs":e.verify?"verify":"artifacts",file:i.file||null,runId:e.run||null,service:e.service||null,kind:e.kind,includePayload:n||e.format==="text"});if(r.mode==="runs"){const{runs:t}=r;if(n)return{ok:!0,runs:t};t.length===0&&o("No immutable Testkit runs found.");for(const u of t)o(`${u.runId} ${u.status||"unknown"} ${u.finishedAt||u.startedAt||"unknown-time"}`);return{ok:!0,count:t.length}}const a=r.runId;if(r.mode==="verify"){const{verification:t}=r;return t.ok||(process.exitCode=1),n?{ok:t.ok,verification:t}:(o(t.ok?`Verified ${a??"unknown"}: ${String(t.checkedFiles)} files (${t.manifestDigest??"unknown-digest"})`:`Integrity verification failed for ${a??"unknown"}: ${t.errors.join("; ")}`),{ok:t.ok,verification:t})}const{entries:f}=r;if(n)return{ok:!0,runId:a,artifacts:f.map(t=>({file:t.file.path,service:t.service.name,suite:{engineId:t.suite.engineId,name:t.suite.name,profileId:t.suite.profileId,testType:t.suite.testType},ref:t.artifactRef,payload:t.payload}))};if(e.format==="text")o(p(f));else for(const t of m(f))o(t);return{ok:!0,runId:a,count:f.length}}function m(i){if(i.length===0)return["No artifacts matched."];const e=[];for(const n of i)e.push(`${n.artifactRef.name}${n.artifactRef.kind?` [${n.artifactRef.kind}]`:""}`),n.artifactRef.summary&&e.push(` ${n.artifactRef.summary}`),e.push(` ${n.artifactRef.path}`);return e}function p(i){return i.length===0?"No artifacts matched.":i.map(e=>k(e)).join(`
2
2
 
3
- `)}function h(i){if(typeof i.payload?.text=="string")return i.payload.text.trimEnd();const e=c(i.payload?.data);return typeof e?.text=="string"?e.text.trimEnd():i.artifactRef.contentType==="application/json"&&s.existsSync(i.absolutePath)?s.readFileSync(i.absolutePath,"utf8").trimEnd():JSON.stringify(i.payload?.data??i.payload??null,null,2)}
3
+ `)}function k(i){if(typeof i.payload?.text=="string")return i.payload.text.trimEnd();const e=c(i.payload?.data);return typeof e?.text=="string"?e.text.trimEnd():i.artifactRef.contentType==="application/json"&&s.existsSync(i.absolutePath)?s.readFileSync(i.absolutePath,"utf8").trimEnd():JSON.stringify(i.payload?.data??i.payload??null,null,2)}
@@ -1 +1 @@
1
- import{executeDatabaseSchemaRefreshOperation as n}from"../../../operations/db/schema/refresh/operation.js";import{renderDatabaseSchemaRefreshResult as f}from"../../../renderers/db-schema/text.js";export default async function s({flags:r,json:t,log:a}){const e=await n(r);if(!t)for(const o of f(e))a(o);return e}
1
+ import{executeDatabaseSchemaRefreshOperation as f}from"../../../operations/db/schema/refresh/operation.js";import{renderDatabaseSchemaRefreshResult as n}from"../../../renderers/db-schema/text.js";export default async function s({flags:t,json:e,log:a}){const r=await f({...t,json:e});if(!e)for(const o of n(r))a(o);return r}
@@ -1 +1 @@
1
- import{executeDatabaseSchemaVerifyOperation as n}from"../../../operations/db/schema/verify/operation.js";import{renderDatabaseSchemaVerifyResult as f}from"../../../renderers/db-schema/text.js";export default async function i({flags:r,json:t,log:a}){const e=await n(r);if(!t)for(const o of f(e))a(o);return e}
1
+ import{executeDatabaseSchemaVerifyOperation as f}from"../../../operations/db/schema/verify/operation.js";import{renderDatabaseSchemaVerifyResult as i}from"../../../renderers/db-schema/text.js";export default async function n({flags:t,json:e,log:a}){const r=await f({...t,json:e});if(!e)for(const o of i(r))a(o);return r}
@@ -1 +1 @@
1
- import{runDoctor as n}from"../../app/doctor/index.js";import{renderDoctorResult as i}from"../renderers/doctor/text.js";export default async function f({flags:o,json:e,log:c}){const r=await n({dir:o.dir,typecheck:o.typecheck});if(!e)for(const t of i(r))c(t);if(!r.ok)throw Object.assign(new Error("testkit doctor failed"),{result:r});return r}
1
+ import{attachErrorDiagnostics as i}from"../../kernel/error-serialization.js";import{runDoctor as n}from"../../app/doctor/index.js";import{renderDoctorResult as s}from"../renderers/doctor/text.js";export default async function f({flags:o,json:c,log:e}){const r=await n({dir:o.dir,typecheck:o.typecheck});if(!c)for(const t of s(r))e(t);if(!r.ok){const t=Object.assign(new Error("testkit doctor failed"),{result:r});throw i(t,{checks:r.checks})}return r}
@@ -2,5 +2,6 @@ import { type DatabaseSchemaRefreshOptions as ApplicationOptions, type DatabaseS
2
2
  export type { DatabaseSchemaRefreshResult } from "../../../../../app/db-schema/refresh.js";
3
3
  export interface DatabaseSchemaRefreshOptions extends ApplicationOptions {
4
4
  debug?: boolean;
5
+ json?: boolean;
5
6
  }
6
7
  export declare function executeDatabaseSchemaRefreshOperation(options?: Readonly<DatabaseSchemaRefreshOptions>): Promise<DatabaseSchemaRefreshResult>;
@@ -1 +1 @@
1
- import{executeDatabaseSchemaRefresh as r}from"../../../../../app/db-schema/refresh.js";import{createTextStepProgressReporter as t}from"../../../../renderers/run/text-reporter.js";export function executeDatabaseSchemaRefreshOperation(e={}){return r(e,t({debug:e.debug}))}
1
+ import{executeDatabaseSchemaRefresh as t}from"../../../../../app/db-schema/refresh.js";export async function executeDatabaseSchemaRefreshOperation(e={}){return t(e,e.json?{outputMode:"json"}:(await import("../../../../renderers/run/text-reporter.js")).createTextStepProgressReporter({debug:e.debug}))}
@@ -2,5 +2,6 @@ import { type DatabaseSchemaVerifyOptions as ApplicationOptions, type DatabaseSc
2
2
  export type { DatabaseSchemaVerifyResult } from "../../../../../app/db-schema/verify.js";
3
3
  export interface DatabaseSchemaVerifyOptions extends ApplicationOptions {
4
4
  debug?: boolean;
5
+ json?: boolean;
5
6
  }
6
7
  export declare function executeDatabaseSchemaVerifyOperation(options?: Readonly<DatabaseSchemaVerifyOptions>): Promise<DatabaseSchemaVerifyResult>;
@@ -1 +1 @@
1
- import{executeDatabaseSchemaVerify as r}from"../../../../../app/db-schema/verify.js";import{createTextStepProgressReporter as t}from"../../../../renderers/run/text-reporter.js";export function executeDatabaseSchemaVerifyOperation(e={}){return r(e,t({debug:e.debug}))}
1
+ import{executeDatabaseSchemaVerify as t}from"../../../../../app/db-schema/verify.js";export async function executeDatabaseSchemaVerifyOperation(e={}){return t(e,e.json?{outputMode:"json"}:(await import("../../../../renderers/run/text-reporter.js")).createTextStepProgressReporter({debug:e.debug}))}
@@ -1,2 +1,2 @@
1
- import{runAll as a}from"../../../app/run/orchestrator.js";import{createRunTextListener as p}from"../../renderers/run/text-reporter.js";import{createRunEventsListener as l}from"../../renderers/run/events.js";export async function executeRunRequest(t,{outputMode:o="compact",json:e=!1,debug:c=!1,terminal:i={}}={}){const u=i.stdout||process.stdout,n=resolveRunOutputMode(o,e,c),f=n==="json"?()=>{}:n==="events"?l({stdout:u}):p({outputMode:n,stdout:u}),r=await a(t.configs,t.typeValues,t.suiteSelectors,{debug:n==="debug",onEvent:f,...t.runOptions},t.allConfigs),s=r.runArtifact.archive.runId;return(n==="compact"||n==="debug")&&u.write(`Run ID: ${s}
1
+ import{runAll as p}from"../../../app/run/orchestrator.js";export async function executeRunRequest(t,{outputMode:o="compact",json:e=!1,debug:c=!1,terminal:i={}}={}){const u=i.stdout||process.stdout,n=resolveRunOutputMode(o,e,c),a=n==="json"?()=>{}:n==="events"?(await import("../../renderers/run/events.js")).createRunEventsListener({stdout:u}):(await import("../../renderers/run/text-reporter.js")).createRunTextListener({outputMode:n,stdout:u}),r=await p(t.configs,t.typeValues,t.suiteSelectors,{debug:n==="debug",onEvent:a,...t.runOptions},t.allConfigs),s=r.runArtifact.archive.runId;return(n==="compact"||n==="debug")&&u.write(`Run ID: ${s}
2
2
  `),{outputMode:n,runId:s,...r}}export function resolveRunOutputMode(t,o,e){return o?"json":e?"debug":t==="debug"||t==="events"||t==="json"?t:"compact"}
@@ -101,6 +101,11 @@ export declare function loadDiscoveredConfigContext(options?: Readonly<Pick<Disc
101
101
  context: NormalizedConfigContext;
102
102
  discovery: DiscoveryResult;
103
103
  }>;
104
+ export interface LoadedDiscoveryContext {
105
+ discovery: DiscoveryResult;
106
+ context: NormalizedConfigContext | null;
107
+ }
108
+ export declare function loadDiscoveryContext(options?: Readonly<DiscoverTestsOptions>, context?: NormalizedConfigContext): Promise<LoadedDiscoveryContext>;
104
109
  export declare function formatDisplayName(value: unknown): string;
105
110
  export declare function testTypeContract(testType: DiscoveryTestType, engineId: EngineId, profileId: RunnerProfileId): DiscoveryTestContract;
106
111
  export declare function fileDisplayName(filePath: string): string;
@@ -1 +1 @@
1
- import g from"path";import{randomUUID as A}from"node:crypto";import{loadConfigContext as P,resolveProductDir as B}from"./load.js";import{discoverProject as H}from"./discovery.js";import{loadTestkitConfig as L}from"./config-loader.js";import{TEST_TYPE_ORDER as V,isRunType as _}from"../../domain/test-types.js";import"../../domain/identifiers.js";import{DEFAULT_RUNTIME_ISOLATION_POLICY as U,resolveRuntimeIsolationPolicy as q}from"./runtime-isolation.js";import{resolveEngineProfile as G}from"../../engines/catalog.js";import{readTestFileMetadata as Y}from"./file-metadata.js";import{normalizeDiscoveryConfig as K}from"./decode-discovery.js";import"./path-policy.js";import{buildDiscoverySummary as J,emptyDiscoverySummary as Q}from"./discovery-summary.js";import{matchesSelectedTypes as W,matchesSuiteSelectors as k,normalizeTypeValues as X,parseSuiteSelectors as Z}from"../../domain/suite-selection.js";import{errorMessage as D}from"../../kernel/error-serialization.js";import{isObjectRecord as R,readObjectRecord as I,readProperty as f}from"./values.js";const ee=9,w=String;export async function discoverTests(e={}){return E(e)}export async function loadDiscoveredConfigContext(e={}){const i=await P({dir:e.dir,configImportCacheSalt:A()}),t=await E({dir:i.productDir,diagnostics:"error"},i);return{context:i,discovery:t}}async function E(e,i){const t=B(process.cwd(),e.dir),o=ae(e),r=i?{config:i.config,configFile:i.configFile,diagnostics:[]}:await ie(t,o.diagnosticsMode),s={schemaVersion:ee,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:Q()};if(!r.config)return T(s);const a=ue(r.config),l=de(a.services),p=i?.discovery??H(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),ce(o.serviceFilter,a.services,p);let y=i??null;try{y??=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:D(n)})}const h=oe({productDir:t,rawDiscovery:p,explicitServices:a.services,configs:y?.configs??[],fileMetadataByPath:v.fileMetadataByPath,filters:o});return T({...s,...h})}export function formatDisplayName(e){return w(e||"").split(/[-_/]+/g).filter(Boolean).map(i=>i.replace(/^\w/,t=>t.toUpperCase())).join(" ")}export function testTypeContract(e,i,t){const o=G(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 ie(e,i){try{const t=await L(e),{config:o,configFile:r}=te(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:D(t)}]}}}function te(e){if(!R(e))throw new Error("testkit config loader must return an object");const i=f(e,"config"),t=f(e,"configFile");if(!R(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 oe({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),d=o.services[n],u=r[n];l.set(n,{name:n,discovered:c!==void 0||!!d,localCwd:c?c.testkit.local?.cwd||".":u?.localCwd||d?.inferredLocalCwd||".",dependsOn:[...c?.testkit.dependsOn??u?.dependsOn??[]],suiteCount:0,fileCount:0,activeFileCount:0,skippedFileCount:0})}for(const n of o.files){const c=n.testType;if(!W(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 d=i.get(n.filePath)??Y(t,n.filePath);i.set(n.filePath,d);const u=a.get(n.serviceName),m=u?.suites[c]?.find(z=>z.name===n.suiteName);if(u&&(!m||m.testType!==c||m.engineId!==d.engineId||m.profileId!==d.profileId))throw new Error(`Normalized suite ${n.serviceName}/${c}:${n.suiteName} conflicts with file ${n.filePath}`);const S=u&&(d.skipReason||u.testkit.skip?.fileReasonByPath.get(n.filePath)||pe(u,c,n.suiteName))||null;if(s.runnableOnly&&S)continue;const M=u?[...new Set([...me(u,c,n.suiteName),...u.testkit.requirements.fileLocksByPath.get(n.filePath)||[],...d.locks])].sort():[],$=l.get(n.serviceName)?.dependsOn||[],C=ne({serviceName:n.serviceName,suiteName:n.suiteName,testType:c,filePath:n.filePath,engineId:d.engineId,profileId:d.profileId,skipReason:S,locks:M,runtimeIsolation:u&&m?q(u,m,n.filePath):U,dependsOn:$});v.push(C);const O=[n.serviceName,c,n.suiteName].join("|"),F=p.get(O);F?F.files.push(C):p.set(O,{serviceName:n.serviceName,testType:c,suiteName:n.suiteName,files:[C]})}const h=[...p.values()].map(n=>re(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(ye),files:v.sort(he)}}function ne(e){const i=testTypeContract(e.testType,e.engineId,e.profileId);return{id:ve(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 re(e,i,t,o,r){const s=se(e,i,t,o),a=o.filter(l=>l.skipped).length;return{id:ge(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 se(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 T(e){return{...e,diagnostics:e.diagnostics.sort(we),summary:J({diagnostics:e.diagnostics,files:e.files,serviceCount:e.services.length,suiteCount:e.suites.length})}}function ce(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 ae(e){const i=le(X(b(e.type))),t=Z(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 le(e){const i=[];for(const t of e){if(!_(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:fe(f(s,"dependsOn")),discovery:x(f(s,"discovery")),localCwd:N(f(a,"cwd")||"."),localDisabled:f(s,"local")===!1}}return{discovery:x(f(e,"discovery")),services:t}}function de(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 x(e){try{return K(e,{allowRoots:!0})}catch{return{}}}function fe(e){return Array.isArray(e)?e.filter(t=>typeof t=="string"):[]}function pe(e,i,t){for(const o of e.testkit.skip?.suites||[])if(k(i,t,[o.selector]))return o.reason;return null}function me(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=>w(t)):e==null||e===""?[]:[w(e)]}function N(e){return w(e).split(g.sep).join("/").replace(/^\.\/+/,"")}export function fileDisplayName(e){const i=new RegExp(`\\.(${V.join("|")})\\.testkit\\.ts$`),t=g.posix.basename(e).replace(i,"");return formatDisplayName(t)}function ge(e,i,t,o,r){return[e,i,t,o,r].join("|")}function ve(e,i,t,o,r){return[e,i,t,o,N(r)].join("|")}function ye(e,i){return e.service.localeCompare(i.service)||e.testType.localeCompare(i.testType)||e.name.localeCompare(i.name)}function he(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 j(e.severity)-j(i.severity)||e.code.localeCompare(i.code)||(e.path||"").localeCompare(i.path||"")}function j(e){return e==="error"?1:2}
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,2 +1,2 @@
1
- import R from"fs";import h from"path";import{DEFAULT_DISCOVERY_EXCLUDES as k,normalizeDiscoveryPath as L,resolveDiscoveryRoots as K,shouldExcludeDiscoveryPath as F}from"./path-policy.js";import{normalizeDiscoveryConfig as D}from"./decode-discovery.js";const C=String,E="__testkit__",z=[{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={},n={}){const r=n.strict!==!1,o=D(n.discovery,{allowRoots:!0}),{suiteFiles:s,legacyFiles:a}=O(t,o),i={},p={},u=Y(a),g=[];for(const c of s){const l=N(c);if(!l)continue;const f=A(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]=W(p[d.name],d);const y=V(d,c),w=q(y,l.suffix);g.push({serviceName:d.name,testType:l.testType,filePath:c,suitePath:w.suitePath})}if(r&&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=M(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 discoverSuites(t,e={},n={}){return discoverProject(t,e,n).suitesByService}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."],n=t.filter(s=>s.code==="legacy_path").map(s=>s.path),r=t.filter(s=>s.code==="unowned_test").map(s=>s.path),o=t.filter(s=>s.code==="ambiguous_test");if(n.length>0){e.push(""),e.push("Legacy test files outside __testkit__:");for(const s of n)e.push(`- ${s}`)}if(r.length>0){e.push(""),e.push("Unowned test files:");for(const s of r)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 O(t,e={}){const n=[],r=[],o=[...new Set([...k,...(e.exclude||[]).map(x)])],s=K(t,e.roots,".");for(;s.length>0;){const a=s.pop();if(a!==void 0)for(const i of R.readdirSync(a,{withFileTypes:!0})){const p=h.join(a,i.name);if(i.isSymbolicLink())continue;if(i.isDirectory()){const g=L(h.relative(t,p));if(F(g,o))continue;s.push(p);continue}if(!i.isFile())continue;const u=x(h.relative(t,p));N(u)&&(u.split("/").includes(E)?n.push(u):r.push(u))}}return{suiteFiles:n.sort((a,i)=>a.localeCompare(i)),legacyFiles:r.sort((a,i)=>a.localeCompare(i))}}function A(t,e,n={}){const r=Object.entries(e).flatMap(([i,p])=>I(i,p,n));if(r.length===0)return[{name:"app",source:"implicit-root",cwdPrefix:"."}];const o=r.filter(i=>i.depth===0||i.matchPrefix&&b(t,i.matchPrefix)),s=o.filter(i=>U(i,t));if(s.length===0)return o.length>0&&o.every(i=>j(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,n=new Map;for(const r of t){const o=r._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,r._suiteKey);break}if(i===r._suiteKey)break;s=""}s||(s=o.join("-")),n.set(r._suiteKey,s)}if(n.size!==t.length)for(const r of t)n.has(r._suiteKey)||n.set(r._suiteKey,r._suitePath.join("-"));if(new Set(n.values()).size!==t.length){const r=new Map;for(const o of t){const s=o._suitePath.join("-"),a=r.get(s)||0;r.set(s,a+1),n.set(o._suiteKey,a===0?s:`${s}-${C(a+1)}`)}}return n}function M(t,e){const n=t.get(e);if(n===void 0)throw new Error(`Discovery suite name was not resolved for "${e}"`);return n}function I(t,e,n={}){const r=e?.local,o=x(r&&r.cwd||"."),s=D(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([...k,...(n.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 U(t,e){return j(t,e)?!1:!!(t.depth===0||t.matchPrefix&&b(e,t.matchPrefix))}function V(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 j(t,e){return F(x(e),t.exclude)}function q(t,e){const n=t.split("/").filter(Boolean),r=n.indexOf(E);if(r===-1)return{suitePath:[h.posix.basename(t,e)]};const o=n.slice(0,r),s=n.slice(r+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 W(t,e){return t?{...t,inferredLocalCwd:t.inferredLocalCwd==="."?e.cwdPrefix||".":t.inferredLocalCwd}:{name:e.name,inferredLocalCwd:e.cwdPrefix||".",source:e.source}}function N(t){return z.find(e=>t.endsWith(e.suffix))||null}function b(t,e){return t===e||t.startsWith(`${e}/`)}function x(t){const e=C(t).split(h.sep).join("/");return e==="."||e==="./"?".":e.replace(/^\.\/+/,"").replace(/\/+$/,"")||"."}function Y(t){return t.map(e=>({code:"legacy_path",severity:"error",message:`Legacy test file outside __testkit__: ${e}`,path:e}))}
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={},n={}){const r=n.strict!==!1,o=C(n.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=B(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(r&&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=M(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 discoverSuites(t,e={},n={}){return discoverProject(t,e,n).suitesByService}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."],n=t.filter(s=>s.code==="legacy_path").map(s=>s.path),r=t.filter(s=>s.code==="unowned_test").map(s=>s.path),o=t.filter(s=>s.code==="ambiguous_test");if(n.length>0){e.push(""),e.push("Legacy test files outside __testkit__:");for(const s of n)e.push(`- ${s}`)}if(r.length>0){e.push(""),e.push("Unowned test files:");for(const s of r)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 n=[],r=[],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)?n.push(u):r.push(u))}}}return{suiteFiles:n.sort((a,i)=>a.localeCompare(i)),legacyFiles:r.sort((a,i)=>a.localeCompare(i))}}function B(t,e,n={}){const r=Object.entries(e).flatMap(([i,p])=>U(i,p,n));if(r.length===0)return[{name:"app",source:"implicit-root",cwdPrefix:"."}];const o=r.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 M(t){const e=new Map,n=new Map;for(const r of t){const o=r._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,r._suiteKey);break}if(i===r._suiteKey)break;s=""}s||(s=o.join("-")),n.set(r._suiteKey,s)}if(n.size!==t.length)for(const r of t)n.has(r._suiteKey)||n.set(r._suiteKey,r._suitePath.join("-"));if(new Set(n.values()).size!==t.length){const r=new Map;for(const o of t){const s=o._suitePath.join("-"),a=r.get(s)||0;r.set(s,a+1),n.set(o._suiteKey,a===0?s:`${s}-${E(a+1)}`)}}return n}function I(t,e){const n=t.get(e);if(n===void 0)throw new Error(`Discovery suite name was not resolved for "${e}"`);return n}function U(t,e,n={}){const r=e?.local,o=x(r&&r.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,...(n.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 n=t.split("/").filter(Boolean),r=n.indexOf(j);if(r===-1)return{suitePath:[h.posix.basename(t,e)]};const o=n.slice(0,r),s=n.slice(r+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{isPathWithin as L}from"../../process/files/paths.js";import y from"fs";import E from"path";import{createRequire as M}from"node:module";import{fileURLToPath as R}from"node:url";import{ENGINE_IDS as A,PUBLIC_TEST_TYPES as O,isEngineId as j}from"../../domain/identifiers.js";import{normalizeRuntimeIsolationPolicy as B}from"./runtime-isolation.js";import{resolveEngineProfile as K,expectedEngineAuthoringModule as V,invalidEngineAuthoringImport as _,requireMatchingHostImport as W}from"../../engines/catalog.js";const z=M(import.meta.url),U=R(import.meta.resolve("typescript"));let G;function a(){return G??=z(U)}const d="@gfdelarue/testkit",J="@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=xe(e,t),s=Ee(t),n=y.readFileSync(r,"utf8"),o=i.createSourceFile(r,n,i.ScriptTarget.Latest,!0,i.ScriptKind.TS),u=Se(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)||!ee(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(!te(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');re(o,p,t);const S=H(l.initializer,t,s);return requireMatchingEngineAuthoringImports(o,t,S),S}function H(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(P(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 X(s,t,i)}function X(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=h(l.name,t,"testkit metadata");if(!ie(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=Y(l.initializer,t);continue}if(p==="skip"){s.skipReason=Q(l.initializer,t);continue}s.runtimeIsolation=Z(l.initializer,t)}if(!n)throw c(t,'a literal "engine" property is required');if(!j(n))throw c(t,`unsupported engine "${n}"; expected one of: ${A.join(", ")}`);let u;try{u=K(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 Y(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(b(s,t,'metadata property "locks" entry'))}return[...new Set(r)].sort()}function Q(e,t){const i=a(),r=x(e);if(r!==null)return T(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(h(s.name,t,'metadata property "skip"')!=="reason")throw c(t,'metadata property "skip" object only accepts "reason"');return b(s.initializer,t,'metadata property "skip.reason"')}function Z(e,t){const i=I(e,t,'metadata property "runtimeIsolation"');try{return B(i,"defineFile runtimeIsolation")}catch(r){throw c(t,$(r),r)}}function P(e){return a().isIdentifier(e)?e.text:null}function h(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 T(r,t,i)}function b(e,t,i){return I(e,t,i).trim()}function T(e,t,i){if(e.trim().length===0)throw c(t,`${i} must not be empty`);return e}function ee(e){const t=a();return(e.modifiers||[]).some(i=>i.kind===t.SyntaxKind.ExportKeyword)}function te(e){const t=a();return(e.declarationList.flags&t.NodeFlags.Const)!==0}function ie(e){return["engine","locks","runtimeIsolation","skip"].includes(e)}function re(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===d){if(s)throw c(i,`defineFile must have exactly one named value import from "${d}"`);s=o}}continue}n+=ye(o)}if(!s)throw c(i,`defineFile must be a named value import from "${d}"`);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){ne(e,t,i);const r=V(i.profileId);for(const s of collectModuleSpecifiers(e))if(W(t,i,s),!(s===d||s===r||!s.startsWith(`${d}/`)&&!s.startsWith(J)))throw _(t,i,s,r)}function ne(e,t,i){const r=a(),s=q(e),n=o=>{if(se(o,t,i),oe(o,s,t,i),r.isCallExpression(o)&&D(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 v(o))n(u);r.forEachChild(o,n)};n(e)}function D(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 se(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 g(t,i,"createRequire")}if(r.isPropertyAccessExpression(e)&&e.name.text==="createRequire"||r.isElementAccessExpression(e)&&x(e.argumentExpression)==="createRequire"||r.isBindingElement(e)&&(le(e)==="createRequire"||r.isIdentifier(e.name)&&e.name.text==="createRequire"))throw g(t,i,"createRequire")}function oe(e,t,i,r){const s=a();if(s.isPropertyAccessExpression(e)&&e.name.text==="require"&&F(e.expression,t)||s.isElementAccessExpression(e)&&x(e.argumentExpression)==="require"&&F(e.expression,t))throw g(i,r,e.getText());if(s.isIdentifier(e)&&e.text==="require"&&!k(e,t)&&!ae(e)&&!ce(e))throw g(i,r,"global require")}function g(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 F(e,t){return a().isIdentifier(e)&&(e.text==="globalThis"||e.text==="module")&&!k(e,t)}function ae(e){return a().isCallExpression(e.parent)&&e.parent.expression===e}function ce(e){const t=a(),i=e.parent;return ue(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 ue(e){const t=a(),i=e.parent;return i.name===e&&!t.isShorthandPropertyAssignment(i)&&!t.isExportSpecifier(i)}function le(e){const t=a(),i=e.propertyName;return i&&(t.isIdentifier(i)||t.isStringLiteralLike(i))?i.text:null}function q(e){const t=a(),i=new WeakMap,r=(n,o)=>{let u=i.get(n);u||(u=new Set,i.set(n,u)),N(u,o)},s=n=>{if(t.isImportDeclaration(n))pe(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?me(n):w(n);r(u,n.name)}else t.isParameter(n)?r(fe(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 pe(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 N(e,t){const i=a();if(i.isIdentifier(t)){e.add(t.text);return}for(const r of t.elements)i.isOmittedExpression(r)||N(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(de(t))return t;t=t.parent}}function me(e){const t=a();let i=e.parent;for(;;){if(t.isFunctionLike(i)||t.isSourceFile(i))return i;i=i.parent}}function fe(e){const t=a();let i=e.parent;for(;;){if(t.isFunctionLike(i)||t.isSourceFile(i))return i;i=i.parent}}function de(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=q(e),s=n=>{const o=ge(n,r);o&&i.add(o);for(const u of v(n))s(u);t.forEachChild(n,s)};return s(e),i}function v(e){return e.jsDoc??[]}function ge(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)||!D(e,t))return null;const r=e.arguments[0];return i.isStringLiteralLike(r)?r.text:null}function ye(e){const t=a();return t.isVariableStatement(e)?e.declarationList.declarations.reduce((i,r)=>i+C(r.name,"defineFile"),0):(t.isClassDeclaration(e)||t.isEnumDeclaration(e)||t.isFunctionDeclaration(e))&&e.name?.text==="defineFile"?1:0}function C(e,t){const i=a();return i.isIdentifier(e)?e.text===t?1:0:e.elements.reduce((r,s)=>i.isOmittedExpression(s)?r:r+C(s.name,t),0)}function xe(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(!L(i,r))throw new Error(`Test file metadata path escapes the product root: ${t}`);if(!y.existsSync(r))throw new Error(`Test file metadata source does not exist: ${t}`);const s=y.realpathSync(i),n=y.realpathSync(r);if(!L(s,n))throw new Error(`Test file metadata path resolves outside the product root: ${t}`);return n}function Se(e){return e.parseDiagnostics??[]}function Ee(e){const t=O.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"./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,5 +1,5 @@
1
1
  export { loadConfigContext, loadConfigs, parseDotenv, resolveProductDir, resolveServiceCwd, type LoadConfigContextOptions, type LoadConfigsOptions, type NormalizedConfigContext, type NormalizedLoadedDatabaseConfig, type NormalizedLocalServiceConfig, type NormalizedServiceConfig, type NormalizedServiceTestkitConfig, type NormalizedSuiteConfig, type ServiceSuites, } from "./load.js";
2
- export { discoverTests, loadDiscoveredConfigContext, fileDisplayName, formatDisplayName, testTypeContract, type DiscoverTestsOptions, type DiscoveryDiagnostic, type DiscoveryFile, type DiscoveryResult, type DiscoveryService, type DiscoverySuite, type DiscoverySummary, type DiscoveryTestContract, type DiscoveryTestType, } from "./discover-tests.js";
2
+ export { discoverTests, loadDiscoveredConfigContext, loadDiscoveryContext, type LoadedDiscoveryContext, fileDisplayName, formatDisplayName, testTypeContract, type DiscoverTestsOptions, type DiscoveryDiagnostic, type DiscoveryFile, type DiscoveryResult, type DiscoveryService, type DiscoverySuite, type DiscoverySummary, type DiscoveryTestContract, type DiscoveryTestType, } from "./discover-tests.js";
3
3
  export { findConfigFile, loadTestkitConfig } from "./config-loader.js";
4
4
  export { materializeDatabaseConfig, type DatabaseMaterializationInput, type MaterializedDatabaseConfig, } from "./database-materialization.js";
5
5
  export type { NormalizedDatabaseConfig, NormalizedDatabaseTemplateConfig, NormalizedSourceSchemaConfig, } from "./database.js";
@@ -8,3 +8,4 @@ export { DEFAULT_RUNTIME_ISOLATION_POLICY, resolveRuntimeIsolationPolicy, type R
8
8
  export { collectModuleSpecifiers, requireMatchingEngineAuthoringImports, type TestFileMetadata, } from "./file-metadata.js";
9
9
  export type { NormalizedSkipConfig } from "./skip-config.js";
10
10
  export { minimumConfirmingRuns } from "./cloud.js";
11
+ export { hasDiscoveryErrors, buildDiscoveryErrorFromDiagnostics } from "./discovery.js";
@@ -1 +1 @@
1
- export{loadConfigContext,loadConfigs,parseDotenv,resolveProductDir,resolveServiceCwd}from"./load.js";export{discoverTests,loadDiscoveredConfigContext,fileDisplayName,formatDisplayName,testTypeContract}from"./discover-tests.js";export{findConfigFile,loadTestkitConfig}from"./config-loader.js";export{materializeDatabaseConfig}from"./database-materialization.js";export{detectNextApp}from"./runtime.js";export{DEFAULT_RUNTIME_ISOLATION_POLICY,resolveRuntimeIsolationPolicy}from"./runtime-isolation.js";export{collectModuleSpecifiers,requireMatchingEngineAuthoringImports}from"./file-metadata.js";export{minimumConfirmingRuns}from"./cloud.js";
1
+ export{loadConfigContext,loadConfigs,parseDotenv,resolveProductDir,resolveServiceCwd}from"./load.js";export{discoverTests,loadDiscoveredConfigContext,loadDiscoveryContext,fileDisplayName,formatDisplayName,testTypeContract}from"./discover-tests.js";export{findConfigFile,loadTestkitConfig}from"./config-loader.js";export{materializeDatabaseConfig}from"./database-materialization.js";export{detectNextApp}from"./runtime.js";export{DEFAULT_RUNTIME_ISOLATION_POLICY,resolveRuntimeIsolationPolicy}from"./runtime-isolation.js";export{collectModuleSpecifiers,requireMatchingEngineAuthoringImports}from"./file-metadata.js";export{minimumConfirmingRuns}from"./cloud.js";export{hasDiscoveryErrors,buildDiscoveryErrorFromDiagnostics}from"./discovery.js";
@@ -1,3 +1,4 @@
1
+ import { type TestFileSelection } from "./test-selection.js";
1
2
  import { type EngineConfigurations } from "../../engines/catalog.js";
2
3
  import { type EngineId, type PublicTestType, type RunnerProfileId } from "../../domain/identifiers.js";
3
4
  import { type DiscoveredSuite, type DiscoveryOptions, type DiscoveryProject } from "./discovery.js";
@@ -63,6 +64,7 @@ export interface PreparedDiscovery {
63
64
  readonly fileMetadataByPath: Map<string, TestFileMetadata>;
64
65
  }
65
66
  export interface LoadConfigContextOptions {
67
+ testSelection?: TestFileSelection;
66
68
  preparedDiscovery?: PreparedDiscovery;
67
69
  configContext?: unknown;
68
70
  configImportCacheSalt?: string;
@@ -1 +1 @@
1
- import A from"path";import{buildEngineConfigurations as B}from"../../engines/catalog.js";import{isPublicTestType as K}from"../../domain/identifiers.js";import{discoverProject as G}from"./discovery.js";import{loadTestkitConfig as H}from"./config-loader.js";import{normalizeToolchainRegistry as J}from"../../toolchains/index.js";import{loadTestFileMetadataMap as Q}from"./file-metadata.js";import{mergeDiscoveryConfigs as V}from"./path-policy.js";import{normalizeDiscoveryConfig as I}from"./decode-discovery.js";import{normalizeFingerprintPolicy as W}from"../../repo/fingerprint-policy.js";import{normalizeDatabaseConfig as X}from"./database.js";import{inferEnvFiles as Y,loadServiceEnv as Z,parseDotenv as _}from"./env.js";import{normalizeRuntimeIsolationConfig as N}from"./runtime-isolation.js";import{resolveProductDir as M,resolveServiceCwd as ee}from"./paths.js";import{inferLocalRuntime as re,normalizeRepoExecution as oe,normalizeRuntimeConfig as te}from"./runtime.js";import{normalizeServiceRequirements as ne,normalizeSkipConfig as ie}from"./skip-config.js";import{normalizeArtifactArchiveConfig as se}from"../../evidence/index.js";import{validateConfigCoverage as ce,validateServiceConfig as fe}from"./validation.js";import{isObjectRecord as g,readProperty as i}from"./values.js";import{assertPortableServiceNameSet as ae,requireServiceName as de}from"../../domain/service-name.js";import{resolveEngineProfile as le}from"../../engines/catalog.js";export{_ as parseDotenv,M as resolveProductDir,ee as resolveServiceCwd};export async function loadConfigContext(e={}){const r=M(process.cwd(),e.dir),o=e.requireLocalRuntime!==!1,t=e.configContext||await H(r,{importCacheSalt:e.configImportCacheSalt??""}),{config:n,configFile:f}=he(t),d=oe(je(i(n,"execution"))),u=se(i(n,"artifacts")),a=J(i(n,"toolchains")),s=W(i(n,"fingerprints")),m=I(i(n,"discovery"),{allowRoots:!0}),c=be(i(n,"services")),p=e.preparedDiscovery?.project??G(r,ye(c),{...e.discoveryOptions||{},discovery:m}),$=Q(r,p.suitesByService,e.preparedDiscovery?.fileMetadataByPath),w=ue(p.suitesByService,$),b=[...new Set([...Object.keys(c),...Object.keys(w)])].sort((l,S)=>l.localeCompare(S)).map(l=>pe({name:l,productDir:r,config:n,configFile:f,execution:d,discovery:m,artifacts:u,toolchains:a,fingerprints:s,explicitService:c[l]||{},discoveredService:p.services[l]||null,suites:w[l]||{},fileMetadataByPath:$,requireLocalRuntime:o}));return ce(b),{productDir:r,config:n,configFile:f,execution:d,toolchains:a,fingerprints:s,explicitServices:c,discovery:p,configs:b}}function ue(e,r){return Object.fromEntries(Object.entries(e).map(([o,t])=>[o,me(o,t||{},r)]))}function me(e,r,o){const t={};for(const[n,f]of Object.entries(r)){if(!K(n))throw new Error(`Discovered service "${e}" contains unknown test type "${n}"`);t[n]=f.map(d=>{const u=d.files[0];if(!u)throw new Error(`Discovered suite ${e}/${n}:${d.name} contains no files`);const a=T(o,u),s=le(n,a.engineId);if(s.id!==a.profileId)throw new Error(`Discovered file ${u} resolved conflicting engine profile ${a.profileId}; expected ${s.id}`);for(const m of d.files.slice(1)){const c=T(o,m);if(c.engineId!==a.engineId||c.profileId!==a.profileId)throw new Error(`Discovered suite ${e}/${n}:${d.name} mixes engine profiles ${a.engineId}/${a.profileId} and ${c.engineId}/${c.profileId}`)}return{...d,testType:n,engineId:a.engineId,profileId:a.profileId}})}return t}function T(e,r){const o=e.get(r);if(!o)throw new Error(`Missing validated testkit metadata for discovered file: ${r}`);return o}export async function loadConfigs(e={}){const r=await loadConfigContext(e),{configs:o}=r,t=e.service?o.filter(n=>n.name===e.service):o;if(e.service&&t.length===0){const n=o.map(f=>f.name).join(", ");throw new Error(`Service "${e.service}" not found. Available: ${n}`)}return t}function pe({name:e,productDir:r,config:o,configFile:t,execution:n,discovery:f,artifacts:d,toolchains:u,fingerprints:a,explicitService:s,discoveredService:m,suites:c,fileMetadataByPath:p,requireLocalRuntime:$}){const w=B(o,new Set(Object.values(c).flatMap(y=>y.map(E=>E.engineId)))),v=ge(e,s,m,r),b=Y(r,s,v),l={...Z(r,b),...F(i(s,"env"),`Service "${e}" env`)},S=V(f,P(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 C=we(s,e),j=te(i(s,"runtime"),e,u),D=ie(i(s,"skip"),{name:e,suites:c}),L=ne(i(s,"requirements"),{name:e,suites:c}),q=N(i(s,"runtimeIsolation"),{name:e,suites:c}),x=new Set(Object.values(c).flatMap(y=>y.flatMap(E=>E.files))),U=new Map([...p.entries()].filter(([y])=>x.has(y))),O=$e(i(s,"dependsOn"),`Service "${e}" dependsOn`),z=h(i(s,"databaseFrom"),`Service "${e}" databaseFrom`);return fe({name:e,local:v,database:C,databaseFrom:z,runtime:j,dependsOn:O,suites:c,productDir:r,requireLocalRuntime:$}),{name:e,productDir:r,configFile:t,...w===void 0?{}:{engineConfigurations:w},stateDir:A.join(r,".testkit",e),artifacts:d,suites:c,testkit:{execution:n,dependsOn:O,discovery:S,database:C,databaseFrom:z,envFiles:b,requirements:L,serviceEnv:l,skip:D,runtime:j,runtimeIsolation:q,ui:i(o,"ui")||null,fingerprints:a,fileMetadataByPath:U,local:v}}}function ge(e,r,o,t){if(Object.prototype.hasOwnProperty.call(r,"local"))return k(i(r,"local"),e);const n=o?.inferredLocalCwd||".",f=re(t,n);if(f)return f}function k(e,r){if(e===!1)return;if(!g(e))throw new Error(`Service "${r}" local must be an object or false`);const o=R(e.baseUrl,`Service "${r}" local.baseUrl`),t=R(e.start,`Service "${r}" local.start`),{shutdownTimeoutMs:n,...f}=e;return{...f,...Ee(n,r),baseUrl:o,start:t,cwd:h(e.cwd,`Service "${r}" local.cwd`)||".",env:F(e.env,`Service "${r}" local.env`),...ve(e.port,`Service "${r}" local.port`),...Se(e.readyTimeoutMs,`Service "${r}" local.readyTimeoutMs`),...Ce(e.readyUrl,r)}}function we(e,r){const o=X(e,r);if(!o)return;const t=h(i(o,"image"),`Service "${r}" database.image`),n=h(i(o,"password"),`Service "${r}" database.password`),f=h(i(o,"user"),`Service "${r}" database.user`);return{...o,...Object.prototype.hasOwnProperty.call(o,"image")?{image:t}:{},...Object.prototype.hasOwnProperty.call(o,"password")?{password:n}:{},...Object.prototype.hasOwnProperty.call(o,"user")?{user:f}:{}}}function P(e,r){try{return I(e,{allowRoots:!0})}catch(o){const t=o instanceof Error?o.message:String(o);throw new Error(`Service "${r}" has invalid discovery config: ${t}`,{cause:o})}}function be(e){if(e==null)return{};if(!g(e))throw new Error("testkit.config.ts services must be an object");const r=Object.fromEntries(Object.entries(e).map(([o,t])=>{if(de(o,`Service name "${o}"`),t==null)return[o,{}];if(!g(t))throw new Error(`Service "${o}" config must be an object`);return[o,t]}));return ae(Object.keys(r)),r}function ye(e){return Object.fromEntries(Object.entries(e).map(([r,o])=>{const t=o||{},n={discovery:P(i(t,"discovery"),r),local:Object.prototype.hasOwnProperty.call(t,"local")?k(i(t,"local"),r)||!1:void 0};return[r,n]}))}function he(e){if(!g(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 o=e.configFile;if(o!==null&&typeof o!="string")throw new Error("Loaded Testkit config file must be a string or null");return{config:r,configFile:o}}function $e(e,r){if(e==null)return[];if(!Array.isArray(e))throw new Error(`${r} must be an array of strings`);return e.map((o,t)=>{if(typeof o!="string")throw new Error(`${r}[${String(t)}] must be a string`);return o})}function F(e,r){if(e==null)return{};if(!g(e))throw new Error(`${r} must be an object of strings`);return Object.fromEntries(Object.entries(e).map(([o,t])=>{if(typeof t!="string")throw new Error(`${r}.${o} must be a string`);return[o,t]}))}function ve(e,r){if(e===void 0)return{};if(typeof e!="number")throw new Error(`${r} must be a number`);return{port:e}}function Se(e,r){if(e===void 0)return{};if(typeof e!="number")throw new Error(`${r} must be a number`);return{readyTimeoutMs:e}}function Ee(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 Ce(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 h(e,r){if(e!==void 0){if(typeof e!="string")throw new Error(`${r} must be a string`);return e}}function R(e,r){if(typeof e!="string")throw new Error(`${r} must be a string`);return e}function je(e){if(e){if(!g(e))throw new Error("testkit.config.ts execution must be an object");return e}}
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,5 +1,7 @@
1
+ import type { TestFileSelection } from "./test-selection.js";
1
2
  import { type NormalizedServiceConfig } from "./load.js";
2
3
  export interface LoadManagedConfigsOptions {
4
+ testSelection?: TestFileSelection;
3
5
  dir?: string;
4
6
  service?: null | string;
5
7
  }
@@ -1 +1 @@
1
- import{loadConfigs as d}from"./load.js";export async function loadManagedConfigs(e={}){const o=await d({dir:e.dir}),a=filterConfigsByService(o,e.service||null);return{allConfigs:o,configs:a}}export function filterConfigsByService(e,o){if(!o)return e;const a=e.filter(t=>t.name===o);if(a.length>0)return a;const i=e.map(t=>t.name).join(", ");throw new Error(`Service "${o}" not found. Available: ${i}`)}export function resolveTargetConfig(e,o=null){if(o){const t=e.find(r=>r.name===o);if(!t){const r=e.map(n=>n.name).join(", ");throw new Error(`Service "${o}" not found. Available: ${r}`)}return t}if(e.length===1)return e[0];const a=e.filter(t=>t.testkit.database);if(a.length===1)return a[0];const i=e.map(t=>t.name).join(", ");throw new Error(`Multiple services available. Pass --service. Available: ${i}`)}export function collectRequiredConfigs(e,o){const a=new Map(e.map(r=>[r.name,r])),i=new Set,t=r=>{if(i.has(r))return;const n=a.get(r);if(!n)throw new Error(`Missing config for dependency "${r}"`);i.add(r);for(const s of n.testkit.dependsOn)t(s)};return t(o),e.filter(r=>i.has(r.name))}export function topologicallySortConfigs(e){const o=new Map(e.map(n=>[n.name,n])),a=new Set,i=new Set,t=[],r=n=>{if(!a.has(n.name)){if(i.has(n.name))throw new Error(`Dependency cycle while resolving configs for "${n.name}"`);i.add(n.name);for(const s of n.testkit.dependsOn){const l=o.get(s);l&&r(l)}i.delete(n.name),a.add(n.name),t.push(n)}};for(const n of e)r(n);return t}
1
+ import{loadConfigs as d}from"./load.js";export async function loadManagedConfigs(e={}){const o=await d({dir:e.dir,testSelection:e.testSelection}),a=filterConfigsByService(o,e.service||null);return{allConfigs:o,configs:a}}export function filterConfigsByService(e,o){if(!o)return e;const a=e.filter(n=>n.name===o);if(a.length>0)return a;const i=e.map(n=>n.name).join(", ");throw new Error(`Service "${o}" not found. Available: ${i}`)}export function resolveTargetConfig(e,o=null){if(o){const n=e.find(r=>r.name===o);if(!n){const r=e.map(t=>t.name).join(", ");throw new Error(`Service "${o}" not found. Available: ${r}`)}return n}if(e.length===1)return e[0];const a=e.filter(n=>n.testkit.database);if(a.length===1)return a[0];const i=e.map(n=>n.name).join(", ");throw new Error(`Multiple services available. Pass --service. Available: ${i}`)}export function collectRequiredConfigs(e,o){const a=new Map(e.map(r=>[r.name,r])),i=new Set,n=r=>{if(i.has(r))return;const t=a.get(r);if(!t)throw new Error(`Missing config for dependency "${r}"`);i.add(r);for(const s of t.testkit.dependsOn)n(s)};return n(o),e.filter(r=>i.has(r.name))}export function topologicallySortConfigs(e){const o=new Map(e.map(t=>[t.name,t])),a=new Set,i=new Set,n=[],r=t=>{if(!a.has(t.name)){if(i.has(t.name))throw new Error(`Dependency cycle while resolving configs for "${t.name}"`);i.add(t.name);for(const s of t.testkit.dependsOn){const l=o.get(s);l&&r(l)}i.delete(t.name),a.add(t.name),n.push(t)}};for(const t of e)r(t);return n}
@@ -2,4 +2,4 @@ export declare function resolveServiceCwd(productDir: string, maybeRelative?: st
2
2
  export declare function ensureExistingPath(productDir: string, relativePath: string, label: string): void;
3
3
  export declare function ensureExistingContainedPath(productDir: string, relativePath: string, label: string): void;
4
4
  export declare function normalizePath(value?: string | null): string;
5
- export declare function resolveProductDir(cwd: string, explicitDir?: string | null): string;
5
+ export { resolveProductDir } from "../../process/files/product-dir.js";
@@ -1 +1 @@
1
- import n from"fs";import s from"path";import{resolveContainedPath as i}from"../../process/files/contained-path.js";export function resolveServiceCwd(o,r){return s.resolve(o,r||".")}export function ensureExistingPath(o,r,e){const t=resolveServiceCwd(o,r);if(!n.existsSync(t))throw new Error(`${e} does not exist: ${r}`)}export function ensureExistingContainedPath(o,r,e){const t=i(o,r,e);if(!n.existsSync(t))throw new Error(`${e} does not exist: ${r}`)}export function normalizePath(o){return(o||"").replaceAll("\\","/").replace(/^\.\/+/,"")}export function resolveProductDir(o,r){const e=r?s.resolve(o,r):o;if(!n.existsSync(e))throw new Error(`Product directory does not exist: ${e}`);return e}
1
+ import n from"fs";import s from"path";import{resolveContainedPath as i}from"../../process/files/contained-path.js";export function resolveServiceCwd(o,r){return s.resolve(o,r||".")}export function ensureExistingPath(o,r,e){const t=resolveServiceCwd(o,r);if(!n.existsSync(t))throw new Error(`${e} does not exist: ${r}`)}export function ensureExistingContainedPath(o,r,e){const t=i(o,r,e);if(!n.existsSync(t))throw new Error(`${e} does not exist: ${r}`)}export function normalizePath(o){return(o||"").replaceAll("\\","/").replace(/^\.\/+/,"")}export{resolveProductDir}from"../../process/files/product-dir.js";
@@ -10,7 +10,7 @@ export interface SelectableSuite {
10
10
  export type ServiceSuiteCollection = Readonly<Partial<Record<PublicTestType, null | readonly SelectableSuite[] | undefined>>>;
11
11
  export interface SkipConfigNormalizationContext {
12
12
  name: string;
13
- suites: ServiceSuiteCollection;
13
+ suites: Readonly<Partial<Record<PublicTestType, null | readonly Pick<SelectableSuite, "name" | "testType" | "files">[] | undefined>>>;
14
14
  }
15
15
  export interface NormalizedSkipFileRule {
16
16
  path: string;
@@ -0,0 +1,15 @@
1
+ import { type SuiteSelector } from "../../domain/suite-selection.js";
2
+ import { type PublicTestType } from "../../domain/identifiers.js";
3
+ import type { DiscoveredSuitesByService, DiscoveredSuite } from "./discovery.js";
4
+ export interface TestFileSelection {
5
+ service?: null | string;
6
+ fileNames?: readonly string[];
7
+ typeValues?: readonly string[];
8
+ suiteSelectors?: readonly SuiteSelector[];
9
+ }
10
+ /** Select by discovered paths/types before reading engine metadata. */
11
+ export declare function selectDiscoveredSuites(discovered: DiscoveredSuitesByService, selection: Readonly<TestFileSelection>): DiscoveredSuitesByService;
12
+ /** Global configuration selectors still validate against every discovered path. */
13
+ export declare function discoveredRuleSuites(suites: Partial<Record<PublicTestType, DiscoveredSuite[]>>): Partial<Record<PublicTestType, Array<DiscoveredSuite & {
14
+ testType: PublicTestType;
15
+ }>>>;
@@ -0,0 +1 @@
1
+ import{matchesSelectedTypes as a,matchesSuiteSelectors as u}from"../../domain/suite-selection.js";import{isPublicTestType as p}from"../../domain/identifiers.js";export function selectDiscoveredSuites(t,e){const s=new Set(e.fileNames?.map(l));return Object.fromEntries(Object.entries(t).map(([i,o])=>[i,e.service&&i!==e.service?{}:Object.fromEntries(Object.entries(o??{}).filter(([n])=>a(n,e.typeValues??[])).map(([n,f])=>[n,f.filter(r=>u(n,r.name,e.suiteSelectors??[])).flatMap(r=>{const c=s.size?r.files.filter(m=>s.has(l(m))):r.files;return c.length?[{...r,files:c}]:[]})]))]))}export function discoveredRuleSuites(t){return Object.fromEntries(Object.entries(t).map(([e,s])=>{if(!p(e))throw new Error(`Unknown discovered test type ${e}`);return[e,s.map(i=>({...i,testType:e}))]}))}function l(t){return t.replaceAll("\\","/")}
@@ -1,8 +1,8 @@
1
+ export { cleanupOrphanedLocalInfrastructure, cleanupTerminalRunLocalInfrastructure } from "./lifecycle-cleanup.js";
1
2
  import { type NormalizedDatabaseConfig } from "../config/loader/index.js";
2
3
  import { type SourceSchemaServiceConfig } from "./schema-source.js";
3
4
  import { type UserHostLocalPostgresActivityOptions } from "./activity-gate.js";
4
5
  import { type LocalPostgresCleanupOptions, type LocalPostgresCleanupResult } from "./cleanup.js";
5
- import { type TerminalRunDatabaseCleanupAuthority } from "../process/runs/ownership.js";
6
6
  import type { DatabaseProgress } from "./progress.js";
7
7
  import { type ProductLifecycleFence } from "../process/lifecycle-fence.js";
8
8
  export { dropDatabaseWithForceOrDrain, isTransientAdminQueryConnectionError, waitForDatabaseConnectionsToDrain, } from "./admin.js";
@@ -34,8 +34,6 @@ export interface ReadyDatabase {
34
34
  export declare function provisionDatabase(config: Readonly<DatabaseRuntimeConfig>, options?: Readonly<ProvisionDatabaseOptions>): Promise<ReadyDatabase | null>;
35
35
  export declare function destroyRuntimeDatabase({ productDir, stateDir }: Readonly<RuntimeDatabaseStateLocation>, activityOptions?: Readonly<UserHostLocalPostgresActivityOptions>): Promise<void>;
36
36
  export declare function destroyServiceDatabaseCache(productDir: string, serviceName: string, activityOptions?: Readonly<UserHostLocalPostgresActivityOptions>): Promise<void>;
37
- export declare function cleanupOrphanedLocalInfrastructure(productDir: string, activityOptions?: Readonly<UserHostLocalPostgresActivityOptions>): Promise<LocalPostgresCleanupResult>;
38
- export declare function cleanupTerminalRunLocalInfrastructure(fence: Readonly<ProductLifecycleFence>, terminalRunAuthority: Readonly<TerminalRunDatabaseCleanupAuthority>, activityOptions?: Readonly<UserHostLocalPostgresActivityOptions>): Promise<LocalPostgresCleanupResult>;
39
37
  export declare function destroyOwnedLocalDatabaseResources(fence: Readonly<ProductLifecycleFence>, options?: Readonly<DatabaseCleanupOptions>): Promise<LocalPostgresCleanupResult>;
40
38
  export declare function cleanupOwnedLocalDatabaseResources(productDir: string, options?: Readonly<DatabaseCleanupOptions>): Promise<LocalPostgresCleanupResult>;
41
39
  export declare function isDatabaseStateDir(dir: string): boolean;
@@ -1 +1 @@
1
- import d from"fs";import u from"path";import{computeTemplateFingerprint as q}from"./fingerprint.js";import{buildDatabaseUrl as R,buildRuntimeDatabaseName as A,buildTemplateDatabaseName as V,hashString as B}from"./naming.js";import{getLocalInfraDir as x,getLocalLocksDir as N,getLocalServiceCacheDir as j,hasRemainingLocalArtifacts as k,requireContainedDatabaseStateDirectory as T,readStateValue as S}from"./state.js";import{createOwnedPgDumpClientImageResolver as U,runTemplateStage as W,runTemplateStep as z}from"./template-steps.js";import{materializeDatabaseConfig as H}from"../config/loader/index.js";import{applySourceSchemaCache as Q,createSourceSchemaMismatchError as G,forceRefreshSourceSchemaCache as J,prepareSourceSchemaCache as K,verifyLocalSchemaMatchesSource as X}from"./schema-source.js";import{cloneDatabaseFromTemplate as Y,createEmptyDatabase as Z,databaseExists as _,dropDatabaseIfExists as b}from"./admin.js";import{withUserHostLocalPostgresActivity as y}from"./activity-gate.js";import{ensureLocalContainer as ee,loadExistingLocalContainer as I}from"./local-postgres.js";import{writeCacheState as P,writeStateValue as h}from"./state-files.js";import{withProcessLock as L}from"../process/locks.js";import{ensureDatabaseRoles as te}from"./roles.js";import{assertTerminalRunDatabaseCleanupAuthority as ae,cleanupLocalPostgresDockerResources as re,cleanupTerminalRunLocalPostgresDockerResources as se,destroyLocalPostgresDockerResources as ne,hasActiveProductRuntime as ie}from"./cleanup.js";import"../process/runs/ownership.js";import{requireProductLifecycleFenceProductDir as g}from"../process/lifecycle-fence.js";export{dropDatabaseWithForceOrDrain,isTransientAdminQueryConnectionError,waitForDatabaseConnectionsToDrain}from"./admin.js";export{readDatabaseInfo,readDatabaseUrl}from"./state.js";export{refreshDatabaseSchema,verifyDatabaseSchema}from"./schema-operations.js";export async function provisionDatabase(e,t={}){const r=e.testkit.database;if(!r)return null;const s=H(r),a={...e,stateDir:T(e.productDir,e.stateDir,`Service "${e.name}" database runtime state directory`),testkit:{...e.testkit,database:s}},i={reporter:t.progress?.reporter||null,setupRegistry:t.progress?.setupRegistry||null,signal:t.signal,skipSchemaSourceVerify:t.skipSchemaSourceVerify};return y(a.productDir,async()=>(d.mkdirSync(a.stateDir,{recursive:!0}),ue(a,i)),{signal:t.signal})}export async function destroyRuntimeDatabase({productDir:e,stateDir:t},r={}){const s=T(e,t,"Runtime database state directory");await y(e,async()=>{await be(e,s)},r)}export async function destroyServiceDatabaseCache(e,t,r={}){await y(e,()=>ce(e,t),r)}async function ce(e,t){const r=j(e,t);if(!d.existsSync(r))return;const s=N(e);d.mkdirSync(s,{recursive:!0}),await L(u.join(s,`template-${t}.lock`),async()=>{const a=await I(e);if(!a){d.rmSync(r,{recursive:!0,force:!0});return}const i=S(u.join(r,"template_database_name"));i&&await b(a,i),d.rmSync(r,{recursive:!0,force:!0})})}export async function cleanupOrphanedLocalInfrastructure(e,t={}){const r=x(e);return v(e,r)?y(e,async()=>{if(!v(e,r)||ie(e))return D();const s=await F(e,{stopIdle:!0});return k(e)||d.rmSync(r,{recursive:!0,force:!0}),s},t):D()}export async function cleanupTerminalRunLocalInfrastructure(e,t,r={}){const s=g(e),a=x(s);return v(s,a)?y(s,async()=>{if(g(e),!v(s,a))return D();const i=await se(e,t,{stopIdle:!0});return k(s)||(ae(e,t),d.rmSync(a,{recursive:!0,force:!0})),i},r):D()}function v(e,t){return d.existsSync(t)||k(e)}function D(){return{kept:[],removed:[],skipped:[],stopped:[],targets:[]}}export async function destroyOwnedLocalDatabaseResources(e,t={}){const r=g(e);return y(r,()=>(g(e),ne(e,t)))}export async function cleanupOwnedLocalDatabaseResources(e,t={}){return y(e,()=>F(e,t))}async function F(e,t){return re({productDir:e,dryRun:!!t.dryRun,stopIdle:t.stopIdle!==!1})}export function isDatabaseStateDir(e){return d.existsSync(e)?d.existsSync(u.join(e,"database_backend")):!1}async function ue(e,t={}){const r=e.testkit.database,s=e.productDir,a=e.name,i=pe(e),c=N(s),m=j(s,a);d.mkdirSync(c,{recursive:!0}),d.mkdirSync(m,{recursive:!0});const n=await ee(s,r,{signal:t.signal}),p=await L(u.join(c,`template-${a}.lock`),async()=>{const o=await K(e,t),w=await M(e,{sourceSchemaState:o});return await te(e,n,t),oe(e,n,m,w,{...t,sourceSchemaState:o})},{signal:t.signal,timeoutMs:null}),l=await L(u.join(c,`runtime-${a}-${B(i,10)}.lock`),()=>le(e,n,m,p,i,t.signal),{signal:t.signal,timeoutMs:null});return{container:n.containerName,databaseName:l.databaseName,templateFingerprint:l.templateFingerprint,url:R(n,l.databaseName)}}async function oe(e,t,r,s,a={}){const i=e.name;let c=s,m=a.sourceSchemaState||null,n=!1;for(;;){const p=S(u.join(r,"template_fingerprint")),l=S(u.join(r,"template_database_name")),o=V(i,c);if(p===c&&l&&await _(t,l,{signal:a.signal}))return a.setupRegistry?.recordCached({config:e,stage:"template",kind:"database-template",summary:"template cache hit"}),P(r,t,l,c),c;l&&l!==o&&await b(t,l,{signal:a.signal}),await _(t,o,{signal:a.signal})&&await b(t,o,{signal:a.signal});const w=R(t,o);if(await Z(t,o,{signal:a.signal}),(await me(e,t,o,w,{...a,sourceSchemaState:m,refreshedSourceAfterMismatch:n})).refreshSourceSchema){await b(t,o,{signal:a.signal}),m=await J(e,m,a),n=!0,c=await M(e,{sourceSchemaState:m});continue}return P(r,t,o,c),c}}async function me(e,t,r,s,a={}){const i=a.setupRegistry?.start({config:e,stage:"template",kind:"database-template",summary:"template rebuild",recordLog:!1}),c={ownedContainer:{containerId:t.containerId,containerPort:t.containerPort},parentOperation:i,reporter:a.reporter||null,setupRegistry:a.setupRegistry||null,signal:a.signal},m=U(e,s,c);try{await Q(e,s,a.sourceSchemaState,c);const n=async()=>{const f=await X(e,s,a.sourceSchemaState,{...c,resolveLocalSnapshotImage:m,skipSchemaSourceVerify:a.skipSchemaSourceVerify});if(f.status!=="mismatch")return null;if(a.refreshedSourceAfterMismatch)throw G(e,he(a.sourceSchemaState),f);return f},p=e.testkit.database?.template,l=!!(p?.migrate?.length||p?.seed?.length),o=async f=>{const O=p?.[f]||[];for(const[$,E]of O.entries())if(await z(e,f,E,$,s,c),await n())return!0;return!1};return await o("migrate")||await o("seed")||!l&&!!await n()?(C(i,a,{status:"passed",summary:"source schema refresh requested"}),{refreshSourceSchema:!0}):(await W(e,"verify",s,c),C(i,a,{status:"passed",summary:"template rebuild"}),{refreshSourceSchema:!1})}catch(n){throw C(i,a,{status:"failed",summary:"template rebuild",error:fe(n)||n}),await b(t,r),n}}function C(e,t,r){const s=e&&t.setupRegistry?t.setupRegistry.finish(e,r):null;s&&t.reporter?.setupOperationFinished?.(s)}async function le(e,t,r,s,a,i){const c=e.name,m=S(u.join(r,"template_database_name"));if(!m)throw new Error(`Missing template database for service "${c}"`);const n=A(c,a,s),p=S(u.join(e.stateDir,"local_database_name")),l=S(u.join(e.stateDir,"local_template_fingerprint")),o=Se(e.testkit.database.reset)||l!==s||p!==n||!await _(t,n,{signal:i});return p&&p!==n&&await b(t,p,{signal:i}),o&&(await b(t,n,{signal:i}),await Y(t,n,m,{signal:i})),d.mkdirSync(e.stateDir,{recursive:!0}),de(e,t,{bindingKey:a,databaseName:n,templateFingerprint:s,templateDbName:m}),{bindingKey:a,databaseName:n,templateFingerprint:s,templateDbName:m}}function de(e,t,r){h(u.join(e.stateDir,"database_backend"),""),h(u.join(e.stateDir,"database_url"),R(t,r.databaseName)),h(u.join(e.stateDir,"local_database_name"),r.databaseName),h(u.join(e.stateDir,"local_template_fingerprint"),r.templateFingerprint),h(u.join(e.stateDir,"local_template_database_name"),r.templateDbName),h(u.join(e.stateDir,"local_container_name"),t.containerName),t.volumeName&&h(u.join(e.stateDir,"volume_name"),t.volumeName),t.volumeMountPath&&h(u.join(e.stateDir,"volume_mount_path"),t.volumeMountPath)}function pe(e){return ye(e.testkit.database.binding)==="shared"?`${e.name}:shared`:e.runtimeId?`${e.name}:${String(e.runtimeId)}`:`${e.name}:${e.stateDir}`}async function be(e,t){const r=S(u.join(t,"local_database_name"));if(!r)return;const s=await I(e);s&&await b(s,r)}function M(e,t={}){return q(e,t)}function he(e){if(!e?.active)throw new Error("Source schema mismatch requires active source schema state");return e}function fe(e){return typeof e!="object"&&typeof e!="function"||e===null?void 0:Reflect.get(e,"message")}function Se(e){return e!==!1}function ye(e){return e||"per-runtime"}function Oe(e){return e}
1
+ export{cleanupOrphanedLocalInfrastructure,cleanupTerminalRunLocalInfrastructure}from"./lifecycle-cleanup.js";import p from"fs";import u from"path";import{computeTemplateFingerprint as M}from"./fingerprint.js";import{buildDatabaseUrl as w,buildRuntimeDatabaseName as O,buildTemplateDatabaseName as P,hashString as $}from"./naming.js";import{getLocalLocksDir as R,getLocalServiceCacheDir as _,requireContainedDatabaseStateDirectory as N,readStateValue as y}from"./state.js";import{createOwnedPgDumpClientImageResolver as E,runTemplateStage as q,runTemplateStep as V}from"./template-steps.js";import{materializeDatabaseConfig as A}from"../config/loader/index.js";import{applySourceSchemaCache as B,createSourceSchemaMismatchError as U,forceRefreshSourceSchemaCache as W,prepareSourceSchemaCache as z,verifyLocalSchemaMatchesSource as H}from"./schema-source.js";import{cloneDatabaseFromTemplate as Q,createEmptyDatabase as G,databaseExists as g,dropDatabaseIfExists as b}from"./admin.js";import{withUserHostLocalPostgresActivity as D}from"./activity-gate.js";import{ensureLocalContainer as J,loadExistingLocalContainer as x}from"./local-postgres.js";import{writeCacheState as L,writeStateValue as h}from"./state-files.js";import{withProcessLock as v}from"../process/locks.js";import{ensureDatabaseRoles as K}from"./roles.js";import{cleanupLocalPostgresDockerResources as X,destroyLocalPostgresDockerResources as Y}from"./cleanup.js";import{requireProductLifecycleFenceProductDir as j}from"../process/lifecycle-fence.js";export{dropDatabaseWithForceOrDrain,isTransientAdminQueryConnectionError,waitForDatabaseConnectionsToDrain}from"./admin.js";export{readDatabaseInfo,readDatabaseUrl}from"./state.js";export{refreshDatabaseSchema,verifyDatabaseSchema}from"./schema-operations.js";export async function provisionDatabase(e,a={}){const r=e.testkit.database;if(!r)return null;const s=A(r),t={...e,stateDir:N(e.productDir,e.stateDir,`Service "${e.name}" database runtime state directory`),testkit:{...e.testkit,database:s}},c={reporter:a.progress?.reporter||null,setupRegistry:a.progress?.setupRegistry||null,signal:a.signal,skipSchemaSourceVerify:a.skipSchemaSourceVerify};return D(t.productDir,async()=>(p.mkdirSync(t.stateDir,{recursive:!0}),ae(t,c)),{signal:a.signal})}export async function destroyRuntimeDatabase({productDir:e,stateDir:a},r={}){const s=N(e,a,"Runtime database state directory");await D(e,async()=>{await ce(e,s)},r)}export async function destroyServiceDatabaseCache(e,a,r={}){await D(e,()=>Z(e,a),r)}async function Z(e,a){const r=_(e,a);if(!p.existsSync(r))return;const s=R(e);p.mkdirSync(s,{recursive:!0}),await v(u.join(s,`template-${a}.lock`),async()=>{const t=await x(e);if(!t){p.rmSync(r,{recursive:!0,force:!0});return}const c=y(u.join(r,"template_database_name"));c&&await b(t,c),p.rmSync(r,{recursive:!0,force:!0})})}export async function destroyOwnedLocalDatabaseResources(e,a={}){const r=j(e);return D(r,()=>(j(e),Y(e,a)))}export async function cleanupOwnedLocalDatabaseResources(e,a={}){return D(e,()=>ee(e,a))}async function ee(e,a){return X({productDir:e,dryRun:!!a.dryRun,stopIdle:a.stopIdle!==!1})}export function isDatabaseStateDir(e){return p.existsSync(e)?p.existsSync(u.join(e,"database_backend")):!1}async function ae(e,a={}){const r=e.testkit.database,s=e.productDir,t=e.name,c=ie(e),i=R(s),m=_(s,t);p.mkdirSync(i,{recursive:!0}),p.mkdirSync(m,{recursive:!0});const n=await J(s,r,{signal:a.signal}),d=await v(u.join(i,`template-${t}.lock`),async()=>{const o=await z(e,a),f=await C(e,{sourceSchemaState:o});return await K(e,n,a),te(e,n,m,f,{...a,sourceSchemaState:o})},{signal:a.signal,timeoutMs:null}),l=await v(u.join(i,`runtime-${t}-${$(c,10)}.lock`),()=>se(e,n,m,d,c,a.signal),{signal:a.signal,timeoutMs:null});return{container:n.containerName,databaseName:l.databaseName,templateFingerprint:l.templateFingerprint,url:w(n,l.databaseName)}}async function te(e,a,r,s,t={}){const c=e.name;let i=s,m=t.sourceSchemaState||null,n=!1;for(;;){const d=y(u.join(r,"template_fingerprint")),l=y(u.join(r,"template_database_name")),o=P(c,i);if(d===i&&l&&await g(a,l,{signal:t.signal}))return t.setupRegistry?.recordCached({config:e,stage:"template",kind:"database-template",summary:"template cache hit"}),L(r,a,l,i),i;l&&l!==o&&await b(a,l,{signal:t.signal}),await g(a,o,{signal:t.signal})&&await b(a,o,{signal:t.signal});const f=w(a,o);if(await G(a,o,{signal:t.signal}),(await re(e,a,o,f,{...t,sourceSchemaState:m,refreshedSourceAfterMismatch:n})).refreshSourceSchema){await b(a,o,{signal:t.signal}),m=await W(e,m,t),n=!0,i=await C(e,{sourceSchemaState:m});continue}return L(r,a,o,i),i}}async function re(e,a,r,s,t={}){const c=t.setupRegistry?.start({config:e,stage:"template",kind:"database-template",summary:"template rebuild",recordLog:!1}),i={ownedContainer:{containerId:a.containerId,containerPort:a.containerPort},parentOperation:c,reporter:t.reporter||null,setupRegistry:t.setupRegistry||null,signal:t.signal},m=E(e,s,i);try{await B(e,s,t.sourceSchemaState,i);const n=async()=>{const S=await H(e,s,t.sourceSchemaState,{...i,resolveLocalSnapshotImage:m,skipSchemaSourceVerify:t.skipSchemaSourceVerify});if(S.status!=="mismatch")return null;if(t.refreshedSourceAfterMismatch)throw U(e,ue(t.sourceSchemaState),S);return S},d=e.testkit.database?.template,l=!!(d?.migrate?.length||d?.seed?.length),o=async S=>{const T=d?.[S]||[];for(const[F,I]of T.entries())if(await V(e,S,I,F,s,i),await n())return!0;return!1};return await o("migrate")||await o("seed")||!l&&!!await n()?(k(c,t,{status:"passed",summary:"source schema refresh requested"}),{refreshSourceSchema:!0}):(await q(e,"verify",s,i),k(c,t,{status:"passed",summary:"template rebuild"}),{refreshSourceSchema:!1})}catch(n){throw k(c,t,{status:"failed",summary:"template rebuild",error:oe(n)||n}),await b(a,r),n}}function k(e,a,r){const s=e&&a.setupRegistry?a.setupRegistry.finish(e,r):null;s&&a.reporter?.setupOperationFinished?.(s)}async function se(e,a,r,s,t,c){const i=e.name,m=y(u.join(r,"template_database_name"));if(!m)throw new Error(`Missing template database for service "${i}"`);const n=O(i,t,s),d=y(u.join(e.stateDir,"local_database_name")),l=y(u.join(e.stateDir,"local_template_fingerprint")),o=me(e.testkit.database.reset)||l!==s||d!==n||!await g(a,n,{signal:c});return d&&d!==n&&await b(a,d,{signal:c}),o&&(await b(a,n,{signal:c}),await Q(a,n,m,{signal:c})),p.mkdirSync(e.stateDir,{recursive:!0}),ne(e,a,{bindingKey:t,databaseName:n,templateFingerprint:s,templateDbName:m}),{bindingKey:t,databaseName:n,templateFingerprint:s,templateDbName:m}}function ne(e,a,r){h(u.join(e.stateDir,"database_backend"),""),h(u.join(e.stateDir,"database_url"),w(a,r.databaseName)),h(u.join(e.stateDir,"local_database_name"),r.databaseName),h(u.join(e.stateDir,"local_template_fingerprint"),r.templateFingerprint),h(u.join(e.stateDir,"local_template_database_name"),r.templateDbName),h(u.join(e.stateDir,"local_container_name"),a.containerName),a.volumeName&&h(u.join(e.stateDir,"volume_name"),a.volumeName),a.volumeMountPath&&h(u.join(e.stateDir,"volume_mount_path"),a.volumeMountPath)}function ie(e){return le(e.testkit.database.binding)==="shared"?`${e.name}:shared`:e.runtimeId?`${e.name}:${String(e.runtimeId)}`:`${e.name}:${e.stateDir}`}async function ce(e,a){const r=y(u.join(a,"local_database_name"));if(!r)return;const s=await x(e);s&&await b(s,r)}function C(e,a={}){return M(e,a)}function ue(e){if(!e?.active)throw new Error("Source schema mismatch requires active source schema state");return e}function oe(e){return typeof e!="object"&&typeof e!="function"||e===null?void 0:Reflect.get(e,"message")}function me(e){return e!==!1}function le(e){return e||"per-runtime"}function Le(e){return e}
@@ -0,0 +1,7 @@
1
+ import { type UserHostLocalPostgresActivityOptions } from "./activity-gate.js";
2
+ import { type ProductLifecycleFence } from "../process/lifecycle-fence.js";
3
+ import type { TerminalRunDatabaseCleanupAuthority } from "../process/runs/ownership.js";
4
+ import type { LocalPostgresCleanupResult } from "./cleanup.js";
5
+ export declare function cleanupOrphanedLocalInfrastructure(productDir: string, activityOptions?: Readonly<UserHostLocalPostgresActivityOptions>): Promise<LocalPostgresCleanupResult>;
6
+ export declare function cleanupOrphanedLocalInfrastructureUnlocked(productDir: string): Promise<LocalPostgresCleanupResult>;
7
+ export declare function cleanupTerminalRunLocalInfrastructure(fence: Readonly<ProductLifecycleFence>, terminalRunAuthority: Readonly<TerminalRunDatabaseCleanupAuthority>, activityOptions?: Readonly<UserHostLocalPostgresActivityOptions>): Promise<LocalPostgresCleanupResult>;
@@ -0,0 +1 @@
1
+ import c from"node:fs";import{getLocalInfraDir as u,hasRemainingLocalArtifacts as i}from"./state.js";import{withUserHostLocalPostgresActivity as l}from"./activity-gate.js";import{requireProductLifecycleFenceProductDir as f}from"../process/lifecycle-fence.js";export async function cleanupOrphanedLocalInfrastructure(r,e={}){const n=u(r);return s(r,n)?l(r,()=>cleanupOrphanedLocalInfrastructureUnlocked(r),e):o()}export async function cleanupOrphanedLocalInfrastructureUnlocked(r){const e=u(r);if(!s(r,e))return o();const{hasActiveProductRuntime:n,cleanupLocalPostgresDockerResources:t}=await import("./cleanup.js");if(n(r))return o();const a=await t({productDir:r,dryRun:!1,stopIdle:!0});return i(r)||c.rmSync(e,{recursive:!0,force:!0}),a}export async function cleanupTerminalRunLocalInfrastructure(r,e,n={}){const t=f(r),a=u(t);return s(t,a)?l(t,async()=>{if(f(r),!s(t,a))return o();const{cleanupTerminalRunLocalPostgresDockerResources:p,assertTerminalRunDatabaseCleanupAuthority:m}=await import("./cleanup.js"),y=await p(r,e,{stopIdle:!0});return i(t)||(m(r,e),c.rmSync(a,{recursive:!0,force:!0})),y},n):o()}function s(r,e){return c.existsSync(e)||i(r)}function o(){return{kept:[],removed:[],skipped:[],stopped:[],targets:[]}}
@@ -1,2 +1,2 @@
1
- import{runDockerMetadataCommand as re}from"./docker-metadata.js";import{randomUUID as C}from"node:crypto";import oe from"fs";import u from"path";import{LOCAL_ADMIN_DB as D,LOCAL_IMAGE as M,LOCAL_MAX_CONNECTIONS as N,LOCAL_PASSWORD as R,LOCAL_POLL_INTERVAL_MS as ie,LOCAL_READY_TIMEOUT_MS as $,LOCAL_USER as j}from"./constants.js";import{buildContainerName as se,buildVolumeName as S}from"./naming.js";import{getLocalInfraDir as V,readStateValue as l}from"./state.js";import{TESTKIT_CREATED_AT_LABEL as U,TESTKIT_RESOURCE_GENERATION_LABEL as z,buildDockerResourceLabels as G,dockerLabelArgs as v,inspectDockerVolume as L,normalizeDockerVolumeInspect as E,removeDockerVolume as ae,runDockerCommand as h}from"./docker.js";import{writeLocalInfraState as ce}from"./state-files.js";const ue="/var/lib/postgresql/data",le="/var/lib/postgresql",A=String;export const LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS=Object.freeze({inspect:3e4,readinessAttempt:1e4,remove:12e4,run:10*6e4,start:12e4,volumeCreate:12e4});export async function ensureLocalContainer(e,n={},t={}){const o=V(e);oe.mkdirSync(o,{recursive:!0});const r=l(u.join(o,"container_name"))||se(e),i=l(u.join(o,"volume_name")),s=n.image||l(u.join(o,"image"))||M,c=l(u.join(o,"image")),f=resolveLocalPostgresVolumeTarget(s),w=n.user||l(u.join(o,"user"))||j,b=n.password||l(u.join(o,"password"))||R,_=q(n.maxConnections||l(u.join(o,"max_connections"))||N);let a=await inspectContainer(r,t);a&&p(a,e,r);const ne=!a&&!i;let d=W(a,f)||i||S(e,C());a&&await K(e,d,t);const O=a?.Config?.Image||c||null;if(a&&!isReusableLocalPostgresContainer(a,{image:s,maxConnections:_,volumeMountPath:f,volumeName:d})&&(await stopAndRemoveContainer(a.Id,r,t),O&&O!==s&&(await we(e,d,t),d=S(e,C())),a=null),a)a.State?.Running||(await k(["start",a.Id],`Docker local Postgres container start for ${r}`,LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.start,t.signal),a=await inspectContainer(r,t),a&&p(a,e,r));else{d=ne?await F(e,t):await de(e,d,t);const te=J(e,r,C());await k(buildDockerRunArgs({containerName:r,labels:te,volumeName:d,volumeMountPath:f,user:w,password:b,image:s,maxConnections:_}),`Docker local Postgres container creation for ${r}`,LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.run,t.signal),a=await inspectContainer(r,t),a&&p(a,e,r)}const I=B(a);if(!a||!I)throw new Error(`Could not determine host port for local database container ${r}`);const x={containerName:r,containerId:a.Id,volumeName:d,volumeMountPath:f,image:s,user:w,password:b,maxConnections:_,host:"127.0.0.1",port:Number(I),containerPort:5432};return await Y(x,t),ce(o,x),x}export function buildDockerRunArgs({containerName:e,labels:n,volumeName:t,volumeMountPath:o,user:r,password:i,image:s,maxConnections:c}){return["run","-d","--name",e,...v(n),"-p","127.0.0.1::5432","--mount",`type=volume,source=${t},target=${o}`,"-e",`POSTGRES_USER=${r}`,"-e",`POSTGRES_PASSWORD=${i}`,"-e",`POSTGRES_DB=${D}`,s,"postgres","-c",`max_connections=${String(c)}`]}export async function loadExistingLocalContainer(e,n={}){const t=V(e),o=l(u.join(t,"container_name"));if(!o)return null;const r=await inspectContainer(o,n);if(!r)return null;p(r,e,o);const i=r.Config?.Image||l(u.join(t,"image"))||M,s=l(u.join(t,"volume_mount_path"))||resolveLocalPostgresVolumeTarget(i),c=W(r,s)||l(u.join(t,"volume_name"));if(!c)return null;await K(e,c,n),r.State?.Running||await k(["start",r.Id],`Docker local Postgres container start for ${o}`,LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.start,n.signal);const f=await inspectContainer(o,n);f&&p(f,e,o);const w=B(f);if(!f||!w)return null;const b={containerName:o,containerId:f.Id,volumeName:c,volumeMountPath:s,image:i,user:l(u.join(t,"user"))||j,password:l(u.join(t,"password"))||R,maxConnections:q(l(u.join(t,"max_connections"))||N),host:"127.0.0.1",port:Number(w),containerPort:H(f)==="host"?Number(w):5432};return await Y(b,n),b}function fe(e,n){const t=e.Config?.Cmd;return Array.isArray(t)?t.includes("-c")&&t.includes(`max_connections=${String(n)}`):!1}function me(e,n,t){return(Array.isArray(e.Mounts)?e.Mounts:[]).some(r=>r?.Type==="volume"&&r.Name===n&&r.Destination===t)}export function isReusableLocalPostgresContainer(e,n){return e.Config?.Image===n.image&&fe(e,n.maxConnections)&&me(e,n.volumeName,n.volumeMountPath)&&H(e)==="bridge"}function H(e){return e.HostConfig?.NetworkMode==="host"?"host":"bridge"}function B(e){return e?.NetworkSettings?.Ports?.["5432/tcp"]?.[0]?.HostPort||null}function q(e){const n=Number(e);if(!Number.isInteger(n)||n<10)throw new Error("database.maxConnections must be an integer greater than or equal to 10");return n}export async function inspectContainer(e,n={}){const t=`Docker local Postgres container inspect for ${e}`,o=["inspect",e],r=await re(o,t,LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.inspect,n.signal,c=>h(o,t,c,n.signal));if(r.exitCode!==0){if(ee(r))return null;throw T(t,r)}let i;try{i=JSON.parse(r.stdout)}catch(c){throw Z(t,r,c)}const s=P(i)&&i.length===1?normalizeLocalPostgresContainerInspect(i[0]):null;if(!s?.Id)throw Z(t,r);return{...s,Id:s.Id}}export function normalizeLocalPostgresContainerInspect(e){return m(e)?{Config:Ce(e.Config),HostConfig:he(e.HostConfig),Id:g(e.Id),Mounts:ye(e.Mounts),NetworkSettings:Pe(e.NetworkSettings),State:ke(e.State)}:null}export async function stopAndRemoveContainer(e,n=e,t={}){const o=`Docker local Postgres container removal for ${n}`,r=await h(["rm","-f","-v",e],o,LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.remove,t.signal);if(!(r.exitCode===0||ee(r)))throw T(o,r)}async function de(e,n,t={}){const o=y(e,n,"existing-generation"),r=await L(n,t);if(r){if(!matchesStableDockerLabels(Q(r)||{},o))throw new Error(`Docker volume ${n} already exists but is not owned by this Testkit product. Remove or rename it before running Testkit.`);return n}return F(e,t)}async function F(e,n){const t=C(),o=S(e,t),r=y(e,o,t);await k(["volume","create",...v(r),o],`Docker local Postgres volume creation for ${o}`,LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.volumeCreate,n.signal);const i=E(await L(o,n));if(i===null||!ge(i.labels,r))throw new Error(`Docker volume ${o} was not published with the expected Testkit generation identity`);return o}async function K(e,n,t){const o=y(e,n,"existing-generation"),r=E(await L(n,t));if(r===null||!matchesStableDockerLabels(r.labels,o))throw new Error(`Docker volume ${n} is missing or is not owned by this Testkit product`);return r}export function matchesStableDockerLabels(e,n){const t=e[U],o=e[z];return typeof t=="string"&&t.length>0&&typeof o=="string"&&o.length>0&&Object.entries(n).every(([r,i])=>r===U||r===z||e[r]===A(i))}function ge(e,n){const t=Object.entries(e).sort(([r],[i])=>r.localeCompare(i)),o=Object.entries(n).map(([r,i])=>[r,A(i)]).sort(([r],[i])=>r.localeCompare(i));return t.length===o.length&&t.every(([r,i],s)=>o[s]?.[0]===r&&o[s]?.[1]===i)}async function we(e,n,t={}){const o=y(e,n,"existing-generation"),r=await L(n,t);if(!r)return;const i=Q(r)||{};if(!matchesStableDockerLabels(i,o))return;const c=E(r);c!==null&&await ae(n,c,t)}function W(e,n){return(Array.isArray(e?.Mounts)?e.Mounts:[]).find(r=>r?.Type==="volume"&&typeof r.Name=="string"&&r.Name.length>0&&r.Destination===n)?.Name||null}function p(e,n,t){const o=J(n,t);if(!matchesStableDockerLabels(e.Config?.Labels||{},o))throw new Error(`Docker container ${t} already exists but is not owned by this Testkit product. Remove or rename it before running Testkit.`)}export function resolveLocalPostgresVolumeTarget(e){return be(e)>=18?le:ue}function be(e){const t=A(e||"").match(/(?:^|[:@/-])(?:pg|postgres)?(\d{2})(?:\D|$)/i);return t?Number(t[1]):16}async function Y(e,n={}){const t=Date.now();for(;Date.now()-t<$;){const o=Math.max(1,$-(Date.now()-t));if((await h(["exec",e.containerId,"pg_isready","-U",e.user,"-d",D,"-p",String(e.containerPort||5432)],`Docker local Postgres readiness probe for ${e.containerName}`,Math.min(LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.readinessAttempt,o),n.signal)).exitCode===0)return;await pe(Math.min(ie,o),n.signal)}throw new Error(`Timed out waiting for local database container ${e.containerName}`)}function J(e,n,t="existing-generation"){return G(e,{generation:t,kind:"postgres-container",name:n,scope:"local-postgres",cachePolicy:"product-cache"})}function y(e,n,t){return G(e,{generation:t,kind:"postgres-volume",name:n,scope:"local-postgres",cachePolicy:"product-cache"})}function pe(e,n){return new Promise((t,o)=>{if(n?.aborted){o(X(n));return}const r=setTimeout(i,e);n?.addEventListener("abort",s,{once:!0});function i(){n?.removeEventListener("abort",s),t()}function s(){clearTimeout(r),o(n?X(n):new Error("Local Postgres operation aborted"))}})}function X(e){return e.reason instanceof Error?e.reason:new Error("Local Postgres operation aborted",{cause:e.reason})}function Q(e){if(!m(e))return null;const n=e.Labels;return m(n)?n:null}function Ce(e){if(e===null)return null;if(m(e))return{Cmd:_e(e.Cmd),Image:g(e.Image),Labels:Le(e.Labels)}}function Le(e){if(!m(e))return null;const n={};for(const[t,o]of Object.entries(e))typeof o=="string"&&(n[t]=o);return n}function he(e){if(e===null)return null;if(m(e))return{NetworkMode:g(e.NetworkMode)}}function ye(e){if(e===null)return null;if(P(e))return e.flatMap(n=>m(n)?[{Destination:g(n.Destination),Name:g(n.Name),Type:g(n.Type)}]:[])}function Pe(e){if(e===null)return null;if(!m(e))return;const n=e.Ports;if(n===null)return{Ports:null};if(!m(n))return{};const t={};for(const[o,r]of Object.entries(n)){if(r===null){t[o]=null;continue}P(r)&&(t[o]=r.flatMap(i=>{if(!m(i))return[];const s=g(i.HostPort);return i.HostPort!==void 0&&s===void 0?[]:[{HostPort:s}]}))}return{Ports:t}}function ke(e){if(e===null)return null;if(m(e))return{Running:e.Running===null||typeof e.Running=="boolean"?e.Running:void 0}}function g(e){if(e===null||typeof e=="string")return e}function m(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function P(e){return Array.isArray(e)}function _e(e){if(e===null)return null;if(P(e))return e.filter(n=>typeof n=="string")}async function k(e,n,t,o){const r=await h(e,n,t,o);if(r.exitCode===0)return r;throw T(n,r)}function T(e,n){const t=n.stderr.trim()||n.stdout.trim(),o=new Error(`${e} failed with exit code ${String(n.exitCode)}${t?`: ${t}`:""}`);return Object.defineProperty(o,"result",{enumerable:!1,value:Object.freeze({...n})}),o}function Z(e,n,t){const o=new Error(`${e} returned malformed Docker inspect output; expected exactly one container record with a non-empty Id`,t===void 0?void 0:{cause:t});return Object.defineProperty(o,"result",{enumerable:!1,value:Object.freeze({...n})}),o}function ee(e){return/\bno such (?:container|object)\b/iu.test(`${e.stderr}
1
+ import{recordWork as M}from"../kernel/work-metrics.js";import{runDockerMetadataCommand as oe}from"./docker-metadata.js";import{randomUUID as C}from"node:crypto";import ie from"fs";import u from"path";import{LOCAL_ADMIN_DB as D,LOCAL_IMAGE as R,LOCAL_MAX_CONNECTIONS as N,LOCAL_PASSWORD as $,LOCAL_POLL_INTERVAL_MS as ae,LOCAL_READY_TIMEOUT_MS as j,LOCAL_USER as V}from"./constants.js";import{buildContainerName as se,buildVolumeName as x}from"./naming.js";import{getLocalInfraDir as U,readStateValue as l}from"./state.js";import{TESTKIT_CREATED_AT_LABEL as z,TESTKIT_RESOURCE_GENERATION_LABEL as G,buildDockerResourceLabels as v,dockerLabelArgs as H,inspectDockerVolume as h,normalizeDockerVolumeInspect as E,removeDockerVolume as ce,runDockerCommand as L}from"./docker.js";import{writeLocalInfraState as ue}from"./state-files.js";const le="/var/lib/postgresql/data",fe="/var/lib/postgresql",A=String;export const LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS=Object.freeze({inspect:3e4,readinessAttempt:1e4,remove:12e4,run:10*6e4,start:12e4,volumeCreate:12e4});export async function ensureLocalContainer(e,n={},r={}){const o=U(e);ie.mkdirSync(o,{recursive:!0});const t=l(u.join(o,"container_name"))||se(e),i=l(u.join(o,"volume_name")),a=n.image||l(u.join(o,"image"))||R,c=l(u.join(o,"image")),f=resolveLocalPostgresVolumeTarget(a),w=n.user||l(u.join(o,"user"))||V,b=n.password||l(u.join(o,"password"))||$,_=F(n.maxConnections||l(u.join(o,"max_connections"))||N);let s=await inspectContainer(t,r);s&&p(s,e,t);const te=!s&&!i;let d=Y(s,f)||i||x(e,C());s&&await W(e,d,r);const T=s?.Config?.Image||c||null;if(s&&!isReusableLocalPostgresContainer(s,{image:a,maxConnections:_,volumeMountPath:f,volumeName:d})&&(await stopAndRemoveContainer(s.Id,t,r),T&&T!==a&&(await be(e,d,r),d=x(e,C())),s=null),s)s.State?.Running||(await k(["start",s.Id],`Docker local Postgres container start for ${t}`,LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.start,r.signal),s=await inspectContainer(t,r),s&&p(s,e,t));else{d=te?await K(e,r):await ge(e,d,r);const re=X(e,t,C());await k(buildDockerRunArgs({containerName:t,labels:re,volumeName:d,volumeMountPath:f,user:w,password:b,image:a,maxConnections:_}),`Docker local Postgres container creation for ${t}`,LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.run,r.signal),s=await inspectContainer(t,r),s&&p(s,e,t)}const O=q(s);if(!s||!O)throw new Error(`Could not determine host port for local database container ${t}`);const S={containerName:t,containerId:s.Id,volumeName:d,volumeMountPath:f,image:a,user:w,password:b,maxConnections:_,host:"127.0.0.1",port:Number(O),containerPort:5432};return await J(S,r),ue(o,S),S}export function buildDockerRunArgs({containerName:e,labels:n,volumeName:r,volumeMountPath:o,user:t,password:i,image:a,maxConnections:c}){return["run","-d","--name",e,...H(n),"-p","127.0.0.1::5432","--mount",`type=volume,source=${r},target=${o}`,"-e",`POSTGRES_USER=${t}`,"-e",`POSTGRES_PASSWORD=${i}`,"-e",`POSTGRES_DB=${D}`,a,"postgres","-c",`max_connections=${String(c)}`]}export async function loadExistingLocalContainer(e,n={}){const r=U(e),o=l(u.join(r,"container_name"));if(!o)return null;const t=await inspectContainer(o,n);if(!t)return null;p(t,e,o);const i=t.Config?.Image||l(u.join(r,"image"))||R,a=l(u.join(r,"volume_mount_path"))||resolveLocalPostgresVolumeTarget(i),c=Y(t,a)||l(u.join(r,"volume_name"));if(!c)return null;await W(e,c,n),t.State?.Running||await k(["start",t.Id],`Docker local Postgres container start for ${o}`,LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.start,n.signal);const f=t.State?.Running?t:await inspectContainer(t.Id,n);f&&p(f,e,o);const w=q(f);if(!f||!w)return null;const b={containerName:o,containerId:f.Id,volumeName:c,volumeMountPath:a,image:i,user:l(u.join(r,"user"))||V,password:l(u.join(r,"password"))||$,maxConnections:F(l(u.join(r,"max_connections"))||N),host:"127.0.0.1",port:Number(w),containerPort:B(f)==="host"?Number(w):5432};return await J(b,n),b}function me(e,n){const r=e.Config?.Cmd;return Array.isArray(r)?r.includes("-c")&&r.includes(`max_connections=${String(n)}`):!1}function de(e,n,r){return(Array.isArray(e.Mounts)?e.Mounts:[]).some(t=>t?.Type==="volume"&&t.Name===n&&t.Destination===r)}export function isReusableLocalPostgresContainer(e,n){return e.Config?.Image===n.image&&me(e,n.maxConnections)&&de(e,n.volumeName,n.volumeMountPath)&&B(e)==="bridge"}function B(e){return e.HostConfig?.NetworkMode==="host"?"host":"bridge"}function q(e){return e?.NetworkSettings?.Ports?.["5432/tcp"]?.[0]?.HostPort||null}function F(e){const n=Number(e);if(!Number.isInteger(n)||n<10)throw new Error("database.maxConnections must be an integer greater than or equal to 10");return n}export async function inspectContainer(e,n={}){M("database.containerInspections");const r=`Docker local Postgres container inspect for ${e}`,o=["inspect",e],t=await oe(o,r,LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.inspect,n.signal,c=>L(o,r,c,n.signal));if(t.exitCode!==0){if(ne(t))return null;throw I(r,t)}let i;try{i=JSON.parse(t.stdout)}catch(c){throw ee(r,t,c)}const a=P(i)&&i.length===1?normalizeLocalPostgresContainerInspect(i[0]):null;if(!a?.Id)throw ee(r,t);return{...a,Id:a.Id}}export function normalizeLocalPostgresContainerInspect(e){return m(e)?{Config:he(e.Config),HostConfig:ye(e.HostConfig),Id:g(e.Id),Mounts:Pe(e.Mounts),NetworkSettings:ke(e.NetworkSettings),State:_e(e.State)}:null}export async function stopAndRemoveContainer(e,n=e,r={}){const o=`Docker local Postgres container removal for ${n}`,t=await L(["rm","-f","-v",e],o,LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.remove,r.signal);if(!(t.exitCode===0||ne(t)))throw I(o,t)}async function ge(e,n,r={}){const o=y(e,n,"existing-generation"),t=await h(n,r);if(t){if(!matchesStableDockerLabels(Z(t)||{},o))throw new Error(`Docker volume ${n} already exists but is not owned by this Testkit product. Remove or rename it before running Testkit.`);return n}return K(e,r)}async function K(e,n){const r=C(),o=x(e,r),t=y(e,o,r);await k(["volume","create",...H(t),o],`Docker local Postgres volume creation for ${o}`,LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.volumeCreate,n.signal);const i=E(await h(o,n));if(i===null||!we(i.labels,t))throw new Error(`Docker volume ${o} was not published with the expected Testkit generation identity`);return o}async function W(e,n,r){const o=y(e,n,"existing-generation"),t=E(await h(n,r));if(t===null||!matchesStableDockerLabels(t.labels,o))throw new Error(`Docker volume ${n} is missing or is not owned by this Testkit product`);return t}export function matchesStableDockerLabels(e,n){const r=e[z],o=e[G];return typeof r=="string"&&r.length>0&&typeof o=="string"&&o.length>0&&Object.entries(n).every(([t,i])=>t===z||t===G||e[t]===A(i))}function we(e,n){const r=Object.entries(e).sort(([t],[i])=>t.localeCompare(i)),o=Object.entries(n).map(([t,i])=>[t,A(i)]).sort(([t],[i])=>t.localeCompare(i));return r.length===o.length&&r.every(([t,i],a)=>o[a]?.[0]===t&&o[a]?.[1]===i)}async function be(e,n,r={}){const o=y(e,n,"existing-generation"),t=await h(n,r);if(!t)return;const i=Z(t)||{};if(!matchesStableDockerLabels(i,o))return;const c=E(t);c!==null&&await ce(n,c,r)}function Y(e,n){return(Array.isArray(e?.Mounts)?e.Mounts:[]).find(t=>t?.Type==="volume"&&typeof t.Name=="string"&&t.Name.length>0&&t.Destination===n)?.Name||null}function p(e,n,r){const o=X(n,r);if(!matchesStableDockerLabels(e.Config?.Labels||{},o))throw new Error(`Docker container ${r} already exists but is not owned by this Testkit product. Remove or rename it before running Testkit.`)}export function resolveLocalPostgresVolumeTarget(e){return pe(e)>=18?fe:le}function pe(e){const r=A(e||"").match(/(?:^|[:@/-])(?:pg|postgres)?(\d{2})(?:\D|$)/i);return r?Number(r[1]):16}async function J(e,n={}){M("database.hostReadinessChecks");const r=Date.now();let o=10;for(;Date.now()-r<j;){const t=Math.max(1,j-(Date.now()-r));if((await L(["exec",e.containerId,"pg_isready","-h","127.0.0.1","-U",e.user,"-d",D,"-p",String(e.containerPort||5432)],`Docker local Postgres readiness probe for ${e.containerName}`,Math.min(LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.readinessAttempt,t),n.signal)).exitCode===0)return;await Ce(Math.min(o,t),n.signal),o=Math.min(o*2,ae)}throw new Error(`Timed out waiting for local database container ${e.containerName}`)}function X(e,n,r="existing-generation"){return v(e,{generation:r,kind:"postgres-container",name:n,scope:"local-postgres",cachePolicy:"product-cache"})}function y(e,n,r){return v(e,{generation:r,kind:"postgres-volume",name:n,scope:"local-postgres",cachePolicy:"product-cache"})}function Ce(e,n){return new Promise((r,o)=>{if(n?.aborted){o(Q(n));return}const t=setTimeout(i,e);n?.addEventListener("abort",a,{once:!0});function i(){n?.removeEventListener("abort",a),r()}function a(){clearTimeout(t),o(n?Q(n):new Error("Local Postgres operation aborted"))}})}function Q(e){return e.reason instanceof Error?e.reason:new Error("Local Postgres operation aborted",{cause:e.reason})}function Z(e){if(!m(e))return null;const n=e.Labels;return m(n)?n:null}function he(e){if(e===null)return null;if(m(e))return{Cmd:Se(e.Cmd),Image:g(e.Image),Labels:Le(e.Labels)}}function Le(e){if(!m(e))return null;const n={};for(const[r,o]of Object.entries(e))typeof o=="string"&&(n[r]=o);return n}function ye(e){if(e===null)return null;if(m(e))return{NetworkMode:g(e.NetworkMode)}}function Pe(e){if(e===null)return null;if(P(e))return e.flatMap(n=>m(n)?[{Destination:g(n.Destination),Name:g(n.Name),Type:g(n.Type)}]:[])}function ke(e){if(e===null)return null;if(!m(e))return;const n=e.Ports;if(n===null)return{Ports:null};if(!m(n))return{};const r={};for(const[o,t]of Object.entries(n)){if(t===null){r[o]=null;continue}P(t)&&(r[o]=t.flatMap(i=>{if(!m(i))return[];const a=g(i.HostPort);return i.HostPort!==void 0&&a===void 0?[]:[{HostPort:a}]}))}return{Ports:r}}function _e(e){if(e===null)return null;if(m(e))return{Running:e.Running===null||typeof e.Running=="boolean"?e.Running:void 0}}function g(e){if(e===null||typeof e=="string")return e}function m(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function P(e){return Array.isArray(e)}function Se(e){if(e===null)return null;if(P(e))return e.filter(n=>typeof n=="string")}async function k(e,n,r,o){const t=await L(e,n,r,o);if(t.exitCode===0)return t;throw I(n,t)}function I(e,n){const r=n.stderr.trim()||n.stdout.trim(),o=new Error(`${e} failed with exit code ${String(n.exitCode)}${r?`: ${r}`:""}`);return Object.defineProperty(o,"result",{enumerable:!1,value:Object.freeze({...n})}),o}function ee(e,n,r){const o=new Error(`${e} returned malformed Docker inspect output; expected exactly one container record with a non-empty Id`,r===void 0?void 0:{cause:r});return Object.defineProperty(o,"result",{enumerable:!1,value:Object.freeze({...n})}),o}function ne(e){return/\bno such (?:container|object)\b/iu.test(`${e.stderr}
2
2
  ${e.stdout}`)}