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,303 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Video Recorder — Records test sessions for debugging and documentation.
|
|
3
|
+
|
|
4
|
+
Features:
|
|
5
|
+
- Full session video recording
|
|
6
|
+
- Automatic segmentation by test
|
|
7
|
+
- Metadata overlay (timestamps, test names)
|
|
8
|
+
- Compression and format options
|
|
9
|
+
- Integration with browser pool
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
import time
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from datetime import UTC, datetime
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
_log = logging.getLogger("patchi.testing.video_recorder")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class RecordingConfig:
|
|
26
|
+
"""Configuration for video recording."""
|
|
27
|
+
|
|
28
|
+
output_dir: Path = Path(".patchi/recordings")
|
|
29
|
+
video_size: dict = field(default_factory=lambda: {"width": 1280, "height": 720})
|
|
30
|
+
fps: int = 30
|
|
31
|
+
codec: str = "vp8" # vp8, h264
|
|
32
|
+
quality: int = 80 # 0-100
|
|
33
|
+
segment_duration: int = 300 # seconds per segment
|
|
34
|
+
include_audio: bool = False
|
|
35
|
+
metadata_overlay: bool = True
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class RecordingSegment:
|
|
40
|
+
"""A single recording segment."""
|
|
41
|
+
|
|
42
|
+
segment_id: str
|
|
43
|
+
test_name: str
|
|
44
|
+
start_time: float
|
|
45
|
+
end_time: float
|
|
46
|
+
duration_seconds: float
|
|
47
|
+
file_path: Path
|
|
48
|
+
file_size: int
|
|
49
|
+
metadata: dict = field(default_factory=dict)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class TestRecording:
|
|
54
|
+
"""Complete recording for a test run."""
|
|
55
|
+
|
|
56
|
+
test_run_id: str
|
|
57
|
+
started_at: str
|
|
58
|
+
completed_at: str
|
|
59
|
+
total_duration_seconds: float
|
|
60
|
+
segments: list[RecordingSegment] = field(default_factory=list)
|
|
61
|
+
total_size_bytes: int = 0
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class VideoRecorder:
|
|
65
|
+
"""
|
|
66
|
+
Records browser test sessions as video.
|
|
67
|
+
|
|
68
|
+
Usage:
|
|
69
|
+
recorder = VideoRecorder()
|
|
70
|
+
|
|
71
|
+
# Get a browser context with video recording
|
|
72
|
+
context, instance = await recorder.start_recording("test-login")
|
|
73
|
+
|
|
74
|
+
# ... run test ...
|
|
75
|
+
|
|
76
|
+
# Stop and get recording
|
|
77
|
+
recording = await recorder.stop_recording(context, instance)
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def __init__(self, config: RecordingConfig = None):
|
|
81
|
+
self.config = config or RecordingConfig()
|
|
82
|
+
self.config.output_dir.mkdir(parents=True, exist_ok=True)
|
|
83
|
+
self._active_recordings: dict[str, dict] = {}
|
|
84
|
+
self._playwright = None
|
|
85
|
+
|
|
86
|
+
async def start_recording(
|
|
87
|
+
self,
|
|
88
|
+
test_name: str,
|
|
89
|
+
browser_type: str = "chromium",
|
|
90
|
+
headless: bool = True,
|
|
91
|
+
) -> tuple[Any, Any]:
|
|
92
|
+
"""
|
|
93
|
+
Start a new recording session.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Tuple of (browser_context, browser_instance)
|
|
97
|
+
"""
|
|
98
|
+
from playwright.async_api import async_playwright
|
|
99
|
+
|
|
100
|
+
if self._playwright is None:
|
|
101
|
+
self._playwright = await async_playwright().start()
|
|
102
|
+
|
|
103
|
+
browser_type_obj = getattr(self._playwright, browser_type)
|
|
104
|
+
browser = await browser_type_obj.launch(headless=headless)
|
|
105
|
+
|
|
106
|
+
# Create context with video recording
|
|
107
|
+
context = await browser.new_context(
|
|
108
|
+
viewport=self.config.video_size,
|
|
109
|
+
record_video_dir=str(self.config.output_dir / "raw"),
|
|
110
|
+
record_video_size=self.config.video_size,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
recording_id = f"{test_name}-{int(time.time())}"
|
|
114
|
+
|
|
115
|
+
self._active_recordings[recording_id] = {
|
|
116
|
+
"test_name": test_name,
|
|
117
|
+
"browser": browser,
|
|
118
|
+
"context": context,
|
|
119
|
+
"start_time": time.time(),
|
|
120
|
+
"segments": [],
|
|
121
|
+
"segment_start": time.time(),
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
_log.info(f"Started recording: {recording_id}")
|
|
125
|
+
return context, browser
|
|
126
|
+
|
|
127
|
+
async def stop_recording(self, recording_id: str) -> TestRecording:
|
|
128
|
+
"""Stop recording and finalize video."""
|
|
129
|
+
if recording_id not in self._active_recordings:
|
|
130
|
+
raise ValueError(f"Recording not found: {recording_id}")
|
|
131
|
+
|
|
132
|
+
recording = self._active_recordings.pop(recording_id)
|
|
133
|
+
context = recording["context"]
|
|
134
|
+
browser = recording["browser"]
|
|
135
|
+
|
|
136
|
+
# Close context to finalize video
|
|
137
|
+
await context.close()
|
|
138
|
+
await browser.close()
|
|
139
|
+
|
|
140
|
+
# Find the video file
|
|
141
|
+
video_dir = self.config.output_dir / "raw"
|
|
142
|
+
video_files = list(video_dir.glob("*.webm"))
|
|
143
|
+
|
|
144
|
+
if not video_files:
|
|
145
|
+
_log.warning(f"No video file found for {recording_id}")
|
|
146
|
+
return TestRecording(
|
|
147
|
+
test_run_id=recording_id,
|
|
148
|
+
started_at=datetime.fromtimestamp(
|
|
149
|
+
recording["start_time"], UTC
|
|
150
|
+
).isoformat(),
|
|
151
|
+
completed_at=datetime.now(UTC).isoformat(),
|
|
152
|
+
total_duration_seconds=time.time() - recording["start_time"],
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Get the most recent video file
|
|
156
|
+
video_file = max(video_files, key=lambda f: f.stat().st_mtime)
|
|
157
|
+
|
|
158
|
+
# Move to final location
|
|
159
|
+
final_path = self.config.output_dir / f"{recording_id}.webm"
|
|
160
|
+
video_file.rename(final_path)
|
|
161
|
+
|
|
162
|
+
duration = time.time() - recording["start_time"]
|
|
163
|
+
|
|
164
|
+
segment = RecordingSegment(
|
|
165
|
+
segment_id="1",
|
|
166
|
+
test_name=recording["test_name"],
|
|
167
|
+
start_time=recording["start_time"],
|
|
168
|
+
end_time=time.time(),
|
|
169
|
+
duration_seconds=duration,
|
|
170
|
+
file_path=final_path,
|
|
171
|
+
file_size=final_path.stat().st_size,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
test_recording = TestRecording(
|
|
175
|
+
test_run_id=recording_id,
|
|
176
|
+
started_at=datetime.fromtimestamp(recording["start_time"], UTC).isoformat(),
|
|
177
|
+
completed_at=datetime.now(UTC).isoformat(),
|
|
178
|
+
total_duration_seconds=duration,
|
|
179
|
+
segments=[segment],
|
|
180
|
+
total_size_bytes=segment.file_size,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
_log.info(
|
|
184
|
+
f"Recording saved: {final_path} ({duration:.1f}s, {segment.file_size / 1024:.1f} KB)"
|
|
185
|
+
)
|
|
186
|
+
return test_recording
|
|
187
|
+
|
|
188
|
+
async def add_metadata_marker(self, recording_id: str, marker: str, data: dict = None):
|
|
189
|
+
"""Add a metadata marker to the current recording."""
|
|
190
|
+
if recording_id in self._active_recordings:
|
|
191
|
+
recording = self._active_recordings[recording_id]
|
|
192
|
+
recording["segments"].append(
|
|
193
|
+
{
|
|
194
|
+
"marker": marker,
|
|
195
|
+
"data": data or {},
|
|
196
|
+
"timestamp": time.time(),
|
|
197
|
+
}
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
async def get_recording_info(self, recording_id: str) -> dict | None:
|
|
201
|
+
"""Get info about an active recording."""
|
|
202
|
+
return self._active_recordings.get(recording_id)
|
|
203
|
+
|
|
204
|
+
async def list_recordings(self) -> list[dict]:
|
|
205
|
+
"""List all saved recordings."""
|
|
206
|
+
recordings = []
|
|
207
|
+
for video_file in self.config.output_dir.glob("*.webm"):
|
|
208
|
+
stat = video_file.stat()
|
|
209
|
+
recordings.append(
|
|
210
|
+
{
|
|
211
|
+
"name": video_file.stem,
|
|
212
|
+
"path": str(video_file),
|
|
213
|
+
"size_bytes": stat.st_size,
|
|
214
|
+
"created_at": datetime.fromtimestamp(stat.st_ctime, UTC).isoformat(),
|
|
215
|
+
"duration_seconds": stat.st_size / (1024 * 100), # Rough estimate
|
|
216
|
+
}
|
|
217
|
+
)
|
|
218
|
+
return sorted(recordings, key=lambda r: r["created_at"], reverse=True)
|
|
219
|
+
|
|
220
|
+
async def cleanup_old(self, max_age_days: int = 7):
|
|
221
|
+
"""Remove old recordings."""
|
|
222
|
+
cutoff = time.time() - (max_age_days * 24 * 3600)
|
|
223
|
+
|
|
224
|
+
for video_file in self.config.output_dir.glob("*.webm"):
|
|
225
|
+
if video_file.stat().st_mtime < cutoff:
|
|
226
|
+
video_file.unlink()
|
|
227
|
+
_log.info(f"Deleted old recording: {video_file}")
|
|
228
|
+
|
|
229
|
+
# Also clean raw directory
|
|
230
|
+
raw_dir = self.config.output_dir / "raw"
|
|
231
|
+
if raw_dir.exists():
|
|
232
|
+
for video_file in raw_dir.glob("*.webm"):
|
|
233
|
+
if video_file.stat().st_mtime < cutoff:
|
|
234
|
+
video_file.unlink()
|
|
235
|
+
|
|
236
|
+
async def shutdown(self):
|
|
237
|
+
"""Shutdown recorder and cleanup."""
|
|
238
|
+
# Stop all active recordings
|
|
239
|
+
for recording_id in list(self._active_recordings.keys()):
|
|
240
|
+
try:
|
|
241
|
+
await self.stop_recording(recording_id)
|
|
242
|
+
except Exception as e:
|
|
243
|
+
_log.warning(f"Error stopping recording {recording_id}: {e}")
|
|
244
|
+
|
|
245
|
+
if self._playwright:
|
|
246
|
+
await self._playwright.stop()
|
|
247
|
+
self._playwright = None
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
# Integration with browser pool
|
|
251
|
+
class RecordingBrowserPool:
|
|
252
|
+
"""Browser pool with integrated video recording."""
|
|
253
|
+
|
|
254
|
+
def __init__(self, pool, recorder: VideoRecorder):
|
|
255
|
+
self.pool = pool
|
|
256
|
+
self.recorder = recorder
|
|
257
|
+
|
|
258
|
+
async def get_page_with_recording(self, test_name: str):
|
|
259
|
+
"""Get a page with video recording enabled."""
|
|
260
|
+
# This would create a new context with recording
|
|
261
|
+
# For now, use the pool's recording method
|
|
262
|
+
from patchi.core.testing.live_v2.browser_pool import get_browser_pool
|
|
263
|
+
|
|
264
|
+
pool = await get_browser_pool()
|
|
265
|
+
context, instance = await pool.get_browser_for_recording()
|
|
266
|
+
page = await context.new_page()
|
|
267
|
+
|
|
268
|
+
return page, context, instance
|
|
269
|
+
|
|
270
|
+
async def release_recording(self, context, instance, test_name: str):
|
|
271
|
+
"""Release recording context and save video."""
|
|
272
|
+
test_recording = await self.recorder.stop_recording(f"{test_name}-{id(instance)}")
|
|
273
|
+
|
|
274
|
+
# Clean up browser instance
|
|
275
|
+
try:
|
|
276
|
+
await instance._browser.close()
|
|
277
|
+
except Exception as _exc:
|
|
278
|
+
_log.warning('release_recording failed: %s', _exc)
|
|
279
|
+
|
|
280
|
+
return test_recording
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
# Convenience function
|
|
284
|
+
async def record_test_session(
|
|
285
|
+
test_name: str,
|
|
286
|
+
test_func: callable,
|
|
287
|
+
base_url: str = None,
|
|
288
|
+
headless: bool = True,
|
|
289
|
+
) -> TestRecording:
|
|
290
|
+
"""Record a complete test session."""
|
|
291
|
+
recorder = VideoRecorder()
|
|
292
|
+
context, browser = await recorder.start_recording(test_name, headless=headless)
|
|
293
|
+
page = await context.new_page()
|
|
294
|
+
|
|
295
|
+
try:
|
|
296
|
+
if base_url:
|
|
297
|
+
await page.goto(base_url)
|
|
298
|
+
await test_func(page)
|
|
299
|
+
finally:
|
|
300
|
+
recording = await recorder.stop_recording(f"{test_name}-{int(time.time())}")
|
|
301
|
+
await recorder.shutdown()
|
|
302
|
+
|
|
303
|
+
return recording
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"""
|
|
2
|
+
RegressionAgent — snapshot tests, golden tests.
|
|
3
|
+
|
|
4
|
+
Compares current behavior to known-good snapshots:
|
|
5
|
+
- Visual snapshots of UI components
|
|
6
|
+
- API response snapshots
|
|
7
|
+
- Database state snapshots
|
|
8
|
+
- File system structure snapshots
|
|
9
|
+
- Command output snapshots
|
|
10
|
+
|
|
11
|
+
Creates new snapshots when none exist.
|
|
12
|
+
Reports diffs when snapshots don't match.
|
|
13
|
+
|
|
14
|
+
Does NOT write to disk (only reads snapshots).
|
|
15
|
+
May call AI to interpret significant changes.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import logging
|
|
21
|
+
import time
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
from ..agents.base import (
|
|
25
|
+
AgentDomain,
|
|
26
|
+
AgentGroup,
|
|
27
|
+
AgentInput,
|
|
28
|
+
AgentResult,
|
|
29
|
+
BaseAgent,
|
|
30
|
+
Severity,
|
|
31
|
+
make_finding,
|
|
32
|
+
register,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
_log = logging.getLogger("patchi.testing.regression_agent")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@register
|
|
39
|
+
class RegressionAgent(BaseAgent):
|
|
40
|
+
"""Agent for running regression tests with snapshots."""
|
|
41
|
+
|
|
42
|
+
group = AgentGroup.TEST
|
|
43
|
+
domain = AgentDomain.TESTING
|
|
44
|
+
name = "RegressionAgent"
|
|
45
|
+
description = "Snapshot tests: visual, API, DB, FS, command output"
|
|
46
|
+
|
|
47
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
48
|
+
"""Compare the current unit-test result to the saved baseline."""
|
|
49
|
+
from patchi.core import memory as mem
|
|
50
|
+
from patchi.core.testing.unit_test_agent import UnitTestAgent
|
|
51
|
+
|
|
52
|
+
unit_result = UnitTestAgent().run(inp)
|
|
53
|
+
suite = unit_result.data.get("suite", {})
|
|
54
|
+
result.data["suite"] = suite
|
|
55
|
+
|
|
56
|
+
baseline = inp.brain.get("test_baseline") or mem.get_brain(inp.root).get("test_baseline")
|
|
57
|
+
if not baseline:
|
|
58
|
+
brain = mem.get_brain(inp.root)
|
|
59
|
+
brain["test_baseline"] = {
|
|
60
|
+
"passed": suite.get("passed", 0),
|
|
61
|
+
"failed": suite.get("failed", 0),
|
|
62
|
+
"total": suite.get("total", 0),
|
|
63
|
+
}
|
|
64
|
+
mem.save_brain(brain, inp.root)
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
if suite.get("failed", 0) > baseline.get("failed", 0):
|
|
68
|
+
result.add_finding(
|
|
69
|
+
make_finding(
|
|
70
|
+
agent=self.name,
|
|
71
|
+
finding_type="regression_detected",
|
|
72
|
+
severity=Severity.HIGH,
|
|
73
|
+
file="tests",
|
|
74
|
+
message="Test regression detected: current run has more failures than baseline.",
|
|
75
|
+
detail=f"Baseline failed={baseline.get('failed', 0)}, current failed={suite.get('failed', 0)}",
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
elif suite.get("failed", 0) < baseline.get("failed", 0) or suite.get(
|
|
79
|
+
"passed", 0
|
|
80
|
+
) > baseline.get("passed", 0):
|
|
81
|
+
brain = mem.get_brain(inp.root)
|
|
82
|
+
brain["test_baseline"] = {
|
|
83
|
+
"passed": suite.get("passed", 0),
|
|
84
|
+
"failed": suite.get("failed", 0),
|
|
85
|
+
"total": suite.get("total", 0),
|
|
86
|
+
}
|
|
87
|
+
mem.save_brain(brain, inp.root)
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
def _find_snapshot_tests(self, root: Path) -> list[dict]:
|
|
91
|
+
"""Find existing snapshot test files and configurations."""
|
|
92
|
+
snapshot_tests = []
|
|
93
|
+
|
|
94
|
+
# Look for common snapshot file patterns
|
|
95
|
+
snapshot_patterns = [
|
|
96
|
+
"**/*.snap", # Jest snapshots
|
|
97
|
+
"**/__snapshots__/**", # Jest-style snapshot directories
|
|
98
|
+
"**/*.snapshot", # General snapshot files
|
|
99
|
+
"**/snapshots/**", # Snapshot directories
|
|
100
|
+
"**/*.golden", # Golden test files
|
|
101
|
+
"**/goldens/**", # Golden test directories
|
|
102
|
+
"**/tests/**/*.py", # Python test files that might contain snapshots
|
|
103
|
+
"**/tests/**/*.js", # JS test files that might contain snapshots
|
|
104
|
+
"**/tests/**/*.ts", # TS test files that might contain snapshots
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
for pattern in snapshot_patterns:
|
|
108
|
+
for snap_file in root.rglob(pattern):
|
|
109
|
+
if snap_file.is_file():
|
|
110
|
+
snapshot_tests.append(
|
|
111
|
+
{"file_path": snap_file, "type": self._infer_snapshot_type(snap_file)}
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
return snapshot_tests
|
|
115
|
+
|
|
116
|
+
def _infer_snapshot_type(self, file_path: Path) -> str:
|
|
117
|
+
"""Infer the type of snapshot from the file path."""
|
|
118
|
+
if ".snap" in file_path.suffix or "__snapshots__" in file_path.parts:
|
|
119
|
+
return "jest_snapshot"
|
|
120
|
+
elif ".golden" in file_path.suffix or "goldens" in file_path.parts:
|
|
121
|
+
return "golden_test"
|
|
122
|
+
elif file_path.suffix in [".py", ".js", ".ts"] and "test" in file_path.name.lower():
|
|
123
|
+
return "code_based_snapshot"
|
|
124
|
+
else:
|
|
125
|
+
return "general_snapshot"
|
|
126
|
+
|
|
127
|
+
def _can_generate_snapshots(self, inp: AgentInput) -> bool:
|
|
128
|
+
"""Check if we can generate snapshot tests based on project structure."""
|
|
129
|
+
# Look for testable components
|
|
130
|
+
testable_components = [
|
|
131
|
+
(inp.root / "src").exists(), # Source code exists
|
|
132
|
+
(inp.root / "tests").exists(), # Test directory exists
|
|
133
|
+
(inp.root / "public").exists(), # Frontend assets exist
|
|
134
|
+
(inp.root / "assets").exists(), # Assets exist
|
|
135
|
+
any(inp.root.rglob("*.js")), # JS files exist
|
|
136
|
+
any(inp.root.rglob("*.py")), # Python files exist
|
|
137
|
+
any(inp.root.rglob("*.jsx")), # JSX files exist
|
|
138
|
+
any(inp.root.rglob("*.tsx")), # TSX files exist
|
|
139
|
+
]
|
|
140
|
+
|
|
141
|
+
return any(testable_components)
|
|
142
|
+
|
|
143
|
+
def _run_snapshot_tests(self, snapshot_tests: list[dict], inp: AgentInput) -> dict:
|
|
144
|
+
"""Run the snapshot tests and compare with stored snapshots."""
|
|
145
|
+
total = len(snapshot_tests)
|
|
146
|
+
passed = 0
|
|
147
|
+
failed = 0
|
|
148
|
+
new = 0
|
|
149
|
+
updated = 0
|
|
150
|
+
test_details = []
|
|
151
|
+
|
|
152
|
+
for test in snapshot_tests:
|
|
153
|
+
file_path = test["file_path"]
|
|
154
|
+
|
|
155
|
+
# For each snapshot test, compare current state with stored snapshot
|
|
156
|
+
try:
|
|
157
|
+
result = self._compare_snapshot(file_path, inp)
|
|
158
|
+
|
|
159
|
+
if result["status"] == "PASSED":
|
|
160
|
+
passed += 1
|
|
161
|
+
elif result["status"] == "FAILED":
|
|
162
|
+
failed += 1
|
|
163
|
+
elif result["status"] == "NEW":
|
|
164
|
+
new += 1
|
|
165
|
+
elif result["status"] == "UPDATED":
|
|
166
|
+
updated += 1
|
|
167
|
+
|
|
168
|
+
test_details.append(
|
|
169
|
+
{
|
|
170
|
+
"name": file_path.name,
|
|
171
|
+
"file": str(file_path.relative_to(inp.root)),
|
|
172
|
+
"status": result["status"],
|
|
173
|
+
"expected": result.get("expected", ""),
|
|
174
|
+
"actual": result.get("actual", ""),
|
|
175
|
+
"snapshot_path": result.get("snapshot_path", ""),
|
|
176
|
+
}
|
|
177
|
+
)
|
|
178
|
+
except Exception as e:
|
|
179
|
+
failed += 1
|
|
180
|
+
test_details.append(
|
|
181
|
+
{
|
|
182
|
+
"name": file_path.name,
|
|
183
|
+
"file": str(file_path.relative_to(inp.root)),
|
|
184
|
+
"status": "ERROR",
|
|
185
|
+
"error": str(e),
|
|
186
|
+
}
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
"success": failed == 0, # Consider success if no failures (but allow new/updated)
|
|
191
|
+
"total": total,
|
|
192
|
+
"passed": passed,
|
|
193
|
+
"failed": failed,
|
|
194
|
+
"new": new,
|
|
195
|
+
"updated": updated,
|
|
196
|
+
"test_details": test_details,
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
def _compare_snapshot(self, file_path: Path, inp: AgentInput) -> dict:
|
|
200
|
+
"""Compare current state with stored snapshot."""
|
|
201
|
+
# Determine the type of comparison based on file extension/type
|
|
202
|
+
if file_path.suffix == ".snap" or "__snapshots__" in file_path.parts:
|
|
203
|
+
# Jest-style snapshot
|
|
204
|
+
return self._compare_jest_snapshot(file_path, inp)
|
|
205
|
+
elif file_path.suffix == ".golden" or "goldens" in file_path.parts:
|
|
206
|
+
# Golden test file
|
|
207
|
+
return self._compare_golden_snapshot(file_path, inp)
|
|
208
|
+
elif file_path.suffix in [".py", ".js", ".ts"] and "test" in file_path.name.lower():
|
|
209
|
+
# Code-based snapshot test
|
|
210
|
+
return self._compare_code_snapshot(file_path, inp)
|
|
211
|
+
else:
|
|
212
|
+
# Generic snapshot comparison
|
|
213
|
+
return self._compare_generic_snapshot(file_path, inp)
|
|
214
|
+
|
|
215
|
+
def _compare_jest_snapshot(self, file_path: Path, inp: AgentInput) -> dict:
|
|
216
|
+
"""Compare Jest-style snapshot."""
|
|
217
|
+
# In a real implementation, we would run the corresponding test
|
|
218
|
+
# For now, we'll just return a placeholder result
|
|
219
|
+
if file_path.exists():
|
|
220
|
+
return {
|
|
221
|
+
"status": "PASSED",
|
|
222
|
+
"expected": "stored_snapshot_content",
|
|
223
|
+
"actual": "current_component_output",
|
|
224
|
+
"snapshot_path": str(file_path),
|
|
225
|
+
}
|
|
226
|
+
else:
|
|
227
|
+
# New snapshot needed
|
|
228
|
+
return {
|
|
229
|
+
"status": "NEW",
|
|
230
|
+
"expected": "",
|
|
231
|
+
"actual": "current_component_output",
|
|
232
|
+
"snapshot_path": str(file_path),
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
def _compare_golden_snapshot(self, file_path: Path, inp: AgentInput) -> dict:
|
|
236
|
+
"""Compare golden test file."""
|
|
237
|
+
# Read the golden file and compare with current output
|
|
238
|
+
try:
|
|
239
|
+
if file_path.exists():
|
|
240
|
+
golden_content = file_path.read_text()
|
|
241
|
+
|
|
242
|
+
# Generate current output (placeholder)
|
|
243
|
+
current_output = self._generate_current_output(file_path, inp)
|
|
244
|
+
|
|
245
|
+
if golden_content.strip() == current_output.strip():
|
|
246
|
+
return {
|
|
247
|
+
"status": "PASSED",
|
|
248
|
+
"expected": golden_content,
|
|
249
|
+
"actual": current_output,
|
|
250
|
+
"snapshot_path": str(file_path),
|
|
251
|
+
}
|
|
252
|
+
else:
|
|
253
|
+
return {
|
|
254
|
+
"status": "FAILED",
|
|
255
|
+
"expected": golden_content,
|
|
256
|
+
"actual": current_output,
|
|
257
|
+
"snapshot_path": str(file_path),
|
|
258
|
+
}
|
|
259
|
+
else:
|
|
260
|
+
# Create new golden file
|
|
261
|
+
current_output = self._generate_current_output(file_path, inp)
|
|
262
|
+
|
|
263
|
+
# Write the golden file (in a real implementation, this would be conditional)
|
|
264
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
265
|
+
file_path.write_text(current_output)
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
"status": "NEW",
|
|
269
|
+
"expected": "",
|
|
270
|
+
"actual": current_output,
|
|
271
|
+
"snapshot_path": str(file_path),
|
|
272
|
+
}
|
|
273
|
+
except Exception as e:
|
|
274
|
+
_log.warning("RegressionAgent._compare_golden_snapshot failed: %s", e)
|
|
275
|
+
return {"status": "ERROR", "error": "Could not read golden file"}
|
|
276
|
+
|
|
277
|
+
def _compare_code_snapshot(self, file_path: Path, inp: AgentInput) -> dict:
|
|
278
|
+
"""Compare code-based snapshot test."""
|
|
279
|
+
# For code-based snapshots, we'd typically run the test file
|
|
280
|
+
# Here we'll just return a placeholder
|
|
281
|
+
return {
|
|
282
|
+
"status": "PASSED",
|
|
283
|
+
"expected": "expected_from_code",
|
|
284
|
+
"actual": "actual_from_code",
|
|
285
|
+
"snapshot_path": str(file_path),
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
def _compare_generic_snapshot(self, file_path: Path, inp: AgentInput) -> dict:
|
|
289
|
+
"""Compare generic snapshot."""
|
|
290
|
+
# Generic comparison - depends on file type
|
|
291
|
+
return {
|
|
292
|
+
"status": "PASSED",
|
|
293
|
+
"expected": "expected_content",
|
|
294
|
+
"actual": "actual_content",
|
|
295
|
+
"snapshot_path": str(file_path),
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
def _generate_current_output(self, file_path: Path, inp: AgentInput) -> str:
|
|
299
|
+
"""Generate current output for comparison."""
|
|
300
|
+
# This would depend on the type of snapshot
|
|
301
|
+
# For now, return a placeholder
|
|
302
|
+
return f"Current output for {file_path.name} at {time.time()}"
|