merge: feature/v3-session-policy-stream into main #399

Merged
jihoson merged 168 commits from feature/main-merge-v3-session-policy-stream-20260303 into main 2026-03-04 00:47:20 +09:00
2 changed files with 55 additions and 5 deletions
Showing only changes of commit f4f8827353 - Show all commits

View File

@@ -5,6 +5,7 @@ from __future__ import annotations
import argparse import argparse
import json import json
import shutil
import re import re
import subprocess import subprocess
import sys import sys
@@ -12,11 +13,31 @@ from pathlib import Path
HEADER_PATTERN = re.compile(r"^##\s+\S+", re.MULTILINE) HEADER_PATTERN = re.compile(r"^##\s+\S+", re.MULTILINE)
LIST_ITEM_PATTERN = re.compile(r"^\s*(?:-|\*|\d+\.)\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():
return str(tea_home)
raise RuntimeError("tea binary not found (checked PATH and ~/bin/tea)")
def validate_pr_body_text(text: str) -> list[str]: def validate_pr_body_text(text: str) -> list[str]:
errors: 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)") errors.append("body contains escaped newline sequence (\\n)")
if text.count("```") % 2 != 0: if text.count("```") % 2 != 0:
errors.append("body has unbalanced fenced code blocks (``` count is odd)") errors.append("body has unbalanced fenced code blocks (``` count is odd)")
@@ -28,10 +49,11 @@ def validate_pr_body_text(text: str) -> list[str]:
def fetch_pr_body(pr_number: int) -> str: def fetch_pr_body(pr_number: int) -> str:
tea_binary = resolve_tea_binary()
try: try:
completed = subprocess.run( completed = subprocess.run(
[ [
"tea", tea_binary,
"api", "api",
"-R", "-R",
"origin", "origin",

View File

@@ -24,9 +24,24 @@ def test_validate_pr_body_text_detects_escaped_newline() -> None:
assert any("escaped newline" in err for err in errors) 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() 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) == [] 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() module = _load_module()
def fake_run(cmd, check, capture_output, text): # noqa: ANN001 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 check is True
assert capture_output is True assert capture_output is True
assert text is True assert text is True
return SimpleNamespace(stdout=json.dumps({"body": "## Summary\n- item"})) 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) monkeypatch.setattr(module.subprocess, "run", fake_run)
assert module.fetch_pr_body(391) == "## Summary\n- item" assert module.fetch_pr_body(391) == "## Summary\n- item"
@@ -79,6 +95,18 @@ def test_fetch_pr_body_rejects_non_string_body(monkeypatch) -> None:
def fake_run(cmd, check, capture_output, text): # noqa: ANN001 def fake_run(cmd, check, capture_output, text): # noqa: ANN001
return SimpleNamespace(stdout=json.dumps({"body": 123})) return SimpleNamespace(stdout=json.dumps({"body": 123}))
monkeypatch.setattr(module, "resolve_tea_binary", lambda: "/tmp/tea-bin")
monkeypatch.setattr(module.subprocess, "run", fake_run) monkeypatch.setattr(module.subprocess, "run", fake_run)
with pytest.raises(RuntimeError): with pytest.raises(RuntimeError):
module.fetch_pr_body(391) 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")
monkeypatch.setattr(module.shutil, "which", lambda _: None)
monkeypatch.setattr(module.Path, "home", lambda: tmp_path)
assert module.resolve_tea_binary() == str(tea_home)