@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,522 @@
|
|
|
1
|
+
"""Tests for AWS EC2 and GCP Compute cloud provider adapters.
|
|
2
|
+
|
|
3
|
+
All cloud API calls are mocked — no real infrastructure required.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from typing import Any, Dict
|
|
10
|
+
from unittest.mock import MagicMock, patch, PropertyMock
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
from skcapstone.blueprints.schema import AgentRole, AgentSpec, ModelTier, ResourceSpec
|
|
15
|
+
from skcapstone.providers.cloud import (
|
|
16
|
+
AWSAdapter,
|
|
17
|
+
CloudProvider,
|
|
18
|
+
GCPAdapter,
|
|
19
|
+
HetznerAdapter,
|
|
20
|
+
_CLOUD_ADAPTERS,
|
|
21
|
+
_build_cloud_init,
|
|
22
|
+
_memory_to_ec2_instance_type,
|
|
23
|
+
_memory_to_gcp_machine_type,
|
|
24
|
+
_memory_to_hetzner_type,
|
|
25
|
+
)
|
|
26
|
+
from skcapstone.team_engine import AgentStatus
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
# Helpers
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _make_spec(
|
|
35
|
+
role: str = "worker",
|
|
36
|
+
model: str = "fast",
|
|
37
|
+
memory: str = "4g",
|
|
38
|
+
cores: int = 2,
|
|
39
|
+
skills: list | None = None,
|
|
40
|
+
) -> AgentSpec:
|
|
41
|
+
"""Build a minimal AgentSpec for testing."""
|
|
42
|
+
return AgentSpec(
|
|
43
|
+
role=AgentRole(role),
|
|
44
|
+
model=ModelTier(model),
|
|
45
|
+
resources=ResourceSpec(memory=memory, cores=cores),
|
|
46
|
+
skills=skills or [],
|
|
47
|
+
env={},
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
# Instance type mapping
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class TestMemoryToEC2InstanceType:
|
|
57
|
+
"""Tests for _memory_to_ec2_instance_type."""
|
|
58
|
+
|
|
59
|
+
def test_micro_for_1g(self):
|
|
60
|
+
assert _memory_to_ec2_instance_type("1g", 1) == "t3.micro"
|
|
61
|
+
|
|
62
|
+
def test_small_for_2g(self):
|
|
63
|
+
assert _memory_to_ec2_instance_type("2g", 2) == "t3.small"
|
|
64
|
+
|
|
65
|
+
def test_medium_for_4g(self):
|
|
66
|
+
assert _memory_to_ec2_instance_type("4g", 2) == "t3.medium"
|
|
67
|
+
|
|
68
|
+
def test_large_for_8g(self):
|
|
69
|
+
assert _memory_to_ec2_instance_type("8g", 2) == "t3.large"
|
|
70
|
+
|
|
71
|
+
def test_xlarge_for_16g(self):
|
|
72
|
+
assert _memory_to_ec2_instance_type("16g", 4) == "t3.xlarge"
|
|
73
|
+
|
|
74
|
+
def test_2xlarge_for_large_memory(self):
|
|
75
|
+
assert _memory_to_ec2_instance_type("64g", 16) == "t3.2xlarge"
|
|
76
|
+
|
|
77
|
+
def test_megabytes_converted(self):
|
|
78
|
+
assert _memory_to_ec2_instance_type("512m", 1) == "t3.micro"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class TestMemoryToGCPMachineType:
|
|
82
|
+
"""Tests for _memory_to_gcp_machine_type."""
|
|
83
|
+
|
|
84
|
+
def test_micro_for_1g(self):
|
|
85
|
+
assert _memory_to_gcp_machine_type("1g", 1) == "e2-micro"
|
|
86
|
+
|
|
87
|
+
def test_small_for_2g(self):
|
|
88
|
+
assert _memory_to_gcp_machine_type("2g", 1) == "e2-small"
|
|
89
|
+
|
|
90
|
+
def test_medium_for_4g(self):
|
|
91
|
+
assert _memory_to_gcp_machine_type("4g", 2) == "e2-medium"
|
|
92
|
+
|
|
93
|
+
def test_standard_2_for_8g(self):
|
|
94
|
+
assert _memory_to_gcp_machine_type("8g", 2) == "e2-standard-2"
|
|
95
|
+
|
|
96
|
+
def test_standard_4_for_16g(self):
|
|
97
|
+
assert _memory_to_gcp_machine_type("16g", 4) == "e2-standard-4"
|
|
98
|
+
|
|
99
|
+
def test_standard_8_for_large(self):
|
|
100
|
+
assert _memory_to_gcp_machine_type("64g", 16) == "e2-standard-8"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# ---------------------------------------------------------------------------
|
|
104
|
+
# CloudProvider adapter dispatch
|
|
105
|
+
# ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class TestCloudProviderDispatch:
|
|
109
|
+
"""Tests for CloudProvider._get_adapter routing."""
|
|
110
|
+
|
|
111
|
+
def test_hetzner_returns_hetzner_adapter(self):
|
|
112
|
+
with patch.object(HetznerAdapter, "__init__", return_value=None):
|
|
113
|
+
provider = CloudProvider(cloud="hetzner")
|
|
114
|
+
assert isinstance(provider._adapter, HetznerAdapter)
|
|
115
|
+
|
|
116
|
+
def test_aws_returns_aws_adapter(self):
|
|
117
|
+
provider = CloudProvider(cloud="aws")
|
|
118
|
+
assert isinstance(provider._adapter, AWSAdapter)
|
|
119
|
+
|
|
120
|
+
def test_gcp_returns_gcp_adapter(self):
|
|
121
|
+
provider = CloudProvider(cloud="gcp")
|
|
122
|
+
assert isinstance(provider._adapter, GCPAdapter)
|
|
123
|
+
|
|
124
|
+
def test_unknown_cloud_raises(self):
|
|
125
|
+
with pytest.raises(RuntimeError, match="Unknown cloud"):
|
|
126
|
+
CloudProvider(cloud="digitalocean")
|
|
127
|
+
|
|
128
|
+
def test_registered_adapters_include_aws_and_gcp(self):
|
|
129
|
+
assert "aws" in _CLOUD_ADAPTERS
|
|
130
|
+
assert "gcp" in _CLOUD_ADAPTERS
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# ---------------------------------------------------------------------------
|
|
134
|
+
# AWS EC2 Adapter
|
|
135
|
+
# ---------------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class TestAWSAdapterProvision:
|
|
139
|
+
"""Tests for AWSAdapter.provision()."""
|
|
140
|
+
|
|
141
|
+
@pytest.fixture()
|
|
142
|
+
def adapter(self):
|
|
143
|
+
return AWSAdapter(region="us-east-1", ami_id="ami-test123")
|
|
144
|
+
|
|
145
|
+
@pytest.fixture()
|
|
146
|
+
def mock_ec2(self):
|
|
147
|
+
mock = MagicMock()
|
|
148
|
+
mock.run_instances.return_value = {
|
|
149
|
+
"Instances": [{
|
|
150
|
+
"InstanceId": "i-abc123",
|
|
151
|
+
"PublicIpAddress": "1.2.3.4",
|
|
152
|
+
}]
|
|
153
|
+
}
|
|
154
|
+
return mock
|
|
155
|
+
|
|
156
|
+
def test_provision_returns_instance_id(self, adapter, mock_ec2):
|
|
157
|
+
with patch.object(adapter, "_ec2_client", return_value=mock_ec2):
|
|
158
|
+
result = adapter.provision("agent-1", _make_spec(), "team-a")
|
|
159
|
+
assert result["instance_id"] == "i-abc123"
|
|
160
|
+
assert result["host"] == "1.2.3.4"
|
|
161
|
+
|
|
162
|
+
def test_provision_tags_instance(self, adapter, mock_ec2):
|
|
163
|
+
with patch.object(adapter, "_ec2_client", return_value=mock_ec2):
|
|
164
|
+
adapter.provision("agent-1", _make_spec(role="coder"), "team-a")
|
|
165
|
+
call_kwargs = mock_ec2.run_instances.call_args[1]
|
|
166
|
+
tags = call_kwargs["TagSpecifications"][0]["Tags"]
|
|
167
|
+
tag_dict = {t["Key"]: t["Value"] for t in tags}
|
|
168
|
+
assert tag_dict["Name"] == "agent-1"
|
|
169
|
+
assert tag_dict["Role"] == "coder"
|
|
170
|
+
assert tag_dict["ManagedBy"] == "skcapstone"
|
|
171
|
+
|
|
172
|
+
def test_provision_uses_cloud_init(self, adapter, mock_ec2):
|
|
173
|
+
with patch.object(adapter, "_ec2_client", return_value=mock_ec2):
|
|
174
|
+
adapter.provision("agent-1", _make_spec(), "team-a")
|
|
175
|
+
call_kwargs = mock_ec2.run_instances.call_args[1]
|
|
176
|
+
assert "skcapstone" in call_kwargs["UserData"]
|
|
177
|
+
|
|
178
|
+
def test_provision_uses_correct_instance_type(self, adapter, mock_ec2):
|
|
179
|
+
spec = _make_spec(memory="8g", cores=2)
|
|
180
|
+
with patch.object(adapter, "_ec2_client", return_value=mock_ec2):
|
|
181
|
+
adapter.provision("agent-1", spec, "team-a")
|
|
182
|
+
call_kwargs = mock_ec2.run_instances.call_args[1]
|
|
183
|
+
assert call_kwargs["InstanceType"] == "t3.large"
|
|
184
|
+
|
|
185
|
+
def test_provision_waits_for_ip_if_missing(self, adapter):
|
|
186
|
+
mock_ec2 = MagicMock()
|
|
187
|
+
mock_ec2.run_instances.return_value = {
|
|
188
|
+
"Instances": [{"InstanceId": "i-noip"}]
|
|
189
|
+
}
|
|
190
|
+
mock_waiter = MagicMock()
|
|
191
|
+
mock_ec2.get_waiter.return_value = mock_waiter
|
|
192
|
+
mock_ec2.describe_instances.return_value = {
|
|
193
|
+
"Reservations": [{
|
|
194
|
+
"Instances": [{"PublicIpAddress": "5.6.7.8"}]
|
|
195
|
+
}]
|
|
196
|
+
}
|
|
197
|
+
with patch.object(adapter, "_ec2_client", return_value=mock_ec2):
|
|
198
|
+
result = adapter.provision("agent-wait", _make_spec(), "team-a")
|
|
199
|
+
assert result["host"] == "5.6.7.8"
|
|
200
|
+
mock_waiter.wait.assert_called_once()
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class TestAWSAdapterLifecycle:
|
|
204
|
+
"""Tests for AWSAdapter start/stop/destroy/health_check."""
|
|
205
|
+
|
|
206
|
+
@pytest.fixture()
|
|
207
|
+
def adapter(self):
|
|
208
|
+
return AWSAdapter(region="us-east-1")
|
|
209
|
+
|
|
210
|
+
@pytest.fixture()
|
|
211
|
+
def provision_result(self):
|
|
212
|
+
return {"instance_id": "i-test123", "host": "1.2.3.4", "region": "us-east-1"}
|
|
213
|
+
|
|
214
|
+
def test_configure_returns_true(self, adapter):
|
|
215
|
+
assert adapter.configure("a", _make_spec(), {}) is True
|
|
216
|
+
|
|
217
|
+
def test_start_calls_start_instances(self, adapter, provision_result):
|
|
218
|
+
mock_ec2 = MagicMock()
|
|
219
|
+
with patch.object(adapter, "_ec2_client", return_value=mock_ec2):
|
|
220
|
+
result = adapter.start("agent-1", provision_result)
|
|
221
|
+
assert result is True
|
|
222
|
+
mock_ec2.start_instances.assert_called_once_with(InstanceIds=["i-test123"])
|
|
223
|
+
|
|
224
|
+
def test_start_returns_false_without_instance_id(self, adapter):
|
|
225
|
+
assert adapter.start("a", {}) is False
|
|
226
|
+
|
|
227
|
+
def test_stop_calls_stop_instances(self, adapter, provision_result):
|
|
228
|
+
mock_ec2 = MagicMock()
|
|
229
|
+
with patch.object(adapter, "_ec2_client", return_value=mock_ec2):
|
|
230
|
+
result = adapter.stop("agent-1", provision_result)
|
|
231
|
+
assert result is True
|
|
232
|
+
mock_ec2.stop_instances.assert_called_once_with(InstanceIds=["i-test123"])
|
|
233
|
+
|
|
234
|
+
def test_stop_returns_false_without_instance_id(self, adapter):
|
|
235
|
+
assert adapter.stop("a", {}) is False
|
|
236
|
+
|
|
237
|
+
def test_destroy_calls_terminate_instances(self, adapter, provision_result):
|
|
238
|
+
mock_ec2 = MagicMock()
|
|
239
|
+
with patch.object(adapter, "_ec2_client", return_value=mock_ec2):
|
|
240
|
+
result = adapter.destroy("agent-1", provision_result)
|
|
241
|
+
assert result is True
|
|
242
|
+
mock_ec2.terminate_instances.assert_called_once_with(InstanceIds=["i-test123"])
|
|
243
|
+
|
|
244
|
+
def test_destroy_returns_false_without_instance_id(self, adapter):
|
|
245
|
+
assert adapter.destroy("a", {}) is False
|
|
246
|
+
|
|
247
|
+
def test_destroy_returns_false_on_error(self, adapter, provision_result):
|
|
248
|
+
mock_ec2 = MagicMock()
|
|
249
|
+
mock_ec2.terminate_instances.side_effect = Exception("access denied")
|
|
250
|
+
with patch.object(adapter, "_ec2_client", return_value=mock_ec2):
|
|
251
|
+
result = adapter.destroy("agent-1", provision_result)
|
|
252
|
+
assert result is False
|
|
253
|
+
|
|
254
|
+
def test_health_running(self, adapter, provision_result):
|
|
255
|
+
mock_ec2 = MagicMock()
|
|
256
|
+
mock_ec2.describe_instances.return_value = {
|
|
257
|
+
"Reservations": [{"Instances": [{"State": {"Name": "running"}}]}]
|
|
258
|
+
}
|
|
259
|
+
with patch.object(adapter, "_ec2_client", return_value=mock_ec2):
|
|
260
|
+
status = adapter.health_check("agent-1", provision_result)
|
|
261
|
+
assert status == AgentStatus.RUNNING
|
|
262
|
+
|
|
263
|
+
def test_health_stopped(self, adapter, provision_result):
|
|
264
|
+
mock_ec2 = MagicMock()
|
|
265
|
+
mock_ec2.describe_instances.return_value = {
|
|
266
|
+
"Reservations": [{"Instances": [{"State": {"Name": "stopped"}}]}]
|
|
267
|
+
}
|
|
268
|
+
with patch.object(adapter, "_ec2_client", return_value=mock_ec2):
|
|
269
|
+
status = adapter.health_check("agent-1", provision_result)
|
|
270
|
+
assert status == AgentStatus.STOPPED
|
|
271
|
+
|
|
272
|
+
def test_health_terminated(self, adapter, provision_result):
|
|
273
|
+
mock_ec2 = MagicMock()
|
|
274
|
+
mock_ec2.describe_instances.return_value = {
|
|
275
|
+
"Reservations": [{"Instances": [{"State": {"Name": "terminated"}}]}]
|
|
276
|
+
}
|
|
277
|
+
with patch.object(adapter, "_ec2_client", return_value=mock_ec2):
|
|
278
|
+
status = adapter.health_check("agent-1", provision_result)
|
|
279
|
+
assert status == AgentStatus.STOPPED
|
|
280
|
+
|
|
281
|
+
def test_health_pending_is_degraded(self, adapter, provision_result):
|
|
282
|
+
mock_ec2 = MagicMock()
|
|
283
|
+
mock_ec2.describe_instances.return_value = {
|
|
284
|
+
"Reservations": [{"Instances": [{"State": {"Name": "pending"}}]}]
|
|
285
|
+
}
|
|
286
|
+
with patch.object(adapter, "_ec2_client", return_value=mock_ec2):
|
|
287
|
+
status = adapter.health_check("agent-1", provision_result)
|
|
288
|
+
assert status == AgentStatus.DEGRADED
|
|
289
|
+
|
|
290
|
+
def test_health_no_reservations_returns_stopped(self, adapter, provision_result):
|
|
291
|
+
mock_ec2 = MagicMock()
|
|
292
|
+
mock_ec2.describe_instances.return_value = {"Reservations": []}
|
|
293
|
+
with patch.object(adapter, "_ec2_client", return_value=mock_ec2):
|
|
294
|
+
status = adapter.health_check("agent-1", provision_result)
|
|
295
|
+
assert status == AgentStatus.STOPPED
|
|
296
|
+
|
|
297
|
+
def test_health_api_error_returns_failed(self, adapter, provision_result):
|
|
298
|
+
mock_ec2 = MagicMock()
|
|
299
|
+
mock_ec2.describe_instances.side_effect = Exception("timeout")
|
|
300
|
+
with patch.object(adapter, "_ec2_client", return_value=mock_ec2):
|
|
301
|
+
status = adapter.health_check("agent-1", provision_result)
|
|
302
|
+
assert status == AgentStatus.FAILED
|
|
303
|
+
|
|
304
|
+
def test_health_no_instance_id_returns_stopped(self, adapter):
|
|
305
|
+
assert adapter.health_check("a", {}) == AgentStatus.STOPPED
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
class TestAWSAdapterAMI:
|
|
309
|
+
"""Tests for AWS AMI resolution."""
|
|
310
|
+
|
|
311
|
+
def test_explicit_ami_used(self):
|
|
312
|
+
adapter = AWSAdapter(ami_id="ami-custom")
|
|
313
|
+
assert adapter._resolve_ami() == "ami-custom"
|
|
314
|
+
|
|
315
|
+
def test_default_ami_for_known_region(self):
|
|
316
|
+
adapter = AWSAdapter(region="us-east-1")
|
|
317
|
+
ami = adapter._resolve_ami()
|
|
318
|
+
assert ami.startswith("ami-")
|
|
319
|
+
|
|
320
|
+
def test_unknown_region_raises(self):
|
|
321
|
+
adapter = AWSAdapter(region="ap-southeast-99")
|
|
322
|
+
with pytest.raises(RuntimeError, match="No default AMI"):
|
|
323
|
+
adapter._resolve_ami()
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
# ---------------------------------------------------------------------------
|
|
327
|
+
# GCP Compute Adapter
|
|
328
|
+
# ---------------------------------------------------------------------------
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
class TestGCPAdapterProvision:
|
|
332
|
+
"""Tests for GCPAdapter.provision()."""
|
|
333
|
+
|
|
334
|
+
@pytest.fixture()
|
|
335
|
+
def adapter(self):
|
|
336
|
+
return GCPAdapter(project="test-project", zone="us-central1-a")
|
|
337
|
+
|
|
338
|
+
@pytest.fixture()
|
|
339
|
+
def _mock_compute_v1(self):
|
|
340
|
+
"""Mock the google.cloud.compute_v1 module so provision() can import it."""
|
|
341
|
+
mock_mod = MagicMock()
|
|
342
|
+
with patch.dict("sys.modules", {"google": MagicMock(), "google.cloud": MagicMock(), "google.cloud.compute_v1": mock_mod}):
|
|
343
|
+
yield mock_mod
|
|
344
|
+
|
|
345
|
+
def test_provision_calls_insert(self, adapter, _mock_compute_v1):
|
|
346
|
+
mock_client = MagicMock()
|
|
347
|
+
mock_op = MagicMock()
|
|
348
|
+
mock_client.insert.return_value = mock_op
|
|
349
|
+
|
|
350
|
+
# Mock get for fetching IP
|
|
351
|
+
mock_inst = MagicMock()
|
|
352
|
+
mock_iface = MagicMock()
|
|
353
|
+
mock_ac = MagicMock()
|
|
354
|
+
mock_ac.nat_i_p = "10.0.0.1"
|
|
355
|
+
mock_iface.access_configs = [mock_ac]
|
|
356
|
+
mock_inst.network_interfaces = [mock_iface]
|
|
357
|
+
mock_client.get.return_value = mock_inst
|
|
358
|
+
|
|
359
|
+
with patch.object(adapter, "_compute_client", return_value=mock_client):
|
|
360
|
+
with patch.object(adapter, "_get_source_image", return_value="projects/debian-cloud/global/images/debian-12"):
|
|
361
|
+
result = adapter.provision("agent-gcp", _make_spec(), "team-g")
|
|
362
|
+
|
|
363
|
+
assert result["instance_name"] == "agent-gcp"
|
|
364
|
+
assert result["host"] == "10.0.0.1"
|
|
365
|
+
assert result["project"] == "test-project"
|
|
366
|
+
mock_client.insert.assert_called_once()
|
|
367
|
+
|
|
368
|
+
def test_provision_no_project_raises(self, _mock_compute_v1):
|
|
369
|
+
adapter = GCPAdapter(project="")
|
|
370
|
+
adapter._project = ""
|
|
371
|
+
with pytest.raises(RuntimeError, match="project not configured"):
|
|
372
|
+
adapter.provision("agent", _make_spec(), "team")
|
|
373
|
+
|
|
374
|
+
def test_provision_normalizes_instance_name(self, adapter, _mock_compute_v1):
|
|
375
|
+
mock_client = MagicMock()
|
|
376
|
+
mock_op = MagicMock()
|
|
377
|
+
mock_client.insert.return_value = mock_op
|
|
378
|
+
mock_client.get.side_effect = Exception("skip")
|
|
379
|
+
|
|
380
|
+
with patch.object(adapter, "_compute_client", return_value=mock_client):
|
|
381
|
+
with patch.object(adapter, "_get_source_image", return_value="image"):
|
|
382
|
+
result = adapter.provision("Agent_Name_1", _make_spec(), "team")
|
|
383
|
+
|
|
384
|
+
assert result["instance_name"] == "agent-name-1"
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
class TestGCPAdapterLifecycle:
|
|
388
|
+
"""Tests for GCPAdapter start/stop/destroy/health_check."""
|
|
389
|
+
|
|
390
|
+
@pytest.fixture()
|
|
391
|
+
def adapter(self):
|
|
392
|
+
return GCPAdapter(project="test-project", zone="us-central1-a")
|
|
393
|
+
|
|
394
|
+
@pytest.fixture()
|
|
395
|
+
def provision_result(self):
|
|
396
|
+
return {
|
|
397
|
+
"instance_name": "agent-gcp",
|
|
398
|
+
"host": "10.0.0.1",
|
|
399
|
+
"zone": "us-central1-a",
|
|
400
|
+
"project": "test-project",
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
def test_configure_returns_true(self, adapter):
|
|
404
|
+
assert adapter.configure("a", _make_spec(), {}) is True
|
|
405
|
+
|
|
406
|
+
def test_start_calls_client_start(self, adapter, provision_result):
|
|
407
|
+
mock_client = MagicMock()
|
|
408
|
+
mock_op = MagicMock()
|
|
409
|
+
mock_client.start.return_value = mock_op
|
|
410
|
+
with patch.object(adapter, "_compute_client", return_value=mock_client):
|
|
411
|
+
result = adapter.start("agent-gcp", provision_result)
|
|
412
|
+
assert result is True
|
|
413
|
+
mock_client.start.assert_called_once_with(
|
|
414
|
+
project="test-project",
|
|
415
|
+
zone="us-central1-a",
|
|
416
|
+
instance="agent-gcp",
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
def test_start_returns_false_without_instance_name(self, adapter):
|
|
420
|
+
assert adapter.start("a", {}) is False
|
|
421
|
+
|
|
422
|
+
def test_stop_calls_client_stop(self, adapter, provision_result):
|
|
423
|
+
mock_client = MagicMock()
|
|
424
|
+
mock_op = MagicMock()
|
|
425
|
+
mock_client.stop.return_value = mock_op
|
|
426
|
+
with patch.object(adapter, "_compute_client", return_value=mock_client):
|
|
427
|
+
result = adapter.stop("agent-gcp", provision_result)
|
|
428
|
+
assert result is True
|
|
429
|
+
mock_client.stop.assert_called_once()
|
|
430
|
+
|
|
431
|
+
def test_stop_returns_false_without_instance_name(self, adapter):
|
|
432
|
+
assert adapter.stop("a", {}) is False
|
|
433
|
+
|
|
434
|
+
def test_destroy_calls_client_delete(self, adapter, provision_result):
|
|
435
|
+
mock_client = MagicMock()
|
|
436
|
+
mock_op = MagicMock()
|
|
437
|
+
mock_client.delete.return_value = mock_op
|
|
438
|
+
with patch.object(adapter, "_compute_client", return_value=mock_client):
|
|
439
|
+
result = adapter.destroy("agent-gcp", provision_result)
|
|
440
|
+
assert result is True
|
|
441
|
+
mock_client.delete.assert_called_once()
|
|
442
|
+
|
|
443
|
+
def test_destroy_returns_false_without_instance_name(self, adapter):
|
|
444
|
+
assert adapter.destroy("a", {}) is False
|
|
445
|
+
|
|
446
|
+
def test_destroy_returns_false_on_error(self, adapter, provision_result):
|
|
447
|
+
mock_client = MagicMock()
|
|
448
|
+
mock_client.delete.side_effect = Exception("permission denied")
|
|
449
|
+
with patch.object(adapter, "_compute_client", return_value=mock_client):
|
|
450
|
+
result = adapter.destroy("agent-gcp", provision_result)
|
|
451
|
+
assert result is False
|
|
452
|
+
|
|
453
|
+
def test_health_running(self, adapter, provision_result):
|
|
454
|
+
mock_client = MagicMock()
|
|
455
|
+
mock_inst = MagicMock()
|
|
456
|
+
mock_inst.status = "RUNNING"
|
|
457
|
+
mock_client.get.return_value = mock_inst
|
|
458
|
+
with patch.object(adapter, "_compute_client", return_value=mock_client):
|
|
459
|
+
status = adapter.health_check("agent-gcp", provision_result)
|
|
460
|
+
assert status == AgentStatus.RUNNING
|
|
461
|
+
|
|
462
|
+
def test_health_terminated(self, adapter, provision_result):
|
|
463
|
+
mock_client = MagicMock()
|
|
464
|
+
mock_inst = MagicMock()
|
|
465
|
+
mock_inst.status = "TERMINATED"
|
|
466
|
+
mock_client.get.return_value = mock_inst
|
|
467
|
+
with patch.object(adapter, "_compute_client", return_value=mock_client):
|
|
468
|
+
status = adapter.health_check("agent-gcp", provision_result)
|
|
469
|
+
assert status == AgentStatus.STOPPED
|
|
470
|
+
|
|
471
|
+
def test_health_staging_is_degraded(self, adapter, provision_result):
|
|
472
|
+
mock_client = MagicMock()
|
|
473
|
+
mock_inst = MagicMock()
|
|
474
|
+
mock_inst.status = "STAGING"
|
|
475
|
+
mock_client.get.return_value = mock_inst
|
|
476
|
+
with patch.object(adapter, "_compute_client", return_value=mock_client):
|
|
477
|
+
status = adapter.health_check("agent-gcp", provision_result)
|
|
478
|
+
assert status == AgentStatus.DEGRADED
|
|
479
|
+
|
|
480
|
+
def test_health_api_error_returns_failed(self, adapter, provision_result):
|
|
481
|
+
mock_client = MagicMock()
|
|
482
|
+
mock_client.get.side_effect = Exception("timeout")
|
|
483
|
+
with patch.object(adapter, "_compute_client", return_value=mock_client):
|
|
484
|
+
status = adapter.health_check("agent-gcp", provision_result)
|
|
485
|
+
assert status == AgentStatus.FAILED
|
|
486
|
+
|
|
487
|
+
def test_health_no_instance_name_returns_stopped(self, adapter):
|
|
488
|
+
assert adapter.health_check("a", {}) == AgentStatus.STOPPED
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
# ---------------------------------------------------------------------------
|
|
492
|
+
# Cloud-init template
|
|
493
|
+
# ---------------------------------------------------------------------------
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
class TestBuildCloudInit:
|
|
497
|
+
"""Tests for _build_cloud_init shared template."""
|
|
498
|
+
|
|
499
|
+
def test_contains_agent_name(self):
|
|
500
|
+
spec = _make_spec()
|
|
501
|
+
ci = _build_cloud_init("my-agent", spec)
|
|
502
|
+
assert "my-agent" in ci
|
|
503
|
+
|
|
504
|
+
def test_contains_skcapstone_install(self):
|
|
505
|
+
spec = _make_spec()
|
|
506
|
+
ci = _build_cloud_init("agent", spec)
|
|
507
|
+
assert "pip3 install skcapstone" in ci
|
|
508
|
+
|
|
509
|
+
def test_contains_tailscale(self):
|
|
510
|
+
spec = _make_spec()
|
|
511
|
+
ci = _build_cloud_init("agent", spec)
|
|
512
|
+
assert "tailscale" in ci
|
|
513
|
+
|
|
514
|
+
def test_contains_role(self):
|
|
515
|
+
spec = _make_spec(role="coder")
|
|
516
|
+
ci = _build_cloud_init("agent", spec)
|
|
517
|
+
assert "coder" in ci
|
|
518
|
+
|
|
519
|
+
def test_is_cloud_config(self):
|
|
520
|
+
spec = _make_spec()
|
|
521
|
+
ci = _build_cloud_init("agent", spec)
|
|
522
|
+
assert ci.startswith("#cloud-config")
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Tests for shell tab completion module.
|
|
2
|
+
|
|
3
|
+
Covers:
|
|
4
|
+
- Script generation for bash, zsh, fish
|
|
5
|
+
- Shell detection from environment
|
|
6
|
+
- Install to correct paths
|
|
7
|
+
- Uninstall removes files
|
|
8
|
+
- Invalid shell raises ValueError
|
|
9
|
+
- CLI commands (install, show, uninstall)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from unittest.mock import patch
|
|
17
|
+
|
|
18
|
+
import pytest
|
|
19
|
+
from click.testing import CliRunner
|
|
20
|
+
|
|
21
|
+
from skcapstone.completions import (
|
|
22
|
+
SUPPORTED_SHELLS,
|
|
23
|
+
detect_shell,
|
|
24
|
+
generate_script,
|
|
25
|
+
install_completions,
|
|
26
|
+
uninstall_completions,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TestGenerateScript:
|
|
31
|
+
"""Test completion script generation."""
|
|
32
|
+
|
|
33
|
+
def test_bash_script(self):
|
|
34
|
+
"""Bash script contains the correct env var."""
|
|
35
|
+
script = generate_script("bash")
|
|
36
|
+
assert "_SKCAPSTONE_COMPLETE=bash_source" in script
|
|
37
|
+
assert "skcapstone" in script
|
|
38
|
+
|
|
39
|
+
def test_zsh_script(self):
|
|
40
|
+
"""Zsh script contains the correct env var."""
|
|
41
|
+
script = generate_script("zsh")
|
|
42
|
+
assert "_SKCAPSTONE_COMPLETE=zsh_source" in script
|
|
43
|
+
|
|
44
|
+
def test_fish_script(self):
|
|
45
|
+
"""Fish script contains the correct env var."""
|
|
46
|
+
script = generate_script("fish")
|
|
47
|
+
assert "_SKCAPSTONE_COMPLETE=fish_source" in script
|
|
48
|
+
|
|
49
|
+
def test_unsupported_shell(self):
|
|
50
|
+
"""Unsupported shell raises ValueError."""
|
|
51
|
+
with pytest.raises(ValueError, match="Unsupported"):
|
|
52
|
+
generate_script("powershell")
|
|
53
|
+
|
|
54
|
+
def test_all_shells_generate(self):
|
|
55
|
+
"""Every supported shell produces a non-empty script."""
|
|
56
|
+
for shell in SUPPORTED_SHELLS:
|
|
57
|
+
script = generate_script(shell)
|
|
58
|
+
assert len(script) > 10
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class TestDetectShell:
|
|
62
|
+
"""Test shell auto-detection."""
|
|
63
|
+
|
|
64
|
+
def test_detect_bash(self):
|
|
65
|
+
"""Detects bash from SHELL env."""
|
|
66
|
+
with patch.dict(os.environ, {"SHELL": "/bin/bash"}):
|
|
67
|
+
assert detect_shell() == "bash"
|
|
68
|
+
|
|
69
|
+
def test_detect_zsh(self):
|
|
70
|
+
"""Detects zsh from SHELL env."""
|
|
71
|
+
with patch.dict(os.environ, {"SHELL": "/usr/bin/zsh"}):
|
|
72
|
+
assert detect_shell() == "zsh"
|
|
73
|
+
|
|
74
|
+
def test_detect_fish(self):
|
|
75
|
+
"""Detects fish from SHELL env."""
|
|
76
|
+
with patch.dict(os.environ, {"SHELL": "/usr/bin/fish"}):
|
|
77
|
+
assert detect_shell() == "fish"
|
|
78
|
+
|
|
79
|
+
def test_unknown_shell(self):
|
|
80
|
+
"""Returns None for unknown shells."""
|
|
81
|
+
with patch.dict(os.environ, {"SHELL": "/bin/csh"}):
|
|
82
|
+
assert detect_shell() is None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class TestInstall:
|
|
86
|
+
"""Test completion installation."""
|
|
87
|
+
|
|
88
|
+
def test_install_bash(self, tmp_path):
|
|
89
|
+
"""Install creates bash completion file."""
|
|
90
|
+
with patch("skcapstone.completions.INSTALL_PATHS",
|
|
91
|
+
{"bash": tmp_path / "skcapstone.bash-completion"}), \
|
|
92
|
+
patch("skcapstone.completions.RC_MARKERS", {}):
|
|
93
|
+
result = install_completions(shell="bash")
|
|
94
|
+
|
|
95
|
+
assert result["success"]
|
|
96
|
+
assert result["shell"] == "bash"
|
|
97
|
+
assert (tmp_path / "skcapstone.bash-completion").exists()
|
|
98
|
+
|
|
99
|
+
def test_install_auto_detect_fails(self):
|
|
100
|
+
"""Install fails gracefully when shell can't be detected."""
|
|
101
|
+
with patch.dict(os.environ, {"SHELL": "/bin/unknown"}):
|
|
102
|
+
result = install_completions(shell=None)
|
|
103
|
+
assert not result["success"]
|
|
104
|
+
assert "error" in result
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class TestUninstall:
|
|
108
|
+
"""Test completion removal."""
|
|
109
|
+
|
|
110
|
+
def test_uninstall_removes_file(self, tmp_path):
|
|
111
|
+
"""Uninstall removes the completion script."""
|
|
112
|
+
script = tmp_path / "skcapstone.bash-completion"
|
|
113
|
+
script.write_text("# completion")
|
|
114
|
+
|
|
115
|
+
with patch("skcapstone.completions.INSTALL_PATHS",
|
|
116
|
+
{"bash": script}):
|
|
117
|
+
result = uninstall_completions(shell="bash")
|
|
118
|
+
|
|
119
|
+
assert not script.exists()
|
|
120
|
+
assert len(result["removed"]) == 1
|
|
121
|
+
|
|
122
|
+
def test_uninstall_no_files(self, tmp_path):
|
|
123
|
+
"""Uninstall on empty is a no-op."""
|
|
124
|
+
with patch("skcapstone.completions.INSTALL_PATHS",
|
|
125
|
+
{"bash": tmp_path / "nonexistent"}):
|
|
126
|
+
result = uninstall_completions(shell="bash")
|
|
127
|
+
|
|
128
|
+
assert result["removed"] == []
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class TestCLI:
|
|
132
|
+
"""Test CLI commands."""
|
|
133
|
+
|
|
134
|
+
def test_completions_help(self):
|
|
135
|
+
"""completions --help works."""
|
|
136
|
+
from skcapstone.cli import main
|
|
137
|
+
runner = CliRunner()
|
|
138
|
+
result = runner.invoke(main, ["completions", "--help"])
|
|
139
|
+
assert result.exit_code == 0
|
|
140
|
+
assert "install" in result.output
|
|
141
|
+
assert "show" in result.output
|
|
142
|
+
assert "uninstall" in result.output
|
|
143
|
+
|
|
144
|
+
def test_show_bash(self):
|
|
145
|
+
"""completions show --shell bash prints script."""
|
|
146
|
+
from skcapstone.cli import main
|
|
147
|
+
runner = CliRunner()
|
|
148
|
+
result = runner.invoke(main, ["completions", "show", "--shell", "bash"])
|
|
149
|
+
assert result.exit_code == 0
|
|
150
|
+
assert "_SKCAPSTONE_COMPLETE" in result.output
|
|
151
|
+
|
|
152
|
+
def test_show_zsh(self):
|
|
153
|
+
"""completions show --shell zsh prints script."""
|
|
154
|
+
from skcapstone.cli import main
|
|
155
|
+
runner = CliRunner()
|
|
156
|
+
result = runner.invoke(main, ["completions", "show", "--shell", "zsh"])
|
|
157
|
+
assert result.exit_code == 0
|
|
158
|
+
assert "zsh_source" in result.output
|