M7.6: nisse-review's image
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m49s
CI / e2e (pull_request) Successful in 4m0s

A `review` target in the Dockerfile: its own user (nisse-review, uid
10002), git for its clones, state and clones in /var/lib/nisse-review,
configuration at /etc/nisse-review, and only its own agent installed.
release.yml publishes gitea.dongar.net/dev/nisse-review beside nisse-dev,
at the same version. review-bringup.sh builds and runs the image, as
bringup.sh runs nisse-dev's.

The test stage adds an agent package its base lacks, without its
dependencies, since the suite's own code imports every agent's. Without
that, the tests over nisse-dev's image no longer import since
test_review_live arrived. tests/deploy/test_images.py checks the image and
that the test stage leaves what it installed as it was.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk
This commit is contained in:
dongarandClaude Opus 5.5 committed 2026-10-20 00:05:31 +00:00
1 parent 5277d7827b
commit 2fb654f92b
6 files changed
+324 -35

No files matched your search

+96
View File
@@ -0,0 +1,96 @@
"""Every agent is its own image (Development Plan §3, M7.6): nisse-review's
runs as its own user with git and only its own agent, and the test stage
over it keeps what it installed. Builds the images, so it needs Docker."""
import shutil
import subprocess
from pathlib import Path
import pytest
ROOT = Path(__file__).parent.parent.parent
pytestmark = [
pytest.mark.integration,
pytest.mark.skipif(
shutil.which("docker") is None
or subprocess.run(
["docker", "info"], capture_output=True, check=False
).returncode,
reason="needs Docker",
),
]
# The installed packages' files and their times, hashed: the same in an
# image and the test stage over it means the tests ran against what was
# built, not a reinstall (even one from the same source).
HASH = (
"import hashlib, pathlib, nisse_review, nisse_core; h = hashlib.sha256(); "
"[h.update(p.read_bytes() + str(p.stat().st_mtime_ns).encode()) for m in (nisse_review, nisse_core) "
"for p in sorted(pathlib.Path(m.__file__).parent.rglob('*.py'))]; "
"print(h.hexdigest())"
)
def build(tag: str, *args: str) -> str:
subprocess.run(
["docker", "build", "--quiet", "-t", tag, *args, str(ROOT)],
check=True,
capture_output=True,
timeout=1200,
)
return tag
def sh(image: str, script: str) -> subprocess.CompletedProcess[str]:
return subprocess.run(
["docker", "run", "--rm", "--entrypoint", "sh", image, "-c", script],
capture_output=True,
text=True,
timeout=120,
check=False,
)
@pytest.fixture(scope="module")
def review() -> str:
return build("nisse-review:image-test", "--target", "review")
def test_the_reviewer_runs_as_its_own_user_with_git(review: str) -> None:
done = sh(review, "id -un; git --version; command -v nisse-review")
user, git, entry = done.stdout.splitlines()
assert user == "nisse-review" and git.startswith("git version")
assert entry == "/opt/nisse/bin/nisse-review"
inspected = subprocess.run(
["docker", "inspect", "-f", "{{json .Config.Volumes}}", review],
capture_output=True,
text=True,
check=True,
).stdout
assert "/var/lib/nisse-review" in inspected
def test_the_reviewer_image_holds_no_other_agent(review: str) -> None:
assert sh(review, "python -c 'import nisse_review'").returncode == 0
assert sh(review, "python -c 'import nisse_dev'").returncode != 0
def test_its_state_is_writable_and_its_config_folder_is_not(review: str) -> None:
assert sh(review, "touch /var/lib/nisse-review/x").returncode == 0
assert sh(review, "touch /etc/nisse-review/x").returncode != 0
def test_the_test_stage_over_it_keeps_what_it_installed(review: str) -> None:
tests = build(
"nisse-review:image-test-tests",
"--target",
"test",
"--build-arg",
f"TEST_BASE={review}",
)
both = sh(tests, "python -c 'import nisse_dev, nisse_review, pytest'")
assert both.returncode == 0, both.stderr # the suite's imports resolve
assert (
sh(review, f'python -c "{HASH}"').stdout
== sh(tests, f'python -c "{HASH}"').stdout
)
+97
View File
@@ -0,0 +1,97 @@
"""scripts/review-bringup.sh runs nisse-review from its image (M7.6): built
from the code under test, with the reviewer's folder at its own path."""
import subprocess
from pathlib import Path
SCRIPT = Path(__file__).parent.parent.parent / "scripts" / "review-bringup.sh"
def _setup(tmp_path: Path, *, running: bool = False) -> dict[str, str]:
"""A source repository with one commit, the reviewer's folder with its
config, and stand-ins for docker, systemd-run and systemctl that log."""
src = tmp_path / "src"
src.mkdir()
(src / "Dockerfile").write_text("FROM scratch\n")
for args in (
["init", "-q"],
["add", "."],
["-c", "user.name=t", "-c", "user.email=t@t", "commit", "-qm", "c"],
):
subprocess.run(["git", "-C", str(src), *args], check=True)
review = tmp_path / "review"
review.mkdir()
(review / "nisse-review.yaml").write_text("state_db: x\n")
(review / ".env").write_text("")
bin_ = tmp_path / "bin"
bin_.mkdir()
log = tmp_path / "calls"
stubs = {
"docker": f'echo "docker $*" >> {log}',
"systemd-run": f'echo "systemd-run $*" >> {log}',
"systemctl": f'echo "systemctl $*" >> {log}; '
f'[ "$1" = is-active ] && exit {0 if running else 3}; exit 0',
}
for name, body in stubs.items():
path = bin_ / name
path.write_text(f"#!/usr/bin/env bash\n{body}\n")
path.chmod(0o755)
return {
"PATH": f"{bin_}:/usr/bin:/bin",
"HOME": str(tmp_path),
"NISSE_REVIEW": str(review),
"NISSE_SRC": str(src),
"LOG": str(log),
}
def _run(env: dict[str, str], *args: str) -> subprocess.CompletedProcess[str]:
return subprocess.run(
["bash", str(SCRIPT), *args],
env=env,
capture_output=True,
text=True,
timeout=30,
check=False,
)
def test_start_builds_the_review_image_and_runs_it(tmp_path: Path) -> None:
env = _setup(tmp_path)
review = env["NISSE_REVIEW"]
done = _run(env, "start")
assert done.returncode == 0, done.stderr
calls = Path(env["LOG"]).read_text().splitlines()
[build] = [c for c in calls if c.startswith("docker build")]
assert "--target review" in build and build.endswith(f"{review}/code")
assert (Path(review) / "code" / "Dockerfile").exists() # the code exported
[run] = [c for c in calls if c.startswith("systemd-run")]
assert "--unit=nisse-review-bringup" in run
assert "--property=KillSignal=SIGTERM" in run
assert "docker run --rm --name nisse-review-bringup" in run
assert f"-v {review}:{review}" in run
assert f"-e NISSE_CONFIG={review}/nisse-review.yaml" in run
assert f"-e NISSE_ENV={review}/.env" in run
assert run.endswith("nisse-review:bringup")
sha = subprocess.run(
["git", "-C", env["NISSE_SRC"], "rev-parse", "HEAD"],
capture_output=True,
text=True,
check=True,
).stdout.strip()
assert (Path(review) / "run-info").read_text().strip() == sha
def test_it_wont_start_a_second_reviewer(tmp_path: Path) -> None:
env = _setup(tmp_path, running=True)
done = _run(env, "start")
assert done.returncode != 0 and "already running" in done.stderr
assert "docker" not in Path(env["LOG"]).read_text()
def test_plan_shows_what_start_would_run(tmp_path: Path) -> None:
env = _setup(tmp_path)
done = _run(env, "plan")
assert done.returncode == 0, done.stderr
assert "docker build --target review -t nisse-review:bringup" in done.stdout
assert not Path(env["LOG"]).exists() # nothing run