Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0de6cef76d | |||
| 3012965404 |
@@ -0,0 +1,206 @@
|
||||
using JobTrackerApi.Controllers;
|
||||
using JobTrackerApi.Data;
|
||||
using JobTrackerApi.Models;
|
||||
using JobTrackerApi.Services;
|
||||
using JobTrackerApi.Tests.TestSupport;
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace JobTrackerApi.Tests;
|
||||
|
||||
public sealed class RuleSettingsFreshnessTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("list")]
|
||||
[InlineData("board")]
|
||||
[InlineData("detail")]
|
||||
[InlineData("reminders")]
|
||||
[InlineData("readiness")]
|
||||
public async Task Saved_owner_thresholds_are_visible_on_the_next_read(string surface)
|
||||
{
|
||||
await using var fixture = await Fixture.CreateAsync();
|
||||
await using var read = fixture.Context("owner-a");
|
||||
var controller = fixture.Controller(read);
|
||||
Assert.False(await FollowUp(controller, fixture.JobA, surface));
|
||||
await using (var write = fixture.Context("owner-a"))
|
||||
Assert.IsType<NoContentResult>(await new RulesController(write).Update(Settings(1), default));
|
||||
// Reuse the reader after the committed write: tracked entities must not mask persistence.
|
||||
Assert.True(await FollowUp(controller, fixture.JobA, surface));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Saving_owner_a_preserves_owner_b_and_existing_readers_see_subsequent_writes()
|
||||
{
|
||||
await using var fixture = await Fixture.CreateAsync();
|
||||
await using var a = fixture.Context("owner-a");
|
||||
await using var b = fixture.Context("owner-b");
|
||||
var ca = fixture.Controller(a);
|
||||
var cb = fixture.Controller(b);
|
||||
Assert.False(await FollowUp(ca, fixture.JobA, "list"));
|
||||
Assert.False(await FollowUp(cb, fixture.JobB, "list"));
|
||||
await using (var write = fixture.Context("owner-a"))
|
||||
await new RulesController(write).Update(Settings(1), default);
|
||||
Assert.True(await FollowUp(ca, fixture.JobA, "list"));
|
||||
Assert.False(await FollowUp(cb, fixture.JobB, "list"));
|
||||
// A later write in the opposite direction must also be visible through both old/new controllers.
|
||||
await using (var write = fixture.Context("owner-a"))
|
||||
await new RulesController(write).Update(Settings(30), default);
|
||||
await using var fresh = fixture.Context("owner-a");
|
||||
Assert.False(await FollowUp(fixture.Controller(fresh), fixture.JobA, "list"));
|
||||
Assert.False(await FollowUp(ca, fixture.JobA, "list"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Completing_an_older_request_cannot_restore_old_rules_after_a_save()
|
||||
{
|
||||
await using var fixture = await Fixture.CreateAsync();
|
||||
await using var oldRead = fixture.Context("owner-a");
|
||||
var reached = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var release = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var users = TestHostFactory.CreateUserManager();
|
||||
users.Setup(x => x.FindByIdAsync("owner-a")).Returns(async () =>
|
||||
{
|
||||
// GetById has already loaded/evaluated the old rules; no model is called.
|
||||
reached.TrySetResult(true);
|
||||
await release.Task;
|
||||
return (ApplicationUser?)null;
|
||||
});
|
||||
var older = new JobApplicationsController(oldRead, Mock.Of<ISummarizerService>(), users.Object)
|
||||
{
|
||||
ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext
|
||||
{
|
||||
User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, "owner-a") }, "test")),
|
||||
} },
|
||||
};
|
||||
var pending = FollowUp(older, fixture.JobA, "detail");
|
||||
try
|
||||
{
|
||||
await reached.Task.WaitAsync(TimeSpan.FromSeconds(10));
|
||||
await using (var write = fixture.Context("owner-a"))
|
||||
await new RulesController(write).Update(Settings(1), default);
|
||||
await using var freshRead = fixture.Context("owner-a");
|
||||
var fresh = fixture.Controller(freshRead);
|
||||
Assert.True(await FollowUp(fresh, fixture.JobA, "list"));
|
||||
release.TrySetResult(true);
|
||||
Assert.False(await pending); // A request begun before the save may return its earlier snapshot.
|
||||
Assert.True(await FollowUp(fresh, fixture.JobA, "list"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
release.TrySetResult(true);
|
||||
await pending;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Global_save_refreshes_fallback_but_preserves_explicit_owner_override()
|
||||
{
|
||||
await using var fixture = await Fixture.CreateAsync();
|
||||
await using (var setup = fixture.Context("owner-a"))
|
||||
{
|
||||
setup.UserRuleSettings.Remove(await setup.UserRuleSettings.SingleAsync());
|
||||
await setup.SaveChangesAsync();
|
||||
}
|
||||
await using var fallback = fixture.Context("owner-a");
|
||||
await using var overridden = fixture.Context("owner-b");
|
||||
var ca = fixture.Controller(fallback);
|
||||
var cb = fixture.Controller(overridden);
|
||||
Assert.False(await FollowUp(ca, fixture.JobA, "list"));
|
||||
Assert.False(await FollowUp(cb, fixture.JobB, "list"));
|
||||
await using (var global = fixture.Context(null))
|
||||
Assert.IsType<NoContentResult>(await new RulesController(global).Update(Settings(1), default));
|
||||
Assert.True(await FollowUp(ca, fixture.JobA, "list"));
|
||||
Assert.False(await FollowUp(cb, fixture.JobB, "list"));
|
||||
}
|
||||
|
||||
private static RuleSettings Settings(int followUp) => new()
|
||||
{
|
||||
AppliedFollowUpDays = followUp, AppliedGhostDays = 365,
|
||||
OfferFollowUpDays = 30, OfferGhostDays = 365,
|
||||
FeedbackFollowUpDays = 30, FeedbackGhostDays = 365,
|
||||
};
|
||||
|
||||
private static async Task<bool> FollowUp(JobApplicationsController controller, int id, string surface)
|
||||
{
|
||||
if (surface == "readiness")
|
||||
{
|
||||
var result = await controller.GetReadiness(id, default);
|
||||
return Assert.IsType<ReadinessDto>(Assert.IsType<OkObjectResult>(result.Result).Value).WorkflowSignal.NeedsFollowUpAction;
|
||||
}
|
||||
if (surface == "detail")
|
||||
{
|
||||
var result = await controller.GetById(id, default);
|
||||
return Assert.IsType<JobApplicationDto>(Assert.IsType<OkObjectResult>(result.Result).Value).NeedsFollowUp;
|
||||
}
|
||||
if (surface == "list")
|
||||
{
|
||||
var result = await controller.GetAll();
|
||||
return Assert.Single(Assert.IsType<PagedResult<JobApplicationDto>>(Assert.IsType<OkObjectResult>(result.Result).Value).Items).NeedsFollowUp;
|
||||
}
|
||||
if (surface == "board")
|
||||
{
|
||||
var result = await controller.GetBoard(needsFollowUp: true);
|
||||
return Assert.IsType<List<JobApplication>>(Assert.IsType<OkObjectResult>(result.Result).Value).Any(job => job.Id == id);
|
||||
}
|
||||
var response = await controller.GetReminders();
|
||||
var jobs = Assert.IsType<List<JobApplicationDto>>(Assert.IsType<OkObjectResult>(response.Result).Value);
|
||||
return jobs.SingleOrDefault(job => job.Id == id)?.NeedsFollowUp ?? false;
|
||||
}
|
||||
|
||||
private sealed class Fixture : IAsyncDisposable
|
||||
{
|
||||
private readonly SqliteConnection _connection = new("Data Source=:memory:");
|
||||
public int JobA { get; private set; }
|
||||
public int JobB { get; private set; }
|
||||
|
||||
public JobTrackerContext Context(string? owner)
|
||||
{
|
||||
var user = new Mock<ICurrentUserService>();
|
||||
user.SetupGet(x => x.UserId).Returns(owner);
|
||||
return new JobTrackerContext(new DbContextOptionsBuilder<JobTrackerContext>().UseSqlite(_connection).Options, user.Object);
|
||||
}
|
||||
|
||||
public JobApplicationsController Controller(JobTrackerContext db) =>
|
||||
new(db, Mock.Of<ISummarizerService>(), TestHostFactory.CreateUserManager().Object);
|
||||
|
||||
public static async Task<Fixture> CreateAsync()
|
||||
{
|
||||
var fixture = new Fixture();
|
||||
await fixture._connection.OpenAsync();
|
||||
await using var db = fixture.Context("owner-a");
|
||||
await db.Database.EnsureCreatedAsync();
|
||||
db.Users.AddRange(new ApplicationUser { Id = "owner-a", UserName = "owner-a" }, new ApplicationUser { Id = "owner-b", UserName = "owner-b" });
|
||||
var global = await db.RuleSettings.SingleAsync();
|
||||
global.AppliedFollowUpDays = 30;
|
||||
global.AppliedGhostDays = 365;
|
||||
foreach (var owner in new[] { "owner-a", "owner-b" })
|
||||
{
|
||||
await using var owned = fixture.Context(owner);
|
||||
owned.UserRuleSettings.Add(new UserRuleSettings { OwnerUserId = owner, AppliedFollowUpDays = 30, AppliedGhostDays = 365 });
|
||||
// Complete package avoids unrelated package-gap reminders masking the rule decision.
|
||||
var job = new JobApplication
|
||||
{
|
||||
OwnerUserId = owner, Company = new Company { Name = owner, OwnerUserId = owner },
|
||||
JobTitle = "Synthetic engineer", Status = "Applied", DateApplied = DateTime.Now.AddDays(-10), FollowUpAt = DateTime.Now.AddDays(100),
|
||||
TailoredCvText = "Synthetic CV", Notes = "<<<APPLICATION_ANSWER_DRAFT>>>\nSynthetic answer\n<<<END_APPLICATION_ANSWER_DRAFT>>>",
|
||||
};
|
||||
owned.JobApplications.Add(job);
|
||||
// Users must exist before owner-scoped rows are inserted.
|
||||
await db.SaveChangesAsync();
|
||||
await owned.SaveChangesAsync();
|
||||
if (owner == "owner-a") fixture.JobA = job.Id; else fixture.JobB = job.Id;
|
||||
}
|
||||
return fixture;
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await _connection.DisposeAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using JobTrackerApi.Data;
|
||||
using JobTrackerApi.Models;
|
||||
using JobTrackerApi.Services;
|
||||
@@ -31,10 +30,9 @@ namespace JobTrackerApi.Controllers
|
||||
private readonly ICvTemplateRenderer _cvTemplateRenderer;
|
||||
private readonly ICvPdfExporter _cvPdfExporter;
|
||||
private readonly IJobCvMatchService _matchService;
|
||||
private readonly IMemoryCache _cache;
|
||||
private readonly IApplicationChecklistService _checklist;
|
||||
|
||||
public JobApplicationsController(JobTrackerContext db, ISummarizerService summarizer, UserManager<ApplicationUser> users, ICvTemplateRenderer? cvTemplateRenderer = null, ICvPdfExporter? cvPdfExporter = null, IJobCvMatchService? matchService = null, IMemoryCache? cache = null, IApplicationChecklistService? checklist = null)
|
||||
public JobApplicationsController(JobTrackerContext db, ISummarizerService summarizer, UserManager<ApplicationUser> users, ICvTemplateRenderer? cvTemplateRenderer = null, ICvPdfExporter? cvPdfExporter = null, IJobCvMatchService? matchService = null, IApplicationChecklistService? checklist = null)
|
||||
{
|
||||
_checklist = checklist ?? new ApplicationChecklistService(db);
|
||||
_db = db;
|
||||
@@ -43,21 +41,6 @@ namespace JobTrackerApi.Controllers
|
||||
_cvTemplateRenderer = cvTemplateRenderer ?? new CvTemplateRenderer();
|
||||
_cvPdfExporter = cvPdfExporter ?? new ThrowingCvPdfExporter();
|
||||
_matchService = matchService ?? new JobCvMatchService();
|
||||
_cache = cache ?? new MemoryCache(new MemoryCacheOptions());
|
||||
}
|
||||
|
||||
// ponytail: RulesEngine.GetSettings is per-user (falls back to the global RuleSettings
|
||||
// singleton only when the user has no override), so the cache key must include the
|
||||
// current user id -- a single global key would leak one user's follow-up rules to another.
|
||||
private async Task<RuleSettings> GetCachedRuleSettingsAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var cacheKey = $"rulesettings:{_db.CurrentUserId ?? "anon"}";
|
||||
var cached = await _cache.GetOrCreateAsync(cacheKey, async entry =>
|
||||
{
|
||||
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(30);
|
||||
return await RulesEngine.GetSettings(_db, cancellationToken);
|
||||
});
|
||||
return cached!;
|
||||
}
|
||||
|
||||
private sealed class ThrowingCvPdfExporter : ICvPdfExporter
|
||||
@@ -539,7 +522,7 @@ Canonical profile:
|
||||
query = query.Where(j => j.Location != null && EF.Functions.Like(j.Location, like));
|
||||
}
|
||||
|
||||
var settings = await GetCachedRuleSettingsAsync(cancellationToken);
|
||||
var settings = await RulesEngine.GetSettings(_db, cancellationToken);
|
||||
var now = DateTime.Now;
|
||||
|
||||
Task<Dictionary<int, DateTime?>> LoadLatestMessagesAsync(IQueryable<Correspondence> messages) => messages
|
||||
@@ -650,7 +633,7 @@ Canonical profile:
|
||||
|
||||
if (job is null) return NotFound();
|
||||
|
||||
var settings = await GetCachedRuleSettingsAsync(cancellationToken);
|
||||
var settings = await RulesEngine.GetSettings(_db, cancellationToken);
|
||||
var now = DateTime.Now;
|
||||
var lm = await _db.Correspondences
|
||||
.AsNoTracking()
|
||||
@@ -693,7 +676,7 @@ Canonical profile:
|
||||
if (upcomingDays < 1) upcomingDays = 1;
|
||||
if (upcomingDays > 90) upcomingDays = 90;
|
||||
|
||||
var settings = await GetCachedRuleSettingsAsync(cancellationToken);
|
||||
var settings = await RulesEngine.GetSettings(_db, cancellationToken);
|
||||
var now = DateTime.Now;
|
||||
var upcomingTo = now.AddDays(upcomingDays);
|
||||
|
||||
@@ -839,7 +822,7 @@ Canonical profile:
|
||||
.Include(j => j.Company)
|
||||
.FirstAsync(j => j.Id == job.Id, cancellationToken);
|
||||
|
||||
var settings = await GetCachedRuleSettingsAsync(cancellationToken);
|
||||
var settings = await RulesEngine.GetSettings(_db, cancellationToken);
|
||||
var followUp = RulesEngine.Evaluate(settings, created, DateTime.Now, lastMessageAt: null);
|
||||
return CreatedAtAction(nameof(GetById), new { id = created.Id }, BuildJobApplicationDto(created, followUp));
|
||||
}
|
||||
@@ -1029,7 +1012,7 @@ Canonical profile:
|
||||
|
||||
await _db.SaveChangesAsync(cancellationToken);
|
||||
|
||||
var settings = await GetCachedRuleSettingsAsync(cancellationToken);
|
||||
var settings = await RulesEngine.GetSettings(_db, cancellationToken);
|
||||
var lastMsg = await _db.Correspondences
|
||||
.AsNoTracking()
|
||||
.Where(c => c.JobApplicationId == id)
|
||||
@@ -1383,7 +1366,7 @@ Candidate CV/profile:
|
||||
.FirstOrDefaultAsync(j => j.Id == id, cancellationToken);
|
||||
if (job is null) return NotFound();
|
||||
|
||||
var settings = await GetCachedRuleSettingsAsync(cancellationToken);
|
||||
var settings = await RulesEngine.GetSettings(_db, cancellationToken);
|
||||
var now = DateTime.Now;
|
||||
var lastMessageAt = await _db.Correspondences
|
||||
.AsNoTracking()
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# PERF-01 Rules settings freshness
|
||||
|
||||
Source baseline: `9c9fe48d5f321c653049f09ced5b7e27701fe2a8`; isolated branch `codex/perf-rules-freshness`.
|
||||
|
||||
Successful Rules settings saves previously left the API's owner-keyed 30-second cache unchanged. Settings could therefore display the saved values while Applications, Board filtering, details, reminders and readiness continued using old thresholds. Invalidating only the owner's entry would also miss global-fallback dependencies and permit an older asynchronous cache fill to repopulate stale data.
|
||||
|
||||
The job controller now reads through the existing `RulesEngine.GetSettings` owner/fallback implementation on each request. There is no rules cache to repopulate, including across separate API processes. Existing database/query-filter contracts and global `AddMemoryCache` remain unchanged. The unused optional constructor cache argument is removed; the following optional checklist argument retains its name and all existing callers compile. Create and AI-refresh responses share the same corrected settings read; neither endpoint is activated by these tests.
|
||||
|
||||
The cost is one owner-primary-key settings lookup per job request, plus a singleton lookup when no override exists. This correctness fix is not a measured latency or throughput improvement. No distributed invalidation infrastructure was introduced. A read begun before a committed update may return its earlier snapshot; a completed older response cannot cache that snapshot for later requests.
|
||||
|
||||
## Verification
|
||||
|
||||
- **RED:** Seven real SQLite cases with a shared `MemoryCache` failed before the application change, all with expected `True` / actual `False` at the post-save freshness assertion. These cover list, Board follow-up filtering, detail, reminders, readiness, owner isolation and global fallback. Local receipt: `JobTrackerApi.Tests/TestResults/rules-red.trx`.
|
||||
- **PASSED:** The final focused regression run passed **35 distinct tests**, including eight new freshness cases, plus query parity, workflow signals, prospect rules and concurrent initialization. The eighth case pauses an older detail request at a mock identity lookup after rule evaluation, saves new rules, verifies a fresh request, completes the older response and verifies another fresh read. This uses no model or provider. Local receipt: `JobTrackerApi.Tests/TestResults/rules-regression.trx`.
|
||||
- **PASSED:** `dotnet build "Job tracker.sln" --no-restore --verbosity quiet`: zero warnings/errors.
|
||||
- **PASSED:** `git diff --check`.
|
||||
- **PASSED:** `graphify update .` rebuilt the local AST graph without API/model use. It reported missing optional PDF/SQL parsers and files without extracted symbols; those are graph coverage limits, not application test failures. Generated `graphify-out/` remains local and untracked, as at the source worktree.
|
||||
|
||||
Durable machine-readable red/final result names, outcomes, counts and timestamps: [verification.json](evidence/rules-freshness/verification.json). Exact native red, development-green and final-regression TRX files are preserved as `.trx.gz` files alongside it. [native-receipts.json](evidence/rules-freshness/native-receipts.json) records raw/compressed SHA256 hashes and byte lengths; gzip decompression reproduces the original receipts without text transformations. The seven-test development-green run overlaps the final regression run.
|
||||
|
||||
Commands use the installed `C:\Users\Connor\.dotnet\dotnet.exe` SDK; the runtime-only Program Files executable cannot execute SDK commands. Test command: `dotnet test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --no-restore --filter "FullyQualifiedName~RuleSettingsFreshnessTests|FullyQualifiedName~JobQueryParityTests|FullyQualifiedName~JobApplicationsWorkflowSignalsTests|FullyQualifiedName~RulesEngineProspectTests|FullyQualifiedName~ConcurrentInitializationTests"`.
|
||||
|
||||
## Limits and rollback
|
||||
|
||||
The seven-test green development run overlaps the final 35-test run and is not added to its total. The controlled older-request check is not a database isolation stress test. Failed-save injection, hosted MariaDB execution, multi-process load and production latency were not run. This does not qualify Phase 10, integrated security or release readiness. Reverting the package restores the old caching behavior and reintroduces the reproduced stale-settings defect. Publication/integration remains with the parent development lane.
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"format": "Native VSTest TRX compressed with gzip; no text transformations",
|
||||
"receipts": [
|
||||
{
|
||||
"rawFile": "rules-red.trx",
|
||||
"rawBytes": 30627,
|
||||
"rawSha256": "c74e5e9703924e1a1775bddab662d8327f3d66ce502ca61d917cd827317e9d68",
|
||||
"compressedFile": "rules-red.trx.gz",
|
||||
"compressedBytes": 2844,
|
||||
"compressedSha256": "630785b741059e1398e777f0c8f84bdf3f7e748844547d7c468ec913cf0261aa"
|
||||
},
|
||||
{
|
||||
"rawFile": "rules-green.trx",
|
||||
"rawBytes": 12087,
|
||||
"rawSha256": "7960a65a79f9208178d91b41a47469c7839ef4e8c352d077d7fe0fa7745354b3",
|
||||
"compressedFile": "rules-green.trx.gz",
|
||||
"compressedBytes": 2092,
|
||||
"compressedSha256": "131c814b17df0fbbde67dea15ed9e65db2e1f2c801afada903453b504b4a64f6"
|
||||
},
|
||||
{
|
||||
"rawFile": "rules-regression.trx",
|
||||
"rawBytes": 53741,
|
||||
"rawSha256": "e2e7e51902a167cdcb6fa46339476759f63331863f5a2953e3d64cf08b97b106",
|
||||
"compressedFile": "rules-regression.trx.gz",
|
||||
"compressedBytes": 6020,
|
||||
"compressedSha256": "7c229b8beb9d2fa8f3ff453f58d5f3cfeac8987d1f79c0bac65db1a55c0883ca"
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,228 @@
|
||||
{
|
||||
"baseline": "9c9fe48d5f321c653049f09ced5b7e27701fe2a8",
|
||||
"scope": "Offline SQLite and mocked identity; no models/providers",
|
||||
"red": {
|
||||
"times": {
|
||||
"creation": "2026-11-10T23:15:22.2984079+01:00",
|
||||
"queuing": "2026-11-10T23:15:22.2984084+01:00",
|
||||
"start": "2026-11-10T23:15:19.6116487+01:00",
|
||||
"finish": "2026-11-10T23:15:22.3054244+01:00"
|
||||
},
|
||||
"counters": {
|
||||
"total": "7",
|
||||
"executed": "7",
|
||||
"passed": "0",
|
||||
"failed": "7",
|
||||
"error": "0",
|
||||
"timeout": "0",
|
||||
"aborted": "0",
|
||||
"inconclusive": "0",
|
||||
"passedButRunAborted": "0",
|
||||
"notRunnable": "0",
|
||||
"notExecuted": "0",
|
||||
"disconnected": "0",
|
||||
"warning": "0",
|
||||
"completed": "0",
|
||||
"inProgress": "0",
|
||||
"pending": "0"
|
||||
},
|
||||
"results": [
|
||||
{
|
||||
"test": "JobTrackerApi.Tests.RuleSettingsFreshnessTests.Saved_owner_thresholds_are_visible_on_the_next_read(surface: \"detail\")",
|
||||
"outcome": "Failed"
|
||||
},
|
||||
{
|
||||
"test": "JobTrackerApi.Tests.RuleSettingsFreshnessTests.Saved_owner_thresholds_are_visible_on_the_next_read(surface: \"reminders\")",
|
||||
"outcome": "Failed"
|
||||
},
|
||||
{
|
||||
"test": "JobTrackerApi.Tests.RuleSettingsFreshnessTests.Saving_owner_a_does_not_change_owner_b_and_old_readers_do_not_republish_stale_settings",
|
||||
"outcome": "Failed"
|
||||
},
|
||||
{
|
||||
"test": "JobTrackerApi.Tests.RuleSettingsFreshnessTests.Global_save_refreshes_fallback_but_preserves_explicit_owner_override",
|
||||
"outcome": "Failed"
|
||||
},
|
||||
{
|
||||
"test": "JobTrackerApi.Tests.RuleSettingsFreshnessTests.Saved_owner_thresholds_are_visible_on_the_next_read(surface: \"readiness\")",
|
||||
"outcome": "Failed"
|
||||
},
|
||||
{
|
||||
"test": "JobTrackerApi.Tests.RuleSettingsFreshnessTests.Saved_owner_thresholds_are_visible_on_the_next_read(surface: \"board\")",
|
||||
"outcome": "Failed"
|
||||
},
|
||||
{
|
||||
"test": "JobTrackerApi.Tests.RuleSettingsFreshnessTests.Saved_owner_thresholds_are_visible_on_the_next_read(surface: \"list\")",
|
||||
"outcome": "Failed"
|
||||
}
|
||||
]
|
||||
},
|
||||
"regression": {
|
||||
"times": {
|
||||
"creation": "2026-11-10T23:16:27.2319904+01:00",
|
||||
"queuing": "2026-11-10T23:16:27.2319908+01:00",
|
||||
"start": "2026-11-10T23:16:25.2545706+01:00",
|
||||
"finish": "2026-11-10T23:16:31.6005940+01:00"
|
||||
},
|
||||
"counters": {
|
||||
"total": "35",
|
||||
"executed": "35",
|
||||
"passed": "35",
|
||||
"failed": "0",
|
||||
"error": "0",
|
||||
"timeout": "0",
|
||||
"aborted": "0",
|
||||
"inconclusive": "0",
|
||||
"passedButRunAborted": "0",
|
||||
"notRunnable": "0",
|
||||
"notExecuted": "0",
|
||||
"disconnected": "0",
|
||||
"warning": "0",
|
||||
"completed": "0",
|
||||
"inProgress": "0",
|
||||
"pending": "0"
|
||||
},
|
||||
"results": [
|
||||
{
|
||||
"test": "JobTrackerApi.Tests.JobQueryParityTests.Table_pages_and_board_have_identical_owned_membership(scenario: \"needs-work\")",
|
||||
"outcome": "Passed"
|
||||
},
|
||||
{
|
||||
"test": "JobTrackerApi.Tests.RuleSettingsFreshnessTests.Saved_owner_thresholds_are_visible_on_the_next_read(surface: \"readiness\")",
|
||||
"outcome": "Passed"
|
||||
},
|
||||
{
|
||||
"test": "JobTrackerApi.Tests.JobQueryParityTests.Table_pages_and_board_have_identical_owned_membership(scenario: \"correspondence\")",
|
||||
"outcome": "Passed"
|
||||
},
|
||||
{
|
||||
"test": "JobTrackerApi.Tests.JobQueryParityTests.Ordinary_table_materialises_correspondence_groups_only_for_the_returned_page(size: 2500)",
|
||||
"outcome": "Passed"
|
||||
},
|
||||
{
|
||||
"test": "JobTrackerApi.Tests.JobQueryParityTests.Table_pages_and_board_have_identical_owned_membership(scenario: \"custom\")",
|
||||
"outcome": "Passed"
|
||||
},
|
||||
{
|
||||
"test": "JobTrackerApi.Tests.RulesEngineProspectTests.A_prospect_is_never_followed_up_or_ghosted_however_old(status: \"Saved\")",
|
||||
"outcome": "Passed"
|
||||
},
|
||||
{
|
||||
"test": "JobTrackerApi.Tests.RulesEngineProspectTests.An_applied_job_with_no_applied_date_is_not_ghosted",
|
||||
"outcome": "Passed"
|
||||
},
|
||||
{
|
||||
"test": "JobTrackerApi.Tests.JobQueryParityTests.Table_pages_and_board_have_identical_owned_membership(scenario: \"combined\")",
|
||||
"outcome": "Passed"
|
||||
},
|
||||
{
|
||||
"test": "JobTrackerApi.Tests.JobQueryParityTests.Table_pages_and_board_have_identical_owned_membership(scenario: \"follow-up\")",
|
||||
"outcome": "Passed"
|
||||
},
|
||||
{
|
||||
"test": "JobTrackerApi.Tests.JobApplicationsWorkflowSignalsTests.Readiness_keeps_package_gap_when_notes_do_not_contain_saved_answer_block",
|
||||
"outcome": "Passed"
|
||||
},
|
||||
{
|
||||
"test": "JobTrackerApi.Tests.JobQueryParityTests.Table_pages_and_board_have_identical_owned_membership(scenario: \"standard\")",
|
||||
"outcome": "Passed"
|
||||
},
|
||||
{
|
||||
"test": "JobTrackerApi.Tests.JobQueryParityTests.Table_pages_and_board_have_identical_owned_membership(scenario: \"foreign-company\")",
|
||||
"outcome": "Passed"
|
||||
},
|
||||
{
|
||||
"test": "JobTrackerApi.Tests.RuleSettingsFreshnessTests.Saving_owner_a_preserves_owner_b_and_existing_readers_see_subsequent_writes",
|
||||
"outcome": "Passed"
|
||||
},
|
||||
{
|
||||
"test": "JobTrackerApi.Tests.JobQueryParityTests.Table_pages_and_board_have_identical_owned_membership(scenario: \"deleted\")",
|
||||
"outcome": "Passed"
|
||||
},
|
||||
{
|
||||
"test": "JobTrackerApi.Tests.JobQueryParityTests.Table_pages_and_board_have_identical_owned_membership(scenario: \"interview\")",
|
||||
"outcome": "Passed"
|
||||
},
|
||||
{
|
||||
"test": "JobTrackerApi.Tests.ConcurrentInitializationTests.Concurrent_checklist_reads_share_the_single_default_item_set",
|
||||
"outcome": "Passed"
|
||||
},
|
||||
{
|
||||
"test": "JobTrackerApi.Tests.RulesEngineProspectTests.A_prospect_is_never_followed_up_or_ghosted_however_old(status: \"Interested\")",
|
||||
"outcome": "Passed"
|
||||
},
|
||||
{
|
||||
"test": "JobTrackerApi.Tests.JobQueryParityTests.Table_pages_and_board_have_identical_owned_membership(scenario: \"notes\")",
|
||||
"outcome": "Passed"
|
||||
},
|
||||
{
|
||||
"test": "JobTrackerApi.Tests.RuleSettingsFreshnessTests.Saved_owner_thresholds_are_visible_on_the_next_read(surface: \"detail\")",
|
||||
"outcome": "Passed"
|
||||
},
|
||||
{
|
||||
"test": "JobTrackerApi.Tests.ConcurrentInitializationTests.Concurrent_rule_reads_share_the_single_created_settings_row",
|
||||
"outcome": "Passed"
|
||||
},
|
||||
{
|
||||
"test": "JobTrackerApi.Tests.JobQueryParityTests.Ordinary_table_materialises_correspondence_groups_only_for_the_returned_page(size: 250)",
|
||||
"outcome": "Passed"
|
||||
},
|
||||
{
|
||||
"test": "JobTrackerApi.Tests.RuleSettingsFreshnessTests.Saved_owner_thresholds_are_visible_on_the_next_read(surface: \"list\")",
|
||||
"outcome": "Passed"
|
||||
},
|
||||
{
|
||||
"test": "JobTrackerApi.Tests.JobQueryParityTests.Table_pages_and_board_have_identical_owned_membership(scenario: \"none\")",
|
||||
"outcome": "Passed"
|
||||
},
|
||||
{
|
||||
"test": "JobTrackerApi.Tests.JobQueryParityTests.Table_pages_and_board_have_identical_owned_membership(scenario: \"location\")",
|
||||
"outcome": "Passed"
|
||||
},
|
||||
{
|
||||
"test": "JobTrackerApi.Tests.RulesEngineProspectTests.A_prospect_is_never_followed_up_or_ghosted_however_old(status: \"Preparing\")",
|
||||
"outcome": "Passed"
|
||||
},
|
||||
{
|
||||
"test": "JobTrackerApi.Tests.RulesEngineProspectTests.An_applied_job_past_the_threshold_still_ghosts",
|
||||
"outcome": "Passed"
|
||||
},
|
||||
{
|
||||
"test": "JobTrackerApi.Tests.RuleSettingsFreshnessTests.Saved_owner_thresholds_are_visible_on_the_next_read(surface: \"reminders\")",
|
||||
"outcome": "Passed"
|
||||
},
|
||||
{
|
||||
"test": "JobTrackerApi.Tests.JobQueryParityTests.Table_pages_and_board_have_identical_owned_membership(scenario: \"include-deleted\")",
|
||||
"outcome": "Passed"
|
||||
},
|
||||
{
|
||||
"test": "JobTrackerApi.Tests.JobQueryParityTests.Table_pages_and_board_have_identical_owned_membership(scenario: \"company\")",
|
||||
"outcome": "Passed"
|
||||
},
|
||||
{
|
||||
"test": "JobTrackerApi.Tests.RuleSettingsFreshnessTests.Saved_owner_thresholds_are_visible_on_the_next_read(surface: \"board\")",
|
||||
"outcome": "Passed"
|
||||
},
|
||||
{
|
||||
"test": "JobTrackerApi.Tests.JobQueryParityTests.Table_pages_and_board_have_identical_owned_membership(scenario: \"all\")",
|
||||
"outcome": "Passed"
|
||||
},
|
||||
{
|
||||
"test": "JobTrackerApi.Tests.RuleSettingsFreshnessTests.Completing_an_older_request_cannot_restore_old_rules_after_a_save",
|
||||
"outcome": "Passed"
|
||||
},
|
||||
{
|
||||
"test": "JobTrackerApi.Tests.RuleSettingsFreshnessTests.Global_save_refreshes_fallback_but_preserves_explicit_owner_override",
|
||||
"outcome": "Passed"
|
||||
},
|
||||
{
|
||||
"test": "JobTrackerApi.Tests.JobQueryParityTests.Table_pages_and_board_have_identical_owned_membership(scenario: \"search\")",
|
||||
"outcome": "Passed"
|
||||
},
|
||||
{
|
||||
"test": "JobTrackerApi.Tests.JobApplicationsWorkflowSignalsTests.Reminders_return_normalized_workflow_routes_for_package_and_follow_up_actions",
|
||||
"outcome": "Passed"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user