@gfdelarue/testkit 0.6.0 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +455 -56
- package/bin/testkit-windows-owner-arm64.exe +0 -0
- package/bin/testkit-windows-owner-x64.exe +0 -0
- package/dist/app/bugs/batch.js +1 -0
- package/dist/app/bugs/ci-report.js +2 -2
- package/dist/app/bugs/ci-summary.js +3 -3
- package/dist/app/bugs/draft.js +2 -2
- package/dist/app/bugs/file.js +1 -1
- package/dist/app/bugs/guide.js +1 -1
- package/dist/app/bugs/record.js +1 -1
- package/dist/app/bugs/run-reading.js +1 -1
- package/dist/app/bugs/store.js +1 -1
- package/dist/app/campaigns/summary.js +1 -1
- package/dist/app/cleanup/index.js +1 -1
- package/dist/app/db-schema/prepare.js +1 -1
- package/dist/app/db-schema/refresh.js +1 -1
- package/dist/app/db-schema/verify.js +1 -1
- package/dist/app/destroy/index.js +1 -1
- package/dist/app/environments/config-driven.js +1 -1
- package/dist/app/environments/file-setup.js +1 -1
- package/dist/app/environments/routing.js +1 -1
- package/dist/app/environments/static-values.js +1 -1
- package/dist/app/environments/supplied.js +1 -1
- package/dist/app/lint/shared.js +1 -1
- package/dist/app/run/live-run.js +1 -1
- package/dist/app/run/orchestrator.js +1 -1
- package/dist/app/run/planning.js +1 -1
- package/dist/app/run/results.js +1 -1
- package/dist/app/run/run-finalization.js +1 -1
- package/dist/app/run/run-finisher.js +1 -0
- package/dist/app/run/setup/runtime-node-setup-protocol.js +1 -1
- package/dist/app/run/setup/runtime-node-setup-worker.js +1 -1
- package/dist/app/run/setup/runtime-node-setup.js +2 -2
- package/dist/app/status/runs.js +1 -1
- package/dist/app/stop/index.js +1 -1
- package/dist/app/tracker/index.js +1 -1
- package/dist/app/tracker/token.js +2 -2
- package/dist/app/typecheck/index.js +4 -4
- package/dist/cli/command-catalog.js +1 -1
- package/dist/cli/command-flags.js +1 -1
- package/dist/cli/commands/bug/file.js +3 -2
- package/dist/cli/framework/help.js +5 -5
- package/dist/cli/framework/parser.js +1 -1
- package/dist/cli/json-errors.js +1 -1
- package/dist/cli/renderers/bug/text.js +1 -1
- package/dist/cli/renderers/run/formatting.js +1 -1
- package/dist/config/loader/config-loader.js +1 -1
- package/dist/config/loader/discover-tests.js +1 -1
- package/dist/config/loader/discovery-result.js +1 -0
- package/dist/config/loader/index.js +1 -1
- package/dist/config/loader/runtime-isolation.js +1 -1
- package/dist/config/loader/skip-config.js +1 -1
- package/dist/config/loader/test-selection.js +1 -1
- package/dist/database/admin.js +5 -2
- package/dist/database/call-caps.js +1 -0
- package/dist/database/cleanup.js +1 -1
- package/dist/database/configured-steps.js +1 -1
- package/dist/database/constants.js +1 -1
- package/dist/database/docker-endpoint.js +1 -0
- package/dist/database/docker-metadata.js +2 -2
- package/dist/database/docker.js +5 -5
- package/dist/database/index.js +2 -1
- package/dist/database/lifecycle-cleanup.js +1 -1
- package/dist/database/local-postgres.js +4 -1
- package/dist/database/naming.js +1 -1
- package/dist/database/operation-deadline.js +1 -0
- package/dist/database/postgres/connection-check.js +1 -1
- package/dist/database/postgres/index.js +2 -2
- package/dist/database/postgres/owned-exec.js +27 -0
- package/dist/database/postgres/protocol.js +2 -2
- package/dist/database/provider.js +1 -0
- package/dist/database/roles.js +2 -2
- package/dist/database/schema-operations.js +5 -1
- package/dist/database/schema-source.js +3 -3
- package/dist/database/steps.js +1 -1
- package/dist/database/template-steps.js +4 -4
- package/dist/domain/suite-selection.js +1 -1
- package/dist/engines/adapters/evidence-files.js +1 -1
- package/dist/engines/adapters/execution.js +1 -1
- package/dist/engines/node-vitest/adapter/adapter.js +1 -1
- package/dist/engines/node-vitest/adapter/driver.js +2 -2
- package/dist/engines/node-vitest/runtime/runtime-input.js +1 -1
- package/dist/engines/playwright/adapter/adapter.js +1 -1
- package/dist/engines/playwright/adapter/browser-network-evidence.js +4 -0
- package/dist/engines/playwright/adapter/config.js +16 -6
- package/dist/engines/playwright/adapter/evidence.js +2 -1
- package/dist/engines/playwright/adapter/failure-details.js +2 -2
- package/dist/engines/playwright/adapter/reporter.js +1 -1
- package/dist/engines/playwright/runtime/browser-network.js +1 -0
- package/dist/engines/playwright/runtime/browser-traffic.js +1 -0
- package/dist/engines/playwright/runtime/chromium-environment.js +1 -0
- package/dist/engines/playwright/runtime/emulation.js +1 -0
- package/dist/engines/playwright/runtime/index.js +1 -1
- package/dist/engines/playwright/runtime/managed-test.js +1 -1
- package/dist/engines/playwright/runtime/runtime-input.js +2 -2
- package/dist/environment/lifecycle-controller.js +1 -1
- package/dist/environment/lifecycle.js +1 -1
- package/dist/environment/maintenance.js +1 -1
- package/dist/environment/run-environment.js +1 -1
- package/dist/environment/run-stopping.js +1 -1
- package/dist/environment/runtime-manager.js +1 -1
- package/dist/evidence/artifacts.js +5 -2
- package/dist/evidence/bundles.js +2 -1
- package/dist/evidence/compressed-artifacts.js +1 -0
- package/dist/evidence/environment-record.js +1 -1
- package/dist/evidence/index.js +1 -1
- package/dist/evidence/live-run-summary.js +1 -0
- package/dist/evidence/local-paths.js +1 -0
- package/dist/evidence/maintenance.js +1 -1
- package/dist/evidence/operation-timings.js +1 -1
- package/dist/evidence/redaction.js +4 -4
- package/dist/evidence/run-evidence.js +1 -0
- package/dist/evidence/setup-progress.js +1 -1
- package/dist/evidence/storage-inventory.js +1 -1
- package/dist/evidence/storage.js +2 -2
- package/dist/kernel/redaction-targets.js +1 -1
- package/dist/kernel/redaction.js +7 -4
- package/dist/process/engine-environment-keys.js +1 -0
- package/dist/process/environment-overlay.js +1 -1
- package/dist/process/environment.js +1 -1
- package/dist/process/execa-bundle.json +3 -3
- package/dist/process/files/evidence-writes.js +1 -0
- package/dist/process/files/product-dir.js +1 -1
- package/dist/process/host-process.js +1 -1
- package/dist/process/index.js +1 -1
- package/dist/process/lifecycle-fence.js +1 -1
- package/dist/process/lock-progress.js +2 -0
- package/dist/process/locks.js +4 -4
- package/dist/process/process-identity.js +1 -1
- package/dist/process/process.js +35 -35
- package/dist/process/recovery.js +1 -1
- package/dist/process/runs/manifest.js +1 -1
- package/dist/process/runs/ownership.js +1 -1
- package/dist/process/runs/stop.js +1 -1
- package/dist/process/shutdown.js +1 -1
- package/dist/process/windows-native.js +1 -1
- package/dist/repo/history.js +7 -4
- package/dist/repo/managed-git.js +1 -1
- package/dist/repo/state.js +4 -4
- package/dist/steps/environment.js +1 -1
- package/dist/toolchains/node-provisioning.js +2 -2
- package/dist/tracker/gitea.js +1 -1
- package/dist/tracker/github.js +1 -1
- package/dist/tracker/http.js +1 -1
- package/dist/tracker/ownership.js +1 -0
- package/dist/tracker/redaction.js +1 -1
- package/dist/tracker/responses.js +1 -1
- package/dist/types/playwright.d.ts +38 -2
- package/dist/types/workspace.d.ts +4 -4
- package/docs/bug-records.md +16 -10
- package/docs/engines.md +52 -4
- package/docs/release-notes.md +749 -0
- package/package.json +14 -2
- package/testkit-config-schema.md +36 -42
- package/dist/config/loader/task-policy.js +0 -1
- package/dist/environment/lifecycle-artifacts.js +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{randomBytes as
|
|
1
|
+
import{randomBytes as R}from"node:crypto";import U from"node:fs";import T from"node:net";import x from"node:os";import M from"node:tls";import{setTimeout as F}from"node:timers/promises";import{NotPostgresError as g,PostgresCheckError as c,PostgresConnectionError as A,PostgresRefusedError as m,PostgresUnansweredError as L,cstring as v,errorFields as D,frontendMessage as S,int32 as Q,md5PasswordResponse as B,parseBackendMessage as _,scramClientFinal as N,scramClientFirst as O,scramServerVerified as j,sslRequest as W,startupMessage as H}from"./protocol.js";const $=["disable","allow","prefer","require","verify-ca","verify-full"],z={2:"Kerberos V5",7:"GSSAPI",9:"SSPI"},G=new Set("08004 08P01 0A000 42501 53100 53200 53300 57P01 57P02 57P03".split(" ")),V={disable:[!1],allow:[!1,!0],prefer:[!0,!1],require:[!0],"verify-ca":[!0],"verify-full":[!0]};export function parsePostgresUrl(t){try{return Z(t)}catch(e){throw k(e,"could not be read")}}export async function checkPostgresConnection(t,e){e.signal?.throwIfAborted();const r=AbortSignal.timeout(e.timeoutMs),a=e.signal===void 0?r:AbortSignal.any([e.signal,r]);try{const n=parsePostgresUrl(t),i=n.host.startsWith("/")?[!1]:V[n.sslmode];for(const[s,o]of i.entries()){const l={retryable:!1,tls:!1};try{await J(n,o,l,a);return}catch(f){const h=l.retryable&&(n.sslmode!=="prefer"||l.tls);if(s===i.length-1||a.aborted||!h)throw f}}}catch(n){throw e.signal?.aborted?e.signal.reason:r.aborted?new L(`there was no PostgreSQL answer within ${String(e.timeoutMs)} ms`,{cause:k(n,"the check failed unexpectedly")}):k(n,"the check failed unexpectedly")}}export async function waitForPostgresConnection(t,e){parsePostgresUrl(t),e.signal?.throwIfAborted();const r=Date.now()+e.timeoutMs;let a=10,n=null;for(let i=e.timeoutMs;i>0;i=r-Date.now()){try{if(await checkPostgresConnection(t,{signal:e.signal,timeoutMs:Math.min(e.attemptTimeoutMs,i)}),Date.now()<=r)return;break}catch(o){if(e.signal?.aborted)throw e.signal.reason;if(o instanceof m)throw o;n=o}if(await e.afterFailure?.(n)===!0){if(Date.now()<=r)return;break}const s=Math.min(a,r-Date.now());if(s<=0)break;try{await F(s,void 0,{signal:e.signal})}catch{throw e.signal?.reason??new c("The database check stopped")}a=Math.min(a*2,e.maxPollIntervalMs)}throw new c(`no connection was accepted within ${String(e.timeoutMs)} ms; the last check: ${k(n,"none finished").message}`,{cause:n})}function Z(t){const e=K(t),r=X(e.query);for(const l of["sslcert","sslkey"])if(r.has(l))throw new c(`sets ${l}, and Testkit's database check presents no client certificate`);if(r.get("channel_binding")==="require")throw new c("requires channel binding, which Testkit's database check does not offer");const a=r.get("host")||e.host;if(a==="")throw new c("names no host");if(a.includes(","))throw new c("names more than one host, and Testkit's database check connects to one");const n=Number(r.get("port")||e.port||"5432");if(!Number.isInteger(n)||n<1||n>65535)throw new c("names a port outside 1 to 65535");const i=r.get("sslrootcert")||null,s=r.get("sslmode")??(i==="system"?"verify-full":"prefer");if(!re(s))throw new c(`sets an sslmode that is not one of ${$.join(", ")}`);if(i==="system"&&s!=="verify-full")throw new c(`sets sslrootcert=system with sslmode=${s}, and libpq allows only verify-full`);if(!a.startsWith("/")&&(s==="verify-ca"||s==="verify-full")&&i===null)throw new c(`sets sslmode=${s} without sslrootcert, a CA file or "system", to verify against`);const o=r.get("user")||e.user||te();return{database:r.get("dbname")||e.database||o,host:a,password:r.get("password")??e.password,port:n,sslmode:s,sslrootcert:i,user:o}}function K(t){const e=/^postgres(?:ql)?:\/\//u.exec(t);if(e===null)throw new c("must be a postgres:// or postgresql:// URL");let r=t.slice(e[0].length),a="",n=null;const i=r.search(/[@/]/u);if(r[i]==="@"){const[u,...y]=r.slice(0,i).split(":");a=b(u),n=y.length===0?null:b(y.join(":")),r=r.slice(i+1)}const s=r.search(/[/?]/u),o=s<0?r:r.slice(0,s);r=s<0?"":r.slice(s);const l=/^\[([^\]]*)\](?::(.*))?$/su.exec(o);if(o.startsWith("[")&&l===null)throw new c("names a malformed IPv6 host");const f=o.lastIndexOf(":"),[h,w]=l?[l[1],l[2]||""]:f<0?[o,""]:[o.slice(0,f),o.slice(f+1)],d=r.indexOf("?");return{database:b((d<0?r:r.slice(0,d)).slice(1)),host:b(h),password:n,port:b(w),query:d<0?"":r.slice(d+1),user:a}}function X(t){const e=new Map,r=t.split("&");r.at(-1)===""&&r.pop();for(const a of r){const[n,i,s]=a.split("=");if(n===""||i===void 0||s!==void 0)throw new c("has a query parameter that is not one name=value pair");e.set(b(n),b(i))}return e}async function J(t,e,r,a){const n=t.host.startsWith("/")?T.connect(`${t.host}/.s.PGSQL.${String(t.port)}`):T.connect(t.port,t.host),i=[n],s=()=>{for(const o of i)o.destroy(new c("The database check stopped"))};a.addEventListener("abort",s,{once:!0});try{a.throwIfAborted();let o=C(n);await new Promise((f,h)=>{n.once("connect",f),n.once("error",w=>{h(I(w,L))})});let l=n;if(e){n.write(W());const f=await o.byte();if(a.throwIfAborted(),f==="S"){r.tls=!0,o.release();try{l=await Y(n,t,i)}catch(h){throw r.retryable=!0,h}o=C(l)}else{if(f!=="N")throw new g;if(t.sslmode!=="allow"&&t.sslmode!=="prefer")throw new c(`TLS failed: the server does not offer TLS, which sslmode=${t.sslmode} needs`)}}l.write(H({user:t.user,database:t.database,application_name:"testkit"})),await ee(l,o,t,r,a),l.write(S("Q",v("SELECT 1")));for(let f=await o.message();f.type!=="Z";f=await o.message())if(f.type==="E")throw q(f.body);await new Promise(f=>{l.end(S("X"),()=>{f()})})}finally{a.removeEventListener("abort",s);for(const o of i)o.destroy()}}function Y(t,e,r){const a=e.sslrootcert,n=a!==null&&e.sslmode!=="allow"&&e.sslmode!=="prefer";let i;if(n&&a!=="system")try{i=U.readFileSync(a)}catch(o){return Promise.reject(new c(`TLS failed: sslrootcert could not be read (${P(o,"unreadable")})`))}const s=M.connect({ca:i,checkServerIdentity:(o,l)=>e.sslmode==="verify-full"?M.checkServerIdentity(e.host,l):void 0,rejectUnauthorized:n,socket:t,...T.isIP(e.host)===0?{servername:e.host}:{}});return r.push(s),new Promise((o,l)=>{s.once("secureConnect",()=>{o(s)}),s.on("error",f=>{l(new c(`TLS failed (${P(f,"handshake")})`))})})}async function ee(t,e,r,a,n){const i=`n=,r=${R(18).toString("base64")}`;let s=null,o=null,l=!1,f=!1;for(let h=!0;;h=!1){const{body:w,type:d}=await e.message();if(h&&d!=="R"&&d!=="E"&&d!=="v")throw new g;if(d==="E")throw a.retryable=!f,q(w);if(d==="Z"){if(!f)throw new c("authentication failed: the server reported ready before it accepted the client");return}if(d!=="R")continue;if(w.length<4)throw new g;const u=w.readInt32BE(0),y=w.subarray(4);if(f)throw new c("authentication failed: the server asked again after it accepted the client");if(u===0){if(s===10&&!l)throw new c("authentication failed: the server ended SCRAM without proving it holds the password");f=!0}else if(s===null&&u===3)s=u,t.write(S("p",v(E(r))));else if(s===null&&u===5){s=u;const p=B(r.user,E(r),y.subarray(0,4));t.write(S("p",v(p)))}else if(s===null&&u===10){if(s=u,!y.toString("utf8").split("\0").includes("SCRAM-SHA-256"))throw new m("the server offers no SASL mechanism that Testkit's database check supports");const p=Buffer.from(O(i));t.write(S("p",v("SCRAM-SHA-256"),Q(p.length),p))}else if(s===10&&u===11&&o===null){const p=await N(E(r),i,y.toString("utf8"),n);o=p.serverSignature,t.write(S("p",Buffer.from(p.clientFinal)))}else if(s===10&&u===12&&o!==null&&!l){if(!j(y.toString("utf8"),o))throw new m("authentication failed: the server's SCRAM signature does not verify");l=!0}else throw s===null&&u!==11&&u!==12?new m(`the server asks for ${z[u]??`method ${String(u)}`} authentication, which Testkit's database check does not support`):new c("authentication failed: the server sent authentication requests out of order")}}function C(t){let e=Buffer.alloc(0),r=null,a=()=>{};const n=s=>{e=Buffer.concat([e,s]),a()};t.on("data",n),t.on("error",s=>{r??=I(s),a()}),t.on("close",()=>{r??=new A("the server closed the connection before startup finished"),a()});const i=async()=>{if(r!==null)throw r;await new Promise(s=>{a=s})};return{async byte(){for(;e.length===0;)await i();const s=e.toString("latin1",0,1);return e=e.subarray(1),s},async message(){for(;;){const s=_(e);if(s!==null)return e=s.rest,s.message;await i()}},release(){if(e.length>0)throw new g;t.off("data",n)}}}function q(t){const e=D(t).C;return e==="28P01"||e==="28000"?new m(`authentication failed (SQLSTATE ${e})`):e==="3D000"?new m("the database does not exist (SQLSTATE 3D000)"):e==="42501"||e==="08P01"?new m(`the server refused the connection (SQLSTATE ${e})`):new c(e!==void 0&&G.has(e)?`the server refused the connection (SQLSTATE ${e})`:"the server refused the connection")}function I(t,e=A){return new e(`the connection failed (${P(t,"socket error")})`)}function P(t,e){return t instanceof Error&&"code"in t&&typeof t.code=="string"?t.code:e}function k(t,e){return t instanceof c?t:new c(e)}function E(t){if(t.password===null)throw new m("authentication failed: the server asks for a password the URL lacks");return t.password}function te(){try{return x.userInfo().username}catch{throw new c("names no user, and the operating system's user name is unavailable")}}function re(t){return $.some(e=>e===t)}function b(t){if(/%(?![0-9A-Fa-f]{2})/u.test(t))throw new c("holds a malformed percent-encoding");if(/%00|\0/u.test(t))throw new c("holds a NUL");try{return decodeURIComponent(t)}catch{throw new c("holds percent-encoded bytes that are not UTF-8, and Testkit's database check reads only UTF-8")}}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import
|
|
2
|
-
`),{cause:
|
|
1
|
+
import w from"node:path";import{randomUUID as O}from"node:crypto";import{setTimeout as N}from"node:timers/promises";import{errorMessage as _}from"../../kernel/error-serialization.js";import{dockerHostAddress as L,shouldUseDockerHostNetwork as v}from"../../process/host/platform.js";import{managedHostProcessInfrastructureError as x,runManagedHostProcess as b,startManagedHostProcess as G}from"../../process/index.js";import{dockerCliEnvironment as R}from"../docker-endpoint.js";import{ownedClientStartFailure as U,ownedPostgresExec as $,reclaimOwnedPostgresExecs as q}from"./owned-exec.js";export const POSTGRES_CLIENT_CONTAINER_LABEL="com.elench.testkit.postgres-client.container";const F="postgres:16",P="postgres";export const POSTGRES_CLIENT_TIMEOUT_MS=30*6e4;const z=["-h","--host","-p","--port","-U","--username","-d","--dbname"],j=/^(?:--host|--port|--username|--dbname)=/,m=String;export function postgresClientImage(e=process.env){return e.TESTKIT_POSTGRES_CLIENT_IMAGE||F}export function runDockerPsql(e){return runDockerPostgresClient({...e,clientCommand:"psql"})}export function startDockerPgDump(e){return startDockerPostgresClient({...e,clientCommand:"pg_dump"})}export async function runDockerPostgresClient(e={}){try{return await startDockerPostgresClient(e)}catch(r){throw A(r)}}export function startDockerPostgresClient({clientCommand:e,args:r=[],consumerSql:o=!1,cwd:n,env:i=process.env,connection:a=null,databaseUrl:s=null,image:c=null,ownedContainer:t=null,purpose:u,signal:d,timeoutMs:f=POSTGRES_CLIENT_TIMEOUT_MS}={}){const p=u?` for ${u}`:"",l=J({args:r,connection:a,databaseUrl:s});if(!X(l))throw new Error(`Dockerized ${e||"Postgres client"} requires a Postgres connection URL or connection object`);if(!e)throw new Error("buildDockerPostgresClientArgs requires clientCommand");const E=y(r,l);if(t&&!o&&rewritePostgresFileArgsForDocker(E,n).mountArgs.length===0){const h=$(t,l,i,[e,"-h","127.0.0.1","-p",m(t.containerPort||5432),"-U",l.user,"-d",l.database||P,...E]);return C("docker",h.args,{cwd:n,displayLabel:`Owned Postgres client${p}`,env:h.env,reclaim:()=>q(t,l,h.id,i),signal:d,startFailure:I=>U(t,e,I),timeoutMs:T(f)})}const g=buildDockerPostgresClientArgs({clientCommand:e,args:r,connection:l,cwd:n,env:i,image:c||postgresClientImage(i),ownedContainer:t}),S=`testkit-postgres-client-${O()}`;g.splice(1,0,"--name",S);const k=D(i,l);return C("docker",g,{cwd:n,displayLabel:`Dockerized Postgres client${p}`,env:k,reclaim:()=>B(S,n,k),signal:d,timeoutMs:T(f)})}function A(e){return H(e)&&e.code==="ENOENT"||e instanceof Error&&e.message.includes('managed host executable "docker"')?new Error("Docker is required for Testkit Postgres client operations, but the docker command was not found."):e}function H(e){return e instanceof Error&&"code"in e}function C(e,r,o){let n;try{n=G({recoveryIdentity:"none",args:r,cwd:o.cwd||process.cwd(),displayLabel:o.displayLabel,env:o.env,executable:e,outputMode:"bounded-capture",signal:o.signal,timeoutMs:o.timeoutMs})}catch(t){throw A(t)}let i=!1,a=!1;const s=n.completion.then(async t=>{const u=a&&W(t)?null:x(o.displayLabel,t);if(t.cancelled||t.timedOut||t.outputLimitExceeded||a)try{await o.reclaim()}catch(f){const p=u||(t.exitCode!==0?new Error(`${o.displayLabel} failed with exit code ${String(t.exitCode)}`):null);if(!p)throw f;const l=new AggregateError([p,f],[p.message,t.stderr.trim(),t.stdout.trim(),_(f)].filter(Boolean).join(`
|
|
2
|
+
`),{cause:p});throw Object.defineProperty(l,"result",{enumerable:!1,value:Object.freeze({...t})}),l}if(u)throw A(u);const d=o.startFailure?.(t);if(d)throw d;return{command:e,escapedCommand:[e,...r].join(" "),exitCode:t.exitCode,failed:t.exitCode!==0||a,signal:t.exitSignal,stderr:t.stderr,stdout:t.stdout}});s.then(()=>{i=!0},()=>{i=!0});const c=s;return Object.defineProperties(c,{pid:{enumerable:!0,get:()=>n.pid},stdout:{enumerable:!0,value:n.stdout},stderr:{enumerable:!0,value:n.stderr},stop:{enumerable:!0,value:()=>i?!1:(a=!0,n.stop(),!0)}}),c}async function B(e,r,o){const n={executable:"docker",cwd:r||process.cwd(),env:o,displayLabel:"Postgres client container cleanup"},i=async()=>{const a=performance.now()+3e4;let s;for(;;){const c=Math.floor(a-performance.now());if(c<=0)throw s??new Error("Postgres client container cleanup timed out");const t=await b({...n,args:["rm","--force",e],timeoutMs:c}),u=x("Postgres client container cleanup",t);if(u)throw u;if(t.exitCode===0)return;const d=t.stderr.trim();if(d===`Error response from daemon: No such container: ${e}`)return;if(s=new Error(`Postgres client container cleanup failed: ${t.stderr}`),d!==`Error response from daemon: removal of container ${e} is already in progress`)throw s;await N(Math.min(100,Math.max(0,a-performance.now())))}};try{(await b({...n,args:["kill","--signal=SIGINT",e],timeoutMs:5e3})).exitCode===0&&await b({...n,args:["wait",e],timeoutMs:2e3})}finally{await i()}}function W(e){return e.error===null&&e.terminationError===null&&!e.timedOut&&!e.cancelled&&!e.outputLimitExceeded}function T(e){if(!Number.isSafeInteger(e)||Number(e)<=0)throw new TypeError("Dockerized Postgres client timeoutMs must be a positive safe integer");return Number(e)}export function buildDockerPostgresClientArgs({clientCommand:e,args:r=[],connection:o,cwd:n,env:i=process.env,image:a=postgresClientImage(i),ownedContainer:s=null}={}){if(!e)throw new Error("buildDockerPostgresClientArgs requires clientCommand");if(!o)throw new TypeError("buildDockerPostgresClientArgs requires connection");const c=y(r,o),{args:t,mountArgs:u}=rewritePostgresFileArgsForDocker(c,n),[d,f,p]=K(o,s),l=D(i,o),E=["PGPASSWORD","PGSSLMODE","PGAPPNAME"].filter(g=>l[g]!=null).flatMap(g=>["-e",g]);return["run","--rm",...d,...s?["--label",`${POSTGRES_CLIENT_CONTAINER_LABEL}=${s.containerId}`]:[],...E,...u,a,e,"-h",f,"-p",p,"-U",o.user,"-d",o.database||P,...t]}function K(e,r){if(!r)return[v()?["--network","host"]:[],m(L(e.host)),m(e.port||5432)];if(!r.containerId)throw new TypeError("ownedContainer.containerId must be a container id");return[["--network",`container:${r.containerId}`],"127.0.0.1",m(r.containerPort||5432)]}export function rewritePostgresFileArgsForDocker(e,r){const o=[],n=[];let i=0;const a=s=>{const c=w.resolve(r||process.cwd(),s),t=`/testkit-postgres-files-${String(i)}`;return i+=1,n.push("--mount",`type=bind,source=${w.dirname(c)},target=${t},readonly`),`${t}/${w.basename(c)}`};for(let s=0;s<e.length;s+=1){const c=e[s],t=e[s+1];if((c==="-f"||c==="--file")&&t){o.push(c,a(t)),s+=1;continue}if(c.startsWith("--file=")){o.push(`--file=${a(c.slice(7))}`);continue}o.push(c)}return{args:o,mountArgs:n}}function J({args:e,connection:r,databaseUrl:o}){return r?{...r,port:r.port||5432,database:r.database||P}:connectionFromDatabaseUrl(o||Q(e))}function Q(e){const r=e[0];return M(r)?r:null}function y(e,r){const o=r?.rawUrl,n=o&&e[0]===o||M(e[0])?e.slice(1):e;return V(n).map(i=>m(i))}function V(e){const r=[];for(let o=0;o<e.length;o+=1){const n=e[o];if(typeof n=="string"&&z.includes(n)){o+=1;continue}j.test(m(n))||r.push(n)}return r}function D(e,r){return R({...e,PGPASSWORD:r.password||"",...r.sslMode?{PGSSLMODE:r.sslMode}:{},...e.PGAPPNAME?{PGAPPNAME:e.PGAPPNAME}:{}})}function M(e){return/^postgres(?:ql)?:\/\//i.test(m(e||""))}function X(e){return!!(e?.host&&e.user)}export function connectionFromDatabaseUrl(e){if(!e)return null;let r;try{r=new URL(m(e))}catch{return null}return["postgres:","postgresql:"].includes(r.protocol)?{rawUrl:e,host:r.hostname,port:r.port||"5432",database:decodeURIComponent(r.pathname.replace(/^\//,""))||P,user:decodeURIComponent(r.username||""),password:decodeURIComponent(r.password||""),sslMode:r.searchParams.get("sslmode")||void 0}:null}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import{randomBytes as $}from"node:crypto";import{errorMessage as p,toError as m}from"../../kernel/error-serialization.js";import{managedHostProcessInfrastructureError as E,runManagedHostProcess as u}from"../../process/index.js";import{dockerCliEnvironment as c}from"../docker-endpoint.js";const d="/tmp/testkit-exec",P=63,g=3e4,x='[ ! -e "$0" ] || exit 130; exec "$@"',I=`id=$1 user=$2 port=$3 dir=${d}
|
|
2
|
+
identity() { stat=$(cat "/proc/$1/stat" 2>/dev/null) || return 1; set -- \${stat##*) }; echo "$2 \${20}"; }
|
|
3
|
+
mkdir -p "$dir" && : >"$dir/$id" || exit 1
|
|
4
|
+
find "$dir" -type f -mmin +1440 -exec rm -f {} + 2>/dev/null
|
|
5
|
+
i=0
|
|
6
|
+
while :; do
|
|
7
|
+
signal=$([ $i -lt 20 ] && echo TERM || echo KILL) remaining=
|
|
8
|
+
for proc in /proc/[0-9]*; do
|
|
9
|
+
pid=\${proc#/proc/}
|
|
10
|
+
before=$(identity "$pid") || continue
|
|
11
|
+
{ tr '\\0' '\\n' <"$proc/environ"; } 2>/dev/null | grep -qx "TESTKIT_EXEC_ID=$id" || continue
|
|
12
|
+
remaining="$remaining $pid"
|
|
13
|
+
[ "\${before%% *}" = 0 ] && [ "$(identity "$pid")" = "$before" ] && kill -"$signal" "$pid" 2>/dev/null
|
|
14
|
+
done
|
|
15
|
+
[ -n "$remaining" ] || break
|
|
16
|
+
[ $i -lt 60 ] || { echo "Client processes are still running:$remaining" >&2; exit 1; }
|
|
17
|
+
sleep 0.1
|
|
18
|
+
i=$((i + 1))
|
|
19
|
+
done
|
|
20
|
+
i=0
|
|
21
|
+
while :; do
|
|
22
|
+
sessions=$(psql -X -qtA -U "$user" -p "$port" -d postgres -c "SELECT count(pg_terminate_backend(pid)) FROM pg_stat_activity WHERE application_name LIKE '%:$id' AND pid <> pg_backend_pid()") || exit 1
|
|
23
|
+
[ "$sessions" != 0 ] || exit 0
|
|
24
|
+
[ $i -lt 100 ] || { echo "$sessions server sessions are still open" >&2; exit 1; }
|
|
25
|
+
sleep 0.1
|
|
26
|
+
i=$((i + 1))
|
|
27
|
+
done`;export function ownedPostgresExec(t,i,n,s,e){if(!t.containerId)throw new TypeError("ownedContainer.containerId must be a container id");const r=$(8).toString("hex"),a=`${(n.PGAPPNAME||"testkit").replace(/[^\x20-\x7e]/gu,"?").slice(0,P-r.length-1)}:${r}`;return{applicationName:a,args:["exec",...["PGPASSWORD","PGAPPNAME","TESTKIT_EXEC_ID",...e?["PGOPTIONS"]:[]].flatMap(l=>["-e",l]),t.containerId,"sh","-c",x,`${d}/${r}`,...s],env:c({...n,PGPASSWORD:i.password||"",PGAPPNAME:a,TESTKIT_EXEC_ID:r,...e?{PGOPTIONS:e}:{}}),id:r}}export async function reclaimOwnedPostgresExecs(t,i,n,s=process.env){const e="Owned Postgres client cleanup";let r;try{const o=await u({args:["exec","-e","PGPASSWORD",t.containerId,"sh","-c",I,"sh",n,i.user,String(t.containerPort||5432)],cwd:process.cwd(),displayLabel:e,env:c({...s,PGPASSWORD:i.password||""}),executable:"docker",timeoutMs:g});r=E(e,o)??(o.exitCode===0?null:new Error(o.stderr.trim()||`${e} exited with ${String(o.exitCode)}`))}catch(o){r=m(o)}if(r)throw new Error(`Terminating the owned Postgres client and its server session failed: ${p(r)}`,{cause:r})}export function ownedClientStartFailure(t,i,n){const s=t.image?`the database image ${t.image}`:"the database image",e=n.stderr.trim();return n.exitCode===127?new Error(`${i} could not start in ${s}: ${e}`):i==="pg_dump"&&e.includes("server version mismatch")?new Error(`pg_dump in ${s} is older than its server: ${e}`):null}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{createHash as a,createHmac as
|
|
2
|
-
parentPort.postMessage(require("node:crypto").pbkdf2Sync(input.password, input.salt, input.iterations, 32, "sha256"));`,c="\\p{Script=Hebrew}\\p{Script=Arabic}\\p{Script=Syriac}\\p{Script=Thaana}\\p{Script=Nko}",
|
|
1
|
+
import{createHash as a,createHmac as x,pbkdf2 as C,timingSafeEqual as R}from"node:crypto";import{once as T}from"node:events";import{promisify as B}from"node:util";import{Worker as A}from"node:worker_threads";const I=196608,O=80877103,w=1024*1024,m="n,,",d=1e7,P=1e5,$=`const { parentPort, workerData: input } = require("node:worker_threads");
|
|
2
|
+
parentPort.postMessage(require("node:crypto").pbkdf2Sync(input.password, input.salt, input.iterations, 32, "sha256"));`,c="\\p{Script=Hebrew}\\p{Script=Arabic}\\p{Script=Syriac}\\p{Script=Thaana}\\p{Script=Nko}",D=/[\u00A0\u1680\u2000-\u200B\u202F\u205F\u3000]/gu,N=/\u00AD|\u034F|\u1806|[\u180B-\u180D]|\u200C|\u200D|\u2060|[\uFE00-\uFE0F]|\uFEFF/gu,k=/[\p{Cc}\p{Cn}\p{Co}\p{Cs}\u0340\u0341\u06DD\u070F\u180E\u200E\u200F\u2028-\u202E\u2061-\u2063\u206A-\u206F\u2FF0-\u2FFB\uFFF9-\uFFFD\u{1D173}-\u{1D17A}\u{E0001}\u{E0020}-\u{E007F}]/u,y=new RegExp(`[${c}]`,"u"),M=new RegExp(`(?![${c}])\\p{L}`,"u"),b=new RegExp(`^[${c}](?:.*[${c}])?$`,"su"),v=B(C);export class PostgresCheckError extends Error{name="PostgresCheckError"}export class PostgresRefusedError extends PostgresCheckError{}export class PostgresConnectionError extends PostgresCheckError{}export class PostgresUnansweredError extends PostgresConnectionError{}export class NotPostgresError extends PostgresCheckError{constructor(){super("the server did not answer as PostgreSQL")}}export function sslRequest(){return Buffer.concat([int32(8),int32(O)])}export function startupMessage(e){const t=Buffer.concat([int32(I),...Object.entries(e).flatMap(([r,n])=>[cstring(r),cstring(n)]),Buffer.from([0])]);return Buffer.concat([int32(4+t.length),t])}export function frontendMessage(e,...t){const r=Buffer.concat(t);return Buffer.concat([Buffer.from(e,"latin1"),int32(4+r.length),r])}export function cstring(e){return Buffer.concat([Buffer.from(e,"utf8"),Buffer.from([0])])}export function int32(e){const t=Buffer.alloc(4);return t.writeInt32BE(e),t}export function parseBackendMessage(e){if(e.length<5)return null;const t=e.readInt32BE(1);if(t<4||t>w)throw new NotPostgresError;return e.length<1+t?null:{message:{type:String.fromCharCode(e[0]),body:e.subarray(5,1+t)},rest:e.subarray(1+t)}}export function errorFields(e){const t={};for(let r=0;r<e.length&&e[r]!==0;){const n=e.indexOf(0,r+1);if(n<0)break;t[String.fromCharCode(e[r])]=e.toString("utf8",r+1,n),r=n+1}return t}export function md5PasswordResponse(e,t,r){const n=a("md5").update(t).update(e).digest("hex");return`md5${a("md5").update(n).update(r).digest("hex")}`}export function scramClientFirst(e){return`${m}${e}`}export async function scramClientFinal(e,t,r,n){const s=g(r),o=g(t).r??"",u=Number(s.i);if(!s.r?.startsWith(o)||!s.s||!Number.isSafeInteger(u)||u<1)throw new PostgresCheckError("the server sent a malformed SCRAM challenge");if(u>d)throw new PostgresRefusedError(`the server asks for ${String(u)} SCRAM iterations, more than the ${String(d)} Testkit's database check computes`);const f=await H(saslprep(e),Buffer.from(s.s,"base64"),u,n),p=i(f,"Client Key"),l=`c=${Buffer.from(m).toString("base64")},r=${s.r}`,E=`${t},${r},${l}`,S=i(a("sha256").update(p).digest(),E),_=Buffer.from(p.map((h,F)=>h^S[F]));return{clientFinal:`${l},p=${_.toString("base64")}`,serverSignature:i(i(f,"Server Key"),E)}}export function scramServerVerified(e,t){const[r,...n]=e.split(",");if(!r.startsWith("v=")||n.some(o=>/^[ev]=/u.test(o)))return!1;const s=Buffer.from(r.slice(2),"base64");return s.length===t.length&&R(s,t)}export function saslprep(e){if(/^[\0-\x7F]*$/u.test(e))return e;const t=e.replace(D," ").replace(N,"").normalize("NFKC");return(!y.test(t)||!M.test(t)&&b.test(t))&&!k.test(t)?t:e}async function H(e,t,r,n){if(n?.throwIfAborted(),r<=P)return v(e,t,r,32,"sha256");const s=new A($,{eval:!0,workerData:{iterations:r,password:e,salt:t}});try{const[o]=await T(s,"message",{signal:n});return Buffer.from(o)}finally{s.terminate()}}function g(e){return Object.fromEntries(e.split(",").map(t=>[t.slice(0,1),t.slice(2)]))}function i(e,t){return x("sha256",e).update(t).digest()}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{listManagedDockerContainers as o,listManagedDockerVolumes as e,removeDockerContainer as r,removeDockerVolume as n,stopDockerContainer as t}from"./docker.js";import{ensureLocalContainer as a,loadExistingLocalContainer as i,stopOwnedLocalContainer as s}from"./local-postgres.js";export const localPostgresProvider={ensure:a,load:i,stopRetained:s,listContainers:o,listVolumes:e,stopContainer:t,removeContainer:r,removeVolume:n};
|
package/dist/database/roles.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{errorMessage as d}from"../kernel/error-serialization.js";import{awaitCapturedResult as
|
|
1
|
+
import{errorMessage as d}from"../kernel/error-serialization.js";import{awaitCapturedResult as O,writeCapturedLine as l}from"../evidence/step-output.js";import{LOCAL_ADMIN_DB as R}from"./constants.js";import{escapeSqlLiteral as S}from"./naming.js";import{runDockerPsql as A}from"./postgres/index.js";import{callCap as g}from"./call-caps.js";export const DATABASE_ROLES_LOCK=[1952805748,1919904869];export async function ensureDatabaseRoles(t,s,e={}){const o=t.testkit.database.roles;if(o.length===0)return;const i=e.setupRegistry?.start({config:t,kind:"database-prerequisites",stage:"database-prerequisites",summary:"ensure declared PostgreSQL roles"}),n=i?._logRecord||null;try{e.signal?.throwIfAborted();const r=e.timeoutMs??g("roleSetup");if(!Number.isSafeInteger(r)||r<=0)throw new Error("PostgreSQL role setup timeout must be a positive integer");if(o.some(c=>c.name===s.user))throw new Error("A declared PostgreSQL role cannot be the owned bootstrap user");const E=await A({args:["-X","-v","ON_ERROR_STOP=1","-c",u(o,r)],connection:{...s,database:R},ownedContainer:{containerId:s.containerId,containerPort:s.containerPort,image:s.image},cwd:t.productDir,env:{...process.env,PGAPPNAME:"testkit:database-prerequisites"},signal:e.signal,timeoutMs:r});O(E,{logRecord:n});const a=i&&e.setupRegistry?.finish(i,{status:"passed",summary:"declared PostgreSQL roles ready"});a&&e.reporter?.setupOperationFinished?.(a)}catch(r){const E=e.signal?.aborted?e.signal.reason:r;E!==r&&l(n,"stderr",d(r)),l(n,"stderr",d(E));const a=i&&e.setupRegistry?.finish(i,{status:e.signal?.aborted?"cancelled":"failed",error:d(E)});throw a&&e.reporter?.setupOperationFinished?.(a),E}}function u(t,s){const e=t.map(i=>N(i.name)).join(", "),o=`DECLARE
|
|
2
2
|
desired text;
|
|
3
3
|
existing pg_authid%ROWTYPE;
|
|
4
4
|
conflicts text[];
|
|
@@ -38,4 +38,4 @@ BEGIN
|
|
|
38
38
|
FOREACH desired IN ARRAY reused LOOP
|
|
39
39
|
RAISE NOTICE 'Reused declared PostgreSQL role "%"', desired;
|
|
40
40
|
END LOOP;
|
|
41
|
-
END`;return`BEGIN; SET LOCAL statement_timeout = ${N(String(
|
|
41
|
+
END`;return`BEGIN; SET LOCAL statement_timeout = ${N(String(s))}; DO ${N(o)}; COMMIT;`}function N(t){return`E'${S(t).replaceAll("\\","\\\\")}'`}
|
|
@@ -1 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import F from"fs";import{resolveContainedTestkitStateDirectory as x}from"../process/runs/product-state.js";import{makeTempDir as P,removeTempDir as U}from"../process/files/temp-dir.js";import{cleanupOrphanedLocalInfrastructure as j,destroyRuntimeDatabase as I,provisionDatabase as H,readRuntimeDatabaseName as V}from"./index.js";import{DEFAULT_SCHEMA_OPERATION_CLEANUP_SECONDS as B,DEFAULT_SCHEMA_OPERATION_TIMEOUT_SECONDS as W,startOperationDeadline as q}from"./operation-deadline.js";import{errorMessage as D}from"../kernel/error-serialization.js";import{observeProcessLockWaits as z}from"../process/locks.js";import{callCapScaleFor as G,withCallCaps as J}from"./call-caps.js";import{forceRefreshSourceSchemaCache as K}from"./schema-source.js";import{bindDatabaseUrl as Q,lendDatabases as X}from"./bindings.js";import{withDatabaseBindings as Y}from"../steps/index.js";export async function verifyDatabaseSchema(a,n,o={}){await L(a,"verify",n,o,async(r,{provision:m})=>{for(const i of r.configs)await m(i)})}export async function refreshDatabaseSchema(a,n,o={}){return L(a,"refresh",n,o,async(r,{bind:m,enter:i,provision:w,signal:d})=>{for(const f of r.configs){if(f.name===r.target.name)break;await w(f)}return i(`refreshing the source schema for ${r.target.name}`),K(m(r.target),null,{reporter:o.progress?.reporter||null,setupRegistry:o.progress?.setupRegistry||null,signal:d})})}async function L(a,n,o,r,m){const i=[],w=new Map;let d=`db schema ${n}`;const f=r.timeoutMs??W*1e3;let M="resolving its services",A=performance.now();const E=e=>{M=e,A=performance.now()};let R="";const S=new Set,c=q(f,()=>(R=`Stopped while ${M}, ${p(performance.now()-A)} into it.`+[...S].map(e=>`
|
|
2
|
+
${e()}.`).join(""),new Error(`${d} did not finish within its ${p(f,0)} limit`))),$=r.signal?AbortSignal.any([r.signal,c.signal]):c.signal,C=r.cleanupTimeoutMs??B*1e3;let b=null;const h=()=>b||(c.dispose(),b=AbortSignal.timeout(C));let v;try{v=Z(a,n)}catch(e){throw c.dispose(),e}let l;try{const e=await o(v.path);d=`${d} for ${e.target.name}`;const s=t=>Y(t,X(e.configs,w));l={value:await z(t=>(S.add(t),()=>{S.delete(t)}),()=>J({overridesMs:r.callCapsMs,scale:G(f)},()=>m(e,{bind:s,enter:E,signal:$,async provision(t){if(!t.testkit.database)return;E(`preparing the database for ${t.name}`),i.push({service:t.name,stateDir:t.stateDir});const g=await H(s(t),{cleanupSignal:h,excludeFromLimit:c.excluding,progress:r.progress,signal:$,skipSchemaSourceVerify:r.skipSchemaSourceVerify});g&&w.set(t.name,Q(g.url))}})))}}catch(e){l={error:$.aborted&&$.reason===c.signal.reason?N(e):e}}finally{c.dispose()}const y=[],O=e=>s=>{y.push(h().aborted?e:`${e}: ${D(s).trim()}`)};for(const{service:e,stateDir:s}of i.reverse()){const t=V(s);t&&await I({productDir:a,stateDir:s},{signal:h()}).catch(O(`the runtime database ${t} for ${e} wasn't dropped`))}if(await j(a,{signal:h()}).catch(O("this product's idle or orphaned Docker resources weren't stopped or removed")),v.remove(),y.length===0){if("error"in l)throw l.error;return l.value}const k=h().aborted?`its cleanup didn't finish within its ${p(C)} allowance:`:"its cleanup failed:",T=[...y.map(e=>`- ${e}`),"`testkit destroy` removes what's left, along with this product's local Postgres container."];if("error"in l)throw new AggregateError([l.error],[D(l.error).trim(),`And ${k}`,...T].join(`
|
|
3
|
+
`),{cause:l.error});throw new Error([`${d} succeeded, but ${k}`,...T].join(`
|
|
4
|
+
`));function N(e){const s=c.signal.reason,t=c.excludedMs(),g=(r.progress?.setupRegistry?.listOperations()||[]).map(u=>{const _=`- ${u.serviceName}: ${u.summary||u.stage}`;return u.status==="running"?`${_}, running for ${p(Date.now()-Date.parse(u.startedAt))}`:`${_}, ${u.status}${u.durationMs?` in ${p(u.durationMs)}`:""}`});return new Error([`${s.message}.${t>=1e3?` Configured template steps ran another ${p(t)}, which the limit doesn't count.`:""}`,...g.length>0?["Steps:",...g]:[],R,...e===s?[]:[D(e).trim()]].join(`
|
|
5
|
+
`),{cause:e})}}function p(a,n=1){return`${(a/1e3).toFixed(n)} s`}function Z(a,n){const o=x(a);F.mkdirSync(o,{recursive:!0});const r=x(a),m=P(`_schema-${n}-`,{parentDir:r});let i=!1;return{path:m,remove(){i||(i=!0,U(m))}}}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{recordWork as l}from"../kernel/work-metrics.js";import o from"fs";import s from"path";import{createRequire as z}from"node:module";import{collectRepoState as J,summarizeRepoStateForMetadata as
|
|
2
|
-
`))}export async function appendSourceSchemaCacheToHash(e,r,t=null){if(!(r.testkit.database?.sourceSchema||null))return;const a=t?.cachePath||await getSourceSchemaCachePath(r),n=t?.active&&t.cacheKey?t.cacheKey:s.relative(getSourceSchemaRootDir(r),s.dirname(a));if(e.update(`source-schema-key:${n}`),e.update(`source-schema-cache:${s.relative(r.productDir,a)}`),!o.existsSync(a)){e.update(":missing");return}const i=o.statSync(a);e.update(`:${i.size.toString()}:${i.mtimeMs.toString()}`);const h=o.readFileSync(a);l("schema.cacheHashFilesRead"),l("schema.cacheHashBytesRead",h.length),e.update(h)}async function j(e,r,t={}){const c=t.setupRegistry,a=c?.start({config:e,stage:"source-schema:refresh",kind:"database-source-schema",summary:`source schema refresh: ${s.relative(e.productDir,r.cachePath)}`});try{const n=await te(
|
|
3
|
-
`}function
|
|
1
|
+
import{recordWork as l}from"../kernel/work-metrics.js";import o from"fs";import s from"path";import{createRequire as z}from"node:module";import{collectRepoState as J,summarizeRepoStateForMetadata as A}from"../repo/state.js";import{findDatabasePlaceholders as V,resolveExecutionEnvValues as X}from"../steps/index.js";import{sourceSchemaEnvNames as $}from"./source-schema-env.js";import{callCap as G}from"./call-caps.js";import{dumpPostgresSchemaToFile as Z,captureTemplateSnapshotText as Q,resolvePgDumpClientImage as Y,runTemplateStep as ee,sanitizeSnapshotText as re}from"./template-steps.js";import{getSourceSchemaRefreshLockPath as te,withSourceSchemaRefreshLock as ae}from"./source-refresh-lock.js";import{resolveSourceSchemaDumpUrl as ce}from"./source-url.js";import{makeTempDir as ne,removeTempDir as se}from"../process/files/temp-dir.js";import{withProcessLockProgress as ie}from"../process/locks.js";import{isNullableString as oe,isUnknownRecord as f}from"../kernel/unknown-values.js";import{requireContainedTestkitStateDirectory as F,resolveContainedTestkitStateDirectory as D}from"../process/runs/product-state.js";const _=z(import.meta.url);let P;function he(){if(P)return P;const e=_.resolve("diff/package.json"),t=JSON.parse(o.readFileSync(e,"utf8"))?.exports?.["."]?.import?.default;if(typeof t!="string"||!t.startsWith("./"))throw new Error("The diff package must declare a local ESM import entry");const c=s.dirname(e),a=s.resolve(c,t),n=s.relative(c,a);if(!n||n===".."||n.startsWith(`..${s.sep}`)||s.isAbsolute(n))throw new Error("The diff package ESM import entry must stay within its package directory");return P=_(a)}const M="db",U="source-schemas",b="source-schema.sql",g="source-schema.meta.json",ue="index.json",N=20,y=String;export function getSourceSchemaRootDir(e){return D(e.productDir,M,e.name,U)}export async function getSourceSchemaCachePath(e,r={}){return(await resolveSourceSchemaCacheLocation(e,r)).cachePath}export async function resolveSourceSchemaCacheLocation(e,r={}){const t=r.repoState||await J(e.productDir,{fingerprints:e.testkit?.fingerprints}),c=getSourceSchemaRootDir(e),a=D(e.productDir,M,e.name,U,...t.cacheKey.split("/").map(w));return{rootDir:c,cacheDir:a,cachePath:s.join(a,b),metadataPath:s.join(a,g),indexPath:s.join(c,ue),cacheKey:t.cacheKey,cacheKind:t.kind,repoState:t}}export async function resolveSourceSchemaState(e,r={}){const t=e.testkit.database?.sourceSchema||null;if(!t)return{active:!1};const c=await resolveSourceSchemaCacheLocation(e,r),a=c.cachePath,n=o.existsSync(a);n&&R(a);const i=r.env||process.env,h=$(e);Re(e,h);const u=X(e,[...h,"CI"],i),m=h.find(W=>{const K=u[W];return typeof K=="string"&&K.trim().length>0})||null,S=m?u[m]:void 0,p=typeof S=="string"?S.trim():null;return t.kind==="env"||!!p||n?{active:!0,cachePath:a,metadataPath:c.metadataPath,indexPath:c.indexPath,rootDir:c.rootDir,cacheDir:c.cacheDir,cacheKey:c.cacheKey,cacheKind:c.cacheKind,repoState:c.repoState,envName:m,sourceUrl:p,sourceSchema:t,cacheExists:n,refreshed:!1,ci:u.CI==="true",unavailableMode:ke(t.unavailable,i)}:{active:!1,cachePath:a}}export async function prepareSourceSchemaCache(e,r={}){r.signal?.throwIfAborted();const t=await resolveSourceSchemaState(e,r);if(!t.active)return t;if(H(t)){if(ye(t))try{const a=await j(e,t,r),n={...t,refreshed:!0,cacheExists:!0,refreshInfo:a};return v(e,n,{refreshed:!0}),n}catch(a){if(r.signal?.aborted)throw a;const n=me(e,t,a,r);if(n)return n;throw a}return v(e,t),r.setupRegistry?.recordCached({config:e,stage:"source-schema",kind:"database-source-schema",summary:"source schema cache hit"}),t}if(t.cacheExists&&t.unavailableMode==="warn-cache")return v(e,t),r.setupRegistry?.recordCached({config:e,stage:"source-schema",kind:"database-source-schema",summary:"source unavailable; using cached source schema"}),t;const c=$(e).join(" or ");throw new Error(`Source schema database URL is unavailable for service "${e.name}". Set ${c}.`)}function me(e,r,t,c={}){if(r.unavailableMode!=="warn-cache")return null;const a=le(e,r);if(!a)return null;const n={...r,...a,cacheExists:!0,refreshed:!1,refreshError:Ke(t)};return c.setupRegistry?.recordCached({config:e,stage:"source-schema",kind:"database-source-schema",summary:`source unavailable; using cached source schema: ${s.relative(e.productDir,a.cachePath)}`}),n}function le(e,r){const t=L(e,r,{cachePath:r.cachePath,metadataPath:r.metadataPath,cacheKey:r.cacheKey,cacheKind:r.cacheKind});if(t)return t;const c=O(r.indexPath);c.sort((a,n)=>Date.parse(n.lastUsedAt||n.refreshedAt||"0")-Date.parse(a.lastUsedAt||a.refreshedAt||"0"));for(const a of c){if(!a.cachePath)continue;const n=s.resolve(e.productDir,a.cachePath),i=L(e,r,{cachePath:n,metadataPath:s.join(s.dirname(n),g),cacheKey:a.cacheKey,cacheKind:a.kind||"unknown"});if(i)return i}return null}function L(e,r,t){const c=s.resolve(t.cachePath);if(s.basename(c)!==b)return null;const a=F(e.productDir,s.dirname(c),`Service "${e.name}" source schema cache directory`),n=s.join(a,b),i=s.resolve(r.rootDir);if(n!==i&&!n.startsWith(`${i}${s.sep}`)||!o.existsSync(n))return null;const h=o.lstatSync(n);if(h.isSymbolicLink()||!h.isFile())throw new Error(`Refusing unsafe source schema cache file at ${n}`);return{cachePath:n,metadataPath:s.join(a,g),cacheDir:a,cacheKey:t.cacheKey,cacheKind:t.cacheKind,indexPath:r.indexPath,rootDir:r.rootDir}}export async function forceRefreshSourceSchemaCache(e,r,t={}){const c=await resolveSourceSchemaState(e,t);if(!c.active||!H(c)){const i=r?.active?r.envName:null,h=r?.active&&r.sourceSchema.kind==="env"?r.sourceSchema.env:null,u=i||h||"source schema env";throw new Error(`Cannot refresh source schema for service "${e.name}" because ${u} is unavailable.`)}const a=await j(e,c,t),n={...c,refreshed:!0,cacheExists:!0,refreshInfo:a};return v(e,n,{refreshed:!0}),n}export async function applySourceSchemaCache(e,r,t,c={}){if(!t?.active)return!1;if(!o.existsSync(t.cachePath))throw new Error(`Missing source schema cache for service "${e.name}": ${t.cachePath}`);const a={kind:"sql-file",path:s.relative(e.productDir,t.cachePath).split(s.sep).join("/"),cwd:null,inputs:[]};return l("schema.sourceReplays"),await ee(e,"source-schema",a,0,r,{...c,stepTimeoutMs:G("sourceSchemaReplay")}),!0}export async function verifyLocalSchemaMatchesSource(e,r,t,c={}){if(!t?.active)return{status:"disabled"};if(!t.sourceSchema.verify||c.skipSchemaSourceVerify)return c.setupRegistry?.recordCached({config:e,stage:"source-schema:verify",kind:"database-source-schema",summary:"source schema verification skipped"}),{status:"skipped"};Pe(e);const a=Se(t.cachePath);l("schema.verificationDumps");const n=await Q(e,r,{ownedContainer:c.ownedContainer||null,reporter:c.reporter||null,signal:c.signal});return l("schema.comparisons"),l("schema.bytesCompared",Buffer.byteLength(a)+Buffer.byteLength(n)),n===a?{status:"matched"}:{status:"mismatch",diagnostics:await ge(e,t,a,n)}}export function createSourceSchemaMismatchError(e,r,t){const c=[`Local schema replay does not match source schema for service "${e.name}".`,`Source cache: ${s.relative(e.productDir,r.cachePath)}`];return t?.status==="mismatch"&&c.push(`Diff: ${s.relative(e.productDir,t.diagnostics.diffPath)}`),new Error(c.join(`
|
|
2
|
+
`))}export async function appendSourceSchemaCacheToHash(e,r,t=null){if(!(r.testkit.database?.sourceSchema||null))return;const a=t?.cachePath||await getSourceSchemaCachePath(r),n=t?.active&&t.cacheKey?t.cacheKey:s.relative(getSourceSchemaRootDir(r),s.dirname(a));if(e.update(`source-schema-key:${n}`),e.update(`source-schema-cache:${s.relative(r.productDir,a)}`),!o.existsSync(a)){e.update(":missing");return}const i=o.statSync(a);e.update(`:${i.size.toString()}:${i.mtimeMs.toString()}`);const h=o.readFileSync(a);l("schema.cacheHashFilesRead"),l("schema.cacheHashBytesRead",h.length),e.update(h)}async function j(e,r,t={}){const c=t.setupRegistry,a=c?.start({config:e,stage:"source-schema:refresh",kind:"database-source-schema",summary:`source schema refresh: ${s.relative(e.productDir,r.cachePath)}`});try{const n=await ae(te(r.cachePath),({waited:h,requestedAt:u})=>ie(`refreshing the source schema for ${e.name}`,async()=>{const m=pe(r,u);if(m)return{reusedExistingRefresh:!0,waitedForLock:h,metadata:m};const S=await de(e,r.sourceUrl,r.cachePath,{reporter:t.reporter||null,logRecord:a?._logRecord||null,env:t.env||process.env,signal:t.signal}),p=fe(e,r,S);return we(r.metadataPath,p),{reusedExistingRefresh:!1,waitedForLock:h,metadata:p}}),{signal:t.signal}),i=a&&c?c.finish(a,{status:"passed",summary:`source schema refresh: ${s.relative(e.productDir,r.cachePath)}`}):null;return i&&t.reporter?.setupOperationFinished?.(i),n}catch(n){const i=a?c?.finish(a,{status:"failed",summary:`source schema refresh: ${s.relative(e.productDir,r.cachePath)}`,error:Ae(n)}):null;throw i&&t.reporter?.setupOperationFinished?.(i),n}}async function de(e,r,t,c={}){const a=ce(r);o.mkdirSync(s.dirname(t),{recursive:!0});const n=ne(".source-schema-",{parentDir:s.dirname(t)}),i=s.join(n,s.basename(t));try{const h=await Y(e,a.dumpUrl,{env:c.env||process.env,pgApplicationName:x(e.name),signal:c.signal});return l("schema.sourceDumps"),await Z(e,i,a.dumpUrl,{...c,image:h,pgApplicationName:x(e.name)}),o.renameSync(i,t),{resolution:a,pgDump:{args:["--schema-only","--no-owner","--no-privileges"],applicationName:x(e.name),image:h}}}finally{se(n)}}function fe(e,r,t){return{refreshedAt:new Date().toISOString(),serviceName:e.name,envName:r.envName,cacheKey:r.cacheKey,cacheKind:r.cacheKind,cachePath:s.relative(e.productDir,r.cachePath),repo:A(r.repoState),sourceUrl:t.resolution.metadata,pgDump:t.pgDump}}function pe(e,r){if(!o.existsSync(e.cachePath))return null;const t=k(e.metadataPath);if(!_e(t))return null;const c=Date.parse(t.refreshedAt);return!Number.isFinite(c)||c<r?null:t}function x(e){return`testkit:schema-source:${e.replace(/[^a-zA-Z0-9._:-]+/g,"-").slice(0,24)||"service"}`}function Se(e){const r=o.readFileSync(e);return l("schema.cacheFilesRead"),l("schema.cacheBytesRead",r.length),re(r.toString("utf8"))}function ye(e){if(!e.cacheExists)return!0;if(e.sourceSchema.refresh.mode==="auto")return e.ci;if(e.sourceSchema.refresh.mode==="ttl"){const r=C(e.metadataPath),t=r?Date.parse(r):0;return!Number.isFinite(t)||t<=0?!0:Date.now()-t>=e.sourceSchema.refresh.ttlSeconds*1e3}return!0}function v(e,r,t={}){if(!r?.active)return;const c=new Date().toISOString(),a=O(r.indexPath).filter(h=>h.cacheKey!==r.cacheKey),n=C(r.metadataPath);a.push({cacheKey:r.cacheKey,kind:r.cacheKind,commitSha:r.repoState.commitSha||null,branch:r.repoState.branch||null,detached:r.repoState.detached,dirty:r.repoState.dirty,dirtyFingerprint:r.repoState.dirtyFingerprint||null,contentFingerprint:r.repoState.contentFingerprint||null,remoteUrl:r.repoState.remoteUrl||null,repository:r.repoState.repository,cachePath:s.relative(e.productDir,r.cachePath),refreshedAt:t.refreshed?c:n,lastUsedAt:c});const i=ve(e,r,a);E(r.indexPath,{version:1,entries:i})}function ve(e,r,t){if(t.length<=N)return t;const c=[...t].sort((n,i)=>Date.parse(i.lastUsedAt||"0")-Date.parse(n.lastUsedAt||"0")),a=new Set(c.slice(0,N).map(n=>n.cacheKey));a.add(r.cacheKey);for(const n of t){if(a.has(n.cacheKey))continue;const i=n.cachePath?s.join(e.productDir,n.cachePath):null,h=i?s.resolve(i):null,u=s.resolve(r.rootDir);if(!i||!h||h!==u&&!h.startsWith(`${u}${s.sep}`))continue;const m=F(e.productDir,s.dirname(i),`Service "${e.name}" pruned source schema cache directory`);o.rmSync(m,{recursive:!0,force:!0})}return t.filter(n=>a.has(n.cacheKey))}function we(e,r){E(e,r)}function E(e,r){o.mkdirSync(s.dirname(e),{recursive:!0}),R(e),o.writeFileSync(e,Ce(r))}function k(e){if(!e||!o.existsSync(e))return null;R(e);try{return JSON.parse(o.readFileSync(e,"utf8"))}catch{return null}}function I(e){return D(e.productDir,"results","schema")}function R(e){if(!o.existsSync(e))return;const r=o.lstatSync(e);if(r.isSymbolicLink()||!r.isFile())throw new Error(`Refusing unsafe source schema state file at ${e}`)}function De(e){return`${w(e.runtimeLabel||e.name)}__${w(e.name)}`}function Pe(e){const r=I(e);if(!o.existsSync(r))return;const t=w(e.name);for(const c of o.readdirSync(r))be(c,t)&&o.rmSync(s.join(r,c),{force:!0})}function be(e,r){for(const t of["__source-schema.sql","__local-replay-schema.sql","__schema.diff","__schema.meta.json"])if(e.endsWith(t))return e.slice(0,-t.length).endsWith(`__${r}`);return!1}async function ge(e,r,t,c){const a=I(e);o.mkdirSync(a,{recursive:!0});const n=De(e),i=s.join(a,`${n}__source-schema.sql`),h=s.join(a,`${n}__local-replay-schema.sql`),u=s.join(a,`${n}__schema.diff`),m=s.join(a,`${n}__schema.meta.json`);return o.writeFileSync(i,t),o.writeFileSync(h,c),o.writeFileSync(u,await Ee(i,h)),E(m,xe(e,r)),{sourceCachePath:r.cachePath,sourcePath:i,localPath:h,diffPath:u,metaPath:m}}function xe(e,r){const t=C(r.metadataPath);return{version:1,createdAt:new Date().toISOString(),serviceName:e.name,runtimeLabel:e.runtimeLabel||e.name,sourceSchema:{envName:r.envName||null,cacheKey:r.cacheKey||null,cacheKind:r.cacheKind||null,cachePath:s.relative(e.productDir,r.cachePath),refreshedAt:t},repo:A(r.repoState)}}function Ee(e,r){return new Promise(t=>{t(buildUnifiedDiffText(o.readFileSync(e,"utf8"),o.readFileSync(r,"utf8"),e,r))})}export function buildUnifiedDiffText(e,r,t="source",c="target"){if(e===r)return"";const{createTwoFilesPatch:a}=he();return a(t,c,y(e),y(r),"","",{context:3})}function ke(e,r){return e==="fail"||e==="warn-cache"?e:r.CI==="true"?"fail":"warn-cache"}function Re(e,r){const t={...e.testkit.serviceEnv,...e.testkit.local?.env};for(const c of r){const a=t[c];if(typeof a!="string")continue;const n=V(a).find(i=>(i.serviceName??e.name)===e.name);if(n)throw new Error(`Service "${e.name}" env ${c} uses ${n.placeholder}, which is the database the source schema verifies. Point it at another service's database or an external database URL.`)}}function w(e){return e.trim().toLowerCase().replace(/[^a-z0-9._-]+/g,"-").replace(/^-+|-+$/g,"")||"schema"}function O(e){const r=k(e);return!f(r)||!Array.isArray(r.entries)?[]:r.entries.filter($e)}function C(e){const r=k(e);return f(r)&&typeof r.refreshedAt=="string"?r.refreshedAt:null}function Ce(e){return`${y(JSON.stringify(e,null,2))}
|
|
3
|
+
`}function Ke(e){const r=T(e,"message");return!!r?r:y(e)}function Ae(e){const r=T(e,"message");return!!r?r:e}function T(e,r){if(!(e===null||typeof e!="object"&&typeof e!="function"))return Reflect.get(e,r)}function H(e){return!!e.sourceUrl}function $e(e){return!f(e)||typeof e.cacheKey!="string"||e.cacheKey.length===0?!1:d(e.branch)&&d(e.cachePath)&&d(e.commitSha)&&d(e.contentFingerprint)&&B(e.detached)&&B(e.dirty)&&d(e.dirtyFingerprint)&&d(e.kind)&&d(e.lastUsedAt)&&d(e.refreshedAt)&&d(e.remoteUrl)&&Fe(e.repository)}function Fe(e){return e==null||f(e)&&typeof e.host=="string"&&typeof e.owner=="string"&&typeof e.name=="string"}function _e(e){return f(e)&&typeof e.refreshedAt=="string"&&(e.sourceUrl===void 0||Me(e.sourceUrl))}function Me(e){return f(e)&&typeof e.originalHost=="string"&&q(e.originalClassification)&&typeof e.resolvedHost=="string"&&q(e.resolvedClassification)&&typeof e.rewritten=="boolean"}function q(e){return e==="neon-pooler"||e==="neon-direct"||e==="unsupported-pooler"||e==="unknown-direct"}function d(e){return e===void 0||oe(e)}function B(e){return e===void 0||typeof e=="boolean"}
|
package/dist/database/steps.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import O from"node:fs";import U from"node:path";import{runDockerPsql as P}from"./postgres/index.js";import{isUnknownRecord as S}from"../kernel/unknown-values.js";import{requireObjectRecord as R}from"../evidence/read-fields.js";const w=String;export async function verifySeed(r={}){const e=f(r),t=l(e.table,"verifySeed.args.table"),n=j(e.where),s=z(e.minRows??1,"verifySeed.args.minRows"),o=g(r),u=`select count(*)::int from ${t}${n?` where ${n}`:""}`,i=Number(await q(o,u,r));if(!Number.isInteger(i)||i<s)throw new Error(`Expected at least ${String(s)} row(s) in ${t}, found ${String(i||0)}`)}export async function verifyRows(r={}){const e=f(r),t=Array.isArray(e.checks)?e.checks:[e],n=g(r),s=[];for(const[o,u]of t.entries()){const i=`verifyRows.args.checks[${String(o)}]`,a=S(u)?u:{},m=l(a.table,`${i}.table`),d=j(a.where),c=z(a.minRows??1,`${i}.minRows`),$=a.severity==="warn"?"warn":"error",A=`select count(*)::int from ${m}${d?` where ${d}`:""}`,y=Number(await q(n,A,r));if(!Number.isInteger(y)||y<c){const N=`Expected at least ${String(c)} row(s) in ${m}, found ${String(y||0)}`;$==="warn"?J(r,{code:"database_verify_rows_min_rows",message:N,table:m,where:d||null,expectedMinRows:c,actualRows:Number.isInteger(y)?y:0}):s.push(N)}}if(s.length>0)throw new Error(`Database row verification failed:
|
|
2
2
|
${s.map(o=>`- ${o}`).join(`
|
|
3
|
-
`)}`)}export async function queryScalar(r={}){const e=f(r),t=g(r),n=v(e.statement??e.query,"queryScalar.args.statement");return q(t,n,r)}export async function runSql(r={}){const e=f(r),t=L(e.statements??e.statement,"runSql.args.statements"),n=g(r);for(const s of t){const o=await b(n,s,r);o.stdout&&process.stdout.write(o.stdout),o.stderr&&process.stderr.write(o.stderr)}}export async function seedRows(r={}){const e=f(r),t=l(e.table,"seedRows.args.table"),n=D(e.rows,"seedRows.args.rows"),s=g(r);if(e.truncate===!0&&await b(s,`truncate table ${t}`,r),n.length===0)return;const o=I(e.columns,n),u=o.map(a=>l(a,`seedRows.args.columns.${a}`)),i=n.map(a=>`(${o.map(m=>E(a[m],r)).join(", ")})`);await b(s,`insert into ${t} (${u.join(", ")}) values ${i.join(", ")}`,r)}export async function materializePostgresBinding(r={}){const e=f(r),t=l(e.table,"materializePostgresBinding.args.table"),n=l(e.keyColumn||"slug","materializePostgresBinding.args.keyColumn"),s=T(e.key,"materializePostgresBinding.args.key"),o=R(e.values,"materializePostgresBinding.args.values"),u=g(r),i=Object.keys(o);if(i.length===0)throw new Error("materializePostgresBinding.args.values must contain at least one column");const a=i.map($=>`${l($,`materializePostgresBinding.args.values.${$}`)} = ${E(o[$],r)}`),m=`with updated as (update ${t} set ${a.join(", ")} where ${n} = ${h(s)} returning 1) select count(*)::int from updated`,d=await b(u,m,r,{scalar:!0}),c=Number((d.stdout||"").trim()||"0");if(c!==1)throw new Error(`Expected to materialize 1 ${t} row for ${n}=${String(s)}, updated ${String(c||0)}`)}export async function updateRows(r={}){const e=f(r),t=l(e.table,"updateRows.args.table"),n=B(e.where,"updateRows.args.where"),s=R(e.values??e.set,"updateRows.args.values"),o=g(r),u=e.expectRows==null?null:z(e.expectRows,"updateRows.args.expectRows"),i=Object.keys(s).map(c=>`${l(c,`updateRows.args.values.${c}`)} = ${E(s[c],r)}`);if(i.length===0)throw new Error("updateRows.args.values must contain at least one column");const a=`with updated as (update ${t} set ${i.join(", ")} where ${W(n,r)} returning 1) select count(*)::int from updated`,m=await b(o,a,r,{scalar:!0}),d=Number((m.stdout||"").trim()||"0");if(u!==null&&d!==u)throw new Error(`Expected to update ${String(u)} ${t} row(s), updated ${String(d||0)}`)}function f(r){return S(r.args)?r.args:{}}function g(r){const e=(r.databaseUrl||r.env?.DATABASE_URL||"").trim();if(!e)throw new Error("Database template step requires DATABASE_URL");return e}async function q(r,e,t){return((await b(r,e,t,{scalar:!0})).stdout||"").trim()}async function b(r,e,t,n={}){const s={args:[r,"-v","ON_ERROR_STOP=1","-X",...n.scalar?["-q","-t","-A"]:[],"-c",e],cwd:t.cwd||t.productDir||process.cwd(),env:t.env||process.env,databaseUrl:r,ownedContainer:t.ownedContainer||null},o=await P(s);if(o.exitCode!==0)throw new Error(o.stderr||`psql failed with exit code ${String(o.exitCode)}`);return o}function L(r,e){const t=Array.isArray(r)?r:r==null?[]:[r];if(t.length===0)throw new Error(`${e} must contain at least one SQL statement`);return t.map((n,s)=>v(n,`${e}[${String(s)}]`))}function v(r,e){const t=w(r||"").trim();if(!t||/;\s*\S/.test(t))throw new Error(`${e} must be one SQL statement`);return t}function D(r,e){if(!Array.isArray(r))throw new Error(`${e} must be an array`);return r.map((n,s)=>R(n,`${e}[${String(s)}]`))}function I(r,e){if(r!=null){if(!Array.isArray(r)||r.length===0)throw new Error("seedRows.args.columns must be a non-empty array");return r.map(n=>String(n))}return[...new Set(e.flatMap(t=>Object.keys(t)))].sort((t,n)=>t.localeCompare(n))}function B(r,e){const t=R(r,e);if(Object.keys(t).length===0)throw new Error(`${e} must contain at least one column`);return t}function W(r,e){return Object.entries(r).map(([t,n])=>`${l(t,`where.${t}`)} = ${E(n,e)}`).join(" and ")}function l(r,e){const t=w(r||"").trim();if(!/^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)?$/.test(t))throw new Error(`${e} must be a SQL identifier or schema-qualified identifier`);return t}function j(r){if(r==null||r==="")return"";const e=w(r).trim();if(!e||/;|--|\/\*/.test(e))throw new Error("verifySeed.args.where must be a single SQL predicate without comments or semicolons");return e}function z(r,e){const t=Number(r);if(!Number.isInteger(t)||t<0)throw new Error(`${e} must be a non-negative integer`);return t}function T(r,e){if(r==null||w(r).length===0)throw new Error(`${e} is required`);return r}function h(r){return r===null?"null":typeof r=="number"&&Number.isFinite(r)?String(r):typeof r=="boolean"?r?"true":"false":`'${w(r).replaceAll("'","''")}'`}function E(r,e){if(!_(r))return h(r);switch(r.kind){case"now":return"now()";case"env":return h(p(e.env||process.env,r.name));case"json":return`${h(JSON.stringify(k(r.value,e)))}::jsonb`;case"postgres-connection-from-env":return`${h(JSON.stringify(C(e.env||process.env,r.prefix)))}::jsonb`;default:throw new Error(`Unsupported database value descriptor "${r.kind}"`)}}function _(r){return S(r)&&typeof r.kind=="string"}function k(r,e){if(Array.isArray(r))return r.map(n=>k(n,e));if(!S(r))return r;if(_(r)){if(r.kind==="env")return p(e.env||process.env,r.name);if(r.kind==="postgres-connection-from-env")return C(e.env||process.env,r.prefix)}return Object.fromEntries(Object.entries(r).map(([t,n])=>[t,k(n,e)]))}function C(r,e){const t=w(e||"").trim();if(!t)throw new Error("postgresConnectionFromEnv requires a non-empty prefix");return{host:p(r,`${t}_HOST`),port:F(r,`${t}_PORT`),database:p(r,`${t}_NAME`),username:p(r,`${t}_USER`),password:p(r,`${t}_PASSWORD`),ssl:Q(r[`${t}_SSL`])}}function p(r,e){const t=String(e),n=r?.[t];if(n==null||n.trim().length===0)throw new Error(`${t} environment variable is required`);return n.trim()}function F(r,e){const t=Number.parseInt(p(r,e),10);if(!Number.isInteger(t)||t<=0)throw new Error(`${e} must be a positive integer`);return t}function Q(r){return r?["1","true","yes","on"].includes(r.trim().toLowerCase()):!1}function J(r,e){const t={severity:"warn",source:"database.verifyRows",serviceName:r.serviceName||null,runtimeId:r.runtimeId||null,timestamp:new Date().toISOString(),...e};typeof r.recordWarning=="function"&&r.recordWarning(t);const n=r.warningFile;n&&(O.mkdirSync(U.dirname(n),{recursive:!0}),O.appendFileSync(n,JSON.stringify(t)+`
|
|
3
|
+
`)}`)}export async function queryScalar(r={}){const e=f(r),t=g(r),n=v(e.statement??e.query,"queryScalar.args.statement");return q(t,n,r)}export async function runSql(r={}){const e=f(r),t=L(e.statements??e.statement,"runSql.args.statements"),n=g(r);for(const s of t){const o=await b(n,s,r);o.stdout&&process.stdout.write(o.stdout),o.stderr&&process.stderr.write(o.stderr)}}export async function seedRows(r={}){const e=f(r),t=l(e.table,"seedRows.args.table"),n=D(e.rows,"seedRows.args.rows"),s=g(r);if(e.truncate===!0&&await b(s,`truncate table ${t}`,r),n.length===0)return;const o=I(e.columns,n),u=o.map(a=>l(a,`seedRows.args.columns.${a}`)),i=n.map(a=>`(${o.map(m=>E(a[m],r)).join(", ")})`);await b(s,`insert into ${t} (${u.join(", ")}) values ${i.join(", ")}`,r)}export async function materializePostgresBinding(r={}){const e=f(r),t=l(e.table,"materializePostgresBinding.args.table"),n=l(e.keyColumn||"slug","materializePostgresBinding.args.keyColumn"),s=T(e.key,"materializePostgresBinding.args.key"),o=R(e.values,"materializePostgresBinding.args.values"),u=g(r),i=Object.keys(o);if(i.length===0)throw new Error("materializePostgresBinding.args.values must contain at least one column");const a=i.map($=>`${l($,`materializePostgresBinding.args.values.${$}`)} = ${E(o[$],r)}`),m=`with updated as (update ${t} set ${a.join(", ")} where ${n} = ${h(s)} returning 1) select count(*)::int from updated`,d=await b(u,m,r,{scalar:!0}),c=Number((d.stdout||"").trim()||"0");if(c!==1)throw new Error(`Expected to materialize 1 ${t} row for ${n}=${String(s)}, updated ${String(c||0)}`)}export async function updateRows(r={}){const e=f(r),t=l(e.table,"updateRows.args.table"),n=B(e.where,"updateRows.args.where"),s=R(e.values??e.set,"updateRows.args.values"),o=g(r),u=e.expectRows==null?null:z(e.expectRows,"updateRows.args.expectRows"),i=Object.keys(s).map(c=>`${l(c,`updateRows.args.values.${c}`)} = ${E(s[c],r)}`);if(i.length===0)throw new Error("updateRows.args.values must contain at least one column");const a=`with updated as (update ${t} set ${i.join(", ")} where ${W(n,r)} returning 1) select count(*)::int from updated`,m=await b(o,a,r,{scalar:!0}),d=Number((m.stdout||"").trim()||"0");if(u!==null&&d!==u)throw new Error(`Expected to update ${String(u)} ${t} row(s), updated ${String(d||0)}`)}function f(r){return S(r.args)?r.args:{}}function g(r){const e=(r.databaseUrl||r.env?.DATABASE_URL||"").trim();if(!e)throw new Error("Database template step requires DATABASE_URL");return e}async function q(r,e,t){return((await b(r,e,t,{scalar:!0})).stdout||"").trim()}async function b(r,e,t,n={}){const s={args:[r,"-v","ON_ERROR_STOP=1","-X",...n.scalar?["-q","-t","-A"]:[],"-c",e],consumerSql:!0,cwd:t.cwd||t.productDir||process.cwd(),env:t.env||process.env,databaseUrl:r,ownedContainer:t.ownedContainer||null},o=await P(s);if(o.exitCode!==0)throw new Error(o.stderr||`psql failed with exit code ${String(o.exitCode)}`);return o}function L(r,e){const t=Array.isArray(r)?r:r==null?[]:[r];if(t.length===0)throw new Error(`${e} must contain at least one SQL statement`);return t.map((n,s)=>v(n,`${e}[${String(s)}]`))}function v(r,e){const t=w(r||"").trim();if(!t||/;\s*\S/.test(t))throw new Error(`${e} must be one SQL statement`);return t}function D(r,e){if(!Array.isArray(r))throw new Error(`${e} must be an array`);return r.map((n,s)=>R(n,`${e}[${String(s)}]`))}function I(r,e){if(r!=null){if(!Array.isArray(r)||r.length===0)throw new Error("seedRows.args.columns must be a non-empty array");return r.map(n=>String(n))}return[...new Set(e.flatMap(t=>Object.keys(t)))].sort((t,n)=>t.localeCompare(n))}function B(r,e){const t=R(r,e);if(Object.keys(t).length===0)throw new Error(`${e} must contain at least one column`);return t}function W(r,e){return Object.entries(r).map(([t,n])=>`${l(t,`where.${t}`)} = ${E(n,e)}`).join(" and ")}function l(r,e){const t=w(r||"").trim();if(!/^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)?$/.test(t))throw new Error(`${e} must be a SQL identifier or schema-qualified identifier`);return t}function j(r){if(r==null||r==="")return"";const e=w(r).trim();if(!e||/;|--|\/\*/.test(e))throw new Error("verifySeed.args.where must be a single SQL predicate without comments or semicolons");return e}function z(r,e){const t=Number(r);if(!Number.isInteger(t)||t<0)throw new Error(`${e} must be a non-negative integer`);return t}function T(r,e){if(r==null||w(r).length===0)throw new Error(`${e} is required`);return r}function h(r){return r===null?"null":typeof r=="number"&&Number.isFinite(r)?String(r):typeof r=="boolean"?r?"true":"false":`'${w(r).replaceAll("'","''")}'`}function E(r,e){if(!_(r))return h(r);switch(r.kind){case"now":return"now()";case"env":return h(p(e.env||process.env,r.name));case"json":return`${h(JSON.stringify(k(r.value,e)))}::jsonb`;case"postgres-connection-from-env":return`${h(JSON.stringify(C(e.env||process.env,r.prefix)))}::jsonb`;default:throw new Error(`Unsupported database value descriptor "${r.kind}"`)}}function _(r){return S(r)&&typeof r.kind=="string"}function k(r,e){if(Array.isArray(r))return r.map(n=>k(n,e));if(!S(r))return r;if(_(r)){if(r.kind==="env")return p(e.env||process.env,r.name);if(r.kind==="postgres-connection-from-env")return C(e.env||process.env,r.prefix)}return Object.fromEntries(Object.entries(r).map(([t,n])=>[t,k(n,e)]))}function C(r,e){const t=w(e||"").trim();if(!t)throw new Error("postgresConnectionFromEnv requires a non-empty prefix");return{host:p(r,`${t}_HOST`),port:F(r,`${t}_PORT`),database:p(r,`${t}_NAME`),username:p(r,`${t}_USER`),password:p(r,`${t}_PASSWORD`),ssl:Q(r[`${t}_SSL`])}}function p(r,e){const t=String(e),n=r?.[t];if(n==null||n.trim().length===0)throw new Error(`${t} environment variable is required`);return n.trim()}function F(r,e){const t=Number.parseInt(p(r,e),10);if(!Number.isInteger(t)||t<=0)throw new Error(`${e} must be a positive integer`);return t}function Q(r){return r?["1","true","yes","on"].includes(r.trim().toLowerCase()):!1}function J(r,e){const t={severity:"warn",source:"database.verifyRows",serviceName:r.serviceName||null,runtimeId:r.runtimeId||null,timestamp:new Date().toISOString(),...e};typeof r.recordWarning=="function"&&r.recordWarning(t);const n=r.warningFile;n&&(O.mkdirSync(U.dirname(n),{recursive:!0}),O.appendFileSync(n,JSON.stringify(t)+`
|
|
4
4
|
`)),Z(r)&&console.warn(t.message)}function Z(r){const e=r.emitWarningsToStderr;return e==null?!0:!["0","false","no","off"].includes(w(e).trim().toLowerCase())}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
`)}})],
|
|
3
|
-
`)).filter(
|
|
4
|
-
`)}function
|
|
1
|
+
import u from"fs";import{buildTemplateExecutionEnv as m,collectConfiguredInputs as E}from"../steps/index.js";import{runConfiguredSteps as T}from"./configured-steps.js";import"../evidence/setup-progress.js";import{captureOutput as v}from"../process/index.js";import{assertPgDumpArgsAreSourceSafe as D}from"./source-url.js";import{runDockerPsql as b,startDockerPgDump as x}from"./postgres/index.js";import{callCap as l,describeCap as d}from"./call-caps.js";const f=String,y=["migrate","seed","verify"];export function runTemplateStage(e,r,t,n={}){const o=e.testkit.database?.template?.[r]||[];return g(e,r,o,0,t,n)}export function runTemplateStep(e,r,t,n,o,s={}){return g(e,r,[t],n,o,s)}async function g(e,r,t,n,o,s){await T({...s,config:e,steps:t,env:{...m(e,{},process.env),DATABASE_URL:o},labelPrefix:`template:${r}`,startIndex:n})}export function collectTemplateInputs(e,r={}){const t=y.flatMap(n=>r[n]||[]);return E(e,{inputs:r.inputs||[],steps:t})}export async function captureTemplateSnapshotText(e,r,t={}){return sanitizeSnapshotText(await S(e,r,t))}export async function dumpPostgresSchemaToFile(e,r,t,n={}){const o=await S(e,t,n);u.writeFileSync(r,o),sanitizeSnapshotFile(r)}async function S(e,r,t){const n={...m(e,{},t.env||process.env),...P(r,{applicationName:t.pgApplicationName})},o=pgDumpArgs();D(o);const s=await C(e,o,n,t);if(s.exitCode!==0)throw new Error(s.shortMessage||s.stderr||s.stdout||"pg_dump failed");return s.stdout}export function pgDumpArgs(){return["--schema-only","--no-owner","--no-privileges"]}function h(e){return{host:e.PGHOST,port:e.PGPORT||5432,database:e.PGDATABASE,user:e.PGUSER,password:e.PGPASSWORD,sslMode:e.PGSSLMODE}}async function C(e,r,t,n={}){const o=l("schemaDump"),s=x({args:r,cwd:e.productDir,env:t,image:n.image||null,ownedContainer:n.ownedContainer||null,purpose:d("the schema-only pg_dump",o),signal:n.signal,timeoutMs:o,connection:h(t)}),a=n.reporter,i=a?.outputMode==="debug"?c=>a.writeDebugLine?.(c):null,p=n.logRecord||null,w=[v(s.stderr,{livePrefix:`[${e.runtimeLabel||e.name}:${e.name}]`,liveWriter:i,onLine(c){p&&p.write(`${new Date().toISOString()} [stderr] ${c}
|
|
2
|
+
`)}})],A=await s;return await Promise.all(w),A}export async function resolvePgDumpClientImage(e,r,t={}){const n=t.env||process.env,o=G(n);if(o)return o;const s={...n,...P(r,{applicationName:t.pgApplicationName})},a=l("serverVersionProbe"),i=await b({args:["-tAc","SHOW server_version_num"],cwd:e.productDir,env:s,purpose:d("the server version probe",a),signal:t.signal,timeoutMs:a,connection:h(s)});if(i.exitCode!==0)throw new Error(i.shortMessage||i.stderr||i.stdout||"Failed to detect source Postgres version");return postgresClientImageForServerVersion(i.stdout)}export function postgresClientImageForServerVersion(e){const r=Number(f(M(e,"")).trim());if(!Number.isFinite(r)||r<1e5)throw new Error(`Unable to determine Postgres major version from server_version_num: ${JSON.stringify(e)}`);return`postgres:${String(Math.floor(r/1e4))}`}function G(e){return e.TESTKIT_POSTGRES_CLIENT_IMAGE||null}function P(e,r={}){const t=new URL(e);return O({PGHOST:t.hostname,PGPORT:t.port||"5432",PGDATABASE:decodeURIComponent(t.pathname.replace(/^\//,"")),PGUSER:decodeURIComponent(t.username||""),PGPASSWORD:decodeURIComponent(t.password||""),PGSSLMODE:t.searchParams.get("sslmode")||void 0,PGAPPNAME:r.applicationName||void 0})}function O(e){const r={};for(const[t,n]of Object.entries(e))n!=null&&n!==""&&(r[t]=n);return r}export function sanitizeSnapshotFile(e){const r=u.readFileSync(e,"utf8"),t=sanitizeSnapshotText(r);t!==r&&u.writeFileSync(e,t)}export function sanitizeSnapshotText(e){return _(f(e).split(`
|
|
3
|
+
`)).filter(r=>{const t=r.trim();return!t.startsWith("-- Dumped from database version ")&&!t.startsWith("-- Dumped by pg_dump version ")&&t!=="SET transaction_timeout = 0;"&&!t.startsWith("\\restrict ")&&!t.startsWith("\\unrestrict ")}).join(`
|
|
4
|
+
`)}function _(e){const r=[];for(let t=0;t<e.length;t+=1){if(e[t]?.trim()==="--"&&e[t+1]?.startsWith("-- Name: public; Type: SCHEMA;")){let n=t+1;for(;n<e.length&&!e[n]?.includes("*not* creating schema");)n+=1;if(n<e.length){for(;n+1<e.length&&e[n+1]?.trim()==="";)n+=1;t=n;continue}}r.push(e[t])}return r}function M(e,r){return!!e?e:r}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{RUN_TYPE_ORDER as f,RUN_TYPES as
|
|
1
|
+
import{RUN_TYPE_ORDER as f,RUN_TYPES as m,isPublicTestType as T,publicTestTypeListText as l}from"./test-types.js";import"./identifiers.js";import{RETIRED_TEST_TYPES as a,rejectRetiredTestType as d}from"./retired-test-types.js";const y=String;export function normalizeTypeValues(e=[],r=a){const t=[];for(const s of e)if(s!=null)for(const n of y(s).split(",")){const i=n.trim();if(!i)continue;const p=i;if(d(p,`Type "${i}"`,r),!m.has(p))throw new Error(`Unknown type "${i}". Expected one of: ${l({includeAll:!0})}.`);t.push(p)}if(t.length===0)return["all"];const o=[...new Set(t)];if(o.includes("all")&&o.length>1)throw new Error('"--type all" cannot be combined with other types.');return o.sort((s,n)=>f.indexOf(s)-f.indexOf(n))}export function parseSuiteSelectors(e=[],r=a){const t=[];for(const o of e)if(o!=null)for(const s of y(o).split(",")){const n=s.trim();if(!n)continue;const i=n.indexOf(":");if(i<0){t.push({kind:"plain",name:n,raw:n});continue}const p=n.slice(0,i),u=n.slice(i+1).trim(),c=p.trim();if(d(c,`Suite selector "${n}"`,r),!T(c))throw new Error(`Unknown suite selector type "${p}". Expected one of: ${l()}.`);if(!u)throw new Error(`Invalid suite selector "${n}". Expected "<type>:<suite-name>".`);t.push({kind:"typed",testType:c,name:u,raw:`${c}:${u}`})}return t}export function suiteKey(e){return`${e.testType??""}:${e.name}`}export function parseTypeOption(e,r=null){const t=[];if(r&&t.push(r),Array.isArray(e)){const o=e;t.push(...o)}else e&&t.push(e);return normalizeTypeValues(t)}export function parseSuiteOption(e){const r=Array.isArray(e)?e:e?[e]:[];return parseSuiteSelectors(r)}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{requireNativeEvidenceJson as
|
|
1
|
+
import{requireNativeEvidenceJson as O}from"./native-report.js";import E from"node:crypto";import{constants as R}from"node:fs";import d from"node:fs/promises";import F from"node:path";import{recordEvidenceWrite as z}from"../../process/files/evidence-writes.js";import{readOnlyNoFollowOpenFlags as U,sameFileSnapshot as C,samePathAndHandleFileSnapshot as L}from"../../process/files/file-snapshot.js";const I=64*1024;export const evidenceFileAccess={lstat:async t=>d.lstat(t,{bigint:!0}),openReadOnly:async t=>d.open(t,U(process.platform,R)),platform:process.platform,readChunk:async(t,n)=>(await t.read(n,0,n.byteLength,null)).bytesRead,statHandle:async t=>t.stat({bigint:!0})};export function stableEvidenceFromBuffer(t){return{contents:t,sha256:E.createHash("sha256").update(t).digest("hex"),sizeBytes:t.byteLength}}export async function readStableJsonFile(t,n,e,r){let i;try{i=await readStableEvidenceFile(t,n,e,r)}catch(o){if(isMissingFileError(o))return{file:null,value:null};throw o}let a;try{a=JSON.parse(i.contents.toString("utf8"))}catch(o){throw new Error(`${e} is not valid JSON`,{cause:o})}return O(a,e),{file:i,value:a}}export async function readStableEvidenceFile(t,n,e,r,i){const a=i??await r.lstat(t);u(a,n,e);const o=await r.openReadOnly(t);try{const w=await r.statHandle(o);u(w,n,e),m(a,w,e,r.platform);const p=[],l=E.createHash("sha256");let h=0;const s=Buffer.allocUnsafe(I);for(;;){const f=await r.readChunk(o,s);if(N(f,s.byteLength,e),f===0)break;if(h+=f,h>n)throw new Error(`${e} exceeds its ${n.toString()} byte limit`);const g=Buffer.from(s.subarray(0,f));p.push(g),l.update(g)}const c=await r.statHandle(o),y=await r.lstat(t);if(u(c,n,e),u(y,n,e),S(w,c,e),S(a,y,e),m(y,c,e,r.platform),BigInt(h)!==w.size)throw new Error(`${e} changed while it was read`);return{contents:Buffer.concat(p,h),sha256:l.digest("hex"),sizeBytes:h}}finally{await o.close()}}export async function writeOwnedEvidenceFile(t,n,e){const r=F.join(t,n),i=F.join(t,`.${n}.${E.randomUUID()}.tmp`);let a=null;try{return a=await d.open(i,"wx",384),await a.writeFile(e),await a.sync(),await a.close(),a=null,z(r),await d.rename(i,r),process.platform!=="win32"&&await d.chmod(r,384),r}catch(o){throw a!==null&&await a.close().catch(()=>{}),await d.rm(i,{force:!0}).catch(()=>{}),o}}export async function snapshotStableEvidenceFile(t,n,e,r,i,a,o){const w=await r.lstat(t);u(w,n,e),S(i,w,e);const p=await r.openReadOnly(t),l=F.join(a,o),h=F.join(a,`.${o}.${E.randomUUID()}.tmp`);let s=null;try{const c=await r.statHandle(p);u(c,n,e),m(w,c,e,r.platform),s=await d.open(h,"wx",384);const y=E.createHash("sha256");let f=0;const g=Buffer.allocUnsafe(I);for(;;){const $=await r.readChunk(p,g);if(N($,g.byteLength,e),$===0)break;if(f+=$,f>n)throw new Error(`${e} exceeds its ${n.toString()} byte limit`);const k=g.subarray(0,$);await s.write(k),y.update(k)}const v=await r.statHandle(p),H=await r.lstat(t);if(u(v,n,e),u(H,n,e),S(c,v,e),S(w,H,e),m(H,v,e,r.platform),BigInt(f)!==c.size)throw new Error(`${e} changed while it was read`);return await s.sync(),await s.close(),s=null,z(l),await d.rename(h,l),process.platform!=="win32"&&await d.chmod(l,384),{filePath:l,identity:{sha256:y.digest("hex"),sizeBytes:f}}}catch(c){throw s!==null&&await s.close().catch(()=>{}),await d.rm(h,{force:!0}).catch(()=>{}),c}finally{await p.close()}}export function evidenceArtifact(t,n,e,r,i,a){return{contentType:e,kind:r,metadata:{sha256:n.sha256,sizeBytes:n.sizeBytes},name:i,path:t,summary:a}}function N(t,n,e){if(!Number.isSafeInteger(t)||t<0||t>n)throw new Error(`${e} returned an invalid read length`)}function u(t,n,e){if(!t.isFile()||t.isSymbolicLink())throw new Error(`${e} must be a regular non-symbolic-link file`);if(t.size<0n||t.size>BigInt(n))throw new Error(`${e} exceeds its ${n.toString()} byte limit`)}function S(t,n,e){if(!C(t,n))throw new Error(`${e} changed while it was read`)}function m(t,n,e,r){if(!L(t,n,r))throw new Error(`${e} changed while it was read`)}export function isMissingFileError(t){return t instanceof Error&&"code"in t&&t.code==="ENOENT"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{isEngineTaskDeadlineExceededReason as d}from"../api/index.js";import"../../domain/test-outcomes.js";import{toError as l}from"../../kernel/error-serialization.js";export async function executeEngineTasks(e,t,n,i,
|
|
1
|
+
import{isEngineTaskDeadlineExceededReason as d}from"../api/index.js";import"../../domain/test-outcomes.js";import{toError as l}from"../../kernel/error-serialization.js";import{redactedPrefix as u}from"../../kernel/redaction.js";export async function executeEngineTasks(e,t,n,i,r){const a=[];for(const o of t){e.observer?.taskStarted(o.task);const s=Date.now(),c=taskBeforeStartOutcome(o.task,e.signal,s,n);c!==null?a.push(c):o.preparationFailure!==null?a.push(terminalOutcome(o.task.id,"failed",s,emptyEvidence(),o.preparationFailure)):a.push(await i(o,s))}return engineExecutionResult(e,a,r)}export function engineExecutionResult(e,t,n,i=[]){for(const r of t)e.observer?.evidenceRecorded(r.taskId,r.evidence),e.observer?.taskFinished(r);return{evidence:{artifacts:t.flatMap(r=>r.evidence.artifacts),logs:t.flatMap(r=>r.evidence.logs),metrics:t.flatMap(r=>r.evidence.metrics),native:n},invocationId:e.invocationId,outcomes:t,...i.length===0?{}:{runErrors:i}}}export function taskBeforeStartOutcome(e,t,n,i){return e.deadlineMs<=n||d(t.reason)?terminalOutcome(e.id,"failed",n,emptyEvidence(),taskTimeoutFailure(e.timeoutMs,i)):t.aborted?terminalOutcome(e.id,"cancelled",n,emptyEvidence(),{code:`${i.codePrefix}_CANCELLED`,message:`${i.label} task was cancelled before its process started`}):null}export function taskTimeoutFailure(e,t){return{code:`${t.codePrefix}_TASK_TIMEOUT`,details:{timeoutMs:e},message:`${t.label} task exceeded its ${e.toString()}ms timeout`}}export function managedProcessFailure(e,t,n=null){return e.terminationError===null?null:{classification:"infra",code:`${t.codePrefix}_PROCESS_TERMINATION_INCOMPLETE`,details:{cancelled:e.cancelled,...n===null?{}:{evidenceCollectionFailure:errorDetails(n)},outputLimitExceeded:e.outputLimitExceeded,timedOut:e.timedOut},message:`${t.label} managed-process termination was incomplete: ${e.terminationError.message}`,...e.terminationError.stack===void 0?{}:{stack:e.terminationError.stack}}}export function classifyManagedProcessResult(e,t,n,i){const r=o=>({failure:o,status:"failed"}),a=managedProcessFailure(e,i);return a!==null?r(a):e.timedOut||n?r(taskTimeoutFailure(t,i)):e.cancelled?{failure:{code:`${i.codePrefix}_CANCELLED`,message:`${i.label} task was cancelled`},status:"cancelled"}:e.outputLimitExceeded?r({code:`${i.codePrefix}_OUTPUT_LIMIT`,message:`${i.label} task exceeded the bounded stdout/stderr capture limit`}):e.error!==null?r({...failureFromError(e.error,`${i.codePrefix}_PROCESS_START_FAILED`),classification:"infra"}):null}export function processExitDetails(e){return{...e.exitCode===null?{}:{exitCode:e.exitCode},...e.exitSignal===null?{}:{exitSignal:e.exitSignal}}}export function processExitFailure(e,t,n=null,i=t.label){return{failure:{code:`${t.codePrefix}_PROCESS_FAILED`,details:processExitDetails(e),message:n??(e.exitCode===null?`${i} process terminated by ${e.exitSignal??"an unknown signal"}`:`${i} process exited with code ${e.exitCode.toString()}`)},status:"failed"}}export function firstOutputLine(e){const t=e.split(/\r?\n/u).map(n=>n.trim()).find(Boolean);return t===void 0?null:u(t,4e3)}export function terminalOutcome(e,t,n,i,r){const a=Date.now();return{durationMs:Math.max(0,a-n),evidence:i,failure:r,finishedAtMs:a,startedAtMs:n,status:t,taskId:e}}export function processOutputEvidence(e){return{artifacts:[],logs:[...e.stdout.length===0?[]:[{stream:"stdout",text:e.stdout}],...e.stderr.length===0?[]:[{stream:"stderr",text:e.stderr}]],metrics:[]}}export function emptyEvidence(){return{artifacts:[],logs:[],metrics:[]}}export function failureFromError(e,t){const n=l(e);return{code:t,message:n.message,...n.stack===void 0?{}:{stack:n.stack}}}export function errorDetails(e){return{message:e.message,name:e.name,...e.stack===void 0?{}:{stack:e.stack}}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{makeTempDirAsync as U}from"../../../process/files/temp-dir.js";import{toError as B}from"../../../kernel/error-serialization.js";import{isDeepStrictEqual as ee}from"node:util";import{classifyManagedProcessResult as te,engineExecutionResult as re,terminalOutcome as P,emptyEvidence as L,failureFromError as j,firstOutputLine as K,processExitDetails as ne,processExitFailure as W,taskBeforeStartOutcome as ie,managedProcessFailure as z,taskTimeoutFailure as oe}from"../../adapters/execution.js";import{requireRootDirectory as se,requireAbsoluteWorkspacePath as H,requireConsumerFile as ae,safeSegment as J}from"../../adapters/workspace.js";import w from"node:fs/promises";import I from"node:path";import{pathToFileURL as ce}from"node:url";import{isEngineTaskDeadlineExceededReason as de,resolveEngineTaskInputs as le}from"../../api/index.js";import{createInvocationProcessEnvironment as ue,runManagedProcess as pe}from"../../../process/index.js";import{managedVitestAliases as me}from"../../installations/node-vitest.js";import{decodeVitestSelection as fe,resolveRepositoryVitest as Ee}from"../selection.js";import{collectNodeVitestEvidence as ve,readNodeVitestRunReport as Te}from"./evidence.js";import{assertNodeVitestSpawnEnvironmentBudget as X,NODE_VITEST_RUNTIME_INPUT_FILE_ENV as Ie,NODE_VITEST_RUNTIME_INPUT_SHA256_ENV as ye,selectNodeVitestForwardedEnvironment as De,splitManagedNodeVitestEnvironment as he,verifyOwnedNodeVitestRuntimeInput as we,writeOwnedNodeVitestRuntimeInput as Ne}from"../runtime/runtime-input.js";import{NODE_VITEST_DRIVER_FILE as Y,writeNodeVitestWorkspace as ge}from"./workspace.js";import{nodeVitestReportOptions as _e}from"./reporter.js";import{engineEvidenceIdentity as Ve}from"../../adapters/evidence-identity.js";import{getNodeStartupCacheEnvironment as Oe}from"../../adapters/node-startup-cache.js";const g={codePrefix:"NODE_VITEST",label:"Node/Vitest"},G="Unhandled Reporter Error";class $ extends Error{constructor(t){const r=B(t);super(r.message,{cause:r}),this.name="NodeVitestConsumerPreparationError"}}export const nodeVitestExecutorDependencies={removeOwnedDirectory:e=>w.rm(e,{force:!0,recursive:!0}),writeRuntimeInput:Ne,writeWorkspace:ge};export async function executeNodeVitestEngine(e,t,r,m=nodeVitestExecutorDependencies){const l=await Re(e,t,m,r),o=Date.now(),s=l.tasks.map(c=>{e.observer?.taskStarted(c.task);const f=c.preparationFailure;return ie(c.task,e.signal,o,g)??(f===null?null:P(c.task.id,"failed",o,L(),f))}),p=l.tasks.filter((c,f)=>s[f]===null),i=[],v=p.length===0?[]:await Se(e,l,p,o,i);let E=0;return re(e,s.map(c=>c??v[E++]),ke(e,l),i)}async function Re(e,t,r,m){const l=e.profile.id,o=await se(e.workspace.rootDirectory);H(e.workspace.temporaryDirectory,"temporary directory"),H(e.workspace.artifactDirectory,"artifact directory"),await w.mkdir(e.workspace.temporaryDirectory,{recursive:!0}),await w.mkdir(e.workspace.artifactDirectory,{recursive:!0});const s=await w.realpath(e.workspace.temporaryDirectory),p=await w.realpath(e.workspace.artifactDirectory),i=await U(`node-vitest-${J(e.invocationId)}-`,{parentDir:s});m.defer(()=>r.removeOwnedDirectory(i));const v=I.join(i,"reports");await w.mkdir(v,{mode:448});const E=await ue(i);let c=[];for(const[n,u]of e.tasks.entries()){const a=le(e,u),N=`${n.toString().padStart(3,"0")}-${J(u.id)}`,y=await U(`${N}-`,{parentDir:p});try{const x=await Z(()=>ae(o,u.file,"Node/Vitest task file")),C=I.join(v,`${N}.json`),M={file:x,reportFile:C,sourceFile:u.file,taskId:u.id},A=I.join(y,"data");c.push({artifactDirectory:y,inputs:a,task:u,preparationFailure:null,prepared:{dataDirectory:A,module:M}})}catch(x){const C=Q(x);c.push({artifactDirectory:y,inputs:a,task:u,preparationFailure:C,prepared:null})}}const f=c.filter(n=>n.prepared!==null),d=I.join(i,"setup.mjs"),T=I.join(i,"input-bootstrap.mjs"),D=I.join(i,"driver-request.json"),R=I.join(v,"run.json"),_=I.join(i,"startup-failure.txt");let V=null,S=null,h=t.vitest;const F=`--import=${JSON.stringify(ce(T).href)}`;let O={...E.environment,FORCE_COLOR:"0",NODE_OPTIONS:F,NO_COLOR:"1"};if(f.length>0)try{xe(f);const{configuration:n}=f[0].inputs,u=n===void 0?null:fe(n),a=u!==null&&u.runner!=="managed"?u:null;a!==null&&(h=Ee(o));const N=Object.keys(f[0].inputs.supportModules);if(N.length>0)throw new $(new Error(`Node/Vitest engine does not support support module(s): ${N.join(", ")}`));let y=await Z(()=>Ce(e,l,f,a?.host??null));if(a===null){const k=he(y.environment);y={...y,environment:k.overlay},O={...O,...k.process}}else{const k=b=>process.platform==="win32"?b.toUpperCase():b,q=new Set(Object.keys(y.environment).map(k));O={...Object.fromEntries(Object.entries(a.host).filter(([b])=>!q.has(k(b)))),...y.environment,NODE_OPTIONS:[F,a.host.NODE_OPTIONS].join(" ").trim()}}V=await r.writeRuntimeInput(i,y);const{file:x,sha256:C,sizeBytes:M}=V;await r.writeWorkspace({inputBootstrapFile:T,runtimeInput:{expectedFile:x,expectedInvocationId:e.invocationId,expectedSha256:C,expectedSizeBytes:M,startupFailure:{driverFile:Y,file:_,vitestWorkersDirectory:I.join(h.root,"dist","workers")}},setupFile:d,vitestModule:h.index});const A={cacheDirectory:I.join(i,"cache"),invocationId:e.invocationId,runReportFile:R,setupFile:d,startupFailureFile:_,vitest:{entrypoint:h.entrypoint,name:"vitest",version:h.version}};S=a===null?{...A,aliases:me(t),config:!1,managed:{externalModules:[t.authoringEntrypoints.data.replaceAll(I.sep,"/")]},project:null,test:{disableConsoleIntercept:!0,environment:"node",fileParallelism:!1,isolate:!0,maxWorkers:1,pool:"forks"}}:{...A,aliases:{},config:a.config,managed:null,project:a.project,test:{}}}catch(n){const u=Q(n);c=c.map(a=>a.prepared===null?a:{...a,preparationFailure:u,prepared:null})}return Object.freeze({driverRequest:S,driverRequestFile:D,installation:t,processEnvironment:O,profileId:l,rootDirectory:o,runReportFile:R,runtimeInput:V,startupFailureFile:_,tasks:Object.freeze(c),vitest:h})}async function Se(e,t,r,m,l){const o=n=>r.map(({task:u})=>P(u.id,"failed",m,L(),n(u.timeoutMs)));try{if(t.runtimeInput===null||t.driverRequest===null)throw new Error("Node/Vitest run was not prepared");await we(t.runtimeInput)}catch(n){return o(()=>({...j(n,"NODE_VITEST_INPUT_INVALID"),classification:"infra"}))}const s=await Fe(e,t,t.runtimeInput,r[0]),p=Math.min(...r.map(({task:n})=>n.deadlineMs))-Date.now();if(p<=0)return o(n=>oe(n,g));const i=t.driverRequest.managed===null?{}:{hookTimeout:p,teardownTimeout:p,testTimeout:p},v=r.map(({prepared:n})=>n.module),E={...t.driverRequest,modules:v,test:{...t.driverRequest.test,...i}};await w.writeFile(t.driverRequestFile,JSON.stringify(E),{flag:"wx",mode:384});const c=await pe({args:[Y,t.driverRequestFile],cwd:t.rootDirectory,displayLabel:"Node/Vitest",env:s,executable:process.execPath,signal:e.signal,timeoutMs:p}),f=await w.readFile(t.startupFailureFile,"utf8").catch(()=>null),d=await Te(e.invocationId,t.runReportFile),T=d instanceof Error?[]:d.unhandledErrors.filter(({type:n})=>n!==G),D=[];for(const{artifactDirectory:n,prepared:u}of r)D.push(await ve({identity:_e(e.invocationId,u.module).identity,artifactDirectory:n,dataDirectory:u.dataDirectory,reportFile:u.module.reportFile,rootDirectory:t.rootDirectory,stderr:c.stderr,stdout:c.stdout,unhandledErrors:T.filter(({testPath:a})=>a===void 0||I.resolve(a)===I.resolve(u.module.file))}).catch(a=>nodeVitestEvidenceCollectionFailure(c,a)));const R=D.map(n=>"reportSummary"in n?n.reportSummary:null),_=de(e.signal.reason),V=c.cancelled||c.timedOut||_,S=V&&d instanceof Error&&R.includes(null),h=T.length>0||R.some(n=>n?.success===!1||n?.numTotalTests===0),F=d instanceof Error?0:c.exitCode??d.exitCode,O=d instanceof Error?S?{exitCode:0,unhandledErrors:[]}:d:{...d,exitCode:F===1&&(h||d.afterTests!==void 0)?0:F};return!(d instanceof Error)&&d.afterTests!==void 0&&l.push(`Vitest failed the run after its tests passed: ${d.afterTests.join("; ")}`),r.map(({task:n},u)=>{const a=D[u];if(f!==null&&!V)return P(n.id,"failed",m,"reportSummary"in a?a.evidence:L(),{classification:"infra",code:"NODE_VITEST_START_FAILED",message:`Vitest could not start: ${f}`});if(!("reportSummary"in a))return P(n.id,"failed",m,L(),a);const{failure:N,status:y}=classifyNodeVitestTaskResult(c,O,a,n.timeoutMs,_,S);return P(n.id,y,m,a.evidence,N)})}export function classifyNodeVitestTaskResult(e,t,r,m,l,o=!1){const s=d=>({failure:d,status:"failed"}),p=z(e,g);if(p!==null)return s(p);const i=r.reportSummary,v=i===null||!o?te(e,m,l,g):null;if(v!==null)return v;const E=t instanceof Error?void 0:t.unhandledErrors.find(({type:d})=>d===G),c=t instanceof Error?void 0:t.failure??(E&&{...E,phase:"reporter"});if(c!==void 0){const{message:d,phase:T,stack:D}=c;return{failure:{classification:"infra",code:"NODE_VITEST_RUN_FAILED",details:{phase:T},message:`Vitest ${T} failed: ${d}`,...D===void 0?{}:{stack:D}},status:T==="config"?"not_run":"failed"}}if(r.environmentFailure!==null)return s(r.environmentFailure);if(i===null||t instanceof Error){const d=K(e.stderr)??K(e.stdout),T=e.exitCode===0?null:W(e,g,d,"Vitest").failure;return s({...j(r.reportError??t,"NODE_VITEST_REPORT_INVALID"),...T===null?{}:{details:T.details,message:T.message},classification:"infra"})}const f=o?null:e.exitSignal;if(t.exitCode!==0||f!==null){const d=f===null?`exited with code ${t.exitCode.toString()}`:`was terminated by ${f}`;return s({classification:"infra",code:"NODE_VITEST_EXIT_CODE",details:ne(e),message:`Vitest ${d}, which its test results do not explain`})}return i.numTotalTests===0&&r.reportErrorCount===0?s({code:"NODE_VITEST_NO_TESTS",message:"Vitest did not discover any tests for the task file"}):!i.success||i.numFailedTests>0||r.reportErrorCount>0?W(e,g,r.reportFailureMessage,"Vitest"):i.numPassedTests===0&&(i.numPendingTests>0||i.numTodoTests>0)?{failure:null,status:"skipped"}:r.dataEvidenceError!==null?s(j(r.dataEvidenceError,"NODE_VITEST_DATA_EVIDENCE_INVALID")):i.numPassedTests===0?s({code:"NODE_VITEST_REPORT_FAILED",message:"Vitest report did not contain a completed passing test"}):{failure:null,status:"passed"}}export function nodeVitestEvidenceCollectionFailure(e,t){const r=B(t);return z(e,g,r)??{...j(r,"NODE_VITEST_EVIDENCE_FAILED"),classification:"infra"}}async function Fe(e,t,r,{inputs:m,task:l}){const o=e.target.baseUrl,s={...t.processEnvironment,...o===null?{}:{BASE_URL:o},TESTKIT_ACTIVE:"1",[Ie]:r.file,[ye]:r.sha256,TESTKIT_INVOCATION_ID:e.invocationId,TESTKIT_LEASE_DIR:l.orchestration.lease.directory,TESTKIT_LEASE_ID:l.orchestration.lease.id,TESTKIT_MODE:l.orchestration.mode,...l.orchestration.runtimeId===void 0?{}:{TESTKIT_RUNTIME_ID:l.orchestration.runtimeId}},p=t.driverRequest?.managed!==null;p&&X(s);const i={...t.processEnvironment,...m.environment};Object.assign(s,await Oe(e.workspace.cacheDirectory,i));try{p&&X(s)}catch{delete s.NODE_COMPILE_CACHE}return s}function xe(e){const t=({prepared:o})=>o.module.file,r=e.find((o,s)=>e.findIndex(p=>t(p)===t(o))!==s);if(r!==void 0)throw new Error(`Node/Vitest task ${r.task.id} selects a file that another task in the request selects`);const m=({inputs:o,task:s})=>[o.environment,o.supportModules,s.orchestration,s.timeoutMs],l=e.find(o=>!ee(m(o),m(e[0])));if(l!==void 0)throw new Error(`Node/Vitest task ${l.task.id} needs a different environment, support modules, orchestration or timeout than ${e[0].task.id}`)}function Ce(e,t,r,m){const{baseUrl:l,databases:o,endpoints:s,handles:p}=e.target;return{...m===null?{}:{host:m},baseUrl:l,databases:o,endpoints:s,engineId:"node-vitest",environment:De(r[0].inputs.environment),handles:p,invocationId:e.invocationId,profileId:t,tasks:r.map(({inputs:i,prepared:v,task:E})=>({context:{baseUrl:l,engineId:"node-vitest",invocationId:e.invocationId,profileId:t,serviceName:E.serviceName,task:{file:E.file,id:E.id,suiteName:E.suiteName,...E.testType===void 0?{}:{testType:E.testType}}},dataDirectory:v.dataDirectory,file:v.module.file,fixtures:i.fixtures,...i.setupData===void 0?{}:{setupData:i.setupData},taskId:E.id})),version:1}}function ke(e,t){return{...Ve({engineId:"node-vitest",installationKind:t.driverRequest?.managed===null?"repository-package":"root-direct-packages",profile:e.profile}),directPackages:{vitest:t.vitest.version}}}function Q(e){return{...j(e,"NODE_VITEST_PREPARE_FAILED"),classification:e instanceof $?"product":"infra"}}async function Z(e){try{return await e()}catch(t){throw new $(t)}}
|
|
1
|
+
import{makeTempDirAsync as U}from"../../../process/files/temp-dir.js";import{toError as B}from"../../../kernel/error-serialization.js";import{isDeepStrictEqual as ee}from"node:util";import{classifyManagedProcessResult as te,engineExecutionResult as re,terminalOutcome as P,emptyEvidence as L,failureFromError as j,firstOutputLine as K,processExitDetails as ne,processExitFailure as W,taskBeforeStartOutcome as ie,managedProcessFailure as z,taskTimeoutFailure as oe}from"../../adapters/execution.js";import{requireRootDirectory as se,requireAbsoluteWorkspacePath as H,requireConsumerFile as ae,safeSegment as J}from"../../adapters/workspace.js";import w from"node:fs/promises";import I from"node:path";import{pathToFileURL as ce}from"node:url";import{isEngineTaskDeadlineExceededReason as de,resolveEngineTaskInputs as le}from"../../api/index.js";import{createInvocationProcessEnvironment as ue,runManagedProcess as pe}from"../../../process/index.js";import{managedVitestAliases as me}from"../../installations/node-vitest.js";import{decodeVitestSelection as fe,resolveRepositoryVitest as Ee}from"../selection.js";import{collectNodeVitestEvidence as ve,readNodeVitestRunReport as Te}from"./evidence.js";import{assertNodeVitestSpawnEnvironmentBudget as X,NODE_VITEST_RUNTIME_INPUT_FILE_ENV as Ie,NODE_VITEST_RUNTIME_INPUT_SHA256_ENV as ye,selectNodeVitestForwardedEnvironment as De,splitManagedNodeVitestEnvironment as he,verifyOwnedNodeVitestRuntimeInput as we,writeOwnedNodeVitestRuntimeInput as Ne}from"../runtime/runtime-input.js";import{NODE_VITEST_DRIVER_FILE as Y,writeNodeVitestWorkspace as ge}from"./workspace.js";import{nodeVitestReportOptions as _e}from"./reporter.js";import{engineEvidenceIdentity as Ve}from"../../adapters/evidence-identity.js";import{getNodeStartupCacheEnvironment as Oe}from"../../adapters/node-startup-cache.js";const g={codePrefix:"NODE_VITEST",label:"Node/Vitest"},G="Unhandled Reporter Error";class $ extends Error{constructor(t){const r=B(t);super(r.message,{cause:r}),this.name="NodeVitestConsumerPreparationError"}}export const nodeVitestExecutorDependencies={removeOwnedDirectory:e=>w.rm(e,{force:!0,recursive:!0}),writeRuntimeInput:Ne,writeWorkspace:ge};export async function executeNodeVitestEngine(e,t,r,m=nodeVitestExecutorDependencies){const l=await Re(e,t,m,r),o=Date.now(),s=l.tasks.map(c=>{e.observer?.taskStarted(c.task);const f=c.preparationFailure;return ie(c.task,e.signal,o,g)??(f===null?null:P(c.task.id,"failed",o,L(),f))}),p=l.tasks.filter((c,f)=>s[f]===null),i=[],v=p.length===0?[]:await Se(e,l,p,o,i);let E=0;return re(e,s.map(c=>c??v[E++]),ke(e,l),i)}async function Re(e,t,r,m){const l=e.profile.id,o=await se(e.workspace.rootDirectory);H(e.workspace.temporaryDirectory,"temporary directory"),H(e.workspace.artifactDirectory,"artifact directory"),await w.mkdir(e.workspace.temporaryDirectory,{recursive:!0}),await w.mkdir(e.workspace.artifactDirectory,{recursive:!0});const s=await w.realpath(e.workspace.temporaryDirectory),p=await w.realpath(e.workspace.artifactDirectory),i=await U(`node-vitest-${J(e.invocationId)}-`,{parentDir:s});m.defer(()=>r.removeOwnedDirectory(i));const v=I.join(i,"reports");await w.mkdir(v,{mode:448});const E=await ue(i);let c=[];for(const[n,u]of e.tasks.entries()){const a=le(e,u),N=`${n.toString().padStart(3,"0")}-${J(u.id)}`,y=await U(`${N}-`,{parentDir:p});try{const x=await Z(()=>ae(o,u.file,"Node/Vitest task file")),C=I.join(v,`${N}.json`),M={file:x,reportFile:C,sourceFile:u.file,taskId:u.id},A=I.join(y,"data");c.push({artifactDirectory:y,inputs:a,task:u,preparationFailure:null,prepared:{dataDirectory:A,module:M}})}catch(x){const C=Q(x);c.push({artifactDirectory:y,inputs:a,task:u,preparationFailure:C,prepared:null})}}const f=c.filter(n=>n.prepared!==null),d=I.join(i,"setup.mjs"),T=I.join(i,"input-bootstrap.mjs"),h=I.join(i,"driver-request.json"),R=I.join(v,"run.json"),_=I.join(i,"startup-failure.txt");let V=null,S=null,D=t.vitest;const F=`--import=${JSON.stringify(ce(T).href)}`;let O={...E.environment,FORCE_COLOR:"0",NODE_OPTIONS:F,NO_COLOR:"1"};if(f.length>0)try{xe(f);const{configuration:n}=f[0].inputs,u=n===void 0?null:fe(n),a=u!==null&&u.runner!=="managed"?u:null;a!==null&&(D=Ee(o));const N=Object.keys(f[0].inputs.supportModules);if(N.length>0)throw new $(new Error(`Node/Vitest engine does not support support module(s): ${N.join(", ")}`));let y=await Z(()=>Ce(e,l,f,a?.host??null));if(a===null){const k=he(y.environment);y={...y,environment:k.overlay},O={...O,...k.process}}else{const k=b=>process.platform==="win32"?b.toUpperCase():b,q=new Set(Object.keys(y.environment).map(k));O={...Object.fromEntries(Object.entries(a.host).filter(([b])=>!q.has(k(b)))),...y.environment,NODE_OPTIONS:[F,a.host.NODE_OPTIONS].join(" ").trim()}}V=await r.writeRuntimeInput(i,y);const{file:x,sha256:C,sizeBytes:M}=V;await r.writeWorkspace({inputBootstrapFile:T,runtimeInput:{expectedFile:x,expectedInvocationId:e.invocationId,expectedSha256:C,expectedSizeBytes:M,startupFailure:{driverFile:Y,file:_,vitestWorkersDirectory:I.join(D.root,"dist","workers")}},setupFile:d,vitestModule:D.index});const A={cacheDirectory:I.join(i,"cache"),invocationId:e.invocationId,runReportFile:R,setupFile:d,startupFailureFile:_,vitest:{entrypoint:D.entrypoint,name:"vitest",version:D.version}};S=a===null?{...A,aliases:me(t),config:!1,managed:{externalModules:[t.authoringEntrypoints.data.replaceAll(I.sep,"/")]},project:null,test:{disableConsoleIntercept:!0,environment:"node",fileParallelism:!1,isolate:!0,maxWorkers:1,pool:"forks"}}:{...A,aliases:{},config:a.config,managed:null,project:a.project,test:{}}}catch(n){const u=Q(n);c=c.map(a=>a.prepared===null?a:{...a,preparationFailure:u,prepared:null})}return Object.freeze({driverRequest:S,driverRequestFile:h,installation:t,processEnvironment:O,profileId:l,rootDirectory:o,runReportFile:R,runtimeInput:V,startupFailureFile:_,tasks:Object.freeze(c),vitest:D})}async function Se(e,t,r,m,l){const o=n=>r.map(({task:u})=>P(u.id,"failed",m,L(),n(u.timeoutMs)));try{if(t.runtimeInput===null||t.driverRequest===null)throw new Error("Node/Vitest run was not prepared");await we(t.runtimeInput)}catch(n){return o(()=>({...j(n,"NODE_VITEST_INPUT_INVALID"),classification:"infra"}))}const s=await Fe(e,t,t.runtimeInput,r[0]),p=Math.min(...r.map(({task:n})=>n.deadlineMs))-Date.now();if(p<=0)return o(n=>oe(n,g));const i=t.driverRequest.managed===null?{}:{hookTimeout:p,teardownTimeout:p,testTimeout:p},v=r.map(({prepared:n})=>n.module),E={...t.driverRequest,modules:v,test:{...t.driverRequest.test,...i}};await w.writeFile(t.driverRequestFile,JSON.stringify(E),{flag:"wx",mode:384});const c=await pe({args:[Y,t.driverRequestFile],cwd:t.rootDirectory,displayLabel:"Node/Vitest",env:s,executable:process.execPath,signal:e.signal,timeoutMs:p}),f=await w.readFile(t.startupFailureFile,"utf8").catch(()=>null),d=await Te(e.invocationId,t.runReportFile),T=d instanceof Error?[]:d.unhandledErrors.filter(({type:n})=>n!==G),h=[];for(const{artifactDirectory:n,prepared:u}of r)h.push(await ve({identity:_e(e.invocationId,u.module).identity,artifactDirectory:n,dataDirectory:u.dataDirectory,reportFile:u.module.reportFile,rootDirectory:t.rootDirectory,stderr:c.stderr,stdout:c.stdout,unhandledErrors:T.filter(({testPath:a})=>a===void 0||I.resolve(a)===I.resolve(u.module.file))}).catch(a=>nodeVitestEvidenceCollectionFailure(c,a)));const R=h.map(n=>"reportSummary"in n?n.reportSummary:null),_=de(e.signal.reason),V=c.cancelled||c.timedOut||_,S=V&&d instanceof Error&&R.includes(null),D=T.length>0||R.some(n=>n?.success===!1||n?.numTotalTests===0),F=d instanceof Error?0:c.exitCode??d.exitCode,O=d instanceof Error?S?{exitCode:0,unhandledErrors:[]}:d:{...d,exitCode:F===1&&(D||d.afterTests!==void 0)?0:F};return!(d instanceof Error)&&d.afterTests!==void 0&&l.push(`Vitest failed the run after its tests ${D?"finished":"passed"}: ${d.afterTests.join("; ")}`),r.map(({task:n},u)=>{const a=h[u];if(f!==null&&!V)return P(n.id,"failed",m,"reportSummary"in a?a.evidence:L(),{classification:"infra",code:"NODE_VITEST_START_FAILED",message:`Vitest could not start: ${f}`});if(!("reportSummary"in a))return P(n.id,"failed",m,L(),a);const{failure:N,status:y}=classifyNodeVitestTaskResult(c,O,a,n.timeoutMs,_,S);return P(n.id,y,m,a.evidence,N)})}export function classifyNodeVitestTaskResult(e,t,r,m,l,o=!1){const s=d=>({failure:d,status:"failed"}),p=z(e,g);if(p!==null)return s(p);const i=r.reportSummary,v=i===null||!o?te(e,m,l,g):null;if(v!==null)return v;const E=t instanceof Error?void 0:t.unhandledErrors.find(({type:d})=>d===G),c=t instanceof Error?void 0:t.failure??(E&&{...E,phase:"reporter"});if(c!==void 0){const{message:d,phase:T,stack:h}=c;return{failure:{classification:"infra",code:"NODE_VITEST_RUN_FAILED",details:{phase:T},message:`Vitest ${T} failed: ${d}`,...h===void 0?{}:{stack:h}},status:T==="config"?"not_run":"failed"}}if(r.environmentFailure!==null)return s(r.environmentFailure);if(i===null||t instanceof Error){const d=K(e.stderr)??K(e.stdout),T=e.exitCode===0?null:W(e,g,d,"Vitest").failure;return s({...j(r.reportError??t,"NODE_VITEST_REPORT_INVALID"),...T===null?{}:{details:T.details,message:T.message},classification:"infra"})}const f=o?null:e.exitSignal;if(t.exitCode!==0||f!==null){const d=f===null?`exited with code ${t.exitCode.toString()}`:`was terminated by ${f}`;return s({classification:"infra",code:"NODE_VITEST_EXIT_CODE",details:ne(e),message:`Vitest ${d}, which its test results do not explain`})}return i.numTotalTests===0&&r.reportErrorCount===0?s({code:"NODE_VITEST_NO_TESTS",message:"Vitest did not discover any tests for the task file"}):r.dataEvidenceError!==null?s(j(r.dataEvidenceError,"NODE_VITEST_DATA_EVIDENCE_INVALID")):!i.success||i.numFailedTests>0||r.reportErrorCount>0?W(e,g,r.reportFailureMessage,"Vitest"):i.numPassedTests===0&&(i.numPendingTests>0||i.numTodoTests>0)?{failure:null,status:"skipped"}:i.numPassedTests===0?s({code:"NODE_VITEST_REPORT_FAILED",message:"Vitest report did not contain a completed passing test"}):{failure:null,status:"passed"}}export function nodeVitestEvidenceCollectionFailure(e,t){const r=B(t);return z(e,g,r)??{...j(r,"NODE_VITEST_EVIDENCE_FAILED"),classification:"infra"}}async function Fe(e,t,r,{inputs:m,task:l}){const o=e.target.baseUrl,s={...t.processEnvironment,...o===null?{}:{BASE_URL:o},TESTKIT_ACTIVE:"1",[Ie]:r.file,[ye]:r.sha256,TESTKIT_INVOCATION_ID:e.invocationId,TESTKIT_LEASE_DIR:l.orchestration.lease.directory,TESTKIT_LEASE_ID:l.orchestration.lease.id,TESTKIT_MODE:l.orchestration.mode,...l.orchestration.runtimeId===void 0?{}:{TESTKIT_RUNTIME_ID:l.orchestration.runtimeId}},p=t.driverRequest?.managed!==null;p&&X(s);const i={...t.processEnvironment,...m.environment};Object.assign(s,await Oe(e.workspace.cacheDirectory,i));try{p&&X(s)}catch{delete s.NODE_COMPILE_CACHE}return s}function xe(e){const t=({prepared:o})=>o.module.file,r=e.find((o,s)=>e.findIndex(p=>t(p)===t(o))!==s);if(r!==void 0)throw new Error(`Node/Vitest task ${r.task.id} selects a file that another task in the request selects`);const m=({inputs:o,task:s})=>[o.environment,o.supportModules,s.orchestration,s.timeoutMs],l=e.find(o=>!ee(m(o),m(e[0])));if(l!==void 0)throw new Error(`Node/Vitest task ${l.task.id} needs a different environment, support modules, orchestration or timeout than ${e[0].task.id}`)}function Ce(e,t,r,m){const{baseUrl:l,databases:o,endpoints:s,handles:p}=e.target;return{...m===null?{}:{host:m},baseUrl:l,databases:o,endpoints:s,engineId:"node-vitest",environment:De(r[0].inputs.environment),handles:p,invocationId:e.invocationId,profileId:t,tasks:r.map(({inputs:i,prepared:v,task:E})=>({context:{baseUrl:l,engineId:"node-vitest",invocationId:e.invocationId,profileId:t,serviceName:E.serviceName,task:{file:E.file,id:E.id,suiteName:E.suiteName,...E.testType===void 0?{}:{testType:E.testType}}},dataDirectory:v.dataDirectory,file:v.module.file,fixtures:i.fixtures,...i.setupData===void 0?{}:{setupData:i.setupData},taskId:E.id})),version:1}}function ke(e,t){return{...Ve({engineId:"node-vitest",installationKind:t.driverRequest?.managed===null?"repository-package":"root-direct-packages",profile:e.profile}),directPackages:{vitest:t.vitest.version}}}function Q(e){return{...j(e,"NODE_VITEST_PREPARE_FAILED"),classification:e instanceof $?"product":"infra"}}async function Z(e){try{return await e()}catch(t){throw new $(t)}}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{existsSync as w,realpathSync as d}from"node:fs";import
|
|
2
|
-
`),process.exitCode=1}finally{w(
|
|
1
|
+
import{existsSync as w,realpathSync as d}from"node:fs";import R from"node:fs/promises";import T from"node:path";import{inspect as j}from"node:util";import{toError as g}from"../../../kernel/error-serialization.js";import{isUnknownRecord as $}from"../../../kernel/unknown-values.js";import{writeNativeReport as k}from"../../adapters/native-report.js";import{importResolvedPackageEntrypoint as V}from"../../installations/package-resolution.js";import{nodeVitestReportOptions as N,reportedError as u,TestkitVitestReporter as C}from"./reporter.js";const o=JSON.parse(await R.readFile(process.argv[2]??"","utf8")),v=new Map(o.modules.map(e=>[d(e.file),e])),O=N(o.invocationId,{reportFile:o.runReportFile,sourceFile:".",taskId:"run"}),p={unhandledErrors:[]},P=16*1024*1024;let h;const b=setInterval(()=>{w(o.startupFailureFile)&&process.exit(1)},100);b.unref();let m;process.env.TEST="true",process.env.VITEST="true",process.env.NODE_ENV??="test";try{const{createVitest:e}=await V(o.vitest),t={cacheDir:o.cacheDirectory,resolve:{alias:Object.entries(o.aliases).map(([r,i])=>({find:S(r),replacement:i})),...o.managed===null?{}:{tsconfigPaths:!0}},test:{...o.test,...o.managed===null?{}:{server:{deps:{external:o.managed.externalModules.map(S)}}}}},s=await e("test",{...{allowOnly:!1,includeTaskLocation:!0,passWithNoTests:!1,update:"none"},config:o.config??void 0,watch:!1},t);h=s,s.config.snapshotOptions.updateSnapshot="none",s.snapshot.options.updateSnapshot="none";for(const r of s.projects)r.config.setupFiles.push(o.setupFile);const n=await I(s),a=n.map(({moduleId:r,project:i})=>({...y(r),project:i.name})),{reporters:f}=s;o.managed!==null&&(f.length=0),f.unshift(new C(o.invocationId,a)),m=_(s);const l=s;await(l.standalone===void 0?l.init():l.standalone()),p.unhandledErrors=(await s.runTestSpecifications(n,!1)).unhandledErrors.map(M),m.testsFinished()}catch(e){p.failure={phase:h===void 0?"config":"run",...u(g(e))},process.stderr.write(`${j(e)}
|
|
2
|
+
`),process.exitCode=1}finally{w(o.startupFailureFile)&&process.exit(1),await h?.close().catch(e=>{p.failure??={phase:"close",...u(g(e))}})}const E=m?.finish(p.failure===void 0)??[];E.length>0&&(p.afterTests=E),await k(O,{...p,exitCode:process.exitCode??0},P);async function I(e){if(o.config===!1)return o.modules.map(({file:s})=>{const n=s.split(T.sep).join("/");return e.getProjectByName("").createSpecification(n)});const t=(await e.globTestSpecifications()).filter(({moduleId:s,project:n})=>v.has(d(s))&&(o.project===null||n.name===o.project)),c=o.modules.find(({file:s})=>!t.some(({moduleId:n})=>y(n).file===s));if(c!==void 0){const s=c.sourceFile.split(T.sep).join("/");throw new Error(`Vitest's configuration no longer selects ${s}, which Testkit listed earlier`)}return t}function _(e){const t={close:0,coverage:0,reporters:0,tests:0},c={close:[],coverage:[]};let s=null;const n=e.logger.error.bind(e.logger);e.logger.error=(...r)=>{s!==null&&c[s].push(r.map(String).join(" ")),n(...r)};const a=()=>{const r=Number(process.exitCode??0);return process.exitCode=void 0,r},f=e,l=f.report.bind(e);return f.report=async(r,...i)=>{if(r!=="onTestRunEnd")return l(r,...i);t.tests=a(),await l(r,...i),t.reporters=a(),s="coverage"},{testsFinished(){t.coverage=a(),s="close"},finish(r){if(t.close=a(),s=null,process.exitCode=[t.close,t.coverage,t.reporters,t.tests].find(F=>F!==0)??0,!r)return[];const i=[];return t.reporters!==0&&i.push(`a reporter set exit code ${t.reporters.toString()}`),t.tests===0&&t.coverage!==0&&i.push(...x(c.coverage,`exit code ${t.coverage.toString()}`)),t.close!==0&&i.push(...x(c.close,`exit code ${t.close.toString()} while Vitest closed`)),i}}}function x(e,t){return e.length>0?e:[t]}function y(e){const t=v.get(d(e));if(t===void 0)throw new Error(`Vitest selected ${e}, which Testkit did not request`);return t}function M(e){if(!$(e)||typeof e.message!="string")return u(g(e));const{name:t,stack:c,type:s,VITEST_TEST_PATH:n}=e;return{...u({message:e.message,...typeof t=="string"?{name:t}:{},...typeof c=="string"?{stack:c}:{}}),...typeof s=="string"?{type:s}:{},...typeof n=="string"?{testPath:n}:{}}}function S(e){return new RegExp(`^${e.replace(/[.*+?^${}()|[\]\\]/gu,"\\$&")}$`,"u")}
|