Skip to content

feat(cli): add server workers option#1851

Open
adityasingh2400 wants to merge 8 commits intoNVIDIA-NeMo:developfrom
adityasingh2400:feat/server-workers-option
Open

feat(cli): add server workers option#1851
adityasingh2400 wants to merge 8 commits intoNVIDIA-NeMo:developfrom
adityasingh2400:feat/server-workers-option

Conversation

@adityasingh2400
Copy link
Copy Markdown

@adityasingh2400 adityasingh2400 commented May 6, 2026

Summary

  • Add a --workers option to nemoguardrails server.
  • Pass the configured worker count through to uvicorn.run.
  • Document the new CLI option in the server command reference.

Fixes #1746

Verification

  • python3 -m compileall nemoguardrails/cli/__init__.py

PR overlap check

  • Searched open PRs for 1746; no existing open PR covers this issue.

Summary by CodeRabbit

  • New Features
    • Added --workers CLI option to the server command for specifying the number of Uvicorn worker processes to run (default: 1, minimum: 1), enabling multi-process serving configurations.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 6, 2026

Documentation preview

https://nvidia-nemo.github.io/Guardrails/review/pr-1851

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 6, 2026

Greptile Summary

This PR adds a --workers CLI option to nemoguardrails server, enabling multi-process uvicorn deployments. It solves the per-process app-state problem by serialising all configuration into environment variables before uvicorn forks workers, and introduces a create_app() factory so each worker imports api.py fresh and reads those env vars at module-load time.

  • nemoguardrails/cli/__init__.py — adds --workers, sets env vars for all flags, and branches between single-worker (direct app object) and multi-worker (import-string + factory=True) uvicorn invocation; correctly guards --prefix and --auto-reload from being combined with workers > 1.
  • nemoguardrails/server/api.py — reads new env vars at module level and exposes create_app() for uvicorn's factory mode.
  • tests/cli/test_cli_main.py — adds tests for env-var propagation and workers/reject paths, but the pre-existing test_server_with_disable_chat_ui and test_server_with_auto_reload tests are now broken by the new os.fspath(rails_config_path) call when api.app is mocked and no local config/ directory exists at the repo root.

Confidence Score: 3/5

The multi-worker logic is correct, but the PR breaks two pre-existing tests that will fail on every CI run.

The new unconditional os.fspath(rails_config_path) call raises TypeError when api.app is mocked and no local config/ directory exists at the repo root. Both test_server_with_disable_chat_ui and test_server_with_auto_reload hit this path and will fail until they receive the same mock decorators used by the newly-added tests.

tests/cli/test_cli_main.py — the two pre-existing server tests need @patch("os.getcwd"), @patch("os.path.exists"), and @patch.dict(os.environ, {}, clear=True) to survive the new code path.

Important Files Changed

Filename Overview
nemoguardrails/cli/init.py Adds --workers option; propagates all CLI flags via env vars before spawning workers; guards --prefix and --auto-reload against multi-worker mode; uses import-string + factory for multi-worker uvicorn invocation.
nemoguardrails/server/api.py Reads new env vars at module-level so each worker process picks up configuration on fresh import; adds create_app() factory for uvicorn's factory mode; minor inconsistency between disable_chat_ui (env var read in __init__) and auto_reload (module-level override).
tests/cli/test_cli_main.py Adds tests for env var propagation, workers factory path, and prefix/auto-reload rejection; however, the pre-existing test_server_with_disable_chat_ui and test_server_with_auto_reload tests are now broken by the new os.fspath call when the repo root has no config/ directory.
docs/reference/cli/index.md Documents the new --workers option in the server command reference table; no issues.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["nemoguardrails server --workers N ..."] --> B{workers > 1?}
    B -- No --> C["Set env vars\nNEMO_GUARDRAILS_CONFIG_PATH / VERBOSE\nDISABLE_CHAT_UI / AUTO_RELOAD\nDEFAULT_CONFIG_ID"]
    B -- Yes --> D{prefix set?}
    D -- Yes --> E["Exit 1: prefix not supported"]
    D -- No --> F{auto_reload set?}
    F -- Yes --> G["Exit 1: auto-reload not supported"]
    F -- No --> C
    C --> H{workers > 1?}
    H -- No --> I["uvicorn.run(server_app, ...)"]
    H -- Yes --> J["uvicorn.run('...api:create_app', factory=True, workers=N)"]
    J --> K["Worker Process 1..N\nimport nemoguardrails.server.api\n(reads env vars at module level)"]
    K --> L["create_app() returns configured app singleton"]
    L --> M["Uvicorn serves requests"]
    I --> M
Loading

Comments Outside Diff (1)

  1. tests/cli/test_cli_main.py, line 195-207 (link)

    P1 Existing tests break on CI — os.fspath raises TypeError on mocked api.app

    The PR's new code path unconditionally calls os.fspath(rails_config_path) before setting the env var. When neither --config is passed nor a local config/ directory exists (confirmed absent at the repo root), the code falls through to rails_config_path = api.app.rails_config_path, which is a MagicMock attribute in these two tests. os.fspath rejects anything that isn't str, bytes, or a proper os.PathLike; it calls __fspath__() on the mock, gets back another MagicMock, and raises TypeError. The invocation exits with code 1, causing assert result.exit_code == 0 to fail. The three newly-added tests avoid this exactly by patching os.getcwd and os.path.exists — the same treatment is needed here.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: tests/cli/test_cli_main.py
    Line: 195-207
    
    Comment:
    **Existing tests break on CI — `os.fspath` raises `TypeError` on mocked `api.app`**
    
    The PR's new code path unconditionally calls `os.fspath(rails_config_path)` before setting the env var. When neither `--config` is passed nor a local `config/` directory exists (confirmed absent at the repo root), the code falls through to `rails_config_path = api.app.rails_config_path`, which is a `MagicMock` attribute in these two tests. `os.fspath` rejects anything that isn't `str`, `bytes`, or a proper `os.PathLike`; it calls `__fspath__()` on the mock, gets back another `MagicMock`, and raises `TypeError`. The invocation exits with code 1, causing `assert result.exit_code == 0` to fail. The three newly-added tests avoid this exactly by patching `os.getcwd` and `os.path.exists` — the same treatment is needed here.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
tests/cli/test_cli_main.py:195-207
**Existing tests break on CI — `os.fspath` raises `TypeError` on mocked `api.app`**

The PR's new code path unconditionally calls `os.fspath(rails_config_path)` before setting the env var. When neither `--config` is passed nor a local `config/` directory exists (confirmed absent at the repo root), the code falls through to `rails_config_path = api.app.rails_config_path`, which is a `MagicMock` attribute in these two tests. `os.fspath` rejects anything that isn't `str`, `bytes`, or a proper `os.PathLike`; it calls `__fspath__()` on the mock, gets back another `MagicMock`, and raises `TypeError`. The invocation exits with code 1, causing `assert result.exit_code == 0` to fail. The three newly-added tests avoid this exactly by patching `os.getcwd` and `os.path.exists` — the same treatment is needed here.

### Issue 2 of 3
tests/cli/test_cli_main.py:221-228
`test_server_api_reads_worker_env_on_import` seeds the subprocess env with `**os.environ`, so any `NEMO_GUARDRAILS_*` variable set in the CI runner bleeds into the subprocess alongside the intended overrides. Starting from a minimal, known-clean env makes the test hermetic.

```suggestion
    def test_server_api_reads_worker_env_on_import(self):
        env = {
            "NEMO_GUARDRAILS_CONFIG_PATH": "/worker/config",
            "NEMO_GUARDRAILS_DISABLE_CHAT_UI": "true",
            "NEMO_GUARDRAILS_AUTO_RELOAD": "true",
            "NEMO_GUARDRAILS_DEFAULT_CONFIG_ID": "worker_config",
        }
```

### Issue 3 of 3
nemoguardrails/server/api.py:73-74
`auto_reload` is initialised to `False` inside `GuardrailsApp.__init__` then overridden by a module-level statement that reads the env var, while `disable_chat_ui` reads the env var directly in `__init__`. Aligning `auto_reload` to also read from the env var in `__init__` makes the initialisation pattern uniform.

```suggestion
        self.disable_chat_ui: bool = os.getenv("NEMO_GUARDRAILS_DISABLE_CHAT_UI", "false").lower() == "true"
        self.auto_reload: bool = os.getenv("NEMO_GUARDRAILS_AUTO_RELOAD", "false").lower() == "true"
```

Reviews (7): Last reviewed commit: "test(server): isolate verbose worker env..." | Re-trigger Greptile

Comment thread nemoguardrails/cli/__init__.py Outdated
api.set_default_config_id(default_config_id) # Call function

uvicorn.run(server_app, port=port, log_level="info", host="0.0.0.0")
uvicorn.run(server_app, port=port, log_level="info", host="0.0.0.0", workers=workers)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 workers > 1 silently exits when app is passed as an instance

Uvicorn requires an import string (e.g. "nemoguardrails.server.api:app") — not an application object — when workers is set to anything greater than 1. Passing the live server_app object with workers=2 causes uvicorn to print "You must pass the application as an import string to enable 'reload' or 'workers'" and then exit with SystemError 1. This means every invocation of --workers N where N > 1 is silently broken.

The fix requires either (a) passing the app as an import string when workers > 1 (which complicates the current per-process attribute-patching approach used for rails_config_path, disable_chat_ui, etc.), or (b) raising an explicit error when workers > 1 with a clear message about this constraint until a proper solution is implemented.

Prompt To Fix With AI
This is a comment left during a code review.
Path: nemoguardrails/cli/__init__.py
Line: 204

Comment:
**`workers > 1` silently exits when app is passed as an instance**

Uvicorn requires an import string (e.g. `"nemoguardrails.server.api:app"`) — not an application object — when `workers` is set to anything greater than 1. Passing the live `server_app` object with `workers=2` causes uvicorn to print `"You must pass the application as an import string to enable 'reload' or 'workers'"` and then exit with `SystemError 1`. This means every invocation of `--workers N` where N > 1 is silently broken.

The fix requires either (a) passing the app as an import string when `workers > 1` (which complicates the current per-process attribute-patching approach used for `rails_config_path`, `disable_chat_ui`, etc.), or (b) raising an explicit error when `workers > 1` with a clear message about this constraint until a proper solution is implemented.

How can I resolve this? If you propose a fix, please make it concise.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 6, 2026

📝 Walkthrough

Walkthrough

A --workers CLI flag is added to the NeMo Guardrails server command, allowing users to specify the number of Uvicorn worker processes. The parameter defaults to 1 for backward compatibility and is passed directly to uvicorn.run(). Documentation is updated accordingly.

Changes

Multi-Worker Server Support

Layer / File(s) Summary
Parameter Definition
nemoguardrails/cli/__init__.py
New workers parameter added to the server() command with typer.Option(default=1, min=1) to accept worker count from CLI.
Uvicorn Integration
nemoguardrails/cli/__init__.py
The workers argument is passed to uvicorn.run() to enable multi-worker serving.
Documentation
docs/reference/cli/index.md
Server options table updated to document the new --workers flag and its behavior (default: 1).

Estimated code review effort

🎯 1 (Trivial) | ⏱️ ~5 minutes

🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Test Results For Major Changes ⚠️ Warning Major new feature (--workers CLI option) added without test results or testing information. Only syntax verification mentioned, no functional tests. Add test results or test cases verifying --workers works correctly, especially for multi-worker scenarios. Include functional testing in PR description or add tests.
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding a server workers option to the CLI. It is specific and clearly indicates the primary modification.
Linked Issues check ✅ Passed The PR successfully implements all coding requirements from issue #1746: adds a workers parameter with default value of 1, passes it to uvicorn.run, and documents the option.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the --workers flag feature. Documentation and CLI code modifications are both in scope for the linked issue.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@nemoguardrails/cli/__init__.py`:
- Line 204: The current call uvicorn.run(server_app, ...) passes a live app
instance so --workers>1 fails and the per-process mutations (rails_config_path,
disable_chat_ui, auto_reload, default_config_id set between lines 173-202) don’t
propagate to workers; change to run Uvicorn with an import string or factory so
workers spawn correctly and receive configuration: expose a factory like
create_app(...) or export the ASGI app as a module-level name (e.g.,
"nemoguardrails.cli.__init__:server_app" or better
"nemoguardrails.cli.__init__:create_app") and call uvicorn.run with that import
string when workers>1, and move the mutating logic (rails_config_path,
disable_chat_ui, auto_reload, default_config_id) into the app factory or into
startup hooks/environment variables so each worker initializes with the intended
config.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: f065d2bf-33da-4e7d-9f2d-b9bcf077d8cf

📥 Commits

Reviewing files that changed from the base of the PR and between 6a8fdf3 and 52e617a.

📒 Files selected for processing (2)
  • docs/reference/cli/index.md
  • nemoguardrails/cli/__init__.py

Comment thread nemoguardrails/cli/__init__.py Outdated
api.set_default_config_id(default_config_id) # Call function

uvicorn.run(server_app, port=port, log_level="info", host="0.0.0.0")
uvicorn.run(server_app, port=port, log_level="info", host="0.0.0.0", workers=workers)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🌐 Web query:

According to the latest Uvicorn documentation, can uvicorn.run(app_instance, workers=2) run correctly, or must multi-worker mode use an import string / app factory? Also confirm how worker process spawning affects parent-process app mutations.

💡 Result:

No, uvicorn.run(app_instance, workers=2) cannot run correctly in multi-worker mode. According to the latest Uvicorn documentation and deployment guides, when using workers > 1 (or reload=True), the application must be passed as an import string (e.g., "module:app") rather than a direct app instance. Passing an app instance triggers a warning and fails to spawn workers properly, as Uvicorn requires the import path to load fresh app instances in each worker process.[1][2][3][4][5][6] For app factories, use factory=True with an import string to a callable that returns the app (e.g., uvicorn.run("module:create_app", factory=True, workers=2)).[7][3] Worker process spawning uses multiprocessing (fork on Unix-like systems, spawn on Windows/macOS). The parent process acts as a manager and does not handle requests; each worker independently imports and instantiates the app from the string. Mutations to the app in the parent process (e.g., adding routes) do not affect workers, as they load their own copies after forking/spawning. Workers are isolated processes with copy-on-write memory (Unix) or fresh execution (spawn), so parent mutations post-spawn have no effect.[8][4][9][10][11]

Citations:


🏁 Script executed:

# First, let's look at the context around line 204
cat -n nemoguardrails/cli/__init__.py | sed -n '190,220p'

Repository: NVIDIA-NeMo/Guardrails

Length of output: 1314


🏁 Script executed:

# Get the function signature and more context around line 204
cat -n nemoguardrails/cli/__init__.py | sed -n '150,210p'

Repository: NVIDIA-NeMo/Guardrails

Length of output: 2376


🏁 Script executed:

# Get the function signature starting from `@app.command`() decorator
cat -n nemoguardrails/cli/__init__.py | sed -n '100,160p'

Repository: NVIDIA-NeMo/Guardrails

Length of output: 2406


--workers parameter is broken with multi-worker mode due to app instance limitation.

At line 204, uvicorn.run() is passed a live app instance (server_app). When workers > 1, Uvicorn cannot spawn multiple workers from an app instance—it requires an import string (e.g., "module:app") or a factory function. Additionally, critical app mutations (lines 173-202) including rails_config_path, disable_chat_ui, auto_reload, and default_config_id are applied in the parent process and will not propagate to worker processes, as each worker loads a fresh copy of the app. This renders the --workers flag non-functional for any value greater than 1, with no error message to alert users.

🧰 Tools
🪛 Ruff (0.15.12)

[error] 204-204: Possible binding to all interfaces

(S104)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@nemoguardrails/cli/__init__.py` at line 204, The current call
uvicorn.run(server_app, ...) passes a live app instance so --workers>1 fails and
the per-process mutations (rails_config_path, disable_chat_ui, auto_reload,
default_config_id set between lines 173-202) don’t propagate to workers; change
to run Uvicorn with an import string or factory so workers spawn correctly and
receive configuration: expose a factory like create_app(...) or export the ASGI
app as a module-level name (e.g., "nemoguardrails.cli.__init__:server_app" or
better "nemoguardrails.cli.__init__:create_app") and call uvicorn.run with that
import string when workers>1, and move the mutating logic (rails_config_path,
disable_chat_ui, auto_reload, default_config_id) into the app factory or into
startup hooks/environment variables so each worker initializes with the intended
config.

Comment on lines +210 to +216
if workers > 1:
if prefix:
typer.secho(
"The --prefix option is not supported with --workers > 1.",
fg=typer.colors.RED,
)
raise typer.Exit(1)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 When --auto-reload and --workers > 1 are combined, each uvicorn worker process inherits NEMO_GUARDRAILS_AUTO_RELOAD=true and independently enters the lifespan's start_auto_reload_monitoring branch, spawning N concurrent file watchers on the same directory. This causes redundant reload signals and potential race conditions. The PR already guards --prefix against workers > 1; the same guard is needed for --auto-reload.

Suggested change
if workers > 1:
if prefix:
typer.secho(
"The --prefix option is not supported with --workers > 1.",
fg=typer.colors.RED,
)
raise typer.Exit(1)
if workers > 1:
if prefix:
typer.secho(
"The --prefix option is not supported with --workers > 1.",
fg=typer.colors.RED,
)
raise typer.Exit(1)
if auto_reload:
typer.secho(
"The --auto-reload option is not supported with --workers > 1.",
fg=typer.colors.RED,
)
raise typer.Exit(1)
Prompt To Fix With AI
This is a comment left during a code review.
Path: nemoguardrails/cli/__init__.py
Line: 210-216

Comment:
When `--auto-reload` and `--workers > 1` are combined, each uvicorn worker process inherits `NEMO_GUARDRAILS_AUTO_RELOAD=true` and independently enters the lifespan's `start_auto_reload_monitoring` branch, spawning N concurrent file watchers on the same directory. This causes redundant reload signals and potential race conditions. The PR already guards `--prefix` against `workers > 1`; the same guard is needed for `--auto-reload`.

```suggestion
    if workers > 1:
        if prefix:
            typer.secho(
                "The --prefix option is not supported with --workers > 1.",
                fg=typer.colors.RED,
            )
            raise typer.Exit(1)
        if auto_reload:
            typer.secho(
                "The --auto-reload option is not supported with --workers > 1.",
                fg=typer.colors.RED,
            )
            raise typer.Exit(1)
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +257 to +273
@patch("uvicorn.run")
@patch("nemoguardrails.server.api.app")
def test_server_rejects_prefix_with_workers(self, mock_app, mock_uvicorn):
result = runner.invoke(app, ["server", "--workers=2", "--prefix=/api/v1"])

assert result.exit_code == 1
assert "The --prefix option is not supported with --workers > 1." in result.output
mock_uvicorn.assert_not_called()

@patch("uvicorn.run")
@patch("nemoguardrails.server.api.app")
def test_server_rejects_auto_reload_with_workers(self, mock_app, mock_uvicorn):
result = runner.invoke(app, ["server", "--workers=2", "--auto-reload"])

assert result.exit_code == 1
assert "The --auto-reload option is not supported with --workers > 1." in result.output
mock_uvicorn.assert_not_called()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 The two reject tests don't mock os.getcwd() / os.path.exists() and don't guard the environment with @patch.dict. When there is no config/ directory in the working directory, rails_config_path falls back to api.app.rails_config_path (a MagicMock), which means os.fspath returns "" and NEMO_GUARDRAILS_CONFIG_PATH is set to an empty string in the real os.environ. That leaked value persists into subsequent tests in the same process and could silently affect any test that reads NEMO_GUARDRAILS_CONFIG_PATH without clearing it first. Adding the same isolation decoration used by the other workers test fixes both problems.

Suggested change
@patch("uvicorn.run")
@patch("nemoguardrails.server.api.app")
def test_server_rejects_prefix_with_workers(self, mock_app, mock_uvicorn):
result = runner.invoke(app, ["server", "--workers=2", "--prefix=/api/v1"])
assert result.exit_code == 1
assert "The --prefix option is not supported with --workers > 1." in result.output
mock_uvicorn.assert_not_called()
@patch("uvicorn.run")
@patch("nemoguardrails.server.api.app")
def test_server_rejects_auto_reload_with_workers(self, mock_app, mock_uvicorn):
result = runner.invoke(app, ["server", "--workers=2", "--auto-reload"])
assert result.exit_code == 1
assert "The --auto-reload option is not supported with --workers > 1." in result.output
mock_uvicorn.assert_not_called()
@patch.dict(os.environ, {}, clear=True)
@patch("uvicorn.run")
@patch("nemoguardrails.server.api.app")
@patch("os.path.exists")
@patch("os.getcwd")
def test_server_rejects_prefix_with_workers(
self, mock_getcwd, mock_exists, mock_app, mock_uvicorn
):
mock_getcwd.return_value = "/current/dir"
mock_exists.return_value = True
result = runner.invoke(app, ["server", "--workers=2", "--prefix=/api/v1"])
assert result.exit_code == 1
assert "The --prefix option is not supported with --workers > 1." in result.output
mock_uvicorn.assert_not_called()
@patch.dict(os.environ, {}, clear=True)
@patch("uvicorn.run")
@patch("nemoguardrails.server.api.app")
@patch("os.path.exists")
@patch("os.getcwd")
def test_server_rejects_auto_reload_with_workers(
self, mock_getcwd, mock_exists, mock_app, mock_uvicorn
):
mock_getcwd.return_value = "/current/dir"
mock_exists.return_value = True
result = runner.invoke(app, ["server", "--workers=2", "--auto-reload"])
assert result.exit_code == 1
assert "The --auto-reload option is not supported with --workers > 1." in result.output
mock_uvicorn.assert_not_called()
Prompt To Fix With AI
This is a comment left during a code review.
Path: tests/cli/test_cli_main.py
Line: 257-273

Comment:
The two reject tests don't mock `os.getcwd()` / `os.path.exists()` and don't guard the environment with `@patch.dict`. When there is no `config/` directory in the working directory, `rails_config_path` falls back to `api.app.rails_config_path` (a `MagicMock`), which means `os.fspath` returns `""` and `NEMO_GUARDRAILS_CONFIG_PATH` is set to an empty string in the real `os.environ`. That leaked value persists into subsequent tests in the same process and could silently affect any test that reads `NEMO_GUARDRAILS_CONFIG_PATH` without clearing it first. Adding the same isolation decoration used by the other workers test fixes both problems.

```suggestion
    @patch.dict(os.environ, {}, clear=True)
    @patch("uvicorn.run")
    @patch("nemoguardrails.server.api.app")
    @patch("os.path.exists")
    @patch("os.getcwd")
    def test_server_rejects_prefix_with_workers(
        self, mock_getcwd, mock_exists, mock_app, mock_uvicorn
    ):
        mock_getcwd.return_value = "/current/dir"
        mock_exists.return_value = True
        result = runner.invoke(app, ["server", "--workers=2", "--prefix=/api/v1"])

        assert result.exit_code == 1
        assert "The --prefix option is not supported with --workers > 1." in result.output
        mock_uvicorn.assert_not_called()

    @patch.dict(os.environ, {}, clear=True)
    @patch("uvicorn.run")
    @patch("nemoguardrails.server.api.app")
    @patch("os.path.exists")
    @patch("os.getcwd")
    def test_server_rejects_auto_reload_with_workers(
        self, mock_getcwd, mock_exists, mock_app, mock_uvicorn
    ):
        mock_getcwd.return_value = "/current/dir"
        mock_exists.return_value = True
        result = runner.invoke(app, ["server", "--workers=2", "--auto-reload"])

        assert result.exit_code == 1
        assert "The --auto-reload option is not supported with --workers > 1." in result.output
        mock_uvicorn.assert_not_called()
```

How can I resolve this? If you propose a fix, please make it concise.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 6, 2026

Codecov Report

❌ Patch coverage is 88.88889% with 3 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
nemoguardrails/server/api.py 62.50% 3 Missing ⚠️

📢 Thoughts on this report? Let us know!

Comment on lines +209 to +215
@patch.dict(os.environ, {}, clear=True)
@patch("uvicorn.run")
@patch("nemoguardrails.server.api.app")
def test_server_with_verbose_sets_worker_env(self, mock_app, mock_uvicorn):
result = runner.invoke(app, ["server", "--verbose"])
assert result.exit_code == 0
assert os.environ["NEMO_GUARDRAILS_VERBOSE"] == "true"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Missing os.getcwd / os.path.exists mocks — test fails in CI

When neither --config is passed nor a config/ directory exists in the cwd, the CLI falls back to rails_config_path = api.app.rails_config_path (the patched MagicMock). The subsequent os.fspath(mock_app.rails_config_path) call requires __fspath__() to return a str or bytes; a MagicMock auto-attribute satisfies neither, so os.fspath raises TypeError. The repo root has no config/ directory, so os.path.exists returns False and the error path is always hit. result.exit_code will be 1 and the assert result.exit_code == 0 will fail. The three newly-added worker/reject tests correctly use @patch("os.getcwd") + @patch("os.path.exists") for exactly this reason — the same fix is needed here.

Prompt To Fix With AI
This is a comment left during a code review.
Path: tests/cli/test_cli_main.py
Line: 209-215

Comment:
**Missing `os.getcwd` / `os.path.exists` mocks — test fails in CI**

When neither `--config` is passed nor a `config/` directory exists in the cwd, the CLI falls back to `rails_config_path = api.app.rails_config_path` (the patched MagicMock). The subsequent `os.fspath(mock_app.rails_config_path)` call requires `__fspath__()` to return a `str` or `bytes`; a MagicMock auto-attribute satisfies neither, so `os.fspath` raises `TypeError`. The repo root has no `config/` directory, so `os.path.exists` returns `False` and the error path is always hit. `result.exit_code` will be `1` and the `assert result.exit_code == 0` will fail. The three newly-added worker/reject tests correctly use `@patch("os.getcwd")` + `@patch("os.path.exists")` for exactly this reason — the same fix is needed here.

How can I resolve this? If you propose a fix, please make it concise.

@Pouyanpi Pouyanpi force-pushed the develop branch 4 times, most recently from a6be550 to c69efe5 Compare May 6, 2026 16:01
Add os.getcwd / os.path.exists mocks to test_server_with_verbose_sets_worker_env so
it does not rely on MagicMock's __fspath__ fallback or the absence of a config/
directory in the working tree, matching the isolation pattern used by the other
worker-related tests.

Signed-off-by: Aditya Singh <adisin650@gmail.com>
@adityasingh2400
Copy link
Copy Markdown
Author

Addressed greptile review comment on the missing os.getcwd / os.path.exists mocks in test_server_with_verbose_sets_worker_env (commit 2492720). The test now follows the same isolation pattern as the other worker-related tests, so it no longer depends on MagicMock.__fspath__ behavior or the absence of a config/ directory in the working tree.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feature: Add --workers flag to nemoguardrails server for multi-worker uvicorn support

1 participant