@gfdelarue/testkit 0.3.4 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- 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/engine-architecture.md +4 -1
- package/docs/test-types.md +9 -0
- package/package.json +2 -1
- package/dist/cli/renderers/artifact-preview.d.ts +0 -4
- package/dist/cli/renderers/artifact-preview.js +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{makeTempDirAsync as R}from"../../../process/files/temp-dir.js";import{classifyManagedProcessResult as
|
|
1
|
+
import{makeTempDirAsync as R}from"../../../process/files/temp-dir.js";import{classifyManagedProcessResult as z,terminalOutcome as I,processOutputEvidence as W,emptyEvidence as J,failureFromError as l,firstOutputLine as h,processExitFailure as H,toError as x,executeEngineTasks as Y,managedProcessFailure as G}from"../../adapters/execution.js";import{requireRootDirectory as Q,requireAbsoluteWorkspacePath as M,requireConsumerFile as P,safeSegment as L}from"../../adapters/workspace.js";import p from"node:fs/promises";import c from"node:path";import{pathToFileURL as X}from"node:url";import{isEngineTaskDeadlineExceededReason as Z,resolveEngineTaskInputs as q}from"../../api/index.js";import{createInvocationProcessEnvironment as tt,runManagedProcess as et}from"../../../process/index.js";import{preflightNodeVitestInstallation as it}from"../../installations/node-vitest.js";import{collectNodeVitestEvidence as rt}from"./evidence.js";import{assertNodeVitestSpawnEnvironmentBudget as ot,NODE_VITEST_RUNTIME_INPUT_FILE_ENV as nt,NODE_VITEST_RUNTIME_INPUT_SHA256_ENV as at,selectNodeVitestForwardedEnvironment as st,verifyOwnedNodeVitestRuntimeInput as ct,writeOwnedNodeVitestRuntimeInput as ut}from"../runtime/runtime-input.js";import{writeNodeVitestWorkspace as dt}from"./workspace.js";import{engineEvidenceIdentity as lt}from"../../adapters/evidence-identity.js";const T={codePrefix:"NODE_VITEST",label:"Node/Vitest"};class A extends Error{constructor(e){const i=x(e);super(i.message,{cause:i}),this.name="NodeVitestConsumerPreparationError"}}export const nodeVitestExecutorDependencies={removeOwnedDirectory:t=>p.rm(t,{force:!0,recursive:!0}),writeRuntimeInput:ut,writeWorkspace:dt};export async function executeNodeVitestEngine(t,e,i,r=nodeVitestExecutorDependencies){const n=await pt(t,e,r,i);return Y(t,n.tasks,T,(o,a)=>ft(t,n,o,a),_t(t,n.installation))}async function pt(t,e,i,r){const n=t.profile.id,o=e.packages.vitest.cli,a=await Q(t.workspace.rootDirectory);M(t.workspace.temporaryDirectory,"temporary directory"),M(t.workspace.artifactDirectory,"artifact directory"),await p.mkdir(t.workspace.temporaryDirectory,{recursive:!0}),await p.mkdir(t.workspace.artifactDirectory,{recursive:!0});const f=await p.realpath(t.workspace.temporaryDirectory),d=await p.realpath(t.workspace.artifactDirectory),v=Object.freeze({...e.authoringEntrypoints,cli:e.packages.vitest.cli,packages:e.packages}),_=await R(`node-vitest-${L(t.invocationId)}-`,{parentDir:f});r.defer(()=>i.removeOwnedDirectory(_));const j=await tt(_),g=[];for(const[K,s]of t.tasks.entries()){const m=q(t,s),S=`${K.toString().padStart(3,"0")}-${L(s.id)}`,u=c.join(_,S),y=await R(`${S}-`,{parentDir:d}),O=c.join(u,"testkit-vitest-report.json"),U=c.join(y,"data","manifest.json"),B=c.join(u,"setup.mjs"),w=c.join(u,"input-bootstrap.mjs"),F=c.join(u,"resolution-bootstrap.cjs"),V=c.join(u,"vitest.config.mjs");try{await p.mkdir(u,{mode:448,recursive:!1});const D=await N(()=>P(a,s.file,"Node/Vitest task file")),k=await N(()=>vt(a,m)),$=await N(()=>It(t,n,s,m)),E=await i.writeRuntimeInput(u,$);await i.writeWorkspace({report:{identity:{engineId:"node-vitest",invocationId:t.invocationId,taskId:s.id,sourceFile:s.file},reportFile:O,workspaceDirectory:u},configFile:k.testkitConfig,entrypoints:v,inputBootstrapFile:w,resolutionBootstrapFile:F,rootDirectory:a,runtimeInput:{expectedFile:E.file,expectedInvocationId:t.invocationId,expectedProfileId:n,expectedSha256:E.sha256,expectedSizeBytes:E.sizeBytes,expectedTaskId:s.id},setupFile:B,sourceFile:D,timeoutMs:Math.max(1,s.deadlineMs-Date.now()),tsconfigFile:k.tsconfig,vitestConfigFile:V}),g.push({artifactDirectory:y,inputs:m,task:s,preparationFailure:null,prepared:{dataManifestFile:U,inputBootstrapFile:w,reportFile:O,resolutionBootstrapFile:F,runtimeInput:E,vitestConfigFile:V}})}catch(D){g.push({artifactDirectory:y,inputs:m,task:s,preparationFailure:gt(D),prepared:null})}}return Object.freeze({cliFile:o,installation:e,invocationEnvironment:j.environment,profileId:n,rootDirectory:a,tasks:Object.freeze(g)})}async function ft(t,e,i,r){const n=Math.max(0,i.task.deadlineMs-r);try{await ct(i.prepared.runtimeInput)}catch(d){return I(i.task.id,"failed",r,J(),{...l(d,"NODE_VITEST_INPUT_INVALID"),classification:"infra"})}const o=await et({args:[e.cliFile,"run","--config",i.prepared.vitestConfigFile,"--configLoader","runner"],cwd:e.rootDirectory,displayLabel:"Node/Vitest",env:Et(t,e,i),executable:process.execPath,signal:t.signal,timeoutMs:n});let a;try{a=await rt({identity:{engineId:"node-vitest",invocationId:t.invocationId,taskId:i.task.id,sourceFile:i.task.file},artifactDirectory:i.artifactDirectory,dataManifestFile:i.prepared.dataManifestFile,profileId:e.profileId,reportFile:i.prepared.reportFile,rootDirectory:e.rootDirectory,stderr:o.stderr,stdout:o.stdout})}catch(d){const v=W(o);return I(i.task.id,"failed",r,v,nodeVitestEvidenceCollectionFailure(o,d))}if(a.reportSummary===null&&o.exitCode!==0&&!t.signal.aborted&&!o.timedOut&&!o.outputLimitExceeded&&o.terminationError===null&&o.error===null)try{await it(e.installation,{signal:t.signal})}catch(d){if(!t.signal.aborted)return I(i.task.id,"failed",r,a.evidence,{...l(d,"NODE_VITEST_INSTALLATION_BROKEN"),classification:"infra"})}const f=classifyNodeVitestProcessResult(o,a,i.task.timeoutMs,Z(t.signal.reason));return I(i.task.id,f.status,r,a.evidence,f.failure)}export function classifyNodeVitestProcessResult(t,e,i,r){const n=z(t,i,r,T);return n!==null?n:e.reportSummary?.numTotalTests===0?{failure:{code:"NODE_VITEST_NO_TESTS",message:"Vitest did not discover any tests for the task file"},status:"failed"}:mt(e.reportSummary)?{failure:null,status:"skipped"}:t.exitCode!==0?H(t,T,e.reportFailureMessage??h(t.stderr)??h(t.stdout),"Vitest"):e.reportError!==null?{failure:{...l(e.reportError,"NODE_VITEST_REPORT_INVALID"),classification:"infra"},status:"failed"}:e.reportFailureMessage!==null?{failure:{code:"NODE_VITEST_REPORT_FAILED",message:e.reportFailureMessage},status:"failed"}:e.dataEvidenceError!==null?{failure:l(e.dataEvidenceError,"NODE_VITEST_DATA_EVIDENCE_INVALID"),status:"failed"}:e.reportSummary===null||!e.reportSummary.success||e.reportSummary.numFailedTests>0||e.reportSummary.numPassedTests===0?{failure:{code:"NODE_VITEST_REPORT_FAILED",message:"Vitest report did not contain a completed passing test"},status:"failed"}:{failure:null,status:"passed"}}export function nodeVitestEvidenceCollectionFailure(t,e){const i=x(e);return G(t,T,i)??{...l(i,"NODE_VITEST_EVIDENCE_FAILED"),classification:"infra"}}function mt(t){return t!==null&&t.success&&t.numTotalTests>0&&t.numPassedTests===0&&t.numFailedTests===0&&(t.numPendingTests>0||t.numTodoTests>0)}function Et(t,e,i){const r=i.task,n=b(t,r.serviceName),o={...e.invocationEnvironment};return Object.assign(o,{...n===null?{}:{BASE_URL:n},FORCE_COLOR:"0",NODE_OPTIONS:Tt(i.prepared.resolutionBootstrapFile,i.prepared.inputBootstrapFile),NO_COLOR:"1",TESTKIT_ACTIVE:"1",[nt]:i.prepared.runtimeInput.file,[at]:i.prepared.runtimeInput.sha256,TESTKIT_INTERNAL_FILE_DEADLINE_MS:String(r.deadlineMs),TESTKIT_INTERNAL_FILE_TIMEOUT_SECONDS:String(Math.max(1,Math.ceil(r.timeoutMs/1e3))),TESTKIT_INVOCATION_ID:t.invocationId,TESTKIT_LEASE_DIR:r.orchestration.lease.directory,TESTKIT_LEASE_ID:r.orchestration.lease.id,TESTKIT_MODE:r.orchestration.mode,...r.orchestration.runtimeId===void 0?{}:{TESTKIT_RUNTIME_ID:r.orchestration.runtimeId},TESTKIT_TEST_FILE:r.file,TESTKIT_TEST_ID:r.id,...e.profileId==="node-vitest-data"?{TESTKIT_DATA_ARTIFACT_DIR:c.dirname(i.prepared.dataManifestFile)}:{}}),ot(o),o}function It(t,e,i,r){const n=b(t,i.serviceName),o=st(r.environment);return{context:{baseUrl:n,engineId:"node-vitest",invocationId:t.invocationId,profileId:e,serviceName:i.serviceName,task:{file:i.file,id:i.id,suiteName:i.suiteName,testType:i.testType}},engineId:"node-vitest",environment:o,fixtures:r.fixtures,invocationId:t.invocationId,profileId:e,...r.setupData===void 0?{}:{setupData:r.setupData},taskId:i.id,version:1}}function Tt(t,e){return[`--require=${JSON.stringify(t)}`,`--import=${JSON.stringify(X(e).href)}`].join(" ")}function b(t,e){return Object.hasOwn(t.target.endpoints,e)?t.target.endpoints[e]:null}async function vt(t,e){return{testkitConfig:await C(t,e.supportModules,"testkitConfig"),tsconfig:await C(t,e.supportModules,"tsconfig")}}async function C(t,e,i){if(!Object.hasOwn(e,i))return null;const r=e[i];if(!c.isAbsolute(r))throw new Error(`supportModules.${i} must be an absolute path`);return P(t,r,`supportModules.${i}`)}function _t(t,e){return{...lt({engineId:"node-vitest",installationKind:"root-direct-packages",profile:t.profile}),directPackages:{esbuild:e.packages.esbuild.version,typescript:e.packages.typescript.version,vite:e.packages.vite.version,vitest:e.packages.vitest.version}}}function gt(t){return{...l(t,"NODE_VITEST_PREPARE_FAILED"),classification:t instanceof A?"product":"infra"}}async function N(t){try{return await t()}catch(e){throw new A(e)}}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{isPathWithin as y}from"../../../process/files/paths.js";import u from"node:fs";import{createRequire as h}from"node:module";import i from"node:path";import{fileURLToPath as l}from"node:url";import{NODE_VITEST_PACKAGE_REQUIREMENTS as N}from"../../installations/
|
|
1
|
+
import{isPathWithin as y}from"../../../process/files/paths.js";import u from"node:fs";import{createRequire as h}from"node:module";import i from"node:path";import{fileURLToPath as l}from"node:url";import{NODE_VITEST_PACKAGE_REQUIREMENTS as N}from"../../installations/package-requirements.js";export const managedTypeScriptFile=l(import.meta.resolve("typescript"));const k=/^@gfdelarue\/testkit(?:\/|$)/u;export function createConsumerTypeScriptPathsPlugin(t,e){const r=h(import.meta.url)(managedTypeScriptFile),n=S(t,r),o=Object.keys(n.paths??{});return o.length===0?null:{enforce:"pre",name:"testkit-node-vitest-consumer-typescript-paths",resolveId(a,s){if(s===void 0||isNodeVitestManagedDependencyImporter(s,e.dependencyRoots)||k.test(a)||!o.some(g=>w(a,g)))return null;const c=x(s);if(c===null)return null;const f=r.resolveModuleName(a,c,n,r.sys).resolvedModule?.resolvedFileName;return f===void 0?null:assertNodeVitestConsumerResolution(a,f,s,e)}}}export function assertNodeVitestConsumerResolution(t,e,r,n){if(isNodeVitestManagedDependencyImporter(r,n.dependencyRoots))return e;const o=d(e);if(o===null)return e;const a=v(o);if(a===null)return e;if(a.privatePackage)throw new Error(`Node/Vitest consumers may not resolve ${JSON.stringify(t)} into private Vitest package ${a.packageName}`);const s=n.aliases[t];if(Object.hasOwn(n.aliases,t)&&m(o,s)||Object.values(n.aliases).some(c=>m(o,c)))return e;throw new Error(`Node/Vitest consumers may not resolve ${JSON.stringify(t)} into managed direct package ${a.packageName}; import an exact public ${JSON.stringify(a.packageName)} export instead`)}function v(t){const e=[t];try{const r=u.realpathSync(t);r!==t&&e.push(r)}catch{}for(const r of e){let n=i.dirname(r);for(;;){const o=i.join(n,"package.json");if(u.existsSync(o)){const s=E(o);if(s!==null){if(s.startsWith("@vitest/"))return{packageName:s,privatePackage:!0};if(N.some(({packageName:c})=>c===s))return{packageName:s,privatePackage:!1}}}const a=i.dirname(n);if(a===n)break;n=a}}return null}function E(t){let e;try{e=JSON.parse(u.readFileSync(t,"utf8"))}catch(n){throw new Error(`Cannot inspect package boundary at ${t}`,{cause:n})}if(typeof e!="object"||e===null||Array.isArray(e))return null;const r=Reflect.get(e,"name");return typeof r=="string"?r:null}export function isNodeVitestManagedDependencyImporter(t,e){if(t===void 0)return!1;const r=d(t);return r===null?!1:e.some(n=>y(n,r))}function d(t){const e=t.split(/[?#]/u,1)[0];if(e.startsWith("\0"))return null;if(e.startsWith("file:"))return l(e);const r=e.startsWith("/@fs/")?e.slice(4):e;return i.isAbsolute(r)?i.resolve(r):null}function m(t,e){try{return u.realpathSync(t)===u.realpathSync(e)}catch{return i.resolve(t)===i.resolve(e)}}function S(t,e){const r=i.resolve(t).replaceAll(i.sep,"/"),n=i.posix.dirname(r),o=e.readConfigFile(r,c=>e.sys.readFile(c));if(o.error!==void 0)throw new Error(p(r,[o.error],e));const a=e.parseJsonConfigFileContent(o.config,e.sys,n,{},r),s=a.errors.filter(c=>c.category===e.DiagnosticCategory.Error&&c.code!==18003);if(s.length>0)throw new Error(p(r,s,e));return a.options}function p(t,e,r){const n=r.formatDiagnostics(e,{getCanonicalFileName:o=>o,getCurrentDirectory:()=>i.posix.dirname(t),getNewLine:()=>`
|
|
2
2
|
`});return`Cannot load explicit consumer TypeScript paths from ${t}:
|
|
3
3
|
${n}`.trim()}function w(t,e){const r=e.indexOf("*");if(r===-1)return t===e;const n=e.slice(0,r),o=e.slice(r+1);return t.startsWith(n)&&t.endsWith(o)}function x(t){const e=t.split(/[?#]/u,1)[0];return e.startsWith("file:")?l(e):i.isAbsolute(e)?e:null}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ManagedNodeModuleResolution } from "../../managed-module-resolution.js";
|
|
2
|
-
import {
|
|
2
|
+
import type { NodeVitestAuthoringEntrypoints, NodeVitestInstallation } from "../../installations/node-vitest.js";
|
|
3
3
|
export interface NodeVitestResolutionEntrypoints extends NodeVitestAuthoringEntrypoints {
|
|
4
4
|
readonly cli: string;
|
|
5
5
|
readonly packages: NodeVitestInstallation["packages"];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import c from"node:path";import{createManagedNodeModuleResolution as d}from"../../managed-module-resolution.js";import{NODE_VITEST_PACKAGE_REQUIREMENTS as i,NODE_VITEST_PRIVATE_PACKAGE_PREFIXES as g}from"../../installations/
|
|
1
|
+
import c from"node:path";import{createManagedNodeModuleResolution as d}from"../../managed-module-resolution.js";import{NODE_VITEST_PACKAGE_REQUIREMENTS as i,NODE_VITEST_PRIVATE_PACKAGE_PREFIXES as g}from"../../installations/package-requirements.js";export function resolveNodeVitestManagedRuntimeResolution(t,a){const o=i.filter(e=>e.authoringResolution==="exact-entrypoint"),n=Object.fromEntries(i.filter(e=>e.authoringResolution==="package-exports").map(e=>[e.packageName,t.packages[e.packageName].entrypoint])),r=o.map(e=>t.packages[e.packageName].root),s=d({consumerRoots:a.consumerRoots,direct:{"@gfdelarue/testkit":t.root,"@gfdelarue/testkit/config":t.config,"@gfdelarue/testkit/node-vitest/contract":t.contract,"@gfdelarue/testkit/node-vitest/data":t.data,"@gfdelarue/testkit/package.json":t.rootPackageJson,...Object.fromEntries(o.map(e=>[e.packageName,t.packages[e.packageName].entrypoint]))},directBridgeRoots:[c.dirname(t.root),...r],engineLabel:"Node/Vitest",forbiddenConsumerPackagePrefixes:g,packageAnchors:n,reservedPackageNames:["@gfdelarue/testkit",...o.map(e=>e.packageName)],stateSymbol:"@gfdelarue/testkit/node-vitest/resolution-hooks/v1",trustedImporterFiles:a.trustedImporterFiles,trustedInternalEntrypoints:a.trustedInternalEntrypoints,verifiedParentlessPackageFiles:{vitest:["dist/workers/forks.js","suppress-warnings.cjs"]},verifiedProcessEntrypoints:[t.cli]});return Object.freeze({...s,dependencyRoots:Object.freeze([...new Set([...s.dependencyRoots,c.dirname(t.root),...r])])})}
|
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
import o from"node:fs/promises";import i from"node:path";import{fileURLToPath as g,pathToFileURL as a}from"node:url";import{NODE_VITEST_RUNTIME_INPUT_SYMBOL as p}from"../runtime/runtime-input.js";import{NODE_VITEST_PACKAGE_REQUIREMENTS as
|
|
1
|
+
import o from"node:fs/promises";import i from"node:path";import{fileURLToPath as g,pathToFileURL as a}from"node:url";import{NODE_VITEST_RUNTIME_INPUT_SYMBOL as p}from"../runtime/runtime-input.js";import{NODE_VITEST_PACKAGE_REQUIREMENTS as R}from"../../installations/package-requirements.js";import{resolveNodeVitestManagedRuntimeResolution as y}from"./resolution.js";import{nativeReporterFile as S}from"../../adapters/native-report.js";const u="elench.testkit.node-vitest.config-runtime.v1",l=g(new URL("../../../../dist/engines/node-vitest/",import.meta.url)),f=i.join(l,"adapter/consumer-typescript-paths.js"),m=i.join(l,"runtime/data/runner.js"),d=i.join(l,"runtime/runtime-input.js");export async function writeNodeVitestWorkspace(e){await o.mkdir(i.dirname(e.setupFile),{recursive:!0});const t=y(e.entrypoints,{consumerRoots:[e.rootDirectory],trustedImporterFiles:[e.inputBootstrapFile,e.setupFile,e.vitestConfigFile],trustedInternalEntrypoints:[f,d]});await Promise.all([o.writeFile(e.setupFile,I(e.configFile,e.runtimeInput.expectedProfileId),"utf8"),o.writeFile(e.inputBootstrapFile,N(e.runtimeInput),"utf8"),o.writeFile(e.resolutionBootstrapFile,t.bootstrapSource,{encoding:"utf8",flag:"wx",mode:384}),o.writeFile(e.vitestConfigFile,O(e,t),"utf8")])}function N(e){const t=a(d).href,n=a(f).href;return`
|
|
2
2
|
const inputRuntime = await import(${JSON.stringify(t)});
|
|
3
|
-
const consumerResolutionRuntime = await import(${JSON.stringify(n)});
|
|
4
3
|
const { installNodeVitestRuntimeInput } = inputRuntime;
|
|
5
4
|
await installNodeVitestRuntimeInput(${JSON.stringify(e)});
|
|
6
5
|
const configRuntimeSymbol = Symbol.for(${JSON.stringify(u)});
|
|
7
6
|
if (Object.hasOwn(globalThis, configRuntimeSymbol))
|
|
8
7
|
throw new Error("Managed Node/Vitest config runtime is already installed");
|
|
8
|
+
// Only the Vitest parent requests config policy. Fork workers still validate
|
|
9
|
+
// their own input, but do not load the unused TypeScript alias-policy graph.
|
|
10
|
+
let loadedConfigRuntime;
|
|
11
|
+
const load = Object.freeze(() => {
|
|
12
|
+
loadedConfigRuntime ??= (async () => {
|
|
13
|
+
const consumerResolutionRuntime = await import(${JSON.stringify(n)});
|
|
9
14
|
const configRuntimeFunctions = [
|
|
10
15
|
consumerResolutionRuntime.assertNodeVitestConsumerResolution,
|
|
11
16
|
consumerResolutionRuntime.createConsumerTypeScriptPathsPlugin,
|
|
@@ -16,13 +21,17 @@ for (const value of configRuntimeFunctions) {
|
|
|
16
21
|
throw new Error("Managed Node/Vitest config runtime is invalid");
|
|
17
22
|
Object.freeze(value);
|
|
18
23
|
}
|
|
19
|
-
|
|
24
|
+
return Object.freeze({
|
|
20
25
|
assertNodeVitestConsumerResolution: configRuntimeFunctions[0],
|
|
21
26
|
createConsumerTypeScriptPathsPlugin: configRuntimeFunctions[1],
|
|
22
27
|
isNodeVitestManagedDependencyImporter: configRuntimeFunctions[2],
|
|
23
28
|
managedTypeScriptFile: consumerResolutionRuntime.managedTypeScriptFile,
|
|
24
29
|
version: 1,
|
|
25
30
|
});
|
|
31
|
+
})();
|
|
32
|
+
return loadedConfigRuntime;
|
|
33
|
+
});
|
|
34
|
+
const configRuntime = Object.freeze({ load, version: 1 });
|
|
26
35
|
Object.defineProperty(
|
|
27
36
|
globalThis,
|
|
28
37
|
configRuntimeSymbol,
|
|
@@ -62,9 +71,20 @@ __testkitRegisterNodeVitestRepositoryConfiguration(
|
|
|
62
71
|
if (installedRuntimeInput?.invocationId !== ${JSON.stringify(e.runtimeInput.expectedInvocationId)}
|
|
63
72
|
|| installedRuntimeInput.profileId !== ${JSON.stringify(e.runtimeInput.expectedProfileId)}
|
|
64
73
|
|| installedRuntimeInput.taskId !== ${JSON.stringify(e.runtimeInput.expectedTaskId)})
|
|
65
|
-
throw new Error("Managed Node/Vitest input bootstrap was not installed in the Vitest parent");`,s=`const
|
|
74
|
+
throw new Error("Managed Node/Vitest input bootstrap was not installed in the Vitest parent");`,s=`const configRuntimeSupplier = globalThis[
|
|
66
75
|
Symbol.for(${JSON.stringify(u)})
|
|
67
76
|
];
|
|
77
|
+
if (
|
|
78
|
+
typeof configRuntimeSupplier !== "object" ||
|
|
79
|
+
configRuntimeSupplier === null ||
|
|
80
|
+
!Object.isFrozen(configRuntimeSupplier) ||
|
|
81
|
+
Reflect.ownKeys(configRuntimeSupplier).length !== 2 ||
|
|
82
|
+
configRuntimeSupplier.version !== 1 ||
|
|
83
|
+
typeof configRuntimeSupplier.load !== "function" ||
|
|
84
|
+
!Object.isFrozen(configRuntimeSupplier.load)
|
|
85
|
+
)
|
|
86
|
+
throw new Error("Managed Node/Vitest config runtime supplier is invalid");
|
|
87
|
+
const configRuntime = await configRuntimeSupplier.load();
|
|
68
88
|
const configRuntimeKeys = [
|
|
69
89
|
"assertNodeVitestConsumerResolution",
|
|
70
90
|
"createConsumerTypeScriptPathsPlugin",
|
|
@@ -98,7 +118,7 @@ if (managedTypeScriptFile !== ${JSON.stringify(e.entrypoints.packages.typescript
|
|
|
98
118
|
${n}
|
|
99
119
|
${s}
|
|
100
120
|
const managedAliases = Object.freeze(${JSON.stringify(t.aliases)});
|
|
101
|
-
const managedPackageNames = Object.freeze(${JSON.stringify(
|
|
121
|
+
const managedPackageNames = Object.freeze(${JSON.stringify(R.map(({packageName:r})=>r))});
|
|
102
122
|
const managedDependencyRoots = Object.freeze(${JSON.stringify(t.dependencyRoots)});
|
|
103
123
|
const consumerResolutionPolicy = Object.freeze({
|
|
104
124
|
aliases: managedAliases,
|
|
@@ -115,6 +135,7 @@ export default {
|
|
|
115
135
|
enforce: "pre",
|
|
116
136
|
name: "testkit-node-vitest-managed-direct-packages",
|
|
117
137
|
async resolveId(specifier, importer, options) {
|
|
138
|
+
|
|
118
139
|
const managedImporter = isNodeVitestManagedDependencyImporter(
|
|
119
140
|
importer,
|
|
120
141
|
managedDependencyRoots,
|
|
@@ -146,6 +167,7 @@ export default {
|
|
|
146
167
|
${c}
|
|
147
168
|
],
|
|
148
169
|
root: ${JSON.stringify(e.rootDirectory)},
|
|
170
|
+
cacheDir: ${JSON.stringify(i.join(i.dirname(e.vitestConfigFile),"cache"))},
|
|
149
171
|
resolve: {
|
|
150
172
|
alias: Object.entries(managedAliases).map(([specifier, replacement]) => ({
|
|
151
173
|
find: exactSpecifierPattern(specifier),
|
|
@@ -153,9 +175,11 @@ export default {
|
|
|
153
175
|
})),
|
|
154
176
|
},
|
|
155
177
|
test: {
|
|
156
|
-
${e.runtimeInput.expectedProfileId==="node-vitest-data"?`runner: ${JSON.stringify(
|
|
157
|
-
|
|
158
|
-
|
|
178
|
+
${e.runtimeInput.expectedProfileId==="node-vitest-data"?`runner: ${JSON.stringify(m)},`:""}
|
|
179
|
+
// The installed SDK is compiled JavaScript. Use its native module instance
|
|
180
|
+
// for setup and consumer imports instead of transforming a second instance.
|
|
181
|
+
server: { deps: { external: ${JSON.stringify([e.entrypoints.config,...e.runtimeInput.expectedProfileId==="node-vitest-data"?[m,e.entrypoints.data]:[]].map(r=>r.replaceAll(i.sep,"/")))}.map(exactSpecifierPattern) } },
|
|
182
|
+
reporters: [[${JSON.stringify(S("node-vitest"))}, ${JSON.stringify(e.report)}]],
|
|
159
183
|
allowOnly: false,
|
|
160
184
|
disableConsoleIntercept: true,
|
|
161
185
|
environment: "node",
|
|
@@ -174,4 +198,4 @@ export default {
|
|
|
174
198
|
watch: false,
|
|
175
199
|
},
|
|
176
200
|
};
|
|
177
|
-
`}const
|
|
201
|
+
`}const v=new Set(["!","(",")","*","+","?","@","[","\\","]","{","}"]);export function toVitestIncludePattern(e){return Array.from(e.replaceAll(i.sep,"/"),t=>v.has(t)?`\\${t}`:t).join("")}
|
|
@@ -49,6 +49,7 @@ export interface InstallNodeVitestRuntimeInputRequest {
|
|
|
49
49
|
readonly expectedInvocationId: string;
|
|
50
50
|
readonly expectedProfileId: NodeVitestProfileId;
|
|
51
51
|
readonly expectedSha256: string;
|
|
52
|
+
readonly expectedSizeBytes: number;
|
|
52
53
|
readonly expectedTaskId: string;
|
|
53
54
|
}
|
|
54
55
|
export declare function writeOwnedNodeVitestRuntimeInput(taskDirectory: string, document: NodeVitestRuntimeInputDocument): Promise<OwnedNodeVitestRuntimeInput>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import m from"node:
|
|
1
|
+
import m from"node:path";import{readVerifiedOwnedFile as a,writeOwnedRuntimeInput as w,RUNTIME_INPUT_BYTE_MEASURE as S}from"../../runtime-input-file.js";import{requireBoundedJsonValue as O,RUNTIME_INPUT_JSON_BOUNDS as h}from"../../contracts/json-bounds.js";import{ENGINE_PROCESS_RESERVED_ENVIRONMENT_KEYS as V}from"../../../process/environment.js";import{installManagedProcessEnvironmentOverlay as R}from"../../../process/environment-overlay.js";export const NODE_VITEST_RUNTIME_INPUT_VERSION=1,NODE_VITEST_RUNTIME_INPUT_FILENAME="runtime-input.json",NODE_VITEST_RUNTIME_INPUT_FILE_ENV="TESTKIT_NODE_VITEST_INPUT_FILE",NODE_VITEST_RUNTIME_INPUT_SHA256_ENV="TESTKIT_NODE_VITEST_INPUT_SHA256",NODE_VITEST_RUNTIME_INPUT_SYMBOL="elench.testkit.node-vitest.runtime-input.v1",NODE_VITEST_DATA_BUDGET_FAILURE_DETAIL_KIND="data-flow-budget",NODE_VITEST_DATA_FAILURE_DETAILS_ARTIFACT_KIND="testkit.failure-details",NODE_VITEST_DATA_FAILURE_DETAILS_ARTIFACT_NAME="data-flow-failure-details";const d={filename:NODE_VITEST_RUNTIME_INPUT_FILENAME,label:"Node/Vitest runtime input",maxSizeBytes:8*1024*1024},f=512,p=4*1024*1024,l=new Set(["BASE_URL","FORCE_COLOR","NODE_OPTIONS","NO_COLOR","UPDATE_SNAPSHOT",...V].map(e=>e.toUpperCase()));export const MAX_NODE_VITEST_SPAWN_ENVIRONMENT_UNITS=24*1024;export async function writeOwnedNodeVitestRuntimeInput(e,t){return w(e,t,d)}export async function verifyOwnedNodeVitestRuntimeInput(e){await a(e,d)}export function selectNodeVitestForwardedEnvironment(e){const t={...e};return _(t),t}export function assertNodeVitestSpawnEnvironmentBudget(e){if(Object.entries(e).reduce((n,[i,r])=>n+i.length+r.length+2,1)>MAX_NODE_VITEST_SPAWN_ENVIRONMENT_UNITS)throw new Error(`Node/Vitest process environment exceeds the ${MAX_NODE_VITEST_SPAWN_ENVIRONMENT_UNITS.toString()} character environment-block budget`)}export async function installNodeVitestRuntimeInput(e){const t=process.env[NODE_VITEST_RUNTIME_INPUT_FILE_ENV],n=process.env[NODE_VITEST_RUNTIME_INPUT_SHA256_ENV];if(t!==e.expectedFile)throw new Error("Node/Vitest runtime input pointer does not match its engine-owned path");if(n!==e.expectedSha256)throw new Error("Node/Vitest runtime input digest does not match its prepared value");const i={directory:m.dirname(e.expectedFile),file:e.expectedFile,sha256:e.expectedSha256,sizeBytes:e.expectedSizeBytes},r=await a(i,d);let s;try{s=JSON.parse(r)}catch(u){throw new Error("Node/Vitest runtime input contains invalid JSON",{cause:u})}const o=E(I(s));if(o.invocationId!==e.expectedInvocationId||o.profileId!==e.expectedProfileId||o.taskId!==e.expectedTaskId)throw new Error("Node/Vitest runtime input identity does not match its task");R({environment:o.environment,label:"Node/Vitest"}),Object.defineProperty(globalThis,Symbol.for(NODE_VITEST_RUNTIME_INPUT_SYMBOL),{configurable:!0,enumerable:!1,value:o,writable:!1}),N(o)}export function applyNodeVitestEngineEnvironment(){const e=readNodeVitestRuntimeInput();if(e===null)throw new Error("Node/Vitest runtime input is not installed");N(e)}function N(e){const t=e.context.baseUrl;typeof t=="string"?process.env.BASE_URL=t:Reflect.deleteProperty(process.env,"BASE_URL")}export function readNodeVitestRuntimeInput(){const e=Reflect.get(globalThis,Symbol.for(NODE_VITEST_RUNTIME_INPUT_SYMBOL));return e===void 0?null:E(I(e,"Installed Node/Vitest runtime input"))}function I(e,t="Node/Vitest runtime input"){if(O(e,t,h,S),!c(e))throw new Error(`${t} must contain an object`);const n=new Set(["context","engineId","environment","fixtures","invocationId","profileId","setupData","taskId","version"]);for(const r of Object.keys(e))if(!n.has(r))throw new Error(`${t} contains unknown field ${r}`);if(e.version!==NODE_VITEST_RUNTIME_INPUT_VERSION)throw new Error(`${t}.version must be 1`);if(e.engineId!=="node-vitest")throw new Error(`${t}.engineId must be "node-vitest"`);if(!D(e.profileId))throw new Error(`${t}.profileId is invalid`);if(!c(e.context)||!c(e.fixtures))throw new Error(`${t} context and fixtures must be objects`);const i=x(e.environment,`${t}.environment`);return _(i),{context:e.context,engineId:"node-vitest",environment:i,fixtures:e.fixtures,invocationId:T(e.invocationId,`${t}.invocationId`),profileId:e.profileId,...Object.hasOwn(e,"setupData")?{setupData:e.setupData}:{},taskId:T(e.taskId,`${t}.taskId`),version:NODE_VITEST_RUNTIME_INPUT_VERSION}}function _(e){const t=Object.entries(e);if(t.length>f)throw new Error(`Node/Vitest forwarded environment exceeds the ${f.toString()} entry limit`);let n=0;const i=new Set;for(const[r,s]of t){y(r,s,"Node/Vitest forwarded environment");const o=r.toUpperCase();if(i.has(o))throw new Error(`Node/Vitest forwarded environment contains case-insensitive duplicate key ${r}`);if(i.add(o),o.startsWith("TESTKIT_")||o.startsWith("VITEST_")||l.has(o))throw new Error(`Node/Vitest forwarded environment contains reserved key ${r}`);n+=Buffer.byteLength(r)+Buffer.byteLength(s)+2}if(n>p)throw new Error(`Node/Vitest forwarded environment exceeds the ${p.toString()} byte limit`)}function x(e,t){if(!c(e))throw new Error(`${t} must be an object`);const n=Object.entries(e);if(!n.every(i=>typeof i[1]=="string"))throw new Error(`${t} values must all be strings`);return Object.fromEntries(n)}function y(e,t,n){if(e.length===0||e.includes("=")||e.includes("\0"))throw new Error(`${n} contains an invalid variable name`);if(t.includes("\0"))throw new Error(`${n}.${e} contains a NUL character`)}function E(e){if(e!==null&&typeof e=="object"&&!Object.isFrozen(e)){Object.freeze(e);for(const t of Object.values(e))E(t)}return e}function T(e,t){if(typeof e!="string"||e.length===0)throw new Error(`${t} must be a non-empty string`);return e}function D(e){return e==="node-vitest-contract"||e==="node-vitest-data"}function c(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}
|
|
@@ -6,9 +6,11 @@ export declare const RUNTIME_INPUT_BYTE_MEASURE: {
|
|
|
6
6
|
/**
|
|
7
7
|
* Single-writer protocol for the files a host prepares inside an owned task
|
|
8
8
|
* directory and later hands to a guest process by pointer + digest. Writes are
|
|
9
|
-
* exclusive-create (0o600),
|
|
9
|
+
* exclusive-create (0o600), closed, and atomically renamed into place; reads
|
|
10
10
|
* re-verify the file is still an owned regular file with the prepared size,
|
|
11
11
|
* owner-only permissions, identity realpath, and matching SHA-256 digest.
|
|
12
|
+
* These disposable handoffs require process-lifetime visibility, not persistence
|
|
13
|
+
* across machine power loss; durable evidence uses its separate synced writer.
|
|
12
14
|
*/
|
|
13
15
|
export interface OwnedFileProtocol {
|
|
14
16
|
readonly filename: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import u from"node:crypto";import a from"node:fs/promises";import o from"node:path";import{serializeBoundedJson as c,RUNTIME_INPUT_JSON_BOUNDS as h}from"./contracts/json-bounds.js";import{readStableEvidenceFile as y,evidenceFileAccess as b}from"./adapters/evidence-files.js";export const ownedRuntimeInputModuleUrl=new URL("../../dist/engines/runtime-input-file.js",import.meta.url).href,RUNTIME_INPUT_BYTE_MEASURE={unitLabel:"bytes",unitsOf:t=>Buffer.byteLength(t,"utf8")};export async function writeOwnedRuntimeInput(t,e,i){const
|
|
1
|
+
import u from"node:crypto";import a from"node:fs/promises";import o from"node:path";import{serializeBoundedJson as c,RUNTIME_INPUT_JSON_BOUNDS as h}from"./contracts/json-bounds.js";import{readStableEvidenceFile as y,evidenceFileAccess as b}from"./adapters/evidence-files.js";export const ownedRuntimeInputModuleUrl=new URL("../../dist/engines/runtime-input-file.js",import.meta.url).href,RUNTIME_INPUT_BYTE_MEASURE={unitLabel:"bytes",unitsOf:t=>Buffer.byteLength(t,"utf8")};export async function writeOwnedRuntimeInput(t,e,i){const r=c(e,i.label,h,RUNTIME_INPUT_BYTE_MEASURE),s=await m(t,i.label),w=Buffer.byteLength(r);if(w>i.maxSizeBytes)throw new Error(`${i.label} exceeds the ${i.maxSizeBytes.toString()} byte limit`);const f=o.join(s,i.filename),l=o.join(s,`.${i.filename}.${process.pid.toString()}.${u.randomUUID()}.tmp`);let n=null;try{n=await a.open(l,"wx",384),await n.writeFile(r,"utf8"),await n.close(),n=null,await a.rename(l,f),await a.chmod(f,384)}catch(d){throw await n?.close().catch(()=>{}),await a.rm(l,{force:!0}).catch(()=>{}),d}return{directory:s,file:f,sha256:u.createHash("sha256").update(r,"utf8").digest("hex"),sizeBytes:w}}export async function readVerifiedOwnedFile(t,e){const i=await m(t.directory,e.label);if(o.dirname(t.file)!==i)throw new Error(`${e.label} escaped its owned task directory`);const r=await a.lstat(t.file,{bigint:!0});if(!r.isFile()||r.isSymbolicLink())throw new Error(`${e.label} must be an owned regular file`);if(r.size!==BigInt(t.sizeBytes)||r.size>BigInt(e.maxSizeBytes))throw new Error(`${e.label} size changed after preparation`);if(process.platform!=="win32"&&(r.mode&63n)!==0n)throw new Error(`${e.label} permissions must be owner-only`);const s=await a.realpath(t.file);if(s!==t.file||o.dirname(s)!==i)throw new Error(`${e.label} resolved outside its owned task directory`);const{contents:w,sha256:f}=await y(s,e.maxSizeBytes,e.label,b,r),l=Buffer.from(t.sha256),n=Buffer.from(f);if(l.length!==n.length||!u.timingSafeEqual(n,l))throw new Error(`${e.label} integrity check failed`);return w.toString("utf8")}async function m(t,e){if(!o.isAbsolute(t))throw new Error(`${e} directory must be absolute`);const i=await a.realpath(t);if(!(await a.stat(i)).isDirectory())throw new Error(`${e} directory must be a directory`);return i}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { SupervisedProcessHandle, SupervisedProcessReady } from "../process/index.js";
|
|
2
|
+
export { isRunManifestActive } from "../process/runs/inspection.js";
|
|
2
3
|
import type { RuntimeInstanceContext } from "./runtime-contexts.js";
|
|
3
4
|
import type { LocalServiceLifecycle } from "./services.js";
|
|
4
5
|
import { type ProductLifecycleFence } from "../process/lifecycle-fence.js";
|
|
@@ -66,6 +67,4 @@ export declare function createUnpublishedRunLifecycle(productDir: string, option
|
|
|
66
67
|
export declare function cleanupOwnedRunLifecycleWithinLifecycleFence(fence: Readonly<ProductLifecycleFence>, lifecycle: Readonly<RunLifecycle>): boolean;
|
|
67
68
|
export declare function readOwnedRunLifecycleWithinLifecycleFence(fence: Readonly<ProductLifecycleFence>, lifecycle: Readonly<RunLifecycle>): OwnedRunLifecycleState;
|
|
68
69
|
export declare function findPortOwner(productDir: string, { host, port }: Readonly<RunPortQuery>): RunPortOwner | null;
|
|
69
|
-
export declare function isRunManifestActive(manifest: Readonly<RunManifest>): boolean;
|
|
70
70
|
export declare function disposeFailedRunLifecycle(lifecycle: RunLifecycle): void;
|
|
71
|
-
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import
|
|
1
|
+
import H from"crypto";import{isTerminalRunStatus as x}from"../process/runs/schema.js";import{observeProcessCreationIdentity as U}from"../process/process-identity.js";import{isRunManifestActive as B}from"../process/runs/inspection.js";export{isRunManifestActive}from"../process/runs/inspection.js";import{normalizeOperationReason as _}from"../domain/operation-reason.js";import{finalizeLiveRunArtifact as Y}from"../evidence/index.js";import{captureProcessCreationIdentity as J}from"../process/service-processes.js";import{requireProductLifecycleFenceProductDir as K,withProductLifecycleFence as Q}from"../process/lifecycle-fence.js";import{createRunManifest as X,listRunManifests as $,readRunMaintenanceStopRequest as Z,removeRunManifestFile as G,requireSafeRunId as ee,RUN_MANIFEST_SCHEMA_VERSION as te,runManifestPath as ne,writeRunManifest as re}from"../process/runs/manifest.js";const ie=1e3,oe=String,T=new WeakMap;export function createUnpublishedRunLifecycle(n,r={}){const i=r.runId===void 0?ce():ee(r.runId,"Testkit run id"),a=ne(n,i),s=new AbortController,m=new AbortController,u=r.pid===void 0?process.pid:r.pid;if(!Number.isSafeInteger(u)||u<0)throw new Error("Testkit run pid must be a non-negative safe integer");const b=u>0?J(u):null;if(u>0&&b===null)throw new Error(`Could not capture the Testkit run process identity for pid ${u.toString()}`);let f={incarnationId:H.randomUUID(),schemaVersion:te,runId:i,pid:u,processIdentity:typeof b=="string"?b:null,controller:ae(r.controller),status:"starting",startedAt:new Date().toISOString(),heartbeatAt:r.heartbeat===!1?null:new Date().toISOString(),interruptReason:null,services:[],runtimeDirs:[]};const O=[];let w,D=null;const y=new Set,S=new Set,L=r.keepAlive===!0?setInterval(()=>{},ie):null;let R=null,g=null,v=null,P=!1;function j(e){const t=T.get(I);if(!t)throw new Error("Testkit run lifecycle ownership is unavailable");if(!t.published){X(n,e),t.published=!0;return}re(n,e)}function A(){const e=Z(n,i);return e===null||e.targetIncarnationId!==f.incarnationId||e.targetPid!==u||e.targetProcessIdentity!==f.processIdentity||e.coordinatorPid===D?.coordinatorPid&&e.coordinatorProcessIdentity===D.coordinatorProcessIdentity?null:U(e.coordinatorPid,e.coordinatorProcessIdentity)==="different"?(D=e,null):e}function h(e){k("mutate run state");const t=se(f);e(t),t.heartbeatAt!==null&&(t.heartbeatAt=new Date().toISOString()),j(t),f=t}function k(e){if(g!==null)throw new Error(`Cannot ${e} after Testkit run ${i} finalization is committed`)}function W(){R||(R=setInterval(()=>{},1e3))}function q(){R&&(clearInterval(R),R=null)}function N(){for(const e of S)clearInterval(e);S.clear()}function z(e="controller process exited before Testkit run finalized"){if(P||x(f.status))return!1;const t=_(e,"controller process exited before Testkit run finalized"),o=new Date,l=f.status==="interrupting"||s.signal.aborted?"interrupted":"failed",c=l==="interrupted"?f.interruptReason??v??t:t;return h(p=>{p.status=l,p.finishedAt=o.toISOString(),p.interruptReason=c,p.heartbeatAt!==null&&(p.heartbeatAt=o.toISOString())}),Y(n,{runId:i,reason:c,status:l,finishedAt:o}),!0}const F=()=>{z()};process.on("exit",F);const I={runId:i,manifestPath:a,maintenanceStopSignal:m.signal,signal:s.signal,commitFinalization(e="finished"){if(g!==null)return g;const t=A(),o=s.signal.aborted||t!==null||f.status==="interrupting",l=o?f.interruptReason??v??t?.reason??"run interrupted":null,c=Object.freeze({interruptReason:l,interrupted:o});return h(p=>{p.status=o?"interrupted":e,p.finishedAt=new Date().toISOString(),p.interruptReason=l}),g=c,N(),c},getInterruptReason(){return f.interruptReason??v},isMaintenanceStopCoordinated(){return A()!==null},isStopRequested(){return s.signal.aborted||g?.interrupted===!0},markRunning(){h(e=>{e.status="running"})},markFinished(e="finished"){h(t=>{t.status=e,t.finishedAt=new Date().toISOString()})},heartbeat(e={}){h(t=>{t.heartbeatAt=new Date().toISOString(),e&&typeof e=="object"&&!Array.isArray(e)&&(t.session={...t.session||{},...e})})},startHeartbeat(e=5e3,t=null){k("start a run heartbeat");const o=Number.isFinite(Number(e))&&Number(e)>0?Math.floor(Number(e)):5e3,l=setInterval(()=>{let c;try{c=typeof t=="function"?t():{}}catch{c={}}I.heartbeat(c)},o);return l.unref(),S.add(l),()=>{clearInterval(l),S.delete(l)}},requestStop(e="interrupted"){if(g!==null||x(f.status))return;const t=A(),o=t?t.reason:_(e,"interrupted"),l=!s.signal.aborted;l&&(v=o),W();let c=null;if(l)try{h(d=>{d.status="interrupting",d.interruptReason=o})}catch(d){c=d instanceof Error?d:new Error("Failed to persist the Testkit stop request",{cause:d})}t!==null&&!m.signal.aborted&&m.abort(new Error(`Testkit run ${i} is being stopped by external maintenance`));const p=[...y].map(({supervisor:d})=>d.stop(t===null?void 0:Date.parse(t.requestedAt)+d.shutdownTimeoutMs));if(Promise.allSettled(p).then(q),s.signal.aborted||s.abort(new Error(`testkit run interrupted (${o})`)),!!l&&c!==null)throw c},trackGraphContext(e){h(t=>{le(t.runtimeDirs,e.runtimeDir)})},registerService(e,t,o,l){if(k("register application service ownership"),o.pid!==t.pid||(process.platform==="win32"?o.recovery.processGroupId!==null:o.recovery.processGroupId!==o.pid))throw new Error("Application service supervisor returned an invalid ownership handoff");const c={supervisor:t,unsubscribe:()=>{}};y.add(c);const p=ue(e);h(d=>{d.services=d.services.filter(M=>M.pid!==o.pid),d.services.push({serviceName:e.name,runtimeLabel:e.runtimeLabel,command:e.testkit.local.start||null,cwd:l,pid:o.pid,processGroupId:o.recovery.processGroupId,processIdentity:o.recovery.creationIdentity,ports:p,startedAt:new Date().toISOString(),shutdownTimeoutMs:t.shutdownTimeoutMs,shutdownDeadlineAt:t.shutdownDeadlineAt})}),c.unsubscribe=t.onShutdownStarted(d=>{h(M=>{const E=M.services.find(V=>V.pid===o.pid);E&&(E.shutdownDeadlineAt=E.shutdownDeadlineAt==null?d:Math.min(E.shutdownDeadlineAt,d))})})},unregisterService(e){k("unregister application service ownership");for(const t of y)t.supervisor.pid===e&&(t.unsubscribe(),y.delete(t));h(t=>{t.services=t.services.filter(o=>o.pid!==e)})},installSignalHandlers(){if(!(O.length>0)&&!(u!==process.pid||U(u,f.processIdentity)!=="matches")){for(const e of["SIGINT","SIGTERM","SIGHUP"]){const t=I.requestStop.bind(I,e);process.on(e,t),O.push([e,t])}process.platform==="win32"&&(w=setInterval(()=>{const e=A();e!==null&&(clearInterval(w),I.requestStop(e.reason))},100),w.unref(),S.add(w))}},removeSignalHandlers(){w&&(clearInterval(w),S.delete(w),w=void 0);for(const[e,t]of O.splice(0))process.off(e,t)},async removeManifest(){await Q(n,e=>cleanupOwnedRunLifecycleWithinLifecycleFence(e,I))},finalizeForControllerExit:z,dispose(){P=!0;for(const e of y)e.unsubscribe();process.off("exit",F),I.removeSignalHandlers(),L&&clearInterval(L),q(),N()}};return T.set(I,{incarnationId:f.incarnationId,pid:u,processIdentity:f.processIdentity,productDir:n,published:!1,runId:i}),I}function se(n){const r={...n,controller:n.controller===null?null:{...n.controller},runtimeDirs:[...n.runtimeDirs],services:n.services.map(i=>({...i,ports:i.ports.map(a=>({...a}))}))};return n.session!==void 0&&(r.session=n.session===null?null:{...n.session}),r}function ae(n){return!n||typeof n!="object"||Array.isArray(n)?null:Object.fromEntries(Object.entries(n).filter(([,r])=>r!==void 0).map(([r,i])=>[r,i===null?null:oe(i)]))}export function cleanupOwnedRunLifecycleWithinLifecycleFence(n,r){const{manifest:i,ownership:a,productDir:s}=readOwnedRunLifecycleWithinLifecycleFence(n,r);if(!i)return!1;if(i.services.length>0)throw new Error(`Cannot clean Testkit run ${r.runId} service ownership while holding the lifecycle fence`);return G(s,r.runId),a.published=!1,!0}export function readOwnedRunLifecycleWithinLifecycleFence(n,r){const i=K(n),a=T.get(r);if(!a||a.productDir!==i)throw new Error("Cannot clean a Testkit run without matching lifecycle ownership");if(a.runId!==r.runId)throw new Error("Testkit run lifecycle identity has been modified");const s=$(i).find(m=>m.runId===r.runId);if(s!==void 0&&(s.incarnationId!==a.incarnationId||s.pid!==a.pid||s.processIdentity!==a.processIdentity))throw new Error(`Testkit run ${r.runId} ownership no longer matches its manifest`);return{manifest:s??null,ownership:a,productDir:i}}export function findPortOwner(n,{host:r,port:i}){const a=$(n);for(const s of a)for(const m of s.services)for(const u of m.ports)if(C(u.host)===C(r)&&u.port===Number(i))return{manifest:s,service:m,active:B(s)};return null}function ue(n){const r=new Set,i=[];for(const a of[n.testkit.local.baseUrl,n.testkit.local.readyUrl])if(a)try{const s=new URL(a),m=C(s.hostname),u=Number(s.port||(s.protocol==="https:"?443:80)),b=`${m}:${u.toString()}`;if(r.has(b))continue;r.add(b),i.push({host:m,port:u})}catch{}return i}function le(n,r){r&&(n.includes(r)||n.push(r))}function ce(){return`${Date.now().toString()}-${process.pid.toString()}-${H.randomBytes(4).toString("hex")}`}function C(n){return n?n==="::1"?"127.0.0.1":n:"127.0.0.1"}export function disposeFailedRunLifecycle(n){const r=T.get(n);n.dispose(),n.removeSignalHandlers(),r?.published===!0&&G(r.productDir,n.runId)}
|
|
@@ -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}
|