Skip to content
Merged
46 changes: 46 additions & 0 deletions .deepsource.toml
Original file line number Diff line number Diff line change
@@ -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
41 changes: 41 additions & 0 deletions .guardrails/config.yml
Original file line number Diff line number Diff line change
@@ -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
30 changes: 30 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -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%
10 changes: 7 additions & 3 deletions src/CodexSessionManager.App/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> LocalDataRootProvider { get; set; }
Expand Down Expand Up @@ -205,18 +205,20 @@ private static List<KnownSessionStore> BuildKnownStores(bool deepScan)
private IndexedLogicalSession[] GetSelectedSessions() =>
SessionsListBox.SelectedItems.Cast<IndexedLogicalSession>().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()
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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();

Expand Down
101 changes: 34 additions & 67 deletions tests/CodexSessionManager.App.Tests/MainWindowViewModelTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Re-add a StatusText assertion in this test. After the helper extraction, this path no longer verifies the expected ready-state text, so a regression there would go undetected.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tests/CodexSessionManager.App.Tests/MainWindowViewModelTests.cs, line 17:

<comment>Re-add a `StatusText` assertion in this test. After the helper extraction, this path no longer verifies the expected ready-state text, so a regression there would go undetected.</comment>

<file context>
@@ -8,37 +8,19 @@ public sealed class MainWindowViewModelTests
-        Assert.Equal(2, viewModel.Sessions.Count);
-        Assert.Equal("session-1", viewModel.SelectedSession?.SessionId);
-        Assert.Contains("Readable transcript A", viewModel.TranscriptText, StringComparison.Ordinal);
+        AssertAllSessionsVisible(viewModel);
     }
 
</file context>
Suggested change
AssertAllSessionsVisible(viewModel);
Assert.Equal("Ready", viewModel.StatusText);
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();
Expand All @@ -53,64 +35,35 @@ 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();
await viewModel.ApplySearchAsync("maint");

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();
await viewModel.ApplySearchAsync("maint");

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();
Expand All @@ -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);
Comment on lines +102 to 106

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


The refactoring dropped the Assert.Equal("Ready", viewModel.StatusText) assertion that was present in the original RefreshAsync_LoadsSessionsAndSelectsFirstSessionAsync test. AssertAllSessionsVisible does not include this check. This is the only unit test for MainWindowViewModel.StatusText — a regression in the ViewModel's StatusText = "Ready" assignment (line 42 of MainWindowViewModel.cs) would now go undetected.

[Bug]

Solve it in vscode or cursor.

Expand All @@ -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,
Expand All @@ -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<IndexedLogicalSession> _sessions;
Expand Down Expand Up @@ -198,4 +166,3 @@ public Task RefreshIndexAsync(CancellationToken cancellationToken)
}
}
}

Loading
Loading