@gfdelarue/testkit 0.4.3 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +109 -22
- package/bin/testkit-windows-owner-arm64.exe +0 -0
- package/bin/testkit-windows-owner-x64.exe +0 -0
- package/dist/app/bugs/categories.js +1 -1
- package/dist/app/bugs/draft.js +3 -3
- package/dist/app/bugs/facts.d.ts +3 -2
- package/dist/app/bugs/facts.js +2 -2
- package/dist/app/bugs/guide.js +1 -1
- package/dist/app/bugs/run-reading.js +1 -1
- package/dist/app/db-schema/prepare.js +1 -1
- package/dist/app/destroy/index.js +1 -1
- package/dist/app/doctor/index.js +1 -1
- package/dist/app/environments/config-driven.d.ts +5 -0
- package/dist/app/environments/config-driven.js +1 -0
- package/dist/app/environments/routing.d.ts +6 -0
- package/dist/app/environments/routing.js +1 -0
- package/dist/app/environments/supplied.d.ts +3 -0
- package/dist/app/environments/supplied.js +1 -0
- package/dist/app/file-selection.js +1 -1
- package/dist/app/lint/native.js +1 -1
- package/dist/app/lint/options.d.ts +0 -1
- package/dist/app/lint/options.js +1 -1
- package/dist/app/lint/shared.d.ts +0 -2
- package/dist/app/lint/shared.js +1 -1
- package/dist/app/package-inspection.js +1 -1
- package/dist/app/reports/files.d.ts +13 -0
- package/dist/app/reports/files.js +1 -0
- package/dist/app/reports/prepare.d.ts +20 -0
- package/dist/app/reports/prepare.js +2 -0
- package/dist/app/reports/publish.d.ts +21 -0
- package/dist/app/reports/publish.js +1 -0
- package/dist/app/run/engine-evidence-persistence.js +1 -1
- package/dist/app/run/engine-execution-cleanup.js +1 -1
- package/dist/app/run/engine-execution-failures.js +1 -1
- package/dist/app/run/engine-execution.d.ts +10 -8
- package/dist/app/run/engine-execution.js +1 -1
- package/dist/app/run/engine-orchestration.d.ts +15 -34
- package/dist/app/run/engine-orchestration.js +1 -1
- package/dist/app/run/environment.d.ts +33 -0
- package/dist/app/run/environment.js +1 -0
- package/dist/app/run/failure-details.js +1 -1
- package/dist/app/run/live-run.d.ts +3 -1
- package/dist/app/run/live-run.js +1 -1
- package/dist/app/run/orchestrator.d.ts +8 -1
- package/dist/app/run/orchestrator.js +1 -1
- package/dist/app/run/planning.d.ts +1 -2
- package/dist/app/run/planning.js +1 -1
- package/dist/app/run/results.d.ts +1 -1
- package/dist/app/run/results.js +1 -1
- package/dist/app/run/run-finalization.d.ts +6 -2
- package/dist/app/run/run-finalization.js +1 -1
- package/dist/app/run/runtime-fixtures.js +1 -1
- package/dist/app/run/runtime-task-types.d.ts +1 -1
- package/dist/app/run/scheduler/index.js +1 -1
- package/dist/app/run/scheduler/locks.d.ts +6 -0
- package/dist/app/run/scheduler/locks.js +1 -0
- package/dist/app/run/selection.d.ts +0 -1
- package/dist/app/run/selection.js +1 -1
- package/dist/app/run/setup/runtime-node-setup-protocol.js +1 -1
- package/dist/app/run/setup/runtime-node-setup-worker.js +1 -1
- package/dist/app/run/task-leases.d.ts +15 -0
- package/dist/app/run/task-leases.js +1 -0
- package/dist/app/run/worker-loop.d.ts +13 -4
- package/dist/app/run/worker-loop.js +1 -1
- package/dist/app/tracker/index.d.ts +6 -1
- package/dist/app/tracker/index.js +1 -1
- package/dist/app/typecheck/index.js +4 -4
- package/dist/cli/command-catalog.d.ts +19 -0
- package/dist/cli/command-catalog.js +1 -1
- package/dist/cli/command-registry.js +1 -1
- package/dist/cli/commands/report/prepare.d.ts +12 -0
- package/dist/cli/commands/report/prepare.js +5 -0
- package/dist/cli/commands/report/publish.d.ts +13 -0
- package/dist/cli/commands/report/publish.js +2 -0
- package/dist/cli/json-errors.js +1 -1
- package/dist/cli/operations/run/operation.d.ts +3 -1
- package/dist/cli/operations/run/operation.js +2 -2
- package/dist/cli/renderers/doctor/text.js +1 -1
- package/dist/cli/renderers/run/formatting.js +1 -1
- package/dist/config/authoring/index.d.ts +4 -13
- package/dist/config/authoring/index.js +1 -1
- package/dist/config/authoring/preset-env.js +1 -1
- package/dist/config/loader/config-loader.js +1 -1
- package/dist/config/loader/database.js +1 -1
- package/dist/config/loader/decode-execution.d.ts +0 -2
- package/dist/config/loader/decode-execution.js +1 -1
- package/dist/config/loader/discover-tests.js +1 -1
- package/dist/config/loader/discovery.d.ts +0 -1
- package/dist/config/loader/discovery.js +2 -2
- package/dist/config/loader/env.js +1 -1
- package/dist/config/loader/execution-model.d.ts +0 -1
- package/dist/config/loader/execution-model.js +1 -1
- package/dist/config/loader/file-metadata.d.ts +1 -1
- package/dist/config/loader/file-metadata.js +1 -1
- package/dist/config/loader/groups.d.ts +33 -0
- package/dist/config/loader/groups.js +1 -0
- package/dist/config/loader/load.d.ts +0 -2
- package/dist/config/loader/load.js +1 -1
- package/dist/config/loader/runtime-isolation.js +1 -1
- package/dist/config/loader/runtime.d.ts +0 -9
- package/dist/config/loader/runtime.js +1 -1
- package/dist/config/loader/skip-config.js +1 -1
- package/dist/config/loader/validation.d.ts +1 -2
- package/dist/config/loader/validation.js +1 -1
- package/dist/config/loader/values.d.ts +0 -1
- package/dist/config/loader/values.js +1 -1
- package/dist/database/activity-gate.js +1 -1
- package/dist/database/admin.js +2 -2
- package/dist/database/lifecycle-cleanup.d.ts +1 -0
- package/dist/database/lifecycle-cleanup.js +1 -1
- package/dist/database/local-postgres.js +1 -1
- package/dist/database/naming.js +1 -1
- package/dist/database/schema-operations.d.ts +1 -1
- package/dist/database/schema-operations.js +1 -1
- package/dist/database/schema-source.js +3 -3
- package/dist/database/source-url.d.ts +0 -1
- package/dist/database/source-url.js +2 -2
- package/dist/database/state.js +1 -1
- package/dist/database/steps.js +3 -3
- package/dist/domain/file-timeout.d.ts +0 -3
- package/dist/domain/file-timeout.js +1 -1
- package/dist/domain/operation-display.js +1 -1
- package/dist/domain/test-outcomes.d.ts +0 -1
- package/dist/domain/test-outcomes.js +1 -1
- package/dist/engines/adapters/evidence-identity.js +1 -1
- package/dist/engines/adapters/execution.d.ts +0 -1
- package/dist/engines/adapters/execution.js +1 -1
- package/dist/engines/api/capabilities.d.ts +0 -10
- package/dist/engines/api/capabilities.js +1 -1
- package/dist/engines/api/execution-scope.js +1 -1
- package/dist/engines/api/k6-runtime-input.js +1 -1
- package/dist/engines/bundle-cache.js +1 -1
- package/dist/engines/contracts/fixture-manifest.js +1 -1
- package/dist/engines/definitions.d.ts +1 -0
- package/dist/engines/definitions.js +1 -1
- package/dist/engines/installations/playwright-resolution-child.js +1 -1
- package/dist/engines/installations/playwright.js +1 -1
- package/dist/engines/k6/adapter/evidence.js +4 -4
- package/dist/engines/k6/adapter/executor.js +1 -1
- package/dist/engines/k6/adapter/native-summary.js +1 -1
- package/dist/engines/k6/runtime/checks.js +1 -1
- package/dist/engines/k6/runtime/dal-fixtures.js +1 -1
- package/dist/engines/k6/runtime/dal-suite.js +1 -1
- package/dist/engines/k6/runtime/http-suite-runtime.js +1 -1
- package/dist/engines/k6/runtime/http.js +1 -1
- package/dist/engines/k6/runtime/shared/db-normalization.js +1 -1
- package/dist/engines/k6/runtime/shared/fixture-engine.js +2 -2
- package/dist/engines/k6/runtime/shared/http-parsing.js +3 -3
- package/dist/engines/managed-module-resolution-policy-input.d.ts +0 -1
- package/dist/engines/managed-module-resolution-policy-input.js +1 -1
- package/dist/engines/managed-module-resolution-policy.js +1 -1
- package/dist/engines/node-vitest/adapter/adapter.js +1 -1
- package/dist/engines/node-vitest/adapter/evidence.js +2 -2
- package/dist/engines/node-vitest/adapter/workspace.js +22 -21
- package/dist/engines/node-vitest/runtime/contract/index.js +1 -1
- package/dist/engines/node-vitest/runtime/data/http.js +1 -1
- package/dist/engines/node-vitest/runtime/data/index.d.ts +2 -2
- package/dist/engines/node-vitest/runtime/data/index.js +1 -1
- package/dist/engines/node-vitest/runtime/data/report.js +5 -5
- package/dist/engines/node-vitest/runtime/data/scenario.d.ts +0 -1
- package/dist/engines/node-vitest/runtime/data/scenario.js +2 -2
- package/dist/engines/node-vitest/runtime/data/types.d.ts +1 -29
- package/dist/engines/node-vitest/runtime/runtime-input.js +1 -1
- package/dist/engines/playwright/adapter/adapter.js +1 -1
- package/dist/engines/playwright/adapter/config.d.ts +1 -1
- package/dist/engines/playwright/adapter/config.js +4 -4
- package/dist/engines/playwright/adapter/evidence-validation.d.ts +0 -3
- package/dist/engines/playwright/adapter/evidence-validation.js +1 -1
- package/dist/engines/playwright/adapter/evidence.js +1 -1
- package/dist/engines/playwright/runtime/config-file.js +1 -1
- package/dist/engines/playwright/runtime/config.js +1 -1
- package/dist/engines/playwright/runtime/runtime-input.d.ts +2 -2
- package/dist/engines/playwright/runtime/runtime-input.js +16 -12
- package/dist/environment/lifecycle-artifacts.js +1 -1
- package/dist/environment/lifecycle-controller.d.ts +1 -11
- package/dist/environment/lifecycle-controller.js +1 -1
- package/dist/environment/lifecycle.d.ts +14 -11
- package/dist/environment/lifecycle.js +1 -1
- package/dist/environment/planning-types.d.ts +0 -2
- package/dist/environment/ports.d.ts +10 -0
- package/dist/environment/ports.js +1 -0
- package/dist/environment/ready-environment.d.ts +2 -5
- package/dist/environment/ready-environment.js +1 -1
- package/dist/environment/run-environment.d.ts +1 -0
- package/dist/environment/run-environment.js +1 -1
- package/dist/environment/runtime-contexts.d.ts +11 -7
- package/dist/environment/runtime-contexts.js +1 -1
- package/dist/environment/runtime-manager.d.ts +1 -9
- package/dist/environment/runtime-manager.js +1 -1
- package/dist/environment/runtime-planning.js +1 -1
- package/dist/environment/runtime-preparation.js +1 -1
- package/dist/environment/runtime-service.d.ts +0 -6
- package/dist/environment/services.d.ts +11 -3
- package/dist/environment/services.js +3 -1
- package/dist/environment/state.d.ts +1 -4
- package/dist/environment/state.js +1 -1
- package/dist/environment/template.d.ts +6 -22
- package/dist/environment/template.js +1 -1
- package/dist/evidence/artifact-config.js +1 -1
- package/dist/evidence/artifact-validation.d.ts +1 -2
- package/dist/evidence/artifact-validation.js +1 -1
- package/dist/evidence/artifacts.js +2 -2
- package/dist/evidence/bundles.js +1 -1
- package/dist/evidence/environment-record.d.ts +22 -0
- package/dist/evidence/environment-record.js +1 -0
- package/dist/evidence/index.d.ts +1 -1
- package/dist/evidence/logs.js +2 -2
- package/dist/evidence/operation-timings.js +1 -1
- package/dist/evidence/read-fields.d.ts +0 -1
- package/dist/evidence/read-fields.js +1 -1
- package/dist/evidence/read-run.js +1 -1
- package/dist/evidence/reading.js +1 -1
- package/dist/evidence/reporting-types.d.ts +19 -0
- package/dist/evidence/reporting.d.ts +2 -2
- package/dist/evidence/reporting.js +1 -1
- package/dist/evidence/storage.d.ts +0 -2
- package/dist/evidence/storage.js +2 -2
- package/dist/evidence/timing.js +1 -1
- package/dist/kernel/error-serialization.d.ts +2 -0
- package/dist/kernel/error-serialization.js +1 -1
- package/dist/kernel/unknown-values.d.ts +5 -0
- package/dist/kernel/unknown-values.js +1 -1
- package/dist/process/environment.js +1 -1
- package/dist/process/execa-bundle.json +3 -3
- package/dist/process/files/json-file.d.ts +0 -1
- package/dist/process/files/json-file.js +1 -1
- package/dist/process/files/paths.d.ts +2 -0
- package/dist/process/files/paths.js +1 -1
- package/dist/process/host/metadata.js +1 -1
- package/dist/process/host/platform.js +1 -1
- package/dist/process/host-process.js +1 -1
- package/dist/process/lifecycle-fence.d.ts +1 -2
- package/dist/process/lifecycle-fence.js +1 -1
- package/dist/process/locks.js +2 -2
- package/dist/process/process-group.js +1 -1
- package/dist/process/process-identity.js +1 -1
- package/dist/process/process.js +30 -30
- package/dist/process/readiness.d.ts +7 -1
- package/dist/process/readiness.js +1 -1
- package/dist/process/recovery.js +1 -1
- package/dist/process/runs/manifest.js +1 -1
- package/dist/process/runs/ownership.js +1 -1
- package/dist/process/runs/product-state.js +1 -1
- package/dist/process/runs/stop.js +1 -1
- package/dist/process/stoppable-settlement.d.ts +26 -0
- package/dist/process/stoppable-settlement.js +1 -0
- package/dist/process/url-redaction.d.ts +1 -0
- package/dist/process/url-redaction.js +1 -0
- package/dist/process/windows-native.d.ts +6 -0
- package/dist/process/windows-native.js +1 -1
- package/dist/repo/ci-environment.js +1 -1
- package/dist/repo/fingerprint-policy.d.ts +0 -2
- package/dist/repo/fingerprint-policy.js +1 -1
- package/dist/repo/physical-tree.js +1 -1
- package/dist/repo/publish-directory.d.ts +20 -0
- package/dist/repo/publish-directory.js +3 -0
- package/dist/repo/state.js +4 -4
- package/dist/steps/build-config.d.ts +0 -1
- package/dist/steps/build-config.js +1 -1
- package/dist/steps/configured-steps.js +1 -1
- package/dist/steps/environment.d.ts +0 -2
- package/dist/steps/environment.js +1 -1
- package/dist/steps/index.d.ts +1 -1
- package/dist/steps/run.js +2 -2
- package/dist/toolchains/archive.js +1 -1
- package/dist/toolchains/index.js +3 -3
- package/dist/toolchains/node-provisioning.js +2 -2
- package/dist/toolchains/vitest/index.js +1 -1
- package/docs/bug-records.md +54 -52
- package/docs/engine-architecture.md +39 -11
- package/package.json +1 -1
- package/testkit-config-schema.md +43 -5
- package/dist/app/run/playwright-configuration.d.ts +0 -2
- package/dist/app/run/playwright-configuration.js +0 -1
- package/dist/environment/readiness.d.ts +0 -12
- package/dist/environment/readiness.js +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import
|
|
1
|
+
import{buildRuntimeIds as w}from"../domain/runtime-identity.js";import{formatError as d}from"../kernel/error-serialization.js";import{cleanupRuntimeInstanceContext as I,createRuntimeInstanceContext as M,ensureRuntimeInstanceReady as L}from"./runtime-contexts.js";import{timeOperation as U}from"../evidence/operation-timings.js";import{createReadyEnvironment as b}from"./ready-environment.js";export function createRuntimeManager({productDir:t,graphs:i,lifecycle:n,runtimeOptions:r={}}){const a=new Map(i.map(e=>[e.key,e])),u=new Map,o=T(i),c=new Map;return{canAcquire(e){if(c.has(e.graphKey))return!1;const s=f(u,a,e);return A(s,e,o)!==null},async acquire(e,s={}){const m=f(u,a,e);if(n.isStopRequested())throw n.signal.reason||new Error("testkit run interrupted");const g=e.leaseId,l=await N(r,s,"testkit.runtime.slot_claim",{summary:"claim runtime slot",scope:{graphKey:e.graphKey,taskId:e.id,serviceName:e.serviceName||null,targetName:e.targetName||null},metadata:{decision:"claim",resourceCost:v(e).toString()}},()=>{const p=S(m,e,o);if(!p)throw new Error(`Task ${e.id.toString()} was claimed before runtime capacity was available`);return p});try{const p=await K(l,t,e,n,r,s);return n.signal.throwIfAborted(),{...b(p,g),resourceCost:v(e),slot:l,context:p}}catch(p){throw R(l,v(e)),l.context||x(l,o),await h(l,n,o),n.isStopRequested()||k(c,e.graphKey,d(p)),p}},async release(e,s={}){e?.slot&&(s.invalidate&&(e.slot.draining=!0),e.released||(R(e.slot,e.resourceCost),e.released=!0),e.slot.draining&&e.slot.activeLeaseCount===0&&await h(e.slot,n,o))},async cleanupAll(){const e=[];for(const s of u.values())for(const m of s.slots){m.draining=!0;try{await h(m,n,o)}catch(g){e.push({slot:m,error:g})}}if(e.length>0)throw new AggregateError(e.map(s=>s.error),`Failed to clean up ${e.length.toString()} runtime ${e.length===1?"slot":"slots"}: ${e.map(s=>`${s.slot.runtimeId}: ${d(s.error)}`).join("; ")}`)},getStats(){return[...u.values()].map(e=>({graphKey:e.slots[0]?.graph.key||null,targetNames:[...e.slots.at(0)?.graph.targetNames??[]],maxConcurrentTasks:Number.isFinite(e.slots[0]?.graph.maxConcurrentTasks)?e.slots[0]?.graph.maxConcurrentTasks??null:null,runtimeCount:e.slots.length,runtimes:e.slots.map(s=>({runtimeId:s.runtimeId,peakLeaseCount:s.peakLeaseCount,peakResourceUnits:s.peakResourceUnits,resourceReservations:s.resourceReservations.map(m=>({...m}))})),resourceBudgets:[...o.values()].filter(s=>e.slots.some(m=>m.graph.resourceReservations.some(g=>g.key===s.key))).map(s=>({key:s.key,capacity:s.capacity,reserved:s.reserved,available:s.available,active:s.active,peakActive:s.peakActive}))})).sort((e,s)=>(e.graphKey||"").localeCompare(s.graphKey||""))},getGraphFailure(e){return c.get(e)??null},getGraph(e){return a.get(e)||null},markGraphFailed(e,s){k(c,e,s)}}}function f(t,i,n){const r=t.get(n.graphKey);if(r)return r;const a=i.get(n.graphKey);if(!a)throw new Error(`Unknown graph "${n.graphKey}"`);const o={slots:w(a.instanceCount).map(c=>({graph:a,runtimeId:c,context:null,activeLeaseCount:0,activeResourceUnits:0,resourceReservations:[],peakLeaseCount:0,peakResourceUnits:0,draining:!1}))};return t.set(n.graphKey,o),o}function S(t,i,n){const r=v(i),a=t.slots.filter(o=>!o.draining);if(a.length===0)return null;const u=[...a].filter(o=>y(o,r)&&C(o,n)).sort((o,c)=>o.activeResourceUnits-c.activeResourceUnits||o.activeLeaseCount-c.activeLeaseCount||o.runtimeId.localeCompare(c.runtimeId)).at(0);return u?($(u,n),u.activeLeaseCount+=1,u.activeResourceUnits+=r,u.peakLeaseCount=Math.max(u.peakLeaseCount,u.activeLeaseCount),u.peakResourceUnits=Math.max(u.peakResourceUnits,u.activeResourceUnits),u):null}function R(t,i){t.activeLeaseCount=Math.max(0,t.activeLeaseCount-1),t.activeResourceUnits=Math.max(0,t.activeResourceUnits-i)}async function K(t,i,n,r,a,u={}){t.context||(t.context=M(t.runtimeId,t.graph,i),r.trackGraphContext(t.context));const o=a.operationTimingRegistry||null,c=o?.start({name:"testkit.runtime.ensure",kind:"runtime",summary:"ensure runtime instance ready",parentId:u.operationTimingParentId||null,scope:{graphKey:t.graph.key,runtimeId:t.runtimeId,serviceName:n.serviceName,targetName:n.targetName}})??null;try{await L(t.context,r,a),o?.finish(c,{status:"passed"})}catch(e){throw o?.finish(c,{status:"failed",error:d(e)}),e}return t.context}function N(t,i,n,r,a){return U(t.operationTimingRegistry||null,{name:n,kind:"runtime",summary:r.summary||null,parentId:i.operationTimingParentId||null,scope:r.scope||null,metadata:r.metadata||null},a)}async function h(t,i,n){!t.context||t.activeLeaseCount>0||(await I(t.context,i),x(t,n),t.context=null,t.draining=!1)}function A(t,i,n){const r=v(i);return t.slots.find(a=>!a.draining&&y(a,r)&&C(a,n))||null}function y(t,i){return t.activeResourceUnits+i<=t.graph.maxConcurrentTasks}function T(t){const i=new Map;for(const n of t)for(const r of n.resourceReservations){const a=i.get(r.key)||{key:r.key,capacity:r.capacity||0,reserved:r.reserved||0,available:r.available||Math.max(0,(r.capacity||0)-(r.reserved||0)),active:0,peakActive:0};a.capacity=Math.max(a.capacity,r.capacity||0),a.reserved=Math.max(a.reserved,r.reserved||0),a.available=Math.max(0,a.capacity-a.reserved),i.set(r.key,a)}return i}function C(t,i){return t.resourceReservations.length>0?!0:t.graph.resourceReservations.every(n=>{const r=i.get(n.key);return r?r.active+n.perRuntime<=r.available:!0})}function $(t,i){if(t.resourceReservations.length>0)return;const n=[];for(const r of t.graph.resourceReservations){const a=i.get(r.key);a&&(a.active+=r.perRuntime,a.peakActive=Math.max(a.peakActive,a.active),n.push({key:r.key,units:r.perRuntime,services:r.services}))}t.resourceReservations=n}function x(t,i){for(const n of t.resourceReservations){const r=i.get(n.key);r&&(r.active=Math.max(0,r.active-n.units))}t.resourceReservations=[]}function k(t,i,n){!i||t.has(i)||t.set(i,n)}function v(t){return t.resourceCost??1}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{resolveRuntimeInstanceConfigs as
|
|
1
|
+
import{resolveRuntimeInstanceConfigs as g}from"./template.js";import{buildGraphResourceReservations as p}from"../database/runtime-reservations.js";import{resolveRuntimeConfigs as m}from"../config/loader/dependencies.js";export const runtimePlanning=Object.freeze({collectDesiredRuntimeGraphs,resolveRuntimeInstanceConfigs:g,buildRuntimeGraphs,resolveRuntimeConfigs:m,buildGraphDirName});export function buildRuntimeGraphs(t){const r=[],s=new Map;for(const e of t){const n=s.get(e.runtimeKey);if(n){n.targetNames.push(e.config.name),n.instanceCount=Math.max(n.instanceCount,e.config.testkit.runtime.instances),n.maxConcurrentTasks=Math.min(n.maxConcurrentTasks,u(e.runtimeConfigs));continue}const i={key:e.runtimeKey,runtimeNames:e.runtimeNames,runtimeConfigs:e.runtimeConfigs,targetNames:[e.config.name],dirName:buildGraphDirName(e.runtimeNames),instanceCount:e.config.testkit.runtime.instances,maxConcurrentTasks:u(e.runtimeConfigs),resourceReservations:p(e.runtimeConfigs)};r.push(i),s.set(e.runtimeKey,i)}return r.sort((e,n)=>e.dirName.localeCompare(n.dirName))}export function buildGraphDirName(t){const r=t.map(f).join("__");return r.length>0?r:"graph"}function f(t){return t.toLowerCase().replace(/[^a-z0-9]+/g,"_").replace(/^_+|_+$/g,"")||"svc"}function u(t){return t.reduce((r,s)=>Math.min(r,s.testkit.runtime.maxConcurrentTasks),Number.POSITIVE_INFINITY)}export function collectDesiredRuntimeGraphs(t=[]){const r=new Map(t.map(e=>[e.name,e])),s=new Map;for(const e of t){const i=m(e,r).map(c=>c.name).sort(),o=i.join("|"),a=s.get(o);if(a){a.targetNames.push(e.name),a.targetNames.sort(),a.instanceCount=Math.max(a.instanceCount,e.testkit.runtime.instances);continue}s.set(o,{key:o,dirName:buildGraphDirName(i),runtimeNames:i,targetNames:[e.name],instanceCount:e.testkit.runtime.instances})}return[...s.values()].sort((e,n)=>e.dirName.localeCompare(n.dirName))}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import S from"crypto";import p from"fs";import k from"path";import{resolveServiceCwd as
|
|
1
|
+
import S from"crypto";import p from"fs";import k from"path";import{resolveServiceCwd as w}from"../config/loader/index.js";import{appendFileToHash as v}from"../repo/fingerprint-policy.js";import{announceResolvedToolchain as D,resolveConfiguredToolchain as y}from"../toolchains/index.js";import{readDatabaseUrl as m}from"../database/state.js";import{buildExecutionEnv as x,buildTemplateExecutionEnv as b,collectConfiguredInputs as R}from"../steps/index.js";import{runConfiguredSteps as g}from"../database/configured-steps.js";import"../evidence/setup-progress.js";import{isUnknownRecord as I,readDynamicProperty as N}from"../kernel/unknown-values.js";const F="prepare-manifest.json";export async function prepareRuntimeServices(e,r={}){for(const t of e)await prepareRuntimeService(t,r)}export async function prepareRuntimeService(e,r={}){const t=e.testkit.runtime.prepare;if(!t||t.steps.length===0||!O(e))return;const n=e.testkit.prepareDir,s=await computeRuntimePrepareFingerprint(e,{signal:r.signal}),i=k.join(n,F);if(A(i)?.fingerprint===s){r.setupRegistry?.recordCached({config:e,stage:"runtime:prepare",kind:"runtime-prepare",summary:"runtime prepare cache hit"});return}p.rmSync(n,{recursive:!0,force:!0}),p.mkdirSync(n,{recursive:!0});const o={...x(e,{...e.testkit.local?.env||{},...r.extraEnv||{}},process.env)},d=m(e.stateDir);d&&(o.DATABASE_URL=d);const u=r.setupRegistry?.start({config:e,stage:"runtime:prepare",kind:"runtime-prepare",summary:"runtime prepare",recordLog:!1}),c=r.setupRegistry;try{await D(e,await y(e,{signal:r.signal}),r.reporter),await g({config:e,steps:t.steps,env:o,labelPrefix:"runtime:prepare",reporter:r.reporter,setupRegistry:c||null,parentOperation:u,signal:r.signal});const a=u?c?.finish(u,{status:"passed",summary:"runtime prepare"}):null;a&&r.reporter?.setupOperationFinished?.(a),P(i,{fingerprint:s,preparedAt:new Date().toISOString(),runtimeId:e.runtimeId||null,serviceName:e.name})}catch(a){const f=u?c?.finish(u,{status:"failed",summary:"runtime prepare",error:N(a,"message")||a}):null;throw f&&r.reporter?.setupOperationFinished?.(f),p.rmSync(n,{recursive:!0,force:!0}),a}}function O(e){return e.testkit.runtime.prepare!=null}export async function computeRuntimePrepareFingerprint(e,r={}){const t=await y(e,{signal:r.signal}),n=S.createHash("sha256");n.update(JSON.stringify({prepare:e.testkit.runtime.prepare,toolchain:t?{kind:"node",install:t.install,nodeVersion:t.nodeVersion,npmVersion:t.npmVersion,nodeSource:t.nodeSource,npmSource:t.npmSource,fingerprint:t.fingerprint}:null,env:b(e,{},{},{includeTestkitControls:!1}),envTemplates:{service:e.testkit.serviceEnv||{},local:e.testkit.local?.env||{}},cwd:e.testkit.local?e.testkit.local.cwd:null})),n.update(JSON.stringify(E(e)));const s=e.testkit.fingerprints||{exclude:[],include:[]};for(const i of e.testkit.envFiles||[])v(n,e.productDir,w(e.productDir,i),s);for(const i of R(e.productDir,e.testkit.runtime.prepare))v(n,e.productDir,i,s);return n.digest("hex")}function E(e){const r=[],t=m(e.stateDir);t&&r.push({service:e.name,url:t});const n=new Set;for(const i of Object.values(e.testkit.serviceEnv||{}))h(i,n,e.name);for(const i of Object.values(e.testkit.local?.env||{}))h(i,n,e.name);const s=e.testkit.templateContext?.stateDirByService;for(const i of[...n].sort()){const l=s?.get(i),o=l?m(l):null;r.push({service:i,url:o||null})}return r}function h(e,r,t){if(typeof e!="string")return;const n=/\{db(?:Url|Host|Port|Name|User|Password|Ssl)(?::([a-zA-Z0-9_-]+))?\}/g;let s=n.exec(e);for(;s;)r.add(s[1]||t),s=n.exec(e)}function A(e){if(!p.existsSync(e))return null;try{const r=JSON.parse(p.readFileSync(e,"utf8"));return I(r)?{fingerprint:r.fingerprint,...typeof r.preparedAt=="string"?{preparedAt:r.preparedAt}:{},...typeof r.runtimeId=="string"||typeof r.runtimeId=="number"||r.runtimeId===null?{runtimeId:r.runtimeId}:{},...typeof r.serviceName=="string"?{serviceName:r.serviceName}:{}}:null}catch{return null}}function P(e,r){p.writeFileSync(e,JSON.stringify(r,null,2))}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import type { EngineJsonObject } from "../engines/api/index.js";
|
|
2
|
-
import type { EngineId } from "../domain/identifiers.js";
|
|
3
1
|
import type { ExecutionEnvironmentConfig } from "../steps/index.js";
|
|
4
2
|
export interface ReadyServiceLocalConfig {
|
|
5
3
|
baseUrl?: null | string;
|
|
@@ -7,12 +5,8 @@ export interface ReadyServiceLocalConfig {
|
|
|
7
5
|
env?: Readonly<Record<string, string>>;
|
|
8
6
|
}
|
|
9
7
|
export interface ReadyServiceConfig extends ExecutionEnvironmentConfig {
|
|
10
|
-
engineConfigurations?: Readonly<Partial<Record<EngineId, EngineJsonObject>>>;
|
|
11
8
|
stateDir: string;
|
|
12
9
|
testkit: ExecutionEnvironmentConfig["testkit"] & {
|
|
13
|
-
execution: {
|
|
14
|
-
fileTimeoutSeconds: number;
|
|
15
|
-
};
|
|
16
10
|
local?: null | ReadyServiceLocalConfig;
|
|
17
11
|
};
|
|
18
12
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { type SupervisedProcessHandle, type SupervisedProcessReady } from "../process/index.js";
|
|
2
2
|
import { type ToolchainResolvedReporter } from "../toolchains/index.js";
|
|
3
|
-
import {
|
|
3
|
+
import type { RuntimeServiceConfig } from "./template.js";
|
|
4
4
|
import { type OutputLineHandler } from "../process/index.js";
|
|
5
5
|
import type { RunLogRegistry } from "../evidence/logs.js";
|
|
6
|
+
import type { RunEnvironmentRecord } from "../evidence/environment-record.js";
|
|
6
7
|
type LocalServiceConfig = RuntimeServiceConfig & {
|
|
7
8
|
runtimeLabel: string;
|
|
8
9
|
stateDir: string;
|
|
@@ -29,7 +30,14 @@ export interface LocalServiceReporter extends ToolchainResolvedReporter<LocalSer
|
|
|
29
30
|
outputMode?: string;
|
|
30
31
|
writeDebugLine?: OutputLineHandler;
|
|
31
32
|
}
|
|
33
|
+
export interface LocalServiceRuntime {
|
|
34
|
+
readonly configs: readonly ResolvedServiceConfig[];
|
|
35
|
+
prepare(serviceNames: ReadonlySet<string>): Promise<void>;
|
|
36
|
+
reassignPort(serviceName: string, startedServiceNames: ReadonlySet<string>): Promise<readonly string[]>;
|
|
37
|
+
releasePort(serviceName: string): Promise<void>;
|
|
38
|
+
}
|
|
32
39
|
export interface StartLocalServicesOptions {
|
|
40
|
+
environmentRecord?: RunEnvironmentRecord | null;
|
|
33
41
|
extraEnv?: NodeJS.ProcessEnv | null;
|
|
34
42
|
logRegistry?: RunLogRegistry | null;
|
|
35
43
|
reporter?: LocalServiceReporter | null;
|
|
@@ -42,7 +50,7 @@ export interface StartedLocalService {
|
|
|
42
50
|
ownership: SupervisedProcessReady;
|
|
43
51
|
supervisor: SupervisedProcessHandle;
|
|
44
52
|
}
|
|
45
|
-
export declare function startLocalServices(
|
|
46
|
-
export declare function startLocalService(config: LocalServiceConfig, lifecycle: LocalServiceLifecycle, options?: Readonly<StartLocalServicesOptions>): Promise<StartedLocalService>;
|
|
53
|
+
export declare function startLocalServices(runtime: LocalServiceRuntime, lifecycle: LocalServiceLifecycle, options?: Readonly<StartLocalServicesOptions>): Promise<StartedLocalService[]>;
|
|
54
|
+
export declare function startLocalService(config: LocalServiceConfig, lifecycle: LocalServiceLifecycle, options?: Readonly<StartLocalServicesOptions>, beforeSpawn?: () => Promise<void>): Promise<StartedLocalService>;
|
|
47
55
|
export declare function stopLocalServices(started: readonly StartedLocalService[], lifecycle: LocalServiceLifecycle | null | undefined): Promise<void>;
|
|
48
56
|
export {};
|
|
@@ -1 +1,3 @@
|
|
|
1
|
-
import{DEFAULT_READY_TIMEOUT_MS as
|
|
1
|
+
import{DEFAULT_READY_TIMEOUT_MS as P,ServiceExitedBeforeReadyError as D,ServiceReadinessTimeoutError as _,waitForReady as C,serviceCompletionError as x}from"../process/readiness.js";import{resolveServiceCwd as I}from"../config/loader/index.js";import{startSupervisedProcess as j,ProcessExitedBeforeReadyError as B,DEFAULT_SHUTDOWN_TIMEOUT_MS as F}from"../process/index.js";import{announceResolvedToolchain as N,applyToolchainEnv as V,resolveConfiguredToolchain as q}from"../toolchains/index.js";import{buildExecutionEnv as H}from"../steps/index.js";import{SERVICE_START_ATTEMPTS as W}from"./ports.js";import{captureOutput as R}from"../process/index.js";import{prepareServiceStartInvocation as X}from"../process/service-processes.js";import{readDatabaseUrl as Y}from"../database/state.js";import{toError as S}from"../kernel/error-serialization.js";const z=20;class y extends Error{outcome;output;constructor(t,r,n){super(n.message,{cause:n}),this.outcome=t,this.output=r,this.name="LocalServiceExitedBeforeReadyError"}}export async function startLocalServices(e,t,r={}){const n=e.configs.filter(O).map(a=>a.name),o=[],c=new Map,u=new Map,f=new Map,b=(a,s)=>{const m=U(a);c.set(a.name,[...c.get(a.name)??[],{port:m,outcome:s}]);let d=f.get(a.name);!d&&r.environmentRecord&&(d=r.environmentRecord.service({runtimeLabel:a.runtimeLabel,serviceName:a.name}),f.set(a.name,d)),d?.recordAttempt({baseUrl:a.testkit.local.baseUrl,outcome:s,port:m,readyUrl:typeof a.testkit.local.readyUrl=="string"?a.testkit.local.readyUrl:null})},g=(a,s,m)=>{const d=c.get(a.name)?.at(-1);if(!m||!d){b(a,s);return}d.outcome=s,f.get(a.name)?.amendLastOutcome(s)};try{for(;o.length<n.length;){const a=n[o.length],s=Q(e.configs,a);let m=!1;const d=async()=>{await e.releasePort(a),b(s,"starting"),m=!0};try{o.push(await startLocalService(s,t,r,d)),g(s,"ready",m)}catch(l){if(g(s,J(l,t),m),t.signal.aborted)throw l;if(!(l instanceof y))throw M(s,c.get(a)??[],l,2);const L=[...u.get(a)??[],l];if(u.set(a,L),L.length>=W)throw K(s,c.get(a)??[],L);try{const E=await e.reassignPort(a,new Set(o.map(h=>h.name))),v=o.findIndex(h=>E.includes(h.name));if(v>=0){const h=o.splice(v);for(const T of h)f.get(T.name)?.amendLastOutcome(`ready, then restarted after ${a} moved to a fresh port`);await stopLocalServices(h,t)}await e.prepare(new Set(n.slice(o.length)))}catch(E){throw G(t)?E:M(s,c.get(a)??[],E,1)}}}}catch(a){try{await stopLocalServices(o,t)}catch(s){throw new AggregateError([a,s],"Local service startup failed and its supervised cleanup also failed",{cause:s})}throw a}return o}function G(e){return e.signal.aborted}function J(e,t){return t.signal.aborted?"cancelled":e instanceof y?e.outcome:e instanceof _?"readiness timed out":"failed to start"}function K(e,t,r){const n=r.at(-1),o=[`Service "${e.runtimeLabel}:${e.name}" exited before becoming ready on all ${r.length.toString()} attempts, each on a fresh OS-assigned port:`,...A(t),...n&&n.output.length>0?[`Last output from attempt ${t.length.toString()}:`,...n.output.map(c=>` ${c}`)]:[]];return new Error(o.join(`
|
|
2
|
+
`),{cause:n})}function M(e,t,r,n){if(t.length<n)return r;const o=S(r);return new Error([`Service "${e.runtimeLabel}:${e.name}" did not start after ${t.length.toString()} attempts, each on a fresh OS-assigned port:`,...A(t),o.message].join(`
|
|
3
|
+
`),{cause:o})}function A(e){return e.map((t,r)=>` attempt ${(r+1).toString()} on port ${t.port.toString()}: ${t.outcome}`)}function Q(e,t){const r=e.find(n=>n.name===t);if(!r||!O(r))throw new Error(`Runtime has no local service "${t}"`);return r}function U(e){const t=e.testkit.local.assignedPort;if(t===void 0)throw new Error(`Testkit assigned no port to service "${e.runtimeLabel}:${e.name}"`);return t}export async function startLocalService(e,t,r={},n=()=>Promise.resolve()){const o=I(e.productDir,e.testkit.local.cwd),c=await q(e,{signal:t.signal});await N(e,c,r.reporter);const u=V(H(e,{...e.testkit.local.env||{},...r.extraEnv||{}},process.env),c),f=U(e);u.PORT=String(f);const b=Y(e.stateDir);b&&(u.DATABASE_URL=b),r.reporter?.localServiceStarting?.(e,e.testkit.local.start);const g=X(e.testkit.local.start,u),a=r.logRegistry?.ensureServiceLogRecord(e),s=r.reporter?.outputMode==="debug"?i=>r.reporter?.writeDebugLine?.(i):null,m=[],d=i=>{m.push(i),m.length>z&&m.shift()};await n();const l=j({args:g.args,windowsVerbatimArguments:g.windowsVerbatimArguments,cwd:o,displayLabel:`Testkit local service ${e.runtimeLabel}:${e.name}`,env:ee(u),executable:g.executable,outputMode:"caller-drained",signal:t.signal,shutdownTimeoutMs:e.testkit.local.shutdownTimeoutMs??F,timeoutMs:null}),L=[R(l.stdout,{livePrefix:`[${e.runtimeLabel}:${e.name}]`,liveWriter:s,onLine(i){d(i),a&&r.logRegistry?.append(a,"stdout",i)}}),R(l.stderr,{livePrefix:`[${e.runtimeLabel}:${e.name}]`,liveWriter:s,onLine(i){d(i),a&&r.logRegistry?.append(a,"stderr",i)}})];let E,v=!1,h=!1;try{E=await l.ready,v=!0,t.registerService(e,l,E,o);const i=e.testkit.local.readyTimeoutMs||P;e.testkit.local.readyUrl!==!1&&e.testkit.local.readyUrl!=null&&await C({name:`${e.runtimeLabel}:${e.name}`,url:e.testkit.local.readyUrl,timeoutMs:i,process:l,signal:t.signal})}catch(i){const p=i instanceof B?x(`${e.runtimeLabel}:${e.name}`,i):i;h=!0;const w=[];try{w.push(...await k(l,L,!1))}catch($){w.push(S($))}if(v)try{t.unregisterService(l.pid)}catch($){w.push(S($))}throw w.length>0?new AggregateError([p,...w],`Local service ${e.runtimeLabel}:${e.name} startup and cleanup failed`,{cause:i}):p instanceof D?new y(p.outcome,m,p):p}return{completionMonitor:l.completion.then(i=>{if(h)return null;if(t.signal.aborted){if(v){v=!1;try{t.unregisterService(l.pid)}catch{}}return null}let p=x(`${e.runtimeLabel}:${e.name}`,i,"unexpectedly");if(v){v=!1;try{t.unregisterService(l.pid)}catch(w){p=new AggregateError([p,w],`Local service ${e.runtimeLabel}:${e.name} exited and its manifest entry could not be removed`)}}try{t.requestStop(p.message)}catch(w){p=new AggregateError([p,w],`Local service ${e.runtimeLabel}:${e.name} exited and the active run could not be interrupted`)}return p}),expectCompletion(){h=!0},name:e.name,outputDrains:L,ownership:E,supervisor:l}}export async function stopLocalServices(e,t){const r=[];for(const n of[...e].reverse()){n.expectCompletion();try{r.push(...await k(n.supervisor,n.outputDrains)),await n.completionMonitor}catch(o){r.push(S(o))}finally{try{t?.unregisterService(n.ownership.pid)}catch(o){r.push(S(o))}}}if(r.length>0)throw new AggregateError(r,"Could not stop all local services")}async function k(e,t,r=!0){const o=await e.stop().then(u=>({error:null,result:u}),u=>({error:S(u),result:null})),c=await Promise.allSettled(t);return[...o.result?Z(o.result,r):[o.error],...c.flatMap(u=>u.status==="rejected"?[S(u.reason)]:[])]}function Z(e,t=!0){return[...t?[e.error]:[],e.terminationError].filter(r=>r!==null)}function ee(e){return Object.fromEntries(Object.entries(e).filter(t=>t[1]!==void 0))}function O(e){return e.testkit.local!=null}
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import type { RuntimeGraph } from "./planning-types.js";
|
|
2
|
-
export type GraphMetadataInput = Pick<RuntimeGraph, "dirName" | "instanceCount" | "key" | "maxConcurrentTasks" | "
|
|
2
|
+
export type GraphMetadataInput = Pick<RuntimeGraph, "dirName" | "instanceCount" | "key" | "maxConcurrentTasks" | "runtimeNames" | "targetNames">;
|
|
3
3
|
export interface GraphMetadata extends Record<string, unknown> {
|
|
4
4
|
dirName?: null | string;
|
|
5
5
|
instanceCount?: null | number;
|
|
6
6
|
key?: null | string;
|
|
7
7
|
maxConcurrentTasks?: null | number;
|
|
8
|
-
portNamespaceIndex?: null | number;
|
|
9
|
-
portNamespaceStride?: null | number;
|
|
10
8
|
runtimeNames?: string[];
|
|
11
9
|
runtimeServices?: string[];
|
|
12
10
|
targetServices?: string[];
|
|
@@ -15,4 +13,3 @@ export declare function findRuntimeStateDirs(rootDir: string, isDatabaseStateDir
|
|
|
15
13
|
export declare function findGraphDirsForService(productDir: string, serviceName: string): string[];
|
|
16
14
|
export declare function writeGraphMetadata(graphDir: string, graph: Readonly<GraphMetadataInput>): void;
|
|
17
15
|
export declare function readGraphMetadata(graphDir: string): GraphMetadata | null;
|
|
18
|
-
export declare function normalizePathSeparators(filePath: string): string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import
|
|
1
|
+
import u from"fs";import c from"path";import{isNullableString as l,isUnknownRecord as N}from"../kernel/unknown-values.js";const y="graph.json";export function findRuntimeStateDirs(e,t){const n=[],s=r=>{if(!u.existsSync(r))return;const i=u.readdirSync(r,{withFileTypes:!0});t(r)&&n.push(r);for(const a of i)a.isDirectory()&&s(c.join(r,a.name))};return s(e),n.sort((r,i)=>i.length-r.length)}export function findGraphDirsForService(e,t){const n=c.join(e,".testkit","_graphs");if(!u.existsSync(n))return[];const s=[];for(const r of u.readdirSync(n,{withFileTypes:!0})){if(!r.isDirectory())continue;const i=c.join(n,r.name),a=readGraphMetadata(i);a&&(a.runtimeServices??[]).includes(t)&&s.push(i)}return s.sort()}export function writeGraphMetadata(e,t){u.mkdirSync(e,{recursive:!0});const n={key:t.key||null,dirName:t.dirName||null,instanceCount:Number.isFinite(t.instanceCount)?t.instanceCount:null,maxConcurrentTasks:Number.isFinite(t.maxConcurrentTasks)?t.maxConcurrentTasks:null,runtimeNames:[...t.runtimeNames].sort(),runtimeServices:t.runtimeNames,targetServices:[...t.targetNames].sort()};u.writeFileSync(c.join(e,y),JSON.stringify(n,null,2))}export function readGraphMetadata(e){const t=c.join(e,y);if(!u.existsSync(t))return null;try{const n=JSON.parse(u.readFileSync(t,"utf8"));return p(n)}catch{return null}}function p(e){if(!N(e))return null;const{dirName:t,instanceCount:n,key:s,maxConcurrentTasks:r,runtimeNames:i,runtimeServices:a,targetServices:f,...S}=e,o={...S};return l(t)&&(o.dirName=t),d(n)&&(o.instanceCount=n),l(s)&&(o.key=s),d(r)&&(o.maxConcurrentTasks=r),Array.isArray(i)&&(o.runtimeNames=i.filter(m)),Array.isArray(a)&&(o.runtimeServices=a.filter(m)),Array.isArray(f)&&(o.targetServices=f.filter(m)),o}function d(e){return e===null||typeof e=="number"&&Number.isFinite(e)}function m(e){return typeof e=="string"}
|
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
import type { RuntimeSocket } from "../process/runtime-socket.js";
|
|
2
1
|
import { type NormalizedDatabaseConfig, type NormalizedRuntimeConfig } from "../config/loader/index.js";
|
|
3
|
-
import { type ExecutionEnvironmentConfig, type ExecutionEnvironmentLocalConfig, type ExecutionEnvironmentTestkitConfig, type ExecutionTemplateContextValues
|
|
2
|
+
import { type ExecutionEnvironmentConfig, type ExecutionEnvironmentLocalConfig, type ExecutionEnvironmentTestkitConfig, type ExecutionTemplateContextValues } from "../steps/index.js";
|
|
4
3
|
import { readDatabaseInfo } from "../database/state.js";
|
|
5
|
-
type RuntimeIdentifier = number | string;
|
|
6
4
|
type StringMap = ReadonlyMap<string, string>;
|
|
7
5
|
interface RuntimeLocalConfig extends ExecutionEnvironmentLocalConfig {
|
|
6
|
+
assignedPort?: number;
|
|
8
7
|
baseUrl: string;
|
|
9
8
|
cwd?: string;
|
|
10
|
-
port?: number;
|
|
11
9
|
readyUrl?: false | null | string;
|
|
12
10
|
shutdownTimeoutMs?: number;
|
|
13
11
|
start: string;
|
|
@@ -20,17 +18,10 @@ interface RuntimeServiceTestkitConfig extends ExecutionEnvironmentTestkitConfig
|
|
|
20
18
|
export interface RuntimeServiceConfig extends ExecutionEnvironmentConfig {
|
|
21
19
|
testkit: RuntimeServiceTestkitConfig;
|
|
22
20
|
}
|
|
23
|
-
export interface RuntimePortNamespace {
|
|
24
|
-
index?: unknown;
|
|
25
|
-
portOffset?: unknown;
|
|
26
|
-
stride?: unknown;
|
|
27
|
-
}
|
|
28
21
|
export interface RuntimeInstanceResolutionOptions {
|
|
29
22
|
externalEndpoints?: Readonly<Record<string, null | string | undefined>>;
|
|
30
23
|
graphDirName?: null | string;
|
|
31
|
-
|
|
32
|
-
portNamespaceStride?: number;
|
|
33
|
-
portOffset?: number;
|
|
24
|
+
ports?: ReadonlyMap<string, number>;
|
|
34
25
|
privateEndpoints?: Readonly<Record<string, null | string | undefined>>;
|
|
35
26
|
publicEndpoints?: Readonly<Record<string, null | string | undefined>>;
|
|
36
27
|
publicHost?: null | string;
|
|
@@ -79,7 +70,6 @@ export declare function resolveRuntimeInstanceConfigs<T extends RuntimeServiceCo
|
|
|
79
70
|
serviceContextUrlByService: StringMap;
|
|
80
71
|
publicHost: string | null;
|
|
81
72
|
stateDirByService: StringMap;
|
|
82
|
-
urlMappings: readonly RuntimeUrlMapping[];
|
|
83
73
|
leaseId: null;
|
|
84
74
|
leaseDir: null;
|
|
85
75
|
databaseBindingsReader: typeof readDatabaseInfo;
|
|
@@ -95,7 +85,7 @@ export declare function resolveRuntimeInstanceConfigs<T extends RuntimeServiceCo
|
|
|
95
85
|
shutdownTimeoutMs?: number;
|
|
96
86
|
start: string;
|
|
97
87
|
cwd: string | undefined;
|
|
98
|
-
|
|
88
|
+
assignedPort: number | undefined;
|
|
99
89
|
baseUrl: string | undefined;
|
|
100
90
|
readyUrl: string | undefined;
|
|
101
91
|
env: {
|
|
@@ -104,8 +94,7 @@ export declare function resolveRuntimeInstanceConfigs<T extends RuntimeServiceCo
|
|
|
104
94
|
} | undefined;
|
|
105
95
|
};
|
|
106
96
|
})[];
|
|
107
|
-
export declare function
|
|
108
|
-
export declare function resolveRuntimeConfig<T extends RuntimeServiceConfig>(config: T, runtimeId: string, runtimeLabel: string, runtimeDir: string, portMap: ReadonlyMap<string, number>, baseUrlByService: StringMap, readyUrlByService: StringMap, publicBaseUrlByService: StringMap, privateBaseUrlByService: StringMap, serviceContextUrlByService: StringMap, publicHost: null | string, stateDirByService: StringMap, urlMappings: readonly RuntimeUrlMapping[]): T & {
|
|
97
|
+
export declare function resolveRuntimeConfig<T extends RuntimeServiceConfig>(config: T, runtimeId: string, runtimeLabel: string, runtimeDir: string, portMap: ReadonlyMap<string, number>, baseUrlByService: StringMap, readyUrlByService: StringMap, publicBaseUrlByService: StringMap, privateBaseUrlByService: StringMap, serviceContextUrlByService: StringMap, publicHost: null | string, stateDirByService: StringMap): T & {
|
|
109
98
|
stateDir: string;
|
|
110
99
|
runtimeId: string;
|
|
111
100
|
runtimeLabel: string;
|
|
@@ -145,7 +134,6 @@ export declare function resolveRuntimeConfig<T extends RuntimeServiceConfig>(con
|
|
|
145
134
|
serviceContextUrlByService: StringMap;
|
|
146
135
|
publicHost: string | null;
|
|
147
136
|
stateDirByService: StringMap;
|
|
148
|
-
urlMappings: readonly RuntimeUrlMapping[];
|
|
149
137
|
leaseId: null;
|
|
150
138
|
leaseDir: null;
|
|
151
139
|
databaseBindingsReader: typeof readDatabaseInfo;
|
|
@@ -161,7 +149,7 @@ export declare function resolveRuntimeConfig<T extends RuntimeServiceConfig>(con
|
|
|
161
149
|
shutdownTimeoutMs?: number;
|
|
162
150
|
start: string;
|
|
163
151
|
cwd: string | undefined;
|
|
164
|
-
|
|
152
|
+
assignedPort: number | undefined;
|
|
165
153
|
baseUrl: string | undefined;
|
|
166
154
|
readyUrl: string | undefined;
|
|
167
155
|
env: {
|
|
@@ -173,8 +161,4 @@ export declare function resolveRuntimeConfig<T extends RuntimeServiceConfig>(con
|
|
|
173
161
|
export declare function resolveServiceStateDir(runtimeDir: string, config: Pick<ExecutionEnvironmentConfig, "name">): string;
|
|
174
162
|
export declare function resolveServicePrepareDir(runtimeDir: string, config: Pick<ExecutionEnvironmentConfig, "name">): string;
|
|
175
163
|
export declare function resolveRuntimeUrl(rawUrl: string, serviceName: string, runtimeId: string, context: RuntimeUrlResolutionContext): string;
|
|
176
|
-
export declare function rewriteUrlPort(rawUrl: string, port: number): string;
|
|
177
|
-
export declare function numericPortFromUrl(rawUrl: string): number | null;
|
|
178
|
-
export declare function socketFromUrl(rawUrl: string): RuntimeSocket | null;
|
|
179
|
-
export declare function normalizeSocketHost(hostname: string): string;
|
|
180
164
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{finalizeRuntimeDatabaseConfig as
|
|
1
|
+
import{finalizeRuntimeDatabaseConfig as w}from"../database/runtime-config.js";import S from"path";import{finalizeConfiguredInputs as E,finalizeConfiguredSteps as U}from"../steps/configured-steps.js";import"../config/loader/index.js";import{finalizeString as k,resolveTemplateString as y}from"../steps/index.js";import{readDatabaseInfo as j}from"../database/state.js";export function resolveRuntimeInstanceConfigs(e,n,s,r={}){const c=B(r.graphDirName,n),p=r.ports??new Map,i=new Map,o=new Map,u=new Map,f=new Map,d=new Map,m=new Map,v=x(r.externalEndpoints),b=x(r.publicEndpoints),l=x(r.privateEndpoints),h=x(r.serviceContextEndpoints);for(const t of e)m.set(t.name,resolveServiceStateDir(s,t));for(const t of e)t.testkit.local&&(i.set(t.name,resolveRuntimeUrl(t.testkit.local.baseUrl,t.name,n,{runtimeId:n,runtimeLabel:c,runtimeDir:s,portMap:p,baseUrlByService:i,readyUrlByService:o,stateDirByService:m})),t.testkit.local.readyUrl!==!1&&t.testkit.local.readyUrl!=null&&o.set(t.name,resolveRuntimeUrl(t.testkit.local.readyUrl,t.name,n,{runtimeId:n,runtimeLabel:c,runtimeDir:s,portMap:p,baseUrlByService:i,readyUrlByService:o,stateDirByService:m})));for(const[t,a]of Object.entries(v))i.has(t)||i.set(t,a),o.has(t)||o.set(t,a);for(const[t,a]of Object.entries(b))u.set(t,a);for(const[t,a]of Object.entries(l))f.set(t,a);for(const[t,a]of Object.entries(h))d.set(t,a);for(const[t,a]of i.entries())u.has(t)||u.set(t,C(a,r.publicHost||null)),d.has(t)||d.set(t,a),f.has(t)||f.set(t,a);return e.map(t=>resolveRuntimeConfig(t,n,c,s,p,i,o,u,f,d,r.publicHost||null,m))}function x(e){return Object.fromEntries(Object.entries(e??{}).map(([n,s])=>[n,(s??"").trim()]).filter(n=>n[1].length>0))}export function resolveRuntimeConfig(e,n,s,r,c,p,i,o,u,f,d,m){const v=resolveServiceStateDir(r,e),b=resolveServicePrepareDir(r,e),l={runtimeId:n,runtimeLabel:s,runtimeDir:r,productDir:e.productDir,serviceName:e.name,serviceCwd:e.testkit.local?.cwd||".",serviceStateDir:v,prepareDir:b,portMap:c,baseUrlByService:p,readyUrlByService:i,publicBaseUrlByService:o,privateBaseUrlByService:u,serviceContextUrlByService:f,publicHost:d,stateDirByService:m,leaseId:null,leaseDir:null,databaseBindingsReader:j},h=w(e.testkit.database,l),t={...e.testkit.runtime,prepare:R(e.testkit.runtime.prepare,l)},a=e.testkit.local?{...e.testkit.local,start:k(e.testkit.local.start,l),cwd:e.testkit.local.cwd!==void 0?k(e.testkit.local.cwd,l):e.testkit.local.cwd,assignedPort:c.get(e.name),baseUrl:p.get(e.name),readyUrl:i.get(e.name),env:{...e.testkit.local.env||{}}}:void 0;return{...e,stateDir:v,runtimeId:n,runtimeLabel:s,testkit:{...e.testkit,database:h,prepareDir:b,templateContext:l,runtime:t,local:a}}}export function resolveServiceStateDir(e,n){return S.join(e,"services",n.name)}export function resolveServicePrepareDir(e,n){return S.join(resolveServiceStateDir(e,n),"prepared")}export function resolveRuntimeUrl(e,n,s,r){return y(e,{...r,runtimeId:s,serviceName:n})}function C(e,n){if(!n)return e;try{const s=new URL(e);s.hostname=n;let r=s.toString();return!e.endsWith("/")&&s.pathname==="/"&&r.endsWith("/")&&(r=r.slice(0,-1)),r}catch{return e}}function R(e,n){return e?{inputs:E(e.inputs,s=>k(s,n)),steps:U(e.steps,s=>k(s,n))}:{inputs:[],steps:[]}}function B(e,n){return e?`${e}/${n}`:n}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export function normalizeArtifactArchiveConfig(r){const n=e(r,"artifacts"),i=e(n?.retention,"artifacts.retention"),t=e(n?.publication,"artifacts.publication");return{retention:{maxRuns:
|
|
1
|
+
import{isUnknownArray as o,isUnknownRecord as s}from"../kernel/unknown-values.js";export function normalizeArtifactArchiveConfig(r){const n=e(r,"artifacts"),i=e(n?.retention,"artifacts.retention"),t=e(n?.publication,"artifacts.publication");return{retention:{maxRuns:a(i?.maxRuns,"artifacts.retention.maxRuns"),maxAgeDays:u(i?.maxAgeDays,"artifacts.retention.maxAgeDays"),preserveSuites:f(i?.preserveSuites,"artifacts.retention.preserveSuites")},publication:t?{directory:m(t.directory,"artifacts.publication.directory"),required:c(t.required,"artifacts.publication.required")}:null}}function a(r,n){if(r==null)return null;if(typeof r!="number"||!Number.isInteger(r)||r<=0)throw new Error(`${n} must be a positive integer`);return r}function u(r,n){if(r==null)return null;if(typeof r!="number"||!Number.isFinite(r)||r<=0)throw new Error(`${n} must be a positive number`);return r}function f(r,n){if(r==null)return[];if(!o(r))throw new Error(`${n} must be an array of non-empty strings`);const i=r.map(t=>{if(typeof t!="string"||t.trim().length===0)throw new Error(`${n} must be an array of non-empty strings`);return t.trim()});return[...new Set(i)].sort()}function m(r,n){if(typeof r!="string"||r.trim().length===0)throw new Error(`${n} is required`);return r.trim()}function c(r,n){if(r===void 0)return!1;if(typeof r!="boolean")throw new Error(`${n} must be a boolean`);return r}function e(r,n){if(r!=null){if(!s(r))throw new Error(`${n} must be an object`);return r}}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
export { requireObjectRecord,
|
|
1
|
+
export { requireObjectRecord, requireStringArray as normalizeStringArray } from "./read-fields.js";
|
|
2
2
|
export type UnknownRecord = Record<string, unknown>;
|
|
3
|
-
export declare function isUnknownArray(value: unknown): value is unknown[];
|
|
4
3
|
export declare function readOptionalProperty(value: UnknownRecord | undefined, key: string): unknown;
|
|
5
4
|
export declare function normalizeOptionalNonEmptyString(value: unknown, label: string): string | null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export{requireObjectRecord,
|
|
1
|
+
export{requireObjectRecord,requireStringArray as normalizeStringArray}from"./read-fields.js";export function readOptionalProperty(r,n){return r?.[n]}export function normalizeOptionalNonEmptyString(r,n){if(r==null||r==="")return null;if(typeof r!="string")throw new Error(`${n} must be a string or null`);return r}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{isPathWithin as F}from"../process/files/paths.js";import{
|
|
2
|
-
`,p=
|
|
1
|
+
import{isPathWithin as F}from"../process/files/paths.js";import{isUnknownArray as P,isUnknownRecord as _}from"../kernel/unknown-values.js";import{readRunDocument as C}from"./read-run.js";import l from"node:fs";import o from"node:path";import"../engines/api/index.js";import"../domain/identifiers.js";import{normalizeArtifactArchiveConfig as M}from"./artifact-config.js";import{applyRunBundleRetention as B,publishRunBundle as $,sealRunBundle as U}from"./bundles.js";import{RETENTION_DECISION_SCHEMA_VERSION as J}from"./contracts.js";import{applyTimingUpdates as q,createEmptyTimings as I,normalizeTimings as V}from"./timing.js";import{normalizeOptionalNonEmptyString as E,normalizeStringArray as K,readOptionalProperty as x,requireObjectRecord as X}from"./artifact-validation.js";import{errorMessage as G,serializeJson as k,writeJsonFileAtomicWithRetry as j}from"../process/files/json-file.js";import{copyRegularFileSecurely as W}from"./secure-file-copy.js";import{hasErrorCode as N}from"../kernel/error-serialization.js";import{posixPath as R}from"../process/files/paths.js";const z="timings.json",A="artifacts",H="logs",Q="setup",T="live.json",Y=1e4,O=String;export function writeRunArtifact(t,e,r={},n={}){const i=e,s=o.join(t,".testkit","results");l.mkdirSync(s,{recursive:!0});const c=i.archive,a=r.archiveConfig??M(x(c,"request")||{}),u=r.declaredEnvironmentKeys??K(x(c,"declaredEnvironmentKeys")||[],"run artifact declaredEnvironmentKeys"),m=n.sealRunBundle??U,p=n.publishRunBundle??$,d=n.applyRunBundleRetention??B,y=n.writeLatestProjection??j,f=m(t,i,{env:r.env,archiveConfig:a,declaredEnvironmentKeys:u}),h=Z(t,f.archive.runId,a.publication,p),S=tt(t,f.archive.runId,a.retention,d),v=i.finalizationFailure,b={...f,...v===void 0?{}:{finalizationFailure:v},run:h.status==="failed"&&h.required===!0||f.archive.cleanup?.status==="failed"||v!==void 0&&f.run.status!=="interrupted"?{...f.run,status:"failed"}:f.run,archive:{...f.archive,publication:h,retention:S}};return et(s,b,y)}function Z(t,e,r,n){try{return n(t,e,r)}catch(i){return{bytesUploaded:0,configured:r!==null,error:w(i),recordedAt:new Date().toISOString(),required:r?.required??!1,runId:e,schemaVersion:1,status:"failed",target:r===null?null:o.resolve(t,r.directory)}}}function tt(t,e,r,n){try{return n(t,e,r)}catch(i){return{currentRunId:e,decidedAt:new Date().toISOString(),error:w(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:J,status:"failed"}}}function et(t,e,r){const n=o.join(t,"latest.json"),i={path:R(o.relative(o.dirname(t),n)),status:"complete"},s={...e,archive:{...e.archive,latest:i}};try{return r(n,s),s}catch(c){return{...s,archive:{...s.archive,latest:{...i,error:w(c),status:"failed"}}}}}function w(t){try{return G(t)}catch{return"Unknown post-seal archive operation failure"}}export function writeLiveRunArtifact(t,e){const r=e,n=o.join(t,".testkit","results");l.mkdirSync(n,{recursive:!0}),j(o.join(n,T),r)}export function finalizeLiveRunArtifact(t,e={}){const r=it(e),n=o.join(t,".testkit","results",T),i=lt(n);if(i===null)return!1;const s=rt(i,r);return s?(writeRunArtifact(t,s),!0):!1}function rt(t,e){const r=C(t,"live run artifact"),n=r.run.id||null;if(e.runId&&n&&n!==e.runId)return null;const i=st(e.finishedAt),s=new Date(i).toISOString(),c=ot(r.run.startedAt),a=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:a,interruptReason:e.reason},timings:nt(r.timings,s,e.reason)}}function nt(t,e,r){if(t==null)return t;const n=X(t,"live run artifact.timings"),i=n.operations;if(i==null)return n;if(!P(i))throw new Error("live run artifact.timings.operations must be an array");return{...n,operations:i.map(s=>!_(s)||s.status!=="running"?s:{...s,status:"cancelled",finishedAt:E(s.finishedAt,"live run operation.finishedAt")??e,message:E(s.message,"live run operation.message")??r})}}function it(t){return{runId:t.runId??null,reason:t.reason??"run interrupted",status:t.status??"interrupted",finishedAt:t.finishedAt??Date.now()}}function st(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 ot(t){if(typeof t!="string")return null;const e=Date.parse(t);return Number.isFinite(e)?e:null}export function resetResultArtifacts(t){l.rmSync(o.join(t,".testkit","results",A),{recursive:!0,force:!0}),l.rmSync(o.join(t,".testkit","results",H),{recursive:!0,force:!0}),l.rmSync(o.join(t,".testkit","results",Q),{recursive:!0,force:!0}),l.rmSync(o.join(t,".testkit","results",T),{force:!0})}export function persistTaskArtifacts(t,e,r){if(r.length===0)return[];const n=g(e.serviceName||"service"),i=L(t,n);return r.map((s,c)=>{const a=at(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:a.name,kind:a.kind,summary:a.summary,contentType:a.contentType,emittedAt:a.emittedAt,data:a.data},m=`${k(u,"runtime artifact payload")}
|
|
2
|
+
`,p=D(i,e.id,c,a.name,".json",y=>{l.writeFileSync(y,m,{flag:"wx"})}),d=o.join(".testkit","results",A,n,p);return{name:u.name,kind:u.kind,summary:u.summary,contentType:u.contentType,path:R(d)}})}export function persistTaskFileArtifacts(t,e,r,n={}){if(r.length===0)return[];const i=g(e.serviceName||"service"),s=L(t,i);return r.map((c,a)=>{const u=ut(c),m=o.resolve(u.path),p=ft(m,u.contentType),d=[],y=D(s,e.id,a,u.name,p,S=>{d.push(W(m,S,{...n.maxBytes===void 0?{}:{maxBytes:n.maxBytes},...n.sourceRoot===void 0?{}:{sourceRoot:n.sourceRoot},...n.sizeLimitDescription===void 0?{}:{sizeLimitDescription:n.sizeLimitDescription}}))}),f=d.at(0);if(f===void 0)throw new Error("File artifact copy completed without a result");n.copyResults?.push(f);const h=o.join(".testkit","results",A,i,y);return{name:u.name,kind:u.kind,summary:u.summary,contentType:u.contentType??dt(m),path:R(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||mt(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=o.join(t,".testkit",z);if(!l.existsSync(e))return I();try{const r=JSON.parse(l.readFileSync(e,"utf8"));return V(r)}catch{return I()}}export function saveTimings(t,e,r){if(r.length===0)return;const n=q(e,r),i=o.join(t,".testkit");l.mkdirSync(i,{recursive:!0}),l.writeFileSync(o.join(i,z),k(n,"timings"))}function at(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 ut(t){return{path:t.path,name:t.name||"artifact",kind:t.kind||null,summary:t.summary||null,contentType:t.contentType||null}}function ct(t,e,r,n,i=0){const s=i===0?"":`-${(i+1).toString().padStart(2,"0")}`;return`task-${g(t)}-${(e+1).toString().padStart(2,"0")}-${g(r)}${s}${n}`}function D(t,e,r,n,i,s){for(let c=0;c<Y;c+=1){const a=ct(e,r,n,i,c),u=o.join(t,a);try{return s(u),a}catch(m){if(N(m,"EEXIST"))continue;throw l.rmSync(u,{force:!0}),m}}throw new Error(`Could not allocate a unique artifact path for task ${O(e)} and artifact ${n}`)}function L(t,e){const r=o.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 c of[".testkit","results",A,e]){s=o.join(s,c);try{l.mkdirSync(s)}catch(m){if(!N(m,"EEXIST"))throw m}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(!F(i,u))throw new Error(`Artifact output directory ${JSON.stringify(s)} escapes productDir`)}return s}function g(t){return O(t).trim().toLowerCase().replace(/[^a-z0-9._-]+/g,"-").replace(/^-+|-+$/g,"")||"artifact"}function lt(t){try{return JSON.parse(l.readFileSync(t,"utf8"))}catch{return null}}function mt(t){const e=t.split(/\r?\n/u).map(r=>r.trim()).find(Boolean);return e?e.slice(0,120):"captured task output"}function ft(t,e){const r=o.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 dt(t){const e=o.extname(t).toLowerCase();return e===".md"||e===".markdown"?"text/markdown":e===".txt"||e===".log"?"text/plain":e===".json"?"application/json":"application/octet-stream"}
|
package/dist/evidence/bundles.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{recordWork as H}from"../kernel/work-metrics.js";import{optionalString as A}from"../kernel/unknown-values.js";import{readRunProvenance as ue}from"./read-provenance.js";import{parseRunBundleArtifact as le,parseRunBundleManifest as J,readRunBundleArtifact as fe}from"./read-bundle.js";import{readRunDocument as de}from"./read-run.js";import me from"crypto";import c from"fs";import u from"path";import{redactSensitiveText as Y}from"../kernel/redaction.js";import{BUNDLE_SCHEMA_VERSION as G,OBJECTS_DIRNAME as Q,PROVENANCE_SCHEMA_VERSION as pe,RETENTION_DECISION_SCHEMA_VERSION as he,RUNS_DIRNAME as ge}from"./contracts.js";import{bundleRelativePath as x,errorMessage as k,hashFile as F,isObjectRecord as $,normalizePath as X,normalizeRunId as w,objectRecords as Z,optionalStringArray as z,optionalUnknownArray as ye,readJsonFile as P,requirePublicationPathOutsideResults as M,requireRunId as ve,runIncludesFile as Se,resolveManifestEntryPath as ee,resolveRunBundleDirectory as O,resolveRunsRoot as te,resultsRoot as U,sanitizeSegment as Re,sha256 as C,stableJson as q,treeBytes as ne,walkFiles as re,writeBufferAtomic as je,writeJson as N}from"./storage.js";import{resolveCiProvenance as be}from"./ci-provenance.js";const W=["artifacts","logs","setup"],Ie=/(?:PASSWORD|TOKEN|SECRET|PRIVATE_KEY|AUTH|COOKIE|SESSION|API_KEY)/iu,we=["package.json","package-lock.json","npm-shrinkwrap.json","pnpm-lock.yaml","yarn.lock","testkit.config.ts","testkit.config.mts","testkit.config.js","testkit.config.mjs"];export function sealRunBundle(e,n,t={},s={}){const r=n,o=ve(r),i=te(e,!0),a=u.dirname(i),f=u.join(i,o),m=s.cleanupActiveRunResults??ze,p=loadVerifiedRunBundleArtifactIfExists(e,o);if(p!==null)return{...p,archive:{...p.archive,cleanup:m(a)}};const d=u.join(i,`.${o}.${String(process.pid)}.${String(Date.now())}.${me.randomBytes(4).toString("hex")}.tmp`);c.mkdirSync(d,{recursive:!0});try{const R=Te(t.env??process.env),g=Ee({productDir:e,resultsDir:a,runId:o,tempDir:d,sensitiveValues:R}),h=Be(r,o,R),y=de(h.artifact,"run artifact");g.redaction.structuredReplacements=h.replacements,g.redaction.replacements+=h.replacements;const v=$e(e,y,o,{...t,redaction:g.redaction}),j={schemaVersion:G,runId:o,status:"complete",canonicalRunPath:x(o,"run.json"),manifestPath:x(o,"manifest.json"),provenancePath:x(o,"provenance.json"),integrity:"sha256",publication:{configured:!!t.archiveConfig?.publication,status:t.archiveConfig?.publication?"pending":"not_configured"},redaction:g.redaction};y.archive=j,N(u.join(d,"run.json"),y),N(u.join(d,"provenance.json"),v);const b=xe(d),D=C(Buffer.from(q(b))),E={schemaVersion:G,runId:o,createdAt:new Date().toISOString(),algorithm:"sha256",files:b,storage:{fileCount:b.length,logicalBytes:b.reduce((T,_)=>T+_.bytes,0),objectBytesWritten:g.objectBytesWritten,deduplicatedBytes:g.deduplicatedBytes},redaction:g.redaction,digest:D};return N(u.join(d,"manifest.json"),E),c.renameSync(d,f),{...y,archive:{...j,cleanup:m(a),manifestDigest:E.digest,storage:E.storage}}}catch(R){try{c.rmSync(d,{recursive:!0,force:!0})}catch{}try{Ce(e)}catch{}throw R}}export function loadVerifiedRunBundleArtifactIfExists(e,n){const t=w(n),s=O(e,t);if(!c.existsSync(s))return null;const r=verifyRunBundle(e,t);if(!r.ok)throw new Error(`Immutable run bundle ${t} already exists but failed integrity verification: ${r.errors.join("; ")}`);return loadRunBundleArtifact(e,t)}export function loadRunBundleArtifact(e,n){return se(e,n).artifact}function se(e,n){const t=w(n),s=fe(e,t),r=loadRunBundleManifest(e,t);return{manifest:r,artifact:{...s,archive:{...s.archive,manifestDigest:r.digest,storage:r.storage}}}}export function loadRunBundleManifest(e,n){const t=w(n),s=O(e,t),r=u.join(s,"manifest.json");if(!c.existsSync(r))throw new Error(`No immutable Testkit run manifest found for ${t}`);return J(P(r),r,t)}export function listRunBundles(e,n={}){return ie(e,n).map(({artifact:t,manifest:s,...r})=>r)}function ie(e,n={}){const t=te(e,!1);if(!c.existsSync(t))return[];const s=c.readdirSync(t,{withFileTypes:!0});return H("artifacts.runDirectoriesRead",1),H("artifacts.runEntriesVisited",s.length),s.filter(o=>o.isDirectory()&&!o.name.startsWith(".")).map(o=>{try{const{artifact:i,manifest:a}=se(e,o.name);return n.file&&!Se(i,n.file)?null:{artifact:i,manifest:a,runId:o.name,status:i.run.status,startedAt:i.run.startedAt,finishedAt:i.run.finishedAt??null,manifestDigest:i.archive.manifestDigest??null,canonicalRunPath:i.archive.canonicalRunPath}}catch{return null}}).filter(o=>o!==null).sort((o,i)=>(i.finishedAt??i.startedAt??"").localeCompare(o.finishedAt??o.startedAt??""))}export function verifyRunBundle(e,n){const t=w(n);try{return V(O(e,t),t)}catch(s){return{ok:!1,runId:t,errors:[k(s)],checkedFiles:0}}}export function publishRunBundle(e,n,t,s={}){const r=w(n),o=s.writePublicationReceipt??Pe;if(!t)return o(e,r,{status:"not_configured",required:!1,target:null,bytesUploaded:0});const i=u.resolve(e,t.directory);try{M(e,i)}catch(d){return o(e,r,{status:"failed",required:t.required,target:i,error:k(d),bytesUploaded:0})}const a=Re(u.basename(e)),f=u.join(i,a),m=u.join(f,r),p=u.join(f,`.${r}.${String(process.pid)}.${String(Date.now())}.tmp`);try{const d=O(e,r);if(c.mkdirSync(f,{recursive:!0}),M(e,f),c.existsSync(m)){const h=c.lstatSync(m);if(h.isSymbolicLink()||!h.isDirectory())throw new Error("Publication target must be a regular bundle directory");M(e,m);const y=V(d,r);if(!y.ok)throw new Error(`Source run bundle failed integrity verification: ${y.errors.join("; ")}`);const v=V(m,r);if(!v.ok)throw new Error(`Publication target failed integrity verification: ${v.errors.join("; ")}`);if(y.manifestDigest!==v.manifestDigest)throw new Error("Publication target already exists with a different manifest digest");return o(e,r,{status:"complete",required:t.required,target:m,manifestDigest:y.manifestDigest,reused:!0,bundleBytes:ne(d),bytesUploaded:0})}const R=ne(d);c.cpSync(d,p,{recursive:!0,preserveTimestamps:!0});const g=V(p,r);if(!g.ok)throw new Error(g.errors.join("; "));return c.renameSync(p,m),o(e,r,{status:"complete",required:t.required,target:m,manifestDigest:g.manifestDigest,reused:!1,bundleBytes:R,bytesUploaded:R})}catch(d){return c.rmSync(p,{recursive:!0,force:!0}),o(e,r,{status:"failed",required:t.required,target:m,error:k(d),bytesUploaded:0})}}export function applyRunBundleRetention(e,n,t={},s={}){const r=w(n),o=s.removeObject??ce,i=s.removeRunBundle??Ne,a=t.maxRuns??null,f=t.maxAgeDays??null,m=new Set(t.preserveSuites||[]),p=ie(e),d=new Set([r]);for(const l of p)Oe(l.artifact,m)&&d.add(l.runId);const R=new Set(a?p.slice(0,a).map(l=>l.runId):p.map(l=>l.runId)),g=f==null?null:Date.now()-f*24*60*60*1e3,h=[];for(const l of p){if(d.has(l.runId))continue;const S=g!=null&&Date.parse(l.finishedAt||l.startedAt||"")<g,I=!R.has(l.runId);if(!S&&!I)continue;const B=[];S&&B.push("max_age"),I&&B.push("max_runs"),h.push({path:u.join(U(e),ge,l.runId),reasons:B,runId:l.runId})}const y=new Map(p.map(l=>[l.runId,l.manifest.files.filter(S=>S.path.startsWith("files/")).map(S=>S.sha256)])),v=oe(e),j=[],b=[];for(const l of h){let S=null;try{i(l.path)}catch(B){S=L(B)}const I=!c.existsSync(l.path);!I&&S===null&&(S="Run bundle still exists after removal completed"),I&&j.push({reasons:l.reasons,runId:l.runId}),S!==null&&b.push({...l,error:S,removed:I})}const D=new Set(j.map(l=>l.runId)),E=new Set(p.filter(l=>!D.has(l.runId)).flatMap(l=>y.get(l.runId)??[])),T=ae(v,E,o),_=new Date().toISOString();return Ve({currentRunId:r,decidedAt:_,garbageCollection:T,policy:{maxRuns:a,maxAgeDays:f,preserveSuites:[...m].sort()},protectedRunIds:[...d].sort(),removed:j,removalErrors:b})}function Ee({resultsDir:e,tempDir:n,sensitiveValues:t}){let s=0,r=0,o=0,i=0,a=0;const f=u.join(e,Q,"sha256");c.mkdirSync(f,{recursive:!0});for(const m of W){const p=u.join(e,m);if(c.existsSync(p))for(const d of re(p)){const R=u.relative(p,d),g=c.readFileSync(d),h=De(g,d,t);h.scanned&&(o+=1),h.replacements>0&&(i+=1,a+=h.replacements);const y=C(h.buffer),v=u.join(f,y);c.existsSync(v)?r+=h.buffer.length:(je(v,h.buffer),c.chmodSync(v,292),s+=h.buffer.length);const j=u.join(n,"files",m,R);c.mkdirSync(u.dirname(j),{recursive:!0});try{c.linkSync(v,j)}catch{c.copyFileSync(v,j)}}}return{objectBytesWritten:s,deduplicatedBytes:r,redaction:{policyVersion:1,status:"applied",textualFilesScanned:o,filesRedacted:i,replacements:a,sensitiveEnvironmentKeys:t.map(m=>m.key).sort(),valuesStored:!1}}}function Be(e,n,t){const s=new Map(W.map(o=>[`.testkit/results/${o}/`,`${x(n,`files/${o}`)}/`])),r=K(e,s);return ke(Ae(r),t)}function Ae(e){if(!$(e))return e;const n={...e};if($(n.product)){const t={...n.product};delete t.directory,n.product=t}if($(n.git)){const t={...n.git};delete t.remoteUrl,delete t.repoRoot,delete t.worktreePath,n.git=t}return delete n.host,n}function K(e,n){if(Array.isArray(e))return e.map(s=>K(s,n));if($(e))return Object.fromEntries(Object.entries(e).map(([s,r])=>[s,K(r,n)]));if(typeof e!="string")return e;let t=e;for(const[s,r]of n)t.startsWith(s)&&(t=`${r}${t.slice(s.length)}`);return t}function ke(e,n){let t=0;function s(r){if(Array.isArray(r))return r.map(s);if($(r))return Object.fromEntries(Object.entries(r).map(([i,a])=>[i,s(a)]));if(typeof r!="string")return r;let o=Y(r);o!==r&&(t+=1);for(const i of n){if(!o.includes(i.value))continue;const a=o.split(i.value);t+=a.length-1,o=a.join("<redacted>")}return o}return{artifact:s(e),replacements:t}}function $e(e,n,t,s){const r=z(n.run.fileNames),o=[...new Set([...we,...r])].map(i=>Fe(e,i)).filter(i=>i!==null);return{schemaVersion:pe,runId:t,ci:be(s.env??process.env),source:{git:n.git??null,files:o},runtime:{controller:{testkitVersion:A(n.run.testkitVersion),node:process.versions.node,platform:process.platform,architecture:process.arch},runtimeStats:ye(n.run.runtimeStats)},scenario:{types:z(n.run.types),suiteSelectors:z(n.run.suiteSelectors),fileNames:r,serviceFilter:A(n.run.serviceFilter),planningDigest:C(Buffer.from(q(n.planning??null))),declaredEnvironmentKeys:[...new Set(s.declaredEnvironmentKeys??[])].sort()},redaction:s.redaction}}function xe(e){return re(e).map(n=>({path:X(u.relative(e,n)),bytes:c.statSync(n).size,sha256:F(n)})).sort((n,t)=>n.path.localeCompare(t.path))}function V(e,n){const t=u.join(e,"manifest.json"),s=J(P(t),t,n);le(P(u.join(e,"run.json")),u.join(e,"run.json"),n);const r=[];for(const i of s.files){let a;try{a=ee(e,i.path)}catch(m){r.push(k(m));continue}if(!c.existsSync(a)){r.push(`missing ${i.path}`);continue}const f=c.lstatSync(a);if(f.isSymbolicLink()){r.push(`symbolic link not allowed ${i.path}`);continue}if(!f.isFile()){r.push(`not a regular file ${i.path}`);continue}f.size!==i.bytes&&r.push(`size mismatch ${i.path}`),F(a)!==i.sha256&&r.push(`checksum mismatch ${i.path}`)}C(Buffer.from(q(s.files)))!==s.digest&&r.push("manifest digest mismatch"),s.storage.fileCount!==s.files.length&&r.push("manifest file count mismatch");for(const i of["run.json","provenance.json"])s.files.some(a=>a.path===i)||r.push(`manifest must include ${i}`);if(r.length===0){const i=ee(e,"provenance.json");ue(P(i),i,n)}return{runId:n,checkedFiles:s.files.length,storage:s.storage,ok:r.length===0,errors:r,manifestDigest:s.digest}}function Pe(e,n,t){const s={schemaVersion:1,runId:n,recordedAt:new Date().toISOString(),...t};return N(u.join(U(e),"publications",`${n}.json`),s),s}function Oe(e,n){return n.size===0?!1:Z(e.services).some(t=>Z(t.suites).some(s=>{const r=A(t.name)??"",o=A(s.name)??"",i=A(s.testType)??"";return[o,`${i}:${o}`,`${r}/${i}:${o}`].some(f=>n.has(f))}))}function Ce(e){const n=new Set;for(const t of listRunBundles(e)){const s=loadRunBundleManifest(e,t.runId);for(const r of s.files)r.path.startsWith("files/")&&n.add(r.sha256)}return ae(oe(e),n,ce)}function oe(e){const n=u.join(U(e),Q,"sha256");return c.existsSync(n)?c.readdirSync(n,{withFileTypes:!0}).filter(t=>t.isFile()).map(t=>{const s=u.join(n,t.name);return{bytes:c.statSync(s).size,digest:t.name,path:s}}):[]}function ae(e,n,t){const s=[];let r=0,o=0;for(const i of e){if(n.has(i.digest))continue;let a=null;try{t(i.path)}catch(m){a=L(m)}!c.existsSync(i.path)?(r+=1,o+=i.bytes):a===null&&(a="Object still exists after removal completed"),a!==null&&s.push({error:a,path:i.path})}return{failures:s,reclaimedBytes:o,removedObjects:r,status:s.length===0?"complete":"failed"}}function Ne(e){c.rmSync(e,{recursive:!0,force:!0})}function ce(e){c.rmSync(e,{force:!0})}function Ve({currentRunId:e,decidedAt:n,garbageCollection:t,policy:s,protectedRunIds:r,removed:o,removalErrors:i}){const a=[...i.map(f=>`remove ${f.runId}: ${f.error}`),...t.failures.map(f=>`garbage collect ${f.path}: ${f.error}`)];return{currentRunId:e,decidedAt:n,garbageCollection:t,policy:s,protectedRunIds:r,removed:o,removalErrors:i,schemaVersion:he,status:a.length===0?"complete":"failed",...a.length===0?{}:{error:a.join("; ")}}}function De(e,n,t){if(!_e(e,n)){for(const i of t)if(e.includes(Buffer.from(i.value)))throw new Error(`Refusing to archive binary file containing sensitive environment value: ${n}`);return{buffer:e,scanned:!1,replacements:0}}const s=e.toString("utf8");let r=Y(s),o=r===s?0:1;for(const i of t){if(!r.includes(i.value))continue;const a=r.split(i.value);o+=a.length-1,r=a.join("<redacted>")}return{buffer:Buffer.from(r),scanned:!0,replacements:o}}function Te(e){const n=[];for(const[t,s]of Object.entries(e))Ie.test(t)&&typeof s=="string"&&s.length>=8&&n.push({key:t,value:s});return n}function _e(e,n){if(e.includes(0))return!1;const t=u.extname(n).toLowerCase();return![".zip",".gz",".png",".jpg",".jpeg",".webm",".pdf"].includes(t)}function Fe(e,n){const t=u.resolve(e,n);return!t.startsWith(`${u.resolve(e)}${u.sep}`)||!c.existsSync(t)||!c.statSync(t).isFile()?null:{path:X(u.relative(e,t)),bytes:c.statSync(t).size,sha256:F(t)}}function ze(e){const n=[];for(const t of[...W.map(s=>u.join(e,s)),u.join(e,"live.json")])try{c.rmSync(t,{recursive:!0,force:!0})}catch(s){n.push(`${t}: ${L(s)}`)}return n.length===0?{status:"complete"}:{error:n.join("; "),status:"failed"}}function L(e){try{return k(e)}catch{return"Unknown post-seal archive operation failure"}}
|
|
1
|
+
import{recordWork as H}from"../kernel/work-metrics.js";import{isUnknownRecord as A,optionalString as k}from"../kernel/unknown-values.js";import{posixPath as J}from"../process/files/paths.js";import{readRunProvenance as ue}from"./read-provenance.js";import{parseRunBundleArtifact as le,parseRunBundleManifest as Y,readRunBundleArtifact as fe}from"./read-bundle.js";import{readRunDocument as de}from"./read-run.js";import me from"crypto";import c from"fs";import u from"path";import{redactSensitiveText as G}from"../kernel/redaction.js";import{BUNDLE_SCHEMA_VERSION as Q,OBJECTS_DIRNAME as X,PROVENANCE_SCHEMA_VERSION as pe,RETENTION_DECISION_SCHEMA_VERSION as he,RUNS_DIRNAME as ge}from"./contracts.js";import{bundleRelativePath as $,errorMessage as x,hashFile as F,normalizeRunId as I,objectRecords as Z,optionalStringArray as z,optionalUnknownArray as ye,readJsonFile as P,requirePublicationPathOutsideResults as M,requireRunId as ve,runIncludesFile as Se,resolveManifestEntryPath as ee,resolveRunBundleDirectory as O,resolveRunsRoot as te,resultsRoot as U,sanitizeSegment as Re,sha256 as C,stableJson as q,treeBytes as ne,walkFiles as re,writeBufferAtomic as je,writeJson as N}from"./storage.js";import{resolveCiProvenance as be}from"./ci-provenance.js";const W=["artifacts","logs","setup"],we=/(?:PASSWORD|TOKEN|SECRET|PRIVATE_KEY|AUTH|COOKIE|SESSION|API_KEY)/iu,Ie=["package.json","package-lock.json","npm-shrinkwrap.json","pnpm-lock.yaml","yarn.lock","testkit.config.ts","testkit.config.mts","testkit.config.js","testkit.config.mjs"];export function sealRunBundle(e,n,t={},s={}){const r=n,o=ve(r),i=te(e,!0),a=u.dirname(i),f=u.join(i,o),m=s.cleanupActiveRunResults??ze,p=loadVerifiedRunBundleArtifactIfExists(e,o);if(p!==null)return{...p,archive:{...p.archive,cleanup:m(a)}};const d=u.join(i,`.${o}.${String(process.pid)}.${String(Date.now())}.${me.randomBytes(4).toString("hex")}.tmp`);c.mkdirSync(d,{recursive:!0});try{const R=Te(t.env??process.env),g=Ee({productDir:e,resultsDir:a,runId:o,tempDir:d,sensitiveValues:R}),h=Be(r,o,R),y=de(h.artifact,"run artifact");g.redaction.structuredReplacements=h.replacements,g.redaction.replacements+=h.replacements;const v=xe(e,y,o,{...t,redaction:g.redaction}),j={schemaVersion:Q,runId:o,status:"complete",canonicalRunPath:$(o,"run.json"),manifestPath:$(o,"manifest.json"),provenancePath:$(o,"provenance.json"),integrity:"sha256",publication:{configured:!!t.archiveConfig?.publication,status:t.archiveConfig?.publication?"pending":"not_configured"},redaction:g.redaction};y.archive=j,N(u.join(d,"run.json"),y),N(u.join(d,"provenance.json"),v);const b=$e(d),D=C(Buffer.from(q(b))),E={schemaVersion:Q,runId:o,createdAt:new Date().toISOString(),algorithm:"sha256",files:b,storage:{fileCount:b.length,logicalBytes:b.reduce((T,_)=>T+_.bytes,0),objectBytesWritten:g.objectBytesWritten,deduplicatedBytes:g.deduplicatedBytes},redaction:g.redaction,digest:D};return N(u.join(d,"manifest.json"),E),c.renameSync(d,f),{...y,archive:{...j,cleanup:m(a),manifestDigest:E.digest,storage:E.storage}}}catch(R){try{c.rmSync(d,{recursive:!0,force:!0})}catch{}try{Ce(e)}catch{}throw R}}export function loadVerifiedRunBundleArtifactIfExists(e,n){const t=I(n),s=O(e,t);if(!c.existsSync(s))return null;const r=verifyRunBundle(e,t);if(!r.ok)throw new Error(`Immutable run bundle ${t} already exists but failed integrity verification: ${r.errors.join("; ")}`);return loadRunBundleArtifact(e,t)}export function loadRunBundleArtifact(e,n){return se(e,n).artifact}function se(e,n){const t=I(n),s=fe(e,t),r=loadRunBundleManifest(e,t);return{manifest:r,artifact:{...s,archive:{...s.archive,manifestDigest:r.digest,storage:r.storage}}}}export function loadRunBundleManifest(e,n){const t=I(n),s=O(e,t),r=u.join(s,"manifest.json");if(!c.existsSync(r))throw new Error(`No immutable Testkit run manifest found for ${t}`);return Y(P(r),r,t)}export function listRunBundles(e,n={}){return ie(e,n).map(({artifact:t,manifest:s,...r})=>r)}function ie(e,n={}){const t=te(e,!1);if(!c.existsSync(t))return[];const s=c.readdirSync(t,{withFileTypes:!0});return H("artifacts.runDirectoriesRead",1),H("artifacts.runEntriesVisited",s.length),s.filter(o=>o.isDirectory()&&!o.name.startsWith(".")).map(o=>{try{const{artifact:i,manifest:a}=se(e,o.name);return n.file&&!Se(i,n.file)?null:{artifact:i,manifest:a,runId:o.name,status:i.run.status,startedAt:i.run.startedAt,finishedAt:i.run.finishedAt??null,manifestDigest:i.archive.manifestDigest??null,canonicalRunPath:i.archive.canonicalRunPath}}catch{return null}}).filter(o=>o!==null).sort((o,i)=>(i.finishedAt??i.startedAt??"").localeCompare(o.finishedAt??o.startedAt??""))}export function verifyRunBundle(e,n){const t=I(n);try{return V(O(e,t),t)}catch(s){return{ok:!1,runId:t,errors:[x(s)],checkedFiles:0}}}export function publishRunBundle(e,n,t,s={}){const r=I(n),o=s.writePublicationReceipt??Pe;if(!t)return o(e,r,{status:"not_configured",required:!1,target:null,bytesUploaded:0});const i=u.resolve(e,t.directory);try{M(e,i)}catch(d){return o(e,r,{status:"failed",required:t.required,target:i,error:x(d),bytesUploaded:0})}const a=Re(u.basename(e)),f=u.join(i,a),m=u.join(f,r),p=u.join(f,`.${r}.${String(process.pid)}.${String(Date.now())}.tmp`);try{const d=O(e,r);if(c.mkdirSync(f,{recursive:!0}),M(e,f),c.existsSync(m)){const h=c.lstatSync(m);if(h.isSymbolicLink()||!h.isDirectory())throw new Error("Publication target must be a regular bundle directory");M(e,m);const y=V(d,r);if(!y.ok)throw new Error(`Source run bundle failed integrity verification: ${y.errors.join("; ")}`);const v=V(m,r);if(!v.ok)throw new Error(`Publication target failed integrity verification: ${v.errors.join("; ")}`);if(y.manifestDigest!==v.manifestDigest)throw new Error("Publication target already exists with a different manifest digest");return o(e,r,{status:"complete",required:t.required,target:m,manifestDigest:y.manifestDigest,reused:!0,bundleBytes:ne(d),bytesUploaded:0})}const R=ne(d);c.cpSync(d,p,{recursive:!0,preserveTimestamps:!0});const g=V(p,r);if(!g.ok)throw new Error(g.errors.join("; "));return c.renameSync(p,m),o(e,r,{status:"complete",required:t.required,target:m,manifestDigest:g.manifestDigest,reused:!1,bundleBytes:R,bytesUploaded:R})}catch(d){return c.rmSync(p,{recursive:!0,force:!0}),o(e,r,{status:"failed",required:t.required,target:m,error:x(d),bytesUploaded:0})}}export function applyRunBundleRetention(e,n,t={},s={}){const r=I(n),o=s.removeObject??ce,i=s.removeRunBundle??Ne,a=t.maxRuns??null,f=t.maxAgeDays??null,m=new Set(t.preserveSuites||[]),p=ie(e),d=new Set([r]);for(const l of p)Oe(l.artifact,m)&&d.add(l.runId);const R=new Set(a?p.slice(0,a).map(l=>l.runId):p.map(l=>l.runId)),g=f==null?null:Date.now()-f*24*60*60*1e3,h=[];for(const l of p){if(d.has(l.runId))continue;const S=g!=null&&Date.parse(l.finishedAt||l.startedAt||"")<g,w=!R.has(l.runId);if(!S&&!w)continue;const B=[];S&&B.push("max_age"),w&&B.push("max_runs"),h.push({path:u.join(U(e),ge,l.runId),reasons:B,runId:l.runId})}const y=new Map(p.map(l=>[l.runId,l.manifest.files.filter(S=>S.path.startsWith("files/")).map(S=>S.sha256)])),v=oe(e),j=[],b=[];for(const l of h){let S=null;try{i(l.path)}catch(B){S=L(B)}const w=!c.existsSync(l.path);!w&&S===null&&(S="Run bundle still exists after removal completed"),w&&j.push({reasons:l.reasons,runId:l.runId}),S!==null&&b.push({...l,error:S,removed:w})}const D=new Set(j.map(l=>l.runId)),E=new Set(p.filter(l=>!D.has(l.runId)).flatMap(l=>y.get(l.runId)??[])),T=ae(v,E,o),_=new Date().toISOString();return Ve({currentRunId:r,decidedAt:_,garbageCollection:T,policy:{maxRuns:a,maxAgeDays:f,preserveSuites:[...m].sort()},protectedRunIds:[...d].sort(),removed:j,removalErrors:b})}function Ee({resultsDir:e,tempDir:n,sensitiveValues:t}){let s=0,r=0,o=0,i=0,a=0;const f=u.join(e,X,"sha256");c.mkdirSync(f,{recursive:!0});for(const m of W){const p=u.join(e,m);if(c.existsSync(p))for(const d of re(p)){const R=u.relative(p,d),g=c.readFileSync(d),h=De(g,d,t);h.scanned&&(o+=1),h.replacements>0&&(i+=1,a+=h.replacements);const y=C(h.buffer),v=u.join(f,y);c.existsSync(v)?r+=h.buffer.length:(je(v,h.buffer),c.chmodSync(v,292),s+=h.buffer.length);const j=u.join(n,"files",m,R);c.mkdirSync(u.dirname(j),{recursive:!0});try{c.linkSync(v,j)}catch{c.copyFileSync(v,j)}}}return{objectBytesWritten:s,deduplicatedBytes:r,redaction:{policyVersion:1,status:"applied",textualFilesScanned:o,filesRedacted:i,replacements:a,sensitiveEnvironmentKeys:t.map(m=>m.key).sort(),valuesStored:!1}}}function Be(e,n,t){const s=new Map(W.map(o=>[`.testkit/results/${o}/`,`${$(n,`files/${o}`)}/`])),r=K(e,s);return ke(Ae(r),t)}function Ae(e){if(!A(e))return e;const n={...e};if(A(n.product)){const t={...n.product};delete t.directory,n.product=t}if(A(n.git)){const t={...n.git};delete t.remoteUrl,delete t.repoRoot,delete t.worktreePath,n.git=t}return delete n.host,n}function K(e,n){if(Array.isArray(e))return e.map(s=>K(s,n));if(A(e))return Object.fromEntries(Object.entries(e).map(([s,r])=>[s,K(r,n)]));if(typeof e!="string")return e;let t=e;for(const[s,r]of n)t.startsWith(s)&&(t=`${r}${t.slice(s.length)}`);return t}function ke(e,n){let t=0;function s(r){if(Array.isArray(r))return r.map(s);if(A(r))return Object.fromEntries(Object.entries(r).map(([i,a])=>[i,s(a)]));if(typeof r!="string")return r;let o=G(r);o!==r&&(t+=1);for(const i of n){if(!o.includes(i.value))continue;const a=o.split(i.value);t+=a.length-1,o=a.join("<redacted>")}return o}return{artifact:s(e),replacements:t}}function xe(e,n,t,s){const r=z(n.run.fileNames),o=[...new Set([...Ie,...r])].map(i=>Fe(e,i)).filter(i=>i!==null);return{schemaVersion:pe,runId:t,ci:be(s.env??process.env),source:{git:n.git??null,files:o},runtime:{controller:{testkitVersion:k(n.run.testkitVersion),node:process.versions.node,platform:process.platform,architecture:process.arch},runtimeStats:ye(n.run.runtimeStats)},scenario:{types:z(n.run.types),suiteSelectors:z(n.run.suiteSelectors),fileNames:r,serviceFilter:k(n.run.serviceFilter),planningDigest:C(Buffer.from(q(n.planning??null))),declaredEnvironmentKeys:[...new Set(s.declaredEnvironmentKeys??[])].sort()},redaction:s.redaction}}function $e(e){return re(e).map(n=>({path:J(u.relative(e,n)),bytes:c.statSync(n).size,sha256:F(n)})).sort((n,t)=>n.path.localeCompare(t.path))}function V(e,n){const t=u.join(e,"manifest.json"),s=Y(P(t),t,n);le(P(u.join(e,"run.json")),u.join(e,"run.json"),n);const r=[];for(const i of s.files){let a;try{a=ee(e,i.path)}catch(m){r.push(x(m));continue}if(!c.existsSync(a)){r.push(`missing ${i.path}`);continue}const f=c.lstatSync(a);if(f.isSymbolicLink()){r.push(`symbolic link not allowed ${i.path}`);continue}if(!f.isFile()){r.push(`not a regular file ${i.path}`);continue}f.size!==i.bytes&&r.push(`size mismatch ${i.path}`),F(a)!==i.sha256&&r.push(`checksum mismatch ${i.path}`)}C(Buffer.from(q(s.files)))!==s.digest&&r.push("manifest digest mismatch"),s.storage.fileCount!==s.files.length&&r.push("manifest file count mismatch");for(const i of["run.json","provenance.json"])s.files.some(a=>a.path===i)||r.push(`manifest must include ${i}`);if(r.length===0){const i=ee(e,"provenance.json");ue(P(i),i,n)}return{runId:n,checkedFiles:s.files.length,storage:s.storage,ok:r.length===0,errors:r,manifestDigest:s.digest}}function Pe(e,n,t){const s={schemaVersion:1,runId:n,recordedAt:new Date().toISOString(),...t};return N(u.join(U(e),"publications",`${n}.json`),s),s}function Oe(e,n){return n.size===0?!1:Z(e.services).some(t=>Z(t.suites).some(s=>{const r=k(t.name)??"",o=k(s.name)??"",i=k(s.testType)??"";return[o,`${i}:${o}`,`${r}/${i}:${o}`].some(f=>n.has(f))}))}function Ce(e){const n=new Set;for(const t of listRunBundles(e)){const s=loadRunBundleManifest(e,t.runId);for(const r of s.files)r.path.startsWith("files/")&&n.add(r.sha256)}return ae(oe(e),n,ce)}function oe(e){const n=u.join(U(e),X,"sha256");return c.existsSync(n)?c.readdirSync(n,{withFileTypes:!0}).filter(t=>t.isFile()).map(t=>{const s=u.join(n,t.name);return{bytes:c.statSync(s).size,digest:t.name,path:s}}):[]}function ae(e,n,t){const s=[];let r=0,o=0;for(const i of e){if(n.has(i.digest))continue;let a=null;try{t(i.path)}catch(m){a=L(m)}!c.existsSync(i.path)?(r+=1,o+=i.bytes):a===null&&(a="Object still exists after removal completed"),a!==null&&s.push({error:a,path:i.path})}return{failures:s,reclaimedBytes:o,removedObjects:r,status:s.length===0?"complete":"failed"}}function Ne(e){c.rmSync(e,{recursive:!0,force:!0})}function ce(e){c.rmSync(e,{force:!0})}function Ve({currentRunId:e,decidedAt:n,garbageCollection:t,policy:s,protectedRunIds:r,removed:o,removalErrors:i}){const a=[...i.map(f=>`remove ${f.runId}: ${f.error}`),...t.failures.map(f=>`garbage collect ${f.path}: ${f.error}`)];return{currentRunId:e,decidedAt:n,garbageCollection:t,policy:s,protectedRunIds:r,removed:o,removalErrors:i,schemaVersion:he,status:a.length===0?"complete":"failed",...a.length===0?{}:{error:a.join("; ")}}}function De(e,n,t){if(!_e(e,n)){for(const i of t)if(e.includes(Buffer.from(i.value)))throw new Error(`Refusing to archive binary file containing sensitive environment value: ${n}`);return{buffer:e,scanned:!1,replacements:0}}const s=e.toString("utf8");let r=G(s),o=r===s?0:1;for(const i of t){if(!r.includes(i.value))continue;const a=r.split(i.value);o+=a.length-1,r=a.join("<redacted>")}return{buffer:Buffer.from(r),scanned:!0,replacements:o}}function Te(e){const n=[];for(const[t,s]of Object.entries(e))we.test(t)&&typeof s=="string"&&s.length>=8&&n.push({key:t,value:s});return n}function _e(e,n){if(e.includes(0))return!1;const t=u.extname(n).toLowerCase();return![".zip",".gz",".png",".jpg",".jpeg",".webm",".pdf"].includes(t)}function Fe(e,n){const t=u.resolve(e,n);return!t.startsWith(`${u.resolve(e)}${u.sep}`)||!c.existsSync(t)||!c.statSync(t).isFile()?null:{path:J(u.relative(e,t)),bytes:c.statSync(t).size,sha256:F(t)}}function ze(e){const n=[];for(const t of[...W.map(s=>u.join(e,s)),u.join(e,"live.json")])try{c.rmSync(t,{recursive:!0,force:!0})}catch(s){n.push(`${t}: ${L(s)}`)}return n.length===0?{status:"complete"}:{error:n.join("; "),status:"failed"}}function L(e){try{return x(e)}catch{return"Unknown post-seal archive operation failure"}}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ReportingEnvironmentService } from "./reporting-types.js";
|
|
2
|
+
export interface EnvironmentServiceAttempt {
|
|
3
|
+
baseUrl: string;
|
|
4
|
+
outcome: string;
|
|
5
|
+
port: number;
|
|
6
|
+
readyUrl: null | string;
|
|
7
|
+
}
|
|
8
|
+
export interface EnvironmentServiceEntry {
|
|
9
|
+
amendLastOutcome(outcome: string): void;
|
|
10
|
+
recordAttempt(attempt: Readonly<EnvironmentServiceAttempt>): void;
|
|
11
|
+
}
|
|
12
|
+
export interface RunEnvironmentRecord {
|
|
13
|
+
listServices(): ReportingEnvironmentService[];
|
|
14
|
+
service(identity: Readonly<{
|
|
15
|
+
runtimeLabel: string;
|
|
16
|
+
serviceName: string;
|
|
17
|
+
}>): EnvironmentServiceEntry;
|
|
18
|
+
}
|
|
19
|
+
export interface RunEnvironmentRecordOptions {
|
|
20
|
+
onChange?: () => void;
|
|
21
|
+
}
|
|
22
|
+
export declare function createRunEnvironmentRecord(options?: Readonly<RunEnvironmentRecordOptions>): RunEnvironmentRecord;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{redactUrlSecrets as o}from"../process/url-redaction.js";export function createRunEnvironmentRecord(m={}){const c=[];return{service({runtimeLabel:e,serviceName:t}){let r=null;return{recordAttempt({baseUrl:n,outcome:a,port:s,readyUrl:l}){const u={baseUrl:o(n),readyUrl:l===null?null:o(l)};r===null&&(r={serviceName:t,runtimeLabel:e,...u,port:s,attempts:[]},c.push(r)),Object.assign(r,u,{port:s}),r.attempts.push({port:s,outcome:a}),m.onChange?.()},amendLastOutcome(n){const a=r?.attempts.at(-1);a&&(a.outcome=n,m.onChange?.())}}},listServices(){return c.map(e=>({...e,attempts:e.attempts.map(t=>({...t}))})).sort((e,t)=>e.runtimeLabel.localeCompare(t.runtimeLabel)||e.serviceName.localeCompare(t.serviceName))}}}
|
package/dist/evidence/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export { normalizeArtifactArchiveConfig, type NormalizedArtifactArchiveConfig, t
|
|
|
2
2
|
export { finalizeLiveRunArtifact, loadTimings, persistTaskArtifacts, persistTaskFileArtifacts, persistTaskOutputArtifacts, resetResultArtifacts, saveTimings, writeLiveRunArtifact, writeRunArtifact, type ArtifactTask, type PersistedArtifactReference, type WriteRunArtifactOptions, } from "./artifacts.js";
|
|
3
3
|
export { applyRunBundleRetention, listRunBundles, loadRunBundleArtifact, loadRunBundleManifest, loadVerifiedRunBundleArtifactIfExists, publishRunBundle, sealRunBundle, verifyRunBundle, type RunBundleVerification, } from "./bundles.js";
|
|
4
4
|
export { collectArtifactEntries, loadCurrentRunArtifact, loadLatestRunArtifact, loadRunArtifact, type CollectedArtifactEntry, type RunArtifactFileSubject, type RunArtifactPayload, } from "./reading.js";
|
|
5
|
-
export { DEFAULT_REPORTED_ARCHIVE_CONFIG, type BuildLiveRunArtifactOptions, type BuildRunArtifactOptions, type ReportedRunArtifact, type ReportedRunService, type ReportingArtifactReference, type ReportingDiagnostic, type ReportingExecution, type ReportingFailureDetail, type ReportingFailureLocation, type ReportingFailureRequest, type ReportingFailureResponse, type ReportingFileSummary, type ReportingMetadata, type ReportingOperationTiming, type ReportingOperationTimingInput, type ReportingRecord, type ReportingRuntimeBatch, type ReportingRuntimeReset, type ReportingServiceLogReference, type ReportingServiceSummary, type ReportingSetupLogReference, type ReportingSetupOperation, type ReportingSuiteSelector, type ReportingSuiteSummary, type RunArtifactDocument, type RunArtifactFile, type RunArtifactRun, type RunArtifactService, type RunArtifactSuite, type RunArtifactSummary, type RunServiceResult, } from "./reporting-types.js";
|
|
5
|
+
export { DEFAULT_REPORTED_ARCHIVE_CONFIG, type BuildLiveRunArtifactOptions, type BuildRunArtifactOptions, type ReportedRunArtifact, type ReportedRunService, type ReportingArtifactReference, type ReportingDiagnostic, type ReportingEnvironmentService, type ReportingExecution, type ReportingFailureDetail, type ReportingFailureLocation, type ReportingFailureRequest, type ReportingFailureResponse, type ReportingFileSummary, type ReportingMetadata, type ReportingOperationTiming, type ReportingOperationTimingInput, type ReportingRecord, type ReportingRuntimeBatch, type ReportingRuntimeReset, type ReportingServiceLogReference, type ReportingServiceSummary, type ReportingSetupLogReference, type ReportingSetupOperation, type ReportingSuiteSelector, type ReportingSuiteSummary, type RunArtifactDocument, type RunArtifactFile, type RunArtifactRun, type RunArtifactService, type RunArtifactSuite, type RunArtifactSummary, type RunServiceResult, } from "./reporting-types.js";
|
|
6
6
|
export { copyRegularFileSecurely, MAX_PERSISTED_FILE_ARTIFACT_BYTES, type SecureFileCopyOptions, type SecureFileCopyResult, } from "./secure-file-copy.js";
|
|
7
7
|
export { collectTestkitStorageInventory } from "./storage-inventory.js";
|
|
8
8
|
export { type Timings, type TimingUpdate } from "./timing.js";
|
package/dist/evidence/logs.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import p from"fs";import
|
|
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
|
|
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 +1 @@
|
|
|
1
|
-
import{performance as
|
|
1
|
+
import{performance as M}from"node:perf_hooks";import{attachErrorOperation as R}from"../kernel/error-serialization.js";import{formatOperationSummary as x}from"../domain/operation-display.js";import{isUnknownRecord as k}from"../kernel/unknown-values.js";const g=String;export function createOperationTimingRegistry({onChange:t=null}={}){const r=[],i=new Map;let m=1;function d(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)));t(a,I())}function f({name:e,kind:a="operation",source:u="testkit",summary:s=null,scope:o=null,parentId:n=null,metadata:l=null}){const c={id:`operation-${g(m++)}`,source:u,name:D(e),kind:a,summary:s,scope:h(o),parentId:n,status:"running",startedAt:new Date().toISOString(),updatedAt:null,finishedAt:null,durationMs:null,error:null,message:null,logRefs:[],metadata:h(l),_startedMonotonicMs:M.now()};return r.push(c),i.set(c.id,c),d([c,...w(c)]),c}function A(e,{summary:a,message:u,metadata:s=null,logRefs:o=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)}),o!==null&&(n.logRefs=S(o)),d([n,...w(n)]),p(n)):null}function O(e,{status:a="passed",summary:u,error:s=null,metadata:o=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)),o!==null&&(n.metadata={...n.metadata,...h(o)}),d([n,...w(n)]),p(n)):null}function $(e={}){const a=b(e,m++),u=i.get(a.id);return u?(Object.assign(u,a),d([u,...w(u)]),p(u)):(r.push(a),i.set(a.id,a),d([a,...w(a)]),p(a))}function I(){return r.map(e=>p(e))}return{start:f,update:A,finish:O,record:$,listOperations:I,countOperations(){return r.length}};function w(e){if(!e.parentId)return[];const a=[],u=new Date().toISOString();let s=i.get(e.parentId);const o=new Set([e.id]);for(;s&&s.status==="running"&&!o.has(s.id);){o.add(s.id),s.updatedAt=u;const n=e.status||"unknown",l=n==="running"?"activeChild":"lastChild";s.message=z(e),s.metadata={...s.metadata,[`${l}OperationId`]:e.id,[`${l}OperationName`]:e.name,[`${l}OperationKind`]:e.kind,[`${l}OperationStatus`]:n,[`${l}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 d=await i(m);return t.finish(m,{status:"passed"}),d}catch(d){const f=t.finish(m,{status:"failed",error:N(d)});throw R(d,f),d}}function b(t,r){const i=y(t.startedAt)||new Date().toISOString(),m=t.status||"passed",d=m==="running"?null:y(t.finishedAt)||i,f=t.durationMs,A=Number(f),O=f!=null&&Number.isFinite(A)?Math.max(0,Math.round(A)):d?Math.max(0,Date.parse(d)-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:d,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 k(t)?{...t}:{}}function S(t){return Array.isArray(t)?t.map(r=>k(r)?{...r}:r):[]}function z(t){const r=x(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 N(t){return(typeof t!="object"||t===null)&&typeof t!="function"?g(t):Reflect.get(t,"message")||g(t)}
|