Merge current PR 14 into distributed worker pool

# Conflicts:
#	testing/tests/test_hermes_chat_quality.py
#	testing/tests/test_hermes_cli_lanes.py
This commit is contained in:
Hermes Agent 2026-08-17 12:55:52 +00:00
commit 7c87285940
19 changed files with 2886 additions and 48 deletions

View File

@ -23,7 +23,7 @@ from gitea_api import (
read_token, read_token,
) )
from gitea_api_policy import _reject_forbidden, _validate_ref, _validate_repo from gitea_api_policy import _reject_forbidden, _validate_ref, _validate_repo
from scm_broker_io import spool_response from scm_broker_io import RejectRedirect, response_status as _status, spool_response
from scm_broker_server import AbsoluteHeaderDeadlineMixin, BoundedThreadingHTTPServer from scm_broker_server import AbsoluteHeaderDeadlineMixin, BoundedThreadingHTTPServer
BROKER_PORT = 9081 BROKER_PORT = 9081
@ -49,22 +49,9 @@ FEATURE_REF_RE = re.compile(
) )
class RejectRedirect(urllib.request.HTTPRedirectHandler):
"""Reject every upstream redirect before credentials can be forwarded."""
def redirect_request(self, req, fp, code, msg, headers, newurl):
raise PolicyError("SCM upstream redirects are not allowed")
UPSTREAM_OPENER = urllib.request.build_opener(RejectRedirect()) UPSTREAM_OPENER = urllib.request.build_opener(RejectRedirect())
def _status(response: object) -> int | None:
value = getattr(response, "status", None)
if value is None and hasattr(response, "getcode"):
value = response.getcode() # type: ignore[attr-defined]
return value
def _read_bounded( def _read_bounded(
stream, stream,
maximum: int, maximum: int,

View File

@ -5,11 +5,27 @@ from __future__ import annotations
import tempfile import tempfile
import time import time
import urllib.request
from typing import BinaryIO from typing import BinaryIO
from gitea_api_policy import PolicyError from gitea_api_policy import PolicyError
class RejectRedirect(urllib.request.HTTPRedirectHandler):
"""Reject every upstream redirect before credentials can be forwarded."""
def redirect_request(self, req, fp, code, msg, headers, newurl):
raise PolicyError("SCM upstream redirects are not allowed")
def response_status(response: object) -> int | None:
"""Read an HTTP status from either urllib response interface."""
value = getattr(response, "status", None)
if value is None and hasattr(response, "getcode"):
value = response.getcode() # type: ignore[attr-defined]
return value
def spool_response( def spool_response(
response, response,
maximum: int, maximum: int,
@ -20,7 +36,9 @@ def spool_response(
deadline_seconds: float, deadline_seconds: float,
) -> tuple[BinaryIO, int]: ) -> tuple[BinaryIO, int]:
"""Spool an upstream response to disk while scanning chunk boundaries.""" """Spool an upstream response to disk while scanning chunk boundaries."""
spool = tempfile.SpooledTemporaryFile(max_size=memory_limit, dir="/tmp") spool = tempfile.SpooledTemporaryFile( # noqa: SIM115 - caller owns returned spool
max_size=memory_limit, dir="/tmp"
)
carry = b"" carry = b""
total = 0 total = 0
deadline = time.monotonic() + deadline_seconds deadline = time.monotonic() + deadline_seconds

View File

@ -11,6 +11,7 @@ import re
import shlex import shlex
import stat import stat
import struct import struct
from contextlib import suppress
from pathlib import Path from pathlib import Path
from node_account_audit import audit_membership, audit_privilege_policies from node_account_audit import audit_membership, audit_privilege_policies
@ -54,7 +55,9 @@ ACL_ENTRY = struct.Struct("<HHI")
ACL_XATTR = "system.posix_acl_access" ACL_XATTR = "system.posix_acl_access"
def _read_regular(path: Path, maximum: int = MAX_ACCOUNT_FILE) -> tuple[bytes, os.stat_result]: def _read_regular(
path: Path, maximum: int = MAX_ACCOUNT_FILE
) -> tuple[bytes, os.stat_result]:
snapshot = read_regular(path, maximum) snapshot = read_regular(path, maximum)
return snapshot.value, path.stat(follow_symlinks=False) return snapshot.value, path.stat(follow_symlinks=False)
@ -162,7 +165,7 @@ def _reconcile_databases() -> None:
records = _records(_read_regular(path)[0], fields, name) records = _records(_read_regular(path)[0], fields, name)
if expected[name] not in records: if expected[name] not in records:
raise HardeningError("dedicated Hermes account validation failed") raise HardeningError("dedicated Hermes account validation failed")
except Exception: except Exception as error:
for path, snapshot, updated in reversed(written): for path, snapshot, updated in reversed(written):
current = read_regular(path, MAX_ACCOUNT_FILE) current = read_regular(path, MAX_ACCOUNT_FILE)
if ( if (
@ -174,7 +177,7 @@ def _reconcile_databases() -> None:
): ):
raise HardeningError( raise HardeningError(
f"concurrent host account change prevents rollback: {path.name}" f"concurrent host account change prevents rollback: {path.name}"
) ) from error
atomic_write(path, snapshot.value, snapshot) atomic_write(path, snapshot.value, snapshot)
raise raise
@ -192,7 +195,9 @@ def _key_identity(line: bytes) -> tuple[str, bytes] | None:
fields = shlex.split(stripped.decode("ascii"), posix=True) fields = shlex.split(stripped.decode("ascii"), posix=True)
except (UnicodeDecodeError, ValueError) as exc: except (UnicodeDecodeError, ValueError) as exc:
raise HardeningError("authorized key line is malformed") from exc raise HardeningError("authorized key line is malformed") from exc
indexes = [index for index, field in enumerate(fields) if KEY_TYPE_RE.fullmatch(field)] indexes = [
index for index, field in enumerate(fields) if KEY_TYPE_RE.fullmatch(field)
]
if len(indexes) != 1 or indexes[0] + 1 >= len(fields): if len(indexes) != 1 or indexes[0] + 1 >= len(fields):
raise HardeningError("authorized key line has ambiguous key material") raise HardeningError("authorized key line has ambiguous key material")
key_type = fields[indexes[0]] key_type = fields[indexes[0]]
@ -226,10 +231,8 @@ def _validated_public_key(path: Path) -> tuple[bytes, tuple[str, bytes]]:
def _directory(path: Path, *, mode: int, uid: int, gid: int) -> None: def _directory(path: Path, *, mode: int, uid: int, gid: int) -> None:
try: with suppress(FileExistsError):
path.mkdir(mode=mode) path.mkdir(mode=mode)
except FileExistsError:
pass
metadata = path.lstat() metadata = path.lstat()
if not stat.S_ISDIR(metadata.st_mode) or stat.S_ISLNK(metadata.st_mode): if not stat.S_ISDIR(metadata.st_mode) or stat.S_ISLNK(metadata.st_mode):
raise HardeningError(f"unsafe account directory: {path.name}") raise HardeningError(f"unsafe account directory: {path.name}")

View File

@ -20,6 +20,13 @@
"ci/scripts/supply_chain_report.py", "ci/scripts/supply_chain_report.py",
"services/mailu/scripts/mailu_sync.py", "services/mailu/scripts/mailu_sync.py",
"services/mailu/scripts/mailu_sync_listener.py", "services/mailu/scripts/mailu_sync_listener.py",
"services/gitea/scripts/gitea_branch_protection_check.py",
"services/hermes/scm-common/scripts/gitea_api.py",
"services/hermes/scm-common/scripts/gitea_api_policy.py",
"services/hermes/scm-common/scripts/scm_broker.py",
"services/hermes/scm-common/scripts/scm_broker_client.py",
"services/hermes/scm-common/scripts/scm_broker_io.py",
"services/hermes/scm-common/scripts/scm_broker_server.py",
"services/hermes/scripts/execution_pool_client.py", "services/hermes/scripts/execution_pool_client.py",
"services/hermes/scripts/execution_pool_coordinator.py", "services/hermes/scripts/execution_pool_coordinator.py",
"services/hermes/scripts/execution_pool_project.py", "services/hermes/scripts/execution_pool_project.py",
@ -27,6 +34,11 @@
"services/hermes/scripts/execution_pool_scm.py", "services/hermes/scripts/execution_pool_scm.py",
"services/hermes/scripts/execution_pool_server.py", "services/hermes/scripts/execution_pool_server.py",
"services/hermes/scripts/execution_pool_worker.py", "services/hermes/scripts/execution_pool_worker.py",
"services/hermes/scripts/hermes_coordinator.py",
"services/hermes/scripts/node_account_audit.py",
"services/hermes/scripts/node_account_hardening.py",
"services/hermes/scripts/node_account_io.py",
"services/hermes/scripts/stage_runtime_access.py",
"testing/__init__.py", "testing/__init__.py",
"testing/quality_contract.py", "testing/quality_contract.py",
"testing/quality_docs.py", "testing/quality_docs.py",
@ -50,6 +62,8 @@
"services/comms/scripts/tests", "services/comms/scripts/tests",
"services/mailu/scripts/mailu_sync.py", "services/mailu/scripts/mailu_sync.py",
"services/mailu/scripts/mailu_sync_listener.py", "services/mailu/scripts/mailu_sync_listener.py",
"services/gitea/scripts/gitea_branch_protection_check.py",
"services/hermes/scm-common/scripts",
"services/hermes/scripts/execution_pool_client.py", "services/hermes/scripts/execution_pool_client.py",
"services/hermes/scripts/execution_pool_coordinator.py", "services/hermes/scripts/execution_pool_coordinator.py",
"services/hermes/scripts/execution_pool_project.py", "services/hermes/scripts/execution_pool_project.py",
@ -57,6 +71,11 @@
"services/hermes/scripts/execution_pool_scm.py", "services/hermes/scripts/execution_pool_scm.py",
"services/hermes/scripts/execution_pool_server.py", "services/hermes/scripts/execution_pool_server.py",
"services/hermes/scripts/execution_pool_worker.py", "services/hermes/scripts/execution_pool_worker.py",
"services/hermes/scripts/hermes_coordinator.py",
"services/hermes/scripts/node_account_audit.py",
"services/hermes/scripts/node_account_hardening.py",
"services/hermes/scripts/node_account_io.py",
"services/hermes/scripts/stage_runtime_access.py",
"testing/tests", "testing/tests",
"testing" "testing"
], ],
@ -72,6 +91,8 @@
"coverage_sources": [ "coverage_sources": [
"ci/scripts", "ci/scripts",
"scripts.render.dashboards_render_atlas", "scripts.render.dashboards_render_atlas",
"services/gitea/scripts",
"services/hermes/scm-common/scripts",
"services/hermes/scripts", "services/hermes/scripts",
"services/mailu/scripts", "services/mailu/scripts",
"testing" "testing"
@ -139,7 +160,14 @@
"ci/tests/**/*.py", "ci/tests/**/*.py",
"scripts/tests/**/*.py", "scripts/tests/**/*.py",
"services/*/scripts/tests/**/*.py", "services/*/scripts/tests/**/*.py",
"services/gitea/scripts/gitea_branch_protection_check.py",
"services/hermes/scm-common/scripts/*.py",
"services/hermes/scripts/execution_pool_*.py", "services/hermes/scripts/execution_pool_*.py",
"services/hermes/scripts/hermes_coordinator.py",
"services/hermes/scripts/node_account_audit.py",
"services/hermes/scripts/node_account_hardening.py",
"services/hermes/scripts/node_account_io.py",
"services/hermes/scripts/stage_runtime_access.py",
"services/mailu/scripts/mailu_sync.py", "services/mailu/scripts/mailu_sync.py",
"services/mailu/scripts/mailu_sync_listener.py" "services/mailu/scripts/mailu_sync_listener.py"
], ],
@ -183,13 +211,20 @@
}, },
"coverage": { "coverage": {
"minimum_percent": 95.0, "minimum_percent": 95.0,
"tracked_files": [ "minimum_branch_percent": 95.0,
"ci/scripts/publish_test_metrics.py", "branch_tracked_files": [
"ci/scripts/publish_test_metrics_quality.py", "services/gitea/scripts/gitea_branch_protection_check.py",
"ci/scripts/semgrep_report.py", "services/hermes/scm-common/scripts/gitea_api.py",
"ci/scripts/supply_chain_report.py", "services/hermes/scm-common/scripts/gitea_api_policy.py",
"services/mailu/scripts/mailu_sync.py", "services/hermes/scm-common/scripts/scm_broker.py",
"services/mailu/scripts/mailu_sync_listener.py", "services/hermes/scm-common/scripts/scm_broker_client.py",
"services/hermes/scm-common/scripts/scm_broker_io.py",
"services/hermes/scm-common/scripts/scm_broker_server.py",
"services/hermes/scripts/hermes_coordinator.py",
"services/hermes/scripts/node_account_audit.py",
"services/hermes/scripts/node_account_hardening.py",
"services/hermes/scripts/node_account_io.py",
"services/hermes/scripts/stage_runtime_access.py",
"services/hermes/scripts/execution_pool_client.py", "services/hermes/scripts/execution_pool_client.py",
"services/hermes/scripts/execution_pool_coordinator.py", "services/hermes/scripts/execution_pool_coordinator.py",
"services/hermes/scripts/execution_pool_project.py", "services/hermes/scripts/execution_pool_project.py",
@ -197,6 +232,34 @@
"services/hermes/scripts/execution_pool_scm.py", "services/hermes/scripts/execution_pool_scm.py",
"services/hermes/scripts/execution_pool_server.py", "services/hermes/scripts/execution_pool_server.py",
"services/hermes/scripts/execution_pool_worker.py", "services/hermes/scripts/execution_pool_worker.py",
"testing/quality_coverage.py"
],
"tracked_files": [
"ci/scripts/publish_test_metrics.py",
"ci/scripts/publish_test_metrics_quality.py",
"ci/scripts/semgrep_report.py",
"ci/scripts/supply_chain_report.py",
"services/mailu/scripts/mailu_sync.py",
"services/mailu/scripts/mailu_sync_listener.py",
"services/gitea/scripts/gitea_branch_protection_check.py",
"services/hermes/scm-common/scripts/gitea_api.py",
"services/hermes/scm-common/scripts/gitea_api_policy.py",
"services/hermes/scm-common/scripts/scm_broker.py",
"services/hermes/scm-common/scripts/scm_broker_client.py",
"services/hermes/scm-common/scripts/scm_broker_io.py",
"services/hermes/scm-common/scripts/scm_broker_server.py",
"services/hermes/scripts/execution_pool_client.py",
"services/hermes/scripts/execution_pool_coordinator.py",
"services/hermes/scripts/execution_pool_project.py",
"services/hermes/scripts/execution_pool_protocol.py",
"services/hermes/scripts/execution_pool_scm.py",
"services/hermes/scripts/execution_pool_server.py",
"services/hermes/scripts/execution_pool_worker.py",
"services/hermes/scripts/hermes_coordinator.py",
"services/hermes/scripts/node_account_audit.py",
"services/hermes/scripts/node_account_hardening.py",
"services/hermes/scripts/node_account_io.py",
"services/hermes/scripts/stage_runtime_access.py",
"testing/quality_contract.py", "testing/quality_contract.py",
"testing/quality_docs.py", "testing/quality_docs.py",
"testing/quality_hygiene.py", "testing/quality_hygiene.py",

View File

@ -3,23 +3,31 @@
from __future__ import annotations from __future__ import annotations
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
def _load_percentages(xml_path: Path, root: Path) -> dict[str, float]: @dataclass(frozen=True)
"""Load per-file line-rate percentages from a Cobertura XML report.""" class CoverageRates:
"""Line and branch percentages reported for one source file."""
line: float
branch: float | None
def _load_percentages(xml_path: Path, root: Path) -> dict[str, CoverageRates]:
"""Load per-file line and branch percentages from a Cobertura report."""
tree = ET.parse(xml_path) tree = ET.parse(xml_path)
xml_root = tree.getroot() xml_root = tree.getroot()
source_roots = [ source_roots = [
Path(node.text) Path(node.text) for node in xml_root.findall("./sources/source") if node.text
for node in xml_root.findall("./sources/source")
if node.text
] ]
percentages: dict[str, float] = {} percentages: dict[str, CoverageRates] = {}
for class_node in xml_root.findall(".//class"): for class_node in xml_root.findall(".//class"):
filename = class_node.attrib.get("filename") filename = class_node.attrib.get("filename")
line_rate = class_node.attrib.get("line-rate") line_rate = class_node.attrib.get("line-rate")
branch_rate = class_node.attrib.get("branch-rate")
if not filename or line_rate is None: if not filename or line_rate is None:
continue continue
normalized = filename.replace("\\", "/") normalized = filename.replace("\\", "/")
@ -32,7 +40,10 @@ def _load_percentages(xml_path: Path, root: Path) -> dict[str, float]:
if candidate.exists(): if candidate.exists():
key = candidate.relative_to(root).as_posix() key = candidate.relative_to(root).as_posix()
break break
percentages[key] = float(line_rate) * 100.0 percentages[key] = CoverageRates(
line=float(line_rate) * 100.0,
branch=float(branch_rate) * 100.0 if branch_rate is not None else None,
)
return percentages return percentages
@ -47,17 +58,39 @@ def run_check(contract: dict[str, Any], root: Path, xml_path: Path) -> list[str]
percentages = _load_percentages(xml_path, root) percentages = _load_percentages(xml_path, root)
minimum = float(contract.get("coverage", {}).get("minimum_percent", 95.0)) minimum = float(contract.get("coverage", {}).get("minimum_percent", 95.0))
coverage_contract = contract.get("coverage", {})
branch_minimum = coverage_contract.get("minimum_branch_percent")
if branch_minimum is not None:
branch_minimum = float(branch_minimum)
branch_tracked = {
path.replace("\\", "/")
for path in coverage_contract.get(
"branch_tracked_files",
coverage_contract.get("tracked_files", [])
if branch_minimum is not None
else [],
)
}
issues: list[str] = [] issues: list[str] = []
for relative_path in contract.get("coverage", {}).get("tracked_files", []): for relative_path in contract.get("coverage", {}).get("tracked_files", []):
normalized = relative_path.replace("\\", "/") normalized = relative_path.replace("\\", "/")
percent = percentages.get(normalized) rates = percentages.get(normalized)
if percent is None: if rates is None:
issues.append(f"coverage missing for tracked file: {relative_path}") issues.append(f"coverage missing for tracked file: {relative_path}")
continue continue
if percent + 1e-9 < minimum: if rates.line + 1e-9 < minimum:
issues.append( issues.append(
f"coverage below {minimum:.1f}%: {relative_path} ({percent:.1f}%)" f"coverage below {minimum:.1f}%: {relative_path} ({rates.line:.1f}%)"
)
if branch_minimum is None or normalized not in branch_tracked:
continue
if rates.branch is None:
issues.append(f"branch coverage missing for tracked file: {relative_path}")
elif rates.branch + 1e-9 < branch_minimum:
issues.append(
"branch coverage below "
f"{branch_minimum:.1f}%: {relative_path} ({rates.branch:.1f}%)"
) )
return issues return issues
@ -77,9 +110,9 @@ def compute_workspace_line_coverage(
samples: list[float] = [] samples: list[float] = []
for relative_path in contract.get("coverage", {}).get("tracked_files", []): for relative_path in contract.get("coverage", {}).get("tracked_files", []):
normalized = relative_path.replace("\\", "/") normalized = relative_path.replace("\\", "/")
percent = percentages.get(normalized) rates = percentages.get(normalized)
if percent is not None: if rates is not None:
samples.append(percent) samples.append(rates.line)
if not samples: if not samples:
return 0.0 return 0.0
return round(sum(samples) / len(samples), 3) return round(sum(samples) / len(samples), 3)

View File

@ -123,6 +123,9 @@ def test_cassandra_sync_repairs_origin_through_broker_without_token(
tmp_path: Path, monkeypatch tmp_path: Path, monkeypatch
): ):
"""Repository sync uses the credential-isolated broker remote.""" """Repository sync uses the credential-isolated broker remote."""
monkeypatch.setenv(
"HERMES_GITEA_TOKEN_FILE", str(tmp_path / "explicitly-unavailable-token")
)
workspace = tmp_path / "cassandra" workspace = tmp_path / "cassandra"
(workspace / ".git").mkdir(parents=True) (workspace / ".git").mkdir(parents=True)
monkeypatch.setattr(coordinator, "CASSANDRA_BASE_PATH", workspace) monkeypatch.setattr(coordinator, "CASSANDRA_BASE_PATH", workspace)

View File

@ -0,0 +1,267 @@
"""Behavioral branch coverage for Hermes project coordination."""
from __future__ import annotations
import json
import subprocess
import sys
import types
from contextlib import nullcontext
from pathlib import Path
import pytest
from testing.tests.test_hermes_coordinator_support import coordinator, routing
def test_workspace_falls_back_when_unset_or_invalid(tmp_path: Path, monkeypatch):
monkeypatch.delenv("HERMES_CASSANDRA_ACTIVE_WORKTREE", raising=False)
assert coordinator.cassandra_workspace() == coordinator.CASSANDRA_BASE_PATH
monkeypatch.setenv("HERMES_CASSANDRA_ACTIVE_WORKTREE", str(tmp_path / "missing"))
assert coordinator.cassandra_workspace() == coordinator.CASSANDRA_BASE_PATH
def test_task_migration_is_noop_for_base_workspace():
class Board:
@staticmethod
def connect_closing(**_kwargs):
raise AssertionError("base workspace must not access Kanban")
coordinator._migrate_open_cassandra_tasks(Board, coordinator.CASSANDRA_BASE_PATH)
class _Project:
id = "project-id"
def _fake_hermes_modules(monkeypatch, *, existing: bool, active: bool):
events = []
kb = types.ModuleType("hermes_cli.kanban_db")
kb.board_exists = lambda slug: existing
kb.create_board = lambda *args, **kwargs: events.append(
("create-board", args, kwargs)
)
kb.set_current_board = lambda slug: events.append(("current", slug))
kb.connect_closing = lambda **kwargs: nullcontext(object())
kb.list_tasks = lambda *_a, **_k: []
pdb = types.ModuleType("hermes_cli.projects_db")
pdb.connect_closing = lambda: nullcontext(object())
pdb.get_project = lambda *_a: _Project() if existing else None
pdb.get_active_id = lambda *_a: "active" if active else None
pdb.create_project = (
lambda *args, **kwargs: events.append(("create-project", kwargs)) or "new-id"
)
pdb.set_active = lambda *args: events.append(("active", args[-1]))
pdb.update_project = lambda *args, **kwargs: events.append(("update", kwargs))
pdb.add_folder = lambda *args, **kwargs: events.append(("folder", args[-1], kwargs))
package = types.ModuleType("hermes_cli")
package.kanban_db = kb
package.projects_db = pdb
monkeypatch.setitem(sys.modules, "hermes_cli", package)
monkeypatch.setitem(sys.modules, "hermes_cli.kanban_db", kb)
monkeypatch.setitem(sys.modules, "hermes_cli.projects_db", pdb)
return events
@pytest.mark.parametrize(
("existing", "active", "expected"),
[
(False, False, {"current", "create-project", "active"}),
(False, True, {"current", "create-project"}),
(True, True, {"update", "folder"}),
],
)
def test_bootstrap_cassandra_creates_or_updates_state(
tmp_path: Path, monkeypatch, existing, active, expected
):
events = _fake_hermes_modules(monkeypatch, existing=existing, active=active)
base = tmp_path / "projects/cassandra"
worktree = tmp_path / "projects/cassandra-worktree"
worktree.mkdir(parents=True)
(worktree / ".git").write_text("gitdir: elsewhere\n", encoding="utf-8")
monkeypatch.setattr(coordinator, "CASSANDRA_BASE_PATH", base)
monkeypatch.setenv("HERMES_CASSANDRA_ACTIVE_WORKTREE", str(worktree))
coordinator.bootstrap_cassandra(tmp_path / "hermes")
names = {event[0] for event in events}
assert expected <= names
assert "create-board" in names
def test_bootstrap_state_ready_path(tmp_path: Path, monkeypatch):
monkeypatch.setattr(coordinator, "bootstrap_cassandra", lambda _root: None)
assert coordinator.bootstrap_cassandra_state(tmp_path) == {"state": "ready"}
def test_corruption_type_imports_pinned_exception(monkeypatch):
class Expected(Exception):
pass
package = types.ModuleType("hermes_cli")
kanban = types.ModuleType("hermes_cli.kanban_db")
kanban.KanbanDbCorruptError = Expected
package.kanban_db = kanban
monkeypatch.setitem(sys.modules, "hermes_cli", package)
monkeypatch.setitem(sys.modules, "hermes_cli.kanban_db", kanban)
assert coordinator._kanban_corruption_type() is Expected
def test_repo_sync_reports_missing_git_and_unmanaged_directory(
tmp_path: Path, monkeypatch
):
monkeypatch.setattr(coordinator.shutil, "which", lambda _name: None)
assert coordinator.sync_cassandra_repo({}) == "git-unavailable"
workspace = tmp_path / "cassandra"
workspace.mkdir()
(workspace / "user-file").write_text("preserve", encoding="utf-8")
monkeypatch.setattr(coordinator, "CASSANDRA_BASE_PATH", workspace)
monkeypatch.setattr(coordinator.shutil, "which", lambda _name: "/usr/bin/git")
assert coordinator.sync_cassandra_repo({}) == "unmanaged-nonempty-directory"
@pytest.mark.parametrize(
("failure", "expected"),
[
("get-url-error", "remote-repair-failed"),
("repair-exit", "remote-repair-failed-9"),
("repair-error", "remote-repair-failed"),
("fetch-exit", "sync-failed-8"),
("fetch-error", "sync-failed"),
],
)
def test_existing_repo_sync_reports_each_failure(
tmp_path: Path, monkeypatch, failure, expected
):
workspace = tmp_path / "cassandra"
(workspace / ".git").mkdir(parents=True)
monkeypatch.setattr(coordinator, "CASSANDRA_BASE_PATH", workspace)
monkeypatch.setattr(coordinator.shutil, "which", lambda _name: "/usr/bin/git")
def run(command, **_kwargs):
action = command[-2:] if len(command) >= 2 else []
if action == ["get-url", "origin"]:
if failure == "get-url-error":
raise OSError("git unavailable")
return subprocess.CompletedProcess(command, 0, stdout="old\n")
if "set-url" in command:
if failure == "repair-error":
raise subprocess.TimeoutExpired(command, 30)
return subprocess.CompletedProcess(
command, 9 if failure == "repair-exit" else 0
)
if failure == "fetch-error":
raise OSError("fetch unavailable")
return subprocess.CompletedProcess(command, 8 if failure == "fetch-exit" else 0)
monkeypatch.setattr(coordinator.subprocess, "run", run)
assert coordinator.sync_cassandra_repo({}) == expected
@pytest.mark.parametrize(
("exit_code", "expected"), [(0, "ready"), (7, "sync-failed-7")]
)
def test_empty_repo_clones_through_broker(
tmp_path: Path, monkeypatch, exit_code, expected
):
workspace = tmp_path / "cassandra"
monkeypatch.setattr(coordinator, "CASSANDRA_BASE_PATH", workspace)
monkeypatch.setattr(coordinator.shutil, "which", lambda _name: "/usr/bin/git")
seen = []
def run(command, **kwargs):
seen.append((command, kwargs["env"]))
return subprocess.CompletedProcess(command, exit_code)
monkeypatch.setattr(coordinator.subprocess, "run", run)
assert (
coordinator.sync_cassandra_repo(
{"API_SERVER_KEY": "not-forwarded", "SAFE": "yes"}
)
== expected
)
assert coordinator.CASSANDRA_REMOTE in seen[0][0]
assert seen[0][1]["SAFE"] == "yes"
assert "API_SERVER_KEY" not in seen[0][1]
def _catalog(provider):
return routing.Catalog(provider, ["model"], True, True, "connected")
def test_refresh_once_writes_complete_status(tmp_path: Path, monkeypatch):
monkeypatch.setattr(coordinator, "_read_env", lambda _path: {"SAFE": "yes"})
monkeypatch.setattr(coordinator, "discover_codex_models", lambda: _catalog("codex"))
monkeypatch.setattr(
coordinator, "discover_claude_models", lambda: _catalog("claude")
)
monkeypatch.setattr(
coordinator, "configure_routes", lambda *_a: {"coordinator": ["auto"]}
)
monkeypatch.setattr(
coordinator, "bootstrap_cassandra_state", lambda _root: {"state": "ready"}
)
monkeypatch.setattr(coordinator, "sync_cassandra_repo", lambda _env: "ready")
monkeypatch.setattr(
coordinator, "cassandra_workspace", lambda: tmp_path / "worktree"
)
written = []
monkeypatch.setattr(
coordinator, "_atomic_write", lambda path, value: written.append((path, value))
)
status = coordinator.refresh_once(tmp_path)
assert status["projects"]["cassandra"]["state"] == "ready"
assert json.loads(written[0][1])["providers"]["codex"]["connected"] is True
def test_main_once_success_and_failure(monkeypatch, capsys):
status = {"providers": {"codex": {"state": "ready"}}}
monkeypatch.setattr(sys, "argv", ["coordinator", "--once"])
monkeypatch.setattr(coordinator, "refresh_once", lambda _root: status)
assert coordinator.main() == 0
assert "codex=ready" in capsys.readouterr().out
monkeypatch.setattr(
coordinator,
"refresh_once",
lambda _root: (_ for _ in ()).throw(RuntimeError("failed")),
)
assert coordinator.main() == 1
assert "RuntimeError" in capsys.readouterr().out
@pytest.mark.parametrize(("interval", "expected"), [(1, 300), (400, 400)])
def test_main_loop_sleeps_with_floor(monkeypatch, interval, expected):
calls = iter(({"providers": {}}, KeyboardInterrupt()))
def refresh(_root):
value = next(calls)
if isinstance(value, BaseException):
raise value
return value
sleeps = []
monkeypatch.setattr(coordinator, "refresh_once", refresh)
monkeypatch.setattr(coordinator.time, "sleep", sleeps.append)
monkeypatch.setattr(
sys, "argv", ["coordinator", "--loop", "--interval", str(interval)]
)
with pytest.raises(KeyboardInterrupt):
coordinator.main()
assert sleeps == [expected]
def test_main_loop_recovers_from_refresh_failure(monkeypatch):
calls = iter((RuntimeError("temporary"), KeyboardInterrupt()))
def refresh(_root):
error = next(calls)
raise error
sleeps = []
monkeypatch.setattr(coordinator, "refresh_once", refresh)
monkeypatch.setattr(coordinator.time, "sleep", sleeps.append)
monkeypatch.setattr(sys, "argv", ["coordinator", "--loop", "--interval", "300"])
with pytest.raises(KeyboardInterrupt):
coordinator.main()
assert sleeps == [300]

View File

@ -0,0 +1,183 @@
"""Behavioral branch coverage for the Gitea protection policy checker."""
from __future__ import annotations
import json
import os
import stat
import sys
from pathlib import Path
import pytest
from testing.tests.test_hermes_scm_broker_support import ROOT, _load_path
def _module(name: str = "branch_protection_coverage"):
return _load_path(
name,
ROOT / "services/gitea/scripts/gitea_branch_protection_check.py",
)
def _rule(module, name: str = "main", **updates):
value = {
"rule_name": name,
"priority": 1,
"created_at": "2026-01-01T00:00:00Z",
**module._required("bstein"),
}
value.update(updates)
return value
def test_read_bounded_accepts_regular_file_and_closes_descriptor(
tmp_path: Path, monkeypatch
):
module = _module("branch_read_regular")
source = tmp_path / "rules.json"
source.write_bytes(b"[]")
closed: list[int] = []
real_close = os.close
def close(descriptor: int) -> None:
closed.append(descriptor)
real_close(descriptor)
monkeypatch.setattr(module.os, "close", close)
assert module._read_bounded(source) == b"[]"
assert len(closed) == 1
@pytest.mark.parametrize("kind", ["directory", "oversized", "short"])
def test_read_bounded_rejects_unsafe_or_changed_input(
tmp_path: Path, monkeypatch, kind
):
module = _module(f"branch_read_{kind}")
source = tmp_path / "rules"
source.write_bytes(b"[]")
real_fstat = module.os.fstat
real_read = module.os.read
if kind == "directory":
monkeypatch.setattr(
module.os,
"fstat",
lambda descriptor: os.stat_result(
(stat.S_IFDIR | 0o700, 0, 0, 0, 0, 0, 2, 0, 0, 0)
),
)
elif kind == "oversized":
monkeypatch.setattr(
module.os,
"fstat",
lambda descriptor: os.stat_result(
(stat.S_IFREG | 0o600, 0, 0, 0, 0, 0, module.MAX_INPUT + 1, 0, 0, 0)
),
)
else:
monkeypatch.setattr(module.os, "fstat", real_fstat)
monkeypatch.setattr(module.os, "read", lambda descriptor, maximum: b"[")
with pytest.raises(module.PolicyError):
module._read_bounded(source)
monkeypatch.setattr(module.os, "read", real_read)
@pytest.mark.parametrize(
("pattern", "branch", "expected"),
[
("m?in", "main", True),
("m[ai]in", "main", True),
("m[!z]in", "main", True),
("m]ain", "m]ain", True),
("m}ain", "m}ain", True),
("m[a-z]in", "main", True),
("m{ain,aster}", "main", True),
("m\\ain", "main", True),
],
)
def test_glob_grammar_accepts_supported_constructs(pattern, branch, expected):
module = _module("branch_glob_supported")
assert module._matches(pattern, branch) is expected
@pytest.mark.parametrize(
"pattern",
[
"m\\",
"m[",
"m[]",
"m[!]",
"m[[a]",
"m[\\a]",
"m[-a]",
"m[a-]",
"m[a-b-c]",
"m[z-a]",
"m[aa-b]",
"m{ain}",
"m{ain,}",
"m{ain,aster",
],
)
def test_invalid_globs_are_literal_nonmatches(pattern):
module = _module("branch_glob_invalid")
assert module._matches(pattern, "main") is False
@pytest.mark.parametrize("pattern", ["", "x" * 256, "máin"])
def test_rule_names_are_ascii_and_bounded(pattern):
module = _module("branch_name_bounds")
with pytest.raises(module.PolicyError, match="rule name"):
module._matches(pattern, "main")
@pytest.mark.parametrize("value", [None, "x" * 65, "invalid", "2026-01-01T00:00:00"])
def test_created_at_requires_short_timezone_aware_iso(value):
module = _module("branch_created_bounds")
with pytest.raises(module.PolicyError, match="creation time"):
module._created(value)
@pytest.mark.parametrize(
"value",
[b"x" * (1024 * 1024 + 1), b"{}", json.dumps([None]).encode()],
)
def test_evaluate_rejects_invalid_top_level_inputs(value):
module = _module("branch_evaluate_top")
with pytest.raises((module.PolicyError, json.JSONDecodeError)):
module.evaluate(value, "main", "bstein")
def test_evaluate_rejects_unknown_branch_and_excess_rules():
module = _module("branch_evaluate_bounds")
with pytest.raises(module.PolicyError, match="input"):
module.evaluate(b"[]", "release", "bstein")
with pytest.raises(module.PolicyError, match="response"):
module.evaluate(json.dumps([{}] * 101).encode(), "main", "bstein")
@pytest.mark.parametrize("priority", [True, 0, 1_000_001, "1"])
def test_evaluate_rejects_ambiguous_priority_types(priority):
module = _module("branch_priority_bounds")
rule = _rule(module, priority=priority)
with pytest.raises(module.PolicyError, match="priority"):
module.evaluate(json.dumps([rule]).encode(), "main", "bstein")
def test_main_reports_success_and_failures(tmp_path: Path, monkeypatch, capsys):
module = _module("branch_main_paths")
source = tmp_path / "rules.json"
source.write_text(json.dumps([_rule(module)]), encoding="utf-8")
monkeypatch.setattr(sys, "argv", ["checker", str(source), "main", "bstein"])
assert module.main() == 0
assert capsys.readouterr().out.strip() == "PRESENT"
source.write_text("not-json", encoding="utf-8")
assert module.main() == 1
assert "branch protection check failed" in capsys.readouterr().err
monkeypatch.setattr(
module, "_read_bounded", lambda _path: (_ for _ in ()).throw(OSError())
)
assert module.main() == 1

View File

@ -0,0 +1,375 @@
"""Behavioral coverage for internal safe-Gitea client and policy paths."""
from __future__ import annotations
import json
import subprocess
import sys
import types
import urllib.error
import urllib.parse
from email.message import Message
from pathlib import Path
import pytest
from testing.tests.test_hermes_gitea_support import CLIENT_PATH, Response, _load
from testing.tests.test_hermes_scm_broker_support import _load_path
def _policy(name: str = "gitea_policy_coverage"):
return _load_path(name, CLIENT_PATH.parent / "gitea_api_policy.py")
def test_runtime_token_reader_and_fixed_origin(tmp_path: Path, monkeypatch):
module = _load()
token = tmp_path / "token"
token.write_text(" synthetic-runtime-value \n", encoding="utf-8")
assert module.read_token(token) == "synthetic-runtime-value"
token.write_text(" \n", encoding="utf-8")
with pytest.raises(ValueError, match="empty"):
module.read_token(token)
monkeypatch.setenv("GITEA_BASE_URL", module.CANONICAL_BASE_URL)
assert module.configured_base_url() == module.CANONICAL_BASE_URL
monkeypatch.setenv("GITEA_BASE_URL", "https://example.invalid")
with pytest.raises(module.PolicyError, match="fixed"):
module.configured_base_url()
def test_safe_urlopen_delegates_only_to_redirect_rejecting_opener(monkeypatch):
module = _load()
sentinel = object()
monkeypatch.setattr(
module._SAFE_OPENER,
"open",
lambda request, timeout: (request, timeout, sentinel),
)
request = object()
assert module._safe_urlopen(request, 7) == (request, 7, sentinel)
def test_api_target_and_request_body_edge_paths(monkeypatch):
module = _load()
with pytest.raises(module.PolicyError, match="path exceeds"):
module._split_api_path("/api/v1/" + "x" * 510)
with pytest.raises(module.PolicyError, match="request body"):
module.authorize_request(
"GET", "/api/v1/repos/atlas/cassandra", {"unexpected": True}
)
with pytest.raises(module.PolicyError, match="query"):
module.authorize_request(
"POST",
"/api/v1/repos/atlas/cassandra/pulls?page=1",
{},
)
with pytest.raises(module.PolicyError, match="only read"):
module.authorize_request("TRACE", "/api/v1/repos/atlas/cassandra/pulls", None)
with pytest.raises(module.PolicyError, match="accepts only"):
module.authorize_request("POST", "/api/v1/repos/atlas/cassandra/pulls", [])
def test_response_status_fallback_and_nested_fail_closed():
module = _load()
class GetCodeResponse(Response):
def __init__(self):
super().__init__({"name": "cassandra"}, status=200)
del self.status
def getcode(self):
return 200
assert (
module.read(
"/api/v1/repos/atlas/cassandra",
token="synthetic",
opener=lambda *_a, **_k: GetCodeResponse(),
)
== b'{"name": "cassandra"}'
)
with pytest.raises(module.PolicyError, match="omitted required"):
module._nested({"base": None}, "base", "ref")
@pytest.mark.parametrize("body", [b"not-json", b"[]"])
def test_create_response_requires_json_object(body: bytes):
module = _load()
with pytest.raises(module.PolicyError, match="invalid pull-request metadata"):
module._require_create_response(
body,
repo="cassandra",
base="main",
head="feature/test",
head_sha="a" * 40,
title="WIP: Test",
body="Evidence",
)
def test_write_body_handles_empty_newline_and_missing_newline(monkeypatch):
module = _load()
class Buffer:
value = bytearray()
@classmethod
def write(cls, value):
cls.value.extend(value)
monkeypatch.setattr(module.sys, "stdout", types.SimpleNamespace(buffer=Buffer))
module._write_body(b"")
module._write_body(b"one\n")
module._write_body(b"two")
assert bytes(Buffer.value) == b"one\ntwo\n"
def test_main_executes_broker_read_and_create_paths(monkeypatch):
module = _load()
outputs: list[bytes] = []
client = types.ModuleType("scm_broker_client")
client.read = lambda path: json.dumps({"path": path}).encode()
client.create_draft = lambda repo, **data: json.dumps(
{"repo": repo, **data}, sort_keys=True
).encode()
monkeypatch.setitem(sys.modules, "scm_broker_client", client)
monkeypatch.setattr(module, "_write_body", outputs.append)
assert module.main(["read", "/api/v1/repos/atlas/cassandra"]) == 0
assert json.loads(outputs.pop()) == {"path": "/api/v1/repos/atlas/cassandra"}
assert (
module.main(
[
"create-draft",
"cassandra",
"--base",
"main",
"--head",
"feature/coverage",
"--head-sha",
"a" * 40,
"--title",
"Coverage repair",
"--body",
"Review the focused tests",
]
)
== 0
)
assert json.loads(outputs.pop())["repo"] == "cassandra"
def test_main_handles_http_and_policy_failures(monkeypatch, capsys):
module = _load()
client = types.ModuleType("scm_broker_client")
def http_failure(_path):
headers = Message()
raise urllib.error.HTTPError("url", 503, "unavailable", headers, None)
client.read = http_failure
client.create_draft = lambda *_a, **_k: b"{}"
monkeypatch.setitem(sys.modules, "scm_broker_client", client)
assert module.main(["read", "/api/v1/repos/atlas/cassandra"]) == 1
assert "HTTP 503" in capsys.readouterr().err
client.read = lambda _path: (_ for _ in ()).throw(module.PolicyError("rejected"))
assert module.main(["read", "/not-allowed"]) == 1
assert "no credential" in capsys.readouterr().err
@pytest.mark.parametrize(
("function", "value"),
[
("_validate_repo", "."),
("_validate_repo", ".."),
("_validate_sha", "short"),
("_validate_pr_number", True),
("_validate_pr_number", 0),
("_validate_pr_number", 2_147_483_648),
],
)
def test_policy_scalar_validators_reject_ambiguous_values(function, value):
module = _policy("gitea_policy_scalar")
with pytest.raises(module.PolicyError):
getattr(module, function)(value)
def test_ref_bounds_cover_utf8_controls_git_failure_and_success(monkeypatch):
module = _policy("gitea_policy_refs")
with pytest.raises(module.PolicyError, match="valid UTF-8"):
module._validate_ref_bounds("\ud800", "head")
with pytest.raises(module.PolicyError, match="UTF-8"):
module._validate_ref_bounds("🧪" * 64, "head")
with pytest.raises(module.PolicyError, match="safe same"):
module._validate_ref("bad\nref", "head")
monkeypatch.setattr(
module.subprocess,
"run",
lambda *_a, **_k: subprocess.CompletedProcess([], 1),
)
with pytest.raises(module.PolicyError, match="same-repository"):
module._validate_ref("invalid-ref", "head")
monkeypatch.setattr(
module.subprocess,
"run",
lambda *_a, **_k: subprocess.CompletedProcess([], 0),
)
assert module._validate_ref("feature/valid", "head") == "feature/valid"
@pytest.mark.parametrize(
("value", "required", "match"),
[
(None, False, "must be text"),
(" ", True, "must not be empty"),
("\ud800", False, "valid UTF-8"),
("x\x00y", False, "safe request limit"),
],
)
def test_text_validation_rejects_nontext_empty_invalid_and_controls(
value, required, match
):
module = _policy("gitea_policy_text")
with pytest.raises(module.PolicyError, match=match):
module._validate_text(value, "field", 20, 20, required=required)
@pytest.mark.parametrize(
("key", "expected"),
[
("password", True),
("accountKey", True),
("registry-key", True),
("clientEmail", True),
("clientId", True),
("accessId", True),
("connectionString", True),
("dockerConfigJson", True),
("release_note", False),
],
)
def test_sensitive_key_semantics_cover_generic_forms(key, expected):
module = _policy("gitea_policy_keys")
assert module._is_sensitive_key(key) is expected
@pytest.mark.parametrize(
("value", "expected"),
[
("", False),
('"synthetic"', True),
("{encoded}", True),
("Bearer value-12345678", True),
("ghp_" + "a" * 24, True),
("https://example.invalid/value", True),
("name@example.invalid", True),
("${RUNTIME_VALUE}", True),
("AbCdEf0123456789", True),
("reject empty values", False),
("ordinary", True),
],
)
def test_assignment_value_shape_distinguishes_prose_from_credentials(value, expected):
module = _policy("gitea_policy_assignment_values")
assert module._looks_sensitive_assignment_value(value) is expected
@pytest.mark.parametrize(
("value", "expected"),
[
("single", False),
("add regression coverage", True),
("add regression_coverage", False),
("ordinary engineering prose", False),
],
)
def test_prose_classifier_covers_length_punctuation_and_lead_words(value, expected):
module = _policy("gitea_policy_prose")
assert module._looks_like_prose(value) is expected
def test_structured_json_walk_covers_lists_nonstring_keys_and_limits():
module = _policy("gitea_policy_json_walk")
assert module._json_value_has_sensitive_assignment({1: "ignored"}) is False
assert module._json_value_has_sensitive_assignment([{"release": "safe"}]) is False
assert (
module._json_value_has_sensitive_assignment({"client_secret": "value"}) is True
)
assert (
module._json_value_has_sensitive_assignment(
{"outer": {"client_secret": "value"}}
)
is True
)
assert (
module._json_value_has_sensitive_assignment({"type": "service-account"}) is True
)
with pytest.raises(module.PolicyError, match="scan limit"):
module._json_value_has_sensitive_assignment([], depth=33)
with pytest.raises(module.PolicyError, match="scan limit"):
module._json_value_has_sensitive_assignment([], nodes=[2048])
def test_json_key_decoder_and_embedded_document_scanner_cover_failures():
module = _policy("gitea_policy_json_decoder")
assert module._decode_json_key("client\\u005fsecret") == ("client_secret", True)
decoded, valid = module._decode_json_key("client\\qsecret")
assert valid is False and "client" in decoded
assert list(module._decoded_json_documents("prose only")) == []
assert list(module._decoded_json_documents("x {bad y [1, 2]")) == [[1, 2]]
with pytest.raises(module.PolicyError, match="scan limit"):
list(module._decoded_json_documents("{" * 33))
assert (
module._has_structured_sensitive_assignment('"client\\qsecret": value') is True
)
assert (
module._has_structured_sensitive_assignment("type:\n service-account") is True
)
def test_high_entropy_detector_covers_long_mixed_and_low_entropy_values():
module = _policy("gitea_policy_entropy")
assert module._has_high_entropy_token("A1_" * 100) is True
assert (
module._has_high_entropy_token(
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/_="
)
is True
)
assert module._has_high_entropy_token("a" * 64) is False
assert module._has_high_entropy_token("short prose") is False
@pytest.mark.parametrize(
"query",
[
"bad",
"a=1&b=2&c=3&d=4",
"page=1&page=2",
"=value",
"unexpected=1",
"page=abc",
"page=01",
"page=10001",
"limit=51",
"state=merged",
],
)
def test_query_validator_covers_each_rejection_class(query):
module = _policy("gitea_policy_query")
target = urllib.parse.urlsplit("/api/v1/repos/atlas/cassandra/pulls?" + query)
with pytest.raises(module.PolicyError):
module._validate_query(target, {"page", "limit", "state"})
def test_draft_title_rejects_prefix_without_content():
module = _policy("gitea_policy_empty_draft")
with pytest.raises(module.PolicyError, match="after the draft prefix"):
module._draft_title("WIP:")
def test_query_validator_rejects_noncanonical_raw_form():
module = _policy("gitea_policy_raw_query")
target = urllib.parse.SplitResult("", "", "/api/v1/repos/atlas/cassandra", "%", "")
with pytest.raises(module.PolicyError, match="canonical ASCII"):
module._validate_query(target, set())

View File

@ -0,0 +1,206 @@
"""Behavioral branch coverage for node-storage ACL hardening."""
from __future__ import annotations
import errno
import os
import pytest
from testing.tests.test_hermes_node_account_support import _fixture, _load
def test_acl_decoder_rejects_version_permissions_and_adds_missing_mask():
module = _load()
valid = [
(module.ACL_USER_OBJ, 7, module.ACL_UNDEFINED_ID),
(module.ACL_GROUP_OBJ, 5, module.ACL_UNDEFINED_ID),
(module.ACL_OTHER, 5, module.ACL_UNDEFINED_ID),
]
wrong_version = module.ACL_HEADER.pack(99) + b"".join(
module.ACL_ENTRY.pack(*entry) for entry in valid
)
with pytest.raises(module.HardeningError, match="version"):
module._decode_acl(wrong_version, 0o755)
bad_permission = module._encode_acl([*valid, (module.ACL_USER, 8, 1200)])
with pytest.raises(module.HardeningError, match="permission"):
module._decode_acl(bad_permission, 0o755)
updated = module._decode_acl(
module._acl_with_deny(module._encode_acl(valid), 0o755), 0o755
)
assert (module.ACL_MASK, 5, module.ACL_UNDEFINED_ID) in updated
assert (module.ACL_USER, 0, module.ACCOUNT_UID) in updated
def test_read_acl_handles_absence_and_propagates_other_errors(monkeypatch, tmp_path):
module = _load()
path = tmp_path / "root"
path.mkdir()
def missing(*_args, **_kwargs):
raise OSError(errno.ENODATA, "missing")
monkeypatch.setattr(module.os, "getxattr", missing)
assert module._read_acl(path) == b""
def denied(*_args, **_kwargs):
raise OSError(errno.EPERM, "denied")
monkeypatch.setattr(module.os, "getxattr", denied)
with pytest.raises(OSError):
module._read_acl(path)
@pytest.mark.parametrize("value", [b"", b"X", b"A", b"Nextra"])
def test_acl_backup_validator_rejects_malformed_encodings(value):
module = _load()
with pytest.raises(module.HardeningError, match="backup is malformed"):
module._validate_acl_backup(value)
module._validate_acl_backup(b"N")
valid = module._encode_acl(
[
(module.ACL_USER_OBJ, 7, module.ACL_UNDEFINED_ID),
(module.ACL_GROUP_OBJ, 5, module.ACL_UNDEFINED_ID),
(module.ACL_OTHER, 5, module.ACL_UNDEFINED_ID),
]
)
module._validate_acl_backup(b"A" + valid)
def test_acl_backup_reuses_valid_existing_copy(tmp_path, monkeypatch):
module = _load()
host_etc = tmp_path / "etc"
root = host_etc / "hermes-node-boundary"
root.mkdir(parents=True)
backup = root / "k3s.acl"
backup.write_bytes(b"N")
monkeypatch.setattr(module, "HOST_ETC", host_etc)
monkeypatch.setattr(module, "HOST_ROOT_UID", os.getuid())
monkeypatch.setattr(module, "HOST_ROOT_GID", os.getgid())
module._acl_backup_once(tmp_path / "k3s", b"")
assert backup.read_bytes() == b"N"
def test_acl_backup_short_write_cleans_temporary(tmp_path, monkeypatch):
module = _load()
host_etc = tmp_path / "etc"
host_etc.mkdir()
monkeypatch.setattr(module, "HOST_ETC", host_etc)
monkeypatch.setattr(module, "HOST_ROOT_UID", os.getuid())
monkeypatch.setattr(module, "HOST_ROOT_GID", os.getgid())
monkeypatch.setattr(module.os, "write", lambda _fd, _value: 0)
with pytest.raises(module.HardeningError, match="short sensitive"):
module._acl_backup_once(tmp_path / "k3s", b"")
assert not list((host_etc / "hermes-node-boundary").glob(".*.hermes-*"))
def test_acl_backup_tolerates_concurrent_first_writer(tmp_path, monkeypatch):
module = _load()
host_etc = tmp_path / "etc"
host_etc.mkdir()
monkeypatch.setattr(module, "HOST_ETC", host_etc)
monkeypatch.setattr(module, "HOST_ROOT_UID", os.getuid())
monkeypatch.setattr(module, "HOST_ROOT_GID", os.getgid())
real_link = os.link
def race(source, target, **kwargs):
real_link(source, target, **kwargs)
raise FileExistsError()
monkeypatch.setattr(module.os, "link", race)
module._acl_backup_once(tmp_path / "k3s", b"")
assert (host_etc / "hermes-node-boundary/k3s.acl").read_bytes() == b"N"
def test_sensitive_root_failure_restores_existing_acl(tmp_path, monkeypatch):
module = _load()
root = tmp_path / "k3s"
root.mkdir()
monkeypatch.setattr(module, "HOST_ROOT_UID", os.getuid())
current = module._encode_acl(
[
(module.ACL_USER_OBJ, 7, module.ACL_UNDEFINED_ID),
(module.ACL_GROUP_OBJ, 5, module.ACL_UNDEFINED_ID),
(module.ACL_MASK, 5, module.ACL_UNDEFINED_ID),
(module.ACL_OTHER, 5, module.ACL_UNDEFINED_ID),
]
)
reads = iter((current, current))
writes = []
monkeypatch.setattr(module, "_read_acl", lambda _path: next(reads))
monkeypatch.setattr(module, "_acl_backup_once", lambda *_a, **_k: None)
monkeypatch.setattr(
module.os, "setxattr", lambda *args, **kwargs: writes.append(args[2])
)
with pytest.raises(module.HardeningError, match="validation failed"):
module._deny_sensitive_root(root)
assert writes[-1] == current
@pytest.mark.parametrize("remove_errno", [errno.ENODATA, errno.EPERM])
def test_sensitive_root_failure_removes_new_acl_or_propagates_cleanup_error(
tmp_path, monkeypatch, remove_errno
):
module = _load()
root = tmp_path / "k3s"
root.mkdir()
monkeypatch.setattr(module, "HOST_ROOT_UID", os.getuid())
monkeypatch.setattr(module, "_read_acl", lambda _path: b"")
monkeypatch.setattr(module, "_acl_backup_once", lambda *_a, **_k: None)
monkeypatch.setattr(
module.os,
"setxattr",
lambda *_a, **_k: (_ for _ in ()).throw(RuntimeError("set failed")),
)
monkeypatch.setattr(
module.os,
"removexattr",
lambda *_a, **_k: (_ for _ in ()).throw(OSError(remove_errno, "remove")),
)
expected = OSError if remove_errno == errno.EPERM else RuntimeError
with pytest.raises(expected):
module._deny_sensitive_root(root)
def test_database_validation_failure_rolls_back_all_writes(tmp_path, monkeypatch):
module, originals, _key_value, _other, _public = _fixture(tmp_path, monkeypatch)
real_read = module._read_regular
calls = 0
def missing_expected(path, maximum=module.MAX_ACCOUNT_FILE):
nonlocal calls
calls += 1
if calls == 1:
return originals["passwd"].encode(), path.stat(follow_symlinks=False)
return real_read(path, maximum)
monkeypatch.setattr(module, "_read_regular", missing_expected)
with pytest.raises(module.HardeningError, match="validation failed"):
module._reconcile_databases()
for name, value in originals.items():
assert (module.HOST_ETC / name).read_text() == value
def test_preexisting_target_key_removal_is_verified(tmp_path, monkeypatch):
module, _originals, key, _other, public = _fixture(tmp_path, monkeypatch)
target = module.HOST_HOME / module.ACCOUNT / ".ssh/authorized_keys"
target.parent.mkdir(parents=True)
target.write_text(key + "\n", encoding="utf-8")
real_write = module.atomic_write
def ignore_preinstall_target(path, value, metadata):
if path == target and value == b"":
return
real_write(path, value, metadata)
monkeypatch.setattr(module, "atomic_write", ignore_preinstall_target)
with pytest.raises(module.HardeningError, match="preexisting Hermes authorization"):
module._move_key(public)
def test_existing_target_without_key_does_not_need_precleanup(tmp_path, monkeypatch):
module, _originals, _key_value, other, public = _fixture(tmp_path, monkeypatch)
target = module.HOST_HOME / module.ACCOUNT / ".ssh/authorized_keys"
target.parent.mkdir(parents=True)
target.write_text(other + "\n", encoding="utf-8")
module._move_key(public)
assert target.read_text().endswith("\n")

View File

@ -0,0 +1,221 @@
"""Behavioral branch coverage for dedicated node-account privilege audits."""
from __future__ import annotations
import os
import struct
import sys
from types import SimpleNamespace
import pytest
from testing.tests.test_hermes_node_account_support import _load
def _audit_module():
hardening = _load()
return sys.modules[hardening.audit_membership.__module__]
def test_member_parser_handles_empty_valid_malformed_and_duplicates():
module = _audit_module()
assert module._members("", "group") == set()
assert module._members("one,two", "group") == {"one", "two"}
for value in ("one,", " one", "one,one"):
with pytest.raises(module.HardeningError):
module._members(value, "group")
def test_membership_audit_accepts_unrelated_records_and_rejects_account():
module = _audit_module()
module.audit_membership(
"hermes-agent", [["disk", "x", "6", "atlas"]], [["disk", "!", "atlas", ""]]
)
with pytest.raises(module.HardeningError, match="supplementary"):
module.audit_membership(
"hermes-agent", [["disk", "x", "6", "hermes-agent"]], []
)
with pytest.raises(module.HardeningError, match="gshadow"):
module.audit_membership("hermes-agent", [], [["disk", "!", "hermes-agent", ""]])
@pytest.mark.parametrize("kind", ["mode", "symlink"])
def test_policy_files_reject_unsafe_directories(tmp_path, kind):
module = _audit_module()
host_etc = tmp_path / "etc"
host_etc.mkdir()
root = host_etc / "sudoers.d"
if kind == "mode":
root.mkdir(mode=0o777)
root.chmod(0o777)
else:
target = tmp_path / "target"
target.mkdir()
root.symlink_to(target)
with pytest.raises(module.HardeningError, match="unsafe sudo/polkit"):
module._policy_files(host_etc, tmp_path / "share", os.getuid())
def test_policy_file_count_is_bounded(tmp_path):
module = _audit_module()
root = tmp_path / "etc/sudoers.d"
root.mkdir(parents=True)
for index in range(257):
(root / str(index)).write_text("# safe\n", encoding="utf-8")
with pytest.raises(module.HardeningError, match="too many"):
module._policy_files(tmp_path / "etc", tmp_path / "share", os.getuid())
def _acl(*entries):
return struct.pack("<I", 2) + b"".join(
struct.pack("<HHI", *entry) for entry in entries
)
def test_policy_metadata_accepts_safe_acl_and_rejects_malformed_or_writable():
module = _audit_module()
safe = SimpleNamespace(
uid=0,
mode=0o600,
xattrs=(("system.posix_acl_access", _acl((1, 6, 0xFFFFFFFF))),),
)
module._audit_policy_metadata(safe, 0, 1200)
malformed = SimpleNamespace(
uid=0, mode=0o600, xattrs=(("system.posix_acl_access", b"bad"),)
)
with pytest.raises(module.HardeningError, match="ACL is malformed"):
module._audit_policy_metadata(malformed, 0, 1200)
writable = SimpleNamespace(
uid=0, mode=0o600, xattrs=(("system.posix_acl_access", _acl((2, 2, 1200))),)
)
with pytest.raises(module.HardeningError, match="grants Hermes write"):
module._audit_policy_metadata(writable, 0, 1200)
def test_active_sudo_policy_handles_blank_comments_numeric_and_includes():
module = _audit_module()
text = (
"\n# ordinary comment\n#1200 ALL=(ALL) ALL\n"
"#includedir /etc/sudoers.d\nroot ALL=(ALL) ALL\n"
)
active = module._active_sudo_policy(text)
assert "#1200" in active and "root ALL" in active
assert "ordinary" not in active and "includedir" not in active
with pytest.raises(module.HardeningError, match="unaudited"):
module._active_sudo_policy("@include /external/policy\n")
def test_identity_matcher_covers_account_numeric_and_unrelated_values():
module = _audit_module()
assert module._mentions_dedicated_identity("hermes-agent ALL", "hermes-agent", 1200)
assert module._mentions_dedicated_identity("#1200 ALL", "hermes-agent", 1200)
assert not module._mentions_dedicated_identity(
"hermes-agent-extra 12001", "hermes-agent", 1200
)
def _snapshot(value: bytes, *, mode=0o600, uid=0, xattrs=()):
return SimpleNamespace(value=value, mode=mode, uid=uid, xattrs=tuple(xattrs))
def test_privilege_audit_accepts_files_systemd_nss_and_comments(tmp_path, monkeypatch):
module = _audit_module()
host_etc = tmp_path / "etc"
host_etc.mkdir()
nss = host_etc / "nsswitch.conf"
nss.write_text(
"passwd: files systemd\nhosts: files dns\n# comment\n", encoding="utf-8"
)
sudoers = host_etc / "sudoers"
sudoers.write_text("# safe comment\nroot ALL=(ALL) ALL\n", encoding="utf-8")
monkeypatch.setattr(
module,
"read_regular",
lambda path, maximum: _snapshot(path.read_bytes(), uid=os.getuid()),
)
module.audit_privilege_policies(
"hermes-agent", 1200, os.getuid(), host_etc, tmp_path / "share"
)
@pytest.mark.parametrize(
("value", "match"),
[
(b"passwd: \n", "external group/account"),
(b"passwd: files\xff\n", "not UTF-8"),
],
)
def test_nsswitch_audit_rejects_empty_or_non_utf8_sources(
tmp_path, monkeypatch, value, match
):
module = _audit_module()
host_etc = tmp_path / "etc"
host_etc.mkdir()
path = host_etc / "nsswitch.conf"
path.write_bytes(value)
monkeypatch.setattr(
module,
"read_regular",
lambda _path, _maximum: _snapshot(value, uid=os.getuid()),
)
with pytest.raises(module.HardeningError, match=match):
module.audit_privilege_policies(
"hermes-agent", 1200, os.getuid(), host_etc, tmp_path / "share"
)
def test_policy_input_rejects_non_utf8_and_total_size(tmp_path, monkeypatch):
module = _audit_module()
host_etc = tmp_path / "etc"
host_etc.mkdir()
sudoers = host_etc / "sudoers"
sudoers.write_bytes(b"\xff")
monkeypatch.setattr(
module,
"read_regular",
lambda _path, _maximum: _snapshot(b"\xff", uid=os.getuid()),
)
with pytest.raises(module.HardeningError, match="not UTF-8"):
module.audit_privilege_policies(
"hermes-agent", 1200, os.getuid(), host_etc, tmp_path / "share"
)
monkeypatch.setattr(module, "_policy_files", lambda *_a: [sudoers] * 9)
monkeypatch.setattr(
module,
"read_regular",
lambda _path, _maximum: _snapshot(b"#" * (256 * 1024), uid=os.getuid()),
)
with pytest.raises(module.HardeningError, match="exceeds safe limit"):
module.audit_privilege_policies(
"hermes-agent", 1200, os.getuid(), host_etc, tmp_path / "share"
)
def test_broad_sudo_and_pkla_paths_are_rejected(tmp_path, monkeypatch):
module = _audit_module()
host_etc = tmp_path / "etc"
host_etc.mkdir()
sudoers = host_etc / "sudoers"
sudoers.write_text("%ALL ALL=(ALL) ALL\n", encoding="utf-8")
monkeypatch.setattr(
module,
"read_regular",
lambda path, maximum: _snapshot(path.read_bytes(), uid=os.getuid()),
)
with pytest.raises(module.HardeningError, match="broad sudo"):
module.audit_privilege_policies(
"hermes-agent", 1200, os.getuid(), host_etc, tmp_path / "share"
)
sudoers.unlink()
policy = host_etc / "polkit-1/localauthority/policy.pkla"
policy.parent.mkdir(parents=True)
policy.write_text(
"Identity=unix-user:*\nAction=org.freedesktop.policykit.exec\nResultAny=yes\n",
encoding="utf-8",
)
with pytest.raises(module.HardeningError, match="root-equivalent broad polkit"):
module.audit_privilege_policies(
"hermes-agent", 1200, os.getuid(), host_etc, tmp_path / "share"
)

View File

@ -0,0 +1,211 @@
"""Behavioral branch coverage for node account/key reconciliation."""
from __future__ import annotations
import base64
import os
import sys
import pytest
from testing.tests.test_hermes_node_account_support import (
_fixture,
_key,
_load,
_typed_key,
)
@pytest.mark.parametrize(
("value", "fields", "match"),
[
(b"name:\xff\n", 2, "not UTF-8"),
(b"name:x", 2, "final newline"),
(b":x\n", 2, "invalid record"),
(b"name:x\nname:y\n", 2, "invalid record"),
],
)
def test_account_record_parser_rejects_encoding_shape_and_duplicates(
value, fields, match
):
module = _load()
with pytest.raises(module.HardeningError, match=match):
module._records(value, fields, "database")
def test_reconcile_record_accepts_exact_existing_identity():
module = _load()
expected = ["hermes-agent", "x", "1200", "1200"]
records = [["root", "x", "0", "0"], expected]
assert module._reconcile_record(records, expected, identity_index=2) is records
def test_database_transaction_rolls_back_prior_writes(tmp_path, monkeypatch):
module, originals, _key_value, _other, _public_key = _fixture(tmp_path, monkeypatch)
real_write = module.atomic_write
writes = 0
def fail_second(path, value, metadata):
nonlocal writes
writes += 1
if writes == 2:
raise module.HardeningError("synthetic database failure")
real_write(path, value, metadata)
monkeypatch.setattr(module, "atomic_write", fail_second)
with pytest.raises(module.HardeningError, match="synthetic database failure"):
module._reconcile_databases()
assert (module.HOST_ETC / "passwd").read_text(encoding="utf-8") == originals[
"passwd"
]
def test_database_rollback_rejects_concurrent_postwrite_change(tmp_path, monkeypatch):
module, _originals, _key_value, _other, _public_key = _fixture(
tmp_path, monkeypatch
)
real_write = module.atomic_write
writes = 0
def race_then_fail(path, value, metadata):
nonlocal writes
writes += 1
if writes == 2:
(module.HOST_ETC / "passwd").write_text(
"raced:x:1:1:r:/r:/bin/sh\n", encoding="utf-8"
)
raise module.HardeningError("synthetic failure")
real_write(path, value, metadata)
monkeypatch.setattr(module, "atomic_write", race_then_fail)
with pytest.raises(module.HardeningError, match="prevents rollback"):
module._reconcile_databases()
@pytest.mark.parametrize(
("line", "match"),
[
(b"", None),
(b"# comment", None),
(b"\xff", "malformed"),
(b'command="unterminated ssh-ed25519 data', "malformed"),
(b"ordinary text", "ambiguous"),
(b"ssh-ed25519", "ambiguous"),
(b"ssh-ed25519 bad-base64!", "payload"),
(b"ssh-ed25519 YWJj", "blob"),
],
)
def test_key_identity_rejects_malformed_authorized_key_lines(line, match):
module = _load()
if match is None:
assert module._key_identity(line) is None
else:
with pytest.raises(module.HardeningError, match=match):
module._key_identity(line)
def test_key_identity_rejects_type_blob_mismatch():
module = _load()
actual = b"ssh-rsaXXXX"
blob = len(actual).to_bytes(4, "big") + actual + b"material"
line = b"ssh-ed25519 " + base64.b64encode(blob)
with pytest.raises(module.HardeningError, match="does not match"):
module._key_identity(line)
@pytest.mark.parametrize("kind", ["multiline", "unsupported"])
def test_public_key_validator_rejects_multiline_and_unsupported(tmp_path, kind):
module = _load()
path = tmp_path / "key"
value = (
"line-one\nline-two\n"
if kind == "multiline"
else _typed_key("ssh-dss", b"synthetic-material") + "\n"
)
path.write_text(value, encoding="utf-8")
with pytest.raises(module.HardeningError):
module._validated_public_key(path)
def test_directory_rejects_file_and_conflicting_owner(tmp_path):
module = _load()
file_path = tmp_path / "file"
file_path.write_text("not-directory", encoding="utf-8")
with pytest.raises(module.HardeningError, match="unsafe account directory"):
module._directory(file_path, mode=0o700, uid=os.getuid(), gid=os.getgid())
directory = tmp_path / "directory"
directory.mkdir()
with pytest.raises(module.HardeningError, match="ownership conflicts"):
module._directory(directory, mode=0o700, uid=123456, gid=123456)
def test_move_key_skips_missing_legacy_files(tmp_path, monkeypatch):
module = _load()
key = _key(b"synthetic-key")
public = tmp_path / "public"
public.write_text(key + "\n", encoding="utf-8")
home = tmp_path / "home"
home.mkdir()
monkeypatch.setattr(module, "HOST_HOME", home)
monkeypatch.setattr(module, "ACCOUNT_UID", os.getuid())
monkeypatch.setattr(module, "ACCOUNT_GID", os.getgid())
module._move_key(public)
assert (home / module.ACCOUNT / ".ssh/authorized_keys").read_text() == key + "\n"
def test_move_key_backs_up_different_existing_target(tmp_path, monkeypatch):
module, _originals, key, other, public = _fixture(tmp_path, monkeypatch)
for legacy in module.LEGACY_ACCOUNTS:
(module.HOST_HOME / legacy / ".ssh/authorized_keys").write_text(other + "\n")
target = module.HOST_HOME / module.ACCOUNT / ".ssh/authorized_keys"
target.parent.mkdir(parents=True)
target.write_text(other + "\n")
module._move_key(public)
assert target.read_text() == key + "\n"
assert (
target.with_name(target.name + ".hermes-boundary-backup").read_text()
== other + "\n"
)
def test_move_key_fails_if_legacy_removal_or_target_install_does_not_persist(
tmp_path, monkeypatch
):
module, _originals, _key_value, _other, public = _fixture(tmp_path, monkeypatch)
real_write = module.atomic_write
def ignore_legacy(path, value, metadata):
if module.ACCOUNT not in path.parts and path.name == "authorized_keys":
return
real_write(path, value, metadata)
monkeypatch.setattr(module, "atomic_write", ignore_legacy)
with pytest.raises(module.HardeningError, match="legacy Hermes authorization"):
module._move_key(public)
second = tmp_path / "second"
second.mkdir()
module, _originals, _key_value, _other, public = _fixture(second, monkeypatch)
real_write = module.atomic_write
def ignore_target(path, value, metadata):
if module.ACCOUNT in path.parts and path.name == "authorized_keys":
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(b"wrong\n")
return
real_write(path, value, metadata)
monkeypatch.setattr(module, "atomic_write", ignore_target)
with pytest.raises(module.HardeningError, match="validation failed"):
module._move_key(public)
def test_main_reconciles_requested_key(monkeypatch, tmp_path, capsys):
module = _load()
key = tmp_path / "key"
key.write_text("value", encoding="utf-8")
seen = []
monkeypatch.setattr(module, "reconcile", seen.append)
monkeypatch.setattr(sys, "argv", ["hardener", "--public-key-file", str(key)])
assert module.main() == 0
assert seen == [key]
assert "reconciled" in capsys.readouterr().out

View File

@ -0,0 +1,189 @@
"""Behavioral branch coverage for crash-safe node account file I/O."""
from __future__ import annotations
import os
import stat
import sys
from pathlib import Path
import pytest
from testing.tests.test_hermes_node_account_support import _load
def _io_module():
hardening = _load()
return sys.modules[hardening.atomic_write.__module__]
def _snapshot(module, path: Path, *, value=None, xattrs=()):
metadata = path.stat()
content = path.read_bytes() if value is None else value
return module.FileSnapshot(
value=content,
device=metadata.st_dev,
inode=metadata.st_ino,
mode=stat.S_IMODE(metadata.st_mode),
uid=metadata.st_uid,
gid=metadata.st_gid,
size=len(content),
mtime_ns=metadata.st_mtime_ns,
ctime_ns=metadata.st_ctime_ns,
xattrs=tuple(xattrs),
)
def test_xattr_reader_uses_compatibility_fallback(monkeypatch):
module = _io_module()
calls = []
def listxattr(target, **kwargs):
calls.append((target, kwargs))
if kwargs:
raise TypeError("fd API")
return ["user.safe"]
def getxattr(target, name, **kwargs):
if kwargs:
raise TypeError("fd API")
return b"value"
monkeypatch.setattr(module.os, "listxattr", listxattr)
monkeypatch.setattr(module.os, "getxattr", getxattr)
assert module._xattrs(7) == (("user.safe", b"value"),)
assert len(calls) == 2
@pytest.mark.parametrize("failure", ["count", "name", "value", "total"])
def test_xattr_reader_enforces_all_bounds(monkeypatch, failure):
module = _io_module()
if failure == "count":
names = [f"user.{index}" for index in range(module.MAX_XATTRS + 1)]
elif failure == "name":
names = ["x" * (module.MAX_XATTR_NAME + 1)]
elif failure == "total":
names = ["user.one", "user.two", "user.three", "user.four", "user.five"]
else:
names = ["user.large"]
monkeypatch.setattr(module.os, "listxattr", lambda *_a, **_k: names)
size = module.MAX_XATTR_VALUE + 1 if failure == "value" else module.MAX_XATTR_VALUE
monkeypatch.setattr(module.os, "getxattr", lambda *_a, **_k: b"x" * size)
with pytest.raises(module.HardeningError):
module._xattrs(7)
def test_restore_xattrs_removes_unexpected_and_sets_expected(monkeypatch):
module = _io_module()
monkeypatch.setattr(
module, "_xattrs", lambda _fd: (("user.old", b"old"), ("user.keep", b"before"))
)
removed = []
written = []
monkeypatch.setattr(
module.os, "removexattr", lambda fd, name: removed.append((fd, name))
)
monkeypatch.setattr(
module.os, "setxattr", lambda fd, name, value: written.append((fd, name, value))
)
module._restore_xattrs(4, (("user.keep", b"after"),))
assert removed == [(4, "user.old")]
assert written == [(4, "user.keep", b"after")]
def test_read_regular_rejects_directory_oversize_and_changed_read(
tmp_path, monkeypatch
):
module = _io_module()
with pytest.raises(module.HardeningError, match="unsafe regular"):
module.read_regular(tmp_path, 10)
source = tmp_path / "file"
source.write_bytes(b"12345")
with pytest.raises(module.HardeningError, match="unsafe regular"):
module.read_regular(source, 4)
monkeypatch.setattr(module.os, "read", lambda _fd, _size: b"1")
with pytest.raises(module.HardeningError, match="short file read"):
module.read_regular(source, 10)
def test_assert_unchanged_rejects_changed_snapshot(tmp_path):
module = _io_module()
source = tmp_path / "passwd"
source.write_bytes(b"old")
snapshot = module.read_regular(source, 16)
source.write_bytes(b"new")
with pytest.raises(module.HardeningError, match="concurrent"):
module.assert_unchanged(source, snapshot, 16)
def test_atomic_write_short_write_unlinks_temporary(tmp_path, monkeypatch):
module = _io_module()
target = tmp_path / "passwd"
target.write_bytes(b"old")
snapshot = module.read_regular(target, 16)
monkeypatch.setattr(module.os, "write", lambda _fd, _value: 0)
with pytest.raises(module.HardeningError, match="short atomic write"):
module.atomic_write(target, b"new", snapshot)
assert target.read_bytes() == b"old"
assert not list(tmp_path.glob(".passwd.hermes-*"))
def test_backup_existing_is_validated_and_short_write_is_removed(tmp_path, monkeypatch):
module = _io_module()
source = tmp_path / "passwd"
source.write_bytes(b"old")
snapshot = module.read_regular(source, 16)
backup = tmp_path / "passwd.hermes-boundary-backup"
backup.write_bytes(b"preserved")
assert module.backup_once(source, snapshot, 16) == backup
backup.unlink()
monkeypatch.setattr(module.os, "write", lambda _fd, _value: 0)
with pytest.raises(module.HardeningError, match="short backup write"):
module.backup_once(source, snapshot, 16)
assert not backup.exists()
def test_account_lock_rejects_unsafe_metadata(tmp_path):
module = _io_module()
lock = tmp_path / ".pwd.lock"
lock.write_text("", encoding="utf-8")
lock.chmod(0o644)
with (
pytest.raises(module.HardeningError, match="unsafe standard account lock"),
module.account_lock(tmp_path, expected_uid=os.getuid()),
):
pass
def test_account_lock_retries_then_succeeds(tmp_path, monkeypatch):
module = _io_module()
attempts = []
def lockf(_descriptor, operation):
attempts.append(operation)
if len(attempts) == 1:
raise BlockingIOError()
monkeypatch.setattr(module.fcntl, "lockf", lockf)
monkeypatch.setattr(module.time, "sleep", lambda _seconds: None)
with module.account_lock(tmp_path, expected_uid=os.getuid()):
assert attempts
assert len(attempts) == 3
def test_account_lock_reports_timeout(tmp_path, monkeypatch):
module = _io_module()
def lockf(_descriptor, operation):
if operation != module.fcntl.LOCK_UN:
raise BlockingIOError()
monkeypatch.setattr(module.fcntl, "lockf", lockf)
ticks = iter((0.0, 16.0))
monkeypatch.setattr(module.time, "monotonic", lambda: next(ticks))
with (
pytest.raises(module.HardeningError, match="lock is busy"),
module.account_lock(tmp_path, expected_uid=os.getuid()),
):
pass

View File

@ -0,0 +1,137 @@
"""Behavioral branch coverage for Vault-to-memory runtime access staging."""
from __future__ import annotations
import json
import sys
from pathlib import Path
import pytest
from testing.tests.test_hermes_runtime_access import _load
def _stage(tmp_path: Path, monkeypatch):
module = _load("stage_runtime_access")
vault = tmp_path / "vault"
runtime = tmp_path / "runtime"
home = tmp_path / "home"
vault.mkdir()
home.mkdir()
monkeypatch.setattr(module, "VAULT_ROOT", vault)
monkeypatch.setattr(module, "RUNTIME_ROOT", runtime)
monkeypatch.setattr(module, "PERSISTENT_HOME", home)
monkeypatch.setattr(module.os, "chown", lambda *_args: None)
return module, vault, runtime, home
def test_private_directory_and_secret_copy_validate_mode_and_content(
tmp_path, monkeypatch
):
module, vault, runtime, _home = _stage(tmp_path, monkeypatch)
module._owned_directory(runtime)
assert runtime.stat().st_mode & 0o777 == 0o700
(vault / "empty").write_text(" \n", encoding="utf-8")
with pytest.raises(RuntimeError, match="is empty"):
module._copy_secret("empty", runtime / "empty")
(vault / "value").write_text(" staged \n", encoding="utf-8")
assert module._copy_secret("value", runtime / "value") == "staged"
assert (runtime / "value").read_text(encoding="utf-8") == "staged\n"
@pytest.mark.parametrize(
("document", "required", "match"),
[
({"outer": {}}, ("outer", "token"), "invalid shape"),
({"outer": {"token": ""}}, ("outer", "token"), "empty credential"),
({"outer": {"token": 7}}, ("outer", "token"), "empty credential"),
],
)
def test_json_staging_rejects_missing_nontext_and_empty_credentials(
tmp_path, monkeypatch, document, required, match
):
module, vault, runtime, _home = _stage(tmp_path, monkeypatch)
runtime.mkdir()
(vault / "credential").write_text(json.dumps(document), encoding="utf-8")
destination = runtime / "credential.json"
with pytest.raises(RuntimeError, match=match):
module._validated_json("credential", destination, required)
assert not destination.exists()
def test_noncredential_linker_skips_absent_symlink_and_existing_entries(
tmp_path, monkeypatch
):
module, _vault, runtime, home = _stage(tmp_path, monkeypatch)
runtime.mkdir()
source = home / "state"
destination = runtime / "state"
destination.mkdir()
module._link_noncredential_state(source, destination, ("allowed",))
source.mkdir()
(source / "allowed").write_text("safe", encoding="utf-8")
(source / "existing").write_text("new", encoding="utf-8")
(source / "target-link").write_text("new", encoding="utf-8")
(source / "linked").symlink_to(source / "allowed")
(destination / "existing").write_text("preserve", encoding="utf-8")
(destination / "target-link").symlink_to(destination / "existing")
module._link_noncredential_state(
source,
destination,
("missing", "linked", "existing", "target-link", "allowed"),
)
assert (destination / "allowed").is_symlink()
assert (destination / "existing").read_text(encoding="utf-8") == "preserve"
def test_empty_auth_store_is_private_and_noncredential(tmp_path, monkeypatch):
module, _vault, runtime, _home = _stage(tmp_path, monkeypatch)
runtime.mkdir()
module._write_empty_auth_store()
path = runtime / "hermes-auth.json"
assert json.loads(path.read_text(encoding="utf-8")) == {
"version": 1,
"providers": {},
"credential_pool": {},
}
assert path.stat().st_mode & 0o777 == 0o600
def test_node_ssh_config_rejects_nul_and_removes_partial_file(tmp_path, monkeypatch):
module, vault, runtime, _home = _stage(tmp_path, monkeypatch)
runtime.mkdir()
(vault / "node-ssh-config").write_bytes(b"Host titan-01\x00\n")
with pytest.raises(RuntimeError, match="control data"):
module._stage_node_ssh_config()
assert not (runtime / "node-ssh-config").exists()
@pytest.mark.parametrize(
("mode", "secret_name"),
[("chat", "chat-relay-key"), ("triage", "triage-api-key")],
)
def test_isolated_tenant_staging_exposes_only_required_secret(
tmp_path, monkeypatch, mode, secret_name
):
module, vault, runtime, _home = _stage(tmp_path, monkeypatch)
(vault / secret_name).write_text("synthetic-value\n", encoding="utf-8")
getattr(module, f"stage_{mode}")()
assert (runtime / secret_name).read_text(encoding="utf-8") == "synthetic-value\n"
assert (runtime / "hermes-auth.json").exists()
assert sorted(path.name for path in runtime.iterdir()) == sorted(
["hermes-auth.json", secret_name]
)
@pytest.mark.parametrize("mode", ["agent", "chat", "triage"])
def test_main_dispatches_each_mode(mode, monkeypatch, capsys):
module = _load("stage_runtime_access")
calls = []
monkeypatch.setattr(module, "stage_agent", lambda: calls.append("agent"))
monkeypatch.setattr(module, "stage_chat", lambda: calls.append("chat"))
monkeypatch.setattr(module, "stage_triage", lambda: calls.append("triage"))
monkeypatch.setattr(sys, "argv", ["stage-runtime-access", mode])
assert module.main() == 0
assert calls == [mode]
assert "staged from Vault" in capsys.readouterr().out

View File

@ -0,0 +1,157 @@
"""Behavioral coverage for the credential-free SCM broker client and I/O."""
from __future__ import annotations
import io
import json
import pytest
from testing.tests.test_hermes_scm_broker_support import Response, _load
def test_client_redirect_and_default_opener_are_fail_closed(monkeypatch):
client = _load("scm_broker_client")
with pytest.raises(client.PolicyError, match="redirects"):
client.RejectRedirectHandler().redirect_request(
object(), None, 302, "redirect", {}, "http://elsewhere.invalid"
)
sentinel = object()
monkeypatch.setattr(
client._OPENER, "open", lambda request, timeout: (request, timeout, sentinel)
)
assert client._open("request", 4) == ("request", 4, sentinel)
@pytest.mark.parametrize(
("endpoint", "payload", "match"),
[
("/v1/admin", {}, "outside"),
("/v1/metadata", {"path": "x" * (64 * 1024)}, "safe size"),
],
)
def test_client_rejects_unknown_or_oversized_operations(endpoint, payload, match):
client = _load("scm_broker_client")
with pytest.raises(client.PolicyError, match=match):
client.request(endpoint, payload, opener=lambda *_a, **_k: None)
class _GetCodeResponse(Response):
def __init__(self, body: bytes, *, content_type="application/json", code=200):
super().__init__(body, content_type=content_type)
del self.status
self._code = code
def getcode(self):
return self._code
def test_client_accepts_getcode_fallback_and_create_payload():
client = _load("scm_broker_client")
seen = []
def opener(request, timeout):
seen.append((request, timeout))
return _GetCodeResponse(b"{}")
result = client.create_draft(
"cassandra",
base="main",
head="feature/coverage",
head_sha="a" * 40,
title="WIP: Coverage",
body="Review evidence",
opener=opener,
)
assert result == b"{}"
assert json.loads(seen[0][0].data)["repo"] == "cassandra"
@pytest.mark.parametrize(
("response", "match"),
[
(_GetCodeResponse(b"{}", code=201), "HTTP status"),
(_GetCodeResponse(b"{}", content_type="text/plain"), "response type"),
(
Response(b"x" * (2 * 1024 * 1024 + 1), content_type="application/json"),
"safe size",
),
(Response(b"not-json", content_type="application/json"), "Expecting value"),
],
)
def test_client_rejects_bad_status_type_size_and_json(response, match):
client = _load("scm_broker_client")
expected = (
json.JSONDecodeError if match == "Expecting value" else client.PolicyError
)
with pytest.raises(expected, match=match):
client.request(
"/v1/metadata",
{"path": "/api/v1/repos/atlas/cassandra"},
opener=lambda *_a, **_k: response,
)
class _Socket:
def __init__(self):
self.timeouts = []
def settimeout(self, value):
self.timeouts.append(value)
class _Stream:
def __init__(self, body: bytes, socket_shape: str = "direct"):
self.body = io.BytesIO(body)
self.socket = _Socket()
if socket_shape == "nested":
self.fp = type(
"FP", (), {"raw": type("Raw", (), {"_sock": self.socket})()}
)()
elif socket_shape == "direct":
self.fp = type("FP", (), {"raw": self.socket})()
else:
self.fp = None
def read(self, size):
return self.body.read(size)
def test_response_spool_handles_direct_and_absent_socket_shapes():
module = _load("scm_broker_io")
for shape in ("direct", "absent"):
stream = _Stream(b"safe", shape)
spool, length = module.spool_response(
stream,
16,
(b"forbidden",),
memory_limit=8,
chunk_size=2,
deadline_seconds=5,
)
try:
assert length == 4 and spool.read() == b"safe"
finally:
spool.close()
@pytest.mark.parametrize("failure", ["deadline", "size", "credential", "read"])
def test_response_spool_closes_and_rejects_each_failure(monkeypatch, failure):
module = _load("scm_broker_io")
stream = _Stream(b"safe-forbidden-value")
kwargs = {
"maximum": 128,
"forbidden": (b"forbidden",),
"memory_limit": 8,
"chunk_size": 5,
"deadline_seconds": 5,
}
if failure == "deadline":
ticks = iter((10.0, 16.0))
monkeypatch.setattr(module.time, "monotonic", lambda: next(ticks))
elif failure == "size":
kwargs["maximum"] = 2
elif failure == "read":
stream.read = lambda _size: (_ for _ in ()).throw(OSError("read failed"))
expected = module.PolicyError if failure != "read" else OSError
with pytest.raises(expected):
module.spool_response(stream, **kwargs)

View File

@ -0,0 +1,265 @@
"""Behavioral branch coverage for SCM broker request handling."""
from __future__ import annotations
import io
import json
import sys
from email.message import Message
import pytest
from testing.tests.test_hermes_scm_broker_support import _load, _receive_command
class _Connection:
def __init__(self):
self.timeouts = []
def settimeout(self, value):
self.timeouts.append(value)
def _headers(*, content_type="application/json", body=b""):
value = Message()
value["Content-Type"] = content_type
value["Content-Length"] = str(len(body))
return value
def _handler(broker, *, path="/healthz", body=b"", content_type="application/json"):
handler = object.__new__(broker.BrokerHandler)
handler.path = path
handler.headers = _headers(content_type=content_type, body=body)
handler.rfile = io.BytesIO(body)
handler.wfile = io.BytesIO()
handler.connection = _Connection()
handler.events = []
handler.send_response = lambda status: handler.events.append(("status", status))
handler.send_header = lambda name, value: handler.events.append((name, value))
handler.end_headers = lambda: handler.events.append(("headers", "done"))
return handler
def test_header_validation_accepts_safe_ascii_and_rejects_controls():
broker = _load("scm_broker")
handler = _handler(broker)
handler._validate_headers()
class HeaderBag:
def items(self):
return [("X-Test", "bad\x01value")]
def get_all(self, _name, default):
return default
handler.headers = HeaderBag()
with pytest.raises(broker.PolicyError, match="controls"):
handler._validate_headers()
class UnicodeHeader(HeaderBag):
def items(self):
return [("X-Test", "máin")]
handler.headers = UnicodeHeader()
with pytest.raises(broker.PolicyError, match="ASCII"):
handler._validate_headers()
def test_json_reject_and_stream_response_methods(monkeypatch):
broker = _load("scm_broker")
handler = _handler(broker)
handler._json(202, b'{"ok":true}')
assert ("status", 202) in handler.events
assert handler.wfile.getvalue() == b'{"ok":true}'
handler = _handler(broker)
handler._reject(403)
assert ("status", 403) in handler.events
assert json.loads(handler.wfile.getvalue()) == {"error": "request rejected"}
handler = _handler(broker)
handler._stream(200, "application/test", io.BytesIO(b"streamed"), 8)
assert handler.wfile.getvalue() == b"streamed"
assert handler.connection.timeouts
handler.log_message("ignored %s", "value")
handler = _handler(broker)
ticks = iter((0.0, 121.0))
monkeypatch.setattr(broker.time, "monotonic", lambda: next(ticks))
with pytest.raises(broker.PolicyError, match="write deadline"):
handler._stream(200, "application/test", io.BytesIO(b"x"), 1)
def test_get_health_and_git_discovery_paths(monkeypatch):
broker = _load("scm_broker")
handler = _handler(broker)
handler.do_GET()
assert json.loads(handler.wfile.getvalue()) == {"status": "ok"}
closed = []
class Body(io.BytesIO):
def close(self):
closed.append(True)
super().close()
handler = _handler(
broker,
path="/git/atlas/cassandra.git/info/refs?service=git-upload-pack",
)
monkeypatch.setattr(broker, "read_token", lambda: "sentinel")
monkeypatch.setattr(
broker,
"_upstream_git_request",
lambda *args, **kwargs: (Body(b"advertisement"), 13),
)
handler.do_GET()
assert handler.wfile.getvalue() == b"advertisement"
assert closed == [True]
handler = _handler(broker, path="/git/atlas/cassandra.git/git-upload-pack")
handler.do_GET()
assert json.loads(handler.wfile.getvalue()) == {"error": "request rejected"}
def test_post_dispatches_control_and_git_or_rejects(monkeypatch):
broker = _load("scm_broker")
control = _handler(broker, path="/v1/metadata", body=b"{}")
git = _handler(broker, path="/git/atlas/cassandra.git/git-upload-pack", body=b"")
calls = []
control._control = lambda: calls.append("control")
control._git_rpc = lambda: calls.append("git")
git._control = lambda: calls.append("control")
git._git_rpc = lambda: calls.append("git")
control.do_POST()
git.do_POST()
assert calls == ["control", "git"]
rejected = _handler(broker, path="/v1/metadata", body=b"{}")
rejected._control = lambda: (_ for _ in ()).throw(broker.PolicyError("no"))
rejected.do_POST()
assert json.loads(rejected.wfile.getvalue()) == {"error": "request rejected"}
def test_control_metadata_and_draft_fields(monkeypatch):
broker = _load("scm_broker")
monkeypatch.setattr(broker, "read_token", lambda: "sentinel")
monkeypatch.setattr(
broker, "read", lambda path, token: json.dumps({"path": path}).encode()
)
monkeypatch.setattr(
broker,
"create_draft",
lambda token, **data: json.dumps({"token_used": bool(token), **data}).encode(),
)
metadata_body = b'{"path":"/api/v1/repos/atlas/cassandra"}'
metadata = _handler(broker, path="/v1/metadata", body=metadata_body)
metadata._control()
assert json.loads(metadata.wfile.getvalue())["path"].endswith("cassandra")
draft_data = {
"base": "main",
"body": "Review evidence",
"head": "feature/coverage",
"head_sha": "a" * 40,
"repo": "cassandra",
"title": "WIP: Coverage",
}
draft_body = json.dumps(draft_data).encode()
draft = _handler(broker, path="/v1/drafts", body=draft_body)
draft._control()
assert json.loads(draft.wfile.getvalue())["repo"] == "cassandra"
for path, body, match in (
("/v1/metadata", b'{"wrong":"field"}', "fields"),
("/v1/drafts", b'{"repo":1}', "fields"),
):
with pytest.raises(broker.PolicyError, match=match):
_handler(broker, path=path, body=body)._control()
monkeypatch.setattr(broker, "read", lambda *_a, **_k: b"sentinel")
with pytest.raises(broker.PolicyError, match="reflected"):
_handler(broker, path="/v1/metadata", body=metadata_body)._control()
@pytest.mark.parametrize("service", ["git-upload-pack", "git-receive-pack"])
def test_git_rpc_streams_allowed_upload_and_receive(monkeypatch, service):
broker = _load("scm_broker")
if service == "git-receive-pack":
body = _receive_command(b"0" * 40, b"1" * 40, b"refs/heads/hermes/coverage")
else:
body = b"upload-request"
handler = _handler(
broker,
path=f"/git/atlas/cassandra.git/{service}",
body=body,
content_type=f"application/x-{service}-request",
)
monkeypatch.setattr(broker, "read_token", lambda: "sentinel")
seen = []
def upstream(target, **kwargs):
seen.append((target, kwargs))
return io.BytesIO(b"result"), 6
monkeypatch.setattr(broker, "_upstream_git_request", upstream)
handler._git_rpc()
assert handler.wfile.getvalue() == b"result"
assert seen[0][0].endswith(service)
assert seen[0][1]["body_length"] == len(body)
@pytest.mark.parametrize(
("path", "content_type", "transfer", "match"),
[
(
"/git/atlas/cassandra.git/info/refs?service=git-upload-pack",
"application/x-git-upload-pack-request",
None,
"operation",
),
(
"/git/atlas/cassandra.git/git-upload-pack",
"text/plain",
None,
"request type",
),
(
"/git/atlas/cassandra.git/git-upload-pack",
"application/x-git-upload-pack-request",
"chunked",
"request type",
),
],
)
def test_git_rpc_rejects_discovery_wrong_type_and_chunking(
path, content_type, transfer, match
):
broker = _load("scm_broker")
handler = _handler(broker, path=path, content_type=content_type)
if transfer:
handler.headers["Transfer-Encoding"] = transfer
with pytest.raises(broker.PolicyError, match=match):
handler._git_rpc()
def test_broker_main_constructs_bounded_server(monkeypatch):
broker = _load("scm_broker")
seen = []
class Server:
def __init__(self, address, handler):
seen.append((address, handler))
def serve_forever(self):
seen.append("served")
monkeypatch.setattr(broker, "BoundedThreadingHTTPServer", Server)
monkeypatch.setattr(
sys, "argv", ["broker", "--listen", "127.0.0.1", "--port", "9191"]
)
assert broker.main() == 0
assert seen[0] == (("127.0.0.1", 9191), broker.BrokerHandler)
assert seen[1] == "served"

View File

@ -0,0 +1,263 @@
"""Behavioral branch coverage for SCM broker protocol helpers."""
from __future__ import annotations
import io
from email.message import Message
import pytest
from testing.tests.test_hermes_scm_broker_support import Response, _load
def test_broker_redirect_status_and_bounded_read_helpers(monkeypatch):
broker = _load("scm_broker")
with pytest.raises(broker.PolicyError, match="redirects"):
broker.RejectRedirect().redirect_request(
object(), None, 302, "redirect", {}, "https://elsewhere.invalid"
)
class CodeOnly:
def getcode(self):
return 204
assert broker._status(type("Status", (), {"status": 200})()) == 200
assert broker._status(CodeOnly()) == 204
assert broker._status(object()) is None
with pytest.raises(broker.PolicyError, match="safe size"):
broker._read_bounded(io.BytesIO(b"x"), 4, -1)
with pytest.raises(broker.PolicyError, match="safe size"):
broker._read_bounded(io.BytesIO(b"xxxxx"), 4)
with pytest.raises(broker.PolicyError, match="safe size"):
broker._read_bounded(io.BytesIO(b"x"), 4, 2)
with pytest.raises(broker.PolicyError, match="deadline"):
broker._read_bounded(io.BytesIO(b"x"), 4, 1, deadline=0)
timeouts = []
assert (
broker._read_bounded(
io.BytesIO(b"xy"),
4,
2,
deadline=broker.time.monotonic() + 5,
set_timeout=timeouts.append,
)
== b"xy"
)
assert timeouts
def test_spool_bounded_rejects_bounds_early_eof_and_closes_on_read_error():
broker = _load("scm_broker")
for length in (-1, 5):
with pytest.raises(broker.PolicyError, match="safe size"):
broker._spool_bounded(
io.BytesIO(b"x"),
4,
length,
token="sentinel",
context="request",
deadline=999999999,
)
with pytest.raises(broker.PolicyError, match="ended early"):
broker._spool_bounded(
io.BytesIO(b"x"),
4,
2,
token="sentinel",
context="request",
deadline=999999999,
)
class Broken:
def read(self, _size):
raise OSError("broken stream")
with pytest.raises(OSError, match="broken"):
broker._spool_bounded(
Broken(), 4, 1, token="sentinel", context="request", deadline=999999999
)
class _HandlerInput:
def __init__(self, body: bytes, *, content_type="application/json", length=None):
self.headers = Message()
self.headers["Content-Type"] = content_type
if length is None:
length = len(body)
self.headers["Content-Length"] = str(length)
self.rfile = io.BytesIO(body)
self.connection = type(
"Connection", (), {"settimeout": lambda self, value: None}
)()
def test_control_json_loader_requires_fixed_json_object():
broker = _load("scm_broker")
handler = _HandlerInput(b"{}")
handler.headers["Transfer-Encoding"] = "chunked"
with pytest.raises(broker.PolicyError, match="chunked"):
broker._load_json(handler)
with pytest.raises(broker.PolicyError, match="must be JSON"):
broker._load_json(_HandlerInput(b"{}", content_type="text/plain"))
with pytest.raises(broker.PolicyError, match="must be an object"):
broker._load_json(_HandlerInput(b"[]"))
assert broker._load_json(_HandlerInput(b'{"path":"safe"}')) == {"path": "safe"}
@pytest.mark.parametrize(
("target", "expected"),
[
(
"/git/atlas/cassandra.git/info/refs?service=git-upload-pack",
("cassandra", "info/refs", "git-upload-pack"),
),
(
"/git/atlas/cassandra.git/info/refs?service=git-receive-pack",
("cassandra", "info/refs", "git-receive-pack"),
),
(
"/git/atlas/cassandra.git/git-upload-pack",
("cassandra", "git-upload-pack", "git-upload-pack"),
),
(
"/git/atlas/cassandra.git/git-receive-pack",
("cassandra", "git-receive-pack", "git-receive-pack"),
),
],
)
def test_git_target_accepts_each_exact_smart_http_route(target, expected):
broker = _load("scm_broker")
assert broker._git_target(target) == expected
def test_content_length_and_credential_helpers_cover_safe_and_rejected_values():
broker = _load("scm_broker")
headers = Message()
headers["Content-Length"] = "0"
assert broker._content_length(headers, 1) == 0
forms = broker._credential_forms("sentinel")
assert len(forms) == 3 and forms[0] == b"sentinel"
broker._reject_credential_bytes(b"safe", "sentinel", "response")
with pytest.raises(broker.PolicyError, match="credential material"):
broker._reject_credential_bytes(b"prefix " + forms[2], "sentinel", "response")
def test_receive_prefix_preserves_stream_position_and_accepts_bytes():
broker = _load("scm_broker")
assert broker._receive_prefix(b"bytes") == b"bytes"
stream = io.BytesIO(b"prefix")
stream.seek(3)
assert broker._receive_prefix(stream) == b"prefix"
assert stream.tell() == 3
@pytest.mark.parametrize(
("body", "match"),
[
(b"bad!", "framing"),
(b"0000", "no ref command"),
(b"0003", "framing"),
(b"0008abc", "framing"),
(b"0008a b\n0000", "ref command"),
],
)
def test_receive_pack_rejects_bad_framing_and_commands(body, match):
broker = _load("scm_broker")
with pytest.raises(broker.PolicyError, match=match):
broker._validate_receive_pack(body, "sentinel")
def _packet(old: bytes, new: bytes, ref: bytes, *, terminator=True) -> bytes:
command = old + b" " + new + b" " + ref + b"\n"
framed = f"{len(command) + 4:04x}".encode() + command
return framed + (b"0000" if terminator else b"")
def test_receive_pack_rejects_nonascii_many_commands_and_missing_terminator(
monkeypatch,
):
broker = _load("scm_broker")
zero = b"0" * 40
commit = b"1" * 40
with pytest.raises(broker.PolicyError, match="canonical ASCII"):
broker._validate_receive_pack(
_packet(zero, commit, b"refs/heads/hermes/\xff"), "sentinel"
)
command = _packet(zero, commit, b"refs/heads/hermes/fix", terminator=False)
with pytest.raises(broker.PolicyError, match="terminator"):
broker._validate_receive_pack(command, "sentinel")
many = command * (broker.MAX_PUSH_COMMANDS + 1) + b"0000"
with pytest.raises(broker.PolicyError, match="too many"):
broker._validate_receive_pack(many, "sentinel")
def test_upstream_request_infers_byte_length_and_requires_stream_length():
broker = _load("scm_broker")
seen = []
def opener(request, timeout):
seen.append((request, timeout))
return Response(b"result", content_type="application/x-git-upload-pack-result")
assert (
broker._upstream_git_request(
"/atlas/cassandra.git/git-upload-pack",
method="POST",
body=b"request",
content_type=None,
expected_type="application/x-git-upload-pack-result",
token="sentinel",
opener=opener,
)
== b"result"
)
assert seen[0][0].get_header("Content-length") == "7"
with pytest.raises(broker.PolicyError, match="length is missing"):
broker._upstream_git_request(
"/atlas/cassandra.git/git-upload-pack",
method="POST",
body=io.BytesIO(b"request"),
content_type=None,
expected_type="application/x-git-upload-pack-result",
token="sentinel",
opener=opener,
)
def test_upstream_request_rejects_wrong_type_and_supports_getcode_status():
broker = _load("scm_broker")
class CodeOnly(Response):
def __init__(self, body, content_type):
super().__init__(body, content_type=content_type)
del self.status
def getcode(self):
return 200
assert (
broker._upstream_git_request(
"/atlas/cassandra.git/info/refs?service=git-upload-pack",
method="GET",
body=None,
content_type=None,
expected_type="application/x-git-upload-pack-advertisement",
token="sentinel",
opener=lambda *_a, **_k: CodeOnly(
b"advertisement", "application/x-git-upload-pack-advertisement"
),
)
== b"advertisement"
)
with pytest.raises(broker.PolicyError, match="response type"):
broker._upstream_git_request(
"/atlas/cassandra.git/info/refs?service=git-upload-pack",
method="GET",
body=None,
content_type=None,
expected_type="application/x-git-upload-pack-advertisement",
token="sentinel",
opener=lambda *_a, **_k: Response(b"bad", content_type="text/plain"),
)

View File

@ -0,0 +1,180 @@
"""Behavioral branch coverage for the SCM broker HTTP server primitives."""
from __future__ import annotations
import io
import pytest
from testing.tests.test_hermes_scm_broker_support import _load
class _Connection:
def __init__(self):
self.timeouts = []
def settimeout(self, value):
self.timeouts.append(value)
class _FlushBuffer(io.BytesIO):
def __init__(self):
super().__init__()
self.flushes = 0
def flush(self):
self.flushes += 1
def test_absolute_reader_supports_idle_limit_newline_eof_and_attribute_delegation():
module = _load("scm_broker_server")
stream = io.BytesIO(b"first\nsecond")
reader = module._AbsoluteDeadlineReader(stream, _Connection())
assert reader.readline(3) == b"fir"
reader.begin(5)
assert reader.readline() == b"st\n"
assert reader.readline() == b"second"
assert reader.readline() == b""
assert reader.closed is False
reader.end()
assert reader.readline() == b""
def _handler_type(module):
class StubHandler:
request_version = "HTTP/1.1"
command = "GET"
def setup(self):
self.connection = _Connection()
self.rfile = io.BytesIO(b"")
self.wfile = _FlushBuffer()
def parse_request(self):
return self.parse_result
def send_error(self, code, *_args):
self.errors.append(code)
def do_GET(self):
self.calls.append("GET")
return type(
"DeadlineHandler", (module.AbsoluteHeaderDeadlineMixin, StubHandler), {}
)
def _handler(module, raw: bytes, *, parse=True):
handler_type = _handler_type(module)
handler = object.__new__(handler_type)
handler.setup()
handler._header_reader._stream = io.BytesIO(raw)
handler.parse_result = parse
handler.errors = []
handler.calls = []
return handler
def test_header_mixin_setup_and_known_request_dispatch():
module = _load("scm_broker_server")
handler = _handler(module, b"GET /healthz HTTP/1.1\r\n")
handler.handle_one_request()
assert handler.calls == ["GET"]
assert handler.wfile.flushes == 1
assert handler._header_reader._deadline is None
@pytest.mark.parametrize(
("raw", "parse", "command", "expected_error", "closed"),
[
(b"x" * 65537, True, "GET", 414, False),
(b"", True, "GET", None, True),
(b"GET / HTTP/1.1\r\n", False, "GET", None, False),
(b"TRACE / HTTP/1.1\r\n", True, "TRACE", 501, False),
],
)
def test_header_mixin_rejects_long_empty_unparsed_and_unknown_requests(
raw, parse, command, expected_error, closed
):
module = _load("scm_broker_server")
handler = _handler(module, raw, parse=parse)
handler.command = command
handler.close_connection = False
handler.handle_one_request()
assert handler.errors == ([] if expected_error is None else [expected_error])
assert handler.close_connection is closed
def test_header_mixin_closes_on_absolute_timeout(monkeypatch):
module = _load("scm_broker_server")
handler = _handler(module, b"GET / HTTP/1.1\r\n")
handler.close_connection = False
monkeypatch.setattr(
handler._header_reader,
"readline",
lambda *_a, **_k: (_ for _ in ()).throw(TimeoutError()),
)
handler.handle_one_request()
assert handler.close_connection is True
class _Slots:
def __init__(self, available=True):
self.available = available
self.releases = 0
def acquire(self, *, blocking):
assert blocking is False
return self.available
def release(self):
self.releases += 1
def _server_shell(module, available=True):
server = object.__new__(module.BoundedThreadingHTTPServer)
server._slots = _Slots(available)
server.shutdowns = []
server.shutdown_request = server.shutdowns.append
return server
def test_bounded_server_delegates_accepted_request(monkeypatch):
module = _load("scm_broker_server")
server = _server_shell(module)
calls = []
monkeypatch.setattr(
module.ThreadingHTTPServer,
"process_request",
lambda self, request, address: calls.append((request, address)),
)
server.process_request("socket", ("127.0.0.1", 1))
assert calls == [("socket", ("127.0.0.1", 1))]
assert server._slots.releases == 0
def test_bounded_server_releases_on_dispatch_failure(monkeypatch):
module = _load("scm_broker_server")
server = _server_shell(module)
monkeypatch.setattr(
module.ThreadingHTTPServer,
"process_request",
lambda *_a, **_k: (_ for _ in ()).throw(RuntimeError("dispatch")),
)
with pytest.raises(RuntimeError, match="dispatch"):
server.process_request("socket", ("127.0.0.1", 1))
assert server._slots.releases == 1
assert server.shutdowns == ["socket"]
def test_bounded_server_thread_always_releases(monkeypatch):
module = _load("scm_broker_server")
server = _server_shell(module)
monkeypatch.setattr(
module.ThreadingHTTPServer,
"process_request_thread",
lambda *_a, **_k: (_ for _ in ()).throw(RuntimeError("handler")),
)
with pytest.raises(RuntimeError, match="handler"):
server.process_request_thread("socket", ("127.0.0.1", 1))
assert server._slots.releases == 1

View File

@ -12,10 +12,15 @@ def test_compute_workspace_line_coverage_handles_missing_xml(tmp_path: Path) ->
"""Missing coverage XML should produce a zero workspace coverage score.""" """Missing coverage XML should produce a zero workspace coverage score."""
contract = {"coverage": {"tracked_files": ["managed.py"]}} contract = {"coverage": {"tracked_files": ["managed.py"]}}
assert compute_workspace_line_coverage(contract, tmp_path, tmp_path / "missing.xml") == 0.0 assert (
compute_workspace_line_coverage(contract, tmp_path, tmp_path / "missing.xml")
== 0.0
)
def test_compute_workspace_line_coverage_averages_present_tracked_files(tmp_path: Path) -> None: def test_compute_workspace_line_coverage_averages_present_tracked_files(
tmp_path: Path,
) -> None:
"""Workspace coverage should average only tracked files that appear in the report.""" """Workspace coverage should average only tracked files that appear in the report."""
coverage_xml = tmp_path / "coverage.xml" coverage_xml = tmp_path / "coverage.xml"
@ -26,8 +31,8 @@ def test_compute_workspace_line_coverage_averages_present_tracked_files(tmp_path
<packages> <packages>
<package> <package>
<classes> <classes>
<class filename="alpha.py" line-rate="1.0" /> <class filename="alpha.py" line-rate="1.0" branch-rate="1.0" />
<class filename="beta.py" line-rate="0.5" /> <class filename="beta.py" line-rate="0.5" branch-rate="0.5" />
</classes> </classes>
</package> </package>
</packages> </packages>
@ -41,7 +46,9 @@ def test_compute_workspace_line_coverage_averages_present_tracked_files(tmp_path
assert compute_workspace_line_coverage(contract, tmp_path, coverage_xml) == 75.0 assert compute_workspace_line_coverage(contract, tmp_path, coverage_xml) == 75.0
def test_run_check_keeps_relative_names_when_source_roots_do_not_match(tmp_path: Path) -> None: def test_run_check_keeps_relative_names_when_source_roots_do_not_match(
tmp_path: Path,
) -> None:
"""Relative filenames should remain relative when no declared source root contains them.""" """Relative filenames should remain relative when no declared source root contains them."""
coverage_xml = tmp_path / "coverage.xml" coverage_xml = tmp_path / "coverage.xml"
@ -74,3 +81,73 @@ def test_run_check_keeps_relative_names_when_source_roots_do_not_match(tmp_path:
) )
assert issues == ["coverage below 95.0%: relative.py (80.0%)"] assert issues == ["coverage below 95.0%: relative.py (80.0%)"]
def test_run_check_enforces_branch_floor_and_requires_branch_data(
tmp_path: Path,
) -> None:
"""Tracked modules must report and meet the configured branch floor."""
coverage_xml = tmp_path / "coverage.xml"
coverage_xml.write_text(
textwrap.dedent(
"""\
<coverage>
<packages>
<package>
<classes>
<class filename="low.py" line-rate="1.0" branch-rate="0.90" />
<class filename="missing.py" line-rate="1.0" />
</classes>
</package>
</packages>
</coverage>
"""
),
encoding="utf-8",
)
issues = run_check(
{
"coverage": {
"minimum_percent": 95.0,
"minimum_branch_percent": 95.0,
"tracked_files": ["low.py", "missing.py"],
}
},
tmp_path,
coverage_xml,
)
assert issues == [
"branch coverage below 95.0%: low.py (90.0%)",
"branch coverage missing for tracked file: missing.py",
]
def test_run_check_limits_branch_floor_to_explicit_branch_tracked_files(
tmp_path: Path,
) -> None:
"""A branch rollout can cover selected security modules without hiding lines."""
coverage_xml = tmp_path / "coverage.xml"
coverage_xml.write_text(
"""<coverage><packages><package><classes>
<class filename="new.py" line-rate="1.0" branch-rate="1.0" />
<class filename="legacy.py" line-rate="1.0" branch-rate="0.5" />
</classes></package></packages></coverage>""",
encoding="utf-8",
)
issues = run_check(
{
"coverage": {
"minimum_percent": 95,
"minimum_branch_percent": 95,
"tracked_files": ["new.py", "legacy.py"],
"branch_tracked_files": ["new.py"],
}
},
tmp_path,
coverage_xml,
)
assert issues == []