// Copyright (c) Microsoft Corporation // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System; using System.Collections.Generic; using System.Collections.Immutable; using System.IO; using Microsoft.CmdPal.Common.Services; using Microsoft.CmdPal.UI.ViewModels.Services; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; namespace Microsoft.CmdPal.UI.ViewModels.UnitTests; [TestClass] public class AppStateServiceTests { private Mock _mockPersistence = null!; private Mock _mockAppInfo = null!; private string _testDirectory = null!; [TestInitialize] public void Setup() { _mockPersistence = new Mock(); _mockAppInfo = new Mock(); _testDirectory = Path.Combine(Path.GetTempPath(), $"CmdPalTest_{Guid.NewGuid():N}"); _mockAppInfo.Setup(a => a.ConfigDirectory).Returns(_testDirectory); // Default: Load returns a new AppStateModel _mockPersistence .Setup(p => p.Load( It.IsAny(), It.IsAny>())) .Returns(new AppStateModel()); } [TestMethod] public void Constructor_LoadsState_ViaPersistenceService() { // Arrange var expectedState = new AppStateModel { RunHistory = ImmutableList.Create("command1", "command2"), }; _mockPersistence .Setup(p => p.Load( It.IsAny(), It.IsAny>())) .Returns(expectedState); // Act var service = new AppStateService(_mockPersistence.Object, _mockAppInfo.Object); // Assert Assert.IsNotNull(service.State); Assert.AreEqual(2, service.State.RunHistory.Count); Assert.AreEqual("command1", service.State.RunHistory[0]); _mockPersistence.Verify( p => p.Load( It.IsAny(), It.IsAny>()), Times.Once); } [TestMethod] public void State_ReturnsLoadedModel() { // Arrange var expectedState = new AppStateModel(); _mockPersistence .Setup(p => p.Load( It.IsAny(), It.IsAny>())) .Returns(expectedState); // Act var service = new AppStateService(_mockPersistence.Object, _mockAppInfo.Object); // Assert Assert.AreSame(expectedState, service.State); } [TestMethod] public void Save_DelegatesToPersistenceService() { // Arrange var service = new AppStateService(_mockPersistence.Object, _mockAppInfo.Object); service.UpdateState(s => s with { RunHistory = s.RunHistory.Add("test-command") }); _mockPersistence.Invocations.Clear(); // Reset after Arrange — UpdateState also persists // Act service.Save(); // Assert _mockPersistence.Verify( p => p.Save( service.State, It.IsAny(), It.IsAny>()), Times.Once); } [TestMethod] public void Save_RaisesStateChangedEvent() { // Arrange var service = new AppStateService(_mockPersistence.Object, _mockAppInfo.Object); var eventRaised = false; service.StateChanged += (sender, state) => { eventRaised = true; }; // Act service.Save(); // Assert Assert.IsTrue(eventRaised); } [TestMethod] public void StateChanged_PassesCorrectArguments() { // Arrange var service = new AppStateService(_mockPersistence.Object, _mockAppInfo.Object); IAppStateService? receivedSender = null; AppStateModel? receivedState = null; service.StateChanged += (sender, state) => { receivedSender = sender; receivedState = state; }; // Act service.Save(); // Assert Assert.AreSame(service, receivedSender); Assert.AreSame(service.State, receivedState); } [TestMethod] public void Save_Always_RaisesStateChangedEvent() { // Arrange - AppStateService.Save() should always raise StateChanged // (unlike SettingsService which has hotReload parameter) var service = new AppStateService(_mockPersistence.Object, _mockAppInfo.Object); var eventCount = 0; service.StateChanged += (sender, state) => { eventCount++; }; // Act service.Save(); service.Save(); // Assert Assert.AreEqual(2, eventCount); } [TestMethod] public void UpdateState_ConcurrentUpdates_NoLostUpdates() { // Arrange — two threads each add items to RunHistory concurrently. // With the CAS loop, every add must land (no lost updates). var service = new AppStateService(_mockPersistence.Object, _mockAppInfo.Object); const int iterations = 50; var barrier = new System.Threading.Barrier(2); // Act var t1 = System.Threading.Tasks.Task.Run(() => { barrier.SignalAndWait(); for (var i = 0; i < iterations; i++) { service.UpdateState(s => s with { RunHistory = s.RunHistory.Add($"t1-{i}") }); } }); var t2 = System.Threading.Tasks.Task.Run(() => { barrier.SignalAndWait(); for (var i = 0; i < iterations; i++) { service.UpdateState(s => s with { RunHistory = s.RunHistory.Add($"t2-{i}") }); } }); System.Threading.Tasks.Task.WaitAll(t1, t2); // Assert — all 100 items must be present (no lost updates) Assert.AreEqual(iterations * 2, service.State.RunHistory.Count, "All concurrent updates should be preserved"); for (var i = 0; i < iterations; i++) { Assert.IsTrue(service.State.RunHistory.Contains($"t1-{i}"), $"Missing t1-{i}"); Assert.IsTrue(service.State.RunHistory.Contains($"t2-{i}"), $"Missing t2-{i}"); } } }