CmdPal: Extract persistence services from SettingsModel and AppStateModel (#46312)
## Summary of the Pull Request
Extracts persistence (load/save) logic from `SettingsModel` and
`AppStateModel` into dedicated service classes, following the
single-responsibility principle. Consumers now interact with
`ISettingsService` and `IAppStateService` instead of receiving raw model
objects through DI.
**New services introduced:**
- `IPersistenceService` / `PersistenceService` — generic `Load<T>` /
`Save<T>` with AOT-compatible `JsonTypeInfo<T>`, ensures target
directory exists before writing
- `ISettingsService` / `SettingsService` — loads settings on
construction, runs migrations, exposes `Settings` property and
`SettingsChanged` event
- `IAppStateService` / `AppStateService` — loads state on construction,
exposes `State` property and `StateChanged` event
**Key changes:**
- `SettingsModel` and `AppStateModel` are now pure data models — all
file I/O, migration, and directory management removed
- Raw `SettingsModel` and `AppStateModel` removed from DI container;
consumers receive the appropriate service
- `IApplicationInfoService.ConfigDirectory` injected into services for
config path resolution (no more hardcoded `Utilities.BaseSettingsPath`)
- ~30 consumer files updated across `Microsoft.CmdPal.UI.ViewModels` and
`Microsoft.CmdPal.UI` projects
- All `#pragma warning disable SA1300` suppressions removed —
convenience accessors replaced with direct `_settingsService.Settings` /
`_appStateService.State` access
- Namespace prefixes (`Services.ISettingsService`) replaced with proper
`using` directives
## PR Checklist
- [ ] **Communication:** I've discussed this with core contributors
already.
- [x] **Tests:** Added/updated and all pass
- [ ] **Localization:** N/A — no end-user-facing strings changed
- [ ] **Dev docs:** N/A — internal refactor, no public API changes
- [ ] **New binaries:** N/A — no new binaries introduced
## Detailed Description of the Pull Request / Additional comments
### Architecture
Services are registered as singletons in `App.xaml.cs`:
```csharp
services.AddSingleton<IPersistenceService, PersistenceService>();
services.AddSingleton<ISettingsService, SettingsService>();
services.AddSingleton<IAppStateService, AppStateService>();
```
`PersistenceService.Save<T>` writes the serialized model directly to
disk, creating the target directory if it doesn't exist. It also does
not attempt to merge existing and new settings/state. `SettingsService`
runs hotkey migrations on load and raises `SettingsChanged` after saves.
`AppStateService` always raises `StateChanged` after saves.
### Files changed (41 files, +1169/−660)
| Area | Files | What changed |
|------|-------|-------------|
| New services | `Services/IPersistenceService.cs`,
`PersistenceService.cs`, `ISettingsService.cs`, `SettingsService.cs`,
`IAppStateService.cs`, `AppStateService.cs` | New service interfaces and
implementations |
| Models | `SettingsModel.cs`, `AppStateModel.cs` | Stripped to pure
data bags |
| DI | `App.xaml.cs` | Service registration, removed raw model DI |
| ViewModels | 12 files | Constructor injection of services |
| UI | 10 files | Service injection replacing model access |
| Settings | `DockSettings.cs` | `Colors.Transparent` replaced with
struct literal to avoid WinUI3 COM dependency |
| Tests | `PersistenceServiceTests.cs`, `SettingsServiceTests.cs`,
`AppStateServiceTests.cs` | 38 unit tests covering all three services |
| Config | `.gitignore` | Added `.squad/`, `.github/agents/` exclusions
|
## Validation Steps Performed
- Built `Microsoft.CmdPal.UI` with MSBuild (x64/Debug) — exit code 0,
clean build
- Ran 38 unit tests via `vstest.console.exe` — all passing
- Verified no remaining `#pragma warning disable SA1300` blocks
- Verified no remaining `Services.` namespace prefixes
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-20 18:58:27 -05:00
|
|
|
// 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;
|
2026-03-27 12:54:58 -05:00
|
|
|
using System.Collections.Immutable;
|
CmdPal: Extract persistence services from SettingsModel and AppStateModel (#46312)
## Summary of the Pull Request
Extracts persistence (load/save) logic from `SettingsModel` and
`AppStateModel` into dedicated service classes, following the
single-responsibility principle. Consumers now interact with
`ISettingsService` and `IAppStateService` instead of receiving raw model
objects through DI.
**New services introduced:**
- `IPersistenceService` / `PersistenceService` — generic `Load<T>` /
`Save<T>` with AOT-compatible `JsonTypeInfo<T>`, ensures target
directory exists before writing
- `ISettingsService` / `SettingsService` — loads settings on
construction, runs migrations, exposes `Settings` property and
`SettingsChanged` event
- `IAppStateService` / `AppStateService` — loads state on construction,
exposes `State` property and `StateChanged` event
**Key changes:**
- `SettingsModel` and `AppStateModel` are now pure data models — all
file I/O, migration, and directory management removed
- Raw `SettingsModel` and `AppStateModel` removed from DI container;
consumers receive the appropriate service
- `IApplicationInfoService.ConfigDirectory` injected into services for
config path resolution (no more hardcoded `Utilities.BaseSettingsPath`)
- ~30 consumer files updated across `Microsoft.CmdPal.UI.ViewModels` and
`Microsoft.CmdPal.UI` projects
- All `#pragma warning disable SA1300` suppressions removed —
convenience accessors replaced with direct `_settingsService.Settings` /
`_appStateService.State` access
- Namespace prefixes (`Services.ISettingsService`) replaced with proper
`using` directives
## PR Checklist
- [ ] **Communication:** I've discussed this with core contributors
already.
- [x] **Tests:** Added/updated and all pass
- [ ] **Localization:** N/A — no end-user-facing strings changed
- [ ] **Dev docs:** N/A — internal refactor, no public API changes
- [ ] **New binaries:** N/A — no new binaries introduced
## Detailed Description of the Pull Request / Additional comments
### Architecture
Services are registered as singletons in `App.xaml.cs`:
```csharp
services.AddSingleton<IPersistenceService, PersistenceService>();
services.AddSingleton<ISettingsService, SettingsService>();
services.AddSingleton<IAppStateService, AppStateService>();
```
`PersistenceService.Save<T>` writes the serialized model directly to
disk, creating the target directory if it doesn't exist. It also does
not attempt to merge existing and new settings/state. `SettingsService`
runs hotkey migrations on load and raises `SettingsChanged` after saves.
`AppStateService` always raises `StateChanged` after saves.
### Files changed (41 files, +1169/−660)
| Area | Files | What changed |
|------|-------|-------------|
| New services | `Services/IPersistenceService.cs`,
`PersistenceService.cs`, `ISettingsService.cs`, `SettingsService.cs`,
`IAppStateService.cs`, `AppStateService.cs` | New service interfaces and
implementations |
| Models | `SettingsModel.cs`, `AppStateModel.cs` | Stripped to pure
data bags |
| DI | `App.xaml.cs` | Service registration, removed raw model DI |
| ViewModels | 12 files | Constructor injection of services |
| UI | 10 files | Service injection replacing model access |
| Settings | `DockSettings.cs` | `Colors.Transparent` replaced with
struct literal to avoid WinUI3 COM dependency |
| Tests | `PersistenceServiceTests.cs`, `SettingsServiceTests.cs`,
`AppStateServiceTests.cs` | 38 unit tests covering all three services |
| Config | `.gitignore` | Added `.squad/`, `.github/agents/` exclusions
|
## Validation Steps Performed
- Built `Microsoft.CmdPal.UI` with MSBuild (x64/Debug) — exit code 0,
clean build
- Ran 38 unit tests via `vstest.console.exe` — all passing
- Verified no remaining `#pragma warning disable SA1300` blocks
- Verified no remaining `Services.` namespace prefixes
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-20 18:58:27 -05:00
|
|
|
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<IPersistenceService> _mockPersistence = null!;
|
|
|
|
|
private Mock<IApplicationInfoService> _mockAppInfo = null!;
|
|
|
|
|
private string _testDirectory = null!;
|
|
|
|
|
|
|
|
|
|
[TestInitialize]
|
|
|
|
|
public void Setup()
|
|
|
|
|
{
|
|
|
|
|
_mockPersistence = new Mock<IPersistenceService>();
|
|
|
|
|
_mockAppInfo = new Mock<IApplicationInfoService>();
|
|
|
|
|
_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<string>(),
|
|
|
|
|
It.IsAny<System.Text.Json.Serialization.Metadata.JsonTypeInfo<AppStateModel>>()))
|
|
|
|
|
.Returns(new AppStateModel());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[TestMethod]
|
|
|
|
|
public void Constructor_LoadsState_ViaPersistenceService()
|
|
|
|
|
{
|
|
|
|
|
// Arrange
|
|
|
|
|
var expectedState = new AppStateModel
|
|
|
|
|
{
|
2026-03-27 12:54:58 -05:00
|
|
|
RunHistory = ImmutableList.Create("command1", "command2"),
|
CmdPal: Extract persistence services from SettingsModel and AppStateModel (#46312)
## Summary of the Pull Request
Extracts persistence (load/save) logic from `SettingsModel` and
`AppStateModel` into dedicated service classes, following the
single-responsibility principle. Consumers now interact with
`ISettingsService` and `IAppStateService` instead of receiving raw model
objects through DI.
**New services introduced:**
- `IPersistenceService` / `PersistenceService` — generic `Load<T>` /
`Save<T>` with AOT-compatible `JsonTypeInfo<T>`, ensures target
directory exists before writing
- `ISettingsService` / `SettingsService` — loads settings on
construction, runs migrations, exposes `Settings` property and
`SettingsChanged` event
- `IAppStateService` / `AppStateService` — loads state on construction,
exposes `State` property and `StateChanged` event
**Key changes:**
- `SettingsModel` and `AppStateModel` are now pure data models — all
file I/O, migration, and directory management removed
- Raw `SettingsModel` and `AppStateModel` removed from DI container;
consumers receive the appropriate service
- `IApplicationInfoService.ConfigDirectory` injected into services for
config path resolution (no more hardcoded `Utilities.BaseSettingsPath`)
- ~30 consumer files updated across `Microsoft.CmdPal.UI.ViewModels` and
`Microsoft.CmdPal.UI` projects
- All `#pragma warning disable SA1300` suppressions removed —
convenience accessors replaced with direct `_settingsService.Settings` /
`_appStateService.State` access
- Namespace prefixes (`Services.ISettingsService`) replaced with proper
`using` directives
## PR Checklist
- [ ] **Communication:** I've discussed this with core contributors
already.
- [x] **Tests:** Added/updated and all pass
- [ ] **Localization:** N/A — no end-user-facing strings changed
- [ ] **Dev docs:** N/A — internal refactor, no public API changes
- [ ] **New binaries:** N/A — no new binaries introduced
## Detailed Description of the Pull Request / Additional comments
### Architecture
Services are registered as singletons in `App.xaml.cs`:
```csharp
services.AddSingleton<IPersistenceService, PersistenceService>();
services.AddSingleton<ISettingsService, SettingsService>();
services.AddSingleton<IAppStateService, AppStateService>();
```
`PersistenceService.Save<T>` writes the serialized model directly to
disk, creating the target directory if it doesn't exist. It also does
not attempt to merge existing and new settings/state. `SettingsService`
runs hotkey migrations on load and raises `SettingsChanged` after saves.
`AppStateService` always raises `StateChanged` after saves.
### Files changed (41 files, +1169/−660)
| Area | Files | What changed |
|------|-------|-------------|
| New services | `Services/IPersistenceService.cs`,
`PersistenceService.cs`, `ISettingsService.cs`, `SettingsService.cs`,
`IAppStateService.cs`, `AppStateService.cs` | New service interfaces and
implementations |
| Models | `SettingsModel.cs`, `AppStateModel.cs` | Stripped to pure
data bags |
| DI | `App.xaml.cs` | Service registration, removed raw model DI |
| ViewModels | 12 files | Constructor injection of services |
| UI | 10 files | Service injection replacing model access |
| Settings | `DockSettings.cs` | `Colors.Transparent` replaced with
struct literal to avoid WinUI3 COM dependency |
| Tests | `PersistenceServiceTests.cs`, `SettingsServiceTests.cs`,
`AppStateServiceTests.cs` | 38 unit tests covering all three services |
| Config | `.gitignore` | Added `.squad/`, `.github/agents/` exclusions
|
## Validation Steps Performed
- Built `Microsoft.CmdPal.UI` with MSBuild (x64/Debug) — exit code 0,
clean build
- Ran 38 unit tests via `vstest.console.exe` — all passing
- Verified no remaining `#pragma warning disable SA1300` blocks
- Verified no remaining `Services.` namespace prefixes
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-20 18:58:27 -05:00
|
|
|
};
|
|
|
|
|
_mockPersistence
|
|
|
|
|
.Setup(p => p.Load(
|
|
|
|
|
It.IsAny<string>(),
|
|
|
|
|
It.IsAny<System.Text.Json.Serialization.Metadata.JsonTypeInfo<AppStateModel>>()))
|
|
|
|
|
.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<string>(),
|
|
|
|
|
It.IsAny<System.Text.Json.Serialization.Metadata.JsonTypeInfo<AppStateModel>>()),
|
|
|
|
|
Times.Once);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[TestMethod]
|
|
|
|
|
public void State_ReturnsLoadedModel()
|
|
|
|
|
{
|
|
|
|
|
// Arrange
|
|
|
|
|
var expectedState = new AppStateModel();
|
|
|
|
|
_mockPersistence
|
|
|
|
|
.Setup(p => p.Load(
|
|
|
|
|
It.IsAny<string>(),
|
|
|
|
|
It.IsAny<System.Text.Json.Serialization.Metadata.JsonTypeInfo<AppStateModel>>()))
|
|
|
|
|
.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);
|
2026-03-27 12:54:58 -05:00
|
|
|
service.UpdateState(s => s with { RunHistory = s.RunHistory.Add("test-command") });
|
|
|
|
|
_mockPersistence.Invocations.Clear(); // Reset after Arrange — UpdateState also persists
|
CmdPal: Extract persistence services from SettingsModel and AppStateModel (#46312)
## Summary of the Pull Request
Extracts persistence (load/save) logic from `SettingsModel` and
`AppStateModel` into dedicated service classes, following the
single-responsibility principle. Consumers now interact with
`ISettingsService` and `IAppStateService` instead of receiving raw model
objects through DI.
**New services introduced:**
- `IPersistenceService` / `PersistenceService` — generic `Load<T>` /
`Save<T>` with AOT-compatible `JsonTypeInfo<T>`, ensures target
directory exists before writing
- `ISettingsService` / `SettingsService` — loads settings on
construction, runs migrations, exposes `Settings` property and
`SettingsChanged` event
- `IAppStateService` / `AppStateService` — loads state on construction,
exposes `State` property and `StateChanged` event
**Key changes:**
- `SettingsModel` and `AppStateModel` are now pure data models — all
file I/O, migration, and directory management removed
- Raw `SettingsModel` and `AppStateModel` removed from DI container;
consumers receive the appropriate service
- `IApplicationInfoService.ConfigDirectory` injected into services for
config path resolution (no more hardcoded `Utilities.BaseSettingsPath`)
- ~30 consumer files updated across `Microsoft.CmdPal.UI.ViewModels` and
`Microsoft.CmdPal.UI` projects
- All `#pragma warning disable SA1300` suppressions removed —
convenience accessors replaced with direct `_settingsService.Settings` /
`_appStateService.State` access
- Namespace prefixes (`Services.ISettingsService`) replaced with proper
`using` directives
## PR Checklist
- [ ] **Communication:** I've discussed this with core contributors
already.
- [x] **Tests:** Added/updated and all pass
- [ ] **Localization:** N/A — no end-user-facing strings changed
- [ ] **Dev docs:** N/A — internal refactor, no public API changes
- [ ] **New binaries:** N/A — no new binaries introduced
## Detailed Description of the Pull Request / Additional comments
### Architecture
Services are registered as singletons in `App.xaml.cs`:
```csharp
services.AddSingleton<IPersistenceService, PersistenceService>();
services.AddSingleton<ISettingsService, SettingsService>();
services.AddSingleton<IAppStateService, AppStateService>();
```
`PersistenceService.Save<T>` writes the serialized model directly to
disk, creating the target directory if it doesn't exist. It also does
not attempt to merge existing and new settings/state. `SettingsService`
runs hotkey migrations on load and raises `SettingsChanged` after saves.
`AppStateService` always raises `StateChanged` after saves.
### Files changed (41 files, +1169/−660)
| Area | Files | What changed |
|------|-------|-------------|
| New services | `Services/IPersistenceService.cs`,
`PersistenceService.cs`, `ISettingsService.cs`, `SettingsService.cs`,
`IAppStateService.cs`, `AppStateService.cs` | New service interfaces and
implementations |
| Models | `SettingsModel.cs`, `AppStateModel.cs` | Stripped to pure
data bags |
| DI | `App.xaml.cs` | Service registration, removed raw model DI |
| ViewModels | 12 files | Constructor injection of services |
| UI | 10 files | Service injection replacing model access |
| Settings | `DockSettings.cs` | `Colors.Transparent` replaced with
struct literal to avoid WinUI3 COM dependency |
| Tests | `PersistenceServiceTests.cs`, `SettingsServiceTests.cs`,
`AppStateServiceTests.cs` | 38 unit tests covering all three services |
| Config | `.gitignore` | Added `.squad/`, `.github/agents/` exclusions
|
## Validation Steps Performed
- Built `Microsoft.CmdPal.UI` with MSBuild (x64/Debug) — exit code 0,
clean build
- Ran 38 unit tests via `vstest.console.exe` — all passing
- Verified no remaining `#pragma warning disable SA1300` blocks
- Verified no remaining `Services.` namespace prefixes
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-20 18:58:27 -05:00
|
|
|
|
|
|
|
|
// Act
|
|
|
|
|
service.Save();
|
|
|
|
|
|
|
|
|
|
// Assert
|
|
|
|
|
_mockPersistence.Verify(
|
|
|
|
|
p => p.Save(
|
|
|
|
|
service.State,
|
|
|
|
|
It.IsAny<string>(),
|
|
|
|
|
It.IsAny<System.Text.Json.Serialization.Metadata.JsonTypeInfo<AppStateModel>>()),
|
|
|
|
|
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);
|
|
|
|
|
}
|
2026-03-27 12:54:58 -05:00
|
|
|
|
|
|
|
|
[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}");
|
|
|
|
|
}
|
|
|
|
|
}
|
CmdPal: Extract persistence services from SettingsModel and AppStateModel (#46312)
## Summary of the Pull Request
Extracts persistence (load/save) logic from `SettingsModel` and
`AppStateModel` into dedicated service classes, following the
single-responsibility principle. Consumers now interact with
`ISettingsService` and `IAppStateService` instead of receiving raw model
objects through DI.
**New services introduced:**
- `IPersistenceService` / `PersistenceService` — generic `Load<T>` /
`Save<T>` with AOT-compatible `JsonTypeInfo<T>`, ensures target
directory exists before writing
- `ISettingsService` / `SettingsService` — loads settings on
construction, runs migrations, exposes `Settings` property and
`SettingsChanged` event
- `IAppStateService` / `AppStateService` — loads state on construction,
exposes `State` property and `StateChanged` event
**Key changes:**
- `SettingsModel` and `AppStateModel` are now pure data models — all
file I/O, migration, and directory management removed
- Raw `SettingsModel` and `AppStateModel` removed from DI container;
consumers receive the appropriate service
- `IApplicationInfoService.ConfigDirectory` injected into services for
config path resolution (no more hardcoded `Utilities.BaseSettingsPath`)
- ~30 consumer files updated across `Microsoft.CmdPal.UI.ViewModels` and
`Microsoft.CmdPal.UI` projects
- All `#pragma warning disable SA1300` suppressions removed —
convenience accessors replaced with direct `_settingsService.Settings` /
`_appStateService.State` access
- Namespace prefixes (`Services.ISettingsService`) replaced with proper
`using` directives
## PR Checklist
- [ ] **Communication:** I've discussed this with core contributors
already.
- [x] **Tests:** Added/updated and all pass
- [ ] **Localization:** N/A — no end-user-facing strings changed
- [ ] **Dev docs:** N/A — internal refactor, no public API changes
- [ ] **New binaries:** N/A — no new binaries introduced
## Detailed Description of the Pull Request / Additional comments
### Architecture
Services are registered as singletons in `App.xaml.cs`:
```csharp
services.AddSingleton<IPersistenceService, PersistenceService>();
services.AddSingleton<ISettingsService, SettingsService>();
services.AddSingleton<IAppStateService, AppStateService>();
```
`PersistenceService.Save<T>` writes the serialized model directly to
disk, creating the target directory if it doesn't exist. It also does
not attempt to merge existing and new settings/state. `SettingsService`
runs hotkey migrations on load and raises `SettingsChanged` after saves.
`AppStateService` always raises `StateChanged` after saves.
### Files changed (41 files, +1169/−660)
| Area | Files | What changed |
|------|-------|-------------|
| New services | `Services/IPersistenceService.cs`,
`PersistenceService.cs`, `ISettingsService.cs`, `SettingsService.cs`,
`IAppStateService.cs`, `AppStateService.cs` | New service interfaces and
implementations |
| Models | `SettingsModel.cs`, `AppStateModel.cs` | Stripped to pure
data bags |
| DI | `App.xaml.cs` | Service registration, removed raw model DI |
| ViewModels | 12 files | Constructor injection of services |
| UI | 10 files | Service injection replacing model access |
| Settings | `DockSettings.cs` | `Colors.Transparent` replaced with
struct literal to avoid WinUI3 COM dependency |
| Tests | `PersistenceServiceTests.cs`, `SettingsServiceTests.cs`,
`AppStateServiceTests.cs` | 38 unit tests covering all three services |
| Config | `.gitignore` | Added `.squad/`, `.github/agents/` exclusions
|
## Validation Steps Performed
- Built `Microsoft.CmdPal.UI` with MSBuild (x64/Debug) — exit code 0,
clean build
- Ran 38 unit tests via `vstest.console.exe` — all passing
- Verified no remaining `#pragma warning disable SA1300` blocks
- Verified no remaining `Services.` namespace prefixes
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-20 18:58:27 -05:00
|
|
|
}
|