pdd-cli 0.0.90__py3-none-any.whl → 0.0.118__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. pdd/__init__.py +38 -6
  2. pdd/agentic_bug.py +323 -0
  3. pdd/agentic_bug_orchestrator.py +497 -0
  4. pdd/agentic_change.py +231 -0
  5. pdd/agentic_change_orchestrator.py +526 -0
  6. pdd/agentic_common.py +521 -786
  7. pdd/agentic_e2e_fix.py +319 -0
  8. pdd/agentic_e2e_fix_orchestrator.py +426 -0
  9. pdd/agentic_fix.py +118 -3
  10. pdd/agentic_update.py +25 -8
  11. pdd/architecture_sync.py +565 -0
  12. pdd/auth_service.py +210 -0
  13. pdd/auto_deps_main.py +63 -53
  14. pdd/auto_include.py +185 -3
  15. pdd/auto_update.py +125 -47
  16. pdd/bug_main.py +195 -23
  17. pdd/cmd_test_main.py +345 -197
  18. pdd/code_generator.py +4 -2
  19. pdd/code_generator_main.py +118 -32
  20. pdd/commands/__init__.py +6 -0
  21. pdd/commands/analysis.py +87 -29
  22. pdd/commands/auth.py +309 -0
  23. pdd/commands/connect.py +290 -0
  24. pdd/commands/fix.py +136 -113
  25. pdd/commands/maintenance.py +3 -2
  26. pdd/commands/misc.py +8 -0
  27. pdd/commands/modify.py +190 -164
  28. pdd/commands/sessions.py +284 -0
  29. pdd/construct_paths.py +334 -32
  30. pdd/context_generator_main.py +167 -170
  31. pdd/continue_generation.py +6 -3
  32. pdd/core/__init__.py +33 -0
  33. pdd/core/cli.py +27 -3
  34. pdd/core/cloud.py +237 -0
  35. pdd/core/errors.py +4 -0
  36. pdd/core/remote_session.py +61 -0
  37. pdd/crash_main.py +219 -23
  38. pdd/data/llm_model.csv +4 -4
  39. pdd/docs/prompting_guide.md +864 -0
  40. pdd/docs/whitepaper_with_benchmarks/data_and_functions/benchmark_analysis.py +495 -0
  41. pdd/docs/whitepaper_with_benchmarks/data_and_functions/creation_compare.py +528 -0
  42. pdd/fix_code_loop.py +208 -34
  43. pdd/fix_code_module_errors.py +6 -2
  44. pdd/fix_error_loop.py +291 -38
  45. pdd/fix_main.py +204 -4
  46. pdd/fix_verification_errors_loop.py +235 -26
  47. pdd/fix_verification_main.py +269 -83
  48. pdd/frontend/dist/assets/index-B5DZHykP.css +1 -0
  49. pdd/frontend/dist/assets/index-DQ3wkeQ2.js +449 -0
  50. pdd/frontend/dist/index.html +376 -0
  51. pdd/frontend/dist/logo.svg +33 -0
  52. pdd/generate_output_paths.py +46 -5
  53. pdd/generate_test.py +212 -151
  54. pdd/get_comment.py +19 -44
  55. pdd/get_extension.py +8 -9
  56. pdd/get_jwt_token.py +309 -20
  57. pdd/get_language.py +8 -7
  58. pdd/get_run_command.py +7 -5
  59. pdd/insert_includes.py +2 -1
  60. pdd/llm_invoke.py +459 -95
  61. pdd/load_prompt_template.py +15 -34
  62. pdd/path_resolution.py +140 -0
  63. pdd/postprocess.py +4 -1
  64. pdd/preprocess.py +68 -12
  65. pdd/preprocess_main.py +33 -1
  66. pdd/prompts/agentic_bug_step10_pr_LLM.prompt +182 -0
  67. pdd/prompts/agentic_bug_step1_duplicate_LLM.prompt +73 -0
  68. pdd/prompts/agentic_bug_step2_docs_LLM.prompt +129 -0
  69. pdd/prompts/agentic_bug_step3_triage_LLM.prompt +95 -0
  70. pdd/prompts/agentic_bug_step4_reproduce_LLM.prompt +97 -0
  71. pdd/prompts/agentic_bug_step5_root_cause_LLM.prompt +123 -0
  72. pdd/prompts/agentic_bug_step6_test_plan_LLM.prompt +107 -0
  73. pdd/prompts/agentic_bug_step7_generate_LLM.prompt +172 -0
  74. pdd/prompts/agentic_bug_step8_verify_LLM.prompt +119 -0
  75. pdd/prompts/agentic_bug_step9_e2e_test_LLM.prompt +289 -0
  76. pdd/prompts/agentic_change_step10_identify_issues_LLM.prompt +1006 -0
  77. pdd/prompts/agentic_change_step11_fix_issues_LLM.prompt +984 -0
  78. pdd/prompts/agentic_change_step12_create_pr_LLM.prompt +131 -0
  79. pdd/prompts/agentic_change_step1_duplicate_LLM.prompt +73 -0
  80. pdd/prompts/agentic_change_step2_docs_LLM.prompt +101 -0
  81. pdd/prompts/agentic_change_step3_research_LLM.prompt +126 -0
  82. pdd/prompts/agentic_change_step4_clarify_LLM.prompt +164 -0
  83. pdd/prompts/agentic_change_step5_docs_change_LLM.prompt +981 -0
  84. pdd/prompts/agentic_change_step6_devunits_LLM.prompt +1005 -0
  85. pdd/prompts/agentic_change_step7_architecture_LLM.prompt +1044 -0
  86. pdd/prompts/agentic_change_step8_analyze_LLM.prompt +1027 -0
  87. pdd/prompts/agentic_change_step9_implement_LLM.prompt +1077 -0
  88. pdd/prompts/agentic_e2e_fix_step1_unit_tests_LLM.prompt +90 -0
  89. pdd/prompts/agentic_e2e_fix_step2_e2e_tests_LLM.prompt +91 -0
  90. pdd/prompts/agentic_e2e_fix_step3_root_cause_LLM.prompt +89 -0
  91. pdd/prompts/agentic_e2e_fix_step4_fix_e2e_tests_LLM.prompt +96 -0
  92. pdd/prompts/agentic_e2e_fix_step5_identify_devunits_LLM.prompt +91 -0
  93. pdd/prompts/agentic_e2e_fix_step6_create_unit_tests_LLM.prompt +106 -0
  94. pdd/prompts/agentic_e2e_fix_step7_verify_tests_LLM.prompt +116 -0
  95. pdd/prompts/agentic_e2e_fix_step8_run_pdd_fix_LLM.prompt +120 -0
  96. pdd/prompts/agentic_e2e_fix_step9_verify_all_LLM.prompt +146 -0
  97. pdd/prompts/agentic_fix_primary_LLM.prompt +2 -2
  98. pdd/prompts/agentic_update_LLM.prompt +192 -338
  99. pdd/prompts/auto_include_LLM.prompt +22 -0
  100. pdd/prompts/change_LLM.prompt +3093 -1
  101. pdd/prompts/detect_change_LLM.prompt +571 -14
  102. pdd/prompts/fix_code_module_errors_LLM.prompt +8 -0
  103. pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +1 -0
  104. pdd/prompts/generate_test_LLM.prompt +20 -1
  105. pdd/prompts/generate_test_from_example_LLM.prompt +115 -0
  106. pdd/prompts/insert_includes_LLM.prompt +262 -252
  107. pdd/prompts/prompt_code_diff_LLM.prompt +119 -0
  108. pdd/prompts/prompt_diff_LLM.prompt +82 -0
  109. pdd/remote_session.py +876 -0
  110. pdd/server/__init__.py +52 -0
  111. pdd/server/app.py +335 -0
  112. pdd/server/click_executor.py +587 -0
  113. pdd/server/executor.py +338 -0
  114. pdd/server/jobs.py +661 -0
  115. pdd/server/models.py +241 -0
  116. pdd/server/routes/__init__.py +31 -0
  117. pdd/server/routes/architecture.py +451 -0
  118. pdd/server/routes/auth.py +364 -0
  119. pdd/server/routes/commands.py +929 -0
  120. pdd/server/routes/config.py +42 -0
  121. pdd/server/routes/files.py +603 -0
  122. pdd/server/routes/prompts.py +1322 -0
  123. pdd/server/routes/websocket.py +473 -0
  124. pdd/server/security.py +243 -0
  125. pdd/server/terminal_spawner.py +209 -0
  126. pdd/server/token_counter.py +222 -0
  127. pdd/summarize_directory.py +236 -237
  128. pdd/sync_animation.py +8 -4
  129. pdd/sync_determine_operation.py +329 -47
  130. pdd/sync_main.py +272 -28
  131. pdd/sync_orchestration.py +136 -75
  132. pdd/template_expander.py +161 -0
  133. pdd/templates/architecture/architecture_json.prompt +41 -46
  134. pdd/trace.py +1 -1
  135. pdd/track_cost.py +0 -13
  136. pdd/unfinished_prompt.py +2 -1
  137. pdd/update_main.py +23 -5
  138. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/METADATA +15 -10
  139. pdd_cli-0.0.118.dist-info/RECORD +227 -0
  140. pdd_cli-0.0.90.dist-info/RECORD +0 -153
  141. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/WHEEL +0 -0
  142. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/entry_points.txt +0 -0
  143. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/licenses/LICENSE +0 -0
  144. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/top_level.txt +0 -0
pdd/cmd_test_main.py CHANGED
@@ -2,234 +2,382 @@
2
2
  Main entry point for the 'test' command.
3
3
  """
4
4
  from __future__ import annotations
5
- import click
5
+
6
+ import json
7
+ import os
6
8
  from pathlib import Path
7
- # pylint: disable=redefined-builtin
8
- from rich import print
9
+
10
+ import click
11
+ import requests
12
+ from rich.console import Console
13
+ from rich.panel import Panel
9
14
 
10
15
  from .config_resolution import resolve_effective_config
11
16
  from .construct_paths import construct_paths
17
+ from .core.cloud import CloudConfig
12
18
  from .generate_test import generate_test
13
19
  from .increase_tests import increase_tests
14
20
 
21
+ # Cloud request timeout
22
+ CLOUD_REQUEST_TIMEOUT = 400 # seconds
23
+
24
+ console = Console()
25
+
15
26
 
16
- # pylint: disable=too-many-arguments, too-many-locals, too-many-return-statements, too-many-branches, too-many-statements, broad-except
17
27
  def cmd_test_main(
18
28
  ctx: click.Context,
19
29
  prompt_file: str,
20
30
  code_file: str,
21
- output: str | None,
22
- language: str | None,
23
- coverage_report: str | None,
24
- existing_tests: list[str] | None,
25
- target_coverage: float | None,
26
- merge: bool | None,
31
+ output: str | None = None,
32
+ language: str | None = None,
33
+ coverage_report: str | None = None,
34
+ existing_tests: list[str] | None = None,
35
+ target_coverage: float | None = None,
36
+ merge: bool = False,
27
37
  strength: float | None = None,
28
38
  temperature: float | None = None,
29
39
  ) -> tuple[str, float, str]:
30
40
  """
31
41
  CLI wrapper for generating or enhancing unit tests.
32
42
 
33
- Reads a prompt file and a code file, generates unit tests using the `generate_test` function,
34
- and handles the output location.
35
-
36
43
  Args:
37
- ctx (click.Context): The Click context object.
38
- prompt_file (str): Path to the prompt file.
39
- code_file (str): Path to the code file.
40
- output (str | None): Path to save the generated test file.
41
- language (str | None): Programming language.
42
- coverage_report (str | None): Path to the coverage report file.
43
- existing_tests (list[str] | None): Paths to the existing unit test files.
44
- target_coverage (float | None): Desired code coverage percentage.
45
- merge (bool | None): Whether to merge new tests with existing tests.
44
+ ctx: Click context object containing global options.
45
+ prompt_file: Path to the prompt file.
46
+ code_file: Path to the code file.
47
+ output: Optional path for the output test file.
48
+ language: Optional programming language.
49
+ coverage_report: Optional path to a coverage report (triggers enhancement mode).
50
+ existing_tests: List of paths to existing test files (required if coverage_report is used).
51
+ target_coverage: Desired coverage percentage (not currently used by logic but accepted).
52
+ merge: If True, merge output into the first existing test file.
53
+ strength: Optional override for LLM strength.
54
+ temperature: Optional override for LLM temperature.
46
55
 
47
56
  Returns:
48
- tuple[str, float, str]: Generated unit test code, total cost, and model name.
57
+ tuple: (generated_test_code, total_cost, model_name)
49
58
  """
50
- # Initialize variables
51
- unit_test = ""
52
- total_cost = 0.0
53
- model_name = ""
54
- output_file_paths = {"output": output}
55
- input_strings = {}
56
-
57
- verbose = ctx.obj["verbose"]
58
- # Note: strength/temperature will be resolved after construct_paths using resolve_effective_config
59
- param_strength = strength # Store the parameter value for later resolution
60
- param_temperature = temperature # Store the parameter value for later resolution
61
-
62
- if verbose:
63
- print(f"[bold blue]Prompt file:[/bold blue] {prompt_file}")
64
- print(f"[bold blue]Code file:[/bold blue] {code_file}")
65
- if output:
66
- print(f"[bold blue]Output:[/bold blue] {output}")
67
- if language:
68
- print(f"[bold blue]Language:[/bold blue] {language}")
69
-
70
- # Construct input strings, output file paths, and determine language
71
- try:
72
- input_file_paths = {
73
- "prompt_file": prompt_file,
74
- "code_file": code_file,
75
- }
76
- if coverage_report:
77
- input_file_paths["coverage_report"] = coverage_report
59
+ # 1. Prepare inputs for path construction
60
+ input_file_paths = {
61
+ "prompt_file": prompt_file,
62
+ "code_file": code_file,
63
+ }
64
+
65
+ # If coverage report is provided, we need existing tests
66
+ if coverage_report:
67
+ if not existing_tests:
68
+ console.print(
69
+ "[bold red]Error: 'existing_tests' is required when "
70
+ "'coverage_report' is provided.[/bold red]"
71
+ )
72
+ return "", 0.0, "Error: Missing existing_tests"
73
+
74
+ input_file_paths["coverage_report"] = coverage_report
75
+ # We pass the first existing test to help construct_paths resolve context if needed,
76
+ # though construct_paths primarily uses prompt/code files for language detection.
78
77
  if existing_tests:
79
- input_file_paths["existing_tests"] = existing_tests[0]
78
+ input_file_paths["existing_test_0"] = existing_tests[0]
80
79
 
81
- command_options = {
82
- "output": output,
83
- "language": language,
84
- "merge": merge,
85
- "target_coverage": target_coverage,
86
- }
80
+ command_options = {
81
+ "output": output,
82
+ "language": language,
83
+ "merge": merge,
84
+ "target_coverage": target_coverage,
85
+ }
87
86
 
88
- resolved_config, input_strings, output_file_paths, language = construct_paths(
87
+ # 2. Construct paths and read inputs
88
+ try:
89
+ resolved_config, input_strings, output_file_paths, detected_language = construct_paths(
89
90
  input_file_paths=input_file_paths,
90
- force=ctx.obj["force"],
91
- quiet=ctx.obj["quiet"],
92
- command="test",
93
91
  command_options=command_options,
94
- context_override=ctx.obj.get('context'),
95
- confirm_callback=ctx.obj.get('confirm_callback')
92
+ force=ctx.obj.get("force", False),
93
+ quiet=ctx.obj.get("quiet", False),
94
+ command="test",
95
+ context_override=ctx.obj.get("context"),
96
+ confirm_callback=ctx.obj.get("confirm_callback"),
96
97
  )
98
+ except Exception as e:
99
+ console.print(f"[bold red]Error constructing paths: {e}[/bold red]")
100
+ return "", 0.0, f"Error: {e}"
97
101
 
98
- # Read multiple existing test files and concatenate their content
99
- if existing_tests:
100
- existing_tests_content = ""
101
- for test_file in existing_tests:
102
- with open(test_file, 'r') as f:
103
- existing_tests_content += f.read() + "\n"
104
- input_strings["existing_tests"] = existing_tests_content
105
-
106
- # Use centralized config resolution with proper priority:
107
- # CLI > pddrc > defaults
108
- effective_config = resolve_effective_config(
109
- ctx,
110
- resolved_config,
111
- param_overrides={"strength": param_strength, "temperature": param_temperature}
112
- )
113
- strength = effective_config["strength"]
114
- temperature = effective_config["temperature"]
115
- time = effective_config["time"]
116
- except click.Abort:
117
- # User cancelled - re-raise to stop the sync loop
118
- raise
119
- except Exception as exception:
120
- # Catching a general exception is necessary here to handle a wide range of
121
- # potential errors during file I/O and path construction, ensuring the
122
- # CLI remains robust.
123
- print(f"[bold red]Error constructing paths: {exception}[/bold red]")
124
- # Return error result instead of ctx.exit(1) to allow orchestrator to handle gracefully
125
- return "", 0.0, f"Error: {exception}"
126
-
127
- if verbose:
128
- print(f"[bold blue]Language detected:[/bold blue] {language}")
129
-
130
- # Determine where the generated tests will be written so we can share it with the LLM
131
- # Always use resolved_output since construct_paths handles numbering for test/bug commands
132
- resolved_output = output_file_paths["output"]
133
- output_file = resolved_output
134
- if merge and existing_tests:
135
- output_file = existing_tests[0]
136
-
137
- if not output_file:
138
- print("[bold red]Error: Output file path could not be determined.[/bold red]")
139
- # Return error result instead of ctx.exit(1) to allow orchestrator to handle gracefully
140
- return "", 0.0, "Error: Output file path could not be determined"
141
-
142
- source_file_path_for_prompt = str(Path(code_file).expanduser().resolve())
143
- test_file_path_for_prompt = str(Path(output_file).expanduser().resolve())
144
- module_name_for_prompt = Path(source_file_path_for_prompt).stem if source_file_path_for_prompt else ""
145
-
146
- # Generate or enhance unit tests
147
- if not coverage_report:
102
+ # 3. Resolve effective configuration (strength, temperature, time)
103
+ # Priority: Function Arg > CLI Context > Config File > Default
104
+ eff_config = resolve_effective_config(
105
+ ctx,
106
+ resolved_config,
107
+ param_overrides={"strength": strength, "temperature": temperature}
108
+ )
109
+
110
+ eff_strength = eff_config["strength"]
111
+ eff_temperature = eff_config["temperature"]
112
+ eff_time = eff_config["time"]
113
+ verbose = ctx.obj.get("verbose", False)
114
+ is_local = ctx.obj.get("local", False)
115
+
116
+ # 4. Prepare content variables
117
+ prompt_content = input_strings.get("prompt_file", "")
118
+ code_content = input_strings.get("code_file", "")
119
+
120
+ # Handle existing tests concatenation
121
+ concatenated_tests = None
122
+ if existing_tests:
123
+ test_contents = []
124
+ for et_path in existing_tests:
125
+ try:
126
+ # We read these manually because construct_paths only reads
127
+ # what's in input_file_paths keys. While we added
128
+ # existing_test_0, we might have multiple. To be safe and
129
+ # consistent with the requirement "read all files", we read
130
+ # them here. Note: construct_paths might have read
131
+ # 'existing_test_0', but we need all of them.
132
+ p = Path(et_path).expanduser().resolve()
133
+ if p.exists():
134
+ test_contents.append(p.read_text(encoding="utf-8"))
135
+ except Exception as e:
136
+ console.print(
137
+ f"[yellow]Warning: Could not read existing test file {et_path}: {e}[/yellow]"
138
+ )
139
+
140
+ if test_contents:
141
+ concatenated_tests = "\n".join(test_contents)
142
+ # Update input_strings for consistency if needed downstream
143
+ input_strings["existing_tests"] = concatenated_tests
144
+
145
+ # 5. Determine Execution Mode (Generate vs Increase)
146
+ mode = "increase" if coverage_report else "generate"
147
+
148
+ # Prepare metadata for generation
149
+ source_file_path = str(Path(code_file).expanduser().resolve())
150
+ # output_file_paths['output_file'] is set by construct_paths based on
151
+ # --output or defaults
152
+ test_file_path = str(
153
+ Path(output_file_paths.get("output_file", "test_output.py"))
154
+ .expanduser().resolve()
155
+ )
156
+ module_name = Path(source_file_path).stem
157
+
158
+ # Determine if code file is an example (for TDD style generation)
159
+ is_example = Path(code_file).stem.endswith("_example")
160
+
161
+ # Check for cloud-only mode
162
+ cloud_only = os.environ.get("PDD_CLOUD_ONLY", "").lower() in ("1", "true", "yes")
163
+
164
+ # 6. Execution Logic (Cloud vs Local)
165
+ generated_content = ""
166
+ total_cost = 0.0
167
+ model_name = "unknown"
168
+
169
+ # --- Cloud Execution ---
170
+ if not is_local:
148
171
  try:
149
- unit_test, total_cost, model_name = generate_test(
150
- input_strings["prompt_file"],
151
- input_strings["code_file"],
152
- strength=strength,
153
- temperature=temperature,
154
- time=time,
155
- language=language,
156
- verbose=verbose,
157
- source_file_path=source_file_path_for_prompt,
158
- test_file_path=test_file_path_for_prompt,
159
- module_name=module_name_for_prompt,
160
- )
161
- except Exception as exception:
162
- # A general exception is caught to handle various errors that can occur
163
- # during the test generation process, which involves external model
164
- # interactions and complex logic.
165
- print(f"[bold red]Error generating tests: {exception}[/bold red]")
166
- # Return error result instead of ctx.exit(1) to allow orchestrator to handle gracefully
167
- return "", 0.0, f"Error: {exception}"
168
- else:
169
- if not existing_tests:
170
- print(
171
- "[bold red]Error: --existing-tests is required "
172
- "when using --coverage-report[/bold red]"
172
+ if verbose:
173
+ console.print(
174
+ Panel(
175
+ f"Attempting Cloud Execution (Mode: {mode})",
176
+ title="Cloud Status",
177
+ style="blue"
178
+ )
179
+ )
180
+
181
+ # Get JWT Token using CloudConfig
182
+ jwt_token = CloudConfig.get_jwt_token(verbose=verbose)
183
+
184
+ if not jwt_token:
185
+ raise Exception("Failed to obtain JWT token.")
186
+
187
+ # Prepare Payload
188
+ payload = {
189
+ "promptContent": prompt_content,
190
+ "language": detected_language,
191
+ "strength": eff_strength,
192
+ "temperature": eff_temperature,
193
+ "time": eff_time,
194
+ "verbose": verbose,
195
+ "sourceFilePath": source_file_path,
196
+ "testFilePath": test_file_path,
197
+ "moduleName": module_name,
198
+ "mode": mode,
199
+ }
200
+
201
+ if mode == "generate":
202
+ if is_example:
203
+ payload["example"] = code_content
204
+ payload["codeContent"] = None
205
+ else:
206
+ payload["codeContent"] = code_content
207
+ payload["example"] = None
208
+
209
+ if concatenated_tests:
210
+ payload["existingTests"] = concatenated_tests
211
+
212
+ elif mode == "increase":
213
+ payload["codeContent"] = code_content
214
+ payload["existingTests"] = concatenated_tests
215
+ payload["coverageReport"] = input_strings.get("coverage_report", "")
216
+
217
+ # Make Request
218
+ cloud_url = CloudConfig.get_endpoint_url("generateTest")
219
+ headers = {"Authorization": f"Bearer {jwt_token}"}
220
+ response = requests.post(
221
+ cloud_url,
222
+ json=payload,
223
+ headers=headers,
224
+ timeout=CLOUD_REQUEST_TIMEOUT
173
225
  )
174
- # Return error result instead of ctx.exit(1) to allow orchestrator to handle gracefully
175
- return "", 0.0, "Error: --existing-tests is required when using --coverage-report"
226
+
227
+ # Check for HTTP errors explicitly
228
+ response.raise_for_status()
229
+
230
+ # Parse response
231
+ try:
232
+ data = response.json()
233
+ except json.JSONDecodeError as json_err:
234
+ if cloud_only:
235
+ raise click.UsageError(f"Cloud returned invalid JSON: {json_err}")
236
+ console.print("[yellow]Cloud returned invalid JSON, falling back to local.[/yellow]")
237
+ is_local = True
238
+ raise # Re-raise to exit try block
239
+
240
+ generated_content = data.get("generatedTest", "")
241
+ total_cost = float(data.get("totalCost", 0.0))
242
+ model_name = data.get("modelName", "cloud-model")
243
+
244
+ # Check for empty response
245
+ if not generated_content or not generated_content.strip():
246
+ if cloud_only:
247
+ raise click.UsageError("Cloud returned empty test content")
248
+ console.print("[yellow]Cloud returned empty test content, falling back to local.[/yellow]")
249
+ is_local = True
250
+ else:
251
+ # Success!
252
+ console.print("[green]Cloud Success[/green]")
253
+
254
+ except click.UsageError:
255
+ # Re-raise UsageError without wrapping
256
+ raise
257
+ except requests.exceptions.Timeout as timeout_err:
258
+ if cloud_only:
259
+ raise click.UsageError(f"Cloud request timed out: {timeout_err}")
260
+ console.print("[yellow]Cloud request timed out, falling back to local.[/yellow]")
261
+ is_local = True
262
+ except requests.exceptions.HTTPError as http_err:
263
+ # Handle HTTP errors from raise_for_status()
264
+ # HTTPError from requests always has a response attribute
265
+ response_obj = getattr(http_err, 'response', None)
266
+ status_code = response_obj.status_code if response_obj is not None else None
267
+ error_text = response_obj.text if response_obj is not None else str(http_err)
268
+
269
+ # Non-recoverable errors - raise UsageError
270
+ if status_code == 402:
271
+ # Display balance info
272
+ try:
273
+ error_data = http_err.response.json()
274
+ balance = error_data.get("currentBalance", "unknown")
275
+ cost = error_data.get("estimatedCost", "unknown")
276
+ console.print(f"[red]Current balance: {balance}, Estimated cost: {cost}[/red]")
277
+ except Exception:
278
+ pass
279
+ raise click.UsageError(f"Insufficient credits: {error_text}")
280
+ elif status_code == 401:
281
+ raise click.UsageError(f"Cloud authentication failed: {error_text}")
282
+ elif status_code == 403:
283
+ raise click.UsageError(f"Access denied: {error_text}")
284
+ elif status_code == 400:
285
+ raise click.UsageError(f"Invalid request: {error_text}")
286
+
287
+ # 5xx and other errors - fall back if allowed
288
+ if cloud_only:
289
+ raise click.UsageError(f"Cloud execution failed (HTTP {status_code}): {error_text}")
290
+ console.print(f"[yellow]Cloud execution failed (HTTP {status_code}), falling back to local.[/yellow]")
291
+ is_local = True
292
+ except json.JSONDecodeError:
293
+ # Already handled above, just ensure we fall through to local
294
+ pass
295
+ except Exception as e:
296
+ if cloud_only:
297
+ raise click.UsageError(f"Cloud execution failed: {e}")
298
+ console.print(f"[yellow]Cloud execution failed: {e}. Falling back to local.[/yellow]")
299
+ is_local = True
300
+
301
+ # --- Local Execution ---
302
+ if is_local:
176
303
  try:
177
- unit_test, total_cost, model_name = increase_tests(
178
- existing_unit_tests=input_strings["existing_tests"],
179
- coverage_report=input_strings["coverage_report"],
180
- code=input_strings["code_file"],
181
- prompt_that_generated_code=input_strings["prompt_file"],
182
- language=language,
183
- strength=strength,
184
- temperature=temperature,
185
- time=time,
186
- verbose=verbose,
187
- )
188
- except Exception as exception:
189
- # This broad exception is used to catch any issue that might arise
190
- # while increasing test coverage, including problems with parsing
191
- # reports or interacting with the language model.
192
- print(f"[bold red]Error increasing test coverage: {exception}[/bold red]")
193
- # Return error result instead of ctx.exit(1) to allow orchestrator to handle gracefully
194
- return "", 0.0, f"Error: {exception}"
195
-
196
- # Handle output - always use resolved file path since construct_paths handles numbering
197
- resolved_output = output_file_paths["output"]
198
- output_file = resolved_output
199
- if merge and existing_tests:
200
- output_file = existing_tests[0] if existing_tests else None
201
-
202
- if not output_file:
203
- print("[bold red]Error: Output file path could not be determined.[/bold red]")
204
- ctx.exit(1)
205
- return "", 0.0, ""
206
-
207
- # Check if unit_test content is empty
208
- if not unit_test or not unit_test.strip():
209
- print(f"[bold red]Error: Generated unit test content is empty or whitespace-only.[/bold red]")
210
- print(f"[bold yellow]Debug: unit_test length: {len(unit_test) if unit_test else 0}[/bold yellow]")
211
- print(f"[bold yellow]Debug: unit_test content preview: {repr(unit_test[:100]) if unit_test else 'None'}[/bold yellow]")
212
- # Return error result instead of ctx.exit(1) to allow orchestrator to handle gracefully
213
- return "", 0.0, "Error: Generated unit test content is empty"
214
-
304
+ if verbose:
305
+ console.print(
306
+ Panel(
307
+ f"Running Local Execution (Mode: {mode})",
308
+ title="Local Status",
309
+ style="green"
310
+ )
311
+ )
312
+
313
+ if mode == "generate":
314
+ # Determine args based on is_example
315
+ code_arg = None if is_example else code_content
316
+ example_arg = code_content if is_example else None
317
+
318
+ generated_content, total_cost, model_name = generate_test(
319
+ prompt=prompt_content,
320
+ code=code_arg,
321
+ example=example_arg,
322
+ strength=eff_strength,
323
+ temperature=eff_temperature,
324
+ time=eff_time,
325
+ language=detected_language,
326
+ verbose=verbose,
327
+ source_file_path=source_file_path,
328
+ test_file_path=test_file_path,
329
+ module_name=module_name,
330
+ existing_tests=concatenated_tests
331
+ )
332
+ else: # mode == "increase"
333
+ generated_content, total_cost, model_name = increase_tests(
334
+ existing_unit_tests=concatenated_tests if concatenated_tests else "",
335
+ coverage_report=input_strings.get("coverage_report", ""),
336
+ code=code_content,
337
+ prompt_that_generated_code=prompt_content,
338
+ language=detected_language,
339
+ strength=eff_strength,
340
+ temperature=eff_temperature,
341
+ time=eff_time,
342
+ verbose=verbose
343
+ )
344
+
345
+ except Exception as e:
346
+ console.print(f"[bold red]Error during local execution: {e}[/bold red]")
347
+ return "", 0.0, f"Error: {e}"
348
+
349
+ # 7. Validate Output
350
+ if not generated_content or not generated_content.strip():
351
+ console.print("[bold red]Error: Generated test content is empty.[/bold red]")
352
+ return "", 0.0, "Error: Empty output"
353
+
354
+ # 8. Write Output
215
355
  try:
356
+ final_output_path = Path(output_file_paths["output"])
357
+
216
358
  # Ensure parent directory exists
217
- output_path = Path(output_file)
218
- output_path.parent.mkdir(parents=True, exist_ok=True)
219
-
220
- with open(output_file, "w", encoding="utf-8") as file_handle:
221
- file_handle.write(unit_test)
222
- print(f"[bold green]Unit tests saved to:[/bold green] {output_file}")
223
- except Exception as exception:
224
- # A broad exception is caught here to handle potential file system errors
225
- # (e.g., permissions, disk space) that can occur when writing the
226
- # output file, preventing the program from crashing unexpectedly.
227
- print(f"[bold red]Error saving tests to file: {exception}[/bold red]")
228
- # Return error result instead of ctx.exit(1) to allow orchestrator to handle gracefully
229
- return "", 0.0, f"Error: {exception}"
230
-
231
- if verbose:
232
- print(f"[bold blue]Total cost:[/bold blue] ${total_cost:.6f}")
233
- print(f"[bold blue]Model used:[/bold blue] {model_name}")
234
-
235
- return unit_test, total_cost, model_name
359
+ final_output_path.parent.mkdir(parents=True, exist_ok=True)
360
+
361
+ write_mode = "w"
362
+ content_to_write = generated_content
363
+
364
+ # Handle merge logic
365
+ if merge and existing_tests:
366
+ # If merging, we write to the first existing test file
367
+ final_output_path = Path(existing_tests[0])
368
+ write_mode = "a"
369
+ content_to_write = "\n\n" + generated_content
370
+ if verbose:
371
+ console.print(f"Merging new tests into existing file: {final_output_path}")
372
+
373
+ with open(str(final_output_path), write_mode, encoding="utf-8") as f:
374
+ f.write(content_to_write)
375
+
376
+ if not ctx.obj.get("quiet", False):
377
+ console.print(f"[green]Successfully wrote tests to {final_output_path}[/green]")
378
+
379
+ except Exception as e:
380
+ console.print(f"[bold red]Error writing output file: {e}[/bold red]")
381
+ return "", 0.0, f"Error: {e}"
382
+
383
+ return generated_content, total_cost, model_name
pdd/code_generator.py CHANGED
@@ -86,7 +86,8 @@ def code_generator(
86
86
  temperature=temperature,
87
87
  time=time,
88
88
  verbose=verbose,
89
- output_schema=output_schema
89
+ output_schema=output_schema,
90
+ language=language,
90
91
  )
91
92
  else:
92
93
  response = llm_invoke(
@@ -96,7 +97,8 @@ def code_generator(
96
97
  temperature=temperature,
97
98
  time=time,
98
99
  verbose=verbose,
99
- output_schema=output_schema
100
+ output_schema=output_schema,
101
+ language=language,
100
102
  )
101
103
  initial_output = response['result']
102
104
  total_cost += response['cost']