207 lines
7.1 KiB
Python
207 lines
7.1 KiB
Python
"""Tests for bincio.render.merge — sidecar edit overlay logic."""
|
|
|
|
import json
|
|
import textwrap
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from bincio.render.merge import apply_sidecar, merge_all, parse_sidecar
|
|
|
|
|
|
# ── parse_sidecar ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
def test_parse_sidecar_full(tmp_path):
|
|
md = tmp_path / "act.md"
|
|
md.write_text(textwrap.dedent("""\
|
|
---
|
|
title: "Ride to the coast"
|
|
sport: cycling
|
|
highlight: true
|
|
private: false
|
|
hide_stats: [cadence, power]
|
|
gear: "Trek Domane"
|
|
---
|
|
|
|
Great day out with Marco.
|
|
"""))
|
|
fm, body = parse_sidecar(md)
|
|
assert fm["title"] == "Ride to the coast"
|
|
assert fm["sport"] == "cycling"
|
|
assert fm["highlight"] is True
|
|
assert fm["private"] is False
|
|
assert fm["hide_stats"] == ["cadence", "power"]
|
|
assert fm["gear"] == "Trek Domane"
|
|
assert body == "Great day out with Marco."
|
|
|
|
|
|
def test_parse_sidecar_no_frontmatter(tmp_path):
|
|
md = tmp_path / "act.md"
|
|
md.write_text("Just a description, no frontmatter.\n")
|
|
fm, body = parse_sidecar(md)
|
|
assert fm == {}
|
|
assert body == "Just a description, no frontmatter."
|
|
|
|
|
|
def test_parse_sidecar_frontmatter_only(tmp_path):
|
|
md = tmp_path / "act.md"
|
|
md.write_text("---\ntitle: Solo spin\n---\n")
|
|
fm, body = parse_sidecar(md)
|
|
assert fm["title"] == "Solo spin"
|
|
assert body == ""
|
|
|
|
|
|
# ── apply_sidecar ─────────────────────────────────────────────────────────────
|
|
|
|
BASE_DETAIL = {
|
|
"id": "2024-01-01T080000Z-morning-ride",
|
|
"title": "Morning Ride",
|
|
"sport": "cycling",
|
|
"started_at": "2024-01-01T08:00:00Z",
|
|
"description": "Original description from Strava.",
|
|
"privacy": "public",
|
|
"gear": None,
|
|
"custom": {},
|
|
}
|
|
|
|
|
|
def test_apply_sidecar_title_and_sport():
|
|
fm = {"title": "Renamed", "sport": "running"}
|
|
result = apply_sidecar(BASE_DETAIL, fm, "")
|
|
assert result["title"] == "Renamed"
|
|
assert result["sport"] == "running"
|
|
# Original must be unchanged
|
|
assert BASE_DETAIL["title"] == "Morning Ride"
|
|
|
|
|
|
def test_apply_sidecar_body_becomes_description():
|
|
result = apply_sidecar(BASE_DETAIL, {}, "My **epic** ride.")
|
|
assert result["description"] == "My **epic** ride."
|
|
|
|
|
|
def test_apply_sidecar_body_takes_precedence_over_fm_description():
|
|
fm = {"description": "FM description"}
|
|
result = apply_sidecar(BASE_DETAIL, fm, "Body description")
|
|
assert result["description"] == "Body description"
|
|
|
|
|
|
def test_apply_sidecar_private_flag():
|
|
result = apply_sidecar(BASE_DETAIL, {"private": True}, "")
|
|
assert result["privacy"] == "private"
|
|
|
|
|
|
def test_apply_sidecar_highlight():
|
|
result = apply_sidecar(BASE_DETAIL, {"highlight": True}, "")
|
|
assert result["custom"]["highlight"] is True
|
|
|
|
|
|
def test_apply_sidecar_hide_stats():
|
|
result = apply_sidecar(BASE_DETAIL, {"hide_stats": ["cadence", "power"]}, "")
|
|
assert result["custom"]["hide_stats"] == ["cadence", "power"]
|
|
|
|
|
|
def test_apply_sidecar_does_not_mutate_input():
|
|
fm = {"title": "New title", "highlight": True}
|
|
original_custom = BASE_DETAIL["custom"]
|
|
apply_sidecar(BASE_DETAIL, fm, "")
|
|
assert BASE_DETAIL["title"] == "Morning Ride"
|
|
assert BASE_DETAIL["custom"] is original_custom
|
|
assert "highlight" not in original_custom
|
|
|
|
|
|
# ── merge_all ─────────────────────────────────────────────────────────────────
|
|
|
|
|
|
@pytest.fixture()
|
|
def data_dir(tmp_path):
|
|
acts = tmp_path / "activities"
|
|
acts.mkdir()
|
|
# Two activities
|
|
for act_id, title, sport, started_at in [
|
|
("2024-01-01T080000Z-morning-ride", "Morning Ride", "cycling", "2024-01-01T08:00:00Z"),
|
|
("2024-01-02T090000Z-easy-run", "Easy Run", "running", "2024-01-02T09:00:00Z"),
|
|
]:
|
|
detail = {
|
|
"id": act_id, "title": title, "sport": sport,
|
|
"started_at": started_at,
|
|
"description": "", "privacy": "public", "custom": {},
|
|
}
|
|
(acts / f"{act_id}.json").write_text(json.dumps(detail))
|
|
# Index
|
|
index = {"activities": [
|
|
{"id": "2024-01-01T080000Z-morning-ride", "title": "Morning Ride",
|
|
"sport": "cycling", "started_at": "2024-01-01T08:00:00Z", "privacy": "public", "custom": {}},
|
|
{"id": "2024-01-02T090000Z-easy-run", "title": "Easy Run",
|
|
"sport": "running", "started_at": "2024-01-02T09:00:00Z", "privacy": "public", "custom": {}},
|
|
]}
|
|
(tmp_path / "index.json").write_text(json.dumps(index))
|
|
return tmp_path
|
|
|
|
|
|
def test_merge_all_no_sidecars(data_dir):
|
|
n = merge_all(data_dir)
|
|
assert n == 0
|
|
merged = data_dir / "_merged"
|
|
assert merged.exists()
|
|
# Unmodified files are symlinked
|
|
detail_link = merged / "activities" / "2024-01-01T080000Z-morning-ride.json"
|
|
assert detail_link.is_symlink()
|
|
|
|
|
|
def test_merge_all_applies_sidecar(data_dir):
|
|
edits = data_dir / "edits"
|
|
edits.mkdir()
|
|
(edits / "2024-01-01T080000Z-morning-ride.md").write_text(
|
|
"---\ntitle: Epic Ride\nhighlight: true\n---\n\nWhat a day!"
|
|
)
|
|
n = merge_all(data_dir)
|
|
assert n == 1
|
|
|
|
merged_json = data_dir / "_merged" / "activities" / "2024-01-01T080000Z-morning-ride.json"
|
|
assert not merged_json.is_symlink()
|
|
data = json.loads(merged_json.read_text())
|
|
assert data["title"] == "Epic Ride"
|
|
assert data["custom"]["highlight"] is True
|
|
assert data["description"] == "What a day!"
|
|
|
|
# Untouched activity is still a symlink
|
|
run_link = data_dir / "_merged" / "activities" / "2024-01-02T090000Z-easy-run.json"
|
|
assert run_link.is_symlink()
|
|
|
|
|
|
def test_merge_all_private_filtered_from_index(data_dir):
|
|
edits = data_dir / "edits"
|
|
edits.mkdir()
|
|
(edits / "2024-01-01T080000Z-morning-ride.md").write_text("---\nprivate: true\n---\n")
|
|
merge_all(data_dir)
|
|
|
|
index = json.loads((data_dir / "_merged" / "index.json").read_text())
|
|
ids = [a["id"] for a in index["activities"]]
|
|
assert "2024-01-01T080000Z-morning-ride" not in ids
|
|
assert "2024-01-02T090000Z-easy-run" in ids
|
|
|
|
|
|
def test_merge_all_highlight_sorts_first(data_dir):
|
|
edits = data_dir / "edits"
|
|
edits.mkdir()
|
|
# Highlight the older activity — it should appear first
|
|
(edits / "2024-01-01T080000Z-morning-ride.md").write_text("---\nhighlight: true\n---\n")
|
|
merge_all(data_dir)
|
|
|
|
index = json.loads((data_dir / "_merged" / "index.json").read_text())
|
|
ids = [a["id"] for a in index["activities"]]
|
|
assert ids[0] == "2024-01-01T080000Z-morning-ride"
|
|
|
|
|
|
def test_merge_all_idempotent(data_dir):
|
|
edits = data_dir / "edits"
|
|
edits.mkdir()
|
|
(edits / "2024-01-01T080000Z-morning-ride.md").write_text("---\ntitle: Renamed\n---\n")
|
|
merge_all(data_dir)
|
|
merge_all(data_dir) # second run should not error or double-apply
|
|
data = json.loads(
|
|
(data_dir / "_merged" / "activities" / "2024-01-01T080000Z-morning-ride.json").read_text()
|
|
)
|
|
assert data["title"] == "Renamed"
|