183 lines
7.1 KiB
Python
183 lines
7.1 KiB
Python
"""
|
|
Phase G — Sync History Tests
|
|
|
|
Tests for GET /history: rendering _sync_log.md as a reverse-chronological table.
|
|
"""
|
|
|
|
import textwrap
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
pytestmark = pytest.mark.asyncio
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
SAMPLE_LOG = textwrap.dedent("""\
|
|
# Sync Log
|
|
|
|
| Timestamp | Direction | Files | Status |
|
|
|-----------|-----------|-------|--------|
|
|
| 2026-03-03 22:15 | push | 1 updated | error: CV.md failed |
|
|
| 2026-03-04 08:00 | pull | 0 changes | ok |
|
|
| 2026-03-05 09:10 | push | 2 updated, 1 created | ok |
|
|
| 2026-03-06 14:32 | pull | 3 updated | ok |
|
|
""")
|
|
|
|
MINIMAL_LOG = textwrap.dedent("""\
|
|
# Sync Log
|
|
|
|
| Timestamp | Direction | Files | Status |
|
|
|-----------|-----------|-------|--------|
|
|
| 2026-01-01 00:00 | pull | 1 updated | ok |
|
|
""")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# US-G1 — History page renders
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestHistoryPage:
|
|
|
|
async def test_history_returns_200(self, client):
|
|
r = await client.get("/history")
|
|
assert r.status_code == 200
|
|
|
|
async def test_history_returns_html(self, client):
|
|
r = await client.get("/history")
|
|
assert "text/html" in r.headers.get("content-type", "")
|
|
|
|
async def test_history_page_contains_table(self, client, vault_dir, sync_log):
|
|
r = await client.get("/history")
|
|
body = r.text.lower()
|
|
assert "<table" in body, "History page must render an HTML table"
|
|
|
|
async def test_history_shows_direction_labels(self, client, vault_dir):
|
|
(vault_dir / "_sync_log.md").write_text(SAMPLE_LOG)
|
|
r = await client.get("/history")
|
|
body = r.text.lower()
|
|
assert "pull" in body, "History must show pull entries"
|
|
assert "push" in body, "History must show push entries"
|
|
|
|
async def test_history_shows_timestamps(self, client, vault_dir):
|
|
(vault_dir / "_sync_log.md").write_text(SAMPLE_LOG)
|
|
r = await client.get("/history")
|
|
body = r.text
|
|
assert "2026-03-06" in body, "History must show timestamps from _sync_log.md"
|
|
|
|
async def test_history_shows_file_counts(self, client, vault_dir):
|
|
(vault_dir / "_sync_log.md").write_text(SAMPLE_LOG)
|
|
r = await client.get("/history")
|
|
body = r.text.lower()
|
|
assert "updated" in body or "created" in body, (
|
|
"History must show file change counts"
|
|
)
|
|
|
|
async def test_history_shows_status(self, client, vault_dir):
|
|
(vault_dir / "_sync_log.md").write_text(SAMPLE_LOG)
|
|
r = await client.get("/history")
|
|
body = r.text.lower()
|
|
assert "ok" in body or "error" in body, "History must show entry status"
|
|
|
|
async def test_history_empty_when_no_log(self, client, vault_dir):
|
|
"""If _sync_log.md does not exist, page should render gracefully (no 500)."""
|
|
log_path = vault_dir / "_sync_log.md"
|
|
if log_path.exists():
|
|
log_path.unlink()
|
|
|
|
r = await client.get("/history")
|
|
assert r.status_code == 200, "History page must not crash when log is missing"
|
|
|
|
async def test_history_empty_state_message(self, client, vault_dir):
|
|
"""Empty history should show a helpful message, not a blank page."""
|
|
log_path = vault_dir / "_sync_log.md"
|
|
if log_path.exists():
|
|
log_path.unlink()
|
|
|
|
r = await client.get("/history")
|
|
body = r.text.lower()
|
|
assert any(phrase in body for phrase in (
|
|
"no history", "no sync", "empty", "no entries", "nothing yet"
|
|
)), "Empty history must show a message"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# US-G2 — _sync_log.md parsing
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestSyncLogParsing:
|
|
|
|
async def test_entries_shown_in_reverse_chronological_order(
|
|
self, client, vault_dir
|
|
):
|
|
"""Most recent entry must appear before older entries in the HTML."""
|
|
(vault_dir / "_sync_log.md").write_text(SAMPLE_LOG)
|
|
r = await client.get("/history")
|
|
body = r.text
|
|
|
|
pos_newest = body.find("2026-03-06")
|
|
pos_oldest = body.find("2026-03-03")
|
|
|
|
assert pos_newest != -1, "Most recent entry must appear in history"
|
|
assert pos_oldest != -1, "Oldest entry must appear in history"
|
|
assert pos_newest < pos_oldest, (
|
|
"Most recent entry (2026-03-06) must appear before oldest (2026-03-03)"
|
|
)
|
|
|
|
async def test_error_entries_visually_distinct(self, client, vault_dir):
|
|
"""Entries with non-ok status should be highlighted differently."""
|
|
(vault_dir / "_sync_log.md").write_text(SAMPLE_LOG)
|
|
r = await client.get("/history")
|
|
body = r.text.lower()
|
|
# Error entry from 2026-03-03 should have visual distinction
|
|
# This is checked loosely: error word near some CSS class or color
|
|
assert "error" in body, "Error entries must be shown in history"
|
|
|
|
async def test_raw_markdown_not_shown_as_pipe_table(self, client, vault_dir):
|
|
"""The raw markdown pipe-table syntax must not be visible in rendered output."""
|
|
(vault_dir / "_sync_log.md").write_text(SAMPLE_LOG)
|
|
r = await client.get("/history")
|
|
body = r.text
|
|
# Pipe characters from the markdown table should NOT appear verbatim
|
|
# (they should be parsed and rendered as HTML <table>)
|
|
raw_table_lines = [l for l in body.splitlines() if l.strip().startswith("|---")]
|
|
assert len(raw_table_lines) == 0, (
|
|
"Raw markdown table separator lines must not appear in rendered HTML"
|
|
)
|
|
|
|
async def test_all_log_entries_appear(self, client, vault_dir):
|
|
"""All 4 entries in SAMPLE_LOG must appear in the rendered history."""
|
|
(vault_dir / "_sync_log.md").write_text(SAMPLE_LOG)
|
|
r = await client.get("/history")
|
|
body = r.text
|
|
|
|
assert "2026-03-06" in body
|
|
assert "2026-03-05" in body
|
|
assert "2026-03-04" in body
|
|
assert "2026-03-03" in body
|
|
|
|
async def test_single_entry_log_renders(self, client, vault_dir):
|
|
(vault_dir / "_sync_log.md").write_text(MINIMAL_LOG)
|
|
r = await client.get("/history")
|
|
assert r.status_code == 200
|
|
assert "2026-01-01" in r.text
|
|
|
|
async def test_history_api_endpoint_returns_json(self, client, vault_dir):
|
|
"""
|
|
GET /history?format=json returns structured history data.
|
|
This is optional but strongly recommended for future HTMX updates.
|
|
"""
|
|
(vault_dir / "_sync_log.md").write_text(SAMPLE_LOG)
|
|
r = await client.get("/history?format=json")
|
|
# If not implemented, 200 HTML is also acceptable
|
|
if r.status_code == 200 and "application/json" in r.headers.get("content-type", ""):
|
|
data = r.json()
|
|
assert isinstance(data, list)
|
|
assert len(data) >= 4
|
|
for entry in data:
|
|
assert "timestamp" in entry or "date" in entry
|
|
assert "direction" in entry
|