@gfdelarue/testkit 0.3.2 → 0.3.3
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.
- package/bin/testkit-windows-owner-arm64.exe +0 -0
- package/bin/testkit-windows-owner-x64.exe +0 -0
- package/dist/app/cloud/control.js +2 -2
- package/dist/app/cloud/login.d.ts +4 -4
- package/dist/app/inspect/artifacts.d.ts +4 -4
- package/dist/app/run/engine-failure-stack.d.ts +1 -0
- package/dist/app/run/engine-failure-stack.js +2 -0
- package/dist/app/run/worker-loop.js +1 -1
- package/dist/app/setup/playwright.js +1 -1
- package/dist/app/typecheck/index.js +4 -4
- package/dist/cli/command-catalog.d.ts +24 -24
- package/dist/cli/command-flags.d.ts +3 -3
- package/dist/cli/commands/artifacts.d.ts +16 -16
- package/dist/cli/commands/cleanup.d.ts +1 -1
- package/dist/cli/commands/cloud/login.d.ts +1 -1
- package/dist/cli/commands/cloud/status.d.ts +3 -3
- package/dist/config/loader/discover-tests.d.ts +5 -0
- package/dist/config/loader/discover-tests.js +1 -1
- package/dist/config/loader/file-metadata.d.ts +1 -1
- package/dist/config/loader/file-metadata.js +1 -1
- package/dist/config/loader/index.d.ts +1 -1
- package/dist/config/loader/index.js +1 -1
- package/dist/database/docker-metadata.d.ts +3 -0
- package/dist/database/docker-metadata.js +3 -0
- package/dist/database/docker.js +5 -5
- package/dist/database/index.js +1 -1
- package/dist/database/local-postgres.js +1 -1
- package/dist/database/postgres/index.js +2 -2
- package/dist/database/runtime-config.d.ts +5 -5
- package/dist/database/schema-source.d.ts +4 -1
- package/dist/database/schema-source.js +3 -3
- package/dist/database/template-steps.d.ts +4 -0
- package/dist/database/template-steps.js +4 -4
- package/dist/engines/managed-module-resolution.js +5 -3
- package/dist/engines/node-vitest/adapter/consumer-typescript-paths.js +2 -2
- package/dist/engines/node-vitest/runtime/runtime-input.js +1 -1
- package/dist/engines/playwright/runtime/runtime-input.js +1 -1
- package/dist/environment/template.d.ts +20 -20
- package/dist/process/execa-bundle-licenses.txt +411 -0
- package/dist/process/execa-bundle.json +874 -0
- package/dist/process/files/json-file.js +1 -1
- package/dist/process/host-process.d.ts +6 -1
- package/dist/process/host-process.js +1 -1
- package/dist/process/index.d.ts +2 -2
- package/dist/process/locks.js +3 -3
- package/dist/process/process-contracts.d.ts +10 -2
- package/dist/process/process-identity.js +1 -1
- package/dist/process/process.d.ts +3 -2
- package/dist/process/process.js +46 -1
- package/dist/repo/metadata.js +1 -1
- package/dist/repo/state.d.ts +1 -0
- package/dist/repo/state.js +4 -4
- package/dist/steps/configured-step-testkit-modules.d.ts +1 -1
- package/dist/steps/run.js +2 -2
- package/dist/toolchains/archive.js +1 -1
- package/dist/toolchains/node-integrity.d.ts +2 -0
- package/dist/toolchains/node-integrity.js +1 -1
- package/dist/toolchains/node-provisioning.js +2 -2
- package/package.json +4 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
import g from"path";import{loadConfigContext as M,resolveProductDir as z}from"./load.js";import{discoverProject as A}from"./discovery.js";import{loadTestkitConfig as B}from"./config-loader.js";import{TEST_TYPE_ORDER as H,isRunType as L}from"../../domain/test-types.js";import"../../domain/identifiers.js";import{DEFAULT_RUNTIME_ISOLATION_POLICY as V,resolveRuntimeIsolationPolicy as _}from"./runtime-isolation.js";import{resolveEngineProfile as q}from"../../engines/catalog.js";import{readTestFileMetadata as U}from"./file-metadata.js";import{normalizeDiscoveryConfig as G}from"./decode-discovery.js";import"./path-policy.js";import{buildDiscoverySummary as Y,emptyDiscoverySummary as K}from"./discovery-summary.js";import{matchesSelectedTypes as J,matchesSuiteSelectors as k,normalizeTypeValues as Q,parseSuiteSelectors as W}from"../../domain/suite-selection.js";import{errorMessage as P}from"../../kernel/error-serialization.js";import{isObjectRecord as R,readObjectRecord as I,readProperty as f}from"./values.js";const X=9,h=String;export async function discoverTests(e={}){const t=z(process.cwd(),e.dir),i=se(e),o=await Z(t,i.diagnosticsMode),s={schemaVersion:X,source:"testkit-discovery",product:{name:g.basename(t),directory:t},configFile:o.configFile?g.relative(t,o.configFile)||g.basename(o.configFile):null,filters:{service:i.serviceFilter,types:i.typeValues,suiteSelectors:i.suiteSelectors.map(p=>p.raw),fileNames:i.fileNames,runnableOnly:i.runnableOnly,diagnostics:i.diagnosticsMode},services:[],suites:[],files:[],diagnostics:[...o.diagnostics],summary:K()};if(!o.config)return x(s);const r=ae(o.config),u=le(r.services),a=A(t,u,{strict:i.diagnosticsMode==="error",discovery:r.discovery}),m={project:a,fileMetadataByPath:new Map};s.diagnostics.push(...a.diagnostics),re(i.serviceFilter,r.services,a);let y=null;try{y=await M({dir:t,preparedDiscovery:m,configContext:{config:o.config,configFile:o.configFile},discoveryOptions:{strict:i.diagnosticsMode==="error"}})}catch(p){if(i.diagnosticsMode==="error")throw p;s.diagnostics.push({code:"config_invalid",severity:"error",message:P(p)})}const C=ie({productDir:t,rawDiscovery:a,explicitServices:r.services,configs:y?.configs??[],fileMetadataByPath:m.fileMetadataByPath,filters:i});return x({...s,...C})}export function formatDisplayName(e){return h(e||"").split(/[-_/]+/g).filter(Boolean).map(t=>t.replace(/^\w/,i=>i.toUpperCase())).join(" ")}export function testTypeContract(e,t,i){const o=q(e,t);if(o.id!==i)throw new Error(`Discovery profile mismatch for ${e}: metadata resolved ${i}, catalog resolved ${o.id}`);return e==="data"?{authoringApi:"@gfdelarue/testkit/node-vitest/data",executionHost:o.capabilities.executionHost,summary:"Data suites use the node-vitest engine and record Testkit data evidence; they are not raw Vitest or service-namespace tests."}:e==="contract"?{authoringApi:"@gfdelarue/testkit/node-vitest/contract",executionHost:o.capabilities.executionHost,summary:"Contract suites use the node-vitest engine for Node-backed service/runtime contracts without data-flow evidence requirements."}:t==="k6"?{authoringApi:"@gfdelarue/testkit/k6",executionHost:o.capabilities.executionHost}:t==="playwright"?{authoringApi:"@gfdelarue/testkit/playwright",executionHost:o.capabilities.executionHost}:{executionHost:o.capabilities.executionHost}}async function Z(e,t){try{const i=await B(e),{config:o,configFile:s}=ee(i);return{config:o,configFile:s,diagnostics:[]}}catch(i){if(t==="error")throw i;return{config:null,configFile:null,diagnostics:[{code:"config_invalid",severity:"error",message:P(i)}]}}}function ee(e){if(!R(e))throw new Error("testkit config loader must return an object");const t=f(e,"config"),i=f(e,"configFile");if(!R(t)){const o=typeof i=="string"?g.basename(i):"testkit config file";throw new Error(`testkit config file must export an object: ${o}`)}if(i!==null&&typeof i!="string")throw new Error("testkit config loader returned an invalid config path");return{config:t,configFile:i}}function ie({configs:e,fileMetadataByPath:t,productDir:i,rawDiscovery:o,explicitServices:s,filters:r}){const u=new Map(e.map(n=>[n.name,n])),a=new Map,m=new Map,y=[],C=new Set([...Object.keys(s),...Object.keys(o.services),...u.keys()]);for(const n of C){if(r.serviceFilter&&n!==r.serviceFilter)continue;const c=u.get(n),d=o.services[n],l=s[n];a.set(n,{name:n,discovered:c!==void 0||!!d,localCwd:c?c.testkit.local?.cwd||".":l?.localCwd||d?.inferredLocalCwd||".",dependsOn:[...c?.testkit.dependsOn??l?.dependsOn??[]],suiteCount:0,fileCount:0,activeFileCount:0,skippedFileCount:0})}for(const n of o.files){const c=n.testType;if(!J(c,r.typeValues)||!k(c,n.suiteName,r.suiteSelectors)||r.serviceFilter&&n.serviceName!==r.serviceFilter||r.fileNameSet.size>0&&!r.fileNameSet.has(n.filePath))continue;const d=t.get(n.filePath)??U(i,n.filePath);t.set(n.filePath,d);const l=u.get(n.serviceName),v=l?.suites[c]?.find($=>$.name===n.suiteName);if(l&&(!v||v.testType!==c||v.engineId!==d.engineId||v.profileId!==d.profileId))throw new Error(`Normalized suite ${n.serviceName}/${c}:${n.suiteName} conflicts with file ${n.filePath}`);const S=l&&(d.skipReason||l.testkit.skip?.fileReasonByPath.get(n.filePath)||de(l,c,n.suiteName))||null;if(r.runnableOnly&&S)continue;const T=l?[...new Set([...fe(l,c,n.suiteName),...l.testkit.requirements.fileLocksByPath.get(n.filePath)||[],...d.locks])].sort():[],j=a.get(n.serviceName)?.dependsOn||[],w=te({serviceName:n.serviceName,suiteName:n.suiteName,testType:c,filePath:n.filePath,engineId:d.engineId,profileId:d.profileId,skipReason:S,locks:T,runtimeIsolation:l&&v?_(l,v,n.filePath):V,dependsOn:j});y.push(w);const O=[n.serviceName,c,n.suiteName].join("|"),F=m.get(O);F?F.files.push(w):m.set(O,{serviceName:n.serviceName,testType:c,suiteName:n.suiteName,files:[w]})}const p=[...m.values()].map(n=>oe(n.serviceName,n.testType,n.suiteName,n.files,a.get(n.serviceName)?.dependsOn||[]));for(const n of p){const c=a.get(n.service)||{name:n.service,discovered:!0,localCwd:o.services[n.service]?.inferredLocalCwd||".",dependsOn:[...s[n.service]?.dependsOn||[]],suiteCount:0,fileCount:0,activeFileCount:0,skippedFileCount:0};c.suiteCount+=1,c.fileCount+=n.fileCount,c.activeFileCount+=n.activeFileCount,c.skippedFileCount+=n.skippedFileCount,a.set(n.service,c)}return{services:[...a.values()].sort((n,c)=>n.name.localeCompare(c.name)),suites:p.sort(ve),files:y.sort(ge)}}function te(e){const t=testTypeContract(e.testType,e.engineId,e.profileId);return{id:me(e.serviceName,e.testType,e.engineId,e.profileId,e.filePath),path:e.filePath,displayName:fileDisplayName(e.filePath),service:e.serviceName,suiteName:e.suiteName,groupLabel:formatDisplayName(e.suiteName),testType:e.testType,engineId:e.engineId,profileId:e.profileId,skipped:!!e.skipReason,skipReason:e.skipReason,locks:e.locks,runtimeIsolation:e.runtimeIsolation,dependsOn:[...e.dependsOn],contract:t}}function oe(e,t,i,o,s){const r=ne(e,t,i,o),u=o.filter(a=>a.skipped).length;return{id:pe(e,t,r.engineId,r.profileId,i),service:e,name:i,displayName:formatDisplayName(i),testType:t,engineId:r.engineId,profileId:r.profileId,groupLabel:formatDisplayName(i),fileCount:o.length,activeFileCount:o.length-u,skippedFileCount:u,dependsOn:[...s],locks:[...new Set(o.flatMap(a=>a.locks))].sort(),filePaths:o.map(a=>a.path),contract:r.contract}}function ne(e,t,i,o){const s=o[0];for(const r of o.slice(1))if(r.engineId!==s.engineId||r.profileId!==s.profileId)throw new Error(`Discovery suite ${e}/${t}:${i} mixes engine profiles ${s.engineId}/${s.profileId} and ${r.engineId}/${r.profileId}`);return{contract:s.contract,engineId:s.engineId,profileId:s.profileId}}function x(e){return{...e,diagnostics:e.diagnostics.sort(ye),summary:Y({diagnostics:e.diagnostics,files:e.files,serviceCount:e.services.length,suiteCount:e.suites.length})}}function re(e,t,i){if(!e)return;const o=new Set([...Object.keys(t),...Object.keys(i.services),...Object.keys(i.suitesByService)]);if(o.has(e))return;const s=[...o].sort((r,u)=>r.localeCompare(u)).join(", ");throw new Error(s.length>0?`Service "${e}" not found. Available: ${s}`:`Service "${e}" not found.`)}function se(e){const t=ce(Q(N(e.type))),i=W(N(e.suite)),o=N(e.file).map(b);return{typeValues:t,suiteSelectors:i,fileNames:o,fileNameSet:new Set(o),serviceFilter:e.service||null,runnableOnly:!!e.runnableOnly,diagnosticsMode:e.diagnostics==="error"?"error":"report"}}function ce(e){const t=[];for(const i of e){if(!L(i))throw new Error(`Discovery produced unsupported test type "${i}".`);t.push(i)}return t}function ae(e){const t=I(f(e,"services")),i={};for(const[o,s]of Object.entries(t)){const r=I(s),u=I(f(r,"local"));i[o]={dependsOn:ue(f(r,"dependsOn")),discovery:D(f(r,"discovery")),localCwd:b(f(u,"cwd")||"."),localDisabled:f(r,"local")===!1}}return{discovery:D(f(e,"discovery")),services:i}}function le(e){const t={};for(const[i,o]of Object.entries(e))o&&(t[i]={discovery:o.discovery,local:o.localDisabled?!1:{baseUrl:"",cwd:o.localCwd,env:{},start:""}});return t}function D(e){try{return G(e,{allowRoots:!0})}catch{return{}}}function ue(e){return Array.isArray(e)?e.filter(i=>typeof i=="string"):[]}function de(e,t,i){for(const o of e.testkit.skip?.suites||[])if(k(t,i,[o.selector]))return o.reason;return null}function fe(e,t,i){const o=new Set;for(const s of e.testkit.requirements.suites)if(k(t,i,[s.selector]))for(const r of s.locks)o.add(r);return[...o].sort()}function N(e){return Array.isArray(e)?e.filter(i=>i!=null).map(i=>h(i)):e==null||e===""?[]:[h(e)]}function b(e){return h(e).split(g.sep).join("/").replace(/^\.\/+/,"")}export function fileDisplayName(e){const t=new RegExp(`\\.(${H.join("|")})\\.testkit\\.ts$`),i=g.posix.basename(e).replace(t,"");return formatDisplayName(i)}function pe(e,t,i,o,s){return[e,t,i,o,s].join("|")}function me(e,t,i,o,s){return[e,t,i,o,b(s)].join("|")}function ve(e,t){return e.service.localeCompare(t.service)||e.testType.localeCompare(t.testType)||e.name.localeCompare(t.name)}function ge(e,t){return e.service.localeCompare(t.service)||e.testType.localeCompare(t.testType)||e.suiteName.localeCompare(t.suiteName)||e.path.localeCompare(t.path)}function ye(e,t){return E(e.severity)-E(t.severity)||e.code.localeCompare(t.code)||(e.path||"").localeCompare(t.path||"")}function E(e){return e==="error"?1:2}
|
|
1
|
+
import g from"path";import{randomUUID as A}from"node:crypto";import{loadConfigContext as P,resolveProductDir as B}from"./load.js";import{discoverProject as H}from"./discovery.js";import{loadTestkitConfig as L}from"./config-loader.js";import{TEST_TYPE_ORDER as V,isRunType as _}from"../../domain/test-types.js";import"../../domain/identifiers.js";import{DEFAULT_RUNTIME_ISOLATION_POLICY as U,resolveRuntimeIsolationPolicy as q}from"./runtime-isolation.js";import{resolveEngineProfile as G}from"../../engines/catalog.js";import{readTestFileMetadata as Y}from"./file-metadata.js";import{normalizeDiscoveryConfig as K}from"./decode-discovery.js";import"./path-policy.js";import{buildDiscoverySummary as J,emptyDiscoverySummary as Q}from"./discovery-summary.js";import{matchesSelectedTypes as W,matchesSuiteSelectors as k,normalizeTypeValues as X,parseSuiteSelectors as Z}from"../../domain/suite-selection.js";import{errorMessage as D}from"../../kernel/error-serialization.js";import{isObjectRecord as R,readObjectRecord as I,readProperty as f}from"./values.js";const ee=9,w=String;export async function discoverTests(e={}){return E(e)}export async function loadDiscoveredConfigContext(e={}){const i=await P({dir:e.dir,configImportCacheSalt:A()}),t=await E({dir:i.productDir,diagnostics:"error"},i);return{context:i,discovery:t}}async function E(e,i){const t=B(process.cwd(),e.dir),o=ae(e),r=i?{config:i.config,configFile:i.configFile,diagnostics:[]}:await ie(t,o.diagnosticsMode),s={schemaVersion:ee,source:"testkit-discovery",product:{name:g.basename(t),directory:t},configFile:r.configFile?g.relative(t,r.configFile)||g.basename(r.configFile):null,filters:{service:o.serviceFilter,types:o.typeValues,suiteSelectors:o.suiteSelectors.map(n=>n.raw),fileNames:o.fileNames,runnableOnly:o.runnableOnly,diagnostics:o.diagnosticsMode},services:[],suites:[],files:[],diagnostics:[...r.diagnostics],summary:Q()};if(!r.config)return T(s);const a=ue(r.config),l=de(a.services),p=i?.discovery??H(t,l,{strict:o.diagnosticsMode==="error",discovery:a.discovery}),v={project:p,fileMetadataByPath:new Map(i?.configs.flatMap(n=>[...n.testkit.fileMetadataByPath])??[])};s.diagnostics.push(...p.diagnostics),ce(o.serviceFilter,a.services,p);let y=i??null;try{y??=await P({dir:t,preparedDiscovery:v,configContext:{config:r.config,configFile:r.configFile},discoveryOptions:{strict:o.diagnosticsMode==="error"}})}catch(n){if(o.diagnosticsMode==="error")throw n;s.diagnostics.push({code:"config_invalid",severity:"error",message:D(n)})}const h=oe({productDir:t,rawDiscovery:p,explicitServices:a.services,configs:y?.configs??[],fileMetadataByPath:v.fileMetadataByPath,filters:o});return T({...s,...h})}export function formatDisplayName(e){return w(e||"").split(/[-_/]+/g).filter(Boolean).map(i=>i.replace(/^\w/,t=>t.toUpperCase())).join(" ")}export function testTypeContract(e,i,t){const o=G(e,i);if(o.id!==t)throw new Error(`Discovery profile mismatch for ${e}: metadata resolved ${t}, catalog resolved ${o.id}`);return e==="data"?{authoringApi:"@gfdelarue/testkit/node-vitest/data",executionHost:o.capabilities.executionHost,summary:"Data suites use the node-vitest engine and record Testkit data evidence; they are not raw Vitest or service-namespace tests."}:e==="contract"?{authoringApi:"@gfdelarue/testkit/node-vitest/contract",executionHost:o.capabilities.executionHost,summary:"Contract suites use the node-vitest engine for Node-backed service/runtime contracts without data-flow evidence requirements."}:i==="k6"?{authoringApi:"@gfdelarue/testkit/k6",executionHost:o.capabilities.executionHost}:i==="playwright"?{authoringApi:"@gfdelarue/testkit/playwright",executionHost:o.capabilities.executionHost}:{executionHost:o.capabilities.executionHost}}async function ie(e,i){try{const t=await L(e),{config:o,configFile:r}=te(t);return{config:o,configFile:r,diagnostics:[]}}catch(t){if(i==="error")throw t;return{config:null,configFile:null,diagnostics:[{code:"config_invalid",severity:"error",message:D(t)}]}}}function te(e){if(!R(e))throw new Error("testkit config loader must return an object");const i=f(e,"config"),t=f(e,"configFile");if(!R(i)){const o=typeof t=="string"?g.basename(t):"testkit config file";throw new Error(`testkit config file must export an object: ${o}`)}if(t!==null&&typeof t!="string")throw new Error("testkit config loader returned an invalid config path");return{config:i,configFile:t}}function oe({configs:e,fileMetadataByPath:i,productDir:t,rawDiscovery:o,explicitServices:r,filters:s}){const a=new Map(e.map(n=>[n.name,n])),l=new Map,p=new Map,v=[],y=new Set([...Object.keys(r),...Object.keys(o.services),...a.keys()]);for(const n of y){if(s.serviceFilter&&n!==s.serviceFilter)continue;const c=a.get(n),d=o.services[n],u=r[n];l.set(n,{name:n,discovered:c!==void 0||!!d,localCwd:c?c.testkit.local?.cwd||".":u?.localCwd||d?.inferredLocalCwd||".",dependsOn:[...c?.testkit.dependsOn??u?.dependsOn??[]],suiteCount:0,fileCount:0,activeFileCount:0,skippedFileCount:0})}for(const n of o.files){const c=n.testType;if(!W(c,s.typeValues)||!k(c,n.suiteName,s.suiteSelectors)||s.serviceFilter&&n.serviceName!==s.serviceFilter||s.fileNameSet.size>0&&!s.fileNameSet.has(n.filePath))continue;const d=i.get(n.filePath)??Y(t,n.filePath);i.set(n.filePath,d);const u=a.get(n.serviceName),m=u?.suites[c]?.find(z=>z.name===n.suiteName);if(u&&(!m||m.testType!==c||m.engineId!==d.engineId||m.profileId!==d.profileId))throw new Error(`Normalized suite ${n.serviceName}/${c}:${n.suiteName} conflicts with file ${n.filePath}`);const S=u&&(d.skipReason||u.testkit.skip?.fileReasonByPath.get(n.filePath)||pe(u,c,n.suiteName))||null;if(s.runnableOnly&&S)continue;const M=u?[...new Set([...me(u,c,n.suiteName),...u.testkit.requirements.fileLocksByPath.get(n.filePath)||[],...d.locks])].sort():[],$=l.get(n.serviceName)?.dependsOn||[],C=ne({serviceName:n.serviceName,suiteName:n.suiteName,testType:c,filePath:n.filePath,engineId:d.engineId,profileId:d.profileId,skipReason:S,locks:M,runtimeIsolation:u&&m?q(u,m,n.filePath):U,dependsOn:$});v.push(C);const O=[n.serviceName,c,n.suiteName].join("|"),F=p.get(O);F?F.files.push(C):p.set(O,{serviceName:n.serviceName,testType:c,suiteName:n.suiteName,files:[C]})}const h=[...p.values()].map(n=>re(n.serviceName,n.testType,n.suiteName,n.files,l.get(n.serviceName)?.dependsOn||[]));for(const n of h){const c=l.get(n.service)||{name:n.service,discovered:!0,localCwd:o.services[n.service]?.inferredLocalCwd||".",dependsOn:[...r[n.service]?.dependsOn||[]],suiteCount:0,fileCount:0,activeFileCount:0,skippedFileCount:0};c.suiteCount+=1,c.fileCount+=n.fileCount,c.activeFileCount+=n.activeFileCount,c.skippedFileCount+=n.skippedFileCount,l.set(n.service,c)}return{services:[...l.values()].sort((n,c)=>n.name.localeCompare(c.name)),suites:h.sort(ye),files:v.sort(he)}}function ne(e){const i=testTypeContract(e.testType,e.engineId,e.profileId);return{id:ve(e.serviceName,e.testType,e.engineId,e.profileId,e.filePath),path:e.filePath,displayName:fileDisplayName(e.filePath),service:e.serviceName,suiteName:e.suiteName,groupLabel:formatDisplayName(e.suiteName),testType:e.testType,engineId:e.engineId,profileId:e.profileId,skipped:!!e.skipReason,skipReason:e.skipReason,locks:e.locks,runtimeIsolation:e.runtimeIsolation,dependsOn:[...e.dependsOn],contract:i}}function re(e,i,t,o,r){const s=se(e,i,t,o),a=o.filter(l=>l.skipped).length;return{id:ge(e,i,s.engineId,s.profileId,t),service:e,name:t,displayName:formatDisplayName(t),testType:i,engineId:s.engineId,profileId:s.profileId,groupLabel:formatDisplayName(t),fileCount:o.length,activeFileCount:o.length-a,skippedFileCount:a,dependsOn:[...r],locks:[...new Set(o.flatMap(l=>l.locks))].sort(),filePaths:o.map(l=>l.path),contract:s.contract}}function se(e,i,t,o){const r=o[0];for(const s of o.slice(1))if(s.engineId!==r.engineId||s.profileId!==r.profileId)throw new Error(`Discovery suite ${e}/${i}:${t} mixes engine profiles ${r.engineId}/${r.profileId} and ${s.engineId}/${s.profileId}`);return{contract:r.contract,engineId:r.engineId,profileId:r.profileId}}function T(e){return{...e,diagnostics:e.diagnostics.sort(we),summary:J({diagnostics:e.diagnostics,files:e.files,serviceCount:e.services.length,suiteCount:e.suites.length})}}function ce(e,i,t){if(!e)return;const o=new Set([...Object.keys(i),...Object.keys(t.services),...Object.keys(t.suitesByService)]);if(o.has(e))return;const r=[...o].sort((s,a)=>s.localeCompare(a)).join(", ");throw new Error(r.length>0?`Service "${e}" not found. Available: ${r}`:`Service "${e}" not found.`)}function ae(e){const i=le(X(b(e.type))),t=Z(b(e.suite)),o=b(e.file).map(N);return{typeValues:i,suiteSelectors:t,fileNames:o,fileNameSet:new Set(o),serviceFilter:e.service||null,runnableOnly:!!e.runnableOnly,diagnosticsMode:e.diagnostics==="error"?"error":"report"}}function le(e){const i=[];for(const t of e){if(!_(t))throw new Error(`Discovery produced unsupported test type "${t}".`);i.push(t)}return i}function ue(e){const i=I(f(e,"services")),t={};for(const[o,r]of Object.entries(i)){const s=I(r),a=I(f(s,"local"));t[o]={dependsOn:fe(f(s,"dependsOn")),discovery:x(f(s,"discovery")),localCwd:N(f(a,"cwd")||"."),localDisabled:f(s,"local")===!1}}return{discovery:x(f(e,"discovery")),services:t}}function de(e){const i={};for(const[t,o]of Object.entries(e))o&&(i[t]={discovery:o.discovery,local:o.localDisabled?!1:{baseUrl:"",cwd:o.localCwd,env:{},start:""}});return i}function x(e){try{return K(e,{allowRoots:!0})}catch{return{}}}function fe(e){return Array.isArray(e)?e.filter(t=>typeof t=="string"):[]}function pe(e,i,t){for(const o of e.testkit.skip?.suites||[])if(k(i,t,[o.selector]))return o.reason;return null}function me(e,i,t){const o=new Set;for(const r of e.testkit.requirements.suites)if(k(i,t,[r.selector]))for(const s of r.locks)o.add(s);return[...o].sort()}function b(e){return Array.isArray(e)?e.filter(t=>t!=null).map(t=>w(t)):e==null||e===""?[]:[w(e)]}function N(e){return w(e).split(g.sep).join("/").replace(/^\.\/+/,"")}export function fileDisplayName(e){const i=new RegExp(`\\.(${V.join("|")})\\.testkit\\.ts$`),t=g.posix.basename(e).replace(i,"");return formatDisplayName(t)}function ge(e,i,t,o,r){return[e,i,t,o,r].join("|")}function ve(e,i,t,o,r){return[e,i,t,o,N(r)].join("|")}function ye(e,i){return e.service.localeCompare(i.service)||e.testType.localeCompare(i.testType)||e.name.localeCompare(i.name)}function he(e,i){return e.service.localeCompare(i.service)||e.testType.localeCompare(i.testType)||e.suiteName.localeCompare(i.suiteName)||e.path.localeCompare(i.path)}function we(e,i){return j(e.severity)-j(i.severity)||e.code.localeCompare(i.code)||(e.path||"").localeCompare(i.path||"")}function j(e){return e==="error"?1:2}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import ts from "typescript";
|
|
1
|
+
import type ts from "typescript";
|
|
2
2
|
import { type EngineId, type RunnerProfileId } from "../../domain/identifiers.js";
|
|
3
3
|
import { type RuntimeIsolationPolicy } from "./runtime-isolation.js";
|
|
4
4
|
import { type EngineAuthoringProfile } from "../../engines/catalog.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{isPathWithin as w}from"../../process/files/paths.js";import g from"fs";import S from"path";import i from"typescript";import{ENGINE_IDS as $,PUBLIC_TEST_TYPES as M,isEngineId as R}from"../../domain/identifiers.js";import{normalizeRuntimeIsolationPolicy as A}from"./runtime-isolation.js";import{resolveEngineProfile as O,expectedEngineAuthoringModule as j,invalidEngineAuthoringImport as B,requireMatchingHostImport as K}from"../../engines/catalog.js";const f="@gfdelarue/testkit",V="@gfdelarue/testkit-engine-";export function loadTestFileMetadataMap(e,t={},r=new Map){for(const s of Object.values(t||{}))for(const n of Object.values(s||{}))for(const o of n||[])for(const a of o.files||[])r.has(a)||r.set(a,readTestFileMetadata(e,a));return r}export function readTestFileMetadata(e,t){const r=pe(e,t),s=fe(t),n=g.readFileSync(r,"utf8"),o=i.createSourceFile(r,n,i.ScriptTarget.Latest,!0,i.ScriptKind.TS),a=me(o);if(a.length>0){const p=a.map(m=>i.flattenDiagnosticMessageText(m.messageText," ")).join("; ");throw c(t,`TypeScript syntax is invalid: ${p}`)}let u=null,l=null;for(const p of o.statements)if(!(!i.isVariableStatement(p)||!H(p))){for(const m of p.declarationList.declarations)if(!(!i.isIdentifier(m.name)||m.name.text!=="testkit")){if(u)throw c(t,'exported "testkit" metadata must be declared exactly once');if(!X(p))throw c(t,'exported "testkit" metadata must be const');u=m,l=p}}if(!u||!l)throw c(t,'export const testkit = defineFile({ engine: "..." }) is required');Q(o,l,t);const y=_(u.initializer,t,s);return requireMatchingEngineAuthoringImports(o,t,y),y}function _(e,t,r){if(!e)throw c(t,'exported "testkit" metadata has no value');if(!i.isCallExpression(e))throw c(t,"testkit metadata must use defineFile({ ... })");if(J(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(!i.isObjectLiteralExpression(s))throw c(t,"defineFile metadata must receive an object literal");return W(s,t,r)}function W(e,t,r){const s={locks:[],runtimeIsolation:null,skipReason:null};let n=null;const o=new Set;for(const u of e.properties){if(i.isSpreadAssignment(u))throw c(t,"testkit metadata may not contain spread properties");if(!i.isPropertyAssignment(u))throw c(t,"testkit metadata properties must use key: value assignments");const l=L(u.name,t,"testkit metadata");if(!Y(l))throw c(t,`unknown metadata property "${l}"`);if(o.has(l))throw c(t,`duplicate metadata property "${l}"`);if(o.add(l),l==="engine"){n=I(u.initializer,t,'metadata property "engine"');continue}if(l==="locks"){s.locks=z(u.initializer,t);continue}if(l==="skip"){s.skipReason=G(u.initializer,t);continue}s.runtimeIsolation=U(u.initializer,t)}if(!n)throw c(t,'a literal "engine" property is required');if(!R(n))throw c(t,`unsupported engine "${n}"; expected one of: ${$.join(", ")}`);let a;try{a=O(r,n)}catch(u){throw c(t,C(u),u)}return{engineId:n,locks:s.locks,profileId:a.id,runtimeIsolation:s.runtimeIsolation,skipReason:s.skipReason}}function z(e,t){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(h(s,t,'metadata property "locks" entry'))}return[...new Set(r)].sort()}function G(e,t){const r=x(e);if(r!==null)return b(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(L(s.name,t,'metadata property "skip"')!=="reason")throw c(t,'metadata property "skip" object only accepts "reason"');return h(s.initializer,t,'metadata property "skip.reason"')}function U(e,t){const r=I(e,t,'metadata property "runtimeIsolation"');try{return A(r,"defineFile runtimeIsolation")}catch(s){throw c(t,C(s),s)}}function J(e){return i.isIdentifier(e)?e.text:null}function L(e,t,r){if(i.isIdentifier(e)||i.isStringLiteral(e))return e.text;throw c(t,`${r} property names must be plain identifiers or string literals`)}function x(e){return i.isStringLiteral(e)||i.isNoSubstitutionTemplateLiteral(e)?e.text:null}function I(e,t,r){const s=x(e);if(s===null)throw c(t,`${r} must be a string literal`);return b(s,t,r)}function h(e,t,r){return I(e,t,r).trim()}function b(e,t,r){if(e.trim().length===0)throw c(t,`${r} must not be empty`);return e}function H(e){return(e.modifiers||[]).some(t=>t.kind===i.SyntaxKind.ExportKeyword)}function X(e){return(e.declarationList.flags&i.NodeFlags.Const)!==0}function Y(e){return["engine","locks","runtimeIsolation","skip"].includes(e)}function Q(e,t,r){let s=null,n=0;for(const o of e.statements){if(i.isImportDeclaration(o)){const a=o.importClause;if(!a||a.phaseModifier===i.SyntaxKind.TypeKeyword)continue;a.name?.text==="defineFile"&&(n+=1);const u=a.namedBindings;if(u&&i.isNamespaceImport(u)){u.name.text==="defineFile"&&(n+=1);continue}if(!u||!i.isNamedImports(u))continue;for(const l of u.elements){if(l.isTypeOnly||l.name.text!=="defineFile")continue;if(n+=1,(l.propertyName?.text??l.name.text)==="defineFile"&&i.isStringLiteral(o.moduleSpecifier)&&o.moduleSpecifier.text===f){if(s)throw c(r,`defineFile must have exactly one named value import from "${f}"`);s=o}}continue}n+=le(o)}if(!s)throw c(r,`defineFile must be a named value import from "${f}"`);if(n!==1)throw c(r,"the imported defineFile binding may not be shadowed or redeclared");if(s.getStart(e)>t.getStart(e))throw c(r,"the defineFile import must precede metadata")}export function requireMatchingEngineAuthoringImports(e,t,r){Z(e,t,r);const s=j(r.profileId);for(const n of collectModuleSpecifiers(e))if(K(t,r,n),!(n===f||n===s||!n.startsWith(`${f}/`)&&!n.startsWith(V)))throw B(t,r,n,s)}function Z(e,t,r){const s=F(e),n=o=>{if(P(o,t,r),ee(o,s,t,r),i.isCallExpression(o)&&D(o,s)){const a=o.arguments.at(0);if(!a||!i.isStringLiteralLike(a)){const u=o.expression.kind===i.SyntaxKind.ImportKeyword?"import()":"require()";throw new Error(`Invalid Testkit authoring in "${t}": ${r.engineId}/${r.profileId} may not use non-literal ${u}; module specifiers must be string literals so engine boundaries can be verified`)}}for(const a of q(o))n(a);i.forEachChild(o,n)};n(e)}function D(e,t){return e.expression.kind===i.SyntaxKind.ImportKeyword||i.isIdentifier(e.expression)&&e.expression.text==="require"&&!E(e.expression,t)}function P(e,t,r){if(i.isImportDeclaration(e)){const n=e.importClause?.namedBindings;if(n&&i.isNamedImports(n)&&n.elements.find(a=>!a.isTypeOnly&&(a.propertyName?.text??a.name.text)==="createRequire"))throw d(t,r,"createRequire")}if(i.isPropertyAccessExpression(e)&&e.name.text==="createRequire"||i.isElementAccessExpression(e)&&x(e.argumentExpression)==="createRequire"||i.isBindingElement(e)&&(ne(e)==="createRequire"||i.isIdentifier(e.name)&&e.name.text==="createRequire"))throw d(t,r,"createRequire")}function ee(e,t,r,s){if(i.isPropertyAccessExpression(e)&&e.name.text==="require"&&T(e.expression,t)||i.isElementAccessExpression(e)&&x(e.argumentExpression)==="require"&&T(e.expression,t))throw d(r,s,e.getText());if(i.isIdentifier(e)&&e.text==="require"&&!E(e,t)&&!te(e)&&!ie(e))throw d(r,s,"global require")}function d(e,t,r){return new Error(`Invalid Testkit authoring in "${e}": ${t.engineId}/${t.profileId} may not alias or indirectly access the module loader through ${r}; use literal import and require specifiers so engine boundaries can be verified`)}function T(e,t){return i.isIdentifier(e)&&(e.text==="globalThis"||e.text==="module")&&!E(e,t)}function te(e){return i.isCallExpression(e.parent)&&e.parent.expression===e}function ie(e){const t=e.parent;return re(e)||i.isPropertyAccessExpression(t)&&t.name===e||i.isPropertyAssignment(t)&&t.name===e||i.isImportSpecifier(t)&&t.propertyName===e||i.isExportSpecifier(t)&&t.parent.parent.moduleSpecifier!==void 0}function re(e){const t=e.parent;return t.name===e&&!i.isShorthandPropertyAssignment(t)&&!i.isExportSpecifier(t)}function ne(e){const t=e.propertyName;return t&&(i.isIdentifier(t)||i.isStringLiteralLike(t))?t.text:null}function F(e){const t=new WeakMap,r=(n,o)=>{let a=t.get(n);a||(a=new Set,t.set(n,a)),N(a,o)},s=n=>{if(i.isImportDeclaration(n))se(n,e,r);else if(i.isImportEqualsDeclaration(n)&&!n.isTypeOnly)r(e,n.name);else if(i.isVariableDeclaration(n)){const o=n.parent,a=i.isVariableDeclarationList(o)&&(o.flags&i.NodeFlags.BlockScoped)===0?oe(n):k(n);r(a,n.name)}else i.isParameter(n)?r(ae(n),n.name):i.isFunctionDeclaration(n)&&n.name?r(k(n),n.name):i.isFunctionExpression(n)&&n.name?r(n,n.name):(i.isClassDeclaration(n)||i.isEnumDeclaration(n)||i.isModuleDeclaration(n))&&n.name&&i.isIdentifier(n.name)?r(k(n),n.name):i.isClassExpression(n)&&n.name?r(n,n.name):i.isCatchClause(n)&&n.variableDeclaration&&r(n,n.variableDeclaration.name);i.forEachChild(n,s)};return s(e),t}function se(e,t,r){const s=e.importClause;if(!s||s.phaseModifier===i.SyntaxKind.TypeKeyword)return;s.name&&r(t,s.name);const n=s.namedBindings;if(n&&i.isNamespaceImport(n))r(t,n.name);else if(n&&i.isNamedImports(n))for(const o of n.elements)o.isTypeOnly||r(t,o.name)}function N(e,t){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){let r=e;for(;;){if(t.get(r)?.has(e.text))return!0;if(i.isSourceFile(r))return!1;r=r.parent}}function k(e){let t=e.parent;for(;;){if(ce(t))return t;t=t.parent}}function oe(e){let t=e.parent;for(;;){if(i.isFunctionLike(t)||i.isSourceFile(t))return t;t=t.parent}}function ae(e){let t=e.parent;for(;;){if(i.isFunctionLike(t)||i.isSourceFile(t))return t;t=t.parent}}function ce(e){return i.isSourceFile(e)||i.isBlock(e)||i.isCaseBlock(e)||i.isCatchClause(e)||i.isForStatement(e)||i.isForInStatement(e)||i.isForOfStatement(e)||i.isFunctionLike(e)||i.isClassStaticBlockDeclaration(e)}export function collectModuleSpecifiers(e){const t=new Set,r=F(e),s=n=>{const o=ue(n,r);o&&t.add(o);for(const a of q(n))s(a);i.forEachChild(n,s)};return s(e),t}function q(e){return e.jsDoc??[]}function ue(e,t){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)||!D(e,t))return null;const r=e.arguments[0];return i.isStringLiteralLike(r)?r.text:null}function le(e){return i.isVariableStatement(e)?e.declarationList.declarations.reduce((t,r)=>t+v(r.name,"defineFile"),0):(i.isClassDeclaration(e)||i.isEnumDeclaration(e)||i.isFunctionDeclaration(e))&&e.name?.text==="defineFile"?1:0}function v(e,t){return i.isIdentifier(e)?e.text===t?1:0:e.elements.reduce((r,s)=>i.isOmittedExpression(s)?r:r+v(s.name,t),0)}function pe(e,t){const r=S.resolve(e);if(S.isAbsolute(t))throw new Error(`Test file metadata path must be product-relative: ${t}`);const s=S.resolve(r,t);if(!w(r,s))throw new Error(`Test file metadata path escapes the product root: ${t}`);if(!g.existsSync(s))throw new Error(`Test file metadata source does not exist: ${t}`);const n=g.realpathSync(r),o=g.realpathSync(s);if(!w(n,o))throw new Error(`Test file metadata path resolves outside the product root: ${t}`);return o}function me(e){return e.parseDiagnostics??[]}function fe(e){const t=M.find(r=>e.endsWith(`.${r}.testkit.ts`));if(t)return t;throw new Error(`Cannot read Testkit file metadata from noncanonical path: ${e}`)}function c(e,t,r){return new Error(`Invalid testkit metadata in "${e}": ${t}.`,{cause:r})}function C(e){return e instanceof Error?e.message.replace(/\.$/u,""):String(e)}
|
|
1
|
+
import{isPathWithin as L}from"../../process/files/paths.js";import y from"fs";import E from"path";import{createRequire as M}from"node:module";import{fileURLToPath as R}from"node:url";import{ENGINE_IDS as A,PUBLIC_TEST_TYPES as O,isEngineId as j}from"../../domain/identifiers.js";import{normalizeRuntimeIsolationPolicy as B}from"./runtime-isolation.js";import{resolveEngineProfile as K,expectedEngineAuthoringModule as V,invalidEngineAuthoringImport as _,requireMatchingHostImport as W}from"../../engines/catalog.js";const z=M(import.meta.url),U=R(import.meta.resolve("typescript"));let G;function a(){return G??=z(U)}const d="@gfdelarue/testkit",J="@gfdelarue/testkit-engine-";export function loadTestFileMetadataMap(e,t={},i=new Map){for(const r of Object.values(t||{}))for(const s of Object.values(r||{}))for(const n of s||[])for(const o of n.files||[])i.has(o)||i.set(o,readTestFileMetadata(e,o));return i}export function readTestFileMetadata(e,t){const i=a(),r=xe(e,t),s=Ee(t),n=y.readFileSync(r,"utf8"),o=i.createSourceFile(r,n,i.ScriptTarget.Latest,!0,i.ScriptKind.TS),u=Se(o);if(u.length>0){const m=u.map(f=>i.flattenDiagnosticMessageText(f.messageText," ")).join("; ");throw c(t,`TypeScript syntax is invalid: ${m}`)}let l=null,p=null;for(const m of o.statements)if(!(!i.isVariableStatement(m)||!ee(m))){for(const f of m.declarationList.declarations)if(!(!i.isIdentifier(f.name)||f.name.text!=="testkit")){if(l)throw c(t,'exported "testkit" metadata must be declared exactly once');if(!te(m))throw c(t,'exported "testkit" metadata must be const');l=f,p=m}}if(!l||!p)throw c(t,'export const testkit = defineFile({ engine: "..." }) is required');re(o,p,t);const S=H(l.initializer,t,s);return requireMatchingEngineAuthoringImports(o,t,S),S}function H(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(P(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 l of e.properties){if(r.isSpreadAssignment(l))throw c(t,"testkit metadata may not contain spread properties");if(!r.isPropertyAssignment(l))throw c(t,"testkit metadata properties must use key: value assignments");const p=h(l.name,t,"testkit metadata");if(!ie(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=I(l.initializer,t,'metadata property "engine"');continue}if(p==="locks"){s.locks=Y(l.initializer,t);continue}if(p==="skip"){s.skipReason=Q(l.initializer,t);continue}s.runtimeIsolation=Z(l.initializer,t)}if(!n)throw c(t,'a literal "engine" property is required');if(!j(n))throw c(t,`unsupported engine "${n}"; expected one of: ${A.join(", ")}`);let u;try{u=K(i,n)}catch(l){throw c(t,$(l),l)}return{engineId:n,locks:s.locks,profileId:u.id,runtimeIsolation:s.runtimeIsolation,skipReason:s.skipReason}}function Y(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(b(s,t,'metadata property "locks" entry'))}return[...new Set(r)].sort()}function Q(e,t){const i=a(),r=x(e);if(r!==null)return T(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(h(s.name,t,'metadata property "skip"')!=="reason")throw c(t,'metadata property "skip" object only accepts "reason"');return b(s.initializer,t,'metadata property "skip.reason"')}function Z(e,t){const i=I(e,t,'metadata property "runtimeIsolation"');try{return B(i,"defineFile runtimeIsolation")}catch(r){throw c(t,$(r),r)}}function P(e){return a().isIdentifier(e)?e.text:null}function h(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 x(e){const t=a();return t.isStringLiteral(e)||t.isNoSubstitutionTemplateLiteral(e)?e.text:null}function I(e,t,i){const r=x(e);if(r===null)throw c(t,`${i} must be a string literal`);return T(r,t,i)}function b(e,t,i){return I(e,t,i).trim()}function T(e,t,i){if(e.trim().length===0)throw c(t,`${i} must not be empty`);return e}function ee(e){const t=a();return(e.modifiers||[]).some(i=>i.kind===t.SyntaxKind.ExportKeyword)}function te(e){const t=a();return(e.declarationList.flags&t.NodeFlags.Const)!==0}function ie(e){return["engine","locks","runtimeIsolation","skip"].includes(e)}function re(e,t,i){const r=a();let s=null,n=0;for(const o of e.statements){if(r.isImportDeclaration(o)){const u=o.importClause;if(!u||u.phaseModifier===r.SyntaxKind.TypeKeyword)continue;u.name?.text==="defineFile"&&(n+=1);const l=u.namedBindings;if(l&&r.isNamespaceImport(l)){l.name.text==="defineFile"&&(n+=1);continue}if(!l||!r.isNamedImports(l))continue;for(const p of l.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===d){if(s)throw c(i,`defineFile must have exactly one named value import from "${d}"`);s=o}}continue}n+=ye(o)}if(!s)throw c(i,`defineFile must be a named value import from "${d}"`);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){ne(e,t,i);const r=V(i.profileId);for(const s of collectModuleSpecifiers(e))if(W(t,i,s),!(s===d||s===r||!s.startsWith(`${d}/`)&&!s.startsWith(J)))throw _(t,i,s,r)}function ne(e,t,i){const r=a(),s=q(e),n=o=>{if(se(o,t,i),oe(o,s,t,i),r.isCallExpression(o)&&D(o,s)){const u=o.arguments.at(0);if(!u||!r.isStringLiteralLike(u)){const l=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 ${l}; module specifiers must be string literals so engine boundaries can be verified`)}}for(const u of v(o))n(u);r.forEachChild(o,n)};n(e)}function D(e,t){const i=a();return e.expression.kind===i.SyntaxKind.ImportKeyword||i.isIdentifier(e.expression)&&e.expression.text==="require"&&!k(e.expression,t)}function se(e,t,i){const r=a();if(r.isImportDeclaration(e)){const n=e.importClause?.namedBindings;if(n&&r.isNamedImports(n)&&n.elements.find(u=>!u.isTypeOnly&&(u.propertyName?.text??u.name.text)==="createRequire"))throw g(t,i,"createRequire")}if(r.isPropertyAccessExpression(e)&&e.name.text==="createRequire"||r.isElementAccessExpression(e)&&x(e.argumentExpression)==="createRequire"||r.isBindingElement(e)&&(le(e)==="createRequire"||r.isIdentifier(e.name)&&e.name.text==="createRequire"))throw g(t,i,"createRequire")}function oe(e,t,i,r){const s=a();if(s.isPropertyAccessExpression(e)&&e.name.text==="require"&&F(e.expression,t)||s.isElementAccessExpression(e)&&x(e.argumentExpression)==="require"&&F(e.expression,t))throw g(i,r,e.getText());if(s.isIdentifier(e)&&e.text==="require"&&!k(e,t)&&!ae(e)&&!ce(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 F(e,t){return a().isIdentifier(e)&&(e.text==="globalThis"||e.text==="module")&&!k(e,t)}function ae(e){return a().isCallExpression(e.parent)&&e.parent.expression===e}function ce(e){const t=a(),i=e.parent;return ue(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 ue(e){const t=a(),i=e.parent;return i.name===e&&!t.isShorthandPropertyAssignment(i)&&!t.isExportSpecifier(i)}function le(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 u=i.get(n);u||(u=new Set,i.set(n,u)),N(u,o)},s=n=>{if(t.isImportDeclaration(n))pe(n,e,r);else if(t.isImportEqualsDeclaration(n)&&!n.isTypeOnly)r(e,n.name);else if(t.isVariableDeclaration(n)){const o=n.parent,u=t.isVariableDeclarationList(o)&&(o.flags&t.NodeFlags.BlockScoped)===0?me(n):w(n);r(u,n.name)}else t.isParameter(n)?r(fe(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 pe(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 k(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(de(t))return t;t=t.parent}}function me(e){const t=a();let i=e.parent;for(;;){if(t.isFunctionLike(i)||t.isSourceFile(i))return i;i=i.parent}}function fe(e){const t=a();let i=e.parent;for(;;){if(t.isFunctionLike(i)||t.isSourceFile(i))return i;i=i.parent}}function de(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=ge(n,r);o&&i.add(o);for(const u of v(n))s(u);t.forEachChild(n,s)};return s(e),i}function v(e){return e.jsDoc??[]}function ge(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)||!D(e,t))return null;const r=e.arguments[0];return i.isStringLiteralLike(r)?r.text:null}function ye(e){const t=a();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=a();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 xe(e,t){const i=E.resolve(e);if(E.isAbsolute(t))throw new Error(`Test file metadata path must be product-relative: ${t}`);const r=E.resolve(i,t);if(!L(i,r))throw new Error(`Test file metadata path escapes the product root: ${t}`);if(!y.existsSync(r))throw new Error(`Test file metadata source does not exist: ${t}`);const s=y.realpathSync(i),n=y.realpathSync(r);if(!L(s,n))throw new Error(`Test file metadata path resolves outside the product root: ${t}`);return n}function Se(e){return e.parseDiagnostics??[]}function Ee(e){const t=O.find(i=>e.endsWith(`.${i}.testkit.ts`));if(t)return t;throw new Error(`Cannot read Testkit file metadata from noncanonical path: ${e}`)}function c(e,t,i){return new Error(`Invalid testkit metadata in "${e}": ${t}.`,{cause:i})}function $(e){return e instanceof Error?e.message.replace(/\.$/u,""):String(e)}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { loadConfigContext, loadConfigs, parseDotenv, resolveProductDir, resolveServiceCwd, type LoadConfigContextOptions, type LoadConfigsOptions, type NormalizedConfigContext, type NormalizedLoadedDatabaseConfig, type NormalizedLocalServiceConfig, type NormalizedServiceConfig, type NormalizedServiceTestkitConfig, type NormalizedSuiteConfig, type ServiceSuites, } from "./load.js";
|
|
2
|
-
export { discoverTests, fileDisplayName, formatDisplayName, testTypeContract, type DiscoverTestsOptions, type DiscoveryDiagnostic, type DiscoveryFile, type DiscoveryResult, type DiscoveryService, type DiscoverySuite, type DiscoverySummary, type DiscoveryTestContract, type DiscoveryTestType, } from "./discover-tests.js";
|
|
2
|
+
export { discoverTests, loadDiscoveredConfigContext, fileDisplayName, formatDisplayName, testTypeContract, type DiscoverTestsOptions, type DiscoveryDiagnostic, type DiscoveryFile, type DiscoveryResult, type DiscoveryService, type DiscoverySuite, type DiscoverySummary, type DiscoveryTestContract, type DiscoveryTestType, } from "./discover-tests.js";
|
|
3
3
|
export { findConfigFile, loadTestkitConfig } from "./config-loader.js";
|
|
4
4
|
export { materializeDatabaseConfig, type DatabaseMaterializationInput, type MaterializedDatabaseConfig, } from "./database-materialization.js";
|
|
5
5
|
export type { NormalizedDatabaseConfig, NormalizedDatabaseTemplateConfig, NormalizedSourceSchemaConfig, } from "./database.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export{loadConfigContext,loadConfigs,parseDotenv,resolveProductDir,resolveServiceCwd}from"./load.js";export{discoverTests,fileDisplayName,formatDisplayName,testTypeContract}from"./discover-tests.js";export{findConfigFile,loadTestkitConfig}from"./config-loader.js";export{materializeDatabaseConfig}from"./database-materialization.js";export{detectNextApp}from"./runtime.js";export{DEFAULT_RUNTIME_ISOLATION_POLICY,resolveRuntimeIsolationPolicy}from"./runtime-isolation.js";export{collectModuleSpecifiers,requireMatchingEngineAuthoringImports}from"./file-metadata.js";export{minimumConfirmingRuns}from"./cloud.js";
|
|
1
|
+
export{loadConfigContext,loadConfigs,parseDotenv,resolveProductDir,resolveServiceCwd}from"./load.js";export{discoverTests,loadDiscoveredConfigContext,fileDisplayName,formatDisplayName,testTypeContract}from"./discover-tests.js";export{findConfigFile,loadTestkitConfig}from"./config-loader.js";export{materializeDatabaseConfig}from"./database-materialization.js";export{detectNextApp}from"./runtime.js";export{DEFAULT_RUNTIME_ISOLATION_POLICY,resolveRuntimeIsolationPolicy}from"./runtime-isolation.js";export{collectModuleSpecifiers,requireMatchingEngineAuthoringImports}from"./file-metadata.js";export{minimumConfirmingRuns}from"./cloud.js";
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { type ManagedProcessResult } from "../process/index.js";
|
|
2
|
+
/** Only successful metadata reads bypass the original command and its diagnostics. */
|
|
3
|
+
export declare function runDockerMetadataCommand(args: readonly string[], displayLabel: string, timeoutMs: number, signal: AbortSignal | undefined, fallback: (remainingMs: number) => Promise<ManagedProcessResult>): Promise<ManagedProcessResult>;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import V from"node:crypto";import O from"node:fs";import A from"node:http";import k from"node:path";import{MAX_SUPERVISED_PROCESS_CAPTURE_BYTES as H}from"../process/process-limits.js";import{managedHostProcessInfrastructureError as I,normalizeManagedHostEnvironment as $,resolveManagedHostExecutable as j,runManagedHostProcess as L}from"../process/index.js";const N=5e3,B=256*1024,U=8*1024*1024,_=H/2,K=["DOCKER_HOST","DOCKER_TLS","DOCKER_TLS_VERIFY","DOCKER_CERT_PATH","DOCKER_API_VERSION","DOCKER_CUSTOM_HEADERS"];let b;export async function runDockerMetadataCommand(e,t,n,r,u){const o=performance.now()+n,h=()=>Math.floor(o-performance.now()),S=()=>{const s=R("");throw I(t,{...s,exitCode:null,cancelled:r?.aborted===!0,timedOut:r?.aborted!==!0})??new Error(`${t} exceeded its managed deadline`)};if(r?.aborted)return u(Math.max(1,h()));const m=new AbortController,y=()=>{m.abort(r?.reason)};r?.addEventListener("abort",y,{once:!0});const i=setTimeout(()=>{m.abort(new Error("Docker metadata probe timed out"))},Math.min(N,n));let l;try{const s=await v(m.signal);if(s!==null){let c=b?.key===s.key?b.value:void 0;c===void 0&&(c=await J(s,h,m.signal),m.signal.aborted||(b={key:s.key,value:c})),c!==null&&(l=await Y(c,e,m.signal),l!==void 0&&(await v(m.signal))?.key!==s.key&&(l=void 0,b=void 0))}}catch(s){if(d(d(s)?.result)?.terminationError)throw s;l=void 0}finally{clearTimeout(i),r?.removeEventListener("abort",y)}const a=h();return(r?.aborted||a<=0)&&S(),l!==void 0&&!m.signal.aborted?R(l):u(a)}async function v(e){e.throwIfAborted();const t=process.cwd(),n=$(process.env),r=j("docker",t,n),u=f=>q(n,f);if(K.some(f=>u(f)!==""))return null;const o=u(process.platform==="win32"?"USERPROFILE":"HOME"),h=u("DOCKER_CONFIG");if(!h&&(!o||!k.isAbsolute(o)))return null;const S=k.resolve(t,h||k.join(o,".docker")),m=await D(k.join(S,"config.json"),e),y=m?.toString("utf8"),i=y===void 0?{}:d(JSON.parse(y));if(i===null||y!==void 0&&W(y))return null;for(const f of Object.keys(i))if(f.toLowerCase()==="httpheaders"&&(d(i[f])===null||Object.keys(i[f]).length>0)||f.toLowerCase()==="currentcontext"&&f!=="currentContext")return null;if(i.currentContext!==void 0&&typeof i.currentContext!="string")return null;const l=u("DOCKER_CONTEXT")||i.currentContext||"default";let a=null,s=null,c=null,g=null;if(l!=="default"){const f=V.createHash("sha256").update(l).digest("hex");if(a=k.join(S,"contexts","meta",f),s=await D(k.join(a,"meta.json"),e),s===null)return null;const E=d(JSON.parse(s.toString("utf8"))),C=d(d(E?.Endpoints)?.docker);if(E?.Name!==l||typeof C?.Host!="string"||C.SkipTLSVerify!==!1)return null;c=C.Host;try{if(g=await O.promises.readdir(k.join(S,"contexts","tls",f)),g.length!==0)return null}catch(M){if(!P(M,"ENOENT"))return null}}const w=await O.promises.realpath(r),p=await O.promises.stat(w,{bigint:!0});return e.throwIfAborted(),{executable:r,cwd:t,env:n,context:l,metadataPath:a,endpoint:c,key:JSON.stringify([t,Object.entries(n).sort(([f],[E])=>f<E?-1:f>E?1:0),r,w,p.dev.toString(),p.ino.toString(),p.size.toString(),p.mtimeNs.toString(),p.ctimeNs.toString(),S,m?.toString("base64")??null,s?.toString("base64")??null,g])}}async function J(e,t,n){const r=async l=>{n.throwIfAborted();const a=Math.min(N,t());if(a<=0)return null;const s=await L({executable:e.executable,args:l,cwd:e.cwd,env:e.env,displayLabel:"Docker metadata transport resolution",timeoutMs:a,signal:n}),c=I("Docker metadata transport resolution",s);if(s.terminationError&&c!==null)throw c;return s.exitCode!==0||c!==null?null:JSON.parse(s.stdout)},u=await r(["context","inspect"]),o=Array.isArray(u)&&u.length===1?d(u[0]):null,h=d(d(o?.Endpoints)?.docker),S=d(o?.TLSMaterial),m=d(o?.Storage);if(o?.Name!==e.context||typeof h?.Host!="string"||h.SkipTLSVerify!==!1||S===null||Object.keys(S).length!==0||e.metadataPath!==null&&(m?.MetadataPath!==e.metadataPath||h.Host!==e.endpoint))return null;const y=F(h.Host);if(y===null)return null;const i=d(await r(["version","--format","{{json .Client}}"]));return typeof i?.Version!="string"||!/^29\.\d+\.\d+$/u.test(i.Version)||i.Context!==e.context||typeof i.Os!="string"||!x(i.DefaultAPIVersion)||!x(i.ApiVersion)?null:{key:e.key,socketPath:y,userAgent:`Docker-Client/${i.Version} (${i.Os})`,maximum:i.DefaultAPIVersion,negotiated:i.ApiVersion,server:null}}async function Y(e,t,n){const r=z(t);if(r===null)return;const u=new A.Agent({keepAlive:!0,maxSockets:1}),o=new Map;let h;const S=m=>new Promise((y,i)=>{const l=A.get({socketPath:e.socketPath,path:m,agent:u,signal:n,headers:{Host:"api.moby.localhost","User-Agent":e.userAgent}},a=>{const s=[];let c=0;a.on("data",g=>{c+=g.length,c>U?l.destroy(new Error("Docker metadata response exceeded its byte limit")):s.push(g)}),a.once("error",i),a.once("end",()=>{a.statusCode!==200||!a.complete?i(new Error("Docker metadata response was not a complete success")):y({headers:a.headers,body:Buffer.concat(s,c)})})});l.once("error",i),l.once("socket",a=>{o.has(a)||o.set(a,new Promise(s=>a.once("close",()=>{s()}))),h!==void 0&&a!==h&&l.destroy(new Error("Docker metadata connection changed during negotiation")),h=a})});try{const m=await S("/_ping"),y=d(JSON.parse((await S("/version")).body.toString("utf8"))),i=y?.ApiVersion,l=y?.MinAPIVersion;if(!x(i)||!x(l)||m.headers["api-version"]!==i)return;const a=T(e.maximum,i)<=0?e.maximum:i;if(a!==e.negotiated||T(a,l)<0){b=void 0;return}const s=JSON.stringify(y);if(e.server!==null&&e.server!==s){b=void 0;return}e.server=s;const c=await S(`/v${a}${r.path}`),g=JSON.parse(c.body.toString("utf8"));let w;if(r.kind==="container"||r.kind==="volume"){const p=d(g)?.[r.kind==="container"?"Id":"Name"];if(typeof p!="string"||p.length===0)return;const f=JSON.stringify([g],null,4).replace(/[<>&\u2028\u2029]/gu,E=>`\\u${E.charCodeAt(0).toString(16).padStart(4,"0")}`);if(c.body.length+Buffer.byteLength(f)+16>_)return;w=`[${c.body.toString("utf8")}]`}else if(r.kind==="containers"){if(!Array.isArray(g)||g.some(p=>typeof d(p)?.Id!="string"||!/^[a-f0-9]{64}$/u.test(d(p)?.Id)))return;w=g.map(p=>(d(p)?.Id).slice(0,12)).join(`
|
|
2
|
+
`)}else{const p=d(g)?.Volumes;if(p===null)w="";else{if(!Array.isArray(p)||p.length>1)return;const f=p.map(E=>d(E)?.Name);if(f.some(E=>typeof E!="string"||E.length===0))return;w=f.join(`
|
|
3
|
+
`)}}return Buffer.byteLength(w)+1<=_?w:void 0}finally{u.destroy();for(const m of o.keys())m.destroy();await Promise.all(o.values())}}function z(e){if(e.length===2&&e[0]==="inspect"&&e[1]&&!e[1].startsWith("-"))return{kind:"container",path:`/containers/${encodeURIComponent(e[1])}/json`};if(e.length===3&&e[0]==="volume"&&e[1]==="inspect"&&e[2]&&!e[2].startsWith("-"))return{kind:"volume",path:`/volumes/${encodeURIComponent(e[2])}`};let t,n;if(e[0]==="ps"&&e[1]==="-a"&&e[2]==="--format"&&e[3]==="{{.ID}}")t="containers",n=e.slice(4);else if(e[0]==="volume"&&e[1]==="ls"&&e[2]==="-q")t="volumes",n=e.slice(3);else return null;const r=[];for(let o=0;o<n.length;o+=2){if(n[o]!=="--filter"||!n[o+1]?.startsWith("label="))return null;r.push(n[o+1].slice(6))}const u=encodeURIComponent(JSON.stringify({label:r}));return{kind:t,path:t==="containers"?`/containers/json?all=true&filters=${u}`:`/volumes?filters=${u}`}}async function D(e,t){const n=O.createReadStream(e,{signal:t,highWaterMark:65536}),r=[];let u=0;try{for await(const o of n){if(u+=o.length,u>B)throw new Error("Docker metadata configuration exceeded its byte limit");r.push(o)}return Buffer.concat(r,u)}catch(o){if(P(o,"ENOENT"))return null;throw o}finally{n.destroy(),n.closed||await new Promise(o=>n.once("close",o))}}function F(e){if(process.platform==="win32"){const n=/^npipe:\/\/\/\/\.\/pipe\/([a-z0-9_.-]+)$/iu.exec(e);return n===null?null:`\\\\.\\pipe\\${n[1]}`}if(!e.startsWith("unix:///"))return null;const t=e.slice(7);return t.includes("\0")||t.includes("?")||t.includes("#")?null:t}function W(e){for(const t of e.matchAll(/"(?:\\.|[^"\\])*"/gu)){let n=t.index+t[0].length;for(;n<e.length&&/\s/u.test(e[n]);)n+=1;if(e[n]!==":")continue;const r=JSON.parse(t[0]);if(typeof r=="string"&&/^httpheaders$/iu.test(r))return!0}return!1}function q(e,t){return process.platform!=="win32"?e[t]||"":Object.entries(e).find(([n])=>n.toUpperCase()===t)?.[1]||""}function d(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)?e:null}function P(e,t){return d(e)?.code===t}function x(e){return typeof e=="string"&&/^1\.[1-9]\d*$/u.test(e)&&Number.isSafeInteger(Number(e.slice(2)))}function T(e,t){return Number(e.slice(2))-Number(t.slice(2))}function R(e){return{cancelled:!1,error:null,exitCode:0,exitSignal:null,outputLimitExceeded:!1,stderr:"",stdout:e,terminationError:null,timedOut:!1}}
|
package/dist/database/docker.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
${
|
|
3
|
-
${
|
|
4
|
-
`).trim(),n
|
|
5
|
-
${
|
|
1
|
+
import{runDockerMetadataCommand as f}from"./docker-metadata.js";import C from"crypto";import T from"fs";import w from"path";import{managedHostProcessInfrastructureError as I,runManagedHostProcess as y}from"../process/index.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 _=4e3,m=String;export const DOCKER_OWNERSHIP_OPERATION_TIMEOUTS_MS=Object.freeze({inspect:3e4,list:3e4,remove:12e4,stop:3e4});export function buildProductIdentity(t){const e=S(t);return{dir:e,id:v(e,24)}}export function buildDockerResourceLabels(t,e){if(!e.generation)throw new Error("Docker resource generation must be non-empty");const n=buildProductIdentity(t);return{[TESTKIT_MANAGED_LABEL]:"true",[TESTKIT_RESOURCE_KIND_LABEL]:e.kind,[TESTKIT_RESOURCE_NAME_LABEL]:e.name,[TESTKIT_PRODUCT_DIR_LABEL]:n.dir,[TESTKIT_PRODUCT_ID_LABEL]:n.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,n])=>["--label",`${e}=${m(n)}`])}export async function inspectDockerContainer(t,e={}){const n=`Docker container inspect for ${t}`,r=["inspect",t],o=await f(r,n,DOCKER_OWNERSHIP_OPERATION_TIMEOUTS_MS.inspect,e.signal,c=>runDockerCommand(r,n,c,e.signal));if(o.exitCode!==0){if(g(o))return null;throw a(n,o)}return h(o,n,"Id")}export async function listManagedDockerContainers(t){return x(["--filter",`label=${TESTKIT_MANAGED_LABEL}=true`,"--filter",`label=${TESTKIT_PRODUCT_ID_LABEL}=${t.id}`])}export async function inspectDockerVolume(t,e={}){const n=`Docker volume inspect for ${t}`,r=["volume","inspect",t],o=await f(r,n,DOCKER_OWNERSHIP_OPERATION_TIMEOUTS_MS.inspect,e.signal,c=>runDockerCommand(r,n,c,e.signal));if(o.exitCode!==0){if(D(o))return null;throw a(n,o)}return h(o,n,"Name")}export async function listManagedDockerVolumes(t){return L(["--filter",`label=${TESTKIT_MANAGED_LABEL}=true`,"--filter",`label=${TESTKIT_PRODUCT_ID_LABEL}=${t.id}`])}export async function removeDockerContainer(t,e=t){const n=`Docker container removal for ${e}`,r=await runDockerCommand(["rm","-f","-v",t],n,DOCKER_OWNERSHIP_OPERATION_TIMEOUTS_MS.remove);if(r.exitCode===0)return r.stdout.trim().length>0;if(g(r))return!1;throw a(n,r)}export async function removeDockerVolume(t,e,n={}){const r=normalizeDockerVolumeInspect(await inspectDockerVolume(t,n));if(r===null||!sameDockerVolumeGeneration(r,e))return!1;const o=`Docker volume removal for ${t}`,c=await runDockerCommand(["volume","rm","-f",t],o,DOCKER_OWNERSHIP_OPERATION_TIMEOUTS_MS.remove,n.signal);if(c.exitCode===0)return!0;if(D(c))return!1;throw a(o,c)}export function sameDockerVolumeGeneration(t,e){return t.name===e.name&&t.createdAt===e.createdAt&&t.driver===e.driver&&t.mountpoint===e.mountpoint&&A(t.labels,e.labels)}export async function stopDockerContainer(t,e=t){const n=`Docker container stop for ${e}`,r=await runDockerCommand(["stop","--time","5",t],n,DOCKER_OWNERSHIP_OPERATION_TIMEOUTS_MS.stop);if(r.exitCode===0)return!0;if(g(r))return!1;throw a(n,r)}async function x(t){const e="Docker container inventory",n=["ps","-a","--format","{{.ID}}",...t],r=await f(n,e,DOCKER_OWNERSHIP_OPERATION_TIMEOUTS_MS.list,void 0,s=>runDockerCommand(n,e,s,void 0));if(r.exitCode!==0)throw a(e,r);const{stdout:o}=r,c=o.split(/\r?\n/).map(s=>s.trim()).filter(Boolean);if(c.length===0)return[];const i=[];for(const s of c){const p=await inspectDockerContainer(s),d=normalizeDockerContainerInspect(p);d&&i.push(d)}return i}async function L(t){const e="Docker volume inventory",n=["volume","ls","-q",...t],r=await f(n,e,DOCKER_OWNERSHIP_OPERATION_TIMEOUTS_MS.list,void 0,s=>runDockerCommand(n,e,s,void 0));if(r.exitCode!==0)throw a(e,r);const{stdout:o}=r,c=o.split(/\r?\n/).map(s=>s.trim()).filter(Boolean);if(c.length===0)return[];const i=[];for(const s of c){const p=await inspectDockerVolume(s),d=normalizeDockerVolumeInspect(p);d&&i.push(d)}return i}export function normalizeDockerContainerInspect(t){const e=l(t);if(!e||typeof e.Id!="string"||!e.Id)return null;const n=l(e.Config),r=l(e.State),o=m(e.Name||"").replace(/^\//,"");return{id:e.Id,shortId:e.Id.slice(0,12),name:o,image:u(n?.Image)||"",createdAt:u(e.Created),running:!!r?.Running,status:u(r?.Status)||"unknown",labels:k(n?.Labels)}}export function normalizeDockerVolumeInspect(t){const e=l(t);return!e||typeof e.Name!="string"||!e.Name?null:{name:e.Name,driver:u(e.Driver)||"",mountpoint:u(e.Mountpoint)||"",labels:k(e.Labels),createdAt:u(e.CreatedAt)}}function h(t,e,n){let r;try{r=JSON.parse(t.stdout)}catch(i){throw b(e,n,t,i)}const o=Array.isArray(r)?r:null,c=o?l(o[0]):null;if(o?.length!==1||!c||typeof c[n]!="string"||c[n].length===0)throw b(e,n,t);return c}function k(t){const e={},n=l(t);if(!n)return e;for(const[r,o]of Object.entries(n))typeof o=="string"&&(e[r]=o);return e}function A(t,e){const n=Object.entries(t).sort(([o],[c])=>o.localeCompare(c)),r=Object.entries(e).sort(([o],[c])=>o.localeCompare(c));return n.length===r.length&&n.every(([o,c],i)=>r[i]?.[0]===o&&r[i]?.[1]===c)}function l(t){return E(t)?t:null}function E(t){return typeof t=="object"&&t!==null&&!Array.isArray(t)}function u(t){return typeof t=="string"&&t?t:null}function O(t,e){return E(t)&&t.code===e}function S(t){const e=w.resolve(t||process.cwd());try{return T.realpathSync.native(e)}catch(n){if(O(n,"ENOENT"))return e;throw n}}function v(t,e=24){return C.createHash("sha256").update(m(t)).digest("hex").slice(0,e)}export async function runDockerCommand(t,e,n,r){const o=await y({args:t,cwd:process.cwd(),displayLabel:e,env:process.env,executable:"docker",signal:r,timeoutMs:n}),c=I(e,o);if(c)throw c;return o}function g(t){const e=`${t.stderr}
|
|
2
|
+
${t.stdout}`.toLowerCase();return/\bno such (?:container|object)\b/u.test(e)}function D(t){const e=`${t.stderr}
|
|
3
|
+
${t.stdout}`.toLowerCase();return/\bno such (?:object|volume)\b/u.test(e)}function a(t,e){const n=[e.stderr,e.stdout].filter(Boolean).join(`
|
|
4
|
+
`).trim(),r=n?`
|
|
5
|
+
${n.slice(-_)}`:"",o=new Error(`${t} exited with code ${String(e.exitCode)}${r}`);return Object.defineProperty(o,"result",{enumerable:!1,value:Object.freeze({...e})}),o}function b(t,e,n,r){const o=new Error(`${t} returned malformed Docker inspect output; expected exactly one record with a non-empty ${e}`,r===void 0?void 0:{cause:r});return Object.defineProperty(o,"result",{enumerable:!1,value:Object.freeze({...n})}),o}
|
package/dist/database/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import d from"fs";import
|
|
1
|
+
import d from"fs";import u from"path";import{computeTemplateFingerprint as q}from"./fingerprint.js";import{buildDatabaseUrl as R,buildRuntimeDatabaseName as A,buildTemplateDatabaseName as V,hashString as B}from"./naming.js";import{getLocalInfraDir as x,getLocalLocksDir as N,getLocalServiceCacheDir as j,hasRemainingLocalArtifacts as k,requireContainedDatabaseStateDirectory as T,readStateValue as S}from"./state.js";import{createOwnedPgDumpClientImageResolver as U,runTemplateStage as W,runTemplateStep as z}from"./template-steps.js";import{materializeDatabaseConfig as H}from"../config/loader/index.js";import{applySourceSchemaCache as Q,createSourceSchemaMismatchError as G,forceRefreshSourceSchemaCache as J,prepareSourceSchemaCache as K,verifyLocalSchemaMatchesSource as X}from"./schema-source.js";import{cloneDatabaseFromTemplate as Y,createEmptyDatabase as Z,databaseExists as _,dropDatabaseIfExists as b}from"./admin.js";import{withUserHostLocalPostgresActivity as y}from"./activity-gate.js";import{ensureLocalContainer as ee,loadExistingLocalContainer as I}from"./local-postgres.js";import{writeCacheState as P,writeStateValue as h}from"./state-files.js";import{withProcessLock as L}from"../process/locks.js";import{ensureDatabaseRoles as te}from"./roles.js";import{assertTerminalRunDatabaseCleanupAuthority as ae,cleanupLocalPostgresDockerResources as re,cleanupTerminalRunLocalPostgresDockerResources as se,destroyLocalPostgresDockerResources as ne,hasActiveProductRuntime as ie}from"./cleanup.js";import"../process/runs/ownership.js";import{requireProductLifecycleFenceProductDir as g}from"../process/lifecycle-fence.js";export{dropDatabaseWithForceOrDrain,isTransientAdminQueryConnectionError,waitForDatabaseConnectionsToDrain}from"./admin.js";export{readDatabaseInfo,readDatabaseUrl}from"./state.js";export{refreshDatabaseSchema,verifyDatabaseSchema}from"./schema-operations.js";export async function provisionDatabase(e,t={}){const r=e.testkit.database;if(!r)return null;const s=H(r),a={...e,stateDir:T(e.productDir,e.stateDir,`Service "${e.name}" database runtime state directory`),testkit:{...e.testkit,database:s}},i={reporter:t.progress?.reporter||null,setupRegistry:t.progress?.setupRegistry||null,signal:t.signal,skipSchemaSourceVerify:t.skipSchemaSourceVerify};return y(a.productDir,async()=>(d.mkdirSync(a.stateDir,{recursive:!0}),ue(a,i)),{signal:t.signal})}export async function destroyRuntimeDatabase({productDir:e,stateDir:t},r={}){const s=T(e,t,"Runtime database state directory");await y(e,async()=>{await be(e,s)},r)}export async function destroyServiceDatabaseCache(e,t,r={}){await y(e,()=>ce(e,t),r)}async function ce(e,t){const r=j(e,t);if(!d.existsSync(r))return;const s=N(e);d.mkdirSync(s,{recursive:!0}),await L(u.join(s,`template-${t}.lock`),async()=>{const a=await I(e);if(!a){d.rmSync(r,{recursive:!0,force:!0});return}const i=S(u.join(r,"template_database_name"));i&&await b(a,i),d.rmSync(r,{recursive:!0,force:!0})})}export async function cleanupOrphanedLocalInfrastructure(e,t={}){const r=x(e);return v(e,r)?y(e,async()=>{if(!v(e,r)||ie(e))return D();const s=await F(e,{stopIdle:!0});return k(e)||d.rmSync(r,{recursive:!0,force:!0}),s},t):D()}export async function cleanupTerminalRunLocalInfrastructure(e,t,r={}){const s=g(e),a=x(s);return v(s,a)?y(s,async()=>{if(g(e),!v(s,a))return D();const i=await se(e,t,{stopIdle:!0});return k(s)||(ae(e,t),d.rmSync(a,{recursive:!0,force:!0})),i},r):D()}function v(e,t){return d.existsSync(t)||k(e)}function D(){return{kept:[],removed:[],skipped:[],stopped:[],targets:[]}}export async function destroyOwnedLocalDatabaseResources(e,t={}){const r=g(e);return y(r,()=>(g(e),ne(e,t)))}export async function cleanupOwnedLocalDatabaseResources(e,t={}){return y(e,()=>F(e,t))}async function F(e,t){return re({productDir:e,dryRun:!!t.dryRun,stopIdle:t.stopIdle!==!1})}export function isDatabaseStateDir(e){return d.existsSync(e)?d.existsSync(u.join(e,"database_backend")):!1}async function ue(e,t={}){const r=e.testkit.database,s=e.productDir,a=e.name,i=pe(e),c=N(s),m=j(s,a);d.mkdirSync(c,{recursive:!0}),d.mkdirSync(m,{recursive:!0});const n=await ee(s,r,{signal:t.signal}),p=await L(u.join(c,`template-${a}.lock`),async()=>{const o=await K(e,t),w=await M(e,{sourceSchemaState:o});return await te(e,n,t),oe(e,n,m,w,{...t,sourceSchemaState:o})},{signal:t.signal,timeoutMs:null}),l=await L(u.join(c,`runtime-${a}-${B(i,10)}.lock`),()=>le(e,n,m,p,i,t.signal),{signal:t.signal,timeoutMs:null});return{container:n.containerName,databaseName:l.databaseName,templateFingerprint:l.templateFingerprint,url:R(n,l.databaseName)}}async function oe(e,t,r,s,a={}){const i=e.name;let c=s,m=a.sourceSchemaState||null,n=!1;for(;;){const p=S(u.join(r,"template_fingerprint")),l=S(u.join(r,"template_database_name")),o=V(i,c);if(p===c&&l&&await _(t,l,{signal:a.signal}))return a.setupRegistry?.recordCached({config:e,stage:"template",kind:"database-template",summary:"template cache hit"}),P(r,t,l,c),c;l&&l!==o&&await b(t,l,{signal:a.signal}),await _(t,o,{signal:a.signal})&&await b(t,o,{signal:a.signal});const w=R(t,o);if(await Z(t,o,{signal:a.signal}),(await me(e,t,o,w,{...a,sourceSchemaState:m,refreshedSourceAfterMismatch:n})).refreshSourceSchema){await b(t,o,{signal:a.signal}),m=await J(e,m,a),n=!0,c=await M(e,{sourceSchemaState:m});continue}return P(r,t,o,c),c}}async function me(e,t,r,s,a={}){const i=a.setupRegistry?.start({config:e,stage:"template",kind:"database-template",summary:"template rebuild",recordLog:!1}),c={ownedContainer:{containerId:t.containerId,containerPort:t.containerPort},parentOperation:i,reporter:a.reporter||null,setupRegistry:a.setupRegistry||null,signal:a.signal},m=U(e,s,c);try{await Q(e,s,a.sourceSchemaState,c);const n=async()=>{const f=await X(e,s,a.sourceSchemaState,{...c,resolveLocalSnapshotImage:m,skipSchemaSourceVerify:a.skipSchemaSourceVerify});if(f.status!=="mismatch")return null;if(a.refreshedSourceAfterMismatch)throw G(e,he(a.sourceSchemaState),f);return f},p=e.testkit.database?.template,l=!!(p?.migrate?.length||p?.seed?.length),o=async f=>{const O=p?.[f]||[];for(const[$,E]of O.entries())if(await z(e,f,E,$,s,c),await n())return!0;return!1};return await o("migrate")||await o("seed")||!l&&!!await n()?(C(i,a,{status:"passed",summary:"source schema refresh requested"}),{refreshSourceSchema:!0}):(await W(e,"verify",s,c),C(i,a,{status:"passed",summary:"template rebuild"}),{refreshSourceSchema:!1})}catch(n){throw C(i,a,{status:"failed",summary:"template rebuild",error:fe(n)||n}),await b(t,r),n}}function C(e,t,r){const s=e&&t.setupRegistry?t.setupRegistry.finish(e,r):null;s&&t.reporter?.setupOperationFinished?.(s)}async function le(e,t,r,s,a,i){const c=e.name,m=S(u.join(r,"template_database_name"));if(!m)throw new Error(`Missing template database for service "${c}"`);const n=A(c,a,s),p=S(u.join(e.stateDir,"local_database_name")),l=S(u.join(e.stateDir,"local_template_fingerprint")),o=Se(e.testkit.database.reset)||l!==s||p!==n||!await _(t,n,{signal:i});return p&&p!==n&&await b(t,p,{signal:i}),o&&(await b(t,n,{signal:i}),await Y(t,n,m,{signal:i})),d.mkdirSync(e.stateDir,{recursive:!0}),de(e,t,{bindingKey:a,databaseName:n,templateFingerprint:s,templateDbName:m}),{bindingKey:a,databaseName:n,templateFingerprint:s,templateDbName:m}}function de(e,t,r){h(u.join(e.stateDir,"database_backend"),""),h(u.join(e.stateDir,"database_url"),R(t,r.databaseName)),h(u.join(e.stateDir,"local_database_name"),r.databaseName),h(u.join(e.stateDir,"local_template_fingerprint"),r.templateFingerprint),h(u.join(e.stateDir,"local_template_database_name"),r.templateDbName),h(u.join(e.stateDir,"local_container_name"),t.containerName),t.volumeName&&h(u.join(e.stateDir,"volume_name"),t.volumeName),t.volumeMountPath&&h(u.join(e.stateDir,"volume_mount_path"),t.volumeMountPath)}function pe(e){return ye(e.testkit.database.binding)==="shared"?`${e.name}:shared`:e.runtimeId?`${e.name}:${String(e.runtimeId)}`:`${e.name}:${e.stateDir}`}async function be(e,t){const r=S(u.join(t,"local_database_name"));if(!r)return;const s=await I(e);s&&await b(s,r)}function M(e,t={}){return q(e,t)}function he(e){if(!e?.active)throw new Error("Source schema mismatch requires active source schema state");return e}function fe(e){return typeof e!="object"&&typeof e!="function"||e===null?void 0:Reflect.get(e,"message")}function Se(e){return e!==!1}function ye(e){return e||"per-runtime"}function Oe(e){return e}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{randomUUID as C}from"node:crypto";import ne from"fs";import u from"path";import{LOCAL_ADMIN_DB as D,LOCAL_IMAGE as M,LOCAL_MAX_CONNECTIONS as N,LOCAL_PASSWORD as R,LOCAL_POLL_INTERVAL_MS as te,LOCAL_READY_TIMEOUT_MS as $,LOCAL_USER as j}from"./constants.js";import{buildContainerName as re,buildVolumeName as S}from"./naming.js";import{getLocalInfraDir as V,readStateValue as l}from"./state.js";import{TESTKIT_CREATED_AT_LABEL as U,TESTKIT_RESOURCE_GENERATION_LABEL as z,buildDockerResourceLabels as G,dockerLabelArgs as v,inspectDockerVolume as L,normalizeDockerVolumeInspect as E,removeDockerVolume as oe,runDockerCommand as h}from"./docker.js";import{writeLocalInfraState as ie}from"./state-files.js";const se="/var/lib/postgresql/data",ae="/var/lib/postgresql",A=String;export const LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS=Object.freeze({inspect:3e4,readinessAttempt:1e4,remove:12e4,run:10*6e4,start:12e4,volumeCreate:12e4});export async function ensureLocalContainer(e,n={},t={}){const o=V(e);ne.mkdirSync(o,{recursive:!0});const r=l(u.join(o,"container_name"))||re(e),i=l(u.join(o,"volume_name")),s=n.image||l(u.join(o,"image"))||M,f=l(u.join(o,"image")),c=resolveLocalPostgresVolumeTarget(s),w=n.user||l(u.join(o,"user"))||j,b=n.password||l(u.join(o,"password"))||R,_=q(n.maxConnections||l(u.join(o,"max_connections"))||N);let a=await inspectContainer(r,t);a&&p(a,e,r);let d=K(a,c)||i||S(e,C());a&&await F(e,d,t);const O=a?.Config?.Image||f||null;if(a&&!isReusableLocalPostgresContainer(a,{image:s,maxConnections:_,volumeMountPath:c,volumeName:d})&&(await stopAndRemoveContainer(a.Id,r,t),O&&O!==s&&(await me(e,d,t),d=S(e,C())),a=null),a)a.State?.Running||(await k(["start",a.Id],`Docker local Postgres container start for ${r}`,LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.start,t.signal),a=await inspectContainer(r,t),a&&p(a,e,r));else{d=await le(e,d,t);const ee=Y(e,r,C());await k(buildDockerRunArgs({containerName:r,labels:ee,volumeName:d,volumeMountPath:c,user:w,password:b,image:s,maxConnections:_}),`Docker local Postgres container creation for ${r}`,LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.run,t.signal),a=await inspectContainer(r,t),a&&p(a,e,r)}const I=B(a);if(!a||!I)throw new Error(`Could not determine host port for local database container ${r}`);const x={containerName:r,containerId:a.Id,volumeName:d,volumeMountPath:c,image:s,user:w,password:b,maxConnections:_,host:"127.0.0.1",port:Number(I),containerPort:5432};return await W(x,t),ie(o,x),x}export function buildDockerRunArgs({containerName:e,labels:n,volumeName:t,volumeMountPath:o,user:r,password:i,image:s,maxConnections:f}){return["run","-d","--name",e,...v(n),"-p","127.0.0.1::5432","--mount",`type=volume,source=${t},target=${o}`,"-e",`POSTGRES_USER=${r}`,"-e",`POSTGRES_PASSWORD=${i}`,"-e",`POSTGRES_DB=${D}`,s,"postgres","-c",`max_connections=${String(f)}`]}export async function loadExistingLocalContainer(e,n={}){const t=V(e),o=l(u.join(t,"container_name"));if(!o)return null;const r=await inspectContainer(o,n);if(!r)return null;p(r,e,o);const i=r.Config?.Image||l(u.join(t,"image"))||M,s=l(u.join(t,"volume_mount_path"))||resolveLocalPostgresVolumeTarget(i),f=K(r,s)||l(u.join(t,"volume_name"));if(!f)return null;await F(e,f,n),r.State?.Running||await k(["start",r.Id],`Docker local Postgres container start for ${o}`,LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.start,n.signal);const c=await inspectContainer(o,n);c&&p(c,e,o);const w=B(c);if(!c||!w)return null;const b={containerName:o,containerId:c.Id,volumeName:f,volumeMountPath:s,image:i,user:l(u.join(t,"user"))||j,password:l(u.join(t,"password"))||R,maxConnections:q(l(u.join(t,"max_connections"))||N),host:"127.0.0.1",port:Number(w),containerPort:H(c)==="host"?Number(w):5432};return await W(b,n),b}function ce(e,n){const t=e.Config?.Cmd;return Array.isArray(t)?t.includes("-c")&&t.includes(`max_connections=${String(n)}`):!1}function ue(e,n,t){return(Array.isArray(e.Mounts)?e.Mounts:[]).some(r=>r?.Type==="volume"&&r.Name===n&&r.Destination===t)}export function isReusableLocalPostgresContainer(e,n){return e.Config?.Image===n.image&&ce(e,n.maxConnections)&&ue(e,n.volumeName,n.volumeMountPath)&&H(e)==="bridge"}function H(e){return e.HostConfig?.NetworkMode==="host"?"host":"bridge"}function B(e){return e?.NetworkSettings?.Ports?.["5432/tcp"]?.[0]?.HostPort||null}function q(e){const n=Number(e);if(!Number.isInteger(n)||n<10)throw new Error("database.maxConnections must be an integer greater than or equal to 10");return n}export async function inspectContainer(e,n={}){const t=`Docker local Postgres container inspect for ${e}`,o=await h(["inspect",e],t,LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.inspect,n.signal);if(o.exitCode!==0){if(Z(o))return null;throw T(t,o)}let r;try{r=JSON.parse(o.stdout)}catch(s){throw Q(t,o,s)}const i=P(r)&&r.length===1?normalizeLocalPostgresContainerInspect(r[0]):null;if(!i?.Id)throw Q(t,o);return{...i,Id:i.Id}}export function normalizeLocalPostgresContainerInspect(e){return m(e)?{Config:we(e.Config),HostConfig:pe(e.HostConfig),Id:g(e.Id),Mounts:Ce(e.Mounts),NetworkSettings:Le(e.NetworkSettings),State:he(e.State)}:null}export async function stopAndRemoveContainer(e,n=e,t={}){const o=`Docker local Postgres container removal for ${n}`,r=await h(["rm","-f","-v",e],o,LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.remove,t.signal);if(!(r.exitCode===0||Z(r)))throw T(o,r)}async function le(e,n,t={}){const o=y(e,n,"existing-generation"),r=await L(n,t);if(r){if(!matchesStableDockerLabels(X(r)||{},o))throw new Error(`Docker volume ${n} already exists but is not owned by this Testkit product. Remove or rename it before running Testkit.`);return n}const i=C(),s=S(e,i),f=y(e,s,i);await k(["volume","create",...v(f),s],`Docker local Postgres volume creation for ${s}`,LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.volumeCreate,t.signal);const c=E(await L(s,t));if(c===null||!fe(c.labels,f))throw new Error(`Docker volume ${s} was not published with the expected Testkit generation identity`);return s}async function F(e,n,t){const o=y(e,n,"existing-generation"),r=E(await L(n,t));if(r===null||!matchesStableDockerLabels(r.labels,o))throw new Error(`Docker volume ${n} is missing or is not owned by this Testkit product`);return r}export function matchesStableDockerLabels(e,n){const t=e[U],o=e[z];return typeof t=="string"&&t.length>0&&typeof o=="string"&&o.length>0&&Object.entries(n).every(([r,i])=>r===U||r===z||e[r]===A(i))}function fe(e,n){const t=Object.entries(e).sort(([r],[i])=>r.localeCompare(i)),o=Object.entries(n).map(([r,i])=>[r,A(i)]).sort(([r],[i])=>r.localeCompare(i));return t.length===o.length&&t.every(([r,i],s)=>o[s]?.[0]===r&&o[s]?.[1]===i)}async function me(e,n,t={}){const o=y(e,n,"existing-generation"),r=await L(n,t);if(!r)return;const i=X(r)||{};if(!matchesStableDockerLabels(i,o))return;const f=E(r);f!==null&&await oe(n,f,t)}function K(e,n){return(Array.isArray(e?.Mounts)?e.Mounts:[]).find(r=>r?.Type==="volume"&&typeof r.Name=="string"&&r.Name.length>0&&r.Destination===n)?.Name||null}function p(e,n,t){const o=Y(n,t);if(!matchesStableDockerLabels(e.Config?.Labels||{},o))throw new Error(`Docker container ${t} already exists but is not owned by this Testkit product. Remove or rename it before running Testkit.`)}export function resolveLocalPostgresVolumeTarget(e){return de(e)>=18?ae:se}function de(e){const t=A(e||"").match(/(?:^|[:@/-])(?:pg|postgres)?(\d{2})(?:\D|$)/i);return t?Number(t[1]):16}async function W(e,n={}){const t=Date.now();for(;Date.now()-t<$;){const o=Math.max(1,$-(Date.now()-t));if((await h(["exec",e.containerId,"pg_isready","-U",e.user,"-d",D,"-p",String(e.containerPort||5432)],`Docker local Postgres readiness probe for ${e.containerName}`,Math.min(LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.readinessAttempt,o),n.signal)).exitCode===0)return;await ge(Math.min(te,o),n.signal)}throw new Error(`Timed out waiting for local database container ${e.containerName}`)}function Y(e,n,t="existing-generation"){return G(e,{generation:t,kind:"postgres-container",name:n,scope:"local-postgres",cachePolicy:"product-cache"})}function y(e,n,t){return G(e,{generation:t,kind:"postgres-volume",name:n,scope:"local-postgres",cachePolicy:"product-cache"})}function ge(e,n){return new Promise((t,o)=>{if(n?.aborted){o(J(n));return}const r=setTimeout(i,e);n?.addEventListener("abort",s,{once:!0});function i(){n?.removeEventListener("abort",s),t()}function s(){clearTimeout(r),o(n?J(n):new Error("Local Postgres operation aborted"))}})}function J(e){return e.reason instanceof Error?e.reason:new Error("Local Postgres operation aborted",{cause:e.reason})}function X(e){if(!m(e))return null;const n=e.Labels;return m(n)?n:null}function we(e){if(e===null)return null;if(m(e))return{Cmd:ye(e.Cmd),Image:g(e.Image),Labels:be(e.Labels)}}function be(e){if(!m(e))return null;const n={};for(const[t,o]of Object.entries(e))typeof o=="string"&&(n[t]=o);return n}function pe(e){if(e===null)return null;if(m(e))return{NetworkMode:g(e.NetworkMode)}}function Ce(e){if(e===null)return null;if(P(e))return e.flatMap(n=>m(n)?[{Destination:g(n.Destination),Name:g(n.Name),Type:g(n.Type)}]:[])}function Le(e){if(e===null)return null;if(!m(e))return;const n=e.Ports;if(n===null)return{Ports:null};if(!m(n))return{};const t={};for(const[o,r]of Object.entries(n)){if(r===null){t[o]=null;continue}P(r)&&(t[o]=r.flatMap(i=>{if(!m(i))return[];const s=g(i.HostPort);return i.HostPort!==void 0&&s===void 0?[]:[{HostPort:s}]}))}return{Ports:t}}function he(e){if(e===null)return null;if(m(e))return{Running:e.Running===null||typeof e.Running=="boolean"?e.Running:void 0}}function g(e){if(e===null||typeof e=="string")return e}function m(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function P(e){return Array.isArray(e)}function ye(e){if(e===null)return null;if(P(e))return e.filter(n=>typeof n=="string")}async function k(e,n,t,o){const r=await h(e,n,t,o);if(r.exitCode===0)return r;throw T(n,r)}function T(e,n){const t=n.stderr.trim()||n.stdout.trim(),o=new Error(`${e} failed with exit code ${String(n.exitCode)}${t?`: ${t}`:""}`);return Object.defineProperty(o,"result",{enumerable:!1,value:Object.freeze({...n})}),o}function Q(e,n,t){const o=new Error(`${e} returned malformed Docker inspect output; expected exactly one container record with a non-empty Id`,t===void 0?void 0:{cause:t});return Object.defineProperty(o,"result",{enumerable:!1,value:Object.freeze({...n})}),o}function Z(e){return/\bno such (?:container|object)\b/iu.test(`${e.stderr}
|
|
1
|
+
import{runDockerMetadataCommand as re}from"./docker-metadata.js";import{randomUUID as C}from"node:crypto";import oe from"fs";import u from"path";import{LOCAL_ADMIN_DB as D,LOCAL_IMAGE as M,LOCAL_MAX_CONNECTIONS as N,LOCAL_PASSWORD as R,LOCAL_POLL_INTERVAL_MS as ie,LOCAL_READY_TIMEOUT_MS as $,LOCAL_USER as j}from"./constants.js";import{buildContainerName as se,buildVolumeName as S}from"./naming.js";import{getLocalInfraDir as V,readStateValue as l}from"./state.js";import{TESTKIT_CREATED_AT_LABEL as U,TESTKIT_RESOURCE_GENERATION_LABEL as z,buildDockerResourceLabels as G,dockerLabelArgs as v,inspectDockerVolume as L,normalizeDockerVolumeInspect as E,removeDockerVolume as ae,runDockerCommand as h}from"./docker.js";import{writeLocalInfraState as ce}from"./state-files.js";const ue="/var/lib/postgresql/data",le="/var/lib/postgresql",A=String;export const LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS=Object.freeze({inspect:3e4,readinessAttempt:1e4,remove:12e4,run:10*6e4,start:12e4,volumeCreate:12e4});export async function ensureLocalContainer(e,n={},t={}){const o=V(e);oe.mkdirSync(o,{recursive:!0});const r=l(u.join(o,"container_name"))||se(e),i=l(u.join(o,"volume_name")),s=n.image||l(u.join(o,"image"))||M,c=l(u.join(o,"image")),f=resolveLocalPostgresVolumeTarget(s),w=n.user||l(u.join(o,"user"))||j,b=n.password||l(u.join(o,"password"))||R,_=q(n.maxConnections||l(u.join(o,"max_connections"))||N);let a=await inspectContainer(r,t);a&&p(a,e,r);const ne=!a&&!i;let d=W(a,f)||i||S(e,C());a&&await K(e,d,t);const O=a?.Config?.Image||c||null;if(a&&!isReusableLocalPostgresContainer(a,{image:s,maxConnections:_,volumeMountPath:f,volumeName:d})&&(await stopAndRemoveContainer(a.Id,r,t),O&&O!==s&&(await we(e,d,t),d=S(e,C())),a=null),a)a.State?.Running||(await k(["start",a.Id],`Docker local Postgres container start for ${r}`,LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.start,t.signal),a=await inspectContainer(r,t),a&&p(a,e,r));else{d=ne?await F(e,t):await de(e,d,t);const te=J(e,r,C());await k(buildDockerRunArgs({containerName:r,labels:te,volumeName:d,volumeMountPath:f,user:w,password:b,image:s,maxConnections:_}),`Docker local Postgres container creation for ${r}`,LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.run,t.signal),a=await inspectContainer(r,t),a&&p(a,e,r)}const I=B(a);if(!a||!I)throw new Error(`Could not determine host port for local database container ${r}`);const x={containerName:r,containerId:a.Id,volumeName:d,volumeMountPath:f,image:s,user:w,password:b,maxConnections:_,host:"127.0.0.1",port:Number(I),containerPort:5432};return await Y(x,t),ce(o,x),x}export function buildDockerRunArgs({containerName:e,labels:n,volumeName:t,volumeMountPath:o,user:r,password:i,image:s,maxConnections:c}){return["run","-d","--name",e,...v(n),"-p","127.0.0.1::5432","--mount",`type=volume,source=${t},target=${o}`,"-e",`POSTGRES_USER=${r}`,"-e",`POSTGRES_PASSWORD=${i}`,"-e",`POSTGRES_DB=${D}`,s,"postgres","-c",`max_connections=${String(c)}`]}export async function loadExistingLocalContainer(e,n={}){const t=V(e),o=l(u.join(t,"container_name"));if(!o)return null;const r=await inspectContainer(o,n);if(!r)return null;p(r,e,o);const i=r.Config?.Image||l(u.join(t,"image"))||M,s=l(u.join(t,"volume_mount_path"))||resolveLocalPostgresVolumeTarget(i),c=W(r,s)||l(u.join(t,"volume_name"));if(!c)return null;await K(e,c,n),r.State?.Running||await k(["start",r.Id],`Docker local Postgres container start for ${o}`,LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.start,n.signal);const f=await inspectContainer(o,n);f&&p(f,e,o);const w=B(f);if(!f||!w)return null;const b={containerName:o,containerId:f.Id,volumeName:c,volumeMountPath:s,image:i,user:l(u.join(t,"user"))||j,password:l(u.join(t,"password"))||R,maxConnections:q(l(u.join(t,"max_connections"))||N),host:"127.0.0.1",port:Number(w),containerPort:H(f)==="host"?Number(w):5432};return await Y(b,n),b}function fe(e,n){const t=e.Config?.Cmd;return Array.isArray(t)?t.includes("-c")&&t.includes(`max_connections=${String(n)}`):!1}function me(e,n,t){return(Array.isArray(e.Mounts)?e.Mounts:[]).some(r=>r?.Type==="volume"&&r.Name===n&&r.Destination===t)}export function isReusableLocalPostgresContainer(e,n){return e.Config?.Image===n.image&&fe(e,n.maxConnections)&&me(e,n.volumeName,n.volumeMountPath)&&H(e)==="bridge"}function H(e){return e.HostConfig?.NetworkMode==="host"?"host":"bridge"}function B(e){return e?.NetworkSettings?.Ports?.["5432/tcp"]?.[0]?.HostPort||null}function q(e){const n=Number(e);if(!Number.isInteger(n)||n<10)throw new Error("database.maxConnections must be an integer greater than or equal to 10");return n}export async function inspectContainer(e,n={}){const t=`Docker local Postgres container inspect for ${e}`,o=["inspect",e],r=await re(o,t,LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.inspect,n.signal,c=>h(o,t,c,n.signal));if(r.exitCode!==0){if(ee(r))return null;throw T(t,r)}let i;try{i=JSON.parse(r.stdout)}catch(c){throw Z(t,r,c)}const s=P(i)&&i.length===1?normalizeLocalPostgresContainerInspect(i[0]):null;if(!s?.Id)throw Z(t,r);return{...s,Id:s.Id}}export function normalizeLocalPostgresContainerInspect(e){return m(e)?{Config:Ce(e.Config),HostConfig:he(e.HostConfig),Id:g(e.Id),Mounts:ye(e.Mounts),NetworkSettings:Pe(e.NetworkSettings),State:ke(e.State)}:null}export async function stopAndRemoveContainer(e,n=e,t={}){const o=`Docker local Postgres container removal for ${n}`,r=await h(["rm","-f","-v",e],o,LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.remove,t.signal);if(!(r.exitCode===0||ee(r)))throw T(o,r)}async function de(e,n,t={}){const o=y(e,n,"existing-generation"),r=await L(n,t);if(r){if(!matchesStableDockerLabels(Q(r)||{},o))throw new Error(`Docker volume ${n} already exists but is not owned by this Testkit product. Remove or rename it before running Testkit.`);return n}return F(e,t)}async function F(e,n){const t=C(),o=S(e,t),r=y(e,o,t);await k(["volume","create",...v(r),o],`Docker local Postgres volume creation for ${o}`,LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.volumeCreate,n.signal);const i=E(await L(o,n));if(i===null||!ge(i.labels,r))throw new Error(`Docker volume ${o} was not published with the expected Testkit generation identity`);return o}async function K(e,n,t){const o=y(e,n,"existing-generation"),r=E(await L(n,t));if(r===null||!matchesStableDockerLabels(r.labels,o))throw new Error(`Docker volume ${n} is missing or is not owned by this Testkit product`);return r}export function matchesStableDockerLabels(e,n){const t=e[U],o=e[z];return typeof t=="string"&&t.length>0&&typeof o=="string"&&o.length>0&&Object.entries(n).every(([r,i])=>r===U||r===z||e[r]===A(i))}function ge(e,n){const t=Object.entries(e).sort(([r],[i])=>r.localeCompare(i)),o=Object.entries(n).map(([r,i])=>[r,A(i)]).sort(([r],[i])=>r.localeCompare(i));return t.length===o.length&&t.every(([r,i],s)=>o[s]?.[0]===r&&o[s]?.[1]===i)}async function we(e,n,t={}){const o=y(e,n,"existing-generation"),r=await L(n,t);if(!r)return;const i=Q(r)||{};if(!matchesStableDockerLabels(i,o))return;const c=E(r);c!==null&&await ae(n,c,t)}function W(e,n){return(Array.isArray(e?.Mounts)?e.Mounts:[]).find(r=>r?.Type==="volume"&&typeof r.Name=="string"&&r.Name.length>0&&r.Destination===n)?.Name||null}function p(e,n,t){const o=J(n,t);if(!matchesStableDockerLabels(e.Config?.Labels||{},o))throw new Error(`Docker container ${t} already exists but is not owned by this Testkit product. Remove or rename it before running Testkit.`)}export function resolveLocalPostgresVolumeTarget(e){return be(e)>=18?le:ue}function be(e){const t=A(e||"").match(/(?:^|[:@/-])(?:pg|postgres)?(\d{2})(?:\D|$)/i);return t?Number(t[1]):16}async function Y(e,n={}){const t=Date.now();for(;Date.now()-t<$;){const o=Math.max(1,$-(Date.now()-t));if((await h(["exec",e.containerId,"pg_isready","-U",e.user,"-d",D,"-p",String(e.containerPort||5432)],`Docker local Postgres readiness probe for ${e.containerName}`,Math.min(LOCAL_POSTGRES_DOCKER_TIMEOUTS_MS.readinessAttempt,o),n.signal)).exitCode===0)return;await pe(Math.min(ie,o),n.signal)}throw new Error(`Timed out waiting for local database container ${e.containerName}`)}function J(e,n,t="existing-generation"){return G(e,{generation:t,kind:"postgres-container",name:n,scope:"local-postgres",cachePolicy:"product-cache"})}function y(e,n,t){return G(e,{generation:t,kind:"postgres-volume",name:n,scope:"local-postgres",cachePolicy:"product-cache"})}function pe(e,n){return new Promise((t,o)=>{if(n?.aborted){o(X(n));return}const r=setTimeout(i,e);n?.addEventListener("abort",s,{once:!0});function i(){n?.removeEventListener("abort",s),t()}function s(){clearTimeout(r),o(n?X(n):new Error("Local Postgres operation aborted"))}})}function X(e){return e.reason instanceof Error?e.reason:new Error("Local Postgres operation aborted",{cause:e.reason})}function Q(e){if(!m(e))return null;const n=e.Labels;return m(n)?n:null}function Ce(e){if(e===null)return null;if(m(e))return{Cmd:_e(e.Cmd),Image:g(e.Image),Labels:Le(e.Labels)}}function Le(e){if(!m(e))return null;const n={};for(const[t,o]of Object.entries(e))typeof o=="string"&&(n[t]=o);return n}function he(e){if(e===null)return null;if(m(e))return{NetworkMode:g(e.NetworkMode)}}function ye(e){if(e===null)return null;if(P(e))return e.flatMap(n=>m(n)?[{Destination:g(n.Destination),Name:g(n.Name),Type:g(n.Type)}]:[])}function Pe(e){if(e===null)return null;if(!m(e))return;const n=e.Ports;if(n===null)return{Ports:null};if(!m(n))return{};const t={};for(const[o,r]of Object.entries(n)){if(r===null){t[o]=null;continue}P(r)&&(t[o]=r.flatMap(i=>{if(!m(i))return[];const s=g(i.HostPort);return i.HostPort!==void 0&&s===void 0?[]:[{HostPort:s}]}))}return{Ports:t}}function ke(e){if(e===null)return null;if(m(e))return{Running:e.Running===null||typeof e.Running=="boolean"?e.Running:void 0}}function g(e){if(e===null||typeof e=="string")return e}function m(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function P(e){return Array.isArray(e)}function _e(e){if(e===null)return null;if(P(e))return e.filter(n=>typeof n=="string")}async function k(e,n,t,o){const r=await h(e,n,t,o);if(r.exitCode===0)return r;throw T(n,r)}function T(e,n){const t=n.stderr.trim()||n.stdout.trim(),o=new Error(`${e} failed with exit code ${String(n.exitCode)}${t?`: ${t}`:""}`);return Object.defineProperty(o,"result",{enumerable:!1,value:Object.freeze({...n})}),o}function Z(e,n,t){const o=new Error(`${e} returned malformed Docker inspect output; expected exactly one container record with a non-empty Id`,t===void 0?void 0:{cause:t});return Object.defineProperty(o,"result",{enumerable:!1,value:Object.freeze({...n})}),o}function ee(e){return/\bno such (?:container|object)\b/iu.test(`${e.stderr}
|
|
2
2
|
${e.stdout}`)}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import P from"node:path";import{randomUUID as
|
|
2
|
-
`),{cause:
|
|
1
|
+
import P from"node:path";import{randomUUID as N}from"node:crypto";import{setTimeout as x}from"node:timers/promises";import{errorMessage as T}from"../../kernel/error-serialization.js";import{dockerHostAddress as M,shouldUseDockerHostNetwork as I}from"../../process/host/platform.js";import{managedHostProcessInfrastructureError as S,runManagedHostProcess as h,startManagedHostProcess as y}from"../../process/index.js";export const POSTGRES_CLIENT_CONTAINER_LABEL="com.elench.testkit.postgres-client.container";const O="postgres:16",w="postgres";export const POSTGRES_CLIENT_TIMEOUT_MS=30*6e4;const _=["-h","--host","-p","--port","-U","--username","-d","--dbname"],v=/^(?:--host|--port|--username|--dbname)=/,g=String;export function postgresClientImage(e=process.env){return e.TESTKIT_POSTGRES_CLIENT_IMAGE||O}export function runDockerPsql(e){return runDockerPostgresClient({...e,clientCommand:"psql"})}export function startDockerPgDump(e){return startDockerPostgresClient({...e,clientCommand:"pg_dump"})}export async function runDockerPostgresClient(e={}){try{return await startDockerPostgresClient(e)}catch(r){throw b(r)}}export function startDockerPostgresClient({clientCommand:e,args:r=[],cwd:t,env:n=process.env,connection:c=null,databaseUrl:u=null,image:s=null,ownedContainer:i=null,signal:o,timeoutMs:m=POSTGRES_CLIENT_TIMEOUT_MS}={}){const d=q({args:r,connection:c,databaseUrl:u});if(!H(d))throw new Error(`Dockerized ${e||"Postgres client"} requires a Postgres connection URL or connection object`);if(!e)throw new Error("buildDockerPostgresClientArgs requires clientCommand");const E=buildDockerPostgresClientArgs({clientCommand:e,args:r,connection:d,cwd:t,env:n,image:s||postgresClientImage(n),ownedContainer:i}),a=`testkit-postgres-client-${N()}`;return E.splice(1,0,"--name",a),R("docker",E,{containerName:a,cwd:t,env:k(n,d),signal:o,timeoutMs:L(m)})}function b(e){return G(e)&&e.code==="ENOENT"||e instanceof Error&&e.message.includes('managed host executable "docker"')?new Error("Docker is required for Testkit Postgres client operations, but the docker command was not found."):e}function G(e){return e instanceof Error&&"code"in e}function R(e,r,t){let n;try{n=y({recoveryIdentity:"none",args:r,cwd:t.cwd||process.cwd(),displayLabel:"Dockerized Postgres client",env:t.env,executable:e,outputMode:"bounded-capture",signal:t.signal,timeoutMs:t.timeoutMs})}catch(o){throw b(o)}let c=!1,u=!1;const s=n.completion.then(async o=>{const m=u&&U(o)?null:S("Dockerized Postgres client",o);if(o.cancelled||o.timedOut||o.outputLimitExceeded||u){const d={executable:"docker",cwd:t.cwd||process.cwd(),env:t.env,displayLabel:"Postgres client container cleanup"},E=async()=>{const a=performance.now()+3e4;let l;for(;;){const f=Math.floor(a-performance.now());if(f<=0)throw l??new Error("Postgres client container cleanup timed out");const p=await h({...d,args:["rm","--force",t.containerName],timeoutMs:f}),A=S("Postgres client container cleanup",p);if(A)throw A;if(p.exitCode===0)return;const C=p.stderr.trim();if(C===`Error response from daemon: No such container: ${t.containerName}`)return;if(l=new Error(`Postgres client container cleanup failed: ${p.stderr}`),C!==`Error response from daemon: removal of container ${t.containerName} is already in progress`)throw l;await x(Math.min(100,Math.max(0,a-performance.now())))}};try{try{(await h({...d,args:["kill","--signal=SIGINT",t.containerName],timeoutMs:5e3})).exitCode===0&&await h({...d,args:["wait",t.containerName],timeoutMs:2e3})}finally{await E()}}catch(a){const l=m||(o.exitCode!==0?new Error(`Dockerized Postgres client failed with exit code ${String(o.exitCode)}`):null);if(!l)throw a;const f=new AggregateError([l,a],[l.message,o.stderr.trim(),o.stdout.trim(),T(a)].filter(Boolean).join(`
|
|
2
|
+
`),{cause:l});throw Object.defineProperty(f,"result",{enumerable:!1,value:Object.freeze({...o})}),f}}if(m)throw b(m);return{command:e,escapedCommand:[e,...r].join(" "),exitCode:o.exitCode,failed:o.exitCode!==0,signal:o.exitSignal,stderr:o.stderr,stdout:o.stdout}});s.then(()=>{c=!0},()=>{c=!0});const i=s;return Object.defineProperties(i,{pid:{enumerable:!0,get:()=>n.pid},stdout:{enumerable:!0,value:n.stdout},stderr:{enumerable:!0,value:n.stderr},stop:{enumerable:!0,value:()=>c?!1:(u=!0,n.stop(),!0)}}),i}function U(e){return e.error===null&&e.terminationError===null&&!e.timedOut&&!e.cancelled&&!e.outputLimitExceeded}function L(e){if(!Number.isSafeInteger(e)||Number(e)<=0)throw new TypeError("Dockerized Postgres client timeoutMs must be a positive safe integer");return Number(e)}export function buildDockerPostgresClientArgs({clientCommand:e,args:r=[],connection:t,cwd:n,env:c=process.env,image:u=postgresClientImage(c),ownedContainer:s=null}={}){if(!e)throw new Error("buildDockerPostgresClientArgs requires clientCommand");if(!t)throw new TypeError("buildDockerPostgresClientArgs requires connection");const i=F(r,t),{args:o,mountArgs:m}=rewritePostgresFileArgsForDocker(i,n),[d,E,a]=$(t,s),l=k(c,t),f=["PGPASSWORD","PGSSLMODE","PGAPPNAME"].filter(p=>l[p]!=null).flatMap(p=>["-e",p]);return["run","--rm",...d,...s?["--label",`${POSTGRES_CLIENT_CONTAINER_LABEL}=${s.containerId}`]:[],...f,...m,u,e,"-h",E,"-p",a,"-U",t.user,"-d",t.database||w,...o]}function $(e,r){if(!r)return[I()?["--network","host"]:[],g(M(e.host)),g(e.port||5432)];if(!r.containerId)throw new TypeError("ownedContainer.containerId must be a container id");return[["--network",`container:${r.containerId}`],"127.0.0.1",g(r.containerPort||5432)]}export function rewritePostgresFileArgsForDocker(e,r){const t=[],n=[];let c=0;const u=s=>{const i=P.resolve(r||process.cwd(),s),o=`/testkit-postgres-files-${String(c)}`;return c+=1,n.push("--mount",`type=bind,source=${P.dirname(i)},target=${o},readonly`),`${o}/${P.basename(i)}`};for(let s=0;s<e.length;s+=1){const i=e[s],o=e[s+1];if((i==="-f"||i==="--file")&&o){t.push(i,u(o)),s+=1;continue}if(i.startsWith("--file=")){t.push(`--file=${u(i.slice(7))}`);continue}t.push(i)}return{args:t,mountArgs:n}}function q({args:e,connection:r,databaseUrl:t}){return r?{...r,port:r.port||5432,database:r.database||w}:connectionFromDatabaseUrl(t||z(e))}function z(e){const r=e[0];return D(r)?r:null}function F(e,r){const t=r?.rawUrl,n=t&&e[0]===t||D(e[0])?e.slice(1):e;return j(n).map(c=>g(c))}function j(e){const r=[];for(let t=0;t<e.length;t+=1){const n=e[t];if(typeof n=="string"&&_.includes(n)){t+=1;continue}v.test(g(n))||r.push(n)}return r}function k(e,r){return{...e,PGPASSWORD:r.password||"",...r.sslMode?{PGSSLMODE:r.sslMode}:{},...e.PGAPPNAME?{PGAPPNAME:e.PGAPPNAME}:{}}}function D(e){return/^postgres(?:ql)?:\/\//i.test(g(e||""))}function H(e){return!!(e?.host&&e.user)}export function connectionFromDatabaseUrl(e){if(!e)return null;let r;try{r=new URL(g(e))}catch{return null}return["postgres:","postgresql:"].includes(r.protocol)?{rawUrl:e,host:r.hostname,port:r.port||"5432",database:decodeURIComponent(r.pathname.replace(/^\//,""))||w,user:decodeURIComponent(r.username||""),password:decodeURIComponent(r.password||""),sslMode:r.searchParams.get("sslmode")||void 0}:null}
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { type NormalizedDatabaseConfig, type NormalizedDatabaseTemplateConfig, type NormalizedSourceSchemaConfig } from "../config/loader/index.js";
|
|
2
2
|
import { type TemplateResolutionContext } from "../steps/index.js";
|
|
3
3
|
export declare function finalizeRuntimeDatabaseConfig(database: NormalizedDatabaseConfig | null | undefined, context: TemplateResolutionContext): import("../config/loader/database-materialization.js").MaterializedDatabaseConfig<{
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
password?: string | undefined;
|
|
8
|
-
user?: string | undefined;
|
|
4
|
+
image?: string;
|
|
5
|
+
password?: string;
|
|
6
|
+
user?: string;
|
|
9
7
|
binding: import("../config/loader/execution-model.js").DatabaseBinding;
|
|
10
8
|
maxConnections: number | undefined;
|
|
11
9
|
reservedConnections: number | undefined;
|
|
@@ -13,4 +11,6 @@ export declare function finalizeRuntimeDatabaseConfig(database: NormalizedDataba
|
|
|
13
11
|
roles: import("../config/authoring/index.js").DatabaseRoleConfig[];
|
|
14
12
|
runtimeConnections: number | undefined;
|
|
15
13
|
serviceName: string;
|
|
14
|
+
sourceSchema: NormalizedSourceSchemaConfig | null;
|
|
15
|
+
template: NormalizedDatabaseTemplateConfig;
|
|
16
16
|
}> | undefined;
|
|
@@ -35,6 +35,9 @@ export interface SourceSchemaOptions extends SourceSchemaCacheLocationOptions, S
|
|
|
35
35
|
signal?: AbortSignal;
|
|
36
36
|
skipSchemaSourceVerify?: boolean;
|
|
37
37
|
}
|
|
38
|
+
interface SourceSchemaVerificationOptions extends SourceSchemaOptions {
|
|
39
|
+
resolveLocalSnapshotImage?: () => Promise<string>;
|
|
40
|
+
}
|
|
38
41
|
export interface InactiveSourceSchemaState {
|
|
39
42
|
active: false;
|
|
40
43
|
cachePath?: string;
|
|
@@ -86,7 +89,7 @@ export declare function resolveSourceSchemaState(config: Readonly<SourceSchemaSe
|
|
|
86
89
|
export declare function prepareSourceSchemaCache(config: Readonly<SourceSchemaServiceConfig>, options?: Readonly<SourceSchemaOptions>): Promise<SourceSchemaState>;
|
|
87
90
|
export declare function forceRefreshSourceSchemaCache(config: Readonly<SourceSchemaServiceConfig>, previousState: Readonly<SourceSchemaState> | null, options?: Readonly<SourceSchemaOptions>): Promise<RefreshedSourceSchemaState>;
|
|
88
91
|
export declare function applySourceSchemaCache(config: Readonly<SourceSchemaServiceConfig>, databaseUrl: string, state: Readonly<SourceSchemaState> | null | undefined, options?: Readonly<SourceSchemaOptions>): Promise<boolean>;
|
|
89
|
-
export declare function verifyLocalSchemaMatchesSource(config: Readonly<SourceSchemaServiceConfig>, databaseUrl: string, state: Readonly<SourceSchemaState> | null | undefined, options?: Readonly<
|
|
92
|
+
export declare function verifyLocalSchemaMatchesSource(config: Readonly<SourceSchemaServiceConfig>, databaseUrl: string, state: Readonly<SourceSchemaState> | null | undefined, options?: Readonly<SourceSchemaVerificationOptions>): Promise<SourceSchemaVerification>;
|
|
90
93
|
export declare function createSourceSchemaMismatchError(config: Readonly<SourceSchemaServiceConfig>, state: Readonly<ActiveSourceSchemaState>, verification: Readonly<SourceSchemaVerification> | null | undefined): Error;
|
|
91
94
|
export declare function appendSourceSchemaCacheToHash(hash: Pick<Hash, "update">, config: Readonly<SourceSchemaServiceConfig>, state?: Readonly<SourceSchemaState> | null): Promise<void>;
|
|
92
95
|
export declare function buildUnifiedDiffText(leftText: unknown, rightText: unknown, leftLabel?: string, rightLabel?: string): string;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import o from"fs";import s from"path";import{
|
|
2
|
-
`))}export async function appendSourceSchemaCacheToHash(e,r,t=null){if(!(r.testkit.database?.sourceSchema||null))return;const
|
|
3
|
-
`}function
|
|
1
|
+
import o from"fs";import s from"path";import{createRequire as J}from"node:module";import{collectRepoState as W,summarizeRepoStateForMetadata as C}from"../repo/state.js";import{buildExecutionEnv as V}from"../steps/index.js";import{dumpPostgresSchemaToFile as X,captureTemplateSnapshotText as Z,resolvePgDumpClientImage as G,runTemplateStep as Q,sanitizeSnapshotText as Y}from"./template-steps.js";import{getSourceSchemaRefreshLockPath as ee,withSourceSchemaRefreshLock as re}from"./source-refresh-lock.js";import{resolveSourceSchemaDumpUrl as te}from"./source-url.js";import{makeTempDir as ae,removeTempDir as ce}from"../process/files/temp-dir.js";import{requireContainedDatabaseStateDirectory as R,resolveContainedDatabaseStateDirectory as D}from"./state.js";const k=J(import.meta.url);let w;function ne(){if(w)return w;const e=k.resolve("diff/package.json"),t=JSON.parse(o.readFileSync(e,"utf8"))?.exports?.["."]?.import?.default;if(typeof t!="string"||!t.startsWith("./"))throw new Error("The diff package must declare a local ESM import entry");const c=s.dirname(e),a=s.resolve(c,t),n=s.relative(c,a);if(!n||n===".."||n.startsWith(`..${s.sep}`)||s.isAbsolute(n))throw new Error("The diff package ESM import entry must stay within its package directory");return w=k(a)}const K="db",U="source-schemas",P="source-schema.sql",E="source-schema.meta.json",se="index.json",M=20,p=String;export function getSourceSchemaRootDir(e){return D(e.productDir,K,e.name,U)}export async function getSourceSchemaCachePath(e,r={}){return(await resolveSourceSchemaCacheLocation(e,r)).cachePath}export async function resolveSourceSchemaCacheLocation(e,r={}){const t=r.repoState||await W(e.productDir,{fingerprints:e.testkit?.fingerprints}),c=getSourceSchemaRootDir(e),a=D(e.productDir,K,e.name,U,...t.cacheKey.split("/").map(v));return{rootDir:c,cacheDir:a,cachePath:s.join(a,P),metadataPath:s.join(a,E),indexPath:s.join(c,se),cacheKey:t.cacheKey,cacheKind:t.kind,repoState:t}}export async function resolveSourceSchemaState(e,r={}){const t=e.testkit.database?.sourceSchema||null;if(!t)return{active:!1};const c=await resolveSourceSchemaCacheLocation(e,r),a=c.cachePath,n=o.existsSync(a);n&&A(a);const i=r.env||process.env,u=V(e,{},i),h=(t.kind==="env"?[t.env]:[T(e.name),"TESTKIT_SCHEMA_SOURCE_DATABASE_URL"]).find(z=>{const _=u[z];return typeof _=="string"&&_.trim().length>0})||null,S=h?u[h]:void 0,f=typeof S=="string"?S.trim():null;return t.kind==="env"||!!f||n?{active:!0,cachePath:a,metadataPath:c.metadataPath,indexPath:c.indexPath,rootDir:c.rootDir,cacheDir:c.cacheDir,cacheKey:c.cacheKey,cacheKind:c.cacheKind,repoState:c.repoState,envName:h,sourceUrl:f,sourceSchema:t,cacheExists:n,refreshed:!1,ci:u.CI==="true",unavailableMode:Ee(t.unavailable,i)}:{active:!1,cachePath:a}}export async function prepareSourceSchemaCache(e,r={}){r.signal?.throwIfAborted();const t=await resolveSourceSchemaState(e,r);if(!t.active)return t;if(H(t)){if(de(t))try{const a=await F(e,t,r),n={...t,refreshed:!0,cacheExists:!0,refreshInfo:a};return y(e,n,{refreshed:!0}),n}catch(a){r.signal?.throwIfAborted();const n=ie(e,t,a,r);if(n)return n;throw a}return y(e,t),r.setupRegistry?.recordCached({config:e,stage:"source-schema",kind:"database-source-schema",summary:"source schema cache hit"}),t}if(t.cacheExists&&t.unavailableMode==="warn-cache")return y(e,t),r.setupRegistry?.recordCached({config:e,stage:"source-schema",kind:"database-source-schema",summary:"source unavailable; using cached source schema"}),t;const c=t.sourceSchema.kind==="env"?t.sourceSchema.env:`${T(e.name)} or TESTKIT_SCHEMA_SOURCE_DATABASE_URL`;throw new Error(`Source schema database URL is unavailable for service "${e.name}". Set ${c}.`)}function ie(e,r,t,c={}){if(r.unavailableMode!=="warn-cache")return null;const a=oe(e,r);if(!a)return null;const n={...r,...a,cacheExists:!0,refreshed:!1,refreshError:be(t)};return c.setupRegistry?.recordCached({config:e,stage:"source-schema",kind:"database-source-schema",summary:`source unavailable; using cached source schema: ${s.relative(e.productDir,a.cachePath)}`}),n}function oe(e,r){const t=$(e,r,{cachePath:r.cachePath,metadataPath:r.metadataPath,cacheKey:r.cacheKey,cacheKind:r.cacheKind});if(t)return t;const c=j(r.indexPath);c.sort((a,n)=>Date.parse(n.lastUsedAt||n.refreshedAt||"0")-Date.parse(a.lastUsedAt||a.refreshedAt||"0"));for(const a of c){if(!a.cachePath)continue;const n=s.resolve(e.productDir,a.cachePath),i=$(e,r,{cachePath:n,metadataPath:s.join(s.dirname(n),E),cacheKey:a.cacheKey,cacheKind:a.kind||"unknown"});if(i)return i}return null}function $(e,r,t){const c=s.resolve(t.cachePath);if(s.basename(c)!==P)return null;const a=R(e.productDir,s.dirname(c),`Service "${e.name}" source schema cache directory`),n=s.join(a,P),i=s.resolve(r.rootDir);if(n!==i&&!n.startsWith(`${i}${s.sep}`)||!o.existsSync(n))return null;const u=o.lstatSync(n);if(u.isSymbolicLink()||!u.isFile())throw new Error(`Refusing unsafe source schema cache file at ${n}`);return{cachePath:n,metadataPath:s.join(a,E),cacheDir:a,cacheKey:t.cacheKey,cacheKind:t.cacheKind,indexPath:r.indexPath,rootDir:r.rootDir}}export async function forceRefreshSourceSchemaCache(e,r,t={}){const c=await resolveSourceSchemaState(e,t);if(!c.active||!H(c)){const i=r?.active?r.envName:null,u=r?.active&&r.sourceSchema.kind==="env"?r.sourceSchema.env:null,m=i||u||"source schema env";throw new Error(`Cannot refresh source schema for service "${e.name}" because ${m} is unavailable.`)}const a=await F(e,c,t),n={...c,refreshed:!0,cacheExists:!0,refreshInfo:a};return y(e,n,{refreshed:!0}),n}export async function applySourceSchemaCache(e,r,t,c={}){if(!t?.active)return!1;if(!o.existsSync(t.cachePath))throw new Error(`Missing source schema cache for service "${e.name}": ${t.cachePath}`);const a={kind:"sql-file",path:s.relative(e.productDir,t.cachePath).split(s.sep).join("/"),cwd:null,inputs:[]};return await Q(e,"source-schema",a,0,r,c),!0}export async function verifyLocalSchemaMatchesSource(e,r,t,c={}){if(!t?.active)return{status:"disabled"};if(!t.sourceSchema.verify||c.skipSchemaSourceVerify)return c.setupRegistry?.recordCached({config:e,stage:"source-schema:verify",kind:"database-source-schema",summary:"source schema verification skipped"}),{status:"skipped"};ye(e);const a=le(t.cachePath),n=await Z(e,r,{image:await c.resolveLocalSnapshotImage?.(),ownedContainer:c.ownedContainer||null,reporter:c.reporter||null,signal:c.signal});return n===a?{status:"matched"}:{status:"mismatch",diagnostics:await De(e,t,a,n)}}export function createSourceSchemaMismatchError(e,r,t){const c=[`Local schema replay does not match source schema for service "${e.name}".`,`Source cache: ${s.relative(e.productDir,r.cachePath)}`];return t?.status==="mismatch"&&c.push(`Diff: ${s.relative(e.productDir,t.diagnostics.diffPath)}`),new Error(c.join(`
|
|
2
|
+
`))}export async function appendSourceSchemaCacheToHash(e,r,t=null){if(!(r.testkit.database?.sourceSchema||null))return;const a=t?.cachePath||await getSourceSchemaCachePath(r),n=t?.active&&t.cacheKey?t.cacheKey:s.relative(getSourceSchemaRootDir(r),s.dirname(a));if(e.update(`source-schema-key:${n}`),e.update(`source-schema-cache:${s.relative(r.productDir,a)}`),!o.existsSync(a)){e.update(":missing");return}const i=o.statSync(a);e.update(`:${i.size.toString()}:${i.mtimeMs.toString()}`),e.update(o.readFileSync(a))}async function F(e,r,t={}){const c=t.setupRegistry,a=c?.start({config:e,stage:"source-schema:refresh",kind:"database-source-schema",summary:`source schema refresh: ${s.relative(e.productDir,r.cachePath)}`});try{const n=await re(ee(r.cachePath),async({waited:u,requestedAt:m})=>{const h=me(r,m);if(h)return{reusedExistingRefresh:!0,waitedForLock:u,metadata:h};const S=await ue(e,r.sourceUrl,r.cachePath,{reporter:t.reporter||null,logRecord:a?._logRecord||null,env:t.env||process.env,signal:t.signal}),f=he(e,r,S);return Se(r.metadataPath,f),{reusedExistingRefresh:!1,waitedForLock:u,metadata:f}},{signal:t.signal}),i=a&&c?c.finish(a,{status:"passed",summary:`source schema refresh: ${s.relative(e.productDir,r.cachePath)}`}):null;return i&&t.reporter?.setupOperationFinished?.(i),n}catch(n){const i=a?c?.finish(a,{status:"failed",summary:`source schema refresh: ${s.relative(e.productDir,r.cachePath)}`,error:Ae(n)}):null;throw i&&t.reporter?.setupOperationFinished?.(i),n}}async function ue(e,r,t,c={}){const a=te(r);o.mkdirSync(s.dirname(t),{recursive:!0});const n=ae(".source-schema-",{parentDir:s.dirname(t)}),i=s.join(n,s.basename(t));try{const u=await G(e,a.dumpUrl,{env:c.env||process.env,pgApplicationName:g(e.name),signal:c.signal});return await X(e,i,a.dumpUrl,{...c,image:u,pgApplicationName:g(e.name)}),o.renameSync(i,t),{resolution:a,pgDump:{args:["--schema-only","--no-owner","--no-privileges"],applicationName:g(e.name),image:u}}}finally{ce(n)}}function he(e,r,t){return{refreshedAt:new Date().toISOString(),serviceName:e.name,envName:r.envName,cacheKey:r.cacheKey,cacheKind:r.cacheKind,cachePath:s.relative(e.productDir,r.cachePath),repo:C(r.repoState),sourceUrl:t.resolution.metadata,pgDump:t.pgDump}}function me(e,r){if(!o.existsSync(e.cachePath))return null;const t=b(e.metadataPath);if(!Ce(t))return null;const c=Date.parse(t.refreshedAt);return!Number.isFinite(c)||c<r?null:t}function g(e){return`testkit:schema-source:${e.replace(/[^a-zA-Z0-9._:-]+/g,"-").slice(0,24)||"service"}`}function le(e){return Y(o.readFileSync(e,"utf8"))}function de(e){if(!e.cacheExists)return!0;if(e.sourceSchema.refresh.mode==="auto")return e.ci;if(e.sourceSchema.refresh.mode==="ttl"){const r=x(e.metadataPath),t=r?Date.parse(r):0;return!Number.isFinite(t)||t<=0?!0:Date.now()-t>=e.sourceSchema.refresh.ttlSeconds*1e3}return!0}function y(e,r,t={}){if(!r?.active)return;const c=new Date().toISOString(),a=j(r.indexPath).filter(u=>u.cacheKey!==r.cacheKey),n=x(r.metadataPath);a.push({cacheKey:r.cacheKey,kind:r.cacheKind,commitSha:r.repoState.commitSha||null,branch:r.repoState.branch||null,detached:r.repoState.detached,dirty:r.repoState.dirty,dirtyFingerprint:r.repoState.dirtyFingerprint||null,contentFingerprint:r.repoState.contentFingerprint||null,remoteUrl:r.repoState.remoteUrl||null,repository:r.repoState.repository,cachePath:s.relative(e.productDir,r.cachePath),refreshedAt:t.refreshed?c:n,lastUsedAt:c});const i=fe(e,r,a);N(r.indexPath,{version:1,entries:i})}function fe(e,r,t){if(t.length<=M)return t;const c=[...t].sort((n,i)=>Date.parse(i.lastUsedAt||"0")-Date.parse(n.lastUsedAt||"0")),a=new Set(c.slice(0,M).map(n=>n.cacheKey));a.add(r.cacheKey);for(const n of t){if(a.has(n.cacheKey))continue;const i=n.cachePath?s.join(e.productDir,n.cachePath):null,u=i?s.resolve(i):null,m=s.resolve(r.rootDir);if(!i||!u||u!==m&&!u.startsWith(`${m}${s.sep}`))continue;const h=R(e.productDir,s.dirname(i),`Service "${e.name}" pruned source schema cache directory`);o.rmSync(h,{recursive:!0,force:!0})}return t.filter(n=>a.has(n.cacheKey))}function Se(e,r){I(e,r)}function N(e,r){I(e,r)}function I(e,r){o.mkdirSync(s.dirname(e),{recursive:!0}),A(e),o.writeFileSync(e,ge(r))}function b(e){if(!e||!o.existsSync(e))return null;A(e);try{return JSON.parse(o.readFileSync(e,"utf8"))}catch{return null}}function L(e){return D(e.productDir,"results","schema")}function A(e){if(!o.existsSync(e))return;const r=o.lstatSync(e);if(r.isSymbolicLink()||!r.isFile())throw new Error(`Refusing unsafe source schema state file at ${e}`)}function pe(e){return`${v(e.runtimeLabel||e.name)}__${v(e.name)}`}function ye(e){const r=L(e);if(!o.existsSync(r))return;const t=v(e.name);for(const c of o.readdirSync(r))ve(c,t)&&o.rmSync(s.join(r,c),{force:!0})}function ve(e,r){for(const t of["__source-schema.sql","__local-replay-schema.sql","__schema.diff","__schema.meta.json"])if(e.endsWith(t))return e.slice(0,-t.length).endsWith(`__${r}`);return!1}async function De(e,r,t,c){const a=L(e);o.mkdirSync(a,{recursive:!0});const n=pe(e),i=s.join(a,`${n}__source-schema.sql`),u=s.join(a,`${n}__local-replay-schema.sql`),m=s.join(a,`${n}__schema.diff`),h=s.join(a,`${n}__schema.meta.json`);return o.writeFileSync(i,t),o.writeFileSync(u,c),o.writeFileSync(m,await Pe(i,u)),N(h,we(e,r)),{sourceCachePath:r.cachePath,sourcePath:i,localPath:u,diffPath:m,metaPath:h}}function we(e,r){const t=x(r.metadataPath);return{version:1,createdAt:new Date().toISOString(),serviceName:e.name,runtimeLabel:e.runtimeLabel||e.name,sourceSchema:{envName:r.envName||null,cacheKey:r.cacheKey||null,cacheKind:r.cacheKind||null,cachePath:s.relative(e.productDir,r.cachePath),refreshedAt:t},repo:C(r.repoState)}}function Pe(e,r){return new Promise(t=>{t(buildUnifiedDiffText(o.readFileSync(e,"utf8"),o.readFileSync(r,"utf8"),e,r))})}export function buildUnifiedDiffText(e,r,t="source",c="target"){if(e===r)return"";const{createTwoFilesPatch:a}=ne();return a(t,c,p(e),p(r),"","",{context:3})}function Ee(e,r){return e==="fail"||e==="warn-cache"?e:r.CI==="true"?"fail":"warn-cache"}function T(e){return`TESTKIT_${e.toUpperCase().replace(/[^A-Z0-9]+/g,"_")}_SCHEMA_SOURCE_DATABASE_URL`}function v(e){return e.trim().toLowerCase().replace(/[^a-z0-9._-]+/g,"-").replace(/^-+|-+$/g,"")||"schema"}function j(e){const r=b(e);return!d(r)||!Array.isArray(r.entries)?[]:r.entries.filter(xe)}function x(e){const r=b(e);return d(r)&&typeof r.refreshedAt=="string"?r.refreshedAt:null}function ge(e){return`${p(JSON.stringify(e,null,2))}
|
|
3
|
+
`}function be(e){const r=O(e,"message");return!!r?r:p(e)}function Ae(e){const r=O(e,"message");return!!r?r:e}function O(e,r){if(!(e===null||typeof e!="object"&&typeof e!="function"))return Reflect.get(e,r)}function H(e){return!!e.sourceUrl}function xe(e){return!d(e)||typeof e.cacheKey!="string"||e.cacheKey.length===0?!1:l(e.branch)&&l(e.cachePath)&&l(e.commitSha)&&l(e.contentFingerprint)&&B(e.detached)&&B(e.dirty)&&l(e.dirtyFingerprint)&&l(e.kind)&&l(e.lastUsedAt)&&l(e.refreshedAt)&&l(e.remoteUrl)&&_e(e.repository)}function _e(e){return e==null||d(e)&&typeof e.host=="string"&&typeof e.owner=="string"&&typeof e.name=="string"}function Ce(e){return d(e)&&typeof e.refreshedAt=="string"&&(e.sourceUrl===void 0||Re(e.sourceUrl))}function Re(e){return d(e)&&typeof e.originalHost=="string"&&q(e.originalClassification)&&typeof e.resolvedHost=="string"&&q(e.resolvedClassification)&&typeof e.rewritten=="boolean"}function q(e){return e==="neon-pooler"||e==="neon-direct"||e==="unsupported-pooler"||e==="unknown-direct"}function ke(e){return e===null||typeof e=="string"}function l(e){return e===void 0||ke(e)}function B(e){return e===void 0||typeof e=="boolean"}function d(e){return e!==null&&typeof e=="object"&&!Array.isArray(e)}
|
|
@@ -29,6 +29,10 @@ export declare function captureTemplateSnapshotText(config: Readonly<DatabaseTem
|
|
|
29
29
|
export declare function dumpPostgresSchemaToFile(config: Readonly<DatabaseTemplateServiceConfig>, outputPath: string, databaseUrl: string, options?: Readonly<PostgresDumpOptions>): Promise<void>;
|
|
30
30
|
export declare function pgDumpArgs(): string[];
|
|
31
31
|
export declare function resolvePgDumpClientImage(config: Readonly<DatabaseTemplateServiceConfig>, databaseUrl: string, options?: Readonly<PostgresDumpOptions>): Promise<string>;
|
|
32
|
+
/** Retain only the detected version of one owned server during a template rebuild. */
|
|
33
|
+
export declare function createOwnedPgDumpClientImageResolver(config: Readonly<DatabaseTemplateServiceConfig>, databaseUrl: string, options: Readonly<PostgresDumpOptions & {
|
|
34
|
+
ownedContainer: OwnedPostgresContainerRoute;
|
|
35
|
+
}>): () => Promise<string>;
|
|
32
36
|
export declare function postgresClientImageForServerVersion(serverVersionNum: unknown): string;
|
|
33
37
|
export declare function sanitizeSnapshotFile(filePath: string): void;
|
|
34
38
|
export declare function sanitizeSnapshotText(dump: unknown): string;
|