Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,393 @@
|
||||
"""Tests for issue #89315 — ``--replace`` must never signal a gateway it
|
||||
cannot prove belongs to this HERMES_HOME.
|
||||
|
||||
Design contract (v3, after andrexibiza's second review): ownership is decided
|
||||
by the persisted identity record ALONE — exact ``_same_hermes_home`` equality
|
||||
bound to the live target by exact PID + start-time. A readable live argv
|
||||
carries no HERMES_HOME, so it can never prove home ownership; it only feeds a
|
||||
token-exact CONSISTENCY check that refuses explicit contradictions.
|
||||
|
||||
Pinned surfaces:
|
||||
|
||||
* record authority — valid+bound same-home allows; missing/legacy/unbound/
|
||||
foreign records refuse;
|
||||
* argv consistency — token-exact: ``--profile timothy`` must NOT read as
|
||||
``tim`` (the substring heuristic's false-allow), while an exact different
|
||||
profile flag contradicts and refuses;
|
||||
* signal boundary — ``start_gateway(replace=True)`` on unprovable ownership
|
||||
returns refusal without calling ``terminate_pid`` or writing a takeover
|
||||
marker; the legitimate bound same-home target still reaches the replace
|
||||
flow.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def profile_env(tmp_path, monkeypatch):
|
||||
"""Isolated HERMES_HOME mirroring tests/hermes_cli/test_profiles.py."""
|
||||
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
||||
default_home = tmp_path / ".hermes"
|
||||
default_home.mkdir(exist_ok=True)
|
||||
monkeypatch.setenv("HERMES_HOME", str(default_home))
|
||||
return tmp_home if (tmp_home := default_home) else default_home
|
||||
|
||||
|
||||
def _record(pid=424242, start=111222333, home=None, argv=None):
|
||||
return {
|
||||
"pid": pid,
|
||||
"kind": "hermes-gateway",
|
||||
"argv": argv or ["python", "-m", "hermes_cli.main", "gateway", "run"],
|
||||
"start_time": start,
|
||||
"hermes_home": home,
|
||||
}
|
||||
|
||||
|
||||
class TestRecordAuthority:
|
||||
def test_valid_bound_same_home_record_allows(self, profile_env):
|
||||
from gateway.run import _replace_target_belongs_to_other_profile
|
||||
|
||||
with (
|
||||
patch(
|
||||
"gateway.status._read_pid_record",
|
||||
return_value=_record(home=str(profile_env / ".hermes")),
|
||||
),
|
||||
patch(
|
||||
"gateway.status._get_pid_path",
|
||||
return_value=profile_env / ".hermes" / "gateway.pid",
|
||||
),
|
||||
patch(
|
||||
"gateway.status._get_process_start_time",
|
||||
return_value=111222333,
|
||||
),
|
||||
patch("gateway.status._read_process_cmdline", return_value=None),
|
||||
patch(
|
||||
"gateway.status._get_process_hermes_home",
|
||||
return_value=profile_env / ".hermes",
|
||||
),
|
||||
):
|
||||
assert _replace_target_belongs_to_other_profile(424242) is False
|
||||
|
||||
def test_foreign_home_record_refuses(self, profile_env):
|
||||
"""Exact-home equality: another root/profile in the record refuses,
|
||||
even with a bare argv that substring matching would have passed."""
|
||||
from gateway.run import _replace_target_belongs_to_other_profile
|
||||
|
||||
with (
|
||||
patch(
|
||||
"gateway.status._read_pid_record",
|
||||
return_value=_record(
|
||||
home="/home/other/.hermes/profiles/timothy"
|
||||
),
|
||||
),
|
||||
patch(
|
||||
"gateway.status._get_pid_path",
|
||||
return_value=profile_env / ".hermes" / "gateway.pid",
|
||||
),
|
||||
patch(
|
||||
"gateway.status._get_process_start_time",
|
||||
return_value=111222333,
|
||||
),
|
||||
patch("gateway.status._read_process_cmdline", return_value=None),
|
||||
patch(
|
||||
"gateway.status._get_process_hermes_home",
|
||||
return_value=profile_env / ".hermes" / "profiles" / "tim",
|
||||
),
|
||||
):
|
||||
assert _replace_target_belongs_to_other_profile(424242) is True
|
||||
|
||||
def test_missing_record_refuses(self, profile_env):
|
||||
"""No valid record → ownership unprovable → refuse."""
|
||||
from gateway.run import _replace_target_belongs_to_other_profile
|
||||
|
||||
with (
|
||||
patch("gateway.status._read_pid_record", return_value=None),
|
||||
patch(
|
||||
"gateway.status._get_pid_path",
|
||||
return_value=profile_env / ".hermes" / "gateway.pid",
|
||||
),
|
||||
patch(
|
||||
"gateway.status._get_process_hermes_home",
|
||||
return_value=profile_env / ".hermes",
|
||||
),
|
||||
):
|
||||
assert _replace_target_belongs_to_other_profile(424242) is True
|
||||
|
||||
def test_legacy_record_without_home_refuses(self, profile_env):
|
||||
"""A pre-hermes_home-stamping record cannot prove ownership."""
|
||||
from gateway.run import _replace_target_belongs_to_other_profile
|
||||
|
||||
legacy = _record(home=None)
|
||||
legacy.pop("hermes_home")
|
||||
|
||||
with (
|
||||
patch(
|
||||
"gateway.status._read_pid_record",
|
||||
return_value=legacy,
|
||||
),
|
||||
patch(
|
||||
"gateway.status._get_pid_path",
|
||||
return_value=profile_env / ".hermes" / "gateway.pid",
|
||||
),
|
||||
patch(
|
||||
"gateway.status._get_process_start_time",
|
||||
return_value=111222333,
|
||||
),
|
||||
patch(
|
||||
"gateway.status._get_process_hermes_home",
|
||||
return_value=profile_env / ".hermes",
|
||||
),
|
||||
):
|
||||
assert _replace_target_belongs_to_other_profile(424242) is True
|
||||
|
||||
def test_unbound_record_wrong_pid_refuses(self, profile_env):
|
||||
"""A record naming a DIFFERENT pid proves nothing (poisoned shape)."""
|
||||
from gateway.run import _replace_target_belongs_to_other_profile
|
||||
|
||||
with (
|
||||
patch(
|
||||
"gateway.status._read_pid_record",
|
||||
return_value=_record(pid=999999, home=str(profile_env / ".hermes")),
|
||||
),
|
||||
patch(
|
||||
"gateway.status._get_pid_path",
|
||||
return_value=profile_env / ".hermes" / "gateway.pid",
|
||||
),
|
||||
patch(
|
||||
"gateway.status._get_process_start_time",
|
||||
return_value=111222333,
|
||||
),
|
||||
patch(
|
||||
"gateway.status._get_process_hermes_home",
|
||||
return_value=profile_env / ".hermes",
|
||||
),
|
||||
):
|
||||
assert _replace_target_belongs_to_other_profile(424242) is True
|
||||
|
||||
def test_unbound_record_stale_start_time_refuses(self, profile_env):
|
||||
"""PID reused since the record was written (start_time drift) → the
|
||||
record no longer describes the live process → refuse."""
|
||||
from gateway.run import _replace_target_belongs_to_other_profile
|
||||
|
||||
with (
|
||||
patch(
|
||||
"gateway.status._read_pid_record",
|
||||
return_value=_record(start=1, home=str(profile_env / ".hermes")),
|
||||
),
|
||||
patch(
|
||||
"gateway.status._get_pid_path",
|
||||
return_value=profile_env / ".hermes" / "gateway.pid",
|
||||
),
|
||||
patch(
|
||||
"gateway.status._get_process_start_time",
|
||||
return_value=42,
|
||||
),
|
||||
patch(
|
||||
"gateway.status._get_process_hermes_home",
|
||||
return_value=profile_env / ".hermes",
|
||||
),
|
||||
):
|
||||
assert _replace_target_belongs_to_other_profile(424242) is True
|
||||
|
||||
def test_probe_exception_fails_closed(self, profile_env):
|
||||
from gateway.run import _replace_target_belongs_to_other_profile
|
||||
|
||||
with patch(
|
||||
"gateway.status._read_pid_record",
|
||||
side_effect=RuntimeError("probe exploded"),
|
||||
):
|
||||
assert _replace_target_belongs_to_other_profile(424242) is True
|
||||
|
||||
|
||||
class TestArgvConsistencyCheck:
|
||||
"""Readable argv is a consistency check ONLY — never authority."""
|
||||
|
||||
def test_prefix_collision_is_not_a_conflict(self, profile_env):
|
||||
"""``--profile timothy`` must NOT read as our profile ``tim``:
|
||||
substring matching would have false-allowed the foreign gateway."""
|
||||
from gateway.run import (
|
||||
_looks_like_profile_conflict_from_cmdline as conflict,
|
||||
)
|
||||
|
||||
tim_home = Path("/home/x/.hermes/profiles/tim")
|
||||
# Foreign target advertising timothy — NOT ours.
|
||||
assert (
|
||||
conflict("python -m hermes_cli.main --profile timothy gateway run", tim_home)
|
||||
is True
|
||||
)
|
||||
# Our own exact name stays consistent.
|
||||
assert (
|
||||
conflict("python -m hermes_cli.main --profile tim gateway run", tim_home)
|
||||
is False
|
||||
)
|
||||
assert (
|
||||
conflict("python -m hermes_cli.main -p tim gateway run", tim_home)
|
||||
is False
|
||||
)
|
||||
|
||||
def test_explicit_home_flag_exact_compare(self, profile_env):
|
||||
"""HERMES_HOME= on the argv compares path-exactly, not by prefix."""
|
||||
from gateway.run import (
|
||||
_looks_like_profile_conflict_from_cmdline as conflict,
|
||||
)
|
||||
|
||||
tim_home = Path("/home/x/.hermes/profiles/tim")
|
||||
assert (
|
||||
conflict(
|
||||
"python -m hermes_cli.main HERMES_HOME=/home/x/.hermes/profiles/timothy gateway run",
|
||||
tim_home,
|
||||
)
|
||||
is True
|
||||
)
|
||||
assert (
|
||||
conflict(
|
||||
"python -m hermes_cli.main --hermes-home /home/x/.hermes/profiles/tim/ gateway run",
|
||||
tim_home,
|
||||
)
|
||||
is False # trailing slash normalizes away
|
||||
)
|
||||
|
||||
def test_default_home_refuses_any_named_profile_flag(self):
|
||||
from gateway.run import (
|
||||
_looks_like_profile_conflict_from_cmdline as conflict,
|
||||
)
|
||||
|
||||
root = Path("/home/x/.hermes")
|
||||
assert conflict("python -m x --profile sam run", root) is True
|
||||
assert conflict("python -m x -p sam run", root) is True
|
||||
assert conflict("python -m x run", root) is False
|
||||
|
||||
def test_consistency_contradiction_refuses_even_with_agreeing_record(
|
||||
self, profile_env
|
||||
):
|
||||
"""Record says same-home but the argv explicitly advertises another
|
||||
profile → refuse (argv contradiction wins the conservative call)."""
|
||||
from gateway.run import _replace_target_belongs_to_other_profile
|
||||
|
||||
with (
|
||||
patch(
|
||||
"gateway.status._read_pid_record",
|
||||
return_value=_record(home=str(profile_env / ".hermes")),
|
||||
),
|
||||
patch(
|
||||
"gateway.status._get_pid_path",
|
||||
return_value=profile_env / ".hermes" / "gateway.pid",
|
||||
),
|
||||
patch(
|
||||
"gateway.status._get_process_start_time",
|
||||
return_value=111222333,
|
||||
),
|
||||
patch(
|
||||
"gateway.status._read_process_cmdline",
|
||||
return_value="python -m hermes_cli.main --profile other-profile gateway run",
|
||||
),
|
||||
patch(
|
||||
"gateway.status._get_process_hermes_home",
|
||||
return_value=profile_env / ".hermes",
|
||||
),
|
||||
):
|
||||
assert _replace_target_belongs_to_other_profile(424242) is True
|
||||
|
||||
|
||||
class TestSignalBoundary:
|
||||
"""Integration witness at the destructive boundary (#89315 review req)."""
|
||||
|
||||
def _run_replace(self, agent_patches):
|
||||
from gateway import run as gateway_run
|
||||
|
||||
calls = {"terminate": 0, "marker": 0}
|
||||
|
||||
def _fake_terminate(pid, force=False):
|
||||
calls["terminate"] += 1
|
||||
|
||||
def _fake_marker(pid):
|
||||
calls["marker"] += 1
|
||||
|
||||
base = [
|
||||
patch("gateway.status.get_running_pid", return_value=424242),
|
||||
patch.object(gateway_run, "_replace_target_belongs_to_other_profile"),
|
||||
patch("gateway.status.terminate_pid", side_effect=_fake_terminate),
|
||||
patch("gateway.status.write_takeover_marker", side_effect=_fake_marker),
|
||||
]
|
||||
import contextlib
|
||||
|
||||
with contextlib.ExitStack() as stack:
|
||||
for p in base:
|
||||
stack.enter_context(p)
|
||||
# caller configures the guard mock
|
||||
agent_patches(stack)
|
||||
try:
|
||||
result = asyncio_run(gateway_run.start_gateway(replace=True))
|
||||
except Exception:
|
||||
result = "raised"
|
||||
return result, calls
|
||||
|
||||
def test_unprovable_ownership_never_signals(self, profile_env):
|
||||
"""Unprovable ownership → start_gateway returns False WITHOUT calling
|
||||
terminate_pid or writing a takeover marker."""
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
def configure(stack):
|
||||
guard = stack.enter_context(
|
||||
patch(
|
||||
"gateway.run._replace_target_belongs_to_other_profile",
|
||||
return_value=True,
|
||||
)
|
||||
)
|
||||
return guard
|
||||
|
||||
result, calls = self._run_replace(lambda s: configure(s))
|
||||
|
||||
assert result is False
|
||||
assert calls["terminate"] == 0, (
|
||||
"--replace must not signal a target whose ownership is unproven"
|
||||
)
|
||||
assert calls["marker"] == 0, (
|
||||
"no takeover marker may be written for a refused target"
|
||||
)
|
||||
|
||||
def test_provable_same_home_reaches_replace_flow(self, profile_env):
|
||||
"""Counterpart: bound same-home target still enters the replace flow
|
||||
(terminate attempted) — the fail-closed gate must not disable legit
|
||||
Windows-style replaces."""
|
||||
def configure(stack):
|
||||
stack.enter_context(
|
||||
patch(
|
||||
"gateway.run._replace_target_belongs_to_other_profile",
|
||||
return_value=False,
|
||||
)
|
||||
)
|
||||
stack.enter_context(
|
||||
patch(
|
||||
"gateway.status.get_process_start_time",
|
||||
return_value=111222333,
|
||||
)
|
||||
)
|
||||
stack.enter_context(patch("gateway.run.time.sleep"))
|
||||
|
||||
result, calls = self._run_replace(configure)
|
||||
|
||||
assert calls["terminate"] == 1, (
|
||||
"a provably same-home target must still be replaceable"
|
||||
)
|
||||
|
||||
|
||||
def asyncio_run(coro):
|
||||
import asyncio
|
||||
|
||||
return asyncio.new_event_loop().run_until_complete(_swallow(coro))
|
||||
|
||||
|
||||
async def _swallow(coro):
|
||||
"""Run the coroutine; later machinery (runtime locks etc.) may raise in
|
||||
unit context — callers inspect side-effect counters, not the outcome."""
|
||||
try:
|
||||
return await coro
|
||||
except Exception:
|
||||
return "raised"
|
||||
Reference in New Issue
Block a user