Compare commits

...
Author SHA1 Message Date
Bot of Thomas Güttler a04b576414 Merge branch 'main' into issue-491-parallelize-check 2026-06-07 04:27:19 +02:00
Bot of Thomas Güttler 76f2635700 fix(search): sort search results by received date descending (#520) 2026-06-07 04:24:24 +02:00
Bot of Thomas Güttler e2bb299300 fix(ci): exclude chaos_monkey_test from regular CI (#518) 2026-06-07 04:24:10 +02:00
Bot of Thomas Güttler f5abe9132b fix(test): sync before searching in second searchEmails IMAP test (#519) 2026-06-07 02:49:53 +02:00
Bot of Thomas Güttler d55b316d4c ci: add concurrency cancel-in-progress to ci.yml (#516) 2026-06-07 02:40:13 +02:00
Bot of Thomas Güttler f7fd30da15 feat(ci): add Print runner wait time step to all workflow jobs (#517) 2026-06-07 02:40:08 +02:00
Thomas GuettlerandClaude Sonnet 4.6 90c08a98cd ci: parallelize Format/Analyze/CheckGenerated/Coverage in Check()
Run the four independent check steps concurrently using an errgroup,
matching the pattern already used for TestBackend/TestIntegration.
This cuts expected wall-clock time for the Check() phase by ~50%.

Closes #491

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 00:16:54 +00:00
12 changed files with 282 additions and 50 deletions
+20
View File
@@ -4,12 +4,32 @@ on:
branches: branches:
- main - main
pull_request: pull_request:
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs: jobs:
check: check:
name: Full Project Check name: Full Project Check
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 60 timeout-minutes: 60
steps: steps:
- name: Print runner wait time
env:
FORGEJO_TOKEN: ${{ github.token }}
RUN_NUMBER: ${{ github.run_number }}
run: |
runner_start=$(date +%s)
created_at=$(curl -sf \
-H "Authorization: token $FORGEJO_TOKEN" \
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/actions/tasks?limit=100" \
| python3 -c "import sys,json;data=json.load(sys.stdin);rs=[r for r in data.get('workflow_runs',[]) if r.get('run_number')==$RUN_NUMBER];print(rs[0]['created_at'] if rs else '')" 2>/dev/null)
if [ -n "$created_at" ]; then
queued_epoch=$(date -d "$created_at" +%s)
wait_seconds=$((runner_start - queued_epoch))
echo "Runner wait time: ${wait_seconds}s (queued at $created_at)"
else
echo "Runner wait time: unknown (API lookup failed)"
fi
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Setup Dagger Remote Engine - name: Setup Dagger Remote Engine
env: env:
+85
View File
@@ -15,6 +15,23 @@ jobs:
linux: ${{ steps.diff.outputs.linux }} linux: ${{ steps.diff.outputs.linux }}
steps: steps:
- name: Print runner wait time
env:
FORGEJO_TOKEN: ${{ github.token }}
RUN_NUMBER: ${{ github.run_number }}
run: |
runner_start=$(date +%s)
created_at=$(curl -sf \
-H "Authorization: token $FORGEJO_TOKEN" \
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/actions/tasks?limit=100" \
| python3 -c "import sys,json;data=json.load(sys.stdin);rs=[r for r in data.get('workflow_runs',[]) if r.get('run_number')==$RUN_NUMBER];print(rs[0]['created_at'] if rs else '')" 2>/dev/null)
if [ -n "$created_at" ]; then
queued_epoch=$(date -d "$created_at" +%s)
wait_seconds=$((runner_start - queued_epoch))
echo "Runner wait time: ${wait_seconds}s (queued at $created_at)"
else
echo "Runner wait time: unknown (API lookup failed)"
fi
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
@@ -141,6 +158,23 @@ jobs:
if: needs.check-changes.outputs.android == 'true' if: needs.check-changes.outputs.android == 'true'
steps: steps:
- name: Print runner wait time
env:
FORGEJO_TOKEN: ${{ github.token }}
RUN_NUMBER: ${{ github.run_number }}
run: |
runner_start=$(date +%s)
created_at=$(curl -sf \
-H "Authorization: token $FORGEJO_TOKEN" \
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/actions/tasks?limit=100" \
| python3 -c "import sys,json;data=json.load(sys.stdin);rs=[r for r in data.get('workflow_runs',[]) if r.get('run_number')==$RUN_NUMBER];print(rs[0]['created_at'] if rs else '')" 2>/dev/null)
if [ -n "$created_at" ]; then
queued_epoch=$(date -d "$created_at" +%s)
wait_seconds=$((runner_start - queued_epoch))
echo "Runner wait time: ${wait_seconds}s (queued at $created_at)"
else
echo "Runner wait time: unknown (API lookup failed)"
fi
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
fetch-depth: 100 fetch-depth: 100
@@ -175,6 +209,23 @@ jobs:
if: needs.check-changes.outputs.android == 'true' if: needs.check-changes.outputs.android == 'true'
steps: steps:
- name: Print runner wait time
env:
FORGEJO_TOKEN: ${{ github.token }}
RUN_NUMBER: ${{ github.run_number }}
run: |
runner_start=$(date +%s)
created_at=$(curl -sf \
-H "Authorization: token $FORGEJO_TOKEN" \
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/actions/tasks?limit=100" \
| python3 -c "import sys,json;data=json.load(sys.stdin);rs=[r for r in data.get('workflow_runs',[]) if r.get('run_number')==$RUN_NUMBER];print(rs[0]['created_at'] if rs else '')" 2>/dev/null)
if [ -n "$created_at" ]; then
queued_epoch=$(date -d "$created_at" +%s)
wait_seconds=$((runner_start - queued_epoch))
echo "Runner wait time: ${wait_seconds}s (queued at $created_at)"
else
echo "Runner wait time: unknown (API lookup failed)"
fi
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
fetch-depth: 100 fetch-depth: 100
@@ -203,6 +254,23 @@ jobs:
if: needs.check-changes.outputs.linux == 'true' if: needs.check-changes.outputs.linux == 'true'
steps: steps:
- name: Print runner wait time
env:
FORGEJO_TOKEN: ${{ github.token }}
RUN_NUMBER: ${{ github.run_number }}
run: |
runner_start=$(date +%s)
created_at=$(curl -sf \
-H "Authorization: token $FORGEJO_TOKEN" \
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/actions/tasks?limit=100" \
| python3 -c "import sys,json;data=json.load(sys.stdin);rs=[r for r in data.get('workflow_runs',[]) if r.get('run_number')==$RUN_NUMBER];print(rs[0]['created_at'] if rs else '')" 2>/dev/null)
if [ -n "$created_at" ]; then
queued_epoch=$(date -d "$created_at" +%s)
wait_seconds=$((runner_start - queued_epoch))
echo "Runner wait time: ${wait_seconds}s (queued at $created_at)"
else
echo "Runner wait time: unknown (API lookup failed)"
fi
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
fetch-depth: 100 fetch-depth: 100
@@ -236,6 +304,23 @@ jobs:
timeout-minutes: 5 timeout-minutes: 5
steps: steps:
- name: Print runner wait time
env:
FORGEJO_TOKEN: ${{ github.token }}
RUN_NUMBER: ${{ github.run_number }}
run: |
runner_start=$(date +%s)
created_at=$(curl -sf \
-H "Authorization: token $FORGEJO_TOKEN" \
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/actions/tasks?limit=100" \
| python3 -c "import sys,json;data=json.load(sys.stdin);rs=[r for r in data.get('workflow_runs',[]) if r.get('run_number')==$RUN_NUMBER];print(rs[0]['created_at'] if rs else '')" 2>/dev/null)
if [ -n "$created_at" ]; then
queued_epoch=$(date -d "$created_at" +%s)
wait_seconds=$((runner_start - queued_epoch))
echo "Runner wait time: ${wait_seconds}s (queued at $created_at)"
else
echo "Runner wait time: unknown (API lookup failed)"
fi
- name: Set CI/Full-Pass or CI/Full-Fail label on tracking issue - name: Set CI/Full-Pass or CI/Full-Fail label on tracking issue
env: env:
FORGEJO_TOKEN: ${{ github.token }} FORGEJO_TOKEN: ${{ github.token }}
+34
View File
@@ -14,6 +14,23 @@ jobs:
has_changes: ${{ steps.diff.outputs.has_changes }} has_changes: ${{ steps.diff.outputs.has_changes }}
steps: steps:
- name: Print runner wait time
env:
FORGEJO_TOKEN: ${{ github.token }}
RUN_NUMBER: ${{ github.run_number }}
run: |
runner_start=$(date +%s)
created_at=$(curl -sf \
-H "Authorization: token $FORGEJO_TOKEN" \
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/actions/tasks?limit=100" \
| python3 -c "import sys,json;data=json.load(sys.stdin);rs=[r for r in data.get('workflow_runs',[]) if r.get('run_number')==$RUN_NUMBER];print(rs[0]['created_at'] if rs else '')" 2>/dev/null)
if [ -n "$created_at" ]; then
queued_epoch=$(date -d "$created_at" +%s)
wait_seconds=$((runner_start - queued_epoch))
echo "Runner wait time: ${wait_seconds}s (queued at $created_at)"
else
echo "Runner wait time: unknown (API lookup failed)"
fi
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
@@ -50,6 +67,23 @@ jobs:
if: needs.check-changes.outputs.has_changes == 'true' if: needs.check-changes.outputs.has_changes == 'true'
steps: steps:
- name: Print runner wait time
env:
FORGEJO_TOKEN: ${{ github.token }}
RUN_NUMBER: ${{ github.run_number }}
run: |
runner_start=$(date +%s)
created_at=$(curl -sf \
-H "Authorization: token $FORGEJO_TOKEN" \
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/actions/tasks?limit=100" \
| python3 -c "import sys,json;data=json.load(sys.stdin);rs=[r for r in data.get('workflow_runs',[]) if r.get('run_number')==$RUN_NUMBER];print(rs[0]['created_at'] if rs else '')" 2>/dev/null)
if [ -n "$created_at" ]; then
queued_epoch=$(date -d "$created_at" +%s)
wait_seconds=$((runner_start - queued_epoch))
echo "Runner wait time: ${wait_seconds}s (queued at $created_at)"
else
echo "Runner wait time: unknown (API lookup failed)"
fi
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
fetch-depth: 1 fetch-depth: 1
+17
View File
@@ -18,6 +18,23 @@ jobs:
timeout-minutes: 60 timeout-minutes: 60
steps: steps:
- name: Print runner wait time
env:
FORGEJO_TOKEN: ${{ github.token }}
RUN_NUMBER: ${{ github.run_number }}
run: |
runner_start=$(date +%s)
created_at=$(curl -sf \
-H "Authorization: token $FORGEJO_TOKEN" \
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/actions/tasks?limit=100" \
| python3 -c "import sys,json;data=json.load(sys.stdin);rs=[r for r in data.get('workflow_runs',[]) if r.get('run_number')==$RUN_NUMBER];print(rs[0]['created_at'] if rs else '')" 2>/dev/null)
if [ -n "$created_at" ]; then
queued_epoch=$(date -d "$created_at" +%s)
wait_seconds=$((runner_start - queued_epoch))
echo "Runner wait time: ${wait_seconds}s (queued at $created_at)"
else
echo "Runner wait time: unknown (API lookup failed)"
fi
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
submodules: recursive submodules: recursive
+29 -21
View File
@@ -539,7 +539,7 @@ func (m *Ci) TestBackend(ctx context.Context) (string, error) {
return m.WithStalwart(m.setup(m.backendSrc())). return m.WithStalwart(m.setup(m.backendSrc())).
WithExec([]string{"/bin/bash", "-c", WithExec([]string{"/bin/bash", "-c",
`tmp=$(mktemp); trap 'rm -f "$tmp"' EXIT; ` + `tmp=$(mktemp); trap 'rm -f "$tmp"' EXIT; ` +
`flutter test --concurrency=1 --reporter expanded --no-pub test/backend >"$tmp" 2>&1 || { cat "$tmp"; exit 1; }; ` + `flutter test --concurrency=1 --reporter expanded --no-pub --exclude-tags=nightly test/backend >"$tmp" 2>&1 || { cat "$tmp"; exit 1; }; ` +
`grep -E '^All [0-9]+ tests passed' "$tmp" || tail -1 "$tmp"`}). `grep -E '^All [0-9]+ tests passed' "$tmp" || tail -1 "$tmp"`}).
Stdout(ctx) Stdout(ctx)
} }
@@ -570,7 +570,7 @@ func (m *Ci) ChaosMonkeyBackend(ctx context.Context) (string, error) {
return m.WithStalwart(m.setup(m.backendSrc())). return m.WithStalwart(m.setup(m.backendSrc())).
WithExec([]string{"/bin/bash", "-c", WithExec([]string{"/bin/bash", "-c",
`tmp=$(mktemp); trap 'rm -f "$tmp"' EXIT; ` + `tmp=$(mktemp); trap 'rm -f "$tmp"' EXIT; ` +
`flutter test test/backend/chaos_monkey_test.dart --reporter expanded --concurrency=1 --no-pub >"$tmp" 2>&1 || { cat "$tmp"; exit 1; }; ` + `flutter test test/backend/chaos_monkey_test.dart --reporter expanded --concurrency=1 --no-pub --tags=nightly >"$tmp" 2>&1 || { cat "$tmp"; exit 1; }; ` +
`grep -E '^All [0-9]+ tests passed' "$tmp" || tail -1 "$tmp"`}). `grep -E '^All [0-9]+ tests passed' "$tmp" || tail -1 "$tmp"`}).
Stdout(ctx) Stdout(ctx)
} }
@@ -594,25 +594,33 @@ func (m *Ci) Check(ctx context.Context) (string, error) {
return "", err return "", err
} }
checkSetup := m.setup(m.checkSrc()) // Run format, analyze, generated-code check, and coverage in parallel —
// they all share the same setup base and have no dependencies on each other.
if _, err := checkSetup.WithExec([]string{"dart", "format", "--output=none", "--set-exit-if-changed", "lib", "test"}).Stdout(ctx); err != nil { var analyze, mocks, coverage string
return "Format check failed", err var checkEg errgroup.Group
} checkEg.Go(func() error {
setup := m.setup(m.checkSrc())
analyze, err := checkSetup.WithExec([]string{"dart", "analyze", "--fatal-infos"}).Stdout(ctx) _, err := setup.WithExec([]string{"dart", "format", "--output=none", "--set-exit-if-changed", "lib", "test"}).Stdout(ctx)
if err != nil { return err
return analyze, err })
} checkEg.Go(func() error {
setup := m.setup(m.checkSrc())
mocks, err := m.CheckGenerated(ctx) var err error
if err != nil { analyze, err = setup.WithExec([]string{"dart", "analyze", "--fatal-infos"}).Stdout(ctx)
return mocks, err return err
} })
checkEg.Go(func() error {
coverage, err := m.Coverage(ctx) var err error
if err != nil { mocks, err = m.CheckGenerated(ctx)
return coverage, err return err
})
checkEg.Go(func() error {
var err error
coverage, err = m.Coverage(ctx)
return err
})
if err := checkEg.Wait(); err != nil {
return "", err
} }
// Use errgroup.Group (not WithContext) so a failing test does not cancel its // Use errgroup.Group (not WithContext) so a failing test does not cancel its
@@ -2922,9 +2922,9 @@ class EmailRepositoryImpl implements EmailRepository {
final sql = accountId != null final sql = accountId != null
? 'SELECT e.* FROM email_fts f JOIN emails e ON e.rowid = f.rowid' ? 'SELECT e.* FROM email_fts f JOIN emails e ON e.rowid = f.rowid'
' WHERE email_fts MATCH ? AND e.account_id = ? ORDER BY rank LIMIT 50' ' WHERE email_fts MATCH ? AND e.account_id = ? ORDER BY e.received_at DESC LIMIT 50'
: 'SELECT e.* FROM email_fts f JOIN emails e ON e.rowid = f.rowid' : 'SELECT e.* FROM email_fts f JOIN emails e ON e.rowid = f.rowid'
' WHERE email_fts MATCH ? ORDER BY rank LIMIT 50'; ' WHERE email_fts MATCH ? ORDER BY e.received_at DESC LIMIT 50';
final variables = accountId != null final variables = accountId != null
? [Variable<String>(ftsQuery), Variable<String>(accountId)] ? [Variable<String>(ftsQuery), Variable<String>(accountId)]
: [Variable<String>(ftsQuery)]; : [Variable<String>(ftsQuery)];
@@ -2942,6 +2942,7 @@ class EmailRepositoryImpl implements EmailRepository {
for (final e in [...emailRows.map(_toModel), ...noteRows]) { for (final e in [...emailRows.map(_toModel), ...noteRows]) {
if (seen.add(e.id)) merged.add(e); if (seen.add(e.id)) merged.add(e);
} }
merged.sort((a, b) => b.receivedAt.compareTo(a.receivedAt));
return merged; return merged;
} }
@@ -2952,16 +2953,12 @@ class EmailRepositoryImpl implements EmailRepository {
String? mailboxPath, String? mailboxPath,
String query, String query,
) async { ) async {
final words = query final words =
.trim() query.trim().split(RegExp(r'\s+')).where((w) => w.isNotEmpty).toList();
.split(RegExp(r'\s+'))
.where((w) => w.isNotEmpty)
.toList();
if (words.isEmpty) return []; if (words.isEmpty) return [];
final noteConditions = words.map((_) => 'n.note_text LIKE ?').join(' AND '); final noteConditions = words.map((_) => 'n.note_text LIKE ?').join(' AND ');
final likeVars = final likeVars = words.map((w) => Variable<String>('%$w%')).toList();
words.map((w) => Variable<String>('%$w%')).toList();
final extraConditions = StringBuffer(); final extraConditions = StringBuffer();
final extraVars = <Variable<String>>[]; final extraVars = <Variable<String>>[];
@@ -2980,14 +2977,13 @@ class EmailRepositoryImpl implements EmailRepository {
' WHERE $noteConditions$extraConditions' ' WHERE $noteConditions$extraConditions'
' ORDER BY e.received_at DESC LIMIT 50'; ' ORDER BY e.received_at DESC LIMIT 50';
final rows = await _db final rows = await _db.customSelect(
.customSelect( sql,
sql, variables: [...likeVars, ...extraVars],
variables: [...likeVars, ...extraVars], readsFrom: {_db.emails, _db.emailNotes},
readsFrom: {_db.emails, _db.emailNotes}, ).get();
) final emailRows =
.get(); await Future.wait(rows.map((r) => _db.emails.mapFromRow(r)));
final emailRows = await Future.wait(rows.map((r) => _db.emails.mapFromRow(r)));
return emailRows.map(_toModel).toList(); return emailRows.map(_toModel).toList();
} }
@@ -2997,9 +2993,7 @@ class EmailRepositoryImpl implements EmailRepository {
static String _toFtsQuery(String query) { static String _toFtsQuery(String query) {
final words = query final words = query
.trim() .trim()
.split(RegExp(r'\s+')) .split(RegExp(r'[^\w]+'))
.where((w) => w.isNotEmpty)
.map((w) => w.replaceAll(RegExp(r'[^\w]'), ''))
.where((w) => w.isNotEmpty) .where((w) => w.isNotEmpty)
.toList(); .toList();
if (words.isEmpty) return ''; if (words.isEmpty) return '';
@@ -3101,6 +3095,8 @@ class EmailRepositoryImpl implements EmailRepository {
} }
@override @override
// Results are limited to emails already synced into the local SQLite FTS5
// index; call syncEmails first to ensure the index is up-to-date.
Future<List<model.Email>> searchEmails( Future<List<model.Email>> searchEmails(
String accountId, String accountId,
String mailboxPath, String mailboxPath,
@@ -3111,7 +3107,7 @@ class EmailRepositoryImpl implements EmailRepository {
const sql = 'SELECT e.* FROM email_fts f JOIN emails e ON e.rowid = f.rowid' const sql = 'SELECT e.* FROM email_fts f JOIN emails e ON e.rowid = f.rowid'
' WHERE email_fts MATCH ? AND e.account_id = ? AND e.mailbox_path = ?' ' WHERE email_fts MATCH ? AND e.account_id = ? AND e.mailbox_path = ?'
' ORDER BY rank LIMIT 50'; ' ORDER BY e.received_at DESC LIMIT 50';
final variables = [ final variables = [
Variable<String>(ftsQuery), Variable<String>(ftsQuery),
Variable<String>(accountId), Variable<String>(accountId),
@@ -3124,14 +3120,14 @@ class EmailRepositoryImpl implements EmailRepository {
queryRows.map((r) => _db.emails.mapFromRow(r)), queryRows.map((r) => _db.emails.mapFromRow(r)),
); );
final noteRows = final noteRows = await _searchEmailsByNotes(accountId, mailboxPath, query);
await _searchEmailsByNotes(accountId, mailboxPath, query);
final seen = <String>{}; final seen = <String>{};
final merged = <model.Email>[]; final merged = <model.Email>[];
for (final e in [...emailRows.map(_toModel), ...noteRows]) { for (final e in [...emailRows.map(_toModel), ...noteRows]) {
if (seen.add(e.id)) merged.add(e); if (seen.add(e.id)) merged.add(e);
} }
merged.sort((a, b) => b.receivedAt.compareTo(a.receivedAt));
return merged; return merged;
} }
+2
View File
@@ -109,6 +109,7 @@ class _SharedInboxAppState extends ConsumerState<SharedInboxApp> {
theme: ThemeData( theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo), colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
useMaterial3: true, useMaterial3: true,
splashFactory: NoSplash.splashFactory,
), ),
darkTheme: ThemeData( darkTheme: ThemeData(
colorScheme: ColorScheme.fromSeed( colorScheme: ColorScheme.fromSeed(
@@ -116,6 +117,7 @@ class _SharedInboxAppState extends ConsumerState<SharedInboxApp> {
brightness: Brightness.dark, brightness: Brightness.dark,
), ),
useMaterial3: true, useMaterial3: true,
splashFactory: NoSplash.splashFactory,
), ),
routerConfig: router, routerConfig: router,
); );
+1
View File
@@ -57,6 +57,7 @@ class CrashScreen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return MaterialApp(
theme: ThemeData(splashFactory: NoSplash.splashFactory),
home: Scaffold( home: Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Something went wrong'), title: const Text('Something went wrong'),
+4 -1
View File
@@ -10,6 +10,9 @@
// CHAOS_ROUNDS (default: 30) — number of random operations to perform // CHAOS_ROUNDS (default: 30) — number of random operations to perform
// CHAOS_SEED (default: current epoch ms) — seed for reproducibility // CHAOS_SEED (default: current epoch ms) — seed for reproducibility
@Tags(['nightly'])
library;
import 'dart:io'; import 'dart:io';
import 'dart:math'; import 'dart:math';
@@ -132,7 +135,7 @@ void main() {
tearDown(() => db.close()); tearDown(() => db.close());
test('chaos monkey — random operations do not crash the repository', test('chaos monkey — random operations do not crash the repository',
() async { timeout: Timeout.none, () async {
final seedStr = _env('CHAOS_SEED'); final seedStr = _env('CHAOS_SEED');
final seed = seedStr.isEmpty final seed = seedStr.isEmpty
? DateTime.now().millisecondsSinceEpoch ? DateTime.now().millisecondsSinceEpoch
@@ -433,6 +433,7 @@ void main() {
final r = makeRepo(); final r = makeRepo();
await r.accounts.addAccount(account, userPass); await r.accounts.addAccount(account, userPass);
await r.emails.syncEmails('test', 'INBOX');
final results = await r.emails.searchEmails( final results = await r.emails.searchEmails(
'test', 'test',
+66 -4
View File
@@ -514,8 +514,7 @@ void main() {
), ),
); );
final results = final results = await r.emails.searchEmailsGlobal(null, 'urgent');
await r.emails.searchEmailsGlobal(null, 'urgent');
expect(results, hasLength(1)); expect(results, hasLength(1));
expect(results.first.subject, 'Weekly report'); expect(results.first.subject, 'Weekly report');
}); });
@@ -569,13 +568,76 @@ void main() {
), ),
); );
final results = final results = await r.emails.searchEmails('acc-1', 'INBOX', 'client');
await r.emails.searchEmails('acc-1', 'INBOX', 'client');
expect(results, hasLength(1)); expect(results, hasLength(1));
expect(results.first.subject, 'Project update'); expect(results.first.subject, 'Project update');
expect(results.first.mailboxPath, 'INBOX'); expect(results.first.mailboxPath, 'INBOX');
}); });
test('searchEmailsGlobal returns results sorted by receivedAt descending',
() async {
final r = _makeRepos();
await r.accounts.addAccount(_account, 'pw');
await r.db.into(r.db.emails).insert(
EmailsCompanion.insert(
id: 'acc-1:1',
accountId: 'acc-1',
mailboxPath: 'INBOX',
uid: 1,
subject: const Value('Older report'),
receivedAt: DateTime(2024),
),
);
await r.db.into(r.db.emails).insert(
EmailsCompanion.insert(
id: 'acc-1:2',
accountId: 'acc-1',
mailboxPath: 'INBOX',
uid: 2,
subject: const Value('Newer report'),
receivedAt: DateTime(2024, 6),
),
);
final results = await r.emails.searchEmailsGlobal(null, 'report');
expect(results, hasLength(2));
expect(results[0].subject, 'Newer report');
expect(results[1].subject, 'Older report');
});
test('searchEmails returns results sorted by receivedAt descending',
() async {
final r = _makeRepos();
await r.accounts.addAccount(_account, 'pw');
await r.db.into(r.db.emails).insert(
EmailsCompanion.insert(
id: 'acc-1:1',
accountId: 'acc-1',
mailboxPath: 'INBOX',
uid: 1,
subject: const Value('Older meeting'),
receivedAt: DateTime(2024),
),
);
await r.db.into(r.db.emails).insert(
EmailsCompanion.insert(
id: 'acc-1:2',
accountId: 'acc-1',
mailboxPath: 'INBOX',
uid: 2,
subject: const Value('Newer meeting'),
receivedAt: DateTime(2024, 6),
),
);
final results = await r.emails.searchEmails('acc-1', 'INBOX', 'meeting');
expect(results, hasLength(2));
expect(results[0].subject, 'Newer meeting');
expect(results[1].subject, 'Older meeting');
});
test( test(
'searchAddresses returns results sorted by most recently used', 'searchAddresses returns results sorted by most recently used',
() async { () async {
+4 -1
View File
@@ -50,7 +50,10 @@ Widget _buildScreen({List<Account> accounts = const []}) {
FakeAccountRepository(accounts), FakeAccountRepository(accounts),
), ),
], ],
child: const MaterialApp(home: AboutScreen()), child: MaterialApp(
theme: ThemeData(splashFactory: NoSplash.splashFactory),
home: const AboutScreen(),
),
); );
} }