@gfdelarue/testkit 0.6.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +455 -56
- package/bin/testkit-windows-owner-arm64.exe +0 -0
- package/bin/testkit-windows-owner-x64.exe +0 -0
- package/dist/app/bugs/batch.js +1 -0
- package/dist/app/bugs/ci-report.js +2 -2
- package/dist/app/bugs/ci-summary.js +3 -3
- package/dist/app/bugs/draft.js +2 -2
- package/dist/app/bugs/file.js +1 -1
- package/dist/app/bugs/guide.js +1 -1
- package/dist/app/bugs/record.js +1 -1
- package/dist/app/bugs/run-reading.js +1 -1
- package/dist/app/bugs/store.js +1 -1
- package/dist/app/campaigns/summary.js +1 -1
- package/dist/app/cleanup/index.js +1 -1
- package/dist/app/db-schema/prepare.js +1 -1
- package/dist/app/db-schema/refresh.js +1 -1
- package/dist/app/db-schema/verify.js +1 -1
- package/dist/app/destroy/index.js +1 -1
- package/dist/app/environments/config-driven.js +1 -1
- package/dist/app/environments/file-setup.js +1 -1
- package/dist/app/environments/routing.js +1 -1
- package/dist/app/environments/static-values.js +1 -1
- package/dist/app/environments/supplied.js +1 -1
- package/dist/app/lint/shared.js +1 -1
- package/dist/app/run/live-run.js +1 -1
- package/dist/app/run/orchestrator.js +1 -1
- package/dist/app/run/planning.js +1 -1
- package/dist/app/run/results.js +1 -1
- package/dist/app/run/run-finalization.js +1 -1
- package/dist/app/run/run-finisher.js +1 -0
- package/dist/app/run/setup/runtime-node-setup-protocol.js +1 -1
- package/dist/app/run/setup/runtime-node-setup-worker.js +1 -1
- package/dist/app/run/setup/runtime-node-setup.js +2 -2
- package/dist/app/status/runs.js +1 -1
- package/dist/app/stop/index.js +1 -1
- package/dist/app/tracker/index.js +1 -1
- package/dist/app/tracker/token.js +2 -2
- package/dist/app/typecheck/index.js +4 -4
- package/dist/cli/command-catalog.js +1 -1
- package/dist/cli/command-flags.js +1 -1
- package/dist/cli/commands/bug/file.js +3 -2
- package/dist/cli/framework/help.js +5 -5
- package/dist/cli/framework/parser.js +1 -1
- package/dist/cli/json-errors.js +1 -1
- package/dist/cli/renderers/bug/text.js +1 -1
- package/dist/cli/renderers/run/formatting.js +1 -1
- package/dist/config/loader/config-loader.js +1 -1
- package/dist/config/loader/discover-tests.js +1 -1
- package/dist/config/loader/discovery-result.js +1 -0
- package/dist/config/loader/index.js +1 -1
- package/dist/config/loader/runtime-isolation.js +1 -1
- package/dist/config/loader/skip-config.js +1 -1
- package/dist/config/loader/test-selection.js +1 -1
- package/dist/database/admin.js +5 -2
- package/dist/database/call-caps.js +1 -0
- package/dist/database/cleanup.js +1 -1
- package/dist/database/configured-steps.js +1 -1
- package/dist/database/constants.js +1 -1
- package/dist/database/docker-endpoint.js +1 -0
- package/dist/database/docker-metadata.js +2 -2
- package/dist/database/docker.js +5 -5
- package/dist/database/index.js +2 -1
- package/dist/database/lifecycle-cleanup.js +1 -1
- package/dist/database/local-postgres.js +4 -1
- package/dist/database/naming.js +1 -1
- package/dist/database/operation-deadline.js +1 -0
- package/dist/database/postgres/connection-check.js +1 -1
- package/dist/database/postgres/index.js +2 -2
- package/dist/database/postgres/owned-exec.js +27 -0
- package/dist/database/postgres/protocol.js +2 -2
- package/dist/database/provider.js +1 -0
- package/dist/database/roles.js +2 -2
- package/dist/database/schema-operations.js +5 -1
- package/dist/database/schema-source.js +3 -3
- package/dist/database/steps.js +1 -1
- package/dist/database/template-steps.js +4 -4
- package/dist/domain/suite-selection.js +1 -1
- package/dist/engines/adapters/evidence-files.js +1 -1
- package/dist/engines/adapters/execution.js +1 -1
- package/dist/engines/node-vitest/adapter/adapter.js +1 -1
- package/dist/engines/node-vitest/adapter/driver.js +2 -2
- package/dist/engines/node-vitest/runtime/runtime-input.js +1 -1
- package/dist/engines/playwright/adapter/adapter.js +1 -1
- package/dist/engines/playwright/adapter/browser-network-evidence.js +4 -0
- package/dist/engines/playwright/adapter/config.js +16 -6
- package/dist/engines/playwright/adapter/evidence.js +2 -1
- package/dist/engines/playwright/adapter/failure-details.js +2 -2
- package/dist/engines/playwright/adapter/reporter.js +1 -1
- package/dist/engines/playwright/runtime/browser-network.js +1 -0
- package/dist/engines/playwright/runtime/browser-traffic.js +1 -0
- package/dist/engines/playwright/runtime/chromium-environment.js +1 -0
- package/dist/engines/playwright/runtime/emulation.js +1 -0
- package/dist/engines/playwright/runtime/index.js +1 -1
- package/dist/engines/playwright/runtime/managed-test.js +1 -1
- package/dist/engines/playwright/runtime/runtime-input.js +2 -2
- package/dist/environment/lifecycle-controller.js +1 -1
- package/dist/environment/lifecycle.js +1 -1
- package/dist/environment/maintenance.js +1 -1
- package/dist/environment/run-environment.js +1 -1
- package/dist/environment/run-stopping.js +1 -1
- package/dist/environment/runtime-manager.js +1 -1
- package/dist/evidence/artifacts.js +5 -2
- package/dist/evidence/bundles.js +2 -1
- package/dist/evidence/compressed-artifacts.js +1 -0
- package/dist/evidence/environment-record.js +1 -1
- package/dist/evidence/index.js +1 -1
- package/dist/evidence/live-run-summary.js +1 -0
- package/dist/evidence/local-paths.js +1 -0
- package/dist/evidence/maintenance.js +1 -1
- package/dist/evidence/operation-timings.js +1 -1
- package/dist/evidence/redaction.js +4 -4
- package/dist/evidence/run-evidence.js +1 -0
- package/dist/evidence/setup-progress.js +1 -1
- package/dist/evidence/storage-inventory.js +1 -1
- package/dist/evidence/storage.js +2 -2
- package/dist/kernel/redaction-targets.js +1 -1
- package/dist/kernel/redaction.js +7 -4
- package/dist/process/engine-environment-keys.js +1 -0
- package/dist/process/environment-overlay.js +1 -1
- package/dist/process/environment.js +1 -1
- package/dist/process/execa-bundle.json +3 -3
- package/dist/process/files/evidence-writes.js +1 -0
- package/dist/process/files/product-dir.js +1 -1
- package/dist/process/host-process.js +1 -1
- package/dist/process/index.js +1 -1
- package/dist/process/lifecycle-fence.js +1 -1
- package/dist/process/lock-progress.js +2 -0
- package/dist/process/locks.js +4 -4
- package/dist/process/process-identity.js +1 -1
- package/dist/process/process.js +35 -35
- package/dist/process/recovery.js +1 -1
- package/dist/process/runs/manifest.js +1 -1
- package/dist/process/runs/ownership.js +1 -1
- package/dist/process/runs/stop.js +1 -1
- package/dist/process/shutdown.js +1 -1
- package/dist/process/windows-native.js +1 -1
- package/dist/repo/history.js +7 -4
- package/dist/repo/managed-git.js +1 -1
- package/dist/repo/state.js +4 -4
- package/dist/steps/environment.js +1 -1
- package/dist/toolchains/node-provisioning.js +2 -2
- package/dist/tracker/gitea.js +1 -1
- package/dist/tracker/github.js +1 -1
- package/dist/tracker/http.js +1 -1
- package/dist/tracker/ownership.js +1 -0
- package/dist/tracker/redaction.js +1 -1
- package/dist/tracker/responses.js +1 -1
- package/dist/types/playwright.d.ts +21 -0
- package/dist/types/workspace.d.ts +4 -4
- package/docs/bug-records.md +16 -10
- package/docs/engines.md +52 -4
- package/docs/release-notes.md +730 -0
- package/package.json +14 -2
- package/testkit-config-schema.md +36 -42
- package/dist/config/loader/task-policy.js +0 -1
- package/dist/environment/lifecycle-artifacts.js +0 -1
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import I from"node:path";import{setTimeout as S}from"node:timers/promises";import{loadTestkitConfig as F}from"../../config/loader/index.js";import{stableJson as v}from"../../evidence/storage.js";import{listCommitFiles as R}from"../../repo/tree-listing.js";import{TrackerError as E}from"../../tracker/http.js";import{namesSameTest as A}from"./draft.js";import{readFactsBlock as C,renderFactsBlock as y}from"./facts.js";import{bugFiling as D,bugRerun as $,readOpenIssues as N,readyBugFiling as q,recordFiledBug as k,renderReadableBugIssue as x,requireCleanPushedCheckout as L,requireCommittedDraftFiles as O,requireFileableDraft as _,requireNoOpenIssue as B,requireRecordedFailure as j,requireReplayableSuppliedInput as M,resolveBugTracker as J,shortCommit as K}from"./file.js";import{bugFacts as P,bugLabels as V,TESTKIT_LABEL as G}from"./issue.js";import{readKnownTests as U}from"./known-failures.js";import{bugDraftFile as W,displayPath as Y,readStoredDrafts as z,resolveBugCheckout as H}from"./store.js";import{suppliedReproduceArgs as Q}from"./supplied-reproduce.js";const X=1e3;export async function prepareBugBatch(e){const d=Q(e.supplied??{}),t=await H(e),{candidates:n,order:i,settled:a}=Z(t,e.ids);if(n.length===0)return{checkout:t,order:i,ready:[],settled:a};e.supplied!==void 0&&M(e.supplied,t,e.env);const o=await R(t.repoRoot,t.headCommit);await L(t,e.env);const{config:s}=await F(t.productDir),r=await J(t,s,e.env),l=await r.listOpenIssues(G),u=U(l).tests,p=[],f=[...a];for(const{draft:c,file:m}of n)try{_(c),O(c,o,t);const g=await D(t,s,o,c),b=q({checkout:t,draft:c,draftFile:m,filing:g,reproduceArgs:d,tracker:r}),w=ee(l,b);if(w!==null){f.push({id:c.id,issue:k(b,w,new Date).issue,status:"already-filed"});continue}B(u,c.test);const h=p.find(T=>A(T.draft,c));if(h!==void 0)throw new Error(`${h.draft.id} in this batch files the same test. Testkit keeps one open bug for each test.`);p.push(b)}catch(g){f.push(rejectedBugBatchDraft(c.id,g))}return{checkout:t,order:i,ready:p,settled:f}}export async function planBugBatchRuns(e,d,t={},n=process.env){const i=[],a=new Map;for(const o of e.ready)try{const s=await $(o,d,t,n),{vitest:r}=s.ready.draft,l=v(r??{}),u=a.get(l)??{drafts:[],files:new Set,vitest:r};u.drafts.push(s.ready);for(const p of s.files)u.files.add(p);a.set(l,u)}catch(s){i.push(rejectedBugBatchDraft(o.draft.id,s))}return{rejected:i,runs:[...a.values()].map(o=>({drafts:o.drafts,files:[...o.files].sort(),vitest:o.vitest}))}}export async function fileBugBatch(e,d={}){const t=e.at(0)?.ready.tracker;if(t===void 0)return[];let n;try{n=[...await N(t)]}catch(r){if(!(r instanceof Error))throw r;const l=`Testkit could not read the open issues again before filing: ${r.message}`;return e.map(({ready:u})=>({id:u.draft.id,reason:l,status:"not-filed"}))}const i=new Set,a=[];let o=null,s=Number.NEGATIVE_INFINITY;for(const{ready:r,run:l}of e){const{id:u}=r.draft;if(o!==null){a.push({id:u,reason:o,status:"not-filed"});continue}let p;try{const f=j(r.draft,l,K(r.checkout.headCommit),r.reproduceArgs);B(n,f),p=x(r.draft,r.filing,r.reproduceArgs)}catch(f){a.push(rejectedBugBatchDraft(u,f));continue}try{const f=V(r.draft.severity,r.filing.area).filter(({name:h})=>!i.has(h));await t.ensureLabels(f);for(const{name:h}of f)i.add(h);await S(Math.max(0,s+X-performance.now()));const c=await t.createIssue(p);s=performance.now();const m=k(r,c,d.at??new Date);n.push({file:r.draft.test.file,issue:m.issue.number,title:r.draft.test.title,url:m.issue.url});const{area:g,introducedBy:b,path:w}=m;a.push({area:g,id:u,introducedBy:b,issue:m.issue,path:w,status:"filed"})}catch(f){if(!(f instanceof Error))throw f;const c=te(f);a.push({id:u,reason:`${f.message}${c}`,status:"not-filed"}),o=`Testkit stopped filing when ${u} failed.${c}`}}return a}export function rejectedBugBatchDraft(e,d){if(!(d instanceof Error))throw d;return{id:e,reason:d.message,status:"rejected"}}export function bugBatchReport(e,d){const t=[...d].sort((n,i)=>e.order.indexOf(n.id)-e.order.indexOf(i.id));return{drafts:t,ok:t.every(({status:n})=>n==="filed"||n==="already-filed")}}function Z(e,d){const t=z(e.productDir),n=new Map(t.unreadable.map(s=>[s.file,s.problem])),i=d.length>0?[...new Set(d)]:[...t.drafts.filter(({draft:s})=>s.filed===void 0).map(({draft:s})=>s.id),...t.unreadable.map(({file:s})=>I.basename(s,".json"))].sort(),a=[],o=[];for(const s of i){const r=t.drafts.find(({draft:p})=>p.id===s),l=W(e.productDir,s),u=n.get(l);r?.draft.filed!==void 0?o.push({id:s,issue:{number:r.draft.filed.number,url:r.draft.filed.url},status:"already-filed"}):r!==void 0?a.push(r):o.push({id:s,reason:u===void 0?`No bug draft has the id ${JSON.stringify(s)}.`:`Testkit cannot read ${Y(e,l)}: ${u}. Record the bug again with \`testkit bug record\`.`,status:"rejected"})}return{candidates:a,order:i,settled:o}}function ee(e,{draft:d,filing:t}){const n=y(P(d,t));for(const i of e){const a=C(i.body);if(a.status==="ok"&&y(a.facts)===n)return{number:i.number,url:i.url}}return null}function te(e){return!(e instanceof E)||e.kind!=="rate-limited"?"":e.retryAfterSeconds===null?" The tracker is limiting requests. Run the batch again later; it skips the drafts it filed.":` The tracker is limiting requests. Run the batch again in ${String(e.retryAfterSeconds)} seconds; it skips the drafts it filed.`}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import k from"node:fs";import
|
|
2
|
-
`),
|
|
1
|
+
import k from"node:fs";import{publishableUrl as h}from"../../tracker/redaction.js";import v from"node:path";import{readBugPolicy as b}from"../../config/loader/bugs.js";import{loadTestkitConfig as g}from"../../config/loader/index.js";import{resolveCiProvenance as T}from"../../evidence/ci-provenance.js";import{loadLatestRun as q}from"../../evidence/reading.js";import{resolveTracker as w}from"../tracker/index.js";import{assessRun as P}from"./assessment.js";import{ciReportMarker as U,readCiContext as x,renderCiSummary as D}from"./ci-summary.js";import{describeRunOutcome as F}from"./known-failures.js";import{readRun as B}from"./run-reading.js";import{resolveBugCheckout as I}from"./store.js";const R="Testkit found no results for this commit";export async function reportCiRun(t){const e=x(t.context),r=await I(t),s=b((await g(r.productDir)).config),a={env:t.env,productDir:r.productDir};let u=null;const l=()=>(u??=w(a),u),{repository:m}=s,o=await P(L(r),m===null?l:()=>w({...a,repository:m})),n=F(o.outcome,s.knownFailures==="allow"),i=r.pullRequestHeadCommit??r.headCommit,c=T(t.env).runUrl,p=D({...o,commit:i,context:e,knownFailuresAllowed:s.knownFailures==="allow",runUrl:c,status:n}),C=M(t.env,p),d=await l(),{creator:S}=await d.setCommitStatus({...n,context:e,sha:i,targetUrl:h(c)}),y=r.pullRequest===null?null:await d.upsertPullRequestComment({author:S,body:p,marker:U(e),pullRequest:r.pullRequest}),f=o.outcome.kind==="tests-failed"?o.outcome.classification:{known:[],new:[]};return{comment:y,commit:i,context:e,description:n.description,known:f.known,new:f.new,pullRequest:r.pullRequest,state:n.state,stepSummary:C,trackerProblem:o.trackerProblem,unreadableIssues:o.unreadableIssues}}function L(t){if(!k.existsSync(v.join(t.productDir,".testkit","results","latest.json")))return{kind:"run-failed",reason:R};const e=q(t.productDir);return e.git.commitSha!==t.headCommit?{kind:"run-failed",reason:R}:B(e,t.productPath)}function M(t,e){const r=t.GITHUB_STEP_SUMMARY?.trim()??"";return r.length===0?null:(k.appendFileSync(r,`${e}
|
|
2
|
+
`),r)}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import"./known-failures.js";import{
|
|
1
|
+
import"./known-failures.js";import{publishableUrl as T,redactBugText as I,redactRunResultText as p}from"../../tracker/redaction.js";export const DEFAULT_CI_CONTEXT="testkit";const k=/^[A-Za-z0-9][A-Za-z0-9 ._/:-]{0,59}$/u,b=12,u=50,w=10,d=160,E=/^https?:\/\/[^\s()<>]+$/u;export function readCiContext(e){if(e===void 0)return DEFAULT_CI_CONTEXT;if(!k.test(e)||e!==e.trim())throw new Error(`--context must be 1 to 60 characters, start with an ASCII letter or a digit, not end with a space, and hold only ASCII letters, digits, spaces, and the characters . _ / : and -. Received ${JSON.stringify(e)}.`);return e}export function ciReportMarker(e){return e===DEFAULT_CI_CONTEXT?"<!-- testkit-ci-report -->":`<!-- testkit-ci-report:${e} -->`}export function renderCiSummary(e){const{context:t,outcome:n}=e,r=[ciReportMarker(t),t===DEFAULT_CI_CONTEXT?"## Testkit":`## Testkit: ${t}`,_(e),`Commit \`${e.commit.slice(0,b)}\`.${h("CI run",e.runUrl," ",".")}`];if(n.kind==="tests-failed"){const{known:i,new:o}=n.classification;if(o.length>0&&r.push("### New failures",f(o.map(s=>`- ${c(s.file)}: ${$(s)}`))),i.length>0&&r.push("### Known failures",f(i.map(({failure:s,issue:a})=>`- ${c(s.file)}: ${$(s)}, ${g(a.issue,a.url)}`))),e.trackerProblem!==null&&r.push(`Testkit could not read the open bugs, so every failure counts as new. ${m(e.trackerProblem,400)}`),e.unreadableIssues.length>0){const s=e.unreadableIssues.slice(0,w).map(l=>g(l.number,l.url)),a=e.unreadableIssues.length-s.length;r.push(`Testkit cannot read the facts block of ${C(e.unreadableIssues.length,"open `testkit` issue")}, so ${e.unreadableIssues.length===1?"it matches":"they match"} no test: ${s.join(", ")}${a>0?`, and ${String(a)} more`:""}.`)}}return`${r.join(`
|
|
2
2
|
|
|
3
3
|
`)}
|
|
4
|
-
`}function
|
|
5
|
-
`)}function h(e,t,n="",r=""){return
|
|
4
|
+
`}function _({knownFailuresAllowed:e,outcome:t,status:n}){return t.kind==="passed"?`${n.description}.`:t.kind==="run-failed"?`The run failed outside its tests. ${c(t.reason,400)}`:t.classification.new.length>0?`${n.description}.`:e?`${n.description}. Every failure is a known bug and \`bugs.knownFailures\` is \`"allow"\`, so the run passes.`:`${n.description}. Every failure is a known bug, and \`bugs.knownFailures\` is \`"fail"\`, so the run fails.`}function f(e){const t=e.length-u;return[...e.slice(0,u),...t>0?[`- and ${String(t)} more`]:[]].join(`
|
|
5
|
+
`)}function h(e,t,n="",r=""){const i=T(t);return i!==null&&E.test(i)?`${n}[${e}](${i})${r}`:""}function $(e){return e.title===null?"failed without naming a test":c(e.title)}function g(e,t){const n=`#${String(e)}`;return h(n,t)||n}function C(e,t){return`${String(e)} ${t}${e===1?"":"s"}`}function c(e,t=d){return m(p(e),t)}function m(e,t=d){const n=Array.from(I(e.replace(/\s+/gu," ").trim())),r=n.length>t?`${n.slice(0,t-3).join("")}...`:n.join("");if(r.length===0)return"an empty name";const i=Math.max(0,...[...r.matchAll(/`+/gu)].map(s=>s[0].length)),o="`".repeat(i+1);return r.startsWith("`")||r.endsWith("`")?`${o} ${r} ${o}`:`${o}${r}${o}`}
|
package/dist/app/bugs/draft.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import G from"node:crypto";import A from"node:path";import{isPublicTestType as j}from"../../domain/identifiers.js";import{isUnknownRecord as g}from"../../kernel/unknown-values.js";import{BUG_CATCHERS as O,BUG_CAUSES as P,BUG_CONFIDENCES as C,BUG_ESCAPES as U,BUG_IMPACTS as F,BUG_SEVERITIES as R,categoryIds as _,isOneOf as E}from"./categories.js";import{BUG_FACTS_TEST_TYPES as z,decodeFaultLines as D,isCommitId as $}from"./facts.js";import{redactBugText as B}from"../../tracker/redaction.js";import{isTimestamp as w}from"../campaigns/campaign.js";export const BUG_DRAFT_SCHEMA_VERSION=1,BUG_TITLE_MAX_LENGTH=200,BUG_SECTION_MAX_LENGTH=12e3;const V=48,L=/^[a-z0-9]+(?:-[a-z0-9]+)*$/u;export const CAMPAIGN_ID_MAX_LENGTH=60;export class BugRecordError extends Error{problems;constructor(e,n){super(`${e}:
|
|
2
2
|
${n.map(s=>`- ${s}`).join(`
|
|
3
|
-
`)}`),this.name="BugRecordError",this.problems=n}}export function isCampaignId(t){return typeof t=="string"&&t.length<=CAMPAIGN_ID_MAX_LENGTH&&L.test(t)}export function testTypeOfFile(t){const e=/\.([a-z0-9]+)\.testkit\.ts$/u.exec(t)?.[1];return j(e)?e:null}function M(t){const e=/\.([a-z0-9]+)\.testkit\.ts$/u.exec(t)?.[1];return z.find(n=>n===e)??null}export function toRepositoryPath(t,e){const n
|
|
4
|
-
${t.test.title}`).digest("hex");return`${e||"bug"}-${n.slice(0,8)}`}export function isBugDraftId(t){return typeof t=="string"&&/^[a-z0-9]+(?:-[a-z0-9]+)*-[0-9a-f]{8}$/u.test(t)}export function namesSameTest(t,e){return t.test.file===e.test.file&&t.test.title===e.test.title}function I(t,e,n){const s=(
|
|
3
|
+
`)}`),this.name="BugRecordError",this.problems=n}}export function isCampaignId(t){return typeof t=="string"&&t.length<=CAMPAIGN_ID_MAX_LENGTH&&L.test(t)}export function testTypeOfFile(t){const e=/\.([a-z0-9]+)\.testkit\.ts$/u.exec(t)?.[1];return j(e)?e:null}function M(t){const e=/\.([a-z0-9]+)\.testkit\.ts$/u.exec(t)?.[1];return z.find(n=>n===e)??null}export function toRepositoryPath(t,e,n=process.platform){const s=n==="win32"?e.replaceAll("\\","/"):e;if(s.startsWith("/")||/^[a-z]:/iu.test(s))return null;const i=A.posix.normalize(A.posix.join(t,s));return i==="."||i===".."||i.startsWith("../")?null:i}export function toProductPath(t,e){return t===""?e:A.posix.relative(t,e)}export function readBugReport(t,e){if(!g(t))throw new BugRecordError("The bug record is invalid",["it must be a JSON object"]);const n=[],s=[],i=I(t,n,{path:r=>toRepositoryPath(e,r),text:(r,d)=>{const a=B(r);return a!==r&&s.push(d),a}});if(n.length>0)throw new BugRecordError("The bug record is invalid",n);return{redactedFields:s,report:i}}export function createBugDraft(t,e){const n=testTypeOfFile(t.test.file);return{schemaVersion:BUG_DRAFT_SCHEMA_VERSION,id:bugDraftId(t),recordedAt:e.recordedAt.toISOString(),recordedCommit:e.recordedCommit,productPath:e.productPath,caughtBy:e.campaign===null?"manual":"campaign",...e.campaign===null?{}:{campaign:e.campaign},...n===null?{}:{testType:n},...e.vitest===void 0?{}:{vitest:e.vitest},...t}}export function requireOneVitestProject(t,e,n){if(e.length<2||n.project!==null)return;const s=e.map(({project:i})=>JSON.stringify(i)).join(", ");throw new Error(`test.file ${t} runs in the Vitest projects ${s}. Name the one whose run fails with --vitest-project.`)}export function readBugDraft(t){const e="The bug draft is unreadable";if(!g(t))throw new BugRecordError(e,["it must be a JSON object"]);if(t.schemaVersion!==BUG_DRAFT_SCHEMA_VERSION)throw new BugRecordError(e,[`unknown schemaVersion ${JSON.stringify(t.schemaVersion)}`]);const n=[],s=(p,y,b,x)=>y(p)?p:(n.push(x),b),i=I(t,n,{path:p=>toRepositoryPath("",p),text:p=>p}),r=t.testType===void 0&&testTypeOfFile(i.test.file)===null?null:M(i.test.file);t.testType!==(r??void 0)&&n.push(r===null?"testType must be absent for a test file without a type":"testType does not match test.file");const d=s(t.caughtBy,J,"manual","caughtBy must be campaign or manual"),a=s(t.campaign,k,void 0,"campaign must be a campaign id");d==="campaign"!=(a!==void 0)&&n.push("campaign must be present when caughtBy is campaign and absent otherwise");const T={schemaVersion:BUG_DRAFT_SCHEMA_VERSION,id:s(t.id,isBugDraftId,"","id is not a draft id"),recordedAt:s(t.recordedAt,w,"","recordedAt must be a timestamp"),recordedCommit:s(t.recordedCommit,$,"","recordedCommit must be a full commit id"),productPath:s(t.productPath,N,"","productPath must be a string"),caughtBy:d,...a===void 0?{}:{campaign:a},...r===null?{}:{testType:r},...t.vitest===void 0?{}:{vitest:s(t.vitest,X,{},"vitest must hold runner, config and project text")},...i,...t.filed===void 0?{}:{filed:H(t.filed,n)}};if(n.length>0)throw new BugRecordError(e,n);return T}export function bugDraftId(t){const e=t.title.normalize("NFKD").replace(/\p{M}+/gu,"").toLowerCase().replace(/[^a-z0-9]+/gu,"-").slice(0,V).replace(/^-+|-+$/gu,""),n=G.createHash("sha256").update(`${t.test.file}
|
|
4
|
+
${t.test.title}`).digest("hex");return`${e||"bug"}-${n.slice(0,8)}`}export function isBugDraftId(t){return typeof t=="string"&&/^[a-z0-9]+(?:-[a-z0-9]+)*-[0-9a-f]{8}$/u.test(t)}export function namesSameTest(t,e){return t.test.file===e.test.file&&t.test.title===e.test.title}function I(t,e,n){const s=(o,c,u,f)=>{const m=o[c];if(typeof m!="string"||m.trim().length===0)return e.push(`${u} must be a non-empty string`),"";const h=m.trim(),l=h.length>f?h:n.text(h,u);return l.length>f&&e.push(`${u} must be at most ${String(f)} characters`),l},i=(o,c,u)=>{const f=o[c],m=typeof f=="string"?f.trim():"",h=m.length>0?n.path(m):null;return h===null?e.push(`${u} must be a path inside the repository`):[m,h].some(l=>B(l)!==l)&&e.push(`${u} is a path that holds text Testkit redacts as a secret`),h??""},r=(o,c,u)=>E(o,c)?c:(e.push(`${u} must be one of ${o.join(", ")}`),o[0]),d=o=>{const c=t[o];return g(c)?c:(e.push(`${o} must be an object`),{})},a=(o,c,u)=>{const f=s(o,c,u,BUG_TITLE_MAX_LENGTH);return/[\r\n]/u.test(f)&&e.push(`${u} must be one line`),f},T=a(t,"title","title"),p=d("test"),y=d("fault"),b=d("confidence"),x=i(p,"file","test.file");let S=[1,1];try{S=D(y.lines)}catch(o){e.push(o instanceof Error?o.message:"fault.lines is invalid")}return{title:T,severity:r(R,t.severity,"severity"),expected:s(t,"expected","expected",BUG_SECTION_MAX_LENGTH),authority:s(t,"authority","authority",BUG_SECTION_MAX_LENGTH),observed:s(t,"observed","observed",BUG_SECTION_MAX_LENGTH),rootCause:s(t,"rootCause","rootCause",BUG_SECTION_MAX_LENGTH),test:{file:x,title:a(p,"title","test.title")},fault:{lines:S,path:i(y,"path","fault.path")},cause:r(_(P),t.cause,"cause"),impact:r(_(F),t.impact,"impact"),confidence:{cause:r(C,b.cause,"confidence.cause"),impact:r(C,b.impact,"confidence.impact")},escape:r(_(U),t.escape,"escape")}}function H(t,e){return g(t)&&w(t.filedAt)&&$(t.foundAt)&&typeof t.number=="number"&&Number.isSafeInteger(t.number)&&t.number>=1&&typeof t.url=="string"?{filedAt:t.filedAt,foundAt:t.foundAt,number:t.number,url:t.url}:(e.push("filed must hold filedAt, foundAt, number, and url"),{filedAt:"",foundAt:"",number:0,url:""})}function k(t){return t===void 0||isCampaignId(t)}function J(t){return E(O,t)}function X(t){return g(t)&&Object.values(t).every(N)}function N(t){return typeof t=="string"}
|
package/dist/app/bugs/file.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import b from"node:fs";import p from"node:path";import{readAreaMap as C,readBugPolicy as O}from"../../config/loader/bugs.js";import{loadTestkitConfig as D}from"../../config/loader/index.js";import{findIntroducingCommit as j,listRemoteRefsContaining as x,listUncommittedTrackedFiles as B}from"../../repo/history.js";import{listCommitFiles as N}from"../../repo/tree-listing.js";import{resolveTracker as _}from"../tracker/index.js";import{isTypedTestFile as P}from"../../domain/identifiers.js";import{rejectRetiredTestFile as L}from"../../domain/retired-test-types.js";import{listPlainVitestFiles as M,resolveVitestSelection as J}from"../../engines/catalog.js";import{resolveFaultArea as q}from"./areas.js";import{requireOneVitestProject as K,toProductPath as g}from"./draft.js";import{FACTS_END_MARKER as U,FACTS_START_MARKER as V,readFactsBlock as W,renderFactsBlock as T}from"./facts.js";import{bugFacts as G,bugLabels as H,renderBugIssue as Q,reproduceCommand as y,TESTKIT_LABEL as z}from"./issue.js";import{findKnownTest as X,readKnownTests as Y}from"./known-failures.js";import{holdsRedaction as Z,redactBugText as w}from"../../tracker/redaction.js";import{readEnvironmentFile as E}from"../environments/environment-file.js";import{resolveSuppliedInput as k,suppliedBindings as tt}from"../environments/supplied-input.js";import{suppliedReproduceArgs as et}from"./supplied-reproduce.js";import{bugDraftFile as rt,displayPath as v,readStoredDrafts as it,resolveBugCheckout as st,writeJsonFile as ot}from"./store.js";const nt=12,R=10;export async function prepareBugFiling(t){const i=et(t.supplied??{}),e=await st(t),{draft:r,file:s}=lt(e,t.id);if(r.filed!==void 0)return{draft:r,issue:{number:r.filed.number,url:r.filed.url},status:"already-filed"};L(r.test.file),requireNoSecretInCommand(r),t.supplied!==void 0&&at(t.supplied,e,t.env);const o=I(e.headCommit),a=await N(e.repoRoot,e.headCommit);for(const c of[r.test.file,r.fault.path])if(!a.some(m=>m.path===c))throw new Error(`${c} is not in commit ${o}. Commit it and push the commit, then run this again.`);const l=(await B(e.repoRoot)).filter(c=>!ut(e,c));if(l.length>0)throw new Error(`Tracked files have uncommitted changes: ${ft(l)}. The issue says that the test fails at ${o}, so commit or discard the changes first.`);if((await x(e.repoRoot,e.headCommit)).length===0)throw new Error(`No remote branch contains commit ${o}. Push it, then run this again. The issue points readers to the test at that commit.`);const{config:u}=await D(e.productDir),d=dt(q(C(u),a,r.fault.path),r.fault.path),n=await j(e.repoRoot,e.headCommit,r.fault.path,r.fault.lines),f=await _({env:t.env,productDir:e.productDir,repository:O(u).repository??void 0});return F(await S(f),r.test),{checkout:e,draft:r,draftFile:s,filing:{area:d,foundAt:e.headCommit,introducedBy:n.status==="found"?n.commit:null},reproduceArgs:i,status:"ready",testFile:p.join(e.productDir,g(e.productPath,r.test.file)),tracker:f}}export async function bugRerun(t,i,e={},r=process.env){const{checkout:s,draft:o}=t,a=Object.entries(i).filter(([h,A])=>h.startsWith("vitest-")&&typeof A=="string"),l={...o.vitest,...Object.fromEntries(a)},u=Object.keys(l).length===0?o:{...o,vitest:l};requireNoSecretInCommand(u);const d=e.environmentFile,n=k(e,d===void 0?null:E(d,s.cwd),r),f=J(l,s.productDir,r),c=g(s.productPath,o.test.file),m=P(c)?[]:await M(s.productDir,{...f,host:{...f.host,...tt(n)}}),$=m.filter(h=>h.file===c);return K(o.test.file,$,f),{files:{...r,...n.env}.TESTKIT_REQUIRE_COMPLETE_SUITE==="1"&&$.length>0?[...new Set(m.map(({file:h})=>p.join(s.productDir,h)))]:[t.testFile],ready:{...t,draft:u}}}export async function fileBug(t,i,e={}){const{checkout:r,draft:s,filing:o,reproduceArgs:a,tracker:l}=t,u=requireRecordedFailure(s,i,I(r.headCommit),a);F(await S(l),u);const d=renderReadableBugIssue(s,o,a);await l.ensureLabels(H(s.severity,o.area));const n=await l.createIssue(d),f={...s,filed:{filedAt:(e.at??new Date).toISOString(),foundAt:o.foundAt,number:n.number,url:n.url}};return ot(t.draftFile,f),{area:o.area,covers:"test",draft:f,introducedBy:o.introducedBy,issue:{number:n.number,url:n.url},path:v(r,t.draftFile)}}export function renderReadableBugIssue(t,i,e=[]){const r=Q(t,i,e),s=W(r.body),o=T(G(t,i));if(s.status==="ok"&&T(s.facts)===o)return r;throw new Error(`Testkit cannot read back the facts of the issue it would file, so it files nothing. This happens when test.title, test.file, fault.path, or the area's name holds ${V} or ${U}. Rename the test, the file, or the area, then record the bug again.`)}export function requireNoSecretInCommand(t){const i=y(t);if(w(i)!==i)throw new Error("The command that reproduces this bug holds text that Testkit redacts as a secret, and an issue carries that command. The text is in the path of the product directory, in the test's path from that directory, or in a --vitest-config or --vitest-project selector. Rename what holds it, commit and push the change, then record the bug again.")}function at(t,i,e){const r=t.environmentFile;if(r!==void 0&&!p.isAbsolute(r)&&b.realpathSync.native(i.cwd)!==b.realpathSync.native(i.repoRoot))throw new Error(`The issue's reproduce command runs from the repository root, but --environment-file ${r} is relative to ${i.cwd}. Run testkit bug file from ${i.repoRoot}, or pass the file's absolute path.`);k(t,r===void 0?null:E(r,i.cwd),e)}export function requireRecordedFailure(t,i,e,r=[]){const{file:s,title:o}=t.test,a=`\`${y(t,r)}\``;if(i.kind==="passed")throw new Error(`${s} passed at ${e}, so Testkit has no failure to file. ${a} must fail.`);if(i.kind==="run-failed")throw new Error(`${a} failed outside its tests at ${e}: ${i.reason.replace(/\.?$/u,".")} Testkit files a bug when the test itself fails.`);const l=i.failures.filter(n=>n.file===s);if(l.length===0)throw new Error(`${a} failed at ${e}, but ${s} is not among the files that failed.`);const u=l.flatMap(n=>n.title===null?[]:[n.title]),d=u.filter(n=>!/[\r\n]/u.test(n)&&!Z(n)&&w(n)===n);if(d.includes(o)){if(l.some(n=>n.title===o&&n.ambiguous===!0))throw new Error(`${s} has more than one test titled ${JSON.stringify(o)} at ${e}. An issue names its test by file and title, so it could not say which of them failed. Give each test in the file its own title, commit and push the change, then record the bug again.`);return{file:s,title:o}}if(u.length>0){const n=u.length-d.length;throw new Error([`${s} failed at ${e}, but not under the title ${JSON.stringify(o)}.`,...d.length===0?[]:[`Testkit reported ${d.map(f=>JSON.stringify(f)).join(", ")}. Set test.title to the title Testkit reports, then record the bug again.`],...n===0?[]:[`Testkit ${d.length===0?"":"also "}reported ${n===1?"1 title that spans lines or holds":`${String(n)} titles that span lines or hold`} text it redacts as a secret. It files no bug under such a title. Rename the test, or label the assertion, so that its title is one line and holds no such text.`]].join(" "))}throw new Error(`${s} failed at ${e} without reaching a result, so Testkit cannot tell that ${JSON.stringify(o)} failed. A file fails this way when it crashes, times out, or does not load. Make the test fail on its assertion, then run this again.`)}async function S(t){return Y(await t.listOpenIssues(z)).tests}function F(t,i){const e=X(t,i);if(e===null)return;const r=`Open issue #${String(e.issue)}`;throw new Error(e.title===null?`${r} already covers the whole file ${i.file}: ${e.url}. While it is open, Testkit files no other bug for that file.`:`${r} already covers this test: ${e.url}. Testkit keeps one open bug for each test.`)}function lt(t,i){const e=it(t.productDir),r=e.drafts.find(a=>a.draft.id===i);if(r!==void 0)return r;const s=e.unreadable.find(a=>a.file===rt(t.productDir,i));if(s!==void 0)throw new Error(`Testkit cannot read ${v(t,s.file)}: ${s.problem}. Record the bug again with \`testkit bug record\`.`);const o=e.drafts.filter(a=>a.draft.filed===void 0).map(a=>a.draft.id);throw new Error(o.length===0?`No bug draft has the id ${JSON.stringify(i)}, and this checkout has no unfiled draft. Record the bug with \`testkit bug record\` first.`:`No bug draft has the id ${JSON.stringify(i)}. Unfiled drafts: ${o.join(", ")}.`)}function dt(t,i){if(t.status==="no-map")return null;const e="Fix `areas` in testkit.config.ts, commit and push the change, then run this again.";if(t.status==="resolved"){const{area:r}=t;if(w(r.name)===r.name)return r;throw new Error(`areas.${r.id}.name holds text that Testkit redacts as a secret, and an issue carries its area's name. ${e}`)}throw new Error(t.status==="unclaimed"?`No area claims ${i}. Every bug needs its fault in one area. ${e}`:`The areas ${t.areas.join(" and ")} both claim ${i}. A file belongs to one area. ${e}`)}function ut(t,i){return g(t.productPath,i).startsWith(".testkit/")}function ft(t){const i=t.length-R;return`${t.slice(0,R).join(", ")}${i>0?`, and ${String(i)} more`:""}`}function I(t){return t.slice(0,nt)}
|
|
1
|
+
import h from"node:fs";import{setTimeout as T}from"node:timers/promises";import m from"node:path";import{readAreaMap as R,readBugPolicy as x}from"../../config/loader/bugs.js";import{loadTestkitConfig as A}from"../../config/loader/index.js";import{findIntroducingCommit as C,findRemoteBranchHolding as _,listUncommittedTrackedFiles as D}from"../../repo/history.js";import{listCommitFiles as O}from"../../repo/tree-listing.js";import{resolveTracker as B}from"../tracker/index.js";import{isTypedTestFile as j}from"../../domain/identifiers.js";import{rejectRetiredTestFile as L}from"../../domain/retired-test-types.js";import{listPlainVitestFiles as M,resolveVitestSelection as N}from"../../engines/catalog.js";import{resolveFaultArea as P}from"./areas.js";import{requireOneVitestProject as q,toProductPath as p}from"./draft.js";import{FACTS_END_MARKER as J,FACTS_START_MARKER as K,readFactsBlock as U,renderFactsBlock as k}from"./facts.js";import{bugFacts as G,bugLabels as H,renderBugIssue as V,reproduceCommand as E,TESTKIT_LABEL as W}from"./issue.js";import{findKnownTest as Q,readKnownTests as Y}from"./known-failures.js";import{holdsRedaction as z,redactBugText as g}from"../../tracker/redaction.js";import{errorMessage as X}from"../../kernel/error-serialization.js";import{readEnvironmentFile as S}from"../environments/environment-file.js";import{resolveSuppliedInput as v,suppliedBindings as Z}from"../environments/supplied-input.js";import{suppliedReproduceArgs as tt}from"./supplied-reproduce.js";import{bugDraftFile as et,displayPath as F,readStoredDrafts as rt,resolveBugCheckout as it,writeJsonFile as st,writeTextFile as ot}from"./store.js";const nt=12,y=10,at=[500,1e3,2e3,4e3],lt=2e3;export async function prepareBugFiling(t){const i=tt(t.supplied??{}),e=await it(t),{draft:r,file:s}=ft(e,t.id);if(r.filed!==void 0)return{draft:r,issue:{number:r.filed.number,url:r.filed.url},status:"already-filed"};requireFileableDraft(r),t.supplied!==void 0&&requireReplayableSuppliedInput(t.supplied,e,t.env);const o=await O(e.repoRoot,e.headCommit);requireCommittedDraftFiles(r,o,e),await requireCleanPushedCheckout(e,t.env);const{config:n}=await A(e.productDir),c=await bugFiling(e,n,o,r),u=await resolveBugTracker(e,n,t.env);return requireNoOpenIssue(await readOpenIssues(u),r.test),readyBugFiling({checkout:e,draft:r,draftFile:s,filing:c,reproduceArgs:i,tracker:u})}export function requireFileableDraft(t){L(t.test.file),requireNoSecretInCommand(t)}export function requireCommittedDraftFiles(t,i,e){for(const r of[t.test.file,t.fault.path])if(!i.some(s=>s.path===r))throw new Error(`${r} is not in commit ${shortCommit(e.headCommit)}. Commit it and push the commit, then run this again.`)}export async function requireCleanPushedCheckout(t,i){const e=shortCommit(t.headCommit),r=(await D(t.repoRoot)).filter(o=>!mt(t,o));if(r.length>0)throw new Error(`Tracked files have uncommitted changes: ${pt(r)}. The issue says that the test fails at ${e}, so commit or discard the changes first.`);const s=await _(t.repoRoot,t.headCommit,i);if(s.status==="missing")throw new Error(`No remote branch contains commit ${e}. Push it, then run this again. The issue points readers to the test at that commit.`);if(s.status==="unknown")throw new Error([`Testkit could not tell whether a remote branch contains commit ${e}, and the issue points readers to the test at that commit.`,...s.problems.map(o=>o.replace(/\.?$/u,".")),...s.problems.length===0?[]:["Make sure Git can read the remote from this checkout, for example by giving the checkout credentials that can read the repository, then run this again."],...s.shallow?["This checkout is a shallow clone, so its history can stop before a branch tip reaches the commit. Fetch the full history with `git fetch --unshallow`, then run this again."]:[]].join(" "))}export async function bugFiling(t,i,e,r){const s=ht(P(R(i),e,r.fault.path),r.fault.path),o=await C(t.repoRoot,t.headCommit,r.fault.path,r.fault.lines);return{area:s,foundAt:t.headCommit,introducedBy:o.status==="found"?o.commit:null}}export function resolveBugTracker(t,i,e){return B({env:e,productDir:t.productDir,repository:x(i).repository??void 0})}export function readyBugFiling(t){return{...t,status:"ready",testFile:m.join(t.checkout.productDir,p(t.checkout.productPath,t.draft.test.file))}}export async function bugRerun(t,i,e={},r=process.env){const{checkout:s,draft:o}=t,n=Object.entries(i).filter(([f,I])=>f.startsWith("vitest-")&&typeof I=="string"),c={...o.vitest,...Object.fromEntries(n)},u=Object.keys(c).length===0?o:{...o,vitest:c};requireNoSecretInCommand(u);const l=e.environmentFile,a=v(e,l===void 0?null:S(l,s.cwd),r),d=N(c,s.productDir,r),w=p(s.productPath,o.test.file),b=j(w)?[]:await M(s.productDir,{...d,host:{...d.host,...Z(a)}}),$=b.filter(f=>f.file===w);return q(o.test.file,$,d),{files:{...r,...a.env}.TESTKIT_REQUIRE_COMPLETE_SUITE==="1"&&$.length>0?[...new Set(b.map(({file:f})=>m.join(s.productDir,f)))]:[t.testFile],ready:{...t,draft:u}}}export async function fileBug(t,i,e={}){const r=requireRecordedFailure(t.draft,i,shortCommit(t.checkout.headCommit),t.reproduceArgs);requireNoOpenIssue(await readOpenIssues(t.tracker),r);const s=renderReadableBugIssue(t.draft,t.filing,t.reproduceArgs);await t.tracker.ensureLabels(H(t.draft.severity,t.filing.area));const o=h.readFileSync(t.draftFile,"utf8"),n=await t.tracker.createIssue(s),c=recordFiledBug(t,n,e.at??new Date),u=await ut(t.tracker,n.number).catch(a=>{throw new Error(`Testkit filed #${String(n.number)}: ${n.url}, and its draft records that, but Testkit could not read the open issues again to check whether another checkout filed this test at the same time: ${X(a).replace(/\.?$/u,".")} Look for a second open issue for this test.`)}),l=dt(u.issues,r,n.number);if(l!==null){const a=await t.tracker.closeDuplicateIssue(n,{number:l.issue,url:l.url});throw ot(t.draftFile,o),ct(l,n,a)}return{...c,listed:u.listed}}export function recordFiledBug({checkout:t,draft:i,draftFile:e,filing:r},s,o){const n={...i,filed:{filedAt:o.toISOString(),foundAt:r.foundAt,number:s.number,url:s.url}};return st(e,n),{area:r.area,covers:"test",draft:n,introducedBy:r.introducedBy,issue:{number:s.number,url:s.url},path:F(t,e)}}export function renderReadableBugIssue(t,i,e=[]){const r=V(t,i,e),s=U(r.body),o=k(G(t,i));if(s.status==="ok"&&k(s.facts)===o)return r;throw new Error(`Testkit cannot read back the facts of the issue it would file, so it files nothing. This happens when test.title, test.file, fault.path, or the area's name holds ${K} or ${J}. Rename the test, the file, or the area, then record the bug again.`)}export function requireNoSecretInCommand(t){const i=E(t);if(g(i)!==i)throw new Error("The command that reproduces this bug holds text that Testkit redacts as a secret, and an issue carries that command. The text is in the path of the product directory, in the test's path from that directory, or in a --vitest-config or --vitest-project selector. Rename what holds it, commit and push the change, then record the bug again.")}export function requireReplayableSuppliedInput(t,i,e){const r=t.environmentFile;if(r!==void 0&&!m.isAbsolute(r)&&h.realpathSync.native(i.cwd)!==h.realpathSync.native(i.repoRoot))throw new Error(`The issue's reproduce command runs from the repository root, but --environment-file ${r} is relative to ${i.cwd}. Run testkit bug file from ${i.repoRoot}, or pass the file's absolute path.`);v(t,r===void 0?null:S(r,i.cwd),e)}export function requireRecordedFailure(t,i,e,r=[]){const{file:s,title:o}=t.test,n=`\`${E(t,r)}\``;if(i.kind==="passed")throw new Error(`${s} passed at ${e}, so Testkit has no failure to file. ${n} must fail.`);if(i.kind==="run-failed")throw new Error(`${n} failed outside its tests at ${e}: ${i.reason.replace(/\.?$/u,".")} Testkit files a bug when the test itself fails.`);const c=i.failures.filter(a=>a.file===s);if(c.length===0)throw new Error(`${n} failed at ${e}, but ${s} is not among the files that failed.`);const u=c.flatMap(a=>a.title===null?[]:[a.title]),l=u.filter(a=>!/[\r\n]/u.test(a)&&!z(a)&&g(a)===a);if(l.includes(o)){if(c.some(a=>a.title===o&&a.ambiguous===!0))throw new Error(`${s} has more than one test titled ${JSON.stringify(o)} at ${e}. An issue names its test by file and title, so it could not say which of them failed. Give each test in the file its own title, commit and push the change, then record the bug again.`);return{file:s,title:o}}if(u.length>0){const a=u.length-l.length;throw new Error([`${s} failed at ${e}, but not under the title ${JSON.stringify(o)}.`,...l.length===0?[]:[`Testkit reported ${l.map(d=>JSON.stringify(d)).join(", ")}. Set test.title to the title Testkit reports, then record the bug again.`],...a===0?[]:[`Testkit ${l.length===0?"":"also "}reported ${a===1?"1 title that spans lines or holds":`${String(a)} titles that span lines or hold`} text it redacts as a secret. It files no bug under such a title. Rename the test, or label the assertion, so that its title is one line and holds no such text.`]].join(" "))}throw new Error(`${s} failed at ${e} without reaching a result, so Testkit cannot tell that ${JSON.stringify(o)} failed. A file fails this way when it crashes, times out, or does not load. Make the test fail on its assertion, then run this again.`)}export async function readOpenIssues(t){return Y(await t.listOpenIssues(W)).tests}async function ut(t,i){let e=await readOpenIssues(t);for(const r of at){if(e.some(s=>s.issue===i))break;await T(r),e=await readOpenIssues(t)}return e.some(r=>r.issue===i)?t.target.provider!=="github"?{issues:e,listed:!0}:(await T(lt),{issues:await readOpenIssues(t),listed:!0}):{issues:e,listed:!1}}function ct(t,i,e){const r=`#${String(i.number)}`;return new Error(`Open issue #${String(t.issue)} already covers this test: ${t.url}. Another checkout filed it while this one filed ${r}, ${e?`so Testkit closed ${r}`:`and Testkit could not close ${r}; close it by hand: ${i.url}`}.`)}function dt(t,i,e){return t.filter(s=>s.issue<e&&s.file===i.file&&(s.title===null||s.title===i.title)).sort((s,o)=>s.issue-o.issue).at(0)??null}export function requireNoOpenIssue(t,i){const e=Q(t,i);if(e===null)return;const r=`Open issue #${String(e.issue)}`;throw new Error(e.title===null?`${r} already covers the whole file ${i.file}: ${e.url}. While it is open, Testkit files no other bug for that file.`:`${r} already covers this test: ${e.url}. Testkit keeps one open bug for each test.`)}function ft(t,i){const e=rt(t.productDir),r=e.drafts.find(n=>n.draft.id===i);if(r!==void 0)return r;const s=e.unreadable.find(n=>n.file===et(t.productDir,i));if(s!==void 0)throw new Error(`Testkit cannot read ${F(t,s.file)}: ${s.problem}. Record the bug again with \`testkit bug record\`.`);const o=e.drafts.filter(n=>n.draft.filed===void 0).map(n=>n.draft.id);throw new Error(o.length===0?`No bug draft has the id ${JSON.stringify(i)}, and this checkout has no unfiled draft. Record the bug with \`testkit bug record\` first.`:`No bug draft has the id ${JSON.stringify(i)}. Unfiled drafts: ${o.join(", ")}.`)}function ht(t,i){if(t.status==="no-map")return null;const e="Fix `areas` in testkit.config.ts, commit and push the change, then run this again.";if(t.status==="resolved"){const{area:r}=t;if(g(r.name)===r.name)return r;throw new Error(`areas.${r.id}.name holds text that Testkit redacts as a secret, and an issue carries its area's name. ${e}`)}throw new Error(t.status==="unclaimed"?`No area claims ${i}. Every bug needs its fault in one area. ${e}`:`The areas ${t.areas.join(" and ")} both claim ${i}. A file belongs to one area. ${e}`)}function mt(t,i){return p(t.productPath,i).startsWith(".testkit/")}function pt(t){const i=t.length-y;return`${t.slice(0,y).join(", ")}${i>0?`, and ${String(i)} more`:""}`}export function shortCommit(t){return t.slice(0,nt)}
|
package/dist/app/bugs/guide.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{BUG_CAUSES as
|
|
1
|
+
import{BUG_CAUSES as s,BUG_CONFIDENCES as h,BUG_ESCAPES as a,BUG_IMPACTS as i,BUG_SEVERITIES as o}from"./categories.js";import{BUG_TITLE_MAX_LENGTH as r}from"./draft.js";import{SEVERITY_LABELS as u}from"./issue.js";export const BUG_RECORD_EXAMPLE={title:"A refund pays out twice when two requests overlap",severity:"high",expected:"A refund pays out once.",authority:'`docs/refunds.md`, "Refunds are idempotent on the order id".',observed:"Two overlapping refund requests for one order both pay out.",rootCause:"`refund()` reads the order's refund state and writes the payout without a lock.",test:{file:"src/checkout/refund.test.ts",title:"a refund pays out once"},fault:{path:"src/checkout/refunds.ts",lines:[41,58]},cause:"race-condition",impact:"financial-error",confidence:{cause:"high",impact:"high"},escape:"no-test"};const c=e=>e.map(t=>`\`${t}\``).join(", "),d=e=>`One of ${c(h)}. How sure you are of the ${e}.`,n=e=>({meaning:`One id from the ${e.field} list below.`,name:e.field});export const BUG_GUIDE={fields:[{name:"title",meaning:`One line of at most ${String(r)} characters that names the broken behaviour. It becomes the issue title.`},{name:"severity",meaning:`One of ${c(o)}. The severities are defined below.`},{name:"expected",meaning:"What the product should do."},{name:"authority",meaning:"The source that says so: documentation, a specification, an API contract, or the code's own stated intent. Name it and quote the part that applies."},{name:"observed",meaning:"What the product does instead."},{name:"rootCause",meaning:"Why the code behaves this way."},{name:"test.file",meaning:"The Testkit test file that fails because of the bug."},{name:"test.title",meaning:`The failure's title. It is one line of at most ${String(r)} characters.`},{name:"fault.path",meaning:"The source file that holds the mistake."},{name:"fault.lines",meaning:"The first and last faulty line as `[start, end]`, counted from 1. One faulty line is `[41, 41]`."},n(s),n(i),{name:"confidence.cause",meaning:d("cause")},{name:"confidence.impact",meaning:d("impact")},n(a)],rules:["Write `test.file` and `fault.path` relative to the product directory, the directory that holds `testkit.config.ts`.","`test.title` must be the failure's title exactly as Testkit reports it. In a Vitest or Playwright file, that is the names of the `describe` blocks around the test and the test's own name, joined by single spaces. When `testkit bug file` rejects the title, its message lists the titles a bug record can use.","Keep credentials, tokens, and passwords out of the record. Testkit redacts the secrets it recognises before it stores the record and again before it files the issue. It files no bug under a failure's title that holds text it redacts, so rename such a test or label such an assertion first.","Testkit doesn't rewrite `test.file`, `fault.path`, or an area's name, so they must hold no text that Testkit redacts as a secret. `testkit bug record` rejects a record whose `test.file` or `fault.path` holds such text, and `testkit bug file` stops when the fault's area has such a name. Rename the file, or the area in `testkit.config.ts`, first."],example:BUG_RECORD_EXAMPLE,severities:o.map(e=>({definition:u[e].description,id:e})),categoryLists:[s,i,a]};export function renderBugGuide(e){return["## Bug record","Write one JSON object for each confirmed bug. Every field is required.",e.fields.map(t=>`- \`${t.name}\`: ${t.meaning}`).join(`
|
|
2
2
|
`),...e.rules,"Example:",["```json",JSON.stringify(e.example,null,2),"```"].join(`
|
|
3
3
|
`),"## Severities",e.severities.map(t=>`- \`${t.id}\`: ${t.definition}`).join(`
|
|
4
4
|
`),...e.categoryLists.flatMap(l)].join(`
|
package/dist/app/bugs/record.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import c from"node:fs";import h from"node:path";import{text as b}from"node:stream/consumers";import{loadTestkitConfig as D}from"../../config/loader/index.js";import{discoverOwnedTests as k}from"../../config/loader/owned-discovery.js";import{isTypedTestFile as
|
|
1
|
+
import c from"node:fs";import h from"node:path";import{text as b}from"node:stream/consumers";import{loadTestkitConfig as D}from"../../config/loader/index.js";import{discoverOwnedTests as k}from"../../config/loader/owned-discovery.js";import{isTypedTestFile as S}from"../../domain/identifiers.js";import{resolveVitestSelection as T}from"../../engines/catalog.js";import{posixPath as E}from"../../process/files/paths.js";import{findActiveCampaign as P,readCampaigns as F}from"../campaigns/store.js";import{BugRecordError as $,createBugDraft as x,namesSameTest as C,readBugReport as R,requireOneVitestProject as j,toProductPath as B}from"./draft.js";import{requireNoSecretInCommand as O}from"./file.js";import{bugDraftFile as N,displayPath as y,displayUnreadableDrafts as I,readStoredDrafts as J,resolveBugCheckout as q,writeJsonFile as A}from"./store.js";export async function recordBug(o){const t=await q(o),{redactedFields:i,report:e}=W(await U(t.cwd,o.file),t.productPath);for(const[r,g]of[["test.file",e.test.file],["fault.path",e.fault.path]])if(c.statSync(h.join(t.repoRoot,g),{throwIfNoEntry:!1})?.isFile()!==!0)throw new Error(`${r} resolves to ${g} from the repository root, and no file is there. Write the path relative to the product directory.`);const{config:n}=await D(t.productDir),f=T(o.vitest??{},t.productDir),l=B(t.productPath,e.test.file),d=await k(t.productDir,n,{plain:!S(l),selection:f}),m=d.problems.at(0)??d.migration;if(m!==null)throw new Error(m);if(!d.project.files.some(r=>r.filePath===l))throw new Error(`test.file resolves to ${e.test.file}, and Testkit does not discover a test there. Run \`testkit discover\` to list the tests it runs.`);j(e.test.file,d.plainFiles.filter(({file:r})=>r===l),f);const a=x(e,{campaign:P(F(t.productDir))?.id??null,productPath:t.productPath,recordedAt:o.at??new Date,recordedCommit:t.headCommit,vitest:V(o.vitest??{},t.productDir,f)});O(a);const u=J(t.productDir),p=u.drafts.filter(r=>C(r.draft,a)),w=p.find(r=>r.draft.filed!==void 0)?.draft.filed;if(w!==void 0)throw new Error(`This test already has a filed bug: ${w.url}. Testkit keeps one bug for each test.`);const s=N(t.productDir,a.id),v=u.unreadable.find(r=>r.file===s);if(v!==void 0)throw new Error(`${y(t,s)} exists and Testkit cannot read it: ${v.problem}. Fix or delete the file, then run this again.`);A(s,a);for(const r of p)r.file!==s&&c.rmSync(r.file,{force:!0});return{draft:a,path:y(t,s),redactedFields:i,replaced:p.length>0,unreadableDrafts:I(t,u.unreadable)}}function V({"vitest-project":o,"vitest-runner":t},i,{config:e}){const n={...t===void 0?{}:{"vitest-runner":t},...e===null?{}:{"vitest-config":E(h.relative(c.realpathSync(i),e))},...o===void 0?{}:{"vitest-project":o}};return Object.keys(n).length===0?void 0:n}async function U(o,t){if(t==="-")return{source:"standard input",text:await b(process.stdin)};try{return{source:t,text:c.readFileSync(h.resolve(o,t),"utf8")}}catch(i){throw new Error(`Testkit cannot read the bug record file ${t}.`,{cause:i})}}function W(o,t){let i;try{i=JSON.parse(o.text)}catch(e){throw new Error(`The bug record in ${o.source} is not valid JSON.`,{cause:e})}try{return R(i,t)}catch(e){throw e instanceof $?new Error(`${e.message}
|
|
2
2
|
Run \`testkit bug guide\` to see the record format.`,{cause:e}):e}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{runModel as p}from"../../evidence/index.js";import{isUnknownRecord as
|
|
1
|
+
import{runModel as p}from"../../evidence/index.js";import{isUnknownRecord as d}from"../../kernel/unknown-values.js";const h=new Set(["engine-failure","playwright-not-run","playwright-other-file-spec","vitest-error"]),F=1;export function readRunArtifact(e,n){return readRun(p(e),n)}export function readRun(e,n){const t=e.groups.flatMap(c),s=k(e,e.groups,t,n);return s!==null?s:e.run.status!=="passed"?l(t.length===0?"no runnable test files executed":"the run did not pass"):{kind:"passed",passedFiles:t.length}}export function readRunArtifactFile(e,n,t){return readRunFile(p(e),n,t)}export function readRunFile(e,n,t){const s=o=>g(o,n)===t,i=e.groups.filter(o=>c(o).some(s)),a=i.flatMap(c).filter(s);return k(e,i,a,n)??(a.length===0?l(`${t} did not run`):{kind:"passed",passedFiles:a.length})}function k(e,n,t,s){if(e.finalizationFailure!==null)return l(e.finalizationFailure.message);if(e.run.status==="interrupted")return l("the run was interrupted");const{cleanup:i,publication:a}=e.archive;if(a?.status==="failed"&&a.required)return l("Testkit could not publish the run's results");if(i?.status==="failed")return l("Testkit could not clean up after the run");const o=n.flatMap(r=>r.errors).at(0);if(o!==void 0)return l(o);if(t.some(r=>r.infraFailed===!0||r.blockedByRuntime===!0))return l("test infrastructure failure");if(t.some(r=>r.status==="not_run"))return l("one or more required test files did not run");if(t.some(r=>r.status==="skipped"))return l("one or more selected test files were skipped");const f=t.find(r=>r.status!=="passed"&&r.status!=="failed");if(f!==void 0)return l(`${f.path} ended as ${f.status}`);const u=t.filter(r=>r.status==="failed");return u.length>0?{failures:u.flatMap(r=>y(r,s)),kind:"tests-failed"}:null}function c(e){return e.suites.flatMap(n=>n.files)}function g(e,n){return n===""?e.path:`${n}/${e.path}`}function y(e,n){const t=g(e,n),s=e.failureDetails??[],i=s.filter(u=>!h.has(u.kind??"")),a=new Set(i.filter(x).map(m)),o=[...new Set(i.map(m))].map(u=>({...a.has(u)?{ambiguous:!0}:{},file:t,title:u}));return s.filter(u=>u.kind!==void 0&&h.has(u.kind)).map(u=>u.kind==="engine-failure"?E(u,i):"unexplained").includes("unexplained")||o.length===0?[...o,{file:t,title:null}]:o}function x(e){return e.kind==="vitest-test"&&(e.sharedTitle===!0||typeof e.count=="number"&&e.count>1)}function m(e){const n=e.key?.trim()??"";return n===""?e.title:e.kind==="playwright-spec"?n:e.title}function E(e,n){const t=d(e.actual)&&d(e.actual.details)?e.actual.details:null;if(t===null)return"unexplained";if(e.key==="PLAYWRIGHT_SPECS_FAILED"){const s=b(n.filter(f=>f.kind==="playwright-spec")),i=d(t.stats)?t.stats:{},a=t.failOnFlakyTests===!0?i.flaky:0;return typeof i.unexpected=="number"&&typeof a=="number"&&i.unexpected+a===s&&t.reportErrors===0&&t.attachmentFailures===0&&t.evidenceErrors===0&&t.runStatus==="failed"&&t.exitCode===F&&t.reportersFinished===!0&&typeof t.failOnFlakyTests=="boolean"?"restated":"unexplained"}return"unexplained"}function b(e){return e.reduce((n,t)=>n+(typeof t.count=="number"?t.count:1),0)}function l(e){return{kind:"run-failed",reason:e}}
|
package/dist/app/bugs/store.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import
|
|
1
|
+
import c from"node:fs";import s from"node:path";import{errorMessage as m}from"../../kernel/error-serialization.js";import{writeTextFileAtomicWithRetry as p}from"../../process/files/json-file.js";import{resolveProductDir as f}from"../../process/files/product-dir.js";import{collectGitRepoState as d}from"../../repo/state.js";import{BugRecordError as h,readBugDraft as g}from"./draft.js";export async function resolveBugCheckout(r){const t=r.cwd??process.cwd(),e=f(t,r.dir),o=await d(e,{env:r.env??process.env});if(o===null||o.repoRoot===null)throw new Error(`${e} is not in a git repository. Bug records and campaigns record the commit they start from.`);if(o.commitSha===null)throw new Error(`The repository at ${o.repoRoot} has no commit. Make the first commit, then run this again.`);const l=s.relative(o.repoRoot,c.realpathSync.native(e)).split(s.sep).join("/");return{cwd:t,headCommit:o.commitSha,productDir:e,productPath:l,pullRequest:o.pullRequest,pullRequestHeadCommit:o.pullRequestHeadSha,repoRoot:o.repoRoot}}export function displayPath(r,t){return s.relative(r.cwd,t)||"."}export function displayUnreadableDrafts(r,t){return t.map(e=>({file:displayPath(r,e.file),problem:e.problem}))}export function bugDraftFile(r,t){return s.join(r,".testkit","bugs",`${t}.json`)}export function readStoredDrafts(r){const t=s.join(r,".testkit","bugs"),e=[],o=[];if(!c.existsSync(t))return{drafts:e,unreadable:o};const l=c.readdirSync(t,{withFileTypes:!0}).filter(n=>n.isFile()&&n.name.endsWith(".json")&&!n.name.startsWith(".")).map(n=>n.name);for(const n of l.sort()){const a=s.join(t,n);let u;try{u=c.readFileSync(a,"utf8")}catch(i){o.push({file:a,problem:m(i)});continue}try{const i=JSON.parse(u);e.push({draft:g(i),file:a})}catch(i){if(i instanceof h)o.push({file:a,problem:S(i.problems)});else if(i instanceof SyntaxError)o.push({file:a,problem:"it is not valid JSON"});else throw i}}return{drafts:e,unreadable:o}}function S(r){const[t="it is not a bug draft",...e]=r;return e.length===0?t:`${t}, and ${String(e.length)} more problem${e.length===1?"":"s"}`}export function writeTextFile(r,t){c.mkdirSync(s.dirname(r),{recursive:!0}),p(r,t)}export function writeJsonFile(r,t){writeTextFile(r,`${JSON.stringify(t,null,2)}
|
|
2
2
|
`)}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const u=7;export function renderCampaignSummary(e){const{campaign:s,filed:i,unfiled:o}=e,n=`\`${r(s.startCommit)}..${r(e.headCommit)}\``;return[`## Testkit campaign ${s.id}`,[`- Target: ${s.target}`,`- Started: ${s.startedAt}`,...s.finishedAt===void 0?[]:[`- Finished: ${s.finishedAt}`],`- Commits: ${n}`].join(`
|
|
2
2
|
`),"### Filed bugs",i.length===0?"The campaign filed no bugs.":["| Issue | Title | Severity | Test |","| --- | --- | --- | --- |",...i.map(t=>`| ${l(t)} | ${t.title.replaceAll("|","\\|")} | ${t.severity} | \`${t.test.file}\` |`)].join(`
|
|
3
|
-
`),...o.length===0?[]:["### Recorded and not filed","These bugs are not in the tracker yet. Run `testkit bug file
|
|
3
|
+
`),...o.length===0?[]:["### Recorded and not filed","These bugs are not in the tracker yet. Run `testkit bug file --batch` to file them.",o.map(t=>`- \`${t.id}\`: ${t.title}, ${t.severity}, \`${t.test.file}\``).join(`
|
|
4
4
|
`)],...e.unreadableDrafts.length===0?[]:["Testkit could not read these draft files, so this summary leaves them out:",e.unreadableDrafts.map(t=>`- \`${t}\``).join(`
|
|
5
5
|
`)],"### Testkit tests added or changed",e.testFiles===null?`Testkit could not read the commit range ${n}, because the start commit is missing or is not an ancestor of the current commit.`:e.testFiles.length===0?`No Testkit test file changed in ${n}.`:e.testFiles.map(t=>`- \`${t}\``).join(`
|
|
6
6
|
`),...i.length===0?[]:["### Merge gate",a(e.knownFailures)]].join(`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{formatRunSummary as m}from"../../process/runs/manifest.js";import d from"path";import{pruneEmptyDir as p}from"../../process/files/directory-status.js";import f from"fs";import{loadManagedConfigs as v}from"../../config/loader/managed-configs.js";import{collectBundleCleanupTargets as g}from"../../engines/bundle-cache.js";import{createEnvironmentMaintenance as y}from"../../environment/maintenance.js";import{runtimePlanning as h}from"../../environment/runtime-planning.js";import{resolveProductDir as C}from"../../process/files/product-dir.js";export async function cleanupEnvironment(e){const r=C(process.cwd(),e.dir),{allConfigs:n}=await v({dir:r,service:e.service}),t=await cleanup(r,{allConfigs:n,serviceName:e.service||null,dryRun:e.dryRun,cache:e.cache,resources:e.resources}),a={cleaned:
|
|
1
|
+
import{formatRunSummary as m}from"../../process/runs/manifest.js";import d from"path";import{pruneEmptyDir as p}from"../../process/files/directory-status.js";import f from"fs";import{loadManagedConfigs as v}from"../../config/loader/managed-configs.js";import{collectBundleCleanupTargets as g}from"../../engines/bundle-cache.js";import{createEnvironmentMaintenance as y}from"../../environment/maintenance.js";import{runtimePlanning as h}from"../../environment/runtime-planning.js";import{resolveProductDir as C}from"../../process/files/product-dir.js";import{createRunEvidencePublisher as R}from"../../evidence/run-evidence.js";export async function cleanupEnvironment(e){const r=C(process.cwd(),e.dir),{allConfigs:n}=await v({dir:r,service:e.service}),t=await cleanup(r,{allConfigs:n,serviceName:e.service||null,dryRun:e.dryRun,cache:e.cache,resources:e.resources}),a={cleaned:c(t.cleaned),stoppedActive:c(t.stoppedActive),skippedActive:c(t.skippedActive)};return{productDir:r,result:t,runSummaries:a}}export async function cleanup(e,r={}){const n=r.allConfigs??[],t=r.serviceName||null,a=b(r.cache),{environment:s,otherCaches:u}=await y(e).maintain({kind:"cleanup",evidence:R(),desiredGraphs:h.collectDesiredRuntimeGraphs(n),serviceName:t,dryRun:!!r.dryRun,resources:!!r.resources},()=>{const i=a.includes("bundles")?g(e,{allConfigs:n,serviceName:t}):[],o=[];return{result:{targets:i,cleaned:o},prune(){for(const l of i)f.rmSync(l.path,{force:!0}),o.push(l);p(d.join(e,".testkit","_bundles"))}}});return{...s,targets:{runtime:s.targets,bundles:u.targets},bundleCleaned:u.cleaned}}function b(e){const n=(Array.isArray(e)?e:e?[e]:[]).filter(Boolean).map(String);return n.includes("all")?["runtime","bundles"]:n}function c(e){return e.map(r=>({runId:r.runId,summary:m(r)}))}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import
|
|
1
|
+
import g from"node:path";import{resolveProductDir as p}from"../../config/loader/index.js";import{collectRequiredConfigs as u,loadManagedConfigs as d,resolveTargetConfig as v,topologicallySortConfigs as C}from"../../config/loader/managed-configs.js";import{runtimePlanning as w}from"../../environment/runtime-planning.js";import{withEnvFileValues as E}from"../../config/loader/env.js";import{portsOf as O,releasePorts as T,reservePorts as S}from"../../environment/ports.js";import{DEFAULT_SCHEMA_OPERATION_TIMEOUT_SECONDS as h,parseOperationTimeoutSeconds as D}from"../../database/operation-deadline.js";export async function prepareSchemaOperation(o,c){const l=D(o["timeout-seconds"]??h)*1e3,t=p(process.cwd(),o.dir),{configs:n}=await d({dir:t}),e=v(n,o.service||null),i=u(n,e.name).map(E);return{productDir:t,serviceName:e.name,timeoutMs:l,async resolveConfigs(f){const s=await S(i.filter(r=>r.testkit.local).map(r=>r.name));await T(s.values());const a=w.resolveRuntimeInstanceConfigs(i,"runtime-1",g.join(f,"runtime-1"),{graphDirName:`${c}-${e.name}`,ports:O(s)}),m=a.find(r=>r.name===e.name);if(!m)throw new Error(`Resolved runtime config missing target service "${e.name}"`);return{configs:C(a),target:m}}}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import
|
|
1
|
+
import c from"path";import{refreshDatabaseSchema as l}from"../../database/index.js";import{createRunLogRegistry as h}from"../../evidence/logs.js";import"../../steps/index.js";import{createSetupOperationRegistry as p}from"../../evidence/setup-progress.js";import{prepareSchemaOperation as f}from"./prepare.js";export async function executeDatabaseSchemaRefresh(r,o){const{productDir:a,serviceName:i,resolveConfigs:n,timeoutMs:m}=await f(r,"schema-refresh"),s=h(a),u=p({logRegistry:s});try{const e=await l(a,n,{callCapsMs:r.callCapsMs,cleanupTimeoutMs:r.cleanupTimeoutMs,progress:{reporter:o,setupRegistry:u},signal:r.signal,timeoutMs:m}),t=e.cachePath;return{ok:!0,productDir:a,service:i,outputPath:t,outputLabel:c.relative(a,t)||c.basename(t),cacheKey:e.cacheKey,cacheKind:e.cacheKind,envName:e.envName,sourceUrl:e.refreshInfo.metadata.sourceUrl||null,reusedExistingRefresh:e.refreshInfo.reusedExistingRefresh}}finally{await s.closeAll()}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{verifyDatabaseSchema as
|
|
1
|
+
import{verifyDatabaseSchema as m}from"../../database/index.js";import{createRunLogRegistry as p}from"../../evidence/logs.js";import"../../steps/index.js";import{createSetupOperationRegistry as l}from"../../evidence/setup-progress.js";import{prepareSchemaOperation as u}from"./prepare.js";export async function executeDatabaseSchemaVerify(e,t){const{productDir:r,serviceName:i,resolveConfigs:s,timeoutMs:c}=await u(e,"schema-verify"),a=p(r),o=l({logRegistry:a});try{return await m(r,s,{progress:{reporter:t,setupRegistry:o},callCapsMs:e.callCapsMs,cleanupTimeoutMs:e.cleanupTimeoutMs,signal:e.signal,skipSchemaSourceVerify:e["skip-schema-source-verify"],timeoutMs:c}),{ok:!0,productDir:r,service:i}}finally{await a.closeAll()}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{filterConfigsByService as
|
|
1
|
+
import{filterConfigsByService as o}from"../../config/loader/managed-configs.js";import{loadConfigContext as a}from"../../config/loader/index.js";import{createEnvironmentMaintenance as s}from"../../environment/maintenance.js";import{hasLocalDatabaseState as d}from"../../database/lifecycle-cleanup.js";import{createRunEvidencePublisher as c}from"../../evidence/run-evidence.js";export async function destroy(r,e,t={}){await s(r).maintain({kind:"destroy",evidence:c(),configs:e,destroyOwnedResources:t.destroyOwnedResources})}export async function destroyEnvironment(r){const e=await a({dir:r.dir,allowUnmigratedServices:!0,plain:!1,retired:[]}),t=o(e.services,r.service||null),n=e.services.some(i=>i.testkit.database!==void 0||i.testkit.databaseFrom!==void 0);return await destroy(e.productDir,t,{destroyOwnedResources:!r.service&&(n||d(e.productDir))}),{ok:!0,productDir:e.productDir,results:t.map(i=>({name:i.name,destroyed:!0}))}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createEnvironment as S,environmentTaskIdentities as C,renderSharedEnvironment as k}from"../run/environment.js";import{buildTaskExecutionEnv as R,buildTemplateContext as A,finalizeSerializable as O}from"../../steps/index.js";import{runtimePlanning as j}from"../../environment/runtime-planning.js";import{sourceSchemaCredentialEnvNames as M}from"../../database/source-schema-env.js";import{prepareFileSetup as U}from"./file-setup.js";import{cloneTaskDatabases as
|
|
1
|
+
import{createEnvironment as S,environmentTaskIdentities as C,renderSharedEnvironment as k}from"../run/environment.js";import{buildTaskExecutionEnv as R,buildTemplateContext as A,finalizeSerializable as O}from"../../steps/index.js";import{runtimePlanning as j}from"../../environment/runtime-planning.js";import{sourceSchemaCredentialEnvNames as M}from"../../database/source-schema-env.js";import{hasFileSetup as T,prepareFileSetup as U}from"./file-setup.js";import{cloneTaskDatabases as N}from"./task-databases.js";import{envRedactionTargets as x,jsonRedactionTargets as D,sensitiveKeyMatcher as E,urlRedactionTargets as F}from"../../kernel/redaction-targets.js";export function planConfiguredCapacity(e){const t=j.buildRuntimeGraphs(e);return{graphs:t,instanceCount:t.reduce((s,a)=>s+a.instanceCount,0),resourceBudgets:I(t)}}function I(e){const t=new Map;for(const s of e)for(const a of s.resourceReservations){const o=t.get(a.key)??{key:a.key,kind:a.kind,scope:a.scope,resourceName:a.resourceName,capacity:a.capacity,reserved:a.reserved,available:a.available,graphs:[]};o.capacity=Math.max(o.capacity,a.capacity),o.reserved=Math.max(o.reserved,a.reserved),o.available=Math.max(0,o.capacity-o.reserved),o.graphs.push({graphKey:s.key,services:a.services,perRuntime:a.perRuntime}),t.set(a.key,o)}return[...t.values()].sort((s,a)=>s.key.localeCompare(a.key))}export function createConfigDrivenEnvironments(e,t,s,a=null){const o=E(a?.redactKeys);return{canAcquire:i=>e.canAcquire(i),getGraph:i=>e.getGraph(i),getGraphFailure:i=>e.getGraphFailure(i),hasSetup:i=>T(s,i),markGraphFailed:(i,d)=>{e.markGraphFailed(i,d)},async acquire(i,d){const p=a?.acquisition({source:"configured",graphKey:i.graphKey,leaseId:d.id,tasks:C(i)});try{const u=c=>{const r=t.get(c);if(r===void 0)throw new Error(`No services are declared for group "${c}"`);return r},h={name:i.targetName,uses:u(i.targetName)},g=await e.acquire({...i,leaseId:d.id},{onRuntimeAssigned:c=>p?.assigned(c)});p?.acquired(K(g));const v=c=>L(p,()=>e.release(g.leaseId,c));try{const c=n=>a?.addRedactionTarget(n),r=k(i,n=>renderEnvironment(g,{name:n,uses:u(n)},d,c,o));return p?.rendered(r),{environment:r,async prepare(n,l){const f={lease:d,productDir:s,record:p,redact:c};if(n.runtimeIsolation!=="resetBefore")return U(n,l,{...f,environment:r});const m=await N(g,r,n,d,l),b=renderEnvironment(m.ready,h,d,c,o);p?.rendered(b);const y=await U(n,l,{...f,environment:b}).catch(async w=>{throw await m.release(),w});return{artifacts:y?.artifacts??[],environment:b,handles:y?.handles??{},setupData:y===null?b.setupData:y.setupData,release:async()=>[...await y?.release()??[],...await m.release()]}},release:v}}catch(c){throw await v({invalidate:!1}),c}}catch(u){throw e.signal.aborted&&u===e.signal.reason?p?.cancelled(u):p?.failed(u),u}}}}function K(e){return{runtimeId:e.runtimeId,services:[...e.services.values()].map(({config:t})=>({serviceName:t.name,runtimeLabel:String(t.runtimeLabel??t.name)}))}}async function L(e,t){try{await t()}catch(s){throw e?.releaseFailed(s),s}e?.released()}const B=new Set(["BASE_URL","DATABASE_URL"]);export function renderEnvironment(e,t,s,a=()=>{},o=E()){const i={leaseDir:s.directory,leaseId:s.id},d=new Map([...e.services].map(([r,n])=>{const l=_(R(n.config,i,{},null,{includeTestkitControls:!1,inheritProcessEnv:!1}));for(const f of[...x(l,o),...[n.baseUrl,n.database?.url].flatMap(m=>m?F(m):[])])a(f);return[r,l]})),p=G(e),u=t.uses.map(r=>{const n=e.services.get(r),l=d.get(r);if(!n||!l)throw new Error(`Runtime instance is missing used service "${r}"`);const f=q(l,p),m=Object.entries(f).find(([b])=>b.toUpperCase()==="DATABASE_URL")?.[1];return{name:r,config:n.config,baseUrl:typeof n.baseUrl=="string"&&n.baseUrl.length>0?n.baseUrl:null,databaseUrl:n.database?.url??(m||null),env:Object.fromEntries(Object.entries(f).filter(([b])=>!B.has(b.toUpperCase())))}}),h={},g=new Set;for(const r of u){const n=Object.entries(r.env).filter(([l])=>!g.has(l.toUpperCase()));for(const[l,f]of n)h[l]=f;for(const[l]of n)g.add(l.toUpperCase())}const v=u.at(0)?.config,c=v?.testkit.ui?z(O(v.testkit.ui,A(v,i))):null;for(const r of D("ui",c,o))a(r);return S({baseUrl:u.find(r=>r.baseUrl!==null)?.baseUrl??null,databaseUrl:u.find(r=>r.databaseUrl!==null)?.databaseUrl??null,databases:Object.fromEntries(u.flatMap(r=>r.databaseUrl===null?[]:[[r.name,r.databaseUrl]])),endpoints:Object.fromEntries(u.flatMap(r=>r.baseUrl===null?[]:[[r.name,r.baseUrl]])),env:h,group:t.name,ui:c})}function G(e){return new Set([...e.services.values()].flatMap(({config:t})=>M(t)).map(t=>t.toUpperCase()))}function q(e,t){return Object.fromEntries(Object.entries(e).filter(([s])=>!t.has(s.toUpperCase())))}function _(e){return Object.fromEntries(Object.entries(e).filter(t=>typeof t[1]=="string"))}function z(e){const t=JSON.stringify(e);if(typeof t!="string")return null;const s=JSON.parse(t);return s!==null&&typeof s=="object"&&!Array.isArray(s)?s:null}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import f from"node:path";import{httpCaptureRedactionTargets as
|
|
1
|
+
import f from"node:path";import{httpCaptureRedactionTargets as M}from"../run/setup/runtime-node-setup-protocol.js";import{classifyRuntimeNodeSetupFailure as N,findRuntimeNodeSetupFile as m,getRuntimeNodeSetupCleanupFailures as w,MAX_RUNTIME_NODE_SETUP_CLEANUP_TIMEOUT_MS as y,runRuntimeNodeSetup as D,runtimeNodeSetupDeadlineError as F}from"../run/setup/runtime-node-setup.js";import{defaultEnvironmentValues as T}from"../run/engine-orchestration.js";import{TaskPreparationError as b}from"../run/environment.js";import{toError as g}from"../../kernel/error-serialization.js";import{isEngineTaskDeadlineExceededReason as R}from"../../engines/api/index.js";import{ambiguousHandleDescriptor as U}from"../../engines/environment/handles.js";export function hasFileSetup(e,r){return m(e,r.file)!==null}export async function prepareFileSetup(e,r,{environment:t,lease:n,productDir:a,record:s,redact:c}){let o,u,l;try{const i=m(a,e.file);if(i===null)return null;u=f.relative(a,i).split(f.sep).join("/"),o=s?.setup({module:u,task:{file:e.file,group:e.targetName,id:e.id}}),l=await D({cleanupTimeoutMs:Math.min(r.timeoutMs,y),deadlineMs:r.deadlineMs,lease:{leaseDir:n.directory,leaseId:n.id},onSidecar:d=>{for(const p of M(d))c(p)},runtimeEnv:T(t),signal:_(r.signal,u),targetConfig:{name:e.targetName,productDir:a},task:e})}catch(i){const d=N(i),p=S(i);d==="cancelled"?o?.cancelled(i):o?.failed(i);for(const E of p)o?.releaseFailed(E.error);throw new b(d,g(i),p)}o?.prepared(l.setupData);let h=null;return{artifacts:[v(u,l)],handles:C(l.sidecars),setupData:l.setupData,release:()=>h??=A(l.cleanup,o)}}function _(e,r){const t=new AbortController,n=()=>{t.abort(R(e.reason)?F(e.reason.deadlineMs,r):e.reason)};return e.aborted?n():e.addEventListener("abort",n,{once:!0}),t.signal}function v(e,r){return{name:"node-setup",kind:"testkit.node-setup",summary:`node setup ${e}`,contentType:"application/json",data:{module:e,sidecars:r.sidecars.map(t=>({...t})),setupData:r.setupData},emittedAt:new Date().toISOString()}}async function A(e,r){try{await e()}catch(t){const n=S(t),a=n.length>0?n:[{error:g(t),owner:"testkit"}];for(const s of a)r?.releaseFailed(s.error);return a}return r?.released(),[]}function C(e){const r={};for(const t of new Set(e.map(n=>n.id))){const n=e.filter(o=>o.id===t),[{requestsUrl:a,resetUrl:s,url:c}]=n;Object.defineProperty(r,t,{enumerable:!0,value:n.length>1?U(n.length):{kind:"http-capture",requestsUrl:a,resetUrl:s,url:c}})}return r}function S(e){return w(e).map(({error:r,owner:t})=>({error:r,owner:t}))}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export function declaresProvisioning(
|
|
1
|
+
import{buildTaskBatch as f}from"../run/task-batches.js";export function declaresProvisioning(i){return i.some(({testkit:r})=>r.local!==void 0||r.database!==void 0||r.databaseFrom!==void 0||r.runtime.build!==null||r.runtime.prepare.steps.length>0||r.runtime.toolchain!==null)}export function declaresValues(i){return i.some(({testkit:r})=>Object.keys(r.serviceEnv).length>0||r.envFiles.length>0||r.ui!=null)}export function routeEnvironments(i){const r=e=>{const o=i.get(e);if(o===void 0)throw new Error(`No environment source for runtime graph "${e}"`);return o};return{async acquire(e,o){const t=new Map;for(const a of"batchTasks"in e?e.batchTasks:[e]){const n=r(a.graphKey);t.set(n,[...t.get(n)??[],a])}if(t.size===1)return r(e.graphKey).acquire(e,o);const s=new Map;for(const[a,n]of t)s.set(a,await a.acquire(f(n)??n[0],o));const l=[...s.values()];return{environment:l[0].environment,prepare(a,n){const u=s.get(r(a.graphKey));if(u===void 0)throw new Error(`Task ${String(a.id)} is not in this batch`);return u.prepare(a,n)},async release(a){for(const n of l)await n.release(a)}}},canAcquire:e=>r(e.graphKey).canAcquire(e),getGraph:e=>r(e).getGraph(e),getGraphFailure:e=>r(e).getGraphFailure(e),hasSetup:e=>r(e.graphKey).hasSetup(e),markGraphFailed:(e,o)=>{r(e).markGraphFailed(e,o)}}}export function holdBeforeRelease(i,r){return{...i,async acquire(e,o){const t=await i.acquire(e,o);let s=null,l=!1;const a=()=>s??=Promise.resolve().then(()=>r(e.targetName,t.environment)).then(()=>({failed:!1}),n=>({error:n,failed:!0}));return{environment:t.environment,async prepare(n,u){const c=await t.prepare(n,u);return c===null?null:{...c,async release(){return await a(),c.release()}}},async release(n){const u=await a(),c=u.failed&&!l;l||=u.failed;try{await t.release(n)}catch(p){if(!c)throw p}if(c)throw u.error}}}}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{environmentTaskIdentities as p,PREVIEW_LEASE as h,renderSharedEnvironment as w}from"../run/environment.js";import{withEnvFileValues as g}from"../../config/loader/env.js";import{sensitiveKeyMatcher as y}from"../../kernel/redaction-targets.js";import{renderEnvironment as E}from"./config-driven.js";import{prepareFileSetup as v}from"./file-setup.js";import{resetBeforeRefusal as
|
|
1
|
+
import{environmentTaskIdentities as p,PREVIEW_LEASE as h,renderSharedEnvironment as w}from"../run/environment.js";import{withEnvFileValues as g}from"../../config/loader/env.js";import{sensitiveKeyMatcher as y}from"../../kernel/redaction-targets.js";import{renderEnvironment as E}from"./config-driven.js";import{hasFileSetup as S,prepareFileSetup as v}from"./file-setup.js";import{resetBeforeRefusal as I}from"./task-databases.js";const T=new Set(["bin","lease","leaseDir","pathList","productDir","service","stateDir"]);export function createStaticValueEnvironments(r,s,i=null){const u=new Map,m=e=>{const n=u.get(e.name);if(n!==void 0)return n;const t=g(e);return A(t),u.set(e.name,t),t},d=(e,n,t,o)=>{const a=r.get(e);if(a===void 0)throw new Error(`No services are declared for group "${e}"`);const l={graphKey:n,leaseId:t.id,runtimeId:n,targetNames:[e],services:new Map(a.map(c=>[c.name,{baseUrl:null,database:null,config:m(c)}]))};return E(l,{name:e,uses:a.map(({name:c})=>c)},t,o,y(i?.redactKeys))};return{canAcquire:()=>!0,getGraph:()=>null,getGraphFailure:()=>null,hasSetup:e=>S(s,e),markGraphFailed:()=>{},preview(e){try{return d(e,"",h,()=>{})}catch{return null}},acquire(e,n){const t=i?.acquisition({source:"static",graphKey:e.graphKey,leaseId:n.id,tasks:p(e)});return Promise.resolve().then(()=>{const o=w(e,a=>d(a,e.graphKey,n,l=>i?.addRedactionTarget(l)));return t?.acquired({runtimeId:null,services:[]}),t?.rendered(o),{environment:o,prepare:async(a,l)=>{if(a.runtimeIsolation==="resetBefore")throw I(a,!1);return v(a,l,{environment:o,lease:n,productDir:s,record:t,redact:c=>i?.addRedactionTarget(c)})},release:()=>(t?.released(),Promise.resolve())}}).catch(o=>{throw t?.failed(o),o})}}}function A(r){const s=[...Object.entries(r.testkit.serviceEnv).map(([i,u])=>[`env.${i}`,u]),["ui",r.testkit.ui]];for(const[i,u]of s)for(const m of f(u))if(!T.has(m))throw new Error(`Service "${r.name}" uses {${m}} in ${i}, which needs a running service, and "${r.name}" starts none`)}function f(r){return typeof r=="string"?[...r.matchAll(/\{([a-zA-Z]+)(?::[^}]+)?\}/gu)].map(s=>s[1]):Array.isArray(r)?r.flatMap(f):r!==null&&typeof r=="object"?Object.values(r).flatMap(f):[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createEnvironment as
|
|
1
|
+
import{createEnvironment as g,environmentTaskIdentities as O,renderSharedEnvironment as R}from"../run/environment.js";import{httpCaptureRedactionTargets as T}from"../run/setup/runtime-node-setup-protocol.js";import{checkPostgresConnection as j,parsePostgresUrl as E}from"../../database/postgres/connection-check.js";import{errorMessage as m}from"../../kernel/error-serialization.js";import{envRedactionTargets as M,jsonRedactionTargets as l,sensitiveKeyMatcher as S,urlRedactionTargets as q}from"../../kernel/redaction-targets.js";import{parseHttpUrl as D,requireHttpResponse as I}from"../../process/readiness.js";import{hasFileSetup as P,prepareFileSetup as $}from"./file-setup.js";import{resetBeforeRefusal as C}from"./task-databases.js";const h=3e4;export function createSuppliedEnvironments(t,{productDir:e,record:a=null,signal:o}){const i=S(a?.redactKeys),n=r=>suppliedEnvironment(r,Object.hasOwn(t,r)?t[r]:{});return{canAcquire:()=>!0,checksServices(r){const{databases:p,endpoints:c}=n(r);return Object.keys({...p,...c}).length>0},getGraph:()=>null,getGraphFailure:()=>null,hasSetup:r=>!Object.hasOwn(t,r.targetName)&&P(e,r),markGraphFailed:()=>{},preview(r){try{return n(r)}catch{return null}},async acquire(r,p){const c=Object.hasOwn(t,r.targetName),d=a?.acquisition({source:c?"supplied":"none",graphKey:r.graphKey,leaseId:p.id,tasks:O(r)}),w=c?t[r.targetName]:{};for(const s of F(w,i))a?.addRedactionTarget(s);let u;try{u=R(r,n),await N(r.targetName,u,o)}catch(s){throw o?.aborted&&s===o.reason?d?.cancelled(s):d?.failed(s),s}return d?.acquired({runtimeId:null,services:[]}),d?.rendered(u),{environment:u,prepare:async(s,U)=>{if(s.runtimeIsolation==="resetBefore")throw C(s,c);return c?null:$(s,U,{environment:u,lease:p,productDir:e,record:d,redact:y=>a?.addRedactionTarget(y)})},release:()=>(d?.released(),Promise.resolve())}}}}function F(t,e){return[...M(t.env??{},e),...[t.baseUrl,t.databaseUrl,...Object.values(t.databases??{}),...Object.values(t.endpoints??{})].flatMap(a=>a?q(a):[]),...l("handles",t.handles??{},e),...Object.values(t.handles??{}).filter(a=>a.kind==="http-capture").flatMap(T),...l("setupData",t.setupData??{},e),...l("ui",t.ui??{},e)]}async function N(t,e,a){const o=b(e.endpoints),i=b(e.databases);for(const[n,r]of o)await f(t,"endpoint",r,()=>D(n),a);for(const[n,r]of i)await f(t,"database",r,()=>{_(n)},a);for(const[n,r]of o)await f(t,"endpoint",r,()=>I(n,{signal:a,timeoutMs:h}),a);for(const[n,r]of i)await f(t,"database",r,()=>j(n,{signal:a,timeoutMs:h}),a)}function b(t){const e=new Map;for(const[a,o]of Object.entries(t))e.has(o)||e.set(o,a);return e}function _(t){try{E(t)}catch(e){throw new Error(`its URL ${m(e)}`,{cause:e})}}async function f(t,e,a,o,i){try{await o()}catch(n){throw i?.aborted?i.reason:new Error(`Supplied ${e} "${a}" for group "${t}" is not usable: ${m(n)}`,{cause:n})}}export function suppliedEnvironment(t,e){return{...g({baseUrl:e.baseUrl??null,databaseUrl:e.databaseUrl||null,databases:e.databases??{},endpoints:e.endpoints??{},env:e.env??{},group:t,...e.setupData===void 0?{}:{setupData:e.setupData},ui:e.ui??null}),...e.handles===void 0?{}:{handles:{...e.handles}}}}
|
package/dist/app/lint/shared.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import n from"node:fs";import a from"node:path";import{minimatch as l}from"minimatch";import{glob as
|
|
1
|
+
import n from"node:fs";import a from"node:path";import{minimatch as l}from"minimatch";import{glob as u}from"tinyglobby";import{TEST_TYPE_ORDER as x}from"../../domain/test-types.js";import{REPOSITORY_COMMAND_GUARD_ARGS as c,runManagedGitCommand as m}from"../../repo/managed-git.js";import{posixPath as i}from"../../process/files/paths.js";export const DEFAULT_EXCLUDED_DIRS=[".git",".next",".next-testkit",".testkit","coverage","dist","node_modules","playwright-report"],SOURCE_PATTERNS=["**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,json}"],SOURCE_IGNORES=DEFAULT_EXCLUDED_DIRS.map(t=>`**/${t}/**`),DEFAULT_TESTKIT_BOUNDARY_ALLOWED=["testkit.config.ts","testkit.config.mts","testkit.config.mjs","testkit.config.js","**/__testkit__/**"];export async function collectCurrentFiles(t,e){const o=e?await m(t,[...c,"ls-files","--cached","--others","--exclude-standard","-z"]):void 0,s=o?.exitCode===0?o.stdout.split("\0").filter(r=>r&&n.existsSync(a.join(t,r))):await u(SOURCE_PATTERNS,{cwd:t,ignore:SOURCE_IGNORES,dot:!0,followSymbolicLinks:!1});return[...new Set(s.map(i))].filter(r=>isSourceLikePath(r)&&!isExcludedRelativePath(r)).sort((r,p)=>r.localeCompare(p))}export async function collectTrackedFiles(t){const e=await m(t,[...c,"ls-files","-z"]);return e.exitCode!==0?null:e.stdout.split("\0").map(o=>i(o)).filter(Boolean).filter(o=>n.existsSync(a.join(t,o))).sort((o,s)=>o.localeCompare(s))}export function isSourceLikePath(t){return/\.(?:[cm]?[jt]sx?|json)$/u.test(t)}export function isTypeScriptLikePath(t){return/\.[cm]?[jt]sx?$/u.test(t)}export function isTestkitSuitePath(t){return x.some(e=>t.endsWith(`.${e}.testkit.ts`))}export function isExcludedRelativePath(t){return SOURCE_IGNORES.some(e=>l(t,e,{dot:!0}))}export function matchesAnyPathPattern(t,e){return e.some(o=>l(t,o,{dot:!0}))}export function compareFindings(t,e){return t.file.localeCompare(e.file)||t.line-e.line||t.ruleId.localeCompare(e.ruleId)||t.kind.localeCompare(e.kind)||t.target.localeCompare(e.target)}export function normalizeFinding(t){return{ruleName:t.ruleName,ruleId:t.ruleId,kind:t.kind||"violation",target:t.target||"",file:i(t.file),line:t.line||1,message:t.message,...t.snippet?{snippet:t.snippet}:{}}}export function findingKey(t){return[t.file,String(t.line),t.ruleId,t.kind,t.target].join("\0")}
|
package/dist/app/run/live-run.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{finalizeServiceResult as
|
|
1
|
+
import{finalizeServiceResult as R}from"./results.js";import{buildLiveRunArtifact as b}from"../../evidence/reporting.js";import{writeLiveRunArtifact as x}from"../../evidence/artifacts.js";export function createLiveSnapshotWriter({productDir:e,runId:l,configs:p,trackers:m,startedAt:s,execution:c,workerState:g,selection:t,archiveConfig:y=null,declaredEnvironmentKeys:d=[],metadata:w,environmentRecord:h,operationTimingRegistry:v=null,reporter:S=null}){let n=null,u=!1;const a=()=>{const f=Date.now(),L=p.map(r=>R(m.get(r.name),s,f));try{x(e,b({productDir:e,runId:l,results:L,startedAt:s,updatedAt:f,execution:c,workerCount:g.workerCount,typeValues:t.typeValues,suiteSelectors:t.suiteSelectors,fileNames:t.fileNames,groupFilter:t.groupFilter,planning:t.planning||null,archiveConfig:y,declaredEnvironmentKeys:d,metadata:w,environment:h.snapshot(),operationTimings:v?.listOperations()||[]}))}catch(r){S?.warning?.(`Live run artifact update skipped: ${k(V(r))}`)}},i=()=>{n!==null&&(clearImmediate(n),n=null,process.off("exit",o),a())},o=()=>{try{i()}catch{}};return{update(){if(u){a();return}n===null&&(n=setImmediate(i),process.prependListener("exit",o))},flush:i,finish(){u=!0,i()}}}function V(e){return(typeof e=="object"&&e!==null||typeof e=="function")&&Reflect.get(e,"message")||e}function k(e){if(typeof e=="symbol")throw new TypeError("Cannot convert a Symbol value to a string");return String(e)}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{resolveRuntimeClosure as we}from"../../config/loader/dependencies.js";import{claimNextTask as Se,collectSuites as Ce}from"./planning.js";import{buildRunPlanningMetadata as Fe,buildScheduledQueue as Ee}from"./scheduler/index.js";import{addTrackerError as be,buildServiceTrackers as Ae,finalizeServiceResult as le,recordGraphError as Te,recordTaskOutcome as Ke}from"./results.js";import{loadTimings as xe,saveTimings as Me,writeRunArtifact as Ie}from"../../evidence/index.js";import{stableJson as qe}from"../../evidence/storage.js";import{createRunEnvironmentRecord as Oe}from"../../evidence/environment-record.js";import{createRunLogRegistry as Pe}from"../../evidence/logs.js";import{createOperationTimingRegistry as ze,timeOperation as U}from"../../evidence/operation-timings.js";import{createRunReporterFromListener as Ne}from"./run-events.js";import{createCallerRedaction as $e}from"./caller-redaction.js";import{createSetupOperationRegistry as je}from"../../evidence/setup-progress.js";import{createLocalRunEnvironment as De}from"../../environment/run-environment.js";import{writeRunMaintenance as Le}from"../../evidence/maintenance.js";import{createConfigDrivenEnvironments as Ge,planConfiguredCapacity as Ve}from"../environments/config-driven.js";import{declaresProvisioning as Be,declaresValues as me,holdBeforeRelease as We,routeEnvironments as Ue}from"../environments/routing.js";import{createStaticValueEnvironments as He}from"../environments/static-values.js";import{findRuntimeNodeSetupFile as Je}from"./setup/runtime-node-setup.js";import{createSuppliedEnvironments as Qe}from"../environments/supplied.js";import{environmentTaskIdentity as _e}from"./environment.js";import{createTaskLeases as Xe}from"./task-leases.js";import{createTaskLocks as Ye}from"./scheduler/locks.js";import{readPackageMetadata as Ze,safeHostname as et,safeUsername as tt}from"../../process/host/metadata.js";import{collectGitMetadata as rt}from"../../repo/metadata.js";import{resolveExecutionConfig as nt}from"../../config/loader/decode-execution.js";import{resolveTaskPolicy as it}from"../../config/loader/task-policy.js";import"../../environment/run-environment.js";import{createEngineManager as at}from"../../engines/factory.js";import{createWorker as ot,runWorker as st}from"./worker-loop.js";import{ensureRequestedFilesMatch as ct}from"./run-guards.js";import{createLiveSnapshotWriter as ut}from"./live-run.js";import{finalizeRunArtifacts as lt}from"./run-finalization.js";import{buildRunArtifact as mt}from"../../evidence/reporting.js";import{createOperationReporterBridge as ft}from"./operation-progress.js";import{formatError as P,serializeError as Y}from"../../kernel/error-serialization.js";export async function runAll(e,i,o,r,m=[]){const R=r.fileNames||[];ct(e,i,o,R);const{productDir:u}=r,p=De(u),y=at(u),v=new Map(m.map(n=>[n.name,n])),t=Date.now(),C=e[0]?.artifacts??null,b=[...new Set(e.flatMap(n=>n.uses.flatMap(l=>Object.keys(v.get(l)?.testkit.serviceEnv??{}))))].sort(),f={git:await rt(u),host:{hostname:et(),username:tt()},testkitVersion:Ze().version},F=e[0]?.redact??[],w=$e(process.env,F),Z=r.onEvent,k=Z?Ne(n=>{Z(w.redact(n))},{debug:r.debug}):null,M=Pe(u),B={workerCount:0},z=resolveRunExecutionConfig(e[0],r),W=collectServicePlans(e,v,i,o,r,z,k);k?.setServicePlans?.(W);const N=Ae(W,t),$=(n,l="product")=>{for(const c of N.values())c.skipped||be(c,n,{classification:l});k?.runError?.(n)},de=o.map(n=>({...n})),H={typeValues:i,suiteSelectors:de,fileNames:R,groupFilter:r.serviceFilter||null,planning:null};let S=()=>{};const pe=ft(k),K=ze({onChange:n=>{pe(n),S()}}),ee=je({logRegistry:M,onChange:()=>{S()}}),d=Oe({logRegistry:M,redactKeys:F,setupRegistry:ee,onPublishedChange:()=>{S()}});w.useSourceTargets(()=>d.redactionTargets()),d.recordGroups(W.map(n=>({dbBackend:n.dbBackend,name:n.config.name})));const J=W.filter(n=>!n.skipped),Q=r.environments?.supplied??{},{plans:I,provisionedGraphKeys:_,staticGraphKeys:te}=assignRuntimeGraphs(J,new Set(Object.keys(Q)));let g;const s=await p.start({keepAlive:!0,localInfrastructure:_.size>0,redactKeys:F,redactionTargets:()=>d.redactionTargets(),resetResultsBeforePublish:!0});S=ut({productDir:u,runId:s.runId,configs:e,trackers:N,startedAt:t,execution:z,workerState:B,selection:H,archiveConfig:C,declaredEnvironmentKeys:b,metadata:f,environmentRecord:d,operationTimingRegistry:K,reporter:k});const j={artifacts:null},D={result:null},re=n=>{const l=w.redact(n);try{return Le(u,s.runId,l),l}catch(c){const h=`Could not write the maintenance record: ${w.redact(P(c))}`;return{...l,status:"failed",error:l.error===null?h:`${l.error}; ${h}`}}},ne=n=>(D.result=n.maintenance,n.maintenance.status==="failed"&&k?.warning?.(`Post-run maintenance failed: ${n.maintenance.error??"unknown error"}`),n.sealed),ge=async(n,l)=>(s.beginFinalization(),ne(await s.finish(n,c=>{const h=l(c);return j.artifacts={runArtifact:h.runArtifact},h},"normal",re)));let A,q;const ie=(n,l,c)=>({productDir:u,runId:s.runId,results:n,startedAt:t,finishedAt:l,execution:z,workerCount:B.workerCount,selection:H,archiveConfig:C,declaredEnvironmentKeys:b,metadata:f,environmentRecord:d,operationTimingRegistry:K,reporter:k,runInterruptReason:s.getInterruptReason(),runStatus:c,publishRunFinalization:ge});S();let T=null,ae=Symbol("no release failure");try{try{if(J.length>0){const c=xe(u),{capacity:h,queue:L}=await U(K,{name:"testkit.plan",kind:"planning",summary:"plan test execution",scope:{serviceCount:J.length}},()=>({capacity:Ve(I.filter(a=>_.has(a.runtimeKey))),queue:Ee(I.map(a=>prepareSchedulerPlan(a,Object.hasOwn(Q,a.config.name)?()=>!1:E=>Je(u,E)!==null)),{timings:c})}));H.planning=Fe(L),d.recordCapacity(h),k?.setTotalFileCount?.(L.length),B.workerCount=Math.max(1,Math.min(z.taskConcurrency,L.length));const oe=Array.from({length:B.workerCount},(a,E)=>ot(E+1,u));h.graphs.length>0&&s.provision(h.graphs,{reporter:k,environmentRecord:d,logRegistry:M,setupRegistry:ee,operationTimingRegistry:K,skipSchemaSourceVerify:r.skipSchemaSourceVerify});const he=Ge(s,new Map(I.map(a=>[a.config.name,a.config.uses])),u,d),ye=He(new Map(I.filter(a=>te.has(a.runtimeKey)).map(a=>[a.config.name,a.uses])),u,d),ve=Qe(Q,{productDir:u,record:d,signal:s.signal}),se=Xe(u),ce=a=>_.has(a)?{kind:"configured",entry:he}:te.has(a)?{kind:"static",entry:ye}:{kind:"supplied",entry:ve},ke={recordRunError:$,runId:s.runId,signal:s.signal,isStopRequested:()=>s.isStopRequested(),environments:yt(Ue(new Map(I.map(({runtimeKey:a})=>[a,ce(a).entry]))),r.environments?.hold),leases:gt(se,d),locks:Ye(),request:vt(u,e,z,r.vitest),environmentKeys:new Map(I.map(({config:a,runtimeKey:E})=>{const{entry:G,kind:O}=ce(E),x=G.preview?.(a.name)??null;return[a.name,qe(x===null?[O,E,a.name]:[O,x])]}))},ue=[];try{const E=(await U(K,{name:"testkit.execute",kind:"execution",summary:"execute local worker queue",scope:{workerCount:oe.length,taskCount:L.length}},async()=>Promise.allSettled(oe.map(G=>st(G,L,ke,y,N,ue,Se,(O,x,V,X)=>{Ke(O,x,V,X),V.environmentValuesMissing!==void 0&&d.recordMissingValues(_e(x),V.environmentValuesMissing),S()},(O,x,V,X,Re)=>{Te(O,x,V,X,Re),S()},k))))).flatMap(workerResultErrorsForOrchestrator);for(const G of E)$(G);E.length>0&&S(),d.recordRuntimeStats(s.getStats())}finally{h.graphs.length>0&&await U(K,{name:"testkit.cleanup",kind:"cleanup",summary:"stop managed runtimes and services"},()=>ht(d,()=>s.cleanupAll()).catch(a=>{throw ae=a,$(Y(a).message||P(a),"infra"),a})),se.close()}Me(u,c,ue)}}catch(c){T=Y(c),c!==ae&&$(T.message||P(c)),S()}g=T===null?0:s.isStopRequested()?130:1,q=Date.now(),A=e.map(c=>le(N.get(c.name),t,q));const n=resolveRunGateFailure(A,r.allowEmpty===!0);await U(K,{name:"testkit.collect",kind:"collection",summary:"close and collect run logs"},()=>M.closeAll());const l=await lt(ie(A,q,s.isStopRequested()?"interrupted":T===null&&n===null?null:"failed"));return T!==null&&(g=1),A.some(c=>c.failed||c.infraFailed)&&(g=1),n!==null&&(g=1),l.shouldFailArchivePublication&&(g=1),l.shouldFailArchiveCleanup&&(g=1),(l.runArtifact.run.status==="interrupted"||s.isStopRequested()||s.committedFinalization?.artifactStatus==="interrupted")&&(g=Math.max(g,130)),{runArtifact:l.runArtifact,results:w.redact(A),exitCode:g,...T===null?{}:{failure:w.redact(T)},...D.result===null?{}:{maintenance:w.redact(D.result)}}}catch(n){const l=Y(n);$(l.message||P(n),"infra"),S(),s.beginFinalization(),g=s.isStopRequested()||s.committedFinalization?.artifactStatus==="interrupted"?130:1,q=Date.now(),A=e.map(h=>le(N.get(h.name),t,q)),await fe(M);let c=j.artifacts;if(c===null)try{c=await dt(ie(A,q,s.isStopRequested()?"interrupted":"failed"),s,j,l,re,ne)}catch{if(j.artifacts===null)throw n;c=j.artifacts}return(c.runArtifact.run.status==="interrupted"||s.isStopRequested()||s.committedFinalization?.artifactStatus==="interrupted")&&(g=Math.max(g,130)),{runArtifact:c.runArtifact,results:w.redact(A),exitCode:g,failure:w.redact(T??l),...D.result===null?{}:{maintenance:w.redact(D.result)}}}finally{s.dispose(),await fe(M),pt(d,r.environments?.observeRecord,k)}}async function dt(e,i,o,r,m,R){const u=await i.finish("failed",p=>{const y=p.artifactStatus==="interrupted"?p:{artifactStatus:"failed",interruptReason:null},v=e.declaredEnvironmentKeys??[],t=mt({productDir:e.productDir,runId:e.runId,results:e.results,startedAt:e.startedAt,finishedAt:e.finishedAt,execution:e.execution,workerCount:e.workerCount,typeValues:e.selection.typeValues,suiteSelectors:e.selection.suiteSelectors,fileNames:e.selection.fileNames,groupFilter:e.selection.groupFilter,planning:e.selection.planning??null,archiveConfig:e.archiveConfig,declaredEnvironmentKeys:v,metadata:e.metadata,environment:e.environmentRecord.snapshot(),operationTimings:e.operationTimingRegistry?.listOperations()??[],runInterruptReason:y.interruptReason,runStatus:y.artifactStatus});t.finalizationFailure={...r};const b={runArtifact:Ie(e.productDir,t,{archiveConfig:e.archiveConfig,declaredEnvironmentKeys:v,redactKeys:e.environmentRecord.redactKeys,redactionTargets:e.environmentRecord.redactionTargets()})};return o.artifacts=b,b},"recovery",m);return R(u)}function pt(e,i,o){if(i!==void 0)try{i(e.snapshot())}catch(r){try{o?.warning?.(`Environment record observer failed: ${P(r)}`)}catch{}}}function gt(e,i){return{...e,remove(o){const r=i.releaseFor(o.id);try{e.remove(o)}catch(m){throw r?.releaseFailed(m),m}r?.released()}}}async function ht(e,i){try{await i()}catch(o){throw e.recordCleanup(o),o}e.recordCleanup(null)}async function fe(e){try{await e.closeAll()}catch{}}export function workerResultErrorsForOrchestrator(e){return e.status==="rejected"?[P(e.reason)]:[...new Set(e.value.unpersistedErrors)]}export function resolveRunGateFailure(e,i=!1){return e.some(r=>r.failed)?"test failure":e.some(r=>r.infraFailed)?"test infrastructure failure":e.some(r=>r.notRunFileCount>0)?"one or more required test files did not run":e.some(r=>r.skippedFileCount>0)?"one or more selected test files were skipped":e.reduce((r,m)=>r+m.completedFileCount,0)===0&&!i?"no runnable test files executed":null}function yt(e,i){return i===void 0?e:We(e,i)}function vt(e,i,o,r){return{vitest:r,productDir:e,fileTimeoutSeconds:o.fileTimeoutSeconds,ownerRoots:Object.fromEntries(i.map(m=>[m.name,m.roots]))}}export function resolveRunExecutionConfig(e,i){return nt({cli:{fileTimeoutSeconds:i.fileTimeoutSeconds,taskConcurrency:i.taskConcurrency},repo:e?{fileTimeoutSeconds:e.testkit.execution.fileTimeoutSeconds,taskConcurrency:e.testkit.execution.taskConcurrency}:void 0})}export function assignRuntimeGraphs(e,i){const o=e.filter(t=>t.suites.some(C=>C.files.length>0)),r=new Set(o.filter(t=>!i.has(t.config.name)&&Be(t.runtimeConfigs))),m=new Set([...r].map(t=>t.runtimeKey)),R=t=>!r.has(t)&&!i.has(t.config.name)&&me(t.runtimeConfigs),u=new Set(o.filter(t=>R(t)&&t.runtimeKey!=="").map(t=>t.runtimeKey)),p=new Set(o.map(t=>t.runtimeKey)),y=t=>t.runtimeKey!==""&&!m.has(t.runtimeKey)&&(R(t)||!u.has(t.runtimeKey))?t.runtimeKey:p.has(t.config.name)?`group:${t.config.name}`:t.config.name,v=o.map(t=>r.has(t)?t:{...t,runtimeKey:y(t)});return{plans:v,provisionedGraphKeys:m,staticGraphKeys:new Set(v.filter(t=>!m.has(t.runtimeKey)&&!i.has(t.config.name)&&me(t.runtimeConfigs)).map(t=>t.runtimeKey))}}export function prepareSchedulerPlan(e,i){return{graphKey:e.runtimeKey,owner:e.config.name,suites:e.suites.map(o=>({...o,tasks:o.files.map(r=>({file:r,hasSetup:i(r),...it(e.config.testkit,o,r)}))}))}}export function collectServicePlans(e,i,o,r,m,R,u=null){return e.map(p=>{const y=p.uses.map(f=>{const F=i.get(f);if(F===void 0)throw new Error(`Group "${p.name}" uses unknown service "${f}"`);return F}),v=y.some(f=>f.testkit.database!==void 0)?"local":null,t=Ce(p,o,r,m.fileNames||[],m);if(t.length===0)return u?.serviceSkipped?.(p,`no matching files (types=${o.join(",")||"all"} suites=${r.map(f=>f.raw).join(",")||"all"} files=${(m.fileNames||[]).join(",")||"all"})`),{config:p,dbBackend:v,execution:R,skipped:!0,suites:[],runtimeConfigs:[],runtimeNames:[],runtimeKey:null,uses:y};for(const f of t)for(const F of f.skippedFiles)u?.plannedSkip?.({serviceName:p.name,testType:f.testType,engineId:f.engineId,profileId:f.profileId,file:F.path,reason:F.reason});const C=we(y,i),b=C.map(f=>f.name).sort();return{config:p,dbBackend:v,execution:R,skipped:!1,suites:t,runtimeConfigs:C,runtimeNames:b,runtimeKey:b.join("|"),uses:y}})}
|
|
1
|
+
import{resolveRuntimeClosure as Ee}from"../../config/loader/dependencies.js";import{claimNextTask as Ke,collectSuites as Te}from"./planning.js";import{buildRunPlanningMetadata as Ae,buildScheduledQueue as be}from"./scheduler/index.js";import{addTrackerError as Me,buildServiceTrackers as xe,finalizeServiceResult as ge,recordGraphError as Ie,recordTaskOutcome as Pe,resolveRunGateFailure as qe}from"./results.js";import{loadTimings as ze,saveTimings as Oe}from"../../evidence/index.js";import{writeRunArtifact as Ne}from"../../evidence/artifacts.js";import{stableJson as $e}from"../../evidence/storage.js";import{createRunEnvironmentRecord as Le}from"../../evidence/environment-record.js";import{createRunLogRegistry as je}from"../../evidence/logs.js";import{createOperationTimingRegistry as De,timeOperation as J}from"../../evidence/operation-timings.js";import{createRunReporterFromListener as Ge}from"./run-events.js";import{createCallerRedaction as Ve}from"./caller-redaction.js";import{createSetupOperationRegistry as Be}from"../../evidence/setup-progress.js";import{createLocalRunEnvironment as We}from"../../environment/run-environment.js";import{writeRunMaintenance as Ue}from"../../evidence/maintenance.js";import{createConfigDrivenEnvironments as _e,planConfiguredCapacity as He}from"../environments/config-driven.js";import{declaresProvisioning as Je,declaresValues as he,holdBeforeRelease as Qe,routeEnvironments as Xe}from"../environments/routing.js";import{createStaticValueEnvironments as Ye}from"../environments/static-values.js";import{createSuppliedEnvironments as Ze}from"../environments/supplied.js";import{environmentTaskIdentity as et}from"./environment.js";import{createTaskLeases as tt}from"./task-leases.js";import{createTaskLocks as rt}from"./scheduler/locks.js";import{readPackageMetadata as nt,safeHostname as it,safeUsername as at}from"../../process/host/metadata.js";import{collectGitMetadata as st}from"../../repo/metadata.js";import{resolveExecutionConfig as ot}from"../../config/loader/decode-execution.js";import{projectGroupTestPolicy as ct}from"../../config/loader/discover-tests.js";import{resolveTaskPolicy as ut}from"../../config/loader/test-selection.js";import"../../environment/run-environment.js";import{createEngineManager as lt}from"../../engines/factory.js";import{createWorker as mt,runWorker as ft}from"./worker-loop.js";import{ensureRequestedFilesMatch as dt}from"./run-guards.js";import{createLiveSnapshotWriter as pt}from"./live-run.js";import{finalizeRunArtifacts as gt}from"./run-finalization.js";import{createRunFinisher as ht}from"./run-finisher.js";import{createRunEvidencePublisher as yt}from"../../evidence/run-evidence.js";import{buildRunArtifact as vt}from"../../evidence/reporting.js";import{createOperationReporterBridge as kt}from"./operation-progress.js";import{formatError as z,serializeError as Z}from"../../kernel/error-serialization.js";export async function runAll(e,a,o,n,m=[]){const k=n.fileNames||[];dt(e,a,o,k);const{productDir:c}=n,p=We(c),h=lt(c),y=new Map(m.map(r=>[r.name,r])),t=Date.now(),S=e[0]?.artifacts??null,E=[...new Set(e.flatMap(r=>r.uses.flatMap(l=>Object.keys(y.get(l)?.testkit.serviceEnv??{}))))].sort(),f={git:await st(c),host:{hostname:it(),username:at()},testkitVersion:nt().version},C=e[0]?.redact??[],R=Ve(process.env,C),ee=n.onEvent;let te=()=>{};const v=ee?Ge(r=>{r.type==="run.task_started"&&te(),ee(R.redact(r))},{debug:n.debug}):null,M=je(c),B={workerCount:0},O=resolveRunExecutionConfig(e[0],n),W=collectServicePlans(e,y,a,o,n,O,v);v?.setServicePlans?.(W);const N=xe(W,t),$=(r,l="product")=>{for(const s of N.values())s.skipped||Me(s,r,{classification:l});v?.runError?.(r)},ve=o.map(r=>({...r})),Q={typeValues:a,suiteSelectors:ve,fileNames:k,groupFilter:n.serviceFilter||null,planning:null};let w=()=>{};const ke=kt(v),b=De({onChange:r=>{ke(r),w()}}),re=Be({logRegistry:M,onChange:()=>{w()}}),d=Le({logRegistry:M,redactKeys:C,setupRegistry:re,onPublishedChange:()=>{w()}});R.useSourceTargets(()=>d.redactionTargets()),d.recordGroups(W.map(r=>({dbBackend:r.dbBackend,name:r.config.name})));const X=W.filter(r=>!r.skipped),ne=n.environments?.supplied??{},{plans:x,provisionedGraphKeys:ie,staticGraphKeys:ae}=assignRuntimeGraphs(X,new Set(Object.keys(ne)));let g;const u=await p.start({evidence:yt({redactKeys:C,redactionTargets:()=>d.redactionTargets(),resetResults:!0}),keepAlive:!0}),L=ht(u),U=pt({productDir:c,runId:u.runId,configs:e,trackers:N,startedAt:t,execution:O,workerState:B,selection:Q,archiveConfig:S,declaredEnvironmentKeys:E,metadata:f,environmentRecord:d,operationTimingRegistry:b,reporter:v});w=()=>{U.update()},te=()=>{U.flush()};const j={artifacts:null},D={result:null},se=r=>{const l=R.redact(r);try{return Ue(c,u.runId,l),l}catch(s){const F=`Could not write the maintenance record: ${R.redact(z(s))}`;return{...l,status:"failed",error:l.error===null?F:`${l.error}; ${F}`}}},oe=r=>(D.result=r.maintenance,r.maintenance.status==="failed"&&v?.warning?.(`Post-run maintenance failed: ${r.maintenance.error??"unknown error"}`),r.sealed),Re=async(r,l)=>(u.beginFinalization(),oe(await L.finish(r,s=>{const F=l(s);return j.artifacts={runArtifact:F.runArtifact},F},"normal",se)));let K,I;const ce=(r,l,s)=>({productDir:c,runId:u.runId,results:r,startedAt:t,finishedAt:l,execution:O,workerCount:B.workerCount,selection:Q,archiveConfig:S,declaredEnvironmentKeys:E,metadata:f,environmentRecord:d,operationTimingRegistry:b,reporter:v,runInterruptReason:u.getInterruptReason(),runStatus:s,publishRunFinalization:Re});w();let T=null,ue=Symbol("no release failure");try{try{if(X.length>0){const s=ze(c),F=_e(u,new Map(x.map(i=>[i.config.name,i.config.uses])),c,d),we=Ye(new Map(x.filter(i=>ae.has(i.runtimeKey)).map(i=>[i.config.name,i.uses])),c,d),Se=Ze(ne,{productDir:c,record:d,signal:u.signal}),le=i=>ie.has(i)?F:ae.has(i)?we:Se,me=Xe(new Map(x.map(({runtimeKey:i})=>[i,le(i)]))),{capacity:_,queue:G}=await J(b,{name:"testkit.plan",kind:"planning",summary:"plan test execution",scope:{serviceCount:X.length}},()=>({capacity:He(x.filter(i=>ie.has(i.runtimeKey))),queue:be(x.map(i=>prepareSchedulerPlan(i,A=>me.hasSetup({file:A,graphKey:i.runtimeKey,targetName:i.config.name}))),{timings:s})}));Q.planning=Ae(G),d.recordCapacity(_),v?.setTotalFileCount?.(G.length),B.workerCount=Math.max(1,Math.min(O.taskConcurrency,G.length));const fe=Array.from({length:B.workerCount},(i,A)=>mt(A+1,c));_.graphs.length>0&&u.provision(_.graphs,{reporter:v,environmentRecord:d,logRegistry:M,setupRegistry:re,operationTimingRegistry:b,skipSchemaSourceVerify:n.skipSchemaSourceVerify});const de=tt(c),Ce={recordRunError:$,runId:u.runId,signal:u.signal,isStopRequested:()=>u.isStopRequested(),environments:Ft(me,n.environments?.hold),leases:St(de,d),locks:rt(),request:Et(c,e,O,n.vitest),environmentKeys:new Map(x.flatMap(({config:i,runtimeKey:A})=>{const P=le(A),q=P.preview?.(i.name)??null;return q===null?[]:[[i.name,$e([q,P.checksServices?.(i.name)??!1])]]}))},pe=[];try{const A=(await J(b,{name:"testkit.execute",kind:"execution",summary:"execute local worker queue",scope:{workerCount:fe.length,taskCount:G.length}},async()=>Promise.allSettled(fe.map(P=>ft(P,G,Ce,h,N,pe,Ke,(q,H,V,Y)=>{Pe(q,H,V,Y),V.environmentValuesMissing!==void 0&&d.recordMissingValues(et(H),V.environmentValuesMissing),w()},(q,H,V,Y,Fe)=>{Ie(q,H,V,Y,Fe),w()},v))))).flatMap(workerResultErrorsForOrchestrator);for(const P of A)$(P);A.length>0&&w(),d.recordRuntimeStats(u.getStats())}finally{_.graphs.length>0&&await J(b,{name:"testkit.cleanup",kind:"cleanup",summary:"stop managed runtimes and services"},()=>Ct(d,()=>u.cleanupAll()).catch(i=>{throw ue=i,$(Z(i).message||z(i),"infra"),i})),de.close()}Oe(c,s,pe)}}catch(s){T=Z(s),s!==ue&&$(T.message||z(s)),w()}U.finish(),g=T===null?0:u.isStopRequested()?130:1,I=Date.now(),K=e.map(s=>ge(N.get(s.name),t,I));const r=qe(K,n.allowEmpty===!0);await J(b,{name:"testkit.collect",kind:"collection",summary:"close and collect run logs"},()=>M.closeAll());const l=await gt(ce(K,I,u.isStopRequested()?"interrupted":T===null&&r===null?null:"failed"));return T!==null&&(g=1),K.some(s=>s.failed||s.infraFailed)&&(g=1),r!==null&&(g=1),l.shouldFailArchivePublication&&(g=1),l.shouldFailArchiveCleanup&&(g=1),(l.runArtifact.run.status==="interrupted"||u.isStopRequested()||L.committedFinalization?.artifactStatus==="interrupted")&&(g=Math.max(g,130)),{runArtifact:l.runArtifact,results:R.redact(K),exitCode:g,...T===null?{}:{failure:R.redact(T)},...D.result===null?{}:{maintenance:R.redact(D.result)}}}catch(r){const l=Z(r);$(l.message||z(r),"infra"),w(),U.finish(),u.beginFinalization(),g=u.isStopRequested()||L.committedFinalization?.artifactStatus==="interrupted"?130:1,I=Date.now(),K=e.map(F=>ge(N.get(F.name),t,I)),await ye(M);let s=j.artifacts;if(s===null)try{s=await Rt(ce(K,I,u.isStopRequested()?"interrupted":"failed"),L,j,l,se,oe)}catch{if(j.artifacts===null)throw r;s=j.artifacts}return(s.runArtifact.run.status==="interrupted"||u.isStopRequested()||L.committedFinalization?.artifactStatus==="interrupted")&&(g=Math.max(g,130)),{runArtifact:s.runArtifact,results:R.redact(K),exitCode:g,failure:R.redact(T??l),...D.result===null?{}:{maintenance:R.redact(D.result)}}}finally{u.dispose(),await ye(M),wt(d,n.environments?.observeRecord,v)}}async function Rt(e,a,o,n,m,k){const c=await a.finish("failed",p=>{const h=p.artifactStatus==="interrupted"?p:{artifactStatus:"failed",interruptReason:null},y=e.declaredEnvironmentKeys??[],t=vt({productDir:e.productDir,runId:e.runId,results:e.results,startedAt:e.startedAt,finishedAt:e.finishedAt,execution:e.execution,workerCount:e.workerCount,typeValues:e.selection.typeValues,suiteSelectors:e.selection.suiteSelectors,fileNames:e.selection.fileNames,groupFilter:e.selection.groupFilter,planning:e.selection.planning??null,archiveConfig:e.archiveConfig,declaredEnvironmentKeys:y,metadata:e.metadata,environment:e.environmentRecord.snapshot(),operationTimings:e.operationTimingRegistry?.listOperations()??[],runInterruptReason:h.interruptReason,runStatus:h.artifactStatus});t.finalizationFailure={...n};const E={runArtifact:Ne(e.productDir,t,{archiveConfig:e.archiveConfig,declaredEnvironmentKeys:y,redactKeys:e.environmentRecord.redactKeys,redactionTargets:e.environmentRecord.redactionTargets()})};return o.artifacts=E,E},"recovery",m);return k(c)}function wt(e,a,o){if(a!==void 0)try{a(e.snapshot())}catch(n){try{o?.warning?.(`Environment record observer failed: ${z(n)}`)}catch{}}}function St(e,a){return{...e,remove(o){const n=a.releaseFor(o.id);try{e.remove(o)}catch(m){throw n?.releaseFailed(m),m}n?.released()}}}async function Ct(e,a){try{await a()}catch(o){throw e.recordCleanup(o),o}e.recordCleanup(null)}async function ye(e){try{await e.closeAll()}catch{}}export function workerResultErrorsForOrchestrator(e){return e.status==="rejected"?[z(e.reason)]:[...new Set(e.value.unpersistedErrors)]}function Ft(e,a){return a===void 0?e:Qe(e,a)}function Et(e,a,o,n){return{vitest:n,productDir:e,fileTimeoutSeconds:o.fileTimeoutSeconds,ownerRoots:Object.fromEntries(a.map(m=>[m.name,m.roots]))}}export function resolveRunExecutionConfig(e,a){return ot({cli:{fileTimeoutSeconds:a.fileTimeoutSeconds,taskConcurrency:a.taskConcurrency},repo:e?{fileTimeoutSeconds:e.testkit.execution.fileTimeoutSeconds,taskConcurrency:e.testkit.execution.taskConcurrency}:void 0})}export function assignRuntimeGraphs(e,a){const o=e.filter(t=>t.suites.some(S=>S.files.length>0)),n=new Set(o.filter(t=>!a.has(t.config.name)&&Je(t.runtimeConfigs))),m=new Set([...n].map(t=>t.runtimeKey)),k=t=>!n.has(t)&&!a.has(t.config.name)&&he(t.runtimeConfigs),c=new Set(o.filter(t=>k(t)&&t.runtimeKey!=="").map(t=>t.runtimeKey)),p=new Set(o.map(t=>t.runtimeKey)),h=t=>t.runtimeKey!==""&&!m.has(t.runtimeKey)&&(k(t)||!c.has(t.runtimeKey))?t.runtimeKey:p.has(t.config.name)?`group:${t.config.name}`:t.config.name,y=o.map(t=>n.has(t)?t:{...t,runtimeKey:h(t)});return{plans:y,provisionedGraphKeys:m,staticGraphKeys:new Set(y.filter(t=>!m.has(t.runtimeKey)&&!a.has(t.config.name)&&he(t.runtimeConfigs)).map(t=>t.runtimeKey))}}export function prepareSchedulerPlan(e,a){const o=ct(e.config.testkit);return{graphKey:e.runtimeKey,owner:e.config.name,suites:e.suites.map(n=>({...n,tasks:n.files.map(m=>({file:m,hasSetup:a(m),...ut(o,n,m)}))}))}}export function collectServicePlans(e,a,o,n,m,k,c=null){return e.map(p=>{const h=p.uses.map(f=>{const C=a.get(f);if(C===void 0)throw new Error(`Group "${p.name}" uses unknown service "${f}"`);return C}),y=h.some(f=>f.testkit.database!==void 0)?"local":null,t=Te(p,o,n,m.fileNames||[],m);if(t.length===0)return c?.serviceSkipped?.(p,`no matching files (types=${o.join(",")||"all"} suites=${n.map(f=>f.raw).join(",")||"all"} files=${(m.fileNames||[]).join(",")||"all"})`),{config:p,dbBackend:y,execution:k,skipped:!0,suites:[],runtimeConfigs:[],runtimeNames:[],runtimeKey:null,uses:h};for(const f of t)for(const C of f.skippedFiles)c?.plannedSkip?.({serviceName:p.name,testType:f.testType,engineId:f.engineId,profileId:f.profileId,file:C.path,reason:C.reason});const S=Ee(h,a),E=S.map(f=>f.name).sort();return{config:p,dbBackend:y,execution:k,skipped:!1,suites:t,runtimeConfigs:S,runtimeNames:E,runtimeKey:E.join("|"),uses:h}})}
|
package/dist/app/run/planning.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{matchesSelectedTypes as x,matchesSuiteSelectors as d}from"../../
|
|
1
|
+
import{matchesSelectedTypes as x,matchesSuiteSelectors as d}from"../../config/loader/test-selection.js";import{PUBLIC_TEST_TYPES as m}from"../../domain/identifiers.js";import{resolveExecutionProfile as S}from"../../engines/catalog.js";export function collectSuites(e,l,a,o=[],r={}){const t=new Set(o.map(h)),f=[];let c=0;for(const n of[...e.suites].sort((i,s)=>k(i.testType)-k(s.testType))){const i=S(n),s=t.size===0?n.files:n.files.filter(g=>t.has(h(g)));if(!x(n.testType,l)||!d(n.testType,n.name,a)||s.length===0)continue;const{files:p,skippedFiles:u}=y(e,n.testType,n.name,s,r);f.push({...n,files:p,skippedFiles:u,orderIndex:c,weight:n.testkit?.weight||(i.taskBatching==="compatible"?Math.max(2,Math.max(1,p.length)):Math.max(1,p.length)),totalFileCount:s.length}),c+=1}return f}function k(e){return e===void 0?m.length:m.indexOf(e)}export function claimNextTask(e,l,a=()=>!0){if(e.length===0)return null;const o=[],r=[];for(let t=0;t<e.length;t+=1)l&&e[t].graphKey===l?o.push(t):r.push(t);for(const t of[...o,...r])if(a(e[t]))return e.splice(t,1)[0];return null}function y(e,l,a,o,r={}){if(r.ignoreSkipRules)return{files:o,skippedFiles:[]};const t=e.testkit.skip,f=(t?.suites||[]).find(i=>d(l,a,[i.selector]))?.reason||null,c=[],n=[];for(const i of o){const s=h(i),u=(e.testkit.fileMetadataByPath?.get(s)||null)?.skipReason||t?.fileReasonByPath?.get(s)||f;if(u){n.push({path:s,reason:u});continue}c.push(i)}return{files:c,skippedFiles:n}}function h(e){return String(e).split("\\").join("/")}
|
package/dist/app/run/results.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{suiteKey as x}from"../../domain/suite-selection.js";import{mergeFailureDetails as b}from"./failure-details.js";import{posixPath as g}from"../../process/files/paths.js";export function buildServiceTrackers(e,r){const o=new Map;for(const t of e){const a=t.skipped?[]:t.suites.map(w),l={name:t.config.name,skipped:t.skipped===!0,suiteCount:a.length,suites:a,suitesByKey:new Map(a.map(n=>[n.key,n])),errors:[],errorSet:new Set,errorDetails:[],errorDetailSet:new Set,startedAt:r,firstTaskAt:null,lastTaskAt:null,totalTaskDurationMs:0};o.set(t.config.name,l)}return o}function w(e){const r=new Map;for(const o of e.files){const t=g(o);r.set(t,
|
|
1
|
+
import{suiteKey as x}from"../../domain/suite-selection.js";import{mergeFailureDetails as b}from"./failure-details.js";import{posixPath as g}from"../../process/files/paths.js";export function buildServiceTrackers(e,r){const o=new Map;for(const t of e){const a=t.skipped?[]:t.suites.map(w),l={name:t.config.name,skipped:t.skipped===!0,suiteCount:a.length,suites:a,suitesByKey:new Map(a.map(n=>[n.key,n])),errors:[],errorSet:new Set,errorDetails:[],errorDetailSet:new Set,startedAt:r,firstTaskAt:null,lastTaskAt:null,totalTaskDurationMs:0};o.set(t.config.name,l)}return o}function w(e){const r=new Map;for(const o of e.files){const t=g(o);r.set(t,h(t,"not_run",null))}for(const o of e.skippedFiles??[])r.set(o.path,h(o.path,"skipped",o.reason));return{key:x(e),name:e.name,testType:e.testType,engineId:e.engineId,profileId:e.profileId,orderIndex:e.orderIndex,failedFiles:[],failedFileSet:new Set,fileResultsByPath:r,durationMs:0,error:null}}function h(e,r,o){return{path:e,failed:r==="failed",durationMs:0,error:null,reason:o,status:r,artifacts:[],infraFailed:!1,blockedByRuntime:!1,failureDetails:[],runtimeIsolation:null,runtimeBatch:null,runtimeResets:[]}}export function recordTaskOutcome(e,r,o,t=Date.now()){const a=e.get(r.serviceName);if(!a||a.skipped)return;const l=a.suitesByKey.get(r.suiteKey);if(!l)return;const n=P(o,r,t);a.firstTaskAt=a.firstTaskAt===null?n.startedAt:Math.min(a.firstTaskAt,n.startedAt),a.lastTaskAt=a.lastTaskAt===null?n.finishedAt:Math.max(a.lastTaskAt,n.finishedAt),a.totalTaskDurationMs+=n.durationMs,l.durationMs+=n.durationMs;const f=g(r.file),s=l.fileResultsByPath.get(f),d=s??h(f,n.status,n.reason);d.failed=n.status==="failed",d.durationMs=n.durationMs,d.error=n.error,d.reason=n.reason,d.status=n.status,d.artifacts=n.artifacts,d.infraFailed=n.infraFailed,d.blockedByRuntime=n.blockedByRuntime,d.failureDetails=n.failureDetails,d.runtimeIsolation=n.runtimeIsolation,d.runtimeBatch=n.runtimeBatch,d.runtimeResets=n.runtimeResets,s||l.fileResultsByPath.set(f,{...d}),n.status==="failed"&&!l.failedFileSet.has(r.file)&&(l.failedFileSet.add(r.file),l.failedFiles.push(r.file)),n.error&&!l.error&&(l.error=n.error)}function P(e,r,o){const t=e.durationMs??0,a=e.finishedAt??o;return{artifacts:e.artifacts??[],blockedByRuntime:e.blockedByRuntime??!1,durationMs:t,error:e.error??null,failureDetails:b(e.failureDetails??[]),finishedAt:a,infraFailed:e.infraFailed??!1,reason:e.reason??null,runtimeBatch:e.runtimeBatch??null,runtimeIsolation:e.runtimeIsolation??r.runtimeIsolation??null,runtimeResets:e.runtimeResets??[],startedAt:e.startedAt??Math.max(0,a-t),status:e.status}}export function recordGraphError(e,r,o,t=Date.now(),a={}){const l=r?.targetNames||[];for(const n of l){const f=e.get(n);f&&!f.skipped&&(addTrackerError(f,o,a),f.lastTaskAt=t)}}export function addTrackerError(e,r,o={}){const t=r.trim();if(!t)return;const a=o.classification??"product";e.errorSet.has(t)||(e.errorSet.add(t),e.errors.push(t));const l=`${a}\0${t}`;e.errorDetailSet.has(l)||(e.errorDetailSet.add(l),e.errorDetails.push({message:t,classification:a}))}export function finalizeServiceResult(e,r,o){if(!e||e.skipped)return{name:e?.name||"unknown",failed:!1,skipped:!0,suiteCount:0,completedSuiteCount:0,skippedSuiteCount:0,failedSuiteCount:0,totalFileCount:0,completedFileCount:0,passedFileCount:0,failedFileCount:0,skippedFileCount:0,notRunFileCount:0,durationMs:0,totalTaskDurationMs:0,suites:[],errors:[],errorDetails:[],infraFailed:!1,blockedByRuntime:!1};const t=[...e.suites].sort((i,u)=>i.orderIndex-u.orderIndex||i.name.localeCompare(u.name)).map(i=>z(i)),a=t.filter(i=>i.completedFileCount+i.skippedFileCount===i.fileCount).length,l=t.filter(i=>i.skippedFileCount===i.fileCount&&i.fileCount>0).length,n=t.filter(i=>i.failedFileCount>0).length,f=t.reduce((i,u)=>i+u.fileCount,0),s=t.reduce((i,u)=>i+u.completedFileCount,0),d=t.reduce((i,u)=>i+u.failedFileCount,0),R=t.reduce((i,u)=>i+u.passedFileCount,0),C=t.reduce((i,u)=>i+u.skippedFileCount,0),p=f-s-C,m=t.reduce((i,u)=>i+u.files.filter(c=>c.infraFailed===!0).length,0),k=t.reduce((i,u)=>i+u.files.filter(c=>c.blockedByRuntime===!0).length,0),y=t.reduce((i,u)=>i+u.files.filter(c=>c.status==="cancelled"||c.status==="interrupted").length,0),T=p>0&&y===p,M=p>0&&m===p,F=e.errorDetails,B=F.filter(i=>i.classification!=="infra").length,D=F.filter(i=>i.classification==="infra").length,I=m>0||D>0,S=k>0,A=e.firstTaskAt!==null&&e.lastTaskAt!==null?Math.max(0,e.lastTaskAt-e.firstTaskAt):Math.max(0,o-r);return{name:e.name,failed:n>0||B>0||a<e.suiteCount&&!T&&!M,infraFailed:I,blockedByRuntime:S,infraFailedFileCount:m,blockedByRuntimeFileCount:k,skipped:!1,suiteCount:e.suiteCount,completedSuiteCount:a,skippedSuiteCount:l,failedSuiteCount:n,totalFileCount:f,completedFileCount:s,passedFileCount:R,failedFileCount:d,skippedFileCount:C,notRunFileCount:p,durationMs:A,totalTaskDurationMs:e.totalTaskDurationMs,suites:t,errors:e.errors,errorDetails:F}}function z(e){const r=[...e.fileResultsByPath.values()].sort((s,d)=>s.path.localeCompare(d.path)).map(s=>({path:s.path,failed:s.failed,status:s.status,durationMs:s.durationMs,error:s.error,reason:s.reason,...s.infraFailed?{infraFailed:!0}:{},...s.blockedByRuntime?{blockedByRuntime:!0}:{},...s.failureDetails.length>0?{failureDetails:s.failureDetails}:{},...s.artifacts.length>0?{artifacts:s.artifacts}:{},...s.runtimeIsolation?{runtimeIsolation:s.runtimeIsolation}:{},...s.runtimeBatch?{runtimeBatch:s.runtimeBatch}:{},...s.runtimeResets.length>0?{runtimeResets:s.runtimeResets}:{}})),o=r.length,t=r.filter(s=>s.status==="passed").length,a=r.filter(s=>s.status==="failed").length,l=r.filter(s=>s.status==="skipped").length,n=t+a,f=o-n-l;return{name:e.name,...e.testType===void 0?{}:{testType:e.testType},engineId:e.engineId,profileId:e.profileId,failed:a>0,fileCount:o,completedFileCount:n,passedFileCount:t,failedFileCount:a,skippedFileCount:l,notRunFileCount:f,failedFiles:e.failedFiles,durationMs:e.durationMs,error:e.error,files:r}}export function resolveRunGateFailure(e,r=!1){return e.some(t=>t.failed)?"test failure":e.some(t=>t.infraFailed)?"test infrastructure failure":e.some(t=>t.notRunFileCount>0)?"one or more required test files did not run":e.some(t=>t.skippedFileCount>0)?"one or more selected test files were skipped":e.reduce((t,a)=>t+a.completedFileCount,0)===0&&!r?"no runnable test files executed":null}
|