Files
playbook/test/test_build_tsl_playbook.py
T
cshandClaude Opus 4.8 94d968d68d 🔒 security(ci): keep sync token out of remote url via GIT_ASKPASS
- inline .gitea/ci/sync_tsl_playbook.sh into sync-tsl-playbook.yml
- clone over plain repo url; credentials flow through an ephemeral
  GIT_ASKPASS helper removed in cleanup
- rewrite tests to extract and exercise the workflow run block

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 13:11:58 +08:00

279 lines
9.2 KiB
Python

import os
import shutil
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SCRIPT = ROOT / "scripts" / "build_tsl_playbook.py"
SYNC_WORKFLOW = ROOT / ".gitea" / "workflows" / "sync-tsl-playbook.yml"
class BuildTslPlaybookTests(unittest.TestCase):
def test_builds_minimal_tsl_playbook_tree(self):
with tempfile.TemporaryDirectory() as tmp_dir:
output = Path(tmp_dir) / "tsl-playbook"
result = subprocess.run(
[sys.executable, str(SCRIPT), "--output", str(output)],
capture_output=True,
text=True,
)
self.assertEqual(result.returncode, 0, msg=result.stderr)
self.assertEqual(
sorted(path.name for path in output.iterdir()),
["AGENTS.md", "docs", "skills"],
)
self.assertFalse((output / "playbook.toml").exists())
self.assertFalse((output / ".agents").exists())
self.assertFalse((output / "docs" / "index.md").exists())
self.assertTrue((output / "docs" / "tsl" / "index.md").is_file())
self.assertTrue(
(output / "skills" / "tsl-api-reference" / "SKILL.md").is_file()
)
self.assertTrue(
(
output
/ "skills"
/ "tsl-api-reference"
/ "scripts"
/ "lookup.py"
).is_file()
)
agents_text = (output / "AGENTS.md").read_text(encoding="utf-8")
self.assertIn("# TSL Agent Instructions", agents_text)
self.assertIn("docs/tsl/index.md", agents_text)
self.assertIn("tsl-api-reference", agents_text)
self.assertNotIn(".agents/index.md", agents_text)
self.assertNotIn(".agents/tsl/index.md", agents_text)
source_docs = count_files(ROOT / "docs" / "tsl")
output_docs = count_files(output / "docs" / "tsl")
self.assertEqual(output_docs, source_docs)
source_skill = count_files(ROOT / "skills" / "tsl-api-reference")
output_skill = count_files(output / "skills" / "tsl-api-reference")
self.assertEqual(output_skill, source_skill)
def test_sync_workflow_does_not_remove_entire_target_branch(self):
text = SYNC_WORKFLOW.read_text(encoding="utf-8")
self.assertNotRegex(text, r"git rm -rf --quiet\s+\.")
self.assertIn("generated_paths=(AGENTS.md docs skills)", text)
self.assertIn('git add -A "${generated_paths[@]}"', text)
self.assertNotIn(".gitea/ci/", text)
self.assertNotIn("https://oauth2", text)
self.assertNotIn("oauth2:${TOKEN}", text)
self.assertNotRegex(text, r"REPO_URL=.*(TOKEN|WORKFLOW)")
self.assertNotIn("git remote set-url", text)
self.assertIn("GIT_ASKPASS", text)
self.assertIn('REPO_URL="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git"', text)
def test_sync_preserves_files_outside_generated_paths(self):
if shutil.which("bash") is None:
self.skipTest("bash is required to run sync workflow script")
with tempfile.TemporaryDirectory() as tmp_dir:
repo = create_source_repo(Path(tmp_dir))
git(repo, "checkout", "--orphan", "tsl-playbook")
git(repo, "rm", "-rf", ".")
(repo / "README.md").write_text(
"manual branch note\n", encoding="utf-8", newline="\n"
)
git(repo, "add", "README.md")
git(repo, "commit", "-m", "manual target branch note")
git(repo, "push", "-u", "origin", "tsl-playbook")
git(repo, "checkout", "main")
run_sync(repo)
readme = run(
["git", "show", "HEAD:README.md"],
cwd=repo,
check=False,
)
self.assertEqual(readme.returncode, 0, msg=readme.stderr)
self.assertEqual(readme.stdout, "manual branch note\n")
for path in (
"AGENTS.md",
"docs/tsl/index.md",
"skills/tsl-api-reference/SKILL.md",
):
git(repo, "cat-file", "-e", f"HEAD:{path}")
def test_sync_creates_new_branch_without_source_files(self):
if shutil.which("bash") is None:
self.skipTest("bash is required to run sync workflow script")
with tempfile.TemporaryDirectory() as tmp_dir:
repo = create_source_repo(Path(tmp_dir))
run_sync(repo)
for path in (
"AGENTS.md",
"docs/tsl/index.md",
"skills/tsl-api-reference/SKILL.md",
):
git(repo, "cat-file", "-e", f"HEAD:{path}")
for path in (
".gitea/workflows/sync-tsl-playbook.yml",
"scripts/build_tsl_playbook.py",
"rulesets/tsl/index.md",
):
result = run(
["git", "cat-file", "-e", f"HEAD:{path}"],
cwd=repo,
check=False,
)
self.assertNotEqual(result.returncode, 0, msg=f"{path} leaked")
def create_source_repo(tmp: Path) -> Path:
repo = tmp / "repo"
remote = tmp / "remote.git"
run(["git", "init", "--bare", str(remote)])
run(["git", "init", str(repo)])
git(repo, "checkout", "-b", "main")
git(repo, "config", "user.name", "test")
git(repo, "config", "user.email", "test@example.invalid")
copy_required_sources(repo)
git(repo, "add", ".")
git(repo, "commit", "-m", "initial sources")
git(repo, "remote", "add", "origin", "../remote.git")
git(repo, "push", "-u", "origin", "main")
return repo
def run_sync(repo: Path) -> None:
env = os.environ.copy()
env.update(
{
"REPO_DIR": str(repo),
"TARGET_BRANCH": "tsl-playbook",
"COMMIT_AUTHOR_NAME": "test",
"COMMIT_AUTHOR_EMAIL": "test@example.invalid",
}
)
with tempfile.NamedTemporaryFile(
"w", suffix=".sh", encoding="utf-8", newline="\n", delete=False
) as script_file:
script_file.write(extract_sync_workflow_script())
script_path = script_file.name
try:
result = subprocess.run(
["bash", script_path],
cwd=repo,
env=env,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
)
finally:
os.unlink(script_path)
if result.returncode != 0:
raise AssertionError(result.stderr + result.stdout)
def copy_required_sources(repo: Path) -> None:
(repo / ".gitea" / "workflows").mkdir(parents=True)
shutil.copy2(
SYNC_WORKFLOW, repo / ".gitea" / "workflows" / "sync-tsl-playbook.yml"
)
(repo / "scripts").mkdir()
shutil.copy2(SCRIPT, repo / "scripts" / "build_tsl_playbook.py")
(repo / "docs" / "tsl").mkdir(parents=True)
(repo / "docs" / "tsl" / "index.md").write_text(
"# TSL Index\n", encoding="utf-8", newline="\n"
)
skill = repo / "skills" / "tsl-api-reference"
(skill / "scripts").mkdir(parents=True)
(skill / "SKILL.md").write_text(
"---\nname: tsl-api-reference\n---\n", encoding="utf-8", newline="\n"
)
(skill / "scripts" / "lookup.py").write_text(
"print('lookup')\n", encoding="utf-8", newline="\n"
)
(repo / "rulesets" / "tsl").mkdir(parents=True)
(repo / "rulesets" / "tsl" / "index.md").write_text(
"# TSL Agent Instructions\n", encoding="utf-8", newline="\n"
)
def count_files(path: Path) -> int:
return sum(1 for item in path.rglob("*") if item.is_file())
def extract_sync_workflow_script() -> str:
lines = SYNC_WORKFLOW.read_text(encoding="utf-8").splitlines()
in_sync_step = False
in_run_block = False
script_lines: list[str] = []
for line in lines:
if line.startswith(" - name: 📦 Build and publish tsl-playbook"):
in_sync_step = True
continue
if in_sync_step and line.startswith(" - name: "):
break
if in_sync_step and line == " run: |":
in_run_block = True
continue
if not in_run_block:
continue
if line.startswith(" "):
script_lines.append(line[10:])
continue
if line.strip() == "":
script_lines.append("")
continue
break
if not script_lines:
raise AssertionError("sync workflow run block was not found")
return "\n".join(script_lines) + "\n"
def git(repo: Path, *args: str) -> subprocess.CompletedProcess[str]:
return run(["git", *args], cwd=repo)
def run(
args: list[str],
cwd: Path | None = None,
check: bool = True,
) -> subprocess.CompletedProcess[str]:
result = subprocess.run(
args,
cwd=cwd,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
)
if check and result.returncode != 0:
raise AssertionError(
f"command failed: {' '.join(args)}\n{result.stderr}{result.stdout}"
)
return result
if __name__ == "__main__":
unittest.main()