also looking at this
test(android): eleven refusals recorded, so the ratchet cannot be gamed
#153
1 file
+44
-4
| @@ -141,9 +141,8 @@ const Deferral = struct { | ||
| 141 | 141 | /// the tenth, and the two guards below are the whole point: a row Zig starts |
| 142 | 142 | /// serving fails, and so does a row naming an action the Kotlin no longer has. |
| 143 | 143 | /// |
| 144 | /// Deliberately non-exhaustive. Ninety-one actions are unmigrated and almost | |
| 145 | /// all of them are simply not reached yet, which is an honest state that needs | |
| 146 | /// no row. A reason is owed only where someone would otherwise try and find | |
| 144 | /// Deliberately non-exhaustive. Most unmigrated actions are simply not reached | |
| 145 | /// yet, which is an honest state that needs no row. A reason is owed only where someone would otherwise try and find | |
| 147 | 146 | /// out the hard way — and demanding one for the rest would invite an invented |
| 148 | 147 | /// reason, which is worse than silence. |
| 149 | 148 | const deliberate_deferrals = [_]Deferral{ |
| @@ -172,6 +171,47 @@ const deliberate_deferrals = [_]Deferral{ | ||
| 172 | 171 | // and that is a change to the shim rather than a migration away from it. |
| 173 | 172 | .{ .action = "setFlashlight", .reason = "writes isFlashlightOn, which toggleFlashlight reads; Zig cannot reach the field" }, |
| 174 | 173 | .{ .action = "toggleFlashlight", .reason = "reads isFlashlightOn, which only the Kotlin setFlashlight maintains" }, |
| 174 | ||
| 175 | // ---- Actions whose whole implementation is a refusal -------------- | |
| 176 | // | |
| 177 | // These eight reject with a message and do nothing else. They are the | |
| 178 | // easiest actions in the file to "migrate" — the body is one string — and | |
| 179 | // migrating one would be a way of making this ratchet lie. | |
| 180 | // | |
| 181 | // The number counts actions Zig *serves*, meaning actions a page gets an | |
| 182 | // answer from Zig for. Moving a refusal across changes which language | |
| 183 | // holds the string and nothing a page can observe, so it would lower the | |
| 184 | // count without serving anything. That is worth recording explicitly, | |
| 185 | // because the incentive runs the other way: eleven rows here are eleven | |
| 186 | // easy decrements left on the table. | |
| 187 | // | |
| 188 | // Each is also honest as it stands, which is the other half of the reason. | |
| 189 | // Unlike `setBadge`, a page calling these is told plainly that the feature | |
| 190 | // is not there. | |
| 191 | .{ .action = "purchase", .reason = "the Kotlin rejects with 'Purchase flow not fully implemented'; porting a refusal serves nothing" }, | |
| 192 | .{ .action = "scanQRCode", .reason = "the Kotlin rejects; ML Kit scanning was never wired up" }, | |
| 193 | .{ .action = "signInWithGoogle", .reason = "the Kotlin rejects; Google Sign In needs build.gradle configuration" }, | |
| 194 | .{ .action = "startAR", .reason = "the Kotlin rejects; ARCore needs Activity integration that does not exist" }, | |
| 195 | .{ .action = "placeARObject", .reason = "the Kotlin rejects, for the same missing ARCore integration" }, | |
| 196 | .{ .action = "removeARObject", .reason = "the Kotlin rejects, for the same missing ARCore integration" }, | |
| 197 | .{ .action = "sendToWatch", .reason = "the Kotlin rejects; Wear OS needs a companion app" }, | |
| 198 | .{ .action = "updateWatchContext", .reason = "the Kotlin rejects, for the same missing companion app" }, | |
| 199 | ||
| 200 | // ---- And three that answer successfully instead ------------------ | |
| 201 | // | |
| 202 | // Same shape, opposite honesty. `startAR` refuses to begin a session, and | |
| 203 | // then these two report success for operations on the session it refused | |
| 204 | // to create: `stopAR` resolves `{stopped: true}` and `getARPlanes` | |
| 205 | // resolves `[]`. | |
| 206 | // | |
| 207 | // The empty array is the one that costs something. A page cannot tell it | |
| 208 | // from "AR is running and has found no planes yet", so the honest failure | |
| 209 | // `startAR` already gave is undone by the next call. Recorded rather than | |
| 210 | // migrated for the same reason as `setBadge`: there is no working | |
| 211 | // behaviour to carry across, and porting the fabrication would spread it. | |
| 212 | .{ .action = "stopAR", .reason = "resolves {stopped:true} for a session startAR always refuses to open" }, | |
| 213 | .{ .action = "getARPlanes", .reason = "resolves [], which a page cannot tell from AR running with no planes found" }, | |
| 214 | .{ .action = "closePDF", .reason = "resolves true and does nothing; the Kotlin says PDFs open in an external viewer" }, | |
| 175 | 215 | }; |
| 176 | 216 | |
| 177 | 217 | test "every recorded deferral is real, and still a deferral" { |
| @@ -205,7 +245,7 @@ test "every recorded deferral is real, and still a deferral" { | ||
| 205 | 245 | } |
| 206 | 246 | |
| 207 | 247 | // Non-vacuity: the loop above is satisfied by an empty table. |
| 208 | try testing.expect(deliberate_deferrals.len >= 4); | |
| 248 | try testing.expect(deliberate_deferrals.len >= 15); | |
| 209 | 249 | |
| 210 | 250 | // And the table cannot claim more than remain unmigrated. |
| 211 | 251 | try testing.expect(deliberate_deferrals.len <= max_not_yet_migrated); |