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,655 @@
|
|
|
1
|
+
"""Test Generation Workflow.
|
|
2
|
+
|
|
3
|
+
Main workflow orchestration for test generation.
|
|
4
|
+
|
|
5
|
+
Copyright 2025 Smart-AI-Memory
|
|
6
|
+
Licensed under Fair Source License 0.9
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from ..base import BaseWorkflow, ModelTier
|
|
14
|
+
from .ast_analyzer import ASTFunctionAnalyzer
|
|
15
|
+
from .config import DEFAULT_SKIP_PATTERNS
|
|
16
|
+
from .test_templates import (
|
|
17
|
+
generate_test_for_class,
|
|
18
|
+
generate_test_for_function,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TestGenerationWorkflow(BaseWorkflow):
|
|
23
|
+
"""Generate tests targeting areas with historical bugs.
|
|
24
|
+
|
|
25
|
+
Prioritizes test generation for files that have historically
|
|
26
|
+
been bug-prone and have low test coverage.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
name = "test-gen"
|
|
30
|
+
description = "Generate tests targeting areas with historical bugs"
|
|
31
|
+
stages = ["identify", "analyze", "generate", "review"]
|
|
32
|
+
tier_map = {
|
|
33
|
+
"identify": ModelTier.CHEAP,
|
|
34
|
+
"analyze": ModelTier.CAPABLE,
|
|
35
|
+
"generate": ModelTier.CAPABLE,
|
|
36
|
+
"review": ModelTier.PREMIUM,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
patterns_dir: str = "./patterns",
|
|
42
|
+
min_tests_for_review: int = 10,
|
|
43
|
+
write_tests: bool = False,
|
|
44
|
+
output_dir: str = "tests/generated",
|
|
45
|
+
enable_auth_strategy: bool = True,
|
|
46
|
+
**kwargs: Any,
|
|
47
|
+
):
|
|
48
|
+
"""Initialize test generation workflow.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
patterns_dir: Directory containing learned patterns
|
|
52
|
+
min_tests_for_review: Minimum tests generated to trigger premium review
|
|
53
|
+
write_tests: If True, write generated tests to output_dir
|
|
54
|
+
output_dir: Directory to write generated test files
|
|
55
|
+
enable_auth_strategy: Enable intelligent auth routing (default: True)
|
|
56
|
+
**kwargs: Additional arguments passed to BaseWorkflow
|
|
57
|
+
|
|
58
|
+
"""
|
|
59
|
+
super().__init__(**kwargs)
|
|
60
|
+
self.patterns_dir = patterns_dir
|
|
61
|
+
self.min_tests_for_review = min_tests_for_review
|
|
62
|
+
self.write_tests = write_tests
|
|
63
|
+
self.output_dir = output_dir
|
|
64
|
+
self.enable_auth_strategy = enable_auth_strategy
|
|
65
|
+
self._test_count: int = 0
|
|
66
|
+
self._bug_hotspots: list[str] = []
|
|
67
|
+
self._auth_mode_used: str | None = None
|
|
68
|
+
self._load_bug_hotspots()
|
|
69
|
+
|
|
70
|
+
def _load_bug_hotspots(self) -> None:
|
|
71
|
+
"""Load files with historical bugs from pattern library."""
|
|
72
|
+
debugging_file = Path(self.patterns_dir) / "debugging.json"
|
|
73
|
+
if debugging_file.exists():
|
|
74
|
+
try:
|
|
75
|
+
with open(debugging_file) as fh:
|
|
76
|
+
data = json.load(fh)
|
|
77
|
+
patterns = data.get("patterns", [])
|
|
78
|
+
# Extract files from bug patterns
|
|
79
|
+
files = set()
|
|
80
|
+
for p in patterns:
|
|
81
|
+
for file_entry in p.get("files_affected", []):
|
|
82
|
+
if file_entry is None:
|
|
83
|
+
continue
|
|
84
|
+
files.add(str(file_entry))
|
|
85
|
+
self._bug_hotspots = list(files)
|
|
86
|
+
except (json.JSONDecodeError, OSError):
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
def should_skip_stage(self, stage_name: str, input_data: Any) -> tuple[bool, str | None]:
|
|
90
|
+
"""Downgrade review stage if few tests generated.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
stage_name: Name of the stage to check
|
|
94
|
+
input_data: Current workflow data
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Tuple of (should_skip, reason)
|
|
98
|
+
|
|
99
|
+
"""
|
|
100
|
+
if stage_name == "review":
|
|
101
|
+
if self._test_count < self.min_tests_for_review:
|
|
102
|
+
# Downgrade to CAPABLE
|
|
103
|
+
self.tier_map["review"] = ModelTier.CAPABLE
|
|
104
|
+
return False, None
|
|
105
|
+
return False, None
|
|
106
|
+
|
|
107
|
+
async def run_stage(
|
|
108
|
+
self,
|
|
109
|
+
stage_name: str,
|
|
110
|
+
tier: ModelTier,
|
|
111
|
+
input_data: Any,
|
|
112
|
+
) -> tuple[Any, int, int]:
|
|
113
|
+
"""Route to specific stage implementation."""
|
|
114
|
+
if stage_name == "identify":
|
|
115
|
+
return await self._identify(input_data, tier)
|
|
116
|
+
if stage_name == "analyze":
|
|
117
|
+
return await self._analyze(input_data, tier)
|
|
118
|
+
if stage_name == "generate":
|
|
119
|
+
return await self._generate(input_data, tier)
|
|
120
|
+
if stage_name == "review":
|
|
121
|
+
return await self._review(input_data, tier)
|
|
122
|
+
raise ValueError(f"Unknown stage: {stage_name}")
|
|
123
|
+
|
|
124
|
+
async def _identify(self, input_data: dict, tier: ModelTier) -> tuple[dict, int, int]:
|
|
125
|
+
"""Identify files needing tests.
|
|
126
|
+
|
|
127
|
+
Finds files with low coverage, historical bugs, or
|
|
128
|
+
no existing tests.
|
|
129
|
+
|
|
130
|
+
Configurable options via input_data:
|
|
131
|
+
max_files_to_scan: Maximum files to scan before stopping (default: 1000)
|
|
132
|
+
max_file_size_kb: Skip files larger than this (default: 200)
|
|
133
|
+
max_candidates: Maximum candidates to return (default: 50)
|
|
134
|
+
skip_patterns: List of directory patterns to skip (default: DEFAULT_SKIP_PATTERNS)
|
|
135
|
+
include_all_files: Include files with priority=0 (default: False)
|
|
136
|
+
"""
|
|
137
|
+
target_path = input_data.get("path", ".")
|
|
138
|
+
file_types = input_data.get("file_types", [".py"])
|
|
139
|
+
|
|
140
|
+
# === AUTH STRATEGY INTEGRATION ===
|
|
141
|
+
if self.enable_auth_strategy:
|
|
142
|
+
try:
|
|
143
|
+
import logging
|
|
144
|
+
from pathlib import Path
|
|
145
|
+
|
|
146
|
+
from attune.models import (
|
|
147
|
+
count_lines_of_code,
|
|
148
|
+
get_auth_strategy,
|
|
149
|
+
get_module_size_category,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
logger = logging.getLogger(__name__)
|
|
153
|
+
|
|
154
|
+
# Calculate total LOC for the project/path
|
|
155
|
+
target = Path(target_path)
|
|
156
|
+
total_lines = 0
|
|
157
|
+
if target.is_file():
|
|
158
|
+
total_lines = count_lines_of_code(target)
|
|
159
|
+
elif target.is_dir():
|
|
160
|
+
# Estimate total lines for directory
|
|
161
|
+
for py_file in target.rglob("*.py"):
|
|
162
|
+
try:
|
|
163
|
+
total_lines += count_lines_of_code(py_file)
|
|
164
|
+
except Exception:
|
|
165
|
+
pass
|
|
166
|
+
|
|
167
|
+
if total_lines > 0:
|
|
168
|
+
strategy = get_auth_strategy()
|
|
169
|
+
recommended_mode = strategy.get_recommended_mode(total_lines)
|
|
170
|
+
self._auth_mode_used = recommended_mode.value
|
|
171
|
+
|
|
172
|
+
size_category = get_module_size_category(total_lines)
|
|
173
|
+
logger.info(
|
|
174
|
+
f"Test generation target: {target_path} "
|
|
175
|
+
f"({total_lines:,} LOC, {size_category})"
|
|
176
|
+
)
|
|
177
|
+
logger.info(f"Recommended auth mode: {recommended_mode.value}")
|
|
178
|
+
|
|
179
|
+
cost_estimate = strategy.estimate_cost(total_lines, recommended_mode)
|
|
180
|
+
if recommended_mode.value == "subscription":
|
|
181
|
+
logger.info(f"Cost: {cost_estimate['quota_cost']}")
|
|
182
|
+
else:
|
|
183
|
+
logger.info(f"Cost: ~${cost_estimate['monetary_cost']:.4f}")
|
|
184
|
+
|
|
185
|
+
except Exception as e:
|
|
186
|
+
import logging
|
|
187
|
+
|
|
188
|
+
logger = logging.getLogger(__name__)
|
|
189
|
+
logger.warning(f"Auth strategy detection failed: {e}")
|
|
190
|
+
|
|
191
|
+
# Parse configurable limits with sensible defaults
|
|
192
|
+
max_files_to_scan = input_data.get("max_files_to_scan", 1000)
|
|
193
|
+
max_file_size_kb = input_data.get("max_file_size_kb", 200)
|
|
194
|
+
max_candidates = input_data.get("max_candidates", 50)
|
|
195
|
+
skip_patterns = input_data.get("skip_patterns", DEFAULT_SKIP_PATTERNS)
|
|
196
|
+
include_all_files = input_data.get("include_all_files", False)
|
|
197
|
+
|
|
198
|
+
target = Path(target_path)
|
|
199
|
+
candidates: list[dict] = []
|
|
200
|
+
|
|
201
|
+
# Track project scope for enterprise reporting
|
|
202
|
+
total_source_files = 0
|
|
203
|
+
existing_test_files = 0
|
|
204
|
+
|
|
205
|
+
# Track scan summary for debugging/visibility
|
|
206
|
+
# Use separate counters for type safety
|
|
207
|
+
scan_counts = {
|
|
208
|
+
"files_scanned": 0,
|
|
209
|
+
"files_too_large": 0,
|
|
210
|
+
"files_read_error": 0,
|
|
211
|
+
"files_excluded_by_pattern": 0,
|
|
212
|
+
}
|
|
213
|
+
early_exit_reason: str | None = None
|
|
214
|
+
|
|
215
|
+
max_file_size_bytes = max_file_size_kb * 1024
|
|
216
|
+
scan_limit_reached = False
|
|
217
|
+
|
|
218
|
+
if target.exists():
|
|
219
|
+
for ext in file_types:
|
|
220
|
+
if scan_limit_reached:
|
|
221
|
+
break
|
|
222
|
+
|
|
223
|
+
for file_path in target.rglob(f"*{ext}"):
|
|
224
|
+
# Check if we've hit the scan limit
|
|
225
|
+
if scan_counts["files_scanned"] >= max_files_to_scan:
|
|
226
|
+
early_exit_reason = f"max_files_to_scan ({max_files_to_scan}) reached"
|
|
227
|
+
scan_limit_reached = True
|
|
228
|
+
break
|
|
229
|
+
|
|
230
|
+
# Skip non-code directories using configurable patterns
|
|
231
|
+
file_str = str(file_path)
|
|
232
|
+
if any(skip in file_str for skip in skip_patterns):
|
|
233
|
+
scan_counts["files_excluded_by_pattern"] += 1
|
|
234
|
+
continue
|
|
235
|
+
|
|
236
|
+
# Count test files separately for scope awareness
|
|
237
|
+
if "test_" in file_str or "_test." in file_str or "/tests/" in file_str:
|
|
238
|
+
existing_test_files += 1
|
|
239
|
+
continue
|
|
240
|
+
|
|
241
|
+
# Check file size before reading
|
|
242
|
+
try:
|
|
243
|
+
file_size = file_path.stat().st_size
|
|
244
|
+
if file_size > max_file_size_bytes:
|
|
245
|
+
scan_counts["files_too_large"] += 1
|
|
246
|
+
continue
|
|
247
|
+
except OSError:
|
|
248
|
+
scan_counts["files_read_error"] += 1
|
|
249
|
+
continue
|
|
250
|
+
|
|
251
|
+
# Count source files and increment scan counter
|
|
252
|
+
total_source_files += 1
|
|
253
|
+
scan_counts["files_scanned"] += 1
|
|
254
|
+
|
|
255
|
+
try:
|
|
256
|
+
content = file_path.read_text(errors="ignore")
|
|
257
|
+
lines = len(content.splitlines())
|
|
258
|
+
|
|
259
|
+
# Check if in bug hotspots
|
|
260
|
+
is_hotspot = any(hotspot in file_str for hotspot in self._bug_hotspots)
|
|
261
|
+
|
|
262
|
+
# Check for existing tests
|
|
263
|
+
test_file = self._find_test_file(file_path)
|
|
264
|
+
has_tests = test_file.exists() if test_file else False
|
|
265
|
+
|
|
266
|
+
# Calculate priority
|
|
267
|
+
priority = 0
|
|
268
|
+
if is_hotspot:
|
|
269
|
+
priority += 50
|
|
270
|
+
if not has_tests:
|
|
271
|
+
priority += 30
|
|
272
|
+
if lines > 100:
|
|
273
|
+
priority += 10
|
|
274
|
+
if lines > 300:
|
|
275
|
+
priority += 10
|
|
276
|
+
|
|
277
|
+
# Include if priority > 0 OR include_all_files is set
|
|
278
|
+
if priority > 0 or include_all_files:
|
|
279
|
+
candidates.append(
|
|
280
|
+
{
|
|
281
|
+
"file": file_str,
|
|
282
|
+
"lines": lines,
|
|
283
|
+
"is_hotspot": is_hotspot,
|
|
284
|
+
"has_tests": has_tests,
|
|
285
|
+
"priority": priority,
|
|
286
|
+
},
|
|
287
|
+
)
|
|
288
|
+
except OSError:
|
|
289
|
+
scan_counts["files_read_error"] += 1
|
|
290
|
+
continue
|
|
291
|
+
|
|
292
|
+
# Sort by priority
|
|
293
|
+
candidates.sort(key=lambda x: -x["priority"])
|
|
294
|
+
|
|
295
|
+
input_tokens = len(str(input_data)) // 4
|
|
296
|
+
output_tokens = len(str(candidates)) // 4
|
|
297
|
+
|
|
298
|
+
# Calculate scope metrics for enterprise reporting
|
|
299
|
+
analyzed_count = min(max_candidates, len(candidates))
|
|
300
|
+
coverage_pct = (analyzed_count / len(candidates) * 100) if candidates else 100
|
|
301
|
+
|
|
302
|
+
return (
|
|
303
|
+
{
|
|
304
|
+
"candidates": candidates[:max_candidates],
|
|
305
|
+
"total_candidates": len(candidates),
|
|
306
|
+
"hotspot_count": sum(1 for c in candidates if c["is_hotspot"]),
|
|
307
|
+
"untested_count": sum(1 for c in candidates if not c["has_tests"]),
|
|
308
|
+
# Scope awareness fields for enterprise reporting
|
|
309
|
+
"total_source_files": total_source_files,
|
|
310
|
+
"existing_test_files": existing_test_files,
|
|
311
|
+
"large_project_warning": len(candidates) > 100,
|
|
312
|
+
"analysis_coverage_percent": coverage_pct,
|
|
313
|
+
# Scan summary for debugging/visibility
|
|
314
|
+
"scan_summary": {**scan_counts, "early_exit_reason": early_exit_reason},
|
|
315
|
+
# Pass through config for subsequent stages
|
|
316
|
+
"config": {
|
|
317
|
+
"max_files_to_analyze": input_data.get("max_files_to_analyze", 20),
|
|
318
|
+
"max_functions_per_file": input_data.get("max_functions_per_file", 30),
|
|
319
|
+
"max_classes_per_file": input_data.get("max_classes_per_file", 15),
|
|
320
|
+
"max_files_to_generate": input_data.get("max_files_to_generate", 15),
|
|
321
|
+
"max_functions_to_generate": input_data.get("max_functions_to_generate", 8),
|
|
322
|
+
"max_classes_to_generate": input_data.get("max_classes_to_generate", 4),
|
|
323
|
+
},
|
|
324
|
+
**input_data,
|
|
325
|
+
},
|
|
326
|
+
input_tokens,
|
|
327
|
+
output_tokens,
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
def _find_test_file(self, source_file: Path) -> Path | None:
|
|
331
|
+
"""Find corresponding test file for a source file."""
|
|
332
|
+
name = source_file.stem
|
|
333
|
+
parent = source_file.parent
|
|
334
|
+
|
|
335
|
+
# Check common test locations
|
|
336
|
+
possible = [
|
|
337
|
+
parent / f"test_{name}.py",
|
|
338
|
+
parent / "tests" / f"test_{name}.py",
|
|
339
|
+
parent.parent / "tests" / f"test_{name}.py",
|
|
340
|
+
]
|
|
341
|
+
|
|
342
|
+
for p in possible:
|
|
343
|
+
if p.exists():
|
|
344
|
+
return p
|
|
345
|
+
|
|
346
|
+
return possible[0] # Return expected location even if doesn't exist
|
|
347
|
+
|
|
348
|
+
async def _analyze(self, input_data: dict, tier: ModelTier) -> tuple[dict, int, int]:
|
|
349
|
+
"""Analyze code structure for test generation.
|
|
350
|
+
|
|
351
|
+
Examines functions, classes, and patterns to determine
|
|
352
|
+
what tests should be generated.
|
|
353
|
+
|
|
354
|
+
Uses config from _identify stage for limits:
|
|
355
|
+
max_files_to_analyze: Maximum files to analyze (default: 20)
|
|
356
|
+
max_functions_per_file: Maximum functions per file (default: 30)
|
|
357
|
+
max_classes_per_file: Maximum classes per file (default: 15)
|
|
358
|
+
"""
|
|
359
|
+
# Get config from previous stage or use defaults
|
|
360
|
+
config = input_data.get("config", {})
|
|
361
|
+
max_files_to_analyze = config.get("max_files_to_analyze", 20)
|
|
362
|
+
max_functions_per_file = config.get("max_functions_per_file", 30)
|
|
363
|
+
max_classes_per_file = config.get("max_classes_per_file", 15)
|
|
364
|
+
|
|
365
|
+
candidates = input_data.get("candidates", [])[:max_files_to_analyze]
|
|
366
|
+
analysis: list[dict] = []
|
|
367
|
+
parse_errors: list[str] = [] # Track files that failed to parse
|
|
368
|
+
|
|
369
|
+
for candidate in candidates:
|
|
370
|
+
file_path = Path(candidate["file"])
|
|
371
|
+
if not file_path.exists():
|
|
372
|
+
continue
|
|
373
|
+
|
|
374
|
+
try:
|
|
375
|
+
content = file_path.read_text(errors="ignore")
|
|
376
|
+
|
|
377
|
+
# Extract testable items with configurable limits and error tracking
|
|
378
|
+
functions, func_error = self._extract_functions(
|
|
379
|
+
content,
|
|
380
|
+
candidate["file"],
|
|
381
|
+
max_functions_per_file,
|
|
382
|
+
)
|
|
383
|
+
classes, class_error = self._extract_classes(
|
|
384
|
+
content,
|
|
385
|
+
candidate["file"],
|
|
386
|
+
max_classes_per_file,
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
# Track parse errors for visibility
|
|
390
|
+
if func_error:
|
|
391
|
+
parse_errors.append(func_error)
|
|
392
|
+
if class_error and class_error != func_error:
|
|
393
|
+
parse_errors.append(class_error)
|
|
394
|
+
|
|
395
|
+
analysis.append(
|
|
396
|
+
{
|
|
397
|
+
"file": candidate["file"],
|
|
398
|
+
"priority": candidate["priority"],
|
|
399
|
+
"functions": functions,
|
|
400
|
+
"classes": classes,
|
|
401
|
+
"function_count": len(functions),
|
|
402
|
+
"class_count": len(classes),
|
|
403
|
+
"test_suggestions": self._generate_suggestions(functions, classes),
|
|
404
|
+
},
|
|
405
|
+
)
|
|
406
|
+
except OSError:
|
|
407
|
+
continue
|
|
408
|
+
|
|
409
|
+
input_tokens = len(str(input_data)) // 4
|
|
410
|
+
output_tokens = len(str(analysis)) // 4
|
|
411
|
+
|
|
412
|
+
return (
|
|
413
|
+
{
|
|
414
|
+
"analysis": analysis,
|
|
415
|
+
"total_functions": sum(a["function_count"] for a in analysis),
|
|
416
|
+
"total_classes": sum(a["class_count"] for a in analysis),
|
|
417
|
+
"parse_errors": parse_errors, # Expose errors for debugging
|
|
418
|
+
**input_data,
|
|
419
|
+
},
|
|
420
|
+
input_tokens,
|
|
421
|
+
output_tokens,
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
def _extract_functions(
|
|
425
|
+
self,
|
|
426
|
+
content: str,
|
|
427
|
+
file_path: str = "",
|
|
428
|
+
max_functions: int = 30,
|
|
429
|
+
) -> tuple[list[dict], str | None]:
|
|
430
|
+
"""Extract function definitions from Python code using AST analysis.
|
|
431
|
+
|
|
432
|
+
Args:
|
|
433
|
+
content: Python source code
|
|
434
|
+
file_path: File path for error reporting
|
|
435
|
+
max_functions: Maximum functions to extract (configurable)
|
|
436
|
+
|
|
437
|
+
Returns:
|
|
438
|
+
Tuple of (functions list, error message or None)
|
|
439
|
+
|
|
440
|
+
"""
|
|
441
|
+
analyzer = ASTFunctionAnalyzer()
|
|
442
|
+
functions, _ = analyzer.analyze(content, file_path)
|
|
443
|
+
|
|
444
|
+
result = []
|
|
445
|
+
for sig in functions[:max_functions]:
|
|
446
|
+
if not sig.name.startswith("_") or sig.name.startswith("__"):
|
|
447
|
+
result.append(
|
|
448
|
+
{
|
|
449
|
+
"name": sig.name,
|
|
450
|
+
"params": [(p[0], p[1], p[2]) for p in sig.params],
|
|
451
|
+
"param_names": [p[0] for p in sig.params],
|
|
452
|
+
"is_async": sig.is_async,
|
|
453
|
+
"return_type": sig.return_type,
|
|
454
|
+
"raises": list(sig.raises),
|
|
455
|
+
"has_side_effects": sig.has_side_effects,
|
|
456
|
+
"complexity": sig.complexity,
|
|
457
|
+
"docstring": sig.docstring,
|
|
458
|
+
},
|
|
459
|
+
)
|
|
460
|
+
return result, analyzer.last_error
|
|
461
|
+
|
|
462
|
+
def _extract_classes(
|
|
463
|
+
self,
|
|
464
|
+
content: str,
|
|
465
|
+
file_path: str = "",
|
|
466
|
+
max_classes: int = 15,
|
|
467
|
+
) -> tuple[list[dict], str | None]:
|
|
468
|
+
"""Extract class definitions from Python code using AST analysis.
|
|
469
|
+
|
|
470
|
+
Args:
|
|
471
|
+
content: Python source code
|
|
472
|
+
file_path: File path for error reporting
|
|
473
|
+
max_classes: Maximum classes to extract (configurable)
|
|
474
|
+
|
|
475
|
+
Returns:
|
|
476
|
+
Tuple of (classes list, error message or None)
|
|
477
|
+
|
|
478
|
+
"""
|
|
479
|
+
analyzer = ASTFunctionAnalyzer()
|
|
480
|
+
_, classes = analyzer.analyze(content, file_path)
|
|
481
|
+
|
|
482
|
+
result = []
|
|
483
|
+
for sig in classes[:max_classes]:
|
|
484
|
+
# Skip enums - they don't need traditional class tests
|
|
485
|
+
if sig.is_enum:
|
|
486
|
+
continue
|
|
487
|
+
|
|
488
|
+
methods = [
|
|
489
|
+
{
|
|
490
|
+
"name": m.name,
|
|
491
|
+
"params": [(p[0], p[1], p[2]) for p in m.params],
|
|
492
|
+
"is_async": m.is_async,
|
|
493
|
+
"raises": list(m.raises),
|
|
494
|
+
}
|
|
495
|
+
for m in sig.methods
|
|
496
|
+
if not m.name.startswith("_") or m.name == "__init__"
|
|
497
|
+
]
|
|
498
|
+
result.append(
|
|
499
|
+
{
|
|
500
|
+
"name": sig.name,
|
|
501
|
+
"init_params": [(p[0], p[1], p[2]) for p in sig.init_params],
|
|
502
|
+
"methods": methods,
|
|
503
|
+
"base_classes": sig.base_classes,
|
|
504
|
+
"docstring": sig.docstring,
|
|
505
|
+
"is_dataclass": sig.is_dataclass,
|
|
506
|
+
"required_init_params": sig.required_init_params,
|
|
507
|
+
},
|
|
508
|
+
)
|
|
509
|
+
return result, analyzer.last_error
|
|
510
|
+
|
|
511
|
+
def _generate_suggestions(self, functions: list[dict], classes: list[dict]) -> list[str]:
|
|
512
|
+
"""Generate test suggestions based on code structure."""
|
|
513
|
+
suggestions = []
|
|
514
|
+
|
|
515
|
+
for func in functions[:5]:
|
|
516
|
+
if func["params"]:
|
|
517
|
+
suggestions.append(f"Test {func['name']} with valid inputs")
|
|
518
|
+
suggestions.append(f"Test {func['name']} with edge cases")
|
|
519
|
+
if func["is_async"]:
|
|
520
|
+
suggestions.append(f"Test {func['name']} async behavior")
|
|
521
|
+
|
|
522
|
+
for cls in classes[:3]:
|
|
523
|
+
suggestions.append(f"Test {cls['name']} initialization")
|
|
524
|
+
suggestions.append(f"Test {cls['name']} methods")
|
|
525
|
+
|
|
526
|
+
return suggestions
|
|
527
|
+
|
|
528
|
+
async def _generate(self, input_data: dict, tier: ModelTier) -> tuple[dict, int, int]:
|
|
529
|
+
"""Generate test cases.
|
|
530
|
+
|
|
531
|
+
Creates test code targeting identified functions
|
|
532
|
+
and classes, focusing on edge cases.
|
|
533
|
+
|
|
534
|
+
Uses config from _identify stage for limits:
|
|
535
|
+
max_files_to_generate: Maximum files to generate tests for (default: 15)
|
|
536
|
+
max_functions_to_generate: Maximum functions per file (default: 8)
|
|
537
|
+
max_classes_to_generate: Maximum classes per file (default: 4)
|
|
538
|
+
"""
|
|
539
|
+
# Get config from previous stages or use defaults
|
|
540
|
+
config = input_data.get("config", {})
|
|
541
|
+
max_files_to_generate = config.get("max_files_to_generate", 15)
|
|
542
|
+
max_functions_to_generate = config.get("max_functions_to_generate", 8)
|
|
543
|
+
max_classes_to_generate = config.get("max_classes_to_generate", 4)
|
|
544
|
+
|
|
545
|
+
analysis = input_data.get("analysis", [])
|
|
546
|
+
generated_tests: list[dict] = []
|
|
547
|
+
|
|
548
|
+
for item in analysis[:max_files_to_generate]:
|
|
549
|
+
file_path = item["file"]
|
|
550
|
+
module_name = Path(file_path).stem
|
|
551
|
+
|
|
552
|
+
tests = []
|
|
553
|
+
for func in item.get("functions", [])[:max_functions_to_generate]:
|
|
554
|
+
test_code = generate_test_for_function(module_name, func)
|
|
555
|
+
tests.append(
|
|
556
|
+
{
|
|
557
|
+
"target": func["name"],
|
|
558
|
+
"type": "function",
|
|
559
|
+
"code": test_code,
|
|
560
|
+
},
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
for cls in item.get("classes", [])[:max_classes_to_generate]:
|
|
564
|
+
test_code = generate_test_for_class(module_name, cls)
|
|
565
|
+
tests.append(
|
|
566
|
+
{
|
|
567
|
+
"target": cls["name"],
|
|
568
|
+
"type": "class",
|
|
569
|
+
"code": test_code,
|
|
570
|
+
},
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
if tests:
|
|
574
|
+
generated_tests.append(
|
|
575
|
+
{
|
|
576
|
+
"source_file": file_path,
|
|
577
|
+
"test_file": f"test_{module_name}.py",
|
|
578
|
+
"tests": tests,
|
|
579
|
+
"test_count": len(tests),
|
|
580
|
+
},
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
self._test_count = sum(t["test_count"] for t in generated_tests)
|
|
584
|
+
|
|
585
|
+
# Write tests to files if enabled (via input_data or instance config)
|
|
586
|
+
write_tests = input_data.get("write_tests", self.write_tests)
|
|
587
|
+
output_dir = input_data.get("output_dir", self.output_dir)
|
|
588
|
+
written_files: list[str] = []
|
|
589
|
+
|
|
590
|
+
if write_tests and generated_tests:
|
|
591
|
+
output_path = Path(output_dir)
|
|
592
|
+
output_path.mkdir(parents=True, exist_ok=True)
|
|
593
|
+
|
|
594
|
+
for test_item in generated_tests:
|
|
595
|
+
test_filename = test_item["test_file"]
|
|
596
|
+
test_file_path = output_path / test_filename
|
|
597
|
+
|
|
598
|
+
# Combine all test code for this file
|
|
599
|
+
combined_code = []
|
|
600
|
+
imports_added = set()
|
|
601
|
+
|
|
602
|
+
for test in test_item["tests"]:
|
|
603
|
+
code = test["code"]
|
|
604
|
+
# Extract and dedupe imports
|
|
605
|
+
for line in code.split("\n"):
|
|
606
|
+
if line.startswith("import ") or line.startswith("from "):
|
|
607
|
+
if line not in imports_added:
|
|
608
|
+
imports_added.add(line)
|
|
609
|
+
elif line.strip():
|
|
610
|
+
combined_code.append(line)
|
|
611
|
+
|
|
612
|
+
# Write the combined test file
|
|
613
|
+
final_code = "\n".join(sorted(imports_added)) + "\n\n" + "\n".join(combined_code)
|
|
614
|
+
test_file_path.write_text(final_code)
|
|
615
|
+
written_files.append(str(test_file_path))
|
|
616
|
+
test_item["written_to"] = str(test_file_path)
|
|
617
|
+
|
|
618
|
+
input_tokens = len(str(input_data)) // 4
|
|
619
|
+
output_tokens = sum(len(str(t)) for t in generated_tests) // 4
|
|
620
|
+
|
|
621
|
+
return (
|
|
622
|
+
{
|
|
623
|
+
"generated_tests": generated_tests,
|
|
624
|
+
"total_tests_generated": self._test_count,
|
|
625
|
+
"written_files": written_files,
|
|
626
|
+
"tests_written": len(written_files) > 0,
|
|
627
|
+
**input_data,
|
|
628
|
+
},
|
|
629
|
+
input_tokens,
|
|
630
|
+
output_tokens,
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
def main():
|
|
637
|
+
"""CLI entry point for test generation workflow."""
|
|
638
|
+
import asyncio
|
|
639
|
+
|
|
640
|
+
async def run():
|
|
641
|
+
workflow = TestGenerationWorkflow()
|
|
642
|
+
result = await workflow.execute(path=".", file_types=[".py"])
|
|
643
|
+
|
|
644
|
+
print("\nTest Generation Results")
|
|
645
|
+
print("=" * 50)
|
|
646
|
+
print(f"Provider: {result.provider}")
|
|
647
|
+
print(f"Success: {result.success}")
|
|
648
|
+
print(f"Tests Generated: {result.final_output.get('total_tests', 0)}")
|
|
649
|
+
print("\nCost Report:")
|
|
650
|
+
print(f" Total Cost: ${result.cost_report.total_cost:.4f}")
|
|
651
|
+
savings = result.cost_report.savings
|
|
652
|
+
pct = result.cost_report.savings_percent
|
|
653
|
+
print(f" Savings: ${savings:.4f} ({pct:.1f}%)")
|
|
654
|
+
|
|
655
|
+
asyncio.run(run())
|