@jaguilar87/gaia-ops 4.4.0 → 4.7.2
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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +12 -3
- package/ARCHITECTURE.md +9 -8
- package/CHANGELOG.md +34 -0
- package/README.md +43 -11
- package/agents/terraform-architect.md +1 -1
- package/bin/README.md +2 -2
- package/bin/gaia-doctor.js +18 -5
- package/bin/gaia-history.js +0 -1
- package/bin/gaia-metrics.js +2 -2
- package/bin/gaia-scan.py +23 -1
- package/bin/gaia-update.js +346 -54
- package/bin/pre-publish-validate.js +33 -10
- package/commands/gaia.md +37 -0
- package/config/README.md +3 -9
- package/config/context-contracts.json +47 -15
- package/config/surface-routing.json +9 -1
- package/dist/gaia-ops/.claude-plugin/plugin.json +22 -0
- package/dist/gaia-ops/agents/cloud-troubleshooter.md +73 -0
- package/dist/gaia-ops/agents/devops-developer.md +57 -0
- package/dist/gaia-ops/agents/gaia-system.md +58 -0
- package/dist/gaia-ops/agents/gitops-operator.md +60 -0
- package/dist/gaia-ops/agents/speckit-planner.md +71 -0
- package/dist/gaia-ops/agents/terraform-architect.md +60 -0
- package/dist/gaia-ops/commands/gaia.md +37 -0
- package/dist/gaia-ops/config/README.md +58 -0
- package/dist/gaia-ops/config/cloud/aws.json +140 -0
- package/dist/gaia-ops/config/cloud/gcp.json +145 -0
- package/dist/gaia-ops/config/context-contracts.json +131 -0
- package/dist/gaia-ops/config/git_standards.json +72 -0
- package/dist/gaia-ops/config/surface-routing.json +197 -0
- package/dist/gaia-ops/config/universal-rules.json +10 -0
- package/dist/gaia-ops/hooks/adapters/__init__.py +52 -0
- package/dist/gaia-ops/hooks/adapters/base.py +219 -0
- package/dist/gaia-ops/hooks/adapters/channel.py +17 -0
- package/dist/gaia-ops/hooks/adapters/claude_code.py +1477 -0
- package/dist/gaia-ops/hooks/adapters/types.py +194 -0
- package/dist/gaia-ops/hooks/adapters/utils.py +25 -0
- package/dist/gaia-ops/hooks/hooks.json +126 -0
- package/dist/gaia-ops/hooks/modules/__init__.py +15 -0
- package/dist/gaia-ops/hooks/modules/agents/__init__.py +29 -0
- package/dist/gaia-ops/hooks/modules/agents/contract_validator.py +647 -0
- package/dist/gaia-ops/hooks/modules/agents/response_contract.py +496 -0
- package/dist/gaia-ops/hooks/modules/agents/skill_injection_verifier.py +124 -0
- package/dist/gaia-ops/hooks/modules/agents/task_info_builder.py +74 -0
- package/dist/gaia-ops/hooks/modules/agents/transcript_analyzer.py +458 -0
- package/dist/gaia-ops/hooks/modules/agents/transcript_reader.py +152 -0
- package/dist/gaia-ops/hooks/modules/audit/__init__.py +28 -0
- package/dist/gaia-ops/hooks/modules/audit/event_detector.py +168 -0
- package/dist/gaia-ops/hooks/modules/audit/logger.py +131 -0
- package/dist/gaia-ops/hooks/modules/audit/metrics.py +134 -0
- package/dist/gaia-ops/hooks/modules/audit/workflow_auditor.py +576 -0
- package/dist/gaia-ops/hooks/modules/audit/workflow_recorder.py +296 -0
- package/dist/gaia-ops/hooks/modules/context/__init__.py +11 -0
- package/dist/gaia-ops/hooks/modules/context/anchor_tracker.py +317 -0
- package/dist/gaia-ops/hooks/modules/context/compact_context_builder.py +215 -0
- package/dist/gaia-ops/hooks/modules/context/context_cache.py +129 -0
- package/dist/gaia-ops/hooks/modules/context/context_freshness.py +145 -0
- package/dist/gaia-ops/hooks/modules/context/context_injector.py +427 -0
- package/dist/gaia-ops/hooks/modules/context/context_writer.py +518 -0
- package/dist/gaia-ops/hooks/modules/context/contracts_loader.py +161 -0
- package/dist/gaia-ops/hooks/modules/core/__init__.py +40 -0
- package/dist/gaia-ops/hooks/modules/core/hook_entry.py +78 -0
- package/dist/gaia-ops/hooks/modules/core/paths.py +160 -0
- package/dist/gaia-ops/hooks/modules/core/plugin_mode.py +149 -0
- package/dist/gaia-ops/hooks/modules/core/plugin_setup.py +558 -0
- package/dist/gaia-ops/hooks/modules/core/state.py +179 -0
- package/dist/gaia-ops/hooks/modules/core/stdin.py +24 -0
- package/dist/gaia-ops/hooks/modules/events/__init__.py +1 -0
- package/dist/gaia-ops/hooks/modules/events/event_writer.py +210 -0
- package/dist/gaia-ops/hooks/modules/identity/__init__.py +0 -0
- package/dist/gaia-ops/hooks/modules/identity/identity_provider.py +21 -0
- package/dist/gaia-ops/hooks/modules/identity/ops_identity.py +34 -0
- package/dist/gaia-ops/hooks/modules/identity/security_identity.py +10 -0
- package/dist/gaia-ops/hooks/modules/memory/__init__.py +8 -0
- package/dist/gaia-ops/hooks/modules/memory/episode_writer.py +227 -0
- package/dist/gaia-ops/hooks/modules/orchestrator/__init__.py +1 -0
- package/dist/gaia-ops/hooks/modules/orchestrator/delegate_mode.py +128 -0
- package/dist/gaia-ops/hooks/modules/scanning/__init__.py +8 -0
- package/dist/gaia-ops/hooks/modules/scanning/scan_trigger.py +84 -0
- package/dist/gaia-ops/hooks/modules/security/__init__.py +89 -0
- package/dist/gaia-ops/hooks/modules/security/approval_cleanup.py +87 -0
- package/dist/gaia-ops/hooks/modules/security/approval_constants.py +23 -0
- package/dist/gaia-ops/hooks/modules/security/approval_grants.py +912 -0
- package/dist/gaia-ops/hooks/modules/security/approval_messages.py +71 -0
- package/dist/gaia-ops/hooks/modules/security/approval_scopes.py +153 -0
- package/dist/gaia-ops/hooks/modules/security/blocked_commands.py +584 -0
- package/dist/gaia-ops/hooks/modules/security/blocked_message_formatter.py +86 -0
- package/dist/gaia-ops/hooks/modules/security/command_semantics.py +130 -0
- package/dist/gaia-ops/hooks/modules/security/gitops_validator.py +179 -0
- package/dist/gaia-ops/hooks/modules/security/mutative_verbs.py +850 -0
- package/dist/gaia-ops/hooks/modules/security/prompt_validator.py +40 -0
- package/dist/gaia-ops/hooks/modules/security/tiers.py +196 -0
- package/dist/gaia-ops/hooks/modules/session/__init__.py +10 -0
- package/dist/gaia-ops/hooks/modules/session/session_context_writer.py +100 -0
- package/dist/gaia-ops/hooks/modules/session/session_event_injector.py +158 -0
- package/dist/gaia-ops/hooks/modules/session/session_manager.py +31 -0
- package/dist/gaia-ops/hooks/modules/tools/__init__.py +25 -0
- package/dist/gaia-ops/hooks/modules/tools/bash_validator.py +708 -0
- package/dist/gaia-ops/hooks/modules/tools/cloud_pipe_validator.py +181 -0
- package/dist/gaia-ops/hooks/modules/tools/hook_response.py +55 -0
- package/dist/gaia-ops/hooks/modules/tools/shell_parser.py +227 -0
- package/dist/gaia-ops/hooks/modules/tools/task_validator.py +283 -0
- package/dist/gaia-ops/hooks/modules/validation/__init__.py +23 -0
- package/dist/gaia-ops/hooks/modules/validation/commit_validator.py +380 -0
- package/dist/gaia-ops/hooks/post_compact.py +43 -0
- package/dist/gaia-ops/hooks/post_tool_use.py +54 -0
- package/dist/gaia-ops/hooks/pre_tool_use.py +383 -0
- package/dist/gaia-ops/hooks/session_start.py +69 -0
- package/dist/gaia-ops/hooks/stop_hook.py +69 -0
- package/dist/gaia-ops/hooks/subagent_start.py +71 -0
- package/dist/gaia-ops/hooks/subagent_stop.py +288 -0
- package/dist/gaia-ops/hooks/task_completed.py +70 -0
- package/dist/gaia-ops/hooks/user_prompt_submit.py +177 -0
- package/dist/gaia-ops/settings.json +72 -0
- package/dist/gaia-ops/skills/README.md +109 -0
- package/dist/gaia-ops/skills/agent-protocol/SKILL.md +105 -0
- package/dist/gaia-ops/skills/agent-protocol/examples.md +170 -0
- package/dist/gaia-ops/skills/agent-response/SKILL.md +53 -0
- package/dist/gaia-ops/skills/approval/SKILL.md +85 -0
- package/dist/gaia-ops/skills/approval/examples.md +140 -0
- package/dist/gaia-ops/skills/approval/reference.md +57 -0
- package/dist/gaia-ops/skills/command-execution/SKILL.md +64 -0
- package/dist/gaia-ops/skills/command-execution/reference.md +83 -0
- package/dist/gaia-ops/skills/context-updater/SKILL.md +76 -0
- package/dist/gaia-ops/skills/context-updater/examples.md +71 -0
- package/dist/gaia-ops/skills/developer-patterns/SKILL.md +93 -0
- package/dist/gaia-ops/skills/developer-patterns/reference.md +112 -0
- package/dist/gaia-ops/skills/execution/SKILL.md +66 -0
- package/dist/gaia-ops/skills/fast-queries/SKILL.md +47 -0
- package/dist/gaia-ops/skills/gaia-patterns/SKILL.md +92 -0
- package/dist/gaia-ops/skills/gaia-patterns/reference.md +22 -0
- package/dist/gaia-ops/skills/git-conventions/SKILL.md +48 -0
- package/dist/gaia-ops/skills/gitops-patterns/SKILL.md +73 -0
- package/dist/gaia-ops/skills/gitops-patterns/reference.md +183 -0
- package/dist/gaia-ops/skills/investigation/SKILL.md +77 -0
- package/dist/gaia-ops/skills/orchestrator-approval/SKILL.md +64 -0
- package/dist/gaia-ops/skills/reference.md +134 -0
- package/dist/gaia-ops/skills/security-tiers/SKILL.md +61 -0
- package/dist/gaia-ops/skills/security-tiers/destructive-commands-reference.md +623 -0
- package/dist/gaia-ops/skills/security-tiers/reference.md +39 -0
- package/dist/gaia-ops/skills/skill-creation/SKILL.md +119 -0
- package/dist/gaia-ops/skills/specification/SKILL.md +186 -0
- package/dist/gaia-ops/skills/speckit-workflow/SKILL.md +165 -0
- package/dist/gaia-ops/skills/speckit-workflow/reference.md +117 -0
- package/dist/gaia-ops/skills/terraform-patterns/SKILL.md +63 -0
- package/dist/gaia-ops/skills/terraform-patterns/reference.md +93 -0
- package/dist/gaia-ops/speckit/README.md +516 -0
- package/dist/gaia-ops/speckit/scripts/.gitkeep +0 -0
- package/dist/gaia-ops/speckit/templates/adr-template.md +118 -0
- package/dist/gaia-ops/speckit/templates/agent-file-template.md +23 -0
- package/dist/gaia-ops/speckit/templates/plan-template.md +227 -0
- package/dist/gaia-ops/speckit/templates/spec-template.md +140 -0
- package/dist/gaia-ops/speckit/templates/tasks-template.md +257 -0
- package/dist/gaia-ops/tools/context/README.md +132 -0
- package/dist/gaia-ops/tools/context/__init__.py +42 -0
- package/dist/gaia-ops/tools/context/_paths.py +20 -0
- package/dist/gaia-ops/tools/context/context_provider.py +476 -0
- package/dist/gaia-ops/tools/context/context_section_reader.py +330 -0
- package/dist/gaia-ops/tools/context/deep_merge.py +159 -0
- package/dist/gaia-ops/tools/context/pending_updates.py +760 -0
- package/dist/gaia-ops/tools/context/surface_router.py +278 -0
- package/dist/gaia-ops/tools/fast-queries/README.md +65 -0
- package/dist/gaia-ops/tools/fast-queries/__init__.py +30 -0
- package/dist/gaia-ops/tools/fast-queries/appservices/quicktriage_devops_developer.sh +75 -0
- package/dist/gaia-ops/tools/fast-queries/cloud/aws/quicktriage_aws_troubleshooter.sh +32 -0
- package/dist/gaia-ops/tools/fast-queries/cloud/gcp/quicktriage_gcp_troubleshooter.sh +88 -0
- package/dist/gaia-ops/tools/fast-queries/gitops/quicktriage_gitops_operator.sh +48 -0
- package/dist/gaia-ops/tools/fast-queries/run_triage.sh +59 -0
- package/dist/gaia-ops/tools/fast-queries/terraform/quicktriage_terraform_architect.sh +80 -0
- package/dist/gaia-ops/tools/gaia_simulator/__init__.py +33 -0
- package/dist/gaia-ops/tools/gaia_simulator/cli.py +354 -0
- package/dist/gaia-ops/tools/gaia_simulator/extractor.py +457 -0
- package/dist/gaia-ops/tools/gaia_simulator/reporter.py +258 -0
- package/dist/gaia-ops/tools/gaia_simulator/routing_simulator.py +334 -0
- package/dist/gaia-ops/tools/gaia_simulator/runner.py +539 -0
- package/dist/gaia-ops/tools/gaia_simulator/skills_mapper.py +262 -0
- package/dist/gaia-ops/tools/memory/README.md +0 -0
- package/dist/gaia-ops/tools/memory/__init__.py +20 -0
- package/dist/gaia-ops/tools/memory/episodic.py +1196 -0
- package/dist/gaia-ops/tools/persist_transcript_analysis.py +85 -0
- package/dist/gaia-ops/tools/review/__init__.py +1 -0
- package/dist/gaia-ops/tools/review/review_engine.py +157 -0
- package/dist/gaia-ops/tools/scan/__init__.py +35 -0
- package/dist/gaia-ops/tools/scan/config.py +247 -0
- package/dist/gaia-ops/tools/scan/merge.py +212 -0
- package/dist/gaia-ops/tools/scan/orchestrator.py +549 -0
- package/dist/gaia-ops/tools/scan/registry.py +127 -0
- package/dist/gaia-ops/tools/scan/scanners/__init__.py +18 -0
- package/dist/gaia-ops/tools/scan/scanners/base.py +137 -0
- package/dist/gaia-ops/tools/scan/scanners/environment.py +324 -0
- package/dist/gaia-ops/tools/scan/scanners/git.py +570 -0
- package/dist/gaia-ops/tools/scan/scanners/infrastructure.py +875 -0
- package/dist/gaia-ops/tools/scan/scanners/orchestration.py +600 -0
- package/dist/gaia-ops/tools/scan/scanners/stack.py +1085 -0
- package/dist/gaia-ops/tools/scan/scanners/tools.py +260 -0
- package/dist/gaia-ops/tools/scan/setup.py +753 -0
- package/dist/gaia-ops/tools/scan/tests/__init__.py +1 -0
- package/dist/gaia-ops/tools/scan/tests/conftest.py +796 -0
- package/dist/gaia-ops/tools/scan/tests/test_environment.py +323 -0
- package/dist/gaia-ops/tools/scan/tests/test_git.py +419 -0
- package/dist/gaia-ops/tools/scan/tests/test_infrastructure.py +382 -0
- package/dist/gaia-ops/tools/scan/tests/test_integration.py +920 -0
- package/dist/gaia-ops/tools/scan/tests/test_merge.py +269 -0
- package/dist/gaia-ops/tools/scan/tests/test_orchestration.py +304 -0
- package/dist/gaia-ops/tools/scan/tests/test_stack.py +604 -0
- package/dist/gaia-ops/tools/scan/tests/test_tools.py +349 -0
- package/dist/gaia-ops/tools/scan/ui.py +624 -0
- package/dist/gaia-ops/tools/scan/verify.py +266 -0
- package/dist/gaia-ops/tools/scan/walk.py +118 -0
- package/dist/gaia-ops/tools/scan/workspace.py +85 -0
- package/dist/gaia-ops/tools/validation/README.md +244 -0
- package/dist/gaia-ops/tools/validation/__init__.py +17 -0
- package/dist/gaia-ops/tools/validation/approval_gate.py +321 -0
- package/dist/gaia-ops/tools/validation/validate_skills.py +189 -0
- package/dist/gaia-security/.claude-plugin/plugin.json +22 -0
- package/dist/gaia-security/config/universal-rules.json +10 -0
- package/dist/gaia-security/hooks/adapters/__init__.py +52 -0
- package/dist/gaia-security/hooks/adapters/base.py +219 -0
- package/dist/gaia-security/hooks/adapters/channel.py +17 -0
- package/dist/gaia-security/hooks/adapters/claude_code.py +1477 -0
- package/dist/gaia-security/hooks/adapters/types.py +194 -0
- package/dist/gaia-security/hooks/adapters/utils.py +25 -0
- package/dist/gaia-security/hooks/hooks.json +57 -0
- package/dist/gaia-security/hooks/modules/__init__.py +15 -0
- package/dist/gaia-security/hooks/modules/agents/__init__.py +29 -0
- package/dist/gaia-security/hooks/modules/agents/contract_validator.py +647 -0
- package/dist/gaia-security/hooks/modules/agents/response_contract.py +496 -0
- package/dist/gaia-security/hooks/modules/agents/skill_injection_verifier.py +124 -0
- package/dist/gaia-security/hooks/modules/agents/task_info_builder.py +74 -0
- package/dist/gaia-security/hooks/modules/agents/transcript_analyzer.py +458 -0
- package/dist/gaia-security/hooks/modules/agents/transcript_reader.py +152 -0
- package/dist/gaia-security/hooks/modules/audit/__init__.py +28 -0
- package/dist/gaia-security/hooks/modules/audit/event_detector.py +168 -0
- package/dist/gaia-security/hooks/modules/audit/logger.py +131 -0
- package/dist/gaia-security/hooks/modules/audit/metrics.py +134 -0
- package/dist/gaia-security/hooks/modules/audit/workflow_auditor.py +576 -0
- package/dist/gaia-security/hooks/modules/audit/workflow_recorder.py +296 -0
- package/dist/gaia-security/hooks/modules/context/__init__.py +11 -0
- package/dist/gaia-security/hooks/modules/context/anchor_tracker.py +317 -0
- package/dist/gaia-security/hooks/modules/context/compact_context_builder.py +215 -0
- package/dist/gaia-security/hooks/modules/context/context_cache.py +129 -0
- package/dist/gaia-security/hooks/modules/context/context_freshness.py +145 -0
- package/dist/gaia-security/hooks/modules/context/context_injector.py +427 -0
- package/dist/gaia-security/hooks/modules/context/context_writer.py +518 -0
- package/dist/gaia-security/hooks/modules/context/contracts_loader.py +161 -0
- package/dist/gaia-security/hooks/modules/core/__init__.py +40 -0
- package/dist/gaia-security/hooks/modules/core/hook_entry.py +78 -0
- package/dist/gaia-security/hooks/modules/core/paths.py +160 -0
- package/dist/gaia-security/hooks/modules/core/plugin_mode.py +149 -0
- package/dist/gaia-security/hooks/modules/core/plugin_setup.py +558 -0
- package/dist/gaia-security/hooks/modules/core/state.py +179 -0
- package/dist/gaia-security/hooks/modules/core/stdin.py +24 -0
- package/dist/gaia-security/hooks/modules/events/__init__.py +1 -0
- package/dist/gaia-security/hooks/modules/events/event_writer.py +210 -0
- package/dist/gaia-security/hooks/modules/identity/__init__.py +0 -0
- package/dist/gaia-security/hooks/modules/identity/identity_provider.py +21 -0
- package/dist/gaia-security/hooks/modules/identity/ops_identity.py +34 -0
- package/dist/gaia-security/hooks/modules/identity/security_identity.py +10 -0
- package/dist/gaia-security/hooks/modules/memory/__init__.py +8 -0
- package/dist/gaia-security/hooks/modules/memory/episode_writer.py +227 -0
- package/dist/gaia-security/hooks/modules/orchestrator/__init__.py +1 -0
- package/dist/gaia-security/hooks/modules/orchestrator/delegate_mode.py +128 -0
- package/dist/gaia-security/hooks/modules/scanning/__init__.py +8 -0
- package/dist/gaia-security/hooks/modules/scanning/scan_trigger.py +84 -0
- package/dist/gaia-security/hooks/modules/security/__init__.py +89 -0
- package/dist/gaia-security/hooks/modules/security/approval_cleanup.py +87 -0
- package/dist/gaia-security/hooks/modules/security/approval_constants.py +23 -0
- package/dist/gaia-security/hooks/modules/security/approval_grants.py +912 -0
- package/dist/gaia-security/hooks/modules/security/approval_messages.py +71 -0
- package/dist/gaia-security/hooks/modules/security/approval_scopes.py +153 -0
- package/dist/gaia-security/hooks/modules/security/blocked_commands.py +584 -0
- package/dist/gaia-security/hooks/modules/security/blocked_message_formatter.py +86 -0
- package/dist/gaia-security/hooks/modules/security/command_semantics.py +130 -0
- package/dist/gaia-security/hooks/modules/security/gitops_validator.py +179 -0
- package/dist/gaia-security/hooks/modules/security/mutative_verbs.py +850 -0
- package/dist/gaia-security/hooks/modules/security/prompt_validator.py +40 -0
- package/dist/gaia-security/hooks/modules/security/tiers.py +196 -0
- package/dist/gaia-security/hooks/modules/session/__init__.py +10 -0
- package/dist/gaia-security/hooks/modules/session/session_context_writer.py +100 -0
- package/dist/gaia-security/hooks/modules/session/session_event_injector.py +158 -0
- package/dist/gaia-security/hooks/modules/session/session_manager.py +31 -0
- package/dist/gaia-security/hooks/modules/tools/__init__.py +25 -0
- package/dist/gaia-security/hooks/modules/tools/bash_validator.py +708 -0
- package/dist/gaia-security/hooks/modules/tools/cloud_pipe_validator.py +181 -0
- package/dist/gaia-security/hooks/modules/tools/hook_response.py +55 -0
- package/dist/gaia-security/hooks/modules/tools/shell_parser.py +227 -0
- package/dist/gaia-security/hooks/modules/tools/task_validator.py +283 -0
- package/dist/gaia-security/hooks/modules/validation/__init__.py +23 -0
- package/dist/gaia-security/hooks/modules/validation/commit_validator.py +380 -0
- package/dist/gaia-security/hooks/post_tool_use.py +54 -0
- package/dist/gaia-security/hooks/pre_tool_use.py +383 -0
- package/dist/gaia-security/hooks/session_start.py +69 -0
- package/dist/gaia-security/hooks/stop_hook.py +69 -0
- package/dist/gaia-security/hooks/user_prompt_submit.py +177 -0
- package/dist/gaia-security/settings.json +58 -0
- package/git-hooks/commit-msg +41 -0
- package/hooks/README.md +8 -6
- package/hooks/adapters/channel.py +0 -25
- package/hooks/adapters/claude_code.py +364 -125
- package/hooks/elicitation_result.py +132 -0
- package/hooks/hooks.json +10 -1
- package/hooks/modules/README.md +3 -2
- package/hooks/modules/agents/contract_validator.py +3 -51
- package/hooks/modules/agents/response_contract.py +4 -8
- package/hooks/modules/agents/transcript_reader.py +4 -5
- package/hooks/modules/audit/__init__.py +4 -6
- package/hooks/modules/audit/event_detector.py +0 -2
- package/hooks/modules/audit/metrics.py +108 -187
- package/hooks/modules/audit/workflow_auditor.py +0 -4
- package/hooks/modules/audit/workflow_recorder.py +0 -5
- package/hooks/modules/context/compact_context_builder.py +1 -0
- package/hooks/modules/context/context_cache.py +129 -0
- package/hooks/modules/context/context_injector.py +18 -40
- package/hooks/modules/context/context_writer.py +1 -25
- package/hooks/modules/context/contracts_loader.py +7 -10
- package/hooks/modules/core/hook_entry.py +1 -0
- package/hooks/modules/core/paths.py +12 -13
- package/hooks/modules/core/plugin_mode.py +74 -4
- package/hooks/modules/core/plugin_setup.py +395 -23
- package/hooks/modules/events/__init__.py +1 -0
- package/hooks/modules/events/event_writer.py +210 -0
- package/hooks/modules/identity/ops_identity.py +18 -27
- package/hooks/modules/memory/episode_writer.py +1 -6
- package/hooks/modules/orchestrator/__init__.py +1 -0
- package/hooks/modules/orchestrator/delegate_mode.py +128 -0
- package/hooks/modules/security/__init__.py +2 -4
- package/hooks/modules/security/approval_constants.py +5 -1
- package/hooks/modules/security/approval_grants.py +189 -6
- package/hooks/modules/security/approval_messages.py +9 -21
- package/hooks/modules/security/blocked_commands.py +98 -34
- package/hooks/modules/security/command_semantics.py +0 -4
- package/hooks/modules/security/gitops_validator.py +1 -11
- package/hooks/modules/security/mutative_verbs.py +179 -38
- package/hooks/modules/security/tiers.py +1 -19
- package/hooks/modules/session/session_event_injector.py +1 -25
- package/hooks/modules/tools/bash_validator.py +310 -94
- package/hooks/modules/tools/shell_parser.py +0 -1
- package/hooks/modules/tools/task_validator.py +9 -29
- package/hooks/post_tool_use.py +0 -72
- package/hooks/pre_tool_use.py +42 -102
- package/hooks/session_start.py +4 -2
- package/hooks/subagent_start.py +6 -2
- package/hooks/subagent_stop.py +1 -13
- package/hooks/user_prompt_submit.py +119 -37
- package/index.js +1 -1
- package/package.json +5 -3
- package/skills/README.md +3 -5
- package/skills/agent-protocol/SKILL.md +17 -16
- package/skills/agent-protocol/examples.md +6 -6
- package/skills/agent-response/SKILL.md +11 -14
- package/skills/approval/SKILL.md +28 -13
- package/skills/approval/reference.md +2 -2
- package/skills/execution/SKILL.md +1 -1
- package/skills/gaia-patterns/SKILL.md +2 -3
- package/skills/orchestrator-approval/SKILL.md +22 -50
- package/skills/security-tiers/SKILL.md +1 -1
- package/templates/README.md +9 -9
- package/templates/managed-settings.template.json +43 -0
- package/tools/gaia_simulator/runner.py +34 -1
- package/tools/scan/orchestrator.py +13 -0
- package/tools/scan/scanners/base.py +8 -0
- package/tools/scan/scanners/git.py +78 -0
- package/tools/scan/scanners/infrastructure.py +65 -0
- package/tools/scan/scanners/stack.py +110 -0
- package/tools/scan/setup.py +120 -13
- package/tools/scan/workspace.py +85 -0
- package/config/context-contracts.aws.json +0 -42
- package/config/context-contracts.gcp.json +0 -39
- package/skills/project-dispatch/SKILL.md +0 -34
- package/templates/settings.template.json +0 -226
|
@@ -0,0 +1,920 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Integration tests for the full scan pipeline (M6: T034-T038).
|
|
3
|
+
|
|
4
|
+
Tests the ScanOrchestrator end-to-end with realistic project fixtures,
|
|
5
|
+
verifying all 6 scanners produce correct v2 sections and agent-enriched
|
|
6
|
+
data is preserved. Backward-compat sections are no longer produced.
|
|
7
|
+
|
|
8
|
+
Tasks:
|
|
9
|
+
T034: Full scan on mock DevOps project
|
|
10
|
+
T035: Scan on minimal project
|
|
11
|
+
T036: Idempotency test
|
|
12
|
+
T037: Scanner failure isolation
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import copy
|
|
16
|
+
import json
|
|
17
|
+
import textwrap
|
|
18
|
+
import time
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any, Dict
|
|
21
|
+
from unittest.mock import patch
|
|
22
|
+
|
|
23
|
+
import pytest
|
|
24
|
+
|
|
25
|
+
from tools.scan.config import ScanConfig
|
|
26
|
+
from tools.scan.merge import AGENT_ENRICHED_SECTIONS
|
|
27
|
+
from tools.scan.orchestrator import ScanOrchestrator, ScanOutput
|
|
28
|
+
from tools.scan.registry import ScannerRegistry
|
|
29
|
+
from tools.scan.scanners.base import BaseScanner, ScanResult
|
|
30
|
+
from tools.scan.tests.conftest import create_git_dir
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
# Fixtures
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@pytest.fixture
|
|
39
|
+
def full_devops_project(tmp_path: Path) -> Path:
|
|
40
|
+
"""Create a realistic multi-language DevOps project fixture.
|
|
41
|
+
|
|
42
|
+
Includes:
|
|
43
|
+
- package.json (Node.js with NestJS deps)
|
|
44
|
+
- pyproject.toml (Python)
|
|
45
|
+
- .tf files with GCP provider
|
|
46
|
+
- Dockerfile, docker-compose.yml
|
|
47
|
+
- .github/workflows/ directory
|
|
48
|
+
- .git/ directory with GitHub remote
|
|
49
|
+
- K8s manifests
|
|
50
|
+
- Helm Chart.yaml
|
|
51
|
+
- Flux kustomization
|
|
52
|
+
"""
|
|
53
|
+
# -- Node.js / NestJS application --
|
|
54
|
+
pkg = {
|
|
55
|
+
"name": "devops-integration-app",
|
|
56
|
+
"version": "2.0.0",
|
|
57
|
+
"description": "Integration test DevOps application",
|
|
58
|
+
"dependencies": {
|
|
59
|
+
"express": "^4.18.0",
|
|
60
|
+
"@nestjs/core": "^10.0.0",
|
|
61
|
+
"@nestjs/common": "^10.0.0",
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"typescript": "^5.0.0",
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
(tmp_path / "package.json").write_text(json.dumps(pkg, indent=2))
|
|
68
|
+
(tmp_path / "package-lock.json").write_text("{}")
|
|
69
|
+
(tmp_path / "tsconfig.json").write_text('{"compilerOptions": {"strict": true}}')
|
|
70
|
+
|
|
71
|
+
# -- Python project --
|
|
72
|
+
pyproject = textwrap.dedent("""\
|
|
73
|
+
[project]
|
|
74
|
+
name = "devops-backend"
|
|
75
|
+
version = "1.0.0"
|
|
76
|
+
description = "Backend service"
|
|
77
|
+
dependencies = [
|
|
78
|
+
"fastapi>=0.100.0",
|
|
79
|
+
]
|
|
80
|
+
""")
|
|
81
|
+
backend_dir = tmp_path / "backend"
|
|
82
|
+
backend_dir.mkdir()
|
|
83
|
+
(backend_dir / "pyproject.toml").write_text(pyproject)
|
|
84
|
+
(backend_dir / "requirements.txt").write_text("fastapi>=0.100.0\nuvicorn>=0.23.0\n")
|
|
85
|
+
|
|
86
|
+
# -- Terraform with GCP provider --
|
|
87
|
+
tf_dir = tmp_path / "terraform"
|
|
88
|
+
tf_dir.mkdir()
|
|
89
|
+
(tf_dir / "main.tf").write_text(textwrap.dedent("""\
|
|
90
|
+
provider "google" {
|
|
91
|
+
project = "my-gcp-project"
|
|
92
|
+
region = "us-central1"
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
resource "google_compute_instance" "default" {
|
|
96
|
+
name = "test"
|
|
97
|
+
machine_type = "e2-medium"
|
|
98
|
+
}
|
|
99
|
+
"""))
|
|
100
|
+
(tf_dir / "variables.tf").write_text(textwrap.dedent("""\
|
|
101
|
+
variable "project_id" {
|
|
102
|
+
type = string
|
|
103
|
+
}
|
|
104
|
+
"""))
|
|
105
|
+
|
|
106
|
+
# -- Container files --
|
|
107
|
+
(tmp_path / "Dockerfile").write_text("FROM node:20-alpine\nWORKDIR /app\nCOPY . .\n")
|
|
108
|
+
(tmp_path / "docker-compose.yml").write_text(textwrap.dedent("""\
|
|
109
|
+
version: "3.8"
|
|
110
|
+
services:
|
|
111
|
+
app:
|
|
112
|
+
build: .
|
|
113
|
+
ports:
|
|
114
|
+
- "3000:3000"
|
|
115
|
+
"""))
|
|
116
|
+
|
|
117
|
+
# -- GitHub Actions CI --
|
|
118
|
+
workflows_dir = tmp_path / ".github" / "workflows"
|
|
119
|
+
workflows_dir.mkdir(parents=True)
|
|
120
|
+
(workflows_dir / "ci.yml").write_text(textwrap.dedent("""\
|
|
121
|
+
name: CI
|
|
122
|
+
on: [push]
|
|
123
|
+
jobs:
|
|
124
|
+
test:
|
|
125
|
+
runs-on: ubuntu-latest
|
|
126
|
+
steps:
|
|
127
|
+
- uses: actions/checkout@v4
|
|
128
|
+
"""))
|
|
129
|
+
|
|
130
|
+
# -- Git directory with GitHub remote --
|
|
131
|
+
create_git_dir(
|
|
132
|
+
root=tmp_path,
|
|
133
|
+
remote_url="git@github.com:example/devops-integration-app.git",
|
|
134
|
+
default_branch="main",
|
|
135
|
+
extra_remotes={
|
|
136
|
+
"upstream": "https://github.com/upstream/devops-integration-app.git",
|
|
137
|
+
},
|
|
138
|
+
branches=["develop", "feature/scan-integration"],
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# -- Kubernetes manifests --
|
|
142
|
+
k8s_dir = tmp_path / "k8s"
|
|
143
|
+
k8s_dir.mkdir()
|
|
144
|
+
(k8s_dir / "deployment.yaml").write_text(textwrap.dedent("""\
|
|
145
|
+
apiVersion: apps/v1
|
|
146
|
+
kind: Deployment
|
|
147
|
+
metadata:
|
|
148
|
+
name: devops-app
|
|
149
|
+
spec:
|
|
150
|
+
replicas: 3
|
|
151
|
+
"""))
|
|
152
|
+
(k8s_dir / "service.yaml").write_text(textwrap.dedent("""\
|
|
153
|
+
apiVersion: v1
|
|
154
|
+
kind: Service
|
|
155
|
+
metadata:
|
|
156
|
+
name: devops-app
|
|
157
|
+
spec:
|
|
158
|
+
type: ClusterIP
|
|
159
|
+
"""))
|
|
160
|
+
|
|
161
|
+
# -- Helm chart --
|
|
162
|
+
chart_dir = tmp_path / "charts" / "devops-app"
|
|
163
|
+
chart_dir.mkdir(parents=True)
|
|
164
|
+
(chart_dir / "Chart.yaml").write_text(textwrap.dedent("""\
|
|
165
|
+
apiVersion: v2
|
|
166
|
+
name: devops-app
|
|
167
|
+
version: 1.0.0
|
|
168
|
+
description: DevOps integration test chart
|
|
169
|
+
"""))
|
|
170
|
+
|
|
171
|
+
# -- Flux GitOps --
|
|
172
|
+
gitops_dir = tmp_path / "gitops" / "clusters" / "dev"
|
|
173
|
+
gitops_dir.mkdir(parents=True)
|
|
174
|
+
(gitops_dir / "kustomization.yaml").write_text(textwrap.dedent("""\
|
|
175
|
+
apiVersion: kustomize.toolkit.fluxcd.io/v1
|
|
176
|
+
kind: Kustomization
|
|
177
|
+
metadata:
|
|
178
|
+
name: dev-cluster
|
|
179
|
+
namespace: flux-system
|
|
180
|
+
spec:
|
|
181
|
+
interval: 5m
|
|
182
|
+
path: ./gitops/clusters/dev
|
|
183
|
+
prune: true
|
|
184
|
+
"""))
|
|
185
|
+
|
|
186
|
+
# -- Env files --
|
|
187
|
+
(tmp_path / ".env.example").write_text("")
|
|
188
|
+
|
|
189
|
+
return tmp_path
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@pytest.fixture
|
|
193
|
+
def existing_agent_context(tmp_path: Path) -> Dict[str, Any]:
|
|
194
|
+
"""Create a pre-existing project-context.json with agent-enriched data.
|
|
195
|
+
|
|
196
|
+
Simulates a scenario where agents previously populated cluster_details
|
|
197
|
+
and operational_guidelines.
|
|
198
|
+
"""
|
|
199
|
+
return {
|
|
200
|
+
"metadata": {
|
|
201
|
+
"version": "2.0",
|
|
202
|
+
"last_updated": "2026-01-01T00:00:00Z",
|
|
203
|
+
"scan_config": {
|
|
204
|
+
"staleness_hours": 24,
|
|
205
|
+
"last_scan": "2026-01-01T00:00:00Z",
|
|
206
|
+
"scanner_version": "0.1.0",
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
"paths": {},
|
|
210
|
+
"sections": {
|
|
211
|
+
"cluster_details": {
|
|
212
|
+
"_source": "agent:cloud-troubleshooter",
|
|
213
|
+
"cluster_name": "prod-us-central1",
|
|
214
|
+
"node_count": 5,
|
|
215
|
+
},
|
|
216
|
+
"operational_guidelines": {
|
|
217
|
+
"_source": "agent:devops-developer",
|
|
218
|
+
"deployment_strategy": "blue-green",
|
|
219
|
+
"rollback_procedure": "manual",
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _make_orchestrator(
|
|
226
|
+
project_root: Path,
|
|
227
|
+
output_path: Path,
|
|
228
|
+
parallel: bool = False,
|
|
229
|
+
) -> ScanOrchestrator:
|
|
230
|
+
"""Build a ScanOrchestrator with an explicit output path.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
project_root: Project root directory.
|
|
234
|
+
output_path: Path for project-context.json.
|
|
235
|
+
parallel: Whether to run scanners in parallel.
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
Configured ScanOrchestrator.
|
|
239
|
+
"""
|
|
240
|
+
config = ScanConfig(
|
|
241
|
+
project_root=project_root,
|
|
242
|
+
output_path=output_path,
|
|
243
|
+
parallel=parallel,
|
|
244
|
+
)
|
|
245
|
+
registry = ScannerRegistry()
|
|
246
|
+
return ScanOrchestrator(registry=registry, config=config)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
# ===========================================================================
|
|
250
|
+
# T034: Full scan on mock DevOps project
|
|
251
|
+
# ===========================================================================
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class TestFullDevOpsScan:
|
|
255
|
+
"""Integration test: full scan on a realistic multi-language DevOps project."""
|
|
256
|
+
|
|
257
|
+
def test_full_scan_produces_valid_context(
|
|
258
|
+
self, full_devops_project: Path, tmp_path: Path
|
|
259
|
+
) -> None:
|
|
260
|
+
"""Full scan produces a valid project-context.json with all sections."""
|
|
261
|
+
output_path = tmp_path / "output" / "project-context.json"
|
|
262
|
+
orch = _make_orchestrator(full_devops_project, output_path)
|
|
263
|
+
result = orch.run(project_root=full_devops_project)
|
|
264
|
+
|
|
265
|
+
# ScanOutput should be returned
|
|
266
|
+
assert isinstance(result, ScanOutput)
|
|
267
|
+
# File should be written
|
|
268
|
+
assert output_path.is_file()
|
|
269
|
+
|
|
270
|
+
# Validate JSON structure
|
|
271
|
+
written = json.loads(output_path.read_text())
|
|
272
|
+
assert "metadata" in written
|
|
273
|
+
assert "sections" in written
|
|
274
|
+
assert written["metadata"]["version"] == "2.0"
|
|
275
|
+
|
|
276
|
+
def test_project_identity_from_package_json(
|
|
277
|
+
self, full_devops_project: Path, tmp_path: Path
|
|
278
|
+
) -> None:
|
|
279
|
+
"""project_identity section has name from package.json."""
|
|
280
|
+
output_path = tmp_path / "output" / "project-context.json"
|
|
281
|
+
orch = _make_orchestrator(full_devops_project, output_path)
|
|
282
|
+
result = orch.run(project_root=full_devops_project)
|
|
283
|
+
|
|
284
|
+
sections = result.context["sections"]
|
|
285
|
+
assert "project_identity" in sections
|
|
286
|
+
identity = sections["project_identity"]
|
|
287
|
+
assert identity["name"] == "devops-integration-app"
|
|
288
|
+
assert "_source" in identity
|
|
289
|
+
|
|
290
|
+
def test_stack_detects_languages_and_frameworks(
|
|
291
|
+
self, full_devops_project: Path, tmp_path: Path
|
|
292
|
+
) -> None:
|
|
293
|
+
"""stack section lists TypeScript, Python and detects NestJS framework."""
|
|
294
|
+
output_path = tmp_path / "output" / "project-context.json"
|
|
295
|
+
orch = _make_orchestrator(full_devops_project, output_path)
|
|
296
|
+
result = orch.run(project_root=full_devops_project)
|
|
297
|
+
|
|
298
|
+
sections = result.context["sections"]
|
|
299
|
+
assert "stack" in sections
|
|
300
|
+
stack = sections["stack"]
|
|
301
|
+
|
|
302
|
+
# Multi-language: TypeScript (package.json + tsconfig) and Python
|
|
303
|
+
lang_names = [l["name"] for l in stack["languages"]]
|
|
304
|
+
assert "typescript" in lang_names
|
|
305
|
+
assert "python" in lang_names
|
|
306
|
+
|
|
307
|
+
# NestJS framework detected from @nestjs/core
|
|
308
|
+
fw_names = [fw["name"] for fw in stack["frameworks"]]
|
|
309
|
+
assert "nestjs" in fw_names
|
|
310
|
+
|
|
311
|
+
def test_git_detects_github_platform(
|
|
312
|
+
self, full_devops_project: Path, tmp_path: Path
|
|
313
|
+
) -> None:
|
|
314
|
+
"""git section lists GitHub platform from .git/config."""
|
|
315
|
+
output_path = tmp_path / "output" / "project-context.json"
|
|
316
|
+
orch = _make_orchestrator(full_devops_project, output_path)
|
|
317
|
+
result = orch.run(project_root=full_devops_project)
|
|
318
|
+
|
|
319
|
+
sections = result.context["sections"]
|
|
320
|
+
assert "git" in sections
|
|
321
|
+
git = sections["git"]
|
|
322
|
+
assert git["platform"] == "github"
|
|
323
|
+
assert git["default_branch"] == "main"
|
|
324
|
+
assert len(git["remotes"]) >= 1
|
|
325
|
+
|
|
326
|
+
def test_infrastructure_detects_gcp_terraform_docker_ci(
|
|
327
|
+
self, full_devops_project: Path, tmp_path: Path
|
|
328
|
+
) -> None:
|
|
329
|
+
"""infrastructure section detects GCP, Terraform, Docker, GitHub Actions."""
|
|
330
|
+
output_path = tmp_path / "output" / "project-context.json"
|
|
331
|
+
orch = _make_orchestrator(full_devops_project, output_path)
|
|
332
|
+
result = orch.run(project_root=full_devops_project)
|
|
333
|
+
|
|
334
|
+
sections = result.context["sections"]
|
|
335
|
+
assert "infrastructure" in sections
|
|
336
|
+
infra = sections["infrastructure"]
|
|
337
|
+
|
|
338
|
+
# Cloud: GCP from terraform provider
|
|
339
|
+
cloud_names = [cp["name"] for cp in infra.get("cloud_providers", [])]
|
|
340
|
+
assert "gcp" in cloud_names
|
|
341
|
+
|
|
342
|
+
# IaC: Terraform detected
|
|
343
|
+
iac_tools = [i["tool"] for i in infra.get("iac", [])]
|
|
344
|
+
assert "terraform" in iac_tools
|
|
345
|
+
|
|
346
|
+
# CI/CD: GitHub Actions
|
|
347
|
+
ci_platforms = [c["platform"] for c in infra.get("ci_cd", [])]
|
|
348
|
+
assert "github-actions" in ci_platforms
|
|
349
|
+
|
|
350
|
+
# Containers: Docker
|
|
351
|
+
container_tools = [c["tool"] for c in infra.get("containers", [])]
|
|
352
|
+
assert "docker" in container_tools
|
|
353
|
+
|
|
354
|
+
def test_orchestration_detects_flux_k8s_helm(
|
|
355
|
+
self, full_devops_project: Path, tmp_path: Path
|
|
356
|
+
) -> None:
|
|
357
|
+
"""orchestration section detects Flux, Kubernetes manifests, and Helm."""
|
|
358
|
+
output_path = tmp_path / "output" / "project-context.json"
|
|
359
|
+
orch = _make_orchestrator(full_devops_project, output_path)
|
|
360
|
+
result = orch.run(project_root=full_devops_project)
|
|
361
|
+
|
|
362
|
+
sections = result.context["sections"]
|
|
363
|
+
assert "orchestration" in sections
|
|
364
|
+
orch_data = sections["orchestration"]
|
|
365
|
+
|
|
366
|
+
# GitOps: Flux detected
|
|
367
|
+
assert orch_data["gitops"]["tool"] == "flux"
|
|
368
|
+
|
|
369
|
+
# Kubernetes: manifests found
|
|
370
|
+
assert orch_data["kubernetes"]["detected"] is True
|
|
371
|
+
|
|
372
|
+
# Helm: chart detected
|
|
373
|
+
assert orch_data["helm"]["detected"] is True
|
|
374
|
+
|
|
375
|
+
def test_environment_has_os_info(
|
|
376
|
+
self, full_devops_project: Path, tmp_path: Path
|
|
377
|
+
) -> None:
|
|
378
|
+
"""environment section has OS information populated."""
|
|
379
|
+
output_path = tmp_path / "output" / "project-context.json"
|
|
380
|
+
orch = _make_orchestrator(full_devops_project, output_path)
|
|
381
|
+
result = orch.run(project_root=full_devops_project)
|
|
382
|
+
|
|
383
|
+
sections = result.context["sections"]
|
|
384
|
+
assert "environment" in sections
|
|
385
|
+
env = sections["environment"]
|
|
386
|
+
assert "os" in env
|
|
387
|
+
assert env["os"]["platform"] in ("linux", "darwin", "win32")
|
|
388
|
+
assert env["os"]["architecture"] in ("x64", "arm64")
|
|
389
|
+
|
|
390
|
+
def test_v2_sections_present_no_backward_compat(
|
|
391
|
+
self, full_devops_project: Path, tmp_path: Path
|
|
392
|
+
) -> None:
|
|
393
|
+
"""v2 scanner sections present; backward-compat sections NOT produced."""
|
|
394
|
+
output_path = tmp_path / "output" / "project-context.json"
|
|
395
|
+
orch = _make_orchestrator(full_devops_project, output_path)
|
|
396
|
+
result = orch.run(project_root=full_devops_project)
|
|
397
|
+
|
|
398
|
+
sections = result.context["sections"]
|
|
399
|
+
# v2 sections must be present
|
|
400
|
+
assert "project_identity" in sections
|
|
401
|
+
assert "stack" in sections
|
|
402
|
+
assert "git" in sections
|
|
403
|
+
assert "environment" in sections
|
|
404
|
+
assert "infrastructure" in sections
|
|
405
|
+
# backward-compat sections must NOT be produced
|
|
406
|
+
assert "project_details" not in sections
|
|
407
|
+
assert "application_architecture" not in sections
|
|
408
|
+
assert "development_standards" not in sections
|
|
409
|
+
|
|
410
|
+
def test_agent_enriched_data_preserved(
|
|
411
|
+
self,
|
|
412
|
+
full_devops_project: Path,
|
|
413
|
+
existing_agent_context: Dict[str, Any],
|
|
414
|
+
tmp_path: Path,
|
|
415
|
+
) -> None:
|
|
416
|
+
"""Pre-existing agent-enriched sections (cluster_details) are preserved."""
|
|
417
|
+
output_path = tmp_path / "output" / "project-context.json"
|
|
418
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
419
|
+
|
|
420
|
+
# Write existing context with agent-enriched data
|
|
421
|
+
output_path.write_text(json.dumps(existing_agent_context, indent=2))
|
|
422
|
+
|
|
423
|
+
orch = _make_orchestrator(full_devops_project, output_path)
|
|
424
|
+
result = orch.run(project_root=full_devops_project)
|
|
425
|
+
|
|
426
|
+
sections = result.context["sections"]
|
|
427
|
+
# Agent-enriched cluster_details should be preserved
|
|
428
|
+
assert "cluster_details" in sections
|
|
429
|
+
assert sections["cluster_details"]["cluster_name"] == "prod-us-central1"
|
|
430
|
+
assert sections["cluster_details"]["node_count"] == 5
|
|
431
|
+
|
|
432
|
+
# operational_guidelines should also be preserved
|
|
433
|
+
assert "operational_guidelines" in sections
|
|
434
|
+
assert sections["operational_guidelines"]["deployment_strategy"] == "blue-green"
|
|
435
|
+
|
|
436
|
+
def test_scan_completes_under_10_seconds(
|
|
437
|
+
self, full_devops_project: Path, tmp_path: Path
|
|
438
|
+
) -> None:
|
|
439
|
+
"""Full scan completes in under 10 seconds (NFR-001)."""
|
|
440
|
+
output_path = tmp_path / "output" / "project-context.json"
|
|
441
|
+
orch = _make_orchestrator(full_devops_project, output_path)
|
|
442
|
+
|
|
443
|
+
start = time.monotonic()
|
|
444
|
+
result = orch.run(project_root=full_devops_project)
|
|
445
|
+
elapsed = time.monotonic() - start
|
|
446
|
+
|
|
447
|
+
assert elapsed < 10.0, f"Scan took {elapsed:.2f}s, exceeds 10s NFR-001 limit"
|
|
448
|
+
|
|
449
|
+
def test_sections_updated_tracking(
|
|
450
|
+
self, full_devops_project: Path, tmp_path: Path
|
|
451
|
+
) -> None:
|
|
452
|
+
"""ScanOutput tracks which sections were updated."""
|
|
453
|
+
output_path = tmp_path / "output" / "project-context.json"
|
|
454
|
+
orch = _make_orchestrator(full_devops_project, output_path)
|
|
455
|
+
result = orch.run(project_root=full_devops_project)
|
|
456
|
+
|
|
457
|
+
assert len(result.sections_updated) > 0
|
|
458
|
+
# Core scanner-produced sections should be in the updated list
|
|
459
|
+
assert "project_identity" in result.sections_updated
|
|
460
|
+
assert "stack" in result.sections_updated
|
|
461
|
+
assert "git" in result.sections_updated
|
|
462
|
+
|
|
463
|
+
def test_json_schema_written_correctly(
|
|
464
|
+
self, full_devops_project: Path, tmp_path: Path
|
|
465
|
+
) -> None:
|
|
466
|
+
"""project-context.json has correct top-level schema."""
|
|
467
|
+
output_path = tmp_path / "output" / "project-context.json"
|
|
468
|
+
orch = _make_orchestrator(full_devops_project, output_path)
|
|
469
|
+
orch.run(project_root=full_devops_project)
|
|
470
|
+
|
|
471
|
+
written = json.loads(output_path.read_text())
|
|
472
|
+
# Top-level keys
|
|
473
|
+
assert "metadata" in written
|
|
474
|
+
assert "sections" in written
|
|
475
|
+
# Metadata fields
|
|
476
|
+
assert "last_updated" in written["metadata"]
|
|
477
|
+
assert "scan_config" in written["metadata"]
|
|
478
|
+
assert "scanner_version" in written["metadata"]["scan_config"]
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
# ===========================================================================
|
|
482
|
+
# T035: Scan on minimal project
|
|
483
|
+
# ===========================================================================
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def _patch_host_detections():
|
|
487
|
+
"""Return a list of mock patches that suppress host-level detections.
|
|
488
|
+
|
|
489
|
+
The infrastructure scanner checks ~/.aws/config, ~/.config/gcloud, etc.
|
|
490
|
+
The orchestration scanner checks ~/.kube/config and KUBECONFIG env var.
|
|
491
|
+
These detect host-level CLI configs that are not project-specific.
|
|
492
|
+
We patch them out so minimal-project tests are host-independent.
|
|
493
|
+
"""
|
|
494
|
+
return [
|
|
495
|
+
patch(
|
|
496
|
+
"tools.scan.scanners.infrastructure.InfrastructureScanner"
|
|
497
|
+
"._detect_providers_from_cli_configs",
|
|
498
|
+
lambda self, providers: None,
|
|
499
|
+
),
|
|
500
|
+
patch(
|
|
501
|
+
"tools.scan.scanners.infrastructure.InfrastructureScanner"
|
|
502
|
+
"._detect_providers_from_env_vars",
|
|
503
|
+
lambda self, providers: None,
|
|
504
|
+
),
|
|
505
|
+
patch(
|
|
506
|
+
"tools.scan.scanners.orchestration.OrchestrationScanner"
|
|
507
|
+
"._find_kubeconfig",
|
|
508
|
+
lambda self: None,
|
|
509
|
+
),
|
|
510
|
+
]
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
class TestMinimalProjectScan:
|
|
514
|
+
"""Integration test: scan on an empty or minimal project."""
|
|
515
|
+
|
|
516
|
+
def test_empty_directory_completes_without_error(
|
|
517
|
+
self, tmp_path: Path
|
|
518
|
+
) -> None:
|
|
519
|
+
"""Scan on empty directory completes without errors or crashes."""
|
|
520
|
+
output_path = tmp_path / "output" / "project-context.json"
|
|
521
|
+
orch = _make_orchestrator(tmp_path, output_path)
|
|
522
|
+
result = orch.run(project_root=tmp_path)
|
|
523
|
+
|
|
524
|
+
assert isinstance(result, ScanOutput)
|
|
525
|
+
assert len(result.errors) == 0
|
|
526
|
+
|
|
527
|
+
def test_empty_directory_has_environment_os(
|
|
528
|
+
self, tmp_path: Path
|
|
529
|
+
) -> None:
|
|
530
|
+
"""Empty directory scan still populates environment.os."""
|
|
531
|
+
output_path = tmp_path / "output" / "project-context.json"
|
|
532
|
+
orch = _make_orchestrator(tmp_path, output_path)
|
|
533
|
+
result = orch.run(project_root=tmp_path)
|
|
534
|
+
|
|
535
|
+
sections = result.context["sections"]
|
|
536
|
+
assert "environment" in sections
|
|
537
|
+
assert "os" in sections["environment"]
|
|
538
|
+
assert sections["environment"]["os"]["platform"] in ("linux", "darwin", "win32")
|
|
539
|
+
|
|
540
|
+
def test_empty_directory_has_project_identity(
|
|
541
|
+
self, tmp_path: Path
|
|
542
|
+
) -> None:
|
|
543
|
+
"""Empty directory has project_identity with type 'unknown' and dir name."""
|
|
544
|
+
output_path = tmp_path / "output" / "project-context.json"
|
|
545
|
+
orch = _make_orchestrator(tmp_path, output_path)
|
|
546
|
+
result = orch.run(project_root=tmp_path)
|
|
547
|
+
|
|
548
|
+
sections = result.context["sections"]
|
|
549
|
+
assert "project_identity" in sections
|
|
550
|
+
identity = sections["project_identity"]
|
|
551
|
+
assert identity["type"] == "unknown"
|
|
552
|
+
# Name should fall back to directory name
|
|
553
|
+
assert identity["name"] == tmp_path.name
|
|
554
|
+
|
|
555
|
+
def test_empty_directory_has_stack_with_empty_languages(
|
|
556
|
+
self, tmp_path: Path
|
|
557
|
+
) -> None:
|
|
558
|
+
"""Empty directory has stack section with empty languages list."""
|
|
559
|
+
output_path = tmp_path / "output" / "project-context.json"
|
|
560
|
+
orch = _make_orchestrator(tmp_path, output_path)
|
|
561
|
+
result = orch.run(project_root=tmp_path)
|
|
562
|
+
|
|
563
|
+
sections = result.context["sections"]
|
|
564
|
+
assert "stack" in sections
|
|
565
|
+
assert sections["stack"]["languages"] == []
|
|
566
|
+
|
|
567
|
+
def test_empty_directory_has_git_with_null_platform(
|
|
568
|
+
self, tmp_path: Path
|
|
569
|
+
) -> None:
|
|
570
|
+
"""Empty directory has git section with platform null."""
|
|
571
|
+
output_path = tmp_path / "output" / "project-context.json"
|
|
572
|
+
orch = _make_orchestrator(tmp_path, output_path)
|
|
573
|
+
result = orch.run(project_root=tmp_path)
|
|
574
|
+
|
|
575
|
+
sections = result.context["sections"]
|
|
576
|
+
assert "git" in sections
|
|
577
|
+
assert sections["git"]["platform"] is None
|
|
578
|
+
|
|
579
|
+
def test_empty_directory_no_infrastructure(
|
|
580
|
+
self, tmp_path: Path
|
|
581
|
+
) -> None:
|
|
582
|
+
"""Empty directory has no infrastructure section (project-level only).
|
|
583
|
+
|
|
584
|
+
Host-level CLI configs (e.g., ~/.aws/config, ~/.config/gcloud) are
|
|
585
|
+
patched out so this test only checks for project-level indicators.
|
|
586
|
+
"""
|
|
587
|
+
output_path = tmp_path / "output" / "project-context.json"
|
|
588
|
+
patches = _patch_host_detections()
|
|
589
|
+
for p in patches:
|
|
590
|
+
p.start()
|
|
591
|
+
try:
|
|
592
|
+
orch = _make_orchestrator(tmp_path, output_path)
|
|
593
|
+
result = orch.run(project_root=tmp_path)
|
|
594
|
+
finally:
|
|
595
|
+
for p in patches:
|
|
596
|
+
p.stop()
|
|
597
|
+
|
|
598
|
+
sections = result.context["sections"]
|
|
599
|
+
assert "infrastructure" not in sections
|
|
600
|
+
|
|
601
|
+
def test_empty_directory_no_orchestration(
|
|
602
|
+
self, tmp_path: Path
|
|
603
|
+
) -> None:
|
|
604
|
+
"""Empty directory has no orchestration section (project-level only).
|
|
605
|
+
|
|
606
|
+
Host-level kubeconfig detection is patched out so this test only
|
|
607
|
+
checks for project-level Kubernetes/GitOps indicators.
|
|
608
|
+
"""
|
|
609
|
+
output_path = tmp_path / "output" / "project-context.json"
|
|
610
|
+
patches = _patch_host_detections()
|
|
611
|
+
for p in patches:
|
|
612
|
+
p.start()
|
|
613
|
+
try:
|
|
614
|
+
orch = _make_orchestrator(tmp_path, output_path)
|
|
615
|
+
result = orch.run(project_root=tmp_path)
|
|
616
|
+
finally:
|
|
617
|
+
for p in patches:
|
|
618
|
+
p.stop()
|
|
619
|
+
|
|
620
|
+
sections = result.context["sections"]
|
|
621
|
+
assert "orchestration" not in sections
|
|
622
|
+
|
|
623
|
+
def test_minimal_readme_only_project(
|
|
624
|
+
self, tmp_path: Path
|
|
625
|
+
) -> None:
|
|
626
|
+
"""Minimal project with only README.md produces same base structure.
|
|
627
|
+
|
|
628
|
+
Host-level detections are patched out so dynamic sections (infrastructure,
|
|
629
|
+
orchestration) are only produced when project files indicate them.
|
|
630
|
+
"""
|
|
631
|
+
(tmp_path / "README.md").write_text("# My Project\n")
|
|
632
|
+
output_path = tmp_path / "output" / "project-context.json"
|
|
633
|
+
patches = _patch_host_detections()
|
|
634
|
+
for p in patches:
|
|
635
|
+
p.start()
|
|
636
|
+
try:
|
|
637
|
+
orch = _make_orchestrator(tmp_path, output_path)
|
|
638
|
+
result = orch.run(project_root=tmp_path)
|
|
639
|
+
finally:
|
|
640
|
+
for p in patches:
|
|
641
|
+
p.stop()
|
|
642
|
+
|
|
643
|
+
sections = result.context["sections"]
|
|
644
|
+
# Same base sections as empty directory
|
|
645
|
+
assert "project_identity" in sections
|
|
646
|
+
assert "stack" in sections
|
|
647
|
+
assert "git" in sections
|
|
648
|
+
assert "environment" in sections
|
|
649
|
+
# No dynamic sections
|
|
650
|
+
assert "infrastructure" not in sections
|
|
651
|
+
assert "orchestration" not in sections
|
|
652
|
+
|
|
653
|
+
def test_minimal_project_writes_valid_json(
|
|
654
|
+
self, tmp_path: Path
|
|
655
|
+
) -> None:
|
|
656
|
+
"""Minimal project writes valid JSON to disk."""
|
|
657
|
+
(tmp_path / "README.md").write_text("# My Project\n")
|
|
658
|
+
output_path = tmp_path / "output" / "project-context.json"
|
|
659
|
+
orch = _make_orchestrator(tmp_path, output_path)
|
|
660
|
+
orch.run(project_root=tmp_path)
|
|
661
|
+
|
|
662
|
+
assert output_path.is_file()
|
|
663
|
+
written = json.loads(output_path.read_text())
|
|
664
|
+
assert "metadata" in written
|
|
665
|
+
assert "sections" in written
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
# ===========================================================================
|
|
669
|
+
# T036: Idempotency test
|
|
670
|
+
# ===========================================================================
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
class TestIdempotency:
|
|
674
|
+
"""Integration test: running scan twice produces identical results.
|
|
675
|
+
|
|
676
|
+
True idempotency is measured from stabilized state: run 2 vs run 3,
|
|
677
|
+
both of which read back the written context before scanning.
|
|
678
|
+
"""
|
|
679
|
+
|
|
680
|
+
def test_consecutive_scans_produce_identical_sections(
|
|
681
|
+
self, full_devops_project: Path, tmp_path: Path
|
|
682
|
+
) -> None:
|
|
683
|
+
"""Stabilized scans (run 2 and run 3) produce identical sections."""
|
|
684
|
+
output_path = tmp_path / "output" / "project-context.json"
|
|
685
|
+
orch = _make_orchestrator(full_devops_project, output_path)
|
|
686
|
+
|
|
687
|
+
# Run 1: initial scan (creates context from scratch)
|
|
688
|
+
orch.run(project_root=full_devops_project)
|
|
689
|
+
# Run 2: reads back context, merges -- this is the stabilized state
|
|
690
|
+
result2 = orch.run(project_root=full_devops_project)
|
|
691
|
+
# Run 3: should be identical to run 2
|
|
692
|
+
result3 = orch.run(project_root=full_devops_project)
|
|
693
|
+
|
|
694
|
+
assert result2.context["sections"] == result3.context["sections"]
|
|
695
|
+
|
|
696
|
+
def test_only_timestamps_differ_between_scans(
|
|
697
|
+
self, full_devops_project: Path, tmp_path: Path
|
|
698
|
+
) -> None:
|
|
699
|
+
"""Only metadata timestamps differ between stabilized scans."""
|
|
700
|
+
output_path = tmp_path / "output" / "project-context.json"
|
|
701
|
+
orch = _make_orchestrator(full_devops_project, output_path)
|
|
702
|
+
|
|
703
|
+
# Stabilize by running twice first
|
|
704
|
+
orch.run(project_root=full_devops_project)
|
|
705
|
+
result2 = orch.run(project_root=full_devops_project)
|
|
706
|
+
result3 = orch.run(project_root=full_devops_project)
|
|
707
|
+
|
|
708
|
+
ctx2 = copy.deepcopy(result2.context)
|
|
709
|
+
ctx3 = copy.deepcopy(result3.context)
|
|
710
|
+
|
|
711
|
+
# Remove timestamp fields
|
|
712
|
+
for ctx in (ctx2, ctx3):
|
|
713
|
+
ctx["metadata"].pop("last_updated", None)
|
|
714
|
+
if "scan_config" in ctx["metadata"]:
|
|
715
|
+
ctx["metadata"]["scan_config"].pop("last_scan", None)
|
|
716
|
+
|
|
717
|
+
assert ctx2 == ctx3
|
|
718
|
+
|
|
719
|
+
def test_clean_slate_scans_are_deterministic(
|
|
720
|
+
self, full_devops_project: Path, tmp_path: Path
|
|
721
|
+
) -> None:
|
|
722
|
+
"""Two independent clean-slate scans produce identical sections.
|
|
723
|
+
|
|
724
|
+
Each scan writes to a separate output path so there is no existing
|
|
725
|
+
context to merge with, isolating scanner-level determinism.
|
|
726
|
+
"""
|
|
727
|
+
output1 = tmp_path / "out1" / "project-context.json"
|
|
728
|
+
output2 = tmp_path / "out2" / "project-context.json"
|
|
729
|
+
|
|
730
|
+
orch1 = _make_orchestrator(full_devops_project, output1)
|
|
731
|
+
orch2 = _make_orchestrator(full_devops_project, output2)
|
|
732
|
+
|
|
733
|
+
result1 = orch1.run(project_root=full_devops_project)
|
|
734
|
+
result2 = orch2.run(project_root=full_devops_project)
|
|
735
|
+
|
|
736
|
+
assert result1.context["sections"] == result2.context["sections"]
|
|
737
|
+
|
|
738
|
+
def test_agent_enriched_data_preserved_between_scans(
|
|
739
|
+
self,
|
|
740
|
+
full_devops_project: Path,
|
|
741
|
+
existing_agent_context: Dict[str, Any],
|
|
742
|
+
tmp_path: Path,
|
|
743
|
+
) -> None:
|
|
744
|
+
"""Agent-enriched data is preserved across multiple scans."""
|
|
745
|
+
output_path = tmp_path / "output" / "project-context.json"
|
|
746
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
747
|
+
|
|
748
|
+
# Write existing context with agent-enriched data
|
|
749
|
+
output_path.write_text(json.dumps(existing_agent_context, indent=2))
|
|
750
|
+
|
|
751
|
+
orch = _make_orchestrator(full_devops_project, output_path)
|
|
752
|
+
|
|
753
|
+
# Run scan twice
|
|
754
|
+
result1 = orch.run(project_root=full_devops_project)
|
|
755
|
+
result2 = orch.run(project_root=full_devops_project)
|
|
756
|
+
|
|
757
|
+
# Agent-enriched sections must survive both scans
|
|
758
|
+
for result in (result1, result2):
|
|
759
|
+
sections = result.context["sections"]
|
|
760
|
+
assert "cluster_details" in sections
|
|
761
|
+
assert sections["cluster_details"]["cluster_name"] == "prod-us-central1"
|
|
762
|
+
assert "operational_guidelines" in sections
|
|
763
|
+
assert sections["operational_guidelines"]["deployment_strategy"] == "blue-green"
|
|
764
|
+
|
|
765
|
+
def test_no_random_ordering_in_output(
|
|
766
|
+
self, full_devops_project: Path, tmp_path: Path
|
|
767
|
+
) -> None:
|
|
768
|
+
"""Scanner output is deterministic -- no random ordering in lists.
|
|
769
|
+
|
|
770
|
+
After stabilization (run 1 + run 2), subsequent runs produce
|
|
771
|
+
byte-identical sections.
|
|
772
|
+
"""
|
|
773
|
+
output_path = tmp_path / "output" / "project-context.json"
|
|
774
|
+
orch = _make_orchestrator(full_devops_project, output_path)
|
|
775
|
+
|
|
776
|
+
# Stabilize
|
|
777
|
+
orch.run(project_root=full_devops_project)
|
|
778
|
+
orch.run(project_root=full_devops_project)
|
|
779
|
+
|
|
780
|
+
# Run 3 more times and compare
|
|
781
|
+
results = []
|
|
782
|
+
for _ in range(3):
|
|
783
|
+
result = orch.run(project_root=full_devops_project)
|
|
784
|
+
results.append(result.context["sections"])
|
|
785
|
+
|
|
786
|
+
assert results[0] == results[1]
|
|
787
|
+
assert results[1] == results[2]
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
# ===========================================================================
|
|
791
|
+
# T037: Scanner failure isolation
|
|
792
|
+
# ===========================================================================
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
class _FailingScanner(BaseScanner):
|
|
796
|
+
"""A scanner that always raises RuntimeError for testing failure isolation."""
|
|
797
|
+
|
|
798
|
+
@property
|
|
799
|
+
def SCANNER_NAME(self) -> str:
|
|
800
|
+
return "stack"
|
|
801
|
+
|
|
802
|
+
@property
|
|
803
|
+
def SCANNER_VERSION(self) -> str:
|
|
804
|
+
return "1.0.0"
|
|
805
|
+
|
|
806
|
+
@property
|
|
807
|
+
def OWNED_SECTIONS(self):
|
|
808
|
+
return ["project_identity", "stack"]
|
|
809
|
+
|
|
810
|
+
def scan(self, root: Path) -> ScanResult:
|
|
811
|
+
raise RuntimeError("Simulated scanner failure for testing")
|
|
812
|
+
|
|
813
|
+
|
|
814
|
+
class TestScannerFailureIsolation:
|
|
815
|
+
"""Integration test: single scanner failure does not abort entire scan."""
|
|
816
|
+
|
|
817
|
+
def _build_orchestrator_with_failing_stack(
|
|
818
|
+
self, project_root: Path, output_path: Path
|
|
819
|
+
) -> ScanOrchestrator:
|
|
820
|
+
"""Build an orchestrator with the stack scanner replaced by a failing one."""
|
|
821
|
+
config = ScanConfig(
|
|
822
|
+
project_root=project_root,
|
|
823
|
+
output_path=output_path,
|
|
824
|
+
parallel=False,
|
|
825
|
+
)
|
|
826
|
+
registry = ScannerRegistry()
|
|
827
|
+
|
|
828
|
+
# Replace the real stack scanner with the failing one
|
|
829
|
+
failing = _FailingScanner()
|
|
830
|
+
if "stack" in registry._scanners:
|
|
831
|
+
# Remove the existing stack scanner and its section ownership
|
|
832
|
+
old_scanner = registry._scanners.pop("stack")
|
|
833
|
+
for section in old_scanner.OWNED_SECTIONS:
|
|
834
|
+
registry._section_owners.pop(section, None)
|
|
835
|
+
|
|
836
|
+
# Register the failing scanner
|
|
837
|
+
registry._scanners[failing.SCANNER_NAME] = failing
|
|
838
|
+
for section in failing.OWNED_SECTIONS:
|
|
839
|
+
registry._section_owners[section] = failing.SCANNER_NAME
|
|
840
|
+
|
|
841
|
+
return ScanOrchestrator(registry=registry, config=config)
|
|
842
|
+
|
|
843
|
+
def test_scan_completes_despite_scanner_failure(
|
|
844
|
+
self, full_devops_project: Path, tmp_path: Path
|
|
845
|
+
) -> None:
|
|
846
|
+
"""Scan completes without raising when one scanner fails."""
|
|
847
|
+
output_path = tmp_path / "output" / "project-context.json"
|
|
848
|
+
orch = self._build_orchestrator_with_failing_stack(
|
|
849
|
+
full_devops_project, output_path
|
|
850
|
+
)
|
|
851
|
+
|
|
852
|
+
# Should NOT raise
|
|
853
|
+
result = orch.run(project_root=full_devops_project)
|
|
854
|
+
assert isinstance(result, ScanOutput)
|
|
855
|
+
|
|
856
|
+
def test_warning_recorded_for_failed_scanner(
|
|
857
|
+
self, full_devops_project: Path, tmp_path: Path
|
|
858
|
+
) -> None:
|
|
859
|
+
"""ScanOutput.warnings includes a message about the failed scanner."""
|
|
860
|
+
output_path = tmp_path / "output" / "project-context.json"
|
|
861
|
+
orch = self._build_orchestrator_with_failing_stack(
|
|
862
|
+
full_devops_project, output_path
|
|
863
|
+
)
|
|
864
|
+
result = orch.run(project_root=full_devops_project)
|
|
865
|
+
|
|
866
|
+
# Should have a warning about the stack scanner failure
|
|
867
|
+
warning_text = " ".join(result.warnings)
|
|
868
|
+
assert "stack" in warning_text.lower() or "RuntimeError" in warning_text
|
|
869
|
+
|
|
870
|
+
def test_failed_scanner_sections_absent(
|
|
871
|
+
self, full_devops_project: Path, tmp_path: Path
|
|
872
|
+
) -> None:
|
|
873
|
+
"""project_identity and stack sections are absent (failed scanner's sections)."""
|
|
874
|
+
output_path = tmp_path / "output" / "project-context.json"
|
|
875
|
+
orch = self._build_orchestrator_with_failing_stack(
|
|
876
|
+
full_devops_project, output_path
|
|
877
|
+
)
|
|
878
|
+
result = orch.run(project_root=full_devops_project)
|
|
879
|
+
|
|
880
|
+
sections = result.context["sections"]
|
|
881
|
+
# The stack scanner failed, so its owned sections should not be present
|
|
882
|
+
assert "project_identity" not in sections
|
|
883
|
+
assert "stack" not in sections
|
|
884
|
+
|
|
885
|
+
def test_other_scanners_still_produce_output(
|
|
886
|
+
self, full_devops_project: Path, tmp_path: Path
|
|
887
|
+
) -> None:
|
|
888
|
+
"""git, environment, infrastructure, and orchestration still produce output."""
|
|
889
|
+
output_path = tmp_path / "output" / "project-context.json"
|
|
890
|
+
orch = self._build_orchestrator_with_failing_stack(
|
|
891
|
+
full_devops_project, output_path
|
|
892
|
+
)
|
|
893
|
+
result = orch.run(project_root=full_devops_project)
|
|
894
|
+
|
|
895
|
+
sections = result.context["sections"]
|
|
896
|
+
|
|
897
|
+
# Other scanners should still produce their sections
|
|
898
|
+
assert "git" in sections
|
|
899
|
+
assert sections["git"]["platform"] == "github"
|
|
900
|
+
|
|
901
|
+
assert "environment" in sections
|
|
902
|
+
assert "os" in sections["environment"]
|
|
903
|
+
|
|
904
|
+
assert "infrastructure" in sections
|
|
905
|
+
assert "orchestration" in sections
|
|
906
|
+
|
|
907
|
+
def test_scanner_results_include_failed_scanner(
|
|
908
|
+
self, full_devops_project: Path, tmp_path: Path
|
|
909
|
+
) -> None:
|
|
910
|
+
"""scanner_results includes the failed scanner with empty sections."""
|
|
911
|
+
output_path = tmp_path / "output" / "project-context.json"
|
|
912
|
+
orch = self._build_orchestrator_with_failing_stack(
|
|
913
|
+
full_devops_project, output_path
|
|
914
|
+
)
|
|
915
|
+
result = orch.run(project_root=full_devops_project)
|
|
916
|
+
|
|
917
|
+
assert "stack" in result.scanner_results
|
|
918
|
+
failed_result = result.scanner_results["stack"]
|
|
919
|
+
assert failed_result.sections == {}
|
|
920
|
+
assert len(failed_result.warnings) > 0
|