#69 was one instance of a class. The page posts {"data": …}, the handler reads "content", and nothing objects — every bridge parses with ignore_unknown_fields = true or scans for a literal needle, so a name that does not match is not an error. The value is gone and the caller's promise resolves.
I swept all 281 calls the page can make. Nine more, in five bridges:
| call | page sends | native read | what happened |
|---|---|---|---|
fs.writeFile(path, data) | data | content | wrote an empty file, every time |
fs.appendFile(path, data) | data | content | appended nothing |
fs.copy(from, to) | from,to | src,dest | no-op |
fs.move(from, to) | from,to | src,dest | no-op |
window.setFullscreen(on) | value | fullscreen orelse true | setFullscreen(false) entered fullscreen |
window.setOpacity(v) | value | opacity | every window stayed fully opaque |
window.setVibrancy(mat) | material | vibrancy | took the removal branch — stripped vibrancy instead of applying it |
bluetooth.connectDevice(id) | id | address | address empty, call failed its own MissingData check |
updater.setAutomaticChecks / setCheckInterval | value,seconds | enabled,interval | both silently kept defaults |
app.bounce(type) | type | (no payload read) | 'critical' hopped once instead of bouncing until activated |
Each now reads the name the page sends; the old spelling stays accepted for anything posting raw bridge messages. The page's names are the published API — the native side is what was wrong.
Verified end to end by running the binary: writeFile then appendFile produces hello from writeFile + appended, and copy followed by move leaves exactly moved.txt. Before, all four were silent no-ops.
This explains something I waved past
Two days ago, mid-way through unrelated work, I hit "appendFile reports success and writes nothing", wrote it off as "a separate bridge_fs bug, not this PR's", and moved on without filing it. That was the symptom of this. Noticing a defect and not recording it is how it survives.
The durable half
A conformance check: every field name the page sends must appear in the handler that serves it.
- Handler-scoped, not file-scoped. A file-wide search cannot tell
writeFilereading a key onlyreadFilesends — and file-scope demonstrably misses thefsbugs, the most severe ones. - Stops at the next doc comment as well as the next
fn. These bridges document payloads in///blocks, andappendFile's/// JSON: {"content": "data"}leaked intowriteFile's span, making the check pass against the exact bug it was written for. - Distinguishes struct fields from function parameters by requiring a default (
baud: u32 = 9600). Without that,fn writeFile(self: *Self, data: []const u8)supplied the very name it was failing to read. - Six bridges opt in, as a ratchet that may only grow, plus a short exemption list — four window setters that scan for the literal
"false"without keying on a name, and two bluetooth handlers that delegate to a shared helper. Each exemption carries its reason.
A process note, because it cost me
Several of my controls were silently not applying. I used .replace() without asserting it matched, so the file was unchanged and the test "passed" on code I believed I had broken — and I spent an iteration fixing a test that was already correct.
A control that does not assert it applied is not a control. Every control here now asserts its own edit landed, and each fires:
| control | result |
|---|---|
revert writeFile to "content" (name still elsewhere in file) | fails ✓ |
revert copy to src/dest | fails ✓ |
remove the key from every fs handler | fails ✓ |
| clean tree | passes ✓ |
Not changed
Two of the sixteen candidates were not real — serial.open and localServer.start both declare their fields in the parse struct. Left alone.
Verified
zig build · zig build test · zig build test-js · x86_64-windows cross-build · zig fmt --check · 511 bun tests · pickier 38 warnings, unchanged from main.
A second conformance check, in the same commit
This PR also carries a test I wrote separately and then folded in when the two turned out to be the same question asked at different depths. Flagging it rather than leaving it unmentioned in the diff:
every bridge type the page can send is routed by the dispatcher
The existing conformance suite pins the capability registry against the dispatcher, and the channel list against the events the page subscribes to. It never compared what the page can call against what native will answer.
Add craft.foo.bar() to the facade with no arm in handleBridgeMessageJSON
and the message is parsed, matched against every arm, and dropped. _send
resolves the moment it is posted, so the caller gets a fulfilled promise and
nothing happens — the same silence as #27, #47 and #69.
The sweep found the current tree clean at this level: every one of the 50 types
the page sends is routed. The test is what keeps it that way. Two native types,
debug and nativeUI, are routed but unreachable from the page — noted, not
changed, since an unreachable handler is inert rather than wrong.
Controlled both ways: adding a page call with an unrouted type fails it
(craft-bridge.js posts type 'telemetry' but handleBridgeMessageJSON does not
route it), and so does removing a dispatcher arm. The first is uniquely this
test's — a brand-new JS type is invisible to the registry check, because the
registry would not know about it either.