122 lines
3.8 KiB
Python
122 lines
3.8 KiB
Python
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()
|