@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 +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()})}
@@ -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)}
@@ -1,5 +1,8 @@
1
1
  # Cloud command reference
2
2
 
3
+ The `testkit cloud` commands are deprecated. See the
4
+ [Cloud boundary](testkit-cloud-boundary.md) for the direction.
5
+
3
6
  `testkit cloud apply --help` prints the account-command envelope, every supported
4
7
  operation, its scope IDs, and its input fields. This reference ships with Testkit.
5
8
 
@@ -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`
@@ -1,8 +1,10 @@
1
1
  # Testkit Cloud boundary
2
2
 
3
- The local Testkit runner owns execution through publication of an immutable run
4
- bundle. Testkit Cloud owns organisation-wide history, regression state, release
5
- policy, and GitHub automation.
3
+ The `testkit cloud` commands are deprecated. Testkit works without Cloud, and
4
+ bugs and evidence stay in your repository and in GitHub or Gitea. Testkit Cloud
5
+ is becoming an optional analytics dashboard that reads facts from GitHub or
6
+ Gitea instead of receiving run bundles. This document describes the commands
7
+ and the publication contract that still ship.
6
8
 
7
9
  ```text
8
10
  discover -> prepare -> execute -> seal and verify run bundle -> clean up
@@ -13,13 +15,11 @@ discover -> prepare -> execute -> seal and verify run bundle -> clean up
13
15
  Testkit Cloud ingest
14
16
  ```
15
17
 
16
- ## Complete Cloud control
18
+ ## Cloud commands
17
19
 
18
- The CLI is a full control client for Cloud. New or changed product operations
19
- ship their API, installed command or file input, and acceptance coverage together.
20
20
  The local [Cloud command reference](cloud-control.md) lists supported operations
21
21
  and file fields. `testkit cloud apply --help` includes the account-command format.
22
- Cloud continues to own identity, permissions, product state and domain validation.
22
+ Cloud checks identity, permissions, and domain rules on every command.
23
23
 
24
24
  ```sh
25
25
  testkit cloud login
@@ -176,13 +176,13 @@ The token is an environment secret. It is not stored in `testkit.config.ts` or
176
176
  accepted as a command flag. `CI` is execution context only; it does not enable
177
177
  publication or supply authority.
178
178
 
179
- ## Cloud ownership
179
+ ## Cloud ingestion
180
180
 
181
- Testkit Cloud may ingest a verified bundle and derive regression identities,
181
+ Testkit Cloud ingests a verified bundle and derives regression identities,
182
182
  baselines, comparisons, recommendations, GitHub checks, and issue lifecycle
183
183
  actions. Cloud state is separate from the product repository and may be shared
184
- across repositories. Cloud must retain the source bundle identity for every
185
- derived decision so a user can trace a check or issue back to its evidence.
184
+ across repositories. Cloud keeps the source bundle identity for every derived
185
+ decision.
186
186
 
187
187
  Cloud ingestion uses an explicit versioned contract. It rejects an invalid
188
188
  bundle, preserves the underlying run outcome, and reports GitHub projection
@@ -197,21 +197,9 @@ The old `./regression-workflow` package export, runner integration,
197
197
  Git retains their implementation history, but that does not migrate a
198
198
  consumer's live state.
199
199
 
200
- Before a Testkit package containing this removal is published:
201
-
202
- 1. Inventory known uses of the removed export and state file.
203
- 2. Assign each consumer an owner and decide whether to discard or import its
204
- stored state.
205
- 3. Rehearse required consumers against the exact packed candidate.
206
- 4. Block publication when a required consumer cannot move atomically.
207
-
208
- The removed implementation is useful source material for Testkit Cloud, but it
209
- must not be copied back into the runner. Cloud should consume verified bundles
210
- through its own adapter and own its persistence and GitHub credentials.
211
-
212
200
  ## Report an externally executed campaign
213
201
 
214
- Cloud owns campaign state, finding verification, and counts. A user or agent
202
+ Cloud keeps campaign state, finding verification, and counts. A user or agent
215
203
  starts and runs the investigation independently. These commands never start
216
204
  execution or infer a finding from a failed assertion.
217
205
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gfdelarue/testkit",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
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",
@@ -758,6 +758,8 @@ cloud?: {
758
758
  };
759
759
  ```
760
760
 
761
+ This setting is deprecated with the `testkit cloud` commands.
762
+
761
763
  `cloud.verification.minimumConfirmingRuns` is an integer from 1 through 64,
762
764
  defaulting to 2. It controls whether Testkit submits a new `finding.verify`
763
765
  campaign report. It counts distinct full run IDs from available cited accepted
@@ -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}