@gfdelarue/testkit 0.6.4 → 0.6.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +297 -3
  2. package/dist/app/bugs/batch.js +1 -1
  3. package/dist/app/bugs/bisect-dependencies.js +1 -0
  4. package/dist/app/bugs/bisect-identity.js +1 -0
  5. package/dist/app/bugs/bisect-report.js +1 -0
  6. package/dist/app/bugs/bisect-search.js +1 -0
  7. package/dist/app/bugs/bisect.js +2 -0
  8. package/dist/app/bugs/draft.js +3 -3
  9. package/dist/app/bugs/facts.js +2 -2
  10. package/dist/app/bugs/file.js +1 -1
  11. package/dist/app/bugs/issue.js +4 -4
  12. package/dist/app/bugs/record.js +2 -2
  13. package/dist/app/campaigns/brief.js +5 -5
  14. package/dist/app/prove/changes.js +3 -0
  15. package/dist/app/prove/index.js +2 -0
  16. package/dist/app/prove/mutants.js +3 -0
  17. package/dist/app/prove/report.js +3 -0
  18. package/dist/app/prove/verdict.js +1 -0
  19. package/dist/app/run/engine-orchestration.js +1 -1
  20. package/dist/app/run/orchestrator.js +1 -1
  21. package/dist/app/run/request.js +1 -1
  22. package/dist/app/run/time-zone.js +1 -0
  23. package/dist/app/sandbox/index.js +1 -0
  24. package/dist/app/sandbox/run.js +1 -0
  25. package/dist/app/shake/conditions.js +1 -0
  26. package/dist/app/shake/index.js +4 -0
  27. package/dist/app/shake/report.js +6 -0
  28. package/dist/app/shake/verdicts.js +1 -0
  29. package/dist/cli/command-catalog.js +1 -1
  30. package/dist/cli/command-flags.js +1 -1
  31. package/dist/cli/command-registry.js +1 -1
  32. package/dist/cli/commands/bug/bisect.js +1 -0
  33. package/dist/cli/commands/bug/file.js +3 -3
  34. package/dist/cli/commands/bug/record.js +2 -2
  35. package/dist/cli/commands/prove.js +3 -0
  36. package/dist/cli/commands/run.js +2 -2
  37. package/dist/cli/commands/shake.js +1 -0
  38. package/dist/domain/file-timeout.js +1 -1
  39. package/dist/engines/contracts/fixture-manifest.js +1 -1
  40. package/dist/engines/node-vitest/adapter/adapter.js +1 -1
  41. package/dist/engines/node-vitest/adapter/driver.js +2 -2
  42. package/dist/engines/node-vitest/adapter/evidence.js +2 -2
  43. package/dist/engines/node-vitest/adapter/reporter.js +1 -1
  44. package/dist/engines/node-vitest/runtime/data/scenario.js +2 -2
  45. package/dist/engines/node-vitest/selection.js +1 -1
  46. package/dist/engines/playwright/adapter/config.js +20 -5
  47. package/dist/process/environment.js +1 -1
  48. package/dist/process/execa-bundle.json +1 -1
  49. package/dist/repo/history.js +5 -5
  50. package/dist/types/workspace.d.ts +2 -0
  51. package/docs/bug-records.md +32 -23
  52. package/docs/release-notes.md +64 -0
  53. package/package.json +17 -2
@@ -1,2 +1,2 @@
1
- import y from"node:fs/promises";import w from"node:path";import{evidenceFileAccess as O,readStableEvidenceFile as x,writeOwnedEvidenceFile as $}from"../../adapters/evidence-files.js";import{NODE_VITEST_DATA_FAILURE_DETAILS_ARTIFACT_KIND as X,nodeVitestDataScenarioFile as Y}from"../runtime/runtime-input.js";import{dataFlowArtifacts as z}from"../runtime/data/report.js";import{readVitestReportCases as U,sharedVitestTitles as K,vitestErrorDetails as q,vitestReportErrors as D,vitestTestFailureDetails as G}from"./failure-details.js";import{parseEnvironmentValueMissingFailure as H}from"../../contracts/environment-values.js";import{readNativeReport as k,requireNativeEvidenceJson as Q}from"../../adapters/native-report.js";import{isUnknownRecord as g}from"../../../kernel/unknown-values.js";import{toError as N}from"../../../kernel/error-serialization.js";import{isPathWithin as Z}from"../../../process/files/paths.js";import{firstOutputLine as tt}from"../../adapters/execution.js";import{nodeVitestReportOptions as et}from"./reporter.js";const rt=16*1024*1024,j=1024;export const NODE_VITEST_DATA_ARTIFACT_MAX_BYTES=16*1024*1024,NODE_VITEST_DATA_EVIDENCE_MAX_BYTES=64*1024*1024;const nt=".testkit-owned-evidence";export async function collectNodeVitestEvidence(t,e=O){await y.mkdir(t.artifactDirectory,{recursive:!0});const r=await y.realpath(t.artifactDirectory),s=w.join(r,nt);await y.mkdir(s,{mode:448,recursive:!1});const n=[],a=[];t.stdout.length>0&&(a.push({stream:"stdout",text:t.stdout}),n.push(await R(r,"stdout.log","node-vitest-stdout",t.stdout))),t.stderr.length>0&&(a.push({stream:"stderr",text:t.stderr}),n.push(await R(r,"stderr.log","node-vitest-stderr",t.stderr)));const o=await st(t,s,e);n.push(...o.artifacts);const u=await at(t,r,s,o.cases,e);return n.push(...u.artifacts),{dataEvidenceError:u.error,environmentFailure:o.environmentFailure,evidence:{artifacts:n,logs:a,metrics:[],...o.summary===null?{}:{native:{vitest:o.summary}}},reportError:o.error,reportErrorCount:o.errorCount,reportFailureMessage:o.failureMessage,reportSummary:o.summary}}async function st({identity:t,reportFile:e,rootDirectory:r,unhandledErrors:s=[]},n,a){try{const o=await b(e,a);if(!o.exists||o.contents===null)throw new Error("Vitest did not produce its Testkit native report");const u=k(o.value,t).result;if(!g(u)||!g(u.summary))throw new Error("Vitest native report must contain result and summary objects");const f=ct(u.summary),m=U(u.cases,f),l=G(u.failures,r,K(m));if(f.numFailedTests!==l.length)throw new Error("Vitest native report failure count does not match its summary");const h=D(u.errors);if(f.success&&h.length>0)throw new Error("Vitest native report success conflicts with runtime errors");const V=[...h,...s],E=H(u.environmentFailure,"Vitest native report environment failure"),i=await $(n,"testkit-vitest-report.json",E===null?o.contents:Buffer.from(it(o.value))),c=await A(i,"application/json","engine.testkit-vitest-report","testkit-vitest-report","Testkit native Vitest report"),T=await ot(n,l,q(V)),p=[c,...T===null?[]:[T]];if(u.pending!==void 0){const v=new Error("Vitest did not finish the file in every project that selects it");return{artifacts:p,cases:m,environmentFailure:null,error:v,errorCount:0,failureMessage:null,summary:null}}return{artifacts:p,cases:m,environmentFailure:E,error:null,errorCount:V.length,failureMessage:typeof l[0]?.message=="string"?l[0].message:V.find(v=>v.message.trim().length>0)?.message??null,summary:f}}catch(o){return{artifacts:[],cases:[],environmentFailure:null,error:N(o),errorCount:0,failureMessage:null,summary:null}}}function it(t){if(!g(t)||!g(t.result))throw new Error("Vitest native report must contain result and summary objects");const e=Object.fromEntries(Object.entries(t.result).filter(([r])=>r!=="environmentFailure"));return JSON.stringify({...t,result:e})}async function ot(t,e,r){if(e.length+r.length===0)return null;const s={failures:[...e,...r],phase:"exec"},n=await $(t,"vitest-failure-details.json",Buffer.from(`${JSON.stringify(s,null,2)}
2
- `,"utf8"));return{...await A(n,"application/json","testkit.failure-details","vitest-failure-details",[...e.length===0?[]:[I(e.length,"failed Vitest test")],...r.length===0?[]:[`${I(r.length,"Vitest error")} outside a test`]].join(" and ")),data:s}}function I(t,e){return`${t.toString()} ${e}${t===1?"":"s"}`}async function at({dataDirectory:t,identity:e},r,s,n,a){const o=w.dirname(Y(t,"")),u=[];try{const m=(await y.readdir(o).catch(i=>{if(J(i)&&i.code==="ENOENT")return[];throw i})).filter(i=>i.endsWith(".json")).sort();if(m.length>j)throw new Error(`Data evidence exceeds its ${j.toString()} test limit`);let l=0;const h=[];for(const i of m){const c=`Data scenario ${i}`,T=await mt(r,w.join(o,i),c),{contents:p}=await x(T,NODE_VITEST_DATA_ARTIFACT_MAX_BYTES,c,a);if(l+=p.byteLength,l>NODE_VITEST_DATA_EVIDENCE_MAX_BYTES)throw new Error(`Data evidence exceeds its ${NODE_VITEST_DATA_EVIDENCE_MAX_BYTES.toString()} byte aggregate limit`);h.push({contents:p,label:c,name:i})}const E=h.flatMap(({contents:i,label:c,name:T})=>{const p=F(C(i,c),c),v=d(p.attempt,`${c}.attempt`),_=d(p.repeat,`${c}.repeat`),B=d(p.repeats,`${c}.repeats`),L=ut(p.scenario,`${c}.scenario`),S=n.find(({id:W})=>W===decodeURIComponent(T.slice(0,-5)));return S!==void 0&&S.state!=="skipped"&&S.attempts===v&&S.repeat===_&&_===B?[{index:n.indexOf(S),scenario:L}]:[]}).sort((i,c)=>i.index-c.index).map(({scenario:i})=>i);for(const i of E.length===0?[]:z(E,e.sourceFile,e.taskId)){const c=i.contentType==="text/markdown"?".md":".json",T=await $(s,`${i.name}${c}`,Buffer.from(i.contents,"utf8"));u.push({...await A(T,i.contentType,i.kind,i.name,i.summary),...i.kind===X?{data:F(JSON.parse(i.contents),i.name)}:{}})}}catch(f){return{artifacts:u,error:N(f)}}return{artifacts:u,error:null}}function ut(t,e){const r=F(t,e);if(M(r.name,`${e}.name`),M(F(r.location,`${e}.location`).path,`${e}.location.path`),r.status!=="failed"&&r.status!=="passed")throw new Error(`${e}.status must be failed or passed`);const n=["budgetFailures","budgets","events","evidence","findings","metrics","sequences"].find(a=>!Array.isArray(r[a]));if(n!==void 0)throw new Error(`${e}.${n} must be an array`);return r}async function R(t,e,r,s){const n=w.join(t,e);return await y.writeFile(n,s,"utf8"),A(n,"text/plain; charset=utf-8","engine.output",r,tt(s)??"captured output")}async function A(t,e,r,s,n){const a=await y.stat(t);if(!a.isFile())throw new Error(`${s} artifact is not a regular file`);return{contentType:e,kind:r,metadata:{sizeBytes:a.size},name:s,path:t,summary:n}}async function b(t,e){let r;try{({contents:r}=await x(t,rt,t,e))}catch(n){if(J(n)&&n.code==="ENOENT")return{contents:null,exists:!1,value:null};throw n}const s=C(r,t);return{contents:r,exists:!0,value:s}}function C(t,e){const r=JSON.parse(t.toString("utf8"));return Q(r,e),r}function ct(t){const e={numFailedTests:d(t.numFailedTests,"Vitest native report.summary.numFailedTests"),numPassedTests:d(t.numPassedTests,"Vitest native report.summary.numPassedTests"),numPendingTests:d(t.numPendingTests,"Vitest native report.summary.numPendingTests"),numTodoTests:d(t.numTodoTests,"Vitest native report.summary.numTodoTests"),numTotalTests:d(t.numTotalTests,"Vitest native report.summary.numTotalTests"),numFailedTestSuites:d(t.numFailedTestSuites,"Vitest native report.summary.numFailedTestSuites"),numPassedTestSuites:d(t.numPassedTestSuites,"Vitest native report.summary.numPassedTestSuites"),numPendingTestSuites:d(t.numPendingTestSuites,"Vitest native report.summary.numPendingTestSuites"),numTotalTestSuites:d(t.numTotalTestSuites,"Vitest native report.summary.numTotalTestSuites"),success:dt(t.success,"Vitest native report.summary.success")};if(e.numTotalTests!==e.numFailedTests+e.numPassedTests+e.numPendingTests+e.numTodoTests||e.numTotalTestSuites!==e.numFailedTestSuites+e.numPassedTestSuites+e.numPendingTestSuites||e.success&&(e.numFailedTests>0||e.numFailedTestSuites>0||e.numTotalTestSuites===0))throw new Error("Vitest native report summary contains inconsistent counts or success");return e}async function mt(t,e,r){const s=w.isAbsolute(e)?e:w.resolve(t,e),n=await y.lstat(s);if(!n.isFile()||n.isSymbolicLink())throw new Error(`${r} must be a regular non-symbolic-link file`);const a=await y.realpath(s);if(!Z(t,a))throw new Error(`${r} must stay within the task artifact directory`);return a}function F(t,e){if(!g(t))throw new Error(`${e} must contain an object`);const r={};for(const[s,n]of Object.entries(t))r[s]=P(n,`${e}.${s}`);return r}function P(t,e){if(t===null||typeof t=="boolean"||typeof t=="string")return t;if(typeof t=="number"){if(!Number.isFinite(t))throw new Error(`${e} must be finite`);return t}return Array.isArray(t)?t.map((r,s)=>P(r,`${e}[${s.toString()}]`)):F(t,e)}function M(t,e){if(typeof t!="string"||t.trim().length===0)throw new Error(`${e} must be a non-empty string`);return t}function d(t,e){if(typeof t!="number"||!Number.isSafeInteger(t)||t<0)throw new Error(`${e} must be a non-negative safe integer`);return t}function dt(t,e){if(typeof t!="boolean")throw new Error(`${e} must be boolean`);return t}function J(t){return t instanceof Error}export async function readNodeVitestRunReport(t,e,r=O){try{const{identity:s}=et(t,{reportFile:e,sourceFile:".",taskId:"run"}),{result:n}=k((await b(e,r)).value,s);if(!g(n)||!Number.isSafeInteger(n.exitCode)||!Array.isArray(n.unhandledErrors))throw new Error("Vitest run report must contain an exit code and unhandled errors");const a=n.unhandledErrors,o=n.failure===void 0?[]:[n.failure];D([...a,...o],"Vitest run report errors");const u=m=>g(m)&&[m.testPath,m.type].every(l=>l===void 0||typeof l=="string"),f=n.afterTests===void 0?[]:n.afterTests;if(!Array.isArray(f)||!f.every(m=>typeof m=="string")||!a.every(u)||!o.every(m=>g(m)&&["close","config","run"].includes(m.phase)))throw new Error("Vitest run report errors are malformed");return n}catch(s){return N(s)}}
1
+ import y from"node:fs/promises";import w from"node:path";import{evidenceFileAccess as O,readStableEvidenceFile as x,writeOwnedEvidenceFile as $}from"../../adapters/evidence-files.js";import{NODE_VITEST_DATA_FAILURE_DETAILS_ARTIFACT_KIND as X,nodeVitestDataScenarioFile as Y}from"../runtime/runtime-input.js";import{dataFlowArtifacts as z}from"../runtime/data/report.js";import{readVitestReportCases as U,sharedVitestTitles as K,vitestErrorDetails as q,vitestReportErrors as D,vitestTestFailureDetails as G}from"./failure-details.js";import{parseEnvironmentValueMissingFailure as H}from"../../contracts/environment-values.js";import{readNativeReport as j,requireNativeEvidenceJson as Q}from"../../adapters/native-report.js";import{isUnknownRecord as g}from"../../../kernel/unknown-values.js";import{toError as N}from"../../../kernel/error-serialization.js";import{isPathWithin as Z}from"../../../process/files/paths.js";import{firstOutputLine as tt}from"../../adapters/execution.js";import{nodeVitestReportOptions as et}from"./reporter.js";const rt=16*1024*1024,k=1024;export const NODE_VITEST_DATA_ARTIFACT_MAX_BYTES=16*1024*1024,NODE_VITEST_DATA_EVIDENCE_MAX_BYTES=64*1024*1024;const nt=".testkit-owned-evidence";export async function collectNodeVitestEvidence(t,e=O){await y.mkdir(t.artifactDirectory,{recursive:!0});const r=await y.realpath(t.artifactDirectory),s=w.join(r,nt);await y.mkdir(s,{mode:448,recursive:!1});const n=[],a=[];t.stdout.length>0&&(a.push({stream:"stdout",text:t.stdout}),n.push(await R(r,"stdout.log","node-vitest-stdout",t.stdout))),t.stderr.length>0&&(a.push({stream:"stderr",text:t.stderr}),n.push(await R(r,"stderr.log","node-vitest-stderr",t.stderr)));const o=await st(t,s,e);n.push(...o.artifacts);const u=await at(t,r,s,o.cases,e);return n.push(...u.artifacts),{dataEvidenceError:u.error,environmentFailure:o.environmentFailure,evidence:{artifacts:n,logs:a,metrics:[],...o.summary===null?{}:{native:{vitest:o.summary}}},reportError:o.error,reportErrorCount:o.errorCount,reportFailureMessage:o.failureMessage,reportSummary:o.summary}}async function st({identity:t,reportFile:e,rootDirectory:r,unhandledErrors:s=[]},n,a){try{const o=await b(e,a);if(!o.exists||o.contents===null)throw new Error("Vitest did not produce its Testkit native report");const u=j(o.value,t).result;if(!g(u)||!g(u.summary))throw new Error("Vitest native report must contain result and summary objects");const f=ct(u.summary),m=U(u.cases,f),l=G(u.failures,r,K(m));if(f.numFailedTests!==l.length)throw new Error("Vitest native report failure count does not match its summary");const h=D(u.errors);if(f.success&&h.length>0)throw new Error("Vitest native report success conflicts with runtime errors");const V=[...h,...s],E=H(u.environmentFailure,"Vitest native report environment failure"),i=await $(n,"testkit-vitest-report.json",E===null?o.contents:Buffer.from(it(o.value))),c=await A(i,"application/json","engine.testkit-vitest-report","testkit-vitest-report","Testkit native Vitest report"),T=await ot(n,l,q(V)),p=[c,...T===null?[]:[T]];if(u.pending!==void 0){const v=new Error("Vitest did not finish the file in every project that selects it");return{artifacts:p,cases:m,environmentFailure:null,error:v,errorCount:0,failureMessage:null,summary:null}}return{artifacts:p,cases:m,environmentFailure:E,error:null,errorCount:V.length,failureMessage:typeof l[0]?.message=="string"?l[0].message:V.find(v=>v.message.trim().length>0)?.message??null,summary:f}}catch(o){return{artifacts:[],cases:[],environmentFailure:null,error:N(o),errorCount:0,failureMessage:null,summary:null}}}function it(t){if(!g(t)||!g(t.result))throw new Error("Vitest native report must contain result and summary objects");const e=Object.fromEntries(Object.entries(t.result).filter(([r])=>r!=="environmentFailure"));return JSON.stringify({...t,result:e})}async function ot(t,e,r){if(e.length+r.length===0)return null;const s={failures:[...e,...r],phase:"exec"},n=await $(t,"vitest-failure-details.json",Buffer.from(`${JSON.stringify(s,null,2)}
2
+ `,"utf8"));return{...await A(n,"application/json","testkit.failure-details","vitest-failure-details",[...e.length===0?[]:[I(e.length,"failed Vitest test")],...r.length===0?[]:[`${I(r.length,"Vitest error")} outside a test`]].join(" and ")),data:s}}function I(t,e){return`${t.toString()} ${e}${t===1?"":"s"}`}async function at({dataDirectory:t,identity:e},r,s,n,a){const o=w.dirname(Y(t,"")),u=[];try{const m=(await y.readdir(o).catch(i=>{if(J(i)&&i.code==="ENOENT")return[];throw i})).filter(i=>i.endsWith(".json")).sort();if(m.length>k)throw new Error(`Data evidence exceeds its ${k.toString()} test limit`);let l=0;const h=[];for(const i of m){const c=`Data scenario ${i}`,T=await mt(r,w.join(o,i),c),{contents:p}=await x(T,NODE_VITEST_DATA_ARTIFACT_MAX_BYTES,c,a);if(l+=p.byteLength,l>NODE_VITEST_DATA_EVIDENCE_MAX_BYTES)throw new Error(`Data evidence exceeds its ${NODE_VITEST_DATA_EVIDENCE_MAX_BYTES.toString()} byte aggregate limit`);h.push({contents:p,label:c,name:i})}const E=h.flatMap(({contents:i,label:c,name:T})=>{const p=F(C(i,c),c),v=d(p.attempt,`${c}.attempt`),_=d(p.repeat,`${c}.repeat`),B=d(p.repeats,`${c}.repeats`),L=ut(p.scenario,`${c}.scenario`),S=n.find(({id:W})=>W===decodeURIComponent(T.slice(0,-5)));return S!==void 0&&S.state!=="skipped"&&S.attempts===v&&S.repeat===_&&_===B?[{index:n.indexOf(S),scenario:L}]:[]}).sort((i,c)=>i.index-c.index).map(({scenario:i})=>i);for(const i of E.length===0?[]:z(E,e.sourceFile,e.taskId)){const c=i.contentType==="text/markdown"?".md":".json",T=await $(s,`${i.name}${c}`,Buffer.from(i.contents,"utf8"));u.push({...await A(T,i.contentType,i.kind,i.name,i.summary),...i.kind===X?{data:F(JSON.parse(i.contents),i.name)}:{}})}}catch(f){return{artifacts:u,error:N(f)}}return{artifacts:u,error:null}}function ut(t,e){const r=F(t,e);if(M(r.name,`${e}.name`),M(F(r.location,`${e}.location`).path,`${e}.location.path`),r.status!=="failed"&&r.status!=="passed")throw new Error(`${e}.status must be failed or passed`);const n=["budgetFailures","budgets","events","evidence","findings","metrics","sequences"].find(a=>!Array.isArray(r[a]));if(n!==void 0)throw new Error(`${e}.${n} must be an array`);return r}async function R(t,e,r,s){const n=w.join(t,e);return await y.writeFile(n,s,"utf8"),A(n,"text/plain; charset=utf-8","engine.output",r,tt(s)??"captured output")}async function A(t,e,r,s,n){const a=await y.stat(t);if(!a.isFile())throw new Error(`${s} artifact is not a regular file`);return{contentType:e,kind:r,metadata:{sizeBytes:a.size},name:s,path:t,summary:n}}async function b(t,e){let r;try{({contents:r}=await x(t,rt,t,e))}catch(n){if(J(n)&&n.code==="ENOENT")return{contents:null,exists:!1,value:null};throw n}const s=C(r,t);return{contents:r,exists:!0,value:s}}function C(t,e){const r=JSON.parse(t.toString("utf8"));return Q(r,e),r}function ct(t){const e={numFailedTests:d(t.numFailedTests,"Vitest native report.summary.numFailedTests"),numPassedTests:d(t.numPassedTests,"Vitest native report.summary.numPassedTests"),numPendingTests:d(t.numPendingTests,"Vitest native report.summary.numPendingTests"),numTodoTests:d(t.numTodoTests,"Vitest native report.summary.numTodoTests"),numTotalTests:d(t.numTotalTests,"Vitest native report.summary.numTotalTests"),numFailedTestSuites:d(t.numFailedTestSuites,"Vitest native report.summary.numFailedTestSuites"),numPassedTestSuites:d(t.numPassedTestSuites,"Vitest native report.summary.numPassedTestSuites"),numPendingTestSuites:d(t.numPendingTestSuites,"Vitest native report.summary.numPendingTestSuites"),numTotalTestSuites:d(t.numTotalTestSuites,"Vitest native report.summary.numTotalTestSuites"),success:dt(t.success,"Vitest native report.summary.success")};if(e.numTotalTests!==e.numFailedTests+e.numPassedTests+e.numPendingTests+e.numTodoTests||e.numTotalTestSuites!==e.numFailedTestSuites+e.numPassedTestSuites+e.numPendingTestSuites||e.success&&(e.numFailedTests>0||e.numFailedTestSuites>0||e.numTotalTestSuites===0))throw new Error("Vitest native report summary contains inconsistent counts or success");return e}async function mt(t,e,r){const s=w.isAbsolute(e)?e:w.resolve(t,e),n=await y.lstat(s);if(!n.isFile()||n.isSymbolicLink())throw new Error(`${r} must be a regular non-symbolic-link file`);const a=await y.realpath(s);if(!Z(t,a))throw new Error(`${r} must stay within the task artifact directory`);return a}function F(t,e){if(!g(t))throw new Error(`${e} must contain an object`);const r=Object.create(null);for(const[s,n]of Object.entries(t))r[s]=P(n,`${e}.${s}`);return r}function P(t,e){if(t===null||typeof t=="boolean"||typeof t=="string")return t;if(typeof t=="number"){if(!Number.isFinite(t))throw new Error(`${e} must be finite`);return t}return Array.isArray(t)?t.map((r,s)=>P(r,`${e}[${s.toString()}]`)):F(t,e)}function M(t,e){if(typeof t!="string"||t.trim().length===0)throw new Error(`${e} must be a non-empty string`);return t}function d(t,e){if(typeof t!="number"||!Number.isSafeInteger(t)||t<0)throw new Error(`${e} must be a non-negative safe integer`);return t}function dt(t,e){if(typeof t!="boolean")throw new Error(`${e} must be boolean`);return t}function J(t){return t instanceof Error}export async function readNodeVitestRunReport(t,e,r=O){try{const{identity:s}=et(t,{reportFile:e,sourceFile:".",taskId:"run"}),{result:n}=j((await b(e,r)).value,s);if(!g(n)||!Number.isSafeInteger(n.exitCode)||!Array.isArray(n.unhandledErrors))throw new Error("Vitest run report must contain an exit code and unhandled errors");const a=n.unhandledErrors,o=n.failure===void 0?[]:[n.failure];D([...a,...o],"Vitest run report errors");const u=m=>g(m)&&[m.testPath,m.type].every(l=>l===void 0||typeof l=="string"),f=n.afterTests===void 0?[]:n.afterTests;if(!Array.isArray(f)||!f.every(m=>typeof m=="string")||!a.every(u)||!o.every(m=>g(m)&&["close","config","run"].includes(m.phase)))throw new Error("Vitest run report errors are malformed");return n}catch(s){return N(s)}}
@@ -1 +1 @@
1
- import M from"node:fs/promises";import c from"node:path";import{writeNativeReport as y}from"../../adapters/native-report.js";import{boundEnvironmentValueMissingText as S,environmentValueMissingFailure as F,readEnvironmentValueMissing as f}from"../../contracts/environment-values.js";export const ATTEMPT_ERROR_STARTS_META_KEY="testkitAttemptErrorStarts";export function nodeVitestReportOptions(t,{reportFile:n,sourceFile:s,taskId:i}){return{identity:{engineId:"node-vitest",invocationId:t,sourceFile:s,taskId:i},reportFile:n,workspaceDirectory:c.dirname(n)}}export class TestkitVitestReporter{invocationId;modules;finished=new Map;written=new Map;constructor(n,s){this.invocationId=n,this.modules=s}async onTestModuleEnd(n){const s=this.modules.find(({file:e,project:r})=>c.resolve(e)===c.resolve(n.moduleId)&&r===n.project.name);if(s===void 0)return;const i=[...this.finished.get(s.taskId)??[],n];this.finished.set(s.taskId,i);const a=this.modules.filter(({taskId:e})=>e===s.taskId).length-i.length,l=i.flatMap(e=>[e,...e.children.allSuites()]),o=i.flatMap(e=>[...e.children.allTests()]),u=o.filter(e=>e.result().state==="failed"),d=l.filter(e=>e.state()==="failed").length,g=l.filter(e=>e.state()==="pending"||e.state()==="queued"||e.type==="suite"&&e.options.mode==="todo").length,h=l.flatMap(e=>e.errors().filter(r=>!A(e,r))),T=h.map(reportedError),m=[...o.flatMap(e=>e.result().errors??[]),...h].flatMap(e=>{const r=f(e);return r===null?[]:[{message:e.message,missing:r}]}),k={...a===0?{}:{pending:a},summary:{numFailedTestSuites:d,numFailedTests:u.length,numPassedTestSuites:l.length-d-g,numPassedTests:o.filter(e=>e.result().state==="passed").length,numPendingTestSuites:g,numPendingTests:o.filter(e=>e.options.mode!=="todo"&&(e.result().state==="pending"||e.result().state==="skipped")).length,numTodoTests:o.filter(e=>e.options.mode==="todo").length,numTotalTestSuites:l.length,numTotalTests:o.length,success:d===0&&u.length===0&&T.length===0},...m.length===0?{}:{environmentFailure:F(m.map(({missing:e})=>e),m[0].message)},cases:o.map(e=>({attempts:R(e),file:e.module.moduleId,id:e.id,project:e.project.name,repeat:e.diagnostic()?.repeatCount??0,state:e.result().state,title:I(e)})),failures:u.map(e=>({...e.result().errors?.some(r=>f(r)!==null)?{classification:"infra"}:{},id:e.id,project:e.project.name,title:I(e),file:e.module.moduleId,line:e.location?.line??null,column:e.location?.column??null,errors:v(e).final.map(reportedError),previousAttemptErrors:v(e).earlier.map(reportedError)})),errors:T},p={...s,reportFile:`${s.reportFile}.next`},E=(this.written.get(s.taskId)??Promise.resolve()).then(async()=>{await M.rm(p.reportFile,{force:!0}),await y(nodeVitestReportOptions(this.invocationId,p),k,16*1024*1024),await M.rename(p.reportFile,s.reportFile)});this.written.set(s.taskId,E),await E}}function A(t,n){return t.type==="module"&&n.message===`No test suite found in file ${t.moduleId}`}function v(t){const n=t.result().errors??[],s=t.meta()[ATTEMPT_ERROR_STARTS_META_KEY],i=Array.isArray(s)?s.at(-1):void 0,a=typeof i=="number"&&i<n.length?i:0;return{earlier:n.slice(0,a),final:n.slice(a)}}function R(t){const n=t.diagnostic();return n===void 0?0:n.retryCount+1}export function reportedError(t){return{message:w(t,t.name?`${t.name}: ${t.message}`:t.message),...t.stack?{stack:w(t,t.stack)}:{}}}function w(t,n){return f(t)===null?n:S(n)}function I(t){const n=[];let s=t.parent;for(;s.type==="suite";)n.unshift(s.name),s=s.parent;return[...n,t.name].join(" ")}
1
+ import M from"node:fs/promises";import c from"node:path";import{writeNativeReport as y}from"../../adapters/native-report.js";import{boundEnvironmentValueMissingText as S,environmentValueMissingFailure as F,readEnvironmentValueMissing as f}from"../../contracts/environment-values.js";export const ATTEMPT_ERROR_STARTS_META_KEY="testkitAttemptErrorStarts";export function nodeVitestReportOptions(t,{reportFile:n,sourceFile:s,taskId:i}){return{identity:{engineId:"node-vitest",invocationId:t,sourceFile:s,taskId:i},reportFile:n,workspaceDirectory:c.dirname(n)}}export class TestkitVitestReporter{invocationId;modules;finished=new Map;written=new Map;constructor(n,s){this.invocationId=n,this.modules=s}async onTestModuleEnd(n){const s=this.modules.find(({file:e,project:r})=>c.resolve(e)===c.resolve(n.moduleId)&&r===n.project.name);if(s===void 0)return;const i=[...this.finished.get(s.taskId)??[],n];this.finished.set(s.taskId,i);const a=this.modules.filter(({taskId:e})=>e===s.taskId).length-i.length,l=i.flatMap(e=>[e,...e.children.allSuites()]),o=i.flatMap(e=>[...e.children.allTests()]),u=o.filter(e=>e.result().state==="failed"),d=l.filter(e=>e.state()==="failed").length,g=l.filter(e=>e.state()==="pending"||e.state()==="queued"||e.type==="suite"&&e.options.mode==="todo").length,h=l.flatMap(e=>e.errors().filter(r=>!A(e,r))),T=h.map(reportedError),m=[...o.flatMap(e=>e.result().errors??[]),...h].flatMap(e=>{const r=f(e);return r===null?[]:[{message:e.message,missing:r}]}),k={...a===0?{}:{pending:a},summary:{numFailedTestSuites:d,numFailedTests:u.length,numPassedTestSuites:l.length-d-g,numPassedTests:o.filter(e=>e.result().state==="passed").length,numPendingTestSuites:g,numPendingTests:o.filter(e=>e.options.mode!=="todo"&&(e.result().state==="pending"||e.result().state==="skipped")).length,numTodoTests:o.filter(e=>e.options.mode==="todo").length,numTotalTestSuites:l.length,numTotalTests:o.length,success:d===0&&u.length===0&&T.length===0},...m.length===0?{}:{environmentFailure:F(m.map(({missing:e})=>e),m[0].message)},cases:o.map(e=>({attempts:R(e),file:e.module.moduleId,id:e.id,project:e.project.name,repeat:e.diagnostic()?.repeatCount??0,...e.options.fails===!0?{fails:!0}:{},state:e.result().state,title:I(e)})),failures:u.map(e=>({...e.result().errors?.some(r=>f(r)!==null)?{classification:"infra"}:{},id:e.id,project:e.project.name,title:I(e),file:e.module.moduleId,line:e.location?.line??null,column:e.location?.column??null,errors:v(e).final.map(reportedError),previousAttemptErrors:v(e).earlier.map(reportedError)})),errors:T},p={...s,reportFile:`${s.reportFile}.next`},E=(this.written.get(s.taskId)??Promise.resolve()).then(async()=>{await M.rm(p.reportFile,{force:!0}),await y(nodeVitestReportOptions(this.invocationId,p),k,16*1024*1024),await M.rename(p.reportFile,s.reportFile)});this.written.set(s.taskId,E),await E}}function A(t,n){return t.type==="module"&&n.message===`No test suite found in file ${t.moduleId}`}function v(t){const n=t.result().errors??[],s=t.meta()[ATTEMPT_ERROR_STARTS_META_KEY],i=Array.isArray(s)?s.at(-1):void 0,a=typeof i=="number"&&i<n.length?i:0;return{earlier:n.slice(0,a),final:n.slice(a)}}function R(t){const n=t.diagnostic();return n===void 0?0:n.retryCount+1}export function reportedError(t){return{message:w(t,t.name?`${t.name}: ${t.message}`:t.message),...t.stack?{stack:w(t,t.stack)}:{}}}function w(t,n){return f(t)===null?n:S(n)}function I(t){const n=[];let s=t.parent;for(;s.type==="suite";)n.unshift(s.name),s=s.parent;return[...n,t.name].join(" ")}
@@ -1,2 +1,2 @@
1
- import{stringFromUnknown as g}from"./primitives.js";import B from"node:crypto";import{writeDataFlowScenario as M}from"./report.js";import{isUnknownArray as z,isUnknownRecord as p}from"../../../../kernel/unknown-values.js";const A=/(authorization|cookie|password|secret|token|api[_-]?key|private[_-]?key)/iu,$=String,y=new WeakMap;export function dataFlow(e){return E(e)}function E(e){const t=e?.task;if(t?.result===void 0||t.result.duration!==void 0||e?.onTestFinished===void 0)throw new Error("dataFlow(context) needs the context of a running Vitest test: test(name, (context) => { dataFlow(context); })");const r=y.get(e);if(r!==void 0)return r;const n=D(F(t),{path:process.env.TESTKIT_TEST_FILE??t.file.name,...t.location}),s=T(n);return y.set(e,s),e.onTestFinished(()=>{if(y.delete(e),t.result?.state==="skip")return;const i=N(n,t.result),a={attempt:(t.result?.retryCount??0)+1,repeat:t.result?.repeatCount??0,repeats:t.repeats??0};if(M(t.id,a,i),i.budgetFailures.length>0)throw new Error(R(i))}),s}function F(e){const t=[e.name];for(let r=e.suite;r!==void 0&&r!==e.file;r=r.suite)t.unshift(r.name);return t.join(" ")}export function measureJsonBytes(e){return Buffer.byteLength(JSON.stringify(e??null))}export function stableHash(e){const t=x(e);if(t===void 0)throw new TypeError("stableHash(...) requires a serializable value");return B.createHash("sha256").update(t).digest("hex")}function T(e){const t={addFinding(r,n,s={}){return e.findings.push({severity:J(r),message:o(n,"finding message"),details:l(s)}),t},actor(r,n={}){return d(e,"actor",r,n),t},budget(r){return S(e,r)},bytes(r,n,s={}){return t.metric(r,n,{...s,kind:"bytes",unit:s.unit||"bytes"})},count(r,n=1,s={}){return t.metric(r,n,{...s,kind:"count",unit:s.unit||"count"})},duration(r,n,s={}){return t.metric(r,n,{...s,kind:"duration",unit:s.unit||"ms"})},event(r,n={}){return e.events.push({atMs:Date.now()-e.startedAtMs,name:o(r,"event name"),details:l(n)}),t},externalCall(r,n={}){const s=h(n);return d(e,"external-call",r,s),typeof s.durationMs=="number"&&t.duration(`${r}.external.duration`,s.durationMs),typeof s.requestBytes=="number"&&t.bytes(`${r}.external.request.bytes`,s.requestBytes),typeof s.responseBytes=="number"&&t.bytes(`${r}.external.response.bytes`,s.responseBytes),typeof s.costMinor=="number"&&t.money(`${r}.external.cost`,s.costMinor),t},expectMetric(r){return S(e,r)},expectSequence(r){return k(e,r)},finding(r,n,s={}){return t.addFinding(r,n,s)},metric(r,n,s={}){return O(e,r,n,s),t},money(r,n,s={}){return t.metric(r,n,{...s,kind:"money",unit:s.unit||"minor"})},ratio(r,n,s={}){return t.metric(r,n,{...s,kind:"ratio",unit:s.unit||"ratio"})},reconcile(r,n={}){return d(e,"reconciliation",r,n),t},record(r,n={}){const s=h(n);return d(e,"record",r,s),typeof s.count=="number"&&t.count(`${r}.record.count`,s.count),t},recordCache(r,{hits:n=0,misses:s=0,reads:i=0,writes:a=0}={}){const c=o(r,"cache metric name");t.count(`${c}.cache.hits`,n),t.count(`${c}.cache.misses`,s),t.count(`${c}.cache.reads`,i),t.count(`${c}.cache.writes`,a);const m=n+s;return m>0&&t.ratio(`${c}.cache.hit_rate`,n/m),t},recordHttpRequest(r){return t.count("http.request.count",1),t.bytes("http.request.bytes",r.bodyBytes||0),t.bytes("http.response.bytes",r.responseBytes||0),t.duration("http.request.duration",r.durationMs||0),t.sequence("http.methods",[r.method]),t.sequence("http.paths",[r.path]),t.event("http.request",{durationMs:r.durationMs||0,method:r.method,path:r.path,requestBytes:r.bodyBytes||0,responseBytes:r.responseBytes||0,status:r.status}),t},recordLlmUsage(r,n={}){const s=o(r,"llm usage name");for(const[i,a]of Object.entries(n))typeof a=="number"&&Number.isFinite(a)&&t.count(`${s}.${I(i)}`,a,{unit:i.toLowerCase().includes("cost")?"minor":"tokens"});return t},recordRpc(r,{method:n,requestBytes:s=0,responseBytes:i=0}={}){const a=o(r,"rpc metric name");return t.count(`${a}.rpc.count`,1),n&&t.sequence(`${a}.rpc.methods`,[n]),t.bytes(`${a}.rpc.request.bytes`,s),t.bytes(`${a}.rpc.response.bytes`,i),t},sequence(r,n,s={}){return v(e,r,n,s),t},sequenceBudget(r){return k(e,r)},system(r,n={}){return d(e,"system",r,n),t},transition(r,n={}){return d(e,"transition",r,n),t}};return Object.freeze(t)}function S(e,t){const r=o(t,"metric budget name");return{atLeast(n,s={}){return u(e,{name:r,target:"metric",operator:">=",expected:n,message:s.message}),this},atMost(n,s={}){return u(e,{name:r,target:"metric",operator:"<=",expected:n,message:s.message}),this},equals(n,s={}){return u(e,{name:r,target:"metric",operator:"==",expected:n,message:s.message}),this},greaterThan(n,s={}){return u(e,{name:r,target:"metric",operator:">",expected:n,message:s.message}),this},lessThan(n,s={}){return u(e,{name:r,target:"metric",operator:"<",expected:n,message:s.message}),this},required(n={}){return u(e,{name:r,target:"metric",operator:"exists",message:n.message}),this}}}function k(e,t){const r=o(t,"sequence budget name");return{equals(n,s={}){return u(e,{name:r,target:"sequence",operator:"sequence==",expected:w(n),message:s.message}),this},includesOnly(n,s={}){return u(e,{name:r,target:"sequence",operator:"sequenceOnly",expected:w(n),message:s.message}),this},required(n={}){return u(e,{name:r,target:"sequence",operator:"exists",message:n.message}),this}}}function D(e,t){return{budgets:[],events:[],evidence:[],findings:[],location:t,metrics:[],name:o(e,"data scenario name"),sequences:[],startedAtMs:Date.now()}}function N(e,t){const{budgets:r,startedAtMs:n,...s}=e,i=r.map(m=>V(e,m)),a=i.filter(m=>m.status==="failed"),c=t?.state==="fail"?U(t.errors?.at(-1)):null;return{...s,budgetFailures:a,budgets:i,durationMs:Date.now()-n,error:c,status:c||a.length>0?"failed":"passed"}}function O(e,t,r,n={}){const s=r;if(!Number.isFinite(s))throw new Error(`Data metric "${t}" must be a finite number`);e.metrics.push({description:f(n.description),dimensions:b(n.dimensions??{}),kind:f(n.kind)||"metric",name:o(t,"metric name"),unit:f(n.unit),value:s})}function v(e,t,r,n={}){const s=w(r).map(i=>$(i));e.sequences.push({description:f(n.description),dimensions:b(n.dimensions??{}),name:o(t,"sequence name"),values:s})}function u(e,t){e.budgets.push({...t,message:f(t.message)})}function d(e,t,r,n={}){const s=h(n);e.evidence.push({atMs:Date.now()-e.startedAtMs,details:l(s),kind:t,name:o(r,`${t} evidence name`),status:f(s.status)||null})}function h(e){return p(e)?e:{value:e}}function V(e,t){const r=t.target==="sequence"?C(e,t.name):j(e,t.name),n=_(t,r),s=t.message||L(t,r,n);return t.target==="metric"?{...t,actual:r,expected:t.expected??null,message:s,status:n?"passed":"failed"}:{...t,actual:r,expected:t.expected??null,message:s,status:n?"passed":"failed"}}function _(e,t){if(e.target==="metric")return e.operator==="exists"?t!==null:typeof t!="number"?!1:e.operator==="<="?t<=Number(e.expected):e.operator===">="?t>=Number(e.expected):e.operator==="<"?t<Number(e.expected):e.operator===">"?t>Number(e.expected):Object.is(t,Number(e.expected));if(e.operator==="exists")return t!==null;if(!Array.isArray(t))return!1;if(e.operator==="sequence==")return H(t,e.expected);const r=new Set((e.expected??[]).map(n=>g(n)));return t.every(n=>r.has(n))}function j(e,t){for(let r=e.metrics.length-1;r>=0;r-=1)if(e.metrics[r].name===t)return e.metrics[r].value;return null}function C(e,t){const r=[];for(const n of e.sequences)n.name===t&&r.push(...n.values);return r.length>0?r:null}function L(e,t,r){return e.operator==="exists"?r?`${e.target} "${e.name}" was recorded`:`${e.target} "${e.name}" was not recorded`:`${e.target} "${e.name}" expected ${e.operator} ${JSON.stringify(e.expected)}, got ${JSON.stringify(t)}`}function R(e){return[`Data-flow budgets failed in "${e.name}":`,...e.budgetFailures.map(t=>`- ${t.message}`)].join(`
2
- `)}function w(e){return z(e)?[...e]:[e]}function o(e,t){const r=g(e).trim();if(!r)throw new Error(`${t} must be a non-empty string`);return r}function f(e){if(e==null)return null;const t=g(e).trim();return t||null}function J(e){const t=g(e||"info").toLowerCase();return K(t)?t:"info"}function l(e){if(Array.isArray(e))return e.map(r=>l(r));if(!p(e))return e;const t={};for(const[r,n]of Object.entries(e))t[r]=A.test(r)?"[redacted]":l(n);return t}function U(e){return e instanceof Error?{message:e.message,name:e.name,stack:e.stack??null}:{message:q(e,"message")??P(e),name:q(e,"name")??"Error",stack:q(e,"stack")}}function x(e){return Array.isArray(e)?`[${e.map(t=>x(t)).join(",")}]`:p(e)?`{${Object.keys(e).sort().map(t=>{const r=x(e[t]);return`${JSON.stringify(t)}:${r??"undefined"}`}).join(",")}}`:JSON.stringify(e)}function H(e,t){return!Array.isArray(e)||!Array.isArray(t)||e.length!==t.length?!1:e.every((r,n)=>String(r)===String(t[n]))}function I(e){return e.replace(/([a-z0-9])([A-Z])/g,"$1_$2").replace(/[^a-zA-Z0-9]+/g,"_").replace(/^_+|_+$/g,"").toLowerCase()}function b(e){const t=l(e);return p(t)?t:{}}function q(e,t){if(!p(e))return null;try{const r=Reflect.get(e,t);return typeof r=="string"?r:null}catch{return null}}function P(e){try{return $(e)}catch{return"Unknown thrown value"}}function K(e){return e==="error"||e==="info"||e==="waste"||e==="watch"}
1
+ import{stringFromUnknown as g}from"./primitives.js";import B from"node:crypto";import{writeDataFlowScenario as M}from"./report.js";import{isUnknownArray as z,isUnknownRecord as p}from"../../../../kernel/unknown-values.js";const A=/(authorization|cookie|password|secret|token|api[_-]?key|private[_-]?key)/iu,$=String,y=new WeakMap;export function dataFlow(e){return E(e)}function E(e){const t=e?.task;if(t?.result===void 0||t.result.duration!==void 0||e?.onTestFinished===void 0)throw new Error("dataFlow(context) needs the context of a running Vitest test: test(name, (context) => { dataFlow(context); })");const r=y.get(e);if(r!==void 0)return r;const n=D(F(t),{path:process.env.TESTKIT_TEST_FILE??t.file.name,...t.location}),s=T(n);return y.set(e,s),e.onTestFinished(()=>{if(y.delete(e),t.result?.state==="skip")return;const i=N(n,t.result),a={attempt:(t.result?.retryCount??0)+1,repeat:t.result?.repeatCount??0,repeats:t.repeats??0};if(M(t.id,a,i),i.budgetFailures.length>0)throw new Error(R(i))}),s}function F(e){const t=[e.name];for(let r=e.suite;r!==void 0&&r!==e.file;r=r.suite)t.unshift(r.name);return t.join(" ")}export function measureJsonBytes(e){return Buffer.byteLength(JSON.stringify(e??null))}export function stableHash(e){const t=x(e);if(t===void 0)throw new TypeError("stableHash(...) requires a serializable value");return B.createHash("sha256").update(t).digest("hex")}function T(e){const t={addFinding(r,n,s={}){return e.findings.push({severity:J(r),message:o(n,"finding message"),details:l(s)}),t},actor(r,n={}){return d(e,"actor",r,n),t},budget(r){return S(e,r)},bytes(r,n,s={}){return t.metric(r,n,{...s,kind:"bytes",unit:s.unit||"bytes"})},count(r,n=1,s={}){return t.metric(r,n,{...s,kind:"count",unit:s.unit||"count"})},duration(r,n,s={}){return t.metric(r,n,{...s,kind:"duration",unit:s.unit||"ms"})},event(r,n={}){return e.events.push({atMs:Date.now()-e.startedAtMs,name:o(r,"event name"),details:l(n)}),t},externalCall(r,n={}){const s=h(n);return d(e,"external-call",r,s),typeof s.durationMs=="number"&&t.duration(`${r}.external.duration`,s.durationMs),typeof s.requestBytes=="number"&&t.bytes(`${r}.external.request.bytes`,s.requestBytes),typeof s.responseBytes=="number"&&t.bytes(`${r}.external.response.bytes`,s.responseBytes),typeof s.costMinor=="number"&&t.money(`${r}.external.cost`,s.costMinor),t},expectMetric(r){return S(e,r)},expectSequence(r){return b(e,r)},finding(r,n,s={}){return t.addFinding(r,n,s)},metric(r,n,s={}){return O(e,r,n,s),t},money(r,n,s={}){return t.metric(r,n,{...s,kind:"money",unit:s.unit||"minor"})},ratio(r,n,s={}){return t.metric(r,n,{...s,kind:"ratio",unit:s.unit||"ratio"})},reconcile(r,n={}){return d(e,"reconciliation",r,n),t},record(r,n={}){const s=h(n);return d(e,"record",r,s),typeof s.count=="number"&&t.count(`${r}.record.count`,s.count),t},recordCache(r,{hits:n=0,misses:s=0,reads:i=0,writes:a=0}={}){const c=o(r,"cache metric name");t.count(`${c}.cache.hits`,n),t.count(`${c}.cache.misses`,s),t.count(`${c}.cache.reads`,i),t.count(`${c}.cache.writes`,a);const m=n+s;return m>0&&t.ratio(`${c}.cache.hit_rate`,n/m),t},recordHttpRequest(r){return t.count("http.request.count",1),t.bytes("http.request.bytes",r.bodyBytes||0),t.bytes("http.response.bytes",r.responseBytes||0),t.duration("http.request.duration",r.durationMs||0),t.sequence("http.methods",[r.method]),t.sequence("http.paths",[r.path]),t.event("http.request",{durationMs:r.durationMs||0,method:r.method,path:r.path,requestBytes:r.bodyBytes||0,responseBytes:r.responseBytes||0,status:r.status}),t},recordLlmUsage(r,n={}){const s=o(r,"llm usage name");for(const[i,a]of Object.entries(n))typeof a=="number"&&Number.isFinite(a)&&t.count(`${s}.${I(i)}`,a,{unit:i.toLowerCase().includes("cost")?"minor":"tokens"});return t},recordRpc(r,{method:n,requestBytes:s=0,responseBytes:i=0}={}){const a=o(r,"rpc metric name");return t.count(`${a}.rpc.count`,1),n&&t.sequence(`${a}.rpc.methods`,[n]),t.bytes(`${a}.rpc.request.bytes`,s),t.bytes(`${a}.rpc.response.bytes`,i),t},sequence(r,n,s={}){return v(e,r,n,s),t},sequenceBudget(r){return b(e,r)},system(r,n={}){return d(e,"system",r,n),t},transition(r,n={}){return d(e,"transition",r,n),t}};return Object.freeze(t)}function S(e,t){const r=o(t,"metric budget name");return{atLeast(n,s={}){return u(e,{name:r,target:"metric",operator:">=",expected:n,message:s.message}),this},atMost(n,s={}){return u(e,{name:r,target:"metric",operator:"<=",expected:n,message:s.message}),this},equals(n,s={}){return u(e,{name:r,target:"metric",operator:"==",expected:n,message:s.message}),this},greaterThan(n,s={}){return u(e,{name:r,target:"metric",operator:">",expected:n,message:s.message}),this},lessThan(n,s={}){return u(e,{name:r,target:"metric",operator:"<",expected:n,message:s.message}),this},required(n={}){return u(e,{name:r,target:"metric",operator:"exists",message:n.message}),this}}}function b(e,t){const r=o(t,"sequence budget name");return{equals(n,s={}){return u(e,{name:r,target:"sequence",operator:"sequence==",expected:w(n),message:s.message}),this},includesOnly(n,s={}){return u(e,{name:r,target:"sequence",operator:"sequenceOnly",expected:w(n),message:s.message}),this},required(n={}){return u(e,{name:r,target:"sequence",operator:"exists",message:n.message}),this}}}function D(e,t){return{budgets:[],events:[],evidence:[],findings:[],location:t,metrics:[],name:o(e,"data scenario name"),sequences:[],startedAtMs:Date.now()}}function N(e,t){const{budgets:r,startedAtMs:n,...s}=e,i=r.map(m=>V(e,m)),a=i.filter(m=>m.status==="failed"),c=t?.state==="fail"?U(t.errors?.at(-1)):null;return{...s,budgetFailures:a,budgets:i,durationMs:Date.now()-n,error:c,status:c||a.length>0?"failed":"passed"}}function O(e,t,r,n={}){const s=r;if(!Number.isFinite(s))throw new Error(`Data metric "${t}" must be a finite number`);e.metrics.push({description:f(n.description),dimensions:k(n.dimensions??{}),kind:f(n.kind)||"metric",name:o(t,"metric name"),unit:f(n.unit),value:s})}function v(e,t,r,n={}){const s=w(r).map(i=>$(i));e.sequences.push({description:f(n.description),dimensions:k(n.dimensions??{}),name:o(t,"sequence name"),values:s})}function u(e,t){e.budgets.push({...t,message:f(t.message)})}function d(e,t,r,n={}){const s=h(n);e.evidence.push({atMs:Date.now()-e.startedAtMs,details:l(s),kind:t,name:o(r,`${t} evidence name`),status:f(s.status)||null})}function h(e){return p(e)?e:{value:e}}function V(e,t){const r=t.target==="sequence"?C(e,t.name):j(e,t.name),n=_(t,r),s=t.message||L(t,r,n);return t.target==="metric"?{...t,actual:r,expected:t.expected??null,message:s,status:n?"passed":"failed"}:{...t,actual:r,expected:t.expected??null,message:s,status:n?"passed":"failed"}}function _(e,t){if(e.target==="metric")return e.operator==="exists"?t!==null:typeof t!="number"?!1:e.operator==="<="?t<=Number(e.expected):e.operator===">="?t>=Number(e.expected):e.operator==="<"?t<Number(e.expected):e.operator===">"?t>Number(e.expected):Object.is(t,Number(e.expected));if(e.operator==="exists")return t!==null;if(!Array.isArray(t))return!1;if(e.operator==="sequence==")return H(t,e.expected);const r=new Set((e.expected??[]).map(n=>g(n)));return t.every(n=>r.has(n))}function j(e,t){for(let r=e.metrics.length-1;r>=0;r-=1)if(e.metrics[r].name===t)return e.metrics[r].value;return null}function C(e,t){const r=[];for(const n of e.sequences)n.name===t&&r.push(...n.values);return r.length>0?r:null}function L(e,t,r){return e.operator==="exists"?r?`${e.target} "${e.name}" was recorded`:`${e.target} "${e.name}" was not recorded`:`${e.target} "${e.name}" expected ${e.operator} ${JSON.stringify(e.expected)}, got ${JSON.stringify(t)}`}function R(e){return[`Data-flow budgets failed in "${e.name}":`,...e.budgetFailures.map(t=>`- ${t.message}`)].join(`
2
+ `)}function w(e){return z(e)?[...e]:[e]}function o(e,t){const r=g(e).trim();if(!r)throw new Error(`${t} must be a non-empty string`);return r}function f(e){if(e==null)return null;const t=g(e).trim();return t||null}function J(e){const t=g(e||"info").toLowerCase();return K(t)?t:"info"}function l(e){if(Array.isArray(e))return e.map(r=>l(r));if(!p(e))return e;const t=Object.create(null);for(const[r,n]of Object.entries(e))t[r]=A.test(r)?"[redacted]":l(n);return t}function U(e){return e instanceof Error?{message:e.message,name:e.name,stack:e.stack??null}:{message:q(e,"message")??P(e),name:q(e,"name")??"Error",stack:q(e,"stack")}}function x(e){return Array.isArray(e)?`[${e.map(t=>x(t)).join(",")}]`:p(e)?`{${Object.keys(e).sort().map(t=>{const r=x(e[t]);return`${JSON.stringify(t)}:${r??"undefined"}`}).join(",")}}`:JSON.stringify(e)}function H(e,t){return!Array.isArray(e)||!Array.isArray(t)||e.length!==t.length?!1:e.every((r,n)=>String(r)===String(t[n]))}function I(e){return e.replace(/([a-z0-9])([A-Z])/g,"$1_$2").replace(/[^a-zA-Z0-9]+/g,"_").replace(/^_+|_+$/g,"").toLowerCase()}function k(e){const t=l(e);return p(t)?t:{}}function q(e,t){if(!p(e))return null;try{const r=Reflect.get(e,t);return typeof r=="string"?r:null}catch{return null}}function P(e){try{return $(e)}catch{return"Unknown thrown value"}}function K(e){return e==="error"||e==="info"||e==="waste"||e==="watch"}
@@ -1 +1 @@
1
- import s from"node:fs";import{createRequire as v}from"node:module";import c from"node:path";import{isUnknownRecord as u}from"../../kernel/unknown-values.js";import{isPathWithin as S}from"../../process/files/paths.js";import{NODE_VITEST_ENGINE_ENVIRONMENT_KEYS as N}from"./runtime/runtime-input.js";const O=["auto","managed","repo"];export const NODE_VITEST_INVENTORY_MARKER="TESTKIT_VITEST_INVENTORY=";const h=new Set(["BASE_URL","DATABASE_URL","NODE_OPTIONS",...N].map(e=>e.toUpperCase()));export function resolveVitestSelection(e,t,n=process.env){const{"vitest-config":r,"vitest-project":o,"vitest-runner":a}=e;if(a==="managed"&&(r??o)!==void 0)throw new Error("--vitest-config and --vitest-project select the repository's Vitest, not --vitest-runner managed");return{config:r===void 0?null:g(t,r),host:{...Object.fromEntries(Object.entries(n).filter(i=>i[1]!==void 0&&!h.has(i[0].toUpperCase()))),...n.NODE_OPTIONS===void 0?{}:{NODE_OPTIONS:n.NODE_OPTIONS}},project:o??null,runner:a??((r??o)===void 0?"auto":"repo")}}export function decodeVitestSelection(e){if(!u(e)||!O.includes(e.runner)||![e.config,e.project].every(t=>t===null||typeof t=="string")||!u(e.host)||!Object.values(e.host).every(t=>typeof t=="string"))throw new Error("Node/Vitest selection must name a runner, config, project and host environment");return e}function g(e,t){let n;try{n=s.realpathSync(c.resolve(e,t))}catch(r){throw new Error(`--vitest-config ${t} does not exist in ${e}`,{cause:r})}if(!S(s.realpathSync(e),n))throw new Error(`--vitest-config ${t} resolves outside ${e}`);if(!s.statSync(n).isFile())throw new Error(`--vitest-config ${t} is not a file`);return n}export function resolveRepositoryVitest(e,t=!1){const n=c.join(e,"package.json"),r=v(n);let o;try{o=r.resolve("vitest/package.json")}catch(f){const d=!r.resolve.paths("vitest")?.some(m=>s.lstatSync(c.join(m,"vitest"),{throwIfNoEntry:!1}));if(d&&t&&!w(n))return null;const E=d?"is not installed":"is installed but its package metadata is broken";throw new Error(`Vitest ${E} in ${e}; install vitest 4.x, or pass --vitest-runner managed`,{cause:f})}const a=c.dirname(o),i=JSON.parse(s.readFileSync(o,"utf8")),p=`Vitest ${i.version} at ${a}`;if(!/^4\.\d+\.\d+/u.test(i.version))throw new Error(`${p} is not supported; install vitest 4.x, or pass --vitest-runner managed`);const l=i.exports?.["."]?.import?.default;try{if(typeof l!="string")throw new Error("its package exports name no ESM entry point");return{entrypoint:r.resolve("vitest/node"),index:c.join(a,l),name:"vitest",root:a,version:i.version}}catch(f){throw new Error(`${p} is broken; reinstall it`,{cause:f})}}function w(e){if(!s.existsSync(e))return!1;const t=JSON.parse(s.readFileSync(e,"utf8"));return["dependencies","devDependencies","optionalDependencies"].some(n=>Object.hasOwn(t[n]??{},"vitest"))}
1
+ import i from"node:fs";import{createRequire as h}from"node:module";import f from"node:path";import{isUnknownRecord as d}from"../../kernel/unknown-values.js";import{isPathWithin as S}from"../../process/files/paths.js";import{NODE_VITEST_ENGINE_ENVIRONMENT_KEYS as v}from"./runtime/runtime-input.js";const N=["auto","managed","repo"];export const NODE_VITEST_INVENTORY_MARKER="TESTKIT_VITEST_INVENTORY=";const g=new Set(["BASE_URL","DATABASE_URL","NODE_OPTIONS",...v].map(e=>e.toUpperCase()));export function resolveVitestSelection(e,t,n=process.env){const{"vitest-config":r,"vitest-project":s,"vitest-runner":a,"vitest-shuffle":o}=e;if(o!==void 0&&!/^\d{1,9}$/u.test(o))throw new Error(`--vitest-shuffle takes a seed of up to 9 digits, not ${JSON.stringify(o)}`);if(a==="managed"&&(r??s)!==void 0)throw new Error("--vitest-config and --vitest-project select the repository's Vitest, not --vitest-runner managed");return{config:r===void 0?null:O(t,r),host:{...Object.fromEntries(Object.entries(n).filter(c=>c[1]!==void 0&&!g.has(c[0].toUpperCase()))),...n.NODE_OPTIONS===void 0?{}:{NODE_OPTIONS:n.NODE_OPTIONS}},project:s??null,runner:a??((r??s)===void 0?"auto":"repo"),shuffle:o===void 0?null:Number(o)}}export function decodeVitestSelection(e){if(!d(e)||!N.includes(e.runner)||![e.config,e.project].every(t=>t===null||typeof t=="string")||!(e.shuffle===null||Number.isInteger(e.shuffle))||!d(e.host)||!Object.values(e.host).every(t=>typeof t=="string"))throw new Error("Node/Vitest selection must name a runner, config, project and host environment");return e}function O(e,t){let n;try{n=i.realpathSync(f.resolve(e,t))}catch(r){throw new Error(`--vitest-config ${t} does not exist in ${e}`,{cause:r})}if(!S(i.realpathSync(e),n))throw new Error(`--vitest-config ${t} resolves outside ${e}`);if(!i.statSync(n).isFile())throw new Error(`--vitest-config ${t} is not a file`);return n}export function resolveRepositoryVitest(e,t=!1){const n=f.join(e,"package.json"),r=h(n);let s;try{s=r.resolve("vitest/package.json")}catch(l){const u=!r.resolve.paths("vitest")?.some(E=>i.lstatSync(f.join(E,"vitest"),{throwIfNoEntry:!1}));if(u&&t&&!w(n))return null;const m=u?"is not installed":"is installed but its package metadata is broken";throw new Error(`Vitest ${m} in ${e}; install vitest 4.x, or pass --vitest-runner managed`,{cause:l})}const a=f.dirname(s),o=JSON.parse(i.readFileSync(s,"utf8")),c=`Vitest ${o.version} at ${a}`;if(!/^4\.\d+\.\d+/u.test(o.version))throw new Error(`${c} is not supported; install vitest 4.x, or pass --vitest-runner managed`);const p=o.exports?.["."]?.import?.default;try{if(typeof p!="string")throw new Error("its package exports name no ESM entry point");return{entrypoint:r.resolve("vitest/node"),index:f.join(a,p),name:"vitest",root:a,version:o.version}}catch(l){throw new Error(`${c} is broken; reinstall it`,{cause:l})}}function w(e){if(!i.existsSync(e))return!1;const t=JSON.parse(i.readFileSync(e,"utf8"));return["dependencies","devDependencies","optionalDependencies"].some(n=>Object.hasOwn(t[n]??{},"vitest"))}
@@ -1,8 +1,8 @@
1
- import{ownedRuntimeInputModuleUrl as b}from"../../runtime-input-file.js";import P from"node:crypto";import s from"node:fs/promises";import o from"node:path";import{fileURLToPath as j,pathToFileURL as f}from"node:url";import{createManagedNodeModuleResolution as v}from"../../managed-module-resolution.js";import{materializePlaywrightRuntimeInput as T,playwrightRuntimeInputBootstrapSource as k}from"../runtime/runtime-input.js";import{nativeReporterFile as x}from"../../adapters/native-report.js";import{requireConsumerFile as O}from"../../adapters/workspace.js";export class PlaywrightConsumerConfigurationError extends Error{constructor(r){const t=r instanceof Error?r:new Error(String(r));super(t.message,{cause:t}),this.name="PlaywrightConsumerConfigurationError"}}export async function writePlaywrightTaskWorkspace(e){let r;try{F(e.inputs.supportModules),r=await D(e.rootDirectory,e.inputs)}catch(w){throw new PlaywrightConsumerConfigurationError(w)}await s.mkdir(e.taskDirectory,{recursive:!0}),await s.mkdir(e.artifactDirectory,{recursive:!0});const t=P.randomUUID(),n=o.join(e.taskDirectory,`playwright-${t}.config.mjs`),a=o.join(e.taskDirectory,`playwright-${t}.input-bootstrap.mjs`),l=o.join(e.taskDirectory,`playwright-${t}.resolution-bootstrap.cjs`),d=o.join(e.taskDirectory,"testkit-playwright-report.json"),g=o.join(e.taskDirectory,"testkit-playwright-reporters-finished"),p=o.join(e.artifactDirectory,"playwright-output");await s.mkdir(p);const c=o.join(e.artifactDirectory,"playwright-attachments");await s.mkdir(c,{mode:448,recursive:!1});const u=await T({baseUrl:e.baseUrl,databases:e.databases,directory:e.taskDirectory,endpoints:e.endpoints,handles:e.handles,inputs:e.inputs,invocationId:e.invocationId,orchestration:e.orchestration,taskId:e.taskId}),y=x("playwright");return await h(l,playwrightResolutionBootstrapSource(e.managedModuleEntrypoints,{consumerRoots:[e.rootDirectory],trustedImporterFiles:[n,a],trustedInternalEntrypoints:[e.moduleFile,y,j(b)]})),await h(a,k({digest:u.digest,file:u.file,sizeBytes:u.sizeBytes,invocationId:e.invocationId,taskId:e.taskId})),await h(n,S({baseConfigFile:r,baseUrl:e.baseUrl,browserFile:e.browserFile,inputBootstrapFile:a,outputDirectory:p,reporterFile:y,report:{attachmentDirectory:c,identity:{engineId:"playwright",invocationId:e.invocationId,taskId:e.taskId,sourceFile:e.sourceFile},reportFile:d,reportersFinishedFile:g,workspaceDirectory:e.taskDirectory},rootDirectory:e.rootDirectory,sourceFile:e.sourceFile,testkitPackageDirectory:await s.realpath(o.dirname(e.managedModuleEntrypoints.rootPackageJson))})),{attachmentDirectory:c,configFile:n,inputBootstrapFile:a,outputDirectory:p,reportFile:d,reportersFinishedFile:g,resolutionBootstrapFile:l}}export function playwrightResolutionBootstrapSource(e,r){const t={authoring:i(e.authoring,"authoring"),cli:i(e.cli,"cli"),environment:i(e.environment,"environment"),managedTest:i(e.managedTest,"managedTest"),playwright:i(e.playwright,"playwright"),playwrightCore:i(e.playwrightCore,"playwrightCore"),playwrightTest:i(e.playwrightTest,"playwrightTest"),root:i(e.root,"root"),rootPackageJson:i(e.rootPackageJson,"rootPackageJson")},n={"@playwright/test":t.playwrightTest,playwright:t.playwright,"playwright-core":t.playwrightCore};return v({consumerRoots:r.consumerRoots,direct:{"@gfdelarue/testkit":t.root,"@gfdelarue/testkit/environment":t.environment,"@gfdelarue/testkit/package.json":t.rootPackageJson,"@gfdelarue/testkit/playwright":t.authoring,"@playwright/test":t.managedTest,"playwright/test":t.managedTest},directBridgeRoots:[o.dirname(t.root)],engineLabel:"Playwright",forbiddenConsumerPackagePrefixes:[],packageAnchors:n,reservedPackageNames:["@gfdelarue/testkit"],stateSymbol:"@gfdelarue/testkit/playwright-resolution-hooks/v1",trustedImporterFiles:r.trustedImporterFiles,trustedInternalEntrypoints:r.trustedInternalEntrypoints,verifiedParentlessPackageFiles:{playwright:["lib/loader/loaderProcessEntry.js","lib/worker/workerProcessEntry.js"]},verifiedProcessEntrypoints:[t.cli]}).bootstrapSource}function i(e,r){if(!o.isAbsolute(e))throw new Error(`Resolved Playwright installation ${r} entrypoint must be absolute`);return e}function S(e){return`
1
+ import{ownedRuntimeInputModuleUrl as b}from"../../runtime-input-file.js";import P from"node:crypto";import s from"node:fs/promises";import o from"node:path";import{fileURLToPath as j,pathToFileURL as y}from"node:url";import{createManagedNodeModuleResolution as v}from"../../managed-module-resolution.js";import{materializePlaywrightRuntimeInput as T,playwrightRuntimeInputBootstrapSource as O}from"../runtime/runtime-input.js";import{nativeReporterFile as k}from"../../adapters/native-report.js";import{requireConsumerFile as S}from"../../adapters/workspace.js";export class PlaywrightConsumerConfigurationError extends Error{constructor(r){const t=r instanceof Error?r:new Error(String(r));super(t.message,{cause:t}),this.name="PlaywrightConsumerConfigurationError"}}export async function writePlaywrightTaskWorkspace(e){let r;try{D(e.inputs.supportModules),r=await I(e.rootDirectory,e.inputs)}catch(w){throw new PlaywrightConsumerConfigurationError(w)}await s.mkdir(e.taskDirectory,{recursive:!0}),await s.mkdir(e.artifactDirectory,{recursive:!0});const t=P.randomUUID(),n=o.join(e.taskDirectory,`playwright-${t}.config.mjs`),a=o.join(e.taskDirectory,`playwright-${t}.input-bootstrap.mjs`),l=o.join(e.taskDirectory,`playwright-${t}.resolution-bootstrap.cjs`),d=o.join(e.taskDirectory,"testkit-playwright-report.json"),g=o.join(e.taskDirectory,"testkit-playwright-reporters-finished"),p=o.join(e.artifactDirectory,"playwright-output");await s.mkdir(p);const c=o.join(e.artifactDirectory,"playwright-attachments");await s.mkdir(c,{mode:448,recursive:!1});const u=await T({baseUrl:e.baseUrl,databases:e.databases,directory:e.taskDirectory,endpoints:e.endpoints,handles:e.handles,inputs:e.inputs,invocationId:e.invocationId,orchestration:e.orchestration,taskId:e.taskId}),f=k("playwright");return await h(l,playwrightResolutionBootstrapSource(e.managedModuleEntrypoints,{consumerRoots:[e.rootDirectory],trustedImporterFiles:[n,a],trustedInternalEntrypoints:[e.moduleFile,f,j(b)]})),await h(a,O({digest:u.digest,file:u.file,sizeBytes:u.sizeBytes,invocationId:e.invocationId,taskId:e.taskId})),await h(n,x({baseConfigFile:r,baseUrl:e.baseUrl,browserFile:e.browserFile,inputBootstrapFile:a,outputDirectory:p,reporterFile:f,report:{attachmentDirectory:c,identity:{engineId:"playwright",invocationId:e.invocationId,taskId:e.taskId,sourceFile:e.sourceFile},reportFile:d,reportersFinishedFile:g,workspaceDirectory:e.taskDirectory},retries:typeof e.inputs.configuration?.retries=="number"?e.inputs.configuration.retries:null,rootDirectory:e.rootDirectory,sourceFile:e.sourceFile,testkitPackageDirectory:await s.realpath(o.dirname(e.managedModuleEntrypoints.rootPackageJson))})),{attachmentDirectory:c,configFile:n,inputBootstrapFile:a,outputDirectory:p,reportFile:d,reportersFinishedFile:g,resolutionBootstrapFile:l}}export function playwrightResolutionBootstrapSource(e,r){const t={authoring:i(e.authoring,"authoring"),cli:i(e.cli,"cli"),environment:i(e.environment,"environment"),managedTest:i(e.managedTest,"managedTest"),playwright:i(e.playwright,"playwright"),playwrightCore:i(e.playwrightCore,"playwrightCore"),playwrightTest:i(e.playwrightTest,"playwrightTest"),root:i(e.root,"root"),rootPackageJson:i(e.rootPackageJson,"rootPackageJson")},n={"@playwright/test":t.playwrightTest,playwright:t.playwright,"playwright-core":t.playwrightCore};return v({consumerRoots:r.consumerRoots,direct:{"@gfdelarue/testkit":t.root,"@gfdelarue/testkit/environment":t.environment,"@gfdelarue/testkit/package.json":t.rootPackageJson,"@gfdelarue/testkit/playwright":t.authoring,"@playwright/test":t.managedTest,"playwright/test":t.managedTest},directBridgeRoots:[o.dirname(t.root)],engineLabel:"Playwright",forbiddenConsumerPackagePrefixes:[],packageAnchors:n,reservedPackageNames:["@gfdelarue/testkit"],stateSymbol:"@gfdelarue/testkit/playwright-resolution-hooks/v1",trustedImporterFiles:r.trustedImporterFiles,trustedInternalEntrypoints:r.trustedInternalEntrypoints,verifiedParentlessPackageFiles:{playwright:["lib/loader/loaderProcessEntry.js","lib/worker/workerProcessEntry.js"]},verifiedProcessEntrypoints:[t.cli]}).bootstrapSource}function i(e,r){if(!o.isAbsolute(e))throw new Error(`Resolved Playwright installation ${r} entrypoint must be absolute`);return e}function x(e){return`
2
2
  import path from "node:path";
3
3
  import { createRequire } from "node:module";
4
4
 
5
- const OPTIONS = ${JSON.stringify({baseConfigUrl:e.baseConfigFile===null?null:f(e.baseConfigFile).href,baseUrl:e.baseUrl,browserFile:e.browserFile,exactTaskMatchSource:playwrightExactFileFilter(e.sourceFile),originalConfigDirectory:e.baseConfigFile===null?e.rootDirectory:o.dirname(e.baseConfigFile),originalConfigFile:e.baseConfigFile??o.join(e.rootDirectory,"__testkit_playwright_config__.mjs"),inputBootstrapUrl:f(e.inputBootstrapFile).href,outputDirectory:e.outputDirectory,report:e.report,reporterFile:e.reporterFile,rootDirectory:e.rootDirectory,sourceFile:e.sourceFile,testkitPackageMatchSource:`^${m(e.testkitPackageDirectory)}[\\\\/]`})};
5
+ const OPTIONS = ${JSON.stringify({baseConfigUrl:e.baseConfigFile===null?null:y(e.baseConfigFile).href,baseUrl:e.baseUrl,browserFile:e.browserFile,exactTaskMatchSource:playwrightExactFileFilter(e.sourceFile),originalConfigDirectory:e.baseConfigFile===null?e.rootDirectory:o.dirname(e.baseConfigFile),originalConfigFile:e.baseConfigFile??o.join(e.rootDirectory,"__testkit_playwright_config__.mjs"),inputBootstrapUrl:y(e.inputBootstrapFile).href,outputDirectory:e.outputDirectory,report:e.report,reporterFile:e.reporterFile,retries:e.retries,rootDirectory:e.rootDirectory,sourceFile:e.sourceFile,testkitPackageMatchSource:`^${m(e.testkitPackageDirectory)}[\\\\/]`})};
6
6
  const inputBootstrap = await import(OPTIONS.inputBootstrapUrl);
7
7
  if (typeof inputBootstrap.installPlaywrightRuntimeInput !== "function")
8
8
  throw new Error("Managed Playwright runtime-input bootstrap is invalid");
@@ -55,7 +55,7 @@ function normalizeExpect(expect) {
55
55
  };
56
56
  }
57
57
 
58
- function normalizeUse(use, label, inheritedSandbox) {
58
+ function normalizeUse(use, label, inheritedSandbox, inheritedTimeZone) {
59
59
  const value = use && typeof use === "object" && !Array.isArray(use) ? use : {};
60
60
  const launchOptions = value.launchOptions && typeof value.launchOptions === "object" ? value.launchOptions : {};
61
61
  if (value.browserName !== undefined && value.browserName !== "chromium")
@@ -68,6 +68,9 @@ function normalizeUse(use, label, inheritedSandbox) {
68
68
  ...value,
69
69
  browserName: "chromium",
70
70
  ...(OPTIONS.baseUrl === null ? {} : { baseURL: OPTIONS.baseUrl }),
71
+ ...(value.timezoneId === undefined && inheritedTimeZone === undefined && knownTimeZone(process.env.TZ)
72
+ ? { timezoneId: process.env.TZ }
73
+ : {}),
71
74
  launchOptions: {
72
75
  ...launchOptions,
73
76
  chromiumSandbox: launchOptions.chromiumSandbox ?? inheritedSandbox ?? true,
@@ -76,6 +79,16 @@ function normalizeUse(use, label, inheritedSandbox) {
76
79
  };
77
80
  }
78
81
 
82
+ function knownTimeZone(zone) {
83
+ if (!zone) return false;
84
+ try {
85
+ new Intl.DateTimeFormat("en-US", { timeZone: zone });
86
+ return true;
87
+ } catch {
88
+ return false;
89
+ }
90
+ }
91
+
79
92
  function normalizeSetups(value) {
80
93
  if (value === undefined) return [];
81
94
  return (Array.isArray(value) ? value : [value]).map(absolutePath);
@@ -137,8 +150,9 @@ function normalizeProject(project, index, supportNames) {
137
150
  snapshotPathTemplate: absoluteTemplate(project.snapshotPathTemplate),
138
151
  }),
139
152
  expect: normalizeExpect(project.expect),
153
+ ...(OPTIONS.retries === null ? {} : { retries: OPTIONS.retries }),
140
154
  ...(isSupportProject ? {} : { testMatch: exactTaskMatch }),
141
- use: normalizeUse(project.use, "Playwright config.projects[" + index + "].use", baseSandbox),
155
+ use: normalizeUse(project.use, "Playwright config.projects[" + index + "].use", baseSandbox, base.use?.timezoneId),
142
156
  outputDir: OPTIONS.outputDirectory,
143
157
  };
144
158
  }
@@ -154,6 +168,7 @@ const supportNames = projects === null ? new Set() : supportProjectNames(project
154
168
 
155
169
  export default {
156
170
  ...base,
171
+ ...(OPTIONS.retries === null ? {} : { retries: OPTIONS.retries }),
157
172
  testDir: base.testDir === undefined ? OPTIONS.rootDirectory : absolutePath(base.testDir),
158
173
  ...(projects === null ? { testMatch: exactTaskMatch } : {}),
159
174
  ...(base.snapshotDir === undefined ? {} : { snapshotDir: absolutePath(base.snapshotDir) }),
@@ -182,4 +197,4 @@ export default {
182
197
  fullyParallel: false,
183
198
  workers: 1,
184
199
  };
185
- `}export function playwrightExactFileFilter(e){return`^${m(e)}$`}function m(e){return e.split(/[\\/]+/u).map(r=>r.replace(/[.*+?^${}()|[\]\\]/gu,"\\$&")).join("[\\\\/]")}export function playwrightFileSuiteGrep(e){const r=e.split(/[\\/]+/u).filter(a=>a!=="").map(a=>a.replace(/[.*+?^${}()|[\]\\]/gu,"\\$&")),t=r.pop()??"";return`(?:^| )(?:\\.\\.[\\\\/])*${r.reduce((a,l)=>`(?:${a}${l}[\\\\/])?`,"")}${t}(?: |$)`}async function D(e,r){if(!Object.hasOwn(r.supportModules,"playwrightConfig"))return null;const t=r.supportModules.playwrightConfig;if(!o.isAbsolute(t))throw new Error("supportModules.playwrightConfig must be an absolute path");return O(e,t,"supportModules.playwrightConfig")}function F(e){const r=Object.keys(e).filter(t=>t!=="playwrightConfig");if(r.length>0)throw new Error(`Playwright engine does not support support module(s): ${r.join(", ")}`)}async function h(e,r){const t=await s.open(e,"wx",384);try{await t.writeFile(r,"utf8")}finally{await t.close()}}
200
+ `}export function playwrightExactFileFilter(e){return`^${m(e)}$`}function m(e){return e.split(/[\\/]+/u).map(r=>r.replace(/[.*+?^${}()|[\]\\]/gu,"\\$&")).join("[\\\\/]")}export function playwrightFileSuiteGrep(e){const r=e.split(/[\\/]+/u).filter(a=>a!=="").map(a=>a.replace(/[.*+?^${}()|[\]\\]/gu,"\\$&")),t=r.pop()??"";return`(?:^| )(?:\\.\\.[\\\\/])*${r.reduce((a,l)=>`(?:${a}${l}[\\\\/])?`,"")}${t}(?: |$)`}async function I(e,r){if(!Object.hasOwn(r.supportModules,"playwrightConfig"))return null;const t=r.supportModules.playwrightConfig;if(!o.isAbsolute(t))throw new Error("supportModules.playwrightConfig must be an absolute path");return S(e,t,"supportModules.playwrightConfig")}function D(e){const r=Object.keys(e).filter(t=>t!=="playwrightConfig");if(r.length>0)throw new Error(`Playwright engine does not support support module(s): ${r.join(", ")}`)}async function h(e,r){const t=await s.open(e,"wx",384);try{await t.writeFile(r,"utf8")}finally{await t.close()}}
@@ -1 +1 @@
1
- import c from"node:fs/promises";import P from"node:os";import i from"node:path";import{toError as w}from"../kernel/error-serialization.js";import{isUnknownRecord as N}from"../kernel/unknown-values.js";import{captureProcessCreationIdentity as T,observeProcessCreationIdentity as O}from"./process-identity.js";const y="testkit-engine-",v=".testkit-owner.json",E=new Set;export async function createInvocationProcessEnvironment(t,r=process.env,e=process.platform){return h(t,i.join(t,"home"),i.join(t,"tmp"),!0,r,e)}export async function createNativeInvocationProcessEnvironment(t,r={}){const e=r.platform??process.platform,s=r.ambientEnvironment??process.env,n=await g(r.nativeTemporaryRootDirectory??I(s,e)),u=await R(e);E.has(n)||(E.add(n),await b(n,e,u));let o=null;try{const a=await c.mkdtemp(i.join(n,y));return o=a,e!=="win32"&&await c.chmod(a,448),o=await j(a,n,e),await c.writeFile(i.join(o,v),JSON.stringify({pid:process.pid,pidNamespace:u,processIdentity:T(process.pid)}),{flag:"wx",mode:384}),await h(t,i.join(o,"home"),o,!1,s,e)}catch(a){if(o===null)throw a;try{await c.rm(o,{force:!0,recursive:!0})}catch(l){throw new AggregateError([w(a),w(l)],`Could not initialize or remove native invocation temporary directory ${o}`,{cause:l})}throw a}}async function h(t,r,e,s,n,u){if(!i.isAbsolute(t))throw new Error("Engine invocation directory must be absolute");if(!i.isAbsolute(e))throw new Error("Engine invocation temporary directory must be absolute");const o=i.join(t,"cache"),a=i.join(t,"config"),l=i.join(t,"data"),f=i.join(t,"local-app-data"),m=i.join(t,"app-data");await Promise.all([r,...s?[e]:[],o,a,l,f,m].map(A=>c.mkdir(A,{mode:448})));const p={APPDATA:m,HOME:r,LOCALAPPDATA:f,TEMP:e,TMP:e,TMPDIR:e,USERPROFILE:r,XDG_CACHE_HOME:o,XDG_CONFIG_HOME:a,XDG_DATA_HOME:l};return d(p,n,"LANG"),d(p,n,"LC_ALL"),u==="win32"&&(p.PATH="",d(p,n,"ComSpec"),d(p,n,"SystemRoot")),Object.freeze({environment:Object.freeze(p),homeDirectory:r,temporaryDirectory:e})}async function R(t){if(t!=="linux")return null;try{return await c.readlink("/proc/self/ns/pid")}catch{return null}}async function b(t,r,e){if(r==="linux"&&e===null)return;let s;try{s=await c.readdir(t)}catch{return}for(const n of s.filter(u=>u.startsWith(y))){const u=i.join(t,n);try{const o=await c.lstat(u);if(!o.isDirectory()||o.isSymbolicLink()||r!=="win32"&&(o.uid!==process.getuid?.()||(o.mode&63)!==0))continue;const a=JSON.parse(await c.readFile(i.join(u,v),"utf8"));if(!N(a)||a.pidNamespace!==e||O(a.pid,a.processIdentity)!=="different")continue;await c.rm(u,{force:!0,recursive:!0})}catch{continue}}}async function g(t){if(!i.isAbsolute(t))throw new Error("Native invocation temporary root must be absolute");const r=await c.realpath(t);if(!(await c.stat(r)).isDirectory())throw new Error("Native invocation temporary root must be a directory");return r}async function j(t,r,e){const s=await c.lstat(t);if(!s.isDirectory()||s.isSymbolicLink())throw new Error("Native invocation temporary directory must be a non-symbolic-link directory");if(e!=="win32"&&(s.mode&63)!==0)throw new Error("Native invocation temporary directory must not grant group or other access");const n=await c.realpath(t);if(i.dirname(n)!==r)throw new Error("Native invocation temporary directory escaped its operating-system root");return n}function I(t,r){if(r!=="win32")return"/tmp";const e=t.TEMP??t.TMP;return e===void 0||e.length===0?P.tmpdir():e}function d(t,r,e){const s=r[e];s!==void 0&&(t[e]=s)}
1
+ import c from"node:fs/promises";import P from"node:os";import n from"node:path";import{toError as w}from"../kernel/error-serialization.js";import{isUnknownRecord as T}from"../kernel/unknown-values.js";import{captureProcessCreationIdentity as N,observeProcessCreationIdentity as O}from"./process-identity.js";const y="testkit-engine-",v=".testkit-owner.json",E=new Set;export async function createInvocationProcessEnvironment(t,r=process.env,e=process.platform){return h(t,n.join(t,"home"),n.join(t,"tmp"),!0,r,e)}export async function createNativeInvocationProcessEnvironment(t,r={}){const e=r.platform??process.platform,s=r.ambientEnvironment??process.env,o=await j(r.nativeTemporaryRootDirectory??I(s,e)),u=await R(e);E.has(o)||(E.add(o),await g(o,e,u));let i=null;try{const a=await c.mkdtemp(n.join(o,y));return i=a,e!=="win32"&&await c.chmod(a,448),i=await b(a,o,e),await c.writeFile(n.join(i,v),JSON.stringify({pid:process.pid,pidNamespace:u,processIdentity:N(process.pid)}),{flag:"wx",mode:384}),await h(t,n.join(i,"home"),i,!1,s,e)}catch(a){if(i===null)throw a;try{await c.rm(i,{force:!0,recursive:!0})}catch(l){throw new AggregateError([w(a),w(l)],`Could not initialize or remove native invocation temporary directory ${i}`,{cause:l})}throw a}}async function h(t,r,e,s,o,u){if(!n.isAbsolute(t))throw new Error("Engine invocation directory must be absolute");if(!n.isAbsolute(e))throw new Error("Engine invocation temporary directory must be absolute");const i=n.join(t,"cache"),a=n.join(t,"config"),l=n.join(t,"data"),f=n.join(t,"local-app-data"),m=n.join(t,"app-data");await Promise.all([r,...s?[e]:[],i,a,l,f,m].map(A=>c.mkdir(A,{mode:448})));const p={APPDATA:m,HOME:r,LOCALAPPDATA:f,TEMP:e,TMP:e,TMPDIR:e,USERPROFILE:r,XDG_CACHE_HOME:i,XDG_CONFIG_HOME:a,XDG_DATA_HOME:l};return d(p,o,"LANG"),d(p,o,"LC_ALL"),d(p,o,"TZ"),u==="win32"&&(p.PATH="",d(p,o,"ComSpec"),d(p,o,"SystemRoot")),Object.freeze({environment:Object.freeze(p),homeDirectory:r,temporaryDirectory:e})}async function R(t){if(t!=="linux")return null;try{return await c.readlink("/proc/self/ns/pid")}catch{return null}}async function g(t,r,e){if(r==="linux"&&e===null)return;let s;try{s=await c.readdir(t)}catch{return}for(const o of s.filter(u=>u.startsWith(y))){const u=n.join(t,o);try{const i=await c.lstat(u);if(!i.isDirectory()||i.isSymbolicLink()||r!=="win32"&&(i.uid!==process.getuid?.()||(i.mode&63)!==0))continue;const a=JSON.parse(await c.readFile(n.join(u,v),"utf8"));if(!T(a)||a.pidNamespace!==e||O(a.pid,a.processIdentity)!=="different")continue;await c.rm(u,{force:!0,recursive:!0})}catch{continue}}}async function j(t){if(!n.isAbsolute(t))throw new Error("Native invocation temporary root must be absolute");const r=await c.realpath(t);if(!(await c.stat(r)).isDirectory())throw new Error("Native invocation temporary root must be a directory");return r}async function b(t,r,e){const s=await c.lstat(t);if(!s.isDirectory()||s.isSymbolicLink())throw new Error("Native invocation temporary directory must be a non-symbolic-link directory");if(e!=="win32"&&(s.mode&63)!==0)throw new Error("Native invocation temporary directory must not grant group or other access");const o=await c.realpath(t);if(n.dirname(o)!==r)throw new Error("Native invocation temporary directory escaped its operating-system root");return o}function I(t,r){if(r!=="win32")return"/tmp";const e=t.TEMP??t.TMP;return e===void 0||e.length===0?P.tmpdir():e}function d(t,r,e){const s=r[e];s!==void 0&&(t[e]=s)}
@@ -3,7 +3,7 @@
3
3
  "entrypoint": "dist/process/process.js",
4
4
  "sourceSha256": "e7301ca010796bb92d983c039db47f36e155c26ccfb42ff2d057d47addab3121",
5
5
  "bundleSha256": "db5da25a23e4577451c0c047360de70af8abbd63be686dd7b97d65486cbba3b8",
6
- "packageLockSha256": "41f889dea57e892f03549d17c252c8cd601b637210d1c9fbb5ff095a4f34f506",
6
+ "packageLockSha256": "02f00c22ea8dc6e37ae37c2d18d9e405c516ce692a9f48d7186a65d64ae542f1",
7
7
  "esbuildVersion": "0.28.1",
8
8
  "options": {
9
9
  "format": "esm",
@@ -1,7 +1,7 @@
1
- import{errorMessage as M}from"../kernel/error-serialization.js";import{requireManagedHostCommandSuccess as d}from"../process/index.js";import{REPOSITORY_COMMAND_GUARD_ARGS as $,runManagedGitCommand as u}from"./managed-git.js";const A=/^([0-9a-f]{40}|[0-9a-f]{64}) \d+ \d+(?: \d+)?$/u,x=/^committer-time (\d+)$/u,C=/^([0-9a-f]{40}|[0-9a-f]{64})\trefs\/heads\/(.+)$/u,p=50;export async function findIntroducingCommit(t,n,s,e){const r=`${String(e[0])},${String(e[1])}`,i=await u(t,["blame","-w","-M","--porcelain","-L",r,n,"--",s]);if(i.exitCode!==0)throw new Error(`Git cannot blame lines ${String(e[0])} to ${String(e[1])} of ${s} at ${n}: ${H(i.stderr)}`);const f=newestBlamedCommit(parseBlame(i.stdout));if(f===null)throw new Error(`Git blamed no lines of ${s} at ${n}`);return f.boundary&&await E(t)?{status:"outside-history"}:{commit:f.commit,status:"found"}}export function parseBlame(t){const n=new Map;let s=null;for(const e of t.split(`
2
- `)){if(e.startsWith(" "))continue;const r=A.exec(e);if(r!==null){s=n.get(r[1])??{boundary:!1,committedAt:0},n.set(r[1],s);continue}if(s===null)continue;const i=x.exec(e);i!==null?s.committedAt=Number(i[1]):e==="boundary"&&(s.boundary=!0)}return[...n].map(([e,r])=>({commit:e,...r}))}export function newestBlamedCommit(t){let n=null;for(const s of t)(n===null||s.committedAt>n.committedAt)&&(n=s);return n}export async function listRemoteRefsContaining(t,n){return d("git for-each-ref",await u(t,["for-each-ref","--format=%(refname)","--contains",n,"refs/remotes"])).stdout.split(`
3
- `).map(r=>r.trim()).filter(r=>r.length>0&&!r.endsWith("/HEAD"))}export async function findRemoteBranchHolding(t,n,s=process.env){const e=await listRemoteRefsContaining(t,n);if(e.length===0)return{status:"missing"};const r=d("git remote",await u(t,["remote"])).stdout.split(`
4
- `).map(a=>a.trim()).filter(a=>a.length>0).sort((a,l)=>l.length-a.length),i=new Map;for(const a of e){const l=a.slice(13),o=r.find(m=>l.startsWith(`${m}/`));o!==void 0&&i.set(o,(i.get(o)??new Set).add(l.slice(o.length+1)))}const f={env:{...s,GIT_TERMINAL_PROMPT:"0"},shallow:await E(t)},c=[];for(const[a,l]of i){const o=await T(t,a,l,n,f);if(o.status==="found")return o;o.status==="unknown"&&c.push(o)}return c.length===0?{status:"missing"}:{problems:c.flatMap(a=>a.problems),shallow:c.some(a=>a.shallow),status:"unknown"}}async function T(t,n,s,e,r){const i=await y(t,["ls-remote","--heads",n],r.env);if(typeof i=="string")return{problems:[`Git could not list the branches on ${n}: ${i}`],shallow:!1,status:"unknown"};const f=_(i.stdout);let c=!1;for(const a of[f.filter(([l])=>s.has(l)),f.filter(([l])=>!s.has(l))]){const l=[];for(const[,o]of a){const m=await b(t,o,e);if(m==="yes")return{status:"found"};m==="absent"?l.push(o):c=!0}for(let o=0;o<l.length;o+=p){const m=l.slice(o,o+p),h=await y(t,["fetch","--quiet","--no-tags","--no-write-fetch-head",n,...m],r.env);if(typeof h=="string")return{problems:[`Git could not fetch the branch tips from ${n}: ${h}`],shallow:r.shallow&&c,status:"unknown"};for(const g of m){const w=await b(t,g,e);if(w==="yes")return{status:"found"};c||=w==="no"}}}return r.shallow&&c?{problems:[],shallow:!0,status:"unknown"}:{status:"missing"}}async function y(t,n,s){try{const e=await u(t,n,{env:s});return e.exitCode===0?e:S(e.stderr)}catch(e){return M(e)}}function _(t){return t.split(`
5
- `).flatMap(n=>{const s=C.exec(n.trim());return s===null?[]:[[s[2],s[1]]]})}async function b(t,n,s){const e=await u(t,["merge-base","--is-ancestor",s,n]);return e.exitCode===0?"yes":e.exitCode===1?"no":"absent"}export async function listUncommittedTrackedFiles(t){const n=[...$,"diff","--name-only","-z","HEAD","--"];return d("git diff",await u(t,n)).stdout.split("\0").filter(e=>e.length>0)}export async function listFilesChangedSince(t,n){if((await u(t,["merge-base","--is-ancestor",n,"HEAD"])).exitCode!==0)return null;const e=["diff","--name-only","--diff-filter=d","-z",`${n}..HEAD`,"--"];return d("git diff",await u(t,e)).stdout.split("\0").filter(i=>i.length>0)}async function E(t){return d("git rev-parse",await u(t,["rev-parse","--is-shallow-repository"])).stdout.trim()==="true"}function H(t){return t.trim().split(`
1
+ import{errorMessage as $}from"../kernel/error-serialization.js";import{requireManagedHostCommandSuccess as d}from"../process/index.js";import{REPOSITORY_COMMAND_GUARD_ARGS as p,runManagedGitCommand as l}from"./managed-git.js";const x=/^([0-9a-f]{40}|[0-9a-f]{64}) \d+ \d+(?: \d+)?$/u,A=/^committer-time (\d+)$/u,C=/^([0-9a-f]{40}|[0-9a-f]{64})\trefs\/heads\/(.+)$/u,y=50;export async function findIntroducingCommit(t,e,s,n){const r=`${String(n[0])},${String(n[1])}`,i=await l(t,["blame","-w","-M","--porcelain","-L",r,e,"--",s]);if(i.exitCode!==0)throw new Error(`Git cannot blame lines ${String(n[0])} to ${String(n[1])} of ${s} at ${e}: ${H(i.stderr)}`);const f=newestBlamedCommit(parseBlame(i.stdout));if(f===null)throw new Error(`Git blamed no lines of ${s} at ${e}`);return f.boundary&&await M(t)?{status:"outside-history"}:{commit:f.commit,status:"found"}}export function parseBlame(t){const e=new Map;let s=null;for(const n of t.split(`
2
+ `)){if(n.startsWith(" "))continue;const r=x.exec(n);if(r!==null){s=e.get(r[1])??{boundary:!1,committedAt:0},e.set(r[1],s);continue}if(s===null)continue;const i=A.exec(n);i!==null?s.committedAt=Number(i[1]):n==="boundary"&&(s.boundary=!0)}return[...e].map(([n,r])=>({commit:n,...r}))}export function newestBlamedCommit(t){let e=null;for(const s of t)(e===null||s.committedAt>e.committedAt)&&(e=s);return e}export async function listRemoteRefsContaining(t,e){return d("git for-each-ref",await l(t,["for-each-ref","--format=%(refname)","--contains",e,"refs/remotes"])).stdout.split(`
3
+ `).map(r=>r.trim()).filter(r=>r.length>0&&!r.endsWith("/HEAD"))}export async function findRemoteBranchHolding(t,e,s=process.env){const n=await listRemoteRefsContaining(t,e);if(n.length===0)return{status:"missing"};const r=d("git remote",await l(t,["remote"])).stdout.split(`
4
+ `).map(a=>a.trim()).filter(a=>a.length>0).sort((a,c)=>c.length-a.length),i=new Map;for(const a of n){const c=a.slice(13),o=r.find(m=>c.startsWith(`${m}/`));o!==void 0&&i.set(o,(i.get(o)??new Set).add(c.slice(o.length+1)))}const f={env:{...s,GIT_TERMINAL_PROMPT:"0"},shallow:await M(t)},u=[];for(const[a,c]of i){const o=await T(t,a,c,e,f);if(o.status==="found")return o;o.status==="unknown"&&u.push(o)}return u.length===0?{status:"missing"}:{problems:u.flatMap(a=>a.problems),shallow:u.some(a=>a.shallow),status:"unknown"}}async function T(t,e,s,n,r){const i=await b(t,["ls-remote","--heads",e],r.env);if(typeof i=="string")return{problems:[`Git could not list the branches on ${e}: ${i}`],shallow:!1,status:"unknown"};const f=_(i.stdout);let u=!1;for(const a of[f.filter(([c])=>s.has(c)),f.filter(([c])=>!s.has(c))]){const c=[];for(const[,o]of a){const m=await E(t,o,n);if(m==="yes")return{status:"found"};m==="absent"?c.push(o):u=!0}for(let o=0;o<c.length;o+=y){const m=c.slice(o,o+y),h=await b(t,["fetch","--quiet","--no-tags","--no-write-fetch-head",e,...m],r.env);if(typeof h=="string")return{problems:[`Git could not fetch the branch tips from ${e}: ${h}`],shallow:r.shallow&&u,status:"unknown"};for(const g of m){const w=await E(t,g,n);if(w==="yes")return{status:"found"};u||=w==="no"}}}return r.shallow&&u?{problems:[],shallow:!0,status:"unknown"}:{status:"missing"}}async function b(t,e,s){try{const n=await l(t,e,{env:s});return n.exitCode===0?n:S(n.stderr)}catch(n){return $(n)}}function _(t){return t.split(`
5
+ `).flatMap(e=>{const s=C.exec(e.trim());return s===null?[]:[[s[2],s[1]]]})}async function E(t,e,s){const n=await l(t,["merge-base","--is-ancestor",s,e]);return n.exitCode===0?"yes":n.exitCode===1?"no":"absent"}export async function isAncestor(t,e,s){const n=[...p,"merge-base","--is-ancestor",e,s];return(await l(t,n)).exitCode===0}export async function listUncommittedTrackedFiles(t){const e=[...p,"diff","--name-only","-z","HEAD","--"];return d("git diff",await l(t,e)).stdout.split("\0").filter(n=>n.length>0)}export async function listFilesChangedSince(t,e){if((await l(t,["merge-base","--is-ancestor",e,"HEAD"])).exitCode!==0)return null;const n=["diff","--name-only","--diff-filter=d","-z",`${e}..HEAD`,"--"];return d("git diff",await l(t,n)).stdout.split("\0").filter(i=>i.length>0)}async function M(t){return d("git rev-parse",await l(t,["rev-parse","--is-shallow-repository"])).stdout.trim()==="true"}function H(t){return t.trim().split(`
6
6
  `).at(-1)??""}function S(t){return t.trim().split(`
7
7
  `)[0]??""}
@@ -386,6 +386,8 @@ export type VitestSelection = Readonly<{
386
386
  host: Readonly<Record<string, string>>;
387
387
  project: null | string;
388
388
  runner: "auto" | "managed" | "repo";
389
+ shuffle: null | number;
390
+ retry?: number;
389
391
  }>;
390
392
  export interface DiscoverySummary {
391
393
  activeFiles: number;
@@ -85,10 +85,14 @@ Fault: `src/checkout/refunds.ts`, lines 41 to 58.
85
85
 
86
86
  The reproduce command runs from the repository root. When `testkit.config.ts` is in a subdirectory, the command carries `--dir <directory>` and the `--file` path is relative to that directory. The `Test:` and `Fault:` lines always give paths from the repository root.
87
87
 
88
+ When the bug was recorded with `--vitest-shuffle <seed>`, `--timezone <zone>`, or `--retries <n>`, the command carries the same flag, after any Vitest selectors, so it runs the tests in the order, the time zone, or with the retries that make them fail. The facts block does not change.
89
+
88
90
  When `testkit bug file` ran the test against [supplied values](../README.md#run-against-a-supplied-environment), the command ends with the `--base-url`, `--endpoint`, `--env-from`, and `--environment-file` flags it was given, as given. They hold no raw secret: `--env-from` names host variables, the environment file stays outside the issue, and `testkit bug file` accepts a URL only without credentials, a query, or a fragment, and an endpoint name only of letters, digits, dot, underscore, and hyphen. It refuses `--database` and `--env`, whose values the command would otherwise carry, and a relative `--environment-file` unless it runs from the repository root. It checks these values, and that every `--env-from` host variable is set, before it contacts the tracker. The command is written for a POSIX shell: flags and simple mappings appear unquoted, and any argument that needs quoting, such as a URL with brackets or a path with a space, is quoted for that shell. The facts block does not change.
89
91
 
90
92
  A fault on one line reads `line 41`.
91
93
 
94
+ When `testkit bug bisect` found where the test starts to fail, the `Fault:` line is followed by ``The test passes at `<good>` and fails from its child `<bad>`.``, or, for a range, ``The test passes at `<good>` and fails at `<bad>`. The fault came in between them.``, with full commit ids. A line ``Testkit ran it at: `<commit>` fails, `<commit>` passes, ….`` follows, with 12-character commit ids and `gave no verdict` for a skipped commit, and the facts block carries `bisect`. Testkit leaves all three out when the test, its title, or its flags changed since the bisect, or when `bisect.bad` isn't an ancestor of `foundAt`.
95
+
92
96
  Testkit redacts the secrets it recognises in the title, in every section, and in `test.title`. It does so when it records the bug and again when it files the issue. `testkit run` applies the same rules when it stores a run result. Testkit recognises these forms.
93
97
 
94
98
  - The value of a `KEY=value` assignment whose key names a secret.
@@ -142,29 +146,34 @@ A body with no start marker has no facts. A body with a start marker and no read
142
146
 
143
147
  All paths in the block use `/` and are relative to the repository root.
144
148
 
145
- | Field | Type | Written | Meaning |
146
- | ------------------- | -------------------------------------------------------- | -------------------------------------- | --------------------------------------------------------------- |
147
- | `v` | `1` | Always | The format version. |
148
- | `cause` | Cause id | Always | The kind of mistake in the code. |
149
- | `impact` | Impact id | Always | What is at stake for the business. |
150
- | `confidence.cause` | `high` or `low` | Always | The finder's confidence in `cause`. |
151
- | `confidence.impact` | `high` or `low` | Always | The finder's confidence in `impact`. |
152
- | `escape` | Escape id | Always | Why the existing tests did not catch the bug. |
153
- | `caughtBy` | `campaign` or `manual` | Always | `campaign` when a campaign was active at record time. |
154
- | `campaign` | Campaign id | With `caughtBy: "campaign"` | The campaign that caught the bug. |
155
- | `testType` | `ui`, `e2e`, `int`, `contract`, `data`, `dal`, or `load` | When the test file's name gives a type | The type of the regression test, from its file suffix. |
156
- | `test.file` | Path | Always | The regression test file. |
157
- | `test.title` | String | When the issue covers one test | The failure's title. See [Known failures](#known-failures). |
158
- | `foundAt` | Full commit id | Always | The commit the test failed at. |
159
- | `fault.path` | Path | Always | The source file that holds the mistake. |
160
- | `fault.lines` | `[start, end]` | Always | The first and last faulty line at `foundAt`, counted from 1. |
161
- | `area.id` | Area id | When the area map claims `fault.path` | The area's key in `areas`. |
162
- | `area.name` | String | With `area.id` | The area's name at filing time. |
163
- | `area.share` | Number from 0 to 1 | With `area.id` | The area's share of the repository's source bytes at `foundAt`. |
164
- | `introducedBy` | Full commit id | When the blame resolves | The newest commit among the fault lines at `foundAt`. |
165
- | `introducedMethod` | `blame` | With `introducedBy` | How Testkit found `introducedBy`. |
166
-
167
- Testkit writes every field it can resolve. A reader accepts a version 1 block that lacks `escape`, `campaign`, `testType`, `test.title`, `fault`, `fault.lines`, `area`, `introducedBy`, or `introducedMethod`, because issues that predate the CLI were given facts by hand and a test file's name may give no type. A reader ignores fields it does not know.
149
+ | Field | Type | Written | Meaning |
150
+ | ------------------- | -------------------------------------------------------- | -------------------------------------- | ------------------------------------------------------------------- |
151
+ | `v` | `1` | Always | The format version. |
152
+ | `cause` | Cause id | Always | The kind of mistake in the code. |
153
+ | `impact` | Impact id | Always | What is at stake for the business. |
154
+ | `confidence.cause` | `high` or `low` | Always | The finder's confidence in `cause`. |
155
+ | `confidence.impact` | `high` or `low` | Always | The finder's confidence in `impact`. |
156
+ | `escape` | Escape id | Always | Why the existing tests did not catch the bug. |
157
+ | `caughtBy` | `campaign` or `manual` | Always | `campaign` when a campaign was active at record time. |
158
+ | `campaign` | Campaign id | With `caughtBy: "campaign"` | The campaign that caught the bug. |
159
+ | `testType` | `ui`, `e2e`, `int`, `contract`, `data`, `dal`, or `load` | When the test file's name gives a type | The type of the regression test, from its file suffix. |
160
+ | `test.file` | Path | Always | The regression test file. |
161
+ | `test.title` | String | When the issue covers one test | The failure's title. See [Known failures](#known-failures). |
162
+ | `foundAt` | Full commit id | Always | The commit the test failed at. |
163
+ | `fault.path` | Path | Always | The source file that holds the mistake. |
164
+ | `fault.lines` | `[start, end]` | Always | The first and last faulty line at `foundAt`, counted from 1. |
165
+ | `area.id` | Area id | When the area map claims `fault.path` | The area's key in `areas`. |
166
+ | `area.name` | String | With `area.id` | The area's name at filing time. |
167
+ | `area.share` | Number from 0 to 1 | With `area.id` | The area's share of the repository's source bytes at `foundAt`. |
168
+ | `introducedBy` | Full commit id | When the blame resolves | The newest commit among the fault lines at `foundAt`. |
169
+ | `introducedMethod` | `blame` | With `introducedBy` | How Testkit found `introducedBy`. |
170
+ | `bisect.good` | Full commit id | After `testkit bug bisect` finds it | The newest commit, on the first-parent line, where the test passes. |
171
+ | `bisect.bad` | Full commit id | With `bisect.good` | The oldest commit after `bisect.good` where the test fails. |
172
+ | `bisect.exact` | `true` or `false` | With `bisect.good` | Whether `bisect.good` is the first parent of `bisect.bad`. |
173
+
174
+ `testkit bug bisect` runs the regression test at commits on the first-parent line of `foundAt`. When `bisect.exact` is `true`, `bisect.bad` is the commit that made the test fail. Otherwise Testkit skipped the commits between them or ran out of probes, and the fault came in among them. `introducedBy` still says what `git blame` found, so the two can disagree, for example when a later commit only reformatted the faulty line.
175
+
176
+ Testkit writes every field it can resolve. A reader accepts a version 1 block that lacks `escape`, `campaign`, `testType`, `test.title`, `fault`, `fault.lines`, `area`, `introducedBy`, `introducedMethod`, or `bisect`, because issues that predate the CLI were given facts by hand and a test file's name may give no type. A reader ignores fields it does not know.
168
177
 
169
178
  A reader accepts every `testType` in the list above, including a type that Testkit no longer writes. Issues keep the type they were filed with, so the list never loses a value. A new type is a format change.
170
179
 
@@ -1,3 +1,67 @@
1
+ # 0.6.6
2
+
3
+ ## Data-flow evidence preserves own JSON properties
4
+
5
+ `dataFlow.record()` now preserves an own `__proto__` property in saved JSON
6
+ evidence, including in nested objects and arrays. Recording and artifact
7
+ validation previously dropped that property. Sensitive values remain redacted,
8
+ and recording leaves the caller's data unchanged.
9
+
10
+ # 0.6.5
11
+
12
+ ## `testkit bug bisect` finds the commit that broke a recorded test
13
+
14
+ `testkit bug bisect <id>` runs a recorded bug's regression test at older
15
+ commits, each in a temporary copy of the checkout with the test added, and
16
+ finds the last commit where it passes and the first where it fails. It checks
17
+ the commit that `git blame` names first, so a correct blame costs two runs,
18
+ and searches the first-parent line when the blame is wrong, for example when a
19
+ later commit only reformatted the faulty line. Every commit it runs the
20
+ test at, the checked-out one included, gets its own dependencies from a
21
+ frozen install of its lockfiles, with the checkout's ignored `.env` files and
22
+ package manager settings in place. The draft keeps both commits,
23
+ and `testkit bug file` adds them to the issue and to the facts block as
24
+ `bisect.good`, `bisect.bad`, and `bisect.exact`, with the list of commits
25
+ it ran the test at. Readers that don't know `bisect` ignore it, as
26
+ `docs/bug-records.md` allows. See
27
+ [Find the commit that broke it](../README.md#find-the-commit-that-broke-it).
28
+
29
+ ## `testkit shake` finds the condition that makes a test flaky
30
+
31
+ `testkit shake --file <test>` reruns test files under conditions that make
32
+ flaky tests fail: a seeded random order, copies of each file running at the same time
33
+ on the same services and database, a time zone 14 hours ahead of UTC and one
34
+ 11 hours behind, and every CPU core busy. Each condition runs `--runs` times
35
+ beside a control. Fisher's exact test against the control gives each condition
36
+ a verdict such as `reproduces` or `raises failures`, with p corrected for the
37
+ number of tests compared and a 95% Wilson interval for each failure rate. The
38
+ report gives a command that reproduces each failure, and Testkit runs it once
39
+ more to say whether the failure came back. The run bundles of failed runs stay
40
+ under `.testkit/shake/<id>/runs/`. `--since <ref>` shakes the test files
41
+ changed since the ref, so a job can check new tests before they merge. See
42
+ [Shake out flaky tests](../README.md#shake-out-flaky-tests).
43
+
44
+ `testkit run` takes `--vitest-shuffle <seed>`, which shuffles plain Vitest
45
+ files and their tests in the order that seed gives, and `--timezone <zone>`,
46
+ which sets `TZ` for the tests, the services Testkit starts, and a `ui` test's
47
+ browser. `--retries <n>` overrides the retries that the Vitest and Playwright
48
+ configs set, including in each project. `testkit bug record` takes all three,
49
+ and `testkit bug file` and the issue's reproduce command carry them. Testkit now passes `TZ` from its own
50
+ environment to the test processes it starts, as it passes `LANG` and `LC_ALL`.
51
+
52
+ ## `testkit prove` checks that the tests notice a change
53
+
54
+ `testkit prove --since <ref>` mutates the product code changed since the ref,
55
+ one place at a time, and runs the selected tests against each mutant in a
56
+ temporary copy of the checkout. Testkit starts the services for every run, so
57
+ an HTTP test of a service or a `ui` test of the page a service sends can catch
58
+ a mutant. Every mutated place records when a test process, a service, or a
59
+ browser page runs it, so a mutant no test noticed reads as `survived` only when
60
+ its code ran, and as `not-reached` when nothing ran it, such as code a service
61
+ serves from an old build. `--mutants` runs mutants an agent wrote, and
62
+ `--fail-under` fails a job below a mutation score. See
63
+ [Prove the tests catch a change](../README.md#prove-the-tests-catch-a-change).
64
+
1
65
  # 0.6.4
2
66
 
3
67
  ## Testkit supports Node 26 LTS
package/package.json CHANGED
@@ -1,7 +1,22 @@
1
1
  {
2
2
  "name": "@gfdelarue/testkit",
3
- "version": "0.6.4",
4
- "description": "Test orchestration platform for product acceptance and QA workflows",
3
+ "version": "0.6.6",
4
+ "description": "Test runner and CLI for coding agents and CI. Your agent hunts bugs, and Testkit files each one in GitHub or Gitea with a failing test.",
5
+ "keywords": [
6
+ "testing",
7
+ "test-runner",
8
+ "cli",
9
+ "ai",
10
+ "coding-agents",
11
+ "bug-hunting",
12
+ "regression-tests",
13
+ "vitest",
14
+ "playwright",
15
+ "ci",
16
+ "github",
17
+ "gitea"
18
+ ],
19
+ "homepage": "https://turnstone.cloud",
5
20
  "license": "SEE LICENSE IN LICENSE",
6
21
  "type": "module",
7
22
  "repository": {