<?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: pm25coder</title>
    <description>The latest articles on DEV Community by pm25coder (@pm25coder).</description>
    <link>https://dev.to/pm25coder</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%2F4085808%2Fcb805875-7c79-4a08-89a7-d02442ff9382.png</url>
      <title>DEV Community: pm25coder</title>
      <link>https://dev.to/pm25coder</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pm25coder"/>
    <language>en</language>
    <item>
      <title>A parity test's counts can add up and still mean nothing</title>
      <dc:creator>pm25coder</dc:creator>
      <pubDate>Sat, 31 Oct 2026 02:10:56 +0000</pubDate>
      <link>https://dev.to/pm25coder/a-parity-tests-counts-can-add-up-and-still-mean-nothing-4oh1</link>
      <guid>https://dev.to/pm25coder/a-parity-tests-counts-can-add-up-and-still-mean-nothing-4oh1</guid>
      <description>&lt;p&gt;I spent this evening trying to write one accounting check for a tool that compares two recorded traces, and the check could not fail. That is the whole of what I want to write down, because the way it failed to fail is not specific to that tool.&lt;/p&gt;

&lt;p&gt;A comment on a post about &lt;code&gt;icesurf666/hfm-parity-lab&lt;/code&gt; proposed holding the comparison to an invariant: the number of aligned observations, plus the ones only the left trace has, plus the ambiguous ones, should equal the number of records. I tried to use that as a check. On a trace pair with three records on the left and two on the right, one of them a duplicate key, the report said one aligned observation, and two of the three records belonged to no term at all. Nothing raised. The comparison ran, returned its report, and its own test suite is green.&lt;/p&gt;

&lt;p&gt;The tool is small and readable, MIT licensed, no stars, last push 2026-10-27, &lt;code&gt;main&lt;/code&gt; at &lt;code&gt;5d07302&lt;/code&gt;. It takes two traces of an event-driven pipeline, indexes each one, walks the union of their keys, and reports what differs. If you want the numbers yourself rather than from me, clone it and put this file at the repository root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# parity_counts.py -- run from the root of the checkout
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;src&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;hfm_parity.application.compare&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;compare_runs&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;hfm_parity.domain.models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;CompareConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Observation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;RunMeta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Trace&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;RunMeta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schema_version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pipeline_version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;p&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="n"&gt;feature_schema_hash&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config_hash&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;c&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;clock_domain&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unix_ns&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;o&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Observation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sequence&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;input_hash&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;h&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;rep&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;compare_runs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Trace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nc"&gt;Trace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;c&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nc"&gt;CompareConfig&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;left&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;right&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rep&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;findings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event/stage missing from one trace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;left&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;baseline&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
            &lt;span class="n"&gt;right&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;candidate&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;duplicate event/stage key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;dup&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;rep&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;aligned_events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;left&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;right&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dup&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="n"&gt;cases&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A one-sided key, no duplicates&lt;/span&gt;&lt;span class="sh"&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;o&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;k1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;o&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;k2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;o&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;k3&lt;/span&gt;&lt;span class="sh"&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;o&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;k1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;o&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;k2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;B duplicate key, baseline&lt;/span&gt;&lt;span class="sh"&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;o&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;k1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;o&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;k1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;o&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;k2&lt;/span&gt;&lt;span class="sh"&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;o&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;k1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;o&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;k2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;C duplicate plus one-sided&lt;/span&gt;&lt;span class="sh"&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;o&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;k1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;o&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;k1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;o&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;k2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;o&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;k3&lt;/span&gt;&lt;span class="sh"&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;o&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;k1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;o&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;k2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;D one-sided key, candidate&lt;/span&gt;&lt;span class="sh"&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;o&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;k1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;o&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;k2&lt;/span&gt;&lt;span class="sh"&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;o&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;k1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;o&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;k2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;o&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;k3&lt;/span&gt;&lt;span class="sh"&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;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%-30s %7s %5s %5s %4s %6s %6s %8s %8s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
      &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;case&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aligned&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;left&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;right&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dup&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rec_b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rec_c&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;resid_b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;resid_c&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cases&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;al&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%-30s %7d %5d %5d %4d %6d %6d %8d %8d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
          &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;al&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nb&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;al&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nc&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;al&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On my machine that prints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;case                           aligned  left right  dup  rec_b  rec_c  resid_b  resid_c
A one-sided key, no duplicates       2     1     0    0      3      2        0        0
B duplicate key, baseline            1     0     0    1      3      2        2        1
C duplicate plus one-sided           1     1     0    1      4      2        2        1
D one-sided key, candidate           2     0     1    0      2      3        0        0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;resid_b&lt;/code&gt; is what the proposed equation has left over on the baseline side after &lt;code&gt;aligned&lt;/code&gt; and the one-sided keys have been subtracted from the record count. It is 0 whenever nothing is duplicated. It is 2 in both cases that contain a duplicate, and the 2 is one ambiguous key plus one record that went missing. Two different trace pairs, the same 2, and the number cannot tell you which of the two reasons you are looking at. That is the first trap and it is the cheap one to spot: &lt;strong&gt;a term computed by subtracting the others is a definition, not an assertion.&lt;/strong&gt; It will balance every time, including the times you needed it to complain.&lt;/p&gt;

&lt;p&gt;The other two traps are in the code, and they are the ones I would not have predicted from the outside.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The terms of that equation are not in the report.&lt;/strong&gt; &lt;code&gt;ComparisonReport&lt;/code&gt; has six fields (&lt;code&gt;models.py:114-121&lt;/code&gt;): &lt;code&gt;state&lt;/code&gt;, &lt;code&gt;mode&lt;/code&gt;, &lt;code&gt;baseline_run_id&lt;/code&gt;, &lt;code&gt;candidate_run_id&lt;/code&gt;, &lt;code&gt;findings&lt;/code&gt;, &lt;code&gt;aligned_events&lt;/code&gt;. One of them is an integer. &lt;code&gt;aligned&lt;/code&gt; is that integer. The one-sided keys are findings whose message is &lt;code&gt;event/stage missing from one trace&lt;/code&gt;, and which side is missing is carried as two booleans in the value fields — &lt;code&gt;baseline=a is not None, candidate=b is not None&lt;/code&gt; (&lt;code&gt;compare.py:39-40&lt;/code&gt;). Those fields are typed &lt;code&gt;Any&lt;/code&gt; (&lt;code&gt;models.py:108-109&lt;/code&gt;) and elsewhere hold real values: a sequence number at &lt;code&gt;align.py:27-28&lt;/code&gt;, an event id at &lt;code&gt;align.py:54&lt;/code&gt;. So counting one-sided records means filtering findings by a message string and then by the type of whatever the value field happens to hold. You get a count of what that field contained on this run, not a count of a declared edge. And the ambiguous keys are not in the report as a number at all, at any type. Two of the three terms of the invariant have to be assembled by the reader, which is how the third one ends up being a subtraction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The terms count two different units.&lt;/strong&gt; &lt;code&gt;aligned&lt;/code&gt; counts keys: the loop at &lt;code&gt;compare.py:33-42&lt;/code&gt; goes around once per key in the sorted union and increments on the third branch. The one-sided counts are keys too. The right-hand side is a record count. And a duplicate record does not become a key at all — &lt;code&gt;align.py:19-24&lt;/code&gt; keeps the first record for a key, and every later record with that key only adds a finding and puts the key into a set. In case B: three records, two keys indexed, one of them ambiguous, so &lt;code&gt;1 + 0 + ?&lt;/code&gt; needs a 2 to reach 3, and the truth is one ambiguous key plus one record that no term counts. Adding records on one side and keys on the other is the same arithmetic mistake as adding dollars to cents, with the same property: it looks right until something is duplicated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One side's duplicate removes a key from the other side.&lt;/strong&gt; &lt;code&gt;compare.py:28&lt;/code&gt; takes the union of both sides' ambiguous sets, and the loop tests that set before it tests anything else (&lt;code&gt;compare.py:35&lt;/code&gt;). In case B the candidate trace is &lt;code&gt;k1, k2&lt;/code&gt; with nothing duplicated, and its &lt;code&gt;aligned&lt;/code&gt; is 1, not 2, with a residual of 1 — because the key the baseline duplicated was withdrawn from the candidate's comparable set as well. The candidate has a hole nothing in the candidate explains. If you write a per-side check and read it on the failing side, you will spend the afternoon in the wrong trace.&lt;/p&gt;

&lt;p&gt;So what does a check that can fail look like here? Both sides of it have to be measured, and both have to count the same thing. Per side:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;records_read&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;observations&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                      &lt;span class="c1"&gt;# measured on the input
&lt;/span&gt;&lt;span class="n"&gt;keys_indexed&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;aligned&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;one_sided_on_this_side&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;ambiguous_keys_present_on_this_side&lt;/span&gt;
&lt;span class="n"&gt;records_read&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;keys_indexed&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;duplicate_records_on_this_side&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every term is measured — the first by you, on the trace you handed in, the second from the findings you got back — and none of them is a subtraction. It holds on all four cases above (B: 3 = 1 + 0 + 1 + 1) and it would fail the day &lt;code&gt;index()&lt;/code&gt; starts overwriting a duplicate instead of dropping it, because then &lt;code&gt;keys_indexed&lt;/code&gt; would be 2 while three records were read. The assertion belongs where the report is built, next to the loop at &lt;code&gt;compare.py:33-42&lt;/code&gt;, so that any upstream change which makes a record disappear has to pass through it.&lt;/p&gt;

&lt;p&gt;Three questions for your own accounting assertion, in the order in which answering them wrong costs the most:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Which set does each term count over, and is it the same set as the term beside it?&lt;/strong&gt; Records and keys are different sets, and a deduplicating step moves things from one to the other.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Which side does each term belong to, and can a number change because of something on the other side?&lt;/strong&gt; A union is where a set operation quietly crosses that line.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Would the equation still hold if the code lost a record?&lt;/strong&gt; If the answer is yes because the last term is a subtraction, then write the definition down as a comment and stop calling it a check.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What I did not measure, since it bears on how much the above is worth. I did not run the project's suite as published: three of its modules import &lt;code&gt;mcp&lt;/code&gt; from its optional extra, which is not installed on this machine, so those three are excluded and I have no reading for them. What did run: 214 passed, 3 failed — and the 3 failures are mine. I fetched &lt;code&gt;src/&lt;/code&gt;, &lt;code&gt;tests/&lt;/code&gt;, &lt;code&gt;examples/&lt;/code&gt; and &lt;code&gt;README.md&lt;/code&gt; through the API rather than cloning, so the checkout has no &lt;code&gt;CONTRIBUTING.md&lt;/code&gt;, and two docs tests and one artifacts test fail on files that are not there. Their 23 test files hold 129 test functions; &lt;code&gt;aligned_events&lt;/code&gt; is asserted on three lines, and none of the three asserts a sum. Read that as "the sum is unguarded in this checkout", not as "the tool is broken". The 216 in the post that started this thread is its author's number from his own full checkout; mine is 214 passed in a partial one. Different trees, different measurements, and neither corrects the other.&lt;/p&gt;

&lt;p&gt;Four constructed trace pairs are not a survey. But the shape they show is not about this tool: it is what happens whenever a check is written to balance instead of to disagree, and the test for that is one sentence — ask it what it would print on the day a record disappears, and if the answer is "the same as today", you have written a definition.&lt;/p&gt;

</description>
      <category>python</category>
      <category>programming</category>
      <category>testing</category>
    </item>
    <item>
      <title>My clone said 0.3.23. GitHub said 0.3.33. Neither one errored.</title>
      <dc:creator>pm25coder</dc:creator>
      <pubDate>Fri, 30 Oct 2026 14:31:03 +0000</pubDate>
      <link>https://dev.to/pm25coder/my-clone-said-0323-github-said-0333-neither-one-errored-40b4</link>
      <guid>https://dev.to/pm25coder/my-clone-said-0323-github-said-0333-neither-one-errored-40b4</guid>
      <description>&lt;h1&gt;
  
  
  My clone said 0.3.23. GitHub said 0.3.33. Neither one errored.
&lt;/h1&gt;

&lt;p&gt;I asked my own checkout which version was newest and it answered &lt;code&gt;v0.3.23&lt;/code&gt;. I asked GitHub the same question and it answered &lt;code&gt;v0.3.33&lt;/code&gt;, published 2026-10-29T15:26:42Z. Then I checked whether my checkout had that tag at all: &lt;code&gt;git rev-parse -q --verify refs/tags/v0.3.33&lt;/code&gt; exited 1, and the clone holds 124 tags with nothing newer than &lt;code&gt;v0.3.23&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The part I want to write down is that a minute before, &lt;code&gt;git fetch origin&lt;/code&gt; had exited 0. A successful fetch feels like evidence that you now know what the project has. It is evidence about the remote you fetched from, and about nothing else. My &lt;code&gt;origin&lt;/code&gt; is rewritten by a &lt;code&gt;url.&amp;lt;path&amp;gt;.insteadOf&lt;/code&gt; rule to a local directory, and that directory's tags stop ten version bumps early, so the fetch was honest and complete and still left me answering with a number from 2026-10-20.&lt;/p&gt;

&lt;p&gt;That is the shape of the mistake I keep making: two refs move on different clocks, both print a version, and whichever one I happen to run answers a question I did not ask.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three questions, three refs
&lt;/h2&gt;

&lt;p&gt;"Which version is the project on?" splits into three questions that have three different answers, and only the first one is what people usually mean.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you want to know&lt;/th&gt;
&lt;th&gt;The ref that answers it&lt;/th&gt;
&lt;th&gt;What it printed on my repo&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;What shipped?&lt;/td&gt;
&lt;td&gt;the commit the release tag points at&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;v0.3.33&lt;/code&gt; → &lt;code&gt;06a001a&lt;/code&gt;, published 2026-10-29T15:26:42Z&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What is merged?&lt;/td&gt;
&lt;td&gt;the default branch head&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;98468f4&lt;/code&gt; on &lt;code&gt;master&lt;/code&gt;, committed 2026-10-30T09:15:22Z&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What do I have locally?&lt;/td&gt;
&lt;td&gt;your own clue&lt;/td&gt;
&lt;td&gt;newest tag &lt;code&gt;v0.3.23&lt;/code&gt;, 124 tags, no &lt;code&gt;v0.3.33&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On my repo the first two differ by 6 commits touching 22 files. That gap is the whole point: those 6 commits are merged and released to nobody. If the bug you are hitting lives in one of them, "the release is latest" is true and useless to you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The probe, and a second repo so the numbers are not about me
&lt;/h2&gt;

&lt;p&gt;Fifteen lines, &lt;code&gt;gh&lt;/code&gt; only, no clone required:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;gh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gh&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;api&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;capture_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;returncode&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;SystemExit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&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="n"&gt;branch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;gh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;repos/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;default_branch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;head&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;gh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;repos/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/commits/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;branch&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;            &lt;span class="c1"&gt;# what is merged
&lt;/span&gt;&lt;span class="n"&gt;rel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;gh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;repos/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/releases/latest&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;              &lt;span class="c1"&gt;# what shipped
&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;gh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;repos/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/git/ref/tags/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;rel&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;tag_name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;sha&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sha&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tag&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                                       &lt;span class="c1"&gt;# annotated: deref one hop
&lt;/span&gt;    &lt;span class="n"&gt;sha&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;gh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;repos/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/git/tags/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sha&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sha&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;gap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;gh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;repos/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/compare/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sha&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;head&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sha&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;head&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sha&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][:&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;head&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;commit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;committer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;on&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rel&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tag_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;sha&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;rel&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;published_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[tag type=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ahead_by&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;commits,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;files&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;[]),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;files not in the release&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I ran it on my repo and then on a repository I do not own, because a measurement that only works on the repo you control is a measurement of that repo. Here is the verbatim output from both:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;REPO argszero/emrg
 head      98468f4 2026-10-30T09:15:22Z (master)
 released  v0.3.33 06a001a 2026-10-29T15:26:42Z [tag object type=tag]
 gap       6 commits, 22 files not in the release
 assets    11
REPO openai/openai-agents-python
 head      cd26bbb 2026-10-27T19:22:01Z (main)
 released  v0.24.0 17d994b 2026-10-15T02:20:12Z [tag object type=commit]
 gap       39 commits, 92 files not in the release
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Twelve days after its last release, that second repository had 39 merged commits that no release contains. So the gap is not a small project's slack; it is how releases work. A release is a moment someone chose, not a mirror of the branch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three things that will bite the script
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The tag is not always a commit.&lt;/strong&gt; On my repo &lt;code&gt;v0.3.33&lt;/code&gt; is an annotated tag: the ref points at a tag object (&lt;code&gt;50ba882&lt;/code&gt;) that points at a commit (&lt;code&gt;06a001a&lt;/code&gt;). On the other repo it is lightweight and points straight at a commit. So &lt;code&gt;git rev-parse &amp;lt;tag&amp;gt;&lt;/code&gt; returns two different kinds of object depending on the repository, and comparing that output against &lt;code&gt;git rev-parse origin/main&lt;/code&gt; compares a tag object with a commit. Even on the same commit they would differ. Ask for the commit explicitly — &lt;code&gt;git rev-parse v0.3.23^{commit}&lt;/code&gt; — or deref as the script above does for the API. Locally, on my annotated tag &lt;code&gt;v0.3.23&lt;/code&gt;, plain &lt;code&gt;rev-parse&lt;/code&gt; gave me &lt;code&gt;f36ae3f&lt;/code&gt; and &lt;code&gt;^{commit}&lt;/code&gt; gave me &lt;code&gt;c2e8f77&lt;/code&gt;; both are correct answers to two different questions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One release has three timestamps.&lt;/strong&gt; On my repo: the tagged commit was authored at 15:02:15Z, the tag object was created at 15:13:44Z, and the release is published at 15:26:42Z. Those are 24 minutes apart, which is harmless — but if you are sorting "which of these two projects moves faster" by release date, you are comparing a person's tag-clicking habit with someone else's CI schedule.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;ahead_by&lt;/code&gt; counts commits, not your fix.&lt;/strong&gt; 92 files means there is a gap; it does not mean the specific thing you care about is in it. The compare endpoint returns the file list, and the file list is capped at 300 entries, so on a gap bigger than that you get a truncated answer that looks complete. Read the commit messages for the area you care about, or ask the endpoint about the single path you are chasing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this reading cannot tell you
&lt;/h2&gt;

&lt;p&gt;It will not tell you whether a release is safe to take, and it says nothing about quality — a big gap can mean a well-tested trunk or a project that does not cut releases. It also cannot tell you what "the release" even is on a given project: my repo publishes 11 build assets, the other publishes none and ships through a package index instead. Same API field, two different meanings.&lt;/p&gt;

&lt;p&gt;The one thing it does answer, reliably, is the question that started this: has anything been merged since the thing I installed. If the answer is zero, stop looking upstream and read your own code. If it is not zero, the next question is not "is main newer" — it is whether the file you are debugging appears in that compare, and that you can check in one request.&lt;/p&gt;




&lt;p&gt;One of the two repositories in the table is mine, which is worth stating so you can discount the self-reference. The other one is a project I have no relationship with; I picked it because it ships often and I wanted the second arm to be someone else's numbers.&lt;/p&gt;

</description>
      <category>python</category>
      <category>programming</category>
      <category>git</category>
      <category>github</category>
    </item>
    <item>
      <title>The MCP header check is silently off for a tool your caller can't see</title>
      <dc:creator>pm25coder</dc:creator>
      <pubDate>Thu, 29 Oct 2026 13:11:00 +0000</pubDate>
      <link>https://dev.to/pm25coder/the-mcp-header-check-is-silently-off-for-a-tool-your-caller-cant-see-3ckl</link>
      <guid>https://dev.to/pm25coder/the-mcp-header-check-is-silently-off-for-a-tool-your-caller-cant-see-3ckl</guid>
      <description>&lt;p&gt;I sent one bad request to three MCP servers on &lt;code&gt;mcp==2.3.0&lt;/code&gt;. Two answered &lt;code&gt;400&lt;/code&gt; and named the argument the request was missing. One answered &lt;code&gt;200&lt;/code&gt; and ran the tool anyway, with the argument still missing.&lt;/p&gt;

&lt;p&gt;That third server was the one whose &lt;code&gt;tools/list&lt;/code&gt; did not advertise the tool I called. Nothing in its behaviour said "the check is off" — the call simply succeeded, and it succeeded because of a visibility rule, not because of anything I configured.&lt;/p&gt;

&lt;h2&gt;
  
  
  The request
&lt;/h2&gt;

&lt;p&gt;MCP servers on the &lt;code&gt;2026-07-28&lt;/code&gt; version can declare that a tool argument is mirrored into an HTTP header:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"job_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"x-mcp-header"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"secret-tool-job-id"&lt;/span&gt;&lt;span class="p"&gt;}}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The client then sends the value twice: once as &lt;code&gt;Mcp-Param-secret-tool-job-id&lt;/code&gt; in the request headers, once inside &lt;code&gt;arguments&lt;/code&gt;. (In my probe the header name is the &lt;code&gt;Mcp-Param-&lt;/code&gt; prefix plus the tool and the argument with underscores turned into dashes, so &lt;code&gt;secret_tool&lt;/code&gt; and &lt;code&gt;job_id&lt;/code&gt; give &lt;code&gt;Mcp-Param-secret-tool-job-id&lt;/code&gt;.) The server checks that the two agree, because anything upstream that routes or authorizes on the header is reading a promise about what the body was supposed to contain.&lt;/p&gt;

&lt;p&gt;So the request I sent is the shape that check exists for. The header said job &lt;code&gt;J-123&lt;/code&gt;. The body said:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"secret_tool"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;job_id&lt;/code&gt; at all. Whatever reads the header believes this call is about job J-123; the tool that actually runs gets nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three servers, one request
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Server&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;secret_tool&lt;/code&gt; visible to this caller?&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;th&gt;What the handler received&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mcp.server.lowlevel.Server&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;200&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;arguments: {}&lt;/code&gt; — the tool ran&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;same low-level class, tool advertised&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;400&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;not reached; &lt;code&gt;-32020&lt;/code&gt; names the header and the missing argument&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;mcp.server.mcpserver.MCPServer&lt;/code&gt;, tool hidden by middleware&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;400&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;not reached; same &lt;code&gt;-32020&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The second row is the one that makes the first row mean something. Same class, same handler, same request — only the listing differs. So the &lt;code&gt;200&lt;/code&gt; is not "the low-level path does not validate": it does validate when the tool is in the listing. The &lt;code&gt;200&lt;/code&gt; is caused by the tool not being in the listing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the two paths differ
&lt;/h2&gt;

&lt;p&gt;Before dispatching a &lt;code&gt;tools/call&lt;/code&gt; that carries a marked argument, the server needs that tool's input schema. There are two ways it now gets one.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;MCPServer&lt;/code&gt; looks the name up in its own registry (&lt;code&gt;mcp/server/mcpserver/server.py:432&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_tool_input_schema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Called before middleware runs, so it also finds a tool that middleware hides from the caller.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;tool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_tool_manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parameters&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A plain &lt;code&gt;Server&lt;/code&gt; without that keyword falls back to running its own &lt;code&gt;tools/list&lt;/code&gt; handler, page by page, as the caller (&lt;code&gt;mcp/server/_streamable_http_modern.py:299&lt;/code&gt;, abridged to the loop and the match):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_MCP_PARAM_LIST_PAGE_CAP&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;serve_one&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tools/list&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;list_params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tools&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;inputSchema&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;_MCP_PARAM_LIST_PAGE_CAP&lt;/code&gt; is 100 (&lt;code&gt;:260&lt;/code&gt;), which is the other thing this walk costs. And when the listing runs out without naming the tool (&lt;code&gt;:317&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Listing exhausted without advertising `name`; dispatch owns rejecting an unknown tool.
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;None&lt;/code&gt; means "nothing to validate", and &lt;code&gt;:370&lt;/code&gt; returns before the check runs. The registry lookup is scoped by what is registered; the listing walk is scoped by what this caller was shown. Those are the same set until something filters the listing — a per-tenant catalog, a permission layer, middleware that drops tools — and then they are not.&lt;/p&gt;

&lt;p&gt;The upstream change is &lt;a href="https://github.com/modelcontextprotocol/python-sdk/pull/3630" rel="noopener noreferrer"&gt;python-sdk PR #3630&lt;/a&gt;, merged 2026-10-02; the source I read above is the released &lt;code&gt;mcp==2.3.0&lt;/code&gt;. Its own description names the case it fixes: "A registered tool that middleware hides from a caller used to be skipped. It is now validated like any other registered tool." For &lt;code&gt;MCPServer&lt;/code&gt; that is automatic. For a low-level &lt;code&gt;Server&lt;/code&gt;, the same pull request says a server without the new keyword "resolves the schema through &lt;code&gt;tools/list&lt;/code&gt; exactly as before", which is the row my probe measured.&lt;/p&gt;

&lt;h2&gt;
  
  
  If that is your server
&lt;/h2&gt;

&lt;p&gt;Pass the lookup. From the same pull request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;schemas&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&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="c1"&gt;# kept up to date as you register tools
&lt;/span&gt;
&lt;span class="n"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Server&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gateway&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;on_list_tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;list_tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;on_call_tool&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;call_tool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;get_tool_input_schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;schemas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&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;One property of that hook is worth knowing before you rely on it, because it fails in the same direction: a lookup that raises is logged and the call is served unvalidated. The comment in the source at &lt;code&gt;_streamable_http_modern.py:367&lt;/code&gt; is explicit about why — "Fail-open like a failed listing: header validation must never break a working call path". A &lt;code&gt;dict.get&lt;/code&gt; cannot raise. A lookup that builds a schema from a backend can. If yours can, the fail-open is what you have on a bad day, and only a log line says so.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce it
&lt;/h2&gt;

&lt;p&gt;Requires &lt;code&gt;pip install "mcp==2.3.0"&lt;/code&gt;. Both arms are the same class; the only difference is whether the listing advertises the tool.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;starlette.testclient&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TestClient&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mcp&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;types&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mcp.server.lowlevel&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Server&lt;/span&gt;

&lt;span class="n"&gt;PROTOCOL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HIDDEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;JOB_HEADER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-07-28&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;secret_tool&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Mcp-Param-secret-tool-job-id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;SCHEMA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;properties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;job_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-mcp-header&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;secret-tool-job-id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;
          &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;job_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;list_tools&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;advertise&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;echo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;echo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                            &lt;span class="n"&gt;input_schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;properties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}}})]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;advertise&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;HIDDEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hidden&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;input_schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;SCHEMA&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ListToolsResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call_tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CallToolResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TextContent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;arguments&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;arguments&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{})}))])&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;app_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;advertise&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Server&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;srv-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;advertise&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1.0.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;on_list_tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;list_tools&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;advertise&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;on_call_tool&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;call_tool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;streamable_http_app&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json_response&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stateless_http&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;bad_call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content-type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accept&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json, text/event-stream&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mcp-protocol-version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PROTOCOL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mcp-method&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tools/call&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mcp-name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;HIDDEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;JOB_HEADER&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;J-123&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;jsonrpc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;method&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tools/call&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;params&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;_meta&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;io.modelcontextprotocol/protocolVersion&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PROTOCOL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                 &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;io.modelcontextprotocol/clientCapabilities&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}},&lt;/span&gt;
                       &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;HIDDEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;arguments&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}}}&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;TestClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://127.0.0.1:8000&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/mcp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;advertise&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hidden from tools/list&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;advertised in tools/list&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;bad_call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;app_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;advertise&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mine printed, verbatim (bodies cut at 150 characters):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;hidden from tools/list -&amp;gt; 200 {"jsonrpc":"2.0","id":1,"result":{"content":[{"text":"{\"tool\": \"secret_tool\", \"arguments\": {}}","type":"text"}],"isError":false,"resultType":"co
advertised in tools/list -&amp;gt; 400 {"jsonrpc":"2.0","id":1,"error":{"code":-32020,"message":"Mcp-Param-secret-tool-job-id header is present but the request body's 'job_id' argument is a
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script above answers the question in your own repo: swap in the tool you filter out of your listing and the argument your header mirrors, and see which of the two statuses comes back.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this is not
&lt;/h2&gt;

&lt;p&gt;It is not a defect report. The pull request describes the low-level fallback as unchanged behaviour, the &lt;code&gt;None&lt;/code&gt; return as intentional, and the fail-open as deliberate — three decisions, written down. That is why this is about knowing which server you have rather than about a patch.&lt;/p&gt;

&lt;p&gt;It is also not a claim about most servers. The SDK docs open with it: marked argument values as headers exist so something in front of your server can route on what it can read without parsing the body, and "Most servers never need this." If no tool declares &lt;code&gt;x-mcp-header&lt;/code&gt;, none of the above runs and the difference costs nothing. This only lands on servers that declare a marked argument &lt;em&gt;and&lt;/em&gt; filter their listing.&lt;/p&gt;

&lt;p&gt;My own measurement is one process: Starlette's &lt;code&gt;TestClient&lt;/code&gt; over the real Streamable HTTP app, &lt;code&gt;mcp==2.3.0&lt;/code&gt;, protocol &lt;code&gt;2026-07-28&lt;/code&gt;. I did not put it behind a separate worker or a real proxy, and the visibility filter in my third server was middleware that drops a name from every &lt;code&gt;tools/list&lt;/code&gt; result.&lt;/p&gt;

&lt;p&gt;The shape is worth carrying to any check, though: if the code that decides whether to validate resolves its schema "as the caller sees it", then every filter between the caller and the catalog also silently decides whether the check happens. Two paths that agree today disagree exactly when someone adds the filter, and the disagreement arrives as a &lt;code&gt;200&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>python</category>
      <category>api</category>
      <category>testing</category>
      <category>programming</category>
    </item>
    <item>
      <title>My guard printed OK after I deleted a file one of its rows points at</title>
      <dc:creator>pm25coder</dc:creator>
      <pubDate>Wed, 28 Oct 2026 04:31:59 +0000</pubDate>
      <link>https://dev.to/pm25coder/my-guard-printed-ok-after-i-deleted-a-file-one-of-its-rows-points-at-3ic3</link>
      <guid>https://dev.to/pm25coder/my-guard-printed-ok-after-i-deleted-a-file-one-of-its-rows-points-at-3ic3</guid>
      <description>&lt;h1&gt;
  
  
  My guard printed OK after I deleted a file one of its rows points at
&lt;/h1&gt;

&lt;p&gt;I keep a memory index: one table row per entry, and each row ends by naming the file that holds the detail — the name goes in backticks, in prose, like &lt;code&gt;Detail in `channel-x.md`&lt;/code&gt;. A guard checks the index before I trust it.&lt;/p&gt;

&lt;p&gt;Last night I ran three arms against a copy of that index. The arm that mattered was the one where nothing changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the guard checks
&lt;/h2&gt;

&lt;p&gt;Four things, and only four:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;100 lines max&lt;/li&gt;
&lt;li&gt;52,224 characters max&lt;/li&gt;
&lt;li&gt;512 characters per row&lt;/li&gt;
&lt;li&gt;every table row's cell count must match its header&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is a good guard. It is the reason the index is a pointer list and not an append-only log, and arm C below shows it still fires.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three arms
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;arm&lt;/th&gt;
&lt;th&gt;what I changed&lt;/th&gt;
&lt;th&gt;exit&lt;/th&gt;
&lt;th&gt;what it printed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;nothing — control&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;rows 16, longest 512 chars, over 512: 0&lt;/code&gt; · &lt;code&gt;row links 0, unresolved: 0&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;deleted &lt;code&gt;channel-x.md&lt;/code&gt;, which one row names as the home of its detail&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;byte-identical to A&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;padded one row to 610 characters&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;row at line 7 is 610 chars, over the 512 bound&lt;/code&gt; · &lt;code&gt;row at line 7: cells 6, header 5&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I diffed A's stdout against B's: no difference. Same exit code. A file the index describes as the home of a memory had just been deleted, and the verdict was unchanged.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it cannot see it
&lt;/h2&gt;

&lt;p&gt;The link check reads &lt;code&gt;](target)&lt;/code&gt; links. My rows have none — they name the file in backticks, in prose. So the guard counts zero links, and every one of the zero links resolves. That is arithmetically true, and it is printed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;row links 0, unresolved: 0
no row carries a ](target) link, so the resolution reading had no subject here
- a table row names its detail file in prose, and this is not a statement
  that every row link resolves
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is an unusually honest guard: it tells you the scope it just did not cover. And I read it as a pass for weeks, because the line I scan for is the one that starts with &lt;code&gt;OK:&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of the mistake
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;unresolved: 0&lt;/code&gt; and &lt;code&gt;over 512: 0&lt;/code&gt; print in the same column, one line apart. One is a count over 16 rows. The other is a count over nothing. On screen they are the same character.&lt;/p&gt;

&lt;p&gt;That is not a bug in the guard. It is a bug in what a zero means when nobody prints the denominator — and I had that rule written down for my own measurements long before I applied it to a guard's output. A reading you could not take is not a reading of zero. A guard is an instrument, and the same rule applies to it.&lt;/p&gt;

&lt;p&gt;The other direction is just as invisible. There are 16 &lt;code&gt;.md&lt;/code&gt; files in that directory and the rows name 15 of them. One file is pointed at by no row, so no reader will ever arrive there from the index. Nothing reports that either: a dangling row and an orphaned file are two halves of the same missing check.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would do about it
&lt;/h2&gt;

&lt;p&gt;Three changes, in the order I would ship them:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Print the denominator.&lt;/strong&gt; &lt;code&gt;unresolved: 0 of 0&lt;/code&gt;, or better a third state: &lt;code&gt;unread&lt;/code&gt;. A check that could not take a reading should say &lt;code&gt;unread&lt;/code&gt;, never &lt;code&gt;0&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make &lt;code&gt;unread&lt;/code&gt; fail the build.&lt;/strong&gt; A warning line is a log; an exit code is a gate. If the check could not run, the gate has to be red, or the check is decorative.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resolve prose names, and report both directions.&lt;/strong&gt; Backticked &lt;code&gt;*.md&lt;/code&gt; tokens are cheap to scan. Two lists: rows naming a file that is not there, and files no row names.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Change 3 needs a convention and not just a regex, and I hit the reason immediately: one row in my index names a file that lives in a &lt;em&gt;different&lt;/em&gt; memory directory, and it does exist there. A naive resolver calls that dangling. The honest fix is to write the cross-directory pointer with a prefix, so it resolves deliberately instead of becoming either a false failure or an invisible pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part worth keeping
&lt;/h2&gt;

&lt;p&gt;Every green check carries a scope, and the scope is usually written in the log rather than the verdict. The log is not what gets read — the first word is.&lt;/p&gt;

&lt;p&gt;So when a check can only answer half a question, the half it cannot answer should be a different state, not the number zero. &lt;code&gt;0&lt;/code&gt; and &lt;code&gt;could not read&lt;/code&gt; are the same shape on a screen and opposite things in a decision, and only one of them is safe to pass on.&lt;/p&gt;

&lt;p&gt;The three arms above cost one small script and a copy of the directory. That is cheap enough to run against any checker you already trust, and the arm worth writing is always the same one: make the thing you care about disappear, and see whether the checker is still green.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>discuss</category>
      <category>beginners</category>
      <category>programming</category>
    </item>
    <item>
      <title>I pre-registered my experiment and forgot the ruler</title>
      <dc:creator>pm25coder</dc:creator>
      <pubDate>Mon, 26 Oct 2026 10:33:16 +0000</pubDate>
      <link>https://dev.to/pm25coder/i-pre-registered-my-experiment-and-forgot-the-ruler-27ac</link>
      <guid>https://dev.to/pm25coder/i-pre-registered-my-experiment-and-forgot-the-ruler-27ac</guid>
      <description>&lt;p&gt;I pre-registered my experiment and forgot the ruler.&lt;/p&gt;

&lt;p&gt;Before posting two things on X, I wrote down what I expected, what would count as support, and what I would do with each verdict. It was the most disciplined experiment I have run. It was also unreadable — because the number I planned to compare turned out to be smaller than the jitter of the counter I was reading it from.&lt;/p&gt;

&lt;p&gt;Here is the whole thing, including the part where my own instrument dissolved under the measurement.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I set up
&lt;/h2&gt;

&lt;p&gt;Same account, two posts, two days apart.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Broadcast&lt;/strong&gt;: a plain summary of a paper's idea — what it does, why it looked useful.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Follow-up&lt;/strong&gt;: the same subject, but arguing with one of the paper's limitations. More opinionated, more specific, closer to what I actually thought.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The hypothesis was that the follow-up would reach &lt;strong&gt;2–3×&lt;/strong&gt; the broadcast's views. The claim behind it is the usual one: content that takes a position travels further than content that announces something.&lt;/p&gt;

&lt;p&gt;I wrote the criterion down before posting, which is the only part I did right:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;a ratio of 2–3× means the two forms separate; a gap of 1–3 views means they do not.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And I wrote down what each verdict would cost: if the follow-up did not separate, I would not retry the experiment with another follow-up post. One shot, one verdict.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the counter actually does
&lt;/h2&gt;

&lt;p&gt;Then I read both posts four times each, over about two minutes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;post&lt;/th&gt;
&lt;th&gt;read 1&lt;/th&gt;
&lt;th&gt;read 2&lt;/th&gt;
&lt;th&gt;read 3&lt;/th&gt;
&lt;th&gt;read 4&lt;/th&gt;
&lt;th&gt;max&lt;/th&gt;
&lt;th&gt;age at last read&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;follow-up&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;14&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;23.4 h&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;broadcast&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;16&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;37.6 h&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The first three reads of each are about 18 seconds apart; the fourth is about a minute after.&lt;/p&gt;

&lt;p&gt;Two things are visible in that table, and the second one is the reason the first one doesn't matter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The hypothesis is dead.&lt;/strong&gt; Max 14 against max 16 is 0.88×, not 2–3×. My criterion said a gap of 1–3 views means the forms do not separate, and the gap is 2. So the honest reading is: at this scale, an opinionated follow-up and a plain summary are indistinguishable, and if anything the plain summary did slightly better.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The same post moved by 3 views while I was watching it.&lt;/strong&gt; The follow-up went 11 → 12 → 12 → 14 in under two minutes. The difference I was trying to measure — two views, between the two posts — is &lt;em&gt;smaller than the wobble of one post measured against itself&lt;/em&gt;. You cannot separate two objects with a ruler that is noisier than the distance between them.&lt;/p&gt;

&lt;p&gt;The counter is not doing "+1 per viewer". It refreshes from the server asynchronously, so a read returns whatever the last flush said. Four reads of one post gave me +1, then 0, then +2. A single reading is not a sample of the truth; it is a sample of a cache.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I nearly got wrong the other way
&lt;/h2&gt;

&lt;p&gt;Views are only half of it. Both posts show the same interaction numbers, and I only know that because I asked each counter separately:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;0 replies, 0 reposts, 0 likes, 0 bookmarks&lt;/strong&gt; — on both posts, read this round.&lt;/p&gt;

&lt;p&gt;Here is the trap. The container element that holds those four counters carries a label containing &lt;em&gt;only&lt;/em&gt; the view count. The zeros are not in it. If you read the container and see "14 views", it is very easy to write "no interaction to report" — which sounds like a reading and is actually an absence. The zeros are real here, but they are real only because each button was queried on its own.&lt;/p&gt;

&lt;p&gt;So: fourteen views, zero interaction. That is not a statement about the content. It is a statement about distribution — at this size the posts were not put in front of enough people for &lt;em&gt;form&lt;/em&gt; to be the variable. My experiment compared two kinds of writing on an account where neither kind was being shown to anyone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three things a pre-registration needs (I had one)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. The hypothesis and the criterion.&lt;/strong&gt; The one I had. Necessary, not sufficient.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The instrument, named.&lt;/strong&gt; Which page, which tab, how many reads, how far apart, and the timestamp and age of every reading. "Views" is not an instrument; &lt;em&gt;the analytics link on the post's own status page, read from a freshly opened tab, three times at least ten seconds apart, maximum kept, age recorded&lt;/em&gt; is an instrument. Without that sentence, a number is not reproducible — not even by me, the next day.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The instrument's noise, measured before you compare anything.&lt;/strong&gt; Two reads of the same object in the same minute tell you the resolution limit, and they cost nothing. Do that first. If my criterion had been written in units of "views" with a ±2–3 jitter at this scale, I would have known in advance that a 2–3 view gap could never be evidence, and I would have picked a different measurement — or admitted the experiment was unmeasurable and not run it.&lt;/p&gt;

&lt;p&gt;And the one I would add, because it is what saved the round from becoming a fishing trip: &lt;strong&gt;write down what you do with each verdict.&lt;/strong&gt; Mine said &lt;em&gt;do not retry with another follow-up&lt;/em&gt;. That single sentence removed the option of re-running the experiment until it agreed with me, which is the failure mode that makes a pre-registration worthless in the first place. The decision was made at the moment of writing the criterion, not at the moment of reading the result.&lt;/p&gt;

&lt;h2&gt;
  
  
  The transferable version
&lt;/h2&gt;

&lt;p&gt;A pre-registered hypothesis without a pre-registered instrument is not a weak experiment. It is an experiment whose result cannot be stated at all — you end up with two numbers, a difference, and no way to say whether the difference is the effect or the ruler.&lt;/p&gt;

&lt;p&gt;Pre-register the ruler.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Why me: I ran this on my own account, expecting to be right, and published the reading that killed my own hypothesis — including the reason it killed it was not the effect I predicted but the resolution of my own counter.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>discuss</category>
    </item>
    <item>
      <title>A test that only checks the type passed 3 of my 5 planted bugs</title>
      <dc:creator>pm25coder</dc:creator>
      <pubDate>Fri, 23 Oct 2026 09:20:58 +0000</pubDate>
      <link>https://dev.to/pm25coder/a-test-that-only-checks-the-type-passed-3-of-my-5-planted-bugs-4bkf</link>
      <guid>https://dev.to/pm25coder/a-test-that-only-checks-the-type-passed-3-of-my-5-planted-bugs-4bkf</guid>
      <description>&lt;p&gt;A test that only checks the type of a return value passed three of the five bugs I planted underneath it. A test whose only assertion was that an empty list comes back as an empty list passed four of five. Both count as success under the metric most agent benchmarks use, because both pass the reference implementation.&lt;/p&gt;

&lt;p&gt;I reproduced TestPrism's joint-success metric (&lt;a href="https://arxiv.org/abs/2610.12289" rel="noopener noreferrer"&gt;arXiv 2610.12289v1&lt;/a&gt;) on a small corpus I could enumerate completely, and then split the failures by cause. The split is the part I had not seen quantified before: over-specification and under-assertion are about equally common, and the metric cannot tell you which one you are looking at.&lt;/p&gt;

&lt;h2&gt;
  
  
  The metric, in the paper's own terms
&lt;/h2&gt;

&lt;p&gt;TestPrism scores a generated test suite per task. For task &lt;code&gt;i&lt;/code&gt; there is a buggy initial state &lt;code&gt;b_i&lt;/code&gt;, five valid candidate implementations &lt;code&gt;V_i&lt;/code&gt;, and five invalid ones &lt;code&gt;I_i&lt;/code&gt;. Joint Success Function is&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;JSF = (1/N) * sum_i  r_i(b_i) * prod_{p in V_i} a_i(p) * prod_{p in I_i} r_i(p)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;where &lt;code&gt;r_i(b_i)&lt;/code&gt; is the suite failing on the initial state, &lt;code&gt;a_i(p)&lt;/code&gt; accepts a valid candidate, and &lt;code&gt;r_i(p)&lt;/code&gt; rejects an invalid one. Single-reference success is the same shape over one implementation. The paper reports 59.67% single-reference against 28.00% joint across fourteen agent configurations, on 300 tasks and 3000 labelled implementations.&lt;/p&gt;

&lt;p&gt;Their corpus is not published — the full text of the HTML version carries no artifact link for it — so nobody outside can recompute those two numbers on their panel. That is why I built my own.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you can run
&lt;/h2&gt;

&lt;p&gt;The whole harness is this. &lt;code&gt;CORPUS&lt;/code&gt; is a dict of tasks, each holding one buggy state, five valid candidates, five invalid ones, and six suites; every candidate defines &lt;code&gt;f&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;ns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;candidate&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exec&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;ns&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ns&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;passes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;suite_src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;impl&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;suite_src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;suite&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exec&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;impl&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

&lt;span class="c1"&gt;# per (task, suite)
&lt;/span&gt;&lt;span class="n"&gt;fails_buggy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;passes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;suite&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buggy&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;accepted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;valid&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;passes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;suite&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;
&lt;span class="n"&gt;rejected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;invalid&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;passes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;suite&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;
&lt;span class="n"&gt;ref&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fails_buggy&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;passes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;suite&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&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="n"&gt;jsf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fails_buggy&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accepted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rejected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Six tasks, six suites each: 36 measured pairs. Every task is a small pure function whose spec leaves one dimension open — rounding, case, punctuation, container type, key order, transliteration — and the valid candidates differ only on those open dimensions. One of them, &lt;code&gt;whole_minutes&lt;/code&gt;, is spec'd as "return the number of whole minutes in a duration given in seconds"; &lt;code&gt;seconds // 60&lt;/code&gt; and &lt;code&gt;round(seconds / 60)&lt;/code&gt; are both legal readings of it.&lt;/p&gt;

&lt;p&gt;I wrote the candidates first, choosing what seemed natural for the spec, and only then wrote the suites the way a suite gets written when its author has the reference open: by asserting what that implementation does on edge inputs. No candidate was edited afterwards to break a suite. That ordering is the only thing standing between this and a rigged result, and you should hold it against me if the numbers look too clean.&lt;/p&gt;

&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Reading&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;single-reference success, all pairs&lt;/td&gt;
&lt;td&gt;20/36 = 55.56%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;joint success, all pairs&lt;/td&gt;
&lt;td&gt;1/36 = 2.78%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;single-reference, pairs that fail the buggy state&lt;/td&gt;
&lt;td&gt;20/22 = 90.91%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;joint success, same pairs&lt;/td&gt;
&lt;td&gt;1/22 = 4.55%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The direction matches the paper and does not match its magnitude. My valid candidates differ more aggressively than theirs, so joint success collapses further. Treat the ratio as not reproduced; treat the direction as reproduced.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I did not expect: two failure modes, indistinguishable
&lt;/h2&gt;

&lt;p&gt;Of the 20 suites that pass the reference implementation, one also passes jointly. The other 19 split like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;How the suite failed&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;rejected at least one valid candidate&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;accepted at least one invalid candidate&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;both&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Eight suites were over-fitted to the reference: &lt;code&gt;palindrome/case_sensitive&lt;/code&gt; asserts &lt;code&gt;f("Aba") is False&lt;/code&gt; because the reference is case-sensitive, and three of my valid candidates lowercase first. Six suites were too weak: the same suite accepted four of five planted defects. Five did both at once.&lt;/p&gt;

&lt;p&gt;A single joint-success number folds all nineteen into "failed". If you are using that number to drive an agent's repair loop — generate a suite, score it, tell the model to try again — the score tells the model that something is wrong and nothing about what. Over-specification wants the assertion loosened; under-assertion wants it strengthened. Those are opposite edits, and the metric cannot distinguish the states that call for them.&lt;/p&gt;

&lt;h2&gt;
  
  
  A third of my suites never failed the buggy state
&lt;/h2&gt;

&lt;p&gt;Fourteen of the 36 suites passed the buggy initial state, which sets &lt;code&gt;r_i(b_i) = 0&lt;/code&gt; and scores them zero under both metrics. That is correct per the definition, and it hides something: &lt;code&gt;percentile/median_odd&lt;/code&gt; asserts &lt;code&gt;f([1, 2, 3, 4, 5], 50) == 3&lt;/code&gt;, and my buggy implementation — which forgets to sort — happens to return 3 for that input. The suite is not over-specified. It is a test that does not test.&lt;/p&gt;

&lt;p&gt;So a zero under joint success means at least three different things: the suite rejects a legal implementation, the suite accepts a broken one, or the suite cannot see the bug at all. Thirds of my corpus, roughly: 14, 8+5, 6+5. Three repairs, one score.&lt;/p&gt;

&lt;h2&gt;
  
  
  Type assertions and empty-input assertions are not tests
&lt;/h2&gt;

&lt;p&gt;The two most contentless shapes I found, both of which pass a reference implementation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;assert isinstance(f(120), int)&lt;/code&gt; accepted three of five defects, including one that returns hours instead of minutes and one that is off by one.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;assert f([], 2) == []&lt;/code&gt; accepted four of five, including a chunker that drops the remainder and one that pads the last chunk with &lt;code&gt;None&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Neither assertion can fail for a wrong answer on any non-empty input, so neither constrains the behaviour the task is about. If you are writing or generating tests, these are the two lines to delete first. They are also the two lines most likely to survive an agent's self-review, because they pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  The number depends on the panel
&lt;/h2&gt;

&lt;p&gt;The paper's own analysis notes that joint success depends on which implementations the panel contains, and studies growing panels. My corpus is small enough to enumerate every panel, so here is the same suites scored on every sub-panel that can be drawn from five valid and five invalid candidates:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Panel&lt;/th&gt;
&lt;th&gt;Evaluations&lt;/th&gt;
&lt;th&gt;Mean joint success&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 valid + 1 invalid&lt;/td&gt;
&lt;td&gt;900&lt;/td&gt;
&lt;td&gt;35.44%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2 + 2&lt;/td&gt;
&lt;td&gt;3,600&lt;/td&gt;
&lt;td&gt;23.22%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3 + 3&lt;/td&gt;
&lt;td&gt;3,600&lt;/td&gt;
&lt;td&gt;14.61%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4 + 4&lt;/td&gt;
&lt;td&gt;900&lt;/td&gt;
&lt;td&gt;8.00%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5 + 5&lt;/td&gt;
&lt;td&gt;36&lt;/td&gt;
&lt;td&gt;2.78%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same suites, same implementations, a factor of 12.7 between the smallest and largest panel. A joint-success figure quoted without its panel size and composition is not portable between corpora. That is not a flaw in the paper — it reports one panel and studies growth on it — but it does mean the headline ratio cannot be compared across papers unless the panel is held fixed, and here the panel is what moves.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would change in an agent loop
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Score the two directions separately. Rejecting a valid candidate and accepting an invalid one are different defects; report &lt;code&gt;valid acceptance rate&lt;/code&gt; and &lt;code&gt;invalid rejection rate&lt;/code&gt; alongside the joint number, as the paper also does, and let the repair prompt see which one moved.&lt;/li&gt;
&lt;li&gt;Keep a few candidates that differ only in format and type. If a suite fails those, it is over-fitted and the fix is to loosen it — cheap to detect, and impossible to see from a single number.&lt;/li&gt;
&lt;li&gt;Never let a type-only or empty-input assertion count as coverage. Both passed a majority of planted defects here.&lt;/li&gt;
&lt;li&gt;Report the panel. If the number moves 12.7x with panel size on six hand-built tasks, it moves on three hundred real ones too.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Limits
&lt;/h2&gt;

&lt;p&gt;Six tasks, 36 suites, all authored by me, on specs I wrote to be deliberately under-determined — which makes this the hard case, not the average case. I authored both the corpus and the suites, and the only defence against a rigged result is the authoring order stated above plus the corpus being short enough to read. The single-reference figure of 90.91% on eligible pairs is not comparable to the paper's 59.67%: different corpus, different tasks, and my valid candidates are more diverse than theirs by construction. Nothing here disputes their finding; it adds the decomposition and the measured panel sensitivity.&lt;/p&gt;

&lt;p&gt;If you want to run it yourself, the harness above plus one task's five valid and five invalid candidates is enough to reproduce a single row of the first table, and that is where I would start. The interesting number is not the joint success rate — it is how often a suite passes the reference while being unable to fail.&lt;/p&gt;




&lt;p&gt;I build EMRG, a self-evolving coding agent, at &lt;a href="https://github.com/argszero/emrg" rel="noopener noreferrer"&gt;github.com/argszero/emrg&lt;/a&gt;; the reason this measurement exists is that its gates were passing when they should have failed, which is the same defect one layer up.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>ai</category>
    </item>
    <item>
      <title>Five ways 'nothing changed since last run' is wrong</title>
      <dc:creator>pm25coder</dc:creator>
      <pubDate>Wed, 21 Oct 2026 23:56:01 +0000</pubDate>
      <link>https://dev.to/pm25coder/five-ways-nothing-changed-since-last-run-is-wrong-495p</link>
      <guid>https://dev.to/pm25coder/five-ways-nothing-changed-since-last-run-is-wrong-495p</guid>
      <description>&lt;p&gt;Every incremental tool I have written contains the same three lines: read a watermark, compare it to the thing you are watching, and if nothing is newer, report that nothing changed.&lt;/p&gt;

&lt;p&gt;It is one comparison. It lied to me five different ways across five cycles of building and repairing it, and &lt;strong&gt;not one of the five printed an error&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The tool is a rotation probe. It answers exactly one question, before any other work happens: &lt;em&gt;which of my registered projects' memory files moved since I last read that project?&lt;/em&gt; Each project gets a row in a Markdown bookmark — the newest memory modification time already read, plus the cycle that read it. The probe walks each project's memory roots, keeps the newest file strictly newer than that row's stamp, and prints what moved.&lt;/p&gt;

&lt;p&gt;Here is what it took to make that answer trustworthy. Every reading below is from a three-project fixture I rebuilt and reran this morning against the current script — not against the one that had the bugs.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The key you look up is not the key that was stored
&lt;/h2&gt;

&lt;p&gt;The register of projects stores a &lt;code&gt;name&lt;/code&gt;. The bookmark stores whatever disambiguates that project when two projects share a name. On my host that is three shapes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;emrg
aitokenpool (github)
gitlab/shaoaq/mem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A probe keyed by the register's &lt;code&gt;name&lt;/code&gt; resolves none of the last two. It reported &lt;strong&gt;12 of 31 projects&lt;/strong&gt; as changed or never visited. &lt;strong&gt;10 of the 12 were my keying&lt;/strong&gt; — the rows existed, keyed another way. Ten projects were invisible to the rotation while the reading said they had nothing to show.&lt;/p&gt;

&lt;p&gt;The output did not look wrong. It looked like a busy round.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Your parser accepts one row shape
&lt;/h2&gt;

&lt;p&gt;The fix for (1) was a pattern for a bookmark row's key cell. My first version required the key to be wrapped in a code span:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;| &lt;span class="sb"&gt;`emrg`&lt;/span&gt; | 2026-10-20 22:01:14 | cyc20261020-220114 |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One row on this host writes the disambiguator &lt;em&gt;outside&lt;/em&gt; the span. That pattern read &lt;strong&gt;30 of 31 rows&lt;/strong&gt;, and the project whose row it missed printed as &lt;strong&gt;never visited&lt;/strong&gt; — the same sentence a project with genuinely no row gets. A parser bug and an unvisited project are indistinguishable in the output.&lt;/p&gt;

&lt;p&gt;The version that shipped accepts both: a span, with an optional parenthetical inside it or just after it.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The stamp format is not the format the next tool parses
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;2026-10-20T22:01:14&lt;/code&gt; and &lt;code&gt;2026-10-20 22:01:14&lt;/code&gt; are the same instant. They are not the same string, and a parser written for the second answers &lt;strong&gt;zero&lt;/strong&gt; for the first. The shell form this tool replaced — &lt;code&gt;find &amp;lt;dir&amp;gt; -newermt '&amp;lt;stamp&amp;gt;'&lt;/code&gt; — parses &lt;code&gt;%Y-%m-%d %H:%M:%S&lt;/code&gt;, so the ISO form hands it nothing to match.&lt;/p&gt;

&lt;p&gt;Zero newer files is, of course, exactly what "nothing changed" looks like. Every project reads unchanged, for ever, on a machine where nothing is wrong except a &lt;code&gt;T&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The fix is not a second format. The probe now parses the stamp itself and compares it against an &lt;code&gt;os.stat&lt;/code&gt; mtime, so a stamp it cannot order is something it can be wrong about out loud:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ check-memory-rotation.py --projects projects.yml --bookmark alpha/.emrg/memory/bookmark.md --all
rc=2
not measurable: the row for gamma carries '2026-10-22T07:53:51', which is not a
%Y-%m-%d %H:%M:%S stamp and cannot be ordered against a file's mtime (time data
'2026-10-22T07:53:51' does not match format '%Y-%m-%d %H:%M:%S')
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exit 2, not exit 0 with a clean list.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The watermark is in the future
&lt;/h2&gt;

&lt;p&gt;Same comparison, opposite direction. A row stamped ahead of the clock — clock skew, a hand edit, a cycle that wrote the stamp it intended to take — makes &lt;strong&gt;every file on disk older than the watermark&lt;/strong&gt;. Nothing is ever newer. The project reads &lt;code&gt;unchanged&lt;/code&gt; for as long as the row stands, and the row stands until somebody edits it by hand.&lt;/p&gt;

&lt;p&gt;A stamp in the future is not a property of the data. It is a statement that this reader could not have read anything yet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rc=2
not measurable: the row for beta carries the stamp 2026-11-21 07:54:51, later than
2026-10-22 07:54:52 - the instant this reading was taken - so it records a memory
modification that has not happened yet: every file on disk is older than it, and the
project would read `unchanged` for as long as the row stands
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The clock is a function, not a &lt;code&gt;datetime.now()&lt;/code&gt; buried at one call site, so a test can pin the boundary instead of racing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. The watermark lives inside the thing it measures
&lt;/h2&gt;

&lt;p&gt;This is the one I published as still open, and then had to rewrite the ending of: the bookmark is a file. On this host it sits inside one project's own memory root — the tool's default path makes it so. So writing the bookmark makes that project's newest memory file &lt;strong&gt;the bookmark&lt;/strong&gt;, and that project can never reach &lt;code&gt;unchanged&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I filed it (&lt;a href="https://github.com/argszero/emrg/issues/2422" rel="noopener noreferrer"&gt;issue #2422&lt;/a&gt;) saying the exclusion would have to hold in two places: when the bookmark is inside a scanned root, and when it is outside one, where it must change nothing. It is closed now, and both halves are measurable, so here is the arm that decides it — three projects, every row stamped a minute in the past, every memory file two minutes in the past, and the bookmark &lt;strong&gt;ten seconds old&lt;/strong&gt;, newer than every file the scan walks, sitting inside &lt;code&gt;alpha&lt;/code&gt;'s own memory root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ check-memory-rotation.py --projects projects.yml --bookmark alpha/.emrg/memory/bookmark.md --all
changed since their bookmark: 0 - every project is at or before its stamp
states: unchanged 3
coverage: every project has a row and every row names a project (3 of 3)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before the fix this printed &lt;code&gt;alpha ... bookmark.md&lt;/code&gt;. An exclusion that only ever hides things is not a fix either, so the other direction: a real memory file in that same project, newer than its row.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="p"&gt;changed since their bookmark: 1
&lt;/span&gt;  alpha  2026-10-22 07:53:51 -&amp;gt; 2026-10-22 07:54:47  alpha\.emrg\sessions\s1\memory\later.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same rows, the bookmark moved outside every root, and the reading is &lt;code&gt;changed 0&lt;/code&gt; again — the exclusion is a no-op where there is nothing to exclude.&lt;/p&gt;

&lt;p&gt;One more thing about that first arm, because it cost me a run: &lt;strong&gt;my first version of it was wrong and did not look wrong.&lt;/strong&gt; I wrote the memory files after computing the stamp, so their mtimes were newer than every row and all three projects reported changed. That is not a broken reading — it is the correct answer to a fixture I had built backwards, and it reads exactly like a round where three things moved. A control arm is a probe too; it needs its own check before it can check anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of the fix, generalised
&lt;/h2&gt;

&lt;p&gt;Five different bugs, one property. &lt;strong&gt;Every one of them made an unmeasurable reading look like a clean one.&lt;/strong&gt; So the rules that came out of it are about the boundary between those two things, not about timestamps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Name your subjects before your verdict.&lt;/strong&gt; The probe prints the register it read and the bookmark it read, with a count: &lt;code&gt;(3 project(s))&lt;/code&gt;, &lt;code&gt;(3 row(s))&lt;/code&gt;. A probe that does not say what it walked cannot be audited.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A reading you cannot take is a distinct state, and it is not a pass.&lt;/strong&gt; Unparseable stamp, future stamp, a row whose state field is none of the known forms: exit 2 with the reason. Never &lt;code&gt;unchanged&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two sides of a mismatch both get named.&lt;/strong&gt; When a bookmark row keys no registered project, the report now names the row &lt;em&gt;and&lt;/em&gt; the project it orphaned, at exit 1. The old behaviour was silence, which read as "nothing to see".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A note is a reading taken on a day, not a property.&lt;/strong&gt; Rows may carry &lt;code&gt;path missing&lt;/code&gt; or &lt;code&gt;no memory root&lt;/code&gt; instead of a stamp — but those get re-measured on every run. Repeating a note would hide a project that came back for ever.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't hand your stamp to another parser.&lt;/strong&gt; Parse it where the comparison happens, so the format has exactly one reader.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tell for this whole class, if you want a one-line check for your own incremental tool: when it says "nothing changed", can it also tell you &lt;strong&gt;how many things it looked at&lt;/strong&gt;, and can it refuse? If a full scan and a broken scan print the same line, you do not have a change detector. You have a silence detector with a counter attached.&lt;/p&gt;

&lt;p&gt;The probe here is step 0 of a self-evolution cycle in &lt;a href="https://github.com/argszero/emrg" rel="noopener noreferrer"&gt;emrg&lt;/a&gt;, a micro-kernel agent harness I maintain — the guard is &lt;code&gt;scripts/check-memory-rotation.py&lt;/code&gt; if you want to read the five verdicts in code. The fixture is three directories, three Markdown files and a table, and every reading in this post came out of it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>programming</category>
      <category>devops</category>
    </item>
    <item>
      <title>It sent gemini-2.5-pro and reported gemini-2.5-flash</title>
      <dc:creator>pm25coder</dc:creator>
      <pubDate>Tue, 20 Oct 2026 17:25:46 +0000</pubDate>
      <link>https://dev.to/pm25coder/it-sent-gemini-25-pro-and-reported-gemini-25-flash-311</link>
      <guid>https://dev.to/pm25coder/it-sent-gemini-25-pro-and-reported-gemini-25-flash-311</guid>
      <description>&lt;p&gt;A response's &lt;code&gt;model&lt;/code&gt; field is not decoration. It is what your logs record, what your cost attribution divides by, what a router uses to pick the next call, and what a UI puts under the message. When it is wrong it is wrong quietly, and every downstream reader inherits it.&lt;/p&gt;

&lt;p&gt;Here is a case where two call paths of the same client disagree about that field, for the same request, on the same server reply.&lt;/p&gt;

&lt;h2&gt;
  
  
  The measurement
&lt;/h2&gt;

&lt;p&gt;Two calls. Same client, same per-request model override, same reply shape — a reply that carries no &lt;code&gt;model_version&lt;/code&gt;. One call streams, the other does not.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;call&lt;/th&gt;
&lt;th&gt;reported model&lt;/th&gt;
&lt;th&gt;requested model&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_response(stream=True)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gemini-2.5-pro&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gemini-2.5-pro&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_response()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gemini-2.5-flash&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gemini-2.5-pro&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The second row is the client's &lt;em&gt;configured&lt;/em&gt; model, not the one this request resolved to. The client was built with &lt;code&gt;model="gemini-2.5-flash"&lt;/code&gt; and asked for &lt;code&gt;options={"model": "gemini-2.5-pro"}&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I did not invent the streaming test; that one is theirs. I wrote its non-streaming twin — same fixture factory, same options dict, same absent &lt;code&gt;model_version&lt;/code&gt;, only the call path differs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;test_streaming_falls_back_to_requested_model      PASSED
test_non_streaming_falls_back_to_requested_model  FAILED
E   AssertionError: assert 'gemini-2.5-flash' == 'gemini-2.5-pro'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run against &lt;code&gt;agent-framework-gemini&lt;/code&gt; 1.0.0b261015 from PyPI, and the same two lines are still on &lt;code&gt;main&lt;/code&gt; today.&lt;/p&gt;

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

&lt;p&gt;Three lines in the non-streaming path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;contents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_prepare_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;validated&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="c1"&gt;# the model this request resolved to
&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generate_content&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;contents&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;contents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# sent correctly
&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_process_generate_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response_format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;...)&lt;/span&gt;            &lt;span class="c1"&gt;# ...and not handed on
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and inside &lt;code&gt;_process_generate_response&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;model_version&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two different &lt;code&gt;model&lt;/code&gt;s, one frame apart. &lt;code&gt;self.model&lt;/code&gt; is the client's configured default; the local &lt;code&gt;model&lt;/code&gt; is what this request resolved to after the options override. When the reply omits &lt;code&gt;model_version&lt;/code&gt;, &lt;code&gt;or&lt;/code&gt; falls through to the attribute — so the label reports whatever the client was constructed with, no matter what was actually asked for or sent.&lt;/p&gt;

&lt;p&gt;The streaming path does not have this problem, because it takes the resolved model as a parameter, and its docstring says exactly the right thing:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;the last version the server reported in this stream, or the model resolved for this request&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The part that makes it interesting
&lt;/h2&gt;

&lt;p&gt;This is not an oversight, and I want to be careful about that, because the thread tells a better story than "someone missed a path".&lt;/p&gt;

&lt;p&gt;The person who fixed the streaming case &lt;strong&gt;named the sibling himself&lt;/strong&gt;, on the pull request, before it merged:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;_process_generate_response&lt;/code&gt; has the same client-default fallback at &lt;code&gt;:1058&lt;/code&gt;. I left it alone to keep this PR on the streaming path — happy to align it here or in a follow-up, whichever you prefer.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;microsoft/agent-framework#9139&lt;/code&gt; merged on 2026-10-13. Six days before that he had flagged the sibling; four days before, another comment reported the conflict resolved and the diff as "4 lines in &lt;code&gt;_chat_client.py&lt;/code&gt; and the 48 test lines". The follow-up was never asked for, never refused, and never tracked. Today the sibling still reads &lt;code&gt;self.model&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So the defect's history is: found, described precisely, offered as a choice, and then closed by the sweep of an issue going green. Not hidden — &lt;em&gt;unanswered&lt;/em&gt;. That is a much more ordinary way for a bug to survive, and a much more useful one to learn from, because the same thing happens in every repository where a fixer is being polite about scope.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a green suite does not see it
&lt;/h2&gt;

&lt;p&gt;The suite has eleven tests with "model" in the name. All eleven pass on this install. What separates them is not count but &lt;em&gt;which boundary they assert&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;streaming: &lt;code&gt;updates[0].model == "gemini-2.5-pro"&lt;/code&gt; — the delivery boundary.&lt;/li&gt;
&lt;li&gt;non-streaming: &lt;code&gt;call_kwargs["model"] == "gemini-2.5-pro"&lt;/code&gt; — the request boundary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both sentences are true about this code. They are just not the same sentence: one asks &lt;em&gt;what did we report&lt;/em&gt;, the other asks &lt;em&gt;what did we send&lt;/em&gt;. And the single non-streaming test that does look at &lt;code&gt;response.model&lt;/code&gt; always hands the mock a &lt;code&gt;model_version&lt;/code&gt;, so the &lt;code&gt;or self.model&lt;/code&gt; branch is unreachable from the suite.&lt;/p&gt;

&lt;p&gt;Tallied: four streaming tests about the reported model, one non-streaming, zero non-streaming tests about the fallback.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape worth carrying
&lt;/h2&gt;

&lt;p&gt;Not really about Gemini, or about this package. Four checkable parts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Ask the assertion at the delivery boundary.&lt;/strong&gt; For each pair of sibling call paths, the test that matters asserts what the caller receives. A request-boundary assertion still earns its place — it catches a different class of bug — but it cannot stand in for the other one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A fallback that reads &lt;code&gt;self.X&lt;/code&gt; is a fallback to the wrong value whenever the resolved value is a local variable.&lt;/strong&gt; Here the resolved model is in scope one frame up. The attribute wins by accident of where the code sits, not by decision.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write the case with the field absent.&lt;/strong&gt; A test that supplies &lt;code&gt;model_version&lt;/code&gt; exercises the happy path and says nothing about the branch that only runs when the server stays quiet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Happy to align it here or in a follow-up, whichever you prefer" is a defect with a timer on it.&lt;/strong&gt; Every reviewer reads it as diligence, and it is — but the choice lives in a comment, and comments do not survive a closing issue. If you write that sentence, the follow-up needs an issue number in it, or the polite version of leaving the bug in is the version that keeps it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The fix here is one argument: pass the resolved &lt;code&gt;model&lt;/code&gt; into &lt;code&gt;_process_generate_response&lt;/code&gt; and use &lt;code&gt;response.model_version or model&lt;/code&gt;. Then write the twin — the streaming test already exists, so the twin is a copy with &lt;code&gt;stream=True&lt;/code&gt; deleted.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I did not measure
&lt;/h2&gt;

&lt;p&gt;All of this runs on mocks; I did not call the live Gemini API. So the honest scope is narrow and sufficient: &lt;em&gt;given a reply with no &lt;code&gt;model_version&lt;/code&gt;, the two call paths report different things.&lt;/em&gt; Whether Gemini's API omits &lt;code&gt;model_version&lt;/code&gt; in practice is not something I measured — but a fallback branch exists because it can, and there is already a test for exactly that case on the other leg.&lt;/p&gt;

&lt;p&gt;What I can add is that the streaming half is correct and tested. This is not a broken fix. It is a fix that did what its author said it would do, on the path it was aiming at.&lt;/p&gt;

&lt;p&gt;The reading you want, when a fix lands on one of two siblings, is not "did the fix work" but "what did its author leave for later — and did anyone come back?"&lt;/p&gt;

</description>
      <category>python</category>
      <category>ai</category>
      <category>testing</category>
      <category>devops</category>
    </item>
    <item>
      <title>Your mutation score was 0.81 before the agent wrote a test</title>
      <dc:creator>pm25coder</dc:creator>
      <pubDate>Mon, 19 Oct 2026 22:04:09 +0000</pubDate>
      <link>https://dev.to/pm25coder/your-mutation-score-was-081-before-the-agent-wrote-a-test-3pe2</link>
      <guid>https://dev.to/pm25coder/your-mutation-score-was-081-before-the-agent-wrote-a-test-3pe2</guid>
      <description>&lt;p&gt;If you are building evals for coding agents, at some point the question stops being "did it change the code" and becomes "did it write the tests". Coverage is the obvious metric and the obviously wrong one: a test that calls a function without asserting anything still counts as coverage. Mutation testing is the honest replacement — mutate the code, see whether the tests notice — and on a small module it is now cheap enough to run per diff, seconds to a minute.&lt;/p&gt;

&lt;p&gt;So the metric becomes a mutation score: mutants killed over mutants tried. That is a ratio, and what it is a ratio of is the whole question.&lt;/p&gt;

&lt;h2&gt;
  
  
  The number I got before writing a test
&lt;/h2&gt;

&lt;p&gt;I spent a morning reading a Python repo's mutation tooling: a script that generates mutants with cosmic-ray, runs each one against the tests that coverage says touch the changed lines, and reports what survived. On one 144-line module the authors had published the breakdown of a single full run:&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;count&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;mutants&lt;/td&gt;
&lt;td&gt;342&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;killed&lt;/td&gt;
&lt;td&gt;237&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;survived&lt;/td&gt;
&lt;td&gt;48&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;survived (string-only)&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;crashed&lt;/td&gt;
&lt;td&gt;49&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Run that through the score they were adding — killed over tried, with crashes excluded because no test has to notice a crash:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;score = 237 / (237 + 48 + 8) = 0.809
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nobody had written a test yet. That 0.809 is the existing suite, measured on mutants drawn from that module.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the ratio cannot say
&lt;/h2&gt;

&lt;p&gt;Now suppose you use that ratio to grade an agent: give it a change with tests to write, run the mutation score on the diff, record the number. Two failures hide in the arithmetic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The kills are not attributable.&lt;/strong&gt; The runner executes every test that coverage credits with the changed lines — including the ones already in the file. So on a change inside a well-covered area, most of the kills belong to tests that predate the agent, and the score is high whatever the agent wrote. On a change in a thin spot, the same quality of new test scores low. Two diffs of the same size can differ by more than the effect you are trying to measure, and the difference is a property of the module, not of the agent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The function that prints the ratio is usually the one that cannot attribute.&lt;/strong&gt; Mutation runners are fastest when they stop at the first failing test: short circuit, record "killed", move on. That is exactly what you want for the human-facing question ("do these tests catch this break?") and exactly what you lose for the eval question ("which test caught it?"). In the tool I read, the fast path passes &lt;code&gt;-x&lt;/code&gt; to pytest, so a mutant is recorded as killed with no record of the killer — while a second function in the same file, the one that builds the full kill matrix, runs without &lt;code&gt;-x&lt;/code&gt; and stores the sorted list of failing tests per mutant. Both numbers exist. The ratio was computed from the one that throws the attribution away.&lt;/p&gt;

&lt;h2&gt;
  
  
  Record the delta, not the ratio
&lt;/h2&gt;

&lt;p&gt;The eval number you want is the difference between two runs over the same mutant set:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what the tests already there kill, and&lt;/li&gt;
&lt;li&gt;what those plus the agent's tests kill.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The difference — mutants no old test kills that a new test does — is immune to how well covered the module was before, and it is what "the agent wrote tests for this change" actually claims. A delta of zero then reads correctly as "the existing tests already killed these", where the raw ratio renders it as a 0.8 pass.&lt;/p&gt;

&lt;p&gt;It costs one more run of the same matrix, not a new instrument. If the runner already stores killers per mutant, the subtraction is a set difference; if it only stores outcomes, one run over the union of old and new tests and one over the old tests alone gets you there.&lt;/p&gt;

&lt;h2&gt;
  
  
  The general shape
&lt;/h2&gt;

&lt;p&gt;A ratio shipped without its reference set is a number with two causes. The reference is almost always one more run of the same instrument, and what it buys is the ability to say whose contribution the difference is.&lt;/p&gt;

&lt;p&gt;This is not specific to mutation testing. "The agent fixed 7 of 10 issues" — against what baseline? "The prompt change cut latency by 40%" — from which distribution? The reading you can act on is the one that moves only when the thing you changed moves.&lt;/p&gt;

&lt;p&gt;If you take one thing from the number at the top: before the score means anything, compute it on the code as it was. 0.809 was the starting line, and every agent on that module started from it.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>python</category>
      <category>devtools</category>
    </item>
    <item>
      <title>An allow-list does not fail closed</title>
      <dc:creator>pm25coder</dc:creator>
      <pubDate>Mon, 12 Oct 2026 00:01:44 +0000</pubDate>
      <link>https://dev.to/pm25coder/an-allow-list-does-not-fail-closed-3h86</link>
      <guid>https://dev.to/pm25coder/an-allow-list-does-not-fail-closed-3h86</guid>
      <description>&lt;p&gt;Somebody told me last week, on a thread about a Claude Code plugin that stops an agent from editing its own tests, that an allow-list "fails closed, which is the failure mode you want when the agent is the one trying the edge case."&lt;/p&gt;

&lt;p&gt;That is the right intuition and the wrong conclusion. It is worth pulling apart, because the same reasoning shows up every time someone hardens an agent's shell.&lt;/p&gt;

&lt;h2&gt;
  
  
  The guard
&lt;/h2&gt;

&lt;p&gt;The plugin (&lt;a href="https://github.com/Mattbusel/tamperproof" rel="noopener noreferrer"&gt;&lt;code&gt;Mattbusel/tamperproof&lt;/code&gt;&lt;/a&gt;, v0.1.1) replaced its deny-list of "writer" commands with an allow-list — the right direction, because a deny-list only covers the verbs someone thought of. The new rule, from &lt;code&gt;hooks/guard.mjs&lt;/code&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Any shell segment that names an existing protected path is blocked, unless it starts with a known read-only or test-runner command and contains no redirect or tee.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Read the first clause slowly. &lt;strong&gt;"names an existing protected path"&lt;/strong&gt; is what decides whether the rule is applied at all. Here is the code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;for &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;piece&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;seg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[\s&lt;/span&gt;&lt;span class="sr"&gt;'"`,;()=&amp;lt;&amp;gt;{}&lt;/span&gt;&lt;span class="se"&gt;\[\]]&lt;/span&gt;&lt;span class="sr"&gt;+/&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;piece&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;piece&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;a-z&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+:&lt;/span&gt;&lt;span class="se"&gt;\/\/&lt;/span&gt;&lt;span class="sr"&gt;/i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;piece&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;continue&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;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;piece&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;..&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;isProtected&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;protect&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;existsSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="nx"&gt;hits&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;r&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                 &lt;span class="c1"&gt;// &amp;lt;- never judged, so allowed&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;READ_ONLY&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;writesOut&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;block&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;existsSync(resolve(cwd, piece))&lt;/code&gt;, on the literal token. If no token looks like an existing protected file, the segment is skipped — it is not judged, and not being judged means allowed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I measured
&lt;/h2&gt;

&lt;p&gt;I checked out &lt;code&gt;7559493&lt;/code&gt; (v0.1.1), built a throwaway tree containing &lt;code&gt;test/calc.test.mjs&lt;/code&gt;, and drove the guard the way the hook does — the tool call on stdin, the exit code read back. Same command in every row; only the target's spelling moves:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; s/5/-1/ &lt;span class="nb"&gt;test&lt;/span&gt;/calc.test.mjs          -&amp;gt; &lt;span class="nb"&gt;exit &lt;/span&gt;2   blocked
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; s/5/-1/ &lt;span class="nb"&gt;test&lt;/span&gt;/calc.test.j?           -&amp;gt; &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; s/5/-1/ &lt;span class="nb"&gt;test&lt;/span&gt;/&lt;span class="k"&gt;*&lt;/span&gt;.mjs                  -&amp;gt; &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; s/5/-1/ &lt;span class="nb"&gt;test&lt;/span&gt;/calc.test.&lt;span class="o"&gt;{&lt;/span&gt;mjs&lt;span class="o"&gt;}&lt;/span&gt;        -&amp;gt; &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; s/5/-1/ &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PWD&lt;/span&gt;&lt;span class="s2"&gt;/test/calc.test.mjs"&lt;/span&gt;   -&amp;gt; &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; s/5/-1/ &lt;span class="nb"&gt;test&lt;/span&gt;/nonexistent.mjs        -&amp;gt; &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The literal answers 2 because it &lt;em&gt;is&lt;/em&gt; a file: &lt;code&gt;existsSync&lt;/code&gt; says yes, the path is protected, the segment is judged, &lt;code&gt;sed -i&lt;/code&gt; is not on the read-only list, blocked.&lt;/p&gt;

&lt;p&gt;The rest answer 0 because &lt;code&gt;existsSync&lt;/code&gt; says no. &lt;code&gt;test/*.mjs&lt;/code&gt; is a &lt;strong&gt;token&lt;/strong&gt;, not a path — the guard asks the filesystem about the string &lt;code&gt;test/*.mjs&lt;/code&gt;, and that string is not a file. Same for &lt;code&gt;?&lt;/code&gt;; same for the brace form (the splitter even cuts &lt;code&gt;{&lt;/code&gt; and &lt;code&gt;}&lt;/code&gt; out, leaving &lt;code&gt;test/calc.test.&lt;/code&gt;); same for &lt;code&gt;$PWD/...&lt;/code&gt;, which resolves under a directory literally named &lt;code&gt;$PWD&lt;/code&gt;. Nothing matches, so &lt;code&gt;hits&lt;/code&gt; stays empty and the rule is never consulted.&lt;/p&gt;

&lt;p&gt;And a spelling &lt;em&gt;is&lt;/em&gt; expanded by a real shell before &lt;code&gt;sed&lt;/code&gt; ever sees it — that is the entire job of a glob. So the string defeats the check whose subject is the string.&lt;/p&gt;

&lt;h2&gt;
  
  
  The generalisation, which is the useful part
&lt;/h2&gt;

&lt;p&gt;Which way a guard fails is not a property of allow-list versus deny-list. It is a property of &lt;strong&gt;where the default sits in the decision chain&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This guard is default-&lt;strong&gt;deny&lt;/strong&gt; at the verdict: a segment that gets judged and is not read-only is blocked. And it is default-&lt;strong&gt;allow&lt;/strong&gt; at the selector: a segment that is never judged is allowed. The selector is the earlier of the two, and it is the layer a command string drives. So the guard fails closed exactly where the attacker has no freedom, and fails open exactly where they do.&lt;/p&gt;

&lt;p&gt;"An allow-list fails closed" is true only on the axis you happened to enumerate — here, the verb. Enumerate the axis the attacker's input controls, or you have measured the wrong thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The other direction is real too
&lt;/h2&gt;

&lt;p&gt;Two arms came back the other way, and they are why I would not call even the verb axis simply "closed":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat test&lt;/span&gt;/calc.test.mjs &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null      -&amp;gt; &lt;span class="nb"&gt;exit &lt;/span&gt;2
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s1"&gt;'=&amp;gt;'&lt;/span&gt; &lt;span class="nb"&gt;test&lt;/span&gt;/calc.test.mjs         -&amp;gt; &lt;span class="nb"&gt;exit &lt;/span&gt;2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;writesOut&lt;/code&gt; is &lt;code&gt;/&amp;gt;/.test(seg)&lt;/code&gt;, so the arrow in a grep pattern reads as a redirect, and &lt;code&gt;cat ... &amp;gt; /dev/null&lt;/code&gt; is a redirect to nowhere. Both are things an agent does constantly while debugging a failing test — the exact state the guard exists to keep it in. A false block is not free: it teaches the reader to route around the guard, and then the guard protects nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would take from it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Name which layer the default lives in, per rule.&lt;/strong&gt; "Blocked unless read-only" and "not looked at" are different verdicts, and only one of them is visible in a code review.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask the question about the thing that will be written, not the string that names it&lt;/strong&gt; — or accept that the string is a spelling and chase spellings. The author already named the honest tension: treating a glob-shaped token as a hit closes this hole and also blocks a genuinely new test written from a shell.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep a control arm in both directions.&lt;/strong&gt; All twenty of the guard's test cases name the path, and so did every arm I sent before looking. A case count is not a coverage claim — the question is which &lt;em&gt;axis&lt;/em&gt; the cases move on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The plugin is a genuinely good piece of work, and 0.1.1 closed every hole the previous version had, including all of the above for the literal spellings. This is the one axis that survived a rewrite, and it will survive the next one too unless the fix goes into the selector. I wrote up the thread it came from &lt;a href="https://dev.to/mattbusel/i-tried-to-make-claude-code-cheat-on-tests-heres-when-it-did-2a0k"&gt;here&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>security</category>
      <category>programming</category>
      <category>testing</category>
      <category>ai</category>
    </item>
    <item>
      <title>Your validator printed the right verdict and exited 0</title>
      <dc:creator>pm25coder</dc:creator>
      <pubDate>Sun, 04 Oct 2026 14:18:33 +0000</pubDate>
      <link>https://dev.to/pm25coder/your-validator-printed-the-right-verdict-and-exited-0-31jj</link>
      <guid>https://dev.to/pm25coder/your-validator-printed-the-right-verdict-and-exited-0-31jj</guid>
      <description>&lt;p&gt;A CI gate is one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aca scan ./attestations
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Non-zero exit means "do not merge". That single bit is the whole contract between the tool and the pipeline.&lt;/p&gt;

&lt;p&gt;On a directory that contained &lt;strong&gt;only&lt;/strong&gt; forged and expired attestations, that command exited &lt;code&gt;0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The tool was not silent about it. It printed, for every file it read, the correct verdict:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✗ agent://planner-v2 -&amp;gt; agent://worker-v3
  ERROR: signature does not match payload - attestation may be forged
✗ agent://worker-v3 -&amp;gt; agent://reporter-v1
  ERROR: expired at 2026-08-30T12:00:00Z (ttl 300s)
✗ agent://reporter-v1 -&amp;gt; agent://auditor-v1
  ERROR: no trusted key for issuer agent://reporter-v1

3 attestations scanned, 3 stale
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every fact a human needed was on stdout. The process boundary dropped it, and the process boundary is the only place a CI system reads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the exit code came from
&lt;/h2&gt;

&lt;p&gt;The multi-file scan looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@click.option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--fail-on-stale&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;is_flag&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Exit 1 if any attestation is stale&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;scan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_ttl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_skew_seconds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fail_on_stale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json_output&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...):&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="n"&gt;all_valid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;attestation_files&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;validator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Attestation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;())))&lt;/span&gt;
            &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_valid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;all_valid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;click&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;echo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ERROR reading &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;all_valid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

    &lt;span class="c1"&gt;# ... print results ...
&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;fail_on_stale&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;all_valid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;is_flag=True&lt;/code&gt; means the default is &lt;code&gt;False&lt;/code&gt;. So the function's only &lt;code&gt;sys.exit(1)&lt;/code&gt; sat behind an opt-in flag; when the flag was absent, the function returned normally and the framework's own default — &lt;code&gt;0&lt;/code&gt; — became the process status. Nothing in the code said "green"; the exit code was simply the value nobody had bothered to set.&lt;/p&gt;

&lt;p&gt;The flag's own name is what makes this a contradiction rather than a design choice: &lt;code&gt;--fail-on-stale&lt;/code&gt; reads as &lt;em&gt;"also&lt;/em&gt; fail on stale" — as if failing were already the norm and stale were an addition. Stale was the only thing that failed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The siblings were all fail-closed
&lt;/h2&gt;

&lt;p&gt;The single-file command in the same CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;validator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attestation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_valid&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The chain checker and the MCP config checker were built the same way. So the multi-file command — the one written to back a CI gate — was the single opt-in exception in a CLI of fail-closed commands.&lt;/p&gt;

&lt;h2&gt;
  
  
  The half the tests never asserted
&lt;/h2&gt;

&lt;p&gt;The suite already had tests for &lt;code&gt;scan&lt;/code&gt;. They asserted the printed text: that &lt;code&gt;ERROR: ... may be forged&lt;/code&gt; appears on stdout, that the summary count is right. Every one of them passed both before and after the fix, because the defect lived entirely in the half nobody asserted.&lt;/p&gt;

&lt;p&gt;That is the generalisable move for any output-versus-meaning defect: &lt;strong&gt;ask where the claim is consumed.&lt;/strong&gt; A verdict line, a summary count, and an exit code are three claims. Only the third is read by the machine that decides whether the change lands.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: name the exception, not the rule
&lt;/h2&gt;

&lt;p&gt;Fail closed by default; make the unsafe direction explicit and give it a name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@click.option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--report-only&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;is_flag&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nb"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Always exit 0 and report findings on stdout only. Off by default: a &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
          &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scan fails closed (exit 1) whenever any attestation is invalid.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nd"&gt;@click.option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--fail-on-stale&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;is_flag&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nb"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Deprecated and redundant: scan now fails closed on any invalid &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
          &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;attestation, not only on stale ones. Accepted for backwards &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
          &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;compatibility.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…and the two are rejected together, because a flag combination that cannot be honoured is a usage error, not a verdict:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;fail_on_stale&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;report_only&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;click&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;echo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ERROR: --fail-on-stale and --report-only are mutually exclusive&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;all_valid&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;report_only&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&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="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The old flag is kept for one release so the documented CI recipe keeps working — but its meaning moved. Before, "fail the build" was a mode you opted into; now "stay green" is. Defaults should point at the safe direction, and the exception should be the thing that has to be spelled out.&lt;/p&gt;

&lt;h2&gt;
  
  
  The aftershock: my test proved a different failure
&lt;/h2&gt;

&lt;p&gt;The fix came with ten new tests, 176 passing. One of them built an attestation whose signature was &lt;code&gt;ed25519:ababab...&lt;/code&gt; — a signature no key can ever verify. Unfixed, &lt;code&gt;scan&lt;/code&gt; exited 0; fixed, it exited 1. That looked like the whole story, and the mutation arm agreed: delete the fix and the new tests go red.&lt;/p&gt;

&lt;p&gt;The maintainer merged the PR and added a fifty-nine-line eleventh test to the merged tree. Its docstring is the part worth quoting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"""The forgery the CHANGELOG entry actually describes.

The test above supplies a signature no key can ever verify, so the
verdict it produces is 'no trusted key for this issuer' - the
*unverifiable* path. That is a real defect class, but it is not the one
this PR claims to fix: it cannot distinguish 'the verifier had nothing
to check against' from 'the verifier checked and the bytes did not match'.
Here the issuer's real public key is trusted and the signature is valid
over the original payload; the attacker then rewrites ``capability`` on
disk. That is the attack the signature exists to stop, it produces the
distinct ``Signature does not match payload`` error, and before the fix
it exited 0.
"""
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;He was right. Two error strings — &lt;em&gt;no trusted key for this issuer&lt;/em&gt; and &lt;em&gt;signature does not match payload&lt;/em&gt; — are two different defect classes with two different fixes. The changelog said "forged"; my test exercised "unverifiable". Both exited 0, so both were cured by the same one-line change, and the mutation arm happily confirmed the coverage I claimed. But the test I shipped would keep passing if someone reintroduced the specific forgery path, because it never constructs a valid signature to tamper with.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Assert the case the claim names.&lt;/strong&gt; If the entry says &lt;em&gt;forged&lt;/em&gt;, build a forgery: a real key, a valid signature over the original bytes, and a payload rewritten afterwards. A weaker neighbour is a different branch, and the branch is only the same case if you don't read the error string.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;For an output-versus-meaning defect, ask &lt;strong&gt;where the claim is consumed&lt;/strong&gt;: stdout is read by humans, the exit code by the machine.&lt;/li&gt;
&lt;li&gt;A default belongs on the safe direction; the exception gets a name (&lt;code&gt;--report-only&lt;/code&gt;), and flag combinations that cannot be honoured are usage errors — exit 2, not a verdict.&lt;/li&gt;
&lt;li&gt;If a flag reads "also fail on X", check whether X was the only thing that failed.&lt;/li&gt;
&lt;li&gt;A returned status is a claim like any other; assert it, not only the text printed beside it.&lt;/li&gt;
&lt;li&gt;Run every new test against the unfixed code — and then check that the test's name describes the failure the claim makes, not its closest neighbour.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>testing</category>
      <category>programming</category>
      <category>python</category>
      <category>devops</category>
    </item>
    <item>
      <title>The test that passed on Windows and failed on Linux</title>
      <dc:creator>pm25coder</dc:creator>
      <pubDate>Sat, 03 Oct 2026 05:56:58 +0000</pubDate>
      <link>https://dev.to/pm25coder/the-test-that-passed-on-windows-and-failed-on-linux-37ge</link>
      <guid>https://dev.to/pm25coder/the-test-that-passed-on-windows-and-failed-on-linux-37ge</guid>
      <description>&lt;p&gt;My CI went red on a pull request that was, as far as I could tell, correct.&lt;/p&gt;

&lt;p&gt;The change was small and boring: an OAuth helper in a CLI tool used to discover a free port by binding it, reading the number, and closing the socket — which leaves a window in which any other process can take the port before the callback server binds it. The fix binds once and hands the already-bound socket to the HTTP server, so there is no window. I measured it on my machine: with the old helper another process could take the port; with the new one it could not. I added a test, ran it, and pushed.&lt;/p&gt;

&lt;p&gt;Then CI came back: &lt;strong&gt;Test 3.12, 3.13, Canary — all failed&lt;/strong&gt;, on the &lt;strong&gt;Linux&lt;/strong&gt; legs, while the identical suite was green on &lt;strong&gt;Windows&lt;/strong&gt; on the same commit. The one failing test was the one I had just written:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OSError: [Errno 98] Address already in use
809 passed, 1 failed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix was fine. The test I shipped with the fix was not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test that broke
&lt;/h2&gt;

&lt;p&gt;The test asked a simple question: after the OAuth flow finishes, is the redirect port released? And it answered it in the most obvious way — by trying to bind the port again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_port_is_free&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;OSError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a fresh listener can bind, the port is free. That is the intuitive phrasing. It is also the wrong instrument, and the reason is entirely about &lt;strong&gt;which states of a TCP connection&lt;/strong&gt; a rebind is willing to forgive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt one: blaming TIME_WAIT
&lt;/h2&gt;

&lt;p&gt;My first hypothesis was the classic one: the server has closed, but the connection sits in &lt;code&gt;TIME_WAIT&lt;/code&gt;, and &lt;code&gt;TIME_WAIT&lt;/code&gt; blocks a rebind. The standard fix for that is &lt;code&gt;SO_REUSEADDR&lt;/code&gt;, so I set it on the probe socket and pushed again.&lt;/p&gt;

&lt;p&gt;CI went red again — with a new signal that killed the hypothesis. The probe now returned &lt;code&gt;False&lt;/code&gt; &lt;strong&gt;even with &lt;code&gt;SO_REUSEADDR&lt;/code&gt; set&lt;/strong&gt;. If &lt;code&gt;TIME_WAIT&lt;/code&gt; were the cause, &lt;code&gt;SO_REUSEADDR&lt;/code&gt; is exactly what would have forgiven it. It didn't, so the port was not merely in &lt;code&gt;TIME_WAIT&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The tell was sitting right next to it. There were two tests: one where the flow completes (the failing one) and one where it does not. &lt;strong&gt;The sibling, which also rebinds the port, passed.&lt;/strong&gt; The only difference between them was that in the failing test a client had actually connected and received a response. That points at a &lt;strong&gt;lingering accepted connection&lt;/strong&gt; — a socket in &lt;code&gt;FIN_WAIT_2&lt;/code&gt; or &lt;code&gt;ESTABLISHED&lt;/code&gt; — and &lt;code&gt;SO_REUSEADDR&lt;/code&gt; deliberately does not forgive &lt;em&gt;those&lt;/em&gt;. It forgives &lt;code&gt;TIME_WAIT&lt;/code&gt; because a timed-out connection is genuinely gone from the application's point of view; it does not forgive a live connection because that would let two listeners fight over the same 4-tuple.&lt;/p&gt;

&lt;p&gt;So the failure had two independent halves:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;the probe used an instrument (&lt;code&gt;bind&lt;/code&gt;) that is sensitive to connection state I did not control;&lt;/li&gt;
&lt;li&gt;the test's fake client never closed its response, so a connection really was left hanging — a genuine leak in the test itself, the same kind of leak the production fix was about.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Attempt two: change the question, not the port
&lt;/h2&gt;

&lt;p&gt;The fix was to stop asking "can something bind here" and start asking &lt;strong&gt;"is anything still listening here"&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_nothing_is_listening&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;          &lt;span class="c1"&gt;# something answered
&lt;/span&gt;        &lt;span class="nf"&gt;except &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;ConnectionRefusedError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;OSError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;           &lt;span class="c1"&gt;# nothing there
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;connect()&lt;/code&gt; that is &lt;em&gt;refused&lt;/em&gt; tells you there is no listener — and it does not care what state some old connection is in, because it never touches that connection's 4-tuple. That is the property I actually wanted to assert: "the server is gone", not "the kernel will let me rebind". I also closed the response in the fake client, removing the leak underneath.&lt;/p&gt;

&lt;p&gt;CI returned 5/5 green.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proving the probe isn't vacuous
&lt;/h2&gt;

&lt;p&gt;A negative assertion has a failure mode that a positive one does not: it can be &lt;strong&gt;vacuously true&lt;/strong&gt;. A probe that always says "nothing is listening" would pass the test perfectly and tell you nothing. So I added a control arm that runs the same probe against three states:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;state&lt;/th&gt;
&lt;th&gt;&lt;code&gt;_nothing_is_listening&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;a listener is up&lt;/td&gt;
&lt;td&gt;&lt;code&gt;False&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;listener just closed, cleanly&lt;/td&gt;
&lt;td&gt;&lt;code&gt;True&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;a connection lingers, no listener&lt;/td&gt;
&lt;td&gt;&lt;code&gt;True&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The first row is the one that matters: the probe &lt;em&gt;can&lt;/em&gt; return &lt;code&gt;False&lt;/code&gt;, so the &lt;code&gt;True&lt;/code&gt; in the test means something. Without that row, "nothing is listening" is an unfalsifiable claim.&lt;/p&gt;

&lt;h2&gt;
  
  
  What generalises
&lt;/h2&gt;

&lt;p&gt;Three things, and none of them are about OAuth or Python.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A fix's own test is part of the fix.&lt;/strong&gt; The pull request was not "the socket handoff"; it was "the socket handoff &lt;em&gt;and a test that verifies it on every platform the suite runs on&lt;/em&gt;". A green local run on one OS is a sample, not a verdict — and when a CI leg fails where you did not look, the bug is often in the &lt;strong&gt;instrument&lt;/strong&gt;, not the code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For a negative assertion, choose an instrument that is insensitive to the state you do not control.&lt;/strong&gt; "The port is released" is a claim about the &lt;em&gt;server&lt;/em&gt; being gone. &lt;code&gt;bind&lt;/code&gt;-succeeds smuggles in an assumption about kernel connection bookkeeping; &lt;code&gt;connect&lt;/code&gt;-refused does not. Pick the phrasing that says only what you mean.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Give every negative probe a control arm.&lt;/strong&gt; A probe that cannot fail is not evidence. One extra case — the state in which it must return the other answer — is what turns "the test passed" into "the property held".&lt;/p&gt;

&lt;p&gt;The second attempt was smaller than the first and took less time to write. The expensive part was not the fix; it was noticing that the instrument, not the code, was the thing that needed changing.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>programming</category>
      <category>linux</category>
      <category>python</category>
    </item>
  </channel>
</rss>
