Files
doormile_milderapp/test/work_scope_test.dart
Thiru-tenext d7348e253f Miler rider app: surface system, visible design language, backend lifecycle
Design system
- MilerSurface ladder (canvas → working → raised → floating) with MilerPanel
  as layer 1; canvas moved to #DEE3EA so white separates at 1.290:1.
- Visible vocabulary applied across Home, Deliveries, Activity, Account and
  the sheets: hero heads (tabular numeral + small caption, clamped at 1.3x),
  canvas wells for anything that opens, small filled tags for shelf labels,
  demoted placeholders. Recorded in DESIGN_SYSTEM.md §6.
- One icon family: 222 Material glyphs migrated to Lucide; none left outside
  lib/xpress.
- Colour semantics corrected: amber only for what is genuinely owed, brand red
  reserved for the live stop, disabled primaries go neutral rather than pale.

Data and lifecycle
- lib/data/lifecycle.dart reads mutations for what they prove; route_order.dart
  makes admin sequence the single ordering authority; service_day.dart, and
  stop_area.dart rewritten against live Coimbatore addresses (digit-token
  stripping, city stoplist, street suffixes, stammer collapse).
- countLabel states the load once, in bags.

Testing
- 1440 tests passing; golden shot harnesses for Home, Deliveries, Activity,
  sheets and verify, with test/failures/ now gitignored (diff debris).
- New pins: home_gutter_test, stop_area_test, plus updated structural bounds.

Note: this commit also carries pre-existing working-tree deletions that were
present before this work (API_SPEC.md, README.md, demo test fixtures).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-22 05:40:35 +05:30

247 lines
8.7 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:miler/data/accepted_store.dart';
import 'package:miler/data/service_profile.dart';
import 'package:miler/data/work_scope.dart';
/// ─────────────────────────────────────────────────────────────────────────
/// TWO LINES, ONE HANDSET, NO MIXING
///
/// Miler runs a milk-man round and a logistics day off one app and one login,
/// and it kept finished work under *global* SharedPreferences keys —
/// `completed_bookings` and friends. One phone, one drawer, whoever wrote
/// last: log out and back in as another rider, or move tenant, and the
/// previous scope's history was sitting in the new session's Activity.
///
/// These pin the ownership rule at the data boundary. Nothing here filters on
/// a display string — no kitchen names, no "Milk", no screen titles — only on
/// identity the session can prove: rider, tenant, line.
/// ─────────────────────────────────────────────────────────────────────────
void main() {
const milkA = WorkScope(userId: 38, tenantId: 13, line: ServiceLine.milkMan);
const logisticsA = WorkScope(
userId: 38,
tenantId: 13,
line: ServiceLine.parcel,
);
const milkB = WorkScope(userId: 99, tenantId: 13, line: ServiceLine.milkMan);
const otherTenant = WorkScope(
userId: 38,
tenantId: 77,
line: ServiceLine.milkMan,
);
group('the key', () {
test('rider, tenant and line each change the drawer', () {
final keys = {
milkA.scoped('completed_bookings'),
logisticsA.scoped('completed_bookings'),
milkB.scoped('completed_bookings'),
otherTenant.scoped('completed_bookings'),
};
expect(
keys.length,
4,
reason:
'all three identity parts must be in the key — two scopes '
'sharing a key is the leak itself',
);
expect(
milkA.scoped('completed_bookings'),
contains('completed_bookings'),
);
});
test('the same session resolves to the same drawer', () {
expect(
milkA.scoped('skipped_bookings'),
const WorkScope(
userId: 38,
tenantId: 13,
line: ServiceLine.milkMan,
).scoped('skipped_bookings'),
);
});
});
group('ownership', () {
test('a milk-man record is not a logistics record', () {
final row = milkA.stamp({'orderid': 'A1'});
expect(milkA.owns(row), isTrue);
expect(
logisticsA.owns(row),
isFalse,
reason: 'same rider, same tenant, other LINE — must not cross',
);
expect(logisticsA.excludes(row), isTrue);
});
test('another rider cannot claim it', () {
final row = milkA.stamp({'orderid': 'A1'});
expect(milkB.owns(row), isFalse);
expect(milkB.excludes(row), isTrue);
});
test('another tenant cannot claim it', () {
final row = milkA.stamp({'orderid': 'A1'});
expect(otherTenant.owns(row), isFalse);
expect(otherTenant.excludes(row), isTrue);
});
test('the API stamps its own identity and it is honoured', () {
// Rows straight off `/miler/assignments` carry the rider; no scope stamp
// is involved and it must still be respected.
expect(milkA.excludes({'orderid': 'A1', 'mileruserid': 99}), isTrue);
expect(milkA.excludes({'orderid': 'A1', 'mileruserid': 38}), isFalse);
});
test('silence is read differently by the two questions', () {
// A row with no identity: `owns` refuses to adopt it (used on legacy
// rows, where inventing ownership IS the leak), `excludes` keeps it
// (used on rows the API just returned for this session).
final bare = {'orderid': 'A1'};
expect(milkA.owns(bare), isFalse);
expect(milkA.excludes(bare), isFalse);
});
});
group('the store, scoped', () {
setUp(() => ServiceProfile.setActive(ServiceProfile.milkMan));
tearDown(() => ServiceProfile.setActive(ServiceProfile.parcel));
String today() {
final n = DateTime.now();
return '${n.year}-${n.month.toString().padLeft(2, '0')}-'
'${n.day.toString().padLeft(2, '0')}';
}
test(
'completed work written as one rider is invisible to the next',
() async {
// Rider 38 finishes a stop.
SharedPreferences.setMockInitialValues({'userid': 38});
await addCompletedBookings([
{'orderid': 'MILK-1', 'pickupcustomer': 'Joe'},
], terminalStatus: 'delivered');
final mine = await getCompletedBookings();
expect(mine.map((r) => r['orderid']), contains('MILK-1'));
// Rider 99 signs in on the same handset. Same day, same store, same
// process — a different drawer.
SharedPreferences.setMockInitialValues({'userid': 99});
final theirs = await getCompletedBookings();
expect(
theirs.where((r) => r['orderid'] == 'MILK-1'),
isEmpty,
reason: "logging in must never expose the previous rider's history",
);
},
);
test('switching line cannot expose the other line’s history', () async {
SharedPreferences.setMockInitialValues({'userid': 38});
ServiceProfile.setActive(ServiceProfile.milkMan);
await addCompletedBookings([
{'orderid': 'ROUND-1'},
], terminalStatus: 'delivered');
expect(
(await getCompletedBookings()).map((r) => r['orderid']),
contains('ROUND-1'),
);
// Same rider, same tenant, logistics day.
ServiceProfile.setActive(ServiceProfile.parcel);
expect(
(await getCompletedBookings()).where((r) => r['orderid'] == 'ROUND-1'),
isEmpty,
reason: 'a milk-man delivery belongs only to milk-man Activity',
);
// And a logistics collection stays out of the round's history.
await addCompletedBookings([
{'orderid': 'PARCEL-1'},
], terminalStatus: 'picked');
ServiceProfile.setActive(ServiceProfile.milkMan);
expect(
(await getCompletedBookings()).where((r) => r['orderid'] == 'PARCEL-1'),
isEmpty,
reason: 'a logistics pickup belongs only to logistics Activity',
);
});
test('a stored record carries the identity that produced it', () async {
SharedPreferences.setMockInitialValues({'userid': 38});
await addCompletedBookings([
{'orderid': 'STAMP-1'},
], terminalStatus: 'delivered');
final row = (await getCompletedBookings()).single;
expect(row['scopeuserid'], 38);
expect(row['scopeline'], ServiceLine.milkMan.name);
});
test('logout empties this scope', () async {
SharedPreferences.setMockInitialValues({'userid': 38});
await addCompletedBookings([
{'orderid': 'BYE-1'},
], terminalStatus: 'delivered');
expect(await getCompletedBookings(), isNotEmpty);
await clearScopedStores();
expect(
await getCompletedBookings(),
isEmpty,
reason: 'signing out must not leave records for the next rider',
);
});
test('unattributable legacy rows are dropped, not adopted', () async {
// The pre-scoping global key, holding somebody's rows. Nothing on them
// says whose, so adopting them would be inventing ownership.
SharedPreferences.setMockInitialValues({
'userid': 38,
'completed_bookings': jsonEncode([
{'orderid': 'LEGACY-1', 'completedday': today()},
]),
});
await migrateLegacyStores();
expect(
(await getCompletedBookings()).where((r) => r['orderid'] == 'LEGACY-1'),
isEmpty,
reason: 'a row that cannot prove ownership must not be adopted',
);
final prefs = await SharedPreferences.getInstance();
expect(
prefs.containsKey('completed_bookings'),
isFalse,
reason: 'the global key is drained so it can never be read again',
);
});
test('a legacy row that proves ownership is carried across', () async {
SharedPreferences.setMockInitialValues({
'userid': 38,
'completed_bookings': jsonEncode([
{
'orderid': 'LEGACY-MINE',
'completedday': today(),
'mileruserid': 38,
'scopeline': 'milkMan',
},
]),
});
await migrateLegacyStores();
expect(
(await getCompletedBookings()).map((r) => r['orderid']),
contains('LEGACY-MINE'),
);
});
});
}