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,354 @@
|
|
|
1
|
+
"""
|
|
2
|
+
E2EFlowAgent — AI-generated end-to-end test scripts via Playwright.
|
|
3
|
+
|
|
4
|
+
Uses the AI layer to:
|
|
5
|
+
- Analyze the app's HTML/routes and generate Playwright test scripts
|
|
6
|
+
- Test full user flows (login → dashboard → action → logout)
|
|
7
|
+
- Test error states and edge cases
|
|
8
|
+
- Generate tests from app contract critical flows
|
|
9
|
+
- Run generated tests and report results
|
|
10
|
+
|
|
11
|
+
Each test script is generated as a standalone Python file using playwright.sync_api.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import re
|
|
17
|
+
import subprocess
|
|
18
|
+
import sys
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from patchi.core.brain.languages import DEFAULT_IGNORE_DIRS
|
|
22
|
+
|
|
23
|
+
from ..agents.base import (
|
|
24
|
+
AgentDomain,
|
|
25
|
+
AgentGroup,
|
|
26
|
+
AgentInput,
|
|
27
|
+
AgentResult,
|
|
28
|
+
BaseAgent,
|
|
29
|
+
Severity,
|
|
30
|
+
make_finding,
|
|
31
|
+
register,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@register
|
|
36
|
+
class E2EFlowAgent(BaseAgent):
|
|
37
|
+
"""AI-generated E2E tests for full user flows via Playwright."""
|
|
38
|
+
|
|
39
|
+
group = AgentGroup.TEST
|
|
40
|
+
domain = AgentDomain.TESTING
|
|
41
|
+
name = "E2EFlowAgent"
|
|
42
|
+
description = "AI-generated Playwright E2E tests: login, checkout, forms, navigation"
|
|
43
|
+
timeout = 300
|
|
44
|
+
|
|
45
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
46
|
+
try:
|
|
47
|
+
from playwright.sync_api import (
|
|
48
|
+
sync_playwright, # noqa: F401 — used to check availability
|
|
49
|
+
)
|
|
50
|
+
except ImportError:
|
|
51
|
+
self.skip(result, "playwright not installed")
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
base_url = self._find_base_url(inp)
|
|
55
|
+
if not base_url:
|
|
56
|
+
self.skip(result, "no running server found")
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
# Step 1: Discover pages and routes
|
|
60
|
+
pages = self._discover_pages(inp.root)
|
|
61
|
+
routes = self._extract_routes(inp)
|
|
62
|
+
flows = self._get_contract_flows(inp)
|
|
63
|
+
|
|
64
|
+
# Step 2: Generate test scripts using AI
|
|
65
|
+
test_scripts = self._generate_test_scripts(inp, pages, routes, flows, base_url)
|
|
66
|
+
|
|
67
|
+
if not test_scripts:
|
|
68
|
+
# Fallback: generate basic smoke tests without AI
|
|
69
|
+
test_scripts = self._generate_smoke_tests(pages, routes, base_url)
|
|
70
|
+
|
|
71
|
+
# Step 3: Run generated tests
|
|
72
|
+
test_results = self._run_generated_tests(test_scripts, inp.root)
|
|
73
|
+
|
|
74
|
+
# Step 4: Report results
|
|
75
|
+
total = test_results.get("total", 0)
|
|
76
|
+
passed = test_results.get("passed", 0)
|
|
77
|
+
failed = test_results.get("failed", 0)
|
|
78
|
+
|
|
79
|
+
for detail in test_results.get("details", []):
|
|
80
|
+
severity = Severity.INFO if detail["passed"] else Severity.HIGH
|
|
81
|
+
result.add_finding(
|
|
82
|
+
make_finding(
|
|
83
|
+
agent=self.name,
|
|
84
|
+
finding_type="e2e_pass" if detail["passed"] else "e2e_fail",
|
|
85
|
+
severity=severity,
|
|
86
|
+
file=detail.get("file", "generated_test"),
|
|
87
|
+
message=f"{'PASS' if detail['passed'] else 'FAIL'}: {detail['name']}",
|
|
88
|
+
detail=detail.get("error", "")[:500] if not detail["passed"] else "",
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
result.data["suite"] = {
|
|
93
|
+
"runner": "playwright_e2e",
|
|
94
|
+
"total": total,
|
|
95
|
+
"passed": passed,
|
|
96
|
+
"failed": failed,
|
|
97
|
+
"scripts_generated": len(test_scripts),
|
|
98
|
+
"details": test_results.get("details", [])[:50],
|
|
99
|
+
}
|
|
100
|
+
result.files_scanned = len(pages)
|
|
101
|
+
|
|
102
|
+
def _generate_test_scripts(
|
|
103
|
+
self, inp: AgentInput, pages: list, routes: list, flows: list, base_url: str
|
|
104
|
+
) -> list[dict]:
|
|
105
|
+
"""Use AI to generate Playwright test scripts."""
|
|
106
|
+
from ..ai.client import call_ai
|
|
107
|
+
|
|
108
|
+
config = inp.config
|
|
109
|
+
if not config.get("ai", {}).get("keys") and not config.get("ai", {}).get(
|
|
110
|
+
"local_model_name"
|
|
111
|
+
):
|
|
112
|
+
return []
|
|
113
|
+
|
|
114
|
+
pages_text = "\n".join(f"- {p}" for p in pages[:20])
|
|
115
|
+
routes_text = "\n".join(f"- {r}" for r in routes[:20]) if routes else "No routes detected"
|
|
116
|
+
flows_text = ""
|
|
117
|
+
if flows:
|
|
118
|
+
flows_text = "\nConfirmed critical flows:\n" + "\n".join(
|
|
119
|
+
f"- {f.get('name', '?')}: {f.get('description', '')}" for f in flows
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
system = """You are a Playwright test generator for the Patchi testing framework.
|
|
123
|
+
|
|
124
|
+
Generate Python test scripts using playwright.sync_api that test the web application.
|
|
125
|
+
|
|
126
|
+
RULES:
|
|
127
|
+
- Use `from playwright.sync_api import sync_playwright` (sync API, not async)
|
|
128
|
+
- Each test function must accept `base_url` as a parameter
|
|
129
|
+
- Tests must be self-contained (no external fixtures)
|
|
130
|
+
- Use try/except to handle errors gracefully
|
|
131
|
+
- Print structured results: print("PASS: test_name") or print("FAIL: test_name: error")
|
|
132
|
+
- Test real user interactions: click buttons, fill forms, verify content
|
|
133
|
+
- Test navigation between pages
|
|
134
|
+
- Test error states (invalid inputs, missing required fields)
|
|
135
|
+
- Test responsive behavior at mobile viewport
|
|
136
|
+
|
|
137
|
+
Generate 5-10 test functions that cover the most important user flows.
|
|
138
|
+
Each function should be named `test_<something>` and take `base_url` as argument.
|
|
139
|
+
At the end, include a `def main():` that runs all tests and prints a summary."""
|
|
140
|
+
|
|
141
|
+
user_prompt = f"""Generate Playwright E2E tests for this application.
|
|
142
|
+
|
|
143
|
+
Base URL: {base_url}
|
|
144
|
+
|
|
145
|
+
Available pages:
|
|
146
|
+
{pages_text}
|
|
147
|
+
|
|
148
|
+
Routes:
|
|
149
|
+
{routes_text}
|
|
150
|
+
{flows_text}
|
|
151
|
+
|
|
152
|
+
Generate comprehensive E2E tests covering:
|
|
153
|
+
1. Page load and rendering tests
|
|
154
|
+
2. Button click tests (verify actions happen)
|
|
155
|
+
3. Form submission tests (valid + invalid inputs)
|
|
156
|
+
4. Navigation tests (clicking links moves between pages)
|
|
157
|
+
5. Error state tests (what happens with bad input)
|
|
158
|
+
6. At least one responsive test (mobile viewport)
|
|
159
|
+
|
|
160
|
+
Return ONLY Python code in a single code block."""
|
|
161
|
+
|
|
162
|
+
response = call_ai(config, system, user_prompt, max_tokens=6000)
|
|
163
|
+
if not response:
|
|
164
|
+
return []
|
|
165
|
+
|
|
166
|
+
# Extract Python code blocks
|
|
167
|
+
blocks = re.findall(r"```(?:python)?\n(.*?)```", response, re.DOTALL)
|
|
168
|
+
if not blocks:
|
|
169
|
+
# Try to find function definitions directly
|
|
170
|
+
if "def test_" in response:
|
|
171
|
+
blocks = [response]
|
|
172
|
+
|
|
173
|
+
scripts = []
|
|
174
|
+
for i, block in enumerate(blocks):
|
|
175
|
+
scripts.append(
|
|
176
|
+
{
|
|
177
|
+
"name": f"ai_generated_{i}",
|
|
178
|
+
"code": block,
|
|
179
|
+
"source": "ai",
|
|
180
|
+
}
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
return scripts
|
|
184
|
+
|
|
185
|
+
def _generate_smoke_tests(self, pages: list, routes: list, base_url: str) -> list[dict]:
|
|
186
|
+
"""Generate basic smoke tests without AI as a fallback."""
|
|
187
|
+
test_code = f'''
|
|
188
|
+
from playwright.sync_api import sync_playwright
|
|
189
|
+
import sys
|
|
190
|
+
|
|
191
|
+
def test_page_loads(base_url, page_path):
|
|
192
|
+
"""Test that a page loads without errors."""
|
|
193
|
+
with sync_playwright() as p:
|
|
194
|
+
browser = p.chromium.launch(headless=True)
|
|
195
|
+
page = browser.new_page()
|
|
196
|
+
url = f"{{base_url}}/{{page_path}}" if not page_path.startswith("http") else page_path
|
|
197
|
+
try:
|
|
198
|
+
response = page.goto(url, wait_until="domcontentloaded", timeout=15000)
|
|
199
|
+
if response and response.status < 400:
|
|
200
|
+
print(f"PASS: page_loads_{{page_path.replace('/', '_').replace('.html', '')}}")
|
|
201
|
+
else:
|
|
202
|
+
print(f"FAIL: page_loads_{{page_path.replace('/', '_').replace('.html', '')}}: status {{response.status if response else 'None'}}")
|
|
203
|
+
except Exception as e:
|
|
204
|
+
print(f"FAIL: page_loads_{{page_path.replace('/', '_').replace('.html', '')}}: {{e}}")
|
|
205
|
+
finally:
|
|
206
|
+
browser.close()
|
|
207
|
+
|
|
208
|
+
def test_no_console_errors(base_url, page_path):
|
|
209
|
+
"""Test that no JavaScript errors occur on page load."""
|
|
210
|
+
with sync_playwright() as p:
|
|
211
|
+
browser = p.chromium.launch(headless=True)
|
|
212
|
+
page = browser.new_page()
|
|
213
|
+
errors = []
|
|
214
|
+
page.on("console", lambda msg: errors.append(msg.text) if msg.type == "error" else None)
|
|
215
|
+
url = f"{{base_url}}/{{page_path}}" if not page_path.startswith("http") else page_path
|
|
216
|
+
try:
|
|
217
|
+
page.goto(url, wait_until="domcontentloaded", timeout=15000)
|
|
218
|
+
page.wait_for_timeout(2000)
|
|
219
|
+
if errors:
|
|
220
|
+
print(f"FAIL: console_errors_{{page_path.replace('/', '_').replace('.html', '')}}: {{errors[0][:100]}}")
|
|
221
|
+
else:
|
|
222
|
+
print(f"PASS: console_errors_{{page_path.replace('/', '_').replace('.html', '')}}")
|
|
223
|
+
except Exception as e:
|
|
224
|
+
print(f"PASS: console_errors_{{page_path.replace('/', '_').replace('.html', '')}}: page failed to load")
|
|
225
|
+
finally:
|
|
226
|
+
browser.close()
|
|
227
|
+
|
|
228
|
+
if __name__ == "__main__":
|
|
229
|
+
base = "{base_url}"
|
|
230
|
+
pages = {pages[:10]}
|
|
231
|
+
passed = failed = 0
|
|
232
|
+
for page_path in pages:
|
|
233
|
+
test_page_loads(base, page_path)
|
|
234
|
+
test_no_console_errors(base, page_path)
|
|
235
|
+
'''
|
|
236
|
+
|
|
237
|
+
return [{"name": "smoke_tests", "code": test_code, "source": "generated"}]
|
|
238
|
+
|
|
239
|
+
def _run_generated_tests(self, scripts: list[dict], root: Path) -> dict:
|
|
240
|
+
"""Run generated test scripts as subprocesses."""
|
|
241
|
+
total = 0
|
|
242
|
+
passed = 0
|
|
243
|
+
failed = 0
|
|
244
|
+
details = []
|
|
245
|
+
|
|
246
|
+
for script in scripts:
|
|
247
|
+
# Write script to temp file
|
|
248
|
+
script_path = root / ".patchi" / "tests" / "e2e" / f"{script['name']}.py"
|
|
249
|
+
script_path.parent.mkdir(parents=True, exist_ok=True)
|
|
250
|
+
script_path.write_text(script["code"], encoding="utf-8")
|
|
251
|
+
|
|
252
|
+
try:
|
|
253
|
+
proc = subprocess.run(
|
|
254
|
+
[sys.executable, str(script_path)],
|
|
255
|
+
capture_output=True,
|
|
256
|
+
text=True,
|
|
257
|
+
timeout=120,
|
|
258
|
+
cwd=str(root),
|
|
259
|
+
)
|
|
260
|
+
output = proc.stdout + proc.stderr
|
|
261
|
+
|
|
262
|
+
for line in output.splitlines():
|
|
263
|
+
if line.startswith("PASS:"):
|
|
264
|
+
total += 1
|
|
265
|
+
passed += 1
|
|
266
|
+
details.append(
|
|
267
|
+
{"name": line[6:].strip(), "passed": True, "file": script["name"]}
|
|
268
|
+
)
|
|
269
|
+
elif line.startswith("FAIL:"):
|
|
270
|
+
total += 1
|
|
271
|
+
failed += 1
|
|
272
|
+
parts = line[6:].split(":", 1)
|
|
273
|
+
details.append(
|
|
274
|
+
{
|
|
275
|
+
"name": parts[0].strip(),
|
|
276
|
+
"passed": False,
|
|
277
|
+
"error": parts[1].strip() if len(parts) > 1 else "unknown",
|
|
278
|
+
"file": script["name"],
|
|
279
|
+
}
|
|
280
|
+
)
|
|
281
|
+
except subprocess.TimeoutExpired:
|
|
282
|
+
total += 1
|
|
283
|
+
failed += 1
|
|
284
|
+
details.append(
|
|
285
|
+
{
|
|
286
|
+
"name": script["name"],
|
|
287
|
+
"passed": False,
|
|
288
|
+
"error": "timeout",
|
|
289
|
+
"file": script["name"],
|
|
290
|
+
}
|
|
291
|
+
)
|
|
292
|
+
except Exception as e:
|
|
293
|
+
total += 1
|
|
294
|
+
failed += 1
|
|
295
|
+
details.append(
|
|
296
|
+
{
|
|
297
|
+
"name": script["name"],
|
|
298
|
+
"passed": False,
|
|
299
|
+
"error": str(e)[:200],
|
|
300
|
+
"file": script["name"],
|
|
301
|
+
}
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
return {"total": total, "passed": passed, "failed": failed, "details": details}
|
|
305
|
+
|
|
306
|
+
def _find_base_url(self, inp: AgentInput) -> str | None:
|
|
307
|
+
extra_base = (inp.extra or {}).get("base_url")
|
|
308
|
+
if extra_base:
|
|
309
|
+
return extra_base
|
|
310
|
+
test_config = inp.config.get("test_config", {})
|
|
311
|
+
if test_config.get("base_url"):
|
|
312
|
+
return test_config["base_url"]
|
|
313
|
+
import socket
|
|
314
|
+
|
|
315
|
+
# Check config/brain for configured dev server port first (LIMIT-12)
|
|
316
|
+
config = inp.config or {}
|
|
317
|
+
brain = inp.brain or {}
|
|
318
|
+
config_port = config.get("web_port") or brain.get("web_port")
|
|
319
|
+
if config_port:
|
|
320
|
+
try:
|
|
321
|
+
with socket.create_connection(("127.0.0.1", int(config_port)), timeout=1):
|
|
322
|
+
return f"http://127.0.0.1:{int(config_port)}"
|
|
323
|
+
except (ConnectionRefusedError, OSError, ValueError):
|
|
324
|
+
pass
|
|
325
|
+
for port in [3000, 5173, 8080, 4200, 8000, 4321, 5189]:
|
|
326
|
+
try:
|
|
327
|
+
with socket.create_connection(("127.0.0.1", port), timeout=1):
|
|
328
|
+
return f"http://127.0.0.1:{port}"
|
|
329
|
+
except (ConnectionRefusedError, OSError):
|
|
330
|
+
continue
|
|
331
|
+
return None
|
|
332
|
+
|
|
333
|
+
def _discover_pages(self, root: Path) -> list[str]:
|
|
334
|
+
pages = []
|
|
335
|
+
for p in root.rglob("*.html"):
|
|
336
|
+
if any(part in DEFAULT_IGNORE_DIRS for part in p.parts):
|
|
337
|
+
continue
|
|
338
|
+
pages.append(p.relative_to(root).as_posix())
|
|
339
|
+
return sorted(set(pages))[:20]
|
|
340
|
+
|
|
341
|
+
def _extract_routes(self, inp: AgentInput) -> list[str]:
|
|
342
|
+
brain = inp.brain or {}
|
|
343
|
+
route_map = brain.get("route_map", {})
|
|
344
|
+
routes = []
|
|
345
|
+
for _key, val in route_map.items():
|
|
346
|
+
if isinstance(val, dict) and "path" in val:
|
|
347
|
+
routes.append(val["path"])
|
|
348
|
+
elif isinstance(val, str):
|
|
349
|
+
routes.append(val)
|
|
350
|
+
return routes[:20]
|
|
351
|
+
|
|
352
|
+
def _get_contract_flows(self, inp: AgentInput) -> list[dict]:
|
|
353
|
+
brain = inp.brain or {}
|
|
354
|
+
return brain.get("confirmed_flows", [])
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"""FlakeDetectorAgent — tracks test results across runs to detect flaky tests.
|
|
2
|
+
|
|
3
|
+
Persists per-run test results (name, status, duration) to SQLite.
|
|
4
|
+
After N runs, queries history to find tests that pass/fail inconsistently
|
|
5
|
+
and tests with abnormal duration variance.
|
|
6
|
+
|
|
7
|
+
Language-agnostic: any test framework output that produces test names,
|
|
8
|
+
pass/fail status, and durations works here.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import sqlite3
|
|
14
|
+
import statistics
|
|
15
|
+
import time
|
|
16
|
+
import uuid
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from ..agents.base import (
|
|
20
|
+
AgentDomain,
|
|
21
|
+
AgentGroup,
|
|
22
|
+
AgentInput,
|
|
23
|
+
AgentResult,
|
|
24
|
+
AgentStatus,
|
|
25
|
+
BaseAgent,
|
|
26
|
+
Finding,
|
|
27
|
+
Severity,
|
|
28
|
+
register,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
_FLAKE_DB_NAME = "flake_history.db"
|
|
32
|
+
_MIN_RUNS_FOR_FLAKE = 2
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _get_flake_db(root: Path) -> sqlite3.Connection:
|
|
36
|
+
db_path = root / ".patchi" / _FLAKE_DB_NAME
|
|
37
|
+
db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
38
|
+
conn = sqlite3.connect(str(db_path), check_same_thread=False)
|
|
39
|
+
conn.executescript("""
|
|
40
|
+
CREATE TABLE IF NOT EXISTS test_runs (
|
|
41
|
+
run_id TEXT PRIMARY KEY,
|
|
42
|
+
timestamp TEXT NOT NULL,
|
|
43
|
+
runner TEXT,
|
|
44
|
+
total INT DEFAULT 0,
|
|
45
|
+
passed INT DEFAULT 0,
|
|
46
|
+
failed INT DEFAULT 0,
|
|
47
|
+
skipped INT DEFAULT 0,
|
|
48
|
+
duration_ms INT DEFAULT 0
|
|
49
|
+
);
|
|
50
|
+
CREATE TABLE IF NOT EXISTS test_results (
|
|
51
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
52
|
+
run_id TEXT NOT NULL,
|
|
53
|
+
test_name TEXT NOT NULL,
|
|
54
|
+
passed INT NOT NULL,
|
|
55
|
+
duration_ms INT DEFAULT 0,
|
|
56
|
+
file TEXT DEFAULT '',
|
|
57
|
+
line INT DEFAULT 0,
|
|
58
|
+
FOREIGN KEY (run_id) REFERENCES test_runs(run_id)
|
|
59
|
+
);
|
|
60
|
+
CREATE INDEX IF NOT EXISTS idx_test_results_name ON test_results(test_name);
|
|
61
|
+
""")
|
|
62
|
+
return conn
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _record_test_run(
|
|
66
|
+
root: Path,
|
|
67
|
+
runner: str,
|
|
68
|
+
cases: list[dict],
|
|
69
|
+
total: int,
|
|
70
|
+
passed: int,
|
|
71
|
+
failed: int,
|
|
72
|
+
skipped: int,
|
|
73
|
+
duration_ms: int,
|
|
74
|
+
) -> str:
|
|
75
|
+
run_id = uuid.uuid4().hex[:12]
|
|
76
|
+
now = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
77
|
+
conn = _get_flake_db(root)
|
|
78
|
+
try:
|
|
79
|
+
conn.execute(
|
|
80
|
+
"INSERT INTO test_runs (run_id, timestamp, runner, total, passed, failed, skipped, duration_ms) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
|
81
|
+
(run_id, now, runner, total, passed, failed, skipped, duration_ms),
|
|
82
|
+
)
|
|
83
|
+
for c in cases:
|
|
84
|
+
conn.execute(
|
|
85
|
+
"INSERT INTO test_results (run_id, test_name, passed, duration_ms, file, line) VALUES (?, ?, ?, ?, ?, ?)",
|
|
86
|
+
(
|
|
87
|
+
run_id,
|
|
88
|
+
c.get("name", "?"),
|
|
89
|
+
1 if c.get("passed", True) else 0,
|
|
90
|
+
c.get("duration_ms", 0),
|
|
91
|
+
c.get("file", ""),
|
|
92
|
+
c.get("line", 0),
|
|
93
|
+
),
|
|
94
|
+
)
|
|
95
|
+
conn.commit()
|
|
96
|
+
finally:
|
|
97
|
+
conn.close()
|
|
98
|
+
return run_id
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _get_all_runs(root: Path) -> list[dict]:
|
|
102
|
+
conn = _get_flake_db(root)
|
|
103
|
+
try:
|
|
104
|
+
cur = conn.execute(
|
|
105
|
+
"SELECT run_id, timestamp, runner, total, passed, failed, skipped, duration_ms FROM test_runs ORDER BY rowid ASC"
|
|
106
|
+
)
|
|
107
|
+
return [
|
|
108
|
+
{
|
|
109
|
+
"run_id": r[0],
|
|
110
|
+
"timestamp": r[1],
|
|
111
|
+
"runner": r[2],
|
|
112
|
+
"total": r[3],
|
|
113
|
+
"passed": r[4],
|
|
114
|
+
"failed": r[5],
|
|
115
|
+
"skipped": r[6],
|
|
116
|
+
"duration_ms": r[7],
|
|
117
|
+
}
|
|
118
|
+
for r in cur.fetchall()
|
|
119
|
+
]
|
|
120
|
+
finally:
|
|
121
|
+
conn.close()
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _get_test_history(root: Path, test_name: str) -> list[dict]:
|
|
125
|
+
conn = _get_flake_db(root)
|
|
126
|
+
try:
|
|
127
|
+
cur = conn.execute(
|
|
128
|
+
"SELECT tr.run_id, tr.timestamp, res.passed, res.duration_ms, res.file, res.line "
|
|
129
|
+
"FROM test_results res JOIN test_runs tr ON res.run_id = tr.run_id "
|
|
130
|
+
"WHERE res.test_name = ? ORDER BY tr.rowid ASC",
|
|
131
|
+
(test_name,),
|
|
132
|
+
)
|
|
133
|
+
return [
|
|
134
|
+
{
|
|
135
|
+
"run_id": r[0],
|
|
136
|
+
"timestamp": r[1],
|
|
137
|
+
"passed": bool(r[2]),
|
|
138
|
+
"duration_ms": r[3],
|
|
139
|
+
"file": r[4],
|
|
140
|
+
"line": r[5],
|
|
141
|
+
}
|
|
142
|
+
for r in cur.fetchall()
|
|
143
|
+
]
|
|
144
|
+
finally:
|
|
145
|
+
conn.close()
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _get_all_test_names(root: Path) -> list[str]:
|
|
149
|
+
conn = _get_flake_db(root)
|
|
150
|
+
try:
|
|
151
|
+
cur = conn.execute("SELECT DISTINCT test_name FROM test_results ORDER BY test_name")
|
|
152
|
+
return [r[0] for r in cur.fetchall()]
|
|
153
|
+
finally:
|
|
154
|
+
conn.close()
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _detect_flaky_tests(root: Path, min_runs: int = _MIN_RUNS_FOR_FLAKE) -> list[dict]:
|
|
158
|
+
flakes: list[dict] = []
|
|
159
|
+
for test_name in _get_all_test_names(root):
|
|
160
|
+
history = _get_test_history(root, test_name)
|
|
161
|
+
if len(history) < min_runs:
|
|
162
|
+
continue
|
|
163
|
+
outcomes = [h["passed"] for h in history]
|
|
164
|
+
if len(set(outcomes)) > 1:
|
|
165
|
+
flakes.append(
|
|
166
|
+
{
|
|
167
|
+
"test_name": test_name,
|
|
168
|
+
"history": history,
|
|
169
|
+
"run_count": len(history),
|
|
170
|
+
"pass_count": sum(1 for h in history if h["passed"]),
|
|
171
|
+
"fail_count": sum(1 for h in history if not h["passed"]),
|
|
172
|
+
"latest_file": history[-1]["file"],
|
|
173
|
+
"latest_line": history[-1]["line"],
|
|
174
|
+
}
|
|
175
|
+
)
|
|
176
|
+
return flakes
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _detect_duration_outliers(root: Path, z_threshold: float = 3.0) -> list[dict]:
|
|
180
|
+
outliers: list[dict] = []
|
|
181
|
+
for test_name in _get_all_test_names(root):
|
|
182
|
+
history = _get_test_history(root, test_name)
|
|
183
|
+
durations = [h["duration_ms"] for h in history if h["duration_ms"] > 0]
|
|
184
|
+
if len(durations) < 3:
|
|
185
|
+
continue
|
|
186
|
+
mean = statistics.mean(durations)
|
|
187
|
+
stdev = statistics.stdev(durations) if len(durations) > 1 else 0.0
|
|
188
|
+
if stdev == 0:
|
|
189
|
+
continue
|
|
190
|
+
latest_dur = history[-1]["duration_ms"]
|
|
191
|
+
z_score = (latest_dur - mean) / stdev
|
|
192
|
+
if z_score > z_threshold:
|
|
193
|
+
outliers.append(
|
|
194
|
+
{
|
|
195
|
+
"test_name": test_name,
|
|
196
|
+
"mean_duration_ms": round(mean, 1),
|
|
197
|
+
"stdev_duration_ms": round(stdev, 1),
|
|
198
|
+
"latest_duration_ms": latest_dur,
|
|
199
|
+
"z_score": round(z_score, 2),
|
|
200
|
+
"history": history,
|
|
201
|
+
"latest_file": history[-1]["file"],
|
|
202
|
+
"latest_line": history[-1]["line"],
|
|
203
|
+
}
|
|
204
|
+
)
|
|
205
|
+
return outliers
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@register
|
|
209
|
+
class FlakeDetectorAgent(BaseAgent):
|
|
210
|
+
"""Detects flaky tests and duration regressions across multiple test runs."""
|
|
211
|
+
|
|
212
|
+
group = AgentGroup.TEST
|
|
213
|
+
domain = AgentDomain.TESTING
|
|
214
|
+
name = "FlakeDetectorAgent"
|
|
215
|
+
description = (
|
|
216
|
+
"Track test results across runs to detect flaky tests "
|
|
217
|
+
"(inconsistent pass/fail) and duration regressions. "
|
|
218
|
+
"Language-agnostic: works with pytest, jest, mocha, unittest, go test, etc."
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
min_runs: int = 2
|
|
222
|
+
|
|
223
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
224
|
+
|
|
225
|
+
if not inp.root:
|
|
226
|
+
result.status = AgentStatus.FAILED
|
|
227
|
+
result.error = "No project root provided"
|
|
228
|
+
return
|
|
229
|
+
|
|
230
|
+
run_count = _get_all_runs(inp.root)
|
|
231
|
+
result.data["total_runs"] = len(run_count)
|
|
232
|
+
|
|
233
|
+
flakes = _detect_flaky_tests(inp.root, min_runs=self.min_runs)
|
|
234
|
+
result.data["flaky_tests"] = len(flakes)
|
|
235
|
+
result.data["flaky_details"] = flakes
|
|
236
|
+
|
|
237
|
+
outliers = _detect_duration_outliers(inp.root, z_threshold=3.0)
|
|
238
|
+
result.data["duration_outliers"] = len(outliers)
|
|
239
|
+
result.data["duration_outlier_details"] = outliers
|
|
240
|
+
|
|
241
|
+
for f in flakes:
|
|
242
|
+
result.add_finding(
|
|
243
|
+
Finding(
|
|
244
|
+
agent=self.name,
|
|
245
|
+
type="flaky_test",
|
|
246
|
+
severity=Severity.MEDIUM,
|
|
247
|
+
file=f.get("latest_file", ""),
|
|
248
|
+
line=f.get("latest_line", 0),
|
|
249
|
+
message=f"Flaky test: {f['test_name']}",
|
|
250
|
+
detail=(
|
|
251
|
+
f"Passed {f['pass_count']}/{f['run_count']} runs, "
|
|
252
|
+
f"failed {f['fail_count']}/{f['run_count']} runs."
|
|
253
|
+
),
|
|
254
|
+
)
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
for o in outliers:
|
|
258
|
+
result.add_finding(
|
|
259
|
+
Finding(
|
|
260
|
+
agent=self.name,
|
|
261
|
+
type="duration_regression",
|
|
262
|
+
severity=Severity.LOW,
|
|
263
|
+
file=o.get("latest_file", ""),
|
|
264
|
+
line=o.get("latest_line", 0),
|
|
265
|
+
message=f"Duration regression: {o['test_name']}",
|
|
266
|
+
detail=(
|
|
267
|
+
f"Latest run: {o['latest_duration_ms']}ms "
|
|
268
|
+
f"(mean {o['mean_duration_ms']}ms, "
|
|
269
|
+
f"z-score {o['z_score']})"
|
|
270
|
+
),
|
|
271
|
+
)
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
result.files_scanned = result.data["total_runs"]
|
|
275
|
+
return
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def record_test_run(
|
|
279
|
+
root: Path,
|
|
280
|
+
runner: str,
|
|
281
|
+
cases: list[dict],
|
|
282
|
+
total: int,
|
|
283
|
+
passed: int,
|
|
284
|
+
failed: int,
|
|
285
|
+
skipped: int,
|
|
286
|
+
duration_ms: int,
|
|
287
|
+
) -> str:
|
|
288
|
+
"""Public entry point for UnitTestAgent (or any caller) to record results.
|
|
289
|
+
|
|
290
|
+
Returns the run_id.
|
|
291
|
+
"""
|
|
292
|
+
return _record_test_run(root, runner, cases, total, passed, failed, skipped, duration_ms)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Gate Rule — injected into every Testing/Live/Attack agent.
|
|
3
|
+
|
|
4
|
+
Before doing any work, MUST confirm:
|
|
5
|
+
1. P-Check has run
|
|
6
|
+
2. It returned READY_TO_SERVE with zero unresolved errors
|
|
7
|
+
If false: do not run, request P-Check, stay idle.
|
|
8
|
+
Once READY_TO_SERVE confirmed: retrieve URL and begin work.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def require_ready(root: Path) -> tuple[bool, str | None, dict | None]:
|
|
17
|
+
"""Check gate. Returns (ready, url, status_dict)."""
|
|
18
|
+
from patchi.cli.commands.check_cmd import is_ready
|
|
19
|
+
|
|
20
|
+
ready, st = is_ready(root)
|
|
21
|
+
if ready and st:
|
|
22
|
+
return True, st.get("url"), st
|
|
23
|
+
return False, None, st
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def gate_message(status: dict | None) -> str:
|
|
27
|
+
if not status:
|
|
28
|
+
return "P-Check has not run — request `p check` and wait for READY_TO_SERVE. Do not run/test/attack anything."
|
|
29
|
+
if status.get("status") != "READY_TO_SERVE":
|
|
30
|
+
return f"P-Check is {status.get('status')} ({status.get('error','')}) — request `p check` re-run and let it escalate to Brain. Stay idle."
|
|
31
|
+
return "READY_TO_SERVE confirmed"
|