Skip to content

feat(account): auto-select best account per service#128

Merged
mpiton merged 5 commits intomainfrom
feat/task-24-account-selector
Apr 29, 2026
Merged

feat(account): auto-select best account per service#128
mpiton merged 5 commits intomainfrom
feat/task-24-account-selector

Conversation

@mpiton
Copy link
Copy Markdown
Owner

@mpiton mpiton commented Apr 29, 2026

Summary

Implements AccountSelector application service to rank multiple accounts for the same hoster/debrid service deterministically. Supports three configurable strategies (BestTraffic, RoundRobin, Manual) read from live config. Unblocks task 25 (rotation auto sur quota).

Why

Task 24 from PRD §6.4: plugins need deterministic account dispatch to handle quota rotation and fallback chains. BestTraffic ranks by traffic availability with tie-breakers for reproducibility. RoundRobin distributes load across eligible accounts. Manual reserved for future pinning UI.

Changes

  • Add AccountSelector service in src-tauri/src/application/services/account_selector.rs (~685 lines)

    • Three strategies: BestTraffic (enabled → non-expired → most traffic → most recent validation → smallest id), RoundRobin (per-service cursor), Manual (alias of BestTraffic + regression test)
    • Per-service candidate cache invalidated on account-touching events
    • Domain events: NoAccountAvailable { service_name } and AccountSelected { id, service_name, strategy }
  • Extend domain models

    • AccountSelectionStrategy enum in domain/model/account.rs with Display/FromStr (snake_case serialization)
    • account_selection_strategy field on AppConfig and ConfigPatch with apply_patch() support
  • Update adapters

    • Tauri bridge: serialize no-account-available and account-selected events to frontend
    • TOML store: persist strategy to config.toml (snake_case: best_traffic, round_robin, manual)
    • IPC DTOs: add account_selection_strategy field (camelCase) for settings/patch endpoints
  • Extend CommandBus with account_selector() accessor and resolve_account_for(service_name) convenience method

  • Flag planned hook in resolve_links.rs (comment only; actual per-URL calls deferred to avoid emitting AccountSelected once per probed link)

  • Update CHANGELOG.md with full feature description

Testing

✅ All 12 unit tests pass covering four PRD acceptance criteria:

  • 3-account scenario (full-traffic account wins over expired/low-traffic)
  • All-expired returns None + NoAccountAvailable event
  • Comparative ranking table (enabled → non-expired → traffic → validation → id)
  • RoundRobin alternance with cursor persistence across cache invalidation

✅ Comprehensive edge cases: cache invalidation, case-mismatch behavior, tie-breaker correctness, config strategy reading

✅ Integration: config patch apply, event emission, Tauri IPC serialization

Command output:

cargo test --package vortex-app --lib account_selector
# 12 passed
cargo clippy --workspace -- -D warnings
# clean
cargo fmt --check
# ok

Related Issues

  • Refs: PRD §6.4 (Auto-select meilleur compte)
  • Refs: PRD-v2 §P1.5 (task 24)
  • Unblocks: task 25 (rotation auto sur quota)

Notes for Reviewer

  • Traffic ranking: Unlimited enum variant ranks above any Finite(u64) via Ord derivation, ensuring unlimited plans always win
  • Cache survival: per-service cursors in RoundRobin survive bulk cache invalidation so hot-reload of account set does not reset rotation
  • Weak<Mutex> pattern prevents Arc cycle between EventBus subscriber and AccountSelector while allowing events to invalidate cache
  • Config override: resolve_account_for() reads live strategy from AppConfig each call, honoring runtime changes without restart
  • Deterministic tie-breaking: enabled → not expired → traffic_left → last_validated_at → id ensures reproducible selection for same candidate set

Checklist

  • Tests added and passing (12 unit tests, 4 acceptance criteria verified)
  • Docs updated (CHANGELOG.md, inline doc comments, task frontmatter)
  • No secrets, debug prints, or allow-directives
  • Self-reviewed diff
  • Cargo clippy + fmt clean

Summary by cubic

Auto-select the best account per service with a configurable strategy. Deterministic selection from live repo state, a single entry point for plugins/downloads, and backward-compatible config loading; implements PRD §6.4 (Linear task 24).

  • New Features

    • Added AccountSelector with three strategies: BestTraffic (default), RoundRobin (per-service cursor), and Manual (alias of BestTraffic for now).
    • New events: NoAccountAvailable and AccountSelected; forwarded via Tauri as no-account-available and account-selected.
    • CommandBus::resolve_account_for(service_name) uses the live AppConfig strategy.
    • Config: account_selection_strategy on AppConfig/ConfigPatch, persisted to TOML and exposed via IPC ("best_traffic" | "round_robin" | "manual").
  • Bug Fixes

    • Removed selector caching to fix a race that could serve stale rows.
    • RoundRobin: a poisoned cursor now surfaces as a validation error instead of returning None.
    • Strict validation of account_selection_strategy in IPC and TOML; settings_update surfaces errors. Legacy config.toml without the field now loads with the default (BestTraffic), while unknown non-empty values fail fast.
    • resolve_account_for propagates ConfigStore::get_config() failures; tests cover repo-fresh selection, case sensitivity, validation, poisoned-cursor handling, and legacy-config compatibility.

Written for commit 85a7f56. Summary will update on new commits. Review in cubic

Summary by CodeRabbit

  • New Features

    • Configurable account selection strategies in Settings: Best Traffic, Round Robin, Manual.
    • Account resolution API for operations needing credentials.
    • Accounts management UI with add/update/delete/validate/import/export and persisted account storage.
  • Bug Fixes

    • Invalid/unknown strategy values are rejected and surface as errors (no silent fallback).
  • Behavior

    • Account lifecycle events forwarded to the frontend: account selected, no account available.

Adds the `AccountSelector` application service so multiple accounts for
the same hoster / debrid service can be ranked and dispatched
deterministically. Three strategies are honoured from the live
`AppConfig::account_selection_strategy`:

  - `BestTraffic` (default): rank `enabled → not expired → most
    traffic_left → most recent last_validated → smallest id`, with
    `Unlimited` traffic ranking above any finite value.
  - `RoundRobin`: per-service cursor over enabled non-expired
    candidates ordered by id, surviving cache invalidation so a hot
    reload of the candidate set does not reset rotation.
  - `Manual`: alias of `BestTraffic` until pinning UI lands. Frozen
    by a regression test so a future change is deliberate.

Cache: per-service candidate snapshot is dropped in bulk on every
account-touching event (`AccountAdded` / `AccountUpdated` /
`AccountDeleted` / `AccountValidated` / `AccountValidationFailed` /
`AccountsImported`).

Domain events: new `NoAccountAvailable { service_name }` (emitted when
no candidate passes the filter, paired with `Ok(None)`) and
`AccountSelected { id, service_name, strategy }` (emitted on every
pick), both forwarded by the Tauri bridge as `no-account-available`
and `account-selected`.

CommandBus exposes `resolve_account_for(service_name)` so download /
link-grabber flows have a single entry point. `resolve_links` now
flags the planned hook in a comment but stops short of calling it
per-URL to avoid emitting `AccountSelected` once per probed link.

Config: new `account_selection_strategy` field on `AppConfig`,
`ConfigPatch` and `apply_patch`, plus the matching IPC and TOML
serialisation paths (snake_case `"best_traffic" | "round_robin" |
"manual"`).

Twelve unit tests cover the four PRD §6.4 acceptance criteria
(3-account scenario, all-expired surface, comparative ranking table,
round-robin alternance) plus cache invalidation and case-mismatch
behaviour. Unblocks task 25 (rotation auto sur quota).

Refs: PRD §6.4, PRD-v2 §P1.5
@github-actions github-actions Bot added documentation Improvements or additions to documentation rust labels Apr 29, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 29, 2026

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds an AccountSelector service and AccountSelectionStrategy; threads selection through CommandBus; persists/parses strategy via IPC/TOML with strict validation and legacy compatibility; emits DomainEvent::NoAccountAvailable and DomainEvent::AccountSelected and forwards them to the frontend; updates tests and CHANGELOG.

Changes

Cohort / File(s) Summary
Domain: account & config
src-tauri/src/domain/model/account.rs, src-tauri/src/domain/model/config.rs
Add AccountSelectionStrategy (BestTraffic/RoundRobin/Manual) with Display/FromStr/DEFAULT; add account_selection_strategy to AppConfig and optional in ConfigPatch; include tests and patch-apply logic.
AccountSelector service
src-tauri/src/application/services/account_selector.rs, src-tauri/src/application/services/mod.rs
New AccountSelector implementing filtering (enabled/non-expired), BestTraffic ranking, RoundRobin cursor, Manual alias; publishes NoAccountAvailable / AccountSelected; extensive unit tests.
CommandBus integration
src-tauri/src/application/command_bus.rs, src-tauri/src/application/commands/resolve_links.rs
CommandBus gains optional AccountSelector, builder/accessor, and resolve_account_for(service_name) delegating to selector and surfacing config read errors; docs updated for plugin usage.
IPC adapters (Tauri)
src-tauri/src/adapters/driving/tauri_ipc.rs
Expose SettingsDto.account_selection_strategy: String and ConfigPatchDto.account_selection_strategy: Option<String>; convert ConfigPatchDto -> ConfigPatch to TryFrom to reject invalid strategy strings and surface IPC errors; unit tests added.
TOML config store
src-tauri/src/adapters/driven/config/toml_config_store.rs
Persist account_selection_strategy in TOML DTO; load via fallible conversion that maps parse failures to DomainError::StorageError; legacy missing-key defaults to DEFAULT; tests added.
Events & bridges
src-tauri/src/domain/event.rs, src-tauri/src/adapters/driven/event/tauri_bridge.rs, src-tauri/src/adapters/driven/logging/download_log_bridge.rs
Add DomainEvent::NoAccountAvailable { service_name } and DomainEvent::AccountSelected { id, service_name, strategy }; Tauri bridge forwards both events as JSON; download_log_bridge ignores them for download logs.
Misc: wiring & docs
src-tauri/src/application/..., CHANGELOG.md
Wiring updates across layers (builder setters, accessors) and changelog entry documenting feature, IPC/TOML validation, and accounts CQRS/persistence behavior.

Sequence Diagram

sequenceDiagram
    participant Client as Client
    participant CommandBus as CommandBus
    participant ConfigStore as ConfigStore
    participant AccountSelector as AccountSelector
    participant AccountRepo as AccountRepository
    participant EventBus as EventBus
    participant TauriBridge as TauriEventBridge

    Client->>CommandBus: resolve_account_for(service)
    CommandBus->>ConfigStore: get_config()
    ConfigStore-->>CommandBus: AppConfig (strategy)
    CommandBus->>AccountSelector: select_best(service, strategy)
    AccountSelector->>AccountRepo: list_by_service(service)
    AccountRepo-->>AccountSelector: accounts
    AccountSelector->>AccountSelector: filter & rank/select

    alt Account Selected
        AccountSelector->>EventBus: publish AccountSelected(id, service, strategy)
        AccountSelector-->>CommandBus: Some(Account)
    else No Eligible Account
        AccountSelector->>EventBus: publish NoAccountAvailable(service)
        AccountSelector-->>CommandBus: None
    end

    EventBus->>TauriBridge: forward event
    TauriBridge->>Client: JSON payload
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

frontend

Poem

🐰 I hopped through configs, keys, and queues,
I picked by traffic, turn, or human views,
I call the bus, then sing the pick aloud,
If none are found I whisper — events are proud. 🥕

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main feature being implemented: automatic account selection based on service, which is the primary objective of this substantial PR introducing AccountSelector.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 feat/task-24-account-selector

Review rate limit: 4/5 reviews remaining, refill in 12 minutes.

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

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e29ba8537a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src-tauri/src/adapters/driving/tauri_ipc.rs Outdated
Comment thread src-tauri/src/application/command_bus.rs Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (2)
src-tauri/src/domain/event.rs (1)

284-293: Use AccountSelectionStrategy here instead of a raw String.

This is a closed set already modeled in the domain. Keeping strategy as String makes the event payload weaker than the source model and lets future publishers emit invalid values without compiler help. Prefer the enum here and stringify only at the adapter boundary.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src-tauri/src/domain/event.rs` around lines 284 - 293, The AccountSelected
event currently uses a raw String for strategy; change the payload to use the
enum AccountSelectionStrategy instead (replace the field signature strategy:
String with strategy: AccountSelectionStrategy in the AccountSelected variant)
and update any code that constructs this event (e.g.,
AccountSelector::select_best) to pass the enum value; keep string serialization
only at the adapter/transport boundary by converting AccountSelectionStrategy
to/from a string where events are serialized/deserialized so existing external
formats remain unchanged.
src-tauri/src/adapters/driven/event/tauri_bridge.rs (1)

71-72: Add regression coverage for the new account event bridge contract.

These mappings are hand-written and frontend-facing, but the test module does not currently assert "no-account-available" / "account-selected" or the serviceName/strategy payload keys. A small bridge test here would lock down the IPC contract you just introduced.

Also applies to: 201-214

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src-tauri/src/adapters/driven/event/tauri_bridge.rs` around lines 71 - 72,
The new IPC mapping for DomainEvent::NoAccountAvailable =>
"no-account-available" and DomainEvent::AccountSelected => "account-selected"
needs unit tests in the module's test suite to lock the bridge contract: add
tests that construct those DomainEvent variants, run them through the same
bridge/serialization path used by the code that emits IPC events (the
match/mapper that yields the event name string), assert the emitted event name
equals "no-account-available" / "account-selected", and assert the serialized
payload contains the frontend-facing keys "serviceName" and "strategy"; apply
the same assertions for the other similar mappings in the same mapping block
(the entries around the other event arms).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src-tauri/src/adapters/driving/tauri_ipc.rs`:
- Around line 1012-1015: The current conversion of account_selection_strategy
silently maps parse failures to None by using as_deref().and_then(|s|
s.parse().ok()); change this to a fallible parse that returns an explicit
validation error on unknown strings so settings_update can reject bad payloads.
Specifically, replace the .and_then(... .parse().ok()) flow in the
account_selection_strategy handling with a parse call that returns Result and
propagate or map its Err into a validation error (so settings_update returns an
error response) and update settings_update to handle that error path and return
a clear validation message for invalid accountSelectionStrategy values.

In `@src-tauri/src/application/command_bus.rs`:
- Around line 295-299: Currently the code swallows ConfigStore::get_config()
errors by falling back to AccountSelectionStrategy::DEFAULT; change to propagate
failures instead by replacing the map(...).unwrap_or(...) with a direct ?-based
extraction: e.g. let strategy =
self.config_store.get_config()?.account_selection_strategy; update the enclosing
function signatures to return Result (or propagate the error type) and adjust
callers to handle the propagated error so config read failures are surfaced
instead of silently using AccountSelectionStrategy::DEFAULT.

In `@src-tauri/src/application/services/account_selector.rs`:
- Around line 83-87: The current code calls guard.cache.clear() which evicts all
cached service entries; instead, locate the specific service key for the touched
account (the identifier used as the cache key in account_selector.rs) and remove
only that entry from the cache (e.g., use guard.cache.remove(&service_key) or
guard.cache.retain to drop just that key) inside the same weak_state.upgrade()
&& state.lock() block so only the affected service's cache is invalidated rather
than the whole cache.

---

Nitpick comments:
In `@src-tauri/src/adapters/driven/event/tauri_bridge.rs`:
- Around line 71-72: The new IPC mapping for DomainEvent::NoAccountAvailable =>
"no-account-available" and DomainEvent::AccountSelected => "account-selected"
needs unit tests in the module's test suite to lock the bridge contract: add
tests that construct those DomainEvent variants, run them through the same
bridge/serialization path used by the code that emits IPC events (the
match/mapper that yields the event name string), assert the emitted event name
equals "no-account-available" / "account-selected", and assert the serialized
payload contains the frontend-facing keys "serviceName" and "strategy"; apply
the same assertions for the other similar mappings in the same mapping block
(the entries around the other event arms).

In `@src-tauri/src/domain/event.rs`:
- Around line 284-293: The AccountSelected event currently uses a raw String for
strategy; change the payload to use the enum AccountSelectionStrategy instead
(replace the field signature strategy: String with strategy:
AccountSelectionStrategy in the AccountSelected variant) and update any code
that constructs this event (e.g., AccountSelector::select_best) to pass the enum
value; keep string serialization only at the adapter/transport boundary by
converting AccountSelectionStrategy to/from a string where events are
serialized/deserialized so existing external formats remain unchanged.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: dbbd2dbf-8f81-4599-8e9f-003aa075af58

📥 Commits

Reviewing files that changed from the base of the PR and between f128f17 and e29ba85.

📒 Files selected for processing (12)
  • CHANGELOG.md
  • src-tauri/src/adapters/driven/config/toml_config_store.rs
  • src-tauri/src/adapters/driven/event/tauri_bridge.rs
  • src-tauri/src/adapters/driven/logging/download_log_bridge.rs
  • src-tauri/src/adapters/driving/tauri_ipc.rs
  • src-tauri/src/application/command_bus.rs
  • src-tauri/src/application/commands/resolve_links.rs
  • src-tauri/src/application/services/account_selector.rs
  • src-tauri/src/application/services/mod.rs
  • src-tauri/src/domain/event.rs
  • src-tauri/src/domain/model/account.rs
  • src-tauri/src/domain/model/config.rs

Comment thread src-tauri/src/adapters/driving/tauri_ipc.rs Outdated
Comment thread src-tauri/src/application/command_bus.rs Outdated
Comment thread src-tauri/src/application/services/account_selector.rs Outdated
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

2 issues found across 12 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src-tauri/src/adapters/driving/tauri_ipc.rs">

<violation number="1" location="src-tauri/src/adapters/driving/tauri_ipc.rs:1015">
P2: Invalid `account_selection_strategy` values are silently ignored instead of being rejected.</violation>
</file>

<file name="src-tauri/src/application/command_bus.rs">

<violation number="1" location="src-tauri/src/application/command_bus.rs:299">
P2: Do not swallow `get_config()` errors in `resolve_account_for`; falling back to default can silently apply the wrong selection strategy.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread src-tauri/src/adapters/driving/tauri_ipc.rs Outdated
Comment thread src-tauri/src/application/command_bus.rs Outdated
…pe cache eviction

Addresses three review findings on PR #128:

  - `ConfigPatchDto` -> `ConfigPatch` is now `TryFrom` so an unknown
    `accountSelectionStrategy` value comes back as a validation error
    instead of being silently dropped to `None`. `settings_update`
    surfaces the parse failure to the caller. `ConfigPatchDto` now
    derives `Default` so tests can construct a single-field patch.
  - `CommandBus::resolve_account_for` propagates `ConfigStore::get_config()`
    failures via `?` instead of swallowing them with `unwrap_or(DEFAULT)`,
    so a corrupt or unreadable config no longer pretends `BestTraffic`
    when the user asked for `RoundRobin`/`Manual`.
  - `AccountSelector` cache invalidation is now scoped per service:
    `AccountAdded` evicts the slot named by its `service_name`,
    `AccountUpdated` / `AccountDeleted` / `AccountValidated` /
    `AccountValidationFailed` evict the slot whose snapshot already
    contained the affected `id`, and `AccountsImported` keeps the
    bulk clear. An event for service A no longer warms-up service B
    on its next pick.

Six new tests pin the contract: `test_config_patch_dto_rejects_unknown_account_selection_strategy`,
`test_config_patch_dto_accepts_known_account_selection_strategy`,
`test_config_patch_dto_passes_through_when_strategy_is_none`,
`test_resolve_account_for_propagates_config_store_failure`,
`test_account_event_invalidates_only_touched_service`,
`test_account_added_invalidates_only_target_service_cache`,
`test_accounts_imported_invalidates_every_cache_slot`.

`cargo test --lib` 1147 passed. `cargo clippy --lib -- -D warnings` clean.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8849b5c594

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src-tauri/src/application/services/account_selector.rs Outdated
Comment thread src-tauri/src/adapters/driven/config/toml_config_store.rs Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

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 the current code and only fix it if needed.

Inline comments:
In `@src-tauri/src/adapters/driving/tauri_ipc.rs`:
- Line 963: Update the doc comment to reference the correct conversion trait:
replace the mention of ConfigPatch::from(ConfigPatchDto) with
ConfigPatch::try_from(ConfigPatchDto) (or mention TryFrom/try_from on
ConfigPatch for converting from ConfigPatchDto) so it accurately reflects that
unknown values are rejected via TryFrom rather than From; ensure the comment
names the types ConfigPatch and ConfigPatchDto and the TryFrom conversion.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8077251b-5a9a-40fc-bb23-c48165c70839

📥 Commits

Reviewing files that changed from the base of the PR and between e29ba85 and 8849b5c.

📒 Files selected for processing (4)
  • CHANGELOG.md
  • src-tauri/src/adapters/driving/tauri_ipc.rs
  • src-tauri/src/application/command_bus.rs
  • src-tauri/src/application/services/account_selector.rs
✅ Files skipped from review due to trivial changes (1)
  • CHANGELOG.md

Comment thread src-tauri/src/adapters/driving/tauri_ipc.rs Outdated
…x doc

Addresses three follow-up review findings on PR #128:

  - `AccountSelector` no longer caches `list_by_service` results. The
    earlier event-driven invalidation raced with `TokioEventBus` —
    `bus.publish(AccountUpdated)` returns immediately while the
    subscriber dispatches on a `tokio::spawn`'d task, so a `select_best`
    call landing between publish and dispatch could still observe stale
    rows. SQLite reads are cheap for the row counts in scope (≤ a few
    dozen accounts per service), so the cache was trading correctness
    for negligible savings. The round-robin cursor stays — it is the
    only state the selector needs to keep.
  - `From<ConfigDto> for AppConfig` is now `TryFrom<…>` returning
    `DomainError`. A hand-edited `config.toml` carrying an unknown
    `account_selection_strategy` previously coerced to `best_traffic`
    via `unwrap_or(DEFAULT)`, masking corruption. The TOML store now
    surfaces the failure as `StorageError("invalid config: …")` so the
    runtime refuses to start with an invalid persisted strategy.
  - `ConfigPatchDto::account_selection_strategy` doc string now points
    at `ConfigPatch::try_from(ConfigPatchDto)` to match the actual
    contract introduced in 8849b5c.

Six cache-mechanics tests in `account_selector.rs` collapsed into two
that pin the new contract: `test_select_best_always_reflects_current_repo_state`
and `test_select_best_is_case_sensitive_on_service_name`. New TOML store
tests `test_get_config_rejects_unknown_persisted_account_selection_strategy`
and `test_get_config_accepts_known_persisted_account_selection_strategy`
pin the strict-parse path.

`cargo test --lib` 1145 passed. `cargo clippy --lib -- -D warnings` clean.
`cargo fmt --check` clean.
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 4 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src-tauri/src/application/services/account_selector.rs">

<violation number="1" location="src-tauri/src/application/services/account_selector.rs:112">
P2: Round-robin selection silently drops to `None` on mutex poison, which can return no account even when eligible accounts exist.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread src-tauri/src/application/services/account_selector.rs Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

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 the current code and only fix it if needed.

Inline comments:
In `@src-tauri/src/application/services/account_selector.rs`:
- Around line 112-116: Change pick_round_robin to return
Result<Option<&Account>, AppError> instead of Option<&Account>: on lock failure
of self.rr_cursor (i.e., Mutex::lock()), convert the poisoned-lock error into
AppError::Validation("Round-robin cursor poisoned") and return Err(...) so
callers can distinguish real failures; keep the existing round-robin logic
(entry, cursor increment, selection) and wrap the final Some(pick) in
Ok(Some(pick)) and return Ok(None) when sorted.is_empty(). Then update the
caller in select_best to call pick_round_robin()? and propagate the Result (use
?), so a poisoned mutex yields Err(AppError::Validation(...)) instead of
silently returning Ok(None) and suppressing NoAccountAvailable.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fe74b824-1448-44c0-b87c-77049636aed9

📥 Commits

Reviewing files that changed from the base of the PR and between 8849b5c and 63d55ed.

📒 Files selected for processing (4)
  • CHANGELOG.md
  • src-tauri/src/adapters/driven/config/toml_config_store.rs
  • src-tauri/src/adapters/driving/tauri_ipc.rs
  • src-tauri/src/application/services/account_selector.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • src-tauri/src/adapters/driven/config/toml_config_store.rs
  • CHANGELOG.md
  • src-tauri/src/adapters/driving/tauri_ipc.rs

Comment thread src-tauri/src/application/services/account_selector.rs Outdated
Both CodeRabbit and cubic flagged that `pick_round_robin` swallowed
mutex poisoning by writing `self.rr_cursor.lock().ok()?`, which folds
the `PoisonError` into the `Option` short-circuit and returns
`Ok(None)` from `select_best`. That value is reserved for "no eligible
accounts" — when used for a poisoned cursor, callers cannot distinguish
zero-candidate from internal corruption and the `NoAccountAvailable`
event would not fire as the contract documents.

`pick_round_robin` now returns `Result<Option<&Account>, AppError>`
and maps `Mutex::lock()` failures to `AppError::Validation(
"round-robin cursor mutex poisoned")`. `select_best` propagates with
`?`. Regression test
`test_pick_round_robin_returns_err_on_poisoned_cursor` deliberately
poisons the cursor from a worker thread and asserts the
`Validation` variant.

`cargo test --lib` 1146 passed. `cargo clippy --lib -- -D warnings`
clean. `cargo fmt --check` clean.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7ae03739aa

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src-tauri/src/adapters/driven/config/toml_config_store.rs Outdated
Codex P1 finding: an existing `config.toml` written before task 24
introduced `account_selection_strategy` has no key for it.
`#[serde(default)]` on `ConfigDto` causes the missing field to
deserialize as `String::default()` (empty), and the strict `parse()`
in `TryFrom<ConfigDto> for AppConfig` would then reject every legacy
file at startup.

Treat the empty-string case as `AccountSelectionStrategy::DEFAULT` so
upgraded users keep working while non-empty typo / corruption values
still surface as `StorageError("invalid config: …")`. Regression test
`test_get_config_accepts_legacy_config_without_strategy_field` writes
a TOML carrying only `api_key` and asserts the loaded `AppConfig`
hydrates `BestTraffic`.

`cargo test --lib` 1147 passed. `cargo clippy --lib -- -D warnings`
clean. `cargo fmt --check` clean.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
src-tauri/src/adapters/driven/config/toml_config_store.rs (2)

257-260: Consider trimming persisted strategy before validation.

Line 257 and Line 260 currently treat " round_robin " as invalid. Trimming before empty-check/parse improves resilience for hand-edited TOML without weakening unknown-value rejection.

♻️ Suggested adjustment
-        let account_selection_strategy: AccountSelectionStrategy =
-            if d.account_selection_strategy.is_empty() {
+        let raw_strategy = d.account_selection_strategy.trim();
+        let account_selection_strategy: AccountSelectionStrategy =
+            if raw_strategy.is_empty() {
                 AccountSelectionStrategy::DEFAULT
             } else {
-                d.account_selection_strategy.parse()?
+                raw_strategy.parse()?
             };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src-tauri/src/adapters/driven/config/toml_config_store.rs` around lines 257 -
260, The persisted account selection strategy value d.account_selection_strategy
should be trimmed before validation so values like " round_robin " are accepted;
update the logic around d.account_selection_strategy (used with
AccountSelectionStrategy::DEFAULT and .parse()) to call .trim() (or equivalent)
before checking is_empty() and before invoking parse(), ensuring you still
propagate parse errors for unknown values.

663-679: Add one explicit empty-string regression test.

This test validates a missing key path; the empty-string fallback branch at Line 257 is still unproven. Add a focused case with account_selection_strategy = "" to lock in the intended legacy behavior.

🧪 Suggested test case
+    #[test]
+    fn test_get_config_accepts_empty_strategy_string_as_default() {
+        let dir = tempfile::tempdir().unwrap();
+        let path = dir.path().join("config.toml");
+        std::fs::write(
+            &path,
+            "api_key = \"legacy-key\"\naccount_selection_strategy = \"\"\n",
+        )
+        .unwrap();
+
+        let store = TomlConfigStore::new(path, None, Some("default-key".to_string()));
+        let config = store.get_config().expect("empty strategy should load as default");
+        assert_eq!(config.account_selection_strategy, AccountSelectionStrategy::DEFAULT);
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src-tauri/src/adapters/driven/config/toml_config_store.rs` around lines 663 -
679, Add a new unit test alongside
test_get_config_accepts_legacy_config_without_strategy_field that writes a TOML
file containing account_selection_strategy = "" and verifies
TomlConfigStore::new(...).get_config() returns
AccountSelectionStrategy::DEFAULT; this explicitly exercises the empty-string
fallback branch (the legacy hydration path) so the empty-string value is treated
the same as a missing field. Use the same tempfile setup pattern, call
TomlConfigStore::new(path, None, Some("default-key".to_string())), call
get_config() and assert config.account_selection_strategy ==
AccountSelectionStrategy::DEFAULT.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src-tauri/src/adapters/driven/config/toml_config_store.rs`:
- Around line 257-260: The persisted account selection strategy value
d.account_selection_strategy should be trimmed before validation so values like
" round_robin " are accepted; update the logic around
d.account_selection_strategy (used with AccountSelectionStrategy::DEFAULT and
.parse()) to call .trim() (or equivalent) before checking is_empty() and before
invoking parse(), ensuring you still propagate parse errors for unknown values.
- Around line 663-679: Add a new unit test alongside
test_get_config_accepts_legacy_config_without_strategy_field that writes a TOML
file containing account_selection_strategy = "" and verifies
TomlConfigStore::new(...).get_config() returns
AccountSelectionStrategy::DEFAULT; this explicitly exercises the empty-string
fallback branch (the legacy hydration path) so the empty-string value is treated
the same as a missing field. Use the same tempfile setup pattern, call
TomlConfigStore::new(path, None, Some("default-key".to_string())), call
get_config() and assert config.account_selection_strategy ==
AccountSelectionStrategy::DEFAULT.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4e2216f9-2a21-4853-80df-fb796fb1472e

📥 Commits

Reviewing files that changed from the base of the PR and between 7ae0373 and 85a7f56.

📒 Files selected for processing (2)
  • CHANGELOG.md
  • src-tauri/src/adapters/driven/config/toml_config_store.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • CHANGELOG.md

@mpiton mpiton merged commit c551d53 into main Apr 29, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation rust

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant