Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 42 additions & 15 deletions mini_agent/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -483,11 +483,43 @@ async def _quiet_cleanup():
pass


async def run_agent(workspace_dir: Path, task: str = None):
def resolve_workspace_dir(cli_workspace: str | None, config: Config) -> Path:
"""Resolve the effective workspace directory.

Precedence (highest first):
1. ``--workspace`` CLI argument
2. ``workspace_dir`` from config.yaml (``config.agent.workspace_dir``)
3. ``Path.cwd()`` (current working directory)

Relative paths are resolved against the current working directory so the
behaviour matches what users see when invoking the CLI from a project root
(e.g. ``./workspace`` -> ``<cwd>/workspace``).

Args:
cli_workspace: Raw ``--workspace`` argument value (may be ``None``).
config: Loaded :class:`Config` instance.

Returns:
Absolute :class:`Path` to the workspace directory.
"""
if cli_workspace:
return Path(cli_workspace).expanduser().resolve()

config_workspace = config.agent.workspace_dir
if config_workspace:
return Path(config_workspace).expanduser().resolve()

return Path.cwd()


async def run_agent(cli_workspace: str | None, task: str = None):
"""Run Agent in interactive or non-interactive mode.

Args:
workspace_dir: Workspace directory path
cli_workspace: Raw ``--workspace`` CLI argument (``None`` if not
provided). The effective workspace directory is resolved after
loading the configuration so ``workspace_dir`` from ``config.yaml``
is honoured when no CLI argument is given.
task: If provided, execute this task and exit (non-interactive mode)
"""
session_start = datetime.now()
Expand Down Expand Up @@ -535,6 +567,10 @@ async def run_agent(workspace_dir: Path, task: str = None):
print(f"{Colors.RED}❌ Error: Failed to load configuration file: {e}{Colors.RESET}")
return

# 1.5 Resolve workspace directory honouring CLI > config.yaml > cwd precedence
workspace_dir = resolve_workspace_dir(cli_workspace, config)
workspace_dir.mkdir(parents=True, exist_ok=True)

# 2. Initialize LLM client
from mini_agent.retry import RetryConfig as RetryConfigBase

Expand Down Expand Up @@ -854,19 +890,10 @@ def main():
show_log_directory(open_file_manager=True)
return

# Determine workspace directory
# Expand ~ to user home directory for portability
if args.workspace:
workspace_dir = Path(args.workspace).expanduser().absolute()
else:
# Use current working directory
workspace_dir = Path.cwd()

# Ensure workspace directory exists
workspace_dir.mkdir(parents=True, exist_ok=True)

# Run the agent (config always loaded from package directory)
asyncio.run(run_agent(workspace_dir, task=args.task))
# Workspace directory is resolved inside run_agent() after the config has
# been loaded, so the precedence ``--workspace`` > ``workspace_dir`` in
# config.yaml > ``Path.cwd()`` is honoured uniformly (see #90).
asyncio.run(run_agent(args.workspace, task=args.task))


if __name__ == "__main__":
Expand Down
88 changes: 88 additions & 0 deletions tests/test_cli_workspace.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
"""Tests for CLI workspace directory resolution.

Regression coverage for #90: ``workspace_dir`` configured in ``config.yaml``
must be honoured when no ``--workspace`` CLI argument is provided.
"""

import os
from pathlib import Path

import pytest

from mini_agent.cli import resolve_workspace_dir
from mini_agent.config import AgentConfig, Config, LLMConfig, ToolsConfig


def _make_config(workspace_dir: str) -> Config:
"""Build a minimal :class:`Config` for tests with a custom workspace."""
return Config(
llm=LLMConfig(api_key="test-key"),
agent=AgentConfig(workspace_dir=workspace_dir),
tools=ToolsConfig(),
)


def test_resolve_workspace_prefers_cli_argument(tmp_path: Path) -> None:
"""When ``--workspace`` is provided it wins over ``config.workspace_dir``."""
cli_dir = tmp_path / "from-cli"
config_dir = tmp_path / "from-config"
config = _make_config(str(config_dir))

resolved = resolve_workspace_dir(str(cli_dir), config)

assert resolved == cli_dir.resolve()


def test_resolve_workspace_falls_back_to_config(tmp_path: Path) -> None:
"""Without a CLI argument the value from ``config.yaml`` is used."""
config_dir = tmp_path / "from-config"
config = _make_config(str(config_dir))

resolved = resolve_workspace_dir(None, config)

assert resolved == config_dir.resolve()


def test_resolve_workspace_relative_config_resolves_against_cwd(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Relative ``workspace_dir`` from config resolves against the current cwd.

Reproduces the scenario from issue #90 where users set
``workspace_dir: "./workspace"`` and expect ``<cwd>/workspace`` to be used.
"""
monkeypatch.chdir(tmp_path)
config = _make_config("./workspace")

resolved = resolve_workspace_dir(None, config)

assert resolved == (tmp_path / "workspace").resolve()


def test_resolve_workspace_falls_back_to_cwd_when_config_empty(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Empty ``workspace_dir`` in config falls back to the current cwd."""
monkeypatch.chdir(tmp_path)
config = _make_config("")

resolved = resolve_workspace_dir(None, config)

assert resolved == tmp_path


def test_resolve_workspace_expands_user_home(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""``~`` in ``workspace_dir`` is expanded to the user home directory."""
fake_home = tmp_path / "home" / "user"
fake_home.mkdir(parents=True)
monkeypatch.setenv("HOME", str(fake_home))
# On Windows ``Path.expanduser`` honours USERPROFILE, set both to be safe.
monkeypatch.setenv("USERPROFILE", str(fake_home))

config = _make_config("~/my-workspace")

resolved = resolve_workspace_dir(None, config)

assert resolved == (fake_home / "my-workspace").resolve()