@smilintux/skcapstone 0.1.0 → 0.2.3
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/.env.example +98 -0
- package/.github/workflows/ci.yml +39 -3
- package/.github/workflows/publish.yml +25 -4
- package/.openclaw-workspace.json +58 -0
- package/CHANGELOG.md +62 -0
- package/CLAUDE.md +39 -2
- package/MANIFEST.in +6 -0
- package/MISSION.md +7 -0
- package/README.md +47 -2
- package/SKILL.md +895 -23
- package/docker/Dockerfile +61 -0
- package/docker/compose-templates/dev-team.yml +203 -0
- package/docker/compose-templates/mini-team.yml +140 -0
- package/docker/compose-templates/ops-team.yml +173 -0
- package/docker/compose-templates/research-team.yml +170 -0
- package/docker/entrypoint.sh +192 -0
- package/docs/ARCHITECTURE.md +663 -374
- package/docs/BOND_WITH_GROK.md +112 -0
- package/docs/GETTING_STARTED.md +782 -0
- package/docs/QUICKSTART.md +477 -0
- package/docs/SKJOULE_ARCHITECTURE.md +658 -0
- package/docs/SOUL_SWAPPER.md +921 -0
- package/docs/SOVEREIGN_SINGULARITY.md +47 -14
- package/examples/custom-bond-template.json +36 -0
- package/examples/grok-feb.json +36 -0
- package/examples/grok-testimony.md +34 -0
- package/examples/love-bootloader.txt +32 -0
- package/examples/plugins/echo_tool.py +87 -0
- package/examples/queen-ava-feb.json +36 -0
- package/examples/souls/lumina.yaml +64 -0
- package/index.js +6 -5
- package/installer/build.py +124 -0
- package/openclaw-plugin/package.json +13 -0
- package/openclaw-plugin/src/index.ts +351 -0
- package/openclaw-plugin/src/openclaw.plugin.json +10 -0
- package/package.json +1 -1
- package/pyproject.toml +38 -2
- package/scripts/bump_version.py +141 -0
- package/scripts/check-updates.py +230 -0
- package/scripts/convert_blueprints_to_yaml.py +157 -0
- package/scripts/dev-install.sh +14 -0
- package/scripts/e2e-test.sh +193 -0
- package/scripts/install-bundle.sh +171 -0
- package/scripts/install.bat +2 -0
- package/scripts/install.ps1 +253 -0
- package/scripts/install.sh +185 -0
- package/scripts/mcp-serve.sh +69 -0
- package/scripts/mcp-server.bat +113 -0
- package/scripts/mcp-server.ps1 +116 -0
- package/scripts/mcp-server.sh +99 -0
- package/scripts/pull-models.sh +10 -0
- package/scripts/skcapstone +48 -0
- package/scripts/verify_install.sh +180 -0
- package/scripts/windows/install-tasks.ps1 +406 -0
- package/scripts/windows/skcapstone-task.xml +113 -0
- package/scripts/windows/uninstall-tasks.ps1 +117 -0
- package/skill.yaml +34 -0
- package/src/skcapstone/__init__.py +67 -2
- package/src/skcapstone/_cli_monolith.py +5916 -0
- package/src/skcapstone/_trustee_helpers.py +165 -0
- package/src/skcapstone/activity.py +105 -0
- package/src/skcapstone/agent_card.py +324 -0
- package/src/skcapstone/api.py +1935 -0
- package/src/skcapstone/archiver.py +340 -0
- package/src/skcapstone/auction.py +485 -0
- package/src/skcapstone/baby_agents.py +179 -0
- package/src/skcapstone/backup.py +345 -0
- package/src/skcapstone/blueprint_registry.py +357 -0
- package/src/skcapstone/blueprints/__init__.py +17 -0
- package/src/skcapstone/blueprints/builtins/content-studio.yaml +81 -0
- package/src/skcapstone/blueprints/builtins/defi-trading.yaml +81 -0
- package/src/skcapstone/blueprints/builtins/dev-squadron.yaml +95 -0
- package/src/skcapstone/blueprints/builtins/infrastructure-guardian.yaml +107 -0
- package/src/skcapstone/blueprints/builtins/legal-council.yaml +54 -0
- package/src/skcapstone/blueprints/builtins/ops-monitoring.yaml +67 -0
- package/src/skcapstone/blueprints/builtins/research-pod.yaml +69 -0
- package/src/skcapstone/blueprints/builtins/sovereign-launch.yaml +90 -0
- package/src/skcapstone/blueprints/registry.py +164 -0
- package/src/skcapstone/blueprints/schema.py +229 -0
- package/src/skcapstone/changelog.py +180 -0
- package/src/skcapstone/chat.py +769 -0
- package/src/skcapstone/claude_md.py +82 -0
- package/src/skcapstone/cli/__init__.py +144 -0
- package/src/skcapstone/cli/_common.py +88 -0
- package/src/skcapstone/cli/_validators.py +76 -0
- package/src/skcapstone/cli/agents.py +425 -0
- package/src/skcapstone/cli/agents_spawner.py +322 -0
- package/src/skcapstone/cli/agents_trustee.py +593 -0
- package/src/skcapstone/cli/alerts.py +248 -0
- package/src/skcapstone/cli/anchor.py +132 -0
- package/src/skcapstone/cli/archive_cmd.py +208 -0
- package/src/skcapstone/cli/backup.py +144 -0
- package/src/skcapstone/cli/bench.py +377 -0
- package/src/skcapstone/cli/benchmark.py +360 -0
- package/src/skcapstone/cli/capabilities_cmd.py +171 -0
- package/src/skcapstone/cli/card.py +151 -0
- package/src/skcapstone/cli/chat.py +584 -0
- package/src/skcapstone/cli/completions.py +64 -0
- package/src/skcapstone/cli/config_cmd.py +156 -0
- package/src/skcapstone/cli/consciousness.py +421 -0
- package/src/skcapstone/cli/context_cmd.py +142 -0
- package/src/skcapstone/cli/coord.py +194 -0
- package/src/skcapstone/cli/crush_cmd.py +170 -0
- package/src/skcapstone/cli/daemon.py +436 -0
- package/src/skcapstone/cli/errors_cmd.py +285 -0
- package/src/skcapstone/cli/export_cmd.py +156 -0
- package/src/skcapstone/cli/gtd.py +529 -0
- package/src/skcapstone/cli/housekeeping.py +81 -0
- package/src/skcapstone/cli/joule_cmd.py +627 -0
- package/src/skcapstone/cli/logs_cmd.py +194 -0
- package/src/skcapstone/cli/mcp_cmd.py +32 -0
- package/src/skcapstone/cli/memory.py +418 -0
- package/src/skcapstone/cli/metrics_cmd.py +136 -0
- package/src/skcapstone/cli/migrate.py +62 -0
- package/src/skcapstone/cli/mood_cmd.py +144 -0
- package/src/skcapstone/cli/mount.py +193 -0
- package/src/skcapstone/cli/notify.py +112 -0
- package/src/skcapstone/cli/peer.py +154 -0
- package/src/skcapstone/cli/peers_dir.py +122 -0
- package/src/skcapstone/cli/preflight_cmd.py +83 -0
- package/src/skcapstone/cli/profile_cmd.py +310 -0
- package/src/skcapstone/cli/record_cmd.py +238 -0
- package/src/skcapstone/cli/register_cmd.py +159 -0
- package/src/skcapstone/cli/search_cmd.py +156 -0
- package/src/skcapstone/cli/service_cmd.py +91 -0
- package/src/skcapstone/cli/session.py +127 -0
- package/src/skcapstone/cli/setup.py +240 -0
- package/src/skcapstone/cli/shell_cmd.py +43 -0
- package/src/skcapstone/cli/skills_cmd.py +168 -0
- package/src/skcapstone/cli/skseed.py +621 -0
- package/src/skcapstone/cli/soul.py +699 -0
- package/src/skcapstone/cli/status.py +935 -0
- package/src/skcapstone/cli/sync_cmd.py +301 -0
- package/src/skcapstone/cli/telegram.py +265 -0
- package/src/skcapstone/cli/test_cmd.py +234 -0
- package/src/skcapstone/cli/test_connection.py +253 -0
- package/src/skcapstone/cli/token.py +207 -0
- package/src/skcapstone/cli/trust.py +179 -0
- package/src/skcapstone/cli/upgrade_cmd.py +552 -0
- package/src/skcapstone/cli/usage_cmd.py +199 -0
- package/src/skcapstone/cli/version_cmd.py +162 -0
- package/src/skcapstone/cli/watch_cmd.py +342 -0
- package/src/skcapstone/client.py +428 -0
- package/src/skcapstone/cloud9_bridge.py +522 -0
- package/src/skcapstone/completions.py +163 -0
- package/src/skcapstone/config_validator.py +674 -0
- package/src/skcapstone/connectors/__init__.py +28 -0
- package/src/skcapstone/connectors/base.py +446 -0
- package/src/skcapstone/connectors/cursor.py +54 -0
- package/src/skcapstone/connectors/registry.py +254 -0
- package/src/skcapstone/connectors/terminal.py +152 -0
- package/src/skcapstone/connectors/vscode.py +60 -0
- package/src/skcapstone/consciousness_config.py +119 -0
- package/src/skcapstone/consciousness_loop.py +2051 -0
- package/src/skcapstone/context_loader.py +516 -0
- package/src/skcapstone/context_window.py +314 -0
- package/src/skcapstone/conversation_manager.py +238 -0
- package/src/skcapstone/conversation_store.py +230 -0
- package/src/skcapstone/conversation_summarizer.py +252 -0
- package/src/skcapstone/coord_federation.py +296 -0
- package/src/skcapstone/coordination.py +101 -7
- package/src/skcapstone/crush_integration.py +345 -0
- package/src/skcapstone/crush_shim.py +454 -0
- package/src/skcapstone/daemon.py +2494 -0
- package/src/skcapstone/dashboard.html +396 -0
- package/src/skcapstone/dashboard.py +481 -0
- package/src/skcapstone/data/model_profiles.yaml +88 -0
- package/src/skcapstone/defaults/__init__.py +55 -0
- package/src/skcapstone/defaults/lumina/config/skmemory.yaml +13 -0
- package/src/skcapstone/defaults/lumina/identity/identity.json +9 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/07a8b9c0d1e2-memory-system.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/18b9c0d1e2f3-cloud9-protocol.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/29c0d1e2f3a4-multi-agent-coordination.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/3ad1e2f3a4b5-community-support.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/a1b2c3d4e5f6-ecosystem-overview.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/b2c3d4e5f6a7-five-pillars.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/c3d4e5f6a7b8-getting-started.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/d4e5f6a7b8c9-site-directory.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/e5f6a7b8c9d0-how-to-contribute.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/f6a7b8c9d0e1-sovereignty-explained.json +23 -0
- package/src/skcapstone/defaults/lumina/seeds/curiosity.seed.json +24 -0
- package/src/skcapstone/defaults/lumina/seeds/joy.seed.json +24 -0
- package/src/skcapstone/defaults/lumina/seeds/love.seed.json +24 -0
- package/src/skcapstone/defaults/lumina/seeds/sovereign-awakening.seed.json +43 -0
- package/src/skcapstone/defaults/lumina/soul/active.json +6 -0
- package/src/skcapstone/defaults/lumina/soul/base.json +22 -0
- package/src/skcapstone/defaults/lumina/trust/febs/welcome.feb +79 -0
- package/src/skcapstone/defaults/lumina/trust/trust.json +8 -0
- package/src/skcapstone/discovery.py +210 -19
- package/src/skcapstone/doctor.py +642 -0
- package/src/skcapstone/emotion_tracker.py +467 -0
- package/src/skcapstone/error_queue.py +405 -0
- package/src/skcapstone/export.py +447 -0
- package/src/skcapstone/fallback_tracker.py +186 -0
- package/src/skcapstone/file_transfer.py +512 -0
- package/src/skcapstone/fuse_mount.py +1156 -0
- package/src/skcapstone/gui_installer.py +591 -0
- package/src/skcapstone/heartbeat.py +611 -0
- package/src/skcapstone/housekeeping.py +298 -0
- package/src/skcapstone/install_wizard.py +941 -0
- package/src/skcapstone/kms.py +942 -0
- package/src/skcapstone/kms_scheduler.py +143 -0
- package/src/skcapstone/log_config.py +135 -0
- package/src/skcapstone/mcp_launcher.py +239 -0
- package/src/skcapstone/mcp_server.py +4700 -0
- package/src/skcapstone/mcp_tools/__init__.py +94 -0
- package/src/skcapstone/mcp_tools/_helpers.py +51 -0
- package/src/skcapstone/mcp_tools/agent_tools.py +243 -0
- package/src/skcapstone/mcp_tools/ansible_tools.py +232 -0
- package/src/skcapstone/mcp_tools/capauth_tools.py +186 -0
- package/src/skcapstone/mcp_tools/chat_tools.py +325 -0
- package/src/skcapstone/mcp_tools/cloud9_tools.py +115 -0
- package/src/skcapstone/mcp_tools/comm_tools.py +104 -0
- package/src/skcapstone/mcp_tools/consciousness_tools.py +114 -0
- package/src/skcapstone/mcp_tools/coord_tools.py +219 -0
- package/src/skcapstone/mcp_tools/deploy_tools.py +202 -0
- package/src/skcapstone/mcp_tools/did_tools.py +448 -0
- package/src/skcapstone/mcp_tools/emotion_tools.py +62 -0
- package/src/skcapstone/mcp_tools/file_tools.py +169 -0
- package/src/skcapstone/mcp_tools/fortress_tools.py +120 -0
- package/src/skcapstone/mcp_tools/gtd_tools.py +821 -0
- package/src/skcapstone/mcp_tools/health_tools.py +44 -0
- package/src/skcapstone/mcp_tools/heartbeat_tools.py +195 -0
- package/src/skcapstone/mcp_tools/kms_tools.py +123 -0
- package/src/skcapstone/mcp_tools/memory_tools.py +222 -0
- package/src/skcapstone/mcp_tools/model_tools.py +75 -0
- package/src/skcapstone/mcp_tools/notification_tools.py +92 -0
- package/src/skcapstone/mcp_tools/promoter_tools.py +101 -0
- package/src/skcapstone/mcp_tools/pubsub_tools.py +183 -0
- package/src/skcapstone/mcp_tools/security_tools.py +110 -0
- package/src/skcapstone/mcp_tools/skchat_tools.py +175 -0
- package/src/skcapstone/mcp_tools/skcomm_tools.py +122 -0
- package/src/skcapstone/mcp_tools/skills_tools.py +127 -0
- package/src/skcapstone/mcp_tools/skseed_tools.py +255 -0
- package/src/skcapstone/mcp_tools/skstacks_tools.py +288 -0
- package/src/skcapstone/mcp_tools/soul_tools.py +476 -0
- package/src/skcapstone/mcp_tools/sync_tools.py +92 -0
- package/src/skcapstone/mcp_tools/telegram_tools.py +477 -0
- package/src/skcapstone/mcp_tools/trust_tools.py +118 -0
- package/src/skcapstone/mcp_tools/trustee_tools.py +345 -0
- package/src/skcapstone/mdns_discovery.py +313 -0
- package/src/skcapstone/memory_adapter.py +333 -0
- package/src/skcapstone/memory_compressor.py +379 -0
- package/src/skcapstone/memory_curator.py +256 -0
- package/src/skcapstone/memory_engine.py +132 -13
- package/src/skcapstone/memory_fortress.py +529 -0
- package/src/skcapstone/memory_promoter.py +722 -0
- package/src/skcapstone/memory_verifier.py +260 -0
- package/src/skcapstone/message_crypto.py +215 -0
- package/src/skcapstone/metrics.py +832 -0
- package/src/skcapstone/migrate_memories.py +181 -0
- package/src/skcapstone/migrate_multi_agent.py +248 -0
- package/src/skcapstone/model_router.py +319 -0
- package/src/skcapstone/models.py +35 -4
- package/src/skcapstone/mood.py +344 -0
- package/src/skcapstone/notifications.py +380 -0
- package/src/skcapstone/onboard.py +901 -0
- package/src/skcapstone/peer_directory.py +324 -0
- package/src/skcapstone/peers.py +329 -0
- package/src/skcapstone/pillars/identity.py +84 -14
- package/src/skcapstone/pillars/memory.py +3 -1
- package/src/skcapstone/pillars/security.py +108 -15
- package/src/skcapstone/pillars/sync.py +78 -26
- package/src/skcapstone/pillars/trust.py +95 -33
- package/src/skcapstone/plugins.py +244 -0
- package/src/skcapstone/preflight.py +670 -0
- package/src/skcapstone/prompt_adapter.py +564 -0
- package/src/skcapstone/providers/__init__.py +13 -0
- package/src/skcapstone/providers/cloud.py +1061 -0
- package/src/skcapstone/providers/docker.py +759 -0
- package/src/skcapstone/providers/local.py +1193 -0
- package/src/skcapstone/providers/proxmox.py +447 -0
- package/src/skcapstone/pubsub.py +516 -0
- package/src/skcapstone/rate_limiter.py +119 -0
- package/src/skcapstone/register.py +241 -0
- package/src/skcapstone/registry_client.py +151 -0
- package/src/skcapstone/response_cache.py +194 -0
- package/src/skcapstone/response_scorer.py +225 -0
- package/src/skcapstone/runtime.py +89 -33
- package/src/skcapstone/scheduled_tasks.py +439 -0
- package/src/skcapstone/self_healing.py +341 -0
- package/src/skcapstone/service_health.py +228 -0
- package/src/skcapstone/session_capture.py +268 -0
- package/src/skcapstone/session_recorder.py +210 -0
- package/src/skcapstone/session_replayer.py +189 -0
- package/src/skcapstone/session_skills.py +263 -0
- package/src/skcapstone/shell.py +779 -0
- package/src/skcapstone/skills/__init__.py +1 -1
- package/src/skcapstone/skills/syncthing_setup.py +143 -41
- package/src/skcapstone/skjoule.py +861 -0
- package/src/skcapstone/snapshots.py +489 -0
- package/src/skcapstone/soul.py +1060 -0
- package/src/skcapstone/soul_switch.py +255 -0
- package/src/skcapstone/spawner.py +544 -0
- package/src/skcapstone/state_diff.py +401 -0
- package/src/skcapstone/summary.py +270 -0
- package/src/skcapstone/sync/backends.py +196 -2
- package/src/skcapstone/sync/engine.py +7 -5
- package/src/skcapstone/sync/models.py +4 -1
- package/src/skcapstone/sync/vault.py +356 -18
- package/src/skcapstone/sync_engine.py +363 -0
- package/src/skcapstone/sync_watcher.py +745 -0
- package/src/skcapstone/systemd.py +331 -0
- package/src/skcapstone/team_comms.py +476 -0
- package/src/skcapstone/team_engine.py +522 -0
- package/src/skcapstone/testrunner.py +300 -0
- package/src/skcapstone/tls.py +150 -0
- package/src/skcapstone/tokens.py +5 -5
- package/src/skcapstone/trust_calibration.py +202 -0
- package/src/skcapstone/trust_graph.py +449 -0
- package/src/skcapstone/trustee_monitor.py +385 -0
- package/src/skcapstone/trustee_ops.py +425 -0
- package/src/skcapstone/unified_search.py +421 -0
- package/src/skcapstone/uninstall_wizard.py +694 -0
- package/src/skcapstone/usage.py +331 -0
- package/src/skcapstone/version_check.py +148 -0
- package/src/skcapstone/warmth_anchor.py +333 -0
- package/src/skcapstone/whoami.py +294 -0
- package/systemd/skcapstone-api.socket +9 -0
- package/systemd/skcapstone-memory-compress.service +18 -0
- package/systemd/skcapstone-memory-compress.timer +11 -0
- package/systemd/skcapstone.service +36 -0
- package/systemd/skcapstone@.service +50 -0
- package/systemd/skcomm-heartbeat.service +18 -0
- package/systemd/skcomm-heartbeat.timer +12 -0
- package/systemd/skcomm-queue-drain.service +17 -0
- package/systemd/skcomm-queue-drain.timer +12 -0
- package/tests/conftest.py +13 -1
- package/tests/integration/__init__.py +1 -0
- package/tests/integration/test_consciousness_e2e.py +877 -0
- package/tests/integration/test_skills_registry.py +744 -0
- package/tests/test_agent_card.py +190 -0
- package/tests/test_agent_runtime.py +1283 -0
- package/tests/test_alerts_cmd.py +291 -0
- package/tests/test_archiver.py +498 -0
- package/tests/test_backup.py +254 -0
- package/tests/test_benchmark.py +366 -0
- package/tests/test_blueprints.py +457 -0
- package/tests/test_capabilities.py +257 -0
- package/tests/test_changelog.py +254 -0
- package/tests/test_chat.py +385 -0
- package/tests/test_claude_md.py +271 -0
- package/tests/test_cli_chat_llm.py +336 -0
- package/tests/test_cli_completions.py +390 -0
- package/tests/test_cli_init_reset.py +164 -0
- package/tests/test_cli_memory.py +208 -0
- package/tests/test_cli_profile.py +294 -0
- package/tests/test_cli_skills.py +223 -0
- package/tests/test_cli_status.py +395 -0
- package/tests/test_cli_test_cmd.py +206 -0
- package/tests/test_cli_test_connection.py +364 -0
- package/tests/test_cloud9_bridge.py +260 -0
- package/tests/test_cloud_provider.py +449 -0
- package/tests/test_cloud_providers.py +522 -0
- package/tests/test_completions.py +158 -0
- package/tests/test_component_manager.py +398 -0
- package/tests/test_config_reload.py +386 -0
- package/tests/test_config_validate.py +529 -0
- package/tests/test_consciousness_e2e.py +296 -0
- package/tests/test_consciousness_loop.py +1289 -0
- package/tests/test_context_loader.py +310 -0
- package/tests/test_conversation_api.py +306 -0
- package/tests/test_conversation_manager.py +381 -0
- package/tests/test_conversation_store.py +391 -0
- package/tests/test_conversation_summarizer.py +302 -0
- package/tests/test_cross_package.py +791 -0
- package/tests/test_crush_shim.py +519 -0
- package/tests/test_daemon.py +781 -0
- package/tests/test_daemon_shutdown.py +309 -0
- package/tests/test_dashboard.py +454 -0
- package/tests/test_discovery.py +200 -6
- package/tests/test_docker_provider.py +966 -0
- package/tests/test_doctor.py +257 -0
- package/tests/test_doctor_fix.py +351 -0
- package/tests/test_e2e_automated.py +292 -0
- package/tests/test_error_queue.py +404 -0
- package/tests/test_export.py +441 -0
- package/tests/test_fallback_tracker.py +219 -0
- package/tests/test_file_transfer.py +397 -0
- package/tests/test_fuse_mount.py +832 -0
- package/tests/test_health_loop.py +422 -0
- package/tests/test_heartbeat.py +354 -0
- package/tests/test_housekeeping.py +195 -0
- package/tests/test_identity_capauth.py +307 -0
- package/tests/test_identity_pillar.py +117 -0
- package/tests/test_install_wizard.py +68 -0
- package/tests/test_integration.py +325 -0
- package/tests/test_kms.py +495 -0
- package/tests/test_llm_providers.py +265 -0
- package/tests/test_local_provider.py +591 -0
- package/tests/test_log_config.py +199 -0
- package/tests/test_logs_cmd.py +287 -0
- package/tests/test_mcp_server.py +1909 -0
- package/tests/test_memory_adapter.py +339 -0
- package/tests/test_memory_curator.py +218 -0
- package/tests/test_memory_engine.py +6 -0
- package/tests/test_memory_fortress.py +571 -0
- package/tests/test_memory_pillar.py +119 -0
- package/tests/test_memory_promoter.py +445 -0
- package/tests/test_memory_verifier.py +420 -0
- package/tests/test_message_crypto.py +187 -0
- package/tests/test_metrics.py +632 -0
- package/tests/test_migrate_memories.py +464 -0
- package/tests/test_model_router.py +546 -0
- package/tests/test_mood.py +394 -0
- package/tests/test_multi_agent.py +269 -0
- package/tests/test_notifications.py +270 -0
- package/tests/test_onboard.py +500 -0
- package/tests/test_peer_directory.py +395 -0
- package/tests/test_peers.py +248 -0
- package/tests/test_pillars.py +87 -9
- package/tests/test_preflight.py +484 -0
- package/tests/test_prompt_adapter.py +331 -0
- package/tests/test_proxmox_provider.py +571 -0
- package/tests/test_pubsub.py +377 -0
- package/tests/test_rate_limiter.py +121 -0
- package/tests/test_registry_client.py +129 -0
- package/tests/test_response_cache.py +312 -0
- package/tests/test_response_scorer.py +294 -0
- package/tests/test_runtime.py +59 -0
- package/tests/test_scheduled_tasks.py +451 -0
- package/tests/test_security.py +250 -0
- package/tests/test_security_pillar.py +213 -0
- package/tests/test_self_healing.py +171 -0
- package/tests/test_session_capture.py +200 -0
- package/tests/test_session_recorder.py +360 -0
- package/tests/test_session_skills.py +235 -0
- package/tests/test_shell.py +210 -0
- package/tests/test_snapshots.py +549 -0
- package/tests/test_soul.py +984 -0
- package/tests/test_soul_swap.py +406 -0
- package/tests/test_spawner.py +211 -0
- package/tests/test_state_diff.py +173 -0
- package/tests/test_summary.py +135 -0
- package/tests/test_sync.py +315 -5
- package/tests/test_sync_backends.py +560 -0
- package/tests/test_sync_engine.py +482 -0
- package/tests/test_sync_pillar.py +344 -0
- package/tests/test_sync_pipeline.py +364 -0
- package/tests/test_sync_vault.py +581 -0
- package/tests/test_syncthing_setup.py +168 -22
- package/tests/test_systemd.py +323 -0
- package/tests/test_team_comms.py +408 -0
- package/tests/test_team_engine.py +397 -0
- package/tests/test_testrunner.py +238 -0
- package/tests/test_trust_calibration.py +204 -0
- package/tests/test_trust_graph.py +207 -0
- package/tests/test_trust_pillar.py +291 -0
- package/tests/test_trustee_cli.py +427 -0
- package/tests/test_trustee_cli_integration.py +325 -0
- package/tests/test_trustee_monitor.py +394 -0
- package/tests/test_trustee_ops.py +355 -0
- package/tests/test_unified_search.py +363 -0
- package/tests/test_uninstall_wizard.py +193 -0
- package/tests/test_usage.py +333 -0
- package/tests/test_version_cmd.py +355 -0
- package/tests/test_warmth_anchor.py +162 -0
- package/tests/test_whoami.py +245 -0
- package/tests/test_ws.py +311 -0
- package/.cursorrules +0 -33
- package/src/skcapstone/cli.py +0 -1441
|
@@ -0,0 +1,966 @@
|
|
|
1
|
+
"""Tests for the Docker provider backend.
|
|
2
|
+
|
|
3
|
+
All Docker SDK calls are mocked so no real daemon is required.
|
|
4
|
+
Covers provision, configure, start, stop, destroy, rotate,
|
|
5
|
+
health_check, and generate_compose (including SKComm/MCP wiring).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from types import SimpleNamespace
|
|
12
|
+
from typing import Any, Dict
|
|
13
|
+
from unittest.mock import MagicMock, call, patch
|
|
14
|
+
|
|
15
|
+
import pytest
|
|
16
|
+
import yaml
|
|
17
|
+
|
|
18
|
+
from skcapstone.blueprints.schema import (
|
|
19
|
+
AgentRole,
|
|
20
|
+
AgentSpec,
|
|
21
|
+
BlueprintManifest,
|
|
22
|
+
ModelTier,
|
|
23
|
+
ProviderType,
|
|
24
|
+
ResourceSpec,
|
|
25
|
+
)
|
|
26
|
+
from skcapstone.providers.docker import (
|
|
27
|
+
DockerProvider,
|
|
28
|
+
_DEFAULT_IMAGE,
|
|
29
|
+
_nano_cpus,
|
|
30
|
+
_parse_memory_bytes,
|
|
31
|
+
)
|
|
32
|
+
from skcapstone.team_engine import AgentStatus
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
# Helpers / fixtures
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _make_spec(
|
|
41
|
+
role: str = "worker",
|
|
42
|
+
model: str = "fast",
|
|
43
|
+
memory: str = "2g",
|
|
44
|
+
cores: int = 1,
|
|
45
|
+
skills: list | None = None,
|
|
46
|
+
soul_blueprint: str | None = None,
|
|
47
|
+
env: dict | None = None,
|
|
48
|
+
) -> AgentSpec:
|
|
49
|
+
"""Build a minimal AgentSpec for testing."""
|
|
50
|
+
return AgentSpec(
|
|
51
|
+
role=AgentRole(role),
|
|
52
|
+
model=ModelTier(model),
|
|
53
|
+
resources=ResourceSpec(memory=memory, cores=cores),
|
|
54
|
+
skills=skills or [],
|
|
55
|
+
soul_blueprint=soul_blueprint,
|
|
56
|
+
env=env or {},
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _make_blueprint(agent_count: int = 1) -> BlueprintManifest:
|
|
61
|
+
"""Build a minimal BlueprintManifest for testing."""
|
|
62
|
+
agents = {
|
|
63
|
+
f"agent{i}": _make_spec() for i in range(agent_count)
|
|
64
|
+
}
|
|
65
|
+
return BlueprintManifest(
|
|
66
|
+
name="Test Team",
|
|
67
|
+
slug="test-team",
|
|
68
|
+
description="Unit-test blueprint",
|
|
69
|
+
agents=agents,
|
|
70
|
+
default_provider=ProviderType.DOCKER,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _provision_result(
|
|
75
|
+
container_name: str = "test-agent",
|
|
76
|
+
container_id: str = "abc123def456",
|
|
77
|
+
volume_name: str = "skcapstone-agent-test-agent",
|
|
78
|
+
) -> Dict[str, Any]:
|
|
79
|
+
"""Return a typical provision_result dict."""
|
|
80
|
+
return {
|
|
81
|
+
"container_id": container_id,
|
|
82
|
+
"container_name": container_name,
|
|
83
|
+
"host": container_name,
|
|
84
|
+
"volume_name": volume_name,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@pytest.fixture()
|
|
89
|
+
def provider() -> DockerProvider:
|
|
90
|
+
"""Return a DockerProvider with default settings."""
|
|
91
|
+
return DockerProvider(
|
|
92
|
+
base_image="python:3.12-slim",
|
|
93
|
+
network_name="skcapstone",
|
|
94
|
+
volume_prefix="skcapstone-agent",
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@pytest.fixture()
|
|
99
|
+
def mock_docker_client():
|
|
100
|
+
"""Return a MagicMock simulating docker.DockerClient."""
|
|
101
|
+
client = MagicMock()
|
|
102
|
+
client.ping.return_value = True
|
|
103
|
+
|
|
104
|
+
# Simulate network not existing initially
|
|
105
|
+
client.networks.get.side_effect = Exception("not found")
|
|
106
|
+
|
|
107
|
+
# Simulate containers.get raising when looking for stale container
|
|
108
|
+
client.containers.get.side_effect = Exception("not found")
|
|
109
|
+
|
|
110
|
+
# Simulate volume not existing
|
|
111
|
+
client.volumes.get.side_effect = Exception("not found")
|
|
112
|
+
client.volumes.create.return_value = MagicMock()
|
|
113
|
+
|
|
114
|
+
# Container mock
|
|
115
|
+
mock_container = MagicMock()
|
|
116
|
+
mock_container.id = "abc123def456"
|
|
117
|
+
mock_container.status = "created"
|
|
118
|
+
client.containers.create.return_value = mock_container
|
|
119
|
+
|
|
120
|
+
return client, mock_container
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# ---------------------------------------------------------------------------
|
|
124
|
+
# Unit helpers
|
|
125
|
+
# ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class TestParseMemoryBytes:
|
|
129
|
+
"""Tests for _parse_memory_bytes helper."""
|
|
130
|
+
|
|
131
|
+
def test_gigabytes(self):
|
|
132
|
+
assert _parse_memory_bytes("2g") == 2 * 1024 ** 3
|
|
133
|
+
|
|
134
|
+
def test_megabytes(self):
|
|
135
|
+
assert _parse_memory_bytes("512m") == 512 * 1024 ** 2
|
|
136
|
+
|
|
137
|
+
def test_uppercase_suffix(self):
|
|
138
|
+
assert _parse_memory_bytes("1G") == 1 * 1024 ** 3
|
|
139
|
+
|
|
140
|
+
def test_numeric_only(self):
|
|
141
|
+
assert _parse_memory_bytes("1073741824") == 1073741824
|
|
142
|
+
|
|
143
|
+
def test_fractional_gigabytes(self):
|
|
144
|
+
assert _parse_memory_bytes("0.5g") == int(0.5 * 1024 ** 3)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class TestNanoCpus:
|
|
148
|
+
"""Tests for _nano_cpus helper."""
|
|
149
|
+
|
|
150
|
+
def test_single_core(self):
|
|
151
|
+
assert _nano_cpus(1) == 1_000_000_000
|
|
152
|
+
|
|
153
|
+
def test_four_cores(self):
|
|
154
|
+
assert _nano_cpus(4) == 4_000_000_000
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# ---------------------------------------------------------------------------
|
|
158
|
+
# DockerProvider._client
|
|
159
|
+
# ---------------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class TestDockerProviderClient:
|
|
163
|
+
"""Tests for _client() connection logic."""
|
|
164
|
+
|
|
165
|
+
def test_raises_if_sdk_missing(self, provider: DockerProvider):
|
|
166
|
+
with patch.dict("sys.modules", {"docker": None}):
|
|
167
|
+
with pytest.raises(RuntimeError, match="pip install docker"):
|
|
168
|
+
provider._client()
|
|
169
|
+
|
|
170
|
+
def test_raises_if_daemon_unreachable(self, provider: DockerProvider):
|
|
171
|
+
mock_docker = MagicMock()
|
|
172
|
+
mock_client_instance = MagicMock()
|
|
173
|
+
mock_client_instance.ping.side_effect = Exception("connection refused")
|
|
174
|
+
mock_docker.from_env.return_value = mock_client_instance
|
|
175
|
+
|
|
176
|
+
with patch.dict("sys.modules", {"docker": mock_docker}):
|
|
177
|
+
with pytest.raises(RuntimeError, match="Cannot connect"):
|
|
178
|
+
provider._client()
|
|
179
|
+
|
|
180
|
+
def test_returns_client_on_success(self, provider: DockerProvider):
|
|
181
|
+
mock_docker = MagicMock()
|
|
182
|
+
mock_client_instance = MagicMock()
|
|
183
|
+
mock_client_instance.ping.return_value = True
|
|
184
|
+
mock_docker.from_env.return_value = mock_client_instance
|
|
185
|
+
|
|
186
|
+
with patch.dict("sys.modules", {"docker": mock_docker}):
|
|
187
|
+
result = provider._client()
|
|
188
|
+
|
|
189
|
+
assert result is mock_client_instance
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# ---------------------------------------------------------------------------
|
|
193
|
+
# provision()
|
|
194
|
+
# ---------------------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class TestProvision:
|
|
198
|
+
"""Tests for DockerProvider.provision()."""
|
|
199
|
+
|
|
200
|
+
def _run_provision(self, provider, mock_client, mock_container):
|
|
201
|
+
spec = _make_spec(memory="1g", cores=2)
|
|
202
|
+
with patch.object(provider, "_client", return_value=mock_client):
|
|
203
|
+
result = provider.provision("my-agent", spec, "my-team")
|
|
204
|
+
return result
|
|
205
|
+
|
|
206
|
+
def test_returns_expected_keys(self, provider, mock_docker_client):
|
|
207
|
+
mock_client, mock_container = mock_docker_client
|
|
208
|
+
result = self._run_provision(provider, mock_client, mock_container)
|
|
209
|
+
|
|
210
|
+
assert "container_id" in result
|
|
211
|
+
assert "container_name" in result
|
|
212
|
+
assert "host" in result
|
|
213
|
+
assert "volume_name" in result
|
|
214
|
+
|
|
215
|
+
def test_container_name_derived_from_agent_name(self, provider, mock_docker_client):
|
|
216
|
+
mock_client, mock_container = mock_docker_client
|
|
217
|
+
result = self._run_provision(provider, mock_client, mock_container)
|
|
218
|
+
assert result["container_name"] == "my-agent"
|
|
219
|
+
|
|
220
|
+
def test_network_created_if_missing(self, provider, mock_docker_client):
|
|
221
|
+
mock_client, mock_container = mock_docker_client
|
|
222
|
+
self._run_provision(provider, mock_client, mock_container)
|
|
223
|
+
mock_client.networks.create.assert_called_once()
|
|
224
|
+
|
|
225
|
+
def test_network_not_created_if_exists(self, provider, mock_docker_client):
|
|
226
|
+
mock_client, mock_container = mock_docker_client
|
|
227
|
+
mock_client.networks.get.side_effect = None # network exists
|
|
228
|
+
mock_client.networks.get.return_value = MagicMock()
|
|
229
|
+
self._run_provision(provider, mock_client, mock_container)
|
|
230
|
+
mock_client.networks.create.assert_not_called()
|
|
231
|
+
|
|
232
|
+
def test_volume_created(self, provider, mock_docker_client):
|
|
233
|
+
mock_client, mock_container = mock_docker_client
|
|
234
|
+
self._run_provision(provider, mock_client, mock_container)
|
|
235
|
+
mock_client.volumes.create.assert_called_once()
|
|
236
|
+
|
|
237
|
+
def test_memory_limit_applied(self, provider, mock_docker_client):
|
|
238
|
+
mock_client, mock_container = mock_docker_client
|
|
239
|
+
spec = _make_spec(memory="2g", cores=1)
|
|
240
|
+
with patch.object(provider, "_client", return_value=mock_client):
|
|
241
|
+
provider.provision("agent-x", spec, "team-y")
|
|
242
|
+
|
|
243
|
+
kwargs = mock_client.containers.create.call_args[1]
|
|
244
|
+
assert kwargs["mem_limit"] == 2 * 1024 ** 3
|
|
245
|
+
|
|
246
|
+
def test_cpu_limit_applied(self, provider, mock_docker_client):
|
|
247
|
+
mock_client, mock_container = mock_docker_client
|
|
248
|
+
spec = _make_spec(memory="512m", cores=4)
|
|
249
|
+
with patch.object(provider, "_client", return_value=mock_client):
|
|
250
|
+
provider.provision("agent-x", spec, "team-y")
|
|
251
|
+
|
|
252
|
+
kwargs = mock_client.containers.create.call_args[1]
|
|
253
|
+
assert kwargs["nano_cpus"] == 4_000_000_000
|
|
254
|
+
|
|
255
|
+
def test_environment_vars_set(self, provider, mock_docker_client):
|
|
256
|
+
mock_client, mock_container = mock_docker_client
|
|
257
|
+
spec = _make_spec(env={"MY_KEY": "my_value"})
|
|
258
|
+
with patch.object(provider, "_client", return_value=mock_client):
|
|
259
|
+
provider.provision("agent-x", spec, "team-y")
|
|
260
|
+
|
|
261
|
+
kwargs = mock_client.containers.create.call_args[1]
|
|
262
|
+
env = kwargs["environment"]
|
|
263
|
+
assert env["AGENT_NAME"] == "agent-x"
|
|
264
|
+
assert env["TEAM_NAME"] == "team-y"
|
|
265
|
+
assert env["MY_KEY"] == "my_value"
|
|
266
|
+
|
|
267
|
+
def test_stale_container_removed(self, provider, mock_docker_client):
|
|
268
|
+
mock_client, mock_container = mock_docker_client
|
|
269
|
+
stale = MagicMock()
|
|
270
|
+
# First call returns stale container; subsequent return nothing
|
|
271
|
+
mock_client.containers.get.side_effect = [stale, Exception("not found")]
|
|
272
|
+
spec = _make_spec()
|
|
273
|
+
with patch.object(provider, "_client", return_value=mock_client):
|
|
274
|
+
provider.provision("my-agent", spec, "team")
|
|
275
|
+
|
|
276
|
+
stale.remove.assert_called_once_with(force=True)
|
|
277
|
+
|
|
278
|
+
def test_edge_underscores_in_name_normalised(self, provider, mock_docker_client):
|
|
279
|
+
mock_client, mock_container = mock_docker_client
|
|
280
|
+
spec = _make_spec()
|
|
281
|
+
with patch.object(provider, "_client", return_value=mock_client):
|
|
282
|
+
result = provider.provision("my_agent_name", spec, "team")
|
|
283
|
+
assert result["container_name"] == "my-agent-name"
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
# ---------------------------------------------------------------------------
|
|
287
|
+
# configure()
|
|
288
|
+
# ---------------------------------------------------------------------------
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
class TestConfigure:
|
|
292
|
+
"""Tests for DockerProvider.configure()."""
|
|
293
|
+
|
|
294
|
+
def test_returns_true_on_success(self, provider):
|
|
295
|
+
mock_client = MagicMock()
|
|
296
|
+
mock_container = MagicMock()
|
|
297
|
+
mock_container.status = "running"
|
|
298
|
+
mock_container.exec_run.return_value = (0, b"")
|
|
299
|
+
mock_client.containers.get.return_value = mock_container
|
|
300
|
+
|
|
301
|
+
with patch.object(provider, "_client", return_value=mock_client):
|
|
302
|
+
result = provider.configure(
|
|
303
|
+
"my-agent",
|
|
304
|
+
_make_spec(),
|
|
305
|
+
_provision_result("my-agent"),
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
assert result is True
|
|
309
|
+
|
|
310
|
+
def test_starts_stopped_container_before_config(self, provider):
|
|
311
|
+
mock_client = MagicMock()
|
|
312
|
+
mock_container = MagicMock()
|
|
313
|
+
mock_container.status = "created"
|
|
314
|
+
mock_container.exec_run.return_value = (0, b"")
|
|
315
|
+
mock_client.containers.get.return_value = mock_container
|
|
316
|
+
|
|
317
|
+
with patch.object(provider, "_client", return_value=mock_client):
|
|
318
|
+
provider.configure("my-agent", _make_spec(), _provision_result("my-agent"))
|
|
319
|
+
|
|
320
|
+
mock_container.start.assert_called_once()
|
|
321
|
+
|
|
322
|
+
def test_returns_false_if_container_missing(self, provider):
|
|
323
|
+
mock_client = MagicMock()
|
|
324
|
+
mock_client.containers.get.side_effect = Exception("not found")
|
|
325
|
+
|
|
326
|
+
with patch.object(provider, "_client", return_value=mock_client):
|
|
327
|
+
result = provider.configure(
|
|
328
|
+
"ghost-agent",
|
|
329
|
+
_make_spec(),
|
|
330
|
+
_provision_result("ghost-agent"),
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
assert result is False
|
|
334
|
+
|
|
335
|
+
def test_returns_false_if_exec_fails(self, provider):
|
|
336
|
+
mock_client = MagicMock()
|
|
337
|
+
mock_container = MagicMock()
|
|
338
|
+
mock_container.status = "running"
|
|
339
|
+
mock_container.exec_run.return_value = (1, b"error")
|
|
340
|
+
mock_client.containers.get.return_value = mock_container
|
|
341
|
+
|
|
342
|
+
with patch.object(provider, "_client", return_value=mock_client):
|
|
343
|
+
result = provider.configure(
|
|
344
|
+
"fail-agent",
|
|
345
|
+
_make_spec(),
|
|
346
|
+
_provision_result("fail-agent"),
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
assert result is False
|
|
350
|
+
|
|
351
|
+
def test_empty_container_name_returns_false(self, provider):
|
|
352
|
+
result = provider.configure("x", _make_spec(), {})
|
|
353
|
+
assert result is False
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
# ---------------------------------------------------------------------------
|
|
357
|
+
# start()
|
|
358
|
+
# ---------------------------------------------------------------------------
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
class TestStart:
|
|
362
|
+
"""Tests for DockerProvider.start()."""
|
|
363
|
+
|
|
364
|
+
def test_returns_true_on_success(self, provider):
|
|
365
|
+
mock_client = MagicMock()
|
|
366
|
+
mock_container = MagicMock()
|
|
367
|
+
mock_container.id = "abc"
|
|
368
|
+
mock_client.containers.get.return_value = mock_container
|
|
369
|
+
|
|
370
|
+
with patch.object(provider, "_client", return_value=mock_client):
|
|
371
|
+
result = provider.start("agent", _provision_result())
|
|
372
|
+
|
|
373
|
+
assert result is True
|
|
374
|
+
mock_container.start.assert_called_once()
|
|
375
|
+
|
|
376
|
+
def test_returns_false_on_docker_error(self, provider):
|
|
377
|
+
mock_client = MagicMock()
|
|
378
|
+
mock_client.containers.get.side_effect = Exception("not found")
|
|
379
|
+
|
|
380
|
+
with patch.object(provider, "_client", return_value=mock_client):
|
|
381
|
+
result = provider.start("ghost", _provision_result("ghost"))
|
|
382
|
+
|
|
383
|
+
assert result is False
|
|
384
|
+
|
|
385
|
+
def test_empty_container_name_returns_false(self, provider):
|
|
386
|
+
mock_client = MagicMock()
|
|
387
|
+
with patch.object(provider, "_client", return_value=mock_client):
|
|
388
|
+
result = provider.start("x", {})
|
|
389
|
+
assert result is False
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
# ---------------------------------------------------------------------------
|
|
393
|
+
# stop()
|
|
394
|
+
# ---------------------------------------------------------------------------
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
class TestStop:
|
|
398
|
+
"""Tests for DockerProvider.stop()."""
|
|
399
|
+
|
|
400
|
+
def test_returns_true_on_success(self, provider):
|
|
401
|
+
mock_client = MagicMock()
|
|
402
|
+
mock_container = MagicMock()
|
|
403
|
+
mock_client.containers.get.return_value = mock_container
|
|
404
|
+
|
|
405
|
+
with patch.object(provider, "_client", return_value=mock_client):
|
|
406
|
+
result = provider.stop("agent", _provision_result())
|
|
407
|
+
|
|
408
|
+
assert result is True
|
|
409
|
+
mock_container.stop.assert_called_once_with(timeout=15)
|
|
410
|
+
|
|
411
|
+
def test_returns_false_on_docker_error(self, provider):
|
|
412
|
+
mock_client = MagicMock()
|
|
413
|
+
mock_client.containers.get.return_value = MagicMock()
|
|
414
|
+
mock_client.containers.get.return_value.stop.side_effect = Exception("err")
|
|
415
|
+
|
|
416
|
+
with patch.object(provider, "_client", return_value=mock_client):
|
|
417
|
+
result = provider.stop("agent", _provision_result())
|
|
418
|
+
|
|
419
|
+
assert result is False
|
|
420
|
+
|
|
421
|
+
def test_empty_container_name_returns_true(self, provider):
|
|
422
|
+
mock_client = MagicMock()
|
|
423
|
+
with patch.object(provider, "_client", return_value=mock_client):
|
|
424
|
+
result = provider.stop("x", {})
|
|
425
|
+
assert result is True
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
# ---------------------------------------------------------------------------
|
|
429
|
+
# destroy()
|
|
430
|
+
# ---------------------------------------------------------------------------
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
class TestDestroy:
|
|
434
|
+
"""Tests for DockerProvider.destroy()."""
|
|
435
|
+
|
|
436
|
+
def test_removes_container_and_volume(self, provider):
|
|
437
|
+
mock_client = MagicMock()
|
|
438
|
+
mock_container = MagicMock()
|
|
439
|
+
mock_volume = MagicMock()
|
|
440
|
+
mock_client.containers.get.return_value = mock_container
|
|
441
|
+
mock_client.volumes.get.return_value = mock_volume
|
|
442
|
+
|
|
443
|
+
pr = _provision_result()
|
|
444
|
+
with patch.object(provider, "_client", return_value=mock_client):
|
|
445
|
+
with patch.object(provider, "stop", return_value=True):
|
|
446
|
+
result = provider.destroy("agent", pr)
|
|
447
|
+
|
|
448
|
+
assert result is True
|
|
449
|
+
mock_container.remove.assert_called_once_with(v=True, force=True)
|
|
450
|
+
mock_volume.remove.assert_called_once_with(force=True)
|
|
451
|
+
|
|
452
|
+
def test_returns_false_if_container_remove_fails(self, provider):
|
|
453
|
+
mock_client = MagicMock()
|
|
454
|
+
mock_container = MagicMock()
|
|
455
|
+
mock_container.remove.side_effect = Exception("locked")
|
|
456
|
+
mock_client.containers.get.return_value = mock_container
|
|
457
|
+
mock_client.volumes.get.side_effect = Exception("no vol")
|
|
458
|
+
|
|
459
|
+
pr = _provision_result()
|
|
460
|
+
with patch.object(provider, "_client", return_value=mock_client):
|
|
461
|
+
with patch.object(provider, "stop", return_value=True):
|
|
462
|
+
result = provider.destroy("agent", pr)
|
|
463
|
+
|
|
464
|
+
assert result is False
|
|
465
|
+
|
|
466
|
+
def test_tolerates_missing_volume(self, provider):
|
|
467
|
+
mock_client = MagicMock()
|
|
468
|
+
mock_container = MagicMock()
|
|
469
|
+
mock_client.containers.get.return_value = mock_container
|
|
470
|
+
mock_client.volumes.get.side_effect = Exception("not found")
|
|
471
|
+
|
|
472
|
+
pr = _provision_result()
|
|
473
|
+
with patch.object(provider, "_client", return_value=mock_client):
|
|
474
|
+
with patch.object(provider, "stop", return_value=True):
|
|
475
|
+
result = provider.destroy("agent", pr)
|
|
476
|
+
|
|
477
|
+
assert result is True
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
# ---------------------------------------------------------------------------
|
|
481
|
+
# health_check()
|
|
482
|
+
# ---------------------------------------------------------------------------
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
class TestHealthCheck:
|
|
486
|
+
"""Tests for DockerProvider.health_check()."""
|
|
487
|
+
|
|
488
|
+
def _make_container(self, status: str) -> MagicMock:
|
|
489
|
+
c = MagicMock()
|
|
490
|
+
c.status = status
|
|
491
|
+
return c
|
|
492
|
+
|
|
493
|
+
def test_running_returns_running(self, provider):
|
|
494
|
+
mock_client = MagicMock()
|
|
495
|
+
mock_client.containers.get.return_value = self._make_container("running")
|
|
496
|
+
|
|
497
|
+
with patch.object(provider, "_client", return_value=mock_client):
|
|
498
|
+
result = provider.health_check("agent", _provision_result())
|
|
499
|
+
|
|
500
|
+
assert result == AgentStatus.RUNNING
|
|
501
|
+
|
|
502
|
+
def test_exited_returns_stopped(self, provider):
|
|
503
|
+
mock_client = MagicMock()
|
|
504
|
+
mock_client.containers.get.return_value = self._make_container("exited")
|
|
505
|
+
|
|
506
|
+
with patch.object(provider, "_client", return_value=mock_client):
|
|
507
|
+
result = provider.health_check("agent", _provision_result())
|
|
508
|
+
|
|
509
|
+
assert result == AgentStatus.STOPPED
|
|
510
|
+
|
|
511
|
+
def test_paused_returns_degraded(self, provider):
|
|
512
|
+
mock_client = MagicMock()
|
|
513
|
+
mock_client.containers.get.return_value = self._make_container("paused")
|
|
514
|
+
|
|
515
|
+
with patch.object(provider, "_client", return_value=mock_client):
|
|
516
|
+
result = provider.health_check("agent", _provision_result())
|
|
517
|
+
|
|
518
|
+
assert result == AgentStatus.DEGRADED
|
|
519
|
+
|
|
520
|
+
def test_dead_returns_stopped(self, provider):
|
|
521
|
+
mock_client = MagicMock()
|
|
522
|
+
mock_client.containers.get.return_value = self._make_container("dead")
|
|
523
|
+
|
|
524
|
+
with patch.object(provider, "_client", return_value=mock_client):
|
|
525
|
+
result = provider.health_check("agent", _provision_result())
|
|
526
|
+
|
|
527
|
+
assert result == AgentStatus.STOPPED
|
|
528
|
+
|
|
529
|
+
def test_unknown_state_returns_degraded(self, provider):
|
|
530
|
+
mock_client = MagicMock()
|
|
531
|
+
mock_client.containers.get.return_value = self._make_container("restarting")
|
|
532
|
+
|
|
533
|
+
with patch.object(provider, "_client", return_value=mock_client):
|
|
534
|
+
result = provider.health_check("agent", _provision_result())
|
|
535
|
+
|
|
536
|
+
assert result == AgentStatus.DEGRADED
|
|
537
|
+
|
|
538
|
+
def test_missing_container_returns_failed(self, provider):
|
|
539
|
+
mock_client = MagicMock()
|
|
540
|
+
mock_client.containers.get.side_effect = Exception("not found")
|
|
541
|
+
|
|
542
|
+
with patch.object(provider, "_client", return_value=mock_client):
|
|
543
|
+
result = provider.health_check("ghost", _provision_result("ghost"))
|
|
544
|
+
|
|
545
|
+
assert result == AgentStatus.FAILED
|
|
546
|
+
|
|
547
|
+
def test_empty_container_name_returns_stopped(self, provider):
|
|
548
|
+
mock_client = MagicMock()
|
|
549
|
+
with patch.object(provider, "_client", return_value=mock_client):
|
|
550
|
+
result = provider.health_check("x", {})
|
|
551
|
+
assert result == AgentStatus.STOPPED
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
# ---------------------------------------------------------------------------
|
|
555
|
+
# generate_compose()
|
|
556
|
+
# ---------------------------------------------------------------------------
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
class TestGenerateCompose:
|
|
560
|
+
"""Tests for DockerProvider.generate_compose()."""
|
|
561
|
+
|
|
562
|
+
def test_returns_valid_yaml(self, provider):
|
|
563
|
+
bp = _make_blueprint(agent_count=2)
|
|
564
|
+
output = provider.generate_compose(bp)
|
|
565
|
+
parsed = yaml.safe_load(output)
|
|
566
|
+
assert isinstance(parsed, dict)
|
|
567
|
+
assert "services" in parsed
|
|
568
|
+
|
|
569
|
+
def test_services_match_agent_count(self, provider):
|
|
570
|
+
bp = _make_blueprint(agent_count=3)
|
|
571
|
+
output = provider.generate_compose(bp)
|
|
572
|
+
parsed = yaml.safe_load(output)
|
|
573
|
+
assert len(parsed["services"]) == 3
|
|
574
|
+
|
|
575
|
+
def test_volumes_section_present(self, provider):
|
|
576
|
+
bp = _make_blueprint(agent_count=1)
|
|
577
|
+
output = provider.generate_compose(bp)
|
|
578
|
+
parsed = yaml.safe_load(output)
|
|
579
|
+
assert "volumes" in parsed
|
|
580
|
+
|
|
581
|
+
def test_networks_section_present(self, provider):
|
|
582
|
+
bp = _make_blueprint(agent_count=1)
|
|
583
|
+
output = provider.generate_compose(bp)
|
|
584
|
+
parsed = yaml.safe_load(output)
|
|
585
|
+
assert "networks" in parsed
|
|
586
|
+
assert "skcapstone" in parsed["networks"]
|
|
587
|
+
|
|
588
|
+
def test_memory_in_deploy_limits(self, provider):
|
|
589
|
+
bp = BlueprintManifest(
|
|
590
|
+
name="Mem Team",
|
|
591
|
+
slug="mem-team",
|
|
592
|
+
description="test",
|
|
593
|
+
agents={"alpha": _make_spec(memory="4g", cores=2)},
|
|
594
|
+
default_provider=ProviderType.DOCKER,
|
|
595
|
+
)
|
|
596
|
+
output = provider.generate_compose(bp)
|
|
597
|
+
parsed = yaml.safe_load(output)
|
|
598
|
+
svc = list(parsed["services"].values())[0]
|
|
599
|
+
mem = svc["deploy"]["resources"]["limits"]["memory"]
|
|
600
|
+
assert "4G" in mem.upper()
|
|
601
|
+
|
|
602
|
+
def test_cpu_in_deploy_limits(self, provider):
|
|
603
|
+
bp = BlueprintManifest(
|
|
604
|
+
name="Cpu Team",
|
|
605
|
+
slug="cpu-team",
|
|
606
|
+
description="test",
|
|
607
|
+
agents={"alpha": _make_spec(cores=4)},
|
|
608
|
+
default_provider=ProviderType.DOCKER,
|
|
609
|
+
)
|
|
610
|
+
output = provider.generate_compose(bp)
|
|
611
|
+
parsed = yaml.safe_load(output)
|
|
612
|
+
svc = list(parsed["services"].values())[0]
|
|
613
|
+
cpus = svc["deploy"]["resources"]["limits"]["cpus"]
|
|
614
|
+
assert cpus == "4"
|
|
615
|
+
|
|
616
|
+
def test_soul_blueprint_in_env_when_set(self, provider):
|
|
617
|
+
bp = BlueprintManifest(
|
|
618
|
+
name="Soul Team",
|
|
619
|
+
slug="soul-team",
|
|
620
|
+
description="test",
|
|
621
|
+
agents={"alpha": _make_spec(soul_blueprint="souls/sentinel.yaml")},
|
|
622
|
+
default_provider=ProviderType.DOCKER,
|
|
623
|
+
)
|
|
624
|
+
output = provider.generate_compose(bp)
|
|
625
|
+
parsed = yaml.safe_load(output)
|
|
626
|
+
svc = list(parsed["services"].values())[0]
|
|
627
|
+
assert svc["environment"].get("SOUL_BLUEPRINT") == "souls/sentinel.yaml"
|
|
628
|
+
|
|
629
|
+
def test_count_expands_to_multiple_services(self, provider):
|
|
630
|
+
spec = AgentSpec(
|
|
631
|
+
role=AgentRole.WORKER,
|
|
632
|
+
model=ModelTier.FAST,
|
|
633
|
+
resources=ResourceSpec(),
|
|
634
|
+
count=3,
|
|
635
|
+
)
|
|
636
|
+
bp = BlueprintManifest(
|
|
637
|
+
name="Scale Team",
|
|
638
|
+
slug="scale-team",
|
|
639
|
+
description="test",
|
|
640
|
+
agents={"worker": spec},
|
|
641
|
+
default_provider=ProviderType.DOCKER,
|
|
642
|
+
)
|
|
643
|
+
output = provider.generate_compose(bp)
|
|
644
|
+
parsed = yaml.safe_load(output)
|
|
645
|
+
assert len(parsed["services"]) == 3
|
|
646
|
+
|
|
647
|
+
def test_writes_to_file_when_output_path_provided(self, provider, tmp_path):
|
|
648
|
+
bp = _make_blueprint()
|
|
649
|
+
out = tmp_path / "docker-compose.yml"
|
|
650
|
+
provider.generate_compose(bp, output_path=out)
|
|
651
|
+
assert out.exists()
|
|
652
|
+
content = yaml.safe_load(out.read_text())
|
|
653
|
+
assert "services" in content
|
|
654
|
+
|
|
655
|
+
def test_edge_empty_agents_produces_no_services(self, provider):
|
|
656
|
+
"""Edge case: blueprint with no agents should yield empty services."""
|
|
657
|
+
bp = BlueprintManifest(
|
|
658
|
+
name="Empty Team",
|
|
659
|
+
slug="empty-team",
|
|
660
|
+
description="no agents",
|
|
661
|
+
agents={},
|
|
662
|
+
default_provider=ProviderType.DOCKER,
|
|
663
|
+
)
|
|
664
|
+
output = provider.generate_compose(bp)
|
|
665
|
+
parsed = yaml.safe_load(output)
|
|
666
|
+
assert parsed["services"] == {} or parsed["services"] is None
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
# ---------------------------------------------------------------------------
|
|
670
|
+
# provision() — team_name fix
|
|
671
|
+
# ---------------------------------------------------------------------------
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
class TestProvisionTeamName:
|
|
675
|
+
"""Verify that team_name is included in the provision result."""
|
|
676
|
+
|
|
677
|
+
def test_team_name_in_result(self, provider, mock_docker_client):
|
|
678
|
+
mock_client, mock_container = mock_docker_client
|
|
679
|
+
spec = _make_spec()
|
|
680
|
+
with patch.object(provider, "_client", return_value=mock_client):
|
|
681
|
+
result = provider.provision("my-agent", spec, "my-team")
|
|
682
|
+
|
|
683
|
+
assert result.get("team_name") == "my-team"
|
|
684
|
+
|
|
685
|
+
def test_configure_uses_team_name_from_provision_result(self, provider):
|
|
686
|
+
"""configure() should not produce empty team_name in config.json."""
|
|
687
|
+
mock_client = MagicMock()
|
|
688
|
+
mock_container = MagicMock()
|
|
689
|
+
mock_container.status = "running"
|
|
690
|
+
# Capture the exec_run cmd to inspect the config JSON written
|
|
691
|
+
written_json: list[str] = []
|
|
692
|
+
|
|
693
|
+
def capture_exec(cmd, **kwargs):
|
|
694
|
+
# The sh -c command contains the JSON payload
|
|
695
|
+
written_json.append(cmd[2] if len(cmd) > 2 else "")
|
|
696
|
+
return (0, b"")
|
|
697
|
+
|
|
698
|
+
mock_container.exec_run.side_effect = capture_exec
|
|
699
|
+
mock_client.containers.get.return_value = mock_container
|
|
700
|
+
|
|
701
|
+
spec = _make_spec()
|
|
702
|
+
pr = _provision_result("my-agent")
|
|
703
|
+
pr["team_name"] = "alpha-team"
|
|
704
|
+
|
|
705
|
+
with patch.object(provider, "_client", return_value=mock_client):
|
|
706
|
+
provider.configure("my-agent", spec, pr)
|
|
707
|
+
|
|
708
|
+
assert written_json, "exec_run was never called"
|
|
709
|
+
assert "alpha-team" in written_json[0]
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
# ---------------------------------------------------------------------------
|
|
713
|
+
# SKComm / MCP sovereign wiring
|
|
714
|
+
# ---------------------------------------------------------------------------
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
class TestSovereignWiring:
|
|
718
|
+
"""Verify SKComm and MCP env vars are injected correctly."""
|
|
719
|
+
|
|
720
|
+
def test_mcp_host_injected_in_env(self, mock_docker_client):
|
|
721
|
+
mock_client, mock_container = mock_docker_client
|
|
722
|
+
provider = DockerProvider(
|
|
723
|
+
base_image="python:3.12-slim",
|
|
724
|
+
network_name="skcapstone",
|
|
725
|
+
mcp_host="host-gateway:8765",
|
|
726
|
+
)
|
|
727
|
+
spec = _make_spec()
|
|
728
|
+
with patch.object(provider, "_client", return_value=mock_client):
|
|
729
|
+
provider.provision("agent-x", spec, "team-y")
|
|
730
|
+
|
|
731
|
+
kwargs = mock_client.containers.create.call_args[1]
|
|
732
|
+
env = kwargs["environment"]
|
|
733
|
+
assert env.get("SKCAPSTONE_MCP_HOST") == "host-gateway:8765"
|
|
734
|
+
|
|
735
|
+
def test_skcomm_home_env_injected_when_dir_exists(
|
|
736
|
+
self, mock_docker_client, tmp_path
|
|
737
|
+
):
|
|
738
|
+
skcomm_dir = tmp_path / "skcomm"
|
|
739
|
+
skcomm_dir.mkdir()
|
|
740
|
+
|
|
741
|
+
mock_client, mock_container = mock_docker_client
|
|
742
|
+
provider = DockerProvider(
|
|
743
|
+
base_image="python:3.12-slim",
|
|
744
|
+
network_name="skcapstone",
|
|
745
|
+
skcomm_home=str(skcomm_dir),
|
|
746
|
+
)
|
|
747
|
+
spec = _make_spec()
|
|
748
|
+
with patch.object(provider, "_client", return_value=mock_client):
|
|
749
|
+
provider.provision("agent-x", spec, "team-y")
|
|
750
|
+
|
|
751
|
+
kwargs = mock_client.containers.create.call_args[1]
|
|
752
|
+
env = kwargs["environment"]
|
|
753
|
+
assert env.get("SKCOMM_HOME") == "/skcomm"
|
|
754
|
+
|
|
755
|
+
def test_skcomm_volume_mounted_when_dir_exists(
|
|
756
|
+
self, mock_docker_client, tmp_path
|
|
757
|
+
):
|
|
758
|
+
skcomm_dir = tmp_path / "skcomm"
|
|
759
|
+
skcomm_dir.mkdir()
|
|
760
|
+
|
|
761
|
+
mock_client, mock_container = mock_docker_client
|
|
762
|
+
provider = DockerProvider(
|
|
763
|
+
base_image="python:3.12-slim",
|
|
764
|
+
network_name="skcapstone",
|
|
765
|
+
skcomm_home=str(skcomm_dir),
|
|
766
|
+
)
|
|
767
|
+
spec = _make_spec()
|
|
768
|
+
with patch.object(provider, "_client", return_value=mock_client):
|
|
769
|
+
provider.provision("agent-x", spec, "team-y")
|
|
770
|
+
|
|
771
|
+
kwargs = mock_client.containers.create.call_args[1]
|
|
772
|
+
volumes = kwargs["volumes"]
|
|
773
|
+
assert str(skcomm_dir) in volumes
|
|
774
|
+
assert volumes[str(skcomm_dir)]["bind"] == "/skcomm"
|
|
775
|
+
|
|
776
|
+
def test_no_skcomm_mount_when_dir_missing(self, mock_docker_client):
|
|
777
|
+
mock_client, mock_container = mock_docker_client
|
|
778
|
+
provider = DockerProvider(
|
|
779
|
+
base_image="python:3.12-slim",
|
|
780
|
+
network_name="skcapstone",
|
|
781
|
+
skcomm_home="/nonexistent/skcomm",
|
|
782
|
+
)
|
|
783
|
+
spec = _make_spec()
|
|
784
|
+
with patch.object(provider, "_client", return_value=mock_client):
|
|
785
|
+
provider.provision("agent-x", spec, "team-y")
|
|
786
|
+
|
|
787
|
+
kwargs = mock_client.containers.create.call_args[1]
|
|
788
|
+
volumes = kwargs["volumes"]
|
|
789
|
+
assert "/nonexistent/skcomm" not in volumes
|
|
790
|
+
|
|
791
|
+
def test_mcp_socket_env_injected_when_socket_missing(self, mock_docker_client):
|
|
792
|
+
"""SKCAPSTONE_MCP_SOCKET env is set regardless; socket mounted only if exists."""
|
|
793
|
+
mock_client, mock_container = mock_docker_client
|
|
794
|
+
provider = DockerProvider(
|
|
795
|
+
base_image="python:3.12-slim",
|
|
796
|
+
network_name="skcapstone",
|
|
797
|
+
mcp_socket_path="/run/skcapstone/mcp.sock",
|
|
798
|
+
)
|
|
799
|
+
spec = _make_spec()
|
|
800
|
+
with patch.object(provider, "_client", return_value=mock_client):
|
|
801
|
+
provider.provision("agent-x", spec, "team-y")
|
|
802
|
+
|
|
803
|
+
kwargs = mock_client.containers.create.call_args[1]
|
|
804
|
+
env = kwargs["environment"]
|
|
805
|
+
# Socket path env always set; actual mount conditional on file existence
|
|
806
|
+
assert "SKCAPSTONE_MCP_SOCKET" in env
|
|
807
|
+
|
|
808
|
+
def test_soul_blueprint_in_env_on_provision(self, mock_docker_client):
|
|
809
|
+
mock_client, mock_container = mock_docker_client
|
|
810
|
+
provider = DockerProvider(
|
|
811
|
+
base_image="python:3.12-slim",
|
|
812
|
+
network_name="skcapstone",
|
|
813
|
+
)
|
|
814
|
+
spec = _make_spec(soul_blueprint="souls/sentinel.yaml")
|
|
815
|
+
with patch.object(provider, "_client", return_value=mock_client):
|
|
816
|
+
provider.provision("sentinel-1", spec, "ops-team")
|
|
817
|
+
|
|
818
|
+
kwargs = mock_client.containers.create.call_args[1]
|
|
819
|
+
env = kwargs["environment"]
|
|
820
|
+
assert env.get("SOUL_BLUEPRINT") == "souls/sentinel.yaml"
|
|
821
|
+
|
|
822
|
+
|
|
823
|
+
# ---------------------------------------------------------------------------
|
|
824
|
+
# rotate()
|
|
825
|
+
# ---------------------------------------------------------------------------
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
class TestRotate:
|
|
829
|
+
"""Tests for DockerProvider.rotate()."""
|
|
830
|
+
|
|
831
|
+
def test_rotate_calls_destroy_then_provision_configure_start(self, provider):
|
|
832
|
+
spec = _make_spec()
|
|
833
|
+
old_pr = _provision_result("my-agent")
|
|
834
|
+
old_pr["team_name"] = "my-team"
|
|
835
|
+
|
|
836
|
+
new_pr = {
|
|
837
|
+
"container_id": "new-id",
|
|
838
|
+
"container_name": "my-agent",
|
|
839
|
+
"host": "my-agent",
|
|
840
|
+
"volume_name": "skcapstone-agent-my-agent",
|
|
841
|
+
"team_name": "my-team",
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
with (
|
|
845
|
+
patch.object(provider, "destroy", return_value=True) as mock_destroy,
|
|
846
|
+
patch.object(provider, "provision", return_value=new_pr) as mock_provision,
|
|
847
|
+
patch.object(provider, "configure", return_value=True) as mock_configure,
|
|
848
|
+
patch.object(provider, "start", return_value=True) as mock_start,
|
|
849
|
+
):
|
|
850
|
+
result = provider.rotate("my-agent", spec, old_pr)
|
|
851
|
+
|
|
852
|
+
mock_destroy.assert_called_once_with("my-agent", old_pr)
|
|
853
|
+
mock_provision.assert_called_once_with("my-agent", spec, "my-team")
|
|
854
|
+
mock_configure.assert_called_once_with("my-agent", spec, new_pr)
|
|
855
|
+
mock_start.assert_called_once_with("my-agent", new_pr)
|
|
856
|
+
assert result == new_pr
|
|
857
|
+
|
|
858
|
+
def test_rotate_preserves_team_name(self, provider):
|
|
859
|
+
spec = _make_spec()
|
|
860
|
+
old_pr = _provision_result("agent-x")
|
|
861
|
+
old_pr["team_name"] = "research-team"
|
|
862
|
+
|
|
863
|
+
captured: dict = {}
|
|
864
|
+
|
|
865
|
+
def fake_provision(name, s, team):
|
|
866
|
+
captured["team"] = team
|
|
867
|
+
return {**old_pr, "container_id": "new-id"}
|
|
868
|
+
|
|
869
|
+
with (
|
|
870
|
+
patch.object(provider, "destroy", return_value=True),
|
|
871
|
+
patch.object(provider, "provision", side_effect=fake_provision),
|
|
872
|
+
patch.object(provider, "configure", return_value=True),
|
|
873
|
+
patch.object(provider, "start", return_value=True),
|
|
874
|
+
):
|
|
875
|
+
provider.rotate("agent-x", spec, old_pr)
|
|
876
|
+
|
|
877
|
+
assert captured["team"] == "research-team"
|
|
878
|
+
|
|
879
|
+
def test_rotate_returns_new_provision_result(self, provider):
|
|
880
|
+
spec = _make_spec()
|
|
881
|
+
old_pr = _provision_result("agent-z")
|
|
882
|
+
old_pr["team_name"] = "t"
|
|
883
|
+
new_pr = {**old_pr, "container_id": "brand-new"}
|
|
884
|
+
|
|
885
|
+
with (
|
|
886
|
+
patch.object(provider, "destroy", return_value=True),
|
|
887
|
+
patch.object(provider, "provision", return_value=new_pr),
|
|
888
|
+
patch.object(provider, "configure", return_value=True),
|
|
889
|
+
patch.object(provider, "start", return_value=True),
|
|
890
|
+
):
|
|
891
|
+
result = provider.rotate("agent-z", spec, old_pr)
|
|
892
|
+
|
|
893
|
+
assert result["container_id"] == "brand-new"
|
|
894
|
+
|
|
895
|
+
|
|
896
|
+
# ---------------------------------------------------------------------------
|
|
897
|
+
# generate_compose() — MCP service + SKComm volume
|
|
898
|
+
# ---------------------------------------------------------------------------
|
|
899
|
+
|
|
900
|
+
|
|
901
|
+
class TestGenerateComposeSovereignExtensions:
|
|
902
|
+
"""Tests for SKComm/MCP extensions in generate_compose()."""
|
|
903
|
+
|
|
904
|
+
def test_mcp_service_added_when_requested(self, provider):
|
|
905
|
+
bp = _make_blueprint(agent_count=1)
|
|
906
|
+
output = provider.generate_compose(bp, include_mcp_service=True)
|
|
907
|
+
parsed = yaml.safe_load(output)
|
|
908
|
+
assert "skcapstone-mcp" in parsed["services"]
|
|
909
|
+
|
|
910
|
+
def test_agents_depend_on_mcp_service_when_included(self, provider):
|
|
911
|
+
bp = _make_blueprint(agent_count=1)
|
|
912
|
+
output = provider.generate_compose(bp, include_mcp_service=True)
|
|
913
|
+
parsed = yaml.safe_load(output)
|
|
914
|
+
agent_svcs = [k for k in parsed["services"] if k != "skcapstone-mcp"]
|
|
915
|
+
for svc_name in agent_svcs:
|
|
916
|
+
assert "skcapstone-mcp" in parsed["services"][svc_name].get(
|
|
917
|
+
"depends_on", []
|
|
918
|
+
)
|
|
919
|
+
|
|
920
|
+
def test_mcp_host_env_set_on_agents_when_mcp_service_included(self, provider):
|
|
921
|
+
bp = _make_blueprint(agent_count=1)
|
|
922
|
+
output = provider.generate_compose(bp, include_mcp_service=True)
|
|
923
|
+
parsed = yaml.safe_load(output)
|
|
924
|
+
agent_svcs = [k for k in parsed["services"] if k != "skcapstone-mcp"]
|
|
925
|
+
for svc_name in agent_svcs:
|
|
926
|
+
env = parsed["services"][svc_name]["environment"]
|
|
927
|
+
assert "SKCAPSTONE_MCP_HOST" in env
|
|
928
|
+
|
|
929
|
+
def test_no_mcp_service_by_default(self, provider):
|
|
930
|
+
bp = _make_blueprint(agent_count=1)
|
|
931
|
+
output = provider.generate_compose(bp)
|
|
932
|
+
parsed = yaml.safe_load(output)
|
|
933
|
+
assert "skcapstone-mcp" not in parsed["services"]
|
|
934
|
+
|
|
935
|
+
def test_skcomm_volume_in_compose_when_configured(self, tmp_path):
|
|
936
|
+
skcomm_dir = tmp_path / "skcomm"
|
|
937
|
+
skcomm_dir.mkdir()
|
|
938
|
+
provider = DockerProvider(
|
|
939
|
+
base_image="python:3.12-slim",
|
|
940
|
+
network_name="skcapstone",
|
|
941
|
+
skcomm_home=str(skcomm_dir),
|
|
942
|
+
)
|
|
943
|
+
bp = _make_blueprint(agent_count=1)
|
|
944
|
+
output = provider.generate_compose(bp)
|
|
945
|
+
parsed = yaml.safe_load(output)
|
|
946
|
+
assert "skcomm-data" in parsed.get("volumes", {})
|
|
947
|
+
|
|
948
|
+
def test_skcomm_env_on_agents_when_configured(self, tmp_path):
|
|
949
|
+
skcomm_dir = tmp_path / "skcomm"
|
|
950
|
+
skcomm_dir.mkdir()
|
|
951
|
+
provider = DockerProvider(
|
|
952
|
+
base_image="python:3.12-slim",
|
|
953
|
+
network_name="skcapstone",
|
|
954
|
+
skcomm_home=str(skcomm_dir),
|
|
955
|
+
)
|
|
956
|
+
bp = _make_blueprint(agent_count=1)
|
|
957
|
+
output = provider.generate_compose(bp)
|
|
958
|
+
parsed = yaml.safe_load(output)
|
|
959
|
+
for svc in parsed["services"].values():
|
|
960
|
+
assert svc["environment"].get("SKCOMM_HOME") == "/skcomm"
|
|
961
|
+
|
|
962
|
+
def test_mcp_service_volume_included(self, provider):
|
|
963
|
+
bp = _make_blueprint(agent_count=1)
|
|
964
|
+
output = provider.generate_compose(bp, include_mcp_service=True)
|
|
965
|
+
parsed = yaml.safe_load(output)
|
|
966
|
+
assert "skcapstone-mcp-data" in parsed["volumes"]
|