@gfdelarue/testkit 0.6.6 → 0.6.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/README.md +134 -0
  2. package/dist/app/bugs/ci-report.js +2 -2
  3. package/dist/app/campaigns/brief.js +1 -1
  4. package/dist/app/cleanup/index.js +1 -1
  5. package/dist/app/doctor/index.js +1 -1
  6. package/dist/app/environments/static-values.js +1 -1
  7. package/dist/app/environments/supplied.js +1 -1
  8. package/dist/app/environments/task-databases.js +2 -2
  9. package/dist/app/evidence/ci-status.js +1 -0
  10. package/dist/app/evidence/ci.js +1 -0
  11. package/dist/app/evidence/index.js +1 -0
  12. package/dist/app/evidence/inputs.js +1 -0
  13. package/dist/app/evidence/report.js +1 -0
  14. package/dist/app/lint/index.js +1 -1
  15. package/dist/app/run/engine-evidence-persistence.js +1 -1
  16. package/dist/app/run/engine-execution-failures.js +1 -1
  17. package/dist/app/run/engine-execution.js +1 -1
  18. package/dist/app/run/operation-progress.js +1 -1
  19. package/dist/app/run/orchestrator.js +1 -1
  20. package/dist/app/run/results.js +1 -1
  21. package/dist/app/run/run-events.js +1 -1
  22. package/dist/app/run/run-finalization.js +1 -1
  23. package/dist/app/run/run-guards.js +3 -3
  24. package/dist/app/run/scheduler/estimates.js +1 -1
  25. package/dist/app/run/scheduler/index.js +1 -1
  26. package/dist/app/run/task-batches.js +1 -1
  27. package/dist/app/run/worker-loop.js +1 -1
  28. package/dist/app/status/runs.js +1 -1
  29. package/dist/app/typecheck/index.js +4 -4
  30. package/dist/cli/command-catalog.js +1 -1
  31. package/dist/cli/command-registry.js +1 -1
  32. package/dist/cli/commands/evidence.js +2 -0
  33. package/dist/cli/renderers/destroy/text.js +1 -1
  34. package/dist/cli/renderers/evidence/text.js +1 -0
  35. package/dist/cli/renderers/run/failure.js +1 -1
  36. package/dist/cli/renderers/run/status.js +1 -1
  37. package/dist/cli/renderers/run/text-reporter.js +23 -23
  38. package/dist/config/loader/decode-discovery.js +1 -1
  39. package/dist/config/loader/discover-tests.js +1 -1
  40. package/dist/config/loader/discovery-result.js +1 -1
  41. package/dist/config/loader/discovery.js +2 -2
  42. package/dist/config/loader/env.js +1 -1
  43. package/dist/config/loader/file-metadata.js +1 -1
  44. package/dist/config/loader/groups.js +2 -2
  45. package/dist/config/loader/index.js +1 -1
  46. package/dist/config/loader/load.js +1 -1
  47. package/dist/config/loader/path-policy.js +1 -1
  48. package/dist/config/loader/runtime-isolation.js +1 -1
  49. package/dist/config/loader/runtime.js +1 -1
  50. package/dist/config/loader/test-selection.js +1 -1
  51. package/dist/config/loader/validation.js +1 -1
  52. package/dist/database/admin.js +4 -4
  53. package/dist/database/cleanup.js +1 -1
  54. package/dist/database/configured-steps.js +1 -1
  55. package/dist/database/docker.js +3 -5
  56. package/dist/database/index.js +2 -2
  57. package/dist/database/local-postgres.js +3 -4
  58. package/dist/database/postgres/index.js +2 -2
  59. package/dist/database/progress.js +1 -1
  60. package/dist/database/roles.js +2 -2
  61. package/dist/database/runtime-config.js +1 -1
  62. package/dist/database/schema-source.js +3 -3
  63. package/dist/database/source-url.js +1 -1
  64. package/dist/database/template-steps.js +3 -3
  65. package/dist/domain/operation-reason.js +1 -1
  66. package/dist/domain/suite-selection.js +1 -1
  67. package/dist/engines/adapters/evidence-files.js +1 -1
  68. package/dist/engines/adapters/evidence-identity.js +1 -1
  69. package/dist/engines/adapters/execution.js +1 -1
  70. package/dist/engines/adapters/node-startup-cache.js +1 -1
  71. package/dist/engines/adapters/workspace.js +1 -1
  72. package/dist/engines/contracts/fixture-manifest.js +1 -1
  73. package/dist/engines/installations/package-resolution.js +1 -1
  74. package/dist/engines/managed-module-resolution.js +2 -2
  75. package/dist/engines/node-vitest/adapter/adapter.js +1 -1
  76. package/dist/engines/node-vitest/runtime/data/report.js +3 -3
  77. package/dist/engines/node-vitest/runtime/data/scenario.js +2 -2
  78. package/dist/engines/node-vitest/selection.js +1 -1
  79. package/dist/engines/playwright/adapter/adapter.js +1 -1
  80. package/dist/engines/playwright/adapter/config.js +3 -3
  81. package/dist/engines/playwright/adapter/index.js +1 -1
  82. package/dist/environment/lifecycle-controller.js +1 -1
  83. package/dist/environment/template.js +1 -1
  84. package/dist/evidence/artifacts.js +3 -3
  85. package/dist/evidence/bundles.js +2 -2
  86. package/dist/evidence/environment-record.js +1 -1
  87. package/dist/evidence/index.js +1 -1
  88. package/dist/evidence/logs.js +2 -2
  89. package/dist/evidence/operation-timings.js +1 -1
  90. package/dist/evidence/reading.js +1 -1
  91. package/dist/evidence/reporting.js +1 -1
  92. package/dist/evidence/run-evidence.js +1 -1
  93. package/dist/evidence/run-inputs.js +1 -0
  94. package/dist/evidence/run-model.js +1 -1
  95. package/dist/evidence/setup-progress.js +1 -1
  96. package/dist/evidence/storage-inventory.js +1 -1
  97. package/dist/evidence/storage.js +2 -2
  98. package/dist/process/capture-output.js +1 -1
  99. package/dist/process/execa-bundle.json +3 -3
  100. package/dist/process/host/platform.js +1 -1
  101. package/dist/process/locks.js +4 -4
  102. package/dist/process/process-group.js +1 -1
  103. package/dist/process/process.js +1 -1
  104. package/dist/process/ready-deadline.js +1 -1
  105. package/dist/process/runs/manifest.js +1 -1
  106. package/dist/process/runs/stop.js +1 -1
  107. package/dist/process/windows-native.js +1 -1
  108. package/dist/repo/content.js +4 -0
  109. package/dist/repo/fingerprint-policy.js +1 -1
  110. package/dist/repo/managed-git.js +1 -1
  111. package/dist/repo/metadata.js +1 -1
  112. package/dist/repo/state.js +4 -4
  113. package/dist/steps/configured-step-testkit-modules.js +1 -1
  114. package/dist/steps/configured-steps.js +1 -1
  115. package/dist/steps/environment.js +1 -1
  116. package/dist/steps/index.js +1 -1
  117. package/dist/steps/run.js +2 -2
  118. package/dist/toolchains/esbuild.js +1 -1
  119. package/dist/toolchains/index.js +3 -3
  120. package/dist/toolchains/node-integrity.js +1 -1
  121. package/dist/toolchains/node-provisioning.js +2 -2
  122. package/dist/toolchains/semver.js +1 -1
  123. package/dist/tracker/gitea.js +1 -1
  124. package/dist/tracker/github.js +1 -1
  125. package/dist/tracker/requests.js +1 -1
  126. package/dist/tracker/responses.js +1 -1
  127. package/docs/bug-records.md +18 -2
  128. package/docs/release-notes.md +27 -0
  129. package/package.json +1 -1
  130. package/dist/app/run/planning-types.js +0 -1
  131. package/dist/app/run/selection.js +0 -1
  132. package/dist/config/authoring/http-methods.js +0 -1
  133. package/dist/evidence/artifact-validation.js +0 -1
@@ -1,2 +1,2 @@
1
- import{stringFromUnknown as g}from"./primitives.js";import B from"node:crypto";import{writeDataFlowScenario as M}from"./report.js";import{isUnknownArray as z,isUnknownRecord as p}from"../../../../kernel/unknown-values.js";const A=/(authorization|cookie|password|secret|token|api[_-]?key|private[_-]?key)/iu,$=String,y=new WeakMap;export function dataFlow(e){return E(e)}function E(e){const t=e?.task;if(t?.result===void 0||t.result.duration!==void 0||e?.onTestFinished===void 0)throw new Error("dataFlow(context) needs the context of a running Vitest test: test(name, (context) => { dataFlow(context); })");const r=y.get(e);if(r!==void 0)return r;const n=D(F(t),{path:process.env.TESTKIT_TEST_FILE??t.file.name,...t.location}),s=T(n);return y.set(e,s),e.onTestFinished(()=>{if(y.delete(e),t.result?.state==="skip")return;const i=N(n,t.result),a={attempt:(t.result?.retryCount??0)+1,repeat:t.result?.repeatCount??0,repeats:t.repeats??0};if(M(t.id,a,i),i.budgetFailures.length>0)throw new Error(R(i))}),s}function F(e){const t=[e.name];for(let r=e.suite;r!==void 0&&r!==e.file;r=r.suite)t.unshift(r.name);return t.join(" ")}export function measureJsonBytes(e){return Buffer.byteLength(JSON.stringify(e??null))}export function stableHash(e){const t=x(e);if(t===void 0)throw new TypeError("stableHash(...) requires a serializable value");return B.createHash("sha256").update(t).digest("hex")}function T(e){const t={addFinding(r,n,s={}){return e.findings.push({severity:J(r),message:o(n,"finding message"),details:l(s)}),t},actor(r,n={}){return d(e,"actor",r,n),t},budget(r){return S(e,r)},bytes(r,n,s={}){return t.metric(r,n,{...s,kind:"bytes",unit:s.unit||"bytes"})},count(r,n=1,s={}){return t.metric(r,n,{...s,kind:"count",unit:s.unit||"count"})},duration(r,n,s={}){return t.metric(r,n,{...s,kind:"duration",unit:s.unit||"ms"})},event(r,n={}){return e.events.push({atMs:Date.now()-e.startedAtMs,name:o(r,"event name"),details:l(n)}),t},externalCall(r,n={}){const s=h(n);return d(e,"external-call",r,s),typeof s.durationMs=="number"&&t.duration(`${r}.external.duration`,s.durationMs),typeof s.requestBytes=="number"&&t.bytes(`${r}.external.request.bytes`,s.requestBytes),typeof s.responseBytes=="number"&&t.bytes(`${r}.external.response.bytes`,s.responseBytes),typeof s.costMinor=="number"&&t.money(`${r}.external.cost`,s.costMinor),t},expectMetric(r){return S(e,r)},expectSequence(r){return b(e,r)},finding(r,n,s={}){return t.addFinding(r,n,s)},metric(r,n,s={}){return O(e,r,n,s),t},money(r,n,s={}){return t.metric(r,n,{...s,kind:"money",unit:s.unit||"minor"})},ratio(r,n,s={}){return t.metric(r,n,{...s,kind:"ratio",unit:s.unit||"ratio"})},reconcile(r,n={}){return d(e,"reconciliation",r,n),t},record(r,n={}){const s=h(n);return d(e,"record",r,s),typeof s.count=="number"&&t.count(`${r}.record.count`,s.count),t},recordCache(r,{hits:n=0,misses:s=0,reads:i=0,writes:a=0}={}){const c=o(r,"cache metric name");t.count(`${c}.cache.hits`,n),t.count(`${c}.cache.misses`,s),t.count(`${c}.cache.reads`,i),t.count(`${c}.cache.writes`,a);const m=n+s;return m>0&&t.ratio(`${c}.cache.hit_rate`,n/m),t},recordHttpRequest(r){return t.count("http.request.count",1),t.bytes("http.request.bytes",r.bodyBytes||0),t.bytes("http.response.bytes",r.responseBytes||0),t.duration("http.request.duration",r.durationMs||0),t.sequence("http.methods",[r.method]),t.sequence("http.paths",[r.path]),t.event("http.request",{durationMs:r.durationMs||0,method:r.method,path:r.path,requestBytes:r.bodyBytes||0,responseBytes:r.responseBytes||0,status:r.status}),t},recordLlmUsage(r,n={}){const s=o(r,"llm usage name");for(const[i,a]of Object.entries(n))typeof a=="number"&&Number.isFinite(a)&&t.count(`${s}.${I(i)}`,a,{unit:i.toLowerCase().includes("cost")?"minor":"tokens"});return t},recordRpc(r,{method:n,requestBytes:s=0,responseBytes:i=0}={}){const a=o(r,"rpc metric name");return t.count(`${a}.rpc.count`,1),n&&t.sequence(`${a}.rpc.methods`,[n]),t.bytes(`${a}.rpc.request.bytes`,s),t.bytes(`${a}.rpc.response.bytes`,i),t},sequence(r,n,s={}){return v(e,r,n,s),t},sequenceBudget(r){return b(e,r)},system(r,n={}){return d(e,"system",r,n),t},transition(r,n={}){return d(e,"transition",r,n),t}};return Object.freeze(t)}function S(e,t){const r=o(t,"metric budget name");return{atLeast(n,s={}){return u(e,{name:r,target:"metric",operator:">=",expected:n,message:s.message}),this},atMost(n,s={}){return u(e,{name:r,target:"metric",operator:"<=",expected:n,message:s.message}),this},equals(n,s={}){return u(e,{name:r,target:"metric",operator:"==",expected:n,message:s.message}),this},greaterThan(n,s={}){return u(e,{name:r,target:"metric",operator:">",expected:n,message:s.message}),this},lessThan(n,s={}){return u(e,{name:r,target:"metric",operator:"<",expected:n,message:s.message}),this},required(n={}){return u(e,{name:r,target:"metric",operator:"exists",message:n.message}),this}}}function b(e,t){const r=o(t,"sequence budget name");return{equals(n,s={}){return u(e,{name:r,target:"sequence",operator:"sequence==",expected:w(n),message:s.message}),this},includesOnly(n,s={}){return u(e,{name:r,target:"sequence",operator:"sequenceOnly",expected:w(n),message:s.message}),this},required(n={}){return u(e,{name:r,target:"sequence",operator:"exists",message:n.message}),this}}}function D(e,t){return{budgets:[],events:[],evidence:[],findings:[],location:t,metrics:[],name:o(e,"data scenario name"),sequences:[],startedAtMs:Date.now()}}function N(e,t){const{budgets:r,startedAtMs:n,...s}=e,i=r.map(m=>V(e,m)),a=i.filter(m=>m.status==="failed"),c=t?.state==="fail"?U(t.errors?.at(-1)):null;return{...s,budgetFailures:a,budgets:i,durationMs:Date.now()-n,error:c,status:c||a.length>0?"failed":"passed"}}function O(e,t,r,n={}){const s=r;if(!Number.isFinite(s))throw new Error(`Data metric "${t}" must be a finite number`);e.metrics.push({description:f(n.description),dimensions:k(n.dimensions??{}),kind:f(n.kind)||"metric",name:o(t,"metric name"),unit:f(n.unit),value:s})}function v(e,t,r,n={}){const s=w(r).map(i=>$(i));e.sequences.push({description:f(n.description),dimensions:k(n.dimensions??{}),name:o(t,"sequence name"),values:s})}function u(e,t){e.budgets.push({...t,message:f(t.message)})}function d(e,t,r,n={}){const s=h(n);e.evidence.push({atMs:Date.now()-e.startedAtMs,details:l(s),kind:t,name:o(r,`${t} evidence name`),status:f(s.status)||null})}function h(e){return p(e)?e:{value:e}}function V(e,t){const r=t.target==="sequence"?C(e,t.name):j(e,t.name),n=_(t,r),s=t.message||L(t,r,n);return t.target==="metric"?{...t,actual:r,expected:t.expected??null,message:s,status:n?"passed":"failed"}:{...t,actual:r,expected:t.expected??null,message:s,status:n?"passed":"failed"}}function _(e,t){if(e.target==="metric")return e.operator==="exists"?t!==null:typeof t!="number"?!1:e.operator==="<="?t<=Number(e.expected):e.operator===">="?t>=Number(e.expected):e.operator==="<"?t<Number(e.expected):e.operator===">"?t>Number(e.expected):Object.is(t,Number(e.expected));if(e.operator==="exists")return t!==null;if(!Array.isArray(t))return!1;if(e.operator==="sequence==")return H(t,e.expected);const r=new Set((e.expected??[]).map(n=>g(n)));return t.every(n=>r.has(n))}function j(e,t){for(let r=e.metrics.length-1;r>=0;r-=1)if(e.metrics[r].name===t)return e.metrics[r].value;return null}function C(e,t){const r=[];for(const n of e.sequences)n.name===t&&r.push(...n.values);return r.length>0?r:null}function L(e,t,r){return e.operator==="exists"?r?`${e.target} "${e.name}" was recorded`:`${e.target} "${e.name}" was not recorded`:`${e.target} "${e.name}" expected ${e.operator} ${JSON.stringify(e.expected)}, got ${JSON.stringify(t)}`}function R(e){return[`Data-flow budgets failed in "${e.name}":`,...e.budgetFailures.map(t=>`- ${t.message}`)].join(`
2
- `)}function w(e){return z(e)?[...e]:[e]}function o(e,t){const r=g(e).trim();if(!r)throw new Error(`${t} must be a non-empty string`);return r}function f(e){if(e==null)return null;const t=g(e).trim();return t||null}function J(e){const t=g(e||"info").toLowerCase();return K(t)?t:"info"}function l(e){if(Array.isArray(e))return e.map(r=>l(r));if(!p(e))return e;const t=Object.create(null);for(const[r,n]of Object.entries(e))t[r]=A.test(r)?"[redacted]":l(n);return t}function U(e){return e instanceof Error?{message:e.message,name:e.name,stack:e.stack??null}:{message:q(e,"message")??P(e),name:q(e,"name")??"Error",stack:q(e,"stack")}}function x(e){return Array.isArray(e)?`[${e.map(t=>x(t)).join(",")}]`:p(e)?`{${Object.keys(e).sort().map(t=>{const r=x(e[t]);return`${JSON.stringify(t)}:${r??"undefined"}`}).join(",")}}`:JSON.stringify(e)}function H(e,t){return!Array.isArray(e)||!Array.isArray(t)||e.length!==t.length?!1:e.every((r,n)=>String(r)===String(t[n]))}function I(e){return e.replace(/([a-z0-9])([A-Z])/g,"$1_$2").replace(/[^a-zA-Z0-9]+/g,"_").replace(/^_+|_+$/g,"").toLowerCase()}function k(e){const t=l(e);return p(t)?t:{}}function q(e,t){if(!p(e))return null;try{const r=Reflect.get(e,t);return typeof r=="string"?r:null}catch{return null}}function P(e){try{return $(e)}catch{return"Unknown thrown value"}}function K(e){return e==="error"||e==="info"||e==="waste"||e==="watch"}
1
+ import{stringFromUnknown as y}from"./primitives.js";import k from"node:crypto";import{writeDataFlowScenario as A}from"./report.js";import{isUnknownArray as E,isUnknownRecord as p}from"../../../../kernel/unknown-values.js";const F=/(authorization|cookie|password|secret|token|api[_-]?key|private[_-]?key)/iu,S=String,g=new WeakMap;export function dataFlow(e){return T(e)}function T(e){const t=e?.task;if(t?.result===void 0||t.result.duration!==void 0||e?.onTestFinished===void 0)throw new Error("dataFlow(context) needs the context of a running Vitest test: test(name, (context) => { dataFlow(context); })");const s=g.get(e);if(s!==void 0)return s;const n=N(D(t),{path:process.env.TESTKIT_TEST_FILE??t.file.name,...t.location}),r=O(n);return g.set(e,r),e.onTestFinished(()=>{if(g.delete(e),t.result?.state==="skip")return;const i=v(n,t.result),o={attempt:(t.result?.retryCount??0)+1,repeat:t.result?.repeatCount??0,repeats:t.repeats??0};if(A(t.id,o,i),i.budgetFailures.length>0)throw new Error(U(i))}),r}function D(e){const t=[e.name];for(let s=e.suite;s!==void 0&&s!==e.file;s=s.suite)t.unshift(s.name);return t.join(" ")}export function measureJsonBytes(e){return Buffer.byteLength(JSON.stringify(e??null))}export function stableHash(e){const t=q(e);if(t===void 0)throw new TypeError("stableHash(...) requires a serializable value");return k.createHash("sha256").update(t).digest("hex")}function O(e){const t=(r,i)=>(o,u,a={})=>n.metric(o,u,{...a,kind:r,unit:a.unit||i}),s=t("count","count"),n={addFinding(r,i,o={}){return e.findings.push({severity:H(r),message:c(i,"finding message"),details:d(o)}),n},actor(r,i={}){return f(e,"actor",r,i),n},budget(r){return b(e,r)},bytes:t("bytes","bytes"),count:(r,i=1,o)=>s(r,i,o),duration:t("duration","ms"),event(r,i={}){return e.events.push({atMs:Date.now()-e.startedAtMs,name:c(r,"event name"),details:d(i)}),n},externalCall(r,i={}){const o=w(i);return f(e,"external-call",r,o),typeof o.durationMs=="number"&&n.duration(`${r}.external.duration`,o.durationMs),typeof o.requestBytes=="number"&&n.bytes(`${r}.external.request.bytes`,o.requestBytes),typeof o.responseBytes=="number"&&n.bytes(`${r}.external.response.bytes`,o.responseBytes),typeof o.costMinor=="number"&&n.money(`${r}.external.cost`,o.costMinor),n},expectMetric(r){return b(e,r)},expectSequence(r){return B(e,r)},finding(r,i,o={}){return n.addFinding(r,i,o)},metric(r,i,o={}){return V(e,r,i,o),n},money:t("money","minor"),ratio:t("ratio","ratio"),reconcile(r,i={}){return f(e,"reconciliation",r,i),n},record(r,i={}){const o=w(i);return f(e,"record",r,o),typeof o.count=="number"&&n.count(`${r}.record.count`,o.count),n},recordCache(r,{hits:i=0,misses:o=0,reads:u=0,writes:a=0}={}){const l=c(r,"cache metric name");n.count(`${l}.cache.hits`,i),n.count(`${l}.cache.misses`,o),n.count(`${l}.cache.reads`,u),n.count(`${l}.cache.writes`,a);const x=i+o;return x>0&&n.ratio(`${l}.cache.hit_rate`,i/x),n},recordHttpRequest(r){return n.count("http.request.count",1),n.bytes("http.request.bytes",r.bodyBytes||0),n.bytes("http.response.bytes",r.responseBytes||0),n.duration("http.request.duration",r.durationMs||0),n.sequence("http.methods",[r.method]),n.sequence("http.paths",[r.path]),n.event("http.request",{durationMs:r.durationMs||0,method:r.method,path:r.path,requestBytes:r.bodyBytes||0,responseBytes:r.responseBytes||0,status:r.status}),n},recordLlmUsage(r,i={}){const o=c(r,"llm usage name");for(const[u,a]of Object.entries(i))typeof a=="number"&&Number.isFinite(a)&&n.count(`${o}.${P(u)}`,a,{unit:u.toLowerCase().includes("cost")?"minor":"tokens"});return n},recordRpc(r,{method:i,requestBytes:o=0,responseBytes:u=0}={}){const a=c(r,"rpc metric name");return n.count(`${a}.rpc.count`,1),i&&n.sequence(`${a}.rpc.methods`,[i]),n.bytes(`${a}.rpc.request.bytes`,o),n.bytes(`${a}.rpc.response.bytes`,u),n},sequence(r,i,o={}){return _(e,r,i,o),n},sequenceBudget(r){return B(e,r)},system(r,i={}){return f(e,"system",r,i),n},transition(r,i={}){return f(e,"transition",r,i),n}};return Object.freeze(n)}function b(e,t){const s=c(t,"metric budget name"),n=r=>function(i,o={}){return h(e,{name:s,target:"metric",operator:r,expected:i,message:o.message}),this};return{atLeast:n(">="),atMost:n("<="),equals:n("=="),greaterThan:n(">"),lessThan:n("<"),required(r={}){return h(e,{name:s,target:"metric",operator:"exists",message:r.message}),this}}}function B(e,t){const s=c(t,"sequence budget name"),n=r=>function(i,o={}){return h(e,{name:s,target:"sequence",operator:r,expected:M(i),message:o.message}),this};return{equals:n("sequence=="),includesOnly:n("sequenceOnly"),required(r={}){return h(e,{name:s,target:"sequence",operator:"exists",message:r.message}),this}}}function N(e,t){return{budgets:[],events:[],evidence:[],findings:[],location:t,metrics:[],name:c(e,"data scenario name"),sequences:[],startedAtMs:Date.now()}}function v(e,t){const{budgets:s,startedAtMs:n,...r}=e,i=s.map(a=>j(e,a)),o=i.filter(a=>a.status==="failed"),u=t?.state==="fail"?I(t.errors?.at(-1)):null;return{...r,budgetFailures:o,budgets:i,durationMs:Date.now()-n,error:u,status:u||o.length>0?"failed":"passed"}}function V(e,t,s,n={}){const r=s;if(!Number.isFinite(r))throw new Error(`Data metric "${t}" must be a finite number`);e.metrics.push({description:m(n.description),dimensions:z(n.dimensions??{}),kind:m(n.kind)||"metric",name:c(t,"metric name"),unit:m(n.unit),value:r})}function _(e,t,s,n={}){const r=M(s).map(i=>S(i));e.sequences.push({description:m(n.description),dimensions:z(n.dimensions??{}),name:c(t,"sequence name"),values:r})}function h(e,t){e.budgets.push({...t,message:m(t.message)})}function f(e,t,s,n={}){const r=w(n);e.evidence.push({atMs:Date.now()-e.startedAtMs,details:d(r),kind:t,name:c(s,`${t} evidence name`),status:m(r.status)||null})}function w(e){return p(e)?e:{value:e}}function j(e,t){const s=t.target==="sequence"?R(e,t.name):L(e,t.name),n=C(t,s),r=t.message||J(t,s,n);return t.target==="metric"?{...t,actual:s,expected:t.expected??null,message:r,status:n?"passed":"failed"}:{...t,actual:s,expected:t.expected??null,message:r,status:n?"passed":"failed"}}function C(e,t){if(e.target==="metric")return e.operator==="exists"?t!==null:typeof t!="number"?!1:e.operator==="<="?t<=Number(e.expected):e.operator===">="?t>=Number(e.expected):e.operator==="<"?t<Number(e.expected):e.operator===">"?t>Number(e.expected):Object.is(t,Number(e.expected));if(e.operator==="exists")return t!==null;if(!Array.isArray(t))return!1;if(e.operator==="sequence==")return K(t,e.expected);const s=new Set((e.expected??[]).map(n=>y(n)));return t.every(n=>s.has(n))}function L(e,t){for(let s=e.metrics.length-1;s>=0;s-=1)if(e.metrics[s].name===t)return e.metrics[s].value;return null}function R(e,t){const s=[];for(const n of e.sequences)n.name===t&&s.push(...n.values);return s.length>0?s:null}function J(e,t,s){return e.operator==="exists"?s?`${e.target} "${e.name}" was recorded`:`${e.target} "${e.name}" was not recorded`:`${e.target} "${e.name}" expected ${e.operator} ${JSON.stringify(e.expected)}, got ${JSON.stringify(t)}`}function U(e){return[`Data-flow budgets failed in "${e.name}":`,...e.budgetFailures.map(t=>`- ${t.message}`)].join(`
2
+ `)}function M(e){return E(e)?[...e]:[e]}function c(e,t){const s=y(e).trim();if(!s)throw new Error(`${t} must be a non-empty string`);return s}function m(e){if(e==null)return null;const t=y(e).trim();return t||null}function H(e){const t=y(e||"info").toLowerCase();return W(t)?t:"info"}function d(e){if(Array.isArray(e))return e.map(s=>d(s));if(!p(e))return e;const t=Object.create(null);for(const[s,n]of Object.entries(e))t[s]=F.test(s)?"[redacted]":d(n);return t}function I(e){return e instanceof Error?{message:e.message,name:e.name,stack:e.stack??null}:{message:$(e,"message")??Z(e),name:$(e,"name")??"Error",stack:$(e,"stack")}}function q(e){return Array.isArray(e)?`[${e.map(t=>q(t)).join(",")}]`:p(e)?`{${Object.keys(e).sort().map(t=>{const s=q(e[t]);return`${JSON.stringify(t)}:${s??"undefined"}`}).join(",")}}`:JSON.stringify(e)}function K(e,t){return!Array.isArray(e)||!Array.isArray(t)||e.length!==t.length?!1:e.every((s,n)=>String(s)===String(t[n]))}function P(e){return e.replace(/([a-z0-9])([A-Z])/g,"$1_$2").replace(/[^a-zA-Z0-9]+/g,"_").replace(/^_+|_+$/g,"").toLowerCase()}function z(e){const t=d(e);return p(t)?t:{}}function $(e,t){if(!p(e))return null;try{const s=Reflect.get(e,t);return typeof s=="string"?s:null}catch{return null}}function Z(e){try{return S(e)}catch{return"Unknown thrown value"}}function W(e){return e==="error"||e==="info"||e==="waste"||e==="watch"}
@@ -1 +1 @@
1
- import i from"node:fs";import{createRequire as h}from"node:module";import f from"node:path";import{isUnknownRecord as d}from"../../kernel/unknown-values.js";import{isPathWithin as S}from"../../process/files/paths.js";import{NODE_VITEST_ENGINE_ENVIRONMENT_KEYS as v}from"./runtime/runtime-input.js";const N=["auto","managed","repo"];export const NODE_VITEST_INVENTORY_MARKER="TESTKIT_VITEST_INVENTORY=";const g=new Set(["BASE_URL","DATABASE_URL","NODE_OPTIONS",...v].map(e=>e.toUpperCase()));export function resolveVitestSelection(e,t,n=process.env){const{"vitest-config":r,"vitest-project":s,"vitest-runner":a,"vitest-shuffle":o}=e;if(o!==void 0&&!/^\d{1,9}$/u.test(o))throw new Error(`--vitest-shuffle takes a seed of up to 9 digits, not ${JSON.stringify(o)}`);if(a==="managed"&&(r??s)!==void 0)throw new Error("--vitest-config and --vitest-project select the repository's Vitest, not --vitest-runner managed");return{config:r===void 0?null:O(t,r),host:{...Object.fromEntries(Object.entries(n).filter(c=>c[1]!==void 0&&!g.has(c[0].toUpperCase()))),...n.NODE_OPTIONS===void 0?{}:{NODE_OPTIONS:n.NODE_OPTIONS}},project:s??null,runner:a??((r??s)===void 0?"auto":"repo"),shuffle:o===void 0?null:Number(o)}}export function decodeVitestSelection(e){if(!d(e)||!N.includes(e.runner)||![e.config,e.project].every(t=>t===null||typeof t=="string")||!(e.shuffle===null||Number.isInteger(e.shuffle))||!d(e.host)||!Object.values(e.host).every(t=>typeof t=="string"))throw new Error("Node/Vitest selection must name a runner, config, project and host environment");return e}function O(e,t){let n;try{n=i.realpathSync(f.resolve(e,t))}catch(r){throw new Error(`--vitest-config ${t} does not exist in ${e}`,{cause:r})}if(!S(i.realpathSync(e),n))throw new Error(`--vitest-config ${t} resolves outside ${e}`);if(!i.statSync(n).isFile())throw new Error(`--vitest-config ${t} is not a file`);return n}export function resolveRepositoryVitest(e,t=!1){const n=f.join(e,"package.json"),r=h(n);let s;try{s=r.resolve("vitest/package.json")}catch(l){const u=!r.resolve.paths("vitest")?.some(E=>i.lstatSync(f.join(E,"vitest"),{throwIfNoEntry:!1}));if(u&&t&&!w(n))return null;const m=u?"is not installed":"is installed but its package metadata is broken";throw new Error(`Vitest ${m} in ${e}; install vitest 4.x, or pass --vitest-runner managed`,{cause:l})}const a=f.dirname(s),o=JSON.parse(i.readFileSync(s,"utf8")),c=`Vitest ${o.version} at ${a}`;if(!/^4\.\d+\.\d+/u.test(o.version))throw new Error(`${c} is not supported; install vitest 4.x, or pass --vitest-runner managed`);const p=o.exports?.["."]?.import?.default;try{if(typeof p!="string")throw new Error("its package exports name no ESM entry point");return{entrypoint:r.resolve("vitest/node"),index:f.join(a,p),name:"vitest",root:a,version:o.version}}catch(l){throw new Error(`${c} is broken; reinstall it`,{cause:l})}}function w(e){if(!i.existsSync(e))return!1;const t=JSON.parse(i.readFileSync(e,"utf8"));return["dependencies","devDependencies","optionalDependencies"].some(n=>Object.hasOwn(t[n]??{},"vitest"))}
1
+ import o from"node:fs";import{createRequire as v}from"node:module";import c from"node:path";import{isPathWithin as m}from"../../process/files/paths.js";import{NODE_VITEST_ENGINE_ENVIRONMENT_KEYS as S}from"./runtime/runtime-input.js";export const NODE_VITEST_INVENTORY_MARKER="TESTKIT_VITEST_INVENTORY=";const h=new Set(["BASE_URL","DATABASE_URL","NODE_OPTIONS",...S].map(e=>e.toUpperCase()));export function resolveVitestSelection(e,n,t=process.env){const{"vitest-config":r,"vitest-project":i,"vitest-runner":a,"vitest-shuffle":s}=e;if(s!==void 0&&!/^\d{1,9}$/u.test(s))throw new Error(`--vitest-shuffle takes a seed of up to 9 digits, not ${JSON.stringify(s)}`);if(a==="managed"&&(r??i)!==void 0)throw new Error("--vitest-config and --vitest-project select the repository's Vitest, not --vitest-runner managed");return{config:r===void 0?null:O(n,r),host:{...Object.fromEntries(Object.entries(t).filter(f=>f[1]!==void 0&&!h.has(f[0].toUpperCase()))),...t.NODE_OPTIONS===void 0?{}:{NODE_OPTIONS:t.NODE_OPTIONS}},project:i??null,runner:a??((r??i)===void 0?"auto":"repo"),shuffle:s===void 0?null:Number(s)}}function O(e,n){let t;try{t=o.realpathSync(c.resolve(e,n))}catch(r){throw new Error(`--vitest-config ${n} does not exist in ${e}`,{cause:r})}if(!m(o.realpathSync(e),t))throw new Error(`--vitest-config ${n} resolves outside ${e}`);if(!o.statSync(t).isFile())throw new Error(`--vitest-config ${n} is not a file`);return t}export function resolveRepositoryVitest(e,n=!1){const t=c.join(e,"package.json"),r=v(t);let i;try{i=r.resolve("vitest/package.json")}catch(l){const u=!r.resolve.paths("vitest")?.some(E=>o.lstatSync(c.join(E,"vitest"),{throwIfNoEntry:!1}));if(u&&n&&!N(t))return null;const d=u?"is not installed":"is installed but its package metadata is broken";throw new Error(`Vitest ${d} in ${e}; install vitest 4.x, or pass --vitest-runner managed`,{cause:l})}const a=c.dirname(i),s=JSON.parse(o.readFileSync(i,"utf8")),f=`Vitest ${s.version} at ${a}`;if(!/^4\.\d+\.\d+/u.test(s.version))throw new Error(`${f} is not supported; install vitest 4.x, or pass --vitest-runner managed`);const p=s.exports?.["."]?.import?.default;try{if(typeof p!="string")throw new Error("its package exports name no ESM entry point");return{entrypoint:r.resolve("vitest/node"),index:c.join(a,p),name:"vitest",root:a,version:s.version}}catch(l){throw new Error(`${f} is broken; reinstall it`,{cause:l})}}function N(e){if(!o.existsSync(e))return!1;const n=JSON.parse(o.readFileSync(e,"utf8"));return["dependencies","devDependencies","optionalDependencies"].some(t=>Object.hasOwn(n[t]??{},"vitest"))}
@@ -1 +1 @@
1
- import{makeTempDirAsync as D}from"../../../process/files/temp-dir.js";import{toError as g}from"../../../kernel/error-serialization.js";import{classifyManagedProcessResult as A,terminalOutcome as T,processOutputEvidence as N,failureFromError as P,firstOutputLine as R,processExitDetails as M,processExitFailure as b,executeEngineTasks as C,managedProcessFailure as x,emptyEvidence as W,taskTimeoutFailure as U}from"../../adapters/execution.js";import{requireRootDirectory as B,requireAbsoluteWorkspacePath as O,requireConsumerFile as H,safeSegment as S}from"../../adapters/workspace.js";import u from"node:fs/promises";import Y from"node:path";import{setTimeout as $}from"node:timers/promises";import{pathToFileURL as G}from"node:url";import{isEngineTaskDeadlineExceededReason as K,resolveEngineTaskInputs as V}from"../../api/index.js";import{createNativeInvocationProcessEnvironment as j,runManagedProcess as J}from"../../../process/index.js";import{PlaywrightConsumerConfigurationError as z,playwrightFileSuiteGrep as X,writePlaywrightTaskWorkspace as Q}from"./config.js";import{collectPlaywrightEvidence as Z}from"./evidence.js";import{engineEvidenceIdentity as q}from"../../adapters/evidence-identity.js";import{getNodeStartupCacheEnvironment as rr}from"../../adapters/node-startup-cache.js";import{CHROMIUM_STARTUP_ENVIRONMENT as tr}from"../runtime/chromium-environment.js";const y={codePrefix:"PLAYWRIGHT",label:"Playwright"};export const PLAYWRIGHT_SPAWN_ENVIRONMENT_MAX_BYTES=24*1024;const F=64;class L extends Error{constructor(r){const t=g(r);super(t.message,{cause:t}),this.name="PlaywrightConsumerPreparationError"}}export const playwrightAdapterDependencies={createInvocationEnvironment:j,removeOwnedDirectory:or,writeTaskWorkspace:Q};const er=new Set(["EBUSY","ENOTEMPTY","EPERM"]),ir=5;async function or(e){for(let r=0;;r+=1)try{await u.rm(e,{force:!0,recursive:!0});return}catch(t){const i=t instanceof Error&&"code"in t?t.code:void 0;if(r===ir||typeof i!="string"||!er.has(i))throw t;await $((r+1)*100)}}export async function executePlaywrightEngine(e,r,t,i=playwrightAdapterDependencies){const o=await nr(e,r,i,t);return C(e,o.tasks,y,(a,s)=>ar(e,o,a,s),pr(e,o.installation))}async function nr(e,r,t,i){const o=await B(e.workspace.rootDirectory);O(e.workspace.temporaryDirectory,"temporary directory"),O(e.workspace.artifactDirectory,"artifact directory"),await u.mkdir(e.workspace.temporaryDirectory,{recursive:!0}),await u.mkdir(e.workspace.artifactDirectory,{recursive:!0});const a=await u.realpath(e.workspace.temporaryDirectory),s=await u.realpath(e.workspace.artifactDirectory),n=await D(`playwright-${S(e.invocationId)}-`,{parentDir:a});i.defer(()=>t.removeOwnedDirectory(n));const l=await t.createInvocationEnvironment(n);i.defer(()=>t.removeOwnedDirectory(l.temporaryDirectory));const p=[];for(const[h,c]of e.tasks.entries()){const I=V(e,c),v=`${h.toString().padStart(3,"0")}-${S(c.id)}`,_=Y.join(n,v),f=await D(`${v}-`,{parentDir:s});try{const m=lr(e,c.serviceName),E=await ur(()=>H(o,c.file,"Playwright task file")),w=await t.writeTaskWorkspace({artifactDirectory:f,baseUrl:m,browserFile:r.browserExecutable,databases:e.target.databases,endpoints:e.target.endpoints,handles:e.target.handles,inputs:I,invocationId:e.invocationId,managedModuleEntrypoints:{authoring:r.authoringEntrypoints.authoring,cli:r.family.cli,environment:r.authoringEntrypoints.environment,managedTest:r.authoringEntrypoints.managedTest,playwright:r.family.playwright.entrypoint,playwrightCore:r.family.playwrightCore.entrypoint,playwrightTest:r.family.playwrightTest.entrypoint,root:r.authoringEntrypoints.root,rootPackageJson:r.authoringEntrypoints.rootPackageJson},moduleFile:r.family.playwrightTest.entrypoint,orchestration:c.orchestration,rootDirectory:o,serviceName:c.serviceName,sourceFile:E,taskDirectory:_,taskId:c.id});i.defer(async()=>{try{await t.removeOwnedDirectory(w.attachmentDirectory)}catch(d){if(!(d instanceof Error&&"code"in d&&d.code==="ENOTDIR"))throw d}}),p.push({artifactDirectory:f,inputs:I,task:c,preparationFailure:null,prepared:{baseUrl:m,sourceFile:E,workspace:w}})}catch(m){const E=(await yr([_,f],t)).map(w=>w.error);p.push({artifactDirectory:f,inputs:I,task:c,preparationFailure:playwrightPreparationFailure(m,E),prepared:null})}}return Object.freeze({installation:r,invocationEnvironment:l.environment,rootDirectory:o,tasks:Object.freeze(p)})}async function ar(e,r,t,i){const a={...await rr(e.workspace.cacheDirectory,t.inputs.environment),...sr(e,r,t)};try{k(a)}catch{delete a.NODE_COMPILE_CACHE}const s=t.task.deadlineMs-Date.now();if(s<=0)return T(t.task.id,"failed",i,W(),U(t.task.timeoutMs,y));const n=await J({args:[r.installation.family.cli,"test","--grep",X(t.prepared.sourceFile),"--config",t.prepared.workspace.configFile],cwd:r.rootDirectory,displayLabel:"Playwright",env:a,executable:process.execPath,signal:e.signal,timeoutMs:s});let l;try{l=await Z({identity:{engineId:"playwright",invocationId:e.invocationId,taskId:t.task.id,sourceFile:t.prepared.sourceFile},artifactDirectory:t.artifactDirectory,outputDirectory:t.prepared.workspace.outputDirectory,reportFile:t.prepared.workspace.reportFile,reportersFinishedFile:t.prepared.workspace.reportersFinishedFile,rootDirectory:r.rootDirectory,sourceFile:t.prepared.sourceFile,stderr:n.stderr,stdout:n.stdout})}catch(h){return T(t.task.id,"failed",i,N(n),playwrightEvidenceCollectionFailure(n,h))}const p=classifyPlaywrightProcessResult(n,l,t.task.timeoutMs,K(e.signal.reason));return T(t.task.id,p.status,i,l.evidence,p.failure)}export function classifyPlaywrightProcessResult(e,r,t,i){const o=A(e,t,i,y);if(o!==null)return o;if(r.outputLimit!==null){const n=r.outputLimit;return{failure:{code:"PLAYWRIGHT_OUTPUT_ARTIFACT_LIMIT",details:{...n},message:`Playwright output exceeded its ${n.limit.toString()} ${n.kind==="bytes"?"byte aggregate":"entry"} artifact limit`},status:"failed"}}const a=r.attachmentFailures.find(n=>n.code==="PLAYWRIGHT_ATTACHMENT_LIMIT");if(a!==void 0)return{failure:a,status:"failed"};if(r.report.failure!==null){const n=r.report.failure;return{...r.report,failure:{...n,details:{...n.details,evidenceErrors:r.reportError===null?0:1,...M(e)}}}}const s=r.attachmentFailures.at(0);return s!==void 0?{failure:s,status:"failed"}:e.exitCode!==0&&(r.reportError===null||!r.reportersFinished)?b(e,y,R(e.stderr)??R(e.stdout)):r.reportError!==null?{failure:{...P(r.reportError,"PLAYWRIGHT_REPORT_INVALID"),classification:"infra"},status:"failed"}:r.report}export function playwrightEvidenceCollectionFailure(e,r){const t=g(r);return x(e,y,t)??{...P(t,"PLAYWRIGHT_EVIDENCE_FAILED"),classification:"infra"}}function sr(e,r,t){const i={...r.invocationEnvironment},o=t.task,a=t.prepared.workspace.inputBootstrapFile,s=t.prepared.workspace.resolutionBootstrapFile;return Object.assign(i,{...t.prepared.baseUrl===null?{}:{BASE_URL:t.prepared.baseUrl},...tr,FORCE_COLOR:"0",NO_COLOR:"1",PLAYWRIGHT_BROWSERS_PATH:r.installation.registry.canonicalValue,PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD:"1",TESTKIT_ACTIVE:"1",TESTKIT_INTERNAL_FILE_DEADLINE_MS:String(o.deadlineMs),TESTKIT_INTERNAL_FILE_TIMEOUT_SECONDS:String(Math.max(1,Math.ceil(o.timeoutMs/1e3))),TESTKIT_INVOCATION_ID:e.invocationId,TESTKIT_LEASE_DIR:o.orchestration.lease.directory,TESTKIT_LEASE_ID:o.orchestration.lease.id,TESTKIT_MODE:o.orchestration.mode,...o.orchestration.runtimeId===void 0?{}:{TESTKIT_RUNTIME_ID:o.orchestration.runtimeId},TESTKIT_TEST_FILE:o.file,TESTKIT_TEST_ID:o.id}),i.NODE_OPTIONS=cr(s,a),k(i),i}function cr(e,r){return["--experimental-strip-types",`--require=${JSON.stringify(e)}`,`--import=${JSON.stringify(G(r).href)}`].join(" ")}export function playwrightSpawnEnvironmentBytes(e){return Object.entries(e).reduce((r,[t,i])=>r+(process.platform==="win32"?(t.length+i.length+2)*2:Buffer.byteLength(t,"utf8")+Buffer.byteLength(i,"utf8")+2),0)}function k(e){if(Object.keys(e).length>F)throw new Error(`Playwright physical spawn environment exceeds its ${F.toString()} entry limit`);if(playwrightSpawnEnvironmentBytes(e)>PLAYWRIGHT_SPAWN_ENVIRONMENT_MAX_BYTES)throw new Error(`Playwright physical spawn environment exceeds its ${PLAYWRIGHT_SPAWN_ENVIRONMENT_MAX_BYTES.toString()} byte limit`)}function lr(e,r){const t=e.target.baseUrl;if(t===null)return null;if(typeof t!="string"||t.trim().length===0)throw new Error(`Playwright task service ${r} endpoint must be a non-empty URL`);let i;try{i=new URL(t)}catch(o){throw new Error(`Playwright task service ${r} endpoint is not a valid URL`,{cause:o})}if(i.protocol!=="http:"&&i.protocol!=="https:")throw new Error(`Playwright task service ${r} endpoint must use http or https`);return i.href}function pr(e,r){return{...q({engineId:"playwright",installationKind:"root-playwright-family",profile:e.profile}),browserName:"chromium",packages:{playwright:r.family.playwright.version,playwrightCore:r.family.playwrightCore.version,playwrightTest:r.family.playwrightTest.version},registryMode:r.registry.mode}}export function playwrightPreparationFailure(e,r){if(r.length===0)return{...P(e,"PLAYWRIGHT_PREPARE_FAILED"),classification:e instanceof L||e instanceof z?"product":"infra"};const t=g(e);return{classification:"infra",code:"PLAYWRIGHT_PREPARE_CLEANUP_FAILED",details:{cleanupFailures:r.map(i=>i.message),preparationFailure:t.message},message:`Playwright task preparation failed and its partial workspace could not be removed: ${r.map(i=>i.message).join("; ")}`,...t.stack===void 0?{}:{stack:t.stack}}}async function ur(e){try{return await e()}catch(r){throw new L(r)}}async function yr(e,r){const t=[];for(const i of e)try{await r.removeOwnedDirectory(i)}catch(o){t.push({directory:i,error:g(o)})}return t}
1
+ import{makeTempDirAsync as F}from"../../../process/files/temp-dir.js";import{toError as P}from"../../../kernel/error-serialization.js";import{classifyManagedProcessResult as O,terminalOutcome as h,processOutputEvidence as L,failureFromError as T,firstOutputLine as v,processExitDetails as S,processExitFailure as A,executeEngineTasks as N,emptyEvidence as D,taskTimeoutFailure as k,ConsumerPreparationError as b,consumerPreparationBoundary as M,evidenceCollectionFailure as C,taskOrchestrationEnvironment as x}from"../../adapters/execution.js";import{createInvocationWorkspace as U,requireConsumerFile as W,safeSegment as B}from"../../adapters/workspace.js";import H from"node:fs/promises";import Y from"node:path";import{setTimeout as $}from"node:timers/promises";import{pathToFileURL as G}from"node:url";import{isEngineTaskDeadlineExceededReason as j,resolveEngineTaskInputs as V}from"../../api/index.js";import{createNativeInvocationProcessEnvironment as K,runManagedProcess as J}from"../../../process/index.js";import{playwrightFileSuiteGrep as z,writePlaywrightTaskWorkspace as X}from"./config.js";import{collectPlaywrightEvidence as Q}from"./evidence.js";import{engineEvidenceIdentity as Z}from"../../adapters/evidence-identity.js";import{getNodeStartupCacheEnvironment as q}from"../../adapters/node-startup-cache.js";import{CHROMIUM_STARTUP_ENVIRONMENT as tt}from"../runtime/chromium-environment.js";const u={codePrefix:"PLAYWRIGHT",label:"Playwright"};export const PLAYWRIGHT_SPAWN_ENVIRONMENT_MAX_BYTES=24*1024;const R=64,rt=new Set(["EBUSY","ENOTEMPTY","EPERM"]),et=5;async function d(e){for(let t=0;;t+=1)try{await H.rm(e,{force:!0,recursive:!0});return}catch(r){const i=r instanceof Error&&"code"in r?r.code:void 0;if(t===et||typeof i!="string"||!rt.has(i))throw r;await $((t+1)*100)}}export async function executePlaywrightEngine(e,t,r){const i=await it(e,t,r);return N(e,i.tasks,u,(o,a)=>nt(e,i,o,a),ct(e,i.installation))}async function it(e,t,r){const{artifactRoot:i,invocationDirectory:o,rootDirectory:a}=await U(e,"playwright",r,d),s=await K(o);r.defer(()=>d(s.temporaryDirectory));const n=[];for(const[l,c]of e.tasks.entries()){const p=V(e,c),w=`${l.toString().padStart(3,"0")}-${B(c.id)}`,I=Y.join(o,w),f=await F(`${w}-`,{parentDir:i});try{const m=st(e,c.serviceName),y=await M("PlaywrightConsumerPreparationError",()=>W(a,c.file,"Playwright task file")),g=await X({artifactDirectory:f,baseUrl:m,browserFile:t.browserExecutable,databases:e.target.databases,endpoints:e.target.endpoints,handles:e.target.handles,inputs:p,invocationId:e.invocationId,managedModuleEntrypoints:{authoring:t.authoringEntrypoints.authoring,cli:t.family.cli,environment:t.authoringEntrypoints.environment,managedTest:t.authoringEntrypoints.managedTest,playwright:t.family.playwright.entrypoint,playwrightCore:t.family.playwrightCore.entrypoint,playwrightTest:t.family.playwrightTest.entrypoint,root:t.authoringEntrypoints.root,rootPackageJson:t.authoringEntrypoints.rootPackageJson},moduleFile:t.family.playwrightTest.entrypoint,rootDirectory:a,sourceFile:y,taskDirectory:I,taskId:c.id});r.defer(async()=>{try{await d(g.attachmentDirectory)}catch(E){if(!(E instanceof Error&&"code"in E&&E.code==="ENOTDIR"))throw E}}),n.push({artifactDirectory:f,inputs:p,task:c,preparationFailure:null,prepared:{baseUrl:m,sourceFile:y,workspace:g}})}catch(m){const y=(await lt([I,f])).map(g=>g.error);n.push({artifactDirectory:f,inputs:p,task:c,preparationFailure:playwrightPreparationFailure(m,y),prepared:null})}}return Object.freeze({installation:t,invocationEnvironment:s.environment,rootDirectory:a,tasks:Object.freeze(n)})}async function nt(e,t,r,i){const a={...await q(e.workspace.cacheDirectory,r.inputs.environment),...ot(e,t,r)};try{_(a)}catch{delete a.NODE_COMPILE_CACHE}const s=r.task.deadlineMs-Date.now();if(s<=0)return h(r.task.id,"failed",i,D(),k(r.task.timeoutMs,u));const n=await J({args:[t.installation.family.cli,"test","--grep",z(r.prepared.sourceFile),"--config",r.prepared.workspace.configFile],cwd:t.rootDirectory,displayLabel:"Playwright",env:a,executable:process.execPath,signal:e.signal,timeoutMs:s});let l;try{l=await Q({identity:{engineId:"playwright",invocationId:e.invocationId,taskId:r.task.id,sourceFile:r.prepared.sourceFile},artifactDirectory:r.artifactDirectory,outputDirectory:r.prepared.workspace.outputDirectory,reportFile:r.prepared.workspace.reportFile,reportersFinishedFile:r.prepared.workspace.reportersFinishedFile,rootDirectory:t.rootDirectory,sourceFile:r.prepared.sourceFile,stderr:n.stderr,stdout:n.stdout})}catch(p){return h(r.task.id,"failed",i,L(n),playwrightEvidenceCollectionFailure(n,p))}const c=classifyPlaywrightProcessResult(n,l,r.task.timeoutMs,j(e.signal.reason));return h(r.task.id,c.status,i,l.evidence,c.failure)}export function classifyPlaywrightProcessResult(e,t,r,i){const o=O(e,r,i,u);if(o!==null)return o;if(t.outputLimit!==null){const n=t.outputLimit;return{failure:{code:"PLAYWRIGHT_OUTPUT_ARTIFACT_LIMIT",details:{...n},message:`Playwright output exceeded its ${n.limit.toString()} ${n.kind==="bytes"?"byte aggregate":"entry"} artifact limit`},status:"failed"}}const a=t.attachmentFailures.find(n=>n.code==="PLAYWRIGHT_ATTACHMENT_LIMIT");if(a!==void 0)return{failure:a,status:"failed"};if(t.report.failure!==null){const n=t.report.failure;return{...t.report,failure:{...n,details:{...n.details,evidenceErrors:t.reportError===null?0:1,...S(e)}}}}const s=t.attachmentFailures.at(0);return s!==void 0?{failure:s,status:"failed"}:e.exitCode!==0&&(t.reportError===null||!t.reportersFinished)?A(e,u,v(e.stderr)??v(e.stdout)):t.reportError!==null?{failure:{...T(t.reportError,"PLAYWRIGHT_REPORT_INVALID"),classification:"infra"},status:"failed"}:t.report}export function playwrightEvidenceCollectionFailure(e,t){return C(e,u,t)}function ot(e,t,r){const i={...t.invocationEnvironment},o=r.task,a=r.prepared.workspace.inputBootstrapFile,s=r.prepared.workspace.resolutionBootstrapFile;return Object.assign(i,{...r.prepared.baseUrl===null?{}:{BASE_URL:r.prepared.baseUrl},...tt,FORCE_COLOR:"0",NO_COLOR:"1",PLAYWRIGHT_BROWSERS_PATH:t.installation.registry.canonicalValue,PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD:"1",TESTKIT_ACTIVE:"1",TESTKIT_INTERNAL_FILE_DEADLINE_MS:String(o.deadlineMs),TESTKIT_INTERNAL_FILE_TIMEOUT_SECONDS:String(Math.max(1,Math.ceil(o.timeoutMs/1e3))),...x(e.invocationId,o),TESTKIT_TEST_FILE:o.file,TESTKIT_TEST_ID:o.id}),i.NODE_OPTIONS=at(s,a),_(i),i}function at(e,t){return["--experimental-strip-types",`--require=${JSON.stringify(e)}`,`--import=${JSON.stringify(G(t).href)}`].join(" ")}export function playwrightSpawnEnvironmentBytes(e){return Object.entries(e).reduce((t,[r,i])=>t+(process.platform==="win32"?(r.length+i.length+2)*2:Buffer.byteLength(r,"utf8")+Buffer.byteLength(i,"utf8")+2),0)}function _(e){if(Object.keys(e).length>R)throw new Error(`Playwright physical spawn environment exceeds its ${R.toString()} entry limit`);if(playwrightSpawnEnvironmentBytes(e)>PLAYWRIGHT_SPAWN_ENVIRONMENT_MAX_BYTES)throw new Error(`Playwright physical spawn environment exceeds its ${PLAYWRIGHT_SPAWN_ENVIRONMENT_MAX_BYTES.toString()} byte limit`)}function st(e,t){const r=e.target.baseUrl;if(r===null)return null;if(typeof r!="string"||r.trim().length===0)throw new Error(`Playwright task service ${t} endpoint must be a non-empty URL`);let i;try{i=new URL(r)}catch(o){throw new Error(`Playwright task service ${t} endpoint is not a valid URL`,{cause:o})}if(i.protocol!=="http:"&&i.protocol!=="https:")throw new Error(`Playwright task service ${t} endpoint must use http or https`);return i.href}function ct(e,t){return{...Z({engineId:"playwright",installationKind:"root-playwright-family",profile:e.profile}),browserName:"chromium",packages:{playwright:t.family.playwright.version,playwrightCore:t.family.playwrightCore.version,playwrightTest:t.family.playwrightTest.version},registryMode:t.registry.mode}}export function playwrightPreparationFailure(e,t){if(t.length===0)return{...T(e,"PLAYWRIGHT_PREPARE_FAILED"),classification:e instanceof b?"product":"infra"};const r=P(e);return{classification:"infra",code:"PLAYWRIGHT_PREPARE_CLEANUP_FAILED",details:{cleanupFailures:t.map(i=>i.message),preparationFailure:r.message},message:`Playwright task preparation failed and its partial workspace could not be removed: ${t.map(i=>i.message).join("; ")}`,...r.stack===void 0?{}:{stack:r.stack}}}async function lt(e){const t=[];for(const r of e)try{await d(r)}catch(i){t.push({directory:r,error:P(i)})}return t}
@@ -1,8 +1,8 @@
1
- import{ownedRuntimeInputModuleUrl as b}from"../../runtime-input-file.js";import P from"node:crypto";import s from"node:fs/promises";import o from"node:path";import{fileURLToPath as j,pathToFileURL as y}from"node:url";import{createManagedNodeModuleResolution as v}from"../../managed-module-resolution.js";import{materializePlaywrightRuntimeInput as T,playwrightRuntimeInputBootstrapSource as O}from"../runtime/runtime-input.js";import{nativeReporterFile as k}from"../../adapters/native-report.js";import{requireConsumerFile as S}from"../../adapters/workspace.js";export class PlaywrightConsumerConfigurationError extends Error{constructor(r){const t=r instanceof Error?r:new Error(String(r));super(t.message,{cause:t}),this.name="PlaywrightConsumerConfigurationError"}}export async function writePlaywrightTaskWorkspace(e){let r;try{D(e.inputs.supportModules),r=await I(e.rootDirectory,e.inputs)}catch(w){throw new PlaywrightConsumerConfigurationError(w)}await s.mkdir(e.taskDirectory,{recursive:!0}),await s.mkdir(e.artifactDirectory,{recursive:!0});const t=P.randomUUID(),n=o.join(e.taskDirectory,`playwright-${t}.config.mjs`),a=o.join(e.taskDirectory,`playwright-${t}.input-bootstrap.mjs`),l=o.join(e.taskDirectory,`playwright-${t}.resolution-bootstrap.cjs`),d=o.join(e.taskDirectory,"testkit-playwright-report.json"),g=o.join(e.taskDirectory,"testkit-playwright-reporters-finished"),p=o.join(e.artifactDirectory,"playwright-output");await s.mkdir(p);const c=o.join(e.artifactDirectory,"playwright-attachments");await s.mkdir(c,{mode:448,recursive:!1});const u=await T({baseUrl:e.baseUrl,databases:e.databases,directory:e.taskDirectory,endpoints:e.endpoints,handles:e.handles,inputs:e.inputs,invocationId:e.invocationId,orchestration:e.orchestration,taskId:e.taskId}),f=k("playwright");return await h(l,playwrightResolutionBootstrapSource(e.managedModuleEntrypoints,{consumerRoots:[e.rootDirectory],trustedImporterFiles:[n,a],trustedInternalEntrypoints:[e.moduleFile,f,j(b)]})),await h(a,O({digest:u.digest,file:u.file,sizeBytes:u.sizeBytes,invocationId:e.invocationId,taskId:e.taskId})),await h(n,x({baseConfigFile:r,baseUrl:e.baseUrl,browserFile:e.browserFile,inputBootstrapFile:a,outputDirectory:p,reporterFile:f,report:{attachmentDirectory:c,identity:{engineId:"playwright",invocationId:e.invocationId,taskId:e.taskId,sourceFile:e.sourceFile},reportFile:d,reportersFinishedFile:g,workspaceDirectory:e.taskDirectory},retries:typeof e.inputs.configuration?.retries=="number"?e.inputs.configuration.retries:null,rootDirectory:e.rootDirectory,sourceFile:e.sourceFile,testkitPackageDirectory:await s.realpath(o.dirname(e.managedModuleEntrypoints.rootPackageJson))})),{attachmentDirectory:c,configFile:n,inputBootstrapFile:a,outputDirectory:p,reportFile:d,reportersFinishedFile:g,resolutionBootstrapFile:l}}export function playwrightResolutionBootstrapSource(e,r){const t={authoring:i(e.authoring,"authoring"),cli:i(e.cli,"cli"),environment:i(e.environment,"environment"),managedTest:i(e.managedTest,"managedTest"),playwright:i(e.playwright,"playwright"),playwrightCore:i(e.playwrightCore,"playwrightCore"),playwrightTest:i(e.playwrightTest,"playwrightTest"),root:i(e.root,"root"),rootPackageJson:i(e.rootPackageJson,"rootPackageJson")},n={"@playwright/test":t.playwrightTest,playwright:t.playwright,"playwright-core":t.playwrightCore};return v({consumerRoots:r.consumerRoots,direct:{"@gfdelarue/testkit":t.root,"@gfdelarue/testkit/environment":t.environment,"@gfdelarue/testkit/package.json":t.rootPackageJson,"@gfdelarue/testkit/playwright":t.authoring,"@playwright/test":t.managedTest,"playwright/test":t.managedTest},directBridgeRoots:[o.dirname(t.root)],engineLabel:"Playwright",forbiddenConsumerPackagePrefixes:[],packageAnchors:n,reservedPackageNames:["@gfdelarue/testkit"],stateSymbol:"@gfdelarue/testkit/playwright-resolution-hooks/v1",trustedImporterFiles:r.trustedImporterFiles,trustedInternalEntrypoints:r.trustedInternalEntrypoints,verifiedParentlessPackageFiles:{playwright:["lib/loader/loaderProcessEntry.js","lib/worker/workerProcessEntry.js"]},verifiedProcessEntrypoints:[t.cli]}).bootstrapSource}function i(e,r){if(!o.isAbsolute(e))throw new Error(`Resolved Playwright installation ${r} entrypoint must be absolute`);return e}function x(e){return`
1
+ import{ownedRuntimeInputModuleUrl as g}from"../../runtime-input-file.js";import b from"node:crypto";import i from"node:fs/promises";import o from"node:path";import{fileURLToPath as w,pathToFileURL as m}from"node:url";import{createManagedNodeModuleResolution as P}from"../../managed-module-resolution.js";import{materializePlaywrightRuntimeInput as j,playwrightRuntimeInputBootstrapSource as v}from"../runtime/runtime-input.js";import{nativeReporterFile as T}from"../../adapters/native-report.js";import{requireConsumerFile as O}from"../../adapters/workspace.js";import{consumerPreparationBoundary as k}from"../../adapters/execution.js";export async function writePlaywrightTaskWorkspace(e){const t=await k("PlaywrightConsumerConfigurationError",()=>(I(e.inputs.supportModules),x(e.rootDirectory,e.inputs)));await i.mkdir(e.taskDirectory,{recursive:!0}),await i.mkdir(e.artifactDirectory,{recursive:!0});const r=b.randomUUID(),n=o.join(e.taskDirectory,`playwright-${r}.config.mjs`),a=o.join(e.taskDirectory,`playwright-${r}.input-bootstrap.mjs`),s=o.join(e.taskDirectory,`playwright-${r}.resolution-bootstrap.cjs`),h=o.join(e.taskDirectory,"testkit-playwright-report.json"),d=o.join(e.taskDirectory,"testkit-playwright-reporters-finished"),l=o.join(e.artifactDirectory,"playwright-output");await i.mkdir(l);const p=o.join(e.artifactDirectory,"playwright-attachments");await i.mkdir(p,{mode:448,recursive:!1});const c=await j({baseUrl:e.baseUrl,databases:e.databases,directory:e.taskDirectory,endpoints:e.endpoints,handles:e.handles,inputs:e.inputs,invocationId:e.invocationId,taskId:e.taskId}),f=T("playwright");return await u(s,playwrightResolutionBootstrapSource(e.managedModuleEntrypoints,{consumerRoots:[e.rootDirectory],trustedImporterFiles:[n,a],trustedInternalEntrypoints:[e.moduleFile,f,w(g)]})),await u(a,v({digest:c.digest,file:c.file,sizeBytes:c.sizeBytes,invocationId:e.invocationId,taskId:e.taskId})),await u(n,S({baseConfigFile:t,baseUrl:e.baseUrl,browserFile:e.browserFile,inputBootstrapFile:a,outputDirectory:l,reporterFile:f,report:{attachmentDirectory:p,identity:{engineId:"playwright",invocationId:e.invocationId,taskId:e.taskId,sourceFile:e.sourceFile},reportFile:h,reportersFinishedFile:d,workspaceDirectory:e.taskDirectory},retries:typeof e.inputs.configuration?.retries=="number"?e.inputs.configuration.retries:null,rootDirectory:e.rootDirectory,sourceFile:e.sourceFile,testkitPackageDirectory:await i.realpath(o.dirname(e.managedModuleEntrypoints.rootPackageJson))})),{attachmentDirectory:p,configFile:n,inputBootstrapFile:a,outputDirectory:l,reportFile:h,reportersFinishedFile:d,resolutionBootstrapFile:s}}export function playwrightResolutionBootstrapSource(e,t){const r={"@playwright/test":e.playwrightTest,playwright:e.playwright,"playwright-core":e.playwrightCore};return P({consumerRoots:t.consumerRoots,direct:{"@gfdelarue/testkit":e.root,"@gfdelarue/testkit/environment":e.environment,"@gfdelarue/testkit/package.json":e.rootPackageJson,"@gfdelarue/testkit/playwright":e.authoring,"@playwright/test":e.managedTest,"playwright/test":e.managedTest},directBridgeRoots:[o.dirname(e.root)],engineLabel:"Playwright",forbiddenConsumerPackagePrefixes:[],packageAnchors:r,reservedPackageNames:["@gfdelarue/testkit"],stateSymbol:"@gfdelarue/testkit/playwright-resolution-hooks/v1",trustedImporterFiles:t.trustedImporterFiles,trustedInternalEntrypoints:t.trustedInternalEntrypoints,verifiedParentlessPackageFiles:{playwright:["lib/loader/loaderProcessEntry.js","lib/worker/workerProcessEntry.js"]},verifiedProcessEntrypoints:[e.cli]}).bootstrapSource}function S(e){return`
2
2
  import path from "node:path";
3
3
  import { createRequire } from "node:module";
4
4
 
5
- const OPTIONS = ${JSON.stringify({baseConfigUrl:e.baseConfigFile===null?null:y(e.baseConfigFile).href,baseUrl:e.baseUrl,browserFile:e.browserFile,exactTaskMatchSource:playwrightExactFileFilter(e.sourceFile),originalConfigDirectory:e.baseConfigFile===null?e.rootDirectory:o.dirname(e.baseConfigFile),originalConfigFile:e.baseConfigFile??o.join(e.rootDirectory,"__testkit_playwright_config__.mjs"),inputBootstrapUrl:y(e.inputBootstrapFile).href,outputDirectory:e.outputDirectory,report:e.report,reporterFile:e.reporterFile,retries:e.retries,rootDirectory:e.rootDirectory,sourceFile:e.sourceFile,testkitPackageMatchSource:`^${m(e.testkitPackageDirectory)}[\\\\/]`})};
5
+ const OPTIONS = ${JSON.stringify({baseConfigUrl:e.baseConfigFile===null?null:m(e.baseConfigFile).href,baseUrl:e.baseUrl,browserFile:e.browserFile,exactTaskMatchSource:playwrightExactFileFilter(e.sourceFile),originalConfigDirectory:e.baseConfigFile===null?e.rootDirectory:o.dirname(e.baseConfigFile),originalConfigFile:e.baseConfigFile??o.join(e.rootDirectory,"__testkit_playwright_config__.mjs"),inputBootstrapUrl:m(e.inputBootstrapFile).href,outputDirectory:e.outputDirectory,report:e.report,reporterFile:e.reporterFile,retries:e.retries,rootDirectory:e.rootDirectory,sourceFile:e.sourceFile,testkitPackageMatchSource:`^${y(e.testkitPackageDirectory)}[\\\\/]`})};
6
6
  const inputBootstrap = await import(OPTIONS.inputBootstrapUrl);
7
7
  if (typeof inputBootstrap.installPlaywrightRuntimeInput !== "function")
8
8
  throw new Error("Managed Playwright runtime-input bootstrap is invalid");
@@ -197,4 +197,4 @@ export default {
197
197
  fullyParallel: false,
198
198
  workers: 1,
199
199
  };
200
- `}export function playwrightExactFileFilter(e){return`^${m(e)}$`}function m(e){return e.split(/[\\/]+/u).map(r=>r.replace(/[.*+?^${}()|[\]\\]/gu,"\\$&")).join("[\\\\/]")}export function playwrightFileSuiteGrep(e){const r=e.split(/[\\/]+/u).filter(a=>a!=="").map(a=>a.replace(/[.*+?^${}()|[\]\\]/gu,"\\$&")),t=r.pop()??"";return`(?:^| )(?:\\.\\.[\\\\/])*${r.reduce((a,l)=>`(?:${a}${l}[\\\\/])?`,"")}${t}(?: |$)`}async function I(e,r){if(!Object.hasOwn(r.supportModules,"playwrightConfig"))return null;const t=r.supportModules.playwrightConfig;if(!o.isAbsolute(t))throw new Error("supportModules.playwrightConfig must be an absolute path");return S(e,t,"supportModules.playwrightConfig")}function D(e){const r=Object.keys(e).filter(t=>t!=="playwrightConfig");if(r.length>0)throw new Error(`Playwright engine does not support support module(s): ${r.join(", ")}`)}async function h(e,r){const t=await s.open(e,"wx",384);try{await t.writeFile(r,"utf8")}finally{await t.close()}}
200
+ `}export function playwrightExactFileFilter(e){return`^${y(e)}$`}function y(e){return e.split(/[\\/]+/u).map(t=>t.replace(/[.*+?^${}()|[\]\\]/gu,"\\$&")).join("[\\\\/]")}export function playwrightFileSuiteGrep(e){const t=e.split(/[\\/]+/u).filter(a=>a!=="").map(a=>a.replace(/[.*+?^${}()|[\]\\]/gu,"\\$&")),r=t.pop()??"";return`(?:^| )(?:\\.\\.[\\\\/])*${t.reduce((a,s)=>`(?:${a}${s}[\\\\/])?`,"")}${r}(?: |$)`}async function x(e,t){if(!Object.hasOwn(t.supportModules,"playwrightConfig"))return null;const r=t.supportModules.playwrightConfig;if(!o.isAbsolute(r))throw new Error("supportModules.playwrightConfig must be an absolute path");return O(e,r,"supportModules.playwrightConfig")}function I(e){const t=Object.keys(e).filter(r=>r!=="playwrightConfig");if(t.length>0)throw new Error(`Playwright engine does not support support module(s): ${t.join(", ")}`)}async function u(e,t){const r=await i.open(e,"wx",384);try{await r.writeFile(t,"utf8")}finally{await r.close()}}
@@ -1 +1 @@
1
- export{executePlaywrightEngine}from"./adapter.js";export{readPlaywrightContext}from"../runtime/runtime-input.js";export{playwrightResolutionBootstrapSource}from"./config.js";
1
+ export{executePlaywrightEngine}from"./adapter.js";
@@ -1 +1 @@
1
- import N from"crypto";import{isTerminalRunStatus as z}from"../process/runs/schema.js";import{observeProcessCreationIdentity as x}from"../process/process-identity.js";import{protectTestkitStateRoot as W}from"../process/files/product-dir.js";export{isRunManifestActive}from"../process/runs/inspection.js";import{normalizeOperationReason as U}from"../domain/operation-reason.js";import{captureProcessCreationIdentity as V}from"../process/service-processes.js";import{requireProductLifecycleFenceProductDir as B,withProductLifecycleFence as Y}from"../process/lifecycle-fence.js";import{createRunManifest as J,listRunManifests as K,readRunMaintenanceStopRequest as Q,removeRunManifestFile as _,requireSafeRunId as X,RUN_MANIFEST_SCHEMA_VERSION as Z,runManifestPath as ee,writeRunManifest as te}from"../process/runs/manifest.js";const ne=1e3,re=String,k=new WeakMap;export function createUnpublishedRunLifecycle(n,r){const s=r.runId===void 0?ue():X(r.runId,"Testkit run id");W(n,!0);const u=ee(n,s),o=new AbortController,I=new AbortController,d=r.pid===void 0?process.pid:r.pid;if(!Number.isSafeInteger(d)||d<0)throw new Error("Testkit run pid must be a non-negative safe integer");const w=d>0?V(d):null;if(d>0&&w===null)throw new Error(`Could not capture the Testkit run process identity for pid ${d.toString()}`);let f={incarnationId:N.randomUUID(),schemaVersion:Z,runId:s,pid:d,processIdentity:typeof w=="string"?w:null,controller:se(r.controller),status:"starting",startedAt:new Date().toISOString(),heartbeatAt:r.heartbeat===!1?null:new Date().toISOString(),interruptReason:null,services:[],runtimeDirs:[]};const O=[];let S,C=null;const y=new Set,b=new Set,M=r.keepAlive===!0?setInterval(()=>{},ne):null;let R=null,g=null,v=null,L=!1;function $(e){const t=k.get(h);if(!t)throw new Error("Testkit run lifecycle ownership is unavailable");if(!t.published){J(n,e),t.published=!0;return}te(n,e)}function A(){const e=Q(n,s);return e===null||e.targetIncarnationId!==f.incarnationId||e.targetPid!==d||e.targetProcessIdentity!==f.processIdentity||e.coordinatorPid===C?.coordinatorPid&&e.coordinatorProcessIdentity===C.coordinatorProcessIdentity?null:x(e.coordinatorPid,e.coordinatorProcessIdentity)==="different"?(C=e,null):e}function m(e){E("mutate run state");const t=ie(f);e(t),t.heartbeatAt!==null&&(t.heartbeatAt=new Date().toISOString()),$(t),f=t}function E(e){if(g!==null)throw new Error(`Cannot ${e} after Testkit run ${s} finalization is committed`)}function G(){R||(R=setInterval(()=>{},1e3))}function P(){R&&(clearInterval(R),R=null)}function q(){for(const e of b)clearInterval(e);b.clear()}function F(e="controller process exited before Testkit run finalized"){if(L||z(f.status))return!1;const t=U(e,"controller process exited before Testkit run finalized"),i=new Date,a=f.status==="interrupting"||o.signal.aborted?"interrupted":"failed",l=a==="interrupted"?f.interruptReason??v??t:t;return m(p=>{p.status=a,p.finishedAt=i.toISOString(),p.interruptReason=l,p.heartbeatAt!==null&&(p.heartbeatAt=i.toISOString())}),r.evidence.controllerExited(n,{runId:s,reason:l,status:a,finishedAt:i}),!0}const H=()=>{F()};process.on("exit",H);const h={runId:s,manifestPath:u,maintenanceStopSignal:I.signal,signal:o.signal,commitFinalization(e="finished"){if(g!==null)return g;const t=A(),i=o.signal.aborted||t!==null||f.status==="interrupting",a=i?f.interruptReason??v??t?.reason??"run interrupted":null,l=Object.freeze({interruptReason:a,interrupted:i});return m(p=>{p.status=i?"interrupted":e,p.finishedAt=new Date().toISOString(),p.interruptReason=a}),g=l,q(),l},getInterruptReason(){return f.interruptReason??v},isMaintenanceStopCoordinated(){return A()!==null},isStopRequested(){return o.signal.aborted||g?.interrupted===!0},markRunning(){m(e=>{e.status="running"})},markFinished(e="finished"){m(t=>{t.status=e,t.finishedAt=new Date().toISOString()})},heartbeat(e={}){m(t=>{t.heartbeatAt=new Date().toISOString(),e&&typeof e=="object"&&!Array.isArray(e)&&(t.session={...t.session||{},...e})})},startHeartbeat(e=5e3,t=null){E("start a run heartbeat");const i=Number.isFinite(Number(e))&&Number(e)>0?Math.floor(Number(e)):5e3,a=setInterval(()=>{let l;try{l=typeof t=="function"?t():{}}catch{l={}}h.heartbeat(l)},i);return a.unref(),b.add(a),()=>{clearInterval(a),b.delete(a)}},requestStop(e="interrupted"){if(g!==null||z(f.status))return;const t=A(),i=t?t.reason:U(e,"interrupted"),a=!o.signal.aborted;a&&(v=i),G();let l=null;if(a)try{m(c=>{c.status="interrupting",c.interruptReason=i})}catch(c){l=c instanceof Error?c:new Error("Failed to persist the Testkit stop request",{cause:c})}t!==null&&!I.signal.aborted&&I.abort(new Error(`Testkit run ${s} is being stopped by external maintenance`));const p=[...y].map(({supervisor:c})=>c.stop(t===null?void 0:Date.parse(t.requestedAt)+c.shutdownTimeoutMs));if(Promise.allSettled(p).then(P),o.signal.aborted||o.abort(new Error(`testkit run interrupted (${i})`)),!!a&&l!==null)throw l},trackGraphContext(e){m(t=>{ae(t.runtimeDirs,e.runtimeDir)})},registerService(e,t,i,a){if(E("register application service ownership"),i.pid!==t.pid||(process.platform==="win32"?i.recovery.processGroupId!==null:i.recovery.processGroupId!==i.pid))throw new Error("Application service supervisor returned an invalid ownership handoff");const l={supervisor:t,unsubscribe:()=>{}};y.add(l);const p=oe(e);m(c=>{c.services=c.services.filter(D=>D.pid!==i.pid),c.services.push({serviceName:e.name,runtimeLabel:e.runtimeLabel,command:e.testkit.local.start||null,cwd:a,pid:i.pid,processGroupId:i.recovery.processGroupId,processIdentity:i.recovery.creationIdentity,ports:p,startedAt:new Date().toISOString(),shutdownTimeoutMs:t.shutdownTimeoutMs,shutdownDeadlineAt:t.shutdownDeadlineAt})}),l.unsubscribe=t.onShutdownStarted(c=>{m(D=>{const T=D.services.find(j=>j.pid===i.pid);T&&(T.shutdownDeadlineAt=T.shutdownDeadlineAt==null?c:Math.min(T.shutdownDeadlineAt,c))})})},unregisterService(e){E("unregister application service ownership");for(const t of y)t.supervisor.pid===e&&(t.unsubscribe(),y.delete(t));m(t=>{t.services=t.services.filter(i=>i.pid!==e)})},installSignalHandlers(){if(!(O.length>0)&&!(d!==process.pid||x(d,f.processIdentity)!=="matches")){for(const e of["SIGINT","SIGTERM","SIGHUP"]){const t=h.requestStop.bind(h,e);process.on(e,t),O.push([e,t])}process.platform==="win32"&&(S=setInterval(()=>{const e=A();e!==null&&(clearInterval(S),h.requestStop(e.reason))},100),S.unref(),b.add(S))}},removeSignalHandlers(){S&&(clearInterval(S),b.delete(S),S=void 0);for(const[e,t]of O.splice(0))process.off(e,t)},async removeManifest(){await Y(n,e=>cleanupOwnedRunLifecycleWithinLifecycleFence(e,h))},finalizeForControllerExit:F,dispose(){L=!0;for(const e of y)e.unsubscribe();process.off("exit",H),h.removeSignalHandlers(),M&&clearInterval(M),P(),q()}};return k.set(h,{incarnationId:f.incarnationId,pid:d,processIdentity:f.processIdentity,productDir:n,published:!1,runId:s}),h}function ie(n){const r={...n,controller:n.controller===null?null:{...n.controller},runtimeDirs:[...n.runtimeDirs],services:n.services.map(s=>({...s,ports:s.ports.map(u=>({...u}))}))};return n.session!==void 0&&(r.session=n.session===null?null:{...n.session}),r}function se(n){return!n||typeof n!="object"||Array.isArray(n)?null:Object.fromEntries(Object.entries(n).filter(([,r])=>r!==void 0).map(([r,s])=>[r,s===null?null:re(s)]))}export function cleanupOwnedRunLifecycleWithinLifecycleFence(n,r){const{manifest:s,ownership:u,productDir:o}=readOwnedRunLifecycleWithinLifecycleFence(n,r);if(!s)return!1;if(s.services.length>0)throw new Error(`Cannot clean Testkit run ${r.runId} service ownership while holding the lifecycle fence`);return _(o,r.runId),u.published=!1,!0}export function readOwnedRunLifecycleWithinLifecycleFence(n,r){const s=B(n),u=k.get(r);if(!u||u.productDir!==s)throw new Error("Cannot clean a Testkit run without matching lifecycle ownership");if(u.runId!==r.runId)throw new Error("Testkit run lifecycle identity has been modified");const o=K(s).find(I=>I.runId===r.runId);if(o!==void 0&&(o.incarnationId!==u.incarnationId||o.pid!==u.pid||o.processIdentity!==u.processIdentity))throw new Error(`Testkit run ${r.runId} ownership no longer matches its manifest`);return{manifest:o??null,ownership:u,productDir:s}}function oe(n){const r=new Set,s=[];for(const u of[n.testkit.local.baseUrl,n.testkit.local.readyUrl])if(u)try{const o=new URL(u),I=le(o.hostname),d=Number(o.port||(o.protocol==="https:"?443:80)),w=`${I}:${d.toString()}`;if(r.has(w))continue;r.add(w),s.push({host:I,port:d})}catch{}return s}function ae(n,r){r&&(n.includes(r)||n.push(r))}function ue(){return`${Date.now().toString()}-${process.pid.toString()}-${N.randomBytes(4).toString("hex")}`}function le(n){return n?n==="::1"?"127.0.0.1":n:"127.0.0.1"}export function disposeFailedRunLifecycle(n){const r=k.get(n);n.dispose(),n.removeSignalHandlers(),r?.published===!0&&_(r.productDir,n.runId)}
1
+ import x from"crypto";import{isTerminalRunStatus as z}from"../process/runs/schema.js";import{observeProcessCreationIdentity as U}from"../process/process-identity.js";import{protectTestkitStateRoot as W}from"../process/files/product-dir.js";export{isRunManifestActive}from"../process/runs/inspection.js";import{normalizeOperationReason as $}from"../domain/operation-reason.js";import{captureProcessCreationIdentity as V}from"../process/service-processes.js";import{requireProductLifecycleFenceProductDir as B,withProductLifecycleFence as Y}from"../process/lifecycle-fence.js";import{createRunManifest as J,listRunManifests as K,readRunMaintenanceStopRequest as Q,removeRunManifestFile as N,requireSafeRunId as X,RUN_MANIFEST_SCHEMA_VERSION as Z,runManifestPath as ee,writeRunManifest as te}from"../process/runs/manifest.js";const ne=1e3,re=String,k=new WeakMap;export function createUnpublishedRunLifecycle(n,r){const s=r.runId===void 0?ue():X(r.runId,"Testkit run id");W(n,!0);const a=ee(n,s),o=new AbortController,I=new AbortController,p=r.pid===void 0?process.pid:r.pid,w=p>0?V(p):null;if(p>0&&w===null)throw new Error(`Could not capture the Testkit run process identity for pid ${p.toString()}`);let c={incarnationId:x.randomUUID(),schemaVersion:Z,runId:s,pid:p,processIdentity:typeof w=="string"?w:null,controller:se(r.controller),status:"starting",startedAt:new Date().toISOString(),heartbeatAt:r.heartbeat===!1?null:new Date().toISOString(),interruptReason:null,services:[],runtimeDirs:[]};const O=[];let S,C=null;const y=new Set,b=new Set,M=r.keepAlive===!0?setInterval(()=>{},ne):null;let R=null,g=null,v=null,L=!1;function _(e){const t=k.get(h);if(!t)throw new Error("Testkit run lifecycle ownership is unavailable");if(!t.published){J(n,e),t.published=!0;return}te(n,e)}function A(){const e=Q(n,s);return e===null||e.targetIncarnationId!==c.incarnationId||e.targetPid!==p||e.targetProcessIdentity!==c.processIdentity||e.coordinatorPid===C?.coordinatorPid&&e.coordinatorProcessIdentity===C.coordinatorProcessIdentity?null:U(e.coordinatorPid,e.coordinatorProcessIdentity)==="different"?(C=e,null):e}function m(e){E("mutate run state");const t=ie(c);e(t),t.heartbeatAt!==null&&(t.heartbeatAt=new Date().toISOString()),_(t),c=t}function E(e){if(g!==null)throw new Error(`Cannot ${e} after Testkit run ${s} finalization is committed`)}function G(){R||(R=setInterval(()=>{},1e3))}function P(){R&&(clearInterval(R),R=null)}function q(){for(const e of b)clearInterval(e);b.clear()}function H(e="controller process exited before Testkit run finalized"){if(L||z(c.status))return!1;const t=$(e,"controller process exited before Testkit run finalized"),i=new Date,u=c.status==="interrupting"||o.signal.aborted?"interrupted":"failed",f=u==="interrupted"?c.interruptReason??v??t:t;return m(d=>{d.status=u,d.finishedAt=i.toISOString(),d.interruptReason=f,d.heartbeatAt!==null&&(d.heartbeatAt=i.toISOString())}),r.evidence.controllerExited(n,{runId:s,reason:f,status:u,finishedAt:i}),!0}const F=()=>{H()};process.on("exit",F);const h={runId:s,manifestPath:a,maintenanceStopSignal:I.signal,signal:o.signal,commitFinalization(e="finished"){if(g!==null)return g;const t=A(),i=o.signal.aborted||t!==null||c.status==="interrupting",u=i?c.interruptReason??v??t?.reason??"run interrupted":null,f=Object.freeze({interruptReason:u,interrupted:i});return m(d=>{d.status=i?"interrupted":e,d.finishedAt=new Date().toISOString(),d.interruptReason=u}),g=f,q(),f},getInterruptReason(){return c.interruptReason??v},isMaintenanceStopCoordinated(){return A()!==null},isStopRequested(){return o.signal.aborted||g?.interrupted===!0},markRunning(){m(e=>{e.status="running"})},markFinished(e="finished"){m(t=>{t.status=e,t.finishedAt=new Date().toISOString()})},heartbeat(e={}){m(t=>{t.heartbeatAt=new Date().toISOString(),e&&typeof e=="object"&&!Array.isArray(e)&&(t.session={...t.session||{},...e})})},startHeartbeat(e=5e3,t=null){E("start a run heartbeat");const i=setInterval(()=>{let u;try{u=typeof t=="function"?t():{}}catch{u={}}h.heartbeat(u)},e);return i.unref(),b.add(i),()=>{clearInterval(i),b.delete(i)}},requestStop(e="interrupted"){if(g!==null||z(c.status))return;const t=A(),i=t?t.reason:$(e,"interrupted"),u=!o.signal.aborted;u&&(v=i),G();let f=null;if(u)try{m(l=>{l.status="interrupting",l.interruptReason=i})}catch(l){f=l instanceof Error?l:new Error("Failed to persist the Testkit stop request",{cause:l})}t!==null&&!I.signal.aborted&&I.abort(new Error(`Testkit run ${s} is being stopped by external maintenance`));const d=[...y].map(({supervisor:l})=>l.stop(t===null?void 0:Date.parse(t.requestedAt)+l.shutdownTimeoutMs));if(Promise.allSettled(d).then(P),o.signal.aborted||o.abort(new Error(`testkit run interrupted (${i})`)),!!u&&f!==null)throw f},trackGraphContext(e){m(t=>{ae(t.runtimeDirs,e.runtimeDir)})},registerService(e,t,i,u){if(E("register application service ownership"),i.pid!==t.pid||(process.platform==="win32"?i.recovery.processGroupId!==null:i.recovery.processGroupId!==i.pid))throw new Error("Application service supervisor returned an invalid ownership handoff");const f={supervisor:t,unsubscribe:()=>{}};y.add(f);const d=oe(e);m(l=>{l.services=l.services.filter(D=>D.pid!==i.pid),l.services.push({serviceName:e.name,runtimeLabel:e.runtimeLabel,command:e.testkit.local.start||null,cwd:u,pid:i.pid,processGroupId:i.recovery.processGroupId,processIdentity:i.recovery.creationIdentity,ports:d,startedAt:new Date().toISOString(),shutdownTimeoutMs:t.shutdownTimeoutMs,shutdownDeadlineAt:t.shutdownDeadlineAt})}),f.unsubscribe=t.onShutdownStarted(l=>{m(D=>{const T=D.services.find(j=>j.pid===i.pid);T&&(T.shutdownDeadlineAt=T.shutdownDeadlineAt==null?l:Math.min(T.shutdownDeadlineAt,l))})})},unregisterService(e){E("unregister application service ownership");for(const t of y)t.supervisor.pid===e&&(t.unsubscribe(),y.delete(t));m(t=>{t.services=t.services.filter(i=>i.pid!==e)})},installSignalHandlers(){if(!(O.length>0)&&!(p!==process.pid||U(p,c.processIdentity)!=="matches")){for(const e of["SIGINT","SIGTERM","SIGHUP"]){const t=h.requestStop.bind(h,e);process.on(e,t),O.push([e,t])}process.platform==="win32"&&(S=setInterval(()=>{const e=A();e!==null&&(clearInterval(S),h.requestStop(e.reason))},100),S.unref(),b.add(S))}},removeSignalHandlers(){S&&(clearInterval(S),b.delete(S),S=void 0);for(const[e,t]of O.splice(0))process.off(e,t)},async removeManifest(){await Y(n,e=>cleanupOwnedRunLifecycleWithinLifecycleFence(e,h))},finalizeForControllerExit:H,dispose(){L=!0;for(const e of y)e.unsubscribe();process.off("exit",F),h.removeSignalHandlers(),M&&clearInterval(M),P(),q()}};return k.set(h,{incarnationId:c.incarnationId,pid:p,processIdentity:c.processIdentity,productDir:n,published:!1,runId:s}),h}function ie(n){const r={...n,controller:n.controller===null?null:{...n.controller},runtimeDirs:[...n.runtimeDirs],services:n.services.map(s=>({...s,ports:s.ports.map(a=>({...a}))}))};return n.session!==void 0&&(r.session=n.session===null?null:{...n.session}),r}function se(n){return!n||typeof n!="object"||Array.isArray(n)?null:Object.fromEntries(Object.entries(n).filter(([,r])=>r!==void 0).map(([r,s])=>[r,s===null?null:re(s)]))}export function cleanupOwnedRunLifecycleWithinLifecycleFence(n,r){const{manifest:s,ownership:a,productDir:o}=readOwnedRunLifecycleWithinLifecycleFence(n,r);if(!s)return!1;if(s.services.length>0)throw new Error(`Cannot clean Testkit run ${r.runId} service ownership while holding the lifecycle fence`);return N(o,r.runId),a.published=!1,!0}export function readOwnedRunLifecycleWithinLifecycleFence(n,r){const s=B(n),a=k.get(r);if(!a||a.productDir!==s)throw new Error("Cannot clean a Testkit run without matching lifecycle ownership");if(a.runId!==r.runId)throw new Error("Testkit run lifecycle identity has been modified");const o=K(s).find(I=>I.runId===r.runId);if(o!==void 0&&(o.incarnationId!==a.incarnationId||o.pid!==a.pid||o.processIdentity!==a.processIdentity))throw new Error(`Testkit run ${r.runId} ownership no longer matches its manifest`);return{manifest:o??null,ownership:a,productDir:s}}function oe(n){const r=new Set,s=[];for(const a of[n.testkit.local.baseUrl,n.testkit.local.readyUrl])if(a)try{const o=new URL(a),I=le(o.hostname),p=Number(o.port||(o.protocol==="https:"?443:80)),w=`${I}:${p.toString()}`;if(r.has(w))continue;r.add(w),s.push({host:I,port:p})}catch{}return s}function ae(n,r){r&&(n.includes(r)||n.push(r))}function ue(){return`${Date.now().toString()}-${process.pid.toString()}-${x.randomBytes(4).toString("hex")}`}function le(n){return n?n==="::1"?"127.0.0.1":n:"127.0.0.1"}export function disposeFailedRunLifecycle(n){const r=k.get(n);n.dispose(),n.removeSignalHandlers(),r?.published===!0&&N(r.productDir,n.runId)}
@@ -1 +1 @@
1
- import{finalizeRuntimeDatabaseConfig as w}from"../database/runtime-config.js";import h from"path";import{finalizeConfiguredInputs as E,finalizeConfiguredSteps as U}from"../steps/configured-steps.js";import"../config/loader/index.js";import{finalizeString as b,resolveTemplateString as y}from"../steps/index.js";export function resolveRuntimeInstanceConfigs(e,n,r,s={}){const c=buildRuntimeLabel(s.graphDirName,n),p=s.ports??new Map,a=new Map,l=new Map,u=new Map,d=new Map,f=new Map,x=k(s.externalEndpoints),m=k(s.publicEndpoints),v=k(s.privateEndpoints),o=k(s.serviceContextEndpoints);for(const t of e)t.testkit.local&&(t.testkit.local.baseUrl!==void 0&&a.set(t.name,resolveRuntimeUrl(t.testkit.local.baseUrl,t.name,n,{runtimeId:n,runtimeLabel:c,runtimeDir:r,portMap:p,baseUrlByService:a,readyUrlByService:l})),t.testkit.local.readyUrl!==!1&&t.testkit.local.readyUrl!=null&&l.set(t.name,resolveRuntimeUrl(t.testkit.local.readyUrl,t.name,n,{runtimeId:n,runtimeLabel:c,runtimeDir:r,portMap:p,baseUrlByService:a,readyUrlByService:l})));for(const[t,i]of Object.entries(x))a.has(t)||a.set(t,i),l.has(t)||l.set(t,i);for(const[t,i]of Object.entries(m))u.set(t,i);for(const[t,i]of Object.entries(v))d.set(t,i);for(const[t,i]of Object.entries(o))f.set(t,i);for(const[t,i]of a.entries())u.has(t)||u.set(t,C(i,s.publicHost||null)),f.has(t)||f.set(t,i),d.has(t)||d.set(t,i);return e.map(t=>resolveRuntimeConfig(t,n,c,r,p,a,l,u,d,f,s.publicHost||null,s.databaseBindings??new Map))}function k(e){return Object.fromEntries(Object.entries(e??{}).map(([n,r])=>[n,(r??"").trim()]).filter(n=>n[1].length>0))}export function resolveRuntimeConfig(e,n,r,s,c,p,a,l,u,d,f,x){const m=resolveServiceStateDir(s,e),v=resolveServicePrepareDir(s,e),o={runtimeId:n,runtimeLabel:r,runtimeDir:s,productDir:e.productDir,serviceName:e.name,serviceCwd:e.testkit.local?.cwd||".",serviceStateDir:m,prepareDir:v,portMap:c,baseUrlByService:p,readyUrlByService:a,publicBaseUrlByService:l,privateBaseUrlByService:u,serviceContextUrlByService:d,publicHost:f,leaseId:null,leaseDir:null,databaseBindingsByService:x},t=w(e.testkit.database,o),i={...e.testkit.runtime,prepare:j(e.testkit.runtime.prepare,o)},S=e.testkit.local?{...e.testkit.local,start:b(e.testkit.local.start,o),cwd:e.testkit.local.cwd!==void 0?b(e.testkit.local.cwd,o):e.testkit.local.cwd,assignedPort:c.get(e.name),baseUrl:p.get(e.name),readyUrl:a.get(e.name),env:{...e.testkit.local.env||{}}}:void 0;return{...e,stateDir:m,runtimeId:n,runtimeLabel:r,testkit:{...e.testkit,database:t,prepareDir:v,templateContext:o,runtime:i,local:S}}}export function resolveServiceStateDir(e,n){return h.join(e,"services",n.name)}export function resolveServicePrepareDir(e,n){return h.join(resolveServiceStateDir(e,n),"prepared")}export function resolveRuntimeUrl(e,n,r,s){return y(e,{...s,runtimeId:r,serviceName:n})}function C(e,n){if(!n)return e;try{const r=new URL(e);r.hostname=n;let s=r.toString();return!e.endsWith("/")&&r.pathname==="/"&&s.endsWith("/")&&(s=s.slice(0,-1)),s}catch{return e}}function j(e,n){return e?{inputs:E(e.inputs,r=>b(r,n)),steps:U(e.steps,r=>b(r,n))}:{inputs:[],steps:[]}}export function buildRuntimeLabel(e,n){return e?`${e}/${n}`:n}
1
+ import{finalizeRuntimeDatabaseConfig as U}from"../database/runtime-config.js";import x from"path";import{finalizeConfiguredInputs as y,finalizeConfiguredSteps as C}from"../steps/configured-steps.js";import"../config/loader/index.js";import{finalizeString as v,resolveTemplateString as h}from"../steps/index.js";export function resolveRuntimeInstanceConfigs(e,t,a,s={}){const c=buildRuntimeLabel(s.graphDirName,t),o=s.ports??new Map,i=new Map,l=new Map,k=b(s.externalEndpoints),p=new Map(Object.entries(b(s.publicEndpoints))),u=new Map(Object.entries(b(s.privateEndpoints))),d=new Map(Object.entries(b(s.serviceContextEndpoints))),m={runtimeId:t,runtimeLabel:c,runtimeDir:a,portMap:o,baseUrlByService:i,readyUrlByService:l};for(const n of e){const r=n.testkit.local;if(!r)continue;const f={...m,serviceName:n.name};r.baseUrl!==void 0&&i.set(n.name,h(r.baseUrl,f)),r.readyUrl!==!1&&r.readyUrl!=null&&l.set(n.name,h(r.readyUrl,f))}for(const[n,r]of Object.entries(k))i.has(n)||i.set(n,r),l.has(n)||l.set(n,r);for(const[n,r]of i.entries())p.has(n)||p.set(n,j(r,s.publicHost||null)),d.has(n)||d.set(n,r),u.has(n)||u.set(n,r);return e.map(n=>resolveRuntimeConfig(n,t,c,a,o,i,l,p,u,d,s.publicHost||null,s.databaseBindings??new Map))}function b(e){return Object.fromEntries(Object.entries(e??{}).map(([t,a])=>[t,(a??"").trim()]).filter(t=>t[1].length>0))}export function resolveRuntimeConfig(e,t,a,s,c,o,i,l,k,p,u,d){const m=resolveServiceStateDir(s,e),n=resolveServicePrepareDir(s,e),r={runtimeId:t,runtimeLabel:a,runtimeDir:s,productDir:e.productDir,serviceName:e.name,serviceCwd:e.testkit.local?.cwd||".",serviceStateDir:m,prepareDir:n,portMap:c,baseUrlByService:o,readyUrlByService:i,publicBaseUrlByService:l,privateBaseUrlByService:k,serviceContextUrlByService:p,publicHost:u,leaseId:null,leaseDir:null,databaseBindingsByService:d},f=U(e.testkit.database,r),S={...e.testkit.runtime,prepare:B(e.testkit.runtime.prepare,r)},w=e.testkit.local?{...e.testkit.local,start:v(e.testkit.local.start,r),cwd:e.testkit.local.cwd!==void 0?v(e.testkit.local.cwd,r):e.testkit.local.cwd,assignedPort:c.get(e.name),baseUrl:o.get(e.name),readyUrl:i.get(e.name),env:{...e.testkit.local.env||{}}}:void 0;return{...e,stateDir:m,runtimeId:t,runtimeLabel:a,testkit:{...e.testkit,database:f,prepareDir:n,templateContext:r,runtime:S,local:w}}}export function resolveServiceStateDir(e,t){return x.join(e,"services",t.name)}export function resolveServicePrepareDir(e,t){return x.join(resolveServiceStateDir(e,t),"prepared")}function j(e,t){if(!t)return e;try{const a=new URL(e);a.hostname=t;let s=a.toString();return!e.endsWith("/")&&a.pathname==="/"&&s.endsWith("/")&&(s=s.slice(0,-1)),s}catch{return e}}function B(e,t){return e?{inputs:y(e.inputs,a=>v(a,t)),steps:C(e.steps,a=>v(a,t))}:{inputs:[],steps:[]}}export function buildRuntimeLabel(e,t){return e?`${e}/${t}`:t}
@@ -1,5 +1,5 @@
1
- import{isPathWithin as V}from"../process/files/paths.js";import{isUnknownArray as q,isUnknownRecord as A}from"../kernel/unknown-values.js";import{readRunDocument as J}from"./read-run.js";import l from"node:fs";import c from"node:path";import"../engines/api/index.js";import"../domain/identifiers.js";import{normalizeArtifactArchiveConfig as W}from"./artifact-config.js";import{applyRunBundleRetention as X,publishRunBundle as G,RefusedArtifactsError as z,sealRunBundle as H}from"./bundles.js";import{RETENTION_DECISION_SCHEMA_VERSION as Q}from"./contracts.js";import{redactedRunValue as v,runRedaction as Y}from"./redaction.js";import{applyTimingUpdates as Z,createEmptyTimings as N,normalizeTimings as tt}from"./timing.js";import{normalizeOptionalNonEmptyString as O,normalizeStringArray as et,readOptionalProperty as F,requireObjectRecord as rt}from"./artifact-validation.js";import{errorMessage as nt,serializeJson as L,writeJsonFileAtomicWithRetry as I,readJsonFileOrNull as b}from"../process/files/json-file.js";import{recordEvidenceWrite as x}from"../process/files/evidence-writes.js";import{recordWork as D}from"../kernel/work-metrics.js";import{copyRegularFileSecurely as it}from"./secure-file-copy.js";import{hasErrorCode as P,serializeError as st}from"../kernel/error-serialization.js";import{redactedPrefix as at}from"../kernel/redaction.js";import{posixPath as k}from"../process/files/paths.js";const _="timings.json",S="artifacts",ot="logs",ct="setup",R="live.json",ut=1e4,C=String;export function writeRunArtifact(t,e,r={},n={}){const i=e,s=c.join(t,".testkit","results");l.mkdirSync(s,{recursive:!0});const o=i.archive,a=r.archiveConfig??W(F(o,"request")||{}),u=r.declaredEnvironmentKeys??et(F(o,"declaredEnvironmentKeys")||[],"run artifact declaredEnvironmentKeys"),f=n.sealRunBundle??H,p=n.publishRunBundle??G,d=n.applyRunBundleRetention??X,y=n.writeLatestProjection??I,m=f(t,i,{env:r.env,archiveConfig:a,declaredEnvironmentKeys:u,recordRefusals:U=>{dt(s,i.run.id,U)},redactKeys:r.redactKeys,redactionTargets:r.redactionTargets}),h=lt(t,m.archive.runId,a.publication,p),w=ft(t,m.archive.runId,a.retention,d),{targets:g}=Y({env:r.env,redactKeys:r.redactKeys,redactionTargets:r.redactionTargets}),E=v(i.finalizationFailure,g),K={...m,...E===void 0?{}:{finalizationFailure:E},run:h.status==="failed"&&h.required===!0||m.archive.cleanup?.status==="failed"||E!==void 0&&m.run.status!=="interrupted"?{...m.run,status:"failed"}:m.run,archive:{...m.archive,publication:v(h,g),retention:v(w,g)}};return mt(s,K,y,g)}function lt(t,e,r,n){try{return n(t,e,r)}catch(i){return{bytesUploaded:0,configured:r!==null,error:j(i),recordedAt:new Date().toISOString(),required:r?.required??!1,runId:e,schemaVersion:1,status:"failed",target:r===null?null:c.resolve(t,r.directory)}}}function ft(t,e,r,n){try{return n(t,e,r)}catch(i){return{currentRunId:e,decidedAt:new Date().toISOString(),error:j(i),garbageCollection:{failures:[],reclaimedBytes:0,removedObjects:0,status:"not_started"},policy:{maxAgeDays:r.maxAgeDays,maxRuns:r.maxRuns,preserveSuites:[...r.preserveSuites]},protectedRunIds:[e],removed:[],removalErrors:[],schemaVersion:Q,status:"failed"}}}function mt(t,e,r,n){const i=c.join(t,"latest.json"),s={path:k(c.relative(c.dirname(t),i)),status:"complete"},o={...e,archive:{...e.archive,latest:s}};try{return x(i),r(i,o),o}catch(a){return{...o,archive:{...o.archive,latest:{...s,error:v(j(a),n),status:"failed"}}}}}function j(t){try{return nt(t)}catch{return"Unknown post-seal archive operation failure"}}export function writeLiveRunArtifact(t,e){const r=e,n=c.join(t,".testkit","results");l.mkdirSync(n,{recursive:!0});const i=c.join(n,R);x(i),I(i,r)}export function finalizeLiveRunArtifact(t,e={}){const r=ht(e),n=c.join(t,".testkit","results",R),i=b(n);if(i===null)return!1;const s=pt(i,r);if(!s)return!1;const o={redactKeys:e.redactKeys,redactionTargets:e.redactionTargets};try{writeRunArtifact(t,s,o)}catch(a){if(!(a instanceof z))throw a;writeRunArtifact(t,M(s,a),o)}return!0}function dt(t,e,r){const n=c.join(t,R),i=b(n);e===null||!A(i)||!A(i.run)||i.run.id!==e||I(n,M(i,new z(r)))}function M(t,e){const r=st(e),n="finalizationFailure"in t?t.finalizationFailure:void 0,i=A(n)&&typeof n.message=="string"?n.message.split(`
1
+ import{isPathWithin as C}from"../process/files/paths.js";import{isUnknownArray as $,isUnknownRecord as g}from"../kernel/unknown-values.js";import{readRunDocument as B}from"./read-run.js";import f from"node:fs";import a from"node:path";import"../engines/api/index.js";import"../domain/identifiers.js";import{normalizeArtifactArchiveConfig as K}from"./artifact-config.js";import{applyRunBundleRetention as U,publishRunBundle as q,RefusedArtifactsError as j,sealRunBundle as V}from"./bundles.js";import{RETENTION_DECISION_SCHEMA_VERSION as J}from"./contracts.js";import{redactedRunValue as A,runRedaction as W}from"./redaction.js";import{applyTimingUpdates as X,createEmptyTimings as k,normalizeTimings as G}from"./timing.js";import{requireObjectRecord as H,requireStringArray as Q}from"./read-fields.js";import{errorMessage as Y,serializeJson as N,writeJsonFileAtomicWithRetry as w,readJsonFileOrNull as z}from"../process/files/json-file.js";import{recordEvidenceWrite as E}from"../process/files/evidence-writes.js";import{recordWork as D}from"../kernel/work-metrics.js";import{copyRegularFileSecurely as Z}from"./secure-file-copy.js";import{hasErrorCode as O,serializeError as tt}from"../kernel/error-serialization.js";import{redactedPrefix as et}from"../kernel/redaction.js";import{posixPath as I}from"../process/files/paths.js";import{sanitizeSegment as S}from"./storage.js";const b="timings.json",v="artifacts",rt="logs",nt="setup",T="live.json",it=1e4,st=String;export function writeRunArtifact(t,e,r={},n={}){const i=e,s=a.join(t,".testkit","results");f.mkdirSync(s,{recursive:!0});const c=i.archive,o=r.archiveConfig??K(c?.request||{}),u=r.declaredEnvironmentKeys??Q(c?.declaredEnvironmentKeys||[],"run artifact declaredEnvironmentKeys"),l=n.publishRunBundle??q,m=V(t,i,{env:r.env,archiveConfig:o,declaredEnvironmentKeys:u,recordRefusals:M=>{ut(s,i.run.id,M)},redactKeys:r.redactKeys,redactionTargets:r.redactionTargets}),d=ot(t,m.archive.runId,o.publication,l),y=at(t,m.archive.runId,o.retention),{targets:p}=W({env:r.env,redactKeys:r.redactKeys,redactionTargets:r.redactionTargets}),h=A(i.finalizationFailure,p),R={...m,...h===void 0?{}:{finalizationFailure:h},run:d.status==="failed"&&d.required===!0||m.archive.cleanup?.status==="failed"||h!==void 0&&m.run.status!=="interrupted"?{...m.run,status:"failed"}:m.run,archive:{...m.archive,publication:A(d,p),retention:A(y,p)}};return ct(s,R,p)}function ot(t,e,r,n){try{return n(t,e,r)}catch(i){return{bytesUploaded:0,configured:r!==null,error:x(i),recordedAt:new Date().toISOString(),required:r?.required??!1,runId:e,schemaVersion:1,status:"failed",target:r===null?null:a.resolve(t,r.directory)}}}function at(t,e,r){try{return U(t,e,r)}catch(n){return{currentRunId:e,decidedAt:new Date().toISOString(),error:x(n),garbageCollection:{failures:[],reclaimedBytes:0,removedObjects:0,status:"not_started"},policy:{maxAgeDays:r.maxAgeDays,maxRuns:r.maxRuns,preserveSuites:[...r.preserveSuites]},protectedRunIds:[e],removed:[],removalErrors:[],schemaVersion:J,status:"failed"}}}function ct(t,e,r){const n=a.join(t,"latest.json"),i={path:I(a.relative(a.dirname(t),n)),status:"complete"},s={...e,archive:{...e.archive,latest:i}};try{return E(n),w(n,s),s}catch(c){return{...s,archive:{...s.archive,latest:{...i,error:A(x(c),r),status:"failed"}}}}}function x(t){try{return Y(t)}catch{return"Unknown post-seal archive operation failure"}}export function writeLiveRunArtifact(t,e){const r=e,n=a.join(t,".testkit","results");f.mkdirSync(n,{recursive:!0});const i=a.join(n,T);E(i),w(i,r)}export function finalizeLiveRunArtifact(t,e={}){const r=mt(e),n=a.join(t,".testkit","results",T),i=z(n);if(i===null)return!1;const s=ft(i,r);if(!s)return!1;const c={redactKeys:e.redactKeys,redactionTargets:e.redactionTargets};try{writeRunArtifact(t,s,c)}catch(o){if(!(o instanceof j))throw o;writeRunArtifact(t,F(s,o),c)}return!0}function ut(t,e,r){const n=a.join(t,T),i=z(n);e===null||!g(i)||!g(i.run)||i.run.id!==e||w(n,F(i,new j(r)))}function F(t,e){const r=tt(e),n="finalizationFailure"in t?t.finalizationFailure:void 0,i=g(n)&&typeof n.message=="string"?n.message.split(`
2
2
  `):[];for(const s of r.message.split(`
3
3
  `))i.includes(s)||i.push(s);return{...t,finalizationFailure:{...r,message:i.join(`
4
- `)}}}function pt(t,e){const r=J(t,"live run artifact"),n=r.run.id||null;if(e.runId&&n&&n!==e.runId)return null;const i=gt(e.finishedAt),s=new Date(i).toISOString(),o=At(r.run.startedAt),a=o===null?r.run.durationMs:Math.max(0,i-o);return{...r,generatedAt:s,run:{...r.run,id:n??e.runId,status:e.status,finishedAt:s,durationMs:a,interruptReason:e.reason},timings:yt(r.timings,s,e.reason)}}function yt(t,e,r){if(t==null)return t;const n=rt(t,"live run artifact.timings"),i=n.operations;if(i==null)return n;if(!q(i))throw new Error("live run artifact.timings.operations must be an array");return{...n,operations:i.map(s=>!A(s)||s.status!=="running"?s:{...s,status:"cancelled",finishedAt:O(s.finishedAt,"live run operation.finishedAt")??e,message:O(s.message,"live run operation.message")??r})}}function ht(t){return{runId:t.runId??null,reason:t.reason??"run interrupted",status:t.status??"interrupted",finishedAt:t.finishedAt??Date.now()}}function gt(t){if(t instanceof Date&&Number.isFinite(t.getTime()))return t.getTime();if(typeof t=="number"&&Number.isFinite(t))return t;if(typeof t=="string"){const e=Date.parse(t);if(Number.isFinite(e))return e}return Date.now()}function At(t){if(typeof t!="string")return null;const e=Date.parse(t);return Number.isFinite(e)?e:null}export function resetResultArtifacts(t){l.rmSync(c.join(t,".testkit","results",S),{recursive:!0,force:!0}),l.rmSync(c.join(t,".testkit","results",ot),{recursive:!0,force:!0}),l.rmSync(c.join(t,".testkit","results",ct),{recursive:!0,force:!0}),l.rmSync(c.join(t,".testkit","results",R),{force:!0})}export function persistTaskArtifacts(t,e,r){if(r.length===0)return[];const n=T(e.serviceName||"service"),i=$(t,n);return r.map((s,o)=>{const a=vt(s),u={schemaVersion:2,source:"testkit-runtime-artifact",service:e.serviceName,suite:{engineId:e.engineId,key:e.suiteKey,name:e.suiteName,profileId:e.profileId,testType:e.testType},file:e.file,taskId:e.id,index:o,name:a.name,kind:a.kind,summary:a.summary,contentType:a.contentType,emittedAt:a.emittedAt,data:a.data},f=`${L(u,"runtime artifact payload")}
5
- `,p=B(i,e.id,o,a.name,".json",y=>{l.writeFileSync(y,f,{flag:"wx"}),D("evidence.filesWritten")}),d=c.join(".testkit","results",S,n,p);return{name:u.name,kind:u.kind,summary:u.summary,contentType:u.contentType,path:k(d)}})}export function persistTaskFileArtifacts(t,e,r,n={}){if(r.length===0)return[];const i=T(e.serviceName||"service"),s=$(t,i);return r.map((o,a)=>{const u=St(o),f=c.resolve(u.path),p=wt(f,u.contentType),d=[],y=B(s,e.id,a,u.name,p,w=>{d.push(it(f,w,{...n.maxBytes===void 0?{}:{maxBytes:n.maxBytes},...n.sourceRoot===void 0?{}:{sourceRoot:n.sourceRoot},...n.sizeLimitDescription===void 0?{}:{sizeLimitDescription:n.sizeLimitDescription}})),D("evidence.filesWritten")}),m=d.at(0);if(m===void 0)throw new Error("File artifact copy completed without a result");n.copyResults?.push(m);const h=c.join(".testkit","results",S,i,y);return{name:u.name,kind:u.kind,summary:u.summary,contentType:u.contentType??Et(f),path:k(h)}})}export function persistTaskOutputArtifacts(t,e,r){if(r.length===0)return[];const n=[];for(const i of r){const s=i.text;s.trim().length!==0&&n.push({name:i.name||"task-output",kind:i.kind||"runtime.output",summary:i.summary||Tt(s),contentType:i.contentType||"text/plain",emittedAt:i.emittedAt||new Date().toISOString(),data:{stream:i.stream||null,text:s}})}return persistTaskArtifacts(t,e,n)}export function loadTimings(t){const e=c.join(t,".testkit",_);if(!l.existsSync(e))return N();try{const r=JSON.parse(l.readFileSync(e,"utf8"));return tt(r)}catch{return N()}}export function saveTimings(t,e,r){if(r.length===0)return;const n=Z(e,r),i=c.join(t,".testkit");l.mkdirSync(i,{recursive:!0});const s=c.join(i,_);x(s),l.writeFileSync(s,L(n,"timings"))}function vt(t){return{name:t.name,kind:t.kind||null,summary:t.summary||null,contentType:t.contentType||"application/json",emittedAt:t.emittedAt||null,data:t.data}}function St(t){return{path:t.path,name:t.name||"artifact",kind:t.kind||null,summary:t.summary||null,contentType:t.contentType||null}}function Rt(t,e,r,n,i=0){const s=i===0?"":`-${(i+1).toString().padStart(2,"0")}`;return`task-${T(t)}-${(e+1).toString().padStart(2,"0")}-${T(r)}${s}${n}`}function B(t,e,r,n,i,s){for(let o=0;o<ut;o+=1){const a=Rt(e,r,n,i,o),u=c.join(t,a);try{return s(u),a}catch(f){if(P(f,"EEXIST"))continue;throw l.rmSync(u,{force:!0}),f}}throw new Error(`Could not allocate a unique artifact path for task ${C(e)} and artifact ${n}`)}function $(t,e){const r=c.resolve(t);if(!l.statSync(r).isDirectory())throw new Error("Artifact product directory must be a directory");const i=l.realpathSync(r);let s=r;for(const o of[".testkit","results",S,e]){s=c.join(s,o);try{l.mkdirSync(s)}catch(f){if(!P(f,"EEXIST"))throw f}const a=l.lstatSync(s);if(a.isSymbolicLink()||!a.isDirectory())throw new Error(`Artifact output directory ${JSON.stringify(s)} must be a real directory below productDir`);const u=l.realpathSync(s);if(!V(i,u))throw new Error(`Artifact output directory ${JSON.stringify(s)} escapes productDir`)}return s}function T(t){return C(t).trim().toLowerCase().replace(/[^a-z0-9._-]+/g,"-").replace(/^-+|-+$/g,"")||"artifact"}function Tt(t){const e=t.split(/\r?\n/u).map(r=>r.trim()).find(Boolean);return e?at(e,120):"captured task output"}function wt(t,e){const r=c.extname(t);if(r)return r;const n=(e??"").split(";",1)[0].trim().toLowerCase();return n==="text/markdown"?".md":n==="text/plain"?".txt":n==="application/json"?".json":n==="application/zip"?".zip":n==="image/png"?".png":n==="image/jpeg"?".jpg":n==="text/html"?".html":n==="video/webm"?".webm":".artifact"}function Et(t){const e=c.extname(t).toLowerCase();return e===".md"||e===".markdown"?"text/markdown":e===".txt"||e===".log"?"text/plain":e===".json"?"application/json":"application/octet-stream"}
4
+ `)}}}function ft(t,e){const r=B(t,"live run artifact"),n=r.run.id||null;if(e.runId&&n&&n!==e.runId)return null;const i=dt(e.finishedAt),s=new Date(i).toISOString(),c=pt(r.run.startedAt),o=c===null?r.run.durationMs:Math.max(0,i-c);return{...r,generatedAt:s,run:{...r.run,id:n??e.runId,status:e.status,finishedAt:s,durationMs:o,interruptReason:e.reason},timings:lt(r.timings,s,e.reason)}}function lt(t,e,r){if(t==null)return t;const n=H(t,"live run artifact.timings"),i=n.operations;if(i==null)return n;if(!$(i))throw new Error("live run artifact.timings.operations must be an array");return{...n,operations:i.map(s=>!g(s)||s.status!=="running"?s:{...s,status:"cancelled",finishedAt:L(s.finishedAt,"live run operation.finishedAt")??e,message:L(s.message,"live run operation.message")??r})}}function L(t,e){if(t==null||t==="")return null;if(typeof t!="string")throw new Error(`${e} must be a string or null`);return t}function mt(t){return{runId:t.runId??null,reason:t.reason??"run interrupted",status:t.status??"interrupted",finishedAt:t.finishedAt??Date.now()}}function dt(t){if(t instanceof Date&&Number.isFinite(t.getTime()))return t.getTime();if(typeof t=="number"&&Number.isFinite(t))return t;if(typeof t=="string"){const e=Date.parse(t);if(Number.isFinite(e))return e}return Date.now()}function pt(t){if(typeof t!="string")return null;const e=Date.parse(t);return Number.isFinite(e)?e:null}export function resetResultArtifacts(t){const e=a.join(t,".testkit","results");for(const r of[v,rt,nt])f.rmSync(a.join(e,r),{recursive:!0,force:!0});f.rmSync(a.join(e,T),{force:!0})}export function persistTaskArtifacts(t,e,r){if(r.length===0)return[];const n=S(e.serviceName||"service","artifact"),i=_(t,n);return r.map((s,c)=>{const o=yt(s),u={schemaVersion:2,source:"testkit-runtime-artifact",service:e.serviceName,suite:{engineId:e.engineId,key:e.suiteKey,name:e.suiteName,profileId:e.profileId,testType:e.testType},file:e.file,taskId:e.id,index:c,name:o.name,kind:o.kind,summary:o.summary,contentType:o.contentType,emittedAt:o.emittedAt,data:o.data},l=`${N(u,"runtime artifact payload")}
5
+ `,m=P(i,e.id,c,o.name,".json",y=>{f.writeFileSync(y,l,{flag:"wx"}),D("evidence.filesWritten")}),d=a.join(".testkit","results",v,n,m);return{name:u.name,kind:u.kind,summary:u.summary,contentType:u.contentType,path:I(d)}})}export function persistTaskFileArtifacts(t,e,r,n={}){if(r.length===0)return[];const i=S(e.serviceName||"service","artifact"),s=_(t,i);return r.map((c,o)=>{const u=ht(c),l=a.resolve(u.path),m=St(l,u.contentType),d=[],y=P(s,e.id,o,u.name,m,R=>{d.push(Z(l,R,{...n.maxBytes===void 0?{}:{maxBytes:n.maxBytes},...n.sourceRoot===void 0?{}:{sourceRoot:n.sourceRoot},...n.sizeLimitDescription===void 0?{}:{sizeLimitDescription:n.sizeLimitDescription}})),D("evidence.filesWritten")}),p=d.at(0);if(p===void 0)throw new Error("File artifact copy completed without a result");n.copyResults?.push(p);const h=a.join(".testkit","results",v,i,y);return{name:u.name,kind:u.kind,summary:u.summary,contentType:u.contentType??vt(l),path:I(h)}})}export function persistTaskOutputArtifacts(t,e,r){if(r.length===0)return[];const n=[];for(const i of r){const s=i.text;s.trim().length!==0&&n.push({name:i.name||"task-output",kind:i.kind||"runtime.output",summary:i.summary||At(s),contentType:i.contentType||"text/plain",emittedAt:i.emittedAt||new Date().toISOString(),data:{stream:i.stream||null,text:s}})}return persistTaskArtifacts(t,e,n)}export function loadTimings(t){const e=a.join(t,".testkit",b);if(!f.existsSync(e))return k();try{const r=JSON.parse(f.readFileSync(e,"utf8"));return G(r)}catch{return k()}}export function saveTimings(t,e,r){if(r.length===0)return;const n=X(e,r),i=a.join(t,".testkit");f.mkdirSync(i,{recursive:!0});const s=a.join(i,b);E(s),f.writeFileSync(s,N(n,"timings"))}function yt(t){return{name:t.name,kind:t.kind||null,summary:t.summary||null,contentType:t.contentType||"application/json",emittedAt:t.emittedAt||null,data:t.data}}function ht(t){return{path:t.path,name:t.name||"artifact",kind:t.kind||null,summary:t.summary||null,contentType:t.contentType||null}}function gt(t,e,r,n,i=0){const s=i===0?"":`-${(i+1).toString().padStart(2,"0")}`;return`task-${S(String(t),"artifact")}-${(e+1).toString().padStart(2,"0")}-${S(r,"artifact")}${s}${n}`}function P(t,e,r,n,i,s){for(let c=0;c<it;c+=1){const o=gt(e,r,n,i,c),u=a.join(t,o);try{return s(u),o}catch(l){if(O(l,"EEXIST"))continue;throw f.rmSync(u,{force:!0}),l}}throw new Error(`Could not allocate a unique artifact path for task ${st(e)} and artifact ${n}`)}function _(t,e){const r=a.resolve(t);if(!f.statSync(r).isDirectory())throw new Error("Artifact product directory must be a directory");const i=f.realpathSync(r);let s=r;for(const c of[".testkit","results",v,e]){s=a.join(s,c);try{f.mkdirSync(s)}catch(l){if(!O(l,"EEXIST"))throw l}const o=f.lstatSync(s);if(o.isSymbolicLink()||!o.isDirectory())throw new Error(`Artifact output directory ${JSON.stringify(s)} must be a real directory below productDir`);const u=f.realpathSync(s);if(!C(i,u))throw new Error(`Artifact output directory ${JSON.stringify(s)} escapes productDir`)}return s}function At(t){const e=t.split(/\r?\n/u).map(r=>r.trim()).find(Boolean);return e?et(e,120):"captured task output"}function St(t,e){const r=a.extname(t);if(r)return r;const n=(e??"").split(";",1)[0].trim().toLowerCase();return n==="text/markdown"?".md":n==="text/plain"?".txt":n==="application/json"?".json":n==="application/zip"?".zip":n==="image/png"?".png":n==="image/jpeg"?".jpg":n==="text/html"?".html":n==="video/webm"?".webm":".artifact"}function vt(t){const e=a.extname(t).toLowerCase();return e===".md"||e===".markdown"?"text/markdown":e===".txt"||e===".log"?"text/plain":e===".json"?"application/json":"application/octet-stream"}
@@ -1,2 +1,2 @@
1
- import{recordWork as G}from"../kernel/work-metrics.js";import{recordEvidenceWrite as ye}from"../process/files/evidence-writes.js";import{isUnknownRecord as B,optionalString as Q}from"../kernel/unknown-values.js";import{posixPath as V}from"../process/files/paths.js";import{readRunProvenance as ge}from"./read-provenance.js";import{parseRunBundleArtifact as ve,parseRunBundleManifest as Z,readRunBundleArtifact as Se}from"./read-bundle.js";import{readRunDocument as Re}from"./read-run.js";import{runModel as ee}from"./run-model.js";import{isUtf8 as we}from"buffer";import be from"crypto";import u from"fs";import f from"path";import{localDirectories as je,localPathRewriter as xe}from"./local-paths.js";import{redactRunText as N,redactRunValue as Be,runRedaction as Ee}from"./redaction.js";import{isCompressedArtifact as te,sealCompressedArtifact as Ie,UnreadableArchiveError as Ae}from"./compressed-artifacts.js";import{BUNDLE_SCHEMA_VERSION as ne,OBJECTS_DIRNAME as re,PROVENANCE_SCHEMA_VERSION as $e,RETENTION_DECISION_SCHEMA_VERSION as ke,RUNS_DIRNAME as Pe}from"./contracts.js";import{bundleRelativePath as O,hashFile as U,normalizeRunId as A,optionalStringArray as M,optionalUnknownArray as Oe,readJsonFile as C,requirePublicationPathOutsideResults as q,requireRunId as Ce,runIncludesFile as Te,resolveManifestEntryPath as se,resolveRunBundleDirectory as T,resolveRunsRoot as L,resultsRoot as W,sanitizeSegment as Fe,sha256 as F,stableJson as J,treeBytes as oe,walkFiles as ie,writeBufferAtomic as ze,writeJson as z}from"./storage.js";import{resolveCiProvenance as _e}from"./ci-provenance.js";import{errorMessage as $}from"../kernel/error-serialization.js";const H=["artifacts","logs","setup"];export class RefusedArtifactsError extends Error{}class k extends Error{}const ae=Buffer.from([255,254]),ce=Buffer.from([254,255]),De=new Set([9,10,11,12,13,27]),Ve=["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={},r={}){const s=n,a=Ce(s),o=L(e,!0),i=f.dirname(o),l=f.join(o,a),c=r.cleanupActiveRunResults??st,d=loadVerifiedRunBundleArtifactIfExists(e,a);if(d!==null)return{...d,archive:{...d.archive,cleanup:c(i)}};const h=f.join(o,`.${a}.${String(process.pid)}.${String(Date.now())}.${be.randomBytes(4).toString("hex")}.tmp`);u.mkdirSync(h,{recursive:!0});try{const R=Ee(t),w=xe(je(e,r.homeDirectory)),p=Ne({recordRefusals:t.recordRefusals,resultsDir:i,tempDir:h,redaction:R,rewriteLocalPaths:w}),g=Me(Ue(s,p.changedPayloads),a,R.targets,w),y=Re(g.artifact,"run artifact");p.redaction.structuredReplacements=g.replacements,p.redaction.replacements+=g.replacements;const E=Le(e,y,a,{...t,redaction:p.redaction}),I={schemaVersion:ne,runId:a,status:"complete",canonicalRunPath:O(a,"run.json"),manifestPath:O(a,"manifest.json"),provenancePath:O(a,"provenance.json"),integrity:"sha256",publication:{configured:!!t.archiveConfig?.publication,status:t.archiveConfig?.publication?"pending":"not_configured"},redaction:p.redaction};y.archive=I,z(f.join(h,"run.json"),y),z(f.join(h,"provenance.json"),E);const b=We(h),j=F(Buffer.from(J(b))),m={schemaVersion:ne,runId:a,createdAt:new Date().toISOString(),algorithm:"sha256",files:b,storage:{fileCount:b.length,logicalBytes:b.reduce((v,S)=>v+S.bytes,0),objectBytesWritten:p.objectBytesWritten,deduplicatedBytes:p.deduplicatedBytes},redaction:p.redaction,digest:j};return z(f.join(h,"manifest.json"),m),u.renameSync(h,l),{...y,archive:{...I,cleanup:c(i),manifestDigest:m.digest,storage:m.storage}}}catch(R){try{u.rmSync(h,{recursive:!0,force:!0})}catch{}try{Ke(e)}catch{}throw R}}export function loadVerifiedRunBundleArtifactIfExists(e,n){const t=A(n),r=T(e,t);if(!u.existsSync(r))return null;const s=verifyRunBundle(e,t);if(!s.ok)throw new Error(`Immutable run bundle ${t} already exists but failed integrity verification: ${s.errors.join("; ")}`);return loadRunBundleArtifact(e,t)}export function loadRunBundleArtifact(e,n){return ue(e,n).artifact}function ue(e,n){const t=A(n),r=Se(e,t),s=loadRunBundleManifest(e,t);return{manifest:s,artifact:{...r,archive:{...r.archive,manifestDigest:s.digest,storage:s.storage}}}}export function loadRunBundleManifest(e,n){const t=A(n),r=T(e,t),s=f.join(r,"manifest.json");if(!u.existsSync(s))throw new Error(`No immutable Testkit run manifest found for ${t}`);return Z(C(s),s,t)}export function listRunBundles(e,n={}){return K(e,n).map(({artifact:t,manifest:r,...s})=>s)}function K(e,n={}){const t=L(e,!1);if(!u.existsSync(t))return[];const r=u.readdirSync(t,{withFileTypes:!0});return G("artifacts.runDirectoriesRead",1),G("artifacts.runEntriesVisited",r.length),r.filter(a=>a.isDirectory()&&!a.name.startsWith(".")).map(a=>{try{const{artifact:o,manifest:i}=ue(e,a.name);return n.file&&!Te(ee(o),n.file)?null:{artifact:o,manifest:i,runId:a.name,status:o.run.status,startedAt:o.run.startedAt,finishedAt:o.run.finishedAt??null,manifestDigest:o.archive.manifestDigest??null,canonicalRunPath:o.archive.canonicalRunPath}}catch{return null}}).filter(a=>a!==null).sort((a,o)=>(o.finishedAt??o.startedAt??"").localeCompare(a.finishedAt??a.startedAt??""))}export function verifyRunBundle(e,n){const t=A(n);try{return D(T(e,t),t)}catch(r){return{ok:!1,runId:t,errors:[$(r)],checkedFiles:0}}}export function publishRunBundle(e,n,t,r={}){const s=A(n),a=r.writePublicationReceipt??Je;if(!t)return a(e,s,{status:"not_configured",required:!1,target:null,bytesUploaded:0});const o=f.resolve(e,t.directory);try{q(e,o)}catch(h){return a(e,s,{status:"failed",required:t.required,target:o,error:$(h),bytesUploaded:0})}const i=Fe(f.basename(e)),l=f.join(o,i),c=f.join(l,s),d=f.join(l,`.${s}.${String(process.pid)}.${String(Date.now())}.tmp`);try{const h=T(e,s);if(u.mkdirSync(l,{recursive:!0}),q(e,l),u.existsSync(c)){const p=u.lstatSync(c);if(p.isSymbolicLink()||!p.isDirectory())throw new Error("Publication target must be a regular bundle directory");q(e,c);const g=D(h,s);if(!g.ok)throw new Error(`Source run bundle failed integrity verification: ${g.errors.join("; ")}`);const y=D(c,s);if(!y.ok)throw new Error(`Publication target failed integrity verification: ${y.errors.join("; ")}`);if(g.manifestDigest!==y.manifestDigest)throw new Error("Publication target already exists with a different manifest digest");return a(e,s,{status:"complete",required:t.required,target:c,manifestDigest:g.manifestDigest,reused:!0,bundleBytes:oe(h),bytesUploaded:0})}const R=oe(h);u.cpSync(h,d,{recursive:!0,preserveTimestamps:!0});const w=D(d,s);if(!w.ok)throw new Error(w.errors.join("; "));return u.renameSync(d,c),a(e,s,{status:"complete",required:t.required,target:c,manifestDigest:w.manifestDigest,reused:!1,bundleBytes:R,bytesUploaded:R})}catch(h){return u.rmSync(d,{recursive:!0,force:!0}),a(e,s,{status:"failed",required:t.required,target:c,error:$(h),bytesUploaded:0})}}export function applyRunBundleRetention(e,n,t={},r={}){const s=A(n),a=r.removeObject??de,o=r.removeRunBundle??Xe,i=t.maxRuns??null,l=t.maxAgeDays??null,c=new Set(t.preserveSuites||[]),d=K(e),h=new Set([s]);for(const m of d)He(ee(m.artifact),c)&&h.add(m.runId);const R=new Set(i?d.slice(0,i).map(m=>m.runId):d.map(m=>m.runId)),w=l==null?null:Date.now()-l*24*60*60*1e3,p=[];for(const m of d){if(h.has(m.runId))continue;const v=w!=null&&Date.parse(m.finishedAt||m.startedAt||"")<w,S=!R.has(m.runId);if(!v&&!S)continue;const x=[];v&&x.push("max_age"),S&&x.push("max_runs"),p.push({path:f.join(W(e),Pe,m.runId),reasons:x,runId:m.runId})}const g=me(e),y=[],E=[];for(const m of p){let v=null;try{o(m.path)}catch(x){v=X(x)}const S=!u.existsSync(m.path);!S&&v===null&&(v="Run bundle still exists after removal completed"),S&&y.push({reasons:m.reasons,runId:m.runId}),v!==null&&E.push({...m,error:v,removed:S})}const I=new Set(y.map(m=>m.runId)),b=fe(e,g,a,d.filter(m=>!I.has(m.runId))),j=new Date().toISOString();return Ge({currentRunId:s,decidedAt:j,garbageCollection:b,policy:{maxRuns:i,maxAgeDays:l,preserveSuites:[...c].sort()},protectedRunIds:[...h].sort(),removed:y,removalErrors:E})}function Ne({recordRefusals:e,resultsDir:n,tempDir:t,redaction:r,rewriteLocalPaths:s}){const a=new Map;let o=0,i=0,l=0,c=0,d=0;const h=[],R=[],w=f.join(n,re,"sha256");u.mkdirSync(w,{recursive:!0});for(const p of H){const g=f.join(n,p);if(u.existsSync(g))for(const y of ie(g)){const E=f.relative(g,y),I=u.readFileSync(y);let b;try{b=Qe(I,y,r.targets,s)}catch(x){if(!(x instanceof k))throw x;R.push(y),h.push(`Refusing to archive ${x.message}, and removed it: ${y}`);continue}l+=b.scanned,b.replacements>0&&(c+=1,d+=b.replacements);const j=b.buffer,m=F(j);j!==I&&a.set(`.testkit/results/${p}/${V(E)}`,{sha256:m,sizeBytes:j.length});const v=f.join(w,m);u.existsSync(v)?i+=j.length:(ze(v,j),u.chmodSync(v,256),o+=j.length);const S=f.join(t,"files",p,E);u.mkdirSync(f.dirname(S),{recursive:!0}),ye(S);try{u.linkSync(v,S)}catch{u.copyFileSync(v,S)}}}if(h.length>0){const p=N(h.join(`
2
- `),r.targets).text;e?.(p);for(const g of R)u.rmSync(g,{force:!0});throw new RefusedArtifactsError(p)}return{changedPayloads:a,objectBytesWritten:o,deduplicatedBytes:i,redaction:{policyVersion:1,status:r.status,textualFilesScanned:l,filesRedacted:c,replacements:d,sensitiveEnvironmentKeys:r.keys,valuesStored:!1}}}function Ue(e,n){const t=r=>P(r,"suites",s=>P(s,"files",a=>P(a,"artifacts",o=>{const i=typeof o.path=="string"?n.get(o.path):void 0;if(i===void 0||!B(o.metadata))return o;const l={...o.metadata};return Object.hasOwn(l,"sha256")&&(l.sha256=i.sha256),Object.hasOwn(l,"sizeBytes")&&(l.sizeBytes=i.sizeBytes),{...o,metadata:l}})));return P(P(e,"groups",t),"services",t)}function P(e,n,t){const r=B(e)?e[n]:void 0;return!B(e)||!Array.isArray(r)?e:{...e,[n]:r.map(s=>B(s)?t(s):s)}}function le(e,n,t){const r=t(e.text);if(r===e.text)return e;const s=N(r,n);return{replacements:e.replacements+s.replacements,text:s.text}}function Me(e,n,t,r){const s=new Map(H.map(c=>[`.testkit/results/${c}/`,`${O(n,`files/${c}`)}/`])),a=_(e,c=>{for(const[d,h]of s)if(c.startsWith(d))return`${h}${c.slice(d.length)}`;return c}),o=Be(qe(a),t);let i=o.replacements;return{artifact:_(o.value,c=>{const d=le({replacements:0,text:c},t,r);return i+=d.replacements,d.text}),replacements:i}}function qe(e){if(!B(e))return e;const n={...e};if(B(n.product)){const t={...n.product};delete t.directory,n.product=t}if(B(n.git)){const t={...n.git};delete t.remoteUrl,delete t.repoRoot,delete t.worktreePath,n.git=t}return delete n.host,n}function _(e,n){return Array.isArray(e)?e.map(t=>_(t,n)):B(e)?Object.fromEntries(Object.entries(e).map(([t,r])=>[t,_(r,n)])):typeof e=="string"?n(e):e}function Le(e,n,t,r){const s=M(n.run.fileNames),a=[...new Set([...Ve,...s])].map(o=>rt(e,o)).filter(o=>o!==null);return{schemaVersion:$e,runId:t,ci:_e(r.env??process.env),source:{git:n.git??null,files:a},runtime:{controller:{testkitVersion:Q(n.run.testkitVersion),node:process.versions.node,platform:process.platform,architecture:process.arch},runtimeStats:n.schemaVersion===19?Oe(n.run.runtimeStats):[...n.environment?.capacity?.runtimeStats??[]]},scenario:{types:M(n.run.types),suiteSelectors:M(n.run.suiteSelectors),fileNames:s,serviceFilter:Q(n.schemaVersion===19?n.run.serviceFilter:n.run.groupFilter),planningDigest:F(Buffer.from(J(n.planning??null))),declaredEnvironmentKeys:[...new Set(r.declaredEnvironmentKeys??[])].sort()},redaction:r.redaction}}function We(e){return ie(e).map(n=>({path:V(f.relative(e,n)),bytes:u.statSync(n).size,sha256:U(n)})).sort((n,t)=>n.path.localeCompare(t.path))}function D(e,n){const t=f.join(e,"manifest.json"),r=Z(C(t),t,n);ve(C(f.join(e,"run.json")),f.join(e,"run.json"),n);const s=[];for(const o of r.files){let i;try{i=se(e,o.path)}catch(c){s.push($(c));continue}if(!u.existsSync(i)){s.push(`missing ${o.path}`);continue}const l=u.lstatSync(i);if(l.isSymbolicLink()){s.push(`symbolic link not allowed ${o.path}`);continue}if(!l.isFile()){s.push(`not a regular file ${o.path}`);continue}l.size!==o.bytes&&s.push(`size mismatch ${o.path}`),U(i)!==o.sha256&&s.push(`checksum mismatch ${o.path}`)}F(Buffer.from(J(r.files)))!==r.digest&&s.push("manifest digest mismatch"),r.storage.fileCount!==r.files.length&&s.push("manifest file count mismatch");for(const o of["run.json","provenance.json"])r.files.some(i=>i.path===o)||s.push(`manifest must include ${o}`);if(s.length===0){const o=se(e,"provenance.json");ge(C(o),o,n)}return{runId:n,checkedFiles:r.files.length,storage:r.storage,ok:s.length===0,errors:s,manifestDigest:r.digest}}function Je(e,n,t){const r={schemaVersion:1,runId:n,recordedAt:new Date().toISOString(),...t};return z(f.join(W(e),"publications",`${n}.json`),r),r}function He(e,n){return n.size===0?!1:e.groups.some(t=>t.suites.some(r=>{const s=r.testType??"";return[r.name,`${s}:${r.name}`,`${t.name}/${s}:${r.name}`].some(o=>n.has(o))}))}function Ke(e){return fe(e,me(e),de,K(e))}function fe(e,n,t,r){const s=L(e,!1),a=new Map(r.map(c=>[c.runId,c.manifest])),o=new Set,i=[],l=u.existsSync(s)?u.readdirSync(s,{withFileTypes:!0}):[];for(const c of l)if(!(!c.isDirectory()||c.name.startsWith(".")))try{for(const d of(a.get(c.name)??loadRunBundleManifest(e,c.name)).files)d.path.startsWith("files/")&&o.add(d.sha256)}catch(d){i.push({error:`No stored object was removed: ${$(d)}`,path:f.join(s,c.name)})}return i.length>0?{failures:i,reclaimedBytes:0,removedObjects:0,status:"not_started"}:Ye(n,o,t)}function me(e){const n=f.join(W(e),re,"sha256");return u.existsSync(n)?u.readdirSync(n,{withFileTypes:!0}).filter(t=>t.isFile()).map(t=>{const r=f.join(n,t.name);return{bytes:u.statSync(r).size,digest:t.name,path:r}}):[]}function Ye(e,n,t){const r=[];let s=0,a=0;for(const o of e){if(n.has(o.digest))continue;let i=null;try{t(o.path)}catch(c){i=X(c)}!u.existsSync(o.path)?(s+=1,a+=o.bytes):i===null&&(i="Object still exists after removal completed"),i!==null&&r.push({error:i,path:o.path})}return{failures:r,reclaimedBytes:a,removedObjects:s,status:r.length===0?"complete":"failed"}}function Xe(e){u.rmSync(e,{recursive:!0,force:!0})}function de(e){u.rmSync(e,{force:!0})}function Ge({currentRunId:e,decidedAt:n,garbageCollection:t,policy:r,protectedRunIds:s,removed:a,removalErrors:o}){const i=[...o.map(l=>`remove ${l.runId}: ${l.error}`),...t.failures.map(l=>`garbage collect ${l.path}: ${l.error}`)];return{currentRunId:e,decidedAt:n,garbageCollection:t,policy:r,protectedRunIds:s,removed:a,removalErrors:o,schemaVersion:ke,status:i.length===0?"complete":"failed",...i.length===0?{}:{error:i.join("; ")}}}function Qe(e,n,t,r){const s=a=>le(N(a,t),t,r);if(pe(e,n))return{...he(e,"utf8",s),scanned:1};if(te(e,n))return Ze(e,n,t,s);if(Y(e,t))throw new k("binary file containing sensitive environment value");return{buffer:e,replacements:0,scanned:0}}function Ze(e,n,t,r){let s=0,a=0,o;try{o=Ie(e,n,i=>{if(te(i.content,i.name))throw new k(`compressed file whose entry ${i.name} is itself an archive`);const l=et(i.content,i.name);if(l===null){if(!Y(i.content,t))return i.content;throw new k(`compressed file whose binary entry ${i.name} contains sensitive environment value`)}const c=he(i.content,l,r);return a+=1,s+=c.replacements,c.buffer})}catch(i){throw i instanceof Ae?new k(`compressed file that Testkit can't read, because ${i.message}`):i}if(Y(o,t))throw new k("compressed file containing sensitive environment value outside its entries' text");return{buffer:o,replacements:s,scanned:a}}function et(e,n){const t=e.subarray(0,2);return e.length%2===0&&t.equals(ae)?"utf16le":e.length%2===0&&t.equals(ce)?"utf16be":!pe(e,n)||!we(e)||e.some(r=>r===127||r<32&&!De.has(r))?null:"utf8"}function he(e,n,t){const r=tt(e,n),s=t(r);return{buffer:s.text===r?e:nt(s.text,n),replacements:s.replacements}}function tt(e,n){if(n==="utf8")return e.toString("utf8");const t=Buffer.from(e.subarray(2));return(n==="utf16be"?t.swap16():t).toString("utf16le")}function nt(e,n){if(n==="utf8")return Buffer.from(e);const t=Buffer.from(e,"utf16le");return n==="utf16be"?Buffer.concat([ce,t.swap16()]):Buffer.concat([ae,t])}function Y(e,n){return n.some(t=>{const r=Buffer.from(t,"utf16le");return[Buffer.from(t),r,Buffer.from(r).swap16()].some(s=>e.includes(s))})}function pe(e,n){if(e.includes(0))return!1;const t=f.extname(n).toLowerCase();return![".zip",".gz",".png",".jpg",".jpeg",".webm",".pdf"].includes(t)}function rt(e,n){const t=f.resolve(e,n);return!t.startsWith(`${f.resolve(e)}${f.sep}`)||!u.existsSync(t)||!u.statSync(t).isFile()?null:{path:V(f.relative(e,t)),bytes:u.statSync(t).size,sha256:U(t)}}function st(e){const n=[];for(const t of[...H.map(r=>f.join(e,r)),f.join(e,"live.json")])try{u.rmSync(t,{recursive:!0,force:!0})}catch(r){n.push(`${t}: ${X(r)}`)}return n.length===0?{status:"complete"}:{error:n.join("; "),status:"failed"}}function X(e){try{return $(e)}catch{return"Unknown post-seal archive operation failure"}}
1
+ import{recordWork as ee}from"../kernel/work-metrics.js";import{recordEvidenceWrite as Se}from"../process/files/evidence-writes.js";import{isUnknownRecord as I,optionalString as te}from"../kernel/unknown-values.js";import{posixPath as U}from"../process/files/paths.js";import{readRunProvenance as we}from"./read-provenance.js";import{parseRunBundleArtifact as ne,parseRunBundleManifest as q,readRunBundleArtifact as Re}from"./read-bundle.js";import{readRunDocument as je}from"./read-run.js";import{runModel as L}from"./run-model.js";import{isUtf8 as be}from"buffer";import xe from"crypto";import u from"fs";import f from"path";import{localDirectories as Be,localPathRewriter as Ee}from"./local-paths.js";import{redactRunText as W,redactRunValue as Ie,runRedaction as Ae}from"./redaction.js";import{isCompressedArtifact as re,sealCompressedArtifact as $e,UnreadableArchiveError as ke}from"./compressed-artifacts.js";import{BUNDLE_SCHEMA_VERSION as se,OBJECTS_DIRNAME as oe,PROVENANCE_SCHEMA_VERSION as Pe,RETENTION_DECISION_SCHEMA_VERSION as Oe,RUNS_DIRNAME as Ce}from"./contracts.js";import{bundleRelativePath as z,hashFile as J,normalizeRunId as A,optionalStringArray as H,optionalUnknownArray as Te,readJsonFile as C,requirePublicationPathOutsideResults as K,requireRunId as Fe,runIncludesFile as ze,resolveManifestEntryPath as ie,resolveRunBundleDirectory as T,resolveRunsRoot as _,resultsRoot as Y,sanitizeSegment as _e,sha256 as k,stableJson as D,treeBytes as ae,walkFiles as ce,writeBufferAtomic as De,writeJson as V}from"./storage.js";import{resolveCiProvenance as Ve}from"./ci-provenance.js";import{errorMessage as $}from"../kernel/error-serialization.js";const X=["artifacts","logs","setup"];export class RefusedArtifactsError extends Error{}class P extends Error{}const ue=Buffer.from([255,254]),le=Buffer.from([254,255]),Me=new Set([9,10,11,12,13,27]),Ne=["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={},r={}){const s=n,a=Fe(s),o=_(e,!0),i=f.dirname(o),m=f.join(o,a),c=loadVerifiedRunBundleArtifactIfExists(e,a);if(c!==null)return{...c,archive:{...c.archive,cleanup:ve(i)}};const d=f.join(o,`.${a}.${String(process.pid)}.${String(Date.now())}.${xe.randomBytes(4).toString("hex")}.tmp`);u.mkdirSync(d,{recursive:!0});try{const h=Ae(t),R=Ee(Be(e,r.homeDirectory)),p=qe({recordRefusals:t.recordRefusals,resultsDir:i,tempDir:d,redaction:h,rewriteLocalPaths:R}),g=We(Le(s,p.changedPayloads),a,h.targets,R),y=je(g.artifact,"run artifact");p.redaction.structuredReplacements=g.replacements,p.redaction.replacements+=g.replacements;const v=He(e,y,a,{...t,redaction:p.redaction}),x={schemaVersion:se,runId:a,status:"complete",canonicalRunPath:z(a,"run.json"),manifestPath:z(a,"manifest.json"),provenancePath:z(a,"provenance.json"),integrity:"sha256",publication:{configured:!!t.archiveConfig?.publication,status:t.archiveConfig?.publication?"pending":"not_configured"},redaction:p.redaction};y.archive=x,V(f.join(d,"run.json"),y),V(f.join(d,"provenance.json"),v);const B=Ke(d),j=k(Buffer.from(D(B))),w={schemaVersion:se,runId:a,createdAt:new Date().toISOString(),algorithm:"sha256",files:B,storage:{fileCount:B.length,logicalBytes:B.reduce((E,l)=>E+l.bytes,0),objectBytesWritten:p.objectBytesWritten,deduplicatedBytes:p.deduplicatedBytes},redaction:p.redaction,digest:j};return V(f.join(d,"manifest.json"),w),u.renameSync(d,m),{...y,archive:{...x,cleanup:ve(i),manifestDigest:w.digest,storage:w.storage}}}catch(h){try{u.rmSync(d,{recursive:!0,force:!0})}catch{}try{Ge(e)}catch{}throw h}}export function loadVerifiedRunBundleArtifactIfExists(e,n){const t=A(n),r=T(e,t);if(!u.existsSync(r))return null;const s=verifyRunBundle(e,t);if(!s.ok)throw new Error(`Immutable run bundle ${t} already exists but failed integrity verification: ${s.errors.join("; ")}`);return loadRunBundleArtifact(e,t)}export function loadRunBundleArtifact(e,n){return fe(e,n).artifact}function fe(e,n){const t=A(n),r=Re(e,t),s=loadRunBundleManifest(e,t);return{manifest:s,artifact:{...r,archive:{...r.archive,manifestDigest:s.digest,storage:s.storage}}}}export function loadRunBundleManifest(e,n){const t=A(n),r=T(e,t),s=f.join(r,"manifest.json");if(!u.existsSync(s))throw new Error(`No immutable Testkit run manifest found for ${t}`);return q(C(s),s,t)}export function listRunBundles(e,n={}){return G(e,n).map(({artifact:t,manifest:r,...s})=>s)}export function loadRunBundleModels(e){const n=_(e,!1);if(!u.existsSync(n))return{models:[],rejected:[]};const t=[],r=[];for(const s of u.readdirSync(n,{withFileTypes:!0}))if(!(!s.isDirectory()||s.name.startsWith(".")))try{t.push({run:L(Ue(e,s.name)),runId:s.name})}catch(a){r.push({reason:$(a),runId:s.name})}return{models:t,rejected:r}}function Ue(e,n){const t=T(e,A(n)),r=f.join(t,"manifest.json"),s=q(C(r),r,n);if(k(Buffer.from(D(s.files)))!==s.digest)throw new Error("its manifest doesn't match the manifest digest");const a=f.join(t,"run.json"),o=s.files.find(c=>c.path==="run.json"),i=u.readFileSync(a);if(o===void 0||o.bytes!==i.byteLength||o.sha256!==k(i))throw new Error("its run.json doesn't match the manifest");const m=JSON.parse(i.toString("utf8"));return ne(m,a,n)}function G(e,n={}){const t=_(e,!1);if(!u.existsSync(t))return[];const r=u.readdirSync(t,{withFileTypes:!0});return ee("artifacts.runDirectoriesRead",1),ee("artifacts.runEntriesVisited",r.length),r.filter(a=>a.isDirectory()&&!a.name.startsWith(".")).map(a=>{try{const{artifact:o,manifest:i}=fe(e,a.name);return n.file&&!ze(L(o),n.file)?null:{artifact:o,manifest:i,runId:a.name,status:o.run.status,startedAt:o.run.startedAt,finishedAt:o.run.finishedAt??null,manifestDigest:o.archive.manifestDigest??null,canonicalRunPath:o.archive.canonicalRunPath}}catch{return null}}).filter(a=>a!==null).sort((a,o)=>(o.finishedAt??o.startedAt??"").localeCompare(a.finishedAt??a.startedAt??""))}export function verifyRunBundle(e,n){const t=A(n);try{return N(T(e,t),t)}catch(r){return{ok:!1,runId:t,errors:[$(r)],checkedFiles:0}}}export function publishRunBundle(e,n,t,r={}){const s=A(n),a=r.writePublicationReceipt??Ye;if(!t)return a(e,s,{status:"not_configured",required:!1,target:null,bytesUploaded:0});const o=f.resolve(e,t.directory);try{K(e,o)}catch(h){return a(e,s,{status:"failed",required:t.required,target:o,error:$(h),bytesUploaded:0})}const i=_e(f.basename(e),"product"),m=f.join(o,i),c=f.join(m,s),d=f.join(m,`.${s}.${String(process.pid)}.${String(Date.now())}.tmp`);try{const h=T(e,s);if(u.mkdirSync(m,{recursive:!0}),K(e,m),u.existsSync(c)){const g=u.lstatSync(c);if(g.isSymbolicLink()||!g.isDirectory())throw new Error("Publication target must be a regular bundle directory");K(e,c);const y=N(h,s);if(!y.ok)throw new Error(`Source run bundle failed integrity verification: ${y.errors.join("; ")}`);const v=N(c,s);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 a(e,s,{status:"complete",required:t.required,target:c,manifestDigest:y.manifestDigest,reused:!0,bundleBytes:ae(h),bytesUploaded:0})}const R=ae(h);u.cpSync(h,d,{recursive:!0,preserveTimestamps:!0});const p=N(d,s);if(!p.ok)throw new Error(p.errors.join("; "));return u.renameSync(d,c),a(e,s,{status:"complete",required:t.required,target:c,manifestDigest:p.manifestDigest,reused:!1,bundleBytes:R,bytesUploaded:R})}catch(h){return u.rmSync(d,{recursive:!0,force:!0}),a(e,s,{status:"failed",required:t.required,target:c,error:$(h),bytesUploaded:0})}}export function applyRunBundleRetention(e,n,t={},r={}){const s=A(n),a=r.removeObject??pe,o=r.removeRunBundle??Ze,i=t.maxRuns??null,m=t.maxAgeDays??null,c=new Set(t.preserveSuites||[]),d=G(e),h=new Set([s]);for(const l of d)Xe(L(l.artifact),c)&&h.add(l.runId);const R=new Set(i?d.slice(0,i).map(l=>l.runId):d.map(l=>l.runId)),p=m==null?null:Date.now()-m*24*60*60*1e3,g=[];for(const l of d){if(h.has(l.runId))continue;const S=p!=null&&Date.parse(l.finishedAt||l.startedAt||"")<p,b=!R.has(l.runId);if(!S&&!b)continue;const O=[];S&&O.push("max_age"),b&&O.push("max_runs"),g.push({path:f.join(Y(e),Ce,l.runId),reasons:O,runId:l.runId})}const y=he(e),v=[],x=[];for(const l of g){let S=null;try{o(l.path)}catch(O){S=Z(O)}const b=!u.existsSync(l.path);!b&&S===null&&(S="Run bundle still exists after removal completed"),b&&v.push({reasons:l.reasons,runId:l.runId}),S!==null&&x.push({...l,error:S,removed:b})}const B=new Set(v.map(l=>l.runId)),j=me(e,y,a,d.filter(l=>!B.has(l.runId))),w=new Date().toISOString(),E=[...x.map(l=>`remove ${l.runId}: ${l.error}`),...j.failures.map(l=>`garbage collect ${l.path}: ${l.error}`)];return{currentRunId:s,decidedAt:w,garbageCollection:j,policy:{maxRuns:i,maxAgeDays:m,preserveSuites:[...c].sort()},protectedRunIds:[...h].sort(),removed:v,removalErrors:x,schemaVersion:Oe,status:E.length===0?"complete":"failed",...E.length===0?{}:{error:E.join("; ")}}}function qe({recordRefusals:e,resultsDir:n,tempDir:t,redaction:r,rewriteLocalPaths:s}){const a=new Map;let o=0,i=0,m=0,c=0,d=0;const h=[],R=[],p=f.join(n,oe,"sha256");u.mkdirSync(p,{recursive:!0});for(const g of X){const y=f.join(n,g);if(u.existsSync(y))for(const v of ce(y)){const x=f.relative(y,v),B=u.readFileSync(v);let j;try{j=et(B,v,r.targets,s)}catch(b){if(!(b instanceof P))throw b;R.push(v),h.push(`Refusing to archive ${b.message}, and removed it: ${v}`);continue}m+=j.scanned,j.replacements>0&&(c+=1,d+=j.replacements);const w=j.buffer,E=k(w);w!==B&&a.set(`.testkit/results/${g}/${U(x)}`,{sha256:E,sizeBytes:w.length});const l=f.join(p,E);u.existsSync(l)?i+=w.length:(De(l,w),u.chmodSync(l,256),o+=w.length);const S=f.join(t,"files",g,x);u.mkdirSync(f.dirname(S),{recursive:!0}),Se(S);try{u.linkSync(l,S)}catch{u.copyFileSync(l,S)}}}if(h.length>0){const g=W(h.join(`
2
+ `),r.targets).text;e?.(g);for(const y of R)u.rmSync(y,{force:!0});throw new RefusedArtifactsError(g)}return{changedPayloads:a,objectBytesWritten:o,deduplicatedBytes:i,redaction:{policyVersion:1,status:r.status,textualFilesScanned:m,filesRedacted:c,replacements:d,sensitiveEnvironmentKeys:r.keys,valuesStored:!1}}}function Le(e,n){const t=r=>F(r,"suites",s=>F(s,"files",a=>F(a,"artifacts",o=>{const i=typeof o.path=="string"?n.get(o.path):void 0;if(i===void 0||!I(o.metadata))return o;const m={...o.metadata};return Object.hasOwn(m,"sha256")&&(m.sha256=i.sha256),Object.hasOwn(m,"sizeBytes")&&(m.sizeBytes=i.sizeBytes),{...o,metadata:m}})));return F(F(e,"groups",t),"services",t)}function F(e,n,t){const r=I(e)?e[n]:void 0;return!I(e)||!Array.isArray(r)?e:{...e,[n]:r.map(s=>I(s)?t(s):s)}}function de(e,n,t){const r=t(e.text);if(r===e.text)return e;const s=W(r,n);return{replacements:e.replacements+s.replacements,text:s.text}}function We(e,n,t,r){const s=new Map(X.map(c=>[`.testkit/results/${c}/`,`${z(n,`files/${c}`)}/`])),a=M(e,c=>{for(const[d,h]of s)if(c.startsWith(d))return`${h}${c.slice(d.length)}`;return c}),o=Ie(Je(a),t);let i=o.replacements;return{artifact:M(o.value,c=>{const d=de({replacements:0,text:c},t,r);return i+=d.replacements,d.text}),replacements:i}}function Je(e){if(!I(e))return e;const n={...e};if(I(n.product)){const t={...n.product};delete t.directory,n.product=t}if(I(n.git)){const t={...n.git};delete t.remoteUrl,delete t.repoRoot,delete t.worktreePath,n.git=t}return delete n.host,n}function M(e,n){return Array.isArray(e)?e.map(t=>M(t,n)):I(e)?Object.fromEntries(Object.entries(e).map(([t,r])=>[t,M(r,n)])):typeof e=="string"?n(e):e}function He(e,n,t,r){const s=H(n.run.fileNames),a=[...new Set([...Ne,...s])].map(o=>ot(e,o)).filter(o=>o!==null);return{schemaVersion:Pe,runId:t,ci:Ve(r.env??process.env),source:{git:n.git??null,files:a},runtime:{controller:{testkitVersion:te(n.run.testkitVersion),node:process.versions.node,platform:process.platform,architecture:process.arch},runtimeStats:n.schemaVersion===19?Te(n.run.runtimeStats):[...n.environment?.capacity?.runtimeStats??[]]},scenario:{types:H(n.run.types),suiteSelectors:H(n.run.suiteSelectors),fileNames:s,serviceFilter:te(n.schemaVersion===19?n.run.serviceFilter:n.run.groupFilter),planningDigest:k(Buffer.from(D(n.planning??null))),declaredEnvironmentKeys:[...new Set(r.declaredEnvironmentKeys??[])].sort()},redaction:r.redaction}}function Ke(e){return ce(e).map(n=>({path:U(f.relative(e,n)),bytes:u.statSync(n).size,sha256:J(n)})).sort((n,t)=>n.path.localeCompare(t.path))}function N(e,n){const t=f.join(e,"manifest.json"),r=q(C(t),t,n);ne(C(f.join(e,"run.json")),f.join(e,"run.json"),n);const s=[];for(const o of r.files){let i;try{i=ie(e,o.path)}catch(c){s.push($(c));continue}if(!u.existsSync(i)){s.push(`missing ${o.path}`);continue}const m=u.lstatSync(i);if(m.isSymbolicLink()){s.push(`symbolic link not allowed ${o.path}`);continue}if(!m.isFile()){s.push(`not a regular file ${o.path}`);continue}m.size!==o.bytes&&s.push(`size mismatch ${o.path}`),J(i)!==o.sha256&&s.push(`checksum mismatch ${o.path}`)}k(Buffer.from(D(r.files)))!==r.digest&&s.push("manifest digest mismatch"),r.storage.fileCount!==r.files.length&&s.push("manifest file count mismatch");for(const o of["run.json","provenance.json"])r.files.some(i=>i.path===o)||s.push(`manifest must include ${o}`);if(s.length===0){const o=ie(e,"provenance.json");we(C(o),o,n)}return{runId:n,checkedFiles:r.files.length,storage:r.storage,ok:s.length===0,errors:s,manifestDigest:r.digest}}function Ye(e,n,t){const r={schemaVersion:1,runId:n,recordedAt:new Date().toISOString(),...t};return V(f.join(Y(e),"publications",`${n}.json`),r),r}function Xe(e,n){return n.size===0?!1:e.groups.some(t=>t.suites.some(r=>{const s=r.testType??"";return[r.name,`${s}:${r.name}`,`${t.name}/${s}:${r.name}`].some(o=>n.has(o))}))}function Ge(e){return me(e,he(e),pe,G(e))}function me(e,n,t,r){const s=_(e,!1),a=new Map(r.map(c=>[c.runId,c.manifest])),o=new Set,i=[],m=u.existsSync(s)?u.readdirSync(s,{withFileTypes:!0}):[];for(const c of m)if(!(!c.isDirectory()||c.name.startsWith(".")))try{for(const d of(a.get(c.name)??loadRunBundleManifest(e,c.name)).files)d.path.startsWith("files/")&&o.add(d.sha256)}catch(d){i.push({error:`No stored object was removed: ${$(d)}`,path:f.join(s,c.name)})}return i.length>0?{failures:i,reclaimedBytes:0,removedObjects:0,status:"not_started"}:Qe(n,o,t)}function he(e){const n=f.join(Y(e),oe,"sha256");return u.existsSync(n)?u.readdirSync(n,{withFileTypes:!0}).filter(t=>t.isFile()).map(t=>{const r=f.join(n,t.name);return{bytes:u.statSync(r).size,digest:t.name,path:r}}):[]}function Qe(e,n,t){const r=[];let s=0,a=0;for(const o of e){if(n.has(o.digest))continue;let i=null;try{t(o.path)}catch(c){i=Z(c)}!u.existsSync(o.path)?(s+=1,a+=o.bytes):i===null&&(i="Object still exists after removal completed"),i!==null&&r.push({error:i,path:o.path})}return{failures:r,reclaimedBytes:a,removedObjects:s,status:r.length===0?"complete":"failed"}}function Ze(e){u.rmSync(e,{recursive:!0,force:!0})}function pe(e){u.rmSync(e,{force:!0})}function et(e,n,t,r){const s=a=>de(W(a,t),t,r);if(ge(e,n))return{...ye(e,"utf8",s),scanned:1};if(re(e,n))return tt(e,n,t,s);if(Q(e,t))throw new P("binary file containing sensitive environment value");return{buffer:e,replacements:0,scanned:0}}function tt(e,n,t,r){let s=0,a=0,o;try{o=$e(e,n,i=>{if(re(i.content,i.name))throw new P(`compressed file whose entry ${i.name} is itself an archive`);const m=nt(i.content,i.name);if(m===null){if(!Q(i.content,t))return i.content;throw new P(`compressed file whose binary entry ${i.name} contains sensitive environment value`)}const c=ye(i.content,m,r);return a+=1,s+=c.replacements,c.buffer})}catch(i){throw i instanceof ke?new P(`compressed file that Testkit can't read, because ${i.message}`):i}if(Q(o,t))throw new P("compressed file containing sensitive environment value outside its entries' text");return{buffer:o,replacements:s,scanned:a}}function nt(e,n){const t=e.subarray(0,2);return e.length%2===0&&t.equals(ue)?"utf16le":e.length%2===0&&t.equals(le)?"utf16be":!ge(e,n)||!be(e)||e.some(r=>r===127||r<32&&!Me.has(r))?null:"utf8"}function ye(e,n,t){const r=rt(e,n),s=t(r);return{buffer:s.text===r?e:st(s.text,n),replacements:s.replacements}}function rt(e,n){if(n==="utf8")return e.toString("utf8");const t=Buffer.from(e.subarray(2));return(n==="utf16be"?t.swap16():t).toString("utf16le")}function st(e,n){if(n==="utf8")return Buffer.from(e);const t=Buffer.from(e,"utf16le");return n==="utf16be"?Buffer.concat([le,t.swap16()]):Buffer.concat([ue,t])}function Q(e,n){return n.some(t=>{const r=Buffer.from(t,"utf16le");return[Buffer.from(t),r,Buffer.from(r).swap16()].some(s=>e.includes(s))})}function ge(e,n){if(e.includes(0))return!1;const t=f.extname(n).toLowerCase();return![".zip",".gz",".png",".jpg",".jpeg",".webm",".pdf"].includes(t)}function ot(e,n){const t=f.resolve(e,n);return!t.startsWith(`${f.resolve(e)}${f.sep}`)||!u.existsSync(t)||!u.statSync(t).isFile()?null:{path:U(f.relative(e,t)),bytes:u.statSync(t).size,sha256:J(t)}}function ve(e){const n=[];for(const t of[...X.map(r=>f.join(e,r)),f.join(e,"live.json")])try{u.rmSync(t,{recursive:!0,force:!0})}catch(r){n.push(`${t}: ${Z(r)}`)}return n.length===0?{status:"complete"}:{error:n.join("; "),status:"failed"}}function Z(e){try{return $(e)}catch{return"Unknown post-seal archive operation failure"}}
@@ -1 +1 @@
1
- import{formatError as v}from"../kernel/error-serialization.js";import{jsonRedactionTargets as E,orderRedactionTargets as B,sensitiveKeyMatcher as I}from"../kernel/redaction-targets.js";import{redactUrlSecrets as C}from"../process/url-redaction.js";import{redactedRunValue as K}from"./redaction.js";export function publishEnvironmentRecord(a){return{...a,acquisitions:a.acquisitions.map(d=>({...d,settledAt:O(d.settledAt),setups:d.setups.map(p=>({...p,settledAt:O(p.settledAt),startedAt:new Date(p.startedAt).toISOString()})),startedAt:new Date(d.startedAt).toISOString()})),dbBackend:a.groups.some(d=>d.dbBackend==="local")?"local":null}}function O(a){return a===null?null:new Date(a).toISOString()}export function createRunEnvironmentRecord(a={}){const d=[],p=[],h=[],R=new Set,b=I(a.redactKeys);let y=[],m={instanceCount:0,resourceBudgets:[],runtimeStats:[]},k=null;const g=a.now??Date.now,f=()=>{a.onPublishedChange?.()},w=(s,e)=>({releaseFailed(r){for(const n of s)n.releaseErrors.push(v(r)),n.failingReleases.add(e),n.releaseReported=!0},released(){for(const r of s)r.failingReleases.delete(e),r.releaseReported=!0}});return{redactKeys:a.redactKeys??[],service({runtimeLabel:s,serviceName:e}){let r=null;return{recordAttempt({baseUrl:n,outcome:t,port:u,readyUrl:l}){const c={baseUrl:n===null?null:C(n),readyUrl:l===null?null:C(l)};r===null&&(r={serviceName:e,runtimeLabel:s,...c,port:u,attempts:[]},d.push(r)),Object.assign(r,c,{port:u}),r.attempts.push({port:u,outcome:t}),f()},amendLastOutcome(n){const t=r?.attempts.at(-1);t&&(t.outcome=n,f())}}},acquisition({graphKey:s,leaseId:e,source:r,tasks:n}){const t={source:r,graphKey:s,leaseId:e,runtimeId:null,services:[],status:"pending",startedAt:g(),settledAt:null,error:null,releaseErrors:[],failingReleases:new Set,releaseReported:!1,setups:[],tasks:n.map(l=>({...l})),values:null};p.push(t);const u=({runtimeId:l,services:c})=>{t.runtimeId=l,t.services=c.map(i=>({...i}))};return{...w([t],"source"),assigned:u,acquired(l){u(l),t.status="acquired",t.settledAt=g()},cancelled(l){t.status="cancelled",t.error=v(l),t.settledAt??=g()},failed(l){t.status="failed",t.error=v(l),t.settledAt??=g()},rendered(l){t.values=l,f()},setup({module:l,task:c}){const i={error:null,module:l,releaseErrors:[],releaseReported:!1,settledAt:null,startedAt:g(),status:"pending",task:{...c}};t.setups.push(i);const S=(o,A)=>{i.status=o,i.error=A===null?null:v(A),i.settledAt??=g()};return{prepared(o){S("prepared",null);for(const A of E("setupData",o,b))R.add(A);t.values!==null&&(t.values={...t.values,setupData:o}),f()},failed(o){S("failed",o)},cancelled(o){S("cancelled",o)},releaseFailed(o){i.releaseErrors.push(v(o)),i.releaseReported=!0},released(){i.releaseReported=!0}}}}},releaseFor(s){const e=p.filter(r=>r.leaseId===s);return e.length===0?null:w(e,"lease")},recordCapacity({instanceCount:s,resourceBudgets:e}){m={...m,instanceCount:s,resourceBudgets:[...e]},f()},recordRuntimeStats(s){m={...m,runtimeStats:[...s]},f()},recordGroups(s){y=s.map(e=>({...e}))},recordMissingValues(s,e){for(const{kind:r,valueName:n}of e)h.push({kind:r,task:{...s},valueName:n})},recordCleanup(s){k=s==null?{status:"released"}:{status:"failed",errors:[v(s)],recovered:!1}},addRedactionTarget(s){s.length>0&&R.add(s)},redactionTargets(){return[...R]},snapshot(){const s={acquisitions:p.map(({failingReleases:e,releaseErrors:r,releaseReported:n,...t})=>({...t,services:t.services.map(u=>({...u})),setups:t.setups.map(({releaseErrors:u,releaseReported:l,...c})=>({...c,task:{...c.task},release:u.length>0?{status:"failed",errors:[...u],recovered:!1}:l?{status:"released"}:null})),tasks:t.tasks.map(u=>({...u})),values:t.values,release:r.length>0?{status:"failed",errors:[...r],recovered:e.size===0}:n?{status:"released"}:null})),capacity:{instanceCount:m.instanceCount,resourceBudgets:[...m.resourceBudgets],runtimeStats:[...m.runtimeStats]},cleanup:L(k),groups:y.map(e=>({...e})),logs:{services:a.logRegistry?.listServiceLogs()??[],setup:a.logRegistry?.listSetupLogs()??[]},missingValues:h.map(e=>({...e,task:{...e.task}})),services:d.map(e=>({...e,attempts:e.attempts.map(r=>({...r}))})).sort((e,r)=>e.runtimeLabel.localeCompare(r.runtimeLabel)||e.serviceName.localeCompare(r.serviceName)),setupOperations:a.setupRegistry?.listOperations()??[]};return K(s,B(R))}}}function L(a){return a===null||a.status==="released"?a:{status:"failed",errors:[...a.errors],recovered:a.recovered}}
1
+ import{formatError as f}from"../kernel/error-serialization.js";import{jsonRedactionTargets as q,orderRedactionTargets as C,sensitiveKeyMatcher as I}from"../kernel/redaction-targets.js";import{redactUrlSecrets as b}from"../process/url-redaction.js";import{redactedRunValue as K}from"./redaction.js";export function publishEnvironmentRecord(l){return{...l,acquisitions:l.acquisitions.map(o=>({...o,settledAt:E(o.settledAt),setups:o.setups.map(p=>({...p,settledAt:E(p.settledAt),startedAt:new Date(p.startedAt).toISOString()})),startedAt:new Date(o.startedAt).toISOString()})),dbBackend:l.groups.some(o=>o.dbBackend==="local")?"local":null}}function E(l){return l===null?null:new Date(l).toISOString()}export function createRunEnvironmentRecord(l={}){const o=[],p=[],S=[],R=new Set,k=I(l.redactKeys);let y=[],v={instanceCount:0,resourceBudgets:[],runtimeStats:[]},w=null;const m=l.now??Date.now,g=()=>{l.onPublishedChange?.()},O=(e,r)=>({releaseFailed(s){for(const n of e)n.releaseErrors.push(f(s)),n.failingReleases.add(r),n.releaseReported=!0},released(){for(const s of e)s.failingReleases.delete(r),s.releaseReported=!0}});return{redactKeys:l.redactKeys??[],service({runtimeLabel:e,serviceName:r}){let s=null;return{recordAttempt({baseUrl:n,outcome:t,port:d,readyUrl:a}){const c={baseUrl:n===null?null:b(n),readyUrl:a===null?null:b(a)};s===null&&(s={serviceName:r,runtimeLabel:e,...c,port:d,attempts:[]},o.push(s)),Object.assign(s,c,{port:d}),s.attempts.push({port:d,outcome:t}),g()},amendLastOutcome(n){const t=s?.attempts.at(-1);t&&(t.outcome=n,g())}}},acquisition({graphKey:e,leaseId:r,source:s,tasks:n}){const t={source:s,graphKey:e,leaseId:r,runtimeId:null,services:[],status:"pending",startedAt:m(),settledAt:null,error:null,releaseErrors:[],failingReleases:new Set,releaseReported:!1,setups:[],tasks:n.map(a=>({...a})),values:null};p.push(t);const d=({runtimeId:a,services:c})=>{t.runtimeId=a,t.services=c.map(i=>({...i}))};return{...O([t],"source"),assigned:d,acquired(a){d(a),t.status="acquired",t.settledAt=m()},cancelled(a){t.status="cancelled",t.error=f(a),t.settledAt??=m()},failed(a){t.status="failed",t.error=f(a),t.settledAt??=m()},rendered(a){t.values=a,g()},setup({module:a,task:c}){const i={error:null,module:a,releaseErrors:[],releaseReported:!1,settledAt:null,startedAt:m(),status:"pending",task:{...c}};t.setups.push(i);const h=(u,A)=>{i.status=u,i.error=A===null?null:f(A),i.settledAt??=m()};return{prepared(u){h("prepared",null);for(const A of q("setupData",u,k))R.add(A);t.values!==null&&(t.values={...t.values,setupData:u}),g()},failed(u){h("failed",u)},cancelled(u){h("cancelled",u)},releaseFailed(u){i.releaseErrors.push(f(u)),i.releaseReported=!0},released(){i.releaseReported=!0}}}}},releaseFor(e){const r=p.filter(s=>s.leaseId===e);return r.length===0?null:O(r,"lease")},recordCapacity({instanceCount:e,resourceBudgets:r}){v={...v,instanceCount:e,resourceBudgets:[...r]},g()},recordRuntimeStats(e){v={...v,runtimeStats:[...e]},g()},recordGroups(e){y=e.map(r=>({...r}))},recordMissingValues(e,r){for(const{kind:s,valueName:n}of r)S.push({kind:s,task:{...e},valueName:n})},recordCleanup(e){w=e==null?{status:"released"}:{status:"failed",errors:[f(e)],recovered:!1}},addRedactionTarget(e){e.length>0&&R.add(e)},redactionTargets(){return[...R]},snapshot(){const e={acquisitions:p.map(({failingReleases:r,releaseErrors:s,releaseReported:n,...t})=>({...t,setups:t.setups.map(({releaseErrors:d,releaseReported:a,...c})=>({...c,release:d.length>0?{status:"failed",errors:d,recovered:!1}:a?{status:"released"}:null})),release:s.length>0?{status:"failed",errors:s,recovered:r.size===0}:n?{status:"released"}:null})),capacity:v,cleanup:w,groups:y,logs:{services:l.logRegistry?.listServiceLogs()??[],setup:l.logRegistry?.listSetupLogs()??[]},missingValues:S,services:[...o].sort((r,s)=>r.runtimeLabel.localeCompare(s.runtimeLabel)||r.serviceName.localeCompare(s.serviceName)),setupOperations:l.setupRegistry?.listOperations()??[]};return K(e,C(R))}}}
@@ -1 +1 @@
1
- export{normalizeArtifactArchiveConfig}from"./artifact-config.js";export{loadTimings,persistTaskArtifacts,persistTaskFileArtifacts,persistTaskOutputArtifacts,saveTimings}from"./artifacts.js";export{applyRunBundleRetention,listRunBundles,loadRunBundleArtifact,loadRunBundleManifest,loadVerifiedRunBundleArtifactIfExists,verifyRunBundle}from"./bundles.js";export{collectArtifactEntries,loadCurrentRunArtifact,loadLatestRunArtifact,loadRun,loadRunArtifact}from"./reading.js";export{DEFAULT_REPORTED_ARCHIVE_CONFIG}from"./reporting-types.js";export{runModel}from"./run-model.js";export{copyRegularFileSecurely,MAX_PERSISTED_FILE_ARTIFACT_BYTES}from"./secure-file-copy.js";export{collectTestkitStorageInventory}from"./storage-inventory.js";export{}from"./timing.js";
1
+ export{normalizeArtifactArchiveConfig}from"./artifact-config.js";export{loadTimings,persistTaskArtifacts,persistTaskOutputArtifacts,saveTimings}from"./artifacts.js";export{listRunBundles,loadRunBundleArtifact,loadRunBundleModels,loadRunBundleManifest,verifyRunBundle}from"./bundles.js";export{collectArtifactEntries,loadCurrentRunArtifact,loadLatestRunArtifact,loadRun}from"./reading.js";export{}from"./reporting-types.js";export{runModel}from"./run-model.js";export{RUN_INPUTS_VERSION}from"./run-inputs.js";export{MAX_PERSISTED_FILE_ARTIFACT_BYTES}from"./secure-file-copy.js";export{collectTestkitStorageInventory}from"./storage-inventory.js";export{}from"./timing.js";
@@ -1,2 +1,2 @@
1
- import p from"fs";import u from"path";import{posixPath as b}from"../process/files/paths.js";const S="logs",N="setup";export function createRunLogRegistry(r){const m=new Map,s=new Map,n=new Set;function L(e){const t=u.join(r,e.path);p.mkdirSync(u.dirname(t),{recursive:!0});const a=p.createWriteStream(t,{fd:p.openSync(t,"a"),flags:"a"});return n.add(a),{...e,write(o){a.write(o)}}}return{ensureServiceLogRecord(e){const t=`${e.runtimeLabel||e.name}:${e.name}`,a=m.get(t);if(a)return a;const o=`${i(e.runtimeLabel||e.name)}__${i(e.name)}.log`,c=u.join(".testkit","results",S,o),l=L({serviceName:e.name,runtimeLabel:e.runtimeLabel||e.name,path:b(c)});return m.set(t,l),l},ensureSetupLogRecord(e,t){const a=`${e.runtimeLabel||e.name}:${e.name}:${t}`,o=s.get(a);if(o)return o;const c=`${i(e.runtimeLabel||e.name)}__${i(e.name)}__${i(t)}.log`,l=u.join(".testkit","results",N,c),v=L({serviceName:e.name,runtimeLabel:e.runtimeLabel||e.name,stage:t,path:b(l)});return s.set(a,v),v},append(e,t,a){!e||typeof a!="string"||e.write(`${new Date().toISOString()} [${t}] ${a}
2
- `)},listServiceLogs(){return[...m.values()].map(e=>({serviceName:e.serviceName,runtimeLabel:e.runtimeLabel,path:e.path})).sort((e,t)=>e.serviceName.localeCompare(t.serviceName)||e.runtimeLabel.localeCompare(t.runtimeLabel))},listSetupLogs(){return[...s.values()].map(e=>({serviceName:e.serviceName,runtimeLabel:e.runtimeLabel,stage:e.stage,path:e.path})).sort((e,t)=>e.serviceName.localeCompare(t.serviceName)||e.runtimeLabel.localeCompare(t.runtimeLabel)||e.stage.localeCompare(t.stage))},async closeAll(){await Promise.all([...n].map(e=>$(e)))}}}function i(r){return r.trim().toLowerCase().replace(/[^a-z0-9._-]+/g,"-").replace(/^-+|-+$/g,"")||"log"}function $(r){return!r||r.destroyed||r.closed?Promise.resolve():new Promise(m=>{let s=!1;const n=()=>{s||(s=!0,r.off("close",n),r.off("error",n),m())};r.once("close",n),r.once("error",n),r.end()})}
1
+ import p from"fs";import u from"path";import{posixPath as b}from"../process/files/paths.js";import{sanitizeSegment as l}from"./storage.js";const S="logs",N="setup";export function createRunLogRegistry(a){const m=new Map,s=new Map,n=new Set;function L(e){const t=u.join(a,e.path);p.mkdirSync(u.dirname(t),{recursive:!0});const r=p.createWriteStream(t,{fd:p.openSync(t,"a"),flags:"a"});return n.add(r),{...e,write(o){r.write(o)}}}return{ensureServiceLogRecord(e){const t=`${e.runtimeLabel||e.name}:${e.name}`,r=m.get(t);if(r)return r;const o=`${l(e.runtimeLabel||e.name,"log")}__${l(e.name,"log")}.log`,c=u.join(".testkit","results",S,o),i=L({serviceName:e.name,runtimeLabel:e.runtimeLabel||e.name,path:b(c)});return m.set(t,i),i},ensureSetupLogRecord(e,t){const r=`${e.runtimeLabel||e.name}:${e.name}:${t}`,o=s.get(r);if(o)return o;const c=`${l(e.runtimeLabel||e.name,"log")}__${l(e.name,"log")}__${l(t,"log")}.log`,i=u.join(".testkit","results",N,c),v=L({serviceName:e.name,runtimeLabel:e.runtimeLabel||e.name,stage:t,path:b(i)});return s.set(r,v),v},append(e,t,r){!e||typeof r!="string"||e.write(`${new Date().toISOString()} [${t}] ${r}
2
+ `)},listServiceLogs(){return[...m.values()].map(e=>({serviceName:e.serviceName,runtimeLabel:e.runtimeLabel,path:e.path})).sort((e,t)=>e.serviceName.localeCompare(t.serviceName)||e.runtimeLabel.localeCompare(t.runtimeLabel))},listSetupLogs(){return[...s.values()].map(e=>({serviceName:e.serviceName,runtimeLabel:e.runtimeLabel,stage:e.stage,path:e.path})).sort((e,t)=>e.serviceName.localeCompare(t.serviceName)||e.runtimeLabel.localeCompare(t.runtimeLabel)||e.stage.localeCompare(t.stage))},async closeAll(){await Promise.all([...n].map(e=>$(e)))}}}function $(a){return!a||a.destroyed||a.closed?Promise.resolve():new Promise(m=>{let s=!1;const n=()=>{s||(s=!0,a.off("close",n),a.off("error",n),m())};a.once("close",n),a.once("error",n),a.end()})}
@@ -1 +1 @@
1
- import{performance as M}from"node:perf_hooks";import{attachErrorOperation as x}from"../kernel/error-serialization.js";import{formatOperationSummary as b}from"../domain/operation-display.js";import{isUnknownRecord as I}from"../kernel/unknown-values.js";import{recordWork as k}from"../kernel/work-metrics.js";const g=String;export function createOperationTimingRegistry({onChange:t=null}={}){const r=[],i=new Map;let m=1;function o(e=[]){if(!t)return;const a=[],u=new Set;for(const s of e)!s||u.has(s.id)||(u.add(s.id),a.push(p(s)));k("operations.changedEntriesDelivered",a.length),t(a)}function l({name:e,kind:a="operation",source:u="testkit",summary:s=null,scope:d=null,parentId:n=null,metadata:f=null}){const c={id:`operation-${g(m++)}`,source:u,name:D(e),kind:a,summary:s,scope:h(d),parentId:n,status:"running",startedAt:new Date().toISOString(),updatedAt:null,finishedAt:null,durationMs:null,error:null,message:null,logRefs:[],metadata:h(f),_startedMonotonicMs:M.now()};return r.push(c),i.set(c.id,c),o([c,...w(c)]),c}function A(e,{summary:a,message:u,metadata:s=null,logRefs:d=null}={}){const n=typeof e=="string"?i.get(e):e;return n?(n.updatedAt=new Date().toISOString(),a!==void 0&&(n.summary=a),u!==void 0&&(n.message=u),s!==null&&(n.metadata={...n.metadata,...h(s)}),d!==null&&(n.logRefs=S(d)),o([n,...w(n)]),p(n)):null}function O(e,{status:a="passed",summary:u,error:s=null,metadata:d=null}={}){const n=typeof e=="string"?i.get(e):e;return n?(n.finishedAt=new Date().toISOString(),n.updatedAt=n.finishedAt,n.durationMs=Math.max(0,Math.round(M.now()-n._startedMonotonicMs)),n.status=a,u!==void 0&&(n.summary=u),s!==null&&(n.error=g(s)),d!==null&&(n.metadata={...n.metadata,...h(d)}),o([n,...w(n)]),p(n)):null}function $(e={}){const a=z(e,m++),u=i.get(a.id);return u?(Object.assign(u,a),o([u,...w(u)]),p(u)):(r.push(a),i.set(a.id,a),o([a,...w(a)]),p(a))}function R(){return k("operations.historyClones"),r.map(e=>p(e))}return{start:l,update:A,finish:O,record:$,listOperations:R,countOperations(){return r.length}};function w(e){if(!e.parentId)return[];const a=[],u=new Date().toISOString();let s=i.get(e.parentId);const d=new Set([e.id]);for(;s&&s.status==="running"&&!d.has(s.id);){d.add(s.id),s.updatedAt=u;const n=e.status||"unknown",f=n==="running"?"activeChild":"lastChild";s.message=N(e),s.metadata={...s.metadata,[`${f}OperationId`]:e.id,[`${f}OperationName`]:e.name,[`${f}OperationKind`]:e.kind,[`${f}OperationStatus`]:n,[`${f}OperationSummary`]:e.summary||""},a.push(s),s=s.parentId?i.get(s.parentId):void 0}return a}}export async function timeOperation(t,r,i){if(!t)return i();const m=t.start(r);try{const o=await i(m);return t.finish(m,{status:"passed"}),o}catch(o){const l=t.finish(m,{status:"failed",error:C(o)});throw x(o,l),o}}function z(t,r){const i=y(t.startedAt)||new Date().toISOString(),m=t.status||"passed",o=m==="running"?null:y(t.finishedAt)||i,l=t.durationMs,A=Number(l),O=l!=null&&Number.isFinite(A)?Math.max(0,Math.round(A)):o?Math.max(0,Date.parse(o)-Date.parse(i)):null;return{id:t.id||`operation-${g(r)}`,source:t.source||"testkit",name:D(t.name),kind:t.kind||"operation",summary:t.summary||null,scope:h(t.scope),parentId:t.parentId||null,status:m,startedAt:i,updatedAt:y(t.updatedAt)||null,finishedAt:o,durationMs:O,error:t.error||null,message:t.message||null,logRefs:S(t.logRefs),metadata:h(t.metadata),_startedMonotonicMs:M.now()-(O||0)}}function p(t){return{id:t.id,source:t.source,name:t.name,kind:t.kind,summary:t.summary,scope:t.scope,parentId:t.parentId,status:t.status,startedAt:t.startedAt,updatedAt:t.updatedAt,finishedAt:t.finishedAt,durationMs:t.durationMs,error:t.error,message:t.message,logRefs:S(t.logRefs),metadata:t.metadata}}function D(t){const r=g(t||"").trim();if(!r)throw new Error("operation timing name is required");return r}function h(t){return I(t)?{...t}:{}}function S(t){return Array.isArray(t)?t.map(r=>I(r)?{...r}:r):[]}function N(t){const r=b(t),i=typeof r=="string"?r:"";return i?`${t.name} ${t.status||"unknown"} - ${i}`:`${t.name} ${t.status||"unknown"}`}function y(t){const r=Date.parse(t||"");return Number.isFinite(r)?new Date(r).toISOString():null}function C(t){return(typeof t!="object"||t===null)&&typeof t!="function"?g(t):Reflect.get(t,"message")||g(t)}
1
+ import{performance as A}from"node:perf_hooks";import{attachErrorOperation as I}from"../kernel/error-serialization.js";import{formatOperationSummary as $}from"../domain/operation-display.js";import{isUnknownRecord as O}from"../kernel/unknown-values.js";import{recordWork as y}from"../kernel/work-metrics.js";const l=String;export function createOperationTimingRegistry({onChange:t=null}={}){const r=[],a=new Map;let c=1;function o(e=[]){if(!t)return;const i=[],u=new Set;for(const n of e)!n||u.has(n.id)||(u.add(n.id),i.push(g(n)));y("operations.changedEntriesDelivered",i.length),t(i)}function p({name:e,kind:i="operation",source:u="testkit",summary:n=null,scope:d=null,parentId:s=null,metadata:m=null}){const f={id:`operation-${l(c++)}`,source:u,name:k(e),kind:i,summary:n,scope:h(d),parentId:s,status:"running",startedAt:new Date().toISOString(),updatedAt:null,finishedAt:null,durationMs:null,error:null,message:null,logRefs:[],metadata:h(m),_startedMonotonicMs:A.now()};return r.push(f),a.set(f.id,f),o([f,...w(f)]),f}function M(e,{status:i="passed",summary:u,error:n=null,metadata:d=null}={}){const s=typeof e=="string"?a.get(e):e;return s?(s.finishedAt=new Date().toISOString(),s.updatedAt=s.finishedAt,s.durationMs=Math.max(0,Math.round(A.now()-s._startedMonotonicMs)),s.status=i,u!==void 0&&(s.summary=u),n!==null&&(s.error=l(n)),d!==null&&(s.metadata={...s.metadata,...h(d)}),o([s,...w(s)]),g(s)):null}function S(){return y("operations.historyClones"),r.map(e=>g(e))}return{start:p,finish:M,listOperations:S};function w(e){if(!e.parentId)return[];const i=[],u=new Date().toISOString();let n=a.get(e.parentId);const d=new Set([e.id]);for(;n&&n.status==="running"&&!d.has(n.id);){d.add(n.id),n.updatedAt=u;const s=e.status||"unknown",m=s==="running"?"activeChild":"lastChild";n.message=x(e),n.metadata={...n.metadata,[`${m}OperationId`]:e.id,[`${m}OperationName`]:e.name,[`${m}OperationKind`]:e.kind,[`${m}OperationStatus`]:s,[`${m}OperationSummary`]:e.summary||""},i.push(n),n=n.parentId?a.get(n.parentId):void 0}return i}}export async function timeOperation(t,r,a){if(!t)return a();const c=t.start(r);try{const o=await a(c);return t.finish(c,{status:"passed"}),o}catch(o){const p=t.finish(c,{status:"failed",error:C(o)});throw I(o,p),o}}function g(t){return{id:t.id,source:t.source,name:t.name,kind:t.kind,summary:t.summary,scope:t.scope,parentId:t.parentId,status:t.status,startedAt:t.startedAt,updatedAt:t.updatedAt,finishedAt:t.finishedAt,durationMs:t.durationMs,error:t.error,message:t.message,logRefs:R(t.logRefs),metadata:t.metadata}}function k(t){const r=l(t||"").trim();if(!r)throw new Error("operation timing name is required");return r}function h(t){return O(t)?{...t}:{}}function R(t){return Array.isArray(t)?t.map(r=>O(r)?{...r}:r):[]}function x(t){const r=$(t),a=typeof r=="string"?r:"";return a?`${t.name} ${t.status||"unknown"} - ${a}`:`${t.name} ${t.status||"unknown"}`}function C(t){return(typeof t!="object"||t===null)&&typeof t!="function"?l(t):Reflect.get(t,"message")||l(t)}