fix(797): persist Folder Settings and repair the User Email lookup - #804
Merged
Conversation
Preparation-mode outputs for issue #797, Folder Settings values never persisting and User Email rendering a generic placeholder. No production or test source is changed by this commit. Includes the promoted bug record, the issue document carrying the eight maintainer-settled acceptance criteria verbatim, the research findings, the specification, and the atomic plan (7 phases, 72 tasks). The plan passed the MCP plan validator on four successive revisions and cleared atomic-executor preflight after four review rounds, which resolved 35 findings. Notable ones: the repository-local .NET SDK, the dotnet-coverage global tool and the packages directory are all absent and are now bootstrapped in Phase 0; msbuild and vstest are resolved through vswhere rather than assumed on PATH; the new controller partial needs a nullable pragma or the type-check gate fails on CS8632; and the coverage helper must derive the test-assembly module exclusion in memory, because coverage.config alone would place test code in the denominator. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Two independent root causes made Settings -> Folder Settings unusable on any machine where StoresWrapper.json had never been created. Root cause 1, the bootstrap gap between the loader and the serializer: - AC1 LoadStoresAsync now applies the already-resolved loader configuration to the freshly built stores wrapper, so the wrapper carries the resource-defined path instead of the FilePathHelper default empty string. The fix is confined to the store-loading globals partial; the shared deserialize overload is unchanged because its null return is a load-bearing fail-soft contract for the folder-predictor load path. - AC2 SmartSerializable<T>.Serialize() rejects a null or empty configured path with an error-level log naming the serialized item type and the rejected value, instead of returning silently. The previous guard compared only against the empty string, so a null path passed it and reached the write path. - AC4 a new SerializeNow entry point writes inline through the existing thread-safe write method rather than through the three-second deferred timer, so an explicit Save is not lost when Outlook exits inside that window. The deferred behaviour for every other caller is unchanged and is pinned by its own test. The AC2 guard runs first so the fix does not substitute one silent failure for another. - AC5 the junk-folder call site no longer reflects over a method name. A new narrow IJunkFolderSelectionSink interface in UtilitiesCS is implemented explicitly by the TaskMaster globals partial, so the call is compile-checked and a globals implementation without the seam is reported at error level. Root cause 2, the unretried COM failure in the Exchange SMTP lookup: - AC6 GetSmtpAddressFromStore falls back in a fixed order with per-step COM handling instead of one outer catch: the Exchange primary SMTP address, then the address entry address when it contains an at-sign, then the store display name when it does. The failure reason is captured, the dialog renders a specific unavailability message naming that reason instead of the generic placeholder, and the lookup is retried once per dialog open when the address is null. Adjacent defects in the same rendering method: - AC7 Inbox and Root Folder render without the leading store prefix. - AC8 a null current store selection renders the existing placeholder text instead of throwing, in both PopulateWithCurrent and GetRelativeFsPath. The store wrapper controller stood at 478 lines against the 500-line cap with four criteria landing in it, so its three rendering members were relocated verbatim into a new display partial before any behavioural edit. The relocation was proven behaviour-preserving on its own. Declared test-expectation change: PopulateWithCurrent_NullCurrent_SetsErrorLoadingText previously asserted a NullReferenceException, contradicting its own name. AC8 changes that behaviour, so the assertion is inverted to require that the act does not throw and that the placeholder literals render. The new assertion is stricter than the original. AC3, that a saved value survives an Outlook restart, requires a live VSTO host and is not automatable here. It is left unchecked and handed to the maintainer with a nine-step manual procedure and a fail-before exception dossier. Verification: csharpier format and check, the analyzer rebuild and the warnings-as-errors rebuild all exit 0 with zero warnings and zero errors. The scoped run over the two affected assemblies is green at 5262 passed and 0 failed, against 5237 at baseline; all sixteen previously failing tests pass. Changed-line coverage is 91.09 percent and document-level line coverage moved from 53.23 to 53.26 percent under the same scope. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
5 tasks
drmoisan
added a commit
that referenced
this pull request
Sep 7, 2026
Files the advisory CR-1 finding from PR #804's code review as a tracked bug: the User Email retry is documented as once-per-dialog-open but runs on every store re-selection while the lookup keeps failing. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fix Folder Settings persistence bootstrap gap and the unretried Exchange SMTP lookup
Summary
StoresWrapper.jsonhas never existed — which is every fresh install — the fresh-build path now adopts the resource-defined disk configuration, so the first Save creates the file.SmartSerializable<T>.Serialize()no longer returns silently when the configured disk path is empty or null. It logs at error level, so the defect class that produced this bug can no longer be invisible.IJunkFolderSelectionSink, and the failed-cast path now logs at error level rather than returning silently.Currentstore selection renders the placeholder instead of throwing, and Inbox / Root Folder are displayed without the leading\\store prefix.Why
Two independent root causes were confirmed by code read and by a captured session log.
Root cause 1 — a bootstrap gap between the loader and the serializer.
SmartSerializableBase.Deserialize<T,U>(loader)copies the loader's disk configuration onto the instance only inside itsinstance is not nullbranch. When the settings file does not exist, deserialization returns null, that branch is skipped, and the configuration is discarded. The loader then builds a fresh wrapper whoseConfig.Disk.FilePathis the path-helper default of"".Serialize()guarded onFilePath != ""and returned without writing and without logging. Because the file was never written, every subsequent start took the same null path, so the settings could never bootstrap themselves. In-session persistence appeared to work only because the values lived in the in-memory wrapper.Root cause 2 — an unretried COM failure.
GetSmtpAddressFromStorethrew aCOMExceptionreading the current user from the store session. The exception was caught and converted to null, and the controller rendered null as "Error Loading". The lookup ran once during initialization and was never retried, so a single transient COM failure produced a permanently wrong display for the rest of the session.The silent-return behaviour is what made this expensive: the defect produced no diagnostic signal at all, which is why it is fixed alongside the persistence bug rather than after it.
What Changed
Core persistence and serialization
UtilitiesCS/ReusableTypeClasses/NewSmartSerializable/SmartSerializable.cs— error-level logging when the disk path is empty or null; a synchronous write path so an explicit Save does not depend on the deferred timer.TaskMaster/AppGlobals/AppOlObjects.StoreLoading.cs— the fresh-build path adopts the loader's already-resolved disk configuration.Store wrapper and the Folder Settings dialog
UtilitiesCS/OutlookObjects/Store/StoreWrapper.cs— SMTP fallback chain and a refresh entry point.UtilitiesCS/OutlookObjects/Store/StoreWrapperController.cs— null-Currentguard; retry on populate; specific unavailability text carrying the failure reason.UtilitiesCS/OutlookObjects/Store/StoreWrapperController.Display.cs(new) — display projection helpers, including the store-prefix trim.Typed seam replacing reflection
UtilitiesCS/Interfaces/IGlobals/IJunkFolderSelectionSink.cs(new).TaskMaster/AppGlobals/AppOlObjects.JunkFolders.cs— implements the seam.Tests
UtilitiesCS.Test/ReusableTypeClasses/SmartSerializableSerializeGuardTests.cs,UtilitiesCS.Test/OutlookObjects/Store/StoreWrapperController_Tests.Display.cs.StoreWrapperControllerTests.cs,StoreWrapperController_Tests.ButtonAndPopulate.cs,StoreWrapperTests.cs,TaskMaster.Test/AppGlobals/AppOlObjectsCoverageTests.cs.Project files —
UtilitiesCS.csprojandUtilitiesCS.Test.csprojgain compile entries for the new files. These projects are non-SDK-style with explicit compile lists and no wildcard globbing, so the entries are hand-added. The change is additions only; no existing entry was removed or reordered.Architecture / How It Fits Together
The settings file is owned by
StoresWrapper, which derives fromSmartSerializable<StoresWrapper>, so theConfigthe serializer reads is literally the instance property the loader populates. The fix restores the broken link in that chain: the loader's resolved configuration is copied onto the fresh instance viaCopyFrom, which propagates down to the path helper's backing field.Serialize()then sees a real path and writes.The junk-folder seam inverts a previously reflective dependency.
UtilitiesCSdeclaresIJunkFolderSelectionSink; theTaskMasterglobals object implements it; the controller consumes the interface. The cast failure is now an error-level log rather than a silent return.The display concerns were extracted into a partial class so the controller file did not grow further.
Verification
Completed (all recorded as evidence artifacts under the feature folder):
/t:RebuildwithEnableNETAnalyzersandEnforceCodeStyleInBuild: exit 0,0 Error(s), 0 warnings./t:RebuildwithTreatWarningsAsErrors: exit 0,0 Error(s), 0 warnings.UtilitiesCS.TestandTaskMaster.Testwith/InIsolation: 5262 total, 5262 passed, 0 failed, 0 skipped.lines-validmoved 0.085%, so the two documents are comparable and no-regression holds. Each new member measures at full line and branch coverage exceptGetSmtpAddressFromStoreat 0.8710 line / 0.9444 branch.Coverage authority note: every coverage run here is scoped to the two test assemblies this change touches, which is a narrower denominator than the full-suite denominator the repository-wide floor is written against. The binding gates for this change are therefore the same-scope no-regression comparison and the changed-line figure, both of which pass. The absolute percentage under this narrowed scope is a pre-existing condition that this change neither created nor resolved.
Not verified in this PR — AC3 requires observing a saved value survive a full Outlook restart. That needs a live VSTO host and cannot be performed in an automated environment. The procedure is recorded step by step, marked not performed, and handed to the maintainer at
evidence/other/p6-t1-ac3-manual-verification.2026-09-06T22-00.md. Its acceptance checkbox is deliberately left unchecked.Recommended before merge:
Backward Compatibility / Migration Notes
IJunkFolderSelectionSinkis additive.Risks and Mitigations
Review Guide
SmartSerializable.csandAppOlObjects.StoreLoading.cs— the persistence fix and the highest-risk ordering change.StoreWrapper.csandStoreWrapperController.cs— the fallback chain and the retry gate.IJunkFolderSelectionSink.csandAppOlObjects.JunkFolders.cs— the seam replacing reflection. Note that the explicit interface implementation's unqualified self-named call binds to the internal method rather than recursing, because explicit implementations are excluded from member lookup.StoreWrapperController.Display.cs— mechanical extraction plus the prefix trim..csprojdiff is four added lines.Follow-ups
PopulateWithCurrentis reached from the store re-selection handler, which fires on every selection change. A failed retry leaves the address null, so the gate stays open. The real bound is one blocking COM chain per populate on a still-null store. This does not affect the acceptance criterion as written, which requires only retry-on-open, but the in-code comments and the specification prose overstate the bound and should be corrected.GitHub Auto-close
🤖 Generated with Claude Code