Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ public async Task AreaWithHomePage_PillNavigatesDirectly_AndHomeIsDroppedFromToc
// The pill links to the area home and is flagged so the click handler lets the
// href navigate instead of just previewing the TOC.
pill.GetAttribute("data-area-direct").ShouldBe("true");
pill.GetAttribute("href").ShouldContain("showcase");
var href = pill.GetAttribute("href");
href.ShouldNotBeNull();
href.ShouldContain("showcase");

// The home entry is the only showcase page, so dropping it leaves an empty TOC —
// no link back to the home the pill already points at.
Expand Down
14 changes: 7 additions & 7 deletions tests/Pennington.Tests/Generation/AuditRunnerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,10 @@ public async Task StartAsync_InHeadlessOneShot_RunsRenderedAuditor_AndCachesItsD
cache.Diagnostics.ShouldBeEmpty();
lifetime.FireStarted();

// RunAsync started on ApplicationStarted; give the cache a moment to be populated.
for (var i = 0; i < 50 && cache.Diagnostics.IsEmpty; i++)
{
await Task.Delay(10, TestContext.Current.CancellationToken);
}
// FireStarted synchronously kicks off the initial pass; await it to completion so the
// assertion reads a settled cache rather than polling with a bounded timeout (which could
// expire under thread-pool starvation on a loaded runner).
await runner.WaitForInitialPassAsync();

cache.Diagnostics.Count.ShouldBe(1);
cache.Diagnostics[0].Message.ShouldBe("rendered audit ran");
Expand Down Expand Up @@ -80,8 +79,9 @@ public async Task StartAsync_InDevMode_SkipsRenderedAuditors()
await runner.StartAsync(TestContext.Current.CancellationToken);
lifetime.FireStarted();

// Wait long enough for any background run to settle.
await Task.Delay(100, TestContext.Current.CancellationToken);
// Await the pass to completion, then assert the rendered auditor was never invoked —
// deterministic where a fixed delay only hoped the (skipped) run had settled.
await runner.WaitForInitialPassAsync();

renderedAuditor.AuditCalls.ShouldBe(0);
cache.Diagnostics.ShouldBeEmpty();
Expand Down
23 changes: 18 additions & 5 deletions tests/Pennington.Tests/Infrastructure/FileWatcherTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,18 @@ public async Task FileWatcher_CoalescesRapidBurst_IntoSingleNotification()
{
// One logical save fans out into a truncate/write/close burst (plus Windows duplicate
// events). The trailing-edge debounce must collapse the whole burst into a single
// notification delivered after the file goes quiet — not one per raw event.
// notification — not one per raw event.
//
// Drive the debounce off a fake clock so the assertion is deterministic. The raw OS events
// still arrive in real time, but the debounce timer fires only when we advance the clock, so
// while it stays frozen every event re-arms the same pending timer no matter how spread out
// their delivery is. That removes the real-clock flake this replaces: a loaded runner that
// dribbled the six events out over >100ms used to let the timer elapse mid-burst, splitting
// it into two notifications.
var ct = TestContext.Current.CancellationToken;
var fs = new RealFileSystem();
using var watcher = new FileWatcher(fs);
var clock = new FakeTimeProvider();
using var watcher = new FileWatcher(fs, clock);
var count = 0;

var filePath = Path.Combine(_tempDir, "burst.txt");
Expand All @@ -133,14 +141,19 @@ public async Task FileWatcher_CoalescesRapidBurst_IntoSingleNotification()
watcher.AddPathWatch(_tempDir, "*.txt", (_, _) => { });
watcher.SubscribeToChanges(() => System.Threading.Interlocked.Increment(ref count));

// Tight synchronous burst — completes far inside the debounce window.
// Tight synchronous burst.
for (var i = 1; i <= 6; i++)
{
File.WriteAllText(filePath, $"v{i}");
}

// Wait comfortably past the debounce window for the single coalesced notification.
await Task.Delay(600, ct);
// Let the OS watcher drain every raw event into the pending buffer. The frozen clock keeps
// any of them from firing, so they collapse onto one re-armed timer; this wait only has to
// outlast event *delivery*, not sit above the debounce window.
await Task.Delay(500, ct);

// Advance once past the 100ms debounce window: the single settled timer fires exactly once.
clock.Advance(TimeSpan.FromMilliseconds(150));

count.ShouldBe(1);
}
Expand Down
Loading