Compare commits

...
Author SHA1 Message Date
Thomas SharedInboxandClaude Sonnet 4.6 e16bb72fb0 fix(ci): fix YAML parse errors in deploy, firebase-tests, website workflows
The multi-line python3 -c "..." blocks had unindented Python code that
terminated YAML block scalars, causing preExecutionErrors on every CI
run. Inline the Python as one-liners in all three workflow files.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 03:44:08 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 82385d70a5 fix(ci): fix YAML parse error in Print runner wait time step
The multi-line python3 -c "..." block had unindented Python code that
terminated the YAML block scalar at line 26, causing a preExecutionError
on every CI run since this step was added.

Inline the Python as a one-liner to stay within the YAML block's indentation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 03:06:55 +02:00
Thomas SharedInbox e07255800b Merge remote-tracking branch 'origin/main' into fix-ink-sparkle-remaining-tests 2026-06-07 02:58:44 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 84e454dd7b fix(tests): fix searchEmails FTS5 query, chaos monkey timeout, and format
_toFtsQuery was stripping non-word chars within tokens rather than
splitting on them — 'searchable-{timestamp}' became 'searchable{timestamp}*'
but FTS5 tokenizes on hyphens so the merged token never matched. Split on
non-word chars so each FTS token is queried separately.

chaos_monkey_test had no timeout annotation, hitting Dart's 30s default
for a test that needs ~60s with 30 SMTP/IMAP rounds. Added Timeout.none.

Also applied dart format to files from the merged main commit (#512) that
were committed without running the formatter.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 02:21:23 +02:00
Thomas SharedInbox bbce46d0f2 Merge remote-tracking branch 'origin/main' into fix-ink-sparkle-remaining-tests 2026-06-07 02:20:37 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 e73e4230fa fix(tests): suppress ink_sparkle shader crash in integration test
The pre-compiled ink_sparkle.frag shader built for an earlier SDK version
causes an INVALID_ARGUMENT crash when rendered via software (LIBGL_ALWAYS_
SOFTWARE=1) in the CI Dagger container. Any button tap in the integration
test triggers an ink effect, which loads the shader and crashes.

Adding NoSplash.splashFactory to both app themes prevents the shader from
being loaded. This matches what helpers.dart already does for widget tests
and what CrashScreen does for its own MaterialApp.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 01:44:50 +02:00
Thomas SharedInbox d442dd45ee Merge remote-tracking branch 'origin/main' into fix-ink-sparkle-remaining-tests 2026-06-07 01:44:43 +02:00
Thomas SharedInbox b454cf651e Merge branch 'main' into fix-ink-sparkle-remaining-tests 2026-06-07 00:30:33 +02:00
Thomas SharedInbox f6b3c2caaa Merge branch 'main' into fix-ink-sparkle-remaining-tests 2026-06-07 00:15:28 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 2bf4feadc8 fix(ci): forward SSH tunnel directly to dagger engine socket
Eliminates the socat bridge dependency by using OpenSSH's built-in
Unix socket forwarding (-L port:socket_path). The dagger user already
owns /run/dagger/engine.sock so no intermediate TCP listener is needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 23:41:13 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 a0071c86f8 fix(tests): suppress ink_sparkle shader crash in about/crash screen tests
CrashScreen creates its own MaterialApp, so the NoSplash fix in helpers.dart
(from #486) did not cover tests that exercise CrashScreen directly or via a
wrapper MaterialApp. The inner MaterialApp loaded InkSparkle on first tap,
crashing with "Unsupported runtime stages format version".

Fixes:
- Add splashFactory: NoSplash.splashFactory to CrashScreen's own MaterialApp
  (appropriate for an error screen — no ripple effects needed).
- Add splashFactory: NoSplash.splashFactory to the bare MaterialApp in
  about_screen_test.dart's _buildScreen helper.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 22:52:20 +02:00
10 changed files with 31 additions and 98 deletions
+1 -8
View File
@@ -22,14 +22,7 @@ jobs:
created_at=$(curl -sf \ created_at=$(curl -sf \
-H "Authorization: token $FORGEJO_TOKEN" \ -H "Authorization: token $FORGEJO_TOKEN" \
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/actions/tasks?limit=100" \ "${{ github.server_url }}/api/v1/repos/${{ github.repository }}/actions/tasks?limit=100" \
| python3 -c " | 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)
import sys, json
data = json.load(sys.stdin)
for r in data.get('workflow_runs', []):
if r.get('run_number') == $RUN_NUMBER:
print(r['created_at'])
break
" 2>/dev/null)
if [ -n "$created_at" ]; then if [ -n "$created_at" ]; then
queued_epoch=$(date -d "$created_at" +%s) queued_epoch=$(date -d "$created_at" +%s)
wait_seconds=$((runner_start - queued_epoch)) wait_seconds=$((runner_start - queued_epoch))
+5 -40
View File
@@ -24,14 +24,7 @@ jobs:
created_at=$(curl -sf \ created_at=$(curl -sf \
-H "Authorization: token $FORGEJO_TOKEN" \ -H "Authorization: token $FORGEJO_TOKEN" \
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/actions/tasks?limit=100" \ "${{ github.server_url }}/api/v1/repos/${{ github.repository }}/actions/tasks?limit=100" \
| python3 -c " | 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)
import sys, json
data = json.load(sys.stdin)
for r in data.get('workflow_runs', []):
if r.get('run_number') == $RUN_NUMBER:
print(r['created_at'])
break
" 2>/dev/null)
if [ -n "$created_at" ]; then if [ -n "$created_at" ]; then
queued_epoch=$(date -d "$created_at" +%s) queued_epoch=$(date -d "$created_at" +%s)
wait_seconds=$((runner_start - queued_epoch)) wait_seconds=$((runner_start - queued_epoch))
@@ -174,14 +167,7 @@ for r in data.get('workflow_runs', []):
created_at=$(curl -sf \ created_at=$(curl -sf \
-H "Authorization: token $FORGEJO_TOKEN" \ -H "Authorization: token $FORGEJO_TOKEN" \
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/actions/tasks?limit=100" \ "${{ github.server_url }}/api/v1/repos/${{ github.repository }}/actions/tasks?limit=100" \
| python3 -c " | 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)
import sys, json
data = json.load(sys.stdin)
for r in data.get('workflow_runs', []):
if r.get('run_number') == $RUN_NUMBER:
print(r['created_at'])
break
" 2>/dev/null)
if [ -n "$created_at" ]; then if [ -n "$created_at" ]; then
queued_epoch=$(date -d "$created_at" +%s) queued_epoch=$(date -d "$created_at" +%s)
wait_seconds=$((runner_start - queued_epoch)) wait_seconds=$((runner_start - queued_epoch))
@@ -232,14 +218,7 @@ for r in data.get('workflow_runs', []):
created_at=$(curl -sf \ created_at=$(curl -sf \
-H "Authorization: token $FORGEJO_TOKEN" \ -H "Authorization: token $FORGEJO_TOKEN" \
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/actions/tasks?limit=100" \ "${{ github.server_url }}/api/v1/repos/${{ github.repository }}/actions/tasks?limit=100" \
| python3 -c " | 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)
import sys, json
data = json.load(sys.stdin)
for r in data.get('workflow_runs', []):
if r.get('run_number') == $RUN_NUMBER:
print(r['created_at'])
break
" 2>/dev/null)
if [ -n "$created_at" ]; then if [ -n "$created_at" ]; then
queued_epoch=$(date -d "$created_at" +%s) queued_epoch=$(date -d "$created_at" +%s)
wait_seconds=$((runner_start - queued_epoch)) wait_seconds=$((runner_start - queued_epoch))
@@ -284,14 +263,7 @@ for r in data.get('workflow_runs', []):
created_at=$(curl -sf \ created_at=$(curl -sf \
-H "Authorization: token $FORGEJO_TOKEN" \ -H "Authorization: token $FORGEJO_TOKEN" \
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/actions/tasks?limit=100" \ "${{ github.server_url }}/api/v1/repos/${{ github.repository }}/actions/tasks?limit=100" \
| python3 -c " | 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)
import sys, json
data = json.load(sys.stdin)
for r in data.get('workflow_runs', []):
if r.get('run_number') == $RUN_NUMBER:
print(r['created_at'])
break
" 2>/dev/null)
if [ -n "$created_at" ]; then if [ -n "$created_at" ]; then
queued_epoch=$(date -d "$created_at" +%s) queued_epoch=$(date -d "$created_at" +%s)
wait_seconds=$((runner_start - queued_epoch)) wait_seconds=$((runner_start - queued_epoch))
@@ -341,14 +313,7 @@ for r in data.get('workflow_runs', []):
created_at=$(curl -sf \ created_at=$(curl -sf \
-H "Authorization: token $FORGEJO_TOKEN" \ -H "Authorization: token $FORGEJO_TOKEN" \
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/actions/tasks?limit=100" \ "${{ github.server_url }}/api/v1/repos/${{ github.repository }}/actions/tasks?limit=100" \
| python3 -c " | 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)
import sys, json
data = json.load(sys.stdin)
for r in data.get('workflow_runs', []):
if r.get('run_number') == $RUN_NUMBER:
print(r['created_at'])
break
" 2>/dev/null)
if [ -n "$created_at" ]; then if [ -n "$created_at" ]; then
queued_epoch=$(date -d "$created_at" +%s) queued_epoch=$(date -d "$created_at" +%s)
wait_seconds=$((runner_start - queued_epoch)) wait_seconds=$((runner_start - queued_epoch))
+2 -16
View File
@@ -23,14 +23,7 @@ jobs:
created_at=$(curl -sf \ created_at=$(curl -sf \
-H "Authorization: token $FORGEJO_TOKEN" \ -H "Authorization: token $FORGEJO_TOKEN" \
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/actions/tasks?limit=100" \ "${{ github.server_url }}/api/v1/repos/${{ github.repository }}/actions/tasks?limit=100" \
| python3 -c " | 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)
import sys, json
data = json.load(sys.stdin)
for r in data.get('workflow_runs', []):
if r.get('run_number') == $RUN_NUMBER:
print(r['created_at'])
break
" 2>/dev/null)
if [ -n "$created_at" ]; then if [ -n "$created_at" ]; then
queued_epoch=$(date -d "$created_at" +%s) queued_epoch=$(date -d "$created_at" +%s)
wait_seconds=$((runner_start - queued_epoch)) wait_seconds=$((runner_start - queued_epoch))
@@ -83,14 +76,7 @@ for r in data.get('workflow_runs', []):
created_at=$(curl -sf \ created_at=$(curl -sf \
-H "Authorization: token $FORGEJO_TOKEN" \ -H "Authorization: token $FORGEJO_TOKEN" \
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/actions/tasks?limit=100" \ "${{ github.server_url }}/api/v1/repos/${{ github.repository }}/actions/tasks?limit=100" \
| python3 -c " | 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)
import sys, json
data = json.load(sys.stdin)
for r in data.get('workflow_runs', []):
if r.get('run_number') == $RUN_NUMBER:
print(r['created_at'])
break
" 2>/dev/null)
if [ -n "$created_at" ]; then if [ -n "$created_at" ]; then
queued_epoch=$(date -d "$created_at" +%s) queued_epoch=$(date -d "$created_at" +%s)
wait_seconds=$((runner_start - queued_epoch)) wait_seconds=$((runner_start - queued_epoch))
+1 -8
View File
@@ -27,14 +27,7 @@ jobs:
created_at=$(curl -sf \ created_at=$(curl -sf \
-H "Authorization: token $FORGEJO_TOKEN" \ -H "Authorization: token $FORGEJO_TOKEN" \
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/actions/tasks?limit=100" \ "${{ github.server_url }}/api/v1/repos/${{ github.repository }}/actions/tasks?limit=100" \
| python3 -c " | 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)
import sys, json
data = json.load(sys.stdin)
for r in data.get('workflow_runs', []):
if r.get('run_number') == $RUN_NUMBER:
print(r['created_at'])
break
" 2>/dev/null)
if [ -n "$created_at" ]; then if [ -n "$created_at" ]; then
queued_epoch=$(date -d "$created_at" +%s) queued_epoch=$(date -d "$created_at" +%s)
wait_seconds=$((runner_start - queued_epoch)) wait_seconds=$((runner_start - queued_epoch))
@@ -2952,16 +2952,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 +2976,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 +2992,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 '';
@@ -3126,8 +3119,7 @@ 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>[];
+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'),
+1 -1
View File
@@ -132,7 +132,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
+2 -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,8 +568,7 @@ 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');
+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(),
),
); );
} }