Skip to content
19 changes: 9 additions & 10 deletions code_puppy/command_line/core_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def handle_cd_command(command: str) -> bool:
# Use shlex.split to handle quoted paths properly
import shlex

from code_puppy.messaging import emit_error, emit_info, emit_success, emit_warning
from code_puppy.messaging import emit_error, emit_info, emit_success

try:
tokens = shlex.split(command)
Expand All @@ -77,20 +77,19 @@ def handle_cd_command(command: str) -> bool:
if os.path.isdir(target):
os.chdir(target)
emit_success(f"Changed directory to: {target}")
# Reload the agent so the system prompt and project-local
# AGENT.md rules reflect the new working directory. Without
# this, the LLM keeps receiving stale path information for the
# remainder of the session (the PydanticAgent instructions are
# baked in at construction time and never refreshed otherwise).

# Reload the agent to pick up new working directory context
# This ensures AGENTS.md is re-read and system prompt is updated
try:
from code_puppy.agents.agent_manager import get_current_agent

# reload_code_generation_agent() invalidates cached rules
# and rebuilds prompt/context from the new cwd
get_current_agent().reload_code_generation_agent()
emit_info("Agent context updated for new directory")
except Exception as e:
emit_warning(
f"Directory changed, but agent reload failed: {e}. "
"You may need to run /agent or /model to force a refresh."
)
# Non-fatal: directory change succeeded even if reload failed
emit_error(f"Could not reload agent context: {e}")
else:
emit_error(f"Not a directory: {dirname}")
return True
Expand Down
8 changes: 5 additions & 3 deletions tests/agents/test_base_agent_full_coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -2129,9 +2129,11 @@ def test_loads_from_project_dir(self, agent, tmp_path):
patch("code_puppy.config.CONFIG_DIR", str(tmp_path / "nonexistent")),
patch(
"pathlib.Path.exists",
side_effect=lambda self: str(self) == str(rules_file)
or str(self).endswith("AGENTS.md")
and "nonexistent" not in str(self),
side_effect=lambda self: (
str(self) == str(rules_file)
or str(self).endswith("AGENTS.md")
and "nonexistent" not in str(self)
),
),
):
# Complex to test due to pathlib patching, just test cached path
Expand Down
55 changes: 55 additions & 0 deletions tests/command_line/test_core_commands_extended.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,61 @@ def test_cd_listing_with_permission_error(self):
args, kwargs = mock_error.call_args
assert "Access denied" in args[0]

def test_cd_reloads_agent_context(self):
"""Test that /cd reloads agent to pick up new directory context (AGENTS.md, etc)."""
mock_agent = MagicMock()

with patch("code_puppy.messaging.emit_success"):
with patch("code_puppy.messaging.emit_info") as mock_emit_info:
with patch("os.path.expanduser", side_effect=lambda x: x):
with patch("os.path.isabs", return_value=True):
with patch("os.path.isdir", return_value=True):
with patch("os.chdir"):
with patch(
"code_puppy.agents.agent_manager.get_current_agent",
return_value=mock_agent,
):
result = handle_cd_command("/cd /new/dir")

# Verify directory changed
assert result is True

# Verify agent was reloaded (public behavior)
mock_agent.reload_code_generation_agent.assert_called_once()

# Verify context refresh message was emitted
mock_emit_info.assert_called_once_with(
"Agent context updated for new directory"
)

def test_cd_handles_agent_reload_failure_gracefully(self):
"""Test that /cd continues even if agent reload fails."""
mock_agent = MagicMock()
mock_agent.reload_code_generation_agent.side_effect = Exception("Reload failed")

with patch("code_puppy.messaging.emit_success"):
with patch("code_puppy.messaging.emit_error") as mock_error:
with patch("os.path.expanduser", side_effect=lambda x: x):
with patch("os.path.isabs", return_value=True):
with patch("os.path.isdir", return_value=True):
with patch("os.chdir"):
with patch(
"code_puppy.agents.agent_manager.get_current_agent",
return_value=mock_agent,
):
result = handle_cd_command("/cd /new/dir")

# Directory change should still succeed
assert result is True

# Error should be emitted about reload failure
mock_error.assert_called_once()
error_msg = mock_error.call_args[0][0]
assert error_msg.startswith(
"Could not reload agent context:"
)
assert "Reload failed" in error_msg

def test_cd_with_nonexistent_parent(self):
"""Test cd command with path containing nonexistent parent directories."""
with patch("code_puppy.messaging.emit_error") as mock_error:
Expand Down
11 changes: 7 additions & 4 deletions tests/command_line/test_model_settings_menu_coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,10 +151,13 @@ def test_get_supported_settings(self, mock_supports):
def test_load_model_settings_with_openai(
self, mock_supports, mock_get_all, mock_effort, mock_verb
):
mock_supports.side_effect = lambda m, s: s in (
"temperature",
"reasoning_effort",
"verbosity",
mock_supports.side_effect = lambda m, s: (
s
in (
"temperature",
"reasoning_effort",
"verbosity",
)
)
menu = _make_menu()
menu._load_model_settings("gpt-5")
Expand Down
14 changes: 7 additions & 7 deletions tests/test_command_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ def test_cd_valid_change_reload_failure_is_nonfatal():
"""A reload failure after /cd must not abort the directory change."""
mocks = setup_messaging_mocks()
mock_emit_success = mocks["emit_success"].start()
mock_emit_warning = mocks["emit_warning"].start()
mock_emit_error = mocks["emit_error"].start()

try:
mock_agent = MagicMock()
Expand All @@ -111,14 +111,14 @@ def test_cd_valid_change_reload_failure_is_nonfatal():
mock_chdir.assert_called_once_with("/some/dir")
mock_emit_success.assert_called_once_with("Changed directory to: /some/dir")
mock_agent.reload_code_generation_agent.assert_called_once()
# Reload failure should emit a warning, not silently pass
mock_emit_warning.assert_called_once()
warning_msg = str(mock_emit_warning.call_args)
assert "agent reload failed" in warning_msg
assert "boom" in warning_msg
# Reload failure should emit an error, not silently pass
mock_emit_error.assert_called_once()
error_msg = mock_emit_error.call_args[0][0]
assert error_msg.startswith("Could not reload agent context:")
assert "boom" in error_msg
finally:
mocks["emit_success"].stop()
mocks["emit_warning"].stop()
mocks["emit_error"].stop()


def test_cd_invalid_directory():
Expand Down
Loading