Merge branch 'train-days' into integration

# Conflicts:
#	apps/train/halkyon_train_flutter/lib/src/app/train_paths.dart
#	docs/adr/0030-halkyon-train.md
This commit is contained in:
Tizian 「ティツィアーン」 2026-11-03 23:14:02 +01:00
commit 81adeb91cc
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -0,0 +1,177 @@
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_app/testing.dart';
import 'package:halkyon_train_flutter/src/core/privacy/train_keys.dart';
import 'package:halkyon_train_flutter/src/feature/health/data/health_connect.dart';
import 'package:halkyon_train_flutter/src/feature/health/data/health_platform.dart';
import 'package:halkyon_train_flutter/src/feature/health/data/health_store.dart';
import 'package:halkyon_train_flutter/src/feature/weight/data/weight_repository.dart';
import 'package:halkyon_train_flutter/src/feature/weight/model/weight.dart';
import 'package:halkyon_train_flutter/src/feature/workout/data/workout_repository.dart';
import '../../support/fake_health_platform.dart';
import '../../support/memory_content_key_gateway.dart';
import '../../support/memory_weight_gateway.dart';
import '../../support/memory_workout_gateway.dart';
/// Other apps' weights brought in from Health Connect (ADR 0030, D64): off
/// until the person turns it on, then each weight once, as a weight of
/// Train's own store; one the person entered in Train already is a double
/// and stays out, as does one deleted in Train.
void main() {
final morning = DateTime.utc(2026, 11, 3, 6, 30);
const scale = 'com.example.scale';
late Directory folder;
late TrainKeys keys;
late WorkoutRepository workouts;
late WeightRepository weights;
late FakeHealthPlatform platform;
final opened = <HealthConnect>[];
HealthWeight measured(String id, double kilograms, {int days = 0}) =>
HealthWeight(
id: id,
at: morning.add(Duration(days: days)),
kilograms: kilograms,
app: scale,
);
HealthConnect open() {
final health = HealthConnect(
platform,
HealthStore(File('${folder.path}/health_connect.json')),
workouts,
weights: weights,
)..start();
opened.add(health);
return health;
}
Future<void> settled(HealthConnect health) async {
await pumpEventQueue();
await health.idle;
}
Future<List<WeightContent>> kept() async => [
for (final weight in await weights.all()) weight.content!,
];
setUp(() {
folder = Directory.systemTemp.createTempSync('health');
keys = TrainKeys(
MemoryContentKeyGateway(),
FakeContentCipher(),
() async => FakeAccountCrypto().keysNumbered(1),
);
workouts = WorkoutRepository(MemoryWorkoutGateway(), keys);
weights = WeightRepository(MemoryWeightGateway(), keys);
platform = FakeHealthPlatform();
});
tearDown(() async {
for (final health in opened) {
await health.dispose();
}
opened.clear();
await workouts.dispose();
await weights.dispose();
keys.dispose();
folder.deleteSync(recursive: true);
});
test('nothing is read until the person turns it on, which asks Health '
'Connect for the weights alone', () async {
platform.otherWeights.add(measured('a', 74.3));
final health = open();
await settled(health);
expect(await weights.all(), isEmpty);
expect(platform.asked, isEmpty);
expect(await health.setReadingWeights(on: true), isTrue);
await settled(health);
expect(platform.asked, [
{HealthAccess.weight},
]);
expect(await kept(), [
WeightContent(
at: morning,
kilograms: 74.3,
source: WeightSource.healthConnect,
healthId: 'a',
app: scale,
),
]);
});
test('each weight comes in once, read again or not', () async {
platform.otherWeights.add(measured('a', 74.3));
final health = open();
await health.setReadingWeights(on: true);
await settled(health);
platform.otherWeights.add(measured('b', 74.1, days: 1));
await health.read();
await settled(health);
await health.read();
await settled(health);
expect([for (final weight in await kept()) weight.healthId], ['a', 'b']);
});
test('one the person entered in Train already, within ten minutes and a '
'tenth of a kilogram, stays out', () async {
await weights.save(
WeightContent(
at: morning.add(const Duration(minutes: 4)),
kilograms: 74.35,
),
);
platform.otherWeights
..add(measured('double', 74.3))
..add(measured('other', 74.9, days: 1));
final health = open();
await health.setReadingWeights(on: true);
await settled(health);
expect(
[for (final weight in await kept()) weight.healthId],
[null, 'other'],
);
});
test('one deleted in Train stays out', () async {
platform.otherWeights.add(measured('a', 74.3));
final health = open();
await health.setReadingWeights(on: true);
await settled(health);
await weights.delete((await weights.all()).single.id);
await health.read();
await settled(health);
expect(await weights.all(), isEmpty);
});
test('refused, it stays off and says so; turned off, nothing more '
'comes', () async {
platform = FakeHealthPlatform(grants: {});
platform.otherWeights.add(measured('a', 74.3));
final health = open();
expect(await health.setReadingWeights(on: true), isFalse);
expect(health.settings.readsWeights, isFalse);
expect(health.settings.weightsRefused, isTrue);
expect(await weights.all(), isEmpty);
});
test("the switch and what came in are kept with the phone's settings", () {
final store = HealthStore(File('${folder.path}/health_connect.json'));
const settings = HealthSettings(
readsWeights: true,
weightsRefused: true,
takenWeights: {'a', 'b'},
);
store.save(settings);
expect(store.read(), settings);
});
}