@gfdelarue/testkit 0.3.4 → 0.3.6
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 +11 -73
- package/dist/app/doctor/index.js +1 -1
- package/dist/app/inspect/artifacts.d.ts +1 -0
- package/dist/app/inspect/artifacts.js +1 -1
- package/dist/app/inspect/index.js +1 -1
- package/dist/app/lint/index.js +1 -1
- package/dist/app/lint/native.js +1 -1
- package/dist/app/lint/policy.js +1 -1
- package/dist/app/lint/rules/data.js +1 -1
- package/dist/app/lint/rules/engine-imports.js +1 -1
- package/dist/app/lint/rules/syntax.js +1 -1
- package/dist/app/lint/rules/unit.js +1 -1
- package/dist/app/lint/shared.d.ts +1 -1
- package/dist/app/lint/shared.js +1 -1
- package/dist/app/run/orchestrator.js +1 -1
- package/dist/app/run/request.js +1 -1
- package/dist/app/setup/playwright.js +1 -1
- package/dist/app/status/index.d.ts +2 -0
- package/dist/app/status/index.js +1 -1
- package/dist/app/status/runs.d.ts +2 -2
- package/dist/app/status/runs.js +1 -1
- package/dist/app/stop/index.js +1 -1
- package/dist/app/typecheck/index.d.ts +2 -0
- package/dist/app/typecheck/index.js +4 -4
- package/dist/cli/commands/artifacts.js +2 -2
- package/dist/cli/commands/db/schema/refresh.js +1 -1
- package/dist/cli/commands/db/schema/verify.js +1 -1
- package/dist/cli/commands/doctor.js +1 -1
- package/dist/cli/operations/db/schema/refresh/operation.d.ts +1 -0
- package/dist/cli/operations/db/schema/refresh/operation.js +1 -1
- package/dist/cli/operations/db/schema/verify/operation.d.ts +1 -0
- package/dist/cli/operations/db/schema/verify/operation.js +1 -1
- package/dist/cli/operations/run/operation.js +1 -1
- package/dist/config/loader/discover-tests.d.ts +5 -0
- package/dist/config/loader/discover-tests.js +1 -1
- package/dist/config/loader/discovery.js +2 -2
- package/dist/config/loader/file-metadata.js +1 -1
- package/dist/config/loader/index.d.ts +2 -1
- package/dist/config/loader/index.js +1 -1
- package/dist/config/loader/load.d.ts +2 -0
- package/dist/config/loader/load.js +1 -1
- package/dist/config/loader/managed-configs.d.ts +2 -0
- package/dist/config/loader/managed-configs.js +1 -1
- package/dist/config/loader/paths.d.ts +1 -1
- package/dist/config/loader/paths.js +1 -1
- package/dist/config/loader/skip-config.d.ts +1 -1
- package/dist/config/loader/test-selection.d.ts +15 -0
- package/dist/config/loader/test-selection.js +1 -0
- package/dist/database/index.d.ts +1 -3
- package/dist/database/index.js +1 -1
- package/dist/database/lifecycle-cleanup.d.ts +7 -0
- package/dist/database/lifecycle-cleanup.js +1 -0
- package/dist/database/local-postgres.js +1 -1
- package/dist/database/schema-source.js +3 -3
- package/dist/engines/adapters/evidence-files.js +1 -1
- package/dist/engines/catalog.d.ts +3 -8
- package/dist/engines/catalog.js +1 -1
- package/dist/engines/definitions.d.ts +8 -1
- package/dist/engines/definitions.js +1 -1
- package/dist/engines/generated-performance.d.ts +1 -0
- package/dist/engines/generated-performance.js +12 -0
- package/dist/engines/installations/node-vitest.d.ts +8 -22
- package/dist/engines/installations/node-vitest.js +1 -1
- package/dist/engines/installations/package-requirements.d.ts +28 -0
- package/dist/engines/installations/package-requirements.js +1 -0
- package/dist/engines/installations/playwright.d.ts +1 -11
- package/dist/engines/installations/playwright.js +1 -1
- package/dist/engines/k6/adapter/repository-configuration.d.ts +1 -1
- package/dist/engines/k6/adapter/repository-configuration.js +1 -1
- package/dist/engines/managed-module-resolution.js +111 -21
- package/dist/engines/manager.d.ts +1 -0
- package/dist/engines/manager.js +1 -1
- package/dist/engines/node-vitest/adapter/adapter.d.ts +1 -1
- package/dist/engines/node-vitest/adapter/adapter.js +1 -1
- package/dist/engines/node-vitest/adapter/consumer-typescript-paths.js +1 -1
- package/dist/engines/node-vitest/adapter/resolution.d.ts +1 -1
- package/dist/engines/node-vitest/adapter/resolution.js +1 -1
- package/dist/engines/node-vitest/adapter/workspace.js +33 -9
- package/dist/engines/node-vitest/runtime/runtime-input.d.ts +1 -0
- package/dist/engines/node-vitest/runtime/runtime-input.js +1 -1
- package/dist/engines/runtime-input-file.d.ts +3 -1
- package/dist/engines/runtime-input-file.js +1 -1
- package/dist/environment/lifecycle-controller.d.ts +1 -2
- package/dist/environment/lifecycle-controller.js +1 -1
- package/dist/environment/lifecycle.js +1 -1
- package/dist/environment/maintenance.d.ts +1 -1
- package/dist/environment/maintenance.js +1 -1
- package/dist/environment/ready-environment.d.ts +1 -1
- package/dist/environment/ready-environment.js +1 -1
- package/dist/environment/runtime-contexts.d.ts +1 -1
- package/dist/environment/runtime-contexts.js +1 -1
- package/dist/environment/runtime-preparation.js +1 -1
- package/dist/environment/services.js +1 -1
- package/dist/environment/template.d.ts +1 -1
- package/dist/environment/template.js +1 -1
- package/dist/evidence/bundles.js +1 -1
- package/dist/evidence/read-run.js +1 -1
- package/dist/evidence/reading.d.ts +1 -1
- package/dist/evidence/reading.js +1 -1
- package/dist/evidence/storage.js +2 -2
- package/dist/kernel/error-serialization.js +1 -1
- package/dist/kernel/work-metrics.d.ts +6 -0
- package/dist/kernel/work-metrics.js +1 -0
- package/dist/process/capture-output.js +1 -1
- package/dist/process/execa-bundle.json +1 -1
- package/dist/process/files/product-dir.d.ts +1 -0
- package/dist/process/files/product-dir.js +1 -0
- package/dist/process/locks.js +4 -4
- package/dist/process/readiness.js +1 -1
- package/dist/process/runs/inspection.d.ts +5 -0
- package/dist/process/runs/inspection.js +1 -0
- package/dist/process/runs/manifest.d.ts +1 -0
- package/dist/process/runs/manifest.js +1 -1
- package/dist/process/service-processes.d.ts +1 -1
- package/dist/process/service-processes.js +1 -1
- package/dist/toolchains/index.js +2 -2
- package/docs/cloud-control.md +3 -0
- package/docs/engine-architecture.md +4 -1
- package/docs/test-types.md +9 -0
- package/docs/testkit-cloud-boundary.md +12 -24
- package/package.json +2 -1
- package/testkit-config-schema.md +2 -0
- package/dist/cli/renderers/artifact-preview.d.ts +0 -4
- package/dist/cli/renderers/artifact-preview.js +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{formatRunSummary as w}from"../process/runs/manifest.js";import{cleanupOrphanedLocalInfrastructure as R,cleanupTerminalRunLocalInfrastructure as m}from"../database/
|
|
1
|
+
import{formatRunSummary as w}from"../process/runs/manifest.js";import{cleanupOrphanedLocalInfrastructure as R,cleanupTerminalRunLocalInfrastructure as m}from"../database/lifecycle-cleanup.js";import{resetResultArtifacts as v}from"../evidence/index.js";import{requireProductLifecycleFenceProductDir as d,withProductLifecycleFence as l}from"../process/lifecycle-fence.js";import{listRunManifests as u,removeRunManifestFile as y,requireSafeRunId as I}from"../process/runs/manifest.js";import{stopRunManifest as A}from"./run-stopping.js";import{createUnpublishedRunLifecycle as L,disposeFailedRunLifecycle as k,isRunManifestActive as a,cleanupOwnedRunLifecycleWithinLifecycleFence as S,readOwnedRunLifecycleWithinLifecycleFence as p}from"./lifecycle-controller.js";import{reconcileInactiveRunArtifact as g,reconcileOrphanedLiveRunArtifact as x}from"./lifecycle-artifacts.js";class f extends Error{constructor(n){super(`Stale Testkit run ${n} still owns application services and requires phased cleanup`),this.name="StaleRunServiceCleanupRequiredError"}}export async function startRunLifecycle(e,n={}){for(;;){await cleanupStaleRuns(e);try{return await l(e,t=>startRunLifecycleWithinLifecycleFence(t,n))}catch(t){if(!(t instanceof f))throw t}}}export async function startRunLifecycleWithinLifecycleFence(e,n={}){const t=d(e);h(t),await cleanupStaleRunsWithinLifecycleFence(e);const c=u(t).find(i=>!a(i)&&i.services.length>0);if(c!==void 0)throw new f(c.runId);h(t);const r=L(t,n);try{return r.installSignalHandlers(),n.resetResultsBeforePublish===!0&&v(t),r.markRunning(),r}catch(i){throw k(r),i}}function h(e){const n=u(e).find(a);if(n)throw new Error(`Active Testkit run ${w(n)} already owns this product`)}export async function cleanupRuns(e,{includeActive:n=!1}={}){const t=[],c=[];for(;;){const i=await l(e,()=>u(e).flatMap(s=>{const o=a(s);return n&&o||!o&&s.services.length>0?[{active:o,manifest:s}]:[]}));if(i.length===0)break;for(const s of i)await A(e,s.manifest,{reason:s.active?"cleanup":"stale run process not active"}),s.active?t.push(s.manifest):c.push(s.manifest)}const r=await l(e,i=>cleanupRunsWithinLifecycleFence(i));return{...r,cleaned:[...t,...c,...r.cleaned],stoppedActive:t}}export async function cleanupRunsWithinLifecycleFence(e,{protectedLiveRunIds:n=new Set}={}){const t=d(e),c=u(t),r={cleaned:[],skippedActive:[],stoppedActive:[]};for(const i of c){if(a(i)||i.services.length>0){r.skippedActive.push(i);continue}g(t,i),y(t,O(i)),r.cleaned.push(i)}return x(t,{activeRunIds:new Set([...r.skippedActive.map(i=>i.runId).filter(i=>typeof i=="string"),...n])}),r.resourceCleanup=await R(t),{cleaned:r.cleaned,resourceCleanup:r.resourceCleanup,skippedActive:r.skippedActive,stoppedActive:r.stoppedActive}}export async function cleanupStaleRuns(e){const n=[];for(;;){const t=await cleanupRuns(e);if(n.push(...t.cleaned),!t.skippedActive.some(r=>!a(r)&&r.services.length>0))return n}}export async function cleanupStaleRunsWithinLifecycleFence(e){return(await cleanupRunsWithinLifecycleFence(e)).cleaned}export async function cleanupOwnedRunLifecycleResourcesWithinLifecycleFence(e,n){const t=p(e,n);if(t.manifest===null)throw new Error(`Cannot clean terminal Testkit run ${n.runId} resources without its owned manifest`);if(t.manifest.services.length>0)throw new Error(`Cannot clean terminal Testkit run ${n.runId} resources while service ownership remains published`);const c=C(t.manifest);await m(e,c);const r=p(e,n);if(r.manifest===null||!F(r.manifest,c))throw new Error(`Cannot remove terminal Testkit run ${n.runId} because its manifest changed during resource cleanup`);return S(e,n)}function C(e){if(e.status!=="failed"&&e.status!=="finished"&&e.status!=="interrupted")throw new Error(`Cannot clean Testkit run ${e.runId} database resources before terminal finalization`);if(typeof e.finishedAt!="string"||e.finishedAt.length===0)throw new Error(`Cannot clean terminal Testkit run ${e.runId} database resources without a completion time`);return{finishedAt:e.finishedAt,incarnationId:e.incarnationId,pid:e.pid,processIdentity:e.processIdentity,runId:e.runId,status:e.status}}function F(e,n){return e.runId===n.runId&&e.incarnationId===n.incarnationId&&e.pid===n.pid&&e.processIdentity===n.processIdentity&&e.finishedAt===n.finishedAt&&e.status===n.status&&e.services.length===0}function O(e){return I(e.runId,"Testkit run manifest runId")}export{listRunManifests,RunManifestInventoryError}from"../process/runs/manifest.js";export{stopRunById,stopRunManifest}from"./run-stopping.js";export{cleanupOwnedRunLifecycleWithinLifecycleFence,findPortOwner,isRunManifestActive}from"./lifecycle-controller.js";export{isPidRunning}from"../process/runs/stop.js";export{formatRunSummary}from"../process/runs/manifest.js";
|
|
@@ -55,7 +55,7 @@ export interface CacheMaintenance<Result> {
|
|
|
55
55
|
}
|
|
56
56
|
export interface EnvironmentMaintenance {
|
|
57
57
|
inspect(desiredGraphs: readonly DesiredRuntimeGraph[], serviceName?: string | null): EnvironmentInventory;
|
|
58
|
-
inspectRuns(): Array<RunManifest & {
|
|
58
|
+
inspectRuns(runId?: string): Array<RunManifest & {
|
|
59
59
|
active: boolean;
|
|
60
60
|
}>;
|
|
61
61
|
stop(selection: RunSelection, reason: string): Promise<StoppedRun[]>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import o from"fs";import
|
|
1
|
+
import{inspectRunManifests as G}from"../process/runs/inspection.js";import o from"fs";import m from"path";import{cleanupOwnedLocalDatabaseResources as L,cleanupOrphanedLocalInfrastructure as x,destroyOwnedLocalDatabaseResources as I,destroyRuntimeDatabase as R,destroyServiceDatabaseCache as F,isDatabaseStateDir as S}from"../database/index.js";import{cleanupRuns as C,cleanupRunsWithinLifecycleFence as k,isRunManifestActive as A,listRunManifests as g,stopRunManifest as N}from"./lifecycle.js";import{findGraphDirsForService as T,findRuntimeStateDirs as M}from"./state.js";import{collectRuntimeGraphStatus as E,collectRunStatus as O}from"./status-runtime.js";import{withProductLifecycleFence as p}from"../process/lifecycle-fence.js";import{pruneEmptyDir as $}from"../process/files/directory-status.js";export function createEnvironmentMaintenance(e){async function n(t,a){if(t.kind==="destroy")return j(e,t.configs,t);if(a===void 0)throw new Error("Cleanup requires a cache maintenance plan");if(t.dryRun)return P(e,t,a);const s=await C(e,{includeActive:!0});return p(e,i=>P(e,t,a,i,s))}return{inspect(t,a=null){return{runtimeGraphs:E(e,{desiredGraphs:t,serviceName:a}),runs:O(e)}},inspectRuns:t=>G(e,t),async stop(t,a){const s=await p(e,()=>{const r=g(e);return t.kind==="run"?r.filter(u=>u.runId===t.runId):t.kind==="all"?r:r.filter(A).slice(-1)}),i=[];for(const r of s)i.push(await N(e,r,{reason:a}));return i},maintain:n}}async function j(e,n,t={}){B(e,n),await C(e,{includeActive:!0}),await p(e,async a=>{const s=await k(a);b(s,"destroy");const i=new Set(n.flatMap(r=>[r.stateDir,...T(e,r.name)]));for(const r of i){if(!o.existsSync(r))continue;const u=M(r,S);for(const f of u)await R({productDir:e,stateDir:f});o.rmSync(r,{recursive:!0,force:!0})}for(const r of n)await F(e,r.name);t.destroyOwnedResources&&await I(a),await x(e)})}async function P(e,n,t,a=null,s=null){const{dryRun:i}=n,r=n.serviceName||null,u=n.resources;let f,d=[];if(i)f=W(e);else{if(a===null)throw new Error("Destructive cleanup requires the product lifecycle fence");const c=await k(a);b(c,"cleanup"),f={...c,cleaned:[...s?.cleaned??[],...c.cleaned],stoppedActive:[...s?.stoppedActive??[]]},d=[...s===null?[]:[s.resourceCleanup],c.resourceCleanup]}const w=E(e,{desiredGraphs:n.desiredGraphs,serviceName:r}).flatMap(c=>c.staleRuntimeDirs.filter(l=>!l.active).map(l=>({graph:c.name,runtimeId:l.id,path:l.path}))),h=t(),y=[];let v={targets:[],removed:[],kept:[],skipped:[],stopped:[]};if(!i){for(const c of w)await U(e,c.path),y.push(c);await h.prune(),$(m.join(e,".testkit","_graphs"))}return u&&(v=await L(e,{dryRun:i})),{environment:{...f,dryRun:i,targets:w,runtimeCleaned:y,resourceCleanup:v,automaticResourceCleanup:d},otherCaches:h.result}}function b(e,n){if(e.skippedActive.length!==0)throw new Error(`A Testkit run started while ${n} was acquiring exclusive product ownership; retry after the run finishes`)}function B(e,n){const t=o.realpathSync.native(m.resolve(e));for(const a of n)if(o.realpathSync.native(m.resolve(a.productDir))!==t)throw new Error(`Cannot destroy service ${a.name} from a different Testkit product`)}async function U(e,n){if(!n||!o.existsSync(n))return;const t=M(n,S);for(const a of t)await R({productDir:e,stateDir:a});o.rmSync(n,{recursive:!0,force:!0})}function W(e){const n={cleaned:[],skippedActive:[],stoppedActive:[]};for(const t of g(e))A(t)?n.skippedActive.push(t):n.cleaned.push(t);return n}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type DatabaseInfo } from "../database/
|
|
1
|
+
import { type DatabaseInfo } from "../database/state.js";
|
|
2
2
|
import type { RuntimeInstanceContext } from "./runtime-contexts.js";
|
|
3
3
|
import type { ReadyServiceConfig } from "./runtime-service.js";
|
|
4
4
|
export interface ReadyService {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{readDatabaseInfo as s}from"../database/
|
|
1
|
+
import{readDatabaseInfo as s}from"../database/state.js";export function createReadyEnvironment(t,n){const a=new Map(t.runtimeConfigs.map(e=>[e.name,s(e.stateDir)]));return{...n,graphKey:t.graphKey,runtimeId:t.runtimeId,targetNames:[...t.targetNames],services:new Map(t.runtimeConfigs.map(e=>{const r={...e.testkit.templateContext};return delete r.databaseBindingsReader,[e.name,{baseUrl:e.testkit.local?.baseUrl||null,database:a.get(e.name)??null,config:{...e,testkit:{...e.testkit,templateContext:{...r,databaseBindingsByService:a}}}}]}))}}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ProvisionDatabaseOptions } from "../database/index.js";
|
|
2
2
|
import type { RuntimeServiceSpec, RuntimeGraph } from "./planning-types.js";
|
|
3
3
|
import { type RuntimePreparationConfig, type RuntimePreparationOptions } from "./runtime-preparation.js";
|
|
4
4
|
import { type LocalServiceLifecycle, type StartedLocalService, type StartLocalServicesOptions } from "./services.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import p from"fs";import t from"path";import{
|
|
1
|
+
import p from"fs";import t from"path";import{prepareRuntimeServices as m}from"./runtime-preparation.js";import{writeGraphMetadata as o}from"./state.js";import{startLocalServices as u,stopLocalServices as f}from"./services.js";import{resolveRuntimeInstanceConfigs as d}from"./template.js";export function createRuntimeInstanceContext(r,e,i){const a=t.join(i,".testkit","_graphs",e.dirName),s=t.join(a,"runtimes",r);p.mkdirSync(s,{recursive:!0}),o(a,e);const n=d(e.runtimeConfigs,r,s,{graphDirName:e.dirName,portNamespaceIndex:e.portNamespaceIndex,portNamespaceStride:e.portNamespaceStride});return{graphKey:e.key,targetNames:[...e.targetNames],graphDir:a,runtimeDir:s,runtimeId:r,runtimeConfigs:n,prepared:!1,preparationPromise:null,started:!1,startupPromise:null,startedServices:[]}}export async function ensureRuntimeInstanceReady(r,e,i,a={}){return r.prepared||(r.preparationPromise||(r.preparationPromise=(async()=>{await prepareDatabases(r.runtimeConfigs,{...a,signal:i.signal}),await m(r.runtimeConfigs,{...a,signal:i.signal}),r.prepared=!0})().finally(()=>{r.preparationPromise=null})),await r.preparationPromise),e&&!r.started&&(r.startupPromise||(r.startupPromise=(async()=>{r.startedServices=await u(r.runtimeConfigs,i,a),r.started=!0})().finally(()=>{r.startupPromise=null})),await r.startupPromise),r}async function l(r,e){r?.started&&(await f(r.startedServices,e),r.started=!1,r.startedServices=[])}export async function cleanupRuntimeInstanceContext(r,e){r&&await l(r,e)}export async function prepareDatabases(r,e={}){if(!r.some(a=>a.testkit.database))return;const{provisionDatabase:i}=await import("../database/index.js");for(const a of r)await i(a,{progress:{reporter:e.reporter,setupRegistry:e.setupRegistry},signal:e.signal,skipSchemaSourceVerify:e.skipSchemaSourceVerify})}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import S from"crypto";import p from"fs";import k from"path";import{resolveServiceCwd as D}from"../config/loader/index.js";import{appendFileToHash as y}from"../repo/fingerprint-policy.js";import{announceResolvedToolchain as b,resolveConfiguredToolchain as v}from"../toolchains/index.js";import{readDatabaseUrl as m}from"../database/
|
|
1
|
+
import S from"crypto";import p from"fs";import k from"path";import{resolveServiceCwd as D}from"../config/loader/index.js";import{appendFileToHash as y}from"../repo/fingerprint-policy.js";import{announceResolvedToolchain as b,resolveConfiguredToolchain as v}from"../toolchains/index.js";import{readDatabaseUrl as m}from"../database/state.js";import{buildExecutionEnv as w,collectConfiguredInputs as x}from"../steps/index.js";import{runConfiguredSteps as R}from"../database/configured-steps.js";import"../evidence/setup-progress.js";const I="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||!N(e))return;const n=e.testkit.prepareDir,s=await computeRuntimePrepareFingerprint(e,{signal:r.signal}),i=k.join(n,I);if(g(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={...w(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 b(e,await v(e,{signal:r.signal}),r.reporter),await R({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),A(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:U(a,"message")||a}):null;throw f&&r.reporter?.setupOperationFinished?.(f),p.rmSync(n,{recursive:!0,force:!0}),a}}function N(e){return e.testkit.runtime.prepare!=null}export async function computeRuntimePrepareFingerprint(e,r={}){const t=await v(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,serviceEnv:e.testkit.serviceEnv||{},local:e.testkit.local?{baseUrl:e.testkit.local.baseUrl,cwd:e.testkit.local.cwd,env:e.testkit.local.env||{},readyUrl:e.testkit.local.readyUrl,start:e.testkit.local.start}:null})),n.update(JSON.stringify(O(e)));const s=e.testkit.fingerprints||{exclude:[],include:[]};for(const i of e.testkit.envFiles||[])y(n,e.productDir,D(e.productDir,i),s);for(const i of x(e.productDir,e.testkit.runtime.prepare))y(n,e.productDir,i,s);return n.digest("hex")}function O(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 g(e){if(!p.existsSync(e))return null;try{const r=JSON.parse(p.readFileSync(e,"utf8"));return F(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 A(e,r){p.writeFileSync(e,JSON.stringify(r,null,2))}function F(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function U(e,r){if(!((typeof e!="object"||e===null)&&typeof e!="function"))return Reflect.get(e,r)}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{DEFAULT_READY_TIMEOUT_MS as x,waitForReady as M,serviceCompletionError as y}from"../process/readiness.js";import{resolveServiceCwd as A}from"../config/loader/index.js";import{startSupervisedProcess as f,ProcessExitedBeforeReadyError as k,DEFAULT_SHUTDOWN_TIMEOUT_MS as R}from"../process/index.js";import{announceResolvedToolchain as U,applyToolchainEnv as g,resolveConfiguredToolchain as D}from"../toolchains/index.js";import{buildExecutionEnv as O}from"../steps/index.js";import{numericPortFromUrl as P}from"./template.js";import{assertLocalServicePortsAvailable as _}from"./readiness.js";import{captureOutput as T}from"../process/index.js";import{prepareServiceStartInvocation as C}from"../process/service-processes.js";import{readDatabaseUrl as F}from"../database/
|
|
1
|
+
import{DEFAULT_READY_TIMEOUT_MS as x,waitForReady as M,serviceCompletionError as y}from"../process/readiness.js";import{resolveServiceCwd as A}from"../config/loader/index.js";import{startSupervisedProcess as f,ProcessExitedBeforeReadyError as k,DEFAULT_SHUTDOWN_TIMEOUT_MS as R}from"../process/index.js";import{announceResolvedToolchain as U,applyToolchainEnv as g,resolveConfiguredToolchain as D}from"../toolchains/index.js";import{buildExecutionEnv as O}from"../steps/index.js";import{numericPortFromUrl as P}from"./template.js";import{assertLocalServicePortsAvailable as _}from"./readiness.js";import{captureOutput as T}from"../process/index.js";import{prepareServiceStartInvocation as C}from"../process/service-processes.js";import{readDatabaseUrl as F}from"../database/state.js";export async function startLocalServices(e,r,t={}){const s=[];try{for(const a of e){if(!B(a))continue;const n=await startLocalService(a,r,t);s.push(n)}}catch(a){try{await stopLocalServices(s,r)}catch(n){throw new AggregateError([a,n],"Local service startup failed and its supervised cleanup also failed",{cause:n})}throw a}return s}export async function startLocalService(e,r,t={}){const s=A(e.productDir,e.testkit.local.cwd),a=await D(e,{signal:r.signal});await U(e,a,t.reporter);const n=g(O(e,{...e.testkit.local.env||{},...t.extraEnv||{}},process.env),a),l=e.testkit.local.port||P(e.testkit.local.baseUrl);l&&(n.PORT=String(l));const b=F(e.stateDir);b&&(n.DATABASE_URL=b),await _(e),t.reporter?.localServiceStarting?.(e,e.testkit.local.start);const v=C(e.testkit.local.start,n),d=t.logRegistry?.ensureServiceLogRecord(e),L=t.reporter?.outputMode==="debug"?o=>t.reporter?.writeDebugLine?.(o):null,i=f({args:v.args,windowsVerbatimArguments:v.windowsVerbatimArguments,cwd:s,displayLabel:`Testkit local service ${e.runtimeLabel}:${e.name}`,env:I(n),executable:v.executable,outputMode:"caller-drained",signal:r.signal,shutdownTimeoutMs:e.testkit.local.shutdownTimeoutMs??R,timeoutMs:null}),S=[T(i.stdout,{livePrefix:`[${e.runtimeLabel}:${e.name}]`,liveWriter:L,onLine(o){d&&t.logRegistry?.append(d,"stdout",o)}}),T(i.stderr,{livePrefix:`[${e.runtimeLabel}:${e.name}]`,liveWriter:L,onLine(o){d&&t.logRegistry?.append(d,"stderr",o)}})];let h,p=!1,w=!1;try{h=await i.ready,p=!0,r.registerService(e,i,h,s);const o=e.testkit.local.readyTimeoutMs||x;e.testkit.local.readyUrl!==!1&&e.testkit.local.readyUrl!=null&&await M({name:`${e.runtimeLabel}:${e.name}`,url:e.testkit.local.readyUrl,timeoutMs:o,process:i,signal:r.signal})}catch(o){const u=o instanceof k?y(`${e.runtimeLabel}:${e.name}`,o):o;w=!0;const c=[];try{c.push(...await $(i,S,!1))}catch(E){c.push(m(E))}if(p)try{r.unregisterService(i.pid)}catch(E){c.push(m(E))}throw c.length>0?new AggregateError([u,...c],`Local service ${e.runtimeLabel}:${e.name} startup and cleanup failed`,{cause:o}):u}return{completionMonitor:i.completion.then(o=>{if(w)return null;if(r.signal.aborted){if(p){p=!1;try{r.unregisterService(i.pid)}catch{}}return null}let u=y(`${e.runtimeLabel}:${e.name}`,o,"unexpectedly");if(p){p=!1;try{r.unregisterService(i.pid)}catch(c){u=new AggregateError([u,c],`Local service ${e.runtimeLabel}:${e.name} exited and its manifest entry could not be removed`)}}try{r.requestStop(u.message)}catch(c){u=new AggregateError([u,c],`Local service ${e.runtimeLabel}:${e.name} exited and the active run could not be interrupted`)}return u}),expectCompletion(){w=!0},name:e.name,outputDrains:S,ownership:h,supervisor:i}}export async function stopLocalServices(e,r){const t=[];for(const s of[...e].reverse()){s.expectCompletion();try{t.push(...await $(s.supervisor,s.outputDrains)),await s.completionMonitor}catch(a){t.push(m(a))}finally{try{r?.unregisterService(s.ownership.pid)}catch(a){t.push(m(a))}}}if(t.length>0)throw new AggregateError(t,"Could not stop all local services")}async function $(e,r,t=!0){const a=await e.stop().then(l=>({error:null,result:l}),l=>({error:m(l),result:null})),n=await Promise.allSettled(r);return[...a.result?j(a.result,t):[a.error],...n.flatMap(l=>l.status==="rejected"?[m(l.reason)]:[])]}function j(e,r=!0){return[...r?[e.error]:[],e.terminationError].filter(t=>t!==null)}function I(e){return Object.fromEntries(Object.entries(e).filter(r=>r[1]!==void 0))}function m(e){return e instanceof Error?e:new Error(String(e))}function B(e){return e.testkit.local!=null}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { RuntimeSocket } from "../process/runtime-socket.js";
|
|
2
2
|
import { type NormalizedDatabaseConfig, type NormalizedRuntimeConfig } from "../config/loader/index.js";
|
|
3
3
|
import { type ExecutionEnvironmentConfig, type ExecutionEnvironmentLocalConfig, type ExecutionEnvironmentTestkitConfig, type ExecutionTemplateContextValues, type RuntimeUrlMapping } from "../steps/index.js";
|
|
4
|
-
import { readDatabaseInfo } from "../database/
|
|
4
|
+
import { readDatabaseInfo } from "../database/state.js";
|
|
5
5
|
type RuntimeIdentifier = number | string;
|
|
6
6
|
type StringMap = ReadonlyMap<string, string>;
|
|
7
7
|
interface RuntimeLocalConfig extends ExecutionEnvironmentLocalConfig {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{finalizeRuntimeDatabaseConfig as R}from"../database/runtime-config.js";import w from"path";import{finalizeConfiguredInputs as N,finalizeConfiguredSteps as y}from"../steps/configured-steps.js";import"../config/loader/index.js";import{finalizeString as h,resolveTemplateString as E}from"../steps/index.js";import{readDatabaseInfo as M}from"../database/
|
|
1
|
+
import{finalizeRuntimeDatabaseConfig as R}from"../database/runtime-config.js";import w from"path";import{finalizeConfiguredInputs as N,finalizeConfiguredSteps as y}from"../steps/configured-steps.js";import"../config/loader/index.js";import{finalizeString as h,resolveTemplateString as E}from"../steps/index.js";import{readDatabaseInfo as M}from"../database/state.js";const O=100;export function resolveRuntimeInstanceConfigs(t,e,r,s={}){const o=B(s.graphDirName,e),c=buildPortMap(t,e,{index:s.portNamespaceIndex||0,stride:s.portNamespaceStride||1,portOffset:s.portOffset||0}),i=new Map,p=new Map,l=new Map,u=new Map,d=new Map,m=new Map,S=x(s.externalEndpoints),v=x(s.publicEndpoints),k=x(s.privateEndpoints),f=x(s.serviceContextEndpoints);for(const n of t)m.set(n.name,resolveServiceStateDir(r,n));for(const n of t)n.testkit.local&&(i.set(n.name,resolveRuntimeUrl(n.testkit.local.baseUrl,n.name,e,{runtimeId:e,runtimeLabel:o,runtimeDir:r,portMap:c,baseUrlByService:i,readyUrlByService:p,stateDirByService:m})),n.testkit.local.readyUrl!==!1&&n.testkit.local.readyUrl!=null&&p.set(n.name,resolveRuntimeUrl(n.testkit.local.readyUrl,n.name,e,{runtimeId:e,runtimeLabel:o,runtimeDir:r,portMap:c,baseUrlByService:i,readyUrlByService:p,stateDirByService:m})));for(const[n,a]of Object.entries(S))i.has(n)||i.set(n,a),p.has(n)||p.set(n,a);for(const[n,a]of Object.entries(v))l.set(n,a);for(const[n,a]of Object.entries(k))u.set(n,a);for(const[n,a]of Object.entries(f))d.set(n,a);for(const[n,a]of i.entries())l.has(n)||l.set(n,g(a,s.publicHost||null)),d.has(n)||d.set(n,a),u.has(n)||u.set(n,a);const b=[];for(const n of t){if(!n.testkit.local)continue;const a=i.get(n.name),U=p.get(n.name);a&&b.push([n.testkit.local.baseUrl,a]),U&&b.push([n.testkit.local.readyUrl,U])}return t.map(n=>resolveRuntimeConfig(n,e,o,r,c,i,p,l,u,d,s.publicHost||null,m,b))}function x(t){return Object.fromEntries(Object.entries(t??{}).map(([e,r])=>[e,(r??"").trim()]).filter(e=>e[1].length>0))}export function buildPortMap(t,e,r={}){const s=new Map,o=new Map,c=j(e,r)+Number(r.portOffset||0);for(const i of t){if(!i.testkit.local)continue;const p=i.testkit.local.port||numericPortFromUrl(i.testkit.local.baseUrl);if(!p)continue;const l=p+c;if(l>65535)throw new Error(`Runtime port resolution exceeded 65535 for service "${i.name}" (${String(l)}). Reduce runtime instances or choose lower local.port/baseUrl ports in testkit.config.ts.`);const u=o.get(l);if(u)throw new Error(`Runtime port collision: services "${u}" and "${i.name}" both resolve to ${String(l)}. Assign distinct local.port/baseUrl ports in testkit.config.ts.`);o.set(l,i.name),s.set(i.name,l)}return s}export function resolveRuntimeConfig(t,e,r,s,o,c,i,p,l,u,d,m,S){const v=resolveServiceStateDir(s,t),k=resolveServicePrepareDir(s,t),f={runtimeId:e,runtimeLabel:r,runtimeDir:s,productDir:t.productDir,serviceName:t.name,serviceCwd:t.testkit.local?.cwd||".",serviceStateDir:v,prepareDir:k,portMap:o,baseUrlByService:c,readyUrlByService:i,publicBaseUrlByService:p,privateBaseUrlByService:l,serviceContextUrlByService:u,publicHost:d,stateDirByService:m,urlMappings:S,leaseId:null,leaseDir:null,databaseBindingsReader:M},b=R(t.testkit.database,f),n={...t.testkit.runtime,prepare:P(t.testkit.runtime.prepare,f)},a=t.testkit.local?{...t.testkit.local,start:h(t.testkit.local.start,f),cwd:t.testkit.local.cwd!==void 0?h(t.testkit.local.cwd,f):t.testkit.local.cwd,port:o.get(t.name)||t.testkit.local.port,baseUrl:c.get(t.name),readyUrl:i.get(t.name),env:{...t.testkit.local.env||{}}}:void 0;return{...t,stateDir:v,runtimeId:e,runtimeLabel:r,testkit:{...t.testkit,database:b,prepareDir:k,templateContext:f,runtime:n,local:a}}}export function resolveServiceStateDir(t,e){return w.join(t,"services",e.name)}export function resolveServicePrepareDir(t,e){return w.join(resolveServiceStateDir(t,e),"prepared")}export function resolveRuntimeUrl(t,e,r,s){const o=E(t,{...s,runtimeId:r,serviceName:e}),c=s.portMap.get(e);return c?rewriteUrlPort(o,c):o}function g(t,e){if(!e)return t;try{const r=new URL(t);r.hostname=e;let s=r.toString();return!t.endsWith("/")&&r.pathname==="/"&&s.endsWith("/")&&(s=s.slice(0,-1)),s}catch{return t}}function P(t,e){return t?{inputs:N(t.inputs,r=>h(r,e)),steps:y(t.steps,r=>h(r,e))}:{inputs:[],steps:[]}}export function rewriteUrlPort(t,e){try{const r=new URL(t);if(!r.port)return t;const s=new URL(t);s.port=String(e);let o=s.toString();return!t.endsWith("/")&&r.pathname==="/"&&o.endsWith("/")&&(o=o.slice(0,-1)),o}catch{return t}}export function numericPortFromUrl(t){try{const e=new URL(t),r=Number(e.port);return Number.isInteger(r)&&r>0?r:null}catch{return null}}export function socketFromUrl(t){try{const e=new URL(t),r=Number(e.port);if(!Number.isInteger(r)||r<=0)return null;const s=normalizeSocketHost(e.hostname);return s?{host:s,port:r}:null}catch{return null}}export function normalizeSocketHost(t){return!t||t==="localhost"?"127.0.0.1":t==="[::1]"?"::1":t}function j(t,e={}){const r=String(t).match(/(\d+)$/),s=r?Number.parseInt(r[1],10)-1:0,o=Math.max(0,Number(e.index||0)),c=Math.max(1,Number(e.stride||1));return O*(o*c+s)}function B(t,e){return t?`${t}/${e}`:e}
|
package/dist/evidence/bundles.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{optionalString as A}from"../kernel/unknown-values.js";import{readRunProvenance as oe}from"./read-provenance.js";import{parseRunBundleArtifact as ae,parseRunBundleManifest as J,readRunBundleArtifact as ce}from"./read-bundle.js";import{readRunDocument as ue}from"./read-run.js";import le from"crypto";import c from"fs";import u from"path";import{redactSensitiveText as Y}from"../kernel/redaction.js";import{BUNDLE_SCHEMA_VERSION as G,OBJECTS_DIRNAME as Q,PROVENANCE_SCHEMA_VERSION as fe,RETENTION_DECISION_SCHEMA_VERSION as de,RUNS_DIRNAME as me}from"./contracts.js";import{bundleRelativePath as P,errorMessage as k,hashFile as z,isObjectRecord as $,normalizePath as X,normalizeRunId as w,objectRecords as Z,optionalStringArray as D,optionalUnknownArray as pe,readJsonFile as O,requirePublicationPathOutsideResults as M,requireRunId as he,runIncludesFile as ge,resolveManifestEntryPath as U,resolveRunBundleDirectory as x,resolveRunsRoot as ee,resultsRoot as q,sanitizeSegment as ye,sha256 as C,stableJson as W,treeBytes as te,walkFiles as ne,writeBufferAtomic as ve,writeJson as N}from"./storage.js";import{resolveCiProvenance as Se}from"./ci-provenance.js";const K=["artifacts","logs","setup"],Re=/(?:PASSWORD|TOKEN|SECRET|PRIVATE_KEY|AUTH|COOKIE|SESSION|API_KEY)/iu,je=["package.json","package-lock.json","npm-shrinkwrap.json","pnpm-lock.yaml","yarn.lock","testkit.config.ts","testkit.config.mts","testkit.config.js","testkit.config.mjs"];export function sealRunBundle(e,r,t={},s={}){const n=r,i=he(n),o=ee(e,!0),a=u.dirname(o),f=u.join(o,i),m=s.cleanupActiveRunResults??_e,p=loadVerifiedRunBundleArtifactIfExists(e,i);if(p!==null)return{...p,archive:{...p.archive,cleanup:m(a)}};const d=u.join(o,`.${i}.${String(process.pid)}.${String(Date.now())}.${le.randomBytes(4).toString("hex")}.tmp`);c.mkdirSync(d,{recursive:!0});try{const R=Ne(t.env??process.env),g=be({productDir:e,resultsDir:a,runId:i,tempDir:d,sensitiveValues:R}),h=Ie(n,i,R),y=ue(h.artifact,"run artifact");g.redaction.structuredReplacements=h.replacements,g.redaction.replacements+=h.replacements;const v=Be(e,y,i,{...t,redaction:g.redaction}),j={schemaVersion:G,runId:i,status:"complete",canonicalRunPath:P(i,"run.json"),manifestPath:P(i,"manifest.json"),provenancePath:P(i,"provenance.json"),integrity:"sha256",publication:{configured:!!t.archiveConfig?.publication,status:t.archiveConfig?.publication?"pending":"not_configured"},redaction:g.redaction};y.archive=j,N(u.join(d,"run.json"),y),N(u.join(d,"provenance.json"),v);const b=Ae(d),V=C(Buffer.from(W(b))),E={schemaVersion:G,runId:i,createdAt:new Date().toISOString(),algorithm:"sha256",files:b,storage:{fileCount:b.length,logicalBytes:b.reduce((_,F)=>_+F.bytes,0),objectBytesWritten:g.objectBytesWritten,deduplicatedBytes:g.deduplicatedBytes},redaction:g.redaction,digest:V};return N(u.join(d,"manifest.json"),E),c.renameSync(d,f),{...y,archive:{...j,cleanup:m(a),manifestDigest:E.digest,storage:E.storage}}}catch(R){try{c.rmSync(d,{recursive:!0,force:!0})}catch{}try{xe(e)}catch{}throw R}}export function loadVerifiedRunBundleArtifactIfExists(e,r){const t=w(r),s=x(e,t);if(!c.existsSync(s))return null;const n=verifyRunBundle(e,t);if(!n.ok)throw new Error(`Immutable run bundle ${t} already exists but failed integrity verification: ${n.errors.join("; ")}`);return loadRunBundleArtifact(e,t)}export function loadRunBundleArtifact(e,r){const t=w(r),s=ce(e,t),n=loadRunBundleManifest(e,t);return{...s,archive:{...s.archive,manifestDigest:n.digest,storage:n.storage}}}export function loadRunBundleManifest(e,r){const t=w(r),s=x(e,t),n=u.join(s,"manifest.json");if(!c.existsSync(n))throw new Error(`No immutable Testkit run manifest found for ${t}`);return J(O(n),n,t)}export function listRunBundles(e,r={}){const t=ee(e,!1);return c.existsSync(t)?c.readdirSync(t,{withFileTypes:!0}).filter(n=>n.isDirectory()&&!n.name.startsWith(".")).map(n=>{try{const i=loadRunBundleArtifact(e,n.name);return r.file&&!ge(i,r.file)?null:{runId:n.name,status:i.run.status,startedAt:i.run.startedAt,finishedAt:i.run.finishedAt??null,manifestDigest:i.archive.manifestDigest??null,canonicalRunPath:i.archive.canonicalRunPath}}catch{return null}}).filter(n=>n!==null).sort((n,i)=>(i.finishedAt??i.startedAt??"").localeCompare(n.finishedAt??n.startedAt??"")):[]}export function verifyRunBundle(e,r){const t=w(r);try{return T(x(e,t),t)}catch(s){return{ok:!1,runId:t,errors:[k(s)],checkedFiles:0}}}export function prepareVerifiedRunBundleUpload(e,r=null){const t=r??listRunBundles(e).at(0)?.runId??null;if(t===null)throw new Error("No immutable Testkit run bundle is available to publish");const s=verifyRunBundle(e,t);if(!s.ok)throw new Error(`Immutable run bundle ${t} failed integrity verification: ${s.errors.join("; ")}`);const n=loadRunBundleManifest(e,t),i=x(e,t);return{runId:t,manifest:n,files:n.files.map(o=>({...o,absolutePath:U(i,o.path)}))}}export function publishRunBundle(e,r,t,s={}){const n=w(r),i=s.writePublicationReceipt??ke;if(!t)return i(e,n,{status:"not_configured",required:!1,target:null,bytesUploaded:0});const o=u.resolve(e,t.directory);try{M(e,o)}catch(d){return i(e,n,{status:"failed",required:t.required,target:o,error:k(d),bytesUploaded:0})}const a=ye(u.basename(e)),f=u.join(o,a),m=u.join(f,n),p=u.join(f,`.${n}.${String(process.pid)}.${String(Date.now())}.tmp`);try{const d=x(e,n);if(c.mkdirSync(f,{recursive:!0}),M(e,f),c.existsSync(m)){const h=c.lstatSync(m);if(h.isSymbolicLink()||!h.isDirectory())throw new Error("Publication target must be a regular bundle directory");M(e,m);const y=T(d,n);if(!y.ok)throw new Error(`Source run bundle failed integrity verification: ${y.errors.join("; ")}`);const v=T(m,n);if(!v.ok)throw new Error(`Publication target failed integrity verification: ${v.errors.join("; ")}`);if(y.manifestDigest!==v.manifestDigest)throw new Error("Publication target already exists with a different manifest digest");return i(e,n,{status:"complete",required:t.required,target:m,manifestDigest:y.manifestDigest,reused:!0,bundleBytes:te(d),bytesUploaded:0})}const R=te(d);c.cpSync(d,p,{recursive:!0,preserveTimestamps:!0});const g=T(p,n);if(!g.ok)throw new Error(g.errors.join("; "));return c.renameSync(p,m),i(e,n,{status:"complete",required:t.required,target:m,manifestDigest:g.manifestDigest,reused:!1,bundleBytes:R,bytesUploaded:R})}catch(d){return c.rmSync(p,{recursive:!0,force:!0}),i(e,n,{status:"failed",required:t.required,target:m,error:k(d),bytesUploaded:0})}}export function applyRunBundleRetention(e,r,t={},s={}){const n=w(r),i=s.removeObject??ie,o=s.removeRunBundle??Pe,a=t.maxRuns??null,f=t.maxAgeDays??null,m=new Set(t.preserveSuites||[]),p=listRunBundles(e).map(l=>({...l,artifact:loadRunBundleArtifact(e,l.runId)})),d=new Set([n]);for(const l of p)$e(l.artifact,m)&&d.add(l.runId);const R=new Set(a?p.slice(0,a).map(l=>l.runId):p.map(l=>l.runId)),g=f==null?null:Date.now()-f*24*60*60*1e3,h=[];for(const l of p){if(d.has(l.runId))continue;const S=g!=null&&Date.parse(l.finishedAt||l.startedAt||"")<g,I=!R.has(l.runId);if(!S&&!I)continue;const B=[];S&&B.push("max_age"),I&&B.push("max_runs"),h.push({path:u.join(q(e),me,l.runId),reasons:B,runId:l.runId})}const y=new Map(p.map(l=>[l.runId,loadRunBundleManifest(e,l.runId).files.filter(S=>S.path.startsWith("files/")).map(S=>S.sha256)])),v=re(e),j=[],b=[];for(const l of h){let S=null;try{o(l.path)}catch(B){S=H(B)}const I=!c.existsSync(l.path);!I&&S===null&&(S="Run bundle still exists after removal completed"),I&&j.push({reasons:l.reasons,runId:l.runId}),S!==null&&b.push({...l,error:S,removed:I})}const V=new Set(j.map(l=>l.runId)),E=new Set(p.filter(l=>!V.has(l.runId)).flatMap(l=>y.get(l.runId)??[])),_=se(v,E,i),F=new Date().toISOString();return Oe({currentRunId:n,decidedAt:F,garbageCollection:_,policy:{maxRuns:a,maxAgeDays:f,preserveSuites:[...m].sort()},protectedRunIds:[...d].sort(),removed:j,removalErrors:b})}function be({resultsDir:e,tempDir:r,sensitiveValues:t}){let s=0,n=0,i=0,o=0,a=0;const f=u.join(e,Q,"sha256");c.mkdirSync(f,{recursive:!0});for(const m of K){const p=u.join(e,m);if(c.existsSync(p))for(const d of ne(p)){const R=u.relative(p,d),g=c.readFileSync(d),h=Ce(g,d,t);h.scanned&&(i+=1),h.replacements>0&&(o+=1,a+=h.replacements);const y=C(h.buffer),v=u.join(f,y);c.existsSync(v)?n+=h.buffer.length:(ve(v,h.buffer),c.chmodSync(v,292),s+=h.buffer.length);const j=u.join(r,"files",m,R);c.mkdirSync(u.dirname(j),{recursive:!0});try{c.linkSync(v,j)}catch{c.copyFileSync(v,j)}}}return{objectBytesWritten:s,deduplicatedBytes:n,redaction:{policyVersion:1,status:"applied",textualFilesScanned:i,filesRedacted:o,replacements:a,sensitiveEnvironmentKeys:t.map(m=>m.key).sort(),valuesStored:!1}}}function Ie(e,r,t){const s=new Map(K.map(i=>[`.testkit/results/${i}/`,`${P(r,`files/${i}`)}/`])),n=L(e,s);return Ee(we(n),t)}function we(e){if(!$(e))return e;const r={...e};if($(r.product)){const t={...r.product};delete t.directory,r.product=t}if($(r.git)){const t={...r.git};delete t.remoteUrl,delete t.repoRoot,delete t.worktreePath,r.git=t}return delete r.host,r}function L(e,r){if(Array.isArray(e))return e.map(s=>L(s,r));if($(e))return Object.fromEntries(Object.entries(e).map(([s,n])=>[s,L(n,r)]));if(typeof e!="string")return e;let t=e;for(const[s,n]of r)t.startsWith(s)&&(t=`${n}${t.slice(s.length)}`);return t}function Ee(e,r){let t=0;function s(n){if(Array.isArray(n))return n.map(s);if($(n))return Object.fromEntries(Object.entries(n).map(([o,a])=>[o,s(a)]));if(typeof n!="string")return n;let i=Y(n);i!==n&&(t+=1);for(const o of r){if(!i.includes(o.value))continue;const a=i.split(o.value);t+=a.length-1,i=a.join("<redacted>")}return i}return{artifact:s(e),replacements:t}}function Be(e,r,t,s){const n=D(r.run.fileNames),i=[...new Set([...je,...n])].map(o=>Ve(e,o)).filter(o=>o!==null);return{schemaVersion:fe,runId:t,ci:Se(s.env??process.env),source:{git:r.git??null,files:i},runtime:{controller:{testkitVersion:A(r.run.testkitVersion),node:process.versions.node,platform:process.platform,architecture:process.arch},runtimeStats:pe(r.run.runtimeStats)},scenario:{types:D(r.run.types),suiteSelectors:D(r.run.suiteSelectors),fileNames:n,serviceFilter:A(r.run.serviceFilter),planningDigest:C(Buffer.from(W(r.planning??null))),declaredEnvironmentKeys:[...new Set(s.declaredEnvironmentKeys??[])].sort()},redaction:s.redaction}}function Ae(e){return ne(e).map(r=>({path:X(u.relative(e,r)),bytes:c.statSync(r).size,sha256:z(r)})).sort((r,t)=>r.path.localeCompare(t.path))}function T(e,r){const t=u.join(e,"manifest.json"),s=J(O(t),t,r);ae(O(u.join(e,"run.json")),u.join(e,"run.json"),r);const n=[];for(const o of s.files){let a;try{a=U(e,o.path)}catch(m){n.push(k(m));continue}if(!c.existsSync(a)){n.push(`missing ${o.path}`);continue}const f=c.lstatSync(a);if(f.isSymbolicLink()){n.push(`symbolic link not allowed ${o.path}`);continue}if(!f.isFile()){n.push(`not a regular file ${o.path}`);continue}f.size!==o.bytes&&n.push(`size mismatch ${o.path}`),z(a)!==o.sha256&&n.push(`checksum mismatch ${o.path}`)}C(Buffer.from(W(s.files)))!==s.digest&&n.push("manifest digest mismatch"),s.storage.fileCount!==s.files.length&&n.push("manifest file count mismatch");for(const o of["run.json","provenance.json"])s.files.some(a=>a.path===o)||n.push(`manifest must include ${o}`);if(n.length===0){const o=U(e,"provenance.json");oe(O(o),o,r)}return{runId:r,checkedFiles:s.files.length,storage:s.storage,ok:n.length===0,errors:n,manifestDigest:s.digest}}function ke(e,r,t){const s={schemaVersion:1,runId:r,recordedAt:new Date().toISOString(),...t};return N(u.join(q(e),"publications",`${r}.json`),s),s}function $e(e,r){return r.size===0?!1:Z(e.services).some(t=>Z(t.suites).some(s=>{const n=A(t.name)??"",i=A(s.name)??"",o=A(s.testType)??"";return[i,`${o}:${i}`,`${n}/${o}:${i}`].some(f=>r.has(f))}))}function xe(e){const r=new Set;for(const t of listRunBundles(e)){const s=loadRunBundleManifest(e,t.runId);for(const n of s.files)n.path.startsWith("files/")&&r.add(n.sha256)}return se(re(e),r,ie)}function re(e){const r=u.join(q(e),Q,"sha256");return c.existsSync(r)?c.readdirSync(r,{withFileTypes:!0}).filter(t=>t.isFile()).map(t=>{const s=u.join(r,t.name);return{bytes:c.statSync(s).size,digest:t.name,path:s}}):[]}function se(e,r,t){const s=[];let n=0,i=0;for(const o of e){if(r.has(o.digest))continue;let a=null;try{t(o.path)}catch(m){a=H(m)}!c.existsSync(o.path)?(n+=1,i+=o.bytes):a===null&&(a="Object still exists after removal completed"),a!==null&&s.push({error:a,path:o.path})}return{failures:s,reclaimedBytes:i,removedObjects:n,status:s.length===0?"complete":"failed"}}function Pe(e){c.rmSync(e,{recursive:!0,force:!0})}function ie(e){c.rmSync(e,{force:!0})}function Oe({currentRunId:e,decidedAt:r,garbageCollection:t,policy:s,protectedRunIds:n,removed:i,removalErrors:o}){const a=[...o.map(f=>`remove ${f.runId}: ${f.error}`),...t.failures.map(f=>`garbage collect ${f.path}: ${f.error}`)];return{currentRunId:e,decidedAt:r,garbageCollection:t,policy:s,protectedRunIds:n,removed:i,removalErrors:o,schemaVersion:de,status:a.length===0?"complete":"failed",...a.length===0?{}:{error:a.join("; ")}}}function Ce(e,r,t){if(!Te(e,r)){for(const o of t)if(e.includes(Buffer.from(o.value)))throw new Error(`Refusing to archive binary file containing sensitive environment value: ${r}`);return{buffer:e,scanned:!1,replacements:0}}const s=e.toString("utf8");let n=Y(s),i=n===s?0:1;for(const o of t){if(!n.includes(o.value))continue;const a=n.split(o.value);i+=a.length-1,n=a.join("<redacted>")}return{buffer:Buffer.from(n),scanned:!0,replacements:i}}function Ne(e){const r=[];for(const[t,s]of Object.entries(e))Re.test(t)&&typeof s=="string"&&s.length>=8&&r.push({key:t,value:s});return r}function Te(e,r){if(e.includes(0))return!1;const t=u.extname(r).toLowerCase();return![".zip",".gz",".png",".jpg",".jpeg",".webm",".pdf"].includes(t)}function Ve(e,r){const t=u.resolve(e,r);return!t.startsWith(`${u.resolve(e)}${u.sep}`)||!c.existsSync(t)||!c.statSync(t).isFile()?null:{path:X(u.relative(e,t)),bytes:c.statSync(t).size,sha256:z(t)}}function _e(e){const r=[];for(const t of[...K.map(s=>u.join(e,s)),u.join(e,"live.json")])try{c.rmSync(t,{recursive:!0,force:!0})}catch(s){r.push(`${t}: ${H(s)}`)}return r.length===0?{status:"complete"}:{error:r.join("; "),status:"failed"}}function H(e){try{return k(e)}catch{return"Unknown post-seal archive operation failure"}}
|
|
1
|
+
import{recordWork as J}from"../kernel/work-metrics.js";import{optionalString as A}from"../kernel/unknown-values.js";import{readRunProvenance as ue}from"./read-provenance.js";import{parseRunBundleArtifact as le,parseRunBundleManifest as Y,readRunBundleArtifact as fe}from"./read-bundle.js";import{readRunDocument as de}from"./read-run.js";import me from"crypto";import c from"fs";import u from"path";import{redactSensitiveText as G}from"../kernel/redaction.js";import{BUNDLE_SCHEMA_VERSION as Q,OBJECTS_DIRNAME as X,PROVENANCE_SCHEMA_VERSION as pe,RETENTION_DECISION_SCHEMA_VERSION as he,RUNS_DIRNAME as ge}from"./contracts.js";import{bundleRelativePath as P,errorMessage as k,hashFile as z,isObjectRecord as $,normalizePath as Z,normalizeRunId as I,objectRecords as ee,optionalStringArray as D,optionalUnknownArray as ye,readJsonFile as O,requirePublicationPathOutsideResults as M,requireRunId as ve,runIncludesFile as Se,resolveManifestEntryPath as U,resolveRunBundleDirectory as x,resolveRunsRoot as te,resultsRoot as q,sanitizeSegment as Re,sha256 as C,stableJson as W,treeBytes as ne,walkFiles as re,writeBufferAtomic as je,writeJson as N}from"./storage.js";import{resolveCiProvenance as be}from"./ci-provenance.js";const K=["artifacts","logs","setup"],we=/(?:PASSWORD|TOKEN|SECRET|PRIVATE_KEY|AUTH|COOKIE|SESSION|API_KEY)/iu,Ie=["package.json","package-lock.json","npm-shrinkwrap.json","pnpm-lock.yaml","yarn.lock","testkit.config.ts","testkit.config.mts","testkit.config.js","testkit.config.mjs"];export function sealRunBundle(e,n,t={},s={}){const r=n,o=ve(r),i=te(e,!0),a=u.dirname(i),f=u.join(i,o),m=s.cleanupActiveRunResults??De,p=loadVerifiedRunBundleArtifactIfExists(e,o);if(p!==null)return{...p,archive:{...p.archive,cleanup:m(a)}};const d=u.join(i,`.${o}.${String(process.pid)}.${String(Date.now())}.${me.randomBytes(4).toString("hex")}.tmp`);c.mkdirSync(d,{recursive:!0});try{const R=_e(t.env??process.env),g=Ee({productDir:e,resultsDir:a,runId:o,tempDir:d,sensitiveValues:R}),h=Be(r,o,R),y=de(h.artifact,"run artifact");g.redaction.structuredReplacements=h.replacements,g.redaction.replacements+=h.replacements;const v=$e(e,y,o,{...t,redaction:g.redaction}),j={schemaVersion:Q,runId:o,status:"complete",canonicalRunPath:P(o,"run.json"),manifestPath:P(o,"manifest.json"),provenancePath:P(o,"provenance.json"),integrity:"sha256",publication:{configured:!!t.archiveConfig?.publication,status:t.archiveConfig?.publication?"pending":"not_configured"},redaction:g.redaction};y.archive=j,N(u.join(d,"run.json"),y),N(u.join(d,"provenance.json"),v);const b=xe(d),T=C(Buffer.from(W(b))),E={schemaVersion:Q,runId:o,createdAt:new Date().toISOString(),algorithm:"sha256",files:b,storage:{fileCount:b.length,logicalBytes:b.reduce((_,F)=>_+F.bytes,0),objectBytesWritten:g.objectBytesWritten,deduplicatedBytes:g.deduplicatedBytes},redaction:g.redaction,digest:T};return N(u.join(d,"manifest.json"),E),c.renameSync(d,f),{...y,archive:{...j,cleanup:m(a),manifestDigest:E.digest,storage:E.storage}}}catch(R){try{c.rmSync(d,{recursive:!0,force:!0})}catch{}try{Ce(e)}catch{}throw R}}export function loadVerifiedRunBundleArtifactIfExists(e,n){const t=I(n),s=x(e,t);if(!c.existsSync(s))return null;const r=verifyRunBundle(e,t);if(!r.ok)throw new Error(`Immutable run bundle ${t} already exists but failed integrity verification: ${r.errors.join("; ")}`);return loadRunBundleArtifact(e,t)}export function loadRunBundleArtifact(e,n){return se(e,n).artifact}function se(e,n){const t=I(n),s=fe(e,t),r=loadRunBundleManifest(e,t);return{manifest:r,artifact:{...s,archive:{...s.archive,manifestDigest:r.digest,storage:r.storage}}}}export function loadRunBundleManifest(e,n){const t=I(n),s=x(e,t),r=u.join(s,"manifest.json");if(!c.existsSync(r))throw new Error(`No immutable Testkit run manifest found for ${t}`);return Y(O(r),r,t)}export function listRunBundles(e,n={}){return ie(e,n).map(({artifact:t,manifest:s,...r})=>r)}function ie(e,n={}){const t=te(e,!1);if(!c.existsSync(t))return[];const s=c.readdirSync(t,{withFileTypes:!0});return J("artifacts.runDirectoriesRead",1),J("artifacts.runEntriesVisited",s.length),s.filter(o=>o.isDirectory()&&!o.name.startsWith(".")).map(o=>{try{const{artifact:i,manifest:a}=se(e,o.name);return n.file&&!Se(i,n.file)?null:{artifact:i,manifest:a,runId:o.name,status:i.run.status,startedAt:i.run.startedAt,finishedAt:i.run.finishedAt??null,manifestDigest:i.archive.manifestDigest??null,canonicalRunPath:i.archive.canonicalRunPath}}catch{return null}}).filter(o=>o!==null).sort((o,i)=>(i.finishedAt??i.startedAt??"").localeCompare(o.finishedAt??o.startedAt??""))}export function verifyRunBundle(e,n){const t=I(n);try{return V(x(e,t),t)}catch(s){return{ok:!1,runId:t,errors:[k(s)],checkedFiles:0}}}export function prepareVerifiedRunBundleUpload(e,n=null){const t=n??listRunBundles(e).at(0)?.runId??null;if(t===null)throw new Error("No immutable Testkit run bundle is available to publish");const s=verifyRunBundle(e,t);if(!s.ok)throw new Error(`Immutable run bundle ${t} failed integrity verification: ${s.errors.join("; ")}`);const r=loadRunBundleManifest(e,t),o=x(e,t);return{runId:t,manifest:r,files:r.files.map(i=>({...i,absolutePath:U(o,i.path)}))}}export function publishRunBundle(e,n,t,s={}){const r=I(n),o=s.writePublicationReceipt??Pe;if(!t)return o(e,r,{status:"not_configured",required:!1,target:null,bytesUploaded:0});const i=u.resolve(e,t.directory);try{M(e,i)}catch(d){return o(e,r,{status:"failed",required:t.required,target:i,error:k(d),bytesUploaded:0})}const a=Re(u.basename(e)),f=u.join(i,a),m=u.join(f,r),p=u.join(f,`.${r}.${String(process.pid)}.${String(Date.now())}.tmp`);try{const d=x(e,r);if(c.mkdirSync(f,{recursive:!0}),M(e,f),c.existsSync(m)){const h=c.lstatSync(m);if(h.isSymbolicLink()||!h.isDirectory())throw new Error("Publication target must be a regular bundle directory");M(e,m);const y=V(d,r);if(!y.ok)throw new Error(`Source run bundle failed integrity verification: ${y.errors.join("; ")}`);const v=V(m,r);if(!v.ok)throw new Error(`Publication target failed integrity verification: ${v.errors.join("; ")}`);if(y.manifestDigest!==v.manifestDigest)throw new Error("Publication target already exists with a different manifest digest");return o(e,r,{status:"complete",required:t.required,target:m,manifestDigest:y.manifestDigest,reused:!0,bundleBytes:ne(d),bytesUploaded:0})}const R=ne(d);c.cpSync(d,p,{recursive:!0,preserveTimestamps:!0});const g=V(p,r);if(!g.ok)throw new Error(g.errors.join("; "));return c.renameSync(p,m),o(e,r,{status:"complete",required:t.required,target:m,manifestDigest:g.manifestDigest,reused:!1,bundleBytes:R,bytesUploaded:R})}catch(d){return c.rmSync(p,{recursive:!0,force:!0}),o(e,r,{status:"failed",required:t.required,target:m,error:k(d),bytesUploaded:0})}}export function applyRunBundleRetention(e,n,t={},s={}){const r=I(n),o=s.removeObject??ce,i=s.removeRunBundle??Ne,a=t.maxRuns??null,f=t.maxAgeDays??null,m=new Set(t.preserveSuites||[]),p=ie(e),d=new Set([r]);for(const l of p)Oe(l.artifact,m)&&d.add(l.runId);const R=new Set(a?p.slice(0,a).map(l=>l.runId):p.map(l=>l.runId)),g=f==null?null:Date.now()-f*24*60*60*1e3,h=[];for(const l of p){if(d.has(l.runId))continue;const S=g!=null&&Date.parse(l.finishedAt||l.startedAt||"")<g,w=!R.has(l.runId);if(!S&&!w)continue;const B=[];S&&B.push("max_age"),w&&B.push("max_runs"),h.push({path:u.join(q(e),ge,l.runId),reasons:B,runId:l.runId})}const y=new Map(p.map(l=>[l.runId,l.manifest.files.filter(S=>S.path.startsWith("files/")).map(S=>S.sha256)])),v=oe(e),j=[],b=[];for(const l of h){let S=null;try{i(l.path)}catch(B){S=H(B)}const w=!c.existsSync(l.path);!w&&S===null&&(S="Run bundle still exists after removal completed"),w&&j.push({reasons:l.reasons,runId:l.runId}),S!==null&&b.push({...l,error:S,removed:w})}const T=new Set(j.map(l=>l.runId)),E=new Set(p.filter(l=>!T.has(l.runId)).flatMap(l=>y.get(l.runId)??[])),_=ae(v,E,o),F=new Date().toISOString();return Ve({currentRunId:r,decidedAt:F,garbageCollection:_,policy:{maxRuns:a,maxAgeDays:f,preserveSuites:[...m].sort()},protectedRunIds:[...d].sort(),removed:j,removalErrors:b})}function Ee({resultsDir:e,tempDir:n,sensitiveValues:t}){let s=0,r=0,o=0,i=0,a=0;const f=u.join(e,X,"sha256");c.mkdirSync(f,{recursive:!0});for(const m of K){const p=u.join(e,m);if(c.existsSync(p))for(const d of re(p)){const R=u.relative(p,d),g=c.readFileSync(d),h=Te(g,d,t);h.scanned&&(o+=1),h.replacements>0&&(i+=1,a+=h.replacements);const y=C(h.buffer),v=u.join(f,y);c.existsSync(v)?r+=h.buffer.length:(je(v,h.buffer),c.chmodSync(v,292),s+=h.buffer.length);const j=u.join(n,"files",m,R);c.mkdirSync(u.dirname(j),{recursive:!0});try{c.linkSync(v,j)}catch{c.copyFileSync(v,j)}}}return{objectBytesWritten:s,deduplicatedBytes:r,redaction:{policyVersion:1,status:"applied",textualFilesScanned:o,filesRedacted:i,replacements:a,sensitiveEnvironmentKeys:t.map(m=>m.key).sort(),valuesStored:!1}}}function Be(e,n,t){const s=new Map(K.map(o=>[`.testkit/results/${o}/`,`${P(n,`files/${o}`)}/`])),r=L(e,s);return ke(Ae(r),t)}function Ae(e){if(!$(e))return e;const n={...e};if($(n.product)){const t={...n.product};delete t.directory,n.product=t}if($(n.git)){const t={...n.git};delete t.remoteUrl,delete t.repoRoot,delete t.worktreePath,n.git=t}return delete n.host,n}function L(e,n){if(Array.isArray(e))return e.map(s=>L(s,n));if($(e))return Object.fromEntries(Object.entries(e).map(([s,r])=>[s,L(r,n)]));if(typeof e!="string")return e;let t=e;for(const[s,r]of n)t.startsWith(s)&&(t=`${r}${t.slice(s.length)}`);return t}function ke(e,n){let t=0;function s(r){if(Array.isArray(r))return r.map(s);if($(r))return Object.fromEntries(Object.entries(r).map(([i,a])=>[i,s(a)]));if(typeof r!="string")return r;let o=G(r);o!==r&&(t+=1);for(const i of n){if(!o.includes(i.value))continue;const a=o.split(i.value);t+=a.length-1,o=a.join("<redacted>")}return o}return{artifact:s(e),replacements:t}}function $e(e,n,t,s){const r=D(n.run.fileNames),o=[...new Set([...Ie,...r])].map(i=>ze(e,i)).filter(i=>i!==null);return{schemaVersion:pe,runId:t,ci:be(s.env??process.env),source:{git:n.git??null,files:o},runtime:{controller:{testkitVersion:A(n.run.testkitVersion),node:process.versions.node,platform:process.platform,architecture:process.arch},runtimeStats:ye(n.run.runtimeStats)},scenario:{types:D(n.run.types),suiteSelectors:D(n.run.suiteSelectors),fileNames:r,serviceFilter:A(n.run.serviceFilter),planningDigest:C(Buffer.from(W(n.planning??null))),declaredEnvironmentKeys:[...new Set(s.declaredEnvironmentKeys??[])].sort()},redaction:s.redaction}}function xe(e){return re(e).map(n=>({path:Z(u.relative(e,n)),bytes:c.statSync(n).size,sha256:z(n)})).sort((n,t)=>n.path.localeCompare(t.path))}function V(e,n){const t=u.join(e,"manifest.json"),s=Y(O(t),t,n);le(O(u.join(e,"run.json")),u.join(e,"run.json"),n);const r=[];for(const i of s.files){let a;try{a=U(e,i.path)}catch(m){r.push(k(m));continue}if(!c.existsSync(a)){r.push(`missing ${i.path}`);continue}const f=c.lstatSync(a);if(f.isSymbolicLink()){r.push(`symbolic link not allowed ${i.path}`);continue}if(!f.isFile()){r.push(`not a regular file ${i.path}`);continue}f.size!==i.bytes&&r.push(`size mismatch ${i.path}`),z(a)!==i.sha256&&r.push(`checksum mismatch ${i.path}`)}C(Buffer.from(W(s.files)))!==s.digest&&r.push("manifest digest mismatch"),s.storage.fileCount!==s.files.length&&r.push("manifest file count mismatch");for(const i of["run.json","provenance.json"])s.files.some(a=>a.path===i)||r.push(`manifest must include ${i}`);if(r.length===0){const i=U(e,"provenance.json");ue(O(i),i,n)}return{runId:n,checkedFiles:s.files.length,storage:s.storage,ok:r.length===0,errors:r,manifestDigest:s.digest}}function Pe(e,n,t){const s={schemaVersion:1,runId:n,recordedAt:new Date().toISOString(),...t};return N(u.join(q(e),"publications",`${n}.json`),s),s}function Oe(e,n){return n.size===0?!1:ee(e.services).some(t=>ee(t.suites).some(s=>{const r=A(t.name)??"",o=A(s.name)??"",i=A(s.testType)??"";return[o,`${i}:${o}`,`${r}/${i}:${o}`].some(f=>n.has(f))}))}function Ce(e){const n=new Set;for(const t of listRunBundles(e)){const s=loadRunBundleManifest(e,t.runId);for(const r of s.files)r.path.startsWith("files/")&&n.add(r.sha256)}return ae(oe(e),n,ce)}function oe(e){const n=u.join(q(e),X,"sha256");return c.existsSync(n)?c.readdirSync(n,{withFileTypes:!0}).filter(t=>t.isFile()).map(t=>{const s=u.join(n,t.name);return{bytes:c.statSync(s).size,digest:t.name,path:s}}):[]}function ae(e,n,t){const s=[];let r=0,o=0;for(const i of e){if(n.has(i.digest))continue;let a=null;try{t(i.path)}catch(m){a=H(m)}!c.existsSync(i.path)?(r+=1,o+=i.bytes):a===null&&(a="Object still exists after removal completed"),a!==null&&s.push({error:a,path:i.path})}return{failures:s,reclaimedBytes:o,removedObjects:r,status:s.length===0?"complete":"failed"}}function Ne(e){c.rmSync(e,{recursive:!0,force:!0})}function ce(e){c.rmSync(e,{force:!0})}function Ve({currentRunId:e,decidedAt:n,garbageCollection:t,policy:s,protectedRunIds:r,removed:o,removalErrors:i}){const a=[...i.map(f=>`remove ${f.runId}: ${f.error}`),...t.failures.map(f=>`garbage collect ${f.path}: ${f.error}`)];return{currentRunId:e,decidedAt:n,garbageCollection:t,policy:s,protectedRunIds:r,removed:o,removalErrors:i,schemaVersion:he,status:a.length===0?"complete":"failed",...a.length===0?{}:{error:a.join("; ")}}}function Te(e,n,t){if(!Fe(e,n)){for(const i of t)if(e.includes(Buffer.from(i.value)))throw new Error(`Refusing to archive binary file containing sensitive environment value: ${n}`);return{buffer:e,scanned:!1,replacements:0}}const s=e.toString("utf8");let r=G(s),o=r===s?0:1;for(const i of t){if(!r.includes(i.value))continue;const a=r.split(i.value);o+=a.length-1,r=a.join("<redacted>")}return{buffer:Buffer.from(r),scanned:!0,replacements:o}}function _e(e){const n=[];for(const[t,s]of Object.entries(e))we.test(t)&&typeof s=="string"&&s.length>=8&&n.push({key:t,value:s});return n}function Fe(e,n){if(e.includes(0))return!1;const t=u.extname(n).toLowerCase();return![".zip",".gz",".png",".jpg",".jpeg",".webm",".pdf"].includes(t)}function ze(e,n){const t=u.resolve(e,n);return!t.startsWith(`${u.resolve(e)}${u.sep}`)||!c.existsSync(t)||!c.statSync(t).isFile()?null:{path:Z(u.relative(e,t)),bytes:c.statSync(t).size,sha256:z(t)}}function De(e){const n=[];for(const t of[...K.map(s=>u.join(e,s)),u.join(e,"live.json")])try{c.rmSync(t,{recursive:!0,force:!0})}catch(s){n.push(`${t}: ${H(s)}`)}return n.length===0?{status:"complete"}:{error:n.join("; "),status:"failed"}}function H(e){try{return k(e)}catch{return"Unknown post-seal archive operation failure"}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{isEngineId as w,isPublicTestType as q,isRunnerProfileId as S}from"../domain/identifiers.js";import{isEngineProfileIdentity as T}from"../engines/
|
|
1
|
+
import{isEngineId as w,isPublicTestType as q,isRunnerProfileId as S}from"../domain/identifiers.js";import{isEngineProfileIdentity as T}from"../engines/definitions.js";import{requireObjectRecord as r,requireNumber as m,requireStringArray as B,optionalBoolean as y,optionalNonNegativeNumber as p,isObjectRecord as E,requireUnknownArray as h,requireString as c,requireResultsReferencePath as k,optionalStringValue as s,optionalNullableStringValue as a,optionalNumberOrStringValue as L,optionalNullableNumberValue as v}from"./read-fields.js";export function readRunDocument(n,t){const e=r(n,t);if(e.schemaVersion!==19)throw new Error(`${t}.schemaVersion must be 19`);const i=M(e.run,`${t}.run`),o=f(e.services,`${t}.services`,P)??[];e.archive!==void 0&&r(e.archive,`${t}.archive`),e.finalizationFailure!==void 0&&X(e.finalizationFailure,`${t}.finalizationFailure`),s(e.generatedAt,`${t}.generatedAt`);const d=K(e.summary,`${t}.summary`),u=e.logs===void 0?void 0:x(e.logs,`${t}.logs`),g=e.setup===void 0?void 0:V(e.setup,`${t}.setup`);return{...e,schemaVersion:19,...d===void 0?{}:{summary:d},run:i,services:o,...u===void 0?{}:{logs:u},...g===void 0?{}:{setup:g}}}function M(n,t){const e=r(n,t);return v(e.durationMs,`${t}.durationMs`),f(e.types,`${t}.types`,c),{...e,finishedAt:a(e.finishedAt,`${t}.finishedAt`)??null,id:a(e.id,`${t}.id`)??null,startedAt:c(e.startedAt,`${t}.startedAt`),status:c(e.status,`${t}.status`)}}function x(n,t){const e=r(n,t),i=f(e.services,`${t}.services`,H),o=f(e.setup,`${t}.setup`,F);return{...e,...i===void 0?{}:{services:i},...o===void 0?{}:{setup:o}}}function V(n,t){const e=r(n,t),i=f(e.operations,`${t}.operations`,J);return{...e,...i===void 0?{}:{operations:i}}}function P(n,t){const e=r(n,t),i=f(e.suites,`${t}.suites`,D)??[];return{...e,name:c(e.name,`${t}.name`),suites:i}}function D(n,t){const e=r(n,t);if(e.testType!=="scenario"&&!q(e.testType))throw new Error(`${t}.testType must be a public test type`);if(!w(e.engineId))throw new Error(`${t}.engineId must be a supported engine id`);if(!S(e.profileId))throw new Error(`${t}.profileId must be a supported runner profile id`);if(!(e.testType==="scenario"?e.engineId==="k6"&&e.profileId==="k6-http":T(e)))throw new Error(`${t}.profileId does not match ${e.engineId}/${e.testType}`);const o=f(e.files,`${t}.files`,O);return y(e.failed,`${t}.failed`),A(e.diagnostics,`${t}.diagnostics`),p(e.completedFileCount,`${t}.completedFileCount`),p(e.failedFileCount,`${t}.failedFileCount`),p(e.fileCount,`${t}.fileCount`),p(e.passedFileCount,`${t}.passedFileCount`),p(e.skippedFileCount,`${t}.skippedFileCount`),{...e,files:o,name:c(e.name,`${t}.name`),testType:e.testType,engineId:e.engineId,profileId:e.profileId}}function O(n,t){const e=r(n,t),i=f(e.artifacts,`${t}.artifacts`,j),o=f(e.failureDetails,`${t}.failureDetails`,z);return y(e.blockedByRuntime,`${t}.blockedByRuntime`),y(e.failed,`${t}.failed`),y(e.infraFailed,`${t}.infraFailed`),a(e.reason,`${t}.reason`),a(e.runtimeIsolation,`${t}.runtimeIsolation`),e.runtimeBatch!=null&&Q(e.runtimeBatch,`${t}.runtimeBatch`),f(e.runtimeResets,`${t}.runtimeResets`,W),{...e,error:a(e.error,`${t}.error`),artifacts:i,durationMs:v(e.durationMs,`${t}.durationMs`),failureDetails:o,path:c(e.path,`${t}.path`),status:c(e.status,`${t}.status`)}}function j(n,t){const e=r(n,t);return{...e,contentType:a(e.contentType,`${t}.contentType`),kind:a(e.kind,`${t}.kind`),summary:a(e.summary,`${t}.summary`),name:c(e.name,`${t}.name`),path:k(e.path,`${t}.path`)}}function z(n,t){const e=r(n,t),i=e.request===void 0?void 0:U(e.request,`${t}.request`),o=e.response===void 0?void 0:G(e.response,`${t}.response`),d=e.location===void 0?void 0:r(e.location,`${t}.location`);return d!==void 0&&(s(d.path,`${t}.location.path`),d.line!==void 0&&m(d.line,`${t}.location.line`),d.column!==void 0&&m(d.column,`${t}.location.column`)),{...e,key:s(e.key,`${t}.key`),kind:s(e.kind,`${t}.kind`),message:s(e.message,`${t}.message`),stack:s(e.stack,`${t}.stack`),location:d,request:i,response:o,title:c(e.title,`${t}.title`)}}function U(n,t){const e=r(n,t);return{...e,method:s(e.method,`${t}.method`),path:s(e.path,`${t}.path`),requestId:s(e.requestId,`${t}.requestId`)}}function G(n,t){const e=r(n,t);return{...e,bodyPreview:s(e.bodyPreview,`${t}.bodyPreview`),contentType:s(e.contentType,`${t}.contentType`),status:L(e.status,`${t}.status`)}}function H(n,t){const e=r(n,t);return{...e,path:k(e.path,`${t}.path`),runtimeLabel:c(e.runtimeLabel,`${t}.runtimeLabel`),serviceName:c(e.serviceName,`${t}.serviceName`)}}function J(n,t){const e=r(n,t),i=e.logRef==null?e.logRef:F(e.logRef,`${t}.logRef`);return{...e,durationMs:v(e.durationMs,`${t}.durationMs`),error:a(e.error,`${t}.error`),summary:a(e.summary,`${t}.summary`),logRef:i,serviceName:c(e.serviceName,`${t}.serviceName`),stage:c(e.stage,`${t}.stage`),startedAt:s(e.startedAt,`${t}.startedAt`),status:c(e.status,`${t}.status`)}}function F(n,t){const e=r(n,t);return s(e.runtimeLabel,`${t}.runtimeLabel`),s(e.serviceName,`${t}.serviceName`),s(e.stage,`${t}.stage`),{...e,path:k(e.path,`${t}.path`)}}function K(n,t){if(n===void 0)return;const e=r(n,t),i=e.files===void 0?{}:r(e.files,`${t}.files`),o=e.services===void 0?{}:r(e.services,`${t}.services`),d=e.suites===void 0?{}:r(e.suites,`${t}.suites`),u=(g,I,N)=>{const $=g[I];if($===void 0)return 0;if(typeof $!="number"||!Number.isFinite($)||$<0)throw new Error(`${t}.${N}.${I} must be a non-negative finite number`);return $};return A(e.diagnostics,`${t}.diagnostics`),{...e,files:{blockedByRuntime:u(i,"blockedByRuntime","files"),cancelled:u(i,"cancelled","files"),failed:u(i,"failed","files"),infraFailed:u(i,"infraFailed","files"),interrupted:u(i,"interrupted","files"),notRun:u(i,"notRun","files"),passed:u(i,"passed","files"),skipped:u(i,"skipped","files"),total:u(i,"total","files")},services:{blockedByRuntime:u(o,"blockedByRuntime","services"),failed:u(o,"failed","services"),infraFailed:u(o,"infraFailed","services"),passed:u(o,"passed","services"),skipped:u(o,"skipped","services"),total:u(o,"total","services")},suites:{completed:u(d,"completed","suites"),failed:u(d,"failed","suites"),passed:u(d,"passed","suites"),skipped:u(d,"skipped","suites"),total:u(d,"total","suites")}}}function A(n,t){if(n!==void 0)return h(n,t).map((e,i)=>{const o=`${t}[${String(i)}]`,d=r(e,o);return{...d,code:c(d.code,`${o}.code`),message:c(d.message,`${o}.message`),service:s(d.service,`${o}.service`),suite:s(d.suite,`${o}.suite`)}})}function Q(n,t){const e=r(n,t);return{...e,id:s(e.id,`${t}.id`),fileCount:p(e.fileCount,`${t}.fileCount`),index:p(e.index,`${t}.index`),files:e.files===void 0?void 0:B(e.files,`${t}.files`)}}function W(n,t){const e=r(n,t);return{...e,position:s(e.position,`${t}.position`),reason:s(e.reason,`${t}.reason`),runtimeIsolation:s(e.runtimeIsolation,`${t}.runtimeIsolation`),trigger:s(e.trigger,`${t}.trigger`)}}function X(n,t){const e=r(n,t),i=e.operation===void 0?void 0:r(e.operation,`${t}.operation`);return{...e,name:c(e.name,`${t}.name`),message:c(e.message,`${t}.message`),code:s(e.code,`${t}.code`),stack:s(e.stack,`${t}.stack`),phase:s(e.phase,`${t}.phase`),exitCode:e.exitCode===void 0?void 0:m(e.exitCode,`${t}.exitCode`),status:e.status===void 0?void 0:m(e.status,`${t}.status`),diagnostics:e.diagnostics===void 0?void 0:R(e.diagnostics,`${t}.diagnostics`),operation:i===void 0?void 0:{...i,id:s(i.id,`${t}.operation.id`),source:c(i.source,`${t}.operation.source`),name:c(i.name,`${t}.operation.name`),kind:c(i.kind,`${t}.operation.kind`),summary:s(i.summary,`${t}.operation.summary`),status:s(i.status,`${t}.operation.status`),error:s(i.error,`${t}.operation.error`),scope:R(i.scope,`${t}.operation.scope`)}}}function R(n,t){return Object.fromEntries(Object.entries(r(n,t)).map(([e,i])=>[e,C(i,`${t}.${e}`)]))}function C(n,t){return n===null||typeof n=="string"||typeof n=="boolean"?n:Array.isArray(n)?h(n,t).map((e,i)=>C(e,`${t}[${String(i)}]`)):E(n)?R(n,t):m(n,t)}function f(n,t,e){return n===void 0?void 0:h(n,t).map((i,o)=>e(i,`${t}[${String(o)}]`))}
|
|
@@ -24,5 +24,5 @@ export declare function loadLatestRunArtifact(productDir: string): RunArtifactDo
|
|
|
24
24
|
export declare function loadCurrentRunArtifact(productDir: string): RunArtifactDocument;
|
|
25
25
|
export declare function loadRunArtifact(productDir: string, runId?: null | string): RunArtifactDocument;
|
|
26
26
|
export declare function resolveFileSubject(runArtifact: RunArtifactDocument, selector?: null | string, serviceFilter?: null | string): RunArtifactFileSubject;
|
|
27
|
-
export declare function collectArtifactEntries(productDir: string, runArtifact: RunArtifactDocument, selector?: null | string, serviceFilter?: null | string): CollectedArtifactEntry[];
|
|
27
|
+
export declare function collectArtifactEntries(productDir: string, runArtifact: RunArtifactDocument, selector?: null | string, serviceFilter?: null | string, kindFilter?: string, includePayload?: boolean): CollectedArtifactEntry[];
|
|
28
28
|
export {};
|
package/dist/evidence/reading.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import
|
|
1
|
+
import{isWorkObserved as k,recordWork as h}from"../kernel/work-metrics.js";import a from"fs";import l from"path";import{normalizeEngineJsonValue as S}from"../engines/api/index.js";import{readJsonFile as p,normalizePath as g}from"./storage.js";import{loadRunBundleArtifact as P}from"./bundles.js";import{readRunDocument as y}from"./read-run.js";import{requireObjectRecord as R,requireResultsReferencePath as A,optionalStringValue as x,optionalNullableStringValue as f}from"./read-fields.js";export function loadLatestRunArtifact(r){const t=l.join(r,".testkit","results","latest.json");if(!a.existsSync(t))throw new Error(`No run artifact found at ${l.relative(r,t)}`);return y(p(t),t)}export function loadCurrentRunArtifact(r){const t=l.join(r,".testkit","results","live.json");return a.existsSync(t)?y(p(t),t):loadLatestRunArtifact(r)}export function loadRunArtifact(r,t=null){return t?P(r,t):loadLatestRunArtifact(r)}export function resolveFileSubject(r,t=null,e=null){const n=w(r,e);if(n.length===0)throw new Error("No file results were found in the latest run artifact.");if(!t)return n.find(s=>s.file.status==="failed")||n[0];const o=g(t),i=n.filter(s=>s.file.path===o);if(i.length===1)return i[0];if(i.length>1)throw new Error(`Multiple files matched "${t}". Re-run with --service to disambiguate.`);const u=n.filter(s=>s.file.path.endsWith(`/${o}`));if(u.length===1)return u[0];throw u.length>1?new Error(`Multiple files matched "${t}". Re-run with --service to disambiguate.`):new Error(`No file matched "${t}".`)}export function collectArtifactEntries(r,t,e=null,n=null,o,i=!0){const u=e?[resolveFileSubject(t,e,n)]:w(t,n),s=[];for(const d of u)for(const c of d.file.artifacts||[]){if(o&&c.kind!==o)continue;const m=T(r,c.path,`artifact "${c.name}" path`),$=i&&a.existsSync(m)?E(m,c):null;s.push({...d,artifactRef:c,absolutePath:m,payload:$})}return s}function E(r,t){let e;if(k()){const o=a.readFileSync(r);h("artifacts.payloadFilesRead",1),h("artifacts.payloadBytesRead",o.byteLength),e=o.toString("utf8")}else e=a.readFileSync(r,"utf8");const n=t.contentType||"";if(n==="text/markdown"||n==="text/plain")return{contentType:n,kind:t.kind||null,name:t.name||null,summary:t.summary||null,text:e};try{const o=JSON.parse(e);return j(o,r)}catch{return{contentType:n||"text/plain",kind:t.kind||null,name:t.name||null,summary:t.summary||null,text:e}}}function w(r,t=null){const e=[];for(const n of r.services)if(!(t&&n.name!==t))for(const o of n.suites)for(const i of o.files||[])e.push({service:n,suite:o,file:i});return e}function j(r,t){const e=R(r,t),n=x(e.emittedAt,`${t}.emittedAt`);return{...e,contentType:f(e.contentType,`${t}.contentType`),...e.data===void 0?{}:{data:S(e.data,`${t}.data`)},...n===void 0?{}:{emittedAt:n},kind:f(e.kind,`${t}.kind`),name:f(e.name,`${t}.name`),summary:f(e.summary,`${t}.summary`),text:x(e.text,`${t}.text`)}}function T(r,t,e){const n=A(t,e),o=l.resolve(r,".testkit","results"),i=l.resolve(r,n);if(v(o,i,e),a.existsSync(i)){const u=a.realpathSync(o),s=a.realpathSync(i);v(u,s,e)}return i}function v(r,t,e){const n=l.relative(r,t);if(n.length===0||n===".."||n.startsWith(`..${l.sep}`)||l.isAbsolute(n))throw new Error(`${e} must resolve within .testkit/results`)}
|
package/dist/evidence/storage.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{isPathWithin as
|
|
2
|
-
`)}export function hashFile(r){
|
|
1
|
+
import{isWorkObserved as f,recordWork as l}from"../kernel/work-metrics.js";import{isPathWithin as p}from"../process/files/paths.js";import x from"crypto";import i from"fs";import o from"path";import{RUNS_DIRNAME as m}from"./contracts.js";import{isObjectRecord as w,requireManifestRelativePath as v}from"./read-fields.js";export{isObjectRecord}from"./read-fields.js";export function readJsonFile(r){try{let t;if(f()){const n=i.readFileSync(r);l("json.filesRead",1),l("json.bytesRead",n.byteLength),t=n.toString("utf8")}else t=i.readFileSync(r,"utf8");return JSON.parse(t)}catch(t){throw new Error(`Invalid JSON in ${r}: ${errorMessage(t)}`,{cause:t})}}export function runIncludesFile(r,t){const e=normalizePath(t);return r.services.some(n=>n.suites.some(u=>u.files?.some(s=>s.path===e||s.path.endsWith(e))))}export function requireRunId(r){const t=r.run.id==null?null:normalizeRunId(r.run.id);if(t===null)throw new Error("Cannot seal a run bundle without a run id");return t}export function normalizeRunId(r){const t=typeof r=="string"?r:"";if(!t||t==="."||t===".."||t.includes("\0")||t.includes("/")||t.includes("\\"))throw new Error(`Invalid Testkit run id: ${t||"<empty>"}`);return t}export function walkFiles(r){if(!i.existsSync(r))return[];const t=[];for(const e of i.readdirSync(r,{withFileTypes:!0})){const n=o.join(r,e.name);e.isDirectory()?t.push(...walkFiles(n)):e.isFile()&&t.push(n)}return t}export function treeBytes(r){return walkFiles(r).reduce((t,e)=>t+i.statSync(e).size,0)}export function writeBufferAtomic(r,t){i.mkdirSync(o.dirname(r),{recursive:!0});const e=`${r}.${String(process.pid)}.${String(Date.now())}.tmp`;i.writeFileSync(e,t);try{i.renameSync(e,r)}catch(n){if(i.rmSync(e,{force:!0}),!i.existsSync(r))throw n}}export function writeJson(r,t){i.mkdirSync(o.dirname(r),{recursive:!0});const e=JSON.stringify(t,null,2);l("json.serializations"),f()&&l("json.bytesSerialized",Buffer.byteLength(e)),i.writeFileSync(r,`${e}
|
|
2
|
+
`)}export function hashFile(r){const t=i.readFileSync(r);return l("evidence.hashFilesRead",1),l("evidence.hashBytesRead",t.byteLength),sha256(t)}export function sha256(r){return x.createHash("sha256").update(r).digest("hex")}export function stableJson(r){const t=JSON.stringify(d(r));return l("json.canonicalSerializations"),f()&&l("json.canonicalBytesSerialized",Buffer.byteLength(t)),t}function d(r){return Array.isArray(r)?r.map(d):w(r)?Object.fromEntries(Object.keys(r).sort().map(t=>[t,d(r[t])])):r}export function objectRecords(r){return Array.isArray(r)?r.filter(w):[]}export function resolveRunBundleDirectory(r,t){const e=resolveRunsRoot(r,!1),n=o.join(e,t),u=i.lstatSync(n,{throwIfNoEntry:!1});if(u===void 0)return n;if(u.isSymbolicLink())throw new Error(`Immutable run bundle directory must not be a symbolic link: ${t}`);if(!u.isDirectory())throw new Error(`Immutable run bundle path must be a directory: ${t}`);const s=i.realpathSync(e),c=i.realpathSync(n),a=o.relative(s,c);if(a.length===0||a===".."||a.startsWith(`..${o.sep}`)||o.isAbsolute(a))throw new Error(`Immutable run bundle directory must resolve within the runs root: ${t}`);return n}export function resolveRunsRoot(r,t){const e=o.resolve(r),n=resultsRoot(e),u=o.join(n,m);if(t)g(e,n,"Testkit results root"),i.mkdirSync(n,{recursive:!0});else if(!i.existsSync(n))return u;const s=S(n,"Testkit results root");if(y(i.realpathSync(e),s,"Testkit results root must resolve within the product directory"),t)i.mkdirSync(u,{recursive:!0});else if(!i.existsSync(u))return u;const c=S(u,"Immutable run bundles root");return y(s,c,"Immutable run bundles root must resolve within .testkit/results"),u}function g(r,t,e){const n=o.resolve(r),u=i.realpathSync(n);let s=o.resolve(t),c=i.lstatSync(s,{throwIfNoEntry:!1});for(;c===void 0;){const a=o.dirname(s);if(a===s)throw new Error(`${e} has no existing parent directory`);s=a,c=i.lstatSync(s,{throwIfNoEntry:!1})}if(s!==n&&c.isSymbolicLink())throw new Error(`${e} must not traverse a symbolic link`);if(!c.isDirectory())throw new Error(`${e} parent must be a directory`);y(u,i.realpathSync(s),`${e} must resolve within the product directory`,!0)}function S(r,t){const e=i.lstatSync(r,{throwIfNoEntry:!1});if(e===void 0)throw new Error(`${t} does not exist`);if(e.isSymbolicLink())throw new Error(`${t} must not be a symbolic link`);if(!e.isDirectory())throw new Error(`${t} must be a directory`);return i.realpathSync(r)}function y(r,t,e,n=!1){const u=o.relative(r,t);if(!n&&u.length===0||u===".."||u.startsWith(`..${o.sep}`)||o.isAbsolute(u))throw new Error(e)}export function requirePublicationPathOutsideResults(r,t){const e=o.resolve(resultsRoot(r)),n=o.resolve(t);if(p(e,n))throw new Error("Publication directory must be outside .testkit/results");const u=i.realpathSync(e);let s=n,c=i.lstatSync(s,{throwIfNoEntry:!1});for(;c===void 0;){const h=o.dirname(s);if(h===s)throw new Error("Publication directory has no existing parent");s=h,c=i.lstatSync(s,{throwIfNoEntry:!1})}const a=i.realpathSync(s);if(p(u,a))throw new Error("Publication directory must be outside .testkit/results")}export function resolveManifestEntryPath(r,t){const e=o.resolve(r),n=o.resolve(e,v(t,"manifest entry path"));return b(e,n,t),i.existsSync(n)&&b(i.realpathSync(e),i.realpathSync(n),t),n}function b(r,t,e){const n=o.relative(r,t);if(n.length===0||n===".."||n.startsWith(`..${o.sep}`)||o.isAbsolute(n))throw new Error(`manifest path escapes bundle directory: ${e}`)}export function optionalStringArray(r){return Array.isArray(r)?r.filter(t=>typeof t=="string"):[]}export function optionalUnknownArray(r){return Array.isArray(r)?r:[]}export function errorMessage(r){return r instanceof Error?r.message:String(r)}export function bundleRelativePath(r,t){return normalizePath(o.join(".testkit","results",m,r,t))}export function resultsRoot(r){return o.join(r,".testkit","results")}export function normalizePath(r){return r.split(o.sep).join("/")}export function sanitizeSegment(r){return r.toLowerCase().replace(/[^a-z0-9._-]+/gu,"-").replace(/^-+|-+$/gu,"")||"product"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
const g="testkitOperation",m="testkitDiagnostics",c=String;export function attachErrorOperation(t,n){if(!r(t)||!r(n)||s(t,g))return t;const e=y(n);return e!==null&&d(t,g,e),t}export function attachErrorDiagnostics(t,n){if(!r(t)||!r(n))return t;const e=getErrorDiagnostics(t),i={...f(e),...f(n)};return d(t,m,i),t}export function getErrorOperation(t){return r(t)?y(s(t,g)||s(t,"operation")):null}export function getErrorDiagnostics(t){return r(t)?f(s(t,m)||s(t,"diagnostics")):{}}export function errorMessage(t){return t instanceof Error?t.message:c(t)}export function formatError(t){return sanitizeErrorMessage(errorMessage(t))}export function sanitizeErrorMessage(t){return t.replace(/(postgres(?:ql)?:\/\/[^:\s"'@]+:)[^@\s"']+(@)/gi,"$1<redacted>$2").replace(/("password"\s*:\s*")[^"]*(")/gi,"$1<redacted>$2").replace(/('password'\s*:\s*')[^']*(')/gi,"$1<redacted>$2").replace(/(PGPASSWORD=)[^\s'"\\]+/g,"$1<redacted>")}export function errorDetail(t){if(r(t)){const n=s(t,"stack");if(typeof n=="string"&&n.length>0)return n;const e=s(t,"message");if(typeof e=="string"&&e.length>0)return e}return t instanceof Error?t.stack||t.message:c(t)}export function serializeError(t,{includeStack:n=!1}={}){if(t instanceof Error){const e=getErrorOperation(t),i=getErrorDiagnostics(t),o=s(t,"code"),u=s(t,"exitCode"),a=s(t,"status");return{name:t.name||"Error",message:t.message||c(t),...typeof o=="string"?{code:o}:{},...typeof u=="number"&&Number.isInteger(u)?{exitCode:u}:{},...typeof a=="number"&&Number.isInteger(a)?{status:a}:{},...e?{phase:e.name,operation:e}:{},...Object.keys(i).length>0?{diagnostics:i}:{},...n&&typeof t.stack=="string"?{stack:t.stack}:{}}}return{name:"Error",message:c(t)}}function d(t,n,e){Object.defineProperty(t,n,{value:e,configurable:!0,enumerable:!1,writable:!0})}function y(t){if(!r(t))return null;const n=s(t,"id"),e=s(t,"source"),i=s(t,"name"),o=s(t,"kind"),u=s(t,"summary"),a=s(t,"status"),p=s(t,"error");return{...n?{id:c(n)}:{},source:typeof e=="string"?e:"testkit",name:typeof i=="string"&&i?i:"operation",kind:typeof o=="string"&&o?o:"operation",...u?{summary:c(u)}:{},scope:f(s(t,"scope")),...a?{status:c(a)}:{},...p?{error:c(p)}:{}}}function f(t){if(!r(t)||E(t))return{};const n=[];for(const e of Object.keys(t)){const i=s(t,e);i!==void 0&&n.push([e,l(i)])}return Object.fromEntries(n)}function l(t){return t===null
|
|
1
|
+
const g="testkitOperation",m="testkitDiagnostics",c=String;export function attachErrorOperation(t,n){if(!r(t)||!r(n)||s(t,g))return t;const e=y(n);return e!==null&&d(t,g,e),t}export function attachErrorDiagnostics(t,n){if(!r(t)||!r(n))return t;const e=getErrorDiagnostics(t),i={...f(e),...f(n)};return d(t,m,i),t}export function getErrorOperation(t){return r(t)?y(s(t,g)||s(t,"operation")):null}export function getErrorDiagnostics(t){return r(t)?f(s(t,m)||s(t,"diagnostics")):{}}export function errorMessage(t){return t instanceof Error?t.message:c(t)}export function formatError(t){return sanitizeErrorMessage(errorMessage(t))}export function sanitizeErrorMessage(t){return t.replace(/(postgres(?:ql)?:\/\/[^:\s"'@]+:)[^@\s"']+(@)/gi,"$1<redacted>$2").replace(/("password"\s*:\s*")[^"]*(")/gi,"$1<redacted>$2").replace(/('password'\s*:\s*')[^']*(')/gi,"$1<redacted>$2").replace(/(PGPASSWORD=)[^\s'"\\]+/g,"$1<redacted>")}export function errorDetail(t){if(r(t)){const n=s(t,"stack");if(typeof n=="string"&&n.length>0)return n;const e=s(t,"message");if(typeof e=="string"&&e.length>0)return e}return t instanceof Error?t.stack||t.message:c(t)}export function serializeError(t,{includeStack:n=!1}={}){if(t instanceof Error){const e=getErrorOperation(t),i=getErrorDiagnostics(t),o=s(t,"code"),u=s(t,"exitCode"),a=s(t,"status");return{name:t.name||"Error",message:t.message||c(t),...typeof o=="string"?{code:o}:{},...typeof u=="number"&&Number.isInteger(u)?{exitCode:u}:{},...typeof a=="number"&&Number.isInteger(a)?{status:a}:{},...e?{phase:e.name,operation:e}:{},...Object.keys(i).length>0?{diagnostics:i}:{},...n&&typeof t.stack=="string"?{stack:t.stack}:{}}}return{name:"Error",message:c(t)}}function d(t,n,e){Object.defineProperty(t,n,{value:e,configurable:!0,enumerable:!1,writable:!0})}function y(t){if(!r(t))return null;const n=s(t,"id"),e=s(t,"source"),i=s(t,"name"),o=s(t,"kind"),u=s(t,"summary"),a=s(t,"status"),p=s(t,"error");return{...n?{id:c(n)}:{},source:typeof e=="string"?e:"testkit",name:typeof i=="string"&&i?i:"operation",kind:typeof o=="string"&&o?o:"operation",...u?{summary:c(u)}:{},scope:f(s(t,"scope")),...a?{status:c(a)}:{},...p?{error:c(p)}:{}}}function f(t){if(!r(t)||E(t))return{};const n=[];for(const e of Object.keys(t)){const i=s(t,e);i!==void 0&&n.push([e,l(i)])}return Object.fromEntries(n)}function l(t){return t===null?null:typeof t=="string"||typeof t=="number"||typeof t=="boolean"?t:E(t)?t.map(n=>l(n)):r(t)?f(t):c(t)}function s(t,n){return Reflect.get(t,n)}function E(t){return Array.isArray(t)}function r(t){return t!==null&&typeof t=="object"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Call-through observation of work performed by Testkit, independent of time. */
|
|
2
|
+
export type WorkObserver = (metric: string, amount: number) => void;
|
|
3
|
+
export declare function isWorkObserved(): boolean;
|
|
4
|
+
export declare function recordWork(metric: string, amount?: number): void;
|
|
5
|
+
/** Install an observer for this process and restore the previous observer. */
|
|
6
|
+
export declare function observeWork(next: WorkObserver): () => void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const s=Symbol.for("testkit.work.observer"),t=globalThis,e=t[s]??={};export function isWorkObserved(){return e.observer!==void 0}export function recordWork(r,o=1){e.observer?.(r,o)}export function observeWork(r){const o=e.observer;return e.observer=r,()=>{e.observer=o}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
const c=String;export function captureOutput(t,e={}){if(!t)return Promise.resolve();const u=t;if(u.destroyed===!0||u.readableEnded===!0)return Promise.resolve();const l=
|
|
1
|
+
const c=String;export function captureOutput(t,e={}){if(!t)return Promise.resolve();const u=t;if(u.destroyed===!0||u.readableEnded===!0)return Promise.resolve();const l=y(e.onLine),r=y(e.liveWriter),i=w(e.livePrefix);let n="";return new Promise(g=>{let p=!1;const f=()=>{p||(p=!0,g())};t.on("data",d=>{n+=v(d);const s=n.split(/\r?\n/);n=s.pop()||"";for(const o of s)o.length!==0&&(l?.(o),r&&r(i?`${i} ${o}`:o))}),t.on("end",()=>{n.length>0&&(l?.(n),r&&r(i?`${i} ${n}`:n)),f()}),t.on("close",f),t.on("error",f)})}function y(t){return typeof t!="function"?null:e=>{Reflect.apply(t,void 0,[e])}}function v(t){if(t==null)throw new TypeError("Output chunks must provide toString()");if(typeof t=="string")return t;if(typeof t=="number"||typeof t=="boolean"||typeof t=="bigint"||typeof t=="symbol")return t.toString();if(typeof t!="object"&&typeof t!="function")throw new TypeError("Output chunks must provide toString()");const e=Reflect.get(t,"toString");if(!S(e))throw new TypeError("Output chunks must provide toString()");return c(Reflect.apply(e,t,[]))}function S(t){return typeof t=="function"}function w(t){return t??""}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"entrypoint": "dist/process/process.js",
|
|
4
4
|
"sourceSha256": "a54f01864b603cee568b2945390dff35d578e07f6d26b5add06014169c3d17b0",
|
|
5
5
|
"bundleSha256": "62f76cf9fd019f6c552bfde2787f058917caf279670a62e7a6bfa12be34cb4b1",
|
|
6
|
-
"packageLockSha256": "
|
|
6
|
+
"packageLockSha256": "c0f28403d05b2838f4d0d104862548469ad7cc74e1aeb9fb9a33c7df856d7f5b",
|
|
7
7
|
"esbuildVersion": "0.28.1",
|
|
8
8
|
"options": {
|
|
9
9
|
"format": "esm",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function resolveProductDir(cwd: string, explicitDir?: string | null): string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import e from"node:fs";import s from"node:path";export function resolveProductDir(o,t){const r=t?s.resolve(o,t):o;if(!e.existsSync(r))throw new Error(`Product directory does not exist: ${r}`);return r}
|
package/dist/process/locks.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{AsyncLocalStorage as
|
|
2
|
-
`,{encoding:"utf8",flag:"wx",mode:384}),
|
|
3
|
-
`,{encoding:"utf8",flag:"wx",mode:384})}catch(n){throw
|
|
4
|
-
`,{encoding:"utf8",flag:"wx",mode:384}),
|
|
1
|
+
import{AsyncLocalStorage as te}from"node:async_hooks";import{randomUUID as re}from"node:crypto";import i from"node:fs";import ne from"node:os";import a from"node:path";import{setTimeout as oe}from"node:timers/promises";import{captureProcessCreationIdentity as j}from"./process-identity.js";import{channel as se}from"node:diagnostics_channel";const x=se("testkit.work");function ie(e){try{i.mkdirSync(e,{mode:448})}catch(t){if(!c(t,"EEXIST"))throw t}return V(e)}function ce(e){try{const t=i.lstatSync(e);return t.isDirectory()&&!t.isSymbolicLink()?"real":"unsafe"}catch(t){if(c(t,"ENOENT"))return"absent";throw t}}function K(e){const t=i.lstatSync(e,{bigint:!0});if(!t.isDirectory()||t.isSymbolicLink())throw new Error(`Process lock path is not a real directory: ${e}`);return{birthtimeNs:t.birthtimeNs,dev:t.dev,ino:t.ino}}function V(e){try{return K(e)}catch(t){if(c(t,"ENOENT")||ue(t))return null;throw t}}function h(e,t){const n=V(e);return n!==null&&n.birthtimeNs===t.birthtimeNs&&n.dev===t.dev&&n.ino===t.ino}function ae(e,t,n){return h(e,t)?v(n)?"unsafe":"retry":h(n,t)?"published":"unsafe"}function ue(e){return e instanceof Error&&e.message.startsWith("Process lock path is not a real directory:")}const U="owner.json",g=1,T="claim-",M="choosing",W="ticket",le=100,q=65e3,fe=100,me=new Set(["EACCES","EBUSY","EPERM"]),de=5e3,Y=ne.hostname(),he=process.platform==="win32"?de:0,O=new te;class H extends Error{constructor(t,n){super(`Timed out waiting for process lock ${a.basename(t)}${Fe(n)}`),this.name="ProcessLockTimeoutError"}}export async function withProcessLock(e,t,n={}){const r=J(e);_e(r);const o=await ye(r,n),s=O.getStore(),u=new Set(s??[]);u.add(r);try{return await O.run(u,t)}finally{o()}}export async function tryWithProcessLock(e,t,n={}){let r;try{r=await withProcessLock(e,async()=>{try{return{ok:!0,value:await t()}}catch(o){return{error:o,ok:!1}}},{...n,timeoutMs:0})}catch(o){if(o instanceof H)return{acquired:!1};throw o}if(!r.ok)throw r.error;return{acquired:!0,value:r.value}}export function probeProcessLock(e,t={}){const n=J(e),r=G(n);if(!v(r))return"absent";const o=t.identityObservations??new Map,s=R(r,o,t);return s.rootAvailable&&s.claims.length===0?(_(r),"absent"):"active"}async function ye(e,t){const n=je(t.timeoutMs,null,"process lock timeout"),r=xe(t.retryIntervalMs,le,"process lock retry interval");let o=t.retryIntervalMs===void 0?10:r;async function s(){const b=o;o=Math.min(r,o*2),await $e(b,t.signal,e)}const u=t.identityObservations??new Map,f=j(process.pid);if(f===null)throw new Error(`Cannot acquire process lock ${e}: current process creation identity is unavailable`);const E=Date.now(),y={createdAt:new Date(E).toISOString(),hostname:Y,pid:process.pid,processIdentity:f,schemaVersion:g,token:re()},m=G(e),l=a.join(m,`${T}${y.token}`),k=`${m}.preparing-${y.token}`;we(k,y);let w=null,D=!1,S=null,I=!1,F=0;try{for(w=K(k);;){L(t.signal,e);const p=ie(m);if(p!==null){try{i.renameSync(k,l),I=!0,S=p}catch(d){if(!c(d,"ENOENT"))if(Ee(d)){const N=ae(k,w,l);if(N==="published")I=!0,S=p;else if(N==="retry"&&F<fe)F+=1;else throw d}else throw d}if(I){if(!h(m,p))throw new Error(`Process lock directory changed during claim publication: ${m}`);if(!h(l,w))throw new Error(`Process lock claim changed during publication: ${l}`);break}}else R(m,u,t);Q(e,null,E,n),await s()}if(S===null)throw new Error(`Process lock claim was published without its directory identity: ${l}`);const b=S,Z=R(m,u,t,b),A=ke(Z.claims);for(pe(l,A);;){L(t.signal,e);const p=R(m,u,t,b),d=p.claims.find(ee=>ee.claimDirectory===l);if(d?.state!=="ready"||d.ticket!==A||d.ownerRecord===null||!X(d.ownerRecord,y))throw new Error(`Process lock claim disappeared or changed before acquisition: ${l}`);const N=Se(p.claims,d);if(N===null)return D=!0,()=>{Ae(l,m,b,y,A)};Q(e,N.ownerRecord,E,n),await s()}}finally{I?!D&&S!==null&&w!==null&&h(m,S)&&h(l,w)&&(i.rmSync(l,{force:!0,recursive:!0}),u.delete(l),_(m)):w!==null&&h(k,w)&&i.rmSync(k,{force:!0,recursive:!0})}}function Ee(e){return process.platform==="win32"&&$(e)&&typeof e.code=="string"&&me.has(e.code)}function we(e,t){i.mkdirSync(e,{mode:448});try{i.writeFileSync(a.join(e,U),`${JSON.stringify(t)}
|
|
2
|
+
`,{encoding:"utf8",flag:"wx",mode:384}),i.writeFileSync(a.join(e,M),`${t.token}
|
|
3
|
+
`,{encoding:"utf8",flag:"wx",mode:384})}catch(n){throw i.rmSync(e,{force:!0,recursive:!0}),n}}function pe(e,t){i.writeFileSync(a.join(e,W),`${t.toString()}
|
|
4
|
+
`,{encoding:"utf8",flag:"wx",mode:384}),i.rmSync(a.join(e,M))}function ke(e){const t=e.reduce((n,r)=>r.ticket===null?n:Math.max(n,r.ticket),0);if(t>=Number.MAX_SAFE_INTEGER)throw new Error("Process lock ticket space is exhausted");return t+1}function Se(e,t){if(t.ticket===null||t.ownerRecord===null)return t;const n=t.ownerRecord.token;for(const r of e){if(r.claimDirectory===t.claimDirectory)continue;if(r.state!=="ready"||r.ticket===null||r.ownerRecord===null)return r;const o=r.ownerRecord.token;if(be(r.ticket,o,t.ticket,n)<0)return r}return null}function be(e,t,n,r){return e===n?t.localeCompare(r):e-n}function R(e,t,n,r=null){const o=ce(e);if(o==="absent")return{claims:[],rootAvailable:!0};if(o==="unsafe"){const f=P(e);return Le(e,t,n)?{claims:[],rootAvailable:!0}:{claims:[f],rootAvailable:!1}}if(r!==null&&!h(e,r))return{claims:[P(e)],rootAvailable:!1};let s;try{s=i.readdirSync(e,{withFileTypes:!0})}catch(f){if(c(f,"ENOENT"))return{claims:[],rootAvailable:!0};throw f}const u=[];for(const f of s){const E=a.join(e,f.name),y=!f.name.startsWith(T)||!f.isDirectory()?P(E):z(E);Ie(y,t,n)?Re(E,t):u.push(y)}return{claims:u,rootAvailable:!0}}function P(e){return{claimDirectory:e,ownerRecord:null,state:"malformed",ticket:null}}function z(e){const t=Me(e),n=v(a.join(e,M)),r=Ne(e),o=a.basename(e).slice(T.length),s=t===null||t.token===o;return{claimDirectory:e,ownerRecord:t,state:t===null||!s?"malformed":n?"choosing":r===null?"malformed":"ready",ticket:r}}function Ne(e){try{const t=i.readFileSync(a.join(e,W),"utf8").trim(),n=Number(t);return Number.isSafeInteger(n)&&n>0?n:null}catch{return null}}function Ie(e,t,n){const r=e.ownerRecord;if(r===null||r.hostname!==Y)return n.hostScope==="current-host"&&Ce(e.claimDirectory)>=q;if(ve(r.pid)==="absent")return!0;const o=C(r),s=t.get(e.claimDirectory);if(s?.ownerKey===o&&Date.now()-s.checkedAtMs<he)return!1;const u=j(r.pid);return t.set(e.claimDirectory,{checkedAtMs:Date.now(),ownerKey:o}),u!==null&&u!==r.processIdentity}function Re(e,t){t.delete(e),i.rmSync(e,{force:!0,recursive:!0})}function Le(e,t,n){if(n.hostScope!=="current-host")return!1;const r=B(e);if(r==="absent")return!0;if(r==="real-directory"||Te(r)<q)return!1;const o=B(e);if(o==="absent")return!0;if(o==="real-directory"||!ge(o,r))return!1;try{return i.unlinkSync(e),t.delete(e),!0}catch(s){if(c(s,"ENOENT"))return!0;if(c(s,"EISDIR")||c(s,"EPERM"))return!1;throw s}}function Ae(e,t,n,r,o){if(!h(t,n))throw new Error(`Process lock directory changed before release: ${t}`);const s=z(e);if(s.state!=="ready"||s.ticket!==o||s.ownerRecord===null||!X(s.ownerRecord,r))throw new Error(`Process lock ownership changed before release: ${e}`);i.rmSync(e,{force:!0,recursive:!0}),_(t)}function B(e){try{const t=i.lstatSync(e,{bigint:!0});return t.isDirectory()&&!t.isSymbolicLink()?"real-directory":{birthtimeNs:t.birthtimeNs,ctimeNs:t.ctimeNs,dev:t.dev,ino:t.ino,mode:t.mode,mtimeNs:t.mtimeNs,size:t.size}}catch(t){if(c(t,"ENOENT"))return"absent";throw t}}function ge(e,t){return e.birthtimeNs===t.birthtimeNs&&e.ctimeNs===t.ctimeNs&&e.dev===t.dev&&e.ino===t.ino&&e.mode===t.mode&&e.mtimeNs===t.mtimeNs&&e.size===t.size}function Te(e){return Math.max(0,Date.now()-Number(e.mtimeNs)/1e6)}function _(e){try{i.rmdirSync(e)}catch(t){if(!c(t,"ENOENT")&&!c(t,"ENOTEMPTY")&&!c(t,"EEXIST"))throw t}}function Me(e){let t;try{t=JSON.parse(i.readFileSync(a.join(e,U),"utf8"))}catch{return null}return $(t)?Oe(t):null}function Oe(e){const t=Object.keys(e).sort(),n=["createdAt","hostname","pid","processIdentity","schemaVersion","token"];return t.length!==n.length||t.some((r,o)=>r!==n[o])||e.schemaVersion!==g||!Ke(e.createdAt)||typeof e.hostname!="string"||e.hostname.length===0||!Ve(e.pid)||typeof e.processIdentity!="string"||e.processIdentity.length===0||typeof e.token!="string"||e.token.length===0?null:{createdAt:e.createdAt,hostname:e.hostname,pid:e.pid,processIdentity:e.processIdentity,schemaVersion:g,token:e.token}}function X(e,t){return C(e)===C(t)}function C(e){return[e.hostname,e.pid.toString(),e.processIdentity,e.token,e.createdAt].join("\0")}function G(e){return`${e}.dir`}function J(e){const t=a.resolve(Pe(e)),n=i.realpathSync.native(a.dirname(t));return a.join(n,a.basename(t))}function Pe(e){if(e.trim().length===0)throw new Error("Process lock path must not be empty");return e}function _e(e){if(O.getStore()?.has(e))throw new Error(`Reentrant process lock acquisition is not allowed: ${e}`)}function Q(e,t,n,r){if(r!==null&&Date.now()-n>=r)throw new H(e,t)}function Ce(e){try{return Math.max(0,Date.now()-i.lstatSync(e).mtimeMs)}catch(t){if(c(t,"ENOENT"))return Number.POSITIVE_INFINITY;throw t}}function ve(e){try{return process.kill(e,0),"alive"}catch(t){return c(t,"ESRCH")?"absent":c(t,"EPERM")?"alive":"unknown"}}async function $e(e,t,n){L(t,n),x.hasSubscribers&&x.publish({metric:"locks.contentionRetries",amount:1});try{await oe(e,void 0,{signal:t})}catch(r){throw L(t,n),r}}function L(e,t){if(e?.aborted)throw De(e,t)}function De(e,t){return e.reason instanceof Error?e.reason:new Error(`Waiting for process lock was aborted: ${t}`)}function Fe(e){return e===null?"":` (host=${e.hostname} pid=${e.pid.toString()} identity=${e.processIdentity} createdAt=${e.createdAt})`}function je(e,t,n){const r=e===void 0?t:e;if(r!==null&&(!Number.isSafeInteger(r)||r<0))throw new Error(`${n} must be null or a non-negative safe integer`);return r}function xe(e,t,n){const r=e??t;if(!Number.isSafeInteger(r)||r<=0)throw new Error(`${n} must be a positive safe integer`);return r}function Ke(e){return typeof e=="string"&&e.length>0&&Number.isFinite(Date.parse(e))}function Ve(e){return Number.isSafeInteger(e)&&Number(e)>0}function v(e){try{return i.lstatSync(e),!0}catch(t){if(c(t,"ENOENT"))return!1;throw t}}function c(e,t){return $(e)&&e.code===t}function $(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import
|
|
1
|
+
import p from"net";import{setTimeout as y}from"node:timers/promises";export const DEFAULT_READY_TIMEOUT_MS=12e4;export async function waitForReady({name:e,url:t,timeoutMs:r,process:c,signal:o}){const s=Date.now(),a=Promise.resolve(c.completion).then(f=>({kind:"completion",result:f})),n=E(o);let i=10,d;try{for(;Date.now()-s<r;){const f=Math.max(1,r-(Date.now()-s));d?.abort(),d=new AbortController;const u=y(Math.min(i,f),void 0,{signal:d.signal}).then(()=>({kind:"interval"}));i=Math.min(i*2,1e3);const m=new AbortController,b=fetch(t,{signal:m.signal}).then(w=>({kind:"endpoint",ready:w.ok}),()=>({kind:"endpoint",ready:!1}));let l=await Promise.race([a,...n?[n.promise]:[],b,u]).finally(()=>{m.abort()});if(l.kind==="endpoint"){if(l.ready)return;l=await Promise.race([a,...n?[n.promise]:[],u])}if(l.kind==="completion")throw serviceCompletionError(e,l.result);if(l.kind==="abort")throw l.reason}}finally{d?.abort(),n?.dispose()}throw new Error(`Timed out waiting for "${e}" to become ready at ${t}`)}export function serviceCompletionError(e,t,r="before becoming ready"){const c=r==="unexpectedly"?"Local service":"Service",o=t.error||t.terminationError||null,s=t.exitCode!==null?`exit code ${t.exitCode.toString()}`:t.exitSignal?`signal ${t.exitSignal}`:"no exit status";return new Error(`${c} "${e}" exited ${r} with ${s}${o?`: ${o.message}`:""}`,o?{cause:o}:void 0)}function E(e){if(!e)return null;let t=()=>{};const r=new Promise(c=>{t=()=>{c({kind:"abort",reason:e.reason||new Error("Service startup aborted")})},e.aborted?t():e.addEventListener("abort",t,{once:!0})});return{dispose(){e.removeEventListener("abort",t)},promise:r}}export async function isPortInUse({host:e,port:t}){return new Promise((r,c)=>{const o=new p.Socket;let s=!1;const a=(n,i=null)=>{if(!s){if(s=!0,o.destroy(),i){c(i);return}r(n)}};o.setTimeout(1e3),o.once("connect",()=>{a(!0)}),o.once("timeout",()=>{a(!1)}),o.once("error",n=>{const i=h(n,"code");if(typeof i=="string"&&["ECONNREFUSED","EHOSTUNREACH","ENOTFOUND"].includes(i)){a(!1);return}a(!1,n)}),o.connect(t,e)})}function h(e,t){if(!((typeof e!="object"||e===null)&&typeof e!="function"))return Reflect.get(e,t)}export function connectEndpoint(e,t,r){return new Promise(c=>{const o=p.createConnection({host:e.host,port:e.port});let s=!1;const a=Date.now(),n=d=>{s||(s=!0,r?.removeEventListener("abort",i),o.destroy(),c({host:e.host,port:e.port,durationMs:Math.max(0,Date.now()-a),...d}))};function i(){n({ok:!1,error:"connection cancelled"})}o.setTimeout(t),o.once("connect",()=>{n({ok:!0})}),o.once("timeout",()=>{n({ok:!1,error:`connect timed out after ${t.toString()}ms`})}),o.once("error",d=>{n({ok:!1,error:d.message})}),r?.addEventListener("abort",i,{once:!0}),r?.aborted&&i()})}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{observeProcessCreationIdentity as i}from"../process-identity.js";import{loadRunManifest as o,listRunManifests as r}from"./manifest.js";export function isRunManifestActive(t){return t.pid>0&&i(t.pid,t.processIdentity)!=="different"}export function inspectRunManifests(t,e){const n=e?o(t,e):null;return(e?n?[n]:[]:r(t)).map(s=>({...s,active:isRunManifestActive(s)}))}
|
|
@@ -79,6 +79,7 @@ export declare function writeRunMaintenanceStopRequest(productDir: string, reque
|
|
|
79
79
|
export declare function readRunMaintenanceStopRequest(productDir: string, runId: unknown): RunMaintenanceStopRequest | null;
|
|
80
80
|
export declare function removeRunMaintenanceStopRequest(productDir: string, runId: unknown): void;
|
|
81
81
|
export declare function removeRunManifestFile(productDir: string, runId: unknown): void;
|
|
82
|
+
export declare function loadRunManifest(productDir: string, runId: unknown): RunManifest | null;
|
|
82
83
|
export declare function listRunManifests(productDir: string): RunManifest[];
|
|
83
84
|
export declare function requireSafeRunId(value: unknown, label: string): string;
|
|
84
85
|
export declare function normalizeRunManifest(value: unknown): RunManifest | null;
|