From 4d554c2fac178414f97e123ed2a89900cadaacc9 Mon Sep 17 00:00:00 2001 From: Tanmoy Sarkar Date: Tue, 9 Jun 2026 23:05:41 +0530 Subject: [PATCH] test(companion): Add control command tests Wire ControlServer.handle_line to a real CompanionManager.handle_command and assert the full decode/dispatch/encode round trip: status returns the companion list, start routes through and reports a message, and unknown command, missing name, and reread without a loader each return an error envelope. Co-Authored-By: Claude Opus 4.8 --- docs/design/companion-process-manager.md | 2 +- tests/test_companion_control.py | 49 ++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/docs/design/companion-process-manager.md b/docs/design/companion-process-manager.md index 5d41b4f4..6b60b3e7 100644 --- a/docs/design/companion-process-manager.md +++ b/docs/design/companion-process-manager.md @@ -697,7 +697,7 @@ No per-companion logic in Arbiter. - [x] Add lifecycle logs. - [x] Add tests for config validation. - [x] Add tests for state transitions. -- [ ] Add tests for control commands. +- [x] Add tests for control commands. - [ ] Add tests for transactional reread. - [ ] Add tests that HTTP worker behavior is unchanged. diff --git a/tests/test_companion_control.py b/tests/test_companion_control.py index 2eb6f565..45013dc7 100644 --- a/tests/test_companion_control.py +++ b/tests/test_companion_control.py @@ -7,12 +7,23 @@ from unittest import mock import pytest +from gunicorn.companion.config import CompanionConfig from gunicorn.companion.control import ( CommandError, ControlServer, decode_command, encode_response, ) +from gunicorn.companion.manager import CompanionManager + + +def make_manager(*names): + configs = [CompanionConfig(name=name, target=lambda: None) for name in names] + return CompanionManager(configs, log=mock.Mock()) + + +def server_for(manager): + return ControlServer(dispatch=manager.handle_command, path="/tmp/x.sock") def test_decode_command_valid(): @@ -85,3 +96,41 @@ def test_close_unlinks(): server.close() unlink.assert_called_once_with("/tmp/x.sock") assert server.listener is None + + +def test_control_status_command_end_to_end(): + manager = make_manager("rq") + response = json.loads(server_for(manager).handle_line('{"cmd": "status"}')) + assert response["ok"] is True + assert response["companions"][0]["name"] == "rq" + + +def test_control_start_command_end_to_end(): + manager = make_manager("rq") + with mock.patch("os.fork", return_value=10): + response = json.loads( + server_for(manager).handle_line('{"cmd": "start", "name": "rq"}')) + assert response["ok"] is True + assert "rq" in response["message"] + + +def test_control_unknown_command_error_envelope(): + manager = make_manager("rq") + response = json.loads( + server_for(manager).handle_line('{"cmd": "bogus", "name": "rq"}')) + assert response["ok"] is False + assert "unknown" in response["error"] + + +def test_control_missing_name_error_envelope(): + manager = make_manager("rq") + response = json.loads(server_for(manager).handle_line('{"cmd": "start"}')) + assert response["ok"] is False + assert "name" in response["error"] + + +def test_control_reread_without_loader_error_envelope(): + manager = make_manager("rq") + response = json.loads(server_for(manager).handle_line('{"cmd": "reread"}')) + assert response["ok"] is False + assert "reread" in response["error"]