# ruff: noqa: INP001 """Unit tests for runtime_activity service helpers. All tests are pure-Python — no gateway connection required. """ from __future__ import annotations from datetime import datetime import pytest from app.services.openclaw.runtime_activity import ( RuntimeMessageEvent, _collect_tool_names, _correlate_session, _extract_text, _parse_timestamp, normalize_message, redact_tool_args, ) # --------------------------------------------------------------------------- # _extract_text # --------------------------------------------------------------------------- class TestExtractText: def test_string_content_short(self): text, truncated = _extract_text("Hello world") assert text == "Hello world" assert not truncated def test_string_content_truncated(self): long_text = "x" * 400 text, truncated = _extract_text(long_text) assert len(text) == 300 assert truncated def test_none_content(self): text, truncated = _extract_text(None) assert text == "" assert not truncated def test_list_text_block(self): content = [{"type": "text", "text": "The answer is 42."}] text, truncated = _extract_text(content) assert "42" in text assert not truncated def test_list_tool_use_block(self): content = [ {"type": "text", "text": "I'll run a command."}, {"type": "tool_use", "name": "bash", "input": {"command": "ls -la"}}, ] text, _ = _extract_text(content) assert "run a command" in text assert "[tool: bash]" in text def test_list_tool_result_string(self): content = [ {"type": "tool_result", "tool_use_id": "x", "content": "file1.txt\nfile2.txt"}, ] text, _ = _extract_text(content) assert "[result:" in text def test_list_multiple_text_blocks(self): content = [ {"type": "text", "text": "First."}, {"type": "text", "text": "Second."}, ] text, _ = _extract_text(content) assert "First" in text assert "Second" in text def test_empty_list(self): text, truncated = _extract_text([]) assert text == "" assert not truncated def test_non_dict_blocks_skipped(self): content = ["plain string", {"type": "text", "text": "valid"}] text, _ = _extract_text(content) assert "valid" in text # --------------------------------------------------------------------------- # redact_tool_args # --------------------------------------------------------------------------- class TestRedactToolArgs: def test_no_sensitive_keys(self): args = {"command": "ls -la", "cwd": "/home"} result = redact_tool_args(args) assert result["command"] == "ls -la" assert result["cwd"] == "/home" def test_password_redacted(self): result = redact_tool_args({"password": "secret123", "user": "admin"}) assert result["password"] == "[REDACTED]" assert result["user"] == "admin" def test_token_redacted(self): result = redact_tool_args({"token": "sk-abc123", "action": "read"}) assert result["token"] == "[REDACTED]" def test_api_key_redacted(self): result = redact_tool_args({"api_key": "abc", "model": "gpt-4"}) assert result["api_key"] == "[REDACTED]" def test_case_insensitive(self): result = redact_tool_args({"PASSWORD": "x", "Secret": "y"}) assert result["PASSWORD"] == "[REDACTED]" assert result["Secret"] == "[REDACTED]" def test_long_string_truncated(self): result = redact_tool_args({"content": "A" * 1000}) assert len(result["content"]) < 1000 assert "truncated" in result["content"] def test_empty_dict(self): assert redact_tool_args({}) == {} def test_non_dict_returns_empty(self): assert redact_tool_args("bad") == {} # type: ignore[arg-type] def test_credentials_key_redacted(self): result = redact_tool_args({"credentials": "AKIA...", "region": "us-east-1"}) assert result["credentials"] == "[REDACTED]" # --------------------------------------------------------------------------- # _collect_tool_names # --------------------------------------------------------------------------- class TestCollectToolNames: def test_no_tool_use(self): assert _collect_tool_names("plain text") == [] def test_single_tool_use(self): content = [{"type": "tool_use", "name": "bash", "input": {}}] assert _collect_tool_names(content) == ["bash"] def test_multiple_tool_uses(self): content = [ {"type": "text", "text": "hi"}, {"type": "tool_use", "name": "read_file", "input": {}}, {"type": "tool_use", "name": "bash", "input": {}}, ] names = _collect_tool_names(content) assert names == ["read_file", "bash"] def test_no_name_fallback(self): content = [{"type": "tool_use"}] assert _collect_tool_names(content) == ["unknown"] # --------------------------------------------------------------------------- # _parse_timestamp # --------------------------------------------------------------------------- class TestParseTimestamp: def test_iso_zulu(self): msg = {"timestamp": "2026-05-21T10:00:00Z"} ts = _parse_timestamp(msg) assert isinstance(ts, datetime) assert ts.year == 2026 assert ts.hour == 10 def test_iso_with_offset(self): msg = {"timestamp": "2026-05-21T12:00:00+02:00"} ts = _parse_timestamp(msg) assert ts is not None assert ts.hour == 10 # converted to UTC def test_created_at_fallback(self): msg = {"created_at": "2026-05-21T09:30:00Z"} ts = _parse_timestamp(msg) assert ts is not None def test_no_timestamp_returns_none(self): assert _parse_timestamp({}) is None def test_malformed_returns_none(self): assert _parse_timestamp({"timestamp": "not-a-date"}) is None # --------------------------------------------------------------------------- # _correlate_session # --------------------------------------------------------------------------- class TestCorrelateSession: def test_lead_session_extracts_board_id(self): board_id = "d8ec2aa9-fa86-4ab2-9f17-6518ccd600df" agent_slug, bid = _correlate_session(f"agent:lead-{board_id}:main") assert agent_slug is None assert bid == board_id def test_agent_session_extracts_slug(self): slug, bid = _correlate_session("agent:my-agent:main") assert slug == "my-agent" assert bid is None def test_unknown_format_returns_nones(self): slug, bid = _correlate_session("some-random-key") assert slug is None assert bid is None def test_empty_returns_nones(self): slug, bid = _correlate_session("") assert slug is None assert bid is None # --------------------------------------------------------------------------- # normalize_message # --------------------------------------------------------------------------- class TestNormalizeMessage: def _make_msg(self, **kwargs) -> dict: return { "role": "assistant", "content": "Hello!", "model": "claude-sonnet-4-6", **kwargs, } def test_basic_assistant_message(self): msg = self._make_msg() event = normalize_message("agent:test:main", "Test", msg, 0) assert isinstance(event, RuntimeMessageEvent) assert event.role == "assistant" assert event.model == "claude-sonnet-4-6" assert event.content_preview == "Hello!" assert not event.content_truncated assert not event.has_tool_use assert event.session_key == "agent:test:main" assert event.session_label == "Test" assert event.message_index == 0 def test_user_message(self): msg = {"role": "user", "content": "What time is it?"} event = normalize_message("session:1", None, msg, 1) assert event.role == "user" assert event.model is None assert event.session_label is None def test_tool_use_detected(self): msg = { "role": "assistant", "content": [ {"type": "text", "text": "Running command."}, {"type": "tool_use", "name": "bash", "input": {"command": "ls"}}, ], } event = normalize_message("s", None, msg, 0) assert event.has_tool_use assert "bash" in event.tool_names def test_long_content_truncated(self): long = "w" * 400 msg = self._make_msg(content=long) event = normalize_message("s", None, msg, 0) assert event.content_truncated assert len(event.content_preview) == 300 def test_event_id_is_stable(self): msg = self._make_msg() e1 = normalize_message("s", None, msg, 0) e2 = normalize_message("s", None, msg, 0) assert e1.event_id == e2.event_id def test_event_id_differs_by_index(self): msg = self._make_msg() e1 = normalize_message("s", None, msg, 0) e2 = normalize_message("s", None, msg, 1) assert e1.event_id != e2.event_id def test_board_id_from_lead_session(self): board_id = "d8ec2aa9-fa86-4ab2-9f17-6518ccd600df" msg = self._make_msg() event = normalize_message(f"agent:lead-{board_id}:main", None, msg, 0) assert event.board_id == board_id assert event.agent_id is None def test_to_dict_serialisable(self): msg = self._make_msg() event = normalize_message("s", "Label", msg, 0) d = event.to_dict() assert d["role"] == "assistant" assert d["session_label"] == "Label" assert isinstance(d["tool_names"], list) # timestamp is None here since no timestamp in msg assert d["timestamp"] is None def test_timestamp_parsed(self): msg = self._make_msg(timestamp="2026-05-21T10:00:00Z") event = normalize_message("s", None, msg, 0) assert event.timestamp is not None assert event.timestamp.hour == 10