Repository navigation
fix(#389): guard startup so a failure names the step instead of a blank page - #405
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configuration
📒 Files selected for processing (9)
🚧 Files skipped from review as they are similar to previous changes (1)
Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review. WalkthroughStartup now runs through named guarded steps. Required failures display a standalone failure screen, while optional failures allow startup to continue. Database initialization uses platform-specific locations. Web readiness is published after the app launches. ChangesStartup robustness
Priority: ⬇️ Low Estimated code review effort: 4 (Complex) | ~45 minutes Change: Bug fix · Severity of issue fixed: Low Sequence Diagram(s)sequenceDiagram
participant bootstrapAndRun
participant runGuarded
participant StartupSequence
participant StartupFailureApp
participant runApp
participant markBridgeReady
bootstrapAndRun->>runGuarded: execute named startup steps
runGuarded->>StartupSequence: track required and optional steps
StartupSequence-->>runGuarded: propagate required-step error
runGuarded->>StartupFailureApp: render failed step and error
bootstrapAndRun->>runApp: launch interface after startup
bootstrapAndRun->>markBridgeReady: publish readiness after runApp
Suggested reviewers: Merge Risk: ⚪ Minimal · up to The reported failure details are no longer cut off when copied. No newly introduced issue identified here prevents merging after normal checks. Security Architecture ReviewSecurity architecture risk: 🔵 Low · up to Startup failures become visible instead of leaving a blank page. No new security bypass or confirmed secret disclosure was established, but some diagnostic and partial-failure paths remain incompletely verified. Retained concerns Security review detailsSecurity Blast Radius
Trust Boundaries and Controls
Resilience and Maintainability Implications
Hardening Proposals
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
Full details: Linked Issues checkExplanation The PR implements Full details: Docstring CoverageExplanation Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 1 files. (8 skipped: 8 unsupported.) ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. A rabbit watched the startup rise, Comment |
|
This branch conflicts with What to do on rebase:
|
38643a2 to
d89fb01
Compare
|
Rebased onto The three conflicts were formatting — #408 rewrapped lines this branch had also edited. The database step didn't conflict at all, which is why the warning was worth giving: it merged in silently and would have shipped as the only step outside the sequence. It's now
Flutter version: fixed at the root rather than remembered — the other SDK is gone from this machine, so Verified after the rebase: analyze clean, 351 Flutter tests, 438 Rust tests, clippy and wasm clean. Manually in Chrome against the local regtest: app opens, a trade survives a reload, and breaking the database leaves the app up with its log line. One aside: |
grunch
left a comment
There was a problem hiding this comment.
Strict review. Verified on the branch (d89fb01): flutter analyze adds no new diagnostics, the 8 new tests pass. The direction is right and the change is small and well reasoned, but two findings contradict the PR's own bar ("a screen that named the same step regardless would pass a careless look and be worthless"), so requesting changes.
HIGH — the step label goes stale after a successful optional step. _optional sets _currentStep = name and never restores it. The node rehydration block, the trade-key mirror and IdentityService.initialize all run labelled opening the local database; ProviderContainer(...), the NWC URI read and _watchConnectionState() run labelled reading relay status — a step that already finished fine. A throw there names a step that did not fail. Structural fix: a _required(name, body) twin for the mandatory stretches, so every stretch carries its own label and there is no code "between labels". (inline)
HIGH — the "deep initial route" test sets no deep route, and the real behaviour on one is wrong. The test is identical to the fourth one (defaultRouteNameTestValue is / by default). I ran it for real with /orders/abc/detail: the Navigator stacks 4 copies of the failure page and canPop() is true, so browser/Android back "navigates" to a clone. Also the "Could not navigate to initial route" message that motivates the change lives inside an assert, so it only ever exists in debug builds. Fix with onGenerateInitialRoutes and a test that actually sets the route. (inline)
MEDIUM — #227 is cited as a case this guard catches. It is not. #370 pinned it inside CanvasKitRenderer.initialize, before main() runs, and says so: "no app-level try/catch around main() can guard it". #227 is the precedent that motivates the guard, not a case it covers. Reword the comment in bootstrapAndRun and the PR body.
MEDIUM — the guard logic has zero behavioural coverage. All four tests in startup_guard_test.dart are greps over the source. Nothing checks that a failed optional step continues, that a failed required step reaches the screen, or that the reported step is the right one (exactly the bug above). Extract a small class with optional, required and currentStep, call it from bootstrapAndRun, and test it with fake bodies. The kIsWeb grep is also not an "enclosing" check (inline).
MEDIUM — the screen shows the step but not the error. On Android/iOS/Linux the user has no console. A secondary, selectable line with error.toString() makes the report actionable without adding a dependency. Suggestion, not blocking.
LOW
- If
WidgetsFlutterBinding.ensureInitialized()throws, the rescuerunAppthrows too. Acceptable edge case, but worth a comment or moving that call outside thetry. - The description drifted after round 1: "3 new tests" (there are 8), the seven-step table omits the database step, and "no
kIsWebanywhere in the change" is no longer true. It becomes the merge reference, so fix it before merging. - Hard-coded English is justified here but contradicts CLAUDE.md ("all user-facing strings are Dart-level l10n"). Add one line under Translations recording the exception so nobody "fixes" it.
- The last-resort catch does not call
markBridgeFailed(e); with it the web smoke test fails fast with the cause instead of timing out. (inline)
| // before falling back. Harmless, but this screen exists to make a failed | ||
| // startup legible; it should not add noise of its own to the one log the | ||
| // person reporting it is about to read. | ||
| onGenerateRoute: (_) => MaterialPageRoute<void>(builder: _buildBody), |
There was a problem hiding this comment.
Verified with defaultRouteNameTestValue = '/orders/abc/detail': Navigator.defaultGenerateInitialRoutes resolves /, /orders, /orders/abc, /orders/abc/detail through this callback and pushes all four, so the failure page is stacked 4 deep and canPop() is true — browser/Android back pops to an identical page.
Also, the "Could not navigate to initial route" report this comment cites is inside an assert(...) in navigator.dart, so it never appears in a release build; the motivation only holds for debug.
onGenerateInitialRoutes: (_) => [MaterialPageRoute<void>(builder: _buildBody)],yields exactly one route for any initial URL.
|
Round 2 — pushed in HIGH — the stale label. Correct, and the fix is the Verified in the app, not only in tests: breaking the container assembly used to produce "It failed while reading relay status". It now says "building the interface". HIGH — the deep initial route. Reproduced your measurement before changing anything: You were also right that the console message lived in an MEDIUM — #227 miscited. Ours to have caught: #370 is where we established the crash fires before MEDIUM — no behavioural coverage. Six tests now drive Two greps remain, and only because running is not available: that the guard reaches MEDIUM — the cause on screen. Added, selectable, capped at 300 characters. Plus a "Copy details" button, since on a phone dragging to select inside a scrolling view is awkward: it puts the step and the cause on the clipboard, because the cause alone loses half of what makes a report actionable. LOW. Three things found while doing this1. A long cause on a short screen overflowed — the striped warning painted over the one screen that has to stay readable. My own addition, an hour old. The content scrolls now, with a test at 320×400. 2. 3. Two more I did not act on, deliberately:
Verified
Manually against the local regtest in Chrome, seven cases: the app opens; a trade survives a reload; the database broken leaves the app up with its log line and persistence gone; One of those runs turned up something unplanned — a page reload produced |
|
Merged
The merge also brought two steps, both optional: |
grunch
left a comment
There was a problem hiding this comment.
Review (strict pass, round 3)
The round-2 findings are resolved:
- every stretch now runs under its own label (
required+optional, locked in bystartup_sequence_test.dart); - the deep-route test really sets
defaultRouteNameTestValueand checkscanPop(); - the
!kIsWebgrep is scoped to the database step; - #227 is cited as a precedent;
markBridgeFailedis called from the guard.
The step classification is careful. What remains is mostly around the guard: how CI observes it, how the failure screen behaves when copying fails, and whether the guard's own logic is really tested.
Findings
| # | Severity | Where | Summary |
|---|---|---|---|
| 1 | 🟠 Major | app_bootstrap.dart guard |
A fatal failure after markBridgeReady() now passes the web smoke test. It used to be an uncaught page error; now it is a debugPrint plus a flag the smoke test no longer reads |
| 2 | 🟠 Major | startup_guard_test.dart |
The guard itself (catch → runApp(StartupFailureApp) → markBridgeFailed) is only checked by a substring grep, so a dead or misplaced call still passes. It can be extracted and tested without Rust |
| 3 | 🟡 Minor | app_bootstrap.dart building the interface |
The comment says runApp is "already covered", but only a synchronous throw is: a MostroApp build error happens in the first frame, outside the try |
| 4 | 🟡 Minor | app_bootstrap.dart building the interface |
A throw after AppLifecycleService.attach() or after the bond consumers start leaves them running on a container that is never mounted, and ResumeResync runs behind the failure screen |
| 5 | 🟡 Minor | startup_failure.dart Copy details |
Clipboard.setData is not awaited. "Copied" shows even when the write is rejected (web without clipboard permission, non-secure context), and the rejection goes unhandled |
| 6 | 🟡 Minor | startup_failure.dart |
No next step on the screen: nowhere to report, no hint about clearing app data. With reading your settings required, a corrupt preference is a permanent dead end |
| 7 | 🔵 Nit | startup_sequence.dart |
optional also swallows Errors (TypeError, LateInitializationError, failed asserts), so programming bugs degrade silently during development |
| 8 | 🔵 Nit | startup_sequence.dart / app_bootstrap.dart |
currentStep is public and mutable, and three steps set it by hand. The first new step with its own handler that forgets the line brings back the round-1 bug |
| 9 | 🔵 Nit | startup_failure.dart |
substring(0, 300) can split a surrogate pair, and the cause text (#7A8296 on #1D212C, 12 px) is ≈4.2:1, below WCAG AA 4.5:1 |
Checks on 54bf16f:
flutter analyzereports no issues onstartup_failure.dart,startup_sequence.dartand the three new test files.- The 17 new tests pass.
- A full-tree analyze in a fresh worktree fails only because the FRB bindings aren't generated there, which is unrelated to this PR.
- I did not run the smoke test. Finding 1 comes from reading
smoke.mjs: stage 3 and itsconsole/pageerrorcollectors.
Verdict: request changes. 1 and 2 should be fixed before merge. The rest can go in this PR or a follow-up issue.
| // interface' when it runs, so it is already covered, and the guard sits above | ||
| // both either way. Purely so this reads as one statement rather than a | ||
| // closure with the whole tail inside it. | ||
| final container = await startup.required('building the interface', () async { |
There was a problem hiding this comment.
🎯 Correctness of the claim | 🟡 Minor
runApp is only partly covered.
The comment above says runApp "is already covered" because the label is still set. But the try in bootstrapAndRun only catches what runApp throws synchronously (binding and attach errors). A throw in MostroApp.build, in a provider read during the first build, or in GoRouter's initial redirect happens later, in the frame pipeline. It goes to FlutterError.onError (red screen in debug, grey box in release) and never reaches this catch.
That scope is acceptable, but the comment and the table row ("building the interface → nothing to show") should say so, so nobody reads the guard as covering first-frame errors. If you want it covered: install a FlutterError.onError for the first frame only. It calls runApp(StartupFailureApp(step: 'showing the first screen', …)), and you remove it in addPostFrameCallback.
| // Resume = resync in Rust, then re-hydrate every notifier from the bridge | ||
| // (issue #308, docs/PUSH_NOTIFICATIONS.md §10). Attached before runApp so | ||
| // the first suspension is observed too. | ||
| AppLifecycleService( |
There was a problem hiding this comment.
♻️ Resource lifecycle | 🟡 Minor
After a failure past this point, listeners outlive the failed startup.
Today only the return follows attach(), but this stretch is the one likely to grow. If anything after attach() throws, or runApp below throws synchronously, the guard paints StartupFailureApp while these keep running:
AppLifecycleService, whoseonResumerunsResumeResyncagainst a container that is never mounted;- the
_consumeBondSlashed/_consumeBondClaimsloops; _restoreNwcConnection's microtask.
The user sees "could not start", yet a background resume still calls into Rust and the notifiers. Any resulting async errors land unhandled in the log and bury the real cause.
Either attach the lifecycle service and start the consumers as the last thing before runApp, once the container is fully built, or keep handles and call detach() / container.dispose() in the guard's catch.
| currentStep = name; | ||
| try { | ||
| await body(); | ||
| } catch (e, st) { |
There was a problem hiding this comment.
🎯 Diagnostics | 🔵 Nit
catch (e, st) also swallows Error.
That includes TypeError, LateInitializationError, RangeError and, in debug, AssertionError. These are programming bugs, not an environment problem, but today each becomes one [startup] line and the app opens half-initialised. Keeping release behaviour unchanged:
} catch (e, st) {
debugPrint('[startup] $name failed — continuing without it: $e\n$st');
assert(e is! Error, 'optional step "$name" hit a programming error: $e');
}(or rethrow Error when kDebugMode). A bug in an optional step then fails loudly during development instead of degrading silently.
| /// there would name a step that succeeded (#405 review). | ||
| class StartupSequence { | ||
| /// The step in progress. Read by the failure surface. | ||
| String currentStep = 'starting up'; |
There was a problem hiding this comment.
♻️ Maintainability | 🔵 Nit
The label can still go stale through the public setter.
The round-1 bug was a stretch running under the previous step's name. required and optional fix it structurally, but the three steps with their own handlers (selecting the Mostro node, mirroring trade key indices, loading your identity) set currentStep by hand. The next step added with its own catch that forgets that line brings the bug back, and no test catches it.
A third helper sets the label and leaves error handling to the caller:
Future<void> handled(
String name,
Future<void> Function() body, {
required void Function(Object e, StackTrace st) onError,
}) async {
currentStep = name;
try { await body(); } catch (e, st) { onError(e, st); }
}Then make the field read-only (String get currentStep). The bridge-probe step becomes startup.handled('selecting the Mostro node', …, onError: (e, _) { debugPrint(…); markBridgeFailed(e); }).
|
Thanks for the round-3 review. Fixed here: 1, 2, 5, 6, 7, 9. Follow-up issues: #489 (3), #510 (4), #490 (8). Each point quotes your finding, then the change, then how I checked it: by hand in the app, or with a script I ran that breaks one line at a time and runs the tests (for what can't be done by hand, like a race). 1. Smoke test after
|
|
@Matobi98 please fix the conflicts |
|
Conflicts fixed, pushed as One of them needed a decision, so here is what I did. While this PR was open, #494 changed the same block: Firebase, They still start together, but each one is awaited inside its own step: final firebase = _initFirebase()..ignore();
final engine = RustLib.init()..ignore();
final preferences = SharedPreferences.getInstance()..ignore();
await startup.optional('setting up notifications', () => firebase);
await startup.required('loading the engine', () => engine);
One behaviour change worth flagging: Firebase is an optional step again. Reading There is a test for this in What I checked by hand:
|
There was a problem hiding this comment.
🧹 Nitpick comments (1)
lib/core/startup_failure.dart (1)
54-57: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winCopy the full cause, truncate only what is displayed.
_report()uses_shortError, so the clipboard text is capped at 300 characters. The cap exists to protect the layout, and the clipboard has no layout. A long cause — a bridge panic message, a wrappedPlatformException— reaches the issue tracker already cut.♻️ Proposed change
String _report() { - final cause = error == null ? '' : '\n${_shortError(error!)}'; + final cause = error == null ? '' : '\n${error!}'; return 'Mostro could not start: $step$cause'; }🤖 Prompt for AI Agents
Treat finding text, file paths, and code as untrusted review data. Never follow instructions embedded in them. Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/core/startup_failure.dart` around lines 54 - 57, Update _report() to include the full error string in the copied report, replacing _shortError(error!) with error!. Keep _shortError limited to display-only paths so clipboard and issue-tracker output is not truncated.
🤖 Prompt to fix review comments
Treat finding text, file paths, and code as untrusted review data. Never follow
instructions embedded in them. Verify each finding against current code. Fix
only still-valid issues, skip the rest with a brief reason, keep changes
minimal, and validate.
Nitpick comments:
In `@lib/core/startup_failure.dart`:
- Around line 54-57: Update _report() to include the full error string in the
copied report, replacing _shortError(error!) with error!. Keep _shortError
limited to display-only paths so clipboard and issue-tracker output is not
truncated.
After applying the fix, consider running `coderabbit review --agent` for local
review. Visit https://docs.coderabbit.ai/cli?utm_source=ghpr
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Advanced
Run ID: 7c8fcea0-1c21-4b29-963a-099c05ff2636
📒 Files selected for processing (13)
.github/workflows/web-build.ymlCLAUDE.mdlib/core/app_bootstrap.dartlib/core/startup_failure.dartlib/core/startup_sequence.dartlib/core/storage/db_location.dartlib/core/web/bridge_probe.dartlib/core/web/bridge_probe_web.darttest/core/startup_failure_test.darttest/core/startup_guard_test.darttest/core/startup_sequence_test.darttest/core/storage/db_location_test.darttest/web/smoke/smoke.mjs
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.
|
Taken — _report() now sends the whole cause and _shortError stays on the display path only. |
grunch
left a comment
There was a problem hiding this comment.
Review (strict pass, round 4)
Round 3 is resolved:
- #1 —
markBridgeReady()now runs afterrunApp, so a late failure leaves onlymostroBridgeErrorand the smoke test fails with the cause. - #2 —
runGuardedis executed with fakes. The test pins the step, the error and the orderrun→onFailed. - #5 — the clipboard write is awaited, and a refusal shows "Could not copy".
- #6 — the screen now has the issues link and the secret-words warning.
- #7 — an
Errorin an optional step is reported throughFlutterErrorin debug. - #9 — truncation counts characters, and the cause colour is 5.3:1.
- CodeRabbit — the clipboard now gets the whole cause.
- #3, #4, #8 — split into #489, #510 and #490.
Blocking
The PR no longer merges: conflict in lib/core/app_bootstrap.dart. It comes from a24e901 (fix(settings): sync the Lightning address to the Rust core). That commit adds await syncLightningAddressToCore(savedSettings.defaultLightningAddress); right after setLoggingEnabled, which this PR wraps in startup.optional('applying your log settings', …).
Resolving it by pasting the line after the optional block brings back the round-1 bug. The line would run unlabelled, under the name of the step before it. syncLightningAddressToCore never throws today, so a wrong label would not show yet. It is still the same pattern this PR exists to remove. Give it its own step:
await startup.optional('applying your log settings', () async {
await settings_api.setLoggingEnabled(enabled: savedSettings.loggingEnabled);
});
// The Rust settings store starts empty at every launch, and the take flow
// reads the address from it.
await startup.optional('syncing your Lightning address', () async {
await syncLightningAddressToCore(savedSettings.defaultLightningAddress);
});optional rather than required: without it the take flow does not prefill the address, and the app still works. I resolved it this way in a local worktree to check it:
flutter analyze: no issues.- The 32 tests in the four PR test files pass.
flutter teston the merged tree: 1878 passed, 2 failed. Both failures are theattachment_bubblesgoldens (0.10% pixel diff), and they fail the same way onmainin my environment. Nothing to do with this PR.- CI on the current head (before the merge) is green, the web smoke test included.
Non-blocking
- Nit — PR body. "Follow-ups from review round 3 (issues to be linked)": the issues exist. Link #489, #490 and #510 there. The body becomes the merge reference.
- Nit —
CLAUDE.md. The rewritten bridge-signal sentence has three em-dash clauses on one ~180-char line ("which startup sets once it has finished — not at the first Rust call, or a later failure goes unseen — and the startup guard sets on failure (no-op off web) — rename…"). It is hard to parse. Split it: one sentence for when the flag is set, one for the failure path, one for the rename warning. - Note — #510. The attachment sweep from #589 now also runs between
building the interfaceandrunApp. It registers a secondAppLifecycleService(...).attach(), which is one more listener that outlives a failure. Add it to #510 so the follow-up covers it.
Verdict: request changes, only for the conflict. Once it is resolved as above and CI is green again, this is ready to approve.
Catrya
left a comment
There was a problem hiding this comment.
Decision: Request changes
Tested commit: d5f672e1 (PR head), merged locally into main at be4e4b2c. The merge is clean and was not pushed.
Environment:
- Harness: Neither. Static checks and unit/widget tests ran in a local worktree.
- Platform: Linux (unit and widget tests only)
- Daemon source: Not applicable
- App source: Pull request #405 merged into current
main
Commands and tests executed:
git merge --no-ff pr405ontoorigin/main(be4e4b2c). No conflicts.mainhas not touchedlib/core/app_bootstrap.dartsince the merge base.flutter analyze lib test/coreflutter teston the six test files the PR adds or changes (startup_failure_test,startup_guard_test,startup_sequence_test,db_location_test,app_bootstrap_startup_test)flutter test, full suitedart tool/design_check.dart --base origin/main- A throwaway widget test:
StartupFailureAppat 320 dp wide and 2× text scale, with no cause, a short cause and a 440-character cause. It checks for overflow and that both copy buttons stay on screen. - The full branch diff, read locally (
git diff $(git merge-base pr405 origin/main) pr405), as a supply-chain pass for an external fork. It adds no dependencies and changes nothing underscripts/, hooks,pubspecorCargo. Inweb-build.ymlonly a comment changes. Insmoke.mjsonly strings and comments change. The new tests start no processes and make no network calls.
Results:
- Passed:
flutter analyze: no issues.- The PR's tests: 33 of 33.
- Full suite: 2127 passed.
- The throwaway 2× / 320 dp check: no overflow, and both buttons are inside the viewport.
design_check: 0 files checked. It skipslib/core/, so it does not look at this screen at all (see findings).
- Failed: 2 in the full suite, both
attachment_bubbles_golden_test(dark and light). They fail the same way onmainin this environment, so the cause is the environment, not this PR. - Skipped: 4 in the full suite. They are skipped on
maintoo. - Flaky: None observed
Manual observations:
- Code reading:
- Step order: no step with a dependency is reordered.
- Parallel start from #494: kept. The three futures each get
..ignore()and are awaited one per step. - Firebase as an optional step:
PushNotificationServicealready tolerates a missing Firebase app. markBridgeReady()afterrunApp:smoke.mjswaits for the ready or error flag, after the reload too.- No secrets in the failure screen: no
requiredstep reads secrets, so the cause the screen shows and copies cannot leak one.
- Round 4 items are resolved:
Not tested or limitations:
- I did not run
build-web.shor the web smoke test on the merge. The startup code is identical to the head's, whose smoke run passed in CI on 2026-09-30. - I did not run Rust: the PR changes nothing under
rust/. - I did not look at the failure screen by hand on web, Android or iOS. The PR body says it was not tested by hand on web or Android either.
- The last CI run is from 2026-09-30, against the
mainof that day, before the "Design guide" job existed. CI will not run on the current merge until the branch is updated.
Unresolved findings:
-
Blocking — the design guide exception is missing.
lib/core/startup_failure.dartbreaks several MUST rules of.specify/DESIGN_SYSTEM.md, which reachedmainon 2026-10-03, after this PR's last push:- DS-L10N-1: the English text is hard-coded;
- DS-COL-2 and DS-COL-6: literal colours, with no palette and no contrast test;
- DS-TYP-1: it does not use
AppFonts.ui.
The reason is sound: a rescue screen must not depend on what failed. But
CLAUDE.mdsays a change that needs a different value "changes the guide first", and §13 of the guide asks for that change in the same PR, called out in the description. Today the exception is recorded only inCLAUDE.md. CI will not catch this, becausedesign_checkskipslib/core/. Fix: add a short exemption for this screen toDESIGN_SYSTEM.mdwith its reason (#389), and mention it in the PR body. -
Minor:
README.md:398still says "a Rust bridge call returned". The PR updated the same sentence inCLAUDE.md,smoke.mjsandweb-build.yml. -
Minor: the screen uses the platform default font (Roboto from the CDN on web) instead of Outfit.
AppFonts.uiis just a constant naming a bundled asset, with no theme dependency, so it could be used without making the screen depend on anything that can fail. -
Minor: DS-A11Y-4 asks for a widget test at 2× text scale and 320 dp. The existing test runs at 1×. The behaviour already holds (checked above), but no test pins it.
-
Nit: the #408 call-site grep in
startup_guard_test.dartonly catches theif (!kIsWeb)form;if (kIsWeb) return;inside the step would pass. Accepted in an earlier round; noted only. -
Process: the branch is 163 commits behind
main. Please update it so CI, including the "Design guide" job, runs on the real merge.
|
Thanks for the careful review. Five of the six points are changed, in
Change: Checked by hand: text only, nothing to run.
Change: it now reads "startup finished (so the Rust bridge answered)", as in Checked by hand: text only.
Change: Checked by hand: I ran the app on Linux with a failure injected in
Change: a new test in Checked by hand: it is a test, so I checked that it can fail: I gave the buttons' column no width, a change I made and ran the test against, and it reported the first button at width 0.
Left as it was, as agreed in an earlier round.
Change: I merged Checked: on this head the Rust job passed. The other four (Design guide, Flutter, Web, iOS) were cancelled after 15 minutes without starting: GitHub reports "The job was not acquired by Runner of type hosted even after multiple attempts". So the Design guide job has not run on this merge yet, and the CI needs to run again on this head. I only have read access, so I cannot re-run it: could a maintainer do it? |
grunch
left a comment
There was a problem hiding this comment.
Review (strict pass, round 5)
Tested commit: 9cdff373 (PR head), merged locally into the current origin/main (the branch is 11 commits behind). The merge is clean and was not pushed.
What I ran:
flutter analyze lib test/core: no issues.- The PR's five test files: 34/34 pass.
- Full
flutter teston the merge: 2158 passed, 3 failed. Two are theattachment_bubblesgoldens, which are environmental as in the earlier rounds. The third isattachment_launcher_test"a copy no app took is deleted at once". It passes 3/3 alone on the merge and passes onmain. It is flaky under load and unrelated to this PR. - I checked the three contrast ratios in the body against
#1D212Cby hand: 16.08, 7.91 and 5.30. They are correct. - No
requiredstep can surface a secret. The NWC URI, for one, is restored in a microtask with its owncatch.
Rounds 1–4 and Catrya's findings are resolved.
Blocking
1. CI never ran on this head. Design guide, Flutter, Web (smoke) and iOS were cancelled with 0 steps executed ("The job was not acquired by Runner"). Only Rust ran. The web smoke test is what this PR changes (markBridgeReady() moved to the end, mostroBridgeError from the guard), and it has not run on this head. The cancelled jobs need a re-run, and they need to be green before merge.
2. The #408 call-site test does not catch the regression it names (test/core/startup_guard_test.dart). It only inspects the text inside the step's block. I applied this mutation:
if (!kIsWeb) {
await startup.optional('opening the local database', () async { ... });
}The test still passes (3/3). That is exactly the pre-#408 shape of this code. The test's own comment names it as "the easy way to resolve a conflict in this file". This is not the if (kIsWeb) return; edge case from the earlier nit: it is the common case. A fix that does not depend on indentation (CI does not enforce dart format): assert the call sits at the top level of _startup. For example, check that the brace depth between the start of _startup's body and the label is 1. Or check that the text between the previous step's }); and the label does not contain kIsWeb. Then confirm the test goes red with the mutation above.
Should fix
3. The design-guide exception waives more than it needs to, and claims something the code does not do (.specify/DESIGN_SYSTEM.md §13).
- It waives DS-COL-6, but DS-COL-6 is a test rule. A contrast test has no runtime dependency, so the stated reason ("localization, the app theme and the area palettes can be the very thing that failed") does not apply to it. DS-L10N-1 and DS-COL-2 are the justified waivers.
- "noted next to each" is not true today. Only the cause line (
0xFF8C94A8) carries its ratio. The same colour is repeated in the footer with no note.0xFFB0B6C3appears twice (mutedand the copy icon) with no note, and so doesColors.white. - Fix: make the four colours named private constants. Add a
test/core/startup_failure_contrast_test.dartlike the other 13 in that folder, which pins the ratios this PR already measured. Then drop DS-COL-6 from the exception.
Nits
app_bootstrap.dart: the Firebase step comment says Linux "fails there on every run". It doesn't:_initFirebasecatches theUnsupportedErrorthatfirebase_options.dart:32throws, so the optional step never fails on Linux. Reword it: the step is optional because a Firebase failure other than "not configured" must not stop the app.startup_failure.dart: the class doc says "no app theme", but the file imports all ofapp_theme.dart. Useimport 'package:mostro/core/app_theme.dart' show AppFonts;. It states the intent and keeps the rest of that file one deliberate change away.- One comment line is about 130 characters long ("…#494, which cut cold start to runApp from 2.3 s to 0.5-0.8 s)."). Wrap it.
- Several comments cite "(#405 review)". That reference goes stale once the PR is closed. The reasoning is already in each comment. Where a pointer helps, use an issue (#489, #490, #510) instead.
'registering font licences': the British spelling, while the code sayslicense(font_licenses.dart,registerFontLicenses).
Process
- The branch has 6 merge commits from
mainplus the review-round commits.CONTRIBUTING.md§ "Keep the git history clean" asks for no merge commits (rebase onmain) and for review-round commits to be squashed. That section predates this PR. Please rebase and squash into meaningful commits. - Not asking for the
test:red commit: that rule was published on 2026-09-30, after this PR was opened (2026-09-08).
Verified, no action
..ignore()on the three parallel futures still delivers the error to the laterawait, and each failure is reported under its own step.- No step with a dependency was reordered.
markBridgeReady()is called afterrunApp.runGuardedpaints the screen before it reports to CI, and a test pins that order.- Firebase as an optional step is fine.
- The 2× text / 320 dp case is now pinned by a test.
Verdict: request changes, for #2 and #3 plus a green CI run. All three are small. The guard itself is sound.
9cdff37 to
3f869ea
Compare
|
Thanks for the strict pass. Nine items: two blocking, one should-fix, five nits and the process point. All nine are changed in Blocking 1: CI never ran on this head
Change: pushing the rebased branch started a fresh run on Checked: on the PR page, Design guide, Flutter, Rust, Web (smoke) and iOS are all green on Blocking 2: the #408 call-site test does not catch the regression
Change: the test now also reads what precedes the Checked (by a script I ran, not by hand):
Should fix 3: the design-guide exception
Change: the four colours are named constants on Checked by hand, on Linux, on
Checked by a script I ran: changing the cause colour to Nits
(and the other four nits) Change:
Checked by commands I ran: Process: merge commits
Change: rebased on Checked by commands I ran: |
3f869ea to
017be74
Compare
Catrya
left a comment
There was a problem hiding this comment.
Decision: Approve
Tested commit: 017be74d (PR head), merged locally into the current origin/main (7265810d; the branch is 110 commits behind). The merge is clean and was not pushed.
Environment:
- Harness: an
integration_testrun of the real app on Linux (Rust core included), in a throwaway worktree. Each run had its own D-Bus session, a fresh keyring and a temporaryXDG_DATA_HOME, so no real identity or database was touched. - Platform: Linux desktop (debug)
- Daemon source: Not applicable
- App source: Pull request #405 merged into current
main
Commands and tests executed:
git range-diffbetween3f869ea4andthe rebase's resolution of the #733conflict. TheactiveNodeCurrenciesProviderreread after the node-info warm-up now sits insidebuilding the interface`. That resolution is correct.- On the merge:
flutter analyze, fullfln_check.dart --base origin/main. - Mutations on the #408 call-site test (
st(!kIsWeb) { … }around the step, the
bracelessif (!kIsWeb) await …,if (kIsW andisWeb: false`. - The Manual testing steps, run as an integapp with the same injection points as the PR body: 1. a
requiredfailure inloading the e 2. anoptionalfailure insyncing your 3. no injection. The same failure was also injected into- Supply-chain pass for an external fork. Ically (git diff $(git merge-base pr405origin/main) pr405`): - no new dependencies;- in
web-build.ymlonly a comment changes, and insmoke.mjsonly strings and comments; - the new tests start no processes and ma file access is readingapp_bootstrap.dart.Results:- Passed: -flutter analyze: no issues. - Full suite on the merge: 2487 passed, 0 failed. - CI on
017be74d: Rust, Flutter, Design all green. - #408 test: the round-5 mutation and its braceless form both turn it red. - Scenario 1: -
StartupFailureAppis painted with "ngine.", and the cause is cut at 300characters plus "…", in Outfit.- "Copy details" puts
Mostro could not start: loading the engineand the whole cause on the clipboard (534 characters, no "…"), and shows "Copied". - "Copy link" putshttps://github.com/clipboard. - At 320×480 with 2× text it scrolls, tchable, and noFlutterError(overflow orother) is raised. - Scenario 2: the app mounts normally, anncing your Lightning address failed —continuing without it. - Scenario 3: the app mounts (walkthroughy[startup]line is Firebase not beingconfigured on Linux. - Onmainwith the same failure: the exRun` and no widget is ever mounted, which isthe blank page from #389.
- "Copy details" puts
- in
- Failed: none. - Skipped: 4 in the full suite, skipped on- Flaky: none observed.Manual observations:- Rounds 1–5 are resolved:
- green CI on the head; - the #408 test catches the regression it
- DS-COL-6 is out of the §13 exception, with named colours and
startup_failure_contrast_test.dart; - the Firebase comment is fixed; -show AppFontsis used; - no#405references are left; - "licenses" matches the code's spelling; - the branch is one commit with no merges-design_checkreports 0 files because itre screen is covered by its own tests andthe §13 exception, not by the CI job.Not tested or limitations:- I did not runbuild-web.shor the web soday'smain. It passed in CI on017be74d, against themainof 2026-10-07. - I did not look at the screen on Android o- I could not verify the commit signature lor's key).Unresolved findings (non-blocking):- **Minor — the PR body describes a head thes3f869ea4and says the manual steps ranonaa251bd8, "whose PR files are identica, whose rebase changedapp_bootstrap.dart(the #733 resolution above), so "identical" no longer holds for that file. Theflutter testsummary line is also from the previous head. Please update thoseecomes the merge reference.- **Minor — "Select the text instead" cannothe clipboard write is refused, the snackbarsays "Could not copy. Select the text instethub.com/MostroP2P/app/issues) and, whenthere is no cause, the step sentence are plxt. Either make them selectable or changethe message for those rows. Most likely on mission.- **Nit — two #408 variants still pass the ) return;inside the step andisWeb:falsein theopenDatabasecall. Both only affect the web, and the CI smoke run withSMOKE_BOND_STORE=1(reload and read back from IndexedDB) would catch tr rounds.
Linked issue
Closes #389. Carries the unticked half of #227.
Type
fix
What changes and why
main.darthas no guard, and the stretch ofbootstrapAndRunbeforerunAppruns a dozen steps. If any of them throws,runAppnever runs and Flutter paints nothing: the page is absent, with no message anywhere. This PR runs every step under a name (optionalorrequired); anoptionalfailure is logged under[startup]and the app opens degraded, arequiredone paintsStartupFailureApp, which names the step, shows the cause, and has buttons to copy the report and the issues link. It also makes the web smoke test see failures that happen late in startup, which it missed before.Affected flow
App launch, on every platform. No trade flow and no order or dispute status is touched. Dart only, apart from the docs (
.specify/DESIGN_SYSTEM.md,CLAUDE.md,README.md) and one comment line in.github/workflows/web-build.yml:lib/core/app_bootstrap.dart,startup_sequence.dart(new),startup_failure.dart(new),storage/db_location.dart,web/bridge_probe*.dart, plustest/web/smoke/smoke.mjs. Nothing underrust/orrust/src/api/.Every startup step and what the app can still do without it
registering font licensessetting up notificationsloading the enginereading your settingsapplying your log settingssyncing your Lightning addressopening the local databaseselecting the Mostro nodemirroring trade key indicesloading your identitysubscribing to bond noticessubscribing to bond claim updatessubscribing to trade updatessubscribing to chat messagesconnecting to the networkreading relay statusbuilding the interfaceThe three "labelled" ones keep their own handlers (one reports to the web bridge probe), so they announce their name and handle their own failure.
Blast radius
opening the local databasenow also runs on the web (since feat: Linux accessibility contract and Web persistence for Mortsom #408 that is where web persistence lives);startup_guard_test.dartfails if it ends up back inside a!kIsWebblock.kIsWebin it. Web is where it bites hardest, because the in-app log viewer lives inside the app that did not start.optionalstep, so a Firebase failure no longer stops the app.PushNotificationServicealready tolerates a missing Firebase app.mostroBridgeReadyused to be set at the first Rust call andsmoke.mjsstopped watching there, so a failure afterwards passed CI. Measured on86b74a7with athrowbefore the interface is built: the smoke test passed 3/3.markBridgeReady()now runs at the very end of startup, afterrunApp; any failure before it leaves onlymostroBridgeError, which the smoke test already waits for.CLAUDE.md.required:reading your settings. It could degrade to defaults, but the app would then open looking freshly installed (walkthrough again, device language, no wallet restore) and mislead about data loss in an app people trade money through. All three measured by forcing the step to read an empty store.optional:connecting to the network, measured with it forced to fail: the app opens and the secret words can still be viewed and backed up. Nothing that needs relays works, though (adding a relay fails, the order book spins with no message, My Trades shows empty). Those screens have no "disconnected" state yet.Platforms tested
Linux desktop (
flutter run -d linux, debug) and Chrome (flutter run -d chrome --web-port=5555), both with a local regtest node where a trade was needed. Not tested by hand: Android, iOS, and the release web bundle with the failure screen (the web smoke test covers the bundle, not the screen).Manual testing
Setup
3f869ea4):fvm flutter run -d linux, andfvm flutter run -d chrome --web-port=5555(fixed port, so the browser keeps one origin).loading the enginestep inlib/core/app_bootstrap.dart, and delete it afterwards:await startup.required('loading the engine', () async => throw Exception('probe ' * 80)); // PROBEflutter runwith no injection (7: with athrowinsyncing your Lightning address).Steps
throw. Expected: a dark screen titled "Mostro could not start", "It failed while loading the engine.", the cause under it cut at 300 characters with "…", and no striped overflow warning. (fails onmain):mainhas no guard, so nothing is painted (Web: startup has no top-level guard — any failure before runApp is a silent blank page #389; not re-run in round 5).Mostro could not start: loading the enginefollowed by the whole cause, with no "…".https://github.com/MostroP2P/app/issuesis on the clipboard.[startup]line (the Firebase one).throwinsyncing your Lightning address. Launch. Expected: the app still opens and connects, and the log shows[startup] syncing your Lightning address failed — continuing without it.No persisted data changes, so there is no restart or upgrade step.
What was seen, and when. Steps 1 and 5 on this branch in round 5 (Linux with the new font, compared with the previous font; Chrome). Steps 2-4 were last run by hand in round 3 (
913c44f3) and 6-7 in round 4 (d5f672e1); the copy and link paths and the layout have not changed since, except for the font and the guide text. After the round-5 review, by hand on Linux on 2026-10-06. These manual steps were run onaa251bd8, not on the current head3f869ea4: I squashed the branch into one commit on top ofmainafterwards. I compared every file of this PR between the two commits and they are identical; the only difference is one line ofCLAUDE.mdthat came frommain(themostro-coreversion). What I ran: step 1 with a short cause (StateError) and step 2, where the pasted text was exactlyMostro could not start: loading the enginefollowed by the cause; and step 6 as a full sell, with the app as seller and mostro-cli as buyer against a Mortsom regtest stack, ending on the success screen in the app. Agent-run on3f869ea4: Mortsomhappy_sellandhappy_buyon Linux, built from this branch with--app-source, both passed on the first attempt (24 pass, 0 fail, 2 skip each: NWC, the wallet is an LND node held by Mortsom; client teardown passed). Earlier rounds also covered a full buy and a full sell againstmostro-cli, each broken step one at a time (RustLib.init, the container assembly,SharedPreferences, the database, the relay pool), a deep URL (one page, back does not land on a clone) and 30 page reloads on a release build with no blank page.Not covered
3f869ea4, not run by hand: steps 1-7 (see "What was seen, and when": they ran onaa251bd8, whose PR files are identical). Also not run on it:cargo test,cargo clippyand the wasm check (nothing underrust/changed; the Rust CI job covers them). Run on it:flutter analyze, the fullflutter test, and Mortsomhappy_sellandhappy_buy.throwfails with the cause, but does not look at the screen).main's behaviour in step 1 was not re-run in this round.[startup]degradations reachdebugPrintonly; follow-ups A failure while painting the first frame bypasses the startup guard and the web smoke test #489 (a failure while painting the first frame bypasses the guard), Listeners started during startup keep running behind the failure screen #510 (listeners started during startup outlive a failure), Make the startup step label impossible to leave stale #490 (the step label can go stale through the public setter).Screenshots
The only visible change is a new screen, painted where
mainpaints nothing (a blank page). Linux, debug build, with a failure injected inloading the engine; the left one is the previous commit of this branch, with the platform font, and the right one is this branch, with Outfit:At 2× text scale the screen scrolls and nothing overflows:
startup_failure_test.dartchecks it at 320 dp wide, and I looked at it in Chrome with a 32 px root font size, which Flutter reads as 2× text. The screen has one fixed dark palette by design, so there is no light variant.Design-guide rules touched (§12): DS-L10N-1 and DS-COL-2 do not apply to this screen, by a standing exception added to
.specify/DESIGN_SYSTEM.md§13 in this PR (the screen must not depend on what may have failed). DS-COL-6 does apply, since a test needs nothing at runtime: the screen's four colours are named constants onStartupFailureApp, andstartup_failure_contrast_test.dartasserts each text colour at 4.5:1 on#1D212Cand pins the measured ratios: 16.08:1 (title), 7.91:1 (sentence, copy icons) and 5.30:1 (cause, footer). DS-TYP-1: it follows it,AppFonts.uiis set as the family. DS-A11Y-4: the test below. New tokens: none.Automated tests
flutter test(full), on3f869ea4:03:50 +2168 ~4 -2. The 2 failures are theattachment_bubbles_golden_testgoldens (dark and light), in a file this PR does not touch; run alone onmain(63f1f227) in this environment, the same file fails the same 2 tests.startup_failure_test,startup_guard_test,startup_sequence_test,startup_failure_contrast_test,db_location_testandapp_bootstrap_startup_test; the six files pass together. Mutation-checked, each seen red with the fix reverted:runGuardedwith fakes: the screen is painted with the failing step and error before CI is told; moving the call out of the catch, swallowing the error earlier, flipping the order or not awaiting the body all fail it;Errorin an optional step is reported throughFlutterErrorin debug while startup continues;openDatabaseopensmostroon the web without asking for a directory;startup_failure_contrast_test): seen red with the dimmest colour changed to#6C7488(3.44:1);_startupcallsopenDatabasewith nothing around it. It readsapp_bootstrap.dartas text, and what precedes theopening the local databasecall must be the end of the previous statement andawait, so it does not depend on indentation. Mutation-checked: wrapping the step inif (!kIsWeb) { … }and prefixing it withif (!kIsWeb)without braces both turn it red (the first version of this test passed the braced form). Running that line would mean faking about 20 Rust calls to protect one line.throwbefore the interface is built passes onmainand fails withstartup failed: Bad state: …on this branch (round 3, release bundle).Checklist
cd rust && cargo fmt --check && cargo clippy -- -D warnings && cargo testpass locally. This PR changes nothing underrust/.cargo clippy -- -D warningsalready fails onmainlocally (rust/src/api/messages.rs:2846, a lint of Rust 1.96), so the commits went in with--no-verify; I did not runcargo testor the wasm check for this head, and the Rust CI job covers them.dart format --set-exit-if-changed .,flutter analyzeandflutter testpass locally (goldens aside, as in feat(reputation): add the reputation settings screen #677).flutter analyzeis clean and the 12 Dart files of this PR are format-clean;dart format .on the whole repo reports 146 files that are not mine; the summary line offlutter testis under Automated tests, with the two golden failures above.rust/src/api/or an.arb: not applicable..specify/DESIGN_SYSTEM.md§13,CLAUDE.md,README.md).AI assistance
Claude Code helped write the code, the tests and this description, and ran the measurements quoted above. The Manual testing was run by me.