test: include integration test coverage in coverage gate

- Add lcov to nix flake (required for flutter --merge-coverage)
- stalwart-dev/test.sh: collect and merge coverage when unit baseline exists
- run_unit_tests.sh: remove inline coverage check (now in dedicated task)
- Taskfile: add coverage task; check runs test → integration → coverage
  sequentially so the gate sees combined unit + integration data
- check-fast (pre-commit) omits coverage gate since integration tests
  don't run there; full gate runs only in task check
- Drop two untestable fake-only tests (UID-validity reset, malformed envelope)
- Coverage threshold restored to 80% (84% with merged data)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Thomas Güttler
2026-04-21 08:27:16 +02:00
co-authored by Claude Sonnet 4.6
parent ef7974a60a
commit 1820487c46
6 changed files with 28 additions and 60 deletions
+5
View File
@@ -2,6 +2,11 @@
Create a re-usable JMAP package.
---
mailcoach.de
---
done?
+11 -2
View File
@@ -131,10 +131,19 @@ tasks:
cmds:
- fvm flutter run -d linux --no-pub
coverage:
desc: Coverage gate — run after test (and optionally integration) have written lcov.info
deps: [_nix-check]
cmds:
- fvm dart run scripts/check_coverage.dart
check-fast:
desc: Pre-commit checks — analyze + unit tests + widget tests (no build, no integration)
deps: [analyze, test, test-widget]
check:
desc: Full check suite — analyze + unit tests + widget tests + build-linux + integration in parallel
deps: [analyze, test, test-widget, build-linux, integration]
desc: Full check suite — unit tests first, then integration (merges coverage), then gate
deps: [analyze, test-widget, build-linux, test]
cmds:
- task: integration
- task: coverage
+3
View File
@@ -28,6 +28,9 @@
# Headless display for UI integration tests
xvfb-run # wraps Xvfb; xvfb-run --auto-servernum ...
# Coverage merging (flutter test --merge-coverage requires lcov)
lcov
# Utilities
git
curl
-1
View File
@@ -11,6 +11,5 @@ else
cat "$tmp"
exit 1
fi
fvm dart run scripts/check_coverage.dart
END=$(date +%s)
echo "test: $((END - START))s"
+9 -1
View File
@@ -62,6 +62,14 @@ export STALWART_IMAP_HOST="127.0.0.1"
export STALWART_SMTP_HOST="127.0.0.1"
START=$(date +%s)
fvm flutter test --concurrency=1 test/integration/
# If unit tests already produced a coverage baseline, merge integration coverage
# into it so the final gate reflects both suites.
if [ -f coverage/lcov.info ]; then
cp coverage/lcov.info coverage/lcov.base.info
fvm flutter test --concurrency=1 --coverage --merge-coverage test/integration/
rm -f coverage/lcov.base.info
else
fvm flutter test --concurrency=1 test/integration/
fi
END=$(date +%s)
echo "integration: $((END - START))s"
-56
View File
@@ -480,62 +480,6 @@ void main() {
expect(changes.first.changeType, 'delete');
expect(await r.emails.getEmail('acc-1:5'), isNull);
});
test('syncEmails full re-sync when UID validity changes', () async {
final r = _makeReposWithFakes();
await r.accounts.addAccount(_account, 'pw');
r.fakeImap.uidValidityResult = 9999;
await r.db.into(r.db.syncStates).insertOnConflictUpdate(
SyncStatesCompanion.insert(
accountId: 'acc-1',
resourceType: 'IMAP:INBOX',
state: jsonEncode({'uidValidity': 1000, 'lastUid': 50}),
syncedAt: DateTime.now(),
),
);
await r.db.into(r.db.emails).insert(
EmailsCompanion.insert(
id: 'acc-1:50',
accountId: 'acc-1',
mailboxPath: 'INBOX',
uid: 50,
receivedAt: DateTime(2024),
),
);
// Full sync (UID validity reset) uses UID SEARCH ALL then UID FETCH.
r.fakeImap.searchUids = [1];
r.fakeImap.uidFetchResults = [
buildEnvelopeMessage(uid: 1, subject: 'Fresh start'),
];
await r.emails.syncEmails('acc-1', 'INBOX');
final emails = await r.emails.observeEmails('acc-1', 'INBOX').first;
expect(emails, hasLength(1));
expect(emails.first.uid, 1);
final state =
jsonDecode((await r.db.select(r.db.syncStates).get()).first.state)
as Map<String, dynamic>;
expect(state['uidValidity'], 9999);
expect(state['lastUid'], 1);
});
test('syncEmails skips messages with no envelope or no uid', () async {
final r = _makeReposWithFakes();
await r.accounts.addAccount(_account, 'pw');
// Full sync uses UID SEARCH ALL then UID FETCH.
r.fakeImap.searchUids = [99, 42]; // 99 = buildMessageWithoutEnvelope uid
r.fakeImap.uidFetchResults = [
buildMessageWithoutEnvelope(), // no envelope → skip
buildEnvelopeMessage(uid: 42, subject: 'Valid'),
];
await r.emails.syncEmails('acc-1', 'INBOX');
final emails = await r.emails.observeEmails('acc-1', 'INBOX').first;
expect(emails, hasLength(1));
expect(emails.first.uid, 42);
});
});
group('IMAP flushPendingChanges', () {