@gfdelarue/testkit 0.4.3 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (276) hide show
  1. package/README.md +109 -22
  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
@@ -6,7 +6,4 @@ export declare function optionalNonEmptyString(value: unknown): string | null;
6
6
  export declare function portableReportPath(value: string): string;
7
7
  export declare function portableRelativePath(rootDirectory: string, candidate: string): string;
8
8
  export declare function requireRecord(value: unknown, label: string): Record<string, unknown>;
9
- export declare function isRecord(value: unknown): value is Record<string, unknown>;
10
- export declare function isPathWithin(rootDirectory: string, candidate: string): boolean;
11
9
  export declare function isMissingFileError(error: unknown): boolean;
12
- export declare function toError(error: unknown): Error;
@@ -1 +1 @@
1
- import e from"node:path";export function nonNegativeInteger(t,r){if(typeof t!="number"||!Number.isSafeInteger(t)||t<0)throw new Error(`${r} must be a non-negative safe integer`);return t}export function optionalNonNegativeInteger(t,r){return t==null?null:nonNegativeInteger(t,r)}export function nonNegativeNumber(t,r){if(typeof t!="number"||!Number.isFinite(t)||t<0)throw new Error(`${r} must be a non-negative finite number`);return t}export function requireNonEmptyString(t,r){const n=optionalNonEmptyString(t);if(n===null)throw new Error(`${r} must be a non-empty string`);return n}export function optionalNonEmptyString(t){return typeof t=="string"&&t.trim().length>0?t.trim():null}export function portableReportPath(t){return t.replaceAll("\\","/")}export function portableRelativePath(t,r){return portableReportPath(e.relative(t,r))}export function requireRecord(t,r){if(!isRecord(t))throw new Error(`${r} must contain an object`);return t}export function isRecord(t){return typeof t=="object"&&t!==null&&!Array.isArray(t)}export function isPathWithin(t,r){const n=e.relative(t,r);return n===""||!n.startsWith("..")&&!e.isAbsolute(n)}export function isMissingFileError(t){return t instanceof Error&&"code"in t&&t.code==="ENOENT"}export function toError(t){return t instanceof Error?t:new Error(String(t))}
1
+ import e from"node:path";import{isUnknownRecord as o}from"../../../kernel/unknown-values.js";export function nonNegativeInteger(n,t){if(typeof n!="number"||!Number.isSafeInteger(n)||n<0)throw new Error(`${t} must be a non-negative safe integer`);return n}export function optionalNonNegativeInteger(n,t){return n==null?null:nonNegativeInteger(n,t)}export function nonNegativeNumber(n,t){if(typeof n!="number"||!Number.isFinite(n)||n<0)throw new Error(`${t} must be a non-negative finite number`);return n}export function requireNonEmptyString(n,t){const r=optionalNonEmptyString(n);if(r===null)throw new Error(`${t} must be a non-empty string`);return r}export function optionalNonEmptyString(n){return typeof n=="string"&&n.trim().length>0?n.trim():null}export function portableReportPath(n){return n.replaceAll("\\","/")}export function portableRelativePath(n,t){return portableReportPath(e.relative(n,t))}export function requireRecord(n,t){if(!o(n))throw new Error(`${t} must contain an object`);return n}export function isMissingFileError(n){return n instanceof Error&&"code"in n&&n.code==="ENOENT"}
@@ -1 +1 @@
1
- import w from"node:fs/promises";import g from"node:path";import{evidenceArtifact as F,evidenceFileAccess as q,readStableJsonFile as tt,snapshotStableEvidenceFile as C,stableEvidenceFromBuffer as et,writeOwnedEvidenceFile as Y}from"../../adapters/evidence-files.js";import{materializePlaywrightFailureDetails as it}from"./failure-details.js";import{isMissingFileError as rt,isPathWithin as $,nonNegativeInteger as E,nonNegativeNumber as H,optionalNonEmptyString as N,optionalNonNegativeInteger as G,portableRelativePath as U,portableReportPath as W,requireNonEmptyString as b,requireRecord as S,toError as nt}from"./evidence-validation.js";import{readNativeReport as at}from"../../adapters/native-report.js";import{attachmentLimitError as st,attachmentIoError as ot,MAX_ATTACHMENT_BYTES as X,MAX_TOTAL_ATTACHMENT_BYTES as ct}from"./attachments.js";const lt=64*1024*1024,J=1e4,M=512*1024*1024,ut=2*1024*1024,V=128,pt=".testkit-playwright-evidence",dt=/(?:^|\n)(?:Error:\s*)?No tests found(?:\.|\n|$)/u,ht=["failed","interrupted","passed","timedout"];class R extends Error{limit;constructor(e){super(vt(e)),this.limit=e,this.name="PlaywrightOutputArtifactLimitError"}}export async function collectPlaywrightEvidence(t,e=q){await w.mkdir(t.artifactDirectory,{recursive:!0});const i=await w.realpath(t.artifactDirectory),c=await w.realpath(t.rootDirectory),n=g.join(i,pt);await w.mkdir(n,{mode:448,recursive:!1});const a=[],s=[];for(const r of["stdout","stderr"]){const l=t[r];if(l.length===0)continue;a.push({stream:r,text:l});const m=et(Buffer.from(l,"utf8")),y=await Y(n,`${r}.log`,m.contents);s.push(F(y,m,"text/plain; charset=utf-8","engine.process-output",`playwright-${r}`,`${m.sizeBytes.toString()} bytes`))}let p=null,d=null,f=new Set,h=[];try{const r=await tt(t.reportFile,lt,"Testkit native Playwright report",e);if(r.file===null)p=new Error("Playwright did not produce its Testkit native report");else{d=gt(at(r.value,t.identity).result,t.sourceFile);const l=await Y(n,"testkit-playwright-report.json",r.file.contents);s.push(F(l,r.file,"application/json","engine.testkit-playwright-report","testkit-playwright-report","Testkit native Playwright report")),a.push(...d.logs);const m=await it(n,d.failureDetails);m!==null&&s.push(m);const y=await wt(i,n,c,d.attachments,e);s.push(...y.artifacts),f=y.sourcePaths,h=y.failures}}catch(r){p=nt(r)}let o={artifacts:[],bytes:0,entries:0},u=null;try{o=await Pt(i,n,t.outputDirectory,f,e),s.push(...o.artifacts)}catch(r){if(!(r instanceof R))throw r;u=r.limit,a.push({stream:"engine",text:r.message})}const P=ft(d,c,t.sourceFile,h.length),k=u??o;return{attachmentFailures:h,evidence:{artifacts:s,logs:a,metrics:[...Object.entries(d?.stats??{}).map(([r,l])=>({name:r==="duration"?"playwright.duration":`playwright.tests.${r}`,unit:r==="duration"?"milliseconds":"tests",value:l})),...["bytes","entries"].map(r=>({name:`playwright.output.${r}`,unit:r,value:k[r]}))],...d===null?{}:{native:{playwright:{attachmentFailures:h.map(r=>({...r})),specs:d.specs,stats:d.stats}}}},outputLimit:u,report:P,reportError:p}}function ft(t,e,i,c){if(t===null)return{failure:null,status:"failed"};const n=t.specs.filter((s,p)=>K(i,t.sourceFiles[p])),a=t.failureMessages.length>0&&t.failureMessages.every(s=>dt.test(s));return t.stats.unexpected>0||t.failureMessages.length>0&&(!a||n.length>0)?{failure:{...t.failureMessages.some(s=>/browserType\.launch(?:PersistentContext)?:/u.test(s)&&/Executable doesn't exist|Host system is missing dependencies|error while loading shared libraries|Failed to launch/u.test(s))?{classification:"infra"}:{},code:"PLAYWRIGHT_SPECS_FAILED",details:{attachmentFailures:c,failOnFlakyTests:t.failOnFlakyTests,reportErrors:t.reportErrors,runStatus:t.runStatus,stats:t.stats,specs:t.specs.slice(0,50)},message:t.failureMessages.at(0)??`${t.stats.unexpected.toString()} Playwright test(s) failed`},status:"failed"}:n.length>0?{failure:null,status:n.every(s=>s.status==="skipped")?"skipped":"passed"}:{failure:{code:"PLAYWRIGHT_NO_TESTS",details:{file:U(e,i),reportedSpecs:t.specs.slice(0,50),stats:t.stats},message:"Selected Playwright task did not execute any tests"},status:"failed"}}function K(t,e){return g.relative(g.normalize(t),e)===""}function gt(t,e){const i=S(t,"Playwright native report result"),c=ht.find(r=>r===i.status);if(c===void 0)throw new Error("Playwright native report status is invalid");if(typeof i.failOnFlakyTests!="boolean")throw new Error("Playwright native report failOnFlakyTests must be a boolean");const n=i.failOnFlakyTests,a=bt(i.stats),s=O(i.errors,"errors"),p=s.length,d=[],f=[],h=[],o=new Set,u={expected:0,flaky:0,skipped:0,unexpected:0};for(const r of A(i.specs,"specs")){const l=S(r,"Playwright native report spec"),m=b(l.id,"Playwright native spec.id");if(o.has(m))throw new Error("Playwright native report contains a duplicate test identity");o.add(m);const y=b(l.title,"Playwright native spec.title"),v=O(l.parents,"spec.parents");v.length>0&&(v[0]=W(v[0]));const L=W(b(l.file,"Playwright native spec.file")),x=b(l.sourceFile,"Playwright native spec.sourceFile");if(!g.isAbsolute(x)&&!g.win32.isAbsolute(x))throw new Error("Playwright native spec.sourceFile must be absolute");const Z=_(l.projectName,"spec.projectName"),T=l.status;if(T!=="expected"&&T!=="flaky"&&T!=="skipped"&&T!=="unexpected")throw new Error("Playwright native spec.status is invalid");u[T]+=1;const j=O(l.failureMessages,"spec.failureMessages"),z=l.failureStack===null?null:_(l.failureStack,"spec.failureStack"),D=G(l.line,"Playwright native spec.line"),B=G(l.column,"Playwright native spec.column");T==="unexpected"&&(s.push(...j),h.push({kind:K(e,x)?"playwright-spec":"playwright-other-file-spec",key:[...v.slice(1),y].filter(Boolean).join(" "),title:y,count:1,message:j[0]??`Playwright reported "${y}" as an unexpected failure`,...v.length===0?{}:{suitePath:v},location:{path:L,...D===null?{}:{line:D},...B===null?{}:{column:B}},...z===null?{}:{stack:z}})),f.push(x),d.push({durationMs:H(l.durationMs,"Playwright native spec.durationMs"),retries:E(l.retries,"Playwright native spec.retries"),file:L,projectName:Z,status:T,title:[...v,y].filter(Boolean).join(" > ")})}for(const r of["expected","flaky","skipped","unexpected"])if(a[r]!==u[r])throw new Error("Playwright native report stats do not match its cases");const P=A(i.logs,"logs").map(r=>{const l=S(r,"Playwright native report log");if(l.stream!=="stdout"&&l.stream!=="stderr")throw new Error("Playwright native report log stream is invalid");return{stream:l.stream,text:_(l.text,"log.text")}});return{attachments:A(i.attachments,"attachments").map((r,l)=>yt(r,l,o)),failOnFlakyTests:n,failureDetails:h,failureMessages:s,logs:St(P),reportErrors:p,runStatus:c,sourceFiles:f,specs:d,stats:a}}function A(t,e){if(!Array.isArray(t))throw new Error(`Playwright native report ${e} must be an array`);return t}function O(t,e){return A(t,e).map(i=>_(i,e))}function _(t,e){if(typeof t!="string")throw new Error(`Playwright native report ${e} must be a string`);return t}function yt(t,e,i){const c=S(t,`Playwright attachment[${e.toString()}]`),n=b(c.specId,"Playwright attachment.specId");if(!i.has(n))throw new Error("Playwright native attachment references an unknown test identity");const a=N(c.path),s=c.error===void 0?void 0:mt(c.error);if(c.body!==void 0||a===null&&s===void 0)throw new Error("Playwright attachment must contain a path or an error");return{contentType:N(c.contentType)??"application/octet-stream",name:N(c.name)??`attachment-${e.toString()}`,specId:n,...a===null?{}:{path:a},...s===void 0?{}:{error:s},retry:E(c.retry,"Playwright attachment.retry"),specTitle:b(c.specTitle,"Playwright attachment.specTitle")}}function mt(t){const e=S(t,"Playwright attachment.error");if(e.kind==="io")return{kind:"io",message:b(e.message,"Playwright attachment.error.message")};const i=e.kind;if(i!=="individual"&&i!=="total")throw new Error("Playwright attachment.error has an invalid kind");const c=i==="individual"?X:ct,n=E(e.observed,"Playwright attachment.error.observed");if(e.limit!==c||n<=c)throw new Error("Playwright attachment.error has an invalid limit");return{kind:i,limit:c,observed:n}}function I(t,e){const{name:i,specId:c,specTitle:n,retry:a}=t,s=e.kind==="io"?e.message:`exceeds its ${e.limit.toString()} byte ${e.kind} limit (observed ${e.observed.toString()} bytes)`;return{code:e.kind==="io"?"PLAYWRIGHT_ATTACHMENT_FAILED":"PLAYWRIGHT_ATTACHMENT_LIMIT",...e.kind==="io"?{classification:"infra"}:{},details:{name:i,specId:c,specTitle:n,retry:a,...e},message:`Playwright attachment "${i}" (${n}, retry ${a.toString()}): ${s}`}}async function wt(t,e,i,c,n){const a=[],s=[],p=new Set;let d=0;const f=g.join(e,"attachments");c.length>0&&await w.mkdir(f,{mode:448,recursive:!1});for(const[h,o]of c.entries())try{if(o.error!==void 0){o.path!==void 0&&p.add(g.resolve(i,o.path)),s.push(I(o,o.error));continue}const u=await Tt(o.path,i,t,n);p.add(u.file);const P=E(Number(u.identity.size),"Playwright attachment size"),k=st(P,d+P);if(k!==null){s.push(I(o,k));continue}const r=await C(u.file,X,"Playwright attachment",n,u.identity,f,`${h.toString().padStart(4,"0")}-${Q(o.name)}${kt(o)}`);d+=r.identity.sizeBytes,a.push({path:r.filePath,contentType:o.contentType,kind:"engine.playwright-attachment",name:o.name,summary:`${o.specTitle} retry ${o.retry.toString()}`,metadata:{...r.identity,specId:o.specId,specTitle:o.specTitle,retry:o.retry}})}catch(u){s.push(I(o,ot(u)))}return{artifacts:a,failures:s,sourcePaths:p}}async function Pt(t,e,i,c,n){try{const a=await n.lstat(i);if(!a.isDirectory()||a.isSymbolicLink())throw new Error("Playwright output directory must be a non-symbolic-link directory");const s=await w.realpath(i);if(!$(t,s))throw new Error("Playwright output directory escaped the artifact root");const p=await Et(s,c,n),d=[],f=g.join(e,"output");p.candidates.length>0&&await w.mkdir(f,{mode:448,recursive:!1});for(const[h,o]of p.candidates.entries()){const u=await C(o.file,M,"Playwright output file",n,o.identity,f,`${h.toString().padStart(5,"0")}-${Q(g.basename(o.file))}${At(o.file)}`);d.push(F(u.filePath,u.identity,xt(o.file),"engine.playwright-output",o.relativePath,"Playwright output"))}return{artifacts:d,bytes:p.bytes,entries:p.entries}}catch(a){if(rt(a))return{artifacts:[],bytes:0,entries:0};throw a}}async function Et(t,e,i){const c=[];let n=0,a=0;const s=[{depth:0,directory:t}];for(;s.length>0;){const p=s.pop();if(p===void 0)continue;if(p.depth>V)throw new Error(`Playwright output exceeds the ${V.toString()} level directory depth limit`);const d=p.directory,f=await w.readdir(d,{withFileTypes:!0});f.sort((h,o)=>h.name.localeCompare(o.name));for(const h of f){if(a+=1,a>J)throw new R({bytes:n,entries:a,kind:"entries",limit:J,observed:a});const o=g.join(d,h.name),u=await i.lstat(o);if(u.isSymbolicLink())throw new Error("Playwright output must not contain symbolic links");if(u.isDirectory()){s.push({depth:p.depth+1,directory:o});continue}if(!u.isFile())continue;if(u.size<0n||u.size>BigInt(Number.MAX_SAFE_INTEGER))throw new Error("Playwright output file has an invalid size");const P=Number(u.size);if(n+=P,!Number.isSafeInteger(n)||n>M)throw new R({bytes:n,entries:a,kind:"bytes",limit:M,observed:n});e.has(o)||c.push({file:o,identity:u,relativePath:U(t,o)})}}return{bytes:n,candidates:c,entries:a}}function vt(t){return t.kind==="bytes"?`Playwright output exceeds its ${t.limit.toString()} byte aggregate limit (observed ${t.observed.toString()} bytes)`:`Playwright output exceeds its ${t.limit.toString()} entry limit (observed ${t.observed.toString()} entries)`}async function Tt(t,e,i,c){if(t===void 0)throw new Error("Playwright path attachment did not declare a path");const n=g.resolve(e,t),a=await c.lstat(n);if(!a.isFile()||a.isSymbolicLink())throw new Error("Playwright attachment path must be a regular non-symbolic-link file");const s=await w.realpath(n);if(!$(e,s)&&!$(i,s))throw new Error("Playwright attachment path must stay within the consumer or artifact root");return{file:s,identity:a}}function bt(t){const e=S(t,"Playwright report.stats");return{expected:E(e.expected,"Playwright stats.expected"),unexpected:E(e.unexpected,"Playwright stats.unexpected"),flaky:E(e.flaky,"Playwright stats.flaky"),skipped:E(e.skipped,"Playwright stats.skipped"),duration:H(e.duration,"Playwright stats.duration")}}function St(t){let e=ut;const i=[];for(const c of t){if(e<=0)break;const a=Buffer.from(c.text,"utf8").subarray(0,e).toString("utf8");a.length>0&&i.push({...c,text:a}),e-=Buffer.byteLength(a)}return e<=0&&i.push({stream:"engine",text:"Playwright report log evidence was truncated at its bounded limit"}),i}function kt(t){if(t.path!==void 0){const e=g.extname(t.path);if(/^\.[a-z0-9]{1,12}$/iu.test(e))return e}return t.contentType==="application/json"?".json":t.contentType.startsWith("text/")?".txt":t.contentType==="image/png"?".png":t.contentType==="image/jpeg"?".jpg":".bin"}function xt(t){const e=g.extname(t).toLowerCase();return e===".json"?"application/json":e===".zip"?"application/zip":e===".png"?"image/png":e===".jpg"||e===".jpeg"?"image/jpeg":e===".webm"?"video/webm":[".log",".txt",".md"].includes(e)?"text/plain; charset=utf-8":"application/octet-stream"}function At(t){const e=g.extname(t).toLowerCase();return/^\.[a-z0-9]{1,12}$/u.test(e)?e:".bin"}function Q(t){const e=t.trim().toLowerCase().replace(/[^a-z0-9._-]+/gu,"-").replace(/^-+|-+$/gu,"").slice(0,80);return e.length>0?e:"attachment"}
1
+ import{toError as q}from"../../../kernel/error-serialization.js";import{isPathWithin as F}from"../../../process/files/paths.js";import w from"node:fs/promises";import g from"node:path";import{evidenceArtifact as $,evidenceFileAccess as tt,readStableJsonFile as et,snapshotStableEvidenceFile as C,stableEvidenceFromBuffer as it,writeOwnedEvidenceFile as Y}from"../../adapters/evidence-files.js";import{materializePlaywrightFailureDetails as rt}from"./failure-details.js";import{isMissingFileError as nt,nonNegativeInteger as E,nonNegativeNumber as H,optionalNonEmptyString as N,optionalNonNegativeInteger as G,portableRelativePath as U,portableReportPath as W,requireNonEmptyString as b,requireRecord as S}from"./evidence-validation.js";import{readNativeReport as at}from"../../adapters/native-report.js";import{attachmentLimitError as st,attachmentIoError as ot,MAX_ATTACHMENT_BYTES as X,MAX_TOTAL_ATTACHMENT_BYTES as ct}from"./attachments.js";const lt=64*1024*1024,J=1e4,M=512*1024*1024,ut=2*1024*1024,V=128,pt=".testkit-playwright-evidence",dt=/(?:^|\n)(?:Error:\s*)?No tests found(?:\.|\n|$)/u,ht=["failed","interrupted","passed","timedout"];class R extends Error{limit;constructor(e){super(vt(e)),this.limit=e,this.name="PlaywrightOutputArtifactLimitError"}}export async function collectPlaywrightEvidence(t,e=tt){await w.mkdir(t.artifactDirectory,{recursive:!0});const i=await w.realpath(t.artifactDirectory),c=await w.realpath(t.rootDirectory),n=g.join(i,pt);await w.mkdir(n,{mode:448,recursive:!1});const a=[],s=[];for(const r of["stdout","stderr"]){const l=t[r];if(l.length===0)continue;a.push({stream:r,text:l});const m=it(Buffer.from(l,"utf8")),y=await Y(n,`${r}.log`,m.contents);s.push($(y,m,"text/plain; charset=utf-8","engine.process-output",`playwright-${r}`,`${m.sizeBytes.toString()} bytes`))}let p=null,d=null,f=new Set,h=[];try{const r=await et(t.reportFile,lt,"Testkit native Playwright report",e);if(r.file===null)p=new Error("Playwright did not produce its Testkit native report");else{d=gt(at(r.value,t.identity).result,t.sourceFile);const l=await Y(n,"testkit-playwright-report.json",r.file.contents);s.push($(l,r.file,"application/json","engine.testkit-playwright-report","testkit-playwright-report","Testkit native Playwright report")),a.push(...d.logs);const m=await rt(n,d.failureDetails);m!==null&&s.push(m);const y=await wt(i,n,c,d.attachments,e);s.push(...y.artifacts),f=y.sourcePaths,h=y.failures}}catch(r){p=q(r)}let o={artifacts:[],bytes:0,entries:0},u=null;try{o=await Pt(i,n,t.outputDirectory,f,e),s.push(...o.artifacts)}catch(r){if(!(r instanceof R))throw r;u=r.limit,a.push({stream:"engine",text:r.message})}const P=ft(d,c,t.sourceFile,h.length),k=u??o;return{attachmentFailures:h,evidence:{artifacts:s,logs:a,metrics:[...Object.entries(d?.stats??{}).map(([r,l])=>({name:r==="duration"?"playwright.duration":`playwright.tests.${r}`,unit:r==="duration"?"milliseconds":"tests",value:l})),...["bytes","entries"].map(r=>({name:`playwright.output.${r}`,unit:r,value:k[r]}))],...d===null?{}:{native:{playwright:{attachmentFailures:h.map(r=>({...r})),specs:d.specs,stats:d.stats}}}},outputLimit:u,report:P,reportError:p}}function ft(t,e,i,c){if(t===null)return{failure:null,status:"failed"};const n=t.specs.filter((s,p)=>K(i,t.sourceFiles[p])),a=t.failureMessages.length>0&&t.failureMessages.every(s=>dt.test(s));return t.stats.unexpected>0||t.failureMessages.length>0&&(!a||n.length>0)?{failure:{...t.failureMessages.some(s=>/browserType\.launch(?:PersistentContext)?:/u.test(s)&&/Executable doesn't exist|Host system is missing dependencies|error while loading shared libraries|Failed to launch/u.test(s))?{classification:"infra"}:{},code:"PLAYWRIGHT_SPECS_FAILED",details:{attachmentFailures:c,failOnFlakyTests:t.failOnFlakyTests,reportErrors:t.reportErrors,runStatus:t.runStatus,stats:t.stats,specs:t.specs.slice(0,50)},message:t.failureMessages.at(0)??`${t.stats.unexpected.toString()} Playwright test(s) failed`},status:"failed"}:n.length>0?{failure:null,status:n.every(s=>s.status==="skipped")?"skipped":"passed"}:{failure:{code:"PLAYWRIGHT_NO_TESTS",details:{file:U(e,i),reportedSpecs:t.specs.slice(0,50),stats:t.stats},message:"Selected Playwright task did not execute any tests"},status:"failed"}}function K(t,e){return g.relative(g.normalize(t),e)===""}function gt(t,e){const i=S(t,"Playwright native report result"),c=ht.find(r=>r===i.status);if(c===void 0)throw new Error("Playwright native report status is invalid");if(typeof i.failOnFlakyTests!="boolean")throw new Error("Playwright native report failOnFlakyTests must be a boolean");const n=i.failOnFlakyTests,a=bt(i.stats),s=O(i.errors,"errors"),p=s.length,d=[],f=[],h=[],o=new Set,u={expected:0,flaky:0,skipped:0,unexpected:0};for(const r of A(i.specs,"specs")){const l=S(r,"Playwright native report spec"),m=b(l.id,"Playwright native spec.id");if(o.has(m))throw new Error("Playwright native report contains a duplicate test identity");o.add(m);const y=b(l.title,"Playwright native spec.title"),v=O(l.parents,"spec.parents");v.length>0&&(v[0]=W(v[0]));const L=W(b(l.file,"Playwright native spec.file")),x=b(l.sourceFile,"Playwright native spec.sourceFile");if(!g.isAbsolute(x)&&!g.win32.isAbsolute(x))throw new Error("Playwright native spec.sourceFile must be absolute");const Z=_(l.projectName,"spec.projectName"),T=l.status;if(T!=="expected"&&T!=="flaky"&&T!=="skipped"&&T!=="unexpected")throw new Error("Playwright native spec.status is invalid");u[T]+=1;const j=O(l.failureMessages,"spec.failureMessages"),z=l.failureStack===null?null:_(l.failureStack,"spec.failureStack"),D=G(l.line,"Playwright native spec.line"),B=G(l.column,"Playwright native spec.column");T==="unexpected"&&(s.push(...j),h.push({kind:K(e,x)?"playwright-spec":"playwright-other-file-spec",key:[...v.slice(1),y].filter(Boolean).join(" "),title:y,count:1,message:j[0]??`Playwright reported "${y}" as an unexpected failure`,...v.length===0?{}:{suitePath:v},location:{path:L,...D===null?{}:{line:D},...B===null?{}:{column:B}},...z===null?{}:{stack:z}})),f.push(x),d.push({durationMs:H(l.durationMs,"Playwright native spec.durationMs"),retries:E(l.retries,"Playwright native spec.retries"),file:L,projectName:Z,status:T,title:[...v,y].filter(Boolean).join(" > ")})}for(const r of["expected","flaky","skipped","unexpected"])if(a[r]!==u[r])throw new Error("Playwright native report stats do not match its cases");const P=A(i.logs,"logs").map(r=>{const l=S(r,"Playwright native report log");if(l.stream!=="stdout"&&l.stream!=="stderr")throw new Error("Playwright native report log stream is invalid");return{stream:l.stream,text:_(l.text,"log.text")}});return{attachments:A(i.attachments,"attachments").map((r,l)=>yt(r,l,o)),failOnFlakyTests:n,failureDetails:h,failureMessages:s,logs:St(P),reportErrors:p,runStatus:c,sourceFiles:f,specs:d,stats:a}}function A(t,e){if(!Array.isArray(t))throw new Error(`Playwright native report ${e} must be an array`);return t}function O(t,e){return A(t,e).map(i=>_(i,e))}function _(t,e){if(typeof t!="string")throw new Error(`Playwright native report ${e} must be a string`);return t}function yt(t,e,i){const c=S(t,`Playwright attachment[${e.toString()}]`),n=b(c.specId,"Playwright attachment.specId");if(!i.has(n))throw new Error("Playwright native attachment references an unknown test identity");const a=N(c.path),s=c.error===void 0?void 0:mt(c.error);if(c.body!==void 0||a===null&&s===void 0)throw new Error("Playwright attachment must contain a path or an error");return{contentType:N(c.contentType)??"application/octet-stream",name:N(c.name)??`attachment-${e.toString()}`,specId:n,...a===null?{}:{path:a},...s===void 0?{}:{error:s},retry:E(c.retry,"Playwright attachment.retry"),specTitle:b(c.specTitle,"Playwright attachment.specTitle")}}function mt(t){const e=S(t,"Playwright attachment.error");if(e.kind==="io")return{kind:"io",message:b(e.message,"Playwright attachment.error.message")};const i=e.kind;if(i!=="individual"&&i!=="total")throw new Error("Playwright attachment.error has an invalid kind");const c=i==="individual"?X:ct,n=E(e.observed,"Playwright attachment.error.observed");if(e.limit!==c||n<=c)throw new Error("Playwright attachment.error has an invalid limit");return{kind:i,limit:c,observed:n}}function I(t,e){const{name:i,specId:c,specTitle:n,retry:a}=t,s=e.kind==="io"?e.message:`exceeds its ${e.limit.toString()} byte ${e.kind} limit (observed ${e.observed.toString()} bytes)`;return{code:e.kind==="io"?"PLAYWRIGHT_ATTACHMENT_FAILED":"PLAYWRIGHT_ATTACHMENT_LIMIT",...e.kind==="io"?{classification:"infra"}:{},details:{name:i,specId:c,specTitle:n,retry:a,...e},message:`Playwright attachment "${i}" (${n}, retry ${a.toString()}): ${s}`}}async function wt(t,e,i,c,n){const a=[],s=[],p=new Set;let d=0;const f=g.join(e,"attachments");c.length>0&&await w.mkdir(f,{mode:448,recursive:!1});for(const[h,o]of c.entries())try{if(o.error!==void 0){o.path!==void 0&&p.add(g.resolve(i,o.path)),s.push(I(o,o.error));continue}const u=await Tt(o.path,i,t,n);p.add(u.file);const P=E(Number(u.identity.size),"Playwright attachment size"),k=st(P,d+P);if(k!==null){s.push(I(o,k));continue}const r=await C(u.file,X,"Playwright attachment",n,u.identity,f,`${h.toString().padStart(4,"0")}-${Q(o.name)}${kt(o)}`);d+=r.identity.sizeBytes,a.push({path:r.filePath,contentType:o.contentType,kind:"engine.playwright-attachment",name:o.name,summary:`${o.specTitle} retry ${o.retry.toString()}`,metadata:{...r.identity,specId:o.specId,specTitle:o.specTitle,retry:o.retry}})}catch(u){s.push(I(o,ot(u)))}return{artifacts:a,failures:s,sourcePaths:p}}async function Pt(t,e,i,c,n){try{const a=await n.lstat(i);if(!a.isDirectory()||a.isSymbolicLink())throw new Error("Playwright output directory must be a non-symbolic-link directory");const s=await w.realpath(i);if(!F(t,s))throw new Error("Playwright output directory escaped the artifact root");const p=await Et(s,c,n),d=[],f=g.join(e,"output");p.candidates.length>0&&await w.mkdir(f,{mode:448,recursive:!1});for(const[h,o]of p.candidates.entries()){const u=await C(o.file,M,"Playwright output file",n,o.identity,f,`${h.toString().padStart(5,"0")}-${Q(g.basename(o.file))}${At(o.file)}`);d.push($(u.filePath,u.identity,xt(o.file),"engine.playwright-output",o.relativePath,"Playwright output"))}return{artifacts:d,bytes:p.bytes,entries:p.entries}}catch(a){if(nt(a))return{artifacts:[],bytes:0,entries:0};throw a}}async function Et(t,e,i){const c=[];let n=0,a=0;const s=[{depth:0,directory:t}];for(;s.length>0;){const p=s.pop();if(p===void 0)continue;if(p.depth>V)throw new Error(`Playwright output exceeds the ${V.toString()} level directory depth limit`);const d=p.directory,f=await w.readdir(d,{withFileTypes:!0});f.sort((h,o)=>h.name.localeCompare(o.name));for(const h of f){if(a+=1,a>J)throw new R({bytes:n,entries:a,kind:"entries",limit:J,observed:a});const o=g.join(d,h.name),u=await i.lstat(o);if(u.isSymbolicLink())throw new Error("Playwright output must not contain symbolic links");if(u.isDirectory()){s.push({depth:p.depth+1,directory:o});continue}if(!u.isFile())continue;if(u.size<0n||u.size>BigInt(Number.MAX_SAFE_INTEGER))throw new Error("Playwright output file has an invalid size");const P=Number(u.size);if(n+=P,!Number.isSafeInteger(n)||n>M)throw new R({bytes:n,entries:a,kind:"bytes",limit:M,observed:n});e.has(o)||c.push({file:o,identity:u,relativePath:U(t,o)})}}return{bytes:n,candidates:c,entries:a}}function vt(t){return t.kind==="bytes"?`Playwright output exceeds its ${t.limit.toString()} byte aggregate limit (observed ${t.observed.toString()} bytes)`:`Playwright output exceeds its ${t.limit.toString()} entry limit (observed ${t.observed.toString()} entries)`}async function Tt(t,e,i,c){if(t===void 0)throw new Error("Playwright path attachment did not declare a path");const n=g.resolve(e,t),a=await c.lstat(n);if(!a.isFile()||a.isSymbolicLink())throw new Error("Playwright attachment path must be a regular non-symbolic-link file");const s=await w.realpath(n);if(!F(e,s)&&!F(i,s))throw new Error("Playwright attachment path must stay within the consumer or artifact root");return{file:s,identity:a}}function bt(t){const e=S(t,"Playwright report.stats");return{expected:E(e.expected,"Playwright stats.expected"),unexpected:E(e.unexpected,"Playwright stats.unexpected"),flaky:E(e.flaky,"Playwright stats.flaky"),skipped:E(e.skipped,"Playwright stats.skipped"),duration:H(e.duration,"Playwright stats.duration")}}function St(t){let e=ut;const i=[];for(const c of t){if(e<=0)break;const a=Buffer.from(c.text,"utf8").subarray(0,e).toString("utf8");a.length>0&&i.push({...c,text:a}),e-=Buffer.byteLength(a)}return e<=0&&i.push({stream:"engine",text:"Playwright report log evidence was truncated at its bounded limit"}),i}function kt(t){if(t.path!==void 0){const e=g.extname(t.path);if(/^\.[a-z0-9]{1,12}$/iu.test(e))return e}return t.contentType==="application/json"?".json":t.contentType.startsWith("text/")?".txt":t.contentType==="image/png"?".png":t.contentType==="image/jpeg"?".jpg":".bin"}function xt(t){const e=g.extname(t).toLowerCase();return e===".json"?"application/json":e===".zip"?"application/zip":e===".png"?"image/png":e===".jpg"||e===".jpeg"?"image/jpeg":e===".webm"?"video/webm":[".log",".txt",".md"].includes(e)?"text/plain; charset=utf-8":"application/octet-stream"}function At(t){const e=g.extname(t).toLowerCase();return/^\.[a-z0-9]{1,12}$/u.test(e)?e:".bin"}function Q(t){const e=t.trim().toLowerCase().replace(/[^a-z0-9._-]+/gu,"-").replace(/^-+|-+$/gu,"").slice(0,80);return e.length>0?e:"attachment"}
@@ -1 +1 @@
1
- import r from"node:fs";import e from"node:path";export const PLAYWRIGHT_CONFIG_FILENAMES=Object.freeze(["playwright.config.ts","playwright.config.mts","playwright.config.js","playwright.config.mjs","playwright.config.cjs","playwright.config.cts"]);export function findPlaywrightConfigFile(t){for(const o of PLAYWRIGHT_CONFIG_FILENAMES){const i=e.join(t,o);try{if(r.statSync(i).isFile())return i}catch(n){if(c(n,"ENOENT"))continue;throw new Error(`Could not inspect optional support module ${i}`,{cause:n})}}return null}function c(t,o){return typeof t=="object"&&t!==null&&Reflect.get(t,"code")===o}
1
+ import n from"node:fs";import c from"node:path";import{hasErrorCode as e}from"../../../kernel/error-serialization.js";export const PLAYWRIGHT_CONFIG_FILENAMES=Object.freeze(["playwright.config.ts","playwright.config.mts","playwright.config.js","playwright.config.mjs","playwright.config.cjs","playwright.config.cts"]);export function findPlaywrightConfigFile(r){for(const i of PLAYWRIGHT_CONFIG_FILENAMES){const o=c.join(r,i);try{if(n.statSync(o).isFile())return o}catch(t){if(e(t,"ENOENT"))continue;throw new Error(`Could not inspect optional support module ${o}`,{cause:t})}}return null}
@@ -1 +1 @@
1
- import{DEFAULT_FILE_TIMEOUT_SECONDS as w,INTERNAL_FILE_TIMEOUT_SECONDS_ENV as b}from"../../../domain/file-timeout.js";import{readPlaywrightContext as E}from"./runtime-input.js";import{defineConfig as T}from"@playwright/test";import{loadDotenvFiles as y}from"../../../config/runtime/env/index.js";export function defineTestkitConfig(e={},n={}){const t=n.env||process.env;y({cwd:n.cwd,env:t,files:n.dotenvFiles||[".env.local"],onlyWhenAllowed:n.onlyWhenAllowed,override:n.override});const o=S(t,n.defaultFileTimeoutSeconds)*1e3,i=E(),c=i?.managed===!0,s=a(e.use,"Playwright config.use"),d=a(e.expect,"Playwright config.expect"),f=U(s.baseURL,"Playwright config.use.baseURL"),u=i?.baseUrl??t.BASE_URL??f,m={...s,...u?{baseURL:u}:{},actionTimeout:o,navigationTimeout:o},g={...e,...c?{globalTimeout:o}:{},timeout:o,expect:{...d,timeout:o},use:m,...c?{webServer:void 0}:{}};return T(g)}function S(e,n){const t=e[b],r=l(n??w);return t==null?r:l(t)}function l(e){const n=Number(e);if(!Number.isInteger(n)||n<=0)throw new Error("Playwright file timeout seconds must be a positive integer.");return n}function a(e,n){if(e==null)return{};if(!h(e))throw new Error(`${n} must be an object`);return e}function h(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function U(e,n){if(e!=null){if(typeof e!="string")throw new Error(`${n} must be a string`);return e}}
1
+ import{DEFAULT_FILE_TIMEOUT_SECONDS as w,INTERNAL_FILE_TIMEOUT_SECONDS_ENV as E}from"../../../domain/file-timeout.js";import{readPlaywrightContext as b}from"./runtime-input.js";import{defineConfig as T}from"@playwright/test";import{loadDotenvFiles as S}from"../../../config/runtime/env/index.js";import{isUnknownRecord as h}from"../../../kernel/unknown-values.js";export function defineTestkitConfig(n={},e={}){const t=e.env||process.env;S({cwd:e.cwd,env:t,files:e.dotenvFiles||[".env.local"],onlyWhenAllowed:e.onlyWhenAllowed,override:e.override});const o=U(t,e.defaultFileTimeoutSeconds)*1e3,i=b(),s=i?.managed===!0,c=a(n.use,"Playwright config.use"),m=a(n.expect,"Playwright config.expect"),d=p(c.baseURL,"Playwright config.use.baseURL"),u=i?.baseUrl??t.BASE_URL??d,f={...c,...u?{baseURL:u}:{},actionTimeout:o,navigationTimeout:o},g={...n,...s?{globalTimeout:o}:{},timeout:o,expect:{...m,timeout:o},use:f,...s?{webServer:void 0}:{}};return T(g)}function U(n,e){const t=n[E],r=l(e??w);return t==null?r:l(t)}function l(n){const e=Number(n);if(!Number.isInteger(e)||e<=0)throw new Error("Playwright file timeout seconds must be a positive integer.");return e}function a(n,e){if(n==null)return{};if(!h(n))throw new Error(`${e} must be an object`);return n}function p(n,e){if(n!=null){if(typeof n!="string")throw new Error(`${e} must be a string`);return n}}
@@ -3,7 +3,7 @@ export declare const PLAYWRIGHT_RUNTIME_INPUT_MAX_BYTES: number;
3
3
  export declare const PLAYWRIGHT_RUNTIME_ENVIRONMENT_MAX_BYTES: number;
4
4
  export declare const PLAYWRIGHT_RUNTIME_ENVIRONMENT_MAX_ENTRIES = 512;
5
5
  export interface PlaywrightTestContext {
6
- readonly baseUrl: string;
6
+ readonly baseUrl: null | string;
7
7
  readonly endpoints: Readonly<Record<string, string>>;
8
8
  readonly env: Readonly<Record<string, string>>;
9
9
  readonly fixtures: EngineJsonObject;
@@ -12,7 +12,7 @@ export interface PlaywrightTestContext {
12
12
  readonly uiConfig: EngineJsonObject;
13
13
  }
14
14
  export interface PlaywrightRuntimeInputRequest {
15
- readonly baseUrl: string;
15
+ readonly baseUrl: null | string;
16
16
  readonly directory: string;
17
17
  readonly endpoints: Readonly<Record<string, string>>;
18
18
  readonly inputs: EngineExecutionInputs;
@@ -1,4 +1,4 @@
1
- import l from"node:fs/promises";import c from"node:path";import{writeOwnedRuntimeInput as m,ownedRuntimeInputModuleUrl as f}from"../../runtime-input-file.js";import{requireBoundedJsonValue as y,RUNTIME_INPUT_JSON_BOUNDS as h}from"../../contracts/json-bounds.js";import{ENGINE_PROCESS_RESERVED_ENVIRONMENT_KEYS as g}from"../../../process/environment.js";import{managedProcessEnvironmentOverlayInstallerSource as w}from"../../../process/environment-overlay.js";export const PLAYWRIGHT_RUNTIME_INPUT_MAX_BYTES=4*1024*1024,PLAYWRIGHT_RUNTIME_ENVIRONMENT_MAX_BYTES=2*1024*1024,PLAYWRIGHT_RUNTIME_ENVIRONMENT_MAX_ENTRIES=512;const d=new Set(["BASE_URL","FORCE_COLOR","NODE_OPTIONS","NO_COLOR","PLAYWRIGHT_BROWSERS_PATH","PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD","TESTKIT_ACTIVE","TESTKIT_INTERNAL_FILE_DEADLINE_MS","TESTKIT_INTERNAL_FILE_TIMEOUT_SECONDS","TESTKIT_INVOCATION_ID","TESTKIT_RUNTIME_ID","TESTKIT_TEST_FILE","TESTKIT_TEST_ID",...g].map(e=>e.toUpperCase())),p={filename:"runtime-input.json",label:"Playwright runtime input",maxSizeBytes:PLAYWRIGHT_RUNTIME_INPUT_MAX_BYTES},E=Symbol.for("@gfdelarue/testkit/playwright-runtime-input/v1");export function readPlaywrightContext(){const e=Reflect.get(globalThis,E);return T(e)?e.context:null}export async function materializePlaywrightRuntimeInput(e){if(!c.isAbsolute(e.directory))throw new Error("Playwright runtime-input directory must be absolute");await l.mkdir(e.directory,{recursive:!0,mode:448}),process.platform!=="win32"&&await l.chmod(e.directory,448);const t=I(e.inputs.environment),r={baseUrl:v(e.baseUrl),endpoints:e.endpoints,engineId:"playwright",environment:t,fixtures:e.inputs.fixtures,invocationId:e.invocationId,managed:!0,schemaVersion:1,taskId:e.taskId,uiConfig:e.inputs.configuration??{},...e.inputs.setupData===void 0?{}:{setupData:e.inputs.setupData}},o=await m(e.directory,r,p);return{digest:o.sha256,file:o.file,sizeBytes:o.sizeBytes}}export function playwrightRuntimeInputBootstrapSource(e){const t=JSON.stringify({digest:e.digest,file:e.file,ownedInput:{directory:c.dirname(e.file),file:e.file,sha256:e.digest,sizeBytes:e.sizeBytes},protocol:p,invocationId:e.invocationId,jsonBounds:h,maximumEnvironmentBytes:PLAYWRIGHT_RUNTIME_ENVIRONMENT_MAX_BYTES,maximumEnvironmentEntries:PLAYWRIGHT_RUNTIME_ENVIRONMENT_MAX_ENTRIES,reservedEnvironmentKeys:[...d],taskId:e.taskId}),n=w(),r=`const requireBoundedJsonValue = (${y.toString()});`;return`
1
+ import l from"node:fs/promises";import c from"node:path";import{writeOwnedRuntimeInput as m,ownedRuntimeInputModuleUrl as f}from"../../runtime-input-file.js";import{requireBoundedJsonValue as y,RUNTIME_INPUT_JSON_BOUNDS as h}from"../../contracts/json-bounds.js";import{ENGINE_PROCESS_RESERVED_ENVIRONMENT_KEYS as w}from"../../../process/environment.js";import{managedProcessEnvironmentOverlayInstallerSource as g}from"../../../process/environment-overlay.js";import{isUnknownRecord as u}from"../../../kernel/unknown-values.js";export const PLAYWRIGHT_RUNTIME_INPUT_MAX_BYTES=4*1024*1024,PLAYWRIGHT_RUNTIME_ENVIRONMENT_MAX_BYTES=2*1024*1024,PLAYWRIGHT_RUNTIME_ENVIRONMENT_MAX_ENTRIES=512;const d=new Set(["BASE_URL","FORCE_COLOR","NODE_OPTIONS","NO_COLOR","PLAYWRIGHT_BROWSERS_PATH","PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD","TESTKIT_ACTIVE","TESTKIT_INTERNAL_FILE_DEADLINE_MS","TESTKIT_INTERNAL_FILE_TIMEOUT_SECONDS","TESTKIT_INVOCATION_ID","TESTKIT_RUNTIME_ID","TESTKIT_TEST_FILE","TESTKIT_TEST_ID",...w].map(e=>e.toUpperCase())),p={filename:"runtime-input.json",label:"Playwright runtime input",maxSizeBytes:PLAYWRIGHT_RUNTIME_INPUT_MAX_BYTES},E=Symbol.for("@gfdelarue/testkit/playwright-runtime-input/v1");export function readPlaywrightContext(){const e=Reflect.get(globalThis,E);return T(e)?e.context:null}export async function materializePlaywrightRuntimeInput(e){if(!c.isAbsolute(e.directory))throw new Error("Playwright runtime-input directory must be absolute");await l.mkdir(e.directory,{recursive:!0,mode:448}),process.platform!=="win32"&&await l.chmod(e.directory,448);const t=I(e.inputs.environment),r={baseUrl:e.baseUrl===null?null:v(e.baseUrl),endpoints:e.endpoints,engineId:"playwright",environment:t,fixtures:e.inputs.fixtures,invocationId:e.invocationId,managed:!0,schemaVersion:1,taskId:e.taskId,uiConfig:e.inputs.configuration??{},...e.inputs.setupData===void 0?{}:{setupData:e.inputs.setupData}},o=await m(e.directory,r,p);return{digest:o.sha256,file:o.file,sizeBytes:o.sizeBytes}}export function playwrightRuntimeInputBootstrapSource(e){const t=JSON.stringify({digest:e.digest,file:e.file,ownedInput:{directory:c.dirname(e.file),file:e.file,sha256:e.digest,sizeBytes:e.sizeBytes},protocol:p,invocationId:e.invocationId,jsonBounds:h,maximumEnvironmentBytes:PLAYWRIGHT_RUNTIME_ENVIRONMENT_MAX_BYTES,maximumEnvironmentEntries:PLAYWRIGHT_RUNTIME_ENVIRONMENT_MAX_ENTRIES,reservedEnvironmentKeys:[...d],taskId:e.taskId}),n=g(),r=`const requireBoundedJsonValue = (${y.toString()});`;return`
2
2
  import path from "node:path";
3
3
  import { readVerifiedOwnedFile } from ${JSON.stringify(f)};
4
4
 
@@ -80,16 +80,7 @@ function validateRuntimeInput(value) {
80
80
  throw new Error("Playwright runtime input has an unsupported identity");
81
81
  if (input.managed !== true)
82
82
  throw new Error("Playwright runtime input must be managed by Testkit");
83
- if (typeof input.baseUrl !== "string" || input.baseUrl.trim().length === 0)
84
- throw new Error("Playwright runtime input.baseUrl must be a non-empty string");
85
- let parsedBaseUrl;
86
- try {
87
- parsedBaseUrl = new URL(input.baseUrl);
88
- } catch (error) {
89
- throw new Error("Playwright runtime input.baseUrl must be a valid URL", { cause: error });
90
- }
91
- if ((parsedBaseUrl.protocol !== "http:" && parsedBaseUrl.protocol !== "https:") || parsedBaseUrl.href !== input.baseUrl)
92
- throw new Error("Playwright runtime input.baseUrl must be a canonical HTTP(S) URL");
83
+ if (input.baseUrl !== null) validateBaseUrl(input.baseUrl);
93
84
  if (input.invocationId !== OPTIONS.invocationId || input.taskId !== OPTIONS.taskId)
94
85
  throw new Error("Playwright runtime input does not match its invocation and task");
95
86
  requireStringRecord(input.endpoints, "Playwright runtime input.endpoints");
@@ -103,6 +94,19 @@ function validateRuntimeInput(value) {
103
94
  return input;
104
95
  }
105
96
 
97
+ function validateBaseUrl(value) {
98
+ if (typeof value !== "string" || value.trim().length === 0)
99
+ throw new Error("Playwright runtime input.baseUrl must be null or a non-empty string");
100
+ let parsed;
101
+ try {
102
+ parsed = new URL(value);
103
+ } catch (error) {
104
+ throw new Error("Playwright runtime input.baseUrl must be a valid URL", { cause: error });
105
+ }
106
+ if ((parsed.protocol !== "http:" && parsed.protocol !== "https:") || parsed.href !== value)
107
+ throw new Error("Playwright runtime input.baseUrl must be a canonical HTTP(S) URL");
108
+ }
109
+
106
110
  function validateEnvironment(value) {
107
111
  const environment = requireStringRecord(value, "Playwright runtime input.environment");
108
112
  const keys = Object.keys(environment);
@@ -148,4 +152,4 @@ function requireRecord(value, label) {
148
152
  }
149
153
 
150
154
  await installPlaywrightRuntimeInput();
151
- `}function T(e){return!u(e)||!u(e.context)||!u(e.input)?!1:typeof e.digest=="string"&&typeof e.file=="string"&&e.input.engineId==="playwright"&&e.input.schemaVersion===1&&Object.isFrozen(e)&&Object.isFrozen(e.context)&&Object.isFrozen(e.input)}function u(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function I(e){const t=Object.create(null),n=new Set;let r=0;const o=Object.entries(e);if(o.length>PLAYWRIGHT_RUNTIME_ENVIRONMENT_MAX_ENTRIES)throw new Error(`Playwright consumer environment exceeds its ${PLAYWRIGHT_RUNTIME_ENVIRONMENT_MAX_ENTRIES.toString()} entry limit`);for(const[s,a]of o){O(s,a);const i=s.toUpperCase();if(n.has(i))throw new Error(`Playwright consumer environment contains case-insensitive duplicate key ${s}`);if(n.add(i),i.startsWith("TESTKIT_")||i.startsWith("PLAYWRIGHT_")||i.startsWith("PWTEST_")||i.startsWith("PW_TEST_")||d.has(i))throw new Error(`Playwright consumer environment contains reserved key ${s}`);if(r+=Buffer.byteLength(s,"utf8")+Buffer.byteLength(a,"utf8")+2,r>PLAYWRIGHT_RUNTIME_ENVIRONMENT_MAX_BYTES)throw new Error(`Playwright consumer environment exceeds its ${PLAYWRIGHT_RUNTIME_ENVIRONMENT_MAX_BYTES.toString()} byte limit`);t[s]=a}return t}function v(e){if(typeof e!="string"||e.trim().length===0)throw new Error("Playwright runtime input base URL must be non-empty");let t;try{t=new URL(e)}catch(n){throw new Error("Playwright runtime input base URL must be a valid URL",{cause:n})}if(t.protocol!=="http:"&&t.protocol!=="https:")throw new Error("Playwright runtime input base URL must use HTTP or HTTPS");return t.href}function O(e,t){if(e.length===0||e.includes("=")||e.includes("\0")||typeof t!="string")throw new Error("Playwright consumer environment contains an invalid key");if(t.includes("\0"))throw new Error(`Playwright consumer environment contains an invalid value for ${e}`)}
155
+ `}function T(e){return!u(e)||!u(e.context)||!u(e.input)?!1:typeof e.digest=="string"&&typeof e.file=="string"&&e.input.engineId==="playwright"&&e.input.schemaVersion===1&&Object.isFrozen(e)&&Object.isFrozen(e.context)&&Object.isFrozen(e.input)}function I(e){const t=Object.create(null),n=new Set;let r=0;const o=Object.entries(e);if(o.length>PLAYWRIGHT_RUNTIME_ENVIRONMENT_MAX_ENTRIES)throw new Error(`Playwright consumer environment exceeds its ${PLAYWRIGHT_RUNTIME_ENVIRONMENT_MAX_ENTRIES.toString()} entry limit`);for(const[a,s]of o){O(a,s);const i=a.toUpperCase();if(n.has(i))throw new Error(`Playwright consumer environment contains case-insensitive duplicate key ${a}`);if(n.add(i),i.startsWith("TESTKIT_")||i.startsWith("PLAYWRIGHT_")||i.startsWith("PWTEST_")||i.startsWith("PW_TEST_")||d.has(i))throw new Error(`Playwright consumer environment contains reserved key ${a}`);if(r+=Buffer.byteLength(a,"utf8")+Buffer.byteLength(s,"utf8")+2,r>PLAYWRIGHT_RUNTIME_ENVIRONMENT_MAX_BYTES)throw new Error(`Playwright consumer environment exceeds its ${PLAYWRIGHT_RUNTIME_ENVIRONMENT_MAX_BYTES.toString()} byte limit`);t[a]=s}return t}function v(e){if(typeof e!="string"||e.trim().length===0)throw new Error("Playwright runtime input base URL must be non-empty");let t;try{t=new URL(e)}catch(n){throw new Error("Playwright runtime input base URL must be a valid URL",{cause:n})}if(t.protocol!=="http:"&&t.protocol!=="https:")throw new Error("Playwright runtime input base URL must use HTTP or HTTPS");return t.href}function O(e,t){if(e.length===0||e.includes("=")||e.includes("\0")||typeof t!="string")throw new Error("Playwright consumer environment contains an invalid key");if(t.includes("\0"))throw new Error(`Playwright consumer environment contains an invalid value for ${e}`)}
@@ -1 +1 @@
1
- import o from"fs";import a from"path";import{finalizeLiveRunArtifact as i}from"../evidence/index.js";export function reconcileInactiveRunArtifact(t,n){typeof n.runId=="string"&&i(t,{runId:n.runId,reason:"stale run process not active",status:"interrupted"})}export function reconcileOrphanedLiveRunArtifact(t,{activeRunIds:n=new Set}={}){const r=c(t),e=r?.run.id||null;return!e||r?.run.status!=="running"||n.has(e)?!1:i(t,{runId:e,reason:"stale live artifact has no active run manifest",status:"interrupted"})}function c(t){try{const n=JSON.parse(o.readFileSync(a.join(t,".testkit","results","live.json"),"utf8"));return f(n)}catch{return null}}function f(t){if(!u(t)||!u(t.run))return null;const n=t.run.id,r=t.run.status;return!s(n)||!s(r)?null:{run:{id:n,status:r}}}function u(t){return typeof t=="object"&&t!==null&&!Array.isArray(t)}function s(t){return t===void 0||typeof t=="string"}
1
+ import o from"fs";import a from"path";import{finalizeLiveRunArtifact as e}from"../evidence/index.js";import{isUnknownRecord as u}from"../kernel/unknown-values.js";export function reconcileInactiveRunArtifact(n,t){typeof t.runId=="string"&&e(n,{runId:t.runId,reason:"stale run process not active",status:"interrupted"})}export function reconcileOrphanedLiveRunArtifact(n,{activeRunIds:t=new Set}={}){const r=f(n),i=r?.run.id||null;return!i||r?.run.status!=="running"||t.has(i)?!1:e(n,{runId:i,reason:"stale live artifact has no active run manifest",status:"interrupted"})}function f(n){try{const t=JSON.parse(o.readFileSync(a.join(n,".testkit","results","live.json"),"utf8"));return c(t)}catch{return null}}function c(n){if(!u(n)||!u(n.run))return null;const t=n.run.id,r=n.run.status;return!s(t)||!s(r)?null:{run:{id:t,status:r}}}function s(n){return n===void 0||typeof n=="string"}
@@ -3,7 +3,7 @@ export { isRunManifestActive } from "../process/runs/inspection.js";
3
3
  import type { RuntimeInstanceContext } from "./runtime-contexts.js";
4
4
  import type { LocalServiceLifecycle } from "./services.js";
5
5
  import { type ProductLifecycleFence } from "../process/lifecycle-fence.js";
6
- import { type RunManifest, type RunManifestService } from "../process/runs/manifest.js";
6
+ import { type RunManifest } from "../process/runs/manifest.js";
7
7
  type LifecycleServiceConfig = Parameters<LocalServiceLifecycle["registerService"]>[0];
8
8
  export interface RunLifecycleOptions {
9
9
  controller?: unknown;
@@ -13,15 +13,6 @@ export interface RunLifecycleOptions {
13
13
  resetResultsBeforePublish?: boolean;
14
14
  runId?: string;
15
15
  }
16
- export interface RunPortQuery {
17
- host?: unknown;
18
- port?: unknown;
19
- }
20
- export interface RunPortOwner {
21
- active: boolean;
22
- manifest: RunManifest;
23
- service: RunManifestService;
24
- }
25
16
  type RunGraphContext = Pick<RuntimeInstanceContext, "runtimeDir">;
26
17
  export interface RunLifecycleFinalizationState {
27
18
  interruptReason: null | string;
@@ -66,5 +57,4 @@ interface OwnedRunLifecycleState {
66
57
  export declare function createUnpublishedRunLifecycle(productDir: string, options?: Readonly<RunLifecycleOptions>): RunLifecycle;
67
58
  export declare function cleanupOwnedRunLifecycleWithinLifecycleFence(fence: Readonly<ProductLifecycleFence>, lifecycle: Readonly<RunLifecycle>): boolean;
68
59
  export declare function readOwnedRunLifecycleWithinLifecycleFence(fence: Readonly<ProductLifecycleFence>, lifecycle: Readonly<RunLifecycle>): OwnedRunLifecycleState;
69
- export declare function findPortOwner(productDir: string, { host, port }: Readonly<RunPortQuery>): RunPortOwner | null;
70
60
  export declare function disposeFailedRunLifecycle(lifecycle: RunLifecycle): void;
@@ -1 +1 @@
1
- import H from"crypto";import{isTerminalRunStatus as x}from"../process/runs/schema.js";import{observeProcessCreationIdentity as U}from"../process/process-identity.js";import{isRunManifestActive as B}from"../process/runs/inspection.js";export{isRunManifestActive}from"../process/runs/inspection.js";import{normalizeOperationReason as _}from"../domain/operation-reason.js";import{finalizeLiveRunArtifact as Y}from"../evidence/index.js";import{captureProcessCreationIdentity as J}from"../process/service-processes.js";import{requireProductLifecycleFenceProductDir as K,withProductLifecycleFence as Q}from"../process/lifecycle-fence.js";import{createRunManifest as X,listRunManifests as $,readRunMaintenanceStopRequest as Z,removeRunManifestFile as G,requireSafeRunId as ee,RUN_MANIFEST_SCHEMA_VERSION as te,runManifestPath as ne,writeRunManifest as re}from"../process/runs/manifest.js";const ie=1e3,oe=String,T=new WeakMap;export function createUnpublishedRunLifecycle(n,r={}){const i=r.runId===void 0?ce():ee(r.runId,"Testkit run id"),a=ne(n,i),s=new AbortController,m=new AbortController,u=r.pid===void 0?process.pid:r.pid;if(!Number.isSafeInteger(u)||u<0)throw new Error("Testkit run pid must be a non-negative safe integer");const b=u>0?J(u):null;if(u>0&&b===null)throw new Error(`Could not capture the Testkit run process identity for pid ${u.toString()}`);let f={incarnationId:H.randomUUID(),schemaVersion:te,runId:i,pid:u,processIdentity:typeof b=="string"?b:null,controller:ae(r.controller),status:"starting",startedAt:new Date().toISOString(),heartbeatAt:r.heartbeat===!1?null:new Date().toISOString(),interruptReason:null,services:[],runtimeDirs:[]};const O=[];let w,D=null;const y=new Set,S=new Set,L=r.keepAlive===!0?setInterval(()=>{},ie):null;let R=null,g=null,v=null,P=!1;function j(e){const t=T.get(I);if(!t)throw new Error("Testkit run lifecycle ownership is unavailable");if(!t.published){X(n,e),t.published=!0;return}re(n,e)}function A(){const e=Z(n,i);return e===null||e.targetIncarnationId!==f.incarnationId||e.targetPid!==u||e.targetProcessIdentity!==f.processIdentity||e.coordinatorPid===D?.coordinatorPid&&e.coordinatorProcessIdentity===D.coordinatorProcessIdentity?null:U(e.coordinatorPid,e.coordinatorProcessIdentity)==="different"?(D=e,null):e}function h(e){k("mutate run state");const t=se(f);e(t),t.heartbeatAt!==null&&(t.heartbeatAt=new Date().toISOString()),j(t),f=t}function k(e){if(g!==null)throw new Error(`Cannot ${e} after Testkit run ${i} finalization is committed`)}function W(){R||(R=setInterval(()=>{},1e3))}function q(){R&&(clearInterval(R),R=null)}function N(){for(const e of S)clearInterval(e);S.clear()}function z(e="controller process exited before Testkit run finalized"){if(P||x(f.status))return!1;const t=_(e,"controller process exited before Testkit run finalized"),o=new Date,l=f.status==="interrupting"||s.signal.aborted?"interrupted":"failed",c=l==="interrupted"?f.interruptReason??v??t:t;return h(p=>{p.status=l,p.finishedAt=o.toISOString(),p.interruptReason=c,p.heartbeatAt!==null&&(p.heartbeatAt=o.toISOString())}),Y(n,{runId:i,reason:c,status:l,finishedAt:o}),!0}const F=()=>{z()};process.on("exit",F);const I={runId:i,manifestPath:a,maintenanceStopSignal:m.signal,signal:s.signal,commitFinalization(e="finished"){if(g!==null)return g;const t=A(),o=s.signal.aborted||t!==null||f.status==="interrupting",l=o?f.interruptReason??v??t?.reason??"run interrupted":null,c=Object.freeze({interruptReason:l,interrupted:o});return h(p=>{p.status=o?"interrupted":e,p.finishedAt=new Date().toISOString(),p.interruptReason=l}),g=c,N(),c},getInterruptReason(){return f.interruptReason??v},isMaintenanceStopCoordinated(){return A()!==null},isStopRequested(){return s.signal.aborted||g?.interrupted===!0},markRunning(){h(e=>{e.status="running"})},markFinished(e="finished"){h(t=>{t.status=e,t.finishedAt=new Date().toISOString()})},heartbeat(e={}){h(t=>{t.heartbeatAt=new Date().toISOString(),e&&typeof e=="object"&&!Array.isArray(e)&&(t.session={...t.session||{},...e})})},startHeartbeat(e=5e3,t=null){k("start a run heartbeat");const o=Number.isFinite(Number(e))&&Number(e)>0?Math.floor(Number(e)):5e3,l=setInterval(()=>{let c;try{c=typeof t=="function"?t():{}}catch{c={}}I.heartbeat(c)},o);return l.unref(),S.add(l),()=>{clearInterval(l),S.delete(l)}},requestStop(e="interrupted"){if(g!==null||x(f.status))return;const t=A(),o=t?t.reason:_(e,"interrupted"),l=!s.signal.aborted;l&&(v=o),W();let c=null;if(l)try{h(d=>{d.status="interrupting",d.interruptReason=o})}catch(d){c=d instanceof Error?d:new Error("Failed to persist the Testkit stop request",{cause:d})}t!==null&&!m.signal.aborted&&m.abort(new Error(`Testkit run ${i} is being stopped by external maintenance`));const p=[...y].map(({supervisor:d})=>d.stop(t===null?void 0:Date.parse(t.requestedAt)+d.shutdownTimeoutMs));if(Promise.allSettled(p).then(q),s.signal.aborted||s.abort(new Error(`testkit run interrupted (${o})`)),!!l&&c!==null)throw c},trackGraphContext(e){h(t=>{le(t.runtimeDirs,e.runtimeDir)})},registerService(e,t,o,l){if(k("register application service ownership"),o.pid!==t.pid||(process.platform==="win32"?o.recovery.processGroupId!==null:o.recovery.processGroupId!==o.pid))throw new Error("Application service supervisor returned an invalid ownership handoff");const c={supervisor:t,unsubscribe:()=>{}};y.add(c);const p=ue(e);h(d=>{d.services=d.services.filter(M=>M.pid!==o.pid),d.services.push({serviceName:e.name,runtimeLabel:e.runtimeLabel,command:e.testkit.local.start||null,cwd:l,pid:o.pid,processGroupId:o.recovery.processGroupId,processIdentity:o.recovery.creationIdentity,ports:p,startedAt:new Date().toISOString(),shutdownTimeoutMs:t.shutdownTimeoutMs,shutdownDeadlineAt:t.shutdownDeadlineAt})}),c.unsubscribe=t.onShutdownStarted(d=>{h(M=>{const E=M.services.find(V=>V.pid===o.pid);E&&(E.shutdownDeadlineAt=E.shutdownDeadlineAt==null?d:Math.min(E.shutdownDeadlineAt,d))})})},unregisterService(e){k("unregister application service ownership");for(const t of y)t.supervisor.pid===e&&(t.unsubscribe(),y.delete(t));h(t=>{t.services=t.services.filter(o=>o.pid!==e)})},installSignalHandlers(){if(!(O.length>0)&&!(u!==process.pid||U(u,f.processIdentity)!=="matches")){for(const e of["SIGINT","SIGTERM","SIGHUP"]){const t=I.requestStop.bind(I,e);process.on(e,t),O.push([e,t])}process.platform==="win32"&&(w=setInterval(()=>{const e=A();e!==null&&(clearInterval(w),I.requestStop(e.reason))},100),w.unref(),S.add(w))}},removeSignalHandlers(){w&&(clearInterval(w),S.delete(w),w=void 0);for(const[e,t]of O.splice(0))process.off(e,t)},async removeManifest(){await Q(n,e=>cleanupOwnedRunLifecycleWithinLifecycleFence(e,I))},finalizeForControllerExit:z,dispose(){P=!0;for(const e of y)e.unsubscribe();process.off("exit",F),I.removeSignalHandlers(),L&&clearInterval(L),q(),N()}};return T.set(I,{incarnationId:f.incarnationId,pid:u,processIdentity:f.processIdentity,productDir:n,published:!1,runId:i}),I}function se(n){const r={...n,controller:n.controller===null?null:{...n.controller},runtimeDirs:[...n.runtimeDirs],services:n.services.map(i=>({...i,ports:i.ports.map(a=>({...a}))}))};return n.session!==void 0&&(r.session=n.session===null?null:{...n.session}),r}function ae(n){return!n||typeof n!="object"||Array.isArray(n)?null:Object.fromEntries(Object.entries(n).filter(([,r])=>r!==void 0).map(([r,i])=>[r,i===null?null:oe(i)]))}export function cleanupOwnedRunLifecycleWithinLifecycleFence(n,r){const{manifest:i,ownership:a,productDir:s}=readOwnedRunLifecycleWithinLifecycleFence(n,r);if(!i)return!1;if(i.services.length>0)throw new Error(`Cannot clean Testkit run ${r.runId} service ownership while holding the lifecycle fence`);return G(s,r.runId),a.published=!1,!0}export function readOwnedRunLifecycleWithinLifecycleFence(n,r){const i=K(n),a=T.get(r);if(!a||a.productDir!==i)throw new Error("Cannot clean a Testkit run without matching lifecycle ownership");if(a.runId!==r.runId)throw new Error("Testkit run lifecycle identity has been modified");const s=$(i).find(m=>m.runId===r.runId);if(s!==void 0&&(s.incarnationId!==a.incarnationId||s.pid!==a.pid||s.processIdentity!==a.processIdentity))throw new Error(`Testkit run ${r.runId} ownership no longer matches its manifest`);return{manifest:s??null,ownership:a,productDir:i}}export function findPortOwner(n,{host:r,port:i}){const a=$(n);for(const s of a)for(const m of s.services)for(const u of m.ports)if(C(u.host)===C(r)&&u.port===Number(i))return{manifest:s,service:m,active:B(s)};return null}function ue(n){const r=new Set,i=[];for(const a of[n.testkit.local.baseUrl,n.testkit.local.readyUrl])if(a)try{const s=new URL(a),m=C(s.hostname),u=Number(s.port||(s.protocol==="https:"?443:80)),b=`${m}:${u.toString()}`;if(r.has(b))continue;r.add(b),i.push({host:m,port:u})}catch{}return i}function le(n,r){r&&(n.includes(r)||n.push(r))}function ce(){return`${Date.now().toString()}-${process.pid.toString()}-${H.randomBytes(4).toString("hex")}`}function C(n){return n?n==="::1"?"127.0.0.1":n:"127.0.0.1"}export function disposeFailedRunLifecycle(n){const r=T.get(n);n.dispose(),n.removeSignalHandlers(),r?.published===!0&&G(r.productDir,n.runId)}
1
+ import H from"crypto";import{isTerminalRunStatus as N}from"../process/runs/schema.js";import{observeProcessCreationIdentity as x}from"../process/process-identity.js";export{isRunManifestActive}from"../process/runs/inspection.js";import{normalizeOperationReason as U}from"../domain/operation-reason.js";import{finalizeLiveRunArtifact as W}from"../evidence/index.js";import{captureProcessCreationIdentity as V}from"../process/service-processes.js";import{requireProductLifecycleFenceProductDir as B,withProductLifecycleFence as Y}from"../process/lifecycle-fence.js";import{createRunManifest as J,listRunManifests as K,readRunMaintenanceStopRequest as Q,removeRunManifestFile as _,requireSafeRunId as X,RUN_MANIFEST_SCHEMA_VERSION as Z,runManifestPath as ee,writeRunManifest as te}from"../process/runs/manifest.js";const ne=1e3,re=String,k=new WeakMap;export function createUnpublishedRunLifecycle(n,r={}){const s=r.runId===void 0?ue():X(r.runId,"Testkit run id"),u=ee(n,s),o=new AbortController,I=new AbortController,d=r.pid===void 0?process.pid:r.pid;if(!Number.isSafeInteger(d)||d<0)throw new Error("Testkit run pid must be a non-negative safe integer");const S=d>0?V(d):null;if(d>0&&S===null)throw new Error(`Could not capture the Testkit run process identity for pid ${d.toString()}`);let f={incarnationId:H.randomUUID(),schemaVersion:Z,runId:s,pid:d,processIdentity:typeof S=="string"?S:null,controller:se(r.controller),status:"starting",startedAt:new Date().toISOString(),heartbeatAt:r.heartbeat===!1?null:new Date().toISOString(),interruptReason:null,services:[],runtimeDirs:[]};const D=[];let w,O=null;const y=new Set,b=new Set,M=r.keepAlive===!0?setInterval(()=>{},ne):null;let R=null,g=null,v=null,L=!1;function $(e){const t=k.get(h);if(!t)throw new Error("Testkit run lifecycle ownership is unavailable");if(!t.published){J(n,e),t.published=!0;return}te(n,e)}function A(){const e=Q(n,s);return e===null||e.targetIncarnationId!==f.incarnationId||e.targetPid!==d||e.targetProcessIdentity!==f.processIdentity||e.coordinatorPid===O?.coordinatorPid&&e.coordinatorProcessIdentity===O.coordinatorProcessIdentity?null:x(e.coordinatorPid,e.coordinatorProcessIdentity)==="different"?(O=e,null):e}function m(e){E("mutate run state");const t=ie(f);e(t),t.heartbeatAt!==null&&(t.heartbeatAt=new Date().toISOString()),$(t),f=t}function E(e){if(g!==null)throw new Error(`Cannot ${e} after Testkit run ${s} finalization is committed`)}function G(){R||(R=setInterval(()=>{},1e3))}function P(){R&&(clearInterval(R),R=null)}function q(){for(const e of b)clearInterval(e);b.clear()}function z(e="controller process exited before Testkit run finalized"){if(L||N(f.status))return!1;const t=U(e,"controller process exited before Testkit run finalized"),i=new Date,a=f.status==="interrupting"||o.signal.aborted?"interrupted":"failed",l=a==="interrupted"?f.interruptReason??v??t:t;return m(p=>{p.status=a,p.finishedAt=i.toISOString(),p.interruptReason=l,p.heartbeatAt!==null&&(p.heartbeatAt=i.toISOString())}),W(n,{runId:s,reason:l,status:a,finishedAt:i}),!0}const F=()=>{z()};process.on("exit",F);const h={runId:s,manifestPath:u,maintenanceStopSignal:I.signal,signal:o.signal,commitFinalization(e="finished"){if(g!==null)return g;const t=A(),i=o.signal.aborted||t!==null||f.status==="interrupting",a=i?f.interruptReason??v??t?.reason??"run interrupted":null,l=Object.freeze({interruptReason:a,interrupted:i});return m(p=>{p.status=i?"interrupted":e,p.finishedAt=new Date().toISOString(),p.interruptReason=a}),g=l,q(),l},getInterruptReason(){return f.interruptReason??v},isMaintenanceStopCoordinated(){return A()!==null},isStopRequested(){return o.signal.aborted||g?.interrupted===!0},markRunning(){m(e=>{e.status="running"})},markFinished(e="finished"){m(t=>{t.status=e,t.finishedAt=new Date().toISOString()})},heartbeat(e={}){m(t=>{t.heartbeatAt=new Date().toISOString(),e&&typeof e=="object"&&!Array.isArray(e)&&(t.session={...t.session||{},...e})})},startHeartbeat(e=5e3,t=null){E("start a run heartbeat");const i=Number.isFinite(Number(e))&&Number(e)>0?Math.floor(Number(e)):5e3,a=setInterval(()=>{let l;try{l=typeof t=="function"?t():{}}catch{l={}}h.heartbeat(l)},i);return a.unref(),b.add(a),()=>{clearInterval(a),b.delete(a)}},requestStop(e="interrupted"){if(g!==null||N(f.status))return;const t=A(),i=t?t.reason:U(e,"interrupted"),a=!o.signal.aborted;a&&(v=i),G();let l=null;if(a)try{m(c=>{c.status="interrupting",c.interruptReason=i})}catch(c){l=c instanceof Error?c:new Error("Failed to persist the Testkit stop request",{cause:c})}t!==null&&!I.signal.aborted&&I.abort(new Error(`Testkit run ${s} is being stopped by external maintenance`));const p=[...y].map(({supervisor:c})=>c.stop(t===null?void 0:Date.parse(t.requestedAt)+c.shutdownTimeoutMs));if(Promise.allSettled(p).then(P),o.signal.aborted||o.abort(new Error(`testkit run interrupted (${i})`)),!!a&&l!==null)throw l},trackGraphContext(e){m(t=>{ae(t.runtimeDirs,e.runtimeDir)})},registerService(e,t,i,a){if(E("register application service ownership"),i.pid!==t.pid||(process.platform==="win32"?i.recovery.processGroupId!==null:i.recovery.processGroupId!==i.pid))throw new Error("Application service supervisor returned an invalid ownership handoff");const l={supervisor:t,unsubscribe:()=>{}};y.add(l);const p=oe(e);m(c=>{c.services=c.services.filter(C=>C.pid!==i.pid),c.services.push({serviceName:e.name,runtimeLabel:e.runtimeLabel,command:e.testkit.local.start||null,cwd:a,pid:i.pid,processGroupId:i.recovery.processGroupId,processIdentity:i.recovery.creationIdentity,ports:p,startedAt:new Date().toISOString(),shutdownTimeoutMs:t.shutdownTimeoutMs,shutdownDeadlineAt:t.shutdownDeadlineAt})}),l.unsubscribe=t.onShutdownStarted(c=>{m(C=>{const T=C.services.find(j=>j.pid===i.pid);T&&(T.shutdownDeadlineAt=T.shutdownDeadlineAt==null?c:Math.min(T.shutdownDeadlineAt,c))})})},unregisterService(e){E("unregister application service ownership");for(const t of y)t.supervisor.pid===e&&(t.unsubscribe(),y.delete(t));m(t=>{t.services=t.services.filter(i=>i.pid!==e)})},installSignalHandlers(){if(!(D.length>0)&&!(d!==process.pid||x(d,f.processIdentity)!=="matches")){for(const e of["SIGINT","SIGTERM","SIGHUP"]){const t=h.requestStop.bind(h,e);process.on(e,t),D.push([e,t])}process.platform==="win32"&&(w=setInterval(()=>{const e=A();e!==null&&(clearInterval(w),h.requestStop(e.reason))},100),w.unref(),b.add(w))}},removeSignalHandlers(){w&&(clearInterval(w),b.delete(w),w=void 0);for(const[e,t]of D.splice(0))process.off(e,t)},async removeManifest(){await Y(n,e=>cleanupOwnedRunLifecycleWithinLifecycleFence(e,h))},finalizeForControllerExit:z,dispose(){L=!0;for(const e of y)e.unsubscribe();process.off("exit",F),h.removeSignalHandlers(),M&&clearInterval(M),P(),q()}};return k.set(h,{incarnationId:f.incarnationId,pid:d,processIdentity:f.processIdentity,productDir:n,published:!1,runId:s}),h}function ie(n){const r={...n,controller:n.controller===null?null:{...n.controller},runtimeDirs:[...n.runtimeDirs],services:n.services.map(s=>({...s,ports:s.ports.map(u=>({...u}))}))};return n.session!==void 0&&(r.session=n.session===null?null:{...n.session}),r}function se(n){return!n||typeof n!="object"||Array.isArray(n)?null:Object.fromEntries(Object.entries(n).filter(([,r])=>r!==void 0).map(([r,s])=>[r,s===null?null:re(s)]))}export function cleanupOwnedRunLifecycleWithinLifecycleFence(n,r){const{manifest:s,ownership:u,productDir:o}=readOwnedRunLifecycleWithinLifecycleFence(n,r);if(!s)return!1;if(s.services.length>0)throw new Error(`Cannot clean Testkit run ${r.runId} service ownership while holding the lifecycle fence`);return _(o,r.runId),u.published=!1,!0}export function readOwnedRunLifecycleWithinLifecycleFence(n,r){const s=B(n),u=k.get(r);if(!u||u.productDir!==s)throw new Error("Cannot clean a Testkit run without matching lifecycle ownership");if(u.runId!==r.runId)throw new Error("Testkit run lifecycle identity has been modified");const o=K(s).find(I=>I.runId===r.runId);if(o!==void 0&&(o.incarnationId!==u.incarnationId||o.pid!==u.pid||o.processIdentity!==u.processIdentity))throw new Error(`Testkit run ${r.runId} ownership no longer matches its manifest`);return{manifest:o??null,ownership:u,productDir:s}}function oe(n){const r=new Set,s=[];for(const u of[n.testkit.local.baseUrl,n.testkit.local.readyUrl])if(u)try{const o=new URL(u),I=le(o.hostname),d=Number(o.port||(o.protocol==="https:"?443:80)),S=`${I}:${d.toString()}`;if(r.has(S))continue;r.add(S),s.push({host:I,port:d})}catch{}return s}function ae(n,r){r&&(n.includes(r)||n.push(r))}function ue(){return`${Date.now().toString()}-${process.pid.toString()}-${H.randomBytes(4).toString("hex")}`}function le(n){return n?n==="::1"?"127.0.0.1":n:"127.0.0.1"}export function disposeFailedRunLifecycle(n){const r=k.get(n);n.dispose(),n.removeSignalHandlers(),r?.published===!0&&_(r.productDir,n.runId)}
@@ -8,22 +8,25 @@ export interface RunCleanupSummary {
8
8
  skippedActive: RunManifest[];
9
9
  stoppedActive: RunManifest[];
10
10
  }
11
- interface RunCleanupOptions {
11
+ export interface LocalInfrastructureMaintenance {
12
+ localInfrastructure?: boolean;
13
+ }
14
+ interface RunCleanupOptions extends LocalInfrastructureMaintenance {
12
15
  includeActive?: boolean;
13
16
  }
14
- export interface RunCleanupWithinLifecycleFenceOptions {
17
+ export interface RunCleanupWithinLifecycleFenceOptions extends LocalInfrastructureMaintenance {
15
18
  protectedLiveRunIds?: ReadonlySet<string>;
16
19
  }
17
- export declare function startRunLifecycle(productDir: string, options?: Readonly<RunLifecycleOptions>): Promise<RunLifecycle>;
18
- export declare function startRunLifecycleWithinLifecycleFence(fence: Readonly<ProductLifecycleFence>, options?: Readonly<RunLifecycleOptions>): Promise<RunLifecycle>;
19
- export declare function cleanupRuns(productDir: string, { includeActive }?: Readonly<RunCleanupOptions>): Promise<RunCleanupSummary>;
20
- export declare function cleanupRunsWithinLifecycleFence(fence: Readonly<ProductLifecycleFence>, { protectedLiveRunIds }?: Readonly<RunCleanupWithinLifecycleFenceOptions>): Promise<RunCleanupSummary>;
21
- export declare function cleanupStaleRuns(productDir: string): Promise<RunManifest[]>;
22
- export declare function cleanupStaleRunsWithinLifecycleFence(fence: Readonly<ProductLifecycleFence>): Promise<RunManifest[]>;
23
- export declare function cleanupOwnedRunLifecycleResourcesWithinLifecycleFence(fence: Readonly<ProductLifecycleFence>, lifecycle: Readonly<RunLifecycle>): Promise<boolean>;
20
+ export declare function startRunLifecycle(productDir: string, options?: Readonly<RunLifecycleOptions>, maintenance?: Readonly<LocalInfrastructureMaintenance>): Promise<RunLifecycle>;
21
+ export declare function startRunLifecycleWithinLifecycleFence(fence: Readonly<ProductLifecycleFence>, options?: Readonly<RunLifecycleOptions>, maintenance?: Readonly<LocalInfrastructureMaintenance>): Promise<RunLifecycle>;
22
+ export declare function cleanupRuns(productDir: string, { includeActive, localInfrastructure }?: Readonly<RunCleanupOptions>): Promise<RunCleanupSummary>;
23
+ export declare function cleanupRunsWithinLifecycleFence(fence: Readonly<ProductLifecycleFence>, { protectedLiveRunIds, localInfrastructure, }?: Readonly<RunCleanupWithinLifecycleFenceOptions>): Promise<RunCleanupSummary>;
24
+ export declare function cleanupStaleRuns(productDir: string, maintenance?: Readonly<LocalInfrastructureMaintenance>): Promise<RunManifest[]>;
25
+ export declare function cleanupStaleRunsWithinLifecycleFence(fence: Readonly<ProductLifecycleFence>, maintenance?: Readonly<LocalInfrastructureMaintenance>): Promise<RunManifest[]>;
26
+ export declare function cleanupOwnedRunLifecycleResourcesWithinLifecycleFence(fence: Readonly<ProductLifecycleFence>, lifecycle: Readonly<RunLifecycle>, { localInfrastructure }?: Readonly<LocalInfrastructureMaintenance>): Promise<boolean>;
24
27
  export { listRunManifests, RunManifestInventoryError, type RunManifest, type RunManifestController, type RunManifestPort, type RunManifestService, } from "../process/runs/manifest.js";
25
28
  export { stopRunById, stopRunManifest, type StoppedRun } from "./run-stopping.js";
26
- export { cleanupOwnedRunLifecycleWithinLifecycleFence, findPortOwner, isRunManifestActive, } from "./lifecycle-controller.js";
27
- export type { RunLifecycleOptions, RunPortQuery, RunPortOwner, RunLifecycleFinalizationState, RunLifecycle, } from "./lifecycle-controller.js";
29
+ export { cleanupOwnedRunLifecycleWithinLifecycleFence, isRunManifestActive } from "./lifecycle-controller.js";
30
+ export type { RunLifecycleOptions, RunLifecycleFinalizationState, RunLifecycle } from "./lifecycle-controller.js";
28
31
  export { isPidRunning } from "../process/runs/stop.js";
29
32
  export { formatRunSummary } from "../process/runs/manifest.js";
@@ -1 +1 @@
1
- import{formatRunSummary as w}from"../process/runs/manifest.js";import{cleanupOrphanedLocalInfrastructure as R,cleanupTerminalRunLocalInfrastructure as m}from"../database/lifecycle-cleanup.js";import{resetResultArtifacts as v}from"../evidence/index.js";import{requireProductLifecycleFenceProductDir as d,withProductLifecycleFence as l}from"../process/lifecycle-fence.js";import{listRunManifests as u,removeRunManifestFile as y,requireSafeRunId as I}from"../process/runs/manifest.js";import{stopRunManifest as A}from"./run-stopping.js";import{createUnpublishedRunLifecycle as L,disposeFailedRunLifecycle as k,isRunManifestActive as a,cleanupOwnedRunLifecycleWithinLifecycleFence as S,readOwnedRunLifecycleWithinLifecycleFence as p}from"./lifecycle-controller.js";import{reconcileInactiveRunArtifact as g,reconcileOrphanedLiveRunArtifact as x}from"./lifecycle-artifacts.js";class f extends Error{constructor(n){super(`Stale Testkit run ${n} still owns application services and requires phased cleanup`),this.name="StaleRunServiceCleanupRequiredError"}}export async function startRunLifecycle(e,n={}){for(;;){await cleanupStaleRuns(e);try{return await l(e,t=>startRunLifecycleWithinLifecycleFence(t,n))}catch(t){if(!(t instanceof f))throw t}}}export async function startRunLifecycleWithinLifecycleFence(e,n={}){const t=d(e);h(t),await cleanupStaleRunsWithinLifecycleFence(e);const c=u(t).find(i=>!a(i)&&i.services.length>0);if(c!==void 0)throw new f(c.runId);h(t);const r=L(t,n);try{return r.installSignalHandlers(),n.resetResultsBeforePublish===!0&&v(t),r.markRunning(),r}catch(i){throw k(r),i}}function h(e){const n=u(e).find(a);if(n)throw new Error(`Active Testkit run ${w(n)} already owns this product`)}export async function cleanupRuns(e,{includeActive:n=!1}={}){const t=[],c=[];for(;;){const i=await l(e,()=>u(e).flatMap(s=>{const o=a(s);return n&&o||!o&&s.services.length>0?[{active:o,manifest:s}]:[]}));if(i.length===0)break;for(const s of i)await A(e,s.manifest,{reason:s.active?"cleanup":"stale run process not active"}),s.active?t.push(s.manifest):c.push(s.manifest)}const r=await l(e,i=>cleanupRunsWithinLifecycleFence(i));return{...r,cleaned:[...t,...c,...r.cleaned],stoppedActive:t}}export async function cleanupRunsWithinLifecycleFence(e,{protectedLiveRunIds:n=new Set}={}){const t=d(e),c=u(t),r={cleaned:[],skippedActive:[],stoppedActive:[]};for(const i of c){if(a(i)||i.services.length>0){r.skippedActive.push(i);continue}g(t,i),y(t,O(i)),r.cleaned.push(i)}return x(t,{activeRunIds:new Set([...r.skippedActive.map(i=>i.runId).filter(i=>typeof i=="string"),...n])}),r.resourceCleanup=await R(t),{cleaned:r.cleaned,resourceCleanup:r.resourceCleanup,skippedActive:r.skippedActive,stoppedActive:r.stoppedActive}}export async function cleanupStaleRuns(e){const n=[];for(;;){const t=await cleanupRuns(e);if(n.push(...t.cleaned),!t.skippedActive.some(r=>!a(r)&&r.services.length>0))return n}}export async function cleanupStaleRunsWithinLifecycleFence(e){return(await cleanupRunsWithinLifecycleFence(e)).cleaned}export async function cleanupOwnedRunLifecycleResourcesWithinLifecycleFence(e,n){const t=p(e,n);if(t.manifest===null)throw new Error(`Cannot clean terminal Testkit run ${n.runId} resources without its owned manifest`);if(t.manifest.services.length>0)throw new Error(`Cannot clean terminal Testkit run ${n.runId} resources while service ownership remains published`);const c=C(t.manifest);await m(e,c);const r=p(e,n);if(r.manifest===null||!F(r.manifest,c))throw new Error(`Cannot remove terminal Testkit run ${n.runId} because its manifest changed during resource cleanup`);return S(e,n)}function C(e){if(e.status!=="failed"&&e.status!=="finished"&&e.status!=="interrupted")throw new Error(`Cannot clean Testkit run ${e.runId} database resources before terminal finalization`);if(typeof e.finishedAt!="string"||e.finishedAt.length===0)throw new Error(`Cannot clean terminal Testkit run ${e.runId} database resources without a completion time`);return{finishedAt:e.finishedAt,incarnationId:e.incarnationId,pid:e.pid,processIdentity:e.processIdentity,runId:e.runId,status:e.status}}function F(e,n){return e.runId===n.runId&&e.incarnationId===n.incarnationId&&e.pid===n.pid&&e.processIdentity===n.processIdentity&&e.finishedAt===n.finishedAt&&e.status===n.status&&e.services.length===0}function O(e){return I(e.runId,"Testkit run manifest runId")}export{listRunManifests,RunManifestInventoryError}from"../process/runs/manifest.js";export{stopRunById,stopRunManifest}from"./run-stopping.js";export{cleanupOwnedRunLifecycleWithinLifecycleFence,findPortOwner,isRunManifestActive}from"./lifecycle-controller.js";export{isPidRunning}from"../process/runs/stop.js";export{formatRunSummary}from"../process/runs/manifest.js";
1
+ import{formatRunSummary as R}from"../process/runs/manifest.js";import{cleanupOrphanedLocalInfrastructure as v,cleanupTerminalRunLocalInfrastructure as m}from"../database/lifecycle-cleanup.js";import{resetResultArtifacts as y}from"../evidence/index.js";import{requireProductLifecycleFenceProductDir as p,withProductLifecycleFence as l}from"../process/lifecycle-fence.js";import{listRunManifests as o,removeRunManifestFile as I,requireSafeRunId as A}from"../process/runs/manifest.js";import{stopRunManifest as L}from"./run-stopping.js";import{createUnpublishedRunLifecycle as k,disposeFailedRunLifecycle as g,isRunManifestActive as u,cleanupOwnedRunLifecycleWithinLifecycleFence as S,readOwnedRunLifecycleWithinLifecycleFence as f}from"./lifecycle-controller.js";import{reconcileInactiveRunArtifact as x,reconcileOrphanedLiveRunArtifact as C}from"./lifecycle-artifacts.js";class h extends Error{constructor(n){super(`Stale Testkit run ${n} still owns application services and requires phased cleanup`),this.name="StaleRunServiceCleanupRequiredError"}}export async function startRunLifecycle(e,n={},c={}){for(;;){await cleanupStaleRuns(e,c);try{return await l(e,t=>startRunLifecycleWithinLifecycleFence(t,n,c))}catch(t){if(!(t instanceof h))throw t}}}export async function startRunLifecycleWithinLifecycleFence(e,n={},c={}){const t=p(e);w(t),await cleanupStaleRunsWithinLifecycleFence(e,c);const s=o(t).find(i=>!u(i)&&i.services.length>0);if(s!==void 0)throw new h(s.runId);w(t);const r=k(t,n);try{return r.installSignalHandlers(),n.resetResultsBeforePublish===!0&&y(t),r.markRunning(),r}catch(i){throw g(r),i}}function w(e){const n=o(e).find(u);if(n)throw new Error(`Active Testkit run ${R(n)} already owns this product`)}export async function cleanupRuns(e,{includeActive:n=!1,localInfrastructure:c}={}){const t=[],s=[];for(;;){const i=await l(e,()=>o(e).flatMap(a=>{const d=u(a);return n&&d||!d&&a.services.length>0?[{active:d,manifest:a}]:[]}));if(i.length===0)break;for(const a of i)await L(e,a.manifest,{reason:a.active?"cleanup":"stale run process not active"}),a.active?t.push(a.manifest):s.push(a.manifest)}const r=await l(e,i=>cleanupRunsWithinLifecycleFence(i,{localInfrastructure:c}));return{...r,cleaned:[...t,...s,...r.cleaned],stoppedActive:t}}export async function cleanupRunsWithinLifecycleFence(e,{protectedLiveRunIds:n=new Set,localInfrastructure:c=!0}={}){const t=p(e),s=o(t),r={cleaned:[],skippedActive:[],stoppedActive:[]};for(const i of s){if(u(i)||i.services.length>0){r.skippedActive.push(i);continue}x(t,i),I(t,E(i)),r.cleaned.push(i)}return C(t,{activeRunIds:new Set([...r.skippedActive.map(i=>i.runId).filter(i=>typeof i=="string"),...n])}),r.resourceCleanup=c?await v(t):{kept:[],removed:[],skipped:[],stopped:[],targets:[]},{cleaned:r.cleaned,resourceCleanup:r.resourceCleanup,skippedActive:r.skippedActive,stoppedActive:r.stoppedActive}}export async function cleanupStaleRuns(e,n={}){const c=[];for(;;){const t=await cleanupRuns(e,n);if(c.push(...t.cleaned),!t.skippedActive.some(r=>!u(r)&&r.services.length>0))return c}}export async function cleanupStaleRunsWithinLifecycleFence(e,n={}){return(await cleanupRunsWithinLifecycleFence(e,n)).cleaned}export async function cleanupOwnedRunLifecycleResourcesWithinLifecycleFence(e,n,{localInfrastructure:c=!0}={}){const t=f(e,n);if(t.manifest===null)throw new Error(`Cannot clean terminal Testkit run ${n.runId} resources without its owned manifest`);if(t.manifest.services.length>0)throw new Error(`Cannot clean terminal Testkit run ${n.runId} resources while service ownership remains published`);const s=F(t.manifest);c&&await m(e,s);const r=f(e,n);if(r.manifest===null||!b(r.manifest,s))throw new Error(`Cannot remove terminal Testkit run ${n.runId} because its manifest changed during resource cleanup`);return S(e,n)}function F(e){if(e.status!=="failed"&&e.status!=="finished"&&e.status!=="interrupted")throw new Error(`Cannot clean Testkit run ${e.runId} database resources before terminal finalization`);if(typeof e.finishedAt!="string"||e.finishedAt.length===0)throw new Error(`Cannot clean terminal Testkit run ${e.runId} database resources without a completion time`);return{finishedAt:e.finishedAt,incarnationId:e.incarnationId,pid:e.pid,processIdentity:e.processIdentity,runId:e.runId,status:e.status}}function b(e,n){return e.runId===n.runId&&e.incarnationId===n.incarnationId&&e.pid===n.pid&&e.processIdentity===n.processIdentity&&e.finishedAt===n.finishedAt&&e.status===n.status&&e.services.length===0}function E(e){return A(e.runId,"Testkit run manifest runId")}export{listRunManifests,RunManifestInventoryError}from"../process/runs/manifest.js";export{stopRunById,stopRunManifest}from"./run-stopping.js";export{cleanupOwnedRunLifecycleWithinLifecycleFence,isRunManifestActive}from"./lifecycle-controller.js";export{isPidRunning}from"../process/runs/stop.js";export{formatRunSummary}from"../process/runs/manifest.js";
@@ -19,8 +19,6 @@ export interface RuntimeGraph<Config extends RuntimeServiceSpec = RuntimeService
19
19
  instanceCount: number;
20
20
  key: string;
21
21
  maxConcurrentTasks: number;
22
- portNamespaceIndex: number;
23
- portNamespaceStride: number;
24
22
  resourceReservations: RuntimeDatabaseReservation[];
25
23
  runtimeConfigs: readonly Config[];
26
24
  runtimeNames: readonly string[];
@@ -0,0 +1,10 @@
1
+ export declare const SERVICE_START_ATTEMPTS = 3;
2
+ export interface PortReservation {
3
+ readonly port: number;
4
+ release(): Promise<void>;
5
+ }
6
+ export declare function reservePort(): Promise<PortReservation>;
7
+ export declare function reserveExactPort(port: number): Promise<null | PortReservation>;
8
+ export declare function reservePorts(serviceNames: Iterable<string>): Promise<Map<string, PortReservation>>;
9
+ export declare function releasePorts(reservations: Iterable<PortReservation>): Promise<void>;
10
+ export declare function portsOf(reservations: ReadonlyMap<string, PortReservation>): Map<string, number>;
@@ -0,0 +1 @@
1
+ import l from"node:net";export const SERVICE_START_ATTEMPTS=3;const f=32,p=new Set(["EADDRNOTAVAIL","EAFNOSUPPORT","EPROTONOSUPPORT"]);export async function reservePort(){for(let e=0;e<f;e+=1){const r=await c("127.0.0.1",0),t=await i(r);if(t)return t}throw new Error("The OS assigned no TCP port that is free on both IPv4 and IPv6 loopback")}export async function reserveExactPort(e){let r;try{r=await c("127.0.0.1",e)}catch(t){if(s(t)==="EADDRINUSE"||s(t)==="EACCES")return null;throw t}return i(r)}export async function reservePorts(e){const r=new Map;try{for(const t of e)r.set(t,await reservePort())}catch(t){throw await releasePorts(r.values()),t}return r}export async function releasePorts(e){await Promise.all([...e].map(r=>r.release()))}export function portsOf(e){return new Map([...e].map(([r,t])=>[r,t.port]))}async function i(e){const r=P(e);let t=null;try{t=await c("::1",r)}catch(a){if(!p.has(s(a)??"")){if(await u(e),s(a)==="EADDRINUSE")return null;throw a}}const n=t?[e,t]:[e];let o=null;return{port:r,release(){return o??=Promise.all(n.map(u)).then(()=>{}),o}}}function c(e,r){const t=l.createServer(n=>{n.destroy()});return t.unref(),new Promise((n,o)=>{t.once("error",o),t.listen({host:e,port:r,exclusive:!0,...e==="::1"?{ipv6Only:!0}:{}},()=>{t.off("error",o),n(t)})})}function u(e){return new Promise(r=>{e.close(()=>{r()})})}function P(e){const r=e.address();if(r===null||typeof r=="string")throw new Error("The OS did not assign a TCP port");return r.port}function s(e){const r=typeof e=="object"&&e!==null?Reflect.get(e,"code"):void 0;return typeof r=="string"?r:void 0}
@@ -1,5 +1,5 @@
1
1
  import { type DatabaseInfo } from "../database/state.js";
2
- import { type RuntimeInstanceContext } from "./runtime-contexts.js";
2
+ import type { RuntimeInstanceContext } from "./runtime-contexts.js";
3
3
  import type { ReadyServiceConfig } from "./runtime-service.js";
4
4
  export interface ReadyService {
5
5
  readonly baseUrl: string | null;
@@ -7,13 +7,10 @@ export interface ReadyService {
7
7
  readonly database: Readonly<DatabaseInfo> | null;
8
8
  }
9
9
  export interface ReadyEnvironment {
10
- readonly engineCacheDirectory?: string;
11
- readonly engineWorkspaceDirectory?: string;
12
10
  readonly graphKey: string;
13
- readonly leaseDir: string;
14
11
  readonly leaseId: string;
15
12
  readonly runtimeId: string;
16
13
  readonly services: ReadonlyMap<string, ReadyService>;
17
14
  readonly targetNames: readonly string[];
18
15
  }
19
- export declare function createReadyEnvironment(context: RuntimeInstanceContext, lease: Pick<ReadyEnvironment, "leaseDir" | "leaseId">): ReadyEnvironment;
16
+ export declare function createReadyEnvironment(context: RuntimeInstanceContext, leaseId: string): ReadyEnvironment;
@@ -1 +1 @@
1
- import i from"node:path";import{readDatabaseInfo as m}from"../database/state.js";import{runtimeEngineDirectory as s}from"./runtime-contexts.js";export function createReadyEnvironment(t,a){const r=new Map(t.runtimeConfigs.map(e=>[e.name,m(e.stateDir)]));return{...a,engineCacheDirectory:i.join(s(t),"cache"),engineWorkspaceDirectory:i.join(s(t),"leases",a.leaseId),graphKey:t.graphKey,runtimeId:t.runtimeId,targetNames:[...t.targetNames],services:new Map(t.runtimeConfigs.map(e=>{const n={...e.testkit.templateContext};return delete n.databaseBindingsReader,[e.name,{baseUrl:e.testkit.local?.baseUrl||null,database:r.get(e.name)??null,config:{...e,testkit:{...e.testkit,templateContext:{...n,databaseBindingsByService:r}}}}]}))}}
1
+ import{readDatabaseInfo as s}from"../database/state.js";export function createReadyEnvironment(t,n){const a=new Map(t.runtimeConfigs.map(e=>[e.name,s(e.stateDir)]));return{leaseId:n,graphKey:t.graphKey,runtimeId:t.runtimeId,targetNames:[...t.targetNames],services:new Map(t.runtimeConfigs.map(e=>{const r={...e.testkit.templateContext};return delete r.databaseBindingsReader,[e.name,{baseUrl:e.testkit.local?.baseUrl||null,database:a.get(e.name)??null,config:{...e,testkit:{...e.testkit,templateContext:{...r,databaseBindingsByService:a}}}}]}))}}
@@ -9,6 +9,7 @@ export interface RuntimeGraphIdentity {
9
9
  export interface RunProvisioningOptions {
10
10
  heartbeat?: boolean;
11
11
  keepAlive?: boolean;
12
+ localInfrastructure?: boolean;
12
13
  resetResultsBeforePublish?: boolean;
13
14
  runId?: string;
14
15
  }
@@ -1 +1 @@
1
- import{cleanupOwnedRunLifecycleResourcesWithinLifecycleFence as R,cleanupRunsWithinLifecycleFence as v,listRunManifests as F,startRunLifecycle as q}from"./lifecycle.js";import{createRuntimeManager as I}from"./runtime-manager.js";import{resolveCommittedRunFinalization as S}from"../evidence/finalization.js";import{withProductLifecycleFence as b}from"../process/lifecycle-fence.js";export function createLocalRunEnvironment(s){return{async start(d){const n=await q(s,d),c=n.startHeartbeat(2e3,()=>({kind:"controller",status:n.isStopRequested()?"interrupting":"running"}));let i=null;const p=new Map,l=()=>{if(i===null)throw new Error("Run runtimes have not been provisioned");return i};let m=null;return{runId:n.runId,signal:n.signal,isStopRequested:()=>n.isStopRequested(),getInterruptReason:()=>n.getInterruptReason(),requestStop:e=>{n.requestStop(e)},beginFinalization:c,dispose(){c(),n.removeSignalHandlers(),n.dispose()},get committedFinalization(){return m},provision(e,t){if(i!==null)throw new Error("Run runtimes are already provisioned");i=I({productDir:s,graphs:e,lifecycle:n,runtimeOptions:t})},async acquire(e,t){const r=await l().acquire(e,t);p.set(r.leaseId,r);const{engineCacheDirectory:o,engineWorkspaceDirectory:u,leaseId:a,leaseDir:h,runtimeId:g,graphKey:f,targetNames:w,services:y}=r;return{engineCacheDirectory:o,engineWorkspaceDirectory:u,leaseId:a,leaseDir:h,runtimeId:g,graphKey:f,targetNames:w,services:y}},canAcquire:e=>l().canAcquire(e),async release(e,t){const r=p.get(e);r!==void 0&&(await l().release(r,t),p.delete(e))},async cleanupAll(){await i?.cleanupAll()},getStats:()=>i?.getStats()??[],getGraph(e){const t=i?.getGraph(e);return t?{key:t.key,targetNames:[...t.targetNames]}:null},getGraphFailure:e=>i?.getGraphFailure(e)??null,markGraphFailed:(e,t)=>{l().markGraphFailed(e,t)},async finish(e,t,r){return c(),b(s,async o=>{const u=F(s).find(a=>a.runId===n.runId);if(u!==void 0&&u.services.length>0)throw new Error(r==="normal"?"Cannot commit a run while application service ownership remains published":"Cannot publish terminal failure while application service ownership remains published");m??=S(e,n.commitFinalization(e==="failed"?"failed":"finished"));try{await v(o,{protectedLiveRunIds:new Set([n.runId])}),await R(o,n)}catch(a){if(r==="normal")throw a}return t(m)})}}}}}
1
+ import{cleanupOwnedRunLifecycleResourcesWithinLifecycleFence as w,cleanupRunsWithinLifecycleFence as y,listRunManifests as R,startRunLifecycle as v}from"./lifecycle.js";import{createRuntimeManager as F}from"./runtime-manager.js";import{resolveCommittedRunFinalization as I}from"../evidence/finalization.js";import{withProductLifecycleFence as q}from"../process/lifecycle-fence.js";export function createLocalRunEnvironment(s){return{async start(f){const c={localInfrastructure:f.localInfrastructure!==!1},n=await v(s,f,c),p=n.startHeartbeat(2e3,()=>({kind:"controller",status:n.isStopRequested()?"interrupting":"running"}));let a=null;const m=new Map,l=()=>{if(a===null)throw new Error("Run runtimes have not been provisioned");return a};let d=null;return{runId:n.runId,signal:n.signal,isStopRequested:()=>n.isStopRequested(),getInterruptReason:()=>n.getInterruptReason(),requestStop:e=>{n.requestStop(e)},beginFinalization:p,dispose(){p(),n.removeSignalHandlers(),n.dispose()},get committedFinalization(){return d},provision(e,t){if(a!==null)throw new Error("Run runtimes are already provisioned");a=F({productDir:s,graphs:e,lifecycle:n,runtimeOptions:t})},async acquire(e,t){const r=await l().acquire(e,t);m.set(r.leaseId,r);const{leaseId:o,runtimeId:u,graphKey:i,targetNames:h,services:g}=r;return{leaseId:o,runtimeId:u,graphKey:i,targetNames:h,services:g}},canAcquire:e=>l().canAcquire(e),async release(e,t){const r=m.get(e);r!==void 0&&(await l().release(r,t),m.delete(e))},async cleanupAll(){await a?.cleanupAll()},getStats:()=>a?.getStats()??[],getGraph(e){const t=a?.getGraph(e);return t?{key:t.key,targetNames:[...t.targetNames]}:null},getGraphFailure:e=>a?.getGraphFailure(e)??null,markGraphFailed:(e,t)=>{l().markGraphFailed(e,t)},async finish(e,t,r){return p(),q(s,async o=>{const u=R(s).find(i=>i.runId===n.runId);if(u!==void 0&&u.services.length>0)throw new Error(r==="normal"?"Cannot commit a run while application service ownership remains published":"Cannot publish terminal failure while application service ownership remains published");d??=I(e,n.commitFinalization(e==="failed"?"failed":"finished"));try{await y(o,{...c,protectedLiveRunIds:new Set([n.runId])}),await w(o,n,c)}catch(i){if(r==="normal")throw i}return t(d)})}}}}}
@@ -3,7 +3,8 @@ import type { RuntimeServiceSpec, RuntimeGraph } from "./planning-types.js";
3
3
  import { type RuntimePreparationConfig, type RuntimePreparationOptions } from "./runtime-preparation.js";
4
4
  import { type LocalServiceLifecycle, type StartedLocalService, type StartLocalServicesOptions } from "./services.js";
5
5
  import { type RuntimeServiceConfig } from "./template.js";
6
- import type { ConfiguredStepServiceConfig } from "../steps/index.js";
6
+ import { type PortReservation } from "./ports.js";
7
+ import { type ConfiguredStepServiceConfig } from "../steps/index.js";
7
8
  import type { ReadyServiceConfig } from "./runtime-service.js";
8
9
  export type RuntimeContextSourceConfig = RuntimeServiceSpec & RuntimeServiceConfig & ReadyServiceConfig & ConfiguredStepServiceConfig & {
9
10
  stateDir: string;
@@ -15,19 +16,22 @@ export type RuntimeContextOptions = RuntimePreparationOptions & StartLocalServic
15
16
  export type RuntimeInstanceConfig = RuntimePreparationConfig & ReadyServiceConfig;
16
17
  export interface RuntimeInstanceContext {
17
18
  graphDir: string;
19
+ graphDirName: string;
18
20
  graphKey: string;
19
- preparationPromise: Promise<void> | null;
20
- prepared: boolean;
21
+ portReservations: Map<string, PortReservation>;
22
+ ports: Map<string, number> | null;
23
+ triedPortReservations: PortReservation[];
24
+ triedPorts: Map<string, Set<number>>;
25
+ ready: boolean;
26
+ readyPromise: Promise<void> | null;
21
27
  runtimeConfigs: RuntimeInstanceConfig[];
22
28
  runtimeDir: string;
23
29
  runtimeId: string;
24
- started: boolean;
25
30
  startedServices: StartedLocalService[];
26
- startupPromise: Promise<void> | null;
31
+ sourceConfigs: readonly RuntimeContextSourceConfig[];
27
32
  targetNames: string[];
28
33
  }
29
- export declare function runtimeEngineDirectory(context: Pick<RuntimeInstanceContext, "graphDir" | "runtimeId">): string;
30
34
  export declare function createRuntimeInstanceContext(runtimeId: string, graph: RuntimeGraph<RuntimeContextSourceConfig>, productDir: string): RuntimeInstanceContext;
31
- export declare function ensureRuntimeInstanceReady(context: RuntimeInstanceContext, startApplicationServices: boolean, lifecycle: LocalServiceLifecycle, options?: Readonly<RuntimeContextOptions>): Promise<RuntimeInstanceContext>;
35
+ export declare function ensureRuntimeInstanceReady(context: RuntimeInstanceContext, lifecycle: LocalServiceLifecycle, options?: Readonly<RuntimeContextOptions>): Promise<RuntimeInstanceContext>;
32
36
  export declare function cleanupRuntimeInstanceContext(context: RuntimeInstanceContext | null | undefined, lifecycle: LocalServiceLifecycle): Promise<void>;
33
37
  export declare function prepareDatabases(runtimeConfigs: readonly RuntimePreparationConfig[], options?: Readonly<RuntimeContextOptions>): Promise<void>;
@@ -1 +1 @@
1
- import o from"fs";import n from"path";import{prepareRuntimeServices as m}from"./runtime-preparation.js";import{writeGraphMetadata as p}from"./state.js";import{startLocalServices as u,stopLocalServices as f}from"./services.js";import{resolveRuntimeInstanceConfigs as c}from"./template.js";export function runtimeEngineDirectory(r){return n.join(r.graphDir,"node_modules","testkit-engine",r.runtimeId)}export function createRuntimeInstanceContext(r,e,i){const a=n.join(i,".testkit","_graphs",e.dirName),t=n.join(a,"runtimes",r);o.mkdirSync(t,{recursive:!0}),p(a,e);const s=c(e.runtimeConfigs,r,t,{graphDirName:e.dirName,portNamespaceIndex:e.portNamespaceIndex,portNamespaceStride:e.portNamespaceStride});return{graphKey:e.key,targetNames:[...e.targetNames],graphDir:a,runtimeDir:t,runtimeId:r,runtimeConfigs:s,prepared:!1,preparationPromise:null,started:!1,startupPromise:null,startedServices:[]}}export async function ensureRuntimeInstanceReady(r,e,i,a={}){return r.prepared||(r.preparationPromise||(r.preparationPromise=(async()=>{await prepareDatabases(r.runtimeConfigs,{...a,signal:i.signal}),await m(r.runtimeConfigs,{...a,signal:i.signal}),r.prepared=!0})().finally(()=>{r.preparationPromise=null})),await r.preparationPromise),e&&!r.started&&(r.startupPromise||(r.startupPromise=(async()=>{r.startedServices=await u(r.runtimeConfigs,i,a),r.started=!0})().finally(()=>{r.startupPromise=null})),await r.startupPromise),r}async function d(r,e){r?.started&&(await f(r.startedServices,e),r.started=!1,r.startedServices=[])}export async function cleanupRuntimeInstanceContext(r,e){if(!r)return;await d(r,e);const i=runtimeEngineDirectory(r);o.rmSync(n.join(i,"cache"),{recursive:!0,force:!0});for(const a of[n.join(i,"leases"),i])try{o.rmdirSync(a)}catch(t){const s=t.code;if(s!=="ENOENT"&&s!=="ENOTEMPTY"&&s!=="EEXIST")throw t}}export async function prepareDatabases(r,e={}){if(!r.some(a=>a.testkit.database))return;const{provisionDatabase:i}=await import("../database/index.js");for(const a of r)await i(a,{progress:{reporter:e.reporter,setupRegistry:e.setupRegistry},signal:e.signal,skipSchemaSourceVerify:e.skipSchemaSourceVerify})}
1
+ import v from"fs";import p from"path";import{prepareRuntimeServices as m}from"./runtime-preparation.js";import{writeGraphMetadata as y}from"./state.js";import{startLocalServices as g,stopLocalServices as c}from"./services.js";import{resolveRuntimeInstanceConfigs as w}from"./template.js";import{portsOf as R,releasePorts as o,reserveExactPort as P,reservePort as C,reservePorts as S}from"./ports.js";import{buildTemplateExecutionEnv as h}from"../steps/index.js";export function createRuntimeInstanceContext(r,e,s){const t=p.join(s,".testkit","_graphs",e.dirName),i=p.join(t,"runtimes",r);return v.mkdirSync(i,{recursive:!0}),y(t,e),{graphKey:e.key,targetNames:[...e.targetNames],graphDir:t,graphDirName:e.dirName,runtimeDir:i,runtimeId:r,sourceConfigs:e.runtimeConfigs,ports:null,portReservations:new Map,triedPortReservations:[],triedPorts:new Map,runtimeConfigs:[],ready:!1,readyPromise:null,startedServices:[]}}export async function ensureRuntimeInstanceReady(r,e,s={}){return r.ready||(r.readyPromise??=k(r,e,s).finally(()=>{r.readyPromise=null}),await r.readyPromise),r}async function k(r,e,s){const t={...s,signal:e.signal};await D(r),await prepareDatabases(r.runtimeConfigs,t),await m(r.runtimeConfigs,t),r.startedServices=await g(I(r,t),e,s),r.ready=!0}async function D(r){if(r.ports)return;const e=r.sourceConfigs.filter(t=>t.testkit.local).map(t=>t.name),s=await S(e);try{f(r,R(s))}catch(t){throw await o(s.values()),t}r.portReservations=s}function f(r,e){r.runtimeConfigs=w(r.sourceConfigs,r.runtimeId,r.runtimeDir,{graphDirName:r.graphDirName,ports:e}),r.ports=e}function I(r,e){return{get configs(){return r.runtimeConfigs},async releasePort(s){const t=r.portReservations.get(s);r.portReservations.delete(s),await t?.release()},async reassignPort(s,t){const i=r.triedPorts.get(s)??new Set,n=r.ports?.get(s);if(n!==void 0){i.add(n);const a=await P(n);a&&r.triedPortReservations.push(a)}r.triedPorts.set(s,i),await r.portReservations.get(s)?.release();const u=await M(i);r.portReservations.set(s,u);const d=new Map(r.runtimeConfigs.map(a=>[a.name,l(a)]));return f(r,new Map([...r.ports??[],[s,u.port]])),r.runtimeConfigs.filter(a=>t.has(a.name)&&l(a)!==d.get(a.name)).map(a=>a.name)},async prepare(s){await m(r.runtimeConfigs.filter(t=>s.has(t.name)),e)}}}async function M(r){const e=[];try{for(;;){const s=await C();if(!r.has(s.port))return s;e.push(s)}}finally{await o(e)}}function l(r){return JSON.stringify({env:h(r,{},{},{includeTestkitControls:!1}),prepare:r.testkit.runtime.prepare,readyUrl:r.testkit.local?.readyUrl??null,start:r.testkit.local?.start??null})}async function b(r,e){r?.ready&&(await c(r.startedServices,e),r.ready=!1,r.startedServices=[])}export async function cleanupRuntimeInstanceContext(r,e){r&&(await b(r,e),await o([...r.portReservations.values(),...r.triedPortReservations]),r.portReservations.clear(),r.triedPortReservations=[])}export async function prepareDatabases(r,e={}){if(!r.some(t=>t.testkit.database))return;const{provisionDatabase:s}=await import("../database/index.js");for(const t of r)await s(t,{progress:{reporter:e.reporter,setupRegistry:e.setupRegistry},signal:e.signal,skipSchemaSourceVerify:e.skipSchemaSourceVerify})}
@@ -6,12 +6,11 @@ import { type ReadyEnvironment } from "./ready-environment.js";
6
6
  export type RuntimeManagerGraph = RuntimeGraph<RuntimeContextSourceConfig>;
7
7
  export interface RuntimeCapacityRequirement {
8
8
  graphKey: string;
9
- locks?: readonly string[];
10
9
  resourceCost?: number;
11
10
  }
12
11
  export interface RuntimeRequirement extends RuntimeCapacityRequirement {
13
12
  id: number;
14
- startApplicationServices: boolean;
13
+ leaseId: string;
15
14
  serviceName?: string;
16
15
  targetName?: string;
17
16
  }
@@ -30,11 +29,6 @@ export interface RuntimeManagerAcquireOptions {
30
29
  export interface RuntimeManagerReleaseOptions {
31
30
  invalidate?: boolean;
32
31
  }
33
- interface RuntimeLeaseReleaseState {
34
- leaseDir: boolean;
35
- locks: boolean;
36
- runtimeSlot: boolean;
37
- }
38
32
  interface RuntimeSlotResourceReservation {
39
33
  key: string;
40
34
  services: readonly string[];
@@ -53,9 +47,7 @@ export interface RuntimeSlot {
53
47
  }
54
48
  export interface RuntimeLease extends ReadyEnvironment {
55
49
  context: RuntimeInstanceContext;
56
- lockNames: readonly string[];
57
50
  released?: boolean;
58
- releaseState?: RuntimeLeaseReleaseState;
59
51
  resourceCost: number;
60
52
  slot: RuntimeSlot;
61
53
  }