- 01-03: wave 2 → wave 3 (depends on 01-02 which is also wave 2; must be wave 3) - 01-06: add ProgressUpdatedMessage.cs to files_modified; add third StatusBarItem (progress %) to XAML per locked CONTEXT.md decision; add ProgressUpdatedMessage subscription in MainWindowViewModel.OnActivated() - 01-08: add comment to empty <files> element (auto task with no file output) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
12 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 01-foundation | 03 | execute | 3 |
|
|
true |
|
|
Purpose: Profiles and settings are the first user-visible data. Corrupt files or wrong field names would break existing users' data on migration. Unit tests lock in the JSON schema contract. Output: 4 production files + 2 test files with passing unit tests.
<execution_context> @C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md @C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/phases/01-foundation/01-CONTEXT.md @.planning/phases/01-foundation/01-RESEARCH.md @.planning/phases/01-foundation/01-02-SUMMARY.md ```csharp namespace SharepointToolbox.Core.Models; public class TenantProfile { public string Name { get; set; } = string.Empty; public string TenantUrl { get; set; } = string.Empty; public string ClientId { get; set; } = string.Empty; } ```// Sharepoint_Export_profiles.json { "profiles": [{ "name": "...", "tenantUrl": "...", "clientId": "..." }] }
// Sharepoint_Settings.json { "dataFolder": "...", "lang": "en" }
Task 1: ProfileRepository and ProfileService with write-then-replace SharepointToolbox/Infrastructure/Persistence/ProfileRepository.cs, SharepointToolbox/Services/ProfileService.cs, SharepointToolbox.Tests/Services/ProfileServiceTests.cs - Test: SaveAsync then LoadAsync round-trips a list of TenantProfiles with correct field values - Test: LoadAsync on missing file returns empty list (no exception) - Test: LoadAsync on corrupt JSON throws InvalidDataException (not silently returns empty) - Test: Concurrent SaveAsync calls don't corrupt the file (SemaphoreSlim ensures ordering) - Test: ProfileService.AddProfile assigns the new profile and persists immediately - Test: ProfileService.RenameProfile changes Name, persists, throws if profile not found - Test: ProfileService.DeleteProfile removes by Name, throws if not found - Test: Saved JSON wraps profiles in { "profiles": [...] } root object (schema compatibility) Create `Infrastructure/Persistence/` and `Services/` directories.**ProfileRepository.cs** — handles raw file I/O:
```csharp
namespace SharepointToolbox.Infrastructure.Persistence;
public class ProfileRepository
{
private readonly string _filePath;
private readonly SemaphoreSlim _writeLock = new(1, 1);
public ProfileRepository(string filePath)
{
_filePath = filePath;
}
public async Task<IReadOnlyList<TenantProfile>> LoadAsync()
{
if (!File.Exists(_filePath))
return Array.Empty<TenantProfile>();
var json = await File.ReadAllTextAsync(_filePath, Encoding.UTF8);
var root = JsonSerializer.Deserialize<ProfilesRoot>(json,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
return root?.Profiles ?? Array.Empty<TenantProfile>();
}
public async Task SaveAsync(IReadOnlyList<TenantProfile> profiles)
{
await _writeLock.WaitAsync();
try
{
var root = new ProfilesRoot { Profiles = profiles.ToList() };
var json = JsonSerializer.Serialize(root,
new JsonSerializerOptions { WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
var tmpPath = _filePath + ".tmp";
Directory.CreateDirectory(Path.GetDirectoryName(_filePath)!);
await File.WriteAllTextAsync(tmpPath, json, Encoding.UTF8);
// Validate round-trip before replacing
JsonDocument.Parse(await File.ReadAllTextAsync(tmpPath)).Dispose();
File.Move(tmpPath, _filePath, overwrite: true);
}
finally { _writeLock.Release(); }
}
private sealed class ProfilesRoot
{
public List<TenantProfile> Profiles { get; set; } = new();
}
}
```
Note: Use `PropertyNamingPolicy = JsonNamingPolicy.CamelCase` to serialize `Name`→`name`, `TenantUrl`→`tenantUrl`, `ClientId`→`clientId` matching the existing JSON schema.
**ProfileService.cs** — CRUD on top of repository:
- Constructor takes `ProfileRepository` (inject via DI later; for now accept in constructor)
- `Task<IReadOnlyList<TenantProfile>> GetProfilesAsync()`
- `Task AddProfileAsync(TenantProfile profile)` — validates Name not empty, TenantUrl valid URL, ClientId not empty; throws `ArgumentException` for invalid inputs
- `Task RenameProfileAsync(string existingName, string newName)` — throws `KeyNotFoundException` if not found
- `Task DeleteProfileAsync(string name)` — throws `KeyNotFoundException` if not found
- All mutations load → modify in-memory list → save (single-load-modify-save to preserve order)
**ProfileServiceTests.cs** — Replace the stub with real tests using temp file paths:
```csharp
public class ProfileServiceTests : IDisposable
{
private readonly string _tempFile = Path.GetTempFileName();
// Dispose deletes temp file
[Fact]
public async Task SaveAndLoad_RoundTrips_Profiles() { ... }
// etc.
}
```
Tests must use a temp file, not the real user data file. All tests in `[Trait("Category", "Unit")]`.
cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet test SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "FullyQualifiedName~ProfileServiceTests" 2>&1 | tail -10
All ProfileServiceTests pass (no skips). JSON output uses camelCase field names matching existing schema. Write-then-replace with SemaphoreSlim implemented.
Task 2: SettingsRepository and SettingsService
SharepointToolbox/Infrastructure/Persistence/SettingsRepository.cs,
SharepointToolbox/Services/SettingsService.cs,
SharepointToolbox.Tests/Services/SettingsServiceTests.cs
- Test: LoadAsync returns default settings (dataFolder = empty string, lang = "en") when file missing
- Test: SaveAsync then LoadAsync round-trips dataFolder and lang values exactly
- Test: Serialized JSON contains "dataFolder" and "lang" keys (not DataFolder/Lang — schema compatibility)
- Test: SaveAsync uses write-then-replace (tmp file created, then moved)
- Test: SettingsService.SetLanguageAsync("fr") persists lang="fr"
- Test: SettingsService.SetDataFolderAsync("C:\\Exports") persists dataFolder path
**AppSettings model** (add to `Core/Models/AppSettings.cs`):
```csharp
namespace SharepointToolbox.Core.Models;
public class AppSettings
{
public string DataFolder { get; set; } = string.Empty;
public string Lang { get; set; } = "en";
}
```
Note: STJ with `PropertyNamingPolicy.CamelCase` will serialize `DataFolder`→`dataFolder`, `Lang`→`lang`.
**SettingsRepository.cs** — same write-then-replace pattern as ProfileRepository:
- `Task<AppSettings> LoadAsync()` — returns `new AppSettings()` if file missing; throws `InvalidDataException` on corrupt JSON
- `Task SaveAsync(AppSettings settings)` — write-then-replace with `SemaphoreSlim(1)` and camelCase serialization
**SettingsService.cs**:
- Constructor takes `SettingsRepository`
- `Task<AppSettings> GetSettingsAsync()`
- `Task SetLanguageAsync(string cultureCode)` — validates "en" or "fr"; throws `ArgumentException` otherwise
- `Task SetDataFolderAsync(string path)` — saves path (empty string allowed — means default)
**SettingsServiceTests.cs** — Replace stub with real tests using temp file.
All tests in `[Trait("Category", "Unit")]`.
cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet test SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "FullyQualifiedName~SettingsServiceTests" 2>&1 | tail -10
All SettingsServiceTests pass. AppSettings serializes to dataFolder/lang (camelCase). Default settings returned when file is absent.
- `dotnet test SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "Category=Unit"` — all pass
- JSON output from ProfileRepository contains `"profiles"` root key with `"name"`, `"tenantUrl"`, `"clientId"` field names
- JSON output from SettingsRepository contains `"dataFolder"` and `"lang"` field names
- Both repositories use `SemaphoreSlim(1)` write lock
- Both repositories use write-then-replace (`.tmp` file then `File.Move`)
<success_criteria> Unit tests green for ProfileService and SettingsService. JSON schema compatibility verified by test assertions on serialized output. Write-then-replace pattern protects against crash-corruption. </success_criteria>
After completion, create `.planning/phases/01-foundation/01-03-SUMMARY.md`