ref-agents 1.0.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.
- ref_agents/__init__.py +9 -0
- ref_agents/api_keys.json.example +8 -0
- ref_agents/auth.py +129 -0
- ref_agents/codemap/..md +62 -0
- ref_agents/codemap/CODE_MAP.md +37 -0
- ref_agents/codemap/core.md +43 -0
- ref_agents/codemap/models.md +43 -0
- ref_agents/codemap/prompts.md +40 -0
- ref_agents/codemap/security.md +45 -0
- ref_agents/codemap/tools.md +94 -0
- ref_agents/codemap/tools_browser.md +44 -0
- ref_agents/codemap/utils.md +42 -0
- ref_agents/codemap/workflow.md +42 -0
- ref_agents/config/ai_patterns.yaml +101 -0
- ref_agents/config/frameworks/angular.yaml +104 -0
- ref_agents/config/frameworks/aspnet.yaml +84 -0
- ref_agents/config/frameworks/ef_core.yaml +81 -0
- ref_agents/config/frameworks/react.yaml +111 -0
- ref_agents/config/frameworks/spring_boot.yaml +117 -0
- ref_agents/config/languages/csharp.yaml +153 -0
- ref_agents/config/languages/java.yaml +188 -0
- ref_agents/config/languages/javascript.yaml +172 -0
- ref_agents/config/languages/python.yaml +153 -0
- ref_agents/config/languages/typescript.yaml +193 -0
- ref_agents/constants.py +553 -0
- ref_agents/core/__init__.py +15 -0
- ref_agents/core/config_loader.py +160 -0
- ref_agents/core/config_models.py +167 -0
- ref_agents/core/config_parsing.py +84 -0
- ref_agents/core/language_detector.py +388 -0
- ref_agents/core/validation_models.py +66 -0
- ref_agents/core/validation_primitives.py +176 -0
- ref_agents/errors.py +34 -0
- ref_agents/license_client.py +247 -0
- ref_agents/models/__init__.py +22 -0
- ref_agents/models/gherkin.py +45 -0
- ref_agents/models/hierarchy.py +80 -0
- ref_agents/models/invest.py +59 -0
- ref_agents/models/version.py +49 -0
- ref_agents/prompts/__init__.py +9 -0
- ref_agents/prompts/start_agent.py +772 -0
- ref_agents/rules/architecture/backend_patterns.md +43 -0
- ref_agents/rules/architecture/diagramming.md +100 -0
- ref_agents/rules/architecture/frontend_patterns.md +40 -0
- ref_agents/rules/architecture/impact_analysis.md +129 -0
- ref_agents/rules/architecture/migration_strategy.md +208 -0
- ref_agents/rules/architecture/regression_protocol.md +77 -0
- ref_agents/rules/architecture/system_design.md +97 -0
- ref_agents/rules/common/codemap_standard.md +97 -0
- ref_agents/rules/common/core_protocol.md +59 -0
- ref_agents/rules/common/prompt_engineering.md +294 -0
- ref_agents/rules/development/debugging.md +32 -0
- ref_agents/rules/development/implementation.md +205 -0
- ref_agents/rules/operations/completion.md +119 -0
- ref_agents/rules/operations/cutover_protocol.md +218 -0
- ref_agents/rules/operations/discovery.md +179 -0
- ref_agents/rules/operations/fix_workflow.md +87 -0
- ref_agents/rules/operations/forensics.md +278 -0
- ref_agents/rules/operations/platform.md +263 -0
- ref_agents/rules/operations/synchronous_flow.md +25 -0
- ref_agents/rules/product/ac_validation.md +25 -0
- ref_agents/rules/product/brainstorming.md +27 -0
- ref_agents/rules/product/ref_flow.md +101 -0
- ref_agents/rules/product/requirements_std.md +114 -0
- ref_agents/rules/product/spec_writing.md +235 -0
- ref_agents/rules/product/strategy.md +96 -0
- ref_agents/rules/quality/documentation_standards.md +46 -0
- ref_agents/rules/quality/parity_testing.md +234 -0
- ref_agents/rules/quality/project_documentation.md +56 -0
- ref_agents/rules/quality/qa_lead.md +111 -0
- ref_agents/rules/quality/test_design.md +146 -0
- ref_agents/rules/quality/testing_standards.md +293 -0
- ref_agents/rules/review/pr_review.md +116 -0
- ref_agents/rules/security/security_audit.md +83 -0
- ref_agents/security/__init__.py +22 -0
- ref_agents/security/dependency_audit.py +188 -0
- ref_agents/security/file_audit.py +208 -0
- ref_agents/security/network_scan.py +179 -0
- ref_agents/security/report_generator.py +313 -0
- ref_agents/security/secret_scan.py +252 -0
- ref_agents/security/url_scan.py +240 -0
- ref_agents/security_scan.py +236 -0
- ref_agents/server.py +1586 -0
- ref_agents/session.py +100 -0
- ref_agents/tool_names.py +55 -0
- ref_agents/tools/__init__.py +8 -0
- ref_agents/tools/agents_generator.py +315 -0
- ref_agents/tools/ai_pattern_detector.py +815 -0
- ref_agents/tools/brownfield_populator.py +529 -0
- ref_agents/tools/browser/__init__.py +50 -0
- ref_agents/tools/browser/evidence_verifier.py +302 -0
- ref_agents/tools/browser/execution_logger.py +249 -0
- ref_agents/tools/browser/playwright_mcp_client.py +259 -0
- ref_agents/tools/browser/screenshot_utils.py +184 -0
- ref_agents/tools/browser/test_executor.py +537 -0
- ref_agents/tools/code_quality_scanner.py +629 -0
- ref_agents/tools/codemap/..md +93 -0
- ref_agents/tools/codemap/CODE_MAP.md +30 -0
- ref_agents/tools/codemap/browser.md +44 -0
- ref_agents/tools/codemap.py +403 -0
- ref_agents/tools/codemap_freshness.py +234 -0
- ref_agents/tools/comment_smell_scanner.py +346 -0
- ref_agents/tools/complexity.py +436 -0
- ref_agents/tools/complexity_ast.py +333 -0
- ref_agents/tools/compliance.py +246 -0
- ref_agents/tools/compliance_remediation.py +846 -0
- ref_agents/tools/context_graph.py +839 -0
- ref_agents/tools/context_manager.py +550 -0
- ref_agents/tools/context_tools.py +121 -0
- ref_agents/tools/cross_repo_linker.py +393 -0
- ref_agents/tools/dead_code_scanner.py +637 -0
- ref_agents/tools/debt_scanner.py +1092 -0
- ref_agents/tools/dependency_graph.py +272 -0
- ref_agents/tools/discovery_audit.py +372 -0
- ref_agents/tools/docs_scanner.py +600 -0
- ref_agents/tools/evaluate_gate.py +119 -0
- ref_agents/tools/external_detector.py +524 -0
- ref_agents/tools/features_generator.py +282 -0
- ref_agents/tools/flow_gap_detector.py +373 -0
- ref_agents/tools/flow_mapper.py +327 -0
- ref_agents/tools/full_suite_runner.py +740 -0
- ref_agents/tools/gherkin_parser.py +227 -0
- ref_agents/tools/guard_tools.py +139 -0
- ref_agents/tools/handoff_tools.py +282 -0
- ref_agents/tools/health_scanner.py +1211 -0
- ref_agents/tools/hierarchy_manager.py +289 -0
- ref_agents/tools/invest_scorer.py +249 -0
- ref_agents/tools/jira_confluence_export.py +306 -0
- ref_agents/tools/json_output.py +76 -0
- ref_agents/tools/migration_mapper.py +946 -0
- ref_agents/tools/migration_readiness_scanner.py +209 -0
- ref_agents/tools/pattern_learner.py +522 -0
- ref_agents/tools/report_utils.py +155 -0
- ref_agents/tools/requirements_serializer.py +225 -0
- ref_agents/tools/security_audit_tool.py +106 -0
- ref_agents/tools/sequencing_engine.py +288 -0
- ref_agents/tools/summary_generator.py +275 -0
- ref_agents/tools/symbol_resolver.py +306 -0
- ref_agents/tools/symbol_smoke_runner.py +336 -0
- ref_agents/tools/test_plan_validator.py +189 -0
- ref_agents/tools/test_smell_walker.py +902 -0
- ref_agents/tools/tier1_fixer.py +502 -0
- ref_agents/tools/validators/__init__.py +419 -0
- ref_agents/tools/validators/architect.py +268 -0
- ref_agents/tools/validators/cutover_engineer.py +167 -0
- ref_agents/tools/validators/developer.py +180 -0
- ref_agents/tools/validators/discovery.py +150 -0
- ref_agents/tools/validators/forensic_engineer.py +191 -0
- ref_agents/tools/validators/impact_architect.py +181 -0
- ref_agents/tools/validators/migration_planner.py +166 -0
- ref_agents/tools/validators/parity_tester.py +155 -0
- ref_agents/tools/validators/platform_engineer.py +134 -0
- ref_agents/tools/validators/pr_reviewer.py +129 -0
- ref_agents/tools/validators/product_manager.py +291 -0
- ref_agents/tools/validators/qa_lead.py +172 -0
- ref_agents/tools/validators/scrum_master.py +212 -0
- ref_agents/tools/validators/security_owner.py +162 -0
- ref_agents/tools/validators/specifier.py +134 -0
- ref_agents/tools/validators/strategist.py +149 -0
- ref_agents/tools/validators/tester.py +121 -0
- ref_agents/tools/version_manager.py +202 -0
- ref_agents/tools/workflow_tools.py +1549 -0
- ref_agents/utils/__init__.py +21 -0
- ref_agents/utils/git_utils.py +351 -0
- ref_agents/utils/handoff_logger.py +368 -0
- ref_agents/utils/ignore_matcher.py +270 -0
- ref_agents/workflow/__init__.py +19 -0
- ref_agents/workflow/capabilities.py +328 -0
- ref_agents/workflow/state_machine.py +708 -0
- ref_agents/workflow/transitions.py +658 -0
- ref_agents-1.0.0.dist-info/METADATA +365 -0
- ref_agents-1.0.0.dist-info/RECORD +175 -0
- ref_agents-1.0.0.dist-info/WHEEL +4 -0
- ref_agents-1.0.0.dist-info/entry_points.txt +2 -0
- ref_agents-1.0.0.dist-info/licenses/LICENSE +115 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"""Evidence verifier for browser test results.
|
|
2
|
+
|
|
3
|
+
Verifies all test evidence (screenshots, logs, timestamps)
|
|
4
|
+
to ensure test results are not hallucinated.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import structlog
|
|
12
|
+
|
|
13
|
+
from ref_agents.tools.browser.execution_logger import ExecutionLogger
|
|
14
|
+
from ref_agents.tools.browser.screenshot_utils import (
|
|
15
|
+
get_screenshot_dir,
|
|
16
|
+
get_screenshot_mtime,
|
|
17
|
+
verify_screenshot_content,
|
|
18
|
+
verify_screenshot_exists,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
logger = structlog.get_logger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class VerificationResult:
|
|
26
|
+
"""Result of evidence verification.
|
|
27
|
+
|
|
28
|
+
Attributes:
|
|
29
|
+
valid: Whether all evidence is valid.
|
|
30
|
+
issues: List of issues found.
|
|
31
|
+
screenshot_count: Number of screenshots verified.
|
|
32
|
+
log_entry_count: Number of log entries verified.
|
|
33
|
+
warnings: Non-critical issues.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
valid: bool
|
|
37
|
+
issues: list[str] = field(default_factory=list)
|
|
38
|
+
screenshot_count: int = 0
|
|
39
|
+
log_entry_count: int = 0
|
|
40
|
+
warnings: list[str] = field(default_factory=list)
|
|
41
|
+
|
|
42
|
+
def to_dict(self) -> dict:
|
|
43
|
+
"""Convert to dictionary."""
|
|
44
|
+
return {
|
|
45
|
+
"valid": self.valid,
|
|
46
|
+
"issues": self.issues,
|
|
47
|
+
"screenshot_count": self.screenshot_count,
|
|
48
|
+
"log_entry_count": self.log_entry_count,
|
|
49
|
+
"warnings": self.warnings,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class EvidenceVerifier:
|
|
54
|
+
"""Verify test execution evidence.
|
|
55
|
+
|
|
56
|
+
Ensures test results are backed by real evidence:
|
|
57
|
+
- Screenshots exist and are valid
|
|
58
|
+
- Execution log exists and is consistent
|
|
59
|
+
- Timestamps are sequential and realistic
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def __init__(self, story_id: str, base_dir: Path | None = None):
|
|
63
|
+
"""Initialize evidence verifier.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
story_id: Story identifier.
|
|
67
|
+
base_dir: Base directory for evidence files.
|
|
68
|
+
"""
|
|
69
|
+
self.story_id = story_id
|
|
70
|
+
|
|
71
|
+
if base_dir is None:
|
|
72
|
+
from ref_agents.session import SessionManager
|
|
73
|
+
|
|
74
|
+
session = SessionManager.get()
|
|
75
|
+
base_dir = session.project_root or Path.cwd()
|
|
76
|
+
|
|
77
|
+
self.base_dir = base_dir
|
|
78
|
+
self.execution_logger = ExecutionLogger(story_id, base_dir)
|
|
79
|
+
self.screenshot_dir = get_screenshot_dir(story_id, base_dir)
|
|
80
|
+
|
|
81
|
+
def verify_screenshot(self, path: Path) -> tuple[bool, str]:
|
|
82
|
+
"""Verify a single screenshot file.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
path: Path to screenshot file.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Tuple of (valid, error_message).
|
|
89
|
+
"""
|
|
90
|
+
# First check existence
|
|
91
|
+
valid, error = verify_screenshot_exists(path)
|
|
92
|
+
if not valid:
|
|
93
|
+
return False, error
|
|
94
|
+
|
|
95
|
+
# Then check content
|
|
96
|
+
valid, error = verify_screenshot_content(path)
|
|
97
|
+
if not valid:
|
|
98
|
+
return False, error
|
|
99
|
+
|
|
100
|
+
return True, ""
|
|
101
|
+
|
|
102
|
+
def verify_all_screenshots(self) -> tuple[bool, list[str], int]:
|
|
103
|
+
"""Verify all screenshots in the screenshot directory.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
Tuple of (all_valid, issues, count).
|
|
107
|
+
"""
|
|
108
|
+
issues = []
|
|
109
|
+
count = 0
|
|
110
|
+
|
|
111
|
+
if not self.screenshot_dir.exists():
|
|
112
|
+
return False, ["Screenshot directory does not exist"], 0
|
|
113
|
+
|
|
114
|
+
screenshots = list(self.screenshot_dir.glob("*.png"))
|
|
115
|
+
|
|
116
|
+
if not screenshots:
|
|
117
|
+
return False, ["No screenshots found"], 0
|
|
118
|
+
|
|
119
|
+
for screenshot in screenshots:
|
|
120
|
+
valid, error = self.verify_screenshot(screenshot)
|
|
121
|
+
if valid:
|
|
122
|
+
count += 1
|
|
123
|
+
else:
|
|
124
|
+
issues.append(error)
|
|
125
|
+
|
|
126
|
+
return len(issues) == 0, issues, count
|
|
127
|
+
|
|
128
|
+
def verify_log_integrity(self) -> tuple[bool, list[str]]:
|
|
129
|
+
"""Verify execution log is valid.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Tuple of (valid, issues).
|
|
133
|
+
"""
|
|
134
|
+
return self.execution_logger.verify_log_integrity()
|
|
135
|
+
|
|
136
|
+
def verify_timestamp_consistency(self) -> tuple[bool, list[str]]:
|
|
137
|
+
"""Verify timestamps are consistent.
|
|
138
|
+
|
|
139
|
+
Checks:
|
|
140
|
+
- Log timestamps are sequential
|
|
141
|
+
- Log timestamps are realistic (not too fast/slow)
|
|
142
|
+
- Screenshot mtimes are close to log timestamps
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
Tuple of (valid, issues).
|
|
146
|
+
"""
|
|
147
|
+
issues = []
|
|
148
|
+
entries = self.execution_logger.read_log()
|
|
149
|
+
|
|
150
|
+
if not entries:
|
|
151
|
+
return False, ["No log entries to verify"]
|
|
152
|
+
|
|
153
|
+
prev_timestamp = None
|
|
154
|
+
|
|
155
|
+
for entry in entries:
|
|
156
|
+
try:
|
|
157
|
+
timestamp = datetime.fromisoformat(entry.timestamp)
|
|
158
|
+
|
|
159
|
+
# Check sequential
|
|
160
|
+
if prev_timestamp:
|
|
161
|
+
if timestamp < prev_timestamp:
|
|
162
|
+
issues.append(
|
|
163
|
+
f"Timestamp not sequential: {entry.test_id} step {entry.step_number}"
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# Check realistic duration (between 50ms and 5 min)
|
|
167
|
+
duration = (timestamp - prev_timestamp).total_seconds()
|
|
168
|
+
if duration < 0.05: # noqa: PLR2004 # TECH_DEBT: extract to named constant — G25
|
|
169
|
+
issues.append(
|
|
170
|
+
f"Suspiciously fast execution: {duration:.3f}s for {entry.test_id} step {entry.step_number}"
|
|
171
|
+
)
|
|
172
|
+
elif duration > 300: # noqa: PLR2004 # TECH_DEBT: extract to named constant — G25
|
|
173
|
+
issues.append(
|
|
174
|
+
f"Suspiciously slow execution: {duration:.1f}s for {entry.test_id} step {entry.step_number}"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# Check screenshot timestamp if available
|
|
178
|
+
if entry.screenshot_path:
|
|
179
|
+
screenshot_path = Path(entry.screenshot_path)
|
|
180
|
+
if screenshot_path.exists():
|
|
181
|
+
try:
|
|
182
|
+
mtime = get_screenshot_mtime(screenshot_path)
|
|
183
|
+
screenshot_time = datetime.fromtimestamp(mtime)
|
|
184
|
+
time_diff = abs(
|
|
185
|
+
(timestamp - screenshot_time).total_seconds()
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
if (
|
|
189
|
+
time_diff > 10 # noqa: PLR2004 # TECH_DEBT: extract to named constant — G25
|
|
190
|
+
): # More than 10 seconds difference # noqa: PLR2004 # TECH_DEBT: extract to named constant — G25
|
|
191
|
+
issues.append(
|
|
192
|
+
f"Screenshot mtime mismatch: {time_diff:.1f}s diff for {entry.test_id} step {entry.step_number}"
|
|
193
|
+
)
|
|
194
|
+
except FileNotFoundError:
|
|
195
|
+
pass # Already caught by screenshot verification
|
|
196
|
+
|
|
197
|
+
prev_timestamp = timestamp
|
|
198
|
+
|
|
199
|
+
except ValueError as e:
|
|
200
|
+
issues.append(f"Invalid timestamp: {entry.timestamp} - {e}")
|
|
201
|
+
|
|
202
|
+
return len(issues) == 0, issues
|
|
203
|
+
|
|
204
|
+
def verify_log_screenshot_consistency(self) -> tuple[bool, list[str]]:
|
|
205
|
+
"""Verify log entries have corresponding screenshots.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
Tuple of (valid, issues).
|
|
209
|
+
"""
|
|
210
|
+
issues = []
|
|
211
|
+
entries = self.execution_logger.read_log()
|
|
212
|
+
|
|
213
|
+
for entry in entries:
|
|
214
|
+
if entry.screenshot_path:
|
|
215
|
+
screenshot = Path(entry.screenshot_path)
|
|
216
|
+
if not screenshot.exists():
|
|
217
|
+
issues.append(
|
|
218
|
+
f"Screenshot missing: {entry.screenshot_path} for {entry.test_id} step {entry.step_number}"
|
|
219
|
+
)
|
|
220
|
+
elif screenshot.stat().st_size != entry.screenshot_size:
|
|
221
|
+
issues.append(
|
|
222
|
+
f"Screenshot size mismatch: {entry.test_id} step {entry.step_number}"
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
return len(issues) == 0, issues
|
|
226
|
+
|
|
227
|
+
def cross_validate(self) -> VerificationResult:
|
|
228
|
+
"""Cross-validate all evidence sources.
|
|
229
|
+
|
|
230
|
+
Performs comprehensive verification:
|
|
231
|
+
1. Verify all screenshots exist and are valid
|
|
232
|
+
2. Verify execution log integrity
|
|
233
|
+
3. Verify timestamp consistency
|
|
234
|
+
4. Verify log-screenshot consistency
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
VerificationResult with all findings.
|
|
238
|
+
"""
|
|
239
|
+
all_issues = []
|
|
240
|
+
warnings = []
|
|
241
|
+
|
|
242
|
+
# 1. Verify screenshots
|
|
243
|
+
screenshots_valid, screenshot_issues, screenshot_count = (
|
|
244
|
+
self.verify_all_screenshots()
|
|
245
|
+
)
|
|
246
|
+
if not screenshots_valid:
|
|
247
|
+
all_issues.extend(screenshot_issues)
|
|
248
|
+
|
|
249
|
+
# 2. Verify log integrity
|
|
250
|
+
log_valid, log_issues = self.verify_log_integrity()
|
|
251
|
+
if not log_valid:
|
|
252
|
+
all_issues.extend(log_issues)
|
|
253
|
+
|
|
254
|
+
log_entry_count = self.execution_logger.get_entry_count()
|
|
255
|
+
|
|
256
|
+
# 3. Verify timestamp consistency
|
|
257
|
+
timestamp_valid, timestamp_issues = self.verify_timestamp_consistency()
|
|
258
|
+
if not timestamp_valid:
|
|
259
|
+
# Timestamp issues are warnings, not critical
|
|
260
|
+
warnings.extend(timestamp_issues)
|
|
261
|
+
|
|
262
|
+
# 4. Verify log-screenshot consistency
|
|
263
|
+
consistency_valid, consistency_issues = self.verify_log_screenshot_consistency()
|
|
264
|
+
if not consistency_valid:
|
|
265
|
+
all_issues.extend(consistency_issues)
|
|
266
|
+
|
|
267
|
+
valid = len(all_issues) == 0
|
|
268
|
+
|
|
269
|
+
result = VerificationResult(
|
|
270
|
+
valid=valid,
|
|
271
|
+
issues=all_issues,
|
|
272
|
+
screenshot_count=screenshot_count,
|
|
273
|
+
log_entry_count=log_entry_count,
|
|
274
|
+
warnings=warnings,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
logger.info(
|
|
278
|
+
"evidence_verification_complete",
|
|
279
|
+
valid=valid,
|
|
280
|
+
screenshot_count=screenshot_count,
|
|
281
|
+
log_entry_count=log_entry_count,
|
|
282
|
+
issue_count=len(all_issues),
|
|
283
|
+
warning_count=len(warnings),
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
return result
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def verify_test_evidence(
|
|
290
|
+
story_id: str, base_dir: Path | None = None
|
|
291
|
+
) -> VerificationResult:
|
|
292
|
+
"""Verify all test evidence for a story.
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
story_id: Story identifier.
|
|
296
|
+
base_dir: Base directory for evidence files.
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
VerificationResult with all findings.
|
|
300
|
+
"""
|
|
301
|
+
verifier = EvidenceVerifier(story_id, base_dir)
|
|
302
|
+
return verifier.cross_validate()
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""Execution logger for browser test audit trail.
|
|
2
|
+
|
|
3
|
+
Logs all MCP tool calls with timestamps, arguments, responses,
|
|
4
|
+
and screenshot paths for complete audit trail.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from dataclasses import asdict, dataclass, field
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import structlog
|
|
13
|
+
|
|
14
|
+
logger = structlog.get_logger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class ExecutionLogEntry:
|
|
19
|
+
"""Single execution log entry.
|
|
20
|
+
|
|
21
|
+
Attributes:
|
|
22
|
+
timestamp: ISO8601 timestamp of execution.
|
|
23
|
+
test_id: Test case identifier.
|
|
24
|
+
step_number: Step number within test case.
|
|
25
|
+
tool_name: Name of MCP tool called.
|
|
26
|
+
tool_args: Arguments passed to tool.
|
|
27
|
+
response: Response from tool.
|
|
28
|
+
screenshot_path: Path to captured screenshot.
|
|
29
|
+
screenshot_size: Size of screenshot file in bytes.
|
|
30
|
+
duration_ms: Execution duration in milliseconds.
|
|
31
|
+
status: Step status (PASS/FAIL).
|
|
32
|
+
error: Error message if failed.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
timestamp: str
|
|
36
|
+
test_id: str
|
|
37
|
+
step_number: int
|
|
38
|
+
tool_name: str
|
|
39
|
+
tool_args: dict = field(default_factory=dict)
|
|
40
|
+
response: dict = field(default_factory=dict)
|
|
41
|
+
screenshot_path: str | None = None
|
|
42
|
+
screenshot_size: int = 0
|
|
43
|
+
duration_ms: int = 0
|
|
44
|
+
status: str = "PASS"
|
|
45
|
+
error: str | None = None
|
|
46
|
+
|
|
47
|
+
def to_json(self) -> str:
|
|
48
|
+
"""Convert to JSON string."""
|
|
49
|
+
return json.dumps(asdict(self), ensure_ascii=False)
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def from_json(cls, json_str: str) -> "ExecutionLogEntry":
|
|
53
|
+
"""Create from JSON string."""
|
|
54
|
+
data = json.loads(json_str)
|
|
55
|
+
return cls(**data)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def get_log_path(story_id: str, base_dir: Path | None = None) -> Path:
|
|
59
|
+
"""Get execution log path for a story.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
story_id: Story identifier.
|
|
63
|
+
base_dir: Base directory. Defaults to project root.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Path to log file.
|
|
67
|
+
"""
|
|
68
|
+
if base_dir is None:
|
|
69
|
+
from ref_agents.session import SessionManager
|
|
70
|
+
|
|
71
|
+
session = SessionManager.get()
|
|
72
|
+
base_dir = session.project_root or Path.cwd()
|
|
73
|
+
|
|
74
|
+
return base_dir / "ref-reports" / "qa_lead" / f"test_execution_log_{story_id}.jsonl"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class ExecutionLogger:
|
|
78
|
+
"""Log all test execution for audit trail.
|
|
79
|
+
|
|
80
|
+
Writes JSONL format with one entry per line.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def __init__(self, story_id: str, base_dir: Path | None = None):
|
|
84
|
+
"""Initialize execution logger.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
story_id: Story identifier.
|
|
88
|
+
base_dir: Base directory for log files.
|
|
89
|
+
"""
|
|
90
|
+
self.story_id = story_id
|
|
91
|
+
self.log_file = get_log_path(story_id, base_dir)
|
|
92
|
+
self._ensure_log_dir()
|
|
93
|
+
|
|
94
|
+
def _ensure_log_dir(self) -> None:
|
|
95
|
+
"""Ensure log directory exists."""
|
|
96
|
+
self.log_file.parent.mkdir(parents=True, exist_ok=True)
|
|
97
|
+
|
|
98
|
+
def log_execution(self, entry: ExecutionLogEntry) -> None:
|
|
99
|
+
"""Append execution entry to log file.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
entry: Execution log entry to append.
|
|
103
|
+
"""
|
|
104
|
+
with open(self.log_file, "a", encoding="utf-8") as f:
|
|
105
|
+
f.write(entry.to_json() + "\n")
|
|
106
|
+
|
|
107
|
+
logger.info(
|
|
108
|
+
"execution_logged",
|
|
109
|
+
test_id=entry.test_id,
|
|
110
|
+
step=entry.step_number,
|
|
111
|
+
tool=entry.tool_name,
|
|
112
|
+
status=entry.status,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def log_step( # noqa: PLR0913 # TECH_DEBT: refactor to TypedDict — STORY-046
|
|
116
|
+
self,
|
|
117
|
+
test_id: str,
|
|
118
|
+
step_number: int,
|
|
119
|
+
tool_name: str,
|
|
120
|
+
tool_args: dict,
|
|
121
|
+
response: dict,
|
|
122
|
+
screenshot_path: Path | None = None,
|
|
123
|
+
duration_ms: int = 0,
|
|
124
|
+
status: str = "PASS",
|
|
125
|
+
error: str | None = None,
|
|
126
|
+
) -> ExecutionLogEntry:
|
|
127
|
+
"""Log a test step execution.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
test_id: Test case identifier.
|
|
131
|
+
step_number: Step number.
|
|
132
|
+
tool_name: MCP tool name.
|
|
133
|
+
tool_args: Tool arguments.
|
|
134
|
+
response: Tool response.
|
|
135
|
+
screenshot_path: Path to screenshot.
|
|
136
|
+
duration_ms: Execution duration.
|
|
137
|
+
status: Step status.
|
|
138
|
+
error: Error message if failed.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
The logged entry.
|
|
142
|
+
"""
|
|
143
|
+
screenshot_size = 0
|
|
144
|
+
screenshot_str = None
|
|
145
|
+
|
|
146
|
+
if screenshot_path and screenshot_path.exists():
|
|
147
|
+
screenshot_size = screenshot_path.stat().st_size
|
|
148
|
+
screenshot_str = str(screenshot_path)
|
|
149
|
+
|
|
150
|
+
entry = ExecutionLogEntry(
|
|
151
|
+
timestamp=datetime.now().isoformat(),
|
|
152
|
+
test_id=test_id,
|
|
153
|
+
step_number=step_number,
|
|
154
|
+
tool_name=tool_name,
|
|
155
|
+
tool_args=tool_args,
|
|
156
|
+
response=response,
|
|
157
|
+
screenshot_path=screenshot_str,
|
|
158
|
+
screenshot_size=screenshot_size,
|
|
159
|
+
duration_ms=duration_ms,
|
|
160
|
+
status=status,
|
|
161
|
+
error=error,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
self.log_execution(entry)
|
|
165
|
+
return entry
|
|
166
|
+
|
|
167
|
+
def read_log(self) -> list[ExecutionLogEntry]:
|
|
168
|
+
"""Read all entries from log file.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
List of log entries.
|
|
172
|
+
"""
|
|
173
|
+
if not self.log_file.exists():
|
|
174
|
+
return []
|
|
175
|
+
|
|
176
|
+
entries = []
|
|
177
|
+
with open(self.log_file, "r", encoding="utf-8") as f:
|
|
178
|
+
for line in f:
|
|
179
|
+
line = line.strip()
|
|
180
|
+
if line:
|
|
181
|
+
try:
|
|
182
|
+
entries.append(ExecutionLogEntry.from_json(line))
|
|
183
|
+
except json.JSONDecodeError as e:
|
|
184
|
+
logger.warning("invalid_log_entry", line=line, error=str(e))
|
|
185
|
+
|
|
186
|
+
return entries
|
|
187
|
+
|
|
188
|
+
def clear_log(self) -> None:
|
|
189
|
+
"""Clear the log file."""
|
|
190
|
+
if self.log_file.exists():
|
|
191
|
+
self.log_file.unlink()
|
|
192
|
+
self._ensure_log_dir()
|
|
193
|
+
|
|
194
|
+
def verify_log_integrity(self) -> tuple[bool, list[str]]:
|
|
195
|
+
"""Verify log file integrity.
|
|
196
|
+
|
|
197
|
+
Checks:
|
|
198
|
+
- Log file exists
|
|
199
|
+
- All entries are valid JSON
|
|
200
|
+
- Timestamps are sequential
|
|
201
|
+
- No gaps in step numbers per test
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
Tuple of (valid, list of issues).
|
|
205
|
+
"""
|
|
206
|
+
issues = []
|
|
207
|
+
|
|
208
|
+
if not self.log_file.exists():
|
|
209
|
+
return False, ["Log file does not exist"]
|
|
210
|
+
|
|
211
|
+
entries = self.read_log()
|
|
212
|
+
|
|
213
|
+
if not entries:
|
|
214
|
+
return False, ["Log file is empty"]
|
|
215
|
+
|
|
216
|
+
# Check timestamps are sequential
|
|
217
|
+
prev_timestamp = None
|
|
218
|
+
for entry in entries:
|
|
219
|
+
try:
|
|
220
|
+
timestamp = datetime.fromisoformat(entry.timestamp)
|
|
221
|
+
if prev_timestamp and timestamp < prev_timestamp:
|
|
222
|
+
issues.append(
|
|
223
|
+
f"Timestamp not sequential: {entry.timestamp} < {prev_timestamp.isoformat()}"
|
|
224
|
+
)
|
|
225
|
+
prev_timestamp = timestamp
|
|
226
|
+
except ValueError:
|
|
227
|
+
issues.append(f"Invalid timestamp format: {entry.timestamp}")
|
|
228
|
+
|
|
229
|
+
# Check step numbers per test
|
|
230
|
+
test_steps: dict[str, list[int]] = {}
|
|
231
|
+
for entry in entries:
|
|
232
|
+
if entry.test_id not in test_steps:
|
|
233
|
+
test_steps[entry.test_id] = []
|
|
234
|
+
test_steps[entry.test_id].append(entry.step_number)
|
|
235
|
+
|
|
236
|
+
for test_id, steps in test_steps.items():
|
|
237
|
+
expected = list(range(1, len(steps) + 1))
|
|
238
|
+
if sorted(steps) != expected:
|
|
239
|
+
issues.append(f"Step numbers not sequential for {test_id}: {steps}")
|
|
240
|
+
|
|
241
|
+
return len(issues) == 0, issues
|
|
242
|
+
|
|
243
|
+
def get_entry_count(self) -> int:
|
|
244
|
+
"""Get number of entries in log.
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
Number of log entries.
|
|
248
|
+
"""
|
|
249
|
+
return len(self.read_log())
|