From 14a2ca144200fb66ea8efcc3d140ed6153cf8fe5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A2=A8=E8=8F=8A?= <277378677+maiphucgiang@users.noreply.github.com> Date: Wed, 16 Sep 2026 11:27:29 +0800 Subject: [PATCH] Honor native bind settings and forward runtime environment options --- .env.example | 45 +++++---- app/runtime_management.py | 8 +- app/settings.py | 4 +- converter.py | 6 +- docker-compose.yml | 7 ++ docs/deployment.md | 9 +- docs/deployment.zh-CN.md | 9 +- tests/test_environment_config.py | 154 +++++++++++++++++++++++++++++++ 8 files changed, 212 insertions(+), 30 deletions(-) create mode 100644 tests/test_environment_config.py diff --git a/.env.example b/.env.example index b4661c9..af30ada 100644 --- a/.env.example +++ b/.env.example @@ -1,42 +1,53 @@ # Copy to .env for first use; preserve existing settings when updating. -# Compose reads .env automatically; local launches use uv run --env-file .env. +# Compose loads .env automatically; native launches use uv run --env-file .env. -# Compose builds local source by default; set a versioned image name to use a release. -CODEBUDDY2API_IMAGE=codebuddy2api:local +# Native listener and Compose host mapping; explicit --host/--port override native values. CODEBUDDY2API_BIND=127.0.0.1 CODEBUDDY2API_PORT=8787 + +# Compose-only image and host data directory; container data stays in /data/auth. +CODEBUDDY2API_IMAGE=codebuddy2api:local CODEBUDDY2API_AUTH_PATH=./auth -# An empty key permits legacy inference but locks management; set a random key before public binding. +# Optional native data directory; Compose fixes CODEBUDDY_AUTH_DIR to /data/auth. +# CODEBUDDY_AUTH_DIR=./auth +# Import directory defaults to the data directory's imports/; use container paths in Compose. +# CODEBUDDY_IMPORT_DIR=./auth/imports + +# Empty locks management; set a random key before exposing inference beyond loopback. CODEBUDDY2API_KEY= +# Native-only unsafe opt-in; Compose uses its host mapping to control exposure. +# CODEBUDDY2API_ALLOW_OPEN_NOAUTH=false -# Disable Origin/CSRF checks only in trusted local environments; authentication remains required. +# Disabling CSRF checks does not disable authentication; use only on trusted local networks. CODEBUDDY2API_ADMIN_CSRF=true -# Unset uses the WebUI value; an explicit Boolean locks tool-metadata retention. +# Unset leaves tool metadata configurable in the WebUI; an explicit Boolean locks it. # CODEBUDDY2API_KEEP_TOOL_METADATA=true # Image overflow keeps the newest blocks or returns 413; zero forbids all images. CODEBUDDY2API_MAX_IMAGES=16 CODEBUDDY2API_IMAGE_POLICY=truncate - # Positive upstream JSON byte limit after image policy and protocol adaptation. CODEBUDDY2API_MAX_REQUEST_BYTES=33554432 +# Positive raw inbound byte limit, enforced before JSON parsing. +CODEBUDDY2API_MAX_INBOUND_BYTES=67108864 +# Aggregated output bytes and concurrent inference requests; zero disables each limit. +CODEBUDDY2API_MAX_COLLECT_BYTES=8388608 +CODEBUDDY2API_MAX_CONCURRENT=64 -# SQLite audit storage defaults to auth/logs.sqlite3; configure retention in the WebUI. -# This optional setting controls the separate legacy text log. +# SQLite audit defaults to the data directory; this optional path enables a separate text log. CODEBUDDY2API_LOG= -# Bound text previews and redact common credentials and images; zero logs metadata only. +# Redacted text preview bytes; zero logs metadata only. CODEBUDDY2API_LOG_BODY_LIMIT=65536 -# One-time trial credits are claimed manually from the WebUI. - -# Preauthorize first-Buddy tasks, one bounded WorkBuddy chat (may use credits), adoption and travel. +# International trial credits remain manual-only in the WebUI. +# Preauthorize first-Buddy tasks, one bounded chat (may use credits), adoption and travel. CODEBUDDY2API_AUTO_ACCEPT_BUDDY=false -# Maximum credential failovers before response bytes are sent; zero disables replay. -# Some gateway failures may already be billed; see docs/advanced.md for replay boundaries. +# Extra generations for malformed tool arguments; each may consume credits, zero disables retries. +CODEBUDDY2API_TOOL_CALL_MAX_RETRY=3 +# Pre-response credential failovers; zero disables replay. See docs/advanced.md for billing risks. CODEBUDDY2API_FAILOVER_MAX=0 - -# Opt in to replaying incomplete writes only if partial requests cannot be billed upstream. +# Replay incomplete writes only when partial requests cannot be billed upstream. CODEBUDDY2API_RETRY_WRITE_TIMEOUT=false diff --git a/app/runtime_management.py b/app/runtime_management.py index 34b5cd6..0eb6b2e 100644 --- a/app/runtime_management.py +++ b/app/runtime_management.py @@ -59,7 +59,7 @@ def close(self): pass -def initialize(gateway, args, argv=None): +def initialize(gateway, args, argv=None, *, parser=None): config = gateway.CONFIG config["auto_accept_buddy"] = buddy.auto_accept_from_env(os.environ) config["auto_accept_buddy_source"] = "environment" if "CODEBUDDY2API_AUTO_ACCEPT_BUDDY" in os.environ else "default" @@ -70,9 +70,13 @@ def initialize(gateway, args, argv=None): config["model_guard"] = not args.no_model_guard aliases = {"log": "log_path", "no_model_guard": "model_guard"} explicit = set() + options = parser._option_string_actions if parser is not None else {} for argument in (sys.argv[1:] if argv is None else argv): if argument.startswith("--"): - key = argument[2:].split("=", 1)[0].replace("-", "_") + flag = argument.split("=", 1)[0] + matches = [flag] if flag in options else [name for name in options if name.startswith(flag)] + # Resolve argparse's accepted abbreviations before assigning precedence. + key = options[matches[0]].dest if len(matches) == 1 else flag[2:].replace("-", "_") explicit.add(aliases.get(key, key)) apply_persisted_settings(config, explicit=explicit) for key in SCHEMA: diff --git a/app/settings.py b/app/settings.py index f6edfa9..c180bfb 100644 --- a/app/settings.py +++ b/app/settings.py @@ -19,8 +19,8 @@ def _item(default, type_, label, *, mode="hot", env=None, minimum=None, maximum= SCHEMA = { - "host": _item("127.0.0.1", "string", "监听地址", mode="restart"), - "port": _item(8787, "integer", "监听端口", mode="restart", minimum=1, maximum=65535), + "host": _item("127.0.0.1", "string", "监听地址", mode="restart", env="CODEBUDDY2API_BIND"), + "port": _item(8787, "integer", "监听端口", mode="restart", env="CODEBUDDY2API_PORT", minimum=1, maximum=65535), "api_key": _item(None, "secret", "管理与推理密钥", mode="startup", env="CODEBUDDY2API_KEY", sensitive=True), "auth_file": _item(None, "paths", "显式凭证文件", mode="startup", sensitive=True), "auth_dir": _item(None, "path", "凭证目录", mode="startup", env="CODEBUDDY_AUTH_DIR", sensitive=True), diff --git a/converter.py b/converter.py index 2a3d4d4..2dc7deb 100644 --- a/converter.py +++ b/converter.py @@ -3326,8 +3326,8 @@ def main(): help="login 站点:cn 国内站(默认);intl 国际 WorkBuddy;intl-codebuddy 国际 CodeBuddy") ap.add_argument("--no-browser", action="store_true", help="login 仅显示授权链接,不自动打开浏览器(服务器/容器环境)") - ap.add_argument("--host", default="127.0.0.1") - ap.add_argument("--port", type=int, default=8787) + ap.add_argument("--host", default="127.0.0.1", help="监听地址;覆盖 CODEBUDDY2API_BIND") + ap.add_argument("--port", type=int, default=8787, help="监听端口;覆盖 CODEBUDDY2API_PORT") ap.add_argument("--api-key", default=os.environ.get("CODEBUDDY2API_KEY", ""), help="可选:要求客户端携带的 API key(默认不校验)") ap.add_argument("--admin-csrf", type=_boolean_arg, nargs="?", const=True, @@ -3416,7 +3416,7 @@ def main(): # File logging is enabled only when a path is configured. CONFIG["log_path"] = args.log if args.log else os.environ.get("CODEBUDDY2API_LOG") from app import runtime_management - runtime_management.initialize(sys.modules[__name__], args) + runtime_management.initialize(sys.modules[__name__], args, parser=ap) # Validate effective binding and authentication before credential scans or background work. if (args.host not in ("127.0.0.1", "::1", "localhost") and not CONFIG.get("api_key") and os.environ.get("CODEBUDDY2API_ALLOW_OPEN_NOAUTH", "").lower() not in ("1", "true", "yes")): diff --git a/docker-compose.yml b/docker-compose.yml index 03d9e2c..95a7fe5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -24,6 +24,13 @@ services: CODEBUDDY2API_MAX_REQUEST_BYTES: ${CODEBUDDY2API_MAX_REQUEST_BYTES:-33554432} CODEBUDDY2API_LOG_BODY_LIMIT: ${CODEBUDDY2API_LOG_BODY_LIMIT:-65536} CODEBUDDY2API_LOG: ${CODEBUDDY2API_LOG:-} + CODEBUDDY2API_MAX_INBOUND_BYTES: ${CODEBUDDY2API_MAX_INBOUND_BYTES:-67108864} + CODEBUDDY2API_MAX_COLLECT_BYTES: ${CODEBUDDY2API_MAX_COLLECT_BYTES:-8388608} + CODEBUDDY2API_MAX_CONCURRENT: ${CODEBUDDY2API_MAX_CONCURRENT:-64} + CODEBUDDY2API_TOOL_CALL_MAX_RETRY: ${CODEBUDDY2API_TOOL_CALL_MAX_RETRY:-3} + CODEBUDDY2API_FAILOVER_MAX: + CODEBUDDY2API_RETRY_WRITE_TIMEOUT: + CODEBUDDY_IMPORT_DIR: ${CODEBUDDY_IMPORT_DIR:-/data/auth/imports} # Bind container interfaces; host port mapping controls external access. CODEBUDDY2API_ALLOW_OPEN_NOAUTH: "true" CODEBUDDY_AUTH_DIR: /data/auth diff --git a/docs/deployment.md b/docs/deployment.md index 38831e0..5c80641 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -11,11 +11,14 @@ On first setup, run `cp .env.example .env` and set `CODEBUDDY2API_KEY` to your o | Setting | Purpose | |---------|---------| | `CODEBUDDY2API_IMAGE` | Compose image; the template uses `codebuddy2api:local` | -| `CODEBUDDY2API_BIND` / `CODEBUDDY2API_PORT` | Compose host binding / port; the template uses `127.0.0.1:8787` | +| `CODEBUDDY2API_BIND` / `CODEBUDDY2API_PORT` | Native listener and Compose host mapping; default `127.0.0.1:8787` | | `CODEBUDDY2API_AUTH_PATH` | Compose host data directory; defaults to `./auth`, mounted at `/data/auth` | | `CODEBUDDY_AUTH_DIR` | Local Python data directory; defaults to the repository's `auth/`. Compose sets it to `/data/auth` inside the container | +| `CODEBUDDY_IMPORT_DIR` | Optional import directory; defaults to `imports/` under the data directory. Use container paths with Compose | -Compose reads declared variables from `.env`; shell variables take precedence. Do not skip the template: without `.env`, compatibility defaults may expose all host interfaces. Set a random key, HTTPS and access restrictions before allowing remote connections. +The example lists all active runtime variables, including inbound/aggregate byte limits, concurrency, tool retries and failover. Compose forwards these limits; unset optional tool-metadata/failover settings remain configurable in the WebUI. Zero disables the aggregate/concurrency limit or extra retries, not the required positive inbound limit. + +Compose reads declared variables from `.env`; shell variables take precedence. The default host mapping is loopback. Set a random key, HTTPS and access restrictions before allowing remote connections. Container binding remains `0.0.0.0:8787`; change host exposure using `BIND/PORT`, not container listener arguments. Mount the entire data directory on writable local storage, not just one SQLite file, and do not share it between instances. Stop the gateway and back up the whole directory before upgrading; see [data and backups](webui.md). @@ -57,7 +60,7 @@ Configure `.env` as above before starting, then open `/dashboard` to add account Without uv, run `python3 -m venv .venv`, activate it, install dependencies with `pip install --require-hashes --only-binary=:all: -r requirements.txt`, and start with `python3 converter.py --desensitize`. **Plain Python does not load `.env`**; export environment variables or pass CLI flags explicitly. -Local Python binding uses `--host` and `--port`. Compose-only `CODEBUDDY2API_BIND`, `CODEBUDDY2API_PORT` and `CODEBUDDY2API_AUTH_PATH` do not change the local listener or data directory. +Native binding follows explicit `--host/--port` > `CODEBUDDY2API_BIND/PORT` > saved WebUI values > defaults. Remove explicit flags if `.env` should control the listener; changes require restart. `CODEBUDDY2API_IMAGE/AUTH_PATH` remain Compose-only; use `CODEBUDDY_AUTH_DIR` for native data. ## Dependency locks diff --git a/docs/deployment.zh-CN.md b/docs/deployment.zh-CN.md index 8346cdc..eceb6db 100644 --- a/docs/deployment.zh-CN.md +++ b/docs/deployment.zh-CN.md @@ -11,11 +11,14 @@ | 设置 | 用途 | |------|------| | `CODEBUDDY2API_IMAGE` | Compose 镜像;模板为 `codebuddy2api:local` | -| `CODEBUDDY2API_BIND` / `CODEBUDDY2API_PORT` | Compose 的宿主机监听地址 / 端口;模板为 `127.0.0.1:8787` | +| `CODEBUDDY2API_BIND` / `CODEBUDDY2API_PORT` | 本地监听及 Compose 宿主机映射;默认 `127.0.0.1:8787` | | `CODEBUDDY2API_AUTH_PATH` | Compose 宿主机数据目录,默认 `./auth`,挂载至容器 `/data/auth` | | `CODEBUDDY_AUTH_DIR` | 本地 Python 的数据目录,默认仓库下 `auth/`;Compose 容器内固定为 `/data/auth` | +| `CODEBUDDY_IMPORT_DIR` | 可选导入目录,默认数据目录下 `imports/`;Compose 中使用容器路径 | -Compose 自动读取 `.env` 中已声明的变量,Shell 环境优先。不要省略模板配置:Compose 在缺少 `.env` 时为兼容旧部署可能监听全部网卡。对外访问前设置随机 key、HTTPS 和访问限制。 +示例列出当前运行变量,包括入站/聚合字节限制、并发数、工具重试及换号重放。Compose 转发这些限制;未设置的可选工具描述/重放项仍由 WebUI 配置。聚合、并发及额外重试可设为 0 关闭,入站限制必须为正整数。 + +Compose 读取已声明的 `.env` 变量,Shell 环境优先;默认仅映射回环地址。对外访问前设置随机 key、HTTPS 和访问限制。容器内始终监听 `0.0.0.0:8787`,通过 `BIND/PORT` 改宿主机入口,不改容器监听参数。 整个数据目录必须可写,且应位于本地文件系统;不要只挂载一个 SQLite 文件,也不要让多个实例共用目录。升级前停止服务并备份整个目录,详见 [数据与备份](webui.zh-CN.md)。 @@ -57,7 +60,7 @@ uv run --locked --no-build --env-file .env converter.py --desensitize 不使用 uv 时,可执行 `python3 -m venv .venv`,激活环境后用 `pip install --require-hashes --only-binary=:all: -r requirements.txt` 安装依赖,将运行命令换为 `python3 converter.py --desensitize`。**普通 Python 不自动读取 `.env`**,须显式导出环境变量或传入 CLI 参数。 -本地 Python 的监听地址和端口由 `--host`、`--port` 控制;Compose 专用的 `CODEBUDDY2API_BIND`、`CODEBUDDY2API_PORT`、`CODEBUDDY2API_AUTH_PATH` 不改变本地监听和数据目录。 +本地监听优先级为显式 `--host/--port` > `CODEBUDDY2API_BIND/PORT` > WebUI 保存值 > 默认值。希望由 `.env` 控制时应移除显式监听参数,修改后重启。`CODEBUDDY2API_IMAGE/AUTH_PATH` 仍仅用于 Compose,本地数据目录使用 `CODEBUDDY_AUTH_DIR`。 ## 依赖锁定 diff --git a/tests/test_environment_config.py b/tests/test_environment_config.py new file mode 100644 index 0000000..f970b05 --- /dev/null +++ b/tests/test_environment_config.py @@ -0,0 +1,154 @@ +"""Verify native and Compose environment settings without credentials or upstream requests.""" +import sys +from pathlib import Path +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +import ast +import contextlib +import io +import json +import os +import re +import shutil +import subprocess +import tempfile +import unittest +from unittest.mock import patch + +import converter +from app import runtime_management +from app.control_store import ControlStore +from app.settings import resolve_settings + +ROOT = Path(__file__).resolve().parents[1] + + +class EnvironmentConfigTests(unittest.TestCase): + def setUp(self): + self.root = Path(self.enterContext(tempfile.TemporaryDirectory())) + self.auth = self.root / 'auth' + + def start(self, environ=None, cli=(), saved=None): + if saved: + with contextlib.closing(ControlStore(self.auth / 'control.sqlite3')) as store: + store.update_settings(saved, store.snapshot()['revision']) + env = {'HOME': str(self.root), 'CODEBUDDY_AUTH_DIR': str(self.auth), 'CODEBUDDY2API_KEY': 'synthetic-key'} + env.update(environ or {}) + with patch.dict(os.environ, env, clear=True), patch.dict(converter.CONFIG, dict(converter.CONFIG), clear=True), \ + patch.object(sys, 'argv', ['converter.py', '--skip-check', *cli]), \ + patch.object(converter, 'seed_credentials') as seed, patch.object(converter, 'CredentialPool') as pool, \ + patch.object(converter, '_publish_model_cache'), patch.object(runtime_management, 'install'), \ + patch.object(converter.threading, 'Thread'), patch.object(converter, '_log'), \ + patch.object(converter.uvicorn, 'run') as server, contextlib.redirect_stderr(io.StringIO()): + try: + converter.main() + snapshot = {item['key']: item for item in resolve_settings(converter.CONFIG)} + return dict(server.call_args.kwargs), snapshot, dict(converter.CONFIG) + except (ValueError, SystemExit): + server.assert_not_called() + seed.assert_not_called() + pool.assert_not_called() + raise + finally: + runtime_management.close(converter.CONFIG) + + def test_environment_bind_and_port_override_saved_values_and_are_locked(self): + server, items, _ = self.start({'CODEBUDDY2API_BIND': '127.0.0.2', 'CODEBUDDY2API_PORT': '9081'}, + saved={'host': '127.0.0.3', 'port': 9082}) + self.assertEqual((server['host'], server['port']), ('127.0.0.2', 9081)) + for name in ('host', 'port'): + self.assertEqual(items[name]['source'], 'environment') + self.assertTrue(items[name]['locked']) + self.assertEqual(items[name]['mode'], 'restart') + + def test_cli_binding_wins_even_over_invalid_lower_priority_environment(self): + server, items, _ = self.start({'CODEBUDDY2API_BIND': '', 'CODEBUDDY2API_PORT': 'invalid'}, + cli=('--host=127.0.0.4', '--port', '9084')) + self.assertEqual((server['host'], server['port']), ('127.0.0.4', 9084)) + self.assertTrue(all(items[name]['source'] == 'cli' and items[name]['locked'] for name in ('host', 'port'))) + + def test_accepted_cli_abbreviations_keep_cli_precedence(self): + server, items, _ = self.start({'CODEBUDDY2API_BIND': '127.0.0.2', 'CODEBUDDY2API_PORT': '9081'}, + cli=('--ho', '127.0.0.4', '--po=9084')) + self.assertEqual((server['host'], server['port']), ('127.0.0.4', 9084)) + self.assertTrue(all(items[name]['source'] == 'cli' for name in ('host', 'port'))) + + + def test_saved_binding_and_default_loopback_still_work_without_environment(self): + server, items, _ = self.start() + self.assertEqual((server['host'], server['port']), ('127.0.0.1', 8787)) + self.assertFalse(items['host']['locked']) + server, items, _ = self.start(saved={'host': '127.0.0.5', 'port': 9085}) + self.assertEqual((server['host'], server['port']), ('127.0.0.5', 9085)) + self.assertEqual(items['port']['source'], 'management') + + def test_invalid_binding_environment_fails_before_server_or_credential_scan(self): + for env in ({'CODEBUDDY2API_BIND': ''}, {'CODEBUDDY2API_BIND': 'x\ny'}, + {'CODEBUDDY2API_PORT': '0'}, {'CODEBUDDY2API_PORT': '65536'}, {'CODEBUDDY2API_PORT': 'bad'}): + with self.subTest(env=env), self.assertRaises(ValueError): + self.start(env) + + def test_environment_public_binding_without_key_is_rejected(self): + with self.assertRaises(SystemExit): + self.start({'CODEBUDDY2API_BIND': '0.0.0.0', 'CODEBUDDY2API_KEY': ''}) + + def test_existing_runtime_limit_and_retry_variables_reach_effective_config(self): + values = {'MAX_INBOUND_BYTES': '4096', 'MAX_COLLECT_BYTES': '0', 'MAX_CONCURRENT': '2', + 'TOOL_CALL_MAX_RETRY': '1', 'FAILOVER_MAX': '1', 'RETRY_WRITE_TIMEOUT': 'true'} + _, _, config = self.start({'CODEBUDDY2API_' + name: value for name, value in values.items()}) + for name, value in values.items(): + expected = value == 'true' if value in ('true', 'false') else int(value) + self.assertEqual(config[name.lower()], expected) + + def test_example_covers_all_active_runtime_environment_names(self): + example = (ROOT / '.env.example').read_text() + documented = set(re.findall(r'(?m)^(?:# )?(CODEBUDDY[A-Z0-9_]+)=', example)) + referenced = set() + for path in [ROOT / 'converter.py', *sorted((ROOT / 'app').rglob('*.py'))]: + for node in ast.walk(ast.parse(path.read_text())): + if isinstance(node, ast.Constant) and isinstance(node.value, str) and re.fullmatch('CODEBUDDY[A-Z0-9_]+', node.value): + referenced.add(node.value) + referenced.discard('CODEBUDDY2API_AUTO_TRIAL') + self.assertLessEqual(referenced, documented) + self.assertNotIn('CODEBUDDY2API_AUTO_TRIAL', documented) + + def compose(self, values): + if shutil.which('docker') is None: + self.skipTest('Docker CLI unavailable') + env = {'PATH': os.environ.get('PATH', os.defpath), 'HOME': str(self.root)} + version = subprocess.run(['docker', 'compose', 'version'], env=env, capture_output=True, text=True, timeout=15) + if version.returncode: + self.skipTest('Compose plugin unavailable') + dotenv = self.root / 'compose.env' + dotenv.write_text(''.join(name + '=' + value + '\n' for name, value in values.items())) + command = ['docker', 'compose', '--project-directory', str(self.root), '--env-file', str(dotenv), + '-f', str(ROOT / 'docker-compose.yml'), 'config', '--format', 'json'] + result = subprocess.run(command, env=env, capture_output=True, text=True, timeout=20) + self.assertEqual(result.returncode, 0, result.stderr) + return json.loads(result.stdout)['services']['codebuddy2api'] + + def test_compose_forwards_dotenv_limits_and_retries_without_changing_internal_binding(self): + values = {'CODEBUDDY2API_BIND': '127.0.0.2', 'CODEBUDDY2API_PORT': '9087', + 'CODEBUDDY2API_MAX_INBOUND_BYTES': '4096', 'CODEBUDDY2API_MAX_COLLECT_BYTES': '0', + 'CODEBUDDY2API_MAX_CONCURRENT': '2', 'CODEBUDDY2API_TOOL_CALL_MAX_RETRY': '1', + 'CODEBUDDY2API_FAILOVER_MAX': '1', 'CODEBUDDY2API_RETRY_WRITE_TIMEOUT': 'true', + 'CODEBUDDY2API_KEEP_TOOL_METADATA': 'false', 'CODEBUDDY_IMPORT_DIR': '/data/auth/incoming'} + service = self.compose(values) + port = service['ports'][0] + self.assertEqual((port['host_ip'], str(port['published']), port['target']), ('127.0.0.2', '9087', 8787)) + self.assertEqual(service['command'][:5], ['python3', 'converter.py', '--host', '0.0.0.0', '--port']) + self.assertEqual(service['environment']['CODEBUDDY_AUTH_DIR'], '/data/auth') + for name, value in values.items(): + if name not in ('CODEBUDDY2API_BIND', 'CODEBUDDY2API_PORT'): + self.assertEqual(service['environment'][name], value) + + def test_compose_unset_optional_settings_do_not_override_webui(self): + service = self.compose({}) + for name in ('CODEBUDDY2API_KEEP_TOOL_METADATA', 'CODEBUDDY2API_FAILOVER_MAX', 'CODEBUDDY2API_RETRY_WRITE_TIMEOUT'): + self.assertIsNone(service['environment'].get(name)) + self.assertEqual(service['ports'][0]['host_ip'], '127.0.0.1') + self.assertEqual(service['environment']['CODEBUDDY_IMPORT_DIR'], '/data/auth/imports') + + +if __name__ == '__main__': + unittest.main()