attune-ai 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- attune/__init__.py +358 -0
- attune/adaptive/__init__.py +13 -0
- attune/adaptive/task_complexity.py +127 -0
- attune/agent_monitoring.py +414 -0
- attune/cache/__init__.py +117 -0
- attune/cache/base.py +166 -0
- attune/cache/dependency_manager.py +256 -0
- attune/cache/hash_only.py +251 -0
- attune/cache/hybrid.py +457 -0
- attune/cache/storage.py +285 -0
- attune/cache_monitor.py +356 -0
- attune/cache_stats.py +298 -0
- attune/cli/__init__.py +152 -0
- attune/cli/__main__.py +12 -0
- attune/cli/commands/__init__.py +1 -0
- attune/cli/commands/batch.py +264 -0
- attune/cli/commands/cache.py +248 -0
- attune/cli/commands/help.py +331 -0
- attune/cli/commands/info.py +140 -0
- attune/cli/commands/inspect.py +436 -0
- attune/cli/commands/inspection.py +57 -0
- attune/cli/commands/memory.py +48 -0
- attune/cli/commands/metrics.py +92 -0
- attune/cli/commands/orchestrate.py +184 -0
- attune/cli/commands/patterns.py +207 -0
- attune/cli/commands/profiling.py +202 -0
- attune/cli/commands/provider.py +98 -0
- attune/cli/commands/routing.py +285 -0
- attune/cli/commands/setup.py +96 -0
- attune/cli/commands/status.py +235 -0
- attune/cli/commands/sync.py +166 -0
- attune/cli/commands/tier.py +121 -0
- attune/cli/commands/utilities.py +114 -0
- attune/cli/commands/workflow.py +579 -0
- attune/cli/core.py +32 -0
- attune/cli/parsers/__init__.py +68 -0
- attune/cli/parsers/batch.py +118 -0
- attune/cli/parsers/cache.py +65 -0
- attune/cli/parsers/help.py +41 -0
- attune/cli/parsers/info.py +26 -0
- attune/cli/parsers/inspect.py +66 -0
- attune/cli/parsers/metrics.py +42 -0
- attune/cli/parsers/orchestrate.py +61 -0
- attune/cli/parsers/patterns.py +54 -0
- attune/cli/parsers/provider.py +40 -0
- attune/cli/parsers/routing.py +110 -0
- attune/cli/parsers/setup.py +42 -0
- attune/cli/parsers/status.py +47 -0
- attune/cli/parsers/sync.py +31 -0
- attune/cli/parsers/tier.py +33 -0
- attune/cli/parsers/workflow.py +77 -0
- attune/cli/utils/__init__.py +1 -0
- attune/cli/utils/data.py +242 -0
- attune/cli/utils/helpers.py +68 -0
- attune/cli_legacy.py +3957 -0
- attune/cli_minimal.py +1159 -0
- attune/cli_router.py +437 -0
- attune/cli_unified.py +814 -0
- attune/config/__init__.py +66 -0
- attune/config/xml_config.py +286 -0
- attune/config.py +545 -0
- attune/coordination.py +870 -0
- attune/core.py +1511 -0
- attune/core_modules/__init__.py +15 -0
- attune/cost_tracker.py +626 -0
- attune/dashboard/__init__.py +41 -0
- attune/dashboard/app.py +512 -0
- attune/dashboard/simple_server.py +435 -0
- attune/dashboard/standalone_server.py +547 -0
- attune/discovery.py +306 -0
- attune/emergence.py +306 -0
- attune/exceptions.py +123 -0
- attune/feedback_loops.py +373 -0
- attune/hot_reload/README.md +473 -0
- attune/hot_reload/__init__.py +62 -0
- attune/hot_reload/config.py +83 -0
- attune/hot_reload/integration.py +229 -0
- attune/hot_reload/reloader.py +298 -0
- attune/hot_reload/watcher.py +183 -0
- attune/hot_reload/websocket.py +177 -0
- attune/levels.py +577 -0
- attune/leverage_points.py +441 -0
- attune/logging_config.py +261 -0
- attune/mcp/__init__.py +10 -0
- attune/mcp/server.py +506 -0
- attune/memory/__init__.py +237 -0
- attune/memory/claude_memory.py +469 -0
- attune/memory/config.py +224 -0
- attune/memory/control_panel.py +1290 -0
- attune/memory/control_panel_support.py +145 -0
- attune/memory/cross_session.py +845 -0
- attune/memory/edges.py +179 -0
- attune/memory/encryption.py +159 -0
- attune/memory/file_session.py +770 -0
- attune/memory/graph.py +570 -0
- attune/memory/long_term.py +913 -0
- attune/memory/long_term_types.py +99 -0
- attune/memory/mixins/__init__.py +25 -0
- attune/memory/mixins/backend_init_mixin.py +249 -0
- attune/memory/mixins/capabilities_mixin.py +208 -0
- attune/memory/mixins/handoff_mixin.py +208 -0
- attune/memory/mixins/lifecycle_mixin.py +49 -0
- attune/memory/mixins/long_term_mixin.py +352 -0
- attune/memory/mixins/promotion_mixin.py +109 -0
- attune/memory/mixins/short_term_mixin.py +182 -0
- attune/memory/nodes.py +179 -0
- attune/memory/redis_bootstrap.py +540 -0
- attune/memory/security/__init__.py +31 -0
- attune/memory/security/audit_logger.py +932 -0
- attune/memory/security/pii_scrubber.py +640 -0
- attune/memory/security/secrets_detector.py +678 -0
- attune/memory/short_term.py +2192 -0
- attune/memory/simple_storage.py +302 -0
- attune/memory/storage/__init__.py +15 -0
- attune/memory/storage_backend.py +167 -0
- attune/memory/summary_index.py +583 -0
- attune/memory/types.py +446 -0
- attune/memory/unified.py +182 -0
- attune/meta_workflows/__init__.py +74 -0
- attune/meta_workflows/agent_creator.py +248 -0
- attune/meta_workflows/builtin_templates.py +567 -0
- attune/meta_workflows/cli_commands/__init__.py +56 -0
- attune/meta_workflows/cli_commands/agent_commands.py +321 -0
- attune/meta_workflows/cli_commands/analytics_commands.py +442 -0
- attune/meta_workflows/cli_commands/config_commands.py +232 -0
- attune/meta_workflows/cli_commands/memory_commands.py +182 -0
- attune/meta_workflows/cli_commands/template_commands.py +354 -0
- attune/meta_workflows/cli_commands/workflow_commands.py +382 -0
- attune/meta_workflows/cli_meta_workflows.py +59 -0
- attune/meta_workflows/form_engine.py +292 -0
- attune/meta_workflows/intent_detector.py +409 -0
- attune/meta_workflows/models.py +569 -0
- attune/meta_workflows/pattern_learner.py +738 -0
- attune/meta_workflows/plan_generator.py +384 -0
- attune/meta_workflows/session_context.py +397 -0
- attune/meta_workflows/template_registry.py +229 -0
- attune/meta_workflows/workflow.py +984 -0
- attune/metrics/__init__.py +12 -0
- attune/metrics/collector.py +31 -0
- attune/metrics/prompt_metrics.py +194 -0
- attune/models/__init__.py +172 -0
- attune/models/__main__.py +13 -0
- attune/models/adaptive_routing.py +437 -0
- attune/models/auth_cli.py +444 -0
- attune/models/auth_strategy.py +450 -0
- attune/models/cli.py +655 -0
- attune/models/empathy_executor.py +354 -0
- attune/models/executor.py +257 -0
- attune/models/fallback.py +762 -0
- attune/models/provider_config.py +282 -0
- attune/models/registry.py +472 -0
- attune/models/tasks.py +359 -0
- attune/models/telemetry/__init__.py +71 -0
- attune/models/telemetry/analytics.py +594 -0
- attune/models/telemetry/backend.py +196 -0
- attune/models/telemetry/data_models.py +431 -0
- attune/models/telemetry/storage.py +489 -0
- attune/models/token_estimator.py +420 -0
- attune/models/validation.py +280 -0
- attune/monitoring/__init__.py +52 -0
- attune/monitoring/alerts.py +946 -0
- attune/monitoring/alerts_cli.py +448 -0
- attune/monitoring/multi_backend.py +271 -0
- attune/monitoring/otel_backend.py +362 -0
- attune/optimization/__init__.py +19 -0
- attune/optimization/context_optimizer.py +272 -0
- attune/orchestration/__init__.py +67 -0
- attune/orchestration/agent_templates.py +707 -0
- attune/orchestration/config_store.py +499 -0
- attune/orchestration/execution_strategies.py +2111 -0
- attune/orchestration/meta_orchestrator.py +1168 -0
- attune/orchestration/pattern_learner.py +696 -0
- attune/orchestration/real_tools.py +931 -0
- attune/pattern_cache.py +187 -0
- attune/pattern_library.py +542 -0
- attune/patterns/debugging/all_patterns.json +81 -0
- attune/patterns/debugging/workflow_20260107_1770825e.json +77 -0
- attune/patterns/refactoring_memory.json +89 -0
- attune/persistence.py +564 -0
- attune/platform_utils.py +265 -0
- attune/plugins/__init__.py +28 -0
- attune/plugins/base.py +361 -0
- attune/plugins/registry.py +268 -0
- attune/project_index/__init__.py +32 -0
- attune/project_index/cli.py +335 -0
- attune/project_index/index.py +667 -0
- attune/project_index/models.py +504 -0
- attune/project_index/reports.py +474 -0
- attune/project_index/scanner.py +777 -0
- attune/project_index/scanner_parallel.py +291 -0
- attune/prompts/__init__.py +61 -0
- attune/prompts/config.py +77 -0
- attune/prompts/context.py +177 -0
- attune/prompts/parser.py +285 -0
- attune/prompts/registry.py +313 -0
- attune/prompts/templates.py +208 -0
- attune/redis_config.py +302 -0
- attune/redis_memory.py +799 -0
- attune/resilience/__init__.py +56 -0
- attune/resilience/circuit_breaker.py +256 -0
- attune/resilience/fallback.py +179 -0
- attune/resilience/health.py +300 -0
- attune/resilience/retry.py +209 -0
- attune/resilience/timeout.py +135 -0
- attune/routing/__init__.py +43 -0
- attune/routing/chain_executor.py +433 -0
- attune/routing/classifier.py +217 -0
- attune/routing/smart_router.py +234 -0
- attune/routing/workflow_registry.py +343 -0
- attune/scaffolding/README.md +589 -0
- attune/scaffolding/__init__.py +35 -0
- attune/scaffolding/__main__.py +14 -0
- attune/scaffolding/cli.py +240 -0
- attune/scaffolding/templates/base_wizard.py.jinja2 +121 -0
- attune/scaffolding/templates/coach_wizard.py.jinja2 +321 -0
- attune/scaffolding/templates/domain_wizard.py.jinja2 +408 -0
- attune/scaffolding/templates/linear_flow_wizard.py.jinja2 +203 -0
- attune/socratic/__init__.py +256 -0
- attune/socratic/ab_testing.py +958 -0
- attune/socratic/blueprint.py +533 -0
- attune/socratic/cli.py +703 -0
- attune/socratic/collaboration.py +1114 -0
- attune/socratic/domain_templates.py +924 -0
- attune/socratic/embeddings.py +738 -0
- attune/socratic/engine.py +794 -0
- attune/socratic/explainer.py +682 -0
- attune/socratic/feedback.py +772 -0
- attune/socratic/forms.py +629 -0
- attune/socratic/generator.py +732 -0
- attune/socratic/llm_analyzer.py +637 -0
- attune/socratic/mcp_server.py +702 -0
- attune/socratic/session.py +312 -0
- attune/socratic/storage.py +667 -0
- attune/socratic/success.py +730 -0
- attune/socratic/visual_editor.py +860 -0
- attune/socratic/web_ui.py +958 -0
- attune/telemetry/__init__.py +39 -0
- attune/telemetry/agent_coordination.py +475 -0
- attune/telemetry/agent_tracking.py +367 -0
- attune/telemetry/approval_gates.py +545 -0
- attune/telemetry/cli.py +1231 -0
- attune/telemetry/commands/__init__.py +14 -0
- attune/telemetry/commands/dashboard_commands.py +696 -0
- attune/telemetry/event_streaming.py +409 -0
- attune/telemetry/feedback_loop.py +567 -0
- attune/telemetry/usage_tracker.py +591 -0
- attune/templates.py +754 -0
- attune/test_generator/__init__.py +38 -0
- attune/test_generator/__main__.py +14 -0
- attune/test_generator/cli.py +234 -0
- attune/test_generator/generator.py +355 -0
- attune/test_generator/risk_analyzer.py +216 -0
- attune/test_generator/templates/unit_test.py.jinja2 +272 -0
- attune/tier_recommender.py +384 -0
- attune/tools.py +183 -0
- attune/trust/__init__.py +28 -0
- attune/trust/circuit_breaker.py +579 -0
- attune/trust_building.py +527 -0
- attune/validation/__init__.py +19 -0
- attune/validation/xml_validator.py +281 -0
- attune/vscode_bridge.py +173 -0
- attune/workflow_commands.py +780 -0
- attune/workflow_patterns/__init__.py +33 -0
- attune/workflow_patterns/behavior.py +249 -0
- attune/workflow_patterns/core.py +76 -0
- attune/workflow_patterns/output.py +99 -0
- attune/workflow_patterns/registry.py +255 -0
- attune/workflow_patterns/structural.py +288 -0
- attune/workflows/__init__.py +539 -0
- attune/workflows/autonomous_test_gen.py +1268 -0
- attune/workflows/base.py +2667 -0
- attune/workflows/batch_processing.py +342 -0
- attune/workflows/bug_predict.py +1084 -0
- attune/workflows/builder.py +273 -0
- attune/workflows/caching.py +253 -0
- attune/workflows/code_review.py +1048 -0
- attune/workflows/code_review_adapters.py +312 -0
- attune/workflows/code_review_pipeline.py +722 -0
- attune/workflows/config.py +645 -0
- attune/workflows/dependency_check.py +644 -0
- attune/workflows/document_gen/__init__.py +25 -0
- attune/workflows/document_gen/config.py +30 -0
- attune/workflows/document_gen/report_formatter.py +162 -0
- attune/workflows/document_gen/workflow.py +1426 -0
- attune/workflows/document_manager.py +216 -0
- attune/workflows/document_manager_README.md +134 -0
- attune/workflows/documentation_orchestrator.py +1205 -0
- attune/workflows/history.py +510 -0
- attune/workflows/keyboard_shortcuts/__init__.py +39 -0
- attune/workflows/keyboard_shortcuts/generators.py +391 -0
- attune/workflows/keyboard_shortcuts/parsers.py +416 -0
- attune/workflows/keyboard_shortcuts/prompts.py +295 -0
- attune/workflows/keyboard_shortcuts/schema.py +193 -0
- attune/workflows/keyboard_shortcuts/workflow.py +509 -0
- attune/workflows/llm_base.py +363 -0
- attune/workflows/manage_docs.py +87 -0
- attune/workflows/manage_docs_README.md +134 -0
- attune/workflows/manage_documentation.py +821 -0
- attune/workflows/new_sample_workflow1.py +149 -0
- attune/workflows/new_sample_workflow1_README.md +150 -0
- attune/workflows/orchestrated_health_check.py +849 -0
- attune/workflows/orchestrated_release_prep.py +600 -0
- attune/workflows/output.py +413 -0
- attune/workflows/perf_audit.py +863 -0
- attune/workflows/pr_review.py +762 -0
- attune/workflows/progress.py +785 -0
- attune/workflows/progress_server.py +322 -0
- attune/workflows/progressive/README 2.md +454 -0
- attune/workflows/progressive/README.md +454 -0
- attune/workflows/progressive/__init__.py +82 -0
- attune/workflows/progressive/cli.py +219 -0
- attune/workflows/progressive/core.py +488 -0
- attune/workflows/progressive/orchestrator.py +723 -0
- attune/workflows/progressive/reports.py +520 -0
- attune/workflows/progressive/telemetry.py +274 -0
- attune/workflows/progressive/test_gen.py +495 -0
- attune/workflows/progressive/workflow.py +589 -0
- attune/workflows/refactor_plan.py +694 -0
- attune/workflows/release_prep.py +895 -0
- attune/workflows/release_prep_crew.py +969 -0
- attune/workflows/research_synthesis.py +404 -0
- attune/workflows/routing.py +168 -0
- attune/workflows/secure_release.py +593 -0
- attune/workflows/security_adapters.py +297 -0
- attune/workflows/security_audit.py +1329 -0
- attune/workflows/security_audit_phase3.py +355 -0
- attune/workflows/seo_optimization.py +633 -0
- attune/workflows/step_config.py +234 -0
- attune/workflows/telemetry_mixin.py +269 -0
- attune/workflows/test5.py +125 -0
- attune/workflows/test5_README.md +158 -0
- attune/workflows/test_coverage_boost_crew.py +849 -0
- attune/workflows/test_gen/__init__.py +52 -0
- attune/workflows/test_gen/ast_analyzer.py +249 -0
- attune/workflows/test_gen/config.py +88 -0
- attune/workflows/test_gen/data_models.py +38 -0
- attune/workflows/test_gen/report_formatter.py +289 -0
- attune/workflows/test_gen/test_templates.py +381 -0
- attune/workflows/test_gen/workflow.py +655 -0
- attune/workflows/test_gen.py +54 -0
- attune/workflows/test_gen_behavioral.py +477 -0
- attune/workflows/test_gen_parallel.py +341 -0
- attune/workflows/test_lifecycle.py +526 -0
- attune/workflows/test_maintenance.py +627 -0
- attune/workflows/test_maintenance_cli.py +590 -0
- attune/workflows/test_maintenance_crew.py +840 -0
- attune/workflows/test_runner.py +622 -0
- attune/workflows/tier_tracking.py +531 -0
- attune/workflows/xml_enhanced_crew.py +285 -0
- attune_ai-2.0.0.dist-info/METADATA +1026 -0
- attune_ai-2.0.0.dist-info/RECORD +457 -0
- attune_ai-2.0.0.dist-info/WHEEL +5 -0
- attune_ai-2.0.0.dist-info/entry_points.txt +26 -0
- attune_ai-2.0.0.dist-info/licenses/LICENSE +201 -0
- attune_ai-2.0.0.dist-info/licenses/LICENSE_CHANGE_ANNOUNCEMENT.md +101 -0
- attune_ai-2.0.0.dist-info/top_level.txt +5 -0
- attune_healthcare/__init__.py +13 -0
- attune_healthcare/monitors/__init__.py +9 -0
- attune_healthcare/monitors/clinical_protocol_monitor.py +315 -0
- attune_healthcare/monitors/monitoring/__init__.py +44 -0
- attune_healthcare/monitors/monitoring/protocol_checker.py +300 -0
- attune_healthcare/monitors/monitoring/protocol_loader.py +214 -0
- attune_healthcare/monitors/monitoring/sensor_parsers.py +306 -0
- attune_healthcare/monitors/monitoring/trajectory_analyzer.py +389 -0
- attune_llm/README.md +553 -0
- attune_llm/__init__.py +28 -0
- attune_llm/agent_factory/__init__.py +53 -0
- attune_llm/agent_factory/adapters/__init__.py +85 -0
- attune_llm/agent_factory/adapters/autogen_adapter.py +312 -0
- attune_llm/agent_factory/adapters/crewai_adapter.py +483 -0
- attune_llm/agent_factory/adapters/haystack_adapter.py +298 -0
- attune_llm/agent_factory/adapters/langchain_adapter.py +362 -0
- attune_llm/agent_factory/adapters/langgraph_adapter.py +333 -0
- attune_llm/agent_factory/adapters/native.py +228 -0
- attune_llm/agent_factory/adapters/wizard_adapter.py +423 -0
- attune_llm/agent_factory/base.py +305 -0
- attune_llm/agent_factory/crews/__init__.py +67 -0
- attune_llm/agent_factory/crews/code_review.py +1113 -0
- attune_llm/agent_factory/crews/health_check.py +1262 -0
- attune_llm/agent_factory/crews/refactoring.py +1128 -0
- attune_llm/agent_factory/crews/security_audit.py +1018 -0
- attune_llm/agent_factory/decorators.py +287 -0
- attune_llm/agent_factory/factory.py +558 -0
- attune_llm/agent_factory/framework.py +193 -0
- attune_llm/agent_factory/memory_integration.py +328 -0
- attune_llm/agent_factory/resilient.py +320 -0
- attune_llm/agents_md/__init__.py +22 -0
- attune_llm/agents_md/loader.py +218 -0
- attune_llm/agents_md/parser.py +271 -0
- attune_llm/agents_md/registry.py +307 -0
- attune_llm/claude_memory.py +466 -0
- attune_llm/cli/__init__.py +8 -0
- attune_llm/cli/sync_claude.py +487 -0
- attune_llm/code_health.py +1313 -0
- attune_llm/commands/__init__.py +51 -0
- attune_llm/commands/context.py +375 -0
- attune_llm/commands/loader.py +301 -0
- attune_llm/commands/models.py +231 -0
- attune_llm/commands/parser.py +371 -0
- attune_llm/commands/registry.py +429 -0
- attune_llm/config/__init__.py +29 -0
- attune_llm/config/unified.py +291 -0
- attune_llm/context/__init__.py +22 -0
- attune_llm/context/compaction.py +455 -0
- attune_llm/context/manager.py +434 -0
- attune_llm/contextual_patterns.py +361 -0
- attune_llm/core.py +907 -0
- attune_llm/git_pattern_extractor.py +435 -0
- attune_llm/hooks/__init__.py +24 -0
- attune_llm/hooks/config.py +306 -0
- attune_llm/hooks/executor.py +289 -0
- attune_llm/hooks/registry.py +302 -0
- attune_llm/hooks/scripts/__init__.py +39 -0
- attune_llm/hooks/scripts/evaluate_session.py +201 -0
- attune_llm/hooks/scripts/first_time_init.py +285 -0
- attune_llm/hooks/scripts/pre_compact.py +207 -0
- attune_llm/hooks/scripts/session_end.py +183 -0
- attune_llm/hooks/scripts/session_start.py +163 -0
- attune_llm/hooks/scripts/suggest_compact.py +225 -0
- attune_llm/learning/__init__.py +30 -0
- attune_llm/learning/evaluator.py +438 -0
- attune_llm/learning/extractor.py +514 -0
- attune_llm/learning/storage.py +560 -0
- attune_llm/levels.py +227 -0
- attune_llm/pattern_confidence.py +414 -0
- attune_llm/pattern_resolver.py +272 -0
- attune_llm/pattern_summary.py +350 -0
- attune_llm/providers.py +967 -0
- attune_llm/routing/__init__.py +32 -0
- attune_llm/routing/model_router.py +362 -0
- attune_llm/security/IMPLEMENTATION_SUMMARY.md +413 -0
- attune_llm/security/PHASE2_COMPLETE.md +384 -0
- attune_llm/security/PHASE2_SECRETS_DETECTOR_COMPLETE.md +271 -0
- attune_llm/security/QUICK_REFERENCE.md +316 -0
- attune_llm/security/README.md +262 -0
- attune_llm/security/__init__.py +62 -0
- attune_llm/security/audit_logger.py +929 -0
- attune_llm/security/audit_logger_example.py +152 -0
- attune_llm/security/pii_scrubber.py +640 -0
- attune_llm/security/secrets_detector.py +678 -0
- attune_llm/security/secrets_detector_example.py +304 -0
- attune_llm/security/secure_memdocs.py +1192 -0
- attune_llm/security/secure_memdocs_example.py +278 -0
- attune_llm/session_status.py +745 -0
- attune_llm/state.py +246 -0
- attune_llm/utils/__init__.py +5 -0
- attune_llm/utils/tokens.py +349 -0
- attune_software/SOFTWARE_PLUGIN_README.md +57 -0
- attune_software/__init__.py +13 -0
- attune_software/cli/__init__.py +120 -0
- attune_software/cli/inspect.py +362 -0
- attune_software/cli.py +574 -0
- attune_software/plugin.py +188 -0
- workflow_scaffolding/__init__.py +11 -0
- workflow_scaffolding/__main__.py +12 -0
- workflow_scaffolding/cli.py +206 -0
- workflow_scaffolding/generator.py +265 -0
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
"""Test Maintenance Workflow - Automatic Test Lifecycle Management
|
|
2
|
+
|
|
3
|
+
Integrates with Project Index to:
|
|
4
|
+
- Track files requiring tests
|
|
5
|
+
- Detect when tests become stale
|
|
6
|
+
- Generate test plans based on file events
|
|
7
|
+
- Execute automatic test generation
|
|
8
|
+
- Report on test health
|
|
9
|
+
|
|
10
|
+
Key events handled:
|
|
11
|
+
- File created: Check if needs tests, queue for generation
|
|
12
|
+
- File modified: Check if tests need updating
|
|
13
|
+
- File deleted: Mark associated tests as orphaned
|
|
14
|
+
|
|
15
|
+
Copyright 2025 Smart AI Memory, LLC
|
|
16
|
+
Licensed under Fair Source 0.9
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import heapq
|
|
20
|
+
import logging
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from datetime import datetime
|
|
23
|
+
from enum import Enum
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
from ..project_index import FileRecord, ProjectIndex
|
|
28
|
+
from ..project_index.reports import ReportGenerator
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TestAction(str, Enum):
|
|
34
|
+
"""Actions that can be taken for test management."""
|
|
35
|
+
|
|
36
|
+
CREATE = "create" # Create new tests
|
|
37
|
+
UPDATE = "update" # Update existing tests
|
|
38
|
+
REVIEW = "review" # Review and possibly regenerate
|
|
39
|
+
DELETE = "delete" # Delete orphaned tests
|
|
40
|
+
SKIP = "skip" # No action needed
|
|
41
|
+
MANUAL = "manual" # Requires manual intervention
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class TestPriority(str, Enum):
|
|
45
|
+
"""Priority levels for test actions."""
|
|
46
|
+
|
|
47
|
+
CRITICAL = "critical" # High-impact files, blocking
|
|
48
|
+
HIGH = "high" # Important files
|
|
49
|
+
MEDIUM = "medium" # Standard priority
|
|
50
|
+
LOW = "low" # Nice to have
|
|
51
|
+
DEFERRED = "deferred" # Can wait
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class TestPlanItem:
|
|
56
|
+
"""A single item in a test maintenance plan."""
|
|
57
|
+
|
|
58
|
+
file_path: str
|
|
59
|
+
action: TestAction
|
|
60
|
+
priority: TestPriority
|
|
61
|
+
reason: str
|
|
62
|
+
test_file_path: str | None = None
|
|
63
|
+
estimated_effort: str = "unknown"
|
|
64
|
+
auto_executable: bool = True
|
|
65
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
66
|
+
|
|
67
|
+
def to_dict(self) -> dict[str, Any]:
|
|
68
|
+
return {
|
|
69
|
+
"file_path": self.file_path,
|
|
70
|
+
"action": self.action.value,
|
|
71
|
+
"priority": self.priority.value,
|
|
72
|
+
"reason": self.reason,
|
|
73
|
+
"test_file_path": self.test_file_path,
|
|
74
|
+
"estimated_effort": self.estimated_effort,
|
|
75
|
+
"auto_executable": self.auto_executable,
|
|
76
|
+
"metadata": self.metadata,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass
|
|
81
|
+
class TestMaintenancePlan:
|
|
82
|
+
"""Complete test maintenance plan for a project."""
|
|
83
|
+
|
|
84
|
+
generated_at: datetime = field(default_factory=datetime.now)
|
|
85
|
+
items: list[TestPlanItem] = field(default_factory=list)
|
|
86
|
+
summary: dict[str, Any] = field(default_factory=dict)
|
|
87
|
+
options: list[dict[str, Any]] = field(default_factory=list)
|
|
88
|
+
|
|
89
|
+
def to_dict(self) -> dict[str, Any]:
|
|
90
|
+
return {
|
|
91
|
+
"generated_at": self.generated_at.isoformat(),
|
|
92
|
+
"items": [item.to_dict() for item in self.items],
|
|
93
|
+
"summary": self.summary,
|
|
94
|
+
"options": self.options,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
def get_items_by_action(self, action: TestAction) -> list[TestPlanItem]:
|
|
98
|
+
return [item for item in self.items if item.action == action]
|
|
99
|
+
|
|
100
|
+
def get_items_by_priority(self, priority: TestPriority) -> list[TestPlanItem]:
|
|
101
|
+
return [item for item in self.items if item.priority == priority]
|
|
102
|
+
|
|
103
|
+
def get_auto_executable_items(self) -> list[TestPlanItem]:
|
|
104
|
+
return [item for item in self.items if item.auto_executable]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class TestMaintenanceWorkflow:
|
|
108
|
+
"""Workflow for automatic test lifecycle management.
|
|
109
|
+
|
|
110
|
+
Integrates with Project Index to track and manage tests.
|
|
111
|
+
Can run automatically on file events or manually on demand.
|
|
112
|
+
|
|
113
|
+
Modes:
|
|
114
|
+
- analyze: Generate plan without executing
|
|
115
|
+
- execute: Execute plan items (with confirmation)
|
|
116
|
+
- auto: Automatically execute auto_executable items
|
|
117
|
+
- report: Generate detailed test health report
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
def __init__(self, project_root: str, index: ProjectIndex | None = None):
|
|
121
|
+
self.name = "test_maintenance"
|
|
122
|
+
self.description = "Automatic test lifecycle management"
|
|
123
|
+
self.project_root = Path(project_root)
|
|
124
|
+
self.index = index or ProjectIndex(str(project_root))
|
|
125
|
+
self._ensure_index_loaded()
|
|
126
|
+
|
|
127
|
+
def _ensure_index_loaded(self) -> None:
|
|
128
|
+
"""Ensure index is loaded, refresh if needed."""
|
|
129
|
+
if not self.index.load():
|
|
130
|
+
logger.info("Index not found, refreshing...")
|
|
131
|
+
self.index.refresh()
|
|
132
|
+
|
|
133
|
+
async def run(self, context: dict[str, Any]) -> dict[str, Any]:
|
|
134
|
+
"""Run the test maintenance workflow.
|
|
135
|
+
|
|
136
|
+
Context options:
|
|
137
|
+
mode: "analyze" | "execute" | "auto" | "report"
|
|
138
|
+
changed_files: List of files that changed (for event-driven)
|
|
139
|
+
max_items: Maximum items to process (default: 20)
|
|
140
|
+
priority_filter: Only process items of this priority or higher
|
|
141
|
+
dry_run: If True, don't actually execute (default: False)
|
|
142
|
+
"""
|
|
143
|
+
mode = context.get("mode", "analyze")
|
|
144
|
+
changed_files = context.get("changed_files", [])
|
|
145
|
+
max_items = context.get("max_items", 20)
|
|
146
|
+
dry_run = context.get("dry_run", False)
|
|
147
|
+
|
|
148
|
+
# Refresh index if files changed
|
|
149
|
+
if changed_files:
|
|
150
|
+
self.index.refresh()
|
|
151
|
+
|
|
152
|
+
# Generate the plan
|
|
153
|
+
plan = self._generate_plan(changed_files, max_items)
|
|
154
|
+
|
|
155
|
+
result = {
|
|
156
|
+
"workflow": self.name,
|
|
157
|
+
"mode": mode,
|
|
158
|
+
"generated_at": datetime.now().isoformat(),
|
|
159
|
+
"plan": plan.to_dict(),
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if mode == "analyze":
|
|
163
|
+
# Just return the plan
|
|
164
|
+
result["status"] = "plan_generated"
|
|
165
|
+
result["message"] = f"Generated plan with {len(plan.items)} items"
|
|
166
|
+
|
|
167
|
+
elif mode == "execute":
|
|
168
|
+
if dry_run:
|
|
169
|
+
result["status"] = "dry_run"
|
|
170
|
+
result["message"] = f"Would execute {len(plan.items)} items"
|
|
171
|
+
else:
|
|
172
|
+
execution_result = await self._execute_plan(plan, auto_only=False)
|
|
173
|
+
result["execution"] = execution_result
|
|
174
|
+
result["status"] = "executed"
|
|
175
|
+
|
|
176
|
+
elif mode == "auto":
|
|
177
|
+
auto_items = plan.get_auto_executable_items()
|
|
178
|
+
if dry_run:
|
|
179
|
+
result["status"] = "dry_run"
|
|
180
|
+
result["message"] = f"Would auto-execute {len(auto_items)} items"
|
|
181
|
+
else:
|
|
182
|
+
execution_result = await self._execute_plan(plan, auto_only=True)
|
|
183
|
+
result["execution"] = execution_result
|
|
184
|
+
result["status"] = "auto_executed"
|
|
185
|
+
|
|
186
|
+
elif mode == "report":
|
|
187
|
+
report = self._generate_report()
|
|
188
|
+
result["report"] = report
|
|
189
|
+
result["status"] = "report_generated"
|
|
190
|
+
|
|
191
|
+
return result
|
|
192
|
+
|
|
193
|
+
def _generate_plan(
|
|
194
|
+
self,
|
|
195
|
+
changed_files: list[str],
|
|
196
|
+
max_items: int,
|
|
197
|
+
) -> TestMaintenancePlan:
|
|
198
|
+
"""Generate a test maintenance plan."""
|
|
199
|
+
plan = TestMaintenancePlan()
|
|
200
|
+
items: list[TestPlanItem] = []
|
|
201
|
+
|
|
202
|
+
# If specific files changed, prioritize them
|
|
203
|
+
if changed_files:
|
|
204
|
+
for file_path in changed_files:
|
|
205
|
+
record = self.index.get_file(file_path)
|
|
206
|
+
if record:
|
|
207
|
+
item = self._create_plan_item_for_file(record, event="modified")
|
|
208
|
+
if item and item.action != TestAction.SKIP:
|
|
209
|
+
items.append(item)
|
|
210
|
+
|
|
211
|
+
# Add files needing tests (not in changed_files)
|
|
212
|
+
changed_set = set(changed_files)
|
|
213
|
+
for record in self.index.get_files_needing_tests():
|
|
214
|
+
if record.path not in changed_set:
|
|
215
|
+
item = self._create_plan_item_for_file(record, event="missing_tests")
|
|
216
|
+
if item and item.action != TestAction.SKIP:
|
|
217
|
+
items.append(item)
|
|
218
|
+
|
|
219
|
+
# Add stale test files
|
|
220
|
+
for record in self.index.get_stale_files():
|
|
221
|
+
if record.path not in changed_set:
|
|
222
|
+
item = self._create_plan_item_for_file(record, event="stale")
|
|
223
|
+
if item and item.action != TestAction.SKIP:
|
|
224
|
+
items.append(item)
|
|
225
|
+
|
|
226
|
+
# Sort by priority
|
|
227
|
+
priority_order = {
|
|
228
|
+
TestPriority.CRITICAL: 0,
|
|
229
|
+
TestPriority.HIGH: 1,
|
|
230
|
+
TestPriority.MEDIUM: 2,
|
|
231
|
+
TestPriority.LOW: 3,
|
|
232
|
+
TestPriority.DEFERRED: 4,
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
def get_sort_key(item: TestPlanItem) -> tuple[int, float]:
|
|
236
|
+
file_rec = self.index.get_file(item.file_path)
|
|
237
|
+
impact = float(-file_rec.impact_score) if file_rec else 0.0
|
|
238
|
+
return (priority_order[item.priority], impact)
|
|
239
|
+
|
|
240
|
+
items.sort(key=get_sort_key)
|
|
241
|
+
|
|
242
|
+
# Limit items
|
|
243
|
+
plan.items = items[:max_items]
|
|
244
|
+
|
|
245
|
+
# Generate summary
|
|
246
|
+
plan.summary = {
|
|
247
|
+
"total_items": len(items),
|
|
248
|
+
"shown_items": len(plan.items),
|
|
249
|
+
"by_action": {
|
|
250
|
+
action.value: len([i for i in items if i.action == action]) for action in TestAction
|
|
251
|
+
},
|
|
252
|
+
"by_priority": {
|
|
253
|
+
priority.value: len([i for i in items if i.priority == priority])
|
|
254
|
+
for priority in TestPriority
|
|
255
|
+
},
|
|
256
|
+
"auto_executable": len([i for i in items if i.auto_executable]),
|
|
257
|
+
"manual_required": len([i for i in items if not i.auto_executable]),
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
# Generate options for the user
|
|
261
|
+
plan.options = self._generate_options(plan)
|
|
262
|
+
|
|
263
|
+
return plan
|
|
264
|
+
|
|
265
|
+
def _create_plan_item_for_file(
|
|
266
|
+
self,
|
|
267
|
+
record: FileRecord,
|
|
268
|
+
event: str,
|
|
269
|
+
) -> TestPlanItem | None:
|
|
270
|
+
"""Create a plan item for a specific file."""
|
|
271
|
+
# Determine action based on event and file state
|
|
272
|
+
if event == "missing_tests":
|
|
273
|
+
action = TestAction.CREATE
|
|
274
|
+
reason = "File requires tests but none exist"
|
|
275
|
+
elif event == "stale":
|
|
276
|
+
action = TestAction.UPDATE
|
|
277
|
+
reason = f"Tests are {record.staleness_days} days stale"
|
|
278
|
+
elif event == "modified":
|
|
279
|
+
if record.tests_exist:
|
|
280
|
+
action = TestAction.REVIEW
|
|
281
|
+
reason = "Source file modified, tests may need update"
|
|
282
|
+
else:
|
|
283
|
+
action = TestAction.CREATE
|
|
284
|
+
reason = "Modified file needs tests"
|
|
285
|
+
elif event == "deleted":
|
|
286
|
+
action = TestAction.DELETE
|
|
287
|
+
reason = "Source file deleted, tests may be orphaned"
|
|
288
|
+
else:
|
|
289
|
+
action = TestAction.SKIP
|
|
290
|
+
reason = "No action needed"
|
|
291
|
+
|
|
292
|
+
# Determine priority based on impact score
|
|
293
|
+
if record.impact_score >= 10.0:
|
|
294
|
+
priority = TestPriority.CRITICAL
|
|
295
|
+
elif record.impact_score >= 5.0:
|
|
296
|
+
priority = TestPriority.HIGH
|
|
297
|
+
elif record.impact_score >= 2.0:
|
|
298
|
+
priority = TestPriority.MEDIUM
|
|
299
|
+
else:
|
|
300
|
+
priority = TestPriority.LOW
|
|
301
|
+
|
|
302
|
+
# Estimate effort
|
|
303
|
+
if record.lines_of_code < 50:
|
|
304
|
+
effort = "small (< 1 hour)"
|
|
305
|
+
elif record.lines_of_code < 200:
|
|
306
|
+
effort = "medium (1-2 hours)"
|
|
307
|
+
else:
|
|
308
|
+
effort = "large (2+ hours)"
|
|
309
|
+
|
|
310
|
+
# Determine if auto-executable
|
|
311
|
+
auto_executable = (
|
|
312
|
+
action in [TestAction.CREATE, TestAction.UPDATE]
|
|
313
|
+
and record.language == "python"
|
|
314
|
+
and record.lines_of_code < 500
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
return TestPlanItem(
|
|
318
|
+
file_path=record.path,
|
|
319
|
+
action=action,
|
|
320
|
+
priority=priority,
|
|
321
|
+
reason=reason,
|
|
322
|
+
test_file_path=record.test_file_path,
|
|
323
|
+
estimated_effort=effort,
|
|
324
|
+
auto_executable=auto_executable,
|
|
325
|
+
metadata={
|
|
326
|
+
"lines_of_code": record.lines_of_code,
|
|
327
|
+
"impact_score": record.impact_score,
|
|
328
|
+
"language": record.language,
|
|
329
|
+
"complexity": record.complexity_score,
|
|
330
|
+
},
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
def _generate_options(self, plan: TestMaintenancePlan) -> list[dict[str, Any]]:
|
|
334
|
+
"""Generate execution options for the user."""
|
|
335
|
+
options = []
|
|
336
|
+
|
|
337
|
+
# Option 1: Execute all auto-executable
|
|
338
|
+
auto_count = len(plan.get_auto_executable_items())
|
|
339
|
+
if auto_count > 0:
|
|
340
|
+
options.append(
|
|
341
|
+
{
|
|
342
|
+
"id": "auto_all",
|
|
343
|
+
"name": "Auto-execute all",
|
|
344
|
+
"description": f"Automatically generate/update tests for {auto_count} files",
|
|
345
|
+
"item_count": auto_count,
|
|
346
|
+
"estimated_time": f"{auto_count * 5}-{auto_count * 15} minutes",
|
|
347
|
+
"command": "python -m attune.workflows.test_maintenance auto",
|
|
348
|
+
},
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
# Option 2: Critical only
|
|
352
|
+
critical_count = len(plan.get_items_by_priority(TestPriority.CRITICAL))
|
|
353
|
+
if critical_count > 0:
|
|
354
|
+
options.append(
|
|
355
|
+
{
|
|
356
|
+
"id": "critical_only",
|
|
357
|
+
"name": "Critical files only",
|
|
358
|
+
"description": f"Focus on {critical_count} critical high-impact files",
|
|
359
|
+
"item_count": critical_count,
|
|
360
|
+
"estimated_time": f"{critical_count * 10}-{critical_count * 20} minutes",
|
|
361
|
+
"command": "python -m attune.workflows.test_maintenance execute --priority critical",
|
|
362
|
+
},
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
# Option 3: Create new tests only
|
|
366
|
+
create_count = len(plan.get_items_by_action(TestAction.CREATE))
|
|
367
|
+
if create_count > 0:
|
|
368
|
+
options.append(
|
|
369
|
+
{
|
|
370
|
+
"id": "create_only",
|
|
371
|
+
"name": "Create new tests only",
|
|
372
|
+
"description": f"Generate tests for {create_count} files without tests",
|
|
373
|
+
"item_count": create_count,
|
|
374
|
+
"estimated_time": f"{create_count * 10}-{create_count * 20} minutes",
|
|
375
|
+
"command": "python -m attune.workflows.test_maintenance execute --action create",
|
|
376
|
+
},
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
# Option 4: Update stale tests only
|
|
380
|
+
update_count = len(plan.get_items_by_action(TestAction.UPDATE))
|
|
381
|
+
if update_count > 0:
|
|
382
|
+
options.append(
|
|
383
|
+
{
|
|
384
|
+
"id": "update_stale",
|
|
385
|
+
"name": "Update stale tests",
|
|
386
|
+
"description": f"Update {update_count} stale test files",
|
|
387
|
+
"item_count": update_count,
|
|
388
|
+
"estimated_time": f"{update_count * 5}-{update_count * 10} minutes",
|
|
389
|
+
"command": "python -m attune.workflows.test_maintenance execute --action update",
|
|
390
|
+
},
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
# Option 5: Manual review
|
|
394
|
+
options.append(
|
|
395
|
+
{
|
|
396
|
+
"id": "manual_review",
|
|
397
|
+
"name": "Manual review",
|
|
398
|
+
"description": "Review the plan and select specific items",
|
|
399
|
+
"item_count": len(plan.items),
|
|
400
|
+
"command": "python -m attune.workflows.test_maintenance analyze --json",
|
|
401
|
+
},
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
return options
|
|
405
|
+
|
|
406
|
+
async def _execute_plan(
|
|
407
|
+
self,
|
|
408
|
+
plan: TestMaintenancePlan,
|
|
409
|
+
auto_only: bool = False,
|
|
410
|
+
) -> dict[str, Any]:
|
|
411
|
+
"""Execute items in the plan."""
|
|
412
|
+
items_to_execute = plan.get_auto_executable_items() if auto_only else plan.items
|
|
413
|
+
|
|
414
|
+
# Use typed variables for proper type inference
|
|
415
|
+
succeeded = 0
|
|
416
|
+
failed = 0
|
|
417
|
+
skipped = 0
|
|
418
|
+
details: list[dict[str, Any]] = []
|
|
419
|
+
|
|
420
|
+
for item in items_to_execute:
|
|
421
|
+
try:
|
|
422
|
+
if item.action == TestAction.CREATE:
|
|
423
|
+
success = await self._create_tests_for_file(item)
|
|
424
|
+
elif item.action == TestAction.UPDATE:
|
|
425
|
+
success = await self._update_tests_for_file(item)
|
|
426
|
+
elif item.action == TestAction.REVIEW:
|
|
427
|
+
success = await self._review_tests_for_file(item)
|
|
428
|
+
elif item.action == TestAction.DELETE:
|
|
429
|
+
success = await self._delete_orphaned_tests(item)
|
|
430
|
+
else:
|
|
431
|
+
success = False
|
|
432
|
+
skipped += 1
|
|
433
|
+
continue
|
|
434
|
+
|
|
435
|
+
if success:
|
|
436
|
+
succeeded += 1
|
|
437
|
+
# Update index
|
|
438
|
+
self.index.update_file(
|
|
439
|
+
item.file_path,
|
|
440
|
+
tests_exist=True,
|
|
441
|
+
tests_last_modified=datetime.now(),
|
|
442
|
+
is_stale=False,
|
|
443
|
+
staleness_days=0,
|
|
444
|
+
)
|
|
445
|
+
else:
|
|
446
|
+
failed += 1
|
|
447
|
+
|
|
448
|
+
details.append(
|
|
449
|
+
{
|
|
450
|
+
"file": item.file_path,
|
|
451
|
+
"action": item.action.value,
|
|
452
|
+
"success": success,
|
|
453
|
+
},
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
except Exception as e:
|
|
457
|
+
logger.error(f"Error processing {item.file_path}: {e}")
|
|
458
|
+
failed += 1
|
|
459
|
+
details.append(
|
|
460
|
+
{
|
|
461
|
+
"file": item.file_path,
|
|
462
|
+
"action": item.action.value,
|
|
463
|
+
"success": False,
|
|
464
|
+
"error": str(e),
|
|
465
|
+
},
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
return {
|
|
469
|
+
"total": len(items_to_execute),
|
|
470
|
+
"succeeded": succeeded,
|
|
471
|
+
"failed": failed,
|
|
472
|
+
"skipped": skipped,
|
|
473
|
+
"details": details,
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
async def _create_tests_for_file(self, item: TestPlanItem) -> bool:
|
|
477
|
+
"""Create tests for a file using test-gen workflow."""
|
|
478
|
+
# This would integrate with the test-gen workflow
|
|
479
|
+
# For now, return True as placeholder
|
|
480
|
+
logger.info(f"Would create tests for: {item.file_path}")
|
|
481
|
+
return True
|
|
482
|
+
|
|
483
|
+
async def _update_tests_for_file(self, item: TestPlanItem) -> bool:
|
|
484
|
+
"""Update existing tests for a file."""
|
|
485
|
+
logger.info(f"Would update tests for: {item.file_path}")
|
|
486
|
+
return True
|
|
487
|
+
|
|
488
|
+
async def _review_tests_for_file(self, item: TestPlanItem) -> bool:
|
|
489
|
+
"""Review and possibly regenerate tests."""
|
|
490
|
+
logger.info(f"Would review tests for: {item.file_path}")
|
|
491
|
+
return True
|
|
492
|
+
|
|
493
|
+
async def _delete_orphaned_tests(self, item: TestPlanItem) -> bool:
|
|
494
|
+
"""Delete orphaned test files."""
|
|
495
|
+
logger.info(f"Would delete orphaned tests for: {item.file_path}")
|
|
496
|
+
return True
|
|
497
|
+
|
|
498
|
+
def _generate_report(self) -> dict[str, Any]:
|
|
499
|
+
"""Generate detailed test health report."""
|
|
500
|
+
generator = ReportGenerator(
|
|
501
|
+
self.index.get_summary(),
|
|
502
|
+
self.index.get_all_files(),
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
return {
|
|
506
|
+
"health": generator.health_report(),
|
|
507
|
+
"test_gap": generator.test_gap_report(),
|
|
508
|
+
"staleness": generator.staleness_report(),
|
|
509
|
+
"coverage": generator.coverage_report(),
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
# ===== Event Handlers =====
|
|
513
|
+
|
|
514
|
+
async def on_file_created(self, file_path: str) -> dict[str, Any]:
|
|
515
|
+
"""Handle file creation event."""
|
|
516
|
+
self.index.refresh()
|
|
517
|
+
record = self.index.get_file(file_path)
|
|
518
|
+
|
|
519
|
+
if not record:
|
|
520
|
+
return {"status": "not_indexed", "file": file_path}
|
|
521
|
+
|
|
522
|
+
if record.test_requirement.value == "required":
|
|
523
|
+
item = self._create_plan_item_for_file(record, event="missing_tests")
|
|
524
|
+
return {
|
|
525
|
+
"status": "needs_tests",
|
|
526
|
+
"file": file_path,
|
|
527
|
+
"plan_item": item.to_dict() if item else None,
|
|
528
|
+
"message": f"New file {file_path} requires tests",
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
return {"status": "no_tests_required", "file": file_path}
|
|
532
|
+
|
|
533
|
+
async def on_file_modified(self, file_path: str) -> dict[str, Any]:
|
|
534
|
+
"""Handle file modification event."""
|
|
535
|
+
record = self.index.get_file(file_path)
|
|
536
|
+
|
|
537
|
+
if not record:
|
|
538
|
+
self.index.refresh()
|
|
539
|
+
record = self.index.get_file(file_path)
|
|
540
|
+
|
|
541
|
+
if not record:
|
|
542
|
+
return {"status": "not_indexed", "file": file_path}
|
|
543
|
+
|
|
544
|
+
# Mark as potentially stale
|
|
545
|
+
if record.tests_exist and record.test_file_path:
|
|
546
|
+
self.index.update_file(
|
|
547
|
+
file_path,
|
|
548
|
+
last_modified=datetime.now(),
|
|
549
|
+
is_stale=True,
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
item = self._create_plan_item_for_file(record, event="modified")
|
|
553
|
+
return {
|
|
554
|
+
"status": "tests_may_need_update",
|
|
555
|
+
"file": file_path,
|
|
556
|
+
"test_file": record.test_file_path,
|
|
557
|
+
"plan_item": item.to_dict() if item else None,
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if record.test_requirement.value == "required":
|
|
561
|
+
item = self._create_plan_item_for_file(record, event="modified")
|
|
562
|
+
return {
|
|
563
|
+
"status": "needs_tests",
|
|
564
|
+
"file": file_path,
|
|
565
|
+
"plan_item": item.to_dict() if item else None,
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
return {"status": "no_action_needed", "file": file_path}
|
|
569
|
+
|
|
570
|
+
async def on_file_deleted(self, file_path: str) -> dict[str, Any]:
|
|
571
|
+
"""Handle file deletion event."""
|
|
572
|
+
record = self.index.get_file(file_path)
|
|
573
|
+
|
|
574
|
+
if record and record.test_file_path:
|
|
575
|
+
test_path = self.project_root / record.test_file_path
|
|
576
|
+
if test_path.exists():
|
|
577
|
+
return {
|
|
578
|
+
"status": "orphaned_tests",
|
|
579
|
+
"file": file_path,
|
|
580
|
+
"test_file": record.test_file_path,
|
|
581
|
+
"message": f"Tests at {record.test_file_path} may be orphaned",
|
|
582
|
+
"action": "review_for_deletion",
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
# Refresh index to remove deleted file
|
|
586
|
+
self.index.refresh()
|
|
587
|
+
|
|
588
|
+
return {"status": "file_removed", "file": file_path}
|
|
589
|
+
|
|
590
|
+
# ===== Convenience Methods =====
|
|
591
|
+
|
|
592
|
+
def get_files_needing_tests(self, limit: int = 20) -> list[dict[str, Any]]:
|
|
593
|
+
"""Get files that need tests, prioritized by impact."""
|
|
594
|
+
files = self.index.get_files_needing_tests()
|
|
595
|
+
return [
|
|
596
|
+
{
|
|
597
|
+
"path": f.path,
|
|
598
|
+
"impact_score": f.impact_score,
|
|
599
|
+
"lines_of_code": f.lines_of_code,
|
|
600
|
+
"language": f.language,
|
|
601
|
+
}
|
|
602
|
+
for f in heapq.nlargest(limit, files, key=lambda x: x.impact_score)
|
|
603
|
+
]
|
|
604
|
+
|
|
605
|
+
def get_stale_tests(self, limit: int = 20) -> list[dict[str, Any]]:
|
|
606
|
+
"""Get files with stale tests."""
|
|
607
|
+
files = self.index.get_stale_files()
|
|
608
|
+
return [
|
|
609
|
+
{
|
|
610
|
+
"path": f.path,
|
|
611
|
+
"test_file": f.test_file_path,
|
|
612
|
+
"staleness_days": f.staleness_days,
|
|
613
|
+
}
|
|
614
|
+
for f in heapq.nlargest(limit, files, key=lambda x: x.staleness_days)
|
|
615
|
+
]
|
|
616
|
+
|
|
617
|
+
def get_test_health_summary(self) -> dict[str, Any]:
|
|
618
|
+
"""Get quick test health summary."""
|
|
619
|
+
summary = self.index.get_summary()
|
|
620
|
+
return {
|
|
621
|
+
"files_requiring_tests": summary.files_requiring_tests,
|
|
622
|
+
"files_with_tests": summary.files_with_tests,
|
|
623
|
+
"files_without_tests": summary.files_without_tests,
|
|
624
|
+
"coverage_avg": summary.test_coverage_avg,
|
|
625
|
+
"stale_count": summary.stale_file_count,
|
|
626
|
+
"test_to_code_ratio": summary.test_to_code_ratio,
|
|
627
|
+
}
|