from __future__ import annotations import hashlib from pathlib import Path import pytest from django.core.management import call_command from control_plane.authoring.models import ( DocumentAuthority, DocumentType, Series, SourceDocument, Work, ) from control_plane.authoring.sources import discover_source_paths, passage_spans, register_source def source_work() -> Work: series = Series.objects.create(title="Labyrinth Hero", slug="labyrinth-hero") return Work.objects.create(series=series, title="The Fortune Below", slug="the-fortune-below") def test_passage_spans_preserve_exact_source_locations() -> None: content = "# Heading\n\nFirst line.\nSecond line.\n\nLast.\n" passages = passage_spans(content) assert [(item["start_line"], item["end_line"]) for item in passages] == [(1, 1), (3, 4), (6, 6)] assert all( content[item["start_char"] : item["end_char"]] == item["content"] for item in passages ) def test_source_discovery_supports_explicit_classification_globs(tmp_path: Path) -> None: canon = tmp_path / "canon" planning = tmp_path / "planning" canon.mkdir() planning.mkdir() (canon / "facts.md").write_text("Canon", encoding="utf-8") (planning / "notes.md").write_text("Planning", encoding="utf-8") discovered = discover_source_paths(tmp_path, ["canon/*.md"]) assert [path.name for path in discovered] == ["facts.md"] def test_register_source_is_idempotent_and_versions_changed_content(tmp_path: Path) -> None: work = source_work() root = tmp_path / "corpus" root.mkdir() path = root / "scene.md" path.write_text("# Scene\n\nFirst version.\n", encoding="utf-8") first = register_source( work=work, path=path, root=root, authority=DocumentAuthority.PROVISIONAL, document_type=DocumentType.SCENE, ) unchanged = register_source( work=work, path=path, root=root, authority=DocumentAuthority.PROVISIONAL, document_type=DocumentType.SCENE, ) reclassified = register_source( work=work, path=path, root=root, authority=DocumentAuthority.CANON, document_type=DocumentType.SCENE, ) path.write_text("# Scene\n\nSecond version.\n", encoding="utf-8") second = register_source( work=work, path=path, root=root, authority=DocumentAuthority.CANON, document_type=DocumentType.SCENE, ) document = SourceDocument.objects.get(work=work, logical_key="scene.md") versions = list(document.versions.order_by("version")) assert first.status == "created" assert unchanged.status == "unchanged" assert second.status == "versioned" assert reclassified.status == "versioned" assert len(versions) == 3 assert versions[1].supersedes == versions[0] assert versions[1].source_sha256 == versions[0].source_sha256 assert versions[1].authority == DocumentAuthority.CANON assert versions[2].supersedes == versions[1] assert versions[2].passages.count() == 2 assert versions[2].source_sha256 == hashlib.sha256(path.read_bytes()).hexdigest() versions[2].authority = DocumentAuthority.REJECTED with pytest.raises(ValueError, match="immutable"): versions[2].save() def test_story_sources_dry_run_does_not_create_database_records(tmp_path: Path) -> None: path = tmp_path / "canon.md" path.write_text("# Canon\n\nA fact.\n", encoding="utf-8") call_command( "story_sources", "register", root=tmp_path, series_slug="labyrinth-hero", series_title="Labyrinth Hero", work_slug="the-fortune-below", work_title="The Fortune Below", authority=DocumentAuthority.PROVISIONAL, dry_run=True, ) assert not Series.objects.exists() assert not SourceDocument.objects.exists()