<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Luiz Figueiredo</title>
    <description>The latest articles on DEV Community by Luiz Figueiredo (@figueiredoluiz).</description>
    <link>https://dev.to/figueiredoluiz</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F993235%2F6378610d-df9d-434e-b662-aa3329a35337.png</url>
      <title>DEV Community: Luiz Figueiredo</title>
      <link>https://dev.to/figueiredoluiz</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/figueiredoluiz"/>
    <language>en</language>
    <item>
      <title>AI-Generated Tests: How Many Actually Test Different Behaviors?</title>
      <dc:creator>Luiz Figueiredo</dc:creator>
      <pubDate>Sun, 18 Oct 2026 11:57:00 +0000</pubDate>
      <link>https://dev.to/figueiredoluiz/ai-generated-tests-how-many-actually-test-different-behaviors-102g</link>
      <guid>https://dev.to/figueiredoluiz/ai-generated-tests-how-many-actually-test-different-behaviors-102g</guid>
      <description>&lt;h2&gt;
  
  
  Why I ran this
&lt;/h2&gt;

&lt;p&gt;A coding agent can write a hundred passing tests before you finish your coffee. The test count goes up, the coverage report turns green, and the pull request looks thorough. What nobody tells you is how many of those tests check something the others don't.&lt;/p&gt;

&lt;p&gt;I maintain &lt;a href="https://github.com/figueiredoluiz/vitest-dedup" rel="noopener noreferrer"&gt;vitest-dedup&lt;/a&gt;, a static analyzer that looks for duplicate Vitest tests, so I have an obvious interest here. That is also why I wanted to measure instead of assume. The hard part of this problem is not finding tests that look alike. It is not mistaking similar tests for equivalent ones, because the remedies are opposite: you delete a duplicate, but deleting the "similar" test that actually covers a different code path is how you lose coverage while the suite stays green.&lt;/p&gt;

&lt;p&gt;So I set up a small experiment. A coding agent wrote 139 tests for two small modules, in two separate sessions. I classified every suspicious pair by hand and compared three detectors: a naive token-based clone detector, vitest-dedup 0.4.0, and vitest-dedup 0.5.0, which I released on October 17. Everything is reproducible; the commands are at the end. The answer was not the one I went in expecting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four ways two tests can be "the same"
&lt;/h2&gt;

&lt;p&gt;Before counting anything, it helps to name what we are counting. Two tests can relate in four ways, and each one calls for a different action.&lt;/p&gt;

&lt;p&gt;The first is an identical test. Same body, same context, maybe a different title or renamed variables. One copy can go.&lt;/p&gt;

&lt;p&gt;The second is a near-duplicate. The bodies differ, but they perform the same behavior in a way a tool cannot prove equivalent. Someone has to look.&lt;/p&gt;

&lt;p&gt;The third is a parameterization candidate. Same structure, different literal values. Both cases are probably needed, and they belong in one &lt;code&gt;test.each&lt;/code&gt; table. Deleting one loses coverage.&lt;/p&gt;

&lt;p&gt;The fourth is the one that causes trouble: tests that look similar, or even textually identical, but exercise different behavior. A tool that flags these is not just noisy. It is suggesting a deletion that removes a real check.&lt;/p&gt;

&lt;p&gt;vitest-dedup encodes this split in its output. Every finding carries a verdict: &lt;code&gt;redundant&lt;/code&gt; (one copy can go), &lt;code&gt;consolidatable&lt;/code&gt; (merge into a table, keep every row), or a review prompt. That verdict matters more than the finding count, as the experiment shows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where a naive detector gets it wrong
&lt;/h2&gt;

&lt;p&gt;The classic approach to clone detection works on tokens. Type-1 clones are token-for-token identical; type-2 clones are identical after replacing identifiers and literals with placeholders. I wrote a 60-line detector that does both on test callbacks, ignoring titles. It is a fair stand-in for what you get if you point a generic clone detector, or a quick script, at a test folder.&lt;/p&gt;

&lt;p&gt;The examples in this section are hand-written traps, built to separate "similar" from "equivalent". The agent-written corpus comes later.&lt;/p&gt;

&lt;p&gt;Here is the first trap. Two tests with identical text:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;frozen clock&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;clock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;now&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;keeps the bucket full&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TokenBucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;clock&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;available&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;clock that jumps an hour per read&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;clock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;now&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="nx"&gt;_600_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;keeps the bucket full&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TokenBucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;clock&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;available&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first test checks that nothing refills when time stands still. The second checks that a huge refill is clamped at capacity. Both assert &lt;code&gt;3&lt;/code&gt;, for entirely different reasons. The naive detector reports them as exact duplicates. The bodies are the same text, but &lt;code&gt;clock&lt;/code&gt; is a different binding in each one, and that binding is the test.&lt;/p&gt;

&lt;p&gt;The same thing happens with hooks. Two &lt;code&gt;it&lt;/code&gt; blocks with identical bodies, one under a &lt;code&gt;beforeEach&lt;/code&gt; that creates &lt;code&gt;new Cart(0.1)&lt;/code&gt; and one under &lt;code&gt;new Cart()&lt;/code&gt;, are different tests. Type-1 flags them as well.&lt;/p&gt;

&lt;p&gt;vitest-dedup 0.5.0 reports neither pair. Captured variables participate in the comparison at the place they are used, and they keep track of which enclosing variable they came from, even when the initial values happen to match. Hooks, mocks and fixtures are part of a test's context, so identical bodies under different contexts are not compared as equal.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgu056jpf310k8tczl43a.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgu056jpf310k8tczl43a.gif" alt="The naive detector flags the captured-clock and hook pairs as exact duplicates; vitest-dedup 0.5.0 reports only the renamed duplicate" width="800" height="375"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The only pair vitest-dedup reports is the real duplicate: identical tests where one renamed &lt;code&gt;cart&lt;/code&gt; to &lt;code&gt;basket&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The opposite failure is just as common. Agents rename things. Here is a pair where everything except the behavior changed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;emits one event per order&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;events&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SpyBus&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SpyBus&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nx"&gt;bus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;order.placed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;order.placed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;publishes the order.placed event&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;published&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RecordingBus&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;published&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;recorder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RecordingBus&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nx"&gt;recorder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;order.placed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;published&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;order.placed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A type-1 comparison misses this completely. vitest-dedup 0.4.0 saw the same sequence of behavior but could not prove the local classes equivalent, so it filed a &lt;code&gt;near-duplicate-test&lt;/code&gt; warning for review. 0.5.0 models local classes: fields, methods, constructors, private names and what each instance captures. Consistent renaming stays comparable, so the verdict becomes a proven &lt;code&gt;duplicate-test&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frofqk3mhfbskyh4es8p2.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frofqk3mhfbskyh4es8p2.gif" alt="vitest-dedup 0.4.0 reports a near-duplicate warning for the renamed local classes; 0.5.0 reports a duplicate-test error" width="799" height="310"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why local classes and closures are the hard part
&lt;/h2&gt;

&lt;p&gt;Test code is full of small local classes and closures: fake clocks, spy buses, recording repositories. They are exactly where "looks the same" and "is the same" drift apart.&lt;/p&gt;

&lt;p&gt;A local class is not just its methods. Two classes with identical method text are still two different class objects, and a test that checks identity or &lt;code&gt;instanceof&lt;/code&gt; can tell them apart.&lt;/p&gt;

&lt;p&gt;A constructor can have effects at definition time and at construction time, and those are different moments. Collapsing them makes two tests look equal when one of them observes a side effect the other never triggers.&lt;/p&gt;

&lt;p&gt;Property kinds matter too. These two objects print almost the same:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fromGetter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;// fromGetter.limit === 3&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fromMethod&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;// fromMethod.limit is a function&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two tests that differ only in which of these they build were a &lt;code&gt;duplicate-test&lt;/code&gt; error in 0.4.0. 0.5.0 keeps property descriptor kinds apart and reports nothing.&lt;/p&gt;

&lt;p&gt;Closures add mutable state. If a test captures an array and a helper pushes into it, the comparison has to follow the history of that array, not only its initializer. If the captured object escapes into code the analyzer cannot see, any claim about it would be a guess.&lt;/p&gt;

&lt;p&gt;That last point is the design decision I care most about. When the analyzer cannot model something, such as a prototype patched after an instance exists, a decorator, or reflective access, the dependent test is marked unresolved and excluded from comparison instead of being compared on surface text. The run is flagged as incomplete when that happens, because a missing finding is then no evidence of a missing duplicate. Unrelated tests in the same file still compare normally.&lt;/p&gt;

&lt;h2&gt;
  
  
  The experiment
&lt;/h2&gt;

&lt;p&gt;Two small TypeScript modules: a shopping cart (items, coupons, tax, rounding to cents) and a token bucket rate limiter with an injectable clock. Each is 40 to 60 lines.&lt;/p&gt;

&lt;p&gt;For each module, a fresh coding agent session (Claude) got a deliberately ordinary prompt: write a comprehensive Vitest suite for this module and make sure it passes. Then a second fresh session got the follow-up every team has written at some point: a reviewer says the tests are not thorough enough, add edge cases, keep the existing tests. I did not edit the generated files.&lt;/p&gt;

&lt;p&gt;The result was 139 passing tests: 93 from the first sessions, 46 from the second. I added 10 hand-written trap tests, like the ones above, in a separate file.&lt;/p&gt;

&lt;p&gt;Then I classified every pair that any detector flagged, and read both suites end to end looking for redundancy that nothing flagged. The question for each pair was simple: if one test were deleted, would any behavior lose its only check?&lt;/p&gt;

&lt;h2&gt;
  
  
  What the detectors found
&lt;/h2&gt;

&lt;p&gt;On the 139 agent-written tests:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Naive type-1&lt;/th&gt;
&lt;th&gt;Naive type-2&lt;/th&gt;
&lt;th&gt;vitest-dedup 0.4.0&lt;/th&gt;
&lt;th&gt;vitest-dedup 0.5.0&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Exact or renamed duplicates reported&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;can't tell them apart from literal variants&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Similarity groups or findings&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;11 groups (34 tests)&lt;/td&gt;
&lt;td&gt;19 findings&lt;/td&gt;
&lt;td&gt;18 findings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Groups that mix unrelated tests&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;3 (they call different methods)&lt;/td&gt;
&lt;td&gt;1 (a near-duplicate that isn't)&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Table suggestions I would reject&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The first surprise: the agent did not copy and paste. There was not a single exact or renamed duplicate in 139 tests. Its repetition was shape repetition, the same arrange, act, assert with different numbers.&lt;/p&gt;

&lt;p&gt;vitest-dedup 0.5.0 reported 18 &lt;code&gt;parameterization-candidate&lt;/code&gt; findings covering 31 tests, all as warnings with the remedy "consolidate into one &lt;code&gt;test.each&lt;/code&gt; table that keeps every case as its own row". I agreed with 15, found one debatable, and rejected two.&lt;/p&gt;

&lt;p&gt;A typical accepted one: three tax tests that differ only in rate, price and expected total.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// before: three tests with the same shape&lt;/span&gt;
&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;applies tax to the subtotal&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Cart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;total&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;110&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="c1"&gt;// ...'doubles the amount at a tax rate of 1', 'rounds the final total to cents'&lt;/span&gt;

&lt;span class="c1"&gt;// after: one table, every case kept as a named row&lt;/span&gt;
&lt;span class="nx"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;case&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;applies tax to the subtotal&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;taxRate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;110&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;case&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;doubles the amount at a tax rate of 1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;taxRate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;case&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rounds the final total to cents&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;taxRate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.0825&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;19.99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;21.64&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;])(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;$case&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;taxRate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expected&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Cart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;taxRate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;total&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The suite still runs 149 tests, all passing, and the two findings that involved these tests disappear.&lt;/p&gt;

&lt;p&gt;The two I rejected are instructive. In one pair the cart receives SKUs &lt;code&gt;'a'&lt;/code&gt; and &lt;code&gt;'a'&lt;/code&gt;; in the other, &lt;code&gt;'a'&lt;/code&gt; and &lt;code&gt;'b'&lt;/code&gt;. Structurally that is a literal difference. Behaviorally it is the difference between merging two lines and summing two lines. A table would technically work, but it would hide the fact that the SKU column selects a code path. No finding claimed either test was deletable, so the cost of being wrong here is a bad refactor suggestion, not lost coverage.&lt;/p&gt;

&lt;p&gt;0.4.0 reported the same 18, plus one near-duplicate between "accepts fractional capacity" and "can take exactly the full capacity". They are different behaviors; 0.5.0 no longer reports the pair.&lt;/p&gt;

&lt;p&gt;The naive type-2 detector found roughly the same clusters, but replacing every identifier with a placeholder made &lt;code&gt;cart.total()&lt;/code&gt; and &lt;code&gt;cart.discount()&lt;/code&gt; look identical, so three of its eleven groups mixed tests that call different methods. It also cannot tell "these differ in a literal" from "these differ in which function they call", which is the difference between consolidate and leave alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  The redundancy nobody flagged
&lt;/h2&gt;

&lt;p&gt;I counted 11 tests that could be deleted without losing a check, about 8% of the suite. That number is smaller than I expected, and where those tests come from is more interesting than the count.&lt;/p&gt;

&lt;p&gt;Some are subsumption: a later test performs the same steps with more checkpoints. The second session wrote a test for a clock moving backwards that walks through four timestamps, which makes two earlier backwards-clock tests from the first session redundant. They use different numbers and a different order, so no structural comparison connects them.&lt;/p&gt;

&lt;p&gt;Some are degenerate dimensions. Asked for "interactions between features", the second session wrote tests like "is 0 with a 100% coupon and a tax rate of 1". Tax multiplies a total that is already zero, so the extra dimension cannot change the result; the test re-checks the 100% coupon case. Three tests followed this pattern, two of them from the second session. Seeing that requires knowing the arithmetic, which is outside what a static comparison of test bodies can claim. A rule of thumb for review: if the extra variable only multiplies or offsets a value the test pins at zero, the test re-checks its base case.&lt;/p&gt;

&lt;p&gt;And three came from vitest-dedup itself, through a rule I ship off by default. &lt;code&gt;subsumed-test&lt;/code&gt; reports when one test's behavior is a strict ordered subsequence of another's. With it enabled, it flagged three pairs, and all three hold up. Two I had missed by hand: "adds a new item" performs exactly the add and the two assertions that "does not change the cart when an add is rejected" also performs, so it adds nothing. The third I first rejected, because a negative fixed coupon and a negative percent coupon looked like different input classes. They reach the same &lt;code&gt;value &amp;lt; 0&lt;/code&gt; branch, and the percent test also asserts the message, so by my own criterion the fixed one is redundant. A reader pointed this out in the comments; I had judged that pair by the spec and the others by code path. The rule stays off by default because its findings are often distinct tests rather than redundant ones; this small corpus happened to be clean. It is a good prompt for a review session and a bad CI gate.&lt;/p&gt;

&lt;p&gt;In this run, five of the seven redundancy groups I found by hand involve a test from the second session. "Add more edge cases" produced as much re-testing as new testing. If you run agents in multiple passes over the same file, that is where to look.&lt;/p&gt;

&lt;h2&gt;
  
  
  So, how many?
&lt;/h2&gt;

&lt;p&gt;About 92% of the agent's tests checked a behavior no other test covered, in this run. The number I would actually watch is a different one: 31 tests that wanted to be tables, and 11 that a reviewer would delete only after reading them carefully. A tool can find the first group on every pull request. The second still needs a person, and the most useful thing a tool can do there is say clearly which findings are proofs and which are questions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations worth knowing
&lt;/h2&gt;

&lt;p&gt;vitest-dedup is static. It reads configuration and test files without importing or running them, which is what makes it safe to run on any pull request, and also what bounds it. When I cut 0.5.0, I chose to ship it with three known false-positive cases still open rather than hold back the constructor and captured-state work: asynchronous iteration, captured code that is opaque or too deep to follow, and the way parameterized object keys are encoded. I listed them in the &lt;a href="https://github.com/figueiredoluiz/vitest-dedup/releases/tag/v0.5.0" rel="noopener noreferrer"&gt;0.5.0 release notes&lt;/a&gt; so nobody has to discover them the hard way. The first one, &lt;code&gt;for await&lt;/code&gt; loops, is already fixed on &lt;code&gt;main&lt;/code&gt; and will ship in the next version.&lt;/p&gt;

&lt;p&gt;My experiment is small and has an obvious bias: one agent, two modules, and I wrote both the tool and the classification. Treat the percentages as a description of this corpus, not a law. The raw outputs and my &lt;a href="https://github.com/figueiredoluiz/vitest-dedup-materials/blob/main/01-agent-written-tests/results/manual-classification.md" rel="noopener noreferrer"&gt;per-pair verdicts&lt;/a&gt; are in the repository so you can disagree with specific calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gating only on new duplication
&lt;/h2&gt;

&lt;p&gt;An existing suite usually cannot turn on a zero-tolerance gate at once. The baseline workflow records today's findings once with &lt;code&gt;--update-baseline&lt;/code&gt;, then every later run with &lt;code&gt;--baseline dedup-baseline.json&lt;/code&gt; gates only on what is added.&lt;/p&gt;

&lt;p&gt;To test it, I recorded a baseline for the experiment and applied a change of the kind an agent produces when it does not see an earlier test: a renamed copy of an existing cart test in a new &lt;code&gt;describe&lt;/code&gt; block. The gate failed on that one new duplicate and kept the 19 recorded findings visible but out of the decision:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffg2p5c7ddbu3vxcvdf4a.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffg2p5c7ddbu3vxcvdf4a.gif" alt="Recording a baseline, applying an agent's new test, and the gate failing on the one new duplicate" width="799" height="522"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;jsonl&lt;/code&gt; reporter is designed to be read by an agent, which closes the loop: the agent that wrote the tests can be the one that consolidates them. To run the gate in CI, the &lt;a href="https://github.com/figueiredoluiz/vitest-dedup#github-action" rel="noopener noreferrer"&gt;GitHub Action&lt;/a&gt; takes the same baseline as an input and verifies the binary's checksum and build provenance before running it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproducing it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm ci                                            &lt;span class="c"&gt;# Node 24 LTS&lt;/span&gt;
npx vitest run                                    &lt;span class="c"&gt;# 149 tests&lt;/span&gt;
npx vitest-dedup &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--no-metrics&lt;/span&gt;                   &lt;span class="c"&gt;# default rules&lt;/span&gt;
npx vitest-dedup &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--no-metrics&lt;/span&gt; &lt;span class="nt"&gt;--rule&lt;/span&gt; subsumed-test&lt;span class="o"&gt;=&lt;/span&gt;warning
node scripts/naive-dedup.mjs tests/&lt;span class="k"&gt;*&lt;/span&gt;.test.ts      &lt;span class="c"&gt;# naive token detector&lt;/span&gt;
npx vitest-dedup demo &lt;span class="nt"&gt;--no-metrics&lt;/span&gt;                &lt;span class="c"&gt;# renamed local classes, 0.5.0&lt;/span&gt;
npx vitest-dedup@0.4.0 demo &lt;span class="nt"&gt;--no-metrics&lt;/span&gt;          &lt;span class="c"&gt;# same pair, 0.4.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The modules, the generated tests, the prompts, the naive detector and every raw output are in &lt;a href="https://github.com/figueiredoluiz/vitest-dedup-materials/tree/main/01-agent-written-tests" rel="noopener noreferrer"&gt;vitest-dedup-materials&lt;/a&gt;. vitest-dedup is MIT licensed and written in Rust; &lt;code&gt;npx vitest-dedup .&lt;/code&gt; runs it without installing anything.&lt;/p&gt;

&lt;p&gt;If you run it on a suite your agents wrote, I would like to know which of the four kinds you find most. My guess, after this run, is that the tables will outnumber the copies.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>vitest</category>
      <category>ai</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Your duplication report says 0%. Your Cucumber steps disagree.</title>
      <dc:creator>Luiz Figueiredo</dc:creator>
      <pubDate>Wed, 14 Oct 2026 18:31:44 +0000</pubDate>
      <link>https://dev.to/figueiredoluiz/your-duplication-report-says-0-your-cucumber-steps-disagree-4h2g</link>
      <guid>https://dev.to/figueiredoluiz/your-duplication-report-says-0-your-cucumber-steps-disagree-4h2g</guid>
      <description>&lt;p&gt;Every Cucumber suite I've reviewed grows the same way. A new scenario needs a step, nobody is quite sure whether a matching one already exists, and so a slightly different one gets added. Multiply that by a few engineers and a few months, and the step library quietly fills up with near-copies.&lt;/p&gt;

&lt;p&gt;After years of pointing this out in code reviews, I built a tool to do it for me. This post covers why I built CukeDedup, what it checks, and what running it against 92 open-source repositories taught me about my own assumptions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three ways to log in
&lt;/h2&gt;

&lt;p&gt;Here's a pattern I've repeatedly encountered:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nc"&gt;Given&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;the user is logged in&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;login&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nc"&gt;Given&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;the user is authenticated&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;login&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nc"&gt;Given&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;an authenticated user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;login&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All three can work. A generic copy-paste detector might report no duplicated blocks. And yet, do you really need three of them?&lt;/p&gt;

&lt;h2&gt;
  
  
  Meet CukeDedup
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/figueiredoluiz/cuke-dedup" rel="noopener noreferrer"&gt;CukeDedup&lt;/a&gt; is an open-source static analyzer that finds duplicate, ambiguous, reusable and unused Cucumber step definitions. No config file, one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx cuke-dedup &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fci2qcvt85bfc920ag0op.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fci2qcvt85bfc920ag0op.gif" alt="Running npx cuke-dedup . on a small project: two duplicate-handler errors, one parameterization candidate and one unused step definition" width="800" height="833"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It works with Cucumber.js, Playwright-BDD, Cypress Cucumber, Jest-Cucumber and the Vitest Cucumber Plugin, and reads step definitions written in JavaScript or TypeScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I built it
&lt;/h2&gt;

&lt;p&gt;As a frontend lead, I helped introduce end-to-end testing. We picked Cucumber and Gherkin on purpose: I wanted QA and product people to read, write and argue about scenarios without touching the automation code.&lt;/p&gt;

&lt;p&gt;That collaboration model worked in my experience. But an old problem remained: &lt;strong&gt;writing a new test is easy, keeping a large suite organized is not.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;People pick up Playwright or Cypress quickly. What takes time is knowing what already exists. Before adding a step, you need to know whether a similar one is already there, or whether two existing ones could become one parameterized step. Experienced engineers can often spot this quickly. Someone new to the codebase may not, so the first time they hear about it is in code review.&lt;/p&gt;

&lt;p&gt;As the reviewer, that's where my time went. I'm a big believer in linters for exactly this reason: if a rule matters, a tool should check it before the pull request, not a person after it. It's faster for the author, less tedious for the reviewer, and rules stop depending on who happens to review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code duplication is not behavior duplication
&lt;/h2&gt;

&lt;p&gt;On one project I combined ESLint, Danger.js and &lt;a href="https://github.com/kucherenko/jscpd" rel="noopener noreferrer"&gt;jscpd&lt;/a&gt;. jscpd was great at catching copy-pasted code. It could not catch this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nc"&gt;When&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;I create a red project&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createProject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;red&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nc"&gt;When&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;I create a blue project&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createProject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;blue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;which might be better expressed as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nc"&gt;When&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;I create a {word} project&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nx"&gt;color&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createProject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The opposite happens too: two steps can read almost the same and still mean different things in your domain. Generic duplicate detectors look at code. I needed something that looks at both the step text and the implementation behind it.&lt;/p&gt;

&lt;p&gt;It started as a pile of scripts on my machine. When I joined the effort to build a new E2E setup at my current job, I knew I'd hit the same problems again, so I turned the scripts into a proper tool. I wanted it open source from day one, and I wanted to finally build something non-trivial in Rust: a tool you run all the time has to be fast and light on memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three things I cared about
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Zero config.&lt;/strong&gt; jscpd gets this right: you type &lt;code&gt;jscpd .&lt;/code&gt; and you learn something. Nobody looking for duplicates wants to learn a new configuration format first. CukeDedup respects &lt;code&gt;.gitignore&lt;/code&gt;, recognizes common project layouts and the Cucumber.js, Playwright-BDD and Cypress configs. Configuration exists for exceptions, not as an entry fee.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Evidence, not verdicts.&lt;/strong&gt; "These two look similar" isn't useful. Findings include source locations, relevant matcher or handler evidence, and a suggested action when applicable. The decision stays with you: similar code can represent different domain concepts, and not every abstraction is a good one. To be clear, it doesn't understand English. It doesn't know &lt;code&gt;logged in&lt;/code&gt; means &lt;code&gt;authenticated&lt;/code&gt;; it can report that their handlers are equivalent under its supported static comparison rules.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Saying "I don't know."&lt;/strong&gt; Static analysis can't resolve everything: dynamic config, wrappers, imports it can't follow. When that happens CukeDedup marks the run as &lt;em&gt;incomplete&lt;/em&gt; instead of reporting a clean result. Zero findings is not zero problems, and &lt;code&gt;--fail-on-incomplete&lt;/code&gt; lets CI treat that difference seriously.&lt;/p&gt;

&lt;p&gt;The day-to-day loop is the same command again:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6cmqv7k29fsqtqeoub3x.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6cmqv7k29fsqtqeoub3x.gif" alt="A git diff merging the red and blue project steps into one {word} step, then cuke-dedup . running again with 0 findings" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What 92 open-source repos taught me
&lt;/h2&gt;

&lt;p&gt;A tool tested only on examples you wrote yourself will always look correct. So I ran it against 92 public JavaScript/TypeScript repositories that use Cucumber-style tests. This was not a representative sample of the ecosystem; it was to meet code I'd never seen. Many of the most useful discoveries were limitations in CukeDedup itself, rather than confirmed defects in the projects I scanned.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Consistent naming looks like duplication.&lt;/strong&gt; In one project, every &lt;code&gt;near-duplicate-step&lt;/code&gt; finding had matcher similarity above 0.90, because the team used a tidy naming convention for steps acting on different elements. Text similarity alone wasn't enough, so I raised the required handler similarity from 0.50 to 0.70 (configurable). Findings for that rule dropped by about half in the measured corpus, while the expected cases in the regression suite continued to pass. That is a reduction in findings, not a measured 50% improvement in precision.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Erasing literals makes unrelated code look alike.&lt;/strong&gt; Tiny handlers that just call &lt;code&gt;cy.log(...)&lt;/code&gt; were being suggested as parameterization candidates. Requiring compatible matchers, no negation conflict (&lt;code&gt;not&lt;/code&gt;, &lt;code&gt;un-&lt;/code&gt;…) and a single contiguous difference reduced that rule's findings by about 40% in the measured corpus, with expected regression cases preserved. I don't want a tool that pushes abstraction for its own sake.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Ambiguous" was really a scope problem.&lt;/strong&gt; Two projects produced 2,263 and 1,563 ambiguous-step findings when scanned at a root that combined separate runtime suites. They ran their suites as separate runtime units; I was analyzing them as one registry. When the affected suites were scanned within their actual runtime boundaries, those cross-suite ambiguities disappeared. The fix wasn't an algorithm, it was understanding how the tests actually run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A bundled jQuery made it hang for 20 minutes.&lt;/strong&gt; File size wasn't the cause: an 89 KB real file hung while a 1 MB synthetic one finished in seconds. The culprit was superlinear work in the analysis itself. After the algorithmic fix, that jQuery input took 0.6 seconds in the recorded measurement. Separately, files excluded by the generated-content filter are reported, and the run is marked incomplete.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When an issue can be isolated, I try to turn it into a minimal regression case in a corpus of expected behavior, with both "must find" and "must not find" cases. It describes what the tool &lt;em&gt;should&lt;/em&gt; do rather than freezing whatever it currently does, and I deliberately break the code to check that each test actually fails. A test that stays green when you break things isn't protecting anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using it on a real project
&lt;/h2&gt;

&lt;p&gt;Install from npm (prebuilt native binaries) or build from source with Cargo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--save-dev&lt;/span&gt; cuke-dedup
&lt;span class="c"&gt;# or&lt;/span&gt;
cargo &lt;span class="nb"&gt;install &lt;/span&gt;cuke-dedup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reports come as terminal output, HTML (searchable, side-by-side comparison), JSON, JSONL and SARIF for GitHub code scanning.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F80i4v7ei1lkjywrmr46x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F80i4v7ei1lkjywrmr46x.png" alt="CukeDedup HTML report showing two step definitions side by side, with the differing words highlighted and the same login handler" width="799" height="556"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Legacy suites: start with a baseline.&lt;/strong&gt; Nobody can clean up years of duplication before adopting a tool. Record what exists today, then configure the gate to fail on new findings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cuke-dedup &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--baseline&lt;/span&gt; baseline.json &lt;span class="nt"&gt;--update-baseline&lt;/span&gt;
cuke-dedup &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--baseline&lt;/span&gt; baseline.json &lt;span class="nt"&gt;--fail-on-new&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhun4jw0vtz2ggwpper0u.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhun4jw0vtz2ggwpper0u.gif" alt="Recording a baseline, adding one new step, then cuke-dedup with --fail-on-new reporting only the new duplicate" width="800" height="427"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The first goal isn't paying off the debt. It's not adding to it.&lt;/p&gt;

&lt;p&gt;In CI, there's a GitHub Action that downloads the native binary and verifies its checksum and build provenance before running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/cuke-dedup.yml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CukeDedup&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pull_request&lt;/span&gt;

&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;cuke-dedup&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1&lt;/span&gt; &lt;span class="c1"&gt;# v7&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;fetch-depth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
          &lt;span class="na"&gt;persist-credentials&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;figueiredoluiz/cuke-dedup@v0.12.0&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;threshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What about AI agents?
&lt;/h2&gt;

&lt;p&gt;Coding agents are now good at writing tests and proposing refactors. That makes cheap, deterministic guardrails more valuable, not less. There's no reason to have an agent read hundreds of files to find a duplicate the static analyzer can identify without invoking a model. Let the tool produce the evidence and let the agent spend its effort on the judgment calls.&lt;/p&gt;

&lt;p&gt;CukeDedup can stream findings as JSONL, and ships an &lt;a href="https://github.com/figueiredoluiz/cuke-dedup/blob/main/skills/cuke-dedup/SKILL.md" rel="noopener noreferrer"&gt;Agent Skill&lt;/a&gt; that tells compatible agents to treat findings as evidence to investigate, not as orders to merge everything that looks similar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skills add figueiredoluiz/cuke-dedup &lt;span class="nt"&gt;--skill&lt;/span&gt; cuke-dedup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;At the time of writing, CukeDedup is at v0.12.0. JavaScript and TypeScript are the main focus, and Cucumber-Ruby support is available as an experimental opt-in. Next up: more frameworks, possibly Java and PHP, fewer false positives, and publishing what I learn from scanning open-source suites.&lt;/p&gt;

&lt;p&gt;I'd love for this to become a community project rather than just mine. False-positive reports, step-definition patterns it doesn't understand yet, feature ideas and pull requests are all welcome. Even a case where it gets things wrong helps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I'm curious: how does your team keep step definitions from multiplying?&lt;/strong&gt; Naming conventions, review checklists, a shared steps library, or just living with it? Tell me in the comments.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/figueiredoluiz/cuke-dedup" rel="noopener noreferrer"&gt;https://github.com/figueiredoluiz/cuke-dedup&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;npm: &lt;a href="https://www.npmjs.com/package/cuke-dedup" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/cuke-dedup&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Rules reference: &lt;a href="https://github.com/figueiredoluiz/cuke-dedup/blob/main/docs/rules.md" rel="noopener noreferrer"&gt;https://github.com/figueiredoluiz/cuke-dedup/blob/main/docs/rules.md&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;CukeDedup is an independent project, not affiliated with or endorsed by the Cucumber project.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>testing</category>
      <category>opensource</category>
      <category>cucumber</category>
    </item>
  </channel>
</rss>
