diff --git a/.deepsource.toml b/.deepsource.toml new file mode 100644 index 0000000..2de0446 --- /dev/null +++ b/.deepsource.toml @@ -0,0 +1,46 @@ +version = 1 + +# Keep this file explicit so provider-side analysis re-reads repo scope +# changes. Mirrors the conventions used across the Prekzursil fleet +# (env-inspector, Airline-Reservations-System, etc.). + +exclude_patterns = [ + "**/bin/**", + "**/obj/**", + "**/TestResults/**", + "artifacts/**", + "publish/**", + "packaging/**", + "docs/**", + "fixtures/**", + "scripts/**", + "tests/**", +] + +test_patterns = [ + "tests/**", + "**/*.Tests/**", + "**/*Tests.cs", +] + +[[analyzers]] +name = "csharp" +enabled = true + + [analyzers.meta] + # The .NET SDK pin lives in ``global.json`` (currently 8.0.x — see the + # ``sdk.version`` field, "8.0.419"). DeepSource accepts the dotted + # family identifier and resolves to the latest patch. + dotnet_version = "8.0" + +[[analyzers]] +name = "secrets" +enabled = true + +[[analyzers]] +name = "shell" +enabled = true + +[[analyzers]] +name = "test-coverage" +enabled = true diff --git a/.guardrails/config.yml b/.guardrails/config.yml new file mode 100644 index 0000000..cdb8730 --- /dev/null +++ b/.guardrails/config.yml @@ -0,0 +1,41 @@ +# GuardRails configuration +# Reference: https://docs.guardrails.io/docs/configuration +# +# This config keeps GuardRails scanning enabled but bounds it to source +# code (excluding build output, generated artifacts, tests, and vendored +# fixtures) so its status check converges instead of timing out on the +# repository's full tree. All available analyzers are enabled in their +# default-rule configuration; we do not silence any rule — the goal is +# to make GuardRails finish a scan, not to make findings disappear. + +version: 1 + +ignore-paths: + - bin + - obj + - artifacts + - publish + - packaging + - docs + - fixtures + - TestResults + - "**/bin/**" + - "**/obj/**" + - "**/TestResults/**" + - "tests/**" + - "**/*.Tests/**" + +# Engine pins follow GuardRails' "stable" channel; explicit so a silent +# upstream default change cannot regress the gate without a PR. +engines: + csharp: + enabled: true + secrets: + enabled: true + sast: + enabled: true + general: + secrets: + enabled: true + vulnerable_dependencies: + enabled: true diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..eed305e --- /dev/null +++ b/codecov.yml @@ -0,0 +1,30 @@ +codecov: + require_ci_to_pass: true + +ignore: + - ".guardrails/**" + - ".deepsource.toml" + - ".qlty/**" + - ".github/dependabot.yml" + +flags: + app: + paths: + - src/CodexSessionManager.App/ + core: + paths: + - src/CodexSessionManager.Core/ + storage: + paths: + - src/CodexSessionManager.Storage/ + +coverage: + status: + project: + default: + target: 100.0% + threshold: 0% + patch: + default: + target: 100.0% + threshold: 0% diff --git a/src/CodexSessionManager.App/MainWindow.xaml.cs b/src/CodexSessionManager.App/MainWindow.xaml.cs index 4ea56e6..900ed71 100644 --- a/src/CodexSessionManager.App/MainWindow.xaml.cs +++ b/src/CodexSessionManager.App/MainWindow.xaml.cs @@ -23,7 +23,7 @@ public partial class MainWindow : Window private SessionWorkspaceIndexer? _workspaceIndexer; private MaintenanceExecutor? _maintenanceExecutor; private MaintenancePreview? _currentMaintenancePreview; - // DeepSource: CS-R1137 suppressed — field is mutated via Interlocked.Exchange in partial class SessionOperations + // skipcq: CS-R1137 - field is mutated via Interlocked.Exchange in partial class SessionOperations; readonly would prevent the swap private CancellationTokenSource? _searchCts; internal Func LocalDataRootProvider { get; set; } @@ -205,18 +205,20 @@ private static List BuildKnownStores(bool deepScan) private IndexedLogicalSession[] GetSelectedSessions() => SessionsListBox.SelectedItems.Cast().ToArray(); - // DeepSource: CS-R1005 suppressed — WPF event handler requires async void + // skipcq: CS-R1005 - WPF event handler signature is fixed by the framework and must return void private async void SessionsListBox_OnSelectionChanged(object _, System.Windows.Controls.SelectionChangedEventArgs __) => await LoadSelectedSessionAsync(); - // DeepSource: CS-R1005 suppressed — WPF event handler requires async void + // skipcq: CS-R1005 - WPF event handler signature is fixed by the framework and must return void private async void SearchTextBox_OnTextChanged(object _, System.Windows.Controls.TextChangedEventArgs __) => await SearchSessionsAsync(); [ExcludeFromCodeCoverage] + // skipcq: CS-R1005 - WPF event handler signature is fixed by the framework and must return void private async void RefreshButton_OnClick(object _, RoutedEventArgs __) => await RefreshAsync(deepScan: false); [ExcludeFromCodeCoverage] + // skipcq: CS-R1005 - WPF event handler signature is fixed by the framework and must return void private async void DeepScanButton_OnClick(object _, RoutedEventArgs __) => await RefreshAsync(deepScan: true); private async Task SaveSelectedMetadataAsync() @@ -247,6 +249,7 @@ private async Task SaveSelectedMetadataAsync() } [ExcludeFromCodeCoverage] + // skipcq: CS-R1005 - WPF event handler signature is fixed by the framework and must return void private async void SaveMetadataButton_OnClick(object _, RoutedEventArgs __) => await SaveSelectedMetadataAsync(); @@ -380,6 +383,7 @@ await RunOnUiThreadAsync(() => StatusTextBlock.Text = result.Executed } [ExcludeFromCodeCoverage] + // skipcq: CS-R1005 - WPF event handler signature is fixed by the framework and must return void private async void ExecuteMaintenanceButton_OnClick(object _, RoutedEventArgs __) => await ExecuteMaintenanceAsync(); diff --git a/tests/CodexSessionManager.App.Tests/MainWindowViewModelTests.cs b/tests/CodexSessionManager.App.Tests/MainWindowViewModelTests.cs index e03d42a..fc0205b 100644 --- a/tests/CodexSessionManager.App.Tests/MainWindowViewModelTests.cs +++ b/tests/CodexSessionManager.App.Tests/MainWindowViewModelTests.cs @@ -8,37 +8,19 @@ public sealed class MainWindowViewModelTests [Fact] public async Task RefreshAsync_LoadsSessionsAndSelectsFirstSessionAsync() { - var service = new FakeSessionBrowserService( - sessions: - [ - BuildSession("session-1", "Renderer work", "Readable transcript A"), - BuildSession("session-2", "Maintenance", "Readable transcript B") - ]); + var service = new FakeSessionBrowserService(BuildTwoSessions()); var viewModel = new MainWindowViewModel(service); await viewModel.RefreshAsync(); - Assert.Equal("Ready", viewModel.StatusText); - Assert.Equal(2, viewModel.Sessions.Count); - Assert.Equal("session-1", viewModel.SelectedSession?.SessionId); - Assert.Contains("Readable transcript A", viewModel.TranscriptText, StringComparison.Ordinal); + AssertAllSessionsVisible(viewModel); } [Fact] public async Task ApplySearchAsync_UsesSearchHitsToFilterVisibleSessionsAsync() { - var sessions = new[] - { - BuildSession("session-1", "Renderer work", "Readable transcript A"), - BuildSession("session-2", "Maintenance", "Readable transcript B") - }; - var service = new FakeSessionBrowserService( - sessions, - searchHits: - [ - new SessionSearchHit("session-2", "Maintenance", @"C:\sessions\session-2.jsonl", "Maintenance snippet", 1) - ]); + var service = new FakeSessionBrowserService(BuildTwoSessions(), BuildMaintenanceHit()); var viewModel = new MainWindowViewModel(service); await viewModel.RefreshAsync(); @@ -53,17 +35,7 @@ public async Task ApplySearchAsync_UsesSearchHitsToFilterVisibleSessionsAsync() [Fact] public async Task ApplySearchAsync_WithBlankQuery_RestoresAllSessionsAsync() { - var sessions = new[] - { - BuildSession("session-1", "Renderer work", "Readable transcript A"), - BuildSession("session-2", "Maintenance", "Readable transcript B") - }; - var service = new FakeSessionBrowserService( - sessions, - searchHits: - [ - new SessionSearchHit("session-2", "Maintenance", @"C:\sessions\session-2.jsonl", "Maintenance snippet", 1) - ]); + var service = new FakeSessionBrowserService(BuildTwoSessions(), BuildMaintenanceHit()); var viewModel = new MainWindowViewModel(service); await viewModel.RefreshAsync(); @@ -71,25 +43,13 @@ public async Task ApplySearchAsync_WithBlankQuery_RestoresAllSessionsAsync() await viewModel.ApplySearchAsync(" "); - Assert.Equal(2, viewModel.Sessions.Count); - Assert.Equal("session-1", viewModel.SelectedSession?.SessionId); - Assert.Contains("Readable transcript A", viewModel.TranscriptText, StringComparison.Ordinal); + AssertAllSessionsVisible(viewModel); } [Fact] public async Task ApplySearchAsync_WithNullQuery_RestoresAllSessionsAsync() { - var sessions = new[] - { - BuildSession("session-1", "Renderer work", "Readable transcript A"), - BuildSession("session-2", "Maintenance", "Readable transcript B") - }; - var service = new FakeSessionBrowserService( - sessions, - searchHits: - [ - new SessionSearchHit("session-2", "Maintenance", @"C:\sessions\session-2.jsonl", "Maintenance snippet", 1) - ]); + var service = new FakeSessionBrowserService(BuildTwoSessions(), BuildMaintenanceHit()); var viewModel = new MainWindowViewModel(service); await viewModel.RefreshAsync(); @@ -97,20 +57,13 @@ public async Task ApplySearchAsync_WithNullQuery_RestoresAllSessionsAsync() await viewModel.ApplySearchAsync(null!); - Assert.Equal(2, viewModel.Sessions.Count); - Assert.Equal("session-1", viewModel.SelectedSession?.SessionId); - Assert.Contains("Readable transcript A", viewModel.TranscriptText, StringComparison.Ordinal); + AssertAllSessionsVisible(viewModel); } [Fact] public async Task ApplySearchAsync_WithNoHits_clears_selection_and_transcriptAsync() { - var sessions = new[] - { - BuildSession("session-1", "Renderer work", "Readable transcript A"), - BuildSession("session-2", "Maintenance", "Readable transcript B") - }; - var service = new FakeSessionBrowserService(sessions, searchHits: []); + var service = new FakeSessionBrowserService(BuildTwoSessions(), searchHits: []); var viewModel = new MainWindowViewModel(service); await viewModel.RefreshAsync(); @@ -125,18 +78,29 @@ public async Task ApplySearchAsync_WithNoHits_clears_selection_and_transcriptAsy [Fact] public async Task ApplySearchAsync_single_parameter_overload_normalizes_null_queryAsync() { - var sessions = new[] - { - BuildSession("session-1", "Renderer work", "Readable transcript A"), - BuildSession("session-2", "Maintenance", "Readable transcript B") - }; - var service = new FakeSessionBrowserService(sessions, searchHits: []); + var service = new FakeSessionBrowserService(BuildTwoSessions(), searchHits: []); var viewModel = new MainWindowViewModel(service); await viewModel.RefreshAsync(); await viewModel.ApplySearchAsync(null!); + AssertAllSessionsVisible(viewModel); + } + + private static IndexedLogicalSession[] BuildTwoSessions() => + [ + BuildSession("session-1", "Renderer work", "Readable transcript A"), + BuildSession("session-2", "Maintenance", "Readable transcript B") + ]; + + private static SessionSearchHit[] BuildMaintenanceHit() => + [ + new SessionSearchHit("session-2", "Maintenance", @"C:\sessions\session-2.jsonl", "Maintenance snippet", 1) + ]; + + private static void AssertAllSessionsVisible(MainWindowViewModel viewModel) + { Assert.Equal(2, viewModel.Sessions.Count); Assert.Equal("session-1", viewModel.SelectedSession?.SessionId); Assert.Contains("Readable transcript A", viewModel.TranscriptText, StringComparison.Ordinal); @@ -146,11 +110,8 @@ private static IndexedLogicalSession BuildSession(string sessionId, string threa new( SessionId: sessionId, ThreadName: threadName, - PreferredCopy: new SessionPhysicalCopy(sessionId, $@"C:\Users\Prekzursil\.codex\sessions\{sessionId}.jsonl", SessionStoreKind.Live, new SessionPhysicalCopyState(DateTimeOffset.UtcNow, 1024, false)), - PhysicalCopies: - [ - new SessionPhysicalCopy(sessionId, $@"C:\Users\Prekzursil\.codex\sessions\{sessionId}.jsonl", SessionStoreKind.Live, new SessionPhysicalCopyState(DateTimeOffset.UtcNow, 1024, false)) - ], + PreferredCopy: BuildCopy(sessionId), + PhysicalCopies: [BuildCopy(sessionId)], SearchDocument: new SessionSearchDocument { ReadableTranscript = transcript, @@ -165,6 +126,13 @@ private static IndexedLogicalSession BuildSession(string sessionId, string threa Notes = string.Empty }); + private static SessionPhysicalCopy BuildCopy(string sessionId) => + new( + sessionId, + $@"C:\Users\Prekzursil\.codex\sessions\{sessionId}.jsonl", + SessionStoreKind.Live, + new SessionPhysicalCopyState(DateTimeOffset.UtcNow, 1024, false)); + private sealed class FakeSessionBrowserService : CodexSessionManager.App.ViewModels.ISessionBrowserService { private readonly IReadOnlyList _sessions; @@ -198,4 +166,3 @@ public Task RefreshIndexAsync(CancellationToken cancellationToken) } } } - diff --git a/tests/CodexSessionManager.Storage.Tests/StorageCoverageExpansionTests.cs b/tests/CodexSessionManager.Storage.Tests/StorageCoverageExpansionTests.cs index 4ac99d6..9d80d83 100644 --- a/tests/CodexSessionManager.Storage.Tests/StorageCoverageExpansionTests.cs +++ b/tests/CodexSessionManager.Storage.Tests/StorageCoverageExpansionTests.cs @@ -129,13 +129,33 @@ [new SessionStoreRoot(backupRoot, SessionStoreKind.Backup)], } } - [Fact] - public async Task RebuildAsync_SkipsMissingSessionDirectories_AndBuildsSearchDocumentAsync() + private static (string Root, string LiveRoot, string LiveSessions) CreateLiveStoreLayout() { var root = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); var liveRoot = Path.Combine(root, ".codex"); var liveSessions = Path.Combine(liveRoot, "sessions"); Directory.CreateDirectory(liveSessions); + return (root, liveRoot, liveSessions); + } + + private static KnownSessionStore CreateLiveStore(string liveRoot, string liveSessions) => + new( + liveRoot, + SessionStoreKind.Live, + liveSessions, + Path.Combine(liveRoot, "session_index.jsonl")); + + private static async Task CreateIndexerAsync(string databasePath) + { + var repository = new SessionCatalogRepository(databasePath); + await repository.InitializeAsync(CancellationToken.None); + return new SessionWorkspaceIndexer(repository); + } + + [Fact] + public async Task RebuildAsync_SkipsMissingSessionDirectories_AndBuildsSearchDocumentAsync() + { + var (root, liveRoot, liveSessions) = CreateLiveStoreLayout(); var nestedDir = Path.Combine(liveSessions, "2026", "03", "26"); Directory.CreateDirectory(nestedDir); @@ -150,17 +170,10 @@ await File.WriteAllLinesAsync( try { - var databasePath = Path.Combine(root, "catalog.db"); - var repository = new SessionCatalogRepository(databasePath); - await repository.InitializeAsync(CancellationToken.None); - var indexer = new SessionWorkspaceIndexer(repository); + var indexer = await CreateIndexerAsync(Path.Combine(root, "catalog.db")); var sessions = await indexer.RebuildAsync( [ - new KnownSessionStore( - liveRoot, - SessionStoreKind.Live, - liveSessions, - Path.Combine(liveRoot, "session_index.jsonl")), + CreateLiveStore(liveRoot, liveSessions), new KnownSessionStore( root, SessionStoreKind.Backup, @@ -325,10 +338,7 @@ await File.WriteAllLinesAsync( [Fact] public async Task RebuildAsync_IgnoresMalformedSessionIndexEntriesAsync() { - var root = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); - var liveRoot = Path.Combine(root, ".codex"); - var liveSessions = Path.Combine(liveRoot, "sessions"); - Directory.CreateDirectory(liveSessions); + var (root, liveRoot, liveSessions) = CreateLiveStoreLayout(); var sessionPath = Path.Combine(liveSessions, "session-idx.jsonl"); await File.WriteAllLinesAsync( @@ -348,17 +358,11 @@ await File.WriteAllLinesAsync( try { - var repository = new SessionCatalogRepository(Path.Combine(root, "catalog.db")); - await repository.InitializeAsync(CancellationToken.None); - var indexer = new SessionWorkspaceIndexer(repository); + var indexer = await CreateIndexerAsync(Path.Combine(root, "catalog.db")); var sessions = await indexer.RebuildAsync( [ - new KnownSessionStore( - liveRoot, - SessionStoreKind.Live, - liveSessions, - Path.Combine(liveRoot, "session_index.jsonl")) + CreateLiveStore(liveRoot, liveSessions) ], CancellationToken.None);