Skip to content

Conversation

@jrh3k5
Copy link
Contributor

@jrh3k5 jrh3k5 commented Dec 28, 2025

If I provided an empty configuration for albums, people, or excluded albums, the deserialized settings appear to leave the respective .Albums, .People, and .ExcludedAlbums properties as null. This causes errors like the one below when ImmichFrame tries to iterate over them:

25-12-28 20:20:26 fail: Microsoft.AspNetCore.Server.Kestrel[13] Connection id "0HNI68OJKQ412", Request id "0HNI68OJKQ412:00000002": An unhandled exception was thrown by the application. System.ArgumentNullException: Value cannot be null. (Parameter 'source')    at System.Linq.ThrowHelper.ThrowArgumentNullException(ExceptionArgument argument)    at System.Linq.Enumerable.TryGetNonEnumeratedCount[TSource](IEnumerable`1 source, Int32& count)    at System.Linq.Enumerable.Any[TSource](IEnumerable`1 source)    at ImmichFrame.Core.Logic.PooledImmichFrameLogic.BuildPool(IAccountSettings accountSettings) in /source/ImmichFrame.Core/Logic/PooledImmichFrameLogic.cs:line 54    at ImmichFrame.Core.Logic.PooledImmichFrameLogic..ctor(IAccountSettings accountSettings, IGeneralSettings generalSettings, IHttpClientFactory httpClientFactory) in /source/ImmichFrame.Core/Logic/PooledImmichFrameLogic.cs:line 28    at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)    at System.Reflection.ConstructorInvoker.InvokeDirectByRefWithFewArgs(Span`1 copyOfArgs)    at System.Reflection.ConstructorInvoker.InvokeDirectByRef(Object arg1, Object arg2, Object arg3, Object arg4)    at System.Reflection.ConstructorInvoker.InvokeImpl(Object arg1, Object arg2, Object arg3, Object arg4)    at System.Reflection.ConstructorInvoker.Invoke(Span`1 arguments)    at Microsoft.Extensions.DependencyInjection.ActivatorUtilities.ConstructorMatcher.CreateInstance(IServiceProvider provider)    at Microsoft.Extensions.DependencyInjection.ActivatorUtilities.CreateInstance(IServiceProvider provider, Type instanceType, Object[] parameters)    at Microsoft.Extensions.DependencyInjection.ActivatorUtilities.CreateInstance[T](IServiceProvider provider, Object[] parameters)    at Program.<>c__DisplayClass0_1.<<Main>$>b__8(IAccountSettings account) in /source/ImmichFrame.WebApi/Program.cs:line 67    at System.Linq.Enumerable.ToDictionary[TSource,TKey,TElement](IEnumerable`1 source, Func`2 keySelector, Func`2 elementSelector, IEqualityComparer`1 comparer)    at System.Collections.Frozen.FrozenDictionary.ToFrozenDictionary[TSource,TKey,TElement](IEnumerable`1 source, Func`2 keySelector, Func`2 elementSelector, IEqualityComparer`1 comparer)    at ImmichFrame.Core.Logic.MultiImmichFrameLogicDelegate..ctor(IServerSettings serverSettings, Func`2 logicFactory, ILogger`1 logger, IAccountSelectionStrategy accountSelectionStrategy) in /source/ImmichFrame.Core/Logic/MultiImmichFrameLogicDelegate.cs:line 23    at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)    at System.Reflection.MethodBaseInvoker.InvokeDirectByRefWithFewArgs(Object obj, Span`1 copyOfArgs, BindingFlags invokeAttr)    at System.Reflection.MethodBaseInvoker.InvokeWithFewArgs(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)    at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)    at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitRootCache(ServiceCallSite callSite, RuntimeResolverContext context)    at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)    at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.Resolve(ServiceCallSite callSite, ServiceProviderEngineScope scope)    at Microsoft.Extensions.DependencyInjection.ServiceProvider.CreateServiceAccessor(ServiceIdentifier serviceIdentifier)    at System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd(TKey key, Func`2 valueFactory)    at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(ServiceIdentifier serviceIdentifier, ServiceProviderEngineScope serviceProviderEngineScope)    at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope.GetService(Type serviceType)    at lambda_method18(Closure, IServiceProvider, Object[])    at Microsoft.AspNetCore.Mvc.Controllers.ControllerFactoryProvider.<>c__DisplayClass6_0.<CreateControllerFactory>g__CreateController|0(ControllerContext controllerContext)    at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)    at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync() --- End of stack trace from previous location ---    at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)    at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)    at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)    at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)    at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)    at CustomAuthenticationMiddleware.InvokeAsync(HttpContext context) in /source/ImmichFrame.WebApi/Helpers/CustomAuthenticationMiddleware.cs:line 23    at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHttpApplication`1 application)

This adds null awareness around uses of these enumerables to protect against these errors.

Summary by CodeRabbit

  • Bug Fixes

    • Enhanced null-safety handling for account settings collections to prevent potential runtime errors.
  • Tests

    • Added test coverage for edge cases with null collection configurations.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Dec 28, 2025

📝 Walkthrough

Walkthrough

This pull request systematically introduces null-safety checks across the pool logic codebase to prevent null reference exceptions when account settings collections (Albums, ExcludedAlbums, People) are null. Corresponding test cases are added to verify proper handling of these null scenarios.

Changes

Cohort / File(s) Summary
Pool Logic Null-Safety Fixes
ImmichFrame.Core/Logic/Pool/AlbumAssetsPool.cs, AllAssetsPool.cs, PeopleAssetsPool.cs, PooledImmichFrameLogic.cs
Introduces null-conditional operators and local variable guards for Albums, ExcludedAlbums, and People collections. Replaces direct Any() calls with null-conditional checks using ?. operator and ?? false fallback. AlbumAssetsPool and PeopleAssetsPool now load collections into local variables before iterating, with early returns for null cases. PooledImmichFrameLogic extracts derived boolean flags (hasAlbums, hasPeople) using null-conditional Any() checks.
Pool Logic Test Coverage
ImmichFrame.Core.Tests/Logic/Pool/AlbumAssetsPoolTests.cs, AllAssetsPoolTests.cs, PersonAssetsPoolTests.cs
Adds edge-case test coverage for null collection scenarios: LoadAssets\_NullAlbums\_ReturnsEmpty, LoadAssets\_NullExcludedAlbums\_Succeeds, GetAssets\_NullExcludedAlbums\_Succeeds, and LoadAssets\_NullPeople\_ReturnsEmpty. Tests verify empty results are returned and no downstream API calls are made when collections are null.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

A rabbit hops through null-safe code, 🐰
Where collections dance or fade,
With guards and checks along the road,
No crashes in the shade,
Safe pools of assets—a safer load! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and directly summarizes the main change: adding null-safety checks for albums, people, and excluded albums throughout the codebase.

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

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@jrh3k5 jrh3k5 marked this pull request as ready for review December 28, 2025 19:54
Copy link

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

🧹 Nitpick comments (1)
ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs (1)

65-77: Consider using the local variable for consistency.

Line 69 directly uses accountSettings.ExcludedAlbums, while the calling code at line 53 loads it into a local variable. For consistency with the null-safety pattern used in AlbumAssetsPool.cs (lines 16, 28) and PeopleAssetsPool.cs (line 18), consider either:

  • Passing excludedAlbums as a parameter to this method, or
  • Adding a defensive null check at the start of this method

This improves maintainability and prevents potential issues if the method is called from other contexts in the future.

🔎 Proposed refactor

Option 1: Pass the local variable as a parameter

-    private async Task<IEnumerable<AssetResponseDto>> GetExcludedAlbumAssets(CancellationToken ct = default)
+    private async Task<IEnumerable<AssetResponseDto>> GetExcludedAlbumAssets(IEnumerable<Guid> excludedAlbums, CancellationToken ct = default)
     {
         var excludedAlbumAssets = new List<AssetResponseDto>();
 
-        foreach (var albumId in accountSettings.ExcludedAlbums)
+        foreach (var albumId in excludedAlbums)
         {
             var albumInfo = await immichApi.GetAlbumInfoAsync(albumId, null, null, ct);
 
             excludedAlbumAssets.AddRange(albumInfo.Assets);
         }
         
         return excludedAlbumAssets;
     }

And update the call site:

-            var excludedAssetList = await GetExcludedAlbumAssets(ct);
+            var excludedAssetList = await GetExcludedAlbumAssets(excludedAlbums, ct);
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 575e9d3 and 4040bc3.

📒 Files selected for processing (8)
  • ImmichFrame.Core.Tests/Logic/Pool/AlbumAssetsPoolTests.cs
  • ImmichFrame.Core.Tests/Logic/Pool/AllAssetsPoolTests.cs
  • ImmichFrame.Core.Tests/Logic/Pool/PersonAssetsPoolTests.cs
  • ImmichFrame.Core.Tests/Logic/PooledImmichFrameLogicTests.cs
  • ImmichFrame.Core/Logic/Pool/AlbumAssetsPool.cs
  • ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs
  • ImmichFrame.Core/Logic/Pool/PeopleAssetsPool.cs
  • ImmichFrame.Core/Logic/PooledImmichFrameLogic.cs
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-09T16:12:49.488Z
Learnt from: JoeRu
Repo: immichFrame/ImmichFrame PR: 481
File: ImmichFrame.Core.Tests/Logic/Pool/ChronologicalAssetsPoolWrapperTests.cs:306-306
Timestamp: 2025-10-09T16:12:49.488Z
Learning: When testing the ChronologicalAssetsPoolWrapper in ImmichFrame.Core.Tests, use `Is.SupersetOf` rather than `Is.EquivalentTo` or `Is.EqualTo` assertions because the wrapper uses Fisher-Yates shuffle to randomize set order, making output non-deterministic between runs. The wrapper also uses a 10x fetch multiplier (capped at 1000) that may return more assets than requested, which is legitimate behavior.

Applied to files:

  • ImmichFrame.Core.Tests/Logic/Pool/AllAssetsPoolTests.cs
  • ImmichFrame.Core.Tests/Logic/PooledImmichFrameLogicTests.cs
  • ImmichFrame.Core/Logic/PooledImmichFrameLogic.cs
  • ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs
  • ImmichFrame.Core.Tests/Logic/Pool/PersonAssetsPoolTests.cs
  • ImmichFrame.Core.Tests/Logic/Pool/AlbumAssetsPoolTests.cs
  • ImmichFrame.Core/Logic/Pool/AlbumAssetsPool.cs
🧬 Code graph analysis (3)
ImmichFrame.Core.Tests/Logic/PooledImmichFrameLogicTests.cs (1)
ImmichFrame.Core/Logic/PooledImmichFrameLogic.cs (2)
  • PooledImmichFrameLogic (9-144)
  • PooledImmichFrameLogic (17-29)
ImmichFrame.Core.Tests/Logic/Pool/PersonAssetsPoolTests.cs (1)
ImmichFrame.Core/Logic/Pool/PeopleAssetsPool.cs (1)
  • Task (8-45)
ImmichFrame.Core.Tests/Logic/Pool/AlbumAssetsPoolTests.cs (2)
ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs (3)
  • Task (8-13)
  • Task (15-62)
  • Task (65-77)
ImmichFrame.Core/Logic/Pool/AlbumAssetsPool.cs (1)
  • Task (9-36)
🔇 Additional comments (8)
ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs (1)

53-59: Good null-safety implementation.

The defensive check for null and empty excluded albums prevents unnecessary API calls and potential exceptions. The pattern aligns well with the PR objective.

ImmichFrame.Core/Logic/PooledImmichFrameLogic.cs (1)

38-63: Excellent null-safety implementation.

The introduction of hasAlbums and hasPeople flags with explicit null checks cleanly separates validation from pool construction logic. This pattern prevents ArgumentNullException when iterating collections in downstream pool classes and makes the pool selection logic more readable.

ImmichFrame.Core/Logic/Pool/PeopleAssetsPool.cs (1)

12-18: LGTM - Clean null-safety pattern.

The early return when people is null prevents iteration over a null collection, and the local variable is used consistently throughout the method. This aligns well with the similar pattern in AlbumAssetsPool.cs.

ImmichFrame.Core.Tests/Logic/Pool/PersonAssetsPoolTests.cs (1)

94-101: Good test coverage for null scenario.

This test appropriately verifies that when People is null, no API calls are made and an empty result is returned. This complements the existing empty-list test and validates the null-safety changes in PeopleAssetsPool.cs.

ImmichFrame.Core.Tests/Logic/Pool/AllAssetsPoolTests.cs (1)

137-175: Comprehensive test coverage for null and empty excluded albums.

Both tests appropriately verify that no album info calls are made when ExcludedAlbums is null or empty, validating the optimization introduced in the null-safety changes. The assertions confirm that all assets are returned without exclusion filtering.

ImmichFrame.Core.Tests/Logic/Pool/AlbumAssetsPoolTests.cs (1)

102-126: Well-structured tests for null album scenarios.

Both tests appropriately cover the edge cases:

  • LoadAssets_NullAlbums_ReturnsEmpty verifies no API calls when Albums is null
  • LoadAssets_NullExcludedAlbums_ReturnsAlbumAssets verifies that null ExcludedAlbums doesn't prevent album asset loading

These tests validate the null-safety implementation in AlbumAssetsPool.cs.

ImmichFrame.Core/Logic/Pool/AlbumAssetsPool.cs (1)

13-33: Exemplary null-safety implementation.

The consistent use of local variables (excludedAlbums, albums) with null checks throughout the method demonstrates excellent defensive programming. This pattern is used consistently for both excluded and included albums, preventing potential null reference exceptions while maintaining clean, readable code.

ImmichFrame.Core.Tests/Logic/PooledImmichFrameLogicTests.cs (1)

1-144: Comprehensive test coverage for pool building with null/empty People.

The test suite effectively covers various scenarios:

  • Null People (lines 48-61, 87-101, 128-143)
  • Empty People (lines 68-80)
  • Populated People (lines 108-121)
  • Mixed settings combinations

The tests verify that pool construction doesn't throw exceptions when People is null or empty, which directly validates the null-safety changes in PooledImmichFrameLogic.cs. The approach of testing construction success and AccountSettings state is appropriate for validating the fix.

jrh3k5 and others added 3 commits January 3, 2026 20:25
Co-authored-by: Jan <17313367+JW-CH@users.noreply.github.com>
Co-authored-by: Jan <17313367+JW-CH@users.noreply.github.com>
Co-authored-by: Jan <17313367+JW-CH@users.noreply.github.com>
Copy link

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

🧹 Nitpick comments (1)
ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs (1)

53-53: Remove unused local variable.

The excludedAlbums variable is assigned but never used. Line 54 and the subsequent code reference accountSettings.ExcludedAlbums directly instead of using this local variable.

🔎 Proposed fix
-        var excludedAlbums = accountSettings.ExcludedAlbums;
         if (accountSettings.ExcludedAlbums?.Any() ?? false)
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4040bc3 and c6ff021.

📒 Files selected for processing (2)
  • ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs
  • ImmichFrame.Core/Logic/PooledImmichFrameLogic.cs
🚧 Files skipped from review as they are similar to previous changes (1)
  • ImmichFrame.Core/Logic/PooledImmichFrameLogic.cs
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: JoeRu
Repo: immichFrame/ImmichFrame PR: 481
File: ImmichFrame.Core.Tests/Logic/Pool/ChronologicalAssetsPoolWrapperTests.cs:306-306
Timestamp: 2025-10-09T16:12:49.488Z
Learning: When testing the ChronologicalAssetsPoolWrapper in ImmichFrame.Core.Tests, use `Is.SupersetOf` rather than `Is.EquivalentTo` or `Is.EqualTo` assertions because the wrapper uses Fisher-Yates shuffle to randomize set order, making output non-deterministic between runs. The wrapper also uses a 10x fetch multiplier (capped at 1000) that may return more assets than requested, which is legitimate behavior.
📚 Learning: 2025-10-09T16:12:49.488Z
Learnt from: JoeRu
Repo: immichFrame/ImmichFrame PR: 481
File: ImmichFrame.Core.Tests/Logic/Pool/ChronologicalAssetsPoolWrapperTests.cs:306-306
Timestamp: 2025-10-09T16:12:49.488Z
Learning: When testing the ChronologicalAssetsPoolWrapper in ImmichFrame.Core.Tests, use `Is.SupersetOf` rather than `Is.EquivalentTo` or `Is.EqualTo` assertions because the wrapper uses Fisher-Yates shuffle to randomize set order, making output non-deterministic between runs. The wrapper also uses a 10x fetch multiplier (capped at 1000) that may return more assets than requested, which is legitimate behavior.

Applied to files:

  • ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs
🔇 Additional comments (1)
ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs (1)

54-54: LGTM! Proper null-safety implementation.

The null-conditional check correctly prevents ArgumentNullException when ExcludedAlbums is null, and ensures the exclusion logic only runs when excluded albums are present. This guards both the Any() call and the subsequent iteration in GetExcludedAlbumAssets.

Copy link
Collaborator

@JW-CH JW-CH left a comment

Choose a reason for hiding this comment

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

Sorry, the previous comments were just a hint on how to change it. This would be leftovers :)

var assets = await immichApi.SearchRandomAsync(searchDto, ct);

if (accountSettings.ExcludedAlbums.Any())
var excludedAlbums = accountSettings.ExcludedAlbums;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Not needed anymore.

private IAssetPool BuildPool(IAccountSettings accountSettings)
{
if (!accountSettings.ShowFavorites && !accountSettings.ShowMemories && !accountSettings.Albums.Any() && !accountSettings.People.Any())
var albums = accountSettings.Albums;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Not needed anymore.

var albums = accountSettings.Albums;
var hasAlbums = accountSettings.Albums?.Any() ?? false;

var people = accountSettings.People;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Not needed anymore.

@jrh3k5
Copy link
Contributor Author

jrh3k5 commented Jan 4, 2026

@JW-CH - sorry, I should have previewed these changes in the IDE before committing them - the 'unused variable' warnings would have been more visible.

I've also replaced the calls to .Any() with comparisons against the .Count field - the in-IDE linter said that was more performant.

Copy link

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

🧹 Nitpick comments (1)
ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs (1)

65-77: Consider aligning with AlbumAssetsPool's null-check pattern.

The current implementation is safe because GetExcludedAlbumAssets is only called when excludedAlbumsCount > 0 (line 54), preventing null access at line 69. However, AlbumAssetsPool.cs uses a more explicit pattern that captures the collection reference and checks null before iterating:

var excludedAlbums = accountSettings.ExcludedAlbums;
if (excludedAlbums != null)
{
    foreach (var albumId in excludedAlbums) 
    {
        // ...
    }
}

This pattern is clearer, more defensive against potential concurrent modifications, and maintains consistency across pool implementations.

🔎 Suggested refactor for consistency
 private async Task<IEnumerable<AssetResponseDto>> GetExcludedAlbumAssets(CancellationToken ct = default)
 {
     var excludedAlbumAssets = new List<AssetResponseDto>();
-
-    foreach (var albumId in accountSettings.ExcludedAlbums)
+    
+    var excludedAlbums = accountSettings.ExcludedAlbums;
+    if (excludedAlbums != null)
     {
-        var albumInfo = await immichApi.GetAlbumInfoAsync(albumId, null, null, ct);
-
-        excludedAlbumAssets.AddRange(albumInfo.Assets);
+        foreach (var albumId in excludedAlbums)
+        {
+            var albumInfo = await immichApi.GetAlbumInfoAsync(albumId, null, null, ct);
+            excludedAlbumAssets.AddRange(albumInfo.Assets);
+        }
     }
     
     return excludedAlbumAssets;
 }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c6ff021 and 48b9ad0.

📒 Files selected for processing (2)
  • ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs
  • ImmichFrame.Core/Logic/PooledImmichFrameLogic.cs
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: JoeRu
Repo: immichFrame/ImmichFrame PR: 481
File: ImmichFrame.Core.Tests/Logic/Pool/ChronologicalAssetsPoolWrapperTests.cs:306-306
Timestamp: 2025-10-09T16:12:49.488Z
Learning: When testing the ChronologicalAssetsPoolWrapper in ImmichFrame.Core.Tests, use `Is.SupersetOf` rather than `Is.EquivalentTo` or `Is.EqualTo` assertions because the wrapper uses Fisher-Yates shuffle to randomize set order, making output non-deterministic between runs. The wrapper also uses a 10x fetch multiplier (capped at 1000) that may return more assets than requested, which is legitimate behavior.
📚 Learning: 2025-10-09T16:12:49.488Z
Learnt from: JoeRu
Repo: immichFrame/ImmichFrame PR: 481
File: ImmichFrame.Core.Tests/Logic/Pool/ChronologicalAssetsPoolWrapperTests.cs:306-306
Timestamp: 2025-10-09T16:12:49.488Z
Learning: When testing the ChronologicalAssetsPoolWrapper in ImmichFrame.Core.Tests, use `Is.SupersetOf` rather than `Is.EquivalentTo` or `Is.EqualTo` assertions because the wrapper uses Fisher-Yates shuffle to randomize set order, making output non-deterministic between runs. The wrapper also uses a 10x fetch multiplier (capped at 1000) that may return more assets than requested, which is legitimate behavior.

Applied to files:

  • ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs
  • ImmichFrame.Core/Logic/PooledImmichFrameLogic.cs
🧬 Code graph analysis (2)
ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs (3)
ImmichFrame.Core.Tests/Logic/Pool/AlbumAssetsPoolTests.cs (4)
  • Test (74-85)
  • s (92-92)
  • s (78-78)
  • api (71-71)
ImmichFrame.Core.Tests/Logic/Pool/AllAssetsPoolTests.cs (4)
  • s (36-36)
  • s (120-120)
  • api (124-124)
  • Test (111-135)
ImmichFrame.Core/Logic/Pool/AlbumAssetsPool.cs (1)
  • AlbumAssetsPool (7-29)
ImmichFrame.Core/Logic/PooledImmichFrameLogic.cs (1)
ImmichFrame.Core/Logic/Pool/AlbumAssetsPool.cs (1)
  • AlbumAssetsPool (7-37)
🔇 Additional comments (3)
ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs (1)

53-54: Null-safety fix looks good.

The null-conditional operator and count check correctly prevent ArgumentNullException when ExcludedAlbums is null. This addresses the core issue described in the PR.

Note: A past review comment suggested using ?.Any() ?? false pattern instead of ?.Count ?? 0. Both approaches work correctly for this use case.

ImmichFrame.Core/Logic/PooledImmichFrameLogic.cs (2)

38-39: Null-safety flags correctly prevent ArgumentNullException.

The use of ?.Count > 0 safely handles null collections by leveraging C#'s lifted comparison operators, which return false when the nullable operand is null. This correctly addresses the issue described in the PR.

Note: A past review comment suggested using ?.Any() ?? false pattern. While both approaches are functionally equivalent and correct, the current implementation is concise and idiomatic.


41-41: LGTM! Pool construction logic correctly uses null-safe flags.

The conditional pool construction now properly handles null or empty collections:

  • AllAssetsPool is used when no specific sources are configured
  • AlbumAssetsPool and PersonAssetsPool are only added when their respective collections have items

This prevents the ArgumentNullException that occurred when iterating over null collections.

Also applies to: 54-54, 57-57

@JW-CH
Copy link
Collaborator

JW-CH commented Jan 4, 2026

@JW-CH - sorry, I should have previewed these changes in the IDE before committing them - the 'unused variable' warnings would have been more visible.

I've also replaced the calls to .Any() with comparisons against the .Count field - the in-IDE linter said that was more performant.

Sorry for the back and forth. I just did some reading (on a quite outdated article, lol) https://www.tabsoverspaces.com/233649-comparing-speed-of-count-0-and-any

But only the numbers should be outdated, not the point this guy is making:

... I write readable code first, often that also means performant enough, and optimize later when needed and backed by numbers.

In this case, I quite agree with that and would ignore the lesser performance and value readability higher; it also fits the style of the rest better. What are your thoughts? (By the way, LINQ got better over time and more optimized. I don't think it will be much of a difference, plus the love LINQ got performance-wise in .NET 10 is incredible (I know, Frame is on .NET 8 as of now))

@jrh3k5
Copy link
Contributor Author

jrh3k5 commented Jan 4, 2026

What are your thoughts?

My thoughts are it's been a very hot minute since I've written .NET professionally. 😄 I'm fine with .Any() ?? false if that's more readable and consistent with the codebase, and have updated the PR accordingly.

@jrh3k5 jrh3k5 requested a review from JW-CH January 4, 2026 17:15
@jrh3k5
Copy link
Contributor Author

jrh3k5 commented Jan 14, 2026

Hey, @JW-CH - apologies if I'm being pushy, but I also know that it's easy for PR notifications to get lost in the shuffle. Just want to nudge that this PR is ready for review.

@3rob3 3rob3 added the enhancement New feature or request label Jan 14, 2026
@3rob3
Copy link
Collaborator

3rob3 commented Jan 14, 2026

I didn't review the tests, but the logic looks good to me. 👍

@JW-CH
Copy link
Collaborator

JW-CH commented Jan 15, 2026

Sorry, I lack behind with reviews. Hopefully, I'll find this weekend :)

Copy link
Collaborator

@JW-CH JW-CH left a comment

Choose a reason for hiding this comment

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

Can you check the tests again? I assume this is AI assisted/generated. Please verify that those checks are needed. In general, if you want my reivew, please review it yourself first :)

// Act
var logic = new PooledImmichFrameLogic(_mockAccountSettings.Object, _mockGeneralSettings.Object, _mockHttpClientFactory.Object);

// Assert - The pool should be AllAssetsPool, not MultiAssetPool
Copy link
Collaborator

Choose a reason for hiding this comment

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

these comments are quite off? can you check this file again?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Also some of these checks do not really do anything? Checking if a constructor added something successfully does not make much sense to me.

public void BuildPool_WithMixedSettingsAndNullPeople_DoesNotIncludePersonAssetsPool()
{
// Arrange - enable some settings but keep People null
_mockAccountSettings.SetupGet(s => s.ShowFavorites).Returns(true);
Copy link
Collaborator

Choose a reason for hiding this comment

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

why are these set up?

var logic = new PooledImmichFrameLogic(_mockAccountSettings.Object, _mockGeneralSettings.Object, _mockHttpClientFactory.Object);

// Assert
Assert.That(logic.AccountSettings.People, Is.Null);
Copy link
Collaborator

Choose a reason for hiding this comment

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

wouldn't this throw if you access logic.AccountSettings.People if logic.AccountSettings is null? Asking because of the next check a line below.

@jrh3k5
Copy link
Contributor Author

jrh3k5 commented Jan 17, 2026

@JW-CH - apologies, I did export the work on the tests to Copilot. I should have looked more closely at the tests before submitting the PR.

With that in mind, I'm trying to do some TDD to verify that the tests actually catch errors, and I'm struggling with Moq here. In this case, I've removed the calls to .SetupGet() in the Setup() method (so that the default setup of returning non-null lists wouldn't interfere with the test and commented out the null check, expecting it to throw a null pointer exception, and it doesn't.

image

We know that it can be null at runtime because of the stacktrace attached in the PR description, so I suspect that Moq is trying to "help" here and return a non-null default value.

If that's the case, I could possibly try to get around it by maybe creating a concrete instance of the settings and, within the scope of the test, create a new instance of TestableAlbumAssetsPool? But I generally hate doing that because it makes changes to constructors a multiplicative effort for tests (and I've seen it more than once lead to poorly-maintained tests).

I apologize, again, for wasting your time with my AI-generated unit tests.

@JW-CH
Copy link
Collaborator

JW-CH commented Jan 17, 2026

I apologize, again, for wasting your time with my AI-generated unit tests.

No need to apologize, I like reviewing stuff, but this takes time :) I'm glad that you like the project and want to make changes yourself. If you can't get the tests to work, please just remove them. I'd prefer having no tests over AI-gibberish tests. I can also add some myself.

I have not the greatest knowledge myself in testing software, that's why I do not have a strong opinion about this.

@jrh3k5
Copy link
Contributor Author

jrh3k5 commented Jan 18, 2026

@JW-CH - figured out the unlock: .Returns() can accept a lambda, which allows me to bypass whatever Moq logic was preventing me from returning null. I've got all the tests working and also removed the AI slop unit tests here:

2207031

Copy link

@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

🤖 Fix all issues with AI agents
In `@ImmichFrame.Core.Tests/Logic/PooledImmichFrameLogicTests.cs`:
- Around line 43-55: The test currently never calls BuildPool; update the test
in PooledImmichFrameLogicTests to call the BuildPool method on the _logic
instance (e.g., _logic.BuildPool()) and assert it does not throw; additionally,
if there is a public accessor for pools (e.g., _logic.Pools or similar), assert
that no PersonAssetsPool was added when AccountSettings.People is null (or
assert Pools.Count remains unchanged) to catch regressions where a
PersonAssetsPool is created.

@JW-CH JW-CH self-requested a review January 18, 2026 13:00
Copy link
Collaborator

@JW-CH JW-CH left a comment

Choose a reason for hiding this comment

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

I have pushed on your PR, I hope you don't mind. If you want to return null in the setup, you are able to do it like so:

_mockAccountSettings.SetupGet(s => s.ExcludedAlbums).Returns((List<Guid>)null);

I assume that was what you tried to do with your added property :) Thank you for your PR!

@JW-CH JW-CH merged commit 9dfb5cc into immichFrame:main Jan 18, 2026
7 checks passed
@jrh3k5
Copy link
Contributor Author

jrh3k5 commented Jan 18, 2026

@JW-CH:

I have pushed on your PR, I hope you don't mind. If you want to return null in the setup, you are able to do it like so:

I found that actually didn't work. The reason I had to make the change to a property-driven provisioning of the albums reference is that, if I removed the null checks I added and used the .SetupGet() as your change does, it did not actually reproduce the error - Moq seems to silently ignore such setups. As a result, .SetupGet(...).Returns((List<Guid>) null)) actually ends up returning a non-null, empty List<Guid> instance.

It's not a hill I'll die on, but I do want to stress that the tests, as they exist in main, now, do not actually test for when ExcludedAlbums, Albums, and People are null - the tests are all false positives because, despite what the tests are setting up, Moq is actually setting up a return of non-null, empty lists.

@jrh3k5 jrh3k5 deleted the null-settings branch January 18, 2026 13:32
@JW-CH
Copy link
Collaborator

JW-CH commented Jan 18, 2026

@JW-CH:

I have pushed on your PR, I hope you don't mind. If you want to return null in the setup, you are able to do it like so:

I found that actually didn't work. The reason I had to make the change to a property-driven provisioning of the albums reference is that, if I removed the null checks I added and used the .SetupGet() as your change does, it did not actually reproduce the error - Moq seems to silently ignore such setups. As a result, .SetupGet(...).Returns((List<Guid>) null)) actually ends up returning a non-null, empty List<Guid> instance.

It's not a hill I'll die on, but I do want to stress that the tests, as they exist in main, now, do not actually test for when ExcludedAlbums, Albums, and People are null - the tests are all false positives because, despite what the tests are setting up, Moq is actually setting up a return of non-null, empty lists.

Good to know! Thank you! I'll have rework the tests sometimes in the future again. Will keep this in mind. For now, I'll think we are ok.

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

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants