chore: import upstream snapshot with attribution
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:10:45 +08:00
commit 4b6817381b
3933 changed files with 525247 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
# Package marker for mirrored tests.
@@ -0,0 +1,126 @@
"""Tests for interactive-shell action rendering."""
from __future__ import annotations
import io
from dataclasses import dataclass
import pytest
from rich.console import Console
import tools.interactive_shell.actions.slash as slash_tool
from core.agent_harness.turns.turn_results import ToolCallingTurnResult
from surfaces.interactive_shell.runtime.action_turn import run_action_tool_turn
from surfaces.interactive_shell.runtime.shell_turn_execution import execute_shell_turn
from surfaces.interactive_shell.session import Session
from surfaces.interactive_shell.ui.action_rendering import ActionRenderObserver
from surfaces.interactive_shell.ui.input_prompt.rendering import _prompt_turn_number
from tests.core.agent.orchestration.action_execution_test_harness import (
ActionExecutionHarness,
FakeActionLLM,
no_tool_response,
)
def test_slash_invoke_tool_start_does_not_record_cli_agent() -> None:
session = Session()
buffer = io.StringIO()
console = Console(file=buffer, force_terminal=False, highlight=False)
observer = ActionRenderObserver(session=session, console=console, message="/model show")
observer(
"tool_start",
{"name": "slash_invoke", "input": {"command": "/model", "args": ["show"]}},
)
assert session.history == []
assert observer.planned_count == 1
assert buffer.getvalue() == ""
def test_shell_run_tool_start_does_not_record_cli_agent() -> None:
session = Session()
console = Console(file=io.StringIO(), force_terminal=False, highlight=False)
observer = ActionRenderObserver(session=session, console=console, message="!true")
observer("tool_start", {"name": "shell_run", "input": {"command": "true"}})
assert session.history == []
assert observer.planned_count == 1
def test_literal_slash_command_records_single_history_entry(
monkeypatch: pytest.MonkeyPatch,
) -> None:
dispatched: list[str] = []
def _fake_dispatch(
command: str,
session: Session,
console: Console,
**_kwargs: object,
) -> bool:
dispatched.append(command)
session.record("slash", command, ok=True)
return True
monkeypatch.setattr(slash_tool, "dispatch_slash", _fake_dispatch)
session = Session()
harness = ActionExecutionHarness(llm=FakeActionLLM([no_tool_response()]))
result = run_action_tool_turn(
"/model show",
session,
harness.console,
deps=harness.deps,
)
assert result.handled is True
assert dispatched == ["/model show"]
assert session.history == [{"type": "slash", "text": "/model show", "ok": True}]
assert _prompt_turn_number(session) == 2
@dataclass
class _FakeLlmRun:
response_text: str = "hello back"
def test_chat_turn_records_single_cli_agent_history_entry() -> None:
session = Session()
console = Console(file=io.StringIO(), force_terminal=False, highlight=False)
def _no_actions(
_text: str,
_session: Session,
_console: Console,
**kwargs: object,
) -> ToolCallingTurnResult:
return ToolCallingTurnResult(
planned_count=0,
executed_count=0,
executed_success_count=0,
has_unhandled_clause=False,
handled=False,
accounting_status="not_run",
)
def _answer(
_text: str,
_session: Session,
_console: Console,
**kwargs: object,
) -> _FakeLlmRun:
return _FakeLlmRun()
execute_shell_turn(
"what broke in prod?",
session,
console,
recorder=None,
execute_actions=_no_actions,
answer_agent=_answer,
)
assert session.history == [{"type": "cli_agent", "text": "what broke in prod?", "ok": True}]
assert _prompt_turn_number(session) == 2
@@ -0,0 +1,361 @@
"""Pure rendering tests for the ``/fleet`` dashboard table (issue #1488).
These cover ``render_agents_table`` in isolation — no slash-command
dispatch, no real registry I/O. The integration tests in
``test_agents_commands.py`` cover the dispatch path that consumes
this function.
"""
from __future__ import annotations
import io
from datetime import UTC, datetime, timedelta
from pathlib import Path
import pytest
from rich.console import Console
from rich.table import Table
from surfaces.interactive_shell.ui.agents import agents_view as agents_view_mod
from surfaces.interactive_shell.ui.agents.agents_view import _build_agents_table
from tools.system.fleet_monitoring import config as config_mod
from tools.system.fleet_monitoring.probe import ProcessSnapshot
from tools.system.fleet_monitoring.registry import AgentRecord
from tools.system.fleet_monitoring.token_rate import TOKEN_RATE_TRACKER
@pytest.fixture(autouse=True)
def isolated_agents_yaml(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Path:
"""Autouse: redirect ``agents_config_path`` to a per-test tmp file
so the rendering tests don't read the developer's real
``~/.opensre/agents.yaml`` (which would let real budgets
leak into the placeholder assertions and create cross-machine
flakes).
"""
target = tmp_path / "agents.yaml"
monkeypatch.setattr(config_mod, "agents_config_path", lambda: target)
return target
@pytest.fixture(autouse=True)
def _clear_sampler_module_state() -> None:
"""Reset module-level state in the sampler before each test.
Three globals can leak across test files:
- :data:`tools.system.fleet_monitoring.sampler._latest` (probe snapshots dict)
- :data:`tools.system.fleet_monitoring.token_rate.TOKEN_RATE_TRACKER` (per-PID deque)
- :data:`tools.system.fleet_monitoring.sampler._TickCache.registry_snapshot` and
:data:`_TickCache.agents_config` (tick caches added in #2023 to
avoid re-reading the disk on every render)
Sampler tests populate all of them; view tests must start clean
so placeholder assertions are stable under ``pytest-xdist`` and
in arbitrary alphabetical order.
"""
from tools.system.fleet_monitoring import sampler as sampler_mod
sampler_mod._latest.clear()
sampler_mod._TickCache.registry_snapshot = {}
sampler_mod._TickCache.agents_config = None
for pid in list(TOKEN_RATE_TRACKER.known_pids()):
TOKEN_RATE_TRACKER.forget(pid)
# The columns this PR ships are the contract for #1490 and later
# tickets that thread snapshot data into the rendering layer; pin
# them here so a downstream reorder doesn't silently break the
# dashboard preview.
_DASHBOARD_COLUMNS: tuple[str, ...] = (
"agent",
"pid",
"uptime",
"cpu%",
"tokens/min",
"$/hr",
"status",
)
def _render(records: list[AgentRecord]) -> tuple[Table, str]:
"""Build the table and capture the printed form for substring assertions."""
table = _build_agents_table(records)
buf = io.StringIO()
Console(file=buf, force_terminal=False, highlight=False, width=120).print(table)
return table, buf.getvalue()
# ---------------------------------------------------------------------------
# Column structure — the contract downstream tickets lean on
# ---------------------------------------------------------------------------
def test_table_has_full_dashboard_column_set_in_documented_order() -> None:
table, _ = _render([])
headers = tuple(str(col.header) for col in table.columns)
assert headers == _DASHBOARD_COLUMNS
def test_pid_column_is_right_justified_to_match_numeric_dashboard_preview() -> None:
"""Numeric columns (pid, uptime, cpu%, tokens/min, $/hr) are
right-justified; agent name and status are left-justified. This
matches the spacing in the issue's mock and keeps later
snapshot-injected cells aligned without re-styling."""
table, _ = _render([])
by_header = {str(col.header): col for col in table.columns}
assert by_header["pid"].justify == "right"
assert by_header["uptime"].justify == "right"
assert by_header["cpu%"].justify == "right"
assert by_header["tokens/min"].justify == "right"
assert by_header["$/hr"].justify == "right"
assert by_header["agent"].justify == "left"
assert by_header["status"].justify == "left"
# ---------------------------------------------------------------------------
# Empty state
# ---------------------------------------------------------------------------
def test_empty_records_renders_table_with_zero_rows() -> None:
table, _ = _render([])
assert table.row_count == 0
def test_empty_records_caption_announces_empty_state() -> None:
"""Empty-state UX: the table caption tells the user the fleet
is empty rather than leaving a blank table that looks like a bug.
"""
_, out = _render([])
assert "no agents discovered or registered yet" in out
def test_non_empty_records_have_no_caption() -> None:
"""When the registry has rows, the caption is suppressed —
the table content speaks for itself and a caption would be noise."""
table, _ = _render([AgentRecord(name="claude-code", pid=8421, command="claude")])
assert table.caption is None
# ---------------------------------------------------------------------------
# Row content
# ---------------------------------------------------------------------------
def test_row_contains_agent_name_and_pid() -> None:
_, out = _render([AgentRecord(name="claude-code", pid=8421, command="claude")])
assert "claude-code" in out
assert "8421" in out
def test_metric_cells_are_placeholders_when_no_sampler_data() -> None:
"""All five metric cells render ``-`` when the sampler has no
data for the PID (REPL not running, non-interactive ``opensre
agents list``, or fresh registration that has not yet been
sampled). #2023 split ``tokens/min`` and ``$/hr`` from the
yaml budget; both still fall back to ``-`` here for the same
"no data" reason.
"""
table, _ = _render([AgentRecord(name="claude-code", pid=8421, command="claude")])
# row_count == 1, so iterate directly to inspect the rendered cells
assert table.row_count == 1
rendered_cells = [list(col.cells)[0] for col in table.columns]
# cells[0] = agent, cells[1] = pid, then metric cells and status.
assert rendered_cells[2:] == ["-", "-", "-", "-", "-"]
def test_table_shows_live_probe_data_when_snapshot_exists(monkeypatch: pytest.MonkeyPatch) -> None:
fixed_now = datetime(2026, 5, 10, 14, 0, 0, tzinfo=UTC)
started_at = datetime(2026, 5, 10, 12, 0, 0, tzinfo=UTC) # exactly 2h before
class FrozenDatetime(datetime):
@classmethod
def now(cls, _tz=None): # type: ignore[override]
return fixed_now
monkeypatch.setattr(agents_view_mod, "datetime", FrozenDatetime)
fake_snapshot = ProcessSnapshot(
pid=8421,
cpu_percent=23.5,
rss_mb=128.0,
num_fds=42,
num_connections=3,
status="running",
started_at=started_at,
)
monkeypatch.setattr(agents_view_mod, "get_snapshot", lambda _pid: fake_snapshot)
table, _ = _render([AgentRecord(name="cursor", pid=8444, command="cursor")])
rendered_cells = [list(col.cells)[0] for col in table.columns]
# ``tokens/min`` and ``$/hr`` are still ``-`` because no token
# tracker entry exists for this PID — the snapshot fixture covers
# only the resource side. The full live-data case is covered by
# ``test_table_shows_tokens_and_cost_when_tracker_has_data``.
# Without recent output activity, the status heuristic falls back
# to the process start time and renders STUCK with a progress-time annotation.
assert rendered_cells[2:] == ["2h0m", "23.5", "-", "-", "[red]stuck (2h0m no progress)[/red]"]
def test_recent_agent_output_keeps_status_active(monkeypatch: pytest.MonkeyPatch) -> None:
fixed_now = datetime(2026, 5, 10, 14, 0, 0, tzinfo=UTC)
class FrozenDatetime(datetime):
@classmethod
def now(cls, _tz=None): # type: ignore[override]
return fixed_now
monkeypatch.setattr(agents_view_mod, "datetime", FrozenDatetime)
fake_snapshot = ProcessSnapshot(
pid=8421,
cpu_percent=23.5,
rss_mb=128.0,
num_fds=42,
num_connections=3,
status="running",
started_at=fixed_now - timedelta(hours=2),
last_output_at=fixed_now - timedelta(seconds=30),
)
monkeypatch.setattr(agents_view_mod, "get_snapshot", lambda _pid: fake_snapshot)
table, _ = _render([AgentRecord(name="cursor", pid=8444, command="cursor")])
rendered_cells = [list(col.cells)[0] for col in table.columns]
assert rendered_cells[6] == "[green]active[/green]"
def test_stuck_message_anchors_to_last_output_at_not_started_at(
monkeypatch: pytest.MonkeyPatch,
) -> None:
fixed_now = datetime(2026, 5, 10, 14, 0, 0, tzinfo=UTC)
class FrozenDatetime(datetime):
@classmethod
def now(cls, _tz=None): # type: ignore[override]
return fixed_now
monkeypatch.setattr(agents_view_mod, "datetime", FrozenDatetime)
fake_snapshot = ProcessSnapshot(
pid=8421,
cpu_percent=5.0,
rss_mb=64.0,
num_fds=10,
num_connections=1,
status="running",
started_at=fixed_now - timedelta(hours=3),
last_output_at=fixed_now - timedelta(minutes=10),
)
monkeypatch.setattr(agents_view_mod, "get_snapshot", lambda _pid: fake_snapshot)
table, _ = _render([AgentRecord(name="cursor", pid=8444, command="cursor")])
rendered_cells = [list(col.cells)[0] for col in table.columns]
assert rendered_cells[6] == "[red]stuck (10m no progress)[/red]"
def test_table_shows_tokens_and_cost_when_tracker_has_data(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""End-to-end of the #2023 wiring on the view side: when the
sampler's accessors return live numbers, the view formats them
into the ``tokens/min`` and ``$/hr`` columns.
"""
monkeypatch.setattr(agents_view_mod, "get_tokens_per_min", lambda _pid: 120.0)
monkeypatch.setattr(agents_view_mod, "get_usd_per_hour", lambda _pid: 0.27)
table, _ = _render([AgentRecord(name="claude-code-8421", pid=8421, command="claude")])
rendered_cells = [list(col.cells)[0] for col in table.columns]
# cells[4] = tokens/min, cells[5] = $/hr
assert rendered_cells[4] == "120"
assert rendered_cells[5] == "$0.27"
def test_tokens_per_min_formatter_uses_k_suffix_above_1000(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A busy session emitting > 1k tokens/min must not blow out the
column width; the formatter falls back to ``1.2k`` shorthand.
"""
monkeypatch.setattr(agents_view_mod, "get_tokens_per_min", lambda _pid: 1234.0)
monkeypatch.setattr(agents_view_mod, "get_usd_per_hour", lambda _pid: None)
table, _ = _render([AgentRecord(name="claude-code", pid=8421, command="claude")])
rendered_cells = [list(col.cells)[0] for col in table.columns]
assert rendered_cells[4] == "1.2k"
def test_idle_observed_agent_renders_zero_tokens_per_min_not_dash(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""The ``0`` vs ``-`` distinction the tracker enforces must
propagate to the cell: a known-but-idle agent renders ``0``,
a never-observed agent renders ``-``.
"""
monkeypatch.setattr(agents_view_mod, "get_tokens_per_min", lambda _pid: 0.0)
monkeypatch.setattr(agents_view_mod, "get_usd_per_hour", lambda _pid: 0.0)
table, _ = _render([AgentRecord(name="claude-code", pid=8421, command="claude")])
rendered_cells = [list(col.cells)[0] for col in table.columns]
assert rendered_cells[4] == "0"
assert rendered_cells[5] == "$0.00"
def test_usd_per_hour_renders_dash_when_model_unknown(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""``get_usd_per_hour`` returns ``None`` when the model cannot
be resolved (and therefore pricing cannot be applied). The cell
must render ``-`` rather than ``$0.00`` so the user does not
misread "unknown" as "free".
"""
monkeypatch.setattr(agents_view_mod, "get_tokens_per_min", lambda _pid: 500.0)
monkeypatch.setattr(agents_view_mod, "get_usd_per_hour", lambda _pid: None)
table, _ = _render([AgentRecord(name="claude-code", pid=8421, command="claude")])
rendered_cells = [list(col.cells)[0] for col in table.columns]
# tokens/min still shows the live figure …
assert rendered_cells[4] == "500"
# … but $/hr is honest about the missing rate.
assert rendered_cells[5] == "-"
def test_multiple_records_are_each_rendered_in_order() -> None:
records = [
AgentRecord(name="claude-code", pid=8421, command="claude"),
AgentRecord(name="cursor-tab", pid=9133, command="cursor"),
AgentRecord(name="aider", pid=7702, command="aider"),
]
table, out = _render(records)
assert table.row_count == 3
# Substring order in the rendered output preserves input order:
pos_claude = out.index("claude-code")
pos_cursor = out.index("cursor-tab")
pos_aider = out.index("aider")
assert pos_claude < pos_cursor < pos_aider
# ---------------------------------------------------------------------------
# Defense against Rich-markup injection
# ---------------------------------------------------------------------------
def test_record_name_is_rich_escaped_so_markup_does_not_render() -> None:
"""An adversarial agent name containing Rich markup tags must
render literally, not interpreted. Without ``escape()``, a name
like ``[bold red]ghost[/]`` would visually mimic a styled cell
and could mask other dashboard content."""
records = [
AgentRecord(name="[bold red]ghost[/]", pid=1, command="bin"),
]
_, out = _render(records)
# Literal brackets survive in the rendered output:
assert "[bold red]ghost[/]" in out
# Resilience to a schema-invalid ``agents.yaml`` is now enforced by
# the sampler's catch-all around ``load_agents_config`` (see
# ``test_sampler.py``); the view no longer touches the config.
@@ -0,0 +1,318 @@
"""End-to-end integration tests for the ``/fleet`` token wiring (#2023).
These tests drive the *real* pipeline — provider resolver, real
:class:`ClaudeCodeJsonlSource` / :class:`CodexRolloutSource`, real
:class:`ClaudeCodeMeter` / :class:`CodexMeter`, real
:class:`TokenRateTracker`, real ``_resolved_model_for_pid``, real
``_format_tokens_per_min`` / ``_format_usd_per_hour`` — against a
fixture on disk.
The only stubs are :func:`tools.system.fleet_monitoring.probe.cwd_for_pid` and
:func:`started_at_for_pid` (no real PIDs in a unit test) and the
``AgentRegistry`` lookup (no real ``~/.opensre/agents.jsonl``).
Everything else exercises production code.
"""
from __future__ import annotations
import io
import time
from datetime import UTC, datetime
from pathlib import Path
import pytest
from rich.console import Console
from surfaces.interactive_shell.ui.agents.agents_view import _build_agents_table
from tools.system.fleet_monitoring import sampler as sampler_mod
from tools.system.fleet_monitoring.probe import ProcessSnapshot
from tools.system.fleet_monitoring.registry import AgentRecord
from tools.system.fleet_monitoring.token_rate import TOKEN_RATE_TRACKER
from tools.system.fleet_monitoring.token_sources import claude_code as claude_source_mod
from tools.system.fleet_monitoring.token_sources import codex as codex_source_mod
@pytest.fixture(autouse=True)
def _isolate_sampler_globals() -> None:
"""Same isolation pattern as the unit tests in this directory."""
sampler_mod._latest.clear()
sampler_mod._TickCache.registry_snapshot = {}
sampler_mod._TickCache.agents_config = None
for pid in list(TOKEN_RATE_TRACKER.known_pids()):
TOKEN_RATE_TRACKER.forget(pid)
def _render(records: list[AgentRecord]) -> str:
table = _build_agents_table(records)
buf = io.StringIO()
Console(file=buf, force_terminal=False, highlight=False, width=140).print(table)
return buf.getvalue()
# ---------- Claude Code end-to-end ----------------------------------
def test_claude_code_end_to_end_renders_real_tokens_and_cost(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
"""Real pipeline: psutil-stubbed cwd → real JSONL on disk →
real source → real meter → real tracker → real view formatter.
The view shows live ``tokens/min`` and ``$/hr`` cells, not ``-``.
"""
# Build a Claude Code project directory and session JSONL.
fake_cwd = tmp_path / "repo"
fake_cwd.mkdir()
projects_root = tmp_path / "claude_projects"
project_dir = projects_root / str(fake_cwd).replace("/", "-")
project_dir.mkdir(parents=True)
session = project_dir / "session-abc.jsonl"
# Start with one historical event — the source seeks past it on
# first read (no retro-pricing) so it should not contribute.
session.write_text(
'{"type":"system","subtype":"init","session_id":"abc","model":"claude-sonnet-4-5"}\n',
encoding="utf-8",
)
# Stub psutil-fenced helpers — no real PID lookups in unit tests.
monkeypatch.setattr(
"tools.system.fleet_monitoring.token_sources.claude_code.cwd_for_pid",
lambda _pid: fake_cwd,
)
# After the Greptile-driven fix, the claude source requires
# fd-level evidence to attribute a JSONL to a PID (no more
# silent fallback to newest-by-mtime — mirrors codex).
monkeypatch.setattr(
"tools.system.fleet_monitoring.token_sources.claude_code.open_files_for_pid",
lambda _pid: (session,),
)
# Wire a fresh source against the test projects root so the
# production singleton (which would hit ``~/.claude/projects/``)
# never enters this test's resolution path.
isolated_source = claude_source_mod.ClaudeCodeJsonlSource(projects_root=projects_root)
monkeypatch.setattr(
"tools.system.fleet_monitoring.sampler.get_token_source",
lambda provider: isolated_source if provider == "claude-code" else None,
)
# Registered agent: Claude Code at PID 8421.
record = AgentRecord(
name="claude-code-8421",
pid=8421,
command="claude --output-format stream-json",
provider="claude-code",
)
class _Registry:
def list(self) -> list[AgentRecord]:
return [record]
def get(self, pid: int) -> AgentRecord | None:
return record if pid == 8421 else None
monkeypatch.setattr("tools.system.fleet_monitoring.sampler.AgentRegistry", _Registry)
# Stub probe so the snapshot path is also alive (uptime/cpu%).
fake_snapshot = ProcessSnapshot(
pid=8421,
cpu_percent=18.4,
rss_mb=128.0,
num_fds=42,
num_connections=3,
status="running",
started_at=datetime(2026, 5, 14, 10, 0, 0, tzinfo=UTC),
)
monkeypatch.setattr("tools.system.fleet_monitoring.sampler.probe", lambda _pid: fake_snapshot)
# First sampler tick: cold-start the source (seeks to EOF) and
# populate registry caches. No tokens yet — historical content
# below EOF is correctly ignored.
sampler_mod._TickCache.registry_snapshot[record.pid] = record
sampler_mod._TickCache.agents_config = None
sampler_mod._sample_tokens(record)
# Append a real Claude assistant turn with usage. This is the
# delta the next sample call should pick up.
with session.open("a", encoding="utf-8") as fh:
fh.write(
'{"type":"assistant","message":{"model":"claude-sonnet-4-5",'
'"usage":{"input_tokens":200,"output_tokens":50}}}\n'
)
# Brief wait so mtime > started_at - 5s on every filesystem
# (APFS sub-second mtime quirks rarely matter, but cheap).
time.sleep(0.01)
sampler_mod._sample_tokens(record)
# Render through the real view.
out = _render([record])
# ``tokens/min`` populated: 250 tokens in a 60 s window → 250.
assert "250" in out
# ``$/hr`` populated with a real dollar figure for sonnet-4-5
# (3 USD/M input × 0.7 + 15 USD/M output × 0.3 blend ≈ 6.6e-6
# per token; 250 tok/min × 60 × 6.6e-6 ≈ $0.099). The actual
# rendering rounds to two decimals, so look for the ``$`` prefix.
assert "$0.0" in out or "$0.1" in out
# And the placeholder is gone for the metric columns.
assert "claude-code-8421" in out
def test_claude_code_end_to_end_shows_dash_when_source_resolves_nothing(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
"""When psutil cannot read the cwd (macOS hardened-runtime
denials, cross-user processes), the dashboard stays honest:
``tokens/min`` and ``$/hr`` render ``-`` rather than ``0`` or a
bogus number.
"""
monkeypatch.setattr(
"tools.system.fleet_monitoring.token_sources.claude_code.cwd_for_pid",
lambda _pid: None,
)
isolated_source = claude_source_mod.ClaudeCodeJsonlSource(
projects_root=tmp_path / "claude_projects",
)
monkeypatch.setattr(
"tools.system.fleet_monitoring.sampler.get_token_source",
lambda provider: isolated_source if provider == "claude-code" else None,
)
record = AgentRecord(
name="claude-code-8421",
pid=8421,
command="claude",
provider="claude-code",
)
class _Registry:
def list(self) -> list[AgentRecord]:
return [record]
def get(self, pid: int) -> AgentRecord | None:
return record if pid == 8421 else None
monkeypatch.setattr("tools.system.fleet_monitoring.sampler.AgentRegistry", _Registry)
monkeypatch.setattr("tools.system.fleet_monitoring.sampler.probe", lambda _pid: None)
sampler_mod._TickCache.registry_snapshot[record.pid] = record
sampler_mod._sample_tokens(record)
sampler_mod._sample_tokens(record)
# Drive the real render and inspect the actual table cells rather
# than substring-matching the printed form (which contains the
# ``$/hr`` header literal).
table = _build_agents_table([record])
assert table.row_count == 1
rendered_cells = [list(col.cells)[0] for col in table.columns]
# cells[0]=agent, [1]=pid, [2..6]=uptime/cpu/tokens/min/$/hr/status.
# Every metric cell — including ``$/hr`` — falls back to ``-``.
assert rendered_cells[2:] == ["-", "-", "-", "-", "-"]
# The printed form should still include the agent name (so
# rendering didn't silently bail).
out = _render([record])
assert "claude-code-8421" in out
# ---------- Codex end-to-end ----------------------------------------
def test_codex_end_to_end_renders_real_tokens_and_cost(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
"""Mirrors the Claude Code test for the Codex rollout source."""
codex_home = tmp_path / "codex_home"
started_at_epoch = time.time() - 30 # process started 30 s ago
started_dt = datetime.fromtimestamp(started_at_epoch, tz=UTC)
rollout_dir = (
codex_home
/ "sessions"
/ started_dt.strftime("%Y")
/ started_dt.strftime("%m")
/ started_dt.strftime("%d")
)
rollout_dir.mkdir(parents=True)
rollout = rollout_dir / "rollout-001-abc.jsonl"
rollout.write_text(
'{"type":"thread.started","model":"gpt-5-codex"}\n{"type":"turn.started"}\n',
encoding="utf-8",
)
import os as _os
_os.utime(rollout, (started_at_epoch + 1, started_at_epoch + 1))
monkeypatch.setattr(
"tools.system.fleet_monitoring.token_sources.codex.started_at_for_pid",
lambda _pid: started_at_epoch,
)
# Source now requires fd-level confirmation that the PID owns
# the rollout (production: each codex holds its rollout fd open).
monkeypatch.setattr(
"tools.system.fleet_monitoring.token_sources.codex.open_files_for_pid",
lambda _pid: (rollout,),
)
isolated_source = codex_source_mod.CodexRolloutSource(codex_home=codex_home)
monkeypatch.setattr(
"tools.system.fleet_monitoring.sampler.get_token_source",
lambda provider: isolated_source if provider == "codex" else None,
)
record = AgentRecord(
name="codex-9999",
pid=9999,
command="codex exec",
provider="codex",
)
class _Registry:
def list(self) -> list[AgentRecord]:
return [record]
def get(self, pid: int) -> AgentRecord | None:
return record if pid == 9999 else None
monkeypatch.setattr("tools.system.fleet_monitoring.sampler.AgentRegistry", _Registry)
fake_snapshot = ProcessSnapshot(
pid=9999,
cpu_percent=4.2,
rss_mb=64.0,
num_fds=20,
num_connections=1,
status="running",
started_at=started_dt,
)
monkeypatch.setattr("tools.system.fleet_monitoring.sampler.probe", lambda _pid: fake_snapshot)
sampler_mod._TickCache.registry_snapshot[record.pid] = record
sampler_mod._TickCache.agents_config = None
sampler_mod._sample_tokens(record)
# Append a real per-turn token_count event (the on-disk format
# codex-cli 0.130.0 writes — distinct from ``codex exec --json``
# stdout). ``turn_context`` carries the model; ``event_msg`` with
# ``payload.type == "token_count"`` carries ``last_token_usage``.
with rollout.open("a", encoding="utf-8") as fh:
fh.write(
'{"type":"turn_context","payload":{"turn_id":"t_1","model":"gpt-5-codex"}}\n'
'{"type":"event_msg","payload":{"type":"token_count","info":'
'{"last_token_usage":'
'{"input_tokens":175,"cached_input_tokens":0,'
'"output_tokens":50,"reasoning_output_tokens":0,"total_tokens":225}}}}\n'
)
time.sleep(0.01)
sampler_mod._sample_tokens(record)
out = _render([record])
# 225 tokens (input + output) over a 60 s window.
assert "225" in out
# Real cost figure from the gpt-5-codex price table.
assert "$0.0" in out or "$0.1" in out
assert "codex-9999" in out
+171
View File
@@ -0,0 +1,171 @@
"""Tests for the interactive-shell launch banner."""
from __future__ import annotations
import io
from rich.console import Console
from surfaces.interactive_shell.ui.banner import banner as banner_module
from surfaces.interactive_shell.ui.banner import banner_state as banner_state_module
from surfaces.interactive_shell.ui.components import rendering as rendering_module
def test_banner_shows_ollama_model(monkeypatch: object) -> None:
monkeypatch.setenv("LLM_PROVIDER", "ollama")
monkeypatch.setenv("OLLAMA_MODEL", "qwen2.5:7b")
console_file = io.StringIO()
console = Console(file=console_file, force_terminal=False, highlight=False)
banner_module.render_banner(console)
output = console_file.getvalue()
assert "ollama" in output
assert "qwen2.5:7b" in output
assert "ollama · default" not in output
def test_ready_box_uses_active_theme_palette() -> None:
from platform.terminal.theme import set_active_theme
set_active_theme("pink")
pink_rgb = "255;179;217"
green_rgb = "185;237;175"
console = Console(record=True, width=120)
console.print(banner_module.build_ready_panel(console))
styled = console.export_text(styles=True)
assert pink_rgb in styled
assert green_rgb not in styled
def test_splash_gradient_style_for_gradient_theme() -> None:
from platform.terminal.theme import set_active_theme
set_active_theme("webflux")
assert banner_module._splash_block_style(1, 3) == "bold #864C96"
assert banner_module._splash_block_style(0, 1) == "bold #E23636"
def test_splash_gradient_style_falls_back_to_highlight_without_gradient() -> None:
from platform.terminal.theme import HIGHLIGHT, set_active_theme
set_active_theme("nord")
assert banner_module._splash_block_style(0, 1) == f"bold {HIGHLIGHT}"
def test_refresh_welcome_poster_uses_repl_safe_render(monkeypatch: object) -> None:
console = Console(record=True, width=120)
render_calls: list[dict[str, object | None]] = []
monkeypatch.setattr(
"surfaces.interactive_shell.ui.components.rendering.repl_clear_screen",
lambda: None,
)
def _fake_render(
_console: Console,
*,
session: object = None,
theme_notice: str | None = None,
) -> None:
render_calls.append({"session": session, "theme_notice": theme_notice})
monkeypatch.setattr(
"surfaces.interactive_shell.ui.components.rendering.repl_render_launch_poster",
_fake_render,
)
rendering_module.refresh_welcome_poster(console, session="sess", theme_notice="pink")
assert render_calls == [{"session": "sess", "theme_notice": "pink"}]
def test_get_username_prefers_github_handle(monkeypatch: object) -> None:
monkeypatch.setattr(banner_module, "_github_username", lambda: "octocat")
monkeypatch.setattr(banner_module.getpass, "getuser", lambda: "system-user")
assert banner_module._get_username() == "octocat"
def test_get_username_falls_back_to_system_user(monkeypatch: object) -> None:
monkeypatch.setattr(banner_module, "_github_username", lambda: "")
monkeypatch.setattr(banner_module.getpass, "getuser", lambda: "system-user")
assert banner_module._get_username() == "system-user"
def test_github_username_reads_saved_credential(monkeypatch: object) -> None:
monkeypatch.setattr(
"integrations.store.get_integration",
lambda service: {"credentials": {"username": "octocat"}} if service == "github" else None,
)
assert banner_module._github_username() == "octocat"
def test_github_username_empty_when_not_configured(monkeypatch: object) -> None:
monkeypatch.setattr("integrations.store.get_integration", lambda _service: None)
assert banner_module._github_username() == ""
def test_github_username_survives_identity_import_failure(monkeypatch: object) -> None:
import builtins
real_import = builtins.__import__
def _fail_github_identity(name: str, *args: object, **kwargs: object) -> object:
if name == "integrations.github.identity":
raise ImportError("simulated heavy import failure")
return real_import(name, *args, **kwargs)
monkeypatch.setattr(builtins, "__import__", _fail_github_identity)
assert banner_module._github_username() == ""
def test_ambient_column_marks_incomplete_integration(monkeypatch: object) -> None:
# A hosted MCP record saved without an API token is "present" but cannot
# connect; the banner must mark it rather than imply it works.
monkeypatch.setattr(
banner_state_module,
"_load_integration_health",
lambda: [("Sentry", "ok"), ("Posthog_Mcp", "incomplete")],
)
monkeypatch.setattr(banner_state_module, "_is_alert_listener_active", lambda: False)
text = banner_state_module._build_ambient_right_column().plain
assert "Sentry" in text
assert "Posthog_Mcp ⚠" in text
assert "⚠ incomplete — run /integrations verify" in text
def test_ambient_column_no_warning_when_all_healthy(monkeypatch: object) -> None:
monkeypatch.setattr(
banner_state_module,
"_load_integration_health",
lambda: [("Sentry", "ok"), ("GitHub", "ok")],
)
monkeypatch.setattr(banner_state_module, "_is_alert_listener_active", lambda: False)
text = banner_state_module._build_ambient_right_column().plain
assert "Sentry" in text
assert "GitHub" in text
assert "" not in text
def test_ready_box_expands_to_console_width() -> None:
console_file = io.StringIO()
console = Console(file=console_file, force_terminal=False, highlight=False, width=120)
banner_module.render_ready_box(console)
lines = [
line for line in console_file.getvalue().splitlines() if line.startswith(("", "", ""))
]
assert lines
assert max(len(line) for line in lines) == 120
@@ -0,0 +1,122 @@
"""Tests for inline raw-terminal choice menu rendering."""
from __future__ import annotations
import io
import re
import sys
from types import SimpleNamespace
from surfaces.interactive_shell.ui.components import choice_menu
_ANSI_RE = re.compile(r"\x1b\[[0-9;:]*[A-Za-z]")
def test_draw_menu_uses_carriage_return_newlines(monkeypatch) -> None:
"""Raw-mode terminals do not translate LF to CRLF for us.
Plain ``\n`` makes each line begin at the previous line's ending column,
which renders the picker as a diagonal staircase. The inline menu should
write explicit ``\r\n`` newlines and reset to column zero for every row.
"""
out = io.StringIO()
monkeypatch.setattr(sys, "stdout", out)
monkeypatch.setattr(choice_menu, "_cols", lambda: 80)
choice_menu._draw_menu(
title="integrations",
crumb="/integrations",
labels=["/integrations list", "/integrations verify"],
index=0,
erase_lines=0,
)
rendered = out.getvalue()
plain = _ANSI_RE.sub("", rendered)
assert "\n" in rendered
assert all(rendered[index - 1] == "\r" for index, char in enumerate(rendered) if char == "\n")
assert "\rintegrations" in plain
assert "\r/integrations" in plain
assert "\r > /integrations list" in plain
def test_erase_menu_block_resets_to_column_zero(monkeypatch) -> None:
out = io.StringIO()
monkeypatch.setattr(sys, "stdout", out)
choice_menu._erase_menu("crumb", ["one", "two"])
rendered = out.getvalue()
assert rendered.startswith("\r\x1b[")
assert "A\r\x1b[J" in rendered
assert rendered.endswith("\r")
def test_reset_tty_column_writes_carriage_return(monkeypatch) -> None:
out = io.StringIO()
monkeypatch.setattr(sys, "stdout", out)
choice_menu.reset_tty_column()
assert out.getvalue() == "\r"
def test_pick_ignores_unmapped_keys(monkeypatch) -> None:
out = io.StringIO()
actions = iter(["ignore", "enter"])
monkeypatch.setattr(sys, "stdout", out)
monkeypatch.setattr(choice_menu, "_cols", lambda: 80)
monkeypatch.setattr(choice_menu, "_read_action", lambda: next(actions))
assert choice_menu._pick(title="test", crumb="", labels=["one"]) == 0
rendered = out.getvalue()
assert rendered.count("test") == 2
assert "A\r\x1b[J" in rendered
def test_read_action_treats_space_as_enter(monkeypatch) -> None:
monkeypatch.setattr(choice_menu.os, "name", "nt")
monkeypatch.setitem(sys.modules, "msvcrt", SimpleNamespace(getch=lambda: b" "))
assert choice_menu._read_action() == "enter"
def test_read_action_treats_right_arrow_as_enter(monkeypatch) -> None:
keys = iter([b"\xe0", b"M"])
monkeypatch.setattr(choice_menu.os, "name", "nt")
monkeypatch.setitem(sys.modules, "msvcrt", SimpleNamespace(getch=lambda: next(keys)))
assert choice_menu._read_action() == "enter"
def test_repl_choose_one_starts_at_initial_value(monkeypatch) -> None:
out = io.StringIO()
actions = iter(["enter"])
monkeypatch.setattr(choice_menu, "repl_tty_interactive", lambda: True)
monkeypatch.setattr(
"surfaces.interactive_shell.ui.components.cpr_stdin.drain_stale_cpr_bytes",
lambda: None,
)
monkeypatch.setattr(sys, "stdout", out)
monkeypatch.setattr(choice_menu, "_cols", lambda: 80)
monkeypatch.setattr(choice_menu, "_read_action", lambda: next(actions))
result = choice_menu.repl_choose_one(
title="theme",
breadcrumb="/theme",
choices=[("green", "green"), ("blue", "blue (current)"), ("pink", "pink")],
initial_value="blue",
)
assert result == "blue"
plain = _ANSI_RE.sub("", out.getvalue())
assert "> blue (current)" in plain
def test_read_action_ignores_left_arrow(monkeypatch) -> None:
keys = iter([b"\xe0", b"K"])
monkeypatch.setattr(choice_menu.os, "name", "nt")
monkeypatch.setitem(sys.modules, "msvcrt", SimpleNamespace(getch=lambda: next(keys)))
assert choice_menu._read_action() == "ignore"
@@ -0,0 +1,34 @@
"""Tests for CPR stdin hygiene helpers."""
from __future__ import annotations
import pytest
from surfaces.interactive_shell.ui.components.cpr_stdin import (
contains_cpr_sequence,
strip_cpr_sequences,
)
@pytest.mark.parametrize(
("text", "expected"),
[
("\x1b[32;1R", ""),
("[32;1R", ""),
("\x9b32;1R", ""),
("what is our current model?[32;1R", "what is our current model?"),
("before \x1b[12;80R after", "before after"),
("7R[25;57R23;57R", ""),
("25;57R", ""),
],
)
def test_strip_cpr_sequences_removes_terminal_cursor_replies(
text: str,
expected: str,
) -> None:
assert strip_cpr_sequences(text) == expected
def test_contains_cpr_sequence_detects_leaked_bytes() -> None:
assert contains_cpr_sequence("\x1b[12;80R")
assert not contains_cpr_sequence("plain prompt text")
@@ -0,0 +1,157 @@
"""Tests for the execution-policy interaction layer (``execution_allowed``).
These cover the terminal-facing half of the execution gate: console output and
the confirmation prompt. The pure decision is tested in
``tests/tools/interactive_shell/shared/test_execution_policy.py``.
"""
from __future__ import annotations
import io
from rich.console import Console
from surfaces.interactive_shell.session import Session
from surfaces.interactive_shell.ui.execution_confirm import execution_allowed
from tools.interactive_shell.shared import (
ExecutionPolicyResult,
allow_tool,
)
def _ask_result() -> ExecutionPolicyResult:
"""An explicit ``ask`` verdict (the default policy no longer emits these)."""
return ExecutionPolicyResult(
verdict="ask",
tool_type="slash",
reason="this command may change configuration or run heavy work",
)
# --- execution_allowed: default-allow runs without prompting ----------------
def test_allow_verdict_runs_without_prompt() -> None:
session = Session()
buf = io.StringIO()
console = Console(file=buf, force_terminal=False)
def _confirm(_: str) -> str: # pragma: no cover - must never be called
raise AssertionError("default-allow must not prompt for confirmation")
r = allow_tool("slash")
assert execution_allowed(
r,
session=session,
console=console,
action_summary="/integrations verify foo",
confirm_fn=_confirm,
is_tty=True,
)
assert "Confirm" not in buf.getvalue()
def test_non_tty_allows_default_policy() -> None:
"""Default-allow no longer fails closed on non-interactive stdin."""
session = Session()
buf = io.StringIO()
console = Console(file=buf, force_terminal=False)
r = allow_tool("slash")
assert execution_allowed(
r,
session=session,
console=console,
action_summary="/save out.md",
is_tty=False,
)
def test_deny_verdict_blocks() -> None:
session = Session()
buf = io.StringIO()
console = Console(file=buf, force_terminal=False)
# The default policy never emits a deny; construct one explicitly to cover
# the execution_allowed deny path.
r = ExecutionPolicyResult(
verdict="deny",
tool_type="shell",
reason="empty command.",
hint="Enter a command to run.",
)
assert not execution_allowed(
r,
session=session,
console=console,
action_summary="!",
is_tty=True,
)
assert "blocked" in buf.getvalue()
# --- Retained ask machinery (reachable only via explicit ask) ---------------
def test_explicit_ask_trust_mode_allows() -> None:
session = Session()
session.terminal.trust_mode = True
buf = io.StringIO()
console = Console(file=buf, force_terminal=False)
assert execution_allowed(
_ask_result(),
session=session,
console=console,
action_summary="/investigate x",
confirm_fn=lambda _: "n",
is_tty=True,
)
def test_explicit_ask_non_tty_blocks() -> None:
session = Session()
session.terminal.trust_mode = False
buf = io.StringIO()
console = Console(file=buf, force_terminal=False)
assert not execution_allowed(
_ask_result(),
session=session,
console=console,
action_summary="/save out.md",
is_tty=False,
)
assert "not a TTY" in buf.getvalue()
def test_explicit_ask_tty_accepts_empty_confirmation() -> None:
session = Session()
buf = io.StringIO()
console = Console(file=buf, force_terminal=False)
captured: list[str] = []
def _confirm(prompt: str) -> str:
captured.append(prompt)
return ""
assert execution_allowed(
_ask_result(),
session=session,
console=console,
action_summary="/integrations verify foo",
confirm_fn=_confirm,
is_tty=True,
)
assert captured == ["Proceed? [Y/n] "]
def test_explicit_ask_tty_rejects_explicit_no() -> None:
session = Session()
buf = io.StringIO()
console = Console(file=buf, force_terminal=False)
assert not execution_allowed(
_ask_result(),
session=session,
console=console,
action_summary="/integrations verify foo",
confirm_fn=lambda _: "n",
is_tty=True,
)
assert "cancelled" in buf.getvalue()
+135
View File
@@ -0,0 +1,135 @@
"""Tests for post-investigation feedback UI."""
from __future__ import annotations
import io
import os
import pytest
from rich.console import Console
from surfaces.interactive_shell.ui.feedback import (
_CHOICES,
_format_root_cause_lines,
_print_context,
_root_cause_width,
_run_select,
)
def _fixed_terminal_size(*_args: object, **_kwargs: object) -> os.terminal_size:
return os.terminal_size((60, 24))
def _wide_terminal_size(*_args: object, **_kwargs: object) -> os.terminal_size:
return os.terminal_size((160, 24))
def test_root_cause_width_uses_full_terminal_not_capped(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr("shutil.get_terminal_size", _wide_terminal_size)
assert _root_cause_width(console=None) == 160
def test_print_context_stdout_uses_full_terminal_width(
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
) -> None:
monkeypatch.setattr("shutil.get_terminal_size", _wide_terminal_size)
root = "Schema validation failed because payment_method is missing."
_print_context({"root_cause": root}, console=None)
captured = capsys.readouterr().out
assert captured.startswith("\n" + "" * 160 + "\n")
assert captured.rstrip().endswith("" * 160)
def test_format_root_cause_lines_wraps_long_text_without_truncation() -> None:
root = (
"The Kubernetes job 'etl-transform-error' for pipeline "
"'kubernetes_etl_pipeline' failed in namespace 'tracer-test' because "
"schema validation requires 'payment_method'."
)
lines = _format_root_cause_lines(root, cols=60)
assert len(lines) > 1
assert all("" not in line for line in lines)
assert " ".join(line.strip() for line in lines) == f"Root cause: {root}"
def test_print_context_shows_full_root_cause_in_rich_path() -> None:
root = (
"The Kubernetes job 'etl-transform-error' for pipeline "
"'kubernetes_etl_pipeline' failed because schema validation requires "
"'payment_method'."
)
buf = io.StringIO()
console = Console(file=buf, force_terminal=False, width=60)
_print_context({"root_cause": root}, console=console)
output = buf.getvalue()
assert root in output
assert "" not in output
def test_print_context_escapes_rich_markup_in_root() -> None:
root = "Pod restart [1/3] failed because schema validation requires 'payment_method'."
buf = io.StringIO()
console = Console(file=buf, force_terminal=False, width=80)
_print_context({"root_cause": root}, console=console)
output = buf.getvalue()
assert "[1/3]" in output
assert "" not in output
def test_print_context_shows_full_root_cause_in_stdout_path(
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
) -> None:
monkeypatch.setattr("shutil.get_terminal_size", _fixed_terminal_size)
root = (
"The Kubernetes job 'etl-transform-error' for pipeline "
"'kubernetes_etl_pipeline' failed because schema validation requires "
"'payment_method'."
)
_print_context({"root_cause": root}, console=None)
captured = capsys.readouterr().out
assert "" not in captured
assert " ".join(captured.split()) == ("" * 60 + " Root cause: " + root + " " + "" * 60)
def test_run_select_returns_highlighted_choice_on_enter(
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
) -> None:
monkeypatch.setattr("sys.stdin.isatty", lambda: True)
monkeypatch.setattr("sys.stdout.isatty", lambda: True)
calls = {"n": 0}
def read_then_enter(**_kwargs: object) -> str:
calls["n"] += 1
return "enter" if calls["n"] > 1 else "down"
monkeypatch.setattr(
"surfaces.interactive_shell.ui.feedback.read_key_unix",
read_then_enter,
)
monkeypatch.setattr("surfaces.interactive_shell.ui.feedback.flush_stdin_unix", lambda: None)
monkeypatch.setattr(
"surfaces.interactive_shell.ui.feedback.restore_stdin_terminal",
lambda: None,
)
assert _run_select(_CHOICES) == "partial"
captured = capsys.readouterr().out
assert "Partially accurate" in captured
assert "↑↓" in captured
@@ -0,0 +1,359 @@
"""Tests for the grouped /help picker renderer."""
from __future__ import annotations
import io
import re
import sys
from platform.terminal import theme as ui_theme
from surfaces.interactive_shell.command_registry.types import SlashCommand
from surfaces.interactive_shell.ui.help import help_menu
_ANSI_RE = re.compile(r"\x1b\[[0-9;:]*[A-Za-z]")
def _cmd(name: str, description: str | None = None) -> SlashCommand:
return SlashCommand(name, description or f"{name} description", lambda *_args: True)
def test_flatten_help_rows_preserves_category_headers() -> None:
rows = help_menu._flatten_help_rows(
[
("Session", [_cmd("/status")]),
("Tasks", [_cmd("/tasks"), _cmd("/cancel")]),
]
)
assert [row.section for row in rows if row.section is not None] == ["Session", "Tasks"]
assert sum(1 for row in rows if row.separator) == 1
assert [row.command.name for row in rows if row.command is not None] == [
"/status",
"/tasks",
"/cancel",
]
def test_navigation_skips_category_headers() -> None:
rows = help_menu._flatten_help_rows(
[
("Session", [_cmd("/status")]),
("Tasks", [_cmd("/tasks")]),
]
)
assert help_menu._first_selectable_index(rows) == 1
assert help_menu._next_selectable_index(rows, 1, 1) == 4
assert help_menu._next_selectable_index(rows, 4, -1) == 1
def test_draw_help_menu_renders_horizontal_category_dividers(monkeypatch) -> None:
rows = help_menu._flatten_help_rows(
[
("Session", [_cmd("/status")]),
("Tasks", [_cmd("/tasks")]),
]
)
out = io.StringIO()
monkeypatch.setattr(sys, "stdout", out)
monkeypatch.setattr(help_menu, "menu_columns", lambda: 40)
help_menu._draw_help_menu(
rows,
selected=1,
expanded=None,
erase_lines=0,
viewport_height=7,
)
plain = _ANSI_RE.sub("", out.getvalue())
assert "Session" in plain
assert "Tasks" in plain
assert plain.count(help_menu._separator_rule(40)) >= 2
assert "" in plain
assert help_menu._render_grid_row("Session", "", 40) in plain
assert help_menu._render_grid_row("Tasks", "", 40) in plain
def test_section_rows_keep_divider_dim() -> None:
rendered = help_menu._render_help_row(
help_menu.HelpRow(section="Investigation"),
selected=False,
expanded=False,
width=40,
)
assert f"{ui_theme.BOLD_BRAND_ANSI}Investigation" in rendered
assert f"{ui_theme.DIM_COUNTER_ANSI}" in rendered
def test_detail_rows_use_text_labels_dim_values_and_dim_divider() -> None:
label = help_menu._render_display_row(
help_menu.HelpDisplayRow(detail=help_menu.HelpDetailLine("usage:", "label")),
selected=False,
expanded=False,
width=40,
)
value = help_menu._render_display_row(
help_menu.HelpDisplayRow(detail=help_menu.HelpDetailLine(" /help", "value")),
selected=False,
expanded=False,
width=40,
)
assert f"{ui_theme.DIM_COUNTER_ANSI}{' ' * help_menu._left_column_width(40)}" in label
assert f"{ui_theme.TEXT_ANSI}usage:" in label
assert f"{ui_theme.DIM_COUNTER_ANSI}{' ' * help_menu._left_column_width(40)}" in value
assert f"{ui_theme.DIM_COUNTER_ANSI} /help" in value
def test_draw_help_menu_centers_title(monkeypatch) -> None:
rows = help_menu._flatten_help_rows([("Session", [_cmd("/status")])])
out = io.StringIO()
monkeypatch.setattr(sys, "stdout", out)
monkeypatch.setattr(help_menu, "menu_columns", lambda: 40)
help_menu._draw_help_menu(
rows,
selected=1,
expanded=None,
erase_lines=0,
viewport_height=7,
)
plain_lines = _ANSI_RE.sub("", out.getvalue()).splitlines()
assert " Slash commands " in plain_lines
def test_draw_help_menu_uses_bounded_viewport_and_category_context(monkeypatch) -> None:
sections = [
("Session", [_cmd(f"/session-{index}") for index in range(10)]),
("Tasks", [_cmd(f"/task-{index}") for index in range(10)]),
]
rows = help_menu._flatten_help_rows(sections)
selected = next(
index for index, row in enumerate(rows) if row.command and row.command.name == "/task-2"
)
out = io.StringIO()
monkeypatch.setattr(sys, "stdout", out)
monkeypatch.setattr(help_menu, "menu_columns", lambda: 90)
height = help_menu._draw_help_menu(
rows,
selected=selected,
expanded=None,
erase_lines=0,
viewport_height=7,
)
rendered = out.getvalue()
plain = _ANSI_RE.sub("", rendered)
assert height == 13
assert "Tasks" in plain
assert "/task-2" in plain
assert "/session-0" not in plain
assert "/task-9" not in plain
def test_draw_help_menu_expands_selected_command_inline(monkeypatch) -> None:
command = SlashCommand(
"/new",
"Start a new session keeping conversation context.",
lambda *_args: True,
usage=("/new",),
notes=("Unlike /clear, /new preserves the LLM conversation context.",),
)
rows = help_menu._flatten_help_rows([("Session", [command])])
out = io.StringIO()
monkeypatch.setattr(sys, "stdout", out)
monkeypatch.setattr(help_menu, "menu_columns", lambda: 90)
help_menu._draw_help_menu(
rows,
selected=1,
expanded=1,
erase_lines=0,
viewport_height=7,
)
plain = _ANSI_RE.sub("", out.getvalue())
assert "/new" in plain
assert "> ▾ /new" in plain
assert "│ Start a new session" in plain
assert help_menu._render_grid_row("", "usage:", 90) in plain
assert "usage:" in plain
assert "LLM conversation context" in plain
def test_draw_help_menu_marks_expandable_and_plain_commands(monkeypatch) -> None:
expandable = SlashCommand(
"/trust",
"Manage trust mode.",
lambda *_args: True,
usage=("/trust on", "/trust off"),
)
plain_command = _cmd("/status", "Show session status.")
rows = help_menu._flatten_help_rows([("Session", [expandable, plain_command])])
out = io.StringIO()
monkeypatch.setattr(sys, "stdout", out)
monkeypatch.setattr(help_menu, "menu_columns", lambda: 90)
help_menu._draw_help_menu(
rows,
selected=1,
expanded=None,
erase_lines=0,
viewport_height=7,
)
plain = _ANSI_RE.sub("", out.getvalue())
assert "> ▸ /trust" in plain
assert "│ Manage trust mode." in plain
assert " /status" in plain
assert "▸ /status" not in plain
def test_command_rows_highlight_only_unselected_command_name() -> None:
rendered = help_menu._render_command_row(
SlashCommand(
"/trust",
"Manage trust mode.",
lambda *_args: True,
usage=("/trust on", "/trust off"),
),
selected=False,
expanded=False,
width=60,
)
assert (
f"{ui_theme.DIM_COUNTER_ANSI}{ui_theme.ANSI_RESET}{ui_theme.HIGHLIGHT_ANSI}/trust"
) in rendered
assert f"{ui_theme.HIGHLIGHT_ANSI}" not in rendered
def test_help_menu_command_rows_track_active_theme() -> None:
from platform.terminal.theme import set_active_theme
set_active_theme("green")
green_highlight = ui_theme.HIGHLIGHT_ANSI
set_active_theme("purple")
rendered = help_menu._render_command_row(
SlashCommand("/trust", "Manage trust mode.", lambda *_args: True),
selected=False,
expanded=False,
width=60,
)
assert ui_theme.HIGHLIGHT_ANSI in rendered
assert green_highlight not in rendered
def test_expanded_detail_lines_include_all_usage_examples_and_notes() -> None:
command = SlashCommand(
"/model",
"Show model settings.",
lambda *_args: True,
usage=tuple(f"/model action {index}" for index in range(10)),
examples=("/model set openai",),
notes=("Provider defaults can be restored.",),
)
lines = help_menu._expanded_detail_lines(command)
text_lines = [line.text for line in lines]
assert lines[0].role == "label"
assert lines[1].role == "value"
assert "/model action 0" in lines[1].text
assert " /model action 9" in text_lines
assert "examples:" in text_lines
assert " /model set openai" in text_lines
assert "notes:" in text_lines
assert " Provider defaults can be restored." in text_lines
assert "" not in text_lines
def test_draw_help_menu_expands_viewport_to_show_all_details(monkeypatch) -> None:
command = SlashCommand(
"/model",
"Show model settings.",
lambda *_args: True,
usage=tuple(f"/model action {index}" for index in range(10)),
)
rows = help_menu._flatten_help_rows([("Models", [command, _cmd("/status")])])
out = io.StringIO()
monkeypatch.setattr(sys, "stdout", out)
monkeypatch.setattr(help_menu, "menu_columns", lambda: 90)
height = help_menu._draw_help_menu(
rows,
selected=1,
expanded=1,
erase_lines=0,
viewport_height=7,
)
plain = _ANSI_RE.sub("", out.getvalue())
assert height > 13
assert "/model action 0" in plain
assert "/model action 9" in plain
assert "" not in plain
def test_choose_help_command_space_toggles_inline_details_and_exits(monkeypatch) -> None:
sections = [
(
"Session",
[
_cmd("/status"),
SlashCommand(
"/trust",
"Manage trust mode.",
lambda *_args: True,
usage=("/trust on", "/trust off"),
),
],
)
]
out = io.StringIO()
actions = iter(["down", "space", "cancel"])
monkeypatch.setattr(sys, "stdout", out)
monkeypatch.setattr(help_menu, "menu_columns", lambda: 80)
monkeypatch.setattr(help_menu, "read_menu_action", lambda: next(actions))
selected = help_menu.choose_help_command(sections)
assert selected is None
rendered = out.getvalue()
assert "\x1b[" in rendered
assert "A\r\x1b[J" in rendered
plain = _ANSI_RE.sub("", rendered)
assert "/trust on" in plain
def test_choose_help_command_enter_selects_command_without_details(monkeypatch) -> None:
sections = [("Session", [_cmd("/status")])]
out = io.StringIO()
actions = iter(["enter"])
monkeypatch.setattr(sys, "stdout", out)
monkeypatch.setattr(help_menu, "menu_columns", lambda: 80)
monkeypatch.setattr(help_menu, "read_menu_action", lambda: next(actions))
assert help_menu.choose_help_command(sections) == "/status"
plain = _ANSI_RE.sub("", out.getvalue())
assert "No additional usage." not in plain
def test_choose_help_command_ignores_unknown_actions(monkeypatch) -> None:
sections = [("Session", [_cmd("/status")])]
out = io.StringIO()
actions = iter(["ignore", "cancel"])
monkeypatch.setattr(sys, "stdout", out)
monkeypatch.setattr(help_menu, "menu_columns", lambda: 80)
monkeypatch.setattr(help_menu, "read_menu_action", lambda: next(actions))
assert help_menu.choose_help_command(sections) is None
rendered = out.getvalue()
assert rendered.count("Slash commands") == 2
@@ -0,0 +1,316 @@
"""Tests for incoming alert rendering in the REPL."""
from __future__ import annotations
from datetime import UTC, datetime, timedelta
from rich.console import Console
from core.domain.alerts.inbox import AlertInbox, IncomingAlert
from surfaces.interactive_shell.runtime import Session
from surfaces.interactive_shell.ui.alerts import (
drain_and_render_incoming,
format_incoming_alert,
time_ago,
)
class TestTimeAgo:
"""Test time_ago helper."""
def test_seconds_ago(self) -> None:
now = datetime.now(UTC)
then = now - timedelta(seconds=5)
result = time_ago(then)
assert "5s ago" in result
def test_one_second_ago(self) -> None:
now = datetime.now(UTC)
then = now - timedelta(seconds=1)
result = time_ago(then)
assert "1s ago" in result
def test_minutes_ago(self) -> None:
now = datetime.now(UTC)
then = now - timedelta(minutes=3)
result = time_ago(then)
assert "3m ago" in result
def test_hours_ago(self) -> None:
now = datetime.now(UTC)
then = now - timedelta(hours=2)
result = time_ago(then)
assert "2h ago" in result
def test_days_ago(self) -> None:
now = datetime.now(UTC)
then = now - timedelta(days=1)
result = time_ago(then)
assert "1d ago" in result
def test_none_datetime(self) -> None:
result = time_ago(None)
assert result == "unknown"
class TestFormatIncomingAlert:
"""Test format_incoming_alert rendering."""
def test_renders_with_all_fields(self) -> None:
alert = IncomingAlert(
text="disk usage at 95%",
alert_name="disk_alert",
severity="critical",
source="datadog-webhook",
received_at=datetime.now(UTC),
)
renderable = format_incoming_alert(alert)
# Just verify the alert object was created without error
assert renderable is not None
def test_renders_with_minimal_fields(self) -> None:
alert = IncomingAlert(text="something happened")
renderable = format_incoming_alert(alert)
# Just verify the alert object was created without error
assert renderable is not None
def test_renders_without_source(self) -> None:
alert = IncomingAlert(
text="test alert",
severity="warning",
received_at=datetime.now(UTC),
)
renderable = format_incoming_alert(alert)
# Just verify the alert object was created without error
assert renderable is not None
def test_renders_without_severity(self) -> None:
alert = IncomingAlert(
text="test alert",
source="custom",
received_at=datetime.now(UTC),
)
renderable = format_incoming_alert(alert)
# Just verify the alert object was created without error
assert renderable is not None
def test_escapes_severity_markup(self) -> None:
alert = IncomingAlert(
text="payload",
severity="critical] [red]pwned",
source="webhook",
received_at=datetime.now(UTC),
)
console = Console(record=True)
console.print(format_incoming_alert(alert))
output = console.export_text()
assert "[critical] [red]pwned]" in output
assert "pwned]" in output
def test_severity_like_rich_style_tag_is_literal(self) -> None:
"""Severity values resembling Rich markup must not apply styles."""
alert = IncomingAlert(
text="body",
severity="bold red",
received_at=datetime.now(UTC),
)
console = Console(record=True)
console.print(format_incoming_alert(alert))
output = console.export_text()
assert "[bold red]" in output
class TestDrainAndRenderIncoming:
"""Test drain_and_render_incoming functionality."""
def test_drains_fifo_order(self) -> None:
session = Session()
inbox = AlertInbox(maxsize=10)
console = Console()
# Add alerts in order
alert1 = IncomingAlert(text="first")
alert2 = IncomingAlert(text="second")
alert3 = IncomingAlert(text="third")
inbox.put(alert1)
inbox.put(alert2)
inbox.put(alert3)
# Drain and render
count = drain_and_render_incoming(session, console, inbox)
assert count == 3
assert len(session.alerts.entries) == 3
assert session.alerts.entries[0].text == "first"
assert session.alerts.entries[1].text == "second"
assert session.alerts.entries[2].text == "third"
def test_records_in_history(self) -> None:
session = Session()
inbox = AlertInbox(maxsize=10)
console = Console()
alert = IncomingAlert(text="test alert")
inbox.put(alert)
drain_and_render_incoming(session, console, inbox)
# Check history
assert len(session.history) == 1
assert session.history[0]["type"] == "incoming_alert"
assert session.history[0]["text"] == "test alert"
assert session.history[0]["ok"] is True
def test_renders_to_console(self) -> None:
session = Session()
inbox = AlertInbox(maxsize=10)
console = Console()
alert = IncomingAlert(text="test message")
inbox.put(alert)
# Just verify drain_and_render doesn't raise an exception
count = drain_and_render_incoming(session, console, inbox)
assert count == 1
def test_returns_count(self) -> None:
session = Session()
inbox = AlertInbox(maxsize=10)
console = Console()
inbox.put(IncomingAlert(text="alert1"))
inbox.put(IncomingAlert(text="alert2"))
count = drain_and_render_incoming(session, console, inbox)
assert count == 2
def test_drains_empty_inbox(self) -> None:
session = Session()
inbox = AlertInbox(maxsize=10)
console = Console()
count = drain_and_render_incoming(session, console, inbox)
assert count == 0
assert len(session.history) == 0
def test_caps_incoming_alerts_at_max(self) -> None:
session = Session()
inbox = AlertInbox(maxsize=10)
console = Console()
# Add more alerts than the session cap
for i in range(300):
alert = IncomingAlert(text=f"alert_{i}")
inbox.put(alert)
drain_and_render_incoming(session, console, inbox)
# Should be capped at _INCOMING_ALERTS_MAX (256)
assert len(session.alerts.entries) <= session.alerts._max
class TestSessionIncomingAlerts:
"""Test Session handling of incoming alerts."""
def test_clear_resets_incoming_alerts(self) -> None:
session = Session()
inbox = AlertInbox()
console = Console()
# Add some alerts
inbox.put(IncomingAlert(text="alert1"))
inbox.put(IncomingAlert(text="alert2"))
drain_and_render_incoming(session, console, inbox)
assert len(session.alerts.entries) == 2
# Clear session
session.clear()
assert len(session.alerts.entries) == 0
assert len(session.history) == 0
def test_record_incoming_alert_kind(self) -> None:
session = Session()
alert = IncomingAlert(text="test alert")
session.record_incoming_alert(alert)
assert len(session.history) == 1
assert session.history[0]["type"] == "incoming_alert"
assert session.history[0]["text"] == "test alert"
assert session.history[0]["ok"] is True
assert len(session.alerts.entries) == 1
assert session.alerts.entries[0].text == "test alert"
def test_record_incoming_alert_always_ok(self) -> None:
session = Session()
alert = IncomingAlert(text="test alert")
session.record_incoming_alert(alert)
assert session.history[0]["ok"] is True
def test_incoming_alerts_fifo_list(self) -> None:
session = Session()
session.record_incoming_alert(IncomingAlert(text="first"))
session.record_incoming_alert(IncomingAlert(text="second"))
session.record_incoming_alert(IncomingAlert(text="third"))
assert len(session.alerts.entries) == 3
assert session.alerts.entries[0].text == "first"
assert session.alerts.entries[1].text == "second"
assert session.alerts.entries[2].text == "third"
class TestAlertInboxEventClearing:
"""Test AlertInbox pending_event behavior."""
def test_event_set_on_put(self) -> None:
inbox = AlertInbox()
alert = IncomingAlert(text="test")
# Event should not be set initially
assert not inbox.pending_event.is_set()
inbox.put(alert)
# Event should be set after put
assert inbox.pending_event.is_set()
def test_event_cleared_on_iter_pending(self) -> None:
inbox = AlertInbox()
alert = IncomingAlert(text="test")
inbox.put(alert)
assert inbox.pending_event.is_set()
inbox.iter_pending()
# Event should be cleared after draining
assert not inbox.pending_event.is_set()
def test_event_not_cleared_if_queue_not_empty(self) -> None:
inbox = AlertInbox()
inbox.put(IncomingAlert(text="first"))
inbox.put(IncomingAlert(text="second"))
# Pop only one
inbox.pop_nowait()
# Drain but queue still has one item
# (this is artificial; normally iter_pending drains all)
# Let's test with iter_pending which should drain all
inbox2 = AlertInbox()
inbox2.put(IncomingAlert(text="alert"))
inbox2.iter_pending()
# After draining all, event should be cleared
assert not inbox2.pending_event.is_set()
@@ -0,0 +1,352 @@
"""Tests for prompt placeholder and prefill behavior."""
from __future__ import annotations
import re
from dataclasses import dataclass
import pytest
from prompt_toolkit.completion import Completion
from platform.common.task_types import TaskKind
from surfaces.interactive_shell.runtime.core import state as loop_state
from surfaces.interactive_shell.session import Session
from surfaces.interactive_shell.ui.input_prompt import completion as prompt_completion
from surfaces.interactive_shell.ui.input_prompt import rendering as prompt_rendering
from surfaces.interactive_shell.ui.input_prompt.completion import completion_preview_hint_ansi
from surfaces.interactive_shell.ui.input_prompt.refresh import wire_prompt_refresh
from surfaces.interactive_shell.ui.input_prompt.rendering import (
_DEFAULT_PLACEHOLDER_TEXT,
_prompt_counter_text,
_prompt_turn_number,
resolve_idle_hint_ansi,
resolve_prompt_placeholder,
resolve_prompt_prefix_ansi,
)
def _strip_ansi(text: str) -> str:
return re.sub(r"\x1b\[[0-9;]*m", "", text)
def _placeholder_text(session: Session) -> str:
return resolve_prompt_placeholder(session).value
class _RefreshFakeBuffer:
def __init__(self) -> None:
self.text = ""
self.submitted = False
def validate_and_handle(self) -> None:
self.submitted = True
class _RefreshFakeApp:
is_running = True
def __init__(self) -> None:
self.current_buffer = _RefreshFakeBuffer()
def invalidate(self) -> None:
pass
class _RefreshFakeLoop:
def call_soon_threadsafe(self, fn, *args) -> None: # type: ignore[no-untyped-def]
fn(*args)
class TestPromptRefreshAutoSubmit:
def test_queue_auto_command_fills_and_submits_prompt(self) -> None:
"""An agent-queued interactive command should be both prefilled and
auto-submitted so it dispatches through the exclusive-stdin path."""
session = Session()
app = _RefreshFakeApp()
wire_prompt_refresh(session, app, _RefreshFakeLoop())
session.terminal.set_auto_command("/integrations setup sentry")
assert app.current_buffer.text == "/integrations setup sentry"
assert app.current_buffer.submitted is True
def test_plain_prefill_does_not_auto_submit(self) -> None:
"""A prefill without the auto-submit flag must wait for the user (Enter)."""
session = Session()
app = _RefreshFakeApp()
wire_prompt_refresh(session, app, _RefreshFakeLoop())
session.terminal.pending_prompt_default = "why did it fail?"
session.terminal.notify_prompt_changed()
assert app.current_buffer.text == "why did it fail?"
assert app.current_buffer.submitted is False
class TestPromptTurnCounter:
def test_first_turn_is_numbered_one(self) -> None:
session = Session()
assert _prompt_turn_number(session) == 1
assert _prompt_counter_text(session) == "[1] "
def test_counter_advances_with_history(self) -> None:
session = Session()
session.record("chat", "hello")
assert _prompt_turn_number(session) == 2
assert _prompt_counter_text(session) == "[2] "
class TestResolveIdleHint:
def test_shows_connected_integrations_in_hint_bar(self) -> None:
session = Session()
session.configured_integrations_known = True
session.configured_integrations = ("datadog", "github", "grafana")
rendered = _strip_ansi(resolve_idle_hint_ansi(session))
assert "/ for commands" in rendered
assert "Datadog" in rendered
assert "GitHub" in rendered
assert "Grafana" in rendered
def test_omits_integrations_when_none_configured(self) -> None:
session = Session()
session.configured_integrations_known = True
session.configured_integrations = ()
rendered = _strip_ansi(resolve_idle_hint_ansi(session))
assert "Datadog" not in rendered
assert "/ for commands" in rendered
class TestResolvePromptPlaceholder:
def test_default_when_no_session_context(self) -> None:
session = Session()
assert _DEFAULT_PLACEHOLDER_TEXT in _placeholder_text(session)
def test_shows_trust_mode(self) -> None:
session = Session()
session.terminal.trust_mode = True
text = _placeholder_text(session)
assert "trust on" in text
assert _DEFAULT_PLACEHOLDER_TEXT not in text
def test_shows_running_task_count(self) -> None:
session = Session()
task = session.task_registry.create(TaskKind.SYNTHETIC_TEST)
task.mark_running()
assert "1 task running" in _placeholder_text(session)
second = session.task_registry.create(TaskKind.INVESTIGATION)
second.mark_running()
assert "2 tasks running" in _placeholder_text(session)
def test_shows_resumed_session_name(self) -> None:
session = Session()
session.resumed_from_name = "redis-incident"
text = _placeholder_text(session)
assert "resumed: redis-incident" in text
def test_combines_multiple_state_segments(self) -> None:
session = Session()
session.terminal.trust_mode = True
session.resumed_from_name = "redis-incident"
task = session.task_registry.create(TaskKind.WATCHDOG)
task.mark_running()
text = _placeholder_text(session)
assert "trust on" in text
assert "1 task running" in text
assert "resumed: redis-incident" in text
assert " · " in text
@dataclass
class _FakeCompleteState:
completions: list[Completion]
current_completion: Completion | None = None
@dataclass
class _FakeBuffer:
text: str
complete_state: _FakeCompleteState | None = None
@dataclass
class _FakeOutput:
columns: int = 120
def get_size(self) -> _FakeOutput:
return self
@dataclass
class _FakeApp:
current_buffer: _FakeBuffer
output: _FakeOutput
class TestCompletionPreviewHint:
def test_returns_empty_when_no_app(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(prompt_completion, "get_app_or_none", lambda: None)
assert completion_preview_hint_ansi() == ""
def test_shows_full_slash_command_description(self, monkeypatch: pytest.MonkeyPatch) -> None:
completion = Completion(
"/investigate",
start_position=-1,
display="/investigate",
display_meta="Run an RCA investigation from a file or sample templa…",
)
app = _FakeApp(
current_buffer=_FakeBuffer(
text="/",
complete_state=_FakeCompleteState(
completions=[completion],
current_completion=completion,
),
),
output=_FakeOutput(),
)
monkeypatch.setattr(prompt_completion, "get_app_or_none", lambda: app)
rendered = _strip_ansi(completion_preview_hint_ansi())
assert rendered.startswith("/investigate — ")
assert len(rendered) > len("/investigate — " + completion.display_meta_text)
assert "" not in rendered
def test_unregistered_slash_completion_uses_display_label(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
completion = Completion(
"/plugin-cmd",
start_position=-1,
display="/plugin-cmd",
display_meta="Plugin-provided slash command.",
)
app = _FakeApp(
current_buffer=_FakeBuffer(
text="/",
complete_state=_FakeCompleteState(
completions=[completion],
current_completion=completion,
),
),
output=_FakeOutput(),
)
monkeypatch.setattr(prompt_completion, "get_app_or_none", lambda: app)
rendered = _strip_ansi(completion_preview_hint_ansi())
assert rendered == "/plugin-cmd — Plugin-provided slash command."
def test_shows_subcommand_label_with_parent_command(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
completion = Completion(
"high",
start_position=-1,
display="high",
display_meta="favor more thorough reasoning",
)
app = _FakeApp(
current_buffer=_FakeBuffer(
text="/effort ",
complete_state=_FakeCompleteState(
completions=[completion],
current_completion=completion,
),
),
output=_FakeOutput(),
)
monkeypatch.setattr(prompt_completion, "get_app_or_none", lambda: app)
rendered = _strip_ansi(completion_preview_hint_ansi())
assert rendered == "/effort high — favor more thorough reasoning"
def test_falls_back_to_first_completion_when_none_selected(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
first = Completion(
"/plugin-cmd",
start_position=-1,
display="/plugin-cmd",
display_meta="Plugin-provided slash command.",
)
app = _FakeApp(
current_buffer=_FakeBuffer(
text="/",
complete_state=_FakeCompleteState(
completions=[first],
current_completion=None,
),
),
output=_FakeOutput(),
)
monkeypatch.setattr(prompt_completion, "get_app_or_none", lambda: app)
rendered = _strip_ansi(completion_preview_hint_ansi())
assert rendered == "/plugin-cmd — Plugin-provided slash command."
def test_clips_preview_to_terminal_width(self, monkeypatch: pytest.MonkeyPatch) -> None:
long_meta = (
"Plugin-provided slash command with a deliberately long description "
"that must be clipped to the terminal width."
)
completion = Completion(
"/plugin-cmd",
start_position=-1,
display="/plugin-cmd",
display_meta=long_meta,
)
app = _FakeApp(
current_buffer=_FakeBuffer(
text="/",
complete_state=_FakeCompleteState(
completions=[completion],
current_completion=completion,
),
),
output=_FakeOutput(columns=40),
)
monkeypatch.setattr(prompt_completion, "get_app_or_none", lambda: app)
rendered = _strip_ansi(completion_preview_hint_ansi())
assert rendered.endswith("")
assert len(rendered) <= 40
assert rendered.startswith("/plugin-cmd — ")
class TestResolvePromptPrefix:
def test_prefers_inline_spinner_over_completion_preview(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr(
prompt_rendering,
"completion_preview_hint_ansi",
lambda: "preview line",
)
spinner = loop_state.SpinnerState()
spinner.start()
prefix = resolve_prompt_prefix_ansi(
inline_spinner=spinner.inline_spinner_ansi(),
idle_hint=spinner.idle_hint_ansi(),
)
assert "preview line" not in prefix
assert "esc to cancel" in _strip_ansi(prefix)
def test_prefers_completion_preview_over_idle_hint(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr(
prompt_rendering,
"completion_preview_hint_ansi",
lambda: "preview line",
)
spinner = loop_state.SpinnerState()
prefix = resolve_prompt_prefix_ansi(
inline_spinner=spinner.inline_spinner_ansi(),
idle_hint=spinner.idle_hint_ansi(),
)
assert prefix == "preview line"
assert "/ for commands" not in prefix
def test_falls_back_to_idle_hint_when_no_preview(self) -> None:
spinner = loop_state.SpinnerState()
prefix = resolve_prompt_prefix_ansi(
inline_spinner=spinner.inline_spinner_ansi(),
idle_hint=spinner.idle_hint_ansi(),
)
assert "/ for commands" in _strip_ansi(prefix)
@@ -0,0 +1,261 @@
"""Tests for structured investigation outcomes."""
from __future__ import annotations
from pathlib import Path
from unittest.mock import MagicMock
import pytest
from rich.console import Console
from core.llm.shared.llm_retry import LLMCreditExhaustedError
from platform.common.errors import OpenSREError
from platform.common.task_types import TaskRecord
from surfaces.interactive_shell.session import Session
from surfaces.interactive_shell.ui.foreground_investigation import run_foreground_investigation
from surfaces.interactive_shell.ui.investigation_outcome import (
classify_investigation_failure,
normalize_investigation_target,
user_facing_error_message,
)
def test_normalize_investigation_target_template() -> None:
assert normalize_investigation_target("generic") == "generic"
assert normalize_investigation_target("template:datadog") == "datadog"
def test_normalize_investigation_target_file_path() -> None:
assert normalize_investigation_target(
"alerts/checkout.json", path=Path("alerts/checkout.json")
) == ("checkout.json")
def test_classify_integration_failure() -> None:
category, integration, _detail = classify_investigation_failure(
RuntimeError("grafana query failed: 401 unauthorized")
)
assert category == "integration"
assert integration == "grafana"
def test_user_facing_error_message_includes_suggestion() -> None:
message = user_facing_error_message(
OpenSREError("jenkins is not configured", suggestion="Run /integrations setup jenkins")
)
assert "jenkins is not configured" in message
assert "Suggestion:" in message
def test_run_foreground_investigation_early_cancel_omits_stale_investigation_id(
monkeypatch: pytest.MonkeyPatch,
) -> None:
session = Session()
session.last_investigation_id = "inv-old"
console = Console(force_terminal=False, color_system=None, highlight=False)
task = MagicMock(spec=TaskRecord)
task.cancel_requested = False
monkeypatch.setattr(
session.task_registry,
"create",
lambda *_args, **_kwargs: task,
)
def _raise_interrupt(_task: TaskRecord) -> dict[str, object]:
raise KeyboardInterrupt
outcome = run_foreground_investigation(
session=session,
console=console,
task_command="/investigate generic",
run=_raise_interrupt,
exception_context="test",
target="generic",
)
assert outcome.status == "cancelled"
assert outcome.investigation_id == ""
task.mark_cancelled.assert_called_once()
def test_run_foreground_investigation_credit_exhausted_shows_auth_login_hint(
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
) -> None:
session = Session()
console = Console(force_terminal=False, color_system=None, highlight=False)
task = MagicMock(spec=TaskRecord)
task.cancel_requested = False
monkeypatch.setattr(
session.task_registry,
"create",
lambda *_args, **_kwargs: task,
)
def _raise_credit_exhausted(_task: TaskRecord) -> dict[str, object]:
raise LLMCreditExhaustedError(
"Anthropic credit exhausted (provider billing/quota). Original error: 400"
)
outcome = run_foreground_investigation(
session=session,
console=console,
task_command="/investigate alert.json",
run=_raise_credit_exhausted,
exception_context="test",
target="alert.json",
)
output = capsys.readouterr().out
assert outcome.status == "failed"
assert "/model" in output
assert "/auth login" in output
task.mark_failed.assert_called_once()
def test_run_foreground_investigation_opensre_error_does_not_duplicate_auth_hint(
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
) -> None:
session = Session()
console = Console(force_terminal=False, color_system=None, highlight=False)
task = MagicMock(spec=TaskRecord)
task.cancel_requested = False
monkeypatch.setattr(
session.task_registry,
"create",
lambda *_args, **_kwargs: task,
)
def _raise_credit_exhausted_opensre_error(_task: TaskRecord) -> dict[str, object]:
raise OpenSREError(
"Anthropic credit exhausted (provider billing/quota). Original error: 400",
suggestion=(
"Run /auth login <provider> to re-authenticate or add a different provider."
),
)
outcome = run_foreground_investigation(
session=session,
console=console,
task_command="/investigate alert.json",
run=_raise_credit_exhausted_opensre_error,
exception_context="test",
target="alert.json",
)
output = capsys.readouterr().out
assert outcome.status == "failed"
assert output.count("/auth login") == 1
task.mark_failed.assert_called_once()
def test_run_foreground_investigation_skips_feedback_when_pt_app_running(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""#3690: prompt_app is on session.terminal; must not run raw stdin menu while active."""
session = Session()
session.terminal.prompt_app = MagicMock(is_running=True)
console = Console(force_terminal=False, color_system=None, highlight=False)
task = MagicMock(spec=TaskRecord)
task.cancel_requested = False
monkeypatch.setattr(
session.task_registry,
"create",
lambda *_args, **_kwargs: task,
)
feedback = MagicMock()
monkeypatch.setattr(
"surfaces.interactive_shell.ui.feedback.prompt_investigation_feedback",
feedback,
)
outcome = run_foreground_investigation(
session=session,
console=console,
task_command="/investigate grafana",
run=lambda _task: {"root_cause": "sample"},
exception_context="test",
target="grafana",
)
assert outcome.status == "completed"
feedback.assert_not_called()
def test_run_foreground_investigation_prompts_feedback_when_pt_app_idle(
monkeypatch: pytest.MonkeyPatch,
) -> None:
session = Session()
session.terminal.prompt_app = MagicMock(is_running=False)
console = Console(force_terminal=False, color_system=None, highlight=False)
task = MagicMock(spec=TaskRecord)
task.cancel_requested = False
monkeypatch.setattr(
session.task_registry,
"create",
lambda *_args, **_kwargs: task,
)
feedback = MagicMock()
monkeypatch.setattr(
"surfaces.interactive_shell.ui.feedback.prompt_investigation_feedback",
feedback,
)
monkeypatch.setattr(
"surfaces.interactive_shell.ui.components.choice_menu.repl_tty_interactive",
lambda: True,
)
monkeypatch.setattr(
"surfaces.interactive_shell.ui.components.key_reader.restore_stdin_terminal",
lambda: None,
)
run_foreground_investigation(
session=session,
console=console,
task_command="/investigate",
run=lambda _task: {"root_cause": "sample"},
exception_context="test",
target="",
)
feedback.assert_called_once_with({"root_cause": "sample"})
def test_run_foreground_investigation_skips_feedback_on_headless_session(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Gateway SessionCore must not block on the RCA feedback picker."""
from core.agent_harness.session import SessionCore
from core.agent_harness.session.persistence.memory import InMemorySessionStorage
session = SessionCore(storage=InMemorySessionStorage())
console = Console(force_terminal=False, color_system=None, highlight=False)
task = MagicMock(spec=TaskRecord)
task.cancel_requested = False
monkeypatch.setattr(
session.task_registry,
"create",
lambda *_args, **_kwargs: task,
)
feedback = MagicMock()
monkeypatch.setattr(
"surfaces.interactive_shell.ui.feedback.prompt_investigation_feedback",
feedback,
)
monkeypatch.setattr(
"surfaces.interactive_shell.ui.components.choice_menu.repl_tty_interactive",
lambda: True,
)
outcome = run_foreground_investigation(
session=session,
console=console,
task_command="/investigate grafana",
run=lambda _task: {"root_cause": "sample"},
exception_context="test",
target="grafana",
)
assert outcome.status == "completed"
feedback.assert_not_called()
@@ -0,0 +1,46 @@
"""Tests for terminal cooked-mode restore after raw-mode menus."""
from __future__ import annotations
from types import SimpleNamespace
import pytest
from surfaces.interactive_shell.ui.components import key_reader
def test_restore_stdin_terminal_recooks_input_flags(monkeypatch: pytest.MonkeyPatch) -> None:
# A raw menu (tty.setraw) clears ICRNL, so Enter (CR) stops submitting until it
# is restored. Pin that restore re-enables the cooked-mode flags on a raw snapshot.
termios = pytest.importorskip("termios")
monkeypatch.setattr(key_reader.os, "name", "posix")
monkeypatch.setattr(
key_reader.sys, "stdin", SimpleNamespace(isatty=lambda: True, fileno=lambda: 0)
)
raw_attrs = [0, 0, 0, 0, 0, 0, []] # iflag/oflag/cflag/lflag all cleared (raw mode)
written: dict[str, list[object]] = {}
monkeypatch.setattr(termios, "tcgetattr", lambda _fd: list(raw_attrs))
monkeypatch.setattr(
termios, "tcsetattr", lambda _fd, _when, attrs: written.__setitem__("attrs", attrs)
)
monkeypatch.setattr(termios, "tcflush", lambda _fd, _queue: None)
key_reader.restore_stdin_terminal()
attrs = written["attrs"]
assert attrs[0] & termios.ICRNL # CR -> NL so Enter submits again
assert attrs[1] & termios.OPOST # output newline post-processing
assert attrs[3] & termios.ICANON # line editing
assert attrs[3] & termios.ECHO # keystrokes visible
def test_restore_stdin_terminal_is_a_no_op_when_not_a_tty(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(key_reader.os, "name", "posix")
monkeypatch.setattr(
key_reader.sys,
"stdin",
SimpleNamespace(isatty=lambda: False, fileno=lambda: (_ for _ in ()).throw(AssertionError)),
)
key_reader.restore_stdin_terminal() # returns without touching termios
@@ -0,0 +1,78 @@
"""Tests for the shared interactive-shell LLM loader."""
from __future__ import annotations
import io
from typing import Any
from rich.console import Console
from surfaces.interactive_shell.ui.components import loaders
from surfaces.interactive_shell.ui.components.loaders import llm_loader
def _terminal_console() -> tuple[Console, io.StringIO]:
buf = io.StringIO()
return (
Console(file=buf, force_terminal=True, color_system=None, width=80, highlight=False),
buf,
)
def _plain_console() -> tuple[Console, io.StringIO]:
"""Console that reports ``is_terminal == False`` — the CI / piped case."""
buf = io.StringIO()
return (Console(file=buf, force_terminal=False, color_system=None, width=80), buf)
class TestLLMLoader:
def test_yields_to_caller_and_runs_wrapped_block(self) -> None:
console, _ = _terminal_console()
ran: list[bool] = []
with llm_loader(console):
ran.append(True)
assert ran == [True]
def test_skips_spinner_when_console_is_not_a_terminal(self) -> None:
"""In CI / piped output we must NOT pollute logs with spinner frames."""
console, buf = _plain_console()
with llm_loader(console):
pass
# Nothing should be written — no label, no escape sequences, nothing.
assert buf.getvalue() == ""
def test_loader_uses_subtle_spinner_style(self, monkeypatch: Any) -> None:
"""The loader uses a dim, quiet spinner — less visual noise than a bright accent."""
captured: dict[str, Any] = {}
# Fake context manager so we can introspect the kwargs without
# triggering Rich's Live renderer in the test.
class _FakeStatus:
def __enter__(self) -> _FakeStatus:
return self
def __exit__(self, *exc: object) -> None:
return None
def _fake_status(text: str, **kwargs: Any) -> _FakeStatus:
captured["text"] = text
captured["kwargs"] = kwargs
return _FakeStatus()
console, _ = _terminal_console()
monkeypatch.setattr(console, "status", _fake_status)
with llm_loader(console, label="consulting the model"):
pass
from platform.terminal.theme import SECONDARY
assert SECONDARY in captured["text"]
assert "consulting the model" in captured["text"]
assert captured["kwargs"]["spinner"] == "dots"
assert captured["kwargs"]["spinner_style"] == SECONDARY
def test_module_exports_loader_and_default_label() -> None:
assert "llm_loader" in loaders.__all__
assert "DEFAULT_LOADER_LABEL" in loaders.__all__
@@ -0,0 +1,50 @@
"""Tests for REPL provider/model resolution used by the welcome banner."""
from __future__ import annotations
from types import SimpleNamespace
from core.agent_harness.llm_resolution import resolve_provider_models
from surfaces.interactive_shell.ui.tables import provider as ui_provider
def test_ui_provider_reexports_core_resolver() -> None:
assert ui_provider.resolve_provider_models is resolve_provider_models
def test_antigravity_cli_reads_model_env(monkeypatch: object) -> None:
monkeypatch.setenv("ANTIGRAVITY_CLI_MODEL", "gemini-2.5-pro")
reasoning, toolcall = resolve_provider_models(SimpleNamespace(), "antigravity-cli")
assert reasoning == "gemini-2.5-pro"
assert toolcall == "gemini-2.5-pro"
def test_antigravity_cli_falls_back_to_cli_default(monkeypatch: object) -> None:
monkeypatch.delenv("ANTIGRAVITY_CLI_MODEL", raising=False)
reasoning, toolcall = resolve_provider_models(SimpleNamespace(), "antigravity-cli")
assert reasoning == "CLI default"
assert toolcall == "CLI default"
def test_antigravity_cli_does_not_use_hyphenated_settings_attr() -> None:
"""Regression: hyphenated provider ids are not reachable via getattr(settings, ...)."""
settings = SimpleNamespace(**{"antigravity-cli_model": "should-not-win"})
reasoning, toolcall = resolve_provider_models(settings, "antigravity-cli")
assert reasoning == "CLI default"
assert toolcall == "CLI default"
def test_openai_oauth_displays_codex_model(monkeypatch: object) -> None:
monkeypatch.setenv("LLM_AUTH_METHOD", "oauth")
monkeypatch.setenv("CODEX_MODEL", "gpt-5.5")
reasoning, toolcall = resolve_provider_models(SimpleNamespace(), "openai")
assert reasoning == "gpt-5.5"
assert toolcall == "gpt-5.5"
@@ -0,0 +1,287 @@
"""Tests for Rich rendering helpers used by the interactive shell."""
from __future__ import annotations
import io
import re
import threading
import pytest
from rich.console import Console
from surfaces.interactive_shell.runtime.core.state import SpinnerState
from surfaces.interactive_shell.ui.components.rendering import (
_repl_write_buffer,
print_repl_json,
refresh_welcome_poster,
repl_print,
repl_render_launch_poster,
repl_table,
)
from surfaces.interactive_shell.ui.streaming.console import StreamingConsole
from surfaces.interactive_shell.ui.tables import (
render_integrations_table,
render_mcp_table,
)
def test_repl_table_minimal_box() -> None:
t = repl_table(title="T")
assert t.title == "T"
def test_print_repl_json_tty_uses_single_buffered_write(monkeypatch: pytest.MonkeyPatch) -> None:
class _FakeStdout:
def __init__(self) -> None:
self.writes: list[str] = []
def write(self, text: str) -> int:
self.writes.append(text)
return len(text)
def flush(self) -> None:
return None
def isatty(self) -> bool:
return True
fake_stdout = _FakeStdout()
monkeypatch.setattr("sys.stdout", fake_stdout)
console = Console(file=fake_stdout, force_terminal=True, width=80)
print_repl_json(console, '{"ok": true}')
assert len(fake_stdout.writes) == 1
rendered = re.sub(r"\x1b\[[0-9;]*m", "", fake_stdout.writes[0])
assert rendered.startswith("\r\n")
assert '"ok": true' in rendered
def test_render_integrations_table_empty_shows_hint() -> None:
buf = io.StringIO()
console = Console(file=buf, force_terminal=False)
render_integrations_table(console, [])
assert "opensre onboard" in buf.getvalue()
def test_repl_print_resets_before_each_line(monkeypatch) -> None:
resets: list[bool] = []
monkeypatch.setattr(
"surfaces.interactive_shell.ui.components.choice_menu.prepare_repl_output_line",
lambda: resets.append(True),
)
buf = io.StringIO()
console = Console(file=buf, force_terminal=False, width=80)
repl_print(console, "line one")
repl_print(console, "line two")
assert len(resets) == 2
def test_repl_print_does_not_double_prepare_with_streaming_console(monkeypatch) -> None:
resets: list[bool] = []
monkeypatch.setattr(
"surfaces.interactive_shell.ui.components.choice_menu.prepare_repl_output_line",
lambda: resets.append(True),
)
console = StreamingConsole(
SpinnerState(),
threading.Event(),
file=io.StringIO(),
force_terminal=False,
width=80,
)
repl_print(console, "line")
assert len(resets) == 1
def test_repl_print_streaming_console_prepares_tty_once_when_interactive(
monkeypatch: pytest.MonkeyPatch,
) -> None:
class _FakeStdout:
def __init__(self) -> None:
self.writes: list[str] = []
def write(self, text: str) -> int:
self.writes.append(text)
return len(text)
def flush(self) -> None:
return None
def isatty(self) -> bool:
return True
fake_stdout = _FakeStdout()
monkeypatch.setattr("sys.stdout", fake_stdout)
monkeypatch.setattr(
"surfaces.interactive_shell.ui.components.choice_menu.repl_tty_interactive",
lambda: True,
)
console = StreamingConsole(
SpinnerState(),
threading.Event(),
file=io.StringIO(),
force_terminal=False,
width=80,
)
repl_print(console, "line")
assert fake_stdout.writes == ["\r\n", "\r"]
def test_repl_render_launch_poster_uses_crlf_on_tty(monkeypatch: pytest.MonkeyPatch) -> None:
class _FakeStdout:
def __init__(self) -> None:
self.writes: list[str] = []
def write(self, text: str) -> int:
self.writes.append(text)
return len(text)
def flush(self) -> None:
return None
def isatty(self) -> bool:
return True
fake_stdout = _FakeStdout()
monkeypatch.setattr("sys.stdout", fake_stdout)
from platform.terminal.theme import set_active_theme
set_active_theme("blue")
console = Console(
file=fake_stdout,
force_terminal=True,
highlight=False,
color_system="truecolor",
width=120,
)
repl_render_launch_poster(console, theme_notice="blue")
written = "".join(fake_stdout.writes)
assert "theme set:" in written
assert "blue" in written
assert "38;2;168;212;255" in written
assert "185;237;175" not in written
assert "opensre" in written
assert "Welcome back" in written
assert "\r\n" in written
# REPL path must not emit bare \\n (causes double-spaced splash under patch_stdout).
assert "\r" not in written.replace("\r\n", "")
def test_repl_write_buffer_strips_only_escaped_cpr_sequences(
monkeypatch: pytest.MonkeyPatch,
) -> None:
class _FakeStdout:
def __init__(self) -> None:
self.writes: list[str] = []
def write(self, text: str) -> int:
self.writes.append(text)
return len(text)
def flush(self) -> None:
return None
fake_stdout = _FakeStdout()
monkeypatch.setattr("sys.stdout", fake_stdout)
_repl_write_buffer("\x1b[1;1Rtheme set: pink 12;5R\r\n")
written = "".join(fake_stdout.writes)
assert "theme set: pink" in written
assert "12;5R" in written
assert "\x1b[1;1R" not in written
def test_refresh_welcome_poster_drains_cpr_after_clear(monkeypatch: pytest.MonkeyPatch) -> None:
drains: list[str] = []
monkeypatch.setattr(
"surfaces.interactive_shell.ui.components.rendering.repl_clear_screen",
lambda: drains.append("clear"),
)
monkeypatch.setattr(
"surfaces.interactive_shell.ui.components.cpr_stdin.drain_stale_cpr_bytes",
lambda: drains.append("drain"),
)
monkeypatch.setattr(
"surfaces.interactive_shell.ui.components.rendering.repl_render_launch_poster",
lambda *_args, **_kwargs: drains.append("render"),
)
console = Console(file=io.StringIO(), force_terminal=False)
refresh_welcome_poster(console)
assert drains == ["clear", "drain", "render"]
def test_render_integrations_table_renders_content(
capsys: pytest.CaptureFixture[str],
) -> None:
"""print_repl_table on a non-TTY console writes via console.print to stdout.
The cursor-reset (prepare_repl_output_line) is no longer called from the
table rendering path; on a real TTY the blank line and \\r\\n normalisation
are folded into a single sys.stdout.write call in print_repl_table.
"""
console = Console(force_terminal=False, width=80)
render_integrations_table(
console,
[
{
"service": "grafana",
"source": "local store",
"status": "passed",
"detail": "Connected to https://example.grafana.net",
}
],
)
assert "grafana" in capsys.readouterr().out
def test_render_integrations_table_sorts_services_and_includes_mcp(
capsys: pytest.CaptureFixture[str],
) -> None:
console = Console(force_terminal=False, width=80)
render_integrations_table(
console,
[
{"service": "sentry", "source": "-", "status": "missing", "detail": "missing"},
{"service": "github", "source": "-", "status": "missing", "detail": "missing"},
{"service": "datadog", "source": "env", "status": "passed", "detail": "ok"},
],
)
output = capsys.readouterr().out
assert output.index("datadog") < output.index("github") < output.index("sentry")
assert "github" in output
def test_render_mcp_table_renders_content(
capsys: pytest.CaptureFixture[str],
) -> None:
console = Console(force_terminal=False, width=80)
render_mcp_table(
console,
[
{
"service": "github",
"source": "local store",
"status": "configured",
"detail": "Connected",
}
],
)
assert "github" in capsys.readouterr().out
@@ -0,0 +1,983 @@
"""Tests for the shared live-streaming renderer used by interactive-shell handlers."""
from __future__ import annotations
import io
import re
import threading
from collections.abc import Iterator
import pytest
from rich.console import Console
from surfaces.interactive_shell.ui.streaming import (
format_token_count_short,
render_response_header,
stream_to_console,
)
def _strip_ansi(text: str) -> str:
"""Drop ANSI escapes so assertions check the visible output."""
return re.sub(r"\x1b\[[0-9;?]*[A-Za-z]", "", text)
def _tty_console() -> tuple[Console, io.StringIO]:
"""Build a Console that thinks it is a terminal so Rich.Live actually renders."""
buf = io.StringIO()
return (
Console(file=buf, force_terminal=True, color_system=None, width=80, highlight=False),
buf,
)
def _non_tty_console() -> tuple[Console, io.StringIO]:
buf = io.StringIO()
return Console(file=buf, force_terminal=False, color_system=None, width=80), buf
def _yield_chunks(chunks: list[str]) -> Iterator[str]:
yield from chunks
class TestNonTtyFallback:
"""On a non-terminal console the helper drains, prints, and returns the full text."""
def test_drains_stream_and_prints_without_live_artifacts(self) -> None:
console, buf = _non_tty_console()
result = stream_to_console(
console,
label="assistant",
chunks=_yield_chunks(["Hel", "lo, ", "world"]),
)
output = buf.getvalue()
assert result == "Hello, world"
# Bullet header + label + text reach piped output so captured
# logs are useful. ``●`` is the row marker; ``assistant`` is the
# dim label alongside it.
assert "" in output
assert "assistant" in output
assert "Hello, world" in output
# No spinner / Live cursor-movement artifacts in non-TTY captures.
assert "thinking" not in output
def test_suppression_drains_silently_in_non_tty(self) -> None:
"""Suppressed payloads (machine-readable payloads) must not appear in piped output."""
console, buf = _non_tty_console()
result = stream_to_console(
console,
label="assistant",
chunks=_yield_chunks(['{"actions"', ":[]}"]),
suppress_if_starts_with="{",
)
assert result == '{"actions":[]}'
output = buf.getvalue()
# No bullet header for suppressed responses.
assert "" not in output
assert '{"actions"' not in output
class TestTtyParagraphRender:
"""On a terminal console paragraphs render as Markdown the moment
each ``\\n\\n`` boundary closes them; the final paragraph is
force-flushed at end-of-stream. Code blocks are kept whole (we
don't split mid-fence). The spinner indicator drives the live
streaming feedback within a paragraph.
"""
def test_renders_label_and_streamed_content_as_markdown(self) -> None:
console, buf = _tty_console()
result = stream_to_console(
console,
label="assistant",
chunks=_yield_chunks(["Run **opensre", " investigate** to start."]),
)
output = _strip_ansi(buf.getvalue())
assert result == "Run **opensre investigate** to start."
# Bullet row marker pinned above the rendered paragraph.
assert "" in output
# End-of-stream force-flush rendered Markdown — ``**`` stripped.
assert "**opensre" not in output
assert "opensre investigate" in output
def test_renders_first_paragraph_before_second_completes(self) -> None:
"""A complete paragraph (``\\n\\n``) flushes immediately, even
when more chunks would still arrive after it. The second
paragraph stays buffered until its own boundary or EOS."""
chunks: list[str] = []
def _capture_chunks() -> Iterator[str]:
for c in ["First **para**.\n\n", "Second **para**."]:
chunks.append(c)
yield c
console, buf = _tty_console()
result = stream_to_console(
console,
label="assistant",
chunks=_capture_chunks(),
)
output = _strip_ansi(buf.getvalue())
assert result == "First **para**.\n\nSecond **para**."
# Both paragraphs are rendered (``**`` stripped).
assert "First para." in output
assert "Second para." in output
assert "**para**" not in output
def test_paragraph_break_across_chunk_boundary_flushes(self) -> None:
"""The cross-chunk seam — chunk N ends with ``\\n``, chunk N+1
starts with ``\\n`` — must be detected as a paragraph break.
Without the seam check the fast-path skips the join (no
``\\n\\n`` *inside* either chunk) and the boundary is missed
until end-of-stream.
"""
from surfaces.interactive_shell.ui import streaming as streaming_module
parse_count = [0]
real_markdown = streaming_module.Markdown
class _SpyMarkdown(real_markdown): # type: ignore[misc, valid-type]
def __init__(self, text: str, **kwargs) -> None:
parse_count[0] += 1
super().__init__(text, **kwargs)
# Patch only on this thread; restored by the test fixture's GC.
original_markdown = streaming_module.Markdown
streaming_module.Markdown = _SpyMarkdown
try:
console, _ = _tty_console()
# ``"first.\n"`` then ``"\nsecond."`` — neither chunk
# contains ``\n\n`` standalone, but joined they form a
# paragraph break at the seam.
stream_to_console(
console,
label="assistant",
chunks=_yield_chunks(["first.\n", "\nsecond."]),
)
finally:
streaming_module.Markdown = original_markdown
# 2 parses: first paragraph flushed at the seam, then second
# tail force-flushed at end-of-stream.
assert parse_count[0] == 2, (
f"seam check missed the cross-chunk break — got {parse_count[0]} parses"
)
def test_peeked_chunks_seed_prev_chunk_for_seam_detection(self) -> None:
"""When ``suppress_if_starts_with`` peeks chunks but doesn't
suppress, those peeked chunks become history for the seam
check on the very first main-loop chunk.
Concretely: suppression-peek pulls ``"hello\\n"`` (didn't match
``"{"``); main loop starts with ``"\\nworld"``. The seam should
be detected — ``peeked[-1]`` is the initial ``prev_chunk``.
"""
from surfaces.interactive_shell.ui import streaming as streaming_module
parse_count = [0]
real_markdown = streaming_module.Markdown
class _SpyMarkdown(real_markdown): # type: ignore[misc, valid-type]
def __init__(self, text: str, **kwargs) -> None:
parse_count[0] += 1
super().__init__(text, **kwargs)
original_markdown = streaming_module.Markdown
streaming_module.Markdown = _SpyMarkdown
try:
console, _ = _tty_console()
stream_to_console(
console,
label="assistant",
chunks=_yield_chunks(["hello\n", "\nworld"]),
suppress_if_starts_with="{",
)
finally:
streaming_module.Markdown = original_markdown
# 2 parses — peeked chunk + first main-loop chunk form a seam,
# producing one paragraph; tail is force-flushed at EOS.
assert parse_count[0] == 2
def test_open_code_block_is_not_split_mid_fence(self) -> None:
"""``\\n\\n`` inside an open code block must NOT trigger a
flush — splitting would render a partial fenced block whose
formatting breaks. The fence stays whole until it closes."""
chunks_with_open_fence = [
"Header\n\n",
"```python\n",
"x = 1\n\n", # blank line inside code block — must not flush
"y = 2\n",
"```\n\n",
"Trailing.",
]
console, buf = _tty_console()
result = stream_to_console(
console,
label="assistant",
chunks=_yield_chunks(chunks_with_open_fence),
)
# Full text returned unchanged.
assert "x = 1" in result
assert "y = 2" in result
# Both code lines must appear in the rendered output (i.e. the
# fence wasn't split before its closing ``` was seen).
output = _strip_ansi(buf.getvalue())
assert "x = 1" in output
assert "y = 2" in output
assert "Trailing" in output
def test_post_fence_paragraph_renders_after_block_with_embedded_blank_line(
self,
) -> None:
"""A code block containing a blank line must not bury later
paragraphs at EOS. Once the closing fence arrives, the
completed code-block paragraph flushes and any subsequent
``\\n\\n``-terminated paragraph flushes mid-stream too.
"""
from surfaces.interactive_shell.ui import streaming as streaming_module
parse_count = [0]
real_markdown = streaming_module.Markdown
class _SpyMarkdown(real_markdown): # type: ignore[misc, valid-type]
def __init__(self, text: str, **kwargs) -> None:
parse_count[0] += 1
super().__init__(text, **kwargs)
original_markdown = streaming_module.Markdown
streaming_module.Markdown = _SpyMarkdown
try:
console, _ = _tty_console()
stream_to_console(
console,
label="assistant",
chunks=_yield_chunks(
[
"Intro.\n\n",
"```python\n",
"x = 1\n\n",
"y = 2\n",
"```\n\n",
"Conclusion.\n\n",
]
),
)
finally:
streaming_module.Markdown = original_markdown
# 3 parses — intro flushes on its own ``\n\n``; the fenced block
# flushes once the closing fence + ``\n\n`` arrive (skipping the
# embedded blank line); conclusion flushes on its own ``\n\n``
# before EOS. Without the skip-past-open-fence logic, the fenced
# block + conclusion would defer to a single force-flush at EOS
# → 2 parses.
assert parse_count[0] == 3, (
f"post-fence paragraph deferred to EOS — got {parse_count[0]} parses"
)
def test_multiple_blank_lines_inside_single_fence_render_as_one_block(
self,
) -> None:
"""A single code block with several embedded ``\\n\\n`` must render
once when its fence closes. Exercises ``search_from`` advancing
repeatedly within a single ``_flush_paragraphs`` call.
"""
from surfaces.interactive_shell.ui import streaming as streaming_module
parse_count = [0]
real_markdown = streaming_module.Markdown
class _SpyMarkdown(real_markdown): # type: ignore[misc, valid-type]
def __init__(self, text: str, **kwargs) -> None:
parse_count[0] += 1
super().__init__(text, **kwargs)
original_markdown = streaming_module.Markdown
streaming_module.Markdown = _SpyMarkdown
try:
console, _ = _tty_console()
stream_to_console(
console,
label="assistant",
chunks=_yield_chunks(
[
"Intro.\n\n",
"```python\n",
"a\n\n", # first embedded blank
"b\n\n", # second embedded blank
"c\n\n", # third embedded blank
"d\n",
"```\n\n",
"After.\n\n",
]
),
)
finally:
streaming_module.Markdown = original_markdown
# 3 parses with the fix: intro + fenced block (rendered once when
# fence closes, after 3 skip iterations advance search_from past
# each embedded blank) + after. Without the fix, the inner loop
# would break on the first odd-fence boundary and defer the block
# + after to a single EOS force-flush → 2 parses.
assert parse_count[0] == 3, (
f"expected 3 parses (intro + block + after), got {parse_count[0]}"
)
def test_two_consecutive_fences_each_with_blank_line_render_independently(
self,
) -> None:
"""Two fenced blocks back-to-back, each containing an embedded
``\\n\\n``. Each block must render as its own paragraph when its
fence closes — ``search_from`` is reset to 0 after each render so
the second block isn't blocked by stale state from the first.
"""
from surfaces.interactive_shell.ui import streaming as streaming_module
parse_count = [0]
real_markdown = streaming_module.Markdown
class _SpyMarkdown(real_markdown): # type: ignore[misc, valid-type]
def __init__(self, text: str, **kwargs) -> None:
parse_count[0] += 1
super().__init__(text, **kwargs)
original_markdown = streaming_module.Markdown
streaming_module.Markdown = _SpyMarkdown
try:
console, _ = _tty_console()
stream_to_console(
console,
label="assistant",
chunks=_yield_chunks(
[
"Intro.\n\n",
"```py\nfoo\n\nbar\n```\n\n", # block 1, embedded blank
"```py\nbaz\n\nqux\n```\n\n", # block 2, embedded blank
"End.\n\n",
]
),
)
finally:
streaming_module.Markdown = original_markdown
# 4 parses with the fix: intro + block1 + block2 + end. Without
# the fix, block1's leading embedded ``\n\n`` would lock the inner
# loop on the odd-fence break for every subsequent chunk, so the
# entire tail (block1 + block2 + end) collapses into one EOS
# force-flush → 2 parses total.
assert parse_count[0] == 4, (
f"expected 4 parses (intro + 2 blocks + end), got {parse_count[0]}"
)
def test_inline_triple_backtick_mention_does_not_block_paragraph(self) -> None:
"""Single inline ``\\`\\`\\``` mention inside flowing text must not
be miscounted as an open fence. The substring count would flip to
odd (1), skipping the paragraph's ``\\n\\n`` boundary and deferring
rendering to EOS. Only line-start fences count, so two paragraphs
each render incrementally as their ``\\n\\n`` arrives.
"""
from surfaces.interactive_shell.ui import streaming as streaming_module
parse_count = [0]
real_markdown = streaming_module.Markdown
class _SpyMarkdown(real_markdown): # type: ignore[misc, valid-type]
def __init__(self, text: str, **kwargs) -> None:
parse_count[0] += 1
super().__init__(text, **kwargs)
original_markdown = streaming_module.Markdown
streaming_module.Markdown = _SpyMarkdown
try:
console, _ = _tty_console()
stream_to_console(
console,
label="assistant",
chunks=_yield_chunks(
[
"The ``` marker opens a code block in markdown.\n\n",
"Use it whenever you want to fence example code.\n\n",
]
),
)
finally:
streaming_module.Markdown = original_markdown
# 2 parses: each paragraph flushes on its own ``\n\n``. Without
# the line-start fence check, the single inline ``` would flip
# the count to odd, both ``\n\n`` boundaries would be skipped,
# and the whole stream would force-flush as 1 parse at EOS.
assert parse_count[0] == 2, (
f"inline ``` mention blocked paragraph flush — got {parse_count[0]} parses"
)
def test_mid_line_triple_backtick_does_not_count_as_fence(self) -> None:
"""A ``\\`\\`\\``` that appears mid-line (not at line start) must
NOT be counted as a fence boundary by the parity check. Real
code blocks must keep accumulating across paragraphs until a
line-start closing fence arrives — the mid-line backticks are
inline content (often quoted/embedded in prose), not Markdown
syntax.
Regression for the drive-by review point: a chunk like
``\\nresult: ok\\`\\`\\`\\nmore text`` (closing-fence-shaped
characters mid-line because the chunk boundary fell there)
used to be a worry. The ``^\\`\\`\\``` regex with
``re.MULTILINE`` matches only line-start fences, so this
scenario stays correct.
"""
from surfaces.interactive_shell.ui import streaming as streaming_module
parse_count = [0]
real_markdown = streaming_module.Markdown
class _SpyMarkdown(real_markdown): # type: ignore[misc, valid-type]
def __init__(self, text: str, **kwargs) -> None:
parse_count[0] += 1
super().__init__(text, **kwargs)
original_markdown = streaming_module.Markdown
streaming_module.Markdown = _SpyMarkdown
try:
console, buf = _tty_console()
stream_to_console(
console,
label="assistant",
chunks=_yield_chunks(
[
# Real fence opens at line start.
"```py\n",
"x = 1\n",
# Mid-line backticks inside the still-open fence.
# MUST be ignored by the parity check — fence
# stays open until a real closing fence at line
# start.
"result: ok```\n",
"y = 2\n",
# Now the real closing fence at line start.
"```\n\n",
"After the block.\n\n",
]
),
)
finally:
streaming_module.Markdown = original_markdown
# 2 parses: (1) the entire fenced code block as one Markdown
# parse — proves the mid-line ``` didn't prematurely flush it;
# (2) the "After the block." paragraph. Without the line-anchor,
# the mid-line ``` would flip the parity, close the fence
# early, and we'd see 3+ parses with broken code rendering.
assert parse_count[0] == 2, f"mid-line ``` was miscounted — got {parse_count[0]} parses"
# And the mid-line backticks must reach the rendered output as
# plain text (inside the code block), not get eaten as syntax.
output = _strip_ansi(buf.getvalue())
assert "result: ok" in output
def test_unclosed_fence_with_embedded_blank_line_renders_at_eos(self) -> None:
"""Unclosed fence containing an embedded ``\\n\\n`` must not hang
the inner loop and must surface the partial buffer at end-of-stream.
With the skip-past-open-fence logic, the inner loop advances
``search_from`` past each embedded ``\\n\\n``, eventually returns
``-1`` from ``find``, and exits cleanly. The outer ``finally``
then force-flushes the partial buffer so the user sees the
truncated response rather than nothing.
"""
chunks = [
"```py\n",
"a = 1\n\n", # blank inside fence
"b = 2\n", # stream ends without closing the fence
]
console, buf = _tty_console()
result = stream_to_console(
console,
label="assistant",
chunks=_yield_chunks(chunks),
)
# Both code lines must appear in the rendered output — the
# partial fence is force-flushed at EOS so the user sees what
# was streamed before the LLM cut off.
output = _strip_ansi(buf.getvalue())
assert "a = 1" in output
assert "b = 2" in output
assert "a = 1" in result
assert "b = 2" in result
def test_returns_empty_string_when_stream_is_empty(self) -> None:
"""An empty stream must not leave a frozen spinner on screen."""
console, buf = _tty_console()
result = stream_to_console(
console,
label="assistant",
chunks=_yield_chunks([]),
)
assert result == ""
# Bullet still printed (header fires before chunk processing),
# but no spinner residue at finalize.
assert "" in _strip_ansi(buf.getvalue())
class TestMidStreamError:
"""Errors inside the stream propagate while the partial buffer stays on screen."""
def test_exception_propagates_with_partial_visible(self) -> None:
def _broken_stream() -> Iterator[str]:
yield "partial "
yield "answer"
raise RuntimeError("upstream 503")
console, buf = _tty_console()
with pytest.raises(RuntimeError, match="upstream 503"):
stream_to_console(
console,
label="assistant",
chunks=_broken_stream(),
)
# The partial response was rendered before the exception propagated,
# so the caller can surface an error label below it.
output = _strip_ansi(buf.getvalue())
assert "partial answer" in output
def test_keyboard_interrupt_propagates_with_partial_visible(self) -> None:
"""KeyboardInterrupt mid-stream propagates after the partial renders.
The double-press absorption logic that used to live here was moved
to the prompt_toolkit cancel key bindings (see
:func:`interactive_shell.ui.input_prompt.key_bindings.build_cancel_key_bindings`)
— the streaming code just lets ``KeyboardInterrupt`` propagate,
and the ``finally`` block in :func:`stream_to_console` ensures
the partial buffer is rendered.
"""
class _ChunksThenKbd:
__slots__ = ("_i",)
def __init__(self) -> None:
self._i = 0
def __iter__(self) -> Iterator[str]:
return self
def __next__(self) -> str:
parts = ("partial ", "answer")
if self._i < len(parts):
c = parts[self._i]
self._i += 1
return c
raise KeyboardInterrupt
console, buf = _tty_console()
with pytest.raises(KeyboardInterrupt):
stream_to_console(
console,
label="assistant",
chunks=iter(_ChunksThenKbd()),
)
output = _strip_ansi(buf.getvalue())
# Partial is rendered before the KI propagates — the ``finally``
# in stream_to_console fires the Markdown render of the buffer.
assert "partial answer" in output
class TestTimingFooter:
"""A small dim ``· Ns`` footer appears after a rendered live response."""
def test_footer_printed_after_streamed_response(self) -> None:
console, buf = _tty_console()
stream_to_console(
console,
label="assistant",
chunks=_yield_chunks(["hello"]),
)
output = _strip_ansi(buf.getvalue())
assert re.search(r"·\s+\d+\.\d+s", output) is not None
def test_footer_skipped_when_stream_is_empty(self) -> None:
"""Empty stream must not print a timing footer under nothing."""
console, buf = _tty_console()
stream_to_console(
console,
label="assistant",
chunks=_yield_chunks([]),
)
output = _strip_ansi(buf.getvalue())
assert re.search(r"·\s+\d+\.\d+s", output) is None
def test_footer_skipped_when_response_is_suppressed(self) -> None:
"""Suppressed machine-readable payloads should not get a timing footer either."""
console, buf = _tty_console()
stream_to_console(
console,
label="assistant",
chunks=_yield_chunks(['{"actions"', ":[]}"]),
suppress_if_starts_with="{",
)
output = _strip_ansi(buf.getvalue())
assert re.search(r"·\s+\d+\.\d+s", output) is None
class TestRenderResponseHeader:
"""``render_response_header`` is the bullet-row marker shared with
``action_turn.run_action_tool_turn`` — three call sites collapsed
to one helper, so we lock in the visible output here.
"""
def test_emits_bullet_glyph_and_label(self) -> None:
console, buf = _tty_console()
render_response_header(console, "assistant")
output = _strip_ansi(buf.getvalue())
assert "" in output
assert "assistant" in output
def test_label_is_passthrough(self) -> None:
"""The function takes the label verbatim — callers pass either
``STREAM_LABEL_ANSWER`` or ``STREAM_LABEL_ASSISTANT`` (or any
free-form word). No filtering, no defaults."""
console, buf = _tty_console()
render_response_header(console, "answer")
assert "answer" in _strip_ansi(buf.getvalue())
class TestFormatTokenCountShort:
"""Shared helper used by both the streaming footer and the live spinner."""
@pytest.mark.parametrize(
("count", "expected"),
[
(0, "0"),
(1, "1"),
(999, "999"),
(1000, "1.0k"),
(1234, "1.2k"),
(10000, "10.0k"),
(123456, "123.5k"),
],
)
def test_formats_at_boundaries(self, count: int, expected: str) -> None:
assert format_token_count_short(count) == expected
class _ProgressConsole(Console):
"""Console with the loop's :class:`StreamingConsole` shape — exposes
``update_streaming_progress`` and ``cancel_requested`` for the
streaming layer's ``getattr`` dispatch.
"""
def __init__(
self,
cancel_event: threading.Event | None = None,
cancel_after_n_progress_calls: int | None = None,
**kwargs: object,
) -> None:
super().__init__(**kwargs) # type: ignore[arg-type]
self.progress_calls: list[int] = []
self._cancel_event = cancel_event or threading.Event()
self._cancel_after = cancel_after_n_progress_calls
def update_streaming_progress(self, bytes_received: int) -> None:
self.progress_calls.append(bytes_received)
if self._cancel_after is not None and len(self.progress_calls) >= self._cancel_after:
self._cancel_event.set()
@property
def cancel_requested(self) -> bool:
return self._cancel_event.is_set()
class TestProgressHook:
"""``stream_to_console`` invokes the optional ``update_streaming_progress``
hook on the console and throttles the call rate so worker-thread → UI
cross-thread queueing isn't flooded on long streams.
"""
def test_progress_hook_called_with_running_byte_count(self) -> None:
buf = io.StringIO()
console = _ProgressConsole(file=buf, force_terminal=True, color_system=None, width=80)
chunks = ["Hello, ", "world", "!"]
result = stream_to_console(
console,
label="assistant",
chunks=_yield_chunks(chunks),
)
assert result == "Hello, world!"
assert console.progress_calls, "progress hook never fired"
# Counts must be monotonically non-decreasing — the streaming
# layer pushes a *running* byte total, never a per-chunk delta.
assert console.progress_calls == sorted(console.progress_calls)
# Each reported count must reflect bytes that *had* arrived by
# that point in the stream — never exceed the final total.
assert console.progress_calls[-1] <= len(result)
def test_progress_hook_throttled_on_burst_streams(self) -> None:
"""A burst of 200 small chunks must not produce 200 hook calls.
Throttling target is ~10/s; the test stream finishes well under
a second so we expect a small handful of calls (not one per
chunk). The exact count is timing-dependent — assert ``<= 50``
as a generous upper bound that still proves throttling fires.
"""
buf = io.StringIO()
console = _ProgressConsole(file=buf, force_terminal=True, color_system=None, width=80)
burst = ["x"] * 200
stream_to_console(
console,
label="assistant",
chunks=_yield_chunks(burst),
)
assert len(console.progress_calls) <= 50, (
f"throttle did not fire — got {len(console.progress_calls)} calls"
)
def test_no_hook_when_console_lacks_method(self) -> None:
"""Plain ``Console`` (no progress method) must stream cleanly."""
console, buf = _tty_console()
result = stream_to_console(
console,
label="assistant",
chunks=_yield_chunks(["alpha", "beta"]),
)
assert result == "alphabeta"
def test_progress_hook_failure_does_not_truncate_response(self) -> None:
"""A flaky status widget must never lose response content."""
class _BrokenConsole(Console):
def __init__(self) -> None:
super().__init__(
file=io.StringIO(),
force_terminal=True,
color_system=None,
width=80,
)
def update_streaming_progress(self, bytes_received: int) -> None: # noqa: ARG002
raise RuntimeError("widget gone")
console = _BrokenConsole()
result = stream_to_console(
console,
label="assistant",
chunks=_yield_chunks(["full ", "answer"]),
)
assert result == "full answer"
class TestParagraphFlushThrottle:
"""Long single-paragraph streams must not pay O(n²) work re-joining
the buffer on every chunk. The fast-path skips the join when no
paragraph boundary could possibly land in the new chunk.
"""
def _spy_markdown_parses(self, monkeypatch: pytest.MonkeyPatch) -> list[int]:
"""Wrap ``streaming.Markdown`` so each construction increments a counter."""
from surfaces.interactive_shell.ui import streaming as streaming_module
parse_count = [0]
real_markdown = streaming_module.Markdown
class _SpyMarkdown(real_markdown): # type: ignore[misc, valid-type]
def __init__(self, text: str, **kwargs) -> None:
parse_count[0] += 1
super().__init__(text, **kwargs)
monkeypatch.setattr(streaming_module, "Markdown", _SpyMarkdown)
return parse_count
def test_long_single_paragraph_renders_once(self, monkeypatch: pytest.MonkeyPatch) -> None:
"""No ``\\n\\n`` in any chunk → the only Markdown parse is the
end-of-stream force-flush. Proves the fast-path skips the join
on every intermediate chunk."""
parse_count = self._spy_markdown_parses(monkeypatch)
console, _ = _tty_console()
# 500 chunks, each a few words, no paragraph breaks.
chunks = [f"word{i} " for i in range(500)]
result = stream_to_console(console, label="assistant", chunks=_yield_chunks(chunks))
assert "word0" in result
assert "word499" in result
# End-of-stream force-flush is the only Markdown construction.
assert parse_count[0] == 1, f"expected 1 parse (force-flush), got {parse_count[0]}"
def test_paragraph_boundary_per_chunk_renders_once_per_paragraph(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Each ``\\n\\n`` boundary triggers exactly one Markdown
parse — the trailing tail is force-flushed at end."""
parse_count = self._spy_markdown_parses(monkeypatch)
console, _ = _tty_console()
chunks = [
"para 1.\n\n",
"para 2.\n\n",
"para 3.\n\n",
"trailing tail",
]
stream_to_console(console, label="assistant", chunks=_yield_chunks(chunks))
# 3 in-loop renders + 1 force-flush at end = 4 total.
assert parse_count[0] == 4
def test_chunks_with_only_single_newlines_skip_flush(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Lists / code with single ``\\n`` separators don't trigger
flush until a real ``\\n\\n`` boundary closes the block.
Keeping multi-line list/table syntax intact is required for
Rich's Markdown renderer to produce a proper Table / bullet
list rather than rendering each row as a standalone block."""
parse_count = self._spy_markdown_parses(monkeypatch)
console, _ = _tty_console()
chunks = [
"- item 1\n",
"- item 2\n",
"- item 3\n",
# No \n\n — only force-flush at end.
]
stream_to_console(console, label="assistant", chunks=_yield_chunks(chunks))
assert parse_count[0] == 1
class TestCancelPolling:
"""``stream_to_console`` polls ``console.cancel_requested`` between
chunks so an Esc-driven cancel signal stops the worker-thread stream
before it drains the iterator.
"""
def test_cancel_set_before_stream_returns_empty_partial(self) -> None:
buf = io.StringIO()
cancel_event = threading.Event()
cancel_event.set() # cancel before any chunk is pulled
console = _ProgressConsole(
cancel_event=cancel_event,
file=buf,
force_terminal=True,
color_system=None,
width=80,
)
# If the cancel poll didn't work, the iterator below would
# raise (it's a single-use generator).
chunks_iter = _yield_chunks(["a", "b", "c"])
result = stream_to_console(console, label="assistant", chunks=chunks_iter)
assert result == ""
def test_cancel_mid_stream_truncates_buffer(self) -> None:
"""Cancel signalled mid-stream stops further chunk reads.
Uses a generator that flips the cancel flag from inside its own
yield loop — that's deterministic regardless of throttling, since
the next iteration of ``stream_to_console``'s loop checks the
cancel flag *before* pulling the next chunk.
"""
buf = io.StringIO()
cancel_event = threading.Event()
console = _ProgressConsole(
cancel_event=cancel_event,
file=buf,
force_terminal=True,
color_system=None,
width=80,
)
chunks_yielded: list[int] = []
def _chunks_with_cancel() -> Iterator[str]:
for i in range(20):
chunks_yielded.append(i)
if i == 3:
cancel_event.set()
yield f"chunk{i} "
result = stream_to_console(console, label="assistant", chunks=_chunks_with_cancel())
# The generator should not have been pumped through to chunk 19 —
# ``stream_to_console`` should have broken out of its loop once
# the cancel event was visible.
assert max(chunks_yielded) < 19, (
f"generator yielded too many chunks — got up to {max(chunks_yielded)}"
)
# The result must include chunks read before the cancel was
# observed and must not include the trailing chunks.
assert result.startswith("chunk0 ")
assert "chunk19" not in result
def test_no_cancel_attr_means_stream_runs_to_completion(self) -> None:
"""A console without ``cancel_requested`` must drain normally."""
console, buf = _tty_console()
result = stream_to_console(
console,
label="assistant",
chunks=_yield_chunks(["one ", "two ", "three"]),
)
assert result == "one two three"
class TestSuppressionPeek:
"""``suppress_if_starts_with`` skips live rendering for content the caller will handle."""
def test_suppresses_and_drains_when_first_char_matches(self) -> None:
console, buf = _tty_console()
result = stream_to_console(
console,
label="assistant",
chunks=_yield_chunks(['{"actions"', ":[]", "}"]),
suppress_if_starts_with="{",
)
assert result == '{"actions":[]}'
# No bullet header, no markdown, no live-region artifacts.
output = _strip_ansi(buf.getvalue())
assert "" not in output
assert '{"actions"' not in output
def test_renders_normally_when_first_char_does_not_match(self) -> None:
console, buf = _tty_console()
result = stream_to_console(
console,
label="assistant",
chunks=_yield_chunks(["Hello, ", "world"]),
suppress_if_starts_with="{",
)
assert result == "Hello, world"
output = _strip_ansi(buf.getvalue())
assert "" in output
assert "Hello, world" in output
def test_skips_leading_whitespace_before_deciding(self) -> None:
"""Leading whitespace must not block the suppression peek."""
console, buf = _tty_console()
result = stream_to_console(
console,
label="assistant",
chunks=_yield_chunks([" \n", '{"action"', ':"slash"}']),
suppress_if_starts_with="{",
)
assert result == ' \n{"action":"slash"}'
output = _strip_ansi(buf.getvalue())
assert "" not in output
@@ -0,0 +1,36 @@
from __future__ import annotations
from datetime import UTC, datetime
from surfaces.interactive_shell.ui.components.time_format import (
format_repl_duration,
format_repl_timestamp,
)
def test_format_repl_duration() -> None:
assert format_repl_duration(None) == ""
assert format_repl_duration(45) == "45s"
assert format_repl_duration(125) == "2m 5s"
assert format_repl_duration(3725) == "1h 2m"
def test_format_repl_timestamp_iso_table_style() -> None:
dt = datetime(2026, 5, 29, 10, 15, tzinfo=UTC)
assert format_repl_timestamp(dt.isoformat(), style="table") == dt.astimezone().strftime(
"%Y-%m-%d %H:%M"
)
def test_format_repl_timestamp_compact_style() -> None:
dt = datetime(2026, 5, 29, 10, 15, tzinfo=UTC)
assert format_repl_timestamp(dt, style="compact") == dt.astimezone().strftime("%m-%d %H:%M")
def test_format_repl_timestamp_unix_utc_style() -> None:
ts = datetime(2026, 5, 29, 10, 15, tzinfo=UTC).timestamp()
assert format_repl_timestamp(ts, style="utc") == "2026-05-29 10:15:00 UTC"
def test_format_repl_timestamp_invalid_iso_fallback() -> None:
assert format_repl_timestamp("not-a-timestamp", style="table") == "not-a-timestamp"
@@ -0,0 +1,324 @@
"""Tests for the registered-tool catalog used by the ``/tools list`` slash command."""
from __future__ import annotations
import io
from collections.abc import Iterator
from pathlib import Path
from types import SimpleNamespace
from typing import Any
from unittest.mock import patch
import pytest
from rich.console import Console
from core.tool_framework.registered_tool import RegisteredTool
from surfaces.interactive_shell.command_registry import dispatch_slash
from surfaces.interactive_shell.command_registry.tools_cmds import _TOOLS_FIRST_ARGS, _cmd_tools
from surfaces.interactive_shell.session import Session
from surfaces.interactive_shell.ui.tables import tool_catalog
from surfaces.interactive_shell.ui.tables.tool_catalog import (
ToolCatalogEntry,
_summarize_input_schema,
build_tool_catalog,
format_tool_catalog_text,
)
def _make_tool(
name: str,
*,
description: str = "Tool description.",
surfaces: tuple[str, ...] = ("investigation",),
input_schema: dict[str, Any] | None = None,
origin_module: str = "tools.registry",
) -> RegisteredTool:
"""Construct a minimal RegisteredTool stub for catalog rendering tests."""
return RegisteredTool(
name=name,
description=description,
input_schema=input_schema or {"type": "object", "properties": {}, "required": []},
source="knowledge",
run=lambda **_: None,
surfaces=surfaces,
origin_module=origin_module,
origin_name=name,
)
@pytest.fixture
def fake_registry(monkeypatch: pytest.MonkeyPatch) -> Iterator[list[RegisteredTool]]:
"""Replace the live registry with a curated set so tests are deterministic."""
tools: list[RegisteredTool] = []
def _fake_get_registered_tools(surface: str | None = None) -> list[RegisteredTool]:
if surface is None:
return list(tools)
return [t for t in tools if surface in t.surfaces]
monkeypatch.setattr(tool_catalog, "get_registered_tools", _fake_get_registered_tools)
yield tools
class TestSummarizeInputSchema:
def test_empty_schema_renders_no_params(self) -> None:
assert _summarize_input_schema({}) == "(no params)"
assert (
_summarize_input_schema({"type": "object", "properties": {}, "required": []})
== "(no params)"
)
def test_required_params_render_without_question_mark(self) -> None:
schema = {
"type": "object",
"properties": {"query": {"type": "string"}, "limit": {"type": "integer"}},
"required": ["query", "limit"],
}
assert _summarize_input_schema(schema) == "query: string, limit: integer"
def test_optional_params_get_question_mark_suffix(self) -> None:
schema = {
"type": "object",
"properties": {"query": {"type": "string"}, "limit": {"type": "integer"}},
"required": ["query"],
}
assert _summarize_input_schema(schema) == "query: string, limit?: integer"
def test_untyped_property_renders_as_any(self) -> None:
schema = {
"type": "object",
"properties": {"cloudops_backend": {}},
"required": ["cloudops_backend"],
}
assert _summarize_input_schema(schema) == "cloudops_backend: any"
def test_overlong_summary_is_truncated_with_ellipsis(self) -> None:
# Build a schema large enough to exceed the 200-char cap.
properties = {f"param_{i}": {"type": "string"} for i in range(40)}
schema = {
"type": "object",
"properties": properties,
"required": list(properties.keys()),
}
rendered = _summarize_input_schema(schema)
assert len(rendered) <= 200
assert rendered.endswith("")
class TestBuildToolCatalog:
def test_returns_empty_list_when_no_tools(self, fake_registry: list[RegisteredTool]) -> None:
del fake_registry # unused — registry is empty by default
assert build_tool_catalog() == []
def test_projects_each_tool_into_a_catalog_entry(
self, fake_registry: list[RegisteredTool]
) -> None:
fake_registry.append(
_make_tool(
"search_github",
description="Search GitHub code.",
surfaces=("investigation", "chat"),
input_schema={
"type": "object",
"properties": {"query": {"type": "string"}},
"required": ["query"],
},
)
)
entries = build_tool_catalog()
assert len(entries) == 1
entry = entries[0]
assert entry.name == "search_github"
assert entry.surfaces == ("investigation", "chat")
assert entry.description == "Search GitHub code."
assert entry.input_schema_summary == "query: string"
# Source file for the registry module resolves to its repo-relative path.
assert entry.source_file == "tools/registry.py"
def test_filters_by_surface_when_provided(self, fake_registry: list[RegisteredTool]) -> None:
fake_registry.extend(
[
_make_tool("inv_only", surfaces=("investigation",)),
_make_tool("chat_only", surfaces=("chat",)),
_make_tool("both", surfaces=("investigation", "chat")),
]
)
chat_entries = build_tool_catalog(surface="chat")
names = {e.name for e in chat_entries}
assert names == {"chat_only", "both"}
def test_unresolvable_origin_module_yields_empty_source_file(
self, fake_registry: list[RegisteredTool]
) -> None:
# A tool that registered but whose origin module no longer imports
# cleanly (e.g. partial uninstall) must still surface in the catalog.
fake_registry.append(_make_tool("orphan", origin_module="not.a.real.module"))
entries = build_tool_catalog()
assert len(entries) == 1
assert entries[0].source_file == ""
def test_origin_module_outside_repo_returns_absolute_source_path(
self, fake_registry: list[RegisteredTool], monkeypatch: pytest.MonkeyPatch
) -> None:
outside = Path("/virtual/site-packages/some_pkg/plugin.py")
fake_mod = SimpleNamespace(__file__=str(outside))
monkeypatch.setattr(tool_catalog.importlib, "import_module", lambda _: fake_mod)
fake_registry.append(_make_tool("ext_tool", origin_module="some_pkg.plugin"))
entries = build_tool_catalog()
assert len(entries) == 1
assert entries[0].source_file == outside.as_posix()
class TestFormatToolCatalogText:
def test_returns_empty_string_for_empty_catalog(self) -> None:
assert format_tool_catalog_text([]) == ""
def test_groups_by_surface_with_investigation_first(self) -> None:
entries = [
ToolCatalogEntry(
name="alpha",
surfaces=("investigation",),
description="alpha desc",
source_file="tools/alpha.py",
input_schema_summary="(no params)",
),
ToolCatalogEntry(
name="beta",
surfaces=("chat",),
description="beta desc",
source_file="tools/beta.py",
input_schema_summary="x: string",
),
]
text = format_tool_catalog_text(entries)
# Investigation header must precede chat header (canonical ordering).
inv_pos = text.find("## investigation")
chat_pos = text.find("## chat")
assert inv_pos != -1 and chat_pos != -1
assert inv_pos < chat_pos
assert "alpha" in text and "beta" in text
def test_dual_surface_tool_appears_under_each_surface(self) -> None:
entries = [
ToolCatalogEntry(
name="dual",
surfaces=("investigation", "chat"),
description="dual desc",
source_file="tools/dual.py",
input_schema_summary="(no params)",
)
]
text = format_tool_catalog_text(entries)
# Dual-surface tools surface in BOTH groups so the user can tell which
# tools the chat agent vs the investigation pipeline can reach.
assert text.count("**dual**") == 2
assert "## investigation (1 tool)" in text
assert "## chat (1 tool)" in text
def test_omits_source_line_when_source_file_unknown(self) -> None:
entries = [
ToolCatalogEntry(
name="orphan",
surfaces=("investigation",),
description="orphan desc",
source_file="",
input_schema_summary="(no params)",
)
]
text = format_tool_catalog_text(entries)
assert "**orphan**" in text
assert "source:" not in text
class TestListToolsSlashCommand:
"""``/tools list`` reaches the catalog and prints non-empty output."""
def _capture(self) -> tuple[Console, io.StringIO]:
buf = io.StringIO()
return Console(file=buf, force_terminal=False, highlight=False), buf
def test_list_tools_prints_grouped_catalog(self) -> None:
console, buf = self._capture()
session = Session()
# Stub the catalog so the test stays decoupled from registry contents.
fake = [
ToolCatalogEntry(
name="search_github",
surfaces=("investigation", "chat"),
description="Search GitHub code.",
source_file="tools/search_github.py",
input_schema_summary="query: string",
)
]
with patch(
"surfaces.interactive_shell.command_registry.tools_cmds.build_tool_catalog",
return_value=fake,
):
assert _cmd_tools(session, console, ["list"]) is True
out = buf.getvalue()
assert "search_github" in out
assert "investigation" in out
assert "chat" in out
assert "Search GitHub code." in out
def test_bare_tools_command_prints_full_registered_catalog(self) -> None:
console, buf = self._capture()
session = Session()
fake = [
ToolCatalogEntry(
name="telegram_send_message",
surfaces=("investigation", "chat"),
description="Send a Telegram message.",
source_file="tools/telegram_send_message_tool/tool.py",
input_schema_summary="message: string",
)
]
with patch(
"surfaces.interactive_shell.command_registry.tools_cmds.build_tool_catalog",
return_value=fake,
) as catalog:
assert dispatch_slash("/tools", session, console) is True
catalog.assert_called_once_with()
out = buf.getvalue()
assert "telegram_send_message" in out
assert "investigation" in out
assert "chat" in out
def test_live_catalog_includes_telegram_send_message(self) -> None:
names = {entry.name for entry in build_tool_catalog()}
assert "telegram_send_message" in names
def test_list_tools_disables_markup_for_plain_catalog_text(self) -> None:
console, buf = self._capture()
session = Session()
fake = [
ToolCatalogEntry(
name="risky_tool",
surfaces=("investigation",),
description="Payload [bold]injection[/bold] attempt",
source_file="tools/risky.py",
input_schema_summary="x: string",
)
]
with patch(
"surfaces.interactive_shell.command_registry.tools_cmds.build_tool_catalog",
return_value=fake,
):
assert _cmd_tools(session, console, ["list"]) is True
out = buf.getvalue()
assert "[bold]injection[/bold]" in out
def test_list_tools_handles_empty_registry(self) -> None:
console, buf = self._capture()
session = Session()
with patch(
"surfaces.interactive_shell.command_registry.tools_cmds.build_tool_catalog",
return_value=[],
):
assert _cmd_tools(session, console, ["list"]) is True
assert "no tools registered" in buf.getvalue()
def test_tools_first_args_advertise_list_for_tab_completion(self) -> None:
names = {arg for arg, _hint in _TOOLS_FIRST_ARGS}
assert "list" in names