@gfdelarue/testkit 0.3.3 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/README.md +11 -0
  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/config/loader/validation.js +1 -1
  50. package/dist/database/index.d.ts +1 -3
  51. package/dist/database/index.js +1 -1
  52. package/dist/database/lifecycle-cleanup.d.ts +7 -0
  53. package/dist/database/lifecycle-cleanup.js +1 -0
  54. package/dist/database/local-postgres.js +1 -1
  55. package/dist/database/schema-source.js +3 -3
  56. package/dist/engines/adapters/evidence-files.js +1 -1
  57. package/dist/engines/catalog.d.ts +3 -8
  58. package/dist/engines/catalog.js +1 -1
  59. package/dist/engines/definitions.d.ts +8 -1
  60. package/dist/engines/definitions.js +1 -1
  61. package/dist/engines/generated-performance.d.ts +1 -0
  62. package/dist/engines/generated-performance.js +12 -0
  63. package/dist/engines/installations/node-vitest.d.ts +8 -22
  64. package/dist/engines/installations/node-vitest.js +1 -1
  65. package/dist/engines/installations/package-requirements.d.ts +28 -0
  66. package/dist/engines/installations/package-requirements.js +1 -0
  67. package/dist/engines/installations/playwright.d.ts +1 -11
  68. package/dist/engines/installations/playwright.js +1 -1
  69. package/dist/engines/k6/adapter/repository-configuration.d.ts +1 -1
  70. package/dist/engines/k6/adapter/repository-configuration.js +1 -1
  71. package/dist/engines/managed-module-resolution.js +111 -21
  72. package/dist/engines/manager.d.ts +1 -0
  73. package/dist/engines/manager.js +1 -1
  74. package/dist/engines/node-vitest/adapter/adapter.d.ts +1 -1
  75. package/dist/engines/node-vitest/adapter/adapter.js +1 -1
  76. package/dist/engines/node-vitest/adapter/consumer-typescript-paths.js +1 -1
  77. package/dist/engines/node-vitest/adapter/resolution.d.ts +1 -1
  78. package/dist/engines/node-vitest/adapter/resolution.js +1 -1
  79. package/dist/engines/node-vitest/adapter/workspace.js +33 -9
  80. package/dist/engines/node-vitest/runtime/runtime-input.d.ts +1 -0
  81. package/dist/engines/node-vitest/runtime/runtime-input.js +1 -1
  82. package/dist/engines/runtime-input-file.d.ts +3 -1
  83. package/dist/engines/runtime-input-file.js +1 -1
  84. package/dist/environment/lifecycle-controller.d.ts +1 -2
  85. package/dist/environment/lifecycle-controller.js +1 -1
  86. package/dist/environment/lifecycle.js +1 -1
  87. package/dist/environment/maintenance.d.ts +1 -1
  88. package/dist/environment/maintenance.js +1 -1
  89. package/dist/environment/ready-environment.d.ts +1 -1
  90. package/dist/environment/ready-environment.js +1 -1
  91. package/dist/environment/runtime-contexts.d.ts +1 -1
  92. package/dist/environment/runtime-contexts.js +1 -1
  93. package/dist/environment/runtime-preparation.js +1 -1
  94. package/dist/environment/services.js +1 -1
  95. package/dist/environment/template.d.ts +1 -1
  96. package/dist/environment/template.js +1 -1
  97. package/dist/evidence/bundles.js +1 -1
  98. package/dist/evidence/read-run.js +1 -1
  99. package/dist/evidence/reading.d.ts +1 -1
  100. package/dist/evidence/reading.js +1 -1
  101. package/dist/evidence/storage.js +2 -2
  102. package/dist/kernel/error-serialization.js +1 -1
  103. package/dist/kernel/work-metrics.d.ts +6 -0
  104. package/dist/kernel/work-metrics.js +1 -0
  105. package/dist/process/capture-output.js +1 -1
  106. package/dist/process/execa-bundle.json +1 -1
  107. package/dist/process/files/product-dir.d.ts +1 -0
  108. package/dist/process/files/product-dir.js +1 -0
  109. package/dist/process/locks.js +4 -4
  110. package/dist/process/readiness.js +1 -1
  111. package/dist/process/runs/inspection.d.ts +5 -0
  112. package/dist/process/runs/inspection.js +1 -0
  113. package/dist/process/runs/manifest.d.ts +1 -0
  114. package/dist/process/runs/manifest.js +1 -1
  115. package/dist/process/service-processes.d.ts +1 -1
  116. package/dist/process/service-processes.js +1 -1
  117. package/dist/steps/environment.d.ts +6 -0
  118. package/dist/steps/environment.js +1 -1
  119. package/dist/steps/index.d.ts +1 -1
  120. package/dist/steps/index.js +1 -1
  121. package/dist/toolchains/index.js +2 -2
  122. package/docs/engine-architecture.md +4 -1
  123. package/docs/test-types.md +9 -0
  124. package/package.json +2 -1
  125. package/testkit-config-schema.md +52 -0
  126. package/dist/cli/renderers/artifact-preview.d.ts +0 -4
  127. package/dist/cli/renderers/artifact-preview.js +0 -1
@@ -1 +1 @@
1
- import i from"node:fs";import l from"node:path";import{randomUUID as _}from"node:crypto";import{publishWindowsFile as P}from"../windows-native.js";import{serializeJson as x,writeJsonFileAtomicWithRetry as h}from"../files/json-file.js";import{RUN_MAINTENANCE_STOP_REQUEST_SCHEMA_VERSION as S,RUN_MANIFEST_SCHEMA_VERSION as I}from"./schema.js";export{S as RUN_MAINTENANCE_STOP_REQUEST_SCHEMA_VERSION,I as RUN_MANIFEST_SCHEMA_VERSION};const $=l.join(".testkit","_runs"),D=".maintenance-stop",E=128,U=/^[A-Za-z0-9][A-Za-z0-9._-]*$/u,F=/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u,O=/^(?:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\.|$)/iu;export class RunManifestCollisionError extends Error{code="TESTKIT_RUN_MANIFEST_COLLISION";manifestPath;constructor(t,e){super(`Testkit run manifest already exists at ${t}`,e),this.name="RunManifestCollisionError",this.manifestPath=t}}export class RunManifestInventoryError extends Error{code="TESTKIT_RUN_MANIFEST_INVENTORY_FAILED";manifestPath;constructor(t,e,r){super(`Cannot inventory Testkit run manifest ${t}: ${e}`,r),this.name="RunManifestInventoryError",this.manifestPath=t}}export function runManifestPath(n,t){const e=requireSafeRunId(t,"Testkit run id");return l.join(p(n),`${e}.json`)}export function writeRunManifest(n,t){const e=runManifestPath(n,t.runId);i.mkdirSync(p(n),{recursive:!0}),h(e,t)}export function prepareRunShutdownDeadlines(n,t){return{...n,services:n.services.map(e=>e.shutdownTimeoutMs===void 0?e:{...e,shutdownDeadlineAt:Math.min(e.shutdownDeadlineAt??Number.MAX_VALUE,Date.parse(t)+e.shutdownTimeoutMs)})}}export function persistRunShutdownDeadlines(n,t){h(runManifestPath(n,t.runId),t)}export function createRunManifest(n,t){const e=runManifestPath(n,t.runId),r=p(n);i.mkdirSync(r,{recursive:!0});const o=l.join(r,`.${l.basename(e)}.${process.pid.toString()}.${_()}.tmp`);i.writeFileSync(o,x(t,`run manifest ${t.runId}`),{flag:"wx"});try{process.platform==="win32"?P(o,e):i.linkSync(o,e)}catch(s){throw J(s,"EEXIST")?new RunManifestCollisionError(e,{cause:s}):s}finally{i.rmSync(o,{force:!0})}}export function writeRunMaintenanceStopRequest(n,t){const e=g(n,t.runId);i.mkdirSync(p(n),{recursive:!0}),h(e,t)}export function readRunMaintenanceStopRequest(n,t){const e=requireSafeRunId(t,"Testkit run id"),r=g(n,e);let o;try{o=JSON.parse(i.readFileSync(r,"utf8"))}catch{return null}const s=j(o);return s?.runId===e?s:null}export function removeRunMaintenanceStopRequest(n,t){i.rmSync(g(n,t),{force:!0})}export function removeRunManifestFile(n,t){const e=runManifestPath(n,t),r=p(n);i.rmSync(e,{force:!0}),removeRunMaintenanceStopRequest(n,t),i.existsSync(r)&&i.readdirSync(r).length===0&&i.rmSync(r,{recursive:!0,force:!0})}export function listRunManifests(n){const t=p(n);return i.existsSync(t)?i.readdirSync(t,{withFileTypes:!0}).filter(e=>e.name.endsWith(".json")).map(e=>{const r=l.join(t,e.name);if(!e.isFile())throw new RunManifestInventoryError(r,"manifest path is not a regular file");return C(r)}).sort((e,r)=>(e.startedAt||"").localeCompare(r.startedAt||"")):[]}export function requireSafeRunId(n,t){const e=R(n);if(e!==null)return e;throw new Error(`${t} must be 1-${E.toString()} characters using only ASCII letters, digits, dot, underscore, or hyphen, must start with a letter or digit, and must not use a reserved Windows device name`)}export function normalizeRunManifest(n){if(!m(n)||n.schemaVersion!==I)return null;const t=R(n.runId);if(t===null)return null;const e=n.controller,r=n.finishedAt,o=n.heartbeatAt,s=T(n.incarnationId),u=n.interruptReason,a=n.pid,d=n.processIdentity,c=n.session,f=n.startedAt,y=n.status,b=z(n.services),w=V(n.runtimeDirs);return typeof a!="number"||!Number.isSafeInteger(a)||a<0||s===null||b===null||w===null||a===0&&d!==null||a>0&&(typeof d!="string"||d.length===0)?null:{incarnationId:s,runId:t,runtimeDirs:w,schemaVersion:I,services:b,pid:a,processIdentity:typeof d=="string"?d:null,...W(e)?{controller:e}:{},...typeof r=="string"?{finishedAt:r}:{},...N(o)?{heartbeatAt:o}:{},...N(u)?{interruptReason:u}:{},...q(c)?{session:c}:{},...typeof f=="string"?{startedAt:f}:{},...typeof y=="string"?{status:y}:{}}}function g(n,t){const e=requireSafeRunId(t,"Testkit run id");return l.join(p(n),`${e}${D}`)}function j(n){if(!m(n)||n.schemaVersion!==S)return null;const t=R(n.runId),e=T(n.targetIncarnationId);return t===null||e===null||!M(n.coordinatorPid)||typeof n.coordinatorProcessIdentity!="string"||n.coordinatorProcessIdentity.length===0||typeof n.reason!="string"||n.reason.length===0||typeof n.requestedAt!="string"||!Number.isFinite(Date.parse(n.requestedAt))||!M(n.targetPid)||typeof n.targetProcessIdentity!="string"||n.targetProcessIdentity.length===0?null:{coordinatorPid:n.coordinatorPid,coordinatorProcessIdentity:n.coordinatorProcessIdentity,reason:n.reason,requestedAt:n.requestedAt,runId:t,schemaVersion:S,targetIncarnationId:e,targetPid:n.targetPid,targetProcessIdentity:n.targetProcessIdentity}}function C(n){let t;try{t=i.readFileSync(n,"utf8")}catch(u){throw new RunManifestInventoryError(n,"file could not be read",{cause:u})}let e;try{e=JSON.parse(t)}catch(u){throw new RunManifestInventoryError(n,"file is not valid JSON",{cause:u})}const r=normalizeRunManifest(e);if(r!==null){const u=l.basename(n,".json");if(r.runId!==u)throw new RunManifestInventoryError(n,"embedded runId does not match the manifest filename");return r}const o=m(e)?e.schemaVersion:void 0,s=o===I?`schema-v${I.toString()} record is invalid`:`unsupported schema version ${typeof o=="number"?o.toString():"missing"}`;throw new RunManifestInventoryError(n,s)}function R(n){return typeof n=="string"&&n.length<=E&&U.test(n)&&!O.test(n)?n:null}function T(n){return typeof n=="string"&&F.test(n)?n:null}function z(n){if(!Array.isArray(n))return null;const t=[];for(const e of n){const r=k(e);if(r===null)return null;t.push(r)}return t}function k(n){if(!m(n))return null;const t=n.command,e=n.cwd,r=n.pid,o=n.processIdentity,s=n.processGroupId,u=n.runtimeLabel,a=n.serviceName,d=n.startedAt,c=n.shutdownTimeoutMs,f=n.shutdownDeadlineAt;if(c!==void 0&&(typeof c!="number"||!Number.isFinite(c)||!Number.isInteger(c)||c<=0)||f!=null&&(c===void 0||typeof f!="number"||!Number.isFinite(f)||f<0))return null;const y=L(n.ports);return typeof r!="number"||!Number.isSafeInteger(r)||r<=0||s!==null&&(typeof s!="number"||!Number.isSafeInteger(s)||s!==r)||typeof o!="string"||o.length===0||y===null?null:{...c===void 0?{}:{shutdownTimeoutMs:c},...f===void 0?{}:{shutdownDeadlineAt:f},ports:y,pid:r,processGroupId:s,processIdentity:o,...N(t)?{command:t}:{},...typeof e=="string"?{cwd:e}:{},...typeof u=="string"?{runtimeLabel:u}:{},...typeof a=="string"?{serviceName:a}:{},...typeof d=="string"?{startedAt:d}:{}}}function L(n){if(!Array.isArray(n))return null;const t=[];for(const e of n){if(!m(e))return null;const r=e.host,o=e.port;if(typeof r!="string"||typeof o!="number"||!Number.isSafeInteger(o)||o<=0||o>65535)return null;t.push({...e,host:r,port:o})}return t}function V(n){return!Array.isArray(n)||!n.every(t=>typeof t=="string")?null:n}function M(n){return typeof n=="number"&&Number.isSafeInteger(n)&&n>0}function p(n){return l.join(n,$)}function m(n){return typeof n=="object"&&n!==null&&!Array.isArray(n)}function q(n){return n===null||m(n)}function W(n){return n===null?!0:m(n)?Object.values(n).every(t=>t===null||typeof t=="string"):!1}function N(n){return n===null||typeof n=="string"}function J(n,t){return(typeof n=="object"||typeof n=="function")&&n!==null&&Reflect.get(n,"code")===t}const A=String;export function formatRunSummary(n){const t=normalizeRunManifest(n);if(!t)return A(n);const e=[...new Set(t.services.map(r=>r.runtimeLabel).filter(r=>!!r))];return`${A(t.runId)} pid=${A(t.pid)}${e.length>0?` runtimes=${e.join(",")}`:""}`}
1
+ import{isWorkObserved as $,recordWork as I}from"../../kernel/work-metrics.js";import i from"node:fs";import l from"node:path";import{randomUUID as F}from"node:crypto";import{publishWindowsFile as D}from"../windows-native.js";import{serializeJson as O,writeJsonFileAtomicWithRetry as S}from"../files/json-file.js";import{RUN_MAINTENANCE_STOP_REQUEST_SCHEMA_VERSION as g,RUN_MANIFEST_SCHEMA_VERSION as h}from"./schema.js";export{g as RUN_MAINTENANCE_STOP_REQUEST_SCHEMA_VERSION,h as RUN_MANIFEST_SCHEMA_VERSION};const U=l.join(".testkit","_runs"),j=".maintenance-stop",T=128,C=/^[A-Za-z0-9][A-Za-z0-9._-]*$/u,k=/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u,L=/^(?:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\.|$)/iu;export class RunManifestCollisionError extends Error{code="TESTKIT_RUN_MANIFEST_COLLISION";manifestPath;constructor(n,e){super(`Testkit run manifest already exists at ${n}`,e),this.name="RunManifestCollisionError",this.manifestPath=n}}export class RunManifestInventoryError extends Error{code="TESTKIT_RUN_MANIFEST_INVENTORY_FAILED";manifestPath;constructor(n,e,r){super(`Cannot inventory Testkit run manifest ${n}: ${e}`,r),this.name="RunManifestInventoryError",this.manifestPath=n}}export function runManifestPath(t,n){const e=requireSafeRunId(n,"Testkit run id");return l.join(m(t),`${e}.json`)}export function writeRunManifest(t,n){const e=runManifestPath(t,n.runId);i.mkdirSync(m(t),{recursive:!0}),S(e,n)}export function prepareRunShutdownDeadlines(t,n){return{...t,services:t.services.map(e=>e.shutdownTimeoutMs===void 0?e:{...e,shutdownDeadlineAt:Math.min(e.shutdownDeadlineAt??Number.MAX_VALUE,Date.parse(n)+e.shutdownTimeoutMs)})}}export function persistRunShutdownDeadlines(t,n){S(runManifestPath(t,n.runId),n)}export function createRunManifest(t,n){const e=runManifestPath(t,n.runId),r=m(t);i.mkdirSync(r,{recursive:!0});const s=l.join(r,`.${l.basename(e)}.${process.pid.toString()}.${F()}.tmp`);i.writeFileSync(s,O(n,`run manifest ${n.runId}`),{flag:"wx"});try{process.platform==="win32"?D(s,e):i.linkSync(s,e)}catch(o){throw x(o,"EEXIST")?new RunManifestCollisionError(e,{cause:o}):o}finally{i.rmSync(s,{force:!0})}}export function writeRunMaintenanceStopRequest(t,n){const e=R(t,n.runId);i.mkdirSync(m(t),{recursive:!0}),S(e,n)}export function readRunMaintenanceStopRequest(t,n){const e=requireSafeRunId(n,"Testkit run id"),r=R(t,e);let s;try{s=JSON.parse(i.readFileSync(r,"utf8"))}catch{return null}const o=z(s);return o?.runId===e?o:null}export function removeRunMaintenanceStopRequest(t,n){i.rmSync(R(t,n),{force:!0})}export function removeRunManifestFile(t,n){const e=runManifestPath(t,n),r=m(t);i.rmSync(e,{force:!0}),removeRunMaintenanceStopRequest(t,n),i.existsSync(r)&&i.readdirSync(r).length===0&&i.rmSync(r,{recursive:!0,force:!0})}export function loadRunManifest(t,n){const e=runManifestPath(t,n);I("runs.manifestLookups");let r;try{r=i.lstatSync(e)}catch(s){if(x(s,"ENOENT"))return null;throw new RunManifestInventoryError(e,"manifest path could not be inspected",{cause:s})}if(!r.isFile())throw new RunManifestInventoryError(e,"manifest path is not a regular file");return M(e)}export function listRunManifests(t){const n=m(t);if(!i.existsSync(n))return[];const e=i.readdirSync(n,{withFileTypes:!0});return I("runs.manifestDirectoriesRead"),I("runs.manifestEntriesVisited",e.length),e.filter(r=>r.name.endsWith(".json")).map(r=>{const s=l.join(n,r.name);if(!r.isFile())throw new RunManifestInventoryError(s,"manifest path is not a regular file");return M(s)}).sort((r,s)=>(r.startedAt||"").localeCompare(s.startedAt||""))}export function requireSafeRunId(t,n){const e=N(t);if(e!==null)return e;throw new Error(`${n} must be 1-${T.toString()} characters using only ASCII letters, digits, dot, underscore, or hyphen, must start with a letter or digit, and must not use a reserved Windows device name`)}export function normalizeRunManifest(t){if(!p(t)||t.schemaVersion!==h)return null;const n=N(t.runId);if(n===null)return null;const e=t.controller,r=t.finishedAt,s=t.heartbeatAt,o=_(t.incarnationId),u=t.interruptReason,a=t.pid,d=t.processIdentity,c=t.session,f=t.startedAt,y=t.status,w=V(t.services),E=J(t.runtimeDirs);return typeof a!="number"||!Number.isSafeInteger(a)||a<0||o===null||w===null||E===null||a===0&&d!==null||a>0&&(typeof d!="string"||d.length===0)?null:{incarnationId:o,runId:n,runtimeDirs:E,schemaVersion:h,services:w,pid:a,processIdentity:typeof d=="string"?d:null,...G(e)?{controller:e}:{},...typeof r=="string"?{finishedAt:r}:{},...A(s)?{heartbeatAt:s}:{},...A(u)?{interruptReason:u}:{},...X(c)?{session:c}:{},...typeof f=="string"?{startedAt:f}:{},...typeof y=="string"?{status:y}:{}}}function R(t,n){const e=requireSafeRunId(n,"Testkit run id");return l.join(m(t),`${e}${j}`)}function z(t){if(!p(t)||t.schemaVersion!==g)return null;const n=N(t.runId),e=_(t.targetIncarnationId);return n===null||e===null||!P(t.coordinatorPid)||typeof t.coordinatorProcessIdentity!="string"||t.coordinatorProcessIdentity.length===0||typeof t.reason!="string"||t.reason.length===0||typeof t.requestedAt!="string"||!Number.isFinite(Date.parse(t.requestedAt))||!P(t.targetPid)||typeof t.targetProcessIdentity!="string"||t.targetProcessIdentity.length===0?null:{coordinatorPid:t.coordinatorPid,coordinatorProcessIdentity:t.coordinatorProcessIdentity,reason:t.reason,requestedAt:t.requestedAt,runId:n,schemaVersion:g,targetIncarnationId:e,targetPid:t.targetPid,targetProcessIdentity:t.targetProcessIdentity}}function M(t){let n;try{if($()){const u=i.readFileSync(t);I("runs.manifestFilesRead"),I("runs.manifestBytesRead",u.byteLength),n=u.toString("utf8")}else n=i.readFileSync(t,"utf8")}catch(u){throw new RunManifestInventoryError(t,"file could not be read",{cause:u})}let e;try{e=JSON.parse(n)}catch(u){throw new RunManifestInventoryError(t,"file is not valid JSON",{cause:u})}const r=normalizeRunManifest(e);if(r!==null){const u=l.basename(t,".json");if(r.runId!==u)throw new RunManifestInventoryError(t,"embedded runId does not match the manifest filename");return r}const s=p(e)?e.schemaVersion:void 0,o=s===h?`schema-v${h.toString()} record is invalid`:`unsupported schema version ${typeof s=="number"?s.toString():"missing"}`;throw new RunManifestInventoryError(t,o)}function N(t){return typeof t=="string"&&t.length<=T&&C.test(t)&&!L.test(t)?t:null}function _(t){return typeof t=="string"&&k.test(t)?t:null}function V(t){if(!Array.isArray(t))return null;const n=[];for(const e of t){const r=q(e);if(r===null)return null;n.push(r)}return n}function q(t){if(!p(t))return null;const n=t.command,e=t.cwd,r=t.pid,s=t.processIdentity,o=t.processGroupId,u=t.runtimeLabel,a=t.serviceName,d=t.startedAt,c=t.shutdownTimeoutMs,f=t.shutdownDeadlineAt;if(c!==void 0&&(typeof c!="number"||!Number.isFinite(c)||!Number.isInteger(c)||c<=0)||f!=null&&(c===void 0||typeof f!="number"||!Number.isFinite(f)||f<0))return null;const y=W(t.ports);return typeof r!="number"||!Number.isSafeInteger(r)||r<=0||o!==null&&(typeof o!="number"||!Number.isSafeInteger(o)||o!==r)||typeof s!="string"||s.length===0||y===null?null:{...c===void 0?{}:{shutdownTimeoutMs:c},...f===void 0?{}:{shutdownDeadlineAt:f},ports:y,pid:r,processGroupId:o,processIdentity:s,...A(n)?{command:n}:{},...typeof e=="string"?{cwd:e}:{},...typeof u=="string"?{runtimeLabel:u}:{},...typeof a=="string"?{serviceName:a}:{},...typeof d=="string"?{startedAt:d}:{}}}function W(t){if(!Array.isArray(t))return null;const n=[];for(const e of t){if(!p(e))return null;const r=e.host,s=e.port;if(typeof r!="string"||typeof s!="number"||!Number.isSafeInteger(s)||s<=0||s>65535)return null;n.push({...e,host:r,port:s})}return n}function J(t){return!Array.isArray(t)||!t.every(n=>typeof n=="string")?null:t}function P(t){return typeof t=="number"&&Number.isSafeInteger(t)&&t>0}function m(t){return l.join(t,U)}function p(t){return typeof t=="object"&&t!==null&&!Array.isArray(t)}function X(t){return t===null||p(t)}function G(t){return t===null?!0:p(t)?Object.values(t).every(n=>n===null||typeof n=="string"):!1}function A(t){return t===null||typeof t=="string"}function x(t,n){return(typeof t=="object"||typeof t=="function")&&t!==null&&Reflect.get(t,"code")===n}const b=String;export function formatRunSummary(t){const n=normalizeRunManifest(t);if(!n)return b(t);const e=[...new Set(n.services.map(r=>r.runtimeLabel).filter(r=>!!r))];return`${b(n.runId)} pid=${b(n.pid)}${e.length>0?` runtimes=${e.join(",")}`:""}`}
@@ -10,4 +10,4 @@ export declare function normalizeServiceStartCommand(command: string): string;
10
10
  export declare function prepareServiceStartInvocation(command: string, env: Readonly<NodeJS.ProcessEnv>, platform?: NodeJS.Platform): ServiceStartInvocation;
11
11
  export declare function terminateAttributedProcessLineage(pid: unknown, signal: ProcessSignal, expectedCreationIdentity?: string): Promise<void>;
12
12
  export declare function terminateOwnedPosixProcessGroup(processGroupId: unknown, leaderCreationIdentity: string, graceMs: number): Promise<void>;
13
- export declare function sleep(ms: number): Promise<void>;
13
+ export declare function sleep(ms: number, signal?: AbortSignal): Promise<void>;
@@ -1 +1 @@
1
- import{captureProcessCreationIdentity as a,processCreationIdentityMatches as c,resolvePosixCommandShell as m,terminateAttributedProcessLineage as d,terminateOwnedPosixProcessGroup as u}from"./index.js";export{a as captureProcessCreationIdentity,c as processCreationIdentityMatches};export function normalizeServiceStartCommand(t){const r=t.trim();if(r.length===0)throw new Error("Service start command must not be empty");return r}export function prepareServiceStartInvocation(t,r,o=process.platform){const e=normalizeServiceStartCommand(t);if(o==="win32"){const n=e.replace(/^exec(?:\s+|$)/u,"").trim();if(n.length===0)throw new Error("Service start command must not contain only exec");return{executable:r.ComSpec||process.env.ComSpec||"cmd.exe",args:["/d","/s","/c",`"${n}"`],windowsVerbatimArguments:!0}}const s=m(r),i=/^exec(?:\s|$)/u.test(e)?e:`exec ${e}`;return{executable:s,args:["-lc",i]}}export async function terminateAttributedProcessLineage(t,r,o){const e=Number(t);!Number.isInteger(e)||e<=0||await d({displayLabel:"Testkit application service",...o===void 0?{}:{expectedCreationIdentity:o},pid:e,signal:r})}export async function terminateOwnedPosixProcessGroup(t,r,o){const e=Number(t);if(!Number.isSafeInteger(e)||e<=0)throw new Error("Owned POSIX process-group id must be a positive safe integer");await u({displayLabel:"Testkit application service",graceMs:o,leaderCreationIdentity:r,leaderPid:e,processGroupId:e})}export function sleep(t){return new Promise(r=>setTimeout(r,t))}
1
+ import{captureProcessCreationIdentity as a,processCreationIdentityMatches as c,resolvePosixCommandShell as m,terminateAttributedProcessLineage as d,terminateOwnedPosixProcessGroup as u}from"./index.js";export{a as captureProcessCreationIdentity,c as processCreationIdentityMatches};export function normalizeServiceStartCommand(t){const r=t.trim();if(r.length===0)throw new Error("Service start command must not be empty");return r}export function prepareServiceStartInvocation(t,r,o=process.platform){const e=normalizeServiceStartCommand(t);if(o==="win32"){const i=e.replace(/^exec(?:\s+|$)/u,"").trim();if(i.length===0)throw new Error("Service start command must not contain only exec");return{executable:r.ComSpec||process.env.ComSpec||"cmd.exe",args:["/d","/s","/c",`"${i}"`],windowsVerbatimArguments:!0}}const n=m(r),s=/^exec(?:\s|$)/u.test(e)?e:`exec ${e}`;return{executable:n,args:["-lc",s]}}export async function terminateAttributedProcessLineage(t,r,o){const e=Number(t);!Number.isInteger(e)||e<=0||await d({displayLabel:"Testkit application service",...o===void 0?{}:{expectedCreationIdentity:o},pid:e,signal:r})}export async function terminateOwnedPosixProcessGroup(t,r,o){const e=Number(t);if(!Number.isSafeInteger(e)||e<=0)throw new Error("Owned POSIX process-group id must be a positive safe integer");await u({displayLabel:"Testkit application service",graceMs:o,leaderCreationIdentity:r,leaderPid:e,processGroupId:e})}export function sleep(t,r){return new Promise(o=>{const e=()=>{clearTimeout(n),r?.removeEventListener("abort",e),o()},n=setTimeout(e,t);r?.addEventListener("abort",e,{once:!0}),r?.aborted&&e()})}
@@ -21,6 +21,10 @@ export interface DatabaseTemplateBindings {
21
21
  user: string;
22
22
  }
23
23
  export type DatabaseTemplateBindingsReader = (stateDir: string) => DatabaseTemplateBindings | null;
24
+ export interface DatabasePlaceholder {
25
+ placeholder: string;
26
+ serviceName: null | string;
27
+ }
24
28
  export interface ExecutionTemplateContextValues {
25
29
  [key: string]: unknown;
26
30
  baseUrlByService?: StringMap;
@@ -70,9 +74,11 @@ export interface TemplateResolutionContext extends ExecutionTemplateContextValue
70
74
  export declare function buildExecutionEnv(config: Readonly<ExecutionEnvironmentConfig>, extraEnv?: Readonly<NodeJS.ProcessEnv> | null, processEnv?: Readonly<NodeJS.ProcessEnv> | null, options?: Readonly<BuildExecutionEnvironmentOptions>): NodeJS.ProcessEnv;
71
75
  export declare function buildTemplateExecutionEnv(config: Readonly<ExecutionEnvironmentConfig>, extraEnv?: Readonly<NodeJS.ProcessEnv> | null, processEnv?: Readonly<NodeJS.ProcessEnv> | null, options?: Readonly<BuildExecutionEnvironmentOptions>): NodeJS.ProcessEnv;
72
76
  export declare function buildTaskExecutionEnv(config: Readonly<ExecutionEnvironmentConfig>, lease: Readonly<ExecutionEnvironmentLease> | null, extraEnv?: Readonly<NodeJS.ProcessEnv> | null, processEnv?: Readonly<NodeJS.ProcessEnv> | null, options?: Readonly<BuildExecutionEnvironmentOptions>): NodeJS.ProcessEnv;
77
+ export declare function resolveExecutionEnvValues(config: Readonly<ExecutionEnvironmentConfig>, names: readonly string[], processEnv?: Readonly<NodeJS.ProcessEnv> | null): NodeJS.ProcessEnv;
73
78
  export declare function buildTemplateContext(config: Readonly<ExecutionEnvironmentConfig>, lease: Readonly<ExecutionEnvironmentLease> | null): TemplateResolutionContext;
74
79
  export declare function finalizeString(value: string, context: TemplateResolutionContext): string;
75
80
  export declare function finalizeSerializable(value: unknown, context: TemplateResolutionContext): unknown;
76
81
  export declare function resolveTemplateString(value: string, context: TemplateResolutionContext): string;
77
82
  export declare function resolveTemplateString(value: unknown, context: TemplateResolutionContext): unknown;
83
+ export declare function findDatabasePlaceholders(value: string): DatabasePlaceholder[];
78
84
  export {};
@@ -1 +1 @@
1
- import u from"path";import{joinPathList as m,packageBinPath as b,shellQuote as f}from"../process/host/platform.js";export function buildExecutionEnv(r,e={},t=process.env,a={}){return o(r,null,e,t,a)}export function buildTemplateExecutionEnv(r,e={},t=process.env,a={}){return o(r,null,e,t,{...a,omitRuntimeDatabaseBindings:!0})}export function buildTaskExecutionEnv(r,e,t={},a=process.env,s={}){return o(r,e,t,a,s)}function o(r,e,t,a,s={}){const i=s.inheritProcessEnv===!1?{}:{...a};delete i.DATABASE_URL;const n=buildTemplateContext(r,e),p=s.omitRuntimeDatabaseBindings?d(r.testkit.serviceEnv||{}):r.testkit.serviceEnv||{},v=s.omitRuntimeDatabaseBindings?d(r.testkit.local?.env||{}):r.testkit.local?.env||{};return{...i,...c(p,n),...c(v,n),...c(t,n),...s.includeTestkitControls===!1?{}:{TESTKIT_ACTIVE:"1",TESTKIT_MODE:s.mode||r.testkit.mode||"test",...r.runtimeId?{TESTKIT_RUNTIME_ID:String(r.runtimeId)}:{},...e?.leaseId?{TESTKIT_LEASE_ID:String(e.leaseId)}:{},...e?.leaseDir?{TESTKIT_LEASE_DIR:e.leaseDir}:{}}}}export function buildTemplateContext(r,e){const t=r.testkit.templateContext||{};return{...t,runtimeId:r.runtimeId||t.runtimeId||null,runtimeLabel:r.runtimeLabel||t.runtimeLabel||null,productDir:r.productDir||t.productDir||null,serviceName:r.name,serviceStateDir:r.stateDir||t.serviceStateDir||null,prepareDir:r.testkit.prepareDir||t.prepareDir||null,leaseId:e?.leaseId||null,leaseDir:e?.leaseDir||null}}export function finalizeString(r,e){let t=resolveTemplateString(r,e);for(const[a,s]of e.urlMappings||[])a&&s&&a!==s&&(t=t.split(a).join(s));return t}export function finalizeSerializable(r,e){return typeof r=="string"?finalizeString(r,e):Array.isArray(r)?r.map(t=>finalizeSerializable(t,e)):r===null||typeof r!="object"?r:Object.fromEntries(Object.entries(r).map(([t,a])=>[t,finalizeSerializable(a,e)]))}export function resolveTemplateString(r,e){return typeof r!="string"?r:r.replace(/\{([a-zA-Z]+)(?::([^}]+))?\}/g,(t,a,s)=>{switch(a){case"runtime":case"runtimeId":return String(e.runtimeId);case"service":return e.serviceName;case"stateDir":return String(e.serviceStateDir);case"productDir":return e.productDir||"";case"bin":{const i=u.resolve(e.productDir||process.cwd(),e.serviceCwd||".");return f(b(i,s))}case"pathList":{const i=l(s||"").split(",").map(n=>n.trim()).filter(Boolean).map(n=>u.resolve(e.productDir||process.cwd(),n));return m(i)}case"prepareDir":return e.prepareDir||"";case"lease":return e.leaseId?String(e.leaseId):"";case"leaseDir":return e.leaseDir||"";case"port":{const i=s||e.serviceName,n=e.portMap?.get(i);if(!n)throw new Error(`Unknown port placeholder for service "${i}"`);return String(n)}case"baseUrl":{const i=s||e.serviceName,n=e.baseUrlByService?.get(i);if(!n)throw new Error(`Unknown baseUrl placeholder for service "${i}"`);return n}case"readyUrl":{const i=s||e.serviceName,n=e.readyUrlByService?.get(i);if(!n)throw new Error(`Unknown readyUrl placeholder for service "${i}"`);return n}case"publicHost":return e.publicHost||"127.0.0.1";case"publicBaseUrl":{const i=s||e.serviceName,n=e.publicBaseUrlByService?.get(i)||e.baseUrlByService?.get(i);if(!n)throw new Error(`Unknown publicBaseUrl placeholder for service "${i}"`);return n}case"privateBaseUrl":{const i=s||e.serviceName,n=e.privateBaseUrlByService?.get(i)||e.baseUrlByService?.get(i);if(!n)throw new Error(`Unknown privateBaseUrl placeholder for service "${i}"`);return n}case"serviceContextUrl":{const i=s||e.serviceName,n=e.serviceContextUrlByService?.get(i)||e.baseUrlByService?.get(i);if(!n)throw new Error(`Unknown serviceContextUrl placeholder for service "${i}"`);return n}case"dbUrl":case"dbHost":case"dbPort":case"dbName":case"dbUser":case"dbPassword":case"dbSsl":{const i=s||e.serviceName;return E(a,i,e)}default:throw new Error(`Unsupported template token "{${a}${s?`:${s}`:""}}"`)}})}function c(r,e){return Object.fromEntries(Object.entries(r||{}).map(([t,a])=>[t,typeof a=="string"?finalizeString(a,e):a]))}function d(r={}){return Object.fromEntries(Object.entries(r).filter(e=>{const t=e[1];return typeof t!="string"?!0:!/\{db(?:Url|Host|Port|Name|User|Password|Ssl)(?::[^}]+)?\}/.test(t)}))}function E(r,e,t){const a=t.stateDirByService?.get(e),s=t.databaseBindingsByService?t.databaseBindingsByService.get(e):a?t.databaseBindingsReader?.(a):null;if(!s)throw new Error(`Unknown database placeholder for service "${e}"`);switch(r){case"dbUrl":return s.url;case"dbHost":return s.host;case"dbPort":return String(s.port);case"dbName":return s.database;case"dbUser":return s.user;case"dbPassword":return s.password;case"dbSsl":return w(s)}}function w(r){const e=r.sslMode,t=l(e||"").trim();return t&&!U(t)?t:S(r.sslMode)?"1":"0"}function S(r){const e=l(r||"").trim().toLowerCase();return!!(e&&!["0","false","no","off","disable","disabled"].includes(e))}function U(r){return["0","1","true","false","yes","no","on","off","disable","disabled","require","verify-ca","verify-full"].includes(l(r||"").trim().toLowerCase())}function l(r){const e=Reflect.apply(String,void 0,[r]);if(typeof e!="string")throw new Error("String conversion returned an invalid result");return e}
1
+ import v from"path";import{joinPathList as m,packageBinPath as E,shellQuote as w}from"../process/host/platform.js";export function buildExecutionEnv(r,e={},t=process.env,n={}){return o(r,null,e,t,n)}export function buildTemplateExecutionEnv(r,e={},t=process.env,n={}){return o(r,null,e,t,{...n,omitRuntimeDatabaseBindings:!0})}export function buildTaskExecutionEnv(r,e,t={},n=process.env,i={}){return o(r,e,t,n,i)}export function resolveExecutionEnvValues(r,e,t=process.env){return o(r,null,{},t,{includeTestkitControls:!1},new Set(e))}function o(r,e,t,n,i={},s=null){const a=c(i.inheritProcessEnv===!1?{}:n,s,!1);delete a.DATABASE_URL;const u=buildTemplateContext(r,e),p=i.omitRuntimeDatabaseBindings===!0,f=c(r.testkit.serviceEnv,s,p),b=c(r.testkit.local?.env,s,p);return{...a,...d(f,u),...d(b,u),...d(c(t,s,!1),u),...i.includeTestkitControls===!1?{}:{TESTKIT_ACTIVE:"1",TESTKIT_MODE:i.mode||r.testkit.mode||"test",...r.runtimeId?{TESTKIT_RUNTIME_ID:String(r.runtimeId)}:{},...e?.leaseId?{TESTKIT_LEASE_ID:String(e.leaseId)}:{},...e?.leaseDir?{TESTKIT_LEASE_DIR:e.leaseDir}:{}}}}export function buildTemplateContext(r,e){const t=r.testkit.templateContext||{};return{...t,runtimeId:r.runtimeId||t.runtimeId||null,runtimeLabel:r.runtimeLabel||t.runtimeLabel||null,productDir:r.productDir||t.productDir||null,serviceName:r.name,serviceStateDir:r.stateDir||t.serviceStateDir||null,prepareDir:r.testkit.prepareDir||t.prepareDir||null,leaseId:e?.leaseId||null,leaseDir:e?.leaseDir||null}}export function finalizeString(r,e){let t=resolveTemplateString(r,e);for(const[n,i]of e.urlMappings||[])n&&i&&n!==i&&(t=t.split(n).join(i));return t}export function finalizeSerializable(r,e){return typeof r=="string"?finalizeString(r,e):Array.isArray(r)?r.map(t=>finalizeSerializable(t,e)):r===null||typeof r!="object"?r:Object.fromEntries(Object.entries(r).map(([t,n])=>[t,finalizeSerializable(n,e)]))}export function resolveTemplateString(r,e){return typeof r!="string"?r:r.replace(/\{([a-zA-Z]+)(?::([^}]+))?\}/g,(t,n,i)=>{switch(n){case"runtime":case"runtimeId":return String(e.runtimeId);case"service":return e.serviceName;case"stateDir":return String(e.serviceStateDir);case"productDir":return e.productDir||"";case"bin":{const s=v.resolve(e.productDir||process.cwd(),e.serviceCwd||".");return w(E(s,i))}case"pathList":{const s=l(i||"").split(",").map(a=>a.trim()).filter(Boolean).map(a=>v.resolve(e.productDir||process.cwd(),a));return m(s)}case"prepareDir":return e.prepareDir||"";case"lease":return e.leaseId?String(e.leaseId):"";case"leaseDir":return e.leaseDir||"";case"port":{const s=i||e.serviceName,a=e.portMap?.get(s);if(!a)throw new Error(`Unknown port placeholder for service "${s}"`);return String(a)}case"baseUrl":{const s=i||e.serviceName,a=e.baseUrlByService?.get(s);if(!a)throw new Error(`Unknown baseUrl placeholder for service "${s}"`);return a}case"readyUrl":{const s=i||e.serviceName,a=e.readyUrlByService?.get(s);if(!a)throw new Error(`Unknown readyUrl placeholder for service "${s}"`);return a}case"publicHost":return e.publicHost||"127.0.0.1";case"publicBaseUrl":{const s=i||e.serviceName,a=e.publicBaseUrlByService?.get(s)||e.baseUrlByService?.get(s);if(!a)throw new Error(`Unknown publicBaseUrl placeholder for service "${s}"`);return a}case"privateBaseUrl":{const s=i||e.serviceName,a=e.privateBaseUrlByService?.get(s)||e.baseUrlByService?.get(s);if(!a)throw new Error(`Unknown privateBaseUrl placeholder for service "${s}"`);return a}case"serviceContextUrl":{const s=i||e.serviceName,a=e.serviceContextUrlByService?.get(s)||e.baseUrlByService?.get(s);if(!a)throw new Error(`Unknown serviceContextUrl placeholder for service "${s}"`);return a}case"dbUrl":case"dbHost":case"dbPort":case"dbName":case"dbUser":case"dbPassword":case"dbSsl":{const s=i||e.serviceName;return U(n,t,s,e)}default:throw new Error(`Unsupported template token "{${n}${i?`:${i}`:""}}"`)}})}function d(r,e){return Object.fromEntries(Object.entries(r||{}).map(([t,n])=>[t,typeof n=="string"?S(t,n,e):n]))}function S(r,e,t){try{return finalizeString(e,t)}catch(n){const i=n instanceof Error?n.message:l(n);throw new Error(`Service "${t.serviceName}" env ${r}: ${i}`,{cause:n})}}function c(r,e,t){return Object.fromEntries(Object.entries(r||{}).filter(([n,i])=>e&&!e.has(n)?!1:!(t&&typeof i=="string"&&findDatabasePlaceholders(i).length>0)))}export function findDatabasePlaceholders(r){return[...r.matchAll(/\{db(?:Url|Host|Port|Name|User|Password|Ssl)(?::([^}]+))?\}/g)].map(e=>({placeholder:e[0],serviceName:e.at(1)??null}))}function U(r,e,t,n){const i=n.stateDirByService?.get(t),s=n.databaseBindingsByService?n.databaseBindingsByService.get(t):i?n.databaseBindingsReader?.(i):null;if(!s)throw new Error(`Cannot resolve ${e}: service "${t}" has no database binding in this context`);switch(r){case"dbUrl":return s.url;case"dbHost":return s.host;case"dbPort":return String(s.port);case"dbName":return s.database;case"dbUser":return s.user;case"dbPassword":return s.password;case"dbSsl":return D(s)}}function D(r){const e=r.sslMode,t=l(e||"").trim();return t&&!B(t)?t:h(r.sslMode)?"1":"0"}function h(r){const e=l(r||"").trim().toLowerCase();return!!(e&&!["0","false","no","off","disable","disabled"].includes(e))}function B(r){return["0","1","true","false","yes","no","on","off","disable","disabled","require","verify-ca","verify-full"].includes(l(r||"").trim().toLowerCase())}function l(r){const e=Reflect.apply(String,void 0,[r]);if(typeof e!="string")throw new Error("String conversion returned an invalid result");return e}
@@ -1,2 +1,2 @@
1
- export { buildExecutionEnv, buildTaskExecutionEnv, buildTemplateContext, buildTemplateExecutionEnv, finalizeSerializable, finalizeString, resolveTemplateString, type BuildExecutionEnvironmentOptions, type DatabaseTemplateBindings, type DatabaseTemplateBindingsReader, type ExecutionEnvironmentConfig, type ExecutionEnvironmentLease, type ExecutionEnvironmentLocalConfig, type ExecutionEnvironmentTestkitConfig, type ExecutionTemplateContextValues, type RuntimeUrlMapping, type TemplateResolutionContext, } from "./environment.js";
1
+ export { buildExecutionEnv, buildTaskExecutionEnv, buildTemplateContext, buildTemplateExecutionEnv, finalizeSerializable, finalizeString, findDatabasePlaceholders, resolveExecutionEnvValues, resolveTemplateString, type BuildExecutionEnvironmentOptions, type DatabasePlaceholder, type DatabaseTemplateBindings, type DatabaseTemplateBindingsReader, type ExecutionEnvironmentConfig, type ExecutionEnvironmentLease, type ExecutionEnvironmentLocalConfig, type ExecutionEnvironmentTestkitConfig, type ExecutionTemplateContextValues, type RuntimeUrlMapping, type TemplateResolutionContext, } from "./environment.js";
2
2
  export { collectConfiguredInputs, configuredCommandInvocation, parseModuleSpecifier, resolveConfiguredCwd, resolveConfiguredPath, type ConfiguredCommandInvocation, type ConfiguredStepReporter, type ConfiguredStepServiceConfig, } from "./run.js";
@@ -1 +1 @@
1
- export{buildExecutionEnv,buildTaskExecutionEnv,buildTemplateContext,buildTemplateExecutionEnv,finalizeSerializable,finalizeString,resolveTemplateString}from"./environment.js";export{collectConfiguredInputs,configuredCommandInvocation,parseModuleSpecifier,resolveConfiguredCwd,resolveConfiguredPath}from"./run.js";
1
+ export{buildExecutionEnv,buildTaskExecutionEnv,buildTemplateContext,buildTemplateExecutionEnv,finalizeSerializable,finalizeString,findDatabasePlaceholders,resolveExecutionEnvValues,resolveTemplateString}from"./environment.js";export{collectConfiguredInputs,configuredCommandInvocation,parseModuleSpecifier,resolveConfiguredCwd,resolveConfiguredPath}from"./run.js";
@@ -1,4 +1,4 @@
1
- import _ from"crypto";import f from"fs";import u from"path";import{prepareCommandForSpawn as I,resolveCommandForSpawn as A}from"../process/host/platform.js";import{normalizeContainedRelativePath as V,resolveContainedPath as D}from"../process/files/contained-path.js";import{deriveSemverMinimum as k,isExactSemver as L,normalizeSemver as p,satisfiesSemver as g}from"./semver.js";import{managedHostProcessInfrastructureError as M,runManagedHostProcess as j}from"../process/index.js";import{managedNodeRelease as H}from"./managed-node-releases.js";import{provisionNodeToolchainWithRelease as R}from"./node-provisioning.js";import{createSharedToolchainOperation as F,waitForSharedToolchainOperation as P}from"./shared-operation.js";import{TOOLCHAIN_METADATA_TIMEOUT_MS as $}from"./timeouts.js";const z=".",W="auto",q="require-host",U=600*1e3,B=['"use strict";','const { spawn } = require("node:child_process");',"const [command, ...args] = process.argv.slice(1);",'if (typeof command !== "string" || command.length === 0) {',' process.stderr.write("Missing Windows toolchain command\\n");'," process.exitCode = 126;","} else {"," let spawnFailed = false;"," const child = spawn(command, args, {"," shell: false,",' stdio: "inherit",'," windowsHide: true,"," windowsVerbatimArguments: true,"," });",' child.once("error", (error) => {'," spawnFailed = true;"," process.stderr.write(`${error.stack ?? String(error)}\\n`);"," process.exitCode = 126;"," });",' child.once("close", (code, signal) => {'," if (spawnFailed) return;"," if (signal !== null) {"," process.stderr.write(`Windows toolchain command exited on signal ${signal}\\n`);"," process.exitCode = 1;"," return;"," }"," process.exitCode = code ?? 1;"," });","}"].join(`
2
- `),T=new WeakMap,O=new WeakSet;export function nodeToolchain(n={}){return{...n}}export function normalizeToolchainRegistry(n){if(n==null)return{};if(!N(n))throw new Error("testkit.config.ts toolchains must be an object");const e={};for(const[o,r]of Object.entries(n))e[o]=x(r,`testkit.config.ts toolchains.${o}`);return e}export function normalizeRuntimeToolchain(n,e,o={}){if(n==null)return null;if(typeof n=="string"){const t=n.trim();if(!t)throw new Error(`${e} must reference a non-empty toolchain name`);const i=o[t];if(!i)throw new Error(`${e} references unknown toolchain "${t}"`);return{...i,refName:t,sourceLabel:`${e} -> ${t}`}}return{...x(n,e),refName:null,sourceLabel:e}}export async function resolveConfiguredToolchain(n,e={}){const o=n?.testkit?.runtime?.toolchain;if(!n||!o)return null;let r=T.get(n);if(r===void 0){r=F(i=>J(n,o,{...e,signal:i})),T.set(n,r);const t=r;r.promise.catch(()=>{T.get(n)===t&&T.delete(n)})}return P(r,e.signal)}export function announceResolvedToolchain(n,e,o=null){return!e||O.has(n)||(O.add(n),o?.toolchainResolved&&o.toolchainResolved(n,e)),Promise.resolve()}export function applyToolchainEnv(n,e,o=process.env){if(!e)return{...n};const r={...n,PATH:ne(e.binDir,n.PATH||o.PATH||""),TESTKIT_TOOLCHAIN_KIND:"node",TESTKIT_TOOLCHAIN_NODE_VERSION:e.nodeVersion,TESTKIT_TOOLCHAIN_NODE_SOURCE:e.nodeSource,TESTKIT_TOOLCHAIN_INSTALL_MODE:e.install};return e.npmVersion&&(r.TESTKIT_TOOLCHAIN_NPM_VERSION=e.npmVersion,r.TESTKIT_TOOLCHAIN_NPM_SOURCE=e.npmSource),r}function x(n,e){if(!N(n))throw new Error(`${e} must be an object`);const o=V(d(n.cwd)||z,`${e}.cwd`),r=d(n.detect)||W,t=d(n.install)||q;if(r!=="auto"&&r!=="off")throw new Error(`${e}.detect must be "auto" or "off"`);if(t!=="require-host"&&t!=="download")throw new Error(`${e}.install must be "require-host" or "download"`);return{cwd:o,detect:r,install:t,node:d(n.node),npm:d(n.npm)}}async function J(n,e,o){const r=n.productDir,t=D(r,e.cwd,`${e.sourceLabel}.cwd`),i=K(t),s=e.node||(e.detect==="auto"?i.nodeSpec:null),a=e.node?"toolchain.node":e.detect==="auto"?i.nodeSource??"none":"none",l=e.npm||(e.detect==="auto"?i.npmSpec:null),c=e.npm?"toolchain.npm":e.detect==="auto"?i.npmSource??"none":"none";if(!s&&!l)throw new Error(`Service "${n.name}" enables runtime.toolchain but no Node/npm version could be resolved in ${e.cwd}`);return e.install==="require-host"?G({config:n,cwd:t,nodeSpec:s,nodeSource:a,npmSpec:l,npmSource:c,toolchain:e,options:o}):Q({config:n,cwd:t,nodeSpec:s,nodeSource:a,npmSpec:l,npmSource:c,toolchain:e,options:o})}function K(n){const e=u.join(n,"package.json"),o=Z(e),r=ee(u.join(n,".tool-versions")),t=b(u.join(n,".nvmrc")),i=b(u.join(n,".node-version")),s=d(w(o,"volta","node")),a=d(w(o,"engines","node")),l=d(r.nodejs),c=s||t||i||l||a;let m=null;s?m="package.json#volta.node":t?m=".nvmrc":i?m=".node-version":l?m=".tool-versions#nodejs":a&&(m="package.json#engines.node");const y=d(w(o,"volta","npm")),E=d(w(o,"engines","npm")),v=Y(w(o,"packageManager")),C=y||(v?.name==="npm"?v.version:null)||E;let h=null;return y?h="package.json#volta.npm":v?.name==="npm"?h="package.json#packageManager":E&&(h="package.json#engines.npm"),{nodeSpec:c,nodeSource:m,npmSpec:C,npmSource:h}}async function G({config:n,cwd:e,nodeSpec:o,nodeSource:r,npmSpec:t,npmSource:i,toolchain:s,options:a}){const l=p(a.hostNodeVersion||process.versions.node);if(o&&!g(l,o))throw new Error(`Service "${n.name}" requires Node ${o} (${r}), but host Node is ${l}`);let c=null;if(t&&(c=p(a.hostNpmVersion||await X({command:"npm",args:["--version"],cwd:e,env:process.env,commandExecutor:a.commandExecutor,signal:a.signal})),!g(c,t)))throw new Error(`Service "${n.name}" requires npm ${t} (${i}), but host npm is ${c}`);return{install:s.install,cwd:s.cwd,nodeVersion:l,nodeSource:r,nodeSpec:o,npmVersion:c,npmSource:i,npmSpec:t,nodeExecutable:process.execPath,binDir:u.dirname(process.execPath),toolchainDir:u.dirname(process.execPath),fingerprint:S({install:s.install,nodeVersion:l,npmVersion:c,nodeSource:r,npmSource:i}),summary:`${s.install} node=${l}${c?` npm=${c}`:""} (${r})`}}async function Q({config:n,nodeSpec:e,nodeSource:o,npmSpec:r,npmSource:t,toolchain:i,options:s}){if(!e)throw new Error(`Service "${n.name}" download toolchain mode requires a resolvable Node version`);const a=L(e)?p(e):k(e);if(!a)throw new Error(`Service "${n.name}" cannot deterministically provision Node from "${e}". Provide an exact version in runtime.toolchain.node, .nvmrc, .node-version, .tool-versions, or package.json#volta.node.`);const l=await provisionNodeToolchain({productDir:n.productDir,version:a,fetchImpl:s.fetchImpl||fetchToolchain,commandExecutor:s.commandExecutor||executeToolchainCommand,signal:s.signal});if(!g(a,e))throw new Error(`Service "${n.name}" resolved Node ${a}, which does not satisfy ${e}`);const c=l.npmVersion?p(l.npmVersion):null;if(r&&(!c||!g(c,r)))throw new Error(`Service "${n.name}" requires npm ${r} (${t}), but Node ${a} bundles npm ${c||"unknown"}`);return{install:i.install,cwd:i.cwd,nodeVersion:a,nodeSource:o,nodeSpec:e,npmVersion:c,npmSource:t,npmSpec:r,nodeExecutable:l.nodeExecutable,binDir:l.binDir,toolchainDir:l.toolchainDir,fingerprint:S({install:i.install,nodeVersion:a,npmVersion:c,nodeSource:o,npmSource:t}),summary:`download node=${a}${c?` npm=${c}`:""} (${o})`}}export async function provisionNodeToolchain({productDir:n,version:e,fetchImpl:o=fetchToolchain,commandExecutor:r=executeToolchainCommand,signal:t}){const i=p(e);if(e!==i)throw new Error(`Downloaded Node toolchain version must use canonical x.y.z form; received ${JSON.stringify(e)}`);const s=H(i);return R({commandExecutor:r,fetchImpl:o,productDir:n,release:s,signal:t})}function S(n){return _.createHash("sha256").update(JSON.stringify({kind:"node",...n})).digest("hex")}async function X({command:n,args:e,cwd:o,env:r,commandExecutor:t=executeToolchainCommand,signal:i}){const s=await t(n,e,{cwd:o,env:r,signal:i,stdio:"pipe",timeoutMs:$,windowsHide:!0});return s.stdout==null?"":typeof s.stdout=="string"?s.stdout.trim():Buffer.from(s.stdout).toString().trim()}function Y(n){const e=d(n);if(!e)return null;const o=e.lastIndexOf("@");return o<=0||o===e.length-1?null:{name:e.slice(0,o),version:e.slice(o+1)}}function Z(n){if(!f.existsSync(n))return null;try{return JSON.parse(f.readFileSync(n,"utf8"))}catch{return null}}function b(n){if(!f.existsSync(n))return null;const o=f.readFileSync(n,"utf8").split(/\r?\n/).find(r=>r.trim().length>0);return o?o.trim():null}function ee(n){if(!f.existsSync(n))return{};const e={};for(const o of f.readFileSync(n,"utf8").split(/\r?\n/)){const r=o.trim();if(!r||r.startsWith("#"))continue;const[t,i]=r.split(/\s+/,2);t&&i&&(e[t]=i.trim())}return e}function ne(n,e){const o=d(n);if(!o)return e;const r=(e||"").split(u.delimiter).filter(Boolean).filter(t=>t!==o);return[o,...r].join(u.delimiter)}function d(n){if(typeof n!="string")return null;const e=n.trim();return e.length>0?e:null}export async function fetchToolchain(n,e={}){const o=AbortSignal.timeout(U),r=await fetch(n,{signal:e.signal===void 0?o:AbortSignal.any([e.signal,o])});return{arrayBuffer:()=>r.arrayBuffer(),body:r.body===null?null:oe(r.body),ok:r.ok,status:r.status}}async function*oe(n){const e=n.getReader();let o=!1;try{for(;;){const r=await e.read();if(r.done){o=!0;return}yield r.value}}finally{o||await e.cancel(),e.releaseLock()}}export function prepareToolchainCommandForManagedHost(n,e,o={}){const r={env:o.env||process.env,platform:o.platform},t=A(n,r),i=I(t,e,r);return i.windowsVerbatimArguments?(re(t,e),{executable:o.hostNodeExecutable||process.execPath,args:["--input-type=commonjs","--eval",B,"--",i.command,...i.args]}):{executable:i.command,args:i.args}}function re(n,e){if(/[%!"\0\r\n]/u.test(n))throw new Error(`Windows batch toolchain commands cannot safely preserve command path ${JSON.stringify(n)}`);const o=e.find(r=>/[%!"\0\r\n]/u.test(r)||r.endsWith("\\"));if(o!==void 0)throw new Error(`Windows batch toolchain commands cannot safely preserve argument ${JSON.stringify(o)}`)}export async function executeToolchainCommand(n,e,o={}){const r=te(o.timeoutMs),t=u.resolve(o.cwd||process.cwd()),i=prepareToolchainCommandForManagedHost(n,e,{env:o.env||process.env}),s=await j({args:i.args,cwd:t,displayLabel:`Toolchain command ${n}`,env:o.env||process.env,executable:i.executable,signal:o.signal,timeoutMs:r}),a=M(`Toolchain command ${n}`,s);if(a)throw a;if(s.exitCode!==0)throw new Error(`Toolchain command ${n} exited with code ${String(s.exitCode)}${ie(s.stdout,s.stderr)}`);return{stdout:s.stdout}}function te(n){const e=n??$;if(!Number.isSafeInteger(e)||e<=0)throw new Error("Toolchain command timeout must be a positive safe integer in milliseconds");return e}function ie(n,e){const o=[n,e].map(r=>r.trim()).filter(Boolean).join(`
1
+ import _ from"crypto";import f from"fs";import u from"path";import{prepareCommandForSpawn as I,resolveCommandForSpawn as A}from"../process/host/platform.js";import{normalizeContainedRelativePath as V,resolveContainedPath as D}from"../process/files/contained-path.js";import{deriveSemverMinimum as k,isExactSemver as L,normalizeSemver as p,satisfiesSemver as g}from"./semver.js";import{managedHostProcessInfrastructureError as M,runManagedHostProcess as j}from"../process/index.js";import{managedNodeRelease as H}from"./managed-node-releases.js";import{createSharedToolchainOperation as R,waitForSharedToolchainOperation as F}from"./shared-operation.js";import{TOOLCHAIN_METADATA_TIMEOUT_MS as $}from"./timeouts.js";const P=".",z="auto",W="require-host",q=600*1e3,U=['"use strict";','const { spawn } = require("node:child_process");',"const [command, ...args] = process.argv.slice(1);",'if (typeof command !== "string" || command.length === 0) {',' process.stderr.write("Missing Windows toolchain command\\n");'," process.exitCode = 126;","} else {"," let spawnFailed = false;"," const child = spawn(command, args, {"," shell: false,",' stdio: "inherit",'," windowsHide: true,"," windowsVerbatimArguments: true,"," });",' child.once("error", (error) => {'," spawnFailed = true;"," process.stderr.write(`${error.stack ?? String(error)}\\n`);"," process.exitCode = 126;"," });",' child.once("close", (code, signal) => {'," if (spawnFailed) return;"," if (signal !== null) {"," process.stderr.write(`Windows toolchain command exited on signal ${signal}\\n`);"," process.exitCode = 1;"," return;"," }"," process.exitCode = code ?? 1;"," });","}"].join(`
2
+ `),T=new WeakMap,O=new WeakSet;export function nodeToolchain(n={}){return{...n}}export function normalizeToolchainRegistry(n){if(n==null)return{};if(!N(n))throw new Error("testkit.config.ts toolchains must be an object");const e={};for(const[o,r]of Object.entries(n))e[o]=x(r,`testkit.config.ts toolchains.${o}`);return e}export function normalizeRuntimeToolchain(n,e,o={}){if(n==null)return null;if(typeof n=="string"){const t=n.trim();if(!t)throw new Error(`${e} must reference a non-empty toolchain name`);const i=o[t];if(!i)throw new Error(`${e} references unknown toolchain "${t}"`);return{...i,refName:t,sourceLabel:`${e} -> ${t}`}}return{...x(n,e),refName:null,sourceLabel:e}}export async function resolveConfiguredToolchain(n,e={}){const o=n?.testkit?.runtime?.toolchain;if(!n||!o)return null;let r=T.get(n);if(r===void 0){r=R(i=>B(n,o,{...e,signal:i})),T.set(n,r);const t=r;r.promise.catch(()=>{T.get(n)===t&&T.delete(n)})}return F(r,e.signal)}export function announceResolvedToolchain(n,e,o=null){return!e||O.has(n)||(O.add(n),o?.toolchainResolved&&o.toolchainResolved(n,e)),Promise.resolve()}export function applyToolchainEnv(n,e,o=process.env){if(!e)return{...n};const r={...n,PATH:ee(e.binDir,n.PATH||o.PATH||""),TESTKIT_TOOLCHAIN_KIND:"node",TESTKIT_TOOLCHAIN_NODE_VERSION:e.nodeVersion,TESTKIT_TOOLCHAIN_NODE_SOURCE:e.nodeSource,TESTKIT_TOOLCHAIN_INSTALL_MODE:e.install};return e.npmVersion&&(r.TESTKIT_TOOLCHAIN_NPM_VERSION=e.npmVersion,r.TESTKIT_TOOLCHAIN_NPM_SOURCE=e.npmSource),r}function x(n,e){if(!N(n))throw new Error(`${e} must be an object`);const o=V(d(n.cwd)||P,`${e}.cwd`),r=d(n.detect)||z,t=d(n.install)||W;if(r!=="auto"&&r!=="off")throw new Error(`${e}.detect must be "auto" or "off"`);if(t!=="require-host"&&t!=="download")throw new Error(`${e}.install must be "require-host" or "download"`);return{cwd:o,detect:r,install:t,node:d(n.node),npm:d(n.npm)}}async function B(n,e,o){const r=n.productDir,t=D(r,e.cwd,`${e.sourceLabel}.cwd`),i=J(t),s=e.node||(e.detect==="auto"?i.nodeSpec:null),a=e.node?"toolchain.node":e.detect==="auto"?i.nodeSource??"none":"none",l=e.npm||(e.detect==="auto"?i.npmSpec:null),c=e.npm?"toolchain.npm":e.detect==="auto"?i.npmSource??"none":"none";if(!s&&!l)throw new Error(`Service "${n.name}" enables runtime.toolchain but no Node/npm version could be resolved in ${e.cwd}`);return e.install==="require-host"?K({config:n,cwd:t,nodeSpec:s,nodeSource:a,npmSpec:l,npmSource:c,toolchain:e,options:o}):G({config:n,cwd:t,nodeSpec:s,nodeSource:a,npmSpec:l,npmSource:c,toolchain:e,options:o})}function J(n){const e=u.join(n,"package.json"),o=Y(e),r=Z(u.join(n,".tool-versions")),t=b(u.join(n,".nvmrc")),i=b(u.join(n,".node-version")),s=d(w(o,"volta","node")),a=d(w(o,"engines","node")),l=d(r.nodejs),c=s||t||i||l||a;let m=null;s?m="package.json#volta.node":t?m=".nvmrc":i?m=".node-version":l?m=".tool-versions#nodejs":a&&(m="package.json#engines.node");const y=d(w(o,"volta","npm")),E=d(w(o,"engines","npm")),v=X(w(o,"packageManager")),C=y||(v?.name==="npm"?v.version:null)||E;let h=null;return y?h="package.json#volta.npm":v?.name==="npm"?h="package.json#packageManager":E&&(h="package.json#engines.npm"),{nodeSpec:c,nodeSource:m,npmSpec:C,npmSource:h}}async function K({config:n,cwd:e,nodeSpec:o,nodeSource:r,npmSpec:t,npmSource:i,toolchain:s,options:a}){const l=p(a.hostNodeVersion||process.versions.node);if(o&&!g(l,o))throw new Error(`Service "${n.name}" requires Node ${o} (${r}), but host Node is ${l}`);let c=null;if(t&&(c=p(a.hostNpmVersion||await Q({command:"npm",args:["--version"],cwd:e,env:process.env,commandExecutor:a.commandExecutor,signal:a.signal})),!g(c,t)))throw new Error(`Service "${n.name}" requires npm ${t} (${i}), but host npm is ${c}`);return{install:s.install,cwd:s.cwd,nodeVersion:l,nodeSource:r,nodeSpec:o,npmVersion:c,npmSource:i,npmSpec:t,nodeExecutable:process.execPath,binDir:u.dirname(process.execPath),toolchainDir:u.dirname(process.execPath),fingerprint:S({install:s.install,nodeVersion:l,npmVersion:c,nodeSource:r,npmSource:i}),summary:`${s.install} node=${l}${c?` npm=${c}`:""} (${r})`}}async function G({config:n,nodeSpec:e,nodeSource:o,npmSpec:r,npmSource:t,toolchain:i,options:s}){if(!e)throw new Error(`Service "${n.name}" download toolchain mode requires a resolvable Node version`);const a=L(e)?p(e):k(e);if(!a)throw new Error(`Service "${n.name}" cannot deterministically provision Node from "${e}". Provide an exact version in runtime.toolchain.node, .nvmrc, .node-version, .tool-versions, or package.json#volta.node.`);const l=await provisionNodeToolchain({productDir:n.productDir,version:a,fetchImpl:s.fetchImpl||fetchToolchain,commandExecutor:s.commandExecutor||executeToolchainCommand,signal:s.signal});if(!g(a,e))throw new Error(`Service "${n.name}" resolved Node ${a}, which does not satisfy ${e}`);const c=l.npmVersion?p(l.npmVersion):null;if(r&&(!c||!g(c,r)))throw new Error(`Service "${n.name}" requires npm ${r} (${t}), but Node ${a} bundles npm ${c||"unknown"}`);return{install:i.install,cwd:i.cwd,nodeVersion:a,nodeSource:o,nodeSpec:e,npmVersion:c,npmSource:t,npmSpec:r,nodeExecutable:l.nodeExecutable,binDir:l.binDir,toolchainDir:l.toolchainDir,fingerprint:S({install:i.install,nodeVersion:a,npmVersion:c,nodeSource:o,npmSource:t}),summary:`download node=${a}${c?` npm=${c}`:""} (${o})`}}export async function provisionNodeToolchain({productDir:n,version:e,fetchImpl:o=fetchToolchain,commandExecutor:r=executeToolchainCommand,signal:t}){const i=p(e);if(e!==i)throw new Error(`Downloaded Node toolchain version must use canonical x.y.z form; received ${JSON.stringify(e)}`);const s=H(i),{provisionNodeToolchainWithRelease:a}=await import("./node-provisioning.js");return a({commandExecutor:r,fetchImpl:o,productDir:n,release:s,signal:t})}function S(n){return _.createHash("sha256").update(JSON.stringify({kind:"node",...n})).digest("hex")}async function Q({command:n,args:e,cwd:o,env:r,commandExecutor:t=executeToolchainCommand,signal:i}){const s=await t(n,e,{cwd:o,env:r,signal:i,stdio:"pipe",timeoutMs:$,windowsHide:!0});return s.stdout==null?"":typeof s.stdout=="string"?s.stdout.trim():Buffer.from(s.stdout).toString().trim()}function X(n){const e=d(n);if(!e)return null;const o=e.lastIndexOf("@");return o<=0||o===e.length-1?null:{name:e.slice(0,o),version:e.slice(o+1)}}function Y(n){if(!f.existsSync(n))return null;try{return JSON.parse(f.readFileSync(n,"utf8"))}catch{return null}}function b(n){if(!f.existsSync(n))return null;const o=f.readFileSync(n,"utf8").split(/\r?\n/).find(r=>r.trim().length>0);return o?o.trim():null}function Z(n){if(!f.existsSync(n))return{};const e={};for(const o of f.readFileSync(n,"utf8").split(/\r?\n/)){const r=o.trim();if(!r||r.startsWith("#"))continue;const[t,i]=r.split(/\s+/,2);t&&i&&(e[t]=i.trim())}return e}function ee(n,e){const o=d(n);if(!o)return e;const r=(e||"").split(u.delimiter).filter(Boolean).filter(t=>t!==o);return[o,...r].join(u.delimiter)}function d(n){if(typeof n!="string")return null;const e=n.trim();return e.length>0?e:null}export async function fetchToolchain(n,e={}){const o=AbortSignal.timeout(q),r=await fetch(n,{signal:e.signal===void 0?o:AbortSignal.any([e.signal,o])});return{arrayBuffer:()=>r.arrayBuffer(),body:r.body===null?null:ne(r.body),ok:r.ok,status:r.status}}async function*ne(n){const e=n.getReader();let o=!1;try{for(;;){const r=await e.read();if(r.done){o=!0;return}yield r.value}}finally{o||await e.cancel(),e.releaseLock()}}export function prepareToolchainCommandForManagedHost(n,e,o={}){const r={env:o.env||process.env,platform:o.platform},t=A(n,r),i=I(t,e,r);return i.windowsVerbatimArguments?(oe(t,e),{executable:o.hostNodeExecutable||process.execPath,args:["--input-type=commonjs","--eval",U,"--",i.command,...i.args]}):{executable:i.command,args:i.args}}function oe(n,e){if(/[%!"\0\r\n]/u.test(n))throw new Error(`Windows batch toolchain commands cannot safely preserve command path ${JSON.stringify(n)}`);const o=e.find(r=>/[%!"\0\r\n]/u.test(r)||r.endsWith("\\"));if(o!==void 0)throw new Error(`Windows batch toolchain commands cannot safely preserve argument ${JSON.stringify(o)}`)}export async function executeToolchainCommand(n,e,o={}){const r=re(o.timeoutMs),t=u.resolve(o.cwd||process.cwd()),i=prepareToolchainCommandForManagedHost(n,e,{env:o.env||process.env}),s=await j({args:i.args,cwd:t,displayLabel:`Toolchain command ${n}`,env:o.env||process.env,executable:i.executable,signal:o.signal,timeoutMs:r}),a=M(`Toolchain command ${n}`,s);if(a)throw a;if(s.exitCode!==0)throw new Error(`Toolchain command ${n} exited with code ${String(s.exitCode)}${te(s.stdout,s.stderr)}`);return{stdout:s.stdout}}function re(n){const e=n??$;if(!Number.isSafeInteger(e)||e<=0)throw new Error("Toolchain command timeout must be a positive safe integer in milliseconds");return e}function te(n,e){const o=[n,e].map(r=>r.trim()).filter(Boolean).join(`
3
3
  `);return o?`
4
4
  ${o.slice(-4e3)}`:""}function w(n,...e){let o=n;for(const r of e){if(!N(o))return;o=o[r]}return o}function N(n){return n!==null&&typeof n=="object"&&!Array.isArray(n)}
@@ -522,7 +522,10 @@ installation.
522
522
  Node/Vitest resolves Vitest, Vite, TypeScript, and esbuild through this anchor.
523
523
  Generated configuration, authoring aliases, helpers, and subprocesses use the
524
524
  same managed paths. Evidence reports the direct tools actually exercised by
525
- the run and does not enumerate private native bindings.
525
+ the run and does not enumerate private native bindings. Healthy runs exercise
526
+ these capabilities in the native engine process. The controller imports and
527
+ checks them only to diagnose a failed engine launch with no native report;
528
+ it does not run a duplicate empty compilation before each run.
526
529
 
527
530
  Playwright selects one physical family: `@playwright/test` from the Testkit
528
531
  anchor, `playwright` from that package, and `playwright-core` from the selected
@@ -38,6 +38,15 @@ missing declaration, dynamic engine value, or unsupported pair is a discovery
38
38
  error. The profile is derived from the validated pair and is carried unchanged
39
39
  through scheduling, results, and immutable run evidence.
40
40
 
41
+ Run selection by `--file`, `--service`, `--type`, or `--suite` narrows the files
42
+ whose engine metadata is read and validated. Testkit uses discovered paths,
43
+ canonical type suffixes, and suite names to select candidates; each selected
44
+ file must still declare a valid engine. Invalid metadata in an unselected file
45
+ does not block that focused run. Root configuration, service dependencies, and
46
+ configured skip, requirement, and isolation selectors remain validated against
47
+ the complete discovered project. `discover`, `doctor`, and `typecheck` validate
48
+ metadata across the whole project.
49
+
41
50
  Canonical filename suffixes:
42
51
 
43
52
  - `*.ui.testkit.ts`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gfdelarue/testkit",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "Test orchestration platform for product acceptance and QA workflows",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",
@@ -123,6 +123,7 @@
123
123
  "@playwright/test": "1.59.1",
124
124
  "@types/node": "22.20.1",
125
125
  "@typescript-eslint/parser": "8.65.0",
126
+ "@typescript-eslint/types": "8.65.0",
126
127
  "@typescript-eslint/utils": "8.65.0",
127
128
  "diff": "^9.0.0",
128
129
  "esbuild": "0.28.1",
@@ -333,11 +333,63 @@ Template tokens are supported in strings:
333
333
  - `{port}` / `{port:serviceName}`
334
334
  - `{baseUrl}` / `{baseUrl:serviceName}`
335
335
  - `{readyUrl}` / `{readyUrl:serviceName}`
336
+ - `{dbUrl}`, `{dbHost}`, `{dbPort}`, `{dbName}`, `{dbUser}`, `{dbPassword}`,
337
+ `{dbSsl}`, each with an optional `:serviceName` (env values only, see
338
+ [`env`](#env))
336
339
 
337
340
  ### `dependsOn`
338
341
 
339
342
  Runtime dependencies for service startup order.
340
343
 
344
+ ### `env`
345
+
346
+ Declared environment values for the service process and every task that targets
347
+ the service. Plain services take a string record. `app.node(...)` and
348
+ `app.next(...)` take `{ values, databases }` and place the result under
349
+ `local.env`; `local.env` overrides `env` when both set a name.
350
+
351
+ Values support the template tokens listed under [`local`](#local), including
352
+ the database tokens:
353
+
354
+ | Token | Value |
355
+ | -------------- | --------------------------------------- |
356
+ | `{dbUrl}` | connection URL |
357
+ | `{dbHost}` | host |
358
+ | `{dbPort}` | host port |
359
+ | `{dbName}` | database name |
360
+ | `{dbUser}` | user |
361
+ | `{dbPassword}` | password |
362
+ | `{dbSsl}` | `1` when the binding uses SSL, else `0` |
363
+
364
+ The bare form reads the service's own database, which must be declared under
365
+ `database`. `{dbUrl:api}` reads the database of service `api`, which must be
366
+ listed in `dependsOn` so it is provisioned first. Testkit resolves these tokens
367
+ after provisioning, when it runs `runtime.prepare` steps, starts the service
368
+ process, and builds each task environment, from the database provisioned for
369
+ that run. A repeated run never sees a previous run's connection details.
370
+
371
+ Database tokens resolve only in `env` and `local.env` values. Testkit rejects
372
+ them at config load in `local.start`, `local.cwd`, `runtime.prepare`, and
373
+ `database.template` strings, because it resolves those strings while laying out
374
+ the runtime, before any database exists. Commands read `DATABASE_URL` or a
375
+ declared env entry at run time instead.
376
+
377
+ Source-schema discovery fails when the env entry it reads as the source URL
378
+ points at the service's own database. A source schema has to come from a
379
+ database other than the one it verifies.
380
+
381
+ `env.databases` expands a binding into prefixed entries:
382
+
383
+ ```ts
384
+ env: {
385
+ databases: { api: { prefix: "API", service: "api" } },
386
+ }
387
+ ```
388
+
389
+ produces `API_DATABASE_HOST`, `API_DATABASE_PORT`, `API_DATABASE_NAME`,
390
+ `API_DATABASE_USER`, `API_DATABASE_PASSWORD`, and `API_DATABASE_SSL`, each set
391
+ to the matching `{db*:api}` token. `env.values` entries override expanded names.
392
+
341
393
  ### `envFile` / `envFiles`
342
394
 
343
395
  Optional extra env files loaded into the declared service/task environment for
@@ -1,4 +0,0 @@
1
- import type { ArtifactInspectionEntry } from "../../app/inspect/artifacts.js";
2
- type RunArtifactPayload = NonNullable<ArtifactInspectionEntry["payload"]>;
3
- export declare function formatArtifactPreview(payload: RunArtifactPayload | null, maxLines?: number): string[];
4
- export {};
@@ -1 +0,0 @@
1
- import{asUnknownRecord as u,isUnknownRecord as d,optionalString as n}from"../../kernel/unknown-values.js";export function formatArtifactPreview(t,r=6){if(!t)return["artifact payload missing"];const s=u(t.data);if(t.kind==="testkit.data-flow")return k(t,r);if(t.kind==="testkit.data-flow.report"||t.contentType==="text/markdown")return(t.text??n(s?.text)??"").split(/\r?\n/).filter(i=>i.trim().length>0).slice(0,r);if(t.kind==="testkit.http-traces")return w(t,r);if(t.contentType==="text/plain"&&(typeof s?.text=="string"||typeof t.text=="string"))return(n(s?.text)??t.text??"").split(/\r?\n/).filter(i=>i.trim().length>0).slice(0,r);const o=t.data===void 0?t:t.data;return JSON.stringify(o,null,2).split(/\r?\n/).slice(0,r)}function k(t,r){const s=u(t.data),o=s?.kind==="testkit.data-flow"?s:t,e=[],i=n(o.summaryText);i&&e.push(i);for(const c of p(o.scenarios)){const a=n(c.status)??"unknown",f=n(c.name)??"scenario";e.push(`${a.toUpperCase()} ${f}`);for(const m of p(c.budgetFailures)){const g=n(m.message);g&&e.push(`Budget: ${g}`)}}return e.slice(0,r)}function w(t,r){const s=p(u(t.data)?.traces),o=[];for(const e of s.slice(0,r)){const i=u(e.response),c=n(e.method)??"?",a=n(e.path)??"?",f=n(e.requestId),m=h(i?.status)??"?";o.push(`${c} ${a} -> ${String(m)}${f?` [${f}]`:""}`)}return o}function p(t){return Array.isArray(t)?t.filter(d):[]}function h(t){return typeof t=="string"||typeof t=="number"&&Number.isFinite(t)?t:null}