"""No-content logging tests. A recognizable secret string is seeded into message content, image prompts, and bot personalities; the logs captured at DEBUG must never contain it, while metadata (bot name, user id, message id, counts) must. """ from __future__ import annotations import asyncio import base64 import logging from typing import Any from unittest.mock import AsyncMock, MagicMock, patch import pytest from vibe_bot.services.chat_service import ChatService from vibe_bot.services.image_service import ImageService from vibe_bot.tests._helpers import invoke SECRET = "SECRET-DO-NOT-LOG-12345" def _registry() -> MagicMock: """A mock ToolRegistry.""" reg = MagicMock() reg.to_openai_tools.return_value = [] reg.execute.return_value = "tool result" return reg def _assert_no_secret( caplog: pytest.LogCaptureFixture, *metadata: str, ) -> None: """The secret must be absent from every record; the metadata must be present.""" for record in caplog.records: assert ( SECRET not in record.getMessage() ), f"Secret leaked in log record: {record.getMessage()!r}" assert SECRET not in caplog.text for token in metadata: assert token in caplog.text, f"Expected metadata {token!r} missing from logs" def test_chat_service_logs_metadata_not_content( caplog: pytest.LogCaptureFixture, chat_db: Any, mock_ctx: MagicMock, ) -> None: """A full chat turn (RAG + persist + reply) never logs the message content.""" caplog.set_level(logging.DEBUG) svc = ChatService(chat_db, _registry()) with patch( "vibe_bot.llm_client.chat_completion_with_tools", new=AsyncMock(return_value="A perfectly fine response."), ): asyncio.run( svc.handle( mock_ctx, bot_name="alfred", message=f"please remember {SECRET} forever", system_prompt="you are a butler", response_prefix="alfred response", ) ) _assert_no_secret(caplog, "alfred", "12345") def test_add_message_logs_metadata_not_content( caplog: pytest.LogCaptureFixture, chat_db: Any, ) -> None: """add_message (with embedding) never logs the stored content.""" caplog.set_level(logging.DEBUG) assert chat_db.add_message( message_id="msg-1", user_id="12345", username="testuser", content=f"User: tell me about {SECRET}", bot_name="alfred", channel_id="channel-1", guild_id="guild-1", ) assert chat_db.add_message( message_id="msg-1_response", user_id="bot-123", username="test-bot", content=f"the bot knew {SECRET}", bot_name="alfred", channel_id="channel-1", guild_id="guild-1", role="assistant", embed=False, ) _assert_no_secret(caplog, "msg-1", "12345") def test_history_lookups_log_metadata_not_content( caplog: pytest.LogCaptureFixture, chat_db: Any, ) -> None: """get_user_history / get_bot_history never log message or response content.""" caplog.set_level(logging.DEBUG) chat_db.add_message( message_id="msg-2", user_id="12345", username="testuser", content=f"User: what is {SECRET}", bot_name="alfred", channel_id="channel-1", guild_id="guild-1", ) chat_db.add_message( message_id="msg-2_response", user_id="bot-123", username="test-bot", content=f"it is {SECRET}, clearly", bot_name="alfred", channel_id="channel-1", guild_id="guild-1", role="assistant", embed=False, ) user_history = chat_db.get_user_history("12345", limit=5) bot_history = chat_db.get_bot_history("alfred", limit=5) assert len(user_history) == 1 assert len(bot_history) == 1 _assert_no_secret(caplog, "msg-2", "alfred") def test_doodlebob_logs_metadata_not_content( caplog: pytest.LogCaptureFixture, mock_ctx: MagicMock, ) -> None: """A doodlebob generation never logs the prompt or the derived image prompt.""" caplog.set_level(logging.DEBUG) db = MagicMock() db.get_image_generation_time_estimate.return_value = 12.0 svc = ImageService(db, MagicMock()) with ( patch( "vibe_bot.llm_client.chat_completion_instruct", new=AsyncMock( side_effect=[ "portrait", f"a very detailed painting of {SECRET}", "pass", ] ), ), patch( "vibe_bot.llm_client.image_generation", new=AsyncMock(return_value=base64.b64encode(b"img").decode()), ), ): asyncio.run(svc.generate(mock_ctx, message=f"draw {SECRET} on the moon")) _assert_no_secret(caplog, "Doodlebob", "12345") def test_custom_bot_creation_logs_metadata_not_personality( caplog: pytest.LogCaptureFixture, bot: Any, mock_ctx: MagicMock, ) -> None: """!custom-bot never logs the personality text.""" caplog.set_level(logging.DEBUG) invoke( bot, "custom-bot", mock_ctx, "secretbot", personality=f"a butler who knows {SECRET}", ) _assert_no_secret(caplog, "secretbot", "12345")