"""Client-identity env recognition and the production heartbeat lifecycle (#975). Two defects left behind by #948, covered here against the issue's acceptance criteria: 1. ``GITEA_MCP_CLIENT`` / ``GITEA_MCP_CLIENT_INSTANCE`` / ``GITEA_MCP_CLIENT_SESSION`` were consumed by production while absent from the recognized-key allowlist, so the peer-env scan classified them as unsupported overrides and the capability resolver refused every mutation fleet-wide. 2. ``WorkerRegistry.heartbeat()`` had no production caller, so ``last_heartbeat_at`` never left ``started_at`` and ``heartbeat_ttl_seconds`` became a hard cap on attachment rather than a liveness window. Every TTL assertion here uses an injected clock (AC13). No test waits for a real TTL. The one test that exercises the real thread loop uses a millisecond-scale interval and polls with a bounded timeout, never a TTL. All client, session, and generation identifiers are synthetic. """ from __future__ import annotations import os import tempfile import threading import time import unittest from contextlib import contextmanager from datetime import datetime, timedelta, timezone from typing import Iterator import gitea_config import mcp_worker_identity as mwi NOW = datetime(2026, 7, 30, 0, 40, 9, tzinfo=timezone.utc) TTL = 900.0 #: The three keys production reads as client identity. CLIENT_IDENTITY_ENV_KEYS = ( "GITEA_MCP_CLIENT", "GITEA_MCP_CLIENT_INSTANCE", "GITEA_MCP_CLIENT_SESSION", ) #: Every production-consumed GITEA_* constant this change must keep allowlisted. #: Drift between these names and RECOGNIZED_GITEA_ENV_KEYS reopens the #975 #: unsupported-env mutation-gate failure mode. PRODUCTION_CONSUMED_GITEA_ENV_KEYS = CLIENT_IDENTITY_ENV_KEYS + ( mwi.HEARTBEAT_INTERVAL_ENV, ) def _registry() -> mwi.WorkerRegistry: """A registry on a throwaway path; never the operator's real one.""" handle, path = tempfile.mkstemp(suffix=".sqlite3") os.close(handle) os.unlink(path) return mwi.WorkerRegistry(path) def _attach( registry: mwi.WorkerRegistry, *, client: str = "claude_code", session: str = "sess-0001", generation: str = "gen-0001", profile: str = "prgs-author", role: str = "author", pid: int = 4242, now: datetime = NOW, ttl: float = TTL, ) -> dict: """Register one synthetic worker and return the outcome plus its identity.""" identity = mwi.generate_worker_identity(client, session, now=now) outcome = registry.register( worker_identity=identity, client_name=client, client_instance_id=f"inst-{session}", session_id=session, generation_id=generation, role=role, profile=profile, pid=pid, heartbeat_ttl_seconds=ttl, now=now, ) outcome["identity"] = identity return outcome def _supervisor( registry: mwi.WorkerRegistry, outcome: dict, *, client: str = "claude_code", session: str = "sess-0001", generation: str = "gen-0001", pid: int = 4242, ttl: float = TTL, interval_seconds: float | None = None, clock=None, ) -> mwi.WorkerHeartbeatSupervisor: """A supervisor bound to *outcome*'s registration. Never started here.""" return mwi.WorkerHeartbeatSupervisor( registry, worker_identity=outcome["identity"], fencing_epoch=outcome["fencing_epoch"], session_id=session, generation_id=generation, client_name=client, pid=pid, ttl_seconds=ttl, interval_seconds=interval_seconds, clock=clock, ) class _ManualClock: """Controlled time. Nothing in this suite sleeps to reach a TTL.""" def __init__(self, start: datetime = NOW) -> None: self.now = start def __call__(self) -> datetime: return self.now def advance(self, seconds: float) -> datetime: self.now = self.now + timedelta(seconds=seconds) return self.now # --- AC1-AC3: environment recognition ------------------------------------ class ClientIdentityEnvRecognitionTests(unittest.TestCase): """AC1-AC3: the keys production consumes are the keys the gate accepts.""" def test_each_client_identity_key_is_recognized(self): # AC1: individually asserted, so a partial fix cannot pass. for key in CLIENT_IDENTITY_ENV_KEYS: with self.subTest(key=key): self.assertIn( key, gitea_config.RECOGNIZED_GITEA_ENV_KEYS, f"{key} is read by the server but not recognized by the gate", ) def test_recognized_client_identity_keys_are_not_unconsumed(self): # AC3 at the classification layer the resolver reads: with only these # keys set there is no unsupported override to raise a blocker from. env = { "GITEA_MCP_CLIENT": "claude_code", "GITEA_MCP_CLIENT_INSTANCE": "inst-0001", "GITEA_MCP_CLIENT_SESSION": "sess-0001", mwi.HEARTBEAT_INTERVAL_ENV: "30", } self.assertEqual(gitea_config.get_unconsumed_gitea_env_overrides(env), {}) def test_heartbeat_interval_key_is_recognized(self): # #975 review 652 B1: the documented operator override must be accepted. self.assertEqual( mwi.HEARTBEAT_INTERVAL_ENV, "GITEA_WORKER_HEARTBEAT_INTERVAL_SECONDS" ) self.assertIn( mwi.HEARTBEAT_INTERVAL_ENV, gitea_config.RECOGNIZED_GITEA_ENV_KEYS ) self.assertEqual( gitea_config.get_unconsumed_gitea_env_overrides( {mwi.HEARTBEAT_INTERVAL_ENV: "30"} ), {}, ) def test_nearby_unknown_heartbeat_key_remains_rejected(self): # Exact key only — no broad GITEA_WORKER_* prefix that would admit typos. nearby = "GITEA_WORKER_HEARTBEAT_INTERVAL_SECONDS_EXTRA" env = { mwi.HEARTBEAT_INTERVAL_ENV: "30", nearby: "1", } unconsumed = gitea_config.get_unconsumed_gitea_env_overrides(env) self.assertIn(nearby, unconsumed) self.assertNotIn(mwi.HEARTBEAT_INTERVAL_ENV, unconsumed) def test_unknown_gitea_key_is_still_unsupported(self): # AC2: the gate is not broadened. An unrecognised override is refused # exactly as before, and the recognised siblings do not shield it. env = { "GITEA_MCP_CLIENT": "claude_code", mwi.HEARTBEAT_INTERVAL_ENV: "30", "GITEA_TOTALLY_INVENTED_OVERRIDE": "1", } unconsumed = gitea_config.get_unconsumed_gitea_env_overrides(env) self.assertIn("GITEA_TOTALLY_INVENTED_OVERRIDE", unconsumed) self.assertNotIn("GITEA_MCP_CLIENT", unconsumed) self.assertNotIn(mwi.HEARTBEAT_INTERVAL_ENV, unconsumed) def test_no_new_prefix_admits_arbitrary_client_keys(self): # A prefix would have been the lazy fix; it would admit anything # beginning GITEA_MCP_CLIENT, including typos and future unknowns. env = {"GITEA_MCP_CLIENT_SOMETHING_ELSE": "x"} self.assertIn( "GITEA_MCP_CLIENT_SOMETHING_ELSE", gitea_config.get_unconsumed_gitea_env_overrides(env), ) def test_server_consumes_exactly_the_recognized_key_names(self): # Guards the actual defect class: the constants production reads and the # allowlist drifting apart again. Includes HEARTBEAT_INTERVAL_ENV so the # #975 review 652 B1 regression cannot reappear silently. import gitea_mcp_server as mcp_server for name in ( mcp_server.CLIENT_NAME_ENV, mcp_server.CLIENT_INSTANCE_ENV, mcp_server.CLIENT_SESSION_ENV, mwi.HEARTBEAT_INTERVAL_ENV, ): with self.subTest(env_key=name): self.assertIn(name, gitea_config.RECOGNIZED_GITEA_ENV_KEYS) self.assertIn(name, PRODUCTION_CONSUMED_GITEA_ENV_KEYS) class RuntimeDiagnosticReasonPropagationTests(unittest.TestCase): """`unsupported-env:` must not be swallowed where `stale-runtime:` is not.""" def test_both_reason_families_are_hard_prefixes(self): import gitea_mcp_server as mcp_server self.assertIn("stale-runtime:", mcp_server.RUNTIME_DIAGNOSTIC_HARD_PREFIXES) self.assertIn("unsupported-env:", mcp_server.RUNTIME_DIAGNOSTIC_HARD_PREFIXES) def test_unsupported_env_message_is_recognized_as_hard(self): import gitea_mcp_server as mcp_server message = ( "unsupported-env: Unsupported GITEA_* environment variable override(s) " "detected: GITEA_INVENTED=1. Unknown env overrides are unsupported." ) self.assertTrue( any(p in message for p in mcp_server.RUNTIME_DIAGNOSTIC_HARD_PREFIXES) ) def test_unknown_override_still_produces_hard_unsupported_env_diagnostic(self): # Fail-closed classification: an unknown override remains unconsumed and # the hard-prefix family still matches the diagnostic reason the # capability resolver raises as runtime_reconnect_required. import gitea_mcp_server as mcp_server env = { mwi.HEARTBEAT_INTERVAL_ENV: "30", "GITEA_TOTALLY_INVENTED_OVERRIDE": "1", } unconsumed = gitea_config.get_unconsumed_gitea_env_overrides(env) self.assertIn("GITEA_TOTALLY_INVENTED_OVERRIDE", unconsumed) rendered = ( "unsupported-env: Unsupported GITEA_* environment variable override(s) " f"detected: {', '.join(f'{k}={v}' for k, v in sorted(unconsumed.items()))}. " "Unknown env overrides are unsupported." ) self.assertTrue( any(p in rendered for p in mcp_server.RUNTIME_DIAGNOSTIC_HARD_PREFIXES) ) # --- Interval policy ------------------------------------------------------ class HeartbeatIntervalPolicyTests(unittest.TestCase): """The interval must always leave room for lost beats inside the TTL.""" def test_default_interval_is_one_third_of_ttl(self): self.assertAlmostEqual(mwi.heartbeat_interval_for(900.0), 300.0) def test_interval_is_capped_at_half_the_ttl(self): # An override may not exceed the ceiling, or it would expire the very # registration it exists to renew. self.assertAlmostEqual(mwi.heartbeat_interval_for(900.0, 5000), 450.0) def test_override_below_the_ceiling_is_honoured(self): self.assertAlmostEqual(mwi.heartbeat_interval_for(900.0, 120), 120.0) def test_unparsable_or_nonpositive_override_falls_back_to_policy(self): for bad in ("", "abc", "0", "-5", None): with self.subTest(override=bad): self.assertAlmostEqual(mwi.heartbeat_interval_for(900.0, bad), 300.0) def test_interval_is_always_strictly_below_the_ttl(self): for ttl in (1.0, 30.0, 900.0, 14400.0): with self.subTest(ttl=ttl): self.assertLess(mwi.heartbeat_interval_for(ttl), ttl) # --- AC4-AC6: a healthy worker stays owned -------------------------------- class HealthyWorkerRemainsOwnedTests(unittest.TestCase): """AC4-AC6: the TTL becomes a liveness window instead of an attachment cap.""" def test_unsupervised_worker_goes_unowned_at_the_ttl(self): # The regression itself, asserted so the fix cannot be mistaken for a # TTL change: without a beater the row still expires exactly as before. registry = _registry() outcome = _attach(registry) record = registry.get(outcome["identity"]) self.assertEqual(record["started_at"], record["last_heartbeat_at"]) after_ttl = NOW + timedelta(seconds=TTL + 1) liveness = registry.is_live(record, now=after_ttl, pid_alive=True) self.assertFalse(liveness["live"]) assessment = mwi.assess_provenance( registry=registry, worker_identity=outcome["identity"], generation_id="gen-0001", env={"GITEA_CLIENT_MANAGED": "1"}, native_transport_bound=True, now=after_ttl, pid_alive_probe=lambda _pid: True, ) self.assertEqual(assessment["session_ownership"], "unowned") self.assertEqual(assessment["blocker_kind"], mwi.BLOCKER_NO_ATTACHMENT) def test_supervised_worker_stays_owned_past_two_full_ttls(self): # AC4: beyond two complete TTL periods of simulated time. registry = _registry() outcome = _attach(registry) clock = _ManualClock() supervisor = _supervisor(registry, outcome, clock=clock) interval = supervisor.interval_seconds self.assertLess(interval, TTL) elapsed = 0.0 target = TTL * 2 + interval while elapsed < target: clock.advance(interval) elapsed += interval result = supervisor.beat_once() self.assertTrue(result["renewed"], result.get("reasons")) assessment = mwi.assess_provenance( registry=registry, worker_identity=outcome["identity"], generation_id="gen-0001", env={"GITEA_CLIENT_MANAGED": "1"}, native_transport_bound=True, now=clock.now, pid_alive_probe=lambda _pid: True, ) self.assertEqual( assessment["session_ownership"], "owned", f"ownership lost after {elapsed:.0f}s of simulated time", ) self.assertGreater(elapsed, TTL * 2) def test_last_heartbeat_advances_and_diverges_from_started_at(self): # AC5. registry = _registry() outcome = _attach(registry) clock = _ManualClock() supervisor = _supervisor(registry, outcome, clock=clock) before = registry.get(outcome["identity"]) clock.advance(300) supervisor.beat_once() after = registry.get(outcome["identity"]) self.assertEqual(before["started_at"], after["started_at"]) self.assertNotEqual(after["started_at"], after["last_heartbeat_at"]) self.assertGreater(after["last_heartbeat_at"], before["last_heartbeat_at"]) def test_repeated_heartbeats_create_no_duplicate_rows(self): # AC6: heartbeating is not a second registration path. registry = _registry() outcome = _attach(registry) clock = _ManualClock() supervisor = _supervisor(registry, outcome, clock=clock) for _ in range(5): clock.advance(300) self.assertTrue(supervisor.beat_once()["renewed"]) workers = registry.list_workers() self.assertEqual(len(workers), 1) self.assertEqual(workers[0]["worker_identity"], outcome["identity"]) self.assertEqual(supervisor.status()["beats_renewed"], 5) # --- AC7, AC8, AC12: only the owner may renew ----------------------------- class HeartbeatOwnershipFencingTests(unittest.TestCase): """AC7/AC8/AC12: one worker can never refresh another worker's row.""" def test_two_client_names_stay_distinct_rows_and_identities(self): # AC7. registry = _registry() first = _attach(registry, client="claude_code", session="sess-a") second = _attach(registry, client="gemini", session="sess-b") self.assertNotEqual(first["identity"], second["identity"]) rows = {w["worker_identity"]: w for w in registry.list_workers()} self.assertEqual(len(rows), 2) self.assertEqual(rows[first["identity"]]["client_name"], "claude_code") self.assertEqual(rows[second["identity"]]["client_name"], "gemini") def test_wrong_session_cannot_refresh_the_row(self): # AC8, session dimension. registry = _registry() outcome = _attach(registry, session="sess-real") result = registry.heartbeat( worker_identity=outcome["identity"], fencing_epoch=outcome["fencing_epoch"], expected_session_id="sess-impostor", ) self.assertFalse(result["renewed"]) self.assertEqual(result["blocker_kind"], mwi.BLOCKER_FENCED) self.assertIn( "session_id", [field for field, _r, _p in result["expectation_drift"]] ) def test_wrong_generation_cannot_refresh_the_row(self): # AC8, generation dimension. registry = _registry() outcome = _attach(registry, generation="gen-real") result = registry.heartbeat( worker_identity=outcome["identity"], fencing_epoch=outcome["fencing_epoch"], expected_generation_id="gen-other", ) self.assertFalse(result["renewed"]) self.assertEqual(result["blocker_kind"], mwi.BLOCKER_FENCED) def test_wrong_client_name_cannot_refresh_the_row(self): registry = _registry() outcome = _attach(registry, client="claude_code") result = registry.heartbeat( worker_identity=outcome["identity"], fencing_epoch=outcome["fencing_epoch"], expected_client_name="gemini", ) self.assertFalse(result["renewed"]) self.assertEqual(result["blocker_kind"], mwi.BLOCKER_FENCED) def test_wrong_pid_cannot_refresh_the_row(self): registry = _registry() outcome = _attach(registry, pid=4242) result = registry.heartbeat( worker_identity=outcome["identity"], fencing_epoch=outcome["fencing_epoch"], expected_pid=9999, ) self.assertFalse(result["renewed"]) self.assertEqual(result["blocker_kind"], mwi.BLOCKER_FENCED) def test_namespaces_of_one_application_share_a_client_name(self): # Several namespaces of one application must stay one client, while # separate applications stay distinct. Aliases normalise, and the # expectation check compares normalised values, so an author and a # reviewer namespace of the same product are not treated as impostors. registry = _registry() outcome = _attach(registry, client="claude") self.assertEqual( registry.get(outcome["identity"])["client_name"], "claude_code" ) result = registry.heartbeat( worker_identity=outcome["identity"], fencing_epoch=outcome["fencing_epoch"], expected_client_name="claude_desktop", ) self.assertTrue(result["renewed"], result.get("reasons")) def test_matching_expectations_renew_normally(self): registry = _registry() outcome = _attach(registry) result = registry.heartbeat( worker_identity=outcome["identity"], fencing_epoch=outcome["fencing_epoch"], expected_session_id="sess-0001", expected_generation_id="gen-0001", expected_client_name="claude_code", expected_pid=4242, ) self.assertTrue(result["renewed"], result.get("reasons")) def test_omitted_expectations_preserve_pre_fix_behaviour(self): # AC12: the #948 contract is unchanged for callers presenting nothing. registry = _registry() outcome = _attach(registry) self.assertTrue( registry.heartbeat( worker_identity=outcome["identity"], fencing_epoch=outcome["fencing_epoch"], )["renewed"] ) def test_stale_fencing_epoch_is_still_refused(self): # AC12: the pre-existing epoch fence is untouched. registry = _registry() outcome = _attach(registry) result = registry.heartbeat( worker_identity=outcome["identity"], fencing_epoch=int(outcome["fencing_epoch"]) + 1, ) self.assertFalse(result["renewed"]) self.assertEqual(result["blocker_kind"], mwi.BLOCKER_FENCED) def test_fenced_supervisor_stops_permanently(self): # A fenced session must never beat its way back into ownership. registry = _registry() outcome = _attach(registry) supervisor = mwi.WorkerHeartbeatSupervisor( registry, worker_identity=outcome["identity"], fencing_epoch=int(outcome["fencing_epoch"]) + 1, session_id="sess-0001", generation_id="gen-0001", client_name="claude_code", pid=4242, ttl_seconds=TTL, clock=_ManualClock(), ) first = supervisor.beat_once() self.assertFalse(first["renewed"]) self.assertIsNotNone(supervisor.stopped_reason) # A second attempt does not even reach the registry. second = supervisor.beat_once() self.assertFalse(second["renewed"]) self.assertFalse(second["beat_attempted"]) self.assertEqual(supervisor.status()["beats_attempted"], 1) def test_missing_registration_stops_the_supervisor(self): registry = _registry() supervisor = mwi.WorkerHeartbeatSupervisor( registry, worker_identity=mwi.generate_worker_identity("grok", "sess-gone", now=NOW), fencing_epoch=1, ttl_seconds=TTL, clock=_ManualClock(), ) result = supervisor.beat_once() self.assertEqual(result["blocker_kind"], mwi.BLOCKER_NO_ATTACHMENT) self.assertIsNotNone(supervisor.stopped_reason) # --- AC9, AC10, AC11: stopping and expiry --------------------------------- class ShutdownAndExpiryTests(unittest.TestCase): """AC9-AC11: orderly shutdown stops beating; dead workers still expire.""" def test_stop_ends_the_heartbeat(self): # AC9. registry = _registry() outcome = _attach(registry) clock = _ManualClock() supervisor = _supervisor(registry, outcome, clock=clock) clock.advance(300) self.assertTrue(supervisor.beat_once()["renewed"]) supervisor.stop(reason="orderly shutdown") self.assertFalse(supervisor.status()["running"]) self.assertEqual(supervisor.stopped_reason, "orderly shutdown") clock.advance(300) after_stop = supervisor.beat_once() self.assertFalse(after_stop["renewed"]) self.assertFalse(after_stop["beat_attempted"]) def test_stop_is_idempotent_and_keeps_the_first_reason(self): registry = _registry() outcome = _attach(registry) supervisor = _supervisor(registry, outcome, clock=_ManualClock()) supervisor.stop(reason="orderly shutdown") supervisor.stop(reason="second call") self.assertEqual(supervisor.stopped_reason, "orderly shutdown") def test_stopped_worker_becomes_unowned_once_the_ttl_elapses(self): # AC10: stopping beating restores normal expiration; the fix does not # make a worker immortal. registry = _registry() outcome = _attach(registry) clock = _ManualClock() supervisor = _supervisor(registry, outcome, clock=clock) clock.advance(300) supervisor.beat_once() supervisor.stop(reason="orderly shutdown") last_beat = clock.now clock.advance(TTL + 1) record = registry.get(outcome["identity"]) liveness = registry.is_live(record, now=clock.now, pid_alive=True) self.assertFalse(liveness["live"]) self.assertGreater(liveness["heartbeat_age_seconds"], TTL) assessment = mwi.assess_provenance( registry=registry, worker_identity=outcome["identity"], generation_id="gen-0001", env={"GITEA_CLIENT_MANAGED": "1"}, native_transport_bound=True, now=clock.now, pid_alive_probe=lambda _pid: True, ) self.assertEqual(assessment["session_ownership"], "unowned") self.assertLess(last_beat, clock.now) def test_dead_worker_expires_even_with_a_live_pid(self): # A recycled or long-lived pid must not keep a silent worker owned. registry = _registry() outcome = _attach(registry) record = registry.get(outcome["identity"]) liveness = registry.is_live( record, now=NOW + timedelta(seconds=TTL + 1), pid_alive=True ) self.assertFalse(liveness["live"]) self.assertFalse(liveness["heartbeat_fresh"]) def test_one_worker_expiring_does_not_stale_healthy_workers(self): # AC11. registry = _registry() healthy = _attach(registry, client="claude_code", session="sess-live") abandoned = _attach(registry, client="gemini", session="sess-dead") clock = _ManualClock() supervisor = _supervisor( registry, healthy, client="claude_code", session="sess-live", clock=clock, ) elapsed = 0.0 while elapsed < TTL + 300: clock.advance(supervisor.interval_seconds) elapsed += supervisor.interval_seconds self.assertTrue(supervisor.beat_once()["renewed"]) healthy_live = registry.is_live( registry.get(healthy["identity"]), now=clock.now, pid_alive=True ) abandoned_live = registry.is_live( registry.get(abandoned["identity"]), now=clock.now, pid_alive=True ) self.assertTrue(healthy_live["live"]) self.assertFalse(abandoned_live["live"]) # --- Failure handling and observability ----------------------------------- class HeartbeatFailureHandlingTests(unittest.TestCase): """A heartbeat failure never grants ownership and never crashes a caller.""" def test_transient_exception_is_counted_and_beating_continues(self): registry = _registry() outcome = _attach(registry) clock = _ManualClock() supervisor = _supervisor(registry, outcome, clock=clock) calls = {"n": 0} real = registry.heartbeat def flaky(**kwargs): calls["n"] += 1 if calls["n"] == 1: raise RuntimeError("database is locked") return real(**kwargs) registry.heartbeat = flaky # type: ignore[method-assign] try: clock.advance(300) first = supervisor.beat_once() self.assertFalse(first["renewed"]) self.assertTrue(first["transient"]) self.assertIsNone(supervisor.stopped_reason) clock.advance(300) second = supervisor.beat_once() self.assertTrue(second["renewed"], second.get("reasons")) finally: registry.heartbeat = real # type: ignore[method-assign] status = supervisor.status() self.assertEqual(status["transient_failures"], 1) self.assertEqual(status["beats_renewed"], 1) self.assertTrue(status["supervised"]) def test_status_reports_an_unstarted_supervisor_honestly(self): registry = _registry() outcome = _attach(registry) status = _supervisor(registry, outcome, clock=_ManualClock()).status() self.assertTrue(status["supervised"]) self.assertFalse(status["started"]) self.assertFalse(status["running"]) self.assertEqual(status["beats_renewed"], 0) self.assertIsNone(status["stopped_reason"]) def test_runtime_context_reports_unsupervised_when_none_attached(self): # The observability surface must distinguish "no supervisor" from # "supervisor with no beats yet", or the regression is invisible again. import gitea_mcp_server as mcp_server saved = mcp_server._WORKER_HEARTBEAT_SUPERVISOR mcp_server._WORKER_HEARTBEAT_SUPERVISOR = None try: status = mcp_server._worker_heartbeat_status() finally: mcp_server._WORKER_HEARTBEAT_SUPERVISOR = saved self.assertFalse(status["supervised"]) self.assertFalse(status["running"]) self.assertTrue(status["reasons"]) def test_runtime_context_surfaces_an_attached_supervisor(self): import gitea_mcp_server as mcp_server registry = _registry() outcome = _attach(registry) supervisor = _supervisor(registry, outcome, clock=_ManualClock()) saved = mcp_server._WORKER_HEARTBEAT_SUPERVISOR mcp_server._WORKER_HEARTBEAT_SUPERVISOR = supervisor try: status = mcp_server._worker_heartbeat_status() finally: mcp_server._WORKER_HEARTBEAT_SUPERVISOR = saved self.assertTrue(status["supervised"]) self.assertEqual(status["worker_identity"], outcome["identity"]) self.assertEqual(status["heartbeat_ttl_seconds"], TTL) self.assertLess(status["heartbeat_interval_seconds"], TTL) def test_status_failure_degrades_instead_of_raising(self): import gitea_mcp_server as mcp_server class Exploding: def status(self): raise RuntimeError("boom") saved = mcp_server._WORKER_HEARTBEAT_SUPERVISOR mcp_server._WORKER_HEARTBEAT_SUPERVISOR = Exploding() try: status = mcp_server._worker_heartbeat_status() finally: mcp_server._WORKER_HEARTBEAT_SUPERVISOR = saved self.assertTrue(status["supervised"]) self.assertFalse(status["running"]) self.assertTrue(status["reasons"]) class SupervisorThreadLoopTests(unittest.TestCase): """The real thread, on a millisecond interval. Never waits for a TTL.""" def test_thread_beats_repeatedly_then_stops_on_request(self): registry = _registry() outcome = _attach(registry) beats = threading.Event() seen = {"n": 0} real = registry.heartbeat def counting(**kwargs): result = real(**kwargs) seen["n"] += 1 if seen["n"] >= 3: beats.set() return result registry.heartbeat = counting # type: ignore[method-assign] supervisor = _supervisor(registry, outcome, ttl=1.0, interval_seconds=0.01) try: started = supervisor.start() self.assertTrue(started["started"]) self.assertTrue( beats.wait(timeout=10.0), "heartbeat thread produced no beats" ) self.assertTrue(supervisor.status()["running"]) finally: supervisor.stop(reason="test teardown") registry.heartbeat = real # type: ignore[method-assign] self.assertGreaterEqual(supervisor.status()["beats_renewed"], 3) self.assertFalse(supervisor.status()["running"]) # Stopping is prompt: the loop waits on an Event, not a sleep. deadline = time.monotonic() + 5.0 while supervisor._thread is not None and supervisor._thread.is_alive(): if time.monotonic() > deadline: self.fail("heartbeat thread did not exit after stop()") time.sleep(0.01) def test_start_is_idempotent(self): registry = _registry() outcome = _attach(registry) supervisor = _supervisor(registry, outcome, ttl=1.0, interval_seconds=0.05) try: self.assertFalse(supervisor.start()["already_running"]) self.assertTrue(supervisor.start()["already_running"]) finally: supervisor.stop(reason="test teardown") def test_start_refuses_after_a_terminal_stop(self): registry = _registry() outcome = _attach(registry) supervisor = _supervisor(registry, outcome, clock=_ManualClock()) supervisor.stop(reason="orderly shutdown") self.assertFalse(supervisor.start()["started"]) # --- #975 review 652 B2: production registration → heartbeat wiring --------- @contextmanager def _production_worker_lifecycle( *, client: str = "claude_code", instance: str = "inst-prod-0001", session: str = "sess-prod-0001", interval: str | None = "30", pin_registry: bool = True, ) -> Iterator[tuple[str | None, object]]: """Reset process-local worker state and drive the real production entry path. Pins ``GITEA_WORKER_REGISTRY_DB`` so pytest may open a registry, then restores every module global and environment key on exit. Never writes to the operator's real registry. """ import gitea_mcp_server as mcp_server handle, path = tempfile.mkstemp(suffix=".sqlite3") os.close(handle) os.unlink(path) env_keys = ( mwi.REGISTRY_PATH_ENV, mwi.HEARTBEAT_INTERVAL_ENV, mcp_server.CLIENT_NAME_ENV, mcp_server.CLIENT_INSTANCE_ENV, mcp_server.CLIENT_SESSION_ENV, ) saved_env = {key: os.environ.get(key) for key in env_keys} saved = { "registry": mcp_server._WORKER_REGISTRY, "identity": mcp_server._WORKER_IDENTITY, "generation": mcp_server._WORKER_GENERATION, "attempted": mcp_server._WORKER_REGISTRATION_ATTEMPTED, "supervisor": mcp_server._WORKER_HEARTBEAT_SUPERVISOR, } mcp_server._WORKER_REGISTRY = None mcp_server._WORKER_IDENTITY = None mcp_server._WORKER_GENERATION = None mcp_server._WORKER_REGISTRATION_ATTEMPTED = False mcp_server._WORKER_HEARTBEAT_SUPERVISOR = None if pin_registry: os.environ[mwi.REGISTRY_PATH_ENV] = path else: os.environ.pop(mwi.REGISTRY_PATH_ENV, None) os.environ[mcp_server.CLIENT_NAME_ENV] = client os.environ[mcp_server.CLIENT_INSTANCE_ENV] = instance os.environ[mcp_server.CLIENT_SESSION_ENV] = session if interval is None: os.environ.pop(mwi.HEARTBEAT_INTERVAL_ENV, None) else: os.environ[mwi.HEARTBEAT_INTERVAL_ENV] = str(interval) try: yield (path if pin_registry else None), mcp_server finally: supervisor = mcp_server._WORKER_HEARTBEAT_SUPERVISOR if supervisor is not None: try: supervisor.stop(reason="test teardown") except Exception: pass mcp_server._WORKER_REGISTRY = saved["registry"] mcp_server._WORKER_IDENTITY = saved["identity"] mcp_server._WORKER_GENERATION = saved["generation"] mcp_server._WORKER_REGISTRATION_ATTEMPTED = saved["attempted"] mcp_server._WORKER_HEARTBEAT_SUPERVISOR = saved["supervisor"] for key, value in saved_env.items(): if value is None: os.environ.pop(key, None) else: os.environ[key] = value try: os.unlink(path) except OSError: pass for suffix in ("-wal", "-shm"): try: os.unlink(path + suffix) except OSError: pass class ProductionWorkerLifecycleWiringTests(unittest.TestCase): """#975 review 652 B2: exercise the real production orchestration path. These tests call ``_active_worker_identity()`` / ``_start_worker_heartbeat`` rather than the ``_attach`` / ``_supervisor`` factories. Deleting the production wiring call must fail them. """ def test_successful_registration_starts_heartbeat_supervision(self): # (1) Successful worker registration starts heartbeat supervision. with _production_worker_lifecycle() as (path, mcp_server): self.assertIsNone(mcp_server._WORKER_HEARTBEAT_SUPERVISOR) identity = mcp_server._active_worker_identity() self.assertIsNotNone(identity) supervisor = mcp_server._WORKER_HEARTBEAT_SUPERVISOR self.assertIsNotNone( supervisor, "production registration must call _start_worker_heartbeat; " "removing that call must fail this test", ) self.assertIs(supervisor, mcp_server._WORKER_HEARTBEAT_SUPERVISOR) self.assertEqual(supervisor.worker_identity, identity) status = mcp_server._worker_heartbeat_status() self.assertTrue(status["supervised"]) self.assertEqual(status["worker_identity"], identity) # Under pytest the thread is deliberately not auto-started; the # supervisor is still attached by the production wiring. registry = mwi.WorkerRegistry(path) self.assertIsNotNone(registry.get(identity)) def test_supervision_not_started_before_registration_succeeds(self): # (2) Heartbeat supervision is not started before registration succeeds. with _production_worker_lifecycle(pin_registry=False) as (_path, mcp_server): self.assertIsNone(mcp_server._WORKER_HEARTBEAT_SUPERVISOR) self.assertIsNone(mcp_server._WORKER_IDENTITY) identity = mcp_server._active_worker_identity() self.assertIsNone(identity) self.assertIsNone(mcp_server._WORKER_HEARTBEAT_SUPERVISOR) status = mcp_server._worker_heartbeat_status() self.assertFalse(status["supervised"]) def test_repeated_active_worker_identity_no_duplicate_supervisor_or_rows(self): # (3) Repeated _active_worker_identity() calls create neither a second # supervisor nor a second registry row. with _production_worker_lifecycle() as (path, mcp_server): first = mcp_server._active_worker_identity() supervisor_first = mcp_server._WORKER_HEARTBEAT_SUPERVISOR second = mcp_server._active_worker_identity() third = mcp_server._active_worker_identity() self.assertEqual(first, second) self.assertEqual(second, third) self.assertIs(supervisor_first, mcp_server._WORKER_HEARTBEAT_SUPERVISOR) registry = mwi.WorkerRegistry(path) rows = registry.list_workers(status=None) matching = [ row for row in rows if row.get("worker_identity") == first ] self.assertEqual(len(matching), 1) def test_supervisor_receives_exact_registered_identity_fields(self): # (4) Exact normalized identity, client, session, generation, process # identity, and fencing epoch reach the supervisor from production # expressions — not from shared helper constants on both sides. client_raw = "Claude-Code" session = "sess-prod-exact-fields" instance = "inst-exact-fields" with _production_worker_lifecycle( client=client_raw, instance=instance, session=session, interval="45" ) as (path, mcp_server): identity = mcp_server._active_worker_identity() self.assertIsNotNone(identity) supervisor = mcp_server._WORKER_HEARTBEAT_SUPERVISOR self.assertIsNotNone(supervisor) registry = mwi.WorkerRegistry(path) record = registry.get(identity) self.assertIsNotNone(record) # Record side comes from register(); supervisor side from # _start_worker_heartbeat. They must agree. self.assertEqual(supervisor.worker_identity, record["worker_identity"]) self.assertEqual(int(supervisor.fencing_epoch), int(record["fencing_epoch"])) self.assertEqual(supervisor.session_id, record["session_id"]) self.assertEqual(supervisor.generation_id, record["generation_id"]) self.assertEqual( supervisor.client_name, mwi.normalize_client_name(record["client_name"]), ) self.assertEqual(supervisor.pid, int(record["pid"])) self.assertEqual(supervisor.pid, os.getpid()) self.assertEqual(supervisor.session_id, session) self.assertEqual( supervisor.client_name, mwi.normalize_client_name(client_raw) ) # Generation is process-minted, not a test helper constant. self.assertEqual(supervisor.generation_id, mcp_server._WORKER_GENERATION) self.assertTrue(str(supervisor.generation_id)) def test_failed_registration_leaves_no_active_supervisor(self): # (5) Failed registration (no openable registry under pytest) leaves no # supervisor. Fenced path covered below via claim_generation. with _production_worker_lifecycle(pin_registry=False) as (_path, mcp_server): identity = mcp_server._active_worker_identity() self.assertIsNone(identity) self.assertIsNone(mcp_server._WORKER_HEARTBEAT_SUPERVISOR) self.assertTrue(mcp_server._WORKER_REGISTRATION_ATTEMPTED) def test_fenced_registration_stops_production_supervisor(self): # (5 continued) After production wiring attaches a supervisor, a fenced # beat must stop it permanently and leave it non-running. with _production_worker_lifecycle() as (path, mcp_server): identity = mcp_server._active_worker_identity() supervisor = mcp_server._WORKER_HEARTBEAT_SUPERVISOR self.assertIsNotNone(identity) self.assertIsNotNone(supervisor) registry = mwi.WorkerRegistry(path) record = registry.get(identity) claim = registry.claim_generation( worker_identity=identity, generation_id=record["generation_id"], ) self.assertTrue(claim.get("claimed") or claim.get("success")) # Bumped epoch fences the production-started supervisor's cached epoch. self.assertNotEqual( int(claim["fencing_epoch"]), int(supervisor.fencing_epoch) ) result = supervisor.beat_once(now=NOW) self.assertFalse(result.get("renewed")) self.assertEqual(result.get("blocker_kind"), mwi.BLOCKER_FENCED) self.assertIsNotNone(supervisor.stopped_reason) self.assertFalse(supervisor.status()["running"]) # Restart is refused after a terminal stop. self.assertFalse(supervisor.start()["started"]) def test_orderly_shutdown_stops_production_supervisor_idempotently(self): # (6) Orderly shutdown stops the production-started supervisor idempotently. with _production_worker_lifecycle() as (_path, mcp_server): identity = mcp_server._active_worker_identity() supervisor = mcp_server._WORKER_HEARTBEAT_SUPERVISOR self.assertIsNotNone(identity) self.assertIsNotNone(supervisor) # Simulate non-pytest start, then atexit-style stop. started = supervisor.start() self.assertTrue(started["started"]) first = supervisor.stop(reason="orderly shutdown") second = supervisor.stop(reason="orderly shutdown") self.assertTrue(first["stopped"]) self.assertTrue(second["stopped"]) self.assertEqual(supervisor.stopped_reason, "orderly shutdown") self.assertFalse(supervisor.status()["running"]) self.assertFalse(supervisor.start()["started"]) def test_removing_production_wiring_would_fail_this_suite(self): # (7) Explicit wiring proof: _active_worker_identity must invoke # _start_worker_heartbeat. A spy confirms the production call site is # reached with the registered identity; a missing call fails the assert. with _production_worker_lifecycle() as (_path, mcp_server): seen: list[dict] = [] real = mcp_server._start_worker_heartbeat def spy(**kwargs): seen.append(dict(kwargs)) return real(**kwargs) mcp_server._start_worker_heartbeat = spy # type: ignore[assignment] try: identity = mcp_server._active_worker_identity() finally: mcp_server._start_worker_heartbeat = real # type: ignore[assignment] self.assertIsNotNone(identity) self.assertEqual(len(seen), 1, "production path must call _start_worker_heartbeat exactly once") self.assertEqual(seen[0]["identity"], identity) self.assertIsNotNone(seen[0]["fencing_epoch"]) self.assertEqual(seen[0]["hints"]["session_id"], "sess-prod-0001") self.assertIsNotNone(mcp_server._WORKER_HEARTBEAT_SUPERVISOR) def test_configured_interval_reaches_production_supervisor(self): # (8) The configured heartbeat interval reaches the production supervisor. with _production_worker_lifecycle(interval="30") as (_path, mcp_server): identity = mcp_server._active_worker_identity() supervisor = mcp_server._WORKER_HEARTBEAT_SUPERVISOR self.assertIsNotNone(identity) self.assertIsNotNone(supervisor) expected = mwi.heartbeat_interval_for( mwi.DEFAULT_HEARTBEAT_TTL_SECONDS, "30" ) self.assertAlmostEqual(supervisor.interval_seconds, expected) self.assertAlmostEqual(supervisor.interval_seconds, 30.0) status = mcp_server._worker_heartbeat_status() self.assertAlmostEqual(status["heartbeat_interval_seconds"], 30.0) def test_sqlite_work_remains_off_the_async_event_loop(self): # (9) Heartbeat sqlite writes run on a daemon thread, not the asyncio loop. with _production_worker_lifecycle(interval="0.05") as (path, mcp_server): identity = mcp_server._active_worker_identity() supervisor = mcp_server._WORKER_HEARTBEAT_SUPERVISOR self.assertIsNotNone(identity) self.assertIsNotNone(supervisor) registry = mwi.WorkerRegistry(path) beat_threads: list[str] = [] real = registry.heartbeat def observing(**kwargs): beat_threads.append(threading.current_thread().name) # Rebind the production supervisor's registry method path. return real(**kwargs) # Point the production supervisor at the observing registry methods # by swapping the bound registry on the supervisor instance. supervisor._registry.heartbeat = observing # type: ignore[method-assign] try: started = supervisor.start() self.assertTrue(started["started"]) thread = supervisor._thread self.assertIsNotNone(thread) self.assertTrue(thread.daemon) self.assertIn("gitea-worker-heartbeat", thread.name) # Force one beat immediately (the loop waits first). supervisor.beat_once(now=NOW + timedelta(seconds=1)) # Also let the thread run at least one scheduled beat. deadline = time.monotonic() + 5.0 while supervisor.beats_attempted < 2 and time.monotonic() < deadline: time.sleep(0.02) finally: supervisor.stop(reason="test teardown") try: supervisor._registry.heartbeat = real # type: ignore[method-assign] except Exception: pass self.assertGreaterEqual(supervisor.beats_attempted, 1) # Synchronous beat_once may run on the test thread; the background # loop must still be a distinct daemon worker-heartbeat thread. self.assertTrue( any("gitea-worker-heartbeat" in name for name in beat_threads) or ( supervisor._thread is not None and "gitea-worker-heartbeat" in supervisor._thread.name ) ) if __name__ == "__main__": unittest.main()