Files
Gitea-Tools/tests/test_control_plane_db_server_runtimes.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

283 lines
11 KiB
Python

"""Control-plane MCP server runtime registry (#949).
The registry is the half of the fleet evidence a caller cannot supply: each
server writes exactly one row about itself at native transport bind. These tests
pin the storage contract — additive migration, deterministic ordering, and
PID-reuse/retention pruning that cannot hide a live duplicate.
"""
import os
import tempfile
import unittest
from datetime import datetime, timedelta, timezone
from unittest import mock
import control_plane_db
import mcp_fleet_inventory as mfi
def _stamp(delta_seconds=0):
moment = datetime.now(timezone.utc) + timedelta(seconds=delta_seconds)
return moment.replace(microsecond=0).strftime("%Y-%m-%dT%H:%M:%SZ")
class _DBCase(unittest.TestCase):
def setUp(self):
self._tmp = tempfile.TemporaryDirectory()
self.addCleanup(self._tmp.cleanup)
self.db_path = os.path.join(self._tmp.name, "control_plane.sqlite3")
self.db = control_plane_db.ControlPlaneDB(db_path=self.db_path)
def record(self, namespace, profile, role, pid, **overrides):
base = mfi.build_process_runtime_record(
namespace=namespace,
profile=profile,
role=role,
remote="prgs",
org="Scaled-Tech-Consulting",
repo="Gitea-Tools",
repository_root="/checkout/Gitea-Tools",
pid=pid,
startup_head="82d71b77028a7abd4f8ab4a4e4d89658a187f73d",
transport="stdio",
client_provenance="client_managed",
env={mfi.COHORT_ID_ENV: "ppid:40990"},
boot_id=f"boot{pid}",
)
base.update(overrides)
return base
class TestSchema(_DBCase):
def test_schema_version_is_bumped(self):
self.assertGreaterEqual(control_plane_db.SCHEMA_VERSION, 6)
def test_runtime_table_exists_on_a_fresh_database(self):
self.assertEqual(self.db.list_mcp_server_runtimes(), [])
def test_migration_is_additive_and_idempotent(self):
"""Re-opening an existing DB must not disturb the other tables."""
self.db.upsert_session(session_id="s-a", role="author", profile="prgs-author")
self.db.register_mcp_server_runtime(
self.record("gitea-author", "prgs-author", "author", 41000)
)
reopened = control_plane_db.ControlPlaneDB(db_path=self.db_path)
self.assertEqual(len(reopened.list_sessions()), 1)
self.assertEqual(len(reopened.list_mcp_server_runtimes()), 1)
class TestRegistration(_DBCase):
def test_registration_round_trips_every_field(self):
record = self.record("gitea-controller", "prgs-controller", "controller", 41001)
stored = self.db.register_mcp_server_runtime(record)
for key in (
"runtime_id",
"namespace",
"profile",
"role",
"remote",
"org",
"repo",
"repository_root",
"pid",
"cohort_id",
"cohort_source",
"client_provenance",
"boot_id",
"startup_head",
"transport",
"status",
):
self.assertEqual(stored[key], record[key], key)
def test_registration_requires_a_runtime_id(self):
record = self.record("gitea-author", "prgs-author", "author", 41002)
record["runtime_id"] = ""
with self.assertRaises(ValueError):
self.db.register_mcp_server_runtime(record)
def test_registration_requires_a_namespace(self):
record = self.record("gitea-author", "prgs-author", "author", 41003)
record["namespace"] = " "
with self.assertRaises(ValueError):
self.db.register_mcp_server_runtime(record)
def test_registration_requires_a_pid(self):
record = self.record("gitea-author", "prgs-author", "author", 41004)
record["pid"] = None
with self.assertRaises(ValueError):
self.db.register_mcp_server_runtime(record)
def test_five_members_register_independently(self):
for index, entry in enumerate(mfi.EXPECTED_PRGS_FLEET):
self.db.register_mcp_server_runtime(
self.record(
entry["namespace"], entry["profile"], entry["role"], 41100 + index
)
)
rows = self.db.list_mcp_server_runtimes()
self.assertEqual(len(rows), 5)
self.assertEqual(
sorted(r["profile"] for r in rows),
[
"prgs-author",
"prgs-controller",
"prgs-merger",
"prgs-reconciler",
"prgs-reviewer",
],
)
class TestPruning(_DBCase):
def test_reusing_a_pid_replaces_the_stale_row(self):
first = self.record("gitea-author", "prgs-author", "author", 41200)
self.db.register_mcp_server_runtime(first)
second = self.record("gitea-reviewer", "prgs-reviewer", "reviewer", 41200)
self.db.register_mcp_server_runtime(second)
rows = self.db.list_mcp_server_runtimes()
self.assertEqual(len(rows), 1)
self.assertEqual(rows[0]["runtime_id"], second["runtime_id"])
def test_pruning_never_removes_a_live_duplicate_on_another_pid(self):
"""The defect this must not have: hiding a second running server."""
first = self.record("gitea-author", "prgs-author", "author", 41300)
self.db.register_mcp_server_runtime(first)
second = self.record("gitea-author", "prgs-author", "author", 41301)
self.db.register_mcp_server_runtime(
second, retention_seconds=mfi.RUNTIME_RETENTION_SECONDS
)
rows = self.db.list_mcp_server_runtimes()
self.assertEqual(len(rows), 2)
self.assertEqual(sorted(r["pid"] for r in rows), [41300, 41301])
def test_retention_drops_rows_older_than_the_window(self):
old = self.record(
"gitea-merger",
"prgs-merger",
"merger",
41400,
registered_at=_stamp(-(mfi.RUNTIME_RETENTION_SECONDS + 3600)),
)
self.db.register_mcp_server_runtime(old)
fresh = self.record("gitea-author", "prgs-author", "author", 41401)
self.db.register_mcp_server_runtime(
fresh, retention_seconds=mfi.RUNTIME_RETENTION_SECONDS
)
rows = self.db.list_mcp_server_runtimes()
self.assertEqual([r["pid"] for r in rows], [41401])
def test_retention_is_skipped_when_not_requested(self):
old = self.record(
"gitea-merger",
"prgs-merger",
"merger",
41500,
registered_at=_stamp(-(mfi.RUNTIME_RETENTION_SECONDS + 3600)),
)
self.db.register_mcp_server_runtime(old)
self.db.register_mcp_server_runtime(
self.record("gitea-author", "prgs-author", "author", 41501)
)
self.assertEqual(len(self.db.list_mcp_server_runtimes()), 2)
class TestListing(_DBCase):
def test_listing_is_deterministically_ordered(self):
for index in range(5):
self.db.register_mcp_server_runtime(
self.record(
"gitea-author",
"prgs-author",
"author",
41600 + index,
registered_at="2026-07-28T02:00:00Z",
)
)
first = [r["runtime_id"] for r in self.db.list_mcp_server_runtimes()]
second = [r["runtime_id"] for r in self.db.list_mcp_server_runtimes()]
self.assertEqual(first, second)
self.assertEqual(first, sorted(first))
def test_status_filter_selects_running_rows(self):
running = self.record("gitea-author", "prgs-author", "author", 41700)
self.db.register_mcp_server_runtime(running)
stopped = self.record("gitea-merger", "prgs-merger", "merger", 41701)
self.db.register_mcp_server_runtime(stopped)
self.db.mark_mcp_server_runtime_stopped(stopped["runtime_id"])
rows = self.db.list_mcp_server_runtimes(statuses=("running",))
self.assertEqual([r["pid"] for r in rows], [41700])
def test_listing_without_a_filter_returns_stopped_rows_too(self):
stopped = self.record("gitea-merger", "prgs-merger", "merger", 41800)
self.db.register_mcp_server_runtime(stopped)
self.db.mark_mcp_server_runtime_stopped(stopped["runtime_id"])
rows = self.db.list_mcp_server_runtimes()
self.assertEqual(len(rows), 1)
self.assertEqual(rows[0]["status"], "stopped")
class TestHeartbeat(_DBCase):
def test_heartbeat_advances_only_the_timestamp(self):
record = self.record(
"gitea-author",
"prgs-author",
"author",
41900,
last_heartbeat_at="2026-07-28T02:00:00Z",
)
self.db.register_mcp_server_runtime(record)
self.db.heartbeat_mcp_server_runtime(record["runtime_id"])
row = self.db.list_mcp_server_runtimes()[0]
self.assertNotEqual(row["last_heartbeat_at"], "2026-07-28T02:00:00Z")
self.assertEqual(row["status"], "running")
self.assertEqual(row["pid"], 41900)
def test_heartbeat_for_an_unknown_runtime_is_a_no_op(self):
self.db.heartbeat_mcp_server_runtime("does-not-exist")
self.assertEqual(self.db.list_mcp_server_runtimes(), [])
class TestEndToEndSnapshot(_DBCase):
"""Registry rows feed the classifier without reshaping."""
def test_registered_fleet_classifies_as_healthy(self):
pids = []
for index, entry in enumerate(mfi.EXPECTED_PRGS_FLEET):
pid = 42000 + index
pids.append(pid)
self.db.register_mcp_server_runtime(
self.record(
entry["namespace"],
entry["profile"],
entry["role"],
pid,
registered_at="2026-07-28T02:00:00Z",
)
)
rows = self.db.list_mcp_server_runtimes(statuses=("running",))
scan = {
"available": True,
"processes": [
{"pid": pid, "started_at": "2026-07-28T01:00:00Z"} for pid in pids
],
"reason": None,
}
with mock.patch.object(mfi, "probe_pid_alive", return_value=True):
result = mfi.classify_fleet_inventory(
runtime_rows=rows,
process_scan=scan,
expected_binding={
"remote": "prgs",
"org": "Scaled-Tech-Consulting",
"repo": "Gitea-Tools",
},
)
self.assertTrue(result["inventory_complete"])
self.assertTrue(result["mutation_gate_satisfied"])
self.assertEqual(result["running_member_count"], 5)
if __name__ == "__main__":
unittest.main()