nc1709 1.15.4__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.
- nc1709/__init__.py +13 -0
- nc1709/agent/__init__.py +36 -0
- nc1709/agent/core.py +505 -0
- nc1709/agent/mcp_bridge.py +245 -0
- nc1709/agent/permissions.py +298 -0
- nc1709/agent/tools/__init__.py +21 -0
- nc1709/agent/tools/base.py +440 -0
- nc1709/agent/tools/bash_tool.py +367 -0
- nc1709/agent/tools/file_tools.py +454 -0
- nc1709/agent/tools/notebook_tools.py +516 -0
- nc1709/agent/tools/search_tools.py +322 -0
- nc1709/agent/tools/task_tool.py +284 -0
- nc1709/agent/tools/web_tools.py +555 -0
- nc1709/agents/__init__.py +17 -0
- nc1709/agents/auto_fix.py +506 -0
- nc1709/agents/test_generator.py +507 -0
- nc1709/checkpoints.py +372 -0
- nc1709/cli.py +3380 -0
- nc1709/cli_ui.py +1080 -0
- nc1709/cognitive/__init__.py +149 -0
- nc1709/cognitive/anticipation.py +594 -0
- nc1709/cognitive/context_engine.py +1046 -0
- nc1709/cognitive/council.py +824 -0
- nc1709/cognitive/learning.py +761 -0
- nc1709/cognitive/router.py +583 -0
- nc1709/cognitive/system.py +519 -0
- nc1709/config.py +155 -0
- nc1709/custom_commands.py +300 -0
- nc1709/executor.py +333 -0
- nc1709/file_controller.py +354 -0
- nc1709/git_integration.py +308 -0
- nc1709/github_integration.py +477 -0
- nc1709/image_input.py +446 -0
- nc1709/linting.py +519 -0
- nc1709/llm_adapter.py +667 -0
- nc1709/logger.py +192 -0
- nc1709/mcp/__init__.py +18 -0
- nc1709/mcp/client.py +370 -0
- nc1709/mcp/manager.py +407 -0
- nc1709/mcp/protocol.py +210 -0
- nc1709/mcp/server.py +473 -0
- nc1709/memory/__init__.py +20 -0
- nc1709/memory/embeddings.py +325 -0
- nc1709/memory/indexer.py +474 -0
- nc1709/memory/sessions.py +432 -0
- nc1709/memory/vector_store.py +451 -0
- nc1709/models/__init__.py +86 -0
- nc1709/models/detector.py +377 -0
- nc1709/models/formats.py +315 -0
- nc1709/models/manager.py +438 -0
- nc1709/models/registry.py +497 -0
- nc1709/performance/__init__.py +343 -0
- nc1709/performance/cache.py +705 -0
- nc1709/performance/pipeline.py +611 -0
- nc1709/performance/tiering.py +543 -0
- nc1709/plan_mode.py +362 -0
- nc1709/plugins/__init__.py +17 -0
- nc1709/plugins/agents/__init__.py +18 -0
- nc1709/plugins/agents/django_agent.py +912 -0
- nc1709/plugins/agents/docker_agent.py +623 -0
- nc1709/plugins/agents/fastapi_agent.py +887 -0
- nc1709/plugins/agents/git_agent.py +731 -0
- nc1709/plugins/agents/nextjs_agent.py +867 -0
- nc1709/plugins/base.py +359 -0
- nc1709/plugins/manager.py +411 -0
- nc1709/plugins/registry.py +337 -0
- nc1709/progress.py +443 -0
- nc1709/prompts/__init__.py +22 -0
- nc1709/prompts/agent_system.py +180 -0
- nc1709/prompts/task_prompts.py +340 -0
- nc1709/prompts/unified_prompt.py +133 -0
- nc1709/reasoning_engine.py +541 -0
- nc1709/remote_client.py +266 -0
- nc1709/shell_completions.py +349 -0
- nc1709/slash_commands.py +649 -0
- nc1709/task_classifier.py +408 -0
- nc1709/version_check.py +177 -0
- nc1709/web/__init__.py +8 -0
- nc1709/web/server.py +950 -0
- nc1709/web/templates/index.html +1127 -0
- nc1709-1.15.4.dist-info/METADATA +858 -0
- nc1709-1.15.4.dist-info/RECORD +86 -0
- nc1709-1.15.4.dist-info/WHEEL +5 -0
- nc1709-1.15.4.dist-info/entry_points.txt +2 -0
- nc1709-1.15.4.dist-info/licenses/LICENSE +9 -0
- nc1709-1.15.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Test Generator Agent
|
|
3
|
+
Automatically generates unit tests for code
|
|
4
|
+
"""
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import ast
|
|
8
|
+
from typing import List, Dict, Any, Optional, Tuple
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from enum import Enum
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestFramework(Enum):
|
|
14
|
+
"""Supported test frameworks"""
|
|
15
|
+
PYTEST = "pytest"
|
|
16
|
+
UNITTEST = "unittest"
|
|
17
|
+
JEST = "jest"
|
|
18
|
+
MOCHA = "mocha"
|
|
19
|
+
VITEST = "vitest"
|
|
20
|
+
GO_TEST = "go_test"
|
|
21
|
+
RUST_TEST = "rust_test"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class FunctionInfo:
|
|
26
|
+
"""Information about a function to test"""
|
|
27
|
+
name: str
|
|
28
|
+
file_path: str
|
|
29
|
+
line_number: int
|
|
30
|
+
signature: str
|
|
31
|
+
docstring: Optional[str]
|
|
32
|
+
code: str
|
|
33
|
+
is_async: bool
|
|
34
|
+
is_method: bool
|
|
35
|
+
class_name: Optional[str]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class GeneratedTest:
|
|
40
|
+
"""A generated test"""
|
|
41
|
+
function_name: str
|
|
42
|
+
test_name: str
|
|
43
|
+
test_code: str
|
|
44
|
+
description: str
|
|
45
|
+
framework: TestFramework
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class TestGeneratorAgent:
|
|
49
|
+
"""Agent that generates unit tests for code"""
|
|
50
|
+
|
|
51
|
+
def __init__(self, llm_adapter=None):
|
|
52
|
+
"""Initialize the test generator agent
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
llm_adapter: LLMAdapter instance for generating tests
|
|
56
|
+
"""
|
|
57
|
+
self.llm = llm_adapter
|
|
58
|
+
|
|
59
|
+
def analyze_file(self, file_path: str) -> List[FunctionInfo]:
|
|
60
|
+
"""Analyze a file to find testable functions
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
file_path: Path to the file
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
List of functions found
|
|
67
|
+
"""
|
|
68
|
+
if not os.path.exists(file_path):
|
|
69
|
+
return []
|
|
70
|
+
|
|
71
|
+
ext = os.path.splitext(file_path)[1].lower()
|
|
72
|
+
|
|
73
|
+
with open(file_path, 'r') as f:
|
|
74
|
+
content = f.read()
|
|
75
|
+
|
|
76
|
+
if ext == '.py':
|
|
77
|
+
return self._analyze_python(file_path, content)
|
|
78
|
+
elif ext in ('.js', '.jsx', '.ts', '.tsx'):
|
|
79
|
+
return self._analyze_js_ts(file_path, content)
|
|
80
|
+
else:
|
|
81
|
+
return self._analyze_generic(file_path, content)
|
|
82
|
+
|
|
83
|
+
def generate_tests(
|
|
84
|
+
self,
|
|
85
|
+
functions: List[FunctionInfo],
|
|
86
|
+
framework: Optional[TestFramework] = None
|
|
87
|
+
) -> List[GeneratedTest]:
|
|
88
|
+
"""Generate tests for functions
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
functions: List of functions to test
|
|
92
|
+
framework: Test framework to use (auto-detect if None)
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
List of generated tests
|
|
96
|
+
"""
|
|
97
|
+
if not self.llm:
|
|
98
|
+
raise RuntimeError("LLM adapter required for generating tests")
|
|
99
|
+
|
|
100
|
+
if not framework:
|
|
101
|
+
# Auto-detect based on first function's language
|
|
102
|
+
if functions:
|
|
103
|
+
ext = os.path.splitext(functions[0].file_path)[1].lower()
|
|
104
|
+
framework = self._detect_framework(ext)
|
|
105
|
+
else:
|
|
106
|
+
framework = TestFramework.PYTEST
|
|
107
|
+
|
|
108
|
+
tests = []
|
|
109
|
+
for func in functions:
|
|
110
|
+
test = self._generate_test(func, framework)
|
|
111
|
+
if test:
|
|
112
|
+
tests.append(test)
|
|
113
|
+
|
|
114
|
+
return tests
|
|
115
|
+
|
|
116
|
+
def generate_test_file(
|
|
117
|
+
self,
|
|
118
|
+
source_file: str,
|
|
119
|
+
output_file: Optional[str] = None,
|
|
120
|
+
framework: Optional[TestFramework] = None
|
|
121
|
+
) -> Tuple[str, List[GeneratedTest]]:
|
|
122
|
+
"""Generate a complete test file for a source file
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
source_file: Path to source file
|
|
126
|
+
output_file: Path for output test file (auto-generate if None)
|
|
127
|
+
framework: Test framework to use
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Tuple of (output_file_path, generated_tests)
|
|
131
|
+
"""
|
|
132
|
+
functions = self.analyze_file(source_file)
|
|
133
|
+
|
|
134
|
+
if not functions:
|
|
135
|
+
return "", []
|
|
136
|
+
|
|
137
|
+
# Auto-detect framework
|
|
138
|
+
ext = os.path.splitext(source_file)[1].lower()
|
|
139
|
+
if framework is None:
|
|
140
|
+
framework = self._detect_framework(ext)
|
|
141
|
+
|
|
142
|
+
# Generate output filename
|
|
143
|
+
if output_file is None:
|
|
144
|
+
output_file = self._generate_test_filename(source_file, framework)
|
|
145
|
+
|
|
146
|
+
# Generate tests
|
|
147
|
+
tests = self.generate_tests(functions, framework)
|
|
148
|
+
|
|
149
|
+
# Assemble test file
|
|
150
|
+
test_content = self._assemble_test_file(
|
|
151
|
+
source_file,
|
|
152
|
+
tests,
|
|
153
|
+
framework
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# Write test file
|
|
157
|
+
os.makedirs(os.path.dirname(output_file) or '.', exist_ok=True)
|
|
158
|
+
with open(output_file, 'w') as f:
|
|
159
|
+
f.write(test_content)
|
|
160
|
+
|
|
161
|
+
return output_file, tests
|
|
162
|
+
|
|
163
|
+
def _analyze_python(self, file_path: str, content: str) -> List[FunctionInfo]:
|
|
164
|
+
"""Analyze Python file"""
|
|
165
|
+
functions = []
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
tree = ast.parse(content)
|
|
169
|
+
except SyntaxError:
|
|
170
|
+
return []
|
|
171
|
+
|
|
172
|
+
for node in ast.walk(tree):
|
|
173
|
+
if isinstance(node, ast.FunctionDef) or isinstance(node, ast.AsyncFunctionDef):
|
|
174
|
+
# Skip private functions
|
|
175
|
+
if node.name.startswith('_') and not node.name.startswith('__'):
|
|
176
|
+
continue
|
|
177
|
+
|
|
178
|
+
# Get function signature
|
|
179
|
+
args = []
|
|
180
|
+
for arg in node.args.args:
|
|
181
|
+
args.append(arg.arg)
|
|
182
|
+
|
|
183
|
+
signature = f"{node.name}({', '.join(args)})"
|
|
184
|
+
|
|
185
|
+
# Get docstring
|
|
186
|
+
docstring = ast.get_docstring(node)
|
|
187
|
+
|
|
188
|
+
# Get code
|
|
189
|
+
start_line = node.lineno - 1
|
|
190
|
+
end_line = node.end_lineno
|
|
191
|
+
lines = content.split('\n')
|
|
192
|
+
code = '\n'.join(lines[start_line:end_line])
|
|
193
|
+
|
|
194
|
+
# Check if method
|
|
195
|
+
is_method = False
|
|
196
|
+
class_name = None
|
|
197
|
+
for parent in ast.walk(tree):
|
|
198
|
+
if isinstance(parent, ast.ClassDef):
|
|
199
|
+
if node in ast.walk(parent):
|
|
200
|
+
is_method = True
|
|
201
|
+
class_name = parent.name
|
|
202
|
+
break
|
|
203
|
+
|
|
204
|
+
functions.append(FunctionInfo(
|
|
205
|
+
name=node.name,
|
|
206
|
+
file_path=file_path,
|
|
207
|
+
line_number=node.lineno,
|
|
208
|
+
signature=signature,
|
|
209
|
+
docstring=docstring,
|
|
210
|
+
code=code,
|
|
211
|
+
is_async=isinstance(node, ast.AsyncFunctionDef),
|
|
212
|
+
is_method=is_method,
|
|
213
|
+
class_name=class_name
|
|
214
|
+
))
|
|
215
|
+
|
|
216
|
+
return functions
|
|
217
|
+
|
|
218
|
+
def _analyze_js_ts(self, file_path: str, content: str) -> List[FunctionInfo]:
|
|
219
|
+
"""Analyze JavaScript/TypeScript file"""
|
|
220
|
+
functions = []
|
|
221
|
+
|
|
222
|
+
# Regular function pattern
|
|
223
|
+
func_pattern = r'(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)'
|
|
224
|
+
|
|
225
|
+
for match in re.finditer(func_pattern, content):
|
|
226
|
+
name = match.group(1)
|
|
227
|
+
params = match.group(2)
|
|
228
|
+
start = match.start()
|
|
229
|
+
|
|
230
|
+
# Skip private functions
|
|
231
|
+
if name.startswith('_'):
|
|
232
|
+
continue
|
|
233
|
+
|
|
234
|
+
line_number = content[:start].count('\n') + 1
|
|
235
|
+
|
|
236
|
+
# Get function body (simplified)
|
|
237
|
+
brace_start = content.find('{', match.end())
|
|
238
|
+
if brace_start == -1:
|
|
239
|
+
continue
|
|
240
|
+
|
|
241
|
+
# Find matching closing brace
|
|
242
|
+
depth = 1
|
|
243
|
+
pos = brace_start + 1
|
|
244
|
+
while depth > 0 and pos < len(content):
|
|
245
|
+
if content[pos] == '{':
|
|
246
|
+
depth += 1
|
|
247
|
+
elif content[pos] == '}':
|
|
248
|
+
depth -= 1
|
|
249
|
+
pos += 1
|
|
250
|
+
|
|
251
|
+
code = content[match.start():pos]
|
|
252
|
+
|
|
253
|
+
functions.append(FunctionInfo(
|
|
254
|
+
name=name,
|
|
255
|
+
file_path=file_path,
|
|
256
|
+
line_number=line_number,
|
|
257
|
+
signature=f"{name}({params})",
|
|
258
|
+
docstring=None,
|
|
259
|
+
code=code,
|
|
260
|
+
is_async='async' in content[max(0, match.start()-10):match.start()],
|
|
261
|
+
is_method=False,
|
|
262
|
+
class_name=None
|
|
263
|
+
))
|
|
264
|
+
|
|
265
|
+
# Arrow function pattern
|
|
266
|
+
arrow_pattern = r'(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*=>'
|
|
267
|
+
|
|
268
|
+
for match in re.finditer(arrow_pattern, content):
|
|
269
|
+
name = match.group(1)
|
|
270
|
+
start = match.start()
|
|
271
|
+
|
|
272
|
+
if name.startswith('_'):
|
|
273
|
+
continue
|
|
274
|
+
|
|
275
|
+
line_number = content[:start].count('\n') + 1
|
|
276
|
+
|
|
277
|
+
# Get function body
|
|
278
|
+
arrow_pos = content.find('=>', match.end() - 10)
|
|
279
|
+
if arrow_pos == -1:
|
|
280
|
+
continue
|
|
281
|
+
|
|
282
|
+
# Find end of arrow function
|
|
283
|
+
pos = arrow_pos + 2
|
|
284
|
+
while pos < len(content) and content[pos] in ' \t\n':
|
|
285
|
+
pos += 1
|
|
286
|
+
|
|
287
|
+
if pos < len(content) and content[pos] == '{':
|
|
288
|
+
# Block body
|
|
289
|
+
depth = 1
|
|
290
|
+
pos += 1
|
|
291
|
+
while depth > 0 and pos < len(content):
|
|
292
|
+
if content[pos] == '{':
|
|
293
|
+
depth += 1
|
|
294
|
+
elif content[pos] == '}':
|
|
295
|
+
depth -= 1
|
|
296
|
+
pos += 1
|
|
297
|
+
else:
|
|
298
|
+
# Expression body - find end
|
|
299
|
+
pos = content.find('\n', pos)
|
|
300
|
+
if pos == -1:
|
|
301
|
+
pos = len(content)
|
|
302
|
+
|
|
303
|
+
code = content[match.start():pos]
|
|
304
|
+
|
|
305
|
+
functions.append(FunctionInfo(
|
|
306
|
+
name=name,
|
|
307
|
+
file_path=file_path,
|
|
308
|
+
line_number=line_number,
|
|
309
|
+
signature=name,
|
|
310
|
+
docstring=None,
|
|
311
|
+
code=code,
|
|
312
|
+
is_async='async' in content[max(0, match.start()-10):match.start()],
|
|
313
|
+
is_method=False,
|
|
314
|
+
class_name=None
|
|
315
|
+
))
|
|
316
|
+
|
|
317
|
+
return functions
|
|
318
|
+
|
|
319
|
+
def _analyze_generic(self, file_path: str, content: str) -> List[FunctionInfo]:
|
|
320
|
+
"""Generic function analysis"""
|
|
321
|
+
# Basic pattern for functions
|
|
322
|
+
func_pattern = r'(?:func|def|fn|function)\s+(\w+)\s*\([^)]*\)'
|
|
323
|
+
functions = []
|
|
324
|
+
|
|
325
|
+
for match in re.finditer(func_pattern, content):
|
|
326
|
+
name = match.group(1)
|
|
327
|
+
start = match.start()
|
|
328
|
+
line_number = content[:start].count('\n') + 1
|
|
329
|
+
|
|
330
|
+
functions.append(FunctionInfo(
|
|
331
|
+
name=name,
|
|
332
|
+
file_path=file_path,
|
|
333
|
+
line_number=line_number,
|
|
334
|
+
signature=name,
|
|
335
|
+
docstring=None,
|
|
336
|
+
code=match.group(0),
|
|
337
|
+
is_async=False,
|
|
338
|
+
is_method=False,
|
|
339
|
+
class_name=None
|
|
340
|
+
))
|
|
341
|
+
|
|
342
|
+
return functions
|
|
343
|
+
|
|
344
|
+
def _detect_framework(self, ext: str) -> TestFramework:
|
|
345
|
+
"""Detect appropriate test framework"""
|
|
346
|
+
mapping = {
|
|
347
|
+
'.py': TestFramework.PYTEST,
|
|
348
|
+
'.js': TestFramework.JEST,
|
|
349
|
+
'.jsx': TestFramework.JEST,
|
|
350
|
+
'.ts': TestFramework.JEST,
|
|
351
|
+
'.tsx': TestFramework.JEST,
|
|
352
|
+
'.go': TestFramework.GO_TEST,
|
|
353
|
+
'.rs': TestFramework.RUST_TEST,
|
|
354
|
+
}
|
|
355
|
+
return mapping.get(ext, TestFramework.PYTEST)
|
|
356
|
+
|
|
357
|
+
def _generate_test_filename(self, source_file: str, framework: TestFramework) -> str:
|
|
358
|
+
"""Generate test filename"""
|
|
359
|
+
base = os.path.basename(source_file)
|
|
360
|
+
name, ext = os.path.splitext(base)
|
|
361
|
+
dir_path = os.path.dirname(source_file)
|
|
362
|
+
|
|
363
|
+
if framework in (TestFramework.PYTEST, TestFramework.UNITTEST):
|
|
364
|
+
return os.path.join(dir_path, f"test_{name}{ext}")
|
|
365
|
+
elif framework in (TestFramework.JEST, TestFramework.VITEST, TestFramework.MOCHA):
|
|
366
|
+
return os.path.join(dir_path, f"{name}.test{ext}")
|
|
367
|
+
else:
|
|
368
|
+
return os.path.join(dir_path, f"{name}_test{ext}")
|
|
369
|
+
|
|
370
|
+
def _generate_test(self, func: FunctionInfo, framework: TestFramework) -> Optional[GeneratedTest]:
|
|
371
|
+
"""Generate a test for a function"""
|
|
372
|
+
from ..llm_adapter import TaskType
|
|
373
|
+
|
|
374
|
+
prompt = f"""Generate a unit test for this function.
|
|
375
|
+
|
|
376
|
+
Function: {func.signature}
|
|
377
|
+
File: {func.file_path}
|
|
378
|
+
{'Docstring: ' + func.docstring if func.docstring else ''}
|
|
379
|
+
|
|
380
|
+
Code:
|
|
381
|
+
```
|
|
382
|
+
{func.code}
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
Test Framework: {framework.value}
|
|
386
|
+
{'This is a method of class ' + func.class_name if func.is_method else ''}
|
|
387
|
+
{'This is an async function' if func.is_async else ''}
|
|
388
|
+
|
|
389
|
+
Generate a comprehensive test that:
|
|
390
|
+
1. Tests the main functionality
|
|
391
|
+
2. Tests edge cases
|
|
392
|
+
3. Tests error handling if applicable
|
|
393
|
+
|
|
394
|
+
Provide ONLY the test code, no explanations.
|
|
395
|
+
"""
|
|
396
|
+
|
|
397
|
+
response = self.llm.complete(prompt, task_type=TaskType.CODING, max_tokens=1000)
|
|
398
|
+
|
|
399
|
+
# Extract code from response
|
|
400
|
+
code = self._extract_code(response)
|
|
401
|
+
if not code:
|
|
402
|
+
return None
|
|
403
|
+
|
|
404
|
+
test_name = f"test_{func.name}"
|
|
405
|
+
|
|
406
|
+
return GeneratedTest(
|
|
407
|
+
function_name=func.name,
|
|
408
|
+
test_name=test_name,
|
|
409
|
+
test_code=code,
|
|
410
|
+
description=f"Tests for {func.signature}",
|
|
411
|
+
framework=framework
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
def _extract_code(self, response: str) -> Optional[str]:
|
|
415
|
+
"""Extract code from LLM response"""
|
|
416
|
+
code_match = re.search(r'```(?:\w+)?\n([\s\S]*?)```', response)
|
|
417
|
+
if code_match:
|
|
418
|
+
return code_match.group(1).strip()
|
|
419
|
+
return response.strip()
|
|
420
|
+
|
|
421
|
+
def _assemble_test_file(
|
|
422
|
+
self,
|
|
423
|
+
source_file: str,
|
|
424
|
+
tests: List[GeneratedTest],
|
|
425
|
+
framework: TestFramework
|
|
426
|
+
) -> str:
|
|
427
|
+
"""Assemble tests into a complete test file"""
|
|
428
|
+
module_name = os.path.splitext(os.path.basename(source_file))[0]
|
|
429
|
+
|
|
430
|
+
if framework == TestFramework.PYTEST:
|
|
431
|
+
header = f'''"""
|
|
432
|
+
Tests for {module_name}
|
|
433
|
+
Generated by NC1709
|
|
434
|
+
"""
|
|
435
|
+
import pytest
|
|
436
|
+
from {module_name} import *
|
|
437
|
+
|
|
438
|
+
'''
|
|
439
|
+
elif framework == TestFramework.UNITTEST:
|
|
440
|
+
header = f'''"""
|
|
441
|
+
Tests for {module_name}
|
|
442
|
+
Generated by NC1709
|
|
443
|
+
"""
|
|
444
|
+
import unittest
|
|
445
|
+
from {module_name} import *
|
|
446
|
+
|
|
447
|
+
'''
|
|
448
|
+
elif framework in (TestFramework.JEST, TestFramework.VITEST):
|
|
449
|
+
header = f'''/**
|
|
450
|
+
* Tests for {module_name}
|
|
451
|
+
* Generated by NC1709
|
|
452
|
+
*/
|
|
453
|
+
import {{ describe, it, expect }} from '{framework.value}';
|
|
454
|
+
import * as module from './{module_name}';
|
|
455
|
+
|
|
456
|
+
'''
|
|
457
|
+
else:
|
|
458
|
+
header = f"// Tests for {module_name}\n// Generated by NC1709\n\n"
|
|
459
|
+
|
|
460
|
+
test_code = '\n\n'.join(test.test_code for test in tests)
|
|
461
|
+
|
|
462
|
+
return header + test_code
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def generate_tests_command(file_path: str, output_path: Optional[str] = None) -> str:
|
|
466
|
+
"""Command-line interface for test generation
|
|
467
|
+
|
|
468
|
+
Args:
|
|
469
|
+
file_path: Path to source file
|
|
470
|
+
output_path: Path for output test file
|
|
471
|
+
|
|
472
|
+
Returns:
|
|
473
|
+
Summary of results
|
|
474
|
+
"""
|
|
475
|
+
from ..llm_adapter import LLMAdapter
|
|
476
|
+
|
|
477
|
+
agent = TestGeneratorAgent(LLMAdapter())
|
|
478
|
+
|
|
479
|
+
output = []
|
|
480
|
+
output.append(f"\n{'='*60}")
|
|
481
|
+
output.append(f"Test Generator: {file_path}")
|
|
482
|
+
output.append(f"{'='*60}\n")
|
|
483
|
+
|
|
484
|
+
# Analyze file
|
|
485
|
+
functions = agent.analyze_file(file_path)
|
|
486
|
+
|
|
487
|
+
if not functions:
|
|
488
|
+
output.append("No testable functions found!")
|
|
489
|
+
return '\n'.join(output)
|
|
490
|
+
|
|
491
|
+
output.append(f"Found {len(functions)} function(s):\n")
|
|
492
|
+
for func in functions:
|
|
493
|
+
output.append(f" - {func.signature} (line {func.line_number})")
|
|
494
|
+
|
|
495
|
+
# Generate tests
|
|
496
|
+
output_file, tests = agent.generate_test_file(file_path, output_path)
|
|
497
|
+
|
|
498
|
+
if tests:
|
|
499
|
+
output.append(f"\nGenerated {len(tests)} test(s):")
|
|
500
|
+
for test in tests:
|
|
501
|
+
output.append(f" - {test.test_name}")
|
|
502
|
+
|
|
503
|
+
output.append(f"\nTest file written to: {output_file}")
|
|
504
|
+
else:
|
|
505
|
+
output.append("\nNo tests could be generated.")
|
|
506
|
+
|
|
507
|
+
return '\n'.join(output)
|