From f667ef85ef26b42afead263ea45b35befbccc7ca Mon Sep 17 00:00:00 2001 From: Serhan Date: Wed, 11 Feb 2026 19:06:14 -0500 Subject: [PATCH 1/3] Add failing tests for update --output subdirectory crash (#493) Unit test and E2E test that reproduce the NameError when using pdd update with --output flag on a code file in a subdirectory. context_config is only defined in the else branch but used unconditionally at line 91. Co-Authored-By: Claude Opus 4.6 --- ...test_e2e_issue_493_update_output_subdir.py | 84 ++++++++++++++++ tests/test_update_main.py | 95 ++++++++++++++++++- 2 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 tests/test_e2e_issue_493_update_output_subdir.py diff --git a/tests/test_e2e_issue_493_update_output_subdir.py b/tests/test_e2e_issue_493_update_output_subdir.py new file mode 100644 index 00000000..07da9107 --- /dev/null +++ b/tests/test_e2e_issue_493_update_output_subdir.py @@ -0,0 +1,84 @@ +""" +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 --output `. 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 at line 91. +When --output IS provided and the code file is in a subdirectory (rel_dir != "."), +this causes an UnboundLocalError. +""" + +import os +import pytest +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}" diff --git a/tests/test_update_main.py b/tests/test_update_main.py index 5ab235a6..edb9c13e 100644 --- a/tests/test_update_main.py +++ b/tests/test_update_main.py @@ -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" \ No newline at end of file + "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 NameError 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 at line 91. + """ + # 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 str(output_dir) in prompt_path + 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 line 91). + """ + 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") \ No newline at end of file From fc92adb5cae100558c2bc923ab68a162b1ec7713 Mon Sep 17 00:00:00 2001 From: Serhan Date: Wed, 11 Feb 2026 19:09:54 -0500 Subject: [PATCH 2/3] fix: pdd update --output crashes with NameError when code file is in subdirectory Fixes #493 --- pdd/update_main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pdd/update_main.py b/pdd/update_main.py index 9a45f6f3..11a26e3d 100644 --- a/pdd/update_main.py +++ b/pdd/update_main.py @@ -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) From 835f9f8b484f6820b05c8840c87e8b6eeedda18c Mon Sep 17 00:00:00 2001 From: Serhan Date: Fri, 13 Feb 2026 15:54:29 -0500 Subject: [PATCH 3/3] Address Copilot review comments on test files - Remove unused imports (os, pytest) from e2e test - Replace hardcoded line number references with descriptive text in docstrings - Use Path.is_relative_to() instead of fragile substring check for path assertion - Fix missing newline at EOF in test_update_main.py Co-Authored-By: Claude Opus 4.6 --- tests/test_e2e_issue_493_update_output_subdir.py | 8 +++----- tests/test_update_main.py | 10 +++++----- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/tests/test_e2e_issue_493_update_output_subdir.py b/tests/test_e2e_issue_493_update_output_subdir.py index 07da9107..d3169daa 100644 --- a/tests/test_e2e_issue_493_update_output_subdir.py +++ b/tests/test_e2e_issue_493_update_output_subdir.py @@ -9,13 +9,11 @@ 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 at line 91. -When --output IS provided and the code file is in a subdirectory (rel_dir != "."), -this causes an UnboundLocalError. +(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 os -import pytest import git import click from pathlib import Path diff --git a/tests/test_update_main.py b/tests/test_update_main.py index edb9c13e..fe4f6fb3 100644 --- a/tests/test_update_main.py +++ b/tests/test_update_main.py @@ -719,9 +719,9 @@ def agentic_side_effect(prompt_file, **kwargs): 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 NameError when --output is provided + 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 at line 91. + 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" @@ -747,14 +747,14 @@ def test_resolve_prompt_code_pair_output_dir_with_subdirectory_code_file(tmp_pat # The prompt file should be created under the output directory assert os.path.exists(prompt_path) - assert str(output_dir) in 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 line 91). + (this path doesn't hit the bug since rel_dir == "." skips the code_root logic). """ repo_path = tmp_path / "test_repo" repo_path.mkdir() @@ -801,4 +801,4 @@ def test_resolve_prompt_code_pair_no_output_dir_subdirectory_still_works(tmp_pat ) assert os.path.exists(prompt_path) - assert prompt_path.endswith("module_python.prompt") \ No newline at end of file + assert prompt_path.endswith("module_python.prompt")