Skip to content

fix(UtilitiesCS): treat DirectoryNotFoundException as terminal in WriteTextFileAsync (#707) - #756

Merged
drmoisan merged 8 commits into
mainfrom
bug/narrow-fileio2-retryable-exception-set-707
Sep 3, 2026
Merged

fix(UtilitiesCS): treat DirectoryNotFoundException as terminal in WriteTextFileAsync (#707)#756
drmoisan merged 8 commits into
mainfrom
bug/narrow-fileio2-retryable-exception-set-707

Conversation

@drmoisan

@drmoisan drmoisan commented Sep 3, 2026

Copy link
Copy Markdown
Owner

Suggested title

fix(UtilitiesCS): treat DirectoryNotFoundException as terminal in WriteTextFileAsync

Summary

  • FileIO2.WriteTextFileAsync retried every IOException, including DirectoryNotFoundException, so a missing target folder consumed the full 100-attempt / 100 ms retry budget even though no retry could ever succeed.
  • Added a catch (DirectoryNotFoundException ex) block immediately before the existing catch (IOException ex) block: it logs via logger.Error and returns false on the first attempt, without incrementing the retry counter or invoking the delay delegate.
  • Added a regression test, WriteTextFileAsync_WhenDirectoryDoesNotExist_ShouldReturnFalseWithoutRetrying, confirmed to fail before the fix (100 writer-factory invocations) and pass after (1 invocation, 0 delay invocations).
  • All other retry behavior for genuine transient IOException cases (sharing violations, mid-write failures) is unchanged.
  • Additive only: no signature change to either WriteTextFileAsync overload, no caller-side changes.

Why

DirectoryNotFoundException derives from IOException, so the pre-existing single catch (IOException ex) block treated it as a transient, retryable condition. A missing directory is a structural failure that cannot be corrected by retrying, so the previous behavior wasted the full retry window (up to 100 attempts at 100 ms delay) before returning false. Severity is Low: the one production caller reachable through this path already guards against a missing folder (QuickFiler/Controllers/QfcHomeController.Metrics.cs), but the wasted retry window is still an unnecessary delay whenever the guard is bypassed or the folder is removed mid-run.

What Changed

Core fix

  • UtilitiesCS/To Depricate/FileIO2.cs: inserted catch (DirectoryNotFoundException ex) ahead of the existing catch (IOException ex) block in the internal seam overload of WriteTextFileAsync (pre-change line 126). The new block mirrors the existing opened-terminal-failure shape: logger.Error(...) then return false;, with no call to Interlocked.Increment(ref attempts) or delayAsync.

Tests

  • UtilitiesCS.Test/HelperClasses/FileIO2_Tests.cs: added WriteTextFileAsync_WhenDirectoryDoesNotExist_ShouldReturnFalseWithoutRetrying, following the existing writerFactory/delay seam pattern used by sibling tests. Asserts writer-factory call count 1, delay call count 0, and result false.

Docs/evidence

  • Feature folder docs/features/active/2026-08-31-narrow-fileio2-retryable-exception-set-707/ carries issue.md, spec.md, the atomic plan, and full baseline/regression-testing/qa-gates evidence for every toolchain and coverage gate, plus policy-audit, code-review, and feature-audit artifacts from an independent review pass.

Architecture / How It Fits Together

WriteTextFileAsync's retry loop distinguishes two failure classes inside a single try: failures before the writer opens (retryable, bounded by attempts >= 100) and failures after the writer opens (opened == true, terminal — a partial write cannot be safely retried). DirectoryNotFoundException can only be raised by the StreamWriter constructor (createWriter), i.e. before opened is set, so the new catch block sits alongside the pre-open branch as a second terminal exit, distinguished from the generic IOException branch purely by exception type. C# requires the more-derived catch clause (DirectoryNotFoundException) to appear before the less-derived one (IOException) in the same try; this ordering is enforced by the compiler (CS0160), not by convention.

Verification

Completed (from evidence in docs/features/active/2026-08-31-narrow-fileio2-retryable-exception-set-707/evidence/):

  • dotnet tool run csharpier check . — EXIT_CODE 0 (clean, no diffs)
  • msbuild TaskMaster.sln /t:Rebuild ... /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true — EXIT_CODE 0, 0 warnings / 0 errors
  • msbuild TaskMaster.sln /t:Rebuild ... /p:TreatWarningsAsErrors=true — EXIT_CODE 0, 0 warnings / 0 errors (baseline and post-fix)
  • Regression test pre-fix: WriteTextFileAsync_WhenDirectoryDoesNotExist_ShouldReturnFalseWithoutRetrying failed as expected (observed 100 factory invocations vs. expected 1)
  • Regression test post-fix: same test passed; full FileIO2_Tests suite 12/12 passed, including all 11 pre-existing tests unmodified
  • New-code coverage on the FileIO2.cs delta: 14/14 lines (100%), against the >=90% new-code floor
  • Full UtilitiesCS.Test suite: 4769/4786 passed; the 17 failures are pre-existing Deedle/F# dotnet-coverage instrumentation incompatibilities, identical in the baseline and post-change runs (unrelated to this fix)
  • Independent policy-audit / code-review / feature-audit pass: 0 blocking findings

Recommended (for reviewers who want to reproduce locally):

  • dotnet tool run csharpier check .
  • msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true
  • vstest.console.exe UtilitiesCS.Test.dll /InIsolation "/TestCaseFilter:FullyQualifiedName~FileIO2_Tests"

Backward Compatibility / Migration Notes

None. No signature change to either WriteTextFileAsync overload. Both production callers (TaskMaster/AppGlobals/AppOlObjects.cs, QuickFiler/Controllers/QfcHomeController.Metrics.cs) already consume Task<bool> and already handle a false result; neither requires a code change.

Risks and Mitigations

  • Behavior change for missing-directory cases: callers that relied on the previous (unintentional) 100-attempt delay before a false result will now receive false immediately. Mitigation: both existing callers already treat false as a terminal failure signal, so no caller-side logic depends on the delay.
  • Coverage measurement workaround: the repository's coverage wrapper script (scripts/vscode/Invoke-MSTestWithCoverage.ps1) excludes assemblies under a .claude path segment (tracked separately as issue Bug: coverage-assembly-discovery-excludes-own-worktree-root #752), which this agent's worktree is rooted under. Coverage was captured instead via dotnet-coverage collect wrapping vstest.console.exe directly, producing equivalent Cobertura output. This is a toolchain substitution with unchanged acceptance conditions, not a reduction in verification.

Review Guide

  1. UtilitiesCS/To Depricate/FileIO2.cs — the one-block fix (start here).
  2. UtilitiesCS.Test/HelperClasses/FileIO2_Tests.cs — the new regression test, adjacent to the existing sibling test it mirrors.
  3. docs/features/active/2026-08-31-narrow-fileio2-retryable-exception-set-707/spec.md — Acceptance Criteria (AC1-AC9, all checked) and Proposed Fix section.
  4. docs/features/active/2026-08-31-narrow-fileio2-retryable-exception-set-707/feature-audit.2026-09-03T08-32.md — independent review verdict.
  5. Evidence under docs/features/active/2026-08-31-narrow-fileio2-retryable-exception-set-707/evidence/ — supporting detail only; not required reading for a first pass.

Follow-ups

None identified. PathTooLongException remains explicitly out of scope for this fix per spec.md Scope & Non-Goals.

GitHub Auto-close

drmoisan and others added 8 commits September 2, 2026 09:48
Preparation-mode delivery for issue #707: promotion verification, research,
spec.md, and an atomic plan cleared through five independent preflight
rounds. Narrows the DirectoryNotFoundException retry set in
UtilitiesCS/To Depricate/FileIO2.cs (implementation deferred to
atomic execution by parallel-orchestrator).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LTjXvNFHVh7Fo7kYGgWsx2
Downstream parallel blast-radius harvesting treats any backtick-delimited,
whitespace-free, repository-relative path token in spec.md/plan.md as a
write target regardless of surrounding polarity. Three defects followed:

- scope-exclusion paths (.claude/**, .codex/**, .agents/**,
  config/blast-radius.json, config/orchestration-routing.json) were quoted
  in backticks, so the harvester believed this item writes those shared
  governance surfaces
- QuickFiler/Controllers/QfcHomeController.Metrics.cs was backticked in a
  do-not-touch statement, falsely coupling this item to issue #645, which
  owns that file
- the FileIO2 production path has a space in its directory name
  (UtilitiesCS/To Depricate/FileIO2.cs), which the harvester could not
  parse as a single token, so the real production file was missing from
  the radius entirely

This is a semantics-preserving text-presentation revision only: no task,
acceptance criterion, command, or evidence path changed.

- Add a "## Write Set" section to spec.md naming the two files this
  plan's diff touches, flagging the space in the FileIO2 directory.
- Rewrite every exclusion/do-not-touch/context-reference/verified-no-change
  path in spec.md and the plan as unbackticked prose, preserving each
  sentence's meaning exactly.
- Re-validated via mcp__drm-copilot__validate_orchestration_artifacts
  (ok:true) and re-cleared atomic-executor preflight
  (PREFLIGHT: ALL CLEAR, CONVERGENCE: NO FURTHER ROUNDS EXPECTED).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LTjXvNFHVh7Fo7kYGgWsx2
…teTextFileAsync (#707)

Insert a catch (DirectoryNotFoundException ex) block ahead of the existing
catch (IOException ex) in FileIO2.WriteTextFileAsync's internal seam
overload, so a missing target directory fails fast (log + return false)
instead of consuming the full 100-attempt retry budget. Add a regression
test that fails against the pre-fix source (100 factory invocations) and
passes post-fix (1 factory invocation, 0 delay invocations).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…AC/plan checkoffs

Records the P7-T1/P7-T2 evidence artifacts, the Rollout & Follow-up outcome
note in spec.md, and the final plan checklist state for issue #707.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Adds policy-audit, code-review, and feature-audit artifacts for the
#707 bugfix branch. Verdict: PASS, 0 blocking findings across all
three artifacts; all 9 spec.md acceptance criteria independently
verified.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@drmoisan
drmoisan merged commit 35583f7 into main Sep 3, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: narrow-fileio2-retryable-exception-set

1 participant