Files
Gitea-Tools/tests/test_mcp_fleet_inventory.py
T
sysadminandClaude Opus 5 92615f474b feat(control-plane): add authoritative native MCP fleet inventory (#949)
Every pre-existing runtime surface is per-process. gitea_get_runtime_context
and gitea_assess_master_parity describe only the server answering the call,
and gitea_assess_mcp_namespace_health accepts process, probe_result and
registered_tools from the caller, so it cannot constrain the caller. The
control-plane sessions table records allocator task sessions, not server
processes. Five self-reports of one revision therefore never proved that five
processes exist, that no sixth exists, or that all five share one client
cohort.

Add gitea_assess_fleet_inventory, a strictly read-only capability that takes
no evidence parameters. It combines two sources that must agree:

- a control-plane runtime registry row each server writes about itself,
  from the official entrypoint, immediately after the native transport bind;
- a process observation the answering server performs, never the caller.

A member is running only when both agree and the observed process predates
its registration, so configuration alone never counts and a recycled PID
cannot impersonate an exited server. Classification is a pure function of the
snapshot, so gitea-controller and gitea-reconciler return the same verdict.

Missing, duplicate, unexpected, stale and unregistered members are reported
in separate fields rather than collapsed into one error, because each needs a
different operator action. single_cohort is derived only from recorded cohort
identity and stays null when unknown, so matching revisions never establish a
cohort. Any unreadable registry, unavailable listing, unregistered process,
unknown cohort or unknown revision sets inventory_complete and
mutation_gate_satisfied false with a specific blocked_reason.

The capability performs no restart, reconnect, drain, lease mutation, issue
mutation or process termination, never terminates a duplicate, and never
manufactures restart evidence. The only signal sent is signal 0.

Also resolves the fleet namespace from the expected roster:
role_namespace_gate.infer_mcp_namespace recognises only author and reviewer
and echoes the profile name otherwise, which would have mislabelled the
controller, merger and reconciler members. Widening that shared helper is
controller role-metadata work owned by #950 and is deliberately not done here.

Schema v5 to v6 is additive and idempotent: one new table, no existing table,
tool signature or result field changed. Two control-plane tests that pinned
the schema version to the literal 5 now assert against SCHEMA_VERSION.

#950 (controller role metadata), #951 (restart receipts) and #952 (stale-lease
consistency) remain separate and untouched.

Tests: 118 new across tests/test_mcp_fleet_inventory.py,
tests/test_control_plane_db_server_runtimes.py and
tests/test_fleet_inventory_tool.py, covering every acceptance criterion
including the multi-LLM duplicate-server regression that motivated the issue.

Closes #949

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01NBaQKcRRKeKbZuhk72MhEf
2026-07-27 23:25:45 -04:00

789 lines
31 KiB
Python

"""Authoritative fleet inventory classification (#949).
One test group per acceptance criterion. The classifier is pure, so every
scenario is expressed as a snapshot: registry rows plus a process observation.
PID liveness is the one impure input, so it is patched per test rather than
depending on whatever happens to be running on the machine.
"""
import os
import unittest
from datetime import datetime, timedelta, timezone
from unittest import mock
import mcp_fleet_inventory as mfi
NOW = datetime(2026, 7, 28, 3, 0, 0, tzinfo=timezone.utc)
HEAD_A = "82d71b77028a7abd4f8ab4a4e4d89658a187f73d"
HEAD_B = "35ed8a2fcb11134a37c862ca6eaca26e3028902a"
COHORT_A = "ppid:40990"
COHORT_B = "ppid:51022"
BINDING = {"remote": "prgs", "org": "Scaled-Tech-Consulting", "repo": "Gitea-Tools"}
_BASE_PID = 41000
def row(
namespace,
profile,
role,
pid,
*,
cohort_id=COHORT_A,
startup_head=HEAD_A,
registered_at="2026-07-28T02:00:00Z",
**overrides,
):
"""One control-plane runtime registry row."""
record = {
"runtime_id": f"{namespace}:{pid}:boot{pid}",
"namespace": namespace,
"profile": profile,
"role": role,
"remote": BINDING["remote"],
"org": BINDING["org"],
"repo": BINDING["repo"],
"repository_root": "/checkout/Gitea-Tools",
"pid": pid,
"cohort_id": cohort_id,
"cohort_source": "parent_process",
"client_provenance": "client_managed",
"boot_id": f"boot{pid}",
"startup_head": startup_head,
"daemon_start_head": startup_head,
"transport": "stdio",
"registered_at": registered_at,
"last_heartbeat_at": registered_at,
"status": "running",
}
record.update(overrides)
return record
def healthy_rows():
"""One live row per expected member, all one cohort, all one revision."""
return [
row(entry["namespace"], entry["profile"], entry["role"], _BASE_PID + index)
for index, entry in enumerate(mfi.EXPECTED_PRGS_FLEET)
]
def scan_for(rows, *, available=True, extra_pids=(), started_at="2026-07-28T01:00:00Z"):
"""A process observation that corroborates *rows* (plus any extra PIDs)."""
processes = [
{"pid": r["pid"], "started_at": started_at, "command": "python mcp_server.py"}
for r in rows
]
processes.extend(
{"pid": pid, "started_at": started_at, "command": "python mcp_server.py"}
for pid in extra_pids
)
return {"available": available, "processes": processes, "reason": None}
def classify(rows, scan=None, **kwargs):
kwargs.setdefault("expected_binding", BINDING)
kwargs.setdefault("now", NOW)
return mfi.classify_fleet_inventory(
runtime_rows=rows,
process_scan=scan if scan is not None else scan_for(rows),
**kwargs,
)
class _AliveMixin:
"""Treat every PID as alive unless a test declares it dead or unknown."""
def setUp(self):
super().setUp()
self.dead_pids = set()
self.unknown_pids = set()
def probe(pid):
if pid in self.unknown_pids:
return None
return pid not in self.dead_pids
patcher = mock.patch.object(mfi, "probe_pid_alive", side_effect=probe)
patcher.start()
self.addCleanup(patcher.stop)
class TestHealthyFleet(_AliveMixin, unittest.TestCase):
"""AC1: a healthy five-server fleet reports all five members exactly once."""
def test_five_members_each_reported_once(self):
result = classify(healthy_rows())
self.assertEqual(len(result["configured_members"]), 5)
self.assertEqual(len(result["running_members"]), 5)
self.assertEqual(result["running_member_count"], 5)
self.assertEqual(result["missing_members"], [])
self.assertEqual(result["duplicate_members"], [])
self.assertEqual(result["unexpected_members"], [])
self.assertTrue(result["exactly_one_per_profile"])
for member in result["configured_members"]:
self.assertEqual(member["instance_count"], 1)
self.assertEqual(member["health"], mfi.HEALTH_RUNNING)
def test_healthy_fleet_satisfies_the_mutation_gate(self):
result = classify(healthy_rows())
self.assertTrue(result["inventory_complete"])
self.assertTrue(result["mutation_gate_satisfied"])
self.assertIsNone(result["blocked_reason"])
self.assertTrue(result["single_cohort"])
self.assertFalse(result["mixed_cohort"])
self.assertFalse(result["mixed_revision"])
def test_every_expected_namespace_and_profile_is_present(self):
result = classify(healthy_rows())
self.assertEqual(
sorted(m["profile"] for m in result["configured_members"]),
[
"prgs-author",
"prgs-controller",
"prgs-merger",
"prgs-reconciler",
"prgs-reviewer",
],
)
class TestDuplicateMember(_AliveMixin, unittest.TestCase):
"""AC2: two processes on one profile are a duplicate and fail the gate."""
def test_duplicate_is_reported_with_every_pid(self):
rows = healthy_rows()
rows.append(row("gitea-author", "prgs-author", "author", 49999))
result = classify(rows)
self.assertEqual(len(result["duplicate_members"]), 1)
duplicate = result["duplicate_members"][0]
self.assertEqual(duplicate["profile"], "prgs-author")
self.assertEqual(duplicate["instance_count"], 2)
self.assertEqual(duplicate["pids"], [_BASE_PID, 49999])
def test_duplicate_fails_the_mutation_gate(self):
rows = healthy_rows()
rows.append(row("gitea-author", "prgs-author", "author", 49999))
result = classify(rows)
self.assertFalse(result["exactly_one_per_profile"])
self.assertFalse(result["mutation_gate_satisfied"])
self.assertIn("duplicate", result["blocked_reason"])
def test_duplicate_is_not_reported_as_missing_or_unexpected(self):
rows = healthy_rows()
rows.append(row("gitea-author", "prgs-author", "author", 49999))
result = classify(rows)
self.assertEqual(result["missing_members"], [])
self.assertEqual(result["unexpected_members"], [])
class TestMissingMember(_AliveMixin, unittest.TestCase):
"""AC3: a missing expected server is identified and fails the gate."""
def test_missing_member_is_named(self):
rows = [r for r in healthy_rows() if r["profile"] != "prgs-merger"]
result = classify(rows)
self.assertEqual(len(result["missing_members"]), 1)
self.assertEqual(result["missing_members"][0]["profile"], "prgs-merger")
self.assertEqual(result["missing_members"][0]["health"], mfi.HEALTH_MISSING)
def test_missing_member_fails_the_mutation_gate(self):
rows = [r for r in healthy_rows() if r["profile"] != "prgs-merger"]
result = classify(rows)
self.assertFalse(result["exactly_one_per_profile"])
self.assertFalse(result["mutation_gate_satisfied"])
self.assertIn("prgs-merger", result["blocked_reason"])
def test_missing_member_still_lists_all_configured_members(self):
rows = [r for r in healthy_rows() if r["profile"] != "prgs-merger"]
result = classify(rows)
self.assertEqual(len(result["configured_members"]), 5)
merger = [
m for m in result["configured_members"] if m["profile"] == "prgs-merger"
][0]
self.assertEqual(merger["instance_count"], 0)
self.assertEqual(merger["health"], mfi.HEALTH_MISSING)
class TestUnexpectedMember(_AliveMixin, unittest.TestCase):
"""AC4: an unexpected PRGS server is reported explicitly."""
def test_unexpected_member_is_its_own_category(self):
rows = healthy_rows()
rows.append(row("gitea-shadow", "prgs-shadow", "author", 47777))
result = classify(rows)
self.assertEqual(len(result["unexpected_members"]), 1)
self.assertEqual(result["unexpected_members"][0]["profile"], "prgs-shadow")
self.assertEqual(
result["unexpected_members"][0]["health"], mfi.HEALTH_UNEXPECTED
)
def test_unexpected_member_is_not_collapsed_into_duplicates_or_missing(self):
rows = healthy_rows()
rows.append(row("gitea-shadow", "prgs-shadow", "author", 47777))
result = classify(rows)
self.assertEqual(result["duplicate_members"], [])
self.assertEqual(result["missing_members"], [])
self.assertTrue(result["exactly_one_per_profile"])
self.assertFalse(result["no_unexpected_members"])
self.assertFalse(result["mutation_gate_satisfied"])
def test_unexpected_member_blocks_with_its_own_reason(self):
rows = healthy_rows()
rows.append(row("gitea-shadow", "prgs-shadow", "author", 47777))
result = classify(rows)
self.assertTrue(
any("unexpected" in reason for reason in result["blocked_reasons"])
)
class TestMixedCohort(_AliveMixin, unittest.TestCase):
"""AC5: members from different client cohorts are detected."""
def test_two_cohorts_are_detected(self):
rows = healthy_rows()
rows[0]["cohort_id"] = COHORT_B
result = classify(rows)
self.assertTrue(result["mixed_cohort"])
self.assertFalse(result["single_cohort"])
self.assertEqual(result["cohort_ids"], sorted([COHORT_A, COHORT_B]))
def test_mixed_cohort_fails_the_mutation_gate(self):
rows = healthy_rows()
rows[0]["cohort_id"] = COHORT_B
result = classify(rows)
self.assertFalse(result["mutation_gate_satisfied"])
self.assertTrue(any("cohort" in reason for reason in result["blocked_reasons"]))
def test_mixed_cohort_is_not_a_duplicate_or_missing_report(self):
rows = healthy_rows()
rows[0]["cohort_id"] = COHORT_B
result = classify(rows)
self.assertEqual(result["duplicate_members"], [])
self.assertEqual(result["missing_members"], [])
class TestMixedRevision(_AliveMixin, unittest.TestCase):
"""AC6: members running different startup revisions are detected."""
def test_two_revisions_are_detected(self):
rows = healthy_rows()
rows[0]["startup_head"] = HEAD_B
result = classify(rows)
self.assertTrue(result["mixed_revision"])
self.assertEqual(result["startup_revisions"], sorted([HEAD_A, HEAD_B]))
def test_mixed_revision_fails_the_mutation_gate(self):
rows = healthy_rows()
rows[0]["startup_head"] = HEAD_B
result = classify(rows)
self.assertFalse(result["mutation_gate_satisfied"])
self.assertTrue(
any("revision" in reason for reason in result["blocked_reasons"])
)
def test_mixed_revision_does_not_by_itself_imply_mixed_cohort(self):
rows = healthy_rows()
rows[0]["startup_head"] = HEAD_B
result = classify(rows)
self.assertTrue(result["single_cohort"])
self.assertFalse(result["mixed_cohort"])
class TestRevisionIsNotCohort(_AliveMixin, unittest.TestCase):
"""AC7: matching Git revisions alone do not establish a single cohort."""
def test_identical_revisions_with_unknown_cohort_stay_unknown(self):
rows = healthy_rows()
for r in rows:
r["cohort_id"] = None
result = classify(rows)
self.assertEqual(len({r["startup_head"] for r in rows}), 1)
self.assertIsNone(result["single_cohort"])
self.assertIsNone(result["mixed_cohort"])
def test_identical_revisions_with_unknown_cohort_fail_closed(self):
rows = healthy_rows()
for r in rows:
r["cohort_id"] = None
result = classify(rows)
self.assertFalse(result["inventory_complete"])
self.assertFalse(result["mutation_gate_satisfied"])
self.assertTrue(
any("cohort" in reason for reason in result["incomplete_reasons"])
)
def test_one_unknown_cohort_among_known_ones_still_fails_closed(self):
rows = healthy_rows()
rows[0]["cohort_id"] = None
result = classify(rows)
self.assertIsNone(result["single_cohort"])
self.assertFalse(result["mutation_gate_satisfied"])
def test_cohort_derivation_never_consults_revisions(self):
"""The cohort helper takes no revision input at all."""
identity = mfi.derive_cohort_identity({mfi.COHORT_ID_ENV: "cohort-x"})
self.assertEqual(identity["cohort_id"], "cohort-x")
self.assertEqual(identity["cohort_source"], "explicit_env")
def test_orphaned_process_reports_unknown_cohort(self):
with mock.patch("os.getppid", return_value=1):
identity = mfi.derive_cohort_identity({})
self.assertIsNone(identity["cohort_id"])
self.assertEqual(identity["cohort_source"], "unknown")
def test_parent_process_is_the_cohort_when_no_env_is_set(self):
with mock.patch("os.getppid", return_value=40990):
identity = mfi.derive_cohort_identity({})
self.assertEqual(identity["cohort_id"], COHORT_A)
self.assertEqual(identity["cohort_source"], "parent_process")
class TestConfigurationIsNotRunning(_AliveMixin, unittest.TestCase):
"""AC8: configuration without a live worker is not a running member."""
def test_no_registry_rows_means_every_member_is_missing(self):
result = classify([], scan=scan_for([]))
self.assertEqual(len(result["missing_members"]), 5)
self.assertEqual(result["running_members"], [])
self.assertFalse(result["mutation_gate_satisfied"])
def test_dead_pid_is_stale_not_running(self):
rows = healthy_rows()
self.dead_pids = {rows[0]["pid"]}
result = classify(rows)
self.assertEqual(len(result["running_members"]), 4)
self.assertEqual(len(result["stale_members"]), 1)
self.assertEqual(result["stale_members"][0]["liveness"], mfi.LIVENESS_DEAD)
self.assertEqual(result["stale_members"][0]["health"], mfi.HEALTH_STALE)
self.assertEqual(len(result["missing_members"]), 1)
def test_registry_row_without_a_matching_process_is_not_running(self):
rows = healthy_rows()
scan = scan_for(rows[1:]) # first member's process is absent
result = classify(rows, scan=scan)
self.assertEqual(len(result["running_members"]), 4)
self.assertEqual(
result["stale_members"][0]["liveness"], mfi.LIVENESS_UNOBSERVED
)
self.assertFalse(result["mutation_gate_satisfied"])
def test_recycled_pid_does_not_impersonate_a_dead_server(self):
rows = healthy_rows()
scan = scan_for(rows)
# The process now holding the first PID started *after* registration.
scan["processes"][0]["started_at"] = "2026-07-28T02:30:00Z"
result = classify(rows, scan=scan)
stale = [
m
for m in result["stale_members"]
if m["liveness"] == mfi.LIVENESS_PID_RECYCLED
]
self.assertEqual(len(stale), 1)
self.assertEqual(len(result["running_members"]), 4)
self.assertFalse(result["mutation_gate_satisfied"])
class TestIncompleteEvidenceFailsClosed(_AliveMixin, unittest.TestCase):
"""AC9: unknown or unavailable evidence produces a fail-closed result."""
def test_unavailable_process_listing_fails_closed(self):
rows = healthy_rows()
result = classify(
rows,
scan={"available": False, "processes": [], "reason": "ps unavailable"},
)
self.assertFalse(result["inventory_complete"])
self.assertFalse(result["mutation_gate_satisfied"])
self.assertIn("ps unavailable", result["incomplete_reasons"])
self.assertEqual(result["running_members"], [])
def test_unreadable_registry_fails_closed(self):
result = classify(
[],
scan=scan_for([]),
registry_available=False,
registry_error="registry unreadable",
)
self.assertFalse(result["inventory_complete"])
self.assertFalse(result["mutation_gate_satisfied"])
self.assertIn("registry unreadable", result["incomplete_reasons"])
def test_unregistered_running_process_fails_closed(self):
rows = healthy_rows()
result = classify(rows, scan=scan_for(rows, extra_pids=[59999]))
self.assertEqual([p["pid"] for p in result["unregistered_processes"]], [59999])
self.assertFalse(result["inventory_complete"])
self.assertFalse(result["mutation_gate_satisfied"])
self.assertTrue(
any("59999" in reason for reason in result["incomplete_reasons"])
)
def test_undeterminable_pid_liveness_is_unknown_not_healthy(self):
rows = healthy_rows()
self.unknown_pids = {rows[0]["pid"]}
result = classify(rows)
self.assertEqual(result["stale_members"][0]["liveness"], mfi.LIVENESS_UNKNOWN)
self.assertEqual(result["stale_members"][0]["health"], mfi.HEALTH_UNKNOWN)
self.assertFalse(result["inventory_complete"])
self.assertFalse(result["mutation_gate_satisfied"])
def test_unknown_startup_revision_fails_closed(self):
rows = healthy_rows()
rows[0]["startup_head"] = None
result = classify(rows)
self.assertFalse(result["inventory_complete"])
self.assertFalse(result["mutation_gate_satisfied"])
self.assertTrue(
any("startup revision" in r for r in result["incomplete_reasons"])
)
def test_unknown_is_distinguished_from_healthy(self):
rows = healthy_rows()
for r in rows:
r["cohort_id"] = None
result = classify(rows)
# Not "unhealthy" in the sense of a named defect: nothing is missing,
# duplicated or unexpected. It is *unknown*, and that still fails closed.
self.assertEqual(result["missing_members"], [])
self.assertEqual(result["duplicate_members"], [])
self.assertEqual(result["unexpected_members"], [])
self.assertIsNone(result["single_cohort"])
self.assertFalse(result["mutation_gate_satisfied"])
class TestBindingAndRoleConsistency(_AliveMixin, unittest.TestCase):
"""Repository-binding and role/profile mismatches fail closed."""
def test_repository_binding_mismatch_is_reported(self):
rows = healthy_rows()
rows[0]["repo"] = "Some-Other-Repo"
result = classify(rows)
self.assertEqual(len(result["repository_binding_mismatches"]), 1)
self.assertEqual(
result["repository_binding_mismatches"][0]["repo"], "Some-Other-Repo"
)
self.assertFalse(result["mutation_gate_satisfied"])
def test_incomplete_repository_binding_is_reported(self):
rows = healthy_rows()
rows[0]["org"] = None
result = classify(rows)
self.assertEqual(len(result["repository_binding_mismatches"]), 1)
self.assertIn("complete", result["repository_binding_mismatches"][0]["reason"])
self.assertFalse(result["mutation_gate_satisfied"])
def test_role_mismatch_is_reported(self):
rows = healthy_rows()
rows[0]["role"] = "merger" # prgs-author is configured as author
result = classify(rows)
self.assertEqual(len(result["role_mismatches"]), 1)
self.assertEqual(result["role_mismatches"][0]["expected_role"], "author")
self.assertEqual(result["role_mismatches"][0]["declared_role"], "merger")
self.assertFalse(result["mutation_gate_satisfied"])
def test_profile_served_from_the_wrong_namespace_is_reported(self):
rows = healthy_rows()
rows[0]["namespace"] = "gitea-reviewer"
result = classify(rows)
self.assertTrue(
any(
m.get("expected_namespace") == "gitea-author"
for m in result["role_mismatches"]
)
)
self.assertFalse(result["mutation_gate_satisfied"])
def test_matching_binding_produces_no_mismatch(self):
result = classify(healthy_rows())
self.assertEqual(result["repository_binding_mismatches"], [])
self.assertEqual(result["role_mismatches"], [])
class TestDeterminism(_AliveMixin, unittest.TestCase):
"""AC10 / stable ordering and deterministic structured output."""
def test_identical_snapshots_produce_identical_results(self):
rows = healthy_rows()
first = classify(rows, scan=scan_for(rows))
second = classify(healthy_rows(), scan=scan_for(healthy_rows()))
self.assertEqual(first, second)
def test_row_order_does_not_change_the_result(self):
rows = healthy_rows()
shuffled = list(reversed(healthy_rows()))
forward = classify(rows, scan=scan_for(rows))
backward = classify(shuffled, scan=scan_for(shuffled))
self.assertEqual(forward["running_members"], backward["running_members"])
self.assertEqual(forward["configured_members"], backward["configured_members"])
self.assertEqual(
forward["mutation_gate_satisfied"], backward["mutation_gate_satisfied"]
)
def test_members_are_sorted_by_namespace_then_profile_then_pid(self):
rows = healthy_rows()
rows.append(row("gitea-author", "prgs-author", "author", 40001))
result = classify(rows)
keys = [
(m["namespace"], m["profile"], m["pid"]) for m in result["running_members"]
]
self.assertEqual(keys, sorted(keys))
def test_answering_namespace_does_not_change_the_verdict(self):
"""AC10: controller and reconciler agree for one fleet snapshot."""
rows = healthy_rows()
controller = classify(
rows, scan=scan_for(rows), answering_namespace="gitea-controller"
)
reconciler = classify(
healthy_rows(),
scan=scan_for(healthy_rows()),
answering_namespace="gitea-reconciler",
)
self.assertEqual(controller["answering_namespace"], "gitea-controller")
self.assertEqual(reconciler["answering_namespace"], "gitea-reconciler")
for key in sorted(set(controller) - {"answering_namespace"}):
self.assertEqual(controller[key], reconciler[key], f"{key} disagreed")
def test_controller_and_reconciler_agree_on_an_unhealthy_fleet(self):
rows = [r for r in healthy_rows() if r["profile"] != "prgs-merger"]
controller = classify(
rows, scan=scan_for(rows), answering_namespace="gitea-controller"
)
reconciler = classify(
rows, scan=scan_for(rows), answering_namespace="gitea-reconciler"
)
self.assertEqual(controller["blocked_reason"], reconciler["blocked_reason"])
self.assertEqual(
controller["mutation_gate_satisfied"],
reconciler["mutation_gate_satisfied"],
)
class TestReadOnly(_AliveMixin, unittest.TestCase):
"""AC11: the capability mutates nothing."""
def test_classification_reports_no_mutations(self):
result = classify(healthy_rows())
self.assertEqual(result["mutations_performed"], [])
self.assertTrue(result["read_only"])
def test_classification_does_not_write_the_input_rows_back(self):
rows = healthy_rows()
snapshot = [dict(r) for r in rows]
classify(rows)
self.assertEqual(rows, snapshot)
class TestLivenessProbe(unittest.TestCase):
"""AC11: the real probe never sends a terminating signal.
Deliberately *not* using ``_AliveMixin`` — these tests exercise
``probe_pid_alive`` itself, which the mixin replaces.
"""
def test_only_signal_zero_is_ever_sent(self):
with mock.patch("os.kill") as killer:
mfi.probe_pid_alive(4242)
killer.assert_called_once_with(4242, 0)
def test_classifying_a_duplicate_never_terminates_it(self):
rows = healthy_rows()
rows.append(row("gitea-author", "prgs-author", "author", 49999))
with mock.patch("os.kill") as killer:
result = mfi.classify_fleet_inventory(
runtime_rows=rows,
process_scan=scan_for(rows),
expected_binding=BINDING,
now=NOW,
)
self.assertTrue(killer.call_args_list, "liveness must actually be probed")
for call in killer.call_args_list:
self.assertEqual(call.args[1], 0, "only signal 0 may ever be sent")
self.assertEqual(result["mutations_performed"], [])
def test_liveness_probe_tolerates_a_missing_process(self):
with mock.patch("os.kill", side_effect=ProcessLookupError):
self.assertFalse(mfi.probe_pid_alive(4242))
def test_liveness_probe_treats_permission_error_as_alive(self):
with mock.patch("os.kill", side_effect=PermissionError):
self.assertTrue(mfi.probe_pid_alive(4242))
def test_liveness_probe_returns_unknown_on_other_os_errors(self):
with mock.patch("os.kill", side_effect=OSError):
self.assertIsNone(mfi.probe_pid_alive(4242))
def test_invalid_pid_is_unknown_and_probes_nothing(self):
with mock.patch("os.kill") as killer:
self.assertIsNone(mfi.probe_pid_alive(None))
self.assertIsNone(mfi.probe_pid_alive(0))
killer.assert_not_called()
class TestMultiClientRegression(_AliveMixin, unittest.TestCase):
"""The multi-LLM duplicate-server scenario that motivated #949."""
@staticmethod
def _two_client_rows():
first = healthy_rows()
second = [
row(
entry["namespace"],
entry["profile"],
entry["role"],
50000 + index,
cohort_id=COHORT_B,
)
for index, entry in enumerate(mfi.EXPECTED_PRGS_FLEET)
]
return first + second
def test_second_client_running_the_same_five_profiles_is_caught(self):
"""Two clients, ten servers, same profiles, same revision.
Every member self-reports the same parity, which is exactly why the old
per-process surfaces reported success. The fleet inventory must report
five duplicates, two cohorts, and a closed gate.
"""
rows = self._two_client_rows()
result = classify(rows, scan=scan_for(rows))
self.assertEqual(len(result["duplicate_members"]), 5)
self.assertFalse(result["exactly_one_per_profile"])
self.assertTrue(result["mixed_cohort"])
self.assertFalse(result["single_cohort"])
self.assertFalse(result["mixed_revision"], "both clients share a revision")
self.assertFalse(result["mutation_gate_satisfied"])
self.assertEqual(result["missing_members"], [])
def test_identical_parity_across_ten_servers_is_not_health(self):
rows = self._two_client_rows()
result = classify(rows, scan=scan_for(rows))
self.assertEqual(result["startup_revisions"], [HEAD_A])
self.assertFalse(result["mutation_gate_satisfied"])
def test_every_duplicate_pid_is_named_for_the_operator(self):
rows = self._two_client_rows()
result = classify(rows, scan=scan_for(rows))
for duplicate in result["duplicate_members"]:
self.assertEqual(len(duplicate["pids"]), 2)
class TestProcessScan(unittest.TestCase):
"""The process observation is server-side and fails closed."""
def test_scan_parses_mcp_server_processes(self):
stdout = (
" PID STARTED COMMAND\n"
" 41000 Mon Jul 27 20:00:00 2026 python /path/mcp_server.py\n"
" 41001 Mon Jul 27 20:00:01 2026 python /path/other_server.py\n"
)
result = mfi.scan_mcp_server_processes(
runner=lambda *a, **k: mock.Mock(stdout=stdout)
)
self.assertTrue(result["available"])
self.assertEqual([p["pid"] for p in result["processes"]], [41000])
def test_scan_failure_reports_unavailable_rather_than_empty(self):
def boom(*args, **kwargs):
raise OSError("ps missing")
result = mfi.scan_mcp_server_processes(runner=boom)
self.assertFalse(result["available"])
self.assertEqual(result["processes"], [])
self.assertIn("ps missing", result["reason"])
def test_scan_results_are_sorted_by_pid(self):
stdout = (
" PID STARTED COMMAND\n"
" 41005 Mon Jul 27 20:00:00 2026 python /path/mcp_server.py\n"
" 41001 Mon Jul 27 20:00:01 2026 python /path/mcp_server.py\n"
)
result = mfi.scan_mcp_server_processes(
runner=lambda *a, **k: mock.Mock(stdout=stdout)
)
self.assertEqual([p["pid"] for p in result["processes"]], [41001, 41005])
class TestRuntimeRecord(unittest.TestCase):
"""The row a server writes about itself."""
def test_record_describes_the_calling_process(self):
record = mfi.build_process_runtime_record(
namespace="gitea-controller",
profile="prgs-controller",
role="controller",
remote="prgs",
org=BINDING["org"],
repo=BINDING["repo"],
pid=41022,
startup_head=HEAD_A,
transport="stdio",
client_provenance="client_managed",
env={mfi.COHORT_ID_ENV: COHORT_A},
boot_id="deadbeefcafe0001",
)
self.assertEqual(
record["runtime_id"], "gitea-controller:41022:deadbeefcafe0001"
)
self.assertEqual(record["namespace"], "gitea-controller")
self.assertEqual(record["cohort_id"], COHORT_A)
self.assertEqual(record["cohort_source"], "explicit_env")
self.assertEqual(record["startup_head"], HEAD_A)
self.assertEqual(record["status"], "running")
def test_record_defaults_pid_to_the_current_process(self):
record = mfi.build_process_runtime_record(
namespace="gitea-author", profile="prgs-author", role="author"
)
self.assertEqual(record["pid"], os.getpid())
def test_each_boot_gets_a_distinct_runtime_id(self):
first = mfi.build_process_runtime_record(
namespace="gitea-author", profile="prgs-author", role="author", pid=1
)
second = mfi.build_process_runtime_record(
namespace="gitea-author", profile="prgs-author", role="author", pid=1
)
self.assertNotEqual(first["runtime_id"], second["runtime_id"])
def test_registered_at_uses_the_control_plane_timestamp_format(self):
record = mfi.build_process_runtime_record(
namespace="gitea-author", profile="prgs-author", role="author", pid=1
)
datetime.strptime(record["registered_at"], "%Y-%m-%dT%H:%M:%SZ")
class TestSummary(_AliveMixin, unittest.TestCase):
def test_healthy_summary_names_the_counts(self):
result = classify(healthy_rows())
self.assertIn("5 of 5", mfi.summarize(result))
def test_blocked_summary_repeats_the_blocked_reason(self):
rows = [r for r in healthy_rows() if r["profile"] != "prgs-merger"]
result = classify(rows)
self.assertIn(result["blocked_reason"], mfi.summarize(result))
class TestHeartbeatAge(_AliveMixin, unittest.TestCase):
def test_heartbeat_age_is_reported_for_diagnosis(self):
rows = healthy_rows()
stamp = (NOW - timedelta(minutes=30)).strftime("%Y-%m-%dT%H:%M:%SZ")
rows[0]["last_heartbeat_at"] = stamp
result = classify(rows)
member = [m for m in result["running_members"] if m["pid"] == rows[0]["pid"]][0]
self.assertEqual(member["heartbeat_age_seconds"], 1800)
def test_missing_heartbeat_is_reported_as_unknown_age(self):
rows = healthy_rows()
rows[0]["last_heartbeat_at"] = None
result = classify(rows)
member = [m for m in result["running_members"] if m["pid"] == rows[0]["pid"]][0]
self.assertIsNone(member["heartbeat_age_seconds"])
if __name__ == "__main__":
unittest.main()