Skip to content

Add option to change the category instead of deleting queue items#602

Merged
Flaminel merged 9 commits into
mainfrom
add_option_for_category_change
May 5, 2026
Merged

Add option to change the category instead of deleting queue items#602
Flaminel merged 9 commits into
mainfrom
add_option_for_category_change

Conversation

@Flaminel

@Flaminel Flaminel commented May 4, 2026

Copy link
Copy Markdown
Contributor

Relates to #452 #591

@coderabbitai

coderabbitai Bot commented May 4, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@Cleanuparr Cleanuparr deleted a comment from sourcery-ai Bot May 4, 2026
@qodo-code-review

qodo-code-review Bot commented May 4, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (2) 📎 Requirement gaps (0)

Grey Divider


Action required

1. QueueRuleDto.ChangeCategory undocumented 📘 Rule violation ✧ Quality
Description
A new public DTO property ChangeCategory was added without an XML documentation comment block
containing ``. This violates the requirement for XML docs on public backend API surface and reduces
generated API/reference documentation quality.
Code

code/backend/Cleanuparr.Api/Features/QueueCleaner/Contracts/Requests/QueueRuleDto.cs[28]

+    public bool ChangeCategory { get; set; } = false;
Evidence
PR Compliance ID 225602 requires XML documentation comments for public backend API symbols. The
newly-added public property ChangeCategory in QueueRuleDto has no preceding /// ``
documentation.

Rule 225602: Require XML documentation comments for public backend APIs
code/backend/Cleanuparr.Api/Features/QueueCleaner/Contracts/Requests/QueueRuleDto.cs[27-28]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`QueueRuleDto.ChangeCategory` is a newly added public API contract property but lacks an XML documentation comment block (at least a `<summary>`).
## Issue Context
Compliance requires XML documentation for public backend API symbols, including new/changed properties on DTOs.
## Fix Focus Areas
- code/backend/Cleanuparr.Api/Features/QueueCleaner/Contracts/Requests/QueueRuleDto.cs[27-28]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. ChangeCategory config undocumented ✓ Resolved 📘 Rule violation ✧ Quality
Description
New public ChangeCategory properties were added without XML documentation comments (``). This
violates the XML documentation requirement for public backend API symbols.
Code

code/backend/Cleanuparr.Persistence/Models/Configuration/QueueCleaner/FailedImportConfig.cs[23]

+    public bool ChangeCategory { get; init; }
Evidence
PR Compliance ID 225602 requires XML docs for public classes/members. The newly added public
ChangeCategory properties in multiple backend models/config records have no /// `` documentation
immediately above them.

Rule 225602: Require XML documentation comments for public backend APIs
code/backend/Cleanuparr.Persistence/Models/Configuration/QueueCleaner/FailedImportConfig.cs[22-24]
code/backend/Cleanuparr.Persistence/Models/Configuration/QueueCleaner/QueueRule.cs[31-33]
code/backend/Cleanuparr.Infrastructure/Features/DownloadRemover/Models/QueueItemRemoveRequest.cs[18-21]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
New public `ChangeCategory` properties were introduced without XML documentation comments containing at least `<summary>`.
## Issue Context
These are public backend types/members and must be documented per compliance, especially as they are new API/config surface.
## Fix Focus Areas
- code/backend/Cleanuparr.Persistence/Models/Configuration/QueueCleaner/FailedImportConfig.cs[22-24]
- code/backend/Cleanuparr.Persistence/Models/Configuration/QueueCleaner/QueueRule.cs[31-33]
- code/backend/Cleanuparr.Infrastructure/Features/DownloadRemover/Models/QueueItemRemoveRequest.cs[18-21]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. DeleteQueueItemAsync undocumented param 📘 Rule violation ✧ Quality
Description
The public DeleteQueueItemAsync signature was modified to add changeCategory, but the
method/interface lacks XML documentation (including `` tags). This violates the requirement for XML
docs on public backend APIs and makes the new behavior harder to understand and use correctly.
Code

code/backend/Cleanuparr.Infrastructure/Features/Arr/Interfaces/IArrClient.cs[14]

+    Task DeleteQueueItemAsync(ArrInstance arrInstance, QueueRecord record, bool removeFromClient, bool changeCategory, DeleteReason deleteReason);
Evidence
PR Compliance ID 225602 requires XML docs for public interfaces and public methods (including
parameter documentation). The changed DeleteQueueItemAsync(..., bool changeCategory, ...)
signature appears without any accompanying XML doc block or ` tags describing changeCategory`.

Rule 225602: Require XML documentation comments for public backend APIs
code/backend/Cleanuparr.Infrastructure/Features/Arr/Interfaces/IArrClient.cs[14-14]
code/backend/Cleanuparr.Infrastructure/Features/Arr/ArrClient.cs[163-168]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`DeleteQueueItemAsync` was updated to include a new `changeCategory` parameter, but the public interface/method does not include XML documentation comments (including `<param>` tags).
## Issue Context
This is a public backend API surface (interface + implementation) and needs XML documentation per compliance; the new parameter changes behavior and should be explicitly described.
## Fix Focus Areas
- code/backend/Cleanuparr.Infrastructure/Features/Arr/Interfaces/IArrClient.cs[14-14]
- code/backend/Cleanuparr.Infrastructure/Features/Arr/ArrClient.cs[163-168]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (1)
4. ChangeCategory breaks non-torrents 🐞 Bug ≡ Correctness
Description
In QueueCleaner’s failed-import branch, enabling FailedImport.ChangeCategory forces
removeFromClient=false for all queue items, including non-torrent protocols where category-changing
is not applicable. This can leave failed-import downloads behind in the download client (cleanup
silently stops removing them from the client).
Code

code/backend/Cleanuparr.Infrastructure/Features/Jobs/QueueCleaner.cs[R226-227]

+                    bool changeCategory = queueCleanerConfig.FailedImport.ChangeCategory;
+                    bool removeFromClient = !changeCategory && (!downloadCheckResult.IsPrivate || queueCleanerConfig.FailedImport.DeletePrivate);
Evidence
QueueCleaner computes isTorrent from record.Protocol and only uses it to decide whether to query
torrent download clients, but it does not gate FailedImport.ChangeCategory; when that flag is true,
removeFromClient is forced to false regardless of protocol. The docs also describe this setting as
changing “the torrent’s category”, implying torrent-only semantics.

code/backend/Cleanuparr.Infrastructure/Features/Jobs/QueueCleaner.cs[152-228]
docs/docs/configuration/queue-cleaner/index.mdx[123-131]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`QueueCleaner` applies `FailedImport.ChangeCategory` unconditionally. When it’s enabled, `removeFromClient` becomes `false` for *all* failed-import removals, including non-torrent queue items, which prevents deleting those items from the download client.
### Issue Context
The job already computes `isTorrent` based on `record.Protocol`, and only torrents have an associated “category” concept in this feature (docs: “change the torrent's category to the post-import category”).
### Fix Focus Areas
- code/backend/Cleanuparr.Infrastructure/Features/Jobs/QueueCleaner.cs[152-239]
### Proposed fix approach
- In the failed-import branch, compute `changeCategory` as something like:
- `bool changeCategory = isTorrent && downloadCheckResult.Found && queueCleanerConfig.FailedImport.ChangeCategory;`
- (At minimum: `isTorrent && queueCleanerConfig.FailedImport.ChangeCategory`.)
- Compute `removeFromClient` so that non-torrent items behave exactly as before (i.e., don’t let the ChangeCategory flag force `removeFromClient=false` for non-torrents).
- Consider adding/adjusting a unit/integration test covering a non-torrent failed-import item with `FailedImport.ChangeCategory=true` to ensure it still deletes from client.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

@codecov

codecov Bot commented May 4, 2026

Copy link
Copy Markdown

@greptile-apps

greptile-apps Bot commented May 4, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds a ChangeCategory option to queue-cleaner rules (stall, slow, failed-import) that, when enabled, instructs the *arr instance to move the download to its post-import category instead of deleting it from the download client. The feature is wired end-to-end: persistence migration, model validation, evaluator tuple extension, QueueCleaner logic, ArrClient query-string generation, and Angular UI signals/effects.

  • Backend: GetQueueDeleteUrlQuery consolidated from per-subclass overrides into a single virtual base implementation that now accepts changeCategory; QueueRuleEvaluator correctly gates deleteFromClient on !rule.ChangeCategory; validation prevents ChangeCategory && DeletePrivate(TorrentsFromClient) from being set simultaneously.
  • Frontend: Reactive effect hooks auto-clear deletePrivate when changeCategory is toggled on, and save functions coerce deletePrivateTorrentsFromClient to false when changeCategory is true — matching the backend validation constraint.
  • Tests: New unit tests for QueueRuleEvaluator and integration tests for QueueCleaner cover the happy path, though one test constructs a DownloadCheckResult with an impossible DeleteFromClient=true && ChangeCategory=true combination that can never be produced by the real evaluator.

Confidence Score: 4/5

The core category-change path is correctly implemented, but two unresolved issues from previous review rounds remain open: the Seeker search queue is populated even when changeCategory=true (causing a duplicate download), and usenet queue items are silently left in the usenet client with no *arr tracking when changeCategory=true (because the *arr API's changeCategory parameter is torrent-only).

The feature logic from evaluator through to the *arr API query string is sound, and the mutual-exclusion validation between changeCategory and deletePrivate is properly enforced on both frontend and backend. The two open issues from prior review iterations — Seeker enqueuing a fresh search while the original torrent keeps downloading, and usenet downloads being abandoned in the client — are genuine behavioral defects on the changed paths that should be addressed before merging.

QueueItemRemover.cs (Seeker search not guarded on changeCategory) and QueueCleaner.cs (no protocol guard for usenet when changeCategory=true)

Important Files Changed

Filename Overview
code/backend/Cleanuparr.Infrastructure/Features/Jobs/QueueCleaner.cs Adds changeCategory flag to both the stall/slow and failed-import removal paths; gates removeFromClient on !changeCategory; logic is correct but protocol guard for usenet is absent (flagged in prior round)
code/backend/Cleanuparr.Infrastructure/Features/DownloadRemover/QueueItemRemover.cs Passes changeCategory through to arrClient.DeleteQueueItemAsync; Seeker search is enqueued without checking changeCategory (flagged in prior round)
code/backend/Cleanuparr.Infrastructure/Features/Arr/ArrClient.cs GetQueueDeleteUrlQuery consolidated from abstract/per-subclass to a single virtual implementation; query string correctly sets changeCategory and removeFromClient; log message updated for all three cases
code/backend/Cleanuparr.Infrastructure/Services/QueueRuleEvaluator.cs Return tuples extended with ChangeCategory; deleteFromClient correctly gated on !rule.ChangeCategory for stall, slow-speed and slow-time paths
code/backend/Cleanuparr.Persistence/Models/Configuration/QueueCleaner/QueueRule.cs ChangeCategory bool added with default false; validation throws when both ChangeCategory and DeletePrivateTorrentsFromClient are true
code/backend/Cleanuparr.Persistence/Models/Configuration/QueueCleaner/FailedImportConfig.cs ChangeCategory bool added; validation prevents ChangeCategory && DeletePrivate simultaneously; logic is correct
code/frontend/src/app/features/settings/queue-cleaner/queue-cleaner.component.ts Three new signals and reactive effects for failedChangeCategory, stallChangeCategory, slowChangeCategory; effects auto-clear deletePrivate when changeCategory is toggled on; save functions coerce deletePrivate to false when changeCategory is true
code/backend/Cleanuparr.Infrastructure.Tests/Features/Jobs/QueueCleanerTests.cs Two new ChangeCategory integration tests; one test sets DeleteFromClient=true and ChangeCategory=true simultaneously in the mock, an impossible combination in production that should still be ChangeCategory=true only
code/backend/Cleanuparr.Persistence/Migrations/Data/20260504193343_AddQueueCleanerChangeCategory.cs Adds change_category to stall_rules, slow_rules, and failed_import_change_category to queue_cleaner_configs; all defaulting to false; Down() correctly reverses each column

Sequence Diagram

sequenceDiagram
    participant QC as QueueCleaner
    participant Eval as QueueRuleEvaluator
    participant GH as GenericHandler
    participant QIR as QueueItemRemover
    participant Arr as ArrClient

    QC->>Eval: EvaluateStallRulesAsync(torrent)
    Eval-->>QC: (ShouldRemove, Reason, DeleteFromClient, ChangeCategory)
    QC->>QC: changeCategory = result.ChangeCategory
    QC->>GH: PublishQueueItemRemoveRequest(..., changeCategory)
    GH->>QIR: RemoveQueueItemAsync(request)
    QIR->>Arr: DeleteQueueItemAsync(..., removeFromClient, changeCategory)
    Arr->>Arr: GetQueueDeleteUrlQuery(removeFromClient, changeCategory)
    Note over Arr: changeCategory=true → blocklist=true&skipRedownload=true&changeCategory=true&removeFromClient=false
    Arr-->>QIR: HTTP DELETE response
    QIR->>QIR: PublishQueueItemDeleted(RemoveFromClient=false, reason)
    Note over QIR: ChangeCategory not passed — fires removed-from-arr notification
Loading

Reviews (2): Last reviewed commit: "add stalled-rule ChangeCategory tests fo..." | Re-trigger Greptile

@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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
code/backend/Cleanuparr.Infrastructure/Features/Arr/RadarrClient.cs (1)

45-59: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Extract identical GetQueueDeleteUrlQuery implementation to base ArrClient — currently duplicated across 6 client classes.

This method is copy-pasted identically in RadarrClient, SonarrClient, ReadarrClient, LidarrClient, WhisparrV2Client, and WhisparrV3Client. All share the same branching logic. Convert the abstract method in ArrClient to a virtual implementation with a default body, then remove all six overrides. Subclasses can still override if their API diverges.

♻️ Suggested change in `ArrClient.cs`
-protected abstract string GetQueueDeleteUrlQuery(bool removeFromClient, bool changeCategory);
+protected virtual string GetQueueDeleteUrlQuery(bool removeFromClient, bool changeCategory)
+{
+    string query = "blocklist=true&skipRedownload=true&";
+
+    if (changeCategory)
+    {
+        query += "changeCategory=true&removeFromClient=false";
+        return query;
+    }
+
+    query += "changeCategory=false";
+    query += removeFromClient ? "&removeFromClient=true" : "&removeFromClient=false";
+    return query;
+}

Then remove the identical overrides from all six client classes.

🤖 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 `@code/backend/Cleanuparr.Infrastructure/Features/Arr/RadarrClient.cs` around
lines 45 - 59, The duplicated GetQueueDeleteUrlQuery method appears verbatim in
RadarrClient, SonarrClient, ReadarrClient, LidarrClient, WhisparrV2Client and
WhisparrV3Client; move that implementation into the base ArrClient by changing
the abstract declaration to a virtual method with the current body, then delete
the identical overrides from the six subclasses so they inherit the shared logic
(they can still override later if needed). Ensure the method signature in
ArrClient matches the existing overrides (GetQueueDeleteUrlQuery(bool
removeFromClient, bool changeCategory)) and run tests to confirm no behavioral
changes.
♻️ Duplicate comments (1)
code/backend/Cleanuparr.Infrastructure/Features/Arr/WhisparrV2Client.cs (1)

47-61: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Duplicate of RadarrClient.GetQueueDeleteUrlQuery — see the refactor suggestion on RadarrClient.cs.

The implementation here is byte-for-byte identical to RadarrClient's override. Consolidating into ArrClient as described in the RadarrClient.cs comment resolves both.

🤖 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 `@code/backend/Cleanuparr.Infrastructure/Features/Arr/WhisparrV2Client.cs`
around lines 47 - 61, This override of GetQueueDeleteUrlQuery in
WhisparrV2Client is identical to RadarrClient.GetQueueDeleteUrlQuery; remove the
duplicate from WhisparrV2Client and implement the shared logic once in the base
ArrClient (e.g., add a protected virtual or concrete GetQueueDeleteUrlQuery
implementation there), then have both WhisparrV2Client and RadarrClient
inherit/override only if behavior diverges; reference the method name
GetQueueDeleteUrlQuery and the classes WhisparrV2Client, RadarrClient, and
ArrClient when making the refactor.
🧹 Nitpick comments (6)
code/backend/Cleanuparr.Infrastructure.Tests/Features/DownloadClient/DelugeServiceTests.cs (1)

416-498: ⚡ Quick win

Same result.ChangeCategory assertion gap as in UTorrentServiceTests.cs.

Add result.ChangeCategory.ShouldBeFalse() to both SlowDownload_MatchesRule_RemovesFromQueue (line 453) and StalledDownload_MatchesRule_RemovesFromQueue (line 497), and a ChangeCategory = true propagation test.

🤖 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
`@code/backend/Cleanuparr.Infrastructure.Tests/Features/DownloadClient/DelugeServiceTests.cs`
around lines 416 - 498, The tests SlowDownload_MatchesRule_RemovesFromQueue and
StalledDownload_MatchesRule_RemovesFromQueue are missing assertions for
result.ChangeCategory; add result.ChangeCategory.ShouldBeFalse() to both tests
to mirror UTorrentServiceTests expectations, and add an additional unit test
that sets the rule evaluator to return ChangeCategory = true (e.g., Arrange
EvaluateSlowRulesAsync / EvaluateStallRulesAsync to return (true,
DeleteReason.*, true, true)), call ShouldRemoveFromArrQueueAsync on the SUT, and
assert result.ChangeCategory.ShouldBeTrue() along with the existing DeleteReason
and DeleteFromClient checks so ChangeCategory propagation is covered.
code/backend/Cleanuparr.Infrastructure.Tests/Features/DownloadClient/UTorrentServiceTests.cs (1)

521-613: ⚡ Quick win

result.ChangeCategory is never asserted — ChangeCategory propagation is untested.

SlowDownload_MatchesRule_RemovesFromQueue and StalledDownload_MatchesRule_RemovesFromQueue both verify ShouldRemove, DeleteReason, and DeleteFromClient, but neither asserts the new ChangeCategory field. If the assignment at line 73 of UTorrentServiceQC.cs were accidentally removed, all tests would still pass. The same gap exists in TransmissionServiceTests.cs and DelugeServiceTests.cs.

Consider adding:

  1. result.ChangeCategory.ShouldBeFalse() to the existing ChangeCategory = false scenarios.
  2. A new scenario where the evaluator returns ChangeCategory = true and the result propagates it correctly.
✅ Suggested additions to existing tests + new test
 result.ShouldRemove.ShouldBeTrue();
 result.DeleteReason.ShouldBe(DeleteReason.SlowSpeed);
 result.DeleteFromClient.ShouldBeTrue();
+result.ChangeCategory.ShouldBeFalse();
 result.ShouldRemove.ShouldBeTrue();
 result.DeleteReason.ShouldBe(DeleteReason.Stalled);
 result.DeleteFromClient.ShouldBeTrue();
+result.ChangeCategory.ShouldBeFalse();

New test inside ShouldRemoveFromArrQueueAsync_SlowAndStalledScenarios:

[Fact]
public async Task SlowDownload_ChangeCategoryRule_PropagatesChangeCategoryFlag()
{
    const string hash = "test-hash";
    var sut = _fixture.CreateSut();

    var torrentItem = new UTorrentItem
    {
        Hash = hash, Name = "Test Torrent", Status = 9, DownloadSpeed = 1000
    };
    var torrentProperties = new UTorrentProperties { Hash = hash, Pex = 1, Trackers = "" };

    _fixture.ClientWrapper.GetTorrentAsync(hash).Returns(torrentItem);
    _fixture.ClientWrapper.GetTorrentPropertiesAsync(hash).Returns(torrentProperties);
    _fixture.ClientWrapper.GetTorrentFilesAsync(hash).Returns(new List<UTorrentFile>
    {
        new UTorrentFile { Name = "file1.mkv", Priority = 1, Index = 0, Size = 1000, Downloaded = 500 }
    });

    // ChangeCategory=true → deleteFromClient must be false (rule gates it)
    _fixture.RuleEvaluator
        .EvaluateSlowRulesAsync(Arg.Any<UTorrentItemWrapper>())
        .Returns((true, DeleteReason.SlowSpeed, false, true));

    var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());

    result.ShouldRemove.ShouldBeTrue();
    result.DeleteReason.ShouldBe(DeleteReason.SlowSpeed);
    result.DeleteFromClient.ShouldBeFalse();
    result.ChangeCategory.ShouldBeTrue();
}
🤖 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
`@code/backend/Cleanuparr.Infrastructure.Tests/Features/DownloadClient/UTorrentServiceTests.cs`
around lines 521 - 613, The tests for UTorrentService
(SlowDownload_MatchesRule_RemovesFromQueue and
StalledDownload_MatchesRule_RemovesFromQueue) are missing assertions for the new
ChangeCategory propagation; update those tests to assert
result.ChangeCategory.ShouldBeFalse() (since the RuleEvaluator returns
ChangeCategory=false), and add a new test that stubs
RuleEvaluator.EvaluateSlowRulesAsync (or EvaluateStallRulesAsync) to return
ChangeCategory=true and verifies that ShouldRemove is true, DeleteReason
matches, ChangeCategory is true and DeleteFromClient is false (rule gates
deletion when ChangeCategory=true). Apply the same additional ChangeCategory
assertions and a positive ChangeCategory propagation test pattern to
TransmissionServiceTests and DelugeServiceTests, targeting the
ShouldRemoveFromArrQueueAsync flows and the
EvaluateSlowRulesAsync/EvaluateStallRulesAsync call sites.
code/backend/Cleanuparr.Infrastructure.Tests/Features/DownloadClient/TransmissionServiceTests.cs (1)

617-729: ⚡ Quick win

Same result.ChangeCategory assertion gap as in UTorrentServiceTests.cs.

Add result.ChangeCategory.ShouldBeFalse() to both SlowDownload_MatchesRule_RemovesFromQueue (line 668) and StalledDownload_MatchesRule_RemovesFromQueue (line 727), and a ChangeCategory = true propagation test.

🤖 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
`@code/backend/Cleanuparr.Infrastructure.Tests/Features/DownloadClient/TransmissionServiceTests.cs`
around lines 617 - 729, The two TransmissionServiceTests
(SlowDownload_MatchesRule_RemovesFromQueue and
StalledDownload_MatchesRule_RemovesFromQueue) are missing assertions for
ChangeCategory; add result.ChangeCategory.ShouldBeFalse() to both tests after
the other ShouldBe* assertions, and add an additional unit test that stubs
RuleEvaluator.EvaluateSlowRulesAsync (or EvaluateStallRulesAsync) to return
(true, DeleteReason.SlowSpeed, true, true) (or the stalled equivalent) and
asserts that result.ChangeCategory is true so ChangeCategory propagation is
covered; locate the tests by their method names and update assertions
accordingly.
code/backend/Cleanuparr.Infrastructure.Tests/Features/DownloadRemover/QueueItemRemoverTests.cs (1)

426-444: ⚡ Quick win

Add a changeCategory parameter to CreateRemoveRequest and a corresponding forwarding test.

The helper always produces a ChangeCategory = false request (unset bool default), so ChangeCategory = true is never exercised. There is a RemoveQueueItemAsync_PassesCorrectRemoveFromClientFlag theory test but no analogous theory for ChangeCategory.

✅ Suggested additions
 private QueueItemRemoveRequest<SearchItem> CreateRemoveRequest(
     InstanceType instanceType = InstanceType.Sonarr,
     bool removeFromClient = true,
     DeleteReason deleteReason = DeleteReason.Stalled,
-    bool skipSearch = false)
+    bool skipSearch = false,
+    bool changeCategory = false)
 {
     var instance = GetOrCreateArrInstance(instanceType);

     return new QueueItemRemoveRequest<SearchItem>
     {
         Instance = instance,
         SearchItem = new SearchItem { Id = 123 },
         Record = CreateQueueRecord(),
         RemoveFromClient = removeFromClient,
+        ChangeCategory = changeCategory,
         DeleteReason = deleteReason,
         SkipSearch = skipSearch,
         JobRunId = _jobRunId
     };
 }

And add a forwarding theory alongside RemoveQueueItemAsync_PassesCorrectRemoveFromClientFlag:

[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task RemoveQueueItemAsync_PassesCorrectChangeCategoryFlag(bool changeCategory)
{
    var request = CreateRemoveRequest(changeCategory: changeCategory);

    await _queueItemRemover.RemoveQueueItemAsync(request);

    await _arrClient.Received(1).DeleteQueueItemAsync(
        Arg.Any<ArrInstance>(),
        Arg.Any<QueueRecord>(),
        Arg.Any<bool>(),
        Arg.Is<bool>(x => x == changeCategory),
        Arg.Any<DeleteReason>());
}
🤖 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
`@code/backend/Cleanuparr.Infrastructure.Tests/Features/DownloadRemover/QueueItemRemoverTests.cs`
around lines 426 - 444, Add a new bool parameter changeCategory (default false)
to CreateRemoveRequest and set the returned QueueItemRemoveRequest's
ChangeCategory property to that value so tests can exercise both true/false;
then add a forwarding theory test
RemoveQueueItemAsync_PassesCorrectChangeCategoryFlag that calls
CreateRemoveRequest(changeCategory: changeCategory), invokes
_queueItemRemover.RemoveQueueItemAsync(request), and verifies
_arrClient.Received(1).DeleteQueueItemAsync(..., Arg.Is<bool>(x => x ==
changeCategory), ...) to assert the flag is forwarded.
code/backend/Cleanuparr.Infrastructure/Features/Arr/LidarrClient.cs (1)

45-59: ⚡ Quick win

GetQueueDeleteUrlQuery is duplicated across all ArrClient subclasses — consolidate into the base.

The body of GetQueueDeleteUrlQuery is byte-for-byte identical in LidarrClient, SonarrClient, and RadarrClient (per the context snippet). Since all *arr apps accept the same query parameters ("All inputs are the same in all apps"), there is no per-client variation. The abstract method in ArrClient should become virtual with this shared body as the default; subclasses that genuinely diverge can still override.

♻️ Suggested change in `ArrClient.cs`
-protected abstract string GetQueueDeleteUrlQuery(bool removeFromClient, bool changeCategory);
+protected virtual string GetQueueDeleteUrlQuery(bool removeFromClient, bool changeCategory)
+{
+    string query = "blocklist=true&skipRedownload=true&";
+
+    if (changeCategory)
+    {
+        query += "changeCategory=true&removeFromClient=false";
+        return query;
+    }
+
+    query += "changeCategory=false";
+    query += removeFromClient ? "&removeFromClient=true" : "&removeFromClient=false";
+
+    return query;
+}

Then remove the identical override from LidarrClient, SonarrClient, and RadarrClient (and any other client with the same body).

🤖 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 `@code/backend/Cleanuparr.Infrastructure/Features/Arr/LidarrClient.cs` around
lines 45 - 59, The GetQueueDeleteUrlQuery implementation is identical across
ArrClient subclasses; move the shared body into the base ArrClient by changing
the abstract declaration of GetQueueDeleteUrlQuery to a virtual method that
returns the same query string ("blocklist=true&skipRedownload=true&..." with
changeCategory/removeFromClient logic) so it becomes the default behavior in
ArrClient, then remove the duplicate overrides from LidarrClient, SonarrClient,
RadarrClient (and any other subclass with the same body); keep the method
virtual so subclasses that need different behavior can still override it.
code/backend/Cleanuparr.Infrastructure.Tests/Features/Jobs/QueueCleanerTests.cs (1)

1387-1413: ⚡ Quick win

The first ChangeCategory test's assertion on RemoveFromClient == false is not conclusive.

DownloadCheckResult is constructed without setting DeleteFromClient (it defaults to false). This means RemoveFromClient=false in the assertion would be satisfied even if the ChangeCategory override logic were removed entirely. The test doesn't actually prove that ChangeCategory=true forces RemoveFromClient to false.

Compare this with the second ChangeCategory test (lines 1458–1466), which correctly uses DeleteFromClient = true alongside ChangeCategory = true so the RemoveFromClient == false assertion is only satisfiable due to the override.

♻️ Suggested fix — make the FailedImport path conclusive

Add DeleteFromClient = true to the DownloadCheckResult (or use whatever mechanism controls RemoveFromClient in the failed-import path) so the assertion proves the override:

         .Returns(new DownloadCheckResult
         {
             Found = true,
             ShouldRemove = false,
-            IsPrivate = true
+            IsPrivate = true,
+            DeleteFromClient = true  // ensure RemoveFromClient would be true without ChangeCategory override
         });
🤖 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
`@code/backend/Cleanuparr.Infrastructure.Tests/Features/Jobs/QueueCleanerTests.cs`
around lines 1387 - 1413, The test is inconclusive because the mocked
DownloadCheckResult leaves DeleteFromClient at its default false, so asserting
RemoveFromClient == false doesn't prove ChangeCategory forces the override;
update the mock returned by ShouldRemoveFromArrQueueAsync to set
DeleteFromClient = true on the DownloadCheckResult (in the arrangement before
calling CreateSut/ExecuteAsync) so the assertion on
QueueItemRemoveRequest<SeriesSearchItem> (r => r.ChangeCategory == true &&
r.RemoveFromClient == false && r.DeleteReason == DeleteReason.FailedImport) only
passes if the ChangeCategory override logic actually flips RemoveFromClient to
false.
🤖 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 `@code/backend/Cleanuparr.Infrastructure/Features/Arr/ReadarrClient.cs`:
- Around line 47-53: The query string in ReadarrClient.cs is built with
"blocklist=true&skipRedownload=true&" before checking changeCategory, causing
blocklist to be sent even when changeCategory is true; update the logic in the
method that constructs the string (the local variable query and the
changeCategory branch) so that when changeCategory == true the returned query
excludes blocklist (e.g., only include
changeCategory=true&removeFromClient=false or skipRedownload as appropriate),
otherwise include blocklist=true&skipRedownload=true; ensure the code paths that
return query (the changeCategory branch and the default path) produce the
correct mutually exclusive parameter sets.

In `@code/backend/Cleanuparr.Infrastructure/Features/Arr/WhisparrV3Client.cs`:
- Around line 48-54: The query string for WhisparrV3Client is always initialized
with "blocklist=true&skipRedownload=true&", so when changeCategory is true the
code returns "blocklist=...&changeCategory=..." and wrongly blocklists items;
update the logic in WhisparrV3Client (where the local variable query is built
and the changeCategory branch returns) to mirror RadarrClient: do not include
the blocklist parameter when changeCategory is true—build the base query
conditionally (e.g., initialize query without blocklist when changeCategory) or
remove the blocklist token before returning the changeCategory path so the
returned query contains changeCategory=true&removeFromClient=false plus
skipRedownload if appropriate.

---

Outside diff comments:
In `@code/backend/Cleanuparr.Infrastructure/Features/Arr/RadarrClient.cs`:
- Around line 45-59: The duplicated GetQueueDeleteUrlQuery method appears
verbatim in RadarrClient, SonarrClient, ReadarrClient, LidarrClient,
WhisparrV2Client and WhisparrV3Client; move that implementation into the base
ArrClient by changing the abstract declaration to a virtual method with the
current body, then delete the identical overrides from the six subclasses so
they inherit the shared logic (they can still override later if needed). Ensure
the method signature in ArrClient matches the existing overrides
(GetQueueDeleteUrlQuery(bool removeFromClient, bool changeCategory)) and run
tests to confirm no behavioral changes.

---

Duplicate comments:
In `@code/backend/Cleanuparr.Infrastructure/Features/Arr/WhisparrV2Client.cs`:
- Around line 47-61: This override of GetQueueDeleteUrlQuery in WhisparrV2Client
is identical to RadarrClient.GetQueueDeleteUrlQuery; remove the duplicate from
WhisparrV2Client and implement the shared logic once in the base ArrClient
(e.g., add a protected virtual or concrete GetQueueDeleteUrlQuery implementation
there), then have both WhisparrV2Client and RadarrClient inherit/override only
if behavior diverges; reference the method name GetQueueDeleteUrlQuery and the
classes WhisparrV2Client, RadarrClient, and ArrClient when making the refactor.

---

Nitpick comments:
In
`@code/backend/Cleanuparr.Infrastructure.Tests/Features/DownloadClient/DelugeServiceTests.cs`:
- Around line 416-498: The tests SlowDownload_MatchesRule_RemovesFromQueue and
StalledDownload_MatchesRule_RemovesFromQueue are missing assertions for
result.ChangeCategory; add result.ChangeCategory.ShouldBeFalse() to both tests
to mirror UTorrentServiceTests expectations, and add an additional unit test
that sets the rule evaluator to return ChangeCategory = true (e.g., Arrange
EvaluateSlowRulesAsync / EvaluateStallRulesAsync to return (true,
DeleteReason.*, true, true)), call ShouldRemoveFromArrQueueAsync on the SUT, and
assert result.ChangeCategory.ShouldBeTrue() along with the existing DeleteReason
and DeleteFromClient checks so ChangeCategory propagation is covered.

In
`@code/backend/Cleanuparr.Infrastructure.Tests/Features/DownloadClient/TransmissionServiceTests.cs`:
- Around line 617-729: The two TransmissionServiceTests
(SlowDownload_MatchesRule_RemovesFromQueue and
StalledDownload_MatchesRule_RemovesFromQueue) are missing assertions for
ChangeCategory; add result.ChangeCategory.ShouldBeFalse() to both tests after
the other ShouldBe* assertions, and add an additional unit test that stubs
RuleEvaluator.EvaluateSlowRulesAsync (or EvaluateStallRulesAsync) to return
(true, DeleteReason.SlowSpeed, true, true) (or the stalled equivalent) and
asserts that result.ChangeCategory is true so ChangeCategory propagation is
covered; locate the tests by their method names and update assertions
accordingly.

In
`@code/backend/Cleanuparr.Infrastructure.Tests/Features/DownloadClient/UTorrentServiceTests.cs`:
- Around line 521-613: The tests for UTorrentService
(SlowDownload_MatchesRule_RemovesFromQueue and
StalledDownload_MatchesRule_RemovesFromQueue) are missing assertions for the new
ChangeCategory propagation; update those tests to assert
result.ChangeCategory.ShouldBeFalse() (since the RuleEvaluator returns
ChangeCategory=false), and add a new test that stubs
RuleEvaluator.EvaluateSlowRulesAsync (or EvaluateStallRulesAsync) to return
ChangeCategory=true and verifies that ShouldRemove is true, DeleteReason
matches, ChangeCategory is true and DeleteFromClient is false (rule gates
deletion when ChangeCategory=true). Apply the same additional ChangeCategory
assertions and a positive ChangeCategory propagation test pattern to
TransmissionServiceTests and DelugeServiceTests, targeting the
ShouldRemoveFromArrQueueAsync flows and the
EvaluateSlowRulesAsync/EvaluateStallRulesAsync call sites.

In
`@code/backend/Cleanuparr.Infrastructure.Tests/Features/DownloadRemover/QueueItemRemoverTests.cs`:
- Around line 426-444: Add a new bool parameter changeCategory (default false)
to CreateRemoveRequest and set the returned QueueItemRemoveRequest's
ChangeCategory property to that value so tests can exercise both true/false;
then add a forwarding theory test
RemoveQueueItemAsync_PassesCorrectChangeCategoryFlag that calls
CreateRemoveRequest(changeCategory: changeCategory), invokes
_queueItemRemover.RemoveQueueItemAsync(request), and verifies
_arrClient.Received(1).DeleteQueueItemAsync(..., Arg.Is<bool>(x => x ==
changeCategory), ...) to assert the flag is forwarded.

In
`@code/backend/Cleanuparr.Infrastructure.Tests/Features/Jobs/QueueCleanerTests.cs`:
- Around line 1387-1413: The test is inconclusive because the mocked
DownloadCheckResult leaves DeleteFromClient at its default false, so asserting
RemoveFromClient == false doesn't prove ChangeCategory forces the override;
update the mock returned by ShouldRemoveFromArrQueueAsync to set
DeleteFromClient = true on the DownloadCheckResult (in the arrangement before
calling CreateSut/ExecuteAsync) so the assertion on
QueueItemRemoveRequest<SeriesSearchItem> (r => r.ChangeCategory == true &&
r.RemoveFromClient == false && r.DeleteReason == DeleteReason.FailedImport) only
passes if the ChangeCategory override logic actually flips RemoveFromClient to
false.

In `@code/backend/Cleanuparr.Infrastructure/Features/Arr/LidarrClient.cs`:
- Around line 45-59: The GetQueueDeleteUrlQuery implementation is identical
across ArrClient subclasses; move the shared body into the base ArrClient by
changing the abstract declaration of GetQueueDeleteUrlQuery to a virtual method
that returns the same query string ("blocklist=true&skipRedownload=true&..."
with changeCategory/removeFromClient logic) so it becomes the default behavior
in ArrClient, then remove the duplicate overrides from LidarrClient,
SonarrClient, RadarrClient (and any other subclass with the same body); keep the
method virtual so subclasses that need different behavior can still override it.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 94d878df-0ecd-4f5a-bb5d-ddc68847f31d

📥 Commits

Reviewing files that changed from the base of the PR and between 48c36fa and 851b08f.

📒 Files selected for processing (45)
  • code/backend/Cleanuparr.Api/Features/QueueCleaner/Contracts/Requests/QueueRuleDto.cs
  • code/backend/Cleanuparr.Api/Features/QueueCleaner/Controllers/QueueRulesController.cs
  • code/backend/Cleanuparr.Infrastructure.Tests/Features/DownloadClient/DelugeServiceTests.cs
  • code/backend/Cleanuparr.Infrastructure.Tests/Features/DownloadClient/QBitServiceTests.cs
  • code/backend/Cleanuparr.Infrastructure.Tests/Features/DownloadClient/RTorrentServiceTests.cs
  • code/backend/Cleanuparr.Infrastructure.Tests/Features/DownloadClient/TransmissionServiceTests.cs
  • code/backend/Cleanuparr.Infrastructure.Tests/Features/DownloadClient/UTorrentServiceTests.cs
  • code/backend/Cleanuparr.Infrastructure.Tests/Features/DownloadRemover/QueueItemRemoverTests.cs
  • code/backend/Cleanuparr.Infrastructure.Tests/Features/Jobs/Integration/MalwareBlockerIntegrationTests.cs
  • code/backend/Cleanuparr.Infrastructure.Tests/Features/Jobs/Integration/QueueCleanerIntegrationTests.cs
  • code/backend/Cleanuparr.Infrastructure.Tests/Features/Jobs/QueueCleanerTests.cs
  • code/backend/Cleanuparr.Infrastructure.Tests/Services/QueueRuleEvaluatorTests.cs
  • code/backend/Cleanuparr.Infrastructure/Features/Arr/ArrClient.cs
  • code/backend/Cleanuparr.Infrastructure/Features/Arr/Interfaces/IArrClient.cs
  • code/backend/Cleanuparr.Infrastructure/Features/Arr/LidarrClient.cs
  • code/backend/Cleanuparr.Infrastructure/Features/Arr/RadarrClient.cs
  • code/backend/Cleanuparr.Infrastructure/Features/Arr/ReadarrClient.cs
  • code/backend/Cleanuparr.Infrastructure/Features/Arr/SonarrClient.cs
  • code/backend/Cleanuparr.Infrastructure/Features/Arr/WhisparrV2Client.cs
  • code/backend/Cleanuparr.Infrastructure/Features/Arr/WhisparrV3Client.cs
  • code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/Deluge/DelugeServiceQC.cs
  • code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/DownloadCheckResult.cs
  • code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/QBittorrent/QBitServiceQC.cs
  • code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/RTorrent/RTorrentServiceQC.cs
  • code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/Transmission/TransmissionServiceQC.cs
  • code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/UTorrent/UTorrentServiceQC.cs
  • code/backend/Cleanuparr.Infrastructure/Features/DownloadRemover/Models/QueueItemRemoveRequest.cs
  • code/backend/Cleanuparr.Infrastructure/Features/DownloadRemover/QueueItemRemover.cs
  • code/backend/Cleanuparr.Infrastructure/Features/Jobs/GenericHandler.cs
  • code/backend/Cleanuparr.Infrastructure/Features/Jobs/QueueCleaner.cs
  • code/backend/Cleanuparr.Infrastructure/Services/Interfaces/IQueueRuleEvaluator.cs
  • code/backend/Cleanuparr.Infrastructure/Services/QueueRuleEvaluator.cs
  • code/backend/Cleanuparr.Persistence.Tests/Models/Configuration/QueueCleaner/FailedImportConfigTests.cs
  • code/backend/Cleanuparr.Persistence.Tests/Models/Configuration/QueueCleaner/QueueRuleTests.cs
  • code/backend/Cleanuparr.Persistence/Migrations/Data/20260504193343_AddQueueCleanerChangeCategory.Designer.cs
  • code/backend/Cleanuparr.Persistence/Migrations/Data/20260504193343_AddQueueCleanerChangeCategory.cs
  • code/backend/Cleanuparr.Persistence/Migrations/Data/DataContextModelSnapshot.cs
  • code/backend/Cleanuparr.Persistence/Models/Configuration/QueueCleaner/FailedImportConfig.cs
  • code/backend/Cleanuparr.Persistence/Models/Configuration/QueueCleaner/QueueRule.cs
  • code/frontend/src/app/core/services/documentation.service.ts
  • code/frontend/src/app/features/settings/queue-cleaner/queue-cleaner.component.html
  • code/frontend/src/app/features/settings/queue-cleaner/queue-cleaner.component.ts
  • code/frontend/src/app/shared/models/queue-cleaner-config.model.ts
  • code/frontend/src/app/shared/models/queue-rule.model.ts
  • docs/docs/configuration/queue-cleaner/index.mdx

Comment thread code/backend/Cleanuparr.Infrastructure/Features/Arr/ReadarrClient.cs Outdated
Comment thread code/backend/Cleanuparr.Infrastructure/Features/Arr/WhisparrV3Client.cs Outdated

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

🧹 Nitpick comments (3)
code/backend/Cleanuparr.Infrastructure/Features/Arr/ArrClient.cs (1)

162-168: ⚡ Quick win

Add XML docs for the updated public API method.

Line 162 introduces a changed public signature (changeCategory) but this method has no XML documentation (or /// <inheritdoc/>). Please add docs so the new parameter contract is discoverable from implementation as well.

As per coding guidelines, "Add XML documentation comments for public APIs in C#".

🤖 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 `@code/backend/Cleanuparr.Infrastructure/Features/Arr/ArrClient.cs` around
lines 162 - 168, Add XML documentation comments for the public method
DeleteQueueItemAsync so the new parameter contract is discoverable: add a
<summary> describing what DeleteQueueItemAsync does, and <param> tags for
arrInstance, record, removeFromClient, changeCategory (explain what true/false
means), and deleteReason; include a <returns> describing the returned Task and
any <exception> tags if the method throws specific exceptions. Ensure the doc
sits immediately above the DeleteQueueItemAsync declaration and either fully
documents the parameters or uses <inheritdoc/> with an explicit <param
name="changeCategory"> entry to cover the new parameter.
code/backend/Cleanuparr.Infrastructure.Tests/Features/DownloadClient/UTorrentServiceTests.cs (1)

568-615: ⚡ Quick win

Add a stalled-rule variant where ChangeCategory = true for symmetry.

You covered this for slow rules, but not for stalled rules in this class. A mirrored test would close the remaining path-level gap.

✅ Suggested test addition
+        [Fact]
+        public async Task StalledDownload_RuleWithChangeCategory_PropagatesChangeCategoryFlag()
+        {
+            const string hash = "test-hash";
+            var sut = _fixture.CreateSut();
+
+            var torrentItem = new UTorrentItem
+            {
+                Hash = hash,
+                Name = "Test Torrent",
+                Status = 9,
+                DownloadSpeed = 0,
+                ETA = 0
+            };
+
+            var torrentProperties = new UTorrentProperties
+            {
+                Hash = hash,
+                Pex = 1,
+                Trackers = ""
+            };
+
+            _fixture.ClientWrapper.GetTorrentAsync(hash).Returns(torrentItem);
+            _fixture.ClientWrapper.GetTorrentPropertiesAsync(hash).Returns(torrentProperties);
+            _fixture.ClientWrapper.GetTorrentFilesAsync(hash).Returns(new List<UTorrentFile>
+            {
+                new UTorrentFile { Name = "file1.mkv", Priority = 1, Index = 0, Size = 1000, Downloaded = 500 }
+            });
+
+            _fixture.RuleEvaluator
+                .EvaluateStallRulesAsync(Arg.Any<UTorrentItemWrapper>())
+                .Returns((true, DeleteReason.Stalled, false, true));
+
+            var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
+
+            result.ShouldRemove.ShouldBeTrue();
+            result.DeleteReason.ShouldBe(DeleteReason.Stalled);
+            result.DeleteFromClient.ShouldBeFalse();
+            result.ChangeCategory.ShouldBeTrue();
+        }
🤖 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
`@code/backend/Cleanuparr.Infrastructure.Tests/Features/DownloadClient/UTorrentServiceTests.cs`
around lines 568 - 615, Add a mirrored unit test in UTorrentServiceTests that
exercises the stalled-rule path where ChangeCategory is true: create a new test
similar to StalledDownload_MatchesRule_RemovesFromQueue but have the mocked
RuleEvaluator.EvaluateStallRulesAsync(Arg.Any<UTorrentItemWrapper>()) return
(true, DeleteReason.Stalled, true, true) and assert result.ShouldRemove == true,
result.DeleteReason == DeleteReason.Stalled, result.DeleteFromClient == true and
result.ChangeCategory == true; ensure the test uses the same setup
(UTorrentItem, UTorrentProperties, UTorrentFile list) and calls
ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>()) on the SUT.
code/backend/Cleanuparr.Infrastructure.Tests/Features/DownloadClient/QBitServiceTests.cs (1)

836-950: ⚡ Quick win

Missing stall-path equivalent of SlowDownload_RuleWithChangeCategory_PropagatesChangeCategoryFlag.

A StalledDownload_RuleWithChangeCategory_PropagatesChangeCategoryFlag test is absent. Since stall-rule evaluation is a separate code path through EvaluateStallRulesAsync, the ChangeCategory propagation from that branch is not exercised by the new test.

✅ Suggested addition (mirrors the new slow test)
+        [Fact]
+        public async Task StalledDownload_RuleWithChangeCategory_PropagatesChangeCategoryFlag()
+        {
+            // Arrange
+            const string hash = "test-hash";
+            var sut = _fixture.CreateSut();
+
+            var torrentInfo = new TorrentInfo
+            {
+                Hash = hash,
+                Name = "Test Torrent",
+                State = TorrentState.StalledDownload
+            };
+
+            _fixture.ClientWrapper
+                .GetTorrentListAsync(Arg.Is<TorrentListQuery>(q => q.Hashes != null && q.Hashes.Contains(hash)))
+                .Returns(new[] { torrentInfo });
+
+            _fixture.ClientWrapper
+                .GetTorrentTrackersAsync(hash)
+                .Returns(Array.Empty<TorrentTracker>());
+
+            var properties = new TorrentProperties
+            {
+                AdditionalData = new Dictionary<string, JToken>
+                {
+                    { "is_private", JToken.FromObject(false) }
+                }
+            };
+
+            _fixture.ClientWrapper
+                .GetTorrentPropertiesAsync(hash)
+                .Returns(properties);
+
+            _fixture.ClientWrapper
+                .GetTorrentContentsAsync(hash)
+                .Returns(new[]
+                {
+                    new TorrentContent { Index = 0, Priority = TorrentContentPriority.Normal }
+                });
+
+            _fixture.RuleEvaluator
+                .EvaluateStallRulesAsync(Arg.Any<QBitItemWrapper>())
+                .Returns((true, DeleteReason.Stalled, false, true));
+
+            // Act
+            var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
+
+            // Assert
+            result.ShouldRemove.ShouldBeTrue();
+            result.DeleteReason.ShouldBe(DeleteReason.Stalled);
+            result.DeleteFromClient.ShouldBeFalse();
+            result.ChangeCategory.ShouldBeTrue();
+        }
🤖 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
`@code/backend/Cleanuparr.Infrastructure.Tests/Features/DownloadClient/QBitServiceTests.cs`
around lines 836 - 950, Add a test named
StalledDownload_RuleWithChangeCategory_PropagatesChangeCategoryFlag in the
ShouldRemoveFromArrQueueAsync_StalledDownloadScenarios class that mirrors the
SlowDownload_RuleWithChangeCategory_PropagatesChangeCategoryFlag test but sets
TorrentState = TorrentState.StalledDownload and configures
RuleEvaluator.EvaluateStallRulesAsync to return (true, DeleteReason.Stalled,
true, true); call ShouldRemoveFromArrQueueAsync(hash, ...) on the SUT and assert
result.ShouldRemove is true, result.DeleteReason == DeleteReason.Stalled,
result.DeleteFromClient is true and result.ChangeCategory is true so the
stalled-rule branch (EvaluateStallRulesAsync) propagates the ChangeCategory
flag.
🤖 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.

Nitpick comments:
In
`@code/backend/Cleanuparr.Infrastructure.Tests/Features/DownloadClient/QBitServiceTests.cs`:
- Around line 836-950: Add a test named
StalledDownload_RuleWithChangeCategory_PropagatesChangeCategoryFlag in the
ShouldRemoveFromArrQueueAsync_StalledDownloadScenarios class that mirrors the
SlowDownload_RuleWithChangeCategory_PropagatesChangeCategoryFlag test but sets
TorrentState = TorrentState.StalledDownload and configures
RuleEvaluator.EvaluateStallRulesAsync to return (true, DeleteReason.Stalled,
true, true); call ShouldRemoveFromArrQueueAsync(hash, ...) on the SUT and assert
result.ShouldRemove is true, result.DeleteReason == DeleteReason.Stalled,
result.DeleteFromClient is true and result.ChangeCategory is true so the
stalled-rule branch (EvaluateStallRulesAsync) propagates the ChangeCategory
flag.

In
`@code/backend/Cleanuparr.Infrastructure.Tests/Features/DownloadClient/UTorrentServiceTests.cs`:
- Around line 568-615: Add a mirrored unit test in UTorrentServiceTests that
exercises the stalled-rule path where ChangeCategory is true: create a new test
similar to StalledDownload_MatchesRule_RemovesFromQueue but have the mocked
RuleEvaluator.EvaluateStallRulesAsync(Arg.Any<UTorrentItemWrapper>()) return
(true, DeleteReason.Stalled, true, true) and assert result.ShouldRemove == true,
result.DeleteReason == DeleteReason.Stalled, result.DeleteFromClient == true and
result.ChangeCategory == true; ensure the test uses the same setup
(UTorrentItem, UTorrentProperties, UTorrentFile list) and calls
ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>()) on the SUT.

In `@code/backend/Cleanuparr.Infrastructure/Features/Arr/ArrClient.cs`:
- Around line 162-168: Add XML documentation comments for the public method
DeleteQueueItemAsync so the new parameter contract is discoverable: add a
<summary> describing what DeleteQueueItemAsync does, and <param> tags for
arrInstance, record, removeFromClient, changeCategory (explain what true/false
means), and deleteReason; include a <returns> describing the returned Task and
any <exception> tags if the method throws specific exceptions. Ensure the doc
sits immediately above the DeleteQueueItemAsync declaration and either fully
documents the parameters or uses <inheritdoc/> with an explicit <param
name="changeCategory"> entry to cover the new parameter.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 36f091fd-bbc3-494d-9e87-fa3d3e4d5e0a

📥 Commits

Reviewing files that changed from the base of the PR and between 851b08f and f7d362d.

📒 Files selected for processing (17)
  • code/backend/Cleanuparr.Infrastructure.Tests/Features/DownloadClient/DelugeServiceTests.cs
  • code/backend/Cleanuparr.Infrastructure.Tests/Features/DownloadClient/QBitServiceTests.cs
  • code/backend/Cleanuparr.Infrastructure.Tests/Features/DownloadClient/RTorrentServiceTests.cs
  • code/backend/Cleanuparr.Infrastructure.Tests/Features/DownloadClient/TransmissionServiceTests.cs
  • code/backend/Cleanuparr.Infrastructure.Tests/Features/DownloadClient/UTorrentServiceTests.cs
  • code/backend/Cleanuparr.Infrastructure.Tests/Features/DownloadRemover/QueueItemRemoverTests.cs
  • code/backend/Cleanuparr.Infrastructure.Tests/Features/Jobs/QueueCleanerTests.cs
  • code/backend/Cleanuparr.Infrastructure/Features/Arr/ArrClient.cs
  • code/backend/Cleanuparr.Infrastructure/Features/Arr/Interfaces/IArrClient.cs
  • code/backend/Cleanuparr.Infrastructure/Features/Arr/LidarrClient.cs
  • code/backend/Cleanuparr.Infrastructure/Features/Arr/RadarrClient.cs
  • code/backend/Cleanuparr.Infrastructure/Features/Arr/ReadarrClient.cs
  • code/backend/Cleanuparr.Infrastructure/Features/Arr/SonarrClient.cs
  • code/backend/Cleanuparr.Infrastructure/Features/Arr/WhisparrV2Client.cs
  • code/backend/Cleanuparr.Infrastructure/Features/Arr/WhisparrV3Client.cs
  • code/backend/Cleanuparr.Infrastructure/Features/DownloadRemover/Models/QueueItemRemoveRequest.cs
  • docs/docs/configuration/queue-cleaner/index.mdx
💤 Files with no reviewable changes (6)
  • code/backend/Cleanuparr.Infrastructure/Features/Arr/ReadarrClient.cs
  • code/backend/Cleanuparr.Infrastructure/Features/Arr/WhisparrV2Client.cs
  • code/backend/Cleanuparr.Infrastructure/Features/Arr/WhisparrV3Client.cs
  • code/backend/Cleanuparr.Infrastructure/Features/Arr/LidarrClient.cs
  • code/backend/Cleanuparr.Infrastructure/Features/Arr/SonarrClient.cs
  • code/backend/Cleanuparr.Infrastructure/Features/Arr/RadarrClient.cs
🚧 Files skipped from review as they are similar to previous changes (8)
  • code/backend/Cleanuparr.Infrastructure/Features/Arr/Interfaces/IArrClient.cs
  • docs/docs/configuration/queue-cleaner/index.mdx
  • code/backend/Cleanuparr.Infrastructure/Features/DownloadRemover/Models/QueueItemRemoveRequest.cs
  • code/backend/Cleanuparr.Infrastructure.Tests/Features/DownloadClient/RTorrentServiceTests.cs
  • code/backend/Cleanuparr.Infrastructure.Tests/Features/DownloadRemover/QueueItemRemoverTests.cs
  • code/backend/Cleanuparr.Infrastructure.Tests/Features/Jobs/QueueCleanerTests.cs
  • code/backend/Cleanuparr.Infrastructure.Tests/Features/DownloadClient/TransmissionServiceTests.cs
  • code/backend/Cleanuparr.Infrastructure.Tests/Features/DownloadClient/DelugeServiceTests.cs

@Flaminel

Flaminel commented May 5, 2026

Copy link
Copy Markdown
Contributor Author

@greptileai review

@Cleanuparr Cleanuparr deleted a comment from coderabbitai Bot May 5, 2026
@Cleanuparr Cleanuparr deleted a comment from coderabbitai Bot May 5, 2026
@Flaminel Flaminel merged commit ab792f5 into main May 5, 2026
14 checks passed
@Flaminel Flaminel deleted the add_option_for_category_change branch May 5, 2026 12:59
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