diff --git a/.claude/agent-memory/atomic-executor/MEMORY.md b/.claude/agent-memory/atomic-executor/MEMORY.md index dac422927..01f307886 100644 --- a/.claude/agent-memory/atomic-executor/MEMORY.md +++ b/.claude/agent-memory/atomic-executor/MEMORY.md @@ -34,6 +34,7 @@ - [gh/CI-log traps](project_gh_ci_log_and_download_gotchas.md) (createdAt trips pr-author hook; ANSI in Pester log; gh download never overwrites) · [Abbreviated caller facts](project_caller_supplied_fact_list_can_be_abbreviated_and_look_like_a_plan_defect.md) - [MSTest summary absent on failure](project_mstest_runner_summary_absent_on_failure_voids_flaky_carveout.md) (flaky carve-out keyed on summary never fires; read the trx) +- [Per-file token-count gate forbids helpers; scan artifact self-match](project_per_file_token_count_gate_forbids_helper_and_scan_artifact_self_match.md) (keep statements inlined; never quote the forbidden pattern) - [Generic seam over embedded interop = CS1769](project_generic_seam_over_embedded_interop_type_cs1769.md) (Func seams compile in UtilitiesCS, fail at every test call site) ## Sub-indexes (open when the topic is in scope) - [Test isolation + C# coverage](index_test_isolation_and_coverage.md) — vstest/MSTest hangs, flakiness, Cobertura/dotnet-coverage/Koverage diff --git a/.claude/agent-memory/atomic-executor/project_per_file_token_count_gate_forbids_helper_and_scan_artifact_self_match.md b/.claude/agent-memory/atomic-executor/project_per_file_token_count_gate_forbids_helper_and_scan_artifact_self_match.md new file mode 100644 index 000000000..ca8bc2241 --- /dev/null +++ b/.claude/agent-memory/atomic-executor/project_per_file_token_count_gate_forbids_helper_and_scan_artifact_self_match.md @@ -0,0 +1,16 @@ +--- +name: per-file-token-count-gate-forbids-helper-and-scan-artifact-self-match +description: Exact per-file token-count gates (e.g. "_settingsStore.Save() = 9 in file X") forbid extracting the repeated statement into a helper; an evidence artifact that quotes its own forbidden-token search pattern makes the scan self-match +metadata: + type: project +--- + +Two gate-shape traps seen while executing issue #1005 (settings-store seam). + +1. A plan's after-search asserted an exact per-file count of a statement token (Q2: `_settingsStore.Save()` 9 in AppStagingFilenames.cs, matching "each saving setter followed by `_settingsStore.Save()`"). Extracting the write+save into a private `Persist` helper is cleaner, but it collapses the count to 1 and fails the gate. Before refactoring repeated statements, grep the plan for count assertions on that token and keep the inlined form. The same applies to literal argument forms: a gate that asserts `timeProvider: new FakeTimeProvider()` fails if the value is first assigned to a local. + +2. A "no host path in evidence" scan (drive letter, Users segment, account name, AppData) matched its own artifact, because the artifact's Command field quoted the regex, which contains the account name. Describe the pattern in words in the artifact and re-run the scan after writing it. + +**Why:** both defects pass every build and test and surface only when the textual gate runs, so they cost a rewrite late in the plan. + +**How to apply:** before writing implementation, read the plan's search-definition section for per-file counts and literal forms. Write scan artifacts without quoting the forbidden tokens, then re-run the scan over the evidence tree once the artifact exists. Related: [[project_banned_api_zero_hit_gate_hits_doc_comments]]. diff --git a/.claude/agent-memory/atomic-planner/MEMORY.md b/.claude/agent-memory/atomic-planner/MEMORY.md index 33fae36b6..dac0d7071 100644 --- a/.claude/agent-memory/atomic-planner/MEMORY.md +++ b/.claude/agent-memory/atomic-planner/MEMORY.md @@ -2,6 +2,8 @@ ## Preflight revision seams (per issue; newest first) +- [#1005 R1](project_1005_r1_jacoco_projection_is_package_level_only.md) — JaCoCo projection is package-level only; per-file coverage ACs read Cobertura +- [#1005 R0](project_1005_r0_settings_store_seam_and_probe_seams.md) — SettingsSaving probe locks add/remove; "<=500 and smaller" AC contradiction amended; IDList null-ctor ambiguity - [#985 R1](project_985_r1_scratchpad_identity_and_double_write_seams.md) — never spell the scratchpad (encoded name holds the account); sweep diff + untracked; reflowed tag forces a double write - [#985 R0](project_985_r0_rehearsal_fidelity_and_hygiene_path_seams.md) — merge-into-Dependabot rehearsal needs a simulated nuget update; hygiene guard flags Users paths in tracked plans; Invoke-VSBuild rewrites csproj - [#973 R1](project_973_r1_last_task_artifact_only_commit_and_measured_line_length_seams.md) — last task commits its artifact alone; dossier cites no later result; measure line length with `^.{N,}`; Glob zero-gate needs a positive control diff --git a/.claude/agent-memory/atomic-planner/project_1005_r0_settings_store_seam_and_probe_seams.md b/.claude/agent-memory/atomic-planner/project_1005_r0_settings_store_seam_and_probe_seams.md new file mode 100644 index 000000000..3090409d4 --- /dev/null +++ b/.claude/agent-memory/atomic-planner/project_1005_r0_settings_store_seam_and_probe_seams.md @@ -0,0 +1,18 @@ +--- +name: project-1005-r0-settings-store-seam-and-probe-seams +description: #1005 R0 planning seams - SettingsSaving probe needs lock around add/remove, "<=500 and smaller" AC contradiction for already-oversized files, IDList null-ctor overload ambiguity, coverage script already applies parallel runsettings +metadata: + type: project +--- + +Planned #1005 (parallel test failures: DfDeedle wall clock + shared user.config) on 2026-10-22. + +- `ApplicationSettingsBase.SettingsSaving` has explicit add/remove over a plain delegate field (not atomic); concurrent probe tests on one `Settings.Default` must `lock (settings)` around `+=`/`-=` only, which does not serialize tests. +- An AC saying "every Write Set file <= 500 lines AND file X (already 848) is smaller" is unsatisfiable for X; the planner amended spec AC7 to an explicit exception rather than leaving the executor to interpret it. +- `new IDList(null)` is ambiguous once an `IDList(ISettingsStore)` overload exists; null-guard tests must cast `(ISettingsStore)null` or use the two-arg overload. +- `scripts/vscode/Invoke-MSTestWithCoverage.ps1` already applies `TaskMaster.cli.runsettings` (Workers 0, ClassLevel), writes `coverage.cobertura.jacoco.xml` and `mstest-coverage-run.summary.txt`, and retains the post-processed Cobertura only in `coverage/`; copy it to SCRATCH for a later baseline comparison. +- `docs/features/templates/policy_audit/` does not exist in TaskMaster; self-review tasks record the template as a deviation (same as #1002). +- A test helper not in the spec Write Set was added by amending the spec Write Set in the same planning pass, so the scope fence has one source. + +**Why:** these were the non-obvious points found while re-deriving citations; each would otherwise surface as a preflight or review defect. +**How to apply:** reuse for any later settings-persistence or parallel-isolation plan in TaskMaster. Related: [[reference-invoke-mstest-with-coverage-script]], [[acceptance-criteria-are-amended-by-planners-not-executors]]. diff --git a/.claude/agent-memory/atomic-planner/project_1005_r1_jacoco_projection_is_package_level_only.md b/.claude/agent-memory/atomic-planner/project_1005_r1_jacoco_projection_is_package_level_only.md new file mode 100644 index 000000000..e93b67fb0 --- /dev/null +++ b/.claude/agent-memory/atomic-planner/project_1005_r1_jacoco_projection_is_package_level_only.md @@ -0,0 +1,12 @@ +--- +name: project-1005-r1-jacoco-projection-is-package-level-only +description: TaskMaster's committed JaCoCo projection holds package LINE/BRANCH counters only; per-file or per-member coverage ACs must read the Cobertura document +metadata: + type: project +--- + +`ConvertTo-JacocoPackageProjection` (scripts/vscode/Invoke-MSTestWithCoverage.Projection.ps1) emits package-level LINE and BRANCH counters only, no class/method/sourcefile/line data. An AC that says a file or member reaches N% "read from the committed coverage projection" is unsatisfiable (#1005 preflight R1 substantive defect). + +**Why:** CLAUDE.md permits committing only the projection, so authors reach for it as the source; it cannot carry per-file figures. + +**How to apply:** word per-file/per-member coverage ACs as "read from the post-processed Cobertura document and recorded in the committed coverage-comparison evidence"; amend the spec before P0 copies it. Also: never leave a `PREFLIGHT:` line in the plan body. See [[project-1005-r0-settings-store-seam-and-probe-seams]]. diff --git a/.claude/agent-memory/feature-review/MEMORY.md b/.claude/agent-memory/feature-review/MEMORY.md index 553b6a58d..4fb32b1eb 100644 --- a/.claude/agent-memory/feature-review/MEMORY.md +++ b/.claude/agent-memory/feature-review/MEMORY.md @@ -111,6 +111,7 @@ ## Closed-issue residuals (lookup only; details in the residuals index) - [review-residuals index](project_review-residuals-index.md) — per-issue summaries #442-#968 (#930 host-path PARTIAL; #942 `.csproj` disarms C# hook check) +- [1005](project_1005-review-residuals.md) — helper deletion stripped an unchanged member's only caller; covered-count arithmetic exposes it; cycle 2 PASS; `git log %cr` gives "now" - [1002](project_1002-review-residuals.md) — aria-selected with no CSS = blocking; ratified exemption left AC23 false (cycle 1); cycle 2 PASS; span wrap needs closest() click routing - [985](project_985-review-residuals.md) — cycle 1 blocker: account name in encoded scratchpad dir; cycle 2 PASS; hook sim param is `-RawPayload` - [973](project_973-review-residuals.md) — AC premise defect = autonomous spec amendment; bundled PoshQC FAIL row + vacuous PASS diff --git a/.claude/agent-memory/feature-review/project_1005-review-residuals.md b/.claude/agent-memory/feature-review/project_1005-review-residuals.md new file mode 100644 index 000000000..b6f435283 --- /dev/null +++ b/.claude/agent-memory/feature-review/project_1005-review-residuals.md @@ -0,0 +1,16 @@ +--- +name: 1005-review-residuals +description: "#1005 cycle 1: 8/8 AC PASS but 1 blocking - deleting an AC-mandated test helper stripped the only caller of an unchanged member (IDList.MaxLengthOfID 7/7->0/7); found by covered-count arithmetic, not by the executor's changed-line comparison" +metadata: + type: project +--- + +#1005 (settings-store seam + FakeTimeProvider for parallel tests), review 2026-10-22T17-10: REMEDIATION_REQUIRED, 1 blocking (B-1, autonomous), all 8 AC PASS and left checked. + +B-1: AC2 required deleting `CreateListWithCachedMaxLength` in `IDListGetNextToDoIDTests`; its `_ = list.MaxLengthOfID;` was the only test execution of the public getter. Baseline Cobertura 7/7 lines 4/4 branches -> head 0/7 lines 0/4 branches; `IDList.cs` branch 20.69 -> 19.35 while line rose. The executor's coverage comparison checked changed lines and replaced lines only, so it reported PASS. + +**Why:** a changed-line-only comparison is blind to coverage lost on UNCHANGED lines when a test refactor removes their only caller. The tell was arithmetic: added covered lines (13) vs covered-count delta (+6) left 7 unexplained. + +Cycle 2 (R4, 2026-10-22T17-23, head 3df0782ab): PASS, 0 blocking. Three store-injected MaxLengthOfID tests restored the getter to 7/7, 4/4; IDList.cs 51/169 = 30.18% / 25.81% (arithmetic matched exactly: 44 + 7). Under a git-diff/git-log-only Bash rule, read "now" from `git log -1 --format="%cI | %cr"` on the head commit (commit time + relative age). + +**How to apply:** for every modified production file, compute expected covered = baseline covered + added covered lines; if head is lower, diff `` nodes between the baseline and head Cobertura (the executor's baseline Cobertura usually survives in the session scratchpad under `issue-/`; Glob `AppData/Local/Temp/claude/**/issue--baseline-cobertura.xml`). Also check branch-rate, not just the line figure the executor quoted. Classify as blocking-autonomous when the modified file is sub-floor and regressed (see [[modified-file-subfloor-nonblocking-disposition-230]]); keep the ACs checked when the loss sits outside their literal text. Line-110-style "branch" on a cached static lambda needs >=2 executions in one run to cover both arms. diff --git a/.claude/agent-memory/orchestrator/MEMORY.md b/.claude/agent-memory/orchestrator/MEMORY.md index c521b8fce..714dbe977 100644 --- a/.claude/agent-memory/orchestrator/MEMORY.md +++ b/.claude/agent-memory/orchestrator/MEMORY.md @@ -3,6 +3,7 @@ ## Lifecycle, promotion, checkpoint - [Isolated child: wait via git pickaxe; name the item in Agent prompts](isolated-child-liveness-wait-and-delegation-target-lines.md) — no sleep route; `Canonical issue number` + `branch:` lines - [Evidence + lifecycle for every change](evidence-and-lifecycle-for-every-change.md) · [Small-path = minor-audit](small-path-minor-audit-selection.md) — 1-3 prod files = small path +- [Small->large reroute flips work mode to full-bug](small-to-large-reroute-flips-work-mode.md) — edit the issue.md marker before prd-feature; restamp future-dated evidence - [potential_to_issue creates the issue](potential-to-issue-creates-github-issue.md) · [needs an absolute path](potential-to-issue-needs-absolute-path.md) · [maps sections by heading](potential-to-issue-keeps-only-summary-section.md) - [Promotion potential .md may not persist](promotion-potential-md-may-not-persist.md) · [new_active_feature_folder date prefix](new-active-feature-folder-date-prefix.md) - [Verify an issue is open in SUBSTANCE](verify-issue-still-open-in-substance.md) — the residual may already be its own issue diff --git a/.claude/agent-memory/orchestrator/small-to-large-reroute-flips-work-mode.md b/.claude/agent-memory/orchestrator/small-to-large-reroute-flips-work-mode.md new file mode 100644 index 000000000..df0a20288 --- /dev/null +++ b/.claude/agent-memory/orchestrator/small-to-large-reroute-flips-work-mode.md @@ -0,0 +1,24 @@ +--- +name: small-to-large-reroute-flips-work-mode +description: When a seeded small/minor-audit item turns out to need 4+ production files, flip issue.md to full-bug before prd-feature; also restamp executor evidence whose filename time is in the future +metadata: + type: project +--- + +On #1005 (2026-10-22) a seeded `small` + `minor-audit` checkpoint had to become `large` once AC4 +("fix every test that reaches Settings.Default.Save()") pulled in 7 production files. The large route +runs prd-feature, and a spec.md in a minor-audit folder is an integrity failure, so I edited the +issue.md marker to `- Work Mode: full-bug`, set checkpoint `work-mode`/`path_selected`/`route_id`, and +recorded a `route_change` block. spec.md then became the AC source (issue AC1-AC7 carried forward plus +AC8). The whole run (research, spec, plan, 2 preflights, execution, 1 remediation cycle, PR, CI) finished +in about 2.5 hours and `require_complete` + `require_model_routing` passed with +`required_mcp_tools` naming `new_potential_bug_entry`. + +**Why:** the user's seed said "switch to large if >3 files"; doing the work-mode flip up front avoided +the prd-feature/minor-audit conflict. Give the researcher the 11-label `## Numeric Derivation +Evidence` spec in its prompt (see [[prd-feature-stop-hooks-are-workmode-blind]]); even so it named two +labels `Primary Search Strategy` instead of `... or Query Expression`, so prd-feature wrote count-free ACs. + +**How to apply:** after every executor return, compare evidence filename stamps with file +LastWriteTimeUtc; a sonnet remediation executor stamped 11 files 10-20 minutes in the future. Rename to +the write time and fix the `Timestamp:` line before committing. See [[evidence-timestamps-can-be-synthesized]]. diff --git a/.claude/agent-memory/prd-feature/feedback_ac_gates_verify_satisfiability.md b/.claude/agent-memory/prd-feature/feedback_ac_gates_verify_satisfiability.md index 05c0d3849..40527d450 100644 --- a/.claude/agent-memory/prd-feature/feedback_ac_gates_verify_satisfiability.md +++ b/.claude/agent-memory/prd-feature/feedback_ac_gates_verify_satisfiability.md @@ -23,4 +23,5 @@ Two rules for authoring/correcting acceptance criteria in spec.md: 8. An “every file in the Write Set obeys the 500-line ceiling” gate is a dead gate as written. The Write Set routinely contains non-SDK-style .csproj files that are thousands of lines long and Markdown documents, and `.claude/rules/general-code-change.md` scopes the 500-line limit to production code, test code, and reusable scripts while explicitly exempting Markdown. On #838 (2026-09-12) the caller's AC text said “and so does every file in the Write Set”; I narrowed it to the named .cs files only and stated the project-file/Markdown exemption inside the criterion. Enumerate the .cs files rather than saying “every file in the Write Set”. 12. Refinement of item 7, from reading `.claude/hooks/validate-prd-feature-output.ps1` on #973 (2026-10-02): digits on AC lines are NOT banned outright. The hook fires on `\b\d+\b` in any checkbox line under `## Acceptance Criteria`, then validates ONLY the single `research-path:` file named in the final output, taking the FIRST match of each of the eleven labels inside the first `## Numeric Derivation Evidence` section (`###` sub-blocks stay inside that section because the lookahead is `^##\s`). So when the supplied research record was authored to pass (one-line label values; "entire/all/complete ... repository" in the scope; two differently worded strategies that each contain every Complete Family member verbatim; comma-separated member sets whose cardinality equals the counts; the word "equal/match/identical" in the comparison), trace those checks by hand once, then use the derived counts freely. Counts the record does NOT derive (on #973: fourteen files, thirteen ADAL blocks) still go in as an enumeration or an absence, with one sentence in the AC section saying why. A second research file's derivation blocks can back further counts in the body and in AC prose, but the hook never reads it; report it on a line whose label is not `research-path` (for example `supplemental-research-path:`) so the hook's `^\s*research-path\s*:` regex does not pick it up. 13. Hyphenated identifiers leak standalone digits: `CR-1`, `L4-T1`, `#956`, `T12` differ. `\b\d+\b` matches the `1` in `CR-1` (hyphen is a non-word character) and the `956` in `#956`, but not `T12`, `L4`, `AC16` or `FileIO2` (letter adjacent). On #959 (2026-10-02) two AC lines failed my own post-write Grep on `CR-1` and on the predecessor feature-folder path. Always run `Grep '^- \[ \] AC.*(\b[0-9]+\b|[<>%])'` on the spec after writing and before reporting; rename hyphenated finding labels to words on AC lines ("the predecessor-spec wording correction") and point at in-scope files in other feature folders via "full backticked path in Proposed Fix Dn" so the footprint harvester still sees the path. +15. Hook trigger narrowed (issue #905 rule R2, observed #1005 on 2026-10-22): `Test-SpecNumericCriterion` now fires only on a determiner-led cardinal count of a plural noun on a checkbox line (`all|exactly|the|these|those` + digit or number word up to twenty + plural noun), skipping lines citing `.claude/rules/`. Bare digits no longer fire it. Also check the research record's label names literally: on #1005 the researcher wrote `Primary Search Strategy:` instead of `Primary Search Strategy or Query Expression:`, which fails the hook's label regex, so the record cannot back any count; enumerate the population by name on AC lines and say so in Assumptions. 14. Evidence paths cannot appear in AC lines at all in this repo: the feature-folder slug carries the date and the issue number, so a full feature-relative evidence path trips the zero-digit rule (item 7), and a bare `evidence/...` span trips the blast-radius harvester. Resolution used on #942 (2026-09-29): put the full backticked paths in Test Strategy, and in each AC refer to the projection by its unbackticked bare filename plus "under the feature folder's evidence directory (full path in Test Strategy)". Also avoid `` `try`/`finally` ``: the slash between two adjacent spans reads as a one-character path span to a naive regex; write "try-finally". diff --git a/.claude/agent-memory/task-researcher/MEMORY.md b/.claude/agent-memory/task-researcher/MEMORY.md index f0798a91b..45ca9c662 100644 --- a/.claude/agent-memory/task-researcher/MEMORY.md +++ b/.claude/agent-memory/task-researcher/MEMORY.md @@ -23,6 +23,7 @@ - [svgcontrol-test-418](project_svgcontrol_test_unwired_418.md) — STALE (in .sln since 2026-08-14) ## Threading / test determinism +- [settings-save-1005](project_settings_save_shared_user_config_1005.md) — 26 Settings Save() sites share one user.config; ISettingsStore seam; SettingsSaving cancel probe - [taskrun-getresult-inlines-900](project_taskrun_getresult_inlines_on_pool_thread_900.md) — Task.Run+GetResult on a pool thread INLINES - [taskrun-triage-931](project_taskrun_triage_931.md) — 2/22 Task.Run sites thread-dependent; OpenRead returns FileStream - [prime-marker-944](project_prime_marker_register_before_start_944.md) — ContinueWith(None) never inlines; TCS marker before start diff --git a/.claude/agent-memory/task-researcher/project_settings_save_shared_user_config_1005.md b/.claude/agent-memory/task-researcher/project_settings_save_shared_user_config_1005.md new file mode 100644 index 000000000..0999237f2 --- /dev/null +++ b/.claude/agent-memory/task-researcher/project_settings_save_shared_user_config_1005.md @@ -0,0 +1,12 @@ +--- +name: settings-save-shared-user-config-1005 +description: 26 production Settings Save() sites (15 test-reached) race on one testhost user.config; per-instance ISettingsStore seam + SettingsSaving cancel probe for fail-before +metadata: + type: project +--- + +Issue #1005 (2026-10-22): every `Properties.Settings.Default.Save()` reached by a test races on the single host-derived user.config ("configuration file has been changed by another program"). Repo-wide there are 26 settings Save() sites (IDList 1, AppQuickFilerSettings 6, AppStagingFilenames 9, AppAutoFileObjects 6 via `_defaults.Save()`, AppOlObjects.JunkFolders 3, AppOlObjects DarkMode 1); 15 are test-reached. Test code also calls `defaults.Save()` in AppAutoFileObjectsCoverageExpansionTests.TestCleanup. + +**Why:** `new IDList()` has `_maxIDLength == 0`, so the first GetNextToDoID always saves; TaskTree.Test MakeController hands out a fresh real IDList. + +**How to apply:** recommend a public UtilitiesCS `ISettingsStore` (indexer + Save) with an `ApplicationSettingsBase` adapter, injected per instance. The adapter is coverable via a test subclass overriding the indexer and Save (both overridable). A deterministic fail-before: subscribe `SettingsSaving`, set `e.Cancel = true` (skips `base.Save`), attribute by thread id, unsubscribe in finally. Watch for grep misses: `_defaults.Save()` and `defaults.Save()` do not match `Settings.Default.Save()`. ToDoModel.Test and TaskTree.Test are C# 7.3 (no LangVersion). diff --git a/TaskMaster.Test/AppGlobals/AppAutoFileObjectsCoverageExpansionTests.cs b/TaskMaster.Test/AppGlobals/AppAutoFileObjectsCoverageExpansionTests.cs index 532e1a837..22fc465df 100644 --- a/TaskMaster.Test/AppGlobals/AppAutoFileObjectsCoverageExpansionTests.cs +++ b/TaskMaster.Test/AppGlobals/AppAutoFileObjectsCoverageExpansionTests.cs @@ -15,41 +15,34 @@ namespace TaskMaster.Test.AppGlobals { /// /// Coverage expansion for AppAutoFileObjects initialization and property decision paths that can - /// run without Outlook automation or filesystem temporary files. + /// run without Outlook automation or filesystem temporary files. Every system under test is built + /// over its own seeded , so the scalar settings are read from + /// and saved to the fake and no shared settings instance or settings file is touched. /// [TestClass] public class AppAutoFileObjectsCoverageExpansionTests { - private int _originalLngConvCtPwr; - private int _originalConversationWeight; - private int _originalMatchScore; - private int _originalMismatchScore; - private int _originalGapPenalty; - private int _originalMaxRecents; - - [TestInitialize] - public void TestInitialize() + private const int SeedLngConvCtPwr = 2; + private const int SeedConversationWeight = 10; + private const int SeedMatchScore = 3; + private const int SeedMismatchScore = -1; + private const int SeedGapPenalty = -2; + private const int SeedMaxRecents = 15; + + private static InMemorySettingsStore CreateScalarStore() { - var defaults = TaskMaster.Properties.Settings.Default; - _originalLngConvCtPwr = defaults.ConversationExponent; - _originalConversationWeight = defaults.ConversationWeight; - _originalMatchScore = defaults.SmithWatterman_MatchScore; - _originalMismatchScore = defaults.SmithWatterman_MismatchScore; - _originalGapPenalty = defaults.SmithWatterman_GapPenalty; - _originalMaxRecents = defaults.MaxRecents; - } - - [TestCleanup] - public void TestCleanup() - { - var defaults = TaskMaster.Properties.Settings.Default; - defaults.ConversationExponent = _originalLngConvCtPwr; - defaults.ConversationWeight = _originalConversationWeight; - defaults.SmithWatterman_MatchScore = _originalMatchScore; - defaults.SmithWatterman_MismatchScore = _originalMismatchScore; - defaults.SmithWatterman_GapPenalty = _originalGapPenalty; - defaults.MaxRecents = _originalMaxRecents; - defaults.Save(); + var store = new InMemorySettingsStore(); + store[nameof(TaskMaster.Properties.Settings.ConversationExponent)] = SeedLngConvCtPwr; + store[nameof(TaskMaster.Properties.Settings.ConversationWeight)] = + SeedConversationWeight; + store[nameof(TaskMaster.Properties.Settings.SmithWatterman_MatchScore)] = + SeedMatchScore; + store[nameof(TaskMaster.Properties.Settings.SmithWatterman_MismatchScore)] = + SeedMismatchScore; + store[nameof(TaskMaster.Properties.Settings.SmithWatterman_GapPenalty)] = + SeedGapPenalty; + store[nameof(TaskMaster.Properties.Settings.MaxRecents)] = SeedMaxRecents; + return store; } [TestMethod] @@ -134,23 +127,90 @@ public void FileBackedProperties_WhenOnlyFlowFolderExists_StillSkipPythonStaging public void ScalarSettingsProperties_WhenRoundTripped_UpdateDefaultsAndReturnValues() { // Arrange - var sut = CreateSutWithSpecialFolders(new ConcurrentDictionary()); + var store = CreateScalarStore(); + var sut = CreateSutWithSpecialFolders( + new ConcurrentDictionary(), + store: store + ); // Act - sut.LngConvCtPwr = _originalLngConvCtPwr + 1; - sut.Conversation_Weight = _originalConversationWeight + 1; - sut.SmithWatterman_MatchScore = _originalMatchScore + 1; - sut.SmithWatterman_MismatchScore = _originalMismatchScore + 1; - sut.SmithWatterman_GapPenalty = _originalGapPenalty + 1; - sut.MaxRecents = _originalMaxRecents + 1; + var saves = SettingsSaveProbe.CountSavesOnCurrentThread( + TaskMaster.Properties.Settings.Default, + () => + { + sut.LngConvCtPwr = SeedLngConvCtPwr + 1; + sut.Conversation_Weight = SeedConversationWeight + 1; + sut.SmithWatterman_MatchScore = SeedMatchScore + 1; + sut.SmithWatterman_MismatchScore = SeedMismatchScore + 1; + sut.SmithWatterman_GapPenalty = SeedGapPenalty + 1; + sut.MaxRecents = SeedMaxRecents + 1; + } + ); // Assert - sut.LngConvCtPwr.Should().Be(_originalLngConvCtPwr + 1); - sut.Conversation_Weight.Should().Be(_originalConversationWeight + 1); - sut.SmithWatterman_MatchScore.Should().Be(_originalMatchScore + 1); - sut.SmithWatterman_MismatchScore.Should().Be(_originalMismatchScore + 1); - sut.SmithWatterman_GapPenalty.Should().Be(_originalGapPenalty + 1); - sut.MaxRecents.Should().Be(_originalMaxRecents + 1); + sut.LngConvCtPwr.Should().Be(SeedLngConvCtPwr + 1); + sut.Conversation_Weight.Should().Be(SeedConversationWeight + 1); + sut.SmithWatterman_MatchScore.Should().Be(SeedMatchScore + 1); + sut.SmithWatterman_MismatchScore.Should().Be(SeedMismatchScore + 1); + sut.SmithWatterman_GapPenalty.Should().Be(SeedGapPenalty + 1); + sut.MaxRecents.Should().Be(SeedMaxRecents + 1); + store[nameof(TaskMaster.Properties.Settings.ConversationExponent)] + .Should() + .Be(SeedLngConvCtPwr + 1); + store[nameof(TaskMaster.Properties.Settings.ConversationWeight)] + .Should() + .Be(SeedConversationWeight + 1); + store[nameof(TaskMaster.Properties.Settings.SmithWatterman_MatchScore)] + .Should() + .Be(SeedMatchScore + 1); + store[nameof(TaskMaster.Properties.Settings.SmithWatterman_MismatchScore)] + .Should() + .Be(SeedMismatchScore + 1); + store[nameof(TaskMaster.Properties.Settings.SmithWatterman_GapPenalty)] + .Should() + .Be(SeedGapPenalty + 1); + store[nameof(TaskMaster.Properties.Settings.MaxRecents)] + .Should() + .Be(SeedMaxRecents + 1); + store.SaveCount.Should().Be(6, "each scalar setter saves through the store once"); + saves + .Should() + .Be(0, "the test persisted settings to the shared per-host settings file"); + } + + [TestMethod] + public void Constructor_ParentOnly_UsesApplicationSettingsStore() + { + // Arrange + var parent = new Mock(MockBehavior.Strict); + var field = typeof(AppAutoFileObjects).GetField( + "_settingsStore", + BindingFlags.Instance | BindingFlags.NonPublic + ); + + // Act: inspect the default store without reading or writing any setting. + var store = field!.GetValue(new AppAutoFileObjects(parent.Object)); + + // Assert + store + .Should() + .BeOfType( + "the production constructor persists through TaskMaster's own settings" + ); + } + + [TestMethod] + public void Constructor_NullSettingsStore_ThrowsArgumentNullException() + { + // Arrange + var parent = new Mock(MockBehavior.Strict); + Action act = () => _ = new AppAutoFileObjects(parent.Object, null!); + + // Act / Assert + act.Should() + .Throw() + .Which.ParamName.Should() + .Be("settingsStore"); } [TestMethod] @@ -222,7 +282,8 @@ public void BackupLoaders_WithMissingFiles_ReturnEmptyCollections() private static AppAutoFileObjects CreateSutWithSpecialFolders( ConcurrentDictionary specialFolders, - ConcurrentDictionary config = null + ConcurrentDictionary config = null, + InMemorySettingsStore store = null ) { var mockFs = new Mock(MockBehavior.Strict); @@ -235,7 +296,7 @@ private static AppAutoFileObjects CreateSutWithSpecialFolders( .SetupGet(x => x.IntelRes) .Returns(new StubIntelligenceConfig(mockGlobals.Object, config)); } - return new AppAutoFileObjects(mockGlobals.Object); + return new AppAutoFileObjects(mockGlobals.Object, store ?? CreateScalarStore()); } private static async Task InvokePrivateTaskAsync(AppAutoFileObjects sut, string methodName) diff --git a/TaskMaster.Test/AppGlobals/AppQuickFilerSettingsRemainingPropertiesTests.cs b/TaskMaster.Test/AppGlobals/AppQuickFilerSettingsRemainingPropertiesTests.cs index 2eb42c177..f97aa6896 100644 --- a/TaskMaster.Test/AppGlobals/AppQuickFilerSettingsRemainingPropertiesTests.cs +++ b/TaskMaster.Test/AppGlobals/AppQuickFilerSettingsRemainingPropertiesTests.cs @@ -5,64 +5,50 @@ namespace TaskMaster.Test.AppGlobals { /// - /// Covers the remaining pure boolean properties of not - /// already exercised by AppQuickFilerSettingsTests (which covers HighConfidenceModeEnabled - /// and HighConfidenceThreshold): MoveEntireConversation, SaveAttachments, SavePictures, and - /// SaveEmailCopy. Each getter reads and each internal setter - /// (reachable via InternalsVisibleTo("TaskMaster.Test")) writes Settings.Default and Save()s. - /// The affected Settings.Default values are snapshotted in and - /// restored in so machine state is not mutated; no injectable settings - /// seam is introduced. + /// Covers the remaining boolean properties of not already + /// exercised by AppQuickFilerSettingsTests (which covers HighConfidenceModeEnabled and + /// HighConfidenceThreshold): MoveEntireConversation, SaveAttachments, SavePictures, and + /// SaveEmailCopy. Each test injects its own seeded ; getters + /// read the fake and each internal setter (reachable via InternalsVisibleTo("TaskMaster.Test")) + /// writes the fake and saves through it once, so no shared settings instance or file is touched. /// [TestClass] public class AppQuickFilerSettingsRemainingPropertiesTests { - private bool _origMoveEntireConversations; - private bool _origSaveAttachments; - private bool _origSavePictures; - private bool _origSaveEmailCopy; - - [TestInitialize] - public void TestInitialize() - { - _origMoveEntireConversations = Settings.Default.MoveEntireConversations; - _origSaveAttachments = Settings.Default.SaveAttachments; - _origSavePictures = Settings.Default.SavePictures; - _origSaveEmailCopy = Settings.Default.SaveEmailCopy; - } - - [TestCleanup] - public void TestCleanup() + private static InMemorySettingsStore CreateStore(bool initialValue) { - Settings.Default.MoveEntireConversations = _origMoveEntireConversations; - Settings.Default.SaveAttachments = _origSaveAttachments; - Settings.Default.SavePictures = _origSavePictures; - Settings.Default.SaveEmailCopy = _origSaveEmailCopy; + var store = new InMemorySettingsStore(); + store[nameof(Settings.MoveEntireConversations)] = initialValue; + store[nameof(Settings.SaveAttachments)] = initialValue; + store[nameof(Settings.SavePictures)] = initialValue; + store[nameof(Settings.SaveEmailCopy)] = initialValue; + return store; } [TestMethod] public void MoveEntireConversation_Getter_ReflectsSettingsDefault() { // Arrange - Settings.Default.MoveEntireConversations = true; - var settings = new AppQuickFilerSettings(); + var store = CreateStore(initialValue: true); + var settings = new AppQuickFilerSettings(store); // Act / Assert - settings.MoveEntireConversation.Should().BeTrue("the getter reads Settings.Default"); + settings.MoveEntireConversation.Should().BeTrue("the getter reads the settings store"); - // Arrange a different persisted value - Settings.Default.MoveEntireConversations = false; + // Arrange a different stored value + store[nameof(Settings.MoveEntireConversations)] = false; // Act / Assert settings.MoveEntireConversation.Should().BeFalse(); + store.SaveCount.Should().Be(0, "reading a setting does not persist anything"); } [TestMethod] public void MoveEntireConversation_Setter_RoundTripsThroughSettingsDefault() { // Arrange - Settings.Default.MoveEntireConversations = false; - var settings = new AppQuickFilerSettings(); + var store = CreateStore(initialValue: false); + var settings = new AppQuickFilerSettings(store); // Act settings.MoveEntireConversation = true; @@ -70,65 +56,70 @@ public void MoveEntireConversation_Setter_RoundTripsThroughSettingsDefault() // Assert settings .MoveEntireConversation.Should() - .BeTrue("the setter persists to Settings.Default"); - Settings.Default.MoveEntireConversations.Should().BeTrue(); + .BeTrue("the setter persists to the settings store"); + store[nameof(Settings.MoveEntireConversations)].Should().Be(true); + store.SaveCount.Should().Be(1); } [TestMethod] public void SaveAttachments_Setter_RoundTripsThroughSettingsDefault() { // Arrange - Settings.Default.SaveAttachments = false; - var settings = new AppQuickFilerSettings(); + var store = CreateStore(initialValue: false); + var settings = new AppQuickFilerSettings(store); // Act settings.SaveAttachments = true; // Assert settings.SaveAttachments.Should().BeTrue(); - Settings.Default.SaveAttachments.Should().BeTrue(); + store[nameof(Settings.SaveAttachments)].Should().Be(true); + store.SaveCount.Should().Be(1); } [TestMethod] public void SavePictures_Setter_RoundTripsThroughSettingsDefault() { // Arrange - Settings.Default.SavePictures = false; - var settings = new AppQuickFilerSettings(); + var store = CreateStore(initialValue: false); + var settings = new AppQuickFilerSettings(store); // Act settings.SavePictures = true; // Assert settings.SavePictures.Should().BeTrue(); - Settings.Default.SavePictures.Should().BeTrue(); + store[nameof(Settings.SavePictures)].Should().Be(true); + store.SaveCount.Should().Be(1); } [TestMethod] public void SaveEmailCopy_Setter_RoundTripsThroughSettingsDefault() { // Arrange - Settings.Default.SaveEmailCopy = false; - var settings = new AppQuickFilerSettings(); + var store = CreateStore(initialValue: false); + var settings = new AppQuickFilerSettings(store); // Act settings.SaveEmailCopy = true; // Assert settings.SaveEmailCopy.Should().BeTrue(); - Settings.Default.SaveEmailCopy.Should().BeTrue(); + store[nameof(Settings.SaveEmailCopy)].Should().Be(true); + store.SaveCount.Should().Be(1); } [TestMethod] public void SaveAttachments_Getter_ReflectsSettingsDefaultFalseAndTrue() { // Arrange / Act / Assert: cover both boolean states of the getter. - Settings.Default.SaveAttachments = false; - var settings = new AppQuickFilerSettings(); + var store = CreateStore(initialValue: false); + var settings = new AppQuickFilerSettings(store); settings.SaveAttachments.Should().BeFalse(); - Settings.Default.SaveAttachments = true; + store[nameof(Settings.SaveAttachments)] = true; settings.SaveAttachments.Should().BeTrue(); + store.SaveCount.Should().Be(0, "reading a setting does not persist anything"); } } } diff --git a/TaskMaster.Test/AppGlobals/AppQuickFilerSettingsTests.cs b/TaskMaster.Test/AppGlobals/AppQuickFilerSettingsTests.cs index 8ba0c6090..30a434de3 100644 --- a/TaskMaster.Test/AppGlobals/AppQuickFilerSettingsTests.cs +++ b/TaskMaster.Test/AppGlobals/AppQuickFilerSettingsTests.cs @@ -1,89 +1,128 @@ +using System; +using System.Reflection; using FluentAssertions; using Microsoft.VisualStudio.TestTools.UnitTesting; using TaskMaster.Properties; +using UtilitiesCS; namespace TaskMaster.Test.AppGlobals { /// - /// Verifies the high-confidence settings exposed by : - /// their persisted defaults and that the internal setters round-trip through - /// . The persisted values are snapshotted in - /// and restored in so the tests are - /// independent and do not leave machine state mutated. + /// Verifies the high-confidence settings exposed by . Each + /// test constructs the system under test over its own seeded , + /// so getters read the fake, setters write the fake and count one save per assignment, and no + /// test reads or writes the shared instance or its settings file. /// - [DoNotParallelize] [TestClass] public class AppQuickFilerSettingsTests { - private bool _originalModeEnabled; - private double _originalThreshold; - - [TestInitialize] - public void TestInitialize() + private static InMemorySettingsStore CreateStore(bool modeEnabled, double threshold) { - _originalModeEnabled = Settings.Default.HighConfidenceModeEnabled; - _originalThreshold = Settings.Default.HighConfidenceThreshold; - } - - [TestCleanup] - public void TestCleanup() - { - Settings.Default.HighConfidenceModeEnabled = _originalModeEnabled; - Settings.Default.HighConfidenceThreshold = _originalThreshold; + var store = new InMemorySettingsStore(); + store[nameof(Settings.HighConfidenceModeEnabled)] = modeEnabled; + store[nameof(Settings.HighConfidenceThreshold)] = threshold; + return store; } [TestMethod] public void HighConfidenceModeEnabled_Default_IsFalse() { - // Arrange: ensure the persisted value matches the declared default. - Settings.Default.HighConfidenceModeEnabled = false; - var settings = new AppQuickFilerSettings(); + // Arrange: the store holds the declared default. + var store = CreateStore(modeEnabled: false, threshold: 0.9); + var settings = new AppQuickFilerSettings(store); // Act var result = settings.HighConfidenceModeEnabled; // Assert result.Should().BeFalse(); + store.SaveCount.Should().Be(0, "reading a setting does not persist anything"); } [TestMethod] public void HighConfidenceThreshold_Default_IsZeroPointNine() { - // Arrange: ensure the persisted value matches the declared default. - Settings.Default.HighConfidenceThreshold = 0.9; - var settings = new AppQuickFilerSettings(); + // Arrange: the store holds the declared default. + var store = CreateStore(modeEnabled: false, threshold: 0.9); + var settings = new AppQuickFilerSettings(store); // Act var result = settings.HighConfidenceThreshold; // Assert result.Should().Be(0.9); + store.SaveCount.Should().Be(0, "reading a setting does not persist anything"); } [TestMethod] public void HighConfidenceModeEnabled_WhenSetTrue_ReadsBackTrue() { // Arrange - var settings = new AppQuickFilerSettings(); + var store = CreateStore(modeEnabled: false, threshold: 0.9); + var settings = new AppQuickFilerSettings(store); // Act - settings.HighConfidenceModeEnabled = true; + var saves = SettingsSaveProbe.CountSavesOnCurrentThread( + Settings.Default, + () => settings.HighConfidenceModeEnabled = true + ); // Assert settings.HighConfidenceModeEnabled.Should().BeTrue(); + store[nameof(Settings.HighConfidenceModeEnabled)].Should().Be(true); + store.SaveCount.Should().Be(1, "the setter persists through the injected store once"); + saves + .Should() + .Be(0, "the test persisted settings to the shared per-host settings file"); } [TestMethod] public void HighConfidenceThreshold_WhenSet_ReadsBackSameValue() { // Arrange - var settings = new AppQuickFilerSettings(); + var store = CreateStore(modeEnabled: false, threshold: 0.9); + var settings = new AppQuickFilerSettings(store); // Act settings.HighConfidenceThreshold = 0.75; // Assert settings.HighConfidenceThreshold.Should().Be(0.75); + store[nameof(Settings.HighConfidenceThreshold)].Should().Be(0.75); + store.SaveCount.Should().Be(1, "the setter persists through the injected store once"); + } + + [TestMethod] + public void Constructor_Parameterless_UsesApplicationSettingsStore() + { + // Arrange + var field = typeof(AppQuickFilerSettings).GetField( + "_settingsStore", + BindingFlags.Instance | BindingFlags.NonPublic + ); + + // Act: inspect the default store without reading or writing any setting. + var store = field!.GetValue(new AppQuickFilerSettings()); + + // Assert + store + .Should() + .BeOfType( + "the production constructor persists through TaskMaster's own settings" + ); + } + + [TestMethod] + public void Constructor_NullSettingsStore_ThrowsArgumentNullException() + { + // Arrange + Action act = () => _ = new AppQuickFilerSettings(null!); + + // Act / Assert + act.Should() + .Throw() + .Which.ParamName.Should() + .Be("settingsStore"); } } } diff --git a/TaskMaster.Test/AppGlobals/AppStagingFilenamesTests.cs b/TaskMaster.Test/AppGlobals/AppStagingFilenamesTests.cs index 476615d60..6d37f8311 100644 --- a/TaskMaster.Test/AppGlobals/AppStagingFilenamesTests.cs +++ b/TaskMaster.Test/AppGlobals/AppStagingFilenamesTests.cs @@ -1,48 +1,55 @@ +using System; +using System.Reflection; using FluentAssertions; using Microsoft.VisualStudio.TestTools.UnitTesting; using TaskMaster.Properties; +using UtilitiesCS; namespace TaskMaster.Test.AppGlobals { /// - /// Unit tests for . The class delegates each property to the - /// static singleton (lazy getter via InitProp, and a - /// setter that writes the backing field plus Settings.Default and calls Save() — except - /// EmailInfoStagingFile, whose setter writes only the backing field). There is no - /// injectable settings type, so the established pattern from AppQuickFilerSettingsTests - /// is used: the affected Settings.Default values are snapshotted in - /// and restored in so machine state is not mutated and tests stay - /// independent. No filesystem access and no temp files are used. + /// Unit tests for . Each property has a lazy getter (via + /// InitProp) that reads the injected settings store, and a setter that writes the backing + /// field plus the store and saves through it, except EmailInfoStagingFile, whose setter + /// writes only the backing field. Every test injects its own seeded + /// , so no shared settings instance, settings file, or other + /// filesystem state is read or written. /// [TestClass] public class AppStagingFilenamesTests { - private string _origConditionalReminders; - private string _origCommonWords; - private string _origEmailInfoStaging; - - [TestInitialize] - public void TestInitialize() + private static readonly string[] SettingNames = { - _origConditionalReminders = Settings.Default.File_ConditionalReminders; - _origCommonWords = Settings.Default.File_Common_Words; - _origEmailInfoStaging = Settings.Default.FileName_EmailInfoStaging; - } + nameof(Settings.File_ConditionalReminders), + nameof(Settings.File_Common_Words), + nameof(Settings.File_Subject_Map), + nameof(Settings.File_CTF_Inc), + nameof(Settings.File_CTF_Map), + nameof(Settings.FileName_EmailSessionTmp), + nameof(Settings.FileName_EmailSession), + nameof(Settings.FileName_MovedEmailsBackup), + nameof(Settings.FileName_Recents), + nameof(Settings.FileName_EmailInfoStaging), + }; - [TestCleanup] - public void TestCleanup() + private static InMemorySettingsStore CreateSeededStore() { - Settings.Default.File_ConditionalReminders = _origConditionalReminders; - Settings.Default.File_Common_Words = _origCommonWords; - Settings.Default.FileName_EmailInfoStaging = _origEmailInfoStaging; + var store = new InMemorySettingsStore(); + foreach (var name in SettingNames) + { + store[name] = $@"seeded\{name}.txt"; + } + + return store; } [TestMethod] public void ConditionalReminders_Getter_ReturnsPersistedSettingsValue() { - // Arrange: a fresh instance reads the lazy getter from Settings.Default. - Settings.Default.File_ConditionalReminders = @"persisted\reminders.json"; - var filenames = new AppStagingFilenames(); + // Arrange: a fresh instance reads the lazy getter from the settings store. + var store = CreateSeededStore(); + store[nameof(Settings.File_ConditionalReminders)] = @"persisted\reminders.json"; + var filenames = new AppStagingFilenames(store); // Act var result = filenames.ConditionalReminders; @@ -50,33 +57,43 @@ public void ConditionalReminders_Getter_ReturnsPersistedSettingsValue() // Assert result .Should() - .Be(@"persisted\reminders.json", "the getter lazily reads Settings.Default"); + .Be(@"persisted\reminders.json", "the getter lazily reads the settings store"); + store.SaveCount.Should().Be(0); } [TestMethod] public void ConditionalReminders_Setter_RoundTripsThroughSettingsDefault() { // Arrange - var filenames = new AppStagingFilenames(); + var store = CreateSeededStore(); + var filenames = new AppStagingFilenames(store); // Act - filenames.ConditionalReminders = @"new\reminders.json"; + var saves = SettingsSaveProbe.CountSavesOnCurrentThread( + Settings.Default, + () => filenames.ConditionalReminders = @"new\reminders.json" + ); // Assert filenames .ConditionalReminders.Should() .Be(@"new\reminders.json", "the setter caches the value in the backing field"); - Settings - .Default.File_ConditionalReminders.Should() - .Be(@"new\reminders.json", "the setter persists the value to Settings.Default"); + store[nameof(Settings.File_ConditionalReminders)] + .Should() + .Be(@"new\reminders.json", "the setter persists the value to the settings store"); + store.SaveCount.Should().Be(1); + saves + .Should() + .Be(0, "the test persisted settings to the shared per-host settings file"); } [TestMethod] public void CommonWords_Getter_ReturnsPersistedSettingsValue() { // Arrange - Settings.Default.File_Common_Words = @"persisted\common.txt"; - var filenames = new AppStagingFilenames(); + var store = CreateSeededStore(); + store[nameof(Settings.File_Common_Words)] = @"persisted\common.txt"; + var filenames = new AppStagingFilenames(store); // Act var result = filenames.CommonWords; @@ -89,11 +106,12 @@ public void CommonWords_Getter_ReturnsPersistedSettingsValue() public void CommonWords_GetterAfterSet_ReturnsCachedValueWithoutReReadingSettings() { // Arrange: prove the backing field short-circuits the lazy getter once set. - var filenames = new AppStagingFilenames(); + var store = CreateSeededStore(); + var filenames = new AppStagingFilenames(store); filenames.CommonWords = @"cached\value.txt"; - // Act: change Settings.Default underneath; the cached field should win. - Settings.Default.File_Common_Words = @"changed\underneath.txt"; + // Act: change the settings store underneath; the cached field should win. + store[nameof(Settings.File_Common_Words)] = @"changed\underneath.txt"; var result = filenames.CommonWords; // Assert @@ -103,14 +121,16 @@ public void CommonWords_GetterAfterSet_ReturnsCachedValueWithoutReReadingSetting @"cached\value.txt", "the non-null backing field short-circuits the lazy getter" ); + store.SaveCount.Should().Be(1); } [TestMethod] public void EmailInfoStagingFile_Setter_DoesNotWriteSettingsDefault() { // Arrange: this setter is the documented exception that updates only the backing field. - Settings.Default.FileName_EmailInfoStaging = @"original\staging.json"; - var filenames = new AppStagingFilenames(); + var store = CreateSeededStore(); + store[nameof(Settings.FileName_EmailInfoStaging)] = @"original\staging.json"; + var filenames = new AppStagingFilenames(store); // Act filenames.EmailInfoStagingFile = @"local-only\staging.json"; @@ -119,20 +139,22 @@ public void EmailInfoStagingFile_Setter_DoesNotWriteSettingsDefault() filenames .EmailInfoStagingFile.Should() .Be(@"local-only\staging.json", "the backing field is updated"); - Settings - .Default.FileName_EmailInfoStaging.Should() + store[nameof(Settings.FileName_EmailInfoStaging)] + .Should() .Be( @"original\staging.json", - "the EmailInfoStagingFile setter does NOT persist to Settings.Default" + "the EmailInfoStagingFile setter does NOT persist to the settings store" ); + store.SaveCount.Should().Be(0); } [TestMethod] public void EmailInfoStagingFile_Getter_LazilyReadsSettingsDefaultWhenUnset() { // Arrange - Settings.Default.FileName_EmailInfoStaging = @"persisted\staging.json"; - var filenames = new AppStagingFilenames(); + var store = CreateSeededStore(); + store[nameof(Settings.FileName_EmailInfoStaging)] = @"persisted\staging.json"; + var filenames = new AppStagingFilenames(store); // Act var result = filenames.EmailInfoStagingFile; @@ -140,7 +162,107 @@ public void EmailInfoStagingFile_Getter_LazilyReadsSettingsDefaultWhenUnset() // Assert result .Should() - .Be(@"persisted\staging.json", "the getter lazily reads Settings.Default"); + .Be(@"persisted\staging.json", "the getter lazily reads the settings store"); + } + + [TestMethod] + public void SavingSetters_EachAssignment_WritesStoreAndSavesOnce() + { + // Arrange + var store = CreateSeededStore(); + var filenames = new AppStagingFilenames(store); + + // Act: assign all nine persisting properties. + filenames.ConditionalReminders = "a"; + filenames.CommonWords = "b"; + filenames.SubjectMap = "c"; + filenames.CtfInc = "d"; + filenames.CtfMap = "e"; + filenames.EmailSessionTemp = "f"; + filenames.EmailSession = "g"; + filenames.MovedMails = "h"; + filenames.RecentsFile = "i"; + + // Assert + store[nameof(Settings.File_ConditionalReminders)].Should().Be("a"); + store[nameof(Settings.File_Common_Words)].Should().Be("b"); + store[nameof(Settings.File_Subject_Map)].Should().Be("c"); + store[nameof(Settings.File_CTF_Inc)].Should().Be("d"); + store[nameof(Settings.File_CTF_Map)].Should().Be("e"); + store[nameof(Settings.FileName_EmailSessionTmp)].Should().Be("f"); + store[nameof(Settings.FileName_EmailSession)].Should().Be("g"); + store[nameof(Settings.FileName_MovedEmailsBackup)].Should().Be("h"); + store[nameof(Settings.FileName_Recents)].Should().Be("i"); + store.SaveCount.Should().Be(9, "each persisting setter saves exactly once"); + } + + [TestMethod] + public void Getters_UnsetFields_ReadSeededStoreValues() + { + // Arrange + var store = CreateSeededStore(); + var filenames = new AppStagingFilenames(store); + + // Act + var values = new[] + { + filenames.ConditionalReminders, + filenames.CommonWords, + filenames.SubjectMap, + filenames.CtfInc, + filenames.CtfMap, + filenames.EmailSessionTemp, + filenames.EmailSession, + filenames.MovedMails, + filenames.RecentsFile, + filenames.EmailInfoStagingFile, + }; + + // Assert + for (var i = 0; i < SettingNames.Length; i++) + { + values[i] + .Should() + .Be( + $@"seeded\{SettingNames[i]}.txt", + $"{SettingNames[i]} reads its seeded key" + ); + } + + store.SaveCount.Should().Be(0, "reading a setting does not persist anything"); + } + + [TestMethod] + public void Constructor_Parameterless_UsesApplicationSettingsStore() + { + // Arrange + var field = typeof(AppStagingFilenames).GetField( + "_settingsStore", + BindingFlags.Instance | BindingFlags.NonPublic + ); + + // Act: inspect the default store without reading or writing any setting. + var store = field!.GetValue(new AppStagingFilenames()); + + // Assert + store + .Should() + .BeOfType( + "the production constructor persists through TaskMaster's own settings" + ); + } + + [TestMethod] + public void Constructor_NullSettingsStore_ThrowsArgumentNullException() + { + // Arrange + Action act = () => _ = new AppStagingFilenames(null!); + + // Act / Assert + act.Should() + .Throw() + .Which.ParamName.Should() + .Be("settingsStore"); } } } diff --git a/TaskMaster.Test/AppGlobals/InMemorySettingsStore.cs b/TaskMaster.Test/AppGlobals/InMemorySettingsStore.cs new file mode 100644 index 000000000..15236bb08 --- /dev/null +++ b/TaskMaster.Test/AppGlobals/InMemorySettingsStore.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using UtilitiesCS; + +namespace TaskMaster.Test.AppGlobals +{ + /// + /// Per-test, dictionary-backed . Values are seeded through the + /// indexer, only counts calls, and nothing is read from or written to the + /// per-host settings file. Reading an unseeded key throws so a missing arrangement is explicit. + /// + internal sealed class InMemorySettingsStore : ISettingsStore + { + private readonly Dictionary _values = new Dictionary(); + + /// Gets the number of calls made on this store. + public int SaveCount { get; private set; } + + /// + /// The key has not been seeded. + public object this[string propertyName] + { + get + { + if (!_values.TryGetValue(propertyName, out var value)) + { + throw new KeyNotFoundException( + $"The setting '{propertyName}' was not seeded in the in-memory store." + ); + } + + return value; + } + set => _values[propertyName] = value; + } + + /// + public void Save() => SaveCount++; + } +} diff --git a/TaskMaster.Test/AppGlobals/SettingsSaveProbe.cs b/TaskMaster.Test/AppGlobals/SettingsSaveProbe.cs new file mode 100644 index 000000000..7aacb8546 --- /dev/null +++ b/TaskMaster.Test/AppGlobals/SettingsSaveProbe.cs @@ -0,0 +1,75 @@ +using System; +using System.ComponentModel; +using System.Configuration; +using System.Threading; + +namespace TaskMaster.Test.AppGlobals +{ + /// + /// Test-only probe that detects whether an action persists an + /// instance on the calling test thread. + /// + internal static class SettingsSaveProbe + { + /// + /// Runs and counts the + /// events that raises on the calling thread while it runs. + /// + /// The settings instance whose saves are observed. + /// The action under observation. + /// The number of saves attempted on the calling thread. + /// + /// The handler is thread-attributed: a save raised by another thread (for example a test of + /// another class running in parallel against the same settings instance) is neither counted + /// nor cancelled. Every counted save is cancelled through , + /// so skips the provider and nothing is written to + /// the per-host settings file. The handler is always removed in a finally block. + /// + internal static int CountSavesOnCurrentThread(ApplicationSettingsBase settings, Action act) + { + if (settings is null) + { + throw new ArgumentNullException(nameof(settings)); + } + + if (act is null) + { + throw new ArgumentNullException(nameof(act)); + } + + var threadId = Environment.CurrentManagedThreadId; + var count = 0; + SettingsSavingEventHandler handler = (sender, e) => + { + if (Environment.CurrentManagedThreadId == threadId) + { + Interlocked.Increment(ref count); + e.Cancel = true; + } + }; + + // SettingsSaving uses explicit add and remove accessors over a plain delegate field, + // which are not atomic; locking on the settings instance keeps two concurrent probes + // from losing a subscription. Only the subscribe and unsubscribe statements are locked, + // so the observed action itself never runs serialized. + lock (settings) + { + settings.SettingsSaving += handler; + } + + try + { + act(); + } + finally + { + lock (settings) + { + settings.SettingsSaving -= handler; + } + } + + return Volatile.Read(ref count); + } + } +} diff --git a/TaskMaster.Test/Ribbon/RibbonControllerTests.cs b/TaskMaster.Test/Ribbon/RibbonControllerTests.cs index 8d6615142..bf2beaa99 100644 --- a/TaskMaster.Test/Ribbon/RibbonControllerTests.cs +++ b/TaskMaster.Test/Ribbon/RibbonControllerTests.cs @@ -12,6 +12,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using TaskMaster.Properties; +using TaskMaster.Test.AppGlobals; using UtilitiesCS; using UtilitiesCS.OutlookObjects.Folder; @@ -23,43 +24,39 @@ namespace TaskMaster.Test.Ribbon /// concrete (Globals). To exercise the helpers without /// constructing the full Outlook-backed globals, an uninitialized /// is created and its _quickFilerSettings field is set to a real - /// ; that settings object round-trips through - /// , which is snapshotted in and - /// restored in so the tests are independent and leave no machine - /// state mutated. + /// over a per-test , so the + /// helpers read and save the fake and no shared settings instance or settings file is touched. + /// The class holds no other process-global state, so it runs in parallel. /// - [DoNotParallelize] [TestClass] public partial class RibbonControllerTests { - private bool _originalModeEnabled; - private double _originalThreshold; - - [TestInitialize] - public void TestInitialize() + private static InMemorySettingsStore CreateStore(bool modeEnabled, double threshold) { - _originalModeEnabled = Settings.Default.HighConfidenceModeEnabled; - _originalThreshold = Settings.Default.HighConfidenceThreshold; + var store = new InMemorySettingsStore(); + store[nameof(Settings.HighConfidenceModeEnabled)] = modeEnabled; + store[nameof(Settings.HighConfidenceThreshold)] = threshold; + return store; } - [TestCleanup] - public void TestCleanup() - { - Settings.Default.HighConfidenceModeEnabled = _originalModeEnabled; - Settings.Default.HighConfidenceThreshold = _originalThreshold; - } + /// + /// Builds a RibbonController whose Globals is an uninitialized ApplicationGlobals carrying a + /// real AppQuickFilerSettings over a store seeded with the declared high-confidence defaults. + /// + private static RibbonController CreateController() => + CreateController(CreateStore(modeEnabled: false, threshold: 0.9)); /// /// Builds a RibbonController whose Globals is an uninitialized ApplicationGlobals carrying a - /// real AppQuickFilerSettings, so the high-confidence helpers read/write Settings.Default. + /// real AppQuickFilerSettings over . /// - private static RibbonController CreateController() + private static RibbonController CreateController(InMemorySettingsStore store) { var globals = (ApplicationGlobals) FormatterServices.GetUninitializedObject(typeof(ApplicationGlobals)); typeof(ApplicationGlobals) .GetField("_quickFilerSettings", BindingFlags.NonPublic | BindingFlags.Instance) - .SetValue(globals, new AppQuickFilerSettings()); + .SetValue(globals, new AppQuickFilerSettings(store)); var controller = new RibbonController(); typeof(RibbonController) @@ -76,49 +73,54 @@ private static RibbonController CreateController() public void IsHighConfidenceModeActive_ReturnsStoredValue() { // Arrange - Settings.Default.HighConfidenceModeEnabled = true; - var controller = CreateController(); + var store = CreateStore(modeEnabled: true, threshold: 0.9); + var controller = CreateController(store); // Act var result = controller.IsHighConfidenceModeActive(); // Assert result.Should().BeTrue(); + store.SaveCount.Should().Be(0); } [TestMethod] public void ToggleHighConfidenceMode_FlipsStoredValue() { // Arrange - Settings.Default.HighConfidenceModeEnabled = false; - var controller = CreateController(); + var store = CreateStore(modeEnabled: false, threshold: 0.9); + var controller = CreateController(store); // Act controller.ToggleHighConfidenceMode(); // Assert controller.IsHighConfidenceModeActive().Should().BeTrue(); + store[nameof(Settings.HighConfidenceModeEnabled)].Should().Be(true); + store.SaveCount.Should().Be(1); } [TestMethod] public void SetHighConfidenceModeForLaunch_True_EnablesMode() { // Arrange: start from the disabled state. - Settings.Default.HighConfidenceModeEnabled = false; - var controller = CreateController(); + var store = CreateStore(modeEnabled: false, threshold: 0.9); + var controller = CreateController(store); // Act controller.SetHighConfidenceModeForLaunch(true); // Assert: the high-confidence launch path enables the mode. controller.IsHighConfidenceModeActive().Should().BeTrue(); + store.SaveCount.Should().Be(1); } [TestMethod] public void StandardLaunchAfterHighConfidenceLaunch_DoesNotEnableMode() { // Arrange: simulate a prior high-confidence launch having enabled the mode. - var controller = CreateController(); + var store = CreateStore(modeEnabled: false, threshold: 0.9); + var controller = CreateController(store); controller.SetHighConfidenceModeForLaunch(true); // Act: a subsequent standard launch (or release) resets the launch-scoped flag. @@ -127,14 +129,15 @@ public void StandardLaunchAfterHighConfidenceLaunch_DoesNotEnableMode() // Assert: the standard entry point does not inherit high-confidence mode, so it // never filters (AC6). controller.IsHighConfidenceModeActive().Should().BeFalse(); + store.SaveCount.Should().Be(2, "each launch-mode assignment saves once"); } [TestMethod] public void GetHighConfidenceThresholdText_ReturnsPercentageForm() { // Arrange: stored probability 0.9 should render as "90". - Settings.Default.HighConfidenceThreshold = 0.9; - var controller = CreateController(); + var store = CreateStore(modeEnabled: false, threshold: 0.9); + var controller = CreateController(store); // Act var result = controller.GetHighConfidenceThresholdText(); @@ -147,41 +150,45 @@ public void GetHighConfidenceThresholdText_ReturnsPercentageForm() public void SetHighConfidenceThresholdText_WithValidPercentage_PersistsProbability() { // Arrange - var controller = CreateController(); + var store = CreateStore(modeEnabled: false, threshold: 0.9); + var controller = CreateController(store); // Act controller.SetHighConfidenceThresholdText("75"); // Assert - Settings.Default.HighConfidenceThreshold.Should().Be(0.75); + store[nameof(Settings.HighConfidenceThreshold)].Should().Be(0.75); + store.SaveCount.Should().Be(1); } [TestMethod] public void SetHighConfidenceThresholdText_WithNonNumericInput_LeavesValueUnchanged() { // Arrange - Settings.Default.HighConfidenceThreshold = 0.9; - var controller = CreateController(); + var store = CreateStore(modeEnabled: false, threshold: 0.9); + var controller = CreateController(store); // Act controller.SetHighConfidenceThresholdText("not-a-number"); // Assert - Settings.Default.HighConfidenceThreshold.Should().Be(0.9); + store[nameof(Settings.HighConfidenceThreshold)].Should().Be(0.9); + store.SaveCount.Should().Be(0); } [TestMethod] public void SetHighConfidenceThresholdText_WithOutOfRangeInput_LeavesValueUnchanged() { // Arrange: 150% is out of the [0, 100] range. - Settings.Default.HighConfidenceThreshold = 0.9; - var controller = CreateController(); + var store = CreateStore(modeEnabled: false, threshold: 0.9); + var controller = CreateController(store); // Act controller.SetHighConfidenceThresholdText("150"); // Assert - Settings.Default.HighConfidenceThreshold.Should().Be(0.9); + store[nameof(Settings.HighConfidenceThreshold)].Should().Be(0.9); + store.SaveCount.Should().Be(0); } [TestMethod] diff --git a/TaskMaster.Test/TaskMaster.Test.csproj b/TaskMaster.Test/TaskMaster.Test.csproj index 93f311359..0e138e03f 100644 --- a/TaskMaster.Test/TaskMaster.Test.csproj +++ b/TaskMaster.Test/TaskMaster.Test.csproj @@ -323,6 +323,8 @@ + + diff --git a/TaskMaster/AppGlobals/AppAutoFileObjects.ScalarSettings.cs b/TaskMaster/AppGlobals/AppAutoFileObjects.ScalarSettings.cs new file mode 100644 index 000000000..c9d6970d1 --- /dev/null +++ b/TaskMaster/AppGlobals/AppAutoFileObjects.ScalarSettings.cs @@ -0,0 +1,88 @@ +using System; +using UtilitiesCS; + +namespace TaskMaster +{ + /// + /// Scalar auto-file settings of , read and persisted through an + /// injected per-instance . + /// + public partial class AppAutoFileObjects + { + private readonly ISettingsStore _settingsStore; + + /// + /// Initializes the auto-file objects with the scalar settings held in + /// . + /// + /// The owning application globals. + /// The store that holds the scalar auto-file settings. + /// is null. + internal AppAutoFileObjects(IApplicationGlobals parent, ISettingsStore settingsStore) + { + _settingsStore = + settingsStore ?? throw new ArgumentNullException(nameof(settingsStore)); + _parent = parent; + } + + public int LngConvCtPwr + { + get => (int)_settingsStore[nameof(Properties.Settings.ConversationExponent)]; + set + { + _settingsStore[nameof(Properties.Settings.ConversationExponent)] = value; + _settingsStore.Save(); + } + } + + public int Conversation_Weight + { + get => (int)_settingsStore[nameof(Properties.Settings.ConversationWeight)]; + set + { + _settingsStore[nameof(Properties.Settings.ConversationWeight)] = value; + _settingsStore.Save(); + } + } + + public int SmithWatterman_MatchScore + { + get => (int)_settingsStore[nameof(Properties.Settings.SmithWatterman_MatchScore)]; + set + { + _settingsStore[nameof(Properties.Settings.SmithWatterman_MatchScore)] = value; + _settingsStore.Save(); + } + } + + public int SmithWatterman_MismatchScore + { + get => (int)_settingsStore[nameof(Properties.Settings.SmithWatterman_MismatchScore)]; + set + { + _settingsStore[nameof(Properties.Settings.SmithWatterman_MismatchScore)] = value; + _settingsStore.Save(); + } + } + + public int SmithWatterman_GapPenalty + { + get => (int)_settingsStore[nameof(Properties.Settings.SmithWatterman_GapPenalty)]; + set + { + _settingsStore[nameof(Properties.Settings.SmithWatterman_GapPenalty)] = value; + _settingsStore.Save(); + } + } + + public int MaxRecents + { + get => (int)_settingsStore[nameof(Properties.Settings.MaxRecents)]; + set + { + _settingsStore[nameof(Properties.Settings.MaxRecents)] = value; + _settingsStore.Save(); + } + } + } +} diff --git a/TaskMaster/AppGlobals/AppAutoFileObjects.cs b/TaskMaster/AppGlobals/AppAutoFileObjects.cs index 45bdacfaa..1308141e0 100644 --- a/TaskMaster/AppGlobals/AppAutoFileObjects.cs +++ b/TaskMaster/AppGlobals/AppAutoFileObjects.cs @@ -36,9 +36,7 @@ public partial class AppAutoFileObjects : IAppAutoFileObjects ); public AppAutoFileObjects(IApplicationGlobals parent) - { - _parent = parent; - } + : this(parent, new ApplicationSettingsStore(Properties.Settings.Default)) { } private T Initialized(T obj, Func initializer) { @@ -108,72 +106,12 @@ public System.Action MaximizeQuickFileWindow set => _maximizeQuickFileWindow = value; } - public int LngConvCtPwr - { - get => _defaults.ConversationExponent; - set - { - _defaults.ConversationExponent = value; - _defaults.Save(); - } - } - - public int Conversation_Weight - { - get => _defaults.ConversationWeight; - set - { - _defaults.ConversationWeight = value; - _defaults.Save(); - } - } - public bool SuggestionFilesLoaded { get => _sugFilesLoaded; set => _sugFilesLoaded = value; } - public int SmithWatterman_MatchScore - { - get => _defaults.SmithWatterman_MatchScore; - set - { - _defaults.SmithWatterman_MatchScore = value; - _defaults.Save(); - } - } - - public int SmithWatterman_MismatchScore - { - get => _defaults.SmithWatterman_MismatchScore; - set - { - _defaults.SmithWatterman_MismatchScore = value; - _defaults.Save(); - } - } - - public int SmithWatterman_GapPenalty - { - get => _defaults.SmithWatterman_GapPenalty; - set - { - _defaults.SmithWatterman_GapPenalty = value; - _defaults.Save(); - } - } - - public int MaxRecents - { - get => _defaults.MaxRecents; - set - { - _defaults.MaxRecents = value; - _defaults.Save(); - } - } - private SloStack _movedMails; public SloStack MovedMails { diff --git a/TaskMaster/AppGlobals/AppQuickFilerSettings.cs b/TaskMaster/AppGlobals/AppQuickFilerSettings.cs index b15b08d46..8343dbca3 100644 --- a/TaskMaster/AppGlobals/AppQuickFilerSettings.cs +++ b/TaskMaster/AppGlobals/AppQuickFilerSettings.cs @@ -1,67 +1,87 @@ -using TaskMaster.Properties; +using System; +using TaskMaster.Properties; using UtilitiesCS; namespace TaskMaster { public class AppQuickFilerSettings : IAppQuickFilerSettings { + private readonly ISettingsStore _settingsStore; + + /// + /// Initializes settings that read and persist through . + /// + public AppQuickFilerSettings() + : this(new ApplicationSettingsStore(Settings.Default)) { } + + /// + /// Initializes settings that read and persist through . + /// + /// The store that holds the QuickFiler settings. + /// is null. + internal AppQuickFilerSettings(ISettingsStore settingsStore) + { + _settingsStore = + settingsStore ?? throw new ArgumentNullException(nameof(settingsStore)); + } + public bool MoveEntireConversation { - get => Settings.Default.MoveEntireConversations; + get => (bool)_settingsStore[nameof(Settings.MoveEntireConversations)]; internal set { - Settings.Default.MoveEntireConversations = value; - Settings.Default.Save(); + _settingsStore[nameof(Settings.MoveEntireConversations)] = value; + _settingsStore.Save(); } } public bool SaveAttachments { - get => Settings.Default.SaveAttachments; + get => (bool)_settingsStore[nameof(Settings.SaveAttachments)]; internal set { - Settings.Default.SaveAttachments = value; - Settings.Default.Save(); + _settingsStore[nameof(Settings.SaveAttachments)] = value; + _settingsStore.Save(); } } public bool SavePictures { - get => Settings.Default.SavePictures; + get => (bool)_settingsStore[nameof(Settings.SavePictures)]; internal set { - Settings.Default.SavePictures = value; - Settings.Default.Save(); + _settingsStore[nameof(Settings.SavePictures)] = value; + _settingsStore.Save(); } } public bool SaveEmailCopy { - get => Settings.Default.SaveEmailCopy; + get => (bool)_settingsStore[nameof(Settings.SaveEmailCopy)]; internal set { - Settings.Default.SaveEmailCopy = value; - Settings.Default.Save(); + _settingsStore[nameof(Settings.SaveEmailCopy)] = value; + _settingsStore.Save(); } } public bool HighConfidenceModeEnabled { - get => Settings.Default.HighConfidenceModeEnabled; + get => (bool)_settingsStore[nameof(Settings.HighConfidenceModeEnabled)]; internal set { - Settings.Default.HighConfidenceModeEnabled = value; - Settings.Default.Save(); + _settingsStore[nameof(Settings.HighConfidenceModeEnabled)] = value; + _settingsStore.Save(); } } public double HighConfidenceThreshold { - get => Settings.Default.HighConfidenceThreshold; + get => (double)_settingsStore[nameof(Settings.HighConfidenceThreshold)]; internal set { - Settings.Default.HighConfidenceThreshold = value; - Settings.Default.Save(); + _settingsStore[nameof(Settings.HighConfidenceThreshold)] = value; + _settingsStore.Save(); } } } diff --git a/TaskMaster/AppGlobals/AppStagingFilenames.cs b/TaskMaster/AppGlobals/AppStagingFilenames.cs index 98993bae5..a61ae2d81 100644 --- a/TaskMaster/AppGlobals/AppStagingFilenames.cs +++ b/TaskMaster/AppGlobals/AppStagingFilenames.cs @@ -1,69 +1,95 @@ -using TaskMaster.Properties; +using System; +using TaskMaster.Properties; using UtilitiesCS; namespace TaskMaster { public class AppStagingFilenames : IAppStagingFilenames { + private readonly ISettingsStore _settingsStore; + + /// + /// Initializes file names that read and persist through . + /// + public AppStagingFilenames() + : this(new ApplicationSettingsStore(Settings.Default)) { } + + /// + /// Initializes file names that read and persist through . + /// + /// The store that holds the staging file-name settings. + /// is null. + internal AppStagingFilenames(ISettingsStore settingsStore) + { + _settingsStore = + settingsStore ?? throw new ArgumentNullException(nameof(settingsStore)); + } + private string _conditionalReminders; public string ConditionalReminders { get => _conditionalReminders - ?? InitProp(ref _conditionalReminders, Settings.Default.File_ConditionalReminders); + ?? InitProp( + ref _conditionalReminders, + Read(nameof(Settings.File_ConditionalReminders)) + ); set { _conditionalReminders = value; - Settings.Default.File_ConditionalReminders = value; - Settings.Default.Save(); + _settingsStore[nameof(Settings.File_ConditionalReminders)] = value; + _settingsStore.Save(); } } private string _commonWords; public string CommonWords { - get => _commonWords ?? InitProp(ref _commonWords, Settings.Default.File_Common_Words); + get => + _commonWords + ?? InitProp(ref _commonWords, Read(nameof(Settings.File_Common_Words))); set { _commonWords = value; - Settings.Default.File_Common_Words = value; - Settings.Default.Save(); + _settingsStore[nameof(Settings.File_Common_Words)] = value; + _settingsStore.Save(); } } private string _subjectMap; public string SubjectMap { - get => _subjectMap ?? InitProp(ref _subjectMap, Settings.Default.File_Subject_Map); + get => + _subjectMap ?? InitProp(ref _subjectMap, Read(nameof(Settings.File_Subject_Map))); set { _subjectMap = value; - Settings.Default.File_Subject_Map = value; - Settings.Default.Save(); + _settingsStore[nameof(Settings.File_Subject_Map)] = value; + _settingsStore.Save(); } } private string _ctfInc; public string CtfInc { - get => _ctfInc ?? InitProp(ref _ctfInc, Settings.Default.File_CTF_Inc); + get => _ctfInc ?? InitProp(ref _ctfInc, Read(nameof(Settings.File_CTF_Inc))); set { _ctfInc = value; - Settings.Default.File_CTF_Inc = value; - Settings.Default.Save(); + _settingsStore[nameof(Settings.File_CTF_Inc)] = value; + _settingsStore.Save(); } } private string _ctfMap; public string CtfMap { - get => _ctfMap ?? InitProp(ref _ctfMap, Settings.Default.File_CTF_Map); + get => _ctfMap ?? InitProp(ref _ctfMap, Read(nameof(Settings.File_CTF_Map))); set { _ctfMap = value; - Settings.Default.File_CTF_Map = value; - Settings.Default.Save(); + _settingsStore[nameof(Settings.File_CTF_Map)] = value; + _settingsStore.Save(); } } @@ -72,12 +98,12 @@ public string EmailSessionTemp { get => _emailSessionTemp - ?? InitProp(ref _emailSessionTemp, Settings.Default.FileName_EmailSessionTmp); + ?? InitProp(ref _emailSessionTemp, Read(nameof(Settings.FileName_EmailSessionTmp))); set { _emailSessionTemp = value; - Settings.Default.FileName_EmailSessionTmp = value; - Settings.Default.Save(); + _settingsStore[nameof(Settings.FileName_EmailSessionTmp)] = value; + _settingsStore.Save(); } } @@ -86,12 +112,12 @@ public string EmailSession { get => _emailSession - ?? InitProp(ref _emailSession, Settings.Default.FileName_EmailSession); + ?? InitProp(ref _emailSession, Read(nameof(Settings.FileName_EmailSession))); set { _emailSession = value; - Settings.Default.FileName_EmailSession = value; - Settings.Default.Save(); + _settingsStore[nameof(Settings.FileName_EmailSession)] = value; + _settingsStore.Save(); } } @@ -100,24 +126,25 @@ public string MovedMails { get => _movedMails - ?? InitProp(ref _movedMails, Settings.Default.FileName_MovedEmailsBackup); + ?? InitProp(ref _movedMails, Read(nameof(Settings.FileName_MovedEmailsBackup))); set { _movedMails = value; - Settings.Default.FileName_MovedEmailsBackup = value; - Settings.Default.Save(); + _settingsStore[nameof(Settings.FileName_MovedEmailsBackup)] = value; + _settingsStore.Save(); } } private string _recentsFile; public string RecentsFile { - get => _recentsFile ?? InitProp(ref _recentsFile, Settings.Default.FileName_Recents); + get => + _recentsFile ?? InitProp(ref _recentsFile, Read(nameof(Settings.FileName_Recents))); set { _recentsFile = value; - Settings.Default.FileName_Recents = value; - Settings.Default.Save(); + _settingsStore[nameof(Settings.FileName_Recents)] = value; + _settingsStore.Save(); } } @@ -126,7 +153,10 @@ public string EmailInfoStagingFile { get => _emailInfoStagingFile - ?? InitProp(ref _emailInfoStagingFile, Settings.Default.FileName_EmailInfoStaging); + ?? InitProp( + ref _emailInfoStagingFile, + Read(nameof(Settings.FileName_EmailInfoStaging)) + ); set => _emailInfoStagingFile = value; } @@ -135,5 +165,7 @@ internal string InitProp(ref string prop, string value) prop = value; return value; } + + private string Read(string settingName) => (string)_settingsStore[settingName]; } } diff --git a/TaskMaster/TaskMaster.csproj b/TaskMaster/TaskMaster.csproj index d7a235196..a9da283dc 100644 --- a/TaskMaster/TaskMaster.csproj +++ b/TaskMaster/TaskMaster.csproj @@ -408,6 +408,7 @@ + diff --git a/TaskTree.Test/TaskTreeControllerMoveLogicTests.cs b/TaskTree.Test/TaskTreeControllerMoveLogicTests.cs index a7cf04408..407bffc2d 100644 --- a/TaskTree.Test/TaskTreeControllerMoveLogicTests.cs +++ b/TaskTree.Test/TaskTreeControllerMoveLogicTests.cs @@ -1,5 +1,6 @@ using System.Collections; using System.Collections.Generic; +using System.Configuration; using System.Reflection; using BrightIdeasSoftware; using FluentAssertions; @@ -32,12 +33,17 @@ private static void Attach(TreeNode parent, TreeNode child) private static TaskTreeController MakeController( TreeOfToDoItems model, List messages = null, - IDList idList = null + IDList idList = null, + Mock settingsStore = null ) { var viewer = new Mock(); var mockTD = new Mock(); - mockTD.Setup(x => x.IDList).Returns(idList ?? new IDList()); + // A per-call mock store keeps IDList.GetNextToDoID off the per-host settings file. + var store = settingsStore ?? new Mock(); + mockTD + .Setup(x => x.IDList) + .Returns(idList ?? new IDList(new List(), store.Object)); var globals = new Mock(); globals.Setup(x => x.TD).Returns(mockTD.Object); System.Action seam = @@ -45,6 +51,53 @@ private static TaskTreeController MakeController( return new TaskTreeController(globals.Object, viewer.Object, model, seam); } + /// + /// Runs and counts the saves that + /// ToDoModel.Properties.Settings.Default raises on the calling thread. The settings + /// class is internal to ToDoModel, so it is resolved by reflection. Each counted save is + /// cancelled so nothing is persisted, saves raised by other threads are ignored, and the + /// handler is always removed. + /// + private static int CountToDoModelSettingsSavesOnCurrentThread(System.Action act) + { + var settingsType = typeof(IDList).Assembly.GetType("ToDoModel.Properties.Settings"); + var settings = (ApplicationSettingsBase) + settingsType + .GetProperty("Default", BindingFlags.Public | BindingFlags.Static) + .GetValue(null); + var threadId = System.Environment.CurrentManagedThreadId; + var count = 0; + SettingsSavingEventHandler handler = (sender, e) => + { + if (System.Environment.CurrentManagedThreadId == threadId) + { + System.Threading.Interlocked.Increment(ref count); + e.Cancel = true; + } + }; + + // SettingsSaving add and remove are not atomic; lock only the subscription statements + // so concurrent probes cannot lose a subscription while the action runs unserialized. + lock (settings) + { + settings.SettingsSaving += handler; + } + + try + { + act(); + } + finally + { + lock (settings) + { + settings.SettingsSaving -= handler; + } + } + + return System.Threading.Volatile.Read(ref count); + } + private static ModelDropEventArgs DropArgs( object target, IList sources, @@ -233,19 +286,29 @@ public void MoveObjectsToChildren_DesyncedRoot_FiresMessageSeam() var messages = new List(); var model = new TreeOfToDoItems(new List> { target }); var source = new Mock(); - var controller = MakeController(model, messages); + var settingsStore = new Mock(); + var controller = MakeController(model, messages, settingsStore: settingsStore); // Act - controller.MoveObjectsToChildren( - Mock.Of(), - source.Object, - target, - new ArrayList { orphan } + var saves = CountToDoModelSettingsSavesOnCurrentThread(() => + controller.MoveObjectsToChildren( + Mock.Of(), + source.Object, + target, + new ArrayList { orphan } + ) ); // Assert messages.Should().ContainSingle().Which.Should().Contain("out of sync"); source.Verify(t => t.RemoveObject(orphan), Times.Once); + saves + .Should() + .Be( + 0, + "the test persisted settings to the shared per-host settings file through IDList" + ); + settingsStore.Verify(s => s.Save(), Times.Once()); } [TestMethod] diff --git a/TaskTree.Test/TaskTreeControllerRouteDropTests.cs b/TaskTree.Test/TaskTreeControllerRouteDropTests.cs index 9fb916e70..424594887 100644 --- a/TaskTree.Test/TaskTreeControllerRouteDropTests.cs +++ b/TaskTree.Test/TaskTreeControllerRouteDropTests.cs @@ -37,7 +37,11 @@ private static TaskTreeController MakeController( { var v = viewer ?? new Mock(); var mockTD = new Mock(); - mockTD.Setup(x => x.IDList).Returns(new IDList()); + // A per-call mock store keeps IDList.GetNextToDoID off the per-host settings file. + var settingsStore = new Mock(); + mockTD + .Setup(x => x.IDList) + .Returns(new IDList(new List(), settingsStore.Object)); var globals = new Mock(); globals.Setup(x => x.TD).Returns(mockTD.Object); System.Action seam = diff --git a/ToDoModel.Test/Data Model/ID/IDListGetNextToDoIDTests.cs b/ToDoModel.Test/Data Model/ID/IDListGetNextToDoIDTests.cs index 6344f1f4e..704cf4a9f 100644 --- a/ToDoModel.Test/Data Model/ID/IDListGetNextToDoIDTests.cs +++ b/ToDoModel.Test/Data Model/ID/IDListGetNextToDoIDTests.cs @@ -1,28 +1,32 @@ using System; using System.Collections.Generic; +using System.Reflection; using FluentAssertions; using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; using ToDoModel; +using UtilitiesCS; namespace ToDoModel.Test { /// - /// Unit tests for . The list is constructed only via - /// the Outlook-free constructors (, , - /// ), so no live Outlook is required. Only the pure - /// base-36 arithmetic path is exercised. Tests that call the successful path initialize the - /// list's cached maximum ID length before generating an ID so the method does not persist - /// Settings.Default.MaxLengthOfID. No Filepath is set, so the internal - /// Serialize() branch is not taken. + /// Unit tests for and the settings-store seam it + /// persists through. Every list under test is built with an injected per-test + /// , so MaxLengthOfID persistence is observed on the + /// mock and never reaches the per-host settings file. No live Outlook is required, and no + /// Filepath is set, so the internal Serialize() branch is not taken. /// [TestClass] public class IDListGetNextToDoIDTests { + private const string MaxLengthKey = "MaxLengthOfID"; + [TestMethod] public void GetNextToDoID_NoCollision_ReturnsNextBase36ValueAndAddsIt() { // Arrange: seed "0001" => base10 1; next id is 2 => base36 padded "02". - var list = CreateListWithCachedMaxLength("ZZZZ"); + var store = new Mock(); + var list = new IDList(new List { "ZZZZ" }, store.Object); // Act var result = list.GetNextToDoID("0001"); @@ -36,7 +40,8 @@ public void GetNextToDoID_NoCollision_ReturnsNextBase36ValueAndAddsIt() public void GetNextToDoID_IdAlreadyPresent_AdvancesPastTheCollision() { // Arrange: seed "0001" => next candidate "02" collides; loop advances to "03". - var list = CreateListWithCachedMaxLength("02"); + var store = new Mock(); + var list = new IDList(new List { "02" }, store.Object); // Act var result = list.GetNextToDoID("0001"); @@ -50,7 +55,8 @@ public void GetNextToDoID_IdAlreadyPresent_AdvancesPastTheCollision() public void GetNextToDoID_MultipleConsecutiveCollisions_AdvancesToFirstFreeValue() { // Arrange: candidates "02","03","04" all present; first free is "05". - var list = CreateListWithCachedMaxLength("02", "03", "04"); + var store = new Mock(); + var list = new IDList(new List { "02", "03", "04" }, store.Object); // Act var result = list.GetNextToDoID("0001"); @@ -62,10 +68,10 @@ public void GetNextToDoID_MultipleConsecutiveCollisions_AdvancesToFirstFreeValue [TestMethod] public void GetNextToDoID_LengthBoundaryRollover_ProducesLongerId() { - // Arrange: seed "ZZ" => base10 (35*36 + 35) = 1295; next is 1296 => base36 "100" - // (three digits, padded to even length is still "100" because 3 % 2 == 1 prepends "0" - // => "0100"). Assert the produced value and its length to cover the rollover branch. - var list = CreateListWithCachedMaxLength("0000"); + // Arrange: seed "ZZ" => base10 (35*36 + 35) = 1295; next is 1296 => base36 "100", + // padded to even length "0100". + var store = new Mock(); + var list = new IDList(new List { "0000" }, store.Object); // Act var result = list.GetNextToDoID("ZZ"); @@ -81,7 +87,8 @@ public void GetNextToDoID_LengthBoundaryRollover_ProducesLongerId() public void GetNextToDoID_NullSeed_ThrowsArgumentException() { // Arrange - var list = new IDList(new List()); + var store = new Mock(); + var list = new IDList(new List(), store.Object); // Act Action act = () => list.GetNextToDoID(null); @@ -91,13 +98,15 @@ public void GetNextToDoID_NullSeed_ThrowsArgumentException() .Throw( "GetNextToDoID guards against a null seed via ThrowIfNullOrEmpty" ); + store.Verify(s => s.Save(), Times.Never()); } [TestMethod] public void GetNextToDoID_EmptySeed_ThrowsArgumentException() { // Arrange - var list = new IDList(new List()); + var store = new Mock(); + var list = new IDList(new List(), store.Object); // Act Action act = () => list.GetNextToDoID(string.Empty); @@ -107,14 +116,172 @@ public void GetNextToDoID_EmptySeed_ThrowsArgumentException() .Throw( "GetNextToDoID guards against an empty seed via ThrowIfNullOrEmpty" ); + store.Verify(s => s.Save(), Times.Never()); + } + + [TestMethod] + public void GetNextToDoID_FirstIdOnFreshList_AssignsLengthAndSavesOnce() + { + // Arrange: a fresh list caches a maximum length of zero, so its first id persists. + var store = new Mock(); + var list = new IDList(new List(), store.Object); + + // Act + var result = list.GetNextToDoID("0001"); + + // Assert + result.Should().Be("02"); + store.VerifySet(s => s[MaxLengthKey] = 2, Times.Once()); + store.Verify(s => s.Save(), Times.Once()); + } + + [TestMethod] + public void GetNextToDoID_IdNotLongerThanCachedLength_DoesNotSaveAgain() + { + // Arrange: the first id "0100" caches length 4 and saves once. + var store = new Mock(); + var list = new IDList(new List(), store.Object); + list.GetNextToDoID("ZZ").Should().Be("0100"); + + // Act: the next id "02" is shorter than the cached length. + var result = list.GetNextToDoID("0001"); + + // Assert + result.Should().Be("02"); + store.VerifySet(s => s[MaxLengthKey] = 2, Times.Never()); + store.Verify(s => s.Save(), Times.Once(), "a shorter id does not save"); + } + + [TestMethod] + public void GetNextToDoID_LongerIdAfterFirst_SavesAgain() + { + // Arrange: the first id "02" caches length 2 and saves once. + var store = new Mock(); + var list = new IDList(new List(), store.Object); + list.GetNextToDoID("0001").Should().Be("02"); + + // Act: the next id "0100" is longer than the cached length. + var result = list.GetNextToDoID("ZZ"); + + // Assert + result.Should().Be("0100"); + store.VerifySet(s => s[MaxLengthKey] = 4, Times.Once()); + store.Verify(s => s.Save(), Times.Exactly(2), "a longer id saves again"); + } + + [TestMethod] + public void MaxLengthOfID_ColdCacheOnMixedLengthList_ReturnsLongestIdLength() + { + // Arrange: a list built from ids never calls GetNextToDoID, so the cache is zero. + var store = new Mock(); + var list = new IDList(new List { "02", "0100" }, store.Object); + + // Act + var result = list.MaxLengthOfID; + + // Assert + result.Should().Be(4, "the longest stored id is \"0100\""); + } + + [TestMethod] + public void MaxLengthOfID_ColdCacheOnSecondDistinctList_ReturnsItsOwnLongestIdLength() + { + // Arrange: a second list evaluates the same Max lambda independently. + var first = new IDList( + new List { "02", "0100" }, + new Mock().Object + ); + var second = new IDList( + new List { "02", "03", "04" }, + new Mock().Object + ); + + // Act + var firstResult = first.MaxLengthOfID; + var secondResult = second.MaxLengthOfID; + + // Assert + firstResult.Should().Be(4); + secondResult.Should().Be(2, "every id in the second list has length 2"); + } + + [TestMethod] + public void MaxLengthOfID_WarmCacheAfterGetNextToDoID_ReturnsCachedLengthWithoutSaving() + { + // Arrange: GetNextToDoID("ZZ") yields "0100" and caches length 4 (one save). + var store = new Mock(); + var list = new IDList(new List { "02" }, store.Object); + list.GetNextToDoID("ZZ").Should().Be("0100"); + + // Act + var result = list.MaxLengthOfID; + + // Assert + result.Should().Be(4, "the warm cache holds the length of the produced id"); + store.Verify(s => s.Save(), Times.Once(), "reading the length does not persist"); + } + + [TestMethod] + public void Constructor_EnumerableWithNullStore_ThrowsArgumentNullException() + { + // Arrange + Action act = () => new IDList(new List(), null); + + // Act / Assert + act.Should() + .Throw() + .Which.ParamName.Should() + .Be("settingsStore"); + } + + [TestMethod] + public void Constructor_NullStoreOnly_ThrowsArgumentNullException() + { + // Arrange + Action act = () => new IDList((ISettingsStore)null); + + // Act / Assert + act.Should() + .Throw() + .Which.ParamName.Should() + .Be("settingsStore"); + } + + [TestMethod] + public void Constructor_StoreOnly_SavesThroughInjectedStore() + { + // Arrange + var store = new Mock(); + var list = new IDList(store.Object); + + // Act + var result = list.GetNextToDoID("0001"); + + // Assert + result.Should().Be("02"); + list.Should().ContainSingle().Which.Should().Be("02"); + store.VerifySet(s => s[MaxLengthKey] = 2, Times.Once()); + store.Verify(s => s.Save(), Times.Once()); } - private static IDList CreateListWithCachedMaxLength(params string[] ids) + [TestMethod] + public void Constructor_Default_UsesApplicationSettingsStore() { - var list = new IDList(new List(ids)); - _ = list.MaxLengthOfID; + // Arrange + var field = typeof(IDList).GetField( + "_settingsStore", + BindingFlags.Instance | BindingFlags.NonPublic + ); + + // Act: read the default store without reading or writing any setting. + var store = field.GetValue(new IDList()); - return list; + // Assert + store + .Should() + .BeOfType( + "existing constructors persist through ToDoModel's own settings" + ); } } } diff --git a/ToDoModel/Data Model/ID/IDList.cs b/ToDoModel/Data Model/ID/IDList.cs index 3c5d9bcdd..747772098 100644 --- a/ToDoModel/Data Model/ID/IDList.cs +++ b/ToDoModel/Data Model/ID/IDList.cs @@ -62,8 +62,42 @@ Outlook.Application olApp _olApp = olApp; } + /// + /// Initializes a list from that persists its maximum ID length + /// through instead of the default per-host settings. + /// + /// The initial IDs. + /// The store that receives MaxLengthOfID updates. + /// is null. + public IDList(IEnumerable ids, ISettingsStore settingsStore) + : base(ids) + { + _settingsStore = + settingsStore ?? throw new ArgumentNullException(nameof(settingsStore)); + } + + /// + /// Initializes an empty list that persists its maximum ID length through + /// instead of the default per-host settings. + /// + /// The store that receives MaxLengthOfID updates. + /// is null. + public IDList(ISettingsStore settingsStore) + : base() + { + _settingsStore = + settingsStore ?? throw new ArgumentNullException(nameof(settingsStore)); + } + #endregion + // A field initializer, so every existing constructor (including the deserializing and + // Outlook-bound ones) persists through ToDoModel's own settings exactly as before; the + // store-taking constructors replace it so tests never write the per-host settings file. + private readonly ISettingsStore _settingsStore = new ApplicationSettingsStore( + Properties.Settings.Default + ); + private Outlook.Application _olApp; private int _maxIDLength = 0; @@ -101,8 +135,8 @@ public string GetNextToDoID(string strSeed) if (strMaxID.Length > _maxIDLength) { _maxIDLength = strMaxID.Length; - Properties.Settings.Default.MaxLengthOfID = (int)_maxIDLength; - Properties.Settings.Default.Save(); + _settingsStore[nameof(Properties.Settings.MaxLengthOfID)] = _maxIDLength; + _settingsStore.Save(); } if (this.Filepath is not null) { diff --git a/UtilitiesCS.Test/Extensions/DfDeedle_COM_Tests.QfcColumnsAsync.cs b/UtilitiesCS.Test/Extensions/DfDeedle_COM_Tests.QfcColumnsAsync.cs new file mode 100644 index 000000000..a785105cf --- /dev/null +++ b/UtilitiesCS.Test/Extensions/DfDeedle_COM_Tests.QfcColumnsAsync.cs @@ -0,0 +1,70 @@ +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Extensions.Time.Testing; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace UtilitiesCS.Test.Extensions +{ + /// + /// AddQfcColumnsAsync tests. Each call passes a + /// that is never advanced through the existing + /// timeProvider parameter, so no column-add deadline can fire and the outcome depends + /// only on the column-add work, never on wall-clock time or scheduler load. The + /// columnAdder parameter is left at its production default so the real + /// AddQfcColumns runs against the mocked COM table. + /// + public partial class DfDeedle_COM_Tests + { + [TestMethod] + public async Task AddQfcColumnsAsync_HappyPath_CompletesWithoutThrowing() + { + // Arrange: table + folder arranged so AddQfcColumns succeeds; the fake clock never + // advances, so none of the three column-add deadlines can expire. + var (mockTable, mockColumns) = BuildTableMock(); + var folder = BuildFolderWithUdp("Triage"); + using var cts = new CancellationTokenSource(); + + // Act: call the internal method directly and await it. Reflection is not usable here: + // it does not apply C# default parameter values without Type.Missing plus + // BindingFlags.OptionalParamBinding. + var act = async () => + await DfDeedle.AddQfcColumnsAsync( + mockTable.Object, + folder.Object, + cts.Token, + 0, + timeProvider: new FakeTimeProvider() + ); + + // Assert: completes without exception and the production adder added the column. + await act.Should().NotThrowAsync(); + mockColumns.Verify(c => c.Add("SentOn"), Times.Once); + } + + [TestMethod] + public async Task AddQfcColumnsAsync_PreCancelledToken_CompletesGracefully() + { + // Arrange: a pre-cancelled token causes the inner Task.Run to be cancelled + // immediately; the method's catch block must handle it without re-throwing. + var (mockTable, _) = BuildTableMock(); + var folder = BuildFolderWithUdp("Triage"); + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + // Act + var act = async () => + await DfDeedle.AddQfcColumnsAsync( + mockTable.Object, + folder.Object, + cts.Token, + 0, + timeProvider: new FakeTimeProvider() + ); + + // Assert: must not propagate the cancellation as an unhandled exception. + await act.Should().NotThrowAsync(); + } + } +} diff --git a/UtilitiesCS.Test/Extensions/DfDeedle_COM_Tests.cs b/UtilitiesCS.Test/Extensions/DfDeedle_COM_Tests.cs index d7e6fa1cd..f736a2d0e 100644 --- a/UtilitiesCS.Test/Extensions/DfDeedle_COM_Tests.cs +++ b/UtilitiesCS.Test/Extensions/DfDeedle_COM_Tests.cs @@ -35,7 +35,7 @@ namespace UtilitiesCS.Test.Extensions /// restores it in a finally block, and it has no reader on any parallel-phase path. /// [TestClass] - public class DfDeedle_COM_Tests + public partial class DfDeedle_COM_Tests { // ---------------------------------------------------------------- // Shared helpers @@ -485,46 +485,7 @@ public async Task GetEmailDataInViewAsync_SeparatesTableSnapshotFromDataFrameTra mockTable.Verify(x => x.GetNextRow(), Times.Once); } - // ---------------------------------------------------------------- - // AddQfcColumnsAsync (internal, called directly) - // ---------------------------------------------------------------- - - [TestMethod] - public void AddQfcColumnsAsync_HappyPath_CompletesWithoutThrowing() - { - // Arrange: table + folder arranged so AddQfcColumns succeeds. - var (mockTable, _) = BuildTableMock(); - var folder = BuildFolderWithUdp("Triage"); - var cts = new CancellationTokenSource(); - - // Act: call the internal method directly and await the returned Task. Reflection is - // not usable here: it does not apply C# default parameter values without Type.Missing - // plus BindingFlags.OptionalParamBinding, so a four-element argument array against the - // widened six-parameter method throws TargetParameterCountException. - var task = DfDeedle.AddQfcColumnsAsync(mockTable.Object, folder.Object, cts.Token, 0); - System.Action act = () => task.GetAwaiter().GetResult(); - - // Assert: completes without exception. - act.Should().NotThrow(); - } - - [TestMethod] - public void AddQfcColumnsAsync_PreCancelledToken_CompletesGracefully() - { - // Arrange: a pre-cancelled token causes the inner Task.Run to be cancelled - // immediately; the method's catch block must handle it without re-throwing. - var (mockTable, _) = BuildTableMock(); - var folder = BuildFolderWithUdp("Triage"); - var cts = new CancellationTokenSource(); - cts.Cancel(); - - // Act - var task = DfDeedle.AddQfcColumnsAsync(mockTable.Object, folder.Object, cts.Token, 0); - - // Assert: must not propagate the cancellation as an unhandled exception. - System.Action act = () => task.GetAwaiter().GetResult(); - act.Should().NotThrow(); - } + // AddQfcColumnsAsync tests live in DfDeedle_COM_Tests.QfcColumnsAsync.cs. // ---------------------------------------------------------------- // FromDefaultFolder(Store) — null-table branch diff --git a/UtilitiesCS.Test/HelperClasses/Settings/ApplicationSettingsStoreTests.cs b/UtilitiesCS.Test/HelperClasses/Settings/ApplicationSettingsStoreTests.cs new file mode 100644 index 000000000..95abf15cf --- /dev/null +++ b/UtilitiesCS.Test/HelperClasses/Settings/ApplicationSettingsStoreTests.cs @@ -0,0 +1,99 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Configuration; +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace UtilitiesCS.Test.HelperClasses +{ + /// + /// Unit tests for . The wrapped settings instance is a + /// whose indexer and Save overrides never call + /// the base members, so no settings provider runs and no file is read or written. + /// + [TestClass] + public class ApplicationSettingsStoreTests + { + [TestMethod] + public void Constructor_NullSettings_ThrowsArgumentNullException() + { + // Arrange + Action act = () => _ = new ApplicationSettingsStore(null!); + + // Act / Assert + act.Should() + .Throw("a store without settings has nothing to forward to") + .Which.ParamName.Should() + .Be("settings"); + } + + [TestMethod] + public void Indexer_Get_ReturnsWrappedSettingsValue() + { + // Arrange + var settings = new RecordingApplicationSettings(); + settings["MaxLengthOfID"] = 4; + var store = new ApplicationSettingsStore(settings); + + // Act + var value = store["MaxLengthOfID"]; + + // Assert + value.Should().Be(4, "the getter forwards to the wrapped settings indexer"); + } + + [TestMethod] + public void Indexer_Set_WritesWrappedSettingsValue() + { + // Arrange + var settings = new RecordingApplicationSettings(); + var store = new ApplicationSettingsStore(settings); + + // Act + store["HighConfidenceThreshold"] = 0.75; + + // Assert + settings["HighConfidenceThreshold"] + .Should() + .Be(0.75, "the setter forwards to the wrapped settings indexer"); + settings.SaveCount.Should().Be(0, "assigning a value does not persist it"); + } + + [TestMethod] + public void Save_Invoked_ForwardsToWrappedSettingsOnce() + { + // Arrange + var settings = new RecordingApplicationSettings(); + var store = new ApplicationSettingsStore(settings); + + // Act + store.Save(); + + // Assert + settings + .SaveCount.Should() + .Be(1, "Save forwards exactly one call to the wrapped settings"); + } + + /// + /// Provider-free settings double: the indexer is dictionary-backed and Save only counts + /// calls, so neither member reaches persistence. + /// + private sealed class RecordingApplicationSettings : ApplicationSettingsBase + { + private readonly Dictionary _values = new(); + + public int SaveCount { get; private set; } + + public override object this[string propertyName] + { + get => _values[propertyName]; + set => _values[propertyName] = value; + } + + public override void Save() => SaveCount++; + } + } +} diff --git a/UtilitiesCS.Test/UtilitiesCS.Test.csproj b/UtilitiesCS.Test/UtilitiesCS.Test.csproj index 888312369..711cee202 100644 --- a/UtilitiesCS.Test/UtilitiesCS.Test.csproj +++ b/UtilitiesCS.Test/UtilitiesCS.Test.csproj @@ -199,6 +199,7 @@ + @@ -231,6 +232,7 @@ + diff --git a/UtilitiesCS/HelperClasses/Settings/ApplicationSettingsStore.cs b/UtilitiesCS/HelperClasses/Settings/ApplicationSettingsStore.cs new file mode 100644 index 000000000..ac48d20cf --- /dev/null +++ b/UtilitiesCS/HelperClasses/Settings/ApplicationSettingsStore.cs @@ -0,0 +1,38 @@ +#nullable enable + +using System; +using System.Configuration; + +namespace UtilitiesCS +{ + /// + /// Production that forwards every read, write, and save to a wrapped + /// instance, such as an assembly's + /// Properties.Settings.Default. Generated settings properties are implemented as indexer + /// reads and writes, so routing through this adapter persists the same values as before. + /// + public sealed class ApplicationSettingsStore : ISettingsStore + { + private readonly ApplicationSettingsBase _settings; + + /// + /// Initializes a new instance of the class. + /// + /// The settings instance to wrap. + /// is null. + public ApplicationSettingsStore(ApplicationSettingsBase settings) + { + _settings = settings ?? throw new ArgumentNullException(nameof(settings)); + } + + /// + public object this[string propertyName] + { + get => _settings[propertyName]; + set => _settings[propertyName] = value; + } + + /// + public void Save() => _settings.Save(); + } +} diff --git a/UtilitiesCS/HelperClasses/Settings/ISettingsStore.cs b/UtilitiesCS/HelperClasses/Settings/ISettingsStore.cs new file mode 100644 index 000000000..3aafb42ed --- /dev/null +++ b/UtilitiesCS/HelperClasses/Settings/ISettingsStore.cs @@ -0,0 +1,25 @@ +#nullable enable + +namespace UtilitiesCS +{ + /// + /// Narrow persistence seam over an application settings collection. Consumers read and write + /// named settings through the indexer and persist pending values with , so a + /// test can supply an in-memory store instead of the per-host settings file. + /// + public interface ISettingsStore + { + /// + /// Gets or sets the value of the named setting. + /// + /// The setting name, as declared by the settings class. + /// The current value of the setting. + object this[string propertyName] { get; set; } + + /// + /// Persists every pending setting value, mirroring + /// . + /// + void Save(); + } +} diff --git a/UtilitiesCS/UtilitiesCS.csproj b/UtilitiesCS/UtilitiesCS.csproj index a5d6cb9da..f6134833c 100644 --- a/UtilitiesCS/UtilitiesCS.csproj +++ b/UtilitiesCS/UtilitiesCS.csproj @@ -751,6 +751,8 @@ + + diff --git a/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/code-review.2026-10-22T17-10.md b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/code-review.2026-10-22T17-10.md new file mode 100644 index 000000000..42134a769 --- /dev/null +++ b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/code-review.2026-10-22T17-10.md @@ -0,0 +1,64 @@ +# Code Review: Parallel test failures from wall-clock deadlines and shared user.config (Issue #1005) + +- Branch: `bug/parallel-test-failures-wall-clock-and-shared-user-config-1005` +- Head: `e399de597`; merge base `346651242a2bd6ed25d703a4b1ecb5a055c6ae72` (`main`) +- Review timestamp: 2026-10-22T17-10 +- Reviewer: feature-review agent +- Scope: the full branch diff `346651242..e399de597` (73 files: 7 production C#, 13 test C#, 4 project files, feature documents and evidence, 2 promotion records, 7 agent-memory files) + +## Executive Summary + +The design is narrow and consistent with the maintainer's objective that every test can run in parallel. A two-member `ISettingsStore` interface and a sealed `ApplicationSettingsStore` adapter give each settings consumer a per-instance persistence seam. Production defaults wrap the consumer assembly's own `Properties.Settings.Default`, so the indexer write plus `Save()` is behaviourally identical to the generated property setters it replaces. No static mutable seam, process-wide `SettingsProvider`, `user.config` redirection, temporary file, serialization attribute, sleep or retry is introduced. The DfDeedle fix is test-only and uses the existing `timeProvider` parameter with a never-advanced `FakeTimeProvider`. + +One blocking finding: removing the cache-priming helper in `IDListGetNextToDoIDTests` (required by AC2) also removed the only test that executed `IDList.MaxLengthOfID`, so that public member lost all coverage (7 lines, 4 branches) and the committed coverage comparison does not report it. The remaining observations are minor and non-blocking. + +Verdict: **REMEDIATION_REQUIRED (1 blocking, 0 major, 6 minor or informational)**. + +## Findings Table + +| Severity | File | Location | Finding | Recommendation | Rationale | Evidence | +|---|---|---|---|---|---|---| +| Blocking | `ToDoModel.Test/Data Model/ID/IDListGetNextToDoIDTests.cs`; `ToDoModel/Data Model/ID/IDList.cs` | `IDList.MaxLengthOfID` getter (lines 104 to 114); deleted helper `CreateListWithCachedMaxLength` | B-1: the deleted helper's `_ = list.MaxLengthOfID;` was the only test execution of the getter. Baseline Cobertura: 7/7 lines, 4/4 branches covered; head: 0/7 and 0/4. `IDList.cs` branch rate fell from 20.69% to 19.35%. `coverage-comparison.2026-10-22T16-36.md` reports only line rates for the file and attributes the uncovered residue to the unchanged constructors. | Add a test (or two) in `IDListGetNextToDoIDTests.cs` that asserts `MaxLengthOfID` on a non-empty store-injected list: a cold cache returns the longest ID length, and after `GetNextToDoID` grows the cache the getter returns the cached value without recomputing. Update the coverage comparison to report `IDList.cs` branch figures and the getter. | Coverage is a supporting gate; a modified file below the floor must not regress, and a public `IIDList` member should not become untested as a side effect of a test refactor. | Baseline document `` lines 73 to 79 `hits="1"`; `artifacts/csharp/coverage.xml` lines 107 to 113 `hits="0"`; Grep of `*.Test` for `MaxLengthOfID` finds no reader. | +| Minor | `TaskMaster/AppGlobals/AppAutoFileObjects.cs` | file-level coverage | N-1: per-file line rate 45.66% to 40.81% because 33 covered scalar-property lines moved into `AppAutoFileObjects.ScalarSettings.cs`; branch rate unchanged at 37.10%. | None for this item. The class across both files rises to 46.06% (187/406). | Movement between partial files of one class is not a coverage loss. | Cobertura class rows for both files at baseline and head. | +| Informational | `TaskTree.Test/TaskTreeControllerMoveLogicTests.cs`; `TaskMaster.Test/AppGlobals/SettingsSaveProbe.cs` | `CountToDoModelSettingsSavesOnCurrentThread`; `CountSavesOnCurrentThread` | O-4: the thread-attributed save probe is implemented twice, once per test assembly. | Accept; consider a shared test-utility project only if a third copy is needed. | The test assemblies share no helper project, and the TaskTree copy must resolve the internal ToDoModel settings type by reflection. | Diff of both files. | +| Informational | `SettingsSaveProbe.cs`; `TaskTreeControllerMoveLogicTests.cs` | `lock (settings)` around subscribe and unsubscribe | O-5: the probe locks on the shared `Settings.Default` instance, which a synchronized `SettingsBase` also uses as its own monitor. | Accept. The lock covers only the event add and remove statements, not the observed action, so it cannot serialize the code under test or deadlock with a production save. | The `SettingsSaving` accessors are not atomic; the lock prevents two concurrent probes from losing a subscription. | Code comments in both probes. | +| Informational | `IDListGetNextToDoIDTests.cs`, `AppQuickFilerSettingsTests.cs`, `AppStagingFilenamesTests.cs`, `AppAutoFileObjectsCoverageExpansionTests.cs` | `Constructor_*_UsesApplicationSettingsStore` | O-6: the default-constructor tests read the private field `_settingsStore` by reflection, so a field rename breaks them at run time rather than compile time. | Accept; the field name is stable across all four consumers and the tests fail loudly (null `FieldInfo`). | No public accessor should be added only for tests. | Diff of the four test files. | +| Informational | `ToDoModel/Data Model/ID/IDList.cs` | field initializer `_settingsStore = new ApplicationSettingsStore(Properties.Settings.Default)` | O-7: the store-taking constructors overwrite a default adapter that the field initializer has already allocated, which also touches the `Settings.Default` singleton. | Accept. The allocation is trivial, reading `Settings.Default` performs no I/O, and the initializer guarantees every existing constructor (including the deserializing ones) keeps the production default. | Matches spec D3. | `IDList.cs` lines 94 to 99. | +| Informational | `ToDoModel/Data Model/ID/IDList.cs` | `MaxLengthOfID` getter | O-8: on an empty list with a zero cache the getter calls `Max()` on an empty sequence and throws `InvalidOperationException`. Pre-existing, unchanged by this branch, and no production code reads the property. | Out of scope. When adding the B-1 test, use a non-empty list. | Recorded so the B-1 test is not written against the empty case. | `IDList.cs` line 110; Grep of non-test code for `MaxLengthOfID`. | + +## Design and Correctness Review + +### Settings-store seam + +- `ISettingsStore` exposes only the indexer and `Save()`, the minimum that the four consumers use. `ApplicationSettingsStore` null-guards its argument and forwards both members; it is `sealed` and carries no state beyond the wrapped instance. +- Behaviour preservation: the generated property `MaxLengthOfID` is `((int)(this["MaxLengthOfID"]))` and its setter `this["MaxLengthOfID"] = value` (`ToDoModel/Properties/Settings.Designer.cs` lines 137 to 142). The new code assigns a boxed `int` through the same indexer and calls the same `Save()`, so production persistence is unchanged. `ToDoItem` still reads `Properties.Settings.Default.MaxLengthOfID` directly, which remains consistent because the production default writes to that same instance. +- `nameof(Properties.Settings.X)` keys keep setting names compile-checked against the generated class. +- `AppStagingFilenames` converts all ten settings-backed properties, preserving the lazy `InitProp` caching and the non-persisting `EmailInfoStagingFile` setter; `Read` is a small private helper. +- `AppAutoFileObjects(IApplicationGlobals)` previously only assigned `_parent`; it now chains to the internal overload, which assigns the same field plus the store. No other initialization was skipped. + +### Tests + +- The fail-before probe is sound: it counts `SettingsSaving` only on the test thread and sets `e.Cancel = true`, so the fail-before run itself writes nothing to `user.config`, and concurrent classes are neither counted nor cancelled. +- `InMemorySettingsStore` throws `KeyNotFoundException` for an unseeded key, so a missing arrangement fails explicitly instead of returning a default. +- The DfDeedle happy-path test is now an awaited `async Task` test with `NotThrowAsync`, verifies the production adder added `SentOn`, and leaves `columnAdder` at its default so the production branch stays exercised. +- `TaskTreeControllerMoveLogicTests.MakeController` accepts an optional `Mock` so the probe test can verify exactly one store save while the other move tests receive a fresh mock. + +### Scope fences + +No change to `IIDList`, `TreeOfToDoItems`, `TaskTreeController`, `AppToDoObjects`, `ApplicationGlobals`, runsettings, `coverage.config`, CI workflows or `UtilitiesCS/Extensions/`. Out-of-scope `Settings.Default` in-memory mutation (issue #1006) is untouched. + +## Best-Practice Checklist + +| Area | Verdict | Note | +|---|---|---| +| Naming and documentation | PASS | XML docs on every new public and internal member; comments explain why (field initializer, lock scope, reflection limits). | +| Error handling | PASS | `ArgumentNullException` with correct `ParamName` on every new constructor; tested. | +| Async correctness | PASS | No sync-over-async remains in the converted DfDeedle tests. | +| Thread safety | PASS | Per-instance stores; no shared mutable static added. | +| File size | PASS | Largest changed file other than the two shrinking pre-existing files is 477 lines. | +| Language level | PASS | C# 7.3 constraints respected in `ToDoModel.Test` and `TaskTree.Test`. | +| Test coverage of changed behaviour | FAIL | B-1. | + +## Recommendation + +Address B-1 in a short remediation cycle: one or two `MaxLengthOfID` tests in `IDListGetNextToDoIDTests.cs` (the file is at 235 lines), a full C# toolchain pass in CLAUDE.md order, and a corrected coverage comparison that reports `IDList.cs` line and branch figures. No production change is needed. diff --git a/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/code-review.2026-10-22T17-23.md b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/code-review.2026-10-22T17-23.md new file mode 100644 index 000000000..5ca493792 --- /dev/null +++ b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/code-review.2026-10-22T17-23.md @@ -0,0 +1,64 @@ +# Code Review: Parallel test failures from wall-clock deadlines and shared user.config (Issue #1005) + +- Branch: `bug/parallel-test-failures-wall-clock-and-shared-user-config-1005` +- Head: `3df0782ab`; merge base `346651242a2bd6ed25d703a4b1ecb5a055c6ae72` (`main`) +- Review timestamp: 2026-10-22T17-23 (re-audit R4 after remediation cycle 1; prior review 2026-10-22T17-10 at `e399de597`) +- Reviewer: feature-review agent +- Scope: the full branch diff `346651242..3df0782ab` (91 files: 7 production C#, 13 test C#, 4 project files, feature documents and evidence, 2 promotion records, agent-memory files). Cycle-1 delta `e399de597..3df0782ab`: 19 files, one test C# file plus documents and evidence; no production change. + +## Executive Summary + +The design is unchanged from the prior review and remains narrow: a two-member `ISettingsStore` interface and a sealed `ApplicationSettingsStore` adapter give each settings consumer a per-instance persistence seam whose production default wraps the consumer assembly's own `Properties.Settings.Default`. No static mutable seam, process-wide `SettingsProvider`, `user.config` redirection, temporary file, serialization attribute, sleep or retry is introduced. The DfDeedle fix is test-only and uses the existing `timeProvider` parameter with a never-advanced `FakeTimeProvider`. + +The prior blocking finding B-1 is resolved. Cycle 1 adds three store-injected `MaxLengthOfID` tests to `IDListGetNextToDoIDTests.cs` (287 lines at head). The getter is fully covered again (7/7 lines, 4/4 branches in `artifacts/csharp/coverage.xml`), `IDList.cs` rose to 30.18% line and 25.81% branch against the merge-base 24.36% and 20.69%, and the new coverage comparison reports both rates and names the getter. + +Verdict: **PASS (0 blocking, 0 major, 7 minor or informational)**. + +## Findings Table + +| Severity | File | Location | Finding | Recommendation | Rationale | Evidence | +|---|---|---|---|---|---|---| +| Resolved | `ToDoModel.Test/Data Model/ID/IDListGetNextToDoIDTests.cs`; `ToDoModel/Data Model/ID/IDList.cs` | `IDList.MaxLengthOfID` getter | B-1 (prior review): the getter had lost all test coverage. Cycle 1 adds `MaxLengthOfID_ColdCacheOnMixedLengthList_ReturnsLongestIdLength`, `MaxLengthOfID_ColdCacheOnSecondDistinctList_ReturnsItsOwnLongestIdLength` and `MaxLengthOfID_WarmCacheAfterGetNextToDoID_ReturnsCachedLengthWithoutSaving`. | None. | Coverage restored to the merge-base level on every getter line and branch; disclosure corrected. | `artifacts/csharp/coverage.xml` method `get_MaxLengthOfID` `line-rate="1" branch-rate="1"`, lines 107 to 113 `hits="1"`, branch lines `100% (2/2)`; `coverage-comparison.2026-10-22T17-12.md`. | +| Minor | `TaskMaster/AppGlobals/AppAutoFileObjects.cs` | file-level coverage | N-1: per-file line rate 45.66% to 40.81% because covered scalar-property lines moved into `AppAutoFileObjects.ScalarSettings.cs`; branch rate unchanged at 37.10%. | None for this item. | Movement between partial files of one class is not a coverage loss. | Cobertura class rows for both files. | +| Informational | `TaskTree.Test/TaskTreeControllerMoveLogicTests.cs`; `TaskMaster.Test/AppGlobals/SettingsSaveProbe.cs` | `CountToDoModelSettingsSavesOnCurrentThread`; `CountSavesOnCurrentThread` | O-4: the thread-attributed save probe is implemented twice, once per test assembly. | Accept. | The test assemblies share no helper project. | Diff of both files. | +| Informational | `SettingsSaveProbe.cs`; `TaskTreeControllerMoveLogicTests.cs` | `lock (settings)` around subscribe and unsubscribe | O-5: the probe locks on the shared `Settings.Default` instance. | Accept; the lock covers only event add and remove. | Cannot serialize the code under test. | Code comments in both probes. | +| Informational | `IDListGetNextToDoIDTests.cs`, `AppQuickFilerSettingsTests.cs`, `AppStagingFilenamesTests.cs`, `AppAutoFileObjectsCoverageExpansionTests.cs` | `Constructor_*_UsesApplicationSettingsStore` | O-6: default-constructor tests read the private field `_settingsStore` by reflection. | Accept. | No public accessor should be added only for tests. | Diff of the four test files. | +| Informational | `ToDoModel/Data Model/ID/IDList.cs` | field initializer for `_settingsStore` | O-7: store-taking constructors overwrite a default adapter that the field initializer allocated. | Accept. | Guarantees every existing constructor keeps the production default (spec D3). | `IDList.cs` field declarations. | +| Informational | `ToDoModel/Data Model/ID/IDList.cs` | `MaxLengthOfID` getter | O-8: on an empty list with a zero cache the getter throws `InvalidOperationException` (pre-existing). The cycle-1 tests correctly use non-empty lists. | Out of scope. | Pre-existing behaviour, unchanged by this branch. | Cycle-1 diff. | +| Informational | `ToDoModel.Test/Data Model/ID/IDListGetNextToDoIDTests.cs` | `MaxLengthOfID_WarmCacheAfterGetNextToDoID_ReturnsCachedLengthWithoutSaving` | O-9: the asserted value 4 is also what a recomputation over `{ "02", "0100" }` would return, so the value assertion alone does not distinguish the cached path from the recompute path; the cached (false) branch is proven only by the Cobertura branch count. The `Save` verification (`Times.Once()`) does pin that the read does not persist. Similarly, `..._ColdCacheOnSecondDistinctList_...` re-asserts the first list's value, overlapping the first test. | Optional: a future change could seed a cache value that differs from the recomputed maximum, but `IDList` exposes no such seam and adding one is not justified for this item. | The test meets the remediation's stated purpose (both getter branches executed and the non-persisting read verified). | Cycle-1 diff; head Cobertura branch line 108 `100% (2/2)`. | + +## Design and Correctness Review + +### Settings-store seam + +- `ISettingsStore` exposes only the indexer and `Save()`; `ApplicationSettingsStore` null-guards its argument and forwards both members. +- Behaviour preservation: generated settings properties read and write through `this[name]`, so the store indexer plus `Save()` on the default adapter is equivalent to the generated setters plus `Save()` that it replaces. +- `nameof(Properties.Settings.X)` keys keep setting names compile-checked. +- `AppStagingFilenames` keeps the lazy `InitProp` caching and the non-persisting `EmailInfoStagingFile` setter. + +### Tests + +- The fail-before probe counts `SettingsSaving` only on the test thread and cancels it, so the fail-before run wrote nothing. +- `InMemorySettingsStore` throws `KeyNotFoundException` for an unseeded key. +- The DfDeedle happy-path test is an awaited `async Task` test with `NotThrowAsync`. +- Cycle-1 tests: each builds its own `Mock` and list, uses non-empty lists, follows Arrange/Act/Assert with comments, uses FluentAssertions with `because` text, and complies with C# 7.3. The second cold-cache test reads the getter on two distinct lists within one test, so both arms of the compiler-generated cached-lambda branch on line 110 execute regardless of test order. + +### Scope fences + +No change to `IIDList`, `TreeOfToDoItems`, `TaskTreeController`, `AppToDoObjects`, `ApplicationGlobals`, runsettings, `coverage.config`, CI workflows or `UtilitiesCS/Extensions/`. Cycle 1 touched no production, project or runsettings file. Out-of-scope `Settings.Default` in-memory mutation (issue #1006) is untouched. + +## Best-Practice Checklist + +| Area | Verdict | Note | +|---|---|---| +| Naming and documentation | PASS | XML docs on every new public and internal member; comments explain why. | +| Error handling | PASS | `ArgumentNullException` on every new constructor; tested. | +| Async correctness | PASS | No sync-over-async in the converted DfDeedle tests. | +| Thread safety | PASS | Per-instance stores; no shared mutable static added. | +| File size | PASS | Largest changed file other than the two shrinking pre-existing files is 477 lines; `IDListGetNextToDoIDTests.cs` is 287. | +| Language level | PASS | C# 7.3 constraints respected in `ToDoModel.Test` and `TaskTree.Test`. | +| Test coverage of changed behaviour | PASS | B-1 resolved; new code 100%; no modified file regressed. | + +## Recommendation + +Proceed to PR authoring. The TaskMaster CI checks (actionlint, build-analyzers, build-nullable, format-check, hygiene, mstest-coverage, pester) are a PR-time gate and have not yet run because no pull request exists. diff --git a/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/baseline/baseline-coverage-projection.2026-10-22T16-10.jacoco.xml b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/baseline/baseline-coverage-projection.2026-10-22T16-10.jacoco.xml new file mode 100644 index 000000000..f3994ee1d --- /dev/null +++ b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/baseline/baseline-coverage-projection.2026-10-22T16-10.jacoco.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/baseline/baseline-coverage.2026-10-22T16-10.md b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/baseline/baseline-coverage.2026-10-22T16-10.md new file mode 100644 index 000000000..1b6a5c301 --- /dev/null +++ b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/baseline/baseline-coverage.2026-10-22T16-10.md @@ -0,0 +1,56 @@ +# Baseline Coverage (TEST-COVERAGE) + +Timestamp: 2026-10-22T16-10 +Command: pwsh -NoProfile -File \scripts\vscode\Invoke-MSTestWithCoverage.ps1 +EXIT_CODE: 0 +Output Summary: Test Run Successful. Total 7599, executed 7599, passed 7599, failed 0 (trx summary). First-party coverage: lines 57542/66677 (86.30%), branches 14036/17419 (80.58%). Repository line 86.30% and branch 80.58% against the 80 and 75 percent floors. Per-file line coverage: IDList.cs 24.36% (38/156), AppQuickFilerSettings.cs 100% (30/30), AppStagingFilenames.cs 30.77% (20/65), AppAutoFileObjects.cs 45.66% (184/403). DfDeedle_COM_Tests executed: 26. + +## Runner route + +- Route: the unscoped script `scripts/vscode/Invoke-MSTestWithCoverage.ps1`, invoked through PowerShell 7 with an absolute script path. It applies `scripts/vscode/TaskMaster.cli.runsettings` (Workers 0 -> 24 workers reported, Scope ClassLevel), `/InIsolation`, `TestCategory!=LiveOutlook`, and the `\.claude\` worktree exclusion, inside `dotnet-coverage collect`. +- Shell-icon carve-out: not used. The run did not stall (test time 41.6 s, script elapsed 95 s); the four shell-icon classes ran and passed. + +## Trx summary (derived from the trx, raw trx not committed) + +``` +Test run outcome: Completed +Total 7599, executed 7599, passed 7599, failed 0. +Skipped 0, derived as total minus executed rather than reported by the test platform. +Figures reported verbatim by the test platform: error 0, timeout 0, aborted 0, notExecuted 0, inconclusive 0. +Failed tests: none +``` + +## One-line first-party coverage summary + +``` +First-party coverage: lines 57542/66677 (86.30%), branches 14036/17419 (80.58%) +``` + +## Repository figures (post-processed Cobertura root) + +| Metric | Value | Floor | +| --- | --- | --- | +| Line | 86.30% (57542/66677) | 80% | +| Branch | 80.58% (14036/17419) | 75% | + +## Per-file line coverage (post-processed Cobertura, union of class line entries per file) + +| File | Covered / Valid | Line % | +| --- | --- | --- | +| `ToDoModel/Data Model/ID/IDList.cs` | 38 / 156 | 24.36 | +| `TaskMaster/AppGlobals/AppQuickFilerSettings.cs` | 30 / 30 | 100.00 | +| `TaskMaster/AppGlobals/AppStagingFilenames.cs` | 20 / 65 | 30.77 | +| `TaskMaster/AppGlobals/AppAutoFileObjects.cs` | 184 / 403 | 45.66 | + +## DfDeedle_COM_Tests count (for P5-T2) + +- `DfDeedle_COM_Tests` tests executed: 26 (26 UnitTest definitions with class `UtilitiesCS.Test.Extensions.DfDeedle_COM_Tests`, 26 results, all Passed). + +## Failed tests + +- None. + +## Artifacts + +- JaCoCo projection copy: `evidence/baseline/baseline-coverage-projection.2026-10-22T16-10.jacoco.xml` (no host path content). +- Post-processed Cobertura copy: `SCRATCH` `issue-1005-baseline-cobertura.xml` (not committed). Trx copy: `SCRATCH` `issue-1005-baseline.trx` (not committed). diff --git a/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/baseline/baseline-file-sizes.2026-10-22T16-10.md b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/baseline/baseline-file-sizes.2026-10-22T16-10.md new file mode 100644 index 000000000..60a59df75 --- /dev/null +++ b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/baseline/baseline-file-sizes.2026-10-22T16-10.md @@ -0,0 +1,25 @@ +# Baseline File Sizes (physical lines) + +Timestamp: 2026-10-22T16-10 +Command: @(Get-Content -LiteralPath ).Count for each existing Write Set .cs file +EXIT_CODE: 0 +Output Summary: 14 existing Write Set .cs files counted. Two are above 500: UtilitiesCS.Test/Extensions/DfDeedle_COM_Tests.cs 848 and TaskMaster/AppGlobals/AppAutoFileObjects.cs 853. Largest others: RibbonControllerTests.cs 452, TaskTreeControllerMoveLogicTests.cs 414. + +| File | Physical lines | +| --- | --- | +| `ToDoModel/Data Model/ID/IDList.cs` | 318 | +| `TaskMaster/AppGlobals/AppQuickFilerSettings.cs` | 68 | +| `TaskMaster/AppGlobals/AppStagingFilenames.cs` | 139 | +| `TaskMaster/AppGlobals/AppAutoFileObjects.cs` | 853 | +| `UtilitiesCS.Test/Extensions/DfDeedle_COM_Tests.cs` | 848 | +| `ToDoModel.Test/Data Model/ID/IDListGetNextToDoIDTests.cs` | 120 | +| `TaskTree.Test/TaskTreeControllerMoveLogicTests.cs` | 414 | +| `TaskTree.Test/TaskTreeControllerRouteDropTests.cs` | 220 | +| `TaskMaster.Test/AppGlobals/AppQuickFilerSettingsTests.cs` | 89 | +| `TaskMaster.Test/AppGlobals/AppQuickFilerSettingsRemainingPropertiesTests.cs` | 134 | +| `TaskMaster.Test/AppGlobals/AppStagingFilenamesTests.cs` | 146 | +| `TaskMaster.Test/AppGlobals/AppAutoFileObjectsCoverageExpansionTests.cs` | 274 | +| `TaskMaster.Test/Ribbon/RibbonControllerTests.cs` | 452 | +| `TaskMaster.Test/Ribbon/RibbonControllerTests.Engines.cs` | 73 | + +New Write Set files (not yet present): `ISettingsStore.cs`, `ApplicationSettingsStore.cs`, `AppAutoFileObjects.ScalarSettings.cs`, `DfDeedle_COM_Tests.QfcColumnsAsync.cs`, `ApplicationSettingsStoreTests.cs`, `InMemorySettingsStore.cs`, `SettingsSaveProbe.cs`. diff --git a/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/baseline/baseline-test-disposition.2026-10-22T16-11.md b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/baseline/baseline-test-disposition.2026-10-22T16-11.md new file mode 100644 index 000000000..5a44fa6e7 --- /dev/null +++ b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/baseline/baseline-test-disposition.2026-10-22T16-11.md @@ -0,0 +1,42 @@ +# Baseline Test Disposition (symbols changed by the plan) + +Timestamp: 2026-10-22T16-11 +Command: Grep (ripgrep) over *.Test/**/*.cs for `\bIDList\b|GetNextToDoID|AppQuickFilerSettings|AppStagingFilenames|AppAutoFileObjects|CreateSutWithSpecialFolders|AddQfcColumnsAsync`, and separately for `CreateController` +EXIT_CODE: 0 +Output Summary: 85 files match the combined pattern (426 occurrences). Nine classes construct or call the concrete changed members and are UPDATED by the plan; three are UNCHANGED by design (AppAutoFileObjectsFolderPredictorTests, RibbonControllerTests.Engines.cs, DfDeedleQfcColumnTimeoutTests). All other matches reference only interfaces (`IAppAutoFileObjects`, `IAppQuickFilerSettings`, `IAppStagingFilenames`, `IIDList`), comments, or an unrelated local helper that happens to be named `CreateController`, and are UNCHANGED. No match requires a change outside the Write Set. + +## Updated by this plan + +| Test class (file) | Matching symbol(s) | Disposition | Reason | +| --- | --- | --- | --- | +| `IDListGetNextToDoIDTests` (`ToDoModel.Test/Data Model/ID/IDListGetNextToDoIDTests.cs`) | `IDList`, `GetNextToDoID` | updated (P3-T2) | constructs `IDList` and reaches the `Save()` branch through the cache-priming workaround; converted to an injected `Mock` | +| `TaskTreeControllerMoveLogicTests` (`TaskTree.Test/TaskTreeControllerMoveLogicTests.cs`) | `IDList` (`MakeController`) | updated (P1-T2, P3-T3) | `MakeController` returns `new IDList()`; six tests reach `GetNextToDoID` | +| `TaskTreeControllerRouteDropTests` (`TaskTree.Test/TaskTreeControllerRouteDropTests.cs`) | `IDList` (`MakeController`) | updated (P3-T3) | `MakeController` returns `new IDList()`; three tests reach `GetNextToDoID` | +| `AppQuickFilerSettingsTests` | `AppQuickFilerSettings` | updated (P1-T3, P4-T5) | setter tests save through `Settings.Default` | +| `AppQuickFilerSettingsRemainingPropertiesTests` | `AppQuickFilerSettings` | updated (P4-T6) | setter tests save through `Settings.Default` | +| `AppStagingFilenamesTests` | `AppStagingFilenames` | updated (P1-T4, P4-T7) | setter tests save through `Settings.Default` | +| `AppAutoFileObjectsCoverageExpansionTests` | `AppAutoFileObjects`, `CreateSutWithSpecialFolders` | updated (P1-T5, P4-T8) | scalar setters save; `TestCleanup` calls `defaults.Save()` directly | +| `RibbonControllerTests` (`TaskMaster.Test/Ribbon/RibbonControllerTests.cs`) | `AppQuickFilerSettings`, `CreateController` | updated (P4-T10) | `CreateController()` injects a default `AppQuickFilerSettings`; four tests reach the saving setters | +| `DfDeedle_COM_Tests` (`UtilitiesCS.Test/Extensions/DfDeedle_COM_Tests.cs`) | `AddQfcColumnsAsync` | updated (P5-T1) | two calls pass neither `columnAdder` nor `timeProvider` | + +## Unchanged by design + +| Test class (file) | Matching symbol(s) | Disposition | Reason | +| --- | --- | --- | --- | +| `AppAutoFileObjectsFolderPredictorTests` | `AppAutoFileObjects` | unchanged | parent-only constructor `new AppAutoFileObjects(mockGlobals.Object)`; reads no scalar member; the parent-only constructor keeps its signature and defaults the store to the adapter | +| `RibbonControllerTests` partial in `RibbonControllerTests.Engines.cs` | `CreateController` | unchanged | Decision D-E: the parameterless `CreateController()` keeps its signature | +| `DfDeedleQfcColumnTimeoutTests` | `AddQfcColumnsAsync` | unchanged | already passes both `columnAdder` and `timeProvider` | + +## Further matches (each unchanged) + +| File(s) | Matching symbol(s) | Disposition | Reason | +| --- | --- | --- | --- | +| `TaskMaster.Test/AppGlobals/ApplicationGlobalsTests.cs` | `AppAutoFileObjects` | unchanged | creates an uninitialized `AppAutoFileObjects` via `GetUninitializedObject` and assigns it to `_autoFileObjects`; the test (`LoadWhenIdle`) reads no scalar member, so a null store field is never dereferenced | +| `TaskMaster.Test/AppGlobals/AppToDoObjectsTests.cs`, `AppToDoObjectsCoverageExpansionTests.cs` | `IDList` (property) | unchanged | read the `AppToDoObjects.IDList` property and `Count`; no `GetNextToDoID` call | +| `TaskVisualization.Test/AutoCreateProjectTests.cs`, `ToDoModel.Test/Data Model/ToDo/ToDoItemCoverageExpansionTests.cs` | `GetNextToDoID` | unchanged | calls on `Mock`; no real `IDList` | +| `TaskMaster.Test/AppGlobals/AppToDoObjectsTestDoubles.cs`, `AppOlObjectsTests.cs`, `AppOlObjectsCoverageTests.cs`, `QuickFiler.Test/Controllers/EfcHomeController*Tests.cs`, `UtilitiesCS.Test/EmailIntelligence/EmailDataMiner_TestSupport.cs` | interface-typed members `IAppAutoFileObjects`, `IAppQuickFilerSettings`, `IAppStagingFilenames`, `IIDList` | unchanged | interface members of hand-written globals doubles; interfaces do not change | +| `TaskMaster.Test/Ribbon/SpamManagerResetGateTests.cs`, `TaskVisualization.Test/ManageFiltersControllerTests.cs`, every `QuickFiler.Test/Controllers/Qfc*Tests.cs` match, every `UtilitiesCS.Test/EmailIntelligence/**` and `UtilitiesCS.Test/OutlookObjects/Folder/**` match | `Mock`, `Mock`, `Mock` | unchanged | Moq mocks of unchanged interfaces | +| `UtilitiesCS.Test/Threading/AppGlobalsConverterTests.cs`, `ToDoModel.Test/FolderPathsTest.cs` | `IAppStagingFilenames` | unchanged | commented-out code | +| 24 files with an unrelated local `CreateController` helper: `QuickFiler.Test/Controllers/EfcFormControllerIssue995Tests.cs`, `QfcFormControllerDeactivateTests.cs`, `QfcExplorerControllerTests.cs`, `QfcFormControllerCancelTeardownTests.cs`, `QfcFormControllerCleanupTests.cs`, `QfcExplorerController.ConversationViewTests.cs`, `QfcCollectionControllerTests.cs`, `QfcCollectionControllerLayout.StaTests.cs`, `QfcCollectionControllerDefects468MoveTests.cs`, `QfcCollectionControllerDarkModeTests.cs`, `EfcHomeControllerMetricsTests.cs`, `EfcHomeControllerLifecycleTests.cs`, `EfcHomeControllerExecuteMovesTests.cs`, `QuickFiler.Test/Helper Classes/QfcThemeHelperTests.cs`, `UtilitiesCS.Test/OutlookObjects/Store/StoreWrapperController_Tests*.cs` (4), `StoreWrapperViewerTests.cs`, `DisabledStoresControllerTests.cs`, `UtilitiesCS.Test/EmailIntelligence/FilterOlFoldersController_Tests.cs`, `FolderRemapViewer_Tests.cs`, `FolderRemapController_Tests.cs`, `FolderRemapController_Tests2.cs` | `CreateController` (name only) | unchanged | each is a class-local helper building a different controller; none constructs `RibbonController` or `AppQuickFilerSettings` | + +Result: every match carries a disposition; no match outside the Write Set must change. diff --git a/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/baseline/baseline-toolchain-analyzers.2026-10-22T16-07.md b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/baseline/baseline-toolchain-analyzers.2026-10-22T16-07.md new file mode 100644 index 000000000..3009694d9 --- /dev/null +++ b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/baseline/baseline-toolchain-analyzers.2026-10-22T16-07.md @@ -0,0 +1,11 @@ +# Baseline Toolchain: Analyzers + +Timestamp: 2026-10-22T16-07 +Command: msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true +EXIT_CODE: 0 +Output Summary: `0 Warning(s)`, `0 Error(s)`. Rebuild compiled all 18 solution assemblies (18 ` -> ` output lines in the log). Write Set file warnings: none (0 diagnostics whose path is a Write Set file). + +## Notes + +- MSBuild resolved through vswhere (VS 18). A normal-verbosity file log was written to `SCRATCH` (`issue-1005-baseline-analyze.log`) and is not committed. +- Elapsed 17 s; compilation confirmed by the per-project output lines, not inferred from the exit code. diff --git a/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/baseline/baseline-toolchain-bootstrap.2026-10-22T16-06.md b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/baseline/baseline-toolchain-bootstrap.2026-10-22T16-06.md new file mode 100644 index 000000000..84c9d2ef2 --- /dev/null +++ b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/baseline/baseline-toolchain-bootstrap.2026-10-22T16-06.md @@ -0,0 +1,39 @@ +# Baseline Toolchain Bootstrap (Issue #1005) + +Timestamp: 2026-10-22T16-06 +Command: dotnet tool restore +EXIT_CODE: 0 +Output Summary: Tool 'csharpier' (version '1.2.6') was restored. Restore was successful. + +## Step 1 - Repo SDK install (conditional) + +- Command: `scripts/vscode/Install-RepoDotNetSdk.ps1` +- Not run: the pinned SDK is present (`.dotnet-sdk\dotnet.exe` exists; `dotnet --list-sdks` lists 8.0.205 under `.dotnet-sdk`, matching `global.json`). The plan runs this step only when the pinned SDK is absent. + +## Step 2 - Tool restore + +- Command: `dotnet tool restore` +- EXIT_CODE: 0 +- Output: `Tool 'csharpier' (version '1.2.6') was restored.` / `Restore was successful.` + +## Step 3 - Package restore + +- Command: `pwsh -NoProfile -File \scripts\vscode\Invoke-Restore.ps1` (absolute script path) +- EXIT_CODE: 0 +- Output: MSBuild `/t:Restore /p:RestorePackagesConfig=true` on `TaskMaster.sln`: `Build succeeded.` `0 Warning(s)` `0 Error(s)`. + +## git status --porcelain (before restore) + +``` + M docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/plan.2026-10-22T11-02.md +?? docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/baseline/ +``` + +## git status --porcelain (after restore) + +``` + M docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/plan.2026-10-22T11-02.md +?? docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/baseline/ +``` + +Result: identical listings; no new tracked-file change from the restore (the two entries are this run's plan check-off and evidence folder). diff --git a/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/baseline/baseline-toolchain-format.2026-10-22T16-07.md b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/baseline/baseline-toolchain-format.2026-10-22T16-07.md new file mode 100644 index 000000000..9f33ab4aa --- /dev/null +++ b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/baseline/baseline-toolchain-format.2026-10-22T16-07.md @@ -0,0 +1,6 @@ +# Baseline Toolchain: Format (read-only) + +Timestamp: 2026-10-22T16-07 +Command: dotnet tool run csharpier check . +EXIT_CODE: 0 +Output Summary: `Checked 1675 files in 5442ms.` No drifting files reported. Run from the repository root; `format` was not run, so no file was rewritten. diff --git a/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/baseline/baseline-toolchain-typecheck.2026-10-22T16-08.md b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/baseline/baseline-toolchain-typecheck.2026-10-22T16-08.md new file mode 100644 index 000000000..b0a41332f --- /dev/null +++ b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/baseline/baseline-toolchain-typecheck.2026-10-22T16-08.md @@ -0,0 +1,6 @@ +# Baseline Toolchain: Type Check + +Timestamp: 2026-10-22T16-08 +Command: msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true +EXIT_CODE: 0 +Output Summary: `0 Error(s)`, `0 Warning(s)`. No `/p:Nullable=enable`. Normal-verbosity log kept under `SCRATCH` (`issue-1005-baseline-typecheck.log`), not committed. diff --git a/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/baseline/merge-base-scope.2026-10-22T16-06.md b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/baseline/merge-base-scope.2026-10-22T16-06.md new file mode 100644 index 000000000..f69272eb1 --- /dev/null +++ b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/baseline/merge-base-scope.2026-10-22T16-06.md @@ -0,0 +1,35 @@ +# Merge-Base Scope (Issue #1005) + +Timestamp: 2026-10-22T16-06 +Command: git fetch origin main; git merge-base origin/main HEAD; git diff --name-status HEAD; git status --porcelain +EXIT_CODE: 0 +Output Summary: MERGE-BASE = 346651242a2bd6ed25d703a4b1ecb5a055c6ae72. The branch diff holds 15 paths, all documentation (`.md`) under `.claude/agent-memory/`, the feature folder, and `docs/features/potential/promoted/`. No path ends in `.cs`, `.csproj`, or `.runsettings` in either listing. Decision D-F holds: the baseline measures the merge-base code. + +## git diff --name-status HEAD + +``` +M .claude/agent-memory/atomic-planner/MEMORY.md +A .claude/agent-memory/atomic-planner/project_1005_r0_settings_store_seam_and_probe_seams.md +A .claude/agent-memory/atomic-planner/project_1005_r1_jacoco_projection_is_package_level_only.md +M .claude/agent-memory/prd-feature/feedback_ac_gates_verify_satisfiability.md +M .claude/agent-memory/task-researcher/MEMORY.md +A .claude/agent-memory/task-researcher/project_settings_save_shared_user_config_1005.md +A docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/other/preflight-fixes.2026-10-22T15-47.md +A docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/other/preflight-fixes.2026-10-22T16-01.md +A docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/issue.md +A docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/plan.2026-10-22T11-02.md +A docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/research/2026-10-22T15-21-parallel-test-isolation-wall-clock-and-user-config-research.md +A docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/spec.md +A docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/user-story.md +A docs/features/potential/promoted/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config.md +A docs/features/potential/promoted/2026-10-22-tests-mutate-shared-settings-default-in-memory.md +``` + +## git status --porcelain + +``` + M docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/plan.2026-10-22T11-02.md +?? docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/baseline/ +``` + +Result: PASS. No `.cs`, `.csproj`, or `.runsettings` path appears. diff --git a/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/baseline/phase0-instructions-read.2026-10-22T16-05.md b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/baseline/phase0-instructions-read.2026-10-22T16-05.md new file mode 100644 index 000000000..47d7f651e --- /dev/null +++ b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/baseline/phase0-instructions-read.2026-10-22T16-05.md @@ -0,0 +1,26 @@ +# Phase 0 Instructions Read (Issue #1005) + +Timestamp: 2026-10-22T16-05 +Branch: bug/parallel-test-failures-wall-clock-and-shared-user-config-1005 +Policy Order: CLAUDE.md -> .claude/rules/general-code-change.md -> .claude/rules/general-unit-test.md -> .claude/rules/csharp.md (per policy-compliance-order) + +## Files Read (in order) + +1. `CLAUDE.md` +2. `.claude/rules/general-code-change.md` +3. `.claude/rules/general-unit-test.md` +4. `.claude/rules/csharp.md` +5. `.claude/rules/plan-acceptance-gates.md` +6. `.claude/skills/execution-environment/SKILL.md` +7. `.claude/skills/evidence-and-timestamp-conventions/SKILL.md` +8. `.claude/skills/acceptance-criteria-tracking/SKILL.md` +9. `docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/spec.md` +10. `docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/research/2026-10-22T15-21-parallel-test-isolation-wall-clock-and-user-config-research.md` + +## Spec Baseline Copy + +- `spec.md` copied to `SCRATCH` as `issue-1005-spec-baseline.md` for the P7-T10 criterion-text comparison. + +## Notes + +- AC source (full-bug): `spec.md` only, AC1 to AC8, all unchecked at this time. diff --git a/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/baseline/phase0-instructions-read.2026-10-22T17-07.md b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/baseline/phase0-instructions-read.2026-10-22T17-07.md new file mode 100644 index 000000000..b1180fadd --- /dev/null +++ b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/baseline/phase0-instructions-read.2026-10-22T17-07.md @@ -0,0 +1,13 @@ +Timestamp: 2026-10-22T17-07 + +Policy Order: +1. CLAUDE.md +2. .claude/rules/general-code-change.md +3. .claude/rules/general-unit-test.md +4. .claude/rules/csharp.md + +Files read: +- CLAUDE.md +- .claude/rules/general-code-change.md +- .claude/rules/general-unit-test.md +- .claude/rules/csharp.md diff --git a/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/baseline/remediation-file-size.2026-10-22T17-07.md b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/baseline/remediation-file-size.2026-10-22T17-07.md new file mode 100644 index 000000000..bb5f9a15f --- /dev/null +++ b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/baseline/remediation-file-size.2026-10-22T17-07.md @@ -0,0 +1,5 @@ +Timestamp: 2026-10-22T17-07 +Command: Read ToDoModel.Test/Data Model/ID/IDListGetNextToDoIDTests.cs (physical line count before edit) +EXIT_CODE: 0 +Output Summary: +- IDListGetNextToDoIDTests.cs: 235 lines before the edit; ceiling 500 lines. diff --git a/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/baseline/remediation-idlist-coverage-baseline.2026-10-22T17-07.md b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/baseline/remediation-idlist-coverage-baseline.2026-10-22T17-07.md new file mode 100644 index 000000000..bff9fd3c2 --- /dev/null +++ b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/baseline/remediation-idlist-coverage-baseline.2026-10-22T17-07.md @@ -0,0 +1,6 @@ +Timestamp: 2026-10-22T17-07 +Command: Read artifacts/csharp/coverage.xml (reviewed-run Cobertura), class ToDoModel.IDList and method get_MaxLengthOfID +EXIT_CODE: 0 +Output Summary: +- IDList.cs (class ToDoModel.IDList): line-rate 0.260355 (26.04%), branch-rate 0.193548 (19.35%) +- get_MaxLengthOfID: line-rate 0, branch-rate 0 (regressed head state) diff --git a/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/other/preflight-fixes.2026-10-22T15-47.md b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/other/preflight-fixes.2026-10-22T15-47.md new file mode 100644 index 000000000..8113b3194 --- /dev/null +++ b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/other/preflight-fixes.2026-10-22T15-47.md @@ -0,0 +1,24 @@ +# Preflight Fixes (Issue #1005) + +Timestamp: 2026-10-22T15-47 +Plan: docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/plan.2026-10-22T11-02.md +Validator: mcp__drm-copilot__validate_orchestration_artifacts (plan) passed before and after the fixes. + +Each fix edits task text only. No task was added, removed, or reordered. + +## Fixes + +1. [P0-T7] Unrecorded baseline count. P5-T2 compares the number of `DfDeedle_COM_Tests` tests "as at P0-T7", but the P0-T7 acceptance did not require that count. P0-T7 now also records the number of `DfDeedle_COM_Tests` tests executed, derived from the trx. +2. [P1-T2] Conflict between tasks. P1-T2 required the probe assertion message to name the settings file token, and the P6-T2 added-line scan requires a count of zero for that token. The message now describes the shared per-host settings file without spelling the file name. The probe assertion is unchanged. +3. [P1-T6] Evidence field order. The artifact declares `ExpectedExitCode: 1` and records two commands (ANALYZE, exit 0, and the vstest run, exit 1). The first `EXIT_CODE:` forms the machine-read record, so the task now states that the vstest command and its exit code are recorded first. +4. [P3-T2] Test input aligned with the AC2 wording. AC2 requires that "a shorter ID does not save", and the task described a second ID of the same length. The test now generates "0100" from seed "ZZ" (one save) and then the shorter "02" from seed "0001" (no further save). The register name `GetNextToDoID_IdNotLongerThanCachedLength_DoesNotSaveAgain` already covers this case and is unchanged. +5. [P4-T7] Contradiction inside the task. The task keeps the P1-T4 probe, which passes `Settings.Default` to `SettingsSaveProbe.CountSavesOnCurrentThread`, while its acceptance required no `Settings.Default` reference. The acceptance now exempts that single probe argument. +6. [P4-T8] The same contradiction for the P1-T5 probe argument (`Properties.Settings.Default`). The acceptance now exempts that single probe argument. The "no `Save()` call" clause is unchanged. +7. [P6-T3] Scope fence did not cover existing paths. The merge-base diff already lists `docs/features/potential/promoted/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config.md`, and the working tree holds the untracked issue-1006 promotion record under the same folder. Neither path was in the permitted set. The permitted set now includes any path already listed in the P0-T3 `merge-base-scope` artifact (those promotion records and the `.claude/agent-memory/` changes). "No other `.claude/` path" and the `UtilitiesCS/Extensions/` exclusion are unchanged. +8. [P6-T4] Wording. "In one process" now reads "in one `vstest.console.exe` invocation", because vstest starts one test host per assembly. The elapsed-time field is now required only for runs that include `DfDeedle_COM_Tests`, because the other nine solo runs cannot report that test. + +## Observations (not defects; recorded for the executor) + +- During the P1-T6 fail-before run, `AppAutoFileObjectsCoverageExpansionTests.TestCleanup` still calls `defaults.Save()` outside the probe. That call is not cancelled and writes the settings file. This is existing behavior that the P0-T7 baseline run also shows, and P4-T8 removes it. The probes themselves cancel every save they record. +- `ApplicationSettingsBase.OnSettingsSaving` reads the delegate field twice. A non-probe save on another thread can therefore race with the last probe unsubscribing. After the fix, no test saves, so this exposure is limited to the P1-T6 run and its window is very small. +- The final line of the planner internal review record in the plan file is `PREFLIGHT: REVISIONS REQUIRED`. That line was left by the planner and does not reflect this preflight result. diff --git a/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/other/preflight-fixes.2026-10-22T16-01.md b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/other/preflight-fixes.2026-10-22T16-01.md new file mode 100644 index 000000000..3f715f386 --- /dev/null +++ b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/other/preflight-fixes.2026-10-22T16-01.md @@ -0,0 +1,20 @@ +# Preflight Fixes, Round 2 (Issue #1005) + +Timestamp: 2026-10-22T16-01 +Plan: docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/plan.2026-10-22T11-02.md +Validator: mcp__drm-copilot__validate_orchestration_artifacts (plan) passed before and after the fixes. + +Each fix edits task text only. No task was added, removed, or reordered. + +## Fixes + +1. [P4-T7] Missing instruction behind an acceptance condition. The acceptance requires no `Settings.Default` reference other than the probe argument, but the existing class summary of `AppStagingFilenamesTests` mentions `Settings.Default` several times, and the task, unlike P4-T5 and P4-T6, did not say to update the summary. The task now says to update the class summary. The acceptance is unchanged. +2. [P6-T2] Ambiguous reference. "Both runsettings files" did not name the files, and the repository holds several `.runsettings` files. The task now names `TaskMaster.runsettings` and `scripts/vscode/TaskMaster.cli.runsettings`. The diff scan over `*.runsettings` is unchanged. +3. [P6-T3] Scope fence did not cover agent memory written during execution. The permitted set covered only `.claude/agent-memory/` paths already listed by P0-T3, so an agent-memory file written by any agent after P0-T3 would fail the fence although no plan task edits it. The permitted set now includes any path under `.claude/agent-memory/`. "No other `.claude/` path" and the `UtilitiesCS/Extensions/` exclusion are unchanged. +4. [P7-T7] Count literal that a restarted QA loop breaks. The acceptance allowed exactly "the two `.jacoco.xml` projection copies", but the P7 loop rule writes a new P7-T4 artifact on each pass, so a restart leaves more than one final projection copy. The acceptance now allows only `.jacoco.xml` files named `baseline-coverage-projection.*.jacoco.xml` or `final-coverage-projection.*.jacoco.xml`. The prohibition on raw trx, raw coverage, and Cobertura files is unchanged. + +## Observations (not defects; recorded for the executor) + +- P1-T1: the revised clause about copying a delegate field to a local has no effect on the probe, because the probe holds its handler in a local variable and reads no delegate field. The non-atomic double read is inside `ApplicationSettingsBase.OnSettingsSaving`, which the probe cannot change. Only a save that no probe records is exposed: such a save can run on another thread while the last probe unsubscribes. After the fixes no test saves, so the exposure is limited to the P1-T6 fail-before run (the `AppAutoFileObjectsCoverageExpansionTests.TestCleanup` save). +- `ToDoModel.Test` and `TaskTree.Test` declare no `LangVersion` and therefore compile as C# 7.3 on .NET Framework 4.8.1. `TaskMaster.Test` declares `latest`. +- The post-processed Cobertura document keeps `` elements (the merge and closure-filter helpers under `scripts/vscode/` retain method-level lines), so the per-member figures that P7-T5 and AC7 require can be read from it. diff --git a/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/other/ribbon-static-state-check.2026-10-22T16-23.md b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/other/ribbon-static-state-check.2026-10-22T16-23.md new file mode 100644 index 000000000..532e5873c --- /dev/null +++ b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/other/ribbon-static-state-check.2026-10-22T16-23.md @@ -0,0 +1,39 @@ +# RibbonControllerTests Static-State Check (AC8) + +Timestamp: 2026-10-22T16-23 +Command: Read of the files listed below in full; Grep for `\bstatic\b`, `Settings\.Default`, `Environment\.`, `log4net`, `LogManager`, `UiThread`, `Dispatcher`, `SynchronizationContext` over `TaskMaster/Ribbon/RibbonController*.cs`, `TaskMaster/AppGlobals/ApplicationGlobals.cs`, the test partials, and the UtilitiesCS folder-tree types the tests construct +EXIT_CODE: 0 +Output Summary: No mutable process-global static state is written by any test in RibbonControllerTests other than the high-confidence Settings.Default snapshot, which P4-T10 removes. Verdict: REMOVE. + +## Files read + +- `TaskMaster.Test/Ribbon/RibbonControllerTests.cs` (whole file) +- `TaskMaster.Test/Ribbon/RibbonControllerTests.Engines.cs` (whole file) +- `TaskMaster/Ribbon/RibbonController.cs` (constructor, `Globals`, `SetGlobals`, `FolderTreeService`, static logger) +- `TaskMaster/Ribbon/RibbonController.Intelligence.cs` (whole file; settings-menu and high-confidence helpers) +- `TaskMaster/Ribbon/RibbonController.FolderTree.cs` (whole file) +- `TaskMaster/Ribbon/RibbonController.EngineCommands.cs` (static-member search) +- `TaskMaster/AppGlobals/ApplicationGlobals.cs` (`_quickFilerSettings`, `QfSettings`, `InternalQfSettings`, `Engines`, static members) +- `TaskMaster/AppGlobals/AppQuickFilerSettings.cs` (after P4-T1: instance store field only) +- UtilitiesCS folder-tree types constructed by the comparison tests: `FolderTreeSnapshot`, `FolderTreeSnapshotNode`, `FolderTreeNodeKey`, `FolderTreeRequest`, `FolderTreeSnapshotQueries`, `FolderTreeCompatibilityView`, `FolderWrapper`, and the three `FolderWrapper*Comparer` types (static-member search) + +## Static members found + +| Static member | Mutable | Written by a test in the class | Notes | +| --- | --- | --- | --- | +| `RibbonController.logger` (`static readonly log4net.ILog`) | no (readonly, assigned once by the type initializer) | no | log4net configuration is not changed by any test in the class | +| `ApplicationGlobals.logger` (`static readonly log4net.ILog`) | no | no | the class creates `ApplicationGlobals` through `FormatterServices.GetUninitializedObject`, which runs no instance constructor | +| `ApplicationGlobals.StopAndRestart` (static method) | n/a (no state) | no | not reached by the tests | +| `RibbonController.FormatFileSize`, `CompareFolderSnapshots`, `CreateViewForFolder`, `CompareFolderNodes`, `CompareFolderNodeMembers` (static methods) | n/a (pure functions over arguments) | no | `GetStats_*` and `CompareFolderSnapshots_*` call them; no shared state | +| `FolderWrapper.logger` (`static readonly`) | no | no | | +| `FolderTreeSnapshotQueries`, `FolderTreeRequest.AllStores`/`ForStore`, `FolderTreeNodeKey`/`FolderTreeSnapshotNode` private static helpers | n/a (pure) | no | | +| `SynchronizationContext.SetSynchronizationContext(...)` in `RibbonController` members (`ScrapeAndMineAsync`, `TriageAsync`, `SB`, `PromptUserToSelectFolder`, and others) | thread-local, not process-global | no | none of these members is invoked by a test in the class; `GetFolderTreeSnapshotAsync` (both overloads), `GetStats`, `IsHighConfidenceModeActive`, `ToggleHighConfidenceMode`, `SetHighConfidenceModeForLaunch`, `Get/SetHighConfidenceThresholdText`, and `Engines` do not touch it | +| `TaskMaster.Properties.Settings.Default` high-confidence members | yes | yes before P4-T10 (snapshot, restore, and setter saves) | removed by P4-T10: the tests inject `new AppQuickFilerSettings(store)` over a per-test `InMemorySettingsStore` | +| Other `Settings.Default` members | n/a | no | no test in the class reads or writes any other setting | +| Environment variables, UI-thread or dispatcher statics, singletons | none found | no | `RibbonFolderOperations_DoNotConstructThrowawayFolderTrees` reads a tracked source file read-only; it writes nothing | + +## Verdict + +REMOVE + +Rationale: after P4-T10, every test in `RibbonControllerTests` (both partial files) operates on per-test instances (`RibbonController`, an uninitialized `ApplicationGlobals`, its own `AppQuickFilerSettings` over its own `InMemorySettingsStore`, and Moq mocks). No mutable process-global static state is written, so the class-level `[DoNotParallelize]` has no remaining reason. diff --git a/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/ac-reconciliation.2026-10-22T16-38.md b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/ac-reconciliation.2026-10-22T16-38.md new file mode 100644 index 000000000..c0030ac99 --- /dev/null +++ b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/ac-reconciliation.2026-10-22T16-38.md @@ -0,0 +1,28 @@ +# Acceptance-Criteria Reconciliation (P7-T10) + +Timestamp: 2026-10-22T16-38 +Command: compare each `- [ ] ACn:` / `- [x] ACn:` line of /spec.md, with the checkbox marker normalized, against the same line of the SCRATCH copy issue-1005-spec-baseline.md taken at P0-T1; count checked and unchecked AC lines +EXIT_CODE: 0 +Output Summary: 8 checked, 0 unchecked. All 8 criterion texts are identical to the P0-T1 copy after normalizing the checkbox marker, and every non-AC line of spec.md is identical to the copy. + +### Acceptance Criteria Status +- Source: `docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/spec.md` (Work Mode full-bug) +- Total AC items: 8 +- Checked off (delivered): 8 +- Remaining (unchecked): 0 +- Items remaining: none + +## Per-criterion record + +| AC | Text identical to P0-T1 copy | Checked by | Evidence | +| --- | --- | --- | --- | +| AC1 | yes | P5-T3 | `regression-testing/dfdeedle-pass-after.2026-10-22T16-26.md`; `UtilitiesCS.Test/Extensions/DfDeedle_COM_Tests.QfcColumnsAsync.cs` | +| AC2 | yes | P3-T5 | `regression-testing/idlist-probe-pass-after.2026-10-22T16-18.md`; `ToDoModel/Data Model/ID/IDList.cs` | +| AC3 | yes | P3-T6 | `regression-testing/idlist-probe-pass-after.2026-10-22T16-18.md` (zero probe saves, one mock `Save()`) | +| AC4 | yes | P6-T5 | `regression-testing/ac4-search-before.2026-10-22T16-12.md`, `regression-testing/ac4-search-after.2026-10-22T16-27.md` | +| AC5 | yes | P6-T6 | `regression-testing/settings-probe-fail-before.2026-10-22T16-15.md`, `regression-testing/fail-before-exception.2026-10-22T16-15.md`, `regression-testing/idlist-probe-pass-after.2026-10-22T16-18.md`, `regression-testing/taskmaster-settings-probe-pass-after.2026-10-22T16-25.md` | +| AC6 | yes | P7-T8 | `qa-gates/final-toolchain-tests.2026-10-22T16-35.md`, `qa-gates/parallel-repeat-runs.2026-10-22T16-30.md`, `qa-gates/parallel-constraints-scan.2026-10-22T16-28.md` | +| AC7 | yes | P7-T9 | `qa-gates/final-toolchain-format.2026-10-22T16-32.md`, `qa-gates/final-toolchain-analyzers.2026-10-22T16-32.md`, `qa-gates/final-toolchain-typecheck.2026-10-22T16-33.md`, `qa-gates/final-toolchain-tests.2026-10-22T16-35.md`, `qa-gates/coverage-comparison.2026-10-22T16-36.md`, `qa-gates/file-sizes.2026-10-22T16-37.md` | +| AC8 | yes | P4-T12 | `other/ribbon-static-state-check.2026-10-22T16-23.md` (verdict REMOVE); P4-T5 and P4-T10 removed both class-level attributes | + +The comparison base is the P0-T1 `SCRATCH` copy because `spec.md` was untracked at planning time. diff --git a/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/coverage-comparison.2026-10-22T16-36.md b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/coverage-comparison.2026-10-22T16-36.md new file mode 100644 index 000000000..7741996c9 --- /dev/null +++ b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/coverage-comparison.2026-10-22T16-36.md @@ -0,0 +1,80 @@ +# Coverage Comparison (P7-T5) + +Timestamp: 2026-10-22T16-36 +Command: pwsh -NoProfile -File SCRATCH\issue-1005-coverage-compare.ps1 -MergeBase (reads SCRATCH issue-1005-baseline-cobertura.xml and SCRATCH issue-1005-final-cobertura.xml, both post-processed Cobertura documents, and git diff -U0 per modified production file) +EXIT_CODE: 0 +Output Summary: PASS, no shortfall. Repository line 86.30% -> 86.37% and branch 80.58% -> 80.66% (floors 80 / 75). ApplicationSettingsStore.cs 100% (7/7); AppAutoFileObjects.ScalarSettings.cs 100% (36/36). Every new or changed seam member in IDList, AppQuickFilerSettings, AppStagingFilenames, and the AppAutoFileObjects scalar partial is at 100% line coverage. Changed-line coverage is 100% in every modified production file, and no figure is below the baseline coverage of the lines it replaced. + +Sources: baseline = `SCRATCH` `issue-1005-baseline-cobertura.xml` (P0-T7, merge-base code); final = `SCRATCH` `issue-1005-final-cobertura.xml` (P7-T4). Both are the post-processed Cobertura documents written by `Invoke-MSTestWithCoverage.ps1`; per-member figures come from Cobertura `method` elements, which the committed package-level JaCoCo projection does not carry. MERGE-BASE is recorded in `evidence/baseline/merge-base-scope.2026-10-22T16-06.md`. + +## Repository figures + +| Metric | Baseline | Final | Floor | Result | +| --- | --- | --- | --- | --- | +| Line | 86.30% (57542/66677) | 86.37% (57627/66720) | 80% | PASS, not below baseline | +| Branch | 80.58% (14036/17419) | 80.66% (14060/17431) | 75% | PASS, not below baseline | + +## New files (line coverage) + +| File | Final | Threshold | Result | +| --- | --- | --- | --- | +| `UtilitiesCS/HelperClasses/Settings/ApplicationSettingsStore.cs` | 100.00% (7/7) | 90% | PASS | +| `TaskMaster/AppGlobals/AppAutoFileObjects.ScalarSettings.cs` | 100.00% (36/36) | 90% | PASS | +| `UtilitiesCS/HelperClasses/Settings/ISettingsStore.cs` | not instrumented (interface only, no executable lines) | n/a | n/a | + +## New or changed seam members (line coverage, final) + +| File | Member | Covered / Lines | % | +| --- | --- | --- | --- | +| `ApplicationSettingsStore.cs` | `.ctor(ApplicationSettingsBase)` | 4/4 | 100 | +| `ApplicationSettingsStore.cs` | `get_Item(string)` | 1/1 | 100 | +| `ApplicationSettingsStore.cs` | `set_Item(string, object)` | 1/1 | 100 | +| `ApplicationSettingsStore.cs` | `Save()` | 1/1 | 100 | +| `IDList.cs` | `.ctor(IEnumerable, ISettingsStore)` (new) | 9/9 | 100 | +| `IDList.cs` | `.ctor(ISettingsStore)` (new) | 9/9 | 100 | +| `IDList.cs` | `GetNextToDoID(string)` (changed) | 28/28 | 100 | +| `AppQuickFilerSettings.cs` | `.ctor()` (new) | 1/1 | 100 | +| `AppQuickFilerSettings.cs` | `.ctor(ISettingsStore)` (new) | 5/5 | 100 | +| `AppQuickFilerSettings.cs` | getters `MoveEntireConversation`, `SaveAttachments`, `SavePictures`, `SaveEmailCopy`, `HighConfidenceModeEnabled`, `HighConfidenceThreshold` | 1/1 each | 100 | +| `AppQuickFilerSettings.cs` | setters of the same six properties | 4/4 each | 100 | +| `AppStagingFilenames.cs` | `.ctor()` (new) | 1/1 | 100 | +| `AppStagingFilenames.cs` | `.ctor(ISettingsStore)` (new) | 5/5 | 100 | +| `AppStagingFilenames.cs` | getters of all ten properties | 1/1 to 5/5 each | 100 | +| `AppStagingFilenames.cs` | nine saving setters | 5/5 each | 100 | +| `AppStagingFilenames.cs` | `EmailInfoStagingFile` setter | 1/1 | 100 | +| `AppStagingFilenames.cs` | `Read(string)` (new helper), `InitProp` | 1/1, 4/4 | 100 | +| `AppAutoFileObjects.ScalarSettings.cs` | `.ctor(IApplicationGlobals, ISettingsStore)` (new) | 6/6 | 100 | +| `AppAutoFileObjects.ScalarSettings.cs` | getters of `LngConvCtPwr`, `Conversation_Weight`, `SmithWatterman_MatchScore`, `SmithWatterman_MismatchScore`, `SmithWatterman_GapPenalty`, `MaxRecents` | 1/1 each | 100 | +| `AppAutoFileObjects.ScalarSettings.cs` | setters of the same six properties | 4/4 each | 100 | +| `AppAutoFileObjects.cs` | `.ctor(IApplicationGlobals)` (changed to chain) | 1/1 | 100 | + +## Changed-line coverage per modified production file + +Added or modified lines come from `git diff -U0 ` (whole file for new files); "instrumented" counts those lines that carry a Cobertura line entry. Replaced lines are the removed or modified lines of the same diff, measured in the baseline Cobertura document. + +| File | Added/modified lines | Instrumented | Covered | Changed-line % | Replaced lines instrumented | Replaced covered (baseline) | Baseline % | Result | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | +| `ToDoModel/Data Model/ID/IDList.cs` | 36 | 15 | 15 | 100.00 | 2 | 2 | 100.00 | PASS | +| `TaskMaster/AppGlobals/AppQuickFilerSettings.cs` | 39 | 24 | 24 | 100.00 | 18 | 18 | 100.00 | PASS | +| `TaskMaster/AppGlobals/AppStagingFilenames.cs` | 61 | 42 | 42 | 100.00 | 28 | 7 | 25.00 | PASS | +| `TaskMaster/AppGlobals/AppAutoFileObjects.cs` | 1 | 1 | 1 | 100.00 | 33 | 33 | 100.00 | PASS | +| `TaskMaster/AppGlobals/AppAutoFileObjects.ScalarSettings.cs` (new) | 88 | 36 | 36 | 100.00 | 0 | 0 | n/a | PASS | +| `UtilitiesCS/HelperClasses/Settings/ApplicationSettingsStore.cs` (new) | 38 | 7 | 7 | 100.00 | 0 | 0 | n/a | PASS | +| `UtilitiesCS/HelperClasses/Settings/ISettingsStore.cs` (new) | 25 | 0 | 0 | n/a | 0 | 0 | n/a | n/a | + +## Per-file context (not a gate) + +| File | Baseline | Final | +| --- | --- | --- | +| `ToDoModel/Data Model/ID/IDList.cs` | 24.36% (38/156) | 26.04% (44/169) | +| `TaskMaster/AppGlobals/AppQuickFilerSettings.cs` | 100.00% (30/30) | 100.00% (36/36) | +| `TaskMaster/AppGlobals/AppStagingFilenames.cs` | 30.77% (20/65) | 100.00% (79/79) | +| `TaskMaster/AppGlobals/AppAutoFileObjects.cs` | 45.66% (184/403) | 40.81% (151/370) | + +The `AppAutoFileObjects.cs` file figure falls because the six fully covered scalar properties (33 covered lines) moved to `AppAutoFileObjects.ScalarSettings.cs`, which is at 100%; no line that remains in `AppAutoFileObjects.cs` lost coverage, and its one changed line is covered. `IDList.cs` existing Outlook-bound and deserializing constructors remain uncovered as at baseline; their source is unchanged apart from sharing the new field initializer, whose lines are covered. + +## No exclusion added + +No `[ExcludeFromCodeCoverage]` was added (`parallel-constraints-scan.2026-10-22T16-28.md`: 0 added lines) and `coverage.config` is unchanged. + +Result: PASS. No shortfall; not remediation-required. diff --git a/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/coverage-comparison.2026-10-22T17-12.md b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/coverage-comparison.2026-10-22T17-12.md new file mode 100644 index 000000000..ebc578fd3 --- /dev/null +++ b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/coverage-comparison.2026-10-22T17-12.md @@ -0,0 +1,19 @@ +Timestamp: 2026-10-22T17-12 +Command: Read artifacts/csharp/coverage.xml (copy of the P2-T4 post-processed Cobertura); class ToDoModel.IDList (ToDoModel\Data Model\ID\IDList.cs) and method get_MaxLengthOfID +EXIT_CODE: 0 +Output Summary: IDList.cs line 30.18% and branch 25.81% now, above the merge-base 24.36% and 20.69%; get_MaxLengthOfID is covered again at line-rate 1 and branch-rate 1. + +Merge base: git merge-base of origin/main and HEAD. + +| State | IDList.cs line rate | IDList.cs branch rate | get_MaxLengthOfID | +| --- | --- | --- | --- | +| Merge base | 24.36% (38/156) | 20.69% | 7/7 lines, 4/4 branches | +| Reviewed head (before remediation) | 26.04% | 19.35% | line-rate 0, branch-rate 0 | +| Now (after remediation) | 30.18% (0.301775) | 25.81% (0.258065) | line-rate 1, branch-rate 1 (all lines and branches covered) | + +Result: line and branch rates are not below the merge-base rates; the branch regression recorded as finding B-1 is closed. + +Repository first-party coverage (post-processed Cobertura): lines 57634/66720 (86.38%), branches 14064/17431 (80.68%). +Package-level JaCoCo projection: generated by the run as coverage/coverage.cobertura.jacoco.xml (reconciled by the script); no figure beyond the Cobertura values above is included here. + +Tests added (ToDoModel.Test, IDListGetNextToDoIDTests): MaxLengthOfID_ColdCacheOnMixedLengthList_ReturnsLongestIdLength, MaxLengthOfID_ColdCacheOnSecondDistinctList_ReturnsItsOwnLongestIdLength, MaxLengthOfID_WarmCacheAfterGetNextToDoID_ReturnsCachedLengthWithoutSaving. diff --git a/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/evidence-form.2026-10-22T16-37.md b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/evidence-form.2026-10-22T16-37.md new file mode 100644 index 000000000..bbff41d12 --- /dev/null +++ b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/evidence-form.2026-10-22T16-37.md @@ -0,0 +1,34 @@ +# Evidence Form Check (P7-T7) + +Timestamp: 2026-10-22T16-37 +Command: git diff --name-status -- /evidence/ ; git status --porcelain --untracked-files=all -- /evidence/ ; content search of /evidence/ for a drive-rooted path (a letter, a colon, then a slash or backslash), a Users profile segment, the local account name and its 8.3 short form, and the per-user application data segment +EXIT_CODE: 0 +Output Summary: PASS. 31 evidence files (2 committed preflight records plus 29 untracked from this run). No path ends in .trx or .coverage; the only .xml files are baseline-coverage-projection.2026-10-22T16-10.jacoco.xml and final-coverage-projection.2026-10-22T16-35.jacoco.xml; no path contains "cobertura"; the drive-rooted and user-profile search returned 0 matches. + +## Listing + +Tracked changes since MERGE-BASE under `evidence/` (git diff --name-status): + +- `A evidence/other/preflight-fixes.2026-10-22T15-47.md` +- `A evidence/other/preflight-fixes.2026-10-22T16-01.md` + +Untracked under `evidence/` (git status --porcelain, 29 entries): + +- `baseline/`: `phase0-instructions-read.2026-10-22T16-05.md`, `baseline-toolchain-bootstrap.2026-10-22T16-06.md`, `merge-base-scope.2026-10-22T16-06.md`, `baseline-toolchain-format.2026-10-22T16-07.md`, `baseline-toolchain-analyzers.2026-10-22T16-07.md`, `baseline-toolchain-typecheck.2026-10-22T16-08.md`, `baseline-coverage-projection.2026-10-22T16-10.jacoco.xml`, `baseline-coverage.2026-10-22T16-10.md`, `baseline-file-sizes.2026-10-22T16-10.md`, `baseline-test-disposition.2026-10-22T16-11.md` +- `regression-testing/`: `ac4-search-before.2026-10-22T16-12.md`, `settings-probe-fail-before.2026-10-22T16-15.md`, `fail-before-exception.2026-10-22T16-15.md`, `settings-store-adapter-tests.2026-10-22T16-17.md`, `idlist-probe-pass-after.2026-10-22T16-18.md`, `taskmaster-settings-probe-pass-after.2026-10-22T16-25.md`, `dfdeedle-pass-after.2026-10-22T16-26.md`, `ac4-search-after.2026-10-22T16-27.md` +- `other/`: `ribbon-static-state-check.2026-10-22T16-23.md` +- `qa-gates/`: `parallel-constraints-scan.2026-10-22T16-28.md`, `scope-fence.2026-10-22T16-29.md`, `parallel-repeat-runs.2026-10-22T16-30.md`, `final-toolchain-format.2026-10-22T16-32.md`, `final-toolchain-analyzers.2026-10-22T16-32.md`, `final-toolchain-typecheck.2026-10-22T16-33.md`, `final-coverage-projection.2026-10-22T16-35.jacoco.xml`, `final-toolchain-tests.2026-10-22T16-35.md`, `coverage-comparison.2026-10-22T16-36.md`, `file-sizes.2026-10-22T16-37.md` + +(This artifact is the 30th file written by the run.) + +## Checks + +| Check | Result | +| --- | --- | +| Paths ending `.trx` | 0 | +| Paths ending `.coverage` | 0 | +| `.xml` files other than `baseline-coverage-projection.*.jacoco.xml` / `final-coverage-projection.*.jacoco.xml` | 0 | +| Paths containing `cobertura` | 0 | +| Drive-rooted path or user-profile segment in evidence content | 0 matches | + +Raw trx and post-processed Cobertura documents remain under the ignored `coverage/` folder and under `SCRATCH`; none is in the evidence tree. diff --git a/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/file-sizes.2026-10-22T16-37.md b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/file-sizes.2026-10-22T16-37.md new file mode 100644 index 000000000..2cd0c7f3c --- /dev/null +++ b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/file-sizes.2026-10-22T16-37.md @@ -0,0 +1,31 @@ +# Final File Sizes (P7-T6) + +Timestamp: 2026-10-22T16-37 +Command: @(Get-Content -LiteralPath ).Count for every created or modified .cs file +EXIT_CODE: 0 +Output Summary: PASS. 20 created or modified .cs files; every count is at most 500 except the two pre-existing oversize files, which shrank: UtilitiesCS.Test/Extensions/DfDeedle_COM_Tests.cs 848 -> 809 and TaskMaster/AppGlobals/AppAutoFileObjects.cs 853 -> 791. Largest other file: TaskTree.Test/TaskTreeControllerMoveLogicTests.cs 477. + +| File | P0-T8 | Final | Rule | Result | +| --- | --- | --- | --- | --- | +| `UtilitiesCS/HelperClasses/Settings/ISettingsStore.cs` | new | 25 | <= 500 | PASS | +| `UtilitiesCS/HelperClasses/Settings/ApplicationSettingsStore.cs` | new | 38 | <= 500 | PASS | +| `ToDoModel/Data Model/ID/IDList.cs` | 318 | 352 | <= 500 | PASS | +| `TaskMaster/AppGlobals/AppQuickFilerSettings.cs` | 68 | 88 | <= 500 | PASS | +| `TaskMaster/AppGlobals/AppStagingFilenames.cs` | 139 | 171 | <= 500 | PASS | +| `TaskMaster/AppGlobals/AppAutoFileObjects.cs` | 853 | 791 | below P0-T8 | PASS | +| `TaskMaster/AppGlobals/AppAutoFileObjects.ScalarSettings.cs` | new | 88 | <= 500 | PASS | +| `UtilitiesCS.Test/Extensions/DfDeedle_COM_Tests.cs` | 848 | 809 | below P0-T8 | PASS | +| `UtilitiesCS.Test/Extensions/DfDeedle_COM_Tests.QfcColumnsAsync.cs` | new | 70 | <= 500 | PASS | +| `UtilitiesCS.Test/HelperClasses/Settings/ApplicationSettingsStoreTests.cs` | new | 99 | <= 500 | PASS | +| `ToDoModel.Test/Data Model/ID/IDListGetNextToDoIDTests.cs` | 120 | 235 | <= 500 | PASS | +| `TaskTree.Test/TaskTreeControllerMoveLogicTests.cs` | 414 | 477 | <= 500 | PASS | +| `TaskTree.Test/TaskTreeControllerRouteDropTests.cs` | 220 | 224 | <= 500 | PASS | +| `TaskMaster.Test/AppGlobals/InMemorySettingsStore.cs` | new | 39 | <= 500 | PASS | +| `TaskMaster.Test/AppGlobals/SettingsSaveProbe.cs` | new | 75 | <= 500 | PASS | +| `TaskMaster.Test/AppGlobals/AppQuickFilerSettingsTests.cs` | 89 | 128 | <= 500 | PASS | +| `TaskMaster.Test/AppGlobals/AppQuickFilerSettingsRemainingPropertiesTests.cs` | 134 | 125 | <= 500 | PASS | +| `TaskMaster.Test/AppGlobals/AppStagingFilenamesTests.cs` | 146 | 268 | <= 500 | PASS | +| `TaskMaster.Test/AppGlobals/AppAutoFileObjectsCoverageExpansionTests.cs` | 274 | 335 | <= 500 | PASS | +| `TaskMaster.Test/Ribbon/RibbonControllerTests.cs` | 452 | 459 | <= 500 | PASS | + +`TaskMaster.Test/Ribbon/RibbonControllerTests.Engines.cs` (73 at P0-T8) is unchanged (Decision D-E). diff --git a/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/final-coverage-projection.2026-10-22T16-35.jacoco.xml b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/final-coverage-projection.2026-10-22T16-35.jacoco.xml new file mode 100644 index 000000000..988861d33 --- /dev/null +++ b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/final-coverage-projection.2026-10-22T16-35.jacoco.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/final-toolchain-analyzers.2026-10-22T16-32.md b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/final-toolchain-analyzers.2026-10-22T16-32.md new file mode 100644 index 000000000..e4206f37a --- /dev/null +++ b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/final-toolchain-analyzers.2026-10-22T16-32.md @@ -0,0 +1,11 @@ +# Final Toolchain: Analyzers (P7-T2) + +Timestamp: 2026-10-22T16-32 +Command: msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true +EXIT_CODE: 0 +Output Summary: `0 Warning(s)`, `0 Error(s)`. All 18 solution assemblies were recompiled (18 ` -> ` output lines). Warnings whose path is a created or modified Write Set file: none (0). + +## Notes + +- MSBuild resolved through vswhere (VS 18); normal-verbosity file log kept under `SCRATCH` (`issue-1005-final-analyze.log`), not committed. +- Same toolchain pass as `final-toolchain-format.2026-10-22T16-32.md` (clean pass 2). diff --git a/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/final-toolchain-format.2026-10-22T16-32.md b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/final-toolchain-format.2026-10-22T16-32.md new file mode 100644 index 000000000..ec1d9a231 --- /dev/null +++ b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/final-toolchain-format.2026-10-22T16-32.md @@ -0,0 +1,43 @@ +# Final Toolchain: Format (P7-T1, clean pass) + +Timestamp: 2026-10-22T16-32 +Command: dotnet tool run csharpier format . ; dotnet tool run csharpier check . +EXIT_CODE: 0 +Output Summary: Rewritten-file count 0 (no SHA-256 or porcelain-status difference across the 20 created or modified Write Set .cs files). `dotnet tool run csharpier format .` exit 0 (`Formatted 1682 files in 1610ms.`, a processed-file count). `dotnet tool run csharpier check .` exit 0 with `Checked 1682 files in 5402ms.` + +## Loop history + +- Pass 1 (2026-10-22T16-31): `csharpier format .` rewrote 8 Write Set files (new or rewritten code not yet in CSharpier layout): `TaskMaster/AppGlobals/AppQuickFilerSettings.cs`, `TaskMaster/AppGlobals/AppStagingFilenames.cs`, `TaskMaster/AppGlobals/AppAutoFileObjects.ScalarSettings.cs`, `UtilitiesCS.Test/HelperClasses/Settings/ApplicationSettingsStoreTests.cs`, `ToDoModel.Test/Data Model/ID/IDListGetNextToDoIDTests.cs`, `TaskMaster.Test/AppGlobals/AppQuickFilerSettingsTests.cs`, `TaskMaster.Test/AppGlobals/AppStagingFilenamesTests.cs`, `TaskMaster.Test/AppGlobals/AppAutoFileObjectsCoverageExpansionTests.cs`. Porcelain status (outside the feature folder) was identical before and after, so no file outside the Write Set was rewritten. Per the loop rule the pass was not clean and the loop restarted at P7-T1. +- Pass 2 (this artifact): clean. + +## Capture method + +- Before and after `csharpier format .`: SHA-256 of each created or modified Write Set `.cs` file (first 16 hex characters shown) and `git status --porcelain --untracked-files=all` excluding the feature folder. +- Rewritten-file count = number of hash differences plus status-line differences; the `Formatted N files` line counts processed files and is not used. + +## Pass 2 hashes (before = after) + +| File | SHA-256 (prefix) | +| --- | --- | +| `UtilitiesCS/HelperClasses/Settings/ISettingsStore.cs` | 692E65F3313C71B6 | +| `UtilitiesCS/HelperClasses/Settings/ApplicationSettingsStore.cs` | F53E02304365CCE4 | +| `ToDoModel/Data Model/ID/IDList.cs` | D44C2D2056506A8F | +| `TaskMaster/AppGlobals/AppQuickFilerSettings.cs` | 51084C67F5731493 | +| `TaskMaster/AppGlobals/AppStagingFilenames.cs` | 7DDF2808A7F241CF | +| `TaskMaster/AppGlobals/AppAutoFileObjects.cs` | D32AEFBF3B606F15 | +| `TaskMaster/AppGlobals/AppAutoFileObjects.ScalarSettings.cs` | 0742DC4382D22202 | +| `UtilitiesCS.Test/Extensions/DfDeedle_COM_Tests.cs` | 933964BF66821ABC | +| `UtilitiesCS.Test/Extensions/DfDeedle_COM_Tests.QfcColumnsAsync.cs` | 84BA7E24A8E81E81 | +| `UtilitiesCS.Test/HelperClasses/Settings/ApplicationSettingsStoreTests.cs` | A6F44156BB2877BE | +| `ToDoModel.Test/Data Model/ID/IDListGetNextToDoIDTests.cs` | 96B8FAF707151828 | +| `TaskTree.Test/TaskTreeControllerMoveLogicTests.cs` | A387A9A0BCBA21F6 | +| `TaskTree.Test/TaskTreeControllerRouteDropTests.cs` | 67A6A8F90EC608E6 | +| `TaskMaster.Test/AppGlobals/InMemorySettingsStore.cs` | 980F166F45224CCB | +| `TaskMaster.Test/AppGlobals/SettingsSaveProbe.cs` | 6AFC935413ABCE8E | +| `TaskMaster.Test/AppGlobals/AppQuickFilerSettingsTests.cs` | 8CE52C97FC96B8DB | +| `TaskMaster.Test/AppGlobals/AppQuickFilerSettingsRemainingPropertiesTests.cs` | 396C7E3D4A0E23EF | +| `TaskMaster.Test/AppGlobals/AppStagingFilenamesTests.cs` | 7DF40E39B53CE574 | +| `TaskMaster.Test/AppGlobals/AppAutoFileObjectsCoverageExpansionTests.cs` | 246DD84722A56784 | +| `TaskMaster.Test/Ribbon/RibbonControllerTests.cs` | F29EE6C28F5F0A8D | + +Porcelain status (outside the feature folder): 24 lines before and 24 lines after, no difference. diff --git a/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/final-toolchain-tests.2026-10-22T16-35.md b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/final-toolchain-tests.2026-10-22T16-35.md new file mode 100644 index 000000000..8d17e5700 --- /dev/null +++ b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/final-toolchain-tests.2026-10-22T16-35.md @@ -0,0 +1,76 @@ +# Final Toolchain: Tests with Coverage (P7-T4) + +Timestamp: 2026-10-22T16-35 +Command: pwsh -NoProfile -File \scripts\vscode\Invoke-MSTestWithCoverage.ps1 +EXIT_CODE: 0 +Output Summary: Test Run Successful. Total 7618, executed 7618, passed 7618, failed 0 (trx summary) under the class-level parallel runsettings. First-party coverage: lines 57627/66720 (86.37%), branches 14060/17431 (80.66%). All 4 probe-set tests and all 21 register tests are Passed, plus both converted DfDeedle tests. + +## Runner route + +- Route: the unscoped script `scripts/vscode/Invoke-MSTestWithCoverage.ps1`, invoked through PowerShell 7 with an absolute script path. It runs `dotnet-coverage collect` around `vstest.console.exe` over every discovered `*.Test.dll`, applying `/Settings:scripts/vscode/TaskMaster.cli.runsettings` (MSTest `Workers` 0, reported as 24 workers, `Scope` ClassLevel), `/InIsolation`, `TestCategory!=LiveOutlook`, and the `\.claude\` worktree exclusion. +- Runsettings path: `scripts/vscode/TaskMaster.cli.runsettings`. +- Shell-icon carve-out: not used. The run did not stall (test time 34.9 s, script elapsed 95 s); the four shell-icon classes ran and passed. +- Same toolchain pass as `final-toolchain-format.2026-10-22T16-32.md`, `final-toolchain-analyzers.2026-10-22T16-32.md`, and `final-toolchain-typecheck.2026-10-22T16-33.md`. + +## Trx summary (derived from the trx; raw trx not committed) + +``` +Test run outcome: Completed +Total 7618, executed 7618, passed 7618, failed 0. +Skipped 0, derived as total minus executed rather than reported by the test platform. +Figures reported verbatim by the test platform: error 0, timeout 0, aborted 0, notExecuted 0, inconclusive 0. +Failed tests: none +``` + +Baseline comparison: 7599 at P0-T7; 7618 now (19 new register tests: 4 adapter, 7 IDList, 2 AppQuickFilerSettings, 4 AppStagingFilenames, 2 AppAutoFileObjects). + +## One-line first-party coverage summary + +``` +First-party coverage: lines 57627/66720 (86.37%), branches 14060/17431 (80.66%) +``` + +## Probe-set tests + +| Test | Outcome | +| --- | --- | +| `TaskTreeControllerMoveLogicTests.MoveObjectsToChildren_DesyncedRoot_FiresMessageSeam` | Passed | +| `AppQuickFilerSettingsTests.HighConfidenceModeEnabled_WhenSetTrue_ReadsBackTrue` | Passed | +| `AppStagingFilenamesTests.ConditionalReminders_Setter_RoundTripsThroughSettingsDefault` | Passed | +| `AppAutoFileObjectsCoverageExpansionTests.ScalarSettingsProperties_WhenRoundTripped_UpdateDefaultsAndReturnValues` | Passed | + +## Register tests + +| Test | Outcome | +| --- | --- | +| `ApplicationSettingsStoreTests.Constructor_NullSettings_ThrowsArgumentNullException` | Passed | +| `ApplicationSettingsStoreTests.Indexer_Get_ReturnsWrappedSettingsValue` | Passed | +| `ApplicationSettingsStoreTests.Indexer_Set_WritesWrappedSettingsValue` | Passed | +| `ApplicationSettingsStoreTests.Save_Invoked_ForwardsToWrappedSettingsOnce` | Passed | +| `IDListGetNextToDoIDTests.GetNextToDoID_FirstIdOnFreshList_AssignsLengthAndSavesOnce` | Passed | +| `IDListGetNextToDoIDTests.GetNextToDoID_IdNotLongerThanCachedLength_DoesNotSaveAgain` | Passed | +| `IDListGetNextToDoIDTests.GetNextToDoID_LongerIdAfterFirst_SavesAgain` | Passed | +| `IDListGetNextToDoIDTests.Constructor_EnumerableWithNullStore_ThrowsArgumentNullException` | Passed | +| `IDListGetNextToDoIDTests.Constructor_NullStoreOnly_ThrowsArgumentNullException` | Passed | +| `IDListGetNextToDoIDTests.Constructor_StoreOnly_SavesThroughInjectedStore` | Passed | +| `IDListGetNextToDoIDTests.Constructor_Default_UsesApplicationSettingsStore` | Passed | +| `AppQuickFilerSettingsTests.Constructor_Parameterless_UsesApplicationSettingsStore` | Passed | +| `AppQuickFilerSettingsTests.Constructor_NullSettingsStore_ThrowsArgumentNullException` | Passed | +| `AppStagingFilenamesTests.SavingSetters_EachAssignment_WritesStoreAndSavesOnce` | Passed | +| `AppStagingFilenamesTests.Getters_UnsetFields_ReadSeededStoreValues` | Passed | +| `AppStagingFilenamesTests.Constructor_Parameterless_UsesApplicationSettingsStore` | Passed | +| `AppStagingFilenamesTests.Constructor_NullSettingsStore_ThrowsArgumentNullException` | Passed | +| `AppAutoFileObjectsCoverageExpansionTests.Constructor_ParentOnly_UsesApplicationSettingsStore` | Passed | +| `AppAutoFileObjectsCoverageExpansionTests.Constructor_NullSettingsStore_ThrowsArgumentNullException` | Passed | + +## Converted DfDeedle tests + +| Test | Outcome | +| --- | --- | +| `DfDeedle_COM_Tests.AddQfcColumnsAsync_HappyPath_CompletesWithoutThrowing` | Passed | +| `DfDeedle_COM_Tests.AddQfcColumnsAsync_PreCancelledToken_CompletesGracefully` | Passed | + +## Artifacts + +- JaCoCo projection copy: `evidence/qa-gates/final-coverage-projection.2026-10-22T16-35.jacoco.xml`. +- Post-processed Cobertura copy: `SCRATCH` `issue-1005-final-cobertura.xml` (not committed). Trx copy: `SCRATCH` `issue-1005-final.trx` (not committed). diff --git a/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/final-toolchain-typecheck.2026-10-22T16-33.md b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/final-toolchain-typecheck.2026-10-22T16-33.md new file mode 100644 index 000000000..cf7ca516e --- /dev/null +++ b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/final-toolchain-typecheck.2026-10-22T16-33.md @@ -0,0 +1,6 @@ +# Final Toolchain: Type Check (P7-T3) + +Timestamp: 2026-10-22T16-33 +Command: msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true +EXIT_CODE: 0 +Output Summary: `0 Error(s)`, `0 Warning(s)`. No `/p:Nullable=enable`. The new UtilitiesCS and UtilitiesCS.Test files carry `#nullable enable` and produce no CS86xx diagnostic. Normal-verbosity log kept under `SCRATCH` (`issue-1005-final-typecheck.log`), not committed. Same toolchain pass as `final-toolchain-format.2026-10-22T16-32.md`. diff --git a/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/parallel-constraints-scan.2026-10-22T16-28.md b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/parallel-constraints-scan.2026-10-22T16-28.md new file mode 100644 index 000000000..0ff785cc2 --- /dev/null +++ b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/parallel-constraints-scan.2026-10-22T16-28.md @@ -0,0 +1,46 @@ +# Parallel-Constraints Scan (P6-T2) + +Timestamp: 2026-10-22T16-28 +Command: git diff -- "*.cs" "*.runsettings" "*.config" (added lines) plus every untracked .cs file from git status --porcelain --untracked-files=all, scanned with a case-sensitive substring match per token; git diff --name-only and git status --porcelain over coverage.config, TaskMaster.runsettings, scripts/vscode/TaskMaster.cli.runsettings +EXIT_CODE: 0 +Output Summary: Every token has an added-line count of 0 in the diff and 0 in the 7 untracked .cs files. Removal lines: DoNotParallelize 2 (the class-level attributes removed from AppQuickFilerSettingsTests and RibbonControllerTests), all others 0. coverage.config, TaskMaster.runsettings, and scripts/vscode/TaskMaster.cli.runsettings are unchanged (empty diff and empty status). + +MERGE-BASE: computed by `git merge-base origin/main HEAD` after `git fetch origin main` (recorded in `evidence/baseline/merge-base-scope.2026-10-22T16-06.md`). + +## Scope scanned + +- Diff added lines: 744 (removed lines: 389) across tracked `*.cs`, `*.runsettings`, `*.config`. +- Untracked `.cs` files (431 lines): + - `TaskMaster.Test/AppGlobals/InMemorySettingsStore.cs` + - `TaskMaster.Test/AppGlobals/SettingsSaveProbe.cs` + - `TaskMaster/AppGlobals/AppAutoFileObjects.ScalarSettings.cs` + - `UtilitiesCS.Test/Extensions/DfDeedle_COM_Tests.QfcColumnsAsync.cs` + - `UtilitiesCS.Test/HelperClasses/Settings/ApplicationSettingsStoreTests.cs` + - `UtilitiesCS/HelperClasses/Settings/ApplicationSettingsStore.cs` + - `UtilitiesCS/HelperClasses/Settings/ISettingsStore.cs` + +## Token counts + +| Token | Added (diff) | Untracked files | Removed (reported separately, allowed) | +| --- | --- | --- | --- | +| `DoNotParallelize` | 0 | 0 | 2 | +| `Workers` | 0 | 0 | 0 | +| `Retry` | 0 | 0 | 0 | +| `Thread.Sleep` | 0 | 0 | 0 | +| `Task.Delay` | 0 | 0 | 0 | +| `Timeout` | 0 | 0 | 0 | +| `GetTempPath` | 0 | 0 | 0 | +| `GetTempFileName` | 0 | 0 | 0 | +| `user.config` | 0 | 0 | 0 | +| `SettingsProvider` | 0 | 0 | 0 | +| `ExcludeFromCodeCoverage` | 0 | 0 | 0 | + +## Configuration files + +| File | Diff vs MERGE-BASE | Porcelain status | +| --- | --- | --- | +| `coverage.config` | none | clean | +| `TaskMaster.runsettings` | none | clean | +| `scripts/vscode/TaskMaster.cli.runsettings` | none | clean | + +Result: PASS. Every added-line count is zero. diff --git a/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/parallel-repeat-runs.2026-10-22T16-30.md b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/parallel-repeat-runs.2026-10-22T16-30.md new file mode 100644 index 000000000..412b9f19e --- /dev/null +++ b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/parallel-repeat-runs.2026-10-22T16-30.md @@ -0,0 +1,42 @@ +# Parallel Repeat Runs (P6-T4) + +Timestamp: 2026-10-22T16-30 +Command: vstest.console.exe UtilitiesCS.Test\bin\Debug\UtilitiesCS.Test.dll ToDoModel.Test\bin\Debug\ToDoModel.Test.dll TaskTree.Test\bin\Debug\TaskTree.Test.dll TaskMaster.Test\bin\Debug\TaskMaster.Test.dll /Settings:scripts\vscode\TaskMaster.cli.runsettings /InIsolation "/TestCaseFilter:FullyQualifiedName~DfDeedle_COM_Tests&TestCategory!=LiveOutlook|FullyQualifiedName~ApplicationSettingsStoreTests&TestCategory!=LiveOutlook|FullyQualifiedName~IDListGetNextToDoIDTests&TestCategory!=LiveOutlook|FullyQualifiedName~TaskTreeControllerMoveLogicTests&TestCategory!=LiveOutlook|FullyQualifiedName~TaskTreeControllerRouteDropTests&TestCategory!=LiveOutlook|FullyQualifiedName~AppQuickFilerSettingsTests&TestCategory!=LiveOutlook|FullyQualifiedName~AppQuickFilerSettingsRemainingPropertiesTests&TestCategory!=LiveOutlook|FullyQualifiedName~AppStagingFilenamesTests&TestCategory!=LiveOutlook|FullyQualifiedName~AppAutoFileObjectsCoverageExpansionTests&TestCategory!=LiveOutlook|FullyQualifiedName~RibbonControllerTests&TestCategory!=LiveOutlook" /ResultsDirectory:SCRATCH\issue-1005-results-p6t4-combined- "/Logger:trx;LogFileName=issue-1005-p6t4-combined-.trx" (5 times), then one invocation per class in reverse order with the same runsettings and /InIsolation +EXIT_CODE: 0 +Output Summary: 15 runs, 0 failures. Five consecutive combined runs of the ten affected classes in one vstest invocation: 116 passed, 0 failed each. Ten solo runs, one class per process, in reverse register order: every run passed with 4 to 26 tests matched. AddQfcColumnsAsync_HappyPath_CompletesWithoutThrowing elapsed 2.1 ms to 2.7 ms across the six runs that include DfDeedle_COM_Tests (no wall-clock deadline). + +## Configuration + +- Runsettings: `scripts/vscode/TaskMaster.cli.runsettings` (MSTest `Workers` 0, `Scope` ClassLevel: classes run in parallel within each assembly). +- Isolation: `/InIsolation` on every invocation. +- Preceding build (ANALYZE, once): `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true`, EXIT_CODE 0, `0 Warning(s)`, `0 Error(s)`. +- Raw trx documents are kept under `SCRATCH` and are not committed; the rows below are derived from them. + +## Combined runs (all ten classes, one vstest invocation) + +Class set: `DfDeedle_COM_Tests`, `ApplicationSettingsStoreTests`, `IDListGetNextToDoIDTests`, `TaskTreeControllerMoveLogicTests`, `TaskTreeControllerRouteDropTests`, `AppQuickFilerSettingsTests`, `AppQuickFilerSettingsRemainingPropertiesTests`, `AppStagingFilenamesTests`, `AppAutoFileObjectsCoverageExpansionTests`, `RibbonControllerTests`. + +| Run | Exit | Total | Passed | Failed | `AddQfcColumnsAsync_HappyPath_CompletesWithoutThrowing` elapsed | +| --- | --- | --- | --- | --- | --- | +| 1 | 0 | 116 | 116 | 0 | 00:00:00.0023057 | +| 2 | 0 | 116 | 116 | 0 | 00:00:00.0023350 | +| 3 | 0 | 116 | 116 | 0 | 00:00:00.0021357 | +| 4 | 0 | 116 | 116 | 0 | 00:00:00.0026975 | +| 5 | 0 | 116 | 116 | 0 | 00:00:00.0025364 | + +## Solo runs (one class per vstest invocation, reverse register order) + +| Order | Class | Assembly | Exit | Total | Passed | Failed | Happy-path elapsed | +| --- | --- | --- | --- | --- | --- | --- | --- | +| 1 | `RibbonControllerTests` | TaskMaster.Test | 0 | 17 | 17 | 0 | n/a | +| 2 | `AppAutoFileObjectsCoverageExpansionTests` | TaskMaster.Test | 0 | 10 | 10 | 0 | n/a | +| 3 | `AppStagingFilenamesTests` | TaskMaster.Test | 0 | 10 | 10 | 0 | n/a | +| 4 | `AppQuickFilerSettingsRemainingPropertiesTests` | TaskMaster.Test | 0 | 6 | 6 | 0 | n/a | +| 5 | `AppQuickFilerSettingsTests` | TaskMaster.Test | 0 | 6 | 6 | 0 | n/a | +| 6 | `TaskTreeControllerRouteDropTests` | TaskTree.Test | 0 | 7 | 7 | 0 | n/a | +| 7 | `TaskTreeControllerMoveLogicTests` | TaskTree.Test | 0 | 17 | 17 | 0 | n/a | +| 8 | `IDListGetNextToDoIDTests` | ToDoModel.Test | 0 | 13 | 13 | 0 | n/a | +| 9 | `ApplicationSettingsStoreTests` | UtilitiesCS.Test | 0 | 4 | 4 | 0 | n/a | +| 10 | `DfDeedle_COM_Tests` | UtilitiesCS.Test | 0 | 26 | 26 | 0 | 00:00:00.0023218 | + +Result: PASS. Zero failures in all fifteen runs, and every run matched at least one test. diff --git a/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/remediation-analyzers.2026-10-22T17-09.md b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/remediation-analyzers.2026-10-22T17-09.md new file mode 100644 index 000000000..ea975db5c --- /dev/null +++ b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/remediation-analyzers.2026-10-22T17-09.md @@ -0,0 +1,4 @@ +Timestamp: 2026-10-22T17-09 +Command: msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true +EXIT_CODE: 0 +Output Summary: 0 Warning(s), 0 Error(s). Build log under SCRATCH. diff --git a/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/remediation-format.2026-10-22T17-09.md b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/remediation-format.2026-10-22T17-09.md new file mode 100644 index 000000000..213a6b449 --- /dev/null +++ b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/remediation-format.2026-10-22T17-09.md @@ -0,0 +1,4 @@ +Timestamp: 2026-10-22T17-09 +Command: dotnet tool run csharpier format . ; dotnet tool run csharpier check . +EXIT_CODE: 0 +Output Summary: Final pass: format printed "Formatted 1682 files" and left the tree unchanged (git status showed only the intended test file as modified); check printed "Checked 1682 files" with exit 0 and no diff report. diff --git a/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/remediation-scope.2026-10-22T17-12.md b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/remediation-scope.2026-10-22T17-12.md new file mode 100644 index 000000000..fd6fdccd1 --- /dev/null +++ b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/remediation-scope.2026-10-22T17-12.md @@ -0,0 +1,7 @@ +Timestamp: 2026-10-22T17-12 +Command: git diff (merge base of origin/main and HEAD) --stat -- ToDoModel/ UtilitiesCS/ TaskMaster/ ; git status --porcelain +EXIT_CODE: 0 +Output Summary: +- git status --porcelain: the only modified tracked file is ToDoModel.Test/Data Model/ID/IDListGetNextToDoIDTests.cs; all other entries are untracked files under the feature folder (evidence and the remediation plan). Zero production files changed by this remediation (working tree has no modified file under ToDoModel/, UtilitiesCS/, or TaskMaster/). +- The stat against the merge base lists nine production files; those are the already-committed changes of the reviewed branch commits (HEAD), not this remediation. +- No .trx, .coverage, or Cobertura file is untracked-to-add: coverage/ and artifacts/csharp/coverage.xml are gitignored. diff --git a/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/remediation-tests.2026-10-22T17-12.md b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/remediation-tests.2026-10-22T17-12.md new file mode 100644 index 000000000..763e66f35 --- /dev/null +++ b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/remediation-tests.2026-10-22T17-12.md @@ -0,0 +1,7 @@ +Timestamp: 2026-10-22T17-12 +Command: pwsh -NoProfile -File scripts/vscode/Invoke-MSTestWithCoverage.ps1 +EXIT_CODE: 0 +Output Summary: +- Test-result summary (derived from trx): total 7621, executed 7621, passed 7621, failed 0, skipped 0. +- First-party coverage: lines 57634/66720 (86.38%), branches 14064/17431 (80.68%). Floors 80% line and 75% branch met. +- No test filter exclusion was needed; the shell-icon classes did not hang. Runsettings TaskMaster.cli.runsettings (parallel, ClassLevel), /InIsolation, .claude exclusion applied by the script. diff --git a/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/remediation-typecheck.2026-10-22T17-09.md b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/remediation-typecheck.2026-10-22T17-09.md new file mode 100644 index 000000000..e2110d763 --- /dev/null +++ b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/remediation-typecheck.2026-10-22T17-09.md @@ -0,0 +1,4 @@ +Timestamp: 2026-10-22T17-09 +Command: msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true +EXIT_CODE: 0 +Output Summary: 0 Warning(s), 0 Error(s). No /p:Nullable=enable used. Build log under SCRATCH. diff --git a/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/scope-fence.2026-10-22T16-29.md b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/scope-fence.2026-10-22T16-29.md new file mode 100644 index 000000000..aee9d6adf --- /dev/null +++ b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/scope-fence.2026-10-22T16-29.md @@ -0,0 +1,54 @@ +# Scope Fence (P6-T3) + +Timestamp: 2026-10-22T16-29 +Command: git diff --name-status ; git status --porcelain --untracked-files=all +EXIT_CODE: 0 +Output Summary: PASS. Every listed path is a spec Write Set file (including TaskMaster.Test/AppGlobals/SettingsSaveProbe.cs, Decision D-A), a path under the feature folder, or a pre-existing documentation path already listed in merge-base-scope.2026-10-22T16-06.md (.claude/agent-memory/** and docs/features/potential/promoted/**). ToDoModel.Test/ToDoModel.Test.csproj and TaskMaster.Test/Ribbon/RibbonControllerTests.Engines.cs are absent. No other .claude/ path and no file under UtilitiesCS/Extensions/ appears. + +MERGE-BASE: recorded in `evidence/baseline/merge-base-scope.2026-10-22T16-06.md`. + +## Code and project paths (tracked changes plus untracked) + +| Path | Status | Classification | +| --- | --- | --- | +| `UtilitiesCS/HelperClasses/Settings/ISettingsStore.cs` | untracked (new) | Write Set | +| `UtilitiesCS/HelperClasses/Settings/ApplicationSettingsStore.cs` | untracked (new) | Write Set | +| `ToDoModel/Data Model/ID/IDList.cs` | M | Write Set | +| `TaskMaster/AppGlobals/AppQuickFilerSettings.cs` | M | Write Set | +| `TaskMaster/AppGlobals/AppStagingFilenames.cs` | M | Write Set | +| `TaskMaster/AppGlobals/AppAutoFileObjects.cs` | M | Write Set | +| `TaskMaster/AppGlobals/AppAutoFileObjects.ScalarSettings.cs` | untracked (new) | Write Set | +| `UtilitiesCS/UtilitiesCS.csproj` | M | Write Set | +| `TaskMaster/TaskMaster.csproj` | M | Write Set | +| `UtilitiesCS.Test/Extensions/DfDeedle_COM_Tests.cs` | M | Write Set | +| `UtilitiesCS.Test/Extensions/DfDeedle_COM_Tests.QfcColumnsAsync.cs` | untracked (new) | Write Set | +| `UtilitiesCS.Test/HelperClasses/Settings/ApplicationSettingsStoreTests.cs` | untracked (new) | Write Set | +| `UtilitiesCS.Test/UtilitiesCS.Test.csproj` | M | Write Set | +| `ToDoModel.Test/Data Model/ID/IDListGetNextToDoIDTests.cs` | M | Write Set | +| `TaskTree.Test/TaskTreeControllerMoveLogicTests.cs` | M | Write Set | +| `TaskTree.Test/TaskTreeControllerRouteDropTests.cs` | M | Write Set | +| `TaskMaster.Test/AppGlobals/InMemorySettingsStore.cs` | untracked (new) | Write Set | +| `TaskMaster.Test/AppGlobals/SettingsSaveProbe.cs` | untracked (new) | Write Set (Decision D-A) | +| `TaskMaster.Test/AppGlobals/AppQuickFilerSettingsTests.cs` | M | Write Set | +| `TaskMaster.Test/AppGlobals/AppQuickFilerSettingsRemainingPropertiesTests.cs` | M | Write Set | +| `TaskMaster.Test/AppGlobals/AppStagingFilenamesTests.cs` | M | Write Set | +| `TaskMaster.Test/AppGlobals/AppAutoFileObjectsCoverageExpansionTests.cs` | M | Write Set | +| `TaskMaster.Test/Ribbon/RibbonControllerTests.cs` | M | Write Set | +| `TaskMaster.Test/TaskMaster.Test.csproj` | M | Write Set | + +## Documentation paths + +| Path set | Classification | +| --- | --- | +| `docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/**` (issue, spec, user story, research, plan, preflight records, and this run's evidence) | under the feature folder | +| `.claude/agent-memory/atomic-planner/MEMORY.md`, `.claude/agent-memory/atomic-planner/project_1005_r0_settings_store_seam_and_probe_seams.md`, `.claude/agent-memory/atomic-planner/project_1005_r1_jacoco_projection_is_package_level_only.md`, `.claude/agent-memory/prd-feature/feedback_ac_gates_verify_satisfiability.md`, `.claude/agent-memory/task-researcher/MEMORY.md`, `.claude/agent-memory/task-researcher/project_settings_save_shared_user_config_1005.md` | pre-existing in `merge-base-scope.2026-10-22T16-06.md` (agent memory) | +| `docs/features/potential/promoted/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config.md`, `docs/features/potential/promoted/2026-10-22-tests-mutate-shared-settings-default-in-memory.md` | pre-existing in `merge-base-scope.2026-10-22T16-06.md` (promotion records) | + +## Required absences + +| Path | Present | +| --- | --- | +| `ToDoModel.Test/ToDoModel.Test.csproj` | no | +| `TaskMaster.Test/Ribbon/RibbonControllerTests.Engines.cs` | no | +| any `.claude/` path other than `.claude/agent-memory/**` | no | +| any file under `UtilitiesCS/Extensions/` | no | diff --git a/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/self-review.2026-10-22T16-38.md b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/self-review.2026-10-22T16-38.md new file mode 100644 index 000000000..3d78293c2 --- /dev/null +++ b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/self-review.2026-10-22T16-38.md @@ -0,0 +1,96 @@ +# Self-Review (P7-T11) + +Timestamp: 2026-10-22T16-38 +Command: checklist walk of the branch diff from MERGE-BASE (git diff ) plus the untracked Write Set files listed by git status --porcelain, against the policy-audit canonical sections, a code-review checklist, a feature-audit checklist, and the plan's hard constraints +EXIT_CODE: 0 +Output Summary: 0 FAIL rows. 56 PASS rows, 6 N/A rows. One recorded deviation: the template assets could not be resolved, so the canonical section list from the policy-audit-template-usage skill is used. + +## Template resolution (deviation) + +- `docs/features/templates/policy_audit/` does not exist in TaskMaster (Glob over `docs/features/templates/**` returned no files). +- `.claude/skills/policy-audit-template-usage/SKILL.md` names the MCP tool `mcp__drm-copilot__resolve_policy_audit_template_asset` as the template source; that tool is not available to the executor in this run. +- Deviation recorded: the policy-audit walk uses the skill's canonical major sections; the code-review and feature-audit walks use the checklist items below, derived from `CLAUDE.md`, `.claude/rules/general-code-change.md`, `.claude/rules/general-unit-test.md`, `.claude/rules/csharp.md`, and `spec.md`. + +## Policy audit (canonical sections) + +| # | Item | Result | Basis | +| --- | --- | --- | --- | +| PA-1 | Executive Summary: change scope matches the spec Write Set | PASS | `scope-fence.2026-10-22T16-29.md` | +| PA-2 | 1. General Unit Test Policy: independence and isolation (no shared mutable state between tests) | PASS | per-test `InMemorySettingsStore` / `Mock`; snapshot/restore of `Settings.Default` removed; `parallel-repeat-runs.2026-10-22T16-30.md` | +| PA-3 | 1. Determinism: no wall-clock dependence | PASS | DfDeedle tests use a never-advanced `FakeTimeProvider`; happy-path elapsed 2.1 to 2.7 ms in six runs | +| PA-4 | 1. No temporary files, no external dependencies | PASS | `parallel-constraints-scan.2026-10-22T16-28.md` (`GetTempPath`, `GetTempFileName`, `user.config` = 0 added) | +| PA-5 | 1. Arrange-Act-Assert and documented intent | PASS | every new or converted test carries Arrange/Act/Assert comments and a class summary | +| PA-6 | 1. Scenario completeness: positive, negative, edge, error | PASS | null-store constructors throw; shorter-ID no-save; longer-ID save-again; non-numeric and out-of-range threshold leave value unchanged; `EmailInfoStagingFile` non-persisting | +| PA-7 | 2. General Code Change Policy: simplicity, separation of I/O | PASS | one two-member interface plus one adapter; persistence isolated behind `ISettingsStore` | +| PA-8 | 2. Fail fast, explicit errors | PASS | `ArgumentNullException` guards on every store-taking constructor and on the adapter | +| PA-9 | 2. File size at or under 500 lines (pre-existing oversize files shrink) | PASS | `file-sizes.2026-10-22T16-37.md` | +| PA-10 | 2. Comments explain why | PASS | field-initializer comment in `IDList`; lock comment in the probes; `MakeController` comments | +| PA-11 | 3. C# Code Change Policy: CSharpier, analyzers, nullable gate | PASS | `final-toolchain-format`, `final-toolchain-analyzers`, `final-toolchain-typecheck` (one clean pass) | +| PA-12 | 3. Minimal public surface; `internal` where possible | PASS | Decision D-C: `ISettingsStore`, `ApplicationSettingsStore`, new `IDList` constructors public (no `InternalsVisibleTo` to consumers); TaskMaster store constructors `internal` | +| PA-13 | 3. Legacy project registration (``) for every new `.cs` | PASS | `UtilitiesCS.csproj` (2), `UtilitiesCS.Test.csproj` (2), `TaskMaster.csproj` (1), `TaskMaster.Test.csproj` (2) | +| PA-14 | 3. C# 7.3 limits in ToDoModel.Test and TaskTree.Test | PASS | both compile under the analyzer and type-check builds; no nullable annotations, target-typed `new()`, `is not`, switch expressions, or `using` declarations added there | +| PA-15 | 3. `#nullable enable` on new UtilitiesCS and UtilitiesCS.Test files | PASS | `ISettingsStore.cs`, `ApplicationSettingsStore.cs`, `ApplicationSettingsStoreTests.cs` carry it; `DfDeedle_COM_Tests.QfcColumnsAsync.cs` matches its sibling partial, which carries none | +| PA-16 | 4. C# Unit Test Policy: MSTest, Moq, FluentAssertions only | PASS | no other framework referenced | +| PA-17 | 5. Test Coverage Detail: new seam code at least 90% | PASS | `coverage-comparison.2026-10-22T16-36.md` (all new members 100%) | +| PA-18 | 5. Changed lines lose no coverage | PASS | changed-line coverage 100% in every modified production file | +| PA-19 | 5. Repository floors 80 / 75 | PASS | 86.37% line, 80.66% branch | +| PA-20 | 6. Test Execution Metrics | PASS | 7618 / 7618 passed (`final-toolchain-tests.2026-10-22T16-35.md`) | +| PA-21 | 7. Code Quality Checks: zero analyzer warnings on Write Set files | PASS | `0 Warning(s)` solution-wide | +| PA-22 | 8. Gaps and Exceptions recorded | PASS | DfDeedle fail-before dossier; template-resolution deviation (this file); out-of-scope items deferred to issue #1006 | +| PA-23 | 9. Summary of Changes consistent with the diff | PASS | `scope-fence.2026-10-22T16-29.md` | +| PA-24 | 10. Compliance Verdict | PASS | no FAIL row | +| PA-25 | Appendix A: Test Inventory (probe set and register tests) | PASS | all present and Passed in `final-toolchain-tests.2026-10-22T16-35.md` | +| PA-26 | Appendix B: Toolchain Commands Reference (CLAUDE.md commands used verbatim) | PASS | FORMAT, ANALYZE, TYPECHECK, TEST-COVERAGE as named in the plan | + +## Code review + +| # | Item | Result | Basis | +| --- | --- | --- | --- | +| CR-1 | Production default behavior preserved (same property, same value, same condition, then `Save()`) | PASS | default constructors build `ApplicationSettingsStore(Properties.Settings.Default)`; generated setters are indexer writes; `GetNextToDoID` keeps the order add, persist when longer, serialize | +| CR-2 | No breaking change to existing public API | PASS | existing constructors keep their signatures; `AppQuickFilerSettings` and `AppStagingFilenames` gain an explicit public parameterless constructor equivalent to the former implicit one; interfaces unchanged | +| CR-3 | No static mutable seam introduced | PASS | stores are per-instance `readonly` fields | +| CR-4 | Field-initializer default reaches every `IDList` constructor | PASS | `Constructor_Default_UsesApplicationSettingsStore`; Cobertura shows the initializer lines under each constructor | +| CR-5 | `AppAutoFileObjects` constructor chaining keeps `_parent` assignment and other field initializers | PASS | parent-only constructor chains to the store constructor, which assigns `_parent`; `AppAutoFileObjectsFolderPredictorTests` unchanged and passing | +| CR-6 | `ApplicationGlobalsTests` uninitialized `AppAutoFileObjects` (null store) never dereferenced | PASS | that test reads no scalar member; suite passes | +| CR-7 | `EmailInfoStagingFile` remains non-persisting | PASS | `EmailInfoStagingFile_Setter_DoesNotWriteSettingsDefault` (store value unchanged, `SaveCount` 0) | +| CR-8 | AC4 Q2 shape: each saving setter followed by `_settingsStore.Save()` | PASS | counts 1 / 6 / 9 / 6 in `ac4-search-after.2026-10-22T16-27.md` | +| CR-9 | Probe safety: thread-attributed, cancels its own saves, unsubscribes in `finally`, locks only subscribe and unsubscribe | PASS | `SettingsSaveProbe.CountSavesOnCurrentThread`, `CountToDoModelSettingsSavesOnCurrentThread` | +| CR-10 | No production change under `UtilitiesCS/Extensions/` | PASS | `dfdeedle-pass-after.2026-10-22T16-26.md`, `scope-fence.2026-10-22T16-29.md` | +| CR-11 | Line endings and BOM consistent with the tree | PASS | modified files keep CRLF and their HEAD BOM state; new files CRLF without BOM | +| CR-12 | Test helpers placed under the test tree | PASS | `InMemorySettingsStore`, `SettingsSaveProbe` in `TaskMaster.Test/AppGlobals/` | + +## Feature audit (spec acceptance criteria) + +| # | Item | Result | Basis | +| --- | --- | --- | --- | +| FA-1 | AC1 | PASS | `dfdeedle-pass-after.2026-10-22T16-26.md` | +| FA-2 | AC2 | PASS | `idlist-probe-pass-after.2026-10-22T16-18.md` | +| FA-3 | AC3 | PASS | `idlist-probe-pass-after.2026-10-22T16-18.md` | +| FA-4 | AC4 | PASS | `ac4-search-before.2026-10-22T16-12.md`, `ac4-search-after.2026-10-22T16-27.md` | +| FA-5 | AC5 | PASS | `settings-probe-fail-before.2026-10-22T16-15.md`, `fail-before-exception.2026-10-22T16-15.md`, pass-after artifacts | +| FA-6 | AC6 | PASS | `final-toolchain-tests.2026-10-22T16-35.md`, `parallel-repeat-runs.2026-10-22T16-30.md`, `parallel-constraints-scan.2026-10-22T16-28.md` | +| FA-7 | AC7 | PASS | four `final-toolchain-` artifacts, `coverage-comparison.2026-10-22T16-36.md`, `file-sizes.2026-10-22T16-37.md` | +| FA-8 | AC8 | PASS | `ribbon-static-state-check.2026-10-22T16-23.md`; both class-level attributes removed | +| FA-9 | AC check-off reconciliation (8 / 8, text unchanged) | PASS | `ac-reconciliation.2026-10-22T16-38.md` | +| FA-10 | Out-of-scope items untouched (issue #1006 set; `AppOlObjects*` saves) | PASS | Q1 after still finds `AppOlObjects.JunkFolders.cs` 3 and `AppOlObjects.cs` 1; those files are not in the diff | + +## Plan hard constraints + +| # | Constraint | Result | Basis | +| --- | --- | --- | --- | +| HC-1 | No serialization: no `Workers` of one, no added `[DoNotParallelize]`, no retries, no `Thread.Sleep`, `Task.Delay`, or longer timeout | PASS | `parallel-constraints-scan.2026-10-22T16-28.md` (all added counts 0; 2 removals) | +| HC-2 | No test temporary file; no `user.config` write, redirect, or relocation; no process-wide `SettingsProvider` | PASS | same scan; probe cancels every counted save | +| HC-3 | DfDeedle fix is test-only through the existing seams, no new NuGet | PASS | no project-file package change; `UtilitiesCS/Extensions/` unchanged | +| HC-4 | AC4 before and after searches recorded and every match fixed | PASS | AC4 artifacts | +| HC-5 | Fail-before evidence precedes every fix | PASS | P1-T6 and P1-T7 artifacts are stamped before the Phase 3 to 5 fixes | +| HC-6 | No `[ExcludeFromCodeCoverage]` added; `coverage.config` unchanged | PASS | scan | +| HC-7 | Executor did not commit; no `.claude/**` (other than agent memory) or `CLAUDE.md` edit | PASS | `scope-fence.2026-10-22T16-29.md` | +| HC-8 | Committed evidence is Markdown plus permitted JaCoCo projections only; no host path | PASS | `evidence-form.2026-10-22T16-37.md` | +| HC-9 | Changed-line coverage of the `ISettingsStore` interface | N/A | interface-only file, no executable lines | +| HC-10 | Shell-icon carve-out | N/A | not used; neither coverage run stalled | +| HC-11 | Pester, Jest, and Python isolation runs | N/A | no PowerShell, TypeScript, or Python file changed | +| HC-12 | Execution-environment brief update (mirror parity) | N/A | no fact stated by the brief changed | +| HC-13 | `IDListMaxLengthPersistenceTests.cs` / `ToDoModel.Test.csproj` | N/A | Decision D-D: not needed (235 lines in the existing file) | +| HC-14 | `RibbonControllerTests.Engines.cs` change | N/A | Decision D-E: unchanged | + +Result: no FAIL row; the plan completes without a QA-loop restart from this review. diff --git a/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/self-review.2026-10-22T17-12.md b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/self-review.2026-10-22T17-12.md new file mode 100644 index 000000000..b64b3ec66 --- /dev/null +++ b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/self-review.2026-10-22T17-12.md @@ -0,0 +1,19 @@ +Timestamp: 2026-10-22T17-12 +Command: Walk of the policy-audit, code-review, and feature-audit templates against the branch diff +EXIT_CODE: 0 +Output Summary: No FAIL rows. The template directory docs/features/templates/policy_audit/ holds no tracked files in this checkout, so the templates were walked by their standard checklist classes. + +| Item | Result | +| --- | --- | +| Policy order read (CLAUDE.md, general, unit-test, csharp rules) | PASS | +| Toolchain order: csharpier, analyzers, warnings-as-errors, MSTest coverage; all exit 0 | PASS | +| MSTest, Moq, FluentAssertions; C# 7.3 in ToDoModel.Test | PASS | +| No temp files; store injected via Mock of ISettingsStore, no Properties.Settings.Default | PASS | +| No DoNotParallelize, Workers=1, retries, Sleep, Delay | PASS | +| File size at or under 500 lines (test file 287) | PASS | +| No production code change by this remediation | PASS | +| No AC text change | PASS | +| Coverage: repo 86.38% line, 80.68% branch; IDList.cs not below merge base; getter fully covered | PASS | +| New code coverage at or above 90% (test-only change) | N/A | +| Evidence under evidence/ kinds with four fields; no raw trx or Cobertura committed | PASS | +| Mirror parity, .claude and CLAUDE.md untouched | N/A | diff --git a/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/regression-testing/ac4-search-after.2026-10-22T16-27.md b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/regression-testing/ac4-search-after.2026-10-22T16-27.md new file mode 100644 index 000000000..01180a080 --- /dev/null +++ b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/regression-testing/ac4-search-after.2026-10-22T16-27.md @@ -0,0 +1,85 @@ +# AC4 Search - After Fix (Issue #1005) + +Timestamp: 2026-10-22T16-27 +Command: git grep --untracked -c -E 'Settings\.Default\.Save\(\)|_defaults\.Save\(\)|defaults\.Save\(\)' -- '*.cs' +EXIT_CODE: 0 +Output Summary: Every "Required after" outcome holds. Q1 finds only the two excluded unreached sites (AppOlObjects.JunkFolders.cs 3, AppOlObjects.cs 1). Q2 finds IDList.cs 1, AppQuickFilerSettings.cs 6, AppStagingFilenames.cs 9, AppAutoFileObjects.ScalarSettings.cs 6. Q3 classifies all 46 test constructions as store-injected or default-constructed without spec-member or GetNextToDoID use. Q4 finds 0 calls in DfDeedle_COM_Tests.cs and 2 in DfDeedle_COM_Tests.QfcColumnsAsync.cs, each passing `timeProvider: new FakeTimeProvider()`; the DfDeedleQfcColumnTimeoutTests call is unchanged. Q5: no test reaches an ApplicationSettingsBase.Save(); every reaching test from the before search is converted. + +Before reference: `evidence/regression-testing/ac4-search-before.2026-10-22T16-12.md`. + +## Q1 - production and test settings saves + +Command: `git grep --untracked -c -E 'Settings\.Default\.Save\(\)|_defaults\.Save\(\)|defaults\.Save\(\)' -- '*.cs'` (EXIT_CODE 0) + +| File | Before | After | +| --- | --- | --- | +| `ToDoModel/Data Model/ID/IDList.cs` | 1 | 0 | +| `TaskMaster/AppGlobals/AppQuickFilerSettings.cs` | 6 | 0 | +| `TaskMaster/AppGlobals/AppStagingFilenames.cs` | 9 | 0 | +| `TaskMaster/AppGlobals/AppAutoFileObjects.cs` | 6 | 0 | +| `TaskMaster/AppGlobals/AppOlObjects.JunkFolders.cs` | 3 | 3 (excluded: no test reach) | +| `TaskMaster/AppGlobals/AppOlObjects.cs` | 1 | 1 (excluded: no test reach) | +| `TaskMaster.Test/AppGlobals/AppAutoFileObjectsCoverageExpansionTests.cs` | 1 | 0 | + +Required after: only `AppOlObjects.JunkFolders.cs` 3 and `AppOlObjects.cs` 1. Holds. + +## Q2 - store saves + +Command: `git grep --untracked -c -F '_settingsStore.Save()' -- '*.cs'` (EXIT_CODE 0) + +| File | Count | Required | +| --- | --- | --- | +| `ToDoModel/Data Model/ID/IDList.cs` | 1 | 1 | +| `TaskMaster/AppGlobals/AppQuickFilerSettings.cs` | 6 | 6 | +| `TaskMaster/AppGlobals/AppStagingFilenames.cs` | 9 | 9 | +| `TaskMaster/AppGlobals/AppAutoFileObjects.ScalarSettings.cs` | 6 | 6 | + +Holds. + +## Q3 - test constructions of the consumers + +Command: `git grep --untracked -n -E 'new (IDList|AppQuickFilerSettings|AppStagingFilenames|AppAutoFileObjects)\(' -- '*.Test/*.cs' '*.Test/**/*.cs'` (EXIT_CODE 0) + +| File | Matches | Classification | +| --- | --- | --- | +| `AppAutoFileObjectsCoverageExpansionTests.cs` | 3 | `CreateSutWithSpecialFolders` is store-injected (`store ?? CreateScalarStore()`); `Constructor_ParentOnly_UsesApplicationSettingsStore` is default-constructed and only reads the private store field by reflection (no spec member); `Constructor_NullSettingsStore_ThrowsArgumentNullException` passes a null store (throws) | +| `AppAutoFileObjectsFolderPredictorTests.cs` | 5 | default-constructed; no scalar spec member called | +| `AppQuickFilerSettingsRemainingPropertiesTests.cs` | 6 | store-injected | +| `AppQuickFilerSettingsTests.cs` | 6 | 4 store-injected; `Constructor_Parameterless_UsesApplicationSettingsStore` default-constructed, reads the private field only; `Constructor_NullSettingsStore_ThrowsArgumentNullException` null store (throws) | +| `AppStagingFilenamesTests.cs` | 10 | 8 store-injected; `Constructor_Parameterless_UsesApplicationSettingsStore` default-constructed, reads the private field only; `Constructor_NullSettingsStore_ThrowsArgumentNullException` null store (throws) | +| `RibbonControllerTests.cs` | 1 | store-injected (`new AppQuickFilerSettings(store)` in `CreateController(InMemorySettingsStore)`) | +| `TaskTreeControllerMoveLogicTests.cs` | 1 | store-injected (`MakeController`) | +| `TaskTreeControllerRouteDropTests.cs` | 1 | store-injected (`MakeController`) | +| `IDListGetNextToDoIDTests.cs` | 13 | 10 store-injected; 2 null-store constructor tests (throw); `Constructor_Default_UsesApplicationSettingsStore` default-constructed, reads the private field only and does not call `GetNextToDoID` | + +Required after: every match is store-injected, or default-constructed with the test body calling none of the fifteen spec members and not calling `GetNextToDoID`. Holds (the null-store constructions throw at construction and reach nothing). + +## Q4 - column-add calls + +Command: `git grep --untracked -n -A6 -F 'AddQfcColumnsAsync(' -- '*.Test/*.cs' '*.Test/**/*.cs'` (EXIT_CODE 0), plus `git grep --untracked -c -F 'timeProvider: new FakeTimeProvider()' -- 'UtilitiesCS.Test/Extensions/DfDeedle_COM_Tests.QfcColumnsAsync.cs'` (2) + +| File | Calls | Passes `timeProvider:` | +| --- | --- | --- | +| `UtilitiesCS.Test/Extensions/DfDeedle_COM_Tests.cs` | 0 | n/a | +| `UtilitiesCS.Test/Extensions/DfDeedle_COM_Tests.QfcColumnsAsync.cs` | 2 | yes, both `timeProvider: new FakeTimeProvider()` | +| `UtilitiesCS.Test/Extensions/DfDeedleQfcColumnTimeoutTests.cs` | 1 (`StartColumnAdd`) | unchanged; passes `probe.Adder` and `barrier` positionally | + +Note: the happy-path call was first written with a local `FakeTimeProvider` variable and was aligned to the literal `timeProvider: new FakeTimeProvider()` form before this search was recorded; the behavior is identical (a fake clock that is never advanced). The P6-T4 runs and the P7 final pass re-execute the test in this form. + +Holds. + +## Q5 - reach trace + +| Before: reaching test | After: how it is converted | +| --- | --- | +| `TaskTreeControllerMoveLogicTests` six move tests | `MakeController` returns `new IDList(new List(), store.Object)` over a per-call `Mock`; `MoveObjectsToChildren_DesyncedRoot_FiresMessageSeam` verifies one mock `Save()` and zero probe saves | +| `TaskTreeControllerRouteDropTests` three drop tests | `MakeController` returns `new IDList(new List(), settingsStore.Object)` | +| `AppQuickFilerSettingsRemainingPropertiesTests` four setter tests | `new AppQuickFilerSettings(store)` over a per-test `InMemorySettingsStore` | +| `AppQuickFilerSettingsTests` two setter tests | `new AppQuickFilerSettings(store)` over a per-test `InMemorySettingsStore` | +| `RibbonControllerTests` four setter-reaching tests | `CreateController(InMemorySettingsStore)` injects `new AppQuickFilerSettings(store)` | +| `AppStagingFilenamesTests` two setter tests | `new AppStagingFilenames(store)` over a per-test `InMemorySettingsStore` | +| `AppAutoFileObjectsCoverageExpansionTests.ScalarSettingsProperties_WhenRoundTripped_UpdateDefaultsAndReturnValues` | `CreateSutWithSpecialFolders(..., store: store)` with a seeded `InMemorySettingsStore`; `SaveCount` 6 and zero probe saves | +| `AppAutoFileObjectsCoverageExpansionTests.TestCleanup` direct `defaults.Save()` | deleted with `TestInitialize`, `TestCleanup`, and the six snapshot fields | +| `IDListGetNextToDoIDTests` (cache-priming workaround) | `CreateListWithCachedMaxLength` deleted; every list built with `new IDList(ids, store.Object)` | + +Required after: no test reaches an `ApplicationSettingsBase.Save()`. Holds: the remaining production `Settings.Default.Save()` sites (`AppOlObjects.JunkFolders.cs`, `AppOlObjects.cs`) have no test reach (research reach map), every Q3 match is store-injected or does not call a saving member, and the store-injected tests save only to `InMemorySettingsStore` or `Mock`. diff --git a/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/regression-testing/ac4-search-before.2026-10-22T16-12.md b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/regression-testing/ac4-search-before.2026-10-22T16-12.md new file mode 100644 index 000000000..9d6474353 --- /dev/null +++ b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/regression-testing/ac4-search-before.2026-10-22T16-12.md @@ -0,0 +1,72 @@ +# AC4 Search - Before Fix (Issue #1005) + +Timestamp: 2026-10-22T16-12 +Command: git grep -c -E 'Settings\.Default\.Save\(\)|_defaults\.Save\(\)|defaults\.Save\(\)' -- '*.cs' +EXIT_CODE: 0 +Output Summary: Q1 finds 27 settings saves in 7 files matching the planning read exactly (IDList.cs 1, AppQuickFilerSettings.cs 6, AppStagingFilenames.cs 9, AppAutoFileObjects.cs 6, AppOlObjects.JunkFolders.cs 3, AppOlObjects.cs 1, AppAutoFileObjectsCoverageExpansionTests.cs 1). Q2 finds no `_settingsStore.Save()` (git grep exit 1). Q3 finds 29 test constructions of the four consumers, all default-constructed, 19 of them in tests that reach a spec member save. Q4 finds 2 `AddQfcColumnsAsync(` calls without `timeProvider:` in DfDeedle_COM_Tests.cs plus 1 seamed helper call in DfDeedleQfcColumnTimeoutTests.cs. Q5 lists 18 reaching tests plus the TestCleanup direct save. + +All searches run against the unmodified tracked tree (no code edit yet in this item). + +## Q1 - production and test settings saves + +Command: `git grep -c -E 'Settings\.Default\.Save\(\)|_defaults\.Save\(\)|defaults\.Save\(\)' -- '*.cs'` (EXIT_CODE 0) + +| File | Count | +| --- | --- | +| `ToDoModel/Data Model/ID/IDList.cs` | 1 | +| `TaskMaster/AppGlobals/AppQuickFilerSettings.cs` | 6 | +| `TaskMaster/AppGlobals/AppStagingFilenames.cs` | 9 | +| `TaskMaster/AppGlobals/AppAutoFileObjects.cs` | 6 | +| `TaskMaster/AppGlobals/AppOlObjects.JunkFolders.cs` | 3 | +| `TaskMaster/AppGlobals/AppOlObjects.cs` | 1 | +| `TaskMaster.Test/AppGlobals/AppAutoFileObjectsCoverageExpansionTests.cs` | 1 | + +Matches the seven per-file counts in the plan's AC4 Search Definition. + +## Q2 - store saves + +Command: `git grep -c -F '_settingsStore.Save()' -- '*.cs'` (EXIT_CODE 1, no matches). Before the fix no consumer saves through a store. + +## Q3 - test constructions of the consumers + +Command: `git grep -n -E 'new (IDList|AppQuickFilerSettings|AppStagingFilenames|AppAutoFileObjects)\(' -- '*.Test/*.cs' '*.Test/**/*.cs'` (EXIT_CODE 0) + +| File | Matches | Enclosing tests | Classification (before) | +| --- | --- | --- | --- | +| `TaskMaster.Test/AppGlobals/AppAutoFileObjectsCoverageExpansionTests.cs` | 1 (`CreateSutWithSpecialFolders`) | all 8 tests of the class; `ScalarSettingsProperties_WhenRoundTripped_UpdateDefaultsAndReturnValues` calls the six scalar setters | default-constructed; one test reaches spec members (not compliant) | +| `TaskMaster.Test/AppGlobals/AppAutoFileObjectsFolderPredictorTests.cs` | 5 | folder-predictor load tests | default-constructed, no spec member called (compliant) | +| `TaskMaster.Test/AppGlobals/AppQuickFilerSettingsRemainingPropertiesTests.cs` | 6 | 4 setter tests reach `MoveEntireConversation`, `SaveAttachments`, `SavePictures`, `SaveEmailCopy` setters; 2 getter tests | default-constructed; setter tests not compliant | +| `TaskMaster.Test/AppGlobals/AppQuickFilerSettingsTests.cs` | 4 | `HighConfidenceModeEnabled_WhenSetTrue_ReadsBackTrue`, `HighConfidenceThreshold_WhenSet_ReadsBackSameValue` reach setters; 2 getter tests | default-constructed; setter tests not compliant | +| `TaskMaster.Test/AppGlobals/AppStagingFilenamesTests.cs` | 6 | `ConditionalReminders_Setter_RoundTripsThroughSettingsDefault`, `CommonWords_GetterAfterSet_ReturnsCachedValueWithoutReReadingSettings` reach setters; `EmailInfoStagingFile_Setter_DoesNotWriteSettingsDefault` (non-saving) and 3 getter tests | default-constructed; two setter tests not compliant | +| `TaskMaster.Test/Ribbon/RibbonControllerTests.cs` | 1 (`CreateController`) | `ToggleHighConfidenceMode_FlipsStoredValue`, `SetHighConfidenceModeForLaunch_True_EnablesMode`, `StandardLaunchAfterHighConfidenceLaunch_DoesNotEnableMode`, `SetHighConfidenceThresholdText_WithValidPercentage_PersistsProbability` reach setters | default-constructed; four tests not compliant | +| `TaskTree.Test/TaskTreeControllerMoveLogicTests.cs` | 1 (`MakeController`) | six tests reach `GetNextToDoID` (Q5) | default-constructed; not compliant | +| `TaskTree.Test/TaskTreeControllerRouteDropTests.cs` | 1 (`MakeController`) | three tests reach `GetNextToDoID` (Q5) | default-constructed; not compliant | +| `ToDoModel.Test/Data Model/ID/IDListGetNextToDoIDTests.cs` | 3 | null and empty seed tests throw before any save; `CreateListWithCachedMaxLength` primes the cache so four tests avoid the save by workaround | default-constructed with a workaround that leaves the save path reachable (not compliant per AC2) | + +## Q4 - column-add calls + +Command: `git grep -n -F 'AddQfcColumnsAsync(' -- '*.Test/*.cs' '*.Test/**/*.cs'` (EXIT_CODE 0) + +| File | Call | Passes `timeProvider:` | +| --- | --- | --- | +| `UtilitiesCS.Test/Extensions/DfDeedle_COM_Tests.cs` | `AddQfcColumnsAsync_HappyPath_CompletesWithoutThrowing` | no (neither seam) | +| `UtilitiesCS.Test/Extensions/DfDeedle_COM_Tests.cs` | `AddQfcColumnsAsync_PreCancelledToken_CompletesGracefully` | no (neither seam) | +| `UtilitiesCS.Test/Extensions/DfDeedleQfcColumnTimeoutTests.cs` | helper `StartColumnAdd` | passes `columnAdder` and `timeProvider` positionally (`probe.Adder`, `barrier`) | + +Before: two calls without `timeProvider:` in `DfDeedle_COM_Tests.cs`. + +## Q5 - reach trace + +| Production save reached | Reaching test (enclosing class) | +| --- | --- | +| `IDList.GetNextToDoID` | `TaskTreeControllerMoveLogicTests`: `MoveObjectsToSibling_RootTarget_RemovesFromRootsAndReseeds`, `MoveObjectsToSibling_RootNotInRoots_FiresMessageSeam`, `MoveObjectsToSibling_ChildTarget_InsertsIntoParentChildren`, `MoveObjectsToChildren_RootSource_ReparentsAndRemovesFromRoots`, `MoveObjectsToChildren_DesyncedRoot_FiresMessageSeam`, `MoveObjectsToChildren_NonRootSource_ReparentsUnderTarget` | +| `IDList.GetNextToDoID` | `TaskTreeControllerRouteDropTests`: `RouteDrop_Item_RoutesToChildren_ReturnsTrue`, `RouteDrop_AboveItem_InsertsSiblingBeforeTarget_ReturnsTrue`, `RouteDrop_BelowItem_InsertsSiblingAfterTarget_ReturnsTrue` | +| `AppQuickFilerSettings.MoveEntireConversation` / `SaveAttachments` / `SavePictures` / `SaveEmailCopy` setters | `AppQuickFilerSettingsRemainingPropertiesTests`: the four `*_Setter_RoundTripsThroughSettingsDefault` tests | +| `AppQuickFilerSettings.HighConfidenceModeEnabled` setter | `AppQuickFilerSettingsTests.HighConfidenceModeEnabled_WhenSetTrue_ReadsBackTrue`; `RibbonControllerTests`: `ToggleHighConfidenceMode_FlipsStoredValue`, `SetHighConfidenceModeForLaunch_True_EnablesMode`, `StandardLaunchAfterHighConfidenceLaunch_DoesNotEnableMode` | +| `AppQuickFilerSettings.HighConfidenceThreshold` setter | `AppQuickFilerSettingsTests.HighConfidenceThreshold_WhenSet_ReadsBackSameValue`; `RibbonControllerTests.SetHighConfidenceThresholdText_WithValidPercentage_PersistsProbability` | +| `AppStagingFilenames.ConditionalReminders` setter | `AppStagingFilenamesTests.ConditionalReminders_Setter_RoundTripsThroughSettingsDefault` | +| `AppStagingFilenames.CommonWords` setter | `AppStagingFilenamesTests.CommonWords_GetterAfterSet_ReturnsCachedValueWithoutReReadingSettings` | +| `AppAutoFileObjects` six scalar setters | `AppAutoFileObjectsCoverageExpansionTests.ScalarSettingsProperties_WhenRoundTripped_UpdateDefaultsAndReturnValues` | +| Direct test-code save (`defaults.Save()`) | `AppAutoFileObjectsCoverageExpansionTests.TestCleanup` (runs after every test of the class) | + +The reaching set equals the research reach map (Test-to-site reach map) plus the `TestCleanup` direct save. diff --git a/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/regression-testing/dfdeedle-pass-after.2026-10-22T16-26.md b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/regression-testing/dfdeedle-pass-after.2026-10-22T16-26.md new file mode 100644 index 000000000..e162270b1 --- /dev/null +++ b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/regression-testing/dfdeedle-pass-after.2026-10-22T16-26.md @@ -0,0 +1,34 @@ +# DfDeedle Tests - Pass After (P5-T2) + +Timestamp: 2026-10-22T16-26 +Command: vstest.console.exe UtilitiesCS.Test\bin\Debug\UtilitiesCS.Test.dll /Settings:scripts\vscode\TaskMaster.cli.runsettings /InIsolation "/TestCaseFilter:FullyQualifiedName~DfDeedle_COM_Tests&TestCategory!=LiveOutlook|FullyQualifiedName~DfDeedleQfcColumnTimeoutTests&TestCategory!=LiveOutlook" /ResultsDirectory:SCRATCH\issue-1005-results-p5t2-dfdeedle "/Logger:trx;LogFileName=issue-1005-p5t2-dfdeedle.trx" +EXIT_CODE: 0 +Output Summary: 34 total, 34 executed, 34 passed, 0 failed. DfDeedle_COM_Tests executed 26 tests, the same count as the baseline (P0-T7: 26). Both converted tests passed: AddQfcColumnsAsync_HappyPath_CompletesWithoutThrowing (2.3 ms) and AddQfcColumnsAsync_PreCancelledToken_CompletesGracefully (1.4 ms). DfDeedleQfcColumnTimeoutTests 8 passed. + +## Preceding build (ANALYZE) + +- Command: `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true` +- EXIT_CODE: 0 +- Result: `0 Warning(s)`, `0 Error(s)`. + +## Converted tests + +| Test | Outcome | Duration | +| --- | --- | --- | +| `DfDeedle_COM_Tests.AddQfcColumnsAsync_HappyPath_CompletesWithoutThrowing` | Passed | 00:00:00.0023165 | +| `DfDeedle_COM_Tests.AddQfcColumnsAsync_PreCancelledToken_CompletesGracefully` | Passed | 00:00:00.0014197 | + +Both are `async Task` tests in `UtilitiesCS.Test/Extensions/DfDeedle_COM_Tests.QfcColumnsAsync.cs` that pass `timeProvider: new FakeTimeProvider()` (never advanced), leave `columnAdder` at the production default, await `NotThrowAsync`, and contain no `GetAwaiter().GetResult()` or real wait. The happy-path test also verifies the mocked `Columns.Add("SentOn")` once. + +## Counts + +| Class | Passed | Failed | +| --- | --- | --- | +| `DfDeedle_COM_Tests` | 26 (baseline 26) | 0 | +| `DfDeedleQfcColumnTimeoutTests` | 8 | 0 | + +## File and scope checks + +- `UtilitiesCS.Test/Extensions/DfDeedle_COM_Tests.cs`: 809 physical lines (baseline 848); 0 `AddQfcColumnsAsync(` calls. +- `UtilitiesCS.Test/Extensions/DfDeedle_COM_Tests.QfcColumnsAsync.cs`: 71 physical lines. +- `git status --porcelain -- UtilitiesCS/Extensions`: empty (no production file under `UtilitiesCS/Extensions/` changed). diff --git a/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/regression-testing/fail-before-exception.2026-10-22T16-15.md b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/regression-testing/fail-before-exception.2026-10-22T16-15.md new file mode 100644 index 000000000..9e200ecee --- /dev/null +++ b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/regression-testing/fail-before-exception.2026-10-22T16-15.md @@ -0,0 +1,24 @@ +# Fail-Before Exception Dossier - DfDeedle AddQfcColumnsAsync (Issue #1005) + +Timestamp: 2026-10-22T16-15 +Command: git grep -n -F 'AddQfcColumnsAsync(' -- '*.Test/*.cs' '*.Test/**/*.cs' +EXIT_CODE: 0 +Output Summary: The before-fix AC4 Q4 search finds two `AddQfcColumnsAsync(` calls in `UtilitiesCS.Test/Extensions/DfDeedle_COM_Tests.cs` that pass neither `columnAdder` nor `timeProvider` (`AddQfcColumnsAsync_HappyPath_CompletesWithoutThrowing`, `AddQfcColumnsAsync_PreCancelledToken_CompletesGracefully`). A deterministic failing run is not possible without real timers, so this dossier substitutes for a failing run. + +WhyFailingRunImpossible: The reported failure needs the three real 3000 ms deadlines armed by `TimeoutTask.TimeoutAfter` on `TimeProvider.System` to elapse before the `Task.Run` column-add work completes, which happens only when the testhost is under heavy parallel load. A deterministic local run either completes the work before the first deadline (the baseline full-suite run passed this test) or would need real timers and induced load, which the item constraints prohibit (no `Thread.Sleep`, `Task.Delay`, or real waits). + +## Maintainer's captured exception (quoted from `issue.md`) + +> Observe `AddQfcColumnsAsync_HappyPath_CompletesWithoutThrowing` fail after 9.5 s with `TimeoutException: The column add step timed out after 9000 ms for folder '(unknown folder)'` thrown from `DfDeedle.AddQfcColumnsAsync`. + +Exception text: `TimeoutException: The column add step timed out after 9000 ms for folder '(unknown folder)'` + +## Alternative Proof + +The repeatable demonstration is the before-fix AC4 search recorded in `evidence/regression-testing/ac4-search-before.2026-10-22T16-12.md` (Q4): two `AddQfcColumnsAsync(` calls without `timeProvider:` exist in `DfDeedle_COM_Tests.cs` before the fix. Without a `timeProvider`, `TimeoutAfter` arms `TimeProvider.System.CreateTimer`, so each of the three deadlines is wall-clock bound and the test outcome depends on scheduler load. The after-fix search (`ac4-search-after..md`) is expected to find zero such calls in that file and two calls in `DfDeedle_COM_Tests.QfcColumnsAsync.cs`, each passing `timeProvider: new FakeTimeProvider()`, a clock that never advances, so no deadline can fire. + +## Search scope (negative-claim record) + +- SearchScope: `/evidence/regression-testing/` +- SearchPatterns: a failing-run artifact for `AddQfcColumnsAsync_HappyPath_CompletesWithoutThrowing` +- SearchResult: none; this dossier is the fail-before evidence for the DfDeedle path. diff --git a/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/regression-testing/idlist-probe-pass-after.2026-10-22T16-18.md b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/regression-testing/idlist-probe-pass-after.2026-10-22T16-18.md new file mode 100644 index 000000000..203a43fe5 --- /dev/null +++ b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/regression-testing/idlist-probe-pass-after.2026-10-22T16-18.md @@ -0,0 +1,36 @@ +# IDList Probe - Pass After (P3-T4) + +Timestamp: 2026-10-22T16-18 +Command: vstest.console.exe ToDoModel.Test\bin\Debug\ToDoModel.Test.dll TaskTree.Test\bin\Debug\TaskTree.Test.dll /Settings:scripts\vscode\TaskMaster.cli.runsettings /InIsolation "/TestCaseFilter:FullyQualifiedName~IDListGetNextToDoIDTests&TestCategory!=LiveOutlook|FullyQualifiedName~TaskTreeControllerMoveLogicTests&TestCategory!=LiveOutlook|FullyQualifiedName~TaskTreeControllerRouteDropTests&TestCategory!=LiveOutlook" /ResultsDirectory:SCRATCH\issue-1005-results-p3t4-idlist "/Logger:trx;LogFileName=issue-1005-p3t4-idlist.trx" +EXIT_CODE: 0 +Output Summary: 37 total, 37 executed, 37 passed, 0 failed. MoveObjectsToChildren_DesyncedRoot_FiresMessageSeam passes: the probe records zero saves on ToDoModel.Properties.Settings.Default and the injected Mock records exactly one Save(). All seven new IDListGetNextToDoIDTests register tests pass. + +## Preceding build (ANALYZE) + +- Command: `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true` +- EXIT_CODE: 0 +- Result: `0 Warning(s)`, `0 Error(s)` (ToDoModel.Test and TaskTree.Test compile under C# 7.3). + +## Trx-derived results + +| Test | Outcome | +| --- | --- | +| `IDListGetNextToDoIDTests.Constructor_Default_UsesApplicationSettingsStore` | Passed | +| `IDListGetNextToDoIDTests.Constructor_EnumerableWithNullStore_ThrowsArgumentNullException` | Passed | +| `IDListGetNextToDoIDTests.Constructor_NullStoreOnly_ThrowsArgumentNullException` | Passed | +| `IDListGetNextToDoIDTests.Constructor_StoreOnly_SavesThroughInjectedStore` | Passed | +| `IDListGetNextToDoIDTests.GetNextToDoID_EmptySeed_ThrowsArgumentException` | Passed | +| `IDListGetNextToDoIDTests.GetNextToDoID_FirstIdOnFreshList_AssignsLengthAndSavesOnce` | Passed | +| `IDListGetNextToDoIDTests.GetNextToDoID_IdAlreadyPresent_AdvancesPastTheCollision` | Passed | +| `IDListGetNextToDoIDTests.GetNextToDoID_IdNotLongerThanCachedLength_DoesNotSaveAgain` | Passed | +| `IDListGetNextToDoIDTests.GetNextToDoID_LengthBoundaryRollover_ProducesLongerId` | Passed | +| `IDListGetNextToDoIDTests.GetNextToDoID_LongerIdAfterFirst_SavesAgain` | Passed | +| `IDListGetNextToDoIDTests.GetNextToDoID_MultipleConsecutiveCollisions_AdvancesToFirstFreeValue` | Passed | +| `IDListGetNextToDoIDTests.GetNextToDoID_NoCollision_ReturnsNextBase36ValueAndAddsIt` | Passed | +| `IDListGetNextToDoIDTests.GetNextToDoID_NullSeed_ThrowsArgumentException` | Passed | +| `TaskTreeControllerMoveLogicTests` (17 tests, including `MoveObjectsToChildren_DesyncedRoot_FiresMessageSeam`) | 17 Passed | +| `TaskTreeControllerRouteDropTests` (7 tests) | 7 Passed | + +## Fail-before reference + +`settings-probe-fail-before.2026-10-22T16-15.md` recorded `MoveObjectsToChildren_DesyncedRoot_FiresMessageSeam` failing with 1 save on the test thread before the fix. diff --git a/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/regression-testing/maxlengthofid-tests.2026-10-22T17-09.md b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/regression-testing/maxlengthofid-tests.2026-10-22T17-09.md new file mode 100644 index 000000000..a484d92c2 --- /dev/null +++ b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/regression-testing/maxlengthofid-tests.2026-10-22T17-09.md @@ -0,0 +1,4 @@ +Timestamp: 2026-10-22T17-09 +Command: vstest.console.exe ToDoModel.Test.dll /Settings:scripts/vscode/TaskMaster.cli.runsettings /InIsolation "/TestCaseFilter:Name=MaxLengthOfID_ColdCacheOnMixedLengthList_ReturnsLongestIdLength|Name=MaxLengthOfID_ColdCacheOnSecondDistinctList_ReturnsItsOwnLongestIdLength|Name=MaxLengthOfID_WarmCacheAfterGetNextToDoID_ReturnsCachedLengthWithoutSaving" /Logger:trx (results under SCRATCH) +EXIT_CODE: 0 +Output Summary: 3 executed, 3 passed, 0 failed (trx summary). Parallelization enabled (Workers: 24, Scope: ClassLevel). diff --git a/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/regression-testing/settings-probe-fail-before.2026-10-22T16-15.md b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/regression-testing/settings-probe-fail-before.2026-10-22T16-15.md new file mode 100644 index 000000000..a0166b69b --- /dev/null +++ b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/regression-testing/settings-probe-fail-before.2026-10-22T16-15.md @@ -0,0 +1,26 @@ +# Settings-Saving Probe - Fail Before (Issue #1005) + +Timestamp: 2026-10-22T16-15 +Command: vstest.console.exe TaskTree.Test\bin\Debug\TaskTree.Test.dll TaskMaster.Test\bin\Debug\TaskMaster.Test.dll /Settings:scripts\vscode\TaskMaster.cli.runsettings /InIsolation "/TestCaseFilter:FullyQualifiedName~TaskTreeControllerMoveLogicTests.MoveObjectsToChildren_DesyncedRoot_FiresMessageSeam&TestCategory!=LiveOutlook|FullyQualifiedName~AppQuickFilerSettingsTests.HighConfidenceModeEnabled_WhenSetTrue_ReadsBackTrue&TestCategory!=LiveOutlook|FullyQualifiedName~AppStagingFilenamesTests.ConditionalReminders_Setter_RoundTripsThroughSettingsDefault&TestCategory!=LiveOutlook|FullyQualifiedName~AppAutoFileObjectsCoverageExpansionTests.ScalarSettingsProperties_WhenRoundTripped_UpdateDefaultsAndReturnValues&TestCategory!=LiveOutlook" /ResultsDirectory:SCRATCH\issue-1005-results-p1t6-fail-before "/Logger:trx;LogFileName=issue-1005-p1t6-fail-before.trx" +EXIT_CODE: 1 +ExpectedExitCode: 1 +Output Summary: Expected failure observed. 4 total, 4 executed, 0 passed, 4 failed. Each of the four probe-set tests fails on the probe assertion and records saves on the test thread: MoveObjectsToChildren_DesyncedRoot_FiresMessageSeam 1 save, HighConfidenceModeEnabled_WhenSetTrue_ReadsBackTrue 1 save, ConditionalReminders_Setter_RoundTripsThroughSettingsDefault 1 save, ScalarSettingsProperties_WhenRoundTripped_UpdateDefaultsAndReturnValues 6 saves. No other assertion is the reported failure. Each counted save was cancelled by the probe, so no settings file was written. + +## Preceding build (ANALYZE) + +- Command: `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true` +- EXIT_CODE: 0 +- Result: `0 Warning(s)`, `0 Error(s)`. The probe code (P1-T1 to P1-T5) compiles against the unmodified production code; TaskTree.Test compiles under C# 7.3. + +## Filter note + +The `TestCategory!=LiveOutlook` clause is attached to each `FullyQualifiedName~` alternative because `&` binds tighter than `|` in the vstest filter grammar; a single trailing clause would apply to the last alternative only. The selected test set is the four named tests. + +## Trx-derived results (raw trx kept under SCRATCH, not committed) + +| Test | Outcome | Failure message | +| --- | --- | --- | +| `TaskTreeControllerMoveLogicTests.MoveObjectsToChildren_DesyncedRoot_FiresMessageSeam` | Failed | Expected saves to be 0 because the test persisted settings to the shared per-host settings file through IDList, but found 1 (difference of 1). | +| `AppQuickFilerSettingsTests.HighConfidenceModeEnabled_WhenSetTrue_ReadsBackTrue` | Failed | Expected saves to be 0 because the test persisted settings to the shared per-host settings file, but found 1 (difference of 1). | +| `AppStagingFilenamesTests.ConditionalReminders_Setter_RoundTripsThroughSettingsDefault` | Failed | Expected saves to be 0 because the test persisted settings to the shared per-host settings file, but found 1 (difference of 1). | +| `AppAutoFileObjectsCoverageExpansionTests.ScalarSettingsProperties_WhenRoundTripped_UpdateDefaultsAndReturnValues` | Failed | Expected saves to be 0 because the test persisted settings to the shared per-host settings file, but found 6 (difference of 6). | diff --git a/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/regression-testing/settings-store-adapter-tests.2026-10-22T16-17.md b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/regression-testing/settings-store-adapter-tests.2026-10-22T16-17.md new file mode 100644 index 000000000..01bf2c6fe --- /dev/null +++ b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/regression-testing/settings-store-adapter-tests.2026-10-22T16-17.md @@ -0,0 +1,21 @@ +# Settings-Store Adapter Tests (P2-T4) + +Timestamp: 2026-10-22T16-17 +Command: vstest.console.exe UtilitiesCS.Test\bin\Debug\UtilitiesCS.Test.dll /Settings:scripts\vscode\TaskMaster.cli.runsettings /InIsolation "/TestCaseFilter:FullyQualifiedName~ApplicationSettingsStoreTests&TestCategory!=LiveOutlook" /ResultsDirectory:SCRATCH\issue-1005-results-p2t4-adapter "/Logger:trx;LogFileName=issue-1005-p2t4-adapter.trx" +EXIT_CODE: 0 +Output Summary: 4 total, 4 executed, 4 passed, 0 failed. The net481 ApplicationSettingsBase indexer and Save() are overridable: RecordingApplicationSettings compiles and the adapter forwards get, set, and save to it without any provider call. + +## Preceding build (ANALYZE) + +- Command: `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true` +- EXIT_CODE: 0 +- Result: `0 Warning(s)`, `0 Error(s)`. + +## Trx-derived results + +| Test | Outcome | +| --- | --- | +| `ApplicationSettingsStoreTests.Constructor_NullSettings_ThrowsArgumentNullException` | Passed | +| `ApplicationSettingsStoreTests.Indexer_Get_ReturnsWrappedSettingsValue` | Passed | +| `ApplicationSettingsStoreTests.Indexer_Set_WritesWrappedSettingsValue` | Passed | +| `ApplicationSettingsStoreTests.Save_Invoked_ForwardsToWrappedSettingsOnce` | Passed | diff --git a/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/regression-testing/taskmaster-settings-probe-pass-after.2026-10-22T16-25.md b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/regression-testing/taskmaster-settings-probe-pass-after.2026-10-22T16-25.md new file mode 100644 index 000000000..04a4be48e --- /dev/null +++ b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/regression-testing/taskmaster-settings-probe-pass-after.2026-10-22T16-25.md @@ -0,0 +1,53 @@ +# TaskMaster Settings Probe - Pass After (P4-T11) + +Timestamp: 2026-10-22T16-25 +Command: vstest.console.exe TaskMaster.Test\bin\Debug\TaskMaster.Test.dll /Settings:scripts\vscode\TaskMaster.cli.runsettings /InIsolation "/TestCaseFilter:FullyQualifiedName~AppQuickFilerSettingsTests&TestCategory!=LiveOutlook|FullyQualifiedName~AppQuickFilerSettingsRemainingPropertiesTests&TestCategory!=LiveOutlook|FullyQualifiedName~AppStagingFilenamesTests&TestCategory!=LiveOutlook|FullyQualifiedName~AppAutoFileObjectsCoverageExpansionTests&TestCategory!=LiveOutlook|FullyQualifiedName~AppAutoFileObjectsFolderPredictorTests&TestCategory!=LiveOutlook|FullyQualifiedName~RibbonControllerTests&TestCategory!=LiveOutlook" /ResultsDirectory:SCRATCH\issue-1005-results-p4t11-taskmaster "/Logger:trx;LogFileName=issue-1005-p4t11-taskmaster.trx" +EXIT_CODE: 0 +Output Summary: 54 total, 54 executed, 54 passed, 0 failed. The three TaskMaster probe-set tests pass with zero saves on the shared TaskMaster settings instance, and all eight TaskMaster register tests pass. AppQuickFilerSettingsTests and RibbonControllerTests ran without the class-level DoNotParallelize attribute. + +## Preceding build (ANALYZE) + +- Command: `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true` +- EXIT_CODE: 0 +- Result: `0 Warning(s)`, `0 Error(s)`. + +## Probe-set tests (fail before, pass after) + +| Test | Before (`settings-probe-fail-before.2026-10-22T16-15.md`) | After | +| --- | --- | --- | +| `AppQuickFilerSettingsTests.HighConfidenceModeEnabled_WhenSetTrue_ReadsBackTrue` | Failed (1 save) | Passed | +| `AppStagingFilenamesTests.ConditionalReminders_Setter_RoundTripsThroughSettingsDefault` | Failed (1 save) | Passed | +| `AppAutoFileObjectsCoverageExpansionTests.ScalarSettingsProperties_WhenRoundTripped_UpdateDefaultsAndReturnValues` | Failed (6 saves) | Passed | + +## Register tests + +| Test | Outcome | +| --- | --- | +| `AppQuickFilerSettingsTests.Constructor_Parameterless_UsesApplicationSettingsStore` | Passed | +| `AppQuickFilerSettingsTests.Constructor_NullSettingsStore_ThrowsArgumentNullException` | Passed | +| `AppStagingFilenamesTests.SavingSetters_EachAssignment_WritesStoreAndSavesOnce` | Passed | +| `AppStagingFilenamesTests.Getters_UnsetFields_ReadSeededStoreValues` | Passed | +| `AppStagingFilenamesTests.Constructor_Parameterless_UsesApplicationSettingsStore` | Passed | +| `AppStagingFilenamesTests.Constructor_NullSettingsStore_ThrowsArgumentNullException` | Passed | +| `AppAutoFileObjectsCoverageExpansionTests.Constructor_ParentOnly_UsesApplicationSettingsStore` | Passed | +| `AppAutoFileObjectsCoverageExpansionTests.Constructor_NullSettingsStore_ThrowsArgumentNullException` | Passed | + +## Per-class counts (trx-derived) + +| Class | Passed | Failed | +| --- | --- | --- | +| `AppQuickFilerSettingsTests` | 6 | 0 | +| `AppQuickFilerSettingsRemainingPropertiesTests` | 6 | 0 | +| `AppStagingFilenamesTests` | 10 | 0 | +| `AppAutoFileObjectsCoverageExpansionTests` | 10 | 0 | +| `AppAutoFileObjectsFolderPredictorTests` | 5 | 0 | +| `RibbonControllerTests` (both partials) | 17 | 0 | + +## Acceptance searches (Phase 4 task conditions) + +- `AppQuickFilerSettingsTests.cs`: no `DoNotParallelize`; no `Settings.Default` write (2 references: the probe argument and a documentation `cref`). +- `AppQuickFilerSettingsRemainingPropertiesTests.cs`: 0 `Settings.Default` references. +- `AppStagingFilenamesTests.cs`: 1 `Settings.Default` reference (the probe argument). +- `AppAutoFileObjectsCoverageExpansionTests.cs`: 1 `Properties.Settings.Default` reference (the probe argument); 0 `Save()` calls. +- `RibbonControllerTests.cs`: 0 `Settings.Default` references; 0 `DoNotParallelize` (matches the `REMOVE` verdict); 459 physical lines. +- `TaskMaster/AppGlobals/AppAutoFileObjects.cs`: 791 physical lines (baseline 853). diff --git a/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/feature-audit.2026-10-22T17-10.md b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/feature-audit.2026-10-22T17-10.md new file mode 100644 index 000000000..586116499 --- /dev/null +++ b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/feature-audit.2026-10-22T17-10.md @@ -0,0 +1,55 @@ +# Feature Audit: Parallel test failures from wall-clock deadlines and shared user.config (Issue #1005) + +- Review timestamp: 2026-10-22T17-10 (first review of this branch) +- Reviewer: feature-review agent + +## Scope and Baseline + +- Branch: `bug/parallel-test-failures-wall-clock-and-shared-user-config-1005`, head `e399de597`. +- Resolved base branch: `main`, merge base `346651242a2bd6ed25d703a4b1ecb5a055c6ae72` (caller-supplied; `origin/main` has not advanced past it). The audit diff is `git diff 346651242..HEAD` (73 files). +- Work mode: `full-bug` (`- Work Mode: full-bug` in `issue.md`); AC source: `spec.md` `## Acceptance Criteria` only. `issue.md` AC1 to AC7 are the maintainer's original statement and are carried into `spec.md`; `user-story.md` carries no criteria. +- PR context: `artifacts/pr_context.summary.txt` and `.appendix.txt` describe the #1002 branch and are stale for this branch; they could not be regenerated under the caller's Bash restriction (`git diff` and `git log` only), so scope was derived from `git diff`. +- Evidence basis: executor projections under `evidence/{baseline,regression-testing,qa-gates,other}/`, the canonical coverage artifact `artifacts/csharp/coverage.xml`, the retained merge-base Cobertura document, and reviewer reads of source and diff. + +## Acceptance Criteria Inventory + +Source: `docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/spec.md`, `## Acceptance Criteria`. Eight checkbox items, AC1 to AC8, all checked `[x]` at head. + +| AC | Subject | +|---|---| +| AC1 | DfDeedle happy-path test independent of wall-clock time | +| AC2 | `IDList.GetNextToDoID` persists through an injected `ISettingsStore` | +| AC3 | Every test reaching `GetNextToDoID` injects a store; probe records zero saves | +| AC4 | Before and after search; every reached settings save and seamless `AddQfcColumnsAsync` call fixed | +| AC5 | Fail-before evidence for each fix | +| AC6 | Full suite passes in parallel; no serialization, sleeps, retries, temp files or `user.config` writes | +| AC7 | Full C# toolchain clean; coverage gates; file sizes | +| AC8 | `[DoNotParallelize]` removal with a recorded static-state check | + +## Acceptance Criteria Evaluation + +| AC | Verdict | Evidence | +|---|---|---| +| AC1 | PASS | `DfDeedle_COM_Tests.QfcColumnsAsync.cs`: both tests are `async Task`, pass `timeProvider: new FakeTimeProvider()` (never advanced), leave `columnAdder` at the production default, and await with `NotThrowAsync`; no `GetAwaiter().GetResult()`, sleep or delay. Happy path verifies `Add("SentOn")` once. No file under `UtilitiesCS/Extensions/` is in the diff. Elapsed 2.1 to 2.7 ms in six parallel runs (`parallel-repeat-runs.2026-10-22T16-30.md`). | +| AC2 | PASS | `IDList.cs`: field initializer defaults `_settingsStore` to `new ApplicationSettingsStore(Properties.Settings.Default)` for every existing constructor; two new public constructors null-guard the store; `GetNextToDoID` assigns `MaxLengthOfID` through the indexer and calls `_settingsStore.Save()` under the unchanged condition. Tests `GetNextToDoID_FirstIdOnFreshList_AssignsLengthAndSavesOnce`, `..._IdNotLongerThanCachedLength_DoesNotSaveAgain`, `..._LongerIdAfterFirst_SavesAgain`, two null-store constructor tests and `Constructor_Default_UsesApplicationSettingsStore` pass. `CreateListWithCachedMaxLength` is deleted. Note: that deletion removed the only coverage of `MaxLengthOfID` (finding B-1); AC2 does not address that member, so the verdict is unaffected. | +| AC3 | PASS | `TaskTreeControllerMoveLogicTests.MakeController` and `TaskTreeControllerRouteDropTests.MakeController` return `new IDList(new List(), store.Object)`; every `IDListGetNextToDoIDTests` list is store-injected (the default-constructed list in `Constructor_Default_UsesApplicationSettingsStore` never calls `GetNextToDoID`). `MoveObjectsToChildren_DesyncedRoot_FiresMessageSeam` asserts 0 probe saves and verifies one mock `Save()`; it failed with 1 save before the fix (`settings-probe-fail-before.2026-10-22T16-15.md`) and passes after (`idlist-probe-pass-after.2026-10-22T16-18.md`). | +| AC4 | PASS | `ac4-search-before.2026-10-22T16-12.md` and `ac4-search-after.2026-10-22T16-27.md`. After: production `Settings.Default.Save()` remains only in `AppOlObjects.JunkFolders.cs` (3) and `AppOlObjects.cs` (1), both unreached by tests (spec non-goal); `_settingsStore.Save()` counts 1, 6, 9, 6 in `IDList`, `AppQuickFilerSettings`, `AppStagingFilenames`, `AppAutoFileObjects.ScalarSettings`; the test-code `defaults.Save()` is deleted; both `AddQfcColumnsAsync` calls pass the fake clock. Reviewer Grep of every `*.Test` project for `new IDList(`, `new AppQuickFilerSettings()`, `new AppStagingFilenames()`, `new AppAutoFileObjects(` and for the fifteen setter names confirms the classification: default-constructed instances never call a saving member, and other projects only mock the getters. | +| AC5 | PASS | Probe fail-before run: 4 of 4 failed on the probe assertion with 1, 1, 1 and 6 saves, each cancelled so nothing was written; pass-after projections for the IDList path and the three TaskMaster classes. DfDeedle: `fail-before-exception.2026-10-22T16-15.md` carries `WhyFailingRunImpossible`, quotes the `issue.md` exception, and cites the before-fix AC4 search. | +| AC6 | PASS | `final-toolchain-tests.2026-10-22T16-35.md`: 7618 of 7618 passed under `scripts/vscode/TaskMaster.cli.runsettings` (`Workers` 0, `Scope` ClassLevel). Fifteen repeat runs with zero failures. Reviewer `git diff -G` scan: no added `[DoNotParallelize]`, `Thread.Sleep`, `Task.Delay`, `[Timeout]`, retry, `Workers` or temp-path token; no runsettings file in the diff; no test reaches a real settings save. | +| AC7 | PASS | Final pass in CLAUDE.md order: CSharpier format and check clean (pass 2), analyzer `/t:Rebuild` 0/0, warnings-as-errors `/t:Rebuild` 0/0 without a Nullable override, `Invoke-MSTestWithCoverage.ps1` 7618 passed. Changed lines: every instrumented changed line covered (reviewer read of Cobertura line nodes). `ApplicationSettingsStore` and `AppAutoFileObjects.ScalarSettings` class rates 1.0; every new or changed seam member in `IDList`, `AppQuickFilerSettings`, `AppStagingFilenames` at 100%. 0 added `[ExcludeFromCodeCoverage]`. File sizes: all at or under 500 except `DfDeedle_COM_Tests.cs` 848 to 809 and `AppAutoFileObjects.cs` 853 to 791. The B-1 regression is on an unchanged member and is outside AC7's literal clauses; it is a policy finding, not an AC7 failure. | +| AC8 | PASS | `[DoNotParallelize]` removed from `AppQuickFilerSettingsTests` and `RibbonControllerTests`. `evidence/other/ribbon-static-state-check.2026-10-22T16-23.md` records the whole-class check with verdict REMOVE. Reviewer cross-check: `RibbonController()` has an empty body; `TaskMaster/Ribbon` statics are `static readonly` loggers, immutable catalog maps and pure helpers; the `Engines` partial uses only per-test instances. | + +## Summary + +All eight acceptance criteria evaluate PASS against code and evidence. The review nevertheless returns **REMEDIATION_REQUIRED** with one blocking policy finding, B-1 (autonomous): removing the cache-priming test helper, as AC2 requires, also removed the only test execution of the public `IDList.MaxLengthOfID` getter (7 of 7 lines and 4 of 4 branches covered at the merge base, none at head), the `IDList.cs` branch rate regressed from 20.69% to 19.35%, and the committed coverage comparison does not report it. The remedy is test-only: add `MaxLengthOfID` assertions to `IDListGetNextToDoIDTests.cs`, rerun the C# toolchain, and correct the coverage comparison. + +### Acceptance Criteria Status +- Source: `docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/spec.md` +- Total AC items: 8 +- Checked off (delivered): 8 +- Remaining (unchecked): 0 +- Items remaining: none + +## Acceptance Criteria Check-off + +All eight items were already `- [x]` in `spec.md` when this review began, and all eight evaluate PASS, so no box was changed and no criterion text was edited. No checked item is disputed. Finding B-1 is a coverage-policy finding outside the literal text of every criterion; the reviewer did not uncheck any item for it. diff --git a/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/feature-audit.2026-10-22T17-23.md b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/feature-audit.2026-10-22T17-23.md new file mode 100644 index 000000000..cabf98e49 --- /dev/null +++ b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/feature-audit.2026-10-22T17-23.md @@ -0,0 +1,54 @@ +# Feature Audit: Parallel test failures from wall-clock deadlines and shared user.config (Issue #1005) + +- Review timestamp: 2026-10-22T17-23 (re-audit R4 after remediation cycle 1; prior review 2026-10-22T17-10) +- Reviewer: feature-review agent + +## Scope and Baseline + +- Branch: `bug/parallel-test-failures-wall-clock-and-shared-user-config-1005`, head `3df0782ab`. +- Resolved base branch: `main`, merge base `346651242a2bd6ed25d703a4b1ecb5a055c6ae72` (confirmed by `artifacts/pr_context.summary.txt`, generated 2026-10-22 17:21:51 UTC for head `3df0782ab`). The audit diff is `git diff 346651242..HEAD` (91 files). +- Work mode: `full-bug` (`- Work Mode: full-bug` in `issue.md`); AC source: `spec.md` `## Acceptance Criteria` only. `user-story.md` carries no criteria. +- Evidence basis: executor projections under `evidence/{baseline,regression-testing,qa-gates,other}/` including the cycle-1 files, the canonical coverage artifact `artifacts/csharp/coverage.xml` (cycle-1 final pass), and reviewer reads of source and diff. + +## Acceptance Criteria Inventory + +Source: `docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/spec.md`, `## Acceptance Criteria`. Eight checkbox items, AC1 to AC8, all checked `[x]` at head. + +| AC | Subject | +|---|---| +| AC1 | DfDeedle happy-path test independent of wall-clock time | +| AC2 | `IDList.GetNextToDoID` persists through an injected `ISettingsStore` | +| AC3 | Every test reaching `GetNextToDoID` injects a store; probe records zero saves | +| AC4 | Before and after search; every reached settings save and seamless `AddQfcColumnsAsync` call fixed | +| AC5 | Fail-before evidence for each fix | +| AC6 | Full suite passes in parallel; no serialization, sleeps, retries, temp files or `user.config` writes | +| AC7 | Full C# toolchain clean; coverage gates; file sizes | +| AC8 | `[DoNotParallelize]` removal with a recorded static-state check | + +## Acceptance Criteria Evaluation + +| AC | Verdict | Evidence | +|---|---|---| +| AC1 | PASS | `DfDeedle_COM_Tests.QfcColumnsAsync.cs`: both tests are `async Task`, pass `timeProvider: new FakeTimeProvider()` (never advanced), leave `columnAdder` at the production default, await with `NotThrowAsync`; no `GetAwaiter().GetResult()`, sleep or delay. No file under `UtilitiesCS/Extensions/` in the diff. Unchanged by cycle 1. | +| AC2 | PASS | `IDList.cs`: field initializer defaults the store to `ApplicationSettingsStore` over `ToDoModel.Properties.Settings.Default`; two new public constructors null-guard the store; `GetNextToDoID` assigns through the indexer and calls `_settingsStore.Save()` under the unchanged condition. Persistence-branch and null-store tests present and passing (7621 of 7621). `CreateListWithCachedMaxLength` remains deleted; the cycle-1 tests do not reintroduce it and every new list is store-injected. | +| AC3 | PASS | Both `MakeController` helpers return store-injected `IDList` instances; every `IDListGetNextToDoIDTests` list that calls `GetNextToDoID`, including the cycle-1 warm-cache test, is store-injected. Probe: 1 save before (`settings-probe-fail-before.2026-10-22T16-15.md`), 0 after (`idlist-probe-pass-after.2026-10-22T16-18.md`). | +| AC4 | PASS | `ac4-search-before.2026-10-22T16-12.md` and `ac4-search-after.2026-10-22T16-27.md`: after the fix, production `Settings.Default.Save()` remains only in the unreached `AppOlObjects` sites (spec non-goal); `_settingsStore.Save()` covers every named site; the test-code `defaults.Save()` is deleted; both `AddQfcColumnsAsync` calls pass the fake clock. Cycle 1 adds no settings-reaching test. | +| AC5 | PASS | Probe fail-before run: 4 of 4 failed with 1, 1, 1 and 6 cancelled saves; pass-after projections for each path. DfDeedle: `fail-before-exception.2026-10-22T16-15.md` with `WhyFailingRunImpossible`, quoted exception and the before-fix AC4 search. | +| AC6 | PASS | `remediation-tests.2026-10-22T17-12.md`: 7621 of 7621 passed under `scripts/vscode/TaskMaster.cli.runsettings` (`Workers` 0, `Scope` ClassLevel) with `/InIsolation`; `parallel-repeat-runs.2026-10-22T16-30.md` repeat runs with zero failures; `maxlengthofid-tests.2026-10-22T17-09.md` 3 of 3 with parallelization enabled. No added `[DoNotParallelize]`, `Workers`, retry, sleep, delay, timeout or temp-path token across the branch, including cycle 1. | +| AC7 | PASS | Cycle-1 final pass in CLAUDE.md order: CSharpier format (no change) and check exit 0, analyzer `/t:Rebuild` 0/0, warnings-as-errors `/t:Rebuild` 0/0 without a Nullable override, `Invoke-MSTestWithCoverage.ps1` 7621 passed. Changed lines lose no coverage; `ApplicationSettingsStore` and `AppAutoFileObjects.ScalarSettings` at 100%; seam members in `IDList`, `AppQuickFilerSettings`, `AppStagingFilenames` at 100%; `coverage-comparison.2026-10-22T17-12.md` records the post-processed Cobertura figures. No `[ExcludeFromCodeCoverage]` added. File sizes: all at or under 500 (`IDListGetNextToDoIDTests.cs` 287) except `DfDeedle_COM_Tests.cs` 848 to 809 and `AppAutoFileObjects.cs` 853 to 791, both smaller than at the merge base. The prior B-1 regression is resolved. | +| AC8 | PASS | `[DoNotParallelize]` removed from `AppQuickFilerSettingsTests` and `RibbonControllerTests`; `evidence/other/ribbon-static-state-check.2026-10-22T16-23.md` records the whole-class check with verdict REMOVE. Unchanged by cycle 1. | + +## Summary + +All eight acceptance criteria evaluate PASS against code and evidence at head `3df0782ab`. The prior blocking finding B-1 (loss of `IDList.MaxLengthOfID` coverage) is resolved by three store-injected tests: the getter is at 7/7 lines and 4/4 branches, and `IDList.cs` stands at 30.18% line and 25.81% branch against the merge-base 24.36% and 20.69%. No blocking finding remains; the review verdict is **PASS**. The TaskMaster CI checks are a PR-time gate and have not run because no pull request exists. + +### Acceptance Criteria Status +- Source: `docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/spec.md` +- Total AC items: 8 +- Checked off (delivered): 8 +- Remaining (unchecked): 0 +- Items remaining: none + +## Acceptance Criteria Check-off + +All eight items were already `- [x]` in `spec.md` when this review began, and all eight evaluate PASS, so no box was changed and no criterion text was edited. No checked item is disputed. diff --git a/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/issue.md b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/issue.md new file mode 100644 index 000000000..9049fdbf5 --- /dev/null +++ b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/issue.md @@ -0,0 +1,77 @@ +# parallel-test-failures-wall-clock-and-shared-user-config (Issue #1005) + +- Date captured: 2026-10-22 +- Author: Dan Moisan +- Status: Promoted -> docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/ (Issue #1005) + +> Automation note: Keep the section headings below unchanged; the promotion tooling maps each of them into the GitHub bug issue template. + +- Issue: #1005 +- Issue URL: https://github.com/drmoisan/TaskMaster/issues/1005 +- Last Updated: 2026-10-22 +- Work Mode: full-bug + +## Summary + +Two MSTest tests fail intermittently when the suite runs in parallel in Visual Studio Test Explorer. `DfDeedle_COM_Tests.AddQfcColumnsAsync_HappyPath_CompletesWithoutThrowing` depends on wall-clock deadlines, and `TaskTreeControllerMoveLogicTests.MoveObjectsToChildren_DesyncedRoot_FiresMessageSeam` writes the shared on-disk `user.config` through `Properties.Settings.Default.Save()`. Both violate the unit-test isolation rules; every test must pass when run in parallel. + +## Environment + +- OS/version: Windows 11, Visual Studio 18 Test Explorer (parallel execution) +- Python version: not applicable (C# .NET Framework 4.8, MSTest, Moq, FluentAssertions) +- Command/flags used: Visual Studio Test Explorer run of the full solution +- Data source or fixture: UtilitiesCS.Test `DfDeedle_COM_Tests`; TaskTree.Test `TaskTreeControllerMoveLogicTests` + +## Steps to Reproduce + +1. Run the full test suite in parallel in Visual Studio Test Explorer. +2. Observe `AddQfcColumnsAsync_HappyPath_CompletesWithoutThrowing` fail after 9.5 s with `TimeoutException: The column add step timed out after 9000 ms for folder '(unknown folder)'` thrown from `DfDeedle.AddQfcColumnsAsync`. +3. Observe `MoveObjectsToChildren_DesyncedRoot_FiresMessageSeam` fail with `ConfigurationErrorsException: Failed to save settings: The configuration file has been changed by another program` from `IDList.GetNextToDoID` -> `ApplicationSettingsBase.Save()`, reached through `TreeOfToDoItems.AddChild` and `TaskTreeController.MoveObjectsToChildren`. + +## Expected Behavior + +Both tests pass deterministically in any order and under parallel execution, with no dependence on wall-clock timing and no write to shared on-disk configuration. + +## Actual Behavior + +1. The test calls `DfDeedle.AddQfcColumnsAsync(table, folder, token, 0)` without the `timeProvider` (or `columnAdder`) seam, so the production retry loop waits on three real 3000 ms deadlines for a `Task.Run` over Moq-backed COM mocks; under parallel load the work misses the budget and the method throws. +2. `IDList.GetNextToDoID` persists `MaxLengthOfID` through `Properties.Settings.Default.Save()`, so concurrent tests race on one shared `user.config` file and one save fails. + +## Logs / Screenshots + +- [x] Attached minimal logs or screenshot +- Snippet: `System.TimeoutException: The column add step timed out after 9000 ms for folder '(unknown folder)'` and `System.Configuration.ConfigurationErrorsException: Failed to save settings: The configuration file has been changed by another program. (...\TestSourceHost__...\user.config)` + +## Impact / Severity + +- [ ] Blocker +- [x] High +- [ ] Medium +- [ ] Low + +## Suspected Cause / Notes + +- `UtilitiesCS/Extensions/DfDeedle.QfcColumns.cs` `AddQfcColumnsAsync` already accepts optional `columnAdder` and `timeProvider` seams; the happy-path test does not use them, so it is bound to the wall clock. +- `ToDoModel/Data Model/ID/IDList.cs` `GetNextToDoID` writes `Properties.Settings.Default` and calls `Save()` with no injectable persistence seam, so any test that allocates an ID writes shared machine state. +- Other tests in the repository may share either pattern; the fix should search for them. + +## Proposed Fix / Validation Ideas + +- [ ] Unit coverage areas: make the DfDeedle happy-path test deterministic through the existing seams (a synchronous or pre-completed column adder and a fake time provider); add an injectable settings-persistence seam to `IDList` so tests never write `user.config`, with a production default that preserves current behavior +- [ ] Integration scenario to retest: full MSTest run in parallel (CI shape and Test Explorer shape), plus repeated runs of the two affected classes +- [ ] Manual verification notes: re-run the suite in Visual Studio Test Explorer with parallel execution enabled + +## Acceptance Criteria + +- [ ] AC1: `DfDeedle_COM_Tests.AddQfcColumnsAsync_HappyPath_CompletesWithoutThrowing` no longer depends on wall-clock time: it drives `AddQfcColumnsAsync` through the existing `columnAdder` and/or `timeProvider` seams, contains no real waits, and passes deterministically. +- [ ] AC2: `IDList.GetNextToDoID` persists `MaxLengthOfID` through an injectable seam whose production default preserves the current `Properties.Settings.Default` write and save, and no unit test writes the shared `user.config` file. +- [ ] AC3: `TaskTreeControllerMoveLogicTests.MoveObjectsToChildren_DesyncedRoot_FiresMessageSeam`, and every other test that reaches `IDList.GetNextToDoID`, uses that seam so no test calls `ApplicationSettingsBase.Save()`. +- [ ] AC4: a search of the test projects for other tests that reach `Properties.Settings.Default.Save()` or that call `AddQfcColumnsAsync` without its seams is recorded as evidence, and every match found is fixed in this item. +- [ ] AC5: each fix is preceded by a regression test or a repeatable demonstration that fails before the fix, with the fail-before result recorded as evidence. +- [ ] AC6: the full test suite passes in parallel. The fix uses no `Workers=1`, `[DoNotParallelize]`, retries, sleeps or longer timeouts. +- [ ] AC7: the full C# toolchain (CSharpier, analyzers rebuild, warnings-as-errors rebuild, MSTest with coverage) passes in one clean pass, changed lines lose no coverage, and any new seam code reaches at least 90% coverage. + +## Next Step + +- [ ] Promote to GitHub issue (bug-report template) +- [ ] Move to active fix folder / branch diff --git a/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/plan.2026-10-22T11-02.md b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/plan.2026-10-22T11-02.md new file mode 100644 index 000000000..ebe326a42 --- /dev/null +++ b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/plan.2026-10-22T11-02.md @@ -0,0 +1,216 @@ +# 2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config (Plan) + +- **Issue:** #1005 +- **Parent (optional):** none +- **Owner:** drmoisan +- **Last Updated:** 2026-10-22T11-02 +- **Status:** Ready for preflight +- **Version:** 0.3 +- **Work Mode:** full-bug (acceptance-criteria source: `spec.md` only, AC1 to AC8) +- **Branch:** bug/parallel-test-failures-wall-clock-and-shared-user-config-1005 +- **Route:** large (orchestrated) + +**Fail-closed evidence rule:** Include explicit baseline artifact tasks, final-QA artifact tasks, and coverage-comparison tasks for each in-scope language when policy requires coverage. If any required baseline artifact, QA artifact, or coverage-comparison artifact is missing, the audit verdict must be BLOCKED or INCOMPLETE, never PASS. + +**Evidence accounting rule:** Record the expected artifact path or location in each evidence-producing task. Do not mark evidence-backed work complete without the artifact. + +## Hard Constraints (bind every task) + +- All tests pass when run in parallel. A parallel failure is fixed by removing the shared or timed state, never by serializing: no `Workers` value of one, no `[DoNotParallelize]` added, no retries, no `Thread.Sleep`, no `Task.Delay`, and no longer timeout. +- No test creates a temporary file, and no test writes, redirects, or relocates `user.config`. Settings persistence goes through the per-instance `ISettingsStore` seam (`ApplicationSettingsStore` adapter per spec D2); production defaults keep the current `Properties.Settings.Default` write followed by `Save()`; tests supply `InMemorySettingsStore` or a Moq `Mock`. No static mutable seam and no process-wide `SettingsProvider` is introduced. +- Every settings-saving probe that subscribes to a static `SettingsSaving` event records only saves raised on the subscribing test thread, sets `Cancel` on those saves so nothing is written, and unsubscribes in a `finally` block. +- DfDeedle: the tests use the existing `columnAdder` and `timeProvider` parameters of `AddQfcColumnsAsync` with the already-referenced `FakeTimeProvider`; no NuGet dependency is added and no production file under `UtilitiesCS/Extensions/` changes. +- AC4: the before and after searches are recorded under `/evidence/regression-testing/`, and every match is fixed in this item. +- Bugfix workflow: fail-before evidence (Phase 1) precedes every fix; each fix is minimal. +- MSTest, Moq, and FluentAssertions only. New seam code reaches at least 90 percent line coverage; changed lines lose no coverage; no `[ExcludeFromCodeCoverage]` is added. +- Every created or modified `.cs` file ends at or under 500 physical lines; `UtilitiesCS.Test/Extensions/DfDeedle_COM_Tests.cs` and `TaskMaster/AppGlobals/AppAutoFileObjects.cs` (already above 500) are split into partials and end smaller than at the merge base. +- Legacy non-SDK projects: every new `.cs` file gets a `` in its project file in the same task. `ToDoModel.Test`, `TaskTree.Test`, and `QuickFiler.Test` are C# 7.3 (no nullable annotations, target-typed `new()`, `is not`, switch expressions, or `using` declarations). `UtilitiesCS` is C# 12 and new UtilitiesCS and UtilitiesCS.Test files carry `#nullable enable`. `ToDoModel` (latest) and `TaskMaster` (preview) files touched here carry no `#nullable enable` today and gain no nullable annotations. +- The executor does not commit; the orchestrator commits after execution. No task edits `.claude/**` or `CLAUDE.md`. +- Out of scope (issue #1006, already filed): tests that mutate `Settings.Default` in memory without saving, and the per-method `[DoNotParallelize]` in `ApplicationGlobalsStartupTimingTests`. Production `Save()` sites no test reaches (`AppOlObjects.cs` and `AppOlObjects.JunkFolders.cs`) are not changed. + +## Terms, Routes, and Conventions + +- `FEATURE` names `docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005`. Every evidence path is `/evidence//..md`, stamped with the UTC write time per `.claude/skills/evidence-and-timestamp-conventions/SKILL.md`; a consumer selects the lexicographically greatest `.*.md`. +- `SCRATCH` means `/issue-1005/`. Every scratch file, trx, results directory, raw coverage copy, and helper script lives under it with a name starting `issue-1005-`. Artifacts record the literal token `SCRATCH`, never a host path, host name, or account name. +- Tool routes, launchers, command-text restrictions, and hook context are owned by `.claude/skills/execution-environment/SKILL.md` and are not restated. Its CI section describes the upstream repository; the TaskMaster CI checks are actionlint, build-analyzers, build-nullable, format-check, hygiene, mstest-coverage, and pester. +- Toolchain steps run the exact `CLAUDE.md` commands and are cited by name: FORMAT (`dotnet tool run csharpier format .`, verified by `dotnet tool run csharpier check .`), ANALYZE (`msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true`), TYPECHECK (`msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true`, never with `/p:Nullable=enable`), and TEST-COVERAGE (`scripts/vscode/Invoke-MSTestWithCoverage.ps1`, unscoped, invoked through PowerShell 7 with an absolute script path because `pwsh -WorkingDirectory` does not resolve a `-File` path). TEST-COVERAGE applies `scripts/vscode/TaskMaster.cli.runsettings` (`Workers` 0, `Scope` ClassLevel), `/InIsolation`, the `TestCategory!=LiveOutlook` filter, and the `\.claude\` worktree exclusion itself; it writes the post-processed Cobertura document, its JaCoCo projection, and a trx summary under the ignored `coverage/` folder. MSBuild is resolved through vswhere; `scripts/vscode/Invoke-VSBuild.ps1` is not used, because its package sync rewrites project-file HintPaths outside the Write Set. +- SCOPED-TEST is a vswhere-resolved `vstest.console.exe` run over the named built assemblies (`UtilitiesCS.Test\bin\Debug\UtilitiesCS.Test.dll`, `ToDoModel.Test\bin\Debug\ToDoModel.Test.dll`, `TaskTree.Test\bin\Debug\TaskTree.Test.dll`, `TaskMaster.Test\bin\Debug\TaskMaster.Test.dll`, as the task lists) with `/Settings:scripts\vscode\TaskMaster.cli.runsettings`, `/InIsolation`, a `/TestCaseFilter` joining `FullyQualifiedName~` clauses with `|` and carrying `TestCategory!=LiveOutlook`, and a results directory plus trx file under `SCRATCH`. It always follows an ANALYZE rebuild in the same task, because vstest runs prebuilt assemblies. A run that matches zero tests is a failure. Evidence records a summary derived from the trx (counts plus one outcome row per named test), never the trx. +- Local shell-icon carve-out: four UtilitiesCS.Test classes (`ShellUtilities_Tests`, `ShellUtilitiesStatic_Tests`, `SysImageListHelperTests`, `OSBrowser_Tests`) can stall locally on SHGetFileInfo. If a TEST-COVERAGE run stalls on them, the executor records the stall and the in-progress test names, re-runs the same `dotnet-coverage` collection over a direct vstest invocation whose filter is `TestCategory!=LiveOutlook&FullyQualifiedName!~HelperClasses.ShellUtilities_Tests&FullyQualifiedName!~HelperClasses.ShellUtilitiesStatic_Tests&FullyQualifiedName!~HelperClasses.SysImageListHelperTests&FullyQualifiedName!~EmailIntelligence.OSBrowser_Tests` with the same runsettings, `/InIsolation`, and a blame hang timeout, post-processes it with the script's own dot-sourced functions, and records that exact filter in the artifact. CI covers those classes. No other exclusion is permitted, and no failure outside those four classes is excused. +- Every command-step artifact carries `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:`; an artifact recording an expected non-zero gate carries `ExpectedExitCode:`. Committed evidence is Markdown plus the permitted JaCoCo projection copies; raw trx and raw Cobertura documents stay under `coverage/` or `SCRATCH` and are never committed. +- MERGE-BASE is `git merge-base origin/main HEAD`, computed after `git fetch origin main`. Every scope comparison pairs a diff against MERGE-BASE with `git status --porcelain`, because the executor does not commit and new files are untracked; a search over a file this plan creates uses `git grep --untracked` or the Grep tool, never plain `git grep`. +- Write-set fence: only the spec `## Write Set` files change (the spec Write Set was amended during planning to list `TaskMaster.Test/AppGlobals/SettingsSaveProbe.cs`, Decision D-A, and AC7 was amended to state the size exception for the two files already above the ceiling and, in the preflight revision, to read the ninety percent seam figures from the post-processed Cobertura document recorded in `coverage-comparison..md`, because the committed JaCoCo projection from `ConvertTo-JacocoPackageProjection` carries package-level counters only). A task that finds it needs any other file halts and reports to the orchestrator. + +## Decisions Record + +- D-A (test helper added to the Write Set): `TaskMaster.Test/AppGlobals/SettingsSaveProbe.cs` holds the thread-attributed, save-cancelling probe used by three TaskMaster.Test classes, so the probe is written once (General Code Change Policy, Reusability). It is a test file, registered in `TaskMaster.Test/TaskMaster.Test.csproj`. TaskTree.Test has one probe use and keeps it as a private static helper in `TaskTreeControllerMoveLogicTests`. +- D-B (probe event access): `ApplicationSettingsBase.SettingsSaving` uses explicit add and remove accessors over a plain delegate field, which are not atomic. Each probe performs its `+=` and `-=` inside `lock` on the settings instance so two concurrent probes on the same settings object cannot lose a subscription. The lock covers only the two statements, not the test body, so no test is serialized. +- D-C (visibility): `ISettingsStore`, `ApplicationSettingsStore`, and the new `IDList` constructors are `public` (UtilitiesCS and ToDoModel grant no `InternalsVisibleTo` to the consumers). The new store-taking constructors of `AppQuickFilerSettings`, `AppStagingFilenames`, and `AppAutoFileObjects` are `internal`, because `TaskMaster` grants `InternalsVisibleTo("TaskMaster.Test")` and C#5 prefers `internal`. +- D-D (IDList test file): `ToDoModel.Test/Data Model/ID/IDListGetNextToDoIDTests.cs` is 120 physical lines at the merge base, so the persistence-branch tests stay in it; `IDListMaxLengthPersistenceTests.cs` is not created and `ToDoModel.Test/ToDoModel.Test.csproj` is not edited, as the spec Write Set permits. +- D-E (Ribbon helper): `RibbonControllerTests.CreateController()` keeps its parameterless signature (called by `Engines_WhenGlobalsAssigned_ReturnsGlobalsEngines` in `TaskMaster.Test/Ribbon/RibbonControllerTests.Engines.cs` and by the folder-tree tests) and delegates to a new `CreateController(InMemorySettingsStore)` overload, so `RibbonControllerTests.Engines.cs` is unchanged. +- D-F (baseline at the merge base): the branch diff from MERGE-BASE to `HEAD` holds documentation only (P0-T3 verifies), so the baseline toolchain runs on the current tree before any code edit and measures the merge-base code. If P0-T3 finds a `.cs`, `.csproj`, or `.runsettings` path in that diff, the plan halts for a planner revision. + +## Test Name Register + +Names are binding. Existing names are kept so before-and-after evidence refers to the same tests. + +- `ApplicationSettingsStoreTests` (new): `Constructor_NullSettings_ThrowsArgumentNullException`, `Indexer_Get_ReturnsWrappedSettingsValue`, `Indexer_Set_WritesWrappedSettingsValue`, `Save_Invoked_ForwardsToWrappedSettingsOnce`. +- `IDListGetNextToDoIDTests` (added): `GetNextToDoID_FirstIdOnFreshList_AssignsLengthAndSavesOnce`, `GetNextToDoID_IdNotLongerThanCachedLength_DoesNotSaveAgain`, `GetNextToDoID_LongerIdAfterFirst_SavesAgain`, `Constructor_EnumerableWithNullStore_ThrowsArgumentNullException`, `Constructor_NullStoreOnly_ThrowsArgumentNullException`, `Constructor_StoreOnly_SavesThroughInjectedStore`, `Constructor_Default_UsesApplicationSettingsStore`. +- `AppQuickFilerSettingsTests` (added): `Constructor_Parameterless_UsesApplicationSettingsStore`, `Constructor_NullSettingsStore_ThrowsArgumentNullException`. +- `AppStagingFilenamesTests` (added): `SavingSetters_EachAssignment_WritesStoreAndSavesOnce`, `Getters_UnsetFields_ReadSeededStoreValues`, `Constructor_Parameterless_UsesApplicationSettingsStore`, `Constructor_NullSettingsStore_ThrowsArgumentNullException`. +- `AppAutoFileObjectsCoverageExpansionTests` (added): `Constructor_ParentOnly_UsesApplicationSettingsStore`, `Constructor_NullSettingsStore_ThrowsArgumentNullException`. +- Probe set (fail before, pass after): `TaskTreeControllerMoveLogicTests.MoveObjectsToChildren_DesyncedRoot_FiresMessageSeam`, `AppQuickFilerSettingsTests.HighConfidenceModeEnabled_WhenSetTrue_ReadsBackTrue`, `AppStagingFilenamesTests.ConditionalReminders_Setter_RoundTripsThroughSettingsDefault`, `AppAutoFileObjectsCoverageExpansionTests.ScalarSettingsProperties_WhenRoundTripped_UpdateDefaultsAndReturnValues`. + +## AC4 Search Definition + +The same five queries run before (P0-T10) and after (P6-T1); the after run adds `--untracked` so new files are searched. + +- Q1, production and test settings saves: the regular expression `Settings\.Default\.Save\(\)|_defaults\.Save\(\)|defaults\.Save\(\)` over every tracked `*.cs`, counted per file. Planning read of the tree: `ToDoModel/Data Model/ID/IDList.cs` 1, `TaskMaster/AppGlobals/AppQuickFilerSettings.cs` 6, `TaskMaster/AppGlobals/AppStagingFilenames.cs` 9, `TaskMaster/AppGlobals/AppAutoFileObjects.cs` 6, `TaskMaster/AppGlobals/AppOlObjects.JunkFolders.cs` 3, `TaskMaster/AppGlobals/AppOlObjects.cs` 1, `TaskMaster.Test/AppGlobals/AppAutoFileObjectsCoverageExpansionTests.cs` 1. Required after: only `AppOlObjects.JunkFolders.cs` 3 and `AppOlObjects.cs` 1 (the unreached sites the spec excludes). +- Q2, store saves: the token `_settingsStore.Save()` per file. Required after: `IDList.cs` 1, `AppQuickFilerSettings.cs` 6, `AppStagingFilenames.cs` 9, `TaskMaster/AppGlobals/AppAutoFileObjects.ScalarSettings.cs` 6. +- Q3, test constructions of the consumers: `new IDList(`, `new AppQuickFilerSettings(`, `new AppStagingFilenames(`, and `new AppAutoFileObjects(` across `*.Test/`. Each match is classified by its enclosing test as store-injected, or default-constructed with the test body calling none of the fifteen spec members and not calling `GetNextToDoID`. Required after: every match is one of those two classes. +- Q4, column-add calls: `AddQfcColumnsAsync(` across `*.Test/`, each call classified by whether it passes `timeProvider:`. Before: two calls without it in `UtilitiesCS.Test/Extensions/DfDeedle_COM_Tests.cs`. Required after: zero matches in that file and two in `UtilitiesCS.Test/Extensions/DfDeedle_COM_Tests.QfcColumnsAsync.cs`, each passing `timeProvider: new FakeTimeProvider()`; the `DfDeedleQfcColumnTimeoutTests` calls are unchanged. +- Q5, reach trace: for each test-project match of Q1 and Q3, the enclosing test and the production member it reaches, using the research reach map as the starting population. Required after: no test reaches an `ApplicationSettingsBase.Save()`. + +## Pre-Mortem + +- Review checklists: P7-T11 walks the policy-audit, code-review, and feature-audit checklists into `/evidence/qa-gates/self-review..md`; fail-before artifacts precede every fix (P1-T6, P1-T7) and pass-after artifacts follow each fix (P3-T4, P4-T11, P5-T2); the public-surface widening is justified in Decision D-C; the Ribbon `[DoNotParallelize]` decision rests on the recorded check in P4-T9. +- Coverage: P0-T7 and P7-T4 run TEST-COVERAGE over the whole suite; P7-T5 compares repository line and branch figures against the 80 and 75 percent floors, reports per-file line coverage of `ApplicationSettingsStore.cs` and `AppAutoFileObjects.ScalarSettings.cs` and per-member coverage of every new or changed seam member in `IDList`, `AppQuickFilerSettings`, and `AppStagingFilenames` (at least 90 percent), and derives changed-line coverage from the MERGE-BASE diff against the baseline Cobertura copy. `ApplicationSettingsStore` is covered through the provider-free `RecordingApplicationSettings` subclass; the seven untested `AppStagingFilenames` setters gain `SavingSetters_EachAssignment_WritesStoreAndSavesOnce`; default constructors gain the `UsesApplicationSettingsStore` tests. P6-T2 confirms no `[ExcludeFromCodeCoverage]` and no `coverage.config` change. +- Mirror parity: not applicable. No `.claude`, `.agents`, `.codex`, `.github`, or bundled resource file changes, and no fact stated by the execution-environment brief (CI jobs, required contexts, tool routes, batch budget) is altered, so no brief-update task exists. +- File size: P0-T8 and P7-T6 record physical line counts (not `Measure-Object -Line`). `DfDeedle_COM_Tests.cs` (848 at planning) and `AppAutoFileObjects.cs` (853 at planning) lose a section to a new partial and must end smaller than at MERGE-BASE; `TaskTreeControllerMoveLogicTests.cs` (414) and `RibbonControllerTests.cs` (452) are the nearest to the ceiling and gain at most the probe or the store wiring. +- Test isolation: Jest, Python, and Pester isolation runs are not applicable, because no TypeScript, Python, or PowerShell file changes. The C# isolation runs are P6-T4: five consecutive SCOPED-TEST runs of all affected classes in one process under the class-level parallel runsettings, then each affected class alone in its own process in reverse register order. The probes are thread-attributed, cancel only their own thread's saves, lock only around subscribe and unsubscribe (D-B), and always unsubscribe; every fake store is per test. +- CI-only behavior: CI passes no runsettings, so class-level parallelism is never exercised in CI; the local TEST-COVERAGE run (which applies `TaskMaster.cli.runsettings`) and P6-T4 are the parallel evidence. New files carry explicit `` entries, which a cold CI build would otherwise surface as missing types, and FORMAT runs before every final gate so format-check passes. The four shell-icon classes run only in CI if the carve-out is used. +- Evidence: every task names its artifact under `/evidence//`; artifacts carry the four required fields and record `SCRATCH` instead of host paths; the DfDeedle fail-before is a schema-valid `fail-before-exception..md` dossier (P1-T7); P7-T7 verifies that no raw trx or Cobertura document is committed and that no evidence file names a drive-rooted or user-profile path. + +## Size Justification + +The plan holds 59 tasks, above the 40-task guideline. The item fixes one wall-clock test and fifteen settings-saving production members across three assemblies, and the bugfix workflow requires a fail-before step for each of five fix groups (IDList and three TaskMaster classes by probe, DfDeedle by dossier) before the matching fix. The spec Write Set holds seven production files and fourteen test or project files, and each production file and each converted test class is its own atomic task so a failure identifies one file. The acceptance-criteria-tracking protocol requires one check-off task per criterion (eight) plus a reconciliation task, and the spec names before and after AC4 searches, a Ribbon static-state record, and repeated parallel runs, each with its own producing task. Splitting the item would separate the shared `ISettingsStore` seam from its consumers and break the single write-set fence. + +### Phase 0 — Policy Reads and Baseline Capture + +- [x] [P0-T1] Read, in `policy-compliance-order` order, `CLAUDE.md`, `.claude/rules/general-code-change.md`, `.claude/rules/general-unit-test.md`, `.claude/rules/csharp.md`, then `.claude/rules/plan-acceptance-gates.md`, `.claude/skills/execution-environment/SKILL.md`, `.claude/skills/evidence-and-timestamp-conventions/SKILL.md`, `.claude/skills/acceptance-criteria-tracking/SKILL.md`, `/spec.md`, and `/research/2026-10-22T15-21-parallel-test-isolation-wall-clock-and-user-config-research.md`; copy `/spec.md` to `SCRATCH` as `issue-1005-spec-baseline.md` for the P7-T10 criterion-text comparison; write `/evidence/baseline/phase0-instructions-read..md` with `Timestamp:`, `Policy Order:`, the branch name, and the list of files read. Acceptance: the artifact exists and names every file in this task, and the spec copy exists under `SCRATCH`. +- [x] [P0-T2] Bootstrap the toolchain with `scripts/vscode/Install-RepoDotNetSdk.ps1` (only if the pinned SDK is absent), `dotnet tool restore`, and `scripts/vscode/Invoke-Restore.ps1`; write `/evidence/baseline/baseline-toolchain-bootstrap..md`. Acceptance: each command records `EXIT_CODE: 0`, and `git status --porcelain` captured before and after the restore shows no new tracked-file change. +- [x] [P0-T3] Fetch `origin/main`, compute MERGE-BASE, and record `git diff --name-status HEAD` plus `git status --porcelain` in `/evidence/baseline/merge-base-scope..md` (the merge-base SHA is recorded in the artifact, not in this plan). Acceptance: no path in either listing ends in `.cs`, `.csproj`, or `.runsettings`; otherwise halt for a planner revision per Decision D-F. +- [x] [P0-T4] Record the read-only FORMAT baseline by running only `dotnet tool run csharpier check .` from the repository root (no `format`, so pre-existing drift is recorded rather than repaired); write `/evidence/baseline/baseline-toolchain-format..md`. Acceptance: the artifact records the exit code and either the success line beginning `Checked ` or the list of drifting files. +- [x] [P0-T5] Run ANALYZE on `TaskMaster.sln`; write `/evidence/baseline/baseline-toolchain-analyzers..md`. Acceptance: the artifact records the exit code, the `Error(s)` and `Warning(s)` counts, and every warning whose path is a Write Set file. +- [x] [P0-T6] Run TYPECHECK on `TaskMaster.sln`; write `/evidence/baseline/baseline-toolchain-typecheck..md`. Acceptance: the artifact records the exit code and the `Error(s)` count. +- [x] [P0-T7] Run TEST-COVERAGE through `scripts/vscode/Invoke-MSTestWithCoverage.ps1` (shell-icon carve-out applies); copy the JaCoCo projection to `/evidence/baseline/baseline-coverage-projection..jacoco.xml` and copy the post-processed Cobertura document to `SCRATCH` as `issue-1005-baseline-cobertura.xml` (never committed); write `/evidence/baseline/baseline-coverage..md`. Acceptance: the artifact records the runner route used, passed, failed, and total counts from the trx summary, the one-line first-party coverage summary, numeric repository line and branch percentages, and numeric per-file line coverage for `ToDoModel/Data Model/ID/IDList.cs`, `TaskMaster/AppGlobals/AppQuickFilerSettings.cs`, `TaskMaster/AppGlobals/AppStagingFilenames.cs`, and `TaskMaster/AppGlobals/AppAutoFileObjects.cs`, and the number of `DfDeedle_COM_Tests` tests executed (derived from the trx, for the P5-T2 comparison); no value is a placeholder; any failed test is listed by name. +- [x] [P0-T8] Record physical line counts for every existing Write Set `.cs` file, including `UtilitiesCS.Test/Extensions/DfDeedle_COM_Tests.cs` and `TaskMaster/AppGlobals/AppAutoFileObjects.cs`; write `/evidence/baseline/baseline-file-sizes..md`. Acceptance: one numeric count per file. +- [x] [P0-T9] Search every `*.Test/` tree for the symbols this plan changes (`IDList`, `GetNextToDoID`, `AppQuickFilerSettings`, `AppStagingFilenames`, `AppAutoFileObjects`, `CreateController`, `CreateSutWithSpecialFolders`, `AddQfcColumnsAsync`) and write `/evidence/baseline/baseline-test-disposition..md` with one row per matching test class and its disposition. Acceptance: the artifact lists as updated the classes this plan edits (`IDListGetNextToDoIDTests`, `TaskTreeControllerMoveLogicTests`, `TaskTreeControllerRouteDropTests`, `AppQuickFilerSettingsTests`, `AppQuickFilerSettingsRemainingPropertiesTests`, `AppStagingFilenamesTests`, `AppAutoFileObjectsCoverageExpansionTests`, `RibbonControllerTests`, `DfDeedle_COM_Tests`) and as unchanged `AppAutoFileObjectsFolderPredictorTests` (parent-only constructor, no scalar member), `RibbonControllerTests.Engines.cs` (Decision D-E), and `DfDeedleQfcColumnTimeoutTests` (already passes both seams); any further match carries a disposition, and a match that must change outside the Write Set halts the plan. +- [x] [P0-T10] Run the AC4 Search Definition queries Q1 to Q5 against the unmodified tree; write `/evidence/regression-testing/ac4-search-before..md` with each query, its per-file counts, and the Q3, Q4, and Q5 classifications. Acceptance: Q1 reports the seven per-file counts listed in the definition, Q4 reports the two calls without `timeProvider:` in `DfDeedle_COM_Tests.cs`, and Q5 lists every reaching test named in the research reach map plus the `TestCleanup` direct save. + +### Phase 1 — Fail-Before Evidence (probes and dossier) + +The probes compile against the unmodified production code. Each probe assertion is placed after the test's existing assertions so the recorded failure is the probe's. + +- [x] [P1-T1] Create `TaskMaster.Test/AppGlobals/SettingsSaveProbe.cs` (registered in `TaskMaster.Test/TaskMaster.Test.csproj`): an `internal static class SettingsSaveProbe` whose `CountSavesOnCurrentThread(ApplicationSettingsBase settings, System.Action act)` captures `Environment.CurrentManagedThreadId`, subscribes a `SettingsSaving` handler that, only for that thread ID, increments a count and sets `e.Cancel = true` (the handler delegate is created once into a local, and that same local is passed to both the subscribe and the unsubscribe; wherever the probe reads a delegate held in a field before invoking it, it copies the field to a local once and performs the null check and the invoke on that local, so a concurrent unsubscribe cannot produce a null dereference), runs `act`, unsubscribes in `finally`, performs the subscribe and unsubscribe inside `lock (settings)` (Decision D-B, with a why-comment), and returns the count; XML documentation states the thread attribution and that cancellation prevents any write. Acceptance: P1-T6 builds and the class holds only that member. +- [x] [P1-T2] [expect-fail] In `TaskTree.Test/TaskTreeControllerMoveLogicTests.cs`, add a private static helper with the same contract as `SettingsSaveProbe.CountSavesOnCurrentThread` that resolves `ToDoModel.Properties.Settings.Default` by reflection (`typeof(IDList).Assembly.GetType("ToDoModel.Properties.Settings")`, public static `Default`, cast to `ApplicationSettingsBase`), and wrap the Act of `MoveObjectsToChildren_DesyncedRoot_FiresMessageSeam` in it, asserting after the existing assertions that the recorded save count is zero with a message stating that the test persisted settings to the shared per-host settings file (the message does not spell the file name, so the P6-T2 added-line scan for that token stays at zero). Acceptance: the file compiles in P1-T6 under C# 7.3 and stays at or under 500 physical lines. +- [x] [P1-T3] [expect-fail] In `TaskMaster.Test/AppGlobals/AppQuickFilerSettingsTests.cs`, wrap the Act of `HighConfidenceModeEnabled_WhenSetTrue_ReadsBackTrue` in `SettingsSaveProbe.CountSavesOnCurrentThread(Settings.Default, ...)` and assert after the existing assertion that the count is zero. Acceptance: compiles in P1-T6. +- [x] [P1-T4] [expect-fail] In `TaskMaster.Test/AppGlobals/AppStagingFilenamesTests.cs`, apply the same probe to the Act of `ConditionalReminders_Setter_RoundTripsThroughSettingsDefault`. Acceptance: compiles in P1-T6. +- [x] [P1-T5] [expect-fail] In `TaskMaster.Test/AppGlobals/AppAutoFileObjectsCoverageExpansionTests.cs`, apply the same probe (over `TaskMaster.Properties.Settings.Default`) to the Act of `ScalarSettingsProperties_WhenRoundTripped_UpdateDefaultsAndReturnValues`. Acceptance: compiles in P1-T6. +- [x] [P1-T6] [expect-fail] Run ANALYZE, then SCOPED-TEST over `TaskTree.Test\bin\Debug\TaskTree.Test.dll` and `TaskMaster.Test\bin\Debug\TaskMaster.Test.dll` filtered to the four probe-set tests; write `/evidence/regression-testing/settings-probe-fail-before..md` with `ExpectedExitCode: 1`, recording the vstest command and its `EXIT_CODE:` as the first `Command:` and `EXIT_CODE:` fields of the artifact (the ANALYZE result follows), because the first occurrence forms the machine-read record. Acceptance: `0 Error(s)`; each of the four probe-set tests is reported Failed with the probe assertion message recording at least one save on the test thread; no other assertion in those tests is the reported failure. +- [x] [P1-T7] Write `/evidence/regression-testing/fail-before-exception..md` for the DfDeedle path with `Timestamp:`, `Command:` (the P0-T10 Q4 query), `EXIT_CODE:`, `Output Summary:`, `WhyFailingRunImpossible:` (the failure needs three real 3000 ms deadlines to elapse under parallel load; a deterministic local run either completes before the deadline or needs real timers, which the constraints forbid), the maintainer's captured exception quoted from `/issue.md` (`TimeoutException: The column add step timed out after 9000 ms for folder '(unknown folder)'`), and an alternative-proof section citing `ac4-search-before..md` (two calls without `timeProvider:` before the fix) as the repeatable demonstration. Acceptance: all named fields are present and the quoted exception text matches `issue.md`. + +### Phase 2 — Settings-Store Seam (UtilitiesCS) + +- [x] [P2-T1] Create `UtilitiesCS/HelperClasses/Settings/ISettingsStore.cs` (`#nullable enable`, namespace `UtilitiesCS`, registered in `UtilitiesCS/UtilitiesCS.csproj`): `public interface ISettingsStore` with `object this[string propertyName] { get; set; }` and `void Save();`, with XML documentation stating that `Save` persists every pending value, mirroring `ApplicationSettingsBase`. Acceptance: P2-T4 builds. +- [x] [P2-T2] Create `UtilitiesCS/HelperClasses/Settings/ApplicationSettingsStore.cs` (`#nullable enable`, namespace `UtilitiesCS`, registered in `UtilitiesCS/UtilitiesCS.csproj`): `public sealed class ApplicationSettingsStore : ISettingsStore` holding a `private readonly ApplicationSettingsBase`, whose constructor throws `ArgumentNullException` for null, whose indexer forwards get and set to the wrapped instance, and whose `Save()` forwards to the wrapped `Save()`; XML documentation on the type and constructor. Acceptance: P2-T4 builds and the four adapter tests pass. +- [x] [P2-T3] Create `UtilitiesCS.Test/HelperClasses/Settings/ApplicationSettingsStoreTests.cs` (`#nullable enable`, registered in `UtilitiesCS.Test/UtilitiesCS.Test.csproj`) with a nested `RecordingApplicationSettings : ApplicationSettingsBase` that overrides the indexer (dictionary-backed) and `Save()` (increments a counter) without calling the base members, so no provider runs and no file is touched, and the four `ApplicationSettingsStoreTests` register tests in Arrange-Act-Assert form. Acceptance: the class holds exactly the four register names. +- [x] [P2-T4] Run ANALYZE, then SCOPED-TEST over `UtilitiesCS.Test\bin\Debug\UtilitiesCS.Test.dll` filtered to `ApplicationSettingsStoreTests`; write `/evidence/regression-testing/settings-store-adapter-tests..md`. Acceptance: `0 Error(s)`, `EXIT_CODE: 0`, four passed, zero failed. If the net481 `ApplicationSettingsBase` indexer or `Save()` proves not overridable, the task halts and reports, because the spec fallback changes the test design. + +### Phase 3 — IDList Fix (AC2, AC3) + +- [x] [P3-T1] In `ToDoModel/Data Model/ID/IDList.cs`, add `private readonly ISettingsStore _settingsStore = new ApplicationSettingsStore(Properties.Settings.Default);` with a why-comment, add public constructors `IDList(IEnumerable ids, ISettingsStore settingsStore)` (chaining `base(ids)`) and `IDList(ISettingsStore settingsStore)` (chaining `base()`), each assigning the store after a null guard that throws `ArgumentNullException`, with XML documentation; in `GetNextToDoID(string)` replace the two `Properties.Settings.Default` lines with `_settingsStore[nameof(Properties.Settings.MaxLengthOfID)] = _maxIDLength;` followed by `_settingsStore.Save();`, preserving the order add, persist when longer, serialize. No other member, `IIDList`, `TreeOfToDoItems`, `TaskTreeController`, or `AppToDoObjects` changes. Acceptance: P3-T4 builds and passes. +- [x] [P3-T2] In `ToDoModel.Test/Data Model/ID/IDListGetNextToDoIDTests.cs` (C# 7.3), delete `CreateListWithCachedMaxLength` and its `MaxLengthOfID` pre-read, construct every list with `new IDList(ids, store.Object)` over a per-test `Mock`, update the class summary, and add the seven `IDListGetNextToDoIDTests` register tests: a fresh list's first ID from seed "0001" assigns the indexer key `MaxLengthOfID` the value 2 and calls `Save` once; after a first ID "0100" from seed "ZZ" (one `Save`), a second ID from seed "0001" produces the shorter "02" and adds no `Save` (AC2 "a shorter ID does not save"); a later ID from seed "ZZ" assigns 4 and brings `Save` to twice; `new IDList(new List(), null)` and `new IDList((ISettingsStore)null)` throw `ArgumentNullException`; the store-only constructor yields an empty list that saves through the mock; and `new IDList()` holds an `ApplicationSettingsStore` in its private `_settingsStore` field (read by reflection, no setting read). Acceptance: no test in the file constructs an `IDList` whose `GetNextToDoID` can reach `ApplicationSettingsBase.Save()`, and the file stays under 500 physical lines. +- [x] [P3-T3] In `TaskTree.Test/TaskTreeControllerMoveLogicTests.cs` and `TaskTree.Test/TaskTreeControllerRouteDropTests.cs` (C# 7.3), change `MakeController` to return `new IDList(new List(), settingsStore.Object)` from `mockTD.Setup(x => x.IDList)` with a `Mock` created per call (the MoveLogic `idList` parameter keeps precedence when supplied), and in `MoveObjectsToChildren_DesyncedRoot_FiresMessageSeam` expose that mock so the test also verifies `Save()` was called once on it. Acceptance: no `new IDList()` without a store remains in either file, and both files stay under 500 physical lines. +- [x] [P3-T4] Run ANALYZE, then SCOPED-TEST over `ToDoModel.Test\bin\Debug\ToDoModel.Test.dll` and `TaskTree.Test\bin\Debug\TaskTree.Test.dll` filtered to `IDListGetNextToDoIDTests`, `TaskTreeControllerMoveLogicTests`, and `TaskTreeControllerRouteDropTests`; write `/evidence/regression-testing/idlist-probe-pass-after..md`. Acceptance: `0 Error(s)`, `EXIT_CODE: 0`, zero failed, and `MoveObjectsToChildren_DesyncedRoot_FiresMessageSeam` plus every `IDListGetNextToDoIDTests` register test listed as Passed. +- [x] [P3-T5] Check off AC2 in `/spec.md` citing P3-T4 and `ToDoModel/Data Model/ID/IDList.cs`. Acceptance: only the AC2 box changes. +- [x] [P3-T6] Check off AC3 in `/spec.md` citing P3-T4 (`MoveObjectsToChildren_DesyncedRoot_FiresMessageSeam` with zero probe saves and one mock `Save()`) and `/evidence/regression-testing/idlist-probe-pass-after..md`. Acceptance: only the AC3 box changes. + +### Phase 4 — TaskMaster Settings Writers (AC4, AC8) + +- [x] [P4-T1] In `TaskMaster/AppGlobals/AppQuickFilerSettings.cs`, add `private readonly ISettingsStore _settingsStore`, a public parameterless constructor delegating to the adapter over `Settings.Default`, and an internal `AppQuickFilerSettings(ISettingsStore settingsStore)` that throws `ArgumentNullException` for null (Decision D-C); each of the six getters casts the store value for `nameof(Settings.)` (`MoveEntireConversations`, `SaveAttachments`, `SavePictures`, `SaveEmailCopy`, `HighConfidenceModeEnabled`, `HighConfidenceThreshold`), and each internal setter writes the store and calls `_settingsStore.Save()`. `ApplicationGlobals` is unchanged. Acceptance: P4-T11 builds and passes. +- [x] [P4-T2] In `TaskMaster/AppGlobals/AppStagingFilenames.cs`, add the same field and constructor pair (internal store constructor), route all ten `InitProp` getter reads through the store, and route the nine saving setters through the store followed by `_settingsStore.Save()`, keeping each backing-field assignment; `EmailInfoStagingFile` stays non-persisting. Acceptance: P4-T11 builds and passes. +- [x] [P4-T3] Create `TaskMaster/AppGlobals/AppAutoFileObjects.ScalarSettings.cs` (registered in `TaskMaster/TaskMaster.csproj`) as a partial of `AppAutoFileObjects` holding `private readonly ISettingsStore _settingsStore`, the internal constructor `AppAutoFileObjects(IApplicationGlobals parent, ISettingsStore settingsStore)` (null guard, assigns `_parent`), and the six scalar properties `LngConvCtPwr` (`ConversationExponent`), `Conversation_Weight` (`ConversationWeight`), `SmithWatterman_MatchScore`, `SmithWatterman_MismatchScore`, `SmithWatterman_GapPenalty`, and `MaxRecents` reading and writing through the store with `_settingsStore.Save()` in each setter; in `TaskMaster/AppGlobals/AppAutoFileObjects.cs` delete those six properties and change the existing constructor to chain to the new one with `new ApplicationSettingsStore(Properties.Settings.Default)`; `_defaults` and every file-name read stay. Acceptance: P4-T11 builds and passes, and `AppAutoFileObjects.cs` has fewer physical lines than its P0-T8 count. +- [x] [P4-T4] Create `TaskMaster.Test/AppGlobals/InMemorySettingsStore.cs` (registered in `TaskMaster.Test/TaskMaster.Test.csproj`): an `internal sealed class InMemorySettingsStore : ISettingsStore` backed by a per-instance `Dictionary`, whose getter throws `KeyNotFoundException` naming the key when it is unseeded, whose setter stores the value, and whose `Save()` increments a public `SaveCount`. Acceptance: P4-T11 builds. +- [x] [P4-T5] In `TaskMaster.Test/AppGlobals/AppQuickFilerSettingsTests.cs`, remove the class-level `[DoNotParallelize]`, the snapshot fields, `TestInitialize`, and `TestCleanup`; construct every system under test with a per-test seeded `InMemorySettingsStore`; assert values against the fake and `SaveCount` (zero for getter-only tests, one per setter call); keep the P1-T3 probe; update the class summary; and add the two `AppQuickFilerSettingsTests` register tests. Acceptance: the file contains no `Settings.Default` write and no `DoNotParallelize`. +- [x] [P4-T6] In `TaskMaster.Test/AppGlobals/AppQuickFilerSettingsRemainingPropertiesTests.cs`, remove the snapshot fields, `TestInitialize`, and `TestCleanup`; construct with a seeded `InMemorySettingsStore`; assert getter reads and setter round-trips against the fake with `SaveCount` one per setter call; update the class summary. Acceptance: the file contains no `Settings.Default` reference. +- [x] [P4-T7] In `TaskMaster.Test/AppGlobals/AppStagingFilenamesTests.cs`, remove the snapshot fields, `TestInitialize`, and `TestCleanup`; construct with a seeded `InMemorySettingsStore`; assert against the fake (the cached-field test changes the fake underneath instead of `Settings.Default`; `EmailInfoStagingFile_Setter_DoesNotWriteSettingsDefault` asserts the fake value unchanged and `SaveCount` zero); keep the P1-T4 probe; add the four `AppStagingFilenamesTests` register tests, where `SavingSetters_EachAssignment_WritesStoreAndSavesOnce` assigns all nine saving properties and asserts each store key and a `SaveCount` of nine, and `Getters_UnsetFields_ReadSeededStoreValues` reads all ten properties from seeded keys; update the class summary, which describes the `Settings.Default` snapshot pattern. Acceptance: the file contains no `Settings.Default` reference other than the single `SettingsSaveProbe.CountSavesOnCurrentThread` argument kept from P1-T4. +- [x] [P4-T8] In `TaskMaster.Test/AppGlobals/AppAutoFileObjectsCoverageExpansionTests.cs`, delete the six snapshot fields, `TestInitialize`, and `TestCleanup` including the direct `defaults.Save()`; give `CreateSutWithSpecialFolders` a store argument and pass a per-test `InMemorySettingsStore` seeded with the six scalar keys; make `ScalarSettingsProperties_WhenRoundTripped_UpdateDefaultsAndReturnValues` assert the fake values and a `SaveCount` of six while keeping the P1-T5 probe; add the two `AppAutoFileObjectsCoverageExpansionTests` register tests. Acceptance: the file contains no `Properties.Settings.Default` reference other than the single `SettingsSaveProbe.CountSavesOnCurrentThread` argument kept from P1-T5, and no `Save()` call. +- [x] [P4-T9] Read `TaskMaster.Test/Ribbon/RibbonControllerTests.cs` and `TaskMaster.Test/Ribbon/RibbonControllerTests.Engines.cs` in full, and every production member they reach (`RibbonController`, its `Intelligence` and `FolderTree` partials, and the `ApplicationGlobals` members set by reflection), for process-global static state other than the settings snapshot: static fields and properties, singletons, UI-thread or dispatcher statics, log4net configuration, environment variables, and other `Settings.Default` members; write `/evidence/other/ribbon-static-state-check..md` with one row per static member found, whether it is mutable and written by a test in the class, and a verdict of `REMOVE` or `KEEP` with the state that requires serialization. Acceptance: the artifact carries the file list read, the rows, and exactly one verdict. +- [x] [P4-T10] In `TaskMaster.Test/Ribbon/RibbonControllerTests.cs`, remove the snapshot fields, `TestInitialize`, and `TestCleanup`; add `CreateController(InMemorySettingsStore store)` that sets `_quickFilerSettings` to `new AppQuickFilerSettings(store)`, with the parameterless `CreateController()` delegating to it with a store seeded with both high-confidence keys (Decision D-E); arrange and assert each high-confidence test against its own fake, including `SaveCount` where a setter runs; update the class summary; remove the class-level `[DoNotParallelize]` only when P4-T9 recorded `REMOVE`. Acceptance: the file contains no `Settings.Default` reference, stays under 500 physical lines, and its attribute state matches the P4-T9 verdict. +- [x] [P4-T11] Run ANALYZE, then SCOPED-TEST over `TaskMaster.Test\bin\Debug\TaskMaster.Test.dll` filtered to `AppQuickFilerSettingsTests`, `AppQuickFilerSettingsRemainingPropertiesTests`, `AppStagingFilenamesTests`, `AppAutoFileObjectsCoverageExpansionTests`, `AppAutoFileObjectsFolderPredictorTests`, and `RibbonControllerTests`; write `/evidence/regression-testing/taskmaster-settings-probe-pass-after..md`. Acceptance: `0 Error(s)`, `EXIT_CODE: 0`, zero failed, and the three TaskMaster probe-set tests and every TaskMaster register test listed as Passed. +- [x] [P4-T12] Check off AC8 in `/spec.md` citing P4-T5, P4-T10, and `/evidence/other/ribbon-static-state-check..md`. Acceptance: only the AC8 box changes. + +### Phase 5 — DfDeedle Tests (AC1) + +- [x] [P5-T1] Mark `DfDeedle_COM_Tests` `partial` in `UtilitiesCS.Test/Extensions/DfDeedle_COM_Tests.cs` and move the `AddQfcColumnsAsync` section (both tests) into new `UtilitiesCS.Test/Extensions/DfDeedle_COM_Tests.QfcColumnsAsync.cs` (registered in `UtilitiesCS.Test/UtilitiesCS.Test.csproj`, with the using directives and nullable context it needs), converting both to `public async Task` tests that pass `timeProvider: new FakeTimeProvider()` (never advanced) with `columnAdder` left at the production default and contain no `GetAwaiter().GetResult()` or real wait: `AddQfcColumnsAsync_HappyPath_CompletesWithoutThrowing` awaits `NotThrowAsync` and verifies the mocked `Columns.Add("SentOn")` once; `AddQfcColumnsAsync_PreCancelledToken_CompletesGracefully` awaits `NotThrowAsync` on a pre-cancelled token. No file under `UtilitiesCS/Extensions/` changes. Acceptance: `DfDeedle_COM_Tests.cs` has fewer physical lines than its P0-T8 count and contains no `AddQfcColumnsAsync(` call. +- [x] [P5-T2] Run ANALYZE, then SCOPED-TEST over `UtilitiesCS.Test\bin\Debug\UtilitiesCS.Test.dll` filtered to `DfDeedle_COM_Tests` and `DfDeedleQfcColumnTimeoutTests`; write `/evidence/regression-testing/dfdeedle-pass-after..md`. Acceptance: `0 Error(s)`, `EXIT_CODE: 0`, zero failed, both converted tests listed as Passed, and the same number of `DfDeedle_COM_Tests` tests as at P0-T7. +- [x] [P5-T3] Check off AC1 in `/spec.md` citing P5-T2 and `UtilitiesCS.Test/Extensions/DfDeedle_COM_Tests.QfcColumnsAsync.cs`. Acceptance: only the AC1 box changes. + +### Phase 6 — AC4 After-Search, Constraint Scans, and Parallel Runs + +- [x] [P6-T1] Run the AC4 Search Definition queries Q1 to Q5 with untracked files included; write `/evidence/regression-testing/ac4-search-after..md` beside a reference to `ac4-search-before..md`. Acceptance: every "Required after" outcome in the definition holds, and every reaching test listed by the before search is shown converted. +- [x] [P6-T2] Scan the added lines of `git diff -- "*.cs" "*.runsettings" "*.config"` and every untracked `.cs` file listed by `git status --porcelain` for `DoNotParallelize`, `Workers`, `Retry`, `Thread.Sleep`, `Task.Delay`, `Timeout`, `GetTempPath`, `GetTempFileName`, `user.config`, `SettingsProvider`, and `ExcludeFromCodeCoverage`, and confirm `coverage.config`, `TaskMaster.runsettings`, and `scripts/vscode/TaskMaster.cli.runsettings` are unchanged; write `/evidence/qa-gates/parallel-constraints-scan..md` with each token and its added-line count. Acceptance: every count is zero; removal lines are reported separately and are allowed. +- [x] [P6-T3] Write `/evidence/qa-gates/scope-fence..md` from `git diff --name-status ` plus `git status --porcelain`. Acceptance: every listed path is a spec Write Set file, `TaskMaster.Test/AppGlobals/SettingsSaveProbe.cs`, a path under `/`, or a pre-existing path already listed in the P0-T3 `merge-base-scope` artifact (the promotion records under `docs/features/potential/promoted/` and the `.claude/agent-memory/` changes), or a path under `.claude/agent-memory/` (agent memory written outside the plan's tasks); `ToDoModel.Test/ToDoModel.Test.csproj` and `TaskMaster.Test/Ribbon/RibbonControllerTests.Engines.cs` are absent; no other `.claude/` path and no file under `UtilitiesCS/Extensions/` appears. +- [x] [P6-T4] Run ANALYZE once, then SCOPED-TEST five consecutive times over `UtilitiesCS.Test\bin\Debug\UtilitiesCS.Test.dll`, `ToDoModel.Test\bin\Debug\ToDoModel.Test.dll`, `TaskTree.Test\bin\Debug\TaskTree.Test.dll`, and `TaskMaster.Test\bin\Debug\TaskMaster.Test.dll` in one `vstest.console.exe` invocation, filtered to `DfDeedle_COM_Tests`, `ApplicationSettingsStoreTests`, `IDListGetNextToDoIDTests`, `TaskTreeControllerMoveLogicTests`, `TaskTreeControllerRouteDropTests`, `AppQuickFilerSettingsTests`, `AppQuickFilerSettingsRemainingPropertiesTests`, `AppStagingFilenamesTests`, `AppAutoFileObjectsCoverageExpansionTests`, and `RibbonControllerTests`; then run each of those ten classes alone in its own `vstest.console.exe` invocation in reverse of that order; write `/evidence/qa-gates/parallel-repeat-runs..md` with the runsettings path, each run's class set and counts, and the elapsed time of `AddQfcColumnsAsync_HappyPath_CompletesWithoutThrowing` in each run that includes `DfDeedle_COM_Tests` (the five combined runs and the `DfDeedle_COM_Tests` solo run). Acceptance: zero failures in all fifteen runs and every run matches at least one test. +- [x] [P6-T5] Check off AC4 in `/spec.md` citing `ac4-search-before..md` and `ac4-search-after..md` under `/evidence/regression-testing/`. Acceptance: only the AC4 box changes. +- [x] [P6-T6] Check off AC5 in `/spec.md` citing `settings-probe-fail-before..md`, `fail-before-exception..md`, `idlist-probe-pass-after..md`, and `taskmaster-settings-probe-pass-after..md` under `/evidence/regression-testing/`. Acceptance: only the AC5 box changes. + +### Phase 7 — Final QA Loop, Remaining Check-Offs, and Self-Review + +Loop rule: P7-T1 to P7-T4 form one toolchain pass. If any of them fails or changes a file, fix the cause and restart at P7-T1, writing a fresh artifact for each step of the pass recorded as clean. Editing `spec.md` checkboxes and writing Markdown evidence changes no toolchain input and does not restart the loop. + +- [x] [P7-T1] Capture `git status --porcelain` and the SHA-256 of every created or modified Write Set `.cs` file, run `dotnet tool run csharpier format .`, capture both again, then run `dotnet tool run csharpier check .`; write `/evidence/qa-gates/final-toolchain-format..md` with both captures. The rewritten-file count is the number of hash or status differences, not the `Formatted N files` line, which counts processed files. Acceptance: the rewritten-file count is zero and the check exits 0 with a line beginning `Checked `; a non-zero count restarts the loop, and a rewrite of a file outside the Write Set halts the plan. +- [x] [P7-T2] Run ANALYZE on `TaskMaster.sln`; write `/evidence/qa-gates/final-toolchain-analyzers..md`. Acceptance: `EXIT_CODE: 0`, `0 Error(s)`, and no warning whose path is a created or modified Write Set file. +- [x] [P7-T3] Run TYPECHECK on `TaskMaster.sln`; write `/evidence/qa-gates/final-toolchain-typecheck..md`. Acceptance: `EXIT_CODE: 0` and `0 Error(s)`. +- [x] [P7-T4] Run TEST-COVERAGE through `scripts/vscode/Invoke-MSTestWithCoverage.ps1` (shell-icon carve-out applies); copy the JaCoCo projection to `/evidence/qa-gates/final-coverage-projection..jacoco.xml`; write `/evidence/qa-gates/final-toolchain-tests..md` with the runner route, the runsettings path, the trx-summary counts, the one-line first-party coverage summary, and the outcome of every probe-set and register test. Acceptance: zero failed across the whole suite under the class-level parallel runsettings, and every probe-set and register test appears as Passed. +- [x] [P7-T5] Write `/evidence/qa-gates/coverage-comparison..md` from `SCRATCH` copy `issue-1005-baseline-cobertura.xml` and the P7-T4 Cobertura document: baseline and final repository line and branch percentages against the 80 and 75 percent floors; line coverage of `UtilitiesCS/HelperClasses/Settings/ApplicationSettingsStore.cs` and `TaskMaster/AppGlobals/AppAutoFileObjects.ScalarSettings.cs`; per-member line coverage of the new constructors and `GetNextToDoID` in `IDList`, and of the constructors, getters, and setters in `AppQuickFilerSettings` and `AppStagingFilenames`; changed-line coverage per modified production file, computed as the covered share of the added or modified lines in the MERGE-BASE diff that Cobertura instruments, beside the baseline coverage of the lines they replaced. Acceptance: each new file and new or changed seam member is at least 90 percent, no changed-line figure is below the baseline coverage of the lines it replaced, and the final repository figures are not below the baseline; a shortfall is recorded as remediation-required, not PASS. +- [x] [P7-T6] Write `/evidence/qa-gates/file-sizes..md` with the physical line count of every created or modified `.cs` file beside its P0-T8 value. Acceptance: every count is at most 500, except that `UtilitiesCS.Test/Extensions/DfDeedle_COM_Tests.cs` and `TaskMaster/AppGlobals/AppAutoFileObjects.cs` need only be below their P0-T8 counts. +- [x] [P7-T7] Write `/evidence/qa-gates/evidence-form..md` from `git diff --name-status ` plus `git status --porcelain` over `/evidence/`. Acceptance: no path ends in `.trx` or `.coverage`, the only `.xml` files are `.jacoco.xml` projection copies named `baseline-coverage-projection.*.jacoco.xml` or `final-coverage-projection.*.jacoco.xml` (a restarted QA loop can leave more than one final copy), no path contains `cobertura`, and a search of `/evidence/` for a drive-rooted path or a user-profile segment returns zero. +- [x] [P7-T8] Check off AC6 in `/spec.md` citing `final-toolchain-tests..md`, `parallel-repeat-runs..md`, and `parallel-constraints-scan..md` under `/evidence/qa-gates/`. Acceptance: only the AC6 box changes. +- [x] [P7-T9] Check off AC7 in `/spec.md` citing the four `final-toolchain-` artifacts from one clean pass, `coverage-comparison..md`, and `file-sizes..md` under `/evidence/qa-gates/`, only when the comparison records no shortfall. Acceptance: only the AC7 box changes. +- [x] [P7-T10] Reconcile `/spec.md` `## Acceptance Criteria`: count checked and unchecked boxes and write `/evidence/qa-gates/ac-reconciliation..md` with the acceptance-criteria-tracking status summary. Acceptance: 8 checked and 0 unchecked, with every criterion text unchanged, verified by comparing each AC line, with its checkbox marker normalized, against the same line of the `SCRATCH` copy `issue-1005-spec-baseline.md` taken at P0-T1 (`spec.md` is untracked at planning time, so no committed revision of it can serve as the comparison base). +- [x] [P7-T11] Walk the policy-audit, code-review, and feature-audit template checklists (resolved per `.claude/skills/policy-audit-template-usage/SKILL.md`; `docs/features/templates/policy_audit/` is absent in TaskMaster, so an unresolvable template is recorded as a deviation and the skill's canonical section list is used instead) against the diff from MERGE-BASE plus untracked Write Set files; write `/evidence/qa-gates/self-review..md` with one PASS, FAIL, or N/A row per checklist item, including rows for the hard constraints of this plan. Acceptance: no FAIL row; a FAIL is fixed, the loop re-runs from P7-T1, and the self-review is rewritten. + +## Planner Internal Review Record + +PLANNER-INTERNAL-REVIEW: PASS +CITATION-TO-TREE: PASS +AC-TRACEABILITY: PASS +SCOPE-BOUNDARY: PASS +CITATION: ToDoModel/Data Model/ID/IDList.cs | GetNextToDoID +CITATION: ToDoModel/Data Model/ID/IDList.cs | MaxLengthOfID +CITATION: TaskMaster/AppGlobals/AppQuickFilerSettings.cs | HighConfidenceThreshold +CITATION: TaskMaster/AppGlobals/AppStagingFilenames.cs | InitProp +CITATION: TaskMaster/AppGlobals/AppStagingFilenames.cs | EmailInfoStagingFile +CITATION: TaskMaster/AppGlobals/AppAutoFileObjects.cs | LngConvCtPwr +CITATION: TaskMaster/AppGlobals/AppAutoFileObjects.cs | MaxRecents +CITATION: TaskMaster/AppGlobals/AppAutoFileObjects.cs | AppAutoFileObjects +CITATION: UtilitiesCS/Extensions/DfDeedle.QfcColumns.cs | AddQfcColumnsAsync +CITATION: UtilitiesCS.Test/Extensions/DfDeedle_COM_Tests.cs | AddQfcColumnsAsync_HappyPath_CompletesWithoutThrowing +CITATION: UtilitiesCS.Test/Extensions/DfDeedle_COM_Tests.cs | AddQfcColumnsAsync_PreCancelledToken_CompletesGracefully +CITATION: UtilitiesCS.Test/Extensions/DfDeedle_COM_Tests.cs | BuildTableMock +CITATION: ToDoModel.Test/Data Model/ID/IDListGetNextToDoIDTests.cs | CreateListWithCachedMaxLength +CITATION: TaskTree.Test/TaskTreeControllerMoveLogicTests.cs | MoveObjectsToChildren_DesyncedRoot_FiresMessageSeam +CITATION: TaskTree.Test/TaskTreeControllerMoveLogicTests.cs | MakeController +CITATION: TaskTree.Test/TaskTreeControllerRouteDropTests.cs | MakeController +CITATION: TaskMaster.Test/AppGlobals/AppQuickFilerSettingsTests.cs | HighConfidenceModeEnabled_WhenSetTrue_ReadsBackTrue +CITATION: TaskMaster.Test/AppGlobals/AppStagingFilenamesTests.cs | ConditionalReminders_Setter_RoundTripsThroughSettingsDefault +CITATION: TaskMaster.Test/AppGlobals/AppAutoFileObjectsCoverageExpansionTests.cs | ScalarSettingsProperties_WhenRoundTripped_UpdateDefaultsAndReturnValues +CITATION: TaskMaster.Test/AppGlobals/AppAutoFileObjectsCoverageExpansionTests.cs | CreateSutWithSpecialFolders +CITATION: TaskMaster.Test/Ribbon/RibbonControllerTests.cs | CreateController +CITATION: TaskMaster.Test/Ribbon/RibbonControllerTests.Engines.cs | Engines_WhenGlobalsAssigned_ReturnsGlobalsEngines +CITATION: TaskMaster/Ribbon/RibbonController.Intelligence.cs | SetHighConfidenceThresholdText +CITATION: scripts/vscode/Invoke-MSTestWithCoverage.ps1 | Invoke-MSTestWithCoverageMain +CITATION: scripts/vscode/TaskMaster.cli.runsettings | Parallelize +CITATION: scripts/vscode/Invoke-MSTestWithCoverage.Projection.ps1 | ConvertTo-JacocoPackageProjection +CITATION: docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/spec.md | Acceptance Criteria +AC-INVENTORY: AC1, AC2, AC3, AC4, AC5, AC6, AC7, AC8 +AC-MAPPING: AC1 | IMPLEMENTATION: P5-T1 | TESTS: AddQfcColumnsAsync_HappyPath_CompletesWithoutThrowing | EVIDENCE: dfdeedle-pass-after +AC-MAPPING: AC2 | IMPLEMENTATION: P2-T1, P2-T2, P3-T1, P3-T2 | TESTS: GetNextToDoID_FirstIdOnFreshList_AssignsLengthAndSavesOnce | EVIDENCE: idlist-probe-pass-after +AC-MAPPING: AC3 | IMPLEMENTATION: P1-T2, P3-T3 | TESTS: MoveObjectsToChildren_DesyncedRoot_FiresMessageSeam | EVIDENCE: idlist-probe-pass-after +AC-MAPPING: AC4 | IMPLEMENTATION: P0-T10, P4-T1, P4-T2, P4-T3, P4-T5, P4-T6, P4-T7, P4-T8, P4-T10, P5-T1, P6-T1 | TESTS: AC4 Search Definition Q1 to Q5 | EVIDENCE: ac4-search-after +AC-MAPPING: AC5 | IMPLEMENTATION: P1-T1, P1-T2, P1-T3, P1-T4, P1-T5, P1-T6, P1-T7 | TESTS: probe-set tests | EVIDENCE: settings-probe-fail-before +AC-MAPPING: AC6 | IMPLEMENTATION: P6-T2, P6-T4, P7-T4 | TESTS: full suite under TaskMaster.cli.runsettings | EVIDENCE: parallel-repeat-runs +AC-MAPPING: AC7 | IMPLEMENTATION: P7-T1, P7-T2, P7-T3, P7-T4, P7-T5, P7-T6 | TESTS: ApplicationSettingsStoreTests | EVIDENCE: coverage-comparison +AC-MAPPING: AC8 | IMPLEMENTATION: P4-T5, P4-T9, P4-T10 | TESTS: AppQuickFilerSettingsTests | EVIDENCE: ribbon-static-state-check +UNRESOLVED-GAPS: NONE diff --git a/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/policy-audit.2026-10-22T17-10.md b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/policy-audit.2026-10-22T17-10.md new file mode 100644 index 000000000..aebcfaf4b --- /dev/null +++ b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/policy-audit.2026-10-22T17-10.md @@ -0,0 +1,224 @@ +# Policy Audit: Parallel test failures from wall-clock deadlines and shared user.config (Issue #1005) + +- Branch: `bug/parallel-test-failures-wall-clock-and-shared-user-config-1005` +- Head: `e399de597` +- Base: `main` (resolved `origin/main`) at merge base `346651242a2bd6ed25d703a4b1ecb5a055c6ae72` +- Review timestamp: 2026-10-22T17-10 (first review of this branch) +- Work mode: `full-bug` (AC source: `spec.md` only) +- Reviewer: feature-review agent + +## Template Provenance + +The structure of this artifact follows the canonical major headings listed in `.claude/skills/policy-audit-template-usage/SKILL.md` (Executive Summary, sections 1 to 10, Appendix A and Appendix B), in the same shape as the most recent validated audit in this repository (`docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/policy-audit.2026-10-22T14-10.md`). The template-asset selector step was not run from this agent session. The artifact is validated with `validate_orchestration_artifacts` (artifact type `policy-audit`). + +## Rejected Scope Narrowing + +No scope narrowing was detected. The caller prompt states: "Scope decision by the orchestrator: tests that only mutate Settings.Default in memory without saving, and the per-method [DoNotParallelize] in ApplicationGlobalsStartupTimingTests, are out of scope and tracked as issue #1006; do not raise them as blocking findings for this item." This restates the non-goals of `spec.md` section 4 and directs finding disposition; it does not remove any file, language, toolchain step or coverage check from the audit. The audit scope is the full branch diff `346651242...e399de597` (73 files), and the reviewer still verified that the named out-of-scope tests do not reach a settings `Save()` (section 8). + +The Bash tool was limited by the caller to `git diff` and `git log` command forms. This is an execution constraint, not a scope narrowing: every figure below was read from committed evidence, the canonical coverage artifact, the retained baseline Cobertura document, or the source tree. + +## Executive Summary + +The branch fixes two parallel-execution test failures. The DfDeedle happy-path and pre-cancelled tests now pass a never-advanced `FakeTimeProvider` through the existing `timeProvider` parameter and are awaited `async Task` tests (test-only change). A new per-instance `ISettingsStore` seam (`UtilitiesCS`) with a production adapter `ApplicationSettingsStore` routes every test-reached settings save in `IDList`, `AppQuickFilerSettings`, `AppStagingFilenames` and the `AppAutoFileObjects` scalar properties through an injected store; production defaults wrap the assembly's own `Properties.Settings.Default`, so production writes and saves are unchanged. Tests inject `Mock` or an `InMemorySettingsStore`, and the class-level `[DoNotParallelize]` is removed from `AppQuickFilerSettingsTests` and `RibbonControllerTests`. + +Overall verdict: **REMEDIATION_REQUIRED (1 blocking finding)**. + +- Toolchain: PASS (format, format check, analyzer rebuild, warnings-as-errors rebuild, MSTest with coverage; 7618 of 7618 tests passed under the class-level parallel runsettings). +- Parallel-isolation rules (no serialization, no sleeps, no temp files, no `user.config` redirection, no real save): PASS. +- Repository C# coverage: PASS (86.37% line, 80.66% branch; floors 80% and 75% from root `CLAUDE.md`). +- New and changed C# code: PASS (new files 100%; every instrumented changed line covered). +- Modified-file C# coverage: FAIL. `ToDoModel/Data Model/ID/IDList.cs` lost coverage of the unchanged `MaxLengthOfID` getter (7 of 7 lines and 4 of 4 branches covered at the merge base, 0 at head) because the removed test helper was its only caller; the file's branch rate fell from 20.69% to 19.35%. The committed coverage comparison does not report this. Finding B-1 (autonomous). + +## 1. General Unit Test Policy Compliance + +### 1.1 Core principles + +| Principle | Verdict | Evidence | +|---|---|---| +| Independence | PASS | Every converted test builds its own store (`InMemorySettingsStore` or `Mock`); the `Settings.Default` snapshot/restore blocks and the `TestCleanup` `defaults.Save()` are deleted. | +| Isolation | PASS | New tests each assert one behaviour (null guard, forwarding, save count, persistence branch). | +| Fast execution | PASS | `AddQfcColumnsAsync_HappyPath_CompletesWithoutThrowing` 2.1 to 2.7 ms across six runs (`parallel-repeat-runs.2026-10-22T16-30.md`), down from a 9 s wall-clock budget. | +| Determinism | PASS | The DfDeedle tests pass a `FakeTimeProvider` that is never advanced, so no deadline can fire. 0 added `Thread.Sleep`, `Task.Delay`, `[Timeout]` or retry tokens (reviewer `git diff -G` over `*.cs`, `*.runsettings`, `*.config`, `*.csproj`: the only matching files are the two that remove `[DoNotParallelize]`). | +| Readability | PASS | Descriptive names, Arrange/Act/Assert comments, FluentAssertions `because` text on probe assertions. | +| No temporary files | PASS | 0 `GetTempPath`, `GetTempFileName` or `Path.GetTemp` tokens in the diff. `ApplicationSettingsStoreTests` uses a dictionary-backed `ApplicationSettingsBase` subclass whose indexer and `Save` overrides never reach the provider. | +| No shared settings file write | PASS | The thread-attributed `SettingsSaving` probe cancels any save it observes, so the fail-before run wrote nothing; after the fix it records 0 saves on four representative tests (`settings-probe-fail-before.2026-10-22T16-15.md`, `idlist-probe-pass-after.2026-10-22T16-18.md`, `taskmaster-settings-probe-pass-after.2026-10-22T16-25.md`). | + +### 1.2 Coverage requirements + +Coverage thresholds applied: the root `CLAUDE.md` states C# line `>= 80%` and branch `>= 75%`, new modules, classes and methods `>= 90%`, and "Code changes or refactors must not reduce coverage for the lines that were changed"; these govern under the threshold precedence of `.claude/rules/general-unit-test.md`. The canonical artifact `artifacts/csharp/coverage.xml` is the post-processed Cobertura of the final pass; its root attributes `lines-covered="57627" lines-valid="66720" branches-covered="14060" branches-valid="17431"` equal the figures in `evidence/qa-gates/final-toolchain-tests.2026-10-22T16-35.md`. The merge-base baseline Cobertura document named in `evidence/baseline/baseline-coverage.2026-10-22T16-10.md` is retained in the session scratch folder and was read by the reviewer for the per-member comparison in section 5. `quality-tiers.yml` is absent at the repository root, so tier classification is not a finding. + +**Coverage Metrics by Language:** + +| Language | Files Changed | Tests | Test Result | Baseline Coverage | Post-Change Coverage | New Code Coverage | +|---|---|---|---|---|---|---| +| C# | 7 production (3 new, 4 modified), 13 test (4 new, 9 modified), 4 project files | MSTest | 7618 passed, 0 failed | 86.30% | 86.37% | 100% | +| TypeScript | 0 | none | none | N/A | N/A | N/A | +| Python | 0 | none | none | N/A | N/A | N/A | +| PowerShell | 0 | none | none | N/A | N/A | N/A | + +### Coverage Evidence Checklist + +- C# baseline coverage artifact: `evidence/baseline/baseline-coverage.2026-10-22T16-10.md` with projection `evidence/baseline/baseline-coverage-projection.2026-10-22T16-10.jacoco.xml` +- C# post-change coverage artifact: `artifacts/csharp/coverage.xml` with summaries `evidence/qa-gates/final-toolchain-tests.2026-10-22T16-35.md` and `evidence/qa-gates/coverage-comparison.2026-10-22T16-36.md` +- TypeScript baseline coverage artifact: `N/A - out of scope` +- TypeScript post-change coverage artifact: `N/A - out of scope` +- PowerShell baseline coverage artifact: `N/A - out of scope` +- PowerShell post-change coverage artifact: `N/A - out of scope` +- Python baseline coverage artifact: `N/A - out of scope` +- Python post-change coverage artifact: `N/A - out of scope` +- Per-language comparison summary: section 1.2.1 of this document + +### 1.2.1 Per-Language Coverage Comparison + +- C#: Baseline: 86.30% lines (57542/66677) / 80.58% branches (14036/17419) -> Post-change: 86.37% lines (57627/66720) / 80.66% branches (14060/17431). Change: +0.07% lines, +0.08% branches. New/changed-code coverage: 100%. Disposition: FAIL. Evidence: artifacts/csharp/coverage.xml; evidence/qa-gates/coverage-comparison.2026-10-22T16-36.md; reviewer per-member read of both Cobertura documents (section 5, finding B-1). +- TypeScript: Baseline: N/A. Post-change: N/A. Change: N/A. Disposition: N/A. Evidence: N/A - zero TypeScript files changed on this branch. +- Python: Baseline: N/A. Post-change: N/A. Change: N/A. Disposition: N/A. Evidence: N/A - zero Python files changed on this branch. +- PowerShell: Baseline: N/A. Post-change: N/A. Change: N/A. Disposition: N/A. Evidence: N/A - zero PowerShell files changed on this branch. + +### 1.2.2 Coverage Verdicts + +C# repo-wide coverage verdict: PASS (86.37% line at or above 80, 80.66% branch at or above 75; both rose against the merge-base baseline). + +C# new-code coverage verdict: PASS (`ApplicationSettingsStore.cs` class `line-rate="1" branch-rate="1"`; `AppAutoFileObjects.ScalarSettings.cs` class `line-rate="1" branch-rate="1"`; `ISettingsStore.cs` is interface-only with no class node). + +C# changed-line coverage verdict: PASS (every instrumented added or modified line is covered; reviewer confirmed `hits="1"` on the new `IDList` constructor lines 73 to 77 and 86 to 90, the field-initializer lines 97 to 99, and every `GetNextToDoID` line; class rates 1.0 for `AppQuickFilerSettings` and `AppStagingFilenames`). + +C# modified-file coverage verdict: FAIL. `IDList.cs` 24.36% / 20.69% at baseline to 26.04% / 19.35% at head: below the 80% / 75% per-file floor at both points (pre-existing), and its branch rate regressed because the unchanged `MaxLengthOfID` getter lost all coverage (B-1, blocking). `AppAutoFileObjects.cs` 45.66% / 37.10% to 40.81% / 37.10%: below floor at both points; the line drop is entirely the move of 33 covered scalar-property lines into the new partial `AppAutoFileObjects.ScalarSettings.cs` (100%), and the class across both files rises to 46.06% (187/406); dispositioned non-blocking (N-1). `AppQuickFilerSettings.cs` 100% / 100% both points: PASS. `AppStagingFilenames.cs` 30.77% / 30.00% to 100% / 100%: PASS. + +### 1.3 Scenario completeness + +PASS with one gap. Positive (round-trip, forwarding), negative (null store, null settings), edge (shorter ID does not save, longer ID saves again, `EmailInfoStagingFile` does not save, cached field short-circuits the getter) and state-transition (save count across successive IDs) scenarios are present. The gap is B-1: the `IDList.MaxLengthOfID` lazy-cache getter, a public `IIDList` member, is no longer exercised by any test. + +## 2. General Code Change Policy Compliance + +| Rule | Verdict | Evidence | +|---|---|---| +| Design: simplicity and separation of concerns | PASS | A two-member interface and a four-line adapter; each consumer gains one field and one constructor overload. Persistence is isolated behind the seam. | +| No static mutable seam | PASS | The store is a per-instance `readonly` field in all four consumers; no static setter or ambient provider is introduced. | +| Production behaviour preserved | PASS | Generated settings properties are `this[name]` reads and writes, so `(bool)store[name]`, `(double)store[name]`, `(int)store[name]` and `store[name] = value` followed by `Save()` are equivalent. `ApplicationGlobals` still calls the public parameterless constructors; `AppAutoFileObjects(IApplicationGlobals)` chains to the store overload with the default adapter. | +| Error handling | PASS | Null stores and null settings are rejected with `ArgumentNullException` at construction. | +| File size (500 lines) | PASS | `evidence/qa-gates/file-sizes.2026-10-22T16-37.md`: every created or modified `.cs` file at most 477 lines except the two pre-existing oversize files, which shrank (`DfDeedle_COM_Tests.cs` 848 to 809, `AppAutoFileObjects.cs` 853 to 791). Reviewer cross-check: `IDList.cs` reads 352 lines. | +| Public contracts documented | PASS | XML documentation on `ISettingsStore`, `ApplicationSettingsStore`, the new `IDList` constructors and the new consumer constructors. | +| Minimal public surface | PASS | `ISettingsStore`, `ApplicationSettingsStore` and the new `IDList` constructors are `public` because `UtilitiesCS` and `ToDoModel` grant no `InternalsVisibleTo` to the consuming test assemblies (spec section 9); the TaskMaster overloads are `internal`. | +| Dependencies | PASS | No package added; `Microsoft.Extensions.TimeProvider.Testing` was already referenced by `UtilitiesCS.Test`. | +| Bugfix workflow (failing test first) | PASS | Four probe tests fail before the production change with recorded save counts 1, 1, 1 and 6, then pass after; the DfDeedle path has a fail-before-exception dossier (`fail-before-exception.2026-10-22T16-15.md`) quoting the maintainer's captured exception and citing the before-fix search. | +| Requirements traceability | PASS | Every AC4 site named in `spec.md` maps to a `_settingsStore.Save()` call (`ac4-search-after.2026-10-22T16-27.md` Q2: 1, 6, 9, 6). | +| Host-identifier hygiene in committed files | PASS | Reviewer search of the feature folder and the four added agent-memory notes for drive-rooted paths, `\Users\` and the account name: 0 matches. | + +## 3. Language-Specific Code Change Policy Compliance + +### 3.1 C# (`CLAUDE.md` C# Code Change Policy, `.claude/rules/csharp.md`) + +| Rule | Verdict | Evidence | +|---|---|---| +| CSharpier formatting | PASS | `evidence/qa-gates/final-toolchain-format.2026-10-22T16-32.md`: clean pass 2, 0 rewritten files, `csharpier check .` exit 0 over 1682 files. | +| Analyzer rebuild | PASS | `final-toolchain-analyzers.2026-10-22T16-32.md`: `/t:Rebuild`, 0 warnings, 0 errors. | +| Nullable rebuild (warnings as errors) | PASS | `final-toolchain-typecheck.2026-10-22T16-33.md`: `/t:Rebuild`, 0 warnings, 0 errors, no `/p:Nullable=enable`; the new `UtilitiesCS` files carry `#nullable enable`. | +| Legacy `` registration | PASS | Diff adds entries for `ApplicationSettingsStore.cs` and `ISettingsStore.cs` (`UtilitiesCS.csproj`), `AppAutoFileObjects.ScalarSettings.cs` (`TaskMaster.csproj`), `DfDeedle_COM_Tests.QfcColumnsAsync.cs` and `ApplicationSettingsStoreTests.cs` (`UtilitiesCS.Test.csproj`), `SettingsSaveProbe.cs` and `InMemorySettingsStore.cs` (`TaskMaster.Test.csproj`). | +| C# 7.3 in `ToDoModel.Test` and `TaskTree.Test` | PASS | Added lines in those projects use no target-typed `new()`, `is not`, switch expressions, `using` declarations or nullable annotations; both compile in the final rebuilds. | +| No production change to DfDeedle | PASS | `git diff --stat` shows no file under `UtilitiesCS/Extensions/`. | +| Coverage exclusions | PASS | 0 added `ExcludeFromCodeCoverage` lines (reviewer `git diff -G` scan); `coverage.config` not in the diff. | +| Suppressions | PASS | No `#pragma` or `SuppressMessage` added. | + +## 4. Language-Specific Unit Test Policy Compliance + +| Rule | Verdict | Evidence | +|---|---|---| +| MSTest attributes | PASS | `[TestClass]` and `[TestMethod]` throughout; the DfDeedle partial adds `[TestMethod]` members to the existing `[TestClass]`. | +| Moq | PASS | `Mock` in `ToDoModel.Test` and `TaskTree.Test`; `VerifySet` and `Verify` assert assignment and save counts. | +| FluentAssertions | PASS | All new assertions use FluentAssertions, including `NotThrowAsync` for the async DfDeedle tests. | +| No serialization as a fix | PASS | No `[DoNotParallelize]` added; two class-level attributes removed. No runsettings file in the diff. | +| `[DoNotParallelize]` removal justified (AC8) | PASS | `evidence/other/ribbon-static-state-check.2026-10-22T16-23.md` records no other mutable process-global state; reviewer search of `TaskMaster/Ribbon` for `static` finds only `static readonly` loggers, immutable catalog maps and pure helpers, and `RibbonController()` is an empty constructor. | +| Test location | PASS | Tests sit in the sibling `.Test` projects mirroring production folders (`UtilitiesCS.Test/HelperClasses/Settings/` for `UtilitiesCS/HelperClasses/Settings/`). | + +## 5. Test Coverage Detail + +Per-file figures are the Cobertura `` `line-rate` and `branch-rate` attributes for each `filename` in `artifacts/csharp/coverage.xml` (head) and in the retained merge-base Cobertura document; member figures are the `` line nodes. + +| File | Status | Baseline line / branch | Post-change line / branch | Gate | +|---|---|---|---|---| +| `UtilitiesCS/HelperClasses/Settings/ApplicationSettingsStore.cs` | new | absent | 100% / 100% | new code at least 90%: PASS | +| `UtilitiesCS/HelperClasses/Settings/ISettingsStore.cs` | new | absent | no executable lines | interface only | +| `TaskMaster/AppGlobals/AppAutoFileObjects.ScalarSettings.cs` | new | absent | 100% / 100% | new code at least 90%: PASS | +| `ToDoModel/Data Model/ID/IDList.cs` | modified | 24.36% (38/156) / 20.69% | 26.04% (44/169) / 19.35% | FAIL (B-1, blocking) | +| `TaskMaster/AppGlobals/AppQuickFilerSettings.cs` | modified | 100% / 100% | 100% / 100% | PASS | +| `TaskMaster/AppGlobals/AppStagingFilenames.cs` | modified | 30.77% / 30.00% | 100% / 100% | PASS | +| `TaskMaster/AppGlobals/AppAutoFileObjects.cs` | modified | 45.66% (184/403) / 37.10% | 40.81% (151/370) / 37.10% | FAIL, dispositioned non-blocking (N-1, move only) | + +B-1 detail. In the baseline document, method `get_MaxLengthOfID` of class `ToDoModel.IDList` has seven line nodes (73 to 79), all `hits="1"`, and two branch lines each `condition-coverage="100% (2/2)"`. In the head document the same method (lines 107 to 113) has all seven nodes `hits="0"` and both branch lines `0% (0/2)`. The only test caller was `IDListGetNextToDoIDTests.CreateListWithCachedMaxLength` (`_ = list.MaxLengthOfID;`), deleted as AC2 requires; no other test in the repository references `MaxLengthOfID`. Arithmetic cross-check: the diff adds 13 covered lines to `IDList.cs` (two constructors of five lines and three field-initializer lines), so the covered count would be 51 if nothing were lost; the head figure is 44, a loss of exactly the getter's seven lines. `evidence/qa-gates/coverage-comparison.2026-10-22T16-36.md` reports only line rates for this file and states that the uncovered residue is the unchanged Outlook-bound and deserializing constructors, which omits this loss. The CLAUDE.md changed-line rule is met literally (the getter's lines are unchanged), but the modified file is below the floor and regressed, so the non-blocking disposition used for pre-existing sub-floor files does not apply. + +## 6. Test Execution Metrics + +- MSTest full suite with coverage (final pass): 7618 total, 7618 passed, 0 failed, 0 error, 0 timeout (`final-toolchain-tests.2026-10-22T16-35.md`, derived from the trx). Baseline: 7599 passed; the delta of 19 equals the 19 register tests listed in that artifact. +- Parallel configuration: `scripts/vscode/TaskMaster.cli.runsettings` (MSTest `Workers` 0, `Scope` ClassLevel), `/InIsolation`. +- Repeated parallel runs: five combined runs of the ten affected classes (116 of 116 passed each) and ten solo runs in reverse order (all passed) (`parallel-repeat-runs.2026-10-22T16-30.md`). +- Fail-before and pass-after: `settings-probe-fail-before.2026-10-22T16-15.md` (4 of 4 failed on the probe assertion), `idlist-probe-pass-after.2026-10-22T16-18.md`, `taskmaster-settings-probe-pass-after.2026-10-22T16-25.md`, `dfdeedle-pass-after.2026-10-22T16-26.md`, `settings-store-adapter-tests.2026-10-22T16-17.md`. +- The reviewer did not re-run the suites (Bash limited to `git diff` and `git log`); coverage figures were re-read from the Cobertura documents. + +## 7. Code Quality Checks + +| Check | Command | Result | Verdict | +|---|---|---|---| +| Confidentiality masking scan | Grep of the feature folder and the four added agent-memory notes for drive-rooted paths, `\Users\` and the account name | 0 matches | PASS | +| Suppression scan (added lines) | `git diff 346651242..HEAD -G "ExcludeFromCodeCoverage" --stat -- *.cs` | no file | PASS | +| Serialization scan | `git diff 346651242..HEAD -G --stat`, pattern alternating `DoNotParallelize`, `Thread\.Sleep`, `Task\.Delay`, `\[Timeout`, `Retry`, `Workers` and `GetTemp`, over code, runsettings, config and project files | 2 files, both removing `[DoNotParallelize]` | PASS | +| Workflow change scan | `git diff --stat` filtered for `.github/` | empty | PASS | +| Raw evidence scan | `git diff --stat` filtered for `.trx`, `.coverage`, Cobertura and `artifacts/` | empty; only two `.jacoco.xml` projections under `evidence/` | PASS | +| Scope fence scan | `git diff --stat` for `IIDList`, `TreeOfToDoItems`, `TaskTreeController`, `AppToDoObjects`, `ApplicationGlobals`, runsettings | empty | PASS | + +## Evidence Location Compliance + +`git diff --stat 346651242..HEAD` lists no file under `artifacts/baselines/`, `artifacts/qa/`, `artifacts/evidence/` or `artifacts/coverage/`. All committed evidence is under `/evidence/{baseline,regression-testing,qa-gates,other}/` as Markdown summaries and two package-level JaCoCo projections, which `CLAUDE.md` "Committed Test Evidence Format" permits. `validate_evidence_locations.py --root .` could not be run because the caller restricted the Bash tool to `git diff` and `git log`; the check was performed by diff inspection. Verdict: PASS. + +## 8. Gaps and Exceptions + +Blocking: + +- B-1 (autonomous): `IDList.MaxLengthOfID` lost all test coverage (7 lines, 4 branches) when the cache-priming helper was removed; `IDList.cs` branch rate regressed from 20.69% to 19.35%, and the committed coverage comparison does not disclose it. Remedy: add a `ToDoModel.Test` test that reads `MaxLengthOfID` on a non-empty store-injected list (cold cache computes the maximum length; warm cache returns the cached value after `GetNextToDoID`), and correct `coverage-comparison` to report the per-file branch figures. + +Non-blocking: + +- N-1: `AppAutoFileObjects.cs` per-file line rate fell from 45.66% to 40.81% only because 33 covered lines moved into the new partial; no remaining line lost coverage and the class across both files is 46.06%. The file is below the 80% floor at both points (pre-existing, Outlook-bound members). +- O-1: `artifacts/pr_context.summary.txt` and `.appendix.txt` describe the #1002 branch (head `e94c26130`), not this branch; the reviewer could not regenerate them under the Bash restriction, so scope was derived from `git diff 346651242..HEAD`. +- O-2: The TaskMaster CI checks (actionlint, build-analyzers, build-nullable, format-check, hygiene, mstest-coverage, pester) have not run because no pull request exists; they are a PR-time gate, not an acceptance criterion. +- O-3: Out-of-scope tests named in the caller prompt (issue #1006) were spot-checked: `AppOlObjectsTests.LoadJunkCertain_KeepsStoredValue_WhenReplacementSelectionIsCancelled` mutates `OlJunkCertain` in memory and returns before `LoadJunkCertain` reaches `Save()` (`AppOlObjects.JunkFolders.cs` lines 143 to 149); no test calls a `DarkMode` setter on `AppOlObjects`. Not raised as findings. + +## 9. Summary of Changes + +- Commit `39f38024c`: promotion of the bug to the active feature folder. +- Commit `0a6f080ef`: research, spec and preflight-cleared plan. +- Commit `e399de597`: `ISettingsStore` and `ApplicationSettingsStore`; store seam in `IDList`, `AppQuickFilerSettings`, `AppStagingFilenames` and the new `AppAutoFileObjects.ScalarSettings.cs` partial; DfDeedle tests moved to a partial and given a `FakeTimeProvider`; converted settings tests, `InMemorySettingsStore`, `SettingsSaveProbe`, TaskTree probe; removal of two class-level `[DoNotParallelize]`; evidence under `evidence/`; follow-up promotion record for issue #1006; agent-memory notes. + +## 10. Compliance Verdict + +**REMEDIATION_REQUIRED.** Toolchain, parallel-isolation rules, repository coverage, new-code coverage, changed-line coverage, file sizes, scope fences and evidence form pass. One blocking finding (B-1, autonomous) remains: a coverage regression in the modified file `IDList.cs` that the committed coverage comparison does not report. + +## Appendix A: Test Inventory + +| Test file | Tests | Status | Scope | +|---|---|---|---| +| `UtilitiesCS.Test/HelperClasses/Settings/ApplicationSettingsStoreTests.cs` | 4 | new | adapter null guard and forwarding | +| `UtilitiesCS.Test/Extensions/DfDeedle_COM_Tests.QfcColumnsAsync.cs` | 2 | new (moved and converted) | `AddQfcColumnsAsync` with a fake clock | +| `UtilitiesCS.Test/Extensions/DfDeedle_COM_Tests.cs` | 24 | modified (`partial`, two tests moved out) | unchanged tests | +| `ToDoModel.Test/Data Model/ID/IDListGetNextToDoIDTests.cs` | 13 | modified (7 added) | ID arithmetic and persistence branch | +| `TaskTree.Test/TaskTreeControllerMoveLogicTests.cs` | 17 | modified | move logic with injected store and probe | +| `TaskTree.Test/TaskTreeControllerRouteDropTests.cs` | 7 | modified | drop routing with injected store | +| `TaskMaster.Test/AppGlobals/AppQuickFilerSettingsTests.cs` | 6 | modified (2 added, attribute removed) | high-confidence settings | +| `TaskMaster.Test/AppGlobals/AppQuickFilerSettingsRemainingPropertiesTests.cs` | 6 | modified | remaining boolean settings | +| `TaskMaster.Test/AppGlobals/AppStagingFilenamesTests.cs` | 10 | modified (4 added) | staging file names | +| `TaskMaster.Test/AppGlobals/AppAutoFileObjectsCoverageExpansionTests.cs` | 10 | modified (2 added) | scalar settings and initialization paths | +| `TaskMaster.Test/Ribbon/RibbonControllerTests.cs` | 17 (with `Engines` partial) | modified (attribute removed) | ribbon high-confidence helpers | +| `TaskMaster.Test/AppGlobals/InMemorySettingsStore.cs` | 0 | new helper | per-test fake store | +| `TaskMaster.Test/AppGlobals/SettingsSaveProbe.cs` | 0 | new helper | thread-attributed save probe | + +## Appendix B: Toolchain Commands Reference + +| Stage | Command | Run by | +|---|---|---| +| C# format | `dotnet tool run csharpier format .` then `dotnet tool run csharpier check .` | executor | +| C# analyzers | `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true` | executor | +| C# type check | `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true` | executor | +| C# test and coverage | `pwsh -NoProfile -File scripts/vscode/Invoke-MSTestWithCoverage.ps1` | executor | +| Parallel repeat runs | `vstest.console.exe` over the four test assemblies with `/Settings:scripts/vscode/TaskMaster.cli.runsettings /InIsolation` | executor | +| Diff scope | `git diff 346651242..HEAD --stat`; `git diff 346651242..HEAD -- `; `git diff -G --stat` | reviewer | +| Coverage reading | Grep of `` and `` in `artifacts/csharp/coverage.xml` and the retained baseline Cobertura document | reviewer | diff --git a/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/policy-audit.2026-10-22T17-23.md b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/policy-audit.2026-10-22T17-23.md new file mode 100644 index 000000000..f7f20f21c --- /dev/null +++ b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/policy-audit.2026-10-22T17-23.md @@ -0,0 +1,232 @@ +# Policy Audit: Parallel test failures from wall-clock deadlines and shared user.config (Issue #1005) + +- Branch: `bug/parallel-test-failures-wall-clock-and-shared-user-config-1005` +- Head: `3df0782ab` +- Base: `main` (resolved `origin/main`) at merge base `346651242a2bd6ed25d703a4b1ecb5a055c6ae72` +- Review timestamp: 2026-10-22T17-23 (re-audit R4 after remediation cycle 1; prior review 2026-10-22T17-10 at head `e399de597`) +- Timestamp derivation: `git log -1 --format="%cI | %cr"` on `3df0782ab` reported a commit time of 17:21:21 UTC and "2 minutes ago"; the stamp is the observed current UTC minute. +- Work mode: `full-bug` (AC source: `spec.md` only) +- Reviewer: feature-review agent + +## Template Provenance + +The structure of this artifact follows the canonical major headings listed in `.claude/skills/policy-audit-template-usage/SKILL.md` (Executive Summary, sections 1 to 10, Appendix A and Appendix B), in the same shape as the prior validated audit for this branch (`policy-audit.2026-10-22T17-10.md`). The template-asset selector step was not run from this agent session. The artifact is validated with `validate_orchestration_artifacts` (artifact type `policy-audit`). + +## Rejected Scope Narrowing + +No scope narrowing was detected. The caller prompt states: "Same inputs and full scope as the original review; no scope narrowing." It also records an orchestrator decision that in-memory-only `Settings.Default` mutation and the per-method `[DoNotParallelize]` in `ApplicationGlobalsStartupTimingTests` are out of scope (issue #1006). That restates the non-goals of `spec.md` section 4 and directs finding disposition; it removes no file, language, toolchain step or coverage check. The audit scope is the full branch diff `346651242..3df0782ab` (91 files). + +The Bash tool was limited by the caller to `git diff` and `git log` command forms. This is an execution constraint, not a scope narrowing: every figure below was read from committed evidence, the canonical coverage artifact, or the source tree. + +## Executive Summary + +The branch fixes two parallel-execution test failures. The DfDeedle happy-path and pre-cancelled tests pass a never-advanced `FakeTimeProvider` through the existing `timeProvider` parameter and are awaited `async Task` tests (test-only). A per-instance `ISettingsStore` seam (`UtilitiesCS`) with a production adapter `ApplicationSettingsStore` routes every test-reached settings save in `IDList`, `AppQuickFilerSettings`, `AppStagingFilenames` and the `AppAutoFileObjects` scalar properties through an injected store; production defaults wrap each assembly's own `Properties.Settings.Default`. Tests inject `Mock` or an `InMemorySettingsStore`, and the class-level `[DoNotParallelize]` is removed from `AppQuickFilerSettingsTests` and `RibbonControllerTests`. + +Remediation cycle 1 (commit `3df0782ab`) is test-only: three `MaxLengthOfID` tests in `ToDoModel.Test/Data Model/ID/IDListGetNextToDoIDTests.cs`, a new coverage comparison, toolchain evidence, and the remediation plan. No production file changed in that commit (`git diff e399de597..3df0782ab --stat`). + +Overall verdict: **PASS (0 blocking findings)**. + +- Prior finding B-1: RESOLVED. `get_MaxLengthOfID` is `line-rate="1" branch-rate="1"` in `artifacts/csharp/coverage.xml` (lines 107 to 113 all `hits="1"`, both branch lines `100% (2/2)`); `IDList.cs` is 30.18% line / 25.81% branch, above the merge-base 24.36% / 20.69%. +- Toolchain: PASS (format and check, analyzer rebuild, warnings-as-errors rebuild, MSTest with coverage; 7621 of 7621 tests passed under the class-level parallel runsettings). +- Parallel-isolation rules (no serialization, no sleeps, no temp files, no `user.config` redirection, no real save): PASS. +- C# repository coverage: PASS (86.38% line, 80.68% branch; floors 80% and 75% from root `CLAUDE.md`). +- C# new and changed code: PASS (new files 100%; every instrumented changed line covered). + +## 1. General Unit Test Policy Compliance + +### 1.1 Core principles + +| Principle | Verdict | Evidence | +|---|---|---| +| Independence | PASS | Every converted and added test builds its own store (`InMemorySettingsStore` or `Mock`); the three new `MaxLengthOfID` tests each construct their own lists and mocks. | +| Isolation | PASS | Each new test asserts one behaviour (cold cache, second cold cache, warm cache without save). | +| Fast execution | PASS | `parallel-repeat-runs.2026-10-22T16-30.md`: DfDeedle happy path 2.1 to 2.7 ms, down from a 9 s wall-clock budget. | +| Determinism | PASS | Never-advanced `FakeTimeProvider`; `git diff -G` reviewer scan from the prior review found no added `Thread.Sleep`, `Task.Delay`, `[Timeout]` or retry token, and the cycle-1 diff adds none (read in full). | +| Readability | PASS | Descriptive names, Arrange/Act/Assert comments, FluentAssertions `because` text. | +| No temporary files | PASS | 0 temp-path tokens in the branch diff; cycle-1 tests use in-memory lists and Moq only. | +| No shared settings file write | PASS | Probe evidence records 0 saves after the fix (`idlist-probe-pass-after.2026-10-22T16-18.md`, `taskmaster-settings-probe-pass-after.2026-10-22T16-25.md`); every new `IDList` in cycle 1 is store-injected. | + +### 1.2 Coverage requirements + +Coverage thresholds applied: the root `CLAUDE.md` states C# line `>= 80%` and branch `>= 75%`, new modules, classes and methods `>= 90%`, and that changed lines must not lose coverage; these govern under the threshold precedence of `.claude/rules/general-unit-test.md`. The canonical artifact `artifacts/csharp/coverage.xml` is the post-processed Cobertura from the cycle-1 final pass; its root attributes `lines-covered="57634" lines-valid="66720" branches-covered="14064" branches-valid="17431"` equal the figures in `evidence/qa-gates/remediation-tests.2026-10-22T17-12.md`. `quality-tiers.yml` is absent at the repository root, so tier classification is not a finding. + +**Coverage Metrics by Language:** + +| Language | Files Changed | Tests | Test Result | Baseline Coverage | Post-Change Coverage | New Code Coverage | +|---|---|---|---|---|---|---| +| C# | 7 production (3 new, 4 modified), 13 test (4 new, 9 modified), 4 project files | MSTest | 7621 passed, 0 failed | 86.30% | 86.38% | 100% | +| TypeScript | 0 | none | none | N/A | N/A | N/A | +| Python | 0 | none | none | N/A | N/A | N/A | +| PowerShell | 0 | none | none | N/A | N/A | N/A | + +### Coverage Evidence Checklist + +- C# baseline coverage artifact: `evidence/baseline/baseline-coverage.2026-10-22T16-10.md` with projection `evidence/baseline/baseline-coverage-projection.2026-10-22T16-10.jacoco.xml` +- C# post-change coverage artifact: `artifacts/csharp/coverage.xml` with summaries `evidence/qa-gates/remediation-tests.2026-10-22T17-12.md` and `evidence/qa-gates/coverage-comparison.2026-10-22T17-12.md` +- TypeScript baseline coverage artifact: `N/A - out of scope` +- TypeScript post-change coverage artifact: `N/A - out of scope` +- PowerShell baseline coverage artifact: `N/A - out of scope` +- PowerShell post-change coverage artifact: `N/A - out of scope` +- Python baseline coverage artifact: `N/A - out of scope` +- Python post-change coverage artifact: `N/A - out of scope` +- Per-language comparison summary: section 1.2.1 of this document + +### 1.2.1 Per-Language Coverage Comparison + +- C#: Baseline: 86.30% lines (57542/66677) / 80.58% branches (14036/17419) -> Post-change: 86.38% lines (57634/66720) / 80.68% branches (14064/17431). Change: +0.08% lines, +0.10% branches. New/changed-code coverage: 100%. Disposition: PASS. Evidence: artifacts/csharp/coverage.xml; evidence/qa-gates/coverage-comparison.2026-10-22T17-12.md; reviewer read of the `ToDoModel.IDList` class and `get_MaxLengthOfID` method nodes. +- TypeScript: Baseline: N/A. Post-change: N/A. Change: N/A. Disposition: N/A. Evidence: N/A - zero TypeScript files changed on this branch. +- Python: Baseline: N/A. Post-change: N/A. Change: N/A. Disposition: N/A. Evidence: N/A - zero Python files changed on this branch. +- PowerShell: Baseline: N/A. Post-change: N/A. Change: N/A. Disposition: N/A. Evidence: N/A - zero PowerShell files changed on this branch. + +### 1.2.2 Coverage Verdicts + +C# repo-wide coverage verdict: PASS (86.38% line at or above 80, 80.68% branch at or above 75; both rose against the merge-base baseline). + +C# new-code coverage verdict: PASS (`ApplicationSettingsStore.cs` class `line-rate="1" branch-rate="1"`; `AppAutoFileObjects.ScalarSettings.cs` class `line-rate="1" branch-rate="1"`; `ISettingsStore.cs` is interface-only with no class node). + +C# changed-line coverage verdict: PASS (every instrumented added or modified line is covered; the remediation changed no production line, and the prior review's line-level read of `IDList`, `AppQuickFilerSettings` and `AppStagingFilenames` still applies). + +C# modified-file coverage verdict: PASS for the B-1 regression check (no modified file lost coverage against the merge base on any line). Per-file floor rows in section 5: `IDList.cs` 30.18% / 25.81% and `AppAutoFileObjects.cs` 40.81% / 37.10% remain below the 80% / 75% per-file floor at both points (pre-existing), recorded as FAIL rows dispositioned non-blocking because both improved or held against the merge base with no changed-line regression (N-1, N-2). + +### 1.3 Scenario completeness + +PASS. Positive (round-trip, forwarding, cold-cache maximum), negative (null store, null settings), edge (shorter ID does not save, longer ID saves again, `EmailInfoStagingFile` does not save, warm cache returns the cached value without persisting) and state-transition (save count across successive IDs) scenarios are present. The B-1 gap (no test of `IDList.MaxLengthOfID`) is closed by `MaxLengthOfID_ColdCacheOnMixedLengthList_ReturnsLongestIdLength`, `MaxLengthOfID_ColdCacheOnSecondDistinctList_ReturnsItsOwnLongestIdLength` and `MaxLengthOfID_WarmCacheAfterGetNextToDoID_ReturnsCachedLengthWithoutSaving`. + +## 2. General Code Change Policy Compliance + +| Rule | Verdict | Evidence | +|---|---|---| +| Design: simplicity and separation of concerns | PASS | A two-member interface and a small adapter; each consumer gains one field and one constructor overload. | +| No static mutable seam | PASS | The store is a per-instance `readonly` field in all four consumers. | +| Production behaviour preserved | PASS | Generated settings properties are `this[name]` reads and writes, so the store indexer plus `Save()` is equivalent; `ApplicationGlobals` still calls the public parameterless constructors. Cycle 1 changed no production file. | +| Error handling | PASS | Null stores and null settings are rejected with `ArgumentNullException` at construction. | +| File size (500 lines) | PASS | `file-sizes.2026-10-22T16-37.md`: created or modified `.cs` files at most 477 lines except the two pre-existing oversize files, which shrank (`DfDeedle_COM_Tests.cs` 848 to 809, `AppAutoFileObjects.cs` 853 to 791). `IDListGetNextToDoIDTests.cs` is 287 lines at head (reviewer read; 235 plus 52 added). | +| Public contracts documented | PASS | XML documentation on `ISettingsStore`, `ApplicationSettingsStore`, the new `IDList` constructors and the new consumer constructors. | +| Minimal public surface | PASS | `public` only where no `InternalsVisibleTo` grant exists (spec section 9); TaskMaster overloads are `internal`. | +| Dependencies | PASS | No package added. | +| Bugfix workflow (failing test first) | PASS | Probe tests failed before the production change (saves 1, 1, 1, 6) and pass after; DfDeedle fail-before-exception dossier recorded. | +| Requirements traceability | PASS | Every AC4 site maps to a `_settingsStore.Save()` call (`ac4-search-after.2026-10-22T16-27.md`). | +| Host-identifier hygiene in committed files | PASS | Reviewer Grep of the feature folder for drive-rooted paths, `\Users\` and the account name: 0 matches outside this audit family's own description of the search. Cycle-1 evidence refers to build logs as `SCRATCH`. | + +## 3. Language-Specific Code Change Policy Compliance + +### 3.1 C# (`CLAUDE.md` C# Code Change Policy, `.claude/rules/csharp.md`) + +| Rule | Verdict | Evidence | +|---|---|---| +| CSharpier formatting | PASS | `remediation-format.2026-10-22T17-09.md`: format left the tree unchanged on the final pass; `csharpier check .` exit 0 over 1682 files. | +| Analyzer rebuild | PASS | `remediation-analyzers.2026-10-22T17-09.md`: `/t:Rebuild`, 0 warnings, 0 errors. | +| Nullable rebuild (warnings as errors) | PASS | `remediation-typecheck.2026-10-22T17-09.md`: `/t:Rebuild`, 0 warnings, 0 errors, no `/p:Nullable=enable`. | +| Legacy `` registration | PASS | Entries present for every new `.cs` file (prior review); cycle 1 adds no file. | +| C# 7.3 in `ToDoModel.Test` and `TaskTree.Test` | PASS | Cycle-1 tests use `var`, collection initializers and Moq only; no target-typed `new()`, `is not`, switch expressions, `using` declarations or nullable annotations. | +| No production change to DfDeedle | PASS | No file under `UtilitiesCS/Extensions/` in the diff. | +| Coverage exclusions | PASS | 0 added `ExcludeFromCodeCoverage`; `coverage.config` not in the diff. | +| Suppressions | PASS | No `#pragma` or `SuppressMessage` added. | + +## 4. Language-Specific Unit Test Policy Compliance + +| Rule | Verdict | Evidence | +|---|---|---| +| MSTest attributes | PASS | `[TestClass]` and `[TestMethod]` throughout, including the three cycle-1 tests. | +| Moq | PASS | `Mock` in `ToDoModel.Test` and `TaskTree.Test`; the warm-cache test asserts `store.Verify(s => s.Save(), Times.Once())`. | +| FluentAssertions | PASS | All new assertions use FluentAssertions. | +| No serialization as a fix | PASS | No `[DoNotParallelize]` added; two class-level attributes removed. No runsettings file in the diff. | +| `[DoNotParallelize]` removal justified (AC8) | PASS | `evidence/other/ribbon-static-state-check.2026-10-22T16-23.md`. | +| Test location | PASS | Tests sit in the sibling `.Test` projects mirroring production folders. | + +## 5. Test Coverage Detail + +Per-file figures are the Cobertura `` `line-rate` and `branch-rate` attributes for each `filename` in `artifacts/csharp/coverage.xml` (head) and, for the merge base, the figures recorded in the prior review and `coverage-comparison.2026-10-22T17-12.md`. + +| File | Status | Baseline line / branch | Post-change line / branch | Gate | +|---|---|---|---|---| +| `UtilitiesCS/HelperClasses/Settings/ApplicationSettingsStore.cs` | new | absent | 100% / 100% | new code at least 90%: PASS | +| `UtilitiesCS/HelperClasses/Settings/ISettingsStore.cs` | new | absent | no executable lines | interface only | +| `TaskMaster/AppGlobals/AppAutoFileObjects.ScalarSettings.cs` | new | absent | 100% / 100% | new code at least 90%: PASS | +| `ToDoModel/Data Model/ID/IDList.cs` | modified | 24.36% (38/156) / 20.69% | 30.18% (51/169) / 25.81% | per-file floor FAIL (pre-existing), dispositioned non-blocking (N-2); no regression: PASS | +| `TaskMaster/AppGlobals/AppQuickFilerSettings.cs` | modified | 100% / 100% | 100% / 100% | PASS | +| `TaskMaster/AppGlobals/AppStagingFilenames.cs` | modified | 30.77% / 30.00% | 100% / 100% | PASS | +| `TaskMaster/AppGlobals/AppAutoFileObjects.cs` | modified | 45.66% (184/403) / 37.10% | 40.81% (151/370) / 37.10% | per-file floor FAIL (pre-existing), dispositioned non-blocking (N-1, move only) | + +B-1 closure detail. Head method `get_MaxLengthOfID` of class `ToDoModel.IDList` carries `line-rate="1" branch-rate="1"`; its seven line nodes (107 to 113) are all `hits="1"` and both branch lines (108 and 110) read `condition-coverage="100% (2/2)"`, matching the merge-base 7/7 lines and 4/4 branches. Arithmetic cross-check: prior head covered 44 of 169 lines; adding the getter's 7 gives 51 of 169 = 30.18%, the observed rate, so no other `IDList` line moved. The new comparison `coverage-comparison.2026-10-22T17-12.md` reports both line and branch rates for all three states and names the getter, which closes the disclosure half of B-1. + +## 6. Test Execution Metrics + +- MSTest full suite with coverage (cycle-1 final pass): 7621 total, 7621 passed, 0 failed, 0 skipped (`remediation-tests.2026-10-22T17-12.md`, derived from the trx). Prior pass: 7618; the delta of 3 equals the three added `MaxLengthOfID` tests. +- Parallel configuration: `scripts/vscode/TaskMaster.cli.runsettings` (MSTest `Workers` 0, `Scope` ClassLevel), `/InIsolation`. +- Targeted run: `maxlengthofid-tests.2026-10-22T17-09.md`, 3 executed, 3 passed, parallelization enabled. +- Repeated parallel runs of the affected classes: `parallel-repeat-runs.2026-10-22T16-30.md` (five combined runs, ten solo runs, zero failures). +- The reviewer did not re-run the suites (Bash limited to `git diff` and `git log`); coverage figures were re-read from the Cobertura document. + +## 7. Code Quality Checks + +| Check | Command | Result | Verdict | +|---|---|---|---| +| Confidentiality masking scan | Grep of the feature folder for drive-rooted paths, `\Users\` and the account name | 0 matches in evidence or plan files | PASS | +| Suppression scan (added lines) | `git diff 346651242..HEAD` read for `ExcludeFromCodeCoverage` (prior review `-G` scan; cycle-1 diff read in full) | none added | PASS | +| Serialization scan | prior review `git diff -G` scan for `DoNotParallelize`, `Thread\.Sleep`, `Task\.Delay`, `\[Timeout`, `Retry`, `Workers`, `GetTemp`; cycle-1 diff read in full | only two removals of `[DoNotParallelize]` | PASS | +| Workflow change scan | `git diff 346651242..HEAD --stat` filtered for `.github/` | empty | PASS | +| Raw evidence scan | `git diff 346651242..HEAD --stat` filtered for `.trx`, `.coverage`, Cobertura and `artifacts/` | empty; only two `.jacoco.xml` projections under `evidence/` | PASS | +| Scope fence scan | `git diff e399de597..3df0782ab --stat` | no production, project or runsettings file | PASS | + +## Evidence Location Compliance + +`git diff --stat 346651242..HEAD` lists no file under `artifacts/baselines/`, `artifacts/qa/`, `artifacts/evidence/` or `artifacts/coverage/`. All committed evidence, including the cycle-1 files, is under `/evidence/{baseline,regression-testing,qa-gates,other}/` as Markdown summaries and two package-level JaCoCo projections, which `CLAUDE.md` "Committed Test Evidence Format" permits. `validate_evidence_locations.py --root .` could not be run because the caller restricted the Bash tool to `git diff` and `git log`; the check was performed by diff inspection. Verdict: PASS. + +## 8. Gaps and Exceptions + +Blocking: none. + +Resolved: + +- B-1 (prior review): `IDList.MaxLengthOfID` coverage restored to 7/7 lines and 4/4 branches; `IDList.cs` branch rate 25.81% against the merge-base 20.69%; disclosure corrected in `coverage-comparison.2026-10-22T17-12.md`. + +Non-blocking: + +- N-1: `AppAutoFileObjects.cs` per-file line rate fell from 45.66% to 40.81% only because covered scalar-property lines moved into the new partial (100%); the file is below the 80% floor at both points (pre-existing, Outlook-bound members). +- N-2: `IDList.cs` remains below the 80% / 75% per-file floor (30.18% / 25.81%), pre-existing and improved against the merge base; the uncovered residue is the unchanged Outlook-bound and deserializing constructors. +- O-1: The TaskMaster CI checks (actionlint, build-analyzers, build-nullable, format-check, hygiene, mstest-coverage, pester) have not run because no pull request exists; they are a PR-time gate, not an acceptance criterion and not a blocking finding for this review. +- O-2: Out-of-scope items tracked by issue #1006 were not raised as findings. +- O-3: The prior O-1 (stale PR context) is closed: `artifacts/pr_context.summary.txt` now reports head `3df0782ab` and merge base `346651242`. + +## 9. Summary of Changes + +- Commit `39f38024c`: promotion of the bug to the active feature folder. +- Commit `0a6f080ef`: research, spec and preflight-cleared plan. +- Commit `e399de597`: settings-store seam, DfDeedle fake-clock tests, converted settings tests, probes, `[DoNotParallelize]` removals, evidence. +- Commit `85fb603b9`: first feature-review artifacts. +- Commit `3df0782ab`: three `MaxLengthOfID` tests, remediation plan and its evidence. + +## 10. Compliance Verdict + +**PASS.** Toolchain, parallel-isolation rules, repository coverage, new-code coverage, changed-line coverage, file sizes, scope fences and evidence form pass. Prior blocking finding B-1 is resolved. No blocking finding remains. + +## Appendix A: Test Inventory + +| Test file | Tests | Status | Scope | +|---|---|---|---| +| `UtilitiesCS.Test/HelperClasses/Settings/ApplicationSettingsStoreTests.cs` | 4 | new | adapter null guard and forwarding | +| `UtilitiesCS.Test/Extensions/DfDeedle_COM_Tests.QfcColumnsAsync.cs` | 2 | new (moved and converted) | `AddQfcColumnsAsync` with a fake clock | +| `UtilitiesCS.Test/Extensions/DfDeedle_COM_Tests.cs` | 24 | modified (`partial`, two tests moved out) | unchanged tests | +| `ToDoModel.Test/Data Model/ID/IDListGetNextToDoIDTests.cs` | 16 | modified (10 added, 3 in cycle 1) | ID arithmetic, persistence branch, `MaxLengthOfID` cache | +| `TaskTree.Test/TaskTreeControllerMoveLogicTests.cs` | 17 | modified | move logic with injected store and probe | +| `TaskTree.Test/TaskTreeControllerRouteDropTests.cs` | 7 | modified | drop routing with injected store | +| `TaskMaster.Test/AppGlobals/AppQuickFilerSettingsTests.cs` | 6 | modified (attribute removed) | high-confidence settings | +| `TaskMaster.Test/AppGlobals/AppQuickFilerSettingsRemainingPropertiesTests.cs` | 6 | modified | remaining boolean settings | +| `TaskMaster.Test/AppGlobals/AppStagingFilenamesTests.cs` | 10 | modified | staging file names | +| `TaskMaster.Test/AppGlobals/AppAutoFileObjectsCoverageExpansionTests.cs` | 10 | modified | scalar settings and initialization paths | +| `TaskMaster.Test/Ribbon/RibbonControllerTests.cs` | 17 (with `Engines` partial) | modified (attribute removed) | ribbon high-confidence helpers | +| `TaskMaster.Test/AppGlobals/InMemorySettingsStore.cs` | 0 | new helper | per-test fake store | +| `TaskMaster.Test/AppGlobals/SettingsSaveProbe.cs` | 0 | new helper | thread-attributed save probe | + +## Appendix B: Toolchain Commands Reference + +| Stage | Command | Run by | +|---|---|---| +| C# format | `dotnet tool run csharpier format .` then `dotnet tool run csharpier check .` | executor | +| C# analyzers | `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true` | executor | +| C# type check | `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true` | executor | +| C# test and coverage | `pwsh -NoProfile -File scripts/vscode/Invoke-MSTestWithCoverage.ps1` | executor | +| Targeted test run | `vstest.console.exe ToDoModel.Test.dll /Settings:scripts/vscode/TaskMaster.cli.runsettings /InIsolation /TestCaseFilter:` | executor | +| Diff scope | `git diff 346651242..HEAD --stat`; `git diff e399de597..3df0782ab --stat`; `git diff e399de597..3df0782ab -- ` | reviewer | +| Coverage reading | Grep of `filename="ToDoModel\Data Model\ID\IDList.cs"` and `name="get_MaxLengthOfID"` in `artifacts/csharp/coverage.xml` | reviewer | diff --git a/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/remediation-inputs.2026-10-22T17-10.md b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/remediation-inputs.2026-10-22T17-10.md new file mode 100644 index 000000000..ec03a76cf --- /dev/null +++ b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/remediation-inputs.2026-10-22T17-10.md @@ -0,0 +1,54 @@ +# Remediation Inputs: Issue #1005 (review 2026-10-22T17-10) + +Review-Verdict: REMEDIATION_REQUIRED + +Remediation-Mode: proportionate + +- Feature folder: `docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005` +- Branch head reviewed: `e399de597`; merge base `346651242a2bd6ed25d703a4b1ecb5a055c6ae72` +- Source audit artifacts: + - `docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/policy-audit.2026-10-22T17-10.md` + - `docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/code-review.2026-10-22T17-10.md` + - `docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/feature-audit.2026-10-22T17-10.md` +- Next link: `atomic-planner` authors the remediation plan per `remediation-handoff-atomic-planner`; this review does not author the plan. +- Mode rationale: one test-only addition plus a correction to one evidence summary; no production code, acceptance criterion or design decision changes. + +## Remediation-Required Findings + +### B-1 `IDList.MaxLengthOfID` lost all test coverage; `IDList.cs` branch coverage regressed + +- Severity: Blocking +Remediability: autonomous +Remediability-Evidence: the fix is a test-only addition to an existing test file plus a regenerated evidence summary; the getter's behaviour is fully determined by the source and needs no maintainer input or external system. +- Files: `ToDoModel.Test/Data Model/ID/IDListGetNextToDoIDTests.cs` (235 lines at head); `docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/evidence/qa-gates/coverage-comparison.2026-10-22T16-36.md` (superseded by a new timestamped comparison). +- Problem: the merge-base helper `CreateListWithCachedMaxLength` executed `_ = list.MaxLengthOfID;` and was the only test that ran the getter (`ToDoModel/Data Model/ID/IDList.cs` lines 104 to 114). Deleting it, as AC2 requires, dropped the getter from 7/7 lines and 4/4 branches covered (baseline Cobertura, method `get_MaxLengthOfID`) to 0/7 and 0/4 (`artifacts/csharp/coverage.xml`). The file's branch rate fell from 20.69% to 19.35% while the file is below the 80% / 75% floor. The committed coverage comparison reports only line rates for `IDList.cs` and attributes its uncovered residue to the unchanged constructors. +- Expected state after the fix: `get_MaxLengthOfID` has every line and both branch lines covered, so `IDList.cs` line and branch rates are not below the merge-base rates (24.36% line, 20.69% branch), and a new coverage comparison reports both rates for `IDList.cs` and names the getter. +- Fix: + - In `IDListGetNextToDoIDTests.cs`, add C# 7.3-compatible MSTest tests with an injected `Mock`: + - cold cache: `new IDList(new List { "02", "0100" }, store.Object).MaxLengthOfID` returns 4 (exercises the `_maxIDLength == 0` true branch and the `Max` lambda). Line 110 carries a second compiler-generated branch (the cached-lambda initialization), which the merge-base suite covered through four separate cold-cache reads; read a cold cache on at least two distinct lists so both arms execute in one test run; + - warm cache: on a store-injected list, call `GetNextToDoID("ZZ")` (caches 4), then read `MaxLengthOfID` and assert 4 without recomputation (exercises the false branch); assert `store.Verify(s => s.Save(), Times.Once())` so the read does not persist. + - Use a non-empty list in every new test: on an empty list with a zero cache the getter throws `InvalidOperationException` (pre-existing; out of scope). + - Keep the file at or under 500 lines; do not reintroduce the cache-priming helper. + - Run the full C# toolchain in CLAUDE.md order (CSharpier format and check, analyzer `/t:Rebuild`, warnings-as-errors `/t:Rebuild`, `scripts/vscode/Invoke-MSTestWithCoverage.ps1`), restarting from format if any step changes files or fails. + - Write a new timestamped coverage comparison under `evidence/qa-gates/` that reports `IDList.cs` line and branch rates at baseline and head and the `get_MaxLengthOfID` member figures; leave the earlier file in place. +- Verification commands: + - Grep `artifacts/csharp/coverage.xml` for `name="get_MaxLengthOfID"` and confirm `line-rate="1" branch-rate="1"`. + - Grep `artifacts/csharp/coverage.xml` for `filename="ToDoModel\Data Model\ID\IDList.cs"` and confirm `branch-rate` at or above 0.206897 and `line-rate` at or above 0.24359. + - `git diff 346651242a2bd6ed25d703a4b1ecb5a055c6ae72..HEAD --stat -- ToDoModel/ UtilitiesCS/ TaskMaster/` shows no new production change from the remediation commit. + +## Non-Blocking Items + +- N-1: `TaskMaster/AppGlobals/AppAutoFileObjects.cs` per-file line rate fell from 45.66% to 40.81% only because covered scalar-property lines moved into `AppAutoFileObjects.ScalarSettings.cs`; no action required. +- O-1: `artifacts/pr_context.summary.txt` and `.appendix.txt` describe the #1002 branch; regenerate them before the PR is authored. +- O-2: the TaskMaster CI checks have not run (no PR yet); they remain a PR-time gate. +- O-3 to O-8: informational observations recorded in the policy audit (section 8) and code review (findings table); no action required. + +## Do Not Do + +- Do not change production code; B-1 is test-only. +- Do not add `[ExcludeFromCodeCoverage]`, `coverage.config` entries, `[DoNotParallelize]`, sleeps, delays, retries or temporary files. +- Do not reintroduce `CreateListWithCachedMaxLength` or any test that reaches `ApplicationSettingsBase.Save()` on a real `Properties.Settings.Default`. +- Do not edit, uncheck or add acceptance criteria in `spec.md`. +- Do not address the in-memory `Settings.Default` mutations tracked by issue #1006. +- Do not commit raw `.trx`, `.coverage` or Cobertura documents; evidence stays as Markdown summaries and package-level JaCoCo projections under `evidence/`. +- Do not edit policy documents under `.claude/rules/` or `.github/instructions/`. diff --git a/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/remediation-plan.2026-10-22T17-10.md b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/remediation-plan.2026-10-22T17-10.md new file mode 100644 index 000000000..16f13721d --- /dev/null +++ b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/remediation-plan.2026-10-22T17-10.md @@ -0,0 +1,50 @@ +# Remediation Plan: Issue #1005 (review 2026-10-22T17-10, cycle R1, proportionate) + +- Feature folder: `docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005` +- Source: `remediation-inputs.2026-10-22T17-10.md` finding B-1 (the `IDList.MaxLengthOfID` getter lost all coverage; `IDList.cs` branch coverage regressed from 20.69% to 19.35%). +- Merge base: derived as `git merge-base origin/main HEAD`; merge-base `IDList.cs` figures: 24.36% of lines (38/156) and 20.69% of branches; `get_MaxLengthOfID` 7/7 lines and 4/4 branches. +- `SCRATCH` is `/issue-1005/`; every scratch file starts with `issue-1005-`. +- Scope: test-only. No production code, no `spec.md` change, no `.claude/**` or `CLAUDE.md` edit. The executor does not commit. Raw `.trx` and Cobertura documents are never added to git. +- Hard rules: no `[DoNotParallelize]`, no `Workers=1`, no retries, no sleeps or delays, no longer timeouts, no temporary files; every new test injects a `Mock`; MSTest, Moq, FluentAssertions; `ToDoModel.Test` is C# 7.3 (no nullable annotations); every touched file stays at or under 500 lines; the helper `CreateListWithCachedMaxLength` is not reintroduced. + +## Acceptance Inventory + +- B-1: `get_MaxLengthOfID` has every line and branch covered again, and `IDList.cs` line and branch rates are not below the merge-base rates. +- B-1-EVIDENCE: a new timestamped coverage comparison under `evidence/qa-gates/` reports `IDList.cs` line and branch rates at the merge base and at head and names `get_MaxLengthOfID`. +- SCOPE: no production change; the earlier `coverage-comparison.2026-10-22T16-36.md` is left in place. + +### Phase 0 — Policy reads and current IDList baseline + +- [x] [P0-T1] Read `CLAUDE.md`, `.claude/rules/general-code-change.md`, `.claude/rules/general-unit-test.md`, and `.claude/rules/csharp.md` in that order and write `evidence/baseline/phase0-instructions-read..md` carrying `Timestamp:`, `Policy Order:`, and the list of files read. Acceptance: the artifact exists with all three fields and four listed files. +- [x] [P0-T2] Read `artifacts/csharp/coverage.xml` (the head Cobertura from the reviewed run) and write `evidence/baseline/remediation-idlist-coverage-baseline..md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` recording the `IDList.cs` line rate, the `IDList.cs` branch rate, and the `get_MaxLengthOfID` method line and branch rates. Acceptance: the artifact records `get_MaxLengthOfID` at line-rate 0 and branch-rate 0, and `IDList.cs` branch rate 19.35% (the regressed head state). +- [x] [P0-T3] Record the line count of `ToDoModel.Test/Data Model/ID/IDListGetNextToDoIDTests.cs` before the edit in the same baseline artifact family as `evidence/baseline/remediation-file-size..md`. Acceptance: the artifact states 235 lines and the 500-line ceiling. + +### Phase 1 — Restore MaxLengthOfID coverage with store-injected tests + +- [x] [P1-T1] In `ToDoModel.Test/Data Model/ID/IDListGetNextToDoIDTests.cs` add test `MaxLengthOfID_ColdCacheOnMixedLengthList_ReturnsLongestIdLength`: `new IDList(new List { "02", "0100" }, store.Object).MaxLengthOfID` is 4 (exercises the zero-cache branch and the `Max` lambda). Acceptance: the test passes when run by name. +- [x] [P1-T2] In the same file add test `MaxLengthOfID_ColdCacheOnSecondDistinctList_ReturnsItsOwnLongestIdLength`: a second store-injected list `{ "02", "03", "04" }` returns 2, so the compiler-generated cached-lambda arm on the getter's `Max` line executes on more than one list. Acceptance: the test passes when run by name. +- [x] [P1-T3] In the same file add test `MaxLengthOfID_WarmCacheAfterGetNextToDoID_ReturnsCachedLengthWithoutSaving`: on a store-injected list `{ "02" }` call `GetNextToDoID("ZZ")` (returns `"0100"`, caches 4), then read `MaxLengthOfID` and assert 4 (the non-zero-cache branch), and assert `store.Verify(s => s.Save(), Times.Once())` so the read does not persist. Acceptance: the test passes when run by name. +- [x] [P1-T4] Confirm the touched test file is at or under 500 lines, uses only non-empty lists in the new tests, contains no `CreateListWithCachedMaxLength`, no `[DoNotParallelize]`, no `Thread.Sleep`, no `Task.Delay`, and no `Properties.Settings.Default`. Acceptance: Grep of the file for each token returns zero matches except the existing XML-doc mention of `MaxLengthOfID`, and `@(Get-Content -LiteralPath ).Count` style physical line count is at most 500. +- [x] [P1-T5] Run the three new tests by name through the repository vstest route with `TaskMaster.cli.runsettings` and `/InIsolation` and record `evidence/regression-testing/maxlengthofid-tests..md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:`. Acceptance: the summary reports 3 executed, 3 passed, 0 failed from the trx summary. + +### Phase 2 — Final QA loop (restart from P2-T1 on any failure or file change) + +- [x] [P2-T1] Run `dotnet tool run csharpier format .` and record `evidence/qa-gates/remediation-format..md`; the observation is the formatter summary line with a zero changed-file count on the final pass, then `dotnet tool run csharpier check .` exits 0. Acceptance: the artifact records both commands and a clean check. +- [x] [P2-T2] Run the analyzer `/t:Rebuild` command from `CLAUDE.md` (`EnableNETAnalyzers`, `EnforceCodeStyleInBuild`) and record `evidence/qa-gates/remediation-analyzers..md`. Acceptance: exit 0 with 0 errors recorded in `Output Summary:`. +- [x] [P2-T3] Run the warnings-as-errors `/t:Rebuild` command from `CLAUDE.md` (no `/p:Nullable=enable`) and record `evidence/qa-gates/remediation-typecheck..md`. Acceptance: exit 0 with 0 errors recorded. +- [x] [P2-T4] Run `scripts/vscode/Invoke-MSTestWithCoverage.ps1` over the full suite and record `evidence/qa-gates/remediation-tests..md` with the trx-derived pass and fail counts and the first-party line and branch percentages. Acceptance: 0 failed tests; repository line at or above 80% and branch at or above 75%. +- [x] [P2-T5] Copy `coverage/coverage.cobertura.xml` from the P2-T4 run to `artifacts/csharp/coverage.xml` so the re-audit reads current figures; the file stays untracked. Acceptance: Grep of `artifacts/csharp/coverage.xml` for `name="get_MaxLengthOfID"` shows `line-rate="1" branch-rate="1"` on that method element. +- [x] [P2-T6] Confirm `IDList.cs` in `artifacts/csharp/coverage.xml` has `line-rate` at or above 0.24359 and `branch-rate` at or above 0.206897. Acceptance: both values read from the `filename="ToDoModel\Data Model\ID\IDList.cs"` class element satisfy the thresholds; if either does not, add tests in P1 for the uncovered line or branch and restart the loop. +- [x] [P2-T7] Write `evidence/qa-gates/coverage-comparison..md` (the new file has a timestamp later than `coverage-comparison.2026-10-22T16-36.md`, which stays untouched) with `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` and a table of `IDList.cs` line and branch rates at the merge base (24.36% line, 20.69% branch), at the reviewed head (26.04% line, 19.35% branch), and now, plus the `get_MaxLengthOfID` member lines and branch lines covered now against the merge-base 7/7 and 4/4, and the package-level JaCoCo projection for any included figure. Acceptance: the artifact reports both rates for all three states, names `get_MaxLengthOfID` as covered again, and contains no absolute host path. +- [x] [P2-T8] Verify scope: `git diff --stat -- ToDoModel/ UtilitiesCS/ TaskMaster/` plus `git status --porcelain` show no production file changed by this remediation, and only `ToDoModel.Test/Data Model/ID/IDListGetNextToDoIDTests.cs` and files under the feature folder are newly modified; record `evidence/qa-gates/remediation-scope..md`. Acceptance: the artifact lists zero production changes and no `.trx`, `.coverage`, or Cobertura file as untracked-to-add. +- [x] [P2-T9] Walk the policy-audit, code-review, and feature-audit templates under `docs/features/templates/policy_audit/` against the branch diff from the merge base and write `evidence/qa-gates/self-review..md` with one PASS, FAIL, or N/A row per checklist item. Acceptance: no FAIL row; a FAIL row is fixed and the loop re-run before rewriting. + +## Pre-Mortem + +- Review checklists: P2-T9 walks all three audit templates; B-1's cause (an AC-driven deletion removed the only getter caller) is guarded by P2-T5 and P2-T6, which read the getter and the file rates directly. +- Coverage: the new tests target the getter's zero-cache branch, the warm branch, and the second cold read for the compiler-generated lambda branch; P2-T6 fails the plan if branch rate is below 20.69%, and P2-T7 reports line and branch rates, not line only. +- Mirror parity: not applicable, no mirrored, bundled, or `.claude` file and no fact in the execution-environment brief changes. +- File size: P0-T3 and P1-T4 hold the test file at 235 lines plus roughly 60 added, well under 500; a new file with `` is the fallback if csharpier growth exceeds the ceiling. +- Test isolation: each new test builds its own `Mock` and list, shares no static state, and never reaches `Properties.Settings.Default`; P2-T4 runs under the repository parallel runsettings with no serialization attribute, and no shuffled-order run is added because the change is one MSTest class with no shared state. +- CI-only behavior: the TaskMaster CI checks (actionlint, build-analyzers, build-nullable, format-check, hygiene, mstest-coverage, pester) are PR-time gates; P2-T1 through P2-T4 reproduce format-check, build-analyzers, build-nullable, and mstest-coverage locally, and no evidence artifact carries an absolute host path (hygiene). +- Evidence: every artifact lands under `evidence//` with the four fields and a write-time UTC stamp; consumers select the greatest `.*.md`; only Markdown summaries and projections are committed, never raw trx or Cobertura. diff --git a/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/research/2026-10-22T15-21-parallel-test-isolation-wall-clock-and-user-config-research.md b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/research/2026-10-22T15-21-parallel-test-isolation-wall-clock-and-user-config-research.md new file mode 100644 index 000000000..65fe99100 --- /dev/null +++ b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/research/2026-10-22T15-21-parallel-test-isolation-wall-clock-and-user-config-research.md @@ -0,0 +1,384 @@ +# Research: Parallel test failures from wall-clock deadlines and shared user.config (Issue #1005) + +- Issue: #1005 +- Branch: bug/parallel-test-failures-wall-clock-and-shared-user-config-1005 +- Work Mode: full-bug +- Requirement source: `issue.md` section `## Acceptance Criteria` (AC1 through AC7) +- Filename timestamp basis: the researcher has no clock tool; the stamp was taken as the UTC equivalent of the feature-folder plan stamp (America/New_York) plus the research interval. The orchestrator may rename the file if an exact write time is required. +- Evidence tags: [V-code] read in the repository by this research; [V-web] confirmed from Microsoft documentation or the dotnet/runtime source; [I] inferred, needs confirmation by the orchestrator. + +## 1. Summary of Findings + +1. DfDeedle (AC1): there is no production seam gap. `DfDeedle.AddQfcColumnsAsync` already accepts `columnAdder` and `timeProvider`, `FakeTimeProvider` is already referenced and already used by `DfDeedle_COM_Tests`, and the two offending tests only fail to pass the seam. The fix is test-only. +2. IDList (AC2, AC3): `IDList.GetNextToDoID` writes `Properties.Settings.Default.MaxLengthOfID` and calls `Save()` whenever the generated ID is longer than the cached `_maxIDLength`. A freshly constructed `IDList` has `_maxIDLength == 0`, so the first ID it produces always triggers `Save()`. Nine TaskTree.Test methods reach this path. +3. AC4: the entire repository has 26 production `Save()` calls on an `ApplicationSettingsBase` instance across 6 files. Fifteen are reached by tests (Numeric Derivation Evidence below). One more `Save()` is called directly by test code (`AppAutoFileObjectsCoverageExpansionTests.TestCleanup`). +4. Recommended design: one public settings-store abstraction in UtilitiesCS (`ISettingsStore`: indexer plus `Save()`). Its production adapter (`ApplicationSettingsStore`) wraps any `ApplicationSettingsBase`. Each consumer receives the store per instance through a constructor overload. Parameterless constructors default to the adapter over that assembly's `Properties.Settings.Default`, so production behavior is unchanged. Tests inject an in-memory fake or a Moq mock. No static mutable seam is used. +5. No human coverage-exemption decision is required. The adapter can be fully covered with a test subclass of `ApplicationSettingsBase` that overrides the indexer and `Save()`, so no test touches disk. + +## 2. Current State Analysis + +### 2.1 DfDeedle.AddQfcColumnsAsync [V-code] + +- Declaration (`UtilitiesCS/Extensions/DfDeedle.QfcColumns.cs`, file under `#nullable enable`): + `internal static async Task AddQfcColumnsAsync(Table table, MAPIFolder folder, CancellationToken token, int counter, Action? columnAdder = null, TimeProvider? timeProvider = null)` +- `columnAdder`: when null, uses the production `AddQfcColumns((Table)t, (MAPIFolder)f)`. It is typed over `object` because of CS1769 (embedded interop types). +- Retry loop: the work starts once with `Task.Run(() => adder(table, folder), token)`. For `attempt` from `counter` up to `AttemptLimit` (3), it awaits `work.TimeoutAfter(3000, timeProvider)`. + - Success returns. + - `TaskCanceledException` or `TimeoutException` returns when the token is cancelled; otherwise the loop re-arms the deadline on the same task. + - When all deadlines are spent and the token is not cancelled, it throws `TimeoutException("The column add step timed out after 9000 ms for folder ''...")`. +- `TimeoutTask.TimeoutAfter` (`UtilitiesCS/Threading/TimeOutTask.cs`) returns the task unchanged when it has already completed. Otherwise it arms `(timeProvider ?? TimeProvider.System).CreateTimer(...)`, and the continuation disposes the timer. With a `FakeTimeProvider` that is never advanced, the deadline can never fire, so completion depends only on the work. +- `DfDeedleQfcColumnTimeoutTests` (UtilitiesCS.Test) passes `probe.Adder` (a gated `Action`) and an `ArmingBarrierTimeProvider` (in-repo fake under `UtilitiesCS.Test/TestHelpers`). It fires each 3000 ms deadline with `barrier.Advance`. The class carries `[DoNotParallelize]` because of process-wide log4net state, not because of settings. +- Package availability: `UtilitiesCS.Test/packages.config` lists `Microsoft.Extensions.TimeProvider.Testing` 10.11.0 and `Microsoft.Bcl.TimeProvider` 10.0.13, and `UtilitiesCS.Test.csproj` carries matching ``/`HintPath` entries. `DfDeedle_COM_Tests.cs` already has `using Microsoft.Extensions.Time.Testing;` and uses `new FakeTimeProvider()` in its `GetEmailDataInViewAsync` test. No new NuGet dependency is needed. +- Every `AddQfcColumnsAsync` call site: + +| Call site | Passes columnAdder | Passes timeProvider | Wall-clock bound | +| --- | --- | --- | --- | +| `DfDeedle.GetEmailDataInViewAsync` (production) | no | forwards caller's `timeProvider` | only if caller passes none | +| `DfDeedle_COM_Tests.AddQfcColumnsAsync_HappyPath_CompletesWithoutThrowing` | no | no | yes (the reported failure) | +| `DfDeedle_COM_Tests.AddQfcColumnsAsync_PreCancelledToken_CompletesGracefully` | no | no | no in practice: `Task.Run` with a pre-cancelled token returns an already-cancelled task, so `TimeoutAfter` short-circuits. It still matches the AC4 "without its seams" search and is fixed for uniformity. | +| `DfDeedleQfcColumnTimeoutTests` (all five tests, through `StartColumnAdd`) | yes | yes (`ArmingBarrierTimeProvider`) | no | + +- Indirect callers through `GetEmailDataInViewAsync`: + - `DfDeedle_COM_Tests` (one test) passes `timeProvider: new FakeTimeProvider()`. + - `DfDeedleEtlTimeoutTests` (two tests) pass `barrier` or `new FakeTimeProvider()`. + - The production caller `QfcDatamodel.GetEmailsInViewDfAsync` (`QuickFiler/Controllers/QfcDatamodel.FrameBuilding.cs`) passes no `timeProvider`. Its one test, `QfcDatamodelRethrowTests.GetEmailsInViewDfAsync_InnerFailure_PreservesOriginatingFrameInStack`, throws a sentinel from `Explorer.CurrentView` during table acquisition, upstream of `AddQfcColumnsAsync`, so it never reaches the column add. This is reported as not in the AC4 family (see §7 for a related out-of-scope note). +- Conclusion: no production seam gap exists, and AC1 is satisfied by a test-only change. + +### 2.2 IDList and settings persistence in ToDoModel [V-code] + +- `ToDoModel/Data Model/ID/IDList.cs`: `public class IDList : SerializableList, IIDList`. +- Constructors: + - `()` + - `(IList)` + - `(IEnumerable)` + - `(string filename, string folderpath)` + - `(string, string, Outlook.Application)` `[ExcludeFromCodeCoverage]` + - `(string, string, CSVLoader, string, bool)` + - `(… , Outlook.Application)` `[ExcludeFromCodeCoverage]` + - The filename constructors deserialize through the `SerializableList` base. All of them are ordinary constructors, so a field initializer runs on every construction path. +- `GetNextToDoID(string strSeed)`: + 1. Validates the seed (`ThrowIfNullOrEmpty`). + 2. Increments in base 36 until it finds an ID not in the list, then adds it. + 3. When `strMaxID.Length > _maxIDLength`, updates `_maxIDLength`, sets `Properties.Settings.Default.MaxLengthOfID = (int)_maxIDLength;` and calls `Properties.Settings.Default.Save();`. + 4. Calls `Serialize()` when `Filepath is not null`. `SerializableList.Serialize()` is a no-op when `Filepath == ""`, which is the default. +- `GetNextToDoID()` seeds from `this.Max()`. +- `_maxIDLength` starts at 0 and is set lazily only by the `MaxLengthOfID` getter or by `RefreshIDList`. A list built with `new IDList()` therefore saves on its first generated ID. +- No other code in IDList or ToDoModel writes or saves `Properties.Settings`. `ToDoItem` reads `Properties.Settings.Default.MaxLengthOfID` (read only). `ToDoDefaults` reads `Properties.Settings.Default`. The generated setter in `ToDoModel/Properties/Settings.Designer.cs` is `this["MaxLengthOfID"] = value;`, so setting through the `ApplicationSettingsBase` indexer is behaviorally identical. +- Production construction: + - `AppToDoObjects.LoadIDList` uses `new IDList(FnameIDList, appData, Parent.Ol.App)`. + - `AppToDoObjects.LoadIdListFromDisk` uses `new IDList(ids) { Filename, Folderpath }`. +- Callers of `GetNextToDoID`: + - `TreeOfToDoItems.AssignId`, `ReNumberChildrenIDs`, `AddChild` + - `ToDoItem` (through its `IdList` property) + - `ToDoEvents` + - `FileOperationsPST` + - `TaskTreeController.MoveObjectsToSibling` (root-target branch) + - `TaskVisualization.AutoCreateProject` + - `TaskTreeController.MoveObjectsToSibling` (child-target branch) reaches it through `ReNumberChildrenIDs((IDList)_globals.TD.IDList)`. + - `MoveObjectsToChildren` reaches it through `_dataModel.AddChild(x, target, _globals.TD.IDList)`. +- Test reach: + - TaskTree.Test `MakeController` in both `TaskTreeControllerMoveLogicTests` and `TaskTreeControllerRouteDropTests` sets `mockTD.Setup(x => x.IDList).Returns(new IDList())`, which is a real, fresh list. + - `ToDoModel.Test` `IDListGetNextToDoIDTests` avoids `Save()` by pre-reading `MaxLengthOfID` (`CreateListWithCachedMaxLength`), and its null and empty seed tests throw before the save. That is a workaround, not a seam. + - All other tests use `Mock` (TaskVisualization.Test `AutoCreateProjectTests`, ToDoModel.Test `ToDoItemCoverageExpansionTests`). +- Language versions and project shape: + - ToDoModel: `latest`. + - ToDoModel.Test and TaskTree.Test: no `LangVersion` element, so the .NET Framework default applies: C# 7.3. No nullable annotations, target-typed `new()`, `is not`, switch expressions, or `using` declarations. + - TaskMaster: `preview`. TaskMaster.Test: `latest`. UtilitiesCS: `12.0`. UtilitiesCS.Test: `Latest`. + - Nullable is per-file opt-in through `#nullable enable`. + - All are legacy non-SDK projects, so every new `.cs` file needs a `` entry in its `.csproj`. +- Internals visibility: + - ToDoModel grants `InternalsVisibleTo("ToDoModel.Test")` only (declared in `ToDoItem.cs`), not TaskTree.Test. + - UtilitiesCS grants UtilitiesCS.Test and ToDoModel.Test, not ToDoModel or TaskMaster. + - TaskMaster grants TaskMaster.Test. + - TaskTree grants TaskTree.Test. + - Therefore the store interface must be `public` in UtilitiesCS, and the IDList injection constructor must be `public`. + +### 2.3 TaskMaster settings writers [V-code] + +| File | Members that write and save | Pattern | +| --- | --- | --- | +| `TaskMaster/AppGlobals/AppQuickFilerSettings.cs` (68 lines) | `MoveEntireConversation`, `SaveAttachments`, `SavePictures`, `SaveEmailCopy`, `HighConfidenceModeEnabled`, `HighConfidenceThreshold` (internal setters) | getter reads `Settings.Default.X`; setter writes it and calls `Settings.Default.Save()` | +| `TaskMaster/AppGlobals/AppStagingFilenames.cs` (139 lines) | `ConditionalReminders`, `CommonWords`, `SubjectMap`, `CtfInc`, `CtfMap`, `EmailSessionTemp`, `EmailSession`, `MovedMails`, `RecentsFile` | lazy getter from `Settings.Default`; setter writes the backing field and `Settings.Default`, then calls `Save()`. `EmailInfoStagingFile` does not save. | +| `TaskMaster/AppGlobals/AppAutoFileObjects.cs` (853 lines, partial) | `LngConvCtPwr`, `Conversation_Weight`, `SmithWatterman_MatchScore`, `SmithWatterman_MismatchScore`, `SmithWatterman_GapPenalty`, `MaxRecents` | field `_defaults = Properties.Settings.Default`; setter writes and calls `_defaults.Save()`. `_defaults` is also read for about 20 file-name settings. | +| `TaskMaster/AppGlobals/AppOlObjects.JunkFolders.cs` | `ApplyJunkFolderSelections`, `LoadJunkPotential` (after the user picks a folder), `LoadJunkCertain` (after the user picks a folder) | `Properties.Settings.Default.Save()` | +| `TaskMaster/AppGlobals/AppOlObjects.cs` | `DarkMode` setter | `Properties.Settings.Default.Save()` | + +The other settings classes have no `Save()` call: UtilitiesCS, QuickFiler, TaskVisualization, and Tags (`My.Settings.Save()` appears only inside a commented-out VB block in `Tags/My Project/Settings.Designer.cs`). + +## 3. Why All Settings Classes Share One user.config + +- `ApplicationSettingsBase.Save()` calls `OnSettingsSaving` and then, unless cancelled, `base.Save()`. That call reaches the provider's `SetPropertyValues` [V-web, `ApplicationSettingsBase.Save` reference page and dotnet/runtime source]. The default `LocalFileSettingsProvider` writes user-scoped values [V-web]. +- The user.config location is derived from the host identity (company, executable or AppDomain evidence, version), not from the settings class. Each settings class is a section group keyed by its full type name inside that one file [I; standard .NET Framework behavior, not tool-verified here]. The maintainer's failure path, `...\TestSourceHost__...\user.config`, is consistent with a host-derived location. +- Consequences: + - Within one testhost process, `ToDoModel.Properties.Settings`, `TaskMaster.Properties.Settings`, and any other settings class read and rewrite the same file. + - The configuration system checks whether the file changed since it was loaded and throws `ConfigurationErrorsException: The configuration file has been changed by another program` when a concurrent writer has replaced it. + - When Test Explorer runs several test assemblies in parallel testhost processes from the same host executable, those processes resolve the same path, so cross-assembly races are possible too [I]. + - `TaskMaster.runsettings` and `scripts/vscode/TaskMaster.cli.runsettings` set `Workers=0`, `Scope=ClassLevel`, so classes within one assembly run concurrently. +- Side effect: the snapshot and restore in the TaskMaster test `TestCleanup` methods restores values in memory only. The last test-mutated value that was saved stays in the host's user.config. + +## 4. Candidate Approaches + +### Selected: per-instance `ISettingsStore` (UtilitiesCS) with an `ApplicationSettingsBase` adapter + +- New public interface `UtilitiesCS.ISettingsStore`: + - `object this[string propertyName] { get; set; }` + - `void Save();` + - XML docs state that `Save` persists every pending value, mirroring `ApplicationSettingsBase`. +- New public sealed adapter `UtilitiesCS.ApplicationSettingsStore : ISettingsStore`: + - Constructor takes an `ApplicationSettingsBase` and throws `ArgumentNullException` on null. + - The indexer forwards to `settings[propertyName]`, and `Save()` forwards to `settings.Save()`. +- Consumers hold `private readonly ISettingsStore _settingsStore`. Parameterless or existing constructors default it to `new ApplicationSettingsStore(Properties.Settings.Default)` of their own assembly, and a new overload accepts an `ISettingsStore`. Property names use `nameof(Properties.Settings.X)` to stay tied to the generated class. +- Advantages: + - One small abstraction is reused by two assemblies (ToDoModel and TaskMaster). + - Injection is per instance, so there is no shared static seam. + - The production default is byte-for-byte equivalent, because generated property setters are `this[name] = value`. + - It matches the repo's DI-seam preference order (interface first) in `.claude/rules/csharp.md`. + - The getters can also read through the store, which removes the in-memory `Settings.Default` mutation from the affected TaskMaster tests. +- Limitations: indexer access is untyped (casts at the call sites). This is acceptable for the small, fixed member set. + +### Rejected alternatives (brief) + +- Static settable default persistence delegate or `SerializableList`-style static seam: shared mutable static state across parallel tests, prohibited by the item constraints. +- Redirecting user.config or installing a custom `SettingsProvider`: global, and either touches the file system or mutates process-wide state. +- `[DoNotParallelize]`, `Workers=1`, retries, or longer timeouts: prohibited (AC6). +- Pre-reading `MaxLengthOfID` in tests (the current `IDListGetNextToDoIDTests` workaround): it leaves the `Save()` path reachable, does not work on empty lists (`Max()` throws), and is not applicable to TaskTree's `MakeController`. +- An IDList-specific `Action` delegate: it would solve only IDList and would require a second abstraction for the TaskMaster writers. + +## 5. Recommended Changes + +### 5.1 AC1: DfDeedle tests (test-only) + +- `AddQfcColumnsAsync_HappyPath_CompletesWithoutThrowing`: + - Pass `timeProvider: new FakeTimeProvider()` and never advance it. Keep the production adder (`columnAdder` null) so the default-adder branch remains exercised; the deadline can then never fire. + - Await the task with `await` in an `async Task` test instead of `GetAwaiter().GetResult()`. + - Assert completion without exception. + - Optionally strengthen the test by verifying the mocked `Columns.Add("SentOn")` call. +- `AddQfcColumnsAsync_PreCancelledToken_CompletesGracefully`: pass `timeProvider: new FakeTimeProvider()` as well. +- File size: `UtilitiesCS.Test/Extensions/DfDeedle_COM_Tests.cs` is 848 lines, already above the 500-line cap, and must not grow. Mark the class `partial` and move the `AddQfcColumnsAsync` section (both tests) into a new `UtilitiesCS.Test/Extensions/DfDeedle_COM_Tests.QfcColumnsAsync.cs`. The new file keeps access to the private `BuildTableMock` and `BuildFolderWithUdp` helpers, and the original file shrinks. Add a `` entry in `UtilitiesCS.Test.csproj`. + +### 5.2 AC2, AC3: IDList seam + +- Add to `IDList`: + - `private readonly ISettingsStore _settingsStore = new ApplicationSettingsStore(Properties.Settings.Default);`. The field initializer runs for every constructor, including the deserializing and Outlook-bound ones. + - New public constructors `IDList(IEnumerable ids, ISettingsStore settingsStore)` and optionally `IDList(ISettingsStore settingsStore)`. Each assigns the injected store after a null guard. +- In `GetNextToDoID`, replace the two settings lines with: + +```csharp +_settingsStore[nameof(Properties.Settings.MaxLengthOfID)] = _maxIDLength; +_settingsStore.Save(); +``` + + The redundant `(int)` cast is dropped because `_maxIDLength` is already `int`. +- No change to `IIDList`, `TreeOfToDoItems`, `TaskTreeController`, or `AppToDoObjects`. The production default is identical. +- TaskTree.Test (C# 7.3): change `MakeController` in both test classes to `new IDList(new List(), settingsStore.Object)`, where `settingsStore` is a `Mock`. The `(IDList)` cast in `MoveObjectsToSibling` still succeeds because the object is still an `IDList`. `TaskTree.Test.csproj` already references UtilitiesCS (the test files use the `UtilitiesCS` namespace) and `System.Configuration`. +- ToDoModel.Test (C# 7.3): add tests for the persistence branch: + - The first ID on a fresh list sets `MaxLengthOfID` to the new length and saves once. + - A shorter ID does not save. + - A rollover to a longer ID saves again. + - A null store throws `ArgumentNullException`. + - Convert `CreateListWithCachedMaxLength` to inject a mock store, and remove the cache-priming workaround. + +### 5.3 AC4: TaskMaster writers reached by tests + +- `AppQuickFilerSettings`: + - Add the field and constructors `AppQuickFilerSettings()` (default adapter) and `AppQuickFilerSettings(ISettingsStore settingsStore)`. + - Getters return `(bool)_settingsStore[nameof(Settings.X)]` or `(double)...`. + - Setters assign through the store and call `_settingsStore.Save()`. + - `ApplicationGlobals` keeps `new AppQuickFilerSettings()`, unchanged. +- `AppStagingFilenames`: same pattern for all ten properties. The getters' `InitProp` reads go through the store. The nine saving setters use the store. Converting all nine keeps the class internally consistent, although only two are reached by tests. +- `AppAutoFileObjects`: + - Add `ISettingsStore _settingsStore` (default adapter) and the overload `AppAutoFileObjects(IApplicationGlobals parent, ISettingsStore settingsStore)`. + - Route only the six scalar properties through the store. The file-name reads on `_defaults` are untouched. + - File size: the main file is 853 lines. Move the six scalar properties into a new partial `TaskMaster/AppGlobals/AppAutoFileObjects.ScalarSettings.cs` so the main file shrinks, and add a `` entry in `TaskMaster.csproj`. +- `AppOlObjects` (`ApplyJunkFolderSelections`, `LoadJunkPotential`, `LoadJunkCertain`, `DarkMode`): not reached by any test (see §6), so not in the AC4 family. Recommend no change in this item to keep the COM-bound class out of scope; noted as an optional follow-up. + +### 5.4 Test-side changes for AC4 + +- New `TaskMaster.Test/AppGlobals/InMemorySettingsStore.cs` (C# latest): a dictionary-backed `ISettingsStore` with a `SaveCount` property and seeding through the indexer. Add `` in `TaskMaster.Test.csproj`. +- `AppQuickFilerSettingsTests`, `AppQuickFilerSettingsRemainingPropertiesTests`, `AppStagingFilenamesTests`: + - Construct the system under test with an `InMemorySettingsStore`. + - Assert round-trips against the fake store, and assert that `SaveCount` incremented for saving setters (and did not for `EmailInfoStagingFile`). + - Remove the `Settings.Default` snapshot and restore. Remove `[DoNotParallelize]` from `AppQuickFilerSettingsTests`, whose only shared state was `Settings.Default`. +- `RibbonControllerTests`: + - `CreateController` sets `_quickFilerSettings` to `new AppQuickFilerSettings(store)` with a seeded fake. + - Assertions read the fake store instead of `Settings.Default`. + - Remove the snapshot and restore. + - `[DoNotParallelize]` removal: the class doc attributes the attribute to the settings snapshot. The rest of the 452-line class and the `RibbonControllerTests.Engines.cs` partial must be read by the executor for other process-global state before removal. Keep the attribute if any other shared static is found and record why. +- `AppAutoFileObjectsCoverageExpansionTests`: + - `CreateSutWithSpecialFolders` passes an `InMemorySettingsStore` seeded with the six scalar values. + - `ScalarSettingsProperties_WhenRoundTripped_UpdateDefaultsAndReturnValues` asserts against the fake. + - Delete the `TestInitialize` and `TestCleanup` snapshot, including the direct `defaults.Save()` call. + +### 5.5 Seam unit tests + +New `UtilitiesCS.Test/HelperClasses/Settings/ApplicationSettingsStoreTests.cs` with a nested `RecordingApplicationSettings : ApplicationSettingsBase`. It overrides `public override object this[string]` and `public override void Save()` (both overridable per the dotnet/runtime source [V-web]), stores values in a dictionary, and counts saves. Because both overridden members bypass the base, no provider is invoked and no file is read or written. The tests cover: + +- the null constructor argument +- get forwarding +- set forwarding +- save forwarding + +Add a `` entry. + +## 6. Behavior Semantics and Requirements Mapping + +- Success: + - No unit test calls `ApplicationSettingsBase.Save()` on any `Properties.Settings.Default`. + - No test awaits a wall-clock deadline on `AddQfcColumnsAsync`. + - Production paths save exactly when they saved before (same condition, same property, same value). +- Ordering rule preserved in `GetNextToDoID`: add the ID, then persist the max length if it grew, then serialize the list. +- Edge cases: + - An empty list's first ID always persists (unchanged). + - A null store passed explicitly is rejected at construction (fail fast). Default constructors never hold null. + - `AppStagingFilenames.EmailInfoStagingFile` must remain non-persisting. + +| AC | Design element | +| --- | --- | +| AC1 | §5.1 test changes | +| AC2 | §5.2 IDList store with default adapter | +| AC3 | TaskTree.Test `MakeController` injection in both classes. `IDListGetNextToDoIDTests` converted. No other test reaches a real `IDList.GetNextToDoID`. | +| AC4 | §5.3 and §5.4 changes plus the evidence search (Numeric Derivation Evidence below) | +| AC5 | §8 fail-before strategy | +| AC6 | parallel CLI run with `TaskMaster.cli.runsettings`. No serialization added. `[DoNotParallelize]` removed where it existed only for settings. | +| AC7 | §9 coverage plan plus the full toolchain | + +### Test-to-site reach map (production `Save()` sites reached by tests) + +| Production site | Reaching test methods | +| --- | --- | +| IDList.cs:GetNextToDoID | TaskTreeControllerMoveLogicTests: `MoveObjectsToSibling_RootTarget_RemovesFromRootsAndReseeds`, `MoveObjectsToSibling_RootNotInRoots_FiresMessageSeam`, `MoveObjectsToSibling_ChildTarget_InsertsIntoParentChildren`, `MoveObjectsToChildren_RootSource_ReparentsAndRemovesFromRoots`, `MoveObjectsToChildren_DesyncedRoot_FiresMessageSeam`, `MoveObjectsToChildren_NonRootSource_ReparentsUnderTarget`. TaskTreeControllerRouteDropTests: `RouteDrop_Item_RoutesToChildren_ReturnsTrue`, `RouteDrop_AboveItem_InsertsSiblingBeforeTarget_ReturnsTrue`, `RouteDrop_BelowItem_InsertsSiblingAfterTarget_ReturnsTrue` | +| AppQuickFilerSettings.cs:MoveEntireConversation | AppQuickFilerSettingsRemainingPropertiesTests.`MoveEntireConversation_Setter_RoundTripsThroughSettingsDefault` | +| AppQuickFilerSettings.cs:SaveAttachments | AppQuickFilerSettingsRemainingPropertiesTests.`SaveAttachments_Setter_RoundTripsThroughSettingsDefault` | +| AppQuickFilerSettings.cs:SavePictures | AppQuickFilerSettingsRemainingPropertiesTests.`SavePictures_Setter_RoundTripsThroughSettingsDefault` | +| AppQuickFilerSettings.cs:SaveEmailCopy | AppQuickFilerSettingsRemainingPropertiesTests.`SaveEmailCopy_Setter_RoundTripsThroughSettingsDefault` | +| AppQuickFilerSettings.cs:HighConfidenceModeEnabled | AppQuickFilerSettingsTests.`HighConfidenceModeEnabled_WhenSetTrue_ReadsBackTrue`. RibbonControllerTests: `ToggleHighConfidenceMode_FlipsStoredValue`, `SetHighConfidenceModeForLaunch_True_EnablesMode`, `StandardLaunchAfterHighConfidenceLaunch_DoesNotEnableMode` | +| AppQuickFilerSettings.cs:HighConfidenceThreshold | AppQuickFilerSettingsTests.`HighConfidenceThreshold_WhenSet_ReadsBackSameValue`. RibbonControllerTests.`SetHighConfidenceThresholdText_WithValidPercentage_PersistsProbability` | +| AppStagingFilenames.cs:ConditionalReminders | AppStagingFilenamesTests.`ConditionalReminders_Setter_RoundTripsThroughSettingsDefault` | +| AppStagingFilenames.cs:CommonWords | AppStagingFilenamesTests.`CommonWords_GetterAfterSet_ReturnsCachedValueWithoutReReadingSettings` | +| AppAutoFileObjects.cs (all six scalar setters) | AppAutoFileObjectsCoverageExpansionTests.`ScalarSettingsProperties_WhenRoundTripped_UpdateDefaultsAndReturnValues` | + +Test code that calls `Save()` directly: `AppAutoFileObjectsCoverageExpansionTests.TestCleanup` (`defaults.Save()`). It runs after every test in that class. + +Unreached production sites (not in the family): + +- `AppStagingFilenames` `SubjectMap`, `CtfInc`, `CtfMap`, `EmailSessionTemp`, `EmailSession`, `MovedMails`, and `RecentsFile` setters: no test assigns them, and `AppFileSystemFolderPaths` only constructs the class. +- `AppOlObjects.ApplyJunkFolderSelections`: the only test caller (`StoreWrapperControllerTests`) uses a `RecordingOlObjects` fake in UtilitiesCS.Test. +- `AppOlObjects.LoadJunkPotential`: no test. +- `AppOlObjects.LoadJunkCertain`: `AppOlObjectsTests.LoadJunkCertain_KeepsStoredValue_WhenReplacementSelectionIsCancelled` returns null before the save because `PickFolder` returns null. +- `AppOlObjects.DarkMode`: the QuickFiler.Test callers set `Ol.DarkMode` on `Mock`. + +## Numeric Derivation Evidence + +Complete Family: IDList.cs:GetNextToDoID, AppQuickFilerSettings.cs:MoveEntireConversation, AppQuickFilerSettings.cs:SaveAttachments, AppQuickFilerSettings.cs:SavePictures, AppQuickFilerSettings.cs:SaveEmailCopy, AppQuickFilerSettings.cs:HighConfidenceModeEnabled, AppQuickFilerSettings.cs:HighConfidenceThreshold, AppStagingFilenames.cs:ConditionalReminders, AppStagingFilenames.cs:CommonWords, AppAutoFileObjects.cs:LngConvCtPwr, AppAutoFileObjects.cs:Conversation_Weight, AppAutoFileObjects.cs:SmithWatterman_MatchScore, AppAutoFileObjects.cs:SmithWatterman_MismatchScore, AppAutoFileObjects.cs:SmithWatterman_GapPenalty, AppAutoFileObjects.cs:MaxRecents +Exhaustive Search Scope: the entire repository, every tracked *.cs file in all production projects (UtilitiesCS, ToDoModel, TaskMaster, QuickFiler, TaskTree, TaskVisualization, Tags, SVGControl) and all test projects (*.Test), covering every ApplicationSettingsBase subclass (the six Settings.Designer.cs classes) and every receiver form of Save() on them (Settings.Default.Save(), Properties.Settings.Default.Save(), and a cached field or local such as _defaults.Save() or defaults.Save()) +Inclusion Rules: a production (non-test) member whose body calls Save() on an ApplicationSettingsBase instance, and that at least one test method executes up to that call under the test's own arrangement, traced through the concrete call chain +Exclusion Rules: Save() on Outlook items, views, project data, or other non-settings receivers; commented-out code (including the VB block in Tags/My Project/Settings.Designer.cs); production Save() sites that no test reaches (seven AppStagingFilenames setters, AppOlObjects.JunkFolders.cs ApplyJunkFolderSelections, LoadJunkPotential, LoadJunkCertain, and AppOlObjects.cs DarkMode); test-code Save() calls (AppAutoFileObjectsCoverageExpansionTests.TestCleanup, recorded separately and fixed) +Primary Search Strategy: forward from production. A repository-wide content search of all *.cs for the regular expression `\.Save\(\)` produced every Save() call (Outlook and settings mixed). Filtering to settings receivers through a search for `ApplicationSettingsBase` declarations and the field forms `Properties.Settings _defaults` and `= Properties.Settings.Default;` gave 26 settings sites. Each enclosing member was then traced to its test callers by searching test projects for its setter assignment or caller method, which yielded IDList.cs:GetNextToDoID, AppQuickFilerSettings.cs:MoveEntireConversation, AppQuickFilerSettings.cs:SaveAttachments, AppQuickFilerSettings.cs:SavePictures, AppQuickFilerSettings.cs:SaveEmailCopy, AppQuickFilerSettings.cs:HighConfidenceModeEnabled, AppQuickFilerSettings.cs:HighConfidenceThreshold, AppStagingFilenames.cs:ConditionalReminders, AppStagingFilenames.cs:CommonWords, AppAutoFileObjects.cs:LngConvCtPwr, AppAutoFileObjects.cs:Conversation_Weight, AppAutoFileObjects.cs:SmithWatterman_MatchScore, AppAutoFileObjects.cs:SmithWatterman_MismatchScore, AppAutoFileObjects.cs:SmithWatterman_GapPenalty, AppAutoFileObjects.cs:MaxRecents +Cross-check Search Strategy: backward from tests. A content search of every *.Test project for `Settings\.Default`, `new AppQuickFilerSettings`, `new AppStagingFilenames`, `new AppAutoFileObjects`, `new AppOlObjects`, `new IDList`, `IDList\)\.Returns`, `GetNextToDoID`, the RibbonController toggle and threshold helpers, and the DarkMode and junk-folder members. Each matching test was then traced forward through production call chains (TaskTreeController.MoveObjectsToSibling, TaskTreeController.MoveObjectsToChildren, TreeOfToDoItems.AddChild, TreeOfToDoItems.ReNumberChildrenIDs, RibbonController.Intelligence helpers) to the Save() it executes, which yielded IDList.cs:GetNextToDoID, AppQuickFilerSettings.cs:MoveEntireConversation, AppQuickFilerSettings.cs:SaveAttachments, AppQuickFilerSettings.cs:SavePictures, AppQuickFilerSettings.cs:SaveEmailCopy, AppQuickFilerSettings.cs:HighConfidenceModeEnabled, AppQuickFilerSettings.cs:HighConfidenceThreshold, AppStagingFilenames.cs:ConditionalReminders, AppStagingFilenames.cs:CommonWords, AppAutoFileObjects.cs:LngConvCtPwr, AppAutoFileObjects.cs:Conversation_Weight, AppAutoFileObjects.cs:SmithWatterman_MatchScore, AppAutoFileObjects.cs:SmithWatterman_MismatchScore, AppAutoFileObjects.cs:SmithWatterman_GapPenalty, AppAutoFileObjects.cs:MaxRecents +Primary Member Set: IDList.cs:GetNextToDoID, AppQuickFilerSettings.cs:MoveEntireConversation, AppQuickFilerSettings.cs:SaveAttachments, AppQuickFilerSettings.cs:SavePictures, AppQuickFilerSettings.cs:SaveEmailCopy, AppQuickFilerSettings.cs:HighConfidenceModeEnabled, AppQuickFilerSettings.cs:HighConfidenceThreshold, AppStagingFilenames.cs:ConditionalReminders, AppStagingFilenames.cs:CommonWords, AppAutoFileObjects.cs:LngConvCtPwr, AppAutoFileObjects.cs:Conversation_Weight, AppAutoFileObjects.cs:SmithWatterman_MatchScore, AppAutoFileObjects.cs:SmithWatterman_MismatchScore, AppAutoFileObjects.cs:SmithWatterman_GapPenalty, AppAutoFileObjects.cs:MaxRecents +Cross-check Member Set: IDList.cs:GetNextToDoID, AppQuickFilerSettings.cs:MoveEntireConversation, AppQuickFilerSettings.cs:SaveAttachments, AppQuickFilerSettings.cs:SavePictures, AppQuickFilerSettings.cs:SaveEmailCopy, AppQuickFilerSettings.cs:HighConfidenceModeEnabled, AppQuickFilerSettings.cs:HighConfidenceThreshold, AppStagingFilenames.cs:ConditionalReminders, AppStagingFilenames.cs:CommonWords, AppAutoFileObjects.cs:LngConvCtPwr, AppAutoFileObjects.cs:Conversation_Weight, AppAutoFileObjects.cs:SmithWatterman_MatchScore, AppAutoFileObjects.cs:SmithWatterman_MismatchScore, AppAutoFileObjects.cs:SmithWatterman_GapPenalty, AppAutoFileObjects.cs:MaxRecents +Primary Count: 15 +Cross-check Count: 15 +Member-set Comparison: the normalized primary and cross-check member sets are identical (15 equal members, no member present in only one set). The 26 total production settings Save() sites reconcile as 15 reached plus 11 unreached (seven AppStagingFilenames setters, three AppOlObjects.JunkFolders.cs members, one AppOlObjects.cs DarkMode setter), and the per-file totals are 1 + 6 + 9 + 3 + 1 + 6 = 26. + +The AC4 DfDeedle half is a separate enumeration. Two tests call `AddQfcColumnsAsync` without either seam (the HappyPath and PreCancelledToken tests in `DfDeedle_COM_Tests`). The five `DfDeedleQfcColumnTimeoutTests` pass both seams. All three `GetEmailDataInViewAsync` test callers pass `timeProvider`. + +## 7. Secondary Inventory (report only) and Scope Recommendations + +| Item | Shared state | Saves? | Recommendation | +| --- | --- | --- | --- | +| `AppQuickFilerSettingsTests` `[DoNotParallelize]` | `Settings.Default` high-confidence values | via SUT | Same root cause; fixed in this item (store injection, remove attribute) | +| `RibbonControllerTests` `[DoNotParallelize]` | `Settings.Default` high-confidence values | via SUT | Same root cause; fix here. Remove the attribute only after confirming no other static state in the class. | +| `AppQuickFilerSettingsRemainingPropertiesTests`, `AppStagingFilenamesTests`, `AppAutoFileObjectsCoverageExpansionTests` (in-memory snapshot and restore) | `Settings.Default` | yes | Same root cause; fixed here | +| `ApplicationGlobalsStartupTimingTests` (per-method `[DoNotParallelize]`) and `ApplicationGlobalsTests` | `Settings.Default.StartupTimingEnabled`, in memory | no | Same family (shared static settings) but a different production path that this item does not touch. File a follow-up issue through the MCP promotion lifecycle. | +| `AppEventsTests` | `Settings.Default.EventsHooked`, in memory | no | Follow-up issue (same as above) | +| `AppAutoFileObjectsFolderPredictorTests` | `Settings.Default.UseLcppnPredictor`, in memory | no, but the old `AppAutoFileObjectsCoverageExpansionTests.TestCleanup` save could persist it concurrently | The concurrent-persist risk disappears with this item. The in-memory mutation goes to the follow-up issue. | +| `AppOlObjectsTests` (`JunkPotential`, `OlJunkCertain`) | in memory | no | Follow-up issue | +| `AppToDoObjectsTests` (`PropertyValues` injection into `Settings.Default`) | in memory | no | Follow-up issue | +| `DfDeedleQfcColumnTimeoutTests`, `DfDeedleEtlTimeoutTests`, and other UtilitiesCS.Test `[DoNotParallelize]` classes | log4net, UI-thread, and store-wrapper statics | no | Unrelated to settings; out of scope | +| `QfcDatamodel.GetEmailsInViewDfAsync` calls `GetEmailDataInViewAsync` without `timeProvider` | wall clock in production only | n/a | Its one test throws before any deadline. Not a test defect, so no action. | + +No `Workers=1`, retry, `Thread.Sleep`, or `Task.Delay` usage in a test project exists to cope with shared settings. Workers settings are `0` in both runsettings files and in the `[assembly: Parallelize]` of UtilitiesCS.Test. + +## 8. Fail-Before Strategy (AC5) + +- IDList (TaskTree.Test): add a deterministic shared-settings probe assertion to `MoveObjectsToChildren_DesyncedRoot_FiresMessageSeam` before any production change. + - Resolve `ToDoModel.Properties.Settings.Default` by reflection: `typeof(IDList).Assembly.GetType("ToDoModel.Properties.Settings")`, then the public static `Default` property cast to `ApplicationSettingsBase`. + - Subscribe a `SettingsSaving` handler that records only when `Environment.CurrentManagedThreadId` equals the test thread's ID, and sets `e.Cancel = true`. `ApplicationSettingsBase.Save` skips `base.Save()` when cancelled [V-web], so the fail-before run writes nothing to user.config. + - Unsubscribe in `finally`. + - Assert that no save was recorded. Before the fix: fails (one save on the test thread). After `MakeController` injects the mock store: passes, and the mock verifies `Save()` once. + - The handler is thread-attributed and removed in `finally`, so concurrent classes are not affected after the fix. +- TaskMaster setters: apply the same probe through `TaskMaster.Properties.Settings.Default.SettingsSaving` (accessible through `InternalsVisibleTo`) on one representative test per class: `AppQuickFilerSettingsTests`, `AppStagingFilenamesTests`, `AppAutoFileObjectsCoverageExpansionTests`. Run before and after. +- ToDoModel.Test seam tests: the new persistence-branch tests fail before the seam exists (compile failure). Prefer the runtime probe above as the recorded fail-before, and record the compile-stage result only as supplementary. +- DfDeedle: the original failure needs real wall-clock elapse under load and cannot be reproduced deterministically without timers. Record a `fail-before-exception..md` dossier under `evidence/regression-testing/` with `WhyFailingRunImpossible`. Use the AC4 search as the repeatable alternative proof: it finds two seamless calls before and zero after, with the maintainer's captured exception text quoted from `issue.md`. + +## 9. Testing and Coverage Implications (AC7) + +- New seam code: + - `ISettingsStore` is interface-only and absent from Cobertura. + - `ApplicationSettingsStore` reaches 100% through `RecordingApplicationSettings`. + - The IDList, AppQuickFilerSettings, AppStagingFilenames, and AppAutoFileObjects scalar members are covered by tests that inject fakes. + - The default-constructor initializers (`new ApplicationSettingsStore(Properties.Settings.Default)`) execute in any test that uses a parameterless constructor. Construction reads nothing from disk until a property is read. + - No `[ExcludeFromCodeCoverage]` and no human exemption decision are required. +- Changed lines: every rewritten getter and setter line is executed by the converted tests, so changed-line coverage does not drop. +- Parallel validation (AC6): run the coverage task with `scripts/vscode/TaskMaster.cli.runsettings` (`Workers=0`, `ClassLevel`). Run the affected classes repeatedly in that configuration. CI passes no runsettings, so CI does not exercise class-level parallelism. The local parallel run is the evidence. + +## 10. File Change List and Production Estimate (Q7) + +Production C# files (7): + +1. `UtilitiesCS/HelperClasses/Settings/ISettingsStore.cs` (new) +2. `UtilitiesCS/HelperClasses/Settings/ApplicationSettingsStore.cs` (new) +3. `ToDoModel/Data Model/ID/IDList.cs` +4. `TaskMaster/AppGlobals/AppQuickFilerSettings.cs` +5. `TaskMaster/AppGlobals/AppStagingFilenames.cs` +6. `TaskMaster/AppGlobals/AppAutoFileObjects.cs` (scalar properties removed) +7. `TaskMaster/AppGlobals/AppAutoFileObjects.ScalarSettings.cs` (new partial) + +Build files (2): `UtilitiesCS/UtilitiesCS.csproj`, `TaskMaster/TaskMaster.csproj`. The `.csproj` files of ToDoModel, TaskTree, and QuickFiler are unchanged. + +Test files: + +- `UtilitiesCS.Test/Extensions/DfDeedle_COM_Tests.cs` (made `partial`, section removed) +- `UtilitiesCS.Test/Extensions/DfDeedle_COM_Tests.QfcColumnsAsync.cs` (new) +- `UtilitiesCS.Test/HelperClasses/Settings/ApplicationSettingsStoreTests.cs` (new) +- `UtilitiesCS.Test/UtilitiesCS.Test.csproj` +- `ToDoModel.Test/Data Model/ID/IDListGetNextToDoIDTests.cs` +- optionally `ToDoModel.Test/Data Model/ID/IDListMaxLengthPersistenceTests.cs` (new) with `ToDoModel.Test/ToDoModel.Test.csproj` +- `TaskTree.Test/TaskTreeControllerMoveLogicTests.cs` +- `TaskTree.Test/TaskTreeControllerRouteDropTests.cs` +- `TaskMaster.Test/AppGlobals/InMemorySettingsStore.cs` (new) +- `TaskMaster.Test/AppGlobals/AppQuickFilerSettingsTests.cs` +- `TaskMaster.Test/AppGlobals/AppQuickFilerSettingsRemainingPropertiesTests.cs` +- `TaskMaster.Test/AppGlobals/AppStagingFilenamesTests.cs` +- `TaskMaster.Test/AppGlobals/AppAutoFileObjectsCoverageExpansionTests.cs` +- `TaskMaster.Test/Ribbon/RibbonControllerTests.cs` +- `TaskMaster.Test/TaskMaster.Test.csproj` + +Routing note: seven production C# files exceed the direct-path budget. Route through the orchestrator's large path (per `csharp-change-budget-router`). + +## Automation Feasibility + +No human interaction is required. Every step runs from the command line: + +- the format, analyzer, and type-check toolchain +- the MSTest coverage run with `TaskMaster.cli.runsettings` +- the fail-before probes +- the AC4 searches + +The issue's Visual Studio Test Explorer re-run is an optional manual confirmation. The CLI parallel run with the same `Workers=0`/`ClassLevel` settings replaces it as evidence. No coverage-exemption ratification is needed. + +## Pre-Mortem + +- Review checklists: A reviewer may flag the `public` widening of `IDList` (new constructor) and the new public `ISettingsStore`. Mitigation: the plan states that UtilitiesCS and ToDoModel grant no `InternalsVisibleTo` to the consuming assemblies (ToDoModel, TaskMaster, TaskTree.Test), so `public` is the minimum. XML docs state the contract. A reviewer may also flag `[DoNotParallelize]` removal on `RibbonControllerTests` without proof. Mitigation: a plan task reads the whole class and its partial for other statics and records the finding. +- Coverage: New seam members could fall below 90% if the adapter is tested only through Settings.Default, which is not allowed. Mitigation: `RecordingApplicationSettings` test subclass covers `ApplicationSettingsStore` fully. Persistence-branch tests cover both IDList branches. The coverage comparison reads per-file figures from the post-processed Cobertura projection. +- Mirror parity: Not applicable. No `.claude`, `.agents`, `.github/skills`, or bundled mirror file is changed, and the execution-environment brief states no fact this item alters. +- File size: `DfDeedle_COM_Tests.cs` (848 lines) and `AppAutoFileObjects.cs` (853 lines) are already over 500. Mitigation: both are made smaller by moving sections into new partial files. `RibbonControllerTests.cs` (452 lines) and `TaskTreeControllerMoveLogicTests.cs` (414 lines) must stay at or under 500. The plan records physical line counts (not `Measure-Object -Line`) before and after. +- Test isolation: The fail-before probe subscribes to a static `SettingsSaving` event. Mitigation: the handler is thread-attributed, cancels only to prevent disk writes, and is removed in `finally`. Tests create no temp files. No test reads or writes user.config after the fix. The new in-memory fake is per-test-instance. +- CI-only behavior: CI runs vstest without a runsettings file, so class-level parallelism is not exercised there, and a regression could pass CI while failing in Test Explorer. Mitigation: record a local parallel run with `TaskMaster.cli.runsettings` as AC6 evidence. Changes are pure C# with no path or OS assumptions, and the legacy projects need explicit `` entries for the new files, which CI's cold build would otherwise catch as missing types. +- Evidence: The DfDeedle fail-before is not reproducible deterministically, and an absent dossier would block AC5. Mitigation: a `fail-before-exception..md` dossier plus the AC4 before-and-after search outputs under `evidence/regression-testing/`. The probe-based fail-before runs for IDList and the TaskMaster setters are recorded there too. Evidence files carry `Timestamp`, `Command`, `EXIT_CODE`, and `Output Summary`, and no raw TRX or coverage document is committed. + +## 11. Items for the Orchestrator to Confirm + +- [I] The cross-process sharing of the testhost user.config path (§3) is inferred from documented `LocalFileSettingsProvider` behavior and the reported path. It does not change the design. +- [V-web] The `ApplicationSettingsBase` indexer and `Save()` overridability and the `SettingsSaving` cancel semantics were read from the dotnet/runtime source, which mirrors the .NET Framework reference source. Confirm on net481 when the first adapter test runs. diff --git a/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/spec.md b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/spec.md new file mode 100644 index 000000000..4c7ac693d --- /dev/null +++ b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/spec.md @@ -0,0 +1,204 @@ +# Spec: Parallel test failures from wall-clock deadlines and shared user.config (Issue #1005) + +- Issue: #1005 +- Branch: bug/parallel-test-failures-wall-clock-and-shared-user-config-1005 +- Work Mode: full-bug +- Last Updated: 2026-10-22 +- Status: Draft +- Requirement source: `issue.md` `## Acceptance Criteria` (AC1 through AC7, maintainer-authored) +- Research: `research/2026-10-22T15-21-parallel-test-isolation-wall-clock-and-user-config-research.md` (feature-relative) + +> **AC authority.** This item is in `full-bug` work mode. This `spec.md` is the sole authoritative acceptance-criteria source. AC1 through AC7 carry the meaning of the `issue.md` criteria unchanged, made more precise from the research; AC8 is added by the orchestrator. `user-story.md` exists only because the prd-feature stop hook requires it and contains no acceptance criteria. + +## 1. Problem Statement + +Two MSTest tests fail intermittently when the suite runs in parallel in Visual Studio Test Explorer: + +1. `DfDeedle_COM_Tests.AddQfcColumnsAsync_HappyPath_CompletesWithoutThrowing` calls `DfDeedle.AddQfcColumnsAsync(table, folder, token, 0)` without the existing `columnAdder` or `timeProvider` seam. The production retry loop then waits on three real 3000 ms deadlines for a `Task.Run` over Moq-backed COM mocks. Under parallel load the work misses the 9000 ms budget and the method throws `TimeoutException: The column add step timed out after 9000 ms for folder '(unknown folder)'`. +2. `TaskTreeControllerMoveLogicTests.MoveObjectsToChildren_DesyncedRoot_FiresMessageSeam` reaches `IDList.GetNextToDoID`, which writes `Properties.Settings.Default.MaxLengthOfID` and calls `Properties.Settings.Default.Save()`. All settings classes in one testhost share one host-derived `user.config`, so concurrent saves race and one fails with `ConfigurationErrorsException: Failed to save settings: The configuration file has been changed by another program`. + +Both violate the unit-test isolation rules (UT1 independence and determinism; UT4 no mutable global state, no file I/O). Every test must pass when run in parallel. + +## 2. Root Cause + +- **DfDeedle (wall clock).** There is no production seam gap. `AddQfcColumnsAsync` (`UtilitiesCS/Extensions/DfDeedle.QfcColumns.cs`) already accepts `Action? columnAdder` and `TimeProvider? timeProvider`. `TimeoutTask.TimeoutAfter` arms `(timeProvider ?? TimeProvider.System).CreateTimer(...)`, so with no provider the deadline is the real clock. The tests simply do not pass the seam. `FakeTimeProvider` (`Microsoft.Extensions.TimeProvider.Testing`) is already referenced by UtilitiesCS.Test and already used in `DfDeedle_COM_Tests`. +- **Settings persistence (shared file).** `ApplicationSettingsBase.Save()` persists through `LocalFileSettingsProvider` to a `user.config` whose location derives from the host identity, not from the settings class. Every production member that writes a setting and calls `Save()` with no injectable persistence seam therefore makes any test that reaches it write shared machine state. A fresh `IDList` has a cached max length of zero, so its first generated ID always saves. + +The research enumerated every production `Save()` call on an `ApplicationSettingsBase` instance across the repository and traced each to its test callers (research section "Test-to-site reach map"). The sites reached by tests are: + +| Production file | Members reached by tests | +| --- | --- | +| `ToDoModel/Data Model/ID/IDList.cs` | `GetNextToDoID` | +| `TaskMaster/AppGlobals/AppQuickFilerSettings.cs` | `MoveEntireConversation`, `SaveAttachments`, `SavePictures`, `SaveEmailCopy`, `HighConfidenceModeEnabled`, `HighConfidenceThreshold` setters | +| `TaskMaster/AppGlobals/AppStagingFilenames.cs` | `ConditionalReminders`, `CommonWords` setters | +| `TaskMaster/AppGlobals/AppAutoFileObjects.cs` | `LngConvCtPwr`, `Conversation_Weight`, `SmithWatterman_MatchScore`, `SmithWatterman_MismatchScore`, `SmithWatterman_GapPenalty`, `MaxRecents` setters | + +In addition, test code calls `Save()` directly: `AppAutoFileObjectsCoverageExpansionTests.TestCleanup` invokes `defaults.Save()` after every test in that class. + +Production `Save()` sites with no test reach (seven further AppStagingFilenames setters; AppOlObjects ApplyJunkFolderSelections, LoadJunkPotential, LoadJunkCertain, DarkMode) are not in the family and are not changed by this item (section 4). + +## 3. Proposed Fix (Design) + +### D1. DfDeedle tests (test-only) + +- `AddQfcColumnsAsync_HappyPath_CompletesWithoutThrowing`: pass `timeProvider: new FakeTimeProvider()` and never advance it, keeping `columnAdder` null so the production default-adder branch stays exercised. Convert to an `async Task` test that awaits the call (no `GetAwaiter().GetResult()`). Assert completion without exception, and verify the mocked column-add call. +- `AddQfcColumnsAsync_PreCancelledToken_CompletesGracefully`: pass `timeProvider: new FakeTimeProvider()` for uniformity (it matches the AC4 "without its seams" search, although it is not wall-clock bound in practice). +- `UtilitiesCS.Test/Extensions/DfDeedle_COM_Tests.cs` is already above the 500-line cap. Mark the class `partial` and move the `AddQfcColumnsAsync` section (both tests) into new `UtilitiesCS.Test/Extensions/DfDeedle_COM_Tests.QfcColumnsAsync.cs`, so the original file shrinks and does not grow. Add a `` entry in `UtilitiesCS.Test/UtilitiesCS.Test.csproj`. +- No production change to `DfDeedle`. + +### D2. Settings-store seam (UtilitiesCS) + +- New public interface `UtilitiesCS.ISettingsStore` in `UtilitiesCS/HelperClasses/Settings/ISettingsStore.cs`: `object this[string propertyName] { get; set; }` and `void Save();`. XML docs state that `Save` persists every pending value, mirroring `ApplicationSettingsBase`. +- New public sealed adapter `UtilitiesCS.ApplicationSettingsStore : ISettingsStore` in `UtilitiesCS/HelperClasses/Settings/ApplicationSettingsStore.cs`. The constructor takes an `ApplicationSettingsBase` and throws `ArgumentNullException` on null; the indexer forwards to `settings[propertyName]`; `Save()` forwards to `settings.Save()`. +- Both new files get `` entries in `UtilitiesCS/UtilitiesCS.csproj`. +- `public` is the minimum visibility: UtilitiesCS grants no `InternalsVisibleTo` to ToDoModel, TaskMaster, or TaskTree.Test. + +**Invariant.** Every consumer holds a non-null, per-instance `ISettingsStore`. Its production default is `new ApplicationSettingsStore(Properties.Settings.Default)` of that consumer's own assembly, which is behaviorally identical to the current code because generated settings setters are `this[name] = value`. Explicitly injected null is rejected at construction. No static mutable seam is introduced. + +**Accept-to-persist trace (IDList).** A test constructs `new IDList(new List(), store)` with a `Mock` (non-null, accepted; null would throw `ArgumentNullException` at construction). `GetNextToDoID("0")` generates an ID longer than the cached zero length, assigns `store["MaxLengthOfID"] = newLength`, and calls `store.Save()` on the mock. Nothing reaches `ApplicationSettingsBase.Save()`, so no `ConfigurationErrorsException` can be thrown and `user.config` is not written. In production, the default adapter forwards the same assignment and `Save()` to `ToDoModel.Properties.Settings.Default`, which persists exactly as before. AC2 and AC3 pin this trace. + +### D3. IDList (ToDoModel) + +- In `ToDoModel/Data Model/ID/IDList.cs` add `private readonly ISettingsStore _settingsStore = new ApplicationSettingsStore(Properties.Settings.Default);` (a field initializer, so it runs on every existing constructor including the deserializing and Outlook-bound ones). +- Add public constructors `IDList(IEnumerable ids, ISettingsStore settingsStore)` and `IDList(ISettingsStore settingsStore)`, each null-guarding the store. +- In `GetNextToDoID`, replace the two settings lines with an indexer assignment through `nameof(Properties.Settings.MaxLengthOfID)` and `_settingsStore.Save()`. Ordering is preserved: add the ID, persist the max length if it grew, then serialize. +- No change to `IIDList`, `TreeOfToDoItems`, `TaskTreeController`, or `AppToDoObjects`. ToDoModel.csproj is unchanged. + +### D4. TaskMaster settings writers + +- `TaskMaster/AppGlobals/AppQuickFilerSettings.cs`: add the store field, a parameterless constructor (default adapter) and an `AppQuickFilerSettings(ISettingsStore settingsStore)` overload. Getters read through the store with casts; setters write through the store and call `Save()` on it. `ApplicationGlobals` keeps `new AppQuickFilerSettings()` unchanged. +- `TaskMaster/AppGlobals/AppStagingFilenames.cs`: same pattern for all of its settings-backed properties, so the class is internally consistent. Saving setters save through the store; `EmailInfoStagingFile` remains non-persisting. +- `TaskMaster/AppGlobals/AppAutoFileObjects.cs`: add the store field (default adapter) and an `AppAutoFileObjects(IApplicationGlobals parent, ISettingsStore settingsStore)` overload. Route only the six scalar properties through the store; the file-name reads on `_defaults` are untouched. Because the file is already above 500 lines, move the scalar properties into new partial `TaskMaster/AppGlobals/AppAutoFileObjects.ScalarSettings.cs` so the main file shrinks; add a `` entry in `TaskMaster/TaskMaster.csproj`. + +### D5. Test changes + +- New `UtilitiesCS.Test/HelperClasses/Settings/ApplicationSettingsStoreTests.cs` with a nested `RecordingApplicationSettings : ApplicationSettingsBase` that overrides the indexer and `Save()` (dictionary-backed, save counter), so no provider is invoked and no file is touched. Covers null constructor argument, get forwarding, set forwarding, save forwarding. `` in `UtilitiesCS.Test/UtilitiesCS.Test.csproj`. +- `ToDoModel.Test/Data Model/ID/IDListGetNextToDoIDTests.cs` (C# 7.3): replace the `CreateListWithCachedMaxLength` cache-priming workaround with an injected `Mock`, and add persistence-branch tests: first ID on a fresh list sets `MaxLengthOfID` and saves once; a shorter ID does not save; a rollover to a longer ID saves again; a null store throws `ArgumentNullException`. If the file would exceed 500 lines, place the new tests in a new `ToDoModel.Test/Data Model/ID/IDListMaxLengthPersistenceTests.cs` with a `` in `ToDoModel.Test/ToDoModel.Test.csproj`. +- `TaskTree.Test/TaskTreeControllerMoveLogicTests.cs` and `TaskTree.Test/TaskTreeControllerRouteDropTests.cs` (C# 7.3): `MakeController` returns `new IDList(new List(), settingsStore.Object)` from `mockTD.Setup(x => x.IDList)`, where `settingsStore` is a `Mock`. +- New `TaskMaster.Test/AppGlobals/InMemorySettingsStore.cs`: dictionary-backed `ISettingsStore` with a `SaveCount` property, one instance per test. `` in `TaskMaster.Test/TaskMaster.Test.csproj`. +- `TaskMaster.Test/AppGlobals/AppQuickFilerSettingsTests.cs`, `TaskMaster.Test/AppGlobals/AppQuickFilerSettingsRemainingPropertiesTests.cs`, `TaskMaster.Test/AppGlobals/AppStagingFilenamesTests.cs`: construct the system under test with an `InMemorySettingsStore`, assert round-trips against the fake, assert `SaveCount` increments for saving setters and not for `EmailInfoStagingFile`, and remove the `Settings.Default` snapshot and restore. +- `TaskMaster.Test/Ribbon/RibbonControllerTests.cs` (and, only if needed, `TaskMaster.Test/Ribbon/RibbonControllerTests.Engines.cs`): `CreateController` injects `new AppQuickFilerSettings(store)` with a seeded fake; assertions read the fake; the snapshot and restore are removed. +- `TaskMaster.Test/AppGlobals/AppAutoFileObjectsCoverageExpansionTests.cs`: construct with an `InMemorySettingsStore` seeded with the scalar values, assert against the fake, and delete the `TestInitialize`/`TestCleanup` snapshot including the direct `defaults.Save()` call. + +### D6. Removal of class-level `[DoNotParallelize]` (AC8) + +- `AppQuickFilerSettingsTests`: the attribute's only reason is shared `Settings.Default` state, which D5 removes; delete it. +- `RibbonControllerTests`: before removal, read the entire class (both partial files) for other process-global static state (static fields, singletons, `UiThread`/dispatcher statics, log4net configuration, environment variables, other `Settings.Default` members). Record the finding in the feature's other-evidence folder. Remove the attribute only when the check finds no other shared static state; otherwise keep it and record which state requires it. + +## Acceptance Criteria + +The criteria lines below carry no counts, because the research record's numeric derivation block does not use the required label names (see the note under Assumptions); populations are enumerated by name instead. + +- [x] AC1: `DfDeedle_COM_Tests.AddQfcColumnsAsync_HappyPath_CompletesWithoutThrowing` no longer depends on wall-clock time: it passes a `FakeTimeProvider` that is never advanced through the existing `timeProvider` parameter of `AddQfcColumnsAsync` (with `columnAdder` left at the production default or supplied as a completed adder), is an awaited `async Task` test containing no `GetAwaiter().GetResult()`, `Thread.Sleep`, `Task.Delay` or other real wait, and passes deterministically. `AddQfcColumnsAsync_PreCancelledToken_CompletesGracefully` also passes a `FakeTimeProvider`. No production file under UtilitiesCS Extensions changes for this criterion. +- [x] AC2: `IDList.GetNextToDoID` persists `MaxLengthOfID` only through an injected per-instance `ISettingsStore` (indexer assignment followed by `Save()` on the store). Every existing `IDList` constructor defaults the store to an `ApplicationSettingsStore` over `ToDoModel.Properties.Settings.Default`, so production writes and saves the same property under the same condition as before; new public constructors accept a store and throw `ArgumentNullException` for null. ToDoModel.Test unit tests prove: the first ID on a fresh list assigns the new length and saves exactly once; a shorter ID does not save; a longer ID saves again; a null store is rejected. The cache-priming workaround in `IDListGetNextToDoIDTests` is removed. +- [x] AC3: `TaskTreeControllerMoveLogicTests.MoveObjectsToChildren_DesyncedRoot_FiresMessageSeam`, and every other test that reaches a real `IDList.GetNextToDoID` (every test that uses `MakeController` in `TaskTreeControllerMoveLogicTests` and in `TaskTreeControllerRouteDropTests`, plus `IDListGetNextToDoIDTests`), constructs its `IDList` with an injected store, so no test reaches `ApplicationSettingsBase.Save()` through `IDList`. The settings-saving probe described in Test Strategy records zero saves on the test thread for `MoveObjectsToChildren_DesyncedRoot_FiresMessageSeam` after the fix. +- [x] AC4: a search of all test projects for tests that reach a production settings `Save()` or that call `AddQfcColumnsAsync` without its seams is recorded under the feature's regression-testing evidence folder, both before and after the fix. Every match is fixed in this item: the `IDList.GetNextToDoID` site; the `AppQuickFilerSettings` setters `MoveEntireConversation`, `SaveAttachments`, `SavePictures`, `SaveEmailCopy`, `HighConfidenceModeEnabled` and `HighConfidenceThreshold`; the `AppStagingFilenames` setters `ConditionalReminders` and `CommonWords`; the `AppAutoFileObjects` setters `LngConvCtPwr`, `Conversation_Weight`, `SmithWatterman_MatchScore`, `SmithWatterman_MismatchScore`, `SmithWatterman_GapPenalty` and `MaxRecents`; the test-code `defaults.Save()` in `AppAutoFileObjectsCoverageExpansionTests`; and both seamless `AddQfcColumnsAsync` calls in `DfDeedle_COM_Tests`. Each of those production members saves through an injected `ISettingsStore` whose default is the adapter over `TaskMaster.Properties.Settings.Default` (or ToDoModel's for `IDList`), and each reaching test injects an in-memory fake or Moq mock. The after-fix search finds no test that reaches a settings `Save()` and no seamless `AddQfcColumnsAsync` call. +- [x] AC5: each fix is preceded by fail-before evidence recorded under the feature's regression-testing evidence folder: for the `IDList` path and for one representative test in each of `AppQuickFilerSettingsTests`, `AppStagingFilenamesTests` and `AppAutoFileObjectsCoverageExpansionTests`, a run of the settings-saving probe that fails before the production change and passes after it; for the DfDeedle path, where a deterministic failing run is impossible without real timers, a schema-valid fail-before-exception dossier stating why the failing run is impossible, quoting the maintainer's captured exception from `issue.md`, and citing the before-fix AC4 search result as the repeatable demonstration. +- [x] AC6: the full test suite passes in parallel, recorded as a local MSTest run with `scripts/vscode/TaskMaster.cli.runsettings` (class-level parallelism, workers unrestricted), plus repeated parallel runs of `DfDeedle_COM_Tests`, `TaskTreeControllerMoveLogicTests`, `TaskTreeControllerRouteDropTests` and the converted TaskMaster settings test classes. The fix adds no serial worker setting (such as a Workers value of one), no `[DoNotParallelize]`, no retries, no `Thread.Sleep` or `Task.Delay`, and no lengthened timeout, and no test writes or redirects `user.config` or creates a temporary file. +- [x] AC7: the full C# toolchain passes in one clean pass in this order: `dotnet tool run csharpier format .` (verified with `dotnet tool run csharpier check .`), the analyzer `msbuild TaskMaster.sln /t:Rebuild` with `EnableNETAnalyzers` and `EnforceCodeStyleInBuild`, the `msbuild TaskMaster.sln /t:Rebuild` with `TreatWarningsAsErrors` and without a Nullable override, and `scripts/vscode/Invoke-MSTestWithCoverage.ps1`. Changed lines lose no coverage against the merge-base baseline; `ApplicationSettingsStore` and every new or changed seam member in `IDList`, `AppQuickFilerSettings`, `AppStagingFilenames` and the `AppAutoFileObjects` scalar partial reach at least ninety percent line coverage, read from the post-processed Cobertura document and recorded in the committed coverage-comparison evidence (the committed JaCoCo projection carries package-level counters only); no `[ExcludeFromCodeCoverage]` is added. Every created or modified `.cs` file named in the Write Set is at or under the five-hundred-line ceiling, except `DfDeedle_COM_Tests.cs` and `AppAutoFileObjects.cs`, which were already above it at the merge base and must instead be smaller than at the merge base. +- [x] AC8: the class-level `[DoNotParallelize]` attribute is removed from `AppQuickFilerSettingsTests`. For `RibbonControllerTests`, a recorded check of the whole class (both partial files) for process-global static state other than the removed settings snapshot is committed under the feature's other-evidence folder; the attribute is removed when that check finds no other shared static state, and when it finds some, the attribute stays and the record names the state and why it requires serialization. + +## 4. Scope and Non-Goals + +**In scope (decided by the orchestrator):** + +- The production settings `Save()` sites reached by tests, listed in section 2 and in AC4. +- Both seamless `AddQfcColumnsAsync` tests in `DfDeedle_COM_Tests`. +- The test-code `defaults.Save()` in `AppAutoFileObjectsCoverageExpansionTests`. +- The remaining settings-backed properties of `AppStagingFilenames` (converted for internal consistency of the class, although no test reaches their setters). +- Class-level `[DoNotParallelize]` removal on `AppQuickFilerSettingsTests` and, conditionally, `RibbonControllerTests` (AC8). + +**Out of scope (a follow-up issue will be filed through the MCP promotion lifecycle):** + +- Tests that mutate `Settings.Default` in memory without saving: StartupTimingEnabled (ApplicationGlobalsStartupTimingTests, ApplicationGlobalsTests), EventsHooked (AppEventsTests), UseLcppnPredictor (AppAutoFileObjectsFolderPredictorTests), JunkPotential and OlJunkCertain (AppOlObjectsTests), and the PropertyValues injection in AppToDoObjectsTests. +- The per-method `[DoNotParallelize]` in ApplicationGlobalsStartupTimingTests. + +**Other non-goals:** + +- Production settings `Save()` sites that no test reaches: AppOlObjects.JunkFolders.cs (ApplyJunkFolderSelections, LoadJunkPotential, LoadJunkCertain) and AppOlObjects.cs (DarkMode). No change. +- `[DoNotParallelize]` classes whose shared state is unrelated to settings (DfDeedleQfcColumnTimeoutTests, DfDeedleEtlTimeoutTests and other UtilitiesCS.Test classes guarding log4net, UI-thread or store-wrapper statics). +- QfcDatamodel.GetEmailsInViewDfAsync, which calls GetEmailDataInViewAsync without a time provider in production only; its one test throws before any deadline. +- Any change to `IIDList`, `TreeOfToDoItems`, `TaskTreeController`, `AppToDoObjects`, `ApplicationGlobals`, or the runsettings files. + +## 5. Constraints + +- All tests pass in parallel. No `Workers=1`, no `[DoNotParallelize]` added, no retries, no `Thread.Sleep`, `Task.Delay`, or longer timeouts. +- No temporary files in tests; no redirecting `user.config` to a temp path; no custom `SettingsProvider` installed process-wide. +- No new NuGet dependency (`Microsoft.Extensions.TimeProvider.Testing` is already referenced by UtilitiesCS.Test). +- No static mutable seam; the settings store is injected per instance. +- Bugfix workflow: fail-before evidence first (or a schema-valid fail-before-exception dossier where a deterministic failing run is impossible), then the minimal fix. +- MSTest, Moq, FluentAssertions. New seam code at least 90% coverage; changed lines lose no coverage. +- Every created or modified `.cs` file at or under 500 physical lines. Files already above 500 (`DfDeedle_COM_Tests.cs`, `AppAutoFileObjects.cs`) are split into partials and shrink; they are not grown. `.csproj` and Markdown files are exempt per `.claude/rules/general-code-change.md`. +- Legacy non-SDK projects: every new `.cs` file needs a `` entry. TaskTree.Test, ToDoModel.Test and QuickFiler.Test are C# 7.3 (no nullable annotations, target-typed `new()`, `is not`, switch expressions or `using` declarations). UtilitiesCS is C# 12 with per-file `#nullable enable`. +- C# toolchain per CLAUDE.md, in order: `dotnet tool run csharpier format .`; `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true`; `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true` (no `/p:Nullable=enable`); `scripts/vscode/Invoke-MSTestWithCoverage.ps1`. +- TaskMaster CI checks that must pass on the PR: actionlint, build-analyzers, build-nullable, format-check, hygiene, mstest-coverage, pester. (The execution-environment skill's CI section describes upstream drm-copilot, not this repository.) +- Committed test evidence is projections only (CLAUDE.md "Committed Test Evidence Format"); no raw TRX or Cobertura document is committed. +- Evidence lives under the feature folder's `evidence//` per the evidence-and-timestamp conventions. + +## 6. Write Set + +Production (C#): + +- `UtilitiesCS/HelperClasses/Settings/ISettingsStore.cs` (new) +- `UtilitiesCS/HelperClasses/Settings/ApplicationSettingsStore.cs` (new) +- `ToDoModel/Data Model/ID/IDList.cs` +- `TaskMaster/AppGlobals/AppQuickFilerSettings.cs` +- `TaskMaster/AppGlobals/AppStagingFilenames.cs` +- `TaskMaster/AppGlobals/AppAutoFileObjects.cs` +- `TaskMaster/AppGlobals/AppAutoFileObjects.ScalarSettings.cs` (new partial) + +Build files: + +- `UtilitiesCS/UtilitiesCS.csproj` +- `TaskMaster/TaskMaster.csproj` + +Tests: + +- `UtilitiesCS.Test/Extensions/DfDeedle_COM_Tests.cs` +- `UtilitiesCS.Test/Extensions/DfDeedle_COM_Tests.QfcColumnsAsync.cs` (new) +- `UtilitiesCS.Test/HelperClasses/Settings/ApplicationSettingsStoreTests.cs` (new) +- `UtilitiesCS.Test/UtilitiesCS.Test.csproj` +- `ToDoModel.Test/Data Model/ID/IDListGetNextToDoIDTests.cs` +- `ToDoModel.Test/Data Model/ID/IDListMaxLengthPersistenceTests.cs` (new, only if needed for the size ceiling) and `ToDoModel.Test/ToDoModel.Test.csproj` +- `TaskTree.Test/TaskTreeControllerMoveLogicTests.cs` +- `TaskTree.Test/TaskTreeControllerRouteDropTests.cs` +- `TaskMaster.Test/AppGlobals/InMemorySettingsStore.cs` (new) +- `TaskMaster.Test/AppGlobals/SettingsSaveProbe.cs` (new; the shared thread-attributed settings-saving probe used by the three TaskMaster.Test probe tests, added by the plan's Decision D-A) +- `TaskMaster.Test/AppGlobals/AppQuickFilerSettingsTests.cs` +- `TaskMaster.Test/AppGlobals/AppQuickFilerSettingsRemainingPropertiesTests.cs` +- `TaskMaster.Test/AppGlobals/AppStagingFilenamesTests.cs` +- `TaskMaster.Test/AppGlobals/AppAutoFileObjectsCoverageExpansionTests.cs` +- `TaskMaster.Test/Ribbon/RibbonControllerTests.cs` +- `TaskMaster.Test/Ribbon/RibbonControllerTests.Engines.cs` (only if the AC8 check or the settings conversion requires it) +- `TaskMaster.Test/TaskMaster.Test.csproj` + +Routing: seven production C# files exceed the direct-path budget; this item runs on the orchestrated large path. + +## 7. Test Strategy + +**Settings-saving probe (fail-before for AC3, AC5).** Before any production change, add to `MoveObjectsToChildren_DesyncedRoot_FiresMessageSeam` a probe that resolves `ToDoModel.Properties.Settings.Default` by reflection (`typeof(IDList).Assembly.GetType("ToDoModel.Properties.Settings")`, public static `Default`, cast to `ApplicationSettingsBase`), subscribes a `SettingsSaving` handler that records only when `Environment.CurrentManagedThreadId` equals the test thread's ID and sets `e.Cancel = true` (so the fail-before run writes nothing to `user.config`), unsubscribes in try-finally, and asserts no save was recorded. Before the fix it fails with one recorded save; after `MakeController` injects the mock store it passes, and the mock verifies `Save()` once. Apply the same probe through `TaskMaster.Properties.Settings.Default.SettingsSaving` (visible to TaskMaster.Test through `InternalsVisibleTo`) to one representative test in each of `AppQuickFilerSettingsTests`, `AppStagingFilenamesTests` and `AppAutoFileObjectsCoverageExpansionTests`. The probe is thread-attributed and always removed, so concurrent classes are unaffected. + +**DfDeedle (AC1, AC5).** The original failure requires real elapsed time under load and cannot be reproduced deterministically without timers. Record a fail-before-exception dossier (`WhyFailingRunImpossible`) and use the AC4 search (seamless calls present before, absent after) as the repeatable demonstration. + +**Seam tests (AC2, AC7).** `ApplicationSettingsStoreTests` through `RecordingApplicationSettings`; IDList persistence-branch tests; converted TaskMaster tests asserting against `InMemorySettingsStore` values and `SaveCount`. + +**Parallel validation (AC6).** CI passes no runsettings, so CI does not exercise class-level parallelism. The local run of `scripts/vscode/Invoke-MSTestWithCoverage.ps1` with `scripts/vscode/TaskMaster.cli.runsettings` (`Workers=0`, `Scope=ClassLevel`) plus repeated runs of the affected classes is the evidence. The maintainer's Visual Studio Test Explorer re-run is an optional manual confirmation. + +**Evidence locations** (feature-relative; every artifact carries `Timestamp`, `Command`, `EXIT_CODE`, `Output Summary`): + +- `evidence/regression-testing/` — AC4 before and after search results; settings-saving probe fail-before and pass-after test-result summaries; DfDeedle fail-before-exception dossier. +- `evidence/other/` — RibbonControllerTests static-state check record (AC8). +- `evidence/baseline/` — merge-base coverage projection and summary. +- `evidence/qa-gates/` — final toolchain pass, parallel run test-result summaries, post-change coverage projection and summary, coverage comparison. + +## 8. Assumptions and Open Points + +- The cross-process sharing of the testhost `user.config` path is inferred from documented `LocalFileSettingsProvider` behavior and the reported path; it does not change the design. +- `ApplicationSettingsBase` indexer and `Save()` overridability and `SettingsSaving` cancel semantics were read from the dotnet/runtime source; confirm on net481 when the first adapter test runs. If `Save()` is not overridable on net481, the fallback is to test the adapter through a `SettingsSaving` handler that cancels the save, with the same zero-disk-write property. +- Numeric assertions: the research's `## Numeric Derivation Evidence` block uses the labels `Primary Search Strategy` and `Cross-check Search Strategy` rather than the required `Primary Search Strategy or Query Expression` and `Cross-check Search Strategy or Query Expression`, so it does not satisfy this spec's numeric-evidence contract. The acceptance criteria therefore assert no counts and enumerate the site population by name. The research's figures (fifteen reached sites, twenty-six total, two seamless calls) remain informational in this body only. + +## 9. Risks + +- Public surface widening (`ISettingsStore`, `ApplicationSettingsStore`, new `IDList` constructors): `public` is the minimum because the consuming assemblies have no `InternalsVisibleTo` grant from UtilitiesCS or ToDoModel. +- A regression could pass CI while failing in Test Explorer, because CI runs no class-level parallelism; mitigated by the recorded local parallel run. +- `RibbonControllerTests` may hold other shared static state; mitigated by the AC8 recorded check. diff --git a/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/user-story.md b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/user-story.md new file mode 100644 index 000000000..f195250ee --- /dev/null +++ b/docs/features/active/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config-1005/user-story.md @@ -0,0 +1,11 @@ +# User Story: Parallel test suite runs reliably (Issue #1005) + +> This item is in `full-bug` work mode. `spec.md` in this folder is the sole authoritative acceptance-criteria source. This file exists only because the prd-feature stop hook requires it; it is narrative context, contains no acceptance criteria, and must not be used for check-off. + +## Story + +As the maintainer running the full TaskMaster MSTest suite in parallel in Visual Studio Test Explorer, I want every test to pass regardless of execution order and concurrency, so that a red result always indicates a real defect rather than a wall-clock timeout or a race on the shared `user.config` file. + +- Given the suite runs with class-level parallelism, +- When `DfDeedle_COM_Tests` and the TaskTree controller move and route-drop tests run alongside the TaskMaster settings tests, +- Then no test waits on a real clock deadline, no test saves application settings to disk, and the run is green on every repetition. diff --git a/docs/features/potential/promoted/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config.md b/docs/features/potential/promoted/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config.md new file mode 100644 index 000000000..f9d017343 --- /dev/null +++ b/docs/features/potential/promoted/2026-10-22-parallel-test-failures-wall-clock-and-shared-user-config.md @@ -0,0 +1,65 @@ +# parallel-test-failures-wall-clock-and-shared-user-config (Issue #1005) + +- Date captured: 2026-10-22 +- Author: Dan Moisan +- Status: Promoted -> docs/features/active/parallel-test-failures-wall-clock-and-shared-user-config/ (Issue #1005) + +> Automation note: Keep the section headings below unchanged; the promotion tooling maps each of them into the GitHub bug issue template. + +- Issue: #1005 +- Issue URL: https://github.com/drmoisan/TaskMaster/issues/1005 +- Last Updated: 2026-10-22 +## Summary + +Two MSTest tests fail intermittently when the suite runs in parallel in Visual Studio Test Explorer. `DfDeedle_COM_Tests.AddQfcColumnsAsync_HappyPath_CompletesWithoutThrowing` depends on wall-clock deadlines, and `TaskTreeControllerMoveLogicTests.MoveObjectsToChildren_DesyncedRoot_FiresMessageSeam` writes the shared on-disk `user.config` through `Properties.Settings.Default.Save()`. Both violate the unit-test isolation rules; every test must pass when run in parallel. + +## Environment + +- OS/version: Windows 11, Visual Studio 18 Test Explorer (parallel execution) +- Python version: not applicable (C# .NET Framework 4.8, MSTest, Moq, FluentAssertions) +- Command/flags used: Visual Studio Test Explorer run of the full solution +- Data source or fixture: UtilitiesCS.Test `DfDeedle_COM_Tests`; TaskTree.Test `TaskTreeControllerMoveLogicTests` + +## Steps to Reproduce + +1. Run the full test suite in parallel in Visual Studio Test Explorer. +2. Observe `AddQfcColumnsAsync_HappyPath_CompletesWithoutThrowing` fail after 9.5 s with `TimeoutException: The column add step timed out after 9000 ms for folder '(unknown folder)'` thrown from `DfDeedle.AddQfcColumnsAsync`. +3. Observe `MoveObjectsToChildren_DesyncedRoot_FiresMessageSeam` fail with `ConfigurationErrorsException: Failed to save settings: The configuration file has been changed by another program` from `IDList.GetNextToDoID` -> `ApplicationSettingsBase.Save()`, reached through `TreeOfToDoItems.AddChild` and `TaskTreeController.MoveObjectsToChildren`. + +## Expected Behavior + +Both tests pass deterministically in any order and under parallel execution, with no dependence on wall-clock timing and no write to shared on-disk configuration. + +## Actual Behavior + +1. The test calls `DfDeedle.AddQfcColumnsAsync(table, folder, token, 0)` without the `timeProvider` (or `columnAdder`) seam, so the production retry loop waits on three real 3000 ms deadlines for a `Task.Run` over Moq-backed COM mocks; under parallel load the work misses the budget and the method throws. +2. `IDList.GetNextToDoID` persists `MaxLengthOfID` through `Properties.Settings.Default.Save()`, so concurrent tests race on one shared `user.config` file and one save fails. + +## Logs / Screenshots + +- [x] Attached minimal logs or screenshot +- Snippet: `System.TimeoutException: The column add step timed out after 9000 ms for folder '(unknown folder)'` and `System.Configuration.ConfigurationErrorsException: Failed to save settings: The configuration file has been changed by another program. (...\TestSourceHost__...\user.config)` + +## Impact / Severity + +- [ ] Blocker +- [x] High +- [ ] Medium +- [ ] Low + +## Suspected Cause / Notes + +- `UtilitiesCS/Extensions/DfDeedle.QfcColumns.cs` `AddQfcColumnsAsync` already accepts optional `columnAdder` and `timeProvider` seams; the happy-path test does not use them, so it is bound to the wall clock. +- `ToDoModel/Data Model/ID/IDList.cs` `GetNextToDoID` writes `Properties.Settings.Default` and calls `Save()` with no injectable persistence seam, so any test that allocates an ID writes shared machine state. +- Other tests in the repository may share either pattern; the fix should search for them. + +## Proposed Fix / Validation Ideas + +- [ ] Unit coverage areas: make the DfDeedle happy-path test deterministic through the existing seams (a synchronous or pre-completed column adder and a fake time provider); add an injectable settings-persistence seam to `IDList` so tests never write `user.config`, with a production default that preserves current behavior +- [ ] Integration scenario to retest: full MSTest run in parallel (CI shape and Test Explorer shape), plus repeated runs of the two affected classes +- [ ] Manual verification notes: re-run the suite in Visual Studio Test Explorer with parallel execution enabled + +## Next Step + +- [ ] Promote to GitHub issue (bug-report template) +- [ ] Move to active fix folder / branch diff --git a/docs/features/potential/promoted/2026-10-22-tests-mutate-shared-settings-default-in-memory.md b/docs/features/potential/promoted/2026-10-22-tests-mutate-shared-settings-default-in-memory.md new file mode 100644 index 000000000..84300c7f4 --- /dev/null +++ b/docs/features/potential/promoted/2026-10-22-tests-mutate-shared-settings-default-in-memory.md @@ -0,0 +1,67 @@ +# tests-mutate-shared-settings-default-in-memory (Issue #1006) + +- Date captured: 2026-10-22 +- Author: Dan Moisan +- Status: Promoted -> docs/features/active/tests-mutate-shared-settings-default-in-memory/ (Issue #1006) + +> Automation note: Keep the section headings below unchanged; the promotion tooling maps each of them into the GitHub bug issue template. + +- Issue: #1006 +- Issue URL: https://github.com/drmoisan/TaskMaster/issues/1006 +- Last Updated: 2026-10-22 +## Summary + +Several TaskMaster.Test classes assign values to the process-global `TaskMaster.Properties.Settings.Default` in memory and restore them afterwards. Under class-level parallel execution, two such tests can observe each other's values, and `ApplicationGlobalsStartupTimingTests` serializes with per-method `[DoNotParallelize]` to cope. All tests should run in parallel without shared static settings state. Found during issue #1005, which fixes the tests that call `Settings.Default.Save()`. + +## Environment + +- OS/version: Windows 11, Visual Studio Test Explorer or vstest with `Workers=0` / `ClassLevel` +- Python version: not applicable (C# .NET Framework 4.8, MSTest, Moq, FluentAssertions) +- Command/flags used: full MSTest run in parallel +- Data source or fixture: TaskMaster.Test AppGlobals tests + +## Steps to Reproduce + +1. Run TaskMaster.Test in parallel at class level. +2. Observe that tests mutating the same `Settings.Default` property can interleave (latent; not yet observed as a failure). +3. Observe `[DoNotParallelize]` on `ApplicationGlobalsStartupTimingTests` methods, which exists only because of the shared setting. + +## Expected Behavior + +Each test reads settings through an injected per-instance store, so no test mutates `Settings.Default` and no `[DoNotParallelize]` is needed for settings. + +## Actual Behavior + +Tests mutate the shared settings singleton in memory: +- `ApplicationGlobalsStartupTimingTests` and `ApplicationGlobalsTests`: `StartupTimingEnabled` +- `AppEventsTests`: `EventsHooked` +- `AppAutoFileObjectsFolderPredictorTests`: `UseLcppnPredictor` +- `AppOlObjectsTests`: `JunkPotential`, `OlJunkCertain` +- `AppToDoObjectsTests`: property values injected into `Settings.Default` + +## Logs / Screenshots + +- [ ] Attached minimal logs or screenshot +- Snippet: not applicable (latent isolation defect identified by inventory in issue #1005 research) + +## Impact / Severity + +- [ ] Blocker +- [ ] High +- [x] Medium +- [ ] Low + +## Suspected Cause / Notes + +The production readers of these settings (ApplicationGlobals startup timing, AppEvents, AppAutoFileObjects predictor selection, AppOlObjects junk folders, AppToDoObjects) read `Properties.Settings.Default` directly with no injectable seam. Issue #1005 introduces `UtilitiesCS` `ISettingsStore` with an `ApplicationSettingsStore` adapter; the same seam can be applied to these readers. + +## Proposed Fix / Validation Ideas + +- [ ] Unit coverage areas: inject `ISettingsStore` per instance into each reader; convert the listed tests to an in-memory store; remove the settings-related `[DoNotParallelize]` attributes +- [ ] Integration scenario to retest: full MSTest run with `Workers=0` / `ClassLevel`, repeated for the affected classes +- [ ] Manual verification notes: Visual Studio Test Explorer parallel run + +## Next Step + +- [ ] Promote to GitHub issue (bug-report template) +- [ ] Move to active fix folder / branch