Files
wehub-resource-sync 2cab53bc94
Docker Publish / Build and Push Docker Images (map[description:Skill Seekers CLI - Convert documentation to AI skills dockerfile:Dockerfile name:skill-seekers]) (push) Waiting to run
Docker Publish / Build and Push Docker Images (map[description:Skill Seekers MCP Server - 25 tools for AI assistants dockerfile:Dockerfile.mcp name:skill-seekers-mcp]) (push) Waiting to run
Docker Publish / Test Docker Images (push) Blocked by required conditions
Test Vector Database Adaptors / Test chroma Adaptor (push) Waiting to run
Test Vector Database Adaptors / Test faiss Adaptor (push) Waiting to run
Test Vector Database Adaptors / Test qdrant Adaptor (push) Waiting to run
Test Vector Database Adaptors / Test weaviate Adaptor (push) Waiting to run
Test Vector Database Adaptors / Test MCP Vector DB Tools (push) Waiting to run
Tests / Code Quality (Ruff & Mypy) (push) Waiting to run
Tests / Fast Unit Tests (parallel) (macos-latest, 3.11) (push) Waiting to run
Tests / Fast Unit Tests (parallel) (macos-latest, 3.12) (push) Waiting to run
Tests / Fast Unit Tests (parallel) (ubuntu-latest, 3.10) (push) Waiting to run
Tests / Fast Unit Tests (parallel) (ubuntu-latest, 3.11) (push) Waiting to run
Tests / Fast Unit Tests (parallel) (ubuntu-latest, 3.12) (push) Waiting to run
Tests / Tests (push) Blocked by required conditions
Tests / Serial / Integration / E2E Tests (push) Blocked by required conditions
Tests / MCP Server Tests (push) Blocked by required conditions
chore: import upstream snapshot with attribution
2026-07-13 12:46:28 +08:00

255 lines
8.3 KiB
Python

"""Tests for sync monitor (monitor.py)."""
import json
import pytest
from unittest.mock import patch
from skill_seekers.sync.monitor import SyncMonitor
from skill_seekers.sync.models import ChangeReport, ChangeType, PageChange, SyncState
@pytest.fixture
def sample_config(tmp_path):
config = {"name": "test-skill", "base_url": "https://example.com/docs"}
config_path = tmp_path / "config.json"
config_path.write_text(json.dumps(config))
return config_path, tmp_path
@pytest.fixture
def monitor(sample_config):
config_path, tmp_path = sample_config
state_file = tmp_path / "test-skill_sync.json"
with (
patch.object(SyncMonitor, "_save_state"),
patch.object(SyncMonitor, "_load_state", return_value=SyncState(skill_name="test-skill")),
):
m = SyncMonitor(
config_path=str(config_path),
check_interval=60,
state_file=str(state_file),
)
return m
class TestMonitorInit:
def test_basic_init(self, sample_config):
config_path, _ = sample_config
with (
patch.object(SyncMonitor, "_save_state"),
patch.object(
SyncMonitor, "_load_state", return_value=SyncState(skill_name="test-skill")
),
):
m = SyncMonitor(config_path=str(config_path))
assert m.skill_name == "test-skill"
assert m.check_interval == 3600
assert m.auto_update is False
assert m._running is False
def test_custom_interval(self, sample_config):
config_path, _ = sample_config
with (
patch.object(SyncMonitor, "_save_state"),
patch.object(
SyncMonitor, "_load_state", return_value=SyncState(skill_name="test-skill")
),
):
m = SyncMonitor(config_path=str(config_path), check_interval=1800)
assert m.check_interval == 1800
def test_auto_update(self, sample_config):
config_path, _ = sample_config
with (
patch.object(SyncMonitor, "_save_state"),
patch.object(
SyncMonitor, "_load_state", return_value=SyncState(skill_name="test-skill")
),
):
m = SyncMonitor(config_path=str(config_path), auto_update=True)
assert m.auto_update is True
def test_on_change_callback(self, sample_config):
config_path, _ = sample_config
called = []
def cb(report):
called.append(report)
with (
patch.object(SyncMonitor, "_save_state"),
patch.object(
SyncMonitor, "_load_state", return_value=SyncState(skill_name="test-skill")
),
):
m = SyncMonitor(config_path=str(config_path), on_change=cb)
assert m.on_change is cb
class TestCheckNow:
def test_check_now_no_urls(self, monitor):
report = monitor.check_now()
assert isinstance(report, ChangeReport)
assert report.skill_name == "test-skill"
def test_check_now_updates_state(self, monitor):
monitor.check_now()
assert monitor.state.last_check is not None
assert monitor.state.total_checks == 1
assert monitor.state.status == "idle"
def test_check_now_with_changes(self, sample_config):
config_path, tmp_path = sample_config
old_hash = "abc123"
with (
patch.object(SyncMonitor, "_save_state"),
):
m = SyncMonitor(
config_path=str(config_path),
state_file=str(tmp_path / "state.json"),
)
m.state.page_hashes = {"https://example.com/docs": old_hash}
with patch.object(
m.detector,
"check_pages",
return_value=ChangeReport(
skill_name="test-skill",
total_pages=1,
modified=[
PageChange(
url="https://example.com/docs",
change_type=ChangeType.MODIFIED,
old_hash=old_hash,
new_hash="newhash",
)
],
),
):
m.check_now()
assert m.state.total_changes == 1
assert "https://example.com/docs" in m.state.page_hashes
def test_callback_triggered_on_changes(self, sample_config):
config_path, tmp_path = sample_config
called = []
def cb(report):
called.append(report)
mock_change = ChangeReport(
skill_name="test-skill",
total_pages=1,
modified=[
PageChange(
url="https://example.com/docs",
change_type=ChangeType.MODIFIED,
old_hash="a",
new_hash="b",
)
],
)
with (
patch.object(SyncMonitor, "_save_state"),
patch.object(SyncMonitor, "_notify"),
):
m = SyncMonitor(
config_path=str(config_path),
state_file=str(tmp_path / "state.json"),
on_change=cb,
)
with patch.object(m.detector, "check_pages", return_value=mock_change):
m.check_now()
assert len(called) == 1
assert called[0] is mock_change
class TestStatePersistence:
def test_load_state_new(self, sample_config):
config_path, tmp_path = sample_config
with patch.object(SyncMonitor, "_save_state"):
m = SyncMonitor(
config_path=str(config_path),
state_file=str(tmp_path / "state.json"),
)
state = m._load_state()
assert state.skill_name == "test-skill"
assert state.total_checks == 0
def test_save_and_load_state(self, sample_config):
config_path, tmp_path = sample_config
state_file = tmp_path / "state.json"
m = SyncMonitor(
config_path=str(config_path),
state_file=str(state_file),
)
m.state.total_checks = 5
m.state.page_hashes = {"https://a.com": "abc"}
m._save_state()
assert state_file.exists()
loaded_data = json.loads(state_file.read_text())
assert loaded_data["total_checks"] == 5
assert loaded_data["page_hashes"]["https://a.com"] == "abc"
class TestStats:
def test_stats(self, monitor):
monitor.state.total_checks = 10
monitor.state.total_changes = 3
monitor.state.page_hashes = {"https://a.com": "abc", "https://b.com": "def"}
monitor.state.status = "idle"
stats = monitor.stats()
assert stats["skill_name"] == "test-skill"
assert stats["total_checks"] == 10
assert stats["total_changes"] == 3
assert stats["tracked_pages"] == 2
assert stats["status"] == "idle"
assert stats["running"] is False
class TestContextManager:
def test_context_manager(self, sample_config):
config_path, _ = sample_config
with (
patch.object(SyncMonitor, "_save_state"),
patch.object(
SyncMonitor, "_load_state", return_value=SyncState(skill_name="test-skill")
),
patch.object(SyncMonitor, "check_now"),
):
m = SyncMonitor(config_path=str(config_path), check_interval=86400)
m._thread = None
with patch.object(m, "start") as mock_start, patch.object(m, "stop") as mock_stop:
with m as mon:
assert mon is m
mock_start.assert_called_once()
mock_stop.assert_called_once()
class TestErrorHandling:
def test_check_now_records_error(self, sample_config):
config_path, tmp_path = sample_config
with (
patch.object(SyncMonitor, "_save_state"),
):
m = SyncMonitor(
config_path=str(config_path),
state_file=str(tmp_path / "state.json"),
)
with patch.object(m.detector, "check_pages", side_effect=RuntimeError("Test error")): # noqa: SIM117
with pytest.raises(RuntimeError, match="Test error"):
m.check_now()
assert m.state.status == "error"
assert m.state.error == "Test error"