Compare commits
5 Commits
efa43e2c97
...
79ad108e2f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79ad108e2f | ||
| d9cf056df8 | |||
|
|
bd9286a39f | ||
|
|
f4f8827353 | ||
|
|
7d24f19cc4 |
@@ -5,6 +5,8 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -12,11 +14,31 @@ from pathlib import Path
|
||||
|
||||
HEADER_PATTERN = re.compile(r"^##\s+\S+", re.MULTILINE)
|
||||
LIST_ITEM_PATTERN = re.compile(r"^\s*(?:-|\*|\d+\.)\s+\S+", re.MULTILINE)
|
||||
FENCED_CODE_PATTERN = re.compile(r"```.*?```", re.DOTALL)
|
||||
INLINE_CODE_PATTERN = re.compile(r"`[^`]*`")
|
||||
|
||||
|
||||
def _strip_code_segments(text: str) -> str:
|
||||
without_fences = FENCED_CODE_PATTERN.sub("", text)
|
||||
return INLINE_CODE_PATTERN.sub("", without_fences)
|
||||
|
||||
|
||||
def resolve_tea_binary() -> str:
|
||||
tea_from_path = shutil.which("tea")
|
||||
if tea_from_path:
|
||||
return tea_from_path
|
||||
|
||||
tea_home = Path.home() / "bin" / "tea"
|
||||
if tea_home.exists() and tea_home.is_file() and os.access(tea_home, os.X_OK):
|
||||
return str(tea_home)
|
||||
|
||||
raise RuntimeError("tea binary not found (checked PATH and ~/bin/tea)")
|
||||
|
||||
|
||||
def validate_pr_body_text(text: str) -> list[str]:
|
||||
errors: list[str] = []
|
||||
if "\\n" in text and "\n" not in text:
|
||||
searchable = _strip_code_segments(text)
|
||||
if "\\n" in searchable:
|
||||
errors.append("body contains escaped newline sequence (\\n)")
|
||||
if text.count("```") % 2 != 0:
|
||||
errors.append("body has unbalanced fenced code blocks (``` count is odd)")
|
||||
@@ -28,10 +50,11 @@ def validate_pr_body_text(text: str) -> list[str]:
|
||||
|
||||
|
||||
def fetch_pr_body(pr_number: int) -> str:
|
||||
tea_binary = resolve_tea_binary()
|
||||
try:
|
||||
completed = subprocess.run(
|
||||
[
|
||||
"tea",
|
||||
tea_binary,
|
||||
"api",
|
||||
"-R",
|
||||
"origin",
|
||||
@@ -41,7 +64,7 @@ def fetch_pr_body(pr_number: int) -> str:
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
except (subprocess.CalledProcessError, FileNotFoundError) as exc:
|
||||
except (subprocess.CalledProcessError, FileNotFoundError, PermissionError) as exc:
|
||||
raise RuntimeError(f"failed to fetch PR #{pr_number}: {exc}") from exc
|
||||
|
||||
try:
|
||||
|
||||
@@ -24,9 +24,24 @@ def test_validate_pr_body_text_detects_escaped_newline() -> None:
|
||||
assert any("escaped newline" in err for err in errors)
|
||||
|
||||
|
||||
def test_validate_pr_body_text_allows_literal_sequence_when_multiline() -> None:
|
||||
def test_validate_pr_body_text_detects_escaped_newline_in_multiline_body() -> None:
|
||||
module = _load_module()
|
||||
text = "## Summary\n- escaped sequence example: \\\\n"
|
||||
text = "## Summary\n- first line\n- broken line with \\n literal"
|
||||
errors = module.validate_pr_body_text(text)
|
||||
assert any("escaped newline" in err for err in errors)
|
||||
|
||||
|
||||
def test_validate_pr_body_text_allows_escaped_newline_in_code_blocks() -> None:
|
||||
module = _load_module()
|
||||
text = "\n".join(
|
||||
[
|
||||
"## Summary",
|
||||
"- example uses `\\n` for explanation",
|
||||
"```bash",
|
||||
"printf 'line1\\nline2\\n'",
|
||||
"```",
|
||||
]
|
||||
)
|
||||
assert module.validate_pr_body_text(text) == []
|
||||
|
||||
|
||||
@@ -63,12 +78,13 @@ def test_fetch_pr_body_reads_body_from_tea_api(monkeypatch) -> None:
|
||||
module = _load_module()
|
||||
|
||||
def fake_run(cmd, check, capture_output, text): # noqa: ANN001
|
||||
assert "tea" in cmd[0]
|
||||
assert cmd[0] == "/tmp/tea-bin"
|
||||
assert check is True
|
||||
assert capture_output is True
|
||||
assert text is True
|
||||
return SimpleNamespace(stdout=json.dumps({"body": "## Summary\n- item"}))
|
||||
|
||||
monkeypatch.setattr(module, "resolve_tea_binary", lambda: "/tmp/tea-bin")
|
||||
monkeypatch.setattr(module.subprocess, "run", fake_run)
|
||||
assert module.fetch_pr_body(391) == "## Summary\n- item"
|
||||
|
||||
@@ -79,6 +95,32 @@ def test_fetch_pr_body_rejects_non_string_body(monkeypatch) -> None:
|
||||
def fake_run(cmd, check, capture_output, text): # noqa: ANN001
|
||||
return SimpleNamespace(stdout=json.dumps({"body": 123}))
|
||||
|
||||
monkeypatch.setattr(module, "resolve_tea_binary", lambda: "/tmp/tea-bin")
|
||||
monkeypatch.setattr(module.subprocess, "run", fake_run)
|
||||
with pytest.raises(RuntimeError):
|
||||
module.fetch_pr_body(391)
|
||||
|
||||
|
||||
def test_resolve_tea_binary_falls_back_to_home_bin(monkeypatch, tmp_path) -> None:
|
||||
module = _load_module()
|
||||
tea_home = tmp_path / "bin" / "tea"
|
||||
tea_home.parent.mkdir(parents=True)
|
||||
tea_home.write_text("#!/usr/bin/env bash\n", encoding="utf-8")
|
||||
tea_home.chmod(0o755)
|
||||
|
||||
monkeypatch.setattr(module.shutil, "which", lambda _: None)
|
||||
monkeypatch.setattr(module.Path, "home", lambda: tmp_path)
|
||||
assert module.resolve_tea_binary() == str(tea_home)
|
||||
|
||||
|
||||
def test_resolve_tea_binary_rejects_non_executable_home_bin(monkeypatch, tmp_path) -> None:
|
||||
module = _load_module()
|
||||
tea_home = tmp_path / "bin" / "tea"
|
||||
tea_home.parent.mkdir(parents=True)
|
||||
tea_home.write_text("not executable\n", encoding="utf-8")
|
||||
tea_home.chmod(0o644)
|
||||
|
||||
monkeypatch.setattr(module.shutil, "which", lambda _: None)
|
||||
monkeypatch.setattr(module.Path, "home", lambda: tmp_path)
|
||||
with pytest.raises(RuntimeError):
|
||||
module.resolve_tea_binary()
|
||||
|
||||
Reference in New Issue
Block a user