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
@@ -0,0 +1 @@
# Package marker for mirrored tests.
@@ -0,0 +1,372 @@
"""Unit tests for the miss triage store and benchmark scenario conversion."""
from __future__ import annotations
import json
from datetime import UTC, datetime, timedelta
from pathlib import Path
import pytest
from core.domain.feedback import (
MissRecord,
MissTaxonomy,
compute_recurrence,
compute_stats,
load_misses,
record_miss,
to_benchmark_scenario,
)
from core.domain.feedback.misses import (
export_scenarios,
filter_top_misses,
misses_path,
parse_since,
taxonomy_choices,
)
@pytest.fixture
def opensre_home(monkeypatch, tmp_path: Path) -> Path:
home = tmp_path / ".opensre"
monkeypatch.setattr("config.constants.OPENSRE_HOME_DIR", home)
return home
# ── taxonomy enum surface ─────────────────────────────────────────────────────
def test_taxonomy_choices_includes_all_buckets() -> None:
keys = {key for key, _ in taxonomy_choices()}
assert keys == {t.value for t in MissTaxonomy}
# ── record_miss / load_misses round-trip ──────────────────────────────────────
def _feedback_dict(**overrides) -> dict:
base = {
"feedback_id": "fb-001",
"timestamp": "2026-06-02T10:00:00+00:00",
"run_id": "run-001",
"alert_name": "checkout-api 5xx",
"rating": "inaccurate",
"note": "missed the canary deploy from 09:42",
"root_cause": "The investigation concluded a DB outage.",
"root_cause_category": "database",
"validity_score": 0.45,
"investigation_loop_count": 6,
"user_id": "u-1",
"user_email": "u@example.com",
"org_id": "org-1",
}
base.update(overrides)
return base
def test_record_miss_returns_none_and_warns_on_write_failure(
opensre_home: Path, monkeypatch, capsys
) -> None:
def _explode(*_args, **_kwargs):
raise PermissionError("disk full")
monkeypatch.setattr("pathlib.Path.open", _explode)
result = record_miss(_feedback_dict(), taxonomy=MissTaxonomy.RETRIEVAL_GAP)
assert result is None
err = capsys.readouterr().err
assert "could not record miss" in err
assert "disk full" in err
def test_record_miss_persists_to_jsonl(opensre_home: Path) -> None:
feedback = _feedback_dict()
final_state = {"pipeline_name": "checkout/api", "severity": "critical"}
rec = record_miss(
feedback,
taxonomy=MissTaxonomy.RETRIEVAL_GAP,
final_state=final_state,
)
assert rec is not None
assert rec["taxonomy"] == "retrieval_gap"
assert rec["pipeline_name"] == "checkout/api"
assert rec["severity"] == "critical"
assert rec["feedback_id"] == "fb-001"
path = misses_path()
assert path.exists()
written = [json.loads(line) for line in path.read_text().splitlines() if line.strip()]
assert len(written) == 1
assert written[0]["miss_id"] == rec["miss_id"]
def test_record_miss_accepts_string_taxonomy(opensre_home: Path) -> None:
rec = record_miss(_feedback_dict(), taxonomy="reasoning_gap")
assert rec is not None
assert rec["taxonomy"] == "reasoning_gap"
def test_load_misses_skips_corrupt_lines(opensre_home: Path) -> None:
path = misses_path()
path.parent.mkdir(parents=True, exist_ok=True)
valid = {
"miss_id": "ok",
"alert_name": "a",
"taxonomy": "tool_failure",
"timestamp": "2026-06-02T00:00:00+00:00",
}
path.write_text(
"\n".join(
[
json.dumps(valid),
"not json at all",
"",
'{"miss_id": "partial"', # malformed
]
)
+ "\n",
encoding="utf-8",
)
rows = load_misses()
assert [r["miss_id"] for r in rows] == ["ok"]
def test_load_misses_filters_by_since_and_taxonomy(opensre_home: Path) -> None:
now = datetime.now(UTC)
record_miss(
_feedback_dict(timestamp=(now - timedelta(days=10)).isoformat(), alert_name="old"),
taxonomy=MissTaxonomy.RETRIEVAL_GAP,
)
record_miss(
_feedback_dict(
timestamp=(now - timedelta(hours=2)).isoformat(), alert_name="fresh-retrieval"
),
taxonomy=MissTaxonomy.RETRIEVAL_GAP,
)
record_miss(
_feedback_dict(timestamp=(now - timedelta(hours=1)).isoformat(), alert_name="fresh-tool"),
taxonomy=MissTaxonomy.TOOL_FAILURE,
)
recent = load_misses(since=now - timedelta(days=1))
assert {r["alert_name"] for r in recent} == {"fresh-retrieval", "fresh-tool"}
retrieval = load_misses(taxonomy=MissTaxonomy.RETRIEVAL_GAP)
assert {r["alert_name"] for r in retrieval} == {"old", "fresh-retrieval"}
# ── recurrence + stats ────────────────────────────────────────────────────────
def test_compute_recurrence_groups_by_alert_and_taxonomy(opensre_home: Path) -> None:
record_miss(_feedback_dict(alert_name="A"), taxonomy=MissTaxonomy.RETRIEVAL_GAP)
record_miss(
_feedback_dict(alert_name="A", feedback_id="fb-002"), taxonomy=MissTaxonomy.RETRIEVAL_GAP
)
record_miss(
_feedback_dict(alert_name="A", feedback_id="fb-003"), taxonomy=MissTaxonomy.REASONING_GAP
)
record_miss(
_feedback_dict(alert_name="B", feedback_id="fb-004"), taxonomy=MissTaxonomy.TOOL_FAILURE
)
rec = compute_recurrence(load_misses())
assert rec[("A", "retrieval_gap")] == 2
assert rec[("A", "reasoning_gap")] == 1
assert rec[("B", "tool_failure")] == 1
def test_compute_stats_reports_totals_and_recurring(opensre_home: Path) -> None:
record_miss(_feedback_dict(alert_name="A"), taxonomy=MissTaxonomy.RETRIEVAL_GAP)
record_miss(
_feedback_dict(alert_name="A", feedback_id="fb-002"), taxonomy=MissTaxonomy.RETRIEVAL_GAP
)
record_miss(
_feedback_dict(alert_name="B", feedback_id="fb-003"), taxonomy=MissTaxonomy.TOOL_FAILURE
)
stats = compute_stats(load_misses())
assert stats["total"] == 3
assert stats["by_taxonomy"]["retrieval_gap"] == 2
assert stats["by_taxonomy"]["tool_failure"] == 1
assert stats["unique_alerts"] == 2
assert stats["recurring"] == [("A", "retrieval_gap", 2)]
# ── filter_top_misses prioritises recurring pairs ─────────────────────────────
def test_filter_top_misses_dedupes_and_prefers_recurrence(opensre_home: Path) -> None:
now = datetime.now(UTC)
record_miss(
_feedback_dict(alert_name="A", timestamp=(now - timedelta(days=3)).isoformat()),
taxonomy=MissTaxonomy.RETRIEVAL_GAP,
)
record_miss(
_feedback_dict(
alert_name="A", feedback_id="fb-002", timestamp=(now - timedelta(days=1)).isoformat()
),
taxonomy=MissTaxonomy.RETRIEVAL_GAP,
)
record_miss(
_feedback_dict(alert_name="B", feedback_id="fb-003", timestamp=now.isoformat()),
taxonomy=MissTaxonomy.TOOL_FAILURE,
)
top = filter_top_misses(load_misses(), top=10)
pairs = [(r["alert_name"], r["taxonomy"]) for r in top]
# Deduped to one per (alert, taxonomy)
assert pairs == [("A", "retrieval_gap"), ("B", "tool_failure")]
def test_filter_top_misses_handles_zero_and_empty() -> None:
assert filter_top_misses([], top=5) == []
assert filter_top_misses([{"alert_name": "x"}], top=0) == []
# ── scenario conversion ──────────────────────────────────────────────────────
def test_to_benchmark_scenario_carries_rubric() -> None:
miss = MissRecord(
miss_id="m-1",
run_id="r-1",
alert_name="checkout-api 5xx",
pipeline_name="checkout/api",
severity="critical",
timestamp="2026-06-02T10:00:00+00:00",
taxonomy="retrieval_gap",
taxonomy_detail="missed the canary deploy",
root_cause="Bad deploy at 09:42",
root_cause_category="deploy",
)
scenario = to_benchmark_scenario(miss)
assert scenario["alert_name"] == "checkout-api 5xx"
assert scenario["title"].startswith("[Regression]")
assert scenario["pipeline_name"] == "checkout/api"
assert scenario["severity"] == "critical"
assert scenario["_meta"]["miss_id"] == "m-1"
assert scenario["_meta"]["taxonomy"] == "retrieval_gap"
# The rubric MUST live at commonAnnotations.scoring_points — that is
# where extract_scoring_points reads it and where
# strip_scoring_points_from_alert removes it before the agent sees the
# alert. Anywhere else and either the judge misses the rubric or the
# agent gets handed the answer.
rubric = scenario["commonAnnotations"]["scoring_points"]
assert rubric["expected_root_cause"] == "Bad deploy at 09:42"
assert rubric["expected_category"] == "deploy"
assert rubric["miss_notes"] == "missed the canary deploy"
assert "scoring_points" not in scenario["_meta"]
def test_to_benchmark_scenario_is_strippable_for_blind_agent_runs() -> None:
"""Confirm the produced scenario is compatible with the existing
strip_scoring_points_from_alert helper — i.e. the rubric ends up where
the helper expects and is actually removed for non-evaluate runs."""
from integrations.opensre import (
extract_scoring_points,
strip_scoring_points_from_alert,
)
miss = MissRecord(
miss_id="m-1",
alert_name="checkout-api 5xx",
taxonomy="retrieval_gap",
taxonomy_detail="missed the canary deploy",
root_cause="Bad deploy at 09:42",
root_cause_category="deploy",
)
scenario = to_benchmark_scenario(miss)
# Judge can read the rubric.
rubric_text = extract_scoring_points(scenario)
assert "Bad deploy at 09:42" in rubric_text
# Strip removes it cleanly — agent does not see the answer.
blind = strip_scoring_points_from_alert(scenario)
assert "scoring_points" not in blind["commonAnnotations"]
assert extract_scoring_points(blind) == ""
def test_export_scenarios_handles_json_null_alert_name_and_taxonomy(tmp_path: Path) -> None:
"""A JSON null in misses.jsonl returns Python None from dict.get — the
export path must not crash _slugify's re.sub on those values."""
misses: list[MissRecord] = [
MissRecord(
miss_id="m-1",
alert_name=None, # type: ignore[typeddict-item]
taxonomy=None, # type: ignore[typeddict-item]
timestamp="2026-06-02T10:00:00+00:00",
root_cause="rc",
),
]
out = tmp_path / "scenarios"
written = export_scenarios(misses, out)
assert len(written) == 1
# Slug falls back to the index + "unknown" taxonomy rather than crashing.
assert written[0].parent.name == "0001_miss-0001_unknown"
def test_export_scenarios_writes_per_case_directories(tmp_path: Path) -> None:
misses: list[MissRecord] = [
MissRecord(
miss_id="m-1",
alert_name="checkout 5xx",
taxonomy="retrieval_gap",
timestamp="2026-06-02T10:00:00+00:00",
root_cause="rc",
),
MissRecord(
miss_id="m-2",
alert_name="checkout 5xx",
taxonomy="reasoning_gap",
timestamp="2026-06-02T11:00:00+00:00",
root_cause="rc2",
),
]
out = tmp_path / "scenarios"
written = export_scenarios(misses, out)
assert len(written) == 2
assert all(p.name == "alert.json" for p in written)
parents = [p.parent.name for p in written]
assert any("retrieval_gap" in name for name in parents)
assert any("reasoning_gap" in name for name in parents)
payload = json.loads(written[0].read_text())
assert payload["_meta"]["taxonomy"] in {"retrieval_gap", "reasoning_gap"}
# ── parse_since input parsing ────────────────────────────────────────────────
def test_parse_since_accepts_relative_units() -> None:
now = datetime.now(UTC)
one_day_ago = parse_since("1d")
assert (now - one_day_ago) < timedelta(seconds=5) + timedelta(days=1)
assert (now - one_day_ago) > timedelta(hours=23, minutes=55)
assert parse_since("12h") < now
assert parse_since("2w") < now - timedelta(days=13)
def test_parse_since_accepts_iso_timestamp() -> None:
parsed = parse_since("2026-06-02T10:00:00+00:00")
assert parsed == datetime(2026, 6, 2, 10, 0, tzinfo=UTC)
def test_parse_since_rejects_garbage() -> None:
with pytest.raises(ValueError):
parse_since("")
with pytest.raises(ValueError):
parse_since("forever ago")
@@ -0,0 +1,36 @@
"""ShellOutputSink.render_error appends ``/model`` and ``/auth login`` hints on a credit-exhausted error."""
from __future__ import annotations
from core.llm.shared.llm_retry import CREDIT_EXHAUSTED_MARKER
from surfaces.interactive_shell.runtime.agent_harness_adapters import ShellOutputSink
class _RecordingConsole:
def __init__(self) -> None:
self.lines: list[str] = []
def print(self, message: str = "") -> None:
self.lines.append(str(message))
def _render_error(message: str) -> str:
console = _RecordingConsole()
ShellOutputSink(console).render_error(message) # type: ignore[arg-type]
return "\n".join(console.lines)
def test_render_error_shows_model_hint_on_credit_exhaustion() -> None:
output = _render_error(f"Anthropic {CREDIT_EXHAUSTED_MARKER}. Original error: 400")
assert "/model" in output
def test_render_error_shows_auth_login_hint_on_credit_exhaustion() -> None:
output = _render_error(f"Anthropic {CREDIT_EXHAUSTED_MARKER}. Original error: 400")
assert "/auth login" in output
def test_render_error_no_hint_for_generic_error() -> None:
output = _render_error("some other failure")
assert "/model" not in output
assert "/auth login" not in output
@@ -0,0 +1,51 @@
"""The turn-error render adds ``/model`` and ``/auth login`` recovery hints on a credit-exhausted error."""
from __future__ import annotations
import asyncio
from unittest.mock import MagicMock
from core.llm.shared.llm_retry import LLMCreditExhaustedError
from surfaces.interactive_shell.runtime.agent_presentation import (
AgentEvent,
AgentPresentationState,
_render_agent_presentation_transition,
)
class _RecordingConsole:
def __init__(self) -> None:
self.lines: list[str] = []
def print(self, text: str = "") -> None:
self.lines.append(text)
def _render_turn_error(error: Exception) -> str:
console = _RecordingConsole()
asyncio.run(
_render_agent_presentation_transition(
previous=AgentPresentationState(),
current=AgentPresentationState(),
event=AgentEvent(type="turn_error", error=error),
console=console, # type: ignore[arg-type]
spinner=MagicMock(),
)
)
return "\n".join(console.lines)
def test_credit_exhausted_turn_error_shows_model_hint() -> None:
output = _render_turn_error(LLMCreditExhaustedError("Anthropic credit exhausted"))
assert "/model" in output
def test_credit_exhausted_turn_error_shows_auth_login_hint() -> None:
output = _render_turn_error(LLMCreditExhaustedError("Anthropic credit exhausted"))
assert "/auth login" in output
def test_other_turn_error_has_no_model_hint() -> None:
output = _render_turn_error(RuntimeError("something else broke"))
assert "/model" not in output
assert "/auth login" not in output
@@ -0,0 +1,76 @@
from __future__ import annotations
from surfaces.interactive_shell.runtime.background.notifications import (
deliver_background_notifications,
)
from surfaces.interactive_shell.session.background_investigations import (
BackgroundInvestigationRecord,
)
def test_deliver_background_notifications_sends_email_when_smtp_is_configured(
monkeypatch,
) -> None:
monkeypatch.setattr(
"integrations.catalog.resolve_effective_integrations",
lambda: {
"smtp": {
"source": "local env",
"config": {
"host": "smtp.example.com",
"port": 587,
"security": "starttls",
"from_address": "opensre@example.com",
"default_to": "team@example.com",
},
}
},
)
captured: dict[str, object] = {}
def _fake_send_smtp_report(
*, report: str, subject: str, smtp_ctx: dict[str, object]
) -> tuple[bool, str]:
captured["report"] = report
captured["subject"] = subject
captured["smtp_ctx"] = smtp_ctx
return True, ""
monkeypatch.setattr(
"integrations.smtp.delivery.send_smtp_report",
_fake_send_smtp_report,
)
record = BackgroundInvestigationRecord(
task_id="bg-123",
status="completed",
command="/investigate checkout-latency",
root_cause="postgres connection pool saturation",
top_analysis=("rds cpu spike",),
next_steps=("raise pool size",),
stats={"tool_call_count": 4, "investigation_loop_count": 2, "validity_score": 0.8},
)
results = deliver_background_notifications(record=record, channels=("email",))
assert results == {"email": "sent"}
assert captured["subject"] == "OpenSRE RCA complete: bg-123"
assert "Root cause" in str(captured["report"])
def test_deliver_background_notifications_skips_when_no_channels_configured() -> None:
record = BackgroundInvestigationRecord(
task_id="bg-123", status="completed", command="free-text"
)
results = deliver_background_notifications(record=record, channels=())
assert results == {}
def test_deliver_background_notifications_marks_missing_smtp(monkeypatch) -> None:
monkeypatch.setattr("integrations.catalog.resolve_effective_integrations", lambda: {})
record = BackgroundInvestigationRecord(
task_id="bg-123", status="completed", command="free-text"
)
results = deliver_background_notifications(record=record, channels=("email",))
assert results == {"email": "missing smtp integration"}
@@ -0,0 +1,71 @@
from __future__ import annotations
from unittest.mock import MagicMock
import pytest
from rich.console import Console
from surfaces.interactive_shell.runtime.background.runner import (
drain_background_notices,
start_background_template_investigation,
)
from surfaces.interactive_shell.session import Session
def test_enqueue_and_drain_background_notices() -> None:
import io
from rich.console import Console
session = Session()
session.terminal.enqueue_background_notice("[bold]done[/bold]")
console = Console(file=io.StringIO(), force_terminal=False, highlight=False)
drain_background_notices(session, console)
assert session.terminal.drain_background_notices() == []
assert "done" in console.file.getvalue()
def test_start_background_template_investigation_assigns_fresh_investigation_id(
monkeypatch: pytest.MonkeyPatch,
) -> None:
import io
session = Session()
session.last_investigation_id = "inv-stale"
console = Console(file=io.StringIO(), force_terminal=False, highlight=False)
def _fake_run(
*,
cancel_requested,
template_name: str,
context_overrides,
) -> dict[str, object]:
_ = (cancel_requested, template_name, context_overrides)
return {"root_cause": "done"}
monkeypatch.setattr(
"surfaces.interactive_shell.runtime.investigation_adapter.run_sample_alert_for_session_background",
_fake_run,
)
monkeypatch.setattr(
"surfaces.interactive_shell.runtime.background.runner.track_investigation",
lambda **_kwargs: MagicMock(
__enter__=MagicMock(return_value=MagicMock()),
__exit__=MagicMock(return_value=False),
),
)
task_id = start_background_template_investigation(
template_name="generic",
session=session,
console=console,
display_command="/investigate generic",
investigation_target="generic",
)
assert session.last_investigation_id
assert session.last_investigation_id != "inv-stale"
record = session.terminal.background_investigations[task_id]
assert record.investigation_id == session.last_investigation_id
@@ -0,0 +1,195 @@
"""Tests for validated interactive-shell runtime context assembly."""
from __future__ import annotations
import pytest
from prompt_toolkit import PromptSession
from prompt_toolkit.application import create_app_session
from prompt_toolkit.history import InMemoryHistory
from prompt_toolkit.input import DummyInput
from prompt_toolkit.output import DummyOutput
from pydantic import ValidationError
from platform.common.task_registry import TaskRegistry
from surfaces.interactive_shell.controller import InteractiveShellController
from surfaces.interactive_shell.runtime.context import (
ReplRuntimeContext,
SessionBootstrapSpec,
create_repl_runtime_context,
)
from surfaces.interactive_shell.runtime.core.state import (
ReplState,
SpinnerState,
)
from surfaces.interactive_shell.session import Session
def _prompt_session() -> PromptSession[str]:
return PromptSession(history=InMemoryHistory())
def test_create_context_applies_canonical_session_bootstrap(
monkeypatch: pytest.MonkeyPatch,
) -> None:
registry = TaskRegistry()
hydrate_calls: list[str] = []
def _hydrate(self: Session) -> None:
hydrate_calls.append(self.session_id)
self.configured_integrations = ("github",)
self.configured_integrations_known = True
monkeypatch.setattr(Session, "hydrate_configured_integrations", _hydrate)
monkeypatch.setattr(TaskRegistry, "persistent", staticmethod(lambda: registry))
with create_app_session(input=DummyInput(), output=DummyOutput()):
prompt = _prompt_session()
session = Session()
context = create_repl_runtime_context(
session=session,
pt_session=prompt,
active_theme_name="pink",
)
assert context.session is session
assert session.terminal.active_theme_name == "pink"
assert session.configured_integrations == ("github",)
assert session.configured_integrations_known is True
assert hydrate_calls == [session.session_id]
assert session.task_registry is registry
assert session.terminal.prompt_history_backend is prompt.history
def test_context_supports_lightweight_bootstrap_for_unit_seams(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(
Session,
"hydrate_configured_integrations",
lambda _self: (_ for _ in ()).throw(AssertionError("hydrated")),
)
monkeypatch.setattr(
TaskRegistry,
"persistent",
staticmethod(lambda: (_ for _ in ()).throw(AssertionError("persistent"))),
)
context = create_repl_runtime_context(
active_theme_name="green",
hydrate_integrations=False,
persistent_tasks=False,
)
assert context.session.terminal.active_theme_name == "green"
assert isinstance(context.state, ReplState)
assert isinstance(context.spinner, SpinnerState)
def test_create_context_registers_jsonl_session_trace_sink(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""REPL boot wires the JSONL session-trace sink for ATM / span metrics."""
from platform.observability.trace.spans import (
NoopSessionTraceSink,
get_session_trace_sink,
is_session_trace_active,
set_session_trace_sink,
)
from surfaces.interactive_shell.session.trace_sink import JsonlSessionTraceSink
monkeypatch.setattr(
Session,
"hydrate_configured_integrations",
lambda _self: None,
)
monkeypatch.setattr(
TaskRegistry,
"persistent",
staticmethod(TaskRegistry),
)
set_session_trace_sink(NoopSessionTraceSink())
try:
create_repl_runtime_context(
hydrate_integrations=False,
persistent_tasks=False,
)
assert is_session_trace_active()
assert isinstance(get_session_trace_sink(), JsonlSessionTraceSink)
finally:
set_session_trace_sink(NoopSessionTraceSink())
def test_create_context_uses_noop_trace_sink_for_in_memory_session(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from core.agent_harness.session import InMemorySessionStorage
from platform.observability.trace.spans import (
NoopSessionTraceSink,
get_session_trace_sink,
is_session_trace_active,
set_session_trace_sink,
)
monkeypatch.setattr(
Session,
"hydrate_configured_integrations",
lambda _self: None,
)
set_session_trace_sink(NoopSessionTraceSink())
try:
create_repl_runtime_context(
session=Session(storage=InMemorySessionStorage()),
hydrate_integrations=False,
persistent_tasks=False,
)
assert not is_session_trace_active()
assert isinstance(get_session_trace_sink(), NoopSessionTraceSink)
finally:
set_session_trace_sink(NoopSessionTraceSink())
def test_context_uses_canonical_initial_mutable_state() -> None:
context = ReplRuntimeContext(session=Session())
assert isinstance(context.state, ReplState)
assert isinstance(context.spinner, SpinnerState)
assert context.state.exit_requested is False
assert context.spinner.streaming is False
def test_context_preserves_explicit_partial_mutable_state() -> None:
state = ReplState()
context = ReplRuntimeContext(session=Session(), state=state)
assert context.state is state
assert isinstance(context.spinner, SpinnerState)
def test_context_rejects_invalid_state_contracts() -> None:
with pytest.raises(ValidationError):
ReplRuntimeContext(session=object()) # type: ignore[arg-type]
with pytest.raises(ValidationError):
SessionBootstrapSpec(active_theme_name=" ")
def test_context_assignment_validates_inbox_type() -> None:
context = ReplRuntimeContext(session=Session())
with pytest.raises(ValidationError):
context.inbox = object() # type: ignore[assignment]
def test_controller_reuses_validated_runtime_context() -> None:
session = Session()
state = ReplState()
spinner = SpinnerState()
context = ReplRuntimeContext(session=session, state=state, spinner=spinner)
controller = InteractiveShellController(context)
assert controller.runtime_context is context
assert controller.session is session
assert controller.state is state
assert controller.spinner is spinner
assert controller.input_reader.state is state
@@ -0,0 +1,92 @@
"""Pure input-action decisions for the interactive shell controller."""
from __future__ import annotations
import pytest
from surfaces.interactive_shell.runtime.input import (
InputCancelled,
InputClosed,
InputEvent,
InputSubmitted,
)
from surfaces.interactive_shell.runtime.input.actions import (
QUEUE_DURING_CONFIRMATION_WARNING,
CancelTurn,
CloseShell,
DeliverConfirmation,
IgnoreInput,
ShellInputSnapshot,
SubmitTurn,
decide_input_action,
)
def _decide(
event: InputEvent,
*,
exit_requested: bool = False,
dispatch_running: bool = False,
awaiting_confirmation: bool = False,
needs_exclusive_stdin: bool = False,
) -> object:
return decide_input_action(
event,
ShellInputSnapshot(
exit_requested=exit_requested,
dispatch_running=dispatch_running,
awaiting_confirmation=awaiting_confirmation,
),
needs_exclusive_stdin=lambda _text: needs_exclusive_stdin,
)
def test_decide_closes_on_input_closed() -> None:
assert _decide(InputClosed()) == CloseShell()
def test_decide_cancels_on_input_cancelled() -> None:
assert _decide(InputCancelled()) == CancelTurn()
@pytest.mark.parametrize("text", ["", " "])
def test_decide_ignores_empty_or_blank_submissions(text: str) -> None:
assert _decide(InputSubmitted(text)) == IgnoreInput()
def test_decide_ignores_submitted_input_after_exit_requested() -> None:
assert _decide(InputSubmitted("/status"), exit_requested=True) == IgnoreInput()
def test_decide_cancels_when_cancel_request_is_typed_during_dispatch() -> None:
assert _decide(InputSubmitted(" /cancel "), dispatch_running=True) == CancelTurn(
submitted_text="/cancel"
)
def test_decide_delivers_stripped_confirmation_answer() -> None:
assert _decide(
InputSubmitted(" yes "),
awaiting_confirmation=True,
) == DeliverConfirmation(text="yes")
def test_decide_submits_non_confirmation_input_while_confirmation_is_pending() -> None:
assert _decide(
InputSubmitted("run /status"),
awaiting_confirmation=True,
) == SubmitTurn(
text="run /status",
warning=QUEUE_DURING_CONFIRMATION_WARNING,
)
def test_decide_submits_normal_turn_without_wait_by_default() -> None:
assert _decide(InputSubmitted(" show status ")) == SubmitTurn(text="show status")
def test_decide_submits_normal_turn_with_exclusive_stdin_wait() -> None:
assert _decide(InputSubmitted("/integrations"), needs_exclusive_stdin=True) == SubmitTurn(
text="/integrations",
wait_until_idle=True,
)
@@ -0,0 +1,337 @@
"""Tests for hydrating configured integrations onto the REPL session at boot.
Without this the agent cannot answer "is X installed?" and the integration
guards stay dead because ``configured_integrations_known`` never flips to True.
"""
from __future__ import annotations
import io
from pathlib import Path
from types import SimpleNamespace
from typing import Any
from rich.console import Console
import surfaces.interactive_shell.main as main_entrypoint
from core.agent_harness.session.integration_resolution import IntegrationResolutionResult
from surfaces.interactive_shell.runtime.startup import first_launch_github as flg
from surfaces.interactive_shell.session import Session
def _console() -> Console:
return Console(file=io.StringIO(), force_terminal=False, highlight=False)
def test_hydrate_populates_session_from_effective_resolution(monkeypatch: Any) -> None:
monkeypatch.setattr(
"platform.harness_ports.configured_integration_services",
lambda: ["gitlab", "datadog"],
)
session = Session()
session.hydrate_configured_integrations()
assert session.configured_integrations_known is True
# Metadata discovery covers env + local store and is returned in sorted order.
assert session.configured_integrations == ("datadog", "gitlab")
def test_hydrate_marks_known_even_when_none_configured(monkeypatch: Any) -> None:
monkeypatch.setattr(
"platform.harness_ports.configured_integration_services",
list,
)
session = Session()
session.hydrate_configured_integrations()
assert session.configured_integrations_known is True
assert session.configured_integrations == ()
def test_warm_resolved_integrations_populates_cache(monkeypatch: Any) -> None:
resolved = {"datadog": {"site": "datadoghq.com"}, "grafana": {"url": "http://localhost"}}
monkeypatch.setattr(
"core.agent_harness.session.integration_resolution.resolve_integrations",
lambda: resolved,
)
session = Session()
session.warm_resolved_integrations()
assert session.resolved_integrations_cache == resolved
def test_warm_resolved_integrations_is_idempotent(monkeypatch: Any) -> None:
calls: list[str] = []
def _resolve() -> dict[str, Any]:
calls.append("resolve")
return {"github": {}}
monkeypatch.setattr(
"core.agent_harness.session.integration_resolution.resolve_integrations",
_resolve,
)
session = Session()
session.warm_resolved_integrations()
session.warm_resolved_integrations()
assert calls == ["resolve"]
def test_warm_resolved_integrations_skips_empty_cache(monkeypatch: Any) -> None:
calls: list[str] = []
def _resolve() -> dict[str, Any]:
calls.append("resolve")
return {}
monkeypatch.setattr(
"core.agent_harness.session.integration_resolution.resolve_integrations",
_resolve,
)
session = Session()
session.warm_resolved_integrations()
assert session.resolved_integrations_cache is None
session.warm_resolved_integrations()
assert calls == ["resolve", "resolve"]
def test_warm_resolved_integrations_uses_quiet_resolve(monkeypatch: Any) -> None:
progress_calls: list[str] = []
quiet_calls: list[str] = []
monkeypatch.setattr(
"tools.investigation.stages.resolve_integrations.resolve_integrations",
lambda _state: progress_calls.append("progress") or {"resolved_integrations": {}},
)
monkeypatch.setattr(
"core.agent_harness.session.integration_resolution.resolve_integrations",
lambda: quiet_calls.append("quiet") or {"datadog": {}},
)
session = Session()
session.warm_resolved_integrations()
assert quiet_calls == ["quiet"]
assert progress_calls == []
assert session.resolved_integrations_cache == {"datadog": {}}
def test_get_integrations_returns_pydantic_cached_result(monkeypatch: Any) -> None:
def _unexpected_resolve() -> dict[str, Any]:
raise AssertionError("cached integrations should not re-resolve")
monkeypatch.setattr(
"core.agent_harness.session.integration_resolution.resolve_integrations",
_unexpected_resolve,
)
session = Session()
session.resolved_integrations_cache = {"datadog": {"site": "datadoghq.com"}}
result = session.get_integrations()
assert isinstance(result, IntegrationResolutionResult)
assert result.resolved_integrations == {"datadog": {"site": "datadoghq.com"}}
assert result.services == ("datadog",)
def test_get_integrations_respects_explicit_empty_cache(monkeypatch: Any) -> None:
calls: list[str] = []
monkeypatch.setattr(
"core.agent_harness.session.integration_resolution.resolve_integrations",
lambda: calls.append("resolve") or {"datadog": {}},
)
session = Session()
session.resolved_integrations_cache = {}
result = session.get_integrations()
assert result.resolved_integrations == {}
assert calls == []
def test_get_integrations_warms_metadata_only_cache(monkeypatch: Any) -> None:
calls: list[str] = []
monkeypatch.setattr(
"core.agent_harness.session.integration_resolution.resolve_integrations",
lambda: calls.append("resolve") or {"datadog": {"site": "datadoghq.com"}},
)
session = Session()
session.resolved_integrations_cache = {"_gateway_chat_id": "chat-1"}
result = session.get_integrations()
assert calls == ["resolve"]
assert result.resolved_integrations == {
"_gateway_chat_id": "chat-1",
"datadog": {"site": "datadoghq.com"},
}
def test_stale_background_warm_does_not_overwrite_refreshed_cache() -> None:
session = Session()
stale_generation = session.integrations._warm_generation
session.integrations._warm_generation += 1
session.integrations._store(
{"fresh": {"token": "new"}}, generation=session.integrations._warm_generation
)
session.integrations._store({"stale": {"token": "old"}}, generation=stale_generation)
assert session.resolved_integrations_cache == {"fresh": {"token": "new"}}
def test_hydrate_entrypoint_does_not_warm_before_prompt(monkeypatch: Any) -> None:
monkeypatch.setattr(
"platform.harness_ports.configured_integration_services",
lambda: ["datadog"],
)
resolve_calls: list[str] = []
def _resolve() -> dict[str, Any]:
resolve_calls.append("resolve")
return {"datadog": {"site": "datadoghq.com"}}
monkeypatch.setattr(
"core.agent_harness.session.integration_resolution.resolve_integrations",
_resolve,
)
session = Session()
session.hydrate_configured_integrations()
assert session.configured_integrations_known is True
assert session.resolved_integrations_cache is None
assert resolve_calls == []
def test_hydrate_leaves_unknown_on_failure(monkeypatch: Any) -> None:
def _boom() -> list[str]:
raise RuntimeError("catalog blew up")
monkeypatch.setattr(
"platform.harness_ports.configured_integration_services",
_boom,
)
session = Session()
session.hydrate_configured_integrations()
assert session.configured_integrations_known is False
assert session.configured_integrations == ()
def test_gate_error_blocks_startup_without_bypass(monkeypatch: Any) -> None:
"""On an unexpected gate error we must NOT fail open into the REPL unless an
explicit bypass applies."""
monkeypatch.setattr(
flg,
"should_require_github_login",
lambda: (_ for _ in ()).throw(RuntimeError("gate broke")),
)
monkeypatch.setattr(flg, "_github_login_explicitly_bypassed", lambda: False)
assert flg.require_startup_github_login(_console()) is False
def test_gate_error_allows_startup_with_bypass(monkeypatch: Any) -> None:
monkeypatch.setattr(
flg,
"should_require_github_login",
lambda: (_ for _ in ()).throw(RuntimeError("gate broke")),
)
monkeypatch.setattr(flg, "_github_login_explicitly_bypassed", lambda: True)
assert flg.require_startup_github_login(_console()) is True
def test_run_repl_async_identifies_saved_github_username(monkeypatch: Any) -> None:
identified: list[str] = []
monkeypatch.setattr(
"platform.analytics.cli.identify_saved_github_username",
lambda: identified.append("called"),
)
def _run_initial_input(*_args: Any, **_kwargs: Any) -> int:
return 0
monkeypatch.setattr(main_entrypoint, "run_initial_input", _run_initial_input)
class _Session:
active_theme_name = None
def hydrate_configured_integrations(self) -> None:
return None
def warm_resolved_integrations(self) -> None:
return None
monkeypatch.setattr(
main_entrypoint,
"create_repl_runtime_context",
lambda **_kwargs: SimpleNamespace(session=_Session(), inbox=None),
)
class _PromptSession:
history = None
monkeypatch.setattr(
main_entrypoint,
"build_prompt_session",
lambda: _PromptSession(),
)
import asyncio
asyncio.run(main_entrypoint.run_repl_async(initial_input="hello"))
assert identified == ["called"]
def test_run_repl_async_failed_resume_flushes_starter_session(
monkeypatch: Any, tmp_path: Path
) -> None:
import asyncio
sessions_dir = tmp_path / "sessions"
sessions_dir.mkdir()
monkeypatch.setattr("config.constants.OPENSRE_HOME_DIR", tmp_path)
monkeypatch.setattr(
"platform.analytics.cli.identify_saved_github_username",
lambda: None,
)
monkeypatch.setattr(
"surfaces.interactive_shell.command_registry.session_cmds.resume.resume_session_by_prefix",
lambda *_args, **_kwargs: False,
)
session = Session()
flushed: list[str] = []
original_flush = session.storage.flush
def _track_flush(current_session: Session) -> None:
flushed.append(current_session.session_id)
original_flush(current_session)
monkeypatch.setattr(session.storage, "flush", _track_flush)
class _PromptSession:
history = None
monkeypatch.setattr(
main_entrypoint,
"build_prompt_session",
lambda: _PromptSession(),
)
monkeypatch.setattr(
main_entrypoint,
"create_repl_runtime_context",
lambda **_kwargs: SimpleNamespace(session=session, inbox=None),
)
exit_code = asyncio.run(main_entrypoint.run_repl_async(resume_session_id="missing-session"))
assert exit_code == 1
assert flushed == [session.session_id]
assert not (sessions_dir / f"{session.session_id}.jsonl").exists()
def test_explicit_bypass_detects_skip_env(monkeypatch: Any) -> None:
monkeypatch.setenv("OPENSRE_SKIP_GITHUB_LOGIN", "1")
assert flg._github_login_explicitly_bypassed() is True
def test_explicit_bypass_detects_ci_environment(monkeypatch: Any) -> None:
monkeypatch.delenv("OPENSRE_SKIP_GITHUB_LOGIN", raising=False)
monkeypatch.setenv("CI", "true")
assert flg._github_login_explicitly_bypassed() is True
@@ -0,0 +1,281 @@
from __future__ import annotations
import io
import pytest
from rich.console import Console
from integrations.github import login as github_login_mod
from integrations.github.login import GitHubLoginResult
from integrations.github.mcp import DEFAULT_GITHUB_MCP_TOOLSETS, DEFAULT_GITHUB_MCP_URL
from integrations.github.mcp_oauth import GitHubDeviceCode
from platform.analytics import source as analytics_source
from platform.terminal import theme as ui_theme
from surfaces.interactive_shell.runtime.startup import first_launch_github as flg
def _console() -> Console:
return Console(file=io.StringIO(), force_terminal=False, highlight=False)
def _terminal_console(output: io.StringIO) -> Console:
return Console(file=output, force_terminal=True, color_system="truecolor", highlight=False)
def _force_required(monkeypatch: pytest.MonkeyPatch) -> None:
"""Set every gate input so that login would be required."""
monkeypatch.delenv("OPENSRE_SKIP_GITHUB_LOGIN", raising=False)
monkeypatch.setattr(flg, "is_test_run", lambda: False)
monkeypatch.setattr(flg, "repl_tty_interactive", lambda: True)
monkeypatch.setattr(flg, "_github_already_configured", lambda: False)
monkeypatch.setattr(flg, "read_github_login_deferred", lambda: False)
def test_gate_required_when_all_conditions_met(monkeypatch: pytest.MonkeyPatch) -> None:
_force_required(monkeypatch)
assert flg.should_require_github_login() is True
@pytest.mark.parametrize("value", ["1", "true", "YES", "on"])
def test_gate_skipped_by_env(monkeypatch: pytest.MonkeyPatch, value: str) -> None:
_force_required(monkeypatch)
monkeypatch.setenv("OPENSRE_SKIP_GITHUB_LOGIN", value)
assert flg.should_require_github_login() is False
def test_gate_skipped_in_test_run(monkeypatch: pytest.MonkeyPatch) -> None:
_force_required(monkeypatch)
monkeypatch.setattr(flg, "is_test_run", lambda: True)
assert flg.should_require_github_login() is False
def test_gate_required_on_linux_when_interactive(monkeypatch: pytest.MonkeyPatch) -> None:
_force_required(monkeypatch)
assert flg.should_require_github_login() is True
def test_gate_skipped_when_not_tty(monkeypatch: pytest.MonkeyPatch) -> None:
_force_required(monkeypatch)
monkeypatch.setattr(flg, "repl_tty_interactive", lambda: False)
assert flg.should_require_github_login() is False
def test_gate_skipped_when_github_already_configured(monkeypatch: pytest.MonkeyPatch) -> None:
_force_required(monkeypatch)
monkeypatch.setattr(flg, "_github_already_configured", lambda: True)
assert flg.should_require_github_login() is False
def test_gate_required_when_github_not_configured(monkeypatch: pytest.MonkeyPatch) -> None:
"""Regression: GitHub config is authoritative. A prior completed login (no
longer recorded via any standalone marker) must not let the REPL start once
the GitHub integration has been removed."""
_force_required(monkeypatch)
monkeypatch.setattr(flg, "_github_already_configured", lambda: False)
assert flg.should_require_github_login() is True
@pytest.mark.parametrize(
"ci_env",
[
("CI", "true"),
("GITHUB_ACTIONS", "true"),
("OPENSRE_IS_TEST", "1"),
],
)
def test_gate_skipped_in_ci_like_environment(
monkeypatch: pytest.MonkeyPatch, ci_env: tuple[str, str]
) -> None:
monkeypatch.delenv("OPENSRE_SKIP_GITHUB_LOGIN", raising=False)
monkeypatch.delenv("CI", raising=False)
monkeypatch.delenv("GITHUB_ACTIONS", raising=False)
monkeypatch.delenv("OPENSRE_IS_TEST", raising=False)
monkeypatch.delenv("OPENSRE_INVESTIGATION_SOURCE", raising=False)
monkeypatch.delenv("PYTEST_CURRENT_TEST", raising=False)
monkeypatch.setattr(flg, "is_test_run", analytics_source.is_test_run)
monkeypatch.setattr(flg, "repl_tty_interactive", lambda: True)
monkeypatch.setattr(flg, "_github_already_configured", lambda: False)
monkeypatch.setenv(ci_env[0], ci_env[1])
assert flg.should_require_github_login() is False
def test_gate_skipped_when_github_login_deferred(monkeypatch: pytest.MonkeyPatch) -> None:
_force_required(monkeypatch)
monkeypatch.setattr(flg, "read_github_login_deferred", lambda: True)
assert flg.should_require_github_login() is False
def test_gate_required_when_stale_github_store_record_has_no_token(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Legacy installs may have an abandoned github store row without credentials."""
_force_required(monkeypatch)
monkeypatch.setattr(
"integrations.store.get_integration",
lambda service: (
{
"credentials": {
"mode": "streamable-http",
"url": DEFAULT_GITHUB_MCP_URL,
"toolsets": list(DEFAULT_GITHUB_MCP_TOOLSETS),
}
}
if service == "github"
else None
),
)
assert flg.should_require_github_login() is True
def test_device_code_prompt_highlights_user_code(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("NO_COLOR", raising=False)
monkeypatch.delenv("FORCE_COLOR", raising=False)
ui_theme.set_active_theme("blue")
output = io.StringIO()
code = GitHubDeviceCode(
device_code="dev-123",
user_code="WXYZ-1234",
verification_uri="https://github.com/login/device",
expires_in=900,
interval=5,
)
flg._show_device_code(_terminal_console(output), code)
rendered = output.getvalue()
assert f"{ui_theme.DEVICE_CODE_ANSI}WXYZ-1234" in rendered
def test_orchestrator_success_proceeds_and_propagates(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(flg, "_offer_github_login", lambda _console: True)
monkeypatch.setattr(
github_login_mod,
"authenticate_and_configure_github",
lambda **_kwargs: GitHubLoginResult(ok=True, username="octocat", detail="OK"),
)
completed: list[str] = []
monkeypatch.setattr(flg, "capture_github_login_completed", completed.append)
cleared: list[bool] = []
monkeypatch.setattr(flg, "clear_github_login_deferral", lambda: cleared.append(True))
proceed = flg.require_github_login_on_first_launch(_console())
assert proceed is True
assert completed == ["octocat"]
assert cleared == [True]
def test_orchestrator_skip_at_menu_proceeds(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(flg, "_offer_github_login", lambda _console: False)
deferred: list[bool] = []
monkeypatch.setattr(flg, "write_github_login_deferred", deferred.append)
proceed = flg.require_github_login_on_first_launch(_console())
assert proceed is True
assert deferred == [True]
def test_orchestrator_escape_during_wait_proceeds(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(flg, "_offer_github_login", lambda _console: True)
def _raise_cancel(**_kwargs: object) -> GitHubLoginResult:
raise KeyboardInterrupt
monkeypatch.setattr(github_login_mod, "authenticate_and_configure_github", _raise_cancel)
deferred: list[bool] = []
monkeypatch.setattr(flg, "write_github_login_deferred", deferred.append)
proceed = flg.require_github_login_on_first_launch(_console())
assert proceed is True
assert deferred == [True]
def test_orchestrator_failure_then_decline_retry_skips(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(flg, "_offer_github_login", lambda _console: True)
monkeypatch.setattr(
github_login_mod,
"authenticate_and_configure_github",
lambda **_kwargs: GitHubLoginResult(ok=False, detail="cannot verify"),
)
monkeypatch.setattr(flg, "_ask_retry", lambda _console: False)
deferred: list[bool] = []
monkeypatch.setattr(flg, "write_github_login_deferred", deferred.append)
proceed = flg.require_github_login_on_first_launch(_console())
assert proceed is True
assert deferred == [True]
def test_orchestrator_retries_until_success(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(flg, "_offer_github_login", lambda _console: True)
calls = {"n": 0}
def _login(**_kwargs: object) -> GitHubLoginResult:
calls["n"] += 1
if calls["n"] == 1:
return GitHubLoginResult(ok=False, detail="cannot verify")
return GitHubLoginResult(ok=True, username="octocat", detail="OK")
monkeypatch.setattr(github_login_mod, "authenticate_and_configure_github", _login)
monkeypatch.setattr(flg, "_ask_retry", lambda _console: True)
monkeypatch.setattr(flg, "capture_github_login_completed", lambda _username: None)
monkeypatch.setattr(flg, "clear_github_login_deferral", lambda: None)
proceed = flg.require_github_login_on_first_launch(_console())
assert proceed is True
assert calls["n"] == 2
def test_clear_github_login_deferral_noop_when_not_deferred(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(flg, "read_github_login_deferred", lambda: False)
writes: list[bool] = []
monkeypatch.setattr(flg, "write_github_login_deferred", writes.append)
flg.clear_github_login_deferral()
assert writes == []
def test_clear_github_login_deferral_clears_when_deferred(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(flg, "read_github_login_deferred", lambda: True)
writes: list[bool] = []
monkeypatch.setattr(flg, "write_github_login_deferred", writes.append)
flg.clear_github_login_deferral()
assert writes == [False]
def test_sleep_until_or_cancel_raises_on_escape(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(flg.os, "name", "posix")
monkeypatch.setattr(flg.sys.stdin, "isatty", lambda: True)
monkeypatch.setattr(flg.sys.stdin, "fileno", lambda: 0)
monkeypatch.setattr(
"surfaces.interactive_shell.ui.components.key_reader.read_key_unix",
lambda **_kwargs: "cancel",
)
monkeypatch.setattr("termios.tcgetattr", lambda _fd: [0] * 7)
monkeypatch.setattr("tty.setraw", lambda _fd: None)
restored: list[bool] = []
monkeypatch.setattr(
"termios.tcsetattr",
lambda _fd, _when, _attrs: restored.append(True),
)
def _ready(
_fd: int, *_args: object, **_kwargs: object
) -> tuple[list[int], list[int], list[int]]:
return ([0], [], [])
monkeypatch.setattr("select.select", _ready)
with pytest.raises(KeyboardInterrupt):
flg._sleep_until_or_cancel(1.0)
assert restored == [True]
@@ -0,0 +1,164 @@
"""Tests for prompt input event conversion."""
from __future__ import annotations
import asyncio
import io
import threading
from collections.abc import Callable
from typing import Any
import pytest
from rich.console import Console
from surfaces.interactive_shell.runtime.core.state import ReplState
from surfaces.interactive_shell.runtime.input import (
InputCancelled,
InputClosed,
InputSubmitted,
PromptInputReader,
)
from surfaces.interactive_shell.runtime.input import prompt_input_reader as reader_module
from surfaces.interactive_shell.session import Session
class FakePrompt:
def __init__(self, read: Callable[[], str]) -> None:
self._read = read
async def read_prompt_text(self) -> str:
return self._read()
class SequencePrompt:
def __init__(self, values: list[str]) -> None:
self._values = values
async def read_prompt_text(self) -> str:
return self._values.pop(0)
def _reader(
prompt: Any,
state: ReplState | None = None,
session: Session | None = None,
console: Console | None = None,
) -> PromptInputReader:
return PromptInputReader(
prompt,
state or ReplState(),
session or Session(),
console or Console(file=io.StringIO(), force_terminal=False),
)
def _running_state() -> tuple[ReplState, asyncio.Task[None]]:
state = ReplState()
task = asyncio.create_task(asyncio.sleep(60))
state.start_dispatch(task=task, cancel_event=threading.Event())
return state, task
@pytest.mark.asyncio
async def test_prompt_input_reader_submits_normal_text(
monkeypatch: pytest.MonkeyPatch,
) -> None:
reset_calls = 0
def _reset() -> None:
nonlocal reset_calls
reset_calls += 1
monkeypatch.setattr(reader_module, "repl_reset_ctrl_c_gate", _reset)
event = await _reader(FakePrompt(lambda: "show incidents")).read()
assert event == InputSubmitted("show incidents")
assert reset_calls == 1
@pytest.mark.asyncio
async def test_prompt_input_reader_eof_with_dispatch_running_returns_cancelled() -> None:
state, task = _running_state()
try:
event = await _reader(FakePrompt(lambda: (_ for _ in ()).throw(EOFError)), state).read()
finally:
task.cancel()
await asyncio.gather(task, return_exceptions=True)
assert event == InputCancelled()
@pytest.mark.asyncio
async def test_prompt_input_reader_eof_without_dispatch_renders_resume_hint() -> None:
output = io.StringIO()
session = Session()
session.session_id = "session-123"
console = Console(file=output, force_terminal=False, color_system=None)
event = await _reader(
FakePrompt(lambda: (_ for _ in ()).throw(EOFError)),
session=session,
console=console,
).read()
assert event == InputClosed()
assert "Resume this session with:" in output.getvalue()
assert "/resume session-123" in output.getvalue()
assert "--resume session-123" in output.getvalue()
assert "Goodbye!" in output.getvalue()
@pytest.mark.asyncio
async def test_prompt_input_reader_keyboard_interrupt_with_dispatch_running_returns_cancelled() -> (
None
):
state, task = _running_state()
try:
event = await _reader(
FakePrompt(lambda: (_ for _ in ()).throw(KeyboardInterrupt)),
state,
).read()
finally:
task.cancel()
await asyncio.gather(task, return_exceptions=True)
assert event == InputCancelled()
@pytest.mark.asyncio
async def test_prompt_input_reader_keyboard_interrupt_without_dispatch_uses_ctrl_c_gate(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(reader_module, "repl_prompt_note_ctrl_c", lambda _console, _sid: False)
event = await _reader(FakePrompt(lambda: (_ for _ in ()).throw(KeyboardInterrupt))).read()
assert event == InputCancelled()
@pytest.mark.asyncio
async def test_prompt_input_reader_keyboard_interrupt_without_dispatch_can_close(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(reader_module, "repl_prompt_note_ctrl_c", lambda _console, _sid: True)
event = await _reader(FakePrompt(lambda: (_ for _ in ()).throw(KeyboardInterrupt))).read()
assert event == InputClosed()
@pytest.mark.asyncio
async def test_prompt_input_reader_strips_cpr_sequences() -> None:
event = await _reader(FakePrompt(lambda: "status\x1b[12;80R now")).read()
assert event == InputSubmitted("status now")
@pytest.mark.asyncio
async def test_prompt_input_reader_ignores_cpr_only_input() -> None:
event = await _reader(
SequencePrompt(["\x1b[12;80R", "show status"]),
).read()
assert event == InputSubmitted("show status")
@@ -0,0 +1,70 @@
"""Tests for ReplSubprocessPresenter Rich markup escaping."""
from __future__ import annotations
from io import StringIO
from rich.console import Console
from surfaces.interactive_shell.runtime.subprocess_runner.repl_presenter import (
ReplSubprocessPresenter,
_escape_markup_message,
)
from surfaces.interactive_shell.session import Session
def _presenter() -> tuple[ReplSubprocessPresenter, StringIO]:
buffer = StringIO()
console = Console(file=buffer, force_terminal=True, width=80, color_system=None)
session = Session()
return ReplSubprocessPresenter(session, console), buffer
def test_escape_markup_message_preserves_intentional_tags() -> None:
escaped = _escape_markup_message("[error]failed[/]")
buffer = StringIO()
Console(file=buffer, force_terminal=True, width=80, color_system=None).print(escaped)
output = buffer.getvalue()
assert "failed" in output
def test_escape_markup_message_escapes_plain_markup() -> None:
escaped = _escape_markup_message("Grafana [prod] rate[5m]")
buffer = StringIO()
Console(file=buffer, force_terminal=True, width=80, color_system=None).print(escaped)
output = buffer.getvalue()
assert "[prod]" in output
assert "rate[5m]" in output
def test_escape_markup_message_escapes_dynamic_text_inside_tags() -> None:
escaped = _escape_markup_message("[bold]task [critical][/bold]")
assert "task" in escaped
assert "[critical]" in escaped
def test_print_error_escapes_dynamic_markup() -> None:
presenter, buffer = _presenter()
presenter.print_error("failed: [bold]bad[/]")
output = buffer.getvalue()
assert "[bold]bad[/]" in output
assert "bad" in output
def test_print_escapes_untrusted_suffix_via_print_error_pattern() -> None:
presenter, buffer = _presenter()
presenter.print_error("command failed to start: [bold]bad[/]")
output = buffer.getvalue()
assert "[bold]bad[/]" in output
def test_print_preserves_task_id_markup_with_escaped_brackets() -> None:
presenter, buffer = _presenter()
task_id = "task-[critical]"
presenter.print(
f"[dim]synthetic test started — task[/] [bold]{task_id}[/bold]. "
"[highlight]/tasks[/] [dim]to monitor.[/]"
)
output = buffer.getvalue()
assert task_id in output
assert "/tasks" in output
@@ -0,0 +1,256 @@
"""Tests for Session state."""
from __future__ import annotations
from pathlib import Path
import pytest
import config.constants as const_module
from config.constants.prompts import SUGGESTED_PROMPT_AFTER_FAILED_SYNTHETIC_TEST
from platform.common.task_registry import TaskRegistry
from platform.common.task_types import TaskKind
from surfaces.interactive_shell.session import (
Session,
)
from surfaces.interactive_shell.session.session import _scenario_id_from_synthetic_label
class TestSession:
def test_defaults(self) -> None:
session = Session()
assert session.history == []
assert session.last_state is None
assert session.accumulated_context == {}
assert session.terminal.trust_mode is False
assert session.task_registry.list_recent() == []
assert session.terminal.metrics.turn_count == 0
assert session.terminal.metrics.fallback_count == 0
assert session.terminal.metrics.ctrl_c_intervention_count == 0
assert session.terminal.metrics.correction_intervention_count == 0
assert session.terminal.pending_prompt_default is None
assert session.last_synthetic_observation_path is None
def test_take_pending_prompt_default_returns_and_clears(self) -> None:
session = Session()
session.terminal.pending_prompt_default = "why did it fail?"
assert session.terminal.pop_pending_prompt_default() == "why did it fail?"
assert session.terminal.pending_prompt_default is None
assert session.terminal.pop_pending_prompt_default() == ""
def test_clear_resets_pending_prompt_default(self) -> None:
session = Session()
session.terminal.pending_prompt_default = "why did it fail?"
session.clear()
assert session.terminal.pending_prompt_default is None
def test_queue_auto_command_sets_pending_and_notifies(self) -> None:
session = Session()
calls: list[bool] = []
session.terminal.prompt_refresh_fn = lambda: calls.append(True)
session.terminal.set_auto_command("/integrations setup sentry")
assert session.terminal.pending_prompt_default == "/integrations setup sentry"
assert session.terminal.pending_prompt_autosubmit is True
assert calls == [True]
def test_take_pending_autosubmit_returns_and_clears(self) -> None:
session = Session()
session.terminal.pending_prompt_autosubmit = True
assert session.terminal.pop_pending_autosubmit() is True
assert session.terminal.pending_prompt_autosubmit is False
assert session.terminal.pop_pending_autosubmit() is False
def test_clear_resets_pending_autosubmit(self) -> None:
session = Session()
session.terminal.set_auto_command("/integrations setup sentry")
session.clear()
assert session.terminal.pending_prompt_autosubmit is False
assert session.terminal.pending_prompt_default is None
def test_scenario_id_from_synthetic_label(self) -> None:
assert (
_scenario_id_from_synthetic_label(
"opensre tests synthetic --scenario 001-replication-lag"
)
== "001-replication-lag"
)
assert _scenario_id_from_synthetic_label("rds_postgres:001-replication-lag") == (
"001-replication-lag"
)
assert _scenario_id_from_synthetic_label("opensre tests synthetic --scenario ./evil") == ""
assert _scenario_id_from_synthetic_label("rds_postgres:not-a-scenario") == ""
def test_suggest_synthetic_failure_follow_up_sets_pending(self) -> None:
session = Session()
session.suggest_synthetic_failure_follow_up(
label="opensre tests synthetic --scenario 001-replication-lag",
)
assert (
session.terminal.pending_prompt_default == SUGGESTED_PROMPT_AFTER_FAILED_SYNTHETIC_TEST
)
def test_record_appends_entry(self) -> None:
session = Session()
session.record("alert", "cpu high")
session.record("slash", "/status", ok=True)
session.record("alert", "bad one", ok=False)
assert len(session.history) == 3
assert session.history[-1]["type"] == "alert"
assert session.history[-1]["ok"] is False
def test_mark_latest_updates_most_recent_matching_kind(self) -> None:
session = Session()
session.record("slash", "/investigate missing.json")
session.record("alert", "missing.json", ok=False)
session.mark_latest(ok=False, kind="slash")
assert session.history[0]["ok"] is False
assert session.history[1]["ok"] is False
def test_clear_preserves_trust_mode(self) -> None:
session = Session()
session.terminal.trust_mode = True
session.terminal.background_notification_preferences.set_channels(["email"])
session.accumulated_context["service"] = "api"
session.record("alert", "something")
session.last_state = {"foo": "bar"}
session.agent.messages.append(("user", "hey"))
session.terminal.metrics.record_intervention("ctrl_c")
session.terminal.metrics.record_intervention("correction")
assert session.terminal.history_generation == 0
session.clear()
assert session.terminal.history_generation == 1
assert session.history == []
assert session.last_state is None
assert session.accumulated_context == {}
assert session.agent.messages == []
assert session.task_registry.list_recent() == []
assert session.terminal.metrics.ctrl_c_intervention_count == 0
assert session.terminal.metrics.correction_intervention_count == 0
assert session.terminal.background_notification_preferences.channels == ("email",)
assert session.terminal.trust_mode is True # preserved intentionally
def test_clear_keeps_persisted_task_history_file(
self,
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
session = Session()
monkeypatch.setattr(const_module, "OPENSRE_HOME_DIR", tmp_path)
session.task_registry = TaskRegistry.persistent()
task = session.task_registry.create(
TaskKind.SYNTHETIC_TEST, command="opensre tests synthetic"
)
task.mark_running()
session.clear()
reloaded = TaskRegistry.persistent()
loaded = reloaded.get(task.task_id)
assert loaded is not None
assert loaded.task_id == task.task_id
def test_accumulate_from_state_extracts_known_keys(self) -> None:
session = Session()
session.accumulate_from_state(
{
"service": "orders-api",
"pipeline_name": "events_fact",
"cluster_name": "prod-us-east",
"region": "us-east-1",
"environment": "production",
"root_cause": "disk full", # not accumulated
"evidence": {"ev-1": "x"}, # not accumulated
}
)
assert session.accumulated_context == {
"service": "orders-api",
"pipeline_name": "events_fact",
"cluster_name": "prod-us-east",
"region": "us-east-1",
"environment": "production",
}
def test_accumulate_from_state_skips_empty_and_none(self) -> None:
session = Session()
session.accumulate_from_state(
{
"service": "",
"cluster_name": None,
"region": "us-east-1",
}
)
assert session.accumulated_context == {"region": "us-east-1"}
def test_accumulate_from_state_merges_across_calls(self) -> None:
"""Subsequent investigations fill in context the earlier one didn't have."""
session = Session()
session.accumulate_from_state({"service": "orders-api"})
session.accumulate_from_state({"cluster_name": "prod-us-east"})
assert session.accumulated_context == {
"service": "orders-api",
"cluster_name": "prod-us-east",
}
def test_accumulate_from_state_handles_none_and_empty_state(self) -> None:
session = Session()
session.accumulate_from_state(None)
session.accumulate_from_state({})
assert session.accumulated_context == {}
def test_record_terminal_turn_updates_aggregates(self) -> None:
session = Session()
first = session.terminal.metrics.record_turn(
executed_count=2,
executed_success_count=1,
fallback_to_llm=True,
)
second = session.terminal.metrics.record_turn(
executed_count=1,
executed_success_count=1,
fallback_to_llm=False,
)
assert first.turn_index == 1
assert first.fallback_count == 1
assert first.action_success_percent == 50.0
assert first.fallback_rate_percent == 100.0
assert second.turn_index == 2
assert second.fallback_count == 1
assert round(second.action_success_percent, 2) == 66.67
assert second.fallback_rate_percent == 50.0
def test_record_intervention_increments_per_kind(self) -> None:
session = Session()
session.terminal.metrics.record_intervention("ctrl_c")
session.terminal.metrics.record_intervention("ctrl_c")
session.terminal.metrics.record_intervention("correction")
assert session.terminal.metrics.ctrl_c_intervention_count == 2
assert session.terminal.metrics.correction_intervention_count == 1
def test_record_intervention_kinds_are_independent(self) -> None:
"""Incrementing one kind does not touch the other."""
session = Session()
session.terminal.metrics.record_intervention("correction")
assert session.terminal.metrics.ctrl_c_intervention_count == 0
assert session.terminal.metrics.correction_intervention_count == 1
def test_fresh_session_starts_with_zero_intervention_counts(self) -> None:
"""A new Session does not inherit any prior session's counters."""
first = Session()
first.terminal.metrics.record_intervention("ctrl_c")
first.terminal.metrics.record_intervention("correction")
second = Session()
assert second.terminal.metrics.ctrl_c_intervention_count == 0
assert second.terminal.metrics.correction_intervention_count == 0
@@ -0,0 +1,65 @@
"""Tests for the inline turn spinner in runtime.core.state."""
from __future__ import annotations
import re
import time
from surfaces.interactive_shell.runtime.core.state import SpinnerState
_GLYPHS = SpinnerState._SPINNER_FRAMES
def _glyph(spinner: SpinnerState) -> str:
rendered = spinner.inline_spinner_ansi()
match = re.search("|".join(map(re.escape, _GLYPHS)), rendered)
assert match is not None, f"no spinner glyph in {rendered!r}"
return match.group(0)
def test_spinner_frame_is_a_function_of_elapsed_time_not_call_count() -> None:
"""Regression: the glyph must animate no matter how often it is rendered.
prompt_toolkit evaluates the prompt message callable several times per
render pass. A per-call frame counter advanced exactly one full cycle per
visible render (10 evals x 10 frames), so the on-screen glyph never
changed. Deriving the frame from elapsed time makes repeated evaluations
idempotent and guarantees the animation advances between renders.
"""
spinner = SpinnerState()
spinner.start()
# Repeated evaluations inside one render pass: same frame every time.
first = _glyph(spinner)
assert all(_glyph(spinner) == first for _ in range(10))
# A frame interval later the glyph must have advanced.
spinner.started_at -= SpinnerState._FRAME_INTERVAL_S * 1.01
assert _glyph(spinner) != first
# Over a full cycle of elapsed time, every frame is visited in order.
spinner.start()
seen = []
for step in range(len(_GLYPHS)):
spinner.started_at = time.monotonic() - step * SpinnerState._FRAME_INTERVAL_S * 1.001
seen.append(_glyph(spinner))
assert seen == list(_GLYPHS)
def test_spinner_renders_elapsed_seconds_and_cancel_hint() -> None:
spinner = SpinnerState()
spinner.start()
spinner.started_at = time.monotonic() - 8.2
rendered = spinner.inline_spinner_ansi()
assert "(8s)" in rendered
assert "esc to cancel" in rendered
def test_spinner_empty_when_not_streaming() -> None:
spinner = SpinnerState()
assert spinner.inline_spinner_ansi() == ""
spinner.start()
spinner.stop()
assert spinner.inline_spinner_ansi() == ""
@@ -0,0 +1,52 @@
from __future__ import annotations
import asyncio
from typing import Any, cast
import pytest
from surfaces.interactive_shell.runtime.background.workers import BackgroundTaskManager
from surfaces.interactive_shell.runtime.core.state import ReplState, SpinnerState
from surfaces.interactive_shell.session import Session
@pytest.mark.asyncio
async def test_spinner_ticker_invalidates_once_after_streaming_stops(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""The ticker repaints while streaming and once more on the streaming->idle edge.
Without the trailing invalidation a stopped investigation would leave the
last phase label rendered on the prompt until unrelated I/O redraws it.
"""
state = ReplState()
spinner = SpinnerState()
calls: list[int] = []
manager = BackgroundTaskManager(
cast(Session, cast(Any, object())),
state,
spinner,
None,
lambda: calls.append(1),
)
ticks = 0
real_sleep = asyncio.sleep
async def fake_sleep(_seconds: float) -> None:
nonlocal ticks
ticks += 1
if ticks == 1:
spinner.set_phase("Investigation") # streaming on
elif ticks == 3:
spinner.stop() # streaming off (investigation done)
elif ticks >= 5:
state.exit_requested = True
await real_sleep(0)
monkeypatch.setattr(asyncio, "sleep", fake_sleep)
await manager._spinner_ticker()
# ticks 1-2 stream (2 invalidations) + one trailing edge at tick 3, then
# silence while idle at tick 4.
assert len(calls) == 3
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,565 @@
"""Tests for REPL task registry and /tasks · /cancel."""
from __future__ import annotations
import io
import json
import os
import tempfile
from collections.abc import Callable, Iterator
from pathlib import Path
from unittest.mock import MagicMock
import pytest
from rich.console import Console
from config.constants.prompts import SUGGESTED_PROMPT_AFTER_FAILED_SYNTHETIC_TEST
from platform.common.task_registry import TaskRegistry
from platform.common.task_types import TaskKind, TaskStatus
from surfaces.interactive_shell.command_registry import dispatch_slash
from surfaces.interactive_shell.session import (
Session,
)
from tools.interactive_shell.synthetic.runner import watch_synthetic_subprocess
def _capture() -> tuple[Console, io.StringIO]:
buf = io.StringIO()
return Console(file=buf, force_terminal=False, highlight=False), buf
@pytest.fixture
def stderr_buf() -> Iterator[tempfile.SpooledTemporaryFile]: # type: ignore[type-arg]
"""Stderr buffer for synthetic-watcher tests.
The watcher's ``finally`` block closes the buffer; the ``with`` here
is the belt-and-braces close that runs when a test exits before the
watcher does (e.g. deferred-thread tests where ``pending[0]()`` is
never invoked).
"""
with tempfile.SpooledTemporaryFile() as buf:
yield buf
class TestTaskRecord:
def test_lifecycle_completed(self) -> None:
reg = TaskRegistry()
t = reg.create(TaskKind.INVESTIGATION)
assert t.status == TaskStatus.PENDING
t.mark_running()
assert t.status == TaskStatus.RUNNING
t.mark_completed(result="done")
assert t.status == TaskStatus.COMPLETED
assert t.result == "done"
assert t.ended_at is not None
t.mark_failed("x")
assert t.status == TaskStatus.COMPLETED
def test_mark_cancelled_idempotent_after_terminal(self) -> None:
reg = TaskRegistry()
t = reg.create(TaskKind.INVESTIGATION)
t.mark_running()
t.mark_cancelled()
assert t.status == TaskStatus.CANCELLED
t.mark_completed(result="nope")
assert t.status == TaskStatus.CANCELLED
def test_request_cancel_sets_event_even_when_pending(self) -> None:
reg = TaskRegistry()
t = reg.create(TaskKind.INVESTIGATION)
assert t.request_cancel() is False
assert t.cancel_requested.is_set()
assert t.status == TaskStatus.PENDING
def test_request_cancel_sets_event_and_terminates_process(self) -> None:
reg = TaskRegistry()
t = reg.create(TaskKind.SYNTHETIC_TEST)
t.mark_running()
proc = MagicMock()
proc.poll.return_value = None
t.attach_process(proc)
assert t.request_cancel() is True
proc.terminate.assert_called_once()
assert t.cancel_requested.is_set()
class TestTaskRegistry:
def test_get_single_prefix_match(self) -> None:
reg = TaskRegistry()
t = reg.create(TaskKind.INVESTIGATION)
assert reg.get(t.task_id[:4]) == t
assert reg.get("") is None
def test_candidates_ambiguous_prefix(self, monkeypatch: pytest.MonkeyPatch) -> None:
_ids = iter(["11111111", "11112222"])
def _fake_hex(_nbytes: int) -> str:
return next(_ids)
monkeypatch.setattr("platform.common.task_registry.secrets.token_hex", _fake_hex)
session = Session()
session.task_registry.create(TaskKind.INVESTIGATION)
session.task_registry.create(TaskKind.INVESTIGATION)
console, buf = _capture()
dispatch_slash("/cancel 1111", session, console)
assert "ambiguous" in buf.getvalue().lower()
def test_ring_buffer_drops_oldest(self) -> None:
reg = TaskRegistry(max_tasks=3)
first = reg.create(TaskKind.INVESTIGATION)
reg.create(TaskKind.INVESTIGATION)
reg.create(TaskKind.INVESTIGATION)
reg.create(TaskKind.INVESTIGATION)
recent_ids = [t.task_id for t in reg.list_recent(10)]
assert first.task_id not in recent_ids
assert len(recent_ids) == 3
def test_persistent_registry_reloads_running_pid(
self,
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
import config.constants as const_module
monkeypatch.setattr(const_module, "OPENSRE_HOME_DIR", tmp_path)
reg = TaskRegistry.persistent()
task = reg.create(TaskKind.SYNTHETIC_TEST, command="opensre tests synthetic")
task.mark_running()
task.attach_pid(os.getpid())
reloaded = TaskRegistry.persistent()
[loaded] = reloaded.list_recent()
assert loaded.task_id == task.task_id
assert loaded.status == TaskStatus.RUNNING
assert loaded.pid == os.getpid()
assert loaded.command == "opensre tests synthetic"
def test_persistent_registry_marks_missing_pid_finished(
self,
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
import config.constants as const_module
monkeypatch.setattr(const_module, "OPENSRE_HOME_DIR", tmp_path)
store_path = tmp_path / "interactive_tasks.json"
store_path.parent.mkdir(parents=True, exist_ok=True)
store_path.write_text(
json.dumps(
[
{
"task_id": "abc12345",
"kind": "synthetic_test",
"status": "running",
"started_at": 1.0,
"ended_at": None,
"result": None,
"error": None,
"pid": 999_999,
"command": "opensre tests synthetic",
}
]
),
encoding="utf-8",
)
monkeypatch.setattr(
"platform.common.task_types.os.kill",
lambda _pid, _sig: (_ for _ in ()).throw(ProcessLookupError()),
)
reloaded = TaskRegistry.persistent()
[loaded] = reloaded.list_recent()
assert loaded.status == TaskStatus.COMPLETED
assert loaded.result == "process exited while shell was closed"
def test_cancel_rehydrated_task_does_not_signal_pid(
self,
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
import config.constants as const_module
monkeypatch.setattr(const_module, "OPENSRE_HOME_DIR", tmp_path)
calls: list[tuple[int, int]] = []
def _fake_kill(pid: int, sig: int) -> None:
calls.append((pid, sig))
monkeypatch.setattr("platform.common.task_types.os.kill", _fake_kill)
reg = TaskRegistry.persistent()
task = reg.create(TaskKind.SYNTHETIC_TEST, command="opensre tests synthetic")
task.mark_running()
task.attach_pid(12345)
reloaded = TaskRegistry.persistent()
loaded = reloaded.get(task.task_id)
assert loaded is not None
assert loaded.request_cancel() is True
assert loaded.status == TaskStatus.CANCELLED
assert (12345, 15) not in calls
def test_session_new_does_not_truncate_persistent_task_store(
self,
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
import config.constants as const_module
monkeypatch.setattr(const_module, "OPENSRE_HOME_DIR", tmp_path)
session = Session()
session.task_registry = TaskRegistry.persistent()
task = session.task_registry.create(TaskKind.SYNTHETIC_TEST, command="opensre tests")
task.mark_running()
task.mark_completed(result="ok")
session.clear()
# /new must keep the on-disk store intact: a fresh persistent registry
# still finds the task, and the session's swapped-in registry continues
# to surface persisted history via its disk-backed merge so /tasks does
# not "forget" the user's prior runs after /new.
reloaded = TaskRegistry.persistent()
[loaded] = reloaded.list_recent()
assert loaded.task_id == task.task_id
assert loaded.command == "opensre tests"
[visible_after_new] = session.task_registry.list_recent()
assert visible_after_new.task_id == task.task_id
class TestSlashTaskCommands:
def test_tasks_empty_message(self) -> None:
session = Session()
console, buf = _capture()
dispatch_slash("/tasks", session, console)
assert "no tasks" in buf.getvalue().lower()
def test_tasks_shows_recent_rows(self) -> None:
session = Session()
t = session.task_registry.create(TaskKind.INVESTIGATION)
t.mark_running()
t.mark_completed(result="rc")
console, buf = _capture()
dispatch_slash("/tasks", session, console)
out = buf.getvalue()
assert t.task_id in out
assert "investigation" in out
assert "completed" in out
def test_cancel_usage_without_id(self) -> None:
session = Session()
console, buf = _capture()
dispatch_slash("/cancel", session, console)
assert "usage" in buf.getvalue().lower()
def test_cancel_unknown_id(self) -> None:
session = Session()
console, buf = _capture()
dispatch_slash("/cancel deadbeef", session, console)
assert "no task" in buf.getvalue().lower()
def test_cancel_completed_task_message(self) -> None:
session = Session()
t = session.task_registry.create(TaskKind.INVESTIGATION)
t.mark_running()
t.mark_completed(result="x")
console, buf = _capture()
dispatch_slash(f"/cancel {t.task_id}", session, console)
assert "already finished" in buf.getvalue().lower()
def test_cancel_running_investigation_signals(self) -> None:
session = Session()
t = session.task_registry.create(TaskKind.INVESTIGATION)
t.mark_running()
console, buf = _capture()
dispatch_slash(f"/cancel {t.task_id}", session, console)
out = buf.getvalue()
assert "cancellation" in out.lower()
assert "Ctrl+C" in out
def test_cancel_running_synthetic_signals_and_terminates_process(self) -> None:
session = Session()
t = session.task_registry.create(TaskKind.SYNTHETIC_TEST)
t.mark_running()
proc = MagicMock()
proc.poll.return_value = None
t.attach_process(proc)
console, buf = _capture()
dispatch_slash(f"/cancel {t.task_id}", session, console)
assert t.cancel_requested.is_set()
proc.terminate.assert_called_once()
out = buf.getvalue()
assert "stop requested" in out.lower()
assert t.task_id in out
class _ImmediateThread:
"""Run ``target`` synchronously inside ``start()`` (for deterministic tests)."""
def __init__(
self,
group: object = None,
target: Callable[[], None] | None = None,
name: object = None,
args: tuple[object, ...] = (),
kwargs: dict[str, object] | None = None,
*,
daemon: object = None,
) -> None:
del group, args, kwargs, daemon, name # threaded API baggage
if target is None:
raise TypeError("target required")
self._target = target
def start(self) -> None:
self._target()
class _DeferredSyntheticThread:
"""Queue ``target`` via ``start()``; tests invoke queued callables explicitly."""
pending: list[Callable[[], None]] = []
def __init__(
self,
group: object = None,
target: Callable[[], None] | None = None,
name: object = None,
args: tuple[object, ...] = (),
kwargs: dict[str, object] | None = None,
*,
daemon: object = None,
) -> None:
del group, args, kwargs, daemon, name
if target is None:
raise TypeError("target required")
self._target = target
def start(self) -> None:
_DeferredSyntheticThread.pending.append(self._target)
def _synthetic_presenter(session: Session) -> MagicMock:
presenter = MagicMock()
presenter.session = session
presenter.start_task_output_streams.return_value = []
presenter.join_task_output_streams.return_value = None
presenter.report_exception.return_value = None
presenter.print_error.return_value = None
return presenter
class TestSyntheticSubprocessWatcher:
def test_watch_marks_completed_when_process_already_done(
self,
monkeypatch: pytest.MonkeyPatch,
stderr_buf: tempfile.SpooledTemporaryFile, # type: ignore[type-arg]
) -> None:
monkeypatch.setattr(
"tools.interactive_shell.synthetic.runner.threading.Thread",
_ImmediateThread,
)
proc = MagicMock()
proc.poll.return_value = 0
proc.returncode = 0
session = Session()
presenter = _synthetic_presenter(session)
task = session.task_registry.create(TaskKind.SYNTHETIC_TEST)
task.mark_running()
task.attach_process(proc)
watch_synthetic_subprocess(task, proc, presenter, "rds_postgres", stderr_buf)
assert task.status == TaskStatus.COMPLETED
hist = session.history[-1]
assert hist["type"] == "synthetic_test"
assert hist["ok"] is True
assert "task:" in hist["text"]
def test_watch_honours_exit_code_when_cancel_races_loop_exit(
self,
monkeypatch: pytest.MonkeyPatch,
stderr_buf: tempfile.SpooledTemporaryFile, # type: ignore[type-arg]
) -> None:
"""Process exits naturally (code 0) in the same poll tick that /cancel fires.
The poll loop exits because proc.poll() returns non-None *before* the
cancel_requested branch runs, so terminated_by_watcher stays False.
The task must be COMPLETED, not CANCELLED — the process succeeded.
"""
monkeypatch.setattr(
"tools.interactive_shell.synthetic.runner.threading.Thread",
_ImmediateThread,
)
session = Session()
presenter = _synthetic_presenter(session)
task = session.task_registry.create(TaskKind.SYNTHETIC_TEST)
task.mark_running()
proc = MagicMock()
# poll() returns None once (enter loop body), then cancel fires via
# sleep, which also makes poll return 0 — loop exits via while condition.
pending: list[int | None] = [None]
def _poll_side() -> int | None:
return pending[0]
proc.poll.side_effect = _poll_side
proc.returncode = 0
sleeps: list[float] = []
def _fake_sleep(_secs: float) -> None:
sleeps.append(_secs)
task.cancel_requested.set()
pending[0] = 0 # process finishes naturally in the same window
monkeypatch.setattr("tools.interactive_shell.subprocess.time.sleep", _fake_sleep)
watch_synthetic_subprocess(task, proc, presenter, "rds_postgres", stderr_buf)
# terminated_by_watcher is False → honour exit code 0 → COMPLETED
assert task.status == TaskStatus.COMPLETED
assert sleeps
def test_watch_marks_cancelled_when_watcher_kills_process(
self,
monkeypatch: pytest.MonkeyPatch,
stderr_buf: tempfile.SpooledTemporaryFile, # type: ignore[type-arg]
) -> None:
"""cancel_requested is set while proc is still running; watcher terminates it."""
monkeypatch.setattr(
"tools.interactive_shell.synthetic.runner.threading.Thread",
_ImmediateThread,
)
session = Session()
presenter = _synthetic_presenter(session)
task = session.task_registry.create(TaskKind.SYNTHETIC_TEST)
task.mark_running()
proc = MagicMock()
# poll() always returns None so the watcher's cancel branch runs and
# calls _terminate_child_process; returncode is set after that.
proc.poll.return_value = None
proc.returncode = -15
task.cancel_requested.set() # cancel already set before first loop check
# Skip the sleep so the loop iterates immediately to the cancel branch.
monkeypatch.setattr("tools.interactive_shell.subprocess.time.sleep", lambda _: None)
watch_synthetic_subprocess(task, proc, presenter, "rds_postgres", stderr_buf)
assert task.status == TaskStatus.CANCELLED
hist = session.history[-1]
assert hist["type"] == "synthetic_test"
assert hist["ok"] is False
def test_watch_honours_exit_code_when_cancel_races_natural_exit(
self,
monkeypatch: pytest.MonkeyPatch,
stderr_buf: tempfile.SpooledTemporaryFile, # type: ignore[type-arg]
) -> None:
"""Process exits naturally (code 0) while /cancel fires concurrently.
The watcher should mark the task COMPLETED, not CANCELLED, because we
never called _terminate_child_process — the process was already gone.
"""
monkeypatch.setattr(
"tools.interactive_shell.synthetic.runner.threading.Thread",
_ImmediateThread,
)
session = Session()
presenter = _synthetic_presenter(session)
task = session.task_registry.create(TaskKind.SYNTHETIC_TEST)
task.mark_running()
proc = MagicMock()
# Process already finished; poll returns non-None immediately so the
# while-loop body never executes — terminated_by_watcher stays False.
proc.poll.return_value = 0
proc.returncode = 0
# Simulate /cancel arriving just as the watcher reads poll()
task.cancel_requested.set()
watch_synthetic_subprocess(task, proc, presenter, "rds_postgres", stderr_buf)
assert task.status == TaskStatus.COMPLETED
def test_watch_captures_stderr_on_failure(
self,
monkeypatch: pytest.MonkeyPatch,
stderr_buf: tempfile.SpooledTemporaryFile, # type: ignore[type-arg]
) -> None:
"""Diagnostic stderr output is included in mark_failed message."""
monkeypatch.setattr(
"tools.interactive_shell.synthetic.runner.threading.Thread",
_ImmediateThread,
)
session = Session()
presenter = _synthetic_presenter(session)
task = session.task_registry.create(TaskKind.SYNTHETIC_TEST)
task.mark_running()
proc = MagicMock()
proc.poll.return_value = 1
proc.returncode = 1
stderr_buf.write(b"ConnectionError: database unreachable\n")
watch_synthetic_subprocess(task, proc, presenter, "rds_postgres", stderr_buf)
assert task.status == TaskStatus.FAILED
assert "exit code 1" in (task.error or "")
assert "ConnectionError" in (task.error or "")
assert (
session.terminal.pending_prompt_default == SUGGESTED_PROMPT_AFTER_FAILED_SYNTHETIC_TEST
)
def test_watch_skips_synthetic_history_after_reset(
self,
monkeypatch: pytest.MonkeyPatch,
stderr_buf: tempfile.SpooledTemporaryFile, # type: ignore[type-arg]
) -> None:
_DeferredSyntheticThread.pending.clear()
monkeypatch.setattr(
"tools.interactive_shell.synthetic.runner.threading.Thread",
_DeferredSyntheticThread,
)
proc = MagicMock()
proc.poll.return_value = 0
proc.returncode = 0
session = Session()
presenter = _synthetic_presenter(session)
task = session.task_registry.create(TaskKind.SYNTHETIC_TEST)
task.mark_running()
task.attach_process(proc)
watch_synthetic_subprocess(task, proc, presenter, "rds_postgres", stderr_buf)
assert len(_DeferredSyntheticThread.pending) == 1
session.clear()
_DeferredSyntheticThread.pending[0]()
assert session.history == []
_DeferredSyntheticThread.pending.clear()
def test_deferred_watcher_writes_history_when_no_reset(
self,
monkeypatch: pytest.MonkeyPatch,
stderr_buf: tempfile.SpooledTemporaryFile, # type: ignore[type-arg]
) -> None:
_DeferredSyntheticThread.pending.clear()
monkeypatch.setattr(
"tools.interactive_shell.synthetic.runner.threading.Thread",
_DeferredSyntheticThread,
)
proc = MagicMock()
proc.poll.return_value = 0
proc.returncode = 0
session = Session()
presenter = _synthetic_presenter(session)
task = session.task_registry.create(TaskKind.SYNTHETIC_TEST)
task.mark_running()
task.attach_process(proc)
watch_synthetic_subprocess(task, proc, presenter, "rds_postgres", stderr_buf)
_DeferredSyntheticThread.pending[0]()
assert session.history[-1]["type"] == "synthetic_test"
_DeferredSyntheticThread.pending.clear()
@@ -0,0 +1,145 @@
"""Tests for interactive-shell session token accounting."""
from __future__ import annotations
import io
import time
from collections.abc import Iterator
from typing import Any
from rich.console import Console
from core.agent_harness.accounting.token_accounting import (
build_llm_run_info,
estimate_tokens,
format_token_total,
record_llm_turn,
)
from surfaces.interactive_shell.runtime.answer_turn import answer_shell_question
from surfaces.interactive_shell.session import Session
from surfaces.interactive_shell.ui.streaming import _CHARS_PER_TOKEN
def test_estimate_tokens_uses_chars_per_token_ratio() -> None:
text = "x" * (8 * _CHARS_PER_TOKEN)
assert estimate_tokens(text) == 8
def test_record_token_usage_accumulates_on_session() -> None:
session = Session()
record_llm_turn(session, prompt="a" * 40, response="b" * 20)
assert session.tokens.totals == {
"input": 10,
"output": 5,
"input_estimated": 10,
"output_estimated": 5,
}
assert session.tokens.call_count == 1
assert session.tokens.has_estimates is True
record_llm_turn(session, prompt="c" * 8, response="d" * 4)
assert session.tokens.totals == {
"input": 12,
"output": 6,
"input_estimated": 12,
"output_estimated": 6,
}
assert session.tokens.call_count == 2
def test_record_llm_turn_uses_provider_counts_when_present() -> None:
session = Session()
inp, out, estimated = record_llm_turn(
session,
prompt="ignored when counts supplied",
response="also ignored",
input_tokens=1200,
output_tokens=80,
)
assert (inp, out, estimated) == (1200, 80, False)
assert session.tokens.totals == {
"input": 1200,
"output": 80,
"input_measured": 1200,
"output_measured": 80,
}
assert session.tokens.has_estimates is False
def test_format_token_total_shows_mixed_breakdown() -> None:
session = Session()
record_llm_turn(
session,
prompt="x",
response="y",
input_tokens=300,
output_tokens=40,
)
record_llm_turn(session, prompt="a" * 400, response="b" * 80)
assert format_token_total(session, direction="input") == (
"input tokens",
"400 (300 provider + 100 est.)",
)
assert format_token_total(session, direction="output") == (
"output tokens",
"60 (40 provider + 20 est.)",
)
def test_build_llm_run_info_records_tokens_and_metadata() -> None:
session = Session()
class _Client:
_model = "claude-test"
_provider_label = "Anthropic"
run = build_llm_run_info(
session=session,
prompt="a" * 40,
response_text="b" * 20,
client=_Client(),
started=time.monotonic() - 0.05,
)
assert run.model == "claude-test"
assert run.provider == "anthropic"
assert run.input_tokens == 10
assert run.output_tokens == 5
assert run.response_text == "b" * 20
assert run.latency_ms is not None and run.latency_ms >= 0
def test_coerce_usage_tokens_accepts_float_counts() -> None:
from core.llm.shared.usage import coerce_usage_tokens
assert coerce_usage_tokens(
{"input_tokens": 512.0, "output_tokens": 64.0},
input_key="input_tokens",
output_key="output_tokens",
) == (512, 64)
def test_record_token_usage_skips_zero_counts() -> None:
session = Session()
session.tokens.record()
assert session.tokens.totals == {}
assert session.tokens.call_count == 0
class _FakeLLMClient:
def __init__(self, content: str) -> None:
self._content = content
self.last_prompt: str | None = None
def invoke_stream(self, prompt: str) -> Iterator[str]:
self.last_prompt = prompt
yield self._content
def test_answer_shell_question_records_session_token_usage(monkeypatch: Any) -> None:
client = _FakeLLMClient("assistant reply")
monkeypatch.setattr("core.llm.factory.get_llm", lambda _role: client)
session = Session()
console = Console(file=io.StringIO(), force_terminal=False)
answer_shell_question("hello", session, console)
assert session.tokens.totals["input"] > 0
assert session.tokens.totals["output"] == estimate_tokens("assistant reply")
assert session.tokens.has_estimates is True
@@ -0,0 +1,152 @@
"""Tests for ShellTurnAccounting's pending turn LLM/error consumption."""
from __future__ import annotations
from typing import Any
from core.agent_harness.accounting.token_accounting import LlmRunInfo
from core.agent_harness.turns.turn_results import ShellTurnResult, ToolCallingTurnResult
from surfaces.interactive_shell.runtime.core.turn_accounting import ShellTurnAccounting
from surfaces.interactive_shell.session import Session
class _FakeRecorder:
def __init__(self) -> None:
self.errors: list[tuple[str, str]] = []
self.responses: list[tuple[str, Any]] = []
self.flushed = 0
def set_error(self, kind: str, message: str) -> None:
self.errors.append((kind, message))
def set_response(self, text: str, run: Any | None = None) -> None:
self.responses.append((text, run))
def flush(self) -> None:
self.flushed += 1
def _result(*, llm_run: Any | None = None, text: str = "done") -> ShellTurnResult:
return ShellTurnResult(
final_intent="slash",
action_result=ToolCallingTurnResult(
planned_count=0,
executed_count=0,
executed_success_count=0,
has_unhandled_clause=False,
handled=True,
),
assistant_response_text=text,
llm_run=llm_run,
)
def test_finalize_applies_pending_turn_llm_when_no_conversational_run() -> None:
session = Session()
pending = LlmRunInfo(model="claude-sonnet-4-5", provider="anthropic", input_tokens=100)
session.terminal.set_pending_turn_llm(pending)
recorder = _FakeRecorder()
accounting = ShellTurnAccounting(session=session, text="/investigate", recorder=recorder) # type: ignore[arg-type]
accounting.finalize(_result())
assert recorder.responses == [("done", pending)]
assert recorder.flushed == 1
assert session.terminal.pop_pending_turn_llm() is None
def test_finalize_prefers_conversational_run_over_pending() -> None:
session = Session()
session.terminal.set_pending_turn_llm(LlmRunInfo(model="stale"))
conversational = LlmRunInfo(model="fresh")
recorder = _FakeRecorder()
accounting = ShellTurnAccounting(session=session, text="hi", recorder=recorder) # type: ignore[arg-type]
accounting.finalize(_result(llm_run=conversational))
assert recorder.responses[0][1] is conversational
# The stale pending run was still consumed so it cannot leak later.
assert session.terminal.pop_pending_turn_llm() is None
def test_finalize_sets_structured_error_from_pending_turn_error() -> None:
session = Session()
session.terminal.set_pending_turn_error("config", "ANTHROPIC_API_KEY not set")
recorder = _FakeRecorder()
accounting = ShellTurnAccounting(session=session, text="/investigate", recorder=recorder) # type: ignore[arg-type]
accounting.finalize(_result(text="investigation_failed"))
assert recorder.errors == [("config", "ANTHROPIC_API_KEY not set")]
assert session.terminal.pop_pending_turn_error() is None
def test_finalize_consumes_pending_state_even_without_recorder() -> None:
session = Session()
session.terminal.set_pending_turn_llm(LlmRunInfo(model="m"))
session.terminal.set_pending_turn_error("llm", "boom")
accounting = ShellTurnAccounting(session=session, text="hi", recorder=None)
accounting.finalize(_result())
assert session.terminal.pop_pending_turn_llm() is None
assert session.terminal.pop_pending_turn_error() is None
def test_stage_investigation_turn_telemetry_populates_pending_state() -> None:
from surfaces.interactive_shell.command_registry.investigation import (
_stage_investigation_turn_telemetry,
)
from surfaces.interactive_shell.ui.investigation_outcome import InvestigationOutcome
session = Session()
outcome = InvestigationOutcome(
status="failed",
target="generic",
investigation_id="inv-1",
error_message="Anthropic authentication failed. Check ANTHROPIC_API_KEY.",
failure_category="llm",
llm_model="claude-sonnet-4-5",
llm_provider="anthropic",
llm_input_tokens=500,
llm_output_tokens=120,
duration_ms=4200,
)
_stage_investigation_turn_telemetry(session, outcome)
run = session.terminal.pop_pending_turn_llm()
assert run is not None
assert run.model == "claude-sonnet-4-5"
assert run.provider == "anthropic"
assert run.input_tokens == 500
assert run.output_tokens == 120
assert run.latency_ms == 4200
assert session.terminal.pop_pending_turn_error() == (
"llm",
"Anthropic authentication failed. Check ANTHROPIC_API_KEY.",
)
# Provider-measured tokens also count toward the session totals.
assert session.tokens.totals["input"] == 500
assert session.tokens.totals["output"] == 120
def test_stage_investigation_turn_telemetry_completed_run_has_no_error() -> None:
from surfaces.interactive_shell.command_registry.investigation import (
_stage_investigation_turn_telemetry,
)
from surfaces.interactive_shell.ui.investigation_outcome import InvestigationOutcome
session = Session()
_stage_investigation_turn_telemetry(
session,
InvestigationOutcome(
status="completed",
target="generic",
investigation_id="inv-2",
llm_model="claude-sonnet-4-5",
),
)
assert session.terminal.pop_pending_turn_llm() is not None
assert session.terminal.pop_pending_turn_error() is None