UAT: _ThreadLocalStream.flush() raises ValueError when original stream is closed #4054

Open
opened 2026-04-06 09:38:07 +00:00 by freemo · 0 comments
Owner

Metadata

  • Branch: fix/thread-local-stream-flush-closed-stream
  • Commit Message: fix(validation_pipeline): guard _ThreadLocalStream.flush() against closed stream
  • Milestone: (none — backlog)
  • Parent Epic: #362

Bug Description

The _ThreadLocalStream.flush() method in src/cleveragents/application/services/validation_pipeline.py (lines 77–81) calls self._original.flush() unconditionally, without guarding against the case where the original stream has been closed. This causes ValueError: I/O operation on closed file to be raised when Python's garbage collector finalizes the _ThreadLocalStream object after the original stream has been closed.

Affected Location

  • File: src/cleveragents/application/services/validation_pipeline.py
  • Class: _ThreadLocalStream (lines 48–94)
  • Method: flush() (lines 77–81)

Current Code

def flush(self) -> None:
    buf: io.StringIO | None = getattr(self._local, "buffer", None)
    if buf is not None:
        buf.flush()
    self._original.flush()  # <-- Raises ValueError if stream is closed

Expected Behaviour

The flush() method should guard against closed streams by catching ValueError and OSError:

def flush(self) -> None:
    buf: io.StringIO | None = getattr(self._local, "buffer", None)
    if buf is not None:
        buf.flush()
    try:
        self._original.flush()
    except (ValueError, OSError):
        pass  # Stream may be closed during GC finalisation

Steps to Reproduce

  1. Run any test that uses ValidationPipeline.run() with pytest.
  2. Observe PytestUnraisableExceptionWarning: Exception ignored in: <_ThreadLocalStream object> with ValueError: I/O operation on closed file.

Impact

  • Causes PytestUnraisableExceptionWarning in test environments.
  • Could cause unhandled exceptions in any context where the original stream is closed before the _ThreadLocalStream is garbage collected.
  • Violates the contract of io.TextIOBase.flush() which should be safe to call even when the stream is closed.

Subtasks

  • Write a failing Behave scenario that reproduces the ValueError raised by _ThreadLocalStream.flush() when the original stream is closed
  • Add the try/except (ValueError, OSError): pass guard to _ThreadLocalStream.flush() in validation_pipeline.py
  • Verify the Behave scenario now passes
  • Run nox -e typecheck to confirm no Pyright regressions
  • Run nox -e coverage_report to confirm coverage remains ≥ 97%
  • Run full nox suite to confirm all quality gates pass

Definition of Done

  • A Behave scenario exists that demonstrates the bug (stream closed before GC) and passes after the fix
  • _ThreadLocalStream.flush() no longer raises ValueError or OSError when the original stream is closed
  • All nox stages pass
  • Coverage >= 97%
  • PR merged and this issue closed

Backlog note: This issue was discovered during autonomous operation
on milestone v3.7.0. It does not block milestone completion and has been
placed in the backlog for human review and future milestone assignment.


Automated by CleverAgents Bot
Supervisor: Acting on behalf of: UAT Testing | Agent: ca-new-issue-creator

## Metadata - **Branch**: `fix/thread-local-stream-flush-closed-stream` - **Commit Message**: `fix(validation_pipeline): guard _ThreadLocalStream.flush() against closed stream` - **Milestone**: *(none — backlog)* - **Parent Epic**: #362 ## Bug Description The `_ThreadLocalStream.flush()` method in `src/cleveragents/application/services/validation_pipeline.py` (lines 77–81) calls `self._original.flush()` unconditionally, without guarding against the case where the original stream has been closed. This causes `ValueError: I/O operation on closed file` to be raised when Python's garbage collector finalizes the `_ThreadLocalStream` object after the original stream has been closed. ### Affected Location - **File**: `src/cleveragents/application/services/validation_pipeline.py` - **Class**: `_ThreadLocalStream` (lines 48–94) - **Method**: `flush()` (lines 77–81) ### Current Code ```python def flush(self) -> None: buf: io.StringIO | None = getattr(self._local, "buffer", None) if buf is not None: buf.flush() self._original.flush() # <-- Raises ValueError if stream is closed ``` ### Expected Behaviour The `flush()` method should guard against closed streams by catching `ValueError` and `OSError`: ```python def flush(self) -> None: buf: io.StringIO | None = getattr(self._local, "buffer", None) if buf is not None: buf.flush() try: self._original.flush() except (ValueError, OSError): pass # Stream may be closed during GC finalisation ``` ### Steps to Reproduce 1. Run any test that uses `ValidationPipeline.run()` with pytest. 2. Observe `PytestUnraisableExceptionWarning: Exception ignored in: <_ThreadLocalStream object>` with `ValueError: I/O operation on closed file`. ### Impact - Causes `PytestUnraisableExceptionWarning` in test environments. - Could cause unhandled exceptions in any context where the original stream is closed before the `_ThreadLocalStream` is garbage collected. - Violates the contract of `io.TextIOBase.flush()` which should be safe to call even when the stream is closed. ## Subtasks - [ ] Write a failing Behave scenario that reproduces the `ValueError` raised by `_ThreadLocalStream.flush()` when the original stream is closed - [ ] Add the `try/except (ValueError, OSError): pass` guard to `_ThreadLocalStream.flush()` in `validation_pipeline.py` - [ ] Verify the Behave scenario now passes - [ ] Run `nox -e typecheck` to confirm no Pyright regressions - [ ] Run `nox -e coverage_report` to confirm coverage remains ≥ 97% - [ ] Run full `nox` suite to confirm all quality gates pass ## Definition of Done - [ ] A Behave scenario exists that demonstrates the bug (stream closed before GC) and passes after the fix - [ ] `_ThreadLocalStream.flush()` no longer raises `ValueError` or `OSError` when the original stream is closed - [ ] All nox stages pass - [ ] Coverage >= 97% - [ ] PR merged and this issue closed > **Backlog note:** This issue was discovered during autonomous operation > on milestone v3.7.0. It does not block milestone completion and has been > placed in the backlog for human review and future milestone assignment. --- **Automated by CleverAgents Bot** Supervisor: Acting on behalf of: UAT Testing | Agent: ca-new-issue-creator
HAL9000 added this to the v3.5.0 milestone 2026-04-09 03:11:31 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Blocks
#362 Epic: Security & Safety Hardening
cleveragents/cleveragents-core
Reference
cleveragents/cleveragents-core#4054
No description provided.