@gfdelarue/testkit 0.4.4 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (276) hide show
  1. package/README.md +108 -21
  2. package/bin/testkit-windows-owner-arm64.exe +0 -0
  3. package/bin/testkit-windows-owner-x64.exe +0 -0
  4. package/dist/app/bugs/categories.js +1 -1
  5. package/dist/app/bugs/draft.js +3 -3
  6. package/dist/app/bugs/facts.d.ts +3 -2
  7. package/dist/app/bugs/facts.js +2 -2
  8. package/dist/app/bugs/guide.js +1 -1
  9. package/dist/app/bugs/run-reading.js +1 -1
  10. package/dist/app/db-schema/prepare.js +1 -1
  11. package/dist/app/destroy/index.js +1 -1
  12. package/dist/app/doctor/index.js +1 -1
  13. package/dist/app/environments/config-driven.d.ts +5 -0
  14. package/dist/app/environments/config-driven.js +1 -0
  15. package/dist/app/environments/routing.d.ts +6 -0
  16. package/dist/app/environments/routing.js +1 -0
  17. package/dist/app/environments/supplied.d.ts +3 -0
  18. package/dist/app/environments/supplied.js +1 -0
  19. package/dist/app/file-selection.js +1 -1
  20. package/dist/app/lint/native.js +1 -1
  21. package/dist/app/lint/options.d.ts +0 -1
  22. package/dist/app/lint/options.js +1 -1
  23. package/dist/app/lint/shared.d.ts +0 -2
  24. package/dist/app/lint/shared.js +1 -1
  25. package/dist/app/package-inspection.js +1 -1
  26. package/dist/app/reports/files.d.ts +13 -0
  27. package/dist/app/reports/files.js +1 -0
  28. package/dist/app/reports/prepare.d.ts +20 -0
  29. package/dist/app/reports/prepare.js +2 -0
  30. package/dist/app/reports/publish.d.ts +21 -0
  31. package/dist/app/reports/publish.js +1 -0
  32. package/dist/app/run/engine-evidence-persistence.js +1 -1
  33. package/dist/app/run/engine-execution-cleanup.js +1 -1
  34. package/dist/app/run/engine-execution-failures.js +1 -1
  35. package/dist/app/run/engine-execution.d.ts +10 -8
  36. package/dist/app/run/engine-execution.js +1 -1
  37. package/dist/app/run/engine-orchestration.d.ts +15 -34
  38. package/dist/app/run/engine-orchestration.js +1 -1
  39. package/dist/app/run/environment.d.ts +33 -0
  40. package/dist/app/run/environment.js +1 -0
  41. package/dist/app/run/failure-details.js +1 -1
  42. package/dist/app/run/live-run.d.ts +3 -1
  43. package/dist/app/run/live-run.js +1 -1
  44. package/dist/app/run/orchestrator.d.ts +8 -1
  45. package/dist/app/run/orchestrator.js +1 -1
  46. package/dist/app/run/planning.d.ts +1 -2
  47. package/dist/app/run/planning.js +1 -1
  48. package/dist/app/run/results.d.ts +1 -1
  49. package/dist/app/run/results.js +1 -1
  50. package/dist/app/run/run-finalization.d.ts +6 -2
  51. package/dist/app/run/run-finalization.js +1 -1
  52. package/dist/app/run/runtime-fixtures.js +1 -1
  53. package/dist/app/run/runtime-task-types.d.ts +1 -1
  54. package/dist/app/run/scheduler/index.js +1 -1
  55. package/dist/app/run/scheduler/locks.d.ts +6 -0
  56. package/dist/app/run/scheduler/locks.js +1 -0
  57. package/dist/app/run/selection.d.ts +0 -1
  58. package/dist/app/run/selection.js +1 -1
  59. package/dist/app/run/setup/runtime-node-setup-protocol.js +1 -1
  60. package/dist/app/run/setup/runtime-node-setup-worker.js +1 -1
  61. package/dist/app/run/task-leases.d.ts +15 -0
  62. package/dist/app/run/task-leases.js +1 -0
  63. package/dist/app/run/worker-loop.d.ts +13 -4
  64. package/dist/app/run/worker-loop.js +1 -1
  65. package/dist/app/tracker/index.d.ts +6 -1
  66. package/dist/app/tracker/index.js +1 -1
  67. package/dist/app/typecheck/index.js +4 -4
  68. package/dist/cli/command-catalog.d.ts +19 -0
  69. package/dist/cli/command-catalog.js +1 -1
  70. package/dist/cli/command-registry.js +1 -1
  71. package/dist/cli/commands/report/prepare.d.ts +12 -0
  72. package/dist/cli/commands/report/prepare.js +5 -0
  73. package/dist/cli/commands/report/publish.d.ts +13 -0
  74. package/dist/cli/commands/report/publish.js +2 -0
  75. package/dist/cli/json-errors.js +1 -1
  76. package/dist/cli/operations/run/operation.d.ts +3 -1
  77. package/dist/cli/operations/run/operation.js +2 -2
  78. package/dist/cli/renderers/doctor/text.js +1 -1
  79. package/dist/cli/renderers/run/formatting.js +1 -1
  80. package/dist/config/authoring/index.d.ts +4 -13
  81. package/dist/config/authoring/index.js +1 -1
  82. package/dist/config/authoring/preset-env.js +1 -1
  83. package/dist/config/loader/config-loader.js +1 -1
  84. package/dist/config/loader/database.js +1 -1
  85. package/dist/config/loader/decode-execution.d.ts +0 -2
  86. package/dist/config/loader/decode-execution.js +1 -1
  87. package/dist/config/loader/discover-tests.js +1 -1
  88. package/dist/config/loader/discovery.d.ts +0 -1
  89. package/dist/config/loader/discovery.js +2 -2
  90. package/dist/config/loader/env.js +1 -1
  91. package/dist/config/loader/execution-model.d.ts +0 -1
  92. package/dist/config/loader/execution-model.js +1 -1
  93. package/dist/config/loader/file-metadata.d.ts +1 -1
  94. package/dist/config/loader/file-metadata.js +1 -1
  95. package/dist/config/loader/groups.d.ts +33 -0
  96. package/dist/config/loader/groups.js +1 -0
  97. package/dist/config/loader/load.d.ts +0 -2
  98. package/dist/config/loader/load.js +1 -1
  99. package/dist/config/loader/runtime-isolation.js +1 -1
  100. package/dist/config/loader/runtime.d.ts +0 -9
  101. package/dist/config/loader/runtime.js +1 -1
  102. package/dist/config/loader/skip-config.js +1 -1
  103. package/dist/config/loader/validation.d.ts +1 -2
  104. package/dist/config/loader/validation.js +1 -1
  105. package/dist/config/loader/values.d.ts +0 -1
  106. package/dist/config/loader/values.js +1 -1
  107. package/dist/database/activity-gate.js +1 -1
  108. package/dist/database/admin.js +2 -2
  109. package/dist/database/lifecycle-cleanup.d.ts +1 -0
  110. package/dist/database/lifecycle-cleanup.js +1 -1
  111. package/dist/database/local-postgres.js +1 -1
  112. package/dist/database/naming.js +1 -1
  113. package/dist/database/schema-operations.d.ts +1 -1
  114. package/dist/database/schema-operations.js +1 -1
  115. package/dist/database/schema-source.js +3 -3
  116. package/dist/database/source-url.d.ts +0 -1
  117. package/dist/database/source-url.js +2 -2
  118. package/dist/database/state.js +1 -1
  119. package/dist/database/steps.js +3 -3
  120. package/dist/domain/file-timeout.d.ts +0 -3
  121. package/dist/domain/file-timeout.js +1 -1
  122. package/dist/domain/operation-display.js +1 -1
  123. package/dist/domain/test-outcomes.d.ts +0 -1
  124. package/dist/domain/test-outcomes.js +1 -1
  125. package/dist/engines/adapters/evidence-identity.js +1 -1
  126. package/dist/engines/adapters/execution.d.ts +0 -1
  127. package/dist/engines/adapters/execution.js +1 -1
  128. package/dist/engines/api/capabilities.d.ts +0 -10
  129. package/dist/engines/api/capabilities.js +1 -1
  130. package/dist/engines/api/execution-scope.js +1 -1
  131. package/dist/engines/api/k6-runtime-input.js +1 -1
  132. package/dist/engines/bundle-cache.js +1 -1
  133. package/dist/engines/contracts/fixture-manifest.js +1 -1
  134. package/dist/engines/definitions.d.ts +1 -0
  135. package/dist/engines/definitions.js +1 -1
  136. package/dist/engines/installations/playwright-resolution-child.js +1 -1
  137. package/dist/engines/installations/playwright.js +1 -1
  138. package/dist/engines/k6/adapter/evidence.js +4 -4
  139. package/dist/engines/k6/adapter/executor.js +1 -1
  140. package/dist/engines/k6/adapter/native-summary.js +1 -1
  141. package/dist/engines/k6/runtime/checks.js +1 -1
  142. package/dist/engines/k6/runtime/dal-fixtures.js +1 -1
  143. package/dist/engines/k6/runtime/dal-suite.js +1 -1
  144. package/dist/engines/k6/runtime/http-suite-runtime.js +1 -1
  145. package/dist/engines/k6/runtime/http.js +1 -1
  146. package/dist/engines/k6/runtime/shared/db-normalization.js +1 -1
  147. package/dist/engines/k6/runtime/shared/fixture-engine.js +2 -2
  148. package/dist/engines/k6/runtime/shared/http-parsing.js +3 -3
  149. package/dist/engines/managed-module-resolution-policy-input.d.ts +0 -1
  150. package/dist/engines/managed-module-resolution-policy-input.js +1 -1
  151. package/dist/engines/managed-module-resolution-policy.js +1 -1
  152. package/dist/engines/node-vitest/adapter/adapter.js +1 -1
  153. package/dist/engines/node-vitest/adapter/evidence.js +2 -2
  154. package/dist/engines/node-vitest/adapter/workspace.js +22 -21
  155. package/dist/engines/node-vitest/runtime/contract/index.js +1 -1
  156. package/dist/engines/node-vitest/runtime/data/http.js +1 -1
  157. package/dist/engines/node-vitest/runtime/data/index.d.ts +2 -2
  158. package/dist/engines/node-vitest/runtime/data/index.js +1 -1
  159. package/dist/engines/node-vitest/runtime/data/report.js +5 -5
  160. package/dist/engines/node-vitest/runtime/data/scenario.d.ts +0 -1
  161. package/dist/engines/node-vitest/runtime/data/scenario.js +2 -2
  162. package/dist/engines/node-vitest/runtime/data/types.d.ts +1 -29
  163. package/dist/engines/node-vitest/runtime/runtime-input.js +1 -1
  164. package/dist/engines/playwright/adapter/adapter.js +1 -1
  165. package/dist/engines/playwright/adapter/config.d.ts +1 -1
  166. package/dist/engines/playwright/adapter/config.js +4 -4
  167. package/dist/engines/playwright/adapter/evidence-validation.d.ts +0 -3
  168. package/dist/engines/playwright/adapter/evidence-validation.js +1 -1
  169. package/dist/engines/playwright/adapter/evidence.js +1 -1
  170. package/dist/engines/playwright/runtime/config-file.js +1 -1
  171. package/dist/engines/playwright/runtime/config.js +1 -1
  172. package/dist/engines/playwright/runtime/runtime-input.d.ts +2 -2
  173. package/dist/engines/playwright/runtime/runtime-input.js +16 -12
  174. package/dist/environment/lifecycle-artifacts.js +1 -1
  175. package/dist/environment/lifecycle-controller.d.ts +1 -11
  176. package/dist/environment/lifecycle-controller.js +1 -1
  177. package/dist/environment/lifecycle.d.ts +14 -11
  178. package/dist/environment/lifecycle.js +1 -1
  179. package/dist/environment/planning-types.d.ts +0 -2
  180. package/dist/environment/ports.d.ts +10 -0
  181. package/dist/environment/ports.js +1 -0
  182. package/dist/environment/ready-environment.d.ts +2 -5
  183. package/dist/environment/ready-environment.js +1 -1
  184. package/dist/environment/run-environment.d.ts +1 -0
  185. package/dist/environment/run-environment.js +1 -1
  186. package/dist/environment/runtime-contexts.d.ts +11 -7
  187. package/dist/environment/runtime-contexts.js +1 -1
  188. package/dist/environment/runtime-manager.d.ts +1 -9
  189. package/dist/environment/runtime-manager.js +1 -1
  190. package/dist/environment/runtime-planning.js +1 -1
  191. package/dist/environment/runtime-preparation.js +1 -1
  192. package/dist/environment/runtime-service.d.ts +0 -6
  193. package/dist/environment/services.d.ts +11 -3
  194. package/dist/environment/services.js +3 -1
  195. package/dist/environment/state.d.ts +1 -4
  196. package/dist/environment/state.js +1 -1
  197. package/dist/environment/template.d.ts +6 -22
  198. package/dist/environment/template.js +1 -1
  199. package/dist/evidence/artifact-config.js +1 -1
  200. package/dist/evidence/artifact-validation.d.ts +1 -2
  201. package/dist/evidence/artifact-validation.js +1 -1
  202. package/dist/evidence/artifacts.js +2 -2
  203. package/dist/evidence/bundles.js +1 -1
  204. package/dist/evidence/environment-record.d.ts +22 -0
  205. package/dist/evidence/environment-record.js +1 -0
  206. package/dist/evidence/index.d.ts +1 -1
  207. package/dist/evidence/logs.js +2 -2
  208. package/dist/evidence/operation-timings.js +1 -1
  209. package/dist/evidence/read-fields.d.ts +0 -1
  210. package/dist/evidence/read-fields.js +1 -1
  211. package/dist/evidence/read-run.js +1 -1
  212. package/dist/evidence/reading.js +1 -1
  213. package/dist/evidence/reporting-types.d.ts +19 -0
  214. package/dist/evidence/reporting.d.ts +2 -2
  215. package/dist/evidence/reporting.js +1 -1
  216. package/dist/evidence/storage.d.ts +0 -2
  217. package/dist/evidence/storage.js +2 -2
  218. package/dist/evidence/timing.js +1 -1
  219. package/dist/kernel/error-serialization.d.ts +2 -0
  220. package/dist/kernel/error-serialization.js +1 -1
  221. package/dist/kernel/unknown-values.d.ts +5 -0
  222. package/dist/kernel/unknown-values.js +1 -1
  223. package/dist/process/environment.js +1 -1
  224. package/dist/process/execa-bundle.json +3 -3
  225. package/dist/process/files/json-file.d.ts +0 -1
  226. package/dist/process/files/json-file.js +1 -1
  227. package/dist/process/files/paths.d.ts +2 -0
  228. package/dist/process/files/paths.js +1 -1
  229. package/dist/process/host/metadata.js +1 -1
  230. package/dist/process/host/platform.js +1 -1
  231. package/dist/process/host-process.js +1 -1
  232. package/dist/process/lifecycle-fence.d.ts +1 -2
  233. package/dist/process/lifecycle-fence.js +1 -1
  234. package/dist/process/locks.js +2 -2
  235. package/dist/process/process-group.js +1 -1
  236. package/dist/process/process-identity.js +1 -1
  237. package/dist/process/process.js +30 -30
  238. package/dist/process/readiness.d.ts +7 -1
  239. package/dist/process/readiness.js +1 -1
  240. package/dist/process/recovery.js +1 -1
  241. package/dist/process/runs/manifest.js +1 -1
  242. package/dist/process/runs/ownership.js +1 -1
  243. package/dist/process/runs/product-state.js +1 -1
  244. package/dist/process/runs/stop.js +1 -1
  245. package/dist/process/stoppable-settlement.d.ts +26 -0
  246. package/dist/process/stoppable-settlement.js +1 -0
  247. package/dist/process/url-redaction.d.ts +1 -0
  248. package/dist/process/url-redaction.js +1 -0
  249. package/dist/process/windows-native.d.ts +6 -0
  250. package/dist/process/windows-native.js +1 -1
  251. package/dist/repo/ci-environment.js +1 -1
  252. package/dist/repo/fingerprint-policy.d.ts +0 -2
  253. package/dist/repo/fingerprint-policy.js +1 -1
  254. package/dist/repo/physical-tree.js +1 -1
  255. package/dist/repo/publish-directory.d.ts +20 -0
  256. package/dist/repo/publish-directory.js +3 -0
  257. package/dist/repo/state.js +4 -4
  258. package/dist/steps/build-config.d.ts +0 -1
  259. package/dist/steps/build-config.js +1 -1
  260. package/dist/steps/configured-steps.js +1 -1
  261. package/dist/steps/environment.d.ts +0 -2
  262. package/dist/steps/environment.js +1 -1
  263. package/dist/steps/index.d.ts +1 -1
  264. package/dist/steps/run.js +2 -2
  265. package/dist/toolchains/archive.js +1 -1
  266. package/dist/toolchains/index.js +3 -3
  267. package/dist/toolchains/node-provisioning.js +2 -2
  268. package/dist/toolchains/vitest/index.js +1 -1
  269. package/docs/bug-records.md +54 -52
  270. package/docs/engine-architecture.md +39 -11
  271. package/package.json +1 -1
  272. package/testkit-config-schema.md +43 -5
  273. package/dist/app/run/playwright-configuration.d.ts +0 -2
  274. package/dist/app/run/playwright-configuration.js +0 -1
  275. package/dist/environment/readiness.d.ts +0 -12
  276. package/dist/environment/readiness.js +0 -1
@@ -8,9 +8,8 @@ export interface ServiceConfigValidationInput {
8
8
  local: NormalizedLocalServiceConfig | undefined;
9
9
  name: string;
10
10
  productDir: string;
11
- requireLocalRuntime?: boolean;
12
11
  runtime: NormalizedRuntimeConfig;
13
12
  suites: ServiceSuiteCollection;
14
13
  }
15
14
  export declare function validateConfigCoverage(configs: readonly NormalizedServiceConfig[]): void;
16
- export declare function validateServiceConfig({ name, local, database, databaseFrom, runtime, dependsOn, suites, productDir, requireLocalRuntime, }: Readonly<ServiceConfigValidationInput>): void;
15
+ export declare function validateServiceConfig({ name, local, database, databaseFrom, runtime, dependsOn, suites, productDir, }: Readonly<ServiceConfigValidationInput>): void;
@@ -1 +1 @@
1
- import{finalizeConfiguredInputs as l,finalizeConfiguredSteps as w,validateConfiguredCollection as d}from"../../steps/configured-steps.js";import{findDatabasePlaceholders as h}from"../../steps/environment.js";import{resolveEngineProfile as S}from"../../engines/catalog.js";import{ensureExistingContainedPath as g,ensureExistingPath as E}from"./paths.js";const m=["migrate","seed","verify"];export function validateConfigCoverage(e){const i=new Set(e.map(r=>r.name));for(const r of e){for(const t of r.testkit.dependsOn)if(!i.has(t))throw new Error(`Service "${r.name}" depends on "${t}", but ${t} is not defined`);const n=r.testkit.databaseFrom;if(n&&!i.has(n))throw new Error(`Service "${r.name}" databaseFrom "${n}" is not defined`)}}export function validateServiceConfig({name:e,local:i,database:r,databaseFrom:n,runtime:t,dependsOn:c,suites:p,productDir:s,requireLocalRuntime:v=!0}){const $=Object.entries(p).some(([a,u])=>(u||[]).some(o=>{if(o.testType!==a)throw new Error(`Service "${e}" suite ${a}:${o.name} declares conflicting test type ${o.testType}`);const f=S(o.testType,o.engineId);if(f.id!==o.profileId)throw new Error(`Service "${e}" suite ${o.testType}:${o.name} declares conflicting engine profile ${o.profileId}; expected ${f.id}`);return f.capabilities.applicationRuntime==="required"}));if(v&&$&&!i)throw new Error(`Service "${e}" defines suites whose engine profile requires an application runtime, but no local runtime could be resolved. Add it in testkit.config.ts.`);if(r&&n)throw new Error(`Service "${e}" cannot define both database and databaseFrom`);if(t.instances<1)throw new Error(`Service "${e}" runtime.instances must be a positive integer`);if(t.maxConcurrentTasks<=0)throw new Error(`Service "${e}" runtime.maxConcurrentTasks must be a positive integer when provided`);for(const a of c)if(a===e)throw new Error(`Service "${e}" cannot depend on itself`);if(b({name:e,local:i,database:r,runtime:t}),i?.cwd&&E(s,i.cwd,`Service "${e}" local.cwd`),t.toolchain?.cwd&&g(s,t.toolchain.cwd,`Service "${e}" runtime.toolchain.cwd`),r)for(const a of m)d({productDir:s,label:`Service "${e}" database.template.${a}`,steps:r.template[a]});d({productDir:s,label:`Service "${e}" database.template`,inputs:r?.template.inputs||[]}),d({productDir:s,label:`Service "${e}" runtime.prepare`,inputs:t.prepare.inputs,steps:t.prepare.steps})}function b({name:e,local:i,database:r,runtime:n}){const t=c=>p=>{const s=h(p).at(0);if(s)throw new Error(`Service "${e}" ${c} uses ${s.placeholder}. Database placeholders resolve only in env values; commands read DATABASE_URL or a service env entry at run time.`);return p};if(i&&(t("local.start")(i.start),t("local.cwd")(i.cwd)),l(n.prepare.inputs,t("runtime.prepare.inputs")),w(n.prepare.steps,t("runtime.prepare.steps")),r){l(r.template.inputs,t("database.template.inputs"));for(const c of m)w(r.template[c],t(`database.template.${c}`))}}
1
+ import{finalizeConfiguredInputs as l,finalizeConfiguredSteps as v,validateConfiguredCollection as f}from"../../steps/configured-steps.js";import{findDatabasePlaceholders as m}from"../../steps/environment.js";import{resolveEngineProfile as u}from"../../engines/catalog.js";import{ensureExistingContainedPath as h,ensureExistingPath as S}from"./paths.js";const w=["migrate","seed","verify"];export function validateConfigCoverage(e){const i=new Set(e.map(r=>r.name));for(const r of e){for(const t of r.testkit.dependsOn)if(!i.has(t))throw new Error(`Service "${r.name}" depends on "${t}", but ${t} is not defined`);const n=r.testkit.databaseFrom;if(n&&!i.has(n))throw new Error(`Service "${r.name}" databaseFrom "${n}" is not defined`)}}export function validateServiceConfig({name:e,local:i,database:r,databaseFrom:n,runtime:t,dependsOn:c,suites:p,productDir:s}){for(const[a,$]of Object.entries(p))for(const o of $||[]){if(o.testType!==a)throw new Error(`Service "${e}" suite ${a}:${o.name} declares conflicting test type ${o.testType}`);const d=u(o.testType,o.engineId);if(d.id!==o.profileId)throw new Error(`Service "${e}" suite ${o.testType}:${o.name} declares conflicting engine profile ${o.profileId}; expected ${d.id}`)}if(r&&n)throw new Error(`Service "${e}" cannot define both database and databaseFrom`);if(t.instances<1)throw new Error(`Service "${e}" runtime.instances must be a positive integer`);if(t.maxConcurrentTasks<=0)throw new Error(`Service "${e}" runtime.maxConcurrentTasks must be a positive integer when provided`);for(const a of c)if(a===e)throw new Error(`Service "${e}" cannot depend on itself`);if(g({name:e,local:i,database:r,runtime:t}),i?.cwd&&S(s,i.cwd,`Service "${e}" local.cwd`),t.toolchain?.cwd&&h(s,t.toolchain.cwd,`Service "${e}" runtime.toolchain.cwd`),r)for(const a of w)f({productDir:s,label:`Service "${e}" database.template.${a}`,steps:r.template[a]});f({productDir:s,label:`Service "${e}" database.template`,inputs:r?.template.inputs||[]}),f({productDir:s,label:`Service "${e}" runtime.prepare`,inputs:t.prepare.inputs,steps:t.prepare.steps})}function g({name:e,local:i,database:r,runtime:n}){const t=c=>p=>{const s=m(p).at(0);if(s)throw new Error(`Service "${e}" ${c} uses ${s.placeholder}. Database placeholders resolve only in env values; commands read DATABASE_URL or a service env entry at run time.`);return p};if(i&&(t("local.start")(i.start),t("local.cwd")(i.cwd)),l(n.prepare.inputs,t("runtime.prepare.inputs")),v(n.prepare.steps,t("runtime.prepare.steps")),r){l(r.template.inputs,t("database.template.inputs"));for(const c of w)v(r.template[c],t(`database.template.${c}`))}}
@@ -1,4 +1,3 @@
1
- export declare function isObjectRecord(value: unknown): value is Record<string, unknown>;
2
1
  export declare function isObjectLike(value: unknown): value is object;
3
2
  export declare function readProperty(value: unknown, key: string): unknown;
4
3
  export declare function readObjectRecord(value: unknown): Record<string, unknown>;
@@ -1 +1 @@
1
- export function isObjectRecord(t){return typeof t=="object"&&t!==null&&!Array.isArray(t)}export function isObjectLike(t){return typeof t=="object"&&t!==null||typeof t=="function"}export function readProperty(t,r){return isObjectLike(t)?Reflect.get(t,r):void 0}export function readObjectRecord(t){return isObjectRecord(t)?t:{}}
1
+ import{isUnknownRecord as n}from"../../kernel/unknown-values.js";export function isObjectLike(t){return typeof t=="object"&&t!==null||typeof t=="function"}export function readProperty(t,e){return isObjectLike(t)?Reflect.get(t,e):void 0}export function readObjectRecord(t){return n(t)?t:{}}
@@ -1 +1 @@
1
- import{AsyncLocalStorage as p}from"node:async_hooks";import n from"node:fs";import h from"node:os";import c from"node:path";import{withProcessLock as E}from"../process/locks.js";import{hashString as u}from"./naming.js";const S=Object.freeze([".testkit","_locks","local-postgres"]),P=448,m=18,f=new p;export async function withUserHostLocalPostgresActivity(t,o,e={}){$();const r=e.location??readSystemUserHostLocalPostgresActivityLocation(),s=D(r),i=L(s.productsDirectory,t);return f.run(!0,()=>E(i,o,{hostScope:"current-host",signal:e.signal}))}export function resolveUserHostLocalPostgresActivityPaths({homeDirectory:t,hostname:o,uid:e,username:r}){const s=c.resolve(a(t,"home directory")),i=a(o,"hostname").trim().toLowerCase(),l=a(r,"username").trim().toLowerCase();if(!Number.isSafeInteger(e))throw new Error("Local Postgres activity gate requires a valid user ID");const w=e>=0?`uid:${e.toString()}`:`username:${l}`,y=c.join(s,...S,`host-${u(i,24)}-user-${u(w,24)}`);return Object.freeze({activityDirectory:y,productsDirectory:c.join(y,"products")})}export function readSystemUserHostLocalPostgresActivityLocation(){const t=h.userInfo();return{homeDirectory:t.homedir,hostname:h.hostname(),uid:t.uid,username:t.username}}function D(t){const o=k(t.homeDirectory,t.uid),e=resolveUserHostLocalPostgresActivityPaths({...t,homeDirectory:o});let r=o;const s=c.relative(o,e.activityDirectory).split(c.sep);for(const i of s)r=v(c.join(r,i),o,t.uid);return v(e.productsDirectory,o,t.uid),e}function L(t,o){const e=n.realpathSync.native(c.resolve(a(o,"product directory")));return c.join(t,`product-${u(e,24)}.lock`)}function k(t,o){const e=c.resolve(a(t,"home directory"));if(!n.statSync(e).isDirectory())throw new Error(`Cannot use non-directory home for the local Postgres activity gate: ${e}`);const s=n.realpathSync.native(e);return d(s,!1,o),s}function v(t,o,e){let r;try{r=n.lstatSync(t)}catch(i){if(!g(i,"ENOENT"))throw i;try{n.mkdirSync(t,{mode:P})}catch(l){if(!g(l,"EEXIST"))throw l}r=n.lstatSync(t)}if(!r.isDirectory()||r.isSymbolicLink())throw new Error(`Local Postgres activity lock ancestor is not a real directory: ${t}`);const s=n.realpathSync.native(t);return A(s,o),d(s,!0,e),s}function d(t,o,e){if(e<0)return;let r=n.lstatSync(t);if(r.uid!==e)throw new Error(`Local Postgres activity lock ancestor is not owned by the current user: ${t}`);if((r.mode&m)!==0){if(!o)throw new Error(`Local Postgres activity lock ancestor is writable by another user: ${t}`);if(n.chmodSync(t,r.mode&511&~m),r=n.lstatSync(t),(r.mode&m)!==0)throw new Error(`Could not secure local Postgres activity lock ancestor: ${t}`)}}function A(t,o){const e=c.relative(o,t);if(e===".."||e.startsWith(`..${c.sep}`)||c.isAbsolute(e))throw new Error(`Local Postgres activity lock escaped the user home: ${t}`)}function $(){if(f.getStore()!==void 0)throw new Error("Nested local Postgres activity is not allowed")}function a(t,o){if(t.trim().length===0)throw new Error(`Local Postgres activity gate requires a ${o}`);return t}function g(t,o){return typeof t=="object"&&t!==null&&"code"in t&&Reflect.get(t,"code")===o}
1
+ import{AsyncLocalStorage as p}from"node:async_hooks";import c from"node:fs";import y from"node:os";import i from"node:path";import{withProcessLock as E}from"../process/locks.js";import{hashString as u}from"./naming.js";import{hasErrorCode as f}from"../kernel/error-serialization.js";const S=Object.freeze([".testkit","_locks","local-postgres"]),P=448,m=18,v=new p;export async function withUserHostLocalPostgresActivity(t,o,r={}){$();const e=r.location??readSystemUserHostLocalPostgresActivityLocation(),s=D(e),n=L(s.productsDirectory,t);return v.run(!0,()=>E(n,o,{hostScope:"current-host",signal:r.signal}))}export function resolveUserHostLocalPostgresActivityPaths({homeDirectory:t,hostname:o,uid:r,username:e}){const s=i.resolve(a(t,"home directory")),n=a(o,"hostname").trim().toLowerCase(),l=a(e,"username").trim().toLowerCase();if(!Number.isSafeInteger(r))throw new Error("Local Postgres activity gate requires a valid user ID");const g=r>=0?`uid:${r.toString()}`:`username:${l}`,h=i.join(s,...S,`host-${u(n,24)}-user-${u(g,24)}`);return Object.freeze({activityDirectory:h,productsDirectory:i.join(h,"products")})}export function readSystemUserHostLocalPostgresActivityLocation(){const t=y.userInfo();return{homeDirectory:t.homedir,hostname:y.hostname(),uid:t.uid,username:t.username}}function D(t){const o=k(t.homeDirectory,t.uid),r=resolveUserHostLocalPostgresActivityPaths({...t,homeDirectory:o});let e=o;const s=i.relative(o,r.activityDirectory).split(i.sep);for(const n of s)e=d(i.join(e,n),o,t.uid);return d(r.productsDirectory,o,t.uid),r}function L(t,o){const r=c.realpathSync.native(i.resolve(a(o,"product directory")));return i.join(t,`product-${u(r,24)}.lock`)}function k(t,o){const r=i.resolve(a(t,"home directory"));if(!c.statSync(r).isDirectory())throw new Error(`Cannot use non-directory home for the local Postgres activity gate: ${r}`);const s=c.realpathSync.native(r);return w(s,!1,o),s}function d(t,o,r){let e;try{e=c.lstatSync(t)}catch(n){if(!f(n,"ENOENT"))throw n;try{c.mkdirSync(t,{mode:P})}catch(l){if(!f(l,"EEXIST"))throw l}e=c.lstatSync(t)}if(!e.isDirectory()||e.isSymbolicLink())throw new Error(`Local Postgres activity lock ancestor is not a real directory: ${t}`);const s=c.realpathSync.native(t);return A(s,o),w(s,!0,r),s}function w(t,o,r){if(r<0)return;let e=c.lstatSync(t);if(e.uid!==r)throw new Error(`Local Postgres activity lock ancestor is not owned by the current user: ${t}`);if((e.mode&m)!==0){if(!o)throw new Error(`Local Postgres activity lock ancestor is writable by another user: ${t}`);if(c.chmodSync(t,e.mode&511&~m),e=c.lstatSync(t),(e.mode&m)!==0)throw new Error(`Could not secure local Postgres activity lock ancestor: ${t}`)}}function A(t,o){const r=i.relative(o,t);if(r===".."||r.startsWith(`..${i.sep}`)||i.isAbsolute(r))throw new Error(`Local Postgres activity lock escaped the user home: ${t}`)}function $(){if(v.getStore()!==void 0)throw new Error("Nested local Postgres activity is not allowed")}function a(t,o){if(t.trim().length===0)throw new Error(`Local Postgres activity gate requires a ${o}`);return t}
@@ -1,2 +1,2 @@
1
- import{randomUUID as T}from"node:crypto";import{managedHostProcessInfrastructureError as g,runManagedHostProcess as E}from"../process/index.js";import{LOCAL_ADMIN_DB as u,LOCAL_ADMIN_QUERY_RETRY_INTERVAL_MS as f,LOCAL_ADMIN_QUERY_RETRY_MS as S,LOCAL_DROP_DATABASE_POLL_INTERVAL_MS as O,LOCAL_DROP_DATABASE_TIMEOUT_MS as R}from"./constants.js";import{buildDatabaseUrl as y,escapeIdentifier as c,escapeSqlLiteral as l}from"./naming.js";import{runDockerPsql as b}from"./postgres/index.js";export{y as buildDatabaseUrl};export const ADMIN_QUERY_COMMAND_TIMEOUT_MS=15e3;export async function databaseExists(t,e,o={}){return(await runAdminQuery(t,["-tAc",`SELECT 1 FROM pg_database WHERE datname = '${l(e)}'`],o)).trim()==="1"}export async function createEmptyDatabase(t,e,o={}){await runAdminQuery(t,["-c",`CREATE DATABASE "${c(e)}"`],o)}export async function cloneDatabaseFromTemplate(t,e,o,r={}){await runAdminQuery(t,["-c",`CREATE DATABASE "${c(e)}" TEMPLATE "${c(o)}"`],r)}export async function dropDatabaseIfExists(t,e,o={}){await dropDatabaseWithForceOrDrain(t,e,o)}export async function dropDatabaseWithForceOrDrain(t,e,o={}){const r={signal:o.signal};try{await runAdminQuery(t,["-c",`DROP DATABASE IF EXISTS "${c(e)}" WITH (FORCE)`],r);return}catch(n){if(!M(n))throw n}if(!await databaseExists(t,e,r))return;let s=!1;try{await runAdminQuery(t,["-c",`ALTER DATABASE "${c(e)}" WITH ALLOW_CONNECTIONS false`],r),s=!0,await waitForDatabaseConnectionsToDrain(t,e,{signal:o.signal}),await runAdminQuery(t,["-c",`DROP DATABASE IF EXISTS "${c(e)}"`],r),s=!1}finally{s&&await databaseExists(t,e)&&await runAdminQuery(t,["-c",`ALTER DATABASE "${c(e)}" WITH ALLOW_CONNECTIONS true`]).catch(()=>{})}}export async function waitForDatabaseConnectionsToDrain(t,e,o={}){const r=o.timeoutMs??R,s=Date.now()+r,n={signal:o.signal};for(;;){D(o.signal),await C(t,e,n);const i=await L(t,e,n);if(i===0)return;if(Date.now()>=s)throw new Error(`Timed out waiting for database "${e}" connections to close (${String(i)} remaining)`);await A(O,o.signal)}}export async function runAdminQuery(t,e,o={}){const r=buildAdminQueryCommandArgs(t,e),s=Date.now();for(;;){D(o.signal);try{let n;if(_(t)){const a=`testkit-admin-${T()}`,d=await E({args:["exec","-e",`PGAPPNAME=${a}`,...r.slice(1)],cwd:process.cwd(),displayLabel:`Docker Postgres admin query for ${t.containerName}`,env:process.env,executable:"docker",signal:o.signal,timeoutMs:ADMIN_QUERY_COMMAND_TIMEOUT_MS});d.cancelled&&await E({args:buildAdminQueryCommandArgs(t,["-tAc",`SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE application_name = '${a}'`]),cwd:process.cwd(),displayLabel:`Docker Postgres admin query cancellation for ${t.containerName}`,env:process.env,executable:"docker",timeoutMs:ADMIN_QUERY_COMMAND_TIMEOUT_MS});const m=g(`Docker Postgres admin query for ${t.containerName}`,d);if(m)throw m;n={exitCode:d.exitCode,stderr:d.stderr,stdout:d.stdout}}else{const a={...process.env,PGPASSWORD:t.password,...t.sslMode?{PGSSLMODE:t.sslMode}:{}};n=await b({args:r,env:a,timeoutMs:ADMIN_QUERY_COMMAND_TIMEOUT_MS,connection:{host:t.host,port:t.port,user:t.user,password:t.password,database:t.adminDatabase||u,sslMode:t.sslMode},signal:o.signal})}if(n.exitCode!=null&&n.exitCode!==0){const a=new Error(n.shortMessage||n.stderr||n.stdout||`psql failed with exit code ${String(n.exitCode)}`);throw Reflect.set(a,"stdout",n.stdout||""),Reflect.set(a,"stderr",n.stderr||""),a}const{stdout:i}=n;return i}catch(n){if(Date.now()-s>=S||!isTransientAdminQueryConnectionError(n))throw n;await A(f,o.signal)}}}export function buildAdminQueryCommandArgs(t,e){return _(t)?["exec","-e",`PGPASSWORD=${t.password}`,t.containerId,"psql","-v","ON_ERROR_STOP=1","-U",t.user,"-d",t.adminDatabase||u,"-p",String(t.containerPort||5432),...e]:["-v","ON_ERROR_STOP=1","-h",t.host,"-p",String(t.port),"-U",t.user,"-d",t.adminDatabase||u,...e]}export function isTransientAdminQueryConnectionError(t){const e=w(t);return e.includes('connection to server on socket "/var/run/postgresql/.s.PGSQL.5432" failed')||e.includes("No such file or directory")||e.includes("the database system is starting up")||e.includes("could not connect to server")||e.includes("connection refused")||e.includes("Connection timed out")||e.includes("connection timed out")||e.includes("Operation timed out")||e.includes("i/o timeout")||e.includes("server closed the connection unexpectedly")||e.includes("terminating connection due to administrator command")}async function C(t,e,o){await runAdminQuery(t,["-c",["SELECT pg_terminate_backend(pid)","FROM pg_stat_activity",`WHERE datname = '${l(e)}' AND pid <> pg_backend_pid();`].join(" ")],o)}async function L(t,e,o){const r=await runAdminQuery(t,["-tAc",["SELECT COUNT(*)","FROM pg_stat_activity",`WHERE datname = '${l(e)}' AND pid <> pg_backend_pid();`].join(" ")],o);return Number.parseInt(r.trim(),10)||0}function M(t){const e=w(t);return e.includes('syntax error at or near "WITH"')||e.includes('option "force"')}function A(t,e){return new Promise((o,r)=>{if(e?.aborted){r(p(e));return}const s=setTimeout(n,t);e?.addEventListener("abort",i,{once:!0});function n(){e?.removeEventListener("abort",i),o()}function i(){clearTimeout(s),r(e?p(e):new Error("Database administration aborted"))}})}function D(t){if(t?.aborted)throw p(t)}function p(t){return t.reason instanceof Error?t.reason:new Error("Database administration aborted",{cause:t.reason})}function w(t){return["shortMessage","stderr","stdout","message"].map(e=>P(x(t,e)||"")).join(`
2
- `)}function x(t,e){if(!(typeof t!="object"&&typeof t!="function"||t===null))return Reflect.get(t,e)}function P(t){if(typeof t=="symbol")throw new TypeError("Cannot convert a Symbol value to a string");return String(t)}function _(t){return!!t.containerName}
1
+ import{randomUUID as T}from"node:crypto";import{managedHostProcessInfrastructureError as g,runManagedHostProcess as E}from"../process/index.js";import{LOCAL_ADMIN_DB as u,LOCAL_ADMIN_QUERY_RETRY_INTERVAL_MS as S,LOCAL_ADMIN_QUERY_RETRY_MS as O,LOCAL_DROP_DATABASE_POLL_INTERVAL_MS as R,LOCAL_DROP_DATABASE_TIMEOUT_MS as y}from"./constants.js";import{buildDatabaseUrl as b,escapeIdentifier as c,escapeSqlLiteral as l}from"./naming.js";import{runDockerPsql as f}from"./postgres/index.js";import{readDynamicProperty as C}from"../kernel/unknown-values.js";export{b as buildDatabaseUrl};export const ADMIN_QUERY_COMMAND_TIMEOUT_MS=15e3;export async function databaseExists(t,e,o={}){return(await runAdminQuery(t,["-tAc",`SELECT 1 FROM pg_database WHERE datname = '${l(e)}'`],o)).trim()==="1"}export async function createEmptyDatabase(t,e,o={}){await runAdminQuery(t,["-c",`CREATE DATABASE "${c(e)}"`],o)}export async function cloneDatabaseFromTemplate(t,e,o,n={}){await runAdminQuery(t,["-c",`CREATE DATABASE "${c(e)}" TEMPLATE "${c(o)}"`],n)}export async function dropDatabaseIfExists(t,e,o={}){await dropDatabaseWithForceOrDrain(t,e,o)}export async function dropDatabaseWithForceOrDrain(t,e,o={}){const n={signal:o.signal};try{await runAdminQuery(t,["-c",`DROP DATABASE IF EXISTS "${c(e)}" WITH (FORCE)`],n);return}catch(r){if(!x(r))throw r}if(!await databaseExists(t,e,n))return;let s=!1;try{await runAdminQuery(t,["-c",`ALTER DATABASE "${c(e)}" WITH ALLOW_CONNECTIONS false`],n),s=!0,await waitForDatabaseConnectionsToDrain(t,e,{signal:o.signal}),await runAdminQuery(t,["-c",`DROP DATABASE IF EXISTS "${c(e)}"`],n),s=!1}finally{s&&await databaseExists(t,e)&&await runAdminQuery(t,["-c",`ALTER DATABASE "${c(e)}" WITH ALLOW_CONNECTIONS true`]).catch(()=>{})}}export async function waitForDatabaseConnectionsToDrain(t,e,o={}){const n=o.timeoutMs??y,s=Date.now()+n,r={signal:o.signal};for(;;){D(o.signal),await L(t,e,r);const i=await M(t,e,r);if(i===0)return;if(Date.now()>=s)throw new Error(`Timed out waiting for database "${e}" connections to close (${String(i)} remaining)`);await A(R,o.signal)}}export async function runAdminQuery(t,e,o={}){const n=buildAdminQueryCommandArgs(t,e),s=Date.now();for(;;){D(o.signal);try{let r;if(_(t)){const a=`testkit-admin-${T()}`,d=await E({args:["exec","-e",`PGAPPNAME=${a}`,...n.slice(1)],cwd:process.cwd(),displayLabel:`Docker Postgres admin query for ${t.containerName}`,env:process.env,executable:"docker",signal:o.signal,timeoutMs:ADMIN_QUERY_COMMAND_TIMEOUT_MS});d.cancelled&&await E({args:buildAdminQueryCommandArgs(t,["-tAc",`SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE application_name = '${a}'`]),cwd:process.cwd(),displayLabel:`Docker Postgres admin query cancellation for ${t.containerName}`,env:process.env,executable:"docker",timeoutMs:ADMIN_QUERY_COMMAND_TIMEOUT_MS});const m=g(`Docker Postgres admin query for ${t.containerName}`,d);if(m)throw m;r={exitCode:d.exitCode,stderr:d.stderr,stdout:d.stdout}}else{const a={...process.env,PGPASSWORD:t.password,...t.sslMode?{PGSSLMODE:t.sslMode}:{}};r=await f({args:n,env:a,timeoutMs:ADMIN_QUERY_COMMAND_TIMEOUT_MS,connection:{host:t.host,port:t.port,user:t.user,password:t.password,database:t.adminDatabase||u,sslMode:t.sslMode},signal:o.signal})}if(r.exitCode!=null&&r.exitCode!==0){const a=new Error(r.shortMessage||r.stderr||r.stdout||`psql failed with exit code ${String(r.exitCode)}`);throw Reflect.set(a,"stdout",r.stdout||""),Reflect.set(a,"stderr",r.stderr||""),a}const{stdout:i}=r;return i}catch(r){if(Date.now()-s>=O||!isTransientAdminQueryConnectionError(r))throw r;await A(S,o.signal)}}}export function buildAdminQueryCommandArgs(t,e){return _(t)?["exec","-e",`PGPASSWORD=${t.password}`,t.containerId,"psql","-v","ON_ERROR_STOP=1","-U",t.user,"-d",t.adminDatabase||u,"-p",String(t.containerPort||5432),...e]:["-v","ON_ERROR_STOP=1","-h",t.host,"-p",String(t.port),"-U",t.user,"-d",t.adminDatabase||u,...e]}export function isTransientAdminQueryConnectionError(t){const e=w(t);return e.includes('connection to server on socket "/var/run/postgresql/.s.PGSQL.5432" failed')||e.includes("No such file or directory")||e.includes("the database system is starting up")||e.includes("could not connect to server")||e.includes("connection refused")||e.includes("Connection timed out")||e.includes("connection timed out")||e.includes("Operation timed out")||e.includes("i/o timeout")||e.includes("server closed the connection unexpectedly")||e.includes("terminating connection due to administrator command")}async function L(t,e,o){await runAdminQuery(t,["-c",["SELECT pg_terminate_backend(pid)","FROM pg_stat_activity",`WHERE datname = '${l(e)}' AND pid <> pg_backend_pid();`].join(" ")],o)}async function M(t,e,o){const n=await runAdminQuery(t,["-tAc",["SELECT COUNT(*)","FROM pg_stat_activity",`WHERE datname = '${l(e)}' AND pid <> pg_backend_pid();`].join(" ")],o);return Number.parseInt(n.trim(),10)||0}function x(t){const e=w(t);return e.includes('syntax error at or near "WITH"')||e.includes('option "force"')}function A(t,e){return new Promise((o,n)=>{if(e?.aborted){n(p(e));return}const s=setTimeout(r,t);e?.addEventListener("abort",i,{once:!0});function r(){e?.removeEventListener("abort",i),o()}function i(){clearTimeout(s),n(e?p(e):new Error("Database administration aborted"))}})}function D(t){if(t?.aborted)throw p(t)}function p(t){return t.reason instanceof Error?t.reason:new Error("Database administration aborted",{cause:t.reason})}function w(t){return["shortMessage","stderr","stdout","message"].map(e=>P(C(t,e)||"")).join(`
2
+ `)}function P(t){if(typeof t=="symbol")throw new TypeError("Cannot convert a Symbol value to a string");return String(t)}function _(t){return!!t.containerName}
@@ -5,3 +5,4 @@ import type { LocalPostgresCleanupResult } from "./cleanup.js";
5
5
  export declare function cleanupOrphanedLocalInfrastructure(productDir: string, activityOptions?: Readonly<UserHostLocalPostgresActivityOptions>): Promise<LocalPostgresCleanupResult>;
6
6
  export declare function cleanupOrphanedLocalInfrastructureUnlocked(productDir: string): Promise<LocalPostgresCleanupResult>;
7
7
  export declare function cleanupTerminalRunLocalInfrastructure(fence: Readonly<ProductLifecycleFence>, terminalRunAuthority: Readonly<TerminalRunDatabaseCleanupAuthority>, activityOptions?: Readonly<UserHostLocalPostgresActivityOptions>): Promise<LocalPostgresCleanupResult>;
8
+ export declare function hasLocalDatabaseState(productDir: string): boolean;
@@ -1 +1 @@
1
- import c from"node:fs";import{getLocalInfraDir as u,hasRemainingLocalArtifacts as i}from"./state.js";import{withUserHostLocalPostgresActivity as l}from"./activity-gate.js";import{requireProductLifecycleFenceProductDir as f}from"../process/lifecycle-fence.js";export async function cleanupOrphanedLocalInfrastructure(r,e={}){const n=u(r);return s(r,n)?l(r,()=>cleanupOrphanedLocalInfrastructureUnlocked(r),e):o()}export async function cleanupOrphanedLocalInfrastructureUnlocked(r){const e=u(r);if(!s(r,e))return o();const{hasActiveProductRuntime:n,cleanupLocalPostgresDockerResources:t}=await import("./cleanup.js");if(n(r))return o();const a=await t({productDir:r,dryRun:!1,stopIdle:!0});return i(r)||c.rmSync(e,{recursive:!0,force:!0}),a}export async function cleanupTerminalRunLocalInfrastructure(r,e,n={}){const t=f(r),a=u(t);return s(t,a)?l(t,async()=>{if(f(r),!s(t,a))return o();const{cleanupTerminalRunLocalPostgresDockerResources:p,assertTerminalRunDatabaseCleanupAuthority:m}=await import("./cleanup.js"),y=await p(r,e,{stopIdle:!0});return i(t)||(m(r,e),c.rmSync(a,{recursive:!0,force:!0})),y},n):o()}function s(r,e){return c.existsSync(e)||i(r)}function o(){return{kept:[],removed:[],skipped:[],stopped:[],targets:[]}}
1
+ import u from"node:fs";import{getLocalInfraDir as c,hasRemainingLocalArtifacts as i}from"./state.js";import{withUserHostLocalPostgresActivity as l}from"./activity-gate.js";import{requireProductLifecycleFenceProductDir as f}from"../process/lifecycle-fence.js";export async function cleanupOrphanedLocalInfrastructure(e,r={}){const n=c(e);return o(e,n)?l(e,()=>cleanupOrphanedLocalInfrastructureUnlocked(e),r):s()}export async function cleanupOrphanedLocalInfrastructureUnlocked(e){const r=c(e);if(!o(e,r))return s();const{hasActiveProductRuntime:n,cleanupLocalPostgresDockerResources:t}=await import("./cleanup.js");if(n(e))return s();const a=await t({productDir:e,dryRun:!1,stopIdle:!0});return i(e)||u.rmSync(r,{recursive:!0,force:!0}),a}export async function cleanupTerminalRunLocalInfrastructure(e,r,n={}){const t=f(e),a=c(t);return o(t,a)?l(t,async()=>{if(f(e),!o(t,a))return s();const{cleanupTerminalRunLocalPostgresDockerResources:p,assertTerminalRunDatabaseCleanupAuthority:m}=await import("./cleanup.js"),L=await p(e,r,{stopIdle:!0});return i(t)||(m(e,r),u.rmSync(a,{recursive:!0,force:!0})),L},n):s()}export function hasLocalDatabaseState(e){return o(e,c(e))}function o(e,r){return u.existsSync(r)||i(e)}function s(){return{kept:[],removed:[],skipped:[],stopped:[],targets:[]}}
@@ -1,2 +1,2 @@
1
- import{recordWork as M}from"../kernel/work-metrics.js";import{runDockerMetadataCommand as oe}from"./docker-metadata.js";import{randomUUID as C}from"node:crypto";import ie from"fs";import u from"path";import{LOCAL_ADMIN_DB as D,LOCAL_IMAGE as R,LOCAL_MAX_CONNECTIONS as N,LOCAL_PASSWORD as $,LOCAL_POLL_INTERVAL_MS as ae,LOCAL_READY_TIMEOUT_MS as j,LOCAL_USER as V}from"./constants.js";import{buildContainerName as se,buildVolumeName as x}from"./naming.js";import{getLocalInfraDir as U,readStateValue as l}from"./state.js";import{TESTKIT_CREATED_AT_LABEL as z,TESTKIT_RESOURCE_GENERATION_LABEL as G,buildDockerResourceLabels as v,dockerLabelArgs as H,inspectDockerVolume as h,normalizeDockerVolumeInspect as E,removeDockerVolume as ce,runDockerCommand as L}from"./docker.js";import{writeLocalInfraState as ue}from"./state-files.js";const le="/var/lib/postgresql/data",fe="/var/lib/postgresql",A=String;export const LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS=Object.freeze({inspect:3e4,readinessAttempt:1e4,remove:12e4,run:10*6e4,start:12e4,volumeCreate:12e4});export async function ensureLocalContainer(e,n={},r={}){const o=U(e);ie.mkdirSync(o,{recursive:!0});const t=l(u.join(o,"container_name"))||se(e),i=l(u.join(o,"volume_name")),a=n.image||l(u.join(o,"image"))||R,c=l(u.join(o,"image")),f=resolveLocalPostgresVolumeTarget(a),w=n.user||l(u.join(o,"user"))||V,b=n.password||l(u.join(o,"password"))||$,_=F(n.maxConnections||l(u.join(o,"max_connections"))||N);let s=await inspectContainer(t,r);s&&p(s,e,t);const te=!s&&!i;let d=Y(s,f)||i||x(e,C());s&&await W(e,d,r);const T=s?.Config?.Image||c||null;if(s&&!isReusableLocalPostgresContainer(s,{image:a,maxConnections:_,volumeMountPath:f,volumeName:d})&&(await stopAndRemoveContainer(s.Id,t,r),T&&T!==a&&(await be(e,d,r),d=x(e,C())),s=null),s)s.State?.Running||(await k(["start",s.Id],`Docker local Postgres container start for ${t}`,LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.start,r.signal),s=await inspectContainer(t,r),s&&p(s,e,t));else{d=te?await K(e,r):await ge(e,d,r);const re=X(e,t,C());await k(buildDockerRunArgs({containerName:t,labels:re,volumeName:d,volumeMountPath:f,user:w,password:b,image:a,maxConnections:_}),`Docker local Postgres container creation for ${t}`,LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.run,r.signal),s=await inspectContainer(t,r),s&&p(s,e,t)}const O=q(s);if(!s||!O)throw new Error(`Could not determine host port for local database container ${t}`);const S={containerName:t,containerId:s.Id,volumeName:d,volumeMountPath:f,image:a,user:w,password:b,maxConnections:_,host:"127.0.0.1",port:Number(O),containerPort:5432};return await J(S,r),ue(o,S),S}export function buildDockerRunArgs({containerName:e,labels:n,volumeName:r,volumeMountPath:o,user:t,password:i,image:a,maxConnections:c}){return["run","-d","--name",e,...H(n),"-p","127.0.0.1::5432","--mount",`type=volume,source=${r},target=${o}`,"-e",`POSTGRES_USER=${t}`,"-e",`POSTGRES_PASSWORD=${i}`,"-e",`POSTGRES_DB=${D}`,a,"postgres","-c",`max_connections=${String(c)}`]}export async function loadExistingLocalContainer(e,n={}){const r=U(e),o=l(u.join(r,"container_name"));if(!o)return null;const t=await inspectContainer(o,n);if(!t)return null;p(t,e,o);const i=t.Config?.Image||l(u.join(r,"image"))||R,a=l(u.join(r,"volume_mount_path"))||resolveLocalPostgresVolumeTarget(i),c=Y(t,a)||l(u.join(r,"volume_name"));if(!c)return null;await W(e,c,n),t.State?.Running||await k(["start",t.Id],`Docker local Postgres container start for ${o}`,LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.start,n.signal);const f=t.State?.Running?t:await inspectContainer(t.Id,n);f&&p(f,e,o);const w=q(f);if(!f||!w)return null;const b={containerName:o,containerId:f.Id,volumeName:c,volumeMountPath:a,image:i,user:l(u.join(r,"user"))||V,password:l(u.join(r,"password"))||$,maxConnections:F(l(u.join(r,"max_connections"))||N),host:"127.0.0.1",port:Number(w),containerPort:B(f)==="host"?Number(w):5432};return await J(b,n),b}function me(e,n){const r=e.Config?.Cmd;return Array.isArray(r)?r.includes("-c")&&r.includes(`max_connections=${String(n)}`):!1}function de(e,n,r){return(Array.isArray(e.Mounts)?e.Mounts:[]).some(t=>t?.Type==="volume"&&t.Name===n&&t.Destination===r)}export function isReusableLocalPostgresContainer(e,n){return e.Config?.Image===n.image&&me(e,n.maxConnections)&&de(e,n.volumeName,n.volumeMountPath)&&B(e)==="bridge"}function B(e){return e.HostConfig?.NetworkMode==="host"?"host":"bridge"}function q(e){return e?.NetworkSettings?.Ports?.["5432/tcp"]?.[0]?.HostPort||null}function F(e){const n=Number(e);if(!Number.isInteger(n)||n<10)throw new Error("database.maxConnections must be an integer greater than or equal to 10");return n}export async function inspectContainer(e,n={}){M("database.containerInspections");const r=`Docker local Postgres container inspect for ${e}`,o=["inspect",e],t=await oe(o,r,LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.inspect,n.signal,c=>L(o,r,c,n.signal));if(t.exitCode!==0){if(ne(t))return null;throw I(r,t)}let i;try{i=JSON.parse(t.stdout)}catch(c){throw ee(r,t,c)}const a=P(i)&&i.length===1?normalizeLocalPostgresContainerInspect(i[0]):null;if(!a?.Id)throw ee(r,t);return{...a,Id:a.Id}}export function normalizeLocalPostgresContainerInspect(e){return m(e)?{Config:he(e.Config),HostConfig:ye(e.HostConfig),Id:g(e.Id),Mounts:Pe(e.Mounts),NetworkSettings:ke(e.NetworkSettings),State:_e(e.State)}:null}export async function stopAndRemoveContainer(e,n=e,r={}){const o=`Docker local Postgres container removal for ${n}`,t=await L(["rm","-f","-v",e],o,LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.remove,r.signal);if(!(t.exitCode===0||ne(t)))throw I(o,t)}async function ge(e,n,r={}){const o=y(e,n,"existing-generation"),t=await h(n,r);if(t){if(!matchesStableDockerLabels(Z(t)||{},o))throw new Error(`Docker volume ${n} already exists but is not owned by this Testkit product. Remove or rename it before running Testkit.`);return n}return K(e,r)}async function K(e,n){const r=C(),o=x(e,r),t=y(e,o,r);await k(["volume","create",...H(t),o],`Docker local Postgres volume creation for ${o}`,LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.volumeCreate,n.signal);const i=E(await h(o,n));if(i===null||!we(i.labels,t))throw new Error(`Docker volume ${o} was not published with the expected Testkit generation identity`);return o}async function W(e,n,r){const o=y(e,n,"existing-generation"),t=E(await h(n,r));if(t===null||!matchesStableDockerLabels(t.labels,o))throw new Error(`Docker volume ${n} is missing or is not owned by this Testkit product`);return t}export function matchesStableDockerLabels(e,n){const r=e[z],o=e[G];return typeof r=="string"&&r.length>0&&typeof o=="string"&&o.length>0&&Object.entries(n).every(([t,i])=>t===z||t===G||e[t]===A(i))}function we(e,n){const r=Object.entries(e).sort(([t],[i])=>t.localeCompare(i)),o=Object.entries(n).map(([t,i])=>[t,A(i)]).sort(([t],[i])=>t.localeCompare(i));return r.length===o.length&&r.every(([t,i],a)=>o[a]?.[0]===t&&o[a]?.[1]===i)}async function be(e,n,r={}){const o=y(e,n,"existing-generation"),t=await h(n,r);if(!t)return;const i=Z(t)||{};if(!matchesStableDockerLabels(i,o))return;const c=E(t);c!==null&&await ce(n,c,r)}function Y(e,n){return(Array.isArray(e?.Mounts)?e.Mounts:[]).find(t=>t?.Type==="volume"&&typeof t.Name=="string"&&t.Name.length>0&&t.Destination===n)?.Name||null}function p(e,n,r){const o=X(n,r);if(!matchesStableDockerLabels(e.Config?.Labels||{},o))throw new Error(`Docker container ${r} already exists but is not owned by this Testkit product. Remove or rename it before running Testkit.`)}export function resolveLocalPostgresVolumeTarget(e){return pe(e)>=18?fe:le}function pe(e){const r=A(e||"").match(/(?:^|[:@/-])(?:pg|postgres)?(\d{2})(?:\D|$)/i);return r?Number(r[1]):16}async function J(e,n={}){M("database.hostReadinessChecks");const r=Date.now();let o=10;for(;Date.now()-r<j;){const t=Math.max(1,j-(Date.now()-r));if((await L(["exec",e.containerId,"pg_isready","-h","127.0.0.1","-U",e.user,"-d",D,"-p",String(e.containerPort||5432)],`Docker local Postgres readiness probe for ${e.containerName}`,Math.min(LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.readinessAttempt,t),n.signal)).exitCode===0)return;await Ce(Math.min(o,t),n.signal),o=Math.min(o*2,ae)}throw new Error(`Timed out waiting for local database container ${e.containerName}`)}function X(e,n,r="existing-generation"){return v(e,{generation:r,kind:"postgres-container",name:n,scope:"local-postgres",cachePolicy:"product-cache"})}function y(e,n,r){return v(e,{generation:r,kind:"postgres-volume",name:n,scope:"local-postgres",cachePolicy:"product-cache"})}function Ce(e,n){return new Promise((r,o)=>{if(n?.aborted){o(Q(n));return}const t=setTimeout(i,e);n?.addEventListener("abort",a,{once:!0});function i(){n?.removeEventListener("abort",a),r()}function a(){clearTimeout(t),o(n?Q(n):new Error("Local Postgres operation aborted"))}})}function Q(e){return e.reason instanceof Error?e.reason:new Error("Local Postgres operation aborted",{cause:e.reason})}function Z(e){if(!m(e))return null;const n=e.Labels;return m(n)?n:null}function he(e){if(e===null)return null;if(m(e))return{Cmd:Se(e.Cmd),Image:g(e.Image),Labels:Le(e.Labels)}}function Le(e){if(!m(e))return null;const n={};for(const[r,o]of Object.entries(e))typeof o=="string"&&(n[r]=o);return n}function ye(e){if(e===null)return null;if(m(e))return{NetworkMode:g(e.NetworkMode)}}function Pe(e){if(e===null)return null;if(P(e))return e.flatMap(n=>m(n)?[{Destination:g(n.Destination),Name:g(n.Name),Type:g(n.Type)}]:[])}function ke(e){if(e===null)return null;if(!m(e))return;const n=e.Ports;if(n===null)return{Ports:null};if(!m(n))return{};const r={};for(const[o,t]of Object.entries(n)){if(t===null){r[o]=null;continue}P(t)&&(r[o]=t.flatMap(i=>{if(!m(i))return[];const a=g(i.HostPort);return i.HostPort!==void 0&&a===void 0?[]:[{HostPort:a}]}))}return{Ports:r}}function _e(e){if(e===null)return null;if(m(e))return{Running:e.Running===null||typeof e.Running=="boolean"?e.Running:void 0}}function g(e){if(e===null||typeof e=="string")return e}function m(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function P(e){return Array.isArray(e)}function Se(e){if(e===null)return null;if(P(e))return e.filter(n=>typeof n=="string")}async function k(e,n,r,o){const t=await L(e,n,r,o);if(t.exitCode===0)return t;throw I(n,t)}function I(e,n){const r=n.stderr.trim()||n.stdout.trim(),o=new Error(`${e} failed with exit code ${String(n.exitCode)}${r?`: ${r}`:""}`);return Object.defineProperty(o,"result",{enumerable:!1,value:Object.freeze({...n})}),o}function ee(e,n,r){const o=new Error(`${e} returned malformed Docker inspect output; expected exactly one container record with a non-empty Id`,r===void 0?void 0:{cause:r});return Object.defineProperty(o,"result",{enumerable:!1,value:Object.freeze({...n})}),o}function ne(e){return/\bno such (?:container|object)\b/iu.test(`${e.stderr}
1
+ import{recordWork as M}from"../kernel/work-metrics.js";import{runDockerMetadataCommand as oe}from"./docker-metadata.js";import{randomUUID as C}from"node:crypto";import ie from"fs";import u from"path";import{LOCAL_ADMIN_DB as D,LOCAL_IMAGE as R,LOCAL_MAX_CONNECTIONS as N,LOCAL_PASSWORD as $,LOCAL_POLL_INTERVAL_MS as ae,LOCAL_READY_TIMEOUT_MS as j,LOCAL_USER as V}from"./constants.js";import{buildContainerName as se,buildVolumeName as x}from"./naming.js";import{getLocalInfraDir as U,readStateValue as l}from"./state.js";import{TESTKIT_CREATED_AT_LABEL as z,TESTKIT_RESOURCE_GENERATION_LABEL as v,buildDockerResourceLabels as G,dockerLabelArgs as H,inspectDockerVolume as h,normalizeDockerVolumeInspect as E,removeDockerVolume as ce,runDockerCommand as L}from"./docker.js";import{writeLocalInfraState as ue}from"./state-files.js";import{isUnknownArray as P,isUnknownRecord as m}from"../kernel/unknown-values.js";const le="/var/lib/postgresql/data",fe="/var/lib/postgresql",I=String;export const LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS=Object.freeze({inspect:3e4,readinessAttempt:1e4,remove:12e4,run:10*6e4,start:12e4,volumeCreate:12e4});export async function ensureLocalContainer(e,n={},r={}){const o=U(e);ie.mkdirSync(o,{recursive:!0});const t=l(u.join(o,"container_name"))||se(e),i=l(u.join(o,"volume_name")),a=n.image||l(u.join(o,"image"))||R,c=l(u.join(o,"image")),f=resolveLocalPostgresVolumeTarget(a),w=n.user||l(u.join(o,"user"))||V,b=n.password||l(u.join(o,"password"))||$,_=F(n.maxConnections||l(u.join(o,"max_connections"))||N);let s=await inspectContainer(t,r);s&&p(s,e,t);const te=!s&&!i;let d=Y(s,f)||i||x(e,C());s&&await W(e,d,r);const A=s?.Config?.Image||c||null;if(s&&!isReusableLocalPostgresContainer(s,{image:a,maxConnections:_,volumeMountPath:f,volumeName:d})&&(await stopAndRemoveContainer(s.Id,t,r),A&&A!==a&&(await be(e,d,r),d=x(e,C())),s=null),s)s.State?.Running||(await y(["start",s.Id],`Docker local Postgres container start for ${t}`,LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.start,r.signal),s=await inspectContainer(t,r),s&&p(s,e,t));else{d=te?await K(e,r):await ge(e,d,r);const re=X(e,t,C());await y(buildDockerRunArgs({containerName:t,labels:re,volumeName:d,volumeMountPath:f,user:w,password:b,image:a,maxConnections:_}),`Docker local Postgres container creation for ${t}`,LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.run,r.signal),s=await inspectContainer(t,r),s&&p(s,e,t)}const O=q(s);if(!s||!O)throw new Error(`Could not determine host port for local database container ${t}`);const S={containerName:t,containerId:s.Id,volumeName:d,volumeMountPath:f,image:a,user:w,password:b,maxConnections:_,host:"127.0.0.1",port:Number(O),containerPort:5432};return await J(S,r),ue(o,S),S}export function buildDockerRunArgs({containerName:e,labels:n,volumeName:r,volumeMountPath:o,user:t,password:i,image:a,maxConnections:c}){return["run","-d","--name",e,...H(n),"-p","127.0.0.1::5432","--mount",`type=volume,source=${r},target=${o}`,"-e",`POSTGRES_USER=${t}`,"-e",`POSTGRES_PASSWORD=${i}`,"-e",`POSTGRES_DB=${D}`,a,"postgres","-c",`max_connections=${String(c)}`]}export async function loadExistingLocalContainer(e,n={}){const r=U(e),o=l(u.join(r,"container_name"));if(!o)return null;const t=await inspectContainer(o,n);if(!t)return null;p(t,e,o);const i=t.Config?.Image||l(u.join(r,"image"))||R,a=l(u.join(r,"volume_mount_path"))||resolveLocalPostgresVolumeTarget(i),c=Y(t,a)||l(u.join(r,"volume_name"));if(!c)return null;await W(e,c,n),t.State?.Running||await y(["start",t.Id],`Docker local Postgres container start for ${o}`,LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.start,n.signal);const f=t.State?.Running?t:await inspectContainer(t.Id,n);f&&p(f,e,o);const w=q(f);if(!f||!w)return null;const b={containerName:o,containerId:f.Id,volumeName:c,volumeMountPath:a,image:i,user:l(u.join(r,"user"))||V,password:l(u.join(r,"password"))||$,maxConnections:F(l(u.join(r,"max_connections"))||N),host:"127.0.0.1",port:Number(w),containerPort:B(f)==="host"?Number(w):5432};return await J(b,n),b}function me(e,n){const r=e.Config?.Cmd;return Array.isArray(r)?r.includes("-c")&&r.includes(`max_connections=${String(n)}`):!1}function de(e,n,r){return(Array.isArray(e.Mounts)?e.Mounts:[]).some(t=>t?.Type==="volume"&&t.Name===n&&t.Destination===r)}export function isReusableLocalPostgresContainer(e,n){return e.Config?.Image===n.image&&me(e,n.maxConnections)&&de(e,n.volumeName,n.volumeMountPath)&&B(e)==="bridge"}function B(e){return e.HostConfig?.NetworkMode==="host"?"host":"bridge"}function q(e){return e?.NetworkSettings?.Ports?.["5432/tcp"]?.[0]?.HostPort||null}function F(e){const n=Number(e);if(!Number.isInteger(n)||n<10)throw new Error("database.maxConnections must be an integer greater than or equal to 10");return n}export async function inspectContainer(e,n={}){M("database.containerInspections");const r=`Docker local Postgres container inspect for ${e}`,o=["inspect",e],t=await oe(o,r,LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.inspect,n.signal,c=>L(o,r,c,n.signal));if(t.exitCode!==0){if(ne(t))return null;throw T(r,t)}let i;try{i=JSON.parse(t.stdout)}catch(c){throw ee(r,t,c)}const a=P(i)&&i.length===1?normalizeLocalPostgresContainerInspect(i[0]):null;if(!a?.Id)throw ee(r,t);return{...a,Id:a.Id}}export function normalizeLocalPostgresContainerInspect(e){return m(e)?{Config:he(e.Config),HostConfig:Pe(e.HostConfig),Id:g(e.Id),Mounts:ke(e.Mounts),NetworkSettings:ye(e.NetworkSettings),State:_e(e.State)}:null}export async function stopAndRemoveContainer(e,n=e,r={}){const o=`Docker local Postgres container removal for ${n}`,t=await L(["rm","-f","-v",e],o,LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.remove,r.signal);if(!(t.exitCode===0||ne(t)))throw T(o,t)}async function ge(e,n,r={}){const o=k(e,n,"existing-generation"),t=await h(n,r);if(t){if(!matchesStableDockerLabels(Z(t)||{},o))throw new Error(`Docker volume ${n} already exists but is not owned by this Testkit product. Remove or rename it before running Testkit.`);return n}return K(e,r)}async function K(e,n){const r=C(),o=x(e,r),t=k(e,o,r);await y(["volume","create",...H(t),o],`Docker local Postgres volume creation for ${o}`,LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.volumeCreate,n.signal);const i=E(await h(o,n));if(i===null||!we(i.labels,t))throw new Error(`Docker volume ${o} was not published with the expected Testkit generation identity`);return o}async function W(e,n,r){const o=k(e,n,"existing-generation"),t=E(await h(n,r));if(t===null||!matchesStableDockerLabels(t.labels,o))throw new Error(`Docker volume ${n} is missing or is not owned by this Testkit product`);return t}export function matchesStableDockerLabels(e,n){const r=e[z],o=e[v];return typeof r=="string"&&r.length>0&&typeof o=="string"&&o.length>0&&Object.entries(n).every(([t,i])=>t===z||t===v||e[t]===I(i))}function we(e,n){const r=Object.entries(e).sort(([t],[i])=>t.localeCompare(i)),o=Object.entries(n).map(([t,i])=>[t,I(i)]).sort(([t],[i])=>t.localeCompare(i));return r.length===o.length&&r.every(([t,i],a)=>o[a]?.[0]===t&&o[a]?.[1]===i)}async function be(e,n,r={}){const o=k(e,n,"existing-generation"),t=await h(n,r);if(!t)return;const i=Z(t)||{};if(!matchesStableDockerLabels(i,o))return;const c=E(t);c!==null&&await ce(n,c,r)}function Y(e,n){return(Array.isArray(e?.Mounts)?e.Mounts:[]).find(t=>t?.Type==="volume"&&typeof t.Name=="string"&&t.Name.length>0&&t.Destination===n)?.Name||null}function p(e,n,r){const o=X(n,r);if(!matchesStableDockerLabels(e.Config?.Labels||{},o))throw new Error(`Docker container ${r} already exists but is not owned by this Testkit product. Remove or rename it before running Testkit.`)}export function resolveLocalPostgresVolumeTarget(e){return pe(e)>=18?fe:le}function pe(e){const r=I(e||"").match(/(?:^|[:@/-])(?:pg|postgres)?(\d{2})(?:\D|$)/i);return r?Number(r[1]):16}async function J(e,n={}){M("database.hostReadinessChecks");const r=Date.now();let o=10;for(;Date.now()-r<j;){const t=Math.max(1,j-(Date.now()-r));if((await L(["exec",e.containerId,"pg_isready","-h","127.0.0.1","-U",e.user,"-d",D,"-p",String(e.containerPort||5432)],`Docker local Postgres readiness probe for ${e.containerName}`,Math.min(LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.readinessAttempt,t),n.signal)).exitCode===0)return;await Ce(Math.min(o,t),n.signal),o=Math.min(o*2,ae)}throw new Error(`Timed out waiting for local database container ${e.containerName}`)}function X(e,n,r="existing-generation"){return G(e,{generation:r,kind:"postgres-container",name:n,scope:"local-postgres",cachePolicy:"product-cache"})}function k(e,n,r){return G(e,{generation:r,kind:"postgres-volume",name:n,scope:"local-postgres",cachePolicy:"product-cache"})}function Ce(e,n){return new Promise((r,o)=>{if(n?.aborted){o(Q(n));return}const t=setTimeout(i,e);n?.addEventListener("abort",a,{once:!0});function i(){n?.removeEventListener("abort",a),r()}function a(){clearTimeout(t),o(n?Q(n):new Error("Local Postgres operation aborted"))}})}function Q(e){return e.reason instanceof Error?e.reason:new Error("Local Postgres operation aborted",{cause:e.reason})}function Z(e){if(!m(e))return null;const n=e.Labels;return m(n)?n:null}function he(e){if(e===null)return null;if(m(e))return{Cmd:Se(e.Cmd),Image:g(e.Image),Labels:Le(e.Labels)}}function Le(e){if(!m(e))return null;const n={};for(const[r,o]of Object.entries(e))typeof o=="string"&&(n[r]=o);return n}function Pe(e){if(e===null)return null;if(m(e))return{NetworkMode:g(e.NetworkMode)}}function ke(e){if(e===null)return null;if(P(e))return e.flatMap(n=>m(n)?[{Destination:g(n.Destination),Name:g(n.Name),Type:g(n.Type)}]:[])}function ye(e){if(e===null)return null;if(!m(e))return;const n=e.Ports;if(n===null)return{Ports:null};if(!m(n))return{};const r={};for(const[o,t]of Object.entries(n)){if(t===null){r[o]=null;continue}P(t)&&(r[o]=t.flatMap(i=>{if(!m(i))return[];const a=g(i.HostPort);return i.HostPort!==void 0&&a===void 0?[]:[{HostPort:a}]}))}return{Ports:r}}function _e(e){if(e===null)return null;if(m(e))return{Running:e.Running===null||typeof e.Running=="boolean"?e.Running:void 0}}function g(e){if(e===null||typeof e=="string")return e}function Se(e){if(e===null)return null;if(P(e))return e.filter(n=>typeof n=="string")}async function y(e,n,r,o){const t=await L(e,n,r,o);if(t.exitCode===0)return t;throw T(n,t)}function T(e,n){const r=n.stderr.trim()||n.stdout.trim(),o=new Error(`${e} failed with exit code ${String(n.exitCode)}${r?`: ${r}`:""}`);return Object.defineProperty(o,"result",{enumerable:!1,value:Object.freeze({...n})}),o}function ee(e,n,r){const o=new Error(`${e} returned malformed Docker inspect output; expected exactly one container record with a non-empty Id`,r===void 0?void 0:{cause:r});return Object.defineProperty(o,"result",{enumerable:!1,value:Object.freeze({...n})}),o}function ne(e){return/\bno such (?:container|object)\b/iu.test(`${e.stderr}
2
2
  ${e.stdout}`)}
@@ -1 +1 @@
1
- import p from"crypto";import f from"fs";import l from"path";export function buildDatabaseUrl(e,t){const n=e.sslMode||"disable";return`postgresql://${encodeURIComponent(e.user)}:${encodeURIComponent(e.password)}@${e.host}:${String(e.port)}/${t}?sslmode=${encodeURIComponent(n)}`}export function buildContainerName(e){const t=u(e);return i("testkit_pg",l.basename(t),[hashString(t,10)],63)}export function buildVolumeName(e,t){const n=u(e);if(!t)throw new Error("Local Postgres volume generation must be non-empty");return i("testkit_pgdata",l.basename(n),[hashString(n,10),hashString(t,32)],128)}export function buildTemplateDatabaseName(e,t){return i("tk_tpl",e,[t.slice(0,16)],63)}export function buildRuntimeDatabaseName(e,t,n){return i("tk",e,[hashString(t,10),n.slice(0,12)],63)}function i(e,t,n,a){const c=`${e}_`,s=slugSegment(t);let r=`_${n.join("_")}`,o=a-c.length-r.length;if(s.length>o&&(r=`_${hashString(t,10)}${r}`,o=a-c.length-r.length),o<1)throw new Error(`Cannot retain the identity suffix within a ${String(a)}-character identifier`);const d=limitIdentifier(s,o);return`${c}${d}${r}`}function u(e){const t=l.resolve(e);try{return f.realpathSync.native(t)}catch(n){if(m(n,"ENOENT"))return t;throw n}}function m(e,t){return typeof e=="object"&&e!==null&&"code"in e&&Reflect.get(e,"code")===t}export function hashString(e,t=12){return p.createHash("sha256").update(e).digest("hex").slice(0,t)}export function slugSegment(e){return e.toLowerCase().replace(/[^a-z0-9]+/g,"_").replace(/^_+|_+$/g,"")||"db"}export function limitIdentifier(e,t){return e.length<=t?e:e.slice(0,t)}export function escapeIdentifier(e){return String(e).replace(/"/g,'""')}export function escapeSqlLiteral(e){return String(e).replace(/'/g,"''")}
1
+ import p from"crypto";import f from"fs";import s from"path";import{hasErrorCode as m}from"../kernel/error-serialization.js";export function buildDatabaseUrl(e,t){const r=e.sslMode||"disable";return`postgresql://${encodeURIComponent(e.user)}:${encodeURIComponent(e.password)}@${e.host}:${String(e.port)}/${t}?sslmode=${encodeURIComponent(r)}`}export function buildContainerName(e){const t=u(e);return i("testkit_pg",s.basename(t),[hashString(t,10)],63)}export function buildVolumeName(e,t){const r=u(e);if(!t)throw new Error("Local Postgres volume generation must be non-empty");return i("testkit_pgdata",s.basename(r),[hashString(r,10),hashString(t,32)],128)}export function buildTemplateDatabaseName(e,t){return i("tk_tpl",e,[t.slice(0,16)],63)}export function buildRuntimeDatabaseName(e,t,r){return i("tk",e,[hashString(t,10),r.slice(0,12)],63)}function i(e,t,r,a){const c=`${e}_`,l=slugSegment(t);let n=`_${r.join("_")}`,o=a-c.length-n.length;if(l.length>o&&(n=`_${hashString(t,10)}${n}`,o=a-c.length-n.length),o<1)throw new Error(`Cannot retain the identity suffix within a ${String(a)}-character identifier`);const d=limitIdentifier(l,o);return`${c}${d}${n}`}function u(e){const t=s.resolve(e);try{return f.realpathSync.native(t)}catch(r){if(m(r,"ENOENT"))return t;throw r}}export function hashString(e,t=12){return p.createHash("sha256").update(e).digest("hex").slice(0,t)}export function slugSegment(e){return e.toLowerCase().replace(/[^a-z0-9]+/g,"_").replace(/^_+|_+$/g,"")||"db"}export function limitIdentifier(e,t){return e.length<=t?e:e.slice(0,t)}export function escapeIdentifier(e){return String(e).replace(/"/g,'""')}export function escapeSqlLiteral(e){return String(e).replace(/'/g,"''")}
@@ -6,7 +6,7 @@ export interface ResolvedSchemaOperationPlan {
6
6
  configs: readonly DatabaseRuntimeConfig[];
7
7
  target: DatabaseRuntimeConfig;
8
8
  }
9
- export type SchemaOperationConfigResolver = (runtimeRootDir: string) => ResolvedSchemaOperationPlan;
9
+ export type SchemaOperationConfigResolver = (runtimeRootDir: string) => Promise<ResolvedSchemaOperationPlan>;
10
10
  export interface DatabaseSchemaOperationContext {
11
11
  progress?: DatabaseProgress | null;
12
12
  signal?: AbortSignal;
@@ -1 +1 @@
1
- import u from"fs";import{resolveContainedTestkitStateDirectory as f}from"../process/runs/product-state.js";import{makeTempDir as p,removeTempDir as h}from"../process/files/temp-dir.js";import{cleanupOrphanedLocalInfrastructure as y,destroyRuntimeDatabase as l,provisionDatabase as g}from"./index.js";import{forceRefreshSourceSchemaCache as S}from"./schema-source.js";export async function verifyDatabaseSchema(r,o,t={}){await m(r,"verify",o,t,async(e,s)=>{for(const a of e.configs)await s(a)})}export async function refreshDatabaseSchema(r,o,t={}){return m(r,"refresh",o,t,async(e,s)=>{for(const a of e.configs){if(a.name===e.target.name)break;await s(a)}return S(e.target,null,{reporter:t.progress?.reporter||null,setupRegistry:t.progress?.setupRegistry||null,signal:t.signal})})}async function m(r,o,t,e,s){const a=v(r,o),c=[];try{const n=t(a.path);return await s(n,async i=>{i.testkit.database&&(c.push(i.stateDir),await g(i,{progress:e.progress,signal:e.signal,skipSchemaSourceVerify:e.skipSchemaSourceVerify}))})}finally{for(const n of c.reverse())await l({productDir:r,stateDir:n}).catch(()=>{});await y(r).catch(()=>{}),a.remove()}}function v(r,o){const t=f(r);u.mkdirSync(t,{recursive:!0});const e=f(r),s=p(`_schema-${o}-`,{parentDir:e});let a=!1;return{path:s,remove(){a||(a=!0,h(s))}}}
1
+ import u from"fs";import{resolveContainedTestkitStateDirectory as f}from"../process/runs/product-state.js";import{makeTempDir as p,removeTempDir as h}from"../process/files/temp-dir.js";import{cleanupOrphanedLocalInfrastructure as y,destroyRuntimeDatabase as l,provisionDatabase as g}from"./index.js";import{forceRefreshSourceSchemaCache as S}from"./schema-source.js";export async function verifyDatabaseSchema(r,o,t={}){await m(r,"verify",o,t,async(e,s)=>{for(const a of e.configs)await s(a)})}export async function refreshDatabaseSchema(r,o,t={}){return m(r,"refresh",o,t,async(e,s)=>{for(const a of e.configs){if(a.name===e.target.name)break;await s(a)}return S(e.target,null,{reporter:t.progress?.reporter||null,setupRegistry:t.progress?.setupRegistry||null,signal:t.signal})})}async function m(r,o,t,e,s){const a=v(r,o),c=[];try{const n=await t(a.path);return await s(n,async i=>{i.testkit.database&&(c.push(i.stateDir),await g(i,{progress:e.progress,signal:e.signal,skipSchemaSourceVerify:e.skipSchemaSourceVerify}))})}finally{for(const n of c.reverse())await l({productDir:r,stateDir:n}).catch(()=>{});await y(r).catch(()=>{}),a.remove()}}function v(r,o){const t=f(r);u.mkdirSync(t,{recursive:!0});const e=f(r),s=p(`_schema-${o}-`,{parentDir:e});let a=!1;return{path:s,remove(){a||(a=!0,h(s))}}}
@@ -1,3 +1,3 @@
1
- import{recordWork as l}from"../kernel/work-metrics.js";import o from"fs";import s from"path";import{createRequire as J}from"node:module";import{collectRepoState as V,summarizeRepoStateForMetadata as _}from"../repo/state.js";import{findDatabasePlaceholders as X,resolveExecutionEnvValues as Z}from"../steps/index.js";import{dumpPostgresSchemaToFile as G,captureTemplateSnapshotText as Q,resolvePgDumpClientImage as Y,runTemplateStep as ee,sanitizeSnapshotText as re}from"./template-steps.js";import{getSourceSchemaRefreshLockPath as te,withSourceSchemaRefreshLock as ae}from"./source-refresh-lock.js";import{resolveSourceSchemaDumpUrl as ce}from"./source-url.js";import{makeTempDir as ne,removeTempDir as se}from"../process/files/temp-dir.js";import{requireContainedDatabaseStateDirectory as k,resolveContainedDatabaseStateDirectory as w}from"./state.js";const K=J(import.meta.url);let P;function ie(){if(P)return P;const e=K.resolve("diff/package.json"),t=JSON.parse(o.readFileSync(e,"utf8"))?.exports?.["."]?.import?.default;if(typeof t!="string"||!t.startsWith("./"))throw new Error("The diff package must declare a local ESM import entry");const c=s.dirname(e),a=s.resolve(c,t),n=s.relative(c,a);if(!n||n===".."||n.startsWith(`..${s.sep}`)||s.isAbsolute(n))throw new Error("The diff package ESM import entry must stay within its package directory");return P=K(a)}const U="db",$="source-schemas",b="source-schema.sql",E="source-schema.meta.json",oe="index.json",M=20,y=String;export function getSourceSchemaRootDir(e){return w(e.productDir,U,e.name,$)}export async function getSourceSchemaCachePath(e,r={}){return(await resolveSourceSchemaCacheLocation(e,r)).cachePath}export async function resolveSourceSchemaCacheLocation(e,r={}){const t=r.repoState||await V(e.productDir,{fingerprints:e.testkit?.fingerprints}),c=getSourceSchemaRootDir(e),a=w(e.productDir,U,e.name,$,...t.cacheKey.split("/").map(D));return{rootDir:c,cacheDir:a,cachePath:s.join(a,b),metadataPath:s.join(a,E),indexPath:s.join(c,oe),cacheKey:t.cacheKey,cacheKind:t.kind,repoState:t}}export async function resolveSourceSchemaState(e,r={}){const t=e.testkit.database?.sourceSchema||null;if(!t)return{active:!1};const c=await resolveSourceSchemaCacheLocation(e,r),a=c.cachePath,n=o.existsSync(a);n&&x(a);const i=r.env||process.env,u=t.kind==="env"?[t.env]:[j(e.name),"TESTKIT_SCHEMA_SOURCE_DATABASE_URL"];Ae(e,u);const h=Z(e,[...u,"CI"],i),m=u.find(z=>{const R=h[z];return typeof R=="string"&&R.trim().length>0})||null,S=m?h[m]:void 0,p=typeof S=="string"?S.trim():null;return t.kind==="env"||!!p||n?{active:!0,cachePath:a,metadataPath:c.metadataPath,indexPath:c.indexPath,rootDir:c.rootDir,cacheDir:c.cacheDir,cacheKey:c.cacheKey,cacheKind:c.cacheKind,repoState:c.repoState,envName:m,sourceUrl:p,sourceSchema:t,cacheExists:n,refreshed:!1,ci:h.CI==="true",unavailableMode:ge(t.unavailable,i)}:{active:!1,cachePath:a}}export async function prepareSourceSchemaCache(e,r={}){r.signal?.throwIfAborted();const t=await resolveSourceSchemaState(e,r);if(!t.active)return t;if(B(t)){if(pe(t))try{const a=await L(e,t,r),n={...t,refreshed:!0,cacheExists:!0,refreshInfo:a};return v(e,n,{refreshed:!0}),n}catch(a){r.signal?.throwIfAborted();const n=ue(e,t,a,r);if(n)return n;throw a}return v(e,t),r.setupRegistry?.recordCached({config:e,stage:"source-schema",kind:"database-source-schema",summary:"source schema cache hit"}),t}if(t.cacheExists&&t.unavailableMode==="warn-cache")return v(e,t),r.setupRegistry?.recordCached({config:e,stage:"source-schema",kind:"database-source-schema",summary:"source unavailable; using cached source schema"}),t;const c=t.sourceSchema.kind==="env"?t.sourceSchema.env:`${j(e.name)} or TESTKIT_SCHEMA_SOURCE_DATABASE_URL`;throw new Error(`Source schema database URL is unavailable for service "${e.name}". Set ${c}.`)}function ue(e,r,t,c={}){if(r.unavailableMode!=="warn-cache")return null;const a=he(e,r);if(!a)return null;const n={...r,...a,cacheExists:!0,refreshed:!1,refreshError:Ce(t)};return c.setupRegistry?.recordCached({config:e,stage:"source-schema",kind:"database-source-schema",summary:`source unavailable; using cached source schema: ${s.relative(e.productDir,a.cachePath)}`}),n}function he(e,r){const t=F(e,r,{cachePath:r.cachePath,metadataPath:r.metadataPath,cacheKey:r.cacheKey,cacheKind:r.cacheKind});if(t)return t;const c=O(r.indexPath);c.sort((a,n)=>Date.parse(n.lastUsedAt||n.refreshedAt||"0")-Date.parse(a.lastUsedAt||a.refreshedAt||"0"));for(const a of c){if(!a.cachePath)continue;const n=s.resolve(e.productDir,a.cachePath),i=F(e,r,{cachePath:n,metadataPath:s.join(s.dirname(n),E),cacheKey:a.cacheKey,cacheKind:a.kind||"unknown"});if(i)return i}return null}function F(e,r,t){const c=s.resolve(t.cachePath);if(s.basename(c)!==b)return null;const a=k(e.productDir,s.dirname(c),`Service "${e.name}" source schema cache directory`),n=s.join(a,b),i=s.resolve(r.rootDir);if(n!==i&&!n.startsWith(`${i}${s.sep}`)||!o.existsSync(n))return null;const u=o.lstatSync(n);if(u.isSymbolicLink()||!u.isFile())throw new Error(`Refusing unsafe source schema cache file at ${n}`);return{cachePath:n,metadataPath:s.join(a,E),cacheDir:a,cacheKey:t.cacheKey,cacheKind:t.cacheKind,indexPath:r.indexPath,rootDir:r.rootDir}}export async function forceRefreshSourceSchemaCache(e,r,t={}){const c=await resolveSourceSchemaState(e,t);if(!c.active||!B(c)){const i=r?.active?r.envName:null,u=r?.active&&r.sourceSchema.kind==="env"?r.sourceSchema.env:null,h=i||u||"source schema env";throw new Error(`Cannot refresh source schema for service "${e.name}" because ${h} is unavailable.`)}const a=await L(e,c,t),n={...c,refreshed:!0,cacheExists:!0,refreshInfo:a};return v(e,n,{refreshed:!0}),n}export async function applySourceSchemaCache(e,r,t,c={}){if(!t?.active)return!1;if(!o.existsSync(t.cachePath))throw new Error(`Missing source schema cache for service "${e.name}": ${t.cachePath}`);const a={kind:"sql-file",path:s.relative(e.productDir,t.cachePath).split(s.sep).join("/"),cwd:null,inputs:[]};return l("schema.sourceReplays"),await ee(e,"source-schema",a,0,r,c),!0}export async function verifyLocalSchemaMatchesSource(e,r,t,c={}){if(!t?.active)return{status:"disabled"};if(!t.sourceSchema.verify||c.skipSchemaSourceVerify)return c.setupRegistry?.recordCached({config:e,stage:"source-schema:verify",kind:"database-source-schema",summary:"source schema verification skipped"}),{status:"skipped"};De(e);const a=fe(t.cachePath);l("schema.verificationDumps");const n=await Q(e,r,{image:await c.resolveLocalSnapshotImage?.(),ownedContainer:c.ownedContainer||null,reporter:c.reporter||null,signal:c.signal});return l("schema.comparisons"),l("schema.bytesCompared",Buffer.byteLength(a)+Buffer.byteLength(n)),n===a?{status:"matched"}:{status:"mismatch",diagnostics:await Pe(e,t,a,n)}}export function createSourceSchemaMismatchError(e,r,t){const c=[`Local schema replay does not match source schema for service "${e.name}".`,`Source cache: ${s.relative(e.productDir,r.cachePath)}`];return t?.status==="mismatch"&&c.push(`Diff: ${s.relative(e.productDir,t.diagnostics.diffPath)}`),new Error(c.join(`
2
- `))}export async function appendSourceSchemaCacheToHash(e,r,t=null){if(!(r.testkit.database?.sourceSchema||null))return;const a=t?.cachePath||await getSourceSchemaCachePath(r),n=t?.active&&t.cacheKey?t.cacheKey:s.relative(getSourceSchemaRootDir(r),s.dirname(a));if(e.update(`source-schema-key:${n}`),e.update(`source-schema-cache:${s.relative(r.productDir,a)}`),!o.existsSync(a)){e.update(":missing");return}const i=o.statSync(a);e.update(`:${i.size.toString()}:${i.mtimeMs.toString()}`);const u=o.readFileSync(a);l("schema.cacheHashFilesRead"),l("schema.cacheHashBytesRead",u.length),e.update(u)}async function L(e,r,t={}){const c=t.setupRegistry,a=c?.start({config:e,stage:"source-schema:refresh",kind:"database-source-schema",summary:`source schema refresh: ${s.relative(e.productDir,r.cachePath)}`});try{const n=await ae(te(r.cachePath),async({waited:u,requestedAt:h})=>{const m=de(r,h);if(m)return{reusedExistingRefresh:!0,waitedForLock:u,metadata:m};const S=await me(e,r.sourceUrl,r.cachePath,{reporter:t.reporter||null,logRecord:a?._logRecord||null,env:t.env||process.env,signal:t.signal}),p=le(e,r,S);return ye(r.metadataPath,p),{reusedExistingRefresh:!1,waitedForLock:u,metadata:p}},{signal:t.signal}),i=a&&c?c.finish(a,{status:"passed",summary:`source schema refresh: ${s.relative(e.productDir,r.cachePath)}`}):null;return i&&t.reporter?.setupOperationFinished?.(i),n}catch(n){const i=a?c?.finish(a,{status:"failed",summary:`source schema refresh: ${s.relative(e.productDir,r.cachePath)}`,error:Re(n)}):null;throw i&&t.reporter?.setupOperationFinished?.(i),n}}async function me(e,r,t,c={}){const a=ce(r);o.mkdirSync(s.dirname(t),{recursive:!0});const n=ne(".source-schema-",{parentDir:s.dirname(t)}),i=s.join(n,s.basename(t));try{const u=await Y(e,a.dumpUrl,{env:c.env||process.env,pgApplicationName:g(e.name),signal:c.signal});return l("schema.sourceDumps"),await G(e,i,a.dumpUrl,{...c,image:u,pgApplicationName:g(e.name)}),o.renameSync(i,t),{resolution:a,pgDump:{args:["--schema-only","--no-owner","--no-privileges"],applicationName:g(e.name),image:u}}}finally{se(n)}}function le(e,r,t){return{refreshedAt:new Date().toISOString(),serviceName:e.name,envName:r.envName,cacheKey:r.cacheKey,cacheKind:r.cacheKind,cachePath:s.relative(e.productDir,r.cachePath),repo:_(r.repoState),sourceUrl:t.resolution.metadata,pgDump:t.pgDump}}function de(e,r){if(!o.existsSync(e.cachePath))return null;const t=A(e.metadataPath);if(!Ke(t))return null;const c=Date.parse(t.refreshedAt);return!Number.isFinite(c)||c<r?null:t}function g(e){return`testkit:schema-source:${e.replace(/[^a-zA-Z0-9._:-]+/g,"-").slice(0,24)||"service"}`}function fe(e){const r=o.readFileSync(e);return l("schema.cacheFilesRead"),l("schema.cacheBytesRead",r.length),re(r.toString("utf8"))}function pe(e){if(!e.cacheExists)return!0;if(e.sourceSchema.refresh.mode==="auto")return e.ci;if(e.sourceSchema.refresh.mode==="ttl"){const r=C(e.metadataPath),t=r?Date.parse(r):0;return!Number.isFinite(t)||t<=0?!0:Date.now()-t>=e.sourceSchema.refresh.ttlSeconds*1e3}return!0}function v(e,r,t={}){if(!r?.active)return;const c=new Date().toISOString(),a=O(r.indexPath).filter(u=>u.cacheKey!==r.cacheKey),n=C(r.metadataPath);a.push({cacheKey:r.cacheKey,kind:r.cacheKind,commitSha:r.repoState.commitSha||null,branch:r.repoState.branch||null,detached:r.repoState.detached,dirty:r.repoState.dirty,dirtyFingerprint:r.repoState.dirtyFingerprint||null,contentFingerprint:r.repoState.contentFingerprint||null,remoteUrl:r.repoState.remoteUrl||null,repository:r.repoState.repository,cachePath:s.relative(e.productDir,r.cachePath),refreshedAt:t.refreshed?c:n,lastUsedAt:c});const i=Se(e,r,a);N(r.indexPath,{version:1,entries:i})}function Se(e,r,t){if(t.length<=M)return t;const c=[...t].sort((n,i)=>Date.parse(i.lastUsedAt||"0")-Date.parse(n.lastUsedAt||"0")),a=new Set(c.slice(0,M).map(n=>n.cacheKey));a.add(r.cacheKey);for(const n of t){if(a.has(n.cacheKey))continue;const i=n.cachePath?s.join(e.productDir,n.cachePath):null,u=i?s.resolve(i):null,h=s.resolve(r.rootDir);if(!i||!u||u!==h&&!u.startsWith(`${h}${s.sep}`))continue;const m=k(e.productDir,s.dirname(i),`Service "${e.name}" pruned source schema cache directory`);o.rmSync(m,{recursive:!0,force:!0})}return t.filter(n=>a.has(n.cacheKey))}function ye(e,r){I(e,r)}function N(e,r){I(e,r)}function I(e,r){o.mkdirSync(s.dirname(e),{recursive:!0}),x(e),o.writeFileSync(e,xe(r))}function A(e){if(!e||!o.existsSync(e))return null;x(e);try{return JSON.parse(o.readFileSync(e,"utf8"))}catch{return null}}function T(e){return w(e.productDir,"results","schema")}function x(e){if(!o.existsSync(e))return;const r=o.lstatSync(e);if(r.isSymbolicLink()||!r.isFile())throw new Error(`Refusing unsafe source schema state file at ${e}`)}function ve(e){return`${D(e.runtimeLabel||e.name)}__${D(e.name)}`}function De(e){const r=T(e);if(!o.existsSync(r))return;const t=D(e.name);for(const c of o.readdirSync(r))we(c,t)&&o.rmSync(s.join(r,c),{force:!0})}function we(e,r){for(const t of["__source-schema.sql","__local-replay-schema.sql","__schema.diff","__schema.meta.json"])if(e.endsWith(t))return e.slice(0,-t.length).endsWith(`__${r}`);return!1}async function Pe(e,r,t,c){const a=T(e);o.mkdirSync(a,{recursive:!0});const n=ve(e),i=s.join(a,`${n}__source-schema.sql`),u=s.join(a,`${n}__local-replay-schema.sql`),h=s.join(a,`${n}__schema.diff`),m=s.join(a,`${n}__schema.meta.json`);return o.writeFileSync(i,t),o.writeFileSync(u,c),o.writeFileSync(h,await Ee(i,u)),N(m,be(e,r)),{sourceCachePath:r.cachePath,sourcePath:i,localPath:u,diffPath:h,metaPath:m}}function be(e,r){const t=C(r.metadataPath);return{version:1,createdAt:new Date().toISOString(),serviceName:e.name,runtimeLabel:e.runtimeLabel||e.name,sourceSchema:{envName:r.envName||null,cacheKey:r.cacheKey||null,cacheKind:r.cacheKind||null,cachePath:s.relative(e.productDir,r.cachePath),refreshedAt:t},repo:_(r.repoState)}}function Ee(e,r){return new Promise(t=>{t(buildUnifiedDiffText(o.readFileSync(e,"utf8"),o.readFileSync(r,"utf8"),e,r))})}export function buildUnifiedDiffText(e,r,t="source",c="target"){if(e===r)return"";const{createTwoFilesPatch:a}=ie();return a(t,c,y(e),y(r),"","",{context:3})}function ge(e,r){return e==="fail"||e==="warn-cache"?e:r.CI==="true"?"fail":"warn-cache"}function j(e){return`TESTKIT_${e.toUpperCase().replace(/[^A-Z0-9]+/g,"_")}_SCHEMA_SOURCE_DATABASE_URL`}function Ae(e,r){const t={...e.testkit.serviceEnv,...e.testkit.local?.env};for(const c of r){const a=t[c];if(typeof a!="string")continue;const n=X(a).find(i=>(i.serviceName??e.name)===e.name);if(n)throw new Error(`Service "${e.name}" env ${c} uses ${n.placeholder}, which is the database the source schema verifies. Point it at another service's database or an external database URL.`)}}function D(e){return e.trim().toLowerCase().replace(/[^a-z0-9._-]+/g,"-").replace(/^-+|-+$/g,"")||"schema"}function O(e){const r=A(e);return!f(r)||!Array.isArray(r.entries)?[]:r.entries.filter(_e)}function C(e){const r=A(e);return f(r)&&typeof r.refreshedAt=="string"?r.refreshedAt:null}function xe(e){return`${y(JSON.stringify(e,null,2))}
3
- `}function Ce(e){const r=H(e,"message");return!!r?r:y(e)}function Re(e){const r=H(e,"message");return!!r?r:e}function H(e,r){if(!(e===null||typeof e!="object"&&typeof e!="function"))return Reflect.get(e,r)}function B(e){return!!e.sourceUrl}function _e(e){return!f(e)||typeof e.cacheKey!="string"||e.cacheKey.length===0?!1:d(e.branch)&&d(e.cachePath)&&d(e.commitSha)&&d(e.contentFingerprint)&&W(e.detached)&&W(e.dirty)&&d(e.dirtyFingerprint)&&d(e.kind)&&d(e.lastUsedAt)&&d(e.refreshedAt)&&d(e.remoteUrl)&&ke(e.repository)}function ke(e){return e==null||f(e)&&typeof e.host=="string"&&typeof e.owner=="string"&&typeof e.name=="string"}function Ke(e){return f(e)&&typeof e.refreshedAt=="string"&&(e.sourceUrl===void 0||Ue(e.sourceUrl))}function Ue(e){return f(e)&&typeof e.originalHost=="string"&&q(e.originalClassification)&&typeof e.resolvedHost=="string"&&q(e.resolvedClassification)&&typeof e.rewritten=="boolean"}function q(e){return e==="neon-pooler"||e==="neon-direct"||e==="unsupported-pooler"||e==="unknown-direct"}function $e(e){return e===null||typeof e=="string"}function d(e){return e===void 0||$e(e)}function W(e){return e===void 0||typeof e=="boolean"}function f(e){return e!==null&&typeof e=="object"&&!Array.isArray(e)}
1
+ import{recordWork as l}from"../kernel/work-metrics.js";import o from"fs";import s from"path";import{createRequire as J}from"node:module";import{collectRepoState as V,summarizeRepoStateForMetadata as _}from"../repo/state.js";import{findDatabasePlaceholders as X,resolveExecutionEnvValues as Z}from"../steps/index.js";import{dumpPostgresSchemaToFile as G,captureTemplateSnapshotText as Q,resolvePgDumpClientImage as Y,runTemplateStep as ee,sanitizeSnapshotText as re}from"./template-steps.js";import{getSourceSchemaRefreshLockPath as te,withSourceSchemaRefreshLock as ae}from"./source-refresh-lock.js";import{resolveSourceSchemaDumpUrl as ce}from"./source-url.js";import{makeTempDir as ne,removeTempDir as se}from"../process/files/temp-dir.js";import{requireContainedDatabaseStateDirectory as k,resolveContainedDatabaseStateDirectory as D}from"./state.js";import{isNullableString as ie,isUnknownRecord as f}from"../kernel/unknown-values.js";const K=J(import.meta.url);let P;function oe(){if(P)return P;const e=K.resolve("diff/package.json"),t=JSON.parse(o.readFileSync(e,"utf8"))?.exports?.["."]?.import?.default;if(typeof t!="string"||!t.startsWith("./"))throw new Error("The diff package must declare a local ESM import entry");const c=s.dirname(e),a=s.resolve(c,t),n=s.relative(c,a);if(!n||n===".."||n.startsWith(`..${s.sep}`)||s.isAbsolute(n))throw new Error("The diff package ESM import entry must stay within its package directory");return P=K(a)}const U="db",$="source-schemas",b="source-schema.sql",E="source-schema.meta.json",he="index.json",M=20,y=String;export function getSourceSchemaRootDir(e){return D(e.productDir,U,e.name,$)}export async function getSourceSchemaCachePath(e,r={}){return(await resolveSourceSchemaCacheLocation(e,r)).cachePath}export async function resolveSourceSchemaCacheLocation(e,r={}){const t=r.repoState||await V(e.productDir,{fingerprints:e.testkit?.fingerprints}),c=getSourceSchemaRootDir(e),a=D(e.productDir,U,e.name,$,...t.cacheKey.split("/").map(w));return{rootDir:c,cacheDir:a,cachePath:s.join(a,b),metadataPath:s.join(a,E),indexPath:s.join(c,he),cacheKey:t.cacheKey,cacheKind:t.kind,repoState:t}}export async function resolveSourceSchemaState(e,r={}){const t=e.testkit.database?.sourceSchema||null;if(!t)return{active:!1};const c=await resolveSourceSchemaCacheLocation(e,r),a=c.cachePath,n=o.existsSync(a);n&&x(a);const i=r.env||process.env,h=t.kind==="env"?[t.env]:[j(e.name),"TESTKIT_SCHEMA_SOURCE_DATABASE_URL"];xe(e,h);const u=Z(e,[...h,"CI"],i),m=h.find(z=>{const R=u[z];return typeof R=="string"&&R.trim().length>0})||null,S=m?u[m]:void 0,p=typeof S=="string"?S.trim():null;return t.kind==="env"||!!p||n?{active:!0,cachePath:a,metadataPath:c.metadataPath,indexPath:c.indexPath,rootDir:c.rootDir,cacheDir:c.cacheDir,cacheKey:c.cacheKey,cacheKind:c.cacheKind,repoState:c.repoState,envName:m,sourceUrl:p,sourceSchema:t,cacheExists:n,refreshed:!1,ci:u.CI==="true",unavailableMode:Ae(t.unavailable,i)}:{active:!1,cachePath:a}}export async function prepareSourceSchemaCache(e,r={}){r.signal?.throwIfAborted();const t=await resolveSourceSchemaState(e,r);if(!t.active)return t;if(B(t)){if(Se(t))try{const a=await L(e,t,r),n={...t,refreshed:!0,cacheExists:!0,refreshInfo:a};return v(e,n,{refreshed:!0}),n}catch(a){r.signal?.throwIfAborted();const n=ue(e,t,a,r);if(n)return n;throw a}return v(e,t),r.setupRegistry?.recordCached({config:e,stage:"source-schema",kind:"database-source-schema",summary:"source schema cache hit"}),t}if(t.cacheExists&&t.unavailableMode==="warn-cache")return v(e,t),r.setupRegistry?.recordCached({config:e,stage:"source-schema",kind:"database-source-schema",summary:"source unavailable; using cached source schema"}),t;const c=t.sourceSchema.kind==="env"?t.sourceSchema.env:`${j(e.name)} or TESTKIT_SCHEMA_SOURCE_DATABASE_URL`;throw new Error(`Source schema database URL is unavailable for service "${e.name}". Set ${c}.`)}function ue(e,r,t,c={}){if(r.unavailableMode!=="warn-cache")return null;const a=me(e,r);if(!a)return null;const n={...r,...a,cacheExists:!0,refreshed:!1,refreshError:Re(t)};return c.setupRegistry?.recordCached({config:e,stage:"source-schema",kind:"database-source-schema",summary:`source unavailable; using cached source schema: ${s.relative(e.productDir,a.cachePath)}`}),n}function me(e,r){const t=F(e,r,{cachePath:r.cachePath,metadataPath:r.metadataPath,cacheKey:r.cacheKey,cacheKind:r.cacheKind});if(t)return t;const c=O(r.indexPath);c.sort((a,n)=>Date.parse(n.lastUsedAt||n.refreshedAt||"0")-Date.parse(a.lastUsedAt||a.refreshedAt||"0"));for(const a of c){if(!a.cachePath)continue;const n=s.resolve(e.productDir,a.cachePath),i=F(e,r,{cachePath:n,metadataPath:s.join(s.dirname(n),E),cacheKey:a.cacheKey,cacheKind:a.kind||"unknown"});if(i)return i}return null}function F(e,r,t){const c=s.resolve(t.cachePath);if(s.basename(c)!==b)return null;const a=k(e.productDir,s.dirname(c),`Service "${e.name}" source schema cache directory`),n=s.join(a,b),i=s.resolve(r.rootDir);if(n!==i&&!n.startsWith(`${i}${s.sep}`)||!o.existsSync(n))return null;const h=o.lstatSync(n);if(h.isSymbolicLink()||!h.isFile())throw new Error(`Refusing unsafe source schema cache file at ${n}`);return{cachePath:n,metadataPath:s.join(a,E),cacheDir:a,cacheKey:t.cacheKey,cacheKind:t.cacheKind,indexPath:r.indexPath,rootDir:r.rootDir}}export async function forceRefreshSourceSchemaCache(e,r,t={}){const c=await resolveSourceSchemaState(e,t);if(!c.active||!B(c)){const i=r?.active?r.envName:null,h=r?.active&&r.sourceSchema.kind==="env"?r.sourceSchema.env:null,u=i||h||"source schema env";throw new Error(`Cannot refresh source schema for service "${e.name}" because ${u} is unavailable.`)}const a=await L(e,c,t),n={...c,refreshed:!0,cacheExists:!0,refreshInfo:a};return v(e,n,{refreshed:!0}),n}export async function applySourceSchemaCache(e,r,t,c={}){if(!t?.active)return!1;if(!o.existsSync(t.cachePath))throw new Error(`Missing source schema cache for service "${e.name}": ${t.cachePath}`);const a={kind:"sql-file",path:s.relative(e.productDir,t.cachePath).split(s.sep).join("/"),cwd:null,inputs:[]};return l("schema.sourceReplays"),await ee(e,"source-schema",a,0,r,c),!0}export async function verifyLocalSchemaMatchesSource(e,r,t,c={}){if(!t?.active)return{status:"disabled"};if(!t.sourceSchema.verify||c.skipSchemaSourceVerify)return c.setupRegistry?.recordCached({config:e,stage:"source-schema:verify",kind:"database-source-schema",summary:"source schema verification skipped"}),{status:"skipped"};De(e);const a=pe(t.cachePath);l("schema.verificationDumps");const n=await Q(e,r,{image:await c.resolveLocalSnapshotImage?.(),ownedContainer:c.ownedContainer||null,reporter:c.reporter||null,signal:c.signal});return l("schema.comparisons"),l("schema.bytesCompared",Buffer.byteLength(a)+Buffer.byteLength(n)),n===a?{status:"matched"}:{status:"mismatch",diagnostics:await be(e,t,a,n)}}export function createSourceSchemaMismatchError(e,r,t){const c=[`Local schema replay does not match source schema for service "${e.name}".`,`Source cache: ${s.relative(e.productDir,r.cachePath)}`];return t?.status==="mismatch"&&c.push(`Diff: ${s.relative(e.productDir,t.diagnostics.diffPath)}`),new Error(c.join(`
2
+ `))}export async function appendSourceSchemaCacheToHash(e,r,t=null){if(!(r.testkit.database?.sourceSchema||null))return;const a=t?.cachePath||await getSourceSchemaCachePath(r),n=t?.active&&t.cacheKey?t.cacheKey:s.relative(getSourceSchemaRootDir(r),s.dirname(a));if(e.update(`source-schema-key:${n}`),e.update(`source-schema-cache:${s.relative(r.productDir,a)}`),!o.existsSync(a)){e.update(":missing");return}const i=o.statSync(a);e.update(`:${i.size.toString()}:${i.mtimeMs.toString()}`);const h=o.readFileSync(a);l("schema.cacheHashFilesRead"),l("schema.cacheHashBytesRead",h.length),e.update(h)}async function L(e,r,t={}){const c=t.setupRegistry,a=c?.start({config:e,stage:"source-schema:refresh",kind:"database-source-schema",summary:`source schema refresh: ${s.relative(e.productDir,r.cachePath)}`});try{const n=await ae(te(r.cachePath),async({waited:h,requestedAt:u})=>{const m=fe(r,u);if(m)return{reusedExistingRefresh:!0,waitedForLock:h,metadata:m};const S=await le(e,r.sourceUrl,r.cachePath,{reporter:t.reporter||null,logRecord:a?._logRecord||null,env:t.env||process.env,signal:t.signal}),p=de(e,r,S);return ve(r.metadataPath,p),{reusedExistingRefresh:!1,waitedForLock:h,metadata:p}},{signal:t.signal}),i=a&&c?c.finish(a,{status:"passed",summary:`source schema refresh: ${s.relative(e.productDir,r.cachePath)}`}):null;return i&&t.reporter?.setupOperationFinished?.(i),n}catch(n){const i=a?c?.finish(a,{status:"failed",summary:`source schema refresh: ${s.relative(e.productDir,r.cachePath)}`,error:_e(n)}):null;throw i&&t.reporter?.setupOperationFinished?.(i),n}}async function le(e,r,t,c={}){const a=ce(r);o.mkdirSync(s.dirname(t),{recursive:!0});const n=ne(".source-schema-",{parentDir:s.dirname(t)}),i=s.join(n,s.basename(t));try{const h=await Y(e,a.dumpUrl,{env:c.env||process.env,pgApplicationName:g(e.name),signal:c.signal});return l("schema.sourceDumps"),await G(e,i,a.dumpUrl,{...c,image:h,pgApplicationName:g(e.name)}),o.renameSync(i,t),{resolution:a,pgDump:{args:["--schema-only","--no-owner","--no-privileges"],applicationName:g(e.name),image:h}}}finally{se(n)}}function de(e,r,t){return{refreshedAt:new Date().toISOString(),serviceName:e.name,envName:r.envName,cacheKey:r.cacheKey,cacheKind:r.cacheKind,cachePath:s.relative(e.productDir,r.cachePath),repo:_(r.repoState),sourceUrl:t.resolution.metadata,pgDump:t.pgDump}}function fe(e,r){if(!o.existsSync(e.cachePath))return null;const t=A(e.metadataPath);if(!Ue(t))return null;const c=Date.parse(t.refreshedAt);return!Number.isFinite(c)||c<r?null:t}function g(e){return`testkit:schema-source:${e.replace(/[^a-zA-Z0-9._:-]+/g,"-").slice(0,24)||"service"}`}function pe(e){const r=o.readFileSync(e);return l("schema.cacheFilesRead"),l("schema.cacheBytesRead",r.length),re(r.toString("utf8"))}function Se(e){if(!e.cacheExists)return!0;if(e.sourceSchema.refresh.mode==="auto")return e.ci;if(e.sourceSchema.refresh.mode==="ttl"){const r=C(e.metadataPath),t=r?Date.parse(r):0;return!Number.isFinite(t)||t<=0?!0:Date.now()-t>=e.sourceSchema.refresh.ttlSeconds*1e3}return!0}function v(e,r,t={}){if(!r?.active)return;const c=new Date().toISOString(),a=O(r.indexPath).filter(h=>h.cacheKey!==r.cacheKey),n=C(r.metadataPath);a.push({cacheKey:r.cacheKey,kind:r.cacheKind,commitSha:r.repoState.commitSha||null,branch:r.repoState.branch||null,detached:r.repoState.detached,dirty:r.repoState.dirty,dirtyFingerprint:r.repoState.dirtyFingerprint||null,contentFingerprint:r.repoState.contentFingerprint||null,remoteUrl:r.repoState.remoteUrl||null,repository:r.repoState.repository,cachePath:s.relative(e.productDir,r.cachePath),refreshedAt:t.refreshed?c:n,lastUsedAt:c});const i=ye(e,r,a);N(r.indexPath,{version:1,entries:i})}function ye(e,r,t){if(t.length<=M)return t;const c=[...t].sort((n,i)=>Date.parse(i.lastUsedAt||"0")-Date.parse(n.lastUsedAt||"0")),a=new Set(c.slice(0,M).map(n=>n.cacheKey));a.add(r.cacheKey);for(const n of t){if(a.has(n.cacheKey))continue;const i=n.cachePath?s.join(e.productDir,n.cachePath):null,h=i?s.resolve(i):null,u=s.resolve(r.rootDir);if(!i||!h||h!==u&&!h.startsWith(`${u}${s.sep}`))continue;const m=k(e.productDir,s.dirname(i),`Service "${e.name}" pruned source schema cache directory`);o.rmSync(m,{recursive:!0,force:!0})}return t.filter(n=>a.has(n.cacheKey))}function ve(e,r){I(e,r)}function N(e,r){I(e,r)}function I(e,r){o.mkdirSync(s.dirname(e),{recursive:!0}),x(e),o.writeFileSync(e,Ce(r))}function A(e){if(!e||!o.existsSync(e))return null;x(e);try{return JSON.parse(o.readFileSync(e,"utf8"))}catch{return null}}function T(e){return D(e.productDir,"results","schema")}function x(e){if(!o.existsSync(e))return;const r=o.lstatSync(e);if(r.isSymbolicLink()||!r.isFile())throw new Error(`Refusing unsafe source schema state file at ${e}`)}function we(e){return`${w(e.runtimeLabel||e.name)}__${w(e.name)}`}function De(e){const r=T(e);if(!o.existsSync(r))return;const t=w(e.name);for(const c of o.readdirSync(r))Pe(c,t)&&o.rmSync(s.join(r,c),{force:!0})}function Pe(e,r){for(const t of["__source-schema.sql","__local-replay-schema.sql","__schema.diff","__schema.meta.json"])if(e.endsWith(t))return e.slice(0,-t.length).endsWith(`__${r}`);return!1}async function be(e,r,t,c){const a=T(e);o.mkdirSync(a,{recursive:!0});const n=we(e),i=s.join(a,`${n}__source-schema.sql`),h=s.join(a,`${n}__local-replay-schema.sql`),u=s.join(a,`${n}__schema.diff`),m=s.join(a,`${n}__schema.meta.json`);return o.writeFileSync(i,t),o.writeFileSync(h,c),o.writeFileSync(u,await ge(i,h)),N(m,Ee(e,r)),{sourceCachePath:r.cachePath,sourcePath:i,localPath:h,diffPath:u,metaPath:m}}function Ee(e,r){const t=C(r.metadataPath);return{version:1,createdAt:new Date().toISOString(),serviceName:e.name,runtimeLabel:e.runtimeLabel||e.name,sourceSchema:{envName:r.envName||null,cacheKey:r.cacheKey||null,cacheKind:r.cacheKind||null,cachePath:s.relative(e.productDir,r.cachePath),refreshedAt:t},repo:_(r.repoState)}}function ge(e,r){return new Promise(t=>{t(buildUnifiedDiffText(o.readFileSync(e,"utf8"),o.readFileSync(r,"utf8"),e,r))})}export function buildUnifiedDiffText(e,r,t="source",c="target"){if(e===r)return"";const{createTwoFilesPatch:a}=oe();return a(t,c,y(e),y(r),"","",{context:3})}function Ae(e,r){return e==="fail"||e==="warn-cache"?e:r.CI==="true"?"fail":"warn-cache"}function j(e){return`TESTKIT_${e.toUpperCase().replace(/[^A-Z0-9]+/g,"_")}_SCHEMA_SOURCE_DATABASE_URL`}function xe(e,r){const t={...e.testkit.serviceEnv,...e.testkit.local?.env};for(const c of r){const a=t[c];if(typeof a!="string")continue;const n=X(a).find(i=>(i.serviceName??e.name)===e.name);if(n)throw new Error(`Service "${e.name}" env ${c} uses ${n.placeholder}, which is the database the source schema verifies. Point it at another service's database or an external database URL.`)}}function w(e){return e.trim().toLowerCase().replace(/[^a-z0-9._-]+/g,"-").replace(/^-+|-+$/g,"")||"schema"}function O(e){const r=A(e);return!f(r)||!Array.isArray(r.entries)?[]:r.entries.filter(ke)}function C(e){const r=A(e);return f(r)&&typeof r.refreshedAt=="string"?r.refreshedAt:null}function Ce(e){return`${y(JSON.stringify(e,null,2))}
3
+ `}function Re(e){const r=H(e,"message");return!!r?r:y(e)}function _e(e){const r=H(e,"message");return!!r?r:e}function H(e,r){if(!(e===null||typeof e!="object"&&typeof e!="function"))return Reflect.get(e,r)}function B(e){return!!e.sourceUrl}function ke(e){return!f(e)||typeof e.cacheKey!="string"||e.cacheKey.length===0?!1:d(e.branch)&&d(e.cachePath)&&d(e.commitSha)&&d(e.contentFingerprint)&&W(e.detached)&&W(e.dirty)&&d(e.dirtyFingerprint)&&d(e.kind)&&d(e.lastUsedAt)&&d(e.refreshedAt)&&d(e.remoteUrl)&&Ke(e.repository)}function Ke(e){return e==null||f(e)&&typeof e.host=="string"&&typeof e.owner=="string"&&typeof e.name=="string"}function Ue(e){return f(e)&&typeof e.refreshedAt=="string"&&(e.sourceUrl===void 0||$e(e.sourceUrl))}function $e(e){return f(e)&&typeof e.originalHost=="string"&&q(e.originalClassification)&&typeof e.resolvedHost=="string"&&q(e.resolvedClassification)&&typeof e.rewritten=="boolean"}function q(e){return e==="neon-pooler"||e==="neon-direct"||e==="unsupported-pooler"||e==="unknown-direct"}function d(e){return e===void 0||ie(e)}function W(e){return e===void 0||typeof e=="boolean"}
@@ -18,5 +18,4 @@ export interface SourceSchemaDumpResolution {
18
18
  }
19
19
  export declare function resolveSourceSchemaDumpUrl(databaseUrl: string): SourceSchemaDumpResolution;
20
20
  export declare function classifyDatabaseHost(hostname: string): DatabaseHostClassification;
21
- export declare function redactDatabaseUrl(databaseUrl: string): string;
22
21
  export declare function assertPgDumpArgsAreSourceSafe(args: readonly string[]): void;
@@ -1,2 +1,2 @@
1
- import{errorMessage as c}from"../kernel/error-serialization.js";const s=".neon.tech",a="-pooler.",l=/(^|[-.])(pgbouncer|pooler)([-.]|$)/i;export function resolveSourceSchemaDumpUrl(o){const e=d(o),r=classifyDatabaseHost(e.hostname);if(r.kind==="neon-pooler"){const n=new URL(e.url.href);return n.hostname=e.hostname.replace(a,"."),i({original:e,resolved:n,originalClassification:r,resolvedClassification:classifyDatabaseHost(n.hostname),rewritten:!0,notice:"Source schema URL uses Neon pooler; Testkit is using the direct endpoint for pg_dump."})}if(r.kind==="unsupported-pooler")throw new Error(["Refusing to run pg_dump through a pooled source database URL.",`Host: ${e.hostname}`,"Configure a direct source database URL."].join(`
2
- `));return i({original:e,resolved:e.url,originalClassification:r,resolvedClassification:r,rewritten:!1,notice:null})}export function classifyDatabaseHost(o){const e=o.toLowerCase();return e.endsWith(s)&&e.includes(a)?{kind:"neon-pooler",host:e}:e.endsWith(s)?{kind:"neon-direct",host:e}:l.test(e)?{kind:"unsupported-pooler",host:e}:{kind:"unknown-direct",host:e}}export function redactDatabaseUrl(o){try{const e=new URL(o);return e.username&&(e.username="REDACTED"),e.password&&(e.password="REDACTED"),e.toString()}catch{return"[invalid database URL]"}}export function assertPgDumpArgsAreSourceSafe(o){for(let e=0;e<o.length;e+=1){const r=o[e];if(r==="-j"||r==="--jobs"||r.startsWith("--jobs="))throw new Error("Source schema pg_dump must not use parallel jobs.")}}function d(o){try{const e=new URL(o);if(!e.hostname)throw new Error("missing host");return{url:e,hostname:e.hostname.toLowerCase()}}catch(e){throw new Error(`Invalid source database URL: ${c(e)}`,{cause:e})}}function i({original:o,resolved:e,originalClassification:r,resolvedClassification:n,rewritten:t,notice:u}){return{originalUrl:o.url.href,dumpUrl:e.href,rewritten:t,notice:u,metadata:{originalHost:o.hostname,originalClassification:r.kind,resolvedHost:e.hostname.toLowerCase(),resolvedClassification:n.kind,rewritten:t}}}
1
+ import{errorMessage as l}from"../kernel/error-serialization.js";const s=".neon.tech",i="-pooler.",c=/(^|[-.])(pgbouncer|pooler)([-.]|$)/i;export function resolveSourceSchemaDumpUrl(o){const e=d(o),r=classifyDatabaseHost(e.hostname);if(r.kind==="neon-pooler"){const n=new URL(e.url.href);return n.hostname=e.hostname.replace(i,"."),a({original:e,resolved:n,originalClassification:r,resolvedClassification:classifyDatabaseHost(n.hostname),rewritten:!0,notice:"Source schema URL uses Neon pooler; Testkit is using the direct endpoint for pg_dump."})}if(r.kind==="unsupported-pooler")throw new Error(["Refusing to run pg_dump through a pooled source database URL.",`Host: ${e.hostname}`,"Configure a direct source database URL."].join(`
2
+ `));return a({original:e,resolved:e.url,originalClassification:r,resolvedClassification:r,rewritten:!1,notice:null})}export function classifyDatabaseHost(o){const e=o.toLowerCase();return e.endsWith(s)&&e.includes(i)?{kind:"neon-pooler",host:e}:e.endsWith(s)?{kind:"neon-direct",host:e}:c.test(e)?{kind:"unsupported-pooler",host:e}:{kind:"unknown-direct",host:e}}export function assertPgDumpArgsAreSourceSafe(o){for(let e=0;e<o.length;e+=1){const r=o[e];if(r==="-j"||r==="--jobs"||r.startsWith("--jobs="))throw new Error("Source schema pg_dump must not use parallel jobs.")}}function d(o){try{const e=new URL(o);if(!e.hostname)throw new Error("missing host");return{url:e,hostname:e.hostname.toLowerCase()}}catch(e){throw new Error(`Invalid source database URL: ${l(e)}`,{cause:e})}}function a({original:o,resolved:e,originalClassification:r,resolvedClassification:n,rewritten:t,notice:u}){return{originalUrl:o.url.href,dumpUrl:e.href,rewritten:t,notice:u,metadata:{originalHost:o.hostname,originalClassification:r.kind,resolvedHost:e.hostname.toLowerCase(),resolvedClassification:n.kind,rewritten:t}}}
@@ -1 +1 @@
1
- import u from"fs";import o from"path";import{requireContainedTestkitStateDirectory as D,resolveContainedTestkitStateDirectory as b}from"../process/runs/product-state.js";const y=new Set(["_bundles","_config","_dbcache","_graphs","_infra","_locks","_runs","_template-steps","_toolchains","_typecheck","engines","history","lint","results"]);export function getLocalInfraDir(e){return resolveContainedDatabaseStateDirectory(e,"_infra","local-postgres")}export function getLocalLocksDir(e){return resolveContainedDatabaseStateDirectory(e,"_locks","local-postgres")}export function getLocalServiceCacheDir(e,t){return resolveContainedDatabaseStateDirectory(e,"_dbcache",t)}export function listOwnedDatabaseStateDirs(e){const t=resolveContainedDatabaseStateDirectory(e),r=[],n=getLocalInfraDir(e);u.existsSync(n)&&r.push({dir:n,kind:"infra"});const l=o.join(t,"_dbcache");for(const a of f(l))r.push({dir:a,kind:"cache"});const s=o.join(t,"_graphs");for(const a of f(s))for(const c of f(o.join(a,"runtimes")))for(const m of f(o.join(c,"services")))r.push({dir:m,kind:"runtime"});for(const a of p(t)){if(y.has(a.name)||!a.isDirectory())continue;const c=o.join(t,a.name);d(o.join(c,"database_backend"))&&r.push({dir:c,kind:"legacy"})}return r}export function hasRemainingLocalArtifacts(e){return listOwnedDatabaseStateDirs(e).map(({dir:t,kind:r})=>{const n=d(o.join(t,"database_backend"));return r!=="infra"&&(r==="cache"||n)}).some(Boolean)}export function readOwnedStateValues(e,t){const r=new Set;for(const{dir:n}of listOwnedDatabaseStateDirs(e))for(const l of t){const s=readStateValue(o.join(n,l));s&&r.add(s)}return r}function f(e){return p(e).flatMap(t=>{const r=o.join(e,t.name);if(t.isSymbolicLink())throw i(r,"symlink");if(!t.isDirectory()&&!t.isFile())throw i(r,"special node");return t.isDirectory()?[r]:[]})}function p(e){const t=h(e);if(!t)return[];if(!t.isDirectory())throw i(e,"non-directory node");return u.readdirSync(e,{withFileTypes:!0}).sort((r,n)=>r.name.localeCompare(n.name))}function d(e){const t=h(e);if(t&&!t.isFile())throw i(e,"non-file node");return!!t}function h(e){const t=u.lstatSync(e,{throwIfNoEntry:!1});if(t?.isSymbolicLink())throw i(e,"symlink");return t}export function readStateValue(e){return d(e)?u.readFileSync(e,"utf8").trim():null}export function readDatabaseUrl(e){return readStateValue(o.join(e,"database_url"))}export function readDatabaseInfo(e){return S(readDatabaseUrl(e))}function S(e){if(!e)return null;try{const t=new URL(e),r=Number(t.port||"5432"),n=t.searchParams.get("sslmode")||"disable";return{url:e,host:t.hostname,port:Number.isInteger(r)&&r>0?r:5432,database:t.pathname.replace(/^\//,""),user:decodeURIComponent(t.username||""),password:decodeURIComponent(t.password||""),sslMode:n,ssl:n!=="disable"}}catch{return null}}export function resolveContainedDatabaseStateDirectory(e,...t){return b(e,...t)}export function requireContainedDatabaseStateDirectory(e,t,r="Testkit database state directory"){return D(e,t,r)}function i(e,t){return new Error(`Refusing unsafe Testkit database state ${t} at ${e}`)}
1
+ import u from"fs";import o from"path";import{requireContainedTestkitStateDirectory as D,resolveContainedTestkitStateDirectory as b}from"../process/runs/product-state.js";const y=new Set(["_bundles","_config","_dbcache","_execution","_graphs","_infra","_locks","_runs","_template-steps","_toolchains","_typecheck","engines","history","lint","results"]);export function getLocalInfraDir(e){return resolveContainedDatabaseStateDirectory(e,"_infra","local-postgres")}export function getLocalLocksDir(e){return resolveContainedDatabaseStateDirectory(e,"_locks","local-postgres")}export function getLocalServiceCacheDir(e,t){return resolveContainedDatabaseStateDirectory(e,"_dbcache",t)}export function listOwnedDatabaseStateDirs(e){const t=resolveContainedDatabaseStateDirectory(e),r=[],n=getLocalInfraDir(e);u.existsSync(n)&&r.push({dir:n,kind:"infra"});const l=o.join(t,"_dbcache");for(const a of f(l))r.push({dir:a,kind:"cache"});const s=o.join(t,"_graphs");for(const a of f(s))for(const c of f(o.join(a,"runtimes")))for(const m of f(o.join(c,"services")))r.push({dir:m,kind:"runtime"});for(const a of p(t)){if(y.has(a.name)||!a.isDirectory())continue;const c=o.join(t,a.name);d(o.join(c,"database_backend"))&&r.push({dir:c,kind:"legacy"})}return r}export function hasRemainingLocalArtifacts(e){return listOwnedDatabaseStateDirs(e).map(({dir:t,kind:r})=>{const n=d(o.join(t,"database_backend"));return r!=="infra"&&(r==="cache"||n)}).some(Boolean)}export function readOwnedStateValues(e,t){const r=new Set;for(const{dir:n}of listOwnedDatabaseStateDirs(e))for(const l of t){const s=readStateValue(o.join(n,l));s&&r.add(s)}return r}function f(e){return p(e).flatMap(t=>{const r=o.join(e,t.name);if(t.isSymbolicLink())throw i(r,"symlink");if(!t.isDirectory()&&!t.isFile())throw i(r,"special node");return t.isDirectory()?[r]:[]})}function p(e){const t=h(e);if(!t)return[];if(!t.isDirectory())throw i(e,"non-directory node");return u.readdirSync(e,{withFileTypes:!0}).sort((r,n)=>r.name.localeCompare(n.name))}function d(e){const t=h(e);if(t&&!t.isFile())throw i(e,"non-file node");return!!t}function h(e){const t=u.lstatSync(e,{throwIfNoEntry:!1});if(t?.isSymbolicLink())throw i(e,"symlink");return t}export function readStateValue(e){return d(e)?u.readFileSync(e,"utf8").trim():null}export function readDatabaseUrl(e){return readStateValue(o.join(e,"database_url"))}export function readDatabaseInfo(e){return S(readDatabaseUrl(e))}function S(e){if(!e)return null;try{const t=new URL(e),r=Number(t.port||"5432"),n=t.searchParams.get("sslmode")||"disable";return{url:e,host:t.hostname,port:Number.isInteger(r)&&r>0?r:5432,database:t.pathname.replace(/^\//,""),user:decodeURIComponent(t.username||""),password:decodeURIComponent(t.password||""),sslMode:n,ssl:n!=="disable"}}catch{return null}}export function resolveContainedDatabaseStateDirectory(e,...t){return b(e,...t)}export function requireContainedDatabaseStateDirectory(e,t,r="Testkit database state directory"){return D(e,t,r)}function i(e,t){return new Error(`Refusing unsafe Testkit database state ${t} at ${e}`)}
@@ -1,4 +1,4 @@
1
- import j from"node:fs";import U from"node:path";import{runDockerPsql as P}from"./postgres/index.js";const d=String;export async function verifySeed(r={}){const e=f(r),t=l(e.table,"verifySeed.args.table"),n=_(e.where),s=z(e.minRows??1,"verifySeed.args.minRows"),o=g(r),u=`select count(*)::int from ${t}${n?` where ${n}`:""}`,i=Number(await q(o,u,r));if(!Number.isInteger(i)||i<s)throw new Error(`Expected at least ${String(s)} row(s) in ${t}, found ${String(i||0)}`)}export async function verifyRows(r={}){const e=f(r),t=Array.isArray(e.checks)?e.checks:[e],n=g(r),s=[];for(const[o,u]of t.entries()){const i=`verifyRows.args.checks[${String(o)}]`,a=S(u)?u:{},m=l(a.table,`${i}.table`),w=_(a.where),c=z(a.minRows??1,`${i}.minRows`),$=a.severity==="warn"?"warn":"error",k=`select count(*)::int from ${m}${w?` where ${w}`:""}`,y=Number(await q(n,k,r));if(!Number.isInteger(y)||y<c){const N=`Expected at least ${String(c)} row(s) in ${m}, found ${String(y||0)}`;$==="warn"?J(r,{code:"database_verify_rows_min_rows",message:N,table:m,where:w||null,expectedMinRows:c,actualRows:Number.isInteger(y)?y:0}):s.push(N)}}if(s.length>0)throw new Error(`Database row verification failed:
1
+ import O from"node:fs";import U from"node:path";import{runDockerPsql as P}from"./postgres/index.js";import{isUnknownRecord as h}from"../kernel/unknown-values.js";const d=String;export async function verifySeed(r={}){const e=f(r),t=l(e.table,"verifySeed.args.table"),n=v(e.where),s=z(e.minRows??1,"verifySeed.args.minRows"),o=g(r),u=`select count(*)::int from ${t}${n?` where ${n}`:""}`,i=Number(await q(o,u,r));if(!Number.isInteger(i)||i<s)throw new Error(`Expected at least ${String(s)} row(s) in ${t}, found ${String(i||0)}`)}export async function verifyRows(r={}){const e=f(r),t=Array.isArray(e.checks)?e.checks:[e],n=g(r),s=[];for(const[o,u]of t.entries()){const i=`verifyRows.args.checks[${String(o)}]`,a=h(u)?u:{},m=l(a.table,`${i}.table`),w=v(a.where),c=z(a.minRows??1,`${i}.minRows`),$=a.severity==="warn"?"warn":"error",A=`select count(*)::int from ${m}${w?` where ${w}`:""}`,y=Number(await q(n,A,r));if(!Number.isInteger(y)||y<c){const N=`Expected at least ${String(c)} row(s) in ${m}, found ${String(y||0)}`;$==="warn"?J(r,{code:"database_verify_rows_min_rows",message:N,table:m,where:w||null,expectedMinRows:c,actualRows:Number.isInteger(y)?y:0}):s.push(N)}}if(s.length>0)throw new Error(`Database row verification failed:
2
2
  ${s.map(o=>`- ${o}`).join(`
3
- `)}`)}export async function queryScalar(r={}){const e=f(r),t=g(r),n=O(e.statement??e.query,"queryScalar.args.statement");return q(t,n,r)}export async function runSql(r={}){const e=f(r),t=L(e.statements??e.statement,"runSql.args.statements"),n=g(r);for(const s of t){const o=await b(n,s,r);o.stdout&&process.stdout.write(o.stdout),o.stderr&&process.stderr.write(o.stderr)}}export async function seedRows(r={}){const e=f(r),t=l(e.table,"seedRows.args.table"),n=D(e.rows,"seedRows.args.rows"),s=g(r);if(e.truncate===!0&&await b(s,`truncate table ${t}`,r),n.length===0)return;const o=I(e.columns,n),u=o.map(a=>l(a,`seedRows.args.columns.${a}`)),i=n.map(a=>`(${o.map(m=>E(a[m],r)).join(", ")})`);await b(s,`insert into ${t} (${u.join(", ")}) values ${i.join(", ")}`,r)}export async function materializePostgresBinding(r={}){const e=f(r),t=l(e.table,"materializePostgresBinding.args.table"),n=l(e.keyColumn||"slug","materializePostgresBinding.args.keyColumn"),s=T(e.key,"materializePostgresBinding.args.key"),o=R(e.values,"materializePostgresBinding.args.values"),u=g(r),i=Object.keys(o);if(i.length===0)throw new Error("materializePostgresBinding.args.values must contain at least one column");const a=i.map($=>`${l($,`materializePostgresBinding.args.values.${$}`)} = ${E(o[$],r)}`),m=`with updated as (update ${t} set ${a.join(", ")} where ${n} = ${h(s)} returning 1) select count(*)::int from updated`,w=await b(u,m,r,{scalar:!0}),c=Number((w.stdout||"").trim()||"0");if(c!==1)throw new Error(`Expected to materialize 1 ${t} row for ${n}=${String(s)}, updated ${String(c||0)}`)}export async function updateRows(r={}){const e=f(r),t=l(e.table,"updateRows.args.table"),n=B(e.where,"updateRows.args.where"),s=R(e.values??e.set,"updateRows.args.values"),o=g(r),u=e.expectRows==null?null:z(e.expectRows,"updateRows.args.expectRows"),i=Object.keys(s).map(c=>`${l(c,`updateRows.args.values.${c}`)} = ${E(s[c],r)}`);if(i.length===0)throw new Error("updateRows.args.values must contain at least one column");const a=`with updated as (update ${t} set ${i.join(", ")} where ${W(n,r)} returning 1) select count(*)::int from updated`,m=await b(o,a,r,{scalar:!0}),w=Number((m.stdout||"").trim()||"0");if(u!==null&&w!==u)throw new Error(`Expected to update ${String(u)} ${t} row(s), updated ${String(w||0)}`)}function f(r){return S(r.args)?r.args:{}}function g(r){const e=(r.databaseUrl||r.env?.DATABASE_URL||"").trim();if(!e)throw new Error("Database template step requires DATABASE_URL");return e}async function q(r,e,t){return((await b(r,e,t,{scalar:!0})).stdout||"").trim()}async function b(r,e,t,n={}){const s={args:[r,"-v","ON_ERROR_STOP=1","-X",...n.scalar?["-q","-t","-A"]:[],"-c",e],cwd:t.cwd||t.productDir||process.cwd(),env:t.env||process.env,databaseUrl:r,ownedContainer:t.ownedContainer||null},o=await P(s);if(o.exitCode!==0)throw new Error(o.stderr||`psql failed with exit code ${String(o.exitCode)}`);return o}function L(r,e){const t=Array.isArray(r)?r:r==null?[]:[r];if(t.length===0)throw new Error(`${e} must contain at least one SQL statement`);return t.map((n,s)=>O(n,`${e}[${String(s)}]`))}function O(r,e){const t=d(r||"").trim();if(!t||/;\s*\S/.test(t))throw new Error(`${e} must be one SQL statement`);return t}function D(r,e){if(!Array.isArray(r))throw new Error(`${e} must be an array`);return r.map((n,s)=>R(n,`${e}[${String(s)}]`))}function I(r,e){if(r!=null){if(!Array.isArray(r)||r.length===0)throw new Error("seedRows.args.columns must be a non-empty array");return r.map(n=>String(n))}return[...new Set(e.flatMap(t=>Object.keys(t)))].sort((t,n)=>t.localeCompare(n))}function B(r,e){const t=R(r,e);if(Object.keys(t).length===0)throw new Error(`${e} must contain at least one column`);return t}function W(r,e){return Object.entries(r).map(([t,n])=>`${l(t,`where.${t}`)} = ${E(n,e)}`).join(" and ")}function l(r,e){const t=d(r||"").trim();if(!/^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)?$/.test(t))throw new Error(`${e} must be a SQL identifier or schema-qualified identifier`);return t}function _(r){if(r==null||r==="")return"";const e=d(r).trim();if(!e||/;|--|\/\*/.test(e))throw new Error("verifySeed.args.where must be a single SQL predicate without comments or semicolons");return e}function z(r,e){const t=Number(r);if(!Number.isInteger(t)||t<0)throw new Error(`${e} must be a non-negative integer`);return t}function R(r,e){if(!S(r))throw new Error(`${e} must be an object`);return r}function T(r,e){if(r==null||d(r).length===0)throw new Error(`${e} is required`);return r}function h(r){return r===null?"null":typeof r=="number"&&Number.isFinite(r)?String(r):typeof r=="boolean"?r?"true":"false":`'${d(r).replaceAll("'","''")}'`}function E(r,e){if(!v(r))return h(r);switch(r.kind){case"now":return"now()";case"env":return h(p(e.env||process.env,r.name));case"json":return`${h(JSON.stringify(A(r.value,e)))}::jsonb`;case"postgres-connection-from-env":return`${h(JSON.stringify(C(e.env||process.env,r.prefix)))}::jsonb`;default:throw new Error(`Unsupported database value descriptor "${r.kind}"`)}}function v(r){return S(r)&&typeof r.kind=="string"}function A(r,e){if(Array.isArray(r))return r.map(n=>A(n,e));if(!S(r))return r;if(v(r)){if(r.kind==="env")return p(e.env||process.env,r.name);if(r.kind==="postgres-connection-from-env")return C(e.env||process.env,r.prefix)}return Object.fromEntries(Object.entries(r).map(([t,n])=>[t,A(n,e)]))}function C(r,e){const t=d(e||"").trim();if(!t)throw new Error("postgresConnectionFromEnv requires a non-empty prefix");return{host:p(r,`${t}_HOST`),port:F(r,`${t}_PORT`),database:p(r,`${t}_NAME`),username:p(r,`${t}_USER`),password:p(r,`${t}_PASSWORD`),ssl:Q(r[`${t}_SSL`])}}function p(r,e){const t=String(e),n=r?.[t];if(n==null||n.trim().length===0)throw new Error(`${t} environment variable is required`);return n.trim()}function F(r,e){const t=Number.parseInt(p(r,e),10);if(!Number.isInteger(t)||t<=0)throw new Error(`${e} must be a positive integer`);return t}function Q(r){return r?["1","true","yes","on"].includes(r.trim().toLowerCase()):!1}function J(r,e){const t={severity:"warn",source:"database.verifyRows",serviceName:r.serviceName||null,runtimeId:r.runtimeId||null,timestamp:new Date().toISOString(),...e};typeof r.recordWarning=="function"&&r.recordWarning(t);const n=r.warningFile;n&&(j.mkdirSync(U.dirname(n),{recursive:!0}),j.appendFileSync(n,JSON.stringify(t)+`
4
- `)),Z(r)&&console.warn(t.message)}function Z(r){const e=r.emitWarningsToStderr;return e==null?!0:!["0","false","no","off"].includes(d(e).trim().toLowerCase())}function S(r){return typeof r=="object"&&r!==null&&!Array.isArray(r)}
3
+ `)}`)}export async function queryScalar(r={}){const e=f(r),t=g(r),n=j(e.statement??e.query,"queryScalar.args.statement");return q(t,n,r)}export async function runSql(r={}){const e=f(r),t=L(e.statements??e.statement,"runSql.args.statements"),n=g(r);for(const s of t){const o=await b(n,s,r);o.stdout&&process.stdout.write(o.stdout),o.stderr&&process.stderr.write(o.stderr)}}export async function seedRows(r={}){const e=f(r),t=l(e.table,"seedRows.args.table"),n=D(e.rows,"seedRows.args.rows"),s=g(r);if(e.truncate===!0&&await b(s,`truncate table ${t}`,r),n.length===0)return;const o=I(e.columns,n),u=o.map(a=>l(a,`seedRows.args.columns.${a}`)),i=n.map(a=>`(${o.map(m=>E(a[m],r)).join(", ")})`);await b(s,`insert into ${t} (${u.join(", ")}) values ${i.join(", ")}`,r)}export async function materializePostgresBinding(r={}){const e=f(r),t=l(e.table,"materializePostgresBinding.args.table"),n=l(e.keyColumn||"slug","materializePostgresBinding.args.keyColumn"),s=T(e.key,"materializePostgresBinding.args.key"),o=R(e.values,"materializePostgresBinding.args.values"),u=g(r),i=Object.keys(o);if(i.length===0)throw new Error("materializePostgresBinding.args.values must contain at least one column");const a=i.map($=>`${l($,`materializePostgresBinding.args.values.${$}`)} = ${E(o[$],r)}`),m=`with updated as (update ${t} set ${a.join(", ")} where ${n} = ${S(s)} returning 1) select count(*)::int from updated`,w=await b(u,m,r,{scalar:!0}),c=Number((w.stdout||"").trim()||"0");if(c!==1)throw new Error(`Expected to materialize 1 ${t} row for ${n}=${String(s)}, updated ${String(c||0)}`)}export async function updateRows(r={}){const e=f(r),t=l(e.table,"updateRows.args.table"),n=B(e.where,"updateRows.args.where"),s=R(e.values??e.set,"updateRows.args.values"),o=g(r),u=e.expectRows==null?null:z(e.expectRows,"updateRows.args.expectRows"),i=Object.keys(s).map(c=>`${l(c,`updateRows.args.values.${c}`)} = ${E(s[c],r)}`);if(i.length===0)throw new Error("updateRows.args.values must contain at least one column");const a=`with updated as (update ${t} set ${i.join(", ")} where ${W(n,r)} returning 1) select count(*)::int from updated`,m=await b(o,a,r,{scalar:!0}),w=Number((m.stdout||"").trim()||"0");if(u!==null&&w!==u)throw new Error(`Expected to update ${String(u)} ${t} row(s), updated ${String(w||0)}`)}function f(r){return h(r.args)?r.args:{}}function g(r){const e=(r.databaseUrl||r.env?.DATABASE_URL||"").trim();if(!e)throw new Error("Database template step requires DATABASE_URL");return e}async function q(r,e,t){return((await b(r,e,t,{scalar:!0})).stdout||"").trim()}async function b(r,e,t,n={}){const s={args:[r,"-v","ON_ERROR_STOP=1","-X",...n.scalar?["-q","-t","-A"]:[],"-c",e],cwd:t.cwd||t.productDir||process.cwd(),env:t.env||process.env,databaseUrl:r,ownedContainer:t.ownedContainer||null},o=await P(s);if(o.exitCode!==0)throw new Error(o.stderr||`psql failed with exit code ${String(o.exitCode)}`);return o}function L(r,e){const t=Array.isArray(r)?r:r==null?[]:[r];if(t.length===0)throw new Error(`${e} must contain at least one SQL statement`);return t.map((n,s)=>j(n,`${e}[${String(s)}]`))}function j(r,e){const t=d(r||"").trim();if(!t||/;\s*\S/.test(t))throw new Error(`${e} must be one SQL statement`);return t}function D(r,e){if(!Array.isArray(r))throw new Error(`${e} must be an array`);return r.map((n,s)=>R(n,`${e}[${String(s)}]`))}function I(r,e){if(r!=null){if(!Array.isArray(r)||r.length===0)throw new Error("seedRows.args.columns must be a non-empty array");return r.map(n=>String(n))}return[...new Set(e.flatMap(t=>Object.keys(t)))].sort((t,n)=>t.localeCompare(n))}function B(r,e){const t=R(r,e);if(Object.keys(t).length===0)throw new Error(`${e} must contain at least one column`);return t}function W(r,e){return Object.entries(r).map(([t,n])=>`${l(t,`where.${t}`)} = ${E(n,e)}`).join(" and ")}function l(r,e){const t=d(r||"").trim();if(!/^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)?$/.test(t))throw new Error(`${e} must be a SQL identifier or schema-qualified identifier`);return t}function v(r){if(r==null||r==="")return"";const e=d(r).trim();if(!e||/;|--|\/\*/.test(e))throw new Error("verifySeed.args.where must be a single SQL predicate without comments or semicolons");return e}function z(r,e){const t=Number(r);if(!Number.isInteger(t)||t<0)throw new Error(`${e} must be a non-negative integer`);return t}function R(r,e){if(!h(r))throw new Error(`${e} must be an object`);return r}function T(r,e){if(r==null||d(r).length===0)throw new Error(`${e} is required`);return r}function S(r){return r===null?"null":typeof r=="number"&&Number.isFinite(r)?String(r):typeof r=="boolean"?r?"true":"false":`'${d(r).replaceAll("'","''")}'`}function E(r,e){if(!_(r))return S(r);switch(r.kind){case"now":return"now()";case"env":return S(p(e.env||process.env,r.name));case"json":return`${S(JSON.stringify(k(r.value,e)))}::jsonb`;case"postgres-connection-from-env":return`${S(JSON.stringify(C(e.env||process.env,r.prefix)))}::jsonb`;default:throw new Error(`Unsupported database value descriptor "${r.kind}"`)}}function _(r){return h(r)&&typeof r.kind=="string"}function k(r,e){if(Array.isArray(r))return r.map(n=>k(n,e));if(!h(r))return r;if(_(r)){if(r.kind==="env")return p(e.env||process.env,r.name);if(r.kind==="postgres-connection-from-env")return C(e.env||process.env,r.prefix)}return Object.fromEntries(Object.entries(r).map(([t,n])=>[t,k(n,e)]))}function C(r,e){const t=d(e||"").trim();if(!t)throw new Error("postgresConnectionFromEnv requires a non-empty prefix");return{host:p(r,`${t}_HOST`),port:F(r,`${t}_PORT`),database:p(r,`${t}_NAME`),username:p(r,`${t}_USER`),password:p(r,`${t}_PASSWORD`),ssl:Q(r[`${t}_SSL`])}}function p(r,e){const t=String(e),n=r?.[t];if(n==null||n.trim().length===0)throw new Error(`${t} environment variable is required`);return n.trim()}function F(r,e){const t=Number.parseInt(p(r,e),10);if(!Number.isInteger(t)||t<=0)throw new Error(`${e} must be a positive integer`);return t}function Q(r){return r?["1","true","yes","on"].includes(r.trim().toLowerCase()):!1}function J(r,e){const t={severity:"warn",source:"database.verifyRows",serviceName:r.serviceName||null,runtimeId:r.runtimeId||null,timestamp:new Date().toISOString(),...e};typeof r.recordWarning=="function"&&r.recordWarning(t);const n=r.warningFile;n&&(O.mkdirSync(U.dirname(n),{recursive:!0}),O.appendFileSync(n,JSON.stringify(t)+`
4
+ `)),Z(r)&&console.warn(t.message)}function Z(r){const e=r.emitWarningsToStderr;return e==null?!0:!["0","false","no","off"].includes(d(e).trim().toLowerCase())}
@@ -17,12 +17,9 @@ export interface FileTimeoutEnv {
17
17
  export declare function parseFileTimeoutOption(value: unknown): number;
18
18
  export declare function normalizeFileTimeoutSeconds(value: unknown, label?: string): number;
19
19
  export declare function normalizeWaitIntervalSeconds(value?: unknown): number;
20
- export declare function buildFileTimeoutEnv(fileTimeoutSeconds: number, startedAtMs: number): FileTimeoutEnv;
21
20
  export declare function readFileTimeoutBudget(env: unknown): FileTimeoutBudget;
22
21
  export declare function remainingFileTimeoutMs(budget: FileTimeoutBudget, nowMs?: number): number;
23
22
  export declare function remainingFileTimeoutSeconds(budget: FileTimeoutBudget, nowMs?: number): number;
24
23
  export declare function remainingCooperativeFileTimeoutMs(budget: FileTimeoutBudget, nowMs?: number): number;
25
24
  export declare function cooperativeFileTimeoutErrorReserveMs(fileTimeoutSeconds: number): number;
26
- export declare function subprocessFileTimeoutMs(fileTimeoutSeconds: number): number;
27
25
  export declare function formatWaitForTimeoutError(description: unknown, fileTimeoutSeconds: number): string;
28
- export declare function formatFileTimeoutBudgetError(fileTimeoutSeconds: number): string;
@@ -1 +1 @@
1
- export const DEFAULT_FILE_TIMEOUT_SECONDS=60,FILE_TIMEOUT_INTERVAL_SECONDS=.25,FILE_TIMEOUT_COOPERATIVE_ERROR_RESERVE_MAX_MS=2e3,FILE_TIMEOUT_COOPERATIVE_ERROR_RESERVE_MIN_MS=250,FILE_TIMEOUT_COOPERATIVE_ERROR_RESERVE_RATIO=.2,FILE_TIMEOUT_SUBPROCESS_EXIT_GRACE_MS=1e3,INTERNAL_FILE_TIMEOUT_SECONDS_ENV="TESTKIT_INTERNAL_FILE_TIMEOUT_SECONDS",INTERNAL_FILE_DEADLINE_MS_ENV="TESTKIT_INTERNAL_FILE_DEADLINE_MS";export function parseFileTimeoutOption(e){return i(e,"--file-timeout-seconds")}export function normalizeFileTimeoutSeconds(e,t="execution.fileTimeoutSeconds"){return o(e,t)}export function normalizeWaitIntervalSeconds(e){if(e==null)return .25;const t=Number(e);if(!Number.isFinite(t)||t<=0)throw new Error("waitFor intervalSeconds must be a positive number.");return t}export function buildFileTimeoutEnv(e,t){return{[INTERNAL_FILE_TIMEOUT_SECONDS_ENV]:String(e),[INTERNAL_FILE_DEADLINE_MS_ENV]:String(t+e*1e3)}}export function readFileTimeoutBudget(e){if(!E(e))throw new Error("Runtime file timeout budget is missing.");return{fileTimeoutSeconds:normalizeFileTimeoutSeconds(e[INTERNAL_FILE_TIMEOUT_SECONDS_ENV],INTERNAL_FILE_TIMEOUT_SECONDS_ENV),deadlineMs:n(e[INTERNAL_FILE_DEADLINE_MS_ENV],INTERNAL_FILE_DEADLINE_MS_ENV)}}export function remainingFileTimeoutMs(e,t=Date.now()){return Math.max(0,e.deadlineMs-t)}export function remainingFileTimeoutSeconds(e,t=Date.now()){return remainingFileTimeoutMs(e,t)/1e3}export function remainingCooperativeFileTimeoutMs(e,t=Date.now()){return Math.max(0,remainingFileTimeoutMs(e,t)-cooperativeFileTimeoutErrorReserveMs(e.fileTimeoutSeconds))}export function cooperativeFileTimeoutErrorReserveMs(e){return Math.min(2e3,Math.max(250,Math.floor(e*1e3*.2)))}export function subprocessFileTimeoutMs(e){return e*1e3+cooperativeFileTimeoutErrorReserveMs(e)+1e3}export function formatWaitForTimeoutError(e,t){return`Timed out waiting for ${String(e||"condition").trim()||"condition"} before the ${String(t)}s test file timeout`}export function formatFileTimeoutBudgetError(e){return`Test file exceeded the ${String(e)}s wall-clock timeout`}function i(e,t){const r=Number.parseInt(String(e),10);if(!Number.isInteger(r)||r<=0)throw new Error(`Invalid ${t} value "${String(e)}". Expected a positive integer.`);return r}function o(e,t){const r=Number(e);if(!Number.isInteger(r)||r<=0)throw new Error(`${t} must be a positive integer.`);return r}function n(e,t){const r=Number(e);if(!Number.isInteger(r)||r<=0)throw new Error(`${t} must be a positive integer timestamp.`);return r}function E(e){return e!==null&&typeof e=="object"}
1
+ export const DEFAULT_FILE_TIMEOUT_SECONDS=60,FILE_TIMEOUT_INTERVAL_SECONDS=.25,FILE_TIMEOUT_COOPERATIVE_ERROR_RESERVE_MAX_MS=2e3,FILE_TIMEOUT_COOPERATIVE_ERROR_RESERVE_MIN_MS=250,FILE_TIMEOUT_COOPERATIVE_ERROR_RESERVE_RATIO=.2,FILE_TIMEOUT_SUBPROCESS_EXIT_GRACE_MS=1e3,INTERNAL_FILE_TIMEOUT_SECONDS_ENV="TESTKIT_INTERNAL_FILE_TIMEOUT_SECONDS",INTERNAL_FILE_DEADLINE_MS_ENV="TESTKIT_INTERNAL_FILE_DEADLINE_MS";export function parseFileTimeoutOption(e){return i(e,"--file-timeout-seconds")}export function normalizeFileTimeoutSeconds(e,t="execution.fileTimeoutSeconds"){return o(e,t)}export function normalizeWaitIntervalSeconds(e){if(e==null)return .25;const t=Number(e);if(!Number.isFinite(t)||t<=0)throw new Error("waitFor intervalSeconds must be a positive number.");return t}export function readFileTimeoutBudget(e){if(!E(e))throw new Error("Runtime file timeout budget is missing.");return{fileTimeoutSeconds:normalizeFileTimeoutSeconds(e[INTERNAL_FILE_TIMEOUT_SECONDS_ENV],INTERNAL_FILE_TIMEOUT_SECONDS_ENV),deadlineMs:n(e[INTERNAL_FILE_DEADLINE_MS_ENV],INTERNAL_FILE_DEADLINE_MS_ENV)}}export function remainingFileTimeoutMs(e,t=Date.now()){return Math.max(0,e.deadlineMs-t)}export function remainingFileTimeoutSeconds(e,t=Date.now()){return remainingFileTimeoutMs(e,t)/1e3}export function remainingCooperativeFileTimeoutMs(e,t=Date.now()){return Math.max(0,remainingFileTimeoutMs(e,t)-cooperativeFileTimeoutErrorReserveMs(e.fileTimeoutSeconds))}export function cooperativeFileTimeoutErrorReserveMs(e){return Math.min(2e3,Math.max(250,Math.floor(e*1e3*.2)))}export function formatWaitForTimeoutError(e,t){return`Timed out waiting for ${String(e||"condition").trim()||"condition"} before the ${String(t)}s test file timeout`}function i(e,t){const r=Number.parseInt(String(e),10);if(!Number.isInteger(r)||r<=0)throw new Error(`Invalid ${t} value "${String(e)}". Expected a positive integer.`);return r}function o(e,t){const r=Number(e);if(!Number.isInteger(r)||r<=0)throw new Error(`${t} must be a positive integer.`);return r}function n(e,t){const r=Number(e);if(!Number.isInteger(r)||r<=0)throw new Error(`${t} must be a positive integer timestamp.`);return r}function E(e){return e!==null&&typeof e=="object"}
@@ -1 +1 @@
1
- import{redactSensitiveText as i}from"../kernel/redaction.js";const g=new Set(["passed","succeeded","success","ready","cached"]),h=new Set(["running","pending","queued","starting","applying","cancelling"]),y=new Set(["skipped","skip","noop","no-op","not_applicable","not-applicable","cancelled","canceled","interrupted"]),A=new Set(["failed","error"]);export function normalizeOperationStatus(n){return S(n||"unknown").trim().toLowerCase().replaceAll(" ","_")}export function operationStatusKind(n){const e=normalizeOperationStatus(n);return g.has(e)?"passed":h.has(e)?"running":y.has(e)?"skipped":A.has(e)?"failed":"unknown"}export function operationStatusLabel(n,{runningLabel:e="RUN"}={}){const t=operationStatusKind(n);return t==="passed"?"PASS":t==="running"?e:t==="skipped"?"SKIP":"FAIL"}export function isNeutralOperationStatus(n){return operationStatusKind(n)==="skipped"}export function isSuccessfulOperationStatus(n){return operationStatusKind(n)==="passed"}export function formatOperationSummary(n={}){const e=l(n),t=l(e.metadata),r=[];e.message?r.push(i(e.message)):e.summary?r.push(i(e.summary)):e.error&&r.push(i(e.error));const a=o(t.phase||e.phase),c=o(t.action||e.action),s=o(t.resource||e.resource),u=o(t.decision),p=o(t.reason),f=k(e.logRefs);if(!r.length&&(a||c||s)){const m=[a,c].filter(Boolean).join(".");r.push(m+(s?` ${s}`:""))}return u&&r.push(`decision=${i(u)}`),p&&r.push(`reason=${i(p)}`),f&&r.push(`log=${f}`),r.filter(Boolean).join(" \xB7 ")}function k(n=[]){if(!Array.isArray(n))return"";const e=n.find(d);if(!e)return"";const t=o(e.service||e.serviceName),r=o(e.category||e.kind),a=o(e.step||e.name),c=o(e.state),s=[t,[r,a].filter(Boolean).join("."),c].filter(Boolean).join(" ");return s?i(s):""}function l(n){return d(n)?n:{}}function d(n){return typeof n=="object"&&n!==null&&!Array.isArray(n)}function o(n){return n?S(n):""}const S=String;
1
+ import{redactSensitiveText as i}from"../kernel/redaction.js";import{isUnknownRecord as l}from"../kernel/unknown-values.js";const g=new Set(["passed","succeeded","success","ready","cached"]),h=new Set(["running","pending","queued","starting","applying","cancelling"]),y=new Set(["skipped","skip","noop","no-op","not_applicable","not-applicable","cancelled","canceled","interrupted"]),A=new Set(["failed","error"]);export function normalizeOperationStatus(n){return S(n||"unknown").trim().toLowerCase().replaceAll(" ","_")}export function operationStatusKind(n){const e=normalizeOperationStatus(n);return g.has(e)?"passed":h.has(e)?"running":y.has(e)?"skipped":A.has(e)?"failed":"unknown"}export function operationStatusLabel(n,{runningLabel:e="RUN"}={}){const t=operationStatusKind(n);return t==="passed"?"PASS":t==="running"?e:t==="skipped"?"SKIP":"FAIL"}export function isNeutralOperationStatus(n){return operationStatusKind(n)==="skipped"}export function isSuccessfulOperationStatus(n){return operationStatusKind(n)==="passed"}export function formatOperationSummary(n={}){const e=d(n),t=d(e.metadata),r=[];e.message?r.push(i(e.message)):e.summary?r.push(i(e.summary)):e.error&&r.push(i(e.error));const a=o(t.phase||e.phase),c=o(t.action||e.action),s=o(t.resource||e.resource),u=o(t.decision),p=o(t.reason),f=k(e.logRefs);if(!r.length&&(a||c||s)){const m=[a,c].filter(Boolean).join(".");r.push(m+(s?` ${s}`:""))}return u&&r.push(`decision=${i(u)}`),p&&r.push(`reason=${i(p)}`),f&&r.push(`log=${f}`),r.filter(Boolean).join(" \xB7 ")}function k(n=[]){if(!Array.isArray(n))return"";const e=n.find(l);if(!e)return"";const t=o(e.service||e.serviceName),r=o(e.category||e.kind),a=o(e.step||e.name),c=o(e.state),s=[t,[r,a].filter(Boolean).join("."),c].filter(Boolean).join(" ");return s?i(s):""}function d(n){return l(n)?n:{}}function o(n){return n?S(n):""}const S=String;
@@ -1,3 +1,2 @@
1
1
  export declare const TEST_OUTCOME_STATUS_ORDER: readonly ["passed", "failed", "skipped", "not_run", "cancelled", "interrupted"];
2
2
  export type TestOutcomeStatus = (typeof TEST_OUTCOME_STATUS_ORDER)[number];
3
- export declare function isTestOutcomeStatus(value: unknown): value is TestOutcomeStatus;
@@ -1 +1 @@
1
- export const TEST_OUTCOME_STATUS_ORDER=Object.freeze(["passed","failed","skipped","not_run","cancelled","interrupted"]);const t=new Set(TEST_OUTCOME_STATUS_ORDER);export function isTestOutcomeStatus(e){return typeof e=="string"&&t.has(e)}
1
+ export const TEST_OUTCOME_STATUS_ORDER=Object.freeze(["passed","failed","skipped","not_run","cancelled","interrupted"]);
@@ -1 +1 @@
1
- import o from"node:fs";import{fileURLToPath as i}from"node:url";import{currentPlatformTarget as a,platformTargetKey as n,SUPPORTED_PLATFORM_TARGETS as f,UnsupportedPlatformTargetError as p}from"../../process/host/platform.js";const s=c();export function engineEvidenceIdentity(e){if(e.profile.engineId!==e.engineId)throw new TypeError(`Evidence profile ${e.profile.id} belongs to ${e.profile.engineId}, expected ${e.engineId}`);const t=e.target??a();if(!f.some(r=>n(r)===n(t)))throw new p(t.platform,t.arch,"engine evidence target");return Object.freeze({engineId:e.engineId,installationKind:e.installationKind,nodeVersion:process.versions.node,profileId:e.profile.id,target:Object.freeze({arch:t.arch,platform:t.platform}),testkitVersion:s})}function c(){const e=i(new URL("../../../package.json",import.meta.url));let t;try{t=JSON.parse(o.readFileSync(e,"utf8"))}catch(r){throw new TypeError(`Root Testkit package manifest could not be read from ${e}`,{cause:r})}if(!d(t)||t.name!=="@gfdelarue/testkit")throw new TypeError(`Root Testkit package manifest at ${e} has an invalid package identity`);if(typeof t.version!="string"||!/^(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u.test(t.version))throw new TypeError(`Root Testkit package manifest at ${e} has an invalid version`);return t.version}function d(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}
1
+ import o from"node:fs";import{fileURLToPath as i}from"node:url";import{currentPlatformTarget as a,platformTargetKey as n,SUPPORTED_PLATFORM_TARGETS as f,UnsupportedPlatformTargetError as p}from"../../process/host/platform.js";import{isUnknownRecord as s}from"../../kernel/unknown-values.js";const d=c();export function engineEvidenceIdentity(e){if(e.profile.engineId!==e.engineId)throw new TypeError(`Evidence profile ${e.profile.id} belongs to ${e.profile.engineId}, expected ${e.engineId}`);const t=e.target??a();if(!f.some(r=>n(r)===n(t)))throw new p(t.platform,t.arch,"engine evidence target");return Object.freeze({engineId:e.engineId,installationKind:e.installationKind,nodeVersion:process.versions.node,profileId:e.profile.id,target:Object.freeze({arch:t.arch,platform:t.platform}),testkitVersion:d})}function c(){const e=i(new URL("../../../package.json",import.meta.url));let t;try{t=JSON.parse(o.readFileSync(e,"utf8"))}catch(r){throw new TypeError(`Root Testkit package manifest could not be read from ${e}`,{cause:r})}if(!s(t)||t.name!=="@gfdelarue/testkit")throw new TypeError(`Root Testkit package manifest at ${e} has an invalid package identity`);if(typeof t.version!="string"||!/^(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u.test(t.version))throw new TypeError(`Root Testkit package manifest at ${e} has an invalid version`);return t.version}
@@ -38,5 +38,4 @@ export declare function processOutputEvidence(result: ManagedProcessResult): Eng
38
38
  export declare function emptyEvidence(): EngineEvidence;
39
39
  export declare function failureFromError(error: unknown, code: string): EngineFailureEvidence;
40
40
  export declare function errorDetails(error: Error): EngineJsonObject;
41
- export declare function toError(error: unknown): Error;
42
41
  export {};
@@ -1 +1 @@
1
- import{isEngineTaskDeadlineExceededReason as d}from"../api/index.js";import"../../domain/test-outcomes.js";export async function executeEngineTasks(e,t,n,i,o){const a=[];for(const r of t){e.observer?.taskStarted(r.task);const s=Date.now(),c=taskBeforeStartOutcome(r.task,e.signal,s,n);c!==null?a.push(c):r.preparationFailure!==null?a.push(terminalOutcome(r.task.id,"failed",s,emptyEvidence(),r.preparationFailure)):a.push(await i(r,s))}for(const[r,s]of a.entries())e.observer?.evidenceRecorded(t[r].task.id,s.evidence),e.observer?.taskFinished(s);return{evidence:{artifacts:a.flatMap(r=>r.evidence.artifacts),logs:a.flatMap(r=>r.evidence.logs),metrics:a.flatMap(r=>r.evidence.metrics),native:o},invocationId:e.invocationId,outcomes:a}}export function taskBeforeStartOutcome(e,t,n,i){return e.deadlineMs<=n||d(t.reason)?terminalOutcome(e.id,"failed",n,emptyEvidence(),taskTimeoutFailure(e.timeoutMs,i)):t.aborted?terminalOutcome(e.id,"cancelled",n,emptyEvidence(),{code:`${i.codePrefix}_CANCELLED`,message:`${i.label} task was cancelled before its process started`}):null}export function taskTimeoutFailure(e,t){return{code:`${t.codePrefix}_TASK_TIMEOUT`,details:{timeoutMs:e},message:`${t.label} task exceeded its ${e.toString()}ms timeout`}}export function managedProcessFailure(e,t,n=null){return e.terminationError===null?null:{classification:"infra",code:`${t.codePrefix}_PROCESS_TERMINATION_INCOMPLETE`,details:{cancelled:e.cancelled,...n===null?{}:{evidenceCollectionFailure:errorDetails(n)},outputLimitExceeded:e.outputLimitExceeded,timedOut:e.timedOut},message:`${t.label} managed-process termination was incomplete: ${e.terminationError.message}`,...e.terminationError.stack===void 0?{}:{stack:e.terminationError.stack}}}export function classifyManagedProcessResult(e,t,n,i){const o=r=>({failure:r,status:"failed"}),a=managedProcessFailure(e,i);return a!==null?o(a):e.timedOut||n?o(taskTimeoutFailure(t,i)):e.cancelled?{failure:{code:`${i.codePrefix}_CANCELLED`,message:`${i.label} task was cancelled`},status:"cancelled"}:e.outputLimitExceeded?o({code:`${i.codePrefix}_OUTPUT_LIMIT`,message:`${i.label} task exceeded the bounded stdout/stderr capture limit`}):e.error!==null?o({...failureFromError(e.error,`${i.codePrefix}_PROCESS_START_FAILED`),classification:"infra"}):null}export function processExitDetails(e){return{...e.exitCode===null?{}:{exitCode:e.exitCode},...e.exitSignal===null?{}:{exitSignal:e.exitSignal}}}export function processExitFailure(e,t,n=null,i=t.label){return{failure:{code:`${t.codePrefix}_PROCESS_FAILED`,details:processExitDetails(e),message:n??(e.exitCode===null?`${i} process terminated by ${e.exitSignal??"an unknown signal"}`:`${i} process exited with code ${e.exitCode.toString()}`)},status:"failed"}}export function firstOutputLine(e){const t=e.split(/\r?\n/u).map(n=>n.trim()).find(Boolean);return t===void 0?null:t.slice(0,4e3)}export function terminalOutcome(e,t,n,i,o){const a=Date.now();return{durationMs:Math.max(0,a-n),evidence:i,failure:o,finishedAtMs:a,startedAtMs:n,status:t,taskId:e}}export function processOutputEvidence(e){return{artifacts:[],logs:[...e.stdout.length===0?[]:[{stream:"stdout",text:e.stdout}],...e.stderr.length===0?[]:[{stream:"stderr",text:e.stderr}]],metrics:[]}}export function emptyEvidence(){return{artifacts:[],logs:[],metrics:[]}}export function failureFromError(e,t){const n=toError(e);return{code:t,message:n.message,...n.stack===void 0?{}:{stack:n.stack}}}export function errorDetails(e){return{message:e.message,name:e.name,...e.stack===void 0?{}:{stack:e.stack}}}export function toError(e){return e instanceof Error?e:new Error(String(e))}
1
+ import{isEngineTaskDeadlineExceededReason as d}from"../api/index.js";import"../../domain/test-outcomes.js";import{toError as l}from"../../kernel/error-serialization.js";export async function executeEngineTasks(e,t,n,i,o){const a=[];for(const r of t){e.observer?.taskStarted(r.task);const s=Date.now(),c=taskBeforeStartOutcome(r.task,e.signal,s,n);c!==null?a.push(c):r.preparationFailure!==null?a.push(terminalOutcome(r.task.id,"failed",s,emptyEvidence(),r.preparationFailure)):a.push(await i(r,s))}for(const[r,s]of a.entries())e.observer?.evidenceRecorded(t[r].task.id,s.evidence),e.observer?.taskFinished(s);return{evidence:{artifacts:a.flatMap(r=>r.evidence.artifacts),logs:a.flatMap(r=>r.evidence.logs),metrics:a.flatMap(r=>r.evidence.metrics),native:o},invocationId:e.invocationId,outcomes:a}}export function taskBeforeStartOutcome(e,t,n,i){return e.deadlineMs<=n||d(t.reason)?terminalOutcome(e.id,"failed",n,emptyEvidence(),taskTimeoutFailure(e.timeoutMs,i)):t.aborted?terminalOutcome(e.id,"cancelled",n,emptyEvidence(),{code:`${i.codePrefix}_CANCELLED`,message:`${i.label} task was cancelled before its process started`}):null}export function taskTimeoutFailure(e,t){return{code:`${t.codePrefix}_TASK_TIMEOUT`,details:{timeoutMs:e},message:`${t.label} task exceeded its ${e.toString()}ms timeout`}}export function managedProcessFailure(e,t,n=null){return e.terminationError===null?null:{classification:"infra",code:`${t.codePrefix}_PROCESS_TERMINATION_INCOMPLETE`,details:{cancelled:e.cancelled,...n===null?{}:{evidenceCollectionFailure:errorDetails(n)},outputLimitExceeded:e.outputLimitExceeded,timedOut:e.timedOut},message:`${t.label} managed-process termination was incomplete: ${e.terminationError.message}`,...e.terminationError.stack===void 0?{}:{stack:e.terminationError.stack}}}export function classifyManagedProcessResult(e,t,n,i){const o=r=>({failure:r,status:"failed"}),a=managedProcessFailure(e,i);return a!==null?o(a):e.timedOut||n?o(taskTimeoutFailure(t,i)):e.cancelled?{failure:{code:`${i.codePrefix}_CANCELLED`,message:`${i.label} task was cancelled`},status:"cancelled"}:e.outputLimitExceeded?o({code:`${i.codePrefix}_OUTPUT_LIMIT`,message:`${i.label} task exceeded the bounded stdout/stderr capture limit`}):e.error!==null?o({...failureFromError(e.error,`${i.codePrefix}_PROCESS_START_FAILED`),classification:"infra"}):null}export function processExitDetails(e){return{...e.exitCode===null?{}:{exitCode:e.exitCode},...e.exitSignal===null?{}:{exitSignal:e.exitSignal}}}export function processExitFailure(e,t,n=null,i=t.label){return{failure:{code:`${t.codePrefix}_PROCESS_FAILED`,details:processExitDetails(e),message:n??(e.exitCode===null?`${i} process terminated by ${e.exitSignal??"an unknown signal"}`:`${i} process exited with code ${e.exitCode.toString()}`)},status:"failed"}}export function firstOutputLine(e){const t=e.split(/\r?\n/u).map(n=>n.trim()).find(Boolean);return t===void 0?null:t.slice(0,4e3)}export function terminalOutcome(e,t,n,i,o){const a=Date.now();return{durationMs:Math.max(0,a-n),evidence:i,failure:o,finishedAtMs:a,startedAtMs:n,status:t,taskId:e}}export function processOutputEvidence(e){return{artifacts:[],logs:[...e.stdout.length===0?[]:[{stream:"stdout",text:e.stdout}],...e.stderr.length===0?[]:[{stream:"stderr",text:e.stderr}]],metrics:[]}}export function emptyEvidence(){return{artifacts:[],logs:[],metrics:[]}}export function failureFromError(e,t){const n=l(e);return{code:t,message:n.message,...n.stack===void 0?{}:{stack:n.stack}}}export function errorDetails(e){return{message:e.message,name:e.name,...e.stack===void 0?{}:{stack:e.stack}}}
@@ -1,20 +1,10 @@
1
1
  import type { EngineId, PublicTestType } from "../../domain/identifiers.js";
2
- export declare const EXECUTION_REQUIREMENTS: readonly ["none", "optional", "required"];
3
- export type ExecutionRequirement = (typeof EXECUTION_REQUIREMENTS)[number];
4
- export declare const ENDPOINT_ACCESS_MODES: readonly ["none", "published-http"];
5
- export type EndpointAccessMode = (typeof ENDPOINT_ACCESS_MODES)[number];
6
2
  export declare const ENGINE_EXECUTION_HOSTS: readonly ["orchestrator", "kiln-job"];
7
3
  export type EngineExecutionHost = (typeof ENGINE_EXECUTION_HOSTS)[number];
8
4
  export declare const TASK_BATCHING_MODES: readonly ["isolated", "compatible"];
9
5
  export type TaskBatchingMode = (typeof TASK_BATCHING_MODES)[number];
10
- export declare const RUNTIME_ISOLATION_CAPABILITIES: readonly ["none", "orchestrated"];
11
- export type RuntimeIsolationCapability = (typeof RUNTIME_ISOLATION_CAPABILITIES)[number];
12
6
  export interface RunnerProfileCapabilities {
13
- readonly applicationRuntime: ExecutionRequirement;
14
- readonly databaseRuntime: ExecutionRequirement;
15
- readonly endpointAccess: EndpointAccessMode;
16
7
  readonly executionHost: EngineExecutionHost;
17
- readonly runtimeIsolation: RuntimeIsolationCapability;
18
8
  readonly taskBatching: TaskBatchingMode;
19
9
  }
20
10
  interface RunnerProfileIdsByEngine {
@@ -1 +1 @@
1
- export const EXECUTION_REQUIREMENTS=Object.freeze(["none","optional","required"]),ENDPOINT_ACCESS_MODES=Object.freeze(["none","published-http"]),ENGINE_EXECUTION_HOSTS=Object.freeze(["orchestrator","kiln-job"]),TASK_BATCHING_MODES=Object.freeze(["isolated","compatible"]),RUNTIME_ISOLATION_CAPABILITIES=Object.freeze(["none","orchestrated"]);
1
+ export const ENGINE_EXECUTION_HOSTS=Object.freeze(["orchestrator","kiln-job"]),TASK_BATCHING_MODES=Object.freeze(["isolated","compatible"]);
@@ -1 +1 @@
1
- export class EngineInvocationCleanupDeadlineExceededError extends Error{timeoutMs;constructor(n){super(`Engine invocation cleanup exceeded its independent ${n.toString()} ms deadline`),this.name="EngineInvocationCleanupDeadlineExceededError",this.timeoutMs=n}}export function createEngineInvocationScope(t){const n=[];let i=!0,e=null;const s=Object.freeze({defer(r){if(!i)throw new Error("Engine invocation cleanup cannot be registered after draining starts");n.push(r)}});return Object.freeze({drain(){return e!==null||(i=!1,e=l(n.splice(0).reverse(),t.timeoutMs)),e},scope:s})}async function l(t,n){const i=Date.now()+n,e=[];for(const s of t){let r;try{r=s()}catch(u){e.push(a(u));continue}if(r===void 0)continue;const o=Math.max(0,i-Date.now());if(o===0){Promise.resolve(r).catch(()=>{}),e.push(new EngineInvocationCleanupDeadlineExceededError(n));continue}const c=await d(r,o);c==="timeout"?e.push(new EngineInvocationCleanupDeadlineExceededError(n)):c instanceof Error&&e.push(c)}return Object.freeze(e)}async function d(t,n){let i;const e=Promise.resolve(t).then(()=>"fulfilled",o=>a(o)),s=new Promise(o=>{i=setTimeout(()=>{o("timeout")},n)}),r=await Promise.race([e,s]);return clearTimeout(i),r}function a(t){return t instanceof Error?t:new Error(String(t))}
1
+ import{toError as a}from"../../kernel/error-serialization.js";export class EngineInvocationCleanupDeadlineExceededError extends Error{timeoutMs;constructor(n){super(`Engine invocation cleanup exceeded its independent ${n.toString()} ms deadline`),this.name="EngineInvocationCleanupDeadlineExceededError",this.timeoutMs=n}}export function createEngineInvocationScope(i){const n=[];let r=!0,e=null;const s=Object.freeze({defer(t){if(!r)throw new Error("Engine invocation cleanup cannot be registered after draining starts");n.push(t)}});return Object.freeze({drain(){return e!==null||(r=!1,e=l(n.splice(0).reverse(),i.timeoutMs)),e},scope:s})}async function l(i,n){const r=Date.now()+n,e=[];for(const s of i){let t;try{t=s()}catch(u){e.push(a(u));continue}if(t===void 0)continue;const o=Math.max(0,r-Date.now());if(o===0){Promise.resolve(t).catch(()=>{}),e.push(new EngineInvocationCleanupDeadlineExceededError(n));continue}const c=await d(t,o);c==="timeout"?e.push(new EngineInvocationCleanupDeadlineExceededError(n)):c instanceof Error&&e.push(c)}return Object.freeze(e)}async function d(i,n){let r;const e=Promise.resolve(i).then(()=>"fulfilled",o=>a(o)),s=new Promise(o=>{r=setTimeout(()=>{o("timeout")},n)}),t=await Promise.race([e,s]);return clearTimeout(r),t}
@@ -1 +1 @@
1
- import{CHARACTER_JSON_STRING_MEASURE as E,requireBoundedJsonValue as _,RUNTIME_INPUT_JSON_BOUNDS as f}from"../contracts/json-bounds.js";export const K6_RUNTIME_INPUT_VERSION=1,K6_RUNTIME_INPUT_FILE_ENV="TESTKIT_K6_INPUT_FILE",K6_RUNTIME_INPUT_SHA256_ENV="TESTKIT_K6_INPUT_SHA256",K6_RUNTIME_INVOCATION_ID_ENV="TESTKIT_INVOCATION_ID",K6_RUNTIME_TASK_ID_ENV="TESTKIT_TEST_ID",K6_RUNTIME_INPUT_MAX_CHARACTERS=8*1024*1024;const T=/^[a-f0-9]{64}$/u;export function readK6RuntimeInput(t){const n=t.environment[K6_RUNTIME_INPUT_FILE_ENV],e=t.environment[K6_RUNTIME_INPUT_SHA256_ENV];if(!n&&!e)return null;if(!w(n))throw new Error(`${K6_RUNTIME_INPUT_FILE_ENV} must be an absolute owned path`);if(!e||!T.test(e))throw new Error(`${K6_RUNTIME_INPUT_SHA256_ENV} must be a lowercase SHA-256 digest`);const r=t.openText(n);if(typeof r!="string")throw new Error(`${K6_RUNTIME_INPUT_FILE_ENV} must contain UTF-8 JSON text`);if(r.length>K6_RUNTIME_INPUT_MAX_CHARACTERS)throw new Error(`${K6_RUNTIME_INPUT_FILE_ENV} exceeds the ${String(K6_RUNTIME_INPUT_MAX_CHARACTERS)} character limit`);if(t.sha256Hex(r)!==e)throw new Error(`${K6_RUNTIME_INPUT_FILE_ENV} integrity check failed`);let i;try{i=JSON.parse(r)}catch(s){throw new Error(`${K6_RUNTIME_INPUT_FILE_ENV} contains invalid JSON: ${N(s)}`,{cause:s})}const o=parseK6RuntimeInputDocument(i,K6_RUNTIME_INPUT_FILE_ENV);if(o.invocationId!==t.environment[K6_RUNTIME_INVOCATION_ID_ENV])throw new Error(`${K6_RUNTIME_INPUT_FILE_ENV} invocation identity does not match`);if(o.taskId!==t.environment[K6_RUNTIME_TASK_ID_ENV])throw new Error(`${K6_RUNTIME_INPUT_FILE_ENV} task identity does not match`);return o}export function parseK6RuntimeInputDocument(t,n="k6 runtime input"){if(_(t,n,f,E),!I(t))throw new Error(`${n} must contain an object`);const e=new Set(["engineId","fixtures","invocationId","setupData","taskId","version"]);for(const r of Object.keys(t))if(!e.has(r))throw new Error(`${n} contains unknown field ${r}`);if(t.version!==K6_RUNTIME_INPUT_VERSION)throw new Error(`${n}.version must be 1`);if(t.engineId!=="k6")throw new Error(`${n}.engineId must be "k6"`);if(!I(t.fixtures))throw new Error(`${n}.fixtures must be an object`);return{engineId:"k6",fixtures:t.fixtures,invocationId:c(t.invocationId,`${n}.invocationId`),...Object.hasOwn(t,"setupData")?{setupData:t.setupData}:{},taskId:c(t.taskId,`${n}.taskId`),version:K6_RUNTIME_INPUT_VERSION}}function c(t,n){if(typeof t!="string"||t.length===0)throw new Error(`${n} must be a non-empty string`);return t}function w(t){return typeof t=="string"&&(t.startsWith("/")||/^[a-z]:[\\/]/iu.test(t)||t.startsWith("\\\\"))}function I(t){return typeof t=="object"&&t!==null&&!Array.isArray(t)}function N(t){return t instanceof Error?t.message:String(t)}
1
+ import{CHARACTER_JSON_STRING_MEASURE as E,requireBoundedJsonValue as _,RUNTIME_INPUT_JSON_BOUNDS as T}from"../contracts/json-bounds.js";import{isUnknownRecord as c}from"../../kernel/unknown-values.js";export const K6_RUNTIME_INPUT_VERSION=1,K6_RUNTIME_INPUT_FILE_ENV="TESTKIT_K6_INPUT_FILE",K6_RUNTIME_INPUT_SHA256_ENV="TESTKIT_K6_INPUT_SHA256",K6_RUNTIME_INVOCATION_ID_ENV="TESTKIT_INVOCATION_ID",K6_RUNTIME_TASK_ID_ENV="TESTKIT_TEST_ID",K6_RUNTIME_INPUT_MAX_CHARACTERS=8*1024*1024;const f=/^[a-f0-9]{64}$/u;export function readK6RuntimeInput(t){const n=t.environment[K6_RUNTIME_INPUT_FILE_ENV],e=t.environment[K6_RUNTIME_INPUT_SHA256_ENV];if(!n&&!e)return null;if(!w(n))throw new Error(`${K6_RUNTIME_INPUT_FILE_ENV} must be an absolute owned path`);if(!e||!f.test(e))throw new Error(`${K6_RUNTIME_INPUT_SHA256_ENV} must be a lowercase SHA-256 digest`);const r=t.openText(n);if(typeof r!="string")throw new Error(`${K6_RUNTIME_INPUT_FILE_ENV} must contain UTF-8 JSON text`);if(r.length>K6_RUNTIME_INPUT_MAX_CHARACTERS)throw new Error(`${K6_RUNTIME_INPUT_FILE_ENV} exceeds the ${String(K6_RUNTIME_INPUT_MAX_CHARACTERS)} character limit`);if(t.sha256Hex(r)!==e)throw new Error(`${K6_RUNTIME_INPUT_FILE_ENV} integrity check failed`);let i;try{i=JSON.parse(r)}catch(s){throw new Error(`${K6_RUNTIME_INPUT_FILE_ENV} contains invalid JSON: ${N(s)}`,{cause:s})}const o=parseK6RuntimeInputDocument(i,K6_RUNTIME_INPUT_FILE_ENV);if(o.invocationId!==t.environment[K6_RUNTIME_INVOCATION_ID_ENV])throw new Error(`${K6_RUNTIME_INPUT_FILE_ENV} invocation identity does not match`);if(o.taskId!==t.environment[K6_RUNTIME_TASK_ID_ENV])throw new Error(`${K6_RUNTIME_INPUT_FILE_ENV} task identity does not match`);return o}export function parseK6RuntimeInputDocument(t,n="k6 runtime input"){if(_(t,n,T,E),!c(t))throw new Error(`${n} must contain an object`);const e=new Set(["engineId","fixtures","invocationId","setupData","taskId","version"]);for(const r of Object.keys(t))if(!e.has(r))throw new Error(`${n} contains unknown field ${r}`);if(t.version!==K6_RUNTIME_INPUT_VERSION)throw new Error(`${n}.version must be 1`);if(t.engineId!=="k6")throw new Error(`${n}.engineId must be "k6"`);if(!c(t.fixtures))throw new Error(`${n}.fixtures must be an object`);return{engineId:"k6",fixtures:t.fixtures,invocationId:I(t.invocationId,`${n}.invocationId`),...Object.hasOwn(t,"setupData")?{setupData:t.setupData}:{},taskId:I(t.taskId,`${n}.taskId`),version:K6_RUNTIME_INPUT_VERSION}}function I(t,n){if(typeof t!="string"||t.length===0)throw new Error(`${n} must be a non-empty string`);return t}function w(t){return typeof t=="string"&&(t.startsWith("/")||/^[a-z]:[\\/]/iu.test(t)||t.startsWith("\\\\"))}function N(t){return t instanceof Error?t.message:String(t)}
@@ -1 +1 @@
1
- import p from"fs";import c from"path";import{collectDirectorySummary as E,listFiles as g}from"../process/files/directory-status.js";const S="manifest.json",M=10080*60*1e3;export function collectBundleCacheStatus(t,e="shared"){const n=c.join(t,".testkit","_bundles",e||"shared"),s=E(n),r=g(n),o=r.filter(a=>a.path.endsWith(".js")),u=r.filter(a=>a.path.endsWith(".entry.mjs")),l=C(n),i=new Set(l.entries.flatMap(a=>[a.outputFile,a.entryFile].filter(m).map(x=>c.resolve(x)))),f=D(u),h=[...f.values()].filter(a=>a.length>1).length,d=r.filter(a=>c.basename(a.path)!==S&&!i.has(c.resolve(a.path))),F=w(o,l);return{serviceName:e,path:n,exists:s.exists,sizeBytes:s.sizeBytes,fileCount:s.fileCount,jsFileCount:o.length,entryFileCount:u.length,sourceFileCount:f.size||l.sourceFileCount,duplicatedSourceCount:h,sourcemapFileCount:F.count,sourcemapStatus:F.status,manifest:{exists:l.exists,entryCount:l.entries.length},unmanagedFileCount:d.length}}export function collectBundleCleanupTargets(t,{allConfigs:e=[],serviceName:n=null}={}){const s=n?[n]:e.map(o=>o.name).sort(),r=[];for(const o of s){const u=c.join(t,".testkit","_bundles",o||"shared");if(g(u).length===0)continue;const i=C(u),f=N(u,i.entries);r.push(...f.map(h=>({...h,serviceName:o})))}return _(r)}function C(t){const e=c.join(t,S);if(!p.existsSync(e))return{exists:!1,entries:[],sourceFileCount:0};try{const n=JSON.parse(p.readFileSync(e,"utf8")),s=b(n);return{exists:!0,entries:s,sourceFileCount:new Set(s.map(r=>r.sourceFile).filter(m)).size}}catch{return{exists:!0,entries:[],sourceFileCount:0}}}function b(t){return!A(t)||!Array.isArray(t.entries)?[]:t.entries.map(j).filter(e=>e!==null)}function j(t){if(!A(t))return null;const e={hasSourcemap:!!t.hasSourcemap};return typeof t.cacheKey=="string"&&(e.cacheKey=t.cacheKey),typeof t.createdAt=="string"&&(e.createdAt=t.createdAt),typeof t.entryFile=="string"&&(e.entryFile=t.entryFile),typeof t.lastUsedAt=="string"&&(e.lastUsedAt=t.lastUsedAt),typeof t.outputFile=="string"&&(e.outputFile=t.outputFile),typeof t.sourceFile=="string"&&(e.sourceFile=t.sourceFile),e}function D(t){const e=new Map;for(const n of t){const s=P(n.path);if(!s)continue;const r=e.get(s)||[];r.push(n),e.set(s,r)}return e}function P(t){try{const n=p.readFileSync(t,"utf8").match(/import \* as suiteModule from ("(?:\\.|[^"])+");/);if(!n)return null;const s=JSON.parse(n[1]);return typeof s=="string"?s:null}catch{return null}}function w(t,e){if(e.entries.length>0){const r=e.entries.filter(o=>o.hasSourcemap).length;return{count:r,status:r>0?"present":"none"}}const n=t.slice(0,25);return n.some(r=>T(r.path))?{count:null,status:"detected"}:{count:0,status:t.length>n.length?"not-detected-in-sample":"none"}}function T(t){try{return/sourceMappingURL=data:application\/json/.test(p.readFileSync(t,"utf8"))}catch{return!1}}function N(t,e){if(e.length===0)return[];const n=Date.now(),s=K(e,o=>o.sourceFile||o.cacheKey||o.outputFile),r=[];for(const o of s.values()){const u=[...o].sort(z),l=new Set(u.filter((i,f)=>f<2||n-y(i)<M).map(i=>i.cacheKey||i.outputFile));for(const i of u){const f=i.cacheKey||i.outputFile;if(!l.has(f))for(const h of[i.outputFile,i.entryFile].filter(m)){const d=U(t,h);d&&r.push({path:d,reason:"stale-bundle-version",sourceFile:i.sourceFile||null})}}}return r}function U(t,e){const n=c.resolve(t),s=c.resolve(n,e);if(!B(n,s))return null;const r=c.dirname(s);if(!p.existsSync(r))return s;try{const o=p.realpathSync.native(n),u=p.realpathSync.native(r);if(u!==o&&!B(o,u))return null}catch{return null}return s}function B(t,e){const n=c.relative(t,e);return!!n&&n!==".."&&!n.startsWith(`..${c.sep}`)&&!c.isAbsolute(n)}function z(t,e){return y(e)-y(t)||(t.outputFile||"").localeCompare(e.outputFile||"")}function y(t){return Date.parse(String(t.lastUsedAt||t.createdAt||0))}function K(t,e){const n=new Map;for(const s of t){const r=e(s),o=n.get(r)||[];o.push(s),n.set(r,o)}return n}function _(t){const e=new Map;for(const n of t)n.path&&e.set(c.resolve(n.path),{...n,path:c.resolve(n.path)});return[...e.values()].sort((n,s)=>n.path.localeCompare(s.path))}function A(t){return typeof t=="object"&&t!==null&&!Array.isArray(t)}function m(t){return typeof t=="string"&&t.length>0}
1
+ import p from"fs";import c from"path";import{collectDirectorySummary as E,listFiles as g}from"../process/files/directory-status.js";import{isUnknownRecord as S}from"../kernel/unknown-values.js";const C="manifest.json",M=10080*60*1e3;export function collectBundleCacheStatus(t,e="shared"){const n=c.join(t,".testkit","_bundles",e||"shared"),s=E(n),r=g(n),o=r.filter(a=>a.path.endsWith(".js")),u=r.filter(a=>a.path.endsWith(".entry.mjs")),l=B(n),i=new Set(l.entries.flatMap(a=>[a.outputFile,a.entryFile].filter(y).map(A=>c.resolve(A)))),f=j(u),h=[...f.values()].filter(a=>a.length>1).length,d=r.filter(a=>c.basename(a.path)!==C&&!i.has(c.resolve(a.path))),F=P(o,l);return{serviceName:e,path:n,exists:s.exists,sizeBytes:s.sizeBytes,fileCount:s.fileCount,jsFileCount:o.length,entryFileCount:u.length,sourceFileCount:f.size||l.sourceFileCount,duplicatedSourceCount:h,sourcemapFileCount:F.count,sourcemapStatus:F.status,manifest:{exists:l.exists,entryCount:l.entries.length},unmanagedFileCount:d.length}}export function collectBundleCleanupTargets(t,{allConfigs:e=[],serviceName:n=null}={}){const s=n?[n]:e.map(o=>o.name).sort(),r=[];for(const o of s){const u=c.join(t,".testkit","_bundles",o||"shared");if(g(u).length===0)continue;const i=B(u),f=U(u,i.entries);r.push(...f.map(h=>({...h,serviceName:o})))}return K(r)}function B(t){const e=c.join(t,C);if(!p.existsSync(e))return{exists:!1,entries:[],sourceFileCount:0};try{const n=JSON.parse(p.readFileSync(e,"utf8")),s=D(n);return{exists:!0,entries:s,sourceFileCount:new Set(s.map(r=>r.sourceFile).filter(y)).size}}catch{return{exists:!0,entries:[],sourceFileCount:0}}}function D(t){return!S(t)||!Array.isArray(t.entries)?[]:t.entries.map(b).filter(e=>e!==null)}function b(t){if(!S(t))return null;const e={hasSourcemap:!!t.hasSourcemap};return typeof t.cacheKey=="string"&&(e.cacheKey=t.cacheKey),typeof t.createdAt=="string"&&(e.createdAt=t.createdAt),typeof t.entryFile=="string"&&(e.entryFile=t.entryFile),typeof t.lastUsedAt=="string"&&(e.lastUsedAt=t.lastUsedAt),typeof t.outputFile=="string"&&(e.outputFile=t.outputFile),typeof t.sourceFile=="string"&&(e.sourceFile=t.sourceFile),e}function j(t){const e=new Map;for(const n of t){const s=w(n.path);if(!s)continue;const r=e.get(s)||[];r.push(n),e.set(s,r)}return e}function w(t){try{const n=p.readFileSync(t,"utf8").match(/import \* as suiteModule from ("(?:\\.|[^"])+");/);if(!n)return null;const s=JSON.parse(n[1]);return typeof s=="string"?s:null}catch{return null}}function P(t,e){if(e.entries.length>0){const r=e.entries.filter(o=>o.hasSourcemap).length;return{count:r,status:r>0?"present":"none"}}const n=t.slice(0,25);return n.some(r=>T(r.path))?{count:null,status:"detected"}:{count:0,status:t.length>n.length?"not-detected-in-sample":"none"}}function T(t){try{return/sourceMappingURL=data:application\/json/.test(p.readFileSync(t,"utf8"))}catch{return!1}}function U(t,e){if(e.length===0)return[];const n=Date.now(),s=z(e,o=>o.sourceFile||o.cacheKey||o.outputFile),r=[];for(const o of s.values()){const u=[...o].sort(k),l=new Set(u.filter((i,f)=>f<2||n-m(i)<M).map(i=>i.cacheKey||i.outputFile));for(const i of u){const f=i.cacheKey||i.outputFile;if(!l.has(f))for(const h of[i.outputFile,i.entryFile].filter(y)){const d=N(t,h);d&&r.push({path:d,reason:"stale-bundle-version",sourceFile:i.sourceFile||null})}}}return r}function N(t,e){const n=c.resolve(t),s=c.resolve(n,e);if(!x(n,s))return null;const r=c.dirname(s);if(!p.existsSync(r))return s;try{const o=p.realpathSync.native(n),u=p.realpathSync.native(r);if(u!==o&&!x(o,u))return null}catch{return null}return s}function x(t,e){const n=c.relative(t,e);return!!n&&n!==".."&&!n.startsWith(`..${c.sep}`)&&!c.isAbsolute(n)}function k(t,e){return m(e)-m(t)||(t.outputFile||"").localeCompare(e.outputFile||"")}function m(t){return Date.parse(String(t.lastUsedAt||t.createdAt||0))}function z(t,e){const n=new Map;for(const s of t){const r=e(s),o=n.get(r)||[];o.push(s),n.set(r,o)}return n}function K(t){const e=new Map;for(const n of t)n.path&&e.set(c.resolve(n.path),{...n,path:c.resolve(n.path)});return[...e.values()].sort((n,s)=>n.path.localeCompare(s.path))}function y(t){return typeof t=="string"&&t.length>0}