refactor(issue-5): move delegation scripts into lib_py.delegation with package-bound shims
The four delegation scripts become a real package. Their old paths stay as
shims because those paths are a wire contract: agent instructions, the bash
wrapper, SKILL.md, BOOTSTRAP.md, deploy/install.sh and three test modules all
call them literally.
The shim binds the lib_py.delegation *package* to the tree beside it, once per
process, and never touches sys.path. Two cheaper designs were rejected with
evidence (the context matrix now lives in the test suite):
* inserting the sibling skills dir at sys.path[0] makes the first sandbox
copy imported under pytest shadow lib_py.agents/layout/state for the whole
session, and the identity assertion cannot see it -- `is` proves same
object, not same tree.
* loading only the named module by file path leaves a module that imports
siblings pulling them from whichever tree is on the path, so a frozen
publish_event ran against live dependencies and lost half of B-13.
Binding the package fixes both: one tree for all delegation modules, the parent
lib_py untouched, and a sys.path append only when no lib_py exists anywhere
(true standalone). Bind-once rather than nearest-wins, because rebinding would
split existing references and break monkeypatching.
Also moves the env-file load off import (_load_dotenv -> ensure_env_loaded,
called by each process entry point including reconcile.sh's embedded
subscriber), so importing the library no longer mutates os.environ.
Verified: PV-1 identity+provenance, PV-4's ten import scenarios plus K1/K3/K6
in fresh interpreters, PV-5 shim byte-identity, an AST check that the package
imports no sibling lib_py module, and --help output diffed against the
pre-move capture.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UrDNJkHmm18uu7n7JwV5Qd
This commit is contained in:
1 parent
265a1f6c3d
commit
fb3498e23b
12 files changed
+2187
-1656
No files matched your search
+126
@@ -0,0 +1,126 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Shim context harness (PV-4, issue #5 phase 1).
|
||||
|
||||
Runs ONE import scenario in a fresh interpreter and prints a JSON report of
|
||||
which tree every relevant module resolved to. Kept out of the test module on
|
||||
purpose: each scenario needs a virgin ``sys.modules``, so it must be a
|
||||
subprocess.
|
||||
|
||||
Usage: shim_contexts.py <scenario> <real> <sandbox> <sandbox2> <freeze>
|
||||
|
||||
Tree arguments are skills-directory roots, each containing ``lib_py/`` and
|
||||
``multi-agent-mux-delegate-job/scripts/``. The scenarios mirror the four real
|
||||
execution contexts: pytest in-process (A/A_pe/B/B2/D/E), standalone with no
|
||||
ambient tree (C1/C1_pe), and a frozen copy invoked while PYTHONPATH still points
|
||||
at the live tree (C2/C2_pe) -- the shape every delegated agent job actually runs.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
SCENARIO = sys.argv[1]
|
||||
TREES = {
|
||||
"real": os.path.abspath(sys.argv[2]),
|
||||
"sandbox": os.path.abspath(sys.argv[3]),
|
||||
"sandbox2": os.path.abspath(sys.argv[4]),
|
||||
"freeze": os.path.abspath(sys.argv[5]),
|
||||
}
|
||||
|
||||
|
||||
def scripts(tree):
|
||||
return os.path.join(TREES[tree], "multi-agent-mux-delegate-job", "scripts")
|
||||
|
||||
|
||||
def which_tree(path):
|
||||
if not path:
|
||||
return None
|
||||
path = os.path.abspath(path)
|
||||
for name, root in TREES.items():
|
||||
if path == root or path.startswith(root + os.sep):
|
||||
return name
|
||||
return "OUTSIDE:" + path
|
||||
|
||||
|
||||
def report():
|
||||
out = {"scenario": SCENARIO}
|
||||
mods = sys.modules
|
||||
|
||||
def note(key, mod, attr="__file__"):
|
||||
m = mods.get(mod)
|
||||
if m is None:
|
||||
out[key] = None
|
||||
return
|
||||
if attr == "__path__":
|
||||
out[key] = which_tree(list(getattr(m, "__path__", [None]))[0])
|
||||
else:
|
||||
out[key] = which_tree(getattr(m, attr, None))
|
||||
|
||||
note("alias_mqtt_common", "mqtt_common")
|
||||
note("alias_publish_event", "publish_event")
|
||||
note("pkg_delegation", "lib_py.delegation", "__path__")
|
||||
note("pkg_mqtt_common", "lib_py.delegation.mqtt_common")
|
||||
note("pkg_publish_event", "lib_py.delegation.publish_event")
|
||||
note("lib_py", "lib_py", "__path__")
|
||||
note("lib_py_agents", "lib_py.agents", "__path__")
|
||||
|
||||
# identity: the alias and the package attribute must be one object
|
||||
ident = {}
|
||||
for name in ("mqtt_common", "publish_event"):
|
||||
alias = mods.get(name)
|
||||
pkg_mod = mods.get("lib_py.delegation." + name)
|
||||
ident[name] = None if alias is None else (alias is pkg_mod)
|
||||
out["identity"] = ident
|
||||
|
||||
# PV-3 shape: sys.path entries that would contribute a lib_py package
|
||||
out["lib_py_path_entries"] = [
|
||||
which_tree(e) for e in sys.path
|
||||
if e and os.path.isdir(os.path.join(e, "lib_py"))
|
||||
]
|
||||
return out
|
||||
|
||||
|
||||
if SCENARIO in ("A", "A_pe", "B", "B2", "D", "E"):
|
||||
sys.path.insert(0, TREES["real"]) # models conftest.py inserting the real tree
|
||||
|
||||
if SCENARIO == "A":
|
||||
sys.path.insert(0, scripts("sandbox"))
|
||||
import mqtt_common, publish_event # noqa: F401
|
||||
elif SCENARIO == "A_pe":
|
||||
sys.path.insert(0, scripts("sandbox"))
|
||||
import publish_event # noqa: F401
|
||||
elif SCENARIO == "B":
|
||||
import lib_py.agents # noqa: F401
|
||||
sys.path.insert(0, scripts("sandbox"))
|
||||
import mqtt_common, publish_event # noqa: F401
|
||||
elif SCENARIO == "B2":
|
||||
import lib_py.delegation.mqtt_common # noqa: F401
|
||||
sys.path.insert(0, scripts("sandbox"))
|
||||
import mqtt_common, publish_event # noqa: F401
|
||||
elif SCENARIO == "D":
|
||||
sys.path.insert(0, scripts("sandbox"))
|
||||
import mqtt_common # noqa: F401
|
||||
sys.path.insert(0, scripts("sandbox2"))
|
||||
import publish_event # noqa: F401
|
||||
elif SCENARIO == "E":
|
||||
sys.path.insert(0, scripts("sandbox"))
|
||||
import mqtt_common # noqa: F401
|
||||
from lib_py.delegation import publish_event # noqa: F401
|
||||
elif SCENARIO == "C1":
|
||||
sys.path.insert(0, scripts("freeze"))
|
||||
import mqtt_common, publish_event # noqa: F401
|
||||
elif SCENARIO == "C1_pe":
|
||||
sys.path.insert(0, scripts("freeze"))
|
||||
import publish_event # noqa: F401
|
||||
elif SCENARIO == "C2":
|
||||
sys.path.insert(0, scripts("freeze")) # PYTHONPATH=real set by caller
|
||||
import mqtt_common, publish_event # noqa: F401
|
||||
elif SCENARIO == "C2_pe":
|
||||
sys.path.insert(0, scripts("freeze")) # PYTHONPATH=real set by caller
|
||||
import publish_event # noqa: F401
|
||||
elif SCENARIO == "K3":
|
||||
sys.path.insert(0, scripts("real"))
|
||||
import mqtt_common, publish_event # noqa: F401
|
||||
else:
|
||||
raise SystemExit("unknown scenario: " + SCENARIO)
|
||||
|
||||
print(json.dumps(report()))
|
||||
@@ -17,6 +17,7 @@ Sections
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import signal
|
||||
@@ -390,3 +391,219 @@ def test_reconcile_terminal_mutation_literals_are_frozen():
|
||||
"if _event in ('completed', 'cancelled')",
|
||||
):
|
||||
assert literal in text, literal
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# 4. P1-01 — package binding, provenance and context matrix (PV-1, PV-4, PV-5)
|
||||
# ==============================================================================
|
||||
|
||||
DELEGATION_MODULES = ("mqtt_common", "registry", "publish_event", "job_subscriber")
|
||||
PKG_DIR = SKILLS / "lib_py" / "delegation"
|
||||
SHIM_DIR = SKILLS / "multi-agent-mux-delegate-job" / "scripts"
|
||||
|
||||
|
||||
def test_pv5_all_four_shims_share_one_bootstrap_block():
|
||||
"""PV-5: the four shims differ only by the module name they bind."""
|
||||
normalised = {}
|
||||
for mod in DELEGATION_MODULES:
|
||||
text = (SHIM_DIR / (mod + ".py")).read_text(encoding="utf-8")
|
||||
normalised[mod] = text.replace(mod, "{mod}")
|
||||
base = normalised["mqtt_common"]
|
||||
for mod, text in normalised.items():
|
||||
assert text == base, "shim {} diverged from the shared bootstrap".format(mod)
|
||||
assert "sys.path.insert" not in base, "the shim must never take sys.path precedence"
|
||||
assert "sys.path.append(_SKILLS)" in base, "standalone fallback must remain"
|
||||
|
||||
|
||||
def test_pv1_identity_and_provenance_of_every_alias(mam_sandbox):
|
||||
"""PV-1: each alias IS the package module, and all four share one tree."""
|
||||
import lib_py.delegation
|
||||
|
||||
paths = set()
|
||||
for mod in DELEGATION_MODULES:
|
||||
alias = _import_delegation(mam_sandbox, mod)
|
||||
pkg_mod = getattr(lib_py.delegation, mod, None)
|
||||
if pkg_mod is None:
|
||||
import importlib
|
||||
pkg_mod = importlib.import_module("lib_py.delegation." + mod)
|
||||
assert alias is pkg_mod, "{}: alias is not the package module".format(mod)
|
||||
paths.add(str(Path(alias.__file__).resolve().parent))
|
||||
|
||||
assert len(paths) == 1, "delegation modules span multiple trees: {}".format(paths)
|
||||
bound = Path(paths.pop())
|
||||
real = PKG_DIR.resolve()
|
||||
basetemp_ok = "pytest-" in str(bound)
|
||||
assert bound == real or basetemp_ok, (
|
||||
"delegation bound to an unexpected tree: {}".format(bound)
|
||||
)
|
||||
# the parent package must never be dragged along with it
|
||||
assert Path(lib_py.__file__).resolve().parent == (SKILLS / "lib_py").resolve()
|
||||
|
||||
|
||||
def test_delegation_package_imports_no_sibling_lib_py_modules():
|
||||
"""D1(b): a frozen delegation package must not reach into the parent tree."""
|
||||
import ast
|
||||
|
||||
offenders = []
|
||||
for path in sorted(PKG_DIR.glob("*.py")):
|
||||
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
|
||||
for node in ast.walk(tree):
|
||||
names = []
|
||||
if isinstance(node, ast.Import):
|
||||
names = [a.name for a in node.names]
|
||||
elif isinstance(node, ast.ImportFrom) and node.module:
|
||||
names = [node.module]
|
||||
for name in names:
|
||||
if name in DELEGATION_MODULES:
|
||||
offenders.append("{}:{} top-level `{}`".format(path.name, node.lineno, name))
|
||||
if name == "lib_py" or (
|
||||
name.startswith("lib_py.") and not name.startswith("lib_py.delegation")
|
||||
):
|
||||
offenders.append("{}:{} sibling `{}`".format(path.name, node.lineno, name))
|
||||
assert not offenders, "forbidden imports inside lib_py.delegation:\n " + "\n ".join(offenders)
|
||||
|
||||
|
||||
def test_importing_delegation_does_not_mutate_environment(mam_sandbox):
|
||||
"""D6: importing the library must leave os.environ alone; env loads at entry points."""
|
||||
import importlib
|
||||
mqtt_common = _import_delegation(mam_sandbox, "mqtt_common")
|
||||
before = set(os.environ)
|
||||
importlib.reload(mqtt_common)
|
||||
assert set(os.environ) == before, "library import injected environment variables"
|
||||
# idempotent: the second call is a no-op
|
||||
mqtt_common.ensure_env_loaded()
|
||||
keys = set(os.environ)
|
||||
mqtt_common.ensure_env_loaded()
|
||||
assert set(os.environ) == keys
|
||||
|
||||
|
||||
def test_shim_help_output_matches_pre_move_capture():
|
||||
"""The shim must be indistinguishable from the pre-move script at the CLI."""
|
||||
for mod in ("publish_event", "registry", "job_subscriber"):
|
||||
golden = (FIXTURES / "help-{}.txt".format(mod)).read_text(encoding="utf-8")
|
||||
res = subprocess.run(
|
||||
[sys.executable, str(SHIM_DIR / (mod + ".py")), "--help"],
|
||||
capture_output=True, text=True, timeout=60,
|
||||
)
|
||||
assert res.returncode == 0, res.stderr
|
||||
assert res.stdout + res.stderr == golden, "--help drifted for {}".format(mod)
|
||||
|
||||
|
||||
# --- PV-4: the context matrix, each scenario in a fresh interpreter ------------
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def shim_trees(tmp_path_factory):
|
||||
"""Four copies of the live skills tree: real / sandbox / sandbox2 / freeze."""
|
||||
import shutil
|
||||
|
||||
root = tmp_path_factory.mktemp("shim_trees")
|
||||
for name in ("real", "sandbox", "sandbox2", "freeze"):
|
||||
dest = root / name
|
||||
(dest / "multi-agent-mux-delegate-job").mkdir(parents=True)
|
||||
shutil.copytree(SKILLS / "lib_py", dest / "lib_py")
|
||||
shutil.copytree(SHIM_DIR, dest / "multi-agent-mux-delegate-job" / "scripts")
|
||||
return root
|
||||
|
||||
|
||||
def _run_scenario(shim_trees, scenario, pythonpath=None):
|
||||
env = dict(os.environ)
|
||||
env.pop("PYTHONPATH", None)
|
||||
if pythonpath:
|
||||
env["PYTHONPATH"] = str(shim_trees / pythonpath)
|
||||
res = subprocess.run(
|
||||
[sys.executable, str(FIXTURES / "shim_contexts.py"), scenario]
|
||||
+ [str(shim_trees / t) for t in ("real", "sandbox", "sandbox2", "freeze")],
|
||||
capture_output=True, text=True, env=env, timeout=120,
|
||||
)
|
||||
assert res.returncode == 0, "scenario {} failed:\n{}".format(scenario, res.stderr)
|
||||
return json.loads(res.stdout)
|
||||
|
||||
|
||||
def _delegation_trees(report):
|
||||
keys = ("alias_mqtt_common", "alias_publish_event", "pkg_delegation",
|
||||
"pkg_mqtt_common", "pkg_publish_event")
|
||||
return {report[k] for k in keys if report[k] is not None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("scenario,expected_delegation", [
|
||||
("A", "sandbox"), # sandbox shim binds first
|
||||
("A_pe", "sandbox"), # non-leaf module alone: dependencies must follow it
|
||||
("B", "sandbox"), # real lib_py.agents already imported: delegation still binds to shim tree
|
||||
("B2", "real"), # real delegation already bound: bind-once keeps it
|
||||
("D", "sandbox"), # a second sandbox must not rebind
|
||||
("E", "sandbox"), # direct package import after a shim resolves to the bound tree
|
||||
])
|
||||
def test_pv4_pytest_family_binds_one_tree_and_spares_the_parent(
|
||||
shim_trees, scenario, expected_delegation
|
||||
):
|
||||
rep = _run_scenario(shim_trees, scenario)
|
||||
assert _delegation_trees(rep) == {expected_delegation}, rep
|
||||
assert rep["lib_py"] == "real", "parent lib_py must stay on the real tree: {}".format(rep)
|
||||
if rep["lib_py_agents"] is not None:
|
||||
assert rep["lib_py_agents"] == "real", rep
|
||||
for name, same in rep["identity"].items():
|
||||
if same is not None:
|
||||
assert same, "identity broken for {} in {}".format(name, scenario)
|
||||
assert "sandbox" not in rep["lib_py_path_entries"], rep
|
||||
assert "sandbox2" not in rep["lib_py_path_entries"], rep
|
||||
|
||||
|
||||
@pytest.mark.parametrize("scenario", ["C1", "C1_pe"])
|
||||
def test_pv4_standalone_bootstraps_from_its_own_tree(shim_trees, scenario):
|
||||
"""K4: no ambient lib_py anywhere, so the shim may append its own tree."""
|
||||
rep = _run_scenario(shim_trees, scenario)
|
||||
assert _delegation_trees(rep) == {"freeze"}, rep
|
||||
assert rep["lib_py"] == "freeze", rep
|
||||
|
||||
|
||||
@pytest.mark.parametrize("scenario", ["C2", "C2_pe"])
|
||||
def test_pv4_frozen_copy_wins_over_a_live_pythonpath(shim_trees, scenario):
|
||||
"""K2: the shape every delegated job runs -- frozen script, live PYTHONPATH.
|
||||
|
||||
B-13 requires the frozen code to execute; the parent lib_py may stay live
|
||||
because D1(b) forbids the delegation package from importing siblings."""
|
||||
rep = _run_scenario(shim_trees, scenario, pythonpath="real")
|
||||
assert _delegation_trees(rep) == {"freeze"}, rep
|
||||
assert rep["lib_py"] == "real", rep
|
||||
for name, same in rep["identity"].items():
|
||||
if same is not None:
|
||||
assert same, name
|
||||
|
||||
|
||||
def test_pv4_k3_live_helpers_resolve_to_the_live_tree(shim_trees):
|
||||
rep = _run_scenario(shim_trees, "K3")
|
||||
assert _delegation_trees(rep) == {"real"}, rep
|
||||
assert rep["lib_py"] == "real", rep
|
||||
|
||||
|
||||
def test_k1_module_execution_follows_pythonpath(shim_trees):
|
||||
"""K1: `python -m lib_py.delegation.<mod>` is resolved by PYTHONPATH, not by a shim."""
|
||||
for tree in ("real", "freeze"):
|
||||
env = dict(os.environ)
|
||||
env["PYTHONPATH"] = str(shim_trees / tree)
|
||||
res = subprocess.run(
|
||||
[sys.executable, "-c",
|
||||
"import lib_py.delegation as d; print(list(d.__path__)[0])"],
|
||||
capture_output=True, text=True, env=env, timeout=60,
|
||||
)
|
||||
assert res.returncode == 0, res.stderr
|
||||
assert res.stdout.strip() == str(shim_trees / tree / "lib_py" / "delegation"), res.stdout
|
||||
|
||||
|
||||
def test_k6_subprocess_executes_the_copy_it_was_given(shim_trees):
|
||||
"""K6: e2e-style subprocess execution runs the tree the path points at."""
|
||||
env = dict(os.environ)
|
||||
env.pop("PYTHONPATH", None)
|
||||
shim = shim_trees / "sandbox" / "multi-agent-mux-delegate-job" / "scripts" / "publish_event.py"
|
||||
res = subprocess.run([sys.executable, str(shim), "--help"],
|
||||
capture_output=True, text=True, env=env, timeout=60)
|
||||
assert res.returncode == 0, res.stderr
|
||||
probe = subprocess.run(
|
||||
[sys.executable, "-c",
|
||||
"import sys; sys.path.insert(0, {!r}); import publish_event;"
|
||||
" print(publish_event.__file__)".format(str(shim.parent))],
|
||||
capture_output=True, text=True, env=env, timeout=60,
|
||||
)
|
||||
assert probe.returncode == 0, probe.stderr
|
||||
assert str(shim_trees / "sandbox") in probe.stdout, probe.stdout
|
||||
|
||||
Reference in new issue
Block a user