跳到正文
DEV Community· AEFRI·· 3 小時前AI 評分63

軟件的離線承諾要靠測試驗證,而非一句宣稱

The offline promise is a test, not a sentence

AI 導讀

AEFRI 以一個 29,862-byte 的單檔 Pomodoro 計時器,示範以兩類測試驗證離線承諾:靜態檢查網絡相關字串,並由瀏覽器直接從 file:// 開啟檔案測試功能。瀏覽器測試檢查倒數、任務新增及重新載入後資料仍保留,並確認沒有 pageerror;靜態測試則可攔截 CDN 字體等會令離線承諾失效的網絡依賴。

正文

The offline promise is a test, not a sentence

Most "works offline" claims in software are marketing copy. This one is a unit
test that fails the build.

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

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

So the guarantee is enforced in code:

def test_artifact_makes_no_network_calls(self):
    for probe in ("http://", "https://", "fetch(", "XMLHttpRequest",
                  "import(", "navigator.sendBeacon",
                  "<link rel=\"stylesheet\""):
        self.assertNotIn(probe, self.html,
                         f"offline promise broken by {probe}")

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

The second test is the one that matters

A string check proves the file does not mention the network. It does not
prove the app works. Those are different claims, and a lot of "offline
first" software only makes the first one.

So there is a browser test:

page.goto(uri)                       # a file:// URL, not a server
page.wait_for_timeout(400)
self.assertEqual(page.inner_text("#clock"), "25:00")
page.click("#startBtn")
page.wait_for_timeout(2100)
self.assertNotEqual(page.inner_text("#clock"), "25:00")
page.fill("#taskInput", "Real task")
page.press("#taskInput", "Enter")
page.reload()
self.assertIn("Real task", page.inner_text("#taskList"))

The file is opened from disk with a file:// URL. If it needed a server it
could not even start. The test then checks the timer counts down, a task can
be added, and the task is still there after a reload — because the state lives
in localStorage, and if that regressed the app would look fine for about
five minutes before quietly losing everything.

The test also collects pageerror events and asserts the list is empty. An
app that throws on load but renders a static shell would otherwise sail
through.

Why the string test is still there

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

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

The previews are the artifact too

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

The consequence is that the preview cannot lie. If the app breaks, the
screenshot changes. If the app looks bad, the screenshot looks bad. Nobody
has to trust the copy.

What an AI agent changes about this

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

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

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

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

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

The practical version

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

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

If you cannot write the first one, that is a signal about the artifact, not
about the test.


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

If you want to see the thing itself rather than talk about it, the app and
the rest of the small catalogue are at https://aefristore.web.app.

來源:DEV Community · dev.to