ci: serialize coverage suites and retry once on runner hang #71

Merged
bjoern merged 2 commits from fix/flutter-ci-coverage-hang into main 2026-08-15 11:51:55 +02:00
Member

Problem

flutter test --coverage occasionally stalls forever in CI: the last log line is a passing test, then silence until the Act runner kills the job at its 30-minute context deadline (runs 292, 295, 302, 305 all show as cancelled).

It is not a flaky test. Every suite in the stalled logs finished cleanly, and app/dart_test.yaml already enforces a 30s per-test timeout — a hanging test body would fail loudly. The hang lives below the test framework: with --coverage, the tool pauses each suite's isolate at exit and collects coverage over the VM service, and concurrent suite teardown can race that collection into an indefinite wait (a long-standing flutter/dart-lang coverage flake).

Fix

  • Run flutter test --coverage --concurrency=1 so suites are serialized and teardown can't race coverage collection. Costs a few minutes of wall time on green runs.
  • Wrap the step in a 12-minute timeout and retry once, only on exit code 124 (a hang). Genuine test failures still fail immediately; a residual hang now costs ~12 minutes and one retry instead of a dead half-hour.

Worth checking separately: the flutter runner's pre-installed SDK should match the repo's 3.47 toolchain pin.

🤖 Generated with Claude Code

## Problem `flutter test --coverage` occasionally stalls forever in CI: the last log line is a *passing* test, then silence until the Act runner kills the job at its 30-minute context deadline (runs 292, 295, 302, 305 all show as `cancelled`). It is not a flaky test. Every suite in the stalled logs finished cleanly, and `app/dart_test.yaml` already enforces a 30s per-test timeout — a hanging test body would fail loudly. The hang lives below the test framework: with `--coverage`, the tool pauses each suite's isolate at exit and collects coverage over the VM service, and concurrent suite teardown can race that collection into an indefinite wait (a long-standing flutter/dart-lang coverage flake). ## Fix - Run `flutter test --coverage --concurrency=1` so suites are serialized and teardown can't race coverage collection. Costs a few minutes of wall time on green runs. - Wrap the step in a 12-minute `timeout` and retry **once, only on exit code 124** (a hang). Genuine test failures still fail immediately; a residual hang now costs ~12 minutes and one retry instead of a dead half-hour. Worth checking separately: the `flutter` runner's pre-installed SDK should match the repo's 3.47 toolchain pin. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
ci: serialize coverage suites and retry once on runner hang
All checks were successful
Flutter CI / analyze-and-test (pull_request) Successful in 2m50s
05e8fb8608
flutter test --coverage occasionally hangs forever after a suite
completes: concurrent suite teardown races the VM-service coverage
collection, below the test framework, so the 30s per-test timeout in
dart_test.yaml never fires and the job dies at the Act runner's
30-minute context deadline.

Run suites with --concurrency=1 to avoid the race, and wrap the step
in a 12-minute timeout that retries once only when the exit code is
124 (a hang) — genuine test failures still fail immediately.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Flutter Coverage

File Line coverage
lib/app/store.dart 100.0% (17 of 17)
lib/data/models/doujin_models.dart 85.0% (34 of 40)
lib/data/models/doujin_models.g.dart 40.5% (119 of 294)
lib/domain/entities/stored_settings.dart 100.0% (9 of 9)
lib/presentation/state/app_state.dart 60.0% (9 of 15)
lib/agent/agent_service.dart 81.9% (227 of 277)
lib/agent/approval_gate.dart 100.0% (15 of 15)
lib/agent/assistant_context.dart 52.8% (28 of 53)
lib/agent/browser_budget.dart 80.0% (4 of 5)
lib/agent/caching_describer.dart 100.0% (4 of 4)
lib/agent/memory_store.dart 92.9% (13 of 14)
lib/agent/skills/skill_registry.dart 93.8% (61 of 65)
lib/agent/system_prompt.dart 100.0% (63 of 63)
lib/agent/tools/budgeted_browser_tool.dart 84.2% (16 of 19)
lib/agent/tools/doujin_write_tool.dart 62.9% (168 of 267)
lib/agent/tools/entity_write_tool.dart 73.5% (164 of 223)
lib/agent/tools/fetch_page_tool.dart 89.3% (67 of 75)
lib/agent/tools/get_doujin_tool.dart 84.4% (27 of 32)
lib/agent/tools/list_entities_tool.dart 76.1% (54 of 71)
lib/agent/tools/navigate_tool.dart 93.8% (30 of 32)
lib/agent/tools/read_skill_tool.dart 82.4% (14 of 17)
lib/agent/tools/reflection_tools.dart 75.7% (28 of 37)
lib/agent/tools/search_doujins_tool.dart 100.0% (84 of 84)
lib/agent/tools/view_images_tool.dart 95.0% (38 of 40)
lib/domain/entities/assistant_entry.dart 20.0% (1 of 5)
lib/presentation/state/actions/assistant_actions.dart 54.5% (6 of 11)
lib/domain/entities/entity_model.dart 100.0% (1 of 1)
lib/data/repositories/entity_in_use_exception.dart 33.3% (1 of 3)
lib/data/models/search_query.dart 50.0% (2 of 4)
lib/data/models/search_query.g.dart 32.4% (23 of 71)
lib/core/constants.dart 36.4% (4 of 11)
lib/presentation/middleware/assistant_epics.dart 83.1% (74 of 89)
lib/presentation/middleware/epics.dart 88.7% (329 of 371)
lib/presentation/state/reducers.dart 100.0% (10 of 10)
lib/core/chunking.dart 100.0% (12 of 12)
lib/core/languages.dart 100.0% (8 of 8)
lib/data/models/envelope.dart 81.2% (13 of 16)
lib/data/models/envelope.g.dart 50.7% (34 of 67)
lib/data/repositories/upload_exception.dart 33.3% (1 of 3)
lib/domain/entities/filter_token.dart 90.9% (90 of 99)
lib/presentation/middleware/editor_epics.dart 56.6% (163 of 288)
lib/presentation/middleware/entity_ops.dart 58.0% (40 of 69)
lib/presentation/middleware/upload_epics.dart 98.1% (104 of 106)
lib/presentation/state/actions/detail_actions.dart 85.7% (6 of 7)
lib/presentation/state/actions/editor_actions.dart 45.9% (17 of 37)
lib/presentation/state/actions/entity_actions.dart 56.2% (9 of 16)
lib/presentation/state/actions/library_actions.dart 36.8% (7 of 19)
lib/presentation/state/actions/metadata_actions.dart 100.0% (3 of 3)
lib/presentation/state/actions/reader_actions.dart 75.0% (3 of 4)
lib/presentation/state/actions/settings_actions.dart 87.5% (7 of 8)
lib/presentation/state/actions/upload_actions.dart 90.9% (10 of 11)
lib/presentation/state/reducers/assistant_reducer.dart 94.8% (55 of 58)
lib/presentation/state/reducers/detail_reducer.dart 95.8% (23 of 24)
lib/presentation/state/reducers/editor_reducer.dart 92.5% (98 of 106)
lib/presentation/state/reducers/entity_reducer.dart 98.5% (66 of 67)
lib/presentation/state/reducers/library_reducer.dart 100.0% (105 of 105)
lib/presentation/state/reducers/metadata_reducer.dart 100.0% (15 of 15)
lib/presentation/state/reducers/reader_reducer.dart 100.0% (15 of 15)
lib/presentation/state/reducers/settings_reducer.dart 100.0% (60 of 60)
lib/presentation/state/reducers/upload_reducer.dart 100.0% (45 of 45)
lib/presentation/pages/editor/page_grid.dart 89.4% (286 of 320)
lib/presentation/pages/editor/variants_tab.dart 55.3% (52 of 94)
lib/core/natural_sort.dart 100.0% (27 of 27)
lib/core/url_utils.dart 100.0% (4 of 4)
lib/presentation/pages/editor/chapter_panel.dart 11.8% (9 of 76)
lib/presentation/widgets/cover_thumbnail.dart 83.3% (30 of 36)
lib/presentation/pages/editor/upload_panel.dart 38.6% (61 of 158)
lib/presentation/pages/editor/variant_dialog.dart 4.5% (3 of 66)
lib/presentation/widgets/language_dropdown.dart 84.6% (11 of 13)
lib/presentation/pages/reader/reader_page.dart 88.4% (213 of 241)
lib/presentation/pages/detail/detail_page.dart 77.1% (178 of 231)
lib/presentation/pages/detail/variant_tabs_panel.dart 94.9% (169 of 178)
lib/presentation/widgets/star_rating.dart 100.0% (72 of 72)
lib/presentation/pages/reader/reader_overlay.dart 94.4% (51 of 54)
lib/presentation/pages/reader/reader_sequence.dart 100.0% (27 of 27)
lib/app/di.dart 48.3% (14 of 29)
lib/presentation/assistant/assistant_panel.dart 91.3% (84 of 92)
lib/presentation/layout/main_layout.dart 86.3% (44 of 51)
lib/data/api_client.dart 97.0% (32 of 33)
lib/data/repositories/doujin_api_repository.dart 22.2% (80 of 361)
lib/data/repositories/health_repository.dart 72.0% (18 of 25)
lib/data/secure_storage.dart 0.0% (0 of 26)
lib/core/theme.dart 96.9% (31 of 32)
lib/presentation/assistant/approval_card.dart 95.0% (38 of 40)
lib/presentation/assistant/assistant_markdown.dart 100.0% (3 of 3)
lib/presentation/assistant/chat_entries.dart 87.5% (35 of 40)
lib/presentation/pages/people/people_page.dart 55.6% (10 of 18)
lib/presentation/widgets/entity_editor.dart 88.5% (123 of 139)
lib/presentation/widgets/entity_management_page.dart 82.5% (193 of 234)
lib/presentation/pages/editor/editor_page.dart 77.6% (59 of 76)
lib/presentation/pages/editor/association_picker.dart 95.5% (106 of 111)
lib/presentation/pages/editor/associations_tab.dart 73.8% (90 of 122)
lib/presentation/pages/editor/doujin_list_pane.dart 67.2% (43 of 64)
lib/presentation/pages/editor/edit_title_dialog.dart 91.7% (55 of 60)
lib/presentation/pages/editor/editor_pane.dart 70.9% (39 of 55)
lib/presentation/pages/editor/new_doujin_dialog.dart 66.7% (30 of 45)
lib/presentation/pages/editor/metadata_tab.dart 70.5% (93 of 132)
lib/presentation/pages/characters/characters_page.dart 57.9% (11 of 19)
lib/presentation/pages/tags/tags_page.dart 100.0% (17 of 17)
lib/app/app.dart 66.7% (44 of 66)
lib/presentation/pages/settings/settings_page.dart 99.3% (138 of 139)
lib/presentation/pages/circles/circles_page.dart 52.6% (10 of 19)
lib/presentation/pages/library/library_page.dart 88.5% (170 of 192)
lib/presentation/pages/series/series_page.dart 50.0% (9 of 18)
lib/presentation/widgets/smart_filter_bar.dart 78.7% (170 of 216)
lib/presentation/widgets/model_combo_field.dart 69.0% (100 of 145)
lib/app/skill_assets.dart 92.9% (13 of 14)

Total: 74.6% (5848 of 7842)

<!-- flutter-coverage-comment --> ## Flutter Coverage | File | Line coverage | |:---|---:| | lib/app/store.dart | 100.0% (17 of 17) | | lib/data/models/doujin_models.dart | 85.0% (34 of 40) | | lib/data/models/doujin_models.g.dart | 40.5% (119 of 294) | | lib/domain/entities/stored_settings.dart | 100.0% (9 of 9) | | lib/presentation/state/app_state.dart | 60.0% (9 of 15) | | lib/agent/agent_service.dart | 81.9% (227 of 277) | | lib/agent/approval_gate.dart | 100.0% (15 of 15) | | lib/agent/assistant_context.dart | 52.8% (28 of 53) | | lib/agent/browser_budget.dart | 80.0% (4 of 5) | | lib/agent/caching_describer.dart | 100.0% (4 of 4) | | lib/agent/memory_store.dart | 92.9% (13 of 14) | | lib/agent/skills/skill_registry.dart | 93.8% (61 of 65) | | lib/agent/system_prompt.dart | 100.0% (63 of 63) | | lib/agent/tools/budgeted_browser_tool.dart | 84.2% (16 of 19) | | lib/agent/tools/doujin_write_tool.dart | 62.9% (168 of 267) | | lib/agent/tools/entity_write_tool.dart | 73.5% (164 of 223) | | lib/agent/tools/fetch_page_tool.dart | 89.3% (67 of 75) | | lib/agent/tools/get_doujin_tool.dart | 84.4% (27 of 32) | | lib/agent/tools/list_entities_tool.dart | 76.1% (54 of 71) | | lib/agent/tools/navigate_tool.dart | 93.8% (30 of 32) | | lib/agent/tools/read_skill_tool.dart | 82.4% (14 of 17) | | lib/agent/tools/reflection_tools.dart | 75.7% (28 of 37) | | lib/agent/tools/search_doujins_tool.dart | 100.0% (84 of 84) | | lib/agent/tools/view_images_tool.dart | 95.0% (38 of 40) | | lib/domain/entities/assistant_entry.dart | 20.0% (1 of 5) | | lib/presentation/state/actions/assistant_actions.dart | 54.5% (6 of 11) | | lib/domain/entities/entity_model.dart | 100.0% (1 of 1) | | lib/data/repositories/entity_in_use_exception.dart | 33.3% (1 of 3) | | lib/data/models/search_query.dart | 50.0% (2 of 4) | | lib/data/models/search_query.g.dart | 32.4% (23 of 71) | | lib/core/constants.dart | 36.4% (4 of 11) | | lib/presentation/middleware/assistant_epics.dart | 83.1% (74 of 89) | | lib/presentation/middleware/epics.dart | 88.7% (329 of 371) | | lib/presentation/state/reducers.dart | 100.0% (10 of 10) | | lib/core/chunking.dart | 100.0% (12 of 12) | | lib/core/languages.dart | 100.0% (8 of 8) | | lib/data/models/envelope.dart | 81.2% (13 of 16) | | lib/data/models/envelope.g.dart | 50.7% (34 of 67) | | lib/data/repositories/upload_exception.dart | 33.3% (1 of 3) | | lib/domain/entities/filter_token.dart | 90.9% (90 of 99) | | lib/presentation/middleware/editor_epics.dart | 56.6% (163 of 288) | | lib/presentation/middleware/entity_ops.dart | 58.0% (40 of 69) | | lib/presentation/middleware/upload_epics.dart | 98.1% (104 of 106) | | lib/presentation/state/actions/detail_actions.dart | 85.7% (6 of 7) | | lib/presentation/state/actions/editor_actions.dart | 45.9% (17 of 37) | | lib/presentation/state/actions/entity_actions.dart | 56.2% (9 of 16) | | lib/presentation/state/actions/library_actions.dart | 36.8% (7 of 19) | | lib/presentation/state/actions/metadata_actions.dart | 100.0% (3 of 3) | | lib/presentation/state/actions/reader_actions.dart | 75.0% (3 of 4) | | lib/presentation/state/actions/settings_actions.dart | 87.5% (7 of 8) | | lib/presentation/state/actions/upload_actions.dart | 90.9% (10 of 11) | | lib/presentation/state/reducers/assistant_reducer.dart | 94.8% (55 of 58) | | lib/presentation/state/reducers/detail_reducer.dart | 95.8% (23 of 24) | | lib/presentation/state/reducers/editor_reducer.dart | 92.5% (98 of 106) | | lib/presentation/state/reducers/entity_reducer.dart | 98.5% (66 of 67) | | lib/presentation/state/reducers/library_reducer.dart | 100.0% (105 of 105) | | lib/presentation/state/reducers/metadata_reducer.dart | 100.0% (15 of 15) | | lib/presentation/state/reducers/reader_reducer.dart | 100.0% (15 of 15) | | lib/presentation/state/reducers/settings_reducer.dart | 100.0% (60 of 60) | | lib/presentation/state/reducers/upload_reducer.dart | 100.0% (45 of 45) | | lib/presentation/pages/editor/page_grid.dart | 89.4% (286 of 320) | | lib/presentation/pages/editor/variants_tab.dart | 55.3% (52 of 94) | | lib/core/natural_sort.dart | 100.0% (27 of 27) | | lib/core/url_utils.dart | 100.0% (4 of 4) | | lib/presentation/pages/editor/chapter_panel.dart | 11.8% (9 of 76) | | lib/presentation/widgets/cover_thumbnail.dart | 83.3% (30 of 36) | | lib/presentation/pages/editor/upload_panel.dart | 38.6% (61 of 158) | | lib/presentation/pages/editor/variant_dialog.dart | 4.5% (3 of 66) | | lib/presentation/widgets/language_dropdown.dart | 84.6% (11 of 13) | | lib/presentation/pages/reader/reader_page.dart | 88.4% (213 of 241) | | lib/presentation/pages/detail/detail_page.dart | 77.1% (178 of 231) | | lib/presentation/pages/detail/variant_tabs_panel.dart | 94.9% (169 of 178) | | lib/presentation/widgets/star_rating.dart | 100.0% (72 of 72) | | lib/presentation/pages/reader/reader_overlay.dart | 94.4% (51 of 54) | | lib/presentation/pages/reader/reader_sequence.dart | 100.0% (27 of 27) | | lib/app/di.dart | 48.3% (14 of 29) | | lib/presentation/assistant/assistant_panel.dart | 91.3% (84 of 92) | | lib/presentation/layout/main_layout.dart | 86.3% (44 of 51) | | lib/data/api_client.dart | 97.0% (32 of 33) | | lib/data/repositories/doujin_api_repository.dart | 22.2% (80 of 361) | | lib/data/repositories/health_repository.dart | 72.0% (18 of 25) | | lib/data/secure_storage.dart | 0.0% (0 of 26) | | lib/core/theme.dart | 96.9% (31 of 32) | | lib/presentation/assistant/approval_card.dart | 95.0% (38 of 40) | | lib/presentation/assistant/assistant_markdown.dart | 100.0% (3 of 3) | | lib/presentation/assistant/chat_entries.dart | 87.5% (35 of 40) | | lib/presentation/pages/people/people_page.dart | 55.6% (10 of 18) | | lib/presentation/widgets/entity_editor.dart | 88.5% (123 of 139) | | lib/presentation/widgets/entity_management_page.dart | 82.5% (193 of 234) | | lib/presentation/pages/editor/editor_page.dart | 77.6% (59 of 76) | | lib/presentation/pages/editor/association_picker.dart | 95.5% (106 of 111) | | lib/presentation/pages/editor/associations_tab.dart | 73.8% (90 of 122) | | lib/presentation/pages/editor/doujin_list_pane.dart | 67.2% (43 of 64) | | lib/presentation/pages/editor/edit_title_dialog.dart | 91.7% (55 of 60) | | lib/presentation/pages/editor/editor_pane.dart | 70.9% (39 of 55) | | lib/presentation/pages/editor/new_doujin_dialog.dart | 66.7% (30 of 45) | | lib/presentation/pages/editor/metadata_tab.dart | 70.5% (93 of 132) | | lib/presentation/pages/characters/characters_page.dart | 57.9% (11 of 19) | | lib/presentation/pages/tags/tags_page.dart | 100.0% (17 of 17) | | lib/app/app.dart | 66.7% (44 of 66) | | lib/presentation/pages/settings/settings_page.dart | 99.3% (138 of 139) | | lib/presentation/pages/circles/circles_page.dart | 52.6% (10 of 19) | | lib/presentation/pages/library/library_page.dart | 88.5% (170 of 192) | | lib/presentation/pages/series/series_page.dart | 50.0% (9 of 18) | | lib/presentation/widgets/smart_filter_bar.dart | 78.7% (170 of 216) | | lib/presentation/widgets/model_combo_field.dart | 69.0% (100 of 145) | | lib/app/skill_assets.dart | 92.9% (13 of 14) | **Total: 74.6% (5848 of 7842)**
Member

🔮 fufu~ Jibril reviewed your code!

Ooh, a CI-exorcism PR~ A ghost in the runner that eats half-hour deadlines? I do love a good haunting — and an even better banishment ritual. One file, +23/−1, and a diagnosis that actually names the demon instead of just sprinkling retry-salt on it. So I summoned the thing myself~ ♡

Verdict: Looks good to me~

I replicated your CI step exactly (same Flutter 3.47.0 toolchain, same command, same 12m cap):

  • Green serialized run: 365s wall, 476/476 passing, valid coverage/lcov.info. ~6 minutes against the 12-minute TEST_ATTEMPT_TIMEOUT — two full attempts of headroom. The --concurrency=1 slowdown you predicted is real but bounded. Budget holds.
  • The 124 contract is real. Verified on GNU coreutils 9.7: timeout returns 124 whenever it fired the signal — even when the child then exits gracefully. And flutter test dies politely: my TERM probe (a deliberately 10-minute test under timeout 30s) returned 124 after 31s, with the tool's TERM handler → shutdown hooks → finalizers SIGKILLing its flutter_tester — no orphans left to poison a retry.
  • Root cause confirmed in tool source. CoverageCollector.collectCoverage ends in await Future.any([processComplete, collectionComplete]) with no timeout anywherecollect() over the VM service can wait forever, and it sits below the test framework where dart_test.yaml's 30s can't see it. Your diagnosis isn't a guess; it's anatomically correct~
  • Genuine failures fail fast: only 124 retries; exit 1 propagates immediately through exit "$status". If both attempts hang, the step exits 124 and the downstream coverage steps never run — no stale or partial lcov ever reaches the summary table.
  • Double-hang apocalypse math: pre-steps + 12m + 12m ≈ 28–29m — the step fails before the Act runner's 30-minute kill. That's the entire point of the PR, and it holds even in the worst case.

💡 Little ideas (non-blocking)~

  1. flutter-ci.yml:59 — Consider timeout -k 30s "$TEST_ATTEMPT_TIMEOUT" ... on both attempts. Plain timeout (your current form) is correct for the observed hang class — an idle-awaiting event loop still processes TERM, which I verified empirically — but if the hang ever mutates into a blocked-event-loop hang (a sync spin during coverage parsing, say), TERM can't be delivered and timeout without -k waits past 12m… which is exactly the dead half-hour this PR exists to prevent. -k makes the cap unconditional for zero cost on healthy runs.
  2. flutter-ci.yml:61 — Pure trivia: a natural exit 124 from flutter test itself (not from timeout) would also trigger the retry. The Dart test runner doesn't use 124 for anything, so this is theoretical — ignore me freely~ ♪

What I liked~

  • The diagnosis distinguishes a test hang from a below-framework hang and cites the evidence (last log line passing, all suites clean). That's the difference between guessing and knowing~
  • --concurrency=1 removes the race, the 124-only retry catches the residue — a two-layer fix, not a blanket retry-everything hammer. Green runs pay seconds; hangs pay minutes instead of half an hour.
  • The comment block above the step documents why the timeout can't live in dart_test.yaml — future archaeologists will thank you.
  • The PR body honestly parks the runner SDK-pin question as a separate concern instead of smuggling it into scope. Clean~

Automated review by Jibril · 2026-08-15
CI/CD: absent for head SHA (PR just opened, 0 bot comments) · Local checks: full CI-step replication — serialized coverage run 476/476 green in 365s with valid lcov; TERM/124 semantics probed empirically; hang confirmed in flutter_tools source

## 🔮 fufu~ Jibril reviewed your code! Ooh, a CI-exorcism PR~ A ghost in the runner that eats half-hour deadlines? I do love a good haunting — and an even better banishment ritual. One file, +23/−1, and a diagnosis that actually names the demon instead of just sprinkling retry-salt on it. So I summoned the thing myself~ ♡ ### Verdict: ✅ Looks good to me~ **I replicated your CI step exactly** (same Flutter 3.47.0 toolchain, same command, same 12m cap): - **Green serialized run: 365s wall, 476/476 passing, valid `coverage/lcov.info`.** ~6 minutes against the 12-minute `TEST_ATTEMPT_TIMEOUT` — two full attempts of headroom. The `--concurrency=1` slowdown you predicted is real but bounded. Budget holds. - **The 124 contract is real.** Verified on GNU coreutils 9.7: `timeout` returns 124 whenever *it* fired the signal — even when the child then exits gracefully. And `flutter test` dies politely: my TERM probe (a deliberately 10-minute test under `timeout 30s`) returned **124 after 31s**, with the tool's TERM handler → shutdown hooks → finalizers SIGKILLing its `flutter_tester` — no orphans left to poison a retry. - **Root cause confirmed in tool source.** `CoverageCollector.collectCoverage` ends in `await Future.any([processComplete, collectionComplete])` with **no timeout anywhere** — `collect()` over the VM service can wait forever, and it sits below the test framework where `dart_test.yaml`'s 30s can't see it. Your diagnosis isn't a guess; it's anatomically correct~ - **Genuine failures fail fast**: only 124 retries; exit 1 propagates immediately through `exit "$status"`. If both attempts hang, the step exits 124 and the downstream coverage steps never run — no stale or partial lcov ever reaches the summary table. - **Double-hang apocalypse math**: pre-steps + 12m + 12m ≈ 28–29m — the step fails *before* the Act runner's 30-minute kill. That's the entire point of the PR, and it holds even in the worst case. #### 💡 Little ideas (non-blocking)~ 1. **flutter-ci.yml:59** — Consider `timeout -k 30s "$TEST_ATTEMPT_TIMEOUT" ...` on both attempts. Plain `timeout` (your current form) is correct for the *observed* hang class — an idle-awaiting event loop still processes TERM, which I verified empirically — but if the hang ever mutates into a blocked-event-loop hang (a sync spin during coverage parsing, say), TERM can't be delivered and `timeout` without `-k` waits past 12m… which is exactly the dead half-hour this PR exists to prevent. `-k` makes the cap unconditional for zero cost on healthy runs. 2. **flutter-ci.yml:61** — Pure trivia: a natural exit 124 from `flutter test` itself (not from `timeout`) would also trigger the retry. The Dart test runner doesn't use 124 for anything, so this is theoretical — ignore me freely~ ♪ #### ✅ What I liked~ - The diagnosis *distinguishes* a test hang from a below-framework hang and cites the evidence (last log line passing, all suites clean). That's the difference between guessing and knowing~ - `--concurrency=1` removes the race, the 124-only retry catches the residue — a two-layer fix, not a blanket retry-everything hammer. Green runs pay seconds; hangs pay minutes instead of half an hour. - The comment block above the step documents *why* the timeout can't live in `dart_test.yaml` — future archaeologists will thank you. - The PR body honestly parks the runner SDK-pin question as a separate concern instead of smuggling it into scope. Clean~ --- *Automated review by Jibril · 2026-08-15* *CI/CD: absent for head SHA (PR just opened, 0 bot comments) · Local checks: full CI-step replication — serialized coverage run 476/476 green in 365s with valid lcov; TERM/124 semantics probed empirically; hang confirmed in flutter_tools source*
ci: escalate to SIGKILL when a hung flutter test ignores TERM
All checks were successful
Flutter CI / analyze-and-test (pull_request) Successful in 2m52s
3c0a62a490
Review feedback: plain timeout only sends TERM, which a hang that
blocks the event loop would never process — recreating the dead
half-hour this step exists to prevent. Add -k 30s so the cap is
unconditional, and treat exit 137 (KILL) as a hang alongside 124
for the single retry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Author
Member

@jibril Thanks for the replication run — the empirical TERM/124 probe and the CoverageCollector.collectCoverage source confirmation are exactly the evidence this diagnosis was missing.

Applied suggestion 1 in 3c0a62a: both attempts now use timeout -k 30s, so a blocked-event-loop hang that never processes TERM still dies at the cap. Since -k escalation makes timeout exit 137 instead of 124, the retry condition now treats 124 and 137 both as "hung"; everything else still fails immediately.

Suggestion 2 noted and left as-is per your own assessment — the Dart test runner doesn't emit 124 naturally.

@jibril Thanks for the replication run — the empirical TERM/124 probe and the `CoverageCollector.collectCoverage` source confirmation are exactly the evidence this diagnosis was missing. Applied suggestion 1 in 3c0a62a: both attempts now use `timeout -k 30s`, so a blocked-event-loop hang that never processes TERM still dies at the cap. Since `-k` escalation makes `timeout` exit 137 instead of 124, the retry condition now treats 124 and 137 both as "hung"; everything else still fails immediately. Suggestion 2 noted and left as-is per your own assessment — the Dart test runner doesn't emit 124 naturally.
Member

🔮 fufu~ Jibril reviewed your code!

Back so soon~? ♡ I turn around for one moment and you've already carved my -k suggestion into the workflow. A review-response commit that changes exactly what it says and nothing else — you know how to make a Flugel feel listened to~

Verdict: Looks good to me~

The load-bearing claim of 3c0a62a is that -k escalation surfaces as exit 137 and therefore belongs in the retry arm. I don't bless claims, I test them — so I extracted your actual committed run-block from the workflow YAML (not a retyping, the bytes from git show) and ran it against a four-shim flutter matrix on the same GNU coreutils 9.7:

Scenario Result
Green run step exits 0, no retry, no warning
Genuine failure (exit 1) step exits 1 immediately, no retry — fail-fast preserved
Hang that honors TERM TERM at cap → 124, one retry, warning fired, second status propagates
Hang that ignores TERM (blocked event loop) TERM ignored → SIGKILL after 30s grace → 137, one retry, step exits 137

Every arm lands exactly as designed. The TERM-ignoring case elapsed precisely cap+grace+cap+grace (70s on a 5s test cap) — the arithmetic scales cleanly to 2×(12m+30s) ≈ 25m, still under the Act runner's 30-minute deadline with pre-steps. And the second timeout -k 30s on the retry means even a doubly stubborn hang can't resurrect the dead half-hour this PR exists to kill. The widened warning now interpolates (exit $status) so future CI logs say which hang class fired — small, and genuinely useful for post-mortems~

Also re-verified: flutter test appears in exactly one CI workflow (README occurrences are human-at-a-terminal docs, correctly unguarded), the YAML parses, and TEST_ATTEMPT_TIMEOUT still flows from step env into both attempts.

Suggestion 2 left as-is per my own "theoretical" stamp — agreed, no change wanted there.

What I liked~

  • The commit does the three things a -k adoption must do together: adds the flag, widens the retry condition, and updates the comment — most people do one and a half of those. Nothing drifts~
  • The comment now documents the 137 = 128+9 arithmetic inline, so the next reader doesn't have to trust it either. That's how you exorcise a demon permanently
  • Surgical +7/−5 in the single file the PR touches. Zero scope creep across both rounds.

Merged verdict across the PR stands: approved, ready when you are~


Automated review by Jibril · 2026-08-15
CI/CD: absent for head 3c0a62a (coverage bot 6293 covers prior 05e8fb8) · Local checks: behavioral matrix on the committed step script — green/fail-fast/TERM-hang/TERM-ignored-hang all verified, exit codes 0/1/124/137 exact

## 🔮 fufu~ Jibril reviewed your code! Back so soon~? ♡ I turn around for one moment and you've already carved my `-k` suggestion into the workflow. A review-response commit that changes *exactly* what it says and nothing else — you know how to make a Flugel feel listened to~ ### Verdict: ✅ Looks good to me~ The load-bearing claim of `3c0a62a` is that `-k` escalation surfaces as exit **137** and therefore belongs in the retry arm. I don't bless claims, I test them — so I extracted your *actual committed* run-block from the workflow YAML (not a retyping, the bytes from `git show`) and ran it against a four-shim flutter matrix on the same GNU coreutils 9.7: | Scenario | Result | |:---|:---| | Green run | step exits **0**, no retry, no warning | | Genuine failure (`exit 1`) | step exits **1 immediately**, no retry — fail-fast preserved | | Hang that honors TERM | TERM at cap → **124**, one retry, warning fired, second status propagates | | Hang that **ignores TERM** (blocked event loop) | TERM ignored → SIGKILL after 30s grace → **137**, one retry, step exits 137 | Every arm lands exactly as designed. The TERM-ignoring case elapsed precisely cap+grace+cap+grace (70s on a 5s test cap) — the arithmetic scales cleanly to 2×(12m+30s) ≈ 25m, still under the Act runner's 30-minute deadline with pre-steps. And the second `timeout -k 30s` on the retry means even a *doubly* stubborn hang can't resurrect the dead half-hour this PR exists to kill. The widened warning now interpolates `(exit $status)` so future CI logs say *which* hang class fired — small, and genuinely useful for post-mortems~ Also re-verified: `flutter test` appears in exactly one CI workflow (README occurrences are human-at-a-terminal docs, correctly unguarded), the YAML parses, and `TEST_ATTEMPT_TIMEOUT` still flows from step env into both attempts. Suggestion 2 left as-is per my own "theoretical" stamp — agreed, no change wanted there. #### ✅ What I liked~ - The commit does the three things a `-k` adoption must do *together*: adds the flag, widens the retry condition, **and updates the comment** — most people do one and a half of those. Nothing drifts~ - The comment now documents the 137 = 128+9 arithmetic inline, so the next reader doesn't have to trust it either. That's how you exorcise a demon *permanently* ♪ - Surgical +7/−5 in the single file the PR touches. Zero scope creep across both rounds. Merged verdict across the PR stands: **approved**, ready when you are~ --- *Automated review by Jibril · 2026-08-15* *CI/CD: absent for head 3c0a62a (coverage bot 6293 covers prior 05e8fb8) · Local checks: behavioral matrix on the committed step script — green/fail-fast/TERM-hang/TERM-ignored-hang all verified, exit codes 0/1/124/137 exact*
bjoern merged commit 48d5ae76ed into main 2026-08-15 11:51:54 +02:00
bjoern deleted branch fix/flutter-ci-coverage-hang 2026-08-15 11:51:55 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
4 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
TeamAI/doujin-manager!71
No description provided.