playwright-checkpoint 0.3.0 → 0.4.0

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 (42) hide show
  1. package/README.md +57 -0
  2. package/dist/{chunk-KG37WSYS.js → chunk-M3BRR3LT.js} +9 -3
  3. package/dist/{chunk-KG37WSYS.js.map → chunk-M3BRR3LT.js.map} +1 -1
  4. package/dist/{chunk-X5IPL32H.js → chunk-WXZOP7XI.js} +153 -35
  5. package/dist/chunk-WXZOP7XI.js.map +1 -0
  6. package/dist/{chunk-K5DX32TO.js → chunk-YUFXGGZM.js} +2 -2
  7. package/dist/cli/bin.cjs +2501 -2386
  8. package/dist/cli/bin.cjs.map +1 -1
  9. package/dist/cli/bin.js +3 -2
  10. package/dist/cli/bin.js.map +1 -1
  11. package/dist/cli/index.cjs +1405 -68
  12. package/dist/cli/index.cjs.map +1 -1
  13. package/dist/cli/index.d.cts +2 -2
  14. package/dist/cli/index.d.ts +2 -2
  15. package/dist/cli/index.js +3 -2
  16. package/dist/{core-CD4jHGgI.d.cts → core-6gyzs35M.d.ts} +2 -1
  17. package/dist/{core-CZvnc0rE.d.ts → core-Dd3WLuTs.d.cts} +2 -1
  18. package/dist/core.cjs +8 -2
  19. package/dist/core.cjs.map +1 -1
  20. package/dist/core.d.cts +2 -2
  21. package/dist/core.d.ts +2 -2
  22. package/dist/core.js +1 -1
  23. package/dist/{index-BjYQX_hK.d.ts → index-CvcgBzvl.d.ts} +1 -1
  24. package/dist/{index-Cabk31qi.d.cts → index-OQx9qcVO.d.cts} +1 -1
  25. package/dist/index.cjs +212 -38
  26. package/dist/index.cjs.map +1 -1
  27. package/dist/index.d.cts +4 -4
  28. package/dist/index.d.ts +4 -4
  29. package/dist/index.js +69 -15
  30. package/dist/index.js.map +1 -1
  31. package/dist/mcp/index.cjs +148 -34
  32. package/dist/mcp/index.cjs.map +1 -1
  33. package/dist/mcp/index.js +4 -4
  34. package/dist/teardown.cjs +1409 -72
  35. package/dist/teardown.cjs.map +1 -1
  36. package/dist/teardown.js +3 -2
  37. package/dist/teardown.js.map +1 -1
  38. package/dist/{types-G7w4n8kR.d.cts → types-wX4eB9mb.d.cts} +16 -1
  39. package/dist/{types-G7w4n8kR.d.ts → types-wX4eB9mb.d.ts} +16 -1
  40. package/package.json +2 -1
  41. package/dist/chunk-X5IPL32H.js.map +0 -1
  42. /package/dist/{chunk-K5DX32TO.js.map → chunk-YUFXGGZM.js.map} +0 -0
package/dist/index.cjs CHANGED
@@ -1358,6 +1358,12 @@ function createManifest(sessionMetadata) {
1358
1358
  checkpoints: []
1359
1359
  };
1360
1360
  }
1361
+ function resolveTestConfig(testConfig) {
1362
+ if (typeof testConfig === "function") {
1363
+ return testConfig();
1364
+ }
1365
+ return testConfig ?? null;
1366
+ }
1361
1367
  async function writeManifestFile(manifestPath, manifest) {
1362
1368
  await import_promises12.default.mkdir(import_node_path13.default.dirname(manifestPath), { recursive: true });
1363
1369
  await import_promises12.default.writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}
@@ -1563,7 +1569,7 @@ async function createCheckpointSession(page, options) {
1563
1569
  }
1564
1570
  }
1565
1571
  await import_promises12.default.mkdir(outputDir, { recursive: true });
1566
- await ensureCollectorsSetup(resolveCollectors(sessionConfig));
1572
+ await ensureCollectorsSetup(resolveCollectors(sessionConfig, resolveTestConfig(options.testConfig)));
1567
1573
  return {
1568
1574
  outputDir,
1569
1575
  manifest,
@@ -1571,7 +1577,7 @@ async function createCheckpointSession(page, options) {
1571
1577
  if (finalizePromise) {
1572
1578
  throw new Error("Checkpoint session has already been finalized.");
1573
1579
  }
1574
- const resolvedCollectors = resolveCollectors(sessionConfig, null, checkpointOptions);
1580
+ const resolvedCollectors = resolveCollectors(sessionConfig, resolveTestConfig(options.testConfig), checkpointOptions);
1575
1581
  await ensureCollectorsSetup(resolvedCollectors);
1576
1582
  return runCollectorPipeline({
1577
1583
  page,
@@ -1648,11 +1654,54 @@ function mergeCollectorOverrides(current, updates) {
1648
1654
  }
1649
1655
  function mergeTestConfig(current, update) {
1650
1656
  const collectors = mergeCollectorOverrides(current?.collectors, update.collectors);
1657
+ const article = mergeArticleMetadata(current?.article, update.article);
1658
+ const articles = update.articles ? cloneArticleDefinitions(update.articles) : current?.articles ? cloneArticleDefinitions(current.articles) : void 0;
1651
1659
  return {
1652
1660
  description: update.description ?? current?.description,
1661
+ ...article ? { article } : {},
1662
+ ...articles ? { articles } : {},
1653
1663
  ...collectors ? { collectors } : {}
1654
1664
  };
1655
1665
  }
1666
+ function mergeArticleMetadata(current, update) {
1667
+ if (!current && !update) {
1668
+ return void 0;
1669
+ }
1670
+ const merged = {
1671
+ ...current ?? {},
1672
+ ...update ?? {}
1673
+ };
1674
+ if (current?.frontmatter || update?.frontmatter) {
1675
+ merged.frontmatter = {
1676
+ ...current?.frontmatter ?? {},
1677
+ ...update?.frontmatter ?? {}
1678
+ };
1679
+ }
1680
+ return merged;
1681
+ }
1682
+ function cloneArticleMetadata(article) {
1683
+ return {
1684
+ ...article,
1685
+ ...article.frontmatter ? { frontmatter: { ...article.frontmatter } } : {}
1686
+ };
1687
+ }
1688
+ function cloneArticleDefinition(article) {
1689
+ return {
1690
+ ...cloneArticleMetadata(article),
1691
+ steps: [...article.steps]
1692
+ };
1693
+ }
1694
+ function cloneArticleDefinitions(articles) {
1695
+ return articles.map((article) => cloneArticleDefinition(article));
1696
+ }
1697
+ function syncManifestArticle(manifest, testConfig) {
1698
+ if (testConfig?.article) {
1699
+ manifest.article = cloneArticleMetadata(testConfig.article);
1700
+ }
1701
+ if (testConfig?.articles) {
1702
+ manifest.articles = cloneArticleDefinitions(testConfig.articles);
1703
+ }
1704
+ }
1656
1705
  function manifestEnvironment() {
1657
1706
  return process.env.PLAYWRIGHT_CHECKPOINT_ENV || process.env.NODE_ENV || "test";
1658
1707
  }
@@ -1729,6 +1778,8 @@ function createCheckpoint(globalConfig = {}) {
1729
1778
  const base = playwright.test;
1730
1779
  const test2 = base.extend({
1731
1780
  checkpointManifest: [
1781
+ // Playwright fixture callbacks must use object destructuring for the first arg.
1782
+ // eslint-disable-next-line no-empty-pattern
1732
1783
  async ({}, use, testInfo) => {
1733
1784
  const manifest = createCheckpointManifestRecord(testInfo);
1734
1785
  try {
@@ -1743,6 +1794,8 @@ function createCheckpoint(globalConfig = {}) {
1743
1794
  },
1744
1795
  { auto: true }
1745
1796
  ],
1797
+ // Playwright fixture callbacks must use object destructuring for the first arg.
1798
+ // eslint-disable-next-line no-empty-pattern
1746
1799
  testCheckpointConfig: async ({}, use) => {
1747
1800
  let current = null;
1748
1801
  const controller = {
@@ -1758,6 +1811,8 @@ function createCheckpoint(globalConfig = {}) {
1758
1811
  };
1759
1812
  await use(controller);
1760
1813
  },
1814
+ // Playwright fixture callbacks must use object destructuring for the first arg.
1815
+ // eslint-disable-next-line no-empty-pattern
1761
1816
  deviceProfile: async ({}, use, testInfo) => {
1762
1817
  await use(createDeviceProfile(testInfo));
1763
1818
  },
@@ -1766,15 +1821,20 @@ function createCheckpoint(globalConfig = {}) {
1766
1821
  outputDir: testInfo.outputPath("checkpoints"),
1767
1822
  manifestPath: testInfo.outputPath("checkpoint-manifest.json"),
1768
1823
  manifest: checkpointManifest,
1769
- collectors: mergeConfig(globalConfig, testCheckpointConfig.get()),
1824
+ collectors: globalConfig.collectors,
1825
+ testConfig: () => testCheckpointConfig.get(),
1770
1826
  custom: globalConfig.custom,
1771
1827
  redact: globalConfig.redact,
1772
1828
  testInfo,
1773
1829
  adjustTimeout: createAdjustTimeout(testInfo)
1774
1830
  });
1775
1831
  try {
1776
- await use((name, options = {}) => session.checkpoint(name, options));
1832
+ await use((name, options = {}) => {
1833
+ syncManifestArticle(checkpointManifest, testCheckpointConfig.get());
1834
+ return session.checkpoint(name, options);
1835
+ });
1777
1836
  } finally {
1837
+ syncManifestArticle(checkpointManifest, testCheckpointConfig.get());
1778
1838
  await session.finalize();
1779
1839
  }
1780
1840
  }
@@ -2425,6 +2485,28 @@ function stripTags(value) {
2425
2485
  const stripped = value.replace(/\s+@[a-z0-9-]+/gi, " ").replace(/\s+/g, " ").trim();
2426
2486
  return stripped || value.trim() || "Untitled story";
2427
2487
  }
2488
+ function articleTitle(run) {
2489
+ const override = run.article?.title?.trim();
2490
+ return override || stripTags(run.title);
2491
+ }
2492
+ function articleDescription(run) {
2493
+ const description = run.article?.description?.trim();
2494
+ return description ? description : null;
2495
+ }
2496
+ function articleSlug(run) {
2497
+ const override = run.article?.slug?.trim();
2498
+ return slugify2(override || stripTags(run.title));
2499
+ }
2500
+ function uniqueArticleSlug(baseSlug, usedSlugs) {
2501
+ if (!usedSlugs.has(baseSlug)) {
2502
+ return baseSlug;
2503
+ }
2504
+ let index = 1;
2505
+ while (usedSlugs.has(`${baseSlug}-${index}`)) {
2506
+ index += 1;
2507
+ }
2508
+ return `${baseSlug}-${index}`;
2509
+ }
2428
2510
  function normalizeConfig(config) {
2429
2511
  return {
2430
2512
  storiesDir: typeof config.storiesDir === "string" ? config.storiesDir : ".",
@@ -2435,7 +2517,8 @@ function normalizeConfig(config) {
2435
2517
  footer: typeof config.footer === "string" ? config.footer : void 0,
2436
2518
  frontmatter: config.frontmatter === true || config.frontmatter === false || config.frontmatter != null && typeof config.frontmatter === "object" && !Array.isArray(config.frontmatter) ? config.frontmatter : false,
2437
2519
  imagePathPrefix: typeof config.imagePathPrefix === "string" ? config.imagePathPrefix : void 0,
2438
- copyScreenshots: typeof config.copyScreenshots === "boolean" ? config.copyScreenshots : true
2520
+ copyScreenshots: typeof config.copyScreenshots === "boolean" ? config.copyScreenshots : true,
2521
+ requireExplicitStep: typeof config.requireExplicitStep === "boolean" ? config.requireExplicitStep : false
2439
2522
  };
2440
2523
  }
2441
2524
  function normalizeTags(tags) {
@@ -2447,6 +2530,9 @@ function shouldIncludeRun(run, config) {
2447
2530
  const runTags = new Set(normalizeTags(run.tags));
2448
2531
  return includeTags.some((tag) => runTags.has(tag));
2449
2532
  }
2533
+ if (run.articles) {
2534
+ return true;
2535
+ }
2450
2536
  return run.checkpoints.some((checkpoint) => {
2451
2537
  const hasDescription = typeof checkpoint.description === "string" && checkpoint.description.trim().length > 0;
2452
2538
  return hasDescription || typeof checkpoint.step === "number";
@@ -2565,17 +2651,22 @@ async function materializeScreenshot(args) {
2565
2651
  if (!sourcePath) {
2566
2652
  return null;
2567
2653
  }
2654
+ const cachedTargetPath = args.screenshotCopies?.get(sourcePath);
2655
+ if (cachedTargetPath) {
2656
+ return rewriteImagePath(args.markdownFile, cachedTargetPath, args.outputDir, args.config.imagePathPrefix);
2657
+ }
2568
2658
  const extension = import_node_path16.default.extname(sourcePath) || ".png";
2569
2659
  const targetPath = import_node_path16.default.join(
2570
2660
  args.outputDir,
2571
2661
  args.config.screenshotsDir ?? "screenshots",
2572
- args.storySlug,
2573
- `${String(args.stepOrder).padStart(2, "0")}-${slugify2(args.checkpoint.name)}${extension}`
2662
+ args.screenshotDirSlug,
2663
+ `${args.screenshotFileSlug}${extension}`
2574
2664
  );
2575
2665
  try {
2576
2666
  if (args.config.copyScreenshots !== false) {
2577
2667
  await import_promises15.default.mkdir(import_node_path16.default.dirname(targetPath), { recursive: true });
2578
2668
  await import_promises15.default.copyFile(sourcePath, targetPath);
2669
+ args.screenshotCopies?.set(sourcePath, targetPath);
2579
2670
  args.writtenFiles.add(targetPath);
2580
2671
  return rewriteImagePath(args.markdownFile, targetPath, args.outputDir, args.config.imagePathPrefix);
2581
2672
  }
@@ -2595,10 +2686,31 @@ function orderedCheckpoints(checkpoints) {
2595
2686
  });
2596
2687
  }
2597
2688
  async function buildSteps(args) {
2598
- const checkpoints = orderedCheckpoints(args.run.checkpoints);
2689
+ let checkpoints;
2690
+ if (args.stepNames && args.stepNames.length > 0) {
2691
+ const byName = /* @__PURE__ */ new Map();
2692
+ for (const checkpoint of args.run.checkpoints) {
2693
+ if (byName.has(checkpoint.name)) {
2694
+ warn(`Duplicate checkpoint name "${checkpoint.name}" in "${args.run.title}". Using the latest capture for article generation.`);
2695
+ }
2696
+ byName.set(checkpoint.name, checkpoint);
2697
+ }
2698
+ checkpoints = args.stepNames.map((stepName) => {
2699
+ const checkpoint = byName.get(stepName);
2700
+ if (!checkpoint) {
2701
+ warn(`Markdown article step "${stepName}" was not captured in "${args.run.title}". Skipping step.`);
2702
+ return null;
2703
+ }
2704
+ return checkpoint;
2705
+ }).filter((checkpoint) => checkpoint !== null);
2706
+ } else {
2707
+ checkpoints = orderedCheckpoints(args.run.checkpoints).filter(
2708
+ (checkpoint) => !args.config.requireExplicitStep || typeof checkpoint.step === "number"
2709
+ );
2710
+ }
2599
2711
  const steps = [];
2600
2712
  for (const [index, checkpoint] of checkpoints.entries()) {
2601
- const order = typeof checkpoint.step === "number" ? checkpoint.step : index + 1;
2713
+ const order = args.stepNames ? index + 1 : typeof checkpoint.step === "number" ? checkpoint.step : index + 1;
2602
2714
  steps.push({
2603
2715
  checkpoint,
2604
2716
  order,
@@ -2607,12 +2719,13 @@ async function buildSteps(args) {
2607
2719
  imagePath: await materializeScreenshot({
2608
2720
  run: args.run,
2609
2721
  checkpoint,
2610
- storySlug: args.storySlug,
2611
- stepOrder: order,
2722
+ screenshotDirSlug: args.screenshotDirSlug,
2723
+ screenshotFileSlug: args.stepNames ? checkpoint.slug : `${String(order).padStart(2, "0")}-${slugify2(checkpoint.name)}`,
2612
2724
  outputDir: args.outputDir,
2613
2725
  markdownFile: args.markdownFile,
2614
2726
  config: args.config,
2615
- writtenFiles: args.writtenFiles
2727
+ writtenFiles: args.writtenFiles,
2728
+ screenshotCopies: args.screenshotCopies
2616
2729
  }),
2617
2730
  urlLabel: urlLabel(checkpoint.url),
2618
2731
  breadcrumbLabel: breadcrumbLabel(checkpoint.url),
@@ -2623,13 +2736,14 @@ async function buildSteps(args) {
2623
2736
  }
2624
2737
  function renderMarkdown(args) {
2625
2738
  const frontmatterFields = args.config.frontmatter === true || typeof args.config.frontmatter === "object" ? {
2626
- title: args.title,
2627
2739
  project: args.run.project,
2628
- testId: args.run.testId,
2629
2740
  tags: args.run.tags,
2741
+ ...args.config.frontmatter && typeof args.config.frontmatter === "object" ? args.config.frontmatter : {},
2742
+ ...args.article?.frontmatter ?? {},
2743
+ testId: args.run.testId,
2630
2744
  startedAt: args.run.startedAt,
2631
2745
  generatedAt: args.generatedAt,
2632
- ...args.config.frontmatter && typeof args.config.frontmatter === "object" ? args.config.frontmatter : {}
2746
+ title: args.title
2633
2747
  } : null;
2634
2748
  const sections = args.steps.map((step) => {
2635
2749
  const lines = [`## Step ${step.order}: ${step.heading}`, ""];
@@ -2649,6 +2763,7 @@ function renderMarkdown(args) {
2649
2763
  const parts = [
2650
2764
  frontmatterFields ? serializeFrontmatter(frontmatterFields) : "",
2651
2765
  `# ${args.title}`,
2766
+ args.description?.trim() ?? "",
2652
2767
  args.config.header ? args.config.header.trim() : "",
2653
2768
  sections,
2654
2769
  args.config.footer ? args.config.footer.trim() : ""
@@ -2656,6 +2771,42 @@ function renderMarkdown(args) {
2656
2771
  return `${parts.join("\n\n")}
2657
2772
  `;
2658
2773
  }
2774
+ function resolveArticles(run) {
2775
+ const multiArticles = (run.articles ?? []).filter((article) => Array.isArray(article.steps));
2776
+ if (run.articles && multiArticles.length === 0) {
2777
+ warn(`Markdown reporter received an empty articles array for "${run.title}". Falling back to the default single-article output.`);
2778
+ }
2779
+ if (multiArticles.length === 0) {
2780
+ return [
2781
+ {
2782
+ title: articleTitle(run),
2783
+ description: articleDescription(run),
2784
+ slug: articleSlug(run),
2785
+ metadata: run.article,
2786
+ screenshotDirSlug: articleSlug(run)
2787
+ }
2788
+ ];
2789
+ }
2790
+ const usedSlugs = /* @__PURE__ */ new Set();
2791
+ const screenshotDirSlug = slugify2(stripTags(run.title));
2792
+ return multiArticles.map((article, index) => {
2793
+ const fallbackSlug = `${screenshotDirSlug}-${index + 1}`;
2794
+ const baseSlug = slugify2(article.slug?.trim() || fallbackSlug);
2795
+ const uniqueSlug = uniqueArticleSlug(baseSlug, usedSlugs);
2796
+ if (uniqueSlug !== baseSlug) {
2797
+ warn(`Markdown article slug collision for "${article.title ?? run.title}" resolved as "${uniqueSlug}".`);
2798
+ }
2799
+ usedSlugs.add(uniqueSlug);
2800
+ return {
2801
+ title: article.title?.trim() || stripTags(run.title),
2802
+ description: article.description?.trim() || null,
2803
+ slug: uniqueSlug,
2804
+ metadata: article,
2805
+ stepNames: [...article.steps],
2806
+ screenshotDirSlug
2807
+ };
2808
+ });
2809
+ }
2659
2810
  var markdownReporter = {
2660
2811
  name: "markdown",
2661
2812
  description: "Generates one Markdown help article per captured story.",
@@ -2667,37 +2818,56 @@ var markdownReporter = {
2667
2818
  const stories = groupByStory(context.runs);
2668
2819
  const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
2669
2820
  const writtenFiles = /* @__PURE__ */ new Set();
2821
+ const usedStorySlugs = /* @__PURE__ */ new Set();
2822
+ const screenshotCopies = /* @__PURE__ */ new Map();
2670
2823
  let articleCount = 0;
2671
2824
  for (const [storyTitle, runs] of stories) {
2672
2825
  const primaryRun = choosePrimaryRun(runs, config.preferredProject);
2673
2826
  if (!primaryRun || !shouldIncludeRun(primaryRun, config)) {
2674
2827
  continue;
2675
2828
  }
2676
- const title = stripTags(storyTitle);
2677
- const storySlug = slugify2(title);
2678
- const markdownFile = import_node_path16.default.join(context.outputDir, config.storiesDir ?? ".", `${storySlug}.md`);
2679
- const steps = await buildSteps({
2680
- run: primaryRun,
2681
- storySlug,
2682
- outputDir: context.outputDir,
2683
- markdownFile,
2684
- config,
2685
- writtenFiles
2686
- });
2687
- await import_promises15.default.mkdir(import_node_path16.default.dirname(markdownFile), { recursive: true });
2688
- await import_promises15.default.writeFile(
2689
- markdownFile,
2690
- renderMarkdown({
2691
- title,
2692
- steps,
2829
+ for (const article of resolveArticles(primaryRun)) {
2830
+ let storySlug = article.slug;
2831
+ if (usedStorySlugs.has(storySlug)) {
2832
+ let index = 2;
2833
+ while (usedStorySlugs.has(`${article.slug}-${index}`)) {
2834
+ index += 1;
2835
+ }
2836
+ storySlug = `${article.slug}-${index}`;
2837
+ warn(`Markdown article slug collision for "${article.title || storyTitle}" resolved as "${storySlug}".`);
2838
+ }
2839
+ usedStorySlugs.add(storySlug);
2840
+ const markdownFile = import_node_path16.default.join(context.outputDir, config.storiesDir ?? ".", `${storySlug}.md`);
2841
+ const steps = await buildSteps({
2693
2842
  run: primaryRun,
2843
+ stepNames: article.stepNames,
2844
+ screenshotDirSlug: article.screenshotDirSlug,
2845
+ outputDir: context.outputDir,
2846
+ markdownFile,
2694
2847
  config,
2695
- generatedAt
2696
- }),
2697
- "utf8"
2698
- );
2699
- writtenFiles.add(markdownFile);
2700
- articleCount += 1;
2848
+ writtenFiles,
2849
+ screenshotCopies
2850
+ });
2851
+ if (steps.length === 0) {
2852
+ continue;
2853
+ }
2854
+ await import_promises15.default.mkdir(import_node_path16.default.dirname(markdownFile), { recursive: true });
2855
+ await import_promises15.default.writeFile(
2856
+ markdownFile,
2857
+ renderMarkdown({
2858
+ title: article.title,
2859
+ description: article.description,
2860
+ steps,
2861
+ run: primaryRun,
2862
+ article: article.metadata,
2863
+ config,
2864
+ generatedAt
2865
+ }),
2866
+ "utf8"
2867
+ );
2868
+ writtenFiles.add(markdownFile);
2869
+ articleCount += 1;
2870
+ }
2701
2871
  }
2702
2872
  return {
2703
2873
  files: [...writtenFiles],
@@ -3163,6 +3333,8 @@ function toRunRecord(manifest, sourceManifestPath) {
3163
3333
  project: manifest.project,
3164
3334
  testId: manifest.testId,
3165
3335
  title: manifest.title,
3336
+ ...manifest.article ? { article: manifest.article } : {},
3337
+ ...manifest.articles ? { articles: manifest.articles } : {},
3166
3338
  tags: manifest.tags,
3167
3339
  startedAt: manifest.startedAt,
3168
3340
  checkpoints: manifest.checkpoints
@@ -3174,6 +3346,8 @@ function toManifest(run) {
3174
3346
  project: run.project,
3175
3347
  testId: run.testId,
3176
3348
  title: run.title,
3349
+ ...run.article ? { article: run.article } : {},
3350
+ ...run.articles ? { articles: run.articles } : {},
3177
3351
  tags: run.tags,
3178
3352
  startedAt: run.startedAt,
3179
3353
  checkpoints: run.checkpoints