Files
labyricorn-site/tests/test_project_editor.py
2026-08-13 18:53:18 -07:00

229 lines
9.6 KiB
Python

from __future__ import annotations
from pathlib import Path
import shutil
import sys
import unittest
from unittest.mock import patch
import uuid
from types import SimpleNamespace
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts"))
from project_editor import ( # noqa: E402
ProjectEditorError,
ProjectSourceRegistry,
check_devlog_readiness,
corrective_guidance,
)
from management_launcher import EDITOR_SCRIPTS # noqa: E402
from project_providers import ProjectProviderError, derive_repository_urls # noqa: E402
class FakeProjectSourceError(RuntimeError):
pass
class FakeRepositoryNotPublicError(FakeProjectSourceError):
pass
def fake_importer(**overrides: object) -> SimpleNamespace:
values: dict[str, object] = {
"ProjectSourceError": FakeProjectSourceError,
"RepositoryNotPublicError": FakeRepositoryNotPublicError,
"fetch_json": lambda *_args, **_kwargs: {"private": False, "default_branch": "main"},
"ensure_mirror": lambda *_args, **_kwargs: (Path("mirror.git"), True),
"resolve_branch": lambda *_args, **_kwargs: "a" * 40,
"validate_snapshot": lambda *_args, **_kwargs: ({}, {}),
}
values.update(overrides)
return SimpleNamespace(**values)
class ProjectEditorTests(unittest.TestCase):
def setUp(self) -> None:
cache_root = Path(__file__).resolve().parents[1] / ".cache"
cache_root.mkdir(exist_ok=True)
self.site_root = cache_root / f"project-editor-test-{uuid.uuid4().hex}"
(self.site_root / "configs").mkdir(parents=True)
(self.site_root / "content" / "projects").mkdir(parents=True)
(self.site_root / "Test.lektorproject").write_text("[project]\nname = Test\n", encoding="utf-8")
(self.site_root / "content" / "projects" / "contents.lr").write_text(
"_model: section\n---\ntitle: Projects\n", encoding="utf-8"
)
(self.site_root / "configs" / "project-sources.ini").write_text(
"# Preserve this header.\n\n"
"[alpha]\n"
"repository = https://git.example.test/team/alpha.git\n"
"web_url = https://git.example.test/team/alpha\n"
"api_url = https://git.example.test/api/v1/repos/team/alpha\n"
"branch = main\n"
"featured_order = 10\n"
"future_option = preserve me\n\n"
"[beta]\n"
"repository = https://git.example.test/team/beta.git\n"
"web_url = https://git.example.test/team/beta\n"
"api_url = https://git.example.test/api/v1/repos/team/beta\n"
"branch = stable\n",
encoding="utf-8",
)
self.registry = ProjectSourceRegistry(self.site_root)
def tearDown(self) -> None:
shutil.rmtree(self.site_root)
def test_current_registry_loads(self) -> None:
registry = ProjectSourceRegistry(Path(__file__).resolve().parents[1])
sources = registry.list_sources()
self.assertGreaterEqual(len(sources), 1)
self.assertTrue(all(source.repository.startswith("https://") for source in sources))
def test_management_launcher_exposes_project_editor_as_required_script(self) -> None:
self.assertIn(("Project Editor", "project_editor.py", True), EDITOR_SCRIPTS)
def test_create_edit_and_delete_preserve_other_configuration(self) -> None:
created = self.registry.create_source(
"gamma",
"https://git.example.test/team/gamma",
"main",
"",
)
alpha_before = self.registry.load_source("alpha")
saved = self.registry.save_source(
created,
created.web_url,
"release",
"30",
)
self.registry.delete_source(saved)
text = (self.site_root / "configs" / "project-sources.ini").read_text(encoding="utf-8")
self.assertIn("# Preserve this header.", text)
self.assertIn("future_option = preserve me", text)
self.assertIn("[beta]", text)
self.assertNotIn("[gamma]", text)
self.assertEqual(self.registry.load_source("alpha").repository, alpha_before.repository)
def test_edit_preserves_unknown_options_and_detects_external_change(self) -> None:
source = self.registry.load_source("alpha")
saved = self.registry.save_source(
source, source.web_url, "release", "11"
)
self.assertEqual(dict(saved.unknown_options)["future_option"], "preserve me")
with (self.site_root / "configs" / "project-sources.ini").open("a", encoding="utf-8") as output:
output.write("\n# external change\n")
with self.assertRaisesRegex(ProjectEditorError, "changed on disk"):
self.registry.save_source(
saved, saved.web_url, saved.branch, "11"
)
def test_delete_never_touches_external_repository_and_refuses_last_source(self) -> None:
beta = self.registry.load_source("beta")
self.registry.delete_source(beta)
alpha = self.registry.load_source("alpha")
with self.assertRaisesRegex(ProjectEditorError, "at least one"):
self.registry.delete_source(alpha)
def test_invalid_urls_and_ids_are_rejected(self) -> None:
with self.assertRaises(ProjectEditorError):
self.registry.create_source(
"Not Valid", "http://example.test/team/repo", "main", ""
)
def test_one_public_url_derives_github_and_gitea_configuration(self) -> None:
github = derive_repository_urls("https://github.com/example/public-project.git")
self.assertEqual(github.provider, "github")
self.assertEqual(github.web_url, "https://github.com/example/public-project")
self.assertEqual(github.repository, f"{github.web_url}.git")
self.assertEqual(
github.api_url, "https://api.github.com/repos/example/public-project"
)
gitea = derive_repository_urls("https://git.example.test/team/project")
self.assertEqual(gitea.provider, "gitea")
self.assertEqual(gitea.repository, "https://git.example.test/team/project.git")
self.assertEqual(
gitea.api_url, "https://git.example.test/api/v1/repos/team/project"
)
saved = self.registry.create_source(
"github-project",
"https://github.com/example/public-project",
"main",
"20",
)
self.assertEqual(saved.repository, github.repository)
self.assertEqual(saved.web_url, github.web_url)
self.assertEqual(saved.api_url, github.api_url)
def test_gitlab_url_is_rejected_clearly(self) -> None:
with self.assertRaisesRegex(ProjectProviderError, "GitLab.*not supported"):
derive_repository_urls("https://gitlab.com/example/project")
@patch("project_editor.shutil.which", return_value="git")
@patch("project_editor.tempfile.TemporaryDirectory")
def test_inaccessible_repository_is_not_reported_as_missing_devlog(
self, temporary: object, _which: object
) -> None:
temporary.return_value.__enter__.return_value = str(self.site_root / "temporary")
def fail_fetch(*_args: object, **_kwargs: object) -> object:
raise FakeProjectSourceError("initial repository fetch failed")
importer = fake_importer(ensure_mirror=fail_fetch)
result = check_devlog_readiness(self.registry.load_source("alpha"), importer)
self.assertEqual(result.status, "Repository inaccessible")
self.assertNotIn("Initialize Devlog", result.details)
@patch("project_editor.shutil.which", return_value="git")
@patch("project_editor.tempfile.TemporaryDirectory")
def test_missing_devlog_has_corrective_standalone_editor_guidance(
self, temporary: object, _which: object
) -> None:
temporary.return_value.__enter__.return_value = str(self.site_root / "temporary")
def fail_validation(*_args: object, **_kwargs: object) -> object:
raise FakeProjectSourceError(
"publishing tree is missing ['.labyricorn/devlog/contents.lr']"
)
importer = fake_importer(validate_snapshot=fail_validation)
source = self.registry.load_source("alpha")
result = check_devlog_readiness(source, importer)
self.assertEqual(result.status, "Not initialized")
guidance = corrective_guidance(source)
for phrase in (
"git clone",
"Copy scripts/devlog_editor.py",
"python devlog_editor.py",
"Initialize Devlog",
"Create and save",
"Publish Labyricorn Changes",
"commit and push",
"Check Devlog Status",
):
self.assertIn(phrase, guidance)
@patch("project_editor.shutil.which", return_value="git")
@patch("project_editor.tempfile.TemporaryDirectory")
def test_valid_remote_snapshot_reports_ready(
self, temporary: object, _which: object
) -> None:
temporary.return_value.__enter__.return_value = str(self.site_root / "temporary")
records = {
".labyricorn/project/contents.lr": {"title": "Alpha", "project_id": "alpha"},
".labyricorn/devlog/contents.lr": {"title": "Alpha devlog"},
}
importer = fake_importer(
resolve_branch=lambda *_args: "b" * 40,
validate_snapshot=lambda *_args: ({}, records),
)
result = check_devlog_readiness(self.registry.load_source("alpha"), importer)
self.assertTrue(result.ready)
self.assertEqual(result.status, "Ready")
self.assertIn("Valid devlog entries: 0", result.details)
if __name__ == "__main__":
unittest.main()