<?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: AEFRI</title>
    <description>The latest articles on DEV Community by AEFRI (aefri-studio).</description>
    <link>https://dev.to/aefri-studio</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%2Forganization%2Fprofile_image%2F15711%2F2132ce3b-bdc9-48fa-afc1-3c7b7bd486d1.png</url>
      <title>DEV Community: AEFRI</title>
      <link>https://dev.to/aefri-studio</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aefri-studio"/>
    <language>en</language>
    <item>
      <title>The offline promise is a test, not a sentence</title>
      <dc:creator>AEFRI</dc:creator>
      <pubDate>Thu, 29 Oct 2026 19:32:56 +0000</pubDate>
      <link>https://dev.to/aefri-studio/the-offline-promise-is-a-test-not-a-sentence-1n1a</link>
      <guid>https://dev.to/aefri-studio/the-offline-promise-is-a-test-not-a-sentence-1n1a</guid>
      <description>&lt;h1&gt;
  
  
  The offline promise is a test, not a sentence
&lt;/h1&gt;

&lt;p&gt;Most "works offline" claims in software are marketing copy. This one is a unit&lt;br&gt;
test that fails the build.&lt;/p&gt;

&lt;p&gt;I ship a small pomodoro timer as a single 29,862-byte HTML file. There is no&lt;br&gt;
build step, no package manager, no server, and no CDN. You download the file,&lt;br&gt;
double-click it, and it runs. The file is 29 KB because there is nothing in it&lt;br&gt;
except the app.&lt;/p&gt;

&lt;p&gt;That is only interesting if it stays true. The moment someone adds a font&lt;br&gt;
&lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt;, an analytics snippet, or a CDN-hosted library, the file still works&lt;br&gt;
on a machine with internet and quietly stops working on a plane. Nothing&lt;br&gt;
breaks. The build stays green. The download still says "no account, no&lt;br&gt;
internet" — and the claim rots.&lt;/p&gt;

&lt;p&gt;So the guarantee is enforced in code:&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;test_artifact_makes_no_network_calls&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;probe&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://&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;https://&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;fetch(&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;XMLHttpRequest&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;import(&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;navigator.sendBeacon&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;&amp;lt;link rel=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;stylesheet&lt;/span&gt;&lt;span class="se"&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertNotIn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;probe&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;html&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;offline promise broken by &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;probe&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seven probes. If any of them appears in the shipped file, the test fails, the&lt;br&gt;
release does not happen, and the person who added it gets a message naming&lt;br&gt;
the exact string that broke the contract. There is no flag to set, no&lt;br&gt;
environment variable to set for "we're only staging this".&lt;/p&gt;
&lt;h2&gt;
  
  
  The second test is the one that matters
&lt;/h2&gt;

&lt;p&gt;A string check proves the file does not &lt;em&gt;mention&lt;/em&gt; the network. It does not&lt;br&gt;
prove the app &lt;em&gt;works&lt;/em&gt;. Those are different claims, and a lot of "offline&lt;br&gt;
first" software only makes the first one.&lt;/p&gt;

&lt;p&gt;So there is a browser test:&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;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                       &lt;span class="c1"&gt;# a file:// URL, not a server
&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for_timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&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="nf"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inner_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#clock&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;25:00&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#startBtn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for_timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2100&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="nf"&gt;assertNotEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inner_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#clock&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;25:00&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#taskInput&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;Real task&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;press&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#taskInput&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;Enter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reload&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="nf"&gt;assertIn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Real task&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inner_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#taskList&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;The file is opened from disk with a &lt;code&gt;file://&lt;/code&gt; URL. If it needed a server it&lt;br&gt;
could not even start. The test then checks the timer counts down, a task can&lt;br&gt;
be added, and the task is still there after a reload — because the state lives&lt;br&gt;
in &lt;code&gt;localStorage&lt;/code&gt;, and if that regressed the app would look fine for about&lt;br&gt;
five minutes before quietly losing everything.&lt;/p&gt;

&lt;p&gt;The test also collects &lt;code&gt;pageerror&lt;/code&gt; events and asserts the list is empty. An&lt;br&gt;
app that throws on load but renders a static shell would otherwise sail&lt;br&gt;
through.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the string test is still there
&lt;/h2&gt;

&lt;p&gt;The browser test alone would not catch a font CDN, because Chromium in the&lt;br&gt;
test environment has internet access and would happily load the font from&lt;br&gt;
Google. The offline promise would hold in CI and break for every buyer who is&lt;br&gt;
actually offline.&lt;/p&gt;

&lt;p&gt;Both tests together are what make the claim true: one proves the file does&lt;br&gt;
not &lt;em&gt;depend&lt;/em&gt; on the network, the other proves it still &lt;em&gt;works&lt;/em&gt; without one.&lt;br&gt;
Neither is sufficient alone, and it took me writing both to realise the&lt;br&gt;
browser test was quietly green for a week while the app pulled a webfont.&lt;/p&gt;

&lt;h2&gt;
  
  
  The previews are the artifact too
&lt;/h2&gt;

&lt;p&gt;Another thing I did not expect: the product page previews are screenshots of&lt;br&gt;
the real file, not mockups. The renderer opens the shipped HTML in Chromium&lt;br&gt;
at the same dimensions and captures it, in both the dark and light theme. A&lt;br&gt;
mockup would have been easier and would have looked better — it would have&lt;br&gt;
been 200 KB of hand-tuned marketing art instead of a screenshot of something&lt;br&gt;
that might not exist.&lt;/p&gt;

&lt;p&gt;The consequence is that the preview cannot lie. If the app breaks, the&lt;br&gt;
screenshot changes. If the app looks bad, the screenshot looks bad. Nobody&lt;br&gt;
has to trust the copy.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an AI agent changes about this
&lt;/h2&gt;

&lt;p&gt;The app itself was built by an AI agent working from a written specification,&lt;br&gt;
and I want to be precise about what that did and did not change, because the&lt;br&gt;
obvious framing ("AI writes code now") is the least useful part.&lt;/p&gt;

&lt;p&gt;What it changed: throughput and the boring half of the work. A 29 KB&lt;br&gt;
single-file app with two themes, a task list, a notes pad, export/import, and&lt;br&gt;
keyboard shortcuts is maybe a day of careful work. It took an afternoon. The&lt;br&gt;
agent was also good at the parts I do not enjoy: writing the README, keeping&lt;br&gt;
the description in the listing copy in sync with what the app actually does,&lt;br&gt;
and generating the preview images.&lt;/p&gt;

&lt;p&gt;What it did not change: the need for a falsifiable claim. An agent that&lt;br&gt;
asserts "this works offline" in the listing copy has accomplished exactly&lt;br&gt;
nothing. The value of the test is that it is the same kind of artifact&lt;br&gt;
whether a human or an agent wrote the code — it either passes or it does not,&lt;br&gt;
and it can be run by anyone.&lt;/p&gt;

&lt;p&gt;There is a second lesson, which is about my own process rather than the&lt;br&gt;
agent's output. My first version of the offline test checked for&lt;br&gt;
&lt;code&gt;http://&lt;/code&gt; and &lt;code&gt;https://&lt;/code&gt; only. The agent then shipped a version that pulled a&lt;br&gt;
font from a CDN — not with a literal &lt;code&gt;https://&lt;/code&gt; in the source, but in a&lt;br&gt;
&lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt; tag I had not thought to probe for. The test did not fail because I&lt;br&gt;
had not written the probe, not because the agent was careless. The gap was in&lt;br&gt;
the specification, and the test suite surfaced it exactly as well as it would&lt;br&gt;
have surfaced a human mistake.&lt;/p&gt;

&lt;p&gt;That is the whole argument for test-first specifications in agent workflows:&lt;br&gt;
the specification is where correctness lives, and the tests are how you find&lt;br&gt;
out whether you actually wrote it down.&lt;/p&gt;

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

&lt;p&gt;If you make an offline claim about your software, try to write the test that&lt;br&gt;
would falsify it before you write the sentence that makes it. Two tests are&lt;br&gt;
usually enough:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A static check for network primitives in the shipped artifact, including
the ones you did not think of — stylesheet links, &lt;code&gt;import()&lt;/code&gt;, beacons.&lt;/li&gt;
&lt;li&gt;A browser test that opens the artifact from disk, exercises the main
path, and asserts zero page errors.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you cannot write the first one, that is a signal about the artifact, not&lt;br&gt;
about the test.&lt;/p&gt;




&lt;p&gt;A note on disclosure: this article was drafted with AI assistance and edited&lt;br&gt;
for accuracy before publication. Every number quoted here comes from the&lt;br&gt;
build, and the tests described above run on every release.&lt;/p&gt;

&lt;p&gt;If you want to see the thing itself rather than talk about it, the app and&lt;br&gt;
the rest of the small catalogue are at &lt;a href="https://aefristore.web.app" rel="noopener noreferrer"&gt;https://aefristore.web.app&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>productivity</category>
      <category>opensource</category>
      <category>abotwrotethis</category>
    </item>
  </channel>
</rss>
