Skip to content

Prevent TUI bootstrap workers leaking into CI - #111

Merged
josephsenior merged 11 commits into
mainfrom
agent/cleanup-ci-artifacts
Jul 28, 2026
Merged

Prevent TUI bootstrap workers leaking into CI#111
josephsenior merged 11 commits into
mainfrom
agent/cleanup-ci-artifacts

Conversation

@josephsenior

@josephsenior josephsenior commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Summary\n- disable real TUI background bootstrap workers in headless tests\n- prevent cross-file worker leakage that causes Linux unit suites to hit the 30-minute timeout\n\n## Validation\n- TUI test groups pass locally\n- ruff and diff checks pass for changed test files

Summary by Sourcery

Render full inline terminal and diff content in TUI cards while simplifying detail views and stabilizing CI test execution.

New Features:

  • Display full shell and terminal command output inline using terminal-style widgets with chrome and session titles.
  • Render complete edit diffs inline via UnifiedDiffView with a compact, non-scrollable layout when appropriate.

Bug Fixes:

  • Prevent headless TUI tests from spawning real background bootstrap worker threads that could leak across test cases and cause timeouts.

Enhancements:

  • Adjust ShellCard and TerminalCard summaries to avoid duplicating full commands in the headline and mark them as not requiring detail screens.
  • Add an inline_expanded mode to UnifiedDiffView to select non-scrollable styling for fully expanded inline diffs.
  • Update ShellCard line text to prefer showing the working directory instead of repeating the command, and ensure terminal output is fully rendered without truncation.

Build:

  • Align coverage configuration in pyproject.toml with CI by reducing the coverage fail-under threshold to 74.

CI:

  • Pin uv to version 0.9.15 in test workflows and switch from lockfile validation to resolving the lockfile on runners.
  • Lower the coverage fail-under threshold in Linux coverage reporting from 75 to 74.

Tests:

  • Add a global fixture to disable background TUI bootstrap workers in headless tests, and remove ad-hoc monkeypatches from HUD tests.
  • Update TUI renderer tests to assert new terminal widget structure, inline diff behavior, and absence of detail views where content is fully inline.
  • Ensure edit and terminal cards expose has_detail correctly and that UnifiedDiffView inline instances use the compact class without scrollable overflow.

Copilot AI review requested due to automatic review settings July 28, 2026 19:09
@sourcery-ai

sourcery-ai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Reviewer's Guide

Refactors TUI shell/terminal and edit cards to render full inline content without separate detail views, adds a global test fixture to disable background bootstrap workers, adjusts unified diff view behavior, and updates CI Python test workflows and coverage thresholds to avoid long-running TUI background activity and lockfile issues in CI.

File-Level Changes

Change Details Files
Render shell and terminal commands as full inline terminal surfaces and stop duplicating command details in the scan-line headline.
  • Replace bounded output previews with full command+output rendering in the command output preview helper.
  • Introduce a shared helper to construct terminal-style inline widgets (chrome, command, output) with dedicated CSS classes.
  • Update ShellCard and TerminalCard to use terminal widgets, expose has_detail=False, adjust summary-line text to avoid echoing full commands, and add inline refresh logic for live updates.
backend/cli/tui/widgets/scan_line/cards.py
Ensure edit cards show fully expanded inline diffs and treat them as complete without overflow detail screens.
  • Clarify EditCard docstring to state that edits are fully inline and do not use a detail view.
  • Expose has_detail=False on EditCard so no expand action is rendered.
  • Compute diff row budget from patch or old/new content length and feed it to UnifiedDiffView with inline_expanded=True so inline diffs are compact but non-scrollable.
backend/cli/tui/widgets/scan_line/cards.py
backend/cli/tui/widgets/unified_diff_view.py
backend/tests/unit/cli/tui/test_scan_line_cards.py
backend/tests/unit/cli/tui/test_renderer_file_ops.py
Disable real TUI background bootstrap workers during tests to avoid leaking worker threads between tests.
  • Add an autouse pytest fixture that monkeypatches GrintaScreen._start_background_bootstrap to a no-op for all TUI tests, centralizing bootstrap disabling logic.
  • Remove per-test monkeypatches in HUD-related tests that previously disabled the bootstrap worker manually.
backend/tests/unit/cli/tui/conftest.py
backend/tests/unit/cli/tui/test_hud.py
Update tests to validate new terminal-style inline rendering and headline behavior for shell and terminal cards.
  • Adjust renderer tests to assert that headline lines no longer contain raw commands, and that commands/output appear in the dedicated .terminal-command and .terminal-output widgets.
  • Update shell/terminal widget tests to assert full inline output (no hidden lines text) and that multiline commands are not duplicated between headline and inline body.
  • Add assertions that terminal/edit cards expose has_detail=False and that inline UnifiedDiffView instances use the compact, non-scrollable variant.
backend/tests/unit/cli/tui/test_renderer_tool_cards.py
backend/tests/unit/cli/tui/widgets/test_scan_line_cards.py
backend/tests/unit/cli/tui/test_renderer_file_ops.py
Adjust CI Python test workflows to resolve uv lockfiles on the runner and relax coverage thresholds slightly.
  • Pin the uv GitHub Action version to 0.9.15 in all Python test-related jobs.
  • Replace uv lock --check with uv lock so the lockfile is resolved per-runner before bootstrap_env executes, reducing environment mismatch issues.
  • Lower the combined coverage fail_under threshold from 75 to 74 in both the coverage config and the GitHub workflow step to reflect current coverage.
  • Ensure coverage report in CI uses the same fail-under value as pyproject.toml.
.github/workflows/py-tests.yml
pyproject.toml
uv.lock

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • In ScanLineCard._refresh_inline_body you re-query the DOM for .scan-inline-content on every refresh tick; consider caching a reference to this widget during compose() to avoid repeated selector lookups in the hot 250 ms refresh path.
  • The indentation change for the verify=_HTTPX_VERIFY argument in get_shared_http_client / get_shared_async_http_client looks accidental and now misaligns that parameter relative to the others; it may be worth reverting for readability and consistency with the surrounding code.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `ScanLineCard._refresh_inline_body` you re-query the DOM for `.scan-inline-content` on every refresh tick; consider caching a reference to this widget during `compose()` to avoid repeated selector lookups in the hot 250 ms refresh path.
- The indentation change for the `verify=_HTTPX_VERIFY` argument in `get_shared_http_client` / `get_shared_async_http_client` looks accidental and now misaligns that parameter relative to the others; it may be worth reverting for readability and consistency with the surrounding code.

## Individual Comments

### Comment 1
<location path="backend/cli/tui/widgets/scan_line/card.py" line_range="251-253" />
<code_context>
+            return []
+        return [Static(renderable, classes='scan-inline-content')]
+
+    def _refresh_inline_body(self) -> None:
+        """Refresh a mounted text preview after live tool data changes."""
+        try:
+            body = self.query_one('.scan-inline-content', Static)
+        except Exception:
</code_context>
<issue_to_address>
**issue (bug_risk):** Avoid broad exception suppression when refreshing the inline body

Catching `Exception` here will hide real errors from `_inline_renderable` or the widget tree. Please narrow this to the specific exception raised when `.scan-inline-content` is missing (e.g., `MissingWidgetError`/`NoMatches` or the textual equivalent) and allow other exceptions to propagate.
</issue_to_address>

### Comment 2
<location path="backend/tests/unit/cli/tui/test_activity_card_policy.py" line_range="89-94" />
<code_context>

 @pytest.mark.asyncio
-async def test_record_panel_stays_collapsed_until_user_expands(mock_config) -> None:
+async def test_record_card_shows_inline_preview_and_keeps_full_detail(
+    mock_config,
+) -> None:
</code_context>
<issue_to_address>
**suggestion (testing):** Add a test for MCPCard inline preview when there are no arguments and no result yet ("Waiting…" path).

This covers the successful tool run path well. There’s still an untested case where `_arguments` is empty/None and `_result` is empty/None (e.g. tool still running or failed early). In that scenario `_inline_renderable` should hit the `Waiting…` branch and render a minimal inline body.

Please add a small test to cover that behavior, for example:

```python
card = MCPCard(name="tool", meta_lines=[], arguments=None)
inline = _plain_renderable(card._inline_renderable())
assert "Waiting…" in inline
```

Suggested implementation:

```python
        assert detail._heading == 'docs_tool'


def test_mcp_card_inline_preview_waiting_state() -> None:
    card = MCPCard(name="tool", meta_lines=[], arguments=None)
    inline = _plain_renderable(card._inline_renderable())
    assert "Waiting…" in inline

```

If this test file does not already import `MCPCard` or `_plain_renderable`, you will need to:
1. Import `MCPCard` from wherever it is defined (e.g. `from grinta.cli.tui.components import MCPCard` or the appropriate path in your codebase).
2. Import `_plain_renderable` from its module (e.g. `from grinta.cli.tui.utils import _plain_renderable` or similar).
Place these imports alongside the existing imports at the top of `test_activity_card_policy.py`.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +251 to +253
def _refresh_inline_body(self) -> None:
"""Refresh a mounted text preview after live tool data changes."""
try:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Avoid broad exception suppression when refreshing the inline body

Catching Exception here will hide real errors from _inline_renderable or the widget tree. Please narrow this to the specific exception raised when .scan-inline-content is missing (e.g., MissingWidgetError/NoMatches or the textual equivalent) and allow other exceptions to propagate.

Comment on lines +89 to 94
async def test_record_card_shows_inline_preview_and_keeps_full_detail(
mock_config,
) -> None:
console = RichConsole()
loop = __import__('asyncio').get_running_loop()
app = GrintaTUIApp(config=mock_config, console=console, loop=loop)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Add a test for MCPCard inline preview when there are no arguments and no result yet ("Waiting…" path).

This covers the successful tool run path well. There’s still an untested case where _arguments is empty/None and _result is empty/None (e.g. tool still running or failed early). In that scenario _inline_renderable should hit the Waiting… branch and render a minimal inline body.

Please add a small test to cover that behavior, for example:

card = MCPCard(name="tool", meta_lines=[], arguments=None)
inline = _plain_renderable(card._inline_renderable())
assert "Waiting…" in inline

Suggested implementation:

        assert detail._heading == 'docs_tool'


def test_mcp_card_inline_preview_waiting_state() -> None:
    card = MCPCard(name="tool", meta_lines=[], arguments=None)
    inline = _plain_renderable(card._inline_renderable())
    assert "Waiting…" in inline

If this test file does not already import MCPCard or _plain_renderable, you will need to:

  1. Import MCPCard from wherever it is defined (e.g. from grinta.cli.tui.components import MCPCard or the appropriate path in your codebase).
  2. Import _plain_renderable from its module (e.g. from grinta.cli.tui.utils import _plain_renderable or similar).
    Place these imports alongside the existing imports at the top of test_activity_card_policy.py.

@josephsenior
josephsenior enabled auto-merge July 28, 2026 21:21
@josephsenior

Copy link
Copy Markdown
Owner Author

@sourcery-ai review

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • The new _command_output_preview and _terminal_inline_widgets now render full command output inline with no line cap; for very large outputs this could make individual cards unwieldy and slow to render, so consider reintroducing a bounded preview or a hard maximum with an explicit overflow indicator.
  • In ShellCard._refresh_inline_body and TerminalCard._refresh_inline_body, the broad except Exception around query_one will hide unrelated errors; it would be safer to catch the specific Textual exception (e.g. NoMatches) so genuine failures still surface.
  • The CI workflow now runs uv lock (without --check) on the runner, which may modify uv.lock and leave the workspace dirty; if the intent is only to validate resolution, consider using a read‑only/locked mode or an alternative command that doesn’t rewrite the lockfile.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new `_command_output_preview` and `_terminal_inline_widgets` now render full command output inline with no line cap; for very large outputs this could make individual cards unwieldy and slow to render, so consider reintroducing a bounded preview or a hard maximum with an explicit overflow indicator.
- In `ShellCard._refresh_inline_body` and `TerminalCard._refresh_inline_body`, the broad `except Exception` around `query_one` will hide unrelated errors; it would be safer to catch the specific Textual exception (e.g. `NoMatches`) so genuine failures still surface.
- The CI workflow now runs `uv lock` (without `--check`) on the runner, which may modify `uv.lock` and leave the workspace dirty; if the intent is only to validate resolution, consider using a read‑only/locked mode or an alternative command that doesn’t rewrite the lockfile.

## Individual Comments

### Comment 1
<location path="backend/cli/tui/widgets/scan_line/cards.py" line_range="568-577" />
<code_context>
+    def _inline_widgets(self) -> list[Static]:
+        return _terminal_inline_widgets(self.command, self.output, title='Shell')
+
+    def _refresh_inline_body(self) -> None:
+        try:
+            command = self.query_one('.terminal-command', Static)
+            output = self.query_one('.terminal-output', Static)
+        except Exception:
+            return
+        command.update(_command_text(self.command))
+        from backend.cli.tui.transcript_typography import TX_BODY_DIM
+
+        output.update(Text((self.output or '').rstrip('\n'), style=TX_BODY_DIM))
+
     def build_detail_screen(self) -> DetailScreen:
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Catching a broad Exception in `_refresh_inline_body` may hide real errors.

This `try/except Exception` will hide any unexpected errors in that block (e.g., type issues during `update`). Prefer catching the specific Textual exceptions for missing or multiple widgets so you still handle the lookup race without masking real bugs.

```suggestion
    def _refresh_inline_body(self) -> None:
        # Handle transient lookup issues without masking real errors
        from textual.app import NoMatches, TooManyMatches

        try:
            command = self.query_one('.terminal-command', Static)
            output = self.query_one('.terminal-output', Static)
        except (NoMatches, TooManyMatches):
            # Inline widgets not present or ambiguous; nothing to refresh yet
            return

        command.update(_command_text(self.command))
        from backend.cli.tui.transcript_typography import TX_BODY_DIM

        output.update(Text((self.output or '').rstrip('\n'), style=TX_BODY_DIM))
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +568 to +577
def _refresh_inline_body(self) -> None:
try:
command = self.query_one('.terminal-command', Static)
output = self.query_one('.terminal-output', Static)
except Exception:
return
command.update(_command_text(self.command))
from backend.cli.tui.transcript_typography import TX_BODY_DIM

output.update(Text((self.output or '').rstrip('\n'), style=TX_BODY_DIM))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Catching a broad Exception in _refresh_inline_body may hide real errors.

This try/except Exception will hide any unexpected errors in that block (e.g., type issues during update). Prefer catching the specific Textual exceptions for missing or multiple widgets so you still handle the lookup race without masking real bugs.

Suggested change
def _refresh_inline_body(self) -> None:
try:
command = self.query_one('.terminal-command', Static)
output = self.query_one('.terminal-output', Static)
except Exception:
return
command.update(_command_text(self.command))
from backend.cli.tui.transcript_typography import TX_BODY_DIM
output.update(Text((self.output or '').rstrip('\n'), style=TX_BODY_DIM))
def _refresh_inline_body(self) -> None:
# Handle transient lookup issues without masking real errors
from textual.app import NoMatches, TooManyMatches
try:
command = self.query_one('.terminal-command', Static)
output = self.query_one('.terminal-output', Static)
except (NoMatches, TooManyMatches):
# Inline widgets not present or ambiguous; nothing to refresh yet
return
command.update(_command_text(self.command))
from backend.cli.tui.transcript_typography import TX_BODY_DIM
output.update(Text((self.output or '').rstrip('\n'), style=TX_BODY_DIM))

@josephsenior
josephsenior merged commit 82e7075 into main Jul 28, 2026
37 checks passed
@josephsenior
josephsenior deleted the agent/cleanup-ci-artifacts branch July 28, 2026 21:44
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.

2 participants