@gfdelarue/testkit 0.3.4 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/README.md +4 -0
  2. package/dist/app/doctor/index.js +1 -1
  3. package/dist/app/inspect/artifacts.d.ts +1 -0
  4. package/dist/app/inspect/artifacts.js +1 -1
  5. package/dist/app/inspect/index.js +1 -1
  6. package/dist/app/lint/index.js +1 -1
  7. package/dist/app/lint/native.js +1 -1
  8. package/dist/app/lint/policy.js +1 -1
  9. package/dist/app/lint/rules/data.js +1 -1
  10. package/dist/app/lint/rules/engine-imports.js +1 -1
  11. package/dist/app/lint/rules/syntax.js +1 -1
  12. package/dist/app/lint/rules/unit.js +1 -1
  13. package/dist/app/lint/shared.d.ts +1 -1
  14. package/dist/app/lint/shared.js +1 -1
  15. package/dist/app/run/orchestrator.js +1 -1
  16. package/dist/app/run/request.js +1 -1
  17. package/dist/app/setup/playwright.js +1 -1
  18. package/dist/app/status/index.d.ts +2 -0
  19. package/dist/app/status/index.js +1 -1
  20. package/dist/app/status/runs.d.ts +2 -2
  21. package/dist/app/status/runs.js +1 -1
  22. package/dist/app/stop/index.js +1 -1
  23. package/dist/app/typecheck/index.d.ts +2 -0
  24. package/dist/app/typecheck/index.js +4 -4
  25. package/dist/cli/commands/artifacts.js +2 -2
  26. package/dist/cli/commands/db/schema/refresh.js +1 -1
  27. package/dist/cli/commands/db/schema/verify.js +1 -1
  28. package/dist/cli/commands/doctor.js +1 -1
  29. package/dist/cli/operations/db/schema/refresh/operation.d.ts +1 -0
  30. package/dist/cli/operations/db/schema/refresh/operation.js +1 -1
  31. package/dist/cli/operations/db/schema/verify/operation.d.ts +1 -0
  32. package/dist/cli/operations/db/schema/verify/operation.js +1 -1
  33. package/dist/cli/operations/run/operation.js +1 -1
  34. package/dist/config/loader/discover-tests.d.ts +5 -0
  35. package/dist/config/loader/discover-tests.js +1 -1
  36. package/dist/config/loader/discovery.js +2 -2
  37. package/dist/config/loader/file-metadata.js +1 -1
  38. package/dist/config/loader/index.d.ts +2 -1
  39. package/dist/config/loader/index.js +1 -1
  40. package/dist/config/loader/load.d.ts +2 -0
  41. package/dist/config/loader/load.js +1 -1
  42. package/dist/config/loader/managed-configs.d.ts +2 -0
  43. package/dist/config/loader/managed-configs.js +1 -1
  44. package/dist/config/loader/paths.d.ts +1 -1
  45. package/dist/config/loader/paths.js +1 -1
  46. package/dist/config/loader/skip-config.d.ts +1 -1
  47. package/dist/config/loader/test-selection.d.ts +15 -0
  48. package/dist/config/loader/test-selection.js +1 -0
  49. package/dist/database/index.d.ts +1 -3
  50. package/dist/database/index.js +1 -1
  51. package/dist/database/lifecycle-cleanup.d.ts +7 -0
  52. package/dist/database/lifecycle-cleanup.js +1 -0
  53. package/dist/database/local-postgres.js +1 -1
  54. package/dist/database/schema-source.js +3 -3
  55. package/dist/engines/adapters/evidence-files.js +1 -1
  56. package/dist/engines/catalog.d.ts +3 -8
  57. package/dist/engines/catalog.js +1 -1
  58. package/dist/engines/definitions.d.ts +8 -1
  59. package/dist/engines/definitions.js +1 -1
  60. package/dist/engines/generated-performance.d.ts +1 -0
  61. package/dist/engines/generated-performance.js +12 -0
  62. package/dist/engines/installations/node-vitest.d.ts +8 -22
  63. package/dist/engines/installations/node-vitest.js +1 -1
  64. package/dist/engines/installations/package-requirements.d.ts +28 -0
  65. package/dist/engines/installations/package-requirements.js +1 -0
  66. package/dist/engines/installations/playwright.d.ts +1 -11
  67. package/dist/engines/installations/playwright.js +1 -1
  68. package/dist/engines/k6/adapter/repository-configuration.d.ts +1 -1
  69. package/dist/engines/k6/adapter/repository-configuration.js +1 -1
  70. package/dist/engines/managed-module-resolution.js +111 -21
  71. package/dist/engines/manager.d.ts +1 -0
  72. package/dist/engines/manager.js +1 -1
  73. package/dist/engines/node-vitest/adapter/adapter.d.ts +1 -1
  74. package/dist/engines/node-vitest/adapter/adapter.js +1 -1
  75. package/dist/engines/node-vitest/adapter/consumer-typescript-paths.js +1 -1
  76. package/dist/engines/node-vitest/adapter/resolution.d.ts +1 -1
  77. package/dist/engines/node-vitest/adapter/resolution.js +1 -1
  78. package/dist/engines/node-vitest/adapter/workspace.js +33 -9
  79. package/dist/engines/node-vitest/runtime/runtime-input.d.ts +1 -0
  80. package/dist/engines/node-vitest/runtime/runtime-input.js +1 -1
  81. package/dist/engines/runtime-input-file.d.ts +3 -1
  82. package/dist/engines/runtime-input-file.js +1 -1
  83. package/dist/environment/lifecycle-controller.d.ts +1 -2
  84. package/dist/environment/lifecycle-controller.js +1 -1
  85. package/dist/environment/lifecycle.js +1 -1
  86. package/dist/environment/maintenance.d.ts +1 -1
  87. package/dist/environment/maintenance.js +1 -1
  88. package/dist/environment/ready-environment.d.ts +1 -1
  89. package/dist/environment/ready-environment.js +1 -1
  90. package/dist/environment/runtime-contexts.d.ts +1 -1
  91. package/dist/environment/runtime-contexts.js +1 -1
  92. package/dist/environment/runtime-preparation.js +1 -1
  93. package/dist/environment/services.js +1 -1
  94. package/dist/environment/template.d.ts +1 -1
  95. package/dist/environment/template.js +1 -1
  96. package/dist/evidence/bundles.js +1 -1
  97. package/dist/evidence/read-run.js +1 -1
  98. package/dist/evidence/reading.d.ts +1 -1
  99. package/dist/evidence/reading.js +1 -1
  100. package/dist/evidence/storage.js +2 -2
  101. package/dist/kernel/error-serialization.js +1 -1
  102. package/dist/kernel/work-metrics.d.ts +6 -0
  103. package/dist/kernel/work-metrics.js +1 -0
  104. package/dist/process/capture-output.js +1 -1
  105. package/dist/process/execa-bundle.json +1 -1
  106. package/dist/process/files/product-dir.d.ts +1 -0
  107. package/dist/process/files/product-dir.js +1 -0
  108. package/dist/process/locks.js +4 -4
  109. package/dist/process/readiness.js +1 -1
  110. package/dist/process/runs/inspection.d.ts +5 -0
  111. package/dist/process/runs/inspection.js +1 -0
  112. package/dist/process/runs/manifest.d.ts +1 -0
  113. package/dist/process/runs/manifest.js +1 -1
  114. package/dist/process/service-processes.d.ts +1 -1
  115. package/dist/process/service-processes.js +1 -1
  116. package/dist/toolchains/index.js +2 -2
  117. package/docs/engine-architecture.md +4 -1
  118. package/docs/test-types.md +9 -0
  119. package/package.json +2 -1
  120. package/dist/cli/renderers/artifact-preview.d.ts +0 -4
  121. package/dist/cli/renderers/artifact-preview.js +0 -1
@@ -1 +1 @@
1
- import{optionalString as A}from"../kernel/unknown-values.js";import{readRunProvenance as oe}from"./read-provenance.js";import{parseRunBundleArtifact as ae,parseRunBundleManifest as J,readRunBundleArtifact as ce}from"./read-bundle.js";import{readRunDocument as ue}from"./read-run.js";import le from"crypto";import c from"fs";import u from"path";import{redactSensitiveText as Y}from"../kernel/redaction.js";import{BUNDLE_SCHEMA_VERSION as G,OBJECTS_DIRNAME as Q,PROVENANCE_SCHEMA_VERSION as fe,RETENTION_DECISION_SCHEMA_VERSION as de,RUNS_DIRNAME as me}from"./contracts.js";import{bundleRelativePath as P,errorMessage as k,hashFile as z,isObjectRecord as $,normalizePath as X,normalizeRunId as w,objectRecords as Z,optionalStringArray as D,optionalUnknownArray as pe,readJsonFile as O,requirePublicationPathOutsideResults as M,requireRunId as he,runIncludesFile as ge,resolveManifestEntryPath as U,resolveRunBundleDirectory as x,resolveRunsRoot as ee,resultsRoot as q,sanitizeSegment as ye,sha256 as C,stableJson as W,treeBytes as te,walkFiles as ne,writeBufferAtomic as ve,writeJson as N}from"./storage.js";import{resolveCiProvenance as Se}from"./ci-provenance.js";const K=["artifacts","logs","setup"],Re=/(?:PASSWORD|TOKEN|SECRET|PRIVATE_KEY|AUTH|COOKIE|SESSION|API_KEY)/iu,je=["package.json","package-lock.json","npm-shrinkwrap.json","pnpm-lock.yaml","yarn.lock","testkit.config.ts","testkit.config.mts","testkit.config.js","testkit.config.mjs"];export function sealRunBundle(e,r,t={},s={}){const n=r,i=he(n),o=ee(e,!0),a=u.dirname(o),f=u.join(o,i),m=s.cleanupActiveRunResults??_e,p=loadVerifiedRunBundleArtifactIfExists(e,i);if(p!==null)return{...p,archive:{...p.archive,cleanup:m(a)}};const d=u.join(o,`.${i}.${String(process.pid)}.${String(Date.now())}.${le.randomBytes(4).toString("hex")}.tmp`);c.mkdirSync(d,{recursive:!0});try{const R=Ne(t.env??process.env),g=be({productDir:e,resultsDir:a,runId:i,tempDir:d,sensitiveValues:R}),h=Ie(n,i,R),y=ue(h.artifact,"run artifact");g.redaction.structuredReplacements=h.replacements,g.redaction.replacements+=h.replacements;const v=Be(e,y,i,{...t,redaction:g.redaction}),j={schemaVersion:G,runId:i,status:"complete",canonicalRunPath:P(i,"run.json"),manifestPath:P(i,"manifest.json"),provenancePath:P(i,"provenance.json"),integrity:"sha256",publication:{configured:!!t.archiveConfig?.publication,status:t.archiveConfig?.publication?"pending":"not_configured"},redaction:g.redaction};y.archive=j,N(u.join(d,"run.json"),y),N(u.join(d,"provenance.json"),v);const b=Ae(d),V=C(Buffer.from(W(b))),E={schemaVersion:G,runId:i,createdAt:new Date().toISOString(),algorithm:"sha256",files:b,storage:{fileCount:b.length,logicalBytes:b.reduce((_,F)=>_+F.bytes,0),objectBytesWritten:g.objectBytesWritten,deduplicatedBytes:g.deduplicatedBytes},redaction:g.redaction,digest:V};return N(u.join(d,"manifest.json"),E),c.renameSync(d,f),{...y,archive:{...j,cleanup:m(a),manifestDigest:E.digest,storage:E.storage}}}catch(R){try{c.rmSync(d,{recursive:!0,force:!0})}catch{}try{xe(e)}catch{}throw R}}export function loadVerifiedRunBundleArtifactIfExists(e,r){const t=w(r),s=x(e,t);if(!c.existsSync(s))return null;const n=verifyRunBundle(e,t);if(!n.ok)throw new Error(`Immutable run bundle ${t} already exists but failed integrity verification: ${n.errors.join("; ")}`);return loadRunBundleArtifact(e,t)}export function loadRunBundleArtifact(e,r){const t=w(r),s=ce(e,t),n=loadRunBundleManifest(e,t);return{...s,archive:{...s.archive,manifestDigest:n.digest,storage:n.storage}}}export function loadRunBundleManifest(e,r){const t=w(r),s=x(e,t),n=u.join(s,"manifest.json");if(!c.existsSync(n))throw new Error(`No immutable Testkit run manifest found for ${t}`);return J(O(n),n,t)}export function listRunBundles(e,r={}){const t=ee(e,!1);return c.existsSync(t)?c.readdirSync(t,{withFileTypes:!0}).filter(n=>n.isDirectory()&&!n.name.startsWith(".")).map(n=>{try{const i=loadRunBundleArtifact(e,n.name);return r.file&&!ge(i,r.file)?null:{runId:n.name,status:i.run.status,startedAt:i.run.startedAt,finishedAt:i.run.finishedAt??null,manifestDigest:i.archive.manifestDigest??null,canonicalRunPath:i.archive.canonicalRunPath}}catch{return null}}).filter(n=>n!==null).sort((n,i)=>(i.finishedAt??i.startedAt??"").localeCompare(n.finishedAt??n.startedAt??"")):[]}export function verifyRunBundle(e,r){const t=w(r);try{return T(x(e,t),t)}catch(s){return{ok:!1,runId:t,errors:[k(s)],checkedFiles:0}}}export function prepareVerifiedRunBundleUpload(e,r=null){const t=r??listRunBundles(e).at(0)?.runId??null;if(t===null)throw new Error("No immutable Testkit run bundle is available to publish");const s=verifyRunBundle(e,t);if(!s.ok)throw new Error(`Immutable run bundle ${t} failed integrity verification: ${s.errors.join("; ")}`);const n=loadRunBundleManifest(e,t),i=x(e,t);return{runId:t,manifest:n,files:n.files.map(o=>({...o,absolutePath:U(i,o.path)}))}}export function publishRunBundle(e,r,t,s={}){const n=w(r),i=s.writePublicationReceipt??ke;if(!t)return i(e,n,{status:"not_configured",required:!1,target:null,bytesUploaded:0});const o=u.resolve(e,t.directory);try{M(e,o)}catch(d){return i(e,n,{status:"failed",required:t.required,target:o,error:k(d),bytesUploaded:0})}const a=ye(u.basename(e)),f=u.join(o,a),m=u.join(f,n),p=u.join(f,`.${n}.${String(process.pid)}.${String(Date.now())}.tmp`);try{const d=x(e,n);if(c.mkdirSync(f,{recursive:!0}),M(e,f),c.existsSync(m)){const h=c.lstatSync(m);if(h.isSymbolicLink()||!h.isDirectory())throw new Error("Publication target must be a regular bundle directory");M(e,m);const y=T(d,n);if(!y.ok)throw new Error(`Source run bundle failed integrity verification: ${y.errors.join("; ")}`);const v=T(m,n);if(!v.ok)throw new Error(`Publication target failed integrity verification: ${v.errors.join("; ")}`);if(y.manifestDigest!==v.manifestDigest)throw new Error("Publication target already exists with a different manifest digest");return i(e,n,{status:"complete",required:t.required,target:m,manifestDigest:y.manifestDigest,reused:!0,bundleBytes:te(d),bytesUploaded:0})}const R=te(d);c.cpSync(d,p,{recursive:!0,preserveTimestamps:!0});const g=T(p,n);if(!g.ok)throw new Error(g.errors.join("; "));return c.renameSync(p,m),i(e,n,{status:"complete",required:t.required,target:m,manifestDigest:g.manifestDigest,reused:!1,bundleBytes:R,bytesUploaded:R})}catch(d){return c.rmSync(p,{recursive:!0,force:!0}),i(e,n,{status:"failed",required:t.required,target:m,error:k(d),bytesUploaded:0})}}export function applyRunBundleRetention(e,r,t={},s={}){const n=w(r),i=s.removeObject??ie,o=s.removeRunBundle??Pe,a=t.maxRuns??null,f=t.maxAgeDays??null,m=new Set(t.preserveSuites||[]),p=listRunBundles(e).map(l=>({...l,artifact:loadRunBundleArtifact(e,l.runId)})),d=new Set([n]);for(const l of p)$e(l.artifact,m)&&d.add(l.runId);const R=new Set(a?p.slice(0,a).map(l=>l.runId):p.map(l=>l.runId)),g=f==null?null:Date.now()-f*24*60*60*1e3,h=[];for(const l of p){if(d.has(l.runId))continue;const S=g!=null&&Date.parse(l.finishedAt||l.startedAt||"")<g,I=!R.has(l.runId);if(!S&&!I)continue;const B=[];S&&B.push("max_age"),I&&B.push("max_runs"),h.push({path:u.join(q(e),me,l.runId),reasons:B,runId:l.runId})}const y=new Map(p.map(l=>[l.runId,loadRunBundleManifest(e,l.runId).files.filter(S=>S.path.startsWith("files/")).map(S=>S.sha256)])),v=re(e),j=[],b=[];for(const l of h){let S=null;try{o(l.path)}catch(B){S=H(B)}const I=!c.existsSync(l.path);!I&&S===null&&(S="Run bundle still exists after removal completed"),I&&j.push({reasons:l.reasons,runId:l.runId}),S!==null&&b.push({...l,error:S,removed:I})}const V=new Set(j.map(l=>l.runId)),E=new Set(p.filter(l=>!V.has(l.runId)).flatMap(l=>y.get(l.runId)??[])),_=se(v,E,i),F=new Date().toISOString();return Oe({currentRunId:n,decidedAt:F,garbageCollection:_,policy:{maxRuns:a,maxAgeDays:f,preserveSuites:[...m].sort()},protectedRunIds:[...d].sort(),removed:j,removalErrors:b})}function be({resultsDir:e,tempDir:r,sensitiveValues:t}){let s=0,n=0,i=0,o=0,a=0;const f=u.join(e,Q,"sha256");c.mkdirSync(f,{recursive:!0});for(const m of K){const p=u.join(e,m);if(c.existsSync(p))for(const d of ne(p)){const R=u.relative(p,d),g=c.readFileSync(d),h=Ce(g,d,t);h.scanned&&(i+=1),h.replacements>0&&(o+=1,a+=h.replacements);const y=C(h.buffer),v=u.join(f,y);c.existsSync(v)?n+=h.buffer.length:(ve(v,h.buffer),c.chmodSync(v,292),s+=h.buffer.length);const j=u.join(r,"files",m,R);c.mkdirSync(u.dirname(j),{recursive:!0});try{c.linkSync(v,j)}catch{c.copyFileSync(v,j)}}}return{objectBytesWritten:s,deduplicatedBytes:n,redaction:{policyVersion:1,status:"applied",textualFilesScanned:i,filesRedacted:o,replacements:a,sensitiveEnvironmentKeys:t.map(m=>m.key).sort(),valuesStored:!1}}}function Ie(e,r,t){const s=new Map(K.map(i=>[`.testkit/results/${i}/`,`${P(r,`files/${i}`)}/`])),n=L(e,s);return Ee(we(n),t)}function we(e){if(!$(e))return e;const r={...e};if($(r.product)){const t={...r.product};delete t.directory,r.product=t}if($(r.git)){const t={...r.git};delete t.remoteUrl,delete t.repoRoot,delete t.worktreePath,r.git=t}return delete r.host,r}function L(e,r){if(Array.isArray(e))return e.map(s=>L(s,r));if($(e))return Object.fromEntries(Object.entries(e).map(([s,n])=>[s,L(n,r)]));if(typeof e!="string")return e;let t=e;for(const[s,n]of r)t.startsWith(s)&&(t=`${n}${t.slice(s.length)}`);return t}function Ee(e,r){let t=0;function s(n){if(Array.isArray(n))return n.map(s);if($(n))return Object.fromEntries(Object.entries(n).map(([o,a])=>[o,s(a)]));if(typeof n!="string")return n;let i=Y(n);i!==n&&(t+=1);for(const o of r){if(!i.includes(o.value))continue;const a=i.split(o.value);t+=a.length-1,i=a.join("<redacted>")}return i}return{artifact:s(e),replacements:t}}function Be(e,r,t,s){const n=D(r.run.fileNames),i=[...new Set([...je,...n])].map(o=>Ve(e,o)).filter(o=>o!==null);return{schemaVersion:fe,runId:t,ci:Se(s.env??process.env),source:{git:r.git??null,files:i},runtime:{controller:{testkitVersion:A(r.run.testkitVersion),node:process.versions.node,platform:process.platform,architecture:process.arch},runtimeStats:pe(r.run.runtimeStats)},scenario:{types:D(r.run.types),suiteSelectors:D(r.run.suiteSelectors),fileNames:n,serviceFilter:A(r.run.serviceFilter),planningDigest:C(Buffer.from(W(r.planning??null))),declaredEnvironmentKeys:[...new Set(s.declaredEnvironmentKeys??[])].sort()},redaction:s.redaction}}function Ae(e){return ne(e).map(r=>({path:X(u.relative(e,r)),bytes:c.statSync(r).size,sha256:z(r)})).sort((r,t)=>r.path.localeCompare(t.path))}function T(e,r){const t=u.join(e,"manifest.json"),s=J(O(t),t,r);ae(O(u.join(e,"run.json")),u.join(e,"run.json"),r);const n=[];for(const o of s.files){let a;try{a=U(e,o.path)}catch(m){n.push(k(m));continue}if(!c.existsSync(a)){n.push(`missing ${o.path}`);continue}const f=c.lstatSync(a);if(f.isSymbolicLink()){n.push(`symbolic link not allowed ${o.path}`);continue}if(!f.isFile()){n.push(`not a regular file ${o.path}`);continue}f.size!==o.bytes&&n.push(`size mismatch ${o.path}`),z(a)!==o.sha256&&n.push(`checksum mismatch ${o.path}`)}C(Buffer.from(W(s.files)))!==s.digest&&n.push("manifest digest mismatch"),s.storage.fileCount!==s.files.length&&n.push("manifest file count mismatch");for(const o of["run.json","provenance.json"])s.files.some(a=>a.path===o)||n.push(`manifest must include ${o}`);if(n.length===0){const o=U(e,"provenance.json");oe(O(o),o,r)}return{runId:r,checkedFiles:s.files.length,storage:s.storage,ok:n.length===0,errors:n,manifestDigest:s.digest}}function ke(e,r,t){const s={schemaVersion:1,runId:r,recordedAt:new Date().toISOString(),...t};return N(u.join(q(e),"publications",`${r}.json`),s),s}function $e(e,r){return r.size===0?!1:Z(e.services).some(t=>Z(t.suites).some(s=>{const n=A(t.name)??"",i=A(s.name)??"",o=A(s.testType)??"";return[i,`${o}:${i}`,`${n}/${o}:${i}`].some(f=>r.has(f))}))}function xe(e){const r=new Set;for(const t of listRunBundles(e)){const s=loadRunBundleManifest(e,t.runId);for(const n of s.files)n.path.startsWith("files/")&&r.add(n.sha256)}return se(re(e),r,ie)}function re(e){const r=u.join(q(e),Q,"sha256");return c.existsSync(r)?c.readdirSync(r,{withFileTypes:!0}).filter(t=>t.isFile()).map(t=>{const s=u.join(r,t.name);return{bytes:c.statSync(s).size,digest:t.name,path:s}}):[]}function se(e,r,t){const s=[];let n=0,i=0;for(const o of e){if(r.has(o.digest))continue;let a=null;try{t(o.path)}catch(m){a=H(m)}!c.existsSync(o.path)?(n+=1,i+=o.bytes):a===null&&(a="Object still exists after removal completed"),a!==null&&s.push({error:a,path:o.path})}return{failures:s,reclaimedBytes:i,removedObjects:n,status:s.length===0?"complete":"failed"}}function Pe(e){c.rmSync(e,{recursive:!0,force:!0})}function ie(e){c.rmSync(e,{force:!0})}function Oe({currentRunId:e,decidedAt:r,garbageCollection:t,policy:s,protectedRunIds:n,removed:i,removalErrors:o}){const a=[...o.map(f=>`remove ${f.runId}: ${f.error}`),...t.failures.map(f=>`garbage collect ${f.path}: ${f.error}`)];return{currentRunId:e,decidedAt:r,garbageCollection:t,policy:s,protectedRunIds:n,removed:i,removalErrors:o,schemaVersion:de,status:a.length===0?"complete":"failed",...a.length===0?{}:{error:a.join("; ")}}}function Ce(e,r,t){if(!Te(e,r)){for(const o of t)if(e.includes(Buffer.from(o.value)))throw new Error(`Refusing to archive binary file containing sensitive environment value: ${r}`);return{buffer:e,scanned:!1,replacements:0}}const s=e.toString("utf8");let n=Y(s),i=n===s?0:1;for(const o of t){if(!n.includes(o.value))continue;const a=n.split(o.value);i+=a.length-1,n=a.join("<redacted>")}return{buffer:Buffer.from(n),scanned:!0,replacements:i}}function Ne(e){const r=[];for(const[t,s]of Object.entries(e))Re.test(t)&&typeof s=="string"&&s.length>=8&&r.push({key:t,value:s});return r}function Te(e,r){if(e.includes(0))return!1;const t=u.extname(r).toLowerCase();return![".zip",".gz",".png",".jpg",".jpeg",".webm",".pdf"].includes(t)}function Ve(e,r){const t=u.resolve(e,r);return!t.startsWith(`${u.resolve(e)}${u.sep}`)||!c.existsSync(t)||!c.statSync(t).isFile()?null:{path:X(u.relative(e,t)),bytes:c.statSync(t).size,sha256:z(t)}}function _e(e){const r=[];for(const t of[...K.map(s=>u.join(e,s)),u.join(e,"live.json")])try{c.rmSync(t,{recursive:!0,force:!0})}catch(s){r.push(`${t}: ${H(s)}`)}return r.length===0?{status:"complete"}:{error:r.join("; "),status:"failed"}}function H(e){try{return k(e)}catch{return"Unknown post-seal archive operation failure"}}
1
+ import{recordWork as J}from"../kernel/work-metrics.js";import{optionalString as A}from"../kernel/unknown-values.js";import{readRunProvenance as ue}from"./read-provenance.js";import{parseRunBundleArtifact as le,parseRunBundleManifest as Y,readRunBundleArtifact as fe}from"./read-bundle.js";import{readRunDocument as de}from"./read-run.js";import me from"crypto";import c from"fs";import u from"path";import{redactSensitiveText as G}from"../kernel/redaction.js";import{BUNDLE_SCHEMA_VERSION as Q,OBJECTS_DIRNAME as X,PROVENANCE_SCHEMA_VERSION as pe,RETENTION_DECISION_SCHEMA_VERSION as he,RUNS_DIRNAME as ge}from"./contracts.js";import{bundleRelativePath as P,errorMessage as k,hashFile as z,isObjectRecord as $,normalizePath as Z,normalizeRunId as I,objectRecords as ee,optionalStringArray as D,optionalUnknownArray as ye,readJsonFile as O,requirePublicationPathOutsideResults as M,requireRunId as ve,runIncludesFile as Se,resolveManifestEntryPath as U,resolveRunBundleDirectory as x,resolveRunsRoot as te,resultsRoot as q,sanitizeSegment as Re,sha256 as C,stableJson as W,treeBytes as ne,walkFiles as re,writeBufferAtomic as je,writeJson as N}from"./storage.js";import{resolveCiProvenance as be}from"./ci-provenance.js";const K=["artifacts","logs","setup"],we=/(?:PASSWORD|TOKEN|SECRET|PRIVATE_KEY|AUTH|COOKIE|SESSION|API_KEY)/iu,Ie=["package.json","package-lock.json","npm-shrinkwrap.json","pnpm-lock.yaml","yarn.lock","testkit.config.ts","testkit.config.mts","testkit.config.js","testkit.config.mjs"];export function sealRunBundle(e,n,t={},s={}){const r=n,o=ve(r),i=te(e,!0),a=u.dirname(i),f=u.join(i,o),m=s.cleanupActiveRunResults??De,p=loadVerifiedRunBundleArtifactIfExists(e,o);if(p!==null)return{...p,archive:{...p.archive,cleanup:m(a)}};const d=u.join(i,`.${o}.${String(process.pid)}.${String(Date.now())}.${me.randomBytes(4).toString("hex")}.tmp`);c.mkdirSync(d,{recursive:!0});try{const R=_e(t.env??process.env),g=Ee({productDir:e,resultsDir:a,runId:o,tempDir:d,sensitiveValues:R}),h=Be(r,o,R),y=de(h.artifact,"run artifact");g.redaction.structuredReplacements=h.replacements,g.redaction.replacements+=h.replacements;const v=$e(e,y,o,{...t,redaction:g.redaction}),j={schemaVersion:Q,runId:o,status:"complete",canonicalRunPath:P(o,"run.json"),manifestPath:P(o,"manifest.json"),provenancePath:P(o,"provenance.json"),integrity:"sha256",publication:{configured:!!t.archiveConfig?.publication,status:t.archiveConfig?.publication?"pending":"not_configured"},redaction:g.redaction};y.archive=j,N(u.join(d,"run.json"),y),N(u.join(d,"provenance.json"),v);const b=xe(d),T=C(Buffer.from(W(b))),E={schemaVersion:Q,runId:o,createdAt:new Date().toISOString(),algorithm:"sha256",files:b,storage:{fileCount:b.length,logicalBytes:b.reduce((_,F)=>_+F.bytes,0),objectBytesWritten:g.objectBytesWritten,deduplicatedBytes:g.deduplicatedBytes},redaction:g.redaction,digest:T};return N(u.join(d,"manifest.json"),E),c.renameSync(d,f),{...y,archive:{...j,cleanup:m(a),manifestDigest:E.digest,storage:E.storage}}}catch(R){try{c.rmSync(d,{recursive:!0,force:!0})}catch{}try{Ce(e)}catch{}throw R}}export function loadVerifiedRunBundleArtifactIfExists(e,n){const t=I(n),s=x(e,t);if(!c.existsSync(s))return null;const r=verifyRunBundle(e,t);if(!r.ok)throw new Error(`Immutable run bundle ${t} already exists but failed integrity verification: ${r.errors.join("; ")}`);return loadRunBundleArtifact(e,t)}export function loadRunBundleArtifact(e,n){return se(e,n).artifact}function se(e,n){const t=I(n),s=fe(e,t),r=loadRunBundleManifest(e,t);return{manifest:r,artifact:{...s,archive:{...s.archive,manifestDigest:r.digest,storage:r.storage}}}}export function loadRunBundleManifest(e,n){const t=I(n),s=x(e,t),r=u.join(s,"manifest.json");if(!c.existsSync(r))throw new Error(`No immutable Testkit run manifest found for ${t}`);return Y(O(r),r,t)}export function listRunBundles(e,n={}){return ie(e,n).map(({artifact:t,manifest:s,...r})=>r)}function ie(e,n={}){const t=te(e,!1);if(!c.existsSync(t))return[];const s=c.readdirSync(t,{withFileTypes:!0});return J("artifacts.runDirectoriesRead",1),J("artifacts.runEntriesVisited",s.length),s.filter(o=>o.isDirectory()&&!o.name.startsWith(".")).map(o=>{try{const{artifact:i,manifest:a}=se(e,o.name);return n.file&&!Se(i,n.file)?null:{artifact:i,manifest:a,runId:o.name,status:i.run.status,startedAt:i.run.startedAt,finishedAt:i.run.finishedAt??null,manifestDigest:i.archive.manifestDigest??null,canonicalRunPath:i.archive.canonicalRunPath}}catch{return null}}).filter(o=>o!==null).sort((o,i)=>(i.finishedAt??i.startedAt??"").localeCompare(o.finishedAt??o.startedAt??""))}export function verifyRunBundle(e,n){const t=I(n);try{return V(x(e,t),t)}catch(s){return{ok:!1,runId:t,errors:[k(s)],checkedFiles:0}}}export function prepareVerifiedRunBundleUpload(e,n=null){const t=n??listRunBundles(e).at(0)?.runId??null;if(t===null)throw new Error("No immutable Testkit run bundle is available to publish");const s=verifyRunBundle(e,t);if(!s.ok)throw new Error(`Immutable run bundle ${t} failed integrity verification: ${s.errors.join("; ")}`);const r=loadRunBundleManifest(e,t),o=x(e,t);return{runId:t,manifest:r,files:r.files.map(i=>({...i,absolutePath:U(o,i.path)}))}}export function publishRunBundle(e,n,t,s={}){const r=I(n),o=s.writePublicationReceipt??Pe;if(!t)return o(e,r,{status:"not_configured",required:!1,target:null,bytesUploaded:0});const i=u.resolve(e,t.directory);try{M(e,i)}catch(d){return o(e,r,{status:"failed",required:t.required,target:i,error:k(d),bytesUploaded:0})}const a=Re(u.basename(e)),f=u.join(i,a),m=u.join(f,r),p=u.join(f,`.${r}.${String(process.pid)}.${String(Date.now())}.tmp`);try{const d=x(e,r);if(c.mkdirSync(f,{recursive:!0}),M(e,f),c.existsSync(m)){const h=c.lstatSync(m);if(h.isSymbolicLink()||!h.isDirectory())throw new Error("Publication target must be a regular bundle directory");M(e,m);const y=V(d,r);if(!y.ok)throw new Error(`Source run bundle failed integrity verification: ${y.errors.join("; ")}`);const v=V(m,r);if(!v.ok)throw new Error(`Publication target failed integrity verification: ${v.errors.join("; ")}`);if(y.manifestDigest!==v.manifestDigest)throw new Error("Publication target already exists with a different manifest digest");return o(e,r,{status:"complete",required:t.required,target:m,manifestDigest:y.manifestDigest,reused:!0,bundleBytes:ne(d),bytesUploaded:0})}const R=ne(d);c.cpSync(d,p,{recursive:!0,preserveTimestamps:!0});const g=V(p,r);if(!g.ok)throw new Error(g.errors.join("; "));return c.renameSync(p,m),o(e,r,{status:"complete",required:t.required,target:m,manifestDigest:g.manifestDigest,reused:!1,bundleBytes:R,bytesUploaded:R})}catch(d){return c.rmSync(p,{recursive:!0,force:!0}),o(e,r,{status:"failed",required:t.required,target:m,error:k(d),bytesUploaded:0})}}export function applyRunBundleRetention(e,n,t={},s={}){const r=I(n),o=s.removeObject??ce,i=s.removeRunBundle??Ne,a=t.maxRuns??null,f=t.maxAgeDays??null,m=new Set(t.preserveSuites||[]),p=ie(e),d=new Set([r]);for(const l of p)Oe(l.artifact,m)&&d.add(l.runId);const R=new Set(a?p.slice(0,a).map(l=>l.runId):p.map(l=>l.runId)),g=f==null?null:Date.now()-f*24*60*60*1e3,h=[];for(const l of p){if(d.has(l.runId))continue;const S=g!=null&&Date.parse(l.finishedAt||l.startedAt||"")<g,w=!R.has(l.runId);if(!S&&!w)continue;const B=[];S&&B.push("max_age"),w&&B.push("max_runs"),h.push({path:u.join(q(e),ge,l.runId),reasons:B,runId:l.runId})}const y=new Map(p.map(l=>[l.runId,l.manifest.files.filter(S=>S.path.startsWith("files/")).map(S=>S.sha256)])),v=oe(e),j=[],b=[];for(const l of h){let S=null;try{i(l.path)}catch(B){S=H(B)}const w=!c.existsSync(l.path);!w&&S===null&&(S="Run bundle still exists after removal completed"),w&&j.push({reasons:l.reasons,runId:l.runId}),S!==null&&b.push({...l,error:S,removed:w})}const T=new Set(j.map(l=>l.runId)),E=new Set(p.filter(l=>!T.has(l.runId)).flatMap(l=>y.get(l.runId)??[])),_=ae(v,E,o),F=new Date().toISOString();return Ve({currentRunId:r,decidedAt:F,garbageCollection:_,policy:{maxRuns:a,maxAgeDays:f,preserveSuites:[...m].sort()},protectedRunIds:[...d].sort(),removed:j,removalErrors:b})}function Ee({resultsDir:e,tempDir:n,sensitiveValues:t}){let s=0,r=0,o=0,i=0,a=0;const f=u.join(e,X,"sha256");c.mkdirSync(f,{recursive:!0});for(const m of K){const p=u.join(e,m);if(c.existsSync(p))for(const d of re(p)){const R=u.relative(p,d),g=c.readFileSync(d),h=Te(g,d,t);h.scanned&&(o+=1),h.replacements>0&&(i+=1,a+=h.replacements);const y=C(h.buffer),v=u.join(f,y);c.existsSync(v)?r+=h.buffer.length:(je(v,h.buffer),c.chmodSync(v,292),s+=h.buffer.length);const j=u.join(n,"files",m,R);c.mkdirSync(u.dirname(j),{recursive:!0});try{c.linkSync(v,j)}catch{c.copyFileSync(v,j)}}}return{objectBytesWritten:s,deduplicatedBytes:r,redaction:{policyVersion:1,status:"applied",textualFilesScanned:o,filesRedacted:i,replacements:a,sensitiveEnvironmentKeys:t.map(m=>m.key).sort(),valuesStored:!1}}}function Be(e,n,t){const s=new Map(K.map(o=>[`.testkit/results/${o}/`,`${P(n,`files/${o}`)}/`])),r=L(e,s);return ke(Ae(r),t)}function Ae(e){if(!$(e))return e;const n={...e};if($(n.product)){const t={...n.product};delete t.directory,n.product=t}if($(n.git)){const t={...n.git};delete t.remoteUrl,delete t.repoRoot,delete t.worktreePath,n.git=t}return delete n.host,n}function L(e,n){if(Array.isArray(e))return e.map(s=>L(s,n));if($(e))return Object.fromEntries(Object.entries(e).map(([s,r])=>[s,L(r,n)]));if(typeof e!="string")return e;let t=e;for(const[s,r]of n)t.startsWith(s)&&(t=`${r}${t.slice(s.length)}`);return t}function ke(e,n){let t=0;function s(r){if(Array.isArray(r))return r.map(s);if($(r))return Object.fromEntries(Object.entries(r).map(([i,a])=>[i,s(a)]));if(typeof r!="string")return r;let o=G(r);o!==r&&(t+=1);for(const i of n){if(!o.includes(i.value))continue;const a=o.split(i.value);t+=a.length-1,o=a.join("<redacted>")}return o}return{artifact:s(e),replacements:t}}function $e(e,n,t,s){const r=D(n.run.fileNames),o=[...new Set([...Ie,...r])].map(i=>ze(e,i)).filter(i=>i!==null);return{schemaVersion:pe,runId:t,ci:be(s.env??process.env),source:{git:n.git??null,files:o},runtime:{controller:{testkitVersion:A(n.run.testkitVersion),node:process.versions.node,platform:process.platform,architecture:process.arch},runtimeStats:ye(n.run.runtimeStats)},scenario:{types:D(n.run.types),suiteSelectors:D(n.run.suiteSelectors),fileNames:r,serviceFilter:A(n.run.serviceFilter),planningDigest:C(Buffer.from(W(n.planning??null))),declaredEnvironmentKeys:[...new Set(s.declaredEnvironmentKeys??[])].sort()},redaction:s.redaction}}function xe(e){return re(e).map(n=>({path:Z(u.relative(e,n)),bytes:c.statSync(n).size,sha256:z(n)})).sort((n,t)=>n.path.localeCompare(t.path))}function V(e,n){const t=u.join(e,"manifest.json"),s=Y(O(t),t,n);le(O(u.join(e,"run.json")),u.join(e,"run.json"),n);const r=[];for(const i of s.files){let a;try{a=U(e,i.path)}catch(m){r.push(k(m));continue}if(!c.existsSync(a)){r.push(`missing ${i.path}`);continue}const f=c.lstatSync(a);if(f.isSymbolicLink()){r.push(`symbolic link not allowed ${i.path}`);continue}if(!f.isFile()){r.push(`not a regular file ${i.path}`);continue}f.size!==i.bytes&&r.push(`size mismatch ${i.path}`),z(a)!==i.sha256&&r.push(`checksum mismatch ${i.path}`)}C(Buffer.from(W(s.files)))!==s.digest&&r.push("manifest digest mismatch"),s.storage.fileCount!==s.files.length&&r.push("manifest file count mismatch");for(const i of["run.json","provenance.json"])s.files.some(a=>a.path===i)||r.push(`manifest must include ${i}`);if(r.length===0){const i=U(e,"provenance.json");ue(O(i),i,n)}return{runId:n,checkedFiles:s.files.length,storage:s.storage,ok:r.length===0,errors:r,manifestDigest:s.digest}}function Pe(e,n,t){const s={schemaVersion:1,runId:n,recordedAt:new Date().toISOString(),...t};return N(u.join(q(e),"publications",`${n}.json`),s),s}function Oe(e,n){return n.size===0?!1:ee(e.services).some(t=>ee(t.suites).some(s=>{const r=A(t.name)??"",o=A(s.name)??"",i=A(s.testType)??"";return[o,`${i}:${o}`,`${r}/${i}:${o}`].some(f=>n.has(f))}))}function Ce(e){const n=new Set;for(const t of listRunBundles(e)){const s=loadRunBundleManifest(e,t.runId);for(const r of s.files)r.path.startsWith("files/")&&n.add(r.sha256)}return ae(oe(e),n,ce)}function oe(e){const n=u.join(q(e),X,"sha256");return c.existsSync(n)?c.readdirSync(n,{withFileTypes:!0}).filter(t=>t.isFile()).map(t=>{const s=u.join(n,t.name);return{bytes:c.statSync(s).size,digest:t.name,path:s}}):[]}function ae(e,n,t){const s=[];let r=0,o=0;for(const i of e){if(n.has(i.digest))continue;let a=null;try{t(i.path)}catch(m){a=H(m)}!c.existsSync(i.path)?(r+=1,o+=i.bytes):a===null&&(a="Object still exists after removal completed"),a!==null&&s.push({error:a,path:i.path})}return{failures:s,reclaimedBytes:o,removedObjects:r,status:s.length===0?"complete":"failed"}}function Ne(e){c.rmSync(e,{recursive:!0,force:!0})}function ce(e){c.rmSync(e,{force:!0})}function Ve({currentRunId:e,decidedAt:n,garbageCollection:t,policy:s,protectedRunIds:r,removed:o,removalErrors:i}){const a=[...i.map(f=>`remove ${f.runId}: ${f.error}`),...t.failures.map(f=>`garbage collect ${f.path}: ${f.error}`)];return{currentRunId:e,decidedAt:n,garbageCollection:t,policy:s,protectedRunIds:r,removed:o,removalErrors:i,schemaVersion:he,status:a.length===0?"complete":"failed",...a.length===0?{}:{error:a.join("; ")}}}function Te(e,n,t){if(!Fe(e,n)){for(const i of t)if(e.includes(Buffer.from(i.value)))throw new Error(`Refusing to archive binary file containing sensitive environment value: ${n}`);return{buffer:e,scanned:!1,replacements:0}}const s=e.toString("utf8");let r=G(s),o=r===s?0:1;for(const i of t){if(!r.includes(i.value))continue;const a=r.split(i.value);o+=a.length-1,r=a.join("<redacted>")}return{buffer:Buffer.from(r),scanned:!0,replacements:o}}function _e(e){const n=[];for(const[t,s]of Object.entries(e))we.test(t)&&typeof s=="string"&&s.length>=8&&n.push({key:t,value:s});return n}function Fe(e,n){if(e.includes(0))return!1;const t=u.extname(n).toLowerCase();return![".zip",".gz",".png",".jpg",".jpeg",".webm",".pdf"].includes(t)}function ze(e,n){const t=u.resolve(e,n);return!t.startsWith(`${u.resolve(e)}${u.sep}`)||!c.existsSync(t)||!c.statSync(t).isFile()?null:{path:Z(u.relative(e,t)),bytes:c.statSync(t).size,sha256:z(t)}}function De(e){const n=[];for(const t of[...K.map(s=>u.join(e,s)),u.join(e,"live.json")])try{c.rmSync(t,{recursive:!0,force:!0})}catch(s){n.push(`${t}: ${H(s)}`)}return n.length===0?{status:"complete"}:{error:n.join("; "),status:"failed"}}function H(e){try{return k(e)}catch{return"Unknown post-seal archive operation failure"}}
@@ -1 +1 @@
1
- import{isEngineId as w,isPublicTestType as q,isRunnerProfileId as S}from"../domain/identifiers.js";import{isEngineProfileIdentity as T}from"../engines/catalog.js";import{requireObjectRecord as r,requireNumber as m,requireStringArray as B,optionalBoolean as y,optionalNonNegativeNumber as p,isObjectRecord as E,requireUnknownArray as h,requireString as c,requireResultsReferencePath as k,optionalStringValue as s,optionalNullableStringValue as a,optionalNumberOrStringValue as L,optionalNullableNumberValue as v}from"./read-fields.js";export function readRunDocument(n,t){const e=r(n,t);if(e.schemaVersion!==19)throw new Error(`${t}.schemaVersion must be 19`);const i=M(e.run,`${t}.run`),o=f(e.services,`${t}.services`,P)??[];e.archive!==void 0&&r(e.archive,`${t}.archive`),e.finalizationFailure!==void 0&&X(e.finalizationFailure,`${t}.finalizationFailure`),s(e.generatedAt,`${t}.generatedAt`);const d=K(e.summary,`${t}.summary`),u=e.logs===void 0?void 0:x(e.logs,`${t}.logs`),g=e.setup===void 0?void 0:V(e.setup,`${t}.setup`);return{...e,schemaVersion:19,...d===void 0?{}:{summary:d},run:i,services:o,...u===void 0?{}:{logs:u},...g===void 0?{}:{setup:g}}}function M(n,t){const e=r(n,t);return v(e.durationMs,`${t}.durationMs`),f(e.types,`${t}.types`,c),{...e,finishedAt:a(e.finishedAt,`${t}.finishedAt`)??null,id:a(e.id,`${t}.id`)??null,startedAt:c(e.startedAt,`${t}.startedAt`),status:c(e.status,`${t}.status`)}}function x(n,t){const e=r(n,t),i=f(e.services,`${t}.services`,H),o=f(e.setup,`${t}.setup`,F);return{...e,...i===void 0?{}:{services:i},...o===void 0?{}:{setup:o}}}function V(n,t){const e=r(n,t),i=f(e.operations,`${t}.operations`,J);return{...e,...i===void 0?{}:{operations:i}}}function P(n,t){const e=r(n,t),i=f(e.suites,`${t}.suites`,D)??[];return{...e,name:c(e.name,`${t}.name`),suites:i}}function D(n,t){const e=r(n,t);if(e.testType!=="scenario"&&!q(e.testType))throw new Error(`${t}.testType must be a public test type`);if(!w(e.engineId))throw new Error(`${t}.engineId must be a supported engine id`);if(!S(e.profileId))throw new Error(`${t}.profileId must be a supported runner profile id`);if(!(e.testType==="scenario"?e.engineId==="k6"&&e.profileId==="k6-http":T(e)))throw new Error(`${t}.profileId does not match ${e.engineId}/${e.testType}`);const o=f(e.files,`${t}.files`,O);return y(e.failed,`${t}.failed`),A(e.diagnostics,`${t}.diagnostics`),p(e.completedFileCount,`${t}.completedFileCount`),p(e.failedFileCount,`${t}.failedFileCount`),p(e.fileCount,`${t}.fileCount`),p(e.passedFileCount,`${t}.passedFileCount`),p(e.skippedFileCount,`${t}.skippedFileCount`),{...e,files:o,name:c(e.name,`${t}.name`),testType:e.testType,engineId:e.engineId,profileId:e.profileId}}function O(n,t){const e=r(n,t),i=f(e.artifacts,`${t}.artifacts`,j),o=f(e.failureDetails,`${t}.failureDetails`,z);return y(e.blockedByRuntime,`${t}.blockedByRuntime`),y(e.failed,`${t}.failed`),y(e.infraFailed,`${t}.infraFailed`),a(e.reason,`${t}.reason`),a(e.runtimeIsolation,`${t}.runtimeIsolation`),e.runtimeBatch!=null&&Q(e.runtimeBatch,`${t}.runtimeBatch`),f(e.runtimeResets,`${t}.runtimeResets`,W),{...e,error:a(e.error,`${t}.error`),artifacts:i,durationMs:v(e.durationMs,`${t}.durationMs`),failureDetails:o,path:c(e.path,`${t}.path`),status:c(e.status,`${t}.status`)}}function j(n,t){const e=r(n,t);return{...e,contentType:a(e.contentType,`${t}.contentType`),kind:a(e.kind,`${t}.kind`),summary:a(e.summary,`${t}.summary`),name:c(e.name,`${t}.name`),path:k(e.path,`${t}.path`)}}function z(n,t){const e=r(n,t),i=e.request===void 0?void 0:U(e.request,`${t}.request`),o=e.response===void 0?void 0:G(e.response,`${t}.response`),d=e.location===void 0?void 0:r(e.location,`${t}.location`);return d!==void 0&&(s(d.path,`${t}.location.path`),d.line!==void 0&&m(d.line,`${t}.location.line`),d.column!==void 0&&m(d.column,`${t}.location.column`)),{...e,key:s(e.key,`${t}.key`),kind:s(e.kind,`${t}.kind`),message:s(e.message,`${t}.message`),stack:s(e.stack,`${t}.stack`),location:d,request:i,response:o,title:c(e.title,`${t}.title`)}}function U(n,t){const e=r(n,t);return{...e,method:s(e.method,`${t}.method`),path:s(e.path,`${t}.path`),requestId:s(e.requestId,`${t}.requestId`)}}function G(n,t){const e=r(n,t);return{...e,bodyPreview:s(e.bodyPreview,`${t}.bodyPreview`),contentType:s(e.contentType,`${t}.contentType`),status:L(e.status,`${t}.status`)}}function H(n,t){const e=r(n,t);return{...e,path:k(e.path,`${t}.path`),runtimeLabel:c(e.runtimeLabel,`${t}.runtimeLabel`),serviceName:c(e.serviceName,`${t}.serviceName`)}}function J(n,t){const e=r(n,t),i=e.logRef==null?e.logRef:F(e.logRef,`${t}.logRef`);return{...e,durationMs:v(e.durationMs,`${t}.durationMs`),error:a(e.error,`${t}.error`),summary:a(e.summary,`${t}.summary`),logRef:i,serviceName:c(e.serviceName,`${t}.serviceName`),stage:c(e.stage,`${t}.stage`),startedAt:s(e.startedAt,`${t}.startedAt`),status:c(e.status,`${t}.status`)}}function F(n,t){const e=r(n,t);return s(e.runtimeLabel,`${t}.runtimeLabel`),s(e.serviceName,`${t}.serviceName`),s(e.stage,`${t}.stage`),{...e,path:k(e.path,`${t}.path`)}}function K(n,t){if(n===void 0)return;const e=r(n,t),i=e.files===void 0?{}:r(e.files,`${t}.files`),o=e.services===void 0?{}:r(e.services,`${t}.services`),d=e.suites===void 0?{}:r(e.suites,`${t}.suites`),u=(g,I,N)=>{const $=g[I];if($===void 0)return 0;if(typeof $!="number"||!Number.isFinite($)||$<0)throw new Error(`${t}.${N}.${I} must be a non-negative finite number`);return $};return A(e.diagnostics,`${t}.diagnostics`),{...e,files:{blockedByRuntime:u(i,"blockedByRuntime","files"),cancelled:u(i,"cancelled","files"),failed:u(i,"failed","files"),infraFailed:u(i,"infraFailed","files"),interrupted:u(i,"interrupted","files"),notRun:u(i,"notRun","files"),passed:u(i,"passed","files"),skipped:u(i,"skipped","files"),total:u(i,"total","files")},services:{blockedByRuntime:u(o,"blockedByRuntime","services"),failed:u(o,"failed","services"),infraFailed:u(o,"infraFailed","services"),passed:u(o,"passed","services"),skipped:u(o,"skipped","services"),total:u(o,"total","services")},suites:{completed:u(d,"completed","suites"),failed:u(d,"failed","suites"),passed:u(d,"passed","suites"),skipped:u(d,"skipped","suites"),total:u(d,"total","suites")}}}function A(n,t){if(n!==void 0)return h(n,t).map((e,i)=>{const o=`${t}[${String(i)}]`,d=r(e,o);return{...d,code:c(d.code,`${o}.code`),message:c(d.message,`${o}.message`),service:s(d.service,`${o}.service`),suite:s(d.suite,`${o}.suite`)}})}function Q(n,t){const e=r(n,t);return{...e,id:s(e.id,`${t}.id`),fileCount:p(e.fileCount,`${t}.fileCount`),index:p(e.index,`${t}.index`),files:e.files===void 0?void 0:B(e.files,`${t}.files`)}}function W(n,t){const e=r(n,t);return{...e,position:s(e.position,`${t}.position`),reason:s(e.reason,`${t}.reason`),runtimeIsolation:s(e.runtimeIsolation,`${t}.runtimeIsolation`),trigger:s(e.trigger,`${t}.trigger`)}}function X(n,t){const e=r(n,t),i=e.operation===void 0?void 0:r(e.operation,`${t}.operation`);return{...e,name:c(e.name,`${t}.name`),message:c(e.message,`${t}.message`),code:s(e.code,`${t}.code`),stack:s(e.stack,`${t}.stack`),phase:s(e.phase,`${t}.phase`),exitCode:e.exitCode===void 0?void 0:m(e.exitCode,`${t}.exitCode`),status:e.status===void 0?void 0:m(e.status,`${t}.status`),diagnostics:e.diagnostics===void 0?void 0:R(e.diagnostics,`${t}.diagnostics`),operation:i===void 0?void 0:{...i,id:s(i.id,`${t}.operation.id`),source:c(i.source,`${t}.operation.source`),name:c(i.name,`${t}.operation.name`),kind:c(i.kind,`${t}.operation.kind`),summary:s(i.summary,`${t}.operation.summary`),status:s(i.status,`${t}.operation.status`),error:s(i.error,`${t}.operation.error`),scope:R(i.scope,`${t}.operation.scope`)}}}function R(n,t){return Object.fromEntries(Object.entries(r(n,t)).map(([e,i])=>[e,C(i,`${t}.${e}`)]))}function C(n,t){return n===null||typeof n=="string"||typeof n=="boolean"?n:Array.isArray(n)?h(n,t).map((e,i)=>C(e,`${t}[${String(i)}]`)):E(n)?R(n,t):m(n,t)}function f(n,t,e){return n===void 0?void 0:h(n,t).map((i,o)=>e(i,`${t}[${String(o)}]`))}
1
+ import{isEngineId as w,isPublicTestType as q,isRunnerProfileId as S}from"../domain/identifiers.js";import{isEngineProfileIdentity as T}from"../engines/definitions.js";import{requireObjectRecord as r,requireNumber as m,requireStringArray as B,optionalBoolean as y,optionalNonNegativeNumber as p,isObjectRecord as E,requireUnknownArray as h,requireString as c,requireResultsReferencePath as k,optionalStringValue as s,optionalNullableStringValue as a,optionalNumberOrStringValue as L,optionalNullableNumberValue as v}from"./read-fields.js";export function readRunDocument(n,t){const e=r(n,t);if(e.schemaVersion!==19)throw new Error(`${t}.schemaVersion must be 19`);const i=M(e.run,`${t}.run`),o=f(e.services,`${t}.services`,P)??[];e.archive!==void 0&&r(e.archive,`${t}.archive`),e.finalizationFailure!==void 0&&X(e.finalizationFailure,`${t}.finalizationFailure`),s(e.generatedAt,`${t}.generatedAt`);const d=K(e.summary,`${t}.summary`),u=e.logs===void 0?void 0:x(e.logs,`${t}.logs`),g=e.setup===void 0?void 0:V(e.setup,`${t}.setup`);return{...e,schemaVersion:19,...d===void 0?{}:{summary:d},run:i,services:o,...u===void 0?{}:{logs:u},...g===void 0?{}:{setup:g}}}function M(n,t){const e=r(n,t);return v(e.durationMs,`${t}.durationMs`),f(e.types,`${t}.types`,c),{...e,finishedAt:a(e.finishedAt,`${t}.finishedAt`)??null,id:a(e.id,`${t}.id`)??null,startedAt:c(e.startedAt,`${t}.startedAt`),status:c(e.status,`${t}.status`)}}function x(n,t){const e=r(n,t),i=f(e.services,`${t}.services`,H),o=f(e.setup,`${t}.setup`,F);return{...e,...i===void 0?{}:{services:i},...o===void 0?{}:{setup:o}}}function V(n,t){const e=r(n,t),i=f(e.operations,`${t}.operations`,J);return{...e,...i===void 0?{}:{operations:i}}}function P(n,t){const e=r(n,t),i=f(e.suites,`${t}.suites`,D)??[];return{...e,name:c(e.name,`${t}.name`),suites:i}}function D(n,t){const e=r(n,t);if(e.testType!=="scenario"&&!q(e.testType))throw new Error(`${t}.testType must be a public test type`);if(!w(e.engineId))throw new Error(`${t}.engineId must be a supported engine id`);if(!S(e.profileId))throw new Error(`${t}.profileId must be a supported runner profile id`);if(!(e.testType==="scenario"?e.engineId==="k6"&&e.profileId==="k6-http":T(e)))throw new Error(`${t}.profileId does not match ${e.engineId}/${e.testType}`);const o=f(e.files,`${t}.files`,O);return y(e.failed,`${t}.failed`),A(e.diagnostics,`${t}.diagnostics`),p(e.completedFileCount,`${t}.completedFileCount`),p(e.failedFileCount,`${t}.failedFileCount`),p(e.fileCount,`${t}.fileCount`),p(e.passedFileCount,`${t}.passedFileCount`),p(e.skippedFileCount,`${t}.skippedFileCount`),{...e,files:o,name:c(e.name,`${t}.name`),testType:e.testType,engineId:e.engineId,profileId:e.profileId}}function O(n,t){const e=r(n,t),i=f(e.artifacts,`${t}.artifacts`,j),o=f(e.failureDetails,`${t}.failureDetails`,z);return y(e.blockedByRuntime,`${t}.blockedByRuntime`),y(e.failed,`${t}.failed`),y(e.infraFailed,`${t}.infraFailed`),a(e.reason,`${t}.reason`),a(e.runtimeIsolation,`${t}.runtimeIsolation`),e.runtimeBatch!=null&&Q(e.runtimeBatch,`${t}.runtimeBatch`),f(e.runtimeResets,`${t}.runtimeResets`,W),{...e,error:a(e.error,`${t}.error`),artifacts:i,durationMs:v(e.durationMs,`${t}.durationMs`),failureDetails:o,path:c(e.path,`${t}.path`),status:c(e.status,`${t}.status`)}}function j(n,t){const e=r(n,t);return{...e,contentType:a(e.contentType,`${t}.contentType`),kind:a(e.kind,`${t}.kind`),summary:a(e.summary,`${t}.summary`),name:c(e.name,`${t}.name`),path:k(e.path,`${t}.path`)}}function z(n,t){const e=r(n,t),i=e.request===void 0?void 0:U(e.request,`${t}.request`),o=e.response===void 0?void 0:G(e.response,`${t}.response`),d=e.location===void 0?void 0:r(e.location,`${t}.location`);return d!==void 0&&(s(d.path,`${t}.location.path`),d.line!==void 0&&m(d.line,`${t}.location.line`),d.column!==void 0&&m(d.column,`${t}.location.column`)),{...e,key:s(e.key,`${t}.key`),kind:s(e.kind,`${t}.kind`),message:s(e.message,`${t}.message`),stack:s(e.stack,`${t}.stack`),location:d,request:i,response:o,title:c(e.title,`${t}.title`)}}function U(n,t){const e=r(n,t);return{...e,method:s(e.method,`${t}.method`),path:s(e.path,`${t}.path`),requestId:s(e.requestId,`${t}.requestId`)}}function G(n,t){const e=r(n,t);return{...e,bodyPreview:s(e.bodyPreview,`${t}.bodyPreview`),contentType:s(e.contentType,`${t}.contentType`),status:L(e.status,`${t}.status`)}}function H(n,t){const e=r(n,t);return{...e,path:k(e.path,`${t}.path`),runtimeLabel:c(e.runtimeLabel,`${t}.runtimeLabel`),serviceName:c(e.serviceName,`${t}.serviceName`)}}function J(n,t){const e=r(n,t),i=e.logRef==null?e.logRef:F(e.logRef,`${t}.logRef`);return{...e,durationMs:v(e.durationMs,`${t}.durationMs`),error:a(e.error,`${t}.error`),summary:a(e.summary,`${t}.summary`),logRef:i,serviceName:c(e.serviceName,`${t}.serviceName`),stage:c(e.stage,`${t}.stage`),startedAt:s(e.startedAt,`${t}.startedAt`),status:c(e.status,`${t}.status`)}}function F(n,t){const e=r(n,t);return s(e.runtimeLabel,`${t}.runtimeLabel`),s(e.serviceName,`${t}.serviceName`),s(e.stage,`${t}.stage`),{...e,path:k(e.path,`${t}.path`)}}function K(n,t){if(n===void 0)return;const e=r(n,t),i=e.files===void 0?{}:r(e.files,`${t}.files`),o=e.services===void 0?{}:r(e.services,`${t}.services`),d=e.suites===void 0?{}:r(e.suites,`${t}.suites`),u=(g,I,N)=>{const $=g[I];if($===void 0)return 0;if(typeof $!="number"||!Number.isFinite($)||$<0)throw new Error(`${t}.${N}.${I} must be a non-negative finite number`);return $};return A(e.diagnostics,`${t}.diagnostics`),{...e,files:{blockedByRuntime:u(i,"blockedByRuntime","files"),cancelled:u(i,"cancelled","files"),failed:u(i,"failed","files"),infraFailed:u(i,"infraFailed","files"),interrupted:u(i,"interrupted","files"),notRun:u(i,"notRun","files"),passed:u(i,"passed","files"),skipped:u(i,"skipped","files"),total:u(i,"total","files")},services:{blockedByRuntime:u(o,"blockedByRuntime","services"),failed:u(o,"failed","services"),infraFailed:u(o,"infraFailed","services"),passed:u(o,"passed","services"),skipped:u(o,"skipped","services"),total:u(o,"total","services")},suites:{completed:u(d,"completed","suites"),failed:u(d,"failed","suites"),passed:u(d,"passed","suites"),skipped:u(d,"skipped","suites"),total:u(d,"total","suites")}}}function A(n,t){if(n!==void 0)return h(n,t).map((e,i)=>{const o=`${t}[${String(i)}]`,d=r(e,o);return{...d,code:c(d.code,`${o}.code`),message:c(d.message,`${o}.message`),service:s(d.service,`${o}.service`),suite:s(d.suite,`${o}.suite`)}})}function Q(n,t){const e=r(n,t);return{...e,id:s(e.id,`${t}.id`),fileCount:p(e.fileCount,`${t}.fileCount`),index:p(e.index,`${t}.index`),files:e.files===void 0?void 0:B(e.files,`${t}.files`)}}function W(n,t){const e=r(n,t);return{...e,position:s(e.position,`${t}.position`),reason:s(e.reason,`${t}.reason`),runtimeIsolation:s(e.runtimeIsolation,`${t}.runtimeIsolation`),trigger:s(e.trigger,`${t}.trigger`)}}function X(n,t){const e=r(n,t),i=e.operation===void 0?void 0:r(e.operation,`${t}.operation`);return{...e,name:c(e.name,`${t}.name`),message:c(e.message,`${t}.message`),code:s(e.code,`${t}.code`),stack:s(e.stack,`${t}.stack`),phase:s(e.phase,`${t}.phase`),exitCode:e.exitCode===void 0?void 0:m(e.exitCode,`${t}.exitCode`),status:e.status===void 0?void 0:m(e.status,`${t}.status`),diagnostics:e.diagnostics===void 0?void 0:R(e.diagnostics,`${t}.diagnostics`),operation:i===void 0?void 0:{...i,id:s(i.id,`${t}.operation.id`),source:c(i.source,`${t}.operation.source`),name:c(i.name,`${t}.operation.name`),kind:c(i.kind,`${t}.operation.kind`),summary:s(i.summary,`${t}.operation.summary`),status:s(i.status,`${t}.operation.status`),error:s(i.error,`${t}.operation.error`),scope:R(i.scope,`${t}.operation.scope`)}}}function R(n,t){return Object.fromEntries(Object.entries(r(n,t)).map(([e,i])=>[e,C(i,`${t}.${e}`)]))}function C(n,t){return n===null||typeof n=="string"||typeof n=="boolean"?n:Array.isArray(n)?h(n,t).map((e,i)=>C(e,`${t}[${String(i)}]`)):E(n)?R(n,t):m(n,t)}function f(n,t,e){return n===void 0?void 0:h(n,t).map((i,o)=>e(i,`${t}[${String(o)}]`))}
@@ -24,5 +24,5 @@ export declare function loadLatestRunArtifact(productDir: string): RunArtifactDo
24
24
  export declare function loadCurrentRunArtifact(productDir: string): RunArtifactDocument;
25
25
  export declare function loadRunArtifact(productDir: string, runId?: null | string): RunArtifactDocument;
26
26
  export declare function resolveFileSubject(runArtifact: RunArtifactDocument, selector?: null | string, serviceFilter?: null | string): RunArtifactFileSubject;
27
- export declare function collectArtifactEntries(productDir: string, runArtifact: RunArtifactDocument, selector?: null | string, serviceFilter?: null | string): CollectedArtifactEntry[];
27
+ export declare function collectArtifactEntries(productDir: string, runArtifact: RunArtifactDocument, selector?: null | string, serviceFilter?: null | string, kindFilter?: string, includePayload?: boolean): CollectedArtifactEntry[];
28
28
  export {};
@@ -1 +1 @@
1
- import u from"fs";import l from"path";import{normalizeEngineJsonValue as w}from"../engines/api/index.js";import{readJsonFile as m,normalizePath as $}from"./storage.js";import{loadRunBundleArtifact as v}from"./bundles.js";import{readRunDocument as d}from"./read-run.js";import{requireObjectRecord as k,requireResultsReferencePath as P,optionalStringValue as h,optionalNullableStringValue as c}from"./read-fields.js";export function loadLatestRunArtifact(r){const t=l.join(r,".testkit","results","latest.json");if(!u.existsSync(t))throw new Error(`No run artifact found at ${l.relative(r,t)}`);return d(m(t),t)}export function loadCurrentRunArtifact(r){const t=l.join(r,".testkit","results","live.json");return u.existsSync(t)?d(m(t),t):loadLatestRunArtifact(r)}export function loadRunArtifact(r,t=null){return t?v(r,t):loadLatestRunArtifact(r)}export function resolveFileSubject(r,t=null,e=null){const n=p(r,e);if(n.length===0)throw new Error("No file results were found in the latest run artifact.");if(!t)return n.find(s=>s.file.status==="failed")||n[0];const i=$(t),o=n.filter(s=>s.file.path===i);if(o.length===1)return o[0];if(o.length>1)throw new Error(`Multiple files matched "${t}". Re-run with --service to disambiguate.`);const a=n.filter(s=>s.file.path.endsWith(`/${i}`));if(a.length===1)return a[0];throw a.length>1?new Error(`Multiple files matched "${t}". Re-run with --service to disambiguate.`):new Error(`No file matched "${t}".`)}export function collectArtifactEntries(r,t,e=null,n=null){const i=e?[resolveFileSubject(t,e,n)]:p(t,n),o=[];for(const a of i)for(const s of a.file.artifacts||[]){const f=A(r,s.path,`artifact "${s.name}" path`),x=u.existsSync(f)?S(f,s):null;o.push({...a,artifactRef:s,absolutePath:f,payload:x})}return o}function S(r,t){const e=u.readFileSync(r,"utf8"),n=t.contentType||"";if(n==="text/markdown"||n==="text/plain")return{contentType:n,kind:t.kind||null,name:t.name||null,summary:t.summary||null,text:e};try{const i=JSON.parse(e);return g(i,r)}catch{return{contentType:n||"text/plain",kind:t.kind||null,name:t.name||null,summary:t.summary||null,text:e}}}function p(r,t=null){const e=[];for(const n of r.services)if(!(t&&n.name!==t))for(const i of n.suites)for(const o of i.files||[])e.push({service:n,suite:i,file:o});return e}function g(r,t){const e=k(r,t),n=h(e.emittedAt,`${t}.emittedAt`);return{...e,contentType:c(e.contentType,`${t}.contentType`),...e.data===void 0?{}:{data:w(e.data,`${t}.data`)},...n===void 0?{}:{emittedAt:n},kind:c(e.kind,`${t}.kind`),name:c(e.name,`${t}.name`),summary:c(e.summary,`${t}.summary`),text:h(e.text,`${t}.text`)}}function A(r,t,e){const n=P(t,e),i=l.resolve(r,".testkit","results"),o=l.resolve(r,n);if(y(i,o,e),u.existsSync(o)){const a=u.realpathSync(i),s=u.realpathSync(o);y(a,s,e)}return o}function y(r,t,e){const n=l.relative(r,t);if(n.length===0||n===".."||n.startsWith(`..${l.sep}`)||l.isAbsolute(n))throw new Error(`${e} must resolve within .testkit/results`)}
1
+ import{isWorkObserved as k,recordWork as h}from"../kernel/work-metrics.js";import a from"fs";import l from"path";import{normalizeEngineJsonValue as S}from"../engines/api/index.js";import{readJsonFile as p,normalizePath as g}from"./storage.js";import{loadRunBundleArtifact as P}from"./bundles.js";import{readRunDocument as y}from"./read-run.js";import{requireObjectRecord as R,requireResultsReferencePath as A,optionalStringValue as x,optionalNullableStringValue as f}from"./read-fields.js";export function loadLatestRunArtifact(r){const t=l.join(r,".testkit","results","latest.json");if(!a.existsSync(t))throw new Error(`No run artifact found at ${l.relative(r,t)}`);return y(p(t),t)}export function loadCurrentRunArtifact(r){const t=l.join(r,".testkit","results","live.json");return a.existsSync(t)?y(p(t),t):loadLatestRunArtifact(r)}export function loadRunArtifact(r,t=null){return t?P(r,t):loadLatestRunArtifact(r)}export function resolveFileSubject(r,t=null,e=null){const n=w(r,e);if(n.length===0)throw new Error("No file results were found in the latest run artifact.");if(!t)return n.find(s=>s.file.status==="failed")||n[0];const o=g(t),i=n.filter(s=>s.file.path===o);if(i.length===1)return i[0];if(i.length>1)throw new Error(`Multiple files matched "${t}". Re-run with --service to disambiguate.`);const u=n.filter(s=>s.file.path.endsWith(`/${o}`));if(u.length===1)return u[0];throw u.length>1?new Error(`Multiple files matched "${t}". Re-run with --service to disambiguate.`):new Error(`No file matched "${t}".`)}export function collectArtifactEntries(r,t,e=null,n=null,o,i=!0){const u=e?[resolveFileSubject(t,e,n)]:w(t,n),s=[];for(const d of u)for(const c of d.file.artifacts||[]){if(o&&c.kind!==o)continue;const m=T(r,c.path,`artifact "${c.name}" path`),$=i&&a.existsSync(m)?E(m,c):null;s.push({...d,artifactRef:c,absolutePath:m,payload:$})}return s}function E(r,t){let e;if(k()){const o=a.readFileSync(r);h("artifacts.payloadFilesRead",1),h("artifacts.payloadBytesRead",o.byteLength),e=o.toString("utf8")}else e=a.readFileSync(r,"utf8");const n=t.contentType||"";if(n==="text/markdown"||n==="text/plain")return{contentType:n,kind:t.kind||null,name:t.name||null,summary:t.summary||null,text:e};try{const o=JSON.parse(e);return j(o,r)}catch{return{contentType:n||"text/plain",kind:t.kind||null,name:t.name||null,summary:t.summary||null,text:e}}}function w(r,t=null){const e=[];for(const n of r.services)if(!(t&&n.name!==t))for(const o of n.suites)for(const i of o.files||[])e.push({service:n,suite:o,file:i});return e}function j(r,t){const e=R(r,t),n=x(e.emittedAt,`${t}.emittedAt`);return{...e,contentType:f(e.contentType,`${t}.contentType`),...e.data===void 0?{}:{data:S(e.data,`${t}.data`)},...n===void 0?{}:{emittedAt:n},kind:f(e.kind,`${t}.kind`),name:f(e.name,`${t}.name`),summary:f(e.summary,`${t}.summary`),text:x(e.text,`${t}.text`)}}function T(r,t,e){const n=A(t,e),o=l.resolve(r,".testkit","results"),i=l.resolve(r,n);if(v(o,i,e),a.existsSync(i)){const u=a.realpathSync(o),s=a.realpathSync(i);v(u,s,e)}return i}function v(r,t,e){const n=l.relative(r,t);if(n.length===0||n===".."||n.startsWith(`..${l.sep}`)||l.isAbsolute(n))throw new Error(`${e} must resolve within .testkit/results`)}
@@ -1,2 +1,2 @@
1
- import{isPathWithin as y}from"../process/files/paths.js";import S from"crypto";import i from"fs";import o from"path";import{RUNS_DIRNAME as p}from"./contracts.js";import{isObjectRecord as h,requireManifestRelativePath as x}from"./read-fields.js";export{isObjectRecord}from"./read-fields.js";export function readJsonFile(r){try{return JSON.parse(i.readFileSync(r,"utf8"))}catch(t){throw new Error(`Invalid JSON in ${r}: ${errorMessage(t)}`,{cause:t})}}export function runIncludesFile(r,t){const e=normalizePath(t);return r.services.some(n=>n.suites.some(u=>u.files?.some(s=>s.path===e||s.path.endsWith(e))))}export function requireRunId(r){const t=r.run.id==null?null:normalizeRunId(r.run.id);if(t===null)throw new Error("Cannot seal a run bundle without a run id");return t}export function normalizeRunId(r){const t=typeof r=="string"?r:"";if(!t||t==="."||t===".."||t.includes("\0")||t.includes("/")||t.includes("\\"))throw new Error(`Invalid Testkit run id: ${t||"<empty>"}`);return t}export function walkFiles(r){if(!i.existsSync(r))return[];const t=[];for(const e of i.readdirSync(r,{withFileTypes:!0})){const n=o.join(r,e.name);e.isDirectory()?t.push(...walkFiles(n)):e.isFile()&&t.push(n)}return t}export function treeBytes(r){return walkFiles(r).reduce((t,e)=>t+i.statSync(e).size,0)}export function writeBufferAtomic(r,t){i.mkdirSync(o.dirname(r),{recursive:!0});const e=`${r}.${String(process.pid)}.${String(Date.now())}.tmp`;i.writeFileSync(e,t);try{i.renameSync(e,r)}catch(n){if(i.rmSync(e,{force:!0}),!i.existsSync(r))throw n}}export function writeJson(r,t){i.mkdirSync(o.dirname(r),{recursive:!0}),i.writeFileSync(r,`${JSON.stringify(t,null,2)}
2
- `)}export function hashFile(r){return sha256(i.readFileSync(r))}export function sha256(r){return S.createHash("sha256").update(r).digest("hex")}export function stableJson(r){return JSON.stringify(l(r))}function l(r){return Array.isArray(r)?r.map(l):h(r)?Object.fromEntries(Object.keys(r).sort().map(t=>[t,l(r[t])])):r}export function objectRecords(r){return Array.isArray(r)?r.filter(h):[]}export function resolveRunBundleDirectory(r,t){const e=resolveRunsRoot(r,!1),n=o.join(e,t),u=i.lstatSync(n,{throwIfNoEntry:!1});if(u===void 0)return n;if(u.isSymbolicLink())throw new Error(`Immutable run bundle directory must not be a symbolic link: ${t}`);if(!u.isDirectory())throw new Error(`Immutable run bundle path must be a directory: ${t}`);const s=i.realpathSync(e),c=i.realpathSync(n),a=o.relative(s,c);if(a.length===0||a===".."||a.startsWith(`..${o.sep}`)||o.isAbsolute(a))throw new Error(`Immutable run bundle directory must resolve within the runs root: ${t}`);return n}export function resolveRunsRoot(r,t){const e=o.resolve(r),n=resultsRoot(e),u=o.join(n,p);if(t)b(e,n,"Testkit results root"),i.mkdirSync(n,{recursive:!0});else if(!i.existsSync(n))return u;const s=m(n,"Testkit results root");if(f(i.realpathSync(e),s,"Testkit results root must resolve within the product directory"),t)i.mkdirSync(u,{recursive:!0});else if(!i.existsSync(u))return u;const c=m(u,"Immutable run bundles root");return f(s,c,"Immutable run bundles root must resolve within .testkit/results"),u}function b(r,t,e){const n=o.resolve(r),u=i.realpathSync(n);let s=o.resolve(t),c=i.lstatSync(s,{throwIfNoEntry:!1});for(;c===void 0;){const a=o.dirname(s);if(a===s)throw new Error(`${e} has no existing parent directory`);s=a,c=i.lstatSync(s,{throwIfNoEntry:!1})}if(s!==n&&c.isSymbolicLink())throw new Error(`${e} must not traverse a symbolic link`);if(!c.isDirectory())throw new Error(`${e} parent must be a directory`);f(u,i.realpathSync(s),`${e} must resolve within the product directory`,!0)}function m(r,t){const e=i.lstatSync(r,{throwIfNoEntry:!1});if(e===void 0)throw new Error(`${t} does not exist`);if(e.isSymbolicLink())throw new Error(`${t} must not be a symbolic link`);if(!e.isDirectory())throw new Error(`${t} must be a directory`);return i.realpathSync(r)}function f(r,t,e,n=!1){const u=o.relative(r,t);if(!n&&u.length===0||u===".."||u.startsWith(`..${o.sep}`)||o.isAbsolute(u))throw new Error(e)}export function requirePublicationPathOutsideResults(r,t){const e=o.resolve(resultsRoot(r)),n=o.resolve(t);if(y(e,n))throw new Error("Publication directory must be outside .testkit/results");const u=i.realpathSync(e);let s=n,c=i.lstatSync(s,{throwIfNoEntry:!1});for(;c===void 0;){const d=o.dirname(s);if(d===s)throw new Error("Publication directory has no existing parent");s=d,c=i.lstatSync(s,{throwIfNoEntry:!1})}const a=i.realpathSync(s);if(y(u,a))throw new Error("Publication directory must be outside .testkit/results")}export function resolveManifestEntryPath(r,t){const e=o.resolve(r),n=o.resolve(e,x(t,"manifest entry path"));return w(e,n,t),i.existsSync(n)&&w(i.realpathSync(e),i.realpathSync(n),t),n}function w(r,t,e){const n=o.relative(r,t);if(n.length===0||n===".."||n.startsWith(`..${o.sep}`)||o.isAbsolute(n))throw new Error(`manifest path escapes bundle directory: ${e}`)}export function optionalStringArray(r){return Array.isArray(r)?r.filter(t=>typeof t=="string"):[]}export function optionalUnknownArray(r){return Array.isArray(r)?r:[]}export function errorMessage(r){return r instanceof Error?r.message:String(r)}export function bundleRelativePath(r,t){return normalizePath(o.join(".testkit","results",p,r,t))}export function resultsRoot(r){return o.join(r,".testkit","results")}export function normalizePath(r){return r.split(o.sep).join("/")}export function sanitizeSegment(r){return r.toLowerCase().replace(/[^a-z0-9._-]+/gu,"-").replace(/^-+|-+$/gu,"")||"product"}
1
+ import{isWorkObserved as f,recordWork as l}from"../kernel/work-metrics.js";import{isPathWithin as p}from"../process/files/paths.js";import x from"crypto";import i from"fs";import o from"path";import{RUNS_DIRNAME as m}from"./contracts.js";import{isObjectRecord as w,requireManifestRelativePath as v}from"./read-fields.js";export{isObjectRecord}from"./read-fields.js";export function readJsonFile(r){try{let t;if(f()){const n=i.readFileSync(r);l("json.filesRead",1),l("json.bytesRead",n.byteLength),t=n.toString("utf8")}else t=i.readFileSync(r,"utf8");return JSON.parse(t)}catch(t){throw new Error(`Invalid JSON in ${r}: ${errorMessage(t)}`,{cause:t})}}export function runIncludesFile(r,t){const e=normalizePath(t);return r.services.some(n=>n.suites.some(u=>u.files?.some(s=>s.path===e||s.path.endsWith(e))))}export function requireRunId(r){const t=r.run.id==null?null:normalizeRunId(r.run.id);if(t===null)throw new Error("Cannot seal a run bundle without a run id");return t}export function normalizeRunId(r){const t=typeof r=="string"?r:"";if(!t||t==="."||t===".."||t.includes("\0")||t.includes("/")||t.includes("\\"))throw new Error(`Invalid Testkit run id: ${t||"<empty>"}`);return t}export function walkFiles(r){if(!i.existsSync(r))return[];const t=[];for(const e of i.readdirSync(r,{withFileTypes:!0})){const n=o.join(r,e.name);e.isDirectory()?t.push(...walkFiles(n)):e.isFile()&&t.push(n)}return t}export function treeBytes(r){return walkFiles(r).reduce((t,e)=>t+i.statSync(e).size,0)}export function writeBufferAtomic(r,t){i.mkdirSync(o.dirname(r),{recursive:!0});const e=`${r}.${String(process.pid)}.${String(Date.now())}.tmp`;i.writeFileSync(e,t);try{i.renameSync(e,r)}catch(n){if(i.rmSync(e,{force:!0}),!i.existsSync(r))throw n}}export function writeJson(r,t){i.mkdirSync(o.dirname(r),{recursive:!0});const e=JSON.stringify(t,null,2);l("json.serializations"),f()&&l("json.bytesSerialized",Buffer.byteLength(e)),i.writeFileSync(r,`${e}
2
+ `)}export function hashFile(r){const t=i.readFileSync(r);return l("evidence.hashFilesRead",1),l("evidence.hashBytesRead",t.byteLength),sha256(t)}export function sha256(r){return x.createHash("sha256").update(r).digest("hex")}export function stableJson(r){const t=JSON.stringify(d(r));return l("json.canonicalSerializations"),f()&&l("json.canonicalBytesSerialized",Buffer.byteLength(t)),t}function d(r){return Array.isArray(r)?r.map(d):w(r)?Object.fromEntries(Object.keys(r).sort().map(t=>[t,d(r[t])])):r}export function objectRecords(r){return Array.isArray(r)?r.filter(w):[]}export function resolveRunBundleDirectory(r,t){const e=resolveRunsRoot(r,!1),n=o.join(e,t),u=i.lstatSync(n,{throwIfNoEntry:!1});if(u===void 0)return n;if(u.isSymbolicLink())throw new Error(`Immutable run bundle directory must not be a symbolic link: ${t}`);if(!u.isDirectory())throw new Error(`Immutable run bundle path must be a directory: ${t}`);const s=i.realpathSync(e),c=i.realpathSync(n),a=o.relative(s,c);if(a.length===0||a===".."||a.startsWith(`..${o.sep}`)||o.isAbsolute(a))throw new Error(`Immutable run bundle directory must resolve within the runs root: ${t}`);return n}export function resolveRunsRoot(r,t){const e=o.resolve(r),n=resultsRoot(e),u=o.join(n,m);if(t)g(e,n,"Testkit results root"),i.mkdirSync(n,{recursive:!0});else if(!i.existsSync(n))return u;const s=S(n,"Testkit results root");if(y(i.realpathSync(e),s,"Testkit results root must resolve within the product directory"),t)i.mkdirSync(u,{recursive:!0});else if(!i.existsSync(u))return u;const c=S(u,"Immutable run bundles root");return y(s,c,"Immutable run bundles root must resolve within .testkit/results"),u}function g(r,t,e){const n=o.resolve(r),u=i.realpathSync(n);let s=o.resolve(t),c=i.lstatSync(s,{throwIfNoEntry:!1});for(;c===void 0;){const a=o.dirname(s);if(a===s)throw new Error(`${e} has no existing parent directory`);s=a,c=i.lstatSync(s,{throwIfNoEntry:!1})}if(s!==n&&c.isSymbolicLink())throw new Error(`${e} must not traverse a symbolic link`);if(!c.isDirectory())throw new Error(`${e} parent must be a directory`);y(u,i.realpathSync(s),`${e} must resolve within the product directory`,!0)}function S(r,t){const e=i.lstatSync(r,{throwIfNoEntry:!1});if(e===void 0)throw new Error(`${t} does not exist`);if(e.isSymbolicLink())throw new Error(`${t} must not be a symbolic link`);if(!e.isDirectory())throw new Error(`${t} must be a directory`);return i.realpathSync(r)}function y(r,t,e,n=!1){const u=o.relative(r,t);if(!n&&u.length===0||u===".."||u.startsWith(`..${o.sep}`)||o.isAbsolute(u))throw new Error(e)}export function requirePublicationPathOutsideResults(r,t){const e=o.resolve(resultsRoot(r)),n=o.resolve(t);if(p(e,n))throw new Error("Publication directory must be outside .testkit/results");const u=i.realpathSync(e);let s=n,c=i.lstatSync(s,{throwIfNoEntry:!1});for(;c===void 0;){const h=o.dirname(s);if(h===s)throw new Error("Publication directory has no existing parent");s=h,c=i.lstatSync(s,{throwIfNoEntry:!1})}const a=i.realpathSync(s);if(p(u,a))throw new Error("Publication directory must be outside .testkit/results")}export function resolveManifestEntryPath(r,t){const e=o.resolve(r),n=o.resolve(e,v(t,"manifest entry path"));return b(e,n,t),i.existsSync(n)&&b(i.realpathSync(e),i.realpathSync(n),t),n}function b(r,t,e){const n=o.relative(r,t);if(n.length===0||n===".."||n.startsWith(`..${o.sep}`)||o.isAbsolute(n))throw new Error(`manifest path escapes bundle directory: ${e}`)}export function optionalStringArray(r){return Array.isArray(r)?r.filter(t=>typeof t=="string"):[]}export function optionalUnknownArray(r){return Array.isArray(r)?r:[]}export function errorMessage(r){return r instanceof Error?r.message:String(r)}export function bundleRelativePath(r,t){return normalizePath(o.join(".testkit","results",m,r,t))}export function resultsRoot(r){return o.join(r,".testkit","results")}export function normalizePath(r){return r.split(o.sep).join("/")}export function sanitizeSegment(r){return r.toLowerCase().replace(/[^a-z0-9._-]+/gu,"-").replace(/^-+|-+$/gu,"")||"product"}
@@ -1 +1 @@
1
- const g="testkitOperation",m="testkitDiagnostics",c=String;export function attachErrorOperation(t,n){if(!r(t)||!r(n)||s(t,g))return t;const e=y(n);return e!==null&&d(t,g,e),t}export function attachErrorDiagnostics(t,n){if(!r(t)||!r(n))return t;const e=getErrorDiagnostics(t),i={...f(e),...f(n)};return d(t,m,i),t}export function getErrorOperation(t){return r(t)?y(s(t,g)||s(t,"operation")):null}export function getErrorDiagnostics(t){return r(t)?f(s(t,m)||s(t,"diagnostics")):{}}export function errorMessage(t){return t instanceof Error?t.message:c(t)}export function formatError(t){return sanitizeErrorMessage(errorMessage(t))}export function sanitizeErrorMessage(t){return t.replace(/(postgres(?:ql)?:\/\/[^:\s"'@]+:)[^@\s"']+(@)/gi,"$1<redacted>$2").replace(/("password"\s*:\s*")[^"]*(")/gi,"$1<redacted>$2").replace(/('password'\s*:\s*')[^']*(')/gi,"$1<redacted>$2").replace(/(PGPASSWORD=)[^\s'"\\]+/g,"$1<redacted>")}export function errorDetail(t){if(r(t)){const n=s(t,"stack");if(typeof n=="string"&&n.length>0)return n;const e=s(t,"message");if(typeof e=="string"&&e.length>0)return e}return t instanceof Error?t.stack||t.message:c(t)}export function serializeError(t,{includeStack:n=!1}={}){if(t instanceof Error){const e=getErrorOperation(t),i=getErrorDiagnostics(t),o=s(t,"code"),u=s(t,"exitCode"),a=s(t,"status");return{name:t.name||"Error",message:t.message||c(t),...typeof o=="string"?{code:o}:{},...typeof u=="number"&&Number.isInteger(u)?{exitCode:u}:{},...typeof a=="number"&&Number.isInteger(a)?{status:a}:{},...e?{phase:e.name,operation:e}:{},...Object.keys(i).length>0?{diagnostics:i}:{},...n&&typeof t.stack=="string"?{stack:t.stack}:{}}}return{name:"Error",message:c(t)}}function d(t,n,e){Object.defineProperty(t,n,{value:e,configurable:!0,enumerable:!1,writable:!0})}function y(t){if(!r(t))return null;const n=s(t,"id"),e=s(t,"source"),i=s(t,"name"),o=s(t,"kind"),u=s(t,"summary"),a=s(t,"status"),p=s(t,"error");return{...n?{id:c(n)}:{},source:typeof e=="string"?e:"testkit",name:typeof i=="string"&&i?i:"operation",kind:typeof o=="string"&&o?o:"operation",...u?{summary:c(u)}:{},scope:f(s(t,"scope")),...a?{status:c(a)}:{},...p?{error:c(p)}:{}}}function f(t){if(!r(t)||E(t))return{};const n=[];for(const e of Object.keys(t)){const i=s(t,e);i!==void 0&&n.push([e,l(i)])}return Object.fromEntries(n)}function l(t){return t===null||typeof t=="string"||typeof t=="number"||typeof t=="boolean"?t:E(t)?t.map(n=>l(n)):r(t)?f(t):c(t)}function s(t,n){return Reflect.get(t,n)}function E(t){return Array.isArray(t)}function r(t){return t!==null&&typeof t=="object"}
1
+ const g="testkitOperation",m="testkitDiagnostics",c=String;export function attachErrorOperation(t,n){if(!r(t)||!r(n)||s(t,g))return t;const e=y(n);return e!==null&&d(t,g,e),t}export function attachErrorDiagnostics(t,n){if(!r(t)||!r(n))return t;const e=getErrorDiagnostics(t),i={...f(e),...f(n)};return d(t,m,i),t}export function getErrorOperation(t){return r(t)?y(s(t,g)||s(t,"operation")):null}export function getErrorDiagnostics(t){return r(t)?f(s(t,m)||s(t,"diagnostics")):{}}export function errorMessage(t){return t instanceof Error?t.message:c(t)}export function formatError(t){return sanitizeErrorMessage(errorMessage(t))}export function sanitizeErrorMessage(t){return t.replace(/(postgres(?:ql)?:\/\/[^:\s"'@]+:)[^@\s"']+(@)/gi,"$1<redacted>$2").replace(/("password"\s*:\s*")[^"]*(")/gi,"$1<redacted>$2").replace(/('password'\s*:\s*')[^']*(')/gi,"$1<redacted>$2").replace(/(PGPASSWORD=)[^\s'"\\]+/g,"$1<redacted>")}export function errorDetail(t){if(r(t)){const n=s(t,"stack");if(typeof n=="string"&&n.length>0)return n;const e=s(t,"message");if(typeof e=="string"&&e.length>0)return e}return t instanceof Error?t.stack||t.message:c(t)}export function serializeError(t,{includeStack:n=!1}={}){if(t instanceof Error){const e=getErrorOperation(t),i=getErrorDiagnostics(t),o=s(t,"code"),u=s(t,"exitCode"),a=s(t,"status");return{name:t.name||"Error",message:t.message||c(t),...typeof o=="string"?{code:o}:{},...typeof u=="number"&&Number.isInteger(u)?{exitCode:u}:{},...typeof a=="number"&&Number.isInteger(a)?{status:a}:{},...e?{phase:e.name,operation:e}:{},...Object.keys(i).length>0?{diagnostics:i}:{},...n&&typeof t.stack=="string"?{stack:t.stack}:{}}}return{name:"Error",message:c(t)}}function d(t,n,e){Object.defineProperty(t,n,{value:e,configurable:!0,enumerable:!1,writable:!0})}function y(t){if(!r(t))return null;const n=s(t,"id"),e=s(t,"source"),i=s(t,"name"),o=s(t,"kind"),u=s(t,"summary"),a=s(t,"status"),p=s(t,"error");return{...n?{id:c(n)}:{},source:typeof e=="string"?e:"testkit",name:typeof i=="string"&&i?i:"operation",kind:typeof o=="string"&&o?o:"operation",...u?{summary:c(u)}:{},scope:f(s(t,"scope")),...a?{status:c(a)}:{},...p?{error:c(p)}:{}}}function f(t){if(!r(t)||E(t))return{};const n=[];for(const e of Object.keys(t)){const i=s(t,e);i!==void 0&&n.push([e,l(i)])}return Object.fromEntries(n)}function l(t){return t===null?null:typeof t=="string"||typeof t=="number"||typeof t=="boolean"?t:E(t)?t.map(n=>l(n)):r(t)?f(t):c(t)}function s(t,n){return Reflect.get(t,n)}function E(t){return Array.isArray(t)}function r(t){return t!==null&&typeof t=="object"}
@@ -0,0 +1,6 @@
1
+ /** Call-through observation of work performed by Testkit, independent of time. */
2
+ export type WorkObserver = (metric: string, amount: number) => void;
3
+ export declare function isWorkObserved(): boolean;
4
+ export declare function recordWork(metric: string, amount?: number): void;
5
+ /** Install an observer for this process and restore the previous observer. */
6
+ export declare function observeWork(next: WorkObserver): () => void;
@@ -0,0 +1 @@
1
+ const s=Symbol.for("testkit.work.observer"),t=globalThis,e=t[s]??={};export function isWorkObserved(){return e.observer!==void 0}export function recordWork(r,o=1){e.observer?.(r,o)}export function observeWork(r){const o=e.observer;return e.observer=r,()=>{e.observer=o}}
@@ -1 +1 @@
1
- const c=String;export function captureOutput(t,e={}){if(!t)return Promise.resolve();const u=t;if(u.destroyed===!0||u.readableEnded===!0)return Promise.resolve();const l=g(e.onLine),r=g(e.liveWriter),i=a(e.livePrefix);let n="";return new Promise(d=>{let s=!1;const f=()=>{s||(s=!0,d())};t.on("data",y=>{n+=v(y);const p=n.split(/\r?\n/);n=p.pop()||"";for(const o of p)o.length!==0&&(l?.(o),r&&r(i?`${i} ${o}`:o))}),t.on("end",()=>{n.length>0&&(l?.(n),r&&r(i?`${i} ${n}`:n)),f()}),t.on("close",f),t.on("error",f)})}function g(t){return typeof t!="function"?null:e=>{Reflect.apply(t,void 0,[e])}}function v(t){if(t==null)throw new TypeError("Output chunks must provide toString()");if(typeof t=="string")return t;if(typeof t=="number"||typeof t=="boolean"||typeof t=="bigint"||typeof t=="symbol")return t.toString();const e=Reflect.get(t,"toString");if(!S(e))throw new TypeError("Output chunks must provide toString()");return c(Reflect.apply(e,t,[]))}function S(t){return typeof t=="function"}function a(t){return t??""}
1
+ const c=String;export function captureOutput(t,e={}){if(!t)return Promise.resolve();const u=t;if(u.destroyed===!0||u.readableEnded===!0)return Promise.resolve();const l=y(e.onLine),r=y(e.liveWriter),i=w(e.livePrefix);let n="";return new Promise(g=>{let p=!1;const f=()=>{p||(p=!0,g())};t.on("data",d=>{n+=v(d);const s=n.split(/\r?\n/);n=s.pop()||"";for(const o of s)o.length!==0&&(l?.(o),r&&r(i?`${i} ${o}`:o))}),t.on("end",()=>{n.length>0&&(l?.(n),r&&r(i?`${i} ${n}`:n)),f()}),t.on("close",f),t.on("error",f)})}function y(t){return typeof t!="function"?null:e=>{Reflect.apply(t,void 0,[e])}}function v(t){if(t==null)throw new TypeError("Output chunks must provide toString()");if(typeof t=="string")return t;if(typeof t=="number"||typeof t=="boolean"||typeof t=="bigint"||typeof t=="symbol")return t.toString();if(typeof t!="object"&&typeof t!="function")throw new TypeError("Output chunks must provide toString()");const e=Reflect.get(t,"toString");if(!S(e))throw new TypeError("Output chunks must provide toString()");return c(Reflect.apply(e,t,[]))}function S(t){return typeof t=="function"}function w(t){return t??""}
@@ -3,7 +3,7 @@
3
3
  "entrypoint": "dist/process/process.js",
4
4
  "sourceSha256": "a54f01864b603cee568b2945390dff35d578e07f6d26b5add06014169c3d17b0",
5
5
  "bundleSha256": "62f76cf9fd019f6c552bfde2787f058917caf279670a62e7a6bfa12be34cb4b1",
6
- "packageLockSha256": "59332a4b9bf51d0bedee38025690d15fdf490151211ea06c4109f0896796709f",
6
+ "packageLockSha256": "93deb1e48aab38546ad53118a260f78c22f023ebd9a215d52953481c716d5d1b",
7
7
  "esbuildVersion": "0.28.1",
8
8
  "options": {
9
9
  "format": "esm",
@@ -0,0 +1 @@
1
+ export declare function resolveProductDir(cwd: string, explicitDir?: string | null): string;
@@ -0,0 +1 @@
1
+ import e from"node:fs";import s from"node:path";export function resolveProductDir(o,t){const r=t?s.resolve(o,t):o;if(!e.existsSync(r))throw new Error(`Product directory does not exist: ${r}`);return r}
@@ -1,4 +1,4 @@
1
- import{AsyncLocalStorage as Z}from"node:async_hooks";import{randomUUID as ee}from"node:crypto";import c from"node:fs";import te from"node:os";import l from"node:path";import{setTimeout as re}from"node:timers/promises";import{captureProcessCreationIdentity as D}from"./process-identity.js";function ne(e){try{c.mkdirSync(e,{mode:448})}catch(t){if(!i(t,"EEXIST"))throw t}return j(e)}function oe(e){try{const t=c.lstatSync(e);return t.isDirectory()&&!t.isSymbolicLink()?"real":"unsafe"}catch(t){if(i(t,"ENOENT"))return"absent";throw t}}function F(e){const t=c.lstatSync(e,{bigint:!0});if(!t.isDirectory()||t.isSymbolicLink())throw new Error(`Process lock path is not a real directory: ${e}`);return{birthtimeNs:t.birthtimeNs,dev:t.dev,ino:t.ino}}function j(e){try{return F(e)}catch(t){if(i(t,"ENOENT")||ce(t))return null;throw t}}function h(e,t){const n=j(e);return n!==null&&n.birthtimeNs===t.birthtimeNs&&n.dev===t.dev&&n.ino===t.ino}function se(e,t,n){return h(e,t)?M(n)?"unsafe":"retry":h(n,t)?"published":"unsafe"}function ce(e){return e instanceof Error&&e.message.startsWith("Process lock path is not a real directory:")}const x="owner.json",R=1,A="claim-",g="choosing",K="ticket",ie=100,V=65e3,ae=100,ue=new Set(["EACCES","EBUSY","EPERM"]),le=5e3,U=te.hostname(),fe=process.platform==="win32"?le:0,T=new Z;class W extends Error{constructor(t,n){super(`Timed out waiting for process lock ${l.basename(t)}${Ce(n)}`),this.name="ProcessLockTimeoutError"}}export async function withProcessLock(e,t,n={}){const r=X(e);Oe(r);const o=await me(r,n),s=T.getStore(),m=new Set(s??[]);m.add(r);try{return await T.run(m,t)}finally{o()}}export async function tryWithProcessLock(e,t,n={}){let r;try{r=await withProcessLock(e,async()=>{try{return{ok:!0,value:await t()}}catch(o){return{error:o,ok:!1}}},{...n,timeoutMs:0})}catch(o){if(o instanceof W)return{acquired:!1};throw o}if(!r.ok)throw r.error;return{acquired:!0,value:r.value}}export function probeProcessLock(e,t={}){const n=X(e),r=B(n);if(!M(r))return"absent";const o=t.identityObservations??new Map,s=b(r,o,t);return s.rootAvailable&&s.claims.length===0?(_(r),"absent"):"active"}async function me(e,t){const n=$e(t.timeoutMs,null,"process lock timeout"),r=ve(t.retryIntervalMs,ie,"process lock retry interval"),o=t.identityObservations??new Map,s=D(process.pid);if(s===null)throw new Error(`Cannot acquire process lock ${e}: current process creation identity is unavailable`);const m=Date.now(),f={createdAt:new Date(m).toISOString(),hostname:U,pid:process.pid,processIdentity:s,schemaVersion:R,token:ee()},a=B(e),u=l.join(a,`${A}${f.token}`),w=`${a}.preparing-${f.token}`;he(w,f);let y=null,$=!1,p=null,S=!1,v=0;try{for(y=F(w);;){N(t.signal,e);const E=ne(a);if(E!==null){try{c.renameSync(w,u),S=!0,p=E}catch(d){if(!i(d,"ENOENT"))if(de(d)){const k=se(w,y,u);if(k==="published")S=!0,p=E;else if(k==="retry"&&v<ae)v+=1;else throw d}else throw d}if(S){if(!h(a,E))throw new Error(`Process lock directory changed during claim publication: ${a}`);if(!h(u,y))throw new Error(`Process lock claim changed during publication: ${u}`);break}}else b(a,o,t);z(e,null,m,n),await G(r,t.signal,e)}if(p===null)throw new Error(`Process lock claim was published without its directory identity: ${u}`);const I=p,J=b(a,o,t,I),L=Ee(J.claims);for(ye(u,L);;){N(t.signal,e);const E=b(a,o,t,I),d=E.claims.find(Q=>Q.claimDirectory===u);if(d?.state!=="ready"||d.ticket!==L||d.ownerRecord===null||!H(d.ownerRecord,f))throw new Error(`Process lock claim disappeared or changed before acquisition: ${u}`);const k=we(E.claims,d);if(k===null)return $=!0,()=>{Ie(u,a,I,f,L)};z(e,k.ownerRecord,m,n),await G(r,t.signal,e)}}finally{S?!$&&p!==null&&y!==null&&h(a,p)&&h(u,y)&&(c.rmSync(u,{force:!0,recursive:!0}),o.delete(u),_(a)):y!==null&&h(w,y)&&c.rmSync(w,{force:!0,recursive:!0})}}function de(e){return process.platform==="win32"&&C(e)&&typeof e.code=="string"&&ue.has(e.code)}function he(e,t){c.mkdirSync(e,{mode:448});try{c.writeFileSync(l.join(e,x),`${JSON.stringify(t)}
2
- `,{encoding:"utf8",flag:"wx",mode:384}),c.writeFileSync(l.join(e,g),`${t.token}
3
- `,{encoding:"utf8",flag:"wx",mode:384})}catch(n){throw c.rmSync(e,{force:!0,recursive:!0}),n}}function ye(e,t){c.writeFileSync(l.join(e,K),`${t.toString()}
4
- `,{encoding:"utf8",flag:"wx",mode:384}),c.rmSync(l.join(e,g))}function Ee(e){const t=e.reduce((n,r)=>r.ticket===null?n:Math.max(n,r.ticket),0);if(t>=Number.MAX_SAFE_INTEGER)throw new Error("Process lock ticket space is exhausted");return t+1}function we(e,t){if(t.ticket===null||t.ownerRecord===null)return t;const n=t.ownerRecord.token;for(const r of e){if(r.claimDirectory===t.claimDirectory)continue;if(r.state!=="ready"||r.ticket===null||r.ownerRecord===null)return r;const o=r.ownerRecord.token;if(pe(r.ticket,o,t.ticket,n)<0)return r}return null}function pe(e,t,n,r){return e===n?t.localeCompare(r):e-n}function b(e,t,n,r=null){const o=oe(e);if(o==="absent")return{claims:[],rootAvailable:!0};if(o==="unsafe"){const f=O(e);return Ne(e,t,n)?{claims:[],rootAvailable:!0}:{claims:[f],rootAvailable:!1}}if(r!==null&&!h(e,r))return{claims:[O(e)],rootAvailable:!1};let s;try{s=c.readdirSync(e,{withFileTypes:!0})}catch(f){if(i(f,"ENOENT"))return{claims:[],rootAvailable:!0};throw f}const m=[];for(const f of s){const a=l.join(e,f.name),u=!f.name.startsWith(A)||!f.isDirectory()?O(a):q(a);Se(u,t,n)?be(a,t):m.push(u)}return{claims:m,rootAvailable:!0}}function O(e){return{claimDirectory:e,ownerRecord:null,state:"malformed",ticket:null}}function q(e){const t=Ae(e),n=M(l.join(e,g)),r=ke(e),o=l.basename(e).slice(A.length),s=t===null||t.token===o;return{claimDirectory:e,ownerRecord:t,state:t===null||!s?"malformed":n?"choosing":r===null?"malformed":"ready",ticket:r}}function ke(e){try{const t=c.readFileSync(l.join(e,K),"utf8").trim(),n=Number(t);return Number.isSafeInteger(n)&&n>0?n:null}catch{return null}}function Se(e,t,n){const r=e.ownerRecord;if(r===null||r.hostname!==U)return n.hostScope==="current-host"&&_e(e.claimDirectory)>=V;if(Pe(r.pid)==="absent")return!0;const o=P(r),s=t.get(e.claimDirectory);if(s?.ownerKey===o&&Date.now()-s.checkedAtMs<fe)return!1;const m=D(r.pid);return t.set(e.claimDirectory,{checkedAtMs:Date.now(),ownerKey:o}),m!==null&&m!==r.processIdentity}function be(e,t){t.delete(e),c.rmSync(e,{force:!0,recursive:!0})}function Ne(e,t,n){if(n.hostScope!=="current-host")return!1;const r=Y(e);if(r==="absent")return!0;if(r==="real-directory"||Re(r)<V)return!1;const o=Y(e);if(o==="absent")return!0;if(o==="real-directory"||!Le(o,r))return!1;try{return c.unlinkSync(e),t.delete(e),!0}catch(s){if(i(s,"ENOENT"))return!0;if(i(s,"EISDIR")||i(s,"EPERM"))return!1;throw s}}function Ie(e,t,n,r,o){if(!h(t,n))throw new Error(`Process lock directory changed before release: ${t}`);const s=q(e);if(s.state!=="ready"||s.ticket!==o||s.ownerRecord===null||!H(s.ownerRecord,r))throw new Error(`Process lock ownership changed before release: ${e}`);c.rmSync(e,{force:!0,recursive:!0}),_(t)}function Y(e){try{const t=c.lstatSync(e,{bigint:!0});return t.isDirectory()&&!t.isSymbolicLink()?"real-directory":{birthtimeNs:t.birthtimeNs,ctimeNs:t.ctimeNs,dev:t.dev,ino:t.ino,mode:t.mode,mtimeNs:t.mtimeNs,size:t.size}}catch(t){if(i(t,"ENOENT"))return"absent";throw t}}function Le(e,t){return e.birthtimeNs===t.birthtimeNs&&e.ctimeNs===t.ctimeNs&&e.dev===t.dev&&e.ino===t.ino&&e.mode===t.mode&&e.mtimeNs===t.mtimeNs&&e.size===t.size}function Re(e){return Math.max(0,Date.now()-Number(e.mtimeNs)/1e6)}function _(e){try{c.rmdirSync(e)}catch(t){if(!i(t,"ENOENT")&&!i(t,"ENOTEMPTY")&&!i(t,"EEXIST"))throw t}}function Ae(e){let t;try{t=JSON.parse(c.readFileSync(l.join(e,x),"utf8"))}catch{return null}return C(t)?ge(t):null}function ge(e){const t=Object.keys(e).sort(),n=["createdAt","hostname","pid","processIdentity","schemaVersion","token"];return t.length!==n.length||t.some((r,o)=>r!==n[o])||e.schemaVersion!==R||!De(e.createdAt)||typeof e.hostname!="string"||e.hostname.length===0||!Fe(e.pid)||typeof e.processIdentity!="string"||e.processIdentity.length===0||typeof e.token!="string"||e.token.length===0?null:{createdAt:e.createdAt,hostname:e.hostname,pid:e.pid,processIdentity:e.processIdentity,schemaVersion:R,token:e.token}}function H(e,t){return P(e)===P(t)}function P(e){return[e.hostname,e.pid.toString(),e.processIdentity,e.token,e.createdAt].join("\0")}function B(e){return`${e}.dir`}function X(e){const t=l.resolve(Te(e)),n=c.realpathSync.native(l.dirname(t));return l.join(n,l.basename(t))}function Te(e){if(e.trim().length===0)throw new Error("Process lock path must not be empty");return e}function Oe(e){if(T.getStore()?.has(e))throw new Error(`Reentrant process lock acquisition is not allowed: ${e}`)}function z(e,t,n,r){if(r!==null&&Date.now()-n>=r)throw new W(e,t)}function _e(e){try{return Math.max(0,Date.now()-c.lstatSync(e).mtimeMs)}catch(t){if(i(t,"ENOENT"))return Number.POSITIVE_INFINITY;throw t}}function Pe(e){try{return process.kill(e,0),"alive"}catch(t){return i(t,"ESRCH")?"absent":i(t,"EPERM")?"alive":"unknown"}}async function G(e,t,n){N(t,n);try{await re(e,void 0,{signal:t})}catch(r){throw N(t,n),r}}function N(e,t){if(e?.aborted)throw Me(e,t)}function Me(e,t){return e.reason instanceof Error?e.reason:new Error(`Waiting for process lock was aborted: ${t}`)}function Ce(e){return e===null?"":` (host=${e.hostname} pid=${e.pid.toString()} identity=${e.processIdentity} createdAt=${e.createdAt})`}function $e(e,t,n){const r=e===void 0?t:e;if(r!==null&&(!Number.isSafeInteger(r)||r<0))throw new Error(`${n} must be null or a non-negative safe integer`);return r}function ve(e,t,n){const r=e??t;if(!Number.isSafeInteger(r)||r<=0)throw new Error(`${n} must be a positive safe integer`);return r}function De(e){return typeof e=="string"&&e.length>0&&Number.isFinite(Date.parse(e))}function Fe(e){return Number.isSafeInteger(e)&&Number(e)>0}function M(e){try{return c.lstatSync(e),!0}catch(t){if(i(t,"ENOENT"))return!1;throw t}}function i(e,t){return C(e)&&e.code===t}function C(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}
1
+ import{AsyncLocalStorage as te}from"node:async_hooks";import{randomUUID as re}from"node:crypto";import i from"node:fs";import ne from"node:os";import a from"node:path";import{setTimeout as oe}from"node:timers/promises";import{captureProcessCreationIdentity as j}from"./process-identity.js";import{channel as se}from"node:diagnostics_channel";const x=se("testkit.work");function ie(e){try{i.mkdirSync(e,{mode:448})}catch(t){if(!c(t,"EEXIST"))throw t}return V(e)}function ce(e){try{const t=i.lstatSync(e);return t.isDirectory()&&!t.isSymbolicLink()?"real":"unsafe"}catch(t){if(c(t,"ENOENT"))return"absent";throw t}}function K(e){const t=i.lstatSync(e,{bigint:!0});if(!t.isDirectory()||t.isSymbolicLink())throw new Error(`Process lock path is not a real directory: ${e}`);return{birthtimeNs:t.birthtimeNs,dev:t.dev,ino:t.ino}}function V(e){try{return K(e)}catch(t){if(c(t,"ENOENT")||ue(t))return null;throw t}}function h(e,t){const n=V(e);return n!==null&&n.birthtimeNs===t.birthtimeNs&&n.dev===t.dev&&n.ino===t.ino}function ae(e,t,n){return h(e,t)?v(n)?"unsafe":"retry":h(n,t)?"published":"unsafe"}function ue(e){return e instanceof Error&&e.message.startsWith("Process lock path is not a real directory:")}const U="owner.json",g=1,T="claim-",M="choosing",W="ticket",le=100,q=65e3,fe=100,me=new Set(["EACCES","EBUSY","EPERM"]),de=5e3,Y=ne.hostname(),he=process.platform==="win32"?de:0,O=new te;class H extends Error{constructor(t,n){super(`Timed out waiting for process lock ${a.basename(t)}${Fe(n)}`),this.name="ProcessLockTimeoutError"}}export async function withProcessLock(e,t,n={}){const r=J(e);_e(r);const o=await ye(r,n),s=O.getStore(),u=new Set(s??[]);u.add(r);try{return await O.run(u,t)}finally{o()}}export async function tryWithProcessLock(e,t,n={}){let r;try{r=await withProcessLock(e,async()=>{try{return{ok:!0,value:await t()}}catch(o){return{error:o,ok:!1}}},{...n,timeoutMs:0})}catch(o){if(o instanceof H)return{acquired:!1};throw o}if(!r.ok)throw r.error;return{acquired:!0,value:r.value}}export function probeProcessLock(e,t={}){const n=J(e),r=G(n);if(!v(r))return"absent";const o=t.identityObservations??new Map,s=R(r,o,t);return s.rootAvailable&&s.claims.length===0?(_(r),"absent"):"active"}async function ye(e,t){const n=je(t.timeoutMs,null,"process lock timeout"),r=xe(t.retryIntervalMs,le,"process lock retry interval");let o=t.retryIntervalMs===void 0?10:r;async function s(){const b=o;o=Math.min(r,o*2),await $e(b,t.signal,e)}const u=t.identityObservations??new Map,f=j(process.pid);if(f===null)throw new Error(`Cannot acquire process lock ${e}: current process creation identity is unavailable`);const E=Date.now(),y={createdAt:new Date(E).toISOString(),hostname:Y,pid:process.pid,processIdentity:f,schemaVersion:g,token:re()},m=G(e),l=a.join(m,`${T}${y.token}`),k=`${m}.preparing-${y.token}`;we(k,y);let w=null,D=!1,S=null,I=!1,F=0;try{for(w=K(k);;){L(t.signal,e);const p=ie(m);if(p!==null){try{i.renameSync(k,l),I=!0,S=p}catch(d){if(!c(d,"ENOENT"))if(Ee(d)){const N=ae(k,w,l);if(N==="published")I=!0,S=p;else if(N==="retry"&&F<fe)F+=1;else throw d}else throw d}if(I){if(!h(m,p))throw new Error(`Process lock directory changed during claim publication: ${m}`);if(!h(l,w))throw new Error(`Process lock claim changed during publication: ${l}`);break}}else R(m,u,t);Q(e,null,E,n),await s()}if(S===null)throw new Error(`Process lock claim was published without its directory identity: ${l}`);const b=S,Z=R(m,u,t,b),A=ke(Z.claims);for(pe(l,A);;){L(t.signal,e);const p=R(m,u,t,b),d=p.claims.find(ee=>ee.claimDirectory===l);if(d?.state!=="ready"||d.ticket!==A||d.ownerRecord===null||!X(d.ownerRecord,y))throw new Error(`Process lock claim disappeared or changed before acquisition: ${l}`);const N=Se(p.claims,d);if(N===null)return D=!0,()=>{Ae(l,m,b,y,A)};Q(e,N.ownerRecord,E,n),await s()}}finally{I?!D&&S!==null&&w!==null&&h(m,S)&&h(l,w)&&(i.rmSync(l,{force:!0,recursive:!0}),u.delete(l),_(m)):w!==null&&h(k,w)&&i.rmSync(k,{force:!0,recursive:!0})}}function Ee(e){return process.platform==="win32"&&$(e)&&typeof e.code=="string"&&me.has(e.code)}function we(e,t){i.mkdirSync(e,{mode:448});try{i.writeFileSync(a.join(e,U),`${JSON.stringify(t)}
2
+ `,{encoding:"utf8",flag:"wx",mode:384}),i.writeFileSync(a.join(e,M),`${t.token}
3
+ `,{encoding:"utf8",flag:"wx",mode:384})}catch(n){throw i.rmSync(e,{force:!0,recursive:!0}),n}}function pe(e,t){i.writeFileSync(a.join(e,W),`${t.toString()}
4
+ `,{encoding:"utf8",flag:"wx",mode:384}),i.rmSync(a.join(e,M))}function ke(e){const t=e.reduce((n,r)=>r.ticket===null?n:Math.max(n,r.ticket),0);if(t>=Number.MAX_SAFE_INTEGER)throw new Error("Process lock ticket space is exhausted");return t+1}function Se(e,t){if(t.ticket===null||t.ownerRecord===null)return t;const n=t.ownerRecord.token;for(const r of e){if(r.claimDirectory===t.claimDirectory)continue;if(r.state!=="ready"||r.ticket===null||r.ownerRecord===null)return r;const o=r.ownerRecord.token;if(be(r.ticket,o,t.ticket,n)<0)return r}return null}function be(e,t,n,r){return e===n?t.localeCompare(r):e-n}function R(e,t,n,r=null){const o=ce(e);if(o==="absent")return{claims:[],rootAvailable:!0};if(o==="unsafe"){const f=P(e);return Le(e,t,n)?{claims:[],rootAvailable:!0}:{claims:[f],rootAvailable:!1}}if(r!==null&&!h(e,r))return{claims:[P(e)],rootAvailable:!1};let s;try{s=i.readdirSync(e,{withFileTypes:!0})}catch(f){if(c(f,"ENOENT"))return{claims:[],rootAvailable:!0};throw f}const u=[];for(const f of s){const E=a.join(e,f.name),y=!f.name.startsWith(T)||!f.isDirectory()?P(E):z(E);Ie(y,t,n)?Re(E,t):u.push(y)}return{claims:u,rootAvailable:!0}}function P(e){return{claimDirectory:e,ownerRecord:null,state:"malformed",ticket:null}}function z(e){const t=Me(e),n=v(a.join(e,M)),r=Ne(e),o=a.basename(e).slice(T.length),s=t===null||t.token===o;return{claimDirectory:e,ownerRecord:t,state:t===null||!s?"malformed":n?"choosing":r===null?"malformed":"ready",ticket:r}}function Ne(e){try{const t=i.readFileSync(a.join(e,W),"utf8").trim(),n=Number(t);return Number.isSafeInteger(n)&&n>0?n:null}catch{return null}}function Ie(e,t,n){const r=e.ownerRecord;if(r===null||r.hostname!==Y)return n.hostScope==="current-host"&&Ce(e.claimDirectory)>=q;if(ve(r.pid)==="absent")return!0;const o=C(r),s=t.get(e.claimDirectory);if(s?.ownerKey===o&&Date.now()-s.checkedAtMs<he)return!1;const u=j(r.pid);return t.set(e.claimDirectory,{checkedAtMs:Date.now(),ownerKey:o}),u!==null&&u!==r.processIdentity}function Re(e,t){t.delete(e),i.rmSync(e,{force:!0,recursive:!0})}function Le(e,t,n){if(n.hostScope!=="current-host")return!1;const r=B(e);if(r==="absent")return!0;if(r==="real-directory"||Te(r)<q)return!1;const o=B(e);if(o==="absent")return!0;if(o==="real-directory"||!ge(o,r))return!1;try{return i.unlinkSync(e),t.delete(e),!0}catch(s){if(c(s,"ENOENT"))return!0;if(c(s,"EISDIR")||c(s,"EPERM"))return!1;throw s}}function Ae(e,t,n,r,o){if(!h(t,n))throw new Error(`Process lock directory changed before release: ${t}`);const s=z(e);if(s.state!=="ready"||s.ticket!==o||s.ownerRecord===null||!X(s.ownerRecord,r))throw new Error(`Process lock ownership changed before release: ${e}`);i.rmSync(e,{force:!0,recursive:!0}),_(t)}function B(e){try{const t=i.lstatSync(e,{bigint:!0});return t.isDirectory()&&!t.isSymbolicLink()?"real-directory":{birthtimeNs:t.birthtimeNs,ctimeNs:t.ctimeNs,dev:t.dev,ino:t.ino,mode:t.mode,mtimeNs:t.mtimeNs,size:t.size}}catch(t){if(c(t,"ENOENT"))return"absent";throw t}}function ge(e,t){return e.birthtimeNs===t.birthtimeNs&&e.ctimeNs===t.ctimeNs&&e.dev===t.dev&&e.ino===t.ino&&e.mode===t.mode&&e.mtimeNs===t.mtimeNs&&e.size===t.size}function Te(e){return Math.max(0,Date.now()-Number(e.mtimeNs)/1e6)}function _(e){try{i.rmdirSync(e)}catch(t){if(!c(t,"ENOENT")&&!c(t,"ENOTEMPTY")&&!c(t,"EEXIST"))throw t}}function Me(e){let t;try{t=JSON.parse(i.readFileSync(a.join(e,U),"utf8"))}catch{return null}return $(t)?Oe(t):null}function Oe(e){const t=Object.keys(e).sort(),n=["createdAt","hostname","pid","processIdentity","schemaVersion","token"];return t.length!==n.length||t.some((r,o)=>r!==n[o])||e.schemaVersion!==g||!Ke(e.createdAt)||typeof e.hostname!="string"||e.hostname.length===0||!Ve(e.pid)||typeof e.processIdentity!="string"||e.processIdentity.length===0||typeof e.token!="string"||e.token.length===0?null:{createdAt:e.createdAt,hostname:e.hostname,pid:e.pid,processIdentity:e.processIdentity,schemaVersion:g,token:e.token}}function X(e,t){return C(e)===C(t)}function C(e){return[e.hostname,e.pid.toString(),e.processIdentity,e.token,e.createdAt].join("\0")}function G(e){return`${e}.dir`}function J(e){const t=a.resolve(Pe(e)),n=i.realpathSync.native(a.dirname(t));return a.join(n,a.basename(t))}function Pe(e){if(e.trim().length===0)throw new Error("Process lock path must not be empty");return e}function _e(e){if(O.getStore()?.has(e))throw new Error(`Reentrant process lock acquisition is not allowed: ${e}`)}function Q(e,t,n,r){if(r!==null&&Date.now()-n>=r)throw new H(e,t)}function Ce(e){try{return Math.max(0,Date.now()-i.lstatSync(e).mtimeMs)}catch(t){if(c(t,"ENOENT"))return Number.POSITIVE_INFINITY;throw t}}function ve(e){try{return process.kill(e,0),"alive"}catch(t){return c(t,"ESRCH")?"absent":c(t,"EPERM")?"alive":"unknown"}}async function $e(e,t,n){L(t,n),x.hasSubscribers&&x.publish({metric:"locks.contentionRetries",amount:1});try{await oe(e,void 0,{signal:t})}catch(r){throw L(t,n),r}}function L(e,t){if(e?.aborted)throw De(e,t)}function De(e,t){return e.reason instanceof Error?e.reason:new Error(`Waiting for process lock was aborted: ${t}`)}function Fe(e){return e===null?"":` (host=${e.hostname} pid=${e.pid.toString()} identity=${e.processIdentity} createdAt=${e.createdAt})`}function je(e,t,n){const r=e===void 0?t:e;if(r!==null&&(!Number.isSafeInteger(r)||r<0))throw new Error(`${n} must be null or a non-negative safe integer`);return r}function xe(e,t,n){const r=e??t;if(!Number.isSafeInteger(r)||r<=0)throw new Error(`${n} must be a positive safe integer`);return r}function Ke(e){return typeof e=="string"&&e.length>0&&Number.isFinite(Date.parse(e))}function Ve(e){return Number.isSafeInteger(e)&&Number(e)>0}function v(e){try{return i.lstatSync(e),!0}catch(t){if(c(t,"ENOENT"))return!1;throw t}}function c(e,t){return $(e)&&e.code===t}function $(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}
@@ -1 +1 @@
1
- import u from"net";import{setTimeout as w}from"node:timers/promises";export const DEFAULT_READY_TIMEOUT_MS=12e4;export async function waitForReady({name:e,url:t,timeoutMs:r,process:s,signal:o}){const c=Date.now(),a=Promise.resolve(s.completion).then(i=>({kind:"completion",result:i})),n=b(o);try{for(;Date.now()-c<r;){const i=Math.max(1,r-(Date.now()-c)),f=Promise.resolve(w(Math.min(1e3,i))).then(()=>({kind:"interval"})),l=new AbortController,m=fetch(t,{signal:l.signal}).then(p=>({kind:"endpoint",ready:p.ok}),()=>({kind:"endpoint",ready:!1}));let d=await Promise.race([a,...n?[n.promise]:[],m,f]).finally(()=>{l.abort()});if(d.kind==="endpoint"){if(d.ready)return;d=await Promise.race([a,...n?[n.promise]:[],f])}if(d.kind==="completion")throw serviceCompletionError(e,d.result);if(d.kind==="abort")throw d.reason}}finally{n?.dispose()}throw new Error(`Timed out waiting for "${e}" to become ready at ${t}`)}export function serviceCompletionError(e,t,r="before becoming ready"){const s=r==="unexpectedly"?"Local service":"Service",o=t.error||t.terminationError||null,c=t.exitCode!==null?`exit code ${t.exitCode.toString()}`:t.exitSignal?`signal ${t.exitSignal}`:"no exit status";return new Error(`${s} "${e}" exited ${r} with ${c}${o?`: ${o.message}`:""}`,o?{cause:o}:void 0)}function b(e){if(!e)return null;let t=()=>{};const r=new Promise(s=>{t=()=>{s({kind:"abort",reason:e.reason||new Error("Service startup aborted")})},e.aborted?t():e.addEventListener("abort",t,{once:!0})});return{dispose(){e.removeEventListener("abort",t)},promise:r}}export async function isPortInUse({host:e,port:t}){return new Promise((r,s)=>{const o=new u.Socket;let c=!1;const a=(n,i=null)=>{if(!c){if(c=!0,o.destroy(),i){s(i);return}r(n)}};o.setTimeout(1e3),o.once("connect",()=>{a(!0)}),o.once("timeout",()=>{a(!1)}),o.once("error",n=>{const i=y(n,"code");if(typeof i=="string"&&["ECONNREFUSED","EHOSTUNREACH","ENOTFOUND"].includes(i)){a(!1);return}a(!1,n)}),o.connect(t,e)})}function y(e,t){if(!((typeof e!="object"||e===null)&&typeof e!="function"))return Reflect.get(e,t)}export function connectEndpoint(e,t,r){return new Promise(s=>{const o=u.createConnection({host:e.host,port:e.port});let c=!1;const a=Date.now(),n=f=>{c||(c=!0,r?.removeEventListener("abort",i),o.destroy(),s({host:e.host,port:e.port,durationMs:Math.max(0,Date.now()-a),...f}))};function i(){n({ok:!1,error:"connection cancelled"})}o.setTimeout(t),o.once("connect",()=>{n({ok:!0})}),o.once("timeout",()=>{n({ok:!1,error:`connect timed out after ${t.toString()}ms`})}),o.once("error",f=>{n({ok:!1,error:f.message})}),r?.addEventListener("abort",i,{once:!0}),r?.aborted&&i()})}
1
+ import p from"net";import{setTimeout as y}from"node:timers/promises";export const DEFAULT_READY_TIMEOUT_MS=12e4;export async function waitForReady({name:e,url:t,timeoutMs:r,process:c,signal:o}){const s=Date.now(),a=Promise.resolve(c.completion).then(f=>({kind:"completion",result:f})),n=E(o);let i=10,d;try{for(;Date.now()-s<r;){const f=Math.max(1,r-(Date.now()-s));d?.abort(),d=new AbortController;const u=y(Math.min(i,f),void 0,{signal:d.signal}).then(()=>({kind:"interval"}));i=Math.min(i*2,1e3);const m=new AbortController,b=fetch(t,{signal:m.signal}).then(w=>({kind:"endpoint",ready:w.ok}),()=>({kind:"endpoint",ready:!1}));let l=await Promise.race([a,...n?[n.promise]:[],b,u]).finally(()=>{m.abort()});if(l.kind==="endpoint"){if(l.ready)return;l=await Promise.race([a,...n?[n.promise]:[],u])}if(l.kind==="completion")throw serviceCompletionError(e,l.result);if(l.kind==="abort")throw l.reason}}finally{d?.abort(),n?.dispose()}throw new Error(`Timed out waiting for "${e}" to become ready at ${t}`)}export function serviceCompletionError(e,t,r="before becoming ready"){const c=r==="unexpectedly"?"Local service":"Service",o=t.error||t.terminationError||null,s=t.exitCode!==null?`exit code ${t.exitCode.toString()}`:t.exitSignal?`signal ${t.exitSignal}`:"no exit status";return new Error(`${c} "${e}" exited ${r} with ${s}${o?`: ${o.message}`:""}`,o?{cause:o}:void 0)}function E(e){if(!e)return null;let t=()=>{};const r=new Promise(c=>{t=()=>{c({kind:"abort",reason:e.reason||new Error("Service startup aborted")})},e.aborted?t():e.addEventListener("abort",t,{once:!0})});return{dispose(){e.removeEventListener("abort",t)},promise:r}}export async function isPortInUse({host:e,port:t}){return new Promise((r,c)=>{const o=new p.Socket;let s=!1;const a=(n,i=null)=>{if(!s){if(s=!0,o.destroy(),i){c(i);return}r(n)}};o.setTimeout(1e3),o.once("connect",()=>{a(!0)}),o.once("timeout",()=>{a(!1)}),o.once("error",n=>{const i=h(n,"code");if(typeof i=="string"&&["ECONNREFUSED","EHOSTUNREACH","ENOTFOUND"].includes(i)){a(!1);return}a(!1,n)}),o.connect(t,e)})}function h(e,t){if(!((typeof e!="object"||e===null)&&typeof e!="function"))return Reflect.get(e,t)}export function connectEndpoint(e,t,r){return new Promise(c=>{const o=p.createConnection({host:e.host,port:e.port});let s=!1;const a=Date.now(),n=d=>{s||(s=!0,r?.removeEventListener("abort",i),o.destroy(),c({host:e.host,port:e.port,durationMs:Math.max(0,Date.now()-a),...d}))};function i(){n({ok:!1,error:"connection cancelled"})}o.setTimeout(t),o.once("connect",()=>{n({ok:!0})}),o.once("timeout",()=>{n({ok:!1,error:`connect timed out after ${t.toString()}ms`})}),o.once("error",d=>{n({ok:!1,error:d.message})}),r?.addEventListener("abort",i,{once:!0}),r?.aborted&&i()})}
@@ -0,0 +1,5 @@
1
+ import { type RunManifest } from "./manifest.js";
2
+ export declare function isRunManifestActive(manifest: Readonly<RunManifest>): boolean;
3
+ export declare function inspectRunManifests(productDir: string, runId?: string): Array<RunManifest & {
4
+ active: boolean;
5
+ }>;
@@ -0,0 +1 @@
1
+ import{observeProcessCreationIdentity as i}from"../process-identity.js";import{loadRunManifest as o,listRunManifests as r}from"./manifest.js";export function isRunManifestActive(t){return t.pid>0&&i(t.pid,t.processIdentity)!=="different"}export function inspectRunManifests(t,e){const n=e?o(t,e):null;return(e?n?[n]:[]:r(t)).map(s=>({...s,active:isRunManifestActive(s)}))}
@@ -79,6 +79,7 @@ export declare function writeRunMaintenanceStopRequest(productDir: string, reque
79
79
  export declare function readRunMaintenanceStopRequest(productDir: string, runId: unknown): RunMaintenanceStopRequest | null;
80
80
  export declare function removeRunMaintenanceStopRequest(productDir: string, runId: unknown): void;
81
81
  export declare function removeRunManifestFile(productDir: string, runId: unknown): void;
82
+ export declare function loadRunManifest(productDir: string, runId: unknown): RunManifest | null;
82
83
  export declare function listRunManifests(productDir: string): RunManifest[];
83
84
  export declare function requireSafeRunId(value: unknown, label: string): string;
84
85
  export declare function normalizeRunManifest(value: unknown): RunManifest | null;
@@ -1 +1 @@
1
- import i from"node:fs";import l from"node:path";import{randomUUID as _}from"node:crypto";import{publishWindowsFile as P}from"../windows-native.js";import{serializeJson as x,writeJsonFileAtomicWithRetry as h}from"../files/json-file.js";import{RUN_MAINTENANCE_STOP_REQUEST_SCHEMA_VERSION as S,RUN_MANIFEST_SCHEMA_VERSION as I}from"./schema.js";export{S as RUN_MAINTENANCE_STOP_REQUEST_SCHEMA_VERSION,I as RUN_MANIFEST_SCHEMA_VERSION};const $=l.join(".testkit","_runs"),D=".maintenance-stop",E=128,U=/^[A-Za-z0-9][A-Za-z0-9._-]*$/u,F=/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u,O=/^(?:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\.|$)/iu;export class RunManifestCollisionError extends Error{code="TESTKIT_RUN_MANIFEST_COLLISION";manifestPath;constructor(t,e){super(`Testkit run manifest already exists at ${t}`,e),this.name="RunManifestCollisionError",this.manifestPath=t}}export class RunManifestInventoryError extends Error{code="TESTKIT_RUN_MANIFEST_INVENTORY_FAILED";manifestPath;constructor(t,e,r){super(`Cannot inventory Testkit run manifest ${t}: ${e}`,r),this.name="RunManifestInventoryError",this.manifestPath=t}}export function runManifestPath(n,t){const e=requireSafeRunId(t,"Testkit run id");return l.join(p(n),`${e}.json`)}export function writeRunManifest(n,t){const e=runManifestPath(n,t.runId);i.mkdirSync(p(n),{recursive:!0}),h(e,t)}export function prepareRunShutdownDeadlines(n,t){return{...n,services:n.services.map(e=>e.shutdownTimeoutMs===void 0?e:{...e,shutdownDeadlineAt:Math.min(e.shutdownDeadlineAt??Number.MAX_VALUE,Date.parse(t)+e.shutdownTimeoutMs)})}}export function persistRunShutdownDeadlines(n,t){h(runManifestPath(n,t.runId),t)}export function createRunManifest(n,t){const e=runManifestPath(n,t.runId),r=p(n);i.mkdirSync(r,{recursive:!0});const o=l.join(r,`.${l.basename(e)}.${process.pid.toString()}.${_()}.tmp`);i.writeFileSync(o,x(t,`run manifest ${t.runId}`),{flag:"wx"});try{process.platform==="win32"?P(o,e):i.linkSync(o,e)}catch(s){throw J(s,"EEXIST")?new RunManifestCollisionError(e,{cause:s}):s}finally{i.rmSync(o,{force:!0})}}export function writeRunMaintenanceStopRequest(n,t){const e=g(n,t.runId);i.mkdirSync(p(n),{recursive:!0}),h(e,t)}export function readRunMaintenanceStopRequest(n,t){const e=requireSafeRunId(t,"Testkit run id"),r=g(n,e);let o;try{o=JSON.parse(i.readFileSync(r,"utf8"))}catch{return null}const s=j(o);return s?.runId===e?s:null}export function removeRunMaintenanceStopRequest(n,t){i.rmSync(g(n,t),{force:!0})}export function removeRunManifestFile(n,t){const e=runManifestPath(n,t),r=p(n);i.rmSync(e,{force:!0}),removeRunMaintenanceStopRequest(n,t),i.existsSync(r)&&i.readdirSync(r).length===0&&i.rmSync(r,{recursive:!0,force:!0})}export function listRunManifests(n){const t=p(n);return i.existsSync(t)?i.readdirSync(t,{withFileTypes:!0}).filter(e=>e.name.endsWith(".json")).map(e=>{const r=l.join(t,e.name);if(!e.isFile())throw new RunManifestInventoryError(r,"manifest path is not a regular file");return C(r)}).sort((e,r)=>(e.startedAt||"").localeCompare(r.startedAt||"")):[]}export function requireSafeRunId(n,t){const e=R(n);if(e!==null)return e;throw new Error(`${t} must be 1-${E.toString()} characters using only ASCII letters, digits, dot, underscore, or hyphen, must start with a letter or digit, and must not use a reserved Windows device name`)}export function normalizeRunManifest(n){if(!m(n)||n.schemaVersion!==I)return null;const t=R(n.runId);if(t===null)return null;const e=n.controller,r=n.finishedAt,o=n.heartbeatAt,s=T(n.incarnationId),u=n.interruptReason,a=n.pid,d=n.processIdentity,c=n.session,f=n.startedAt,y=n.status,b=z(n.services),w=V(n.runtimeDirs);return typeof a!="number"||!Number.isSafeInteger(a)||a<0||s===null||b===null||w===null||a===0&&d!==null||a>0&&(typeof d!="string"||d.length===0)?null:{incarnationId:s,runId:t,runtimeDirs:w,schemaVersion:I,services:b,pid:a,processIdentity:typeof d=="string"?d:null,...W(e)?{controller:e}:{},...typeof r=="string"?{finishedAt:r}:{},...N(o)?{heartbeatAt:o}:{},...N(u)?{interruptReason:u}:{},...q(c)?{session:c}:{},...typeof f=="string"?{startedAt:f}:{},...typeof y=="string"?{status:y}:{}}}function g(n,t){const e=requireSafeRunId(t,"Testkit run id");return l.join(p(n),`${e}${D}`)}function j(n){if(!m(n)||n.schemaVersion!==S)return null;const t=R(n.runId),e=T(n.targetIncarnationId);return t===null||e===null||!M(n.coordinatorPid)||typeof n.coordinatorProcessIdentity!="string"||n.coordinatorProcessIdentity.length===0||typeof n.reason!="string"||n.reason.length===0||typeof n.requestedAt!="string"||!Number.isFinite(Date.parse(n.requestedAt))||!M(n.targetPid)||typeof n.targetProcessIdentity!="string"||n.targetProcessIdentity.length===0?null:{coordinatorPid:n.coordinatorPid,coordinatorProcessIdentity:n.coordinatorProcessIdentity,reason:n.reason,requestedAt:n.requestedAt,runId:t,schemaVersion:S,targetIncarnationId:e,targetPid:n.targetPid,targetProcessIdentity:n.targetProcessIdentity}}function C(n){let t;try{t=i.readFileSync(n,"utf8")}catch(u){throw new RunManifestInventoryError(n,"file could not be read",{cause:u})}let e;try{e=JSON.parse(t)}catch(u){throw new RunManifestInventoryError(n,"file is not valid JSON",{cause:u})}const r=normalizeRunManifest(e);if(r!==null){const u=l.basename(n,".json");if(r.runId!==u)throw new RunManifestInventoryError(n,"embedded runId does not match the manifest filename");return r}const o=m(e)?e.schemaVersion:void 0,s=o===I?`schema-v${I.toString()} record is invalid`:`unsupported schema version ${typeof o=="number"?o.toString():"missing"}`;throw new RunManifestInventoryError(n,s)}function R(n){return typeof n=="string"&&n.length<=E&&U.test(n)&&!O.test(n)?n:null}function T(n){return typeof n=="string"&&F.test(n)?n:null}function z(n){if(!Array.isArray(n))return null;const t=[];for(const e of n){const r=k(e);if(r===null)return null;t.push(r)}return t}function k(n){if(!m(n))return null;const t=n.command,e=n.cwd,r=n.pid,o=n.processIdentity,s=n.processGroupId,u=n.runtimeLabel,a=n.serviceName,d=n.startedAt,c=n.shutdownTimeoutMs,f=n.shutdownDeadlineAt;if(c!==void 0&&(typeof c!="number"||!Number.isFinite(c)||!Number.isInteger(c)||c<=0)||f!=null&&(c===void 0||typeof f!="number"||!Number.isFinite(f)||f<0))return null;const y=L(n.ports);return typeof r!="number"||!Number.isSafeInteger(r)||r<=0||s!==null&&(typeof s!="number"||!Number.isSafeInteger(s)||s!==r)||typeof o!="string"||o.length===0||y===null?null:{...c===void 0?{}:{shutdownTimeoutMs:c},...f===void 0?{}:{shutdownDeadlineAt:f},ports:y,pid:r,processGroupId:s,processIdentity:o,...N(t)?{command:t}:{},...typeof e=="string"?{cwd:e}:{},...typeof u=="string"?{runtimeLabel:u}:{},...typeof a=="string"?{serviceName:a}:{},...typeof d=="string"?{startedAt:d}:{}}}function L(n){if(!Array.isArray(n))return null;const t=[];for(const e of n){if(!m(e))return null;const r=e.host,o=e.port;if(typeof r!="string"||typeof o!="number"||!Number.isSafeInteger(o)||o<=0||o>65535)return null;t.push({...e,host:r,port:o})}return t}function V(n){return!Array.isArray(n)||!n.every(t=>typeof t=="string")?null:n}function M(n){return typeof n=="number"&&Number.isSafeInteger(n)&&n>0}function p(n){return l.join(n,$)}function m(n){return typeof n=="object"&&n!==null&&!Array.isArray(n)}function q(n){return n===null||m(n)}function W(n){return n===null?!0:m(n)?Object.values(n).every(t=>t===null||typeof t=="string"):!1}function N(n){return n===null||typeof n=="string"}function J(n,t){return(typeof n=="object"||typeof n=="function")&&n!==null&&Reflect.get(n,"code")===t}const A=String;export function formatRunSummary(n){const t=normalizeRunManifest(n);if(!t)return A(n);const e=[...new Set(t.services.map(r=>r.runtimeLabel).filter(r=>!!r))];return`${A(t.runId)} pid=${A(t.pid)}${e.length>0?` runtimes=${e.join(",")}`:""}`}
1
+ import{isWorkObserved as $,recordWork as I}from"../../kernel/work-metrics.js";import i from"node:fs";import l from"node:path";import{randomUUID as F}from"node:crypto";import{publishWindowsFile as D}from"../windows-native.js";import{serializeJson as O,writeJsonFileAtomicWithRetry as S}from"../files/json-file.js";import{RUN_MAINTENANCE_STOP_REQUEST_SCHEMA_VERSION as g,RUN_MANIFEST_SCHEMA_VERSION as h}from"./schema.js";export{g as RUN_MAINTENANCE_STOP_REQUEST_SCHEMA_VERSION,h as RUN_MANIFEST_SCHEMA_VERSION};const U=l.join(".testkit","_runs"),j=".maintenance-stop",T=128,C=/^[A-Za-z0-9][A-Za-z0-9._-]*$/u,k=/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u,L=/^(?:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\.|$)/iu;export class RunManifestCollisionError extends Error{code="TESTKIT_RUN_MANIFEST_COLLISION";manifestPath;constructor(n,e){super(`Testkit run manifest already exists at ${n}`,e),this.name="RunManifestCollisionError",this.manifestPath=n}}export class RunManifestInventoryError extends Error{code="TESTKIT_RUN_MANIFEST_INVENTORY_FAILED";manifestPath;constructor(n,e,r){super(`Cannot inventory Testkit run manifest ${n}: ${e}`,r),this.name="RunManifestInventoryError",this.manifestPath=n}}export function runManifestPath(t,n){const e=requireSafeRunId(n,"Testkit run id");return l.join(m(t),`${e}.json`)}export function writeRunManifest(t,n){const e=runManifestPath(t,n.runId);i.mkdirSync(m(t),{recursive:!0}),S(e,n)}export function prepareRunShutdownDeadlines(t,n){return{...t,services:t.services.map(e=>e.shutdownTimeoutMs===void 0?e:{...e,shutdownDeadlineAt:Math.min(e.shutdownDeadlineAt??Number.MAX_VALUE,Date.parse(n)+e.shutdownTimeoutMs)})}}export function persistRunShutdownDeadlines(t,n){S(runManifestPath(t,n.runId),n)}export function createRunManifest(t,n){const e=runManifestPath(t,n.runId),r=m(t);i.mkdirSync(r,{recursive:!0});const s=l.join(r,`.${l.basename(e)}.${process.pid.toString()}.${F()}.tmp`);i.writeFileSync(s,O(n,`run manifest ${n.runId}`),{flag:"wx"});try{process.platform==="win32"?D(s,e):i.linkSync(s,e)}catch(o){throw x(o,"EEXIST")?new RunManifestCollisionError(e,{cause:o}):o}finally{i.rmSync(s,{force:!0})}}export function writeRunMaintenanceStopRequest(t,n){const e=R(t,n.runId);i.mkdirSync(m(t),{recursive:!0}),S(e,n)}export function readRunMaintenanceStopRequest(t,n){const e=requireSafeRunId(n,"Testkit run id"),r=R(t,e);let s;try{s=JSON.parse(i.readFileSync(r,"utf8"))}catch{return null}const o=z(s);return o?.runId===e?o:null}export function removeRunMaintenanceStopRequest(t,n){i.rmSync(R(t,n),{force:!0})}export function removeRunManifestFile(t,n){const e=runManifestPath(t,n),r=m(t);i.rmSync(e,{force:!0}),removeRunMaintenanceStopRequest(t,n),i.existsSync(r)&&i.readdirSync(r).length===0&&i.rmSync(r,{recursive:!0,force:!0})}export function loadRunManifest(t,n){const e=runManifestPath(t,n);I("runs.manifestLookups");let r;try{r=i.lstatSync(e)}catch(s){if(x(s,"ENOENT"))return null;throw new RunManifestInventoryError(e,"manifest path could not be inspected",{cause:s})}if(!r.isFile())throw new RunManifestInventoryError(e,"manifest path is not a regular file");return M(e)}export function listRunManifests(t){const n=m(t);if(!i.existsSync(n))return[];const e=i.readdirSync(n,{withFileTypes:!0});return I("runs.manifestDirectoriesRead"),I("runs.manifestEntriesVisited",e.length),e.filter(r=>r.name.endsWith(".json")).map(r=>{const s=l.join(n,r.name);if(!r.isFile())throw new RunManifestInventoryError(s,"manifest path is not a regular file");return M(s)}).sort((r,s)=>(r.startedAt||"").localeCompare(s.startedAt||""))}export function requireSafeRunId(t,n){const e=N(t);if(e!==null)return e;throw new Error(`${n} must be 1-${T.toString()} characters using only ASCII letters, digits, dot, underscore, or hyphen, must start with a letter or digit, and must not use a reserved Windows device name`)}export function normalizeRunManifest(t){if(!p(t)||t.schemaVersion!==h)return null;const n=N(t.runId);if(n===null)return null;const e=t.controller,r=t.finishedAt,s=t.heartbeatAt,o=_(t.incarnationId),u=t.interruptReason,a=t.pid,d=t.processIdentity,c=t.session,f=t.startedAt,y=t.status,w=V(t.services),E=J(t.runtimeDirs);return typeof a!="number"||!Number.isSafeInteger(a)||a<0||o===null||w===null||E===null||a===0&&d!==null||a>0&&(typeof d!="string"||d.length===0)?null:{incarnationId:o,runId:n,runtimeDirs:E,schemaVersion:h,services:w,pid:a,processIdentity:typeof d=="string"?d:null,...G(e)?{controller:e}:{},...typeof r=="string"?{finishedAt:r}:{},...A(s)?{heartbeatAt:s}:{},...A(u)?{interruptReason:u}:{},...X(c)?{session:c}:{},...typeof f=="string"?{startedAt:f}:{},...typeof y=="string"?{status:y}:{}}}function R(t,n){const e=requireSafeRunId(n,"Testkit run id");return l.join(m(t),`${e}${j}`)}function z(t){if(!p(t)||t.schemaVersion!==g)return null;const n=N(t.runId),e=_(t.targetIncarnationId);return n===null||e===null||!P(t.coordinatorPid)||typeof t.coordinatorProcessIdentity!="string"||t.coordinatorProcessIdentity.length===0||typeof t.reason!="string"||t.reason.length===0||typeof t.requestedAt!="string"||!Number.isFinite(Date.parse(t.requestedAt))||!P(t.targetPid)||typeof t.targetProcessIdentity!="string"||t.targetProcessIdentity.length===0?null:{coordinatorPid:t.coordinatorPid,coordinatorProcessIdentity:t.coordinatorProcessIdentity,reason:t.reason,requestedAt:t.requestedAt,runId:n,schemaVersion:g,targetIncarnationId:e,targetPid:t.targetPid,targetProcessIdentity:t.targetProcessIdentity}}function M(t){let n;try{if($()){const u=i.readFileSync(t);I("runs.manifestFilesRead"),I("runs.manifestBytesRead",u.byteLength),n=u.toString("utf8")}else n=i.readFileSync(t,"utf8")}catch(u){throw new RunManifestInventoryError(t,"file could not be read",{cause:u})}let e;try{e=JSON.parse(n)}catch(u){throw new RunManifestInventoryError(t,"file is not valid JSON",{cause:u})}const r=normalizeRunManifest(e);if(r!==null){const u=l.basename(t,".json");if(r.runId!==u)throw new RunManifestInventoryError(t,"embedded runId does not match the manifest filename");return r}const s=p(e)?e.schemaVersion:void 0,o=s===h?`schema-v${h.toString()} record is invalid`:`unsupported schema version ${typeof s=="number"?s.toString():"missing"}`;throw new RunManifestInventoryError(t,o)}function N(t){return typeof t=="string"&&t.length<=T&&C.test(t)&&!L.test(t)?t:null}function _(t){return typeof t=="string"&&k.test(t)?t:null}function V(t){if(!Array.isArray(t))return null;const n=[];for(const e of t){const r=q(e);if(r===null)return null;n.push(r)}return n}function q(t){if(!p(t))return null;const n=t.command,e=t.cwd,r=t.pid,s=t.processIdentity,o=t.processGroupId,u=t.runtimeLabel,a=t.serviceName,d=t.startedAt,c=t.shutdownTimeoutMs,f=t.shutdownDeadlineAt;if(c!==void 0&&(typeof c!="number"||!Number.isFinite(c)||!Number.isInteger(c)||c<=0)||f!=null&&(c===void 0||typeof f!="number"||!Number.isFinite(f)||f<0))return null;const y=W(t.ports);return typeof r!="number"||!Number.isSafeInteger(r)||r<=0||o!==null&&(typeof o!="number"||!Number.isSafeInteger(o)||o!==r)||typeof s!="string"||s.length===0||y===null?null:{...c===void 0?{}:{shutdownTimeoutMs:c},...f===void 0?{}:{shutdownDeadlineAt:f},ports:y,pid:r,processGroupId:o,processIdentity:s,...A(n)?{command:n}:{},...typeof e=="string"?{cwd:e}:{},...typeof u=="string"?{runtimeLabel:u}:{},...typeof a=="string"?{serviceName:a}:{},...typeof d=="string"?{startedAt:d}:{}}}function W(t){if(!Array.isArray(t))return null;const n=[];for(const e of t){if(!p(e))return null;const r=e.host,s=e.port;if(typeof r!="string"||typeof s!="number"||!Number.isSafeInteger(s)||s<=0||s>65535)return null;n.push({...e,host:r,port:s})}return n}function J(t){return!Array.isArray(t)||!t.every(n=>typeof n=="string")?null:t}function P(t){return typeof t=="number"&&Number.isSafeInteger(t)&&t>0}function m(t){return l.join(t,U)}function p(t){return typeof t=="object"&&t!==null&&!Array.isArray(t)}function X(t){return t===null||p(t)}function G(t){return t===null?!0:p(t)?Object.values(t).every(n=>n===null||typeof n=="string"):!1}function A(t){return t===null||typeof t=="string"}function x(t,n){return(typeof t=="object"||typeof t=="function")&&t!==null&&Reflect.get(t,"code")===n}const b=String;export function formatRunSummary(t){const n=normalizeRunManifest(t);if(!n)return b(t);const e=[...new Set(n.services.map(r=>r.runtimeLabel).filter(r=>!!r))];return`${b(n.runId)} pid=${b(n.pid)}${e.length>0?` runtimes=${e.join(",")}`:""}`}
@@ -10,4 +10,4 @@ export declare function normalizeServiceStartCommand(command: string): string;
10
10
  export declare function prepareServiceStartInvocation(command: string, env: Readonly<NodeJS.ProcessEnv>, platform?: NodeJS.Platform): ServiceStartInvocation;
11
11
  export declare function terminateAttributedProcessLineage(pid: unknown, signal: ProcessSignal, expectedCreationIdentity?: string): Promise<void>;
12
12
  export declare function terminateOwnedPosixProcessGroup(processGroupId: unknown, leaderCreationIdentity: string, graceMs: number): Promise<void>;
13
- export declare function sleep(ms: number): Promise<void>;
13
+ export declare function sleep(ms: number, signal?: AbortSignal): Promise<void>;
@@ -1 +1 @@
1
- import{captureProcessCreationIdentity as a,processCreationIdentityMatches as c,resolvePosixCommandShell as m,terminateAttributedProcessLineage as d,terminateOwnedPosixProcessGroup as u}from"./index.js";export{a as captureProcessCreationIdentity,c as processCreationIdentityMatches};export function normalizeServiceStartCommand(t){const r=t.trim();if(r.length===0)throw new Error("Service start command must not be empty");return r}export function prepareServiceStartInvocation(t,r,o=process.platform){const e=normalizeServiceStartCommand(t);if(o==="win32"){const n=e.replace(/^exec(?:\s+|$)/u,"").trim();if(n.length===0)throw new Error("Service start command must not contain only exec");return{executable:r.ComSpec||process.env.ComSpec||"cmd.exe",args:["/d","/s","/c",`"${n}"`],windowsVerbatimArguments:!0}}const s=m(r),i=/^exec(?:\s|$)/u.test(e)?e:`exec ${e}`;return{executable:s,args:["-lc",i]}}export async function terminateAttributedProcessLineage(t,r,o){const e=Number(t);!Number.isInteger(e)||e<=0||await d({displayLabel:"Testkit application service",...o===void 0?{}:{expectedCreationIdentity:o},pid:e,signal:r})}export async function terminateOwnedPosixProcessGroup(t,r,o){const e=Number(t);if(!Number.isSafeInteger(e)||e<=0)throw new Error("Owned POSIX process-group id must be a positive safe integer");await u({displayLabel:"Testkit application service",graceMs:o,leaderCreationIdentity:r,leaderPid:e,processGroupId:e})}export function sleep(t){return new Promise(r=>setTimeout(r,t))}
1
+ import{captureProcessCreationIdentity as a,processCreationIdentityMatches as c,resolvePosixCommandShell as m,terminateAttributedProcessLineage as d,terminateOwnedPosixProcessGroup as u}from"./index.js";export{a as captureProcessCreationIdentity,c as processCreationIdentityMatches};export function normalizeServiceStartCommand(t){const r=t.trim();if(r.length===0)throw new Error("Service start command must not be empty");return r}export function prepareServiceStartInvocation(t,r,o=process.platform){const e=normalizeServiceStartCommand(t);if(o==="win32"){const i=e.replace(/^exec(?:\s+|$)/u,"").trim();if(i.length===0)throw new Error("Service start command must not contain only exec");return{executable:r.ComSpec||process.env.ComSpec||"cmd.exe",args:["/d","/s","/c",`"${i}"`],windowsVerbatimArguments:!0}}const n=m(r),s=/^exec(?:\s|$)/u.test(e)?e:`exec ${e}`;return{executable:n,args:["-lc",s]}}export async function terminateAttributedProcessLineage(t,r,o){const e=Number(t);!Number.isInteger(e)||e<=0||await d({displayLabel:"Testkit application service",...o===void 0?{}:{expectedCreationIdentity:o},pid:e,signal:r})}export async function terminateOwnedPosixProcessGroup(t,r,o){const e=Number(t);if(!Number.isSafeInteger(e)||e<=0)throw new Error("Owned POSIX process-group id must be a positive safe integer");await u({displayLabel:"Testkit application service",graceMs:o,leaderCreationIdentity:r,leaderPid:e,processGroupId:e})}export function sleep(t,r){return new Promise(o=>{const e=()=>{clearTimeout(n),r?.removeEventListener("abort",e),o()},n=setTimeout(e,t);r?.addEventListener("abort",e,{once:!0}),r?.aborted&&e()})}
@@ -1,4 +1,4 @@
1
- import _ from"crypto";import f from"fs";import u from"path";import{prepareCommandForSpawn as I,resolveCommandForSpawn as A}from"../process/host/platform.js";import{normalizeContainedRelativePath as V,resolveContainedPath as D}from"../process/files/contained-path.js";import{deriveSemverMinimum as k,isExactSemver as L,normalizeSemver as p,satisfiesSemver as g}from"./semver.js";import{managedHostProcessInfrastructureError as M,runManagedHostProcess as j}from"../process/index.js";import{managedNodeRelease as H}from"./managed-node-releases.js";import{provisionNodeToolchainWithRelease as R}from"./node-provisioning.js";import{createSharedToolchainOperation as F,waitForSharedToolchainOperation as P}from"./shared-operation.js";import{TOOLCHAIN_METADATA_TIMEOUT_MS as $}from"./timeouts.js";const z=".",W="auto",q="require-host",U=600*1e3,B=['"use strict";','const { spawn } = require("node:child_process");',"const [command, ...args] = process.argv.slice(1);",'if (typeof command !== "string" || command.length === 0) {',' process.stderr.write("Missing Windows toolchain command\\n");'," process.exitCode = 126;","} else {"," let spawnFailed = false;"," const child = spawn(command, args, {"," shell: false,",' stdio: "inherit",'," windowsHide: true,"," windowsVerbatimArguments: true,"," });",' child.once("error", (error) => {'," spawnFailed = true;"," process.stderr.write(`${error.stack ?? String(error)}\\n`);"," process.exitCode = 126;"," });",' child.once("close", (code, signal) => {'," if (spawnFailed) return;"," if (signal !== null) {"," process.stderr.write(`Windows toolchain command exited on signal ${signal}\\n`);"," process.exitCode = 1;"," return;"," }"," process.exitCode = code ?? 1;"," });","}"].join(`
2
- `),T=new WeakMap,O=new WeakSet;export function nodeToolchain(n={}){return{...n}}export function normalizeToolchainRegistry(n){if(n==null)return{};if(!N(n))throw new Error("testkit.config.ts toolchains must be an object");const e={};for(const[o,r]of Object.entries(n))e[o]=x(r,`testkit.config.ts toolchains.${o}`);return e}export function normalizeRuntimeToolchain(n,e,o={}){if(n==null)return null;if(typeof n=="string"){const t=n.trim();if(!t)throw new Error(`${e} must reference a non-empty toolchain name`);const i=o[t];if(!i)throw new Error(`${e} references unknown toolchain "${t}"`);return{...i,refName:t,sourceLabel:`${e} -> ${t}`}}return{...x(n,e),refName:null,sourceLabel:e}}export async function resolveConfiguredToolchain(n,e={}){const o=n?.testkit?.runtime?.toolchain;if(!n||!o)return null;let r=T.get(n);if(r===void 0){r=F(i=>J(n,o,{...e,signal:i})),T.set(n,r);const t=r;r.promise.catch(()=>{T.get(n)===t&&T.delete(n)})}return P(r,e.signal)}export function announceResolvedToolchain(n,e,o=null){return!e||O.has(n)||(O.add(n),o?.toolchainResolved&&o.toolchainResolved(n,e)),Promise.resolve()}export function applyToolchainEnv(n,e,o=process.env){if(!e)return{...n};const r={...n,PATH:ne(e.binDir,n.PATH||o.PATH||""),TESTKIT_TOOLCHAIN_KIND:"node",TESTKIT_TOOLCHAIN_NODE_VERSION:e.nodeVersion,TESTKIT_TOOLCHAIN_NODE_SOURCE:e.nodeSource,TESTKIT_TOOLCHAIN_INSTALL_MODE:e.install};return e.npmVersion&&(r.TESTKIT_TOOLCHAIN_NPM_VERSION=e.npmVersion,r.TESTKIT_TOOLCHAIN_NPM_SOURCE=e.npmSource),r}function x(n,e){if(!N(n))throw new Error(`${e} must be an object`);const o=V(d(n.cwd)||z,`${e}.cwd`),r=d(n.detect)||W,t=d(n.install)||q;if(r!=="auto"&&r!=="off")throw new Error(`${e}.detect must be "auto" or "off"`);if(t!=="require-host"&&t!=="download")throw new Error(`${e}.install must be "require-host" or "download"`);return{cwd:o,detect:r,install:t,node:d(n.node),npm:d(n.npm)}}async function J(n,e,o){const r=n.productDir,t=D(r,e.cwd,`${e.sourceLabel}.cwd`),i=K(t),s=e.node||(e.detect==="auto"?i.nodeSpec:null),a=e.node?"toolchain.node":e.detect==="auto"?i.nodeSource??"none":"none",l=e.npm||(e.detect==="auto"?i.npmSpec:null),c=e.npm?"toolchain.npm":e.detect==="auto"?i.npmSource??"none":"none";if(!s&&!l)throw new Error(`Service "${n.name}" enables runtime.toolchain but no Node/npm version could be resolved in ${e.cwd}`);return e.install==="require-host"?G({config:n,cwd:t,nodeSpec:s,nodeSource:a,npmSpec:l,npmSource:c,toolchain:e,options:o}):Q({config:n,cwd:t,nodeSpec:s,nodeSource:a,npmSpec:l,npmSource:c,toolchain:e,options:o})}function K(n){const e=u.join(n,"package.json"),o=Z(e),r=ee(u.join(n,".tool-versions")),t=b(u.join(n,".nvmrc")),i=b(u.join(n,".node-version")),s=d(w(o,"volta","node")),a=d(w(o,"engines","node")),l=d(r.nodejs),c=s||t||i||l||a;let m=null;s?m="package.json#volta.node":t?m=".nvmrc":i?m=".node-version":l?m=".tool-versions#nodejs":a&&(m="package.json#engines.node");const y=d(w(o,"volta","npm")),E=d(w(o,"engines","npm")),v=Y(w(o,"packageManager")),C=y||(v?.name==="npm"?v.version:null)||E;let h=null;return y?h="package.json#volta.npm":v?.name==="npm"?h="package.json#packageManager":E&&(h="package.json#engines.npm"),{nodeSpec:c,nodeSource:m,npmSpec:C,npmSource:h}}async function G({config:n,cwd:e,nodeSpec:o,nodeSource:r,npmSpec:t,npmSource:i,toolchain:s,options:a}){const l=p(a.hostNodeVersion||process.versions.node);if(o&&!g(l,o))throw new Error(`Service "${n.name}" requires Node ${o} (${r}), but host Node is ${l}`);let c=null;if(t&&(c=p(a.hostNpmVersion||await X({command:"npm",args:["--version"],cwd:e,env:process.env,commandExecutor:a.commandExecutor,signal:a.signal})),!g(c,t)))throw new Error(`Service "${n.name}" requires npm ${t} (${i}), but host npm is ${c}`);return{install:s.install,cwd:s.cwd,nodeVersion:l,nodeSource:r,nodeSpec:o,npmVersion:c,npmSource:i,npmSpec:t,nodeExecutable:process.execPath,binDir:u.dirname(process.execPath),toolchainDir:u.dirname(process.execPath),fingerprint:S({install:s.install,nodeVersion:l,npmVersion:c,nodeSource:r,npmSource:i}),summary:`${s.install} node=${l}${c?` npm=${c}`:""} (${r})`}}async function Q({config:n,nodeSpec:e,nodeSource:o,npmSpec:r,npmSource:t,toolchain:i,options:s}){if(!e)throw new Error(`Service "${n.name}" download toolchain mode requires a resolvable Node version`);const a=L(e)?p(e):k(e);if(!a)throw new Error(`Service "${n.name}" cannot deterministically provision Node from "${e}". Provide an exact version in runtime.toolchain.node, .nvmrc, .node-version, .tool-versions, or package.json#volta.node.`);const l=await provisionNodeToolchain({productDir:n.productDir,version:a,fetchImpl:s.fetchImpl||fetchToolchain,commandExecutor:s.commandExecutor||executeToolchainCommand,signal:s.signal});if(!g(a,e))throw new Error(`Service "${n.name}" resolved Node ${a}, which does not satisfy ${e}`);const c=l.npmVersion?p(l.npmVersion):null;if(r&&(!c||!g(c,r)))throw new Error(`Service "${n.name}" requires npm ${r} (${t}), but Node ${a} bundles npm ${c||"unknown"}`);return{install:i.install,cwd:i.cwd,nodeVersion:a,nodeSource:o,nodeSpec:e,npmVersion:c,npmSource:t,npmSpec:r,nodeExecutable:l.nodeExecutable,binDir:l.binDir,toolchainDir:l.toolchainDir,fingerprint:S({install:i.install,nodeVersion:a,npmVersion:c,nodeSource:o,npmSource:t}),summary:`download node=${a}${c?` npm=${c}`:""} (${o})`}}export async function provisionNodeToolchain({productDir:n,version:e,fetchImpl:o=fetchToolchain,commandExecutor:r=executeToolchainCommand,signal:t}){const i=p(e);if(e!==i)throw new Error(`Downloaded Node toolchain version must use canonical x.y.z form; received ${JSON.stringify(e)}`);const s=H(i);return R({commandExecutor:r,fetchImpl:o,productDir:n,release:s,signal:t})}function S(n){return _.createHash("sha256").update(JSON.stringify({kind:"node",...n})).digest("hex")}async function X({command:n,args:e,cwd:o,env:r,commandExecutor:t=executeToolchainCommand,signal:i}){const s=await t(n,e,{cwd:o,env:r,signal:i,stdio:"pipe",timeoutMs:$,windowsHide:!0});return s.stdout==null?"":typeof s.stdout=="string"?s.stdout.trim():Buffer.from(s.stdout).toString().trim()}function Y(n){const e=d(n);if(!e)return null;const o=e.lastIndexOf("@");return o<=0||o===e.length-1?null:{name:e.slice(0,o),version:e.slice(o+1)}}function Z(n){if(!f.existsSync(n))return null;try{return JSON.parse(f.readFileSync(n,"utf8"))}catch{return null}}function b(n){if(!f.existsSync(n))return null;const o=f.readFileSync(n,"utf8").split(/\r?\n/).find(r=>r.trim().length>0);return o?o.trim():null}function ee(n){if(!f.existsSync(n))return{};const e={};for(const o of f.readFileSync(n,"utf8").split(/\r?\n/)){const r=o.trim();if(!r||r.startsWith("#"))continue;const[t,i]=r.split(/\s+/,2);t&&i&&(e[t]=i.trim())}return e}function ne(n,e){const o=d(n);if(!o)return e;const r=(e||"").split(u.delimiter).filter(Boolean).filter(t=>t!==o);return[o,...r].join(u.delimiter)}function d(n){if(typeof n!="string")return null;const e=n.trim();return e.length>0?e:null}export async function fetchToolchain(n,e={}){const o=AbortSignal.timeout(U),r=await fetch(n,{signal:e.signal===void 0?o:AbortSignal.any([e.signal,o])});return{arrayBuffer:()=>r.arrayBuffer(),body:r.body===null?null:oe(r.body),ok:r.ok,status:r.status}}async function*oe(n){const e=n.getReader();let o=!1;try{for(;;){const r=await e.read();if(r.done){o=!0;return}yield r.value}}finally{o||await e.cancel(),e.releaseLock()}}export function prepareToolchainCommandForManagedHost(n,e,o={}){const r={env:o.env||process.env,platform:o.platform},t=A(n,r),i=I(t,e,r);return i.windowsVerbatimArguments?(re(t,e),{executable:o.hostNodeExecutable||process.execPath,args:["--input-type=commonjs","--eval",B,"--",i.command,...i.args]}):{executable:i.command,args:i.args}}function re(n,e){if(/[%!"\0\r\n]/u.test(n))throw new Error(`Windows batch toolchain commands cannot safely preserve command path ${JSON.stringify(n)}`);const o=e.find(r=>/[%!"\0\r\n]/u.test(r)||r.endsWith("\\"));if(o!==void 0)throw new Error(`Windows batch toolchain commands cannot safely preserve argument ${JSON.stringify(o)}`)}export async function executeToolchainCommand(n,e,o={}){const r=te(o.timeoutMs),t=u.resolve(o.cwd||process.cwd()),i=prepareToolchainCommandForManagedHost(n,e,{env:o.env||process.env}),s=await j({args:i.args,cwd:t,displayLabel:`Toolchain command ${n}`,env:o.env||process.env,executable:i.executable,signal:o.signal,timeoutMs:r}),a=M(`Toolchain command ${n}`,s);if(a)throw a;if(s.exitCode!==0)throw new Error(`Toolchain command ${n} exited with code ${String(s.exitCode)}${ie(s.stdout,s.stderr)}`);return{stdout:s.stdout}}function te(n){const e=n??$;if(!Number.isSafeInteger(e)||e<=0)throw new Error("Toolchain command timeout must be a positive safe integer in milliseconds");return e}function ie(n,e){const o=[n,e].map(r=>r.trim()).filter(Boolean).join(`
1
+ import _ from"crypto";import f from"fs";import u from"path";import{prepareCommandForSpawn as I,resolveCommandForSpawn as A}from"../process/host/platform.js";import{normalizeContainedRelativePath as V,resolveContainedPath as D}from"../process/files/contained-path.js";import{deriveSemverMinimum as k,isExactSemver as L,normalizeSemver as p,satisfiesSemver as g}from"./semver.js";import{managedHostProcessInfrastructureError as M,runManagedHostProcess as j}from"../process/index.js";import{managedNodeRelease as H}from"./managed-node-releases.js";import{createSharedToolchainOperation as R,waitForSharedToolchainOperation as F}from"./shared-operation.js";import{TOOLCHAIN_METADATA_TIMEOUT_MS as $}from"./timeouts.js";const P=".",z="auto",W="require-host",q=600*1e3,U=['"use strict";','const { spawn } = require("node:child_process");',"const [command, ...args] = process.argv.slice(1);",'if (typeof command !== "string" || command.length === 0) {',' process.stderr.write("Missing Windows toolchain command\\n");'," process.exitCode = 126;","} else {"," let spawnFailed = false;"," const child = spawn(command, args, {"," shell: false,",' stdio: "inherit",'," windowsHide: true,"," windowsVerbatimArguments: true,"," });",' child.once("error", (error) => {'," spawnFailed = true;"," process.stderr.write(`${error.stack ?? String(error)}\\n`);"," process.exitCode = 126;"," });",' child.once("close", (code, signal) => {'," if (spawnFailed) return;"," if (signal !== null) {"," process.stderr.write(`Windows toolchain command exited on signal ${signal}\\n`);"," process.exitCode = 1;"," return;"," }"," process.exitCode = code ?? 1;"," });","}"].join(`
2
+ `),T=new WeakMap,O=new WeakSet;export function nodeToolchain(n={}){return{...n}}export function normalizeToolchainRegistry(n){if(n==null)return{};if(!N(n))throw new Error("testkit.config.ts toolchains must be an object");const e={};for(const[o,r]of Object.entries(n))e[o]=x(r,`testkit.config.ts toolchains.${o}`);return e}export function normalizeRuntimeToolchain(n,e,o={}){if(n==null)return null;if(typeof n=="string"){const t=n.trim();if(!t)throw new Error(`${e} must reference a non-empty toolchain name`);const i=o[t];if(!i)throw new Error(`${e} references unknown toolchain "${t}"`);return{...i,refName:t,sourceLabel:`${e} -> ${t}`}}return{...x(n,e),refName:null,sourceLabel:e}}export async function resolveConfiguredToolchain(n,e={}){const o=n?.testkit?.runtime?.toolchain;if(!n||!o)return null;let r=T.get(n);if(r===void 0){r=R(i=>B(n,o,{...e,signal:i})),T.set(n,r);const t=r;r.promise.catch(()=>{T.get(n)===t&&T.delete(n)})}return F(r,e.signal)}export function announceResolvedToolchain(n,e,o=null){return!e||O.has(n)||(O.add(n),o?.toolchainResolved&&o.toolchainResolved(n,e)),Promise.resolve()}export function applyToolchainEnv(n,e,o=process.env){if(!e)return{...n};const r={...n,PATH:ee(e.binDir,n.PATH||o.PATH||""),TESTKIT_TOOLCHAIN_KIND:"node",TESTKIT_TOOLCHAIN_NODE_VERSION:e.nodeVersion,TESTKIT_TOOLCHAIN_NODE_SOURCE:e.nodeSource,TESTKIT_TOOLCHAIN_INSTALL_MODE:e.install};return e.npmVersion&&(r.TESTKIT_TOOLCHAIN_NPM_VERSION=e.npmVersion,r.TESTKIT_TOOLCHAIN_NPM_SOURCE=e.npmSource),r}function x(n,e){if(!N(n))throw new Error(`${e} must be an object`);const o=V(d(n.cwd)||P,`${e}.cwd`),r=d(n.detect)||z,t=d(n.install)||W;if(r!=="auto"&&r!=="off")throw new Error(`${e}.detect must be "auto" or "off"`);if(t!=="require-host"&&t!=="download")throw new Error(`${e}.install must be "require-host" or "download"`);return{cwd:o,detect:r,install:t,node:d(n.node),npm:d(n.npm)}}async function B(n,e,o){const r=n.productDir,t=D(r,e.cwd,`${e.sourceLabel}.cwd`),i=J(t),s=e.node||(e.detect==="auto"?i.nodeSpec:null),a=e.node?"toolchain.node":e.detect==="auto"?i.nodeSource??"none":"none",l=e.npm||(e.detect==="auto"?i.npmSpec:null),c=e.npm?"toolchain.npm":e.detect==="auto"?i.npmSource??"none":"none";if(!s&&!l)throw new Error(`Service "${n.name}" enables runtime.toolchain but no Node/npm version could be resolved in ${e.cwd}`);return e.install==="require-host"?K({config:n,cwd:t,nodeSpec:s,nodeSource:a,npmSpec:l,npmSource:c,toolchain:e,options:o}):G({config:n,cwd:t,nodeSpec:s,nodeSource:a,npmSpec:l,npmSource:c,toolchain:e,options:o})}function J(n){const e=u.join(n,"package.json"),o=Y(e),r=Z(u.join(n,".tool-versions")),t=b(u.join(n,".nvmrc")),i=b(u.join(n,".node-version")),s=d(w(o,"volta","node")),a=d(w(o,"engines","node")),l=d(r.nodejs),c=s||t||i||l||a;let m=null;s?m="package.json#volta.node":t?m=".nvmrc":i?m=".node-version":l?m=".tool-versions#nodejs":a&&(m="package.json#engines.node");const y=d(w(o,"volta","npm")),E=d(w(o,"engines","npm")),v=X(w(o,"packageManager")),C=y||(v?.name==="npm"?v.version:null)||E;let h=null;return y?h="package.json#volta.npm":v?.name==="npm"?h="package.json#packageManager":E&&(h="package.json#engines.npm"),{nodeSpec:c,nodeSource:m,npmSpec:C,npmSource:h}}async function K({config:n,cwd:e,nodeSpec:o,nodeSource:r,npmSpec:t,npmSource:i,toolchain:s,options:a}){const l=p(a.hostNodeVersion||process.versions.node);if(o&&!g(l,o))throw new Error(`Service "${n.name}" requires Node ${o} (${r}), but host Node is ${l}`);let c=null;if(t&&(c=p(a.hostNpmVersion||await Q({command:"npm",args:["--version"],cwd:e,env:process.env,commandExecutor:a.commandExecutor,signal:a.signal})),!g(c,t)))throw new Error(`Service "${n.name}" requires npm ${t} (${i}), but host npm is ${c}`);return{install:s.install,cwd:s.cwd,nodeVersion:l,nodeSource:r,nodeSpec:o,npmVersion:c,npmSource:i,npmSpec:t,nodeExecutable:process.execPath,binDir:u.dirname(process.execPath),toolchainDir:u.dirname(process.execPath),fingerprint:S({install:s.install,nodeVersion:l,npmVersion:c,nodeSource:r,npmSource:i}),summary:`${s.install} node=${l}${c?` npm=${c}`:""} (${r})`}}async function G({config:n,nodeSpec:e,nodeSource:o,npmSpec:r,npmSource:t,toolchain:i,options:s}){if(!e)throw new Error(`Service "${n.name}" download toolchain mode requires a resolvable Node version`);const a=L(e)?p(e):k(e);if(!a)throw new Error(`Service "${n.name}" cannot deterministically provision Node from "${e}". Provide an exact version in runtime.toolchain.node, .nvmrc, .node-version, .tool-versions, or package.json#volta.node.`);const l=await provisionNodeToolchain({productDir:n.productDir,version:a,fetchImpl:s.fetchImpl||fetchToolchain,commandExecutor:s.commandExecutor||executeToolchainCommand,signal:s.signal});if(!g(a,e))throw new Error(`Service "${n.name}" resolved Node ${a}, which does not satisfy ${e}`);const c=l.npmVersion?p(l.npmVersion):null;if(r&&(!c||!g(c,r)))throw new Error(`Service "${n.name}" requires npm ${r} (${t}), but Node ${a} bundles npm ${c||"unknown"}`);return{install:i.install,cwd:i.cwd,nodeVersion:a,nodeSource:o,nodeSpec:e,npmVersion:c,npmSource:t,npmSpec:r,nodeExecutable:l.nodeExecutable,binDir:l.binDir,toolchainDir:l.toolchainDir,fingerprint:S({install:i.install,nodeVersion:a,npmVersion:c,nodeSource:o,npmSource:t}),summary:`download node=${a}${c?` npm=${c}`:""} (${o})`}}export async function provisionNodeToolchain({productDir:n,version:e,fetchImpl:o=fetchToolchain,commandExecutor:r=executeToolchainCommand,signal:t}){const i=p(e);if(e!==i)throw new Error(`Downloaded Node toolchain version must use canonical x.y.z form; received ${JSON.stringify(e)}`);const s=H(i),{provisionNodeToolchainWithRelease:a}=await import("./node-provisioning.js");return a({commandExecutor:r,fetchImpl:o,productDir:n,release:s,signal:t})}function S(n){return _.createHash("sha256").update(JSON.stringify({kind:"node",...n})).digest("hex")}async function Q({command:n,args:e,cwd:o,env:r,commandExecutor:t=executeToolchainCommand,signal:i}){const s=await t(n,e,{cwd:o,env:r,signal:i,stdio:"pipe",timeoutMs:$,windowsHide:!0});return s.stdout==null?"":typeof s.stdout=="string"?s.stdout.trim():Buffer.from(s.stdout).toString().trim()}function X(n){const e=d(n);if(!e)return null;const o=e.lastIndexOf("@");return o<=0||o===e.length-1?null:{name:e.slice(0,o),version:e.slice(o+1)}}function Y(n){if(!f.existsSync(n))return null;try{return JSON.parse(f.readFileSync(n,"utf8"))}catch{return null}}function b(n){if(!f.existsSync(n))return null;const o=f.readFileSync(n,"utf8").split(/\r?\n/).find(r=>r.trim().length>0);return o?o.trim():null}function Z(n){if(!f.existsSync(n))return{};const e={};for(const o of f.readFileSync(n,"utf8").split(/\r?\n/)){const r=o.trim();if(!r||r.startsWith("#"))continue;const[t,i]=r.split(/\s+/,2);t&&i&&(e[t]=i.trim())}return e}function ee(n,e){const o=d(n);if(!o)return e;const r=(e||"").split(u.delimiter).filter(Boolean).filter(t=>t!==o);return[o,...r].join(u.delimiter)}function d(n){if(typeof n!="string")return null;const e=n.trim();return e.length>0?e:null}export async function fetchToolchain(n,e={}){const o=AbortSignal.timeout(q),r=await fetch(n,{signal:e.signal===void 0?o:AbortSignal.any([e.signal,o])});return{arrayBuffer:()=>r.arrayBuffer(),body:r.body===null?null:ne(r.body),ok:r.ok,status:r.status}}async function*ne(n){const e=n.getReader();let o=!1;try{for(;;){const r=await e.read();if(r.done){o=!0;return}yield r.value}}finally{o||await e.cancel(),e.releaseLock()}}export function prepareToolchainCommandForManagedHost(n,e,o={}){const r={env:o.env||process.env,platform:o.platform},t=A(n,r),i=I(t,e,r);return i.windowsVerbatimArguments?(oe(t,e),{executable:o.hostNodeExecutable||process.execPath,args:["--input-type=commonjs","--eval",U,"--",i.command,...i.args]}):{executable:i.command,args:i.args}}function oe(n,e){if(/[%!"\0\r\n]/u.test(n))throw new Error(`Windows batch toolchain commands cannot safely preserve command path ${JSON.stringify(n)}`);const o=e.find(r=>/[%!"\0\r\n]/u.test(r)||r.endsWith("\\"));if(o!==void 0)throw new Error(`Windows batch toolchain commands cannot safely preserve argument ${JSON.stringify(o)}`)}export async function executeToolchainCommand(n,e,o={}){const r=re(o.timeoutMs),t=u.resolve(o.cwd||process.cwd()),i=prepareToolchainCommandForManagedHost(n,e,{env:o.env||process.env}),s=await j({args:i.args,cwd:t,displayLabel:`Toolchain command ${n}`,env:o.env||process.env,executable:i.executable,signal:o.signal,timeoutMs:r}),a=M(`Toolchain command ${n}`,s);if(a)throw a;if(s.exitCode!==0)throw new Error(`Toolchain command ${n} exited with code ${String(s.exitCode)}${te(s.stdout,s.stderr)}`);return{stdout:s.stdout}}function re(n){const e=n??$;if(!Number.isSafeInteger(e)||e<=0)throw new Error("Toolchain command timeout must be a positive safe integer in milliseconds");return e}function te(n,e){const o=[n,e].map(r=>r.trim()).filter(Boolean).join(`
3
3
  `);return o?`
4
4
  ${o.slice(-4e3)}`:""}function w(n,...e){let o=n;for(const r of e){if(!N(o))return;o=o[r]}return o}function N(n){return n!==null&&typeof n=="object"&&!Array.isArray(n)}
@@ -522,7 +522,10 @@ installation.
522
522
  Node/Vitest resolves Vitest, Vite, TypeScript, and esbuild through this anchor.
523
523
  Generated configuration, authoring aliases, helpers, and subprocesses use the
524
524
  same managed paths. Evidence reports the direct tools actually exercised by
525
- the run and does not enumerate private native bindings.
525
+ the run and does not enumerate private native bindings. Healthy runs exercise
526
+ these capabilities in the native engine process. The controller imports and
527
+ checks them only to diagnose a failed engine launch with no native report;
528
+ it does not run a duplicate empty compilation before each run.
526
529
 
527
530
  Playwright selects one physical family: `@playwright/test` from the Testkit
528
531
  anchor, `playwright` from that package, and `playwright-core` from the selected
@@ -38,6 +38,15 @@ missing declaration, dynamic engine value, or unsupported pair is a discovery
38
38
  error. The profile is derived from the validated pair and is carried unchanged
39
39
  through scheduling, results, and immutable run evidence.
40
40
 
41
+ Run selection by `--file`, `--service`, `--type`, or `--suite` narrows the files
42
+ whose engine metadata is read and validated. Testkit uses discovered paths,
43
+ canonical type suffixes, and suite names to select candidates; each selected
44
+ file must still declare a valid engine. Invalid metadata in an unselected file
45
+ does not block that focused run. Root configuration, service dependencies, and
46
+ configured skip, requirement, and isolation selectors remain validated against
47
+ the complete discovered project. `discover`, `doctor`, and `typecheck` validate
48
+ metadata across the whole project.
49
+
41
50
  Canonical filename suffixes:
42
51
 
43
52
  - `*.ui.testkit.ts`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gfdelarue/testkit",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "description": "Test orchestration platform for product acceptance and QA workflows",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",
@@ -123,6 +123,7 @@
123
123
  "@playwright/test": "1.59.1",
124
124
  "@types/node": "22.20.1",
125
125
  "@typescript-eslint/parser": "8.65.0",
126
+ "@typescript-eslint/types": "8.65.0",
126
127
  "@typescript-eslint/utils": "8.65.0",
127
128
  "diff": "^9.0.0",
128
129
  "esbuild": "0.28.1",
@@ -1,4 +0,0 @@
1
- import type { ArtifactInspectionEntry } from "../../app/inspect/artifacts.js";
2
- type RunArtifactPayload = NonNullable<ArtifactInspectionEntry["payload"]>;
3
- export declare function formatArtifactPreview(payload: RunArtifactPayload | null, maxLines?: number): string[];
4
- export {};
@@ -1 +0,0 @@
1
- import{asUnknownRecord as u,isUnknownRecord as d,optionalString as n}from"../../kernel/unknown-values.js";export function formatArtifactPreview(t,r=6){if(!t)return["artifact payload missing"];const s=u(t.data);if(t.kind==="testkit.data-flow")return k(t,r);if(t.kind==="testkit.data-flow.report"||t.contentType==="text/markdown")return(t.text??n(s?.text)??"").split(/\r?\n/).filter(i=>i.trim().length>0).slice(0,r);if(t.kind==="testkit.http-traces")return w(t,r);if(t.contentType==="text/plain"&&(typeof s?.text=="string"||typeof t.text=="string"))return(n(s?.text)??t.text??"").split(/\r?\n/).filter(i=>i.trim().length>0).slice(0,r);const o=t.data===void 0?t:t.data;return JSON.stringify(o,null,2).split(/\r?\n/).slice(0,r)}function k(t,r){const s=u(t.data),o=s?.kind==="testkit.data-flow"?s:t,e=[],i=n(o.summaryText);i&&e.push(i);for(const c of p(o.scenarios)){const a=n(c.status)??"unknown",f=n(c.name)??"scenario";e.push(`${a.toUpperCase()} ${f}`);for(const m of p(c.budgetFailures)){const g=n(m.message);g&&e.push(`Budget: ${g}`)}}return e.slice(0,r)}function w(t,r){const s=p(u(t.data)?.traces),o=[];for(const e of s.slice(0,r)){const i=u(e.response),c=n(e.method)??"?",a=n(e.path)??"?",f=n(e.requestId),m=h(i?.status)??"?";o.push(`${c} ${a} -> ${String(m)}${f?` [${f}]`:""}`)}return o}function p(t){return Array.isArray(t)?t.filter(d):[]}function h(t){return typeof t=="string"||typeof t=="number"&&Number.isFinite(t)?t:null}