patchi 0.7.5__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 (496) hide show
  1. patchi/__init__.py +1 -0
  2. patchi/__main__.py +13 -0
  3. patchi/cli/__init__.py +0 -0
  4. patchi/cli/command_families.py +235 -0
  5. patchi/cli/commands/__init__.py +0 -0
  6. patchi/cli/commands/access_cmd.py +65 -0
  7. patchi/cli/commands/agent_stats_cmd.py +273 -0
  8. patchi/cli/commands/agents_cmd.py +187 -0
  9. patchi/cli/commands/ai_cmd.py +308 -0
  10. patchi/cli/commands/assure_cmd.py +538 -0
  11. patchi/cli/commands/audit_cmd.py +286 -0
  12. patchi/cli/commands/auto_cmd.py +110 -0
  13. patchi/cli/commands/blame_cmd.py +53 -0
  14. patchi/cli/commands/chains_cmd.py +327 -0
  15. patchi/cli/commands/charter_cmd.py +173 -0
  16. patchi/cli/commands/chat_cmd.py +597 -0
  17. patchi/cli/commands/check_cmd.py +232 -0
  18. patchi/cli/commands/cleanup_cmd.py +328 -0
  19. patchi/cli/commands/cockpit_cmd.py +208 -0
  20. patchi/cli/commands/commands_cmd.py +160 -0
  21. patchi/cli/commands/cross_repo_cmd.py +113 -0
  22. patchi/cli/commands/deps_cmd.py +110 -0
  23. patchi/cli/commands/dev_check_cmd.py +342 -0
  24. patchi/cli/commands/dev_cmd.py +445 -0
  25. patchi/cli/commands/doctor_cmd.py +457 -0
  26. patchi/cli/commands/findings_cmd.py +187 -0
  27. patchi/cli/commands/fix_cmd.py +648 -0
  28. patchi/cli/commands/fix_review_cmd.py +273 -0
  29. patchi/cli/commands/heatmap_cmd.py +62 -0
  30. patchi/cli/commands/help_cmd.py +159 -0
  31. patchi/cli/commands/hosted_cmd.py +980 -0
  32. patchi/cli/commands/init.py +456 -0
  33. patchi/cli/commands/key_cmd.py +571 -0
  34. patchi/cli/commands/learn_cmd.py +140 -0
  35. patchi/cli/commands/link_cmd.py +104 -0
  36. patchi/cli/commands/log_cmd.py +57 -0
  37. patchi/cli/commands/memory_cmd.py +215 -0
  38. patchi/cli/commands/model_cmd.py +207 -0
  39. patchi/cli/commands/notify_cmd.py +213 -0
  40. patchi/cli/commands/notify_prompts.py +29 -0
  41. patchi/cli/commands/patch_cmd.py +231 -0
  42. patchi/cli/commands/plan_cmd.py +128 -0
  43. patchi/cli/commands/plugins_cmd.py +307 -0
  44. patchi/cli/commands/queue_cmd.py +152 -0
  45. patchi/cli/commands/quick_cmd.py +159 -0
  46. patchi/cli/commands/ready_cmd.py +292 -0
  47. patchi/cli/commands/report_cmd.py +416 -0
  48. patchi/cli/commands/restrict_cmd.py +127 -0
  49. patchi/cli/commands/review_cmd.py +225 -0
  50. patchi/cli/commands/rules_cmd.py +233 -0
  51. patchi/cli/commands/scan_cmd.py +1908 -0
  52. patchi/cli/commands/settings_cmd.py +45 -0
  53. patchi/cli/commands/status_cmd.py +679 -0
  54. patchi/cli/commands/test_cmd.py +1052 -0
  55. patchi/cli/commands/trend_cmd.py +296 -0
  56. patchi/cli/commands/undo_cmd.py +235 -0
  57. patchi/cli/commands/update_cmd.py +345 -0
  58. patchi/cli/commands/verify_cmd.py +56 -0
  59. patchi/cli/commands/vr_cmd.py +440 -0
  60. patchi/cli/commands/watch_cmd.py +304 -0
  61. patchi/cli/commands/web_cmd.py +114 -0
  62. patchi/cli/console.py +101 -0
  63. patchi/cli/display/__init__.py +1 -0
  64. patchi/cli/display/live_audit.py +269 -0
  65. patchi/cli/display/live_progress.py +169 -0
  66. patchi/cli/framework.py +241 -0
  67. patchi/cli/logo.py +128 -0
  68. patchi/cli/main.py +222 -0
  69. patchi/cli/registry.py +1171 -0
  70. patchi/cli/style.py +68 -0
  71. patchi/cli/themes.py +187 -0
  72. patchi/cli/ux.py +444 -0
  73. patchi/core/__init__.py +0 -0
  74. patchi/core/agents/__init__.py +0 -0
  75. patchi/core/agents/api_fuzzer_agent.py +141 -0
  76. patchi/core/agents/attack_agent.py +242 -0
  77. patchi/core/agents/base.py +1012 -0
  78. patchi/core/agents/bug_predictor_agent.py +58 -0
  79. patchi/core/agents/build_tool_validator.py +150 -0
  80. patchi/core/agents/cache.py +192 -0
  81. patchi/core/agents/chaos_agent.py +133 -0
  82. patchi/core/agents/cicd_generator.py +240 -0
  83. patchi/core/agents/comment_scanner.py +280 -0
  84. patchi/core/agents/console_logging_agent.py +156 -0
  85. patchi/core/agents/coordinator.py +858 -0
  86. patchi/core/agents/core_scanner.py +269 -0
  87. patchi/core/agents/coverage_prioritizer.py +181 -0
  88. patchi/core/agents/dead_code_hygiene.py +296 -0
  89. patchi/core/agents/dead_code_scanner.py +692 -0
  90. patchi/core/agents/dependency_scanner.py +725 -0
  91. patchi/core/agents/doc_claim_agent.py +573 -0
  92. patchi/core/agents/duplicate_scanner.py +568 -0
  93. patchi/core/agents/env_scanner.py +410 -0
  94. patchi/core/agents/feature_flag_agent.py +89 -0
  95. patchi/core/agents/frontend_framework_agent.py +133 -0
  96. patchi/core/agents/goroutine_agent.py +82 -0
  97. patchi/core/agents/governor.py +1972 -0
  98. patchi/core/agents/i18n_agent.py +95 -0
  99. patchi/core/agents/license_compliance.py +247 -0
  100. patchi/core/agents/linking_agent.py +251 -0
  101. patchi/core/agents/memory_profiler_agent.py +102 -0
  102. patchi/core/agents/modernization_agent.py +84 -0
  103. patchi/core/agents/mutation_agent.py +174 -0
  104. patchi/core/agents/nl_query_agent.py +74 -0
  105. patchi/core/agents/promise_rejection_agent.py +100 -0
  106. patchi/core/agents/refactoring_agent.py +286 -0
  107. patchi/core/agents/resource_leak_agent.py +91 -0
  108. patchi/core/agents/route_graph_scanner.py +951 -0
  109. patchi/core/agents/run_agent.py +106 -0
  110. patchi/core/agents/rust_unwrap_agent.py +100 -0
  111. patchi/core/agents/sbom_generator.py +284 -0
  112. patchi/core/agents/scanners.py +67 -0
  113. patchi/core/agents/side/__init__.py +1 -0
  114. patchi/core/agents/side/build_agent.py +65 -0
  115. patchi/core/agents/side/format_agent.py +50 -0
  116. patchi/core/agents/side/install_agent.py +63 -0
  117. patchi/core/agents/side_file_scanner.py +549 -0
  118. patchi/core/agents/snapshot_drift_detector.py +146 -0
  119. patchi/core/agents/spa_route_inventory.py +203 -0
  120. patchi/core/agents/test_scanner.py +442 -0
  121. patchi/core/agents/tool_health.py +109 -0
  122. patchi/core/agents/tool_runner.py +162 -0
  123. patchi/core/agents/type_scanner.py +168 -0
  124. patchi/core/agents/ui_scanner.py +558 -0
  125. patchi/core/ai/__init__.py +20 -0
  126. patchi/core/ai/agent_audit.py +153 -0
  127. patchi/core/ai/agent_profiler.py +277 -0
  128. patchi/core/ai/client.py +499 -0
  129. patchi/core/ai/cost_tracker.py +107 -0
  130. patchi/core/ai/model_router.py +318 -0
  131. patchi/core/ai/orchestrator.py +625 -0
  132. patchi/core/ai/prompts.py +681 -0
  133. patchi/core/ai/smart.py +302 -0
  134. patchi/core/ai/tool_executor.py +24 -0
  135. patchi/core/ai/tools/__init__.py +10 -0
  136. patchi/core/ai/tools/executor.py +621 -0
  137. patchi/core/ai/tools/realize.py +1160 -0
  138. patchi/core/ai/tools/registry.py +1577 -0
  139. patchi/core/assurance/__init__.py +33 -0
  140. patchi/core/assurance/builder.py +211 -0
  141. patchi/core/assurance/graph.py +342 -0
  142. patchi/core/assurance/invariants.py +168 -0
  143. patchi/core/atomic.py +61 -0
  144. patchi/core/attackers/__init__.py +17 -0
  145. patchi/core/attackers/api_abuse.py +89 -0
  146. patchi/core/attackers/auth_bypass.py +73 -0
  147. patchi/core/attackers/base.py +91 -0
  148. patchi/core/attackers/business_logic.py +88 -0
  149. patchi/core/attackers/planner.py +108 -0
  150. patchi/core/attackers/priv_escalation.py +88 -0
  151. patchi/core/attackers/recon.py +87 -0
  152. patchi/core/brain/__init__.py +0 -0
  153. patchi/core/brain/ast_utils/__init__.py +81 -0
  154. patchi/core/brain/ast_utils/assignments.py +219 -0
  155. patchi/core/brain/ast_utils/calls.py +139 -0
  156. patchi/core/brain/ast_utils/config.py +132 -0
  157. patchi/core/brain/ast_utils/control_flow.py +74 -0
  158. patchi/core/brain/ast_utils/dead_symbols.py +205 -0
  159. patchi/core/brain/ast_utils/decorators.py +108 -0
  160. patchi/core/brain/ast_utils/helpers.py +144 -0
  161. patchi/core/brain/ast_utils/imports.py +169 -0
  162. patchi/core/brain/ast_utils/scan.py +143 -0
  163. patchi/core/brain/ast_utils/taint.py +310 -0
  164. patchi/core/brain/audit.py +379 -0
  165. patchi/core/brain/baseline.py +195 -0
  166. patchi/core/brain/blast_radius.py +312 -0
  167. patchi/core/brain/body_tags.py +144 -0
  168. patchi/core/brain/brain.py +1161 -0
  169. patchi/core/brain/brain_context.py +393 -0
  170. patchi/core/brain/brain_watcher.py +340 -0
  171. patchi/core/brain/charter.py +473 -0
  172. patchi/core/brain/classifier.py +214 -0
  173. patchi/core/brain/cockpit.py +350 -0
  174. patchi/core/brain/code_query.py +710 -0
  175. patchi/core/brain/contract.py +889 -0
  176. patchi/core/brain/contract_diff.py +155 -0
  177. patchi/core/brain/council.py +800 -0
  178. patchi/core/brain/cross_repo.py +104 -0
  179. patchi/core/brain/dead_code_tools.py +157 -0
  180. patchi/core/brain/doc_validator.py +197 -0
  181. patchi/core/brain/domain_activator.py +2183 -0
  182. patchi/core/brain/enriched_context.py +220 -0
  183. patchi/core/brain/file_corpus.py +220 -0
  184. patchi/core/brain/framework.py +953 -0
  185. patchi/core/brain/freshness.py +273 -0
  186. patchi/core/brain/git_aware.py +206 -0
  187. patchi/core/brain/heatmap.py +104 -0
  188. patchi/core/brain/ignore_parser.py +153 -0
  189. patchi/core/brain/import_graph.py +486 -0
  190. patchi/core/brain/languages.py +397 -0
  191. patchi/core/brain/layered_brain.py +419 -0
  192. patchi/core/brain/learning.py +229 -0
  193. patchi/core/brain/orphaned_endpoints.py +313 -0
  194. patchi/core/brain/personas/__init__.py +648 -0
  195. patchi/core/brain/personas/bad_user.py +165 -0
  196. patchi/core/brain/personas/base.py +349 -0
  197. patchi/core/brain/proactive.py +807 -0
  198. patchi/core/brain/project_context.py +121 -0
  199. patchi/core/brain/project_reader.py +521 -0
  200. patchi/core/brain/reasoning.py +269 -0
  201. patchi/core/brain/route_detector/__init__.py +15 -0
  202. patchi/core/brain/route_detector/base.py +31 -0
  203. patchi/core/brain/route_detector/csharp.py +209 -0
  204. patchi/core/brain/route_detector/file_based.py +81 -0
  205. patchi/core/brain/route_detector/go.py +152 -0
  206. patchi/core/brain/route_detector/java.py +195 -0
  207. patchi/core/brain/route_detector/javascript.py +197 -0
  208. patchi/core/brain/route_detector/php.py +158 -0
  209. patchi/core/brain/route_detector/python.py +193 -0
  210. patchi/core/brain/route_detector/registry.py +25 -0
  211. patchi/core/brain/route_detector/ruby.py +134 -0
  212. patchi/core/brain/route_detector/rust.py +216 -0
  213. patchi/core/brain/route_detector/swift.py +118 -0
  214. patchi/core/brain/route_mapper.py +265 -0
  215. patchi/core/brain/scanner.py +2136 -0
  216. patchi/core/brain/secrets.py +280 -0
  217. patchi/core/brain/symbol_graph.py +1955 -0
  218. patchi/core/brain/team.py +28 -0
  219. patchi/core/brain/trace_log.py +132 -0
  220. patchi/core/brain/type_checker/__init__.py +65 -0
  221. patchi/core/brain/type_checker/base.py +60 -0
  222. patchi/core/brain/type_checker/csharp.py +59 -0
  223. patchi/core/brain/type_checker/dart.py +59 -0
  224. patchi/core/brain/type_checker/go.py +81 -0
  225. patchi/core/brain/type_checker/java.py +75 -0
  226. patchi/core/brain/type_checker/kotlin.py +75 -0
  227. patchi/core/brain/type_checker/php.py +88 -0
  228. patchi/core/brain/type_checker/python.py +87 -0
  229. patchi/core/brain/type_checker/rust.py +81 -0
  230. patchi/core/brain/type_checker/swift.py +59 -0
  231. patchi/core/brain/type_checker/typescript.py +298 -0
  232. patchi/core/brain/understander.py +130 -0
  233. patchi/core/brain/verify.py +379 -0
  234. patchi/core/campaigns/__init__.py +14 -0
  235. patchi/core/campaigns/base.py +62 -0
  236. patchi/core/campaigns/data_flow_abuse.py +153 -0
  237. patchi/core/campaigns/orchestrator.py +140 -0
  238. patchi/core/campaigns/state_transitions.py +178 -0
  239. patchi/core/ci_bundle.py +104 -0
  240. patchi/core/config.py +311 -0
  241. patchi/core/constants.py +245 -0
  242. patchi/core/debug/__init__.py +25 -0
  243. patchi/core/debug/adapters/__init__.py +11 -0
  244. patchi/core/debug/adapters/codelldb.py +379 -0
  245. patchi/core/debug/adapters/node.py +222 -0
  246. patchi/core/debug/adapters/powershell.py +210 -0
  247. patchi/core/debug/adapters/python.py +244 -0
  248. patchi/core/debug/capture.py +210 -0
  249. patchi/core/debug/dap_client.py +268 -0
  250. patchi/core/detector/__init__.py +0 -0
  251. patchi/core/detector/audit.py +194 -0
  252. patchi/core/detector/bus.py +226 -0
  253. patchi/core/detector/dispatcher.py +275 -0
  254. patchi/core/detector/event.py +254 -0
  255. patchi/core/detector/sigma_engine.py +378 -0
  256. patchi/core/detector/triage.py +289 -0
  257. patchi/core/export/sarif.py +113 -0
  258. patchi/core/fix/__init__.py +17 -0
  259. patchi/core/fix/applier.py +574 -0
  260. patchi/core/fix/base.py +510 -0
  261. patchi/core/fix/code_fixer.py +169 -0
  262. patchi/core/fix/dead_code_remover.py +131 -0
  263. patchi/core/fix/fix_agents.py +775 -0
  264. patchi/core/fix/patch.py +444 -0
  265. patchi/core/fix/risk_gate.py +383 -0
  266. patchi/core/fix/security_fixer.py +93 -0
  267. patchi/core/fix/verify_loop.py +204 -0
  268. patchi/core/fuzz/__init__.py +8 -0
  269. patchi/core/fuzz/corpus.py +137 -0
  270. patchi/core/fuzz/input_fuzzer.py +256 -0
  271. patchi/core/fuzz/sequence_fuzzer.py +162 -0
  272. patchi/core/fuzz/state_fuzzer.py +236 -0
  273. patchi/core/health.py +407 -0
  274. patchi/core/hosted/__init__.py +1 -0
  275. patchi/core/hosted/anomaly.py +298 -0
  276. patchi/core/hosted/audit_log.py +102 -0
  277. patchi/core/hosted/compliance_report.py +148 -0
  278. patchi/core/hosted/ip_reputation.py +169 -0
  279. patchi/core/hosted/log_parsers.py +251 -0
  280. patchi/core/hosted/tokens.py +127 -0
  281. patchi/core/hosted/watchlist.py +164 -0
  282. patchi/core/hosted/webhooks.py +156 -0
  283. patchi/core/memory.py +437 -0
  284. patchi/core/notifications/__init__.py +18 -0
  285. patchi/core/notifications/channels.py +192 -0
  286. patchi/core/notifications/digest.py +141 -0
  287. patchi/core/notifications/escalation.py +158 -0
  288. patchi/core/notifications/notifier.py +182 -0
  289. patchi/core/notifications/quiet_hours.py +69 -0
  290. patchi/core/plugins/__init__.py +48 -0
  291. patchi/core/plugins/analyzer.py +326 -0
  292. patchi/core/plugins/analyzers/__init__.py +6 -0
  293. patchi/core/plugins/analyzers/dead_code_analyzer.py +120 -0
  294. patchi/core/plugins/analyzers/secret_scanner.py +158 -0
  295. patchi/core/plugins/analyzers/todo_scanner.py +115 -0
  296. patchi/core/plugins/registry.py +335 -0
  297. patchi/core/queue.py +375 -0
  298. patchi/core/reliability/__init__.py +13 -0
  299. patchi/core/reliability/chaos.py +163 -0
  300. patchi/core/reliability/fault_injector.py +172 -0
  301. patchi/core/reliability/idempotency.py +128 -0
  302. patchi/core/reliability/recovery.py +147 -0
  303. patchi/core/runtime/__init__.py +5 -0
  304. patchi/core/runtime/tracer.py +230 -0
  305. patchi/core/scan_bus.py +156 -0
  306. patchi/core/security/__init__.py +1 -0
  307. patchi/core/security/ai_validator.py +462 -0
  308. patchi/core/security/app_mapper.py +216 -0
  309. patchi/core/security/app_profile.py +464 -0
  310. patchi/core/security/attack_feedback.py +141 -0
  311. patchi/core/security/attack_scenarios/__init__.py +1 -0
  312. patchi/core/security/attack_tree.py +176 -0
  313. patchi/core/security/auth_audit_agent.py +344 -0
  314. patchi/core/security/authz_agent.py +602 -0
  315. patchi/core/security/auto_fix_proactive.py +620 -0
  316. patchi/core/security/auto_fixer.py +660 -0
  317. patchi/core/security/auto_ticket.py +53 -0
  318. patchi/core/security/bandit_agent.py +152 -0
  319. patchi/core/security/blast_radius.py +226 -0
  320. patchi/core/security/browser_tester.py +333 -0
  321. patchi/core/security/business_logic_agent.py +263 -0
  322. patchi/core/security/catch_block_auditor.py +395 -0
  323. patchi/core/security/cdn_cache_agent.py +357 -0
  324. patchi/core/security/chain_analyzer.py +321 -0
  325. patchi/core/security/chain_to_assurance.py +168 -0
  326. patchi/core/security/charter.py +510 -0
  327. patchi/core/security/cloud_waf_detector.py +295 -0
  328. patchi/core/security/codeql_agent.py +233 -0
  329. patchi/core/security/compliance_agent.py +348 -0
  330. patchi/core/security/confidence_gate.py +423 -0
  331. patchi/core/security/container_scanner.py +282 -0
  332. patchi/core/security/crypto_agent.py +378 -0
  333. patchi/core/security/cve_monitor.py +262 -0
  334. patchi/core/security/dast_agent.py +865 -0
  335. patchi/core/security/defectdojo.py +238 -0
  336. patchi/core/security/defenders/__init__.py +75 -0
  337. patchi/core/security/defenders/auth_middleware.py +115 -0
  338. patchi/core/security/defenders/base.py +97 -0
  339. patchi/core/security/defenders/block_ip.py +71 -0
  340. patchi/core/security/defenders/block_ws_origin.py +36 -0
  341. patchi/core/security/defenders/crypto_fix.py +118 -0
  342. patchi/core/security/defenders/enforce_rate_limit.py +108 -0
  343. patchi/core/security/defenders/fix_code.py +93 -0
  344. patchi/core/security/defenders/invalidate_session.py +40 -0
  345. patchi/core/security/defenders/patch_config.py +70 -0
  346. patchi/core/security/defenders/rotate_secret.py +54 -0
  347. patchi/core/security/defenders/suspend_account.py +41 -0
  348. patchi/core/security/defenders/update_dependency.py +160 -0
  349. patchi/core/security/defense_layer.py +217 -0
  350. patchi/core/security/dependency_vulnerability_agent.py +616 -0
  351. patchi/core/security/detection_pipeline.py +289 -0
  352. patchi/core/security/dns_security_agent.py +497 -0
  353. patchi/core/security/domain_activator_v2.py +913 -0
  354. patchi/core/security/domain_loader.py +1004 -0
  355. patchi/core/security/domains/__init__.py +0 -0
  356. patchi/core/security/email_authentication_agent.py +417 -0
  357. patchi/core/security/env_var_validator.py +304 -0
  358. patchi/core/security/evidence.py +100 -0
  359. patchi/core/security/falco_runtime_agent.py +387 -0
  360. patchi/core/security/fix-playbooks/__init__.py +0 -0
  361. patchi/core/security/gated_finding.py +85 -0
  362. patchi/core/security/git_diff_activator.py +367 -0
  363. patchi/core/security/governance.py +208 -0
  364. patchi/core/security/gradual_gate.py +34 -0
  365. patchi/core/security/history.py +315 -0
  366. patchi/core/security/iac_scanner.py +250 -0
  367. patchi/core/security/ignore_expiry.py +111 -0
  368. patchi/core/security/ignore_learner.py +416 -0
  369. patchi/core/security/injection_agent.py +319 -0
  370. patchi/core/security/insecure_randomness_agent.py +288 -0
  371. patchi/core/security/intent_analyzer.py +326 -0
  372. patchi/core/security/jwt_agent.py +453 -0
  373. patchi/core/security/kubernetes_agent.py +638 -0
  374. patchi/core/security/layer2_orchestrator.py +341 -0
  375. patchi/core/security/llm_security_agent.py +252 -0
  376. patchi/core/security/misconfig_agent.py +600 -0
  377. patchi/core/security/mobile_security_agent.py +474 -0
  378. patchi/core/security/network_agent.py +376 -0
  379. patchi/core/security/noise_filter.py +274 -0
  380. patchi/core/security/orchestrator.py +313 -0
  381. patchi/core/security/pattern_context.py +125 -0
  382. patchi/core/security/pattern_loader.py +101 -0
  383. patchi/core/security/pentest/__init__.py +4 -0
  384. patchi/core/security/pentest/base.py +58 -0
  385. patchi/core/security/pentest/dalfox_adapter.py +40 -0
  386. patchi/core/security/pentest/ffuf_adapter.py +48 -0
  387. patchi/core/security/pentest/nuclei_adapter.py +50 -0
  388. patchi/core/security/pentest/registry.py +80 -0
  389. patchi/core/security/pentest/shannon_adapter.py +107 -0
  390. patchi/core/security/pentest/sqlmap_adapter.py +56 -0
  391. patchi/core/security/pentest/zap_adapter.py +52 -0
  392. patchi/core/security/plan_auditor.py +276 -0
  393. patchi/core/security/policy_engine.py +272 -0
  394. patchi/core/security/prechecks.py +172 -0
  395. patchi/core/security/precommit_hook.py +247 -0
  396. patchi/core/security/privacy_agent.py +361 -0
  397. patchi/core/security/push_notification_agent.py +417 -0
  398. patchi/core/security/pysa_agent.py +143 -0
  399. patchi/core/security/reasoning.py +442 -0
  400. patchi/core/security/red_team_agent.py +184 -0
  401. patchi/core/security/red_team_engine.py +1142 -0
  402. patchi/core/security/remediation.py +360 -0
  403. patchi/core/security/request_interceptor.py +294 -0
  404. patchi/core/security/runtime_validator.py +245 -0
  405. patchi/core/security/saml_sso_agent.py +401 -0
  406. patchi/core/security/sast_agent.py +213 -0
  407. patchi/core/security/sast_gate.py +95 -0
  408. patchi/core/security/scheduler.py +287 -0
  409. patchi/core/security/secrets_runtime_agent.py +476 -0
  410. patchi/core/security/security_agents.py +175 -0
  411. patchi/core/security/security_config.py +551 -0
  412. patchi/core/security/security_probe.py +762 -0
  413. patchi/core/security/security_taint.py +1005 -0
  414. patchi/core/security/sensitive_data_agent.py +271 -0
  415. patchi/core/security/service_mesh_agent.py +412 -0
  416. patchi/core/security/session_management_agent.py +245 -0
  417. patchi/core/security/ssrf_agent.py +422 -0
  418. patchi/core/security/supply_chain.py +449 -0
  419. patchi/core/security/threat_model_generator.py +378 -0
  420. patchi/core/security/threat_model_updater.py +196 -0
  421. patchi/core/security/tool_adapters.py +135 -0
  422. patchi/core/security/tool_verify.py +173 -0
  423. patchi/core/security/websocket_security_agent.py +188 -0
  424. patchi/core/snapshot.py +242 -0
  425. patchi/core/tenant.py +386 -0
  426. patchi/core/testing/__init__.py +17 -0
  427. patchi/core/testing/_browser.py +147 -0
  428. patchi/core/testing/ads_agent.py +141 -0
  429. patchi/core/testing/api_contract_agent.py +450 -0
  430. patchi/core/testing/app_discovery_agent.py +641 -0
  431. patchi/core/testing/app_launcher.py +184 -0
  432. patchi/core/testing/browser_test_agent.py +516 -0
  433. patchi/core/testing/e2e_flow_agent.py +354 -0
  434. patchi/core/testing/flake_detector_agent.py +292 -0
  435. patchi/core/testing/gate.py +31 -0
  436. patchi/core/testing/live_test_runner.py +304 -0
  437. patchi/core/testing/live_v2/__init__.py +1 -0
  438. patchi/core/testing/live_v2/browser_pool.py +349 -0
  439. patchi/core/testing/live_v2/browser_test_runner.py +313 -0
  440. patchi/core/testing/live_v2/runner.py +626 -0
  441. patchi/core/testing/live_v2/screenshot_manager.py +474 -0
  442. patchi/core/testing/live_v2/stress_orchestrator.py +523 -0
  443. patchi/core/testing/live_v2/video_recorder.py +303 -0
  444. patchi/core/testing/regression_agent.py +302 -0
  445. patchi/core/testing/security_test_agent.py +253 -0
  446. patchi/core/testing/stress_test_agent.py +203 -0
  447. patchi/core/testing/test_agents.py +50 -0
  448. patchi/core/testing/test_config.py +194 -0
  449. patchi/core/testing/ui_accessibility_agent.py +324 -0
  450. patchi/core/testing/ui_button_agent.py +294 -0
  451. patchi/core/testing/ui_layout_agent.py +330 -0
  452. patchi/core/testing/unit_test_agent.py +1149 -0
  453. patchi/core/testing/visual_regression_agent.py +260 -0
  454. patchi/web/__init__.py +1 -0
  455. patchi/web/api/__init__.py +1 -0
  456. patchi/web/api/brain_map.py +114 -0
  457. patchi/web/api/charts.py +47 -0
  458. patchi/web/api/chat.py +224 -0
  459. patchi/web/api/cicd.py +576 -0
  460. patchi/web/api/dast_evidence.py +49 -0
  461. patchi/web/api/dev_check.py +320 -0
  462. patchi/web/api/fix.py +134 -0
  463. patchi/web/api/guard.py +65 -0
  464. patchi/web/api/hosted.py +128 -0
  465. patchi/web/api/hosted_v2.py +234 -0
  466. patchi/web/api/live_testing.py +699 -0
  467. patchi/web/api/scan.py +582 -0
  468. patchi/web/api/smart.py +111 -0
  469. patchi/web/api/tenant.py +117 -0
  470. patchi/web/api/tools.py +85 -0
  471. patchi/web/api_legacy.py +2401 -0
  472. patchi/web/app.py +310 -0
  473. patchi/web/events.py +483 -0
  474. patchi/web/routes/__init__.py +1 -0
  475. patchi/web/routes/assurance.py +541 -0
  476. patchi/web/routes/brain.py +36 -0
  477. patchi/web/routes/charter.py +227 -0
  478. patchi/web/routes/chat.py +23 -0
  479. patchi/web/routes/dashboard.py +558 -0
  480. patchi/web/routes/dashboard_v2.py +402 -0
  481. patchi/web/routes/findings.py +330 -0
  482. patchi/web/routes/guard.py +35 -0
  483. patchi/web/routes/history.py +126 -0
  484. patchi/web/routes/landing.py +52 -0
  485. patchi/web/routes/live_testing.py +24 -0
  486. patchi/web/routes/review.py +57 -0
  487. patchi/web/routes/self_improvement.py +110 -0
  488. patchi/web/routes/settings.py +49 -0
  489. patchi/web/routes/tokens.py +29 -0
  490. patchi/web/spawn.py +139 -0
  491. patchi/web/ws.py +141 -0
  492. patchi-0.7.5.dist-info/METADATA +315 -0
  493. patchi-0.7.5.dist-info/RECORD +496 -0
  494. patchi-0.7.5.dist-info/WHEEL +5 -0
  495. patchi-0.7.5.dist-info/licenses/LICENSE +60 -0
  496. patchi-0.7.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,626 @@
1
+ """
2
+ Live Test Runner v2 — Enhanced test orchestration with browser automation,
3
+ visual regression, stress testing, and video recording.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import asyncio
8
+ import logging
9
+ import time
10
+ import uuid
11
+ from collections.abc import Callable
12
+ from dataclasses import dataclass, field
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+ from patchi.core.agents.base import (
17
+ AgentGroup,
18
+ AgentInput,
19
+ AgentResult,
20
+ BaseAgent,
21
+ Finding,
22
+ Severity,
23
+ register,
24
+ )
25
+ from patchi.core.testing.live_test_runner import TestRunConfig, TestRunResult
26
+ from patchi.core.testing.live_v2.browser_pool import BrowserConfig, BrowserPool, get_browser_pool
27
+ from patchi.core.testing.live_v2.screenshot_manager import (
28
+ ScreenshotConfig,
29
+ ScreenshotManager,
30
+ _AsyncVisualRegressionAgent,
31
+ )
32
+ from patchi.core.testing.live_v2.stress_orchestrator import (
33
+ StressConfig,
34
+ StressOrchestrator,
35
+ StressTestReport,
36
+ )
37
+ from patchi.core.testing.live_v2.video_recorder import RecordingConfig, TestRecording, VideoRecorder
38
+
39
+ _log = logging.getLogger("patchi.testing.live_test_runner_v2")
40
+
41
+
42
+ @dataclass
43
+ class LiveTestConfigV2:
44
+ """Enhanced configuration for live test runs."""
45
+
46
+ # Base config
47
+ test_types: list[str] = field(
48
+ default_factory=lambda: ["unit", "regression", "browser", "visual"]
49
+ )
50
+ area: str | None = None
51
+ base_url: str | None = None
52
+ parallel: bool = False
53
+ timeout_per_agent: int = 120
54
+
55
+ # Browser config
56
+ browser_config: BrowserConfig = field(default_factory=BrowserConfig)
57
+
58
+ # Visual regression
59
+ visual_threshold: float = 0.1
60
+ update_baselines: bool = False
61
+
62
+ # Stress testing
63
+ stress_config: StressConfig | None = None
64
+
65
+ # Video recording
66
+ record_video: bool = False
67
+ recording_config: RecordingConfig = field(default_factory=RecordingConfig)
68
+
69
+ # Screenshot config
70
+ screenshot_config: ScreenshotConfig = field(default_factory=ScreenshotConfig)
71
+
72
+
73
+ @dataclass
74
+ class LiveTestResultV2:
75
+ """Enhanced test run result with live testing data."""
76
+
77
+ # Base results
78
+ base_result: TestRunResult
79
+
80
+ # Live testing results
81
+ browser_tests: list[dict] = field(default_factory=list)
82
+ visual_regression: dict | None = None
83
+ stress_test: StressTestReport | None = None
84
+ recordings: list[TestRecording] = field(default_factory=list)
85
+ screenshots: list[dict] = field(default_factory=list)
86
+
87
+ # Summary
88
+ live_test_summary: dict = field(default_factory=dict)
89
+
90
+ def to_dict(self) -> dict:
91
+ base = self.base_result.to_dict()
92
+ base.update(
93
+ {
94
+ "browser_tests": self.browser_tests,
95
+ "visual_regression": self.visual_regression,
96
+ "stress_test": self.stress_test.__dict__ if self.stress_test else None,
97
+ "recordings": [r.__dict__ for r in self.recordings],
98
+ "screenshots": self.screenshots,
99
+ "live_test_summary": self.live_test_summary,
100
+ }
101
+ )
102
+ return base
103
+
104
+
105
+ class LiveTestRunnerV2:
106
+ """
107
+ Enhanced Live Test Runner with browser automation, visual regression,
108
+ stress testing, and video recording.
109
+
110
+ Usage:
111
+ config = LiveTestConfigV2(
112
+ test_types=["unit", "browser", "visual", "stress"],
113
+ base_url="https://app.example.com",
114
+ stress_config=StressConfig(base_url="https://api.example.com", users=50),
115
+ record_video=True,
116
+ )
117
+ runner = LiveTestRunnerV2(project_root)
118
+ result = await runner.run(config)
119
+ """
120
+
121
+ def __init__(
122
+ self,
123
+ root: Path,
124
+ on_progress: Callable[[str], None] = None,
125
+ on_event: Callable[[str, dict], None] = None,
126
+ ):
127
+ self.root = root
128
+ self.on_progress = on_progress or (lambda _: None)
129
+ self.on_event = on_event or (lambda e, d: None)
130
+
131
+ # Components
132
+ self.browser_pool: BrowserPool | None = None
133
+ self.screenshot_manager: ScreenshotManager | None = None
134
+ self.video_recorder: VideoRecorder | None = None
135
+
136
+ # State
137
+ self._run_id = ""
138
+ self._start_time = 0
139
+ self._config: LiveTestConfigV2 | None = None
140
+
141
+ async def run(self, config: LiveTestConfigV2) -> LiveTestResultV2:
142
+ """Run a complete live test suite."""
143
+ self._run_id = f"live-{uuid.uuid4().hex[:8]}"
144
+ self._start_time = time.time()
145
+ self._config = config
146
+
147
+ self.on_progress(f"🚀 Starting Live Test Run: {self._run_id}")
148
+ self.on_event("test_run_started", {"run_id": self._run_id, "config": config.__dict__})
149
+
150
+ # Initialize components
151
+ await self._initialize_components(config)
152
+
153
+ try:
154
+ # Run base test agents
155
+ base_result = await self._run_base_tests(config)
156
+
157
+ # Run live tests
158
+ browser_results = await self._run_browser_tests(config)
159
+ visual_results = await self._run_visual_regression(config)
160
+ stress_results = await self._run_stress_tests(config)
161
+
162
+ # Capture screenshots
163
+ screenshots = await self._capture_screenshots(config)
164
+
165
+ # Build enhanced result
166
+ result = LiveTestResultV2(
167
+ base_result=base_result,
168
+ browser_tests=browser_results,
169
+ visual_regression=visual_results,
170
+ stress_test=stress_results,
171
+ recordings=[], # Would be populated if video recording enabled
172
+ screenshots=screenshots,
173
+ live_test_summary=self._build_live_summary(
174
+ base_result, browser_results, visual_results, stress_results
175
+ ),
176
+ )
177
+
178
+ return result
179
+
180
+ finally:
181
+ await self._cleanup_components()
182
+
183
+ async def _initialize_components(self, config: LiveTestConfigV2):
184
+ """Initialize browser pool, screenshot manager, video recorder."""
185
+ self.on_progress("🔧 Initializing test components...")
186
+
187
+ # Browser pool
188
+ self.browser_pool = await get_browser_pool(config.browser_config)
189
+
190
+ # Screenshot manager
191
+ self.screenshot_manager = ScreenshotManager(
192
+ baseline_dir=self.root / ".patchi" / "visual_baselines",
193
+ threshold=config.visual_threshold,
194
+ on_progress=self.on_progress,
195
+ )
196
+
197
+ # Video recorder (if enabled)
198
+ if config.record_video:
199
+ self.video_recorder = VideoRecorder(config.recording_config)
200
+
201
+ async def _cleanup_components(self):
202
+ """Clean up all components."""
203
+ if self.video_recorder:
204
+ await self.video_recorder.shutdown()
205
+
206
+ # Browser pool is global, don't shutdown here
207
+ # await shutdown_browser_pool()
208
+
209
+ async def _run_base_tests(self, config: LiveTestConfigV2) -> TestRunResult:
210
+ """Run base test agents (unit, regression, etc.)."""
211
+ from patchi.core.testing.live_test_runner import LiveTestRunner
212
+
213
+ base_config = TestRunConfig(
214
+ test_types=[t for t in config.test_types if t not in ["browser", "visual", "stress"]],
215
+ area=config.area,
216
+ base_url=config.base_url,
217
+ parallel=config.parallel,
218
+ timeout_per_agent=config.timeout_per_agent,
219
+ on_event=self.on_event,
220
+ )
221
+
222
+ base_runner = LiveTestRunner(self.root)
223
+ return await base_runner.run(base_config)
224
+
225
+ async def _run_browser_tests(self, config: LiveTestConfigV2) -> list[dict]:
226
+ """Run browser automation tests."""
227
+ if "browser" not in config.test_types or not config.base_url:
228
+ return []
229
+
230
+ self.on_progress("🌐 Running browser tests...")
231
+
232
+ page = await self.browser_pool.get_page()
233
+ results = []
234
+
235
+ try:
236
+ # Define browser test scenarios
237
+ scenarios = self._get_browser_scenarios(config)
238
+
239
+ for scenario in scenarios:
240
+ self.on_progress(f" ▶ {scenario['name']}")
241
+ result = await self._run_browser_scenario(page, scenario, config)
242
+ results.append(result)
243
+
244
+ self.on_event("browser_test_completed", result)
245
+
246
+ finally:
247
+ await self.browser_pool.release_page(page)
248
+
249
+ return results
250
+
251
+ def _get_browser_scenarios(self, config: LiveTestConfigV2) -> list[dict]:
252
+ """Get browser test scenarios based on config."""
253
+ scenarios = [
254
+ {
255
+ "name": "Homepage Load",
256
+ "url": config.base_url,
257
+ "actions": [
258
+ {"action": "goto", "url": config.base_url},
259
+ {"action": "wait_for_load_state", "state": "networkidle"},
260
+ {"action": "assert_title", "contains": "Home"},
261
+ ],
262
+ },
263
+ ]
264
+
265
+ # Add more scenarios based on test_types
266
+ if "e2e" in config.test_types:
267
+ scenarios.append(
268
+ {
269
+ "name": "User Login Flow",
270
+ "url": f"{config.base_url}/login",
271
+ "actions": [
272
+ {"action": "goto", "url": f"{config.base_url}/login"},
273
+ {"action": "fill", "selector": "#username", "value": "testuser"},
274
+ {"action": "fill", "selector": "#password", "value": "testpass"},
275
+ {"action": "click", "selector": "button[type=submit]"},
276
+ {"action": "wait_for_url", "url": "**/dashboard**"},
277
+ {"action": "assert_element", "selector": ".user-menu"},
278
+ ],
279
+ }
280
+ )
281
+
282
+ return scenarios
283
+
284
+ async def _run_browser_scenario(
285
+ self, page: Any, scenario: dict, config: LiveTestConfigV2
286
+ ) -> dict:
287
+ """Run a single browser test scenario."""
288
+ start_time = time.time()
289
+ result = {
290
+ "name": scenario["name"],
291
+ "url": scenario.get("url", config.base_url),
292
+ "steps": [],
293
+ "passed": True,
294
+ "error": None,
295
+ "duration_ms": 0,
296
+ "screenshots": [],
297
+ }
298
+
299
+ try:
300
+ for step in scenario.get("actions", []):
301
+ step_result = await self._execute_browser_step(page, step)
302
+ result["steps"].append(step_result)
303
+
304
+ if not step_result.get("success", False):
305
+ result["passed"] = False
306
+ result["error"] = step_result.get("error")
307
+ break
308
+
309
+ # Take final screenshot
310
+ if config.screenshot_config:
311
+ screenshot = await self.screenshot_manager.capture(
312
+ page,
313
+ scenario.get("url", config.base_url),
314
+ name=f"{scenario['name']}-final",
315
+ config=config.screenshot_config,
316
+ )
317
+ result["screenshots"].append(
318
+ {
319
+ "name": "final",
320
+ "base64": screenshot.image_base64[:100] + "...", # Truncated for logging
321
+ "dimensions": screenshot.dimensions,
322
+ }
323
+ )
324
+
325
+ except Exception as e:
326
+ result["passed"] = False
327
+ result["error"] = str(e)
328
+ _log.error(f"Browser scenario failed: {e}")
329
+
330
+ result["duration_ms"] = int((time.time() - start_time) * 1000)
331
+ return result
332
+
333
+ async def _execute_browser_step(self, page: Any, step: dict) -> dict:
334
+ """Execute a single browser test step."""
335
+ action = step.get("action")
336
+ step_result = {"action": action, "success": False}
337
+
338
+ try:
339
+ if action == "goto":
340
+ await page.goto(step["url"], wait_until="networkidle")
341
+ step_result["success"] = True
342
+
343
+ elif action == "click":
344
+ await page.click(step["selector"])
345
+ step_result["success"] = True
346
+
347
+ elif action == "fill":
348
+ await page.fill(step["selector"], step["value"])
349
+ step_result["success"] = True
350
+
351
+ elif action == "wait_for_load_state":
352
+ await page.wait_for_load_state(step.get("state", "networkidle"))
353
+ step_result["success"] = True
354
+
355
+ elif action == "wait_for_selector":
356
+ await page.wait_for_selector(step["selector"], timeout=step.get("timeout", 10000))
357
+ step_result["success"] = True
358
+
359
+ elif action == "wait_for_url":
360
+ await page.wait_for_url(step["url"], timeout=step.get("timeout", 10000))
361
+ step_result["success"] = True
362
+
363
+ elif action == "assert_title":
364
+ title = await page.title()
365
+ step_result["success"] = step["contains"] in title
366
+ step_result["actual_title"] = title
367
+
368
+ elif action == "assert_element":
369
+ element = await page.query_selector(step["selector"])
370
+ step_result["success"] = element is not None
371
+ step_result["element_found"] = element is not None
372
+
373
+ elif action == "assert_text":
374
+ element = await page.query_selector(step["selector"])
375
+ if element:
376
+ text = await element.text_content()
377
+ step_result["success"] = step["contains"] in text
378
+ step_result["actual_text"] = text
379
+ else:
380
+ step_result["success"] = False
381
+
382
+ elif action == "screenshot":
383
+ screenshot = await self.screenshot_manager.capture(
384
+ page,
385
+ page.url,
386
+ name=step.get("name", "step"),
387
+ config=self._config.screenshot_config if self._config else None,
388
+ )
389
+ step_result["success"] = True
390
+ step_result["screenshot"] = screenshot.image_base64[:100] + "..."
391
+
392
+ else:
393
+ step_result["error"] = f"Unknown action: {action}"
394
+
395
+ except Exception as e:
396
+ step_result["error"] = str(e)
397
+
398
+ return step_result
399
+
400
+ async def _run_visual_regression(self, config: LiveTestConfigV2) -> dict | None:
401
+ """Run visual regression tests."""
402
+ if "visual" not in config.test_types or not config.base_url:
403
+ return None
404
+
405
+ self.on_progress("👁️ Running visual regression tests...")
406
+
407
+ agent = _AsyncVisualRegressionAgent(
408
+ self.root,
409
+ {
410
+ "visual_threshold": config.visual_threshold,
411
+ },
412
+ )
413
+
414
+ # Define URLs to test
415
+ urls = [
416
+ config.base_url,
417
+ f"{config.base_url}/login",
418
+ f"{config.base_url}/dashboard",
419
+ ]
420
+
421
+ try:
422
+ result = await agent.run(
423
+ urls=urls,
424
+ base_url=config.base_url,
425
+ update_baselines=config.update_baselines,
426
+ )
427
+
428
+ self.on_progress(
429
+ f" Visual regression: {result['passed']} passed, {result['failed']} failed"
430
+ )
431
+ self.on_event("visual_regression_completed", result)
432
+
433
+ return result
434
+ except Exception as e:
435
+ _log.error(f"Visual regression failed: {e}")
436
+ return {"error": str(e)}
437
+
438
+ async def _run_stress_tests(self, config: LiveTestConfigV2) -> StressTestReport | None:
439
+ """Run stress/load tests."""
440
+ if "stress" not in config.test_types or not config.stress_config:
441
+ return None
442
+
443
+ self.on_progress("⚡ Running stress tests...")
444
+
445
+ try:
446
+ orchestrator = StressOrchestrator(config.stress_config, on_progress=self.on_progress)
447
+ report = await orchestrator.run()
448
+
449
+ self.on_progress(
450
+ f" Stress test: {report.requests_per_second:.1f} req/s, P95: {report.latency.get('p95', 0):.0f}ms"
451
+ )
452
+ self.on_event("stress_test_completed", report.__dict__)
453
+
454
+ return report
455
+ except Exception as e:
456
+ _log.error(f"Stress test failed: {e}")
457
+ return None
458
+
459
+ async def _capture_screenshots(self, config: LiveTestConfigV2) -> list[dict]:
460
+ """Capture screenshots for key pages."""
461
+ if not config.base_url:
462
+ return []
463
+
464
+ page = await self.browser_pool.get_page()
465
+ screenshots = []
466
+
467
+ try:
468
+ urls = [
469
+ config.base_url,
470
+ f"{config.base_url}/login",
471
+ f"{config.base_url}/dashboard",
472
+ ]
473
+
474
+ for url in urls:
475
+ try:
476
+ result = await self.screenshot_manager.capture(
477
+ page, url, config=config.screenshot_config
478
+ )
479
+ screenshots.append(
480
+ {
481
+ "url": url,
482
+ "dimensions": result.dimensions,
483
+ "timestamp": result.timestamp,
484
+ "base64_preview": result.image_base64[:100] + "...",
485
+ }
486
+ )
487
+ except Exception as e:
488
+ _log.warning(f"Screenshot failed for {url}: {e}")
489
+ screenshots.append({"url": url, "error": str(e)})
490
+
491
+ finally:
492
+ await self.browser_pool.release_page(page)
493
+
494
+ return screenshots
495
+
496
+ def _build_live_summary(
497
+ self,
498
+ base_result: TestRunResult,
499
+ browser_tests: list[dict],
500
+ visual_results: dict | None,
501
+ stress_results: StressTestReport | None,
502
+ ) -> dict:
503
+ """Build live testing summary."""
504
+ browser_passed = sum(1 for t in browser_tests if t.get("passed"))
505
+ browser_total = len(browser_tests)
506
+
507
+ visual_passed = visual_results.get("passed", 0) if visual_results else 0
508
+ visual_total = visual_results.get("total", 0) if visual_results else 0
509
+
510
+ return {
511
+ "base_tests": {
512
+ "total_agents": len(base_result.results),
513
+ "total_findings": base_result.total_findings,
514
+ "passed": base_result.total_passed,
515
+ "failed": base_result.total_failed,
516
+ },
517
+ "browser_tests": {
518
+ "total": browser_total,
519
+ "passed": browser_passed,
520
+ "failed": browser_total - browser_passed,
521
+ },
522
+ "visual_regression": {
523
+ "total": visual_total,
524
+ "passed": visual_passed,
525
+ "failed": visual_total - visual_passed,
526
+ },
527
+ "stress_test": {
528
+ "completed": stress_results is not None,
529
+ "requests_per_second": stress_results.requests_per_second if stress_results else 0,
530
+ "latency_p95_ms": stress_results.latency.get("p95", 0) if stress_results else 0,
531
+ "error_rate": stress_results.failed_requests / stress_results.total_requests
532
+ if stress_results and stress_results.total_requests > 0
533
+ else 0,
534
+ },
535
+ "overall_duration_ms": int((time.time() - self._start_time) * 1000),
536
+ }
537
+
538
+
539
+ # Convenience function
540
+ async def run_live_tests_v2(
541
+ root: Path,
542
+ config: LiveTestConfigV2,
543
+ on_progress: Callable[[str], None] = None,
544
+ ) -> LiveTestResultV2:
545
+ """Run enhanced live tests."""
546
+ runner = LiveTestRunnerV2(root, on_progress=on_progress)
547
+ return await runner.run(config)
548
+
549
+
550
+ # Agent wrapper for integration
551
+ @register
552
+ class LiveTestRunnerV2Agent(BaseAgent):
553
+ """Live Test Runner v2 as a Patchi agent."""
554
+
555
+ name = "LiveTestRunnerV2Agent"
556
+ group = AgentGroup.TEST
557
+ timeout = 600
558
+
559
+ def _run(self, inp: AgentInput, result: AgentResult) -> None:
560
+ # Get config from extra
561
+ config_data = inp.extra.get("live_test_config", {})
562
+
563
+ config = LiveTestConfigV2(
564
+ test_types=config_data.get("test_types", ["unit", "regression"]),
565
+ area=inp.scope[0] if inp.scope else None,
566
+ base_url=config_data.get("base_url"),
567
+ parallel=config_data.get("parallel", False),
568
+ )
569
+
570
+ # Parse stress config if present
571
+ if "stress" in config.test_types and config_data.get("stress_config"):
572
+ sc = config_data["stress_config"]
573
+ config.stress_config = StressConfig(
574
+ base_url=sc.get("base_url", config.base_url),
575
+ scenario=sc.get("scenario", "load"),
576
+ users=sc.get("users", 10),
577
+ duration_seconds=sc.get("duration_seconds", 60),
578
+ )
579
+
580
+ async def run():
581
+ runner = LiveTestRunnerV2(inp.root, on_progress=lambda m: result.add_log(m))
582
+ return await runner.run(config)
583
+
584
+ try:
585
+ loop = asyncio.get_event_loop()
586
+ except RuntimeError:
587
+ loop = asyncio.new_event_loop()
588
+ asyncio.set_event_loop(loop)
589
+
590
+ test_result = loop.run_until_complete(run())
591
+
592
+ # Add findings from base result
593
+ for agent_result in test_result.base_result.results:
594
+ for finding in agent_result.findings:
595
+ result.add_finding(finding)
596
+
597
+ # Add live test findings
598
+ if test_result.visual_regression and test_result.visual_regression.get("failed", 0) > 0:
599
+ for diff in test_result.visual_regression.get("diffs", []):
600
+ if not diff.get("passed", True):
601
+ result.add_finding(
602
+ Finding(
603
+ agent="LiveTestRunnerV2Agent",
604
+ type="visual_regression",
605
+ severity=Severity.MEDIUM,
606
+ file="",
607
+ line=0,
608
+ message=f"Visual regression detected: {diff.get('difference_percent', 0):.1f}% difference",
609
+ detail="Baseline comparison failed for visual test",
610
+ )
611
+ )
612
+
613
+ if test_result.stress_test and test_result.stress_test.failed_requests > 0:
614
+ result.add_finding(
615
+ Finding(
616
+ agent="LiveTestRunnerV2Agent",
617
+ type="stress_test_failure",
618
+ severity=Severity.HIGH,
619
+ file="",
620
+ line=0,
621
+ message=f"Stress test failures: {test_result.stress_test.failed_requests}/{test_result.stress_test.total_requests} requests failed",
622
+ detail=f"Error rate: {test_result.stress_test.failed_requests / test_result.stress_test.total_requests * 100:.1f}%",
623
+ )
624
+ )
625
+
626
+ result.data["live_test_result"] = test_result.to_dict()