377 lines
12 KiB
Python
377 lines
12 KiB
Python
"""Unit-Tests für die ChatSource-Quellstrategie.
|
|
|
|
Testet Text- und JSON-Chat-Export-Parsing, Teilnehmer-Erkennung
|
|
und Artefakt-Generierung.
|
|
|
|
Requirements: 7.3, 7.5
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from monorepo.knowledge.sources.base import SourceConfig
|
|
from monorepo.knowledge.sources.chat import (
|
|
ChatSource,
|
|
_parse_date_str,
|
|
_slugify,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Fixtures
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.fixture
|
|
def chat_dir(tmp_path: Path) -> Path:
|
|
"""Creates a temporary directory for chat export files."""
|
|
chat_dir = tmp_path / "chats"
|
|
chat_dir.mkdir()
|
|
return chat_dir
|
|
|
|
|
|
@pytest.fixture
|
|
def source_config(chat_dir: Path) -> SourceConfig:
|
|
"""A SourceConfig pointing to the chat directory."""
|
|
return SourceConfig(
|
|
type="chat",
|
|
name="Test Chats",
|
|
params={"directory": str(chat_dir)},
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def json_chat_content() -> str:
|
|
"""Sample JSON chat export."""
|
|
messages = [
|
|
{
|
|
"sender": "Alice",
|
|
"timestamp": "2025-01-15 10:00",
|
|
"text": "Good morning!",
|
|
},
|
|
{
|
|
"sender": "Bob",
|
|
"timestamp": "2025-01-15 10:05",
|
|
"text": "Morning! Ready for the meeting?",
|
|
},
|
|
{
|
|
"sender": "Alice",
|
|
"timestamp": "2025-01-15 10:06",
|
|
"text": "Yes, let's go.",
|
|
},
|
|
]
|
|
return json.dumps(messages)
|
|
|
|
|
|
@pytest.fixture
|
|
def text_chat_content() -> str:
|
|
"""Sample text chat export with timestamp pattern."""
|
|
return (
|
|
"[2025-02-20 14:30] Max: Hallo zusammen\n"
|
|
"[2025-02-20 14:31] Anna: Hi Max!\n"
|
|
"[2025-02-20 14:32] Max: Wann ist das nächste Meeting?\n"
|
|
"[2025-02-20 14:35] Anna: Morgen um 10 Uhr.\n"
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def whatsapp_chat_content() -> str:
|
|
"""Sample WhatsApp-style text chat export."""
|
|
return (
|
|
"2025-03-10, 09:00 - Alice: Good morning team\n"
|
|
"2025-03-10, 09:01 - Bob: Morning!\n"
|
|
"2025-03-10, 09:05 - Charlie: Hi all, let's discuss the sprint\n"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Tests: Helper functions
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestParseDateStr:
|
|
def test_iso_datetime(self) -> None:
|
|
result = _parse_date_str("2025-01-15 10:30:00")
|
|
assert result is not None
|
|
assert result.year == 2025
|
|
assert result.month == 1
|
|
assert result.hour == 10
|
|
|
|
def test_iso_datetime_no_seconds(self) -> None:
|
|
result = _parse_date_str("2025-01-15 10:30")
|
|
assert result is not None
|
|
assert result.minute == 30
|
|
|
|
def test_german_format(self) -> None:
|
|
result = _parse_date_str("15.01.2025, 10:30")
|
|
assert result is not None
|
|
assert result.year == 2025
|
|
assert result.day == 15
|
|
|
|
def test_invalid_returns_none(self) -> None:
|
|
assert _parse_date_str("not a date") is None
|
|
|
|
def test_empty_returns_none(self) -> None:
|
|
assert _parse_date_str("") is None
|
|
|
|
|
|
class TestSlugify:
|
|
def test_simple_text(self) -> None:
|
|
assert _slugify("Team Chat") == "team-chat"
|
|
|
|
def test_special_chars(self) -> None:
|
|
result = _slugify("chat_export (1)")
|
|
assert "chat" in result
|
|
assert "export" in result
|
|
|
|
def test_empty(self) -> None:
|
|
assert _slugify("") == "chat-export"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Tests: ChatSource.extract() with JSON
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestChatSourceJson:
|
|
def test_extracts_json_chat(
|
|
self, chat_dir: Path, source_config: SourceConfig, json_chat_content: str
|
|
) -> None:
|
|
"""Test JSON chat export parsing."""
|
|
(chat_dir / "team-chat.json").write_text(json_chat_content, encoding="utf-8")
|
|
|
|
source = ChatSource()
|
|
result = source.extract(source_config, "bahn")
|
|
|
|
assert len(result.artifacts) == 1
|
|
assert len(result.errors) == 0
|
|
|
|
artifact = result.artifacts[0]
|
|
assert artifact.metadata.type == "chat"
|
|
assert "Chat:" in artifact.metadata.title
|
|
assert artifact.metadata.source["type"] == "chat"
|
|
assert artifact.metadata.source["format"] == "json"
|
|
|
|
def test_json_participants_extracted(
|
|
self, chat_dir: Path, source_config: SourceConfig, json_chat_content: str
|
|
) -> None:
|
|
"""Test that participants are extracted from JSON chat."""
|
|
(chat_dir / "team-chat.json").write_text(json_chat_content, encoding="utf-8")
|
|
|
|
source = ChatSource()
|
|
result = source.extract(source_config, "bahn")
|
|
|
|
artifact = result.artifacts[0]
|
|
assert "Alice" in artifact.metadata.people
|
|
assert "Bob" in artifact.metadata.people
|
|
|
|
def test_json_date_range(
|
|
self, chat_dir: Path, source_config: SourceConfig, json_chat_content: str
|
|
) -> None:
|
|
"""Test that date range is extracted from JSON chat."""
|
|
(chat_dir / "team-chat.json").write_text(json_chat_content, encoding="utf-8")
|
|
|
|
source = ChatSource()
|
|
result = source.extract(source_config, "bahn")
|
|
|
|
artifact = result.artifacts[0]
|
|
assert artifact.metadata.created is not None
|
|
assert artifact.metadata.created.year == 2025
|
|
assert artifact.metadata.created.month == 1
|
|
|
|
def test_json_messages_in_content(
|
|
self, chat_dir: Path, source_config: SourceConfig, json_chat_content: str
|
|
) -> None:
|
|
"""Test that messages appear in artifact content."""
|
|
(chat_dir / "team-chat.json").write_text(json_chat_content, encoding="utf-8")
|
|
|
|
source = ChatSource()
|
|
result = source.extract(source_config, "bahn")
|
|
|
|
artifact = result.artifacts[0]
|
|
assert "Good morning!" in artifact.content
|
|
assert "Alice" in artifact.content
|
|
assert "Bob" in artifact.content
|
|
|
|
def test_empty_json_array_skipped(
|
|
self, chat_dir: Path, source_config: SourceConfig
|
|
) -> None:
|
|
"""Test that empty JSON array produces no artifact."""
|
|
(chat_dir / "empty.json").write_text("[]", encoding="utf-8")
|
|
|
|
source = ChatSource()
|
|
result = source.extract(source_config, "bahn")
|
|
|
|
assert len(result.artifacts) == 0
|
|
|
|
def test_non_array_json_skipped(
|
|
self, chat_dir: Path, source_config: SourceConfig
|
|
) -> None:
|
|
"""Test that non-array JSON is skipped."""
|
|
(chat_dir / "object.json").write_text('{"key": "value"}', encoding="utf-8")
|
|
|
|
source = ChatSource()
|
|
result = source.extract(source_config, "bahn")
|
|
|
|
assert len(result.artifacts) == 0
|
|
|
|
def test_json_alternative_keys(
|
|
self, chat_dir: Path, source_config: SourceConfig
|
|
) -> None:
|
|
"""Test that alternative JSON keys (author, message, date) work."""
|
|
messages = [
|
|
{"author": "User1", "date": "2025-03-01 09:00", "message": "Hello"},
|
|
{"from": "User2", "time": "2025-03-01 09:01", "content": "Hi there"},
|
|
]
|
|
(chat_dir / "alt.json").write_text(json.dumps(messages), encoding="utf-8")
|
|
|
|
source = ChatSource()
|
|
result = source.extract(source_config, "bahn")
|
|
|
|
assert len(result.artifacts) == 1
|
|
artifact = result.artifacts[0]
|
|
assert "Hello" in artifact.content
|
|
assert "Hi there" in artifact.content
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Tests: ChatSource.extract() with Text
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestChatSourceText:
|
|
def test_extracts_text_chat_bracket_format(
|
|
self, chat_dir: Path, source_config: SourceConfig, text_chat_content: str
|
|
) -> None:
|
|
"""Test text chat export with [timestamp] format."""
|
|
(chat_dir / "team.txt").write_text(text_chat_content, encoding="utf-8")
|
|
|
|
source = ChatSource()
|
|
result = source.extract(source_config, "bahn")
|
|
|
|
assert len(result.artifacts) == 1
|
|
artifact = result.artifacts[0]
|
|
assert artifact.metadata.type == "chat"
|
|
assert artifact.metadata.source["format"] == "text"
|
|
|
|
def test_text_chat_participants(
|
|
self, chat_dir: Path, source_config: SourceConfig, text_chat_content: str
|
|
) -> None:
|
|
"""Test participant extraction from text chat."""
|
|
(chat_dir / "team.txt").write_text(text_chat_content, encoding="utf-8")
|
|
|
|
source = ChatSource()
|
|
result = source.extract(source_config, "bahn")
|
|
|
|
artifact = result.artifacts[0]
|
|
assert "Max" in artifact.metadata.people
|
|
assert "Anna" in artifact.metadata.people
|
|
|
|
def test_whatsapp_format(
|
|
self, chat_dir: Path, source_config: SourceConfig, whatsapp_chat_content: str
|
|
) -> None:
|
|
"""Test WhatsApp-style chat export parsing."""
|
|
(chat_dir / "whatsapp.txt").write_text(whatsapp_chat_content, encoding="utf-8")
|
|
|
|
source = ChatSource()
|
|
result = source.extract(source_config, "bahn")
|
|
|
|
assert len(result.artifacts) == 1
|
|
artifact = result.artifacts[0]
|
|
assert "Alice" in artifact.metadata.people
|
|
assert "Bob" in artifact.metadata.people
|
|
assert "Charlie" in artifact.metadata.people
|
|
|
|
def test_unrecognized_format_still_creates_artifact(
|
|
self, chat_dir: Path, source_config: SourceConfig
|
|
) -> None:
|
|
"""Test that unrecognized text format still creates an artifact."""
|
|
(chat_dir / "freeform.txt").write_text(
|
|
"Just some notes\nfrom a conversation\nwith no timestamps",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
source = ChatSource()
|
|
result = source.extract(source_config, "bahn")
|
|
|
|
assert len(result.artifacts) == 1
|
|
artifact = result.artifacts[0]
|
|
assert "notes" in artifact.content
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Tests: ChatSource general behavior
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestChatSourceGeneral:
|
|
def test_empty_directory(self, chat_dir: Path, source_config: SourceConfig) -> None:
|
|
"""Test that empty directory returns no artifacts."""
|
|
source = ChatSource()
|
|
result = source.extract(source_config, "bahn")
|
|
|
|
assert len(result.artifacts) == 0
|
|
assert len(result.errors) == 0
|
|
|
|
def test_nonexistent_directory(self, tmp_path: Path) -> None:
|
|
"""Test error for non-existent directory."""
|
|
config = SourceConfig(
|
|
type="chat",
|
|
name="Missing",
|
|
params={"directory": str(tmp_path / "nonexistent")},
|
|
)
|
|
source = ChatSource()
|
|
result = source.extract(config, "bahn")
|
|
|
|
assert len(result.artifacts) == 0
|
|
assert len(result.errors) == 1
|
|
assert "existiert nicht" in result.errors[0].message
|
|
|
|
def test_supports_incremental_returns_false(self) -> None:
|
|
"""Chat source does not support incremental processing."""
|
|
source = ChatSource()
|
|
assert source.supports_incremental() is False
|
|
|
|
def test_content_hash_is_set(
|
|
self, chat_dir: Path, source_config: SourceConfig, json_chat_content: str
|
|
) -> None:
|
|
"""Test that content hash is computed for chat artifacts."""
|
|
(chat_dir / "chat.json").write_text(json_chat_content, encoding="utf-8")
|
|
|
|
source = ChatSource()
|
|
result = source.extract(source_config, "bahn")
|
|
|
|
artifact = result.artifacts[0]
|
|
assert artifact.metadata.content_hash.startswith("sha256:")
|
|
|
|
def test_multiple_files_processed(
|
|
self,
|
|
chat_dir: Path,
|
|
source_config: SourceConfig,
|
|
json_chat_content: str,
|
|
text_chat_content: str,
|
|
) -> None:
|
|
"""Test that multiple chat files in directory are all processed."""
|
|
(chat_dir / "chat1.json").write_text(json_chat_content, encoding="utf-8")
|
|
(chat_dir / "chat2.txt").write_text(text_chat_content, encoding="utf-8")
|
|
|
|
source = ChatSource()
|
|
result = source.extract(source_config, "bahn")
|
|
|
|
assert len(result.artifacts) == 2
|
|
|
|
def test_category_is_inbox(
|
|
self, chat_dir: Path, source_config: SourceConfig, json_chat_content: str
|
|
) -> None:
|
|
"""Test that chat artifacts are categorized as inbox by default."""
|
|
(chat_dir / "chat.json").write_text(json_chat_content, encoding="utf-8")
|
|
|
|
source = ChatSource()
|
|
result = source.extract(source_config, "bahn")
|
|
|
|
assert result.artifacts[0].metadata.category == "inbox"
|