Files
multi-agent-mux/tests/test_issue5_phase4_runtime.py
T
GodopuandClaude Opus 5.5 8b47cc76ee test(issue-17): every test that sources lib.sh does it under its own WORKSPACE_ROOT (review grok, codex)
d198dd5 fixed one helper; the same defect was in eleven more modules
(reviews grok and codex R1/R2): their shells inherited the process
environment, so each `source lib.sh` rewrote <WORKSPACE_ROOT>/.mam/shim/herdr
- the checkout's own when the variable is unset or names this checkout (which
pytest_configure keeps), another workspace's under --noconftest.

- conftest: own_workspace_root, an opt-in fixture setting WORKSPACE_ROOT to
  <tmp_path>/.mam-test-state/workspace. Not autouse: runner.py and
  mqtt_common read WORKSPACE_ROOT in-process, and some tests unset it on
  purpose. mam_sandbox and explicit envs run later and still win.
- pytestmark usefixtures("own_workspace_root") in test_version_consistency,
  test_b19_headless_reconcile_fixes, test_b8_send_keys_verification,
  test_issue6_fail_closed, test_loop_cli, test_issue5_phase3_loop,
  test_issue5_phase4_runtime, test_o3_scoped_guard, test_issue25_plan_reuse,
  test_orc_onboard (their shells inherit os.environ, many with no env at all).
- test_orc_onboard (R2): SCRUBBED_ENV is taken at import and no longer
  carries WORKSPACE_ROOT; every call builds _scrubbed_env(root) with the root
  of the call (ws / cwd / mam_sandbox, else this test's).
- test_workspace_scope (unittest) and test_issue21_worktree_prereqs._mutate
  pin the root they already create.

Tests (test_issue17_workspace_root_isolation.py): W1 copies the checkout and
runs one test of each of those modules there with WORKSPACE_ROOT naming the
copy - the copy gets no .mam; W2 imports test_orc_onboard under a sentinel
root, then calls it under another - the sentinel stays empty. At d198dd5 both
fail (the copy gets .mam/shim/herdr; the sentinel gets it).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HCGpJXhSZasJLU38ksDghe
2026-10-09 00:22:54 +09:00

192 lines
7.0 KiB
Python

"""Issue #5 Phase 4 — Immutable Runtime Verification & Freeze Removal.
Verifies:
- P4-01: Eager import completeness, bytecode inhibition (PYTHONDONTWRITEBYTECODE=1),
and MAM_VERSION protocol sync.
- P4-02: Mid-flight on-disk code modification immunity (B-13 bug prevention).
- P4-03: Freeze bypass under Python mode (default) and legacy fallback preservation.
"""
from __future__ import annotations
import glob
import os
import subprocess
import sys
import tempfile
from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).resolve().parent.parent
SKILLS = REPO_ROOT / ".agents" / "skills"
if str(SKILLS) not in sys.path:
sys.path.insert(0, str(SKILLS))
from lib_py.loop import eager, parser, checkpoint, runner
# Issue #17: shells started here inherit a WORKSPACE_ROOT of this test's own
# (lib.sh would otherwise rewrite the checkout's .mam/shim/herdr).
pytestmark = pytest.mark.usefixtures("own_workspace_root")
def test_p4_01_eager_import_completeness_and_bytecode_inhibition(monkeypatch):
"""P4-01: Verify all core framework modules are eagerly loaded and bytecode writes disabled."""
loaded = eager.eager_load()
assert eager.is_eager_loaded()
for mod in eager.CORE_MODULES:
assert mod in sys.modules, f"Module {mod} was not eagerly loaded into sys.modules"
# Verify bytecode inhibition
assert sys.dont_write_bytecode is True
assert os.environ.get("PYTHONDONTWRITEBYTECODE") == "1"
# Verify framework version constant sync. The literal value is not pinned
# here: tests/test_version_consistency.py owns the lockstep between
# eager.FRAMEWORK_VERSION, lib.sh's MAM_VERSION, VERSIONS.md and the eight
# SKILL.md files, so a release bump must not have to edit this file too.
eager.verify_framework_version(eager.FRAMEWORK_VERSION)
# Verify mismatch raises RuntimeError
monkeypatch.setenv("MAM_VERSION", "9.9.9")
with pytest.raises(RuntimeError, match="MAM_VERSION mismatch"):
eager.verify_framework_version()
def test_p4_01_loop_runner_instantiation_triggers_eager_load():
"""P4-01: LoopRunner.__init__ must eagerly load all modules immediately."""
lr = runner.LoopRunner(["--creator", "test-agent", "--task", "test"])
assert eager.is_eager_loaded()
assert lr.creator == "" # Not parsed until parse_args()
def test_p4_02_midflight_code_replacement_stress(tmp_path):
"""P4-02: Simulates B-13 mid-flight disk corruption.
A running Python loop process that has eagerly loaded its modules must continue
executing correctly across iterations even if underlying source files on disk
are overwritten with syntax-breaking garbage.
"""
# Create a sandbox directory with a copy of a worker module
sandbox = tmp_path / "sandbox"
sandbox.mkdir()
mod_file = sandbox / "dummy_worker.py"
mod_file.write_text(
"def compute_verdict(val: int) -> str:\n"
" return f'PASS (score={val})'\n",
encoding="utf-8"
)
runner_script = tmp_path / "run_subproc.py"
script_content = (
"import sys, os\n"
f"sys.path.insert(0, r'{SKILLS}')\n"
f"sys.path.insert(0, r'{sandbox}')\n"
"from lib_py.loop import eager, parser, checkpoint\n"
"import dummy_worker\n"
"eager.eager_load()\n"
"print('READY', flush=True)\n"
"line = sys.stdin.readline()\n"
"# Invoke functions after disk file has been corrupted\n"
"res1 = dummy_worker.compute_verdict(42)\n"
"verdict = parser.parse_verdict('Review output\\nVERDICT: PASS')\n"
"chk_path = sys.stdin.readline().strip()\n"
"checkpoint.save_checkpoint(chk_path, {'status': 'OK', 'res': res1, 'verdict': verdict})\n"
"loaded = checkpoint.load_checkpoint(chk_path)\n"
"assert loaded['status'] == 'OK'\n"
"print('FINISHED_SUCCESSFULLY', flush=True)\n"
)
runner_script.write_text(script_content, encoding="utf-8")
py_bin = sys.executable
proc = subprocess.Popen(
[py_bin, str(runner_script)],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
# Wait for process to eagerly load and signal READY
ready_line = proc.stdout.readline()
assert ready_line.strip() == "READY", f"Child failed to start: {proc.stderr.read()}"
# --- B-13 Corruption Phase ---
# While process is suspended waiting on stdin, corrupt dummy_worker.py on disk
mod_file.write_text("INVALID SYNTAX !!!\n unexpected EOF def broken():\n", encoding="utf-8")
# Tell child process to proceed
chk_target = tmp_path / "checkpoint.json"
proc.stdin.write(f"PROCEED\n{chk_target}\n")
proc.stdin.flush()
out_line = proc.stdout.readline()
assert out_line.strip() == "FINISHED_SUCCESSFULLY", f"Child error: {proc.stderr.read()}"
proc.communicate(timeout=5)
assert proc.returncode == 0
assert chk_target.exists()
def test_p4_03_freeze_bypass_by_default(tmp_path):
"""P4-03: Under MAM_LOOP_PY=1 (default), run_loop.sh must bypass /tmp/mam-loop-freeze.*."""
run_loop_sh = SKILLS / "multi-agent-mux-loop" / "scripts" / "run_loop.sh"
env = os.environ.copy()
env.pop("MAM_LOOP_FREEZE_DIR", None)
env.pop("MAM_LOOP_FREEZE", None)
env.pop("MAM_LOOP_PY", None) # defaults to 1
# Snapshot current freeze dirs in /tmp
before_dirs = set(glob.glob(f"{tempfile.gettempdir()}/mam-loop-freeze.*"))
res = subprocess.run(
["bash", str(run_loop_sh), "--help"],
capture_output=True,
text=True,
env=env,
cwd=str(REPO_ROOT),
)
assert res.returncode == 1
assert "Usage: " in res.stdout
after_dirs = set(glob.glob(f"{tempfile.gettempdir()}/mam-loop-freeze.*"))
new_dirs = after_dirs - before_dirs
assert len(new_dirs) == 0, f"Freeze dir was unexpectedly created: {new_dirs}"
def test_p4_03_explicit_freeze_fallback(tmp_path):
"""P4-03: Setting MAM_LOOP_FREEZE=1 or MAM_LOOP_PY=0 restores freeze snapshot logic."""
run_loop_sh = SKILLS / "multi-agent-mux-loop" / "scripts" / "run_loop.sh"
# 1. Test MAM_LOOP_FREEZE=1
env_freeze = os.environ.copy()
env_freeze["MAM_LOOP_FREEZE"] = "1"
env_freeze.pop("MAM_LOOP_FREEZE_DIR", None)
res = subprocess.run(
["bash", str(run_loop_sh), "--help"],
capture_output=True,
text=True,
env=env_freeze,
cwd=str(REPO_ROOT),
)
assert res.returncode == 1
assert "Usage: " in res.stdout
# 2. Test MAM_LOOP_NO_FREEZE=1 overrides MAM_LOOP_FREEZE=1
env_no_freeze = os.environ.copy()
env_no_freeze["MAM_LOOP_FREEZE"] = "1"
env_no_freeze["MAM_LOOP_NO_FREEZE"] = "1"
env_no_freeze.pop("MAM_LOOP_FREEZE_DIR", None)
before_dirs = set(glob.glob(f"{tempfile.gettempdir()}/mam-loop-freeze.*"))
res = subprocess.run(
["bash", str(run_loop_sh), "--help"],
capture_output=True,
text=True,
env=env_no_freeze,
cwd=str(REPO_ROOT),
)
assert res.returncode == 1
after_dirs = set(glob.glob(f"{tempfile.gettempdir()}/mam-loop-freeze.*"))
assert len(after_dirs - before_dirs) == 0