ouroboros/tests/test_panic_stop_port_sweep.py
Ouroboros d1c8fca451 v7next F1: domain D11 quiet part - server.py six-leaf split from tip bytes; restart transaction hot-deferred; same-qualname ratchet delta landed
Module side. 34 D11 owners classified: 13 byte-identical across
tip/merge-base/reference (client_surface, gateway __init__/files/logs/mcp/
onboarding_host/schedules/task_events/task_hurry/ui_preferences, server_auth,
server_entrypoint, server_web); 17 pure upstream drift - tip bytes stand
(gateway _helpers/claudexor_accounts/contracts/control/extensions/history/
host_service/marketplace/models/presence_settings/projects/router/
skill_publish/state/tasks/ws, server_runtime); 3 carry only the D03
settings-seam / D04 retired-knob reference deltas - HOT-DEFERRED with that
seam (gateway owner_settings/onboarding/settings, D12/D17 precedent);
server.py split.

The split: 6 leaves, 43 moved spans (rows 1034-1078 + 3948-3949), every span
transplant-tool proof-green against git show HEAD:server.py (ast=tokens=
byte-roundtrip on every symbol, leaf_invariants=[], zero declared names - the
reference design homes shared rebindable state in server_process, so all six
are projection-only leaves, no handles). server.py 3191->1640: server_process
(drive root, logger, restart signals), server_routing_context (13 owner-turn
projections), server_owner_routing (attachment staging, mailbox delivery,
routing receipt, owner-message dispatch, /evolve off), server_liveness (WS3
wedge watchdog), server_maintenance (startup sweeps + periodic cadences),
server_restart (live census, teardown args, update guards, bus shutdown).
Facade = tip parent - moved spans + reference-style top import block (module-
level PORT_FILE/logging reads force top imports), facade audit green: every
kept span byte-identical to tip, every moved name re-exported by identity.
Drift-probe first per leaf: reference leaves byte-true except 10 spans
falsified by upstream drift (attachment-report train, OB-03 monotonic clock,
child-ref promotion, planned-handoff train) - re-emitted from tip bytes, no
oracle semantics replayed over drift.

HOT-DEFERRED with evidence: rows 1070/1072/1073/1074 (_pending_restart,
_handle_restart_in_supervisor, _check_pending_restart_drain,
_perform_supervisor_restart) - the upstream delegation train coupled the
restart performer to main() through the written module global
_planned_delegate_restart_transaction_id; byte-preserving relocation would
fork that state (D09-class second answer about ownership); inventory pinned
in test_server_extraction._SERVER_OWNED. Rows 1080-1081 (lifespan, D03
settings-seam server half) - reader halves 913-917 are hot-deferred by
D12/D17 (settings_integrity rewrite); landing the boot half alone would leave
normalization neither persisted nor re-derived. Gateway ABI/alias rows = F3;
web/ untouched.

LIVE delta landed: the same-qualname FUNCTION_DEBT relocation rule (row 1033,
delta id D11) replayed byte-identical from the reference into the tip-shaped
validate_manifest_transition (ouroboros/review.py is NOT a protected file;
the reference-only MODULE_DEBT_1500 layer NOT replayed - Q11=B). Pin renamed
per the row with reference bytes. This unblocks the D08 lane's row 2016
deferral (FUNCTION_DEBT relocation of _handle_schedule_task).

Test side: pin suite test_server_extraction (6, reference-adapted: deferred
restart rows moved to the _SERVER_OWNED work order, prune-sweep rows 3948-3949
added to _MOVED_OWNERS, facade bound 1700 while the restart organ is
deferred); rows 1192/1259 landed as the D11 slice of the reconciliation theme
split (the two server_maintenance-bound tests re-homed - the byte-debt
ratchet refused the +40-byte in-place retarget, giant 320340->318310); owner
retargets mirrored path-keyed (run_isolation DATA_DIR, phase3c maintenance
owner, project_routing_v664 routing_context, client_surface joined-text pin,
ws3 fake clock -> server_liveness, panic-sweep leaf floor 5->11); patches of
facade-resident readers deliberately NOT retargeted (they ride the deferral).
All touched test files lossless (the one rename is ledger row 1033); no new
ast-identical dup bodies.

size-ratchet manifest regenerated with the official tool (one byte-debt
shrink); ratchet lane 4 passed + 1 pre-existing base red reproduced bit-for-
bit on pristine a56bb76a (parent-pair manifest/tree mismatch at 7d2dca49,
documented in LEDGER_CORRECTIONS 9) - the (a56bb76a -> this commit) pair is
consistent. ruff check . --select F clean. CI-shape battery on this tree:
parallel 11983 passed rc=0, serial 609 passed rc=0. Import smoke
green (identity re-exports, shared Events single home).
docs/v7next/LEDGER_CORRECTIONS.md: D11 lane section (10 entries).

(cherry picked from commit 849f90be0c3b554753b2c580d14ac70450b2c58c)
2026-08-31 00:17:05 +00:00

234 lines
9.6 KiB
Python

"""Which ports a panic stop sweeps, and in what order it hard-exits.
Characterization of the Emergency Stop contract that must survive any change to
how ``server_control.execute_panic_stop`` learns the port the server bound: the
sweep targets the ACTUALLY bound main port (a custom-port install must not
panic-kill an unrelated listener on 8765), the host-service port follows it, and
nothing — including a failing sweep — may delay or reorder the hard exit.
Every destructive operation is neutralized here: no real process, port, daemon
or interpreter teardown runs.
"""
from __future__ import annotations
from types import SimpleNamespace
import pytest
class _ExitCalled(RuntimeError):
pass
def _harness(monkeypatch, tmp_path, *, port_kill=None):
"""Neutralize every destructive teardown op and record the panic timeline."""
from ouroboros import server_control
events: list = []
def _record(name, value=None):
events.append((name, value) if value is not None else (name,))
def _kill_port(port):
_record("kill_process_on_port", port)
if port_kill is not None:
port_kill(port)
monkeypatch.setattr("supervisor.state.load_state", lambda: {})
monkeypatch.setattr("supervisor.state.save_state", lambda _state: None)
monkeypatch.setattr(
"supervisor.evolution_lifecycle.complete_evolution_campaign", lambda *a, **k: {}
)
monkeypatch.setattr("ouroboros.post_task_evolution.drop_pending_request", lambda *a, **k: None)
monkeypatch.setattr(
"ouroboros.local_model.get_manager",
lambda: SimpleNamespace(stop_server=lambda: _record("local_model_stop")),
)
monkeypatch.setattr(
"ouroboros.claudexor_daemon.get_owned_daemon",
lambda: SimpleNamespace(stop=lambda: _record("owned_daemon_stop")),
)
monkeypatch.setattr(
"ouroboros.tools.shell.kill_all_tracked_subprocesses", lambda: _record("kill_shells")
)
monkeypatch.setattr(
"ouroboros.workspace_executor.kill_all_foreground",
lambda *a, **k: _record("kill_foreground"),
)
monkeypatch.setattr(
"ouroboros.tools.services.kill_all_services", lambda *a, **k: _record("kill_services")
)
monkeypatch.setattr(
"ouroboros.extension_companion.panic_kill_all", lambda: _record("panic_kill_companions")
)
monkeypatch.setattr("multiprocessing.active_children", lambda: [])
monkeypatch.setattr("ouroboros.platform_layer.force_kill_pid", lambda *a, **k: None)
monkeypatch.setattr("ouroboros.platform_layer.kill_process_on_port", _kill_port)
monkeypatch.setattr("ouroboros.gateway.host_service.host_service_port", lambda: 8767)
monkeypatch.setattr(
server_control.os, "_exit", lambda code: (_ for _ in ()).throw(_ExitCalled(code))
)
return events, lambda **kw: _record("kill_workers", kw)
def _swept_ports(events: list) -> list:
return [value for name, value in (e for e in events if len(e) == 2) if name == "kill_process_on_port"]
def test_panic_through_the_server_sweeps_the_actually_bound_port(monkeypatch, tmp_path):
"""A custom-port install must panic-kill ITS listener, never a stranger on 8765."""
import server
events, kill_workers = _harness(monkeypatch, tmp_path)
monkeypatch.setattr(server, "DATA_DIR", tmp_path)
monkeypatch.setattr(server, "_ACTUAL_BOUND_PORT", 9123)
with pytest.raises(_ExitCalled) as exit_info:
server._execute_panic_stop(SimpleNamespace(stop=lambda: None), kill_workers)
assert _swept_ports(events) == [9123, 8767]
assert exit_info.value.args[0] == server.PANIC_EXIT_CODE
def test_panic_with_no_known_bound_port_sweeps_the_default_install_port(monkeypatch, tmp_path):
"""Nothing told this panic which port was bound, so the default install port
is the last resort — the panic never skips the sweep."""
from ouroboros import server_control
events, kill_workers = _harness(monkeypatch, tmp_path)
with pytest.raises(_ExitCalled):
server_control.execute_panic_stop(
consciousness=SimpleNamespace(stop=lambda: None),
kill_workers_fn=kill_workers,
data_dir=tmp_path,
panic_exit_code=120,
log=SimpleNamespace(critical=lambda *a, **k: None),
)
assert _swept_ports(events) == [8765, 8767]
def test_a_failing_main_port_sweep_still_sweeps_the_default_and_the_host_service(
monkeypatch, tmp_path,
):
"""The main-port sweep is fail-soft: a raising kill must not cost the panic the
default-port fallback or the host-service sweep that follows it."""
import server
def _boom(port):
if port == 9123:
raise OSError("port sweep failed")
events, kill_workers = _harness(monkeypatch, tmp_path, port_kill=_boom)
monkeypatch.setattr(server, "DATA_DIR", tmp_path)
monkeypatch.setattr(server, "_ACTUAL_BOUND_PORT", 9123)
with pytest.raises(_ExitCalled):
server._execute_panic_stop(SimpleNamespace(stop=lambda: None), kill_workers)
assert _swept_ports(events) == [9123, 8765, 8767]
def test_panic_teardown_order_ends_with_the_port_sweep_then_the_hard_exit(monkeypatch, tmp_path):
"""Cleanup, then the fail-soft child/port sweep, then os._exit — and the
durable panic flag is written before any of the killing starts."""
import server
events, kill_workers = _harness(monkeypatch, tmp_path)
monkeypatch.setattr(server, "DATA_DIR", tmp_path)
monkeypatch.setattr(server, "_ACTUAL_BOUND_PORT", 9123)
with pytest.raises(_ExitCalled):
server._execute_panic_stop(SimpleNamespace(stop=lambda: None), kill_workers)
assert [event[0] for event in events] == [
"local_model_stop",
"owned_daemon_stop",
"kill_shells",
"kill_foreground",
"kill_services",
"panic_kill_companions",
"kill_workers",
"kill_process_on_port",
"kill_process_on_port",
]
# Upstream bytes (dc4c0204) added reconcile_delegate_custody=False to the
# panic's kill_workers call; the pinned contract (kill everything, then
# sweep ports, then hard-exit) is unchanged.
assert events[6][1] == {
"force": True, "archive_service_logs": False, "reconcile_delegate_custody": False,
}
assert (tmp_path / "state" / "panic_stop.flag").read_text(encoding="utf-8") == "panic"
def test_the_server_passes_its_bound_port_instead_of_the_leaf_reaching_back(monkeypatch):
"""Emergency Stop 2A: the composition root owns the bound-port fact and hands
it down as a keyword-only argument with a default, so the panic leaf never has
to reach back into the server module for it."""
import inspect
import server
from ouroboros import server_control
parameter = inspect.signature(server_control.execute_panic_stop).parameters["bound_port"]
assert parameter.kind is inspect.Parameter.KEYWORD_ONLY
assert parameter.default is None
captured: dict = {}
monkeypatch.setattr(server, "_execute_panic_stop_impl", lambda *a, **kw: captured.update(kw))
monkeypatch.setattr(server, "_ACTUAL_BOUND_PORT", 9123)
server._execute_panic_stop(SimpleNamespace(stop=lambda: None), lambda **kw: None)
assert captured["bound_port"] == 9123
def test_no_server_host_leaf_imports_the_composition_root():
"""The lazy `import server` inside the panic port sweep was the last back-edge
from a host leaf to the composition root. Scanned as a class, at any depth, so
a future lazy import inside a function cannot quietly restore it."""
import ast
import pathlib
import server
leaves = sorted((pathlib.Path(server.__file__).parent / "ouroboros").glob("server_*.py"))
# 5 pre-split host leaves + the 6 D11 server-split leaves (liveness,
# maintenance, owner_routing, process, restart, routing_context). The floor
# guards against the glob silently matching nothing.
assert len(leaves) >= 11
for leaf in leaves:
for node in ast.walk(ast.parse(leaf.read_text(encoding="utf-8"))):
if isinstance(node, ast.Import):
assert not any(
alias.name == "server" or alias.name.startswith("server.")
for alias in node.names
), leaf.name
if isinstance(node, ast.ImportFrom):
assert node.module != "server", leaf.name
def test_emergency_process_cleanup_stays_a_separate_path_from_panic(monkeypatch, tmp_path):
"""The uvicorn-hang cleanup is NOT the panic: it finalizes running tasks with an
honest interrupted reason and returns, where panic hard-exits. Keeping them
separate is an explicit design decision, not an oversight."""
import server
worker_calls = []
monkeypatch.setattr(server, "DATA_DIR", tmp_path)
monkeypatch.setattr("ouroboros.tools.shell.kill_all_tracked_subprocesses", lambda: None)
monkeypatch.setattr("ouroboros.workspace_executor.kill_all_foreground", lambda *a, **k: None)
monkeypatch.setattr("ouroboros.tools.services.kill_all_services", lambda *a, **k: None)
monkeypatch.setattr("supervisor.workers.kill_workers", lambda **kw: worker_calls.append(kw))
monkeypatch.setattr("multiprocessing.active_children", lambda: [])
monkeypatch.setattr("ouroboros.platform_layer.force_kill_pid", lambda *a, **k: None)
monkeypatch.setattr("ouroboros.platform_layer.kill_process_on_port", lambda _port: None)
monkeypatch.setattr("ouroboros.extension_companion.panic_kill_all", lambda: None)
monkeypatch.setattr("ouroboros.gateway.host_service.host_service_port", lambda: 8767)
# Returns normally: no os._exit patch is needed, which is the whole point.
server._emergency_process_cleanup(port_sweep=False)
assert worker_calls and worker_calls[0]["force"] is True