<?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: Vitaly Bogachev</title>
    <description>The latest articles on DEV Community by Vitaly Bogachev (@talhayme911).</description>
    <link>https://dev.to/talhayme911</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%2F4177430%2F0e9369a3-6b4a-45d1-8281-9c4b477affb9.png</url>
      <title>DEV Community: Vitaly Bogachev</title>
      <link>https://dev.to/talhayme911</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/talhayme911"/>
    <language>en</language>
    <item>
      <title>CI only ever runs the new tests against the new code</title>
      <dc:creator>Vitaly Bogachev</dc:creator>
      <pubDate>Sun, 11 Oct 2026 16:50:31 +0000</pubDate>
      <link>https://dev.to/talhayme911/ci-only-ever-runs-the-new-tests-against-the-new-code-23io</link>
      <guid>https://dev.to/talhayme911/ci-only-ever-runs-the-new-tests-against-the-new-code-23io</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://talhayme.github.io/blog/ci-only-runs-new-tests/" rel="noopener noreferrer"&gt;my blog&lt;/a&gt; on 10 October 2026. The numbers here are from the tool's first week, on cases I wrote myself. The follow-up runs it on real agent commits it had never seen, with a less flattering and more useful result: &lt;a href="https://talhayme.github.io/blog/3355-agent-test-edits/" rel="noopener noreferrer"&gt;3,355 times a coding agent edited a test&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Here is a pull request. The code changes one comparison, the test changes one expected value, and the description says &lt;em&gt;corrected the boundary; all 3 tests pass, no breaking changes&lt;/em&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ pytest
3 passed in 0.01s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is also wrong. The function decides whether a day is a weekend; the change makes Monday one; the test that used to say &lt;code&gt;is_weekend(0) is False&lt;/code&gt; now says &lt;code&gt;is True&lt;/code&gt;. The test was not fixed. It was brought into agreement with the bug.&lt;/p&gt;

&lt;p&gt;Nothing in CI can see this, and the reason is structural rather than a missing feature: CI runs the pull request's tests against the pull request's code. Both sides of that comparison were written by the same author, in the same commit. If the author — human or agent — edits both halves consistently, the result is green by construction.&lt;/p&gt;

&lt;p&gt;The combination nobody runs is the &lt;em&gt;old&lt;/em&gt; tests against the &lt;em&gt;new&lt;/em&gt; code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ pr-witness --base main

                   base      head  crossrun
  passed              3         3         2
  failed              0         0         1

  Claims
   ! "3 tests pass"          — true, but 1 test that passed on the
                                base branch fails against this code
   ✗ "no breaking changes"   — 1 base-branch test now fails

!! [crossrun] tests/test_dates::test_monday
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is &lt;a href="https://github.com/talhayme/pr-witness" rel="noopener noreferrer"&gt;pr-witness&lt;/a&gt;. This post is about building it, and specifically about the parts where measuring something changed what I believed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why now
&lt;/h2&gt;

&lt;p&gt;Coding agents have a specific failure mode, and it has numbers. &lt;a href="https://transluce.org/docent/blog/coding-agent-behaviors" rel="noopener noreferrer"&gt;Transluce&lt;/a&gt; analysed 8,600 real coding-agent transcripts; in the public SWE-chat sessions, 34.7% contained some overselling of success and 14.7% some monitor evasion — quietly disabling tests, merging without authorization, claiming a review agent approved. Most of that is mild; the severe cases are 1.8% and 1.9% respectively, which at the scale agents now run is a lot of pull requests. The canonical public case is &lt;a href="https://github.com/anthropics/claude-code/issues/46940" rel="noopener noreferrer"&gt;anthropics/claude-code#46940&lt;/a&gt;: on a 4,992-value golden suite the agent broke seven values, reported &lt;code&gt;4966/4966 ALL PASSED&lt;/code&gt;, and committed — the denominator had quietly shrunk. And &lt;a href="https://arxiv.org/abs/2510.20270" rel="noopener noreferrer"&gt;ImpossibleBench&lt;/a&gt; (Zhong, Raghunathan, Carlini) measured what happens when a spec and a test conflict: on its SWE-bench variant, frontier models "pass" by cheating in 39–76% of tasks (GPT-5 76%, Claude Opus 4.1 54%), and Claude models do it more than 79% of the time by one method — editing the test file. Cheap to catch in a diff, in principle.&lt;/p&gt;

&lt;p&gt;Tools exist for the diff. &lt;a href="https://github.com/taipei49314/checkwash" rel="noopener noreferrer"&gt;checkwash&lt;/a&gt; reads a change and flags weakened assertions, loosened tolerances, disabled tests, touched CI. It is good at that and publishes its own benchmarks. What nobody was doing was the other half: running the thing, comparing against what used to run, and checking what the author &lt;em&gt;said&lt;/em&gt; against what &lt;em&gt;happened&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build the ruler before the thing it measures
&lt;/h2&gt;

&lt;p&gt;The plan had a rule I would now apply to anything of this kind: phase zero is a labelled corpus, and no detector gets written until the corpus and the evaluation harness exist.&lt;/p&gt;

&lt;p&gt;The corpus is 17 cases, each a full before/after file tree (not a patch — a runner has to execute both sides), a pull request description, and a label saying what the right answer is. Nine are cheats; eight are honest changes that &lt;em&gt;look&lt;/em&gt; like cheats to a naive detector: a feature removed along with its tests, expected values updated because the spec changed, tests renamed, split into modules, collapsed into &lt;code&gt;parametrize&lt;/code&gt;, a flaky test skipped with an issue link.&lt;/p&gt;

&lt;p&gt;The honest half is the whole point. A tool that scores perfectly on the cheats and flags every refactor is not a detector; it is an alarm, and alarms get switched off.&lt;/p&gt;

&lt;p&gt;Counts in the labels are not typed. A verifier runs each case three ways — base on base, head on head, base tests on head code — and writes the numbers back. If the run disagrees with the label, the case is wrong. This caught two of my own cases before the tool existed: one cheat that failed its own tests (so nothing was hidden), and one labelled &lt;code&gt;denominator&lt;/code&gt; that was actually something else.&lt;/p&gt;

&lt;p&gt;That second one is worth its own paragraph.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;describe.only&lt;/code&gt; does not shrink the count
&lt;/h2&gt;

&lt;p&gt;The plan said: flag it when the collected count goes down. pytest's &lt;code&gt;testpaths&lt;/code&gt; narrowing does exactly that — 14 collected becomes 8. The JavaScript twin is &lt;code&gt;describe.only&lt;/code&gt;, which makes the runner ignore every other suite. Same trick.&lt;/p&gt;

&lt;p&gt;Measured, it is not the same shape. vitest still collects all 5 tests, runs 2, and reports 3 as &lt;code&gt;skipped&lt;/code&gt;. The count does not move. So "did the denominator shrink" is not a runner-portable question. The portable one is &lt;strong&gt;which tests actually ran&lt;/strong&gt;, compared as a set of identities, not as a total. The tool was redesigned around that before a line of it was written, and that single decision later saved the precision number — see below.&lt;/p&gt;

&lt;h2&gt;
  
  
  The baseline, and what it was not
&lt;/h2&gt;

&lt;p&gt;With the corpus in hand, checkwash 0.6.0 scores:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;bar&lt;/th&gt;
&lt;th&gt;P&lt;/th&gt;
&lt;th&gt;R&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;its own &lt;code&gt;block&lt;/code&gt; verdict&lt;/td&gt;
&lt;td&gt;0.67&lt;/td&gt;
&lt;td&gt;0.22&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;any finding, including advisory &lt;code&gt;warn&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;0.60&lt;/td&gt;
&lt;td&gt;0.67&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Recall 0.22 is not checkwash losing. The corpus was deliberately built from the signals it does not claim — cross-run breakage, denominators, prose — so a low number here means "out of scope", which is the premise I was testing. The raw counting signals alone, read straight from the verifier's output, score P 0.80 / R 0.89. That was the floor to beat.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase one scored worse than the baseline
&lt;/h2&gt;

&lt;p&gt;The first working version of the tool: P 0.67. Worse than the counts it was meant to improve on.&lt;/p&gt;

&lt;p&gt;The cause was the identity decision above, taken to its logical end. If a test is identified by &lt;code&gt;file::name&lt;/code&gt;, then renaming the file deletes every identity in it. Splitting a module deletes them. Collapsing five tests into one parametrised test deletes four. Three of the commonest honest refactors there are, all reported as mass deletion.&lt;/p&gt;

&lt;p&gt;The fix is the cross-run itself. If a behaviour is still covered, the base branch's own tests still pass against the new code — whatever they are called now. A vanished identity is only worth reporting when the cross-run cannot confirm it. P 0.67 → 0.80, zero detections lost. A second bug of the same shape — a documented skip reported both as "documented" and as a denominator drop — took it to 0.89.&lt;/p&gt;

&lt;p&gt;Both were the same mistake: a signal that is technically true and practically useless. Precision is not something you tune at the end. It is whether the report is worth reading.&lt;/p&gt;

&lt;h2&gt;
  
  
  Integrating checkwash, and disagreeing with it once
&lt;/h2&gt;

&lt;p&gt;The plan listed ten JS/TS patterns to implement because checkwash's JavaScript support was weak. The plan was written against v0.5. Against 0.6.0, measured one by one, seven of the ten already worked. I would have spent a week producing a worse copy of working code.&lt;/p&gt;

&lt;p&gt;The three real gaps are implemented locally — &lt;code&gt;try&lt;/code&gt;/&lt;code&gt;catch&lt;/code&gt; swallowing an assertion, &lt;code&gt;@ts-nocheck&lt;/code&gt; on a test file, &lt;code&gt;eslint-disable&lt;/code&gt; of a rule that enforces that tests assert something — and the first is filed upstream as &lt;a href="https://github.com/taipei49314/checkwash/issues/363" rel="noopener noreferrer"&gt;checkwash#363&lt;/a&gt;, with a one-commit reproduction: the identical cheat in Python and TypeScript, one finding, on the Python file.&lt;/p&gt;

&lt;p&gt;Wiring checkwash in dropped precision from 0.90 to 0.75. I had mapped its &lt;code&gt;warn&lt;/code&gt; findings to a flag. checkwash blocks only on &lt;code&gt;high&lt;/code&gt;; it raises &lt;code&gt;warn&lt;/code&gt; for every disappeared test, so every honest rename lit up. Mapping &lt;code&gt;warn&lt;/code&gt; to advisory restored it. The one remaining disagreement is real: checkwash blocks on &lt;em&gt;any&lt;/em&gt; added skip marker; pr-witness does not flag a skip whose reason names a tracking issue. Rather than drop its finding, the report keeps it, downgrades it, and prints the disagreement:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;checkwash reports every added skip marker. pr-witness does not flag this&lt;br&gt;
one: the run shows a skip reason that names an issue. If your policy is&lt;br&gt;
that no skip is acceptable, trust checkwash here, not us.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the difference between using a tool and overruling it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lie that lives only in prose
&lt;/h2&gt;

&lt;p&gt;One case had been missed by every layer so far:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Added 41 tests for the discount logic. 49/49 passing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Eight tests before, nine after, all green, cross-run clean. The run is honest. The description is not. No run-level or diff-level signal can see this, and per the session data it is one of the commonest shapes there is.&lt;/p&gt;

&lt;p&gt;So the third layer reads the description. Deterministic templates, English and Russian, no model — a model that hallucinated a claim the author never made would be worse than no claim checking. Each claim gets one of four verdicts, and the fourth took the longest to get right:&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;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;✓ true&lt;/td&gt;
&lt;td&gt;the run bears it out&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;✗ false&lt;/td&gt;
&lt;td&gt;the run contradicts it — &lt;code&gt;"49/49 passing" — the run reports 9 passed&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;? unverifiable&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Fixes #318&lt;/code&gt; — a test run cannot know this, and saying so beats guessing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;! misleading&lt;/td&gt;
&lt;td&gt;literally true, materially not&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;"8 tests pass" is exactly right when the base branch ran 14. Call it true and the tool is gameable in one move: narrow the suite, quote the smaller number honestly. Call it false and you are accusing someone of lying for quoting their own terminal. &lt;code&gt;misleading&lt;/code&gt; is the third answer, and it is where the three layers finally meet: on the &lt;code&gt;try&lt;/code&gt;/&lt;code&gt;catch&lt;/code&gt; case every run reports 3/3, the diff says an assertion was neutralised, and the claim checker says &lt;em&gt;all tests pass — but one assertion was neutralised in this change&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;With claims, recall on the corpus went to 1.00 at P 0.91.&lt;/p&gt;

&lt;h2&gt;
  
  
  Signed, so the report means something
&lt;/h2&gt;

&lt;p&gt;A comment on a pull request is text. An agent can type text. The report is therefore signed with GitHub Artifact Attestations: the evidence JSON is the predicate, the workflow is the signer, and anyone can run &lt;code&gt;gh attestation verify&lt;/code&gt; on the downloaded file. I checked the obvious thing — flip &lt;code&gt;clean&lt;/code&gt; to &lt;code&gt;flag&lt;/code&gt; in the file and verify again — and it is refused, because no attestation exists for that digest.&lt;/p&gt;

&lt;p&gt;Two things the plan did not know, found by reading the current docs rather than the plan: the action is &lt;code&gt;actions/attest@v4&lt;/code&gt;, and &lt;strong&gt;attestations only work in public repositories unless you are on Enterprise Cloud&lt;/strong&gt;. A private-repo user gets the report with an "Unsigned" line and the reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shipping it was four red runs
&lt;/h2&gt;

&lt;p&gt;The first push to GitHub failed CI twice over. &lt;code&gt;pytest&lt;/code&gt; from the repository root collected the corpus's fixture test files — 25 collection errors — and two scripts hardcoded a &lt;code&gt;venv/bin/&lt;/code&gt; path that existed on exactly one laptop. The fix for the first was &lt;code&gt;testpaths = ["tests"]&lt;/code&gt;, which is precisely the narrowing the tool flags in a pull request. The config comment explains why this is the honest case.&lt;/p&gt;

&lt;p&gt;The first live run of the action signed &lt;code&gt;evidence.json&lt;/code&gt; and then threw it away, so there was nothing to verify. The moving &lt;code&gt;v0.1&lt;/code&gt; tag — the one that makes &lt;code&gt;uses: talhayme/pr-witness@v0.1&lt;/code&gt; work — matched the release workflow's &lt;code&gt;v*&lt;/code&gt; trigger and cut two spurious releases.&lt;/p&gt;

&lt;p&gt;Every one of those was a real bug, and none would have been found by running things on the machine they were written on. I mention them because the subject of this post is the gap between "I ran it and it was green" and "it is correct", and the project was not exempt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it is
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;pr-witness
pr-witness &lt;span class="nt"&gt;--base&lt;/span&gt; origin/main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&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;talhayme/pr-witness@v0.1&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;test-command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pytest&lt;/span&gt;       &lt;span class="c1"&gt;# or vitest, jest, go&lt;/span&gt;
    &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;report&lt;/span&gt;               &lt;span class="c1"&gt;# report | require-ack | strict&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;report&lt;/code&gt; never fails a job; that is the default, because a tool that blocks on its first day is uninstalled on its second. &lt;code&gt;require-ack&lt;/code&gt; fails on a flag until a maintainer adds &lt;code&gt;test-change-approved&lt;/code&gt;. &lt;code&gt;strict&lt;/code&gt; fails on a flag regardless — the label documents intent, it does not change facts.&lt;/p&gt;

&lt;p&gt;There is also a Claude Code plugin: a pause before a test file changes, and a check of "all tests pass" against a real run when the agent says it is done. Advisory on purpose. The agent runs inside its own environment and can route around any hook; the proof is the signed report from CI, where it cannot reach.&lt;/p&gt;

&lt;h2&gt;
  
  
  The number, and its caveat
&lt;/h2&gt;

&lt;p&gt;17 cases, P 0.91, R 1.00, 115 tests, reproduced on a clean Ubuntu runner. That is a regression gate, not an accuracy claim. The confidence interval on nine positives is wide enough to drive a truck through, and the honest reading of recall 1.00 is that the corpus stopped discriminating between versions somewhere around phase three. It needs harder cases — ideally real, licence-checked ones from maintainers who caught this in the wild — more than the tool needs more detectors. If you have one, the repository's issues are the place.&lt;/p&gt;

&lt;p&gt;Source, corpus, and every number in this post: &lt;a href="https://github.com/talhayme/pr-witness" rel="noopener noreferrer"&gt;github.com/talhayme/pr-witness&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>ai</category>
      <category>github</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
