Skip to content

workout export fix, luminance dedupe, remove stray local test files - #312

Closed
abdulsaheel wants to merge 1 commit into
audit/fixes-round2-edgefrom
audit/fixes-round3-edge
Closed

workout export fix, luminance dedupe, remove stray local test files#312
abdulsaheel wants to merge 1 commit into
audit/fixes-round2-edgefrom
audit/fixes-round3-edge

Conversation

@abdulsaheel

@abdulsaheel abdulsaheel commented Aug 29, 2026

Copy link
Copy Markdown
Collaborator

User description

another audit pass fixes:

  • a workout row still marked live past the resume ceiling was getting a made-up end time and then exported to Health as if it were a real completed workout. stopped exporting that branch, added a test.
  • P.luminance() in theme.dart reimplemented Color.computeLuminance(), same WCAG formula. now calls the built-in.
  • removed three stray local json files at the repo root that shouldn't have been committed and gitignored the pattern.

tests: flutter test on touched files, flutter analyze clean.

Summary by Sourcery

Prevent fabricated workout durations from reaching Health exports while deduplicating luminance calculations and cleaning up stray local files.

Bug Fixes:

  • Prevent stale orphaned workouts with fabricated end times from being exported to Health as completed workouts.

Enhancements:

  • Use Flutter's built-in color luminance calculation for contrast evaluation instead of maintaining a duplicate implementation.

Documentation:

  • Update UI theme documentation to reflect the removal of the public luminance helper.

Tests:

  • Add regression coverage verifying stale workouts are finalized locally but never sent to Health.

Chores:

  • Remove stray root-level payload JSON files and ignore the corresponding file pattern.

PR Type

Bug fix, Tests, Enhancement


Description

  • Stop exporting stale workouts to Health.

  • Replace custom luminance with built-in Flutter method.

  • Add test for stale workout export prevention.

  • Remove leaked local JSON payload files.


Diagram Walkthrough

flowchart LR
  A["Stale Live Workout"] --> B["Finalize Locally (Reconcile Time)"]
  B -- "Fabricated end_ts" --> C["Do NOT Export to Health"]
Loading

File Walkthrough

Relevant files
Bug fix
1 files
app_state.dart
Stop exporting stale orphaned workouts to Health.               
+6/-7     
Enhancement
1 files
theme.dart
Use built-in Flutter luminance calculation for contrast. 
+3/-8     
Tests
1 files
app_state_regressions_test.dart
Add regression test for stale workout export prevention. 
+47/-0   
Documentation
1 files
README.md
Remove public luminance helper from UI documentation.       
+1/-1     
Miscellaneous
3 files
payload.json
Remove stray local test payload JSON file.                             
+0/-1     
payload_july10.json
Remove stray local test payload JSON file.                             
+0/-1     
payload_null.json
Remove stray local test payload JSON file.                             
+0/-1     

…dupe luminance, drop leaked payload dumps

- app_state.dart: a workout row still 'live' past the resume ceiling gets
  finalized locally with a reconcile-time end_ts, but that end_ts is made
  up (we don't actually know when it ended) — it was still getting pushed
  to Apple Health/Health Connect as a real workout. Stopped the export for
  that branch, added a regression test.
- theme.dart: P.luminance() was reimplementing Color.computeLuminance(),
  same WCAG formula. Just call the built-in.
- removed payload.json/payload_july10.json/payload_null.json — real dated
  HRV/sleep output that shouldn't have been committed. Added payload*.json
  to .gitignore so it can't happen again.
@coderabbitai

coderabbitai Bot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 8f742f41-f80f-4f30-9f73-77d388134297

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sourcery-ai

sourcery-ai Bot commented Aug 29, 2026

Copy link
Copy Markdown

Reviewer's Guide

The PR fixes Health export correctness by keeping stale orphan reconciliation local, consolidates luminance logic on Flutter’s built-in API, and cleans up untracked local payload artifacts with a corresponding ignore rule.

Sequence diagram for stale workout reconciliation without Health export

sequenceDiagram
    participant AppState
    participant LocalDb
    participant HealthExporter

    AppState->>LocalDb: putSession(row with status done and end_ts)
    Note over AppState,HealthExporter: Fabricated reconciliation end_ts is never exported
    AppState-->>AppState: _log(stale live-session row finalized)
Loading

File-Level Changes

Change Details Files
Prevents reconciled stale workouts with fabricated end times from being exported as completed Health workouts.
  • Removes Health export dispatch from the stale-orphan reconciliation branch while still marking the session done locally and recording reconciliation time.
  • Adds a regression test that mocks the Health platform channel and verifies no export occurs while local finalization still happens.
lib/state/app_state.dart
test/app_state_regressions_test.dart
Eliminates duplicated luminance computation by using Flutter’s built-in WCAG implementation.
  • Removes the public P.luminance helper and updates contrast calculation to call Color.computeLuminance().
  • Updates UI documentation to reflect the reduced public API.
lib/ui2/theme.dart
lib/ui2/README.md
Removes committed local payload artifacts and ignores matching files going forward.
  • Adds the payload-file pattern to gitignore.
  • Deletes the three stray root-level JSON payload files.
.gitignore
payload.json
payload_july10.json
payload_null.json

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="lib/state/app_state.dart" line_range="5451-5452" />
<code_context>
           final reconciledEndTs = nowMs ~/ 1000;
           await LocalDb.putSession({
             ...row,
             'status': 'done',
             'end_ts': row['end_ts'] ?? reconciledEndTs,
           });
-          final id = row['id'] as String?;
</code_context>
<issue_to_address>
**issue (broader_impact):** The stale row is marked `done` and given a valid-looking `end_ts`, so removing the immediate `exportWorkoutId` call does not prevent a later full health export from writing it. The health export path skips only rows whose status is `live` and accepts any completed row with `end_ts > start_ts`, causing the fabricated workout to reach Apple Health/Health Connect on a subsequent export.

**Triggers:** When a later full health export processes the reconciled session.

**Suggested fix:** Persist an explicit non-exportable/reconciled marker and make both immediate and full health export paths skip it, or leave the row out of the exportable completed-session set.
</issue_to_address>

### Comment 2
<location path="test/app_state_regressions_test.dart" line_range="226-227" />
<code_context>
+      addTearDown(app.dispose);
+      await app.debugReconcileOrphanedLiveWorkout();
+      // exportWorkoutId is fired unawaited from the reconcile; give it a
+      // chance to run before asserting nothing came through.
+      await Future<void>.delayed(const Duration(milliseconds: 20));
+
+      expect(calls, isEmpty,
</code_context>
<issue_to_address>
**issue (testing):** The regression test waits a fixed 20 ms for the removed unawaited export, but the export chain includes asynchronous preferences, database, configuration, deletion, and platform calls. A reintroduced export can therefore still be running when `calls` is asserted, allowing the test to pass without observing the platform call.

**Triggers:** When the export path takes longer than 20 ms on a loaded or slower test run.

**Suggested fix:** Use an explicit completion signal or polling loop with a bounded timeout, and assert after the export future's observable work has completed rather than relying on a fixed sleep.
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 2 findings to address first, and if this decision is wrong, stale workouts will remain marked done locally but never reach Apple Health or Health Connect, and reverting will not automatically re-export them. The omission is bounded and can be repaired by explicitly reprocessing the affected sessions; the change does not create or publish fabricated health data.

Blocking findings: lib/state/app_state.dart:5452, test/app_state_regressions_test.dart:227


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread lib/state/app_state.dart
Comment on lines 5451 to 5452
'status': 'done',
'end_ts': row['end_ts'] ?? reconciledEndTs,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (broader_impact): The stale row is marked done and given a valid-looking end_ts, so removing the immediate exportWorkoutId call does not prevent a later full health export from writing it. The health export path skips only rows whose status is live and accepts any completed row with end_ts > start_ts, causing the fabricated workout to reach Apple Health/Health Connect on a subsequent export.

Triggers: When a later full health export processes the reconciled session.

Suggested fix: Persist an explicit non-exportable/reconciled marker and make both immediate and full health export paths skip it, or leave the row out of the exportable completed-session set.

Comment on lines +226 to +227
// chance to run before asserting nothing came through.
await Future<void>.delayed(const Duration(milliseconds: 20));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (testing): The regression test waits a fixed 20 ms for the removed unawaited export, but the export chain includes asynchronous preferences, database, configuration, deletion, and platform calls. A reintroduced export can therefore still be running when calls is asserted, allowing the test to pass without observing the platform call.

Triggers: When the export path takes longer than 20 ms on a loaded or slower test run.

Suggested fix: Use an explicit completion signal or polling loop with a bounded timeout, and assert after the export future's observable work has completed rather than relying on a fixed sleep.

@github-actions

Copy link
Copy Markdown
Contributor

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 2 🔵🔵⚪⚪⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Flaky Regression Test (Race Condition)

The test uses a fixed 20ms delay to wait for the unawaited HealthExporter.exportWorkoutId call to reach the platform channel. Because exportWorkoutId only receives an ID, it must perform an async SQLite lookup to fetch the session details before it can invoke the channel. In a CI environment, this DB query can easily take longer than 20ms. If the export bug is ever reintroduced, the test will likely check the calls list before the DB query finishes, see that it is empty, and falsely pass. Consider polling the calls list with a timeout instead of a fixed delay to ensure the test actually guards against regressions.

// exportWorkoutId is fired unawaited from the reconcile; give it a
// chance to run before asserting nothing came through.
await Future<void>.delayed(const Duration(milliseconds: 20));

@github-actions

Copy link
Copy Markdown
Contributor

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Restore Health export for workouts with a real end_ts

By removing the export entirely, valid workouts that already have a real end_ts (but
were stuck in 'live' status due to a crash or interruption) will no longer be
exported to Health. You should restore the export but gate it on row['end_ts'] !=
null so that only fabricated durations are withheld.

lib/state/app_state.dart [5451-5454]

           'status': 'done',
           'end_ts': row['end_ts'] ?? reconciledEndTs,
         });
+        final id = row['id'] as String?;
+        if (id != null && row['end_ts'] != null) {
+          unawaited(HealthExporter.exportWorkoutId(id));
+        }
         _log('[workout] finalized a stale live-session row from a previous run (id=${row['id']}).');
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies that if row['end_ts'] is not null, the duration is not fabricated and could theoretically be exported. However, if a 'live' row by definition never has an end_ts, this might just be defensive programming, making the export restoration safe but potentially unnecessary.

Medium

@abdulsaheel

Copy link
Copy Markdown
Collaborator Author

squashed into #314 for one clean review — closing this round.

@abdulsaheel
abdulsaheel deleted the audit/fixes-round3-edge branch August 29, 2026 13:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant