Compare commits

...

4 Commits

Author SHA1 Message Date
HAL9000 c84507cf2e fix(tests): fix BDD step setup for issue #4741 @token extraction tests
CI / push-validation (pull_request) Successful in 30s
CI / helm (pull_request) Successful in 39s
CI / build (pull_request) Successful in 45s
CI / lint (pull_request) Successful in 59s
CI / quality (pull_request) Successful in 59s
CI / typecheck (pull_request) Successful in 1m48s
CI / security (pull_request) Successful in 1m48s
CI / integration_tests (pull_request) Successful in 4m10s
CI / unit_tests (pull_request) Successful in 6m51s
CI / docker (pull_request) Successful in 2m13s
CI / coverage (pull_request) Successful in 12m48s
CI / status-check (pull_request) Successful in 3s
- Replace __import__() with importlib.import_module() so step code
  correctly accesses cleveragents.tui.app (not the top-level package)
- Fix context.add_cleanup(_restore_modules) → lambda so context arg
  is passed when Behave invokes the cleanup
- Remove redundant double-quotes from Examples table values; the step
  definition's own quotes already delimit the parameter, causing the
  regex to capture a trailing quote and the assertion to fail
- Apply ruff format to bring file in line with project style

ISSUES CLOSED: #4741
2026-05-28 19:26:30 -04:00
HAL9000 886473a80b fix(tui): fix lint errors and test cleanup in BDD steps for #4741
CI / lint (pull_request) Successful in 39s
CI / helm (pull_request) Successful in 33s
CI / build (pull_request) Successful in 35s
CI / typecheck (pull_request) Successful in 1m2s
CI / quality (pull_request) Successful in 1m4s
CI / security (pull_request) Successful in 1m15s
CI / push-validation (pull_request) Successful in 1m6s
CI / integration_tests (pull_request) Successful in 2m52s
CI / unit_tests (pull_request) Failing after 4m25s
CI / coverage (pull_request) Has been skipped
CI / docker (pull_request) Has been skipped
CI / status-check (pull_request) Failing after 3s
- Remove unused imports: re, patch, Context, persona registry/state
- Remove unused variable: original_suggestions
- Replace undefined MockStatic() with MagicMock()
- Replace no-op tuple lambda cleanup with proper _cleanup_tmpdir function
- Add shutil.rmtree cleanup for tempfile.mkdtemp() temp directory
- Replace try/except/pass with contextlib.suppress(Exception)
- Remove @tdd_expected_fail tag from feature file (tests now pass)
- Apply ruff format to bring file into compliance
2026-05-28 18:58:29 -04:00
controller-ci-rerun 831ee434e4 chore: re-trigger CI [controller]
CI / lint (pull_request) Failing after 33s
CI / quality (pull_request) Successful in 50s
CI / helm (pull_request) Successful in 32s
CI / build (pull_request) Successful in 41s
CI / typecheck (pull_request) Successful in 1m26s
CI / security (pull_request) Successful in 1m26s
CI / push-validation (pull_request) Successful in 32s
CI / integration_tests (pull_request) Successful in 3m0s
CI / unit_tests (pull_request) Successful in 4m33s
CI / coverage (pull_request) Has been skipped
CI / docker (pull_request) Has been skipped
CI / status-check (pull_request) Failing after 3s
2026-05-28 18:48:24 -04:00
HAL9000 2606688c76 fix(tui): extract @token text correctly in on_input_submitted suggestions query
Bug #4741: The TUI on_input_submitted handler passed
text.replace("@", "").strip() to suggestions(), e.g. "analyse @proj" ->
"analyse proj", producing garbage fuzzy matches because all @ characters
were stripped and surrounding words polluted the query.

Fix uses re.findall(r"@(\S+)", text)[-1] which extracts only the last
@token as the suggestion query, without surrounding words.

Changes:
- Replace text.replace("@", "").strip() with regex @token extraction in
  src/cleveragents/tui/app.py on_input_submitted handler
- Add TDD BDD regression test covering 4 input scenarios:
  single-token (@proj), category-prefixed (@project:doc), multi-token
  (@actor:x @tool:y -> tool:y), and standalone (@skill:name)
- Update CHANGELOG.md under [Unreleased]/Fixed
- Update CONTRIBUTORS.md with contribution entry

ISSUES CLOSED: #4741
2026-05-28 18:48:24 -04:00
5 changed files with 291 additions and 4 deletions
+7
View File
@@ -7,6 +7,13 @@ Changed `wf10_batch.robot` to be less likely to create files, and
## [Unreleased]
- **fix(tui): extract @token text correctly in on_input_submitted
suggestions query (PR #11004 / issue #4741)** — The TUI handler passed
``text.replace("@", "").strip()`` to ``suggestions()``, producing garbage
fuzzy matches (e.g. "analyse \@proj" → "analyse proj"). Replaced with
``re.findall(r"@(\S+)", text)`` that extracts only the last ``@token`` as
the query, returning just the token value without surrounding words.
- **SubplanExecutionService lazy wiring in CLI** (#10268): Wired `subplan_service`
from the DI container into `_get_plan_executor()` so `PlanExecutor` can spawn child
plans during the Execute phase. When `SubplanService` is available but
+1
View File
@@ -48,3 +48,4 @@ Below are some of the specific details of various contributions.
* HAL 9000 has contributed the DecisionService wiring for PlanExecutor strategize persistence fix (#10813): added decision_service to the PlanExecutor constructor and wired it from the CLI dependency-injection container in `_get_plan_executor()`, plus implemented `_persist_strategy_decisions()` to persist strategy decisions as domain `Decision` objects.
* HAL 9000 has contributed the A2A module rename standardization BDD tests (PR #10583 / issue #8615): comprehensive Behave test suite validating that all 22 A2A symbols are properly exported from `cleveragents.a2a`, no legacy ACP references remain in the module source, and documentation uses correct A2A naming conventions — fixing inline imports, unused behave symbols, cross-scenario context dependencies, and missing type annotations.
* HAL 9000 has contributed the `ActorSelectionOverlay._render``_refresh_display` rename fix (PR #11176 / issue #11039, Epic #8174): renamed `_render()` method to `_refresh_display()` to avoid shadowing Textual's `Widget._render()`, fixing a crash in textual >=1.0 where `get_content_height()` would receive `None` and raise `AttributeError: 'NoneType' object has no attribute 'get_height'`.
* HAL 9000 has contributed the TUI @token text extraction fix for suggestions query (PR #11004 / issue #4741): replaced ``text.replace("@", "").strip()`` with ``re.findall(r"@(\S+)", text)[-1]`` in ``on_input_submitted`` to correctly extract only the last @token as the query, preventing garbage fuzzy matches. Includes TDD BDD regression test suite covering single-token, category-prefixed, multi-token, and standalone-token scenarios (PR #11004).
@@ -0,0 +1,252 @@
"""Step definitions for TDD issue #4741: @token text extraction for suggestions().
Verifies that ``on_input_submitted`` in ``src/cleveragents/tui/app.py`` extracts
only the last ``@token`` from input using regex (``re.findall(r"@(\\S+)", text)``),
instead of the buggy ``text.replace("@", "").strip()``.
Uses a Scenario Outline so each <input> / <expected_query> pair is tested identically.
"""
from __future__ import annotations
import contextlib
import importlib
import shutil
import sys
import tempfile
from pathlib import Path
from types import ModuleType, SimpleNamespace
from typing import Any
from unittest.mock import MagicMock
from behave import given, then
# ---------------------------------------------------------------------------
# Mock Textual
# ---------------------------------------------------------------------------
_MOCK_TEXTUAL_KEYS = [
"textual",
"textual.app",
"textual.containers",
"textual.widgets",
]
def _build_mock_textual() -> dict[str, ModuleType]:
mock_textual = ModuleType("textual")
mock_app_mod = ModuleType("textual.app")
mock_containers = ModuleType("textual.containers")
mock_widgets = ModuleType("textual.widgets")
class MockApp:
def __init__(self, *a, **kw):
pass
class MockVertical:
def __enter__(self):
return self
def __exit__(self, *a, **kw):
pass
class MockHeader:
def __init__(self, *args, **kwargs):
pass
class MockFooter:
def __init__(self, *args, **kwargs):
pass
class MockStatic:
def __init__(self, *a, **kw):
self._text = ""
def update(self, text):
self._text = str(text)
class MockInput:
value: str = ""
def __init__(self, *a, **kw):
pass
def consume_text(self):
return SimpleNamespace(text=self.value)
mock_app_mod.App = MockApp
mock_containers.Vertical = MockVertical
mock_widgets.Header = MockHeader
mock_widgets.Footer = MockFooter
mock_widgets.Static = MockStatic
mock_widgets.Input = MockInput
return {
"textual": mock_textual,
"textual.app": mock_app_mod,
"textual.containers": mock_containers,
"textual.widgets": mock_widgets,
}
def _restore_modules(context):
for key, val in getattr(context, "_tui_saved_modules", {}).items():
if val is None:
sys.modules.pop(key, None)
else:
sys.modules[key] = val
importlib.reload(
importlib.import_module("cleveragents.tui.widgets.help_panel_overlay")
)
importlib.reload(importlib.import_module("cleveragents.tui.widgets.persona_bar"))
importlib.reload(importlib.import_module("cleveragents.tui.widgets.prompt"))
importlib.reload(
importlib.import_module("cleveragents.tui.widgets.reference_picker")
)
importlib.reload(
importlib.import_module("cleveragents.tui.widgets.slash_command_overlay")
)
importlib.reload(importlib.import_module("cleveragents.tui.app"))
# ---------------------------------------------------------------------------
# Given: set up mock + run on_input_submitted with the captured input text
# ---------------------------------------------------------------------------
@given('the TUI on_input_submitted handler processes "{input_text}"')
def step_run_on_input_submitted(context, input_text):
"""Install mock Textual, create app, simulate submission, capture query.
We patch both ``ReferencePickerOverlay.set_suggestions`` and the module-level
``suggestions()`` so we can inspect the exact argument passed to it.
"""
# Remove saved modules from any previous scenario (Scenario Outline)
_restore_modules(context) if getattr(context, "_tui_saved_modules", None) else None
mocks = _build_mock_textual()
context._tui_saved_modules = {}
for key in _MOCK_TEXTUAL_KEYS:
context._tui_saved_modules[key] = sys.modules.pop(key, None)
for key, mod in mocks.items():
sys.modules[key] = mod
# Reload widget modules with mocked base classes
from cleveragents.tui.widgets import (
help_panel_overlay as hp_mod,
persona_bar as pb_mod,
prompt as prompt_mod,
reference_picker as rp_mod,
slash_command_overlay as sco_mod,
)
importlib.reload(hp_mod)
importlib.reload(pb_mod)
importlib.reload(prompt_mod)
importlib.reload(rp_mod)
importlib.reload(sco_mod)
# Install mock persona state and command router before app module reloads
tmp_dir = tempfile.mkdtemp()
context._tui_tmpdir = Path(tmp_dir)
from cleveragents.tui.persona.registry import PersonaRegistry
from cleveragents.tui.persona.state import PersonaState
registry = PersonaRegistry(config_dir=Path(tmp_dir))
registry.ensure_default()
persona_state = PersonaState(registry=registry)
class MockCommandRouter:
def handle(self, raw, *, session_id):
return f"handled:{raw}"
mock_router = MockCommandRouter()
context._tui_persona_state = persona_state
context._tui_cmd_router = mock_router
app_mod = importlib.import_module("cleveragents.tui.app")
importlib.reload(app_mod)
# Patch the suggestions function BEFORE instantiating the app, so that when
# on_input_submitted calls it internally, our spy intercepts it.
captured_queries: list[str] = []
def _spy_suggestions(query, *, category=None, limit=8):
captured_queries.append(query)
return list(
app_mod.suggestions.__wrapped__.__code__.co_varnames[:limit]
) # dummy
# The suggestions function is imported at module level in app.py. We need to
# patch it there, not on reference_parser itself.
def spy(query, **kw):
captured_queries.append(query)
return []
app_mod.suggestions = spy
cls = app_mod._ResolvedTuiApp
# Build mock widgets dict so query_one works
app_inst = cls(command_router=mock_router, persona_state=persona_state)
# Manually populate _widgets from the MockApp instance if it exists
if hasattr(app_inst, "_widgets"):
wdict: dict[str, Any] = app_inst._widgets
else:
wdict = {}
# Create mock widgets that behave correctly
mock_prompt = MagicMock()
mock_prompt.consume_text.return_value = SimpleNamespace(text=input_text)
mock_picker = MagicMock()
mock_picker.set_suggestions = lambda q, s: None # dummy, we spy suggestions instead
wdict["#reference-picker"] = mock_picker
mock_prompt_widget = MagicMock()
mock_prompt_widget.consume_text.return_value = SimpleNamespace(text=input_text)
wdict["#prompt"] = mock_prompt_widget
wdict["#conversation"] = MagicMock()
wdict["#help-panel"] = MagicMock()
wdict["#persona-bar"] = MagicMock()
# Patch query_one to use our widget dict
app_inst.query_one = lambda sel, *args, **kw: wdict.get(sel, MagicMock())
with contextlib.suppress(Exception):
app_inst.on_input_submitted(event=MagicMock())
context._tui_captured_query = captured_queries[0] if captured_queries else None
def _cleanup():
if hasattr(app_inst, "_widgets"):
app_inst._widgets.clear()
def _cleanup_tmpdir(d=context._tui_tmpdir):
if d is not None and d.exists():
shutil.rmtree(str(d), ignore_errors=True)
context.add_cleanup(_cleanup)
context.add_cleanup(lambda: _restore_modules(context))
context.add_cleanup(_cleanup_tmpdir)
# ---------------------------------------------------------------------------
# Then: assertion on the captured query
# ---------------------------------------------------------------------------
@then('the suggestion query should be "{expected}"')
def step_suggestion_query_is_correct(context, expected):
assert context._tui_captured_query == expected, (
f"Bug #4741: extracted suggestion query was "
f"{context._tui_captured_query!r}, expected {expected!r}. "
f"The old buggy .replace(@).strip() approach produces garbage matches."
)
@@ -0,0 +1,23 @@
@tdd_issue @tdd_issue_4741
Feature: TDD Issue #4741 — Extract only the @token text as suggestions() query
Bug #4741: ``on_input_submitted`` passed ``text.replace("@", "").strip()`` to
``suggestions()``, e.g. "analyse \@proj" "analyse proj", producing garbage
fuzzy matches because surrounding words polluted the query.
The fix replaces it with ``re.findall(r"@(\S+)", text)`` taking the last
match as the query string, so "analyse \@proj" "proj".
Scenario Outline: @token extraction produces correct queries for input text
The suggestion query must be the last extracted @token from input text.
With the old buggy code these assertions fail; after the fix they pass.
Given the TUI on_input_submitted handler processes "<input>"
Then the suggestion query should be "<expected_query>"
Examples:
| input | expected_query |
| analyse @proj | proj |
| read @project:main-doc | project:main-doc |
| look-up @actor:x @tool:y | tool:y |
| @skill:doc-generator | skill:doc-generator |
+8 -4
View File
@@ -4,6 +4,7 @@ from __future__ import annotations
import importlib
import os
import re
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any, ClassVar, Protocol
@@ -396,10 +397,13 @@ if _TEXTUAL_AVAILABLE:
expanded = result.expanded_text
if "@" in text:
ref_picker = self.query_one("#reference-picker", ReferencePickerOverlay)
ref_picker.set_suggestions(
text, suggestions(text.replace("@", "").strip())
)
matches = re.findall(r"@(\S+)", text)
if matches:
query_text = matches[-1]
ref_picker = self.query_one(
"#reference-picker", ReferencePickerOverlay
)
ref_picker.set_suggestions(text, suggestions(query_text))
if self._facade is None:
# Facade not wired yet — preview only (graceful degradation)