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
1 change: 1 addition & 0 deletions pdd/update_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ def resolve_prompt_code_pair(code_file_path: str, quiet: bool = False, output_di
pass

# Determine the base prompts directory
context_config = {}
if output_dir:
# Use the custom output directory (absolute path)
base_prompts_dir = os.path.abspath(output_dir)
Expand Down
82 changes: 82 additions & 0 deletions tests/test_e2e_issue_493_update_output_subdir.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"""
E2E Test for Issue #493: pdd update --output crashes with NameError when
code file is in a subdirectory.

This test exercises the full update_main() → resolve_prompt_code_pair() path
that a user triggers via `pdd update <code_file> --output <path>`. It mocks
only the LLM call (update_prompt) and agent availability at the system boundary,
but exercises all real path resolution logic including .pddrc context detection,
git repo discovery, and subdirectory structure preservation.

Bug: resolve_prompt_code_pair() defines context_config only in the else branch
(when --output is NOT provided) but references it unconditionally when computing
code_root. When --output IS provided and the code file is in a subdirectory
(rel_dir != "."), this causes an UnboundLocalError.
"""

import git
import click
from pathlib import Path
from unittest.mock import patch

from pdd.update_main import update_main


def test_e2e_update_output_flag_with_subdirectory_code_file(tmp_path, monkeypatch):
"""
E2E regression test: `pdd update backend/src/module.py --output /tmp/output/`
should NOT crash with NameError when the code file is in a subdirectory.

Exercises: update_main → resolve_prompt_code_pair → path resolution with --output.
Mocks: LLM call (update_prompt), agent availability (get_available_agents).
"""
# Setup: realistic repo structure matching the issue reproduction
repo_path = tmp_path / "pdd-test"
repo_path.mkdir()
sub_dir = repo_path / "backend" / "src"
sub_dir.mkdir(parents=True)
code_file = sub_dir / "module.py"
code_file.write_text("def hello(): return 'world'")

output_dir = tmp_path / "output"
output_dir.mkdir()

# Initialize git repo (required for repo root detection)
repo = git.Repo.init(repo_path)
repo.index.add([str(code_file)])
repo.index.commit("init")

monkeypatch.chdir(repo_path)

# Build a real Click context like the CLI would
ctx = click.Context(click.Command("update"))
ctx.obj = {
"strength": 0.5,
"temperature": 0.0,
"verbose": False,
"quiet": True,
}

with patch("pdd.update_main.update_prompt") as mock_up, \
patch("pdd.update_main.get_available_agents", return_value=[]), \
patch("pdd.update_main.get_language", return_value="python"):

mock_up.return_value = ("generated prompt content", 0.01, "mock-model")

# Act: This is what `pdd update backend/src/module.py --output /tmp/output/` does
result = update_main(
ctx=ctx,
input_prompt_file=None,
modified_code_file=str(code_file),
input_code_file=None,
output=str(output_dir),
use_git=False,
)

# Assert: should succeed, not crash with NameError
assert result is not None, "update_main returned None, indicating a crash or error"
assert result[0] == "generated prompt content"

# The prompt file should exist somewhere under the output directory
prompt_files = list(output_dir.rglob("*_python.prompt"))
assert len(prompt_files) >= 1, f"No prompt file created in {output_dir}"
95 changes: 94 additions & 1 deletion tests/test_update_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -708,4 +708,97 @@ def agentic_side_effect(prompt_file, **kwargs):

# Source must remain unchanged even after agentic failure
assert source_prompt.read_text() == original_content, \
"Source prompt was corrupted by failed agentic update"
"Source prompt was corrupted by failed agentic update"


# --- Tests for issue #493: NameError when using --output with subdirectory code file ---

from pdd.update_main import resolve_prompt_code_pair


def test_resolve_prompt_code_pair_output_dir_with_subdirectory_code_file(tmp_path, monkeypatch):
"""
Regression test for GitHub issue #493.
resolve_prompt_code_pair() crashes with UnboundLocalError when --output is provided
and the code file is in a subdirectory (rel_dir != "."), because context_config
is only defined in the else branch but used unconditionally when computing code_root.
"""
# Setup: git repo with code file in a subdirectory
repo_path = tmp_path / "test_repo"
repo_path.mkdir()
sub_dir = repo_path / "backend" / "src"
sub_dir.mkdir(parents=True)
code_file = sub_dir / "module.py"
code_file.write_text("def hello(): return 'world'")

output_dir = tmp_path / "output"
output_dir.mkdir()

monkeypatch.chdir(repo_path)
git.Repo.init(repo_path)

with patch("pdd.update_main.get_language") as mock_lang:
mock_lang.return_value = "python"

# This should NOT raise NameError for context_config
prompt_path, code_path = resolve_prompt_code_pair(
str(code_file), quiet=True, output_dir=str(output_dir)
)

# The prompt file should be created under the output directory
assert os.path.exists(prompt_path)
assert Path(prompt_path).is_relative_to(output_dir)
assert prompt_path.endswith("module_python.prompt")


def test_resolve_prompt_code_pair_output_dir_with_root_level_code_file(tmp_path, monkeypatch):
"""
Edge case for issue #493: code file at repo root with --output should work
(this path doesn't hit the bug since rel_dir == "." skips the code_root logic).
"""
repo_path = tmp_path / "test_repo"
repo_path.mkdir()
code_file = repo_path / "module.py"
code_file.write_text("def hello(): return 'world'")

output_dir = tmp_path / "output"
output_dir.mkdir()

monkeypatch.chdir(repo_path)
git.Repo.init(repo_path)

with patch("pdd.update_main.get_language") as mock_lang:
mock_lang.return_value = "python"

prompt_path, code_path = resolve_prompt_code_pair(
str(code_file), quiet=True, output_dir=str(output_dir)
)

assert os.path.exists(prompt_path)
assert prompt_path.endswith("module_python.prompt")


def test_resolve_prompt_code_pair_no_output_dir_subdirectory_still_works(tmp_path, monkeypatch):
"""
No-regression test for issue #493: without --output, subdirectory code files
should still work as before (context_config is defined in else branch).
"""
repo_path = tmp_path / "test_repo"
repo_path.mkdir()
sub_dir = repo_path / "backend"
sub_dir.mkdir()
code_file = sub_dir / "module.py"
code_file.write_text("def hello(): return 'world'")

monkeypatch.chdir(repo_path)
git.Repo.init(repo_path)

with patch("pdd.update_main.get_language") as mock_lang:
mock_lang.return_value = "python"

prompt_path, code_path = resolve_prompt_code_pair(
str(code_file), quiet=True
)

assert os.path.exists(prompt_path)
assert prompt_path.endswith("module_python.prompt")