Files
govoplan/tests/test_devkit_environment.py
T
zemion 2ffdb23f69
Dependency Audit / dependency-audit (push) Successful in 1m45s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 11m30s
feat(devkit): add resumable workspace automation and UI review tooling
Verified with the coordinated workspace changes by devkit full run
2026-09-08T225814-186389-0000-3e3ed7cd (all seven phases passed).
This shared UI pass does not mark the individual module reviews complete.
2026-09-09 02:03:17 +02:00

379 lines
14 KiB
Python
Executable File

"""Environment probes read bounded stable files; all fixtures are local."""
import hashlib
import os
from pathlib import Path
import sys
from types import SimpleNamespace
from unittest.mock import patch
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools/devkit"))
from govoplan_devkit import environment
from govoplan_devkit.common import digest
from govoplan_devkit.workspace import Project, Repository
def mutate_during_read(monkeypatch, callback):
original = hashlib.sha256
mutated = False
class MutatingHasher:
def __init__(self):
self.hasher = original()
def update(self, chunk):
nonlocal mutated
self.hasher.update(chunk)
if not mutated:
mutated = True
callback()
def hexdigest(self):
return self.hasher.hexdigest()
monkeypatch.setattr(environment.hashlib, "sha256", MutatingHasher)
@pytest.mark.parametrize("content", [b"", b"stable", b"x" * (1024 * 1024 + 3)])
def test_file_hash_keeps_existing_digest_and_exact_size_boundary(tmp_path, content):
path = tmp_path / "input"
path.write_bytes(content)
assert (
environment._environment_file_hash(path, len(content))
== hashlib.sha256(content).hexdigest()
)
def test_missing_optional_file_remains_optional_without_a_persistent_cache(tmp_path):
path = tmp_path / "input"
assert environment._environment_file_hash(path, 10) is None
path.write_bytes(b"first")
first = environment._environment_file_hash(path, 10)
path.write_bytes(b"other")
assert environment._environment_file_hash(path, 10) != first
@pytest.mark.parametrize("kind", ["fifo", "directory", "oversized", "dangling"])
def test_present_unsafe_input_is_rejected_without_blocking(tmp_path, kind):
path = tmp_path / "input"
if kind == "fifo":
os.mkfifo(path)
elif kind == "directory":
path.mkdir()
elif kind == "oversized":
path.write_bytes(b"too large")
else:
path.symlink_to(tmp_path / "missing")
with pytest.raises(ValueError, match="Environment input"):
environment._environment_file_hash(path, 2)
def test_stable_venv_executable_symlink_keeps_original_path_and_identity(tmp_path):
target = tmp_path / "real-python"
target.write_bytes(b"fixture binary")
executable = tmp_path / "venv" / "bin" / "python"
executable.parent.mkdir(parents=True)
executable.symlink_to(target)
repo = tmp_path / "repo"
metadata = repo / "node_modules" / ".package-lock.json"
metadata.parent.mkdir(parents=True)
metadata.write_bytes(b'{"fixture":true}')
project = Project("Fixture", (Repository("repo", repo),), {})
tools = {"python": str(executable)}
env = {"PATH": "fixture", "PWD": "ignored"}
distributions = b'[["fixture", "1"]]\n'
with (
patch.object(environment, "tool_version", return_value="fixture-version"),
patch.object(
environment,
"require_capture",
return_value=SimpleNamespace(returncode=0, stdout=distributions),
) as capture,
):
actual = environment.environment_fingerprint(tmp_path, project, tools, env)
assert actual == digest(
{
"environment": {"PATH": "fixture"},
"tools": {
"python": {
"path": str(executable),
"version": "fixture-version",
"sha256": hashlib.sha256(target.read_bytes()).hexdigest(),
}
},
"installed": {
str(metadata): hashlib.sha256(metadata.read_bytes()).hexdigest(),
"python_distributions": hashlib.sha256(distributions).hexdigest(),
},
}
)
assert capture.call_args.args[0][0] == str(executable)
def test_symlinked_package_directory_is_allowed_when_stable(tmp_path):
actual = tmp_path / "packages"
actual.mkdir()
(actual / ".package-lock.json").write_bytes(b"fixture")
link = tmp_path / "node_modules"
link.symlink_to(actual, target_is_directory=True)
assert environment._environment_file_hash(link / ".package-lock.json", 10)
def test_fifo_replacement_between_inspection_and_open_does_not_block(
tmp_path, monkeypatch
):
path = tmp_path / "input"
path.write_bytes(b"fixture")
real_open = os.open
def replace_before_open(target, flags):
path.unlink()
os.mkfifo(path)
assert flags & os.O_NONBLOCK
return real_open(target, flags)
monkeypatch.setattr(environment.os, "open", replace_before_open)
with pytest.raises(ValueError, match="bounded regular"):
environment._environment_file_hash(path, 10)
def test_growth_between_inspection_and_open_cannot_bypass_size_bound(
tmp_path, monkeypatch
):
path = tmp_path / "input"
path.write_bytes(b"a")
real_open = os.open
def grow_before_open(target, flags):
path.write_bytes(b"x" * 11)
return real_open(target, flags)
monkeypatch.setattr(environment.os, "open", grow_before_open)
with pytest.raises(ValueError, match="bounded regular"):
environment._environment_file_hash(path, 10)
def test_growth_during_read_cannot_bypass_size_bound(tmp_path, monkeypatch):
path = tmp_path / "input"
path.write_bytes(b"a")
mutate_during_read(monkeypatch, lambda: path.write_bytes(b"x" * 11))
with pytest.raises(ValueError, match="grew beyond"):
environment._environment_file_hash(path, 10)
def test_same_size_edit_with_restored_mtime_is_not_a_stable_identity(
tmp_path, monkeypatch
):
path = tmp_path / "input"
path.write_bytes(b"first")
metadata = path.stat()
def mutate():
path.write_bytes(b"other")
os.utime(path, ns=(metadata.st_atime_ns, metadata.st_mtime_ns))
mutate_during_read(monkeypatch, mutate)
with pytest.raises(ValueError, match="changed during"):
environment._environment_file_hash(path, 10)
def test_replacement_with_identical_bytes_is_not_a_stable_identity(
tmp_path, monkeypatch
):
path = tmp_path / "input"
path.write_bytes(b"fixture")
replacement = tmp_path / "replacement"
replacement.write_bytes(b"fixture")
mutate_during_read(monkeypatch, lambda: replacement.replace(path))
with pytest.raises(ValueError, match="changed during"):
environment._environment_file_hash(path, 10)
def test_symlink_retarget_with_identical_bytes_is_not_a_stable_identity(
tmp_path, monkeypatch
):
first, second, link = (tmp_path / name for name in ("first", "second", "python"))
first.write_bytes(b"fixture")
second.write_bytes(b"fixture")
link.symlink_to(first)
def retarget():
link.unlink()
link.symlink_to(second)
mutate_during_read(monkeypatch, retarget)
with pytest.raises(ValueError, match="changed during"):
environment._environment_file_hash(link, 10)
def test_source_disappearing_during_read_fails_closed(tmp_path, monkeypatch):
path = tmp_path / "input"
path.write_bytes(b"fixture")
mutate_during_read(monkeypatch, path.unlink)
with pytest.raises(ValueError, match="Environment input"):
environment._environment_file_hash(path, 10)
def test_parent_symlink_retarget_is_rejected_even_for_the_same_target_inode(
tmp_path, monkeypatch
):
first, second, link = (tmp_path / name for name in ("first", "second", "bin"))
first.mkdir()
second.mkdir()
(first / "python").write_bytes(b"fixture")
os.link(first / "python", second / "python")
link.symlink_to(first, target_is_directory=True)
def retarget():
link.unlink()
link.symlink_to(second, target_is_directory=True)
mutate_during_read(monkeypatch, retarget)
with pytest.raises(ValueError, match="changed during"):
environment._environment_file_hash(link / "python", 10)
def test_invalid_executable_is_rejected_before_any_version_process(tmp_path):
executable = tmp_path / "python"
os.mkfifo(executable)
project = Project("Fixture", (), {})
with patch.object(environment, "tool_version") as version:
with pytest.raises(ValueError, match="bounded regular"):
environment.environment_fingerprint(
tmp_path, project, {"python": str(executable)}, {}
)
version.assert_not_called()
def discovery_project(root, *, path="govoplan-backend"):
repo = root / path
(repo / "src").mkdir(parents=True)
return Project(
"GovOPlaN",
(Repository("govoplan-backend", repo),),
{"organization": "GovOPlaN"},
)
def test_native_shape_ignores_build_files_and_directory_timestamps(tmp_path):
project = discovery_project(tmp_path)
repo = project.repositories[0].path
(repo / "webui").mkdir()
before = environment._native_discovery_fingerprint(tmp_path, project)
for directory in (repo, repo / "src", repo / "webui"):
(directory / "temporary-build-output").write_text("changed")
(directory / "temporary-build-directory").mkdir()
assert environment._native_discovery_fingerprint(tmp_path, project) == before
(repo / "webui" / "temporary-build-output").unlink()
assert environment._native_discovery_fingerprint(tmp_path, project) == before
def test_native_shape_detects_unknown_sibling_and_source_addition_removal(tmp_path):
project = discovery_project(tmp_path)
baseline = environment._native_discovery_fingerprint(tmp_path, project)
sibling = tmp_path / "govoplan-unregistered"
sibling.mkdir()
empty = environment._native_discovery_fingerprint(tmp_path, project)
assert empty != baseline
for name in ("src", "webui"):
(sibling / name).mkdir()
assert environment._native_discovery_fingerprint(tmp_path, project) != empty
(sibling / name).rmdir()
assert environment._native_discovery_fingerprint(tmp_path, project) == empty
sibling.rmdir()
assert environment._native_discovery_fingerprint(tmp_path, project) == baseline
@pytest.mark.parametrize("repo_path", ["govoplan-backend", "nonstandard-layout"])
def test_native_shape_detects_registered_backend_gaining_webui(tmp_path, repo_path):
project = discovery_project(tmp_path, path=repo_path)
baseline = environment._native_discovery_fingerprint(tmp_path, project)
webui = project.repositories[0].path / "webui"
webui.mkdir()
assert environment._native_discovery_fingerprint(tmp_path, project) != baseline
webui.rmdir()
assert environment._native_discovery_fingerprint(tmp_path, project) == baseline
def test_native_shape_binds_registered_ownership_names(tmp_path):
project = discovery_project(tmp_path, path="nonstandard-layout")
renamed = Project(
project.name,
(Repository("govoplan-renamed", project.repositories[0].path),),
project.config,
)
assert environment._native_discovery_fingerprint(
tmp_path, project
) != environment._native_discovery_fingerprint(tmp_path, renamed)
def test_native_shape_detects_source_link_retarget_and_dangling_target(tmp_path):
project = discovery_project(tmp_path)
first, second = tmp_path / "first", tmp_path / "second"
first.mkdir()
second.mkdir()
link = project.repositories[0].path / "webui"
link.symlink_to(first, target_is_directory=True)
baseline = environment._native_discovery_fingerprint(tmp_path, project)
link.unlink()
link.symlink_to(second, target_is_directory=True)
changed = environment._native_discovery_fingerprint(tmp_path, project)
assert changed != baseline
second.rmdir()
assert environment._native_discovery_fingerprint(tmp_path, project) != changed
def test_native_shape_encodes_non_directory_source_entries(tmp_path):
project = discovery_project(tmp_path)
webui = project.repositories[0].path / "webui"
baseline = environment._native_discovery_fingerprint(tmp_path, project)
webui.write_text("not a directory")
file_shape = environment._native_discovery_fingerprint(tmp_path, project)
assert file_shape != baseline
webui.unlink()
webui.mkdir()
assert environment._native_discovery_fingerprint(tmp_path, project) != file_shape
def test_native_shape_audit_is_bounded_including_nonmatching_children(
tmp_path, monkeypatch
):
project = discovery_project(tmp_path)
(tmp_path / "unrelated-one").mkdir()
(tmp_path / "unrelated-two").mkdir()
monkeypatch.setattr(environment, "MAX_DISCOVERY_CHILDREN", 2)
with pytest.raises(ValueError, match="bounded ownership"):
environment._native_discovery_fingerprint(tmp_path, project)
def test_native_shape_resolution_loop_fails_closed(tmp_path):
project = discovery_project(tmp_path)
link = project.repositories[0].path / "webui"
link.symlink_to(link)
with pytest.raises(ValueError, match="discovery cannot be resolved"):
environment._native_discovery_fingerprint(tmp_path, project)
@pytest.mark.parametrize("portable", [False, True])
def test_only_native_environment_identity_binds_discovery_shape(tmp_path, portable):
project = discovery_project(tmp_path)
if portable:
project.config["schema_version"] = 1
executable = tmp_path / "fixture-python"
executable.write_text("not executed")
tools = {"python": str(executable)}
with (
patch.object(environment, "tool_version", return_value="fixture"),
patch.object(
environment,
"require_capture",
return_value=SimpleNamespace(returncode=0, stdout=b"[]"),
),
):
baseline = environment.environment_fingerprint(tmp_path, project, tools, {})
(tmp_path / "govoplan-new" / "src").mkdir(parents=True)
changed = environment.environment_fingerprint(tmp_path, project, tools, {})
assert (baseline == changed) is portable