@gfdelarue/testkit 0.6.5 → 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.
@@ -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,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"}
@@ -3,7 +3,7 @@
3
3
  "entrypoint": "dist/process/process.js",
4
4
  "sourceSha256": "e7301ca010796bb92d983c039db47f36e155c26ccfb42ff2d057d47addab3121",
5
5
  "bundleSha256": "db5da25a23e4577451c0c047360de70af8abbd63be686dd7b97d65486cbba3b8",
6
- "packageLockSha256": "ba434d693cbd53c0160909d2aa6c445a3e4649130c953a1f77244465d969a05c",
6
+ "packageLockSha256": "02f00c22ea8dc6e37ae37c2d18d9e405c516ce692a9f48d7186a65d64ae542f1",
7
7
  "esbuildVersion": "0.28.1",
8
8
  "options": {
9
9
  "format": "esm",
@@ -1,3 +1,12 @@
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
+
1
10
  # 0.6.5
2
11
 
3
12
  ## `testkit bug bisect` finds the commit that broke a recorded test
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gfdelarue/testkit",
3
- "version": "0.6.5",
3
+ "version": "0.6.6",
4
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
5
  "keywords": [
6
6
  "testing",