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.
- patchi/__init__.py +1 -0
- patchi/__main__.py +13 -0
- patchi/cli/__init__.py +0 -0
- patchi/cli/command_families.py +235 -0
- patchi/cli/commands/__init__.py +0 -0
- patchi/cli/commands/access_cmd.py +65 -0
- patchi/cli/commands/agent_stats_cmd.py +273 -0
- patchi/cli/commands/agents_cmd.py +187 -0
- patchi/cli/commands/ai_cmd.py +308 -0
- patchi/cli/commands/assure_cmd.py +538 -0
- patchi/cli/commands/audit_cmd.py +286 -0
- patchi/cli/commands/auto_cmd.py +110 -0
- patchi/cli/commands/blame_cmd.py +53 -0
- patchi/cli/commands/chains_cmd.py +327 -0
- patchi/cli/commands/charter_cmd.py +173 -0
- patchi/cli/commands/chat_cmd.py +597 -0
- patchi/cli/commands/check_cmd.py +232 -0
- patchi/cli/commands/cleanup_cmd.py +328 -0
- patchi/cli/commands/cockpit_cmd.py +208 -0
- patchi/cli/commands/commands_cmd.py +160 -0
- patchi/cli/commands/cross_repo_cmd.py +113 -0
- patchi/cli/commands/deps_cmd.py +110 -0
- patchi/cli/commands/dev_check_cmd.py +342 -0
- patchi/cli/commands/dev_cmd.py +445 -0
- patchi/cli/commands/doctor_cmd.py +457 -0
- patchi/cli/commands/findings_cmd.py +187 -0
- patchi/cli/commands/fix_cmd.py +648 -0
- patchi/cli/commands/fix_review_cmd.py +273 -0
- patchi/cli/commands/heatmap_cmd.py +62 -0
- patchi/cli/commands/help_cmd.py +159 -0
- patchi/cli/commands/hosted_cmd.py +980 -0
- patchi/cli/commands/init.py +456 -0
- patchi/cli/commands/key_cmd.py +571 -0
- patchi/cli/commands/learn_cmd.py +140 -0
- patchi/cli/commands/link_cmd.py +104 -0
- patchi/cli/commands/log_cmd.py +57 -0
- patchi/cli/commands/memory_cmd.py +215 -0
- patchi/cli/commands/model_cmd.py +207 -0
- patchi/cli/commands/notify_cmd.py +213 -0
- patchi/cli/commands/notify_prompts.py +29 -0
- patchi/cli/commands/patch_cmd.py +231 -0
- patchi/cli/commands/plan_cmd.py +128 -0
- patchi/cli/commands/plugins_cmd.py +307 -0
- patchi/cli/commands/queue_cmd.py +152 -0
- patchi/cli/commands/quick_cmd.py +159 -0
- patchi/cli/commands/ready_cmd.py +292 -0
- patchi/cli/commands/report_cmd.py +416 -0
- patchi/cli/commands/restrict_cmd.py +127 -0
- patchi/cli/commands/review_cmd.py +225 -0
- patchi/cli/commands/rules_cmd.py +233 -0
- patchi/cli/commands/scan_cmd.py +1908 -0
- patchi/cli/commands/settings_cmd.py +45 -0
- patchi/cli/commands/status_cmd.py +679 -0
- patchi/cli/commands/test_cmd.py +1052 -0
- patchi/cli/commands/trend_cmd.py +296 -0
- patchi/cli/commands/undo_cmd.py +235 -0
- patchi/cli/commands/update_cmd.py +345 -0
- patchi/cli/commands/verify_cmd.py +56 -0
- patchi/cli/commands/vr_cmd.py +440 -0
- patchi/cli/commands/watch_cmd.py +304 -0
- patchi/cli/commands/web_cmd.py +114 -0
- patchi/cli/console.py +101 -0
- patchi/cli/display/__init__.py +1 -0
- patchi/cli/display/live_audit.py +269 -0
- patchi/cli/display/live_progress.py +169 -0
- patchi/cli/framework.py +241 -0
- patchi/cli/logo.py +128 -0
- patchi/cli/main.py +222 -0
- patchi/cli/registry.py +1171 -0
- patchi/cli/style.py +68 -0
- patchi/cli/themes.py +187 -0
- patchi/cli/ux.py +444 -0
- patchi/core/__init__.py +0 -0
- patchi/core/agents/__init__.py +0 -0
- patchi/core/agents/api_fuzzer_agent.py +141 -0
- patchi/core/agents/attack_agent.py +242 -0
- patchi/core/agents/base.py +1012 -0
- patchi/core/agents/bug_predictor_agent.py +58 -0
- patchi/core/agents/build_tool_validator.py +150 -0
- patchi/core/agents/cache.py +192 -0
- patchi/core/agents/chaos_agent.py +133 -0
- patchi/core/agents/cicd_generator.py +240 -0
- patchi/core/agents/comment_scanner.py +280 -0
- patchi/core/agents/console_logging_agent.py +156 -0
- patchi/core/agents/coordinator.py +858 -0
- patchi/core/agents/core_scanner.py +269 -0
- patchi/core/agents/coverage_prioritizer.py +181 -0
- patchi/core/agents/dead_code_hygiene.py +296 -0
- patchi/core/agents/dead_code_scanner.py +692 -0
- patchi/core/agents/dependency_scanner.py +725 -0
- patchi/core/agents/doc_claim_agent.py +573 -0
- patchi/core/agents/duplicate_scanner.py +568 -0
- patchi/core/agents/env_scanner.py +410 -0
- patchi/core/agents/feature_flag_agent.py +89 -0
- patchi/core/agents/frontend_framework_agent.py +133 -0
- patchi/core/agents/goroutine_agent.py +82 -0
- patchi/core/agents/governor.py +1972 -0
- patchi/core/agents/i18n_agent.py +95 -0
- patchi/core/agents/license_compliance.py +247 -0
- patchi/core/agents/linking_agent.py +251 -0
- patchi/core/agents/memory_profiler_agent.py +102 -0
- patchi/core/agents/modernization_agent.py +84 -0
- patchi/core/agents/mutation_agent.py +174 -0
- patchi/core/agents/nl_query_agent.py +74 -0
- patchi/core/agents/promise_rejection_agent.py +100 -0
- patchi/core/agents/refactoring_agent.py +286 -0
- patchi/core/agents/resource_leak_agent.py +91 -0
- patchi/core/agents/route_graph_scanner.py +951 -0
- patchi/core/agents/run_agent.py +106 -0
- patchi/core/agents/rust_unwrap_agent.py +100 -0
- patchi/core/agents/sbom_generator.py +284 -0
- patchi/core/agents/scanners.py +67 -0
- patchi/core/agents/side/__init__.py +1 -0
- patchi/core/agents/side/build_agent.py +65 -0
- patchi/core/agents/side/format_agent.py +50 -0
- patchi/core/agents/side/install_agent.py +63 -0
- patchi/core/agents/side_file_scanner.py +549 -0
- patchi/core/agents/snapshot_drift_detector.py +146 -0
- patchi/core/agents/spa_route_inventory.py +203 -0
- patchi/core/agents/test_scanner.py +442 -0
- patchi/core/agents/tool_health.py +109 -0
- patchi/core/agents/tool_runner.py +162 -0
- patchi/core/agents/type_scanner.py +168 -0
- patchi/core/agents/ui_scanner.py +558 -0
- patchi/core/ai/__init__.py +20 -0
- patchi/core/ai/agent_audit.py +153 -0
- patchi/core/ai/agent_profiler.py +277 -0
- patchi/core/ai/client.py +499 -0
- patchi/core/ai/cost_tracker.py +107 -0
- patchi/core/ai/model_router.py +318 -0
- patchi/core/ai/orchestrator.py +625 -0
- patchi/core/ai/prompts.py +681 -0
- patchi/core/ai/smart.py +302 -0
- patchi/core/ai/tool_executor.py +24 -0
- patchi/core/ai/tools/__init__.py +10 -0
- patchi/core/ai/tools/executor.py +621 -0
- patchi/core/ai/tools/realize.py +1160 -0
- patchi/core/ai/tools/registry.py +1577 -0
- patchi/core/assurance/__init__.py +33 -0
- patchi/core/assurance/builder.py +211 -0
- patchi/core/assurance/graph.py +342 -0
- patchi/core/assurance/invariants.py +168 -0
- patchi/core/atomic.py +61 -0
- patchi/core/attackers/__init__.py +17 -0
- patchi/core/attackers/api_abuse.py +89 -0
- patchi/core/attackers/auth_bypass.py +73 -0
- patchi/core/attackers/base.py +91 -0
- patchi/core/attackers/business_logic.py +88 -0
- patchi/core/attackers/planner.py +108 -0
- patchi/core/attackers/priv_escalation.py +88 -0
- patchi/core/attackers/recon.py +87 -0
- patchi/core/brain/__init__.py +0 -0
- patchi/core/brain/ast_utils/__init__.py +81 -0
- patchi/core/brain/ast_utils/assignments.py +219 -0
- patchi/core/brain/ast_utils/calls.py +139 -0
- patchi/core/brain/ast_utils/config.py +132 -0
- patchi/core/brain/ast_utils/control_flow.py +74 -0
- patchi/core/brain/ast_utils/dead_symbols.py +205 -0
- patchi/core/brain/ast_utils/decorators.py +108 -0
- patchi/core/brain/ast_utils/helpers.py +144 -0
- patchi/core/brain/ast_utils/imports.py +169 -0
- patchi/core/brain/ast_utils/scan.py +143 -0
- patchi/core/brain/ast_utils/taint.py +310 -0
- patchi/core/brain/audit.py +379 -0
- patchi/core/brain/baseline.py +195 -0
- patchi/core/brain/blast_radius.py +312 -0
- patchi/core/brain/body_tags.py +144 -0
- patchi/core/brain/brain.py +1161 -0
- patchi/core/brain/brain_context.py +393 -0
- patchi/core/brain/brain_watcher.py +340 -0
- patchi/core/brain/charter.py +473 -0
- patchi/core/brain/classifier.py +214 -0
- patchi/core/brain/cockpit.py +350 -0
- patchi/core/brain/code_query.py +710 -0
- patchi/core/brain/contract.py +889 -0
- patchi/core/brain/contract_diff.py +155 -0
- patchi/core/brain/council.py +800 -0
- patchi/core/brain/cross_repo.py +104 -0
- patchi/core/brain/dead_code_tools.py +157 -0
- patchi/core/brain/doc_validator.py +197 -0
- patchi/core/brain/domain_activator.py +2183 -0
- patchi/core/brain/enriched_context.py +220 -0
- patchi/core/brain/file_corpus.py +220 -0
- patchi/core/brain/framework.py +953 -0
- patchi/core/brain/freshness.py +273 -0
- patchi/core/brain/git_aware.py +206 -0
- patchi/core/brain/heatmap.py +104 -0
- patchi/core/brain/ignore_parser.py +153 -0
- patchi/core/brain/import_graph.py +486 -0
- patchi/core/brain/languages.py +397 -0
- patchi/core/brain/layered_brain.py +419 -0
- patchi/core/brain/learning.py +229 -0
- patchi/core/brain/orphaned_endpoints.py +313 -0
- patchi/core/brain/personas/__init__.py +648 -0
- patchi/core/brain/personas/bad_user.py +165 -0
- patchi/core/brain/personas/base.py +349 -0
- patchi/core/brain/proactive.py +807 -0
- patchi/core/brain/project_context.py +121 -0
- patchi/core/brain/project_reader.py +521 -0
- patchi/core/brain/reasoning.py +269 -0
- patchi/core/brain/route_detector/__init__.py +15 -0
- patchi/core/brain/route_detector/base.py +31 -0
- patchi/core/brain/route_detector/csharp.py +209 -0
- patchi/core/brain/route_detector/file_based.py +81 -0
- patchi/core/brain/route_detector/go.py +152 -0
- patchi/core/brain/route_detector/java.py +195 -0
- patchi/core/brain/route_detector/javascript.py +197 -0
- patchi/core/brain/route_detector/php.py +158 -0
- patchi/core/brain/route_detector/python.py +193 -0
- patchi/core/brain/route_detector/registry.py +25 -0
- patchi/core/brain/route_detector/ruby.py +134 -0
- patchi/core/brain/route_detector/rust.py +216 -0
- patchi/core/brain/route_detector/swift.py +118 -0
- patchi/core/brain/route_mapper.py +265 -0
- patchi/core/brain/scanner.py +2136 -0
- patchi/core/brain/secrets.py +280 -0
- patchi/core/brain/symbol_graph.py +1955 -0
- patchi/core/brain/team.py +28 -0
- patchi/core/brain/trace_log.py +132 -0
- patchi/core/brain/type_checker/__init__.py +65 -0
- patchi/core/brain/type_checker/base.py +60 -0
- patchi/core/brain/type_checker/csharp.py +59 -0
- patchi/core/brain/type_checker/dart.py +59 -0
- patchi/core/brain/type_checker/go.py +81 -0
- patchi/core/brain/type_checker/java.py +75 -0
- patchi/core/brain/type_checker/kotlin.py +75 -0
- patchi/core/brain/type_checker/php.py +88 -0
- patchi/core/brain/type_checker/python.py +87 -0
- patchi/core/brain/type_checker/rust.py +81 -0
- patchi/core/brain/type_checker/swift.py +59 -0
- patchi/core/brain/type_checker/typescript.py +298 -0
- patchi/core/brain/understander.py +130 -0
- patchi/core/brain/verify.py +379 -0
- patchi/core/campaigns/__init__.py +14 -0
- patchi/core/campaigns/base.py +62 -0
- patchi/core/campaigns/data_flow_abuse.py +153 -0
- patchi/core/campaigns/orchestrator.py +140 -0
- patchi/core/campaigns/state_transitions.py +178 -0
- patchi/core/ci_bundle.py +104 -0
- patchi/core/config.py +311 -0
- patchi/core/constants.py +245 -0
- patchi/core/debug/__init__.py +25 -0
- patchi/core/debug/adapters/__init__.py +11 -0
- patchi/core/debug/adapters/codelldb.py +379 -0
- patchi/core/debug/adapters/node.py +222 -0
- patchi/core/debug/adapters/powershell.py +210 -0
- patchi/core/debug/adapters/python.py +244 -0
- patchi/core/debug/capture.py +210 -0
- patchi/core/debug/dap_client.py +268 -0
- patchi/core/detector/__init__.py +0 -0
- patchi/core/detector/audit.py +194 -0
- patchi/core/detector/bus.py +226 -0
- patchi/core/detector/dispatcher.py +275 -0
- patchi/core/detector/event.py +254 -0
- patchi/core/detector/sigma_engine.py +378 -0
- patchi/core/detector/triage.py +289 -0
- patchi/core/export/sarif.py +113 -0
- patchi/core/fix/__init__.py +17 -0
- patchi/core/fix/applier.py +574 -0
- patchi/core/fix/base.py +510 -0
- patchi/core/fix/code_fixer.py +169 -0
- patchi/core/fix/dead_code_remover.py +131 -0
- patchi/core/fix/fix_agents.py +775 -0
- patchi/core/fix/patch.py +444 -0
- patchi/core/fix/risk_gate.py +383 -0
- patchi/core/fix/security_fixer.py +93 -0
- patchi/core/fix/verify_loop.py +204 -0
- patchi/core/fuzz/__init__.py +8 -0
- patchi/core/fuzz/corpus.py +137 -0
- patchi/core/fuzz/input_fuzzer.py +256 -0
- patchi/core/fuzz/sequence_fuzzer.py +162 -0
- patchi/core/fuzz/state_fuzzer.py +236 -0
- patchi/core/health.py +407 -0
- patchi/core/hosted/__init__.py +1 -0
- patchi/core/hosted/anomaly.py +298 -0
- patchi/core/hosted/audit_log.py +102 -0
- patchi/core/hosted/compliance_report.py +148 -0
- patchi/core/hosted/ip_reputation.py +169 -0
- patchi/core/hosted/log_parsers.py +251 -0
- patchi/core/hosted/tokens.py +127 -0
- patchi/core/hosted/watchlist.py +164 -0
- patchi/core/hosted/webhooks.py +156 -0
- patchi/core/memory.py +437 -0
- patchi/core/notifications/__init__.py +18 -0
- patchi/core/notifications/channels.py +192 -0
- patchi/core/notifications/digest.py +141 -0
- patchi/core/notifications/escalation.py +158 -0
- patchi/core/notifications/notifier.py +182 -0
- patchi/core/notifications/quiet_hours.py +69 -0
- patchi/core/plugins/__init__.py +48 -0
- patchi/core/plugins/analyzer.py +326 -0
- patchi/core/plugins/analyzers/__init__.py +6 -0
- patchi/core/plugins/analyzers/dead_code_analyzer.py +120 -0
- patchi/core/plugins/analyzers/secret_scanner.py +158 -0
- patchi/core/plugins/analyzers/todo_scanner.py +115 -0
- patchi/core/plugins/registry.py +335 -0
- patchi/core/queue.py +375 -0
- patchi/core/reliability/__init__.py +13 -0
- patchi/core/reliability/chaos.py +163 -0
- patchi/core/reliability/fault_injector.py +172 -0
- patchi/core/reliability/idempotency.py +128 -0
- patchi/core/reliability/recovery.py +147 -0
- patchi/core/runtime/__init__.py +5 -0
- patchi/core/runtime/tracer.py +230 -0
- patchi/core/scan_bus.py +156 -0
- patchi/core/security/__init__.py +1 -0
- patchi/core/security/ai_validator.py +462 -0
- patchi/core/security/app_mapper.py +216 -0
- patchi/core/security/app_profile.py +464 -0
- patchi/core/security/attack_feedback.py +141 -0
- patchi/core/security/attack_scenarios/__init__.py +1 -0
- patchi/core/security/attack_tree.py +176 -0
- patchi/core/security/auth_audit_agent.py +344 -0
- patchi/core/security/authz_agent.py +602 -0
- patchi/core/security/auto_fix_proactive.py +620 -0
- patchi/core/security/auto_fixer.py +660 -0
- patchi/core/security/auto_ticket.py +53 -0
- patchi/core/security/bandit_agent.py +152 -0
- patchi/core/security/blast_radius.py +226 -0
- patchi/core/security/browser_tester.py +333 -0
- patchi/core/security/business_logic_agent.py +263 -0
- patchi/core/security/catch_block_auditor.py +395 -0
- patchi/core/security/cdn_cache_agent.py +357 -0
- patchi/core/security/chain_analyzer.py +321 -0
- patchi/core/security/chain_to_assurance.py +168 -0
- patchi/core/security/charter.py +510 -0
- patchi/core/security/cloud_waf_detector.py +295 -0
- patchi/core/security/codeql_agent.py +233 -0
- patchi/core/security/compliance_agent.py +348 -0
- patchi/core/security/confidence_gate.py +423 -0
- patchi/core/security/container_scanner.py +282 -0
- patchi/core/security/crypto_agent.py +378 -0
- patchi/core/security/cve_monitor.py +262 -0
- patchi/core/security/dast_agent.py +865 -0
- patchi/core/security/defectdojo.py +238 -0
- patchi/core/security/defenders/__init__.py +75 -0
- patchi/core/security/defenders/auth_middleware.py +115 -0
- patchi/core/security/defenders/base.py +97 -0
- patchi/core/security/defenders/block_ip.py +71 -0
- patchi/core/security/defenders/block_ws_origin.py +36 -0
- patchi/core/security/defenders/crypto_fix.py +118 -0
- patchi/core/security/defenders/enforce_rate_limit.py +108 -0
- patchi/core/security/defenders/fix_code.py +93 -0
- patchi/core/security/defenders/invalidate_session.py +40 -0
- patchi/core/security/defenders/patch_config.py +70 -0
- patchi/core/security/defenders/rotate_secret.py +54 -0
- patchi/core/security/defenders/suspend_account.py +41 -0
- patchi/core/security/defenders/update_dependency.py +160 -0
- patchi/core/security/defense_layer.py +217 -0
- patchi/core/security/dependency_vulnerability_agent.py +616 -0
- patchi/core/security/detection_pipeline.py +289 -0
- patchi/core/security/dns_security_agent.py +497 -0
- patchi/core/security/domain_activator_v2.py +913 -0
- patchi/core/security/domain_loader.py +1004 -0
- patchi/core/security/domains/__init__.py +0 -0
- patchi/core/security/email_authentication_agent.py +417 -0
- patchi/core/security/env_var_validator.py +304 -0
- patchi/core/security/evidence.py +100 -0
- patchi/core/security/falco_runtime_agent.py +387 -0
- patchi/core/security/fix-playbooks/__init__.py +0 -0
- patchi/core/security/gated_finding.py +85 -0
- patchi/core/security/git_diff_activator.py +367 -0
- patchi/core/security/governance.py +208 -0
- patchi/core/security/gradual_gate.py +34 -0
- patchi/core/security/history.py +315 -0
- patchi/core/security/iac_scanner.py +250 -0
- patchi/core/security/ignore_expiry.py +111 -0
- patchi/core/security/ignore_learner.py +416 -0
- patchi/core/security/injection_agent.py +319 -0
- patchi/core/security/insecure_randomness_agent.py +288 -0
- patchi/core/security/intent_analyzer.py +326 -0
- patchi/core/security/jwt_agent.py +453 -0
- patchi/core/security/kubernetes_agent.py +638 -0
- patchi/core/security/layer2_orchestrator.py +341 -0
- patchi/core/security/llm_security_agent.py +252 -0
- patchi/core/security/misconfig_agent.py +600 -0
- patchi/core/security/mobile_security_agent.py +474 -0
- patchi/core/security/network_agent.py +376 -0
- patchi/core/security/noise_filter.py +274 -0
- patchi/core/security/orchestrator.py +313 -0
- patchi/core/security/pattern_context.py +125 -0
- patchi/core/security/pattern_loader.py +101 -0
- patchi/core/security/pentest/__init__.py +4 -0
- patchi/core/security/pentest/base.py +58 -0
- patchi/core/security/pentest/dalfox_adapter.py +40 -0
- patchi/core/security/pentest/ffuf_adapter.py +48 -0
- patchi/core/security/pentest/nuclei_adapter.py +50 -0
- patchi/core/security/pentest/registry.py +80 -0
- patchi/core/security/pentest/shannon_adapter.py +107 -0
- patchi/core/security/pentest/sqlmap_adapter.py +56 -0
- patchi/core/security/pentest/zap_adapter.py +52 -0
- patchi/core/security/plan_auditor.py +276 -0
- patchi/core/security/policy_engine.py +272 -0
- patchi/core/security/prechecks.py +172 -0
- patchi/core/security/precommit_hook.py +247 -0
- patchi/core/security/privacy_agent.py +361 -0
- patchi/core/security/push_notification_agent.py +417 -0
- patchi/core/security/pysa_agent.py +143 -0
- patchi/core/security/reasoning.py +442 -0
- patchi/core/security/red_team_agent.py +184 -0
- patchi/core/security/red_team_engine.py +1142 -0
- patchi/core/security/remediation.py +360 -0
- patchi/core/security/request_interceptor.py +294 -0
- patchi/core/security/runtime_validator.py +245 -0
- patchi/core/security/saml_sso_agent.py +401 -0
- patchi/core/security/sast_agent.py +213 -0
- patchi/core/security/sast_gate.py +95 -0
- patchi/core/security/scheduler.py +287 -0
- patchi/core/security/secrets_runtime_agent.py +476 -0
- patchi/core/security/security_agents.py +175 -0
- patchi/core/security/security_config.py +551 -0
- patchi/core/security/security_probe.py +762 -0
- patchi/core/security/security_taint.py +1005 -0
- patchi/core/security/sensitive_data_agent.py +271 -0
- patchi/core/security/service_mesh_agent.py +412 -0
- patchi/core/security/session_management_agent.py +245 -0
- patchi/core/security/ssrf_agent.py +422 -0
- patchi/core/security/supply_chain.py +449 -0
- patchi/core/security/threat_model_generator.py +378 -0
- patchi/core/security/threat_model_updater.py +196 -0
- patchi/core/security/tool_adapters.py +135 -0
- patchi/core/security/tool_verify.py +173 -0
- patchi/core/security/websocket_security_agent.py +188 -0
- patchi/core/snapshot.py +242 -0
- patchi/core/tenant.py +386 -0
- patchi/core/testing/__init__.py +17 -0
- patchi/core/testing/_browser.py +147 -0
- patchi/core/testing/ads_agent.py +141 -0
- patchi/core/testing/api_contract_agent.py +450 -0
- patchi/core/testing/app_discovery_agent.py +641 -0
- patchi/core/testing/app_launcher.py +184 -0
- patchi/core/testing/browser_test_agent.py +516 -0
- patchi/core/testing/e2e_flow_agent.py +354 -0
- patchi/core/testing/flake_detector_agent.py +292 -0
- patchi/core/testing/gate.py +31 -0
- patchi/core/testing/live_test_runner.py +304 -0
- patchi/core/testing/live_v2/__init__.py +1 -0
- patchi/core/testing/live_v2/browser_pool.py +349 -0
- patchi/core/testing/live_v2/browser_test_runner.py +313 -0
- patchi/core/testing/live_v2/runner.py +626 -0
- patchi/core/testing/live_v2/screenshot_manager.py +474 -0
- patchi/core/testing/live_v2/stress_orchestrator.py +523 -0
- patchi/core/testing/live_v2/video_recorder.py +303 -0
- patchi/core/testing/regression_agent.py +302 -0
- patchi/core/testing/security_test_agent.py +253 -0
- patchi/core/testing/stress_test_agent.py +203 -0
- patchi/core/testing/test_agents.py +50 -0
- patchi/core/testing/test_config.py +194 -0
- patchi/core/testing/ui_accessibility_agent.py +324 -0
- patchi/core/testing/ui_button_agent.py +294 -0
- patchi/core/testing/ui_layout_agent.py +330 -0
- patchi/core/testing/unit_test_agent.py +1149 -0
- patchi/core/testing/visual_regression_agent.py +260 -0
- patchi/web/__init__.py +1 -0
- patchi/web/api/__init__.py +1 -0
- patchi/web/api/brain_map.py +114 -0
- patchi/web/api/charts.py +47 -0
- patchi/web/api/chat.py +224 -0
- patchi/web/api/cicd.py +576 -0
- patchi/web/api/dast_evidence.py +49 -0
- patchi/web/api/dev_check.py +320 -0
- patchi/web/api/fix.py +134 -0
- patchi/web/api/guard.py +65 -0
- patchi/web/api/hosted.py +128 -0
- patchi/web/api/hosted_v2.py +234 -0
- patchi/web/api/live_testing.py +699 -0
- patchi/web/api/scan.py +582 -0
- patchi/web/api/smart.py +111 -0
- patchi/web/api/tenant.py +117 -0
- patchi/web/api/tools.py +85 -0
- patchi/web/api_legacy.py +2401 -0
- patchi/web/app.py +310 -0
- patchi/web/events.py +483 -0
- patchi/web/routes/__init__.py +1 -0
- patchi/web/routes/assurance.py +541 -0
- patchi/web/routes/brain.py +36 -0
- patchi/web/routes/charter.py +227 -0
- patchi/web/routes/chat.py +23 -0
- patchi/web/routes/dashboard.py +558 -0
- patchi/web/routes/dashboard_v2.py +402 -0
- patchi/web/routes/findings.py +330 -0
- patchi/web/routes/guard.py +35 -0
- patchi/web/routes/history.py +126 -0
- patchi/web/routes/landing.py +52 -0
- patchi/web/routes/live_testing.py +24 -0
- patchi/web/routes/review.py +57 -0
- patchi/web/routes/self_improvement.py +110 -0
- patchi/web/routes/settings.py +49 -0
- patchi/web/routes/tokens.py +29 -0
- patchi/web/spawn.py +139 -0
- patchi/web/ws.py +141 -0
- patchi-0.7.5.dist-info/METADATA +315 -0
- patchi-0.7.5.dist-info/RECORD +496 -0
- patchi-0.7.5.dist-info/WHEEL +5 -0
- patchi-0.7.5.dist-info/licenses/LICENSE +60 -0
- 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()
|