fix(cli): review pass on the docsgpt command

- The image pins DOCSGPT_HOME=/app: it ships no checkout, so the data home
  no longer depends on the working directory.
- api, worker, beat and migrate print the data home and env file they
  resolved, so an API and a worker started from different directories show
  it.
- The worker passes -Q only when asked; a bare worker consumes every
  configured queue, which honours EMBEDDINGS_QUEUE and DOCUMENT_PARSE_QUEUE.
- The worker runs through celery.start and returns its exit code; click
  usage errors print usage and exit 2 instead of a traceback.
- Windows: solo pool and no embedded scheduler (celery rejects -B there),
  with a pointer to the new `docsgpt beat` command, which runs the
  scheduler on its own.
- prefetch_models and verify_offline parse their arguments, so --help is
  help rather than a model name.
- A DOCSGPT_ENV_FILE that is not a file raises instead of booting with
  defaults.
This commit is contained in:
Alex committed 2026-09-07 17:53:01 +01:00
1 parent f6f0d49452
commit 3036ece3ff
8 files changed
+134 -23

No files matched your search

+46 -6
View File
@@ -6,6 +6,7 @@ import types
from pathlib import Path
from unittest.mock import MagicMock
import click
import pytest
from docsgpt import cli
@@ -34,7 +35,7 @@ class TestTopLevel:
class TestApi:
def test_gunicorn_argv(self, monkeypatch):
def test_gunicorn_argv(self, monkeypatch, capsys):
run = MagicMock()
monkeypatch.setitem(sys.modules, "gunicorn.app.wsgiapp", types.SimpleNamespace(run=run))
monkeypatch.setattr(sys, "platform", "linux")
@@ -47,6 +48,7 @@ class TestApi:
assert argv[argv.index("-w") + 1] == "2"
assert argv[argv.index("-k") + 1] == "docsgpt.gunicorn_worker.BoundedDrainUvicornWorker"
assert argv[argv.index("--config") + 1] == "python:docsgpt.gunicorn_conf"
assert "data home" in capsys.readouterr().err
def test_reload_uses_uvicorn(self, monkeypatch):
uvicorn = types.SimpleNamespace(run=MagicMock())
@@ -57,31 +59,62 @@ class TestApi:
class TestWorker:
@staticmethod
def _celery(monkeypatch):
def _celery(monkeypatch, start=None):
celery = MagicMock()
celery.start = start or MagicMock(return_value=0)
monkeypatch.setitem(sys.modules, "docsgpt.app", types.SimpleNamespace(celery=celery))
return celery
def test_defaults(self, monkeypatch):
def test_defaults_consume_every_configured_queue(self, monkeypatch, capsys):
celery = self._celery(monkeypatch)
monkeypatch.setattr(sys, "platform", "linux")
assert cli.main(["worker"]) == 0
argv = celery.worker_main.call_args.args[0]
argv = celery.start.call_args.args[0]
assert argv[:3] == ["worker", "-l", "INFO"]
assert argv[argv.index("-Q") + 1] == cli.DEFAULT_QUEUES
assert "-Q" not in argv, "a bare worker honours EMBEDDINGS_QUEUE and DOCUMENT_PARSE_QUEUE"
assert "-B" in argv
assert "--pool" not in argv
assert "data home" in capsys.readouterr().err
def test_options_and_the_macos_solo_pool(self, monkeypatch):
celery = self._celery(monkeypatch)
monkeypatch.setattr(sys, "platform", "darwin")
assert cli.main(["worker", "--no-beat", "-Q", "embeddings", "--concurrency", "2"]) == 0
argv = celery.worker_main.call_args.args[0]
argv = celery.start.call_args.args[0]
assert argv[argv.index("--pool") + 1] == "solo"
assert "-B" not in argv
assert argv[argv.index("-Q") + 1] == "embeddings"
assert argv[argv.index("--concurrency") + 1] == "2"
def test_windows_runs_solo_without_the_embedded_scheduler(self, monkeypatch, capsys):
celery = self._celery(monkeypatch)
monkeypatch.setattr(sys, "platform", "win32")
assert cli.main(["worker"]) == 0
argv = celery.start.call_args.args[0]
assert "-B" not in argv
assert argv[argv.index("--pool") + 1] == "solo"
assert "docsgpt beat" in capsys.readouterr().err
def test_the_worker_exit_code_is_returned(self, monkeypatch):
self._celery(monkeypatch, start=MagicMock(return_value=1))
monkeypatch.setattr(sys, "platform", "linux")
assert cli.main(["worker"]) == 1
def test_a_usage_error_prints_usage_instead_of_a_traceback(self, monkeypatch, capsys):
self._celery(monkeypatch, start=MagicMock(side_effect=click.UsageError("No such option: --bogus")))
monkeypatch.setattr(sys, "platform", "linux")
assert cli.main(["worker"]) == 2
assert "No such option" in capsys.readouterr().err
class TestBeat:
def test_runs_the_scheduler_alone(self, monkeypatch):
celery = MagicMock()
celery.start = MagicMock(return_value=0)
monkeypatch.setitem(sys.modules, "docsgpt.app", types.SimpleNamespace(celery=celery))
assert cli.main(["beat", "-l", "DEBUG"]) == 0
assert celery.start.call_args.args[0] == ["beat", "-l", "DEBUG"]
class TestMigrate:
def test_runs_the_bootstrap(self, monkeypatch):
@@ -109,3 +142,10 @@ class TestScripts:
def test_the_script_exit_code_is_returned(self, monkeypatch):
monkeypatch.setattr("docsgpt.scripts.verify_offline.main", MagicMock(return_value=3))
assert cli.main(["verify-offline"]) == 3
@pytest.mark.parametrize("script", ["prefetch-models", "verify-offline"])
def test_help_is_help_not_a_model_name(self, script, capsys):
with pytest.raises(SystemExit) as exc:
cli.main([script, "--help"])
assert exc.value.code == 0
assert "models" in capsys.readouterr().out