Skip to content

Reconcile legacy avatars.selected on avatar update events#1098

Open
Brutus5000 wants to merge 1 commit into
developfrom
fix/reconcile-legacy-avatar-flag
Open

Reconcile legacy avatars.selected on avatar update events#1098
Brutus5000 wants to merge 1 commit into
developfrom
fix/reconcile-legacy-avatar-flag

Conversation

@Brutus5000

@Brutus5000 Brutus5000 commented Jun 29, 2026

Copy link
Copy Markdown
Member

Problem

Clearing one's avatar via the API doesn't stick. The API sets login.avatar_id = NULL and publishes success.player_avatar.update (verified: Elide fires the update lifecycle hook on the relationship DELETE too). The lobby consumes the event and refreshes — but _avatar_grant_join_onclause falls back to the legacy avatars.selected = 1 row when login.avatar_id IS NULL, so the just-cleared avatar gets resurrected from the stale legacy flag.

Observed:

api:    Avatar 'null' has been selected on player '76365'
lobby:  Player 76365 avatar refreshed ... FAF_Developer.png   # legacy fallback

Root cause: login.avatar_id = NULL is ambiguous — it means both "never migrated" and "explicitly cleared" — and the fallback always resolves it toward the legacy flag.

Fix

An avatar-update event means the API (the authoritative writer of login.avatar_id) just changed this player's selection. So refresh_player_avatar now reconciles avatars.selected with login.avatar_id before re-reading: the granted row matching avatar_id is marked selected, all others deselected (all deselected when avatar_id is null).

  • Clearavatar_id null → legacy flags cleared → no avatar (sticks).
  • Set → matching row selected, others cleared → lazily migrates the legacy flag for that player.
  • Login (fetch_player_data) is unchanged, so players who haven't touched the new system still get their legacy-selected avatar via the fallback until their first update event.

Tests

  • test_refresh_player_avatar_connected — set case: authoritative avatar_id wins and the legacy flag is reconciled to it.
  • test_refresh_player_avatar_clears_legacy_fallback — clear case: no avatar, legacy flags cleaned.
  • Full test_player_service.py + test_avatar_change_queue_service.py pass (24 tests) against a v144 schema.

Summary by CodeRabbit

  • Bug Fixes
    • Refreshing a player’s avatar now keeps the selected avatar state consistent with the saved account avatar.
    • If no avatar is set, any leftover selection flags are cleared instead of reappearing.
    • Avatar refreshes now better handle older stored selection data to prevent incorrect avatar display.

The avatar lookup falls back to the legacy `avatars.selected = 1` row when
`login.avatar_id` is null. Since an avatar-update event means the API (the
authoritative writer of `login.avatar_id`) changed the selection, clearing the
avatar (avatar_id set to null) was undone by the stale legacy flag.

refresh_player_avatar now reconciles `avatars.selected` with `login.avatar_id`
before re-reading, so an explicit clear sticks and a set lazily migrates the
legacy flag. Login (fetch_player_data) still uses the fallback for players who
have not yet touched the new system.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

PlayerService gains a private _sync_legacy_avatar_selection method that reconciles the deprecated avatars.selected column with the authoritative login.avatar_id via a DB-side coalesce update. This method is called at the start of refresh_player_avatar. Tests are updated to verify both the authoritative-avatar and null-avatar cases using direct DB queries.

Legacy Avatar Selection Reconciliation

Layer / File(s) Summary
_sync_legacy_avatar_selection helper and refresh integration
server/player_service.py
Adds func import, implements the helper to update avatars.selected rows based on login.avatar_id (using SQL coalesce to handle null), and calls it in refresh_player_avatar before re-reading avatar data.
Tests for sync and legacy fallback clearing
tests/unit_tests/test_player_service.py
Adds select, avatars, and login imports; rewrites test_refresh_player_avatar_connected to assert avatars.selected flags are reconciled to {1: True, 2: False}; adds test_refresh_player_avatar_clears_legacy_fallback asserting all legacy flags are cleared when login.avatar_id is null.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • FAForever/server#1095: Modifies the same refresh_player_avatar / avatars.selected / login.avatar_id reconciliation logic in server/player_service.py.

Poem

🐇 A rabbit once found an old flag left behind,
Two selected rows with truth hard to find.
With coalesce in paw and a query so bright,
It synced every avatar to get the state right.
Now refresh_player_avatar hops true — what a sight! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main fix: reconciling legacy avatars.selected during avatar update events.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/reconcile-legacy-avatar-flag

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.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/unit_tests/test_player_service.py`:
- Around line 111-115: The legacy-avatar clear-path assertion in the player
service test is vacuous because all(...) will pass on an empty result. Update
the test around the _db.acquire() query for avatars.c.selected and
avatars.c.idUser == 50 to first assert that result contains at least one row,
then verify all returned rows are not selected so the test fails if no legacy
avatar rows exist.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e6d2f9b5-0ebf-4370-82ae-954fe05d3ebd

📥 Commits

Reviewing files that changed from the base of the PR and between ac6d11d and 32be0a4.

📒 Files selected for processing (2)
  • server/player_service.py
  • tests/unit_tests/test_player_service.py

Comment on lines +111 to +115
async with player_service._db.acquire() as conn:
result = await conn.execute(
select(avatars.c.selected).where(avatars.c.idUser == 50)
)
assert all(not row.selected for row in result)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make the clear-path assertion non-vacuous.

all(...) passes on an empty result set, so this test would still pass if player 50 no longer had any legacy avatar rows. Assert that rows exist before checking they were cleared.

Proposed test hardening
     async with player_service._db.acquire() as conn:
         result = await conn.execute(
             select(avatars.c.selected).where(avatars.c.idUser == 50)
         )
-        assert all(not row.selected for row in result)
+        selected = [bool(row.selected) for row in result]
+    assert selected
+    assert all(not is_selected for is_selected in selected)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async with player_service._db.acquire() as conn:
result = await conn.execute(
select(avatars.c.selected).where(avatars.c.idUser == 50)
)
assert all(not row.selected for row in result)
async with player_service._db.acquire() as conn:
result = await conn.execute(
select(avatars.c.selected).where(avatars.c.idUser == 50)
)
selected = [bool(row.selected) for row in result]
assert selected
assert all(not is_selected for is_selected in selected)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit_tests/test_player_service.py` around lines 111 - 115, The
legacy-avatar clear-path assertion in the player service test is vacuous because
all(...) will pass on an empty result. Update the test around the _db.acquire()
query for avatars.c.selected and avatars.c.idUser == 50 to first assert that
result contains at least one row, then verify all returned rows are not selected
so the test fails if no legacy avatar rows exist.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant