@gfdelarue/testkit 0.6.6 → 0.6.7

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 (133) hide show
  1. package/README.md +134 -0
  2. package/dist/app/bugs/ci-report.js +2 -2
  3. package/dist/app/campaigns/brief.js +1 -1
  4. package/dist/app/cleanup/index.js +1 -1
  5. package/dist/app/doctor/index.js +1 -1
  6. package/dist/app/environments/static-values.js +1 -1
  7. package/dist/app/environments/supplied.js +1 -1
  8. package/dist/app/environments/task-databases.js +2 -2
  9. package/dist/app/evidence/ci-status.js +1 -0
  10. package/dist/app/evidence/ci.js +1 -0
  11. package/dist/app/evidence/index.js +1 -0
  12. package/dist/app/evidence/inputs.js +1 -0
  13. package/dist/app/evidence/report.js +1 -0
  14. package/dist/app/lint/index.js +1 -1
  15. package/dist/app/run/engine-evidence-persistence.js +1 -1
  16. package/dist/app/run/engine-execution-failures.js +1 -1
  17. package/dist/app/run/engine-execution.js +1 -1
  18. package/dist/app/run/operation-progress.js +1 -1
  19. package/dist/app/run/orchestrator.js +1 -1
  20. package/dist/app/run/results.js +1 -1
  21. package/dist/app/run/run-events.js +1 -1
  22. package/dist/app/run/run-finalization.js +1 -1
  23. package/dist/app/run/run-guards.js +3 -3
  24. package/dist/app/run/scheduler/estimates.js +1 -1
  25. package/dist/app/run/scheduler/index.js +1 -1
  26. package/dist/app/run/task-batches.js +1 -1
  27. package/dist/app/run/worker-loop.js +1 -1
  28. package/dist/app/status/runs.js +1 -1
  29. package/dist/app/typecheck/index.js +4 -4
  30. package/dist/cli/command-catalog.js +1 -1
  31. package/dist/cli/command-registry.js +1 -1
  32. package/dist/cli/commands/evidence.js +2 -0
  33. package/dist/cli/renderers/destroy/text.js +1 -1
  34. package/dist/cli/renderers/evidence/text.js +1 -0
  35. package/dist/cli/renderers/run/failure.js +1 -1
  36. package/dist/cli/renderers/run/status.js +1 -1
  37. package/dist/cli/renderers/run/text-reporter.js +23 -23
  38. package/dist/config/loader/decode-discovery.js +1 -1
  39. package/dist/config/loader/discover-tests.js +1 -1
  40. package/dist/config/loader/discovery-result.js +1 -1
  41. package/dist/config/loader/discovery.js +2 -2
  42. package/dist/config/loader/env.js +1 -1
  43. package/dist/config/loader/file-metadata.js +1 -1
  44. package/dist/config/loader/groups.js +2 -2
  45. package/dist/config/loader/index.js +1 -1
  46. package/dist/config/loader/load.js +1 -1
  47. package/dist/config/loader/path-policy.js +1 -1
  48. package/dist/config/loader/runtime-isolation.js +1 -1
  49. package/dist/config/loader/runtime.js +1 -1
  50. package/dist/config/loader/test-selection.js +1 -1
  51. package/dist/config/loader/validation.js +1 -1
  52. package/dist/database/admin.js +4 -4
  53. package/dist/database/cleanup.js +1 -1
  54. package/dist/database/configured-steps.js +1 -1
  55. package/dist/database/docker.js +3 -5
  56. package/dist/database/index.js +2 -2
  57. package/dist/database/local-postgres.js +3 -4
  58. package/dist/database/postgres/index.js +2 -2
  59. package/dist/database/progress.js +1 -1
  60. package/dist/database/roles.js +2 -2
  61. package/dist/database/runtime-config.js +1 -1
  62. package/dist/database/schema-source.js +3 -3
  63. package/dist/database/source-url.js +1 -1
  64. package/dist/database/template-steps.js +3 -3
  65. package/dist/domain/operation-reason.js +1 -1
  66. package/dist/domain/suite-selection.js +1 -1
  67. package/dist/engines/adapters/evidence-files.js +1 -1
  68. package/dist/engines/adapters/evidence-identity.js +1 -1
  69. package/dist/engines/adapters/execution.js +1 -1
  70. package/dist/engines/adapters/node-startup-cache.js +1 -1
  71. package/dist/engines/adapters/workspace.js +1 -1
  72. package/dist/engines/contracts/fixture-manifest.js +1 -1
  73. package/dist/engines/installations/package-resolution.js +1 -1
  74. package/dist/engines/managed-module-resolution.js +2 -2
  75. package/dist/engines/node-vitest/adapter/adapter.js +1 -1
  76. package/dist/engines/node-vitest/runtime/data/report.js +3 -3
  77. package/dist/engines/node-vitest/runtime/data/scenario.js +2 -2
  78. package/dist/engines/node-vitest/selection.js +1 -1
  79. package/dist/engines/playwright/adapter/adapter.js +1 -1
  80. package/dist/engines/playwright/adapter/config.js +3 -3
  81. package/dist/engines/playwright/adapter/index.js +1 -1
  82. package/dist/environment/lifecycle-controller.js +1 -1
  83. package/dist/environment/template.js +1 -1
  84. package/dist/evidence/artifacts.js +3 -3
  85. package/dist/evidence/bundles.js +2 -2
  86. package/dist/evidence/environment-record.js +1 -1
  87. package/dist/evidence/index.js +1 -1
  88. package/dist/evidence/logs.js +2 -2
  89. package/dist/evidence/operation-timings.js +1 -1
  90. package/dist/evidence/reading.js +1 -1
  91. package/dist/evidence/reporting.js +1 -1
  92. package/dist/evidence/run-evidence.js +1 -1
  93. package/dist/evidence/run-inputs.js +1 -0
  94. package/dist/evidence/run-model.js +1 -1
  95. package/dist/evidence/setup-progress.js +1 -1
  96. package/dist/evidence/storage-inventory.js +1 -1
  97. package/dist/evidence/storage.js +2 -2
  98. package/dist/process/capture-output.js +1 -1
  99. package/dist/process/execa-bundle.json +3 -3
  100. package/dist/process/host/platform.js +1 -1
  101. package/dist/process/locks.js +4 -4
  102. package/dist/process/process-group.js +1 -1
  103. package/dist/process/process.js +1 -1
  104. package/dist/process/ready-deadline.js +1 -1
  105. package/dist/process/runs/manifest.js +1 -1
  106. package/dist/process/runs/stop.js +1 -1
  107. package/dist/process/windows-native.js +1 -1
  108. package/dist/repo/content.js +4 -0
  109. package/dist/repo/fingerprint-policy.js +1 -1
  110. package/dist/repo/managed-git.js +1 -1
  111. package/dist/repo/metadata.js +1 -1
  112. package/dist/repo/state.js +4 -4
  113. package/dist/steps/configured-step-testkit-modules.js +1 -1
  114. package/dist/steps/configured-steps.js +1 -1
  115. package/dist/steps/environment.js +1 -1
  116. package/dist/steps/index.js +1 -1
  117. package/dist/steps/run.js +2 -2
  118. package/dist/toolchains/esbuild.js +1 -1
  119. package/dist/toolchains/index.js +3 -3
  120. package/dist/toolchains/node-integrity.js +1 -1
  121. package/dist/toolchains/node-provisioning.js +2 -2
  122. package/dist/toolchains/semver.js +1 -1
  123. package/dist/tracker/gitea.js +1 -1
  124. package/dist/tracker/github.js +1 -1
  125. package/dist/tracker/requests.js +1 -1
  126. package/dist/tracker/responses.js +1 -1
  127. package/docs/bug-records.md +18 -2
  128. package/docs/release-notes.md +27 -0
  129. package/package.json +1 -1
  130. package/dist/app/run/planning-types.js +0 -1
  131. package/dist/app/run/selection.js +0 -1
  132. package/dist/config/authoring/http-methods.js +0 -1
  133. package/dist/evidence/artifact-validation.js +0 -1
@@ -1 +1 @@
1
- import{isWorkObserved as $,recordWork as L}from"../../kernel/work-metrics.js";import{isPathWithin as h}from"../../process/files/paths.js";import d from"fs";import S from"path";import{createRequire as A}from"node:module";import{fileURLToPath as B}from"node:url";import{ENGINE_IDS as O,PUBLIC_TEST_TYPES as j,isEngineId as K}from"../../domain/identifiers.js";import{normalizeRuntimeIsolationPolicy as V}from"../../domain/runtime-isolation-contract.js";import{resolveEngineProfile as W,allowsEngineAuthoringImport as _,expectedEngineAuthoringModule as z,invalidEngineAuthoringImport as U,requireMatchingEngineImport as G}from"../../engines/catalog.js";const J=A(import.meta.url),Y=B(import.meta.resolve("typescript"));let H;function a(){return H??=J(Y)}const I="@gfdelarue/testkit";export function loadTestFileMetadataMap(e,t={},i=new Map){for(const r of Object.values(t||{}).flatMap(s=>s??[]))for(const s of r.files||[])i.has(s)||i.set(s,readTestFileMetadata(e,s));return i}export function readTestFileMetadata(e,t){const i=j.find(m=>t.endsWith(`.${m}.testkit.ts`));if(i===void 0)return{engineId:"node-vitest",locks:[],profileId:"node-vitest",runtimeIsolation:null,skipReason:null};const r=a(),s=Ie(e,t);let n;if($()){const m=d.readFileSync(s);L("metadata.filesRead",1),L("metadata.bytesRead",m.byteLength),n=m.toString("utf8")}else n=d.readFileSync(s,"utf8");const o=r.createSourceFile(s,n,r.ScriptTarget.Latest,!0,r.ScriptKind.TS),l=ke(o);if(l.length>0){const m=l.map(f=>r.flattenDiagnosticMessageText(f.messageText," ")).join("; ");throw c(t,`TypeScript syntax is invalid: ${m}`)}let u=null,p=null;for(const m of o.statements)if(!(!r.isVariableStatement(m)||!ie(m))){for(const f of m.declarationList.declarations)if(!(!r.isIdentifier(f.name)||f.name.text!=="testkit")){if(u)throw c(t,'exported "testkit" metadata must be declared exactly once');if(!re(m))throw c(t,'exported "testkit" metadata must be const');u=f,p=m}}if(!u||!p)throw c(t,'export const testkit = defineFile({ engine: "..." }) is required');se(o,p,t);const x=Q(u.initializer,t,i);return requireMatchingEngineAuthoringImports(o,t,x),x}function Q(e,t,i){const r=a();if(!e)throw c(t,'exported "testkit" metadata has no value');if(!r.isCallExpression(e))throw c(t,"testkit metadata must use defineFile({ ... })");if(te(e.expression)!=="defineFile")throw c(t,"testkit metadata must call defineFile");if(e.arguments.length!==1)throw c(t,"defineFile metadata must receive exactly one object literal");const s=e.arguments[0];if(!r.isObjectLiteralExpression(s))throw c(t,"defineFile metadata must receive an object literal");return X(s,t,i)}function X(e,t,i){const r=a(),s={locks:[],runtimeIsolation:null,skipReason:null};let n=null;const o=new Set;for(const u of e.properties){if(r.isSpreadAssignment(u))throw c(t,"testkit metadata may not contain spread properties");if(!r.isPropertyAssignment(u))throw c(t,"testkit metadata properties must use key: value assignments");const p=b(u.name,t,"testkit metadata");if(!ne(p))throw c(t,`unknown metadata property "${p}"`);if(o.has(p))throw c(t,`duplicate metadata property "${p}"`);if(o.add(p),p==="engine"){n=k(u.initializer,t,'metadata property "engine"');continue}if(p==="locks"){s.locks=Z(u.initializer,t);continue}if(p==="skip"){s.skipReason=P(u.initializer,t);continue}s.runtimeIsolation=ee(u.initializer,t)}if(!n)throw c(t,'a literal "engine" property is required');if(!K(n))throw c(t,`unsupported engine "${n}"; expected one of: ${O.join(", ")}`);let l;try{l=W(i,n)}catch(u){throw c(t,M(u),u)}return{engineId:n,locks:s.locks,profileId:l.id,runtimeIsolation:s.runtimeIsolation,skipReason:s.skipReason}}function Z(e,t){const i=a();if(!i.isArrayLiteralExpression(e))throw c(t,'metadata property "locks" must be an array');const r=[];for(const s of e.elements){if(i.isSpreadElement(s)||i.isOmittedExpression(s))throw c(t,'metadata property "locks" must contain only string literals');r.push(D(s,t,'metadata property "locks" entry'))}return[...new Set(r)].sort()}function P(e,t){const i=a(),r=y(e);if(r!==null)return F(r,t,'metadata property "skip"').trim();if(!i.isObjectLiteralExpression(e))throw c(t,'metadata property "skip" must be a string literal or { reason: string literal }');if(e.properties.length!==1)throw c(t,'metadata property "skip" object must contain exactly one reason property');const s=e.properties[0];if(!i.isPropertyAssignment(s))throw c(t,'metadata property "skip" reason must use a string-literal assignment');if(b(s.name,t,'metadata property "skip"')!=="reason")throw c(t,'metadata property "skip" object only accepts "reason"');return D(s.initializer,t,'metadata property "skip.reason"')}function ee(e,t){const i=k(e,t,'metadata property "runtimeIsolation"');try{return V(i,"defineFile runtimeIsolation")}catch(r){throw c(t,M(r),r)}}function te(e){return a().isIdentifier(e)?e.text:null}function b(e,t,i){const r=a();if(r.isIdentifier(e)||r.isStringLiteral(e))return e.text;throw c(t,`${i} property names must be plain identifiers or string literals`)}function y(e){const t=a();return t.isStringLiteral(e)||t.isNoSubstitutionTemplateLiteral(e)?e.text:null}function k(e,t,i){const r=y(e);if(r===null)throw c(t,`${i} must be a string literal`);return F(r,t,i)}function D(e,t,i){return k(e,t,i).trim()}function F(e,t,i){if(e.trim().length===0)throw c(t,`${i} must not be empty`);return e}function ie(e){const t=a();return(e.modifiers||[]).some(i=>i.kind===t.SyntaxKind.ExportKeyword)}function re(e){const t=a();return(e.declarationList.flags&t.NodeFlags.Const)!==0}function ne(e){return["engine","locks","runtimeIsolation","skip"].includes(e)}function se(e,t,i){const r=a();let s=null,n=0;for(const o of e.statements){if(r.isImportDeclaration(o)){const l=o.importClause;if(!l||l.phaseModifier===r.SyntaxKind.TypeKeyword)continue;l.name?.text==="defineFile"&&(n+=1);const u=l.namedBindings;if(u&&r.isNamespaceImport(u)){u.name.text==="defineFile"&&(n+=1);continue}if(!u||!r.isNamedImports(u))continue;for(const p of u.elements){if(p.isTypeOnly||p.name.text!=="defineFile")continue;if(n+=1,(p.propertyName?.text??p.name.text)==="defineFile"&&r.isStringLiteral(o.moduleSpecifier)&&o.moduleSpecifier.text===I){if(s)throw c(i,`defineFile must have exactly one named value import from "${I}"`);s=o}}continue}n+=Se(o)}if(!s)throw c(i,`defineFile must be a named value import from "${I}"`);if(n!==1)throw c(i,"the imported defineFile binding may not be shadowed or redeclared");if(s.getStart(e)>t.getStart(e))throw c(i,"the defineFile import must precede metadata")}export function requireMatchingEngineAuthoringImports(e,t,i){oe(e,t,i);const r=z(i.profileId);for(const s of collectModuleSpecifiers(e))if(G(t,i,s),!_(r,s))throw U(t,i,s,r)}function oe(e,t,i){const r=a(),s=q(e),n=o=>{if(ae(o,t,i),ce(o,s,t,i),r.isCallExpression(o)&&T(o,s)){const l=o.arguments.at(0);if(!l||!r.isStringLiteralLike(l)){const u=o.expression.kind===r.SyntaxKind.ImportKeyword?"import()":"require()";throw new Error(`Invalid Testkit authoring in "${t}": ${i.engineId}/${i.profileId} may not use non-literal ${u}; module specifiers must be string literals so engine boundaries can be verified`)}}for(const l of C(o))n(l);r.forEachChild(o,n)};n(e)}function T(e,t){const i=a();return e.expression.kind===i.SyntaxKind.ImportKeyword||i.isIdentifier(e.expression)&&e.expression.text==="require"&&!E(e.expression,t)}function ae(e,t,i){const r=a();if(r.isImportDeclaration(e)){const n=e.importClause?.namedBindings;if(n&&r.isNamedImports(n)&&n.elements.find(l=>!l.isTypeOnly&&(l.propertyName?.text??l.name.text)==="createRequire"))throw g(t,i,"createRequire")}if(r.isPropertyAccessExpression(e)&&e.name.text==="createRequire"||r.isElementAccessExpression(e)&&y(e.argumentExpression)==="createRequire"||r.isBindingElement(e)&&(me(e)==="createRequire"||r.isIdentifier(e.name)&&e.name.text==="createRequire"))throw g(t,i,"createRequire")}function ce(e,t,i,r){const s=a();if(s.isPropertyAccessExpression(e)&&e.name.text==="require"&&v(e.expression,t)||s.isElementAccessExpression(e)&&y(e.argumentExpression)==="require"&&v(e.expression,t))throw g(i,r,e.getText());if(s.isIdentifier(e)&&e.text==="require"&&!E(e,t)&&!le(e)&&!ue(e))throw g(i,r,"global require")}function g(e,t,i){return new Error(`Invalid Testkit authoring in "${e}": ${t.engineId}/${t.profileId} may not alias or indirectly access the module loader through ${i}; use literal import and require specifiers so engine boundaries can be verified`)}function v(e,t){return a().isIdentifier(e)&&(e.text==="globalThis"||e.text==="module")&&!E(e,t)}function le(e){return a().isCallExpression(e.parent)&&e.parent.expression===e}function ue(e){const t=a(),i=e.parent;return pe(e)||t.isPropertyAccessExpression(i)&&i.name===e||t.isPropertyAssignment(i)&&i.name===e||t.isImportSpecifier(i)&&i.propertyName===e||t.isExportSpecifier(i)&&i.parent.parent.moduleSpecifier!==void 0}function pe(e){const t=a(),i=e.parent;return i.name===e&&!t.isShorthandPropertyAssignment(i)&&!t.isExportSpecifier(i)}function me(e){const t=a(),i=e.propertyName;return i&&(t.isIdentifier(i)||t.isStringLiteralLike(i))?i.text:null}function q(e){const t=a(),i=new WeakMap,r=(n,o)=>{let l=i.get(n);l||(l=new Set,i.set(n,l)),N(l,o)},s=n=>{if(t.isImportDeclaration(n))fe(n,e,r);else if(t.isImportEqualsDeclaration(n)&&!n.isTypeOnly)r(e,n.name);else if(t.isVariableDeclaration(n)){const o=n.parent,l=t.isVariableDeclarationList(o)&&(o.flags&t.NodeFlags.BlockScoped)===0?de(n):w(n);r(l,n.name)}else t.isParameter(n)?r(ge(n),n.name):t.isFunctionDeclaration(n)&&n.name?r(w(n),n.name):t.isFunctionExpression(n)&&n.name?r(n,n.name):(t.isClassDeclaration(n)||t.isEnumDeclaration(n)||t.isModuleDeclaration(n))&&n.name&&t.isIdentifier(n.name)?r(w(n),n.name):t.isClassExpression(n)&&n.name?r(n,n.name):t.isCatchClause(n)&&n.variableDeclaration&&r(n,n.variableDeclaration.name);t.forEachChild(n,s)};return s(e),i}function fe(e,t,i){const r=a(),s=e.importClause;if(!s||s.phaseModifier===r.SyntaxKind.TypeKeyword)return;s.name&&i(t,s.name);const n=s.namedBindings;if(n&&r.isNamespaceImport(n))i(t,n.name);else if(n&&r.isNamedImports(n))for(const o of n.elements)o.isTypeOnly||i(t,o.name)}function N(e,t){const i=a();if(i.isIdentifier(t)){e.add(t.text);return}for(const r of t.elements)i.isOmittedExpression(r)||N(e,r.name)}function E(e,t){const i=a();let r=e;for(;;){if(t.get(r)?.has(e.text))return!0;if(i.isSourceFile(r))return!1;r=r.parent}}function w(e){let t=e.parent;for(;;){if(ye(t))return t;t=t.parent}}function de(e){const t=a();let i=e.parent;for(;;){if(t.isFunctionLike(i)||t.isSourceFile(i))return i;i=i.parent}}function ge(e){const t=a();let i=e.parent;for(;;){if(t.isFunctionLike(i)||t.isSourceFile(i))return i;i=i.parent}}function ye(e){const t=a();return t.isSourceFile(e)||t.isBlock(e)||t.isCaseBlock(e)||t.isCatchClause(e)||t.isForStatement(e)||t.isForInStatement(e)||t.isForOfStatement(e)||t.isFunctionLike(e)||t.isClassStaticBlockDeclaration(e)}export function collectModuleSpecifiers(e){const t=a(),i=new Set,r=q(e),s=n=>{const o=xe(n,r);o&&i.add(o);for(const l of C(n))s(l);t.forEachChild(n,s)};return s(e),i}function C(e){return e.jsDoc??[]}function xe(e,t){const i=a();if(i.isJSDocImportTag(e)&&i.isStringLiteralLike(e.moduleSpecifier)||(i.isImportDeclaration(e)||i.isExportDeclaration(e))&&e.moduleSpecifier&&i.isStringLiteralLike(e.moduleSpecifier))return e.moduleSpecifier.text;if(i.isImportEqualsDeclaration(e)&&i.isExternalModuleReference(e.moduleReference)&&i.isStringLiteralLike(e.moduleReference.expression))return e.moduleReference.expression.text;if(i.isImportTypeNode(e)){const s=e.argument;if(i.isLiteralTypeNode(s)&&i.isStringLiteralLike(s.literal))return s.literal.text}if(!i.isCallExpression(e)||!T(e,t))return null;const r=e.arguments[0];return i.isStringLiteralLike(r)?r.text:null}function Se(e){const t=a();return t.isVariableStatement(e)?e.declarationList.declarations.reduce((i,r)=>i+R(r.name,"defineFile"),0):(t.isClassDeclaration(e)||t.isEnumDeclaration(e)||t.isFunctionDeclaration(e))&&e.name?.text==="defineFile"?1:0}function R(e,t){const i=a();return i.isIdentifier(e)?e.text===t?1:0:e.elements.reduce((r,s)=>i.isOmittedExpression(s)?r:r+R(s.name,t),0)}function Ie(e,t){const i=S.resolve(e);if(S.isAbsolute(t))throw new Error(`Test file metadata path must be product-relative: ${t}`);const r=S.resolve(i,t);if(!h(i,r))throw new Error(`Test file metadata path escapes the product root: ${t}`);if(!d.existsSync(r))throw new Error(`Test file metadata source does not exist: ${t}`);const s=d.realpathSync(i),n=d.realpathSync(r);if(!h(s,n))throw new Error(`Test file metadata path resolves outside the product root: ${t}`);return n}function ke(e){return e.parseDiagnostics??[]}function c(e,t,i){return new Error(`Invalid testkit metadata in "${e}": ${t}.`,{cause:i})}function M(e){return e instanceof Error?e.message.replace(/\.$/u,""):String(e)}
1
+ import{isWorkObserved as $,recordWork as L}from"../../kernel/work-metrics.js";import{isPathWithin as h}from"../../process/files/paths.js";import g from"fs";import S from"path";import{createRequire as M}from"node:module";import{fileURLToPath as A}from"node:url";import{ENGINE_IDS as B,PUBLIC_TEST_TYPES as O,isEngineId as j}from"../../domain/identifiers.js";import{normalizeRuntimeIsolationPolicy as K}from"../../domain/runtime-isolation-contract.js";import{resolveEngineProfile as V,allowsEngineAuthoringImport as W,expectedEngineAuthoringModule as _,invalidEngineAuthoringImport as z,requireMatchingEngineImport as U}from"../../engines/catalog.js";const G=M(import.meta.url),J=A(import.meta.resolve("typescript"));let Y;function c(){return Y??=G(J)}const I="@gfdelarue/testkit";export function loadTestFileMetadataMap(e,t={},i=new Map){for(const r of Object.values(t||{}).flatMap(s=>s??[]))for(const s of r.files||[])i.has(s)||i.set(s,readTestFileMetadata(e,s));return i}export function readTestFileMetadata(e,t){const i=O.find(m=>t.endsWith(`.${m}.testkit.ts`));if(i===void 0)return{engineId:"node-vitest",locks:[],profileId:"node-vitest",runtimeIsolation:null,skipReason:null};const r=c(),s=Ie(e,t);let n;if($()){const m=g.readFileSync(s);L("metadata.filesRead",1),L("metadata.bytesRead",m.byteLength),n=m.toString("utf8")}else n=g.readFileSync(s,"utf8");const l=r.createSourceFile(s,n,r.ScriptTarget.Latest,!0,r.ScriptKind.TS),o=ke(l);if(o.length>0){const m=o.map(d=>r.flattenDiagnosticMessageText(d.messageText," ")).join("; ");throw a(t,`TypeScript syntax is invalid: ${m}`)}let u=null,p=null;for(const m of l.statements)if(!(!r.isVariableStatement(m)||!te(m))){for(const d of m.declarationList.declarations)if(!(!r.isIdentifier(d.name)||d.name.text!=="testkit")){if(u)throw a(t,'exported "testkit" metadata must be declared exactly once');if(!ie(m))throw a(t,'exported "testkit" metadata must be const');u=d,p=m}}if(!u||!p)throw a(t,'export const testkit = defineFile({ engine: "..." }) is required');ne(l,p,t);const f=H(u.initializer,t,i);return se(l,t,f),f}function H(e,t,i){const r=c();if(!e)throw a(t,'exported "testkit" metadata has no value');if(!r.isCallExpression(e))throw a(t,"testkit metadata must use defineFile({ ... })");if(ee(e.expression)!=="defineFile")throw a(t,"testkit metadata must call defineFile");if(e.arguments.length!==1)throw a(t,"defineFile metadata must receive exactly one object literal");const s=e.arguments[0];if(!r.isObjectLiteralExpression(s))throw a(t,"defineFile metadata must receive an object literal");return Q(s,t,i)}function Q(e,t,i){const r=c(),s={locks:[],runtimeIsolation:null,skipReason:null};let n=null;const l=new Set;for(const u of e.properties){if(r.isSpreadAssignment(u))throw a(t,"testkit metadata may not contain spread properties");if(!r.isPropertyAssignment(u))throw a(t,"testkit metadata properties must use key: value assignments");const p=b(u.name,t,"testkit metadata");if(!re(p))throw a(t,`unknown metadata property "${p}"`);if(l.has(p))throw a(t,`duplicate metadata property "${p}"`);if(l.add(p),p==="engine"){n=k(u.initializer,t,'metadata property "engine"');continue}if(p==="locks"){s.locks=X(u.initializer,t);continue}if(p==="skip"){s.skipReason=Z(u.initializer,t);continue}s.runtimeIsolation=P(u.initializer,t)}if(!n)throw a(t,'a literal "engine" property is required');if(!j(n))throw a(t,`unsupported engine "${n}"; expected one of: ${B.join(", ")}`);let o;try{o=V(i,n)}catch(u){throw a(t,R(u),u)}return{engineId:n,locks:s.locks,profileId:o.id,runtimeIsolation:s.runtimeIsolation,skipReason:s.skipReason}}function X(e,t){const i=c();if(!i.isArrayLiteralExpression(e))throw a(t,'metadata property "locks" must be an array');const r=[];for(const s of e.elements){if(i.isSpreadElement(s)||i.isOmittedExpression(s))throw a(t,'metadata property "locks" must contain only string literals');r.push(D(s,t,'metadata property "locks" entry'))}return[...new Set(r)].sort()}function Z(e,t){const i=c(),r=x(e);if(r!==null)return F(r,t,'metadata property "skip"').trim();if(!i.isObjectLiteralExpression(e))throw a(t,'metadata property "skip" must be a string literal or { reason: string literal }');if(e.properties.length!==1)throw a(t,'metadata property "skip" object must contain exactly one reason property');const s=e.properties[0];if(!i.isPropertyAssignment(s))throw a(t,'metadata property "skip" reason must use a string-literal assignment');if(b(s.name,t,'metadata property "skip"')!=="reason")throw a(t,'metadata property "skip" object only accepts "reason"');return D(s.initializer,t,'metadata property "skip.reason"')}function P(e,t){const i=k(e,t,'metadata property "runtimeIsolation"');try{return K(i,"defineFile runtimeIsolation")}catch(r){throw a(t,R(r),r)}}function ee(e){return c().isIdentifier(e)?e.text:null}function b(e,t,i){const r=c();if(r.isIdentifier(e)||r.isStringLiteral(e))return e.text;throw a(t,`${i} property names must be plain identifiers or string literals`)}function x(e){const t=c();return t.isStringLiteral(e)||t.isNoSubstitutionTemplateLiteral(e)?e.text:null}function k(e,t,i){const r=x(e);if(r===null)throw a(t,`${i} must be a string literal`);return F(r,t,i)}function D(e,t,i){return k(e,t,i).trim()}function F(e,t,i){if(e.trim().length===0)throw a(t,`${i} must not be empty`);return e}function te(e){const t=c();return(e.modifiers||[]).some(i=>i.kind===t.SyntaxKind.ExportKeyword)}function ie(e){const t=c();return(e.declarationList.flags&t.NodeFlags.Const)!==0}function re(e){return["engine","locks","runtimeIsolation","skip"].includes(e)}function ne(e,t,i){const r=c();let s=null,n=0;for(const l of e.statements){if(r.isImportDeclaration(l)){const o=l.importClause;if(!o||o.phaseModifier===r.SyntaxKind.TypeKeyword)continue;o.name?.text==="defineFile"&&(n+=1);const u=o.namedBindings;if(u&&r.isNamespaceImport(u)){u.name.text==="defineFile"&&(n+=1);continue}if(!u||!r.isNamedImports(u))continue;for(const p of u.elements){if(p.isTypeOnly||p.name.text!=="defineFile")continue;if(n+=1,(p.propertyName?.text??p.name.text)==="defineFile"&&r.isStringLiteral(l.moduleSpecifier)&&l.moduleSpecifier.text===I){if(s)throw a(i,`defineFile must have exactly one named value import from "${I}"`);s=l}}continue}n+=Se(l)}if(!s)throw a(i,`defineFile must be a named value import from "${I}"`);if(n!==1)throw a(i,"the imported defineFile binding may not be shadowed or redeclared");if(s.getStart(e)>t.getStart(e))throw a(i,"the defineFile import must precede metadata")}function se(e,t,i){const r=oe(e,t,i),s=_(i.profileId);for(const n of r)if(U(t,i,n),!W(s,n))throw z(t,i,n,s)}function oe(e,t,i){const r=c(),s=new Set,n=fe(e),l=o=>{if(ae(o,t,i),ce(o,n,t,i),r.isCallExpression(o)&&T(o,n)){const p=o.arguments.at(0);if(!p||!r.isStringLiteralLike(p)){const f=o.expression.kind===r.SyntaxKind.ImportKeyword?"import()":"require()";throw new Error(`Invalid Testkit authoring in "${t}": ${i.engineId}/${i.profileId} may not use non-literal ${f}; module specifiers must be string literals so engine boundaries can be verified`)}}const u=xe(o,n);u&&s.add(u);for(const p of ye(o))l(p);r.forEachChild(o,l)};return l(e),s}function T(e,t){const i=c();return e.expression.kind===i.SyntaxKind.ImportKeyword||i.isIdentifier(e.expression)&&e.expression.text==="require"&&!w(e.expression,t)}function ae(e,t,i){const r=c();if(r.isImportDeclaration(e)){const n=e.importClause?.namedBindings;if(n&&r.isNamedImports(n)&&n.elements.find(o=>!o.isTypeOnly&&(o.propertyName?.text??o.name.text)==="createRequire"))throw y(t,i,"createRequire")}if(r.isPropertyAccessExpression(e)&&e.name.text==="createRequire"||r.isElementAccessExpression(e)&&x(e.argumentExpression)==="createRequire"||r.isBindingElement(e)&&(me(e)==="createRequire"||r.isIdentifier(e.name)&&e.name.text==="createRequire"))throw y(t,i,"createRequire")}function ce(e,t,i,r){const s=c();if(s.isPropertyAccessExpression(e)&&e.name.text==="require"&&v(e.expression,t)||s.isElementAccessExpression(e)&&x(e.argumentExpression)==="require"&&v(e.expression,t))throw y(i,r,e.getText());if(s.isIdentifier(e)&&e.text==="require"&&!w(e,t)&&!le(e)&&!ue(e))throw y(i,r,"global require")}function y(e,t,i){return new Error(`Invalid Testkit authoring in "${e}": ${t.engineId}/${t.profileId} may not alias or indirectly access the module loader through ${i}; use literal import and require specifiers so engine boundaries can be verified`)}function v(e,t){return c().isIdentifier(e)&&(e.text==="globalThis"||e.text==="module")&&!w(e,t)}function le(e){return c().isCallExpression(e.parent)&&e.parent.expression===e}function ue(e){const t=c(),i=e.parent;return pe(e)||t.isPropertyAccessExpression(i)&&i.name===e||t.isPropertyAssignment(i)&&i.name===e||t.isImportSpecifier(i)&&i.propertyName===e||t.isExportSpecifier(i)&&i.parent.parent.moduleSpecifier!==void 0}function pe(e){const t=c(),i=e.parent;return i.name===e&&!t.isShorthandPropertyAssignment(i)&&!t.isExportSpecifier(i)}function me(e){const t=c(),i=e.propertyName;return i&&(t.isIdentifier(i)||t.isStringLiteralLike(i))?i.text:null}function fe(e){const t=c(),i=new WeakMap,r=(n,l)=>{let o=i.get(n);o||(o=new Set,i.set(n,o)),q(o,l)},s=n=>{if(t.isImportDeclaration(n))de(n,e,r);else if(t.isImportEqualsDeclaration(n)&&!n.isTypeOnly)r(e,n.name);else if(t.isVariableDeclaration(n)){const l=n.parent,o=t.isVariableDeclarationList(l)&&(l.flags&t.NodeFlags.BlockScoped)===0?N(n):E(n);r(o,n.name)}else t.isParameter(n)?r(N(n),n.name):t.isFunctionDeclaration(n)&&n.name?r(E(n),n.name):t.isFunctionExpression(n)&&n.name?r(n,n.name):(t.isClassDeclaration(n)||t.isEnumDeclaration(n)||t.isModuleDeclaration(n))&&n.name&&t.isIdentifier(n.name)?r(E(n),n.name):t.isClassExpression(n)&&n.name?r(n,n.name):t.isCatchClause(n)&&n.variableDeclaration&&r(n,n.variableDeclaration.name);t.forEachChild(n,s)};return s(e),i}function de(e,t,i){const r=c(),s=e.importClause;if(!s||s.phaseModifier===r.SyntaxKind.TypeKeyword)return;s.name&&i(t,s.name);const n=s.namedBindings;if(n&&r.isNamespaceImport(n))i(t,n.name);else if(n&&r.isNamedImports(n))for(const l of n.elements)l.isTypeOnly||i(t,l.name)}function q(e,t){const i=c();if(i.isIdentifier(t)){e.add(t.text);return}for(const r of t.elements)i.isOmittedExpression(r)||q(e,r.name)}function w(e,t){const i=c();let r=e;for(;;){if(t.get(r)?.has(e.text))return!0;if(i.isSourceFile(r))return!1;r=r.parent}}function E(e){let t=e.parent;for(;;){if(ge(t))return t;t=t.parent}}function N(e){const t=c();let i=e.parent;for(;;){if(t.isFunctionLike(i)||t.isSourceFile(i))return i;i=i.parent}}function ge(e){const t=c();return t.isSourceFile(e)||t.isBlock(e)||t.isCaseBlock(e)||t.isCatchClause(e)||t.isForStatement(e)||t.isForInStatement(e)||t.isForOfStatement(e)||t.isFunctionLike(e)||t.isClassStaticBlockDeclaration(e)}function ye(e){return e.jsDoc??[]}function xe(e,t){const i=c();if(i.isJSDocImportTag(e)&&i.isStringLiteralLike(e.moduleSpecifier)||(i.isImportDeclaration(e)||i.isExportDeclaration(e))&&e.moduleSpecifier&&i.isStringLiteralLike(e.moduleSpecifier))return e.moduleSpecifier.text;if(i.isImportEqualsDeclaration(e)&&i.isExternalModuleReference(e.moduleReference)&&i.isStringLiteralLike(e.moduleReference.expression))return e.moduleReference.expression.text;if(i.isImportTypeNode(e)){const s=e.argument;if(i.isLiteralTypeNode(s)&&i.isStringLiteralLike(s.literal))return s.literal.text}if(!i.isCallExpression(e)||!T(e,t))return null;const r=e.arguments[0];return i.isStringLiteralLike(r)?r.text:null}function Se(e){const t=c();return t.isVariableStatement(e)?e.declarationList.declarations.reduce((i,r)=>i+C(r.name,"defineFile"),0):(t.isClassDeclaration(e)||t.isEnumDeclaration(e)||t.isFunctionDeclaration(e))&&e.name?.text==="defineFile"?1:0}function C(e,t){const i=c();return i.isIdentifier(e)?e.text===t?1:0:e.elements.reduce((r,s)=>i.isOmittedExpression(s)?r:r+C(s.name,t),0)}function Ie(e,t){const i=S.resolve(e);if(S.isAbsolute(t))throw new Error(`Test file metadata path must be product-relative: ${t}`);const r=S.resolve(i,t);if(!h(i,r))throw new Error(`Test file metadata path escapes the product root: ${t}`);if(!g.existsSync(r))throw new Error(`Test file metadata source does not exist: ${t}`);const s=g.realpathSync(i),n=g.realpathSync(r);if(!h(s,n))throw new Error(`Test file metadata path resolves outside the product root: ${t}`);return n}function ke(e){return e.parseDiagnostics??[]}function a(e,t,i){return new Error(`Invalid testkit metadata in "${e}": ${t}.`,{cause:i})}function R(e){return e instanceof Error?e.message.replace(/\.$/u,""):String(e)}
@@ -1,2 +1,2 @@
1
- import f from"node:path";import{migrationText as P}from"../../domain/retired-test-types.js";import{requireServiceName as b}from"../../domain/service-name.js";import{errorMessage as x}from"../../kernel/error-serialization.js";import{unknownKeysMessage as A}from"../../kernel/known-keys.js";import{isUnknownRecord as h,readDynamicProperty as a}from"../../kernel/unknown-values.js";import{DATABASE_FIXTURE_DISCOVERY as D}from"../authoring/database-fixture-discovery.js";import{normalizeDiscoveryConfig as k}from"./decode-discovery.js";import{DEFAULT_DISCOVERY_EXCLUDES as z,shouldExcludeDiscoveryPath as m}from"./path-policy.js";const E=String,R=Object.keys({exclude:!0,requirements:!0,roots:!0,runtimeIsolation:!0,skip:!0,uses:!0});export const ROOT_GROUP="root";export function resolveGroups(e,r){const t=[],s=g(a(e,"discovery"),t),i=T(e,t),u=(s.exclude??[]).map(d),n=[...z,...u],o=e!=null&&Object.hasOwn(e,"groups"),p=o?G(a(e,"groups"),i,t):{groups:[],rules:[]};return o&&t.push(...C(i,p.groups)),{assignments:r.map(l=>v(l,p.rules,l.kind==="typed"?n:u)),groups:[...p.groups,{name:ROOT_GROUP,roots:["."],source:"implicit",uses:[]}],migration:o?null:I(i,r,n),problems:t}}export function readDiscoveryScan(e){return g(a(e,"discovery"),[])}export function repoDiscoveryProblem(e){const r=[];return g(a(e,"discovery"),r),r[0]??null}function v(e,r,t){const s=F(e.path);if(m(s,t))return{path:s,status:"excluded"};const i=r.filter(c=>c.prefix===null||O(s,c.prefix)),u=i.filter(c=>!m(s,c.exclude));if(i.length>0&&u.length===0)return{path:s,status:"excluded"};if(u.length===0)return{group:ROOT_GROUP,path:s,relativePath:s,status:"owned"};const n=Math.max(...u.map(c=>c.depth)),o=u.filter(c=>c.depth===n),p=[...new Set(o.map(c=>c.group))].sort((c,j)=>c.localeCompare(j));if(p.length>1)return{groups:p,path:s,status:"ambiguous"};const l=o[0].prefix;return{group:p[0],path:s,relativePath:l===null?s:f.posix.relative(l,s),status:"owned"}}function G(e,r,t){const s={groups:[],rules:[]};if(!h(e))return t.push("testkit.config.ts groups must be an object"),s;for(const[i,u]of Object.entries(e)){const n=`groups.${i}`;if(i===ROOT_GROUP){t.push(`${n} is reserved for the implicit group that owns files no other group claims; rename it`);continue}try{b(i,`Group name "${i}"`)}catch(c){t.push(x(c));continue}if(!h(u)){t.push(`${n} must be an object with roots`);continue}const o=A(u,R,n);o!==null&&t.push(o);const p=w(u.roots,`${n}.roots`,!0,t),l=w(u.exclude,`${n}.exclude`,!1,t);s.groups.push({name:i,roots:p,source:"groups",uses:U(u.uses,n,r,t)});for(const c of p)s.rules.push({depth:c==="."?0:c.split("/").length,exclude:l,group:i,prefix:c==="."?null:c})}return s}const _=["skip","requirements","runtimeIsolation"];function C(e,r){return Object.entries(e).flatMap(([t,s])=>{const i=r.find(o=>o.uses.includes(t))?.name??t,u=_.filter(o=>a(s,o)!==void 0).map(o=>`services.${t}.${o} applies to tests, and services own no tests when groups is set; move it to groups.${i}.${o}`),n=a(s,"discovery");return n!==void 0&&!L(n)&&u.push(`services.${t}.discovery applies to tests, and services own no tests when groups is set; move its roots and exclude to groups.${i}.roots and groups.${i}.exclude, as paths from the product directory`),u})}function L(e){return h(e)&&JSON.stringify(e)===JSON.stringify(D)}function w(e,r,t,s){if(e===void 0&&!t)return[];if(!Array.isArray(e)||t&&e.length===0)return s.push(`${r} must be ${t?"a non-empty array":"an array"} of repo-relative paths`),[];const i=e,u=new Set;for(const n of i){const o=S(n);o===null?s.push(`${r} entry ${JSON.stringify(n)} must be a relative path inside the product directory`):u.add(o)}return[...u]}function U(e,r,t,s){if(e===void 0)return[];if(!J(e))return s.push(`${r}.uses must be an array of service names`),[];const i=[];for(const u of new Set(e))Object.hasOwn(t,u)?i.push(u):s.push(`${r}.uses names "${u}", which is not a service; declare services.${u} or remove it from ${r}.uses`);return i}function I(e,r,t){const s=Object.entries(e).flatMap(([n,o])=>M(n,o)),i=new Set;for(const n of r){if(n.kind!=="typed")continue;const o=v(n,s,t);if(o.status==="ambiguous")for(const p of o.groups)i.add(p);if(o.status==="owned"&&o.group!==ROOT_GROUP&&i.add(o.group),o.status==="excluded"&&!m(o.path,t))for(const p of s)(p.prefix===null||O(o.path,p.prefix))&&i.add(p.group)}return i.size===0?null:["testkit.config.ts has services and no groups. Testkit 0.6.0 removed legacy service ownership, so services no longer own the test files under their directories. Add these groups to keep each typed test file with the service that owned it:","","groups: {",...Object.keys(e).filter(n=>i.has(n)).map(n=>{const o=s.filter(c=>c.group===n),p=o[0].exclude.filter($),l=[`roots: ${y([...new Set(o.map(c=>c.prefix??"."))].filter($))}`,`uses: ${y([n])}`,...p.length>0?[`exclude: ${y(p)}`]:[]];return` ${/^[A-Za-z_$][\w$]*$/u.test(n)?n:JSON.stringify(n)}: { ${l.join(", ")} },`}),"},","","A group also owns the plain Vitest files under its roots.",`${P("Remove legacy service ownership")}.`].join(`
2
- `)}function M(e,r){if(e===ROOT_GROUP)return[];const t=g(a(r,"discovery"),null);if(t===null)return[];const s=N(r),i=(t.exclude??[]).map(u=>d(s==="."?u:f.posix.join(s,u)));return(t.roots&&t.roots.length>0?t.roots:["."]).map(u=>{const n=d(u),o=s==="."?n:n==="."?s:d(f.posix.join(s,n));return{depth:o==="."?0:o.split("/").filter(Boolean).length,exclude:i,group:e,prefix:o==="."?null:o}})}function $(e){return S(e)!==null}function y(e){return`[${e.map(r=>JSON.stringify(r)).join(", ")}]`}function N(e){return d(E(a(a(e,"local"),"cwd")||"."))}function T(e,r){const t=a(e,"services");return t==null?{}:h(t)?t:(r.push("testkit.config.ts services must be an object"),{})}function g(e,r){try{return k(e,{allowRoots:!0})}catch(t){return r===null?null:(r.push(x(t)),{})}}function J(e){return Array.isArray(e)&&e.every(r=>typeof r=="string")}function O(e,r){return e===r||e.startsWith(`${r}/`)}function S(e){if(typeof e!="string")return null;const r=e.trim(),t=r.replaceAll("\\","/");if(t===""||f.posix.isAbsolute(t)||f.win32.isAbsolute(r))return null;const s=f.posix.normalize(t).replace(/\/+$/u,"")||".";return s===".."||s.startsWith("../")?null:s}function F(e){return f.posix.normalize(e.split(f.sep).join("/")).replace(/\/+$/u,"")}function d(e){const r=e.split(f.sep).join("/");return r==="."||r==="./"?".":r.replace(/^\.\/+/u,"").replace(/\/+$/u,"")||"."}
1
+ import f from"node:path";import{migrationText as j}from"../../domain/retired-test-types.js";import{requireServiceName as b}from"../../domain/service-name.js";import{errorMessage as x}from"../../kernel/error-serialization.js";import{unknownKeysMessage as A}from"../../kernel/known-keys.js";import{isUnknownRecord as h,readDynamicProperty as a}from"../../kernel/unknown-values.js";import{DATABASE_FIXTURE_DISCOVERY as D}from"../authoring/database-fixture-discovery.js";import{normalizeDiscoveryConfig as k}from"./decode-discovery.js";import{DEFAULT_DISCOVERY_EXCLUDES as E,normalizeRelativePath as d,shouldExcludeDiscoveryPath as m}from"./path-policy.js";const R=String,z=Object.keys({exclude:!0,requirements:!0,roots:!0,runtimeIsolation:!0,skip:!0,uses:!0});export const ROOT_GROUP="root";export function resolveGroups(e,s){const t=[],r=g(a(e,"discovery"),t),i=T(e,t),u=(r.exclude??[]).map(d),n=[...E,...u],o=e!=null&&Object.hasOwn(e,"groups"),p=o?G(a(e,"groups"),i,t):{groups:[],rules:[]};return o&&t.push(...C(i,p.groups)),{assignments:s.map(l=>v(l,p.rules,l.kind==="typed"?n:u)),groups:[...p.groups,{name:ROOT_GROUP,roots:["."],source:"implicit",uses:[]}],migration:o?null:L(i,s,n),problems:t}}export function readDiscoveryScan(e){return g(a(e,"discovery"),[])}export function repoDiscoveryProblem(e){const s=[];return g(a(e,"discovery"),s),s[0]??null}function v(e,s,t){const r=F(e.path);if(m(r,t))return{path:r,status:"excluded"};const i=s.filter(c=>c.prefix===null||O(r,c.prefix)),u=i.filter(c=>!m(r,c.exclude));if(i.length>0&&u.length===0)return{path:r,status:"excluded"};if(u.length===0)return{group:ROOT_GROUP,path:r,relativePath:r,status:"owned"};const n=Math.max(...u.map(c=>c.depth)),o=u.filter(c=>c.depth===n),p=[...new Set(o.map(c=>c.group))].sort((c,P)=>c.localeCompare(P));if(p.length>1)return{groups:p,path:r,status:"ambiguous"};const l=o[0].prefix;return{group:p[0],path:r,relativePath:l===null?r:f.posix.relative(l,r),status:"owned"}}function G(e,s,t){const r={groups:[],rules:[]};if(!h(e))return t.push("testkit.config.ts groups must be an object"),r;for(const[i,u]of Object.entries(e)){const n=`groups.${i}`;if(i===ROOT_GROUP){t.push(`${n} is reserved for the implicit group that owns files no other group claims; rename it`);continue}try{b(i,`Group name "${i}"`)}catch(c){t.push(x(c));continue}if(!h(u)){t.push(`${n} must be an object with roots`);continue}const o=A(u,z,n);o!==null&&t.push(o);const p=w(u.roots,`${n}.roots`,!0,t),l=w(u.exclude,`${n}.exclude`,!1,t);r.groups.push({name:i,roots:p,source:"groups",uses:I(u.uses,n,s,t)});for(const c of p)r.rules.push({depth:c==="."?0:c.split("/").length,exclude:l,group:i,prefix:c==="."?null:c})}return r}const _=["skip","requirements","runtimeIsolation"];function C(e,s){return Object.entries(e).flatMap(([t,r])=>{const i=s.find(o=>o.uses.includes(t))?.name??t,u=_.filter(o=>a(r,o)!==void 0).map(o=>`services.${t}.${o} applies to tests, and services own no tests when groups is set; move it to groups.${i}.${o}`),n=a(r,"discovery");return n!==void 0&&!U(n)&&u.push(`services.${t}.discovery applies to tests, and services own no tests when groups is set; move its roots and exclude to groups.${i}.roots and groups.${i}.exclude, as paths from the product directory`),u})}function U(e){return h(e)&&JSON.stringify(e)===JSON.stringify(D)}function w(e,s,t,r){if(e===void 0&&!t)return[];if(!Array.isArray(e)||t&&e.length===0)return r.push(`${s} must be ${t?"a non-empty array":"an array"} of repo-relative paths`),[];const i=e,u=new Set;for(const n of i){const o=S(n);o===null?r.push(`${s} entry ${JSON.stringify(n)} must be a relative path inside the product directory`):u.add(o)}return[...u]}function I(e,s,t,r){if(e===void 0)return[];if(!J(e))return r.push(`${s}.uses must be an array of service names`),[];const i=[];for(const u of new Set(e))Object.hasOwn(t,u)?i.push(u):r.push(`${s}.uses names "${u}", which is not a service; declare services.${u} or remove it from ${s}.uses`);return i}function L(e,s,t){const r=Object.entries(e).flatMap(([n,o])=>M(n,o)),i=new Set;for(const n of s){if(n.kind!=="typed")continue;const o=v(n,r,t);if(o.status==="ambiguous")for(const p of o.groups)i.add(p);if(o.status==="owned"&&o.group!==ROOT_GROUP&&i.add(o.group),o.status==="excluded"&&!m(o.path,t))for(const p of r)(p.prefix===null||O(o.path,p.prefix))&&i.add(p.group)}return i.size===0?null:["testkit.config.ts has services and no groups. Testkit 0.6.0 removed legacy service ownership, so services no longer own the test files under their directories. Add these groups to keep each typed test file with the service that owned it:","","groups: {",...Object.keys(e).filter(n=>i.has(n)).map(n=>{const o=r.filter(c=>c.group===n),p=o[0].exclude.filter($),l=[`roots: ${y([...new Set(o.map(c=>c.prefix??"."))].filter($))}`,`uses: ${y([n])}`,...p.length>0?[`exclude: ${y(p)}`]:[]];return` ${/^[A-Za-z_$][\w$]*$/u.test(n)?n:JSON.stringify(n)}: { ${l.join(", ")} },`}),"},","","A group also owns the plain Vitest files under its roots.",`${j("Remove legacy service ownership")}.`].join(`
2
+ `)}function M(e,s){if(e===ROOT_GROUP)return[];const t=g(a(s,"discovery"),null);if(t===null)return[];const r=N(s),i=(t.exclude??[]).map(u=>d(r==="."?u:f.posix.join(r,u)));return(t.roots&&t.roots.length>0?t.roots:["."]).map(u=>{const n=d(u),o=r==="."?n:n==="."?r:d(f.posix.join(r,n));return{depth:o==="."?0:o.split("/").filter(Boolean).length,exclude:i,group:e,prefix:o==="."?null:o}})}function $(e){return S(e)!==null}function y(e){return`[${e.map(s=>JSON.stringify(s)).join(", ")}]`}function N(e){return d(R(a(a(e,"local"),"cwd")||"."))}function T(e,s){const t=a(e,"services");return t==null?{}:h(t)?t:(s.push("testkit.config.ts services must be an object"),{})}function g(e,s){try{return k(e)}catch(t){return s===null?null:(s.push(x(t)),{})}}function J(e){return Array.isArray(e)&&e.every(s=>typeof s=="string")}function O(e,s){return e===s||e.startsWith(`${s}/`)}function S(e){if(typeof e!="string")return null;const s=e.trim(),t=s.replaceAll("\\","/");if(t===""||f.posix.isAbsolute(t)||f.win32.isAbsolute(s))return null;const r=f.posix.normalize(t).replace(/\/+$/u,"")||".";return r===".."||r.startsWith("../")?null:r}function F(e){return f.posix.normalize(e.split(f.sep).join("/")).replace(/\/+$/u,"")}
@@ -1 +1 @@
1
- export{loadConfigContext,loadConfigs,parseDotenv,resolveProductDir,resolveServiceCwd}from"./load.js";export{discoverTests,loadDiscoveredConfigContext,loadDiscoveryContext}from"./discover-tests.js";export{fileDisplayName,formatDisplayName,testTypeContract}from"./discovery-result.js";export{findConfigFile,loadTestkitConfig}from"./config-loader.js";export{materializeDatabaseConfig}from"./database-materialization.js";export{detectNextApp}from"./runtime.js";export{DEFAULT_RUNTIME_ISOLATION_POLICY}from"./runtime-isolation.js";export{collectModuleSpecifiers,requireMatchingEngineAuthoringImports}from"./file-metadata.js";export{hasDiscoveryErrors,buildDiscoveryErrorFromDiagnostics}from"./discovery.js";
1
+ export{loadConfigContext,loadConfigs,resolveProductDir,resolveServiceCwd}from"./load.js";export{discoverTests,loadDiscoveredConfigContext,loadDiscoveryContext}from"./discover-tests.js";export{fileDisplayName,formatDisplayName,testTypeContract}from"./discovery-result.js";export{loadTestkitConfig}from"./config-loader.js";export{materializeDatabaseConfig}from"./database-materialization.js";export{detectNextApp}from"./runtime.js";export{hasDiscoveryErrors,buildDiscoveryErrorFromDiagnostics}from"./discovery.js";
@@ -1 +1 @@
1
- import{selectDiscoveredSuites as D,selectsOnlyTypedFiles as A}from"./test-selection.js";import L from"path";import{isUnknownRecord as w,readDynamicProperty as i}from"../../kernel/unknown-values.js";import{suiteKey as C}from"../../domain/suite-selection.js";import{repoDiscoveryProblem as B}from"./groups.js";import{discoverOwnedTests as q}from"./owned-discovery.js";import{loadTestkitConfig as G}from"./config-loader.js";import{normalizeToolchainRegistry as K}from"../../toolchains/index.js";import{loadTestFileMetadataMap as _}from"./file-metadata.js";import{normalizeFingerprintPolicy as V}from"../../repo/fingerprint-policy.js";import{normalizeDatabaseConfig as H}from"./database.js";import{inferEnvFiles as J,parseDotenv as Q}from"./env.js";import{normalizeRuntimeIsolationConfig as W}from"./runtime-isolation.js";import{resolveProductDir as T,resolveServiceCwd as X}from"./paths.js";import{normalizeRepoExecution as Y,normalizeRuntimeConfig as Z}from"./runtime.js";import{normalizeServiceRequirements as N,normalizeSkipConfig as ee}from"./skip-config.js";import{normalizeArtifactArchiveConfig as re}from"../../evidence/index.js";import{validateConfigCoverage as te,validateServiceConfig as oe}from"./validation.js";import{assertPortableServiceNameSet as ne,requireServiceName as ie}from"../../domain/service-name.js";import{resolveExecutionProfile as se}from"../../engines/catalog.js";import{optionalStringValue as y}from"../../evidence/read-fields.js";const U=["65533","65534"],ae=/\{(?:baseUrl|readyUrl|publicBaseUrl|privateBaseUrl|serviceContextUrl)(?::[^}]+)?\}/gu;export{Q as parseDotenv,T as resolveProductDir,X as resolveServiceCwd};export async function loadConfigContext(e={}){const r=T(process.cwd(),e.dir),t=e.configContext||await G(r,{importCacheSalt:e.configImportCacheSalt??""}),{config:o,configFile:n}=be(t),s=Y(Oe(i(o,"execution"))),c=re(i(o,"artifacts")),m=K(i(o,"toolchains")),a=V(i(o,"fingerprints")),p=ye(i(o,"redact"));O(B(o));const l=he(i(o,"services"));e.preparedDiscovery||we(l);const d=e.plain??!A(e.testSelection??{}),u=e.preparedDiscovery?.owned??await q(r,o,{...e.discoveryOptions,plain:d,retired:e.retired,selection:e.vitest}),f=u.project,h=e.testSelection?D(f.suitesByService,e.testSelection):f.suitesByService,b=_(r,h,e.preparedDiscovery?.fileMetadataByPath),F=ce(h,b),S=[];for(const g of Object.keys(l).sort(($,E)=>$.localeCompare(E)))S.push(le({name:g,productDir:r,config:o,configFile:n,execution:s,artifacts:c,toolchains:m,fingerprints:a,explicitService:l[g]??{}}));O(u.problems[0]??null),e.allowUnmigratedServices||O(u.migration),te(S);const j=[];for(const g of[...u.groups].sort(($,E)=>$.name.localeCompare(E.name)))j.push(ue({declaration:g,productDir:r,configFile:n,execution:s,artifacts:c,policy:de(o,g),redact:p,suites:F[g.name]||[],discoveredSuites:f.suitesByService[g.name]||[],fileMetadataByPath:b,plainListed:d}));return{productDir:r,config:o,configFile:n,execution:s,toolchains:m,fingerprints:a,explicitServices:l,discovery:f,groups:j,plainFiles:u.plainFiles,services:S}}function ce(e,r){return Object.fromEntries(Object.entries(e).map(([t,o])=>[t,(o??[]).map(n=>fe(t,n,r))]))}function fe(e,r,t){const o=r.files[0];if(!o)throw new Error(`Discovered suite ${e}/${C(r)} contains no files`);const n=k(t,o);se({...r,...n});for(const s of r.files.slice(1)){const c=k(t,s);if(c.engineId!==n.engineId||c.profileId!==n.profileId)throw new Error(`Discovered suite ${e}/${C(r)} mixes engine profiles ${n.engineId}/${n.profileId} and ${c.engineId}/${c.profileId}`)}return{...r,engineId:n.engineId,profileId:n.profileId}}function k(e,r){const t=e.get(r);if(!t)throw new Error(`Missing validated testkit metadata for discovered file: ${r}`);return t}export async function loadConfigs(e={}){const{services:r}=await loadConfigContext(e),t=e.service?r.filter(o=>o.name===e.service):r;if(e.service&&t.length===0){const o=r.map(n=>n.name).join(", ");throw new Error(`Service "${e.service}" not found. Available: ${o}`)}return t}function le({name:e,productDir:r,config:t,configFile:o,execution:n,artifacts:s,toolchains:c,fingerprints:m,explicitService:a}){const p=Object.hasOwn(a,"local")?z(i(a,"local"),e):void 0,l=J(r,a,p),d=M(i(a,"env"),`Service "${e}" env`);if(i(a,"migrate")||i(a,"seed"))throw new Error(`Service "${e}" uses removed migrate/seed hooks. Move template lifecycle to database.template.{migrate,seed,verify}.`);const u=ge(a,e),f=Z(i(a,"runtime"),e,c),h=Se(i(a,"dependsOn"),`Service "${e}" dependsOn`),b=y(i(a,"databaseFrom"),`Service "${e}" databaseFrom`);return oe({name:e,local:p,database:u,databaseFrom:b,runtime:f,dependsOn:h,productDir:r}),{name:e,productDir:r,configFile:o,stateDir:L.join(r,".testkit",e),artifacts:s,testkit:{execution:n,dependsOn:h,database:u,databaseFrom:b,envFiles:l,serviceEnv:d,runtime:f,ui:i(t,"ui")||null,fingerprints:m,local:p}}}function ue({declaration:e,productDir:r,configFile:t,execution:o,artifacts:n,policy:s,redact:c,suites:m,discoveredSuites:a,fileMetadataByPath:p,plainListed:l}){const d=`Group "${e.name}"`,u=new Set(m.flatMap(f=>f.files));return{...e,roots:[...e.roots],uses:[...e.uses],artifacts:n,configFile:t,productDir:r,redact:c,suites:m,testkit:{execution:o,fileMetadataByPath:new Map([...p.entries()].filter(([f])=>u.has(f))),requirements:N(i(s,"requirements"),{owner:d,plainListed:l,suites:a}),runtimeIsolation:W(i(s,"runtimeIsolation"),{owner:d,plainListed:l,suites:a}),skip:ee(i(s,"skip"),{owner:d,plainListed:l,suites:a})}}}function de(e,r){if(r.source!=="implicit")return i(i(e,"groups"),r.name)}function z(e,r){if(e===!1)return;if(!w(e))throw new Error(`Service "${r}" local must be an object or false`);if(Object.prototype.hasOwnProperty.call(e,"port"))throw new Error(`Service "${r}" sets local.port. Testkit assigns every service a free port from the OS. Delete the line, or the port option of app.node or app.next, and use {port} in start, baseUrl, and readyUrl.`);const t=R(e.start,`Service "${r}" local.start`),{baseUrl:o,shutdownTimeoutMs:n,...s}=e;return{...s,...Ee(n,r),...me(o,r),start:t,cwd:y(e.cwd,`Service "${r}" local.cwd`)||".",env:M(e.env,`Service "${r}" local.env`),...$e(e.readyTimeoutMs,`Service "${r}" local.readyTimeoutMs`),...ve(e.readyUrl,r)}}function me(e,r){if(e==null)return{};const t=R(e,`Service "${r}" local.baseUrl`);x(t,`Service "${r}" local.baseUrl`);const o=new RegExp(`\\{port(?::${pe(r)})?\\}`,"gu");if(!U.every(n=>I(t,o,n,!1)===n))throw new Error(`Service "${r}" local.baseUrl must use {port} for its port, for example http://127.0.0.1:{port}. Testkit assigns every service a free port from the OS.`);return{baseUrl:t}}function v(e){return e.replace(/[\t\n\r]/gu,"").trim()}function P(e){return/^[a-z][a-z0-9+.-]*:[\\/]{2}(?![\\/])([^/\\?#]*)/iu.exec(v(e))?.[1]}function I(e,r,t,o){const n=v(e).replace(r,t).replace(ae,o?`http://template.invalid:${t}`:"http://template.invalid").replace(/\{[^}]+\}/gu,"template");try{return new URL(n).port}catch{return}}function pe(e){return e.replace(/[.*+?^${}()|[\]\\]/gu,"\\$&")}function x(e,r){if(/^[a-z][a-z0-9+.-]*:/iu.test(v(e))&&P(e)===void 0)throw new Error(`${r} must be an absolute URL with a host, for example http://127.0.0.1:{port}/health.`);const[t,o]=U.map(s=>I(e,/\{port(?::[^}]+)?\}/gu,s,!0)),n=/:(\d+)$/u.exec(P(e)??"")?.[1]??(t&&t===o?t:void 0);if(n!==void 0)throw new Error(`${r} has a literal port (${n}). Testkit assigns every service a free port from the OS. Replace the port with {port}.`)}function ge(e,r){const t=H(e,r);if(!t)return;const o=y(i(t,"image"),`Service "${r}" database.image`),n=y(i(t,"password"),`Service "${r}" database.password`),s=y(i(t,"user"),`Service "${r}" database.user`);return{...t,...Object.prototype.hasOwnProperty.call(t,"image")?{image:o}:{},...Object.prototype.hasOwnProperty.call(t,"password")?{password:n}:{},...Object.prototype.hasOwnProperty.call(t,"user")?{user:s}:{}}}function we(e){for(const[r,t]of Object.entries(e))t&&Object.hasOwn(t,"local")&&z(i(t,"local"),r)}function O(e){if(e!==null)throw new Error(e)}function he(e){if(e==null)return{};if(!w(e))throw new Error("testkit.config.ts services must be an object");const r=Object.fromEntries(Object.entries(e).map(([t,o])=>{if(ie(t,`Service name "${t}"`),o==null)return[t,{}];if(!w(o))throw new Error(`Service "${t}" config must be an object`);return[t,o]}));return ne(Object.keys(r)),r}function be(e){if(!w(e))throw new Error("Loaded Testkit config context must be an object");const r=e.config;if(typeof r!="object"||r===null)throw new Error("Loaded Testkit config must be an object");const t=e.configFile;if(t!==null&&typeof t!="string")throw new Error("Loaded Testkit config file must be a string or null");return{config:r,configFile:t}}function ye(e){if(e==null)return[];if(!Array.isArray(e))throw new Error("testkit.config.ts redact must be an array of env key names");return e.map((r,t)=>{if(typeof r!="string")throw new Error(`testkit.config.ts redact[${String(t)}] must be a string`);if(r.trim().length===0)throw new Error(`testkit.config.ts redact[${String(t)}] must not be empty`);return r.trim()})}function Se(e,r){if(e==null)return[];if(!Array.isArray(e))throw new Error(`${r} must be an array of strings`);return e.map((t,o)=>{if(typeof t!="string")throw new Error(`${r}[${String(o)}] must be a string`);return t})}function M(e,r){if(e==null)return{};if(!w(e))throw new Error(`${r} must be an object of strings`);return Object.fromEntries(Object.entries(e).map(([t,o])=>{if(typeof o!="string")throw new Error(`${r}.${t} must be a string`);return[t,o]}))}function $e(e,r){if(e===void 0)return{};if(typeof e!="number")throw new Error(`${r} must be a number`);return{readyTimeoutMs:e}}function Ee(e,r){if(e===void 0)return{};if(typeof e!="number"||!Number.isInteger(e)||e<=0)throw new Error(`Service "${r}" local.shutdownTimeoutMs must be a positive finite integer in milliseconds`);return{shutdownTimeoutMs:e}}function ve(e,r){if(e===void 0)return{};if(typeof e=="string")return x(e,`Service "${r}" local.readyUrl`),{readyUrl:e};if(e===!1||e===null)return{readyUrl:e};throw new Error(`Service "${r}" local.readyUrl must be a string, false, or null`)}function R(e,r){if(typeof e!="string")throw new Error(`${r} must be a string`);return e}function Oe(e){if(e){if(!w(e))throw new Error("testkit.config.ts execution must be an object");return e}}
1
+ import{selectDiscoveredSuites as F,selectsOnlyTypedFiles as D}from"./test-selection.js";import A from"path";import{isUnknownRecord as h,readDynamicProperty as i}from"../../kernel/unknown-values.js";import{suiteKey as z}from"../../domain/suite-selection.js";import{repoDiscoveryProblem as B}from"./groups.js";import{discoverOwnedTests as q}from"./owned-discovery.js";import{loadTestkitConfig as L}from"./config-loader.js";import{normalizeToolchainRegistry as G}from"../../toolchains/index.js";import{loadTestFileMetadataMap as K}from"./file-metadata.js";import{normalizeFingerprintPolicy as _}from"../../repo/fingerprint-policy.js";import{normalizeDatabaseConfig as V}from"./database.js";import{inferEnvFiles as H}from"./env.js";import{normalizeRuntimeIsolationConfig as J}from"./runtime-isolation.js";import{resolveProductDir as C,resolveServiceCwd as Q}from"./paths.js";import{normalizeRepoExecution as W,normalizeRuntimeConfig as X}from"./runtime.js";import{normalizeServiceRequirements as Y,normalizeSkipConfig as Z}from"./skip-config.js";import{normalizeArtifactArchiveConfig as N}from"../../evidence/index.js";import{validateConfigCoverage as rr,validateServiceConfig as er}from"./validation.js";import{assertPortableServiceNameSet as tr,requireServiceName as or}from"../../domain/service-name.js";import{resolveExecutionProfile as nr}from"../../engines/catalog.js";import{optionalStringValue as y}from"../../evidence/read-fields.js";const j=["65533","65534"],ir=/\{(?:baseUrl|readyUrl|publicBaseUrl|privateBaseUrl|serviceContextUrl)(?::[^}]+)?\}/gu;export{C as resolveProductDir,Q as resolveServiceCwd};export async function loadConfigContext(r={}){const e=C(process.cwd(),r.dir),{config:t,configFile:o}=r.configContext??await L(e,{importCacheSalt:r.configImportCacheSalt??""}),n=W($r(i(t,"execution"))),s=N(i(t,"artifacts")),c=G(i(t,"toolchains")),d=_(i(t,"fingerprints")),a=wr(i(t,"redact"));v(B(t));const l=gr(i(t,"services"));r.preparedDiscovery||dr(l);const m=r.plain??!D(r.testSelection??{}),f=r.preparedDiscovery?.owned??await q(e,t,{...r.discoveryOptions,plain:m,retired:r.retired,selection:r.vitest}),u=f.project,p=r.testSelection?F(u.suitesByService,r.testSelection):u.suitesByService,w=K(e,p,r.preparedDiscovery?.fileMetadataByPath),b=sr(p,w),S=[];for(const g of Object.keys(l).sort(($,E)=>$.localeCompare(E)))S.push(cr({name:g,productDir:e,config:t,configFile:o,execution:n,artifacts:s,toolchains:c,fingerprints:d,explicitService:l[g]??{}}));v(f.problems[0]??null),r.allowUnmigratedServices||v(f.migration),rr(S);const U=[];for(const g of[...f.groups].sort(($,E)=>$.name.localeCompare(E.name)))U.push(fr({declaration:g,productDir:e,configFile:o,execution:n,artifacts:s,policy:lr(t,g),redact:a,suites:b[g.name]||[],discoveredSuites:u.suitesByService[g.name]||[],fileMetadataByPath:w,plainListed:m}));return{productDir:e,config:t,configFile:o,execution:n,toolchains:c,fingerprints:d,explicitServices:l,discovery:u,groups:U,plainFiles:f.plainFiles,services:S}}function sr(r,e){return Object.fromEntries(Object.entries(r).map(([t,o])=>[t,(o??[]).map(n=>ar(t,n,e))]))}function ar(r,e,t){const o=e.files[0];if(!o)throw new Error(`Discovered suite ${r}/${z(e)} contains no files`);const n=T(t,o);nr({...e,...n});for(const s of e.files.slice(1)){const c=T(t,s);if(c.engineId!==n.engineId||c.profileId!==n.profileId)throw new Error(`Discovered suite ${r}/${z(e)} mixes engine profiles ${n.engineId}/${n.profileId} and ${c.engineId}/${c.profileId}`)}return{...e,engineId:n.engineId,profileId:n.profileId}}function T(r,e){const t=r.get(e);if(!t)throw new Error(`Missing validated testkit metadata for discovered file: ${e}`);return t}export async function loadConfigs(r={}){return(await loadConfigContext(r)).services}function cr({name:r,productDir:e,config:t,configFile:o,execution:n,artifacts:s,toolchains:c,fingerprints:d,explicitService:a}){const l=Object.hasOwn(a,"local")?k(i(a,"local"),r):void 0,m=H(e,a,l),f=R(i(a,"env"),`Service "${r}" env`);if(i(a,"migrate")||i(a,"seed"))throw new Error(`Service "${r}" uses removed migrate/seed hooks. Move template lifecycle to database.template.{migrate,seed,verify}.`);const u=mr(a,r),p=X(i(a,"runtime"),r,c),w=hr(i(a,"dependsOn"),`Service "${r}" dependsOn`),b=y(i(a,"databaseFrom"),`Service "${r}" databaseFrom`);return er({name:r,local:l,database:u,databaseFrom:b,runtime:p,dependsOn:w,productDir:e}),{name:r,productDir:e,configFile:o,stateDir:A.join(e,".testkit",r),artifacts:s,testkit:{execution:n,dependsOn:w,database:u,databaseFrom:b,envFiles:m,serviceEnv:f,runtime:p,ui:i(t,"ui")||null,fingerprints:d,local:l}}}function fr({declaration:r,productDir:e,configFile:t,execution:o,artifacts:n,policy:s,redact:c,suites:d,discoveredSuites:a,fileMetadataByPath:l,plainListed:m}){const f=`Group "${r.name}"`,u=new Set(d.flatMap(p=>p.files));return{...r,roots:[...r.roots],uses:[...r.uses],artifacts:n,configFile:t,productDir:e,redact:c,suites:d,testkit:{execution:o,fileMetadataByPath:new Map([...l.entries()].filter(([p])=>u.has(p))),requirements:Y(i(s,"requirements"),{owner:f,plainListed:m,suites:a}),runtimeIsolation:J(i(s,"runtimeIsolation"),{owner:f,plainListed:m,suites:a}),skip:Z(i(s,"skip"),{owner:f,plainListed:m,suites:a})}}}function lr(r,e){if(e.source!=="implicit")return i(i(r,"groups"),e.name)}function k(r,e){if(r===!1)return;if(!h(r))throw new Error(`Service "${e}" local must be an object or false`);if(Object.prototype.hasOwnProperty.call(r,"port"))throw new Error(`Service "${e}" sets local.port. Testkit assigns every service a free port from the OS. Delete the line, or the port option of app.node or app.next, and use {port} in start, baseUrl, and readyUrl.`);const t=x(r.start,`Service "${e}" local.start`),{baseUrl:o,shutdownTimeoutMs:n,...s}=r;return{...s,...br(n,e),...ur(o,e),start:t,cwd:y(r.cwd,`Service "${e}" local.cwd`)||".",env:R(r.env,`Service "${e}" local.env`),...yr(r.readyTimeoutMs,`Service "${e}" local.readyTimeoutMs`),...Sr(r.readyUrl,e)}}function ur(r,e){if(r==null)return{};const t=x(r,`Service "${e}" local.baseUrl`);M(t,`Service "${e}" local.baseUrl`);const o=new RegExp(`\\{port(?::${pr(e)})?\\}`,"gu");if(!j.every(n=>I(t,o,n,!1)===n))throw new Error(`Service "${e}" local.baseUrl must use {port} for its port, for example http://127.0.0.1:{port}. Testkit assigns every service a free port from the OS.`);return{baseUrl:t}}function O(r){return r.replace(/[\t\n\r]/gu,"").trim()}function P(r){return/^[a-z][a-z0-9+.-]*:[\\/]{2}(?![\\/])([^/\\?#]*)/iu.exec(O(r))?.[1]}function I(r,e,t,o){const n=O(r).replace(e,t).replace(ir,o?`http://template.invalid:${t}`:"http://template.invalid").replace(/\{[^}]+\}/gu,"template");try{return new URL(n).port}catch{return}}function pr(r){return r.replace(/[.*+?^${}()|[\]\\]/gu,"\\$&")}function M(r,e){if(/^[a-z][a-z0-9+.-]*:/iu.test(O(r))&&P(r)===void 0)throw new Error(`${e} must be an absolute URL with a host, for example http://127.0.0.1:{port}/health.`);const[t,o]=j.map(s=>I(r,/\{port(?::[^}]+)?\}/gu,s,!0)),n=/:(\d+)$/u.exec(P(r)??"")?.[1]??(t&&t===o?t:void 0);if(n!==void 0)throw new Error(`${e} has a literal port (${n}). Testkit assigns every service a free port from the OS. Replace the port with {port}.`)}function mr(r,e){const t=V(r,e);if(!t)return;const o=y(i(t,"image"),`Service "${e}" database.image`),n=y(i(t,"password"),`Service "${e}" database.password`),s=y(i(t,"user"),`Service "${e}" database.user`);return{...t,...Object.prototype.hasOwnProperty.call(t,"image")?{image:o}:{},...Object.prototype.hasOwnProperty.call(t,"password")?{password:n}:{},...Object.prototype.hasOwnProperty.call(t,"user")?{user:s}:{}}}function dr(r){for(const[e,t]of Object.entries(r))t&&Object.hasOwn(t,"local")&&k(i(t,"local"),e)}function v(r){if(r!==null)throw new Error(r)}function gr(r){if(r==null)return{};if(!h(r))throw new Error("testkit.config.ts services must be an object");const e=Object.fromEntries(Object.entries(r).map(([t,o])=>{if(or(t,`Service name "${t}"`),o==null)return[t,{}];if(!h(o))throw new Error(`Service "${t}" config must be an object`);return[t,o]}));return tr(Object.keys(e)),e}function wr(r){if(r==null)return[];if(!Array.isArray(r))throw new Error("testkit.config.ts redact must be an array of env key names");return r.map((e,t)=>{if(typeof e!="string")throw new Error(`testkit.config.ts redact[${String(t)}] must be a string`);if(e.trim().length===0)throw new Error(`testkit.config.ts redact[${String(t)}] must not be empty`);return e.trim()})}function hr(r,e){if(r==null)return[];if(!Array.isArray(r))throw new Error(`${e} must be an array of strings`);return r.map((t,o)=>{if(typeof t!="string")throw new Error(`${e}[${String(o)}] must be a string`);return t})}function R(r,e){if(r==null)return{};if(!h(r))throw new Error(`${e} must be an object of strings`);return Object.fromEntries(Object.entries(r).map(([t,o])=>{if(typeof o!="string")throw new Error(`${e}.${t} must be a string`);return[t,o]}))}function yr(r,e){if(r===void 0)return{};if(typeof r!="number")throw new Error(`${e} must be a number`);return{readyTimeoutMs:r}}function br(r,e){if(r===void 0)return{};if(typeof r!="number"||!Number.isInteger(r)||r<=0)throw new Error(`Service "${e}" local.shutdownTimeoutMs must be a positive finite integer in milliseconds`);return{shutdownTimeoutMs:r}}function Sr(r,e){if(r===void 0)return{};if(typeof r=="string")return M(r,`Service "${e}" local.readyUrl`),{readyUrl:r};if(r===!1||r===null)return{readyUrl:r};throw new Error(`Service "${e}" local.readyUrl must be a string, false, or null`)}function x(r,e){if(typeof r!="string")throw new Error(`${e} must be a string`);return r}function $r(r){if(r){if(!h(r))throw new Error("testkit.config.ts execution must be an object");return r}}
@@ -1 +1 @@
1
- import l from"fs";import u from"path";const f=String;export const DEFAULT_DISCOVERY_EXCLUDES=Object.freeze([".cache",".git",".hg",".next",".nuxt",".playwright-browsers",".svn",".testkit",".turbo","build","coverage","dist","node_modules","playwright-report","test-results"]);export function resolveDiscoveryRoots(e,t,o="."){const s=Array.isArray(t)&&t.length>0?t:[o],c=new Set,a=[];for(const i of s){const r=normalizeDiscoveryPath(i);if(!r)continue;const n=u.resolve(e,r==="."?".":r);!l.existsSync(n)||!l.statSync(n).isDirectory()||c.has(n)||(c.add(n),a.push(n))}return a.sort((i,r)=>i.localeCompare(r))}export function shouldExcludeDiscoveryPath(e,t=[]){const o=normalizeDiscoveryPath(e);return!o||o==="."?!1:t.some(s=>h(o,s))}export function normalizeDiscoveryPath(e){return f(e||"").trim().split(u.sep).join("/").replace(/^\.\/+/u,"").replace(/\/+/gu,"/").replace(/\/+$/u,"")||"."}function h(e,t){return e===t||e.startsWith(`${t}/`)}
1
+ import l from"fs";import c from"path";const u=String;export const DEFAULT_DISCOVERY_EXCLUDES=Object.freeze([".cache",".git",".hg",".next",".nuxt",".playwright-browsers",".svn",".testkit",".turbo","build","coverage","dist","node_modules","playwright-report","test-results"]);export function resolveDiscoveryRoots(t,e){const o=Array.isArray(e)&&e.length>0?e:["."],s=new Set,a=[];for(const i of o){const r=normalizeDiscoveryPath(i);if(!r)continue;const n=c.resolve(t,r==="."?".":r);!l.existsSync(n)||!l.statSync(n).isDirectory()||s.has(n)||(s.add(n),a.push(n))}return a.sort((i,r)=>i.localeCompare(r))}export function shouldExcludeDiscoveryPath(t,e=[]){const o=normalizeDiscoveryPath(t);return!o||o==="."?!1:e.some(s=>p(o,s))}export function normalizeDiscoveryPath(t){return u(t||"").trim().split(c.sep).join("/").replace(/^\.\/+/u,"").replace(/\/+/gu,"/").replace(/\/+$/u,"")||"."}export function normalizeRelativePath(t){const e=t.split(c.sep).join("/");return e==="."||e==="./"?".":e.replace(/^\.\/+/u,"").replace(/\/+$/u,"")||"."}function p(t,e){return t===e||t.startsWith(`${e}/`)}
@@ -1 +1 @@
1
- import{parseSuiteSelectors as R}from"../../domain/suite-selection.js";import{isTypedTestFile as T}from"../../domain/identifiers.js";import{rejectUnknownKeys as u}from"../../kernel/known-keys.js";import{isUnknownRecord as m}from"../../kernel/unknown-values.js";import{DEFAULT_RUNTIME_ISOLATION_POLICY as h,normalizeRuntimeIsolationPolicy as l}from"../../domain/runtime-isolation-contract.js";import{normalizePath as y}from"./paths.js";import{optionalArray as $}from"./skip-config.js";import{matchesAnySuite as O}from"./test-selection.js";export{DEFAULT_RUNTIME_ISOLATION_POLICY,isRuntimeIsolationPolicy,normalizeRuntimeIsolationPolicy,RUNTIME_ISOLATION_POLICIES}from"../../domain/runtime-isolation-contract.js";const L=String;export function normalizeRuntimeIsolationConfig(e,{owner:t,plainListed:S=!0,suites:w}){if(!e)return E();if(typeof e=="string")return{...E(),defaultPolicy:l(e,`${t} runtimeIsolation`)};if(!m(e))throw new Error(`${t} runtimeIsolation must be a policy string or object`);u(e,["default","files","suites"],`${t} runtimeIsolation`);const c=w??[],P=new Set(c.flatMap(o=>(o.files??[]).map(y))),g=e.default==null?h:l(e.default,`${t} runtimeIsolation.default`),f=[],p=new Set;for(const[o,n]of $(e.suites,`${t} runtimeIsolation.suites`).entries()){if(!m(n))throw new Error(`${t} runtimeIsolation.suites entries must be objects`);const s=n;u(s,["policy","runtimeIsolation","selector"],`${t} runtimeIsolation.suites entry`);const i=L(s.selector||"").trim();if(!i)throw new Error(`${t} runtimeIsolation.suites[${o.toString()}] requires a selector`);const d=R([i]);if(d.length!==1)throw new Error(`${t} runtimeIsolation.suites["${i}"] is invalid`);const r=d[0];if(r.kind!=="typed")throw new Error(`${t} runtimeIsolation.suites["${i}"] must use a typed selector like ui:auth`);if(p.has(r.raw))throw new Error(`${t} defines duplicate runtimeIsolation.suites selector "${r.raw}"`);if(!O(r,c))throw new Error(`${t} runtimeIsolation.suites selector "${r.raw}" did not match any discovered suite`);p.add(r.raw),f.push({selector:r,policy:l(s.policy||s.runtimeIsolation,`${t} runtimeIsolation.suites["${r.raw}"].policy`)})}const a=[],I=new Set;for(const[o,n]of $(e.files,`${t} runtimeIsolation.files`).entries()){if(!m(n))throw new Error(`${t} runtimeIsolation.files entries must be objects`);const s=n;u(s,["path","policy","runtimeIsolation"],`${t} runtimeIsolation.files entry`);const i=y(_(s.path,`${t} runtimeIsolation.files[${o.toString()}].path`));if(!i)throw new Error(`${t} runtimeIsolation.files[${o.toString()}] requires a path`);if(!P.has(i)&&(S||T(i)))throw new Error(`${t} runtimeIsolation.files["${i}"] did not match any discovered test file`);if(I.has(i))throw new Error(`${t} defines duplicate runtimeIsolation.files path "${i}"`);I.add(i),a.push({path:i,policy:l(s.policy||s.runtimeIsolation,`${t} runtimeIsolation.files["${i}"].policy`)})}return{defaultPolicy:g,suites:f,files:a,filePolicyByPath:new Map(a.map(o=>[o.path,o.policy]))}}function E(){return{defaultPolicy:h,suites:[],files:[],filePolicyByPath:new Map}}function _(e,t){if(e==null||typeof e=="string")return e;throw new Error(`${t} must be a string`)}
1
+ import{parseSuiteSelectors as R}from"../../domain/suite-selection.js";import{isTypedTestFile as b}from"../../domain/identifiers.js";import{rejectUnknownKeys as u}from"../../kernel/known-keys.js";import{isUnknownRecord as c}from"../../kernel/unknown-values.js";import{DEFAULT_RUNTIME_ISOLATION_POLICY as I,normalizeRuntimeIsolationPolicy as l}from"../../domain/runtime-isolation-contract.js";import{normalizePath as $}from"./paths.js";import{optionalArray as y}from"./skip-config.js";import{matchesAnySuite as T}from"./test-selection.js";const j=String;export function normalizeRuntimeIsolationConfig(e,{owner:t,plainListed:S=!0,suites:E}){if(!e)return w();if(typeof e=="string")return{...w(),defaultPolicy:l(e,`${t} runtimeIsolation`)};if(!c(e))throw new Error(`${t} runtimeIsolation must be a policy string or object`);u(e,["default","files","suites"],`${t} runtimeIsolation`);const f=E??[],g=new Set(f.flatMap(o=>(o.files??[]).map($))),P=e.default==null?I:l(e.default,`${t} runtimeIsolation.default`),m=[],p=new Set;for(const[o,n]of y(e.suites,`${t} runtimeIsolation.suites`).entries()){if(!c(n))throw new Error(`${t} runtimeIsolation.suites entries must be objects`);const s=n;u(s,["policy","runtimeIsolation","selector"],`${t} runtimeIsolation.suites entry`);const i=j(s.selector||"").trim();if(!i)throw new Error(`${t} runtimeIsolation.suites[${o.toString()}] requires a selector`);const h=R([i]);if(h.length!==1)throw new Error(`${t} runtimeIsolation.suites["${i}"] is invalid`);const r=h[0];if(r.kind!=="typed")throw new Error(`${t} runtimeIsolation.suites["${i}"] must use a typed selector like ui:auth`);if(p.has(r.raw))throw new Error(`${t} defines duplicate runtimeIsolation.suites selector "${r.raw}"`);if(!T(r,f))throw new Error(`${t} runtimeIsolation.suites selector "${r.raw}" did not match any discovered suite`);p.add(r.raw),m.push({selector:r,policy:l(s.policy||s.runtimeIsolation,`${t} runtimeIsolation.suites["${r.raw}"].policy`)})}const a=[],d=new Set;for(const[o,n]of y(e.files,`${t} runtimeIsolation.files`).entries()){if(!c(n))throw new Error(`${t} runtimeIsolation.files entries must be objects`);const s=n;u(s,["path","policy","runtimeIsolation"],`${t} runtimeIsolation.files entry`);const i=$(k(s.path,`${t} runtimeIsolation.files[${o.toString()}].path`));if(!i)throw new Error(`${t} runtimeIsolation.files[${o.toString()}] requires a path`);if(!g.has(i)&&(S||b(i)))throw new Error(`${t} runtimeIsolation.files["${i}"] did not match any discovered test file`);if(d.has(i))throw new Error(`${t} defines duplicate runtimeIsolation.files path "${i}"`);d.add(i),a.push({path:i,policy:l(s.policy||s.runtimeIsolation,`${t} runtimeIsolation.files["${i}"].policy`)})}return{defaultPolicy:P,suites:m,files:a,filePolicyByPath:new Map(a.map(o=>[o.path,o.policy]))}}function w(){return{defaultPolicy:I,suites:[],files:[],filePolicyByPath:new Map}}function k(e,t){if(e==null||typeof e=="string")return e;throw new Error(`${t} must be a string`)}
@@ -1 +1 @@
1
- import t from"fs";import i from"path";import{DEFAULT_FILE_TIMEOUT_SECONDS as c}from"../../domain/file-timeout.js";import{normalizeExecutionConfig as u,normalizeRuntimeMaxConcurrentTasks as f,normalizeRuntimeInstances as l}from"./decode-execution.js";import"./execution-model.js";import{normalizeConfiguredInputs as S,normalizeConfiguredSteps as x}from"../../steps/configured-steps.js";import{buildConfigToPrepare as C,normalizeBuildConfig as T}from"../../steps/build-config.js";import{normalizeRuntimeToolchain as z}from"../../toolchains/index.js";import{isNonNullObject as m}from"../../kernel/unknown-values.js";export function normalizeRuntimeConfig(e,n,a){if(!e)return{build:null,instances:1,maxConcurrentTasks:Number.POSITIVE_INFINITY,prepare:{inputs:[],steps:[]},toolchain:null};const r=m(e)?e:{},o=T(r.build,`Service "${n}" runtime.build`),s=normalizeRuntimePrepareConfig(r.prepare,n),p=C(o);return{build:o,instances:l(r.instances??1,`Service "${n}" runtime.instances`),maxConcurrentTasks:f(r.maxConcurrentTasks,`Service "${n}" runtime.maxConcurrentTasks`),prepare:{inputs:[...new Set([...p.inputs,...s.inputs])],steps:[...p.steps,...s.steps]},toolchain:z(r.toolchain,`Service "${n}" runtime.toolchain`,a||{})}}export function normalizeRuntimePrepareConfig(e,n){if(e==null)return{inputs:[],steps:[]};if(!m(e))throw new Error(`Service "${n}" runtime.prepare must be an object`);return{inputs:S(e.inputs,`Service "${n}" runtime.prepare`),steps:x(e.steps,`Service "${n}" runtime.prepare.steps`)}}export function normalizeRepoExecution(e){return e?u(e):u({workers:1,fileTimeoutSeconds:c})}export function detectNextApp(e){return t.existsSync(i.join(e,"next.config.js"))||t.existsSync(i.join(e,"next.config.mjs"))||t.existsSync(i.join(e,"next.config.ts"))}export{normalizeConfiguredStep as normalizeTemplateLifecycleStep,normalizeConfiguredSteps as normalizeTemplateLifecycleSteps,parseModuleSpecifier}from"../../steps/configured-steps.js";
1
+ import e from"fs";import i from"path";import{normalizeExecutionConfig as c,normalizeRuntimeMaxConcurrentTasks as a,normalizeRuntimeInstances as f}from"./decode-execution.js";import"./execution-model.js";import{normalizeConfiguredInputs as l,normalizeConfiguredSteps as x}from"../../steps/configured-steps.js";import{buildConfigToPrepare as S,normalizeBuildConfig as C}from"../../steps/build-config.js";import{normalizeRuntimeToolchain as g}from"../../toolchains/index.js";import{isNonNullObject as u}from"../../kernel/unknown-values.js";export function normalizeRuntimeConfig(n,r,m){if(!n)return{build:null,instances:1,maxConcurrentTasks:Number.POSITIVE_INFINITY,prepare:{inputs:[],steps:[]},toolchain:null};const t=u(n)?n:{},o=C(t.build,`Service "${r}" runtime.build`),s=normalizeRuntimePrepareConfig(t.prepare,r),p=S(o);return{build:o,instances:f(t.instances??1,`Service "${r}" runtime.instances`),maxConcurrentTasks:a(t.maxConcurrentTasks,`Service "${r}" runtime.maxConcurrentTasks`),prepare:{inputs:[...new Set([...p.inputs,...s.inputs])],steps:[...p.steps,...s.steps]},toolchain:g(t.toolchain,`Service "${r}" runtime.toolchain`,m||{})}}export function normalizeRuntimePrepareConfig(n,r){if(n==null)return{inputs:[],steps:[]};if(!u(n))throw new Error(`Service "${r}" runtime.prepare must be an object`);return{inputs:l(n.inputs,`Service "${r}" runtime.prepare`),steps:x(n.steps,`Service "${r}" runtime.prepare.steps`)}}export function normalizeRepoExecution(n){return c(n??{})}export function detectNextApp(n){return e.existsSync(i.join(n,"next.config.js"))||e.existsSync(i.join(n,"next.config.mjs"))||e.existsSync(i.join(n,"next.config.ts"))}
@@ -1 +1 @@
1
- import{isTypedTestFile as u}from"../../domain/identifiers.js";export function isAllTypeSelection(e=[]){return e.length===0||e.length===1&&e[0]==="all"}export function matchesSelectedTypes(e,t){return isAllTypeSelection(t)||t.includes(e??"plain")}export function matchesSuiteSelectors(e,t,r=[]){return!r||r.length===0?!0:r.some(n=>(n.kind==="plain"||n.testType===e)&&n.name===t)}export function matchesAnySuite(e,t){return t.some(r=>matchesSuiteSelectors(r.testType,r.name,[e]))}export function selectDiscoveredSuites(e,t){const r=new Set(t.fileNames?.map(s));return Object.fromEntries(Object.entries(e).map(([n,i])=>[n,t.group&&n!==t.group?[]:(i??[]).filter(o=>matchesSelectedTypes(o.testType,t.typeValues??[])&&matchesSuiteSelectors(o.testType,o.name,t.suiteSelectors??[])).flatMap(o=>{const l=r.size?o.files.filter(f=>r.has(s(f))):o.files;return l.length?[{...o,files:l}]:[]})]))}export function selectsOnlyTypedFiles({fileNames:e=[],typeValues:t=[]}){return!matchesSelectedTypes(void 0,t)||e.length>0&&e.every(u)}export function findSuiteSkipReason(e,t,r){for(const n of e.suiteSkips)if(matchesSuiteSelectors(t,r,[n.selector]))return n.reason;return null}export function findSuiteLocks(e,t,r){const n=new Set;for(const i of e.suiteLocks)if(matchesSuiteSelectors(t,r,[i.selector]))for(const o of i.locks)n.add(o);return[...n].sort()}export function resolveRuntimeIsolation(e,t,r){const n=c(r),i=e.fileMetadata.get(n)?.runtimeIsolation||e.runtimeIsolation.files.get(n);if(i)return i;for(const o of e.runtimeIsolation.suites)if(matchesSuiteSelectors(t.testType,t.name,[o.selector]))return o.policy;return e.runtimeIsolation.defaultPolicy}export function resolveTaskPolicy(e,t,r){const n=c(r),i=new Set;for(const o of e.suiteLocks)if(matchesSuiteSelectors(t.testType,t.name,[o.selector]))for(const l of o.locks)i.add(l);for(const o of e.fileLocks.get(n)??[])i.add(o);for(const o of e.fileMetadata.get(n)?.locks??[])i.add(o);return{locks:[...i].sort(),runtimeIsolation:resolveRuntimeIsolation(e,t,r)}}function s(e){return e.replaceAll("\\","/")}function c(e){return e.replaceAll("\\","/").replace(/^\.\/+/,"")}
1
+ import{isTypedTestFile as u}from"../../domain/identifiers.js";export function isAllTypeSelection(e=[]){return e.length===0||e.length===1&&e[0]==="all"}export function matchesSelectedTypes(e,t){return isAllTypeSelection(t)||t.includes(e??"plain")}export function matchesSuiteSelectors(e,t,r=[]){return!r||r.length===0?!0:r.some(n=>(n.kind==="plain"||n.testType===e)&&n.name===t)}export function matchesAnySuite(e,t){return t.some(r=>matchesSuiteSelectors(r.testType,r.name,[e]))}export function selectDiscoveredSuites(e,t){const r=new Set(t.fileNames?.map(s));return Object.fromEntries(Object.entries(e).map(([n,i])=>[n,t.group&&n!==t.group?[]:(i??[]).filter(o=>matchesSelectedTypes(o.testType,t.typeValues??[])&&matchesSuiteSelectors(o.testType,o.name,t.suiteSelectors??[])).flatMap(o=>{const l=r.size?o.files.filter(c=>r.has(s(c))):o.files;return l.length?[{...o,files:l}]:[]})]))}export function selectsOnlyTypedFiles({fileNames:e=[],typeValues:t=[]}){return!matchesSelectedTypes(void 0,t)||e.length>0&&e.every(u)}export function findSuiteSkipReason(e,t,r){for(const n of e.suiteSkips)if(matchesSuiteSelectors(t,r,[n.selector]))return n.reason;return null}export function findSuiteLocks(e,t,r){const n=new Set;for(const i of e.suiteLocks)if(matchesSuiteSelectors(t,r,[i.selector]))for(const o of i.locks)n.add(o);return[...n].sort()}export function resolveRuntimeIsolation(e,t,r){const n=f(r),i=e.fileMetadata.get(n)?.runtimeIsolation||e.runtimeIsolation.files.get(n);if(i)return i;for(const o of e.runtimeIsolation.suites)if(matchesSuiteSelectors(t.testType,t.name,[o.selector]))return o.policy;return e.runtimeIsolation.defaultPolicy}export function resolveTaskPolicy(e,t,r){const n=f(r),i=new Set(findSuiteLocks(e,t.testType,t.name));for(const o of e.fileLocks.get(n)??[])i.add(o);for(const o of e.fileMetadata.get(n)?.locks??[])i.add(o);return{locks:[...i].sort(),runtimeIsolation:resolveRuntimeIsolation(e,t,r)}}function s(e){return e.replaceAll("\\","/")}function f(e){return e.replaceAll("\\","/").replace(/^\.\/+/,"")}
@@ -1 +1 @@
1
- import{finalizeConfiguredInputs as c,finalizeConfiguredSteps as f,validateConfiguredCollection as p}from"../../steps/configured-steps.js";import{findDatabasePlaceholders as l}from"../../steps/environment.js";import{ensureExistingContainedPath as v,ensureExistingPath as m}from"./paths.js";const d=["migrate","seed","verify"];export function validateConfigCoverage(t){const n=new Set(t.map(r=>r.name));for(const r of t){for(const e of r.testkit.dependsOn)if(!n.has(e))throw new Error(`Service "${r.name}" depends on "${e}", but ${e} is not defined`);const o=r.testkit.databaseFrom;if(o&&!n.has(o))throw new Error(`Service "${r.name}" databaseFrom "${o}" is not defined`)}}export function validateServiceConfig({name:t,local:n,database:r,databaseFrom:o,runtime:e,dependsOn:s,productDir:i}){if(r&&o)throw new Error(`Service "${t}" cannot define both database and databaseFrom`);if(e.instances<1)throw new Error(`Service "${t}" runtime.instances must be a positive integer`);if(e.maxConcurrentTasks<=0)throw new Error(`Service "${t}" runtime.maxConcurrentTasks must be a positive integer when provided`);for(const a of s)if(a===t)throw new Error(`Service "${t}" cannot depend on itself`);if(w({name:t,local:n,database:r,runtime:e}),n?.cwd&&m(i,n.cwd,`Service "${t}" local.cwd`),e.toolchain?.cwd&&v(i,e.toolchain.cwd,`Service "${t}" runtime.toolchain.cwd`),r)for(const a of d)p({productDir:i,label:`Service "${t}" database.template.${a}`,steps:r.template[a]});p({productDir:i,label:`Service "${t}" database.template`,inputs:r?.template.inputs||[]}),p({productDir:i,label:`Service "${t}" runtime.prepare`,inputs:e.prepare.inputs,steps:e.prepare.steps})}function w({name:t,local:n,database:r,runtime:o}){const e=s=>i=>{const a=l(i).at(0);if(a)throw new Error(`Service "${t}" ${s} uses ${a.placeholder}. Database placeholders resolve only in env values; commands read DATABASE_URL or a service env entry at run time.`);return i};if(n&&(e("local.start")(n.start),e("local.cwd")(n.cwd)),c(o.prepare.inputs,e("runtime.prepare.inputs")),f(o.prepare.steps,e("runtime.prepare.steps")),r){c(r.template.inputs,e("database.template.inputs"));for(const s of d)f(r.template[s],e(`database.template.${s}`))}}
1
+ import{finalizeConfiguredInputs as c,finalizeConfiguredSteps as f,validateConfiguredCollection as p}from"../../steps/configured-steps.js";import{findDatabasePlaceholders as l}from"../../steps/environment.js";import{ensureExistingContainedPath as m,ensureExistingPath as v}from"./paths.js";const d=["migrate","seed","verify"];export function validateConfigCoverage(r){const o=new Set(r.map(e=>e.name));for(const e of r){for(const t of e.testkit.dependsOn)if(!o.has(t))throw new Error(`Service "${e.name}" depends on "${t}", but ${t} is not defined`);const a=e.testkit.databaseFrom;if(a&&!o.has(a))throw new Error(`Service "${e.name}" databaseFrom "${a}" is not defined`)}}export function validateServiceConfig({name:r,local:o,database:e,databaseFrom:a,runtime:t,dependsOn:s,productDir:n}){if(e&&a)throw new Error(`Service "${r}" cannot define both database and databaseFrom`);for(const i of s)if(i===r)throw new Error(`Service "${r}" cannot depend on itself`);if(w({name:r,local:o,database:e,runtime:t}),o?.cwd&&v(n,o.cwd,`Service "${r}" local.cwd`),t.toolchain?.cwd&&m(n,t.toolchain.cwd,`Service "${r}" runtime.toolchain.cwd`),e)for(const i of d)p({productDir:n,label:`Service "${r}" database.template.${i}`,steps:e.template[i]});p({productDir:n,label:`Service "${r}" database.template`,inputs:e?.template.inputs||[]}),p({productDir:n,label:`Service "${r}" runtime.prepare`,inputs:t.prepare.inputs,steps:t.prepare.steps})}function w({name:r,local:o,database:e,runtime:a}){const t=s=>n=>{const i=l(n).at(0);if(i)throw new Error(`Service "${r}" ${s} uses ${i.placeholder}. Database placeholders resolve only in env values; commands read DATABASE_URL or a service env entry at run time.`);return n};if(o&&(t("local.start")(o.start),t("local.cwd")(o.cwd)),c(a.prepare.inputs,t("runtime.prepare.inputs")),f(a.prepare.steps,t("runtime.prepare.steps")),e){c(e.template.inputs,t("database.template.inputs"));for(const s of d)f(e.template[s],t(`database.template.${s}`))}}
@@ -1,5 +1,5 @@
1
- import{managedHostProcessInfrastructureError as U,runManagedHostProcess as Q,startManagedHostProcess as G}from"../process/index.js";import{LOCAL_ADMIN_DB as D,LOCAL_ADMIN_QUERY_RETRY_INTERVAL_MS as j,LOCAL_ADMIN_QUERY_RETRY_MS as Y,LOCAL_DROP_DATABASE_POLL_INTERVAL_MS as V,LOCAL_DROP_DATABASE_TIMEOUT_MS as P}from"./constants.js";import{dockerCliEnvironment as X}from"./docker-endpoint.js";import{buildDatabaseUrl as z,escapeIdentifier as m,escapeSqlLiteral as M}from"./naming.js";import{runDockerPsql as Z}from"./postgres/index.js";import{callCap as x}from"./call-caps.js";import{NODE_TIMER_LIMIT_MS as J}from"./operation-deadline.js";import{ownedClientStartFailure as K,ownedPostgresExec as tt,reclaimOwnedPostgresExecs as et}from"./postgres/owned-exec.js";import{readDynamicProperty as k}from"../kernel/unknown-values.js";import{toError as nt}from"../kernel/error-serialization.js";export{z as buildDatabaseUrl};export const ADMIN_QUERY_COMMAND_TIMEOUT_MS=15e3;const ot=3e4,rt=["a.state","a.wait_event_type","a.wait_event","extract(epoch from now() - a.query_start)::int","(SELECT string_agg('pid ' || b.pid || ' (' || coalesce(nullif(b.application_name, ''), 'unnamed') || ')', ', ') FROM pg_stat_activity b WHERE b.pid = ANY(pg_blocking_pids(a.pid)))"].join(", ");export async function databaseExists(t,e,n={}){return(await runAdminQuery(t,["-tAc",`SELECT 1 FROM pg_database WHERE datname = '${M(e)}'`],n)).trim()==="1"}export async function createEmptyDatabase(t,e,n={}){await runAdminQuery(t,["-c",`CREATE DATABASE "${m(e)}"`],n)}export async function cloneDatabaseFromTemplate(t,e,n,o={}){await runAdminQuery(t,["-c",`CREATE DATABASE "${m(e)}" TEMPLATE "${m(n)}"`],o)}export async function dropDatabaseWithForceOrDrain(t,e,n={}){const o={signal:n.signal};try{await runAdminQuery(t,["-c",`DROP DATABASE IF EXISTS "${m(e)}" WITH (FORCE)`],o);return}catch(s){if(!ct(s))throw s}if(!await databaseExists(t,e,o))return;let a=!1;try{await runAdminQuery(t,["-c",`ALTER DATABASE "${m(e)}" WITH ALLOW_CONNECTIONS false`],o),a=!0,await waitForDatabaseConnectionsToDrain(t,e,{signal:n.signal}),await runAdminQuery(t,["-c",`DROP DATABASE IF EXISTS "${m(e)}"`],o),a=!1}finally{const s={signal:AbortSignal.timeout(P)};a&&await databaseExists(t,e,s).catch(()=>!1)&&await runAdminQuery(t,["-c",`ALTER DATABASE "${m(e)}" WITH ALLOW_CONNECTIONS true`],s).catch(()=>{})}}export async function waitForDatabaseConnectionsToDrain(t,e,n={}){const o=n.timeoutMs??P,a=Date.now()+o,s={signal:n.signal};for(;;){N(n.signal),await st(t,e,s);const i=await at(t,e,s);if(i===0)return;if(Date.now()>=a)throw new Error(`Timed out waiting for database "${e}" connections to close (${String(i)} remaining)`);await b(V,n.signal)}}export async function runAdminQuery(t,e,n={}){const o=buildAdminQueryCommandArgs(t,e),a=g(n.lockTimeoutMs??x("adminLockTimeout"),"lock timeout"),s=g(n.statementTimeoutMs??x("adminStatementTimeout"),"statement timeout"),i=g(n.clientMarginMs??ot,"client margin"),l=g(n.watchdogIntervalMs??ADMIN_QUERY_COMMAND_TIMEOUT_MS,"watchdog interval"),p=g(n.watchdogCheckTimeoutMs??ADMIN_QUERY_COMMAND_TIMEOUT_MS,"watchdog check timeout"),W=Date.now();for(;;){N(n.signal);let _=null,T=null;const q=r=>(r.message=`${r.message.trimEnd()}
2
- ${it(e,a,s,T)}`,r);try{let r;if(y(t)){const u={containerId:t.containerId,containerPort:t.containerPort,image:t.image},A=tt(u,t,{...process.env,PGAPPNAME:"testkit:admin"},o,`-c lock_timeout=${String(a)} -c statement_timeout=${String(s)}`),C=d=>Q({args:["exec","-e","PGPASSWORD",t.containerId,...buildAdminQueryCommandArgs(t,["-tAc",`SELECT ${rt} FROM pg_stat_activity a WHERE a.application_name = '${A.applicationName}'`])],cwd:process.cwd(),displayLabel:`Docker Postgres admin session for ${t.containerName}`,env:X({...process.env,PGPASSWORD:t.password}),executable:"docker",signal:d,timeoutMs:p}),R=Date.now(),$=G({args:A.args,cwd:process.cwd(),displayLabel:`Docker Postgres admin query for ${t.containerName}`,env:A.env,executable:"docker",outputMode:"bounded-capture",recoveryIdentity:"none",signal:n.signal,timeoutMs:Math.min(s+i,J)}),E=new AbortController,B=(async()=>{for(let d=0;d<2;){await b(l,E.signal);const w=await C(E.signal);if(E.signal.aborted)return;if(w.timedOut||w.exitCode!==0)break;const S=v(w.stdout,Date.now()-R);S&&(T=S),d=S?0:d+1}E.abort("stalled"),await $.stop()})().catch(()=>{}),c=await $.completion.finally(()=>{E.abort()});await B;const f=E.signal.reason==="stalled";if((c.cancelled||c.timedOut)&&!f){const d=await Promise.resolve().then(()=>C()).catch(()=>null),w=d?.exitCode===0&&!d.timedOut?v(d.stdout,Date.now()-R):null;w&&(T=w)}(c.cancelled||c.outputLimitExceeded||c.timedOut||f)&&(_=await et(u,t,A.id).then(()=>null,nt));const I=U(`Docker Postgres admin query for ${t.containerName}`,f?{...c,timedOut:!0}:c);if(I)throw ut(I,_);const L=K(u,"psql",c);if(L)throw L;r={exitCode:c.exitCode,stderr:c.stderr,stdout:c.stdout}}else{const u={...process.env,PGPASSWORD:t.password,...t.sslMode?{PGSSLMODE:t.sslMode}:{}};r=await Z({args:o,env:u,timeoutMs:ADMIN_QUERY_COMMAND_TIMEOUT_MS,connection:{host:t.host,port:t.port,user:t.user,password:t.password,database:t.adminDatabase||D,sslMode:t.sslMode},signal:n.signal})}if(r.exitCode!=null&&r.exitCode!==0){const u=new Error(r.shortMessage||r.stderr||r.stdout||`psql failed with exit code ${String(r.exitCode)}`);throw Reflect.set(u,"stdout",r.stdout||""),Reflect.set(u,"stderr",r.stderr||""),u}const{stdout:H}=r;return H}catch(r){if(_!==null||Date.now()-W>=Y||!isTransientAdminQueryConnectionError(r))throw y(t)&&r instanceof Error?q(r):r;await b(j,n.signal)}}}export function buildAdminQueryCommandArgs(t,e){return y(t)?["psql","-v","ON_ERROR_STOP=1","-U",t.user,"-d",t.adminDatabase||D,"-p",String(t.containerPort||5432),...e]:["-v","ON_ERROR_STOP=1","-h",t.host,"-p",String(t.port),"-U",t.user,"-d",t.adminDatabase||D,...e]}export function isTransientAdminQueryConnectionError(t){const e=F(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 st(t,e,n){await runAdminQuery(t,["-c",["SELECT pg_terminate_backend(pid)","FROM pg_stat_activity",`WHERE datname = '${M(e)}' AND pid <> pg_backend_pid();`].join(" ")],n)}async function at(t,e,n){const o=await runAdminQuery(t,["-tAc",["SELECT COUNT(*)","FROM pg_stat_activity",`WHERE datname = '${M(e)}' AND pid <> pg_backend_pid();`].join(" ")],n);return Number.parseInt(o.trim(),10)||0}function g(t,e){if(!Number.isSafeInteger(t)||t<=0)throw new TypeError(`Admin query ${e} must be a positive safe integer in milliseconds`);return t}function v(t,e){const n=t.split(`
3
- `).find(p=>p.trim()!=="");if(n===void 0)return null;const[o="",a="",s="",i="",...l]=n.trim().split("|");return{afterMs:e,blockers:l.join("|"),queryAge:i,state:o||"unknown",waitEvent:a?`${a}:${s}`:""}}function it(t,e,n,o){const a=t.filter((l,p)=>p>0&&/^-[A-Za-z]*c$/u.test(t[p-1]??"")).map(l=>l.length>200?`${l.slice(0,200)}\u2026`:l),s=`lock_timeout ${O(e)}, statement_timeout ${O(n)}`,i=o?` Last seen ${O(o.afterMs)} in: ${o.state}`+(o.queryAge?` for ${o.queryAge} s`:"")+(o.waitEvent?`, waiting on ${o.waitEvent}`:"")+(o.blockers?`, blocked by ${o.blockers}`:"")+".":"";return`Admin statement: ${a.join("; ")||"(none)"} (${s}).${i}`}function O(t){return`${(t/1e3).toFixed(t%1e3===0?0:1)} s`}function ct(t){const e=F(t);return e.includes('syntax error at or near "WITH"')||e.includes('option "force"')}function b(t,e){return new Promise((n,o)=>{if(e?.aborted){o(h(e));return}const a=setTimeout(s,t);e?.addEventListener("abort",i,{once:!0});function s(){e?.removeEventListener("abort",i),n()}function i(){clearTimeout(a),o(e?h(e):new Error("Database administration aborted"))}})}function N(t){if(t?.aborted)throw h(t)}function h(t){return t.reason instanceof Error?t.reason:new Error("Database administration aborted",{cause:t.reason})}function ut(t,e){if(e===null)return t;const n=new AggregateError([t,e],`${t.message}
4
- ${e.message}`,{cause:t});return Object.defineProperty(n,"result",{enumerable:!1,value:k(t,"result")}),n}function F(t){return["shortMessage","stderr","stdout","message"].map(e=>dt(k(t,e)||"")).join(`
1
+ import{setTimeout as B}from"node:timers/promises";import{managedHostProcessInfrastructureError as U,runManagedHostProcess as Q,startManagedHostProcess as j}from"../process/index.js";import{LOCAL_ADMIN_DB as M,LOCAL_ADMIN_QUERY_RETRY_INTERVAL_MS as G,LOCAL_ADMIN_QUERY_RETRY_MS as Y,LOCAL_DROP_DATABASE_POLL_INTERVAL_MS as V,LOCAL_DROP_DATABASE_TIMEOUT_MS as I}from"./constants.js";import{dockerCliEnvironment as X}from"./docker-endpoint.js";import{buildDatabaseUrl as z,escapeIdentifier as m,escapeSqlLiteral as S}from"./naming.js";import{runDockerPsql as Z}from"./postgres/index.js";import{callCap as L}from"./call-caps.js";import{NODE_TIMER_LIMIT_MS as J}from"./operation-deadline.js";import{ownedClientStartFailure as K,ownedPostgresExec as tt,reclaimOwnedPostgresExecs as et}from"./postgres/owned-exec.js";import{readDynamicProperty as P}from"../kernel/unknown-values.js";import{toError as nt}from"../kernel/error-serialization.js";export{z as buildDatabaseUrl};export const ADMIN_QUERY_COMMAND_TIMEOUT_MS=15e3;const ot=3e4,st=["a.state","a.wait_event_type","a.wait_event","extract(epoch from now() - a.query_start)::int","(SELECT string_agg('pid ' || b.pid || ' (' || coalesce(nullif(b.application_name, ''), 'unnamed') || ')', ', ') FROM pg_stat_activity b WHERE b.pid = ANY(pg_blocking_pids(a.pid)))"].join(", ");export async function databaseExists(t,e,n={}){return(await runAdminQuery(t,["-tAc",`SELECT 1 FROM pg_database WHERE datname = '${S(e)}'`],n)).trim()==="1"}export async function createEmptyDatabase(t,e,n={}){await runAdminQuery(t,["-c",`CREATE DATABASE "${m(e)}"`],n)}export async function cloneDatabaseFromTemplate(t,e,n,o={}){await runAdminQuery(t,["-c",`CREATE DATABASE "${m(e)}" TEMPLATE "${m(n)}"`],o)}export async function dropDatabaseWithForceOrDrain(t,e,n={}){const o={signal:n.signal};try{await runAdminQuery(t,["-c",`DROP DATABASE IF EXISTS "${m(e)}" WITH (FORCE)`],o);return}catch(r){if(!ct(r))throw r}if(!await databaseExists(t,e,o))return;let a=!1;try{await runAdminQuery(t,["-c",`ALTER DATABASE "${m(e)}" WITH ALLOW_CONNECTIONS false`],o),a=!0,await waitForDatabaseConnectionsToDrain(t,e,{signal:n.signal}),await runAdminQuery(t,["-c",`DROP DATABASE IF EXISTS "${m(e)}"`],o),a=!1}finally{const r={signal:AbortSignal.timeout(I)};a&&await databaseExists(t,e,r).catch(()=>!1)&&await runAdminQuery(t,["-c",`ALTER DATABASE "${m(e)}" WITH ALLOW_CONNECTIONS true`],r).catch(()=>{})}}export async function waitForDatabaseConnectionsToDrain(t,e,n={}){const o=n.timeoutMs??I,a=Date.now()+o,r={signal:n.signal};for(;;){k(n.signal),await rt(t,e,r);const d=await at(t,e,r);if(d===0)return;if(Date.now()>=a)throw new Error(`Timed out waiting for database "${e}" connections to close (${String(d)} remaining)`);await O(V,n.signal)}}export async function runAdminQuery(t,e,n={}){const o=buildAdminQueryCommandArgs(t,e),a=n.lockTimeoutMs??L("adminLockTimeout"),r=n.statementTimeoutMs??L("adminStatementTimeout"),d=n.clientMarginMs??ot,l=n.watchdogIntervalMs??ADMIN_QUERY_COMMAND_TIMEOUT_MS,p=n.watchdogCheckTimeoutMs??ADMIN_QUERY_COMMAND_TIMEOUT_MS,F=Date.now();for(;;){k(n.signal);let A=null,_=null;const W=s=>(s.message=`${s.message.trimEnd()}
2
+ ${it(e,a,r,_)}`,s);try{let s;if(y(t)){const c={containerId:t.containerId,containerPort:t.containerPort,image:t.image},g=tt(c,t,{...process.env,PGAPPNAME:"testkit:admin"},o,`-c lock_timeout=${String(a)} -c statement_timeout=${String(r)}`),b=u=>Q({args:["exec","-e","PGPASSWORD",t.containerId,...buildAdminQueryCommandArgs(t,["-tAc",`SELECT ${st} FROM pg_stat_activity a WHERE a.application_name = '${g.applicationName}'`])],cwd:process.cwd(),displayLabel:`Docker Postgres admin session for ${t.containerName}`,env:X({...process.env,PGPASSWORD:t.password}),executable:"docker",signal:u,timeoutMs:p}),h=Date.now(),C=j({args:g.args,cwd:process.cwd(),displayLabel:`Docker Postgres admin query for ${t.containerName}`,env:g.env,executable:"docker",outputMode:"bounded-capture",recoveryIdentity:"none",signal:n.signal,timeoutMs:Math.min(r+d,J)}),E=new AbortController,H=(async()=>{for(let u=0;u<2;){await O(l,E.signal);const w=await b(E.signal);if(E.signal.aborted)return;if(w.timedOut||w.exitCode!==0)break;const f=x(w.stdout,Date.now()-h);f&&(_=f),u=f?0:u+1}E.abort("stalled"),await C.stop()})().catch(()=>{}),i=await C.completion.finally(()=>{E.abort()});await H;const T=E.signal.reason==="stalled";if((i.cancelled||i.timedOut)&&!T){const u=await Promise.resolve().then(()=>b()).catch(()=>null),w=u?.exitCode===0&&!u.timedOut?x(u.stdout,Date.now()-h):null;w&&(_=w)}(i.cancelled||i.outputLimitExceeded||i.timedOut||T)&&(A=await et(c,t,g.id).then(()=>null,nt));const R=U(`Docker Postgres admin query for ${t.containerName}`,T?{...i,timedOut:!0}:i);if(R)throw ut(R,A);const $=K(c,"psql",i);if($)throw $;s={exitCode:i.exitCode,stderr:i.stderr,stdout:i.stdout}}else{const c={...process.env,PGPASSWORD:t.password,...t.sslMode?{PGSSLMODE:t.sslMode}:{}};s=await Z({args:o,env:c,timeoutMs:ADMIN_QUERY_COMMAND_TIMEOUT_MS,connection:{host:t.host,port:t.port,user:t.user,password:t.password,database:t.adminDatabase||M,sslMode:t.sslMode},signal:n.signal})}if(s.exitCode!=null&&s.exitCode!==0){const c=new Error(s.shortMessage||s.stderr||s.stdout||`psql failed with exit code ${String(s.exitCode)}`);throw Reflect.set(c,"stdout",s.stdout||""),Reflect.set(c,"stderr",s.stderr||""),c}const{stdout:q}=s;return q}catch(s){if(A!==null||Date.now()-F>=Y||!isTransientAdminQueryConnectionError(s))throw y(t)&&s instanceof Error?W(s):s;await O(G,n.signal)}}}export function buildAdminQueryCommandArgs(t,e){return y(t)?["psql","-v","ON_ERROR_STOP=1","-U",t.user,"-d",t.adminDatabase||M,"-p",String(t.containerPort||5432),...e]:["-v","ON_ERROR_STOP=1","-h",t.host,"-p",String(t.port),"-U",t.user,"-d",t.adminDatabase||M,...e]}export function isTransientAdminQueryConnectionError(t){const e=v(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 rt(t,e,n){await runAdminQuery(t,["-c",["SELECT pg_terminate_backend(pid)","FROM pg_stat_activity",`WHERE datname = '${S(e)}' AND pid <> pg_backend_pid();`].join(" ")],n)}async function at(t,e,n){const o=await runAdminQuery(t,["-tAc",["SELECT COUNT(*)","FROM pg_stat_activity",`WHERE datname = '${S(e)}' AND pid <> pg_backend_pid();`].join(" ")],n);return Number.parseInt(o.trim(),10)||0}function x(t,e){const n=t.split(`
3
+ `).find(p=>p.trim()!=="");if(n===void 0)return null;const[o="",a="",r="",d="",...l]=n.trim().split("|");return{afterMs:e,blockers:l.join("|"),queryAge:d,state:o||"unknown",waitEvent:a?`${a}:${r}`:""}}function it(t,e,n,o){const a=t.filter((l,p)=>p>0&&/^-[A-Za-z]*c$/u.test(t[p-1]??"")).map(l=>l.length>200?`${l.slice(0,200)}\u2026`:l),r=`lock_timeout ${D(e)}, statement_timeout ${D(n)}`,d=o?` Last seen ${D(o.afterMs)} in: ${o.state}`+(o.queryAge?` for ${o.queryAge} s`:"")+(o.waitEvent?`, waiting on ${o.waitEvent}`:"")+(o.blockers?`, blocked by ${o.blockers}`:"")+".":"";return`Admin statement: ${a.join("; ")||"(none)"} (${r}).${d}`}function D(t){return`${(t/1e3).toFixed(t%1e3===0?0:1)} s`}function ct(t){const e=v(t);return e.includes('syntax error at or near "WITH"')||e.includes('option "force"')}function O(t,e){return B(t,void 0,{signal:e}).catch(n=>{throw e?.aborted?N(e):n})}function k(t){if(t?.aborted)throw N(t)}function N(t){return t.reason instanceof Error?t.reason:new Error("Database administration aborted",{cause:t.reason})}function ut(t,e){if(e===null)return t;const n=new AggregateError([t,e],`${t.message}
4
+ ${e.message}`,{cause:t});return Object.defineProperty(n,"result",{enumerable:!1,value:P(t,"result")}),n}function v(t){return["shortMessage","stderr","stdout","message"].map(e=>dt(P(t,e)||"")).join(`
5
5
  `)}function dt(t){if(typeof t=="symbol")throw new TypeError("Cannot convert a Symbol value to a string");return String(t)}function y(t){return!!t.containerName}
@@ -1 +1 @@
1
- import{hasActiveRunManifest as L}from"../process/runs/ownership.js";import{observeProcessCreationIdentity as R}from"../process/index.js";import{TESTKIT_PRODUCT_ID_LABEL as p,TESTKIT_RESOURCE_KIND_LABEL as m,TESTKIT_SCOPE_LABEL as v,buildProductIdentity as T}from"./docker.js";import{localPostgresProvider as a}from"./provider.js";import{requireProductLifecycleFenceProductDir as g}from"../process/lifecycle-fence.js";import{hasRemainingLocalArtifacts as h,readOwnedStateValues as y}from"./state.js";export async function cleanupLocalPostgresDockerResources(e={}){return k({...e,force:!1})}export async function destroyLocalPostgresDockerResources(e,o={}){const t=g(e);return k({...o,assertMutationAuthority:()=>{if(g(e)!==t)throw new Error("Database destroy lifecycle authority changed products")},force:!0,productDir:t})}async function k(e){e.assertMutationAuthority?.();const o=T(e.productDir||process.cwd()),t={signal:e.signal},f=await a.listContainers(o,t),P=await a.listVolumes(o,t),s=[],i=[],u=[],c=[],l=[];for(const r of f){if(!_(r))continue;const n=A(r,o,e);n.action==="remove"?s.push({...r,action:"remove",reason:n.reason}):n.action==="stop"?s.push({...r,action:"stop",reason:n.reason}):n.reason&&i.push({...r,reason:n.reason})}for(const r of P){if(!E(r))continue;const n=D(r,o,e);n.action==="remove"?s.push({...r,action:"remove",reason:n.reason,resourceType:"volume"}):n.reason&&i.push({...r,reason:n.reason,resourceType:"volume"})}if(!e.dryRun)for(const r of s)e.assertMutationAuthority?.(),r.action==="stop"?await a.stopContainer(r.id,r.name,t)?l.push(r):c.push(d(r)):r.resourceType==="volume"?await a.removeVolume(r.name,r,t)?u.push(r):c.push(d(r)):await a.removeContainer(r.id,r.name,t)?u.push(r):c.push(d(r));return{removed:e.dryRun?[]:u,stopped:e.dryRun?[]:l,skipped:e.dryRun?[]:c,targets:s,kept:i}}function d(e){return e.resourceType==="volume"?{...e,action:"skip",intendedAction:"remove",reason:"resource-changed-or-disappeared-before-mutation",selectionReason:e.reason}:{...e,action:"skip",intendedAction:e.action==="stop"?"stop":"remove",reason:"resource-changed-or-disappeared-before-mutation",selectionReason:e.reason}}function A(e,o,t){return e.labels[p]!==o.id?{action:"keep",reason:"different-product"}:t.force?{action:"remove",reason:"destroy-current-product"}:hasActiveProductRuntime(o.dir)?{action:"keep",reason:"current-product-active"}:h(o.dir)?y(o.dir,["container_name","local_container_name"]).has(e.name)?t.stopIdle&&e.running&&!hasActiveProductRuntime(o.dir)?{action:"stop",reason:"current-product-idle"}:{action:"keep",reason:"current-product-referenced"}:{action:"remove",reason:"current-product-unreferenced"}:{action:"remove",reason:"current-product-no-local-artifacts"}}function D(e,o,t){return e.labels[p]!==o.id?{action:"keep",reason:"different-product"}:t.force?{action:"remove",reason:"destroy-current-product"}:hasActiveProductRuntime(o.dir)?{action:"keep",reason:"current-product-active"}:h(o.dir)?y(o.dir,["volume_name"]).has(e.name)?{action:"keep",reason:"current-product-referenced"}:{action:"remove",reason:"current-product-unreferenced"}:{action:"remove",reason:"current-product-no-local-artifacts"}}function _(e){const{labels:o}=e;return o[m]==="postgres-container"&&o[v]==="local-postgres"}function E(e){const{labels:o}=e;return o[m]==="postgres-volume"&&o[v]==="local-postgres"}export function hasActiveProductRuntime(e){return L(e,R)}
1
+ import{hasActiveRunManifest as k}from"../process/runs/ownership.js";import{observeProcessCreationIdentity as L}from"../process/index.js";import{TESTKIT_PRODUCT_ID_LABEL as T,TESTKIT_RESOURCE_KIND_LABEL as g,TESTKIT_SCOPE_LABEL as h,buildProductIdentity as A}from"./docker.js";import{localPostgresProvider as a}from"./provider.js";import{requireProductLifecycleFenceProductDir as v}from"../process/lifecycle-fence.js";import{hasRemainingLocalArtifacts as R,readOwnedStateValues as D}from"./state.js";export async function cleanupLocalPostgresDockerResources(e={}){return y({...e,force:!1})}export async function destroyLocalPostgresDockerResources(e,o={}){const t=v(e);return y({...o,assertMutationAuthority:()=>{if(v(e)!==t)throw new Error("Database destroy lifecycle authority changed products")},force:!0,productDir:t})}async function y(e){e.assertMutationAuthority?.();const o=A(e.productDir||process.cwd()),t={signal:e.signal},n=await a.listContainers(o,t),c=await a.listVolumes(o,t),i=[],d=[],f=[],u=[],m=[];for(const r of n){if(!_(r))continue;const{action:l,reason:s}=P(r,["container_name","local_container_name"],r.running,o,e);l==="keep"?d.push({...r,reason:s}):i.push({...r,action:l,reason:s})}for(const r of c){if(!E(r))continue;const{action:l,reason:s}=P(r,["volume_name"],!1,o,e);l==="keep"?d.push({...r,reason:s,resourceType:"volume"}):i.push({...r,action:"remove",reason:s,resourceType:"volume"})}if(!e.dryRun)for(const r of i)e.assertMutationAuthority?.(),r.action==="stop"?await a.stopContainer(r.id,r.name,t)?m.push(r):u.push(p(r)):r.resourceType==="volume"?await a.removeVolume(r.name,r,t)?f.push(r):u.push(p(r)):await a.removeContainer(r.id,r.name,t)?f.push(r):u.push(p(r));return{removed:f,stopped:m,skipped:u,targets:i,kept:d}}function p(e){return e.resourceType==="volume"?{...e,action:"skip",intendedAction:"remove",reason:"resource-changed-or-disappeared-before-mutation",selectionReason:e.reason}:{...e,action:"skip",intendedAction:e.action==="stop"?"stop":"remove",reason:"resource-changed-or-disappeared-before-mutation",selectionReason:e.reason}}function P(e,o,t,n,c){return e.labels[T]!==n.id?{action:"keep",reason:"different-product"}:c.force?{action:"remove",reason:"destroy-current-product"}:hasActiveProductRuntime(n.dir)?{action:"keep",reason:"current-product-active"}:R(n.dir)?D(n.dir,o).has(e.name)?c.stopIdle&&t&&!hasActiveProductRuntime(n.dir)?{action:"stop",reason:"current-product-idle"}:{action:"keep",reason:"current-product-referenced"}:{action:"remove",reason:"current-product-unreferenced"}:{action:"remove",reason:"current-product-no-local-artifacts"}}function _(e){const{labels:o}=e;return o[g]==="postgres-container"&&o[h]==="local-postgres"}function E(e){const{labels:o}=e;return o[g]==="postgres-volume"&&o[h]==="local-postgres"}export function hasActiveProductRuntime(e){return k(e,L)}
@@ -1 +1 @@
1
- import{runDockerPsql as M}from"./postgres/index.js";import{withProcessLockProgress as _}from"../process/locks.js";import{awaitCapturedResult as P}from"../evidence/step-output.js";import{applySetupNodeOptions as y,runConfiguredStep as D}from"../steps/run.js";import{announceResolvedToolchain as R,applyToolchainEnv as $,resolveConfiguredToolchain as A}from"../toolchains/index.js";import{resolveConfiguredCwd as L,resolveConfiguredPath as N,summarizeConfiguredStep as m}from"../steps/configured-steps.js";const U=3600*1e3;export async function runConfiguredSteps({config:e,steps:n=[],env:g,labelPrefix:O,ownedContainer:p=null,reporter:s=null,setupRegistry:o=null,parentOperation:C=null,startIndex:h=0,afterStep:w=null,excludeFromLimit:b=a=>a(),signal:d,stepTimeoutMs:E}){if(n.length===0)return;const a=I(E),c=await A(e,{signal:d});await R(e,c,s);for(const[v,t]of n.entries()){const S=h+v+1,l=`${O}:${e.name}:${String(S)}`,u=o?.start({config:e,stage:l,kind:"setup-step",summary:m(t),parentId:k(C)});s?.phaseStarted?.(l);try{const i=$(y(g),c),r={livePrefix:`[${e.runtimeLabel||e.name}:${e.name}]`,liveWriter:s?.outputMode==="debug"?f=>{s.writeDebugLine?.(f)}:null,logRecord:u?._logRecord||null};await _(`running ${l}: ${m(t)}`,()=>b(async()=>{if(t.kind==="sql-file"){const f=await M({consumerSql:!0,databaseUrl:i.DATABASE_URL,args:[i.DATABASE_URL,"-v","ON_ERROR_STOP=1","-X","-f",N(e.productDir,t.cwd,t.path)],cwd:L(e.productDir,t.cwd),env:i,ownedContainer:p,signal:d,timeoutMs:a});P(f,r)}else await D(e,t,i,c,{...r,ownedContainer:p,signal:d,timeoutMs:a})}));const T=u&&o?o.finish(u,{status:"passed",summary:m(t)}):null;T&&s?.setupOperationFinished?.(T)}catch(i){const r=u&&o?o.finish(u,{status:"failed",summary:m(t),error:q(i)}):null;throw r&&s?.setupOperationFinished?.(r),i}w&&await w({step:t,index:h+v,stepNumber:S,label:l})}}function k(e){return typeof e=="string"?e||null:e?.id||null}function q(e){return(typeof e!="object"||e===null)&&typeof e!="function"?e:Reflect.get(e,"message")||e}function I(e){const n=e??U;if(!Number.isSafeInteger(n)||n<=0)throw new Error("Configured step timeout must be a positive safe integer in milliseconds");return n}
1
+ import{runDockerPsql as C}from"./postgres/index.js";import{finishSetupOperation as y}from"./progress.js";import{readDynamicProperty as D}from"../kernel/unknown-values.js";import{withProcessLockProgress as E}from"../process/locks.js";import{awaitCapturedResult as R}from"../evidence/step-output.js";import{applySetupNodeOptions as $,runConfiguredStep as b}from"../steps/run.js";import{announceResolvedToolchain as A,applyToolchainEnv as g,resolveConfiguredToolchain as L}from"../toolchains/index.js";import{resolveConfiguredCwd as U,resolveConfiguredPath as k,summarizeConfiguredStep as u}from"../steps/configured-steps.js";const M=3600*1e3;export async function runConfiguredSteps({config:e,steps:d=[],env:O,labelPrefix:P,ownedContainer:p=null,reporter:o=null,setupRegistry:i=null,parentOperation:T=null,startIndex:c=0,afterStep:f=null,excludeFromLimit:_=n=>n(),signal:l,stepTimeoutMs:w=M}){if(d.length===0)return;const n=await L(e,{signal:l});await A(e,n,o);for(const[h,t]of d.entries()){const v=c+h+1,a=`${P}:${e.name}:${String(v)}`,s=i?.start({config:e,stage:a,kind:"setup-step",summary:u(t),parentId:N(T)});o?.phaseStarted?.(a);try{const r=g($(O),n),S={livePrefix:`[${e.runtimeLabel||e.name}:${e.name}]`,liveWriter:o?.outputMode==="debug"?m=>{o.writeDebugLine?.(m)}:null,logRecord:s?._logRecord||null};await E(`running ${a}: ${u(t)}`,()=>_(async()=>{if(t.kind==="sql-file"){const m=await C({consumerSql:!0,databaseUrl:r.DATABASE_URL,args:[r.DATABASE_URL,"-v","ON_ERROR_STOP=1","-X","-f",k(e.productDir,t.cwd,t.path)],cwd:U(e.productDir,t.cwd),env:r,ownedContainer:p,signal:l,timeoutMs:w});R(m,S)}else await b(e,t,r,n,{...S,ownedContainer:p,signal:l,timeoutMs:w})})),y(s,{reporter:o,setupRegistry:i},{status:"passed",summary:u(t)})}catch(r){throw y(s,{reporter:o,setupRegistry:i},{status:"failed",summary:u(t),error:D(r,"message")||r}),r}f&&await f({step:t,index:c+h,stepNumber:v,label:a})}}function N(e){return typeof e=="string"?e||null:e?.id||null}
@@ -1,5 +1,3 @@
1
- import{dockerCliEnvironment as C,prepareDockerEndpoint as w}from"./docker-endpoint.js";import{runDockerMetadataCommand as p}from"./docker-metadata.js";import{managedHostProcessInfrastructureError as I,runManagedHostProcess as T}from"../process/index.js";import{canonicalizeProductDir as _,hashString as y}from"./naming.js";export const TESTKIT_MANAGED_LABEL="com.elench.testkit.managed",TESTKIT_RESOURCE_KIND_LABEL="com.elench.testkit.resource.kind",TESTKIT_RESOURCE_NAME_LABEL="com.elench.testkit.resource.name",TESTKIT_PRODUCT_DIR_LABEL="com.elench.testkit.product.dir",TESTKIT_PRODUCT_ID_LABEL="com.elench.testkit.product.id",TESTKIT_SCOPE_LABEL="com.elench.testkit.scope",TESTKIT_CACHE_POLICY_LABEL="com.elench.testkit.cache.policy",TESTKIT_CREATED_AT_LABEL="com.elench.testkit.created-at",TESTKIT_RESOURCE_GENERATION_LABEL="com.elench.testkit.resource.generation";const L=4e3,g=String;export const DOCKER_OWNERSHIP_OPERATION_TIMEOUTS_MS=Object.freeze({inspect:3e4,list:3e4,remove:12e4,stop:3e4});export function buildProductIdentity(e){const t=_(e||process.cwd());return{dir:t,id:y(t,24)}}export function buildDockerResourceLabels(e,t){if(!t.generation)throw new Error("Docker resource generation must be non-empty");const n=buildProductIdentity(e);return{[TESTKIT_MANAGED_LABEL]:"true",[TESTKIT_RESOURCE_KIND_LABEL]:t.kind,[TESTKIT_RESOURCE_NAME_LABEL]:t.name,[TESTKIT_PRODUCT_DIR_LABEL]:n.dir,[TESTKIT_PRODUCT_ID_LABEL]:n.id,[TESTKIT_SCOPE_LABEL]:t.scope||t.kind,[TESTKIT_CACHE_POLICY_LABEL]:t.cachePolicy||"ephemeral",[TESTKIT_CREATED_AT_LABEL]:t.createdAt||new Date().toISOString(),[TESTKIT_RESOURCE_GENERATION_LABEL]:t.generation}}export function dockerLabelArgs(e){return Object.entries(e).flatMap(([t,n])=>["--label",`${t}=${g(n)}`])}export async function inspectDockerContainer(e,t={}){const n=`Docker container inspect for ${e}`,o=["inspect",e],r=await p(o,n,DOCKER_OWNERSHIP_OPERATION_TIMEOUTS_MS.inspect,t.signal,c=>runDockerCommand(o,n,c,t.signal));if(r.exitCode!==0){if(k(r))return null;throw l(n,r)}return E(r,n,"Id")}export async function listManagedDockerContainers(e,t={}){return x(["--filter",`label=${TESTKIT_MANAGED_LABEL}=true`,"--filter",`label=${TESTKIT_PRODUCT_ID_LABEL}=${e.id}`],t.signal)}export async function inspectDockerVolume(e,t={}){const n=`Docker volume inspect for ${e}`,o=["volume","inspect",e],r=await p(o,n,DOCKER_OWNERSHIP_OPERATION_TIMEOUTS_MS.inspect,t.signal,c=>runDockerCommand(o,n,c,t.signal));if(r.exitCode!==0){if(b(r))return null;throw l(n,r)}return E(r,n,"Name")}export async function listManagedDockerVolumes(e,t={}){return A(["--filter",`label=${TESTKIT_MANAGED_LABEL}=true`,"--filter",`label=${TESTKIT_PRODUCT_ID_LABEL}=${e.id}`],t.signal)}export async function removeDockerContainer(e,t=e,n={}){const o=`Docker container removal for ${t}`,r=await runDockerCommand(["rm","-f","-v",e],o,DOCKER_OWNERSHIP_OPERATION_TIMEOUTS_MS.remove,n.signal);if(r.exitCode===0)return r.stdout.trim().length>0;if(k(r))return!1;throw l(o,r)}export async function removeDockerVolume(e,t,n={}){const o=normalizeDockerVolumeInspect(await inspectDockerVolume(e,n));if(o===null||!sameDockerVolumeGeneration(o,t))return!1;const r=`Docker volume removal for ${e}`,c=await runDockerCommand(["volume","rm","-f",e],r,DOCKER_OWNERSHIP_OPERATION_TIMEOUTS_MS.remove,n.signal);if(c.exitCode===0)return!0;if(b(c))return!1;throw l(r,c)}export function sameDockerVolumeGeneration(e,t){return e.name===t.name&&e.createdAt===t.createdAt&&e.driver===t.driver&&e.mountpoint===t.mountpoint&&O(e.labels,t.labels)}export async function stopDockerContainer(e,t=e,n={}){const o=`Docker container stop for ${t}`,r=await runDockerCommand(["stop","--time","5",e],o,DOCKER_OWNERSHIP_OPERATION_TIMEOUTS_MS.stop,n.signal);if(r.exitCode===0)return!0;if(k(r))return!1;throw l(o,r)}async function x(e,t){const n="Docker container inventory",o=["ps","-a","--format","{{.ID}}",...e],r=await p(o,n,DOCKER_OWNERSHIP_OPERATION_TIMEOUTS_MS.list,t,i=>runDockerCommand(o,n,i,t));if(r.exitCode!==0)throw l(n,r);const{stdout:c}=r,s=c.split(/\r?\n/).map(i=>i.trim()).filter(Boolean);if(s.length===0)return[];const a=[];for(const i of s){const m=await inspectDockerContainer(i,{signal:t}),d=normalizeDockerContainerInspect(m);d&&a.push(d)}return a}async function A(e,t){const n="Docker volume inventory",o=["volume","ls","-q",...e],r=await p(o,n,DOCKER_OWNERSHIP_OPERATION_TIMEOUTS_MS.list,t,i=>runDockerCommand(o,n,i,t));if(r.exitCode!==0)throw l(n,r);const{stdout:c}=r,s=c.split(/\r?\n/).map(i=>i.trim()).filter(Boolean);if(s.length===0)return[];const a=[];for(const i of s){const m=await inspectDockerVolume(i,{signal:t}),d=normalizeDockerVolumeInspect(m);d&&a.push(d)}return a}export function normalizeDockerContainerInspect(e){const t=u(e);if(!t||typeof t.Id!="string"||!t.Id)return null;const n=u(t.Config),o=u(t.State),r=g(t.Name||"").replace(/^\//,"");return{id:t.Id,shortId:t.Id.slice(0,12),name:r,image:f(n?.Image)||"",createdAt:f(t.Created),running:!!o?.Running,status:f(o?.Status)||"unknown",labels:D(n?.Labels)}}export function normalizeDockerVolumeInspect(e){const t=u(e);return!t||typeof t.Name!="string"||!t.Name?null:{name:t.Name,driver:f(t.Driver)||"",mountpoint:f(t.Mountpoint)||"",labels:D(t.Labels),createdAt:f(t.CreatedAt)}}function E(e,t,n){let o;try{o=JSON.parse(e.stdout)}catch(s){throw h(t,n,e,s)}const r=Array.isArray(o)?o:null,c=r?u(r[0]):null;if(r?.length!==1||!c||typeof c[n]!="string"||c[n].length===0)throw h(t,n,e);return c}function D(e){const t={},n=u(e);if(!n)return t;for(const[o,r]of Object.entries(n))typeof r=="string"&&(t[o]=r);return t}function O(e,t){const n=Object.entries(e).sort(([r],[c])=>r.localeCompare(c)),o=Object.entries(t).sort(([r],[c])=>r.localeCompare(c));return n.length===o.length&&n.every(([r,c],s)=>o[s]?.[0]===r&&o[s]?.[1]===c)}function u(e){return S(e)?e:null}function S(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function f(e){return typeof e=="string"&&e?e:null}export async function runDockerCommand(e,t,n,o,r){const c=await w(process.env,o).then(()=>!0,i=>{if(o?.aborted)return!1;throw i}),s=await T({args:e,cwd:process.cwd(),displayLabel:t,env:c?C():process.env,executable:"docker",signal:o,timeoutMs:n,workPurpose:r}),a=I(t,s);if(a)throw a;return s}function k(e){const t=`${e.stderr}
2
- ${e.stdout}`.toLowerCase();return/\bno such (?:container|object)\b/u.test(t)}function b(e){const t=`${e.stderr}
3
- ${e.stdout}`.toLowerCase();return/\bno such (?:object|volume)\b/u.test(t)}function l(e,t){const n=[t.stderr,t.stdout].filter(Boolean).join(`
4
- `).trim(),o=n?`
5
- ${n.slice(-L)}`:"",r=new Error(`${e} exited with code ${String(t.exitCode)}${o}`);return Object.defineProperty(r,"result",{enumerable:!1,value:Object.freeze({...t})}),r}function h(e,t,n,o){const r=new Error(`${e} returned malformed Docker inspect output; expected exactly one record with a non-empty ${t}`,o===void 0?void 0:{cause:o});return Object.defineProperty(r,"result",{enumerable:!1,value:Object.freeze({...n})}),r}
1
+ import{dockerCliEnvironment as C,prepareDockerEndpoint as I}from"./docker-endpoint.js";import{runDockerMetadataCommand as f}from"./docker-metadata.js";import{managedHostProcessInfrastructureError as _,requireManagedHostCommandSuccess as i,runManagedHostProcess as h}from"../process/index.js";import{canonicalizeProductDir as x,hashString as y}from"./naming.js";export const TESTKIT_MANAGED_LABEL="com.elench.testkit.managed",TESTKIT_RESOURCE_KIND_LABEL="com.elench.testkit.resource.kind",TESTKIT_RESOURCE_NAME_LABEL="com.elench.testkit.resource.name",TESTKIT_PRODUCT_DIR_LABEL="com.elench.testkit.product.dir",TESTKIT_PRODUCT_ID_LABEL="com.elench.testkit.product.id",TESTKIT_SCOPE_LABEL="com.elench.testkit.scope",TESTKIT_CACHE_POLICY_LABEL="com.elench.testkit.cache.policy",TESTKIT_CREATED_AT_LABEL="com.elench.testkit.created-at",TESTKIT_RESOURCE_GENERATION_LABEL="com.elench.testkit.resource.generation";const g=String;export const DOCKER_OWNERSHIP_OPERATION_TIMEOUTS_MS=Object.freeze({inspect:3e4,list:3e4,remove:12e4,stop:3e4});export function buildProductIdentity(t){const e=x(t||process.cwd());return{dir:e,id:y(e,24)}}export function buildDockerResourceLabels(t,e){if(!e.generation)throw new Error("Docker resource generation must be non-empty");const r=buildProductIdentity(t);return{[TESTKIT_MANAGED_LABEL]:"true",[TESTKIT_RESOURCE_KIND_LABEL]:e.kind,[TESTKIT_RESOURCE_NAME_LABEL]:e.name,[TESTKIT_PRODUCT_DIR_LABEL]:r.dir,[TESTKIT_PRODUCT_ID_LABEL]:r.id,[TESTKIT_SCOPE_LABEL]:e.scope||e.kind,[TESTKIT_CACHE_POLICY_LABEL]:e.cachePolicy||"ephemeral",[TESTKIT_CREATED_AT_LABEL]:e.createdAt||new Date().toISOString(),[TESTKIT_RESOURCE_GENERATION_LABEL]:e.generation}}export function dockerLabelArgs(t){return Object.entries(t).flatMap(([e,r])=>["--label",`${e}=${g(r)}`])}export async function inspectDockerContainer(t,e={}){const r=`Docker container inspect for ${t}`,o=["inspect",t],n=await f(o,r,DOCKER_OWNERSHIP_OPERATION_TIMEOUTS_MS.inspect,e.signal,c=>runDockerCommand(o,r,c,e.signal));return n.exitCode!==0&&isMissingDockerContainer(n)?null:(i(r,n),E(n,r,"Id"))}export async function listManagedDockerContainers(t,e={}){return k(t,["ps","-a","--format","{{.ID}}"],"Docker container inventory",e.signal,async r=>normalizeDockerContainerInspect(await inspectDockerContainer(r,e)))}export async function inspectDockerVolume(t,e={}){const r=`Docker volume inspect for ${t}`,o=["volume","inspect",t],n=await f(o,r,DOCKER_OWNERSHIP_OPERATION_TIMEOUTS_MS.inspect,e.signal,c=>runDockerCommand(o,r,c,e.signal));return n.exitCode!==0&&b(n)?null:(i(r,n),E(n,r,"Name"))}export async function listManagedDockerVolumes(t,e={}){return k(t,["volume","ls","-q"],"Docker volume inventory",e.signal,async r=>normalizeDockerVolumeInspect(await inspectDockerVolume(r,e)))}export async function removeDockerContainer(t,e=t,r={}){const o=`Docker container removal for ${e}`,n=await runDockerCommand(["rm","-f","-v",t],o,DOCKER_OWNERSHIP_OPERATION_TIMEOUTS_MS.remove,r.signal);return n.exitCode!==0&&isMissingDockerContainer(n)?!1:i(o,n).stdout.trim().length>0}export async function removeDockerVolume(t,e,r={}){const o=normalizeDockerVolumeInspect(await inspectDockerVolume(t,r));if(o===null||!sameDockerVolumeGeneration(o,e))return!1;const n=`Docker volume removal for ${t}`,c=await runDockerCommand(["volume","rm","-f",t],n,DOCKER_OWNERSHIP_OPERATION_TIMEOUTS_MS.remove,r.signal);return c.exitCode!==0&&b(c)?!1:(i(n,c),!0)}export function sameDockerVolumeGeneration(t,e){return t.name===e.name&&t.createdAt===e.createdAt&&t.driver===e.driver&&t.mountpoint===e.mountpoint&&sameStringRecord(t.labels,e.labels)}export async function stopDockerContainer(t,e=t,r={}){const o=`Docker container stop for ${e}`,n=await runDockerCommand(["stop","--time","5",t],o,DOCKER_OWNERSHIP_OPERATION_TIMEOUTS_MS.stop,r.signal);return n.exitCode!==0&&isMissingDockerContainer(n)?!1:(i(o,n),!0)}async function k(t,e,r,o,n){const c=[...e,"--filter",`label=${TESTKIT_MANAGED_LABEL}=true`,"--filter",`label=${TESTKIT_PRODUCT_ID_LABEL}=${t.id}`],s=await f(c,r,DOCKER_OWNERSHIP_OPERATION_TIMEOUTS_MS.list,o,l=>runDockerCommand(c,r,l,o));i(r,s);const p=s.stdout.split(/\r?\n/).map(l=>l.trim()).filter(Boolean),d=[];for(const l of p){const m=await n(l);m&&d.push(m)}return d}export function normalizeDockerContainerInspect(t){const e=a(t);if(!e||typeof e.Id!="string"||!e.Id)return null;const r=a(e.Config),o=a(e.State),n=g(e.Name||"").replace(/^\//,"");return{id:e.Id,shortId:e.Id.slice(0,12),name:n,image:u(r?.Image)||"",createdAt:u(e.Created),running:!!o?.Running,status:u(o?.Status)||"unknown",labels:D(r?.Labels)}}export function normalizeDockerVolumeInspect(t){const e=a(t);return!e||typeof e.Name!="string"||!e.Name?null:{name:e.Name,driver:u(e.Driver)||"",mountpoint:u(e.Mountpoint)||"",labels:D(e.Labels),createdAt:u(e.CreatedAt)}}function E(t,e,r){let o;try{o=JSON.parse(t.stdout)}catch(s){throw T(e,r,t,s)}const n=Array.isArray(o)?o:null,c=n?a(n[0]):null;if(n?.length!==1||!c||typeof c[r]!="string"||c[r].length===0)throw T(e,r,t);return c}function D(t){const e={},r=a(t);if(!r)return e;for(const[o,n]of Object.entries(r))typeof n=="string"&&(e[o]=n);return e}export function sameStringRecord(t,e){const r=Object.entries(t).sort(([n],[c])=>n.localeCompare(c)),o=Object.entries(e).sort(([n],[c])=>n.localeCompare(c));return r.length===o.length&&r.every(([n,c],s)=>o[s]?.[0]===n&&o[s]?.[1]===c)}function a(t){return w(t)?t:null}function w(t){return typeof t=="object"&&t!==null&&!Array.isArray(t)}function u(t){return typeof t=="string"&&t?t:null}export async function runDockerCommand(t,e,r,o,n){const c=await I(process.env,o).then(()=>!0,d=>{if(o?.aborted)return!1;throw d}),s=await h({args:t,cwd:process.cwd(),displayLabel:e,env:c?C():process.env,executable:"docker",signal:o,timeoutMs:r,workPurpose:n}),p=_(e,s);if(p)throw p;return s}export function isMissingDockerContainer(t){const e=`${t.stderr}
2
+ ${t.stdout}`.toLowerCase();return/\bno such (?:container|object)\b/u.test(e)}function b(t){const e=`${t.stderr}
3
+ ${t.stdout}`.toLowerCase();return/\bno such (?:object|volume)\b/u.test(e)}function T(t,e,r,o){const n=new Error(`${t} returned malformed Docker inspect output; expected exactly one record with a non-empty ${e}`,o===void 0?void 0:{cause:o});return Object.defineProperty(n,"result",{enumerable:!1,value:Object.freeze({...r})}),n}
@@ -1,2 +1,2 @@
1
- import{requireContainedTestkitStateDirectory as k}from"../process/runs/product-state.js";export{cleanupOrphanedLocalInfrastructure}from"./lifecycle-cleanup.js";import p from"fs";import c from"path";import{computeTemplateFingerprint as $}from"./fingerprint.js";import{buildDatabaseUrl as v,buildRuntimeDatabaseName as B,buildTaskDatabaseName as V,buildTemplateDatabaseName as U,hashString as W}from"./naming.js";import{getLocalInfraDir as K,getLocalLocksDir as F,getLocalServiceCacheDir as T,readStateValue as y}from"./state.js";import{runTemplateStage as z,runTemplateStep as H}from"./template-steps.js";import{materializeDatabaseConfig as Q}from"../config/loader/index.js";import{applySourceSchemaCache as G,createSourceSchemaMismatchError as J,forceRefreshSourceSchemaCache as X,prepareSourceSchemaCache as Y,verifyLocalSchemaMatchesSource as Z}from"./schema-source.js";import{cloneDatabaseFromTemplate as C,createEmptyDatabase as ee,databaseExists as x,dropDatabaseWithForceOrDrain as h}from"./admin.js";import{withUserHostLocalPostgresActivity as S}from"./activity-gate.js";import{localPostgresProvider as D}from"./provider.js";import{beginTemplateCacheRebuild as ae,writeCacheState as P,writeStateValue as w}from"./state-files.js";import{withProcessLock as j,withProcessLockProgress as g}from"../process/locks.js";import{errorMessage as E}from"../kernel/error-serialization.js";import{ensureDatabaseRoles as te}from"./roles.js";import{cleanupLocalPostgresDockerResources as re,destroyLocalPostgresDockerResources as se}from"./cleanup.js";import{requireProductLifecycleFenceProductDir as A}from"../process/lifecycle-fence.js";export{dropDatabaseWithForceOrDrain,isTransientAdminQueryConnectionError,waitForDatabaseConnectionsToDrain}from"./admin.js";export{refreshDatabaseSchema,verifyDatabaseSchema}from"./schema-operations.js";const R="task-databases";export async function provisionDatabase(e,a={}){const r=e.testkit.database;if(!r)return null;const s=Q(r),t={...e,stateDir:k(e.productDir,e.stateDir,`Service "${e.name}" database runtime state directory`),testkit:{...e.testkit,database:s}},n={cleanupSignal:a.cleanupSignal,excludeFromLimit:a.excludeFromLimit,reporter:a.progress?.reporter||null,setupRegistry:a.progress?.setupRegistry||null,signal:a.signal,skipSchemaSourceVerify:a.skipSchemaSourceVerify};return S(t.productDir,()=>g(`preparing the database for ${t.name}`,async()=>(p.mkdirSync(t.stateDir,{recursive:!0}),ce(t,n,a.provisionedSharedDatabases))),{signal:a.signal})}export async function destroyRuntimeDatabase({productDir:e,stateDir:a},r={}){const s=k(e,a,"Runtime database state directory");await S(e,async()=>{await pe(e,s,r.signal)},r)}export async function destroyServiceDatabaseCache(e,a,r={}){await S(e,()=>ne(e,a),r)}async function ne(e,a){const r=T(e,a);if(!p.existsSync(r))return;const s=F(e);p.mkdirSync(s,{recursive:!0}),await j(c.join(s,`template-${a}.lock`),async()=>{const t=y(c.join(r,"template_database_name")),n=t?await D.load(e):null;n&&t&&await h(n,t),p.rmSync(r,{recursive:!0,force:!0})})}export async function destroyOwnedLocalDatabaseResources(e,a={}){const r=A(e);return S(r,()=>(A(e),se(e,a)),{signal:a.signal})}export async function cleanupOwnedLocalDatabaseResources(e,a={}){return S(e,()=>ie(e,a))}async function ie(e,a){return re({productDir:e,dryRun:!!a.dryRun,stopIdle:a.stopIdle!==!1})}export async function stopRetainedLocalPostgres(e){await S(e,async()=>{const a=y(c.join(K(e),"container_name"));if(a&&!await D.stopRetained(e,a))throw new Error(`Could not stop the retained database container ${a}: it no longer exists`)})}export function isDatabaseStateDir(e){return p.existsSync(e)?p.existsSync(c.join(e,"database_backend")):!1}async function ce(e,a={},r){const s=e.testkit.database,t=e.productDir,n=e.name,u=de(e),l=F(t),i=T(t,n);p.mkdirSync(l,{recursive:!0}),p.mkdirSync(i,{recursive:!0});const o=await g(`starting local Postgres for ${n}`,()=>D.ensure(t,s,{signal:a.signal})),d=await j(c.join(l,`template-${n}.lock`),()=>g(`preparing the template for ${n}`,async()=>{const b=await Y(e,a),_=await $(e,{sourceSchemaState:b});return await te(e,o,a),oe(e,o,i,_,{...a,sourceSchemaState:b})}),{signal:a.signal,timeoutMs:null}),m=M(s.binding)==="shared"?r:void 0,f=await j(c.join(l,`runtime-${n}-${W(u,10)}.lock`),()=>g(`cloning the runtime database for ${n}`,()=>me(e,o,d,u,m,a.signal)),{signal:a.signal,timeoutMs:null});return{container:o.containerName,databaseName:f.databaseName,templateFingerprint:f.templateFingerprint,url:v(o,f.databaseName)}}async function oe(e,a,r,s,t={}){const n=e.name;let u=s,l=t.sourceSchemaState||null,i=!1;for(;;){const o=y(c.join(r,"template_fingerprint")),d=y(c.join(r,"template_database_name")),m=U(n,u);if(o===u&&d&&await x(a,d,{signal:t.signal}))return t.setupRegistry?.recordCached({config:e,stage:"template",kind:"database-template",summary:"template cache hit"}),P(r,a,d,u),{databaseName:d,fingerprint:u};d&&d!==m&&await h(a,d,{signal:t.signal}),await x(a,m,{signal:t.signal})&&await h(a,m,{signal:t.signal}),ae(r,m);const f=v(a,m);if(await ee(a,m,{signal:t.signal}),(await ue(e,a,m,f,{...t,sourceSchemaState:l,refreshedSourceAfterMismatch:i})).refreshSourceSchema){await h(a,m,{signal:t.signal}),l=await X(e,l,t),i=!0,u=await $(e,{sourceSchemaState:l});continue}return P(r,a,m,u),{databaseName:m,fingerprint:u}}}async function ue(e,a,r,s,t={}){const n=t.setupRegistry?.start({config:e,stage:"template",kind:"database-template",summary:"template rebuild",recordLog:!1}),u={ownedContainer:{containerId:a.containerId,containerPort:a.containerPort,image:a.image},parentOperation:n,reporter:t.reporter||null,setupRegistry:t.setupRegistry||null,signal:t.signal},l={...u,excludeFromLimit:t.excludeFromLimit};try{await G(e,s,t.sourceSchemaState,u);const i=async()=>{const b=await Z(e,s,t.sourceSchemaState,{...u,skipSchemaSourceVerify:t.skipSchemaSourceVerify});if(b.status!=="mismatch")return null;if(t.refreshedSourceAfterMismatch)throw J(e,be(t.sourceSchemaState),b);return b},o=e.testkit.database?.template,d=!!(o?.migrate?.length||o?.seed?.length),m=async b=>{const _=o?.[b]||[];for(const[I,q]of _.entries())if(await H(e,b,q,I,s,l),await i())return!0;return!1};return await m("migrate")||await m("seed")||!d&&!!await i()?(N(n,t,{status:"passed",summary:"source schema refresh requested"}),{refreshSourceSchema:!0}):(await z(e,"verify",s,l),N(n,t,{status:"passed",summary:"template rebuild"}),{refreshSourceSchema:!1})}catch(i){throw N(n,t,{status:"failed",summary:"template rebuild",error:he(i)||i}),await O(a,r,"template database",i,t.cleanupSignal?.()),i}}function N(e,a,r){const s=e&&a.setupRegistry?a.setupRegistry.finish(e,r):null;s&&a.reporter?.setupOperationFinished?.(s)}async function me(e,a,r,s,t,n){const u=e.name,l=r.fingerprint,i=B(u,s,l);await L(a,e.stateDir,n);const o=y(c.join(e.stateDir,"local_database_name")),d=y(c.join(e.stateDir,"local_template_fingerprint")),m=!t?.has(i)&&(fe(e.testkit.database.reset)||d!==l||o!==i)||!await x(a,i,{signal:n});o&&o!==i&&await h(a,o,{signal:n}),m&&(await h(a,i,{signal:n}),w(c.join(e.stateDir,"local_database_name"),i),await C(a,i,r.databaseName,{signal:n})),t?.add(i),p.mkdirSync(e.stateDir,{recursive:!0});const f={bindingKey:s,databaseName:i,templateDatabaseName:r.databaseName,templateFingerprint:l};return le(e,a,f),f}function le(e,a,r){w(c.join(e.stateDir,"database_backend"),""),w(c.join(e.stateDir,"local_database_name"),r.databaseName),w(c.join(e.stateDir,"local_template_fingerprint"),r.templateFingerprint),w(c.join(e.stateDir,"local_template_database_name"),r.templateDatabaseName),w(c.join(e.stateDir,"local_container_name"),a.containerName),a.volumeName&&w(c.join(e.stateDir,"volume_name"),a.volumeName),a.volumeMountPath&&w(c.join(e.stateDir,"volume_mount_path"),a.volumeMountPath)}function de(e){return M(e.testkit.database.binding)==="shared"?`${e.name}:shared`:e.runtimeId?`${e.name}:${String(e.runtimeId)}`:`${e.name}:${e.stateDir}`}export function readRuntimeDatabaseName(e){return y(c.join(e,"local_database_name"))}async function pe(e,a,r){const s=readRuntimeDatabaseName(a);s&&await g(`dropping the runtime database ${s}`,async()=>{const t=await D.load(e,{signal:r});t&&(await L(t,a,r),await h(t,s,{signal:r}))})}export async function cloneTaskDatabase({productDir:e,stateDir:a},r,s,t={}){const n=k(e,a,"Runtime database state directory");return S(e,async()=>{const u=y(c.join(n,"local_database_name")),l=y(c.join(n,"local_template_database_name")),i=await D.load(e,{signal:t.signal});if(!u||!l||!i)throw new Error(`Service "${r}" has no provisioned database to clone for this task`);const o=V(r,u,s),d=c.join(n,R,o);p.mkdirSync(c.dirname(d),{recursive:!0}),p.writeFileSync(d,"");try{await C(i,o,l,{signal:t.signal})}catch(m){throw await O(i,o,"task database",m),p.rmSync(d,{force:!0}),m}return{databaseName:o,url:v(i,o)}},{signal:t.signal})}export async function dropTaskDatabase({productDir:e,stateDir:a},r){const s=k(e,a,"Runtime database state directory");await S(e,async()=>{const t=await D.load(e);t&&await h(t,r),p.rmSync(c.join(s,R,r),{force:!0})})}export async function dropRecordedTaskDatabases({productDir:e,stateDir:a}){const r=k(e,a,"Runtime database state directory");p.existsSync(c.join(r,R))&&await S(e,async()=>{const s=await D.load(e);s&&await L(s,r)})}async function O(e,a,r,s,t){let n;try{await g(`dropping the failed ${r} ${a}`,()=>h(e,a,{signal:t}));return}catch(u){n=u}throw new AggregateError([s,n],`${E(s).trimEnd()}
2
- Dropping the ${r} ${a} also failed: ${E(n).trim()}`,{cause:s})}async function L(e,a,r){const s=c.join(a,R);if(p.existsSync(s))for(const t of p.readdirSync(s))await h(e,t,{signal:r}),p.rmSync(c.join(s,t),{force:!0})}function be(e){if(!e?.active)throw new Error("Source schema mismatch requires active source schema state");return e}function he(e){return typeof e!="object"&&typeof e!="function"||e===null?void 0:Reflect.get(e,"message")}function fe(e){return e!==!1}function M(e){return e||"per-runtime"}function Ee(e){return e}
1
+ import{requireContainedTestkitStateDirectory as k}from"../process/runs/product-state.js";export{cleanupOrphanedLocalInfrastructure}from"./lifecycle-cleanup.js";import p from"fs";import c from"path";import{computeTemplateFingerprint as $}from"./fingerprint.js";import{buildDatabaseUrl as v,buildRuntimeDatabaseName as M,buildTaskDatabaseName as q,buildTemplateDatabaseName as V,hashString as B}from"./naming.js";import{getLocalInfraDir as U,getLocalLocksDir as F,getLocalServiceCacheDir as T,readStateValue as y}from"./state.js";import{runTemplateStage as W,runTemplateStep as K}from"./template-steps.js";import{materializeDatabaseConfig as z}from"../config/loader/index.js";import{applySourceSchemaCache as H,createSourceSchemaMismatchError as Q,forceRefreshSourceSchemaCache as G,prepareSourceSchemaCache as J,verifyLocalSchemaMatchesSource as X}from"./schema-source.js";import{cloneDatabaseFromTemplate as C,createEmptyDatabase as Y,databaseExists as x,dropDatabaseWithForceOrDrain as h}from"./admin.js";import{withUserHostLocalPostgresActivity as f}from"./activity-gate.js";import{localPostgresProvider as D}from"./provider.js";import{beginTemplateCacheRebuild as Z,writeCacheState as P,writeStateValue as w}from"./state-files.js";import{withProcessLock as N,withProcessLockProgress as g}from"../process/locks.js";import{errorMessage as A}from"../kernel/error-serialization.js";import{readDynamicProperty as ee}from"../kernel/unknown-values.js";import{ensureDatabaseRoles as ae}from"./roles.js";import{cleanupLocalPostgresDockerResources as te,destroyLocalPostgresDockerResources as re}from"./cleanup.js";import{finishSetupOperation as j}from"./progress.js";import{requireProductLifecycleFenceProductDir as E}from"../process/lifecycle-fence.js";export{dropDatabaseWithForceOrDrain,isTransientAdminQueryConnectionError,waitForDatabaseConnectionsToDrain}from"./admin.js";export{refreshDatabaseSchema,verifyDatabaseSchema}from"./schema-operations.js";const R="task-databases";export async function provisionDatabase(e,a={}){const r=e.testkit.database;if(!r)return null;const s=z(r),t={...e,stateDir:k(e.productDir,e.stateDir,`Service "${e.name}" database runtime state directory`),testkit:{...e.testkit,database:s}},n={cleanupSignal:a.cleanupSignal,excludeFromLimit:a.excludeFromLimit,reporter:a.progress?.reporter||null,setupRegistry:a.progress?.setupRegistry||null,signal:a.signal,skipSchemaSourceVerify:a.skipSchemaSourceVerify};return f(t.productDir,()=>g(`preparing the database for ${t.name}`,async()=>(p.mkdirSync(t.stateDir,{recursive:!0}),ie(t,n,a.provisionedSharedDatabases))),{signal:a.signal})}export async function destroyRuntimeDatabase({productDir:e,stateDir:a},r={}){const s=k(e,a,"Runtime database state directory");await f(e,async()=>{await de(e,s,r.signal)},r)}export async function destroyServiceDatabaseCache(e,a,r={}){await f(e,()=>se(e,a),r)}async function se(e,a){const r=T(e,a);if(!p.existsSync(r))return;const s=F(e);p.mkdirSync(s,{recursive:!0}),await N(c.join(s,`template-${a}.lock`),async()=>{const t=y(c.join(r,"template_database_name")),n=t?await D.load(e):null;n&&t&&await h(n,t),p.rmSync(r,{recursive:!0,force:!0})})}export async function destroyOwnedLocalDatabaseResources(e,a={}){const r=E(e);return f(r,()=>(E(e),re(e,a)),{signal:a.signal})}export async function cleanupOwnedLocalDatabaseResources(e,a={}){return f(e,()=>ne(e,a))}async function ne(e,a){return te({productDir:e,dryRun:!!a.dryRun,stopIdle:a.stopIdle!==!1})}export async function stopRetainedLocalPostgres(e){await f(e,async()=>{const a=y(c.join(U(e),"container_name"));if(a&&!await D.stopRetained(e,a))throw new Error(`Could not stop the retained database container ${a}: it no longer exists`)})}export function isDatabaseStateDir(e){return p.existsSync(e)?p.existsSync(c.join(e,"database_backend")):!1}async function ie(e,a={},r){const s=e.testkit.database,t=e.productDir,n=e.name,m=ue(e),u=F(t),i=T(t,n);p.mkdirSync(u,{recursive:!0}),p.mkdirSync(i,{recursive:!0});const o=await g(`starting local Postgres for ${n}`,()=>D.ensure(t,s,{signal:a.signal})),d=await N(c.join(u,`template-${n}.lock`),()=>g(`preparing the template for ${n}`,async()=>{const b=await J(e,a),_=await $(e,{sourceSchemaState:b});return await ae(e,o,a),ce(e,o,i,_,{...a,sourceSchemaState:b})}),{signal:a.signal,timeoutMs:null}),l=s.binding==="shared"?r:void 0,S=await N(c.join(u,`runtime-${n}-${B(m,10)}.lock`),()=>g(`cloning the runtime database for ${n}`,()=>me(e,o,d,m,l,a.signal)),{signal:a.signal,timeoutMs:null});return{container:o.containerName,databaseName:S.databaseName,templateFingerprint:S.templateFingerprint,url:v(o,S.databaseName)}}async function ce(e,a,r,s,t={}){const n=e.name;let m=s,u=t.sourceSchemaState||null,i=!1;for(;;){const o=y(c.join(r,"template_fingerprint")),d=y(c.join(r,"template_database_name")),l=V(n,m);if(o===m&&d&&await x(a,d,{signal:t.signal}))return t.setupRegistry?.recordCached({config:e,stage:"template",kind:"database-template",summary:"template cache hit"}),P(r,a,d,m),{databaseName:d,fingerprint:m};d&&d!==l&&await h(a,d,{signal:t.signal}),await x(a,l,{signal:t.signal})&&await h(a,l,{signal:t.signal}),Z(r,l);const S=v(a,l);if(await Y(a,l,{signal:t.signal}),(await oe(e,a,l,S,{...t,sourceSchemaState:u,refreshedSourceAfterMismatch:i})).refreshSourceSchema){await h(a,l,{signal:t.signal}),u=await G(e,u,t),i=!0,m=await $(e,{sourceSchemaState:u});continue}return P(r,a,l,m),{databaseName:l,fingerprint:m}}}async function oe(e,a,r,s,t={}){const n=t.setupRegistry?.start({config:e,stage:"template",kind:"database-template",summary:"template rebuild",recordLog:!1}),m={ownedContainer:{containerId:a.containerId,containerPort:a.containerPort,image:a.image},parentOperation:n,reporter:t.reporter||null,setupRegistry:t.setupRegistry||null,signal:t.signal},u={...m,excludeFromLimit:t.excludeFromLimit};try{await H(e,s,t.sourceSchemaState,m);const i=async()=>{const b=await X(e,s,t.sourceSchemaState,{...m,skipSchemaSourceVerify:t.skipSchemaSourceVerify});if(b.status!=="mismatch")return null;if(t.refreshedSourceAfterMismatch)throw Q(e,pe(t.sourceSchemaState),b);return b},o=e.testkit.database.template,d=o.migrate.length>0||o.seed.length>0,l=async b=>{for(const[_,I]of o[b].entries())if(await K(e,b,I,_,s,u),await i())return!0;return!1};return await l("migrate")||await l("seed")||!d&&!!await i()?(j(n,t,{status:"passed",summary:"source schema refresh requested"}),{refreshSourceSchema:!0}):(await W(e,"verify",s,u),j(n,t,{status:"passed",summary:"template rebuild"}),{refreshSourceSchema:!1})}catch(i){throw j(n,t,{status:"failed",summary:"template rebuild",error:ee(i,"message")||i}),await O(a,r,"template database",i,t.cleanupSignal?.()),i}}async function me(e,a,r,s,t,n){const m=e.name,u=r.fingerprint,i=M(m,s,u);await L(a,e.stateDir,n);const o=y(c.join(e.stateDir,"local_database_name")),d=y(c.join(e.stateDir,"local_template_fingerprint")),l=!t?.has(i)&&(e.testkit.database.reset||d!==u||o!==i)||!await x(a,i,{signal:n});o&&o!==i&&await h(a,o,{signal:n}),l&&(await h(a,i,{signal:n}),w(c.join(e.stateDir,"local_database_name"),i),await C(a,i,r.databaseName,{signal:n})),t?.add(i),p.mkdirSync(e.stateDir,{recursive:!0});const S={bindingKey:s,databaseName:i,templateDatabaseName:r.databaseName,templateFingerprint:u};return le(e,a,S),S}function le(e,a,r){w(c.join(e.stateDir,"database_backend"),""),w(c.join(e.stateDir,"local_database_name"),r.databaseName),w(c.join(e.stateDir,"local_template_fingerprint"),r.templateFingerprint),w(c.join(e.stateDir,"local_template_database_name"),r.templateDatabaseName),w(c.join(e.stateDir,"local_container_name"),a.containerName),a.volumeName&&w(c.join(e.stateDir,"volume_name"),a.volumeName),a.volumeMountPath&&w(c.join(e.stateDir,"volume_mount_path"),a.volumeMountPath)}function ue(e){return e.testkit.database.binding==="shared"?`${e.name}:shared`:e.runtimeId?`${e.name}:${String(e.runtimeId)}`:`${e.name}:${e.stateDir}`}export function readRuntimeDatabaseName(e){return y(c.join(e,"local_database_name"))}async function de(e,a,r){const s=readRuntimeDatabaseName(a);s&&await g(`dropping the runtime database ${s}`,async()=>{const t=await D.load(e,{signal:r});t&&(await L(t,a,r),await h(t,s,{signal:r}))})}export async function cloneTaskDatabase({productDir:e,stateDir:a},r,s,t={}){const n=k(e,a,"Runtime database state directory");return f(e,async()=>{const m=y(c.join(n,"local_database_name")),u=y(c.join(n,"local_template_database_name")),i=await D.load(e,{signal:t.signal});if(!m||!u||!i)throw new Error(`Service "${r}" has no provisioned database to clone for this task`);const o=q(r,m,s),d=c.join(n,R,o);p.mkdirSync(c.dirname(d),{recursive:!0}),p.writeFileSync(d,"");try{await C(i,o,u,{signal:t.signal})}catch(l){throw await O(i,o,"task database",l),p.rmSync(d,{force:!0}),l}return{databaseName:o,url:v(i,o)}},{signal:t.signal})}export async function dropTaskDatabase({productDir:e,stateDir:a},r){const s=k(e,a,"Runtime database state directory");await f(e,async()=>{const t=await D.load(e);t&&await h(t,r),p.rmSync(c.join(s,R,r),{force:!0})})}export async function dropRecordedTaskDatabases({productDir:e,stateDir:a}){const r=k(e,a,"Runtime database state directory");p.existsSync(c.join(r,R))&&await f(e,async()=>{const s=await D.load(e);s&&await L(s,r)})}async function O(e,a,r,s,t){let n;try{await g(`dropping the failed ${r} ${a}`,()=>h(e,a,{signal:t}));return}catch(m){n=m}throw new AggregateError([s,n],`${A(s).trimEnd()}
2
+ Dropping the ${r} ${a} also failed: ${A(n).trim()}`,{cause:s})}async function L(e,a,r){const s=c.join(a,R);if(p.existsSync(s))for(const t of p.readdirSync(s))await h(e,t,{signal:r}),p.rmSync(c.join(s,t),{force:!0})}function pe(e){if(!e?.active)throw new Error("Source schema mismatch requires active source schema state");return e}
@@ -1,5 +1,4 @@
1
- import{recordWork as R}from"../kernel/work-metrics.js";import{redactSensitiveText as Pe}from"../kernel/redaction.js";import{orderRedactionTargets as Ae,replaceRedactionTargets as ke}from"../kernel/redaction-targets.js";import{dockerEndpoint as xe}from"./docker-endpoint.js";import{runDockerMetadataCommand as ye}from"./docker-metadata.js";import{randomUUID as L}from"node:crypto";import Te from"fs";import f from"path";import{LOCAL_ADMIN_DB as N,LOCAL_IMAGE as H,LOCAL_MAX_CONNECTIONS as W,LOCAL_PASSWORD as K,LOCAL_EXEC_POLL_INTERVAL_MS as q,LOCAL_READY_EXEC_FALLBACK_AFTER_MS as Se,LOCAL_POLL_INTERVAL_MS as _e,LOCAL_READY_ATTEMPT_TIMEOUT_MS as De,LOCAL_READY_TIMEOUT_MS as P,LOCAL_USER as X}from"./constants.js";import{buildContainerName as $e,buildDatabaseUrl as Ie,buildVolumeName as T,isProductVolumeName as Oe}from"./naming.js";import{waitForPostgresConnection as Me}from"./postgres/connection-check.js";import{NotPostgresError as Re,PostgresCheckError as Ne,PostgresConnectionError as Ve,PostgresRefusedError as je,PostgresUnansweredError as ve}from"./postgres/protocol.js";import{getLocalInfraDir as Y,readStateValue as d}from"./state.js";import{TESTKIT_CREATED_AT_LABEL as J,TESTKIT_RESOURCE_GENERATION_LABEL as Q,buildDockerResourceLabels as Z,dockerLabelArgs as ee,inspectDockerVolume as S,normalizeDockerVolumeInspect as V,removeDockerVolume as Ue,runDockerCommand as C,stopDockerContainer as ze}from"./docker.js";import{writeLocalInfraState as te}from"./state-files.js";import{isUnknownArray as A,isUnknownRecord as m}from"../kernel/unknown-values.js";const ne=String;export const LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS=Object.freeze({inspect:3e4,readinessAttempt:1e4,remove:12e4,run:10*6e4,start:12e4,volumeCreate:12e4});class oe extends Error{container;retainedVolume;reason;constructor(t,n,r,o){super(`Local database container ${t.name} exited while starting${r===null?"":` with exit code ${String(r)}`}: ${o}`),this.container=t,this.retainedVolume=n,this.reason=o}}const Ge=1e3;export async function ensureLocalContainer(e,t={},n={}){try{return await re(e,t,n)}catch(r){if(!(r instanceof oe)||r.retainedVolume===null)throw r;return await j(e,r.retainedVolume,n),(n.notify??Be)(`Testkit is replacing database volume ${r.retainedVolume}, because ${r.message}. The new volume starts with an empty cluster, so its template is rebuilt.`),await stopAndRemoveContainer(r.container.id,r.container.name,n),await v(e,r.retainedVolume,n),re(e,t,n)}}function Be(e){process.stderr.write(`[testkit] ${e}
2
- `)}async function re(e,t,n){const r=Y(e);Te.mkdirSync(r,{recursive:!0});const o=d(f.join(r,"container_name"))||$e(e),i=d(f.join(r,"volume_name")),a=t.image||d(f.join(r,"image"))||H,c=d(f.join(r,"image")),u=t.user||d(f.join(r,"user"))||X,l=t.password||d(f.join(r,"password"))||K,g=se(t.maxConnections||d(f.join(r,"max_connections"))||W);let s=await inspectContainer(o,n);s&&E(s,e,o);const Ee=!s&&!i,z=ce(s,e,i);let w=z?.name||i||T(e,L());s&&await j(e,w,n);const $=s?.Config?.Image||c||null,h=z?.destination||d(f.join(r,"volume_mount_path")),I=s?.Config?.Image===a?s.Config:null;let b=postgresDataVolumeTarget(a,I??await fe(a,n),h);const G=()=>!!h&&h!==b&&($??a)===a;if(s&&!isReusableLocalPostgresContainer(s,{image:a,maxConnections:g,volumeMountPath:b,volumeName:w})){if(I!==null&&(b=postgresDataVolumeTarget(a,await fe(a,n),h),h&&h!==b&&holdsPostgresData(a,I,h)))throw new Error(`Image ${a} now keeps its PostgreSQL data where volume ${w}, mounted at ${h}, can't hold it. Testkit leaves the container, the volume and its cluster alone. To start fresh, remove the container and the volume, or go back to the image the cluster was made with.`);await stopAndRemoveContainer(s.Id,o,n),($&&$!==a||G())&&(await v(e,w,n),w=T(e,L())),s=null}else!s&&i&&G()&&(await v(e,w,n),w=T(e,L()));let O=w;if(s)s.State?.Running||(await y(["start",s.Id],`Docker local Postgres container start for ${o}`,LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.start,n.signal),s=await inspectContainer(o,n),s&&E(s,e,o));else{const F=Ee?null:await We(e,w,n);w=F??await Ke(e,n),O=F;const Le=we(e,o,L());await y(buildDockerRunArgs({containerName:o,labels:Le,volumeName:w,volumeMountPath:b,user:u,password:l,image:a,maxConnections:g}),`Docker local Postgres container creation for ${o}`,LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.run,n.signal),s=await inspectContainer(o,n),s&&E(s,e,o)}if(s&&s.State?.Running===!1)throw await ge({id:s.Id,name:o},O,s,l,n);const B=ae(s);if(!s||!B)throw new Error(`Could not determine host port for local database container ${o}`);const M={containerName:o,containerId:s.Id,volumeName:w,volumeMountPath:b,image:a,user:u,password:l,maxConnections:g,host:"127.0.0.1",port:Number(B),containerPort:5432};return te(r,M),await me(M,n,O),M}export function buildDockerRunArgs({containerName:e,labels:t,volumeName:n,volumeMountPath:r,user:o,password:i,image:a,maxConnections:c}){return["run","-d","--name",e,...ee(t),"-p","127.0.0.1::5432","--mount",`type=volume,source=${n},target=${r}`,"-e",`POSTGRES_USER=${o}`,"-e",`POSTGRES_PASSWORD=${i}`,"-e",`POSTGRES_DB=${N}`,"-e","POSTGRES_INITDB_ARGS=--no-sync",a,"postgres","-c",`max_connections=${String(c)}`]}export async function loadExistingLocalContainer(e,t={}){const n=Y(e),r=d(f.join(n,"container_name"));if(!r)return null;const o=await inspectContainer(r,t);if(!o)return null;E(o,e,r);const i=o.Config?.Image||d(f.join(n,"image"))||H,a=ce(o,e,d(f.join(n,"volume_name"))),c=a?.destination||d(f.join(n,"volume_mount_path"))||postgresDataVolumeTarget(i,o.Config??{}),u=a?.name||d(f.join(n,"volume_name"));if(!u)return null;await j(e,u,t),o.State?.Running||await y(["start",o.Id],`Docker local Postgres container start for ${r}`,LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.start,t.signal);const l=o.State?.Running?o:await inspectContainer(o.Id,t);l&&E(l,e,r);const g=ae(l);if(!l||!g)return null;const s={containerName:r,containerId:l.Id,volumeName:u,volumeMountPath:c,image:i,user:d(f.join(n,"user"))||X,password:d(f.join(n,"password"))||K,maxConnections:se(d(f.join(n,"max_connections"))||W),host:"127.0.0.1",port:Number(g),containerPort:ie(l)==="host"?Number(g):5432};return te(n,s),await me(s,t),s}export async function stopOwnedLocalContainer(e,t){const n=await inspectContainer(t);return n?(E(n,e,t),!n.State?.Running||await ze(n.Id,t)):!1}function Fe(e,t){const n=e.Config?.Cmd;return Array.isArray(n)?n.includes("-c")&&n.includes(`max_connections=${String(t)}`):!1}function He(e,t,n){return(Array.isArray(e.Mounts)?e.Mounts:[]).some(o=>o?.Type==="volume"&&o.Name===t&&o.Destination===n)}export function isReusableLocalPostgresContainer(e,t){return e.Config?.Image===t.image&&Fe(e,t.maxConnections)&&He(e,t.volumeName,t.volumeMountPath)&&ie(e)==="bridge"}function ie(e){return e.HostConfig?.NetworkMode==="host"?"host":"bridge"}function ae(e){return e?.NetworkSettings?.Ports?.["5432/tcp"]?.[0]?.HostPort||null}function se(e){const t=Number(e);if(!Number.isInteger(t)||t<10)throw new Error("database.maxConnections must be an integer greater than or equal to 10");return t}export async function inspectContainer(e,t={}){R("database.containerInspections");const n=`Docker local Postgres container inspect for ${e}`,r=["inspect",e],o=await ye(r,n,LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.inspect,t.signal,c=>C(r,n,c,t.signal));if(o.exitCode!==0){if(Ce(o))return null;throw D(n,o)}let i;try{i=JSON.parse(o.stdout)}catch(c){throw be(n,o,c)}const a=A(i)&&i.length===1?normalizeLocalPostgresContainerInspect(i[0]):null;if(!a?.Id)throw be(n,o);return{...a,Id:a.Id}}export function normalizeLocalPostgresContainerInspect(e){return m(e)?{Config:Ze(e.Config),HostConfig:tt(e.HostConfig),Id:p(e.Id),Mounts:nt(e.Mounts),NetworkSettings:ot(e.NetworkSettings),State:rt(e.State)}:null}export async function stopAndRemoveContainer(e,t=e,n={}){const r=`Docker local Postgres container removal for ${t}`,o=await C(["rm","-f","-v",e],r,LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.remove,n.signal);if(!(o.exitCode===0||Ce(o)))throw D(r,o)}async function We(e,t,n={}){const r=_(e,t,"existing-generation"),o=await S(t,n);if(!o)return null;if(!matchesStableDockerLabels(he(o)||{},r))throw new Error(`Docker volume ${t} already exists but is not owned by this Testkit product. Remove or rename it before running Testkit.`);return t}async function Ke(e,t){const n=L(),r=T(e,n),o=_(e,r,n);await y(["volume","create",...ee(o),r],`Docker local Postgres volume creation for ${r}`,LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.volumeCreate,t.signal);const i=V(await S(r,t));if(i===null||!qe(i.labels,o))throw new Error(`Docker volume ${r} was not published with the expected Testkit generation identity`);return r}async function j(e,t,n){const r=_(e,t,"existing-generation"),o=V(await S(t,n));if(o===null||!matchesStableDockerLabels(o.labels,r))throw new Error(`Docker volume ${t} is missing or is not owned by this Testkit product`);return o}export function matchesStableDockerLabels(e,t){const n=e[J],r=e[Q];return typeof n=="string"&&n.length>0&&typeof r=="string"&&r.length>0&&Object.entries(t).every(([o,i])=>o===J||o===Q||e[o]===ne(i))}function qe(e,t){const n=Object.entries(e).sort(([o],[i])=>o.localeCompare(i)),r=Object.entries(t).map(([o,i])=>[o,ne(i)]).sort(([o],[i])=>o.localeCompare(i));return n.length===r.length&&n.every(([o,i],a)=>r[a]?.[0]===o&&r[a]?.[1]===i)}async function v(e,t,n={}){const r=_(e,t,"existing-generation"),o=await S(t,n);if(!o)return;const i=he(o)||{};if(!matchesStableDockerLabels(i,r))return;const c=V(o);c!==null&&await Ue(t,c,n)}function ce(e,t,n){const r=(Array.isArray(e?.Mounts)?e.Mounts:[]).flatMap(o=>o?.Type==="volume"&&typeof o.Name=="string"&&typeof o.Destination=="string"?[{name:o.Name,destination:o.Destination}]:[]);return r.find(o=>o.name===n)??r.find(o=>Oe(t,o.name))??null}function E(e,t,n){const r=we(t,n);if(!matchesStableDockerLabels(e.Config?.Labels||{},r))throw new Error(`Docker container ${n} already exists but is not owned by this Testkit product. Remove or rename it before running Testkit.`)}export function postgresDataVolumeTarget(e,t,n){const{dataDirectory:r,volumes:o}=le(e,t);return n&&ue(r,o,k(n))?k(n):o.filter(a=>x(r,a)).sort((a,c)=>c.length-a.length)[0]??r}export function holdsPostgresData(e,t,n){const{dataDirectory:r,volumes:o}=le(e,t);return ue(r,o,k(n))}function le(e,t){const n=`Testkit cannot tell where image ${e} keeps its PostgreSQL data`,r=t.Env?.filter(c=>c.startsWith("PGDATA=")).at(-1)?.slice(7);if(!r)throw new Error(`${n}: its configuration sets no PGDATA. Use an image built from the official postgres image, or set PGDATA in it.`);if(!f.posix.isAbsolute(r))throw new Error(`${n}: its PGDATA, ${r}, is not an absolute path.`);const o=k(r),i=Object.keys(t.Volumes??{}).filter(c=>f.posix.isAbsolute(c)).map(k),a=i.find(c=>c!==o&&x(c,o));if(a)throw new Error(`${n}: it declares the volume ${a} inside its PGDATA, ${o}, so Testkit's volume could not hold the whole cluster.`);return{dataDirectory:o,volumes:i}}function ue(e,t,n){return f.posix.isAbsolute(n)&&x(e,n)&&!t.some(r=>r!==n&&x(r,n)&&x(e,r))}function k(e){return Xe(f.posix.normalize(e))}function Xe(e){return e.length>1?e.replace(/\/+$/u,""):e}function x(e,t){return e===t||e.startsWith(t==="/"?"/":`${t}/`)}async function fe(e,t){R("database.imageInspections");const n=`Docker image inspect for ${e}`,r=()=>C(["image","inspect","--format","{{json .Config}}",e],n,LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.inspect,t.signal);let o=await r();if(o.exitCode!==0&&it(o)&&(await y(["pull",e],`Docker image pull for ${e}`,LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.run,t.signal),o=await r()),o.exitCode!==0)throw D(n,o);let i;try{i=JSON.parse(o.stdout)}catch(a){throw new Error(`${n} returned malformed output`,{cause:a})}if(!m(i))throw new Error(`${n} returned no image configuration`);return{Env:A(i.Env)?i.Env.filter(a=>typeof a=="string"):null,Volumes:m(i.Volumes)?i.Volumes:null}}async function me(e,t={},n=null){R("database.hostReadinessChecks");const r=Date.now()+P;let o=Date.now();const i=async()=>{const u=r-Date.now();if(u<=0||Date.now()-o<Ge)return;o=Date.now();const l=AbortSignal.any([AbortSignal.timeout(u),...t.signal?[t.signal]:[]]),g=await inspectContainer(e.containerId,{signal:l}).catch(s=>{if(leftDockerProcessBehind(s))throw s;return null});if(g?.State?.Running===!1)throw await ge({id:e.containerId,name:e.containerName},n,g,e.password,t)};if(!xe().localSocket){await Ye(e,t,i);return}const a=Date.now();let c=a;try{await Me(Ie(e,N),{async afterFailure(u){if(u instanceof Ve&&await i(),!(u instanceof ve||u instanceof Re)&&Date.now()-a<Se)return!1;const g=a+P-Date.now();return g<=0||Date.now()-c<q?!1:(c=Date.now(),de(e,t,g))},attemptTimeoutMs:De,maxPollIntervalMs:_e,signal:t.signal,timeoutMs:P})}catch(u){if(t.signal?.aborted)throw U(t.signal);if(!(u instanceof Ne))throw u;const l=`${e.host}:${String(e.port)}`;throw new Error(u instanceof je?`Local database container ${e.containerName} on ${l} refused Testkit's connection: ${u.message}`:`Timed out waiting for local database container ${e.containerName} on ${l}: ${u.message}`,{cause:u})}}async function Ye(e,t,n){const r=Date.now();let o=10;for(;Date.now()-r<P;){const i=Math.max(1,P-(Date.now()-r));if(await de(e,t,i))return;await n(),await Qe(Math.min(o,i),t.signal),o=Math.min(o*2,q)}throw new Error(`Timed out waiting for local database container ${e.containerName}`)}async function de(e,t,n=LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.readinessAttempt){return(await C(["exec",e.containerId,"pg_isready","-h","127.0.0.1","-U",e.user,"-d",N,"-p",String(e.containerPort||5432)],`Docker local Postgres readiness probe for ${e.containerName}`,Math.min(LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.readinessAttempt,n),t.signal,"readiness").catch(o=>{if(Je(o))return null;throw o}))?.exitCode===0}export function leftDockerProcessBehind(e){const t=m(e)?Reflect.get(e,"result"):void 0;return m(t)&&t.terminationError!==null&&t.terminationError!==void 0}function Je(e){const t=m(e)?Reflect.get(e,"result"):void 0;return m(t)&&t.timedOut===!0&&t.cancelled===!1&&t.error===null&&t.terminationError===null&&t.outputLimitExceeded===!1}function Qe(e,t){return new Promise((n,r)=>{if(t?.aborted){r(U(t));return}const o=setTimeout(i,e);t?.addEventListener("abort",a,{once:!0});function i(){t?.removeEventListener("abort",a),n()}function a(){clearTimeout(o),r(t?U(t):new Error("Local Postgres operation aborted"))}})}async function ge(e,t,n,r,o){const i=await C(["logs","--tail","20",e.id],`Docker local Postgres container logs for ${e.name}`,o.exitLogTimeoutMs??LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.inspect,o.signal).catch(l=>{if(o.signal?.aborted||leftDockerProcessBehind(l))throw l;return null}),a=i?.exitCode===0?`${i.stdout}
3
- ${i.stderr}`.split(/\r?\n/u).map(l=>l.trim()).filter(Boolean):[],c=[...a].reverse().find(l=>/\b(?:FATAL|PANIC):/u.test(l))??a.at(-1),u=c===void 0?i?.exitCode===0?"it wrote no log output":"Testkit couldn't read its log":Pe(ke(c,Ae([r])).text);return new oe(e,t,n.State?.ExitCode??null,u)}function we(e,t,n="existing-generation"){return Z(e,{generation:n,kind:"postgres-container",name:t,scope:"local-postgres",cachePolicy:"product-cache"})}function _(e,t,n){return Z(e,{generation:n,kind:"postgres-volume",name:t,scope:"local-postgres",cachePolicy:"product-cache"})}function U(e){return e.reason instanceof Error?e.reason:new Error("Local Postgres operation aborted",{cause:e.reason})}function he(e){if(!m(e))return null;const t=e.Labels;return m(t)?t:null}function Ze(e){if(e===null)return null;if(m(e))return{Cmd:pe(e.Cmd),Env:pe(e.Env),Image:p(e.Image),Labels:et(e.Labels),Volumes:m(e.Volumes)?e.Volumes:null}}function et(e){if(!m(e))return null;const t={};for(const[n,r]of Object.entries(e))typeof r=="string"&&(t[n]=r);return t}function tt(e){if(e===null)return null;if(m(e))return{NetworkMode:p(e.NetworkMode)}}function nt(e){if(e===null)return null;if(A(e))return e.flatMap(t=>m(t)?[{Destination:p(t.Destination),Name:p(t.Name),Type:p(t.Type)}]:[])}function ot(e){if(e===null)return null;if(!m(e))return;const t=e.Ports;if(t===null)return{Ports:null};if(!m(t))return{};const n={};for(const[r,o]of Object.entries(t)){if(o===null){n[r]=null;continue}A(o)&&(n[r]=o.flatMap(i=>{if(!m(i))return[];const a=p(i.HostPort);return i.HostPort!==void 0&&a===void 0?[]:[{HostPort:a}]}))}return{Ports:n}}function rt(e){if(e===null)return null;if(m(e))return{ExitCode:e.ExitCode===null||typeof e.ExitCode=="number"?e.ExitCode:void 0,Running:e.Running===null||typeof e.Running=="boolean"?e.Running:void 0}}function p(e){if(e===null||typeof e=="string")return e}function pe(e){if(e===null)return null;if(A(e))return e.filter(t=>typeof t=="string")}async function y(e,t,n,r){const o=await C(e,t,n,r);if(o.exitCode===0)return o;throw D(t,o)}function D(e,t){const n=t.stderr.trim()||t.stdout.trim(),r=new Error(`${e} failed with exit code ${String(t.exitCode)}${n?`: ${n}`:""}`);return Object.defineProperty(r,"result",{enumerable:!1,value:Object.freeze({...t})}),r}function be(e,t,n){const r=new Error(`${e} returned malformed Docker inspect output; expected exactly one container record with a non-empty Id`,n===void 0?void 0:{cause:n});return Object.defineProperty(r,"result",{enumerable:!1,value:Object.freeze({...t})}),r}function it(e){return/\bno such image\b/iu.test(`${e.stderr}
4
- ${e.stdout}`)}function Ce(e){return/\bno such (?:container|object)\b/iu.test(`${e.stderr}
1
+ import{recordWork as N}from"../kernel/work-metrics.js";import{redactSensitiveText as Ee}from"../kernel/redaction.js";import{orderRedactionTargets as Le,replaceRedactionTargets as Pe}from"../kernel/redaction-targets.js";import{dockerEndpoint as Ae}from"./docker-endpoint.js";import{runDockerMetadataCommand as ke}from"./docker-metadata.js";import{randomUUID as L}from"node:crypto";import{setTimeout as xe}from"node:timers/promises";import ye from"fs";import f from"path";import{LOCAL_ADMIN_DB as V,LOCAL_IMAGE as B,LOCAL_MAX_CONNECTIONS as H,LOCAL_PASSWORD as W,LOCAL_EXEC_POLL_INTERVAL_MS as K,LOCAL_READY_EXEC_FALLBACK_AFTER_MS as Se,LOCAL_POLL_INTERVAL_MS as Te,LOCAL_READY_ATTEMPT_TIMEOUT_MS as _e,LOCAL_READY_TIMEOUT_MS as P,LOCAL_USER as q}from"./constants.js";import{buildContainerName as De,buildDatabaseUrl as $e,buildVolumeName as S,isProductVolumeName as Ie}from"./naming.js";import{waitForPostgresConnection as Me}from"./postgres/connection-check.js";import{NotPostgresError as Oe,PostgresCheckError as Re,PostgresConnectionError as Ne,PostgresRefusedError as Ve,PostgresUnansweredError as je}from"./postgres/protocol.js";import{getLocalInfraDir as X,readStateValue as m}from"./state.js";import{TESTKIT_CREATED_AT_LABEL as Y,TESTKIT_RESOURCE_GENERATION_LABEL as J,buildDockerResourceLabels as Q,dockerLabelArgs as Z,inspectDockerVolume as T,isMissingDockerContainer as ee,normalizeDockerVolumeInspect as _,removeDockerVolume as ve,runDockerCommand as b,sameStringRecord as Ue,stopDockerContainer as ze}from"./docker.js";import{writeLocalInfraState as te}from"./state-files.js";import{isUnknownArray as A,isUnknownRecord as d}from"../kernel/unknown-values.js";const Ge=String;export const LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS=Object.freeze({inspect:3e4,readinessAttempt:1e4,remove:12e4,run:10*6e4,start:12e4,volumeCreate:12e4});class ne extends Error{container;retainedVolume;reason;constructor(t,n,r,o){super(`Local database container ${t.name} exited while starting${r===null?"":` with exit code ${String(r)}`}: ${o}`),this.container=t,this.retainedVolume=n,this.reason=o}}const Fe=1e3;export async function ensureLocalContainer(e,t={},n={}){try{return await oe(e,t,n)}catch(r){if(!(r instanceof ne)||r.retainedVolume===null)throw r;return await j(e,r.retainedVolume,n),(n.notify??Be)(`Testkit is replacing database volume ${r.retainedVolume}, because ${r.message}. The new volume starts with an empty cluster, so its template is rebuilt.`),await stopAndRemoveContainer(r.container.id,r.container.name,n),await v(e,r.retainedVolume,n),oe(e,t,n)}}function Be(e){process.stderr.write(`[testkit] ${e}
2
+ `)}async function oe(e,t,n){const r=X(e);ye.mkdirSync(r,{recursive:!0});const o=m(f.join(r,"container_name"))||De(e),i=m(f.join(r,"volume_name")),a=t.image||m(f.join(r,"image"))||B,c=m(f.join(r,"image")),u=t.user||m(f.join(r,"user"))||q,l=t.password||m(f.join(r,"password"))||W,g=ae(t.maxConnections||m(f.join(r,"max_connections"))||H);let s=await inspectContainer(o,n);s&&E(s,e,o);const Ce=!s&&!i,U=se(s,e,i);let w=U?.name||i||S(e,L());s&&await j(e,w,n);const I=s?.Config?.Image||c||null,h=U?.destination||m(f.join(r,"volume_mount_path")),M=s?.Config?.Image===a?s.Config:null;let C=postgresDataVolumeTarget(a,M??await ue(a,n),h);const z=()=>!!h&&h!==C&&(I??a)===a;if(s&&!isReusableLocalPostgresContainer(s,{image:a,maxConnections:g,volumeMountPath:C,volumeName:w})){if(M!==null&&(C=postgresDataVolumeTarget(a,await ue(a,n),h),h&&h!==C&&holdsPostgresData(a,M,h)))throw new Error(`Image ${a} now keeps its PostgreSQL data where volume ${w}, mounted at ${h}, can't hold it. Testkit leaves the container, the volume and its cluster alone. To start fresh, remove the container and the volume, or go back to the image the cluster was made with.`);await stopAndRemoveContainer(s.Id,o,n),(I&&I!==a||z())&&(await v(e,w,n),w=S(e,L())),s=null}else!s&&i&&z()&&(await v(e,w,n),w=S(e,L()));let O=w;if(s)s.State?.Running||(await y(["start",s.Id],`Docker local Postgres container start for ${o}`,LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.start,n.signal),s=await inspectContainer(o,n),s&&E(s,e,o));else{const F=Ce?null:await Ke(e,w,n);w=F??await qe(e,n),O=F;const be=ge(e,o,L());await y(buildDockerRunArgs({containerName:o,labels:be,volumeName:w,volumeMountPath:C,user:u,password:l,image:a,maxConnections:g}),`Docker local Postgres container creation for ${o}`,LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.run,n.signal),s=await inspectContainer(o,n),s&&E(s,e,o)}if(s&&s.State?.Running===!1)throw await de({id:s.Id,name:o},O,s,l,n);const G=ie(s);if(!s||!G)throw new Error(`Could not determine host port for local database container ${o}`);const R={containerName:o,containerId:s.Id,volumeName:w,volumeMountPath:C,image:a,user:u,password:l,maxConnections:g,host:"127.0.0.1",port:Number(G),containerPort:5432};return te(r,R),await fe(R,n,O),R}export function buildDockerRunArgs({containerName:e,labels:t,volumeName:n,volumeMountPath:r,user:o,password:i,image:a,maxConnections:c}){return["run","-d","--name",e,...Z(t),"-p","127.0.0.1::5432","--mount",`type=volume,source=${n},target=${r}`,"-e",`POSTGRES_USER=${o}`,"-e",`POSTGRES_PASSWORD=${i}`,"-e",`POSTGRES_DB=${V}`,"-e","POSTGRES_INITDB_ARGS=--no-sync",a,"postgres","-c",`max_connections=${String(c)}`]}export async function loadExistingLocalContainer(e,t={}){const n=X(e),r=m(f.join(n,"container_name"));if(!r)return null;const o=await inspectContainer(r,t);if(!o)return null;E(o,e,r);const i=o.Config?.Image||m(f.join(n,"image"))||B,a=se(o,e,m(f.join(n,"volume_name"))),c=a?.destination||m(f.join(n,"volume_mount_path"))||postgresDataVolumeTarget(i,o.Config??{}),u=a?.name||m(f.join(n,"volume_name"));if(!u)return null;await j(e,u,t),o.State?.Running||await y(["start",o.Id],`Docker local Postgres container start for ${r}`,LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.start,t.signal);const l=o.State?.Running?o:await inspectContainer(o.Id,t);l&&E(l,e,r);const g=ie(l);if(!l||!g)return null;const s={containerName:r,containerId:l.Id,volumeName:u,volumeMountPath:c,image:i,user:m(f.join(n,"user"))||q,password:m(f.join(n,"password"))||W,maxConnections:ae(m(f.join(n,"max_connections"))||H),host:"127.0.0.1",port:Number(g),containerPort:re(l)==="host"?Number(g):5432};return te(n,s),await fe(s,t),s}export async function stopOwnedLocalContainer(e,t){const n=await inspectContainer(t);return n?(E(n,e,t),!n.State?.Running||await ze(n.Id,t)):!1}function He(e,t){const n=e.Config?.Cmd;return Array.isArray(n)?n.includes("-c")&&n.includes(`max_connections=${String(t)}`):!1}function We(e,t,n){return(Array.isArray(e.Mounts)?e.Mounts:[]).some(o=>o?.Type==="volume"&&o.Name===t&&o.Destination===n)}export function isReusableLocalPostgresContainer(e,t){return e.Config?.Image===t.image&&He(e,t.maxConnections)&&We(e,t.volumeName,t.volumeMountPath)&&re(e)==="bridge"}function re(e){return e.HostConfig?.NetworkMode==="host"?"host":"bridge"}function ie(e){return e?.NetworkSettings?.Ports?.["5432/tcp"]?.[0]?.HostPort||null}function ae(e){const t=Number(e);if(!Number.isInteger(t)||t<10)throw new Error("database.maxConnections must be an integer greater than or equal to 10");return t}export async function inspectContainer(e,t={}){N("database.containerInspections");const n=`Docker local Postgres container inspect for ${e}`,r=["inspect",e],o=await ke(r,n,LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.inspect,t.signal,c=>b(r,n,c,t.signal));if(o.exitCode!==0){if(ee(o))return null;throw $(n,o)}let i;try{i=JSON.parse(o.stdout)}catch(c){throw pe(n,o,c)}const a=A(i)&&i.length===1?normalizeLocalPostgresContainerInspect(i[0]):null;if(!a?.Id)throw pe(n,o);return{...a,Id:a.Id}}export function normalizeLocalPostgresContainerInspect(e){return d(e)?{Config:Ze(e.Config),HostConfig:tt(e.HostConfig),Id:p(e.Id),Mounts:nt(e.Mounts),NetworkSettings:ot(e.NetworkSettings),State:rt(e.State)}:null}export async function stopAndRemoveContainer(e,t=e,n={}){const r=`Docker local Postgres container removal for ${t}`,o=await b(["rm","-f","-v",e],r,LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.remove,n.signal);if(!(o.exitCode===0||ee(o)))throw $(r,o)}async function Ke(e,t,n={}){const r=D(e,t,"existing-generation"),o=_(await T(t,n));if(!o)return null;if(!matchesStableDockerLabels(o.labels,r))throw new Error(`Docker volume ${t} already exists but is not owned by this Testkit product. Remove or rename it before running Testkit.`);return t}async function qe(e,t){const n=L(),r=S(e,n),o=D(e,r,n);await y(["volume","create",...Z(o),r],`Docker local Postgres volume creation for ${r}`,LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.volumeCreate,t.signal);const i=_(await T(r,t));if(i===null||!Ue(i.labels,o))throw new Error(`Docker volume ${r} was not published with the expected Testkit generation identity`);return r}async function j(e,t,n){const r=D(e,t,"existing-generation"),o=_(await T(t,n));if(o===null||!matchesStableDockerLabels(o.labels,r))throw new Error(`Docker volume ${t} is missing or is not owned by this Testkit product`);return o}export function matchesStableDockerLabels(e,t){const n=e[Y],r=e[J];return typeof n=="string"&&n.length>0&&typeof r=="string"&&r.length>0&&Object.entries(t).every(([o,i])=>o===Y||o===J||e[o]===Ge(i))}async function v(e,t,n={}){const r=D(e,t,"existing-generation"),o=_(await T(t,n));!o||!matchesStableDockerLabels(o.labels,r)||await ve(t,o,n)}function se(e,t,n){const r=(Array.isArray(e?.Mounts)?e.Mounts:[]).flatMap(o=>o?.Type==="volume"&&typeof o.Name=="string"&&typeof o.Destination=="string"?[{name:o.Name,destination:o.Destination}]:[]);return r.find(o=>o.name===n)??r.find(o=>Ie(t,o.name))??null}function E(e,t,n){const r=ge(t,n);if(!matchesStableDockerLabels(e.Config?.Labels||{},r))throw new Error(`Docker container ${n} already exists but is not owned by this Testkit product. Remove or rename it before running Testkit.`)}export function postgresDataVolumeTarget(e,t,n){const{dataDirectory:r,volumes:o}=ce(e,t);return n&&le(r,o,k(n))?k(n):o.filter(a=>x(r,a)).sort((a,c)=>c.length-a.length)[0]??r}export function holdsPostgresData(e,t,n){const{dataDirectory:r,volumes:o}=ce(e,t);return le(r,o,k(n))}function ce(e,t){const n=`Testkit cannot tell where image ${e} keeps its PostgreSQL data`,r=t.Env?.filter(c=>c.startsWith("PGDATA=")).at(-1)?.slice(7);if(!r)throw new Error(`${n}: its configuration sets no PGDATA. Use an image built from the official postgres image, or set PGDATA in it.`);if(!f.posix.isAbsolute(r))throw new Error(`${n}: its PGDATA, ${r}, is not an absolute path.`);const o=k(r),i=Object.keys(t.Volumes??{}).filter(c=>f.posix.isAbsolute(c)).map(k),a=i.find(c=>c!==o&&x(c,o));if(a)throw new Error(`${n}: it declares the volume ${a} inside its PGDATA, ${o}, so Testkit's volume could not hold the whole cluster.`);return{dataDirectory:o,volumes:i}}function le(e,t,n){return f.posix.isAbsolute(n)&&x(e,n)&&!t.some(r=>r!==n&&x(r,n)&&x(e,r))}function k(e){return Xe(f.posix.normalize(e))}function Xe(e){return e.length>1?e.replace(/\/+$/u,""):e}function x(e,t){return e===t||e.startsWith(t==="/"?"/":`${t}/`)}async function ue(e,t){N("database.imageInspections");const n=`Docker image inspect for ${e}`,r=()=>b(["image","inspect","--format","{{json .Config}}",e],n,LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.inspect,t.signal);let o=await r();if(o.exitCode!==0&&it(o)&&(await y(["pull",e],`Docker image pull for ${e}`,LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.run,t.signal),o=await r()),o.exitCode!==0)throw $(n,o);let i;try{i=JSON.parse(o.stdout)}catch(a){throw new Error(`${n} returned malformed output`,{cause:a})}if(!d(i))throw new Error(`${n} returned no image configuration`);return{Env:A(i.Env)?i.Env.filter(a=>typeof a=="string"):null,Volumes:d(i.Volumes)?i.Volumes:null}}async function fe(e,t={},n=null){N("database.hostReadinessChecks");const r=Date.now()+P;let o=Date.now();const i=async()=>{const u=r-Date.now();if(u<=0||Date.now()-o<Fe)return;o=Date.now();const l=AbortSignal.any([AbortSignal.timeout(u),...t.signal?[t.signal]:[]]),g=await inspectContainer(e.containerId,{signal:l}).catch(s=>{if(leftDockerProcessBehind(s))throw s;return null});if(g?.State?.Running===!1)throw await de({id:e.containerId,name:e.containerName},n,g,e.password,t)};if(!Ae().localSocket){await Ye(e,t,i);return}const a=Date.now();let c=a;try{await Me($e(e,V),{async afterFailure(u){if(u instanceof Ne&&await i(),!(u instanceof je||u instanceof Oe)&&Date.now()-a<Se)return!1;const g=a+P-Date.now();return g<=0||Date.now()-c<K?!1:(c=Date.now(),me(e,t,g))},attemptTimeoutMs:_e,maxPollIntervalMs:Te,signal:t.signal,timeoutMs:P})}catch(u){if(t.signal?.aborted)throw we(t.signal);if(!(u instanceof Re))throw u;const l=`${e.host}:${String(e.port)}`;throw new Error(u instanceof Ve?`Local database container ${e.containerName} on ${l} refused Testkit's connection: ${u.message}`:`Timed out waiting for local database container ${e.containerName} on ${l}: ${u.message}`,{cause:u})}}async function Ye(e,t,n){const r=Date.now();let o=10;for(;Date.now()-r<P;){const i=Math.max(1,P-(Date.now()-r));if(await me(e,t,i))return;await n(),await Qe(Math.min(o,i),t.signal),o=Math.min(o*2,K)}throw new Error(`Timed out waiting for local database container ${e.containerName}`)}async function me(e,t,n=LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.readinessAttempt){return(await b(["exec",e.containerId,"pg_isready","-h","127.0.0.1","-U",e.user,"-d",V,"-p",String(e.containerPort||5432)],`Docker local Postgres readiness probe for ${e.containerName}`,Math.min(LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.readinessAttempt,n),t.signal,"readiness").catch(o=>{if(Je(o))return null;throw o}))?.exitCode===0}export function leftDockerProcessBehind(e){const t=d(e)?Reflect.get(e,"result"):void 0;return d(t)&&t.terminationError!==null&&t.terminationError!==void 0}function Je(e){const t=d(e)?Reflect.get(e,"result"):void 0;return d(t)&&t.timedOut===!0&&t.cancelled===!1&&t.error===null&&t.terminationError===null&&t.outputLimitExceeded===!1}function Qe(e,t){return xe(e,void 0,{signal:t}).catch(n=>{throw t?.aborted?we(t):n})}async function de(e,t,n,r,o){const i=await b(["logs","--tail","20",e.id],`Docker local Postgres container logs for ${e.name}`,o.exitLogTimeoutMs??LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.inspect,o.signal).catch(l=>{if(o.signal?.aborted||leftDockerProcessBehind(l))throw l;return null}),a=i?.exitCode===0?`${i.stdout}
3
+ ${i.stderr}`.split(/\r?\n/u).map(l=>l.trim()).filter(Boolean):[],c=[...a].reverse().find(l=>/\b(?:FATAL|PANIC):/u.test(l))??a.at(-1),u=c===void 0?i?.exitCode===0?"it wrote no log output":"Testkit couldn't read its log":Ee(Pe(c,Le([r])).text);return new ne(e,t,n.State?.ExitCode??null,u)}function ge(e,t,n="existing-generation"){return Q(e,{generation:n,kind:"postgres-container",name:t,scope:"local-postgres",cachePolicy:"product-cache"})}function D(e,t,n){return Q(e,{generation:n,kind:"postgres-volume",name:t,scope:"local-postgres",cachePolicy:"product-cache"})}function we(e){return e.reason instanceof Error?e.reason:new Error("Local Postgres operation aborted",{cause:e.reason})}function Ze(e){if(e===null)return null;if(d(e))return{Cmd:he(e.Cmd),Env:he(e.Env),Image:p(e.Image),Labels:et(e.Labels),Volumes:d(e.Volumes)?e.Volumes:null}}function et(e){if(!d(e))return null;const t={};for(const[n,r]of Object.entries(e))typeof r=="string"&&(t[n]=r);return t}function tt(e){if(e===null)return null;if(d(e))return{NetworkMode:p(e.NetworkMode)}}function nt(e){if(e===null)return null;if(A(e))return e.flatMap(t=>d(t)?[{Destination:p(t.Destination),Name:p(t.Name),Type:p(t.Type)}]:[])}function ot(e){if(e===null)return null;if(!d(e))return;const t=e.Ports;if(t===null)return{Ports:null};if(!d(t))return{};const n={};for(const[r,o]of Object.entries(t)){if(o===null){n[r]=null;continue}A(o)&&(n[r]=o.flatMap(i=>{if(!d(i))return[];const a=p(i.HostPort);return i.HostPort!==void 0&&a===void 0?[]:[{HostPort:a}]}))}return{Ports:n}}function rt(e){if(e===null)return null;if(d(e))return{ExitCode:e.ExitCode===null||typeof e.ExitCode=="number"?e.ExitCode:void 0,Running:e.Running===null||typeof e.Running=="boolean"?e.Running:void 0}}function p(e){if(e===null||typeof e=="string")return e}function he(e){if(e===null)return null;if(A(e))return e.filter(t=>typeof t=="string")}async function y(e,t,n,r){const o=await b(e,t,n,r);if(o.exitCode===0)return o;throw $(t,o)}function $(e,t){const n=t.stderr.trim()||t.stdout.trim(),r=new Error(`${e} failed with exit code ${String(t.exitCode)}${n?`: ${n}`:""}`);return Object.defineProperty(r,"result",{enumerable:!1,value:Object.freeze({...t})}),r}function pe(e,t,n){const r=new Error(`${e} returned malformed Docker inspect output; expected exactly one container record with a non-empty Id`,n===void 0?void 0:{cause:n});return Object.defineProperty(r,"result",{enumerable:!1,value:Object.freeze({...t})}),r}function it(e){return/\bno such image\b/iu.test(`${e.stderr}
5
4
  ${e.stdout}`)}