@gfdelarue/testkit 0.3.6 → 0.4.0
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/dist/app/run/engine-execution.d.ts +2 -0
- package/dist/app/run/engine-execution.js +1 -1
- package/dist/cli/command-catalog.d.ts +0 -127
- package/dist/cli/command-catalog.js +1 -1
- package/dist/cli/command-registry.d.ts +1 -0
- package/dist/cli/command-registry.js +1 -1
- package/dist/cli/entrypoint.js +1 -1
- package/dist/cli/framework/execute.d.ts +1 -0
- package/dist/cli/framework/execute.js +6 -6
- package/dist/cli/index.js +1 -1
- package/dist/cli/version-drift.js +7 -7
- package/dist/config/authoring/index.d.ts +0 -5
- package/dist/config/loader/index.d.ts +0 -1
- package/dist/config/loader/index.js +1 -1
- package/dist/engines/adapters/node-startup-cache.d.ts +8 -0
- package/dist/engines/adapters/node-startup-cache.js +1 -0
- package/dist/engines/api/execution.d.ts +2 -0
- package/dist/engines/managed-module-resolution.js +8 -1
- package/dist/engines/node-vitest/adapter/adapter.js +1 -1
- package/dist/engines/playwright/adapter/adapter.js +1 -1
- package/dist/environment/ready-environment.d.ts +3 -1
- package/dist/environment/ready-environment.js +1 -1
- package/dist/environment/run-environment.js +1 -1
- package/dist/environment/runtime-contexts.d.ts +1 -0
- package/dist/environment/runtime-contexts.js +1 -1
- package/dist/environment/runtime-manager.js +1 -1
- package/dist/evidence/bundles.d.ts +2 -3
- package/dist/evidence/bundles.js +1 -1
- package/dist/evidence/contracts.d.ts +0 -8
- package/dist/evidence/index.d.ts +1 -1
- package/dist/evidence/index.js +1 -1
- package/dist/process/execa-bundle.json +1 -1
- package/docs/engine-architecture.md +20 -0
- package/package.json +3 -10
- package/testkit-config-schema.md +0 -23
- package/dist/app/cloud/campaigns.d.ts +0 -6
- package/dist/app/cloud/campaigns.js +0 -1
- package/dist/app/cloud/control-actions.d.ts +0 -12
- package/dist/app/cloud/control-actions.js +0 -2
- package/dist/app/cloud/control-links.d.ts +0 -5
- package/dist/app/cloud/control-links.js +0 -1
- package/dist/app/cloud/control-read.d.ts +0 -24
- package/dist/app/cloud/control-read.js +0 -1
- package/dist/app/cloud/control-resources.d.ts +0 -3
- package/dist/app/cloud/control-resources.js +0 -1
- package/dist/app/cloud/control.d.ts +0 -34
- package/dist/app/cloud/control.js +0 -5
- package/dist/app/cloud/credentials.d.ts +0 -11
- package/dist/app/cloud/credentials.js +0 -2
- package/dist/app/cloud/index.d.ts +0 -22
- package/dist/app/cloud/index.js +0 -2
- package/dist/app/cloud/login.d.ts +0 -63
- package/dist/app/cloud/login.js +0 -2
- package/dist/app/cloud/migration/archive.d.ts +0 -13
- package/dist/app/cloud/migration/archive.js +0 -1
- package/dist/app/cloud/migration/commands.d.ts +0 -8
- package/dist/app/cloud/migration/commands.js +0 -1
- package/dist/app/cloud/migration/evidence.d.ts +0 -13
- package/dist/app/cloud/migration/evidence.js +0 -2
- package/dist/app/cloud/migration/files.d.ts +0 -16
- package/dist/app/cloud/migration/files.js +0 -2
- package/dist/app/cloud/migration/index.d.ts +0 -6
- package/dist/app/cloud/migration/index.js +0 -1
- package/dist/app/cloud/migration/journal.d.ts +0 -64
- package/dist/app/cloud/migration/journal.js +0 -1
- package/dist/app/cloud/migration/mapping-dispositions.d.ts +0 -3
- package/dist/app/cloud/migration/mapping-dispositions.js +0 -1
- package/dist/app/cloud/migration/mapping.d.ts +0 -11
- package/dist/app/cloud/migration/mapping.js +0 -2
- package/dist/app/cloud/migration/model.d.ts +0 -175
- package/dist/app/cloud/migration/model.js +0 -1
- package/dist/app/cloud/migration/plan.d.ts +0 -7
- package/dist/app/cloud/migration/plan.js +0 -1
- package/dist/app/cloud/migration/progress.d.ts +0 -19
- package/dist/app/cloud/migration/progress.js +0 -1
- package/dist/app/cloud/migration/reconcile.d.ts +0 -22
- package/dist/app/cloud/migration/reconcile.js +0 -1
- package/dist/app/cloud/migration/reconciliation-spool.d.ts +0 -4
- package/dist/app/cloud/migration/reconciliation-spool.js +0 -1
- package/dist/app/cloud/migration/state.d.ts +0 -15
- package/dist/app/cloud/migration/state.js +0 -1
- package/dist/app/cloud/migration/transfer.d.ts +0 -6
- package/dist/app/cloud/migration/transfer.js +0 -1
- package/dist/cli/cloud-apply-help.d.ts +0 -1
- package/dist/cli/cloud-apply-help.js +0 -56
- package/dist/cli/cloud-selection.d.ts +0 -2
- package/dist/cli/cloud-selection.js +0 -3
- package/dist/cli/commands/cloud/apply.d.ts +0 -5
- package/dist/cli/commands/cloud/apply.js +0 -1
- package/dist/cli/commands/cloud/campaign/create.d.ts +0 -5
- package/dist/cli/commands/cloud/campaign/create.js +0 -1
- package/dist/cli/commands/cloud/download.d.ts +0 -5
- package/dist/cli/commands/cloud/download.js +0 -1
- package/dist/cli/commands/cloud/list.d.ts +0 -5
- package/dist/cli/commands/cloud/list.js +0 -1
- package/dist/cli/commands/cloud/login.d.ts +0 -13
- package/dist/cli/commands/cloud/login.js +0 -2
- package/dist/cli/commands/cloud/logout.d.ts +0 -7
- package/dist/cli/commands/cloud/logout.js +0 -1
- package/dist/cli/commands/cloud/migrate/apply.d.ts +0 -2
- package/dist/cli/commands/cloud/migrate/apply.js +0 -1
- package/dist/cli/commands/cloud/migrate/plan.d.ts +0 -7
- package/dist/cli/commands/cloud/migrate/plan.js +0 -1
- package/dist/cli/commands/cloud/migrate/progress.d.ts +0 -2
- package/dist/cli/commands/cloud/migrate/progress.js +0 -2
- package/dist/cli/commands/cloud/migrate/reconcile.d.ts +0 -2
- package/dist/cli/commands/cloud/migrate/reconcile.js +0 -1
- package/dist/cli/commands/cloud/policy/preview.d.ts +0 -5
- package/dist/cli/commands/cloud/policy/preview.js +0 -1
- package/dist/cli/commands/cloud/project/create.d.ts +0 -5
- package/dist/cli/commands/cloud/project/create.js +0 -1
- package/dist/cli/commands/cloud/publish.d.ts +0 -7
- package/dist/cli/commands/cloud/publish.js +0 -1
- package/dist/cli/commands/cloud/session/revoke.d.ts +0 -8
- package/dist/cli/commands/cloud/session/revoke.js +0 -1
- package/dist/cli/commands/cloud/show.d.ts +0 -5
- package/dist/cli/commands/cloud/show.js +0 -1
- package/dist/cli/commands/cloud/snapshot.d.ts +0 -5
- package/dist/cli/commands/cloud/snapshot.js +0 -1
- package/dist/cli/commands/cloud/status.d.ts +0 -33
- package/dist/cli/commands/cloud/status.js +0 -1
- package/dist/cloud/account.d.ts +0 -34
- package/dist/cloud/account.js +0 -1
- package/dist/cloud/archive-contract.d.ts +0 -1
- package/dist/cloud/archive-contract.js +0 -1
- package/dist/cloud/archive.d.ts +0 -12
- package/dist/cloud/archive.js +0 -1
- package/dist/cloud/campaigns.d.ts +0 -200
- package/dist/cloud/campaigns.js +0 -2
- package/dist/cloud/client.d.ts +0 -68
- package/dist/cloud/client.js +0 -4
- package/dist/cloud/download.d.ts +0 -5
- package/dist/cloud/download.js +0 -1
- package/dist/cloud/historical-contract.d.ts +0 -224
- package/dist/cloud/historical-contract.js +0 -1
- package/dist/cloud/historical-json.d.ts +0 -16
- package/dist/cloud/historical-json.js +0 -1
- package/dist/cloud/historical.d.ts +0 -69
- package/dist/cloud/historical.js +0 -1
- package/dist/cloud/pagination.d.ts +0 -5
- package/dist/cloud/pagination.js +0 -1
- package/dist/cloud/transport.d.ts +0 -18
- package/dist/cloud/transport.js +0 -1
- package/dist/config/loader/cloud.d.ts +0 -2
- package/dist/config/loader/cloud.js +0 -1
- package/docs/cloud-control.md +0 -108
- package/docs/testkit-cloud-boundary.md +0 -628
- package/node_modules/buffer-crc32/LICENSE +0 -19
- package/node_modules/buffer-crc32/README.md +0 -55
- package/node_modules/buffer-crc32/dist/index.cjs +0 -306
- package/node_modules/buffer-crc32/dist/index.d.cts +0 -35
- package/node_modules/buffer-crc32/dist/index.d.mts +0 -35
- package/node_modules/buffer-crc32/dist/index.mjs +0 -304
- package/node_modules/buffer-crc32/index.d.ts +0 -35
- package/node_modules/buffer-crc32/package.json +0 -54
- package/node_modules/yazl/LICENSE +0 -21
- package/node_modules/yazl/README.md +0 -457
- package/node_modules/yazl/index.js +0 -826
- package/node_modules/yazl/package.json +0 -34
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cleanupOwnedRunLifecycleResourcesWithinLifecycleFence as
|
|
1
|
+
import{cleanupOwnedRunLifecycleResourcesWithinLifecycleFence as R,cleanupRunsWithinLifecycleFence as v,listRunManifests as F,startRunLifecycle as q}from"./lifecycle.js";import{createRuntimeManager as I}from"./runtime-manager.js";import{resolveCommittedRunFinalization as S}from"../evidence/finalization.js";import{withProductLifecycleFence as b}from"../process/lifecycle-fence.js";export function createLocalRunEnvironment(s){return{async start(d){const n=await q(s,d),c=n.startHeartbeat(2e3,()=>({kind:"controller",status:n.isStopRequested()?"interrupting":"running"}));let i=null;const p=new Map,l=()=>{if(i===null)throw new Error("Run runtimes have not been provisioned");return i};let m=null;return{runId:n.runId,signal:n.signal,isStopRequested:()=>n.isStopRequested(),getInterruptReason:()=>n.getInterruptReason(),requestStop:e=>{n.requestStop(e)},beginFinalization:c,dispose(){c(),n.removeSignalHandlers(),n.dispose()},get committedFinalization(){return m},provision(e,t){if(i!==null)throw new Error("Run runtimes are already provisioned");i=I({productDir:s,graphs:e,lifecycle:n,runtimeOptions:t})},async acquire(e,t){const r=await l().acquire(e,t);p.set(r.leaseId,r);const{engineCacheDirectory:o,engineWorkspaceDirectory:u,leaseId:a,leaseDir:h,runtimeId:g,graphKey:f,targetNames:w,services:y}=r;return{engineCacheDirectory:o,engineWorkspaceDirectory:u,leaseId:a,leaseDir:h,runtimeId:g,graphKey:f,targetNames:w,services:y}},canAcquire:e=>l().canAcquire(e),async release(e,t){const r=p.get(e);r!==void 0&&(await l().release(r,t),p.delete(e))},async cleanupAll(){await i?.cleanupAll()},getStats:()=>i?.getStats()??[],getGraph(e){const t=i?.getGraph(e);return t?{key:t.key,targetNames:[...t.targetNames]}:null},getGraphFailure:e=>i?.getGraphFailure(e)??null,markGraphFailed:(e,t)=>{l().markGraphFailed(e,t)},async finish(e,t,r){return c(),b(s,async o=>{const u=F(s).find(a=>a.runId===n.runId);if(u!==void 0&&u.services.length>0)throw new Error(r==="normal"?"Cannot commit a run while application service ownership remains published":"Cannot publish terminal failure while application service ownership remains published");m??=S(e,n.commitFinalization(e==="failed"?"failed":"finished"));try{await v(o,{protectedLiveRunIds:new Set([n.runId])}),await R(o,n)}catch(a){if(r==="normal")throw a}return t(m)})}}}}}
|
|
@@ -26,6 +26,7 @@ export interface RuntimeInstanceContext {
|
|
|
26
26
|
startupPromise: Promise<void> | null;
|
|
27
27
|
targetNames: string[];
|
|
28
28
|
}
|
|
29
|
+
export declare function runtimeEngineDirectory(context: Pick<RuntimeInstanceContext, "graphDir" | "runtimeId">): string;
|
|
29
30
|
export declare function createRuntimeInstanceContext(runtimeId: string, graph: RuntimeGraph<RuntimeContextSourceConfig>, productDir: string): RuntimeInstanceContext;
|
|
30
31
|
export declare function ensureRuntimeInstanceReady(context: RuntimeInstanceContext, startApplicationServices: boolean, lifecycle: LocalServiceLifecycle, options?: Readonly<RuntimeContextOptions>): Promise<RuntimeInstanceContext>;
|
|
31
32
|
export declare function cleanupRuntimeInstanceContext(context: RuntimeInstanceContext | null | undefined, lifecycle: LocalServiceLifecycle): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import
|
|
1
|
+
import o from"fs";import n from"path";import{prepareRuntimeServices as m}from"./runtime-preparation.js";import{writeGraphMetadata as p}from"./state.js";import{startLocalServices as u,stopLocalServices as f}from"./services.js";import{resolveRuntimeInstanceConfigs as c}from"./template.js";export function runtimeEngineDirectory(r){return n.join(r.graphDir,"node_modules","testkit-engine",r.runtimeId)}export function createRuntimeInstanceContext(r,e,i){const a=n.join(i,".testkit","_graphs",e.dirName),t=n.join(a,"runtimes",r);o.mkdirSync(t,{recursive:!0}),p(a,e);const s=c(e.runtimeConfigs,r,t,{graphDirName:e.dirName,portNamespaceIndex:e.portNamespaceIndex,portNamespaceStride:e.portNamespaceStride});return{graphKey:e.key,targetNames:[...e.targetNames],graphDir:a,runtimeDir:t,runtimeId:r,runtimeConfigs:s,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 d(r,e){r?.started&&(await f(r.startedServices,e),r.started=!1,r.startedServices=[])}export async function cleanupRuntimeInstanceContext(r,e){if(!r)return;await d(r,e);const i=runtimeEngineDirectory(r);o.rmSync(n.join(i,"cache"),{recursive:!0,force:!0});for(const a of[n.join(i,"leases"),i])try{o.rmdirSync(a)}catch(t){const s=t.code;if(s!=="ENOENT"&&s!=="ENOTEMPTY"&&s!=="EEXIST")throw t}}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
|
|
1
|
+
import y from"fs";import x from"path";import{buildRuntimeIds as A}from"../domain/runtime-identity.js";import{formatError as R}from"../kernel/error-serialization.js";import{cleanupRuntimeInstanceContext as D,createRuntimeInstanceContext as K,ensureRuntimeInstanceReady as T}from"./runtime-contexts.js";import{timeOperation as $}from"../evidence/operation-timings.js";import{createReadyEnvironment as E}from"./ready-environment.js";export function createRuntimeManager({productDir:t,graphs:a,lifecycle:n,runtimeOptions:r={}}){const i=new Map(a.map(e=>[e.key,e])),c=new Map,s=new Map,u=W(a),f=new Map;let S=1;return{canAcquire(e){if(f.has(e.graphKey))return!1;const o=w(c,i,e);return j(s,v(e))?P(o,e,u)!==null:!1},async acquire(e,o={}){const l=w(c,i,e);if(n.isStopRequested())throw n.signal.reason||new Error("testkit run interrupted");const{leaseId:p,slot:g}=await G(r,o,"testkit.runtime.slot_claim",{summary:"claim runtime slot",scope:{graphKey:e.graphKey,taskId:e.id,serviceName:e.serviceName||null,targetName:e.targetName||null},metadata:{decision:"claim",lockCount:v(e).length.toString(),resourceCost:d(e).toString()}},()=>{const m=`lease-${e.id.toString()}-${S.toString()}`;if(S+=1,!B(s,v(e),m))throw new Error(`Task ${e.id.toString()} was claimed before its locks were available`);const h=q(l,e,u);if(!h)throw C(s,v(e),m),new Error(`Task ${e.id.toString()} was claimed before runtime capacity was available`);return{leaseId:m,slot:h}});try{const m=await F(g,t,e,n,r,o);n.signal.throwIfAborted();const h=x.join(m.runtimeDir,"leases",p);return y.mkdirSync(h,{recursive:!0}),{...E(m,{leaseId:p,leaseDir:h}),lockNames:v(e),resourceCost:d(e),slot:g,context:m}}catch(m){throw I(g,d(e)),C(s,v(e),p),U({leaseDir:x.join(g.context?.runtimeDir||t,"leases",p)}),g.context||N(g,u),await k(g,n,u),n.isStopRequested()||M(f,e.graphKey,R(m)),m}},async release(e,o={}){e?.slot&&(o.invalidate&&(e.slot.draining=!0),e.releaseState||(e.releaseState={locks:!1,runtimeSlot:!1,leaseDir:!1}),e.releaseState.locks||(C(s,e.lockNames,e.leaseId),e.releaseState.locks=!0),e.releaseState.runtimeSlot||(I(e.slot,e.resourceCost),e.releaseState.runtimeSlot=!0),e.releaseState.leaseDir||(U(e),e.releaseState.leaseDir=!0),e.released=!0,e.slot.draining&&e.slot.activeLeaseCount===0&&await k(e.slot,n,u))},async cleanupAll(){const e=[];for(const o of c.values())for(const l of o.slots){l.draining=!0;try{await k(l,n,u)}catch(p){e.push({slot:l,error:p})}}if(e.length>0)throw new AggregateError(e.map(o=>o.error),`Failed to clean up ${e.length.toString()} runtime ${e.length===1?"slot":"slots"}: ${e.map(o=>`${o.slot.runtimeId}: ${R(o.error)}`).join("; ")}`)},getStats(){return[...c.values()].map(e=>({graphKey:e.slots[0]?.graph.key||null,targetNames:[...e.slots.at(0)?.graph.targetNames??[]],maxConcurrentTasks:Number.isFinite(e.slots[0]?.graph.maxConcurrentTasks)?e.slots[0]?.graph.maxConcurrentTasks??null:null,runtimeCount:e.slots.length,runtimes:e.slots.map(o=>({runtimeId:o.runtimeId,peakLeaseCount:o.peakLeaseCount,peakResourceUnits:o.peakResourceUnits,resourceReservations:o.resourceReservations.map(l=>({...l}))})),resourceBudgets:[...u.values()].filter(o=>e.slots.some(l=>l.graph.resourceReservations.some(p=>p.key===o.key))).map(o=>({key:o.key,capacity:o.capacity,reserved:o.reserved,available:o.available,active:o.active,peakActive:o.peakActive}))})).sort((e,o)=>(e.graphKey||"").localeCompare(o.graphKey||""))},getGraphFailure(e){return f.get(e)??null},getGraph(e){return i.get(e)||null},markGraphFailed(e,o){M(f,e,o)}}}function w(t,a,n){const r=t.get(n.graphKey);if(r)return r;const i=a.get(n.graphKey);if(!i)throw new Error(`Unknown graph "${n.graphKey}"`);const s={slots:A(i.instanceCount).map(u=>({graph:i,runtimeId:u,context:null,activeLeaseCount:0,activeResourceUnits:0,resourceReservations:[],peakLeaseCount:0,peakResourceUnits:0,draining:!1}))};return t.set(n.graphKey,s),s}function q(t,a,n){const r=d(a),i=t.slots.filter(s=>!s.draining);if(i.length===0)return null;const c=[...i].filter(s=>L(s,r)&&b(s,n)).sort((s,u)=>s.activeResourceUnits-u.activeResourceUnits||s.activeLeaseCount-u.activeLeaseCount||s.runtimeId.localeCompare(u.runtimeId)).at(0);return c?(H(c,n),c.activeLeaseCount+=1,c.activeResourceUnits+=r,c.peakLeaseCount=Math.max(c.peakLeaseCount,c.activeLeaseCount),c.peakResourceUnits=Math.max(c.peakResourceUnits,c.activeResourceUnits),c):null}function I(t,a){t.activeLeaseCount=Math.max(0,t.activeLeaseCount-1),t.activeResourceUnits=Math.max(0,t.activeResourceUnits-a)}async function F(t,a,n,r,i,c={}){t.context||(t.context=K(t.runtimeId,t.graph,a),r.trackGraphContext(t.context));const s=i.operationTimingRegistry||null,u=s?.start({name:"testkit.runtime.ensure",kind:"runtime",summary:"ensure runtime instance ready",parentId:c.operationTimingParentId||null,scope:{graphKey:t.graph.key,runtimeId:t.runtimeId,serviceName:n.serviceName,targetName:n.targetName}})??null;try{await T(t.context,n.startApplicationServices,r,i),s?.finish(u,{status:"passed"})}catch(f){throw s?.finish(u,{status:"failed",error:R(f)}),f}return t.context}function G(t,a,n,r,i){return $(t.operationTimingRegistry||null,{name:n,kind:"runtime",summary:r.summary||null,parentId:a.operationTimingParentId||null,scope:r.scope||null,metadata:r.metadata||null},i)}async function k(t,a,n){!t.context||t.activeLeaseCount>0||(await D(t.context,a),N(t,n),t.context=null,t.draining=!1)}function B(t,a,n){const r=[...new Set(a)].sort();for(const i of r)if(t.has(i))return!1;for(const i of r)t.set(i,n);return!0}function j(t,a){return[...new Set(a)].every(n=>!t.has(n))}function P(t,a,n){const r=d(a);return t.slots.find(i=>!i.draining&&L(i,r)&&b(i,n))||null}function L(t,a){return t.activeResourceUnits+a<=t.graph.maxConcurrentTasks}function W(t){const a=new Map;for(const n of t)for(const r of n.resourceReservations){const i=a.get(r.key)||{key:r.key,capacity:r.capacity||0,reserved:r.reserved||0,available:r.available||Math.max(0,(r.capacity||0)-(r.reserved||0)),active:0,peakActive:0};i.capacity=Math.max(i.capacity,r.capacity||0),i.reserved=Math.max(i.reserved,r.reserved||0),i.available=Math.max(0,i.capacity-i.reserved),a.set(r.key,i)}return a}function b(t,a){return t.resourceReservations.length>0?!0:t.graph.resourceReservations.every(n=>{const r=a.get(n.key);return r?r.active+n.perRuntime<=r.available:!0})}function H(t,a){if(t.resourceReservations.length>0)return;const n=[];for(const r of t.graph.resourceReservations){const i=a.get(r.key);i&&(i.active+=r.perRuntime,i.peakActive=Math.max(i.peakActive,i.active),n.push({key:r.key,units:r.perRuntime,services:r.services}))}t.resourceReservations=n}function N(t,a){for(const n of t.resourceReservations){const r=a.get(n.key);r&&(r.active=Math.max(0,r.active-n.units))}t.resourceReservations=[]}function C(t,a,n){for(const r of[...new Set(a)].sort())t.get(r)===n&&t.delete(r)}function M(t,a,n){!a||t.has(a)||t.set(a,n)}function v(t){return t.locks||[]}function d(t){return t.resourceCost??1}function U(t){t?.leaseDir&&(t.engineWorkspaceDirectory&&y.rmSync(t.engineWorkspaceDirectory,{recursive:!0,force:!0}),y.rmSync(t.leaseDir,{recursive:!0,force:!0}))}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { RunArtifactDocument } from "./reporting-types.js";
|
|
2
2
|
import type { NormalizedArtifactPublicationConfig, NormalizedArtifactRetentionConfig } from "./artifact-config.js";
|
|
3
|
-
import { type RunBundleArtifact, type RunBundleCleanup, type RunBundleManifest, type RunBundlePublication, type RunBundleRetentionDecision, type RunBundleSummary, type RunBundleVerification, type
|
|
4
|
-
export type { RunBundleArchive, RunBundleArtifact, RunBundleCleanup, RunBundleLatestProjection, RunBundleManifest, RunBundleManifestEntry, RunBundlePublication, RunBundleRedaction, RunBundleRetentionDecision, RunBundleStorage, RunBundleSummary, RunBundleVerification,
|
|
3
|
+
import { type RunBundleArtifact, type RunBundleCleanup, type RunBundleManifest, type RunBundlePublication, type RunBundleRetentionDecision, type RunBundleSummary, type RunBundleVerification, type SealRunBundleOptions } from "./contracts.js";
|
|
4
|
+
export type { RunBundleArchive, RunBundleArtifact, RunBundleCleanup, RunBundleLatestProjection, RunBundleManifest, RunBundleManifestEntry, RunBundlePublication, RunBundleRedaction, RunBundleRetentionDecision, RunBundleStorage, RunBundleSummary, RunBundleVerification, SealRunBundleOptions, } from "./contracts.js";
|
|
5
5
|
interface PublicationReceiptInput extends RunBundlePublication {
|
|
6
6
|
bytesUploaded: number;
|
|
7
7
|
}
|
|
@@ -23,6 +23,5 @@ export declare function listRunBundles(productDir: string, options?: {
|
|
|
23
23
|
file?: null | string;
|
|
24
24
|
}): RunBundleSummary[];
|
|
25
25
|
export declare function verifyRunBundle(productDir: string, runId: unknown): RunBundleVerification;
|
|
26
|
-
export declare function prepareVerifiedRunBundleUpload(productDir: string, requestedRunId?: null | string): VerifiedRunBundleUpload;
|
|
27
26
|
export declare function publishRunBundle(productDir: string, runId: unknown, publication: NormalizedArtifactPublicationConfig | null, dependencies?: Readonly<RunBundlePublicationDependencies>): RunBundlePublication;
|
|
28
27
|
export declare function applyRunBundleRetention(productDir: string, currentRunId: unknown, retention?: Partial<NormalizedArtifactRetentionConfig>, dependencies?: Readonly<RunBundleRetentionDependencies>): RunBundleRetentionDecision;
|
package/dist/evidence/bundles.js
CHANGED
|
@@ -1 +1 @@
|
|
|
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
|
+
import{recordWork as H}from"../kernel/work-metrics.js";import{optionalString as A}from"../kernel/unknown-values.js";import{readRunProvenance as ue}from"./read-provenance.js";import{parseRunBundleArtifact as le,parseRunBundleManifest as J,readRunBundleArtifact as fe}from"./read-bundle.js";import{readRunDocument as de}from"./read-run.js";import me from"crypto";import c from"fs";import u from"path";import{redactSensitiveText as Y}from"../kernel/redaction.js";import{BUNDLE_SCHEMA_VERSION as G,OBJECTS_DIRNAME as Q,PROVENANCE_SCHEMA_VERSION as pe,RETENTION_DECISION_SCHEMA_VERSION as he,RUNS_DIRNAME as ge}from"./contracts.js";import{bundleRelativePath as x,errorMessage as k,hashFile as F,isObjectRecord as $,normalizePath as X,normalizeRunId as w,objectRecords as Z,optionalStringArray as z,optionalUnknownArray as ye,readJsonFile as P,requirePublicationPathOutsideResults as M,requireRunId as ve,runIncludesFile as Se,resolveManifestEntryPath as ee,resolveRunBundleDirectory as O,resolveRunsRoot as te,resultsRoot as U,sanitizeSegment as Re,sha256 as C,stableJson as q,treeBytes as ne,walkFiles as re,writeBufferAtomic as je,writeJson as N}from"./storage.js";import{resolveCiProvenance as be}from"./ci-provenance.js";const W=["artifacts","logs","setup"],Ie=/(?:PASSWORD|TOKEN|SECRET|PRIVATE_KEY|AUTH|COOKIE|SESSION|API_KEY)/iu,we=["package.json","package-lock.json","npm-shrinkwrap.json","pnpm-lock.yaml","yarn.lock","testkit.config.ts","testkit.config.mts","testkit.config.js","testkit.config.mjs"];export function sealRunBundle(e,n,t={},s={}){const r=n,o=ve(r),i=te(e,!0),a=u.dirname(i),f=u.join(i,o),m=s.cleanupActiveRunResults??ze,p=loadVerifiedRunBundleArtifactIfExists(e,o);if(p!==null)return{...p,archive:{...p.archive,cleanup:m(a)}};const d=u.join(i,`.${o}.${String(process.pid)}.${String(Date.now())}.${me.randomBytes(4).toString("hex")}.tmp`);c.mkdirSync(d,{recursive:!0});try{const R=Te(t.env??process.env),g=Ee({productDir:e,resultsDir:a,runId:o,tempDir:d,sensitiveValues:R}),h=Be(r,o,R),y=de(h.artifact,"run artifact");g.redaction.structuredReplacements=h.replacements,g.redaction.replacements+=h.replacements;const v=$e(e,y,o,{...t,redaction:g.redaction}),j={schemaVersion:G,runId:o,status:"complete",canonicalRunPath:x(o,"run.json"),manifestPath:x(o,"manifest.json"),provenancePath:x(o,"provenance.json"),integrity:"sha256",publication:{configured:!!t.archiveConfig?.publication,status:t.archiveConfig?.publication?"pending":"not_configured"},redaction:g.redaction};y.archive=j,N(u.join(d,"run.json"),y),N(u.join(d,"provenance.json"),v);const b=xe(d),D=C(Buffer.from(q(b))),E={schemaVersion:G,runId:o,createdAt:new Date().toISOString(),algorithm:"sha256",files:b,storage:{fileCount:b.length,logicalBytes:b.reduce((T,_)=>T+_.bytes,0),objectBytesWritten:g.objectBytesWritten,deduplicatedBytes:g.deduplicatedBytes},redaction:g.redaction,digest:D};return N(u.join(d,"manifest.json"),E),c.renameSync(d,f),{...y,archive:{...j,cleanup:m(a),manifestDigest:E.digest,storage:E.storage}}}catch(R){try{c.rmSync(d,{recursive:!0,force:!0})}catch{}try{Ce(e)}catch{}throw R}}export function loadVerifiedRunBundleArtifactIfExists(e,n){const t=w(n),s=O(e,t);if(!c.existsSync(s))return null;const r=verifyRunBundle(e,t);if(!r.ok)throw new Error(`Immutable run bundle ${t} already exists but failed integrity verification: ${r.errors.join("; ")}`);return loadRunBundleArtifact(e,t)}export function loadRunBundleArtifact(e,n){return se(e,n).artifact}function se(e,n){const t=w(n),s=fe(e,t),r=loadRunBundleManifest(e,t);return{manifest:r,artifact:{...s,archive:{...s.archive,manifestDigest:r.digest,storage:r.storage}}}}export function loadRunBundleManifest(e,n){const t=w(n),s=O(e,t),r=u.join(s,"manifest.json");if(!c.existsSync(r))throw new Error(`No immutable Testkit run manifest found for ${t}`);return J(P(r),r,t)}export function listRunBundles(e,n={}){return ie(e,n).map(({artifact:t,manifest:s,...r})=>r)}function ie(e,n={}){const t=te(e,!1);if(!c.existsSync(t))return[];const s=c.readdirSync(t,{withFileTypes:!0});return H("artifacts.runDirectoriesRead",1),H("artifacts.runEntriesVisited",s.length),s.filter(o=>o.isDirectory()&&!o.name.startsWith(".")).map(o=>{try{const{artifact:i,manifest:a}=se(e,o.name);return n.file&&!Se(i,n.file)?null:{artifact:i,manifest:a,runId:o.name,status:i.run.status,startedAt:i.run.startedAt,finishedAt:i.run.finishedAt??null,manifestDigest:i.archive.manifestDigest??null,canonicalRunPath:i.archive.canonicalRunPath}}catch{return null}}).filter(o=>o!==null).sort((o,i)=>(i.finishedAt??i.startedAt??"").localeCompare(o.finishedAt??o.startedAt??""))}export function verifyRunBundle(e,n){const t=w(n);try{return V(O(e,t),t)}catch(s){return{ok:!1,runId:t,errors:[k(s)],checkedFiles:0}}}export function publishRunBundle(e,n,t,s={}){const r=w(n),o=s.writePublicationReceipt??Pe;if(!t)return o(e,r,{status:"not_configured",required:!1,target:null,bytesUploaded:0});const i=u.resolve(e,t.directory);try{M(e,i)}catch(d){return o(e,r,{status:"failed",required:t.required,target:i,error:k(d),bytesUploaded:0})}const a=Re(u.basename(e)),f=u.join(i,a),m=u.join(f,r),p=u.join(f,`.${r}.${String(process.pid)}.${String(Date.now())}.tmp`);try{const d=O(e,r);if(c.mkdirSync(f,{recursive:!0}),M(e,f),c.existsSync(m)){const h=c.lstatSync(m);if(h.isSymbolicLink()||!h.isDirectory())throw new Error("Publication target must be a regular bundle directory");M(e,m);const y=V(d,r);if(!y.ok)throw new Error(`Source run bundle failed integrity verification: ${y.errors.join("; ")}`);const v=V(m,r);if(!v.ok)throw new Error(`Publication target failed integrity verification: ${v.errors.join("; ")}`);if(y.manifestDigest!==v.manifestDigest)throw new Error("Publication target already exists with a different manifest digest");return o(e,r,{status:"complete",required:t.required,target:m,manifestDigest:y.manifestDigest,reused:!0,bundleBytes:ne(d),bytesUploaded:0})}const R=ne(d);c.cpSync(d,p,{recursive:!0,preserveTimestamps:!0});const g=V(p,r);if(!g.ok)throw new Error(g.errors.join("; "));return c.renameSync(p,m),o(e,r,{status:"complete",required:t.required,target:m,manifestDigest:g.manifestDigest,reused:!1,bundleBytes:R,bytesUploaded:R})}catch(d){return c.rmSync(p,{recursive:!0,force:!0}),o(e,r,{status:"failed",required:t.required,target:m,error:k(d),bytesUploaded:0})}}export function applyRunBundleRetention(e,n,t={},s={}){const r=w(n),o=s.removeObject??ce,i=s.removeRunBundle??Ne,a=t.maxRuns??null,f=t.maxAgeDays??null,m=new Set(t.preserveSuites||[]),p=ie(e),d=new Set([r]);for(const l of p)Oe(l.artifact,m)&&d.add(l.runId);const R=new Set(a?p.slice(0,a).map(l=>l.runId):p.map(l=>l.runId)),g=f==null?null:Date.now()-f*24*60*60*1e3,h=[];for(const l of p){if(d.has(l.runId))continue;const S=g!=null&&Date.parse(l.finishedAt||l.startedAt||"")<g,I=!R.has(l.runId);if(!S&&!I)continue;const B=[];S&&B.push("max_age"),I&&B.push("max_runs"),h.push({path:u.join(U(e),ge,l.runId),reasons:B,runId:l.runId})}const y=new Map(p.map(l=>[l.runId,l.manifest.files.filter(S=>S.path.startsWith("files/")).map(S=>S.sha256)])),v=oe(e),j=[],b=[];for(const l of h){let S=null;try{i(l.path)}catch(B){S=L(B)}const I=!c.existsSync(l.path);!I&&S===null&&(S="Run bundle still exists after removal completed"),I&&j.push({reasons:l.reasons,runId:l.runId}),S!==null&&b.push({...l,error:S,removed:I})}const D=new Set(j.map(l=>l.runId)),E=new Set(p.filter(l=>!D.has(l.runId)).flatMap(l=>y.get(l.runId)??[])),T=ae(v,E,o),_=new Date().toISOString();return Ve({currentRunId:r,decidedAt:_,garbageCollection:T,policy:{maxRuns:a,maxAgeDays:f,preserveSuites:[...m].sort()},protectedRunIds:[...d].sort(),removed:j,removalErrors:b})}function Ee({resultsDir:e,tempDir:n,sensitiveValues:t}){let s=0,r=0,o=0,i=0,a=0;const f=u.join(e,Q,"sha256");c.mkdirSync(f,{recursive:!0});for(const m of W){const p=u.join(e,m);if(c.existsSync(p))for(const d of re(p)){const R=u.relative(p,d),g=c.readFileSync(d),h=De(g,d,t);h.scanned&&(o+=1),h.replacements>0&&(i+=1,a+=h.replacements);const y=C(h.buffer),v=u.join(f,y);c.existsSync(v)?r+=h.buffer.length:(je(v,h.buffer),c.chmodSync(v,292),s+=h.buffer.length);const j=u.join(n,"files",m,R);c.mkdirSync(u.dirname(j),{recursive:!0});try{c.linkSync(v,j)}catch{c.copyFileSync(v,j)}}}return{objectBytesWritten:s,deduplicatedBytes:r,redaction:{policyVersion:1,status:"applied",textualFilesScanned:o,filesRedacted:i,replacements:a,sensitiveEnvironmentKeys:t.map(m=>m.key).sort(),valuesStored:!1}}}function Be(e,n,t){const s=new Map(W.map(o=>[`.testkit/results/${o}/`,`${x(n,`files/${o}`)}/`])),r=K(e,s);return ke(Ae(r),t)}function Ae(e){if(!$(e))return e;const n={...e};if($(n.product)){const t={...n.product};delete t.directory,n.product=t}if($(n.git)){const t={...n.git};delete t.remoteUrl,delete t.repoRoot,delete t.worktreePath,n.git=t}return delete n.host,n}function K(e,n){if(Array.isArray(e))return e.map(s=>K(s,n));if($(e))return Object.fromEntries(Object.entries(e).map(([s,r])=>[s,K(r,n)]));if(typeof e!="string")return e;let t=e;for(const[s,r]of n)t.startsWith(s)&&(t=`${r}${t.slice(s.length)}`);return t}function ke(e,n){let t=0;function s(r){if(Array.isArray(r))return r.map(s);if($(r))return Object.fromEntries(Object.entries(r).map(([i,a])=>[i,s(a)]));if(typeof r!="string")return r;let o=Y(r);o!==r&&(t+=1);for(const i of n){if(!o.includes(i.value))continue;const a=o.split(i.value);t+=a.length-1,o=a.join("<redacted>")}return o}return{artifact:s(e),replacements:t}}function $e(e,n,t,s){const r=z(n.run.fileNames),o=[...new Set([...we,...r])].map(i=>Fe(e,i)).filter(i=>i!==null);return{schemaVersion:pe,runId:t,ci:be(s.env??process.env),source:{git:n.git??null,files:o},runtime:{controller:{testkitVersion:A(n.run.testkitVersion),node:process.versions.node,platform:process.platform,architecture:process.arch},runtimeStats:ye(n.run.runtimeStats)},scenario:{types:z(n.run.types),suiteSelectors:z(n.run.suiteSelectors),fileNames:r,serviceFilter:A(n.run.serviceFilter),planningDigest:C(Buffer.from(q(n.planning??null))),declaredEnvironmentKeys:[...new Set(s.declaredEnvironmentKeys??[])].sort()},redaction:s.redaction}}function xe(e){return re(e).map(n=>({path:X(u.relative(e,n)),bytes:c.statSync(n).size,sha256:F(n)})).sort((n,t)=>n.path.localeCompare(t.path))}function V(e,n){const t=u.join(e,"manifest.json"),s=J(P(t),t,n);le(P(u.join(e,"run.json")),u.join(e,"run.json"),n);const r=[];for(const i of s.files){let a;try{a=ee(e,i.path)}catch(m){r.push(k(m));continue}if(!c.existsSync(a)){r.push(`missing ${i.path}`);continue}const f=c.lstatSync(a);if(f.isSymbolicLink()){r.push(`symbolic link not allowed ${i.path}`);continue}if(!f.isFile()){r.push(`not a regular file ${i.path}`);continue}f.size!==i.bytes&&r.push(`size mismatch ${i.path}`),F(a)!==i.sha256&&r.push(`checksum mismatch ${i.path}`)}C(Buffer.from(q(s.files)))!==s.digest&&r.push("manifest digest mismatch"),s.storage.fileCount!==s.files.length&&r.push("manifest file count mismatch");for(const i of["run.json","provenance.json"])s.files.some(a=>a.path===i)||r.push(`manifest must include ${i}`);if(r.length===0){const i=ee(e,"provenance.json");ue(P(i),i,n)}return{runId:n,checkedFiles:s.files.length,storage:s.storage,ok:r.length===0,errors:r,manifestDigest:s.digest}}function Pe(e,n,t){const s={schemaVersion:1,runId:n,recordedAt:new Date().toISOString(),...t};return N(u.join(U(e),"publications",`${n}.json`),s),s}function Oe(e,n){return n.size===0?!1:Z(e.services).some(t=>Z(t.suites).some(s=>{const r=A(t.name)??"",o=A(s.name)??"",i=A(s.testType)??"";return[o,`${i}:${o}`,`${r}/${i}:${o}`].some(f=>n.has(f))}))}function Ce(e){const n=new Set;for(const t of listRunBundles(e)){const s=loadRunBundleManifest(e,t.runId);for(const r of s.files)r.path.startsWith("files/")&&n.add(r.sha256)}return ae(oe(e),n,ce)}function oe(e){const n=u.join(U(e),Q,"sha256");return c.existsSync(n)?c.readdirSync(n,{withFileTypes:!0}).filter(t=>t.isFile()).map(t=>{const s=u.join(n,t.name);return{bytes:c.statSync(s).size,digest:t.name,path:s}}):[]}function ae(e,n,t){const s=[];let r=0,o=0;for(const i of e){if(n.has(i.digest))continue;let a=null;try{t(i.path)}catch(m){a=L(m)}!c.existsSync(i.path)?(r+=1,o+=i.bytes):a===null&&(a="Object still exists after removal completed"),a!==null&&s.push({error:a,path:i.path})}return{failures:s,reclaimedBytes:o,removedObjects:r,status:s.length===0?"complete":"failed"}}function Ne(e){c.rmSync(e,{recursive:!0,force:!0})}function ce(e){c.rmSync(e,{force:!0})}function Ve({currentRunId:e,decidedAt:n,garbageCollection:t,policy:s,protectedRunIds:r,removed:o,removalErrors:i}){const a=[...i.map(f=>`remove ${f.runId}: ${f.error}`),...t.failures.map(f=>`garbage collect ${f.path}: ${f.error}`)];return{currentRunId:e,decidedAt:n,garbageCollection:t,policy:s,protectedRunIds:r,removed:o,removalErrors:i,schemaVersion:he,status:a.length===0?"complete":"failed",...a.length===0?{}:{error:a.join("; ")}}}function De(e,n,t){if(!_e(e,n)){for(const i of t)if(e.includes(Buffer.from(i.value)))throw new Error(`Refusing to archive binary file containing sensitive environment value: ${n}`);return{buffer:e,scanned:!1,replacements:0}}const s=e.toString("utf8");let r=Y(s),o=r===s?0:1;for(const i of t){if(!r.includes(i.value))continue;const a=r.split(i.value);o+=a.length-1,r=a.join("<redacted>")}return{buffer:Buffer.from(r),scanned:!0,replacements:o}}function Te(e){const n=[];for(const[t,s]of Object.entries(e))Ie.test(t)&&typeof s=="string"&&s.length>=8&&n.push({key:t,value:s});return n}function _e(e,n){if(e.includes(0))return!1;const t=u.extname(n).toLowerCase();return![".zip",".gz",".png",".jpg",".jpeg",".webm",".pdf"].includes(t)}function Fe(e,n){const t=u.resolve(e,n);return!t.startsWith(`${u.resolve(e)}${u.sep}`)||!c.existsSync(t)||!c.statSync(t).isFile()?null:{path:X(u.relative(e,t)),bytes:c.statSync(t).size,sha256:F(t)}}function ze(e){const n=[];for(const t of[...W.map(s=>u.join(e,s)),u.join(e,"live.json")])try{c.rmSync(t,{recursive:!0,force:!0})}catch(s){n.push(`${t}: ${L(s)}`)}return n.length===0?{status:"complete"}:{error:n.join("; "),status:"failed"}}function L(e){try{return k(e)}catch{return"Unknown post-seal archive operation failure"}}
|
|
@@ -138,14 +138,6 @@ export interface RunBundleVerification {
|
|
|
138
138
|
runId: string;
|
|
139
139
|
storage?: null | RunBundleStorage;
|
|
140
140
|
}
|
|
141
|
-
export interface RunBundleUploadFile extends RunBundleManifestEntry {
|
|
142
|
-
absolutePath: string;
|
|
143
|
-
}
|
|
144
|
-
export interface VerifiedRunBundleUpload {
|
|
145
|
-
files: RunBundleUploadFile[];
|
|
146
|
-
manifest: RunBundleManifest;
|
|
147
|
-
runId: string;
|
|
148
|
-
}
|
|
149
141
|
export interface SealRunBundleOptions {
|
|
150
142
|
archiveConfig?: Partial<NormalizedArtifactArchiveConfig>;
|
|
151
143
|
declaredEnvironmentKeys?: readonly string[];
|
package/dist/evidence/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { normalizeArtifactArchiveConfig, type NormalizedArtifactArchiveConfig, type NormalizedArtifactPublicationConfig, type NormalizedArtifactRetentionConfig, } from "./artifact-config.js";
|
|
2
2
|
export { finalizeLiveRunArtifact, loadTimings, persistTaskArtifacts, persistTaskFileArtifacts, persistTaskOutputArtifacts, resetResultArtifacts, saveTimings, writeLiveRunArtifact, writeRunArtifact, type ArtifactTask, type PersistedArtifactReference, type WriteRunArtifactOptions, } from "./artifacts.js";
|
|
3
|
-
export { applyRunBundleRetention, listRunBundles, loadRunBundleArtifact, loadRunBundleManifest, loadVerifiedRunBundleArtifactIfExists,
|
|
3
|
+
export { applyRunBundleRetention, listRunBundles, loadRunBundleArtifact, loadRunBundleManifest, loadVerifiedRunBundleArtifactIfExists, publishRunBundle, sealRunBundle, verifyRunBundle, type RunBundleVerification, } from "./bundles.js";
|
|
4
4
|
export { collectArtifactEntries, loadCurrentRunArtifact, loadLatestRunArtifact, loadRunArtifact, type CollectedArtifactEntry, type RunArtifactFileSubject, type RunArtifactPayload, } from "./reading.js";
|
|
5
5
|
export { DEFAULT_REPORTED_ARCHIVE_CONFIG, type BuildLiveRunArtifactOptions, type BuildRunArtifactOptions, type ReportedRunArtifact, type ReportedRunService, type ReportingArtifactReference, type ReportingDiagnostic, type ReportingExecution, type ReportingFailureDetail, type ReportingFailureLocation, type ReportingFailureRequest, type ReportingFailureResponse, type ReportingFileSummary, type ReportingMetadata, type ReportingOperationTiming, type ReportingOperationTimingInput, type ReportingRecord, type ReportingRuntimeBatch, type ReportingRuntimeReset, type ReportingServiceLogReference, type ReportingServiceSummary, type ReportingSetupLogReference, type ReportingSetupOperation, type ReportingSuiteSelector, type ReportingSuiteSummary, type RunArtifactDocument, type RunArtifactFile, type RunArtifactRun, type RunArtifactService, type RunArtifactSuite, type RunArtifactSummary, type RunServiceResult, } from "./reporting-types.js";
|
|
6
6
|
export { copyRegularFileSecurely, MAX_PERSISTED_FILE_ARTIFACT_BYTES, type SecureFileCopyOptions, type SecureFileCopyResult, } from "./secure-file-copy.js";
|
package/dist/evidence/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export{normalizeArtifactArchiveConfig}from"./artifact-config.js";export{finalizeLiveRunArtifact,loadTimings,persistTaskArtifacts,persistTaskFileArtifacts,persistTaskOutputArtifacts,resetResultArtifacts,saveTimings,writeLiveRunArtifact,writeRunArtifact}from"./artifacts.js";export{applyRunBundleRetention,listRunBundles,loadRunBundleArtifact,loadRunBundleManifest,loadVerifiedRunBundleArtifactIfExists,
|
|
1
|
+
export{normalizeArtifactArchiveConfig}from"./artifact-config.js";export{finalizeLiveRunArtifact,loadTimings,persistTaskArtifacts,persistTaskFileArtifacts,persistTaskOutputArtifacts,resetResultArtifacts,saveTimings,writeLiveRunArtifact,writeRunArtifact}from"./artifacts.js";export{applyRunBundleRetention,listRunBundles,loadRunBundleArtifact,loadRunBundleManifest,loadVerifiedRunBundleArtifactIfExists,publishRunBundle,sealRunBundle,verifyRunBundle}from"./bundles.js";export{collectArtifactEntries,loadCurrentRunArtifact,loadLatestRunArtifact,loadRunArtifact}from"./reading.js";export{DEFAULT_REPORTED_ARCHIVE_CONFIG}from"./reporting-types.js";export{copyRegularFileSecurely,MAX_PERSISTED_FILE_ARTIFACT_BYTES}from"./secure-file-copy.js";export{collectTestkitStorageInventory}from"./storage-inventory.js";export{}from"./timing.js";
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"entrypoint": "dist/process/process.js",
|
|
4
4
|
"sourceSha256": "a54f01864b603cee568b2945390dff35d578e07f6d26b5add06014169c3d17b0",
|
|
5
5
|
"bundleSha256": "62f76cf9fd019f6c552bfde2787f058917caf279670a62e7a6bfa12be34cb4b1",
|
|
6
|
-
"packageLockSha256": "
|
|
6
|
+
"packageLockSha256": "21f0898586dbf3747862f6def92186117fbb1612fc057f4ec6b22d1670cdbd00",
|
|
7
7
|
"esbuildVersion": "0.28.1",
|
|
8
8
|
"options": {
|
|
9
9
|
"format": "esm",
|
|
@@ -235,6 +235,26 @@ order. Every cleanup handler is attempted, cleanup has its own bounded deadline,
|
|
|
235
235
|
and any cleanup failure fails the invocation. Adapters do not carry a second
|
|
236
236
|
preparation or execution cleanup path.
|
|
237
237
|
|
|
238
|
+
The workspace can also provide a caller-owned `cacheDirectory`. Node/Vitest and
|
|
239
|
+
Playwright use a private child directory for Node's compilation cache so fresh
|
|
240
|
+
CLI and worker processes can reuse compiled code across files. Evaluated modules,
|
|
241
|
+
consumer globals, runtime inputs, and native runner state remain process-local.
|
|
242
|
+
The local run environment shares the cache across leases of one application
|
|
243
|
+
runtime and removes it after that runtime's executions and services stop.
|
|
244
|
+
Adapters leave invocation temporary directories empty after cleanup. Callers
|
|
245
|
+
that omit `cacheDirectory` retain execution without a shared compilation cache.
|
|
246
|
+
Explicit consumer Node cache controls or V8 coverage disable automatic caching,
|
|
247
|
+
and an unavailable cache falls back to normal execution.
|
|
248
|
+
On Windows, automatic cache allocation and Node startup use the extended path
|
|
249
|
+
form so deeply nested product directories can retain compilation caching.
|
|
250
|
+
|
|
251
|
+
The local run environment places engine workspaces and compilation caches below
|
|
252
|
+
the graph's `node_modules/testkit-engine` directory. Playwright skips that tree
|
|
253
|
+
before collecting files, so concurrent discovery cannot traverse another task's
|
|
254
|
+
temporary outputs as they change. Consumer test directories, ignore rules,
|
|
255
|
+
dependency projects, and snapshot paths keep their original configuration.
|
|
256
|
+
Releasing a lease removes its engine workspace; runtime cleanup removes its cache.
|
|
257
|
+
|
|
238
258
|
Adapters prepare the task batch before executing it. Shared `executeEngineTasks`
|
|
239
259
|
starts tasks in order and defers evidence and terminal notifications until all
|
|
240
260
|
tasks settle. It gives an expired task deadline precedence over cancellation,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gfdelarue/testkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Test orchestration platform for product acceptance and QA workflows",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"type": "module",
|
|
@@ -79,7 +79,6 @@
|
|
|
79
79
|
"test:integration": "npm run build && tsx scripts/test/integration.ts",
|
|
80
80
|
"test:system": "npm run build && vitest run --config scripts/test/system.config.ts",
|
|
81
81
|
"test:consumer-smoke": "npm run build && tsx scripts/test/installed-consumer-smoke.ts",
|
|
82
|
-
"test:cloud-account": "npm run build && vitest run --config scripts/test/cloud-account.config.ts",
|
|
83
82
|
"test:live": "npm run build && vitest run --config scripts/test/live.config.ts",
|
|
84
83
|
"release": "tsx scripts/release/index.ts",
|
|
85
84
|
"prepack": "npm run build"
|
|
@@ -87,18 +86,15 @@
|
|
|
87
86
|
"files": [
|
|
88
87
|
"bin/",
|
|
89
88
|
"dist/",
|
|
90
|
-
"docs/cloud-control.md",
|
|
91
89
|
"docs/engine-architecture.md",
|
|
92
90
|
"docs/test-types.md",
|
|
93
|
-
"docs/testkit-cloud-boundary.md",
|
|
94
91
|
"testkit-config-schema.md"
|
|
95
92
|
],
|
|
96
93
|
"bundleDependencies": [
|
|
97
94
|
"diff",
|
|
98
95
|
"tar",
|
|
99
96
|
"typescript",
|
|
100
|
-
"yauzl"
|
|
101
|
-
"yazl"
|
|
97
|
+
"yauzl"
|
|
102
98
|
],
|
|
103
99
|
"devDependencies": {
|
|
104
100
|
"@eslint/js": "^10.0.1",
|
|
@@ -108,7 +104,6 @@
|
|
|
108
104
|
"@types/k6": "1.6.0",
|
|
109
105
|
"@types/picomatch": "4.0.3",
|
|
110
106
|
"@types/yauzl": "3.4.0",
|
|
111
|
-
"@types/yazl": "3.3.0",
|
|
112
107
|
"globals": "^17.7.0",
|
|
113
108
|
"prettier": "3.9.6",
|
|
114
109
|
"strip-ansi": "^7.1.2",
|
|
@@ -137,12 +132,10 @@
|
|
|
137
132
|
"tar": "7.5.21",
|
|
138
133
|
"tinyglobby": "0.2.17",
|
|
139
134
|
"typescript": "5.9.3",
|
|
140
|
-
"undici": "7.29.1",
|
|
141
135
|
"vite": "8.0.16",
|
|
142
136
|
"vitest": "4.1.11",
|
|
143
137
|
"wrap-ansi": "^10.0.0",
|
|
144
|
-
"yauzl": "3.4.0"
|
|
145
|
-
"yazl": "3.3.1"
|
|
138
|
+
"yauzl": "3.4.0"
|
|
146
139
|
},
|
|
147
140
|
"optionalDependencies": {
|
|
148
141
|
"@gfdelarue/testkit-engine-k6-darwin-arm64": "1.0.0",
|
package/testkit-config-schema.md
CHANGED
|
@@ -747,26 +747,3 @@ compact CLI output and should not be treated as the only identifier.
|
|
|
747
747
|
- `skipCount`
|
|
748
748
|
- `avgDurationMs`
|
|
749
749
|
- `lastStatus`
|
|
750
|
-
|
|
751
|
-
## Optional local Cloud verification rule
|
|
752
|
-
|
|
753
|
-
```ts
|
|
754
|
-
cloud?: {
|
|
755
|
-
verification?: {
|
|
756
|
-
minimumConfirmingRuns?: number;
|
|
757
|
-
};
|
|
758
|
-
};
|
|
759
|
-
```
|
|
760
|
-
|
|
761
|
-
This setting is deprecated with the `testkit cloud` commands.
|
|
762
|
-
|
|
763
|
-
`cloud.verification.minimumConfirmingRuns` is an integer from 1 through 64,
|
|
764
|
-
defaulting to 2. It controls whether Testkit submits a new `finding.verify`
|
|
765
|
-
campaign report. It counts distinct full run IDs from available cited accepted
|
|
766
|
-
receipts. Explicit unverified reports do not require confirming evidence.
|
|
767
|
-
|
|
768
|
-
Cloud never reads or receives this config and independently enforces its
|
|
769
|
-
GUI-configured minimum. A local rejection and a Cloud rejection both produce
|
|
770
|
-
nonzero CLI status with structured diagnostics. See
|
|
771
|
-
[Campaign reporting and recovery](docs/testkit-cloud-boundary.md#report-an-externally-executed-campaign)
|
|
772
|
-
for examples, config directory selection, and exact retry behavior.
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import { CloudClient, type CloudReceipt } from "../../cloud/client.js";
|
|
2
|
-
import { type CampaignReport, type CampaignResult } from "../../cloud/campaigns.js";
|
|
3
|
-
export declare function readCampaignReportFile(file: string): unknown;
|
|
4
|
-
export declare function requiresVerificationPreflight(currentVersion: number, expectedVersion: number): boolean;
|
|
5
|
-
export declare function checkVerificationSubmission(client: CloudClient, report: CampaignReport, productDir: string): Promise<void>;
|
|
6
|
-
export declare function associateReceipt(client: CloudClient, receipt: CloudReceipt, campaignId: string): Promise<CampaignResult>;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import c from"node:fs";import u from"node:path";import p from"node:crypto";import{CloudResponseError as m}from"../../cloud/client.js";import{campaignError as f}from"../../cloud/campaigns.js";import{loadTestkitConfig as l,minimumConfirmingRuns as g}from"../../config/loader/index.js";import{attachErrorDiagnostics as d}from"../../kernel/error-serialization.js";import{shellQuote as h}from"../../process/host/platform.js";export function readCampaignReportFile(r){const e=c.openSync(u.resolve(r),"r");try{if(!c.fstatSync(e).isFile())throw f("file","report must be a regular file");const n=Buffer.alloc(65537);let t=0;for(;t<n.length;){const i=c.readSync(e,n,t,n.length-t,null);if(!i)break;t+=i}if(t>65536)throw f("file","report exceeds 65,536 bytes");try{return JSON.parse(new TextDecoder("utf-8",{fatal:!0}).decode(n.subarray(0,t)))}catch{throw f("file","report must contain valid UTF-8 JSON")}}finally{c.closeSync(e)}}export function requiresVerificationPreflight(r,e){if(r<e)throw d(Object.assign(new Error("Verification report names a future campaign version; read current state before submitting"),{code:"campaign_version_conflict",status:409}),{source:"local",currentVersion:r,expectedVersion:e});return r===e}export async function checkVerificationSubmission(r,e,n){if(e.command.type==="finding.verify"&&requiresVerificationPreflight((await r.campaign(e.campaignId)).version,e.expectedVersion))try{const t=await r.finding(e.campaignId,e.command.findingId);if(!requiresVerificationPreflight(t.campaignVersion,e.expectedVersion))return;const i=g((await l(n)).config),o=new Set(t.evidence.filter(a=>a.available).map(a=>a.receiptId)),s=new Set;for(const a of o)s.add((await r.receipt(a)).runId);if(s.size<i)throw d(Object.assign(new Error(`Local verification submission requires ${String(i)} distinct confirming runs; ${String(s.size)} eligible`),{code:"local_insufficient_confirmations"}),{source:"local",required:i,eligible:s.size})}catch(t){if((await r.campaign(e.campaignId)).version>e.expectedVersion)return;throw t}}export async function associateReceipt(r,e,n){let t;try{const i=await r.campaign(n),o="link:"+p.createHash("sha256").update(JSON.stringify([r.projectId,n,e.id,i.version])).digest("hex");return t={schemaVersion:1,projectId:r.projectId,campaignId:n,expectedVersion:i.version,idempotencyKey:o,command:{type:"receipt.link",receiptId:e.id}},await r.report(t)}catch(i){const o=t?i instanceof m&&i.status<500?"rejected":"unknown":"not_attempted";throw i instanceof Error&&(i.message=`Published run ${e.runId} (${e.id}); campaign association did not return a confirmed result. ${i.message}. Use --json for receipt and report recovery details.`),d(i,{publication:{status:"accepted",receipt:e},association:{campaignId:n,status:o,...t?{report:t}:{}},recovery:o==="rejected"?"Resolve the rejection, then repeat publication to read the current campaign version and reuse the accepted receipt: testkit cloud publish --run "+h(e.runId)+" --campaign "+n+" --json":t?"Save association.report as JSON and retry: testkit cloud apply --campaign "+n+" --file report.json --json":"Read the campaign version, then submit receipt.link for accepted receipt "+e.id})}}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { type ControlOptions } from "./control.js";
|
|
2
|
-
export declare function createCloudCampaign(options: ControlOptions & {
|
|
3
|
-
requestKey?: string;
|
|
4
|
-
focus?: string;
|
|
5
|
-
targetBugCount?: number;
|
|
6
|
-
}): Promise<unknown>;
|
|
7
|
-
export declare function previewCloudPolicy(policy: string, file: string, options: ControlOptions): Promise<unknown>;
|
|
8
|
-
export declare function downloadCloudArtifact(receipt: string, artifactPath: string, output: string, options: ControlOptions): Promise<unknown>;
|
|
9
|
-
export declare function revokeCloudSession(id: string, options: ControlOptions): Promise<{
|
|
10
|
-
id: string;
|
|
11
|
-
revoked: true;
|
|
12
|
-
}>;
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import p from"node:fs";import f from"node:path";import{randomUUID as d}from"node:crypto";import{controlClient as c,resolveOrganisation as w,resolveProject as h}from"./control.js";import{allItems as y,createdResourceDashboardLink as g}from"./control-links.js";import{invalidResponse as v}from"../../cloud/transport.js";export async function createCloudCampaign(e){const t=await c(e),r=await h(t,e),n=e.requestKey??d();e.notify?.(`Request key: ${n}. Reuse --request-key ${n} to retry the same campaign creation.
|
|
2
|
-
`);const a=await t.request("POST",`/api/v1/projects/${encodeURIComponent(r)}/campaigns`,{schemaVersion:1,projectId:r,idempotencyKey:n,focus:e.focus??"",...e.targetBugCount!==void 0?{targetBugCount:e.targetBugCount}:{}});return g(t,"campaign",a,e.env??process.env)}export async function previewCloudPolicy(e,t,r){const n=await c(r),a=await w(n,r.organisation,r.choose),i=p.statSync(t);if(!i.isFile()||i.size>256*1024)throw new Error("Policy preview input must be a JSON file no larger than 256 KiB.");const l=JSON.parse(p.readFileSync(t,"utf8"));return n.request("POST",`/api/v1/organisations/${encodeURIComponent(a)}/policies/${encodeURIComponent(e)}/preview`,l)}export async function downloadCloudArtifact(e,t,r,n){const a=await c(n);if(t.split("/").some(s=>!s||s==="."||s==="..")||t.includes("\\"))throw new Error("Artifact path must be a normalized relative path.");const i=`/api/v1/receipts/${encodeURIComponent(e)}/artifacts`,o=(await y(a,i)).find(s=>s.path===t);if(!o)throw new Error("This receipt has no artifact at the requested path.");if(o.available!==!0)throw new Error("Artifact evidence has expired. Its manifest metadata is still available.");if(typeof o.bytes!="number"||typeof o.sha256!="string")throw v("artifact manifest");const u=f.resolve(r);if(p.existsSync(u))throw new Error("Output file already exists. Choose a new path.");const m=new URL(`${i}/${t.split("/").map(encodeURIComponent).join("/")}`,a.origin);return await a.downloadArtifact(m,u,{bytes:o.bytes,sha256:o.sha256}),{receiptId:e,path:t,output:u,bytes:o.bytes,sha256:o.sha256}}export async function revokeCloudSession(e,t){if(!e.trim())throw new Error("Session ID is required");return await(await c(t,!1)).request("DELETE",`/api/v1/auth/cli/sessions/${encodeURIComponent(e)}`),{id:e,revoked:!0}}
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import { type CloudAccountClient } from "../../cloud/account.js";
|
|
2
|
-
export declare function allItems(client: CloudAccountClient, target: string): Promise<Record<string, unknown>[]>;
|
|
3
|
-
export declare function resourceDashboardLinks(client: CloudAccountClient, resource: string, value: unknown, env: NodeJS.ProcessEnv): Promise<unknown>;
|
|
4
|
-
export declare function cloudDashboardUrl(origin: string, organisation: string, project: string, campaign?: string): string;
|
|
5
|
-
export declare function createdResourceDashboardLink(client: CloudAccountClient, resource: "project" | "campaign", result: unknown, env: NodeJS.ProcessEnv): Promise<unknown>;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{isCloudAccountToken as y}from"../../cloud/account.js";import{isUnknownRecord as t}from"../../kernel/unknown-values.js";import{invalidResponse as c}from"../../cloud/transport.js";export async function allItems(i,s){const r=[],e=new Set;let o="";do{const a=new URL(s,i.origin);a.searchParams.set("limit","100"),o&&a.searchParams.set("cursor",o);const n=await i.request("GET",a.href);if(!t(n)||!Array.isArray(n.items)||!n.items.every(t)||typeof n.nextCursor!="string")throw c("resource page");if(r.push(...n.items),o=n.nextCursor,o&&e.has(o))throw c("repeated page cursor");e.add(o)}while(o);return r}export async function resourceDashboardLinks(i,s,r,e){if(!["project","projects","campaign","campaigns"].includes(s)||!t(r))return r;const o=e.TESTKIT_CLOUD_TOKEN;if(o!==void 0&&!y(o))return r;const a=await allItems(i,"/api/v1/organisations"),n=s==="campaign"||s==="campaigns",d=Array.isArray(r.items)?r.items[0]:r;if(n&&d===void 0)return r;if(n&&(!t(d)||typeof d.projectId!="string"))throw c("campaign project");const u=n&&t(d)?await i.request("GET",`/api/v1/projects/${encodeURIComponent(String(d.projectId))}`):void 0;function m(p){if(!t(p))throw c("dashboard resource");const g=n?u:p;if(!t(g)||typeof g.slug!="string")throw c("project slug");const f=a.find(h=>t(h.organisation)&&h.organisation.id===g.organisationId);if(!f||!t(f.organisation)||typeof f.organisation.slug!="string")throw c("organisation slug");if(n&&typeof p.id!="string")throw c("campaign identity");return{...p,dashboardUrl:cloudDashboardUrl(i.origin,f.organisation.slug,g.slug,n?p.id:void 0)}}return Array.isArray(r.items)?{...r,items:r.items.map(m)}:m(r)}export function cloudDashboardUrl(i,s,r,e){const o=`/dashboard/${encodeURIComponent(s)}/${encodeURIComponent(r)}`;return new URL(e?`${o}/campaigns/${encodeURIComponent(e)}`:o,i).href}export async function createdResourceDashboardLink(i,s,r,e){if(!t(r))return r;const o=s==="project"?r.project:{...r,id:r.campaignId};try{const a=await resourceDashboardLinks(i,s,o,e);return t(a)&&typeof a.dashboardUrl=="string"?{...r,dashboardUrl:a.dashboardUrl}:r}catch{return r}}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
export { cloudDashboardUrl } from "./control-links.js";
|
|
2
|
-
import { type ControlOptions } from "./control.js";
|
|
3
|
-
export interface ReadOptions extends ControlOptions {
|
|
4
|
-
limit?: number;
|
|
5
|
-
cursor?: string;
|
|
6
|
-
branch?: string;
|
|
7
|
-
decision?: string;
|
|
8
|
-
pr?: number;
|
|
9
|
-
sort?: string;
|
|
10
|
-
direction?: string;
|
|
11
|
-
flaky?: boolean;
|
|
12
|
-
receipt?: string;
|
|
13
|
-
caseId?: string;
|
|
14
|
-
testPath?: string;
|
|
15
|
-
page?: number;
|
|
16
|
-
campaign?: string;
|
|
17
|
-
filePath?: string;
|
|
18
|
-
rootCauseKey?: string;
|
|
19
|
-
issueReference?: string;
|
|
20
|
-
view?: string;
|
|
21
|
-
order?: string;
|
|
22
|
-
}
|
|
23
|
-
export declare function listCloudResources(resource: string, options: ReadOptions): Promise<unknown>;
|
|
24
|
-
export declare function showCloudResource(resource: string, id: string | undefined, options: ReadOptions): Promise<unknown>;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{resourceDashboardLinks as p}from"./control-links.js";export{cloudDashboardUrl}from"./control-links.js";import{validateCloudListFilters as w}from"./control-resources.js";import{controlClient as m,resolveOrganisation as t,resolveProject as n}from"./control.js";export async function listCloudResources(i,e){w(i,{...e});const a=await m(e,i!=="sessions");let r;switch(i){case"artifacts":if(!e.receipt)throw new Error("Artifact listing requires --receipt");r=`/api/v1/receipts/${encodeURIComponent(e.receipt)}/artifacts`;break;case"metrics":r=`/api/v1/projects/${encodeURIComponent(await n(a,e))}/metrics`;break;case"case-occurrences":case"case-history":if(!e.caseId)throw new Error("Case history requires --case");r=`/api/v1/projects/${encodeURIComponent(await n(a,e))}/cases/${encodeURIComponent(e.caseId)}/${i==="case-history"?"history":"occurrences"}`;break;case"file-history":if(!e.testPath)throw new Error("File history requires --path");r=`/api/v1/projects/${encodeURIComponent(await n(a,e))}/file-history`;break;case"sessions":r="/api/v1/auth/cli/sessions";break;case"organisations":r="/api/v1/organisations";break;case"projects":r="/api/v1/projects";break;case"members":case"invitations":case"providers":case"policies":case"audit":r=`/api/v1/organisations/${encodeURIComponent(await t(a,e.organisation,e.choose))}/${i}`;break;case"notifications":case"tokens":case"campaigns":case"runs":case"tests":r=`/api/v1/projects/${encodeURIComponent(await n(a,e))}/${i}`;break;case"findings":r=`/api/v1/projects/${encodeURIComponent(await n(a,e))}/campaigns/${e.campaign?`${encodeURIComponent(e.campaign)}/`:""}findings`;break;case"cases":r=`/api/v1/projects/${encodeURIComponent(await n(a,e))}/cases`;break;default:throw new Error(`Unknown resource: ${i}`)}const s=new URL(r,a.origin),c={limit:e.limit,cursor:e.cursor,branch:e.branch,decision:e.decision,pr:e.pr,sort:e.sort,direction:e.direction,flaky:e.flaky,path:e.testPath,page:e.page,rootCauseKey:e.rootCauseKey,issueReference:e.issueReference,view:e.view,order:e.order};for(const[d,o]of Object.entries(c))o!==void 0&&s.searchParams.set(d,String(o));return i==="projects"&&e.organisation&&s.searchParams.set("organisationId",await t(a,e.organisation,e.choose)),p(a,i,await a.request("GET",s.href),e.env??process.env)}export async function showCloudResource(i,e,a){const r=await m(a),s=()=>{if(!e)throw new Error(`cloud show ${i} requires an ID`);return encodeURIComponent(e)};let c;switch(i){case"account":c="/api/v1/account";break;case"organisation":c=`/api/v1/organisations/${encodeURIComponent(await t(r,e??a.organisation,a.choose))}`;break;case"project":c=`/api/v1/projects/${encodeURIComponent(await n(r,{...a,project:e??a.project}))}`;break;case"policy":c=`/api/v1/organisations/${encodeURIComponent(await t(r,a.organisation,a.choose))}/policies/${s()}`;break;case"campaign":c=`/api/v1/projects/${encodeURIComponent(await n(r,a))}/campaigns/${s()}`;break;case"finding":if(!a.campaign)throw new Error("Finding detail requires --campaign");c=`/api/v1/projects/${encodeURIComponent(await n(r,a))}/campaigns/${encodeURIComponent(a.campaign)}/findings/${s()}`;break;case"receipt":c=`/api/v1/receipts/${s()}`;break;case"run":c=`/api/v1/runs/${s()}`;break;case"case":c=`/api/v1/projects/${encodeURIComponent(await n(r,a))}/cases/${s()}`;break;case"test-file":c=`/api/v1/projects/${encodeURIComponent(await n(r,a))}/test-file?path=${s()}`;break;case"project-policy":c=`/api/v1/projects/${encodeURIComponent(await n(r,a))}/policy`;break;case"performance":c=`/api/v1/projects/${encodeURIComponent(await n(r,a))}/performance`;break;case"connection":c=`/api/v1/project-connections/${s()}`;break;default:throw new Error(`Unknown resource: ${i}`)}return p(r,i,await r.request("GET",c),a.env??process.env)}
|
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
export declare const cloudListResources: Readonly<Record<string, readonly string[]>>;
|
|
2
|
-
export declare const cloudShowResources: readonly ["account", "organisation", "project", "policy", "campaign", "finding", "receipt", "run", "case", "test-file", "performance", "project-policy", "connection"];
|
|
3
|
-
export declare function validateCloudListFilters(resource: string, options: Record<string, unknown>): void;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
const e=["limit","cursor"];export const cloudListResources={organisations:e,projects:e,members:e,invitations:e,providers:e,policies:e,notifications:e,tokens:e,sessions:[],campaigns:e,findings:[...e,"campaign","rootCauseKey","issueReference","view","order"],runs:["limit","branch","decision","pr","sort","direction","page"],tests:["flaky"],audit:e,cases:e,artifacts:[...e,"receipt"],"case-history":[...e,"caseId"],"file-history":[...e,"testPath"],"case-occurrences":[...e,"caseId"],metrics:[...e,"testPath"]},cloudShowResources=["account","organisation","project","policy","campaign","finding","receipt","run","case","test-file","performance","project-policy","connection"];export function validateCloudListFilters(s,n){const o=Object.hasOwn(cloudListResources,s)?cloudListResources[s]:void 0;if(!o)throw new Error(`Unknown resource: ${s}`);const c=new Set(Object.values(cloudListResources).flat()),r={caseId:"case",testPath:"path",rootCauseKey:"root-cause-key",issueReference:"issue-reference"};for(const t of c){const i=n[t];if(i!==void 0&&i!==!1&&!o.includes(t))throw new Error(`--${r[t]??t} is not supported by cloud list ${s}`)}}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { CloudAccountClient } from "../../cloud/account.js";
|
|
2
|
-
export interface ControlOptions {
|
|
3
|
-
origin?: string;
|
|
4
|
-
env?: NodeJS.ProcessEnv;
|
|
5
|
-
organisation?: string;
|
|
6
|
-
project?: string;
|
|
7
|
-
browser?: boolean;
|
|
8
|
-
signal?: AbortSignal;
|
|
9
|
-
secretOutput?: string;
|
|
10
|
-
notify?: (message: string) => void;
|
|
11
|
-
choose?: (label: string, choices: readonly {
|
|
12
|
-
id: string;
|
|
13
|
-
name: string;
|
|
14
|
-
}[]) => Promise<string>;
|
|
15
|
-
}
|
|
16
|
-
export declare function controlClient(options?: ControlOptions, refreshCredential?: boolean): Promise<CloudAccountClient>;
|
|
17
|
-
export declare function resolveOrganisation(client: CloudAccountClient, selection?: string, choose?: ControlOptions["choose"]): Promise<string>;
|
|
18
|
-
export declare function resolveProject(client: CloudAccountClient, options: ControlOptions): Promise<string>;
|
|
19
|
-
export declare function projectsForRepository(projects: Record<string, unknown>[], origin: string): Record<string, unknown>[];
|
|
20
|
-
export declare function applyAccountCommand(command: Record<string, unknown>, options?: ControlOptions): Promise<unknown>;
|
|
21
|
-
export declare function waitForConnection(client: CloudAccountClient, initial: unknown, options: ControlOptions): Promise<unknown>;
|
|
22
|
-
export interface CreateProjectOptions extends ControlOptions {
|
|
23
|
-
name?: string;
|
|
24
|
-
repository?: string;
|
|
25
|
-
provider?: string;
|
|
26
|
-
host?: string;
|
|
27
|
-
publishDecisions?: boolean;
|
|
28
|
-
requestKey?: string;
|
|
29
|
-
}
|
|
30
|
-
export declare function createCloudProject(options: CreateProjectOptions): Promise<unknown>;
|
|
31
|
-
export declare function parseRepository(value: string, host?: string): {
|
|
32
|
-
repository: string;
|
|
33
|
-
host: string;
|
|
34
|
-
};
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import{setTimeout as v}from"node:timers/promises";import c from"node:fs";import $ from"node:path";import{randomUUID as I}from"node:crypto";import{execFileSync as R}from"node:child_process";import{CloudAccountClient as E}from"../../cloud/account.js";import{invalidResponse as u}from"../../cloud/transport.js";import{isUnknownRecord as p}from"../../kernel/unknown-values.js";import{cloudToken as k}from"./credentials.js";import{openBrowser as C,selectedCloudOrigin as S}from"./login.js";import{allItems as l,createdResourceDashboardLink as x}from"./control-links.js";export async function controlClient(o={},r=!0){const e=S(o);return new E(e,await k(e,o.env??process.env,r))}async function m(o,r,e,n){const t=r?o.filter(a=>a.id===r||a.slug===r||a.name===r):o;if(t.length>1&&!r&&n){const a=t.map(s=>{if(typeof s.id!="string"||!s.id)throw u(`${e} identity`);return{id:s.id,name:typeof s.name=="string"?s.name:s.id}}),d=await n(e,a);if(!a.some(s=>s.id===d))throw new Error(`Invalid ${e} selection`);return d}if(t.length!==1)throw new Error(`${e} ${r?`"${r}" `:""}${t.length?"is ambiguous":"was not found"}. Select an explicit ID from cloud list ${e==="organisation"?"organisations":"projects"}.`);const i=t[0]?.id;if(typeof i!="string"||!i)throw u(`${e} identity`);return i}export async function resolveOrganisation(o,r,e){const n=await l(o,"/api/v1/organisations");return m(n.map(t=>{if(!p(t.organisation))throw u("organisation");return t.organisation}),r,"organisation",e)}export async function resolveProject(o,r){const e=r.project??(r.env??process.env).TESTKIT_CLOUD_PROJECT_ID;if(e?.startsWith("prj_"))return e;const n=r.organisation?`?organisationId=${encodeURIComponent(await resolveOrganisation(o,r.organisation,r.choose))}`:"",t=await l(o,`/api/v1/projects${n}`),i=e?void 0:g(),a=i?projectsForRepository(t,i):[];return m(a.length?a:t,e,"project",r.choose)}function g(){try{return R("git",["remote","get-url","origin"],{encoding:"utf8",stdio:["ignore","pipe","ignore"]}).trim()}catch{return}}export function projectsForRepository(o,r){let e;try{e=parseRepository(r)}catch{return[]}return o.filter(n=>{const t=n.repository;return p(t)&&t.host===e.host&&`${String(t.owner)}/${String(t.name)}`===e.repository})}export async function applyAccountCommand(o,r={}){const e=o.operation==="token.create";if(r.secretOutput!==void 0&&!e)throw new Error("--secret-output is only supported for token.create.");const n=r.secretOutput?$.resolve(r.secretOutput):void 0;if(e&&!n)throw new Error("Credential creation requires --secret-output <new-file> to save the issued token privately.");const t=n?c.openSync(n,"wx",384):void 0;let i=!1,a=!1,d=!1;try{const s=await controlClient(r);d=e;const f=await s.request("POST","/api/v1/account/commands",o);if(e&&t!==void 0&&n!==void 0){if(i=!0,!p(f)||typeof f.value!="string")throw u("issued credential");c.writeFileSync(t,`${f.value}
|
|
2
|
-
`),c.fsyncSync(t);const y=c.fstatSync(t,{bigint:!0}),h=c.lstatSync(n,{bigint:!0});if(!h.isFile()||h.ino!==y.ino||h.dev!==y.dev)throw new Error("Credential output was replaced while the command was running");c.closeSync(t),a=!0;const w={...f};return delete w.value,{...w,secretFile:n}}return await waitForConnection(s,f,r)}catch(s){throw d&&n!==void 0?new Error(`Cloud ${i?"issued the credential, but saving it failed":"may have issued the credential before the request failed"}. Retry the same command and idempotency key with --secret-output pointing to a new file to recover it. Output path: ${n}.`,{cause:s}):s}finally{if(t!==void 0&&!a){const s=c.fstatSync(t,{bigint:!0});c.closeSync(t),!i&&s.size===0n&&n!==void 0&&b(n,s)}}}function b(o,r){try{const e=c.lstatSync(o,{bigint:!0});e.isFile()&&e.size===0n&&e.ino===r.ino&&e.dev===r.dev&&c.unlinkSync(o)}catch(e){if(!p(e)||e.code!=="ENOENT")throw e}}export async function waitForConnection(o,r,e){if(!p(r)||typeof r.operationId!="string"||r.status==="complete")return r;let n=!1;for(;;){e.signal?.throwIfAborted();const t=await O(o,r.operationId);if(t.status==="complete")return t;if(t.status!=="pending"&&t.status!=="processing")throw new Error(`Repository connection ${r.operationId} is ${String(t.status)}. Inspect with cloud show connection ${r.operationId}; retry the create command with a new request key after checking access.`);if(t.status==="pending"&&!n){if(typeof t.verificationUrl!="string")throw u("repository approval link");const i=new URL(t.verificationUrl);if(i.origin!==o.origin||i.username||i.password||i.pathname!==`/cli/projects/${r.operationId}/authorize`)throw u("repository approval link");if(e.notify?.(`Open ${i.href}
|
|
3
|
-
Waiting for repository authorization. Operation: ${r.operationId}
|
|
4
|
-
`),(e.env??process.env).CI)throw new Error(`Repository authorization requires a person. Open ${i.href}, then retry the same request key. Operation: ${r.operationId}`);e.browser!==!1&&C(i.href),n=!0}if((e.env??process.env).CI)throw new Error(`Repository connection ${r.operationId} is processing. Inspect with cloud show connection ${r.operationId}, then retry the same request key to resume.`);if(typeof t.expiresAt!="string"||!Number.isFinite(Date.parse(t.expiresAt)))throw u("connection expiry");if(Date.now()>=Date.parse(t.expiresAt))throw new Error(`Repository connection ${r.operationId} expired. Start a new request.`);await v(2e3,void 0,{signal:e.signal})}}async function O(o,r){const e=await o.request("GET",`/api/v1/project-connections/${encodeURIComponent(r)}`);if(!p(e)||e.operationId!==r||typeof e.status!="string")throw u("repository connection");return e}export async function createCloudProject(o){const r=o.repository??g();if(!r)throw new Error("Pass --repository owner/repo when outside Git or when origin is unavailable.");const e=parseRepository(r,o.host),n=await controlClient(o),t=await resolveOrganisation(n,o.organisation,o.choose),i=o.requestKey??I();o.notify?.(`Request key: ${i}. Reuse --request-key ${i} to resume this operation.
|
|
5
|
-
`);const a=await applyAccountCommand({schemaVersion:1,idempotencyKey:i,operation:"project.create",organisationId:t,input:{name:o.name??e.repository.split("/")[1],repository:e.repository,host:e.host,provider:o.provider??(e.host==="github.com"?"github":"gitea"),publishDecisions:o.publishDecisions??!1}},o);return x(n,"project",a,o.env??process.env)}export function parseRepository(o,r){let e=o.trim(),n="github.com";if(e.includes("://")){const t=new URL(e);if(!["https:","http:","ssh:","git:"].includes(t.protocol)||t.search||t.hash)throw new Error("Repository must be owner/repo or a Git remote URL.");n=t.protocol==="ssh:"||t.protocol==="git:"?t.hostname:t.host,e=t.pathname.replace(/^\//u,"")}else{const t=/^(?:[^@/]+@)?([^/:]+):(.+)$/.exec(e);t?.[1]&&t[2]&&(n=t[1].toLowerCase(),e=t[2])}if(e=e.replace(/\/+$/u,"").replace(/\.git$/u,""),!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(e)||e.split("/").some(t=>t==="."||t===".."))throw new Error("Repository must be owner/repo or a Git remote URL.");return{host:r??n,repository:e}}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { type AccountSession } from "../../cloud/account.js";
|
|
2
|
-
export interface SavedCredential extends AccountSession {
|
|
3
|
-
origin: string;
|
|
4
|
-
token: string;
|
|
5
|
-
}
|
|
6
|
-
export declare function readCredential(origin: string, env?: NodeJS.ProcessEnv): SavedCredential | null;
|
|
7
|
-
export declare function saveCredential(credential: SavedCredential, env?: NodeJS.ProcessEnv): void;
|
|
8
|
-
export declare function removeCredential(origin: string, env?: NodeJS.ProcessEnv): void;
|
|
9
|
-
export declare function cloudToken(origin: string, env?: NodeJS.ProcessEnv, refresh?: boolean): Promise<string>;
|
|
10
|
-
export declare function shouldRefreshCredential(credential: AccountSession, now: number): boolean;
|
|
11
|
-
export declare function canUseCredentialAfterRefreshFailure(credential: AccountSession, error: unknown, now: number): boolean;
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import s from"node:fs";import p from"node:os";import c from"node:path";import{createHash as y,randomUUID as T}from"node:crypto";import{CloudAccountClient as w,cloudOrigin as l,isCloudAccountToken as E,parseSession as O}from"../../cloud/account.js";import{CloudResponseError as h}from"../../cloud/client.js";import{isUnknownRecord as u}from"../../kernel/unknown-values.js";function d(o,e){const r=e.XDG_CONFIG_HOME??(process.platform==="win32"?e.APPDATA:void 0);if(r&&!c.isAbsolute(r))throw new Error("Cloud credential configuration directory must be absolute");const t=r||c.join(p.homedir(),".config"),n=y("sha256").update(l(o)).digest("hex");return c.join(t,"testkit","cloud",`${n}.json`)}function m(o,e){const r=c.dirname(o),t=c.dirname(r),n=c.dirname(t);for(const i of[n,t,r]){let a;try{a=s.lstatSync(i)}catch(f){if(!u(f)||f.code!=="ENOENT")throw f;if(!e)return!1;s.mkdirSync(i,{recursive:i===n,mode:448}),a=s.lstatSync(i)}if(!a.isDirectory()||a.isSymbolicLink())throw new Error("Cloud credential directory must be a real directory, not a symlink");if(i!==n&&process.platform!=="win32"&&(a.mode&63)!==0)throw new Error("Cloud credential directory must be private (mode 0700)")}return!0}export function readCredential(o,e=process.env){const r=d(o,e);if(!m(r,!1))return null;let t;try{t=s.openSync(r,s.constants.O_RDONLY|s.constants.O_NOFOLLOW)}catch(n){if(u(n)&&n.code==="ENOENT")return null;throw n}try{const n=s.fstatSync(t);if(!n.isFile()||n.size>16384||process.platform!=="win32"&&(n.mode&63)!==0)throw new Error("Cloud credential file must be a private regular file");const i=JSON.parse(s.readFileSync(t,"utf8"));if(!u(i)||i.origin!==l(o)||!E(i.token))throw new Error("Invalid Cloud credential file; run testkit cloud login --force");return{...O(i),origin:i.origin,token:i.token}}finally{s.closeSync(t)}}export function saveCredential(o,e=process.env){const r=d(o.origin,e);m(r,!0);const t=`${r}.${T()}.tmp`;try{s.writeFileSync(t,`${JSON.stringify(o)}
|
|
2
|
-
`,{flag:"wx",mode:384}),s.renameSync(t,r)}finally{s.rmSync(t,{force:!0})}}export function removeCredential(o,e=process.env){const r=d(o,e);m(r,!1)&&s.rmSync(r,{force:!0})}export async function cloudToken(o,e=process.env,r=!0){if(e.TESTKIT_CLOUD_TOKEN!==void 0){if(!e.TESTKIT_CLOUD_TOKEN.trim())throw new Error("TESTKIT_CLOUD_TOKEN is empty");return e.TESTKIT_CLOUD_TOKEN}const t=readCredential(o,e);if(!t||Date.parse(t.expiresAt)<=Date.now())throw new Error(e.CI?"Set TESTKIT_CLOUD_TOKEN to an automation token for CI.":`Run testkit cloud login --url ${l(o)} to connect your account, or set TESTKIT_CLOUD_TOKEN for automation.`);if(r&&shouldRefreshCredential(t,Date.now())){let n;try{n=await new w(o,t.token).refresh()}catch(i){if(!canUseCredentialAfterRefreshFailure(t,i,Date.now()))throw i}n&&saveCredential({...t,...n},e)}return t.token}export function shouldRefreshCredential(o,e){const r=Date.parse(o.expiresAt);return r>e&&r-e<10080*6e4&&r<Date.parse(o.maximumExpiresAt)}export function canUseCredentialAfterRefreshFailure(o,e,r){return Date.parse(o.expiresAt)<=r?!1:e instanceof h?e.status>=500||e.status===429:u(e)&&e.code==="cloud_transport_error"}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { type CampaignSelection, type CampaignResult } from "../../cloud/campaigns.js";
|
|
2
|
-
import { CloudClient, type CloudReceipt } from "../../cloud/client.js";
|
|
3
|
-
import type { ControlOptions } from "./control.js";
|
|
4
|
-
export interface CloudEnvironmentOptions extends Pick<ControlOptions, "browser" | "signal" | "secretOutput" | "notify"> {
|
|
5
|
-
env?: NodeJS.ProcessEnv;
|
|
6
|
-
origin?: string;
|
|
7
|
-
projectId?: string;
|
|
8
|
-
campaignId?: string;
|
|
9
|
-
productDir?: string;
|
|
10
|
-
}
|
|
11
|
-
export interface PublishRunToCloudOptions extends CloudEnvironmentOptions {
|
|
12
|
-
productDir: string;
|
|
13
|
-
runId?: null | string;
|
|
14
|
-
}
|
|
15
|
-
export declare function publishRunToCloud(options: Readonly<PublishRunToCloudOptions>): Promise<{
|
|
16
|
-
receipt: CloudReceipt;
|
|
17
|
-
association?: CampaignResult;
|
|
18
|
-
dashboardUrl: string;
|
|
19
|
-
}>;
|
|
20
|
-
export declare function applyCloudMutationFile(filePath: string, options?: Readonly<CloudEnvironmentOptions>): Promise<unknown>;
|
|
21
|
-
export declare function readCloudSnapshot(options?: Readonly<CloudEnvironmentOptions & CampaignSelection>): Promise<unknown>;
|
|
22
|
-
export declare function cloudClientFromEnvironment(options?: Readonly<CloudEnvironmentOptions>): Promise<CloudClient>;
|
package/dist/app/cloud/index.js
DELETED
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{associateReceipt as s,checkVerificationSubmission as m,readCampaignReportFile as l}from"./campaigns.js";import{requireCloudId as u,parseCampaignReport as f}from"../../cloud/campaigns.js";import{isUnknownRecord as I}from"../../kernel/unknown-values.js";import c from"node:fs";import a from"node:path";import{prepareVerifiedRunBundleUpload as w}from"../../evidence/index.js";import{CloudClient as g}from"../../cloud/client.js";import{applyAccountCommand as y}from"./control.js";import{selectedCloudOrigin as C}from"./login.js";import{cloudToken as h}from"./credentials.js";export async function publishRunToCloud(e){if(e.campaignId!==void 0&&(u(e.campaignId,"campaignId"),!e.runId))throw new Error("Campaign publication requires an explicit --run ID");const r=w(e.productDir,e.runId??null),n=await cloudClientFromEnvironment(e),t=await n.publish(r,i=>{e.notify?.(i)});j(e.productDir,r.runId,t);const o=e.campaignId===void 0?void 0:await s(n,t,e.campaignId);return{receipt:t,dashboardUrl:new URL(`/receipts/${encodeURIComponent(t.id)}`,n.origin).href,...o?{association:o}:{}}}export async function applyCloudMutationFile(e,r={}){if(r.campaignId!==void 0){if(r.secretOutput!==void 0)throw new Error("--secret-output is only supported for token.create.");const d=await cloudClientFromEnvironment(r),p=f(l(e),d.projectId,u(r.campaignId,"campaignId"));return await m(d,p,a.resolve(r.productDir??process.cwd())),d.report(p)}const n=a.resolve(e),t=JSON.parse(c.readFileSync(n,"utf8"));if(!I(t))throw new Error("Cloud mutation file must contain a JSON object");if(typeof t.operation=="string")return y(t,r);if(r.secretOutput!==void 0)throw new Error("--secret-output is only supported for token.create.");const o=await cloudClientFromEnvironment(r),i={...t,projectId:typeof t.projectId=="string"?t.projectId:o.projectId};if(i.projectId!==o.projectId)throw new Error("Cloud mutation file projectId does not match TESTKIT_CLOUD_PROJECT_ID");return o.apply(i)}export async function readCloudSnapshot(e={}){return(await cloudClientFromEnvironment(e)).snapshot(e)}export async function cloudClientFromEnvironment(e={}){const r=e.env??process.env,n=C(e);return new g({origin:n,projectId:e.projectId??r.TESTKIT_CLOUD_PROJECT_ID??"",token:await h(n,r)})}function j(e,r,n){const t=a.join(e,".testkit","cloud","receipts");c.mkdirSync(t,{recursive:!0});const o=a.join(t,`${r}.json`),i=`${o}.${String(process.pid)}.${String(Date.now())}.tmp`;c.writeFileSync(i,`${JSON.stringify(n,null,2)}
|
|
2
|
-
`,{mode:384}),c.renameSync(i,o)}
|