From 68f0ce0daed54d972ea7396c1e7f871c335e472c Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Wed, 24 Jun 2026 11:45:31 +0200 Subject: [PATCH] feat(infra): auto-commit con 3 cambios Co-Authored-By: Claude Opus 4.7 (1M context) --- .../functions/infra/comfyui_ensure_server.md | 90 +++++ .../functions/infra/comfyui_ensure_server.py | 326 ++++++++++++++++++ .../infra/comfyui_ensure_server_test.py | 156 +++++++++ 3 files changed, 572 insertions(+) create mode 100644 python/functions/infra/comfyui_ensure_server.md create mode 100644 python/functions/infra/comfyui_ensure_server.py create mode 100644 python/functions/infra/comfyui_ensure_server_test.py diff --git a/python/functions/infra/comfyui_ensure_server.md b/python/functions/infra/comfyui_ensure_server.md new file mode 100644 index 00000000..182cbe90 --- /dev/null +++ b/python/functions/infra/comfyui_ensure_server.md @@ -0,0 +1,90 @@ +--- +name: comfyui_ensure_server +kind: function +lang: py +domain: infra +version: "1.0.0" +purity: impure +signature: "def comfyui_ensure_server(*, port: int = 8188, lowvram: bool | None = None, health_timeout: int = 60, comfyui_dir: str = '~/ComfyUI', unit_name: str = 'comfyui', runner=None) -> dict" +description: "Garantiza que ComfyUI corre como servicio systemd-user resiliente y sano. Genera/instala el unit systemd-user comfyui.service (ExecStart con el venv de ComfyUI + main.py --port, anadiendo --lowvram si lowvram=True o autodetectando GPUs <= 8 GB; Restart=always — NO on-failure; WantedBy=default.target), hace daemon-reload + enable + start, y comprueba la salud via GET /system_stats (2xx) con timeout. Idempotente: si el servicio ya esta gestionado por systemd, activo y respondiendo, no toca nada. Migracion limpia: si ComfyUI ya corre a mano (puerto ocupado por un proceso main.py que systemd NO gestiona), lo para con SIGTERM (nunca SIGKILL) y lo levanta via systemd. Solo stdlib (subprocess, urllib, os, signal, time, re). No lanza excepciones: devuelve un dict de estado." +tags: [comfyui, systemd, service, server, resilient, ml, healthcheck, infra] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: ["os", "re", "signal", "subprocess", "time", "urllib.request"] +params: + - name: port + desc: "puerto HTTP del backend ComfyUI; tambien el que escribe en el unit (--port) y el que sondea el health check (default 8188)" + - name: lowvram + desc: "True/False fuerza/omite el flag --lowvram en ExecStart; None autodetecta por VRAM (GPUs con <= 8200 MiB -> True). Recomendado True en GPUs de 8 GB para modelos grandes (Flux, video)" + - name: health_timeout + desc: "segundos maximos sondeando GET /system_stats tras arrancar el servicio antes de declararlo no-sano (default 60)" + - name: comfyui_dir + desc: "raiz de la instalacion de ComfyUI; debe contener .venv/bin/python y main.py (default ~/ComfyUI, se expande y normaliza a absoluto)" + - name: unit_name + desc: "nombre del unit systemd-user (sin .service); el archivo va a ~/.config/systemd/user/.service (default 'comfyui')" + - name: runner + desc: "callable(cmd: list) -> CompletedProcess inyectable para tests; default ejecuta subprocess.run capturando salida" +output: "dict con ok (bool: servicio activo y sano), active (ActiveState del unit: active|inactive|failed), port, health (bool: /system_stats respondio 2xx), error (str|None), lowvram (bool aplicado), unit_path (ruta del .service escrito), migrated (bool: paro un ComfyUI a mano para migrar a systemd), reloaded (bool: hubo daemon-reload), idempotent (bool: ya estaba activo+sano y no se toco nada)" +tested: true +tests: + - "_detect_lowvram aplica el umbral de 8 GB (8192/8200 -> True, 8201/24564/None -> False)" + - "_render_unit incluye Restart=always, WantedBy=default.target y nunca on-failure; anade --lowvram solo cuando corresponde" + - "error claro si falta el venv python en comfyui_dir" + - "idempotente: si is-active=active y /system_stats sano, no llama a start" + - "arranque fresco: escribe el unit, daemon-reload + enable + start y espera salud" + - "lowvram=False omite el flag --lowvram en el unit escrito" +test_file_path: "python/functions/infra/comfyui_ensure_server_test.py" +file_path: "python/functions/infra/comfyui_ensure_server.py" +--- + +## Ejemplo + +```python +import sys, os +sys.path.insert(0, os.path.join("python", "functions")) +from infra.comfyui_ensure_server import comfyui_ensure_server + +# Deja ComfyUI corriendo como servicio systemd-user, sano y con --lowvram +# autodetectado en GPUs de 8 GB. Idempotente: relanzarla no rompe nada. +res = comfyui_ensure_server(port=8188, lowvram=True) +print(res) +# {'ok': True, 'active': 'active', 'port': 8188, 'health': True, 'error': None, +# 'lowvram': True, 'unit_path': '/home/enmanuel/.config/systemd/user/comfyui.service', +# 'migrated': True, 'reloaded': True, 'idempotent': False} +``` + +CLI directa (despacha por el venv del registry): + +```bash +python/.venv/bin/python3 python/functions/infra/comfyui_ensure_server.py --port=8188 --lowvram +``` + +El usuario lo gestiona despues con systemd-user normal: + +```bash +systemctl --user status comfyui # estado + ultimos logs +systemctl --user restart comfyui # reiniciar (la salud vuelve verde sola) +systemctl --user stop comfyui # parar +systemctl --user disable --now comfyui # revertir: para y deshabilita el arranque automatico +journalctl --user -u comfyui -n 50 # diagnosticar fallos de arranque +``` + +## Cuando usarla + +Usala cuando necesites que ComfyUI este garantizado arriba y sano antes de +encolar workflows (txt2img, video, 3D), o para convertir el ComfyUI que hoy se +relanza a mano en un servicio que arranca solo al boot y se reinicia si cae +(gap del roadmap 0064). Es el primer paso del grupo `comfyui`: dejar el backend +disponible; despues vienen `comfyui_build_*_workflow` + `comfyui_submit_workflow`. + +## Gotchas + +- **systemd-user requiere linger** para sobrevivir al cierre de sesion / arrancar al boot: `loginctl enable-linger $USER`. Sin linger el unit solo vive mientras hay sesion activa. Si `enable` falla por esto, el dict lo dice en `error`. +- **Migracion limpia con SIGTERM, nunca SIGKILL**: si ComfyUI ya corre a mano ocupando el puerto, la funcion lo para con SIGTERM y espera a que libere el bind (hasta ~25 s) antes de arrancar el servicio. Si el puerto lo ocupa un proceso que NO es ComfyUI (cmdline sin `main.py`), NO lo toca y devuelve `error` — no arranca para no duplicar el bind. +- **Cambiar los flags del unit (p.ej. lowvram) NO reinicia un servicio ya sano**: la funcion reescribe el `.service` y hace daemon-reload, pero si el servicio ya esta active+healthy no lo reinicia para no interrumpir. Para aplicar flags nuevos: `systemctl --user restart comfyui`. +- **Carga la GPU al arrancar**: levantar ComfyUI reserva VRAM. En una GPU de 8 GB compartida, evita lanzarlo mientras otra tarea pesada usa la GPU. +- **Restart=always (no on-failure)**: un `systemctl --user stop` limpio es exit success; con `on-failure` el servicio reviviria solo tras crash. Para pararlo de verdad usa `stop` (no `restart`) o `disable --now`. +- El health check es `GET http://127.0.0.1:/system_stats` y espera 2xx; solo loopback. diff --git a/python/functions/infra/comfyui_ensure_server.py b/python/functions/infra/comfyui_ensure_server.py new file mode 100644 index 00000000..96ce305c --- /dev/null +++ b/python/functions/infra/comfyui_ensure_server.py @@ -0,0 +1,326 @@ +"""Garantiza que ComfyUI corre como servicio systemd-user resiliente y sano. + +Funcion impura: instala/actualiza el unit systemd-user `comfyui.service`, lo +habilita y arranca, y comprueba la salud del backend HTTP. Idempotente: si el +servicio ya esta gestionado por systemd, activo y respondiendo, no toca nada. + +Migracion limpia: si ComfyUI ya corre a mano (puerto ocupado por un proceso +`main.py` que systemd NO gestiona), lo para con SIGTERM y lo levanta via +systemd, para que a partir de ese momento se reinicie solo (Restart=always). + +Solo depende de la stdlib (subprocess, urllib, os, signal, time, re). No lanza +excepciones: siempre devuelve un dict de estado. +""" + +import os +import re +import signal +import subprocess +import time +import urllib.request + + +def _default_runner(cmd): + """Ejecuta un comando capturando salida. Inyectable para tests.""" + return subprocess.run(cmd, capture_output=True, text=True, timeout=30) + + +def _detect_lowvram(vram_mib): + """Decide si conviene --lowvram segun la VRAM total en MiB. + + GPUs con <= 8200 MiB (tarjetas de 8 GB) ganan estabilidad con --lowvram para + modelos grandes (Flux, video). Si no hay dato de VRAM (None), NO asume + lowvram: devuelve False para no penalizar GPUs grandes sin necesidad. + """ + return vram_mib is not None and vram_mib <= 8200 + + +def _query_vram_mib(runner): + """Lee la VRAM total (MiB) de la primera GPU via nvidia-smi. None si falla.""" + try: + r = runner( + [ + "nvidia-smi", + "--query-gpu=memory.total", + "--format=csv,noheader,nounits", + ] + ) + if r.returncode == 0 and r.stdout.strip(): + return int(r.stdout.strip().splitlines()[0].strip()) + except Exception: + pass + return None + + +def _render_unit(python_bin, main_py, working_dir, port, lowvram, description): + """Construye el texto del unit systemd-user. Pura (sin I/O).""" + exec_start = f"{python_bin} {main_py} --port {port}" + if lowvram: + exec_start += " --lowvram" + return ( + "[Unit]\n" + f"Description={description}\n" + "After=network-online.target\n" + "Wants=network-online.target\n" + "\n" + "[Service]\n" + "Type=simple\n" + f"WorkingDirectory={working_dir}\n" + f"ExecStart={exec_start}\n" + # Restart=always (NO on-failure): un SIGTERM limpio es exit success y + # con on-failure el servicio no reviviria. Ver .claude/rules/function_tags.md. + "Restart=always\n" + "RestartSec=5\n" + "\n" + "[Install]\n" + "WantedBy=default.target\n" + ) + + +def _health(port, path="/system_stats", timeout=3): + """True si GET http://127.0.0.1: responde 2xx.""" + url = f"http://127.0.0.1:{port}{path}" + try: + with urllib.request.urlopen(url, timeout=timeout) as resp: + return 200 <= resp.status < 300 + except Exception: + return False + + +def _wait_health(port, timeout, interval=2.0): + """Sondea la salud hasta que responda 2xx o se agote el timeout.""" + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + if _health(port): + return True + time.sleep(interval) + return _health(port) + + +def _systemctl(runner, *args): + return runner(["systemctl", "--user", *args]) + + +def _unit_active_state(runner, unit_name): + """Devuelve el ActiveState del unit: active|inactive|failed|... o '' si no existe.""" + r = _systemctl(runner, "is-active", unit_name) + return (r.stdout or r.stderr or "").strip() + + +def _pid_listening_on_port(port, runner): + """PID del proceso que escucha en 127.0.0.1:, o None. Via `ss`.""" + try: + r = runner(["ss", "-ltnpH", f"sport = :{port}"]) + if r.returncode == 0: + m = re.search(r"pid=(\d+)", r.stdout or "") + if m: + return int(m.group(1)) + except Exception: + pass + return None + + +def _is_comfy_process(pid): + """True si la cmdline del PID contiene 'main.py' (proceso ComfyUI a mano).""" + try: + with open(f"/proc/{pid}/cmdline", "rb") as f: + cmd = f.read().replace(b"\0", b" ").decode(errors="replace") + return "main.py" in cmd + except Exception: + return False + + +def _terminate_manual(pid, port, runner, wait_s=25.0): + """SIGTERM al proceso a mano y espera a que libere el puerto. No usa SIGKILL.""" + try: + os.kill(pid, signal.SIGTERM) + except ProcessLookupError: + return True + except Exception: + return False + deadline = time.monotonic() + wait_s + while time.monotonic() < deadline: + if _pid_listening_on_port(port, runner) is None: + return True + time.sleep(1.0) + # Reintento suave de SIGTERM antes de rendirse (nunca SIGKILL: no destructivo). + try: + os.kill(pid, signal.SIGTERM) + except Exception: + pass + time.sleep(3.0) + return _pid_listening_on_port(port, runner) is None + + +def comfyui_ensure_server( + *, + port=8188, + lowvram=None, + health_timeout=60, + comfyui_dir="~/ComfyUI", + unit_name="comfyui", + runner=None, +): + """Garantiza ComfyUI corriendo y sano como servicio systemd-user. + + Args: + port: puerto HTTP del backend ComfyUI (default 8188). + lowvram: True/False fuerza el flag --lowvram; None autodetecta por VRAM + (GPUs <= 8 GB -> True). + health_timeout: segundos maximos esperando a que /system_stats responda + tras arrancar el servicio. + comfyui_dir: raiz de la instalacion de ComfyUI (con .venv/ y main.py). + unit_name: nombre del unit systemd-user (sin .service). + runner: callable(cmd:list)->CompletedProcess inyectable para tests. + + Returns: + dict con: ok, active (ActiveState), port, health (bool), error (str|None), + lowvram (bool), unit_path, migrated (bool), reloaded (bool), + idempotent (bool). + """ + runner = runner or _default_runner + result = { + "ok": False, + "active": None, + "port": port, + "health": False, + "error": None, + "lowvram": None, + "unit_path": None, + "migrated": False, + "reloaded": False, + "idempotent": False, + } + + comfyui_dir = os.path.abspath(os.path.expanduser(comfyui_dir)) + python_bin = os.path.join(comfyui_dir, ".venv", "bin", "python") + main_py = os.path.join(comfyui_dir, "main.py") + if not os.path.exists(python_bin): + result["error"] = f"venv python no encontrado: {python_bin}" + return result + if not os.path.exists(main_py): + result["error"] = f"main.py no encontrado: {main_py}" + return result + + # 1. Resolver lowvram (autodetect por VRAM si es None). + lv = lowvram if lowvram is not None else _detect_lowvram(_query_vram_mib(runner)) + result["lowvram"] = bool(lv) + + # 2. Renderizar e instalar el unit (solo reescribe si cambio el contenido). + content = _render_unit( + python_bin, main_py, comfyui_dir, port, lv, + "ComfyUI (Stable Diffusion / Flux backend) gestionado por el registry", + ) + unit_dir = os.path.expanduser("~/.config/systemd/user") + try: + os.makedirs(unit_dir, exist_ok=True) + except Exception as e: + result["error"] = f"no se pudo crear {unit_dir}: {e}" + return result + unit_path = os.path.join(unit_dir, f"{unit_name}.service") + result["unit_path"] = unit_path + + existing = None + if os.path.exists(unit_path): + try: + with open(unit_path, "r") as f: + existing = f.read() + except Exception: + existing = None + changed = existing != content + if changed: + tmp = unit_path + ".tmp" + try: + with open(tmp, "w") as f: + f.write(content) + os.replace(tmp, unit_path) + except Exception as e: + result["error"] = f"no se pudo escribir el unit: {e}" + return result + rl = _systemctl(runner, "daemon-reload") + result["reloaded"] = rl.returncode == 0 + if rl.returncode != 0: + result["error"] = f"daemon-reload fallo: {(rl.stderr or '').strip()}" + return result + + # 3. Habilitar (idempotente; el linger del usuario ya debe estar activo). + en = _systemctl(runner, "enable", unit_name) + if en.returncode != 0: + result["error"] = ( + f"systemctl --user enable {unit_name} fallo: " + f"{(en.stderr or '').strip()}. " + "Si es por falta de linger: `loginctl enable-linger $USER`." + ) + return result + + # 4. Estado actual: salud HTTP + si systemd ya lo gestiona. + active_state = _unit_active_state(runner, unit_name) + health_now = _health(port) + + if health_now and active_state == "active": + # Ya gestionado por systemd y sano -> idempotente, no tocar. + result["ok"] = True + result["health"] = True + result["active"] = "active" + result["idempotent"] = not changed + return result + + if health_now and active_state != "active": + # Proceso a mano ocupa el puerto y systemd NO lo gestiona -> migrar limpio. + pid = _pid_listening_on_port(port, runner) + if pid and _is_comfy_process(pid): + if not _terminate_manual(pid, port, runner): + result["error"] = ( + f"no se pudo liberar el puerto {port} (PID {pid}) con SIGTERM; " + "no arranco el servicio para no duplicar el bind." + ) + return result + result["migrated"] = True + elif pid: + result["error"] = ( + f"puerto {port} ocupado por PID {pid} que no parece ComfyUI; " + "no lo toco ni arranco el servicio." + ) + return result + # Si pid es None pero health_now True: race raro; seguimos a start. + + # 5. Arrancar via systemd y esperar salud. + st = _systemctl(runner, "start", unit_name) + if st.returncode != 0: + result["active"] = _unit_active_state(runner, unit_name) + result["error"] = ( + f"systemctl --user start {unit_name} fallo: " + f"{(st.stderr or '').strip()}. Diagnostica con " + f"`journalctl --user -u {unit_name} -n 50`." + ) + return result + + healthy = _wait_health(port, health_timeout) + result["active"] = _unit_active_state(runner, unit_name) + result["health"] = healthy + result["ok"] = healthy + if not healthy: + result["error"] = ( + f"el unit arranco pero /system_stats no respondio 2xx en " + f"{health_timeout}s. Revisa `journalctl --user -u {unit_name} -n 50`." + ) + return result + + +if __name__ == "__main__": + import json + import sys + + kwargs = {} + for arg in sys.argv[1:]: + if arg.startswith("--port="): + kwargs["port"] = int(arg.split("=", 1)[1]) + elif arg == "--lowvram": + kwargs["lowvram"] = True + elif arg == "--no-lowvram": + kwargs["lowvram"] = False + elif arg.startswith("--health-timeout="): + kwargs["health_timeout"] = int(arg.split("=", 1)[1]) + elif arg.startswith("--comfyui-dir="): + kwargs["comfyui_dir"] = arg.split("=", 1)[1] + print(json.dumps(comfyui_ensure_server(**kwargs), indent=2)) diff --git a/python/functions/infra/comfyui_ensure_server_test.py b/python/functions/infra/comfyui_ensure_server_test.py new file mode 100644 index 00000000..527b3ceb --- /dev/null +++ b/python/functions/infra/comfyui_ensure_server_test.py @@ -0,0 +1,156 @@ +"""Tests para comfyui_ensure_server. + +Los tests no tocan systemd ni la red reales: inyectan un runner falso que +registra los comandos systemctl y se mockea el health check. +""" + +import os +import subprocess + +from . import comfyui_ensure_server as mod +from .comfyui_ensure_server import ( + _detect_lowvram, + _render_unit, + comfyui_ensure_server, +) + + +class FakeRunner: + """Runner inyectable: respuestas programables por prefijo de comando.""" + + def __init__(self, active_state="inactive"): + self.calls = [] + self.active_state = active_state + + def __call__(self, cmd): + self.calls.append(list(cmd)) + # nvidia-smi VRAM + if cmd[:1] == ["nvidia-smi"]: + return subprocess.CompletedProcess(cmd, 0, stdout="8192\n", stderr="") + if cmd[:2] == ["systemctl", "--user"]: + sub = cmd[2] if len(cmd) > 2 else "" + if sub == "is-active": + return subprocess.CompletedProcess( + cmd, 0, stdout=self.active_state + "\n", stderr="" + ) + # daemon-reload, enable, start -> exito + return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="") + if cmd[:1] == ["ss"]: + return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="") + return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="") + + def ran(self, *needle): + return any(call[: len(needle)] == list(needle) for call in self.calls) + + +def _fake_comfy_dir(tmp_path): + """Crea un comfyui_dir falso con .venv/bin/python y main.py.""" + d = tmp_path / "ComfyUI" + (d / ".venv" / "bin").mkdir(parents=True) + (d / ".venv" / "bin" / "python").write_text("#!/bin/sh\n") + (d / "main.py").write_text("# fake\n") + return d + + +# --- helpers puros --- + +def test_detect_lowvram_umbral_8gb(): + assert _detect_lowvram(8192) is True + assert _detect_lowvram(8200) is True + assert _detect_lowvram(8201) is False + assert _detect_lowvram(24564) is False + assert _detect_lowvram(None) is False + + +def test_render_unit_restart_always_y_wantedby(): + unit = _render_unit( + "/x/.venv/bin/python", "/x/main.py", "/x", 8188, True, "ComfyUI test" + ) + assert "Restart=always" in unit + assert "on-failure" not in unit # regla function_tags + assert "WantedBy=default.target" in unit + assert "ExecStart=/x/.venv/bin/python /x/main.py --port 8188 --lowvram" in unit + assert "WorkingDirectory=/x" in unit + + +def test_render_unit_sin_lowvram(): + unit = _render_unit( + "/x/.venv/bin/python", "/x/main.py", "/x", 9000, False, "ComfyUI test" + ) + assert "--lowvram" not in unit + assert "--port 9000" in unit + + +# --- orquestacion (runner falso + health mockeado) --- + +def test_error_si_falta_venv(tmp_path, monkeypatch): + monkeypatch.setenv("HOME", str(tmp_path)) + res = comfyui_ensure_server( + comfyui_dir=str(tmp_path / "no_existe"), runner=FakeRunner() + ) + assert res["ok"] is False + assert "venv python no encontrado" in res["error"] + + +def test_idempotente_si_ya_activo_y_sano(tmp_path, monkeypatch): + monkeypatch.setenv("HOME", str(tmp_path)) + monkeypatch.setattr(mod, "_health", lambda *a, **k: True) + d = _fake_comfy_dir(tmp_path) + runner = FakeRunner(active_state="active") + + # 1a llamada: instala el unit por primera vez (changed -> idempotent False), + # pero como ya esta active+sano NO debe arrancar nada. + res1 = comfyui_ensure_server(comfyui_dir=str(d), runner=runner) + assert res1["ok"] is True + assert res1["health"] is True + assert res1["active"] == "active" + assert res1["idempotent"] is False # escribio el unit por primera vez + assert not runner.ran("systemctl", "--user", "start", "comfyui") + + # 2a llamada: el unit ya existe identico -> no toca nada -> idempotent True. + runner2 = FakeRunner(active_state="active") + res2 = comfyui_ensure_server(comfyui_dir=str(d), runner=runner2) + assert res2["ok"] is True + assert res2["idempotent"] is True + assert res2["reloaded"] is False # no reescribio el unit + assert not runner2.ran("systemctl", "--user", "start", "comfyui") + + +def test_arranque_fresco_escribe_unit_y_arranca(tmp_path, monkeypatch): + monkeypatch.setenv("HOME", str(tmp_path)) + # health: False antes de arrancar, True despues + estados = iter([False, True, True, True]) + monkeypatch.setattr(mod, "_health", lambda *a, **k: next(estados, True)) + d = _fake_comfy_dir(tmp_path) + runner = FakeRunner(active_state="inactive") + res = comfyui_ensure_server(comfyui_dir=str(d), runner=runner, health_timeout=5) + assert res["ok"] is True + assert res["health"] is True + assert res["lowvram"] is True # nvidia-smi falso devuelve 8192 + assert runner.ran("systemctl", "--user", "daemon-reload") + assert runner.ran("systemctl", "--user", "enable", "comfyui") + assert runner.ran("systemctl", "--user", "start", "comfyui") + # el unit quedo escrito + unit_path = os.path.join( + str(tmp_path), ".config", "systemd", "user", "comfyui.service" + ) + assert os.path.exists(unit_path) + with open(unit_path) as f: + assert "Restart=always" in f.read() + + +def test_lowvram_forzado_false_omite_flag(tmp_path, monkeypatch): + monkeypatch.setenv("HOME", str(tmp_path)) + estados = iter([False, True]) + monkeypatch.setattr(mod, "_health", lambda *a, **k: next(estados, True)) + d = _fake_comfy_dir(tmp_path) + runner = FakeRunner(active_state="inactive") + res = comfyui_ensure_server( + comfyui_dir=str(d), runner=runner, lowvram=False, health_timeout=5 + ) + assert res["lowvram"] is False + unit_path = os.path.join( + str(tmp_path), ".config", "systemd", "user", "comfyui.service" + ) + with open(unit_path) as f: + assert "--lowvram" not in f.read()