From 4685a0165d033040cb870411ca1d3358635bb122 Mon Sep 17 00:00:00 2001 From: "Guangfei.Zhao" Date: Sat, 27 Jun 2026 10:21:00 +0800 Subject: [PATCH] Enhance user configuration management and logging - Introduced user configuration command to set API key. - Updated README and documentation for user config and logging paths. - Refactored logging to support user-specific log files. - Added tests for user configuration and logging behavior. --- .gitignore | 3 +- README.md | 22 ++- docs/PACKAGING_DISTRIBUTION.md | 187 +++++++++++++++++++++++++ docs/PRD.md | 14 +- docs/REQUIREMENTS_DESIGN.md | 42 +++++- src/nexus_claude_api/cli.py | 36 ++++- src/nexus_claude_api/config.py | 33 ++++- src/nexus_claude_api/logging_config.py | 14 +- tests/test_cli.py | 91 +++++++++++- tests/test_logging_config.py | 17 ++- 10 files changed, 434 insertions(+), 25 deletions(-) create mode 100644 docs/PACKAGING_DISTRIBUTION.md diff --git a/.gitignore b/.gitignore index a324792..78cd1ee 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ .pytest_cache/ .test-tmp/ pytest-cache-files-*/ +dist/ __pycache__/ *.py[cod] *$py.class @@ -17,4 +18,4 @@ nexus-claude-api.local.json .agents # logs -logs \ No newline at end of file +logs diff --git a/README.md b/README.md index 1becc69..e974f8d 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ AI Nexus currently documents AWS Bedrock Converse API as the workaround while An ```powershell cd nexus-claude-api uv sync --extra dev +uv run nexus-claude-api config set --api-key "your-nexus-api-key" uv run nexus-claude-api start --port 4141 --claude-code ``` @@ -32,11 +33,17 @@ The printed command ends with `claude --model ` so the current Credential lookup order: 1. `--api-key` -2. local `nexus-claude-api.local.json` +2. user config `~/.config/nexus-claude-api/config.json` 3. `NEXUS_API_KEY` 4. `AWS_BEARER_TOKEN_BEDROCK` -For local hardcoded configuration, create `nexus-claude-api.local.json`: +To write user config: + +```powershell +nexus-claude-api config set --api-key "your-nexus-api-key" +``` + +This creates `~/.config/nexus-claude-api/config.json`: ```json { @@ -44,7 +51,15 @@ For local hardcoded configuration, create `nexus-claude-api.local.json`: } ``` -This file is ignored by git. +Logs are written to `~/.config/nexus-claude-api/logs/nexus-claude-api.log` by default. + +For repository-local development only, pass `--dev` and create `nexus-claude-api.local.json` in the current directory: + +```powershell +uv run nexus-claude-api start --dev --port 4141 --claude-code +``` + +In development mode, config is read from `nexus-claude-api.local.json` and logs are written to `logs/nexus-claude-api.log` in the current directory. The local config file is ignored by git. The service binds to `127.0.0.1` by default and does not persist API keys. @@ -53,3 +68,4 @@ The service binds to `127.0.0.1` by default and does not persist API keys. - [PRD](docs/PRD.md) - [Requirements Design](docs/REQUIREMENTS_DESIGN.md) - [AI Nexus Claude Documentation](docs/AI_NEXUS_CLAUDE.md) +- [Python Internal Distribution Guide](docs/PACKAGING_DISTRIBUTION.md) diff --git a/docs/PACKAGING_DISTRIBUTION.md b/docs/PACKAGING_DISTRIBUTION.md new file mode 100644 index 0000000..dd89fea --- /dev/null +++ b/docs/PACKAGING_DISTRIBUTION.md @@ -0,0 +1,187 @@ +# Python Internal Distribution Guide + +This document describes how to distribute `nexus-claude-api` internally without publishing to PyPI. + +## Recommended Options + +Use one of these two approaches: + +- **Wheel distribution**: build a `.whl` file and send it to colleagues. This is best for stable internal releases. +- **Private Git installation**: let colleagues install directly from the company Git repository. This is best for teammates who can access the source code or need frequent updates. + +Both options require: + +- Python `>=3.11` +- Access to PyPI or the company Python package mirror for runtime dependencies +- A personal AI Nexus API key + +Do not share `nexus-claude-api.local.json`, `.env`, logs, or any personal API key. + +## Option 1: Build and Share a Wheel + +A wheel is the standard Python install package format. It is not an `.exe`; colleagues install it with `pip`, then run the installed `nexus-claude-api` command. + +### Publisher Steps + +1. Confirm the package version in `pyproject.toml`: + + ```toml + [project] + name = "nexus-claude-api" + version = "0.1.0" + ``` + + Update the version for every released package. Avoid sending different builds with the same version. + +2. Run the supported checks: + + ```powershell + uv sync --extra dev + uv run pytest + uv run nexus-claude-api start --dry-run + ``` + +3. Build the package: + + ```powershell + uv build + ``` + + This creates files under `dist/`, usually: + + ```text + dist/nexus_claude_api-0.1.0-py3-none-any.whl + dist/nexus_claude_api-0.1.0.tar.gz + ``` + +4. Test the wheel in a clean environment: + + ```powershell + py -3.11 -m venv C:\Temp\nexus-claude-api-test + C:\Temp\nexus-claude-api-test\Scripts\python.exe -m pip install .\dist\nexus_claude_api-0.1.0-py3-none-any.whl + C:\Temp\nexus-claude-api-test\Scripts\nexus-claude-api.exe start --dry-run + ``` + +5. Send only the `.whl` file to colleagues through an approved internal channel. + +### Colleague Installation + +Install the wheel: + +```powershell +py -3.11 -m pip install .\nexus_claude_api-0.1.0-py3-none-any.whl +``` + +Configure the personal Nexus key: + +```powershell +nexus-claude-api config set --api-key "your-nexus-api-key" +``` + +Start the local proxy and print the Claude Code helper: + +```powershell +nexus-claude-api start --port 4141 --claude-code +``` + +If PowerShell cannot find the installed script, use the module form: + +```powershell +py -3.11 -m nexus_claude_api start --port 4141 --claude-code +``` + +### Upgrade + +Send a new wheel with a new version, then ask colleagues to run: + +```powershell +py -3.11 -m pip install --upgrade .\nexus_claude_api-0.1.1-py3-none-any.whl +``` + +## Option 2: Install from Private Git + +Private Git installation avoids manual wheel sharing. It still uses the Python package metadata from `pyproject.toml`, so the installed command is the same. + +### Publisher Steps + +1. Push the code to the company Git repository. + +2. Update the version in `pyproject.toml` for each released version. + +3. Tag stable versions: + + ```powershell + git tag v0.1.0 + git push origin v0.1.0 + ``` + + For the next release: + + ```powershell + git tag v0.1.1 + git push origin v0.1.1 + ``` + +4. Share an install command that points to a tag: + + ```powershell + py -3.11 -m pip install "git+https://company-git.example.com/group/nexus-claude-api.git@v0.1.0" + ``` + +Prefer tags or exact commit SHAs. Avoid asking normal users to install from `main`, because repeated installs can produce different code over time. + +### Colleague Installation + +Install from the company Git repository: + +```powershell +py -3.11 -m pip install "git+https://company-git.example.com/group/nexus-claude-api.git@v0.1.0" +``` + +Configure the personal Nexus key: + +```powershell +nexus-claude-api config set --api-key "your-nexus-api-key" +``` + +Start the local proxy: + +```powershell +nexus-claude-api start --port 4141 --claude-code +``` + +### Upgrade + +Upgrade to a newer tag: + +```powershell +py -3.11 -m pip install --upgrade "git+https://company-git.example.com/group/nexus-claude-api.git@v0.1.1" +``` + +### Development Clone + +For colleagues who will modify or test the source code: + +```powershell +git clone https://company-git.example.com/group/nexus-claude-api.git +cd nexus-claude-api +uv sync --extra dev +uv run nexus-claude-api start --dry-run +``` + +## Choosing Between the Two + +- Use **wheel distribution** for normal internal users who only need a stable tool. +- Use **private Git installation** for colleagues who understand the repository, need fast updates, or help develop the project. +- Keep version numbers and Git tags aligned. For example, `pyproject.toml` version `0.1.0` should correspond to tag `v0.1.0`. + +## Release Checklist + +Before sharing a release: + +- Run `uv run pytest`. +- Run `uv run nexus-claude-api start --dry-run`. +- Test wheel installation in a clean Python environment. +- If using Git installation, test install from the exact tag. +- Confirm no personal credentials are included. +- Confirm `nexus-claude-api.local.json`, `.env`, logs, caches, and `dist/` artifacts are not committed accidentally. diff --git a/docs/PRD.md b/docs/PRD.md index 1bd7a85..44810b7 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -53,7 +53,8 @@ The proxy defaults to Opus because this deployment is intended for users whose N ## User Stories -- As a developer, I can store my Nexus key in ignored local config or set `NEXUS_API_KEY`, then run `nexus-claude-api start --claude-code` to get a Claude Code launch command. +- As a developer, I can store my Nexus key in user config with `nexus-claude-api config set --api-key ` or set `NEXUS_API_KEY`, then run `nexus-claude-api start --claude-code` to get a Claude Code launch command. +- As a contributor working from the repository, I can pass `--dev` to use current-directory development config and logs. - As a Claude Code user, I can run coding workflows through local `http://127.0.0.1:4141`. - As a Claude Code user, I can receive streaming model output. - As a Claude Code user, I can use tool calls and tool results. @@ -63,7 +64,12 @@ The proxy defaults to Opus because this deployment is intended for users whose N ## Acceptance Criteria - `uv run nexus-claude-api start --port 4141 --claude-code` starts a local server. +- `nexus-claude-api config set --api-key ` writes user config to `~/.config/nexus-claude-api/config.json`. - The server binds to `127.0.0.1` by default. +- Default startup reads credentials from user config before environment variables. +- `--dev` startup reads current-directory `nexus-claude-api.local.json` instead of user config. +- Default logs are written to `~/.config/nexus-claude-api/logs/nexus-claude-api.log`. +- `--dev` logs are written to current-directory `logs/nexus-claude-api.log`. - Missing Nexus credentials fail fast with a clear error. - `GET /health` returns healthy status. - `GET /v1/models` returns the supported Claude models. @@ -76,8 +82,10 @@ The proxy defaults to Opus because this deployment is intended for users whose N ## Security Requirements -- Do not persist API keys automatically. -- If the user chooses hardcoded local configuration, keep it in ignored `nexus-claude-api.local.json`. +- Do not persist API keys automatically during startup. +- Persist API keys only when the user explicitly runs `nexus-claude-api config set --api-key ` or manually writes a config file. +- Store normal user configuration in `~/.config/nexus-claude-api/config.json`. +- Use ignored current-directory `nexus-claude-api.local.json` only for explicit `--dev` repository-local development mode. - Do not print or log API keys. - Redact authorization headers in debug logs. - Bind locally by default. diff --git a/docs/REQUIREMENTS_DESIGN.md b/docs/REQUIREMENTS_DESIGN.md index bf027f3..727940f 100644 --- a/docs/REQUIREMENTS_DESIGN.md +++ b/docs/REQUIREMENTS_DESIGN.md @@ -54,16 +54,41 @@ Primary command: uv run nexus-claude-api start --port 4141 --claude-code ``` +User configuration command: + +```powershell +nexus-claude-api config set --api-key +``` + Options: - `--host`: default `127.0.0.1` - `--port`, `-p`: default `4141` - `--endpoint-url`: default `https://genai-nexus.api.corpinter.net` -- `--api-key`: optional; fallback to ignored local config, `NEXUS_API_KEY`, then `AWS_BEARER_TOKEN_BEDROCK` +- `--api-key`: optional; overrides config files and environment variables - `--model`: default `claude-opus-4.6` - `--small-model`: default `claude-opus-4.6` - `--claude-code`: print Claude Code launch command - `--verbose`, `-v`: debug logging without secrets +- `--dev`: use current-directory development config and logs +- `--dry-run`: validate config and print helper output without starting the server + +Credential lookup order: + +1. `--api-key` +2. Current-mode config file +3. `NEXUS_API_KEY` +4. `AWS_BEARER_TOKEN_BEDROCK` + +Config file paths: + +- Default mode: `~/.config/nexus-claude-api/config.json` +- Development mode with `--dev`: current-directory `nexus-claude-api.local.json` + +Log file paths: + +- Default mode: `~/.config/nexus-claude-api/logs/nexus-claude-api.log` +- Development mode with `--dev`: current-directory `logs/nexus-claude-api.log` When `--claude-code` is used, print a PowerShell command that sets: @@ -89,7 +114,7 @@ Expose: `ANTHROPIC_AUTH_TOKEN` is printed as `dummy` because Claude Code expects an Anthropic auth token variable to exist. This local proxy does not validate that inbound token by default. It is not the Nexus key. -Inbound authentication headers are accepted for compatibility but not validated by default because the service is local. Outbound Nexus authentication uses `--api-key`, ignored local `nexus-claude-api.local.json`, `NEXUS_API_KEY`, or `AWS_BEARER_TOKEN_BEDROCK`. +Inbound authentication headers are accepted for compatibility but not validated by default because the service is local. Outbound Nexus authentication uses `--api-key`, the current-mode config file, `NEXUS_API_KEY`, or `AWS_BEARER_TOKEN_BEDROCK`. ## Model Mapping @@ -182,7 +207,7 @@ Return Anthropic-compatible errors: Status mapping: - invalid request: `400` -- missing local Nexus credential: startup failure +- missing Nexus credential: startup failure - Nexus auth failure: `401` or `403` - Nexus throttling: `429` - Nexus network/timeout: `502` or `504` @@ -215,3 +240,14 @@ CLI tests: - `nexus-claude-api --help` - Claude Code command generation. - Missing API key validation. +- User config writing with `nexus-claude-api config set --api-key `. +- Default-mode user config lookup from `~/.config/nexus-claude-api/config.json`. +- Default mode ignores current-directory `nexus-claude-api.local.json`. +- Development mode uses current-directory `nexus-claude-api.local.json`. +- `--api-key` overrides config files and config files override environment variables. + +Logging tests: + +- Default-mode log path resolves to `~/.config/nexus-claude-api/logs/nexus-claude-api.log`. +- Development-mode log path resolves to current-directory `logs/nexus-claude-api.log`. +- Verbose logging enables debug details without logging secrets. diff --git a/src/nexus_claude_api/cli.py b/src/nexus_claude_api/cli.py index 6e210b0..58120cb 100644 --- a/src/nexus_claude_api/cli.py +++ b/src/nexus_claude_api/cli.py @@ -5,6 +5,7 @@ import sys import uvicorn +import nexus_claude_api.config as config from nexus_claude_api.config import ( DEFAULT_ENDPOINT_URL, DEFAULT_HOST, @@ -13,7 +14,7 @@ from nexus_claude_api.config import ( DEFAULT_SMALL_MODEL, Settings, ) -from nexus_claude_api.logging_config import configure_logging +from nexus_claude_api.logging_config import configure_logging, resolve_log_file from nexus_claude_api.server import create_app from nexus_claude_api.shell import generate_claude_code_powershell @@ -31,11 +32,21 @@ def build_parser() -> argparse.ArgumentParser: start.add_argument("--small-model", default=DEFAULT_SMALL_MODEL) start.add_argument("--claude-code", action="store_true") start.add_argument("--verbose", "-v", action="store_true") + start.add_argument( + "--dev", + action="store_true", + help="Use local development config and logs in the current directory.", + ) start.add_argument( "--dry-run", action="store_true", help="Validate config and print helper output without starting the server.", ) + + config = subparsers.add_parser("config", help="Manage user configuration") + config_subparsers = config.add_subparsers(dest="config_command") + config_set = config_subparsers.add_parser("set", help="Write user configuration") + config_set.add_argument("--api-key", required=True) return parser @@ -44,10 +55,21 @@ def main(argv: list[str] | None = None) -> int: args = parser.parse_args(argv) if args.command == "start": return run_start(args) + if args.command == "config": + return run_config(args, parser) parser.print_help() return 0 +def run_config(args: argparse.Namespace, parser: argparse.ArgumentParser) -> int: + if args.config_command == "set": + config_path = config.write_user_config({"api_key": args.api_key}) + print(f"Wrote user config to {config_path}") + return 0 + parser.parse_args(["config", "--help"]) + return 0 + + def run_start(args: argparse.Namespace) -> int: settings = Settings.from_values( host=args.host, @@ -57,12 +79,15 @@ def run_start(args: argparse.Namespace) -> int: model=args.model, small_model=args.small_model, verbose=args.verbose, + dev=args.dev, ) if not settings.api_key: print( - "Missing Nexus API key. Add nexus-claude-api.local.json, set " - "NEXUS_API_KEY or AWS_BEARER_TOKEN_BEDROCK, or pass --api-key.", + "Missing Nexus API key. Run " + "`nexus-claude-api config set --api-key `, set NEXUS_API_KEY " + "or AWS_BEARER_TOKEN_BEDROCK, pass --api-key, or use --dev with " + "nexus-claude-api.local.json.", file=sys.stderr, ) return 2 @@ -74,7 +99,10 @@ def run_start(args: argparse.Namespace) -> int: if args.dry_run: return 0 - log_file = configure_logging(verbose=settings.verbose) + log_file = configure_logging( + verbose=settings.verbose, + log_file=resolve_log_file(dev=args.dev), + ) print(f"Writing detailed logs to {log_file}") app = create_app(settings) uvicorn.run( diff --git a/src/nexus_claude_api/config.py b/src/nexus_claude_api/config.py index ee114a5..a102368 100644 --- a/src/nexus_claude_api/config.py +++ b/src/nexus_claude_api/config.py @@ -13,6 +13,8 @@ DEFAULT_OPUS_MODEL = "claude-opus-4.6" DEFAULT_MODEL = DEFAULT_OPUS_MODEL DEFAULT_SMALL_MODEL = DEFAULT_OPUS_MODEL LOCAL_CONFIG_FILENAME = "nexus-claude-api.local.json" +USER_CONFIG_DIR = Path.home() / ".config" / "nexus-claude-api" +USER_CONFIG_FILE = USER_CONFIG_DIR / "config.json" MODEL_ID_MAP = { @@ -48,12 +50,13 @@ class Settings: small_model: str = DEFAULT_SMALL_MODEL, verbose: bool = False, require_api_key: bool = True, + dev: bool = False, ) -> "Settings": - local_config = load_local_config() + config = load_config(dev=dev) resolved_api_key = ( api_key - or local_config.get("api_key") - or local_config.get("nexus_api_key") + or config.get("api_key") + or config.get("nexus_api_key") or os.environ.get("NEXUS_API_KEY") or os.environ.get("AWS_BEARER_TOKEN_BEDROCK") ) @@ -78,6 +81,30 @@ def resolve_backend_model(model: str) -> str: def load_local_config(path: str | Path = LOCAL_CONFIG_FILENAME) -> dict[str, str]: + return load_config_file(path) + + +def load_user_config(path: str | Path | None = None) -> dict[str, str]: + return load_config_file(path or USER_CONFIG_FILE) + + +def load_config(*, dev: bool = False) -> dict[str, str]: + if dev: + return load_local_config() + return load_user_config() + + +def write_user_config(config: dict[str, str], path: str | Path | None = None) -> Path: + config_path = Path(path or USER_CONFIG_FILE) + config_path.parent.mkdir(parents=True, exist_ok=True) + config_path.write_text( + json.dumps(config, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + return config_path + + +def load_config_file(path: str | Path) -> dict[str, str]: config_path = Path(path) if not config_path.exists(): return {} diff --git a/src/nexus_claude_api/logging_config.py b/src/nexus_claude_api/logging_config.py index ebfcf08..f25a4aa 100644 --- a/src/nexus_claude_api/logging_config.py +++ b/src/nexus_claude_api/logging_config.py @@ -3,9 +3,13 @@ from __future__ import annotations import logging from pathlib import Path +import nexus_claude_api.config as config -LOG_DIR = Path("logs") -LOG_FILE = LOG_DIR / "nexus-claude-api.log" +LOCAL_LOG_DIR = Path("logs") +LOCAL_LOG_FILE = LOCAL_LOG_DIR / "nexus-claude-api.log" +USER_LOG_DIR = config.USER_CONFIG_DIR / "logs" +USER_LOG_FILE = USER_LOG_DIR / "nexus-claude-api.log" +LOG_FILE = USER_LOG_FILE LOG_FORMAT = "%(asctime)s %(levelname)s %(name)s: %(message)s" @@ -24,3 +28,9 @@ def configure_logging(*, verbose: bool = False, log_file: Path = LOG_FILE) -> Pa app_logger.propagate = False return log_file + + +def resolve_log_file(*, dev: bool = False) -> Path: + if dev: + return LOCAL_LOG_FILE + return config.USER_CONFIG_DIR / "logs" / "nexus-claude-api.log" diff --git a/tests/test_cli.py b/tests/test_cli.py index ca77e67..5dd980e 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,10 +1,16 @@ from __future__ import annotations +import json import shutil from pathlib import Path from nexus_claude_api.cli import main -from nexus_claude_api.config import Settings, load_local_config +from nexus_claude_api.config import ( + Settings, + load_local_config, + load_user_config, + write_user_config, +) from nexus_claude_api.shell import generate_claude_code_powershell @@ -41,6 +47,8 @@ def test_claude_code_command_uses_custom_model() -> None: def test_missing_api_key_fails(monkeypatch) -> None: tmp_path = _workspace_tmp("missing-key") + user_config = tmp_path / ".config" / "nexus-claude-api" / "config.json" + monkeypatch.setattr("nexus_claude_api.config.USER_CONFIG_FILE", user_config) monkeypatch.delenv("NEXUS_API_KEY", raising=False) monkeypatch.delenv("AWS_BEARER_TOKEN_BEDROCK", raising=False) monkeypatch.chdir(tmp_path) @@ -54,8 +62,43 @@ def test_missing_api_key_fails(monkeypatch) -> None: assert exit_code == 2 -def test_local_config_api_key(monkeypatch) -> None: - tmp_path = _workspace_tmp("local-config") +def test_user_config_api_key(monkeypatch) -> None: + tmp_path = _workspace_tmp("user-config") + user_config = tmp_path / ".config" / "nexus-claude-api" / "config.json" + monkeypatch.setattr("nexus_claude_api.config.USER_CONFIG_FILE", user_config) + write_user_config({"api_key": "user-test-key"}) + + settings = Settings.from_values(require_api_key=False) + + assert settings.api_key == "user-test-key" + assert load_user_config()["api_key"] == "user-test-key" + shutil.rmtree(tmp_path, ignore_errors=True) + + +def test_dev_local_config_api_key(monkeypatch) -> None: + tmp_path = _workspace_tmp("dev-local-config") + monkeypatch.chdir(tmp_path) + (tmp_path / "nexus-claude-api.local.json").write_text( + '{"api_key": "local-test-key"}', + encoding="utf-8", + ) + + try: + settings = Settings.from_values(require_api_key=False, dev=True) + + assert settings.api_key == "local-test-key" + assert load_local_config()["api_key"] == "local-test-key" + finally: + monkeypatch.chdir(Path(__file__).parents[1]) + shutil.rmtree(tmp_path, ignore_errors=True) + + +def test_default_mode_ignores_local_config(monkeypatch) -> None: + tmp_path = _workspace_tmp("default-ignores-local") + user_config = tmp_path / ".config" / "nexus-claude-api" / "config.json" + monkeypatch.setattr("nexus_claude_api.config.USER_CONFIG_FILE", user_config) + monkeypatch.delenv("NEXUS_API_KEY", raising=False) + monkeypatch.delenv("AWS_BEARER_TOKEN_BEDROCK", raising=False) monkeypatch.chdir(tmp_path) (tmp_path / "nexus-claude-api.local.json").write_text( '{"api_key": "local-test-key"}', @@ -65,13 +108,51 @@ def test_local_config_api_key(monkeypatch) -> None: try: settings = Settings.from_values(require_api_key=False) - assert settings.api_key == "local-test-key" - assert load_local_config()["api_key"] == "local-test-key" + assert settings.api_key is None finally: monkeypatch.chdir(Path(__file__).parents[1]) shutil.rmtree(tmp_path, ignore_errors=True) +def test_api_key_precedence_config_before_environment(monkeypatch) -> None: + tmp_path = _workspace_tmp("config-precedence") + user_config = tmp_path / ".config" / "nexus-claude-api" / "config.json" + monkeypatch.setattr("nexus_claude_api.config.USER_CONFIG_FILE", user_config) + monkeypatch.setenv("NEXUS_API_KEY", "env-key") + write_user_config({"api_key": "config-key"}) + + settings = Settings.from_values(require_api_key=False) + + assert settings.api_key == "config-key" + shutil.rmtree(tmp_path, ignore_errors=True) + + +def test_api_key_argument_overrides_config(monkeypatch) -> None: + tmp_path = _workspace_tmp("arg-precedence") + user_config = tmp_path / ".config" / "nexus-claude-api" / "config.json" + monkeypatch.setattr("nexus_claude_api.config.USER_CONFIG_FILE", user_config) + write_user_config({"api_key": "config-key"}) + + settings = Settings.from_values(api_key="arg-key", require_api_key=False) + + assert settings.api_key == "arg-key" + shutil.rmtree(tmp_path, ignore_errors=True) + + +def test_config_set_writes_user_config(monkeypatch) -> None: + tmp_path = _workspace_tmp("config-set") + user_config = tmp_path / ".config" / "nexus-claude-api" / "config.json" + monkeypatch.setattr("nexus_claude_api.config.USER_CONFIG_FILE", user_config) + + exit_code = main(["config", "set", "--api-key", "written-key"]) + + assert exit_code == 0 + assert json.loads(user_config.read_text(encoding="utf-8")) == { + "api_key": "written-key" + } + shutil.rmtree(tmp_path, ignore_errors=True) + + def _workspace_tmp(name: str) -> Path: path = Path(__file__).parents[1] / ".test-tmp" / name shutil.rmtree(path, ignore_errors=True) diff --git a/tests/test_logging_config.py b/tests/test_logging_config.py index 548e080..e0d7531 100644 --- a/tests/test_logging_config.py +++ b/tests/test_logging_config.py @@ -1,8 +1,9 @@ from __future__ import annotations import logging +from pathlib import Path -from nexus_claude_api.logging_config import configure_logging +from nexus_claude_api.logging_config import configure_logging, resolve_log_file def test_configure_logging_writes_app_logs_to_file_without_console( @@ -55,3 +56,17 @@ def test_configure_logging_verbose_writes_debug_app_logs_to_file( app_logger.handlers.extend(original_handlers) app_logger.setLevel(original_level) app_logger.propagate = original_propagate + + +def test_resolve_log_file_uses_user_config_dir_by_default( + tmp_path, + monkeypatch, +) -> None: + user_config_dir = tmp_path / ".config" / "nexus-claude-api" + monkeypatch.setattr("nexus_claude_api.config.USER_CONFIG_DIR", user_config_dir) + + assert resolve_log_file() == user_config_dir / "logs" / "nexus-claude-api.log" + + +def test_resolve_log_file_uses_local_logs_in_dev_mode() -> None: + assert resolve_log_file(dev=True) == Path("logs") / "nexus-claude-api.log"