ReviewOS

also looking at this

craft-native/craft

test(ios): record every deliberate deferral where something checks it

#119
Merged glennmichael123 wants to merge feat/deferral-table into main
1 file +100 -0
packages/zig/test/ios_conformance_test.zigmodified+100-0
Changes to packages/zig/test/ios_conformance_test.zig
@@ -233,6 +233,106 @@ test "every action the page can call is one the spec handles" {
233233 try testing.expect(checked >= 50);
234234}
235235
236/// Every action deliberately left with the Swift shim, and why.
237///
238/// This exists because the reasons kept rotting. They lived in two places that
239/// nothing verified — a prose document and a module comment — and both drifted:
240/// `docs/ios-development.md` listed thirteen actions as permanently Swift while
241/// five of them had already been migrated, and
242/// `bridge_mobile_notifications.zig` explained that `scheduleNotification`
243/// could not be served for a day longer than that was true, which kept the
244/// action with the shim through several passes that each read the note and
245/// believed it.
246///
247/// A list is only load-bearing if something fails when it is wrong. The test
248/// below asserts each entry is a real spec action *and* still unmigrated, so
249/// migrating one of these breaks the build until its row is deleted. That is
250/// the property the doc never had.
251///
252/// Not exhaustive, and deliberately not: this records deferrals that have a
253/// *reason worth keeping*. An action absent from both this table and the Zig
254/// modules is simply not done yet, which is a different and honest state.
255const Deferral = struct { action: []const u8, reason: []const u8 };
256
257const deliberate_deferrals = [_]Deferral{
258 // ARKit / SceneKit. `docs/ios-development.md` asked for this boundary to be
259 // declared in `capability_registry.zig`; that registry is desktop-only —
260 // 51 namespaces, no `mobile`, and neither `ios.zig` nor `ios_dispatch.zig`
261 // reads it — so the boundary is recorded here instead, where the tooling
262 // that counts unmigrated actions can see it.
263 .{ .action = "startAR", .reason = "SceneKit node-graph glue; miserable and low-value through objc_msgSend" },
264 .{ .action = "stopAR", .reason = "SceneKit node-graph glue; miserable and low-value through objc_msgSend" },
265 .{ .action = "getARPlanes", .reason = "SceneKit node-graph glue; miserable and low-value through objc_msgSend" },
266 .{ .action = "placeARObject", .reason = "SceneKit node-graph glue; miserable and low-value through objc_msgSend" },
267 .{ .action = "removeARObject", .reason = "SceneKit node-graph glue; miserable and low-value through objc_msgSend" },
268
269 // StoreKit 2. Narrower than "no ObjC surface": StoreKit *1* is ObjC and
270 // `bridge_iap.zig` already drives it from Zig on macOS, so this is a choice
271 // about which StoreKit iOS targets, not an impossibility.
272 .{ .action = "getProducts", .reason = "Product.PurchaseResult is Swift-only; StoreKit 1 would be a product decision" },
273 .{ .action = "purchase", .reason = "Product.PurchaseResult is Swift-only; StoreKit 1 would be a product decision" },
274 .{ .action = "restorePurchases", .reason = "Product.PurchaseResult is Swift-only; StoreKit 1 would be a product decision" },
275
276 .{ .action = "startLiveActivity", .reason = "ActivityKit is Swift-only; no ObjC class to reach" },
277 .{ .action = "updateLiveActivity", .reason = "ActivityKit is Swift-only; no ObjC class to reach" },
278 .{ .action = "endLiveActivity", .reason = "ActivityKit is Swift-only; no ObjC class to reach" },
279
280 .{ .action = "updateWidget", .reason = "WidgetCenter has no ObjC class; only a swiftself trampoline reaches it" },
281 .{ .action = "reloadWidgets", .reason = "WidgetCenter has no ObjC class; only a swiftself trampoline reaches it" },
282
283 // Structural, and checked again in the sweep that produced this table.
284 .{ .action = "registerBackgroundTask", .reason = "BGTaskScheduler.register must run before launch finishes; a page call is late by construction" },
285 .{ .action = "getInitialURL", .reason = "DeepLinkManager is pure Swift with no @objc; the launch URL is not re-derivable" },
286
287 // The recorder is a CLLocationManager plus two files that
288 // `CraftWebView.Coordinator.init` re-adopts at launch, so a Zig-owned stop
289 // cannot stop a recording Swift has already restored into its own state.
290 .{ .action = "startLocationRecording", .reason = "Coordinator.init re-adopts recordings at launch; Zig cannot stop what Swift restored" },
291 .{ .action = "stopLocationRecording", .reason = "Coordinator.init re-adopts recordings at launch; Zig cannot stop what Swift restored" },
292 .{ .action = "pauseLocationRecording", .reason = "Coordinator.init re-adopts recordings at launch; Zig cannot stop what Swift restored" },
293 .{ .action = "resumeLocationRecording", .reason = "Coordinator.init re-adopts recordings at launch; Zig cannot stop what Swift restored" },
294 .{ .action = "getLocationRecordingState", .reason = "Coordinator.init re-adopts recordings at launch; Zig cannot stop what Swift restored" },
295
296 // Not a capability gap: the spec's own success path has never executed, so
297 // there is no working contract to port. See the tracking issue.
298 .{ .action = "startVideoRecording", .reason = "the spec reads info[.originalImage] for a movie pick, so its success path never runs" },
299};
300
301test "every recorded deferral is real, and still a deferral" {
302 // The anti-rot property. A deferral that has been migrated must fail here
303 // rather than sit in a list telling the next reader not to bother.
304 var spec = try collectCases(testing.allocator, dispatcherRegion());
305 defer spec.deinit();
306
307 var zig = try collectZigActions(testing.allocator);
308 defer zig.deinit();
309
310 for (deliberate_deferrals) |d| {
311 if (!spec.contains(d.action)) {
312 std.debug.print(
313 "deferral table names `{s}`, which the spec's dispatcher does not have — typo, or the action was renamed\n",
314 .{d.action},
315 );
316 return error.DeferralNamesNoSuchAction;
317 }
318 if (zig.contains(d.action)) {
319 std.debug.print(
320 "deferral table still lists `{s}`, but Zig serves it now — delete the row, its reason is spent\n",
321 .{d.action},
322 );
323 return error.DeferralAlreadyMigrated;
324 }
325 // A reason is the entire point of the row.
326 try testing.expect(d.reason.len > 0);
327 }
328
329 // Non-vacuity: the loop above is satisfied by an empty table.
330 try testing.expect(deliberate_deferrals.len >= 20);
331
332 // And the table cannot claim more than remain unmigrated.
333 try testing.expect(deliberate_deferrals.len <= max_not_yet_migrated);
334}
335
236336test "nothing has been dropped on the way from Swift to Zig" {
237337 // The migration guard. Every action the spec answers is either served by
238338 // Zig or explicitly still owed, and the number still owed only goes down.