ReviewOS

also looking at this

craft-native/craft

fix(capabilities): stop the channel manifest claiming knowledge it does not have

#68
Merged chris wants to merge fix/capability-channels into main
15 files +227 -67
packages/zig/src/capabilities.zigmodified+127-45
Changes to packages/zig/src/capabilities.zig
@@ -91,54 +91,103 @@ pub const NamespaceDecl = struct {
9191/// deriving from: `craft:menu:action` is colon-separated and `craft:powerSleep`
9292/// is camel-cased, and both are load-bearing spellings a page already listens
9393/// for. Renaming them to be consistent would break every app.
94///
95/// `test/capabilities_test.zig` checks this list against every `_evt('craft:…')`
96/// in `craft-bridge.js`. It shipped with 22 of the 44 and claimed to be
97/// complete, which is the same overclaiming this whole mechanism exists to
98/// stop — so now the claim is enforced.
9499pub const Channel = enum {
100 bluetooth_deviceconnected,
101 bluetooth_devicedisconnected,
102 bluetooth_devicefound,
103 bonjour_found,
104 bonjour_lost,
105 deeplink,
106 filedrop,
107 fs_change,
108 handoff_incoming,
109 iap_failed,
110 iap_productsloaded,
111 iap_purchased,
112 iap_restored,
113 localserver_request,
114 location_authchanged,
115 location_error,
116 location_update,
95117 menu_action,
96 theme,
97 settings_open,
98 window_focus,
99 window_blur,
100 window_resize,
101 window_move,
118 menubar_statechange,
119 midi_message,
120 networkchange,
121 powersleep,
122 powerwake,
102123 screen_change,
103 deep_link,
104 file_drop,
124 screensharing_change,
125 serial_data,
126 servicemenu_invoked,
127 settings_open,
105128 shortcut,
106 fs_change,
107 power_sleep,
108 power_wake,
109 network_change,
110 bluetooth_device_found,
111 bluetooth_device_connected,
112 bluetooth_device_disconnected,
129 shortcut_error,
130 speechrecognition_final,
131 speechrecognition_partial,
132 theme,
113133 touchbar_action,
114 serial_data,
115 speech_partial,
116 speech_final,
134 tray_menuaction,
135 updateavailable,
136 updatedownloaded,
137 window_blur,
138 window_close,
139 window_focus,
140 window_minimize,
141 window_move,
142 window_resize,
143 window_restore,
117144
118145 pub fn eventName(self: Channel) []const u8 {
119146 return switch (self) {
147 .bluetooth_deviceconnected => "craft:bluetooth:deviceConnected",
148 .bluetooth_devicedisconnected => "craft:bluetooth:deviceDisconnected",
149 .bluetooth_devicefound => "craft:bluetooth:deviceFound",
150 .bonjour_found => "craft:bonjour:found",
151 .bonjour_lost => "craft:bonjour:lost",
152 .deeplink => "craft:deepLink",
153 .filedrop => "craft:fileDrop",
154 .fs_change => "craft:fs:change",
155 .handoff_incoming => "craft:handoff:incoming",
156 .iap_failed => "craft:iap:failed",
157 .iap_productsloaded => "craft:iap:productsLoaded",
158 .iap_purchased => "craft:iap:purchased",
159 .iap_restored => "craft:iap:restored",
160 .localserver_request => "craft:localServer:request",
161 .location_authchanged => "craft:location:authChanged",
162 .location_error => "craft:location:error",
163 .location_update => "craft:location:update",
120164 .menu_action => "craft:menu:action",
121 .theme => "craft:theme",
122 .settings_open => "craft:settings:open",
123 .window_focus => "craft:window:focus",
124 .window_blur => "craft:window:blur",
125 .window_resize => "craft:window:resize",
126 .window_move => "craft:window:move",
165 .menubar_statechange => "craft:menubar:stateChange",
166 .midi_message => "craft:midi:message",
167 .networkchange => "craft:networkChange",
168 .powersleep => "craft:powerSleep",
169 .powerwake => "craft:powerWake",
127170 .screen_change => "craft:screen:change",
128 .deep_link => "craft:deepLink",
129 .file_drop => "craft:fileDrop",
171 .screensharing_change => "craft:screenSharing:change",
172 .serial_data => "craft:serial:data",
173 .servicemenu_invoked => "craft:serviceMenu:invoked",
174 .settings_open => "craft:settings:open",
130175 .shortcut => "craft:shortcut",
131 .fs_change => "craft:fs:change",
132 .power_sleep => "craft:powerSleep",
133 .power_wake => "craft:powerWake",
134 .network_change => "craft:networkChange",
135 .bluetooth_device_found => "craft:bluetooth:deviceFound",
136 .bluetooth_device_connected => "craft:bluetooth:deviceConnected",
137 .bluetooth_device_disconnected => "craft:bluetooth:deviceDisconnected",
176 .shortcut_error => "craft:shortcut:error",
177 .speechrecognition_final => "craft:speechRecognition:final",
178 .speechrecognition_partial => "craft:speechRecognition:partial",
179 .theme => "craft:theme",
138180 .touchbar_action => "craft:touchbar:action",
139 .serial_data => "craft:serial:data",
140 .speech_partial => "craft:speechRecognition:partial",
141 .speech_final => "craft:speechRecognition:final",
181 .tray_menuaction => "craft:tray:menuAction",
182 .updateavailable => "craft:updateAvailable",
183 .updatedownloaded => "craft:updateDownloaded",
184 .window_blur => "craft:window:blur",
185 .window_close => "craft:window:close",
186 .window_focus => "craft:window:focus",
187 .window_minimize => "craft:window:minimize",
188 .window_move => "craft:window:move",
189 .window_resize => "craft:window:resize",
190 .window_restore => "craft:window:restore",
142191 };
143192 }
144193};
@@ -173,6 +222,33 @@ pub fn registerEmitter(channel: Channel) Emitter {
173222 return .{ .channel = channel };
174223}
175224
225/// What craft can honestly say about a channel.
226///
227/// There is no `dead`. A source scan cannot establish absence: `craft:window:*`
228/// names are composed in JavaScript from `__craftDeliverWindowEvent('focus')`,
229/// so no Zig file contains the literal even though the emitter is right there.
230/// The shipped version reported a bare `false` for anything unregistered, which
231/// a page reasonably reads as "this will never fire" — and 21 of 22 channels
232/// said it, including `craft:menu:action` and `craft:theme`, both of which work.
233pub const Liveness = enum {
234 /// Something in this build took out a permit to emit on it.
235 live,
236 /// Craft cannot prove it either way. Subscribe and see; do not disable a
237 /// feature over it.
238 unknown,
239
240 pub fn text(self: Liveness) []const u8 {
241 return switch (self) {
242 .live => "live",
243 .unknown => "unknown",
244 };
245 }
246};
247
248pub fn liveness(channel: Channel) Liveness {
249 return if (live_channels[@backingInt(channel)]) .live else .unknown;
250}
251
176252pub fn isLive(channel: Channel) bool {
177253 return live_channels[@backingInt(channel)];
178254}
@@ -257,8 +333,9 @@ pub fn buildManifest(gpa: std.mem.Allocator, registry: []const NamespaceDecl) ![
257333 if (index > 0) try out.append(gpa, ',');
258334 try out.append(gpa, '"');
259335 try appendEscaped(gpa, &out, channel.eventName());
260 try out.appendSlice(gpa, "\":");
261 try out.appendSlice(gpa, if (isLive(channel)) "true" else "false");
336 try out.appendSlice(gpa, "\":\"");
337 try out.appendSlice(gpa, liveness(channel).text());
338 try out.append(gpa, '"');
262339 }
263340 try out.appendSlice(gpa, "}}");
264341
@@ -281,17 +358,20 @@ test "a channel has no emitter until something registers one" {
281358 try testing.expectEqualStrings("craft:fs:change", emitter.eventName());
282359
283360 // Registering one channel says nothing about any other.
284 try testing.expect(!isLive(.power_sleep));
361 try testing.expect(!isLive(.powersleep));
285362}
286363
287364test "every channel has a distinct event name" {
288365 // A duplicate would make one of the two silently unreportable: the manifest
289366 // is keyed by event name, so the second would overwrite the first.
367 //
368 // Runtime rather than `inline for`: at 44 channels the nested comptime loop
369 // exceeds Zig's backwards-branch budget.
290370 const all = std.enums.values(Channel);
291 inline for (all, 0..) |left, i| {
292 inline for (all, 0..) |right, j| {
293 if (i < j) try testing.expect(!std.mem.eql(u8, left.eventName(), right.eventName()));
294 }
371 var names: [all.len][]const u8 = undefined;
372 for (all, 0..) |ch, i| names[i] = ch.eventName();
373 for (names, 0..) |a, i| {
374 for (names[i + 1 ..]) |b| try testing.expect(!std.mem.eql(u8, a, b));
295375 }
296376}
297377
@@ -388,8 +468,10 @@ test "the manifest reports every channel, live or not" {
388468 // Every channel is present — a dead one has to be reported as dead, not
389469 // omitted, or an app cannot tell "no emitter" from "channel I misspelled".
390470 try testing.expectEqual(@as(usize, channel_count), channels.count());
391 try testing.expect(channels.get("craft:menu:action").?.bool);
392 try testing.expect(!channels.get("craft:fs:change").?.bool);
471 try testing.expectEqualStrings("live", channels.get("craft:menu:action").?.string);
472 // Not "dead" — craft cannot prove a channel has no emitter, and saying so
473 // is what made the shipped manifest tell pages to disable working features.
474 try testing.expectEqualStrings("unknown", channels.get("craft:fs:change").?.string);
393475}
394476
395477test "a reason cannot break the manifest out of its JSON" {