Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
88696b7
wip(950): checkpoint preparation artifacts on quota hold
drmoisan Oct 1, 2026
dd6acfb
wip(950): add incomplete root-cause research on quota hold
drmoisan Oct 1, 2026
a4d89a8
docs(950): close research section 7 and extend to teardown and zero-b…
drmoisan Oct 2, 2026
8bbd48f
docs(950): rewrite spec from completed research
drmoisan Oct 2, 2026
c16cfea
docs(950): author atomic plan in place
drmoisan Oct 2, 2026
e3827fb
docs(950): take the R4 baseline pin inside the gate (preflight round 1)
drmoisan Oct 2, 2026
8e8961f
docs(950): revise plan after preflight round 1
drmoisan Oct 2, 2026
71669a7
docs(950): revise plan after preflight round 2
drmoisan Oct 2, 2026
2449446
docs(950): record preflight clearance after three rounds
drmoisan Oct 2, 2026
b8fcc0f
Merge remote-tracking branch 'origin/main' into bug/quickfiler-tests-…
drmoisan Oct 2, 2026
5387d9b
docs(950): re-anchor plan diff gates to the post-merge main base (rev…
drmoisan Oct 2, 2026
03c0d01
docs(950): record the confirming preflight for plan revision R2
drmoisan Oct 2, 2026
b6be10a
docs(950): record phase 0 baseline evidence
drmoisan Oct 2, 2026
3c466ba
docs(950): record phase 1 fail-before evidence and ambient context ob…
drmoisan Oct 2, 2026
0700ee1
fix(950): add the QfcDatamodel WorkerStarter seam
drmoisan Oct 2, 2026
7091233
test(950): drive the QfcDatamodel worker synchronously and pin the R4…
drmoisan Oct 2, 2026
fe2f80f
fix(950): start the QfcDatamodel worker through a seam and pin the R4…
drmoisan Oct 2, 2026
bd99f92
docs(950): record the implementation commit evidence
drmoisan Oct 2, 2026
2f7602f
docs(950): record negative-control evidence
drmoisan Oct 2, 2026
ec937a9
docs(950): record final QA evidence and acceptance check-offs
drmoisan Oct 2, 2026
11d6d7b
docs(950): record the final commit evidence and plan completion
drmoisan Oct 2, 2026
523c93e
docs(950): record the coordinator ruling on the AC17 evidence source
drmoisan Oct 2, 2026
c1dc0f3
docs(950): add the feature review audit artifacts
drmoisan Oct 2, 2026
d46738a
Merge remote-tracking branch 'origin/main' into bug/quickfiler-tests-…
drmoisan Oct 2, 2026
e591b11
docs(950): check off AC17 from the pull request CI run
drmoisan Oct 2, 2026
dd367e2
docs(950): record the final-head CI run in the AC17 evidence
drmoisan Oct 2, 2026
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
177 changes: 117 additions & 60 deletions QuickFiler.Test/Controllers/QfcDatamodelLivenessTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,45 @@ private static void SetPrivateField(object target, string name, object value)
}

/// <summary>
/// Bounded, event-driven wait for a state transition. This is not a fixed sleep: it returns
/// as soon as the condition holds, and fails the test with a clear message if it never does.
/// Required because <c>BackgroundWorker</c> clears <c>isRunning</c> from an asynchronously
/// posted completion, so the transition is not observable synchronously (the same race the
/// remarks on <see cref="QfcInitEmailQueueZeroBatchTests"/> document).
/// Test-side worker whose <see cref="RaiseDoWork"/> raises <c>DoWork</c> synchronously on
/// the calling thread through the protected <c>OnDoWork</c>, so the privately subscribed
/// <c>Worker_DoWork</c> runs to its first incomplete await before <c>InitEmailQueue</c>
/// returns. Issue #950: this replaces the bounded waits on a thread-pool worker.
/// </summary>
private static void WaitForState(Func<bool> condition, string because)
private sealed class SynchronousBackgroundWorker : BackgroundWorker
{
SpinWait.SpinUntil(condition, TimeSpan.FromSeconds(5)).Should().BeTrue(because);
public void RaiseDoWork() => OnDoWork(new DoWorkEventArgs(null));
}

/// <summary>The synchronous starter assigned to <c>QfcDatamodel.WorkerStarter</c>.</summary>
private static void StartSynchronously(BackgroundWorker worker) =>
((SynchronousBackgroundWorker)worker).RaiseDoWork();

/// <summary>
/// Queues posted continuations and runs them only on an explicit <see cref="Drain"/> call,
/// on the creating thread. Drain runs only work already queued, plus work that work queues,
/// and never blocks. Installed around <c>InitEmailQueue</c> by the tests that observe the
/// loader's continuation, and restored in a <c>finally</c>.
/// </summary>
private sealed class DrainableSynchronizationContext : SynchronizationContext
{
private readonly Queue<Tuple<SendOrPostCallback, object>> _callbacks =
new Queue<Tuple<SendOrPostCallback, object>>();
private readonly int _creatorThreadId = Environment.CurrentManagedThreadId;

public override void Post(SendOrPostCallback d, object state) =>
_callbacks.Enqueue(Tuple.Create(d, state));

/// <summary>Runs every queued callback, including work queued while draining.</summary>
internal void Drain()
{
Environment.CurrentManagedThreadId.Should().Be(_creatorThreadId);
while (_callbacks.Count > 0)
{
Tuple<SendOrPostCallback, object> callback = _callbacks.Dequeue();
callback.Item1(callback.Item2);
}
}
}

/// <summary>Globals wired for the high-confidence dequeue path.</summary>
Expand Down Expand Up @@ -94,20 +124,16 @@ public async Task DequeueNextItemGroupAsync_WhileLoaderStillProducing_KeepsPolli
return await loaderRelease.Task;
};

var worker = new BackgroundWorker();
var worker = new SynchronousBackgroundWorker();
model.WorkerStarter = StartSynchronously;

// Act — the issue #244 zero-batch short-circuit is COM-free and still starts the worker.
// Act — the issue #244 zero-batch short-circuit is COM-free and starts the worker
// through the issue #950 seam, which raises DoWork on this thread.
model.InitEmailQueue(0, worker);

loaderEntered
.Task.Wait(TimeSpan.FromSeconds(5))
.Should()
.BeTrue("the started worker must reach the injected RemainingEmailLoader");
WaitForState(
() => !worker.IsBusy,
"the async void Worker_DoWork returns at its first await, so IsBusy goes false "
+ "while the loader is still producing"
);
.Task.IsCompleted.Should()
.BeTrue("the synchronous starter must reach the injected RemainingEmailLoader");

Task<IList<MailItem>> pending = model.DequeueNextItemGroupAsync(1, 200);
fake.Advance(TimeSpan.FromMilliseconds(200));
Expand Down Expand Up @@ -147,8 +173,12 @@ private static bool ReadLivenessFlag(QfcDatamodel model)

/// <summary>
/// Starts the worker with a <c>RemainingEmailLoader</c> held open by
/// <paramref name="release"/>, and returns once the worker has entered the loader and
/// <c>BackgroundWorker.IsBusy</c> has gone false at the async-void first-await boundary.
/// <paramref name="release"/>. The issue #950 synchronous starter raises <c>DoWork</c> on
/// this thread, so by the time <c>InitEmailQueue</c> returns the async void
/// <c>Worker_DoWork</c> has entered the loader and returned at its first incomplete await.
/// <paramref name="release"/> runs its continuations asynchronously, so a test that has
/// installed <c>DrainableSynchronizationContext</c> observes the resumed loader only
/// through <c>Drain</c>, never inline inside <c>SetResult</c>.
/// </summary>
private static QfcDatamodel StartHeldOpenLoader(
Func<TaskCompletionSource<bool>, Task<bool>> loaderBody,
Expand All @@ -157,7 +187,9 @@ out TaskCompletionSource<bool> release
{
var model = CreateUninitializedDatamodel();
var entered = new TaskCompletionSource<bool>();
var localRelease = new TaskCompletionSource<bool>();
var localRelease = new TaskCompletionSource<bool>(
TaskCreationOptions.RunContinuationsAsynchronously
);
release = localRelease;

model.RemainingEmailLoader = _ =>
Expand All @@ -166,24 +198,21 @@ out TaskCompletionSource<bool> release
return loaderBody(localRelease);
};

var worker = new BackgroundWorker();
var worker = new SynchronousBackgroundWorker();
model.WorkerStarter = StartSynchronously;
model.InitEmailQueue(0, worker);

entered
.Task.Wait(TimeSpan.FromSeconds(5))
.Should()
.BeTrue("the started worker must reach the injected loader");
WaitForState(
() => !worker.IsBusy,
"async void Worker_DoWork returns at its first await"
);
.Task.IsCompleted.Should()
.BeTrue("the synchronous starter must reach the injected loader before returning");
return model;
}

/// <summary>
/// AC 7: the flag stays true across the <c>async void</c> first-await boundary while the
/// loader is still producing — precisely where <c>BackgroundWorker.IsBusy</c> has already
/// gone false.
/// loader is still producing. Issue #950: the synchronous starter has already run
/// <c>Worker_DoWork</c> to that boundary when <c>InitEmailQueue</c> returns, so the flag
/// is read with no wait.
/// </summary>
[TestMethod]
public void RemainingLoadActive_AcrossAsyncVoidFirstAwait_StaysTrueWhileLoaderProduces()
Expand All @@ -205,51 +234,79 @@ out TaskCompletionSource<bool> release
}

/// <summary>
/// AC 7: the flag becomes false only after the loader completes — never before.
/// AC 7: the flag becomes false only after the loader completes — never before. Issue
/// #950: the continuation that clears the flag is drained from a test-owned context.
/// </summary>
[TestMethod]
public void RemainingLoadActive_AfterLoaderCompletes_BecomesFalse()
{
// Arrange / Act
QfcDatamodel model = StartHeldOpenLoader(
signal => signal.Task,
out TaskCompletionSource<bool> release
);
ReadLivenessFlag(model).Should().BeTrue("the loader has not completed yet");
// Arrange
SynchronizationContext previous = SynchronizationContext.Current;
var pump = new DrainableSynchronizationContext();
SynchronizationContext.SetSynchronizationContext(pump);
try
{
QfcDatamodel model = StartHeldOpenLoader(
signal => signal.Task,
out TaskCompletionSource<bool> release
);
ReadLivenessFlag(model).Should().BeTrue("the loader has not completed yet");

release.SetResult(true);
// Act
release.SetResult(true);
pump.Drain();

// Assert
WaitForState(
() => !ReadLivenessFlag(model),
"the finally around the awaited loader must clear the flag once it completes"
);
// Assert
ReadLivenessFlag(model)
.Should()
.BeFalse(
"the finally around the awaited loader must clear the flag once it completes"
);
}
finally
{
SynchronizationContext.SetSynchronizationContext(previous);
}
}

/// <summary>
/// AC 7: the <c>finally</c> clears the flag even when the loader throws.
/// AC 7: the <c>finally</c> clears the flag even when the loader throws. Issue #950: the
/// faulted loader's continuation is drained from a test-owned context.
/// </summary>
[TestMethod]
public void RemainingLoadActive_WhenLoaderThrows_IsStillClearedByFinally()
{
// Arrange / Act
QfcDatamodel model = StartHeldOpenLoader(
async signal =>
{
await signal.Task;
throw new InvalidOperationException("loader failed");
},
out TaskCompletionSource<bool> release
);
ReadLivenessFlag(model).Should().BeTrue("the loader has not failed yet");
// Arrange
SynchronizationContext previous = SynchronizationContext.Current;
var pump = new DrainableSynchronizationContext();
SynchronizationContext.SetSynchronizationContext(pump);
try
{
QfcDatamodel model = StartHeldOpenLoader(
async signal =>
{
await signal.Task;
throw new InvalidOperationException("loader failed");
},
out TaskCompletionSource<bool> release
);
ReadLivenessFlag(model).Should().BeTrue("the loader has not failed yet");

release.SetResult(true);
// Act
release.SetResult(true);
pump.Drain();

// Assert
WaitForState(
() => !ReadLivenessFlag(model),
"the finally must clear the flag on the throwing path too, or the gate would poll forever"
);
// Assert
ReadLivenessFlag(model)
.Should()
.BeFalse(
"the finally must clear the flag on the throwing path too, or the gate would poll forever"
);
}
finally
{
SynchronizationContext.SetSynchronizationContext(previous);
}
}
}
}
47 changes: 28 additions & 19 deletions QuickFiler.Test/Controllers/QfcDatamodelTeardownTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,20 @@ private static object GetPrivateField(object target, string name)
}

/// <summary>
/// Bounded, event-driven wait for a state transition. This is not a fixed sleep: it returns
/// as soon as the condition holds and fails the test with a clear message if it never does.
/// Required because <c>Worker_DoWork</c> is <c>async void</c> and runs on the
/// <see cref="BackgroundWorker"/> thread, so the field assignment it performs is not
/// observable synchronously from the calling thread.
/// Test-side worker whose <see cref="RaiseDoWork"/> raises <c>DoWork</c> synchronously on
/// the calling thread through the protected <c>OnDoWork</c>, so the privately subscribed
/// <c>Worker_DoWork</c> runs to its first incomplete await before <c>InitEmailQueue</c>
/// returns (issue #950). Duplicated per file, following the convention documented on
/// <c>QfcDatamodelLivenessTests</c>.
/// </summary>
private static void WaitForState(Func<bool> condition, string because) =>
SpinWait.SpinUntil(condition, TimeSpan.FromSeconds(5)).Should().BeTrue(because);
private sealed class SynchronousBackgroundWorker : BackgroundWorker
{
public void RaiseDoWork() => OnDoWork(new DoWorkEventArgs(null));
}

/// <summary>The synchronous starter assigned to <c>QfcDatamodel.WorkerStarter</c>.</summary>
private static void StartSynchronously(BackgroundWorker worker) =>
((SynchronousBackgroundWorker)worker).RaiseDoWork();

/// <summary>
/// AC2, the reported crash. Once <c>Cleanup()</c> has nulled <c>_masterQueue</c> and
Expand Down Expand Up @@ -211,22 +217,25 @@ public void Worker_DoWork_CapturesRemainingLoadTask()
return await loaderRelease.Task;
};

using (var worker = new BackgroundWorker())
using (var worker = new SynchronousBackgroundWorker())
{
// Act — the issue #244 zero-batch short-circuit is COM-free and still starts the
// worker, which is the only path that reaches Worker_DoWork without live Outlook.
model.WorkerStarter = StartSynchronously;

// Act — the issue #244 zero-batch short-circuit is COM-free and starts the worker
// through the issue #950 seam, which raises DoWork on this thread, so
// Worker_DoWork has captured the loader task before InitEmailQueue returns.
model.InitEmailQueue(0, worker);
loaderEntered
.Task.Wait(TimeSpan.FromSeconds(5))
.Should()
.BeTrue("the started worker must reach the injected RemainingEmailLoader");

// Assert
WaitForState(
() => GetPrivateField(model, "_remainingLoadTask") != null,
"the loader task must be captured before it is awaited, so the Cancel path has "
+ "a handle to quiesce"
);
loaderEntered
.Task.IsCompleted.Should()
.BeTrue("the synchronous starter must reach the injected RemainingEmailLoader");
GetPrivateField(model, "_remainingLoadTask")
.Should()
.NotBeNull(
"the loader task must be captured before it is awaited, so the Cancel path has "
+ "a handle to quiesce"
);

loaderRelease.TrySetResult(true);
}
Expand Down
Loading
Loading