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.
379 lines
14 KiB
Python
Executable File
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
|