Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/backend/routers/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
from database import db
from dependencies import hash_password
from services.system_seed_service import ensure_first_run_seeded
from services.system_agent_service import system_agent_service
from services.operator_intake_service import submit_operator_intake
from utils.password_validation import validate_password_strength, PASSWORD_REQUIREMENTS_MESSAGE
from utils.admin_identity import admin_username, is_usable_password_hash
Expand Down Expand Up @@ -159,6 +160,16 @@ async def get_setup_status():
}


async def _deploy_system_agent() -> None:
"""Deploy the system agent after setup (#3237). Never raises: setup has
already succeeded, and the next backend start retries."""
try:
result = await system_agent_service.ensure_deployed()
logger.info("System agent after setup: %s - %s", result.get("action"), result.get("message"))
except Exception as e: # noqa: BLE001
logger.error("System agent deploy after setup failed: %s", e)


@router.post("/admin-password")
async def set_admin_password(
data: SetAdminPasswordRequest, request: Request, background_tasks: BackgroundTasks
Expand Down Expand Up @@ -327,6 +338,13 @@ async def set_admin_password(
# never double-provision or surprise an established fleet.
background_tasks.add_task(ensure_first_run_seeded)

# #3237: the system agent needs the admin as its owner. Its only other
# deploy attempt runs at backend startup, which on a fresh install is before
# this endpoint, so without this it stayed missing until the next restart.
# Same background-task rule as the seed pass above; ensure_deployed is
# idempotent, so a later restart stays a no-op.
background_tasks.add_task(_deploy_system_agent)

# Operator intake (trinity-enterprise#38): only on affirmative consent.
# Scheduled as a background task so it runs AFTER the response is sent — it
# can never delay or break setup. The service is idempotent (once-per-install)
Expand Down
51 changes: 51 additions & 0 deletions tests/unit/test_3237_system_agent_after_setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""
#3237: the system agent is deployed once /setup has created the admin.

The only other deploy attempt runs at backend startup, which on a fresh install
is before the admin exists, so it fails with "Admin user 'admin' not found" and
nothing retried it until the backend next restarted.

Module: src/backend/routers/setup.py
"""

import asyncio

import pytest
from fastapi import BackgroundTasks

from test_setup_operator_profile import _get_setup, _req, _run, _task_funcs, patched # noqa: F401

pytestmark = pytest.mark.unit


def test_setup_schedules_system_agent_deploy(patched):
bg = BackgroundTasks()
_run(_req(), bg)
setup = _get_setup()
funcs = _task_funcs(bg)
assert setup._deploy_system_agent in funcs
# After the seed pass: Cornelius and the starter agents come first.
assert funcs.index(setup.ensure_first_run_seeded) < funcs.index(setup._deploy_system_agent)


def test_system_agent_deploy_failure_never_raises(monkeypatch):
setup = _get_setup()

async def boom():
raise RuntimeError("docker unreachable")

monkeypatch.setattr(setup.system_agent_service, "ensure_deployed", boom)
asyncio.run(setup._deploy_system_agent())


def test_system_agent_deploy_calls_ensure_deployed(monkeypatch):
setup = _get_setup()
calls = []

async def ok():
calls.append(1)
return {"action": "created", "status": "success", "message": "ok"}

monkeypatch.setattr(setup.system_agent_service, "ensure_deployed", ok)
asyncio.run(setup._deploy_system_agent())
assert calls == [1]
Loading