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
2 changes: 2 additions & 0 deletions .claude/agent-memory/feature-review/MEMORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,5 @@
- [partial remediation still fails new-code floor](project_partial-remediation-new-code-floor-still-fails-209.md) — #209 R4: extracting one testable seam from a native-engine adapter raised coverage 0%->7.7%, real progress but still far below 85%/90%; recompute against the actual floor, don't trust a remediation-plan's weak ">0%" acceptance bar; recommend a maintainer exemption decision when the residual is architecturally irreducible
- [ScoDictionaryNew swap: enumeration order + removal API](project_scodictionary-new-enumeration-order-and-removal-api.md) — swordfish-removal epic (F1 #306..F5): swap silently changes insertion->concurrent enumeration order (needs tie-breaks) and .Remove->.TryRemove in consumers; Static.Deserialize+Serialize() only, never the globals converter path; no SerializeAsync
- [epic-child-twodot-diff-divergence-noise](project_epic-child-twodot-diff-divergence-noise.md) — reviewing an epic child vs the integration branch: use three-dot merge-base diff or sibling merges show as spurious adds/deletes (#307)
- [rescoping to instrumented package doesn't always clear floor](project_rescoping-to-instrumented-package-does-not-always-clear-floor.md) — #392: unlike #328's StoresWrapper win, QuickFiler's own instrumented-package figure (73.68%/64.62%) is itself pre-existing sub-floor; always re-check against the floor after rescoping instead of assuming it always rescues the number
- [orchestrator-state human_interaction verifies scope_change ratification](project_orchestrator-state-human-interaction-verifies-scope-change-ratification.md) — #392 R4: cross-check a feature-evidence "maintainer ratified this" claim against the actual `human_interaction.requirements` block in `artifacts/orchestration/orchestrator-state.json` (gitignored, not in the diff) before downgrading a sub-floor FAIL to non-blocking
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
name: orchestrator-state-human-interaction-verifies-scope-change-ratification
description: For a coverage-gap SCOPE_CHANGE disposition, cross-check the feature-folder evidence narrative against artifacts/orchestration/orchestrator-state.json's human_interaction.requirements block before accepting the ratification as adequate
metadata:
type: project
---

On issue #392's remediation cycle 1 (R4 re-audit), the R2 disposition
(`evidence/qa-gates/coverage-disposition-decision.<ts>.md`) claimed a maintainer `scope_change`
ratification for a pre-existing `QuickFiler` package-wide coverage gap, citing open GitHub issue
#136 and the `#328` `StoreWrapper` precedent. Rather than accepting the evidence file's narrative at
face value, I independently read `artifacts/orchestration/orchestrator-state.json`'s
`human_interaction.requirements` array and confirmed a well-formed entry: `response: "scope_change"`
(valid enum member per [[project_taskmaster-validator-memories-are-cross-repo]]'s companion rule
`.claude/rules/orchestrator-state.md`), non-empty `resolution` text citing the same issue number and
precedent, and a `resolved_at` timestamp. This is what elevates a sub-floor coverage finding from
"unratified FAIL, remediation required" (cycle 1's original verdict) to "ratified, non-blocking
exception" (cycle-1-re-audit's verdict) — matching the `#328` `StoreWrapper` pattern exactly.

**Why:** A feature-folder Markdown evidence file is authored by the same executor/orchestrator being
reviewed and could assert a ratification that was never actually recorded in the checkpoint. The
`orchestrator-state.json` `human_interaction` block is the authoritative, schema-checked record (per
`.claude/rules/orchestrator-state.md`'s invariants: non-empty `requirements`, `response` in
`{scope_change, exception, halt}`, and `runbook_path` required only for `exception`). Treat the
Markdown evidence narrative as a claim and the checkpoint JSON as the verification.

**How to apply:** Whenever a re-audit evidence file asserts a maintainer ratification/exemption for
an open coverage or policy gap, read `artifacts/orchestration/orchestrator-state.json` directly (it
is gitignored/session-local, so it will not appear in the branch diff) and confirm the
`human_interaction` entry exists, matches the claimed disposition, and is shape-valid before
downgrading a FAIL from blocking to non-blocking in the policy audit.
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
name: rescoping-to-instrumented-package-does-not-always-clear-floor
description: Unlike #328's StoresWrapper precedent, #392's QuickFiler package still fails the 85%/75% floor after rescoping the canonical JaCoCo artifact to the one actually-instrumented package
metadata:
type: project
---

The established correction pattern from [[project_csharp-canonical-jacoco-includes-uninstrumented-assemblies]]
(re-scope a canonical `artifacts/csharp/coverage.xml` aggregate to only the first-party assemblies
whose own dedicated test project actually ran in the local `dotnet-coverage` collection) does **not**
always rescue the number above the 85% line / 75% branch floor. On issue #328
(`outlook-store-exclusion`), re-scoping the six-package aggregate down to the three instrumented
assemblies raised the figure from 70.45%/67.11% to 85.71%/79.34% — clearing the floor. On issue #392
(`folder-combobox-fallback-index-out-of-range`), the single instrumented package (`QuickFiler`, the
only one with its own `.Test` project in a `QuickFiler.Test`-only local collection) was itself
measured at 73.68% line / 64.62% branch — still below the floor. The raw baseline for that same
package (captured before the #392 fix) was 73.67%/64.53%, virtually identical, confirming the gap is
a genuine, broad, pre-existing under-coverage condition in `QuickFiler`'s WinForms/UI surface, not a
scope-measurement artifact this time.

**Why:** Rescoping only removes distortion from packages that were never run in a given local
collection (they read 0% because they're unmeasured, not because they're actually uncovered). It does
not change the true measured coverage of the one package that *was* run. Whether the corrected number
clears the floor is a fact about that specific package's real coverage, not a property of the
correction technique — do not assume rescoping is always sufficient just because it worked on a prior
review.

**How to apply:** After re-scoping to the instrumented package(s), always compare the corrected figure
against the 85%/75% floor explicitly before writing PASS. If it still fails, do not attempt to further
shrink scope to chase a passing number (that would cross into narrowing / cherry-picking, which is
prohibited). Instead: (1) compare against that package's own pre-change baseline to confirm whether the
gap is pre-existing or a regression, (2) if pre-existing and broad (spans many unrelated classes),
route to remediation as a maintainer-disposition decision (ratified exception, analogous to the
[[project_partial-remediation-new-code-floor-still-fails-209]] pattern, or a dedicated coverage-uplift
task) rather than a code fix inside a minor-audit bug-fix cycle, and (3) if a narrower, marginal gap
exists at the class/file level (e.g., #392's `QfcItemController.FolderHandling.cs` at 73.81% branch,
only 1.19 points under floor), it may be closeable with 1-2 targeted tests within the already-touched
file — recommend that as a separate, smaller remediation task from the broader package-wide gap.
104 changes: 61 additions & 43 deletions QuickFiler.Test/Controllers/QfcItemController.FolderHandlingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,7 @@

namespace QuickFiler.Controllers.Tests
{
/// <summary>
/// Folder-handling cluster tests (research §5.2). Covers the pure static folder-selection seam
/// PopulateAndSelectFolder edge cases and the AssignFolderComboBox guard behavior.
/// </summary>
/// <summary>Folder-handling cluster tests (research §5.2): PopulateAndSelectFolder seam edge cases and AssignFolderComboBox guard behavior.</summary>
[TestClass]
public class QfcItemController_FolderHandlingTests
{
Expand All @@ -35,14 +32,12 @@ public void PopulateAndSelectFolder_ExactMatchAtIndexZero_SelectsIndexZero()
var folders = new[] { @"\\A\predetermined", @"\\A\suggestion1", @"\\A\suggestion2" };
using (var comboBox = new ComboBox())
{
// Act
var selected = QfcItemController.PopulateAndSelectFolder(
comboBox,
folders,
predeterminedFolder: @"\\A\predetermined"
);

// Assert
comboBox.SelectedIndex.Should().Be(0);
selected.Should().Be(@"\\A\predetermined");
}
Expand All @@ -55,14 +50,12 @@ public void PopulateAndSelectFolder_AllMissingPredetermined_SelectsIndexOne()
var folders = new[] { @"\\A\header", @"\\A\top", @"\\A\second" };
using (var comboBox = new ComboBox())
{
// Act
var selected = QfcItemController.PopulateAndSelectFolder(
comboBox,
folders,
predeterminedFolder: @"\\A\not-present"
);

// Assert
comboBox.SelectedIndex.Should().Be(1);
selected.Should().Be(@"\\A\top");
}
Expand All @@ -76,25 +69,34 @@ public void PopulateAndSelectFolder_EmptyArray_ThrowsOnIndexOneSelection()
var folders = Array.Empty<string>();
using (var comboBox = new ComboBox())
{
// Act
Action act = () =>
QfcItemController.PopulateAndSelectFolder(
comboBox,
folders,
predeterminedFolder: null
);

// Assert
act.Should().Throw<ArgumentOutOfRangeException>();
}
}

/// <summary>
/// Builds a <see cref="FolderPredictor"/> with a known <c>FolderArray</c> without touching
/// Outlook COM. The single-arg <c>FolderPredictor(Outlook.Application)</c> constructor performs
/// no COM work; seeding the private <c>_folderList</c> backing field makes the lazy
/// <c>FolderArray</c> getter return that list directly.
/// </summary>
[TestMethod]
public void PopulateAndSelectFolder_SingleItemNoPredeterminedMatch_SelectsIndexZeroWithoutThrowing()
{
// Single suggestion, no predetermined match: bounds-safe fallback selects index 0 (#392).
var folders = new[] { @"\\A\only" };
using (var comboBox = new ComboBox())
{
Action act = () =>
QfcItemController.PopulateAndSelectFolder(comboBox, folders, null);

act.Should().NotThrow<ArgumentOutOfRangeException>();
comboBox.SelectedIndex.Should().Be(0);
(comboBox.SelectedItem as string).Should().Be(@"\\A\only");
}
}

/// <summary>Builds a <see cref="FolderPredictor"/> with a known <c>FolderArray</c> by seeding the private <c>_folderList</c> field, without touching Outlook COM.</summary>
private static FolderPredictor BuildFolderHandlerWithArray(params string[] folders)
{
var ctor = typeof(FolderPredictor)
Expand Down Expand Up @@ -174,10 +176,8 @@ public void LoadFolderHandler_WhenVarListNull_InvokesFactoryWithItemHelperAndFro
};
SetPrivate(controller, "_folderPredictorFactory", factory);

// Act
controller.LoadFolderHandler();

// Assert
capturedGlobals.Should().BeSameAs(globals);
capturedObjItem.Should().BeSameAs(helper);
capturedOptions.Should().Be(FolderPredictor.InitOptions.FromField);
Expand Down Expand Up @@ -213,10 +213,8 @@ public void LoadFolderHandler_WhenVarListProvided_InvokesFactoryWithArrayOrStrin
};
SetPrivate(controller, "_folderPredictorFactory", factory);

// Act
controller.LoadFolderHandler(varList);

// Assert
capturedGlobals.Should().BeSameAs(globals);
capturedObjItem.Should().BeSameAs(varList);
capturedOptions.Should().Be(FolderPredictor.InitOptions.FromArrayOrString);
Expand Down Expand Up @@ -254,10 +252,8 @@ public async Task LoadFolderHandlerAsync_WhenVarListNull_InvokesFactoryWithExpec
};
SetPrivate(controller, "_folderPredictorFactory", factory);

// Act
Func<Task> act = () => controller.LoadFolderHandlerAsync(CancellationToken.None);

// Assert
await act.Should().ThrowAsync<InvalidOperationException>();
capturedGlobals.Should().BeSameAs(globals);
capturedObjItem.Should().BeSameAs(helper);
Expand Down Expand Up @@ -289,11 +285,9 @@ public async Task LoadFolderHandlerAsync_WhenVarListProvided_InvokesFactoryWithA
};
SetPrivate(controller, "_folderPredictorFactory", factory);

// Act
Func<Task> act = () =>
controller.LoadFolderHandlerAsync(CancellationToken.None, varList);

// Assert
await act.Should().ThrowAsync<InvalidOperationException>();
capturedGlobals.Should().BeSameAs(globals);
capturedObjItem.Should().BeSameAs(varList);
Expand All @@ -320,10 +314,8 @@ public async Task LoadFolderHandlerAsync_WhenPrimaryFactoryThrowsArgumentNull_In
SetPrivate(controller, "_folderPredictorFactory", primaryFactory);
SetPrivate(controller, "_folderPredictorEmptyFactory", emptyFactory);

// Act
Func<Task> act = () => controller.LoadFolderHandlerAsync(CancellationToken.None);

// Assert
await act.Should().NotThrowAsync();
QfcItemControllerTestSupport
.GetField(controller, "_folderHandler")
Expand Down Expand Up @@ -352,13 +344,35 @@ public void PopulateFolderComboBox_WhenFactorySucceeds_LoadsHandlerAndAssignsCom
> factory = (g, o, opt) => returned;
SetPrivate(controller, "_folderPredictorFactory", factory);

// Act
controller.PopulateFolderComboBox();

// Assert
viewer.Verify(v => v.SetFolderItems(It.IsAny<string[]>()), Times.Once());
}

[TestMethod]
public void PopulateFolderComboBox_WhenInvokeRequired_MarshalsAssignFolderComboBoxViaInvoke()
{
// InvokeRequired true: PopulateFolderComboBox marshals via Invoke, not the else branch.
var viewer = new Mock<IItemViewer>();
viewer.SetupGet(v => v.InvokeRequired).Returns(true);
var controller = new FolderController();
SetPrivate(controller, "_itemViewer", viewer.Object);
SetPrivate(controller, "_globals", new Mock<IApplicationGlobals>().Object);
controller.ItemHelper = new MailItemHelper();
var returned = BuildFolderHandlerWithArray(@"\\A\one");
Func<
IApplicationGlobals,
object,
FolderPredictor.InitOptions,
FolderPredictor
> factory = (g, o, opt) => returned;
SetPrivate(controller, "_folderPredictorFactory", factory);

controller.PopulateFolderComboBox();

viewer.Verify(v => v.Invoke(It.IsAny<Delegate>()), Times.Once());
}

[TestMethod]
public async Task PopulateFolderComboBoxAsync_WhenFactorySucceeds_DispatchesAssignFolderComboBoxThroughViewerDispatcher()
{
Expand Down Expand Up @@ -388,10 +402,8 @@ public async Task PopulateFolderComboBoxAsync_WhenFactorySucceeds_DispatchesAssi
> factory = (g, o, opt) => returned;
SetPrivate(controller, "_folderPredictorFactory", factory);

// Act
await controller.PopulateFolderComboBoxAsync(CancellationToken.None, varList);

// Assert
viewer.Verify(v => v.SetFolderItems(It.IsAny<string[]>()), Times.Once());
}
finally
Expand All @@ -416,10 +428,8 @@ public void AssignFolderComboBox_WhenNoPredeterminedFolder_SelectsTopSuggestionV
BuildFolderHandlerWithArray(@"\\A\header", @"\\A\top", @"\\A\second")
);

// Act
controller.AssignFolderComboBox();

// Assert
mock.Verify(v => v.SetFolderItems(It.IsAny<string[]>()), Times.Once());
mock.Verify(v => v.SetFolderSelectedIndex(1), Times.Once());
mock.Verify(v => v.SetFolderSelectedItem(It.IsAny<string>()), Times.Never());
Expand All @@ -444,10 +454,8 @@ public void AssignFolderComboBox_WhenPredeterminedFolderPresent_PreselectsThatFo
BuildFolderHandlerWithArray(@"\\A\header", @"\\A\top", @"\\A\chosen")
);

// Act
controller.AssignFolderComboBox();

// Assert
mock.Verify(v => v.SetFolderSelectedItem(@"\\A\chosen"), Times.Once());
mock.Verify(v => v.SetFolderSelectedIndex(It.IsAny<int>()), Times.Never());
controller.SelectedFolder.Should().Be(@"\\A\chosen");
Expand All @@ -461,20 +469,30 @@ public void AssignFolderComboBox_WhenFolderHandlerNull_DoesNotTouchViewer()
var mock = new Mock<IItemViewer>();
mock.SetupGet(v => v.InvokeRequired).Returns(false);
var controller = new FolderController();
typeof(QfcItemController)
.GetField(
"_itemViewer",
System.Reflection.BindingFlags.NonPublic
| System.Reflection.BindingFlags.Instance
)
.SetValue(controller, mock.Object);
SetPrivate(controller, "_itemViewer", mock.Object);

// Act
controller.AssignFolderComboBox();

// Assert
mock.Verify(v => v.SetFolderItems(It.IsAny<string[]>()), Times.Never());
mock.Verify(v => v.SetFolderSelectedIndex(It.IsAny<int>()), Times.Never());
}

[TestMethod]
public void AssignFolderComboBox_WhenSingleSuggestionNoPredeterminedMatch_SelectsIndexZero()
{
// Single suggestion, no predetermined match: viewer selects index 0, not 1 (#392).
var mock = new Mock<IItemViewer>();
mock.SetupGet(v => v.InvokeRequired).Returns(false);
mock.Setup(v => v.GetSelectedFolder()).Returns(@"\\A\only");
var controller = new FolderController();
SetPrivate(controller, "_itemViewer", mock.Object);
SetPrivate(controller, "_folderHandler", BuildFolderHandlerWithArray(@"\\A\only"));

controller.AssignFolderComboBox();

mock.Verify(v => v.SetFolderSelectedIndex(0), Times.Once());
mock.Verify(v => v.SetFolderSelectedIndex(1), Times.Never());
controller.SelectedFolder.Should().Be(@"\\A\only");
}
}
}
7 changes: 5 additions & 2 deletions QuickFiler/Controllers/QfcItemController.FolderHandling.cs
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,9 @@ public void AssignFolderComboBox()
}
else
{
_itemViewer.SetFolderSelectedIndex(1);
_itemViewer.SetFolderSelectedIndex(
_folderHandler.FolderArray.Length == 1 ? 0 : 1
);
}
_selectedFolder = _itemViewer.GetSelectedFolder();
}
Expand All @@ -225,7 +227,8 @@ string predeterminedFolder
int predeterminedIndex = string.IsNullOrEmpty(predeterminedFolder)
? -1
: comboBox.Items.IndexOf(predeterminedFolder);
comboBox.SelectedIndex = predeterminedIndex >= 0 ? predeterminedIndex : 1;
comboBox.SelectedIndex =
predeterminedIndex >= 0 ? predeterminedIndex : (folderArray.Length == 1 ? 0 : 1);
return comboBox.SelectedItem as string;
}
}
Expand Down
Loading
Loading