Files
nisse/tests/deploy/test_images.py
T
dongarandClaude Opus 5.5 2fb654f92b
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m49s
CI / e2e (pull_request) Successful in 4m0s
M7.6: nisse-review's image
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
2026-10-20 00:05:31 +00:00

97 lines
3.1 KiB
Python

"""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
)