test(issue-28): make the host-dependent tests portable; POSIX ps in the process scanners

The 33 failures on the Linux / Python 3.14 host (#28) were environment
dependencies, not regressions; each test is worth keeping, so fix the
dependency instead of deleting the test:

- test_workspace_scope: `source` under `shell=True` ran in dash; run the
  command string under bash.
- conftest autouse: MAM_ENV_FILE points at an absent file for every test,
  so a workspace .mam.env with a reachable broker no longer breaks the
  "broker unavailable" tests (T1-4).
- help-capture guard: compare normalized help text (whitespace collapsed,
  "optional arguments"/"options" unified) so argparse re-wrapping across
  Python versions is not drift.
- deploy freshness D-11..D-19 and D-22..D-30: skip with a reason when the
  nats-docker submodule is not initialized.
- lib_py.agents.base.conversation_in_use and heartbeat._process_table:
  `ps -eo pid=,ppid=,args=` (POSIX) instead of the BSD `-axo ... command=`
  form, the most likely cause of the 12 purge failures on Linux (the holder
  scan returned nothing); to be confirmed on that host.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
This commit is contained in:
GodopuandClaude Fable 5.1 committed 2026-09-20 19:15:50 +09:00
1 parent 46ea4cc1e4
commit 7a94ba2c3e
6 files changed
+48 -4

No files matched your search

+27
View File
@@ -279,12 +279,19 @@ def _resolve_private_server_doc() -> str:
PRIVATE_SERVER_DOC_PATH = _resolve_private_server_doc()
# Issue #28: these guards read the nats-docker submodule. A checkout without
# `git submodule update --init` has nothing to check; skip with the reason
# instead of failing on every host that cloned without submodules.
needs_private_server_doc = pytest.mark.skipif(
not os.path.exists(PRIVATE_SERVER_DOC_PATH),
reason="nats-docker submodule not initialized (git submodule update --init)")
# --------------------------------------------------------------------------
# D-11 — (G-D1) PRIVATE_SERVER.md must only document MQTT_* environment
# variables that broker_config_from_env() actually parses.
# --------------------------------------------------------------------------
@needs_private_server_doc
def test_d11_private_server_env_names_valid():
sys.path.insert(0, os.path.join(REPO_ROOT, ".agents", "skills", "multi-agent-mux-delegate-job", "scripts"))
import mqtt_common
@@ -315,6 +322,7 @@ def test_d11_private_server_env_names_valid():
# D-12 — (G-D2) PRIVATE_SERVER.md must not contain invalid MAM_MQTT_* in
# active configuration code blocks.
# --------------------------------------------------------------------------
@needs_private_server_doc
def test_d12_private_server_no_mam_mqtt_in_code_fences():
doc_path = PRIVATE_SERVER_DOC_PATH
with open(doc_path, "r", encoding="utf-8") as f:
@@ -331,6 +339,7 @@ def test_d12_private_server_no_mam_mqtt_in_code_fences():
# D-13 — (G-D3) nats-server launch instructions in PRIVATE_SERVER.md must
# use valid config blocks (mqtt {) and not HTTP port flag (-m 1883).
# --------------------------------------------------------------------------
@needs_private_server_doc
def test_d13_private_server_nats_config_valid():
doc_path = PRIVATE_SERVER_DOC_PATH
with open(doc_path, "r", encoding="utf-8") as f:
@@ -347,6 +356,7 @@ def test_d13_private_server_nats_config_valid():
# D-14 — (G-D4) Verification commands in PRIVATE_SERVER.md must use valid
# CLI flags matching the actual scripts' argparse parsers.
# --------------------------------------------------------------------------
@needs_private_server_doc
def test_d14_private_server_cli_args_valid():
doc_path = PRIVATE_SERVER_DOC_PATH
with open(doc_path, "r", encoding="utf-8") as f:
@@ -367,6 +377,7 @@ def test_d14_private_server_cli_args_valid():
# D-15 — (G-D5) store_dir in code blocks must be absolute path (/ or $HOME)
# and heredocs writing it must be unquoted (<<EOF).
# --------------------------------------------------------------------------
@needs_private_server_doc
def test_d15_private_server_store_dir_valid():
doc_path = PRIVATE_SERVER_DOC_PATH
with open(doc_path, "r", encoding="utf-8") as f:
@@ -388,6 +399,7 @@ def test_d15_private_server_store_dir_valid():
# --------------------------------------------------------------------------
# D-16 — (G-D6) nats image references in code fences must use pinned alpine
# --------------------------------------------------------------------------
@needs_private_server_doc
def test_d16_private_server_nats_image_alpine_pinned():
doc_path = PRIVATE_SERVER_DOC_PATH
with open(doc_path, "r", encoding="utf-8") as f:
@@ -404,6 +416,7 @@ def test_d16_private_server_nats_image_alpine_pinned():
# --------------------------------------------------------------------------
# D-17 — (G-D7) Port 8222 in docker examples must be bound to 127.0.0.1
# --------------------------------------------------------------------------
@needs_private_server_doc
def test_d17_private_server_monitoring_port_localhost_bound():
doc_path = PRIVATE_SERVER_DOC_PATH
with open(doc_path, "r", encoding="utf-8") as f:
@@ -421,6 +434,7 @@ def test_d17_private_server_monitoring_port_localhost_bound():
# --------------------------------------------------------------------------
# D-18 — (G-D8) TLS examples must not use IP literals for MQTT_BROKER
# --------------------------------------------------------------------------
@needs_private_server_doc
def test_d18_private_server_tls_examples_use_domain_names():
doc_path = PRIVATE_SERVER_DOC_PATH
with open(doc_path, "r", encoding="utf-8") as f:
@@ -437,6 +451,7 @@ def test_d18_private_server_tls_examples_use_domain_names():
# --------------------------------------------------------------------------
# D-19 — (G-D9) Subject literals in config examples match DEFAULT_TOPIC_ROOT
# --------------------------------------------------------------------------
@needs_private_server_doc
def test_d19_private_server_subject_literals_match_default_topic_root():
doc_path = PRIVATE_SERVER_DOC_PATH
with open(doc_path, "r", encoding="utf-8") as f:
@@ -492,6 +507,9 @@ def _resolve_docker_dir() -> str:
DOCKER_DIR = _resolve_docker_dir()
COMPOSE_PATH = os.path.join(DOCKER_DIR, "docker-compose.yaml")
needs_docker_assets = pytest.mark.skipif(
not os.path.exists(COMPOSE_PATH),
reason="nats-docker submodule not initialized (git submodule update --init)")
NATS_CONF_PATH = os.path.join(DOCKER_DIR, "nats.conf")
ENV_EXAMPLE_PATH = os.path.join(DOCKER_DIR, ".env.example")
DOCKER_README_PATH = os.path.join(DOCKER_DIR, "README.md")
@@ -523,6 +541,7 @@ def _active_conf_lines():
# --------------------------------------------------------------------------
# D-22 — docker/ assets exist and are populated
# --------------------------------------------------------------------------
@needs_docker_assets
def test_d22_docker_assets_exist_and_are_populated():
for p in [COMPOSE_PATH, NATS_CONF_PATH, ENV_EXAMPLE_PATH, DOCKER_README_PATH]:
assert os.path.exists(p), f"Required asset {p} does not exist"
@@ -533,6 +552,7 @@ def test_d22_docker_assets_exist_and_are_populated():
# --------------------------------------------------------------------------
# D-23 — compose image matches doc and is alpine
# --------------------------------------------------------------------------
@needs_docker_assets
def test_d23_compose_image_matches_doc_and_is_alpine():
_, svc = _load_compose()
compose_img = svc.get("image", "")
@@ -552,6 +572,7 @@ def test_d23_compose_image_matches_doc_and_is_alpine():
# --------------------------------------------------------------------------
# D-24 — compose port exposure contract
# --------------------------------------------------------------------------
@needs_docker_assets
def test_d24_compose_port_exposure_contract():
_, svc = _load_compose()
ports = svc.get("ports", [])
@@ -573,6 +594,7 @@ def test_d24_compose_port_exposure_contract():
# --------------------------------------------------------------------------
# D-25 — secrets are fail closed
# --------------------------------------------------------------------------
@needs_docker_assets
def test_d25_secrets_are_fail_closed():
_, svc = _load_compose()
env = svc.get("environment", {})
@@ -615,6 +637,7 @@ def test_d25_secrets_are_fail_closed():
# --------------------------------------------------------------------------
# D-26 — nats_conf jetstream and mqtt contract
# --------------------------------------------------------------------------
@needs_docker_assets
def test_d26_nats_conf_jetstream_and_mqtt_contract():
_, svc = _load_compose()
volumes = svc.get("volumes", [])
@@ -645,6 +668,7 @@ def test_d26_nats_conf_jetstream_and_mqtt_contract():
# --------------------------------------------------------------------------
# D-27 — observer permissions match topic root
# --------------------------------------------------------------------------
@needs_docker_assets
def test_d27_observer_permissions_match_topic_root():
sys.path.insert(0, os.path.join(REPO_ROOT, ".agents", "skills", "multi-agent-mux-delegate-job", "scripts"))
import mqtt_common
@@ -666,6 +690,7 @@ def test_d27_observer_permissions_match_topic_root():
# --------------------------------------------------------------------------
# D-28 — healthcheck contract and image coupling
# --------------------------------------------------------------------------
@needs_docker_assets
def test_d28_healthcheck_contract_and_image_coupling():
_, svc = _load_compose()
hc = svc.get("healthcheck")
@@ -684,6 +709,7 @@ def test_d28_healthcheck_contract_and_image_coupling():
# --------------------------------------------------------------------------
# D-29 — env secrets never tracked
# --------------------------------------------------------------------------
@needs_docker_assets
def test_d29_env_secrets_never_tracked():
is_submodule = os.path.exists(os.path.join(REPO_ROOT, ".gitmodules")) and "nats-docker" in DOCKER_DIR
target_repo = os.path.join(REPO_ROOT, "nats-docker") if is_submodule else REPO_ROOT
@@ -703,6 +729,7 @@ def test_d29_env_secrets_never_tracked():
# --------------------------------------------------------------------------
# D-30 — websocket origin policy is startable
# --------------------------------------------------------------------------
@needs_docker_assets
def test_d30_websocket_origin_policy_is_startable():
active_lines = _active_conf_lines()
active_text = "\n".join(active_lines)