diff --git a/global.json b/global.json index 67d23d48..c3152d8c 100644 --- a/global.json +++ b/global.json @@ -3,7 +3,7 @@ "Uno.Sdk": "6.5.31" }, "sdk": { - "version": "10.0.202", + "version": "10.0.103", "rollForward": "latestPatch", "allowPrerelease": false } diff --git a/src/SalmonEgg.Presentation.Core/Services/Chat/AcpConnectionSessionRegistry.cs b/src/SalmonEgg.Presentation.Core/Services/Chat/AcpConnectionSessionRegistry.cs index a1d27587..941338a8 100644 --- a/src/SalmonEgg.Presentation.Core/Services/Chat/AcpConnectionSessionRegistry.cs +++ b/src/SalmonEgg.Presentation.Core/Services/Chat/AcpConnectionSessionRegistry.cs @@ -53,6 +53,7 @@ public sealed class InMemoryAcpConnectionSessionRegistry : IAcpConnectionSession { private readonly object _gate = new(); private readonly Dictionary _sessionsByProfile = new(StringComparer.Ordinal); + private readonly Dictionary _profileByService = new(ReferenceEqualityComparer.Instance); /// public event Action? ProfileConnectionChanged; @@ -70,13 +71,10 @@ public bool TryGetProfileId(IChatService service, out string profileId) ArgumentNullException.ThrowIfNull(service); lock (_gate) { - foreach (var pair in _sessionsByProfile) + if (_profileByService.TryGetValue(service, out var existingProfileId)) { - if (ReferenceEquals(pair.Value.Service, service)) - { - profileId = pair.Key; - return true; - } + profileId = existingProfileId; + return true; } } @@ -89,10 +87,17 @@ public void Upsert(AcpConnectionSession session) ArgumentNullException.ThrowIfNull(session); lock (_gate) { + if (_sessionsByProfile.TryGetValue(session.ProfileId, out var existingSession) && !ReferenceEquals(existingSession.Service, session.Service)) + { + _profileByService.Remove(existingSession.Service); + } + _sessionsByProfile[session.ProfileId] = session with { LastUsedUtc = session.LastUsedUtc == default ? DateTime.UtcNow : session.LastUsedUtc }; + + _profileByService[session.Service] = session.ProfileId; } // Raise outside the lock to avoid potential deadlocks from re-entrant subscribers. @@ -101,10 +106,15 @@ public void Upsert(AcpConnectionSession session) public bool RemoveByProfile(string profileId) { - bool removed; + bool removed = false; lock (_gate) { - removed = _sessionsByProfile.Remove(profileId); + if (_sessionsByProfile.TryGetValue(profileId, out var session)) + { + _sessionsByProfile.Remove(profileId); + _profileByService.Remove(session.Service); + removed = true; + } } if (removed) @@ -119,19 +129,19 @@ public bool RemoveByService(IChatService service, out string profileId) { ArgumentNullException.ThrowIfNull(service); bool removed = false; - profileId = string.Empty; lock (_gate) { - foreach (var pair in _sessionsByProfile) + if (_profileByService.TryGetValue(service, out var existingProfileId)) { - if (ReferenceEquals(pair.Value.Service, service)) - { - profileId = pair.Key; - _sessionsByProfile.Remove(pair.Key); - removed = true; - break; - } + _profileByService.Remove(service); + profileId = existingProfileId; + _sessionsByProfile.Remove(existingProfileId!); + removed = true; + } + else + { + profileId = string.Empty; } } @@ -164,7 +174,11 @@ public IReadOnlyList RemoveWhere(Func();", code, StringComparison.Ordinal); @@ -108,7 +108,7 @@ public void MainPage_DoesNotKeepNavigationCoordinatorAsCodeBehindState() [Fact] public void MainPage_DoesNotImperativelyMutateNavigationSelectionOrInvokeCoordinator() { - var code = LoadFile(@"SalmonEgg\SalmonEgg\MainPage.xaml.cs"); + var code = LoadFile(@"SalmonEgg/SalmonEgg/MainPage.xaml.cs"); Assert.DoesNotContain("MainNavView.SelectedItem =", code, StringComparison.Ordinal); Assert.DoesNotContain("_navigationCoordinator.Activate", code, StringComparison.Ordinal); @@ -119,7 +119,7 @@ public void MainPage_DoesNotImperativelyMutateNavigationSelectionOrInvokeCoordin [Fact] public void MainPage_DoesNotBackWriteSelectionFromFrameNavigation() { - var code = LoadFile(@"SalmonEgg\SalmonEgg\MainPage.xaml.cs"); + var code = LoadFile(@"SalmonEgg/SalmonEgg/MainPage.xaml.cs"); Assert.DoesNotContain("MainNavigationContentSyncAdapter", code, StringComparison.Ordinal); Assert.DoesNotContain("_mainNavigationContentSyncAdapter", code, StringComparison.Ordinal); @@ -130,7 +130,7 @@ public void MainPage_DoesNotBackWriteSelectionFromFrameNavigation() [Fact] public void MainNavigationViewModel_DoesNotExposeLegacySelectedItemAlias() { - var code = LoadFile(@"src\SalmonEgg.Presentation.Core\ViewModels\Navigation\MainNavigationViewModel.cs"); + var code = LoadFile(@"src/SalmonEgg.Presentation.Core\ViewModels\Navigation\MainNavigationViewModel.cs"); Assert.DoesNotContain("public object? SelectedItem =>", code, StringComparison.Ordinal); Assert.DoesNotContain("nameof(SelectedItem)", code, StringComparison.Ordinal); @@ -139,7 +139,7 @@ public void MainNavigationViewModel_DoesNotExposeLegacySelectedItemAlias() [Fact] public void MainNavigationViewModel_DoesNotContainDisplayModeTransitionHacks() { - var code = LoadFile(@"src\SalmonEgg.Presentation.Core\ViewModels\Navigation\MainNavigationViewModel.cs"); + var code = LoadFile(@"src/SalmonEgg.Presentation.Core\ViewModels\Navigation\MainNavigationViewModel.cs"); // These methods were hack workarounds for NavigationView ancestor visual issues. // The correct fix is to let NavigationView handle ancestor visuals natively @@ -153,7 +153,7 @@ public void MainNavigationViewModel_DoesNotContainDisplayModeTransitionHacks() [Fact] public void MainNavigationViewModel_DoesNotDependOnSelectionProjectionApplyGate() { - var code = LoadFile(@"src\SalmonEgg.Presentation.Core\ViewModels\Navigation\MainNavigationViewModel.cs"); + var code = LoadFile(@"src/SalmonEgg.Presentation.Core\ViewModels\Navigation\MainNavigationViewModel.cs"); Assert.DoesNotContain("SelectionProjectionApplyGate", code, StringComparison.Ordinal); Assert.DoesNotContain("BeginSelectionInteraction", code, StringComparison.Ordinal); @@ -163,7 +163,7 @@ public void MainNavigationViewModel_DoesNotDependOnSelectionProjectionApplyGate( [Fact] public void MainPage_DoesNotContainDisplayModeTransitionHacks() { - var code = LoadFile(@"SalmonEgg\SalmonEgg\MainPage.xaml.cs"); + var code = LoadFile(@"SalmonEgg/SalmonEgg/MainPage.xaml.cs"); Assert.DoesNotContain("ClearAndDeferRestore", code, StringComparison.Ordinal); Assert.DoesNotContain("ClearAndRestoreSelectionProjection", code, StringComparison.Ordinal); @@ -175,7 +175,7 @@ public void MainPage_DoesNotContainDisplayModeTransitionHacks() [Fact] public void MainPage_DoesNotRouteLeftNavPaneLifecycleThroughCustomPolicies() { - var code = LoadFile(@"SalmonEgg\SalmonEgg\MainPage.xaml.cs"); + var code = LoadFile(@"SalmonEgg/SalmonEgg/MainPage.xaml.cs"); Assert.DoesNotContain("HandlePanePresentationChanged(", code, StringComparison.Ordinal); Assert.DoesNotContain("ShellPanePolicy.ShouldCancelClosing(", code, StringComparison.Ordinal); @@ -185,7 +185,7 @@ public void MainPage_DoesNotRouteLeftNavPaneLifecycleThroughCustomPolicies() [Fact] public void NavigationSelectionProjector_DoesNotSwapLeafSelectionForAncestorOnClosedPane() { - var code = LoadFile(@"src\SalmonEgg.Presentation.Core\Services\NavigationSelectionProjector.cs"); + var code = LoadFile(@"src/SalmonEgg.Presentation.Core\Services\NavigationSelectionProjector.cs"); Assert.DoesNotContain("project the selected ancestor", code, StringComparison.OrdinalIgnoreCase); Assert.DoesNotContain("controlSelectedItem", code, StringComparison.Ordinal); @@ -195,7 +195,7 @@ public void NavigationSelectionProjector_DoesNotSwapLeafSelectionForAncestorOnCl [Fact] public void MainNavigationXaml_ExposesStableAutomationIds_ForGuiTesting() { - var xaml = LoadFile(@"SalmonEgg\SalmonEgg\MainPage.xaml"); + var xaml = LoadFile(@"SalmonEgg/SalmonEgg/MainPage.xaml"); Assert.Contains("AutomationProperties.AutomationId=\"MainNavView\"", xaml, StringComparison.Ordinal); Assert.Contains("AutomationProperties.AutomationId=\"TitleBar.ToggleSidebar\"", xaml, StringComparison.Ordinal); @@ -210,7 +210,7 @@ public void MainNavigationXaml_ExposesStableAutomationIds_ForGuiTesting() [Fact] public void MainNavigationXaml_DoesNotOverrideNativeChildSelectionProjection() { - var xaml = LoadFile(@"SalmonEgg\SalmonEgg\MainPage.xaml"); + var xaml = LoadFile(@"SalmonEgg/SalmonEgg/MainPage.xaml"); Assert.DoesNotContain("IsChildSelected=\"{x:Bind IsActiveDescendant, Mode=OneWay}\"", xaml, StringComparison.Ordinal); } @@ -218,7 +218,7 @@ public void MainNavigationXaml_DoesNotOverrideNativeChildSelectionProjection() [Fact] public void MainNavigationXaml_UsesNativeAutoPaneDisplayMode() { - var xaml = LoadFile(@"SalmonEgg\SalmonEgg\MainPage.xaml"); + var xaml = LoadFile(@"SalmonEgg/SalmonEgg/MainPage.xaml"); Assert.Contains("PaneDisplayMode=\"Auto\"", xaml, StringComparison.Ordinal); Assert.Contains("CompactModeThresholdWidth=\"640\"", xaml, StringComparison.Ordinal); @@ -229,7 +229,7 @@ public void MainNavigationXaml_UsesNativeAutoPaneDisplayMode() [Fact] public void MainNavigationXaml_BindsNativeSelectedItemToProjectedControlSelection() { - var xaml = LoadFile(@"SalmonEgg\SalmonEgg\MainPage.xaml"); + var xaml = LoadFile(@"SalmonEgg/SalmonEgg/MainPage.xaml"); Assert.Contains( "SelectedItem=\"{x:Bind NavVM.ProjectedControlSelectedItem, Mode=OneWay}\"", @@ -240,7 +240,7 @@ public void MainNavigationXaml_BindsNativeSelectedItemToProjectedControlSelectio [Fact] public void MainPage_TreeRebuildReliesOnProjectedSelectionBindingInsteadOfImperativeSelectedItemWriteback() { - var code = LoadFile(@"SalmonEgg\SalmonEgg\MainPage.xaml.cs"); + var code = LoadFile(@"SalmonEgg/SalmonEgg/MainPage.xaml.cs"); Assert.Contains("NavVM.TreeRebuilt += OnNavigationTreeRebuilt;", code, StringComparison.Ordinal); Assert.DoesNotContain("MainNavView.SelectedItem = NavVM.ProjectedControlSelectedItem;", code, StringComparison.Ordinal); @@ -250,7 +250,7 @@ public void MainPage_TreeRebuildReliesOnProjectedSelectionBindingInsteadOfImpera [Fact] public void MainNavigationViewAdapter_ItemInvoked_OwnsDestinationActivationPath() { - var code = LoadFile(@"SalmonEgg\SalmonEgg\Presentation\Navigation\MainNavigationViewAdapter.cs"); + var code = LoadFile(@"SalmonEgg/SalmonEgg/Presentation\Navigation\MainNavigationViewAdapter.cs"); var section = ExtractSection(code, "private Task HandleItemInvokedCoreAsync"); Assert.Contains("ActivateSettingsAsync", section, StringComparison.Ordinal); @@ -262,7 +262,7 @@ public void MainNavigationViewAdapter_ItemInvoked_OwnsDestinationActivationPath( [Fact] public void MainNavigationViewAdapter_SelectionChanged_DoesNotActivateNavigationDestinations() { - var code = LoadFile(@"SalmonEgg\SalmonEgg\Presentation\Navigation\MainNavigationViewAdapter.cs"); + var code = LoadFile(@"SalmonEgg/SalmonEgg/Presentation\Navigation\MainNavigationViewAdapter.cs"); var section = ExtractSection( code, "public Task HandleSelectionChangedAsync", @@ -276,7 +276,7 @@ public void MainNavigationViewAdapter_SelectionChanged_DoesNotActivateNavigation [Fact] public void MainNavigationViewAdapter_SelectionChanged_DoesNotDependOnProjectedSelectionEcho() { - var code = LoadFile(@"SalmonEgg\SalmonEgg\Presentation\Navigation\MainNavigationViewAdapter.cs"); + var code = LoadFile(@"SalmonEgg/SalmonEgg/Presentation\Navigation\MainNavigationViewAdapter.cs"); var section = ExtractSection( code, "public Task HandleSelectionChangedAsync", @@ -290,7 +290,7 @@ public void MainNavigationViewAdapter_SelectionChanged_DoesNotDependOnProjectedS [Fact] public void SessionsDialogXaml_ExposesStableAutomationIds_ForGuiTesting() { - var xaml = LoadFile(@"SalmonEgg\SalmonEgg\Presentation\Views\Navigation\SessionsListDialog.xaml"); + var xaml = LoadFile(@"SalmonEgg/SalmonEgg/Presentation\Views\Navigation\SessionsListDialog.xaml"); Assert.Contains("AutomationProperties.AutomationId=\"SessionsDialog\"", xaml, StringComparison.Ordinal); Assert.Contains("AutomationProperties.AutomationId=\"SessionsDialog.SearchBox\"", xaml, StringComparison.Ordinal); @@ -301,7 +301,7 @@ public void SessionsDialogXaml_ExposesStableAutomationIds_ForGuiTesting() [Fact] public void StartViewXaml_ExposesStableAutomationIds_ForGuiTesting() { - var xaml = LoadFile(@"SalmonEgg\SalmonEgg\Presentation\Views\Start\StartView.xaml"); + var xaml = LoadFile(@"SalmonEgg/SalmonEgg/Presentation\Views\Start\StartView.xaml"); Assert.Contains("AutomationProperties.AutomationId=\"StartView.Title\"", xaml, StringComparison.Ordinal); } @@ -309,7 +309,7 @@ public void StartViewXaml_ExposesStableAutomationIds_ForGuiTesting() [Fact] public void StartViewXaml_ExposesSharedAgentSelector_ForNewSessionLaunch() { - var xaml = LoadFile(@"SalmonEgg\SalmonEgg\Presentation\Views\Start\StartView.xaml"); + var xaml = LoadFile(@"SalmonEgg/SalmonEgg/Presentation\Views\Start\StartView.xaml"); Assert.Contains("AutomationProperties.AutomationId=\"StartView.AgentSelector\"", xaml, StringComparison.Ordinal); Assert.Contains("ItemsSource=\"{x:Bind ViewModel.Chat.AcpProfileList, Mode=OneWay}\"", xaml, StringComparison.Ordinal); @@ -319,7 +319,7 @@ public void StartViewXaml_ExposesSharedAgentSelector_ForNewSessionLaunch() [Fact] public void StartViewXaml_ExposesProjectSelector_ForNewSessionLaunch() { - var xaml = LoadFile(@"SalmonEgg\SalmonEgg\Presentation\Views\Start\StartView.xaml"); + var xaml = LoadFile(@"SalmonEgg/SalmonEgg/Presentation\Views\Start\StartView.xaml"); Assert.Contains("AutomationProperties.AutomationId=\"StartView.ProjectSelector\"", xaml, StringComparison.Ordinal); Assert.Contains("ItemsSource=\"{x:Bind ViewModel.StartProjectOptions, Mode=OneWay}\"", xaml, StringComparison.Ordinal); @@ -331,7 +331,7 @@ public void StartViewXaml_ExposesProjectSelector_ForNewSessionLaunch() [Fact] public void ChatViewXaml_ExposesStableAutomationIds_ForGuiTesting() { - var xaml = LoadFile(@"SalmonEgg\SalmonEgg\Presentation\Views\Chat\ChatView.xaml"); + var xaml = LoadFile(@"SalmonEgg/SalmonEgg/Presentation\Views\Chat\ChatView.xaml"); Assert.Contains("AutomationProperties.AutomationId=\"ChatView.ActiveRoot\"", xaml, StringComparison.Ordinal); Assert.Contains("AutomationProperties.AutomationId=\"ChatView.CurrentSessionNameButton\"", xaml, StringComparison.Ordinal); @@ -343,7 +343,7 @@ public void ChatViewXaml_ExposesStableAutomationIds_ForGuiTesting() [Fact] public void MainPageXaml_UsesAutomationCapableChatLoadingOverlayRoot() { - var xaml = LoadFile(@"SalmonEgg\SalmonEgg\MainPage.xaml"); + var xaml = LoadFile(@"SalmonEgg/SalmonEgg/MainPage.xaml"); Assert.Contains("AutomationProperties.AutomationId=\"ChatView.LoadingOverlay\"", xaml, StringComparison.Ordinal); Assert.Contains("", xaml, StringComparison.Ordinal); @@ -439,7 +439,7 @@ public void MainNavigationXaml_AddProjectUsesStaticAddIcon() [Fact] public void MainNavigationXaml_ProjectItemsAreGroupingOnly() { - var xaml = LoadFile(@"SalmonEgg\SalmonEgg\MainPage.xaml"); + var xaml = LoadFile(@"SalmonEgg/SalmonEgg/MainPage.xaml"); Assert.Contains("Tag=\"{x:Bind navModels:NavItemTag.Project(ProjectId)}\"", xaml, StringComparison.Ordinal); Assert.Contains("SelectsOnInvoked=\"False\"", xaml, StringComparison.Ordinal); @@ -448,7 +448,7 @@ public void MainNavigationXaml_ProjectItemsAreGroupingOnly() [Fact] public void MainNavigationXaml_DoesNotHookPaneClosingOverride() { - var xaml = LoadFile(@"SalmonEgg\SalmonEgg\MainPage.xaml"); + var xaml = LoadFile(@"SalmonEgg/SalmonEgg/MainPage.xaml"); Assert.DoesNotContain("PaneClosing=\"OnMainNavPaneClosing\"", xaml, StringComparison.Ordinal); } @@ -459,27 +459,27 @@ public void NavigationShell_CompensationPolicyFiles_AreRemoved() var root = FindRepoRoot(); Assert.False( - File.Exists(Path.Combine(root, @"src\SalmonEgg.Application\Common\Shell\NavigationViewPanePresentationPolicy.cs")), + File.Exists(Path.Combine(root, @"src/SalmonEgg.Application\Common\Shell\NavigationViewPanePresentationPolicy.cs")), "NavigationViewPanePresentationPolicy.cs must remain removed. Do not reintroduce pane compensation policies."); Assert.False( - File.Exists(Path.Combine(root, @"src\SalmonEgg.Application\Common\Shell\ShellPanePolicy.cs")), + File.Exists(Path.Combine(root, @"src/SalmonEgg.Application\Common\Shell\ShellPanePolicy.cs")), "ShellPanePolicy.cs must remain removed. Do not reintroduce pane closing suppression policies."); Assert.False( - File.Exists(Path.Combine(root, @"SalmonEgg\SalmonEgg\Presentation\Converters\NavigationPaneDisplayModeConverter.cs")), + File.Exists(Path.Combine(root, @"SalmonEgg/SalmonEgg/Presentation\Converters\NavigationPaneDisplayModeConverter.cs")), "NavigationPaneDisplayModeConverter.cs must remain removed. PaneDisplayMode should stay native Auto."); Assert.False( - File.Exists(Path.Combine(root, @"SalmonEgg\SalmonEgg\Presentation\Navigation\MainNavigationContentSyncAdapter.cs")), + File.Exists(Path.Combine(root, @"SalmonEgg/SalmonEgg/Presentation\Navigation\MainNavigationContentSyncAdapter.cs")), "MainNavigationContentSyncAdapter.cs must remain removed. Frame navigation must not back-write shell selection."); Assert.False( - File.Exists(Path.Combine(root, @"src\SalmonEgg.Presentation.Core\Services\Navigation\SelectionProjectionApplyGate.cs")), + File.Exists(Path.Combine(root, @"src/SalmonEgg.Presentation.Core\Services\Navigation\SelectionProjectionApplyGate.cs")), "SelectionProjectionApplyGate.cs must remain removed. Selection projection must stay state-driven."); } [Fact] public void MainNavigation_SessionFlyout_DefersDialogCommandsUntilFlyoutCloses() { - var xaml = LoadFile(@"SalmonEgg\SalmonEgg\MainPage.xaml"); - var code = LoadFile(@"SalmonEgg\SalmonEgg\MainPage.xaml.cs"); + var xaml = LoadFile(@"SalmonEgg/SalmonEgg/MainPage.xaml"); + var code = LoadFile(@"SalmonEgg/SalmonEgg/MainPage.xaml.cs"); Assert.Contains("x:Uid=\"SessionNavMoveItem\"", xaml, StringComparison.Ordinal); Assert.Contains("Click=\"OnSessionMoveMenuItemClick\"", xaml, StringComparison.Ordinal); diff --git a/tests/SalmonEgg.Presentation.Core.Tests/Settings/AcpConnectionSettingsXamlTests.cs b/tests/SalmonEgg.Presentation.Core.Tests/Settings/AcpConnectionSettingsXamlTests.cs index c95ea365..940da043 100644 --- a/tests/SalmonEgg.Presentation.Core.Tests/Settings/AcpConnectionSettingsXamlTests.cs +++ b/tests/SalmonEgg.Presentation.Core.Tests/Settings/AcpConnectionSettingsXamlTests.cs @@ -8,7 +8,7 @@ public sealed class AcpConnectionSettingsXamlTests public void AcpConnectionSettingsPage_PathMappingsEditor_UsesViewModelDrivenBindings() { // Arrange - var xaml = LoadFile(@"SalmonEgg\SalmonEgg\Presentation\Views\Settings\AcpConnectionSettingsPage.xaml"); + var xaml = LoadFile(@"SalmonEgg/SalmonEgg/Presentation\Views\Settings\AcpConnectionSettingsPage.xaml"); // Assert Assert.Contains("ItemsSource=\"{x:Bind ViewModel.PathMappingRows, Mode=OneWay}\"", xaml, StringComparison.Ordinal); @@ -22,7 +22,7 @@ public void AcpConnectionSettingsPage_PathMappingsEditor_UsesViewModelDrivenBind public void AcpConnectionSettingsPage_PathMappingsEditor_ExposesStableAutomationIds() { // Arrange - var xaml = LoadFile(@"SalmonEgg\SalmonEgg\Presentation\Views\Settings\AcpConnectionSettingsPage.xaml"); + var xaml = LoadFile(@"SalmonEgg/SalmonEgg/Presentation\Views\Settings\AcpConnectionSettingsPage.xaml"); // Assert Assert.Contains("AutomationProperties.AutomationId=\"Acp.PathMappings.Section\"", xaml, StringComparison.Ordinal);