Compare commits

...

2 Commits

Author SHA1 Message Date
cesnimda 8f9522b1f9 docs(perf): preserve failed settings save qualification 2026-11-11 12:48:33 +01:00
cesnimda 9f63cff3c8 test(rules): verify failed save preserves committed owner settings 2026-11-11 12:46:30 +01:00
6 changed files with 77 additions and 2 deletions
@@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Moq;
using Xunit;
@@ -15,6 +16,43 @@ namespace JobTrackerApi.Tests;
public sealed class RuleSettingsFreshnessTests
{
[Fact]
public async Task Failed_owner_save_preserves_committed_rules_and_a_later_success_refreshes_existing_readers()
{
await using var fixture = await Fixture.CreateAsync();
await using var readA = fixture.Context("owner-a");
await using var readB = fixture.Context("owner-b");
var ownerA = fixture.Controller(readA);
var ownerB = fixture.Controller(readB);
Assert.False(await FollowUp(ownerA, fixture.JobA, "list"));
Assert.False(await FollowUp(ownerB, fixture.JobB, "list"));
var failure = new SaveFailureInterceptor();
await using var write = fixture.Context("owner-a", failure);
await Assert.ThrowsAsync<DbUpdateException>(() => new RulesController(write).Update(Settings(1), default));
Assert.Equal(1, write.UserRuleSettings.Local.Single().AppliedFollowUpDays);
Assert.Equal(30, (await RulesEngine.GetSettings(write, default)).AppliedFollowUpDays);
Assert.False(await FollowUp(ownerA, fixture.JobA, "list"));
Assert.False(await FollowUp(ownerB, fixture.JobB, "list"));
failure.RejectWrites = false;
Assert.IsType<NoContentResult>(await new RulesController(write).Update(Settings(1), default));
Assert.True(await FollowUp(ownerA, fixture.JobA, "list"));
Assert.False(await FollowUp(ownerB, fixture.JobB, "list"));
}
private sealed class SaveFailureInterceptor : SaveChangesInterceptor
{
public bool RejectWrites { get; set; } = true;
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
DbContextEventData eventData, InterceptionResult<int> result, CancellationToken cancellationToken = default)
{
if (RejectWrites) throw new DbUpdateException("Synthetic settings storage failure");
return base.SavingChangesAsync(eventData, result, cancellationToken);
}
}
[Theory]
[InlineData("list")]
[InlineData("board")]
@@ -158,11 +196,13 @@ public sealed class RuleSettingsFreshnessTests
public int JobA { get; private set; }
public int JobB { get; private set; }
public JobTrackerContext Context(string? owner)
public JobTrackerContext Context(string? owner, IInterceptor? interceptor = null)
{
var user = new Mock<ICurrentUserService>();
user.SetupGet(x => x.UserId).Returns(owner);
return new JobTrackerContext(new DbContextOptionsBuilder<JobTrackerContext>().UseSqlite(_connection).Options, user.Object);
var options = new DbContextOptionsBuilder<JobTrackerContext>().UseSqlite(_connection);
if (interceptor is not null) options.AddInterceptors(interceptor);
return new JobTrackerContext(options.Options, user.Object);
}
public JobApplicationsController Controller(JobTrackerContext db) =>
+9
View File
@@ -0,0 +1,9 @@
# Rules settings failed-save verification
11 November 2026. Baseline `d027754d15fadd945e0d9d38e6ca85b50d8dc885`; branch `codex/perf-rules-failed-save`; test source `9f63cff3c89a2eb1f576df16e9a44bf79dc69c84`.
The additional real-SQLite case warms owner A/B application decisions, then injects a SaveChanges failure while changing A's follow-up threshold. It verifies that A's tracked proposed value differs from the committed value, the shared RulesEngine read still returns committed settings even in that failed writer context, and both owners retain their previous decisions. Allowing a later save through the same writer updates A's existing reader while B remains unaffected.
All nine RuleSettingsFreshnessTests passed with zero failures/skips. Eight existing cases overlap the previous 35-case regression and are not added to that total. The native TRX, compressed/raw hashes and command log are retained under `evidence/rules-failed-save/`. Dependency restore used only the existing local NuGet cache. No application or dependency source changed.
The injected exception occurs before database save. This verifies committed-read behaviour after that failure and subsequent successful retry. Mid-transaction connection loss, MariaDB, multi-process load, latency, operational restore and release qualification remain separate.
@@ -0,0 +1,11 @@
{
"testSourceCommit": "9f63cff3c89a2eb1f576df16e9a44bf79dc69c84",
"rawSha256": "7a8d4ade19612005c7f8fe648f973e646044d559b624bc15daa3230084648200",
"compressedSha256": "50ed32b4eb8539ddc34b70e37a376be8482b5c207fa8207ae47b8d4237d51ee3",
"rawBytes": 15156,
"compressedBytes": 2403,
"total": 9,
"passed": 9,
"failed": 0,
"skipped": 0
}
@@ -0,0 +1,4 @@
Determining projects to restore...
Restored C:\Users\Connor\.codex\worktrees\perf-rules-failed-save\Job tracker\JobTrackerApi.Tests\PdfBrowserStub\PdfBrowserStub.csproj (in 65 ms).
Restored C:\Users\Connor\.codex\worktrees\perf-rules-failed-save\Job tracker\JobTrackerApi\JobTrackerApi.csproj (in 278 ms).
Restored C:\Users\Connor\.codex\worktrees\perf-rules-failed-save\Job tracker\JobTrackerApi.Tests\JobTrackerApi.Tests.csproj (in 287 ms).
@@ -0,0 +1,11 @@
JobTrackerApi -> C:\Users\Connor\.codex\worktrees\perf-rules-failed-save\Job tracker\JobTrackerApi\bin\Debug\net9.0\JobTrackerApi.dll
PdfBrowserStub -> C:\Users\Connor\.codex\worktrees\perf-rules-failed-save\Job tracker\JobTrackerApi.Tests\PdfBrowserStub\bin\Debug\net9.0\PdfBrowserStub.dll
JobTrackerApi.Tests -> C:\Users\Connor\.codex\worktrees\perf-rules-failed-save\Job tracker\JobTrackerApi.Tests\bin\Debug\net9.0\JobTrackerApi.Tests.dll
Test run for C:\Users\Connor\.codex\worktrees\perf-rules-failed-save\Job tracker\JobTrackerApi.Tests\bin\Debug\net9.0\JobTrackerApi.Tests.dll (.NETCoreApp,Version=v9.0)
VSTest version 17.14.1 (x64)
Starting test execution, please wait...
A total of 1 test files matched the specified pattern.
Results File: C:\Users\Connor\.codex\worktrees\perf-rules-failed-save\Job tracker\JobTrackerApi.Tests\TestResults\failed-save.trx
Passed! - Failed: 0, Passed: 9, Skipped: 0, Total: 9, Duration: 2 s - JobTrackerApi.Tests.dll (net9.0)