Files
doormile_milderapp/test/picked_does_not_release_test.dart
2026-08-28 11:13:15 +05:30

310 lines
13 KiB
Dart

import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:miler/Models/stop_status.dart';
import 'package:miler/data/api_config.dart';
import 'package:miler/data/api_status.dart';
import 'package:miler/data/miler_api.dart';
import 'package:miler/data/consignment_state.dart';
import 'package:miler/data/lifecycle.dart';
import 'package:miler/data/milk_run.dart';
import 'package:miler/data/service_profile.dart';
/// ─────────────────────────────────────────────────────────────────────────
/// PICKED IS NOT ACTIVE, AND ONLY THE SLIDER MOVES IT
///
/// The production lifecycle the hub, the console and this app must all agree
/// on:
///
/// ```
/// Pending → Accepted → Arrived → Picked → Active → Delivered
/// ▲ ▲
/// pickup-complete start-delivery
/// Collected_By_Miler Out_for_Delivery
/// ```
///
/// Two separate defects collapsed **Picked** into **Active**, and fixing either
/// alone leaves the symptom:
///
/// 1. **Backend.** With `MILER_COLLECTED_STATE_ENABLED` off, `pickup-complete`
/// writes `Out_for_Delivery` itself. Not reachable from this app and not
/// testable here — it is the deployment's behaviour. What IS pinned below
/// is that the app never claims Picked over such a response.
///
/// 2. **App.** `MyPickups.startPickup` released the consignment when the rider
/// merely *tapped a card* — one tap after Picked, before any slider. That
/// is invisible while the flag is off (the pivot has already released, so
/// the release costs no request) and becomes the whole bug the moment the
/// flag is flipped. Removed; pinned here.
///
/// These tests are the reason the flag flip is safe. Read them before changing
/// any call site that reaches `MilerApi.startDelivery`.
/// ─────────────────────────────────────────────────────────────────────────
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
tearDown(() => ServiceProfile.setActive(ServiceProfile.parcel));
// ══════════════════════════════════════════════════════════════════════
// 1 · PICKED DOES NOT CALL START-DELIVERY
//
// Enforced at the source, because this is a statement about *call sites*
// and no behavioural test can prove the absence of one. Every route to
// `MilerApi.startDelivery` is enumerated; a new one fails this test and has
// to be justified here rather than discovered in production.
// ══════════════════════════════════════════════════════════════════════
group('only the rider setting off releases a consignment', () {
/// Every `lib/` file that mentions a symbol, with its line numbers.
Map<String, List<int>> sitesOf(String symbol) {
final out = <String, List<int>>{};
for (final e in Directory('lib').listSync(recursive: true)) {
if (e is! File || !e.path.endsWith('.dart')) continue;
final lines = e.readAsLinesSync();
for (var i = 0; i < lines.length; i++) {
final l = lines[i];
// Prose in a doc comment is not a call.
if (l.trimLeft().startsWith('//') || l.trimLeft().startsWith('///')) {
continue;
}
if (l.contains(symbol)) (out[e.path] ??= []).add(i + 1);
}
}
return out;
}
test('`MilerApi.startDelivery` has exactly one caller', () {
final sites = sitesOf('MilerApi.startDelivery(');
expect(
sites.keys.toSet(),
{'lib/views/Dashboard/pickups/delivery_actions.dart'},
reason:
'the release is `releaseForDelivery`\'s alone. A second caller is '
'a second lifecycle, and the console sees both.',
);
});
test('`releaseForDelivery` is reached only from setting off or the door', () {
final sites = sitesOf('releaseForDelivery(')
..remove('lib/views/Dashboard/pickups/delivery_actions.dart');
expect(
sites.keys.toSet(),
{
// The Start-ride slider on the stop's own map.
'lib/views/Dashboard/pickups/map.dart',
// The Start-round bar, and nothing else on that screen. This file
// held the defect: `startPickup` called `startRound` on card open.
'lib/views/Dashboard/pickups/pickups.dart',
},
reason: 'a new caller must be a deliberate "the rider has set off"',
);
});
test('tapping a card does not release — the defect, pinned', () {
// `startPickup` opens a stop's map. It must contain no release: the
// rider tapping a card to read an address has not set off, and
// `navigatesToCustomer` is true from the instant Picked succeeds.
final src = File(
'lib/views/Dashboard/pickups/pickups.dart',
).readAsStringSync();
final start = src.indexOf('Future<void> startPickup(');
expect(start, greaterThan(0), reason: 'startPickup was renamed');
final end = src.indexOf('Future<void> resumePickup(', start);
expect(end, greaterThan(start));
final body = src
.substring(start, end)
.split('\n')
.where((l) => !l.trimLeft().startsWith('//'))
.join('\n');
expect(
body.contains('startRound('),
isFalse,
reason:
'opening a stop released it, so the console read Active one tap '
'after Picked and before any slider was touched',
);
expect(body.contains('releaseForDelivery('), isFalse);
});
});
// ══════════════════════════════════════════════════════════════════════
// 2 · COLLECTED_BY_MILER IS PICKED, AND STAYS PICKED
// ══════════════════════════════════════════════════════════════════════
group('Collected_By_Miler reads as Picked', () {
test('the pivot response is read as collected, not as released', () {
final t = MilerLifecycle.pickupComplete(
ApiResult(
ok: true,
status: 200,
data: const {
'bookingid': 78,
'consignmentid': 34,
'status': 'Converted_To_Consignment',
'consignmentstatus': 'Collected_By_Miler',
'next_action': 'start_delivery',
},
),
);
expect(t.isConfirmed, isTrue);
expect(t.bookingStatus, BookingStatus.convertedToConsignment);
expect(t.consignmentState, ConsignmentState.collectedByMiler);
expect(t.consignmentId, '34');
expect(t.awaitsStartDelivery, isTrue);
expect(
t.isCompatibilityMode,
isFalse,
reason: 'the flag is ON in this response — the release is still to come',
);
});
test('a collected stop shows the Picked rung', () {
ServiceProfile.setActive(ServiceProfile.milkMan);
final stop = <String, dynamic>{
'orderid': 'A1',
'orderstatus': ApiConfig.legacyStatusFromConsignment(
'Collected_By_Miler',
),
'consignmentstatus': 'Collected_By_Miler',
};
expect(stop['orderstatus'], 'picked', reason: 'adapter contract');
expect(
MilkRun.stageOf(stop, acceptedIds: const {}, collectedIds: const {'A1'}),
StopStatus.picked,
);
});
test('and it still needs releasing — the bar has work to offer', () {
expect(
consignmentStateFromRaw('Collected_By_Miler').needsRelease,
isTrue,
);
expect(
consignmentStateFromRaw('Collected_By_Miler').isDeliverable,
isFalse,
reason: '`deliver` accepts Out_for_Delivery only',
);
});
});
// ══════════════════════════════════════════════════════════════════════
// 3 · START RIDE IS WHAT MAKES IT ACTIVE
// ══════════════════════════════════════════════════════════════════════
group('Start ride moves collected → out for delivery', () {
test('the released state is the Active rung, and only via the local set', () {
ServiceProfile.setActive(ServiceProfile.milkMan);
final stop = <String, dynamic>{
'orderid': 'A1',
'orderstatus': ApiConfig.legacyStatusFromConsignment('Out_for_Delivery'),
'consignmentstatus': 'Out_for_Delivery',
};
// Before the slider: the rider's own record does not carry it, so the
// rung is Picked even though the consignment is released.
expect(
MilkRun.stageOf(stop, acceptedIds: const {}, collectedIds: const {'A1'}),
StopStatus.picked,
);
// After the slider: `startRound` writes the id, and only then.
expect(
MilkRun.stageOf(
stop,
acceptedIds: const {},
collectedIds: const {'A1'},
outForDeliveryIds: const {'A1'},
),
StopStatus.outForDelivery,
);
});
test('a released consignment is deliverable and needs no second release', () {
final s = consignmentStateFromRaw('Out_for_Delivery');
expect(s.isDeliverable, isTrue);
expect(s.needsRelease, isFalse);
expect(ConsignmentGate.gateFor(s), DeliverGate.deliverable);
});
});
// ══════════════════════════════════════════════════════════════════════
// 4 · A STALE OR EARLY ROW CANNOT COLLAPSE PICKED INTO ACTIVE
//
// `GET /miler/bookings` is a poll behind the rider by design. With the flag
// OFF the pivot releases the consignment itself, so the very next poll
// reports `Out_for_Delivery` for a bag still on the counter. The rider's
// card must not jump to Active on that — his own set is what says he set
// off. This is the app half of the compatibility-mode disagreement, and it
// is display only: nothing here is sent to the server.
// ══════════════════════════════════════════════════════════════════════
group('polling cannot start the round for the rider', () {
test('compatibility mode is reported, never silently adopted', () {
final t = MilerLifecycle.pickupComplete(
ApiResult(
ok: true,
status: 200,
data: const {
'bookingid': 78,
'consignmentid': 34,
'consignmentstatus': 'Out_for_Delivery',
},
),
);
expect(t.isConfirmed, isTrue);
expect(t.consignmentState, ConsignmentState.outForDelivery);
expect(
t.isCompatibilityMode,
isTrue,
reason:
'the flag is OFF on this deployment — the pivot released it, and '
'the console showing Active is the SERVER\'s state, not a client '
'bug. Reported so the trace names the backend.',
);
expect(t.awaitsStartDelivery, isFalse);
});
test('an early Out_for_Delivery row still draws Picked', () {
ServiceProfile.setActive(ServiceProfile.milkMan);
// Exactly what the next poll returns in compatibility mode.
final stop = <String, dynamic>{
'orderid': 'A1',
'orderstatus': 'outfordelivery',
'consignmentstatus': 'Out_for_Delivery',
};
expect(
MilkRun.stageOf(stop, acceptedIds: const {}, collectedIds: const {'A1'}),
StopStatus.picked,
reason:
'the rider has not slid Start ride; the row is the consignment\'s '
'state, not his',
);
});
test('but a genuinely finished stop is never dragged backwards', () {
ServiceProfile.setActive(ServiceProfile.milkMan);
for (final raw in const ['Delivered', 'delivered']) {
// Built the way `ApiConfig.pickupFromBooking` builds a real row: the
// consignment's word is translated into `orderstatus`, which is what
// `stopStatusOf` reads. Setting `consignmentstatus` alone would be a
// row this app never sees, and would prove nothing about the ladder.
final stop = <String, dynamic>{
'orderid': 'A1',
'orderstatus': ApiConfig.legacyStatusFromConsignment(raw),
'consignmentstatus': raw,
};
expect(stop['orderstatus'], 'delivered', reason: 'adapter contract');
expect(
MilkRun.stageOf(
stop,
acceptedIds: const {},
collectedIds: const {'A1'},
),
StopStatus.delivered,
reason: 'precedence must only ever hold a stop back, never a close',
);
}
});
});
}