Skip to content

fix(benchmarks): parameterize hardcoded ports across all 4 benchmarks#114

Merged
haroldship merged 8 commits into
mainfrom
fix/issue-17-hardcoded-ports
Jul 14, 2026
Merged

fix(benchmarks): parameterize hardcoded ports across all 4 benchmarks#114
haroldship merged 8 commits into
mainfrom
fix/issue-17-hardcoded-ports

Conversation

@haroldship

@haroldship haroldship commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Bug Fix Pull Request

Related Issue

Fixes #17
Related: #113 (follow-up, out of scope here)

Description

REGISTRY_PORT was hardcoded as a literal assignment in appworld/bpo/oak's eval.sh/compare.sh (m3 already had this fixed), and the real config_loader/loader.py (imported by every benchmark entrypoint) clobbered any pre-set env var with whatever a benchmark's .env file hardcoded. A live concurrent-run test also uncovered a hardcoded localhost:8001 registry call in appworld's default (cuga) evaluator paths. Together these blocked running any of the 4 benchmarks — or two copies of the same benchmark side by side — on custom ports via env vars.

Type of Changes

  • Bug fix (non-breaking change which fixes an issue)
  • Breaking change (fix that would cause existing functionality to not work as expected)

Root Cause

Three independent causes:

  1. appworld/bpo/oak's eval.sh/compare.sh hardcoded REGISTRY_PORT=8001 as a literal shell assignment (appworld additionally hardcoded its AppWorld env-server health check to port 8000), so setting REGISTRY_PORT / DYNACONF_SERVER_PORTS__REGISTRY before invoking these scripts had no effect. benchmarks/m3 already had the fix for this.
  2. config_loader/loader.py's load_eval_config() (the module every benchmark entrypoint actually imports via sys.path.insert(0, project_root); from config_loader import load_eval_confignot benchmarks/helpers/config_loader.py, a dead duplicate with zero real importers, now deleted) called load_dotenv(..., override=True) for both config/global.env and the benchmark's own .env, so a caller's pre-set env var (e.g. DYNACONF_SERVER_PORTS__APIS_URL before calling load_eval_config for appworld) got silently overwritten by whatever the .env file hardcoded (appworld.env hardcodes APIS_URL=9111). This was backwards from the bash-side convention in load_env.sh, which deliberately loads without override so pre-set env vars win.
  3. benchmarks/appworld/appworld_eval.py and eval_appworld_sdk.py both called requests.get("http://localhost:8001/api/reset") literally, ignoring REGISTRY_PORT entirely — found only by live-testing appworld's default cuga agent path with a non-default registry port, which crashed with ConnectionError.

Solution

  • Parameterized REGISTRY_PORT (and AppWorld's env-server port, via new APPWORLD_ENV_PORT) in appworld/bpo/oak's eval.sh/compare.sh, mirroring the existing m3 pattern: "${VAR:-${DYNACONF_SERVER_PORTS__X:-default}}", exported before any server starts.
  • Rewrote config_loader/loader.py's load_eval_config() to parse both .env files with dotenv_values() and merge them in-memory (benchmark file still overrides global.env, preserving existing layering like DYNACONF_ADVANCED_FEATURES__REGISTRY), then only os.environ.setdefault() keys not already set — so a caller's pre-set env var always wins over both files. Deleted the dead benchmarks/helpers/config_loader.py duplicate (and its unused re-export from benchmarks/helpers/__init__.py) that the original fix had mistakenly landed on.
  • Added a _get_registry_base_url() helper (env var first, settings.server_ports fallback, then the literal default — matching the pattern already correct in appworld_eval_react.py/appworld_eval_codeact.py) to appworld_eval.py and eval_appworld_sdk.py, replacing the hardcoded /api/reset call.

Not in scope: settings.server_ports.demo (cuga-agent's Gradio UI, default port 7860) was checked and is not hardcoded anywhere in this repo — it already passes through Dynaconf env vars untouched, no fix needed.

Deferred to #113: bpo/oak/appworld's app-specific ports are also hardcoded into MCP server YAML (bpo/mcp_servers/bpo.yaml, oak_health_insurance/oak_mcp_servers.yaml for FASTAPI_PORT; appworld/mcp_servers_appworld.yaml for apis_url, 12 occurrences) as the registry's OpenAPI URLs — making these configurable needs YAML templating, tracked separately to keep this PR focused and low-risk.

Testing

  • I have tested this fix locally
  • I have added tests that prove my fix works
  • All new and existing tests passed
  • I have verified the bug no longer occurs

Added tests/test_config_loader.py covering: caller-preset env vars win over both .env files, benchmark .env still overrides global.env when unset, global/benchmark-only keys are still applied, and a bare KEY-with-no-value .env line doesn't reach os.environ.setdefault(). Ran just ci (444 passed, 2 skipped [vendor not installed, pre-existing], bandit clean, no known vulnerabilities) and bash -n on every edited shell script.

Manual concurrency tests — two AppWorld tasks run concurrently (one per instance), started 10s apart, one instance on 4 custom ports (REGISTRY_PORT, DYNACONF_SERVER_PORTS__ENVIRONMENT_URL, DYNACONF_SERVER_PORTS__APIS_URL, DYNACONF_SERVER_PORTS__DEMO), the other on all defaults:

  1. --agent react baseline (before the config_loader/hardcoded-8001 fixes above were found): both instances started cleanly on fully independent ports (verified via lsof, no collisions), ran their full evaluation lifecycle, and exited 0. This test didn't exercise appworld_eval.py's registry call at all (react/codeact already resolve the registry port correctly), so it didn't catch bug fix(m3): repair harness bugs that artificially zeroed CUGA M3 pass rate #3 above.
  2. --sdk --agent cuga (the default agent, via the SDK evaluator): this run is what originally surfaced bugs Solve conflict between .env and command line #2 and fix(m3): repair harness bugs that artificially zeroed CUGA M3 pass rate #3 — the custom-port instance crashed with ConnectionError: ... port 8001 ... Connection refused even though its registry was healthy on port 18001, and apis_url silently stuck at the hardcoded 9111 instead of the requested override. After fixing both, re-ran the identical test: both instances again started on fully independent ports with no collisions or connection errors, ran to completion, and exited 0. (Task-level: both AppWorld tasks themselves didn't pass — one on keyword-match, one hit an external Groq rate-limit from running two concurrent LLM calls — both unrelated to the port fix; harness/infra-level success, which is what this test targets, was confirmed for all 4 ports.)

A pre-existing, unrelated gap was also observed during testing: appworld/m3/oak's registry cleanup only kills the tracked wrapper PID, not the uv run --no-sync registry subprocess it spawns, occasionally leaving an orphaned registry process bound to the port after a run (bpo/eval.sh already has a pkill -f fallback for this that the other three lack). Not fixed here — out of scope for port configurability — but worth a follow-up issue.

Checklist

  • My code follows the code style of this project
  • I have performed a self-review of my own code
  • I have made corresponding changes to the documentation if needed

Summary by CodeRabbit

  • Bug Fixes
    • Made benchmark runs use the configured server ports instead of hard-coded values, improving consistency across setup, execution, and cleanup.
    • Updated shutdown behavior so cleanup targets the correct running processes for each benchmark.
    • Ensured environment settings are loaded with the right precedence, preserving existing values while filling in missing ones.
  • Tests
    • Added regression coverage for environment loading to verify existing variables are not overwritten and file-based values are applied correctly.

- REGISTRY_PORT was a literal `REGISTRY_PORT=8001` in eval.sh, ignoring
  any pre-set env var; now resolved as
  ${REGISTRY_PORT:-${DYNACONF_SERVER_PORTS__REGISTRY:-8001}}, matching
  the pattern already used by benchmarks/m3.
- eval.sh's AppWorld health check was hardcoded to poll port 8000
  regardless of DYNACONF_SERVER_PORTS__ENVIRONMENT_URL, so a custom
  environment_url port would start fine but time out the check. Added
  APPWORLD_ENV_PORT, resolved the same way and exported before `cuga
  start appworld` runs.
- compare.sh now sources load_env.sh and resolves both ports the same
  way so its kill_port_processes cleanup targets the right ports.

Part of #17.
REGISTRY_PORT was a literal `REGISTRY_PORT=8001` in eval.sh, ignoring
any pre-set env var. Now resolved as
${REGISTRY_PORT:-${DYNACONF_SERVER_PORTS__REGISTRY:-8001}} after
load_env.sh, matching the pattern already used by benchmarks/m3.
compare.sh now sources load_env.sh and resolves REGISTRY_PORT the same
way so its kill_port_processes cleanup targets the right port.

FASTAPI_PORT (bpo's own mock app, 8095) is left hardcoded — it's also
baked into benchmarks/bpo/mcp_servers/bpo.yaml as the registry's
OpenAPI URL, so making it configurable needs YAML templating too;
tracked separately in #113.

Part of #17.
REGISTRY_PORT was a literal `REGISTRY_PORT=8001` in eval.sh, ignoring
any pre-set env var. Now resolved as
${REGISTRY_PORT:-${DYNACONF_SERVER_PORTS__REGISTRY:-8001}} after
load_env.sh, matching the pattern already used by benchmarks/m3.
compare.sh now sources load_env.sh and resolves REGISTRY_PORT the same
way so its kill_port_processes cleanup targets the right port.

FASTAPI_PORT (oak's own mock app, 8090) is left hardcoded — it's also
baked into benchmarks/oak_health_insurance/oak_mcp_servers.yaml as the
registry's OpenAPI URL, so making it configurable needs YAML
templating too; tracked separately in #113.

Part of #17.
load_eval_config() called load_dotenv(..., override=True) for both
config/global.env and the benchmark's own .env file, so any env var the
caller pre-set (e.g. DYNACONF_SERVER_PORTS__APIS_URL before calling
load_eval_config for appworld) got silently overwritten by whatever the
.env file hardcoded (appworld.env hardcodes APIS_URL=9111). This was
backwards from load_env.sh's bash-side convention, which deliberately
loads without override so pre-set env vars win.

Switched to parsing both files with dotenv_values() and merging them
in-memory (benchmark file still overrides global.env), then only
os.environ.setdefault()-ing keys that aren't already set — so a
caller's pre-set env var always wins over both files, while the
global->benchmark override layering other settings rely on
(DYNACONF_ADVANCED_FEATURES__REGISTRY differs between global.env and
every benchmark's .env) is preserved.

Closes #17.
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@haroldship, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 15 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: baaa590a-0120-44f5-99c5-5789d9e6f437

📥 Commits

Reviewing files that changed from the base of the PR and between 689ab6b and b151df3.

📒 Files selected for processing (6)
  • benchmarks/appworld/appworld_eval.py
  • benchmarks/appworld/eval_appworld_sdk.py
  • benchmarks/helpers/__init__.py
  • benchmarks/helpers/config_loader.py
  • config_loader/loader.py
  • tests/test_config_loader.py
📝 Walkthrough

Walkthrough

Benchmark eval.sh and compare.sh scripts for appworld, bpo, and oak_health_insurance now derive REGISTRY_PORT (and AppWorld's environment server port) from environment variables after loading configuration, instead of hardcoding them, and export corresponding DYNACONF_SERVER_PORTS__* values used by cleanup/readiness logic. Additionally, config_loader.py's load_eval_config no longer overrides pre-set environment variables when loading .env files, with new regression tests covering precedence behavior.

Changes

Configurable Server Ports

Layer / File(s) Summary
AppWorld port resolution and cleanup
benchmarks/appworld/eval.sh, benchmarks/appworld/compare.sh
Removes hardcoded REGISTRY_PORT/env-server port, resolves them from env vars post-load, exports DYNACONF_SERVER_PORTS__REGISTRY/ENVIRONMENT_URL, and uses resolved ports in readiness checks and cleanup.
BPO port resolution and cleanup
benchmarks/bpo/eval.sh, benchmarks/bpo/compare.sh
Defers REGISTRY_PORT resolution until after env loading, exports the resolved value and DYNACONF_SERVER_PORTS__REGISTRY, and updates cleanup to kill the resolved registry port.
Oak Health Insurance port resolution and cleanup
benchmarks/oak_health_insurance/eval.sh, benchmarks/oak_health_insurance/compare.sh
Removes hardcoded REGISTRY_PORT, resolves it after env loading, exports it and DYNACONF_SERVER_PORTS__REGISTRY, and updates cleanup to use the resolved port.
Non-overriding config loading
benchmarks/helpers/config_loader.py, benchmarks/helpers/tests/test_config_loader.py
Replaces override-based dotenv loading with setdefault-based merging of global and benchmark .env files so pre-set environment variables take precedence; adds regression tests for precedence scenarios.

Estimated code review effort: 2 (Simple) | ~15 minutes

Suggested reviewers: sami-marreed, Sergey-Zeltyn

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The scripts and config loader now honor env/Dynaconf overrides, matching the issue’s required port and env precedence fixes.
Out of Scope Changes check ✅ Passed The changes stay within the requested benchmark port parameterization and config-loading behavior, with tests supporting the fix.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: replacing hardcoded benchmark ports with configurable environment-driven ports.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-17-hardcoded-ports

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
benchmarks/helpers/config_loader.py (1)

46-47: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding a test for the None-value edge case.

dotenv_values returns None for keys present in the file but without a value (e.g., a bare KEY line). The if value is not None guard correctly prevents os.environ.setdefault(key, None) from raising a TypeError. However, no test currently pins this behavior — a future refactor could accidentally drop the guard.

🧪 Suggested test
def test_none_value_from_dotenv_is_skipped(monkeypatch):
    def _fake_dotenv_with_none(path):
        if path.name == "global.env":
            return {}
        return {"EMPTY_KEY": None}

    monkeypatch.setattr(config_loader, "dotenv_values", _fake_dotenv_with_none)
    monkeypatch.delenv("EMPTY_KEY", raising=False)
    config_loader.load_eval_config("fake_benchmark")
    assert "EMPTY_KEY" not in os.environ
🤖 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 `@benchmarks/helpers/config_loader.py` around lines 46 - 47, Add a regression
test for the `None`-value path in `load_eval_config` in `config_loader.py`: mock
`dotenv_values` so one merged entry returns `{"EMPTY_KEY": None}`, clear
`EMPTY_KEY` from `os.environ`, call `load_eval_config`, and assert the key is
not added. This pins the `if value is not None` guard in the `merged_values`
loop and protects against future refactors that might pass `None` into
`os.environ.setdefault`.
🤖 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.

Nitpick comments:
In `@benchmarks/helpers/config_loader.py`:
- Around line 46-47: Add a regression test for the `None`-value path in
`load_eval_config` in `config_loader.py`: mock `dotenv_values` so one merged
entry returns `{"EMPTY_KEY": None}`, clear `EMPTY_KEY` from `os.environ`, call
`load_eval_config`, and assert the key is not added. This pins the `if value is
not None` guard in the `merged_values` loop and protects against future
refactors that might pass `None` into `os.environ.setdefault`.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 662c7e5f-5c92-4ec6-bac7-e7fe55611e32

📥 Commits

Reviewing files that changed from the base of the PR and between 119b091 and 689ab6b.

📒 Files selected for processing (8)
  • benchmarks/appworld/compare.sh
  • benchmarks/appworld/eval.sh
  • benchmarks/bpo/compare.sh
  • benchmarks/bpo/eval.sh
  • benchmarks/helpers/config_loader.py
  • benchmarks/helpers/tests/test_config_loader.py
  • benchmarks/oak_health_insurance/compare.sh
  • benchmarks/oak_health_insurance/eval.sh

…oader

Addresses CodeRabbit's docstring-coverage pre-merge check and nitpick
on PR #114: pins the `if value is not None` guard in
config_loader.load_eval_config so a bare `KEY` line in a .env file
(dotenv_values -> None) can't reach os.environ.setdefault().
The earlier fix for issue #17 (override=True clobbering pre-set env
vars) landed on benchmarks/helpers/config_loader.py, but that module
had zero real importers other than a dead eager re-export in
benchmarks/helpers/__init__.py. Every benchmark entrypoint actually
imports load_eval_config from a separate root-level config_loader
package (sys.path.insert(0, project_root); from config_loader import
load_eval_config), which still had the original bug — confirmed live
when a concurrent appworld test with DYNACONF_SERVER_PORTS__APIS_URL
overridden still resolved to the hardcoded appworld.env value.

- Deleted the dead benchmarks/helpers/config_loader.py + its test, and
  the unused load_eval_config re-export from benchmarks/helpers/__init__.py.
- Applied the same dotenv_values()-merge-then-setdefault fix to the
  real config_loader/loader.py, moving the regression test to
  tests/test_config_loader.py.

Closes #17 (for real this time).
…8001

appworld_eval.py and eval_appworld_sdk.py both called
requests.get("http://localhost:8001/api/reset") literally, ignoring
REGISTRY_PORT/DYNACONF_SERVER_PORTS__REGISTRY entirely. Confirmed live:
running --agent cuga with REGISTRY_PORT=18001 crashed with
ConnectionError (port 8001 refused) even though the registry was up
and healthy on 18001.

Added the same _get_registry_base_url() helper already used correctly
by appworld_eval_react.py/appworld_eval_codeact.py (env var first,
settings.server_ports fallback, then the literal default) to both
files and used it for the reset call.

Part of #17.
@haroldship
haroldship requested a review from AnkitaNaik July 9, 2026 15:27

@AnkitaNaik AnkitaNaik left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

No additional comments for the PR. The PR is good to merge.

@haroldship
haroldship merged commit 2602266 into main Jul 14, 2026
4 checks passed
@haroldship
haroldship deleted the fix/issue-17-hardcoded-ports branch July 14, 2026 12:46
haroldship added a commit that referenced this pull request Jul 16, 2026
… setting in global.env (#126)

fix(config): stop global.env from silently overriding every benchmark's registry setting

config/global.env has set DYNACONF_ADVANCED_FEATURES__REGISTRY=false since
this repo's first commit, contradicting every benchmark's own .env (m3,
appworld, bpo, oak_health_insurance all set it to true). This was masked
for months by config_loader/loader.py's override=True behavior, which let
the benchmark-specific file always win. #114 changed that loader to
os.environ.setdefault (for an unrelated port-override use case), which
now respects whatever the shell already exported - and load_env.sh's own
no-override, global-loads-first order has always resolved that value to
false. Net effect: every benchmark silently gets zero tools.

- Remove the DYNACONF_ADVANCED_FEATURES__REGISTRY=false line and its
  stale "tools loaded directly from containers" comment (that direct-load
  mechanism doesn't exist anywhere in this repo's history)
- No replacement default is set here on purpose: each benchmark's own
  .env already declares the value it wants

Fixes #124
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.

[Bug]: Hardcoded server ports (REGISTRY_PORT, APIS_URL override) block running benchmarks on custom ports

2 participants