From c8a2d563cef8a584be0485330bf37baf10021a65 Mon Sep 17 00:00:00 2001 From: mwiegand Date: Fri, 8 May 2026 18:09:45 +0200 Subject: [PATCH] fix(l4d2-web): server delete job now removes the DB row The delete job ran l4d2ctl delete (host-side cleanup) but never removed the Server row, so deleted servers kept appearing on /servers. Hard-delete the row in the worker's success path and skip the post-op status refresh, since the systemd unit is gone. Co-Authored-By: Claude Opus 4.7 (1M context) --- l4d2web/services/job_worker.py | 9 ++++++++ l4d2web/tests/test_job_worker.py | 38 ++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/l4d2web/services/job_worker.py b/l4d2web/services/job_worker.py index 3f810a0..0c084c9 100644 --- a/l4d2web/services/job_worker.py +++ b/l4d2web/services/job_worker.py @@ -311,6 +311,15 @@ def run_job(job_id: int) -> None: on_stderr=on_stderr, should_cancel=should_cancel, ) + # Host-side cleanup succeeded; remove the DB row so the server + # disappears from /servers. Status refresh is skipped — the + # systemd unit is gone and querying it would just log noise. + with session_scope() as db: + server = db.scalar(select(Server).where(Server.id == server_id)) + if server is not None: + db.delete(server) + finish_job(job_id, "succeeded", 0) + return else: raise ValueError(f"unknown job operation: {operation}") diff --git a/l4d2web/tests/test_job_worker.py b/l4d2web/tests/test_job_worker.py index d71020a..2d0ca52 100644 --- a/l4d2web/tests/test_job_worker.py +++ b/l4d2web/tests/test_job_worker.py @@ -260,6 +260,44 @@ def test_unexpected_exception_fails_job_with_exit_code_one(seeded_worker, monkey job = load_job(job_id) assert job.state == "failed" assert job.exit_code == 1 + # Failed delete must keep the Server row. + with session_scope() as session: + assert session.scalar(select(Server).where(Server.id == ids.server_one)) is not None + + +def test_successful_delete_removes_server_row(seeded_worker, monkeypatch) -> None: + app, ids = seeded_worker + job_id = add_job(ids.user, "delete", server_id=ids.server_one) + + calls = [] + + def fake_delete(server_id, *, on_stdout=None, on_stderr=None, should_cancel=None): + del should_cancel + calls.append(server_id) + on_stdout("removing systemd unit") + + def fake_status(name): # pragma: no cover — would mean delete didn't skip refresh + pytest.fail(f"server_status must not be called after a successful delete (got {name!r})") + + monkeypatch.setattr(l4d2_facade, "delete_server", fake_delete) + monkeypatch.setattr(l4d2_facade, "server_status", fake_status) + + with app.app_context(): + assert run_worker_once() is True + + assert calls == [ids.server_one] + job = load_job(job_id) + assert job.state == "succeeded" + assert job.exit_code == 0 + with session_scope() as session: + assert session.scalar(select(Server).where(Server.id == ids.server_one)) is None + # Sibling server is untouched. + assert session.scalar(select(Server).where(Server.id == ids.server_two)) is not None + # The delete job itself stays in the job log; outerjoin in views shows + # "-" for its (now-orphaned) server_id pointer. + finished_job = session.scalar(select(Job).where(Job.id == job_id)) + assert finished_job is not None + assert finished_job.server_id == ids.server_one def test_same_server_jobs_do_not_overlap(seeded_worker, monkeypatch) -> None: