tane/apps/app_seeds/test/services/lot_history_test.dart
vjrj bb5b3d4a43 feat(app): ask 'how did it do?' when a harvest is noted (schema v13)
The note growers most wish they had two seasons later — did it do well,
was it worth keeping — never had a home. New append-only GardenOutcomes
table (lotId, season year, three-face rating, free note); the question
appears ONCE, inline, right after recording a harvest in the batch story,
and is skippable without a trace. The answer shows as one more story
line. Rides backups/sync like every mutable table (JSON codec, LWW
import, HLC clock absorb). Migration v12→v13 guarded + verified from
every historical version.
2026-07-18 12:18:25 +02:00

170 lines
5.4 KiB
Dart

import 'package:flutter_test/flutter_test.dart';
import 'package:tane/data/variety_repository.dart';
import 'package:tane/db/database.dart';
import 'package:tane/db/enums.dart';
import '../support/test_support.dart';
void main() {
late AppDatabase db;
late VarietyRepository repo;
setUp(() {
db = newTestDatabase();
repo = newTestRepository(db);
});
tearDown(() async => db.close());
Future<String> newLot({String? originName, String? originPlace}) async {
final varietyId = await repo.addQuickVariety(label: 'Tomate rosa');
return repo.addLot(
varietyId: varietyId,
harvestYear: 2025,
originName: originName,
originPlace: originPlace,
);
}
group('recordCultivation', () {
test('records a sown movement with defaults', () async {
final lotId = await newLot();
final id = await repo.recordCultivation(
lotId: lotId,
type: MovementType.sown,
);
expect(id, isNotEmpty);
final history = await repo.lotHistory(lotId);
final sown = history.where(
(e) => e.movementType == MovementType.sown,
);
expect(sown, hasLength(1));
expect(sown.first.kind, LotHistoryKind.movement);
expect(sown.first.when, isNotNull);
});
test('records a harvested movement with a note and date', () async {
final lotId = await newLot();
await repo.recordCultivation(
lotId: lotId,
type: MovementType.harvested,
occurredOn: DateTime(2026, 7, 1).millisecondsSinceEpoch,
notes: 'great year',
);
final history = await repo.lotHistory(lotId);
final harvested = history.singleWhere(
(e) => e.movementType == MovementType.harvested,
);
expect(harvested.notes, 'great year');
expect(
harvested.when,
DateTime(2026, 7, 1).millisecondsSinceEpoch,
);
});
test('rejects non-cultivation movement types', () async {
final lotId = await newLot();
expect(
() => repo.recordCultivation(lotId: lotId, type: MovementType.given),
throwsA(isA<ArgumentError>()),
);
});
});
group('lotHistory', () {
test('starts with the lot creation entry carrying its origin', () async {
final lotId = await newLot(originName: 'María', originPlace: 'Aiako');
final history = await repo.lotHistory(lotId);
expect(history, isNotEmpty);
// Most-recent first: creation is the last entry.
final created = history.last;
expect(created.kind, LotHistoryKind.created);
expect(created.originName, 'María');
expect(created.originPlace, 'Aiako');
});
test('merges movements, germination tests and condition checks', () async {
final lotId = await newLot();
await repo.addGerminationTest(
lotId: lotId,
testedOn: DateTime(2026, 2, 1).millisecondsSinceEpoch,
sampleSize: 10,
germinatedCount: 8,
);
await repo.addConditionCheck(
lotId: lotId,
checkedOn: DateTime(2026, 3, 1).millisecondsSinceEpoch,
containerCount: 2,
desiccantState: DesiccantState.dry,
);
await repo.recordCultivation(
lotId: lotId,
type: MovementType.sown,
occurredOn: DateTime(2026, 4, 1).millisecondsSinceEpoch,
);
await repo.recordHandover(
lotId: lotId,
direction: HandoverDirection.iGave,
counterparty: 'Pepe',
);
final history = await repo.lotHistory(lotId);
expect(history.map((e) => e.kind).toSet(), {
LotHistoryKind.created,
LotHistoryKind.movement,
LotHistoryKind.germinationTest,
LotHistoryKind.conditionCheck,
});
final germination = history.singleWhere(
(e) => e.kind == LotHistoryKind.germinationTest,
);
expect(germination.germinationRate, closeTo(0.8, 1e-9));
final check = history.singleWhere(
(e) => e.kind == LotHistoryKind.conditionCheck,
);
expect(check.containerCount, 2);
// Most-recent first: the handover (today) leads, creation closes.
expect(history.first.movementType, MovementType.given);
expect(history.last.kind, LotHistoryKind.created);
// The middle entries are ordered by their explicit dates, descending.
final dated = history
.where((e) => e.kind != LotHistoryKind.created)
.toList();
for (var i = 0; i + 1 < dated.length; i++) {
expect(dated[i].when ?? 0, greaterThanOrEqualTo(dated[i + 1].when ?? 0));
}
});
test('includes garden outcomes as story lines', () async {
final lotId = await newLot();
await repo.addGardenOutcome(
lotId: lotId,
year: 2026,
rating: GardenOutcomeRating.good,
notes: 'true to type, keeping it',
);
final history = await repo.lotHistory(lotId);
final outcome = history.singleWhere(
(e) => e.kind == LotHistoryKind.gardenOutcome,
);
expect(outcome.rating, GardenOutcomeRating.good);
expect(outcome.year, 2026);
expect(outcome.notes, 'true to type, keeping it');
});
test('ignores other lots', () async {
final lotId = await newLot();
final otherLot = await newLot();
await repo.recordCultivation(lotId: otherLot, type: MovementType.sown);
final history = await repo.lotHistory(lotId);
expect(
history.where((e) => e.kind == LotHistoryKind.movement),
isEmpty,
);
});
});
}