fix(issue-5): address phase 1 cross-review findings (R1, R2, F-1)
Three reviewers examined fc0bd96..8def235. Codex and Claude each reproduced a defect; all three findings were re-verified here before being accepted, and none were rebutted. R1 (codex) — the shell helpers ran `python -m lib_py.delegation.cli`. For -m, sys.path[0] is the working directory, which therefore beats PYTHONPATH: with cwd inside a tree containing lib_py, the live package won even with the frozen path listed first. The documented B-13 guarantee for this path did not hold. Reproduced, then replaced with an explicit bootstrap that inserts SKILL_DIR itself. cwd is left alone, because the registry and audit-log paths are relative to it. The file-based shim path was never affected. R2 (codex) — the persistent connection was reused on host and port alone. Two jobs sharing an endpoint but carrying different credentials or TLS settings silently published over the first connection. Reproduced (the client factory ran once, under the first account, with TLS off). The guard now compares an immutable snapshot of the whole connection identity and names only the differing field, never its value. F-1 (claude) — paho queues a publish issued while disconnected and raises RuntimeError from wait_for_publish. RuntimeError is deliberately outside the retry tuple, so the one state a persistent connection exists to survive, the gap between a drop and paho's reconnect, failed on the first attempt. Confirmed against paho 2.1.0. That single condition is now translated into a retryable error; the tuple is unchanged, so genuine programming errors are still not retried. Two fixtures were hiding these: FakeInfo had no rc and never raised. It now mirrors paho's contract, which is what makes the new tests meaningful. Also folds in the duplication both Claude (O-1) and I had flagged: the CLI path kept its own copy of connect-publish-disconnect plus its own timeout constants. _publish_once now delegates to OneShotPublisher.publish_raw, so a transport fix lands once. Having two copies is exactly why these bugs existed in one of them. Documentation corrected rather than defended: the benchmark's rationale for keeping three forks was imprecise (the || true already guarantees rc 0; the lookup buys quietness and legacy parity), SKILL.md no longer claims PYTHONPATH decides the frozen path, and the daemon's directory creation is recorded as a deliberate difference. Trailing whitespace inherited by the relocated file is stripped, so the commit range is now clean too. Verified: each regression fails against the pre-fix code and passes after (R1 1, R2 4, F-1 1). 66 phase-1 tests pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UrDNJkHmm18uu7n7JwV5Qd
This commit is contained in:
1 parent
8def235dc7
commit
95f3e9a427
9 files changed
+344
-66
No files matched your search
@@ -1000,3 +1000,209 @@ def test_persistent_publisher_reuses_a_healthy_connection_across_events():
|
||||
publisher.close()
|
||||
assert calls["n"] == 1, "a healthy connection must be reused"
|
||||
assert calls["publish"] == 4
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# 8. Cross-review regressions (job 60c980ac)
|
||||
#
|
||||
# R1 (codex): `python -m` puts the working directory ahead of PYTHONPATH, so a
|
||||
# frozen loop whose cwd contained a lib_py package silently ran live
|
||||
# code. The shell helpers now use an explicit bootstrap instead.
|
||||
# R2 (codex): the persistent connection was reused on host/port alone, ignoring
|
||||
# credentials and TLS.
|
||||
# F-1 (claude): paho queues a publish issued while disconnected and then raises
|
||||
# RuntimeError, which sat outside the retry tuple -- so the one state
|
||||
# a persistent connection exists to survive failed on first attempt.
|
||||
# ==============================================================================
|
||||
|
||||
def _decoy_tree(root: Path) -> Path:
|
||||
"""A directory that looks like a skills tree and screams if it is imported."""
|
||||
pkg = root / "lib_py" / "delegation"
|
||||
pkg.mkdir(parents=True)
|
||||
(root / "lib_py" / "__init__.py").write_text("", encoding="utf-8")
|
||||
(pkg / "__init__.py").write_text("", encoding="utf-8")
|
||||
(pkg / "cli.py").write_text(
|
||||
"def main(argv=None):\n"
|
||||
" print('DECOY-TREE-EXECUTED')\n"
|
||||
" return 0\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
return root
|
||||
|
||||
|
||||
def test_r1_shell_helper_ignores_a_lib_py_in_the_working_directory(mam_sandbox, tmp_path):
|
||||
"""R1: the helper must run its own tree even when cwd provides lib_py.
|
||||
|
||||
This is the frozen-loop shape: a decoy package sitting in the working
|
||||
directory must not win over the tree the sourced lib.sh belongs to.
|
||||
"""
|
||||
decoy = _decoy_tree(tmp_path / "decoy_ws")
|
||||
(decoy / ".mam" / "jobs").mkdir(parents=True)
|
||||
|
||||
lib_sh = _sandbox_skills(mam_sandbox) / "lib.sh"
|
||||
env = dict(os.environ)
|
||||
env.pop("HERDR_SESSION_NAME", None)
|
||||
env.pop("PYTHONPATH", None)
|
||||
env["MAM_ENV_FILE"] = str(Path(mam_sandbox) / ".mam.env.absent")
|
||||
|
||||
res = subprocess.run(
|
||||
["bash", "-c",
|
||||
'cd "{}" && source "{}" >/dev/null 2>&1 && '
|
||||
'delegate_submit_job "r1 probe" claude-code herdr:claude'.format(decoy, lib_sh)],
|
||||
capture_output=True, text=True, env=env, timeout=120,
|
||||
)
|
||||
assert "DECOY-TREE-EXECUTED" not in res.stdout, (
|
||||
"the working directory's lib_py shadowed the helper's own tree:\n" + res.stdout
|
||||
)
|
||||
assert res.returncode == 0, res.stderr
|
||||
job_id = res.stdout.strip()
|
||||
assert re.fullmatch(r"[0-9a-f]+", job_id), repr(res.stdout)
|
||||
assert (decoy / ".mam" / "jobs" / (job_id + ".json")).exists(), (
|
||||
"the real cli should have registered the job in the cwd registry"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("field,first,second", [
|
||||
("username", "account-a", "account-b"),
|
||||
("password", "secret-a", "secret-b"),
|
||||
("tls", False, True),
|
||||
("ca_certs", None, "/tmp/ca.pem"),
|
||||
])
|
||||
def test_r2_persistent_publisher_refuses_a_different_connection_identity(field, first, second):
|
||||
"""R2: same endpoint is not the same connection when auth or TLS differ."""
|
||||
from lib_py.delegation import mqtt_common, publish_event
|
||||
from lib_py.delegation.publisher import PersistentPublisher
|
||||
|
||||
built = []
|
||||
|
||||
def factory(_role, config):
|
||||
built.append(getattr(config, field))
|
||||
from lib_py.delegation.bench import FakeClient
|
||||
client = FakeClient(connect_latency=0.0, counters={})
|
||||
return client
|
||||
|
||||
def prepared(value):
|
||||
config = mqtt_common.broker_config_from_env(
|
||||
{"host": "same.example", "port": 1883, field: value})
|
||||
return publish_event.PreparedEvent(
|
||||
"j", "progress", "d", 1, "t", False, {}, b"{}", config)
|
||||
|
||||
publisher = PersistentPublisher(client_factory=factory)
|
||||
try:
|
||||
ok, _err = publisher.publish_with_retry(prepared(first), attempts=1)
|
||||
assert ok is True
|
||||
ok2, err2 = publisher.publish_with_retry(prepared(second), attempts=1)
|
||||
finally:
|
||||
publisher.close()
|
||||
|
||||
assert ok2 is False, "an event with a different {} must not reuse the connection".format(field)
|
||||
assert field in err2, err2
|
||||
assert str(second) not in err2, "the error must name the field, never its value"
|
||||
assert built == [first], "the second event must not have been published on the first client"
|
||||
|
||||
|
||||
def test_r2_identical_configuration_still_reuses_the_connection():
|
||||
"""The guard must not become so strict that reuse never happens."""
|
||||
from lib_py.delegation.publisher import PersistentPublisher
|
||||
|
||||
counters = {"n": 0}
|
||||
|
||||
def factory(_role, _config):
|
||||
counters["n"] += 1
|
||||
from lib_py.delegation.bench import FakeClient
|
||||
return FakeClient(connect_latency=0.0, counters={})
|
||||
|
||||
publisher = PersistentPublisher(client_factory=factory)
|
||||
try:
|
||||
for _ in range(3):
|
||||
assert publisher.publish_with_retry(_prepared(), attempts=1) == (True, None)
|
||||
finally:
|
||||
publisher.close()
|
||||
assert counters["n"] == 1
|
||||
|
||||
|
||||
class _FlakyConnectionClient(object):
|
||||
"""paho-shaped: the first N publishes are issued while disconnected."""
|
||||
|
||||
def __init__(self, no_conn_times, calls):
|
||||
import paho.mqtt.client as mqtt
|
||||
self._mqtt = mqtt
|
||||
self._no_conn_times = no_conn_times
|
||||
self._calls = calls
|
||||
self.on_connect = None
|
||||
|
||||
def connect(self, host, port, keepalive=60):
|
||||
self._calls["connect"] = self._calls.get("connect", 0) + 1
|
||||
self.on_connect(self, None, None, 0, None)
|
||||
|
||||
def loop_start(self):
|
||||
return None
|
||||
|
||||
def loop_stop(self):
|
||||
return None
|
||||
|
||||
def reconnect_delay_set(self, **_kw):
|
||||
return None
|
||||
|
||||
def publish(self, *_a, **_k):
|
||||
from lib_py.delegation.bench import FakeInfo
|
||||
n = self._calls.get("publish", 0)
|
||||
self._calls["publish"] = n + 1
|
||||
if n < self._no_conn_times:
|
||||
return FakeInfo(published=False, rc=int(self._mqtt.MQTT_ERR_NO_CONN))
|
||||
return FakeInfo(published=True, rc=0)
|
||||
|
||||
def disconnect(self):
|
||||
return None
|
||||
|
||||
|
||||
def test_f1_publish_issued_while_disconnected_is_retried_until_reconnect():
|
||||
"""F-1: the gap between a drop and paho's reconnect must be survivable.
|
||||
|
||||
paho queues such a publish and raises RuntimeError from wait_for_publish,
|
||||
which is not in the retry tuple; it used to end the attempt immediately."""
|
||||
from lib_py.delegation.publisher import PersistentPublisher
|
||||
|
||||
calls = {}
|
||||
publisher = PersistentPublisher(
|
||||
client_factory=lambda _r, _c: _FlakyConnectionClient(2, calls))
|
||||
try:
|
||||
ok, err = publisher.publish_with_retry(_prepared(), attempts=3)
|
||||
finally:
|
||||
publisher.close()
|
||||
assert ok is True, "should have recovered on the third attempt, got: {}".format(err)
|
||||
assert calls["publish"] == 3, "every attempt must reach the transport"
|
||||
assert calls["connect"] == 1, "recovery must reuse the connection, not rebuild it"
|
||||
|
||||
|
||||
def test_f1_disconnected_publish_still_fails_when_it_never_recovers():
|
||||
"""Retrying must not mask a genuinely dead connection."""
|
||||
from lib_py.delegation.publisher import PersistentPublisher
|
||||
|
||||
calls = {}
|
||||
publisher = PersistentPublisher(
|
||||
client_factory=lambda _r, _c: _FlakyConnectionClient(99, calls))
|
||||
try:
|
||||
ok, err = publisher.publish_with_retry(_prepared(), attempts=2)
|
||||
finally:
|
||||
publisher.close()
|
||||
assert ok is False
|
||||
assert "not connected" in err, err
|
||||
assert calls["publish"] == 2
|
||||
|
||||
|
||||
def test_one_shot_publish_has_a_single_implementation():
|
||||
"""O-1: the CLI path and the service path must not drift apart again."""
|
||||
from lib_py.delegation import publish_event
|
||||
from lib_py.delegation.publisher import OneShotPublisher
|
||||
|
||||
source = Path(publish_event.__file__).read_text(encoding="utf-8")
|
||||
assert "OneShotPublisher().publish_raw(" in source, (
|
||||
"_publish_once must delegate to the transport module"
|
||||
)
|
||||
for leaked in ("client.loop_start()", "no CONNACK from broker", "wait_for_publish("):
|
||||
assert leaked not in source, (
|
||||
"publish_event still carries its own copy of the transport logic: {}".format(leaked)
|
||||
)
|
||||
assert publish_event.CONNECT_ACK_TIMEOUT is not None
|
||||
assert hasattr(OneShotPublisher, "publish_raw")
|
||||
Reference in new issue
Block a user