@gfdelarue/testkit 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/README.md +125 -0
  2. package/bin/testkit-windows-owner-arm64.exe +0 -0
  3. package/bin/testkit-windows-owner-x64.exe +0 -0
  4. package/dist/app/bugs/areas.d.ts +38 -0
  5. package/dist/app/bugs/areas.js +1 -0
  6. package/dist/app/bugs/assessment.d.ts +9 -0
  7. package/dist/app/bugs/assessment.js +1 -0
  8. package/dist/app/bugs/categories.d.ts +41 -0
  9. package/dist/app/bugs/categories.js +1 -0
  10. package/dist/app/bugs/ci-report.d.ts +21 -0
  11. package/dist/app/bugs/ci-report.js +2 -0
  12. package/dist/app/bugs/ci-summary.d.ts +13 -0
  13. package/dist/app/bugs/ci-summary.js +5 -0
  14. package/dist/app/bugs/draft.d.ts +70 -0
  15. package/dist/app/bugs/draft.js +4 -0
  16. package/dist/app/bugs/facts.d.ts +48 -0
  17. package/dist/app/bugs/facts.js +2 -0
  18. package/dist/app/bugs/file.d.ts +45 -0
  19. package/dist/app/bugs/file.js +1 -0
  20. package/dist/app/bugs/gate.d.ts +22 -0
  21. package/dist/app/bugs/gate.js +1 -0
  22. package/dist/app/bugs/guide.d.ts +20 -0
  23. package/dist/app/bugs/guide.js +8 -0
  24. package/dist/app/bugs/issue.d.ts +18 -0
  25. package/dist/app/bugs/issue.js +6 -0
  26. package/dist/app/bugs/known-failures.d.ts +49 -0
  27. package/dist/app/bugs/known-failures.js +1 -0
  28. package/dist/app/bugs/record.d.ts +14 -0
  29. package/dist/app/bugs/record.js +2 -0
  30. package/dist/app/bugs/run-reading.d.ts +51 -0
  31. package/dist/app/bugs/run-reading.js +1 -0
  32. package/dist/app/bugs/store.d.ts +34 -0
  33. package/dist/app/bugs/store.js +2 -0
  34. package/dist/app/campaigns/brief.d.ts +7 -0
  35. package/dist/app/campaigns/brief.js +10 -0
  36. package/dist/app/campaigns/campaign.d.ts +14 -0
  37. package/dist/app/campaigns/campaign.js +1 -0
  38. package/dist/app/campaigns/index.d.ts +38 -0
  39. package/dist/app/campaigns/index.js +2 -0
  40. package/dist/app/campaigns/store.d.ts +5 -0
  41. package/dist/app/campaigns/store.js +1 -0
  42. package/dist/app/campaigns/summary.d.ts +13 -0
  43. package/dist/app/campaigns/summary.js +8 -0
  44. package/dist/app/run/engine-execution.d.ts +1 -0
  45. package/dist/app/run/engine-execution.js +1 -1
  46. package/dist/app/run/failure-details.d.ts +1 -0
  47. package/dist/app/run/failure-details.js +1 -1
  48. package/dist/app/tracker/index.d.ts +8 -0
  49. package/dist/app/tracker/index.js +1 -0
  50. package/dist/app/tracker/remote.d.ts +13 -0
  51. package/dist/app/tracker/remote.js +1 -0
  52. package/dist/app/tracker/token.d.ts +7 -0
  53. package/dist/app/tracker/token.js +5 -0
  54. package/dist/cli/command-catalog.d.ts +26 -0
  55. package/dist/cli/command-catalog.js +1 -1
  56. package/dist/cli/command-registry.js +1 -1
  57. package/dist/cli/commands/bug/file.d.ts +16 -0
  58. package/dist/cli/commands/bug/file.js +2 -0
  59. package/dist/cli/commands/bug/guide.d.ts +5 -0
  60. package/dist/cli/commands/bug/guide.js +1 -0
  61. package/dist/cli/commands/bug/record.d.ts +9 -0
  62. package/dist/cli/commands/bug/record.js +2 -0
  63. package/dist/cli/commands/campaign/finish.d.ts +10 -0
  64. package/dist/cli/commands/campaign/finish.js +1 -0
  65. package/dist/cli/commands/campaign/start.d.ts +7 -0
  66. package/dist/cli/commands/campaign/start.js +1 -0
  67. package/dist/cli/commands/campaign/status.d.ts +13 -0
  68. package/dist/cli/commands/campaign/status.js +2 -0
  69. package/dist/cli/commands/ci/report.d.ts +15 -0
  70. package/dist/cli/commands/ci/report.js +2 -0
  71. package/dist/cli/commands/run.d.ts +1 -1
  72. package/dist/cli/commands/run.js +2 -1
  73. package/dist/cli/renderers/bug/text.d.ts +8 -0
  74. package/dist/cli/renderers/bug/text.js +1 -0
  75. package/dist/cli/renderers/campaign/text.d.ts +2 -0
  76. package/dist/cli/renderers/campaign/text.js +1 -0
  77. package/dist/cli/renderers/ci/text.d.ts +2 -0
  78. package/dist/cli/renderers/ci/text.js +1 -0
  79. package/dist/config/authoring/index.d.ts +10 -0
  80. package/dist/config/loader/bugs.d.ts +15 -0
  81. package/dist/config/loader/bugs.js +1 -0
  82. package/dist/engines/adapters/execution.d.ts +4 -0
  83. package/dist/engines/adapters/execution.js +1 -1
  84. package/dist/engines/k6/adapter/executor.js +1 -1
  85. package/dist/engines/k6/runtime/checks.js +1 -1
  86. package/dist/engines/k6/runtime/http-assertions.d.ts +2 -2
  87. package/dist/engines/k6/runtime/http-assertions.js +1 -1
  88. package/dist/engines/k6/runtime/types.d.ts +2 -0
  89. package/dist/engines/node-vitest/adapter/evidence.js +2 -2
  90. package/dist/engines/node-vitest/adapter/failure-details.d.ts +1 -0
  91. package/dist/engines/node-vitest/adapter/failure-details.js +2 -2
  92. package/dist/engines/playwright/adapter/adapter.js +1 -1
  93. package/dist/engines/playwright/adapter/attachments.d.ts +1 -0
  94. package/dist/engines/playwright/adapter/config.js +3 -3
  95. package/dist/engines/playwright/adapter/evidence.js +1 -1
  96. package/dist/engines/playwright/adapter/reporter.js +1 -1
  97. package/dist/kernel/redaction.d.ts +1 -0
  98. package/dist/kernel/redaction.js +1 -1
  99. package/dist/process/execa-bundle.json +1 -1
  100. package/dist/process/files/json-file.d.ts +1 -0
  101. package/dist/process/files/json-file.js +1 -1
  102. package/dist/repo/history.d.ts +17 -0
  103. package/dist/repo/history.js +4 -0
  104. package/dist/repo/tree-listing.d.ts +10 -0
  105. package/dist/repo/tree-listing.js +1 -0
  106. package/dist/tracker/gitea.d.ts +14 -0
  107. package/dist/tracker/gitea.js +1 -0
  108. package/dist/tracker/github.d.ts +14 -0
  109. package/dist/tracker/github.js +1 -0
  110. package/dist/tracker/http.d.ts +33 -0
  111. package/dist/tracker/http.js +1 -0
  112. package/dist/tracker/index.d.ts +4 -0
  113. package/dist/tracker/index.js +1 -0
  114. package/dist/tracker/redaction.d.ts +4 -0
  115. package/dist/tracker/redaction.js +2 -0
  116. package/dist/tracker/requests.d.ts +17 -0
  117. package/dist/tracker/requests.js +1 -0
  118. package/dist/tracker/responses.d.ts +24 -0
  119. package/dist/tracker/responses.js +1 -0
  120. package/dist/tracker/types.d.ts +51 -0
  121. package/dist/tracker/types.js +1 -0
  122. package/docs/bug-records.md +303 -0
  123. package/package.json +2 -1
  124. package/testkit-config-schema.md +50 -0
@@ -0,0 +1,6 @@
1
+ import{toProductPath as a}from"./draft.js";import{renderFactsBlock as p}from"./facts.js";import{redactBugText as n}from"../../tracker/redaction.js";export const TESTKIT_LABEL="testkit",SEVERITY_LABEL_PREFIX="severity:",AREA_LABEL_PREFIX="area:";const d={color:"0f766e",description:"Filed by Testkit with a regression test.",name:TESTKIT_LABEL};export const SEVERITY_LABELS={critical:{color:"9f1239",description:"The product is unsafe to run or to trust."},high:{color:"e11d48",description:"A core behaviour is broken with no acceptable workaround."},medium:{color:"d97706",description:"A behaviour is broken and a workaround exists, or the reach is limited."},low:{color:"4d7c0f",description:"A minor defect with little consequence."}};const h="475569";export function bugLabels(e,t){return[d,{...SEVERITY_LABELS[e],name:`${SEVERITY_LABEL_PREFIX}${e}`},...t===null?[]:[{color:h,description:t.name,name:`${AREA_LABEL_PREFIX}${t.id}`}]]}export function bugFacts(e,t,i="test"){return{v:1,cause:e.cause,impact:e.impact,confidence:e.confidence,escape:e.escape,caughtBy:e.caughtBy,...e.campaign===void 0?{}:{campaign:e.campaign},testType:e.testType,test:i==="file"?{file:e.test.file}:e.test,foundAt:t.foundAt,fault:e.fault,...t.area===null?{}:{area:t.area},...t.introducedBy===null?{}:{introducedBy:t.introducedBy,introducedMethod:"blame"}}}export function renderBugIssue(e,t,i="test"){const o=m(e),[r,c]=o.fault.lines,u=r===c?`line ${String(r)}`:`lines ${String(r)} to ${String(c)}`;return{body:`${["## Expected behaviour",o.expected,"## Authority",o.authority,"## Observed behaviour",o.observed,"## Root cause",o.rootCause,"## Reproduce",["```sh",reproduceCommand(o),"```"].join(`
2
+ `),[i==="file"?`Test: \`${o.test.file}\`, the whole script, recorded as "${o.test.title}".`:`Test: \`${o.test.file}\`, "${o.test.title}".`,`Found at \`${t.foundAt}\`.`,`Fault: \`${o.fault.path}\`, ${u}.`].join(`
3
+ `),p(bugFacts(o,t,i))].join(`
4
+
5
+ `)}
6
+ `,labels:bugLabels(o.severity,t.area).map(l=>l.name),title:o.title}}function m(e){return{...e,title:n(e.title),expected:n(e.expected),authority:n(e.authority),observed:n(e.observed),rootCause:n(e.rootCause),test:{file:e.test.file,title:n(e.test.title)}}}export function reproduceCommand(e){const t=a(e.productPath,e.test.file);return e.productPath===""?`npx testkit run --file ${s(t)}`:`npx testkit run --dir ${s(e.productPath)} --file ${s(t)}`}function s(e){return/^[\w./@:+-]+$/u.test(e)?e:`'${e.replaceAll("'","'\\''")}'`}
@@ -0,0 +1,49 @@
1
+ import type { TrackerCommitState, TrackerIssue } from "../../tracker/types.js";
2
+ export declare const COMMIT_STATUS_DESCRIPTION_MAX_LENGTH = 140;
3
+ export interface FailedTest {
4
+ readonly file: string;
5
+ readonly script?: true;
6
+ readonly title: string | null;
7
+ }
8
+ export interface KnownTest {
9
+ readonly file: string;
10
+ readonly issue: number;
11
+ readonly title: string | null;
12
+ readonly url: string;
13
+ }
14
+ export interface UnreadableIssue {
15
+ readonly number: number;
16
+ readonly reason: string;
17
+ readonly url: string;
18
+ }
19
+ export interface KnownTests {
20
+ readonly tests: readonly KnownTest[];
21
+ readonly unreadable: readonly UnreadableIssue[];
22
+ }
23
+ export interface KnownFailure {
24
+ readonly failure: FailedTest;
25
+ readonly issue: KnownTest;
26
+ }
27
+ export interface FailureClassification {
28
+ readonly known: readonly KnownFailure[];
29
+ readonly new: readonly FailedTest[];
30
+ }
31
+ export interface RunStatus {
32
+ readonly description: string;
33
+ readonly state: TrackerCommitState;
34
+ }
35
+ export type RunOutcome = {
36
+ readonly classification: FailureClassification;
37
+ readonly kind: "tests-failed";
38
+ } | {
39
+ readonly kind: "passed";
40
+ readonly passedFiles: number;
41
+ } | {
42
+ readonly kind: "run-failed";
43
+ readonly reason: string;
44
+ };
45
+ export declare function readKnownTests(issues: readonly TrackerIssue[]): KnownTests;
46
+ export declare function describeUnnamedFailure(failure: FailedTest): string;
47
+ export declare function findKnownTest(tests: readonly KnownTest[], failure: FailedTest): KnownTest | null;
48
+ export declare function classifyFailures(failures: readonly FailedTest[], knownTests: readonly KnownTest[] | null): FailureClassification;
49
+ export declare function describeRunOutcome(outcome: RunOutcome, knownFailuresAllowed: boolean): RunStatus;
@@ -0,0 +1 @@
1
+ import{readFactsBlock as o}from"./facts.js";import{redactRunResultText as u}from"../../tracker/redaction.js";export const COMMIT_STATUS_DESCRIPTION_MAX_LENGTH=140;export function readKnownTests(n){const s=[],i=[];for(const e of n){const t=o(e.body);if(t.status==="ok"){const{file:r,title:l}=t.facts.test;s.push({file:r,issue:e.number,title:l??null,url:e.url})}else{const r=t.status==="missing"?"the issue has no facts block":t.reason;i.push({number:e.number,reason:r,url:e.url})}}return{tests:s,unreadable:i}}export function describeUnnamedFailure(n){return n.script===!0?"the script failed as a whole":"failed without naming a test"}export function findKnownTest(n,s){const i=n.filter(t=>t.file===s.file),e=i.find(t=>t.title===null)??null;return s.title===null?s.script===!0?e:null:i.find(t=>t.title===s.title)??e}export function classifyFailures(n,s){const i=[],e=[];for(const t of n){const r=s===null?null:findKnownTest(s,t);r===null?e.push(t):i.push({failure:t,issue:r})}return{known:i,new:e}}export function describeRunOutcome(n,s){if(n.kind==="passed")return{description:`${n.passedFiles===1?"1 file":`${String(n.passedFiles)} files`} passed`,state:"success"};if(n.kind==="run-failed"){const t=u(n.reason);return{description:c(`run failed: ${t}`),state:"failure"}}const i=n.classification.known.length,e=n.classification.new.length;return{description:`${String(i+e)} failed: ${String(e)} new, ${String(i)} known`,state:e===0&&s?"success":"failure"}}function c(n){return n.length<=COMMIT_STATUS_DESCRIPTION_MAX_LENGTH?n:`${n.slice(0,COMMIT_STATUS_DESCRIPTION_MAX_LENGTH-3)}...`}
@@ -0,0 +1,14 @@
1
+ import { type BugDraft } from "./draft.js";
2
+ import { type CheckoutOptions, type UnreadableDraft } from "./store.js";
3
+ export interface RecordBugOptions extends CheckoutOptions {
4
+ readonly at?: Date;
5
+ readonly file: string;
6
+ }
7
+ export interface RecordBugResult {
8
+ readonly draft: BugDraft;
9
+ readonly path: string;
10
+ readonly redactedFields: readonly string[];
11
+ readonly replaced: boolean;
12
+ readonly unreadableDrafts: readonly UnreadableDraft[];
13
+ }
14
+ export declare function recordBug(options: RecordBugOptions): Promise<RecordBugResult>;
@@ -0,0 +1,2 @@
1
+ import s from"node:fs";import h from"node:path";import{text as m}from"node:stream/consumers";import{findActiveCampaign as w,readCampaigns as g}from"../campaigns/store.js";import{BugRecordError as y,createBugDraft as b,namesSameTest as D,readBugReport as k}from"./draft.js";import{bugDraftFile as E,displayPath as p,displayUnreadableDrafts as $,readStoredDrafts as v,resolveBugCheckout as x,writeJsonFile as S}from"./store.js";export async function recordBug(o){const r=await x(o),{redactedFields:a,report:e}=T(await F(r.cwd,o.file),r.productPath);for(const[t,l]of[["test.file",e.test.file],["fault.path",e.fault.path]])if(s.statSync(h.join(r.repoRoot,l),{throwIfNoEntry:!1})?.isFile()!==!0)throw new Error(`${t} resolves to ${l} from the repository root, and no file is there. Write the path relative to the product directory.`);const n=b(e,{campaign:w(g(r.productDir))?.id??null,productPath:r.productPath,recordedAt:o.at??new Date,recordedCommit:r.headCommit}),d=v(r.productDir),c=d.drafts.filter(t=>D(t.draft,n)),f=c.find(t=>t.draft.filed!==void 0)?.draft.filed;if(f!==void 0)throw new Error(`This test already has a filed bug: ${f.url}. Testkit keeps one bug for each test.`);const i=E(r.productDir,n.id),u=d.unreadable.find(t=>t.file===i);if(u!==void 0)throw new Error(`${p(r,i)} exists and Testkit cannot read it: ${u.problem}. Fix or delete the file, then run this again.`);S(i,n);for(const t of c)t.file!==i&&s.rmSync(t.file,{force:!0});return{draft:n,path:p(r,i),redactedFields:a,replaced:c.length>0,unreadableDrafts:$(r,d.unreadable)}}async function F(o,r){if(r==="-")return{source:"standard input",text:await m(process.stdin)};try{return{source:r,text:s.readFileSync(h.resolve(o,r),"utf8")}}catch(a){throw new Error(`Testkit cannot read the bug record file ${r}.`,{cause:a})}}function T(o,r){let a;try{a=JSON.parse(o.text)}catch(e){throw new Error(`The bug record in ${o.source} is not valid JSON.`,{cause:e})}try{return k(a,r)}catch(e){throw e instanceof y?new Error(`${e.message}
2
+ Run \`testkit bug guide\` to see the record format.`,{cause:e}):e}}
@@ -0,0 +1,51 @@
1
+ import type { FailedTest } from "./known-failures.js";
2
+ export interface RunFailureDetailReading {
3
+ readonly actual?: unknown;
4
+ readonly count?: unknown;
5
+ readonly key?: string;
6
+ readonly kind?: string;
7
+ readonly labelled?: unknown;
8
+ readonly title: string;
9
+ }
10
+ export interface RunFileReading {
11
+ readonly blockedByRuntime?: boolean;
12
+ readonly failureDetails?: readonly RunFailureDetailReading[];
13
+ readonly infraFailed?: boolean;
14
+ readonly path: string;
15
+ readonly status: string;
16
+ }
17
+ export interface RunServiceReading {
18
+ readonly errors?: readonly string[];
19
+ readonly suites: readonly {
20
+ readonly files?: readonly RunFileReading[];
21
+ }[];
22
+ }
23
+ export interface RunDocumentReading {
24
+ readonly archive?: {
25
+ readonly cleanup?: {
26
+ readonly status?: unknown;
27
+ } | null;
28
+ readonly publication?: {
29
+ readonly required?: unknown;
30
+ readonly status?: unknown;
31
+ } | null;
32
+ };
33
+ readonly finalizationFailure?: {
34
+ readonly message?: string;
35
+ };
36
+ readonly run: {
37
+ readonly status?: unknown;
38
+ };
39
+ readonly services: readonly RunServiceReading[];
40
+ }
41
+ export type RunReading = {
42
+ readonly failures: readonly FailedTest[];
43
+ readonly kind: "tests-failed";
44
+ } | {
45
+ readonly kind: "passed";
46
+ readonly passedFiles: number;
47
+ } | {
48
+ readonly kind: "run-failed";
49
+ readonly reason: string;
50
+ };
51
+ export declare function readRun(document: RunDocumentReading, productPath: string): RunReading;
@@ -0,0 +1 @@
1
+ const h=new Set(["engine-failure","playwright-other-file-spec","runtime-exception","vitest-error"]),k=1,c=99,E=0;export function readRun(e,i){if(e.finalizationFailure!==void 0)return o(e.finalizationFailure.message??"Testkit could not finish the run");if(e.run.status==="interrupted")return o("the run was interrupted");const{cleanup:t,publication:u}=e.archive??{};if(u?.status==="failed"&&u.required===!0)return o("Testkit could not publish the run's results");if(t?.status==="failed")return o("Testkit could not clean up after the run");const a=e.services.flatMap(n=>n.errors??[]).at(0);if(a!==void 0)return o(a);const r=e.services.flatMap(n=>n.suites.flatMap(p=>p.files??[]));if(r.some(n=>n.infraFailed===!0||n.blockedByRuntime===!0))return o("test infrastructure failure");if(r.some(n=>n.status==="not_run"))return o("one or more required test files did not run");if(r.some(n=>n.status==="skipped"))return o("one or more selected test files were skipped");const l=r.find(n=>n.status!=="passed"&&n.status!=="failed");if(l!==void 0)return o(`${l.path} ended as ${l.status}`);const s=r.filter(n=>n.status==="failed");return s.length>0?{failures:s.flatMap(n=>y(n,i)),kind:"tests-failed"}:e.run.status!=="passed"?o(r.length===0?"no runnable test files executed":"the run did not pass"):{kind:"passed",passedFiles:r.length}}function y(e,i){const t=i===""?e.path:`${i}/${e.path}`,u=e.failureDetails??[],a=u.filter(s=>!h.has(s.kind??"")),r=[...new Set(a.map(_))].map(s=>({file:t,title:s})),l=u.filter(s=>s.kind==="engine-failure"||s.kind==="vitest-error").map(s=>s.kind==="vitest-error"?"unexplained":T(s,a));return l.includes("unexplained")||r.length===0&&!l.includes("script")?[...r,{file:t,title:null}]:l.includes("script")?[...r,{file:t,script:!0,title:null}]:r}function _(e){const i=e.key?.trim()??"";return i===""?e.title:e.kind==="playwright-spec"||e.labelled===!1?i:e.title}function T(e,i){const t=f(e.actual)&&f(e.actual.details)?e.actual.details:null;if(t===null)return"unexplained";if(e.key==="K6_RUNTIME_EXCEPTION")return t.phase==="exec"&&t.summaryRead===!0&&t.exitSignal===void 0&&(t.exitCode===E||t.exitCode===c)?"script":"unexplained";if(e.key==="PLAYWRIGHT_SPECS_FAILED"){const u=d(i.filter(s=>s.kind==="playwright-spec")),a=f(t.stats)?t.stats:{},r=t.failOnFlakyTests!==!1&&a.flaky!==0;return a.unexpected===u&&t.reportErrors===0&&t.attachmentFailures===0&&t.evidenceErrors===0&&t.runStatus==="failed"&&t.exitCode===k&&typeof t.failOnFlakyTests=="boolean"&&!r?"restated":"unexplained"}if(e.key==="K6_THRESHOLDS_FAILED"){if(t.exitCode!==c)return"unexplained";const u=t.thresholds;if(!Array.isArray(u))return"unexplained";const a=u;return a.length>0&&a.every(l=>typeof l=="string"&&l.startsWith("checks("))&&i.length>0&&t.failedChecks===d(i)?"restated":"script"}return"unexplained"}function d(e){return e.reduce((i,t)=>i+(typeof t.count=="number"?t.count:1),0)}function o(e){return{kind:"run-failed",reason:e}}function f(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}
@@ -0,0 +1,34 @@
1
+ import { type BugDraft } from "./draft.js";
2
+ export interface CheckoutOptions {
3
+ readonly cwd?: string | undefined;
4
+ readonly dir?: string | undefined;
5
+ readonly env?: Readonly<NodeJS.ProcessEnv> | undefined;
6
+ }
7
+ export interface BugCheckout {
8
+ readonly cwd: string;
9
+ readonly headCommit: string;
10
+ readonly productDir: string;
11
+ readonly productPath: string;
12
+ readonly pullRequest: number | null;
13
+ readonly pullRequestHeadCommit: string | null;
14
+ readonly repoRoot: string;
15
+ }
16
+ export interface StoredDraft {
17
+ readonly draft: BugDraft;
18
+ readonly file: string;
19
+ }
20
+ export interface UnreadableDraft {
21
+ readonly file: string;
22
+ readonly problem: string;
23
+ }
24
+ export interface StoredDrafts {
25
+ readonly drafts: readonly StoredDraft[];
26
+ readonly unreadable: readonly UnreadableDraft[];
27
+ }
28
+ export declare function resolveBugCheckout(options: CheckoutOptions): Promise<BugCheckout>;
29
+ export declare function displayPath(checkout: Pick<BugCheckout, "cwd">, file: string): string;
30
+ export declare function displayUnreadableDrafts(checkout: Pick<BugCheckout, "cwd">, unreadable: readonly UnreadableDraft[]): readonly UnreadableDraft[];
31
+ export declare function bugDraftFile(productDir: string, id: string): string;
32
+ export declare function readStoredDrafts(productDir: string): StoredDrafts;
33
+ export declare function writeTextFile(file: string, text: string): void;
34
+ export declare function writeJsonFile(file: string, value: unknown): void;
@@ -0,0 +1,2 @@
1
+ import a from"node:fs";import n from"node:path";import{writeTextFileAtomicWithRetry as u}from"../../process/files/json-file.js";import{resolveProductDir as m}from"../../process/files/product-dir.js";import{collectGitRepoState as p}from"../../repo/state.js";import{BugRecordError as f,readBugDraft as d}from"./draft.js";export async function resolveBugCheckout(r){const t=r.cwd??process.cwd(),e=m(t,r.dir),o=await p(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=n.relative(o.repoRoot,a.realpathSync.native(e)).split(n.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 n.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 n.join(r,".testkit","bugs",`${t}.json`)}export function readStoredDrafts(r){const t=n.join(r,".testkit","bugs"),e=[],o=[];if(!a.existsSync(t))return{drafts:e,unreadable:o};const l=a.readdirSync(t,{withFileTypes:!0}).filter(i=>i.isFile()&&i.name.endsWith(".json")&&!i.name.startsWith(".")).map(i=>i.name);for(const i of l.sort()){const c=n.join(t,i);try{const s=JSON.parse(a.readFileSync(c,"utf8"));e.push({draft:d(s),file:c})}catch(s){if(s instanceof f)o.push({file:c,problem:h(s.problems)});else if(s instanceof SyntaxError)o.push({file:c,problem:"it is not valid JSON"});else throw s}}return{drafts:e,unreadable:o}}function h(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){a.mkdirSync(n.dirname(r),{recursive:!0}),u(r,t)}export function writeJsonFile(r,t){writeTextFile(r,`${JSON.stringify(t,null,2)}
2
+ `)}
@@ -0,0 +1,7 @@
1
+ import type { AreaMapCoverage } from "../bugs/areas.js";
2
+ import type { Campaign } from "./campaign.js";
3
+ export declare function formatShare(share: number): string;
4
+ export declare function formatCount(count: number, noun: string): string;
5
+ export declare function campaignSummaryPath(id: string): string;
6
+ export declare function renderCampaignBrief(campaign: Campaign, areas: AreaMapCoverage, productPath: string): string;
7
+ export declare function renderAreaMapReport(areas: AreaMapCoverage): string;
@@ -0,0 +1,10 @@
1
+ import{BUG_GUIDE as s,renderBugGuide as a}from"../bugs/guide.js";const o=50;export function formatShare(e){return`${(e*100).toFixed(1)}%`}export function formatCount(e,n){return`${String(e)} ${n}${e===1?"":"s"}`}export function campaignSummaryPath(e){return`.testkit/campaigns/${e}/summary.md`}export function renderCampaignBrief(e,n,t){return[`# Testkit campaign ${e.id}`,`Target: ${e.target}`,renderAreaMapReport(n),"## How to work",["- Write Testkit tests for the target.","- A failing test is a bug only when an authority says what the product should do: documentation, a specification, an API contract, or the code's own stated intent.","- Never skip a failing test, and never assert the broken behaviour to make a test pass.","- Keep credentials, tokens, and passwords out of records and tests."].join(`
2
+ `),"## For each confirmed bug",["1. Write the bug record as a JSON file. The sections below give its format.","2. Run `testkit bug record <file>`. It prints the draft id.","3. Commit and push the test.","4. Run `testkit bug file <id>`."].join(`
3
+ `),...t===""?[]:[`The product directory is \`${t}\`. Run these commands there, or pass \`--dir\`.`],a(s),"## How to end",`Run \`testkit campaign finish\`. It writes the summary for the pull request that adds the tests to \`${campaignSummaryPath(e.id)}\` and prints it.`].join(`
4
+
5
+ `)}export function renderAreaMapReport(e){const n=["## Area map"];return e.areas.length===0?n.push(`\`testkit.config.ts\` has no \`areas\`. The repository has ${formatCount(e.unclaimed.length,"source file")}, and none belongs to an area.`):(n.push([...e.areas.map(t=>`- \`${t.id}\`, ${t.name}: ${formatCount(t.files,"file")}, ${formatShare(t.share)} of source bytes`),`- Unclaimed: ${formatCount(e.unclaimed.length,"file")}, ${formatShare(e.unclaimedShare)} of source bytes`,...e.doubleClaims.length===0?[]:[`- Claimed by more than one area: ${formatCount(e.doubleClaims.length,"file")}, ${formatShare(e.doubleClaimedShare)} of source bytes`]].join(`
6
+ `)),e.unclaimed.length>0&&n.push("Unclaimed source files:",i(e.unclaimed.map(t=>`\`${t.path}\``))),e.doubleClaims.length>0&&n.push("Source files that more than one area claims:",i(e.doubleClaims.map(t=>`\`${t.path}\`: ${t.areas.map(r=>`\`${r}\``).join(", ")}`)))),n.push("Before you hunt, write or refresh `areas` in `testkit.config.ts` so that every source file belongs to exactly one area. Each path is a glob from the repository root.",["```ts","areas: {",' checkout: { name: "Checkout", paths: ["src/checkout/**"] },',"},","```"].join(`
7
+ `)),n.join(`
8
+
9
+ `)}function i(e){const n=e.length-o;return[...e.slice(0,o).map(t=>`- ${t}`),...n>0?[`- and ${String(n)} more`]:[]].join(`
10
+ `)}
@@ -0,0 +1,14 @@
1
+ export declare const CAMPAIGN_SCHEMA_VERSION = 1;
2
+ export declare const CAMPAIGN_TARGET_MAX_LENGTH = 200;
3
+ export interface Campaign {
4
+ readonly schemaVersion: typeof CAMPAIGN_SCHEMA_VERSION;
5
+ readonly id: string;
6
+ readonly target: string;
7
+ readonly startedAt: string;
8
+ readonly startCommit: string;
9
+ readonly finishedAt?: string;
10
+ }
11
+ export declare function readCampaignTarget(value: string): string;
12
+ export declare function readCampaignId(value: string): string;
13
+ export declare function defaultCampaignId(target: string, startedAt: Date): string;
14
+ export declare function readCampaign(value: unknown): Campaign;
@@ -0,0 +1 @@
1
+ import{redactSensitiveText as g}from"../../kernel/redaction.js";import{isUnknownRecord as p}from"../../kernel/unknown-values.js";import{CAMPAIGN_ID_MAX_LENGTH as i,isCampaignId as o}from"../bugs/draft.js";import{isCommitId as f}from"../bugs/facts.js";export const CAMPAIGN_SCHEMA_VERSION=1,CAMPAIGN_TARGET_MAX_LENGTH=200;const d=11;export function readCampaignTarget(t){const r=g(t.trim());if(r.length===0||r.length>CAMPAIGN_TARGET_MAX_LENGTH||/[\r\n]/u.test(r))throw new Error(`--target must be one line of 1 to ${String(CAMPAIGN_TARGET_MAX_LENGTH)} characters.`);return r}export function readCampaignId(t){if(o(t))return t;throw new Error(`--id must be lowercase letters, digits, and single hyphens, at most ${String(i)} characters. Got ${JSON.stringify(t)}.`)}export function defaultCampaignId(t,r){return`${t.normalize("NFKD").replace(/\p{M}+/gu,"").toLowerCase().replace(/[^a-z0-9]+/gu,"-").replace(/^-+/u,"").slice(0,i-d).replace(/-+$/u,"")||"campaign"}-${r.toISOString().slice(0,10)}`}export function readCampaign(t){if(!p(t))throw new Error("it must be a JSON object");if(t.schemaVersion!==CAMPAIGN_SCHEMA_VERSION)throw new Error(`unknown schemaVersion ${JSON.stringify(t.schemaVersion)}`);const r=[],e=(n,m,c)=>m(n)?n:(r.push(c),""),a={schemaVersion:CAMPAIGN_SCHEMA_VERSION,id:e(t.id,o,"id must be a campaign id"),target:e(t.target,h,"target must be one line of text"),startedAt:e(t.startedAt,s,"startedAt must be a timestamp"),startCommit:e(t.startCommit,f,"startCommit must be a full commit id"),...t.finishedAt===void 0?{}:{finishedAt:e(t.finishedAt,s,"finishedAt must be a timestamp")}};if(r.length>0)throw new Error(r.join(", "));return a}function h(t){return typeof t=="string"&&t.trim().length>0&&!/[\r\n]/u.test(t)}function s(t){return typeof t=="string"&&!Number.isNaN(Date.parse(t))}
@@ -0,0 +1,38 @@
1
+ import { type AreaMapCoverage } from "../bugs/areas.js";
2
+ import type { BugDraft } from "../bugs/draft.js";
3
+ import { type CheckoutOptions, type UnreadableDraft } from "../bugs/store.js";
4
+ import { type Campaign } from "./campaign.js";
5
+ export interface CampaignOptions extends CheckoutOptions {
6
+ readonly at?: Date;
7
+ }
8
+ export interface StartCampaignOptions extends CampaignOptions {
9
+ readonly id?: string;
10
+ readonly target: string;
11
+ }
12
+ export interface CampaignStart {
13
+ readonly areas: AreaMapCoverage;
14
+ readonly brief: string;
15
+ readonly campaign: Campaign;
16
+ }
17
+ export interface CampaignStatus {
18
+ readonly active: Campaign | null;
19
+ readonly areas: {
20
+ readonly areaCount: number;
21
+ readonly doubleClaimCount: number;
22
+ readonly unclaimedShare: number;
23
+ };
24
+ readonly drafts: readonly BugDraft[];
25
+ readonly lastFinished: Campaign | null;
26
+ readonly unreadableDrafts: readonly UnreadableDraft[];
27
+ }
28
+ export interface CampaignFinish {
29
+ readonly campaign: Campaign;
30
+ readonly filed: readonly BugDraft[];
31
+ readonly headCommit: string;
32
+ readonly summary: string;
33
+ readonly summaryPath: string;
34
+ readonly unfiled: readonly BugDraft[];
35
+ }
36
+ export declare function startCampaign(options: StartCampaignOptions): Promise<CampaignStart>;
37
+ export declare function readCampaignStatus(options: CheckoutOptions): Promise<CampaignStatus>;
38
+ export declare function finishCampaign(options: CampaignOptions): Promise<CampaignFinish>;
@@ -0,0 +1,2 @@
1
+ import x from"node:fs";import c from"node:path";import{readAreaMap as A,readBugPolicy as j}from"../../config/loader/bugs.js";import{loadTestkitConfig as w}from"../../config/loader/index.js";import{listFilesChangedSince as P}from"../../repo/history.js";import{listCommitFiles as I}from"../../repo/tree-listing.js";import{measureAreaMap as R}from"../bugs/areas.js";import{displayPath as D,displayUnreadableDrafts as y,readStoredDrafts as S,resolveBugCheckout as u,writeJsonFile as k,writeTextFile as B}from"../bugs/store.js";import{renderCampaignBrief as E}from"./brief.js";import{defaultCampaignId as T,readCampaignId as $,readCampaignTarget as M}from"./campaign.js";import{campaignDir as F,findActiveCampaign as f,findLastFinishedCampaign as O,readCampaigns as p}from"./store.js";import{renderCampaignSummary as J}from"./summary.js";export async function startCampaign(a){const t=a.at??new Date,r=M(a.target),i=a.id===void 0?T(r,t):$(a.id),e=await u(a),n=f(p(e.productDir));if(n!==null)throw new Error(`Campaign ${n.id} is active in this checkout. Run \`testkit campaign finish\` before you start another.`);const s=F(e.productDir,i);if(x.existsSync(s))throw new Error(`${D(e,s)} already exists. Pass --id to give this campaign another id.`);const m=await v(e),d={schemaVersion:1,id:i,target:r,startedAt:t.toISOString(),startCommit:e.headCommit};return k(c.join(s,"campaign.json"),d),{areas:m,brief:E(d,m,e.productPath),campaign:d}}export async function readCampaignStatus(a){const t=await u(a),r=p(t.productDir),i=f(r),e=S(t.productDir),n=await v(t);return{active:i,areas:{areaCount:n.areas.length,doubleClaimCount:n.doubleClaims.length,unclaimedShare:n.unclaimedShare},drafts:i===null?[]:b(e.drafts,i.id),lastFinished:O(r),unreadableDrafts:y(t,e.unreadable)}}export async function finishCampaign(a){const t=await u(a),r=f(p(t.productDir));if(r===null)throw new Error("No campaign is active in this checkout. Run `testkit campaign start --target <text>` to start one.");const i={...r,finishedAt:(a.at??new Date).toISOString()},e=S(t.productDir),n=b(e.drafts,i.id),s=n.filter(o=>o.filed!==void 0),m=n.filter(o=>o.filed===void 0),{config:d}=await w(t.productDir),l=await P(t.repoRoot,i.startCommit),g=J({campaign:i,filed:s,headCommit:t.headCommit,knownFailures:j(d).knownFailures,testFiles:l===null?null:l.filter(o=>o.endsWith(".testkit.ts")),unfiled:m,unreadableDrafts:y(t,e.unreadable).map(o=>o.file)}),h=F(t.productDir,i.id),C=c.join(h,"summary.md");return B(C,`${g}
2
+ `),k(c.join(h,"campaign.json"),i),{campaign:i,filed:s,headCommit:t.headCommit,summary:g,summaryPath:D(t,C),unfiled:m}}function b(a,t){return a.map(r=>r.draft).filter(r=>r.campaign===t)}async function v(a){const{config:t}=await w(a.productDir);return R(A(t),await I(a.repoRoot,a.headCommit))}
@@ -0,0 +1,5 @@
1
+ import { type Campaign } from "./campaign.js";
2
+ export declare function campaignDir(productDir: string, id: string): string;
3
+ export declare function readCampaigns(productDir: string): readonly Campaign[];
4
+ export declare function findActiveCampaign(campaigns: readonly Campaign[]): Campaign | null;
5
+ export declare function findLastFinishedCampaign(campaigns: readonly Campaign[]): Campaign | null;
@@ -0,0 +1 @@
1
+ import i from"node:fs";import a from"node:path";import{readCampaign as c}from"./campaign.js";export function campaignDir(n,t){return a.join(n,".testkit","campaigns",t)}export function readCampaigns(n){const t=a.join(n,".testkit","campaigns");if(!i.existsSync(t))return[];const r=[];for(const e of i.readdirSync(t,{withFileTypes:!0})){const o=a.join(t,e.name,"campaign.json");e.isDirectory()&&i.existsSync(o)&&r.push(f(o,e.name))}return r}export function findActiveCampaign(n){return s(n.filter(t=>t.finishedAt===void 0),t=>t.startedAt)}export function findLastFinishedCampaign(n){return s(n.filter(t=>t.finishedAt!==void 0),t=>t.finishedAt??"")}function s(n,t){return[...n].sort((r,e)=>Date.parse(t(e))-Date.parse(t(r))).at(0)??null}function f(n,t){try{const r=JSON.parse(i.readFileSync(n,"utf8")),e=c(r);if(e.id!==t)throw new Error("id does not match its directory");return e}catch(r){const e=r instanceof SyntaxError?"it is not valid JSON":r instanceof Error?r.message:"";throw new Error(`Testkit cannot read ${n}: ${e}. Fix or delete the file, then run this again.`,{cause:r})}}
@@ -0,0 +1,13 @@
1
+ import type { KnownFailuresPolicy } from "../../config/loader/bugs.js";
2
+ import type { BugDraft } from "../bugs/draft.js";
3
+ import type { Campaign } from "./campaign.js";
4
+ export interface CampaignSummaryInput {
5
+ readonly campaign: Campaign;
6
+ readonly filed: readonly BugDraft[];
7
+ readonly headCommit: string;
8
+ readonly knownFailures: KnownFailuresPolicy;
9
+ readonly testFiles: readonly string[] | null;
10
+ readonly unfiled: readonly BugDraft[];
11
+ readonly unreadableDrafts: readonly string[];
12
+ }
13
+ export declare function renderCampaignSummary(input: CampaignSummaryInput): string;
@@ -0,0 +1,8 @@
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
+ `),"### 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 <id>` for each one.",o.map(t=>`- \`${t.id}\`: ${t.title}, ${t.severity}, \`${t.test.file}\``).join(`
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
+ `)],"### 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
+ `),...i.length===0?[]:["### Merge gate",a(e.knownFailures)]].join(`
7
+
8
+ `)}function a(e){return e==="allow"?'This repository sets `bugs: { knownFailures: "allow" }`, so the regression tests for the bugs above do not fail the run while their issues stay open. Closing an issue makes its test count again.':'The regression tests for the bugs above fail the run. A repository that requires green checks cannot merge this pull request until the bugs are fixed or the team sets `bugs: { knownFailures: "allow" }` in `testkit.config.ts`.'}function l(e){return e.filed===void 0?"":`[#${String(e.filed.number)}](${e.filed.url})`}function r(e){return e.slice(0,7)}
@@ -60,3 +60,4 @@ export declare function mapEngineOutcomes(tasks: readonly ScheduledTask[], resul
60
60
  export declare function buildEndpointReadinessOutcomes(tasks: readonly ScheduledTask[], executableTask: EngineExecutableTask, readiness: Extract<EndpointReadinessResult, {
61
61
  ok: false;
62
62
  }>, cancelled: boolean, startedAt: number, finishedAt: number): EngineTaskExecutionOutcome[];
63
+ export declare function mapEngineOutcome(outcome: Readonly<EngineTaskOutcome>, task: ScheduledTask, artifacts: PersistedArtifactReference[], executableTask: EngineExecutableTask, batchIndex: number): EngineTaskExecutionOutcome;
@@ -1 +1 @@
1
- import U from"node:fs";import F from"node:path";import{EngineTaskDeadlineExceededError as Me}from"../../engines/api/index.js";import"../../domain/identifiers.js";import"../../domain/test-outcomes.js";import{resolveEngineProfile as Ie}from"../../engines/catalog.js";import{BUILT_IN_ENGINE_ORCHESTRATION as be}from"./engine-orchestration.js";import{persistTaskArtifacts as Oe}from"../../evidence/index.js";import{createEvidenceLedger as se,persistTrackedEngineEvidence as ae,requireTaskArtifacts as Ne,requireTaskEvidenceLedger as _e}from"./engine-evidence-persistence.js";import{applyConsumerNodeSetupCleanupFailures as ce,cleanupPreparedNodeSetups as ue}from"./engine-execution-cleanup.js";import{buildBridgeError as Re,EngineExecutionBridgeError as Fe,executeManagedEngineRequest as Ae,infraBridgeDisposition as q,managerFailureBridgeDisposition as Ce,productBridgeDisposition as z,reporterBridgeDisposition as G,resolveBridgeDisposition as de,runtimeNodeSetupBridgeDisposition as Be}from"./engine-execution-failures.js";import{captureEngineReporterCallback as fe,DEFAULT_ENGINE_REPORTER_CALLBACK_TIMEOUT_MS as $e,invokeEngineReporterCallback as le}from"./engine-execution-reporting.js";import{createEngineExecutionObserver as xe}from"./engine-execution-observer.js";export{EngineExecutionBridgeError}from"./engine-execution-failures.js";import{waitForEndpointReadinessPreflight as Pe}from"./endpoint-readiness.js";import{collectCheckDetailsFromRuntimeArtifacts as Le,collectFailureDetailsFromRuntimeArtifacts as je}from"./failure-details.js";import{buildRuntimeFixtureManifest as Je}from"./runtime-fixtures.js";import{MAX_RUNTIME_NODE_SETUP_CLEANUP_TIMEOUT_MS as Ue,runRuntimeNodeSetup as qe}from"./setup/runtime-node-setup.js";export async function executeEngineTask(e,t,n,r,s=null){const i=t,u={signal:r.signal,timeoutMs:$e},m="batchTasks"in n?n.batchTasks:[n],w=m[0],S=t.services.get(w.targetName);if(!S)throw new Error(`Runtime instance is missing target config "${w.targetName}"`);const d=S.config;we(d.productDir,"product directory"),we(i.leaseDir,"runtime lease directory");const v=Ie(w.testType,w.engineId),M=be.resolve(v),O=Xe(d.testkit.execution.fileTimeoutSeconds),ke=Date.now(),V=pe(ke,O),C=Ge(t),N=Object.hasOwn(C,w.serviceName)?C[w.serviceName]:null;if(M.endpointReadinessOwner==="orchestrator-http"){if(N===null)throw new Error(`Service "${d.name}" requires local.baseUrl for ${v.id} suites`);const B=Date.now(),y=H(r.signal,V);let D;try{D=await Pe({baseUrl:N,serviceName:d.name,lifecycle:r,signal:y.signal,options:Ye(Qe(V))})}finally{y.dispose()}if(!D.ok){const I=r.isStopRequested()||r.signal.aborted,k=Date.now(),c=buildEndpointReadinessOutcomes(m,n,D,I,B,k),a=[];let g=null;for(const E of c){const l=a.length;await fe(a,"taskStarted",()=>s?.taskStarted?.(E.task,d),u),g===null&&a.length>l&&(g=String(E.task.id)),await fe(a,"taskFinished",()=>s?.taskFinished?.(E.task,E),u),g===null&&a.length>l&&(g=String(E.task.id))}if(a.length>0){const E=a.length===1?a[0]:new AggregateError(a,"Engine readiness reporting failed");throw new Fe(`${D.reason}; ${a.map(l=>l.message).join("; ")}`,E,de(a,[],!1,I?{classification:void 0,notifyRuntimeError:!1,runtimeImpact:"none",status:"cancelled"}:G("fail-graph")),new Map(m.map(l=>[String(l.id),[]])),new Map(c.map(l=>[String(l.task.id),l])),g)}return{outcomes:c}}}return e.withEngine(v.engineId,r.signal,async B=>{const y=He(i,d.productDir);U.mkdirSync(y.temporaryDirectory,{recursive:!0}),U.mkdirSync(y.artifactDirectory,{recursive:!0});const D=new Map(m.map(p=>[String(p.id),[]])),I=new Map(m.map(p=>[String(p.id),se()])),k=new Map,c=[];let a=z(),g=!1;const E=[],l=[],W=[];let X=null;const Q=new Set,$=async(p,o)=>{const f=String(p.id);Q.has(f)||(Q.add(f),await le("taskFinished",()=>s?.taskFinished?.(p,o),u))};try{for(const[p,o]of m.entries()){a=G();const f=String(o.id);X=f,await le("taskStarted",()=>s?.taskStarted?.(o,d),u),a=z();const Z=M.buildRuntimeEnvironment({baseUrl:N,database:S.database,targetConfig:d,task:o}),_=pe(Date.now(),O),ee=H(r.signal,_);let b;try{b=await qe({cleanupTimeoutMs:Math.min(O,Ue),deadlineMs:_,targetConfig:d,task:o,lease:i,runtimeEnv:Z,signal:ee.signal})}catch(h){throw a=Be(h),h}finally{ee.dispose()}E.push({cleanup:b.cleanup,taskId:f}),a=q();for(const h of b.artifacts){const T=Oe(d.productDir,o,[h]);D.get(String(o.id))?.push(...T)}a=z();const R={baseUrl:N,lease:i,profile:v,runtimeEnvironment:Z,targetConfig:d,task:o},te=M.resolveConfiguration(R),ye={...te===void 0?{}:{configuration:te},environment:M.buildEnvironment(R),fixtures:nt(Je(d.productDir,o.file),`fixture manifest for ${o.file}`),setupData:he(b.setupData,`node setup data for ${o.file}`),supportModules:M.resolveSupportModules(R)},De=ze(R);a=q();const ve=c.length,ne=se(),Te=xe({artifactReferencesByTaskId:D,completedOutcomesByTaskId:k,errors:c,executableTask:n,invocationEvidenceLedger:ne,productDir:d.productDir,taskBatchIndex:p,taskEvidenceLedgers:I,tasks:[o],tasksById:new Map([[f,o]]),workspaceArtifactDirectory:y.artifactDirectory},ge),x=H(r.signal,_);try{const h={invocationId:Ve(r,i,o),observer:Te,profile:v,signal:x.signal,target:{endpoints:C,inputs:Ke()},tasks:[{deadlineMs:_,id:f,file:o.file,serviceName:o.serviceName,suiteName:o.suiteName,testType:o.testType,timeoutMs:O,inputs:ye,orchestration:De}],workspace:y};let T;try{T=await Ae(B,h)}catch(oe){throw a=Ce(r.signal,x.signal,oe),oe}const re=mapEngineOutcomes([o],T)[0],j=Ne(D,f),Se=_e(I,f);ae(re.outcome.evidence,{artifactRoot:y.artifactDirectory,errors:c,ledger:Se,mode:"reconcile",productDir:d.productDir,references:j,scope:"task",task:o});const J=ge(re.outcome,o,j,n,p);k.set(f,J),J.infraFailed&&J.blockedByRuntime&&(a=q("fail-graph"));const ie=et(T);tt(ie)&&ae(ie,{artifactRoot:y.artifactDirectory,errors:c,ledger:ne,mode:"reconcile",productDir:d.productDir,references:j,scope:"invocation",task:o})}finally{x.dispose()}c.length>ve&&(g=!0),E.pop();const P=await ue([{cleanup:b.cleanup,taskId:f}]);if(P.length>0){l.push(...P),ce(k,P);const h=k.get(f);if(h!==void 0)try{await $(o,h)}catch(T){c.push(A(T))}break}if(c.length>0)break;const L=k.get(f);if(L===void 0){c.push(new Error(`Engine task ${f} completed without a mapped outcome`)),g=!0;break}W.push(L),a=G();try{await $(o,L)}catch(h){c.push(A(h)),g=!0;break}}}catch(p){const o=c.length;c.push(A(p)),(o>0||a.classification==="infra")&&(g=!0)}const Y=await ue(E);l.push(...Y),ce(k,Y);for(const p of m){const o=k.get(String(p.id));if(o!==void 0)try{await $(p,o)}catch(f){c.push(A(f))}}if(c.length>0||l.length>0)throw Re(c,l,de(c,l,g,a),D,k,X);return{outcomes:W}})}function ze(e){const t=e.targetConfig.testkit.mode;if(t!==void 0&&(typeof t!="string"||t.trim().length===0))throw new Error("Testkit execution mode must be a non-empty string");const n=e.targetConfig.runtimeId;return{lease:{directory:e.lease.leaseDir,id:String(e.lease.leaseId)},mode:t??"test",...n==null?{}:{runtimeId:String(n)}}}export function mapEngineOutcomes(e,t){const n=new Map(e.map(i=>[String(i.id),i])),r=new Map;for(const i of t.outcomes){const u=n.get(i.taskId);if(!u)throw new Error(`Engine manager returned an outcome for unknown task ${i.taskId}`);if(r.has(i.taskId))throw new Error(`Engine manager returned duplicate outcomes for task ${i.taskId}`);r.set(i.taskId,{task:u,outcome:i})}const s=e.map(i=>String(i.id)).filter(i=>!r.has(i));if(s.length>0)throw new Error(`Engine manager did not return outcomes for task(s): ${s.join(", ")}`);return e.map(i=>{const u=r.get(String(i.id));if(!u)throw new Error("Engine outcome mapping invariant failed");return u})}export function buildEndpointReadinessOutcomes(e,t,n,r,s,i){const u=Math.max(0,i-s);return e.map((m,w)=>({task:m,status:r?"cancelled":"not_run",error:null,reason:n.reason,infraFailed:!r,blockedByRuntime:!r,durationMs:w===0?u:0,startedAt:s,finishedAt:i,artifacts:[],failureDetails:r?[]:[...n.failureDetails],checkDetails:[],...Ee(t,m)}))}function Ge(e){return Object.freeze(Object.fromEntries([...e.services.values()].flatMap(({config:t,baseUrl:n})=>typeof n=="string"&&n.length>0?[[t.name,n]]:[])))}function He(e,t){const n=e.engineWorkspaceDirectory??F.join(e.leaseDir,"engine-execution");return{rootDirectory:t,...e.engineCacheDirectory===void 0?{}:{cacheDirectory:e.engineCacheDirectory},temporaryDirectory:F.join(n,"temporary"),artifactDirectory:F.join(n,"artifacts")}}function Ke(){return{environment:{},fixtures:{},supportModules:{}}}function Ve(e,t,n){return[e.runId,t.leaseId,We(n)].join(":")}function We(e){return"batchId"in e?e.batchId:String(e.id)}function Xe(e){const t=e*1e3;if(!Number.isSafeInteger(t)||t<=0)throw new Error("Engine task timeout must be a positive safe integer");return t}function pe(e,t){const n=e+t;if(!Number.isSafeInteger(n)||n<=e)throw new Error("Engine task deadline must be a positive safe integer");return n}function Qe(e){return Math.max(1,e-Date.now())}function Ye(e){const t=me(process.env.TESTKIT_HTTP_ENDPOINT_PREFLIGHT_TIMEOUT_MS,3e4),n=me(process.env.TESTKIT_HTTP_ENDPOINT_PREFLIGHT_ATTEMPT_TIMEOUT_MS,5e3);return{timeoutMs:Math.min(t,e),attemptTimeoutMs:Math.min(n,e)}}function me(e,t){const n=Number(e);return Number.isInteger(n)&&n>0?n:t}function H(e,t){const n=new AbortController;let r=null;const s=()=>{n.abort(e.reason)},i=()=>{const u=t-Date.now();if(u<=0){n.abort(new Me(t));return}r=setTimeout(i,Math.min(u,2147483647)),r.unref()};return e.aborted?s():(e.addEventListener("abort",s,{once:!0}),i()),{signal:n.signal,dispose(){r!==null&&clearTimeout(r),e.removeEventListener("abort",s)}}}function ge(e,t,n,r,s){const i=je(e.evidence.artifacts),u=i.some(v=>t.engineId==="node-vitest"&&v.kind==="vitest-test"),m=[...e.failure&&!u?[Ze(e.failure)]:[],...i],w=Le(e.evidence.artifacts),S=e.status==="not_run",d=S||e.failure?.classification==="infra";return{task:t,status:e.status,error:e.status==="failed"?e.failure?.message??"Engine task failed without a message":null,reason:e.status==="failed"||e.status==="passed"?null:e.failure?.message??`Engine task ${e.status}`,infraFailed:d,blockedByRuntime:S,durationMs:e.durationMs,startedAt:e.startedAtMs,finishedAt:e.finishedAtMs,artifacts:n,failureDetails:m,checkDetails:w,...Ee(r,t,s)}}function Ee(e,t,n){if(!("batchTasks"in e))return{};const r=n??e.batchTasks.findIndex(s=>s.id===t.id&&s.file===t.file);if(r<0)throw new Error(`Task ${String(t.id)} is missing from its engine batch`);return{runtimeBatch:{id:e.batchId,index:r,fileCount:e.batchTasks.length,files:e.batchTasks.map(s=>s.file)}}}function Ze(e){const t=e.code??"ENGINE_TASK_FAILED",n={code:t,details:e.details===void 0?null:rt(e.details)};return{kind:"engine-failure",key:t,title:t,...e.classification===void 0?{}:{classification:e.classification},count:1,message:e.message,actual:n,...e.stack===void 0?{}:{stack:e.stack}}}function et(e){return{artifacts:K(e.evidence.artifacts,e.outcomes.flatMap(t=>t.evidence.artifacts)),logs:K(e.evidence.logs,e.outcomes.flatMap(t=>t.evidence.logs)),metrics:K(e.evidence.metrics,e.outcomes.flatMap(t=>t.evidence.metrics)),...e.evidence.native===void 0?{}:{native:e.evidence.native}}}function K(e,t){const n=new Map;for(const r of t){const s=JSON.stringify(r);n.set(s,(n.get(s)??0)+1)}return e.filter(r=>{const s=JSON.stringify(r),i=n.get(s)??0;return i===0?!0:(n.set(s,i-1),!1)})}function tt(e){return e.artifacts.length>0||e.logs.length>0||e.metrics.length>0||e.native!==void 0}function nt(e,t){const n=he(e,t);if(n===null||Array.isArray(n)||typeof n!="object")throw new TypeError(`${t} must contain a JSON object`);return n}function he(e,t){try{const n=JSON.stringify(e);if(typeof n!="string")throw new TypeError(`${t} must be JSON-serializable`);return JSON.parse(n)}catch(n){throw new TypeError(`${t} must be JSON-serializable`,{cause:n})}}function rt(e){return JSON.parse(JSON.stringify(e))}function we(e,t){if(!F.isAbsolute(e))throw new Error(`Engine execution ${t} must be absolute`);if(!U.statSync(e).isDirectory())throw new Error(`Engine execution ${t} must be a directory`)}function A(e){return e instanceof Error?e:new Error(String(e))}
1
+ import U from"node:fs";import F from"node:path";import{EngineTaskDeadlineExceededError as Se}from"../../engines/api/index.js";import"../../domain/identifiers.js";import"../../domain/test-outcomes.js";import{resolveEngineProfile as Ie}from"../../engines/catalog.js";import{BUILT_IN_ENGINE_ORCHESTRATION as Me}from"./engine-orchestration.js";import{persistTaskArtifacts as be}from"../../evidence/index.js";import{createEvidenceLedger as se,persistTrackedEngineEvidence as ae,requireTaskArtifacts as Oe,requireTaskEvidenceLedger as Ne}from"./engine-evidence-persistence.js";import{applyConsumerNodeSetupCleanupFailures as ce,cleanupPreparedNodeSetups as ue}from"./engine-execution-cleanup.js";import{buildBridgeError as _e,EngineExecutionBridgeError as Re,executeManagedEngineRequest as Fe,infraBridgeDisposition as q,managerFailureBridgeDisposition as Ae,productBridgeDisposition as z,reporterBridgeDisposition as V,resolveBridgeDisposition as de,runtimeNodeSetupBridgeDisposition as Ce}from"./engine-execution-failures.js";import{captureEngineReporterCallback as fe,DEFAULT_ENGINE_REPORTER_CALLBACK_TIMEOUT_MS as Be,invokeEngineReporterCallback as le}from"./engine-execution-reporting.js";import{createEngineExecutionObserver as xe}from"./engine-execution-observer.js";export{EngineExecutionBridgeError}from"./engine-execution-failures.js";import{waitForEndpointReadinessPreflight as Pe}from"./endpoint-readiness.js";import{collectCheckDetailsFromRuntimeArtifacts as $e,collectFailureDetailsFromRuntimeArtifacts as Le}from"./failure-details.js";import{buildRuntimeFixtureManifest as je}from"./runtime-fixtures.js";import{MAX_RUNTIME_NODE_SETUP_CLEANUP_TIMEOUT_MS as Je,runRuntimeNodeSetup as Ue}from"./setup/runtime-node-setup.js";export async function executeEngineTask(e,t,n,r,s=null){const i=t,u={signal:r.signal,timeoutMs:Be},m="batchTasks"in n?n.batchTasks:[n],w=m[0],S=t.services.get(w.targetName);if(!S)throw new Error(`Runtime instance is missing target config "${w.targetName}"`);const d=S.config;he(d.productDir,"product directory"),he(i.leaseDir,"runtime lease directory");const T=Ie(w.testType,w.engineId),I=Me.resolve(T),O=We(d.testkit.execution.fileTimeoutSeconds),we=Date.now(),K=pe(we,O),C=ze(t),N=Object.hasOwn(C,w.serviceName)?C[w.serviceName]:null;if(I.endpointReadinessOwner==="orchestrator-http"){if(N===null)throw new Error(`Service "${d.name}" requires local.baseUrl for ${T.id} suites`);const B=Date.now(),D=G(r.signal,K);let y;try{y=await Pe({baseUrl:N,serviceName:d.name,lifecycle:r,signal:D.signal,options:Qe(Xe(K))})}finally{D.dispose()}if(!y.ok){const M=r.isStopRequested()||r.signal.aborted,k=Date.now(),c=buildEndpointReadinessOutcomes(m,n,y,M,B,k),a=[];let g=null;for(const E of c){const l=a.length;await fe(a,"taskStarted",()=>s?.taskStarted?.(E.task,d),u),g===null&&a.length>l&&(g=String(E.task.id)),await fe(a,"taskFinished",()=>s?.taskFinished?.(E.task,E),u),g===null&&a.length>l&&(g=String(E.task.id))}if(a.length>0){const E=a.length===1?a[0]:new AggregateError(a,"Engine readiness reporting failed");throw new Re(`${y.reason}; ${a.map(l=>l.message).join("; ")}`,E,de(a,[],!1,M?{classification:void 0,notifyRuntimeError:!1,runtimeImpact:"none",status:"cancelled"}:V("fail-graph")),new Map(m.map(l=>[String(l.id),[]])),new Map(c.map(l=>[String(l.task.id),l])),g)}return{outcomes:c}}}return e.withEngine(T.engineId,r.signal,async B=>{const D=Ve(i,d.productDir);U.mkdirSync(D.temporaryDirectory,{recursive:!0}),U.mkdirSync(D.artifactDirectory,{recursive:!0});const y=new Map(m.map(p=>[String(p.id),[]])),M=new Map(m.map(p=>[String(p.id),se()])),k=new Map,c=[];let a=z(),g=!1;const E=[],l=[],W=[];let X=null;const Q=new Set,x=async(p,o)=>{const f=String(p.id);Q.has(f)||(Q.add(f),await le("taskFinished",()=>s?.taskFinished?.(p,o),u))};try{for(const[p,o]of m.entries()){a=V();const f=String(o.id);X=f,await le("taskStarted",()=>s?.taskStarted?.(o,d),u),a=z();const Z=I.buildRuntimeEnvironment({baseUrl:N,database:S.database,targetConfig:d,task:o}),_=pe(Date.now(),O),ee=G(r.signal,_);let b;try{b=await Ue({cleanupTimeoutMs:Math.min(O,Je),deadlineMs:_,targetConfig:d,task:o,lease:i,runtimeEnv:Z,signal:ee.signal})}catch(h){throw a=Ce(h),h}finally{ee.dispose()}E.push({cleanup:b.cleanup,taskId:f}),a=q();for(const h of b.artifacts){const v=be(d.productDir,o,[h]);y.get(String(o.id))?.push(...v)}a=z();const R={baseUrl:N,lease:i,profile:T,runtimeEnvironment:Z,targetConfig:d,task:o},te=I.resolveConfiguration(R),ke={...te===void 0?{}:{configuration:te},environment:I.buildEnvironment(R),fixtures:nt(je(d.productDir,o.file),`fixture manifest for ${o.file}`),setupData:Ee(b.setupData,`node setup data for ${o.file}`),supportModules:I.resolveSupportModules(R)},De=qe(R);a=q();const ye=c.length,ne=se(),Te=xe({artifactReferencesByTaskId:y,completedOutcomesByTaskId:k,errors:c,executableTask:n,invocationEvidenceLedger:ne,productDir:d.productDir,taskBatchIndex:p,taskEvidenceLedgers:M,tasks:[o],tasksById:new Map([[f,o]]),workspaceArtifactDirectory:D.artifactDirectory},mapEngineOutcome),P=G(r.signal,_);try{const h={invocationId:He(r,i,o),observer:Te,profile:T,signal:P.signal,target:{endpoints:C,inputs:Ge()},tasks:[{deadlineMs:_,id:f,file:o.file,serviceName:o.serviceName,suiteName:o.suiteName,testType:o.testType,timeoutMs:O,inputs:ke,orchestration:De}],workspace:D};let v;try{v=await Fe(B,h)}catch(oe){throw a=Ae(r.signal,P.signal,oe),oe}const re=mapEngineOutcomes([o],v)[0],j=Oe(y,f),ve=Ne(M,f);ae(re.outcome.evidence,{artifactRoot:D.artifactDirectory,errors:c,ledger:ve,mode:"reconcile",productDir:d.productDir,references:j,scope:"task",task:o});const J=mapEngineOutcome(re.outcome,o,j,n,p);k.set(f,J),J.infraFailed&&J.blockedByRuntime&&(a=q("fail-graph"));const ie=et(v);tt(ie)&&ae(ie,{artifactRoot:D.artifactDirectory,errors:c,ledger:ne,mode:"reconcile",productDir:d.productDir,references:j,scope:"invocation",task:o})}finally{P.dispose()}c.length>ye&&(g=!0),E.pop();const $=await ue([{cleanup:b.cleanup,taskId:f}]);if($.length>0){l.push(...$),ce(k,$);const h=k.get(f);if(h!==void 0)try{await x(o,h)}catch(v){c.push(A(v))}break}if(c.length>0)break;const L=k.get(f);if(L===void 0){c.push(new Error(`Engine task ${f} completed without a mapped outcome`)),g=!0;break}W.push(L),a=V();try{await x(o,L)}catch(h){c.push(A(h)),g=!0;break}}}catch(p){const o=c.length;c.push(A(p)),(o>0||a.classification==="infra")&&(g=!0)}const Y=await ue(E);l.push(...Y),ce(k,Y);for(const p of m){const o=k.get(String(p.id));if(o!==void 0)try{await x(p,o)}catch(f){c.push(A(f))}}if(c.length>0||l.length>0)throw _e(c,l,de(c,l,g,a),y,k,X);return{outcomes:W}})}function qe(e){const t=e.targetConfig.testkit.mode;if(t!==void 0&&(typeof t!="string"||t.trim().length===0))throw new Error("Testkit execution mode must be a non-empty string");const n=e.targetConfig.runtimeId;return{lease:{directory:e.lease.leaseDir,id:String(e.lease.leaseId)},mode:t??"test",...n==null?{}:{runtimeId:String(n)}}}export function mapEngineOutcomes(e,t){const n=new Map(e.map(i=>[String(i.id),i])),r=new Map;for(const i of t.outcomes){const u=n.get(i.taskId);if(!u)throw new Error(`Engine manager returned an outcome for unknown task ${i.taskId}`);if(r.has(i.taskId))throw new Error(`Engine manager returned duplicate outcomes for task ${i.taskId}`);r.set(i.taskId,{task:u,outcome:i})}const s=e.map(i=>String(i.id)).filter(i=>!r.has(i));if(s.length>0)throw new Error(`Engine manager did not return outcomes for task(s): ${s.join(", ")}`);return e.map(i=>{const u=r.get(String(i.id));if(!u)throw new Error("Engine outcome mapping invariant failed");return u})}export function buildEndpointReadinessOutcomes(e,t,n,r,s,i){const u=Math.max(0,i-s);return e.map((m,w)=>({task:m,status:r?"cancelled":"not_run",error:null,reason:n.reason,infraFailed:!r,blockedByRuntime:!r,durationMs:w===0?u:0,startedAt:s,finishedAt:i,artifacts:[],failureDetails:r?[]:[...n.failureDetails],checkDetails:[],...ge(t,m)}))}function ze(e){return Object.freeze(Object.fromEntries([...e.services.values()].flatMap(({config:t,baseUrl:n})=>typeof n=="string"&&n.length>0?[[t.name,n]]:[])))}function Ve(e,t){const n=e.engineWorkspaceDirectory??F.join(e.leaseDir,"engine-execution");return{rootDirectory:t,...e.engineCacheDirectory===void 0?{}:{cacheDirectory:e.engineCacheDirectory},temporaryDirectory:F.join(n,"temporary"),artifactDirectory:F.join(n,"artifacts")}}function Ge(){return{environment:{},fixtures:{},supportModules:{}}}function He(e,t,n){return[e.runId,t.leaseId,Ke(n)].join(":")}function Ke(e){return"batchId"in e?e.batchId:String(e.id)}function We(e){const t=e*1e3;if(!Number.isSafeInteger(t)||t<=0)throw new Error("Engine task timeout must be a positive safe integer");return t}function pe(e,t){const n=e+t;if(!Number.isSafeInteger(n)||n<=e)throw new Error("Engine task deadline must be a positive safe integer");return n}function Xe(e){return Math.max(1,e-Date.now())}function Qe(e){const t=me(process.env.TESTKIT_HTTP_ENDPOINT_PREFLIGHT_TIMEOUT_MS,3e4),n=me(process.env.TESTKIT_HTTP_ENDPOINT_PREFLIGHT_ATTEMPT_TIMEOUT_MS,5e3);return{timeoutMs:Math.min(t,e),attemptTimeoutMs:Math.min(n,e)}}function me(e,t){const n=Number(e);return Number.isInteger(n)&&n>0?n:t}function G(e,t){const n=new AbortController;let r=null;const s=()=>{n.abort(e.reason)},i=()=>{const u=t-Date.now();if(u<=0){n.abort(new Se(t));return}r=setTimeout(i,Math.min(u,2147483647)),r.unref()};return e.aborted?s():(e.addEventListener("abort",s,{once:!0}),i()),{signal:n.signal,dispose(){r!==null&&clearTimeout(r),e.removeEventListener("abort",s)}}}export function mapEngineOutcome(e,t,n,r,s){const i=Le(e.evidence.artifacts),u=t.engineId==="node-vitest"&&i.some(T=>T.kind==="vitest-test")&&e.failure!==null&&Ye(e.failure),m=[...e.failure&&!u?[Ze(e.failure)]:[],...i],w=$e(e.evidence.artifacts),S=e.status==="not_run",d=S||e.failure?.classification==="infra";return{task:t,status:e.status,error:e.status==="failed"?e.failure?.message??"Engine task failed without a message":null,reason:e.status==="failed"||e.status==="passed"?null:e.failure?.message??`Engine task ${e.status}`,infraFailed:d,blockedByRuntime:S,durationMs:e.durationMs,startedAt:e.startedAtMs,finishedAt:e.finishedAtMs,artifacts:n,failureDetails:m,checkDetails:w,...ge(r,t,s)}}function ge(e,t,n){if(!("batchTasks"in e))return{};const r=n??e.batchTasks.findIndex(s=>s.id===t.id&&s.file===t.file);if(r<0)throw new Error(`Task ${String(t.id)} is missing from its engine batch`);return{runtimeBatch:{id:e.batchId,index:r,fileCount:e.batchTasks.length,files:e.batchTasks.map(s=>s.file)}}}function Ye(e){return e.code==="NODE_VITEST_REPORT_FAILED"?!0:e.code==="NODE_VITEST_PROCESS_FAILED"&&e.details?.exitCode===1}function Ze(e){const t=e.code??"ENGINE_TASK_FAILED",n={code:t,details:e.details===void 0?null:rt(e.details)};return{kind:"engine-failure",key:t,title:t,...e.classification===void 0?{}:{classification:e.classification},count:1,message:e.message,actual:n,...e.stack===void 0?{}:{stack:e.stack}}}function et(e){return{artifacts:H(e.evidence.artifacts,e.outcomes.flatMap(t=>t.evidence.artifacts)),logs:H(e.evidence.logs,e.outcomes.flatMap(t=>t.evidence.logs)),metrics:H(e.evidence.metrics,e.outcomes.flatMap(t=>t.evidence.metrics)),...e.evidence.native===void 0?{}:{native:e.evidence.native}}}function H(e,t){const n=new Map;for(const r of t){const s=JSON.stringify(r);n.set(s,(n.get(s)??0)+1)}return e.filter(r=>{const s=JSON.stringify(r),i=n.get(s)??0;return i===0?!0:(n.set(s,i-1),!1)})}function tt(e){return e.artifacts.length>0||e.logs.length>0||e.metrics.length>0||e.native!==void 0}function nt(e,t){const n=Ee(e,t);if(n===null||Array.isArray(n)||typeof n!="object")throw new TypeError(`${t} must contain a JSON object`);return n}function Ee(e,t){try{const n=JSON.stringify(e);if(typeof n!="string")throw new TypeError(`${t} must be JSON-serializable`);return JSON.parse(n)}catch(n){throw new TypeError(`${t} must be JSON-serializable`,{cause:n})}}function rt(e){return JSON.parse(JSON.stringify(e))}function he(e,t){if(!F.isAbsolute(e))throw new Error(`Engine execution ${t} must be absolute`);if(!U.statSync(e).isDirectory())throw new Error(`Engine execution ${t} must be a directory`)}function A(e){return e instanceof Error?e:new Error(String(e))}
@@ -12,6 +12,7 @@ export interface NormalizedFailureDetail extends ReportingFailureDetail {
12
12
  groupPath?: string[];
13
13
  key: string;
14
14
  kind: string;
15
+ labelled?: boolean;
15
16
  location?: FailureDetailJsonRecord;
16
17
  message?: string;
17
18
  phase?: string;
@@ -1 +1 @@
1
- export function normalizeFailureDetail(e){const t=a(e);if(!t)return null;const n=s(t.kind),r=typeof t.key=="string"&&t.key.trim().length>0?t.key:null,o=s(t.title);if(!n||!r||!o)return null;const i={kind:n,key:r,title:o,count:V(t.count)??1},c=s(t.classification);c&&(i.classification=c);const u=Array.isArray(t.groupPath)?t.groupPath.filter(F=>typeof F=="string"):[];u.length>0&&(i.groupPath=u);const f=s(t.checkName);f&&(i.checkName=f);const l=x(t.suitePath);l.length>0&&(i.suitePath=l);const m=s(t.phase);m&&(i.phase=m);const h=s(t.message);h&&(i.message=h),t.expected!==void 0&&(i.expected=d(t.expected)),t.actual!==void 0&&(i.actual=d(t.actual));const g=s(t.traceId);g&&(i.traceId=g);const k=p(t.request);k&&(i.request=k);const A=p(t.response);A&&(i.response=A);const z=p(t.location);z&&(i.location=z);const N=s(t.stack);return N&&(i.stack=N),i}export function mergeFailureDetails(e){const t=new Map;for(const n of e){const r=`${n.kind}::${n.key}`,o=t.get(r);if(!o){t.set(r,{...n});continue}o.count+=n.count,!o.message&&n.message&&(o.message=n.message)}return[...t.values()].sort((n,r)=>n.kind.localeCompare(r.kind)||n.key.localeCompare(r.key))}export function collectFailureDetailsFromRuntimeArtifacts(e){const t=[];for(const n of Array.isArray(e)?e:[]){const r=a(n);if(!r||r.kind!=="testkit.failure-details")continue;const o=a(r.data);if(!o)continue;const i=s(o.phase);for(const c of Array.isArray(o.failures)?o.failures:[]){const u=a(c);if(!u)continue;const f=normalizeFailureDetail({...u,phase:u.phase||i||null});f&&t.push(f)}}return mergeFailureDetails(t)}export function collectCheckDetailsFromRuntimeArtifacts(e){const t=[];for(const n of Array.isArray(e)?e:[]){const r=a(n);if(!r||r.kind!=="testkit.checks")continue;const o=a(r.data);if(o)for(const i of Array.isArray(o.checks)?o.checks:[]){const c=a(i);if(!c)continue;const u=s(c.name)??"unnamed",f=b(c.passes),l=b(c.fails);t.push({name:u,path:x(c.path),passes:f,fails:l,passed:f>0&&l===0})}}return t}function a(e){return S(e)?e:null}function S(e){return e!==null&&typeof e=="object"&&!Array.isArray(e)}function x(e){if(!Array.isArray(e))return[];const t=[];for(const n of e){const r=s(n);r&&t.push(r)}return t}function s(e){if(typeof e!="string")return null;const t=e.trim();return t.length>0?t:null}function V(e){return typeof e!="number"||!Number.isInteger(e)||e<=0?null:e}function b(e){try{return Math.max(0,Number(e)||0)}catch{return 0}}function p(e){if(!a(e))return null;const t=d(e);return P(t)?t:null}function d(e){try{const t=JSON.stringify(e);if(typeof t!="string")return String(e);const n=JSON.parse(t);return y(n)?n:String(e)}catch{return String(e)}}function y(e){return e===null||typeof e=="boolean"||typeof e=="number"||typeof e=="string"?!0:Array.isArray(e)?e.every(y):P(e)}function P(e){const t=a(e);return t!==null&&Object.values(t).every(y)}
1
+ export function normalizeFailureDetail(e){const t=a(e);if(!t)return null;const n=s(t.kind),r=typeof t.key=="string"&&t.key.trim().length>0?t.key:null,o=s(t.title);if(!n||!r||!o)return null;const i={kind:n,key:r,title:o,count:J(t.count)??1},c=s(t.classification);c&&(i.classification=c);const l=Array.isArray(t.groupPath)?t.groupPath.filter(S=>typeof S=="string"):[];l.length>0&&(i.groupPath=l),typeof t.labelled=="boolean"&&(i.labelled=t.labelled);const u=s(t.checkName);u&&(i.checkName=u);const f=N(t.suitePath);f.length>0&&(i.suitePath=f);const m=s(t.phase);m&&(i.phase=m);const h=s(t.message);h&&(i.message=h),t.expected!==void 0&&(i.expected=p(t.expected)),t.actual!==void 0&&(i.actual=p(t.actual));const g=s(t.traceId);g&&(i.traceId=g);const k=d(t.request);k&&(i.request=k);const A=d(t.response);A&&(i.response=A);const b=d(t.location);b&&(i.location=b);const z=s(t.stack);return z&&(i.stack=z),i}export function mergeFailureDetails(e){const t=new Map;for(const n of e){const r=JSON.stringify([n.kind,n.labelled??null,n.key,n.title]),o=t.get(r);if(!o){t.set(r,{...n});continue}o.count+=n.count,!o.message&&n.message&&(o.message=n.message)}return[...t.values()].sort((n,r)=>n.kind.localeCompare(r.kind)||n.key.localeCompare(r.key)||n.title.localeCompare(r.title))}export function collectFailureDetailsFromRuntimeArtifacts(e){const t=[];for(const n of Array.isArray(e)?e:[]){const r=a(n);if(!r||r.kind!=="testkit.failure-details")continue;const o=a(r.data);if(!o)continue;const i=s(o.phase);for(const c of Array.isArray(o.failures)?o.failures:[]){const l=a(c);if(!l)continue;const u=normalizeFailureDetail({...l,phase:l.phase||i||null});u&&t.push(u)}}return mergeFailureDetails(t)}export function collectCheckDetailsFromRuntimeArtifacts(e){const t=[];for(const n of Array.isArray(e)?e:[]){const r=a(n);if(!r||r.kind!=="testkit.checks")continue;const o=a(r.data);if(o)for(const i of Array.isArray(o.checks)?o.checks:[]){const c=a(i);if(!c)continue;const l=s(c.name)??"unnamed",u=x(c.passes),f=x(c.fails);t.push({name:l,path:N(c.path),passes:u,fails:f,passed:u>0&&f===0})}}return t}function a(e){return F(e)?e:null}function F(e){return e!==null&&typeof e=="object"&&!Array.isArray(e)}function N(e){if(!Array.isArray(e))return[];const t=[];for(const n of e){const r=s(n);r&&t.push(r)}return t}function s(e){if(typeof e!="string")return null;const t=e.trim();return t.length>0?t:null}function J(e){return typeof e!="number"||!Number.isInteger(e)||e<=0?null:e}function x(e){try{return Math.max(0,Number(e)||0)}catch{return 0}}function d(e){if(!a(e))return null;const t=p(e);return P(t)?t:null}function p(e){try{const t=JSON.stringify(e);if(typeof t!="string")return String(e);const n=JSON.parse(t);return y(n)?n:String(e)}catch{return String(e)}}function y(e){return e===null||typeof e=="boolean"||typeof e=="number"||typeof e=="string"?!0:Array.isArray(e)?e.every(y):P(e)}function P(e){const t=a(e);return t!==null&&Object.values(t).every(y)}
@@ -0,0 +1,8 @@
1
+ import type { Tracker } from "../../tracker/types.js";
2
+ export { TrackerTargetError } from "./remote.js";
3
+ export interface TrackerResolutionRequest {
4
+ readonly env: Readonly<NodeJS.ProcessEnv>;
5
+ readonly productDir: string;
6
+ readonly repository?: string | undefined;
7
+ }
8
+ export declare function resolveTracker(request: TrackerResolutionRequest): Promise<Tracker>;
@@ -0,0 +1 @@
1
+ import{runManagedGitCommand as l}from"../../repo/managed-git.js";import{readGiteaVersion as p}from"../../tracker/gitea.js";import{createTracker as u}from"../../tracker/index.js";import{displayRepositoryReference as m,githubActionsApiUrl as h,parseTrackerRemote as d,parseTrackerRepositoryOverride as c,TrackerTargetError as n}from"./remote.js";import{readGitCredentialToken as f,trackerTokenFromEnvironment as g,trackerTokenVariables as w}from"./token.js";export{TrackerTargetError}from"./remote.js";const v="github.com",T="https://api.github.com";export async function resolveTracker(r){const t=await k(r),{actionsRun:o,apiUrl:e,provider:i}=await U(t,r.env),a=w(i,o),s=g(a,r.env)??await f(r.productDir,t,r.env);if(s===null)throw new n(`Testkit found no token for ${t.origin}. Set ${a.join(" or ")}, or give Git a credential helper that holds one for that host.`);return u({apiUrl:e,name:t.name,owner:t.owner,provider:i,token:s})}async function k(r){const t=r.repository?.trim()??"";if(/^https?:\/\//iu.test(t))return c(t,null);const o=await y(r),e=o===null?null:d(o);if(t.length>0)return c(t,e);if(e!==null)return e;throw new n(o===null?`${r.productDir} has no origin remote, so Testkit cannot tell which repository to report to. Add the remote, or name the repository as a URL such as https://host/owner/name.`:`Testkit cannot read a host, owner, and repository name from the origin remote ${JSON.stringify(m(o))}. Name the repository as a URL such as https://host/owner/name.`)}async function y(r){const t=await l(r.productDir,["config","--get","remote.origin.url"],{env:r.env,repositoryConfigOnly:!0}),o=t.stdout.trim();return t.exitCode===0&&o.length>0?o:null}async function U(r,t){if(new URL(r.origin).host===v)return{actionsRun:!1,apiUrl:T,provider:"github"};const o=h(r.origin,t);if(await p(r.origin)!==null)return{actionsRun:o!==null,apiUrl:o??`${r.origin}/api/v1`,provider:"gitea"};if(o!==null)return{actionsRun:!0,apiUrl:o,provider:"github"};throw new n(`${r.origin} does not answer as GitHub or Gitea. Testkit supports GitHub and Gitea.`)}
@@ -0,0 +1,13 @@
1
+ export interface TrackerRepositoryLocation {
2
+ readonly credentialPath: string;
3
+ readonly name: string;
4
+ readonly origin: string;
5
+ readonly owner: string;
6
+ }
7
+ export declare class TrackerTargetError extends Error {
8
+ constructor(message: string);
9
+ }
10
+ export declare function parseTrackerRemote(remoteUrl: string): TrackerRepositoryLocation | null;
11
+ export declare function parseTrackerRepositoryOverride(repository: string, remote: TrackerRepositoryLocation | null): TrackerRepositoryLocation;
12
+ export declare function displayRepositoryReference(reference: string): string;
13
+ export declare function githubActionsApiUrl(origin: string, env: Readonly<NodeJS.ProcessEnv>): string | null;
@@ -0,0 +1 @@
1
+ export class TrackerTargetError extends Error{constructor(t){super(t),this.name="TrackerTargetError"}}const p=/^[^@/:\s]+@([^/:\s]+):([^/\s]+)\/([^/\s]+)$/u,a=/^[\w.-]+$/u;export function parseTrackerRemote(r){const t=r.trim(),n=p.exec(t);if(n!==null)return c(n[1],n[2],n[3]);if(!URL.canParse(t))return null;const e=new URL(t),o=i(e);return o===null?null:e.protocol==="ssh:"?c(e.hostname,o.owner,o.rawName):e.protocol!=="https:"&&e.protocol!=="http:"?null:l(e.origin,o.owner,o.rawName,e.pathname.replace(/^\/+|\/+$/gu,""))}export function parseTrackerRepositoryOverride(r,t){const n=r.trim(),e=/^([\w.-]+)\/([\w.-]+)$/u.exec(n);if(e!==null){if(t===null)throw new TrackerTargetError(`The repository ${JSON.stringify(n)} names no host, and this checkout has no usable origin remote to take one from. Give the full URL, such as https://host/owner/name.`);const o=l(t.origin,e[1],e[2],null);if(o!==null)return o}if(/^https?:\/\//iu.test(n)&&URL.canParse(n)){const o=new URL(n),s=o.username.length>0||o.password.length>0?null:i(o),u=s===null?null:l(o.origin,s.owner,s.rawName,null);if(u!==null)return u}throw new TrackerTargetError(`The repository must be owner/name or a URL such as https://host/owner/name without credentials. Received ${JSON.stringify(displayRepositoryReference(n))}.`)}export function displayRepositoryReference(r){const t=r.lastIndexOf("@");return t===-1?r:`${/^[a-z][a-z\d+.-]*:\/\//iu.exec(r)?.[0]??""}${r.slice(t+1)}`}export function githubActionsApiUrl(r,t){if(t.GITHUB_ACTIONS!=="true")return null;const n=h(t.GITHUB_SERVER_URL),e=h(t.GITHUB_API_URL);return n===null||e===null||n.origin!==r?null:`${e.origin}${e.pathname}`.replace(/\/+$/u,"")}function i(r){if(r.search.length>0||r.hash.length>0)return null;const t=r.pathname.split("/").filter(Boolean);return t.length===2?{owner:t[0],rawName:t[1]}:null}function c(r,t,n){return URL.canParse(`https://${r}`)?l(new URL(`https://${r}`).origin,t,n,null):null}function l(r,t,n,e){const o=n.replace(/\.git$/iu,"");return!a.test(t)||!a.test(o)||[t,o].some(s=>s==="."||s==="..")?null:{credentialPath:e??`${t}/${o}.git`,name:o,origin:r,owner:t}}function h(r){const t=r?.trim()??"";if(!URL.canParse(t))return null;const n=new URL(t);return n.username.length===0&&n.password.length===0&&n.search.length===0&&n.hash.length===0&&(n.protocol==="https:"||n.protocol==="http:")?n:null}
@@ -0,0 +1,7 @@
1
+ import type { TrackerProvider } from "../../tracker/types.js";
2
+ import type { TrackerRepositoryLocation } from "./remote.js";
3
+ export declare function trackerTokenVariables(provider: TrackerProvider, actionsRun: boolean): readonly string[];
4
+ export declare function trackerTokenFromEnvironment(variables: readonly string[], env: Readonly<NodeJS.ProcessEnv>): string | null;
5
+ export declare function gitCredentialRequest(location: TrackerRepositoryLocation): string;
6
+ export declare function passwordFromGitCredential(output: string): string | null;
7
+ export declare function readGitCredentialToken(productDir: string, location: TrackerRepositoryLocation, env: Readonly<NodeJS.ProcessEnv>): Promise<string | null>;
@@ -0,0 +1,5 @@
1
+ import{startManagedHostProcess as a}from"../../process/index.js";import{createManagedGitEnvironment as l,DEFAULT_GIT_COMMAND_TIMEOUT_MS as u}from"../../repo/managed-git.js";const d=["GITHUB_TOKEN","GH_TOKEN"],T=["GITEA_TOKEN"],A=["GITEA_TOKEN","GITHUB_TOKEN"],E=new Set(["GIT_ASKPASS","SSH_ASKPASS"]),i="password=";export function trackerTokenVariables(e,t){return e==="github"?d:t?A:T}export function trackerTokenFromEnvironment(e,t){for(const n of e){const r=t[n]?.trim();if(r)return r}return null}export function gitCredentialRequest(e){const t=new URL(e.origin);return`protocol=${t.protocol.slice(0,-1)}
2
+ host=${t.host}
3
+ path=${e.credentialPath}
4
+
5
+ `}export function passwordFromGitCredential(e){const t=e.split(/\r?\n/u).find(n=>n.startsWith(i));return t===void 0||t.length===i.length?null:t.slice(i.length)}export async function readGitCredentialToken(e,t,n){const r=Object.fromEntries(Object.entries(n).filter(([c])=>!E.has(c.toUpperCase())));let o;try{o=a({args:["-c","core.askPass=","credential","fill"],cwd:e,displayLabel:"Git credential fill",env:{...l(r),GCM_INTERACTIVE:"never",GIT_TERMINAL_PROMPT:"0"},executable:"git",outputMode:"bounded-capture",recoveryIdentity:"none",timeoutMs:u})}catch{return null}o.stdin.on("error",()=>{}),o.stdin.end(gitCredentialRequest(t));const s=await o.completion;return s.exitCode===0?passwordFromGitCredential(s.stdout):null}
@@ -11,6 +11,32 @@ export declare const commandCatalog: Readonly<{
11
11
  }, {
12
12
  readonly file: import("./framework/definitions.js").ArgumentDefinition<string | undefined>;
13
13
  }>;
14
+ "bug file": import("./framework/command.js").CommandDefinition<{
15
+ readonly dir: import("./framework/definitions.js").FlagDefinition<string | undefined>;
16
+ }, {
17
+ readonly id: import("./framework/definitions.js").ArgumentDefinition<string>;
18
+ }>;
19
+ "bug guide": import("./framework/command.js").CommandDefinition<undefined, undefined>;
20
+ "bug record": import("./framework/command.js").CommandDefinition<{
21
+ readonly dir: import("./framework/definitions.js").FlagDefinition<string | undefined>;
22
+ }, {
23
+ readonly file: import("./framework/definitions.js").ArgumentDefinition<string>;
24
+ }>;
25
+ "campaign finish": import("./framework/command.js").CommandDefinition<{
26
+ readonly dir: import("./framework/definitions.js").FlagDefinition<string | undefined>;
27
+ }, undefined>;
28
+ "campaign start": import("./framework/command.js").CommandDefinition<{
29
+ readonly dir: import("./framework/definitions.js").FlagDefinition<string | undefined>;
30
+ readonly id: import("./framework/definitions.js").FlagDefinition<string | undefined>;
31
+ readonly target: import("./framework/definitions.js").FlagDefinition<string>;
32
+ }, undefined>;
33
+ "campaign status": import("./framework/command.js").CommandDefinition<{
34
+ readonly dir: import("./framework/definitions.js").FlagDefinition<string | undefined>;
35
+ }, undefined>;
36
+ "ci report": import("./framework/command.js").CommandDefinition<{
37
+ readonly context: import("./framework/definitions.js").FlagDefinition<string | undefined>;
38
+ readonly dir: import("./framework/definitions.js").FlagDefinition<string | undefined>;
39
+ }, undefined>;
14
40
  cleanup: import("./framework/command.js").CommandDefinition<{
15
41
  readonly dir: import("./framework/definitions.js").FlagDefinition<string | undefined>;
16
42
  readonly service: import("./framework/definitions.js").FlagDefinition<string | undefined>;
@@ -1 +1 @@
1
- import{Args as r,Flags as e}from"./framework/definitions.js";import{defineCommand as t}from"./framework/command.js";import{sharedFlags as s,runFlags as i}from"./command-flags.js";import{publicTestTypeList as o,publicTestTypeListText as a}from"../domain/test-types.js";export const commandCatalog=Object.freeze({artifacts:t({summary:"List, verify, or inspect artifacts from immutable Testkit runs",enableJsonFlag:!0,args:{file:r.string({description:"Optional test file selector",required:!1})},flags:{...s,kind:e.string({description:"Limit artifacts to one kind"}),format:e.string({description:"Output format",options:["list","text"],default:"list"}),run:e.string({description:"Read one immutable run ID instead of latest"}),list:e.boolean({description:"List retained immutable runs",default:!1}),verify:e.boolean({description:"Verify the selected run manifest and checksums",default:!1})}}),cleanup:t({summary:"Clean stale local testkit state",enableJsonFlag:!0,flags:{...s,"dry-run":e.boolean({description:"Show cleanup actions without deleting state",default:!1}),cache:e.string({description:"Clean cache state: runtime, bundles, or all",multiple:!0,options:["runtime","bundles","all"]}),resources:e.boolean({description:"Clean Testkit-owned Docker resources that are stale or orphaned",default:!1})}}),"db schema refresh":t({summary:"Refresh a source database schema cache",enableJsonFlag:!0,flags:s}),"db schema verify":t({summary:"Verify local schema replay against the source database schema",enableJsonFlag:!0,flags:{...s,"skip-schema-source-verify":e.boolean({description:"Skip the check that local schema replay matches the source database schema",default:!1})}}),destroy:t({summary:"Destroy local testkit state",enableJsonFlag:!0,flags:s}),discover:t({summary:"Discover managed tests and report their metadata",enableJsonFlag:!0,flags:{...s,type:e.string({char:"t",multiple:!0,description:`Filter by suite type(s): ${a({includeAll:!0})}`}),suite:e.string({char:"s",multiple:!0,description:"Filter by suite selector(s)"}),file:e.string({char:"f",multiple:!0,description:"Filter by exact file path(s)"}),"runnable-only":e.boolean({description:"Show only files not skipped by repo rules",default:!1}),strict:e.boolean({description:"Fail instead of reporting discovery/config diagnostics",default:!1}),output:e.string({description:"Write machine-readable discovery JSON to a file"}),"output-mode":e.string({description:"Human-readable output mode",options:["compact","verbose"],default:"compact"})}}),doctor:t({summary:"Run built-in config, discovery, and hygiene checks",enableJsonFlag:!0,flags:{dir:e.string({description:"Product directory"}),typecheck:e.boolean({description:"Include generated TypeScript checks",default:!0,allowNo:!0})}}),"guard unsafe-entrypoint":t({summary:"Fail a local script that must be run through Testkit",flags:{command:e.string({char:"c",description:"Disabled command name shown in the error message",required:!0}),use:e.string({char:"u",description:"Replacement command shown in the error message",required:!0})}}),lint:t({summary:"Run built-in Testkit repository hygiene checks",enableJsonFlag:!0,flags:{dir:e.string({description:"Product directory"})}}),"run status":t({summary:"Show active Testkit run progress",enableJsonFlag:!0,flags:{...s,run:e.string({description:"Specific run id to show"}),full:e.boolean({description:"Include the full live status payload in JSON output"})}}),"run stop":t({summary:"Stop active Testkit run work",enableJsonFlag:!0,flags:{...s,run:e.string({description:"Specific run id to stop; defaults to the newest active run"}),all:e.boolean({description:"Stop all active runs for this product",default:!1}),reason:e.string({description:"Operator-visible reason recorded with cancelled runs and dirty slots"})}}),run:t({summary:"Run test suites",enableJsonFlag:!0,args:{type:r.string({description:`Optional suite type shortcut: ${a({includeAll:!0})}`,required:!1,options:o({includeAll:!0})})},flags:i}),"setup playwright":t({summary:"Install the managed Playwright Chromium runtime",enableJsonFlag:!0,flags:{dir:e.string({description:"Product directory"}),force:e.boolean({default:!1,description:"Force Playwright to repair the browser installation"}),"with-deps":e.boolean({default:!1,description:"Install required host dependencies with Chromium"})}}),status:t({summary:"Show local testkit state",enableJsonFlag:!0,flags:s}),stop:t({summary:"Stop all Testkit-owned runs",enableJsonFlag:!0,flags:{...s,reason:e.string({description:"Operator-visible reason recorded with cancelled runs"})}}),typecheck:t({summary:"Typecheck testkit config, helpers, and suites",enableJsonFlag:!0,flags:{dir:e.string({description:"Product directory"})}}),workspace:t({summary:"Inspect Testkit workstation data for a product",enableJsonFlag:!0,flags:{...s,"target-limit":e.integer({description:"Maximum number of quick-run target previews",default:24}),"no-status":e.boolean({description:"Skip runtime status collection",default:!1})}})});
1
+ import{Args as s,Flags as e}from"./framework/definitions.js";import{defineCommand as t}from"./framework/command.js";import{sharedFlags as r,runFlags as i}from"./command-flags.js";import{publicTestTypeList as n,publicTestTypeListText as a}from"../domain/test-types.js";export const commandCatalog=Object.freeze({artifacts:t({summary:"List, verify, or inspect artifacts from immutable Testkit runs",enableJsonFlag:!0,args:{file:s.string({description:"Optional test file selector",required:!1})},flags:{...r,kind:e.string({description:"Limit artifacts to one kind"}),format:e.string({description:"Output format",options:["list","text"],default:"list"}),run:e.string({description:"Read one immutable run ID instead of latest"}),list:e.boolean({description:"List retained immutable runs",default:!1}),verify:e.boolean({description:"Verify the selected run manifest and checksums",default:!1})}}),"bug file":t({summary:"Run a recorded bug's test and file the bug as an issue",enableJsonFlag:!0,args:{id:s.string({description:"Draft id that `testkit bug record` printed",required:!0})},flags:{dir:e.string({description:"Product directory"})}}),"bug guide":t({summary:"Print the bug record format, the severities, and the category lists",enableJsonFlag:!0}),"bug record":t({summary:"Record a confirmed bug as a local draft",enableJsonFlag:!0,args:{file:s.string({description:"Bug record JSON file, or - to read standard input",required:!0})},flags:{dir:e.string({description:"Product directory"})}}),"campaign finish":t({summary:"Finish the active campaign and write its pull request summary",enableJsonFlag:!0,flags:{dir:e.string({description:"Product directory"})}}),"campaign start":t({summary:"Start a bug-hunting campaign and print the agent's brief",enableJsonFlag:!0,flags:{dir:e.string({description:"Product directory"}),id:e.string({description:"Campaign id, instead of one made from the target and the date"}),target:e.string({description:"What the agent should hunt, in one line of at most 200 characters",required:!0})}}),"campaign status":t({summary:"Show the active campaign, its recorded bugs, and the area map coverage",enableJsonFlag:!0,flags:{dir:e.string({description:"Product directory"})}}),"ci report":t({summary:"Post the last run as a commit status, a step summary, and one pull request comment",enableJsonFlag:!0,flags:{context:e.string({description:"Commit status context, instead of testkit"}),dir:e.string({description:"Product directory"})}}),cleanup:t({summary:"Clean stale local testkit state",enableJsonFlag:!0,flags:{...r,"dry-run":e.boolean({description:"Show cleanup actions without deleting state",default:!1}),cache:e.string({description:"Clean cache state: runtime, bundles, or all",multiple:!0,options:["runtime","bundles","all"]}),resources:e.boolean({description:"Clean Testkit-owned Docker resources that are stale or orphaned",default:!1})}}),"db schema refresh":t({summary:"Refresh a source database schema cache",enableJsonFlag:!0,flags:r}),"db schema verify":t({summary:"Verify local schema replay against the source database schema",enableJsonFlag:!0,flags:{...r,"skip-schema-source-verify":e.boolean({description:"Skip the check that local schema replay matches the source database schema",default:!1})}}),destroy:t({summary:"Destroy local testkit state",enableJsonFlag:!0,flags:r}),discover:t({summary:"Discover managed tests and report their metadata",enableJsonFlag:!0,flags:{...r,type:e.string({char:"t",multiple:!0,description:`Filter by suite type(s): ${a({includeAll:!0})}`}),suite:e.string({char:"s",multiple:!0,description:"Filter by suite selector(s)"}),file:e.string({char:"f",multiple:!0,description:"Filter by exact file path(s)"}),"runnable-only":e.boolean({description:"Show only files not skipped by repo rules",default:!1}),strict:e.boolean({description:"Fail instead of reporting discovery/config diagnostics",default:!1}),output:e.string({description:"Write machine-readable discovery JSON to a file"}),"output-mode":e.string({description:"Human-readable output mode",options:["compact","verbose"],default:"compact"})}}),doctor:t({summary:"Run built-in config, discovery, and hygiene checks",enableJsonFlag:!0,flags:{dir:e.string({description:"Product directory"}),typecheck:e.boolean({description:"Include generated TypeScript checks",default:!0,allowNo:!0})}}),"guard unsafe-entrypoint":t({summary:"Fail a local script that must be run through Testkit",flags:{command:e.string({char:"c",description:"Disabled command name shown in the error message",required:!0}),use:e.string({char:"u",description:"Replacement command shown in the error message",required:!0})}}),lint:t({summary:"Run built-in Testkit repository hygiene checks",enableJsonFlag:!0,flags:{dir:e.string({description:"Product directory"})}}),"run status":t({summary:"Show active Testkit run progress",enableJsonFlag:!0,flags:{...r,run:e.string({description:"Specific run id to show"}),full:e.boolean({description:"Include the full live status payload in JSON output"})}}),"run stop":t({summary:"Stop active Testkit run work",enableJsonFlag:!0,flags:{...r,run:e.string({description:"Specific run id to stop; defaults to the newest active run"}),all:e.boolean({description:"Stop all active runs for this product",default:!1}),reason:e.string({description:"Operator-visible reason recorded with cancelled runs and dirty slots"})}}),run:t({summary:"Run test suites",enableJsonFlag:!0,args:{type:s.string({description:`Optional suite type shortcut: ${a({includeAll:!0})}`,required:!1,options:n({includeAll:!0})})},flags:i}),"setup playwright":t({summary:"Install the managed Playwright Chromium runtime",enableJsonFlag:!0,flags:{dir:e.string({description:"Product directory"}),force:e.boolean({default:!1,description:"Force Playwright to repair the browser installation"}),"with-deps":e.boolean({default:!1,description:"Install required host dependencies with Chromium"})}}),status:t({summary:"Show local testkit state",enableJsonFlag:!0,flags:r}),stop:t({summary:"Stop all Testkit-owned runs",enableJsonFlag:!0,flags:{...r,reason:e.string({description:"Operator-visible reason recorded with cancelled runs"})}}),typecheck:t({summary:"Typecheck testkit config, helpers, and suites",enableJsonFlag:!0,flags:{dir:e.string({description:"Product directory"})}}),workspace:t({summary:"Inspect Testkit workstation data for a product",enableJsonFlag:!0,flags:{...r,"target-limit":e.integer({description:"Maximum number of quick-run target previews",default:24}),"no-status":e.boolean({description:"Skip runtime status collection",default:!1})}})});
@@ -1 +1 @@
1
- import{commandCatalog as e}from"./command-catalog.js";export const commandRegistry=Object.freeze([t("artifacts",()=>import("./commands/artifacts.js")),t("cleanup",()=>import("./commands/cleanup.js")),t("db schema refresh",()=>import("./commands/db/schema/refresh.js")),t("db schema verify",()=>import("./commands/db/schema/verify.js")),t("destroy",()=>import("./commands/destroy.js")),t("discover",()=>import("./commands/discover.js")),t("doctor",()=>import("./commands/doctor.js")),t("guard unsafe-entrypoint",()=>import("./commands/guard/unsafe-entrypoint.js")),t("lint",()=>import("./commands/lint.js")),t("run status",()=>import("./commands/run/status.js")),t("run stop",()=>import("./commands/run/stop.js")),t("run",()=>import("./commands/run.js")),t("setup playwright",()=>import("./commands/setup/playwright.js")),t("status",()=>import("./commands/status.js")),t("stop",()=>import("./commands/stop.js")),t("typecheck",()=>import("./commands/typecheck.js")),t("workspace",()=>import("./commands/workspace.js"))]),removedCommands=new Map([["cloud","The testkit cloud commands were removed in Testkit 0.4.0. Testkit no longer publishes runs to Testkit Cloud."]]);function t(r,o){return Object.freeze({...e[r],id:r.split(" "),load:o})}
1
+ import{commandCatalog as i}from"./command-catalog.js";export const commandRegistry=Object.freeze([t("artifacts",()=>import("./commands/artifacts.js")),t("bug file",()=>import("./commands/bug/file.js")),t("bug guide",()=>import("./commands/bug/guide.js")),t("bug record",()=>import("./commands/bug/record.js")),t("campaign finish",()=>import("./commands/campaign/finish.js")),t("campaign start",()=>import("./commands/campaign/start.js")),t("campaign status",()=>import("./commands/campaign/status.js")),t("ci report",()=>import("./commands/ci/report.js")),t("cleanup",()=>import("./commands/cleanup.js")),t("db schema refresh",()=>import("./commands/db/schema/refresh.js")),t("db schema verify",()=>import("./commands/db/schema/verify.js")),t("destroy",()=>import("./commands/destroy.js")),t("discover",()=>import("./commands/discover.js")),t("doctor",()=>import("./commands/doctor.js")),t("guard unsafe-entrypoint",()=>import("./commands/guard/unsafe-entrypoint.js")),t("lint",()=>import("./commands/lint.js")),t("run status",()=>import("./commands/run/status.js")),t("run stop",()=>import("./commands/run/stop.js")),t("run",()=>import("./commands/run.js")),t("setup playwright",()=>import("./commands/setup/playwright.js")),t("status",()=>import("./commands/status.js")),t("stop",()=>import("./commands/stop.js")),t("typecheck",()=>import("./commands/typecheck.js")),t("workspace",()=>import("./commands/workspace.js"))]),removedCommands=new Map([["cloud","The testkit cloud commands were removed in Testkit 0.4.0. Testkit no longer publishes runs to Testkit Cloud."]]);function t(r,o){return Object.freeze({...i[r],id:r.split(" "),load:o})}
@@ -0,0 +1,16 @@
1
+ import type { CommandContext } from "../../command-catalog.js";
2
+ export default function run({ args, flags, json, log }: CommandContext<"bug file">): Promise<{
3
+ ok: boolean;
4
+ created: boolean;
5
+ draft: import("../../../app/bugs/draft.js").BugDraft;
6
+ issue: import("../../../app/bugs/file.js").BugIssueReference;
7
+ } | {
8
+ area: import("../../../app/bugs/facts.js").BugFactsArea | null;
9
+ covers: import("../../../app/bugs/issue.js").BugCoverage;
10
+ draft: import("../../../app/bugs/draft.js").BugDraft;
11
+ introducedBy: string | null;
12
+ issue: import("../../../app/bugs/file.js").BugIssueReference;
13
+ path: string;
14
+ ok: boolean;
15
+ created: boolean;
16
+ }>;
@@ -0,0 +1,2 @@
1
+ import{fileBug as d,prepareBugFiling as n}from"../../../app/bugs/file.js";import{readRun as c}from"../../../app/bugs/run-reading.js";import{buildRunRequest as f}from"../../../app/run/request.js";import{executeRunRequest as l}from"../../operations/run/operation.js";import{renderAlreadyFiledBug as p,renderBugFileResult as m,renderBugFileRun as R}from"../../renderers/bug/text.js";export default async function w({args:a,flags:i,json:r,log:t}){const e=await n({dir:i.dir,env:process.env,id:a.id});if(e.status==="already-filed")return r||t(p(e.issue)),{ok:!0,created:!1,draft:e.draft,issue:e.issue};r||t(R(e));const s=await f({allowEmpty:!1,dir:i.dir,file:[e.testFile],ignoreSkipRules:!1,skipSchemaSourceVerify:!1},null,process.cwd(),process.cwd()),o=await l(s,{json:r,outputMode:"compact"}),u=await d(e,c(o.runArtifact,e.checkout.productPath));return r||t(m(u).join(`
2
+ `)),{ok:!0,created:!0,...u}}
@@ -0,0 +1,5 @@
1
+ import type { CommandContext } from "../../command-catalog.js";
2
+ export default function run({ json, log }: CommandContext<"bug guide">): {
3
+ ok: boolean;
4
+ guide: import("../../../app/bugs/guide.js").BugGuide;
5
+ };