tactus 0.31.0__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.
- tactus/__init__.py +49 -0
- tactus/adapters/__init__.py +9 -0
- tactus/adapters/broker_log.py +76 -0
- tactus/adapters/cli_hitl.py +189 -0
- tactus/adapters/cli_log.py +223 -0
- tactus/adapters/cost_collector_log.py +56 -0
- tactus/adapters/file_storage.py +367 -0
- tactus/adapters/http_callback_log.py +109 -0
- tactus/adapters/ide_log.py +71 -0
- tactus/adapters/lua_tools.py +336 -0
- tactus/adapters/mcp.py +289 -0
- tactus/adapters/mcp_manager.py +196 -0
- tactus/adapters/memory.py +53 -0
- tactus/adapters/plugins.py +419 -0
- tactus/backends/http_backend.py +58 -0
- tactus/backends/model_backend.py +35 -0
- tactus/backends/pytorch_backend.py +110 -0
- tactus/broker/__init__.py +12 -0
- tactus/broker/client.py +247 -0
- tactus/broker/protocol.py +183 -0
- tactus/broker/server.py +1123 -0
- tactus/broker/stdio.py +12 -0
- tactus/cli/__init__.py +7 -0
- tactus/cli/app.py +2245 -0
- tactus/cli/commands/__init__.py +0 -0
- tactus/core/__init__.py +32 -0
- tactus/core/config_manager.py +790 -0
- tactus/core/dependencies/__init__.py +14 -0
- tactus/core/dependencies/registry.py +180 -0
- tactus/core/dsl_stubs.py +2117 -0
- tactus/core/exceptions.py +66 -0
- tactus/core/execution_context.py +480 -0
- tactus/core/lua_sandbox.py +508 -0
- tactus/core/message_history_manager.py +236 -0
- tactus/core/mocking.py +286 -0
- tactus/core/output_validator.py +291 -0
- tactus/core/registry.py +499 -0
- tactus/core/runtime.py +2907 -0
- tactus/core/template_resolver.py +142 -0
- tactus/core/yaml_parser.py +301 -0
- tactus/docker/Dockerfile +61 -0
- tactus/docker/entrypoint.sh +69 -0
- tactus/dspy/__init__.py +39 -0
- tactus/dspy/agent.py +1144 -0
- tactus/dspy/broker_lm.py +181 -0
- tactus/dspy/config.py +212 -0
- tactus/dspy/history.py +196 -0
- tactus/dspy/module.py +405 -0
- tactus/dspy/prediction.py +318 -0
- tactus/dspy/signature.py +185 -0
- tactus/formatting/__init__.py +7 -0
- tactus/formatting/formatter.py +437 -0
- tactus/ide/__init__.py +9 -0
- tactus/ide/coding_assistant.py +343 -0
- tactus/ide/server.py +2223 -0
- tactus/primitives/__init__.py +49 -0
- tactus/primitives/control.py +168 -0
- tactus/primitives/file.py +229 -0
- tactus/primitives/handles.py +378 -0
- tactus/primitives/host.py +94 -0
- tactus/primitives/human.py +342 -0
- tactus/primitives/json.py +189 -0
- tactus/primitives/log.py +187 -0
- tactus/primitives/message_history.py +157 -0
- tactus/primitives/model.py +163 -0
- tactus/primitives/procedure.py +564 -0
- tactus/primitives/procedure_callable.py +318 -0
- tactus/primitives/retry.py +155 -0
- tactus/primitives/session.py +152 -0
- tactus/primitives/state.py +182 -0
- tactus/primitives/step.py +209 -0
- tactus/primitives/system.py +93 -0
- tactus/primitives/tool.py +375 -0
- tactus/primitives/tool_handle.py +279 -0
- tactus/primitives/toolset.py +229 -0
- tactus/protocols/__init__.py +38 -0
- tactus/protocols/chat_recorder.py +81 -0
- tactus/protocols/config.py +97 -0
- tactus/protocols/cost.py +31 -0
- tactus/protocols/hitl.py +71 -0
- tactus/protocols/log_handler.py +27 -0
- tactus/protocols/models.py +355 -0
- tactus/protocols/result.py +33 -0
- tactus/protocols/storage.py +90 -0
- tactus/providers/__init__.py +13 -0
- tactus/providers/base.py +92 -0
- tactus/providers/bedrock.py +117 -0
- tactus/providers/google.py +105 -0
- tactus/providers/openai.py +98 -0
- tactus/sandbox/__init__.py +63 -0
- tactus/sandbox/config.py +171 -0
- tactus/sandbox/container_runner.py +1099 -0
- tactus/sandbox/docker_manager.py +433 -0
- tactus/sandbox/entrypoint.py +227 -0
- tactus/sandbox/protocol.py +213 -0
- tactus/stdlib/__init__.py +10 -0
- tactus/stdlib/io/__init__.py +13 -0
- tactus/stdlib/io/csv.py +88 -0
- tactus/stdlib/io/excel.py +136 -0
- tactus/stdlib/io/file.py +90 -0
- tactus/stdlib/io/fs.py +154 -0
- tactus/stdlib/io/hdf5.py +121 -0
- tactus/stdlib/io/json.py +109 -0
- tactus/stdlib/io/parquet.py +83 -0
- tactus/stdlib/io/tsv.py +88 -0
- tactus/stdlib/loader.py +274 -0
- tactus/stdlib/tac/tactus/tools/done.tac +33 -0
- tactus/stdlib/tac/tactus/tools/log.tac +50 -0
- tactus/testing/README.md +273 -0
- tactus/testing/__init__.py +61 -0
- tactus/testing/behave_integration.py +380 -0
- tactus/testing/context.py +486 -0
- tactus/testing/eval_models.py +114 -0
- tactus/testing/evaluation_runner.py +222 -0
- tactus/testing/evaluators.py +634 -0
- tactus/testing/events.py +94 -0
- tactus/testing/gherkin_parser.py +134 -0
- tactus/testing/mock_agent.py +315 -0
- tactus/testing/mock_dependencies.py +234 -0
- tactus/testing/mock_hitl.py +171 -0
- tactus/testing/mock_registry.py +168 -0
- tactus/testing/mock_tools.py +133 -0
- tactus/testing/models.py +115 -0
- tactus/testing/pydantic_eval_runner.py +508 -0
- tactus/testing/steps/__init__.py +13 -0
- tactus/testing/steps/builtin.py +902 -0
- tactus/testing/steps/custom.py +69 -0
- tactus/testing/steps/registry.py +68 -0
- tactus/testing/test_runner.py +489 -0
- tactus/tracing/__init__.py +5 -0
- tactus/tracing/trace_manager.py +417 -0
- tactus/utils/__init__.py +1 -0
- tactus/utils/cost_calculator.py +72 -0
- tactus/utils/model_pricing.py +132 -0
- tactus/utils/safe_file_library.py +502 -0
- tactus/utils/safe_libraries.py +234 -0
- tactus/validation/LuaLexerBase.py +66 -0
- tactus/validation/LuaParserBase.py +23 -0
- tactus/validation/README.md +224 -0
- tactus/validation/__init__.py +7 -0
- tactus/validation/error_listener.py +21 -0
- tactus/validation/generated/LuaLexer.interp +231 -0
- tactus/validation/generated/LuaLexer.py +5548 -0
- tactus/validation/generated/LuaLexer.tokens +124 -0
- tactus/validation/generated/LuaLexerBase.py +66 -0
- tactus/validation/generated/LuaParser.interp +173 -0
- tactus/validation/generated/LuaParser.py +6439 -0
- tactus/validation/generated/LuaParser.tokens +124 -0
- tactus/validation/generated/LuaParserBase.py +23 -0
- tactus/validation/generated/LuaParserVisitor.py +118 -0
- tactus/validation/generated/__init__.py +7 -0
- tactus/validation/grammar/LuaLexer.g4 +123 -0
- tactus/validation/grammar/LuaParser.g4 +178 -0
- tactus/validation/semantic_visitor.py +817 -0
- tactus/validation/validator.py +157 -0
- tactus-0.31.0.dist-info/METADATA +1809 -0
- tactus-0.31.0.dist-info/RECORD +160 -0
- tactus-0.31.0.dist-info/WHEEL +4 -0
- tactus-0.31.0.dist-info/entry_points.txt +2 -0
- tactus-0.31.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Behave integration layer for Tactus BDD testing.
|
|
3
|
+
|
|
4
|
+
Generates Behave-compatible .feature files and step definitions
|
|
5
|
+
from parsed Gherkin and registered steps.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import tempfile
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
from .models import ParsedFeature, ParsedScenario
|
|
14
|
+
from .steps.registry import StepRegistry
|
|
15
|
+
from .steps.custom import CustomStepManager
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BehaveFeatureGenerator:
|
|
22
|
+
"""
|
|
23
|
+
Generates Behave-compatible .feature files from parsed Gherkin.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def generate(
|
|
27
|
+
self,
|
|
28
|
+
parsed_feature: ParsedFeature,
|
|
29
|
+
output_dir: Path,
|
|
30
|
+
filename: Optional[str] = None,
|
|
31
|
+
) -> Path:
|
|
32
|
+
"""
|
|
33
|
+
Generate .feature file from parsed Gherkin.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
parsed_feature: Parsed Gherkin feature
|
|
37
|
+
output_dir: Directory to write feature file
|
|
38
|
+
filename: Optional custom filename (defaults to feature name)
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Path to generated feature file
|
|
42
|
+
"""
|
|
43
|
+
if not filename:
|
|
44
|
+
# Sanitize feature name for filename
|
|
45
|
+
filename = parsed_feature.name.lower().replace(" ", "_") + ".feature"
|
|
46
|
+
|
|
47
|
+
feature_file = output_dir / filename
|
|
48
|
+
|
|
49
|
+
with open(feature_file, "w") as f:
|
|
50
|
+
# Write feature header
|
|
51
|
+
if parsed_feature.tags:
|
|
52
|
+
for tag in parsed_feature.tags:
|
|
53
|
+
f.write(f"@{tag}\n")
|
|
54
|
+
|
|
55
|
+
f.write(f"Feature: {parsed_feature.name}\n")
|
|
56
|
+
|
|
57
|
+
if parsed_feature.description:
|
|
58
|
+
# Indent description
|
|
59
|
+
for line in parsed_feature.description.split("\n"):
|
|
60
|
+
f.write(f" {line}\n")
|
|
61
|
+
f.write("\n")
|
|
62
|
+
|
|
63
|
+
# Write scenarios
|
|
64
|
+
for scenario in parsed_feature.scenarios:
|
|
65
|
+
self._write_scenario(f, scenario)
|
|
66
|
+
|
|
67
|
+
logger.info(f"Generated feature file: {feature_file}")
|
|
68
|
+
return feature_file
|
|
69
|
+
|
|
70
|
+
def _write_scenario(self, f, scenario: ParsedScenario) -> None:
|
|
71
|
+
"""Write a scenario to the feature file."""
|
|
72
|
+
# Write scenario tags
|
|
73
|
+
if scenario.tags:
|
|
74
|
+
f.write(" ")
|
|
75
|
+
for tag in scenario.tags:
|
|
76
|
+
f.write(f"@{tag} ")
|
|
77
|
+
f.write("\n")
|
|
78
|
+
|
|
79
|
+
# Add tag for filtering by scenario name
|
|
80
|
+
# Remove special characters that could interfere with behave tags
|
|
81
|
+
import re
|
|
82
|
+
|
|
83
|
+
sanitized_name = re.sub(r"[^a-z0-9_]", "_", scenario.name.lower())
|
|
84
|
+
sanitized_name = re.sub(r"_+", "_", sanitized_name) # Collapse multiple underscores
|
|
85
|
+
f.write(f" @scenario_{sanitized_name}\n")
|
|
86
|
+
|
|
87
|
+
f.write(f" Scenario: {scenario.name}\n")
|
|
88
|
+
|
|
89
|
+
# Write steps
|
|
90
|
+
for step in scenario.steps:
|
|
91
|
+
f.write(f" {step.keyword} {step.message}\n")
|
|
92
|
+
|
|
93
|
+
f.write("\n")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class BehaveStepsGenerator:
|
|
97
|
+
"""
|
|
98
|
+
Generates Python step definitions for Behave.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
def generate(
|
|
102
|
+
self,
|
|
103
|
+
step_registry: StepRegistry,
|
|
104
|
+
custom_steps: CustomStepManager,
|
|
105
|
+
output_dir: Path,
|
|
106
|
+
) -> Path:
|
|
107
|
+
"""
|
|
108
|
+
Generate step_definitions.py for Behave.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
step_registry: Registry of built-in steps
|
|
112
|
+
custom_steps: Manager for custom Lua steps
|
|
113
|
+
output_dir: Directory to write steps file
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Path to generated steps file
|
|
117
|
+
"""
|
|
118
|
+
steps_dir = output_dir / "steps"
|
|
119
|
+
steps_dir.mkdir(exist_ok=True)
|
|
120
|
+
|
|
121
|
+
# Use unique filename based on output_dir to prevent conflicts
|
|
122
|
+
import hashlib
|
|
123
|
+
|
|
124
|
+
dir_hash = hashlib.md5(str(output_dir).encode()).hexdigest()[:8]
|
|
125
|
+
steps_file = steps_dir / f"tactus_steps_{dir_hash}.py"
|
|
126
|
+
|
|
127
|
+
with open(steps_file, "w") as f:
|
|
128
|
+
# Write imports
|
|
129
|
+
f.write("from behave import step, use_step_matcher\n")
|
|
130
|
+
f.write("import sys\n")
|
|
131
|
+
f.write("from pathlib import Path\n\n")
|
|
132
|
+
|
|
133
|
+
# Use parse matcher instead of regex
|
|
134
|
+
f.write("# Use parse matcher for simpler patterns\n")
|
|
135
|
+
f.write("use_step_matcher('parse')\n\n")
|
|
136
|
+
|
|
137
|
+
# Add tactus to path
|
|
138
|
+
f.write("# Add tactus to path\n")
|
|
139
|
+
f.write("sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent))\n\n")
|
|
140
|
+
|
|
141
|
+
# Import all built-in step functions
|
|
142
|
+
f.write("from tactus.testing.steps import builtin\n")
|
|
143
|
+
f.write("from tactus.testing.context import TactusTestContext\n")
|
|
144
|
+
f.write("# Import mock steps for dependency mocking\n")
|
|
145
|
+
# Mock steps temporarily disabled due to pattern conflicts
|
|
146
|
+
# f.write("from tactus.testing.steps import mock_steps\n\n")
|
|
147
|
+
|
|
148
|
+
# Generate decorators for each built-in step pattern
|
|
149
|
+
# Map pattern to actual function (avoid duplicates)
|
|
150
|
+
pattern_to_func = {}
|
|
151
|
+
for pattern, func in step_registry._steps.items():
|
|
152
|
+
# Use pattern string as key to avoid duplicates
|
|
153
|
+
pattern_str = pattern.pattern
|
|
154
|
+
if pattern_str not in pattern_to_func:
|
|
155
|
+
pattern_to_func[pattern_str] = func.__name__
|
|
156
|
+
|
|
157
|
+
for pattern_str, func_name in pattern_to_func.items():
|
|
158
|
+
# Create a unique wrapper function name
|
|
159
|
+
wrapper_name = self._pattern_to_func_name(pattern_str)
|
|
160
|
+
|
|
161
|
+
# Convert regex pattern to parse pattern
|
|
162
|
+
# Replace (?P<name>...) with {name}
|
|
163
|
+
parse_pattern = self._regex_to_parse_pattern(pattern_str)
|
|
164
|
+
|
|
165
|
+
# Escape quotes in pattern for Python string
|
|
166
|
+
escaped_pattern = parse_pattern.replace("'", "\\'").replace('"', '\\"')
|
|
167
|
+
|
|
168
|
+
# Use @step() decorator which works for Given/When/Then/And/But
|
|
169
|
+
# This prevents duplicate step definition errors
|
|
170
|
+
f.write(f"@step('{escaped_pattern}')\n")
|
|
171
|
+
f.write(f"def {wrapper_name}(context, **kwargs):\n")
|
|
172
|
+
f.write(f' """Step: {escaped_pattern[:60]}"""\n')
|
|
173
|
+
f.write(" # Call the actual step function from builtin module\n")
|
|
174
|
+
f.write(f" builtin.{func_name}(context.tac, **kwargs)\n\n")
|
|
175
|
+
|
|
176
|
+
logger.info(f"Generated steps file: {steps_file}")
|
|
177
|
+
return steps_file
|
|
178
|
+
|
|
179
|
+
def _regex_to_parse_pattern(self, regex_pattern: str) -> str:
|
|
180
|
+
"""Convert regex pattern to parse pattern."""
|
|
181
|
+
import re
|
|
182
|
+
|
|
183
|
+
# Replace (?P<name>\w+) with {name:w}
|
|
184
|
+
pattern = re.sub(r"\(\?P<(\w+)>\\w\+\)", r"{\1:w}", regex_pattern)
|
|
185
|
+
# Replace (?P<name>\d+) with {name:d}
|
|
186
|
+
pattern = re.sub(r"\(\?P<(\w+)>\\d\+\)", r"{\1:d}", pattern)
|
|
187
|
+
# Replace (?P<name>.+) with {name}
|
|
188
|
+
pattern = re.sub(r"\(\?P<(\w+)>\.\+\)", r"{\1}", pattern)
|
|
189
|
+
# Replace (?P<name>-?\d+\.?\d*) with {name} (numeric patterns)
|
|
190
|
+
# The backslashes in the original regex need to be matched literally
|
|
191
|
+
pattern = re.sub(r"\(\?P<(\w+)>-\?\\\\d\+\\\.\?\\\\d\*\)", r"{\1}", pattern)
|
|
192
|
+
# Also handle the simpler form without escapes in the source string
|
|
193
|
+
pattern = re.sub(r"\(\?P<(\w+)>-\?\\d\+\\\.\?\\d\*\)", r"{\1}", pattern)
|
|
194
|
+
# Catch-all for any remaining named groups with complex patterns
|
|
195
|
+
pattern = re.sub(r"\(\?P<(\w+)>[^)]+\)", r"{\1}", pattern)
|
|
196
|
+
# Convert regex escapes to literals: \[ -> [, \] -> ]
|
|
197
|
+
pattern = pattern.replace(r"\[", "[").replace(r"\]", "]")
|
|
198
|
+
return pattern
|
|
199
|
+
|
|
200
|
+
def _pattern_to_func_name(self, pattern: str) -> str:
|
|
201
|
+
"""Convert regex pattern to valid Python function name."""
|
|
202
|
+
import re
|
|
203
|
+
import hashlib
|
|
204
|
+
|
|
205
|
+
# Remove regex special characters and convert to snake_case
|
|
206
|
+
name = re.sub(r"[^a-zA-Z0-9_]", "_", pattern)
|
|
207
|
+
name = re.sub(r"_+", "_", name) # Collapse multiple underscores
|
|
208
|
+
name = name.strip("_")
|
|
209
|
+
# Add hash to make unique across different temp directories
|
|
210
|
+
pattern_hash = hashlib.md5(pattern.encode()).hexdigest()[:8]
|
|
211
|
+
return f"step_{name[:40]}_{pattern_hash}" # Limit length + unique hash
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class BehaveEnvironmentGenerator:
|
|
215
|
+
"""
|
|
216
|
+
Generates environment.py for Behave with Tactus context setup.
|
|
217
|
+
"""
|
|
218
|
+
|
|
219
|
+
def generate(
|
|
220
|
+
self,
|
|
221
|
+
output_dir: Path,
|
|
222
|
+
procedure_file: Path,
|
|
223
|
+
mock_tools: Optional[Dict] = None,
|
|
224
|
+
params: Optional[Dict] = None,
|
|
225
|
+
mcp_servers: Optional[Dict] = None,
|
|
226
|
+
tool_paths: Optional[List[str]] = None,
|
|
227
|
+
mocked: bool = False,
|
|
228
|
+
) -> Path:
|
|
229
|
+
"""
|
|
230
|
+
Generate environment.py for Behave.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
output_dir: Directory to write environment file
|
|
234
|
+
procedure_file: Path to the procedure file being tested
|
|
235
|
+
mock_tools: Optional dict of tool_name -> mock_response
|
|
236
|
+
params: Optional dict of parameters to pass to procedure
|
|
237
|
+
mocked: Whether to use mocked dependencies
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
Path to generated environment file
|
|
241
|
+
"""
|
|
242
|
+
env_file = output_dir / "environment.py"
|
|
243
|
+
|
|
244
|
+
# Serialize mock_tools and params for embedding
|
|
245
|
+
import json
|
|
246
|
+
|
|
247
|
+
mock_tools_json = json.dumps(mock_tools or {}).replace("'", "\\'")
|
|
248
|
+
params_json = json.dumps(params or {}).replace("'", "\\'")
|
|
249
|
+
mcp_servers_json = json.dumps(mcp_servers or {}).replace("'", "\\'")
|
|
250
|
+
tool_paths_json = json.dumps(tool_paths or []).replace("'", "\\'")
|
|
251
|
+
|
|
252
|
+
# Convert procedure_file to absolute path so it works from temp behave directory
|
|
253
|
+
absolute_procedure_file = Path(procedure_file).resolve()
|
|
254
|
+
|
|
255
|
+
with open(env_file, "w") as f:
|
|
256
|
+
f.write('"""\n')
|
|
257
|
+
f.write("Behave environment for Tactus BDD testing.\n")
|
|
258
|
+
f.write('"""\n\n')
|
|
259
|
+
|
|
260
|
+
f.write("import sys\n")
|
|
261
|
+
f.write("import json\n")
|
|
262
|
+
f.write("from pathlib import Path\n\n")
|
|
263
|
+
|
|
264
|
+
f.write("# Add tactus to path\n")
|
|
265
|
+
f.write("sys.path.insert(0, str(Path(__file__).parent.parent.parent))\n\n")
|
|
266
|
+
|
|
267
|
+
f.write("from tactus.testing.context import TactusTestContext\n")
|
|
268
|
+
f.write("from tactus.testing.steps.registry import StepRegistry\n")
|
|
269
|
+
f.write("from tactus.testing.steps.builtin import register_builtin_steps\n")
|
|
270
|
+
f.write("from tactus.testing.steps.custom import CustomStepManager\n\n")
|
|
271
|
+
|
|
272
|
+
f.write("def before_all(context):\n")
|
|
273
|
+
f.write(' """Setup before all tests."""\n')
|
|
274
|
+
f.write(" # Initialize step registry\n")
|
|
275
|
+
f.write(" context.step_registry = StepRegistry()\n")
|
|
276
|
+
f.write(" register_builtin_steps(context.step_registry)\n")
|
|
277
|
+
f.write(" \n")
|
|
278
|
+
f.write(" # Initialize custom step manager\n")
|
|
279
|
+
f.write(" context.custom_steps = CustomStepManager()\n")
|
|
280
|
+
f.write(" \n")
|
|
281
|
+
f.write(" # Store test configuration (using absolute path)\n")
|
|
282
|
+
f.write(f" context.procedure_file = Path(r'{absolute_procedure_file}')\n")
|
|
283
|
+
f.write(f" context.mock_tools = json.loads('{mock_tools_json}')\n")
|
|
284
|
+
f.write(f" context.params = json.loads('{params_json}')\n")
|
|
285
|
+
f.write(f" context.mcp_servers = json.loads('{mcp_servers_json}')\n")
|
|
286
|
+
f.write(f" context.tool_paths = json.loads('{tool_paths_json}')\n")
|
|
287
|
+
f.write(f" context.mocked = {mocked}\n\n")
|
|
288
|
+
|
|
289
|
+
f.write("def before_scenario(context, scenario):\n")
|
|
290
|
+
f.write(' """Setup before each scenario."""\n')
|
|
291
|
+
f.write(" # Import mock registry for dependency mocking\n")
|
|
292
|
+
f.write(" from tactus.testing.mock_registry import UnifiedMockRegistry\n")
|
|
293
|
+
f.write(" from tactus.testing.mock_hitl import MockHITLHandler\n")
|
|
294
|
+
f.write(" \n")
|
|
295
|
+
f.write(" # Create fresh Tactus context for each scenario\n")
|
|
296
|
+
f.write(" context.tac = TactusTestContext(\n")
|
|
297
|
+
f.write(" procedure_file=context.procedure_file,\n")
|
|
298
|
+
f.write(" params=context.params,\n")
|
|
299
|
+
f.write(" mock_tools=context.mock_tools,\n")
|
|
300
|
+
f.write(" mcp_servers=context.mcp_servers,\n")
|
|
301
|
+
f.write(" tool_paths=context.tool_paths,\n")
|
|
302
|
+
f.write(" mocked=context.mocked,\n")
|
|
303
|
+
f.write(" )\n")
|
|
304
|
+
f.write(" \n")
|
|
305
|
+
f.write(" # Create mock registry for Gherkin steps to configure\n")
|
|
306
|
+
f.write(" if context.mocked:\n")
|
|
307
|
+
f.write(
|
|
308
|
+
" context.mock_registry = UnifiedMockRegistry(hitl_handler=MockHITLHandler())\n"
|
|
309
|
+
)
|
|
310
|
+
f.write(" # Share mock registry with TactusTestContext\n")
|
|
311
|
+
f.write(" context.tac.mock_registry = context.mock_registry\n\n")
|
|
312
|
+
|
|
313
|
+
f.write("def after_scenario(context, scenario):\n")
|
|
314
|
+
f.write(' """Cleanup after each scenario."""\n')
|
|
315
|
+
f.write(" # Attach execution metrics to scenario for reporting\n")
|
|
316
|
+
f.write(" if hasattr(context, 'tac') and context.tac:\n")
|
|
317
|
+
f.write(" scenario.total_cost = getattr(context.tac, 'total_cost', 0.0)\n")
|
|
318
|
+
f.write(" scenario.total_tokens = getattr(context.tac, 'total_tokens', 0)\n")
|
|
319
|
+
f.write(
|
|
320
|
+
" scenario.cost_breakdown = getattr(context.tac, 'cost_breakdown', [])\n"
|
|
321
|
+
)
|
|
322
|
+
f.write(" scenario.iterations = getattr(context.tac, 'iterations', 0)\n")
|
|
323
|
+
f.write(" scenario.tools_used = getattr(context.tac, 'tools_used', [])\n")
|
|
324
|
+
f.write(" # Cleanup runtime if it was created\n")
|
|
325
|
+
f.write(" if hasattr(context.tac, 'runtime') and context.tac.runtime:\n")
|
|
326
|
+
f.write(" # Any cleanup needed\n")
|
|
327
|
+
f.write(" pass\n")
|
|
328
|
+
|
|
329
|
+
logger.info(f"Generated environment file: {env_file}")
|
|
330
|
+
return env_file
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def setup_behave_directory(
|
|
334
|
+
parsed_feature: ParsedFeature,
|
|
335
|
+
step_registry: StepRegistry,
|
|
336
|
+
custom_steps: CustomStepManager,
|
|
337
|
+
procedure_file: Path,
|
|
338
|
+
work_dir: Optional[Path] = None,
|
|
339
|
+
mock_tools: Optional[Dict] = None,
|
|
340
|
+
params: Optional[Dict] = None,
|
|
341
|
+
mcp_servers: Optional[Dict] = None,
|
|
342
|
+
tool_paths: Optional[List[str]] = None,
|
|
343
|
+
mocked: bool = False,
|
|
344
|
+
) -> Path:
|
|
345
|
+
"""
|
|
346
|
+
Setup complete Behave directory structure.
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
parsed_feature: Parsed Gherkin feature
|
|
350
|
+
step_registry: Registry of built-in steps
|
|
351
|
+
custom_steps: Custom step manager
|
|
352
|
+
procedure_file: Path to procedure file being tested
|
|
353
|
+
work_dir: Optional work directory (creates temp if not provided)
|
|
354
|
+
mock_tools: Optional dict of tool mocks
|
|
355
|
+
params: Optional dict of procedure parameters
|
|
356
|
+
mocked: Whether to use mocked dependencies
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
Path to Behave work directory
|
|
360
|
+
"""
|
|
361
|
+
if work_dir is None:
|
|
362
|
+
# Always create a unique temp directory
|
|
363
|
+
work_dir = Path(tempfile.mkdtemp(prefix="tactus_behave_"))
|
|
364
|
+
else:
|
|
365
|
+
work_dir.mkdir(parents=True, exist_ok=True)
|
|
366
|
+
|
|
367
|
+
# Generate feature file
|
|
368
|
+
feature_gen = BehaveFeatureGenerator()
|
|
369
|
+
feature_gen.generate(parsed_feature, work_dir)
|
|
370
|
+
|
|
371
|
+
# Generate step definitions
|
|
372
|
+
steps_gen = BehaveStepsGenerator()
|
|
373
|
+
steps_gen.generate(step_registry, custom_steps, work_dir)
|
|
374
|
+
|
|
375
|
+
# Generate environment.py with mock tools, params, and mocked flag
|
|
376
|
+
env_gen = BehaveEnvironmentGenerator()
|
|
377
|
+
env_gen.generate(work_dir, procedure_file, mock_tools, params, mcp_servers, tool_paths, mocked)
|
|
378
|
+
|
|
379
|
+
logger.info(f"Behave directory setup complete: {work_dir}")
|
|
380
|
+
return work_dir
|