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
73 changes: 73 additions & 0 deletions QuickFiler.Test/Controllers/QfcCollectionControllerTests.Part2.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using System.Reflection;
using FluentAssertions;
using Microsoft.Office.Interop.Outlook;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using QuickFiler.Controllers;
using UtilitiesCS;

namespace QuickFiler.Controllers.Tests
{
/// <summary>
/// Carrier-list carry tests for <c>QfcCollectionController</c>. Relocated here from
/// <c>QfcCollectionControllerTests.cs</c>, which stood at 499 lines with one line of headroom to
/// the 500-line cap, because the issue #678 widening of <see cref="QfcPreScoredItem"/> adds an
/// argument to the construction below and CSharpier then reflows the call across several lines.
/// No test is deleted or weakened by the move; the base part carries the only
/// <c>[TestClass]</c> attribute.
/// </summary>
public partial class QfcCollectionControllerTests
{
/// <summary>
/// [P4-T7] The carrier-list load path carries each survivor's predetermined folder onto the
/// resulting <see cref="QfcItemGroup.PredeterminedFolder"/>. The full carrier
/// <c>LoadControlsAndHandlers_01Async</c> / <c>EncapsulateItemGroup</c> body constructs a real
/// <see cref="QfcItemController"/> and dequeues a WinForms <c>ItemViewer</c>, which require live
/// COM/WinForms; the COM-free carry contract verified here is that the carrier value flows from
/// <see cref="QfcPreScoredItem.PredeterminedFolder"/> onto the item group's
/// <see cref="QfcItemGroup.PredeterminedFolder"/>. The item controller's consumption of that
/// value (preselecting the folder, not index 1) is verified in P5-T3.
/// Issue #678 extends the same COM-free carry contract to the folder search handler: the
/// carrier now publishes it and the item group now carries it alongside the folder.
/// </summary>
[TestMethod]
public void CarrierLoad_SetsPredeterminedFolderOnItemGroup()
{
// Arrange — the carrier the load path produces for a survivor.
var mail = new Mock<MailItem>(MockBehavior.Loose).Object;
var handler = new Mock<IFolderSearchHandler>().Object;
var carrier = new QfcPreScoredItem(mail, @"\\Archive\Projects\Active", handler);

// Act — replicate the group-level carry that EncapsulateItemGroup performs before any
// COM/WinForms call: new QfcItemGroup(mailItem) { PredeterminedFolder = ... }.
var group = new QfcItemGroup(carrier.MailItem)
{
PredeterminedFolder = carrier.PredeterminedFolder,
CarriedFolderHandler = carrier.FolderHandler,
};

// Assert — the predetermined folder is carried onto the group and the mail item matches.
typeof(QfcItemGroup)
.GetProperty(
nameof(QfcItemGroup.PredeterminedFolder),
BindingFlags.NonPublic | BindingFlags.Instance
)
.GetValue(group)
.Should()
.Be(@"\\Archive\Projects\Active");
group.MailItem.Should().BeSameAs(mail);

// Assert — issue #678: the already-initialised handler is carried onto the group too, so
// the item controller can adopt it instead of running a second scoring pass.
typeof(QfcItemGroup)
.GetProperty(
nameof(QfcItemGroup.CarriedFolderHandler),
BindingFlags.NonPublic | BindingFlags.Instance
)
.GetValue(group)
.Should()
.BeSameAs(handler);
carrier.FolderHandler.Should().BeSameAs(handler);
}
}
}
41 changes: 3 additions & 38 deletions QuickFiler.Test/Controllers/QfcCollectionControllerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ namespace QuickFiler.Controllers.Tests
/// constructor; all required private fields are then injected via reflection.
/// </summary>
[TestClass]
public class QfcCollectionControllerTests
public partial class QfcCollectionControllerTests
{
/// <summary>
/// Creates an uninitialized QfcCollectionController with only the fields required
Expand Down Expand Up @@ -287,43 +287,8 @@ out var removed
removed.Should().Equal("noSuggestion");
}

// ---- Carrier-list PredeterminedFolder carry (Issue #171) ----

/// <summary>
/// [P4-T7] The carrier-list load path carries each survivor's predetermined folder onto the
/// resulting <see cref="QfcItemGroup.PredeterminedFolder"/>. The full carrier
/// <c>LoadControlsAndHandlers_01Async</c> / <c>EncapsulateItemGroup</c> body constructs a real
/// <see cref="QfcItemController"/> and dequeues a WinForms <c>ItemViewer</c>, which require live
/// COM/WinForms; the COM-free carry contract verified here is that the carrier value flows from
/// <see cref="QfcPreScoredItem.PredeterminedFolder"/> onto the item group's
/// <see cref="QfcItemGroup.PredeterminedFolder"/>. The item controller's consumption of that
/// value (preselecting the folder, not index 1) is verified in P5-T3.
/// </summary>
[TestMethod]
public void CarrierLoad_SetsPredeterminedFolderOnItemGroup()
{
// Arrange — the carrier the load path produces for a survivor.
var mail = new Mock<MailItem>(MockBehavior.Loose).Object;
var carrier = new QfcPreScoredItem(mail, @"\\Archive\Projects\Active");

// Act — replicate the group-level carry that EncapsulateItemGroup performs before any
// COM/WinForms call: new QfcItemGroup(mailItem) { PredeterminedFolder = ... }.
var group = new QfcItemGroup(carrier.MailItem)
{
PredeterminedFolder = carrier.PredeterminedFolder,
};

// Assert — the predetermined folder is carried onto the group and the mail item matches.
typeof(QfcItemGroup)
.GetProperty(
nameof(QfcItemGroup.PredeterminedFolder),
BindingFlags.NonPublic | BindingFlags.Instance
)
.GetValue(group)
.Should()
.Be(@"\\Archive\Projects\Active");
group.MailItem.Should().BeSameAs(mail);
}
// Carrier-list carry tests (Issue #171, extended for #678) live in the partial part
// QfcCollectionControllerTests.Part2.cs; see that file for the reason.

// ---- Navigation-key register/unregister on page swap (Issue #232) ----

Expand Down
30 changes: 20 additions & 10 deletions QuickFiler.Test/Controllers/QfcDatamodelTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,9 @@ public async Task WaitForQueue_WhenWorkerBusyAndQueueShort_AwaitsInjectedTwoHund
/// the datamodel discards the folder, so a later consumer has to re-score the same item.
/// Scoring is driven through the <c>ScoringServiceFactory</c> seam added by [P1-T5] so no
/// live Outlook COM is touched, as .claude/rules/general-unit-test.md UT4 requires.
/// Issue #678 widened the seam to a third element, the initialised folder search handler;
/// this test additionally asserts that third element is forwarded rather than dropped, which
/// is the same discard defect one element to the right.
/// </summary>
[TestMethod]
public async Task ScoreRemainingQueueMailItemAsync_ReturnsScoreAndTopFolder()
Expand All @@ -333,6 +336,7 @@ public async Task ScoreRemainingQueueMailItemAsync_ReturnsScoreAndTopFolder()

const long ExpectedScore = 875L;
const string ExpectedTopFolder = @"Inbox\Projects\Alpha";
IFolderSearchHandler expectedHandler = new Mock<IFolderSearchHandler>().Object;

var scoringService = new Mock<IFolderScoringService>(MockBehavior.Strict);
scoringService
Expand All @@ -343,16 +347,14 @@ public async Task ScoreRemainingQueueMailItemAsync_ReturnsScoreAndTopFolder()
It.IsAny<CancellationToken>()
)
)
.ReturnsAsync((ExpectedScore, ExpectedTopFolder));
.ReturnsAsync((ExpectedScore, ExpectedTopFolder, expectedHandler));

SetPrivateField(model, "_globals", globals.Object);
model.ScoringServiceFactory = () => scoringService.Object;

// Act
(long Score, string TopFolder) result = await InvokeScoreRemainingQueueMailItemAsync(
model,
mailItem
);
(long Score, string TopFolder, IFolderSearchHandler Handler) result =
await InvokeScoreRemainingQueueMailItemAsync(model, mailItem);

// Assert
result
Expand All @@ -365,12 +367,20 @@ public async Task ScoreRemainingQueueMailItemAsync_ReturnsScoreAndTopFolder()
"the top-ranked folder the scorer already computed must reach the caller "
+ "instead of being discarded and re-derived downstream"
);
result
.Handler.Should()
.BeSameAs(
expectedHandler,
"issue #678: the folder search handler the scoring pass already initialised "
+ "must reach the caller instead of being discarded and re-initialised"
);
}

private static Task<(long Score, string TopFolder)> InvokeScoreRemainingQueueMailItemAsync(
QfcDatamodel model,
MailItem mailItem
)
private static Task<(
long Score,
string TopFolder,
IFolderSearchHandler Handler
)> InvokeScoreRemainingQueueMailItemAsync(QfcDatamodel model, MailItem mailItem)
{
var method = typeof(QfcDatamodel).GetMethod(
"ScoreRemainingQueueMailItemAsync",
Expand All @@ -382,7 +392,7 @@ MailItem mailItem
"ScoreRemainingQueueMailItemAsync should exist on QfcDatamodel as a private "
+ "instance method"
);
return (Task<(long Score, string TopFolder)>)
return (Task<(long Score, string TopFolder, IFolderSearchHandler Handler)>)
method.Invoke(model, new object[] { mailItem, CancellationToken.None });
}

Expand Down
68 changes: 68 additions & 0 deletions QuickFiler.Test/Controllers/QfcFormControllerTests.Part2.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.Office.Interop.Outlook;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using QuickFiler.Controllers;
using QuickFiler.Interfaces;
using UtilitiesCS;

namespace QuickFiler.Controllers.Tests
{
/// <summary>
/// High-confidence carrier-path tests for <c>QfcFormController</c>. Relocated here from
/// <c>QfcFormControllerTests.cs</c>, which stood at 827 lines and is already over the 500-line
/// cap, so it must not grow at all. The issue #678 widening of <see cref="QfcPreScoredItem"/>
/// adds an argument to the construction below and CSharpier then reflows the call, which would
/// have pushed that file further past its baseline count. No test is deleted or weakened by the
/// move; the base part carries the only <c>[TestClass]</c> attribute.
/// </summary>
public partial class QfcFormControllerTests
{
/// <summary>
/// [P4-T6] The carrier-list <see cref="QfcFormController.LoadItemsAsync(IList{QfcPreScoredItem})"/>
/// path never invokes the post-UI removal pass
/// (<see cref="QfcCollectionController.RemoveBelowThresholdAsync"/> via
/// <see cref="QfcFormController.ApplyHighConfidenceFilterAsync"/>). Because the carrier
/// overload constructs a real <see cref="QfcCollectionController"/> internally (no DI seam at
/// that point) which would require live WinForms/COM, this test exercises the overload via the
/// guard short-circuit (`_states` is null because Init() is not called) with an injected
/// collection-controller mock, and verifies no removal interaction occurs on the carrier path.
/// The positive carrier-overload behavior (LoadControlsAndHandlers_01Async and the carried
/// PredeterminedFolder) is verified at the collection-controller level in P4-T7 / P6-T2.
/// </summary>
[TestMethod]
public async Task LoadItemsAsync_PreScored_DoesNotInvokePostUiRemoval()
{
// Arrange — high-confidence mode on so the disabled-path branch is not the reason.
var settings = new Mock<IAppQuickFilerSettings>();
settings.SetupGet(s => s.HighConfidenceModeEnabled).Returns(true);
settings.SetupGet(s => s.HighConfidenceThreshold).Returns(0.9);
_mockGlobals.SetupGet(g => g.QfSettings).Returns(settings.Object);

_controller = CreateQfcFormController();
var mockGroups = new Mock<IQfcCollectionController>(MockBehavior.Strict);
SetPrivateField(_controller, "_groups", mockGroups.Object);

// Issue #678: the carrier now publishes the already-initialised folder search handler
// as its third member, so this construction site populates it.
var preScored = new List<QfcPreScoredItem>
{
new QfcPreScoredItem(
new Mock<MailItem>().Object,
@"\\A\folder",
new Mock<IFolderSearchHandler>().Object
),
};

// Act
Func<Task> act = () => _controller.LoadItemsAsync(preScored);

// Assert — no exception, and the post-UI removal pass is never invoked on the carrier path.
await act.Should().NotThrowAsync();
mockGroups.Verify(g => g.RemoveBelowThresholdAsync(It.IsAny<double>()), Times.Never);
}
}
}
41 changes: 3 additions & 38 deletions QuickFiler.Test/Controllers/QfcFormControllerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
namespace QuickFiler.Controllers.Tests
{
[TestClass]
public class QfcFormControllerTests
public partial class QfcFormControllerTests
{
private Mock<IApplicationGlobals> _mockGlobals;
private Mock<IQfcFormViewer> _mockFormViewer;
Expand Down Expand Up @@ -784,43 +784,8 @@ public async Task ApplyHighConfidenceFilterAsync_WhenModeDisabled_NeverRemoves()

#region High-confidence pre-filter carrier path (Issue #171)

/// <summary>
/// [P4-T6] The carrier-list <see cref="QfcFormController.LoadItemsAsync(IList{QfcPreScoredItem})"/>
/// path never invokes the post-UI removal pass
/// (<see cref="QfcCollectionController.RemoveBelowThresholdAsync"/> via
/// <see cref="QfcFormController.ApplyHighConfidenceFilterAsync"/>). Because the carrier
/// overload constructs a real <see cref="QfcCollectionController"/> internally (no DI seam at
/// that point) which would require live WinForms/COM, this test exercises the overload via the
/// guard short-circuit (`_states` is null because Init() is not called) with an injected
/// collection-controller mock, and verifies no removal interaction occurs on the carrier path.
/// The positive carrier-overload behavior (LoadControlsAndHandlers_01Async and the carried
/// PredeterminedFolder) is verified at the collection-controller level in P4-T7 / P6-T2.
/// </summary>
[TestMethod]
public async Task LoadItemsAsync_PreScored_DoesNotInvokePostUiRemoval()
{
// Arrange — high-confidence mode on so the disabled-path branch is not the reason.
var settings = new Mock<IAppQuickFilerSettings>();
settings.SetupGet(s => s.HighConfidenceModeEnabled).Returns(true);
settings.SetupGet(s => s.HighConfidenceThreshold).Returns(0.9);
_mockGlobals.SetupGet(g => g.QfSettings).Returns(settings.Object);

_controller = CreateQfcFormController();
var mockGroups = new Mock<IQfcCollectionController>(MockBehavior.Strict);
SetPrivateField(_controller, "_groups", mockGroups.Object);

var preScored = new List<QfcPreScoredItem>
{
new QfcPreScoredItem(new Mock<MailItem>().Object, @"\\A\folder"),
};

// Act
Func<Task> act = () => _controller.LoadItemsAsync(preScored);

// Assert — no exception, and the post-UI removal pass is never invoked on the carrier path.
await act.Should().NotThrowAsync();
mockGroups.Verify(g => g.RemoveBelowThresholdAsync(It.IsAny<double>()), Times.Never);
}
// LoadItemsAsync_PreScored_DoesNotInvokePostUiRemoval lives in the partial part
// QfcFormControllerTests.Part2.cs; see that file for the reason.

#endregion High-confidence pre-filter carrier path (Issue #171)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,15 @@ private static Mock<IFolderScoringService> BuildScoringMock(
(MailItem item, IApplicationGlobals g, CancellationToken t) =>
{
t.ThrowIfCancellationRequested();
// Issue #678 widened the seam's third element to the initialised handler.
// This scripted double publishes none, which the carrier tolerates.
if (script.TryGetValue(item, out var entry))
{
return Task.FromResult((entry.score, entry.topFolder));
return Task.FromResult(
(entry.score, entry.topFolder, (IFolderSearchHandler)null)
);
}
return Task.FromResult((0L, string.Empty));
return Task.FromResult((0L, string.Empty, (IFolderSearchHandler)null));
}
);
return mock;
Expand Down
Loading
Loading