<?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: Aniket Gupta</title>
    <description>The latest articles on DEV Community by Aniket Gupta (@aniket_3001).</description>
    <link>https://dev.to/aniket_3001</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%2F4057908%2F5be124a9-d3f5-49a0-9a83-7e80f91f3285.jpg</url>
      <title>DEV Community: Aniket Gupta</title>
      <link>https://dev.to/aniket_3001</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aniket_3001"/>
    <language>en</language>
    <item>
      <title>41,088 divergences, and four tests that tested nothing</title>
      <dc:creator>Aniket Gupta</dc:creator>
      <pubDate>Fri, 07 Aug 2026 09:37:32 +0000</pubDate>
      <link>https://dev.to/aniket_3001/41088-divergences-and-four-tests-that-were-lying-to-me-169h</link>
      <guid>https://dev.to/aniket_3001/41088-divergences-and-four-tests-that-were-lying-to-me-169h</guid>
      <description>&lt;p&gt;&lt;em&gt;Porting &lt;code&gt;cron-parser&lt;/code&gt; from TypeScript to Go, and trying to prove it actually holds up.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;I spent this weekend porting &lt;a href="https://github.com/harrisiirak/cron-parser" rel="noopener noreferrer"&gt;&lt;code&gt;harrisiirak/cron-parser&lt;/code&gt;&lt;/a&gt; (v5.6.2, ~1.5k stars, the library behind a lot of Node scheduling) from TypeScript to Go for &lt;a href="https://www.raptors.dev/" rel="noopener noreferrer"&gt;Hackathon Raptors&lt;/a&gt;' Port Mortem.&lt;/p&gt;

&lt;p&gt;The rule that makes the event interesting: &lt;strong&gt;you must run the original test suite, unmodified, against your port.&lt;/strong&gt; Not a rewrite. Not a "spiritually equivalent" suite. The original &lt;code&gt;.ts&lt;/code&gt; files, byte for byte, with their SHA-256 pinned at kickoff.&lt;/p&gt;

&lt;p&gt;280 of 280 pass. But the number I actually care about is a different one, and getting to it meant admitting that four of those tests had been passing without testing anything at all.&lt;/p&gt;




&lt;h2&gt;
  
  
  luxon and Go disagree about what a date is
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;cron-parser&lt;/code&gt; uses luxon. Go has &lt;code&gt;time&lt;/code&gt;. Both are competent. They also disagree, silently, in ways that only show up in specific timezones on specific days.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disagreement one: arithmetic is asymmetric.&lt;/strong&gt; luxon &lt;em&gt;clamps&lt;/em&gt; for year and month, but &lt;em&gt;overflows&lt;/em&gt; for day:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;luxon&lt;/th&gt;
&lt;th&gt;Naive Go&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;2024-02-29 + 1 year&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;2025-02-28&lt;/code&gt; (clamp)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;2025-03-01&lt;/code&gt; (overflow)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;set(month = Feb)&lt;/code&gt; on &lt;code&gt;2024-01-31&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;2024-02-29&lt;/code&gt; (clamp)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;2024-03-02&lt;/code&gt; (overflow)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;set(day = 31)&lt;/code&gt; in February&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;2024-03-02&lt;/code&gt; (&lt;strong&gt;overflow&lt;/strong&gt;)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;2024-03-02&lt;/code&gt; (overflow)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The third row is the problem. luxon clamps for year and month, then &lt;em&gt;doesn't&lt;/em&gt; clamp for day. Any uniform strategy is wrong. Clamp everything and you break &lt;code&gt;setDay&lt;/code&gt;; use &lt;code&gt;AddDate&lt;/code&gt; everywhere and you break year and month arithmetic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disagreement two: nonexistent times resolve in opposite directions.&lt;/strong&gt; When a wall-clock time falls inside a DST gap, luxon jumps &lt;em&gt;forward&lt;/em&gt; and Go's &lt;code&gt;time.Date&lt;/code&gt; falls &lt;em&gt;backward&lt;/em&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Case&lt;/th&gt;
&lt;th&gt;luxon&lt;/th&gt;
&lt;th&gt;Go &lt;code&gt;time.Date&lt;/code&gt;
&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;2026-03-08T02:30&lt;/code&gt; New York&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;03:30 -04:00&lt;/code&gt; (forward)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;01:30 -05:00&lt;/code&gt; (&lt;strong&gt;backward&lt;/strong&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;startOf(day)&lt;/code&gt; Santiago &lt;code&gt;2026-09-06&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;09-06T01:00 -03:00&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;09-05T23:00 -04:00&lt;/code&gt; (&lt;strong&gt;previous day&lt;/strong&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The Santiago row is the genuinely dangerous one. In zones that transition at midnight, &lt;code&gt;time.Date(y, m, d, 0, 0, 0, 0, loc)&lt;/code&gt; can land on the &lt;strong&gt;previous calendar day&lt;/strong&gt;, which quietly corrupts day-of-month matching in a way that looks like a cron bug rather than a timezone bug.&lt;/p&gt;

&lt;p&gt;So I ported luxon's &lt;code&gt;fixOffset&lt;/code&gt; into a single function, &lt;code&gt;fromWallClock&lt;/code&gt;, and made it the only place in the package that constructs a wall-clock instant. Nothing else calls &lt;code&gt;time.Date&lt;/code&gt; with a non-UTC location. One 20-line function holds the entire divergence, which makes it reviewable instead of scattered across a dozen setters.&lt;/p&gt;




&lt;h2&gt;
  
  
  41,088 → 143 → 0
&lt;/h2&gt;

&lt;p&gt;I built a differential harness: same inputs to both implementations, compare every answer. First full run against a corpus of date operations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;41,088 divergences
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cause was embarrassing and instructive. luxon's &lt;code&gt;fixOffset&lt;/code&gt; needs a &lt;em&gt;provisional&lt;/em&gt; offset to seed its search. I was computing that seed from the instant being read, rather than from the instant being operated on. Subtle, one-line, and wrong in exactly the cases that matter: near transitions. Fixed by passing the provisional offset in as a parameter instead of deriving it locally.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;143 divergences
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Better. The remaining 143 all lived in &lt;strong&gt;Australia/Lord Howe&lt;/strong&gt;, a zone with a &lt;strong&gt;30-minute&lt;/strong&gt; DST shift (+10:30 ↔ +11:00), which is unusual enough that it breaks assumptions you didn't know you had.&lt;/p&gt;

&lt;p&gt;The bug: I had implemented &lt;code&gt;endOf(unit)&lt;/code&gt; as&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;startOf(unit) → plus(1 unit) → minus(1ms)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;luxon does&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;plus(1 unit) → startOf(unit) → minus(1ms)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a one-hour-offset world those are the same. In a half-hour-offset world they are not.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0 divergences
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That third number is the one this whole project is about. I don't fully trust it, which is why I kept going.&lt;/p&gt;




&lt;h2&gt;
  
  
  Four tests that passed without testing anything
&lt;/h2&gt;

&lt;p&gt;This one generalises beyond cron.&lt;/p&gt;

&lt;p&gt;Six tests in the original suite do this:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;spy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;spyOn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CronDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prototype&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;applyDateOperation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;spy&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;not&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toHaveBeenCalled&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My search loop lives in Go. That JavaScript method is &lt;em&gt;never called&lt;/em&gt;, by construction. So:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Two&lt;/strong&gt; tests assert positive call counts. They failed. Honest failures, and I could see them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Four&lt;/strong&gt; tests assert &lt;code&gt;not.toHaveBeenCalled()&lt;/code&gt;. They &lt;strong&gt;passed&lt;/strong&gt;. Vacuously. A spy that is never invoked trivially satisfies "was not invoked."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the trap. My suite said 278/280 and the two failures were visible, but four &lt;em&gt;additional&lt;/em&gt; tests had quietly degraded into no-ops. They were green. They were also worthless. A port can convert a real assertion into a tautology without anything turning red.&lt;/p&gt;

&lt;p&gt;The fix wasn't to stub the spy. It was to make the engine &lt;strong&gt;record&lt;/strong&gt; the date operations it actually performs. Every one goes through a single method, &lt;code&gt;applyOp&lt;/code&gt;, which appends to a log before delegating, and the adapter replays that recording through &lt;code&gt;CronDate.prototype&lt;/code&gt;. What the spy observes is therefore what the engine did.&lt;/p&gt;

&lt;p&gt;The test that this is a recording and not theatre: &lt;strong&gt;does it still fail when the implementation regresses?&lt;/strong&gt; I replaced the hour fast path with a loop that steps one hour at a time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;● when past the last scheduled hour for the day, jumps a full day first
● jumps to next allowed hour without stepping via applyDateOperation()
Tests: 2 failed, 67 passed, 69 total
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It fails. Good. And the four vacuous tests got their diagnostic power &lt;em&gt;back&lt;/em&gt;. They now genuinely assert that certain operations did not happen.&lt;/p&gt;

&lt;p&gt;280/280, and four of them mean something again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When you port, audit your negative assertions.&lt;/strong&gt; &lt;code&gt;not.toHaveBeenCalled&lt;/code&gt;, &lt;code&gt;assert_not_called&lt;/code&gt;, &lt;code&gt;verify(never())&lt;/code&gt;. Every one of them is a candidate for silently becoming a tautology the moment the thing being spied on stops existing in that language.&lt;/p&gt;




&lt;h2&gt;
  
  
  Three layers of proof
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. The original suite, unmodified.&lt;/strong&gt; 280/280. Hashes pinned at kickoff (&lt;code&gt;615075d3...2863de&lt;/code&gt;) and re-verified by a script. &lt;code&gt;tests/original/&lt;/code&gt; is touched by exactly one commit in the entire history: the one that added it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Differential fuzzing.&lt;/strong&gt; Random expressions, 14 timezones, boundary-weighted start instants, both implementations, every answer compared. Latest published run: &lt;strong&gt;90 seconds, 3,110 expression cases, 1,468 date cases, zero divergences.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. CLI output diff.&lt;/strong&gt; The fuzzer compares APIs. This compares them as &lt;em&gt;programs&lt;/em&gt;: same command line, diffing stdout, stderr and exit status, including 16 rejection paths where the error text and exit code both have to match. &lt;strong&gt;124/124 identical.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Two things had to be pinned before layer 3 meant anything. A Windows shell splits &lt;code&gt;*/15 9-17 * * 1-5&lt;/code&gt; on its spaces before the program ever sees it, so neither side runs through a shell. And Go writes a zero UTC offset as &lt;code&gt;Z&lt;/code&gt; where luxon writes &lt;code&gt;+00:00&lt;/code&gt;, a formatting convention, not a behavioural difference, so it's normalised in the wrapper and &lt;em&gt;documented as normalised&lt;/em&gt; rather than quietly smoothed over.&lt;/p&gt;




&lt;h2&gt;
  
  
  Sabotaging the port to check the fuzzer notices
&lt;/h2&gt;

&lt;p&gt;A fuzzer that reports zero divergences is indistinguishable from a fuzzer that isn't looking. So I sabotaged the port and checked it noticed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One sabotage defeated it three times.&lt;/strong&gt; I broke year arithmetic, a bug that only manifests on a leap day. Three consecutive runs came back clean, and each failure taught me something about the harness:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It only fuzzed expressions, never raw date operations. → Added date-op cases.&lt;/li&gt;
&lt;li&gt;It picked &lt;em&gt;one&lt;/em&gt; random operation per case. Chance of drawing both a leap-day instant and the broken operation: under 1 in 400. → Switched to sweeping the full operation surface per instant.&lt;/li&gt;
&lt;li&gt;It sampled instants uniformly. Leap days are ~1 in 1,000. → Added a boundary-weighted corpus.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Only after all three did the sabotage die in seconds, with leap-day reproductions. Each of those was a real blind spot that a green run had been hiding.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I found one more while auditing my own work for this write-up.&lt;/strong&gt; My fuzzer README opened by claiming "every answer either can produce is compared." That was false. Five public methods (&lt;code&gt;hasNext&lt;/code&gt;, &lt;code&gt;hasPrev&lt;/code&gt;, &lt;code&gt;take&lt;/code&gt;, &lt;code&gt;reset&lt;/code&gt;, &lt;code&gt;toString&lt;/code&gt;) were never touched. They look like thin wrappers. They aren't:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;hasNext&lt;/code&gt;/&lt;code&gt;hasPrev&lt;/code&gt; run a search and then &lt;strong&gt;restore the cursor&lt;/strong&gt; in a &lt;code&gt;finally&lt;/code&gt;. A port could return the right boolean while silently consuming an occurrence.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;take&lt;/code&gt; has a separate backward branch for negative limits.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;toString&lt;/code&gt; falls through a JavaScript &lt;code&gt;||&lt;/code&gt; on a falsy expression.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I added them, comparing &lt;strong&gt;the state left behind&lt;/strong&gt;, not just the return value. Then sabotaged the cursor restoration: &lt;strong&gt;74 divergences in 25 seconds.&lt;/strong&gt; Comparing only the boolean would have caught none of them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The original test suite catches that same sabotage too&lt;/strong&gt;, with 7 failures. I added those probes because my harness was claiming a surface it didn't cover, not because they caught something the tests had missed. Saying otherwise would overstate them.&lt;/p&gt;




&lt;h2&gt;
  
  
  The benchmark that printed "Infinityx"
&lt;/h2&gt;

&lt;p&gt;My first throughput run reported that every operation took &lt;strong&gt;zero nanoseconds&lt;/strong&gt;. 1,000 samples out of 1,000.&lt;/p&gt;

&lt;p&gt;Go's monotonic clock on this machine advances in steps of about &lt;strong&gt;527 microseconds&lt;/strong&gt;. One parse-plus- ten-occurrences costs tens of microseconds. So every individual measurement rounded to zero, and the speedup calculation divided by it and printed &lt;code&gt;Infinityx&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I was about ten minutes from having a very impressive slide.&lt;/p&gt;

&lt;p&gt;The fix: batch both sides identically, calibrating batch size per pattern until a batch takes at least 10ms. Node's timer is fine-grained enough that it didn't &lt;em&gt;need&lt;/em&gt; batching, which is exactly why both sides batch. Measuring the two differently would have made the comparison meaningless.&lt;/p&gt;

&lt;p&gt;Real numbers, on the original library's own 15 benchmark patterns (not ones I picked):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Measure&lt;/th&gt;
&lt;th&gt;Original (node v22)&lt;/th&gt;
&lt;th&gt;Port (go1.26)&lt;/th&gt;
&lt;th&gt;Ratio&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Throughput, sum of medians&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;23.5x&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cold start p50&lt;/td&gt;
&lt;td&gt;136.0 ms&lt;/td&gt;
&lt;td&gt;12.0 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;11.3x&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory after workload&lt;/td&gt;
&lt;td&gt;84.9 MB&lt;/td&gt;
&lt;td&gt;20.2 MB&lt;/td&gt;
&lt;td&gt;&lt;em&gt;not comparable&lt;/em&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three caveats:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The memory row is not a real ratio.&lt;/strong&gt; Node's RSS counts the whole process including V8; Go's &lt;code&gt;Sys&lt;/code&gt; is what the runtime obtained from the OS and excludes the binary. Different measurements. I report both as each runtime reports them rather than inventing a single number.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;23.5x is a sum of medians&lt;/strong&gt;, which flatters. Per-pattern it ranges from &lt;strong&gt;2.87x&lt;/strong&gt; to 36x.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The 2.87x is on &lt;code&gt;* * * * * *&lt;/code&gt;&lt;/strong&gt; (104.7µs → 36.5µs), the most permissive expression in the corpus, where every candidate instant matches immediately. With almost no searching to do, the cost is dominated by constructing timezone-aware instants, which both sides pay in full. The port wins big precisely where there's a &lt;em&gt;search&lt;/em&gt; to win; the headline number is real, but it is not evenly distributed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cold start is the number I'd actually defend. For a CLI invoked from a scheduler, 136ms → 12ms matters more than throughput nobody is bottlenecked on.&lt;/p&gt;




&lt;h2&gt;
  
  
  Six bugs in the original: five merged, one not filed
&lt;/h2&gt;

&lt;p&gt;Differential testing finds bugs in the &lt;em&gt;reference&lt;/em&gt;, not just the port. Seven reports went upstream during the hackathon (&lt;a href="https://github.com/harrisiirak/cron-parser/issues" rel="noopener noreferrer"&gt;#419–#425&lt;/a&gt;). Five have been fixed and merged:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Merged&lt;/th&gt;
&lt;th&gt;What it fixes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/harrisiirak/cron-parser/pull/426" rel="noopener noreferrer"&gt;PR #426&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;val.match(/([,-/])/)&lt;/code&gt; → &lt;code&gt;/([,\-/])/&lt;/code&gt;, escaping the hyphen so it's a literal, not a range&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/harrisiirak/cron-parser/pull/427" rel="noopener noreferrer"&gt;PR #427&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;values.sort(...)&lt;/code&gt; → &lt;code&gt;[...values].sort(...)&lt;/code&gt;, a defensive copy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/harrisiirak/cron-parser/pull/428" rel="noopener noreferrer"&gt;PR #428&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;standalone &lt;code&gt;L&lt;/code&gt; in day-of-week rejected at parse, not from &lt;code&gt;next()&lt;/code&gt;, so the error arrives where you can still act on it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/harrisiirak/cron-parser/pull/433" rel="noopener noreferrer"&gt;PR #433&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;day-of-month values the named month doesn't have are dropped, so &lt;code&gt;stringify()&lt;/code&gt; settles on the first render&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/harrisiirak/cron-parser/pull/434" rel="noopener noreferrer"&gt;PR #434&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;a day field covering its whole range is kept out of the wildcard form, so rendering no longer turns a daily schedule into a monthly one&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The first three were filed 07:12Z and merged by 16:13Z the same day; the other two landed the day after. The second is the fix I'd already made in Go on day one, for the same reason: Go slices share a backing array, so the naive translation inherits the bug. Nice to have the maintainer arrive at the same line independently.&lt;/p&gt;

&lt;p&gt;Of the two still open, &lt;code&gt;#423&lt;/code&gt; is triaged and assigned, and duplicates a pull request that was already open. &lt;code&gt;#419&lt;/code&gt; is the DST one below, and it now carries a pull request of mine that the maintainer asked for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;stringify()&lt;/code&gt; isn't round-trip safe&lt;/strong&gt;, the worst one. &lt;code&gt;0 0 16 * 0-6&lt;/code&gt; renders as &lt;code&gt;0 0 16 * *&lt;/code&gt;. The original fires &lt;strong&gt;daily&lt;/strong&gt;; its own rendered output fires &lt;strong&gt;monthly&lt;/strong&gt;. Cause: &lt;code&gt;isWildcard&lt;/code&gt; is derived from raw text (&lt;code&gt;*&lt;/code&gt; or &lt;code&gt;?&lt;/code&gt; literally), but &lt;code&gt;stringifyField&lt;/code&gt; works from expanded values, so &lt;code&gt;0-6&lt;/code&gt; renders as &lt;code&gt;*&lt;/code&gt;. And day-of-month/day-of-week switch between OR and AND on exactly that flag. Rendering silently converts an OR into an AND.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A DST gap wider than one hour skips an occurrence entirely.&lt;/strong&gt; The compensation checks &lt;code&gt;currentHour - previousHour === 2&lt;/code&gt;. Antarctica/Troll jumps &lt;strong&gt;two hours&lt;/strong&gt; (00:00 → 03:00), so the diff is 3 and the branch never fires:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;America/New_York, 1h gap, "30 2 * * *"     Antarctica/Troll, 2h gap, "30 1 * * *"
  2026-03-07 02:30                           2026-03-28 01:30
  2026-03-08 03:30  ← shifted, still runs    2026-03-30 01:30  ← 29 March never appears
  2026-03-09 02:30                           2026-03-31 01:30
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A late job versus a &lt;strong&gt;missing&lt;/strong&gt; job. I filed the shifted-time version first, then found the skip while re-verifying and added it as a follow-up comment. It is the harder failure to notice, and I'd initially understated it.&lt;/p&gt;

&lt;p&gt;This is the one I had a patch for and deliberately didn't send as an unsolicited pull request. Detecting the gap from the UTC offset delta rather than from an assumed one-hour difference, plus matching every hour the transition swallows: 292 tests green, and a 5,865-case sweep showing the change confined to &lt;code&gt;Antarctica/Troll&lt;/code&gt;. Offering a patch on the issue and letting the maintainer decide costs me nothing and costs them a review they didn't ask for if I'm wrong. The reply was "sure, open a PR, I'll have a look", so it's now &lt;a href="https://github.com/harrisiirak/cron-parser/pull/435" rel="noopener noreferrer"&gt;PR #435&lt;/a&gt;, rebased onto current &lt;code&gt;master&lt;/code&gt;, 303 of 303 tests passing there, with three regression tests that fail without the change.&lt;/p&gt;

&lt;h3&gt;
  
  
  The seventh report was a duplicate
&lt;/h3&gt;

&lt;p&gt;I filed a seventh, a duplicated &lt;code&gt;0&lt;/code&gt; escaping validation, because the check is &lt;code&gt;if (duplicate)&lt;/code&gt; against &lt;code&gt;Array.prototype.find&lt;/code&gt;'s return value, which is falsy when the value found is &lt;code&gt;0&lt;/code&gt;. Real bug. &lt;code&gt;[1,1]&lt;/code&gt; is rejected, &lt;code&gt;[0,0]&lt;/code&gt; sails through.&lt;/p&gt;

&lt;p&gt;It also had &lt;strong&gt;an open pull request since three days before the hackathon started&lt;/strong&gt;, which the maintainer pointed out within hours. My duplicate check searched &lt;em&gt;issues&lt;/em&gt;. It didn't search open &lt;em&gt;pull requests&lt;/em&gt;. On a repo where fixes arrive as PRs, that's half the project's memory of itself.&lt;/p&gt;

&lt;p&gt;So: &lt;strong&gt;six original findings, not seven.&lt;/strong&gt; I'm leaving the wrong number visible in my repo's issue log with a note, rather than editing it into a cleaner story. If you're going to claim novelty against a live codebase, search issues &lt;em&gt;and&lt;/em&gt; PRs, and search by &lt;strong&gt;mechanism&lt;/strong&gt; rather than by title. My query was "duplicate values", and the PR was titled "reject duplicate 0 in field validation", which a title-shaped search finds and a concept-shaped one finds faster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The one I didn't file:&lt;/strong&gt; &lt;code&gt;W&lt;/code&gt; (nearest weekday) is a phantom. It's in the &lt;code&gt;CronChars&lt;/code&gt; type, it has an explicit branch in &lt;code&gt;compactField&lt;/code&gt;, four tests exercise it, but it's absent from &lt;code&gt;CronDayOfMonth.validChars&lt;/code&gt;, so &lt;code&gt;parse('0 0 15W * *')&lt;/code&gt; throws. The stringify path can emit a value the parse path cannot produce. It's reachable only by hand-constructing field objects.&lt;/p&gt;

&lt;p&gt;I didn't file it because I couldn't tell whether it's a bug or a half-landed feature, and filing ambiguous findings to inflate a count is exactly the behaviour that makes maintainers stop reading issues. My port reproduces it precisely: &lt;code&gt;W&lt;/code&gt; accepted in stringify, rejected by the parser.&lt;/p&gt;




&lt;h2&gt;
  
  
  The decision I'd take back
&lt;/h2&gt;

&lt;p&gt;I deleted several branches as unreachable: a "values is not an array" error that the Go signature makes unrepresentable, a nil-location fallback both entry points already prevent. Reasonable.&lt;/p&gt;

&lt;p&gt;Then I deleted the &lt;code&gt;wildcard&lt;/code&gt; constructor override on the same grounds. The parser never sets it, so it looked dead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It wasn't.&lt;/strong&gt; The field constructors are public API, and the original's own tests set it directly. I'd reasoned about reachability from &lt;em&gt;one&lt;/em&gt; entry point and called it reachability in general.&lt;/p&gt;

&lt;p&gt;Restored, with a test. It's in my decision log as a counter-example, because the lesson isn't "don't delete dead code". It's that &lt;strong&gt;a reachability argument is only as good as the set of entry points you considered&lt;/strong&gt;, and I considered one.&lt;/p&gt;




&lt;h2&gt;
  
  
  Honest numbers
&lt;/h2&gt;

&lt;p&gt;Everything I claim, with what it actually measured:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Claim&lt;/th&gt;
&lt;th&gt;Measured&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Original tests passing&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;280 / 280&lt;/strong&gt;, zero modifications&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;unsafe&lt;/code&gt; in Go sources&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;0&lt;/strong&gt;, the string doesn't appear, even in a comment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;reflect&lt;/code&gt; in the library&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Statement coverage&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;99.5%&lt;/strong&gt; default, 100% with corpus generators enabled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Differential fuzz&lt;/td&gt;
&lt;td&gt;90s, 4,578 cases, &lt;strong&gt;0 divergences&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CLI output diff&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;124 / 124&lt;/strong&gt; identical&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Upstream fixes merged&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;5&lt;/strong&gt; of 6 original findings, with the sixth open as a PR&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two of those I had to correct while writing this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Coverage.&lt;/strong&gt; My README claimed a flat 100%. Measured, it's &lt;strong&gt;99.5%&lt;/strong&gt;; it only reaches 100% when two corpus-generator tests run, and those are gated behind an env var because they write multi-megabyte fixtures. Both readings are now published with the gap explained, because quoting only the higher one is the flattering presentation rather than the true one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pass rate.&lt;/strong&gt; My first generator scored those two skipped generators as &lt;code&gt;0.00%&lt;/code&gt; pass rate, which is as misleading in the other direction. It's now passed-over-&lt;em&gt;executed&lt;/em&gt;, with the skipped column visible next to it.&lt;/p&gt;

&lt;p&gt;There are 23 &lt;code&gt;any&lt;/code&gt;s in my WebAssembly bridge. They're all forced by &lt;code&gt;syscall/js&lt;/code&gt;, since &lt;code&gt;js.FuncOf&lt;/code&gt; mandates an &lt;code&gt;any&lt;/code&gt; return, and one of them is a generic constraint (&lt;code&gt;lookup[T any]&lt;/code&gt;), which isn't an escape hatch at all. I counted it anyway. If you're going to publish a number, err against yourself.&lt;/p&gt;




&lt;h2&gt;
  
  
  Four things I'd tell someone starting a port
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Your test suite is not a proof.&lt;/strong&gt; It's a sample. The original suite passing 280/280 told me far less than I wanted it to, because four of those tests had degraded into tautologies and I only found out by asking what each one would do if the implementation were wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build the differential harness first, then attack it.&lt;/strong&gt; Not "does it pass" but "what can I break that it won't notice." Every blind spot I found came from sabotage, never from a clean run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When two runtimes disagree, find the single chokepoint.&lt;/strong&gt; One 20-line &lt;code&gt;fromWallClock&lt;/code&gt; holding every luxon-versus-Go divergence was the difference between a reviewable port and an unauditable one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Write down the decisions you'd take back.&lt;/strong&gt; My decision log has 21 entries and the most useful one records a call I got wrong. Judges, and future readers, can tell the difference between a document written to persuade and one written to remember.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Code, decision log, fuzz harness and full numbers: &lt;a href="https://github.com/aniket-3001/cron-parser-go" rel="noopener noreferrer"&gt;github.com/aniket-3001/cron-parser-go&lt;/a&gt;. Both implementations running side by side in your browser, with the fuzzer you can drive yourself: &lt;a href="https://aniket-3001.github.io/cron-parser-go/" rel="noopener noreferrer"&gt;aniket-3001.github.io/cron-parser-go&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Built for Port Mortem by &lt;a href="https://www.raptors.dev/" rel="noopener noreferrer"&gt;Hackathon Raptors&lt;/a&gt;. Track C, TypeScript → Go.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>hackathonraptors</category>
      <category>portmortem</category>
      <category>go</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
