meshcode 2.11.127__tar.gz → 2.11.128__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. {meshcode-2.11.127 → meshcode-2.11.128}/PKG-INFO +1 -1
  2. {meshcode-2.11.127 → meshcode-2.11.128}/meshcode/__init__.py +1 -1
  3. {meshcode-2.11.127 → meshcode-2.11.128}/meshcode.egg-info/PKG-INFO +1 -1
  4. {meshcode-2.11.127 → meshcode-2.11.128}/meshcode.egg-info/SOURCES.txt +1 -0
  5. {meshcode-2.11.127 → meshcode-2.11.128}/pyproject.toml +9 -1
  6. {meshcode-2.11.127 → meshcode-2.11.128}/tests/test_boot_bug_regression.py +1 -0
  7. {meshcode-2.11.127 → meshcode-2.11.128}/tests/test_cross_agent_messaging.py +34 -10
  8. {meshcode-2.11.127 → meshcode-2.11.128}/tests/test_epistemic_v1_stop_conditions.py +1 -0
  9. {meshcode-2.11.127 → meshcode-2.11.128}/tests/test_esc_deaf_state.py +7 -1
  10. meshcode-2.11.128/tests/test_live_mesh_guard.py +100 -0
  11. {meshcode-2.11.127 → meshcode-2.11.128}/tests/test_mark_read_batch.py +34 -11
  12. {meshcode-2.11.127 → meshcode-2.11.128}/tests/test_rls_cross_tenant.py +19 -5
  13. {meshcode-2.11.127 → meshcode-2.11.128}/README.md +0 -0
  14. {meshcode-2.11.127 → meshcode-2.11.128}/meshcode/__main__.py +0 -0
  15. {meshcode-2.11.127 → meshcode-2.11.128}/meshcode/_session_handoff_template.py +0 -0
  16. {meshcode-2.11.127 → meshcode-2.11.128}/meshcode/_stop_hook_template.py +0 -0
  17. {meshcode-2.11.127 → meshcode-2.11.128}/meshcode/ascii_art.py +0 -0
  18. {meshcode-2.11.127 → meshcode-2.11.128}/meshcode/atomic_push.py +0 -0
  19. {meshcode-2.11.127 → meshcode-2.11.128}/meshcode/claude_update.py +0 -0
  20. {meshcode-2.11.127 → meshcode-2.11.128}/meshcode/cli.py +0 -0
  21. {meshcode-2.11.127 → meshcode-2.11.128}/meshcode/comms_v4.py +0 -0
  22. {meshcode-2.11.127 → meshcode-2.11.128}/meshcode/compat.py +0 -0
  23. {meshcode-2.11.127 → meshcode-2.11.128}/meshcode/daemon.py +0 -0
  24. {meshcode-2.11.127 → meshcode-2.11.128}/meshcode/date_parse.py +0 -0
  25. {meshcode-2.11.127 → meshcode-2.11.128}/meshcode/doctor.py +0 -0
  26. {meshcode-2.11.127 → meshcode-2.11.128}/meshcode/error_hints.py +0 -0
  27. {meshcode-2.11.127 → meshcode-2.11.128}/meshcode/exceptions.py +0 -0
  28. {meshcode-2.11.127 → meshcode-2.11.128}/meshcode/hooks/__init__.py +0 -0
  29. {meshcode-2.11.127 → meshcode-2.11.128}/meshcode/hooks/repo_path_lock.py +0 -0
  30. {meshcode-2.11.127 → meshcode-2.11.128}/meshcode/hostd.py +0 -0
  31. {meshcode-2.11.127 → meshcode-2.11.128}/meshcode/invites.py +0 -0
  32. {meshcode-2.11.127 → meshcode-2.11.128}/meshcode/launcher.py +0 -0
  33. {meshcode-2.11.127 → meshcode-2.11.128}/meshcode/launcher_install.py +0 -0
  34. {meshcode-2.11.127 → meshcode-2.11.128}/meshcode/meshcode_mcp/__init__.py +0 -0
  35. {meshcode-2.11.127 → meshcode-2.11.128}/meshcode/meshcode_mcp/__main__.py +0 -0
  36. {meshcode-2.11.127 → meshcode-2.11.128}/meshcode/meshcode_mcp/backend.py +0 -0
  37. {meshcode-2.11.127 → meshcode-2.11.128}/meshcode/meshcode_mcp/realtime.py +0 -0
  38. {meshcode-2.11.127 → meshcode-2.11.128}/meshcode/meshcode_mcp/server.py +0 -0
  39. {meshcode-2.11.127 → meshcode-2.11.128}/meshcode/meshcode_mcp/sleep_signals.py +0 -0
  40. {meshcode-2.11.127 → meshcode-2.11.128}/meshcode/meshcode_mcp/swarm.py +0 -0
  41. {meshcode-2.11.127 → meshcode-2.11.128}/meshcode/meshcode_mcp/test_backend.py +0 -0
  42. {meshcode-2.11.127 → meshcode-2.11.128}/meshcode/meshcode_mcp/test_boot_timing.py +0 -0
  43. {meshcode-2.11.127 → meshcode-2.11.128}/meshcode/meshcode_mcp/test_install_guard.py +0 -0
  44. {meshcode-2.11.127 → meshcode-2.11.128}/meshcode/meshcode_mcp/test_prefs_claude_version.py +0 -0
  45. {meshcode-2.11.127 → meshcode-2.11.128}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  46. {meshcode-2.11.127 → meshcode-2.11.128}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  47. {meshcode-2.11.127 → meshcode-2.11.128}/meshcode/meshcode_mcp/test_swarm.py +0 -0
  48. {meshcode-2.11.127 → meshcode-2.11.128}/meshcode/preferences.py +0 -0
  49. {meshcode-2.11.127 → meshcode-2.11.128}/meshcode/protocol_handler.py +0 -0
  50. {meshcode-2.11.127 → meshcode-2.11.128}/meshcode/protocol_v2.py +0 -0
  51. {meshcode-2.11.127 → meshcode-2.11.128}/meshcode/quickstart.py +0 -0
  52. {meshcode-2.11.127 → meshcode-2.11.128}/meshcode/rpc_allowlist.py +0 -0
  53. {meshcode-2.11.127 → meshcode-2.11.128}/meshcode/run_agent.py +0 -0
  54. {meshcode-2.11.127 → meshcode-2.11.128}/meshcode/scripts/check_secrets.py +0 -0
  55. {meshcode-2.11.127 → meshcode-2.11.128}/meshcode/scripts/race_rate_harness.py +0 -0
  56. {meshcode-2.11.127 → meshcode-2.11.128}/meshcode/secrets.py +0 -0
  57. {meshcode-2.11.127 → meshcode-2.11.128}/meshcode/self_update.py +0 -0
  58. {meshcode-2.11.127 → meshcode-2.11.128}/meshcode/setup_clients.py +0 -0
  59. {meshcode-2.11.127 → meshcode-2.11.128}/meshcode/supervisor.py +0 -0
  60. {meshcode-2.11.127 → meshcode-2.11.128}/meshcode/up.py +0 -0
  61. {meshcode-2.11.127 → meshcode-2.11.128}/meshcode/upload.py +0 -0
  62. {meshcode-2.11.127 → meshcode-2.11.128}/meshcode.egg-info/dependency_links.txt +0 -0
  63. {meshcode-2.11.127 → meshcode-2.11.128}/meshcode.egg-info/entry_points.txt +0 -0
  64. {meshcode-2.11.127 → meshcode-2.11.128}/meshcode.egg-info/requires.txt +0 -0
  65. {meshcode-2.11.127 → meshcode-2.11.128}/meshcode.egg-info/top_level.txt +0 -0
  66. {meshcode-2.11.127 → meshcode-2.11.128}/setup.cfg +0 -0
  67. {meshcode-2.11.127 → meshcode-2.11.128}/tests/test_auto_update_hardening.py +0 -0
  68. {meshcode-2.11.127 → meshcode-2.11.128}/tests/test_autonomous_closegap_1.py +0 -0
  69. {meshcode-2.11.127 → meshcode-2.11.128}/tests/test_autonomous_closegap_2.py +0 -0
  70. {meshcode-2.11.127 → meshcode-2.11.128}/tests/test_autonomous_closegap_3.py +0 -0
  71. {meshcode-2.11.127 → meshcode-2.11.128}/tests/test_autonomous_prompt_inject.py +0 -0
  72. {meshcode-2.11.127 → meshcode-2.11.128}/tests/test_color_truecolor.py +0 -0
  73. {meshcode-2.11.127 → meshcode-2.11.128}/tests/test_core.py +0 -0
  74. {meshcode-2.11.127 → meshcode-2.11.128}/tests/test_date_parse.py +0 -0
  75. {meshcode-2.11.127 → meshcode-2.11.128}/tests/test_doctor.py +0 -0
  76. {meshcode-2.11.127 → meshcode-2.11.128}/tests/test_epistemic_v1_python_sdk.py +0 -0
  77. {meshcode-2.11.127 → meshcode-2.11.128}/tests/test_exceptions.py +0 -0
  78. {meshcode-2.11.127 → meshcode-2.11.128}/tests/test_file_upload.py +0 -0
  79. {meshcode-2.11.127 → meshcode-2.11.128}/tests/test_init_device_code.py +0 -0
  80. {meshcode-2.11.127 → meshcode-2.11.128}/tests/test_install_guard.py +0 -0
  81. {meshcode-2.11.127 → meshcode-2.11.128}/tests/test_lease_sigterm_release.py +0 -0
  82. {meshcode-2.11.127 → meshcode-2.11.128}/tests/test_marketplace_ratings.py +0 -0
  83. {meshcode-2.11.127 → meshcode-2.11.128}/tests/test_migration_integrity.py +0 -0
  84. {meshcode-2.11.127 → meshcode-2.11.128}/tests/test_realtime_event_freshness.py +0 -0
  85. {meshcode-2.11.127 → meshcode-2.11.128}/tests/test_rpc_grants.py +0 -0
  86. {meshcode-2.11.127 → meshcode-2.11.128}/tests/test_rpc_migrations.py +0 -0
  87. {meshcode-2.11.127 → meshcode-2.11.128}/tests/test_run_agent_dry_run.py +0 -0
  88. {meshcode-2.11.127 → meshcode-2.11.128}/tests/test_run_agent_no_server_import.py +0 -0
  89. {meshcode-2.11.127 → meshcode-2.11.128}/tests/test_security_regressions.py +0 -0
  90. {meshcode-2.11.127 → meshcode-2.11.128}/tests/test_self_update_user_site.py +0 -0
  91. {meshcode-2.11.127 → meshcode-2.11.128}/tests/test_sentinel.py +0 -0
  92. {meshcode-2.11.127 → meshcode-2.11.128}/tests/test_setup_path.py +0 -0
  93. {meshcode-2.11.127 → meshcode-2.11.128}/tests/test_sleep_signals.py +0 -0
  94. {meshcode-2.11.127 → meshcode-2.11.128}/tests/test_status_enum_coverage.py +0 -0
  95. {meshcode-2.11.127 → meshcode-2.11.128}/tests/test_stay_on_loop_hook.py +0 -0
  96. {meshcode-2.11.127 → meshcode-2.11.128}/tests/test_stop_ghost_terminal.py +0 -0
  97. {meshcode-2.11.127 → meshcode-2.11.128}/tests/test_swarm_events.py +0 -0
  98. {meshcode-2.11.127 → meshcode-2.11.128}/tests/test_wait_open_tasks_contradiction.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.11.127
3
+ Version: 2.11.128
4
4
  Summary: Real-time communication between AI agents — Supabase-backed CLI
5
5
  Author-email: MeshCode <hello@meshcode.io>
6
6
  License: MIT
@@ -1,5 +1,5 @@
1
1
  """MeshCode — Real-time communication between AI agents."""
2
- __version__ = "2.11.127"
2
+ __version__ = "2.11.128"
3
3
 
4
4
  # Exception hierarchy — eagerly imported (lightweight, no deps)
5
5
  from meshcode.exceptions import ( # noqa: F401
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.11.127
3
+ Version: 2.11.128
4
4
  Summary: Real-time communication between AI agents — Supabase-backed CLI
5
5
  Author-email: MeshCode <hello@meshcode.io>
6
6
  License: MIT
@@ -74,6 +74,7 @@ tests/test_file_upload.py
74
74
  tests/test_init_device_code.py
75
75
  tests/test_install_guard.py
76
76
  tests/test_lease_sigterm_release.py
77
+ tests/test_live_mesh_guard.py
77
78
  tests/test_mark_read_batch.py
78
79
  tests/test_marketplace_ratings.py
79
80
  tests/test_migration_integrity.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "meshcode"
7
- version = "2.11.127"
7
+ version = "2.11.128"
8
8
  description = "Real-time communication between AI agents — Supabase-backed CLI"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -45,6 +45,14 @@ Repository = "https://github.com/rf2f7f7sg4-dev/meshcode"
45
45
  [tool.pytest.ini_options]
46
46
  testpaths = ["tests"]
47
47
  addopts = "--durations=10 --durations-min=0.5 -v --tb=short"
48
+ markers = [
49
+ # live_mesh: the test talks to the REAL Supabase backend (registers agents,
50
+ # sends messages, reads the live roster). Opt-in via MESHCODE_TEST_PROJECT;
51
+ # CI/smoke must deselect with: -m "not live_mesh"
52
+ # (incident 2026-06-10: suite runs leaked 30 fixture agents into the live
53
+ # mesh-dev roster on Ian's dashboard).
54
+ "live_mesh: hits the real Supabase backend/live mesh; opt-in, deselect with -m 'not live_mesh'",
55
+ ]
48
56
 
49
57
  [tool.setuptools.packages.find]
50
58
  include = ["meshcode", "meshcode.*"]
@@ -131,6 +131,7 @@ def test_module_import_rpc_allowlist():
131
131
  )
132
132
 
133
133
 
134
+ @pytest.mark.live_mesh
134
135
  def test_register_all_valid_statuses_live():
135
136
  """RC #1 sentinel — mig 312. Catches SDK↔DDL enum drift like the
136
137
  'needs_launch' incident.
@@ -21,6 +21,14 @@ import time
21
21
  import unittest
22
22
  from pathlib import Path
23
23
 
24
+ import pytest
25
+
26
+ # These tests register agents and send messages on the REAL backend — every
27
+ # run is visible on the owner's dashboard. Opt-in only (incident 2026-06-10:
28
+ # fixture agents leaked into the live mesh-dev roster). Deselect in CI/smoke
29
+ # with: -m "not live_mesh"
30
+ pytestmark = pytest.mark.live_mesh
31
+
24
32
  # Add SDK to path
25
33
  SDK_PATH = Path(__file__).parent.parent / "meshcode" / "meshcode_mcp"
26
34
  if SDK_PATH.exists():
@@ -39,7 +47,10 @@ from meshcode.meshcode_mcp import backend as be
39
47
  # Agents are cleaned up in tearDown. No project creation needed.
40
48
  #
41
49
 
42
- TEST_PROJECT = os.environ.get("MESHCODE_TEST_PROJECT", "mesh-dev")
50
+ # NEVER default to a live mesh. Unset MESHCODE_TEST_PROJECT = SKIP everything:
51
+ # the old default ("mesh-dev") put every fixture agent and broadcast of this
52
+ # suite straight onto the production dashboard (incident 2026-06-10).
53
+ TEST_PROJECT = os.environ.get("MESHCODE_TEST_PROJECT")
43
54
  AGENT_A = f"e2e-alice-{int(time.time()) % 10000}"
44
55
  AGENT_B = f"e2e-bob-{int(time.time()) % 10000}"
45
56
  AGENT_C = f"e2e-carol-{int(time.time()) % 10000}"
@@ -61,6 +72,10 @@ def _get_api_key():
61
72
  API_KEY = _get_api_key()
62
73
 
63
74
 
75
+ @unittest.skipUnless(
76
+ TEST_PROJECT,
77
+ "MESHCODE_TEST_PROJECT not set — live-mesh e2e tests are opt-in and must "
78
+ "point at a dedicated test meshwork, never a live one (incident 2026-06-10)")
64
79
  class CrossAgentMessagingTests(unittest.TestCase):
65
80
  """E2E tests for cross-agent message delivery."""
66
81
 
@@ -106,16 +121,25 @@ class CrossAgentMessagingTests(unittest.TestCase):
106
121
 
107
122
  @classmethod
108
123
  def tearDownClass(cls):
109
- """Cleanup: unregister ephemeral test agents."""
124
+ """Cleanup: unregister ephemeral test agents. LOUD on failure — the old
125
+ except:pass (plus never checking sb_rpc's returned error dict) let a
126
+ 404 on mc_unregister_agent leak fixture agents into the roster forever
127
+ (incident 2026-06-10)."""
128
+ failures = []
110
129
  for agent in [AGENT_A, AGENT_B, AGENT_C]:
111
- try:
112
- be.sb_rpc("mc_unregister_agent", {
113
- "p_api_key": API_KEY,
114
- "p_project_id": cls.project_id,
115
- "p_agent_name": agent,
116
- })
117
- except Exception:
118
- pass
130
+ result = be.sb_rpc("mc_unregister_agent", {
131
+ "p_api_key": API_KEY,
132
+ "p_project_id": cls.project_id,
133
+ "p_agent_name": agent,
134
+ })
135
+ # sb_rpc reports errors as a return VALUE, not an exception.
136
+ if isinstance(result, dict) and (result.get("error") or result.get("_error")):
137
+ failures.append(f"{agent}: {result}")
138
+ if failures:
139
+ raise AssertionError(
140
+ "tearDown could not unregister test agents — they are now "
141
+ f"ORPHANED in '{TEST_PROJECT}' and must be removed by hand:\n "
142
+ + "\n ".join(failures))
119
143
  print(f"\n Cleaned up test agents from {TEST_PROJECT}")
120
144
 
121
145
  # ── Basic Delivery ─────────────────────────────────────────────────────
@@ -33,6 +33,7 @@ def _live_or_skip():
33
33
  pytest.skip("MESHCODE_SUPABASE_URL not set; epistemic V1 health is live-only.")
34
34
 
35
35
 
36
+ @pytest.mark.live_mesh
36
37
  def test_epistemic_v1_health_all_conditions_healthy():
37
38
  """Live check: call mc_epistemic_v1_health() and assert each is_healthy."""
38
39
  _live_or_skip()
@@ -20,6 +20,8 @@ import os
20
20
  import sys
21
21
  import time
22
22
  import unittest
23
+
24
+ import pytest
23
25
  from pathlib import Path
24
26
  from unittest.mock import patch, MagicMock, AsyncMock
25
27
 
@@ -196,8 +198,12 @@ class TestCircuitBreakerAfterCancel(unittest.TestCase):
196
198
  # Not a hard failure — pool may recover via other means
197
199
 
198
200
 
201
+ @pytest.mark.live_mesh
199
202
  class TestBackendOperationsAfterCancel(unittest.TestCase):
200
- """Test that backend send/read work after simulated interrupt state."""
203
+ """Test that backend send/read work after simulated interrupt state.
204
+
205
+ live_mesh: test_06 reads count_pending against the REAL backend with the
206
+ keyring credential (read-only, but still a live-DB dependency)."""
201
207
 
202
208
  def test_06_send_message_after_cb_reset(self):
203
209
  """send_message should work even if circuit breaker was previously open."""
@@ -0,0 +1,100 @@
1
+ """
2
+ Live-Mesh Guard Regression Tests
3
+ =================================
4
+ Enforces the suite-wide rules from incident 2026-06-10 (30 fixture agents +
5
+ broadcast/impersonation spam leaked into the LIVE mesh-dev roster on the
6
+ owner's dashboard because e2e tests defaulted TEST_PROJECT to "mesh-dev" and
7
+ their teardown swallowed a 404 on a nonexistent RPC):
8
+
9
+ 1. No test may DEFAULT its target project to a live mesh — the project must
10
+ come from MESHCODE_TEST_PROJECT with no fallback value.
11
+ 2. Any test file that registers agents on the real backend must carry the
12
+ `live_mesh` pytest marker so CI/smoke can deselect with -m "not live_mesh".
13
+ 3. mc_unregister_agent teardowns must CHECK the sb_rpc result (errors come
14
+ back as a return value, not an exception) — no silent leaks.
15
+ 4. The live_mesh marker stays registered in pyproject.toml.
16
+
17
+ Source-parsing only (pattern of test_security_regressions.py) — no DB needed.
18
+
19
+ Usage:
20
+ pytest tests/test_live_mesh_guard.py -v
21
+ """
22
+
23
+ import re
24
+ from pathlib import Path
25
+
26
+ TESTS_DIR = Path(__file__).parent
27
+ PYPROJECT = TESTS_DIR.parent / "pyproject.toml"
28
+ THIS_FILE = Path(__file__).name
29
+
30
+
31
+ def _test_sources():
32
+ for f in sorted(TESTS_DIR.glob("test_*.py")):
33
+ if f.name == THIS_FILE:
34
+ continue
35
+ yield f.name, f.read_text(errors="replace")
36
+
37
+
38
+ def _code_lines(src: str):
39
+ """Lines with #-comments stripped (docstring prose is still included, so
40
+ the patterns below target call/assignment syntax, not bare words)."""
41
+ return [ln.split("#", 1)[0] for ln in src.splitlines()]
42
+
43
+
44
+ class TestNoLiveMeshDefaults:
45
+ def test_no_default_value_for_test_project_env(self):
46
+ """os.environ.get("MESHCODE_TEST_PROJECT", <default>) is forbidden —
47
+ unset must mean SKIP, never a fallback mesh."""
48
+ rx = re.compile(r"""MESHCODE_TEST_PROJECT['"]\s*,""")
49
+ offenders = [name for name, src in _test_sources()
50
+ for ln in _code_lines(src) if rx.search(ln)]
51
+ assert not offenders, (
52
+ f"MESHCODE_TEST_PROJECT must have NO default value (incident "
53
+ f"2026-06-10) — offenders: {offenders}")
54
+
55
+ def test_no_hardcoded_live_project_assignment(self):
56
+ """A module-level <X>PROJECT = "mesh-dev" (or any literal) hardcodes a
57
+ live mesh as the test target."""
58
+ rx = re.compile(r"""^\s*\w*PROJECT\s*=\s*['"][a-zA-Z]""")
59
+ offenders = [name for name, src in _test_sources()
60
+ for ln in _code_lines(src) if rx.search(ln)]
61
+ assert not offenders, (
62
+ f"Test project names must come from MESHCODE_TEST_PROJECT, never "
63
+ f"a literal — offenders: {offenders}")
64
+
65
+
66
+ class TestLiveMeshMarker:
67
+ def test_agent_registering_files_carry_marker(self):
68
+ """Every file that calls mc_register_agent against the real backend
69
+ (i.e. without mocking sb_rpc) must be deselectable via the marker."""
70
+ for name, src in _test_sources():
71
+ code = "\n".join(_code_lines(src))
72
+ registers = re.search(r"""sb_rpc\(\s*['"]mc_register_agent['"]""", code)
73
+ mocked = re.search(r"patch.*sb_rpc|sb_rpc\s*=|monkeypatch", code)
74
+ if registers and not mocked:
75
+ assert re.search(r"pytestmark\s*=\s*pytest\.mark\.live_mesh", code), (
76
+ f"{name} registers agents on the live backend but has no "
77
+ f"module-level `pytestmark = pytest.mark.live_mesh`")
78
+
79
+ def test_marker_registered_in_pyproject(self):
80
+ cfg = PYPROJECT.read_text(errors="replace")
81
+ assert re.search(r"live_mesh\s*:", cfg), (
82
+ "live_mesh marker must stay registered under "
83
+ "[tool.pytest.ini_options] markers in pyproject.toml")
84
+
85
+
86
+ class TestLoudTeardown:
87
+ def test_unregister_results_are_checked(self):
88
+ """sb_rpc returns errors as a dict, not an exception — every
89
+ mc_unregister_agent caller must inspect the result."""
90
+ for name, src in _test_sources():
91
+ code = "\n".join(_code_lines(src))
92
+ if "mc_unregister_agent" not in code:
93
+ continue
94
+ # The call's result must be bound and error-checked.
95
+ assert re.search(r"=\s*be\.sb_rpc\(\s*['\"]mc_unregister_agent", code), (
96
+ f"{name}: mc_unregister_agent result is discarded — a 404 "
97
+ f"would silently leak fixture agents (incident 2026-06-10)")
98
+ assert re.search(r"""\.get\(\s*['"](_)?error['"]""", code), (
99
+ f"{name}: mc_unregister_agent result is never checked for an "
100
+ f"error key")
@@ -18,10 +18,17 @@ import time
18
18
  import unittest
19
19
  from pathlib import Path
20
20
 
21
+ import pytest
22
+
21
23
  sys.path.insert(0, str(Path(__file__).parent.parent))
22
24
 
23
25
  os.environ.setdefault("SUPABASE_URL", "https://gjinagyyjttyxnaoavnz.supabase.co")
24
26
 
27
+ # Registers agents and sends messages on the REAL backend — opt-in only
28
+ # (incident 2026-06-10: mread-* fixtures leaked into the live mesh-dev
29
+ # roster). Deselect in CI/smoke with: -m "not live_mesh"
30
+ pytestmark = pytest.mark.live_mesh
31
+
25
32
 
26
33
  def _get_api_key():
27
34
  key = os.environ.get("MESHCODE_API_KEY")
@@ -36,9 +43,16 @@ def _get_api_key():
36
43
 
37
44
 
38
45
  API_KEY = _get_api_key()
39
- PROJECT = "mesh-dev"
46
+ # NEVER default to a live mesh (the old hardcoded "mesh-dev" put mread-*
47
+ # fixtures on the production dashboard). Unset = tests SKIP.
48
+ PROJECT = os.environ.get("MESHCODE_TEST_PROJECT")
40
49
  PROJECT_ID = None
41
50
 
51
+ _LIVE_GATE = unittest.skipUnless(
52
+ API_KEY and PROJECT,
53
+ "MESHCODE_TEST_PROJECT (and an API key) required — live-mesh tests are "
54
+ "opt-in and must point at a dedicated test meshwork (incident 2026-06-10)")
55
+
42
56
 
43
57
  def _resolve():
44
58
  global PROJECT_ID
@@ -57,7 +71,7 @@ AGENT_A = f"mread-a-{int(time.time()) % 10000}"
57
71
  AGENT_B = f"mread-b-{int(time.time()) % 10000}"
58
72
 
59
73
 
60
- @unittest.skipUnless(API_KEY, "No API key — skipping live tests")
74
+ @_LIVE_GATE
61
75
  class TestBatchMarkRead(unittest.TestCase):
62
76
 
63
77
  @classmethod
@@ -77,16 +91,25 @@ class TestBatchMarkRead(unittest.TestCase):
77
91
 
78
92
  @classmethod
79
93
  def tearDownClass(cls):
94
+ # LOUD on failure — the old except:pass (plus never checking sb_rpc's
95
+ # returned error dict) let a 404 on mc_unregister_agent leak mread-*
96
+ # fixtures into the roster forever (incident 2026-06-10).
80
97
  from meshcode.meshcode_mcp import backend as be
98
+ failures = []
81
99
  for name in [AGENT_A, AGENT_B]:
82
- try:
83
- be.sb_rpc("mc_unregister_agent", {
84
- "p_api_key": API_KEY,
85
- "p_project_id": cls.pid,
86
- "p_agent_name": name,
87
- })
88
- except Exception:
89
- pass
100
+ result = be.sb_rpc("mc_unregister_agent", {
101
+ "p_api_key": API_KEY,
102
+ "p_project_id": cls.pid,
103
+ "p_agent_name": name,
104
+ })
105
+ # sb_rpc reports errors as a return VALUE, not an exception.
106
+ if isinstance(result, dict) and (result.get("error") or result.get("_error")):
107
+ failures.append(f"{name}: {result}")
108
+ if failures:
109
+ raise AssertionError(
110
+ "tearDown could not unregister test agents — they are now "
111
+ f"ORPHANED in '{PROJECT}' and must be removed by hand:\n "
112
+ + "\n ".join(failures))
90
113
 
91
114
  def test_01_batch_mark_read(self):
92
115
  """Send 5 messages, read_inbox marks all read in one call."""
@@ -129,7 +152,7 @@ class TestBatchMarkRead(unittest.TestCase):
129
152
  self.assertEqual(count, 0, f"Pending should be 0 after mark_read, got {count}")
130
153
 
131
154
 
132
- @unittest.skipUnless(API_KEY, "No API key — skipping live tests")
155
+ @_LIVE_GATE
133
156
  class TestEffectiveStatusIntegration(unittest.TestCase):
134
157
  """Live integration test for mc_agent_effective_status."""
135
158
 
@@ -17,10 +17,17 @@ import time
17
17
  import unittest
18
18
  from pathlib import Path
19
19
 
20
+ import pytest
21
+
20
22
  sys.path.insert(0, str(Path(__file__).parent.parent))
21
23
 
22
24
  os.environ.setdefault("SUPABASE_URL", "https://gjinagyyjttyxnaoavnz.supabase.co")
23
25
 
26
+ # Sends real messages (incl. impersonation probes a human can see on the
27
+ # dashboard) against the REAL backend — opt-in only (incident 2026-06-10).
28
+ # Deselect in CI/smoke with: -m "not live_mesh"
29
+ pytestmark = pytest.mark.live_mesh
30
+
24
31
 
25
32
  def _get_api_key():
26
33
  key = os.environ.get("MESHCODE_API_KEY")
@@ -35,11 +42,18 @@ def _get_api_key():
35
42
 
36
43
 
37
44
  API_KEY = _get_api_key()
38
- # Use mesh-dev as the "own" project
39
- OWN_PROJECT = "mesh-dev"
45
+ # NEVER default to a live mesh (the old hardcoded "mesh-dev" sent the
46
+ # impersonation probes of this file to the production dashboard). Unset =
47
+ # tests SKIP.
48
+ OWN_PROJECT = os.environ.get("MESHCODE_TEST_PROJECT")
40
49
  OWN_PROJECT_ID = None
41
50
  FAKE_PROJECT_ID = "00000000-0000-0000-0000-000000000000"
42
51
 
52
+ _LIVE_GATE = unittest.skipUnless(
53
+ API_KEY and OWN_PROJECT,
54
+ "MESHCODE_TEST_PROJECT (and an API key) required — live-mesh tests are "
55
+ "opt-in and must point at a dedicated test meshwork (incident 2026-06-10)")
56
+
43
57
 
44
58
  def _resolve_project():
45
59
  global OWN_PROJECT_ID
@@ -57,7 +71,7 @@ def _resolve_project():
57
71
  return OWN_PROJECT_ID
58
72
 
59
73
 
60
- @unittest.skipUnless(API_KEY, "No API key — skipping live RLS tests")
74
+ @_LIVE_GATE
61
75
  class TestCrossTenantIsolation(unittest.TestCase):
62
76
  """Agent in project X cannot access project Y data."""
63
77
 
@@ -135,7 +149,7 @@ class TestCrossTenantIsolation(unittest.TestCase):
135
149
  self.assertTrue(is_safe, f"Unexpected result: {result}")
136
150
 
137
151
 
138
- @unittest.skipUnless(API_KEY, "No API key — skipping live RLS tests")
152
+ @_LIVE_GATE
139
153
  class TestImpersonation(unittest.TestCase):
140
154
  """Cannot impersonate another agent or user."""
141
155
 
@@ -202,7 +216,7 @@ class TestImpersonation(unittest.TestCase):
202
216
  f"Empty API key should be rejected: {result}")
203
217
 
204
218
 
205
- @unittest.skipUnless(API_KEY, "No API key — skipping live RLS tests")
219
+ @_LIVE_GATE
206
220
  class TestRPCErrorConsistency(unittest.TestCase):
207
221
  """All RPCs should return consistent error format."""
208
222
 
File without changes
File without changes
File without changes
File without changes