Files
nisse/tests/integration/e2e_support.py
T
dongarandClaude Opus 5.5 679ace1fd8
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 5m17s
A repository per test run instead of the shared sandbox
Every live test that touches a repository gets one of its own from
e2e_support.fresh_repo, made by the test operator as an operator makes a
project's (D4, D8): seeded from tests/integration/seed/ (the sandbox's
package, without past runs' leftovers), the agent's account a collaborator
with write access, main protected (pushes only by the owner, an approval to
merge), and a deploy key generated for the run. It is deleted afterwards
(NISSE_E2E_KEEP_REPOS keeps it); scripts/e2e-cleanup.sh deletes leftovers.

- Both e2e fixtures use it; per-run base branches are gone. The merge
  helper approves first, as the protection now requires.
- test_gitea_live uses it, so CI needs NISSE_OPERATOR_TOKEN and no longer
  writes the sandbox deploy key (NISSE_PUSH_KEY).
- A live test of the helper: seeded, shared with the agent, protected
  against it, deleted.
- nisse-test.yaml's project entry is a placeholder; pytest and ruff skip
  the seed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
2026-10-06 05:10:27 +00:00

135 lines
4.5 KiB
Python

"""Shared by the live tests: each run gets a repository of its own, made by
the test operator and deleted afterwards, so runs never depend on one
another or on what earlier runs merged."""
import base64
import contextlib
import os
import subprocess
from collections.abc import Iterator
from dataclasses import dataclass
from pathlib import Path
from nisse_dev.gitea import GiteaClient, GiteaError
def task(run: str) -> str:
"""A request no earlier run can have fulfilled."""
return (
f"Add a function vowels_{run}(text) to the sandbox package that returns "
"how many of a, e, i, o, u (either case) the text contains, with tests."
)
def dependent_task(run: str) -> str:
"""A request that builds on task(run): it can only be done after it."""
return (
f"Add a function consonants_{run}(text) to the sandbox package that "
"returns how many letters in the text are not vowels, using "
f"vowels_{run}, with tests."
)
def operator_gitea(url: str) -> GiteaClient:
token = os.environ.get("NISSE_OPERATOR_TOKEN", "")
assert token.strip(), (
"NISSE_OPERATOR_TOKEN is not set: the test operator's Gitea token"
)
return GiteaClient(url, token)
OWNER = "nisse-testing-operator" # the test operator: owns every test repository
SEED = Path(__file__).parent / "seed"
@dataclass(frozen=True)
class FreshRepo:
name: str # owner/repo
push_key: Path # this repository's deploy key, private half
@contextlib.contextmanager
def fresh_repo(
operator: GiteaClient, run: str, keys: Path, *, agent: str
) -> Iterator[FreshRepo]:
"""A repository of its own for one test run, as the operator would make a
project's (D4, D8): seeded with a small Python package, the ``agent``
account a collaborator with write access, the default branch protected (pushes only by the
owner, an approval to merge), and a deploy key of its own, written under
``keys``. Deleted afterwards, unless NISSE_E2E_KEEP_REPOS is set."""
name = f"nisse-e2e-{run}"
full = f"{OWNER}/{name}"
operator._request(
"POST",
"/user/repos",
json={
"name": name,
"private": True,
"auto_init": True,
"default_branch": "main",
},
)
try:
files = [
{
"operation": "create",
"path": str(path.relative_to(SEED)),
"content": base64.b64encode(path.read_bytes()).decode(),
}
for path in sorted(SEED.rglob("*"))
if path.is_file() and path.name != "README.md"
]
operator._request(
"POST",
f"/repos/{full}/contents",
json={"branch": "main", "message": "Seed the test project", "files": files},
)
readme = operator._request("GET", f"/repos/{full}/contents/README.md")
operator._request(
"PUT",
f"/repos/{full}/contents/README.md",
json={
"branch": "main",
"message": "README",
"sha": readme["sha"],
"content": base64.b64encode((SEED / "README.md").read_bytes()).decode(),
},
)
operator._request(
"PUT", f"/repos/{full}/collaborators/{agent}", json={"permission": "write"}
)
keys.mkdir(parents=True, exist_ok=True)
key = keys / f"{name}.key"
key.unlink(missing_ok=True)
subprocess.run(
["ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-C", name, "-f", str(key)],
check=True,
)
operator._request(
"POST",
f"/repos/{full}/keys",
json={
"title": f"{name} push",
"key": key.with_suffix(".key.pub").read_text().strip(),
"read_only": False,
},
)
operator._request(
"POST",
f"/repos/{full}/branch_protections",
# Pushes only by the operator (who sets up fixtures, as an owner
# can); the agent's are refused, and merging needs an approval.
json={
"branch_name": "main",
"enable_push": True,
"enable_push_whitelist": True,
"push_whitelist_usernames": [OWNER],
"required_approvals": 1,
},
)
yield FreshRepo(full, key)
finally:
if not os.environ.get("NISSE_E2E_KEEP_REPOS"):
with contextlib.suppress(GiteaError):
operator._request("DELETE", f"/repos/{full}")