@gfdelarue/testkit 0.4.4 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (276) hide show
  1. package/README.md +108 -21
  2. package/bin/testkit-windows-owner-arm64.exe +0 -0
  3. package/bin/testkit-windows-owner-x64.exe +0 -0
  4. package/dist/app/bugs/categories.js +1 -1
  5. package/dist/app/bugs/draft.js +3 -3
  6. package/dist/app/bugs/facts.d.ts +3 -2
  7. package/dist/app/bugs/facts.js +2 -2
  8. package/dist/app/bugs/guide.js +1 -1
  9. package/dist/app/bugs/run-reading.js +1 -1
  10. package/dist/app/db-schema/prepare.js +1 -1
  11. package/dist/app/destroy/index.js +1 -1
  12. package/dist/app/doctor/index.js +1 -1
  13. package/dist/app/environments/config-driven.d.ts +5 -0
  14. package/dist/app/environments/config-driven.js +1 -0
  15. package/dist/app/environments/routing.d.ts +6 -0
  16. package/dist/app/environments/routing.js +1 -0
  17. package/dist/app/environments/supplied.d.ts +3 -0
  18. package/dist/app/environments/supplied.js +1 -0
  19. package/dist/app/file-selection.js +1 -1
  20. package/dist/app/lint/native.js +1 -1
  21. package/dist/app/lint/options.d.ts +0 -1
  22. package/dist/app/lint/options.js +1 -1
  23. package/dist/app/lint/shared.d.ts +0 -2
  24. package/dist/app/lint/shared.js +1 -1
  25. package/dist/app/package-inspection.js +1 -1
  26. package/dist/app/reports/files.d.ts +13 -0
  27. package/dist/app/reports/files.js +1 -0
  28. package/dist/app/reports/prepare.d.ts +20 -0
  29. package/dist/app/reports/prepare.js +2 -0
  30. package/dist/app/reports/publish.d.ts +21 -0
  31. package/dist/app/reports/publish.js +1 -0
  32. package/dist/app/run/engine-evidence-persistence.js +1 -1
  33. package/dist/app/run/engine-execution-cleanup.js +1 -1
  34. package/dist/app/run/engine-execution-failures.js +1 -1
  35. package/dist/app/run/engine-execution.d.ts +10 -8
  36. package/dist/app/run/engine-execution.js +1 -1
  37. package/dist/app/run/engine-orchestration.d.ts +15 -34
  38. package/dist/app/run/engine-orchestration.js +1 -1
  39. package/dist/app/run/environment.d.ts +33 -0
  40. package/dist/app/run/environment.js +1 -0
  41. package/dist/app/run/failure-details.js +1 -1
  42. package/dist/app/run/live-run.d.ts +3 -1
  43. package/dist/app/run/live-run.js +1 -1
  44. package/dist/app/run/orchestrator.d.ts +8 -1
  45. package/dist/app/run/orchestrator.js +1 -1
  46. package/dist/app/run/planning.d.ts +1 -2
  47. package/dist/app/run/planning.js +1 -1
  48. package/dist/app/run/results.d.ts +1 -1
  49. package/dist/app/run/results.js +1 -1
  50. package/dist/app/run/run-finalization.d.ts +6 -2
  51. package/dist/app/run/run-finalization.js +1 -1
  52. package/dist/app/run/runtime-fixtures.js +1 -1
  53. package/dist/app/run/runtime-task-types.d.ts +1 -1
  54. package/dist/app/run/scheduler/index.js +1 -1
  55. package/dist/app/run/scheduler/locks.d.ts +6 -0
  56. package/dist/app/run/scheduler/locks.js +1 -0
  57. package/dist/app/run/selection.d.ts +0 -1
  58. package/dist/app/run/selection.js +1 -1
  59. package/dist/app/run/setup/runtime-node-setup-protocol.js +1 -1
  60. package/dist/app/run/setup/runtime-node-setup-worker.js +1 -1
  61. package/dist/app/run/task-leases.d.ts +15 -0
  62. package/dist/app/run/task-leases.js +1 -0
  63. package/dist/app/run/worker-loop.d.ts +13 -4
  64. package/dist/app/run/worker-loop.js +1 -1
  65. package/dist/app/tracker/index.d.ts +6 -1
  66. package/dist/app/tracker/index.js +1 -1
  67. package/dist/app/typecheck/index.js +4 -4
  68. package/dist/cli/command-catalog.d.ts +19 -0
  69. package/dist/cli/command-catalog.js +1 -1
  70. package/dist/cli/command-registry.js +1 -1
  71. package/dist/cli/commands/report/prepare.d.ts +12 -0
  72. package/dist/cli/commands/report/prepare.js +5 -0
  73. package/dist/cli/commands/report/publish.d.ts +13 -0
  74. package/dist/cli/commands/report/publish.js +2 -0
  75. package/dist/cli/json-errors.js +1 -1
  76. package/dist/cli/operations/run/operation.d.ts +3 -1
  77. package/dist/cli/operations/run/operation.js +2 -2
  78. package/dist/cli/renderers/doctor/text.js +1 -1
  79. package/dist/cli/renderers/run/formatting.js +1 -1
  80. package/dist/config/authoring/index.d.ts +4 -13
  81. package/dist/config/authoring/index.js +1 -1
  82. package/dist/config/authoring/preset-env.js +1 -1
  83. package/dist/config/loader/config-loader.js +1 -1
  84. package/dist/config/loader/database.js +1 -1
  85. package/dist/config/loader/decode-execution.d.ts +0 -2
  86. package/dist/config/loader/decode-execution.js +1 -1
  87. package/dist/config/loader/discover-tests.js +1 -1
  88. package/dist/config/loader/discovery.d.ts +0 -1
  89. package/dist/config/loader/discovery.js +2 -2
  90. package/dist/config/loader/env.js +1 -1
  91. package/dist/config/loader/execution-model.d.ts +0 -1
  92. package/dist/config/loader/execution-model.js +1 -1
  93. package/dist/config/loader/file-metadata.d.ts +1 -1
  94. package/dist/config/loader/file-metadata.js +1 -1
  95. package/dist/config/loader/groups.d.ts +33 -0
  96. package/dist/config/loader/groups.js +1 -0
  97. package/dist/config/loader/load.d.ts +0 -2
  98. package/dist/config/loader/load.js +1 -1
  99. package/dist/config/loader/runtime-isolation.js +1 -1
  100. package/dist/config/loader/runtime.d.ts +0 -9
  101. package/dist/config/loader/runtime.js +1 -1
  102. package/dist/config/loader/skip-config.js +1 -1
  103. package/dist/config/loader/validation.d.ts +1 -2
  104. package/dist/config/loader/validation.js +1 -1
  105. package/dist/config/loader/values.d.ts +0 -1
  106. package/dist/config/loader/values.js +1 -1
  107. package/dist/database/activity-gate.js +1 -1
  108. package/dist/database/admin.js +2 -2
  109. package/dist/database/lifecycle-cleanup.d.ts +1 -0
  110. package/dist/database/lifecycle-cleanup.js +1 -1
  111. package/dist/database/local-postgres.js +1 -1
  112. package/dist/database/naming.js +1 -1
  113. package/dist/database/schema-operations.d.ts +1 -1
  114. package/dist/database/schema-operations.js +1 -1
  115. package/dist/database/schema-source.js +3 -3
  116. package/dist/database/source-url.d.ts +0 -1
  117. package/dist/database/source-url.js +2 -2
  118. package/dist/database/state.js +1 -1
  119. package/dist/database/steps.js +3 -3
  120. package/dist/domain/file-timeout.d.ts +0 -3
  121. package/dist/domain/file-timeout.js +1 -1
  122. package/dist/domain/operation-display.js +1 -1
  123. package/dist/domain/test-outcomes.d.ts +0 -1
  124. package/dist/domain/test-outcomes.js +1 -1
  125. package/dist/engines/adapters/evidence-identity.js +1 -1
  126. package/dist/engines/adapters/execution.d.ts +0 -1
  127. package/dist/engines/adapters/execution.js +1 -1
  128. package/dist/engines/api/capabilities.d.ts +0 -10
  129. package/dist/engines/api/capabilities.js +1 -1
  130. package/dist/engines/api/execution-scope.js +1 -1
  131. package/dist/engines/api/k6-runtime-input.js +1 -1
  132. package/dist/engines/bundle-cache.js +1 -1
  133. package/dist/engines/contracts/fixture-manifest.js +1 -1
  134. package/dist/engines/definitions.d.ts +1 -0
  135. package/dist/engines/definitions.js +1 -1
  136. package/dist/engines/installations/playwright-resolution-child.js +1 -1
  137. package/dist/engines/installations/playwright.js +1 -1
  138. package/dist/engines/k6/adapter/evidence.js +4 -4
  139. package/dist/engines/k6/adapter/executor.js +1 -1
  140. package/dist/engines/k6/adapter/native-summary.js +1 -1
  141. package/dist/engines/k6/runtime/checks.js +1 -1
  142. package/dist/engines/k6/runtime/dal-fixtures.js +1 -1
  143. package/dist/engines/k6/runtime/dal-suite.js +1 -1
  144. package/dist/engines/k6/runtime/http-suite-runtime.js +1 -1
  145. package/dist/engines/k6/runtime/http.js +1 -1
  146. package/dist/engines/k6/runtime/shared/db-normalization.js +1 -1
  147. package/dist/engines/k6/runtime/shared/fixture-engine.js +2 -2
  148. package/dist/engines/k6/runtime/shared/http-parsing.js +3 -3
  149. package/dist/engines/managed-module-resolution-policy-input.d.ts +0 -1
  150. package/dist/engines/managed-module-resolution-policy-input.js +1 -1
  151. package/dist/engines/managed-module-resolution-policy.js +1 -1
  152. package/dist/engines/node-vitest/adapter/adapter.js +1 -1
  153. package/dist/engines/node-vitest/adapter/evidence.js +2 -2
  154. package/dist/engines/node-vitest/adapter/workspace.js +22 -21
  155. package/dist/engines/node-vitest/runtime/contract/index.js +1 -1
  156. package/dist/engines/node-vitest/runtime/data/http.js +1 -1
  157. package/dist/engines/node-vitest/runtime/data/index.d.ts +2 -2
  158. package/dist/engines/node-vitest/runtime/data/index.js +1 -1
  159. package/dist/engines/node-vitest/runtime/data/report.js +5 -5
  160. package/dist/engines/node-vitest/runtime/data/scenario.d.ts +0 -1
  161. package/dist/engines/node-vitest/runtime/data/scenario.js +2 -2
  162. package/dist/engines/node-vitest/runtime/data/types.d.ts +1 -29
  163. package/dist/engines/node-vitest/runtime/runtime-input.js +1 -1
  164. package/dist/engines/playwright/adapter/adapter.js +1 -1
  165. package/dist/engines/playwright/adapter/config.d.ts +1 -1
  166. package/dist/engines/playwright/adapter/config.js +4 -4
  167. package/dist/engines/playwright/adapter/evidence-validation.d.ts +0 -3
  168. package/dist/engines/playwright/adapter/evidence-validation.js +1 -1
  169. package/dist/engines/playwright/adapter/evidence.js +1 -1
  170. package/dist/engines/playwright/runtime/config-file.js +1 -1
  171. package/dist/engines/playwright/runtime/config.js +1 -1
  172. package/dist/engines/playwright/runtime/runtime-input.d.ts +2 -2
  173. package/dist/engines/playwright/runtime/runtime-input.js +16 -12
  174. package/dist/environment/lifecycle-artifacts.js +1 -1
  175. package/dist/environment/lifecycle-controller.d.ts +1 -11
  176. package/dist/environment/lifecycle-controller.js +1 -1
  177. package/dist/environment/lifecycle.d.ts +14 -11
  178. package/dist/environment/lifecycle.js +1 -1
  179. package/dist/environment/planning-types.d.ts +0 -2
  180. package/dist/environment/ports.d.ts +10 -0
  181. package/dist/environment/ports.js +1 -0
  182. package/dist/environment/ready-environment.d.ts +2 -5
  183. package/dist/environment/ready-environment.js +1 -1
  184. package/dist/environment/run-environment.d.ts +1 -0
  185. package/dist/environment/run-environment.js +1 -1
  186. package/dist/environment/runtime-contexts.d.ts +11 -7
  187. package/dist/environment/runtime-contexts.js +1 -1
  188. package/dist/environment/runtime-manager.d.ts +1 -9
  189. package/dist/environment/runtime-manager.js +1 -1
  190. package/dist/environment/runtime-planning.js +1 -1
  191. package/dist/environment/runtime-preparation.js +1 -1
  192. package/dist/environment/runtime-service.d.ts +0 -6
  193. package/dist/environment/services.d.ts +11 -3
  194. package/dist/environment/services.js +3 -1
  195. package/dist/environment/state.d.ts +1 -4
  196. package/dist/environment/state.js +1 -1
  197. package/dist/environment/template.d.ts +6 -22
  198. package/dist/environment/template.js +1 -1
  199. package/dist/evidence/artifact-config.js +1 -1
  200. package/dist/evidence/artifact-validation.d.ts +1 -2
  201. package/dist/evidence/artifact-validation.js +1 -1
  202. package/dist/evidence/artifacts.js +2 -2
  203. package/dist/evidence/bundles.js +1 -1
  204. package/dist/evidence/environment-record.d.ts +22 -0
  205. package/dist/evidence/environment-record.js +1 -0
  206. package/dist/evidence/index.d.ts +1 -1
  207. package/dist/evidence/logs.js +2 -2
  208. package/dist/evidence/operation-timings.js +1 -1
  209. package/dist/evidence/read-fields.d.ts +0 -1
  210. package/dist/evidence/read-fields.js +1 -1
  211. package/dist/evidence/read-run.js +1 -1
  212. package/dist/evidence/reading.js +1 -1
  213. package/dist/evidence/reporting-types.d.ts +19 -0
  214. package/dist/evidence/reporting.d.ts +2 -2
  215. package/dist/evidence/reporting.js +1 -1
  216. package/dist/evidence/storage.d.ts +0 -2
  217. package/dist/evidence/storage.js +2 -2
  218. package/dist/evidence/timing.js +1 -1
  219. package/dist/kernel/error-serialization.d.ts +2 -0
  220. package/dist/kernel/error-serialization.js +1 -1
  221. package/dist/kernel/unknown-values.d.ts +5 -0
  222. package/dist/kernel/unknown-values.js +1 -1
  223. package/dist/process/environment.js +1 -1
  224. package/dist/process/execa-bundle.json +3 -3
  225. package/dist/process/files/json-file.d.ts +0 -1
  226. package/dist/process/files/json-file.js +1 -1
  227. package/dist/process/files/paths.d.ts +2 -0
  228. package/dist/process/files/paths.js +1 -1
  229. package/dist/process/host/metadata.js +1 -1
  230. package/dist/process/host/platform.js +1 -1
  231. package/dist/process/host-process.js +1 -1
  232. package/dist/process/lifecycle-fence.d.ts +1 -2
  233. package/dist/process/lifecycle-fence.js +1 -1
  234. package/dist/process/locks.js +2 -2
  235. package/dist/process/process-group.js +1 -1
  236. package/dist/process/process-identity.js +1 -1
  237. package/dist/process/process.js +30 -30
  238. package/dist/process/readiness.d.ts +7 -1
  239. package/dist/process/readiness.js +1 -1
  240. package/dist/process/recovery.js +1 -1
  241. package/dist/process/runs/manifest.js +1 -1
  242. package/dist/process/runs/ownership.js +1 -1
  243. package/dist/process/runs/product-state.js +1 -1
  244. package/dist/process/runs/stop.js +1 -1
  245. package/dist/process/stoppable-settlement.d.ts +26 -0
  246. package/dist/process/stoppable-settlement.js +1 -0
  247. package/dist/process/url-redaction.d.ts +1 -0
  248. package/dist/process/url-redaction.js +1 -0
  249. package/dist/process/windows-native.d.ts +6 -0
  250. package/dist/process/windows-native.js +1 -1
  251. package/dist/repo/ci-environment.js +1 -1
  252. package/dist/repo/fingerprint-policy.d.ts +0 -2
  253. package/dist/repo/fingerprint-policy.js +1 -1
  254. package/dist/repo/physical-tree.js +1 -1
  255. package/dist/repo/publish-directory.d.ts +20 -0
  256. package/dist/repo/publish-directory.js +3 -0
  257. package/dist/repo/state.js +4 -4
  258. package/dist/steps/build-config.d.ts +0 -1
  259. package/dist/steps/build-config.js +1 -1
  260. package/dist/steps/configured-steps.js +1 -1
  261. package/dist/steps/environment.d.ts +0 -2
  262. package/dist/steps/environment.js +1 -1
  263. package/dist/steps/index.d.ts +1 -1
  264. package/dist/steps/run.js +2 -2
  265. package/dist/toolchains/archive.js +1 -1
  266. package/dist/toolchains/index.js +3 -3
  267. package/dist/toolchains/node-provisioning.js +2 -2
  268. package/dist/toolchains/vitest/index.js +1 -1
  269. package/docs/bug-records.md +54 -52
  270. package/docs/engine-architecture.md +39 -11
  271. package/package.json +1 -1
  272. package/testkit-config-schema.md +43 -5
  273. package/dist/app/run/playwright-configuration.d.ts +0 -2
  274. package/dist/app/run/playwright-configuration.js +0 -1
  275. package/dist/environment/readiness.d.ts +0 -12
  276. package/dist/environment/readiness.js +0 -1
@@ -1 +1 @@
1
- import y from"fs";import x from"path";import{buildRuntimeIds as A}from"../domain/runtime-identity.js";import{formatError as R}from"../kernel/error-serialization.js";import{cleanupRuntimeInstanceContext as D,createRuntimeInstanceContext as K,ensureRuntimeInstanceReady as T}from"./runtime-contexts.js";import{timeOperation as $}from"../evidence/operation-timings.js";import{createReadyEnvironment as E}from"./ready-environment.js";export function createRuntimeManager({productDir:t,graphs:a,lifecycle:n,runtimeOptions:r={}}){const i=new Map(a.map(e=>[e.key,e])),c=new Map,s=new Map,u=W(a),f=new Map;let S=1;return{canAcquire(e){if(f.has(e.graphKey))return!1;const o=w(c,i,e);return j(s,v(e))?P(o,e,u)!==null:!1},async acquire(e,o={}){const l=w(c,i,e);if(n.isStopRequested())throw n.signal.reason||new Error("testkit run interrupted");const{leaseId:p,slot:g}=await G(r,o,"testkit.runtime.slot_claim",{summary:"claim runtime slot",scope:{graphKey:e.graphKey,taskId:e.id,serviceName:e.serviceName||null,targetName:e.targetName||null},metadata:{decision:"claim",lockCount:v(e).length.toString(),resourceCost:d(e).toString()}},()=>{const m=`lease-${e.id.toString()}-${S.toString()}`;if(S+=1,!B(s,v(e),m))throw new Error(`Task ${e.id.toString()} was claimed before its locks were available`);const h=q(l,e,u);if(!h)throw C(s,v(e),m),new Error(`Task ${e.id.toString()} was claimed before runtime capacity was available`);return{leaseId:m,slot:h}});try{const m=await F(g,t,e,n,r,o);n.signal.throwIfAborted();const h=x.join(m.runtimeDir,"leases",p);return y.mkdirSync(h,{recursive:!0}),{...E(m,{leaseId:p,leaseDir:h}),lockNames:v(e),resourceCost:d(e),slot:g,context:m}}catch(m){throw I(g,d(e)),C(s,v(e),p),U({leaseDir:x.join(g.context?.runtimeDir||t,"leases",p)}),g.context||N(g,u),await k(g,n,u),n.isStopRequested()||M(f,e.graphKey,R(m)),m}},async release(e,o={}){e?.slot&&(o.invalidate&&(e.slot.draining=!0),e.releaseState||(e.releaseState={locks:!1,runtimeSlot:!1,leaseDir:!1}),e.releaseState.locks||(C(s,e.lockNames,e.leaseId),e.releaseState.locks=!0),e.releaseState.runtimeSlot||(I(e.slot,e.resourceCost),e.releaseState.runtimeSlot=!0),e.releaseState.leaseDir||(U(e),e.releaseState.leaseDir=!0),e.released=!0,e.slot.draining&&e.slot.activeLeaseCount===0&&await k(e.slot,n,u))},async cleanupAll(){const e=[];for(const o of c.values())for(const l of o.slots){l.draining=!0;try{await k(l,n,u)}catch(p){e.push({slot:l,error:p})}}if(e.length>0)throw new AggregateError(e.map(o=>o.error),`Failed to clean up ${e.length.toString()} runtime ${e.length===1?"slot":"slots"}: ${e.map(o=>`${o.slot.runtimeId}: ${R(o.error)}`).join("; ")}`)},getStats(){return[...c.values()].map(e=>({graphKey:e.slots[0]?.graph.key||null,targetNames:[...e.slots.at(0)?.graph.targetNames??[]],maxConcurrentTasks:Number.isFinite(e.slots[0]?.graph.maxConcurrentTasks)?e.slots[0]?.graph.maxConcurrentTasks??null:null,runtimeCount:e.slots.length,runtimes:e.slots.map(o=>({runtimeId:o.runtimeId,peakLeaseCount:o.peakLeaseCount,peakResourceUnits:o.peakResourceUnits,resourceReservations:o.resourceReservations.map(l=>({...l}))})),resourceBudgets:[...u.values()].filter(o=>e.slots.some(l=>l.graph.resourceReservations.some(p=>p.key===o.key))).map(o=>({key:o.key,capacity:o.capacity,reserved:o.reserved,available:o.available,active:o.active,peakActive:o.peakActive}))})).sort((e,o)=>(e.graphKey||"").localeCompare(o.graphKey||""))},getGraphFailure(e){return f.get(e)??null},getGraph(e){return i.get(e)||null},markGraphFailed(e,o){M(f,e,o)}}}function w(t,a,n){const r=t.get(n.graphKey);if(r)return r;const i=a.get(n.graphKey);if(!i)throw new Error(`Unknown graph "${n.graphKey}"`);const s={slots:A(i.instanceCount).map(u=>({graph:i,runtimeId:u,context:null,activeLeaseCount:0,activeResourceUnits:0,resourceReservations:[],peakLeaseCount:0,peakResourceUnits:0,draining:!1}))};return t.set(n.graphKey,s),s}function q(t,a,n){const r=d(a),i=t.slots.filter(s=>!s.draining);if(i.length===0)return null;const c=[...i].filter(s=>L(s,r)&&b(s,n)).sort((s,u)=>s.activeResourceUnits-u.activeResourceUnits||s.activeLeaseCount-u.activeLeaseCount||s.runtimeId.localeCompare(u.runtimeId)).at(0);return c?(H(c,n),c.activeLeaseCount+=1,c.activeResourceUnits+=r,c.peakLeaseCount=Math.max(c.peakLeaseCount,c.activeLeaseCount),c.peakResourceUnits=Math.max(c.peakResourceUnits,c.activeResourceUnits),c):null}function I(t,a){t.activeLeaseCount=Math.max(0,t.activeLeaseCount-1),t.activeResourceUnits=Math.max(0,t.activeResourceUnits-a)}async function F(t,a,n,r,i,c={}){t.context||(t.context=K(t.runtimeId,t.graph,a),r.trackGraphContext(t.context));const s=i.operationTimingRegistry||null,u=s?.start({name:"testkit.runtime.ensure",kind:"runtime",summary:"ensure runtime instance ready",parentId:c.operationTimingParentId||null,scope:{graphKey:t.graph.key,runtimeId:t.runtimeId,serviceName:n.serviceName,targetName:n.targetName}})??null;try{await T(t.context,n.startApplicationServices,r,i),s?.finish(u,{status:"passed"})}catch(f){throw s?.finish(u,{status:"failed",error:R(f)}),f}return t.context}function G(t,a,n,r,i){return $(t.operationTimingRegistry||null,{name:n,kind:"runtime",summary:r.summary||null,parentId:a.operationTimingParentId||null,scope:r.scope||null,metadata:r.metadata||null},i)}async function k(t,a,n){!t.context||t.activeLeaseCount>0||(await D(t.context,a),N(t,n),t.context=null,t.draining=!1)}function B(t,a,n){const r=[...new Set(a)].sort();for(const i of r)if(t.has(i))return!1;for(const i of r)t.set(i,n);return!0}function j(t,a){return[...new Set(a)].every(n=>!t.has(n))}function P(t,a,n){const r=d(a);return t.slots.find(i=>!i.draining&&L(i,r)&&b(i,n))||null}function L(t,a){return t.activeResourceUnits+a<=t.graph.maxConcurrentTasks}function W(t){const a=new Map;for(const n of t)for(const r of n.resourceReservations){const i=a.get(r.key)||{key:r.key,capacity:r.capacity||0,reserved:r.reserved||0,available:r.available||Math.max(0,(r.capacity||0)-(r.reserved||0)),active:0,peakActive:0};i.capacity=Math.max(i.capacity,r.capacity||0),i.reserved=Math.max(i.reserved,r.reserved||0),i.available=Math.max(0,i.capacity-i.reserved),a.set(r.key,i)}return a}function b(t,a){return t.resourceReservations.length>0?!0:t.graph.resourceReservations.every(n=>{const r=a.get(n.key);return r?r.active+n.perRuntime<=r.available:!0})}function H(t,a){if(t.resourceReservations.length>0)return;const n=[];for(const r of t.graph.resourceReservations){const i=a.get(r.key);i&&(i.active+=r.perRuntime,i.peakActive=Math.max(i.peakActive,i.active),n.push({key:r.key,units:r.perRuntime,services:r.services}))}t.resourceReservations=n}function N(t,a){for(const n of t.resourceReservations){const r=a.get(n.key);r&&(r.active=Math.max(0,r.active-n.units))}t.resourceReservations=[]}function C(t,a,n){for(const r of[...new Set(a)].sort())t.get(r)===n&&t.delete(r)}function M(t,a,n){!a||t.has(a)||t.set(a,n)}function v(t){return t.locks||[]}function d(t){return t.resourceCost??1}function U(t){t?.leaseDir&&(t.engineWorkspaceDirectory&&y.rmSync(t.engineWorkspaceDirectory,{recursive:!0,force:!0}),y.rmSync(t.leaseDir,{recursive:!0,force:!0}))}
1
+ import{buildRuntimeIds as w}from"../domain/runtime-identity.js";import{formatError as d}from"../kernel/error-serialization.js";import{cleanupRuntimeInstanceContext as I,createRuntimeInstanceContext as M,ensureRuntimeInstanceReady as L}from"./runtime-contexts.js";import{timeOperation as U}from"../evidence/operation-timings.js";import{createReadyEnvironment as b}from"./ready-environment.js";export function createRuntimeManager({productDir:t,graphs:i,lifecycle:n,runtimeOptions:r={}}){const a=new Map(i.map(e=>[e.key,e])),u=new Map,o=T(i),c=new Map;return{canAcquire(e){if(c.has(e.graphKey))return!1;const s=f(u,a,e);return A(s,e,o)!==null},async acquire(e,s={}){const m=f(u,a,e);if(n.isStopRequested())throw n.signal.reason||new Error("testkit run interrupted");const g=e.leaseId,l=await N(r,s,"testkit.runtime.slot_claim",{summary:"claim runtime slot",scope:{graphKey:e.graphKey,taskId:e.id,serviceName:e.serviceName||null,targetName:e.targetName||null},metadata:{decision:"claim",resourceCost:v(e).toString()}},()=>{const p=S(m,e,o);if(!p)throw new Error(`Task ${e.id.toString()} was claimed before runtime capacity was available`);return p});try{const p=await K(l,t,e,n,r,s);return n.signal.throwIfAborted(),{...b(p,g),resourceCost:v(e),slot:l,context:p}}catch(p){throw R(l,v(e)),l.context||x(l,o),await h(l,n,o),n.isStopRequested()||k(c,e.graphKey,d(p)),p}},async release(e,s={}){e?.slot&&(s.invalidate&&(e.slot.draining=!0),e.released||(R(e.slot,e.resourceCost),e.released=!0),e.slot.draining&&e.slot.activeLeaseCount===0&&await h(e.slot,n,o))},async cleanupAll(){const e=[];for(const s of u.values())for(const m of s.slots){m.draining=!0;try{await h(m,n,o)}catch(g){e.push({slot:m,error:g})}}if(e.length>0)throw new AggregateError(e.map(s=>s.error),`Failed to clean up ${e.length.toString()} runtime ${e.length===1?"slot":"slots"}: ${e.map(s=>`${s.slot.runtimeId}: ${d(s.error)}`).join("; ")}`)},getStats(){return[...u.values()].map(e=>({graphKey:e.slots[0]?.graph.key||null,targetNames:[...e.slots.at(0)?.graph.targetNames??[]],maxConcurrentTasks:Number.isFinite(e.slots[0]?.graph.maxConcurrentTasks)?e.slots[0]?.graph.maxConcurrentTasks??null:null,runtimeCount:e.slots.length,runtimes:e.slots.map(s=>({runtimeId:s.runtimeId,peakLeaseCount:s.peakLeaseCount,peakResourceUnits:s.peakResourceUnits,resourceReservations:s.resourceReservations.map(m=>({...m}))})),resourceBudgets:[...o.values()].filter(s=>e.slots.some(m=>m.graph.resourceReservations.some(g=>g.key===s.key))).map(s=>({key:s.key,capacity:s.capacity,reserved:s.reserved,available:s.available,active:s.active,peakActive:s.peakActive}))})).sort((e,s)=>(e.graphKey||"").localeCompare(s.graphKey||""))},getGraphFailure(e){return c.get(e)??null},getGraph(e){return a.get(e)||null},markGraphFailed(e,s){k(c,e,s)}}}function f(t,i,n){const r=t.get(n.graphKey);if(r)return r;const a=i.get(n.graphKey);if(!a)throw new Error(`Unknown graph "${n.graphKey}"`);const o={slots:w(a.instanceCount).map(c=>({graph:a,runtimeId:c,context:null,activeLeaseCount:0,activeResourceUnits:0,resourceReservations:[],peakLeaseCount:0,peakResourceUnits:0,draining:!1}))};return t.set(n.graphKey,o),o}function S(t,i,n){const r=v(i),a=t.slots.filter(o=>!o.draining);if(a.length===0)return null;const u=[...a].filter(o=>y(o,r)&&C(o,n)).sort((o,c)=>o.activeResourceUnits-c.activeResourceUnits||o.activeLeaseCount-c.activeLeaseCount||o.runtimeId.localeCompare(c.runtimeId)).at(0);return u?($(u,n),u.activeLeaseCount+=1,u.activeResourceUnits+=r,u.peakLeaseCount=Math.max(u.peakLeaseCount,u.activeLeaseCount),u.peakResourceUnits=Math.max(u.peakResourceUnits,u.activeResourceUnits),u):null}function R(t,i){t.activeLeaseCount=Math.max(0,t.activeLeaseCount-1),t.activeResourceUnits=Math.max(0,t.activeResourceUnits-i)}async function K(t,i,n,r,a,u={}){t.context||(t.context=M(t.runtimeId,t.graph,i),r.trackGraphContext(t.context));const o=a.operationTimingRegistry||null,c=o?.start({name:"testkit.runtime.ensure",kind:"runtime",summary:"ensure runtime instance ready",parentId:u.operationTimingParentId||null,scope:{graphKey:t.graph.key,runtimeId:t.runtimeId,serviceName:n.serviceName,targetName:n.targetName}})??null;try{await L(t.context,r,a),o?.finish(c,{status:"passed"})}catch(e){throw o?.finish(c,{status:"failed",error:d(e)}),e}return t.context}function N(t,i,n,r,a){return U(t.operationTimingRegistry||null,{name:n,kind:"runtime",summary:r.summary||null,parentId:i.operationTimingParentId||null,scope:r.scope||null,metadata:r.metadata||null},a)}async function h(t,i,n){!t.context||t.activeLeaseCount>0||(await I(t.context,i),x(t,n),t.context=null,t.draining=!1)}function A(t,i,n){const r=v(i);return t.slots.find(a=>!a.draining&&y(a,r)&&C(a,n))||null}function y(t,i){return t.activeResourceUnits+i<=t.graph.maxConcurrentTasks}function T(t){const i=new Map;for(const n of t)for(const r of n.resourceReservations){const a=i.get(r.key)||{key:r.key,capacity:r.capacity||0,reserved:r.reserved||0,available:r.available||Math.max(0,(r.capacity||0)-(r.reserved||0)),active:0,peakActive:0};a.capacity=Math.max(a.capacity,r.capacity||0),a.reserved=Math.max(a.reserved,r.reserved||0),a.available=Math.max(0,a.capacity-a.reserved),i.set(r.key,a)}return i}function C(t,i){return t.resourceReservations.length>0?!0:t.graph.resourceReservations.every(n=>{const r=i.get(n.key);return r?r.active+n.perRuntime<=r.available:!0})}function $(t,i){if(t.resourceReservations.length>0)return;const n=[];for(const r of t.graph.resourceReservations){const a=i.get(r.key);a&&(a.active+=r.perRuntime,a.peakActive=Math.max(a.peakActive,a.active),n.push({key:r.key,units:r.perRuntime,services:r.services}))}t.resourceReservations=n}function x(t,i){for(const n of t.resourceReservations){const r=i.get(n.key);r&&(r.active=Math.max(0,r.active-n.units))}t.resourceReservations=[]}function k(t,i,n){!i||t.has(i)||t.set(i,n)}function v(t){return t.resourceCost??1}
@@ -1 +1 @@
1
- import{resolveRuntimeInstanceConfigs as p}from"./template.js";import{buildGraphResourceReservations as g}from"../database/runtime-reservations.js";import{resolveRuntimeConfigs as m}from"../config/loader/dependencies.js";export const runtimePlanning=Object.freeze({collectDesiredRuntimeGraphs,resolveRuntimeInstanceConfigs:p,buildRuntimeGraphs,resolveRuntimeConfigs:m,buildGraphDirName});export function buildRuntimeGraphs(r){const s=[],a=new Map;for(const e of r){const n=a.get(e.runtimeKey);if(n){n.targetNames.push(e.config.name),n.instanceCount=Math.max(n.instanceCount,e.config.testkit.runtime.instances),n.maxConcurrentTasks=Math.min(n.maxConcurrentTasks,u(e.runtimeConfigs));continue}const i={key:e.runtimeKey,runtimeNames:e.runtimeNames,runtimeConfigs:e.runtimeConfigs,targetNames:[e.config.name],dirName:buildGraphDirName(e.runtimeNames),instanceCount:e.config.testkit.runtime.instances,maxConcurrentTasks:u(e.runtimeConfigs),resourceReservations:g(e.runtimeConfigs)};s.push(i),a.set(e.runtimeKey,i)}const t=s.sort((e,n)=>e.dirName.localeCompare(n.dirName)),o=Math.max(1,...t.map(e=>e.instanceCount));return t.map((e,n)=>({...e,portNamespaceIndex:n,portNamespaceStride:o}))}export function buildGraphDirName(r){const s=r.map(f).join("__");return s.length>0?s:"graph"}function f(r){return r.toLowerCase().replace(/[^a-z0-9]+/g,"_").replace(/^_+|_+$/g,"")||"svc"}function u(r){return r.reduce((s,a)=>Math.min(s,a.testkit.runtime.maxConcurrentTasks),Number.POSITIVE_INFINITY)}export function collectDesiredRuntimeGraphs(r=[]){const s=new Map(r.map(t=>[t.name,t])),a=new Map;for(const t of r){const e=m(t,s).map(c=>c.name).sort(),n=e.join("|"),i=a.get(n);if(i){i.targetNames.push(t.name),i.targetNames.sort(),i.instanceCount=Math.max(i.instanceCount,t.testkit.runtime.instances);continue}a.set(n,{key:n,dirName:buildGraphDirName(e),runtimeNames:e,targetNames:[t.name],instanceCount:t.testkit.runtime.instances})}return[...a.values()].sort((t,o)=>t.dirName.localeCompare(o.dirName))}
1
+ import{resolveRuntimeInstanceConfigs as g}from"./template.js";import{buildGraphResourceReservations as p}from"../database/runtime-reservations.js";import{resolveRuntimeConfigs as m}from"../config/loader/dependencies.js";export const runtimePlanning=Object.freeze({collectDesiredRuntimeGraphs,resolveRuntimeInstanceConfigs:g,buildRuntimeGraphs,resolveRuntimeConfigs:m,buildGraphDirName});export function buildRuntimeGraphs(t){const r=[],s=new Map;for(const e of t){const n=s.get(e.runtimeKey);if(n){n.targetNames.push(e.config.name),n.instanceCount=Math.max(n.instanceCount,e.config.testkit.runtime.instances),n.maxConcurrentTasks=Math.min(n.maxConcurrentTasks,u(e.runtimeConfigs));continue}const i={key:e.runtimeKey,runtimeNames:e.runtimeNames,runtimeConfigs:e.runtimeConfigs,targetNames:[e.config.name],dirName:buildGraphDirName(e.runtimeNames),instanceCount:e.config.testkit.runtime.instances,maxConcurrentTasks:u(e.runtimeConfigs),resourceReservations:p(e.runtimeConfigs)};r.push(i),s.set(e.runtimeKey,i)}return r.sort((e,n)=>e.dirName.localeCompare(n.dirName))}export function buildGraphDirName(t){const r=t.map(f).join("__");return r.length>0?r:"graph"}function f(t){return t.toLowerCase().replace(/[^a-z0-9]+/g,"_").replace(/^_+|_+$/g,"")||"svc"}function u(t){return t.reduce((r,s)=>Math.min(r,s.testkit.runtime.maxConcurrentTasks),Number.POSITIVE_INFINITY)}export function collectDesiredRuntimeGraphs(t=[]){const r=new Map(t.map(e=>[e.name,e])),s=new Map;for(const e of t){const i=m(e,r).map(c=>c.name).sort(),o=i.join("|"),a=s.get(o);if(a){a.targetNames.push(e.name),a.targetNames.sort(),a.instanceCount=Math.max(a.instanceCount,e.testkit.runtime.instances);continue}s.set(o,{key:o,dirName:buildGraphDirName(i),runtimeNames:i,targetNames:[e.name],instanceCount:e.testkit.runtime.instances})}return[...s.values()].sort((e,n)=>e.dirName.localeCompare(n.dirName))}
@@ -1 +1 @@
1
- import S from"crypto";import p from"fs";import k from"path";import{resolveServiceCwd as D}from"../config/loader/index.js";import{appendFileToHash as y}from"../repo/fingerprint-policy.js";import{announceResolvedToolchain as b,resolveConfiguredToolchain as v}from"../toolchains/index.js";import{readDatabaseUrl as m}from"../database/state.js";import{buildExecutionEnv as w,collectConfiguredInputs as x}from"../steps/index.js";import{runConfiguredSteps as R}from"../database/configured-steps.js";import"../evidence/setup-progress.js";const I="prepare-manifest.json";export async function prepareRuntimeServices(e,r={}){for(const t of e)await prepareRuntimeService(t,r)}export async function prepareRuntimeService(e,r={}){const t=e.testkit.runtime.prepare;if(!t||t.steps.length===0||!N(e))return;const n=e.testkit.prepareDir,s=await computeRuntimePrepareFingerprint(e,{signal:r.signal}),i=k.join(n,I);if(g(i)?.fingerprint===s){r.setupRegistry?.recordCached({config:e,stage:"runtime:prepare",kind:"runtime-prepare",summary:"runtime prepare cache hit"});return}p.rmSync(n,{recursive:!0,force:!0}),p.mkdirSync(n,{recursive:!0});const o={...w(e,{...e.testkit.local?.env||{},...r.extraEnv||{}},process.env)},d=m(e.stateDir);d&&(o.DATABASE_URL=d);const u=r.setupRegistry?.start({config:e,stage:"runtime:prepare",kind:"runtime-prepare",summary:"runtime prepare",recordLog:!1}),c=r.setupRegistry;try{await b(e,await v(e,{signal:r.signal}),r.reporter),await R({config:e,steps:t.steps,env:o,labelPrefix:"runtime:prepare",reporter:r.reporter,setupRegistry:c||null,parentOperation:u,signal:r.signal});const a=u?c?.finish(u,{status:"passed",summary:"runtime prepare"}):null;a&&r.reporter?.setupOperationFinished?.(a),A(i,{fingerprint:s,preparedAt:new Date().toISOString(),runtimeId:e.runtimeId||null,serviceName:e.name})}catch(a){const f=u?c?.finish(u,{status:"failed",summary:"runtime prepare",error:U(a,"message")||a}):null;throw f&&r.reporter?.setupOperationFinished?.(f),p.rmSync(n,{recursive:!0,force:!0}),a}}function N(e){return e.testkit.runtime.prepare!=null}export async function computeRuntimePrepareFingerprint(e,r={}){const t=await v(e,{signal:r.signal}),n=S.createHash("sha256");n.update(JSON.stringify({prepare:e.testkit.runtime.prepare,toolchain:t?{kind:"node",install:t.install,nodeVersion:t.nodeVersion,npmVersion:t.npmVersion,nodeSource:t.nodeSource,npmSource:t.npmSource,fingerprint:t.fingerprint}:null,serviceEnv:e.testkit.serviceEnv||{},local:e.testkit.local?{baseUrl:e.testkit.local.baseUrl,cwd:e.testkit.local.cwd,env:e.testkit.local.env||{},readyUrl:e.testkit.local.readyUrl,start:e.testkit.local.start}:null})),n.update(JSON.stringify(O(e)));const s=e.testkit.fingerprints||{exclude:[],include:[]};for(const i of e.testkit.envFiles||[])y(n,e.productDir,D(e.productDir,i),s);for(const i of x(e.productDir,e.testkit.runtime.prepare))y(n,e.productDir,i,s);return n.digest("hex")}function O(e){const r=[],t=m(e.stateDir);t&&r.push({service:e.name,url:t});const n=new Set;for(const i of Object.values(e.testkit.serviceEnv||{}))h(i,n,e.name);for(const i of Object.values(e.testkit.local?.env||{}))h(i,n,e.name);const s=e.testkit.templateContext?.stateDirByService;for(const i of[...n].sort()){const l=s?.get(i),o=l?m(l):null;r.push({service:i,url:o||null})}return r}function h(e,r,t){if(typeof e!="string")return;const n=/\{db(?:Url|Host|Port|Name|User|Password|Ssl)(?::([a-zA-Z0-9_-]+))?\}/g;let s=n.exec(e);for(;s;)r.add(s[1]||t),s=n.exec(e)}function g(e){if(!p.existsSync(e))return null;try{const r=JSON.parse(p.readFileSync(e,"utf8"));return F(r)?{fingerprint:r.fingerprint,...typeof r.preparedAt=="string"?{preparedAt:r.preparedAt}:{},...typeof r.runtimeId=="string"||typeof r.runtimeId=="number"||r.runtimeId===null?{runtimeId:r.runtimeId}:{},...typeof r.serviceName=="string"?{serviceName:r.serviceName}:{}}:null}catch{return null}}function A(e,r){p.writeFileSync(e,JSON.stringify(r,null,2))}function F(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function U(e,r){if(!((typeof e!="object"||e===null)&&typeof e!="function"))return Reflect.get(e,r)}
1
+ import S from"crypto";import p from"fs";import k from"path";import{resolveServiceCwd as w}from"../config/loader/index.js";import{appendFileToHash as v}from"../repo/fingerprint-policy.js";import{announceResolvedToolchain as D,resolveConfiguredToolchain as y}from"../toolchains/index.js";import{readDatabaseUrl as m}from"../database/state.js";import{buildExecutionEnv as x,buildTemplateExecutionEnv as b,collectConfiguredInputs as R}from"../steps/index.js";import{runConfiguredSteps as g}from"../database/configured-steps.js";import"../evidence/setup-progress.js";import{isUnknownRecord as I,readDynamicProperty as N}from"../kernel/unknown-values.js";const F="prepare-manifest.json";export async function prepareRuntimeServices(e,r={}){for(const t of e)await prepareRuntimeService(t,r)}export async function prepareRuntimeService(e,r={}){const t=e.testkit.runtime.prepare;if(!t||t.steps.length===0||!O(e))return;const n=e.testkit.prepareDir,s=await computeRuntimePrepareFingerprint(e,{signal:r.signal}),i=k.join(n,F);if(A(i)?.fingerprint===s){r.setupRegistry?.recordCached({config:e,stage:"runtime:prepare",kind:"runtime-prepare",summary:"runtime prepare cache hit"});return}p.rmSync(n,{recursive:!0,force:!0}),p.mkdirSync(n,{recursive:!0});const o={...x(e,{...e.testkit.local?.env||{},...r.extraEnv||{}},process.env)},d=m(e.stateDir);d&&(o.DATABASE_URL=d);const u=r.setupRegistry?.start({config:e,stage:"runtime:prepare",kind:"runtime-prepare",summary:"runtime prepare",recordLog:!1}),c=r.setupRegistry;try{await D(e,await y(e,{signal:r.signal}),r.reporter),await g({config:e,steps:t.steps,env:o,labelPrefix:"runtime:prepare",reporter:r.reporter,setupRegistry:c||null,parentOperation:u,signal:r.signal});const a=u?c?.finish(u,{status:"passed",summary:"runtime prepare"}):null;a&&r.reporter?.setupOperationFinished?.(a),P(i,{fingerprint:s,preparedAt:new Date().toISOString(),runtimeId:e.runtimeId||null,serviceName:e.name})}catch(a){const f=u?c?.finish(u,{status:"failed",summary:"runtime prepare",error:N(a,"message")||a}):null;throw f&&r.reporter?.setupOperationFinished?.(f),p.rmSync(n,{recursive:!0,force:!0}),a}}function O(e){return e.testkit.runtime.prepare!=null}export async function computeRuntimePrepareFingerprint(e,r={}){const t=await y(e,{signal:r.signal}),n=S.createHash("sha256");n.update(JSON.stringify({prepare:e.testkit.runtime.prepare,toolchain:t?{kind:"node",install:t.install,nodeVersion:t.nodeVersion,npmVersion:t.npmVersion,nodeSource:t.nodeSource,npmSource:t.npmSource,fingerprint:t.fingerprint}:null,env:b(e,{},{},{includeTestkitControls:!1}),envTemplates:{service:e.testkit.serviceEnv||{},local:e.testkit.local?.env||{}},cwd:e.testkit.local?e.testkit.local.cwd:null})),n.update(JSON.stringify(E(e)));const s=e.testkit.fingerprints||{exclude:[],include:[]};for(const i of e.testkit.envFiles||[])v(n,e.productDir,w(e.productDir,i),s);for(const i of R(e.productDir,e.testkit.runtime.prepare))v(n,e.productDir,i,s);return n.digest("hex")}function E(e){const r=[],t=m(e.stateDir);t&&r.push({service:e.name,url:t});const n=new Set;for(const i of Object.values(e.testkit.serviceEnv||{}))h(i,n,e.name);for(const i of Object.values(e.testkit.local?.env||{}))h(i,n,e.name);const s=e.testkit.templateContext?.stateDirByService;for(const i of[...n].sort()){const l=s?.get(i),o=l?m(l):null;r.push({service:i,url:o||null})}return r}function h(e,r,t){if(typeof e!="string")return;const n=/\{db(?:Url|Host|Port|Name|User|Password|Ssl)(?::([a-zA-Z0-9_-]+))?\}/g;let s=n.exec(e);for(;s;)r.add(s[1]||t),s=n.exec(e)}function A(e){if(!p.existsSync(e))return null;try{const r=JSON.parse(p.readFileSync(e,"utf8"));return I(r)?{fingerprint:r.fingerprint,...typeof r.preparedAt=="string"?{preparedAt:r.preparedAt}:{},...typeof r.runtimeId=="string"||typeof r.runtimeId=="number"||r.runtimeId===null?{runtimeId:r.runtimeId}:{},...typeof r.serviceName=="string"?{serviceName:r.serviceName}:{}}:null}catch{return null}}function P(e,r){p.writeFileSync(e,JSON.stringify(r,null,2))}
@@ -1,5 +1,3 @@
1
- import type { EngineJsonObject } from "../engines/api/index.js";
2
- import type { EngineId } from "../domain/identifiers.js";
3
1
  import type { ExecutionEnvironmentConfig } from "../steps/index.js";
4
2
  export interface ReadyServiceLocalConfig {
5
3
  baseUrl?: null | string;
@@ -7,12 +5,8 @@ export interface ReadyServiceLocalConfig {
7
5
  env?: Readonly<Record<string, string>>;
8
6
  }
9
7
  export interface ReadyServiceConfig extends ExecutionEnvironmentConfig {
10
- engineConfigurations?: Readonly<Partial<Record<EngineId, EngineJsonObject>>>;
11
8
  stateDir: string;
12
9
  testkit: ExecutionEnvironmentConfig["testkit"] & {
13
- execution: {
14
- fileTimeoutSeconds: number;
15
- };
16
10
  local?: null | ReadyServiceLocalConfig;
17
11
  };
18
12
  }
@@ -1,8 +1,9 @@
1
1
  import { type SupervisedProcessHandle, type SupervisedProcessReady } from "../process/index.js";
2
2
  import { type ToolchainResolvedReporter } from "../toolchains/index.js";
3
- import { type RuntimeServiceConfig } from "./template.js";
3
+ import type { RuntimeServiceConfig } from "./template.js";
4
4
  import { type OutputLineHandler } from "../process/index.js";
5
5
  import type { RunLogRegistry } from "../evidence/logs.js";
6
+ import type { RunEnvironmentRecord } from "../evidence/environment-record.js";
6
7
  type LocalServiceConfig = RuntimeServiceConfig & {
7
8
  runtimeLabel: string;
8
9
  stateDir: string;
@@ -29,7 +30,14 @@ export interface LocalServiceReporter extends ToolchainResolvedReporter<LocalSer
29
30
  outputMode?: string;
30
31
  writeDebugLine?: OutputLineHandler;
31
32
  }
33
+ export interface LocalServiceRuntime {
34
+ readonly configs: readonly ResolvedServiceConfig[];
35
+ prepare(serviceNames: ReadonlySet<string>): Promise<void>;
36
+ reassignPort(serviceName: string, startedServiceNames: ReadonlySet<string>): Promise<readonly string[]>;
37
+ releasePort(serviceName: string): Promise<void>;
38
+ }
32
39
  export interface StartLocalServicesOptions {
40
+ environmentRecord?: RunEnvironmentRecord | null;
33
41
  extraEnv?: NodeJS.ProcessEnv | null;
34
42
  logRegistry?: RunLogRegistry | null;
35
43
  reporter?: LocalServiceReporter | null;
@@ -42,7 +50,7 @@ export interface StartedLocalService {
42
50
  ownership: SupervisedProcessReady;
43
51
  supervisor: SupervisedProcessHandle;
44
52
  }
45
- export declare function startLocalServices(runtimeConfigs: readonly ResolvedServiceConfig[], lifecycle: LocalServiceLifecycle, options?: Readonly<StartLocalServicesOptions>): Promise<StartedLocalService[]>;
46
- export declare function startLocalService(config: LocalServiceConfig, lifecycle: LocalServiceLifecycle, options?: Readonly<StartLocalServicesOptions>): Promise<StartedLocalService>;
53
+ export declare function startLocalServices(runtime: LocalServiceRuntime, lifecycle: LocalServiceLifecycle, options?: Readonly<StartLocalServicesOptions>): Promise<StartedLocalService[]>;
54
+ export declare function startLocalService(config: LocalServiceConfig, lifecycle: LocalServiceLifecycle, options?: Readonly<StartLocalServicesOptions>, beforeSpawn?: () => Promise<void>): Promise<StartedLocalService>;
47
55
  export declare function stopLocalServices(started: readonly StartedLocalService[], lifecycle: LocalServiceLifecycle | null | undefined): Promise<void>;
48
56
  export {};
@@ -1 +1,3 @@
1
- import{DEFAULT_READY_TIMEOUT_MS as x,waitForReady as M,serviceCompletionError as y}from"../process/readiness.js";import{resolveServiceCwd as A}from"../config/loader/index.js";import{startSupervisedProcess as f,ProcessExitedBeforeReadyError as k,DEFAULT_SHUTDOWN_TIMEOUT_MS as R}from"../process/index.js";import{announceResolvedToolchain as U,applyToolchainEnv as g,resolveConfiguredToolchain as D}from"../toolchains/index.js";import{buildExecutionEnv as O}from"../steps/index.js";import{numericPortFromUrl as P}from"./template.js";import{assertLocalServicePortsAvailable as _}from"./readiness.js";import{captureOutput as T}from"../process/index.js";import{prepareServiceStartInvocation as C}from"../process/service-processes.js";import{readDatabaseUrl as F}from"../database/state.js";export async function startLocalServices(e,r,t={}){const s=[];try{for(const a of e){if(!B(a))continue;const n=await startLocalService(a,r,t);s.push(n)}}catch(a){try{await stopLocalServices(s,r)}catch(n){throw new AggregateError([a,n],"Local service startup failed and its supervised cleanup also failed",{cause:n})}throw a}return s}export async function startLocalService(e,r,t={}){const s=A(e.productDir,e.testkit.local.cwd),a=await D(e,{signal:r.signal});await U(e,a,t.reporter);const n=g(O(e,{...e.testkit.local.env||{},...t.extraEnv||{}},process.env),a),l=e.testkit.local.port||P(e.testkit.local.baseUrl);l&&(n.PORT=String(l));const b=F(e.stateDir);b&&(n.DATABASE_URL=b),await _(e),t.reporter?.localServiceStarting?.(e,e.testkit.local.start);const v=C(e.testkit.local.start,n),d=t.logRegistry?.ensureServiceLogRecord(e),L=t.reporter?.outputMode==="debug"?o=>t.reporter?.writeDebugLine?.(o):null,i=f({args:v.args,windowsVerbatimArguments:v.windowsVerbatimArguments,cwd:s,displayLabel:`Testkit local service ${e.runtimeLabel}:${e.name}`,env:I(n),executable:v.executable,outputMode:"caller-drained",signal:r.signal,shutdownTimeoutMs:e.testkit.local.shutdownTimeoutMs??R,timeoutMs:null}),S=[T(i.stdout,{livePrefix:`[${e.runtimeLabel}:${e.name}]`,liveWriter:L,onLine(o){d&&t.logRegistry?.append(d,"stdout",o)}}),T(i.stderr,{livePrefix:`[${e.runtimeLabel}:${e.name}]`,liveWriter:L,onLine(o){d&&t.logRegistry?.append(d,"stderr",o)}})];let h,p=!1,w=!1;try{h=await i.ready,p=!0,r.registerService(e,i,h,s);const o=e.testkit.local.readyTimeoutMs||x;e.testkit.local.readyUrl!==!1&&e.testkit.local.readyUrl!=null&&await M({name:`${e.runtimeLabel}:${e.name}`,url:e.testkit.local.readyUrl,timeoutMs:o,process:i,signal:r.signal})}catch(o){const u=o instanceof k?y(`${e.runtimeLabel}:${e.name}`,o):o;w=!0;const c=[];try{c.push(...await $(i,S,!1))}catch(E){c.push(m(E))}if(p)try{r.unregisterService(i.pid)}catch(E){c.push(m(E))}throw c.length>0?new AggregateError([u,...c],`Local service ${e.runtimeLabel}:${e.name} startup and cleanup failed`,{cause:o}):u}return{completionMonitor:i.completion.then(o=>{if(w)return null;if(r.signal.aborted){if(p){p=!1;try{r.unregisterService(i.pid)}catch{}}return null}let u=y(`${e.runtimeLabel}:${e.name}`,o,"unexpectedly");if(p){p=!1;try{r.unregisterService(i.pid)}catch(c){u=new AggregateError([u,c],`Local service ${e.runtimeLabel}:${e.name} exited and its manifest entry could not be removed`)}}try{r.requestStop(u.message)}catch(c){u=new AggregateError([u,c],`Local service ${e.runtimeLabel}:${e.name} exited and the active run could not be interrupted`)}return u}),expectCompletion(){w=!0},name:e.name,outputDrains:S,ownership:h,supervisor:i}}export async function stopLocalServices(e,r){const t=[];for(const s of[...e].reverse()){s.expectCompletion();try{t.push(...await $(s.supervisor,s.outputDrains)),await s.completionMonitor}catch(a){t.push(m(a))}finally{try{r?.unregisterService(s.ownership.pid)}catch(a){t.push(m(a))}}}if(t.length>0)throw new AggregateError(t,"Could not stop all local services")}async function $(e,r,t=!0){const a=await e.stop().then(l=>({error:null,result:l}),l=>({error:m(l),result:null})),n=await Promise.allSettled(r);return[...a.result?j(a.result,t):[a.error],...n.flatMap(l=>l.status==="rejected"?[m(l.reason)]:[])]}function j(e,r=!0){return[...r?[e.error]:[],e.terminationError].filter(t=>t!==null)}function I(e){return Object.fromEntries(Object.entries(e).filter(r=>r[1]!==void 0))}function m(e){return e instanceof Error?e:new Error(String(e))}function B(e){return e.testkit.local!=null}
1
+ import{DEFAULT_READY_TIMEOUT_MS as P,ServiceExitedBeforeReadyError as D,ServiceReadinessTimeoutError as _,waitForReady as C,serviceCompletionError as x}from"../process/readiness.js";import{resolveServiceCwd as I}from"../config/loader/index.js";import{startSupervisedProcess as j,ProcessExitedBeforeReadyError as B,DEFAULT_SHUTDOWN_TIMEOUT_MS as F}from"../process/index.js";import{announceResolvedToolchain as N,applyToolchainEnv as V,resolveConfiguredToolchain as q}from"../toolchains/index.js";import{buildExecutionEnv as H}from"../steps/index.js";import{SERVICE_START_ATTEMPTS as W}from"./ports.js";import{captureOutput as R}from"../process/index.js";import{prepareServiceStartInvocation as X}from"../process/service-processes.js";import{readDatabaseUrl as Y}from"../database/state.js";import{toError as S}from"../kernel/error-serialization.js";const z=20;class y extends Error{outcome;output;constructor(t,r,n){super(n.message,{cause:n}),this.outcome=t,this.output=r,this.name="LocalServiceExitedBeforeReadyError"}}export async function startLocalServices(e,t,r={}){const n=e.configs.filter(O).map(a=>a.name),o=[],c=new Map,u=new Map,f=new Map,b=(a,s)=>{const m=U(a);c.set(a.name,[...c.get(a.name)??[],{port:m,outcome:s}]);let d=f.get(a.name);!d&&r.environmentRecord&&(d=r.environmentRecord.service({runtimeLabel:a.runtimeLabel,serviceName:a.name}),f.set(a.name,d)),d?.recordAttempt({baseUrl:a.testkit.local.baseUrl,outcome:s,port:m,readyUrl:typeof a.testkit.local.readyUrl=="string"?a.testkit.local.readyUrl:null})},g=(a,s,m)=>{const d=c.get(a.name)?.at(-1);if(!m||!d){b(a,s);return}d.outcome=s,f.get(a.name)?.amendLastOutcome(s)};try{for(;o.length<n.length;){const a=n[o.length],s=Q(e.configs,a);let m=!1;const d=async()=>{await e.releasePort(a),b(s,"starting"),m=!0};try{o.push(await startLocalService(s,t,r,d)),g(s,"ready",m)}catch(l){if(g(s,J(l,t),m),t.signal.aborted)throw l;if(!(l instanceof y))throw M(s,c.get(a)??[],l,2);const L=[...u.get(a)??[],l];if(u.set(a,L),L.length>=W)throw K(s,c.get(a)??[],L);try{const E=await e.reassignPort(a,new Set(o.map(h=>h.name))),v=o.findIndex(h=>E.includes(h.name));if(v>=0){const h=o.splice(v);for(const T of h)f.get(T.name)?.amendLastOutcome(`ready, then restarted after ${a} moved to a fresh port`);await stopLocalServices(h,t)}await e.prepare(new Set(n.slice(o.length)))}catch(E){throw G(t)?E:M(s,c.get(a)??[],E,1)}}}}catch(a){try{await stopLocalServices(o,t)}catch(s){throw new AggregateError([a,s],"Local service startup failed and its supervised cleanup also failed",{cause:s})}throw a}return o}function G(e){return e.signal.aborted}function J(e,t){return t.signal.aborted?"cancelled":e instanceof y?e.outcome:e instanceof _?"readiness timed out":"failed to start"}function K(e,t,r){const n=r.at(-1),o=[`Service "${e.runtimeLabel}:${e.name}" exited before becoming ready on all ${r.length.toString()} attempts, each on a fresh OS-assigned port:`,...A(t),...n&&n.output.length>0?[`Last output from attempt ${t.length.toString()}:`,...n.output.map(c=>` ${c}`)]:[]];return new Error(o.join(`
2
+ `),{cause:n})}function M(e,t,r,n){if(t.length<n)return r;const o=S(r);return new Error([`Service "${e.runtimeLabel}:${e.name}" did not start after ${t.length.toString()} attempts, each on a fresh OS-assigned port:`,...A(t),o.message].join(`
3
+ `),{cause:o})}function A(e){return e.map((t,r)=>` attempt ${(r+1).toString()} on port ${t.port.toString()}: ${t.outcome}`)}function Q(e,t){const r=e.find(n=>n.name===t);if(!r||!O(r))throw new Error(`Runtime has no local service "${t}"`);return r}function U(e){const t=e.testkit.local.assignedPort;if(t===void 0)throw new Error(`Testkit assigned no port to service "${e.runtimeLabel}:${e.name}"`);return t}export async function startLocalService(e,t,r={},n=()=>Promise.resolve()){const o=I(e.productDir,e.testkit.local.cwd),c=await q(e,{signal:t.signal});await N(e,c,r.reporter);const u=V(H(e,{...e.testkit.local.env||{},...r.extraEnv||{}},process.env),c),f=U(e);u.PORT=String(f);const b=Y(e.stateDir);b&&(u.DATABASE_URL=b),r.reporter?.localServiceStarting?.(e,e.testkit.local.start);const g=X(e.testkit.local.start,u),a=r.logRegistry?.ensureServiceLogRecord(e),s=r.reporter?.outputMode==="debug"?i=>r.reporter?.writeDebugLine?.(i):null,m=[],d=i=>{m.push(i),m.length>z&&m.shift()};await n();const l=j({args:g.args,windowsVerbatimArguments:g.windowsVerbatimArguments,cwd:o,displayLabel:`Testkit local service ${e.runtimeLabel}:${e.name}`,env:ee(u),executable:g.executable,outputMode:"caller-drained",signal:t.signal,shutdownTimeoutMs:e.testkit.local.shutdownTimeoutMs??F,timeoutMs:null}),L=[R(l.stdout,{livePrefix:`[${e.runtimeLabel}:${e.name}]`,liveWriter:s,onLine(i){d(i),a&&r.logRegistry?.append(a,"stdout",i)}}),R(l.stderr,{livePrefix:`[${e.runtimeLabel}:${e.name}]`,liveWriter:s,onLine(i){d(i),a&&r.logRegistry?.append(a,"stderr",i)}})];let E,v=!1,h=!1;try{E=await l.ready,v=!0,t.registerService(e,l,E,o);const i=e.testkit.local.readyTimeoutMs||P;e.testkit.local.readyUrl!==!1&&e.testkit.local.readyUrl!=null&&await C({name:`${e.runtimeLabel}:${e.name}`,url:e.testkit.local.readyUrl,timeoutMs:i,process:l,signal:t.signal})}catch(i){const p=i instanceof B?x(`${e.runtimeLabel}:${e.name}`,i):i;h=!0;const w=[];try{w.push(...await k(l,L,!1))}catch($){w.push(S($))}if(v)try{t.unregisterService(l.pid)}catch($){w.push(S($))}throw w.length>0?new AggregateError([p,...w],`Local service ${e.runtimeLabel}:${e.name} startup and cleanup failed`,{cause:i}):p instanceof D?new y(p.outcome,m,p):p}return{completionMonitor:l.completion.then(i=>{if(h)return null;if(t.signal.aborted){if(v){v=!1;try{t.unregisterService(l.pid)}catch{}}return null}let p=x(`${e.runtimeLabel}:${e.name}`,i,"unexpectedly");if(v){v=!1;try{t.unregisterService(l.pid)}catch(w){p=new AggregateError([p,w],`Local service ${e.runtimeLabel}:${e.name} exited and its manifest entry could not be removed`)}}try{t.requestStop(p.message)}catch(w){p=new AggregateError([p,w],`Local service ${e.runtimeLabel}:${e.name} exited and the active run could not be interrupted`)}return p}),expectCompletion(){h=!0},name:e.name,outputDrains:L,ownership:E,supervisor:l}}export async function stopLocalServices(e,t){const r=[];for(const n of[...e].reverse()){n.expectCompletion();try{r.push(...await k(n.supervisor,n.outputDrains)),await n.completionMonitor}catch(o){r.push(S(o))}finally{try{t?.unregisterService(n.ownership.pid)}catch(o){r.push(S(o))}}}if(r.length>0)throw new AggregateError(r,"Could not stop all local services")}async function k(e,t,r=!0){const o=await e.stop().then(u=>({error:null,result:u}),u=>({error:S(u),result:null})),c=await Promise.allSettled(t);return[...o.result?Z(o.result,r):[o.error],...c.flatMap(u=>u.status==="rejected"?[S(u.reason)]:[])]}function Z(e,t=!0){return[...t?[e.error]:[],e.terminationError].filter(r=>r!==null)}function ee(e){return Object.fromEntries(Object.entries(e).filter(t=>t[1]!==void 0))}function O(e){return e.testkit.local!=null}
@@ -1,12 +1,10 @@
1
1
  import type { RuntimeGraph } from "./planning-types.js";
2
- export type GraphMetadataInput = Pick<RuntimeGraph, "dirName" | "instanceCount" | "key" | "maxConcurrentTasks" | "portNamespaceIndex" | "portNamespaceStride" | "runtimeNames" | "targetNames">;
2
+ export type GraphMetadataInput = Pick<RuntimeGraph, "dirName" | "instanceCount" | "key" | "maxConcurrentTasks" | "runtimeNames" | "targetNames">;
3
3
  export interface GraphMetadata extends Record<string, unknown> {
4
4
  dirName?: null | string;
5
5
  instanceCount?: null | number;
6
6
  key?: null | string;
7
7
  maxConcurrentTasks?: null | number;
8
- portNamespaceIndex?: null | number;
9
- portNamespaceStride?: null | number;
10
8
  runtimeNames?: string[];
11
9
  runtimeServices?: string[];
12
10
  targetServices?: string[];
@@ -15,4 +13,3 @@ export declare function findRuntimeStateDirs(rootDir: string, isDatabaseStateDir
15
13
  export declare function findGraphDirsForService(productDir: string, serviceName: string): string[];
16
14
  export declare function writeGraphMetadata(graphDir: string, graph: Readonly<GraphMetadataInput>): void;
17
15
  export declare function readGraphMetadata(graphDir: string): GraphMetadata | null;
18
- export declare function normalizePathSeparators(filePath: string): string;
@@ -1 +1 @@
1
- import c from"fs";import u from"path";const y="graph.json";export function findRuntimeStateDirs(e,t){const n=[],a=r=>{if(!c.existsSync(r))return;const s=c.readdirSync(r,{withFileTypes:!0});t(r)&&n.push(r);for(const o of s)o.isDirectory()&&a(u.join(r,o.name))};return a(e),n.sort((r,s)=>s.length-r.length)}export function findGraphDirsForService(e,t){const n=u.join(e,".testkit","_graphs");if(!c.existsSync(n))return[];const a=[];for(const r of c.readdirSync(n,{withFileTypes:!0})){if(!r.isDirectory())continue;const s=u.join(n,r.name),o=readGraphMetadata(s);o&&(o.runtimeServices??[]).includes(t)&&a.push(s)}return a.sort()}export function writeGraphMetadata(e,t){c.mkdirSync(e,{recursive:!0});const n={key:t.key||null,dirName:t.dirName||null,instanceCount:Number.isFinite(t.instanceCount)?t.instanceCount:null,maxConcurrentTasks:Number.isFinite(t.maxConcurrentTasks)?t.maxConcurrentTasks:null,portNamespaceIndex:Number.isFinite(t.portNamespaceIndex)?t.portNamespaceIndex:null,portNamespaceStride:Number.isFinite(t.portNamespaceStride)?t.portNamespaceStride:null,runtimeNames:[...t.runtimeNames].sort(),runtimeServices:t.runtimeNames,targetServices:[...t.targetNames].sort()};c.writeFileSync(u.join(e,y),JSON.stringify(n,null,2))}export function readGraphMetadata(e){const t=u.join(e,y);if(!c.existsSync(t))return null;try{const n=JSON.parse(c.readFileSync(t,"utf8"));return x(n)}catch{return null}}export function normalizePathSeparators(e){return e.split(u.sep).join("/")}function x(e){if(!A(e))return null;const{dirName:t,instanceCount:n,key:a,maxConcurrentTasks:r,portNamespaceIndex:s,portNamespaceStride:o,runtimeNames:l,runtimeServices:p,targetServices:d,...S}=e,i={...S};return N(t)&&(i.dirName=t),m(n)&&(i.instanceCount=n),N(a)&&(i.key=a),m(r)&&(i.maxConcurrentTasks=r),m(s)&&(i.portNamespaceIndex=s),m(o)&&(i.portNamespaceStride=o),Array.isArray(l)&&(i.runtimeNames=l.filter(f)),Array.isArray(p)&&(i.runtimeServices=p.filter(f)),Array.isArray(d)&&(i.targetServices=d.filter(f)),i}function A(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function N(e){return e===null||typeof e=="string"}function m(e){return e===null||typeof e=="number"&&Number.isFinite(e)}function f(e){return typeof e=="string"}
1
+ import u from"fs";import c from"path";import{isNullableString as l,isUnknownRecord as N}from"../kernel/unknown-values.js";const y="graph.json";export function findRuntimeStateDirs(e,t){const n=[],s=r=>{if(!u.existsSync(r))return;const i=u.readdirSync(r,{withFileTypes:!0});t(r)&&n.push(r);for(const a of i)a.isDirectory()&&s(c.join(r,a.name))};return s(e),n.sort((r,i)=>i.length-r.length)}export function findGraphDirsForService(e,t){const n=c.join(e,".testkit","_graphs");if(!u.existsSync(n))return[];const s=[];for(const r of u.readdirSync(n,{withFileTypes:!0})){if(!r.isDirectory())continue;const i=c.join(n,r.name),a=readGraphMetadata(i);a&&(a.runtimeServices??[]).includes(t)&&s.push(i)}return s.sort()}export function writeGraphMetadata(e,t){u.mkdirSync(e,{recursive:!0});const n={key:t.key||null,dirName:t.dirName||null,instanceCount:Number.isFinite(t.instanceCount)?t.instanceCount:null,maxConcurrentTasks:Number.isFinite(t.maxConcurrentTasks)?t.maxConcurrentTasks:null,runtimeNames:[...t.runtimeNames].sort(),runtimeServices:t.runtimeNames,targetServices:[...t.targetNames].sort()};u.writeFileSync(c.join(e,y),JSON.stringify(n,null,2))}export function readGraphMetadata(e){const t=c.join(e,y);if(!u.existsSync(t))return null;try{const n=JSON.parse(u.readFileSync(t,"utf8"));return p(n)}catch{return null}}function p(e){if(!N(e))return null;const{dirName:t,instanceCount:n,key:s,maxConcurrentTasks:r,runtimeNames:i,runtimeServices:a,targetServices:f,...S}=e,o={...S};return l(t)&&(o.dirName=t),d(n)&&(o.instanceCount=n),l(s)&&(o.key=s),d(r)&&(o.maxConcurrentTasks=r),Array.isArray(i)&&(o.runtimeNames=i.filter(m)),Array.isArray(a)&&(o.runtimeServices=a.filter(m)),Array.isArray(f)&&(o.targetServices=f.filter(m)),o}function d(e){return e===null||typeof e=="number"&&Number.isFinite(e)}function m(e){return typeof e=="string"}
@@ -1,13 +1,11 @@
1
- import type { RuntimeSocket } from "../process/runtime-socket.js";
2
1
  import { type NormalizedDatabaseConfig, type NormalizedRuntimeConfig } from "../config/loader/index.js";
3
- import { type ExecutionEnvironmentConfig, type ExecutionEnvironmentLocalConfig, type ExecutionEnvironmentTestkitConfig, type ExecutionTemplateContextValues, type RuntimeUrlMapping } from "../steps/index.js";
2
+ import { type ExecutionEnvironmentConfig, type ExecutionEnvironmentLocalConfig, type ExecutionEnvironmentTestkitConfig, type ExecutionTemplateContextValues } from "../steps/index.js";
4
3
  import { readDatabaseInfo } from "../database/state.js";
5
- type RuntimeIdentifier = number | string;
6
4
  type StringMap = ReadonlyMap<string, string>;
7
5
  interface RuntimeLocalConfig extends ExecutionEnvironmentLocalConfig {
6
+ assignedPort?: number;
8
7
  baseUrl: string;
9
8
  cwd?: string;
10
- port?: number;
11
9
  readyUrl?: false | null | string;
12
10
  shutdownTimeoutMs?: number;
13
11
  start: string;
@@ -20,17 +18,10 @@ interface RuntimeServiceTestkitConfig extends ExecutionEnvironmentTestkitConfig
20
18
  export interface RuntimeServiceConfig extends ExecutionEnvironmentConfig {
21
19
  testkit: RuntimeServiceTestkitConfig;
22
20
  }
23
- export interface RuntimePortNamespace {
24
- index?: unknown;
25
- portOffset?: unknown;
26
- stride?: unknown;
27
- }
28
21
  export interface RuntimeInstanceResolutionOptions {
29
22
  externalEndpoints?: Readonly<Record<string, null | string | undefined>>;
30
23
  graphDirName?: null | string;
31
- portNamespaceIndex?: number;
32
- portNamespaceStride?: number;
33
- portOffset?: number;
24
+ ports?: ReadonlyMap<string, number>;
34
25
  privateEndpoints?: Readonly<Record<string, null | string | undefined>>;
35
26
  publicEndpoints?: Readonly<Record<string, null | string | undefined>>;
36
27
  publicHost?: null | string;
@@ -79,7 +70,6 @@ export declare function resolveRuntimeInstanceConfigs<T extends RuntimeServiceCo
79
70
  serviceContextUrlByService: StringMap;
80
71
  publicHost: string | null;
81
72
  stateDirByService: StringMap;
82
- urlMappings: readonly RuntimeUrlMapping[];
83
73
  leaseId: null;
84
74
  leaseDir: null;
85
75
  databaseBindingsReader: typeof readDatabaseInfo;
@@ -95,7 +85,7 @@ export declare function resolveRuntimeInstanceConfigs<T extends RuntimeServiceCo
95
85
  shutdownTimeoutMs?: number;
96
86
  start: string;
97
87
  cwd: string | undefined;
98
- port: number | undefined;
88
+ assignedPort: number | undefined;
99
89
  baseUrl: string | undefined;
100
90
  readyUrl: string | undefined;
101
91
  env: {
@@ -104,8 +94,7 @@ export declare function resolveRuntimeInstanceConfigs<T extends RuntimeServiceCo
104
94
  } | undefined;
105
95
  };
106
96
  })[];
107
- export declare function buildPortMap(runtimeConfigs: readonly RuntimeServiceConfig[], runtimeId: RuntimeIdentifier, namespace?: RuntimePortNamespace): Map<string, number>;
108
- export declare function resolveRuntimeConfig<T extends RuntimeServiceConfig>(config: T, runtimeId: string, runtimeLabel: string, runtimeDir: string, portMap: ReadonlyMap<string, number>, baseUrlByService: StringMap, readyUrlByService: StringMap, publicBaseUrlByService: StringMap, privateBaseUrlByService: StringMap, serviceContextUrlByService: StringMap, publicHost: null | string, stateDirByService: StringMap, urlMappings: readonly RuntimeUrlMapping[]): T & {
97
+ export declare function resolveRuntimeConfig<T extends RuntimeServiceConfig>(config: T, runtimeId: string, runtimeLabel: string, runtimeDir: string, portMap: ReadonlyMap<string, number>, baseUrlByService: StringMap, readyUrlByService: StringMap, publicBaseUrlByService: StringMap, privateBaseUrlByService: StringMap, serviceContextUrlByService: StringMap, publicHost: null | string, stateDirByService: StringMap): T & {
109
98
  stateDir: string;
110
99
  runtimeId: string;
111
100
  runtimeLabel: string;
@@ -145,7 +134,6 @@ export declare function resolveRuntimeConfig<T extends RuntimeServiceConfig>(con
145
134
  serviceContextUrlByService: StringMap;
146
135
  publicHost: string | null;
147
136
  stateDirByService: StringMap;
148
- urlMappings: readonly RuntimeUrlMapping[];
149
137
  leaseId: null;
150
138
  leaseDir: null;
151
139
  databaseBindingsReader: typeof readDatabaseInfo;
@@ -161,7 +149,7 @@ export declare function resolveRuntimeConfig<T extends RuntimeServiceConfig>(con
161
149
  shutdownTimeoutMs?: number;
162
150
  start: string;
163
151
  cwd: string | undefined;
164
- port: number | undefined;
152
+ assignedPort: number | undefined;
165
153
  baseUrl: string | undefined;
166
154
  readyUrl: string | undefined;
167
155
  env: {
@@ -173,8 +161,4 @@ export declare function resolveRuntimeConfig<T extends RuntimeServiceConfig>(con
173
161
  export declare function resolveServiceStateDir(runtimeDir: string, config: Pick<ExecutionEnvironmentConfig, "name">): string;
174
162
  export declare function resolveServicePrepareDir(runtimeDir: string, config: Pick<ExecutionEnvironmentConfig, "name">): string;
175
163
  export declare function resolveRuntimeUrl(rawUrl: string, serviceName: string, runtimeId: string, context: RuntimeUrlResolutionContext): string;
176
- export declare function rewriteUrlPort(rawUrl: string, port: number): string;
177
- export declare function numericPortFromUrl(rawUrl: string): number | null;
178
- export declare function socketFromUrl(rawUrl: string): RuntimeSocket | null;
179
- export declare function normalizeSocketHost(hostname: string): string;
180
164
  export {};
@@ -1 +1 @@
1
- import{finalizeRuntimeDatabaseConfig as R}from"../database/runtime-config.js";import w from"path";import{finalizeConfiguredInputs as N,finalizeConfiguredSteps as y}from"../steps/configured-steps.js";import"../config/loader/index.js";import{finalizeString as h,resolveTemplateString as E}from"../steps/index.js";import{readDatabaseInfo as M}from"../database/state.js";const O=100;export function resolveRuntimeInstanceConfigs(t,e,r,s={}){const o=B(s.graphDirName,e),c=buildPortMap(t,e,{index:s.portNamespaceIndex||0,stride:s.portNamespaceStride||1,portOffset:s.portOffset||0}),i=new Map,p=new Map,l=new Map,u=new Map,d=new Map,m=new Map,S=x(s.externalEndpoints),v=x(s.publicEndpoints),k=x(s.privateEndpoints),f=x(s.serviceContextEndpoints);for(const n of t)m.set(n.name,resolveServiceStateDir(r,n));for(const n of t)n.testkit.local&&(i.set(n.name,resolveRuntimeUrl(n.testkit.local.baseUrl,n.name,e,{runtimeId:e,runtimeLabel:o,runtimeDir:r,portMap:c,baseUrlByService:i,readyUrlByService:p,stateDirByService:m})),n.testkit.local.readyUrl!==!1&&n.testkit.local.readyUrl!=null&&p.set(n.name,resolveRuntimeUrl(n.testkit.local.readyUrl,n.name,e,{runtimeId:e,runtimeLabel:o,runtimeDir:r,portMap:c,baseUrlByService:i,readyUrlByService:p,stateDirByService:m})));for(const[n,a]of Object.entries(S))i.has(n)||i.set(n,a),p.has(n)||p.set(n,a);for(const[n,a]of Object.entries(v))l.set(n,a);for(const[n,a]of Object.entries(k))u.set(n,a);for(const[n,a]of Object.entries(f))d.set(n,a);for(const[n,a]of i.entries())l.has(n)||l.set(n,g(a,s.publicHost||null)),d.has(n)||d.set(n,a),u.has(n)||u.set(n,a);const b=[];for(const n of t){if(!n.testkit.local)continue;const a=i.get(n.name),U=p.get(n.name);a&&b.push([n.testkit.local.baseUrl,a]),U&&b.push([n.testkit.local.readyUrl,U])}return t.map(n=>resolveRuntimeConfig(n,e,o,r,c,i,p,l,u,d,s.publicHost||null,m,b))}function x(t){return Object.fromEntries(Object.entries(t??{}).map(([e,r])=>[e,(r??"").trim()]).filter(e=>e[1].length>0))}export function buildPortMap(t,e,r={}){const s=new Map,o=new Map,c=j(e,r)+Number(r.portOffset||0);for(const i of t){if(!i.testkit.local)continue;const p=i.testkit.local.port||numericPortFromUrl(i.testkit.local.baseUrl);if(!p)continue;const l=p+c;if(l>65535)throw new Error(`Runtime port resolution exceeded 65535 for service "${i.name}" (${String(l)}). Reduce runtime instances or choose lower local.port/baseUrl ports in testkit.config.ts.`);const u=o.get(l);if(u)throw new Error(`Runtime port collision: services "${u}" and "${i.name}" both resolve to ${String(l)}. Assign distinct local.port/baseUrl ports in testkit.config.ts.`);o.set(l,i.name),s.set(i.name,l)}return s}export function resolveRuntimeConfig(t,e,r,s,o,c,i,p,l,u,d,m,S){const v=resolveServiceStateDir(s,t),k=resolveServicePrepareDir(s,t),f={runtimeId:e,runtimeLabel:r,runtimeDir:s,productDir:t.productDir,serviceName:t.name,serviceCwd:t.testkit.local?.cwd||".",serviceStateDir:v,prepareDir:k,portMap:o,baseUrlByService:c,readyUrlByService:i,publicBaseUrlByService:p,privateBaseUrlByService:l,serviceContextUrlByService:u,publicHost:d,stateDirByService:m,urlMappings:S,leaseId:null,leaseDir:null,databaseBindingsReader:M},b=R(t.testkit.database,f),n={...t.testkit.runtime,prepare:P(t.testkit.runtime.prepare,f)},a=t.testkit.local?{...t.testkit.local,start:h(t.testkit.local.start,f),cwd:t.testkit.local.cwd!==void 0?h(t.testkit.local.cwd,f):t.testkit.local.cwd,port:o.get(t.name)||t.testkit.local.port,baseUrl:c.get(t.name),readyUrl:i.get(t.name),env:{...t.testkit.local.env||{}}}:void 0;return{...t,stateDir:v,runtimeId:e,runtimeLabel:r,testkit:{...t.testkit,database:b,prepareDir:k,templateContext:f,runtime:n,local:a}}}export function resolveServiceStateDir(t,e){return w.join(t,"services",e.name)}export function resolveServicePrepareDir(t,e){return w.join(resolveServiceStateDir(t,e),"prepared")}export function resolveRuntimeUrl(t,e,r,s){const o=E(t,{...s,runtimeId:r,serviceName:e}),c=s.portMap.get(e);return c?rewriteUrlPort(o,c):o}function g(t,e){if(!e)return t;try{const r=new URL(t);r.hostname=e;let s=r.toString();return!t.endsWith("/")&&r.pathname==="/"&&s.endsWith("/")&&(s=s.slice(0,-1)),s}catch{return t}}function P(t,e){return t?{inputs:N(t.inputs,r=>h(r,e)),steps:y(t.steps,r=>h(r,e))}:{inputs:[],steps:[]}}export function rewriteUrlPort(t,e){try{const r=new URL(t);if(!r.port)return t;const s=new URL(t);s.port=String(e);let o=s.toString();return!t.endsWith("/")&&r.pathname==="/"&&o.endsWith("/")&&(o=o.slice(0,-1)),o}catch{return t}}export function numericPortFromUrl(t){try{const e=new URL(t),r=Number(e.port);return Number.isInteger(r)&&r>0?r:null}catch{return null}}export function socketFromUrl(t){try{const e=new URL(t),r=Number(e.port);if(!Number.isInteger(r)||r<=0)return null;const s=normalizeSocketHost(e.hostname);return s?{host:s,port:r}:null}catch{return null}}export function normalizeSocketHost(t){return!t||t==="localhost"?"127.0.0.1":t==="[::1]"?"::1":t}function j(t,e={}){const r=String(t).match(/(\d+)$/),s=r?Number.parseInt(r[1],10)-1:0,o=Math.max(0,Number(e.index||0)),c=Math.max(1,Number(e.stride||1));return O*(o*c+s)}function B(t,e){return t?`${t}/${e}`:e}
1
+ import{finalizeRuntimeDatabaseConfig as w}from"../database/runtime-config.js";import S from"path";import{finalizeConfiguredInputs as E,finalizeConfiguredSteps as U}from"../steps/configured-steps.js";import"../config/loader/index.js";import{finalizeString as k,resolveTemplateString as y}from"../steps/index.js";import{readDatabaseInfo as j}from"../database/state.js";export function resolveRuntimeInstanceConfigs(e,n,s,r={}){const c=B(r.graphDirName,n),p=r.ports??new Map,i=new Map,o=new Map,u=new Map,f=new Map,d=new Map,m=new Map,v=x(r.externalEndpoints),b=x(r.publicEndpoints),l=x(r.privateEndpoints),h=x(r.serviceContextEndpoints);for(const t of e)m.set(t.name,resolveServiceStateDir(s,t));for(const t of e)t.testkit.local&&(i.set(t.name,resolveRuntimeUrl(t.testkit.local.baseUrl,t.name,n,{runtimeId:n,runtimeLabel:c,runtimeDir:s,portMap:p,baseUrlByService:i,readyUrlByService:o,stateDirByService:m})),t.testkit.local.readyUrl!==!1&&t.testkit.local.readyUrl!=null&&o.set(t.name,resolveRuntimeUrl(t.testkit.local.readyUrl,t.name,n,{runtimeId:n,runtimeLabel:c,runtimeDir:s,portMap:p,baseUrlByService:i,readyUrlByService:o,stateDirByService:m})));for(const[t,a]of Object.entries(v))i.has(t)||i.set(t,a),o.has(t)||o.set(t,a);for(const[t,a]of Object.entries(b))u.set(t,a);for(const[t,a]of Object.entries(l))f.set(t,a);for(const[t,a]of Object.entries(h))d.set(t,a);for(const[t,a]of i.entries())u.has(t)||u.set(t,C(a,r.publicHost||null)),d.has(t)||d.set(t,a),f.has(t)||f.set(t,a);return e.map(t=>resolveRuntimeConfig(t,n,c,s,p,i,o,u,f,d,r.publicHost||null,m))}function x(e){return Object.fromEntries(Object.entries(e??{}).map(([n,s])=>[n,(s??"").trim()]).filter(n=>n[1].length>0))}export function resolveRuntimeConfig(e,n,s,r,c,p,i,o,u,f,d,m){const v=resolveServiceStateDir(r,e),b=resolveServicePrepareDir(r,e),l={runtimeId:n,runtimeLabel:s,runtimeDir:r,productDir:e.productDir,serviceName:e.name,serviceCwd:e.testkit.local?.cwd||".",serviceStateDir:v,prepareDir:b,portMap:c,baseUrlByService:p,readyUrlByService:i,publicBaseUrlByService:o,privateBaseUrlByService:u,serviceContextUrlByService:f,publicHost:d,stateDirByService:m,leaseId:null,leaseDir:null,databaseBindingsReader:j},h=w(e.testkit.database,l),t={...e.testkit.runtime,prepare:R(e.testkit.runtime.prepare,l)},a=e.testkit.local?{...e.testkit.local,start:k(e.testkit.local.start,l),cwd:e.testkit.local.cwd!==void 0?k(e.testkit.local.cwd,l):e.testkit.local.cwd,assignedPort:c.get(e.name),baseUrl:p.get(e.name),readyUrl:i.get(e.name),env:{...e.testkit.local.env||{}}}:void 0;return{...e,stateDir:v,runtimeId:n,runtimeLabel:s,testkit:{...e.testkit,database:h,prepareDir:b,templateContext:l,runtime:t,local:a}}}export function resolveServiceStateDir(e,n){return S.join(e,"services",n.name)}export function resolveServicePrepareDir(e,n){return S.join(resolveServiceStateDir(e,n),"prepared")}export function resolveRuntimeUrl(e,n,s,r){return y(e,{...r,runtimeId:s,serviceName:n})}function C(e,n){if(!n)return e;try{const s=new URL(e);s.hostname=n;let r=s.toString();return!e.endsWith("/")&&s.pathname==="/"&&r.endsWith("/")&&(r=r.slice(0,-1)),r}catch{return e}}function R(e,n){return e?{inputs:E(e.inputs,s=>k(s,n)),steps:U(e.steps,s=>k(s,n))}:{inputs:[],steps:[]}}function B(e,n){return e?`${e}/${n}`:n}
@@ -1 +1 @@
1
- export function normalizeArtifactArchiveConfig(r){const n=e(r,"artifacts"),i=e(n?.retention,"artifacts.retention"),t=e(n?.publication,"artifacts.publication");return{retention:{maxRuns:o(i?.maxRuns,"artifacts.retention.maxRuns"),maxAgeDays:s(i?.maxAgeDays,"artifacts.retention.maxAgeDays"),preserveSuites:a(i?.preserveSuites,"artifacts.retention.preserveSuites")},publication:t?{directory:u(t.directory,"artifacts.publication.directory"),required:f(t.required,"artifacts.publication.required")}:null}}function o(r,n){if(r==null)return null;if(typeof r!="number"||!Number.isInteger(r)||r<=0)throw new Error(`${n} must be a positive integer`);return r}function s(r,n){if(r==null)return null;if(typeof r!="number"||!Number.isFinite(r)||r<=0)throw new Error(`${n} must be a positive number`);return r}function a(r,n){if(r==null)return[];if(!m(r))throw new Error(`${n} must be an array of non-empty strings`);const i=r.map(t=>{if(typeof t!="string"||t.trim().length===0)throw new Error(`${n} must be an array of non-empty strings`);return t.trim()});return[...new Set(i)].sort()}function u(r,n){if(typeof r!="string"||r.trim().length===0)throw new Error(`${n} is required`);return r.trim()}function f(r,n){if(r===void 0)return!1;if(typeof r!="boolean")throw new Error(`${n} must be a boolean`);return r}function e(r,n){if(r!=null){if(!c(r))throw new Error(`${n} must be an object`);return r}}function c(r){return typeof r=="object"&&r!==null&&!Array.isArray(r)}function m(r){return Array.isArray(r)}
1
+ import{isUnknownArray as o,isUnknownRecord as s}from"../kernel/unknown-values.js";export function normalizeArtifactArchiveConfig(r){const n=e(r,"artifacts"),i=e(n?.retention,"artifacts.retention"),t=e(n?.publication,"artifacts.publication");return{retention:{maxRuns:a(i?.maxRuns,"artifacts.retention.maxRuns"),maxAgeDays:u(i?.maxAgeDays,"artifacts.retention.maxAgeDays"),preserveSuites:f(i?.preserveSuites,"artifacts.retention.preserveSuites")},publication:t?{directory:m(t.directory,"artifacts.publication.directory"),required:c(t.required,"artifacts.publication.required")}:null}}function a(r,n){if(r==null)return null;if(typeof r!="number"||!Number.isInteger(r)||r<=0)throw new Error(`${n} must be a positive integer`);return r}function u(r,n){if(r==null)return null;if(typeof r!="number"||!Number.isFinite(r)||r<=0)throw new Error(`${n} must be a positive number`);return r}function f(r,n){if(r==null)return[];if(!o(r))throw new Error(`${n} must be an array of non-empty strings`);const i=r.map(t=>{if(typeof t!="string"||t.trim().length===0)throw new Error(`${n} must be an array of non-empty strings`);return t.trim()});return[...new Set(i)].sort()}function m(r,n){if(typeof r!="string"||r.trim().length===0)throw new Error(`${n} is required`);return r.trim()}function c(r,n){if(r===void 0)return!1;if(typeof r!="boolean")throw new Error(`${n} must be a boolean`);return r}function e(r,n){if(r!=null){if(!s(r))throw new Error(`${n} must be an object`);return r}}
@@ -1,5 +1,4 @@
1
- export { requireObjectRecord, isObjectRecord, requireStringArray as normalizeStringArray } from "./read-fields.js";
1
+ export { requireObjectRecord, requireStringArray as normalizeStringArray } from "./read-fields.js";
2
2
  export type UnknownRecord = Record<string, unknown>;
3
- export declare function isUnknownArray(value: unknown): value is unknown[];
4
3
  export declare function readOptionalProperty(value: UnknownRecord | undefined, key: string): unknown;
5
4
  export declare function normalizeOptionalNonEmptyString(value: unknown, label: string): string | null;
@@ -1 +1 @@
1
- export{requireObjectRecord,isObjectRecord,requireStringArray as normalizeStringArray}from"./read-fields.js";export function isUnknownArray(r){return Array.isArray(r)}export function readOptionalProperty(r,n){return r?.[n]}export function normalizeOptionalNonEmptyString(r,n){if(r==null||r==="")return null;if(typeof r!="string")throw new Error(`${n} must be a string or null`);return r}
1
+ export{requireObjectRecord,requireStringArray as normalizeStringArray}from"./read-fields.js";export function readOptionalProperty(r,n){return r?.[n]}export function normalizeOptionalNonEmptyString(r,n){if(r==null||r==="")return null;if(typeof r!="string")throw new Error(`${n} must be a string or null`);return r}
@@ -1,2 +1,2 @@
1
- import{isPathWithin as F}from"../process/files/paths.js";import{readRunDocument as P}from"./read-run.js";import l from"node:fs";import o from"node:path";import"../engines/api/index.js";import"../domain/identifiers.js";import{normalizeArtifactArchiveConfig as _}from"./artifact-config.js";import{applyRunBundleRetention as C,publishRunBundle as M,sealRunBundle as B}from"./bundles.js";import{RETENTION_DECISION_SCHEMA_VERSION as $}from"./contracts.js";import{applyTimingUpdates as U,createEmptyTimings as I,normalizeTimings as J}from"./timing.js";import{isObjectRecord as q,isUnknownArray as V,normalizeOptionalNonEmptyString as E,normalizeStringArray as K,readOptionalProperty as x,requireObjectRecord as X}from"./artifact-validation.js";import{errorMessage as G,isPropertyContainer as W,readProperty as H,serializeJson as j,writeJsonFileAtomicWithRetry as k}from"../process/files/json-file.js";import{copyRegularFileSecurely as Q}from"./secure-file-copy.js";const N="timings.json",A="artifacts",Y="logs",Z="setup",R="live.json",tt=1e4,z=String;export function writeRunArtifact(t,e,r={},n={}){const i=e,s=o.join(t,".testkit","results");l.mkdirSync(s,{recursive:!0});const c=i.archive,a=r.archiveConfig??_(x(c,"request")||{}),u=r.declaredEnvironmentKeys??K(x(c,"declaredEnvironmentKeys")||[],"run artifact declaredEnvironmentKeys"),m=n.sealRunBundle??B,p=n.publishRunBundle??M,d=n.applyRunBundleRetention??C,y=n.writeLatestProjection??k,f=m(t,i,{env:r.env,archiveConfig:a,declaredEnvironmentKeys:u}),h=et(t,f.archive.runId,a.publication,p),S=rt(t,f.archive.runId,a.retention,d),v=i.finalizationFailure,L={...f,...v===void 0?{}:{finalizationFailure:v},run:h.status==="failed"&&h.required===!0||f.archive.cleanup?.status==="failed"||v!==void 0&&f.run.status!=="interrupted"?{...f.run,status:"failed"}:f.run,archive:{...f.archive,publication:h,retention:S}};return nt(s,L,y)}function et(t,e,r,n){try{return n(t,e,r)}catch(i){return{bytesUploaded:0,configured:r!==null,error:T(i),recordedAt:new Date().toISOString(),required:r?.required??!1,runId:e,schemaVersion:1,status:"failed",target:r===null?null:o.resolve(t,r.directory)}}}function rt(t,e,r,n){try{return n(t,e,r)}catch(i){return{currentRunId:e,decidedAt:new Date().toISOString(),error:T(i),garbageCollection:{failures:[],reclaimedBytes:0,removedObjects:0,status:"not_started"},policy:{maxAgeDays:r.maxAgeDays,maxRuns:r.maxRuns,preserveSuites:[...r.preserveSuites]},protectedRunIds:[e],removed:[],removalErrors:[],schemaVersion:$,status:"failed"}}}function nt(t,e,r){const n=o.join(t,"latest.json"),i={path:w(o.relative(o.dirname(t),n)),status:"complete"},s={...e,archive:{...e.archive,latest:i}};try{return r(n,s),s}catch(c){return{...s,archive:{...s.archive,latest:{...i,error:T(c),status:"failed"}}}}}function T(t){try{return G(t)}catch{return"Unknown post-seal archive operation failure"}}export function writeLiveRunArtifact(t,e){const r=e,n=o.join(t,".testkit","results");l.mkdirSync(n,{recursive:!0}),k(o.join(n,R),r)}export function finalizeLiveRunArtifact(t,e={}){const r=ot(e),n=o.join(t,".testkit","results",R),i=ft(n);if(i===null)return!1;const s=it(i,r);return s?(writeRunArtifact(t,s),!0):!1}function it(t,e){const r=P(t,"live run artifact"),n=r.run.id||null;if(e.runId&&n&&n!==e.runId)return null;const i=at(e.finishedAt),s=new Date(i).toISOString(),c=ut(r.run.startedAt),a=c===null?r.run.durationMs:Math.max(0,i-c);return{...r,generatedAt:s,run:{...r.run,id:n??e.runId,status:e.status,finishedAt:s,durationMs:a,interruptReason:e.reason},timings:st(r.timings,s,e.reason)}}function st(t,e,r){if(t==null)return t;const n=X(t,"live run artifact.timings"),i=n.operations;if(i==null)return n;if(!V(i))throw new Error("live run artifact.timings.operations must be an array");return{...n,operations:i.map(s=>!q(s)||s.status!=="running"?s:{...s,status:"cancelled",finishedAt:E(s.finishedAt,"live run operation.finishedAt")??e,message:E(s.message,"live run operation.message")??r})}}function ot(t){return{runId:t.runId??null,reason:t.reason??"run interrupted",status:t.status??"interrupted",finishedAt:t.finishedAt??Date.now()}}function at(t){if(t instanceof Date&&Number.isFinite(t.getTime()))return t.getTime();if(typeof t=="number"&&Number.isFinite(t))return t;if(typeof t=="string"){const e=Date.parse(t);if(Number.isFinite(e))return e}return Date.now()}function ut(t){if(typeof t!="string")return null;const e=Date.parse(t);return Number.isFinite(e)?e:null}export function resetResultArtifacts(t){l.rmSync(o.join(t,".testkit","results",A),{recursive:!0,force:!0}),l.rmSync(o.join(t,".testkit","results",Y),{recursive:!0,force:!0}),l.rmSync(o.join(t,".testkit","results",Z),{recursive:!0,force:!0}),l.rmSync(o.join(t,".testkit","results",R),{force:!0})}export function persistTaskArtifacts(t,e,r){if(r.length===0)return[];const n=g(e.serviceName||"service"),i=D(t,n);return r.map((s,c)=>{const a=ct(s),u={schemaVersion:2,source:"testkit-runtime-artifact",service:e.serviceName,suite:{engineId:e.engineId,key:e.suiteKey,name:e.suiteName,profileId:e.profileId,testType:e.testType},file:e.file,taskId:e.id,index:c,name:a.name,kind:a.kind,summary:a.summary,contentType:a.contentType,emittedAt:a.emittedAt,data:a.data},m=`${j(u,"runtime artifact payload")}
2
- `,p=O(i,e.id,c,a.name,".json",y=>{l.writeFileSync(y,m,{flag:"wx"})}),d=o.join(".testkit","results",A,n,p);return{name:u.name,kind:u.kind,summary:u.summary,contentType:u.contentType,path:w(d)}})}export function persistTaskFileArtifacts(t,e,r,n={}){if(r.length===0)return[];const i=g(e.serviceName||"service"),s=D(t,i);return r.map((c,a)=>{const u=lt(c),m=o.resolve(u.path),p=pt(m,u.contentType),d=[],y=O(s,e.id,a,u.name,p,S=>{d.push(Q(m,S,{...n.maxBytes===void 0?{}:{maxBytes:n.maxBytes},...n.sourceRoot===void 0?{}:{sourceRoot:n.sourceRoot},...n.sizeLimitDescription===void 0?{}:{sizeLimitDescription:n.sizeLimitDescription}}))}),f=d.at(0);if(f===void 0)throw new Error("File artifact copy completed without a result");n.copyResults?.push(f);const h=o.join(".testkit","results",A,i,y);return{name:u.name,kind:u.kind,summary:u.summary,contentType:u.contentType??yt(m),path:w(h)}})}export function persistTaskOutputArtifacts(t,e,r){if(r.length===0)return[];const n=[];for(const i of r){const s=i.text;s.trim().length!==0&&n.push({name:i.name||"task-output",kind:i.kind||"runtime.output",summary:i.summary||dt(s),contentType:i.contentType||"text/plain",emittedAt:i.emittedAt||new Date().toISOString(),data:{stream:i.stream||null,text:s}})}return persistTaskArtifacts(t,e,n)}export function loadTimings(t){const e=o.join(t,".testkit",N);if(!l.existsSync(e))return I();try{const r=JSON.parse(l.readFileSync(e,"utf8"));return J(r)}catch{return I()}}export function saveTimings(t,e,r){if(r.length===0)return;const n=U(e,r),i=o.join(t,".testkit");l.mkdirSync(i,{recursive:!0}),l.writeFileSync(o.join(i,N),j(n,"timings"))}function ct(t){return{name:t.name,kind:t.kind||null,summary:t.summary||null,contentType:t.contentType||"application/json",emittedAt:t.emittedAt||null,data:t.data}}function lt(t){return{path:t.path,name:t.name||"artifact",kind:t.kind||null,summary:t.summary||null,contentType:t.contentType||null}}function mt(t,e,r,n,i=0){const s=i===0?"":`-${(i+1).toString().padStart(2,"0")}`;return`task-${g(t)}-${(e+1).toString().padStart(2,"0")}-${g(r)}${s}${n}`}function O(t,e,r,n,i,s){for(let c=0;c<tt;c+=1){const a=mt(e,r,n,i,c),u=o.join(t,a);try{return s(u),a}catch(m){if(b(m,"EEXIST"))continue;throw l.rmSync(u,{force:!0}),m}}throw new Error(`Could not allocate a unique artifact path for task ${z(e)} and artifact ${n}`)}function D(t,e){const r=o.resolve(t);if(!l.statSync(r).isDirectory())throw new Error("Artifact product directory must be a directory");const i=l.realpathSync(r);let s=r;for(const c of[".testkit","results",A,e]){s=o.join(s,c);try{l.mkdirSync(s)}catch(m){if(!b(m,"EEXIST"))throw m}const a=l.lstatSync(s);if(a.isSymbolicLink()||!a.isDirectory())throw new Error(`Artifact output directory ${JSON.stringify(s)} must be a real directory below productDir`);const u=l.realpathSync(s);if(!F(i,u))throw new Error(`Artifact output directory ${JSON.stringify(s)} escapes productDir`)}return s}function g(t){return z(t).trim().toLowerCase().replace(/[^a-z0-9._-]+/g,"-").replace(/^-+|-+$/g,"")||"artifact"}function w(t){return t.split(o.sep).join("/")}function ft(t){try{return JSON.parse(l.readFileSync(t,"utf8"))}catch{return null}}function dt(t){const e=t.split(/\r?\n/u).map(r=>r.trim()).find(Boolean);return e?e.slice(0,120):"captured task output"}function pt(t,e){const r=o.extname(t);if(r)return r;const n=(e??"").split(";",1)[0].trim().toLowerCase();return n==="text/markdown"?".md":n==="text/plain"?".txt":n==="application/json"?".json":n==="application/zip"?".zip":n==="image/png"?".png":n==="image/jpeg"?".jpg":n==="text/html"?".html":n==="video/webm"?".webm":".artifact"}function yt(t){const e=o.extname(t).toLowerCase();return e===".md"||e===".markdown"?"text/markdown":e===".txt"||e===".log"?"text/plain":e===".json"?"application/json":"application/octet-stream"}function b(t,e){return W(t)&&H(t,"code")===e}
1
+ import{isPathWithin as F}from"../process/files/paths.js";import{isUnknownArray as P,isUnknownRecord as _}from"../kernel/unknown-values.js";import{readRunDocument as C}from"./read-run.js";import l from"node:fs";import o from"node:path";import"../engines/api/index.js";import"../domain/identifiers.js";import{normalizeArtifactArchiveConfig as M}from"./artifact-config.js";import{applyRunBundleRetention as B,publishRunBundle as $,sealRunBundle as U}from"./bundles.js";import{RETENTION_DECISION_SCHEMA_VERSION as J}from"./contracts.js";import{applyTimingUpdates as q,createEmptyTimings as I,normalizeTimings as V}from"./timing.js";import{normalizeOptionalNonEmptyString as E,normalizeStringArray as K,readOptionalProperty as x,requireObjectRecord as X}from"./artifact-validation.js";import{errorMessage as G,serializeJson as k,writeJsonFileAtomicWithRetry as j}from"../process/files/json-file.js";import{copyRegularFileSecurely as W}from"./secure-file-copy.js";import{hasErrorCode as N}from"../kernel/error-serialization.js";import{posixPath as R}from"../process/files/paths.js";const z="timings.json",A="artifacts",H="logs",Q="setup",T="live.json",Y=1e4,O=String;export function writeRunArtifact(t,e,r={},n={}){const i=e,s=o.join(t,".testkit","results");l.mkdirSync(s,{recursive:!0});const c=i.archive,a=r.archiveConfig??M(x(c,"request")||{}),u=r.declaredEnvironmentKeys??K(x(c,"declaredEnvironmentKeys")||[],"run artifact declaredEnvironmentKeys"),m=n.sealRunBundle??U,p=n.publishRunBundle??$,d=n.applyRunBundleRetention??B,y=n.writeLatestProjection??j,f=m(t,i,{env:r.env,archiveConfig:a,declaredEnvironmentKeys:u}),h=Z(t,f.archive.runId,a.publication,p),S=tt(t,f.archive.runId,a.retention,d),v=i.finalizationFailure,b={...f,...v===void 0?{}:{finalizationFailure:v},run:h.status==="failed"&&h.required===!0||f.archive.cleanup?.status==="failed"||v!==void 0&&f.run.status!=="interrupted"?{...f.run,status:"failed"}:f.run,archive:{...f.archive,publication:h,retention:S}};return et(s,b,y)}function Z(t,e,r,n){try{return n(t,e,r)}catch(i){return{bytesUploaded:0,configured:r!==null,error:w(i),recordedAt:new Date().toISOString(),required:r?.required??!1,runId:e,schemaVersion:1,status:"failed",target:r===null?null:o.resolve(t,r.directory)}}}function tt(t,e,r,n){try{return n(t,e,r)}catch(i){return{currentRunId:e,decidedAt:new Date().toISOString(),error:w(i),garbageCollection:{failures:[],reclaimedBytes:0,removedObjects:0,status:"not_started"},policy:{maxAgeDays:r.maxAgeDays,maxRuns:r.maxRuns,preserveSuites:[...r.preserveSuites]},protectedRunIds:[e],removed:[],removalErrors:[],schemaVersion:J,status:"failed"}}}function et(t,e,r){const n=o.join(t,"latest.json"),i={path:R(o.relative(o.dirname(t),n)),status:"complete"},s={...e,archive:{...e.archive,latest:i}};try{return r(n,s),s}catch(c){return{...s,archive:{...s.archive,latest:{...i,error:w(c),status:"failed"}}}}}function w(t){try{return G(t)}catch{return"Unknown post-seal archive operation failure"}}export function writeLiveRunArtifact(t,e){const r=e,n=o.join(t,".testkit","results");l.mkdirSync(n,{recursive:!0}),j(o.join(n,T),r)}export function finalizeLiveRunArtifact(t,e={}){const r=it(e),n=o.join(t,".testkit","results",T),i=lt(n);if(i===null)return!1;const s=rt(i,r);return s?(writeRunArtifact(t,s),!0):!1}function rt(t,e){const r=C(t,"live run artifact"),n=r.run.id||null;if(e.runId&&n&&n!==e.runId)return null;const i=st(e.finishedAt),s=new Date(i).toISOString(),c=ot(r.run.startedAt),a=c===null?r.run.durationMs:Math.max(0,i-c);return{...r,generatedAt:s,run:{...r.run,id:n??e.runId,status:e.status,finishedAt:s,durationMs:a,interruptReason:e.reason},timings:nt(r.timings,s,e.reason)}}function nt(t,e,r){if(t==null)return t;const n=X(t,"live run artifact.timings"),i=n.operations;if(i==null)return n;if(!P(i))throw new Error("live run artifact.timings.operations must be an array");return{...n,operations:i.map(s=>!_(s)||s.status!=="running"?s:{...s,status:"cancelled",finishedAt:E(s.finishedAt,"live run operation.finishedAt")??e,message:E(s.message,"live run operation.message")??r})}}function it(t){return{runId:t.runId??null,reason:t.reason??"run interrupted",status:t.status??"interrupted",finishedAt:t.finishedAt??Date.now()}}function st(t){if(t instanceof Date&&Number.isFinite(t.getTime()))return t.getTime();if(typeof t=="number"&&Number.isFinite(t))return t;if(typeof t=="string"){const e=Date.parse(t);if(Number.isFinite(e))return e}return Date.now()}function ot(t){if(typeof t!="string")return null;const e=Date.parse(t);return Number.isFinite(e)?e:null}export function resetResultArtifacts(t){l.rmSync(o.join(t,".testkit","results",A),{recursive:!0,force:!0}),l.rmSync(o.join(t,".testkit","results",H),{recursive:!0,force:!0}),l.rmSync(o.join(t,".testkit","results",Q),{recursive:!0,force:!0}),l.rmSync(o.join(t,".testkit","results",T),{force:!0})}export function persistTaskArtifacts(t,e,r){if(r.length===0)return[];const n=g(e.serviceName||"service"),i=L(t,n);return r.map((s,c)=>{const a=at(s),u={schemaVersion:2,source:"testkit-runtime-artifact",service:e.serviceName,suite:{engineId:e.engineId,key:e.suiteKey,name:e.suiteName,profileId:e.profileId,testType:e.testType},file:e.file,taskId:e.id,index:c,name:a.name,kind:a.kind,summary:a.summary,contentType:a.contentType,emittedAt:a.emittedAt,data:a.data},m=`${k(u,"runtime artifact payload")}
2
+ `,p=D(i,e.id,c,a.name,".json",y=>{l.writeFileSync(y,m,{flag:"wx"})}),d=o.join(".testkit","results",A,n,p);return{name:u.name,kind:u.kind,summary:u.summary,contentType:u.contentType,path:R(d)}})}export function persistTaskFileArtifacts(t,e,r,n={}){if(r.length===0)return[];const i=g(e.serviceName||"service"),s=L(t,i);return r.map((c,a)=>{const u=ut(c),m=o.resolve(u.path),p=ft(m,u.contentType),d=[],y=D(s,e.id,a,u.name,p,S=>{d.push(W(m,S,{...n.maxBytes===void 0?{}:{maxBytes:n.maxBytes},...n.sourceRoot===void 0?{}:{sourceRoot:n.sourceRoot},...n.sizeLimitDescription===void 0?{}:{sizeLimitDescription:n.sizeLimitDescription}}))}),f=d.at(0);if(f===void 0)throw new Error("File artifact copy completed without a result");n.copyResults?.push(f);const h=o.join(".testkit","results",A,i,y);return{name:u.name,kind:u.kind,summary:u.summary,contentType:u.contentType??dt(m),path:R(h)}})}export function persistTaskOutputArtifacts(t,e,r){if(r.length===0)return[];const n=[];for(const i of r){const s=i.text;s.trim().length!==0&&n.push({name:i.name||"task-output",kind:i.kind||"runtime.output",summary:i.summary||mt(s),contentType:i.contentType||"text/plain",emittedAt:i.emittedAt||new Date().toISOString(),data:{stream:i.stream||null,text:s}})}return persistTaskArtifacts(t,e,n)}export function loadTimings(t){const e=o.join(t,".testkit",z);if(!l.existsSync(e))return I();try{const r=JSON.parse(l.readFileSync(e,"utf8"));return V(r)}catch{return I()}}export function saveTimings(t,e,r){if(r.length===0)return;const n=q(e,r),i=o.join(t,".testkit");l.mkdirSync(i,{recursive:!0}),l.writeFileSync(o.join(i,z),k(n,"timings"))}function at(t){return{name:t.name,kind:t.kind||null,summary:t.summary||null,contentType:t.contentType||"application/json",emittedAt:t.emittedAt||null,data:t.data}}function ut(t){return{path:t.path,name:t.name||"artifact",kind:t.kind||null,summary:t.summary||null,contentType:t.contentType||null}}function ct(t,e,r,n,i=0){const s=i===0?"":`-${(i+1).toString().padStart(2,"0")}`;return`task-${g(t)}-${(e+1).toString().padStart(2,"0")}-${g(r)}${s}${n}`}function D(t,e,r,n,i,s){for(let c=0;c<Y;c+=1){const a=ct(e,r,n,i,c),u=o.join(t,a);try{return s(u),a}catch(m){if(N(m,"EEXIST"))continue;throw l.rmSync(u,{force:!0}),m}}throw new Error(`Could not allocate a unique artifact path for task ${O(e)} and artifact ${n}`)}function L(t,e){const r=o.resolve(t);if(!l.statSync(r).isDirectory())throw new Error("Artifact product directory must be a directory");const i=l.realpathSync(r);let s=r;for(const c of[".testkit","results",A,e]){s=o.join(s,c);try{l.mkdirSync(s)}catch(m){if(!N(m,"EEXIST"))throw m}const a=l.lstatSync(s);if(a.isSymbolicLink()||!a.isDirectory())throw new Error(`Artifact output directory ${JSON.stringify(s)} must be a real directory below productDir`);const u=l.realpathSync(s);if(!F(i,u))throw new Error(`Artifact output directory ${JSON.stringify(s)} escapes productDir`)}return s}function g(t){return O(t).trim().toLowerCase().replace(/[^a-z0-9._-]+/g,"-").replace(/^-+|-+$/g,"")||"artifact"}function lt(t){try{return JSON.parse(l.readFileSync(t,"utf8"))}catch{return null}}function mt(t){const e=t.split(/\r?\n/u).map(r=>r.trim()).find(Boolean);return e?e.slice(0,120):"captured task output"}function ft(t,e){const r=o.extname(t);if(r)return r;const n=(e??"").split(";",1)[0].trim().toLowerCase();return n==="text/markdown"?".md":n==="text/plain"?".txt":n==="application/json"?".json":n==="application/zip"?".zip":n==="image/png"?".png":n==="image/jpeg"?".jpg":n==="text/html"?".html":n==="video/webm"?".webm":".artifact"}function dt(t){const e=o.extname(t).toLowerCase();return e===".md"||e===".markdown"?"text/markdown":e===".txt"||e===".log"?"text/plain":e===".json"?"application/json":"application/octet-stream"}
@@ -1 +1 @@
1
- import{recordWork as H}from"../kernel/work-metrics.js";import{optionalString as A}from"../kernel/unknown-values.js";import{readRunProvenance as ue}from"./read-provenance.js";import{parseRunBundleArtifact as le,parseRunBundleManifest as J,readRunBundleArtifact as fe}from"./read-bundle.js";import{readRunDocument as de}from"./read-run.js";import me from"crypto";import c from"fs";import u from"path";import{redactSensitiveText as Y}from"../kernel/redaction.js";import{BUNDLE_SCHEMA_VERSION as G,OBJECTS_DIRNAME as Q,PROVENANCE_SCHEMA_VERSION as pe,RETENTION_DECISION_SCHEMA_VERSION as he,RUNS_DIRNAME as ge}from"./contracts.js";import{bundleRelativePath as x,errorMessage as k,hashFile as F,isObjectRecord as $,normalizePath as X,normalizeRunId as w,objectRecords as Z,optionalStringArray as z,optionalUnknownArray as ye,readJsonFile as P,requirePublicationPathOutsideResults as M,requireRunId as ve,runIncludesFile as Se,resolveManifestEntryPath as ee,resolveRunBundleDirectory as O,resolveRunsRoot as te,resultsRoot as U,sanitizeSegment as Re,sha256 as C,stableJson as q,treeBytes as ne,walkFiles as re,writeBufferAtomic as je,writeJson as N}from"./storage.js";import{resolveCiProvenance as be}from"./ci-provenance.js";const W=["artifacts","logs","setup"],Ie=/(?:PASSWORD|TOKEN|SECRET|PRIVATE_KEY|AUTH|COOKIE|SESSION|API_KEY)/iu,we=["package.json","package-lock.json","npm-shrinkwrap.json","pnpm-lock.yaml","yarn.lock","testkit.config.ts","testkit.config.mts","testkit.config.js","testkit.config.mjs"];export function sealRunBundle(e,n,t={},s={}){const r=n,o=ve(r),i=te(e,!0),a=u.dirname(i),f=u.join(i,o),m=s.cleanupActiveRunResults??ze,p=loadVerifiedRunBundleArtifactIfExists(e,o);if(p!==null)return{...p,archive:{...p.archive,cleanup:m(a)}};const d=u.join(i,`.${o}.${String(process.pid)}.${String(Date.now())}.${me.randomBytes(4).toString("hex")}.tmp`);c.mkdirSync(d,{recursive:!0});try{const R=Te(t.env??process.env),g=Ee({productDir:e,resultsDir:a,runId:o,tempDir:d,sensitiveValues:R}),h=Be(r,o,R),y=de(h.artifact,"run artifact");g.redaction.structuredReplacements=h.replacements,g.redaction.replacements+=h.replacements;const v=$e(e,y,o,{...t,redaction:g.redaction}),j={schemaVersion:G,runId:o,status:"complete",canonicalRunPath:x(o,"run.json"),manifestPath:x(o,"manifest.json"),provenancePath:x(o,"provenance.json"),integrity:"sha256",publication:{configured:!!t.archiveConfig?.publication,status:t.archiveConfig?.publication?"pending":"not_configured"},redaction:g.redaction};y.archive=j,N(u.join(d,"run.json"),y),N(u.join(d,"provenance.json"),v);const b=xe(d),D=C(Buffer.from(q(b))),E={schemaVersion:G,runId:o,createdAt:new Date().toISOString(),algorithm:"sha256",files:b,storage:{fileCount:b.length,logicalBytes:b.reduce((T,_)=>T+_.bytes,0),objectBytesWritten:g.objectBytesWritten,deduplicatedBytes:g.deduplicatedBytes},redaction:g.redaction,digest:D};return N(u.join(d,"manifest.json"),E),c.renameSync(d,f),{...y,archive:{...j,cleanup:m(a),manifestDigest:E.digest,storage:E.storage}}}catch(R){try{c.rmSync(d,{recursive:!0,force:!0})}catch{}try{Ce(e)}catch{}throw R}}export function loadVerifiedRunBundleArtifactIfExists(e,n){const t=w(n),s=O(e,t);if(!c.existsSync(s))return null;const r=verifyRunBundle(e,t);if(!r.ok)throw new Error(`Immutable run bundle ${t} already exists but failed integrity verification: ${r.errors.join("; ")}`);return loadRunBundleArtifact(e,t)}export function loadRunBundleArtifact(e,n){return se(e,n).artifact}function se(e,n){const t=w(n),s=fe(e,t),r=loadRunBundleManifest(e,t);return{manifest:r,artifact:{...s,archive:{...s.archive,manifestDigest:r.digest,storage:r.storage}}}}export function loadRunBundleManifest(e,n){const t=w(n),s=O(e,t),r=u.join(s,"manifest.json");if(!c.existsSync(r))throw new Error(`No immutable Testkit run manifest found for ${t}`);return J(P(r),r,t)}export function listRunBundles(e,n={}){return ie(e,n).map(({artifact:t,manifest:s,...r})=>r)}function ie(e,n={}){const t=te(e,!1);if(!c.existsSync(t))return[];const s=c.readdirSync(t,{withFileTypes:!0});return H("artifacts.runDirectoriesRead",1),H("artifacts.runEntriesVisited",s.length),s.filter(o=>o.isDirectory()&&!o.name.startsWith(".")).map(o=>{try{const{artifact:i,manifest:a}=se(e,o.name);return n.file&&!Se(i,n.file)?null:{artifact:i,manifest:a,runId:o.name,status:i.run.status,startedAt:i.run.startedAt,finishedAt:i.run.finishedAt??null,manifestDigest:i.archive.manifestDigest??null,canonicalRunPath:i.archive.canonicalRunPath}}catch{return null}}).filter(o=>o!==null).sort((o,i)=>(i.finishedAt??i.startedAt??"").localeCompare(o.finishedAt??o.startedAt??""))}export function verifyRunBundle(e,n){const t=w(n);try{return V(O(e,t),t)}catch(s){return{ok:!1,runId:t,errors:[k(s)],checkedFiles:0}}}export function publishRunBundle(e,n,t,s={}){const r=w(n),o=s.writePublicationReceipt??Pe;if(!t)return o(e,r,{status:"not_configured",required:!1,target:null,bytesUploaded:0});const i=u.resolve(e,t.directory);try{M(e,i)}catch(d){return o(e,r,{status:"failed",required:t.required,target:i,error:k(d),bytesUploaded:0})}const a=Re(u.basename(e)),f=u.join(i,a),m=u.join(f,r),p=u.join(f,`.${r}.${String(process.pid)}.${String(Date.now())}.tmp`);try{const d=O(e,r);if(c.mkdirSync(f,{recursive:!0}),M(e,f),c.existsSync(m)){const h=c.lstatSync(m);if(h.isSymbolicLink()||!h.isDirectory())throw new Error("Publication target must be a regular bundle directory");M(e,m);const y=V(d,r);if(!y.ok)throw new Error(`Source run bundle failed integrity verification: ${y.errors.join("; ")}`);const v=V(m,r);if(!v.ok)throw new Error(`Publication target failed integrity verification: ${v.errors.join("; ")}`);if(y.manifestDigest!==v.manifestDigest)throw new Error("Publication target already exists with a different manifest digest");return o(e,r,{status:"complete",required:t.required,target:m,manifestDigest:y.manifestDigest,reused:!0,bundleBytes:ne(d),bytesUploaded:0})}const R=ne(d);c.cpSync(d,p,{recursive:!0,preserveTimestamps:!0});const g=V(p,r);if(!g.ok)throw new Error(g.errors.join("; "));return c.renameSync(p,m),o(e,r,{status:"complete",required:t.required,target:m,manifestDigest:g.manifestDigest,reused:!1,bundleBytes:R,bytesUploaded:R})}catch(d){return c.rmSync(p,{recursive:!0,force:!0}),o(e,r,{status:"failed",required:t.required,target:m,error:k(d),bytesUploaded:0})}}export function applyRunBundleRetention(e,n,t={},s={}){const r=w(n),o=s.removeObject??ce,i=s.removeRunBundle??Ne,a=t.maxRuns??null,f=t.maxAgeDays??null,m=new Set(t.preserveSuites||[]),p=ie(e),d=new Set([r]);for(const l of p)Oe(l.artifact,m)&&d.add(l.runId);const R=new Set(a?p.slice(0,a).map(l=>l.runId):p.map(l=>l.runId)),g=f==null?null:Date.now()-f*24*60*60*1e3,h=[];for(const l of p){if(d.has(l.runId))continue;const S=g!=null&&Date.parse(l.finishedAt||l.startedAt||"")<g,I=!R.has(l.runId);if(!S&&!I)continue;const B=[];S&&B.push("max_age"),I&&B.push("max_runs"),h.push({path:u.join(U(e),ge,l.runId),reasons:B,runId:l.runId})}const y=new Map(p.map(l=>[l.runId,l.manifest.files.filter(S=>S.path.startsWith("files/")).map(S=>S.sha256)])),v=oe(e),j=[],b=[];for(const l of h){let S=null;try{i(l.path)}catch(B){S=L(B)}const I=!c.existsSync(l.path);!I&&S===null&&(S="Run bundle still exists after removal completed"),I&&j.push({reasons:l.reasons,runId:l.runId}),S!==null&&b.push({...l,error:S,removed:I})}const D=new Set(j.map(l=>l.runId)),E=new Set(p.filter(l=>!D.has(l.runId)).flatMap(l=>y.get(l.runId)??[])),T=ae(v,E,o),_=new Date().toISOString();return Ve({currentRunId:r,decidedAt:_,garbageCollection:T,policy:{maxRuns:a,maxAgeDays:f,preserveSuites:[...m].sort()},protectedRunIds:[...d].sort(),removed:j,removalErrors:b})}function Ee({resultsDir:e,tempDir:n,sensitiveValues:t}){let s=0,r=0,o=0,i=0,a=0;const f=u.join(e,Q,"sha256");c.mkdirSync(f,{recursive:!0});for(const m of W){const p=u.join(e,m);if(c.existsSync(p))for(const d of re(p)){const R=u.relative(p,d),g=c.readFileSync(d),h=De(g,d,t);h.scanned&&(o+=1),h.replacements>0&&(i+=1,a+=h.replacements);const y=C(h.buffer),v=u.join(f,y);c.existsSync(v)?r+=h.buffer.length:(je(v,h.buffer),c.chmodSync(v,292),s+=h.buffer.length);const j=u.join(n,"files",m,R);c.mkdirSync(u.dirname(j),{recursive:!0});try{c.linkSync(v,j)}catch{c.copyFileSync(v,j)}}}return{objectBytesWritten:s,deduplicatedBytes:r,redaction:{policyVersion:1,status:"applied",textualFilesScanned:o,filesRedacted:i,replacements:a,sensitiveEnvironmentKeys:t.map(m=>m.key).sort(),valuesStored:!1}}}function Be(e,n,t){const s=new Map(W.map(o=>[`.testkit/results/${o}/`,`${x(n,`files/${o}`)}/`])),r=K(e,s);return ke(Ae(r),t)}function Ae(e){if(!$(e))return e;const n={...e};if($(n.product)){const t={...n.product};delete t.directory,n.product=t}if($(n.git)){const t={...n.git};delete t.remoteUrl,delete t.repoRoot,delete t.worktreePath,n.git=t}return delete n.host,n}function K(e,n){if(Array.isArray(e))return e.map(s=>K(s,n));if($(e))return Object.fromEntries(Object.entries(e).map(([s,r])=>[s,K(r,n)]));if(typeof e!="string")return e;let t=e;for(const[s,r]of n)t.startsWith(s)&&(t=`${r}${t.slice(s.length)}`);return t}function ke(e,n){let t=0;function s(r){if(Array.isArray(r))return r.map(s);if($(r))return Object.fromEntries(Object.entries(r).map(([i,a])=>[i,s(a)]));if(typeof r!="string")return r;let o=Y(r);o!==r&&(t+=1);for(const i of n){if(!o.includes(i.value))continue;const a=o.split(i.value);t+=a.length-1,o=a.join("<redacted>")}return o}return{artifact:s(e),replacements:t}}function $e(e,n,t,s){const r=z(n.run.fileNames),o=[...new Set([...we,...r])].map(i=>Fe(e,i)).filter(i=>i!==null);return{schemaVersion:pe,runId:t,ci:be(s.env??process.env),source:{git:n.git??null,files:o},runtime:{controller:{testkitVersion:A(n.run.testkitVersion),node:process.versions.node,platform:process.platform,architecture:process.arch},runtimeStats:ye(n.run.runtimeStats)},scenario:{types:z(n.run.types),suiteSelectors:z(n.run.suiteSelectors),fileNames:r,serviceFilter:A(n.run.serviceFilter),planningDigest:C(Buffer.from(q(n.planning??null))),declaredEnvironmentKeys:[...new Set(s.declaredEnvironmentKeys??[])].sort()},redaction:s.redaction}}function xe(e){return re(e).map(n=>({path:X(u.relative(e,n)),bytes:c.statSync(n).size,sha256:F(n)})).sort((n,t)=>n.path.localeCompare(t.path))}function V(e,n){const t=u.join(e,"manifest.json"),s=J(P(t),t,n);le(P(u.join(e,"run.json")),u.join(e,"run.json"),n);const r=[];for(const i of s.files){let a;try{a=ee(e,i.path)}catch(m){r.push(k(m));continue}if(!c.existsSync(a)){r.push(`missing ${i.path}`);continue}const f=c.lstatSync(a);if(f.isSymbolicLink()){r.push(`symbolic link not allowed ${i.path}`);continue}if(!f.isFile()){r.push(`not a regular file ${i.path}`);continue}f.size!==i.bytes&&r.push(`size mismatch ${i.path}`),F(a)!==i.sha256&&r.push(`checksum mismatch ${i.path}`)}C(Buffer.from(q(s.files)))!==s.digest&&r.push("manifest digest mismatch"),s.storage.fileCount!==s.files.length&&r.push("manifest file count mismatch");for(const i of["run.json","provenance.json"])s.files.some(a=>a.path===i)||r.push(`manifest must include ${i}`);if(r.length===0){const i=ee(e,"provenance.json");ue(P(i),i,n)}return{runId:n,checkedFiles:s.files.length,storage:s.storage,ok:r.length===0,errors:r,manifestDigest:s.digest}}function Pe(e,n,t){const s={schemaVersion:1,runId:n,recordedAt:new Date().toISOString(),...t};return N(u.join(U(e),"publications",`${n}.json`),s),s}function Oe(e,n){return n.size===0?!1:Z(e.services).some(t=>Z(t.suites).some(s=>{const r=A(t.name)??"",o=A(s.name)??"",i=A(s.testType)??"";return[o,`${i}:${o}`,`${r}/${i}:${o}`].some(f=>n.has(f))}))}function Ce(e){const n=new Set;for(const t of listRunBundles(e)){const s=loadRunBundleManifest(e,t.runId);for(const r of s.files)r.path.startsWith("files/")&&n.add(r.sha256)}return ae(oe(e),n,ce)}function oe(e){const n=u.join(U(e),Q,"sha256");return c.existsSync(n)?c.readdirSync(n,{withFileTypes:!0}).filter(t=>t.isFile()).map(t=>{const s=u.join(n,t.name);return{bytes:c.statSync(s).size,digest:t.name,path:s}}):[]}function ae(e,n,t){const s=[];let r=0,o=0;for(const i of e){if(n.has(i.digest))continue;let a=null;try{t(i.path)}catch(m){a=L(m)}!c.existsSync(i.path)?(r+=1,o+=i.bytes):a===null&&(a="Object still exists after removal completed"),a!==null&&s.push({error:a,path:i.path})}return{failures:s,reclaimedBytes:o,removedObjects:r,status:s.length===0?"complete":"failed"}}function Ne(e){c.rmSync(e,{recursive:!0,force:!0})}function ce(e){c.rmSync(e,{force:!0})}function Ve({currentRunId:e,decidedAt:n,garbageCollection:t,policy:s,protectedRunIds:r,removed:o,removalErrors:i}){const a=[...i.map(f=>`remove ${f.runId}: ${f.error}`),...t.failures.map(f=>`garbage collect ${f.path}: ${f.error}`)];return{currentRunId:e,decidedAt:n,garbageCollection:t,policy:s,protectedRunIds:r,removed:o,removalErrors:i,schemaVersion:he,status:a.length===0?"complete":"failed",...a.length===0?{}:{error:a.join("; ")}}}function De(e,n,t){if(!_e(e,n)){for(const i of t)if(e.includes(Buffer.from(i.value)))throw new Error(`Refusing to archive binary file containing sensitive environment value: ${n}`);return{buffer:e,scanned:!1,replacements:0}}const s=e.toString("utf8");let r=Y(s),o=r===s?0:1;for(const i of t){if(!r.includes(i.value))continue;const a=r.split(i.value);o+=a.length-1,r=a.join("<redacted>")}return{buffer:Buffer.from(r),scanned:!0,replacements:o}}function Te(e){const n=[];for(const[t,s]of Object.entries(e))Ie.test(t)&&typeof s=="string"&&s.length>=8&&n.push({key:t,value:s});return n}function _e(e,n){if(e.includes(0))return!1;const t=u.extname(n).toLowerCase();return![".zip",".gz",".png",".jpg",".jpeg",".webm",".pdf"].includes(t)}function Fe(e,n){const t=u.resolve(e,n);return!t.startsWith(`${u.resolve(e)}${u.sep}`)||!c.existsSync(t)||!c.statSync(t).isFile()?null:{path:X(u.relative(e,t)),bytes:c.statSync(t).size,sha256:F(t)}}function ze(e){const n=[];for(const t of[...W.map(s=>u.join(e,s)),u.join(e,"live.json")])try{c.rmSync(t,{recursive:!0,force:!0})}catch(s){n.push(`${t}: ${L(s)}`)}return n.length===0?{status:"complete"}:{error:n.join("; "),status:"failed"}}function L(e){try{return k(e)}catch{return"Unknown post-seal archive operation failure"}}
1
+ import{recordWork as H}from"../kernel/work-metrics.js";import{isUnknownRecord as A,optionalString as k}from"../kernel/unknown-values.js";import{posixPath as J}from"../process/files/paths.js";import{readRunProvenance as ue}from"./read-provenance.js";import{parseRunBundleArtifact as le,parseRunBundleManifest as Y,readRunBundleArtifact as fe}from"./read-bundle.js";import{readRunDocument as de}from"./read-run.js";import me from"crypto";import c from"fs";import u from"path";import{redactSensitiveText as G}from"../kernel/redaction.js";import{BUNDLE_SCHEMA_VERSION as Q,OBJECTS_DIRNAME as X,PROVENANCE_SCHEMA_VERSION as pe,RETENTION_DECISION_SCHEMA_VERSION as he,RUNS_DIRNAME as ge}from"./contracts.js";import{bundleRelativePath as $,errorMessage as x,hashFile as F,normalizeRunId as I,objectRecords as Z,optionalStringArray as z,optionalUnknownArray as ye,readJsonFile as P,requirePublicationPathOutsideResults as M,requireRunId as ve,runIncludesFile as Se,resolveManifestEntryPath as ee,resolveRunBundleDirectory as O,resolveRunsRoot as te,resultsRoot as U,sanitizeSegment as Re,sha256 as C,stableJson as q,treeBytes as ne,walkFiles as re,writeBufferAtomic as je,writeJson as N}from"./storage.js";import{resolveCiProvenance as be}from"./ci-provenance.js";const W=["artifacts","logs","setup"],we=/(?:PASSWORD|TOKEN|SECRET|PRIVATE_KEY|AUTH|COOKIE|SESSION|API_KEY)/iu,Ie=["package.json","package-lock.json","npm-shrinkwrap.json","pnpm-lock.yaml","yarn.lock","testkit.config.ts","testkit.config.mts","testkit.config.js","testkit.config.mjs"];export function sealRunBundle(e,n,t={},s={}){const r=n,o=ve(r),i=te(e,!0),a=u.dirname(i),f=u.join(i,o),m=s.cleanupActiveRunResults??ze,p=loadVerifiedRunBundleArtifactIfExists(e,o);if(p!==null)return{...p,archive:{...p.archive,cleanup:m(a)}};const d=u.join(i,`.${o}.${String(process.pid)}.${String(Date.now())}.${me.randomBytes(4).toString("hex")}.tmp`);c.mkdirSync(d,{recursive:!0});try{const R=Te(t.env??process.env),g=Ee({productDir:e,resultsDir:a,runId:o,tempDir:d,sensitiveValues:R}),h=Be(r,o,R),y=de(h.artifact,"run artifact");g.redaction.structuredReplacements=h.replacements,g.redaction.replacements+=h.replacements;const v=xe(e,y,o,{...t,redaction:g.redaction}),j={schemaVersion:Q,runId:o,status:"complete",canonicalRunPath:$(o,"run.json"),manifestPath:$(o,"manifest.json"),provenancePath:$(o,"provenance.json"),integrity:"sha256",publication:{configured:!!t.archiveConfig?.publication,status:t.archiveConfig?.publication?"pending":"not_configured"},redaction:g.redaction};y.archive=j,N(u.join(d,"run.json"),y),N(u.join(d,"provenance.json"),v);const b=$e(d),D=C(Buffer.from(q(b))),E={schemaVersion:Q,runId:o,createdAt:new Date().toISOString(),algorithm:"sha256",files:b,storage:{fileCount:b.length,logicalBytes:b.reduce((T,_)=>T+_.bytes,0),objectBytesWritten:g.objectBytesWritten,deduplicatedBytes:g.deduplicatedBytes},redaction:g.redaction,digest:D};return N(u.join(d,"manifest.json"),E),c.renameSync(d,f),{...y,archive:{...j,cleanup:m(a),manifestDigest:E.digest,storage:E.storage}}}catch(R){try{c.rmSync(d,{recursive:!0,force:!0})}catch{}try{Ce(e)}catch{}throw R}}export function loadVerifiedRunBundleArtifactIfExists(e,n){const t=I(n),s=O(e,t);if(!c.existsSync(s))return null;const r=verifyRunBundle(e,t);if(!r.ok)throw new Error(`Immutable run bundle ${t} already exists but failed integrity verification: ${r.errors.join("; ")}`);return loadRunBundleArtifact(e,t)}export function loadRunBundleArtifact(e,n){return se(e,n).artifact}function se(e,n){const t=I(n),s=fe(e,t),r=loadRunBundleManifest(e,t);return{manifest:r,artifact:{...s,archive:{...s.archive,manifestDigest:r.digest,storage:r.storage}}}}export function loadRunBundleManifest(e,n){const t=I(n),s=O(e,t),r=u.join(s,"manifest.json");if(!c.existsSync(r))throw new Error(`No immutable Testkit run manifest found for ${t}`);return Y(P(r),r,t)}export function listRunBundles(e,n={}){return ie(e,n).map(({artifact:t,manifest:s,...r})=>r)}function ie(e,n={}){const t=te(e,!1);if(!c.existsSync(t))return[];const s=c.readdirSync(t,{withFileTypes:!0});return H("artifacts.runDirectoriesRead",1),H("artifacts.runEntriesVisited",s.length),s.filter(o=>o.isDirectory()&&!o.name.startsWith(".")).map(o=>{try{const{artifact:i,manifest:a}=se(e,o.name);return n.file&&!Se(i,n.file)?null:{artifact:i,manifest:a,runId:o.name,status:i.run.status,startedAt:i.run.startedAt,finishedAt:i.run.finishedAt??null,manifestDigest:i.archive.manifestDigest??null,canonicalRunPath:i.archive.canonicalRunPath}}catch{return null}}).filter(o=>o!==null).sort((o,i)=>(i.finishedAt??i.startedAt??"").localeCompare(o.finishedAt??o.startedAt??""))}export function verifyRunBundle(e,n){const t=I(n);try{return V(O(e,t),t)}catch(s){return{ok:!1,runId:t,errors:[x(s)],checkedFiles:0}}}export function publishRunBundle(e,n,t,s={}){const r=I(n),o=s.writePublicationReceipt??Pe;if(!t)return o(e,r,{status:"not_configured",required:!1,target:null,bytesUploaded:0});const i=u.resolve(e,t.directory);try{M(e,i)}catch(d){return o(e,r,{status:"failed",required:t.required,target:i,error:x(d),bytesUploaded:0})}const a=Re(u.basename(e)),f=u.join(i,a),m=u.join(f,r),p=u.join(f,`.${r}.${String(process.pid)}.${String(Date.now())}.tmp`);try{const d=O(e,r);if(c.mkdirSync(f,{recursive:!0}),M(e,f),c.existsSync(m)){const h=c.lstatSync(m);if(h.isSymbolicLink()||!h.isDirectory())throw new Error("Publication target must be a regular bundle directory");M(e,m);const y=V(d,r);if(!y.ok)throw new Error(`Source run bundle failed integrity verification: ${y.errors.join("; ")}`);const v=V(m,r);if(!v.ok)throw new Error(`Publication target failed integrity verification: ${v.errors.join("; ")}`);if(y.manifestDigest!==v.manifestDigest)throw new Error("Publication target already exists with a different manifest digest");return o(e,r,{status:"complete",required:t.required,target:m,manifestDigest:y.manifestDigest,reused:!0,bundleBytes:ne(d),bytesUploaded:0})}const R=ne(d);c.cpSync(d,p,{recursive:!0,preserveTimestamps:!0});const g=V(p,r);if(!g.ok)throw new Error(g.errors.join("; "));return c.renameSync(p,m),o(e,r,{status:"complete",required:t.required,target:m,manifestDigest:g.manifestDigest,reused:!1,bundleBytes:R,bytesUploaded:R})}catch(d){return c.rmSync(p,{recursive:!0,force:!0}),o(e,r,{status:"failed",required:t.required,target:m,error:x(d),bytesUploaded:0})}}export function applyRunBundleRetention(e,n,t={},s={}){const r=I(n),o=s.removeObject??ce,i=s.removeRunBundle??Ne,a=t.maxRuns??null,f=t.maxAgeDays??null,m=new Set(t.preserveSuites||[]),p=ie(e),d=new Set([r]);for(const l of p)Oe(l.artifact,m)&&d.add(l.runId);const R=new Set(a?p.slice(0,a).map(l=>l.runId):p.map(l=>l.runId)),g=f==null?null:Date.now()-f*24*60*60*1e3,h=[];for(const l of p){if(d.has(l.runId))continue;const S=g!=null&&Date.parse(l.finishedAt||l.startedAt||"")<g,w=!R.has(l.runId);if(!S&&!w)continue;const B=[];S&&B.push("max_age"),w&&B.push("max_runs"),h.push({path:u.join(U(e),ge,l.runId),reasons:B,runId:l.runId})}const y=new Map(p.map(l=>[l.runId,l.manifest.files.filter(S=>S.path.startsWith("files/")).map(S=>S.sha256)])),v=oe(e),j=[],b=[];for(const l of h){let S=null;try{i(l.path)}catch(B){S=L(B)}const w=!c.existsSync(l.path);!w&&S===null&&(S="Run bundle still exists after removal completed"),w&&j.push({reasons:l.reasons,runId:l.runId}),S!==null&&b.push({...l,error:S,removed:w})}const D=new Set(j.map(l=>l.runId)),E=new Set(p.filter(l=>!D.has(l.runId)).flatMap(l=>y.get(l.runId)??[])),T=ae(v,E,o),_=new Date().toISOString();return Ve({currentRunId:r,decidedAt:_,garbageCollection:T,policy:{maxRuns:a,maxAgeDays:f,preserveSuites:[...m].sort()},protectedRunIds:[...d].sort(),removed:j,removalErrors:b})}function Ee({resultsDir:e,tempDir:n,sensitiveValues:t}){let s=0,r=0,o=0,i=0,a=0;const f=u.join(e,X,"sha256");c.mkdirSync(f,{recursive:!0});for(const m of W){const p=u.join(e,m);if(c.existsSync(p))for(const d of re(p)){const R=u.relative(p,d),g=c.readFileSync(d),h=De(g,d,t);h.scanned&&(o+=1),h.replacements>0&&(i+=1,a+=h.replacements);const y=C(h.buffer),v=u.join(f,y);c.existsSync(v)?r+=h.buffer.length:(je(v,h.buffer),c.chmodSync(v,292),s+=h.buffer.length);const j=u.join(n,"files",m,R);c.mkdirSync(u.dirname(j),{recursive:!0});try{c.linkSync(v,j)}catch{c.copyFileSync(v,j)}}}return{objectBytesWritten:s,deduplicatedBytes:r,redaction:{policyVersion:1,status:"applied",textualFilesScanned:o,filesRedacted:i,replacements:a,sensitiveEnvironmentKeys:t.map(m=>m.key).sort(),valuesStored:!1}}}function Be(e,n,t){const s=new Map(W.map(o=>[`.testkit/results/${o}/`,`${$(n,`files/${o}`)}/`])),r=K(e,s);return ke(Ae(r),t)}function Ae(e){if(!A(e))return e;const n={...e};if(A(n.product)){const t={...n.product};delete t.directory,n.product=t}if(A(n.git)){const t={...n.git};delete t.remoteUrl,delete t.repoRoot,delete t.worktreePath,n.git=t}return delete n.host,n}function K(e,n){if(Array.isArray(e))return e.map(s=>K(s,n));if(A(e))return Object.fromEntries(Object.entries(e).map(([s,r])=>[s,K(r,n)]));if(typeof e!="string")return e;let t=e;for(const[s,r]of n)t.startsWith(s)&&(t=`${r}${t.slice(s.length)}`);return t}function ke(e,n){let t=0;function s(r){if(Array.isArray(r))return r.map(s);if(A(r))return Object.fromEntries(Object.entries(r).map(([i,a])=>[i,s(a)]));if(typeof r!="string")return r;let o=G(r);o!==r&&(t+=1);for(const i of n){if(!o.includes(i.value))continue;const a=o.split(i.value);t+=a.length-1,o=a.join("<redacted>")}return o}return{artifact:s(e),replacements:t}}function xe(e,n,t,s){const r=z(n.run.fileNames),o=[...new Set([...Ie,...r])].map(i=>Fe(e,i)).filter(i=>i!==null);return{schemaVersion:pe,runId:t,ci:be(s.env??process.env),source:{git:n.git??null,files:o},runtime:{controller:{testkitVersion:k(n.run.testkitVersion),node:process.versions.node,platform:process.platform,architecture:process.arch},runtimeStats:ye(n.run.runtimeStats)},scenario:{types:z(n.run.types),suiteSelectors:z(n.run.suiteSelectors),fileNames:r,serviceFilter:k(n.run.serviceFilter),planningDigest:C(Buffer.from(q(n.planning??null))),declaredEnvironmentKeys:[...new Set(s.declaredEnvironmentKeys??[])].sort()},redaction:s.redaction}}function $e(e){return re(e).map(n=>({path:J(u.relative(e,n)),bytes:c.statSync(n).size,sha256:F(n)})).sort((n,t)=>n.path.localeCompare(t.path))}function V(e,n){const t=u.join(e,"manifest.json"),s=Y(P(t),t,n);le(P(u.join(e,"run.json")),u.join(e,"run.json"),n);const r=[];for(const i of s.files){let a;try{a=ee(e,i.path)}catch(m){r.push(x(m));continue}if(!c.existsSync(a)){r.push(`missing ${i.path}`);continue}const f=c.lstatSync(a);if(f.isSymbolicLink()){r.push(`symbolic link not allowed ${i.path}`);continue}if(!f.isFile()){r.push(`not a regular file ${i.path}`);continue}f.size!==i.bytes&&r.push(`size mismatch ${i.path}`),F(a)!==i.sha256&&r.push(`checksum mismatch ${i.path}`)}C(Buffer.from(q(s.files)))!==s.digest&&r.push("manifest digest mismatch"),s.storage.fileCount!==s.files.length&&r.push("manifest file count mismatch");for(const i of["run.json","provenance.json"])s.files.some(a=>a.path===i)||r.push(`manifest must include ${i}`);if(r.length===0){const i=ee(e,"provenance.json");ue(P(i),i,n)}return{runId:n,checkedFiles:s.files.length,storage:s.storage,ok:r.length===0,errors:r,manifestDigest:s.digest}}function Pe(e,n,t){const s={schemaVersion:1,runId:n,recordedAt:new Date().toISOString(),...t};return N(u.join(U(e),"publications",`${n}.json`),s),s}function Oe(e,n){return n.size===0?!1:Z(e.services).some(t=>Z(t.suites).some(s=>{const r=k(t.name)??"",o=k(s.name)??"",i=k(s.testType)??"";return[o,`${i}:${o}`,`${r}/${i}:${o}`].some(f=>n.has(f))}))}function Ce(e){const n=new Set;for(const t of listRunBundles(e)){const s=loadRunBundleManifest(e,t.runId);for(const r of s.files)r.path.startsWith("files/")&&n.add(r.sha256)}return ae(oe(e),n,ce)}function oe(e){const n=u.join(U(e),X,"sha256");return c.existsSync(n)?c.readdirSync(n,{withFileTypes:!0}).filter(t=>t.isFile()).map(t=>{const s=u.join(n,t.name);return{bytes:c.statSync(s).size,digest:t.name,path:s}}):[]}function ae(e,n,t){const s=[];let r=0,o=0;for(const i of e){if(n.has(i.digest))continue;let a=null;try{t(i.path)}catch(m){a=L(m)}!c.existsSync(i.path)?(r+=1,o+=i.bytes):a===null&&(a="Object still exists after removal completed"),a!==null&&s.push({error:a,path:i.path})}return{failures:s,reclaimedBytes:o,removedObjects:r,status:s.length===0?"complete":"failed"}}function Ne(e){c.rmSync(e,{recursive:!0,force:!0})}function ce(e){c.rmSync(e,{force:!0})}function Ve({currentRunId:e,decidedAt:n,garbageCollection:t,policy:s,protectedRunIds:r,removed:o,removalErrors:i}){const a=[...i.map(f=>`remove ${f.runId}: ${f.error}`),...t.failures.map(f=>`garbage collect ${f.path}: ${f.error}`)];return{currentRunId:e,decidedAt:n,garbageCollection:t,policy:s,protectedRunIds:r,removed:o,removalErrors:i,schemaVersion:he,status:a.length===0?"complete":"failed",...a.length===0?{}:{error:a.join("; ")}}}function De(e,n,t){if(!_e(e,n)){for(const i of t)if(e.includes(Buffer.from(i.value)))throw new Error(`Refusing to archive binary file containing sensitive environment value: ${n}`);return{buffer:e,scanned:!1,replacements:0}}const s=e.toString("utf8");let r=G(s),o=r===s?0:1;for(const i of t){if(!r.includes(i.value))continue;const a=r.split(i.value);o+=a.length-1,r=a.join("<redacted>")}return{buffer:Buffer.from(r),scanned:!0,replacements:o}}function Te(e){const n=[];for(const[t,s]of Object.entries(e))we.test(t)&&typeof s=="string"&&s.length>=8&&n.push({key:t,value:s});return n}function _e(e,n){if(e.includes(0))return!1;const t=u.extname(n).toLowerCase();return![".zip",".gz",".png",".jpg",".jpeg",".webm",".pdf"].includes(t)}function Fe(e,n){const t=u.resolve(e,n);return!t.startsWith(`${u.resolve(e)}${u.sep}`)||!c.existsSync(t)||!c.statSync(t).isFile()?null:{path:J(u.relative(e,t)),bytes:c.statSync(t).size,sha256:F(t)}}function ze(e){const n=[];for(const t of[...W.map(s=>u.join(e,s)),u.join(e,"live.json")])try{c.rmSync(t,{recursive:!0,force:!0})}catch(s){n.push(`${t}: ${L(s)}`)}return n.length===0?{status:"complete"}:{error:n.join("; "),status:"failed"}}function L(e){try{return x(e)}catch{return"Unknown post-seal archive operation failure"}}
@@ -0,0 +1,22 @@
1
+ import type { ReportingEnvironmentService } from "./reporting-types.js";
2
+ export interface EnvironmentServiceAttempt {
3
+ baseUrl: string;
4
+ outcome: string;
5
+ port: number;
6
+ readyUrl: null | string;
7
+ }
8
+ export interface EnvironmentServiceEntry {
9
+ amendLastOutcome(outcome: string): void;
10
+ recordAttempt(attempt: Readonly<EnvironmentServiceAttempt>): void;
11
+ }
12
+ export interface RunEnvironmentRecord {
13
+ listServices(): ReportingEnvironmentService[];
14
+ service(identity: Readonly<{
15
+ runtimeLabel: string;
16
+ serviceName: string;
17
+ }>): EnvironmentServiceEntry;
18
+ }
19
+ export interface RunEnvironmentRecordOptions {
20
+ onChange?: () => void;
21
+ }
22
+ export declare function createRunEnvironmentRecord(options?: Readonly<RunEnvironmentRecordOptions>): RunEnvironmentRecord;
@@ -0,0 +1 @@
1
+ import{redactUrlSecrets as o}from"../process/url-redaction.js";export function createRunEnvironmentRecord(m={}){const c=[];return{service({runtimeLabel:e,serviceName:t}){let r=null;return{recordAttempt({baseUrl:n,outcome:a,port:s,readyUrl:l}){const u={baseUrl:o(n),readyUrl:l===null?null:o(l)};r===null&&(r={serviceName:t,runtimeLabel:e,...u,port:s,attempts:[]},c.push(r)),Object.assign(r,u,{port:s}),r.attempts.push({port:s,outcome:a}),m.onChange?.()},amendLastOutcome(n){const a=r?.attempts.at(-1);a&&(a.outcome=n,m.onChange?.())}}},listServices(){return c.map(e=>({...e,attempts:e.attempts.map(t=>({...t}))})).sort((e,t)=>e.runtimeLabel.localeCompare(t.runtimeLabel)||e.serviceName.localeCompare(t.serviceName))}}}
@@ -2,7 +2,7 @@ export { normalizeArtifactArchiveConfig, type NormalizedArtifactArchiveConfig, t
2
2
  export { finalizeLiveRunArtifact, loadTimings, persistTaskArtifacts, persistTaskFileArtifacts, persistTaskOutputArtifacts, resetResultArtifacts, saveTimings, writeLiveRunArtifact, writeRunArtifact, type ArtifactTask, type PersistedArtifactReference, type WriteRunArtifactOptions, } from "./artifacts.js";
3
3
  export { applyRunBundleRetention, listRunBundles, loadRunBundleArtifact, loadRunBundleManifest, loadVerifiedRunBundleArtifactIfExists, publishRunBundle, sealRunBundle, verifyRunBundle, type RunBundleVerification, } from "./bundles.js";
4
4
  export { collectArtifactEntries, loadCurrentRunArtifact, loadLatestRunArtifact, loadRunArtifact, type CollectedArtifactEntry, type RunArtifactFileSubject, type RunArtifactPayload, } from "./reading.js";
5
- export { DEFAULT_REPORTED_ARCHIVE_CONFIG, type BuildLiveRunArtifactOptions, type BuildRunArtifactOptions, type ReportedRunArtifact, type ReportedRunService, type ReportingArtifactReference, type ReportingDiagnostic, type ReportingExecution, type ReportingFailureDetail, type ReportingFailureLocation, type ReportingFailureRequest, type ReportingFailureResponse, type ReportingFileSummary, type ReportingMetadata, type ReportingOperationTiming, type ReportingOperationTimingInput, type ReportingRecord, type ReportingRuntimeBatch, type ReportingRuntimeReset, type ReportingServiceLogReference, type ReportingServiceSummary, type ReportingSetupLogReference, type ReportingSetupOperation, type ReportingSuiteSelector, type ReportingSuiteSummary, type RunArtifactDocument, type RunArtifactFile, type RunArtifactRun, type RunArtifactService, type RunArtifactSuite, type RunArtifactSummary, type RunServiceResult, } from "./reporting-types.js";
5
+ export { DEFAULT_REPORTED_ARCHIVE_CONFIG, type BuildLiveRunArtifactOptions, type BuildRunArtifactOptions, type ReportedRunArtifact, type ReportedRunService, type ReportingArtifactReference, type ReportingDiagnostic, type ReportingEnvironmentService, type ReportingExecution, type ReportingFailureDetail, type ReportingFailureLocation, type ReportingFailureRequest, type ReportingFailureResponse, type ReportingFileSummary, type ReportingMetadata, type ReportingOperationTiming, type ReportingOperationTimingInput, type ReportingRecord, type ReportingRuntimeBatch, type ReportingRuntimeReset, type ReportingServiceLogReference, type ReportingServiceSummary, type ReportingSetupLogReference, type ReportingSetupOperation, type ReportingSuiteSelector, type ReportingSuiteSummary, type RunArtifactDocument, type RunArtifactFile, type RunArtifactRun, type RunArtifactService, type RunArtifactSuite, type RunArtifactSummary, type RunServiceResult, } from "./reporting-types.js";
6
6
  export { copyRegularFileSecurely, MAX_PERSISTED_FILE_ARTIFACT_BYTES, type SecureFileCopyOptions, type SecureFileCopyResult, } from "./secure-file-copy.js";
7
7
  export { collectTestkitStorageInventory } from "./storage-inventory.js";
8
8
  export { type Timings, type TimingUpdate } from "./timing.js";
@@ -1,2 +1,2 @@
1
- import p from"fs";import i from"path";const S="logs",N="setup";export function createRunLogRegistry(r){const m=new Map,s=new Map,n=new Set;function L(e){const t=i.join(r,e.path);p.mkdirSync(i.dirname(t),{recursive:!0});const a=p.createWriteStream(t,{fd:p.openSync(t,"a"),flags:"a"});return n.add(a),{...e,write(o){a.write(o)}}}return{ensureServiceLogRecord(e){const t=`${e.runtimeLabel||e.name}:${e.name}`,a=m.get(t);if(a)return a;const o=`${l(e.runtimeLabel||e.name)}__${l(e.name)}.log`,c=i.join(".testkit","results",S,o),u=L({serviceName:e.name,runtimeLabel:e.runtimeLabel||e.name,path:b(c)});return m.set(t,u),u},ensureSetupLogRecord(e,t){const a=`${e.runtimeLabel||e.name}:${e.name}:${t}`,o=s.get(a);if(o)return o;const c=`${l(e.runtimeLabel||e.name)}__${l(e.name)}__${l(t)}.log`,u=i.join(".testkit","results",N,c),v=L({serviceName:e.name,runtimeLabel:e.runtimeLabel||e.name,stage:t,path:b(u)});return s.set(a,v),v},append(e,t,a){!e||typeof a!="string"||e.write(`${new Date().toISOString()} [${t}] ${a}
2
- `)},listServiceLogs(){return[...m.values()].map(e=>({serviceName:e.serviceName,runtimeLabel:e.runtimeLabel,path:e.path})).sort((e,t)=>e.serviceName.localeCompare(t.serviceName)||e.runtimeLabel.localeCompare(t.runtimeLabel))},listSetupLogs(){return[...s.values()].map(e=>({serviceName:e.serviceName,runtimeLabel:e.runtimeLabel,stage:e.stage,path:e.path})).sort((e,t)=>e.serviceName.localeCompare(t.serviceName)||e.runtimeLabel.localeCompare(t.runtimeLabel)||e.stage.localeCompare(t.stage))},async closeAll(){await Promise.all([...n].map(e=>$(e)))}}}function l(r){return r.trim().toLowerCase().replace(/[^a-z0-9._-]+/g,"-").replace(/^-+|-+$/g,"")||"log"}function b(r){return r.split(i.sep).join("/")}function $(r){return!r||r.destroyed||r.closed?Promise.resolve():new Promise(m=>{let s=!1;const n=()=>{s||(s=!0,r.off("close",n),r.off("error",n),m())};r.once("close",n),r.once("error",n),r.end()})}
1
+ import p from"fs";import u from"path";import{posixPath as b}from"../process/files/paths.js";const S="logs",N="setup";export function createRunLogRegistry(r){const m=new Map,s=new Map,n=new Set;function L(e){const t=u.join(r,e.path);p.mkdirSync(u.dirname(t),{recursive:!0});const a=p.createWriteStream(t,{fd:p.openSync(t,"a"),flags:"a"});return n.add(a),{...e,write(o){a.write(o)}}}return{ensureServiceLogRecord(e){const t=`${e.runtimeLabel||e.name}:${e.name}`,a=m.get(t);if(a)return a;const o=`${i(e.runtimeLabel||e.name)}__${i(e.name)}.log`,c=u.join(".testkit","results",S,o),l=L({serviceName:e.name,runtimeLabel:e.runtimeLabel||e.name,path:b(c)});return m.set(t,l),l},ensureSetupLogRecord(e,t){const a=`${e.runtimeLabel||e.name}:${e.name}:${t}`,o=s.get(a);if(o)return o;const c=`${i(e.runtimeLabel||e.name)}__${i(e.name)}__${i(t)}.log`,l=u.join(".testkit","results",N,c),v=L({serviceName:e.name,runtimeLabel:e.runtimeLabel||e.name,stage:t,path:b(l)});return s.set(a,v),v},append(e,t,a){!e||typeof a!="string"||e.write(`${new Date().toISOString()} [${t}] ${a}
2
+ `)},listServiceLogs(){return[...m.values()].map(e=>({serviceName:e.serviceName,runtimeLabel:e.runtimeLabel,path:e.path})).sort((e,t)=>e.serviceName.localeCompare(t.serviceName)||e.runtimeLabel.localeCompare(t.runtimeLabel))},listSetupLogs(){return[...s.values()].map(e=>({serviceName:e.serviceName,runtimeLabel:e.runtimeLabel,stage:e.stage,path:e.path})).sort((e,t)=>e.serviceName.localeCompare(t.serviceName)||e.runtimeLabel.localeCompare(t.runtimeLabel)||e.stage.localeCompare(t.stage))},async closeAll(){await Promise.all([...n].map(e=>$(e)))}}}function i(r){return r.trim().toLowerCase().replace(/[^a-z0-9._-]+/g,"-").replace(/^-+|-+$/g,"")||"log"}function $(r){return!r||r.destroyed||r.closed?Promise.resolve():new Promise(m=>{let s=!1;const n=()=>{s||(s=!0,r.off("close",n),r.off("error",n),m())};r.once("close",n),r.once("error",n),r.end()})}
@@ -1 +1 @@
1
- import{performance as y}from"node:perf_hooks";import{attachErrorOperation as R}from"../kernel/error-serialization.js";import{formatOperationSummary as b}from"../domain/operation-display.js";const g=String;export function createOperationTimingRegistry({onChange:t=null}={}){const r=[],i=new Map;let f=1;function d(e=[]){if(!t)return;const a=[],u=new Set;for(const s of e)!s||u.has(s.id)||(u.add(s.id),a.push(p(s)));t(a,I())}function l({name:e,kind:a="operation",source:u="testkit",summary:s=null,scope:o=null,parentId:n=null,metadata:m=null}){const c={id:`operation-${g(f++)}`,source:u,name:k(e),kind:a,summary:s,scope:h(o),parentId:n,status:"running",startedAt:new Date().toISOString(),updatedAt:null,finishedAt:null,durationMs:null,error:null,message:null,logRefs:[],metadata:h(m),_startedMonotonicMs:y.now()};return r.push(c),i.set(c.id,c),d([c,...A(c)]),c}function w(e,{summary:a,message:u,metadata:s=null,logRefs:o=null}={}){const n=typeof e=="string"?i.get(e):e;return n?(n.updatedAt=new Date().toISOString(),a!==void 0&&(n.summary=a),u!==void 0&&(n.message=u),s!==null&&(n.metadata={...n.metadata,...h(s)}),o!==null&&(n.logRefs=M(o)),d([n,...A(n)]),p(n)):null}function O(e,{status:a="passed",summary:u,error:s=null,metadata:o=null}={}){const n=typeof e=="string"?i.get(e):e;return n?(n.finishedAt=new Date().toISOString(),n.updatedAt=n.finishedAt,n.durationMs=Math.max(0,Math.round(y.now()-n._startedMonotonicMs)),n.status=a,u!==void 0&&(n.summary=u),s!==null&&(n.error=g(s)),o!==null&&(n.metadata={...n.metadata,...h(o)}),d([n,...A(n)]),p(n)):null}function $(e={}){const a=x(e,f++),u=i.get(a.id);return u?(Object.assign(u,a),d([u,...A(u)]),p(u)):(r.push(a),i.set(a.id,a),d([a,...A(a)]),p(a))}function I(){return r.map(e=>p(e))}return{start:l,update:w,finish:O,record:$,listOperations:I,countOperations(){return r.length}};function A(e){if(!e.parentId)return[];const a=[],u=new Date().toISOString();let s=i.get(e.parentId);const o=new Set([e.id]);for(;s&&s.status==="running"&&!o.has(s.id);){o.add(s.id),s.updatedAt=u;const n=e.status||"unknown",m=n==="running"?"activeChild":"lastChild";s.message=z(e),s.metadata={...s.metadata,[`${m}OperationId`]:e.id,[`${m}OperationName`]:e.name,[`${m}OperationKind`]:e.kind,[`${m}OperationStatus`]:n,[`${m}OperationSummary`]:e.summary||""},a.push(s),s=s.parentId?i.get(s.parentId):void 0}return a}}export async function timeOperation(t,r,i){if(!t)return i();const f=t.start(r);try{const d=await i(f);return t.finish(f,{status:"passed"}),d}catch(d){const l=t.finish(f,{status:"failed",error:N(d)});throw R(d,l),d}}function x(t,r){const i=S(t.startedAt)||new Date().toISOString(),f=t.status||"passed",d=f==="running"?null:S(t.finishedAt)||i,l=t.durationMs,w=Number(l),O=l!=null&&Number.isFinite(w)?Math.max(0,Math.round(w)):d?Math.max(0,Date.parse(d)-Date.parse(i)):null;return{id:t.id||`operation-${g(r)}`,source:t.source||"testkit",name:k(t.name),kind:t.kind||"operation",summary:t.summary||null,scope:h(t.scope),parentId:t.parentId||null,status:f,startedAt:i,updatedAt:S(t.updatedAt)||null,finishedAt:d,durationMs:O,error:t.error||null,message:t.message||null,logRefs:M(t.logRefs),metadata:h(t.metadata),_startedMonotonicMs:y.now()-(O||0)}}function p(t){return{id:t.id,source:t.source,name:t.name,kind:t.kind,summary:t.summary,scope:t.scope,parentId:t.parentId,status:t.status,startedAt:t.startedAt,updatedAt:t.updatedAt,finishedAt:t.finishedAt,durationMs:t.durationMs,error:t.error,message:t.message,logRefs:M(t.logRefs),metadata:t.metadata}}function k(t){const r=g(t||"").trim();if(!r)throw new Error("operation timing name is required");return r}function h(t){return D(t)?{...t}:{}}function M(t){return Array.isArray(t)?t.map(r=>D(r)?{...r}:r):[]}function z(t){const r=b(t),i=typeof r=="string"?r:"";return i?`${t.name} ${t.status||"unknown"} - ${i}`:`${t.name} ${t.status||"unknown"}`}function S(t){const r=Date.parse(t||"");return Number.isFinite(r)?new Date(r).toISOString():null}function D(t){return t!==null&&typeof t=="object"&&!Array.isArray(t)}function N(t){return(typeof t!="object"||t===null)&&typeof t!="function"?g(t):Reflect.get(t,"message")||g(t)}
1
+ import{performance as M}from"node:perf_hooks";import{attachErrorOperation as R}from"../kernel/error-serialization.js";import{formatOperationSummary as x}from"../domain/operation-display.js";import{isUnknownRecord as k}from"../kernel/unknown-values.js";const g=String;export function createOperationTimingRegistry({onChange:t=null}={}){const r=[],i=new Map;let m=1;function d(e=[]){if(!t)return;const a=[],u=new Set;for(const s of e)!s||u.has(s.id)||(u.add(s.id),a.push(p(s)));t(a,I())}function f({name:e,kind:a="operation",source:u="testkit",summary:s=null,scope:o=null,parentId:n=null,metadata:l=null}){const c={id:`operation-${g(m++)}`,source:u,name:D(e),kind:a,summary:s,scope:h(o),parentId:n,status:"running",startedAt:new Date().toISOString(),updatedAt:null,finishedAt:null,durationMs:null,error:null,message:null,logRefs:[],metadata:h(l),_startedMonotonicMs:M.now()};return r.push(c),i.set(c.id,c),d([c,...w(c)]),c}function A(e,{summary:a,message:u,metadata:s=null,logRefs:o=null}={}){const n=typeof e=="string"?i.get(e):e;return n?(n.updatedAt=new Date().toISOString(),a!==void 0&&(n.summary=a),u!==void 0&&(n.message=u),s!==null&&(n.metadata={...n.metadata,...h(s)}),o!==null&&(n.logRefs=S(o)),d([n,...w(n)]),p(n)):null}function O(e,{status:a="passed",summary:u,error:s=null,metadata:o=null}={}){const n=typeof e=="string"?i.get(e):e;return n?(n.finishedAt=new Date().toISOString(),n.updatedAt=n.finishedAt,n.durationMs=Math.max(0,Math.round(M.now()-n._startedMonotonicMs)),n.status=a,u!==void 0&&(n.summary=u),s!==null&&(n.error=g(s)),o!==null&&(n.metadata={...n.metadata,...h(o)}),d([n,...w(n)]),p(n)):null}function $(e={}){const a=b(e,m++),u=i.get(a.id);return u?(Object.assign(u,a),d([u,...w(u)]),p(u)):(r.push(a),i.set(a.id,a),d([a,...w(a)]),p(a))}function I(){return r.map(e=>p(e))}return{start:f,update:A,finish:O,record:$,listOperations:I,countOperations(){return r.length}};function w(e){if(!e.parentId)return[];const a=[],u=new Date().toISOString();let s=i.get(e.parentId);const o=new Set([e.id]);for(;s&&s.status==="running"&&!o.has(s.id);){o.add(s.id),s.updatedAt=u;const n=e.status||"unknown",l=n==="running"?"activeChild":"lastChild";s.message=z(e),s.metadata={...s.metadata,[`${l}OperationId`]:e.id,[`${l}OperationName`]:e.name,[`${l}OperationKind`]:e.kind,[`${l}OperationStatus`]:n,[`${l}OperationSummary`]:e.summary||""},a.push(s),s=s.parentId?i.get(s.parentId):void 0}return a}}export async function timeOperation(t,r,i){if(!t)return i();const m=t.start(r);try{const d=await i(m);return t.finish(m,{status:"passed"}),d}catch(d){const f=t.finish(m,{status:"failed",error:N(d)});throw R(d,f),d}}function b(t,r){const i=y(t.startedAt)||new Date().toISOString(),m=t.status||"passed",d=m==="running"?null:y(t.finishedAt)||i,f=t.durationMs,A=Number(f),O=f!=null&&Number.isFinite(A)?Math.max(0,Math.round(A)):d?Math.max(0,Date.parse(d)-Date.parse(i)):null;return{id:t.id||`operation-${g(r)}`,source:t.source||"testkit",name:D(t.name),kind:t.kind||"operation",summary:t.summary||null,scope:h(t.scope),parentId:t.parentId||null,status:m,startedAt:i,updatedAt:y(t.updatedAt)||null,finishedAt:d,durationMs:O,error:t.error||null,message:t.message||null,logRefs:S(t.logRefs),metadata:h(t.metadata),_startedMonotonicMs:M.now()-(O||0)}}function p(t){return{id:t.id,source:t.source,name:t.name,kind:t.kind,summary:t.summary,scope:t.scope,parentId:t.parentId,status:t.status,startedAt:t.startedAt,updatedAt:t.updatedAt,finishedAt:t.finishedAt,durationMs:t.durationMs,error:t.error,message:t.message,logRefs:S(t.logRefs),metadata:t.metadata}}function D(t){const r=g(t||"").trim();if(!r)throw new Error("operation timing name is required");return r}function h(t){return k(t)?{...t}:{}}function S(t){return Array.isArray(t)?t.map(r=>k(r)?{...r}:r):[]}function z(t){const r=x(t),i=typeof r=="string"?r:"";return i?`${t.name} ${t.status||"unknown"} - ${i}`:`${t.name} ${t.status||"unknown"}`}function y(t){const r=Date.parse(t||"");return Number.isFinite(r)?new Date(r).toISOString():null}function N(t){return(typeof t!="object"||t===null)&&typeof t!="function"?g(t):Reflect.get(t,"message")||g(t)}