Session expiry, arrival geofence guard, multi-destination stops
Three fixes found by running the app on a real handset against production. 1. An expired token left the app looking signed in and unable to work. MilerApi.onUnauthorized was declared and called on every 401 but never assigned, so the token was dropped and nothing else happened: the profile stayed on disk, logged_out stayed false, and the rider saw his own name over a dashboard whose every call returned 401. He reads that as "no work today". The teardown now lives in endSession() and both ways out of a session — the Log out button and the 401 path — use it. 2. Arrived was written locally even when the rider was not there. updateArrivedStatus answers false for three different things and the caller treated all of them as "the write did not land", which is only true of one. A geofence refusal and a server refusal now stop the rung and hand back the reason; a dead network still advances, as it should. 3. A multi-destination customer pickup collapsed onto one stop. GET /miler/bookings returns a row per destination once collected, all with the same bookingid and reference. Every local store keys on that id, so the accepted store deduped two of three drops away and their consignment ids were unrecoverable. orderid is now the stop key; bookingreference stays the booking's name. Cards show "Stop 2 of 3" and the receiver's own name and number rather than the sender's. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EqVJPB9B4QuieZnBAAKgYQ
This commit is contained in:
@@ -1,67 +1,92 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:miler/controllers/pickups_controller.dart';
|
||||
import 'package:miler/data/geofence.dart';
|
||||
|
||||
/// Whichever way the proximity fence is set, it must be set **once** and on
|
||||
/// purpose.
|
||||
/// ─────────────────────────────────────────────────────────────────────────
|
||||
/// THE SWITCH, AND THE FOUR SHAPES THAT MUST NOT COME BACK
|
||||
///
|
||||
/// [kGeofenceEnforced] decides whether "Picked up" means the rider was standing
|
||||
/// at the door or merely pressed a button, and it has already been wrong in
|
||||
/// three directions: it was `kDebugMode` — so every debug build had no fence
|
||||
/// and the fence was therefore never tested — then a hard `false`, so the flow
|
||||
/// could not be walked at a desk at all, then a define defaulting to off.
|
||||
/// This file used to assert the opposite of what it asserts now, and that is
|
||||
/// the point of keeping it rather than deleting it.
|
||||
///
|
||||
/// **It is OFF, decided 2026-08-25.** It was turned on that morning with a
|
||||
/// radius of [kGeofenceRadiusMeters] and turned off again the same day, at the
|
||||
/// founder's call. Nothing was found wrong with it — this is a decision about
|
||||
/// *when* to switch it on.
|
||||
/// It read:
|
||||
///
|
||||
/// So the direction this file guards has flipped twice in a day, which is
|
||||
/// exactly the pattern that made the old defaults dangerous, and it is why the
|
||||
/// three tests below are not all about the direction. The measurement work is
|
||||
/// unchanged and still asserted: a `best` fix with a shelf life, the phone's
|
||||
/// error credited to the rider, and one radius rather than three. All of it is
|
||||
/// dormant while [kGeofenceEnforced] is false and none of it needs revisiting
|
||||
/// on the day it goes back on.
|
||||
/// expect(kGeofenceEnforced, isFalse); // "recorded decision", 2026-08-25
|
||||
/// expect(kGeofenceRadiusMeters, 10);
|
||||
///
|
||||
/// So the test suite was **enforcing the release blocker**: a developer who
|
||||
/// turned the fence on to fix it would be met with a red test telling him the
|
||||
/// off state was deliberate. Between them those two lines meant every shipped
|
||||
/// build let a rider mark a stop picked up from anywhere on earth, and the
|
||||
/// suite agreed that was correct.
|
||||
///
|
||||
/// Reversed 2026-09-16 as part of the Play Store release audit. The fence now
|
||||
/// lives in `lib/data/geofence.dart` and the behaviour is exercised properly in
|
||||
/// `geofence_test.dart` — 41 cases, the full distance table, every failure
|
||||
/// mode, and an HTTP client that fails the test if a blocked rung posts
|
||||
/// anything.
|
||||
///
|
||||
/// What is left here is the short list of *specific past mistakes*, each of
|
||||
/// which shipped, so that none of them can return quietly.
|
||||
/// ─────────────────────────────────────────────────────────────────────────
|
||||
void main() {
|
||||
test('there is one switch, and the legacy name derives from it', () {
|
||||
// Two constants that could disagree is exactly how a build ends up with
|
||||
// the fence on in one gate and off in the other — the bulk check on Home
|
||||
// and the per-stop check in the controller are separate code paths that
|
||||
// must never answer differently.
|
||||
expect(kBypassGeofenceForTesting, !kGeofenceEnforced);
|
||||
});
|
||||
|
||||
test('enforcement is off, and that is the recorded decision', () {
|
||||
// If this fails, one of two things happened, and both should be read rather
|
||||
// than silenced:
|
||||
//
|
||||
// • the default was turned back on — record the date and the reason here,
|
||||
// as the previous flips did; or
|
||||
// • this run passed `--dart-define=ENFORCE_GEOFENCE=true`, in which case
|
||||
// the failure is correct and is telling you this build has the fence on.
|
||||
test('MISTAKE 1 — the fence must not default to off', () {
|
||||
// `bool.fromEnvironment('ENFORCE_GEOFENCE', defaultValue: false)`. Every
|
||||
// `flutter build appbundle --release` passes no dart-define, so the default
|
||||
// *is* the shipped behaviour. It had been `kDebugMode` before that — off in
|
||||
// exactly the builds anyone tested with — and a hard `false` before that.
|
||||
// Three spellings, one effect.
|
||||
expect(
|
||||
kGeofenceEnforced,
|
||||
isFalse,
|
||||
isTrue,
|
||||
reason:
|
||||
'Proximity enforcement is expected to be OFF (see kGeofenceEnforced). '
|
||||
'Either the default was restored, or this run passed '
|
||||
'--dart-define=ENFORCE_GEOFENCE=true.',
|
||||
'The release build must enforce proximity. If this run passed '
|
||||
'--dart-define=ENFORCE_GEOFENCE=false, this failure is correct.',
|
||||
);
|
||||
});
|
||||
|
||||
test('the radius is one number, and it is the tight one', () {
|
||||
// Asserted while the fence is off, deliberately. The app used to hold
|
||||
// three — a configured `pickupradius` defaulting to 100 in the controller,
|
||||
// a hardcoded 500 in Home's bulk gate, and no agreement between them — so
|
||||
// which fence a rider met depended on whether he ticked boxes or slid a
|
||||
// sheet. Whichever way the switch above is set, that must not come back.
|
||||
expect(kGeofenceRadiusMeters, 10);
|
||||
test('MISTAKE 2 — the radius must not be smaller than the GPS', () {
|
||||
// 10 m is at or inside the error radius of consumer GPS, so the fence
|
||||
// stopped measuring proximity and started measuring whether the satellites
|
||||
// were kind. It refused riders standing at doors, which is what got the
|
||||
// whole control switched off twice.
|
||||
expect(kGeofenceRadiusMetres, 100);
|
||||
expect(
|
||||
kGeofenceRadiusMetres,
|
||||
greaterThan(kGeofenceMaxAccuracyMetres),
|
||||
reason: 'a fence smaller than the fix that measures it cannot be met',
|
||||
);
|
||||
});
|
||||
|
||||
test('a cached fix has a shelf life', () {
|
||||
// On a round, a stale position is reliably the *previous* stop. Anything
|
||||
// much longer than this and the fence starts measuring from the last door.
|
||||
test('MISTAKE 3 — there must be exactly one radius', () {
|
||||
// The app once held three at the same time: a per-tenant `pickupradius`
|
||||
// from prefs defaulting to 100, a hardcoded 500 in Home's bulk gate, and 10
|
||||
// in the controller. Which fence a rider met depended on whether he ticked
|
||||
// boxes on Home or slid the sheet on a stop.
|
||||
//
|
||||
// Both gates call `Geofence.check` now and neither holds arithmetic, so
|
||||
// this asserts the constant is the only knob there is to turn.
|
||||
expect(kGeofenceRadiusMetres, isA<double>());
|
||||
expect(kGeofenceRadiusMetres, greaterThan(0));
|
||||
});
|
||||
|
||||
test('MISTAKE 4 — a cached fix must have a shelf life', () {
|
||||
// A last-known position is instant, free, and can be an hour old. On a
|
||||
// round it is reliably the *previous* stop, which is how a rider marks a
|
||||
// delivery arrived from the last street he was on.
|
||||
expect(kGeofenceFixMaxAge, const Duration(seconds: 30));
|
||||
});
|
||||
|
||||
test('the fence still has a way to be switched off in an emergency', () {
|
||||
// Deliberately kept, and deliberately not the default. If the fence starts
|
||||
// refusing real work at real doors, `--dart-define=ENFORCE_GEOFENCE=false`
|
||||
// gets the fleet moving inside one release rather than one sprint — and
|
||||
// every bypassed check logs in every build mode, so a build's own log says
|
||||
// which way it was compiled.
|
||||
//
|
||||
// Raise `kGeofenceRadiusMetres` before reaching for it.
|
||||
expect(
|
||||
const bool.fromEnvironment('ENFORCE_GEOFENCE', defaultValue: true),
|
||||
kGeofenceEnforced,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user