ReviewOS

also looking at this

craft-native/craft

fix(bridge): stop discarding the fields the page actually sends

#82
Merged glennmichael123 wants to merge fix/bridge-field-names into main
7 files +375 -34
10 days ago

#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:

callpage sendsnative readwhat happened
fs.writeFile(path, data)datacontentwrote an empty file, every time
fs.appendFile(path, data)datacontentappended nothing
fs.copy(from, to)from,tosrc,destno-op
fs.move(from, to)from,tosrc,destno-op
window.setFullscreen(on)valuefullscreen orelse truesetFullscreen(false) entered fullscreen
window.setOpacity(v)valueopacityevery window stayed fully opaque
window.setVibrancy(mat)materialvibrancytook the removal branch — stripped vibrancy instead of applying it
bluetooth.connectDevice(id)idaddressaddress empty, call failed its own MissingData check
updater.setAutomaticChecks / setCheckIntervalvalue,secondsenabled,intervalboth 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 writeFile reading a key only readFile sends — and file-scope demonstrably misses the fs bugs, the most severe ones.
  • Stops at the next doc comment as well as the next fn. These bridges document payloads in /// blocks, and appendFile's /// JSON: {"content": "data"} leaked into writeFile'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:

controlresult
revert writeFile to "content" (name still elsewhere in file)fails ✓
revert copy to src/destfails ✓
remove the key from every fs handlerfails ✓
clean treepasses ✓

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.

7 changed files on the files tab, with 0 review threads.

Sign in to comment on this pull request.