test(companion): Add state transition tests

Add end-to-end chains over the per-unit tests: spawn to STARTING, promote to
RUNNING, unexpected exit to BACKOFF, retry back to STARTING; the stop path
ending in manual STOPPED; and the restart path that respawns immediately when
the old child exits.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Tanmoy Sarkar 2026-06-09 23:04:40 +05:30
parent e780484d24
commit e15dd583b9
2 changed files with 49 additions and 1 deletions

View File

@ -696,7 +696,7 @@ No per-companion logic in Arbiter.
- [x] Add parent-death cleanup.
- [x] Add lifecycle logs.
- [x] Add tests for config validation.
- [ ] Add tests for state transitions.
- [x] Add tests for state transitions.
- [ ] Add tests for control commands.
- [ ] Add tests for transactional reread.
- [ ] Add tests that HTTP worker behavior is unchanged.

View File

@ -579,3 +579,51 @@ def test_spawn_parent_records_pid_and_starting():
assert proc.state == State.STARTING
assert proc.started_at is not None
assert proc.manual_stop is False
def test_lifecycle_running_crash_backoff_retry():
manager = make_manager("rq")
process = manager.processes["rq"]
assert process.state == State.STOPPED
with mock.patch("os.fork", return_value=100):
manager.spawn_process(process)
assert process.state == State.STARTING
manager.promote_running(now=process.started_at + process.config.startsecs)
assert process.state == State.RUNNING
manager.handle_exit(process, now=1000.0)
assert process.state == State.BACKOFF
assert process.next_retry_at == 1000.0 + process.restart_delay
with mock.patch("os.fork", return_value=101):
manager.retry_backoff(now=process.next_retry_at)
assert process.state == State.STARTING
def test_lifecycle_stop_to_stopped():
manager = make_manager("rq")
process = manager.processes["rq"]
with mock.patch("os.fork", return_value=200):
manager.spawn_process(process)
manager.promote_running(now=process.started_at + process.config.startsecs)
with mock.patch("os.kill") as kill:
manager.stop_process("rq", now=500.0)
assert process.state == State.STOPPING
assert process.manual_stop is True
kill.assert_called_once()
manager.handle_exit(process, now=501.0)
assert process.state == State.STOPPED
def test_lifecycle_restart_respawns_after_exit():
manager = make_manager("rq")
process = manager.processes["rq"]
with mock.patch("os.fork", return_value=300):
manager.spawn_process(process)
manager.promote_running(now=process.started_at + process.config.startsecs)
with mock.patch("os.kill"):
manager.restart_process("rq", now=600.0)
assert process.state == State.STOPPING
assert process.restart_pending is True
with mock.patch("os.fork", return_value=301):
manager.handle_exit(process, now=601.0)
assert process.state == State.STARTING
assert process.restart_pending is False