Skip to content

[aw][test audit] _start_input_reader_thread lacks direct unit tests for daemon flag and KeyboardInterrupt path #1056

@microsasa

Description

@microsasa

Root Cause

_start_input_reader_thread in src/copilot_usage/cli.py (lines 184–212) is exercised only through integration-level tests that monkeypatch builtins.input and drive the full _interactive_loop. Three invariants are never directly asserted:

  1. daemon=True flag — the thread is created with daemon=True to ensure it cannot prevent process exit. This is the exact property documented in CODING_GUIDELINES.md as the root cause of the CI hang fixed in issue [aw][code health] Interactive mode auto-refresh silently broken on Windows (select.select stdin incompatibility) #1012 / PR fix: cross-platform non-blocking stdin for interactive mode auto-refresh (#1012) #1015. If daemon=True were accidentally removed, no existing test would catch it — the integration tests would simply hang until a CI timeout killed them.

  2. KeyboardInterrupt_FALLBACK_EOF path — the thread's except (EOFError, KeyboardInterrupt) clause handles both cases, but only the EOFError branch is exercised (via test_interactive_loop_fallback_eof_exits_cleanly). There is no test — at any level — that injects KeyboardInterrupt into the input() call inside the thread and asserts _FALLBACK_EOF lands on the queue.

  3. Normal queue behavior — the happy path (stripped user input placed on the queue) is implicitly covered by the full interactive-loop integration tests but is never isolated to verify the function's contract in isolation.

Missing Test Scenarios

Add a TestStartInputReaderThread class in tests/copilot_usage/test_cli.py with:

1. Daemon flag

def test_thread_is_daemon(self, monkeypatch: pytest.MonkeyPatch) -> None:
    """Spawned thread must be daemon so it cannot block process exit."""
    import copilot_usage.cli as cli_mod
    monkeypatch.setattr("builtins.input", lambda: (_ for _ in ()).throw(EOFError()))
    q = _start_input_reader_thread()
    # drain sentinel so thread exits
    q.get(timeout=1.0)
    # The threading.Thread object is gone by now, but we can capture it.

(Alternatively, patch threading.Thread to capture the daemon kwarg before forwarding to the real constructor.)

2. Normal input placed on queue (stripped)

def test_normal_input_is_stripped_and_queued(self, monkeypatch: pytest.MonkeyPatch) -> None:
    inputs = iter(["  hello  ", EOFError()])
    def _fake_input() -> str:
        val = next(inputs)
        if isinstance(val, BaseException):
            raise val
        return val
    monkeypatch.setattr("builtins.input", _fake_input)
    q = _start_input_reader_thread()
    assert q.get(timeout=1.0) == "hello"
    assert q.get(timeout=1.0) == _FALLBACK_EOF

3. EOFError_FALLBACK_EOF (isolated)

def test_eoferror_puts_fallback_sentinel(self, monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr("builtins.input", lambda: (_ for _ in ()).throw(EOFError()))
    q = _start_input_reader_thread()
    assert q.get(timeout=1.0) == _FALLBACK_EOF

4. KeyboardInterrupt_FALLBACK_EOF (untested at any level)

def test_keyboard_interrupt_puts_fallback_sentinel(self, monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr("builtins.input", lambda: (_ for _ in ()).throw(KeyboardInterrupt()))
    q = _start_input_reader_thread()
    assert q.get(timeout=1.0) == _FALLBACK_EOF

5. Unexpected Exception_FALLBACK_EOF + warning logged (isolated)

def test_unexpected_exception_puts_fallback_and_logs_warning(
    self, monkeypatch: pytest.MonkeyPatch
) -> None:
    monkeypatch.setattr("builtins.input", lambda: (_ for _ in ()).throw(RuntimeError("boom")))
    import copilot_usage.cli as cli_mod
    with patch.object(cli_mod.logger, "warning") as warn_spy:
        q = _start_input_reader_thread()
        sentinel = q.get(timeout=1.0)
    assert sentinel == _FALLBACK_EOF
    warn_spy.assert_called_once()
    assert "Unexpected stdin error" in warn_spy.call_args.args[0]

Regression Scenario

The daemon-flag test is the highest-priority regression guard. Removing daemon=True from the threading.Thread(...) call would not be caught by any existing test — the integration tests would hang indefinitely waiting for the reader thread to exit, eventually being killed by CI timeout (the exact failure mode described in issue #1012).

Files to Modify

  • tests/copilot_usage/test_cli.py — add TestStartInputReaderThread class after TestReadLineNonblocking (currently at line 2145)
  • Import _start_input_reader_thread and _FALLBACK_EOF alongside the existing private imports at the top of the test file

Warning

⚠️ Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • pypi.org

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "pypi.org"

See Network Configuration for more information.

Generated by Test Suite Analysis · ● 10.7M ·

Metadata

Metadata

Assignees

No one assigned

    Labels

    awCreated by agentic workflowaw-dispatchedIssue has been dispatched to implementertest-auditTest coverage gaps

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions