@gfdelarue/testkit 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/README.md +125 -0
  2. package/bin/testkit-windows-owner-arm64.exe +0 -0
  3. package/bin/testkit-windows-owner-x64.exe +0 -0
  4. package/dist/app/bugs/areas.d.ts +38 -0
  5. package/dist/app/bugs/areas.js +1 -0
  6. package/dist/app/bugs/assessment.d.ts +9 -0
  7. package/dist/app/bugs/assessment.js +1 -0
  8. package/dist/app/bugs/categories.d.ts +41 -0
  9. package/dist/app/bugs/categories.js +1 -0
  10. package/dist/app/bugs/ci-report.d.ts +21 -0
  11. package/dist/app/bugs/ci-report.js +2 -0
  12. package/dist/app/bugs/ci-summary.d.ts +13 -0
  13. package/dist/app/bugs/ci-summary.js +5 -0
  14. package/dist/app/bugs/draft.d.ts +70 -0
  15. package/dist/app/bugs/draft.js +4 -0
  16. package/dist/app/bugs/facts.d.ts +48 -0
  17. package/dist/app/bugs/facts.js +2 -0
  18. package/dist/app/bugs/file.d.ts +45 -0
  19. package/dist/app/bugs/file.js +1 -0
  20. package/dist/app/bugs/gate.d.ts +22 -0
  21. package/dist/app/bugs/gate.js +1 -0
  22. package/dist/app/bugs/guide.d.ts +20 -0
  23. package/dist/app/bugs/guide.js +8 -0
  24. package/dist/app/bugs/issue.d.ts +18 -0
  25. package/dist/app/bugs/issue.js +6 -0
  26. package/dist/app/bugs/known-failures.d.ts +49 -0
  27. package/dist/app/bugs/known-failures.js +1 -0
  28. package/dist/app/bugs/record.d.ts +14 -0
  29. package/dist/app/bugs/record.js +2 -0
  30. package/dist/app/bugs/run-reading.d.ts +51 -0
  31. package/dist/app/bugs/run-reading.js +1 -0
  32. package/dist/app/bugs/store.d.ts +34 -0
  33. package/dist/app/bugs/store.js +2 -0
  34. package/dist/app/campaigns/brief.d.ts +7 -0
  35. package/dist/app/campaigns/brief.js +10 -0
  36. package/dist/app/campaigns/campaign.d.ts +14 -0
  37. package/dist/app/campaigns/campaign.js +1 -0
  38. package/dist/app/campaigns/index.d.ts +38 -0
  39. package/dist/app/campaigns/index.js +2 -0
  40. package/dist/app/campaigns/store.d.ts +5 -0
  41. package/dist/app/campaigns/store.js +1 -0
  42. package/dist/app/campaigns/summary.d.ts +13 -0
  43. package/dist/app/campaigns/summary.js +8 -0
  44. package/dist/app/run/engine-execution.d.ts +1 -0
  45. package/dist/app/run/engine-execution.js +1 -1
  46. package/dist/app/run/failure-details.d.ts +1 -0
  47. package/dist/app/run/failure-details.js +1 -1
  48. package/dist/app/tracker/index.d.ts +8 -0
  49. package/dist/app/tracker/index.js +1 -0
  50. package/dist/app/tracker/remote.d.ts +13 -0
  51. package/dist/app/tracker/remote.js +1 -0
  52. package/dist/app/tracker/token.d.ts +7 -0
  53. package/dist/app/tracker/token.js +5 -0
  54. package/dist/cli/command-catalog.d.ts +26 -0
  55. package/dist/cli/command-catalog.js +1 -1
  56. package/dist/cli/command-registry.js +1 -1
  57. package/dist/cli/commands/bug/file.d.ts +16 -0
  58. package/dist/cli/commands/bug/file.js +2 -0
  59. package/dist/cli/commands/bug/guide.d.ts +5 -0
  60. package/dist/cli/commands/bug/guide.js +1 -0
  61. package/dist/cli/commands/bug/record.d.ts +9 -0
  62. package/dist/cli/commands/bug/record.js +2 -0
  63. package/dist/cli/commands/campaign/finish.d.ts +10 -0
  64. package/dist/cli/commands/campaign/finish.js +1 -0
  65. package/dist/cli/commands/campaign/start.d.ts +7 -0
  66. package/dist/cli/commands/campaign/start.js +1 -0
  67. package/dist/cli/commands/campaign/status.d.ts +13 -0
  68. package/dist/cli/commands/campaign/status.js +2 -0
  69. package/dist/cli/commands/ci/report.d.ts +15 -0
  70. package/dist/cli/commands/ci/report.js +2 -0
  71. package/dist/cli/commands/run.d.ts +1 -1
  72. package/dist/cli/commands/run.js +2 -1
  73. package/dist/cli/renderers/bug/text.d.ts +8 -0
  74. package/dist/cli/renderers/bug/text.js +1 -0
  75. package/dist/cli/renderers/campaign/text.d.ts +2 -0
  76. package/dist/cli/renderers/campaign/text.js +1 -0
  77. package/dist/cli/renderers/ci/text.d.ts +2 -0
  78. package/dist/cli/renderers/ci/text.js +1 -0
  79. package/dist/config/authoring/index.d.ts +10 -0
  80. package/dist/config/loader/bugs.d.ts +15 -0
  81. package/dist/config/loader/bugs.js +1 -0
  82. package/dist/engines/adapters/execution.d.ts +4 -0
  83. package/dist/engines/adapters/execution.js +1 -1
  84. package/dist/engines/k6/adapter/executor.js +1 -1
  85. package/dist/engines/k6/runtime/checks.js +1 -1
  86. package/dist/engines/k6/runtime/http-assertions.d.ts +2 -2
  87. package/dist/engines/k6/runtime/http-assertions.js +1 -1
  88. package/dist/engines/k6/runtime/types.d.ts +2 -0
  89. package/dist/engines/node-vitest/adapter/evidence.js +2 -2
  90. package/dist/engines/node-vitest/adapter/failure-details.d.ts +1 -0
  91. package/dist/engines/node-vitest/adapter/failure-details.js +2 -2
  92. package/dist/engines/playwright/adapter/adapter.js +1 -1
  93. package/dist/engines/playwright/adapter/attachments.d.ts +1 -0
  94. package/dist/engines/playwright/adapter/config.js +3 -3
  95. package/dist/engines/playwright/adapter/evidence.js +1 -1
  96. package/dist/engines/playwright/adapter/reporter.js +1 -1
  97. package/dist/kernel/redaction.d.ts +1 -0
  98. package/dist/kernel/redaction.js +1 -1
  99. package/dist/process/execa-bundle.json +1 -1
  100. package/dist/process/files/json-file.d.ts +1 -0
  101. package/dist/process/files/json-file.js +1 -1
  102. package/dist/repo/history.d.ts +17 -0
  103. package/dist/repo/history.js +4 -0
  104. package/dist/repo/tree-listing.d.ts +10 -0
  105. package/dist/repo/tree-listing.js +1 -0
  106. package/dist/tracker/gitea.d.ts +14 -0
  107. package/dist/tracker/gitea.js +1 -0
  108. package/dist/tracker/github.d.ts +14 -0
  109. package/dist/tracker/github.js +1 -0
  110. package/dist/tracker/http.d.ts +33 -0
  111. package/dist/tracker/http.js +1 -0
  112. package/dist/tracker/index.d.ts +4 -0
  113. package/dist/tracker/index.js +1 -0
  114. package/dist/tracker/redaction.d.ts +4 -0
  115. package/dist/tracker/redaction.js +2 -0
  116. package/dist/tracker/requests.d.ts +17 -0
  117. package/dist/tracker/requests.js +1 -0
  118. package/dist/tracker/responses.d.ts +24 -0
  119. package/dist/tracker/responses.js +1 -0
  120. package/dist/tracker/types.d.ts +51 -0
  121. package/dist/tracker/types.js +1 -0
  122. package/docs/bug-records.md +303 -0
  123. package/package.json +2 -1
  124. package/testkit-config-schema.md +50 -0
@@ -1 +1 @@
1
- import P from"node:fs/promises";import d from"node:path";import{evidenceArtifact as A,evidenceFileAccess as J,readStableJsonFile as V,snapshotStableEvidenceFile as z,stableEvidenceFromBuffer as K,writeOwnedEvidenceFile as D}from"../../adapters/evidence-files.js";import{materializePlaywrightFailureDetails as Q}from"./failure-details.js";import{isMissingFileError as Z,isPathWithin as _,nonNegativeInteger as v,nonNegativeNumber as B,optionalNonEmptyString as $,optionalNonNegativeInteger as C,portableRelativePath as Y,portableReportPath as H,requireNonEmptyString as b,requireRecord as S,toError as q}from"./evidence-validation.js";import{readNativeReport as tt}from"../../adapters/native-report.js";import{attachmentLimitError as et,attachmentIoError as it,MAX_ATTACHMENT_BYTES as G,MAX_TOTAL_ATTACHMENT_BYTES as rt}from"./attachments.js";const nt=64*1024*1024,W=1e4,F=512*1024*1024,at=2*1024*1024,X=128,st=".testkit-playwright-evidence",ot=/(?:^|\n)(?:Error:\s*)?No tests found(?:\.|\n|$)/u;class M extends Error{limit;constructor(t){super(gt(t)),this.limit=t,this.name="PlaywrightOutputArtifactLimitError"}}export async function collectPlaywrightEvidence(e,t=J){await P.mkdir(e.artifactDirectory,{recursive:!0});const c=await P.realpath(e.artifactDirectory),s=await P.realpath(e.rootDirectory),a=d.join(c,st);await P.mkdir(a,{mode:448,recursive:!1});const i=[],l=[];for(const o of["stdout","stderr"]){const g=e[o];if(g.length===0)continue;i.push({stream:o,text:g});const y=K(Buffer.from(g,"utf8")),E=await D(a,`${o}.log`,y.contents);l.push(A(E,y,"text/plain; charset=utf-8","engine.process-output",`playwright-${o}`,`${y.sizeBytes.toString()} bytes`))}let u=null,p=null,h=new Set,f=[];try{const o=await V(e.reportFile,nt,"Testkit native Playwright report",t);if(o.file===null)u=new Error("Playwright did not produce its Testkit native report");else{p=lt(tt(o.value,e.identity).result);const g=await D(a,"testkit-playwright-report.json",o.file.contents);l.push(A(g,o.file,"application/json","engine.testkit-playwright-report","testkit-playwright-report","Testkit native Playwright report")),i.push(...p.logs);const y=await Q(a,p.failureDetails);y!==null&&l.push(y);const E=await dt(c,a,s,p.attachments,t);l.push(...E.artifacts),h=E.sourcePaths,f=E.failures}}catch(o){u=q(o)}let r={artifacts:[],bytes:0,entries:0},n=null;try{r=await ht(c,a,e.outputDirectory,h,t),l.push(...r.artifacts)}catch(o){if(!(o instanceof M))throw o;n=o.limit,i.push({stream:"engine",text:o.message})}const m=ct(p,s,e.sourceFile),w=n??r;return{attachmentFailures:f,evidence:{artifacts:l,logs:i,metrics:[...Object.entries(p?.stats??{}).map(([o,g])=>({name:o==="duration"?"playwright.duration":`playwright.tests.${o}`,unit:o==="duration"?"milliseconds":"tests",value:g})),...["bytes","entries"].map(o=>({name:`playwright.output.${o}`,unit:o,value:w[o]}))],...p===null?{}:{native:{playwright:{attachmentFailures:f.map(o=>({...o})),specs:p.specs,stats:p.stats}}}},outputLimit:n,report:m,reportError:u}}function ct(e,t,c){if(e===null)return{failure:null,status:"failed"};const s=e.specs.filter((i,l)=>d.relative(d.normalize(c),e.sourceFiles[l])===""),a=e.failureMessages.length>0&&e.failureMessages.every(i=>ot.test(i));return e.stats.unexpected>0||e.failureMessages.length>0&&(!a||s.length>0)?{failure:{...e.failureMessages.some(i=>/browserType\.launch(?:PersistentContext)?:/u.test(i)&&/Executable doesn't exist|Host system is missing dependencies|error while loading shared libraries|Failed to launch/u.test(i))?{classification:"infra"}:{},code:"PLAYWRIGHT_SPECS_FAILED",details:{stats:e.stats,specs:e.specs.slice(0,50)},message:e.failureMessages.at(0)??`${e.stats.unexpected.toString()} Playwright test(s) failed`},status:"failed"}:s.length>0?{failure:null,status:s.every(i=>i.status==="skipped")?"skipped":"passed"}:{failure:{code:"PLAYWRIGHT_NO_TESTS",details:{file:Y(t,c),reportedSpecs:e.specs.slice(0,50),stats:e.stats},message:"Selected Playwright task did not execute any tests"},status:"failed"}}function lt(e){const t=S(e,"Playwright native report result"),c=mt(t.stats),s=N(t.errors,"errors"),a=[],i=[],l=[],u=new Set,p={expected:0,flaky:0,skipped:0,unexpected:0};for(const r of k(t.specs,"specs")){const n=S(r,"Playwright native report spec"),m=b(n.id,"Playwright native spec.id");if(u.has(m))throw new Error("Playwright native report contains a duplicate test identity");u.add(m);const w=b(n.title,"Playwright native spec.title"),o=N(n.parents,"spec.parents");o.length>0&&(o[0]=H(o[0]));const g=H(b(n.file,"Playwright native spec.file")),y=b(n.sourceFile,"Playwright native spec.sourceFile");if(!d.isAbsolute(y)&&!d.win32.isAbsolute(y))throw new Error("Playwright native spec.sourceFile must be absolute");const E=x(n.projectName,"spec.projectName"),T=n.status;if(T!=="expected"&&T!=="flaky"&&T!=="skipped"&&T!=="unexpected")throw new Error("Playwright native spec.status is invalid");p[T]+=1;const I=N(n.failureMessages,"spec.failureMessages"),L=n.failureStack===null?null:x(n.failureStack,"spec.failureStack"),j=C(n.line,"Playwright native spec.line"),O=C(n.column,"Playwright native spec.column");T==="unexpected"&&(s.push(...I),l.push({kind:"playwright-spec",key:w,title:w,count:1,message:I[0]??`Playwright reported "${w}" as an unexpected failure`,...o.length===0?{}:{suitePath:o},location:{path:g,...j===null?{}:{line:j},...O===null?{}:{column:O}},...L===null?{}:{stack:L}})),i.push(y),a.push({durationMs:B(n.durationMs,"Playwright native spec.durationMs"),retries:v(n.retries,"Playwright native spec.retries"),file:g,projectName:E,status:T,title:[...o,w].filter(Boolean).join(" > ")})}for(const r of["expected","flaky","skipped","unexpected"])if(c[r]!==p[r])throw new Error("Playwright native report stats do not match its cases");const h=k(t.logs,"logs").map(r=>{const n=S(r,"Playwright native report log");if(n.stream!=="stdout"&&n.stream!=="stderr")throw new Error("Playwright native report log stream is invalid");return{stream:n.stream,text:x(n.text,"log.text")}});return{attachments:k(t.attachments,"attachments").map((r,n)=>ut(r,n,u)),failureDetails:l,failureMessages:s,logs:wt(h),sourceFiles:i,specs:a,stats:c}}function k(e,t){if(!Array.isArray(e))throw new Error(`Playwright native report ${t} must be an array`);return e}function N(e,t){return k(e,t).map(c=>x(c,t))}function x(e,t){if(typeof e!="string")throw new Error(`Playwright native report ${t} must be a string`);return e}function ut(e,t,c){const s=S(e,`Playwright attachment[${t.toString()}]`),a=b(s.specId,"Playwright attachment.specId");if(!c.has(a))throw new Error("Playwright native attachment references an unknown test identity");const i=$(s.path),l=s.error===void 0?void 0:pt(s.error);if(s.body!==void 0||i===null&&l===void 0)throw new Error("Playwright attachment must contain a path or an error");return{contentType:$(s.contentType)??"application/octet-stream",name:$(s.name)??`attachment-${t.toString()}`,specId:a,...i===null?{}:{path:i},...l===void 0?{}:{error:l},retry:v(s.retry,"Playwright attachment.retry"),specTitle:b(s.specTitle,"Playwright attachment.specTitle")}}function pt(e){const t=S(e,"Playwright attachment.error");if(t.kind==="io")return{kind:"io",message:b(t.message,"Playwright attachment.error.message")};const c=t.kind;if(c!=="individual"&&c!=="total")throw new Error("Playwright attachment.error has an invalid kind");const s=c==="individual"?G:rt,a=v(t.observed,"Playwright attachment.error.observed");if(t.limit!==s||a<=s)throw new Error("Playwright attachment.error has an invalid limit");return{kind:c,limit:s,observed:a}}function R(e,t){const{name:c,specId:s,specTitle:a,retry:i}=e,l=t.kind==="io"?t.message:`exceeds its ${t.limit.toString()} byte ${t.kind} limit (observed ${t.observed.toString()} bytes)`;return{code:t.kind==="io"?"PLAYWRIGHT_ATTACHMENT_FAILED":"PLAYWRIGHT_ATTACHMENT_LIMIT",...t.kind==="io"?{classification:"infra"}:{},details:{name:c,specId:s,specTitle:a,retry:i,...t},message:`Playwright attachment "${c}" (${a}, retry ${i.toString()}): ${l}`}}async function dt(e,t,c,s,a){const i=[],l=[],u=new Set;let p=0;const h=d.join(t,"attachments");s.length>0&&await P.mkdir(h,{mode:448,recursive:!1});for(const[f,r]of s.entries())try{if(r.error!==void 0){r.path!==void 0&&u.add(d.resolve(c,r.path)),l.push(R(r,r.error));continue}const n=await yt(r.path,c,e,a);u.add(n.file);const m=v(Number(n.identity.size),"Playwright attachment size"),w=et(m,p+m);if(w!==null){l.push(R(r,w));continue}const o=await z(n.file,G,"Playwright attachment",a,n.identity,h,`${f.toString().padStart(4,"0")}-${U(r.name)}${Pt(r)}`);p+=o.identity.sizeBytes,i.push({path:o.filePath,contentType:r.contentType,kind:"engine.playwright-attachment",name:r.name,summary:`${r.specTitle} retry ${r.retry.toString()}`,metadata:{...o.identity,specId:r.specId,specTitle:r.specTitle,retry:r.retry}})}catch(n){l.push(R(r,it(n)))}return{artifacts:i,failures:l,sourcePaths:u}}async function ht(e,t,c,s,a){try{const i=await a.lstat(c);if(!i.isDirectory()||i.isSymbolicLink())throw new Error("Playwright output directory must be a non-symbolic-link directory");const l=await P.realpath(c);if(!_(e,l))throw new Error("Playwright output directory escaped the artifact root");const u=await ft(l,s,a),p=[],h=d.join(t,"output");u.candidates.length>0&&await P.mkdir(h,{mode:448,recursive:!1});for(const[f,r]of u.candidates.entries()){const n=await z(r.file,F,"Playwright output file",a,r.identity,h,`${f.toString().padStart(5,"0")}-${U(d.basename(r.file))}${Et(r.file)}`);p.push(A(n.filePath,n.identity,vt(r.file),"engine.playwright-output",r.relativePath,"Playwright output"))}return{artifacts:p,bytes:u.bytes,entries:u.entries}}catch(i){if(Z(i))return{artifacts:[],bytes:0,entries:0};throw i}}async function ft(e,t,c){const s=[];let a=0,i=0;const l=[{depth:0,directory:e}];for(;l.length>0;){const u=l.pop();if(u===void 0)continue;if(u.depth>X)throw new Error(`Playwright output exceeds the ${X.toString()} level directory depth limit`);const p=u.directory,h=await P.readdir(p,{withFileTypes:!0});h.sort((f,r)=>f.name.localeCompare(r.name));for(const f of h){if(i+=1,i>W)throw new M({bytes:a,entries:i,kind:"entries",limit:W,observed:i});const r=d.join(p,f.name),n=await c.lstat(r);if(n.isSymbolicLink())throw new Error("Playwright output must not contain symbolic links");if(n.isDirectory()){l.push({depth:u.depth+1,directory:r});continue}if(!n.isFile())continue;if(n.size<0n||n.size>BigInt(Number.MAX_SAFE_INTEGER))throw new Error("Playwright output file has an invalid size");const m=Number(n.size);if(a+=m,!Number.isSafeInteger(a)||a>F)throw new M({bytes:a,entries:i,kind:"bytes",limit:F,observed:a});t.has(r)||s.push({file:r,identity:n,relativePath:Y(e,r)})}}return{bytes:a,candidates:s,entries:i}}function gt(e){return e.kind==="bytes"?`Playwright output exceeds its ${e.limit.toString()} byte aggregate limit (observed ${e.observed.toString()} bytes)`:`Playwright output exceeds its ${e.limit.toString()} entry limit (observed ${e.observed.toString()} entries)`}async function yt(e,t,c,s){if(e===void 0)throw new Error("Playwright path attachment did not declare a path");const a=d.resolve(t,e),i=await s.lstat(a);if(!i.isFile()||i.isSymbolicLink())throw new Error("Playwright attachment path must be a regular non-symbolic-link file");const l=await P.realpath(a);if(!_(t,l)&&!_(c,l))throw new Error("Playwright attachment path must stay within the consumer or artifact root");return{file:l,identity:i}}function mt(e){const t=S(e,"Playwright report.stats");return{expected:v(t.expected,"Playwright stats.expected"),unexpected:v(t.unexpected,"Playwright stats.unexpected"),flaky:v(t.flaky,"Playwright stats.flaky"),skipped:v(t.skipped,"Playwright stats.skipped"),duration:B(t.duration,"Playwright stats.duration")}}function wt(e){let t=at;const c=[];for(const s of e){if(t<=0)break;const i=Buffer.from(s.text,"utf8").subarray(0,t).toString("utf8");i.length>0&&c.push({...s,text:i}),t-=Buffer.byteLength(i)}return t<=0&&c.push({stream:"engine",text:"Playwright report log evidence was truncated at its bounded limit"}),c}function Pt(e){if(e.path!==void 0){const t=d.extname(e.path);if(/^\.[a-z0-9]{1,12}$/iu.test(t))return t}return e.contentType==="application/json"?".json":e.contentType.startsWith("text/")?".txt":e.contentType==="image/png"?".png":e.contentType==="image/jpeg"?".jpg":".bin"}function vt(e){const t=d.extname(e).toLowerCase();return t===".json"?"application/json":t===".zip"?"application/zip":t===".png"?"image/png":t===".jpg"||t===".jpeg"?"image/jpeg":t===".webm"?"video/webm":[".log",".txt",".md"].includes(t)?"text/plain; charset=utf-8":"application/octet-stream"}function Et(e){const t=d.extname(e).toLowerCase();return/^\.[a-z0-9]{1,12}$/u.test(t)?t:".bin"}function U(e){const t=e.trim().toLowerCase().replace(/[^a-z0-9._-]+/gu,"-").replace(/^-+|-+$/gu,"").slice(0,80);return t.length>0?t:"attachment"}
1
+ import w from"node:fs/promises";import g from"node:path";import{evidenceArtifact as F,evidenceFileAccess as q,readStableJsonFile as tt,snapshotStableEvidenceFile as C,stableEvidenceFromBuffer as et,writeOwnedEvidenceFile as Y}from"../../adapters/evidence-files.js";import{materializePlaywrightFailureDetails as it}from"./failure-details.js";import{isMissingFileError as rt,isPathWithin as $,nonNegativeInteger as E,nonNegativeNumber as H,optionalNonEmptyString as N,optionalNonNegativeInteger as G,portableRelativePath as U,portableReportPath as W,requireNonEmptyString as b,requireRecord as S,toError as nt}from"./evidence-validation.js";import{readNativeReport as at}from"../../adapters/native-report.js";import{attachmentLimitError as st,attachmentIoError as ot,MAX_ATTACHMENT_BYTES as X,MAX_TOTAL_ATTACHMENT_BYTES as ct}from"./attachments.js";const lt=64*1024*1024,J=1e4,M=512*1024*1024,ut=2*1024*1024,V=128,pt=".testkit-playwright-evidence",dt=/(?:^|\n)(?:Error:\s*)?No tests found(?:\.|\n|$)/u,ht=["failed","interrupted","passed","timedout"];class R extends Error{limit;constructor(e){super(vt(e)),this.limit=e,this.name="PlaywrightOutputArtifactLimitError"}}export async function collectPlaywrightEvidence(t,e=q){await w.mkdir(t.artifactDirectory,{recursive:!0});const i=await w.realpath(t.artifactDirectory),c=await w.realpath(t.rootDirectory),n=g.join(i,pt);await w.mkdir(n,{mode:448,recursive:!1});const a=[],s=[];for(const r of["stdout","stderr"]){const l=t[r];if(l.length===0)continue;a.push({stream:r,text:l});const m=et(Buffer.from(l,"utf8")),y=await Y(n,`${r}.log`,m.contents);s.push(F(y,m,"text/plain; charset=utf-8","engine.process-output",`playwright-${r}`,`${m.sizeBytes.toString()} bytes`))}let p=null,d=null,f=new Set,h=[];try{const r=await tt(t.reportFile,lt,"Testkit native Playwright report",e);if(r.file===null)p=new Error("Playwright did not produce its Testkit native report");else{d=gt(at(r.value,t.identity).result,t.sourceFile);const l=await Y(n,"testkit-playwright-report.json",r.file.contents);s.push(F(l,r.file,"application/json","engine.testkit-playwright-report","testkit-playwright-report","Testkit native Playwright report")),a.push(...d.logs);const m=await it(n,d.failureDetails);m!==null&&s.push(m);const y=await wt(i,n,c,d.attachments,e);s.push(...y.artifacts),f=y.sourcePaths,h=y.failures}}catch(r){p=nt(r)}let o={artifacts:[],bytes:0,entries:0},u=null;try{o=await Pt(i,n,t.outputDirectory,f,e),s.push(...o.artifacts)}catch(r){if(!(r instanceof R))throw r;u=r.limit,a.push({stream:"engine",text:r.message})}const P=ft(d,c,t.sourceFile,h.length),k=u??o;return{attachmentFailures:h,evidence:{artifacts:s,logs:a,metrics:[...Object.entries(d?.stats??{}).map(([r,l])=>({name:r==="duration"?"playwright.duration":`playwright.tests.${r}`,unit:r==="duration"?"milliseconds":"tests",value:l})),...["bytes","entries"].map(r=>({name:`playwright.output.${r}`,unit:r,value:k[r]}))],...d===null?{}:{native:{playwright:{attachmentFailures:h.map(r=>({...r})),specs:d.specs,stats:d.stats}}}},outputLimit:u,report:P,reportError:p}}function ft(t,e,i,c){if(t===null)return{failure:null,status:"failed"};const n=t.specs.filter((s,p)=>K(i,t.sourceFiles[p])),a=t.failureMessages.length>0&&t.failureMessages.every(s=>dt.test(s));return t.stats.unexpected>0||t.failureMessages.length>0&&(!a||n.length>0)?{failure:{...t.failureMessages.some(s=>/browserType\.launch(?:PersistentContext)?:/u.test(s)&&/Executable doesn't exist|Host system is missing dependencies|error while loading shared libraries|Failed to launch/u.test(s))?{classification:"infra"}:{},code:"PLAYWRIGHT_SPECS_FAILED",details:{attachmentFailures:c,failOnFlakyTests:t.failOnFlakyTests,reportErrors:t.reportErrors,runStatus:t.runStatus,stats:t.stats,specs:t.specs.slice(0,50)},message:t.failureMessages.at(0)??`${t.stats.unexpected.toString()} Playwright test(s) failed`},status:"failed"}:n.length>0?{failure:null,status:n.every(s=>s.status==="skipped")?"skipped":"passed"}:{failure:{code:"PLAYWRIGHT_NO_TESTS",details:{file:U(e,i),reportedSpecs:t.specs.slice(0,50),stats:t.stats},message:"Selected Playwright task did not execute any tests"},status:"failed"}}function K(t,e){return g.relative(g.normalize(t),e)===""}function gt(t,e){const i=S(t,"Playwright native report result"),c=ht.find(r=>r===i.status);if(c===void 0)throw new Error("Playwright native report status is invalid");if(typeof i.failOnFlakyTests!="boolean")throw new Error("Playwright native report failOnFlakyTests must be a boolean");const n=i.failOnFlakyTests,a=bt(i.stats),s=O(i.errors,"errors"),p=s.length,d=[],f=[],h=[],o=new Set,u={expected:0,flaky:0,skipped:0,unexpected:0};for(const r of A(i.specs,"specs")){const l=S(r,"Playwright native report spec"),m=b(l.id,"Playwright native spec.id");if(o.has(m))throw new Error("Playwright native report contains a duplicate test identity");o.add(m);const y=b(l.title,"Playwright native spec.title"),v=O(l.parents,"spec.parents");v.length>0&&(v[0]=W(v[0]));const L=W(b(l.file,"Playwright native spec.file")),x=b(l.sourceFile,"Playwright native spec.sourceFile");if(!g.isAbsolute(x)&&!g.win32.isAbsolute(x))throw new Error("Playwright native spec.sourceFile must be absolute");const Z=_(l.projectName,"spec.projectName"),T=l.status;if(T!=="expected"&&T!=="flaky"&&T!=="skipped"&&T!=="unexpected")throw new Error("Playwright native spec.status is invalid");u[T]+=1;const j=O(l.failureMessages,"spec.failureMessages"),z=l.failureStack===null?null:_(l.failureStack,"spec.failureStack"),D=G(l.line,"Playwright native spec.line"),B=G(l.column,"Playwright native spec.column");T==="unexpected"&&(s.push(...j),h.push({kind:K(e,x)?"playwright-spec":"playwright-other-file-spec",key:[...v.slice(1),y].filter(Boolean).join(" "),title:y,count:1,message:j[0]??`Playwright reported "${y}" as an unexpected failure`,...v.length===0?{}:{suitePath:v},location:{path:L,...D===null?{}:{line:D},...B===null?{}:{column:B}},...z===null?{}:{stack:z}})),f.push(x),d.push({durationMs:H(l.durationMs,"Playwright native spec.durationMs"),retries:E(l.retries,"Playwright native spec.retries"),file:L,projectName:Z,status:T,title:[...v,y].filter(Boolean).join(" > ")})}for(const r of["expected","flaky","skipped","unexpected"])if(a[r]!==u[r])throw new Error("Playwright native report stats do not match its cases");const P=A(i.logs,"logs").map(r=>{const l=S(r,"Playwright native report log");if(l.stream!=="stdout"&&l.stream!=="stderr")throw new Error("Playwright native report log stream is invalid");return{stream:l.stream,text:_(l.text,"log.text")}});return{attachments:A(i.attachments,"attachments").map((r,l)=>yt(r,l,o)),failOnFlakyTests:n,failureDetails:h,failureMessages:s,logs:St(P),reportErrors:p,runStatus:c,sourceFiles:f,specs:d,stats:a}}function A(t,e){if(!Array.isArray(t))throw new Error(`Playwright native report ${e} must be an array`);return t}function O(t,e){return A(t,e).map(i=>_(i,e))}function _(t,e){if(typeof t!="string")throw new Error(`Playwright native report ${e} must be a string`);return t}function yt(t,e,i){const c=S(t,`Playwright attachment[${e.toString()}]`),n=b(c.specId,"Playwright attachment.specId");if(!i.has(n))throw new Error("Playwright native attachment references an unknown test identity");const a=N(c.path),s=c.error===void 0?void 0:mt(c.error);if(c.body!==void 0||a===null&&s===void 0)throw new Error("Playwright attachment must contain a path or an error");return{contentType:N(c.contentType)??"application/octet-stream",name:N(c.name)??`attachment-${e.toString()}`,specId:n,...a===null?{}:{path:a},...s===void 0?{}:{error:s},retry:E(c.retry,"Playwright attachment.retry"),specTitle:b(c.specTitle,"Playwright attachment.specTitle")}}function mt(t){const e=S(t,"Playwright attachment.error");if(e.kind==="io")return{kind:"io",message:b(e.message,"Playwright attachment.error.message")};const i=e.kind;if(i!=="individual"&&i!=="total")throw new Error("Playwright attachment.error has an invalid kind");const c=i==="individual"?X:ct,n=E(e.observed,"Playwright attachment.error.observed");if(e.limit!==c||n<=c)throw new Error("Playwright attachment.error has an invalid limit");return{kind:i,limit:c,observed:n}}function I(t,e){const{name:i,specId:c,specTitle:n,retry:a}=t,s=e.kind==="io"?e.message:`exceeds its ${e.limit.toString()} byte ${e.kind} limit (observed ${e.observed.toString()} bytes)`;return{code:e.kind==="io"?"PLAYWRIGHT_ATTACHMENT_FAILED":"PLAYWRIGHT_ATTACHMENT_LIMIT",...e.kind==="io"?{classification:"infra"}:{},details:{name:i,specId:c,specTitle:n,retry:a,...e},message:`Playwright attachment "${i}" (${n}, retry ${a.toString()}): ${s}`}}async function wt(t,e,i,c,n){const a=[],s=[],p=new Set;let d=0;const f=g.join(e,"attachments");c.length>0&&await w.mkdir(f,{mode:448,recursive:!1});for(const[h,o]of c.entries())try{if(o.error!==void 0){o.path!==void 0&&p.add(g.resolve(i,o.path)),s.push(I(o,o.error));continue}const u=await Tt(o.path,i,t,n);p.add(u.file);const P=E(Number(u.identity.size),"Playwright attachment size"),k=st(P,d+P);if(k!==null){s.push(I(o,k));continue}const r=await C(u.file,X,"Playwright attachment",n,u.identity,f,`${h.toString().padStart(4,"0")}-${Q(o.name)}${kt(o)}`);d+=r.identity.sizeBytes,a.push({path:r.filePath,contentType:o.contentType,kind:"engine.playwright-attachment",name:o.name,summary:`${o.specTitle} retry ${o.retry.toString()}`,metadata:{...r.identity,specId:o.specId,specTitle:o.specTitle,retry:o.retry}})}catch(u){s.push(I(o,ot(u)))}return{artifacts:a,failures:s,sourcePaths:p}}async function Pt(t,e,i,c,n){try{const a=await n.lstat(i);if(!a.isDirectory()||a.isSymbolicLink())throw new Error("Playwright output directory must be a non-symbolic-link directory");const s=await w.realpath(i);if(!$(t,s))throw new Error("Playwright output directory escaped the artifact root");const p=await Et(s,c,n),d=[],f=g.join(e,"output");p.candidates.length>0&&await w.mkdir(f,{mode:448,recursive:!1});for(const[h,o]of p.candidates.entries()){const u=await C(o.file,M,"Playwright output file",n,o.identity,f,`${h.toString().padStart(5,"0")}-${Q(g.basename(o.file))}${At(o.file)}`);d.push(F(u.filePath,u.identity,xt(o.file),"engine.playwright-output",o.relativePath,"Playwright output"))}return{artifacts:d,bytes:p.bytes,entries:p.entries}}catch(a){if(rt(a))return{artifacts:[],bytes:0,entries:0};throw a}}async function Et(t,e,i){const c=[];let n=0,a=0;const s=[{depth:0,directory:t}];for(;s.length>0;){const p=s.pop();if(p===void 0)continue;if(p.depth>V)throw new Error(`Playwright output exceeds the ${V.toString()} level directory depth limit`);const d=p.directory,f=await w.readdir(d,{withFileTypes:!0});f.sort((h,o)=>h.name.localeCompare(o.name));for(const h of f){if(a+=1,a>J)throw new R({bytes:n,entries:a,kind:"entries",limit:J,observed:a});const o=g.join(d,h.name),u=await i.lstat(o);if(u.isSymbolicLink())throw new Error("Playwright output must not contain symbolic links");if(u.isDirectory()){s.push({depth:p.depth+1,directory:o});continue}if(!u.isFile())continue;if(u.size<0n||u.size>BigInt(Number.MAX_SAFE_INTEGER))throw new Error("Playwright output file has an invalid size");const P=Number(u.size);if(n+=P,!Number.isSafeInteger(n)||n>M)throw new R({bytes:n,entries:a,kind:"bytes",limit:M,observed:n});e.has(o)||c.push({file:o,identity:u,relativePath:U(t,o)})}}return{bytes:n,candidates:c,entries:a}}function vt(t){return t.kind==="bytes"?`Playwright output exceeds its ${t.limit.toString()} byte aggregate limit (observed ${t.observed.toString()} bytes)`:`Playwright output exceeds its ${t.limit.toString()} entry limit (observed ${t.observed.toString()} entries)`}async function Tt(t,e,i,c){if(t===void 0)throw new Error("Playwright path attachment did not declare a path");const n=g.resolve(e,t),a=await c.lstat(n);if(!a.isFile()||a.isSymbolicLink())throw new Error("Playwright attachment path must be a regular non-symbolic-link file");const s=await w.realpath(n);if(!$(e,s)&&!$(i,s))throw new Error("Playwright attachment path must stay within the consumer or artifact root");return{file:s,identity:a}}function bt(t){const e=S(t,"Playwright report.stats");return{expected:E(e.expected,"Playwright stats.expected"),unexpected:E(e.unexpected,"Playwright stats.unexpected"),flaky:E(e.flaky,"Playwright stats.flaky"),skipped:E(e.skipped,"Playwright stats.skipped"),duration:H(e.duration,"Playwright stats.duration")}}function St(t){let e=ut;const i=[];for(const c of t){if(e<=0)break;const a=Buffer.from(c.text,"utf8").subarray(0,e).toString("utf8");a.length>0&&i.push({...c,text:a}),e-=Buffer.byteLength(a)}return e<=0&&i.push({stream:"engine",text:"Playwright report log evidence was truncated at its bounded limit"}),i}function kt(t){if(t.path!==void 0){const e=g.extname(t.path);if(/^\.[a-z0-9]{1,12}$/iu.test(e))return e}return t.contentType==="application/json"?".json":t.contentType.startsWith("text/")?".txt":t.contentType==="image/png"?".png":t.contentType==="image/jpeg"?".jpg":".bin"}function xt(t){const e=g.extname(t).toLowerCase();return e===".json"?"application/json":e===".zip"?"application/zip":e===".png"?"image/png":e===".jpg"||e===".jpeg"?"image/jpeg":e===".webm"?"video/webm":[".log",".txt",".md"].includes(e)?"text/plain; charset=utf-8":"application/octet-stream"}function At(t){const e=g.extname(t).toLowerCase();return/^\.[a-z0-9]{1,12}$/u.test(e)?e:".bin"}function Q(t){const e=t.trim().toLowerCase().replace(/[^a-z0-9._-]+/gu,"-").replace(/^-+|-+$/gu,"").slice(0,80);return e.length>0?e:"attachment"}
@@ -1 +1 @@
1
- import f from"node:path";import v from"node:fs/promises";import T from"node:crypto";import{writeNativeReport as j}from"../../adapters/native-report.js";import{writeOwnedEvidenceFile as D}from"../../adapters/evidence-files.js";import{attachmentIoError as M,attachmentLimitError as A}from"./attachments.js";export default class F{options;config;suite;errors=[];constructor(o){this.options=o}onBegin(o,l){this.config=o,this.suite=l}onError(o){this.errors.push(P(o))}async onEnd(o){const l=this.suite.allTests(),e={duration:o.duration,expected:0,flaky:0,skipped:0,unexpected:0},u=[],p=[];let m=0;const d=[];for(const r of l){const y=r.outcome();e[y]+=1;let g=0,h=0,w=[],b=null;for(const n of r.results){g+=n.duration,h=Math.max(h,n.retry);for(const t of["stdout","stderr"])for(const s of n[t])u.push({stream:t,text:typeof s=="string"?s:s.toString("utf8")});for(const t of n.attachments){const s={name:t.name,contentType:t.contentType,...t.path===void 0?{}:{path:t.path},specId:r.id,specTitle:r.title,retry:n.retry};try{let a=t.body?.byteLength;if(a===void 0){if(t.path===void 0)throw new Error("Attachment has no body or path");const c=await v.lstat(t.path);if(!c.isFile()||c.isSymbolicLink())throw new Error("Attachment path must be a regular non-symbolic-link file");a=c.size}const k=A(a,m+a);if(k!==null){p.push({...s,error:k});continue}let E=t.path;if(t.body!==void 0){const c=this.options.attachmentDirectory;if(await v.realpath(c)!==f.resolve(c))throw new Error("Attachment directory must not resolve through symbolic links");E=await D(c,T.randomUUID(),t.body)}p.push({...s,path:E}),m+=a}catch(a){p.push({...s,error:M(a)})}}n.errors.length>0&&(w=n.errors.map(P),b=n.errors.find(t=>t.stack)?.stack??null)}d.push({id:r.id,title:r.title,parents:I(r,this.config.rootDir),file:x(this.config.rootDir,r.location.file),sourceFile:r.location.file,projectName:r.parent.project()?.name??"",line:r.location.line,column:r.location.column,status:y,durationMs:g,retries:h,failureMessages:w,failureStack:b})}await j(this.options,{stats:e,specs:d,logs:u,attachments:p,errors:this.errors},64*1024*1024)}}function I(i,o){const l=[];for(let e=i.parent;e&&e.type!=="project"&&e.type!=="root";e=e.parent)l.unshift(e.type==="file"&&e.location?x(o,e.location.file):e.title);return l}function x(i,o){return f.relative(i,o).split(f.sep).join("/")}function P(i){return i.message?.trim()||i.stack?.trim()||i.value?.trim()||"Playwright reported an error"}
1
+ import f from"node:path";import E from"node:fs/promises";import x from"node:crypto";import{writeNativeReport as D}from"../../adapters/native-report.js";import{writeOwnedEvidenceFile as P}from"../../adapters/evidence-files.js";import{attachmentIoError as j,attachmentLimitError as M}from"./attachments.js";export default class A{options;config;suite;errors=[];constructor(e){this.options=e}onBegin(e,r){this.config=e,this.suite=r}onError(e){this.errors.push(F(e))}async onEnd(e){const r=this.suite.allTests(),o={duration:e.duration,expected:0,flaky:0,skipped:0,unexpected:0},u=[],p=[];let m=0;const d=[];for(const i of r){const y=i.outcome();o[y]+=1;let g=0,h=0,w=[],k=null;for(const s of i.results){g+=s.duration,h=Math.max(h,s.retry);for(const t of["stdout","stderr"])for(const a of s[t])u.push({stream:t,text:typeof a=="string"?a:a.toString("utf8")});for(const t of s.attachments){const a={name:t.name,contentType:t.contentType,...t.path===void 0?{}:{path:t.path},specId:i.id,specTitle:i.title,retry:s.retry};try{let c=t.body?.byteLength;if(c===void 0){if(t.path===void 0)throw new Error("Attachment has no body or path");const l=await E.lstat(t.path);if(!l.isFile()||l.isSymbolicLink())throw new Error("Attachment path must be a regular non-symbolic-link file");c=l.size}const b=M(c,m+c);if(b!==null){p.push({...a,error:b});continue}let v=t.path;if(t.body!==void 0){const l=this.options.attachmentDirectory;if(await E.realpath(l)!==f.resolve(l))throw new Error("Attachment directory must not resolve through symbolic links");v=await P(l,x.randomUUID(),t.body)}p.push({...a,path:v}),m+=c}catch(c){p.push({...a,error:j(c)})}}s.errors.length>0&&(w=s.errors.map(F),k=s.errors.find(t=>t.stack)?.stack??null)}d.push({id:i.id,title:i.title,parents:L(i,this.config.rootDir),file:T(this.config.rootDir,i.location.file),sourceFile:I(i,this.config.rootDir),projectName:i.parent.project()?.name??"",line:i.location.line,column:i.location.column,status:y,durationMs:g,retries:h,failureMessages:w,failureStack:k})}await D(this.options,{status:e.status,failOnFlakyTests:this.options.failOnFlakyTests,stats:o,specs:d,logs:u,attachments:p,errors:this.errors},64*1024*1024)}}function I(n,e){for(let r=n.parent;r;r=r.parent)if(r.type==="file")return f.resolve(e,r.title);return n.location.file}function L(n,e){const r=[];for(let o=n.parent;o&&o.type!=="project"&&o.type!=="root";o=o.parent)r.unshift(o.type==="file"&&o.location?T(e,o.location.file):o.title);return r}function T(n,e){return f.relative(n,e).split(f.sep).join("/")}function F(n){return n.message?.trim()||n.stack?.trim()||n.value?.trim()||"Playwright reported an error"}
@@ -1 +1,2 @@
1
+ export declare const SENSITIVE_KEY_PATTERN: RegExp;
1
2
  export declare function redactSensitiveText(value: unknown): string;
@@ -1 +1 @@
1
- const S=/\b([A-Z0-9_]+)=([^\s]+)/giu,T=/PASSWORD|TOKEN|SECRET|PRIVATE_KEY|AUTH|COOKIE|SESSION|API_KEY/iu,a=/(:\/\/[^:\s/@]+:)([^@\s/]+)(@)/gu,o=String;export function redactSensitiveText(r){return u(o(r??"").replace(S,(e,t,s)=>T.test(t)?/(^|_)URL$/iu.test(t)?`${t}=${u(s)}`:`${t}=<redacted>`:e))}function u(r){return r.replace(a,(e,t,s,c,E)=>{for(let n=E-1;n>=0;n--){const i=r[n];if(/[a-z]/iu.test(i))return`${t}<redacted>${c}`;if(!/[0-9+.-]/u.test(i))break}return e})}
1
+ const S=/\b([A-Z0-9_]+)=([^\s]+)/giu;export const SENSITIVE_KEY_PATTERN=/PASSWORD|TOKEN|SECRET|PRIVATE_KEY|AUTH|COOKIE|SESSION|API_KEY/iu;const T=/(:\/\/[^:\s/@]+:)([^@\s/]+)(@)/gu,a=String;export function redactSensitiveText(r){return u(a(r??"").replace(S,(e,t,s)=>SENSITIVE_KEY_PATTERN.test(t)?/(^|_)URL$/iu.test(t)?`${t}=${u(s)}`:`${t}=<redacted>`:e))}function u(r){return r.replace(T,(e,t,s,c,E)=>{for(let n=E-1;n>=0;n--){const i=r[n];if(/[a-z]/iu.test(i))return`${t}<redacted>${c}`;if(!/[0-9+.-]/u.test(i))break}return e})}
@@ -3,7 +3,7 @@
3
3
  "entrypoint": "dist/process/process.js",
4
4
  "sourceSha256": "a54f01864b603cee568b2945390dff35d578e07f6d26b5add06014169c3d17b0",
5
5
  "bundleSha256": "62f76cf9fd019f6c552bfde2787f058917caf279670a62e7a6bfa12be34cb4b1",
6
- "packageLockSha256": "21f0898586dbf3747862f6def92186117fbb1612fc057f4ec6b22d1670cdbd00",
6
+ "packageLockSha256": "88653a3c5a6ad13ad05f232b346a397357b531418ac9ca932cedaf0de4d865a6",
7
7
  "esbuildVersion": "0.28.1",
8
8
  "options": {
9
9
  "format": "esm",
@@ -8,4 +8,5 @@ export interface AtomicWriteOptions {
8
8
  maxDelayMs?: number;
9
9
  }
10
10
  export declare function writeJsonFileAtomicWithRetry(filePath: string, value: unknown, options?: AtomicWriteOptions): void;
11
+ export declare function writeTextFileAtomicWithRetry(filePath: string, text: string, options?: AtomicWriteOptions): void;
11
12
  export declare function isRetryableAtomicReplaceError(error: unknown): boolean;
@@ -1 +1 @@
1
- import s from"node:fs";import c from"node:path";const p=String;export function isPropertyContainer(t){return typeof t=="object"&&t!==null||typeof t=="function"}export function readProperty(t,r){return Reflect.get(t,r)}export function serializeJson(t,r){let e;try{e=JSON.stringify(t,null,2)}catch(o){throw new Error(`${r} is not JSON serializable`,{cause:o})}if(typeof e!="string")throw new Error(`${r} is not JSON serializable`);return e}export function errorMessage(t){if(t instanceof Error)return t.message;if(isPropertyContainer(t)){const r=readProperty(t,"message");if(typeof r=="string"&&r.length>0)return r}return p(t)}const y=new Set(["EPERM","EACCES","EBUSY"]),u=process.platform==="win32"?100:25,l=10,S=100;class A extends Error{attempts;code="TESTKIT_ATOMIC_WRITE_FAILED";cause;path;tempPath;constructor(r,e,o,n){super(`Failed to atomically replace ${r} after ${o.toString()} attempt${o===1?"":"s"}: ${errorMessage(n)}`,{cause:n}),this.name="AtomicWriteError",this.attempts=o,this.cause=n,this.path=r,this.tempPath=e}}export function writeJsonFileAtomicWithRetry(t,r,e={}){const o=c.dirname(t),n=c.join(o,`.${c.basename(t)}.${process.pid.toString()}.${Date.now().toString()}.${Math.random().toString(16).slice(2)}.tmp`),a=e.attempts??u,E=e.baseDelayMs??l,m=e.maxDelayMs??S;s.writeFileSync(n,serializeJson(r,`atomic JSON file ${t}`));try{for(let i=1;i<=a;i+=1)try{s.renameSync(n,t);return}catch(f){if(!isRetryableAtomicReplaceError(f)||i>=a||R(t))throw new A(t,n,i,f);_(Math.min(m,E*i))}}finally{s.rmSync(n,{force:!0})}}export function isRetryableAtomicReplaceError(t){if(!isPropertyContainer(t))return!1;const r=readProperty(t,"code");return typeof r=="string"&&y.has(r)}function R(t){try{return s.lstatSync(t).isDirectory()}catch{return!1}}function _(t){if(t<=0)return;const r=new SharedArrayBuffer(4);Atomics.wait(new Int32Array(r),0,0,t)}
1
+ import s from"node:fs";import c from"node:path";const p=String;export function isPropertyContainer(t){return typeof t=="object"&&t!==null||typeof t=="function"}export function readProperty(t,r){return Reflect.get(t,r)}export function serializeJson(t,r){let e;try{e=JSON.stringify(t,null,2)}catch(o){throw new Error(`${r} is not JSON serializable`,{cause:o})}if(typeof e!="string")throw new Error(`${r} is not JSON serializable`);return e}export function errorMessage(t){if(t instanceof Error)return t.message;if(isPropertyContainer(t)){const r=readProperty(t,"message");if(typeof r=="string"&&r.length>0)return r}return p(t)}const y=new Set(["EPERM","EACCES","EBUSY"]),u=process.platform==="win32"?100:25,l=10,A=100;class S extends Error{attempts;code="TESTKIT_ATOMIC_WRITE_FAILED";cause;path;tempPath;constructor(r,e,o,n){super(`Failed to atomically replace ${r} after ${o.toString()} attempt${o===1?"":"s"}: ${errorMessage(n)}`,{cause:n}),this.name="AtomicWriteError",this.attempts=o,this.cause=n,this.path=r,this.tempPath=e}}export function writeJsonFileAtomicWithRetry(t,r,e={}){writeTextFileAtomicWithRetry(t,serializeJson(r,`atomic JSON file ${t}`),e)}export function writeTextFileAtomicWithRetry(t,r,e={}){const o=c.dirname(t),n=c.join(o,`.${c.basename(t)}.${process.pid.toString()}.${Date.now().toString()}.${Math.random().toString(16).slice(2)}.tmp`),a=e.attempts??u,E=e.baseDelayMs??l,m=e.maxDelayMs??A;s.writeFileSync(n,r);try{for(let i=1;i<=a;i+=1)try{s.renameSync(n,t);return}catch(f){if(!isRetryableAtomicReplaceError(f)||i>=a||R(t))throw new S(t,n,i,f);T(Math.min(m,E*i))}}finally{s.rmSync(n,{force:!0})}}export function isRetryableAtomicReplaceError(t){if(!isPropertyContainer(t))return!1;const r=readProperty(t,"code");return typeof r=="string"&&y.has(r)}function R(t){try{return s.lstatSync(t).isDirectory()}catch{return!1}}function T(t){if(t<=0)return;const r=new SharedArrayBuffer(4);Atomics.wait(new Int32Array(r),0,0,t)}
@@ -0,0 +1,17 @@
1
+ export interface BlamedCommit {
2
+ readonly boundary: boolean;
3
+ readonly commit: string;
4
+ readonly committedAt: number;
5
+ }
6
+ export type IntroducingCommit = {
7
+ readonly commit: string;
8
+ readonly status: "found";
9
+ } | {
10
+ readonly status: "outside-history";
11
+ };
12
+ export declare function findIntroducingCommit(repoRoot: string, commit: string, path: string, lines: readonly [number, number]): Promise<IntroducingCommit>;
13
+ export declare function parseBlame(output: string): readonly BlamedCommit[];
14
+ export declare function newestBlamedCommit(commits: readonly BlamedCommit[]): BlamedCommit | null;
15
+ export declare function listRemoteRefsContaining(repoRoot: string, commit: string): Promise<readonly string[]>;
16
+ export declare function listUncommittedTrackedFiles(repoRoot: string): Promise<readonly string[]>;
17
+ export declare function listFilesChangedSince(repoRoot: string, base: string): Promise<readonly string[] | null>;
@@ -0,0 +1,4 @@
1
+ import{requireManagedHostCommandSuccess as a}from"../process/index.js";import{runManagedGitCommand as s}from"./managed-git.js";const u=/^([0-9a-f]{40}|[0-9a-f]{64}) \d+ \d+(?: \d+)?$/u,l=/^committer-time (\d+)$/u;export async function findIntroducingCommit(e,t,n,r){const i=`${String(r[0])},${String(r[1])}`,o=await s(e,["blame","-w","-M","--porcelain","-L",i,t,"--",n]);if(o.exitCode!==0)throw new Error(`Git cannot blame lines ${String(r[0])} to ${String(r[1])} of ${n} at ${t}: ${m(o.stderr)}`);const c=newestBlamedCommit(parseBlame(o.stdout));if(c===null)throw new Error(`Git blamed no lines of ${n} at ${t}`);return c.boundary&&await f(e)?{status:"outside-history"}:{commit:c.commit,status:"found"}}export function parseBlame(e){const t=new Map;let n=null;for(const r of e.split(`
2
+ `)){if(r.startsWith(" "))continue;const i=u.exec(r);if(i!==null){n=t.get(i[1])??{boundary:!1,committedAt:0},t.set(i[1],n);continue}if(n===null)continue;const o=l.exec(r);o!==null?n.committedAt=Number(o[1]):r==="boundary"&&(n.boundary=!0)}return[...t].map(([r,i])=>({commit:r,...i}))}export function newestBlamedCommit(e){let t=null;for(const n of e)(t===null||n.committedAt>t.committedAt)&&(t=n);return t}export async function listRemoteRefsContaining(e,t){return a("git for-each-ref",await s(e,["for-each-ref","--format=%(refname)","--contains",t,"refs/remotes"])).stdout.split(`
3
+ `).map(i=>i.trim()).filter(i=>i.length>0&&!i.endsWith("/HEAD"))}export async function listUncommittedTrackedFiles(e){return a("git diff",await s(e,["diff","--name-only","-z","HEAD","--"])).stdout.split("\0").filter(r=>r.length>0)}export async function listFilesChangedSince(e,t){if((await s(e,["merge-base","--is-ancestor",t,"HEAD"])).exitCode!==0)return null;const r=["diff","--name-only","--diff-filter=d","-z",`${t}..HEAD`,"--"];return a("git diff",await s(e,r)).stdout.split("\0").filter(o=>o.length>0)}async function f(e){return a("git rev-parse",await s(e,["rev-parse","--is-shallow-repository"])).stdout.trim()==="true"}function m(e){return e.trim().split(`
4
+ `).at(-1)??""}
@@ -0,0 +1,10 @@
1
+ export interface CommitFile {
2
+ readonly path: string;
3
+ readonly size: number;
4
+ }
5
+ export interface TreeListing {
6
+ readonly directories: readonly string[];
7
+ readonly files: readonly CommitFile[];
8
+ }
9
+ export declare function listCommitFiles(repoRoot: string, commit: string): Promise<readonly CommitFile[]>;
10
+ export declare function parseTreeListing(output: string, directory?: string): TreeListing;
@@ -0,0 +1 @@
1
+ import{isUnknownRecord as m}from"../kernel/unknown-values.js";import{requireManagedHostCommandSuccess as h}from"../process/index.js";import{runManagedGitCommand as p}from"./managed-git.js";const w=/^(\d{6}) (blob|commit|tree) [0-9a-f]+ +(-|\d+)\t([\s\S]+)$/u,E=new Set(["100644","100755"]);export function listCommitFiles(t,e){return a(t,e,"")}export function parseTreeListing(t,e=""){const r=e===""?"":`${e}/`,o=[],s=[];for(const i of t.split("\0")){if(i.length===0)continue;const n=w.exec(i);if(n===null)throw new Error("Git returned an unreadable ls-tree record");const[,f,u,d,l]=n;u==="tree"?o.push(`${r}${l}`):u==="blob"&&E.has(f)&&s.push({path:`${r}${l}`,size:Number(d)})}return{directories:o,files:s}}async function a(t,e,r){const o=await c(t,e,r,!0);if(o!==null)return o.files;const s=await c(t,e,r,!1);if(s===null)throw new Error(`Git lists too many entries directly under ${r||"the repository root"} to read`);const i=[...s.files];for(const n of s.directories)i.push(...await a(t,e,n));return i}async function c(t,e,r,o){const s=r===""?e:`${e}:${r}`,i=["ls-tree","--full-tree","-l","-z",...o?["-r"]:[],s];try{const n=h("git ls-tree",await p(t,i));return parseTreeListing(n.stdout,r)}catch(n){if($(n))return null;throw n}}function $(t){if(!(t instanceof Error)||!("result"in t))return!1;const e=t.result;return m(e)&&e.outputLimitExceeded===!0}
@@ -0,0 +1,14 @@
1
+ import { type TrackerRequest } from "./http.js";
2
+ import { type TrackerRepository } from "./requests.js";
3
+ import type { Tracker, TrackerIssueDraft, TrackerLabel, TrackerTarget } from "./types.js";
4
+ export declare function giteaHeaders(token: string): Record<string, string>;
5
+ export declare function giteaRequests(repository: TrackerRepository): {
6
+ routes: import("./requests.js").TrackerRoutes;
7
+ createIssue: (draft: TrackerIssueDraft, labelIds: readonly number[]) => TrackerRequest;
8
+ createLabel: (label: TrackerLabel) => TrackerRequest;
9
+ listComments: (pullRequest: number, number: number) => TrackerRequest;
10
+ listLabels: (number: number) => TrackerRequest;
11
+ listOpenIssues: (label: string, number: number) => TrackerRequest;
12
+ };
13
+ export declare function readGiteaVersion(origin: string): Promise<string | null>;
14
+ export declare function createGiteaTracker(target: TrackerTarget): Tracker;
@@ -0,0 +1 @@
1
+ import{createTrackerHttp as b,TRACKER_PAGE_LIMIT as y,TrackerError as f}from"./http.js";import{commentRequest as T,commitStatusRequest as g,getRequest as k,TRACKER_USER_AGENT as h,trackerRepository as w,trackerRoutes as I}from"./requests.js";import{decodeGiteaVersion as R,decodeTrackerComment as G,decodeTrackerComments as S,decodeTrackerCommitStatusContext as C,decodeTrackerIssue as A,decodeTrackerIssues as E,decodeTrackerLabel as L,decodeTrackerLabels as q,requireIssueLabels as x}from"./responses.js";const $="50";export function giteaHeaders(a){return{Accept:"application/json",Authorization:`token ${a}`,"User-Agent":h}}export function giteaRequests(a){const o=I(a),r=(s,i,m={})=>k(s,{...m,limit:$,page:String(i)});return{routes:o,createIssue:(s,i)=>({body:{body:s.body,labels:i,title:s.title},method:"POST",url:o.issues}),createLabel:s=>({body:{color:`#${s.color}`,description:s.description,name:s.name},method:"POST",url:o.labels}),listComments:(s,i)=>r(o.comments(s),i),listLabels:s=>r(o.labels,s),listOpenIssues:(s,i)=>r(o.issues,i,{labels:s,state:"open",type:"issues"})}}export async function readGiteaVersion(a){const r=await b({headers:{Accept:"application/json","User-Agent":h},label:"Gitea version probe",secrets:[]}).exchange({method:"GET",url:`${a}/api/v1/version`});return r.ok?R(r.body):null}function O(a){const o=a.get("x-total-count");return o!==null&&/^\d+$/u.test(o)?Number(o):Number.POSITIVE_INFINITY}export function createGiteaTracker(a){const o=w(a),r=giteaRequests(o),s=b({headers:giteaHeaders(a.token),label:"Gitea",secrets:[a.token]});async function i(t,c,e){const n=new Map;for(let u=1;u<=y;u++){const l=await s.send(t(u)),p=n.size;for(const d of c(l.body,l.source))n.has(e(d))||n.set(e(d),d);if(n.size===p||n.size>=O(l.headers))return[...n.values()]}throw new f("rejected",`Gitea returned more than ${String(y)} pages`)}const m=()=>i(r.listLabels,q,t=>t.id);return{target:o,async createIssue(t){const c=t.labels.length===0?[]:await m(),e=t.labels.map(l=>{const p=c.find(d=>d.name===l);if(p===void 0)throw new f("rejected",`Gitea repository ${o.owner}/${o.name} has no label named ${JSON.stringify(l)}`);return p.id}),n=await s.send(r.createIssue(t,e)),{issue:u}=A(n.body,n.source);return x(u,t.labels,"Gitea",o)},async ensureLabels(t){if(t.length===0)return;const c=new Set((await m()).map(e=>e.name));for(const e of t){if(c.has(e.name))continue;const n=await s.send(r.createLabel(e));L(n.body,n.source),c.add(e.name)}},async listOpenIssues(t){return(await i(e=>r.listOpenIssues(t,e),E,e=>e.issue.number)).filter(e=>!e.pullRequest&&e.issue.labels.includes(t)).map(e=>e.issue)},async setCommitStatus(t){const c=await s.send(g(r.routes,t));C(c.body,c.source)},async upsertPullRequestComment(t){const e=(await i(u=>r.listComments(t.pullRequest,u),S,u=>u.id)).find(u=>u.body.startsWith(t.marker)),n=await s.send(T(r.routes,t,e));return{created:e===void 0,url:G(n.body,n.source).url}}}}
@@ -0,0 +1,14 @@
1
+ import { type TrackerRequest } from "./http.js";
2
+ import { type TrackerRepository } from "./requests.js";
3
+ import type { Tracker, TrackerIssueDraft, TrackerLabel, TrackerTarget } from "./types.js";
4
+ export declare function githubHeaders(token: string): Record<string, string>;
5
+ export declare function githubRequests(repository: TrackerRepository): {
6
+ routes: import("./requests.js").TrackerRoutes;
7
+ createIssue: (draft: TrackerIssueDraft) => TrackerRequest;
8
+ createLabel: (label: TrackerLabel) => TrackerRequest;
9
+ listComments: (pullRequest: number) => TrackerRequest;
10
+ listLabels: () => TrackerRequest;
11
+ listOpenIssues: (label: string) => TrackerRequest;
12
+ nextPage(link: string | null): TrackerRequest | null;
13
+ };
14
+ export declare function createGithubTracker(target: TrackerTarget): Tracker;
@@ -0,0 +1 @@
1
+ import{createTrackerHttp as g,nextPageLink as w,TRACKER_PAGE_LIMIT as m,TrackerError as b}from"./http.js";import{commentRequest as f,commitStatusRequest as L,getRequest as d,TRACKER_USER_AGENT as h,trackerRepository as k,trackerRoutes as y}from"./requests.js";import{decodeTrackerComment as R,decodeTrackerComments as T,decodeTrackerCommitStatusContext as I,decodeTrackerIssue as C,decodeTrackerIssues as x,decodeTrackerLabel as A,decodeTrackerLabels as E,isDuplicateLabelRejection as P,requireIssueLabels as q}from"./responses.js";const p="100",G=100;export function githubHeaders(u){return{Accept:"application/vnd.github+json",Authorization:`Bearer ${u}`,"User-Agent":h,"X-GitHub-Api-Version":"2022-11-28"}}export function githubRequests(u){const i=y(u);return{routes:i,createIssue:e=>({body:{body:e.body,labels:e.labels,title:e.title},method:"POST",url:i.issues}),createLabel:e=>({body:{color:e.color,description:Array.from(e.description).slice(0,G).join(""),name:e.name},method:"POST",url:i.labels}),listComments:e=>d(i.comments(e),{per_page:p}),listLabels:()=>d(i.labels,{per_page:p}),listOpenIssues:e=>d(i.issues,{labels:e,per_page:p,state:"open"}),nextPage(e){const a=w(e);if(a===null)return null;if(!URL.canParse(a)||new URL(a).origin!==new URL(u.apiUrl).origin)throw new b("rejected",`GitHub returned a next page outside ${u.apiUrl}`);return{method:"GET",url:a}}}}export function createGithubTracker(u){const i=k(u),e=githubRequests(i),a=g({headers:githubHeaders(u.token),label:"GitHub",secrets:[u.token]});async function c(t,o){const s=[];let r=t;for(let n=0;r!==null;n++){if(n===m)throw new b("rejected",`GitHub returned more than ${String(m)} pages`);const l=await a.send(r);s.push(...o(l.body,l.source)),r=e.nextPage(l.headers.get("link"))}return s}return{target:i,async createIssue(t){const o=await a.send(e.createIssue(t)),{issue:s}=C(o.body,o.source);return q(s,t.labels,"GitHub",i)},async ensureLabels(t){if(t.length===0)return;const o=new Set((await c(e.listLabels(),E)).map(s=>s.name.toLowerCase()));for(const s of t){const r=s.name.toLowerCase();if(o.has(r))continue;const n=await a.exchange(e.createLabel(s));if(n.ok)A(n.body,n.source);else if(n.status!==422||!P(n.body))throw a.failure(n);o.add(r)}},async listOpenIssues(t){const o=t.toLowerCase(),s=new Map;for(const r of await c(e.listOpenIssues(t),x))r.pullRequest||!r.issue.labels.some(n=>n.toLowerCase()===o)||s.set(r.issue.number,r.issue);return[...s.values()]},async setCommitStatus(t){const o=await a.send(L(e.routes,t));I(o.body,o.source)},async upsertPullRequestComment(t){const s=(await c(e.listComments(t.pullRequest),T)).find(n=>n.body.startsWith(t.marker)),r=await a.send(f(e.routes,t,s));return{created:s===void 0,url:R(r.body,r.source).url}}}}
@@ -0,0 +1,33 @@
1
+ import type { TrackerFailureKind } from "./types.js";
2
+ export declare const TRACKER_PAGE_LIMIT = 1000;
3
+ export declare class TrackerError extends Error {
4
+ readonly kind: TrackerFailureKind;
5
+ readonly status: number | null;
6
+ constructor(kind: TrackerFailureKind, message: string, status?: number | null);
7
+ }
8
+ export interface TrackerRequest {
9
+ readonly body?: unknown;
10
+ readonly method: "GET" | "PATCH" | "POST";
11
+ readonly url: string;
12
+ }
13
+ export interface TrackerResponse {
14
+ readonly body: unknown;
15
+ readonly headers: Headers;
16
+ readonly json: boolean;
17
+ readonly ok: boolean;
18
+ readonly source: string;
19
+ readonly status: number;
20
+ }
21
+ export interface TrackerHttpOptions {
22
+ readonly headers: Readonly<Record<string, string>>;
23
+ readonly label: string;
24
+ readonly secrets: readonly string[];
25
+ }
26
+ export interface TrackerHttp {
27
+ exchange(request: TrackerRequest): Promise<TrackerResponse>;
28
+ failure(response: TrackerResponse): TrackerError;
29
+ send(request: TrackerRequest): Promise<TrackerResponse>;
30
+ }
31
+ export declare function createTrackerHttp(options: TrackerHttpOptions): TrackerHttp;
32
+ export declare function nextPageLink(link: string | null): string | null;
33
+ export declare function scrubFailureDetail(text: string, secrets: readonly string[]): string;
@@ -0,0 +1 @@
1
+ import{isUnknownRecord as g}from"../kernel/unknown-values.js";import{redactBugText as m}from"./redaction.js";const l=3e4,d=300,f=Symbol("not JSON");export const TRACKER_PAGE_LIMIT=1e3;export class TrackerError extends Error{kind;status;constructor(t,s,o=null){super(s),this.name="TrackerError",this.kind=t,this.status=o}}export function createTrackerHttp(e){const t=r=>scrubFailureDetail(r,e.secrets);async function s(r){const n=`${e.label} ${r.method} ${T(r.url)}`;let i,c;try{i=await fetch(r.url,{body:r.body===void 0?void 0:JSON.stringify(r.body),headers:r.body===void 0?e.headers:{...e.headers,"Content-Type":"application/json"},method:r.method,signal:AbortSignal.timeout(l)}),c=await i.text()}catch(h){throw new TrackerError("unreachable",`${n} did not complete: ${t(y(h))}`)}const u=p(c);return{body:u===f?c:u,headers:i.headers,json:u!==f,ok:i.ok,source:n,status:i.status}}function o(r){const n=t(w(r.body));return new TrackerError($(r.status),`${r.source} returned ${String(r.status)}${n.length>0?`: ${n}`:""}`,r.status)}async function a(r){const n=await s(r);if(!n.ok)throw o(n);if(!n.json)throw new TrackerError("rejected",`${n.source} returned a response that is not JSON`,n.status);return n}return{exchange:s,failure:o,send:a}}export function nextPageLink(e){if(e===null)return null;for(const t of e.matchAll(/<([^>]*)>([^,]*)/gu)){const s=/;\s*rel\s*=\s*(?:"([^"]*)"|([^;\s]*))/iu.exec(t[2]);if((s?.[1]??s?.[2]??"").toLowerCase().split(/\s+/u).includes("next"))return t[1]}return null}export function scrubFailureDetail(e,t){let s=e;for(const a of t)a.length>0&&(s=s.split(a).join("<redacted>"));const o=m(s.replace(/\s+/gu," ").trim());return o.length>d?`${o.slice(0,d)}...`:o}function p(e){if(e.trim().length===0)return null;try{return JSON.parse(e)}catch{return f}}function $(e){return e===401?"unauthorized":e===403?"forbidden":e===404?"not-found":"rejected"}function w(e){if(typeof e=="string")return e;if(!g(e))return"";const t=typeof e.message=="string"?e.message:"";return Array.isArray(e.errors)?`${t} ${JSON.stringify(e.errors)}`:t}function y(e){if(!(e instanceof Error))return"the request failed";if(e.name==="TimeoutError")return`no answer within ${String(l/1e3)} seconds`;const t=e.cause;if(t instanceof Error){if(t.message.length>0)return t.message;if("code"in t&&typeof t.code=="string")return t.code}return e.message}function T(e){const t=new URL(e);return`${t.origin}${t.pathname}`}
@@ -0,0 +1,4 @@
1
+ import type { Tracker, TrackerTarget } from "./types.js";
2
+ export { TrackerError } from "./http.js";
3
+ export type * from "./types.js";
4
+ export declare function createTracker(target: TrackerTarget): Tracker;
@@ -0,0 +1 @@
1
+ import{createGiteaTracker as e}from"./gitea.js";import{createGithubTracker as o}from"./github.js";export{TrackerError}from"./http.js";export function createTracker(r){return r.provider==="github"?o(r):e(r)}
@@ -0,0 +1,4 @@
1
+ export declare function redactBugText(value: string): string;
2
+ export declare function redactRunResultText(value: string): string;
3
+ export declare function holdsRedaction(value: string): boolean;
4
+ export declare function redactUntilUnchanged(value: string, redact: (text: string) => string): string;
@@ -0,0 +1,2 @@
1
+ import{redactSensitiveText as d,SENSITIVE_KEY_PATTERN as T}from"../kernel/redaction.js";const c="<redacted>",E=/<redacted>(?!@)/u,A=/\b([A-Z0-9_]+)=(?=\S)/giu,R=/(?:^|_)URL$/iu,f=/["']?[a-z][a-z0-9+.-]*:\/\//iuy,_=/(?:basic|bearer|digest|negotiate|token)[ \t]/iuy,l=/(:\/\/[^:\s/@]*:)[^\s/]+(?=@)/gu,a=/\s/u,g=/(["'`]?)\b(?:(?:proxy-)?authorization|(?:set-)?cookie)[ \t]*:[ \t]*(?=\S)/giu,S=/"([^"\\\r\n]+)"(\s*:\s*)"(?:[^"\\\r\n]|\\.?)*("|$)/gmu,h=8;export function redactBugText(t){return redactUntilUnchanged(t,r=>P(I(p(d(x(r))))))}export function redactRunResultText(t){const r=t.replace(/\s+/gu," ").trim(),e=E.exec(r);return redactBugText(e===null?r:r.slice(0,e.index+c.length))}export function holdsRedaction(t){return t.includes(c)}export function redactUntilUnchanged(t,r){let e=t;for(let n=0;n<h;n+=1){const s=r(e);if(s===e)return e;e=s}return c}function x(t){const r=[];let e=0;for(const n of t.matchAll(A)){if(n.index<e)continue;const s=n[1];if(!T.test(s))continue;const o=n.index+n[0].length;R.test(s)&&u(f,t,o)||(r.push(t.slice(e,o),c),e=N(t,o))}return r.push(t.slice(e)),r.join("")}function N(t,r){const e=t[r];if(e==='"'||e==="'"){const n=i(t,r+1,e);return t[n]===e?n+1:n}if(u(_,t,r))return i(t,r,"");for(let n=r;n<t.length;n+=1)if(a.test(t[n]))return n;return t.length}function u(t,r,e){return t.lastIndex=e,t.test(r)}function p(t){return t.replace(l,`$1${c}`)}function I(t){const r=[];let e=0;for(const n of t.matchAll(g)){if(n.index<e)continue;const s=n.index+n[0].length;r.push(t.slice(e,s),c),e=i(t,s,n[1])}return r.push(t.slice(e)),r.join("")}function i(t,r,e){for(let n=r;n<t.length;n+=1){const s=t[n];if(s==="\r"||s===`
2
+ `||s===e&&t[n-1]!=="\\")return n}return t.length}function P(t){return t.replace(S,(r,e,n,s)=>T.test(e)?`"${e}"${n}"${c}${s}`:r)}
@@ -0,0 +1,17 @@
1
+ import type { TrackerRequest } from "./http.js";
2
+ import type { TrackerCommentRecord } from "./responses.js";
3
+ import type { TrackerCommitStatus, TrackerPullRequestComment, TrackerTarget } from "./types.js";
4
+ export declare const TRACKER_USER_AGENT = "testkit";
5
+ export type TrackerRepository = Omit<TrackerTarget, "token">;
6
+ export interface TrackerRoutes {
7
+ readonly issues: string;
8
+ readonly labels: string;
9
+ comment(id: number): string;
10
+ comments(pullRequest: number): string;
11
+ status(sha: string): string;
12
+ }
13
+ export declare function trackerRepository(target: TrackerTarget): TrackerRepository;
14
+ export declare function trackerRoutes(repository: TrackerRepository): TrackerRoutes;
15
+ export declare function getRequest(url: string, query: Readonly<Record<string, string>>): TrackerRequest;
16
+ export declare function commitStatusRequest(routes: TrackerRoutes, status: TrackerCommitStatus): TrackerRequest;
17
+ export declare function commentRequest(routes: TrackerRoutes, comment: TrackerPullRequestComment, existing: TrackerCommentRecord | undefined): TrackerRequest;
@@ -0,0 +1 @@
1
+ export const TRACKER_USER_AGENT="testkit";export function trackerRepository(t){return{apiUrl:t.apiUrl.replace(/\/+$/u,""),name:t.name,owner:t.owner,provider:t.provider}}export function trackerRoutes(t){const e=`${t.apiUrl}/repos/${encodeURIComponent(t.owner)}/${encodeURIComponent(t.name)}`;return{comment:o=>`${e}/issues/comments/${String(o)}`,comments:o=>`${e}/issues/${String(o)}/comments`,issues:`${e}/issues`,labels:`${e}/labels`,status:o=>`${e}/statuses/${encodeURIComponent(o)}`}}export function getRequest(t,e){return{method:"GET",url:`${t}?${new URLSearchParams(e).toString()}`}}export function commitStatusRequest(t,e){return{body:{context:e.context,description:e.description,state:e.state,...e.targetUrl===null?{}:{target_url:e.targetUrl}},method:"POST",url:t.status(e.sha)}}export function commentRequest(t,e,o){return o===void 0?{body:{body:e.body},method:"POST",url:t.comments(e.pullRequest)}:{body:{body:e.body},method:"PATCH",url:t.comment(o.id)}}
@@ -0,0 +1,24 @@
1
+ import type { TrackerIssue, TrackerTarget } from "./types.js";
2
+ export interface TrackerLabelRecord {
3
+ readonly id: number;
4
+ readonly name: string;
5
+ }
6
+ export interface TrackerIssueRecord {
7
+ readonly issue: TrackerIssue;
8
+ readonly pullRequest: boolean;
9
+ }
10
+ export interface TrackerCommentRecord {
11
+ readonly body: string;
12
+ readonly id: number;
13
+ readonly url: string;
14
+ }
15
+ export declare function decodeTrackerLabel(value: unknown, source: string): TrackerLabelRecord;
16
+ export declare function decodeTrackerLabels(value: unknown, source: string): TrackerLabelRecord[];
17
+ export declare function decodeTrackerIssue(value: unknown, source: string): TrackerIssueRecord;
18
+ export declare function requireIssueLabels(issue: TrackerIssue, labels: readonly string[], tracker: string, repository: Pick<TrackerTarget, "name" | "owner">): TrackerIssue;
19
+ export declare function decodeTrackerIssues(value: unknown, source: string): TrackerIssueRecord[];
20
+ export declare function decodeTrackerComment(value: unknown, source: string): TrackerCommentRecord;
21
+ export declare function decodeTrackerComments(value: unknown, source: string): TrackerCommentRecord[];
22
+ export declare function decodeTrackerCommitStatusContext(value: unknown, source: string): string;
23
+ export declare function isDuplicateLabelRejection(value: unknown): boolean;
24
+ export declare function decodeGiteaVersion(value: unknown): string | null;
@@ -0,0 +1 @@
1
+ import{isUnknownRecord as u}from"../kernel/unknown-values.js";import{TrackerError as c}from"./http.js";export function decodeTrackerLabel(t,e){const n=s(t,e,"label");return{id:m(n.id,e,"label.id"),name:i(n.name,e,"label.name")}}export function decodeTrackerLabels(t,e){return a(t,e,"labels").map(n=>decodeTrackerLabel(n,e))}export function decodeTrackerIssue(t,e){const n=s(t,e,"issue");return{issue:{body:f(n.body,e,"issue.body"),labels:(n.labels===void 0?[]:a(n.labels,e,"issue.labels")).map(r=>typeof r=="string"?r:i(s(r,e,"issue.labels[]").name,e,"issue.labels[].name")),number:m(n.number,e,"issue.number"),title:i(n.title,e,"issue.title"),url:b(n.html_url,e,"issue.html_url")},pullRequest:n.pull_request!==void 0&&n.pull_request!==null}}export function requireIssueLabels(t,e,n,r){const p=new Set(t.labels.map(l=>l.toLowerCase())),d=e.filter(l=>!p.has(l.toLowerCase()));if(d.length===0)return t;const h=`${r.owner}/${r.name}`;throw new c("forbidden",`${n} created ${t.url} without these labels: ${d.join(", ")}. The token's user may not label issues in ${h}. Close that issue, then run this again with a token whose user may label issues.`)}export function decodeTrackerIssues(t,e){return a(t,e,"issues").map(n=>decodeTrackerIssue(n,e))}export function decodeTrackerComment(t,e){const n=s(t,e,"comment");return{body:f(n.body,e,"comment.body"),id:m(n.id,e,"comment.id"),url:b(n.html_url,e,"comment.html_url")}}export function decodeTrackerComments(t,e){return a(t,e,"comments").map(n=>decodeTrackerComment(n,e))}export function decodeTrackerCommitStatusContext(t,e){return i(s(t,e,"commit status").context,e,"commit status.context")}export function isDuplicateLabelRejection(t){return!u(t)||!Array.isArray(t.errors)?!1:t.errors.some(e=>u(e)&&e.code==="already_exists")}export function decodeGiteaVersion(t){return u(t)&&typeof t.version=="string"&&t.version.length>0?t.version:null}function o(t,e,n){return new c("rejected",`${t} returned a response Testkit cannot read: ${e} must be ${n}`)}function s(t,e,n){if(!u(t))throw o(e,n,"an object");return t}function a(t,e,n){if(!Array.isArray(t))throw o(e,n,"a list");return t}function i(t,e,n){if(typeof t!="string")throw o(e,n,"a string");return t}function f(t,e,n){return t==null?"":i(t,e,n)}function m(t,e,n){if(typeof t!="number"||!Number.isSafeInteger(t)||t<=0)throw o(e,n,"a positive integer");return t}function b(t,e,n){const r=i(t,e,n);if(!/^https?:\/\/[^\s/]/u.test(r)||!URL.canParse(r))throw o(e,n,"an http or https URL");return r}
@@ -0,0 +1,51 @@
1
+ export type TrackerProvider = "gitea" | "github";
2
+ export interface TrackerTarget {
3
+ readonly apiUrl: string;
4
+ readonly name: string;
5
+ readonly owner: string;
6
+ readonly provider: TrackerProvider;
7
+ readonly token: string;
8
+ }
9
+ export interface TrackerLabel {
10
+ readonly color: string;
11
+ readonly description: string;
12
+ readonly name: string;
13
+ }
14
+ export interface TrackerIssue {
15
+ readonly body: string;
16
+ readonly labels: readonly string[];
17
+ readonly number: number;
18
+ readonly title: string;
19
+ readonly url: string;
20
+ }
21
+ export interface TrackerIssueDraft {
22
+ readonly body: string;
23
+ readonly labels: readonly string[];
24
+ readonly title: string;
25
+ }
26
+ export type TrackerCommitState = "failure" | "success";
27
+ export interface TrackerCommitStatus {
28
+ readonly context: string;
29
+ readonly description: string;
30
+ readonly sha: string;
31
+ readonly state: TrackerCommitState;
32
+ readonly targetUrl: string | null;
33
+ }
34
+ export interface TrackerPullRequestComment {
35
+ readonly body: string;
36
+ readonly marker: string;
37
+ readonly pullRequest: number;
38
+ }
39
+ export interface TrackerCommentReference {
40
+ readonly created: boolean;
41
+ readonly url: string;
42
+ }
43
+ export type TrackerFailureKind = "forbidden" | "not-found" | "rejected" | "unauthorized" | "unreachable";
44
+ export interface Tracker {
45
+ readonly target: Omit<TrackerTarget, "token">;
46
+ createIssue(draft: TrackerIssueDraft): Promise<TrackerIssue>;
47
+ ensureLabels(labels: readonly TrackerLabel[]): Promise<void>;
48
+ listOpenIssues(label: string): Promise<readonly TrackerIssue[]>;
49
+ setCommitStatus(status: TrackerCommitStatus): Promise<void>;
50
+ upsertPullRequestComment(comment: TrackerPullRequestComment): Promise<TrackerCommentReference>;
51
+ }
@@ -0,0 +1 @@
1
+ export{};