@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,744 @@
|
|
|
1
|
+
"""Integration tests for the skills-registry subsystem.
|
|
2
|
+
|
|
3
|
+
Covers:
|
|
4
|
+
- discover_skills(): listing skills from local filesystem
|
|
5
|
+
- enable/disable semantics (ACTIVE vs DEGRADED vs MISSING status)
|
|
6
|
+
- skskills_list_tools MCP handler: correct JSON response structure
|
|
7
|
+
- skskills_run_tool MCP handler: executes tools and returns results
|
|
8
|
+
|
|
9
|
+
Run only these tests:
|
|
10
|
+
pytest tests/integration/test_skills_registry.py -v
|
|
11
|
+
|
|
12
|
+
Skip slow integration tests in CI:
|
|
13
|
+
pytest -m "not integration" tests/
|
|
14
|
+
|
|
15
|
+
Related coordination task: f8dfda3493c0ed72
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from textwrap import dedent
|
|
24
|
+
from typing import Any
|
|
25
|
+
from unittest.mock import AsyncMock, MagicMock, patch
|
|
26
|
+
|
|
27
|
+
import pytest
|
|
28
|
+
|
|
29
|
+
# Pre-import to ensure the module stays in sys.modules throughout the test
|
|
30
|
+
# run. patch.dict(sys.modules, ...) snapshots the current state; importing
|
|
31
|
+
# here means the module is included in that snapshot and is never removed
|
|
32
|
+
# between tests (which would trigger a broken pydantic re-import).
|
|
33
|
+
from skcapstone.mcp_tools.skills_tools import (
|
|
34
|
+
HANDLERS,
|
|
35
|
+
TOOLS,
|
|
36
|
+
_handle_skskills_list_tools,
|
|
37
|
+
_handle_skskills_run_tool,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
# Fixtures
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@pytest.fixture
|
|
47
|
+
def skskills_home(tmp_path: Path) -> Path:
|
|
48
|
+
"""Populated SKSkills home directory for integration tests."""
|
|
49
|
+
home = tmp_path / "skskills"
|
|
50
|
+
|
|
51
|
+
# Global installed skills
|
|
52
|
+
for skill_name, version, tags in [
|
|
53
|
+
("syncthing-setup", "1.0.0", ["sync"]),
|
|
54
|
+
("pgp-identity", "0.2.0", ["identity"]),
|
|
55
|
+
]:
|
|
56
|
+
skill_dir = home / "installed" / skill_name
|
|
57
|
+
skill_dir.mkdir(parents=True)
|
|
58
|
+
(skill_dir / "skill.yaml").write_text(dedent(f"""\
|
|
59
|
+
name: {skill_name}
|
|
60
|
+
version: "{version}"
|
|
61
|
+
description: Test skill {skill_name}
|
|
62
|
+
tags: {tags}
|
|
63
|
+
author:
|
|
64
|
+
name: tester
|
|
65
|
+
"""))
|
|
66
|
+
|
|
67
|
+
# Per-agent skill (jarvis)
|
|
68
|
+
agent_skill = home / "agents" / "jarvis" / "code-review"
|
|
69
|
+
agent_skill.mkdir(parents=True)
|
|
70
|
+
(agent_skill / "skill.yaml").write_text(dedent("""\
|
|
71
|
+
name: code-review
|
|
72
|
+
version: "0.1.0"
|
|
73
|
+
description: Code review skill
|
|
74
|
+
tags: [review]
|
|
75
|
+
author:
|
|
76
|
+
name: tester
|
|
77
|
+
"""))
|
|
78
|
+
|
|
79
|
+
return home
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@pytest.fixture
|
|
83
|
+
def agent_home(tmp_path: Path) -> Path:
|
|
84
|
+
"""Minimal skcapstone agent home directory."""
|
|
85
|
+
home = tmp_path / ".skcapstone"
|
|
86
|
+
home.mkdir()
|
|
87
|
+
(home / "skills").mkdir()
|
|
88
|
+
return home
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# ---------------------------------------------------------------------------
|
|
92
|
+
# TestSkillDiscovery — discover_skills() filesystem integration
|
|
93
|
+
# ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@pytest.mark.integration
|
|
97
|
+
class TestSkillDiscovery:
|
|
98
|
+
"""Test discover_skills() reading from real filesystem (tmp_path)."""
|
|
99
|
+
|
|
100
|
+
def test_discovers_global_skills(self, agent_home: Path, skskills_home: Path):
|
|
101
|
+
"""Should list all skills in ~/.skskills/installed/."""
|
|
102
|
+
from skcapstone.discovery import discover_skills
|
|
103
|
+
|
|
104
|
+
with patch.dict(os.environ, {"SKSKILLS_HOME": str(skskills_home)}):
|
|
105
|
+
with patch("skcapstone.discovery._probe_remote_registry"):
|
|
106
|
+
state = discover_skills(agent_home)
|
|
107
|
+
|
|
108
|
+
assert state.installed == 2
|
|
109
|
+
assert "syncthing-setup" in state.skill_names
|
|
110
|
+
assert "pgp-identity" in state.skill_names
|
|
111
|
+
|
|
112
|
+
def test_discovers_per_agent_skills(self, agent_home: Path, skskills_home: Path):
|
|
113
|
+
"""Should include per-agent skills when agent is specified."""
|
|
114
|
+
from skcapstone.discovery import discover_skills
|
|
115
|
+
|
|
116
|
+
with patch.dict(os.environ, {"SKSKILLS_HOME": str(skskills_home)}):
|
|
117
|
+
with patch("skcapstone.discovery._probe_remote_registry"):
|
|
118
|
+
state = discover_skills(agent_home, agent="jarvis")
|
|
119
|
+
|
|
120
|
+
assert "code-review" in state.skill_names
|
|
121
|
+
assert state.installed == 3 # 2 global + 1 agent
|
|
122
|
+
|
|
123
|
+
def test_no_duplicate_when_skill_in_both_namespaces(
|
|
124
|
+
self, agent_home: Path, skskills_home: Path
|
|
125
|
+
):
|
|
126
|
+
"""Same skill in global + agent namespace should appear once."""
|
|
127
|
+
from skcapstone.discovery import discover_skills
|
|
128
|
+
|
|
129
|
+
# Add syncthing-setup also in jarvis agent namespace
|
|
130
|
+
dup = skskills_home / "agents" / "jarvis" / "syncthing-setup"
|
|
131
|
+
dup.mkdir(parents=True)
|
|
132
|
+
(dup / "skill.yaml").write_text("name: syncthing-setup\nversion: '1.0.0'\n")
|
|
133
|
+
|
|
134
|
+
with patch.dict(os.environ, {"SKSKILLS_HOME": str(skskills_home)}):
|
|
135
|
+
with patch("skcapstone.discovery._probe_remote_registry"):
|
|
136
|
+
state = discover_skills(agent_home, agent="jarvis")
|
|
137
|
+
|
|
138
|
+
assert state.skill_names.count("syncthing-setup") == 1
|
|
139
|
+
|
|
140
|
+
def test_status_active_when_skills_found(self, agent_home: Path, skskills_home: Path):
|
|
141
|
+
"""Status should be ACTIVE when at least one skill is installed."""
|
|
142
|
+
from skcapstone.discovery import discover_skills
|
|
143
|
+
from skcapstone.models import PillarStatus
|
|
144
|
+
|
|
145
|
+
with patch.dict(os.environ, {"SKSKILLS_HOME": str(skskills_home)}):
|
|
146
|
+
with patch("skcapstone.discovery._probe_remote_registry"):
|
|
147
|
+
state = discover_skills(agent_home)
|
|
148
|
+
|
|
149
|
+
assert state.status == PillarStatus.ACTIVE
|
|
150
|
+
|
|
151
|
+
def test_status_degraded_when_home_empty(self, agent_home: Path, tmp_path: Path):
|
|
152
|
+
"""Status should be DEGRADED when skskills home exists but has no skills."""
|
|
153
|
+
from skcapstone.discovery import discover_skills
|
|
154
|
+
from skcapstone.models import PillarStatus
|
|
155
|
+
|
|
156
|
+
empty_home = tmp_path / "empty_skskills"
|
|
157
|
+
empty_home.mkdir()
|
|
158
|
+
(empty_home / "installed").mkdir()
|
|
159
|
+
|
|
160
|
+
with patch.dict(os.environ, {"SKSKILLS_HOME": str(empty_home)}):
|
|
161
|
+
with patch("skcapstone.discovery._probe_remote_registry"):
|
|
162
|
+
state = discover_skills(agent_home)
|
|
163
|
+
|
|
164
|
+
assert state.status == PillarStatus.DEGRADED
|
|
165
|
+
|
|
166
|
+
def test_status_missing_when_no_skskills_home(self, agent_home: Path, tmp_path: Path):
|
|
167
|
+
"""Status should be MISSING when skskills home directory does not exist."""
|
|
168
|
+
from skcapstone.discovery import discover_skills
|
|
169
|
+
from skcapstone.models import PillarStatus
|
|
170
|
+
|
|
171
|
+
nonexistent = tmp_path / "no_such_dir"
|
|
172
|
+
|
|
173
|
+
with patch.dict(os.environ, {"SKSKILLS_HOME": str(nonexistent)}):
|
|
174
|
+
with patch("skcapstone.discovery._probe_remote_registry"):
|
|
175
|
+
state = discover_skills(agent_home)
|
|
176
|
+
|
|
177
|
+
assert state.status == PillarStatus.MISSING
|
|
178
|
+
|
|
179
|
+
def test_skill_names_sorted(self, agent_home: Path, skskills_home: Path):
|
|
180
|
+
"""skill_names should be returned in sorted order."""
|
|
181
|
+
from skcapstone.discovery import discover_skills
|
|
182
|
+
|
|
183
|
+
with patch.dict(os.environ, {"SKSKILLS_HOME": str(skskills_home)}):
|
|
184
|
+
with patch("skcapstone.discovery._probe_remote_registry"):
|
|
185
|
+
state = discover_skills(agent_home)
|
|
186
|
+
|
|
187
|
+
assert state.skill_names == sorted(state.skill_names)
|
|
188
|
+
|
|
189
|
+
def test_per_agent_skcapstone_skills(self, agent_home: Path, skskills_home: Path):
|
|
190
|
+
"""Per-agent skcapstone skills (highest priority) should be discovered."""
|
|
191
|
+
from skcapstone.discovery import discover_skills
|
|
192
|
+
|
|
193
|
+
# Create per-agent skcapstone skill (priority 1)
|
|
194
|
+
skcap_agent_dir = agent_home / "skills" / "agents" / "opus"
|
|
195
|
+
skcap_agent_dir.mkdir(parents=True)
|
|
196
|
+
local_skill = skcap_agent_dir / "local-deploy"
|
|
197
|
+
local_skill.mkdir()
|
|
198
|
+
(local_skill / "skill.yaml").write_text("name: local-deploy\nversion: '0.1.0'\n")
|
|
199
|
+
|
|
200
|
+
with patch.dict(os.environ, {"SKSKILLS_HOME": str(skskills_home)}):
|
|
201
|
+
with patch("skcapstone.discovery._probe_remote_registry"):
|
|
202
|
+
state = discover_skills(agent_home, agent="opus")
|
|
203
|
+
|
|
204
|
+
assert "local-deploy" in state.skill_names
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
# ---------------------------------------------------------------------------
|
|
208
|
+
# TestRegistryClientIntegration — RegistryClient with mocked HTTP
|
|
209
|
+
# ---------------------------------------------------------------------------
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class TestRegistryClientIntegration:
|
|
213
|
+
"""Integration tests for RegistryClient against a mocked RemoteRegistry."""
|
|
214
|
+
|
|
215
|
+
def _make_mock_module(self, skills: list[dict[str, Any]]) -> tuple:
|
|
216
|
+
"""Return (mock_sys_module, mock_remote_instance) with the given skill entries."""
|
|
217
|
+
mock_entries = [MagicMock() for _ in skills]
|
|
218
|
+
for entry, data in zip(mock_entries, skills):
|
|
219
|
+
entry.model_dump.return_value = data
|
|
220
|
+
|
|
221
|
+
mock_index = MagicMock()
|
|
222
|
+
mock_index.skills = mock_entries
|
|
223
|
+
|
|
224
|
+
mock_remote = MagicMock()
|
|
225
|
+
mock_remote.fetch_index.return_value = mock_index
|
|
226
|
+
mock_remote.search.return_value = mock_entries
|
|
227
|
+
|
|
228
|
+
mock_module = MagicMock()
|
|
229
|
+
mock_module.RemoteRegistry = MagicMock(return_value=mock_remote)
|
|
230
|
+
return mock_module, mock_remote
|
|
231
|
+
|
|
232
|
+
def test_list_skills_returns_all(self):
|
|
233
|
+
"""list_skills() should return one dict per registry entry."""
|
|
234
|
+
from skcapstone.registry_client import RegistryClient
|
|
235
|
+
|
|
236
|
+
catalog = [
|
|
237
|
+
{"name": "syncthing-setup", "version": "1.0.0", "tags": ["sync"]},
|
|
238
|
+
{"name": "pgp-identity", "version": "0.2.0", "tags": ["identity"]},
|
|
239
|
+
]
|
|
240
|
+
mock_module, _ = self._make_mock_module(catalog)
|
|
241
|
+
|
|
242
|
+
with patch.dict("sys.modules", {"skskills.remote": mock_module}):
|
|
243
|
+
client = RegistryClient("https://test.example.com/api")
|
|
244
|
+
result = client.list_skills()
|
|
245
|
+
|
|
246
|
+
assert len(result) == 2
|
|
247
|
+
names = {s["name"] for s in result}
|
|
248
|
+
assert names == {"syncthing-setup", "pgp-identity"}
|
|
249
|
+
|
|
250
|
+
def test_search_returns_matching_skills(self):
|
|
251
|
+
"""search() should delegate to RemoteRegistry.search() and return dicts."""
|
|
252
|
+
from skcapstone.registry_client import RegistryClient
|
|
253
|
+
|
|
254
|
+
sync_entry = {"name": "syncthing-setup", "version": "1.0.0", "tags": ["sync"]}
|
|
255
|
+
sync_mock = MagicMock()
|
|
256
|
+
sync_mock.model_dump.return_value = sync_entry
|
|
257
|
+
|
|
258
|
+
mock_remote = MagicMock()
|
|
259
|
+
mock_remote.search.return_value = [sync_mock]
|
|
260
|
+
|
|
261
|
+
mock_module = MagicMock()
|
|
262
|
+
mock_module.RemoteRegistry = MagicMock(return_value=mock_remote)
|
|
263
|
+
|
|
264
|
+
with patch.dict("sys.modules", {"skskills.remote": mock_module}):
|
|
265
|
+
client = RegistryClient("https://test.example.com/api")
|
|
266
|
+
result = client.search("syncthing")
|
|
267
|
+
|
|
268
|
+
assert len(result) == 1
|
|
269
|
+
assert result[0]["name"] == "syncthing-setup"
|
|
270
|
+
|
|
271
|
+
def test_is_available_true_when_registry_responds(self):
|
|
272
|
+
"""is_available() returns True when fetch_index does not raise."""
|
|
273
|
+
from skcapstone.registry_client import RegistryClient
|
|
274
|
+
|
|
275
|
+
mock_remote = MagicMock()
|
|
276
|
+
mock_remote.fetch_index.return_value = MagicMock(skills=[])
|
|
277
|
+
mock_module = MagicMock()
|
|
278
|
+
mock_module.RemoteRegistry = MagicMock(return_value=mock_remote)
|
|
279
|
+
|
|
280
|
+
with patch.dict("sys.modules", {"skskills.remote": mock_module}):
|
|
281
|
+
client = RegistryClient("https://test.example.com/api")
|
|
282
|
+
assert client.is_available() is True
|
|
283
|
+
|
|
284
|
+
def test_is_available_false_when_registry_unreachable(self):
|
|
285
|
+
"""is_available() returns False when the registry is offline."""
|
|
286
|
+
from skcapstone.registry_client import RegistryClient
|
|
287
|
+
|
|
288
|
+
mock_remote = MagicMock()
|
|
289
|
+
mock_remote.fetch_index.side_effect = ConnectionError("network error")
|
|
290
|
+
mock_module = MagicMock()
|
|
291
|
+
mock_module.RemoteRegistry = MagicMock(return_value=mock_remote)
|
|
292
|
+
|
|
293
|
+
with patch.dict("sys.modules", {"skskills.remote": mock_module}):
|
|
294
|
+
client = RegistryClient("https://test.example.com/api")
|
|
295
|
+
assert client.is_available() is False
|
|
296
|
+
|
|
297
|
+
def test_install_returns_metadata_dict(self):
|
|
298
|
+
"""install() should return dict with name/version/agent/install_path/status."""
|
|
299
|
+
from skcapstone.registry_client import RegistryClient
|
|
300
|
+
|
|
301
|
+
installed = MagicMock()
|
|
302
|
+
installed.manifest.name = "syncthing-setup"
|
|
303
|
+
installed.manifest.version = "1.0.0"
|
|
304
|
+
installed.agent = "global"
|
|
305
|
+
installed.install_path = "/home/user/.skskills/installed/syncthing-setup"
|
|
306
|
+
installed.status.value = "installed"
|
|
307
|
+
|
|
308
|
+
mock_remote = MagicMock()
|
|
309
|
+
mock_remote.pull.return_value = installed
|
|
310
|
+
mock_module = MagicMock()
|
|
311
|
+
mock_module.RemoteRegistry = MagicMock(return_value=mock_remote)
|
|
312
|
+
|
|
313
|
+
with patch.dict("sys.modules", {"skskills.remote": mock_module}):
|
|
314
|
+
client = RegistryClient("https://test.example.com/api")
|
|
315
|
+
result = client.install("syncthing-setup")
|
|
316
|
+
|
|
317
|
+
assert result["name"] == "syncthing-setup"
|
|
318
|
+
assert result["version"] == "1.0.0"
|
|
319
|
+
assert result["agent"] == "global"
|
|
320
|
+
assert "install_path" in result
|
|
321
|
+
assert result["status"] == "installed"
|
|
322
|
+
|
|
323
|
+
def test_get_skill_returns_none_when_not_found(self):
|
|
324
|
+
"""get_skill() returns None when the remote has no matching skill."""
|
|
325
|
+
from skcapstone.registry_client import RegistryClient
|
|
326
|
+
|
|
327
|
+
mock_remote = MagicMock()
|
|
328
|
+
mock_remote.get_skill_info.return_value = None
|
|
329
|
+
mock_module = MagicMock()
|
|
330
|
+
mock_module.RemoteRegistry = MagicMock(return_value=mock_remote)
|
|
331
|
+
|
|
332
|
+
with patch.dict("sys.modules", {"skskills.remote": mock_module}):
|
|
333
|
+
client = RegistryClient("https://test.example.com/api")
|
|
334
|
+
result = client.get_skill("nonexistent")
|
|
335
|
+
|
|
336
|
+
assert result is None
|
|
337
|
+
|
|
338
|
+
def test_list_skills_returns_empty_when_registry_empty(self):
|
|
339
|
+
"""list_skills() returns [] when no skills are in the registry."""
|
|
340
|
+
from skcapstone.registry_client import RegistryClient
|
|
341
|
+
|
|
342
|
+
mock_index = MagicMock()
|
|
343
|
+
mock_index.skills = []
|
|
344
|
+
mock_remote = MagicMock()
|
|
345
|
+
mock_remote.fetch_index.return_value = mock_index
|
|
346
|
+
mock_module = MagicMock()
|
|
347
|
+
mock_module.RemoteRegistry = MagicMock(return_value=mock_remote)
|
|
348
|
+
|
|
349
|
+
with patch.dict("sys.modules", {"skskills.remote": mock_module}):
|
|
350
|
+
client = RegistryClient("https://test.example.com/api")
|
|
351
|
+
result = client.list_skills()
|
|
352
|
+
|
|
353
|
+
assert result == []
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
# ---------------------------------------------------------------------------
|
|
357
|
+
# Helpers for MCP handler tests
|
|
358
|
+
# ---------------------------------------------------------------------------
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _make_aggregator_module(
|
|
362
|
+
tools: list[dict],
|
|
363
|
+
skills: list[dict],
|
|
364
|
+
skills_loaded: int,
|
|
365
|
+
call_result: Any = None,
|
|
366
|
+
) -> MagicMock:
|
|
367
|
+
"""Build a mock skskills.aggregator module."""
|
|
368
|
+
mock_loader = MagicMock()
|
|
369
|
+
mock_loader.all_tools.return_value = tools
|
|
370
|
+
mock_loader.call_tool = AsyncMock(return_value=call_result)
|
|
371
|
+
|
|
372
|
+
mock_agg = MagicMock()
|
|
373
|
+
mock_agg.load_all_skills.return_value = skills_loaded
|
|
374
|
+
mock_agg.loader = mock_loader
|
|
375
|
+
mock_agg.get_loaded_skills.return_value = skills
|
|
376
|
+
|
|
377
|
+
mock_module = MagicMock()
|
|
378
|
+
mock_module.SkillAggregator = MagicMock(return_value=mock_agg)
|
|
379
|
+
return mock_module
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
# ---------------------------------------------------------------------------
|
|
383
|
+
# TestSkillsListToolsMCP — skskills_list_tools handler
|
|
384
|
+
# ---------------------------------------------------------------------------
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
class TestSkillsListToolsMCP:
|
|
388
|
+
"""Integration tests for the skskills_list_tools MCP handler."""
|
|
389
|
+
|
|
390
|
+
@pytest.mark.asyncio
|
|
391
|
+
async def test_returns_tools_list(self, tmp_path: Path):
|
|
392
|
+
"""skskills_list_tools should return a 'tools' list in the JSON response."""
|
|
393
|
+
tools = [
|
|
394
|
+
{
|
|
395
|
+
"name": "syncthing-setup.check_status",
|
|
396
|
+
"description": "Check Syncthing status",
|
|
397
|
+
"inputSchema": {"type": "object", "properties": {}},
|
|
398
|
+
}
|
|
399
|
+
]
|
|
400
|
+
mock_agg_module = _make_aggregator_module(tools=tools, skills=[], skills_loaded=1)
|
|
401
|
+
|
|
402
|
+
with patch.dict("sys.modules", {"skskills.aggregator": mock_agg_module}):
|
|
403
|
+
with patch(
|
|
404
|
+
"skcapstone.mcp_tools.skills_tools._home", return_value=tmp_path
|
|
405
|
+
):
|
|
406
|
+
with patch(
|
|
407
|
+
"skcapstone.mcp_tools.skills_tools._get_agent_name",
|
|
408
|
+
return_value="test-agent",
|
|
409
|
+
):
|
|
410
|
+
result = await _handle_skskills_list_tools({"agent": "test-agent"})
|
|
411
|
+
|
|
412
|
+
assert len(result) == 1
|
|
413
|
+
data = json.loads(result[0].text)
|
|
414
|
+
assert "tools" in data
|
|
415
|
+
assert len(data["tools"]) == 1
|
|
416
|
+
assert data["tools"][0]["name"] == "syncthing-setup.check_status"
|
|
417
|
+
|
|
418
|
+
@pytest.mark.asyncio
|
|
419
|
+
async def test_returns_skills_metadata(self, tmp_path: Path):
|
|
420
|
+
"""skskills_list_tools should include loaded skills metadata."""
|
|
421
|
+
skills = [{"name": "syncthing-setup", "version": "1.0.0"}]
|
|
422
|
+
mock_agg_module = _make_aggregator_module(tools=[], skills=skills, skills_loaded=1)
|
|
423
|
+
|
|
424
|
+
with patch.dict("sys.modules", {"skskills.aggregator": mock_agg_module}):
|
|
425
|
+
with patch(
|
|
426
|
+
"skcapstone.mcp_tools.skills_tools._home", return_value=tmp_path
|
|
427
|
+
):
|
|
428
|
+
with patch(
|
|
429
|
+
"skcapstone.mcp_tools.skills_tools._get_agent_name",
|
|
430
|
+
return_value="test-agent",
|
|
431
|
+
):
|
|
432
|
+
result = await _handle_skskills_list_tools({})
|
|
433
|
+
|
|
434
|
+
data = json.loads(result[0].text)
|
|
435
|
+
assert data["skills_loaded"] == 1
|
|
436
|
+
assert data["skills"] == [{"name": "syncthing-setup", "version": "1.0.0"}]
|
|
437
|
+
|
|
438
|
+
@pytest.mark.asyncio
|
|
439
|
+
async def test_tools_have_required_fields(self, tmp_path: Path):
|
|
440
|
+
"""Each tool entry must expose name, description, and inputSchema."""
|
|
441
|
+
tools = [
|
|
442
|
+
{
|
|
443
|
+
"name": "pgp-identity.show_key",
|
|
444
|
+
"description": "Show PGP public key",
|
|
445
|
+
"inputSchema": {
|
|
446
|
+
"type": "object",
|
|
447
|
+
"properties": {"format": {"type": "string"}},
|
|
448
|
+
},
|
|
449
|
+
}
|
|
450
|
+
]
|
|
451
|
+
mock_agg_module = _make_aggregator_module(tools=tools, skills=[], skills_loaded=1)
|
|
452
|
+
|
|
453
|
+
with patch.dict("sys.modules", {"skskills.aggregator": mock_agg_module}):
|
|
454
|
+
with patch(
|
|
455
|
+
"skcapstone.mcp_tools.skills_tools._home", return_value=tmp_path
|
|
456
|
+
):
|
|
457
|
+
with patch(
|
|
458
|
+
"skcapstone.mcp_tools.skills_tools._get_agent_name",
|
|
459
|
+
return_value="opus",
|
|
460
|
+
):
|
|
461
|
+
result = await _handle_skskills_list_tools({"agent": "opus"})
|
|
462
|
+
|
|
463
|
+
data = json.loads(result[0].text)
|
|
464
|
+
for tool in data["tools"]:
|
|
465
|
+
assert "name" in tool
|
|
466
|
+
assert "description" in tool
|
|
467
|
+
assert "inputSchema" in tool
|
|
468
|
+
|
|
469
|
+
@pytest.mark.asyncio
|
|
470
|
+
async def test_returns_error_when_skskills_missing(self, tmp_path: Path):
|
|
471
|
+
"""Should return an error response when skskills is not installed.
|
|
472
|
+
|
|
473
|
+
Setting sys.modules["skskills.aggregator"] = None causes Python to
|
|
474
|
+
raise ImportError when the handler does `from skskills.aggregator import ...`.
|
|
475
|
+
"""
|
|
476
|
+
with patch.dict("sys.modules", {"skskills.aggregator": None}):
|
|
477
|
+
with patch(
|
|
478
|
+
"skcapstone.mcp_tools.skills_tools._home", return_value=tmp_path
|
|
479
|
+
):
|
|
480
|
+
result = await _handle_skskills_list_tools({})
|
|
481
|
+
|
|
482
|
+
data = json.loads(result[0].text)
|
|
483
|
+
assert "error" in data
|
|
484
|
+
|
|
485
|
+
@pytest.mark.asyncio
|
|
486
|
+
async def test_agent_name_in_response(self, tmp_path: Path):
|
|
487
|
+
"""The response should include the agent namespace used."""
|
|
488
|
+
mock_agg_module = _make_aggregator_module(tools=[], skills=[], skills_loaded=0)
|
|
489
|
+
|
|
490
|
+
with patch.dict("sys.modules", {"skskills.aggregator": mock_agg_module}):
|
|
491
|
+
with patch(
|
|
492
|
+
"skcapstone.mcp_tools.skills_tools._home", return_value=tmp_path
|
|
493
|
+
):
|
|
494
|
+
with patch(
|
|
495
|
+
"skcapstone.mcp_tools.skills_tools._get_agent_name",
|
|
496
|
+
return_value="jarvis",
|
|
497
|
+
):
|
|
498
|
+
result = await _handle_skskills_list_tools({"agent": "jarvis"})
|
|
499
|
+
|
|
500
|
+
data = json.loads(result[0].text)
|
|
501
|
+
assert data["agent"] == "jarvis"
|
|
502
|
+
|
|
503
|
+
@pytest.mark.asyncio
|
|
504
|
+
async def test_zero_skills_returns_empty_lists(self, tmp_path: Path):
|
|
505
|
+
"""When no skills are installed, tools and skills lists should be empty."""
|
|
506
|
+
mock_agg_module = _make_aggregator_module(tools=[], skills=[], skills_loaded=0)
|
|
507
|
+
|
|
508
|
+
with patch.dict("sys.modules", {"skskills.aggregator": mock_agg_module}):
|
|
509
|
+
with patch(
|
|
510
|
+
"skcapstone.mcp_tools.skills_tools._home", return_value=tmp_path
|
|
511
|
+
):
|
|
512
|
+
with patch(
|
|
513
|
+
"skcapstone.mcp_tools.skills_tools._get_agent_name",
|
|
514
|
+
return_value="anonymous",
|
|
515
|
+
):
|
|
516
|
+
result = await _handle_skskills_list_tools({})
|
|
517
|
+
|
|
518
|
+
data = json.loads(result[0].text)
|
|
519
|
+
assert data["skills_loaded"] == 0
|
|
520
|
+
assert data["tools"] == []
|
|
521
|
+
assert data["skills"] == []
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
# ---------------------------------------------------------------------------
|
|
525
|
+
# TestSkillsRunToolMCP — skskills_run_tool handler
|
|
526
|
+
# ---------------------------------------------------------------------------
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
class TestSkillsRunToolMCP:
|
|
530
|
+
"""Integration tests for the skskills_run_tool MCP handler."""
|
|
531
|
+
|
|
532
|
+
@pytest.mark.asyncio
|
|
533
|
+
async def test_executes_tool_and_returns_json_result(self, tmp_path: Path):
|
|
534
|
+
"""Should call the tool and return its dict result as JSON."""
|
|
535
|
+
mock_agg_module = _make_aggregator_module(
|
|
536
|
+
tools=[], skills=[], skills_loaded=1,
|
|
537
|
+
call_result={"status": "ok", "version": "1.9.3"},
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
with patch.dict("sys.modules", {"skskills.aggregator": mock_agg_module}):
|
|
541
|
+
with patch(
|
|
542
|
+
"skcapstone.mcp_tools.skills_tools._home", return_value=tmp_path
|
|
543
|
+
):
|
|
544
|
+
with patch(
|
|
545
|
+
"skcapstone.mcp_tools.skills_tools._get_agent_name",
|
|
546
|
+
return_value="test-agent",
|
|
547
|
+
):
|
|
548
|
+
result = await _handle_skskills_run_tool(
|
|
549
|
+
{"tool": "syncthing-setup.check_status"}
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
data = json.loads(result[0].text)
|
|
553
|
+
assert data["status"] == "ok"
|
|
554
|
+
assert data["version"] == "1.9.3"
|
|
555
|
+
|
|
556
|
+
# Verify call_tool was invoked with the correct args
|
|
557
|
+
agg_instance = mock_agg_module.SkillAggregator.return_value
|
|
558
|
+
agg_instance.loader.call_tool.assert_awaited_once_with(
|
|
559
|
+
"syncthing-setup.check_status", {}
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
@pytest.mark.asyncio
|
|
563
|
+
async def test_passes_args_to_tool(self, tmp_path: Path):
|
|
564
|
+
"""Tool arguments should be forwarded verbatim to call_tool."""
|
|
565
|
+
mock_agg_module = _make_aggregator_module(
|
|
566
|
+
tools=[], skills=[], skills_loaded=1, call_result={"result": "done"}
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
with patch.dict("sys.modules", {"skskills.aggregator": mock_agg_module}):
|
|
570
|
+
with patch(
|
|
571
|
+
"skcapstone.mcp_tools.skills_tools._home", return_value=tmp_path
|
|
572
|
+
):
|
|
573
|
+
with patch(
|
|
574
|
+
"skcapstone.mcp_tools.skills_tools._get_agent_name",
|
|
575
|
+
return_value="test-agent",
|
|
576
|
+
):
|
|
577
|
+
await _handle_skskills_run_tool(
|
|
578
|
+
{
|
|
579
|
+
"tool": "pgp-identity.show_key",
|
|
580
|
+
"args": {"format": "armored"},
|
|
581
|
+
}
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
agg_instance = mock_agg_module.SkillAggregator.return_value
|
|
585
|
+
agg_instance.loader.call_tool.assert_awaited_once_with(
|
|
586
|
+
"pgp-identity.show_key", {"format": "armored"}
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
@pytest.mark.asyncio
|
|
590
|
+
async def test_missing_tool_arg_returns_error(self, tmp_path: Path):
|
|
591
|
+
"""Should return an error when the 'tool' argument is absent."""
|
|
592
|
+
mock_agg_module = _make_aggregator_module(
|
|
593
|
+
tools=[], skills=[], skills_loaded=0, call_result=None
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
with patch.dict("sys.modules", {"skskills.aggregator": mock_agg_module}):
|
|
597
|
+
with patch(
|
|
598
|
+
"skcapstone.mcp_tools.skills_tools._home", return_value=tmp_path
|
|
599
|
+
):
|
|
600
|
+
result = await _handle_skskills_run_tool({})
|
|
601
|
+
|
|
602
|
+
data = json.loads(result[0].text)
|
|
603
|
+
assert "error" in data
|
|
604
|
+
|
|
605
|
+
@pytest.mark.asyncio
|
|
606
|
+
async def test_unknown_tool_returns_error(self, tmp_path: Path):
|
|
607
|
+
"""Should return an error when call_tool raises KeyError (unknown tool)."""
|
|
608
|
+
mock_loader = MagicMock()
|
|
609
|
+
mock_loader.call_tool = AsyncMock(side_effect=KeyError("no such tool: xyz.run"))
|
|
610
|
+
mock_agg = MagicMock()
|
|
611
|
+
mock_agg.load_all_skills.return_value = 1
|
|
612
|
+
mock_agg.loader = mock_loader
|
|
613
|
+
mock_module = MagicMock()
|
|
614
|
+
mock_module.SkillAggregator = MagicMock(return_value=mock_agg)
|
|
615
|
+
|
|
616
|
+
with patch.dict("sys.modules", {"skskills.aggregator": mock_module}):
|
|
617
|
+
with patch(
|
|
618
|
+
"skcapstone.mcp_tools.skills_tools._home", return_value=tmp_path
|
|
619
|
+
):
|
|
620
|
+
with patch(
|
|
621
|
+
"skcapstone.mcp_tools.skills_tools._get_agent_name",
|
|
622
|
+
return_value="test-agent",
|
|
623
|
+
):
|
|
624
|
+
result = await _handle_skskills_run_tool({"tool": "xyz.run"})
|
|
625
|
+
|
|
626
|
+
data = json.loads(result[0].text)
|
|
627
|
+
assert "error" in data
|
|
628
|
+
|
|
629
|
+
@pytest.mark.asyncio
|
|
630
|
+
async def test_string_result_returned_as_plain_text(self, tmp_path: Path):
|
|
631
|
+
"""When the tool returns a string, response should be plain text (not JSON)."""
|
|
632
|
+
mock_agg_module = _make_aggregator_module(
|
|
633
|
+
tools=[], skills=[], skills_loaded=1,
|
|
634
|
+
call_result="Syncthing is running.",
|
|
635
|
+
)
|
|
636
|
+
|
|
637
|
+
with patch.dict("sys.modules", {"skskills.aggregator": mock_agg_module}):
|
|
638
|
+
with patch(
|
|
639
|
+
"skcapstone.mcp_tools.skills_tools._home", return_value=tmp_path
|
|
640
|
+
):
|
|
641
|
+
with patch(
|
|
642
|
+
"skcapstone.mcp_tools.skills_tools._get_agent_name",
|
|
643
|
+
return_value="test-agent",
|
|
644
|
+
):
|
|
645
|
+
result = await _handle_skskills_run_tool(
|
|
646
|
+
{"tool": "syncthing-setup.check_status"}
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
assert result[0].text == "Syncthing is running."
|
|
650
|
+
|
|
651
|
+
@pytest.mark.asyncio
|
|
652
|
+
async def test_tool_runtime_exception_returns_error(self, tmp_path: Path):
|
|
653
|
+
"""Unexpected exceptions from call_tool should produce an error response."""
|
|
654
|
+
mock_loader = MagicMock()
|
|
655
|
+
mock_loader.call_tool = AsyncMock(side_effect=RuntimeError("daemon not running"))
|
|
656
|
+
mock_agg = MagicMock()
|
|
657
|
+
mock_agg.load_all_skills.return_value = 1
|
|
658
|
+
mock_agg.loader = mock_loader
|
|
659
|
+
mock_module = MagicMock()
|
|
660
|
+
mock_module.SkillAggregator = MagicMock(return_value=mock_agg)
|
|
661
|
+
|
|
662
|
+
with patch.dict("sys.modules", {"skskills.aggregator": mock_module}):
|
|
663
|
+
with patch(
|
|
664
|
+
"skcapstone.mcp_tools.skills_tools._home", return_value=tmp_path
|
|
665
|
+
):
|
|
666
|
+
with patch(
|
|
667
|
+
"skcapstone.mcp_tools.skills_tools._get_agent_name",
|
|
668
|
+
return_value="test-agent",
|
|
669
|
+
):
|
|
670
|
+
result = await _handle_skskills_run_tool(
|
|
671
|
+
{"tool": "syncthing-setup.check_status"}
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
data = json.loads(result[0].text)
|
|
675
|
+
assert "error" in data
|
|
676
|
+
assert "syncthing-setup.check_status" in data["error"]
|
|
677
|
+
|
|
678
|
+
@pytest.mark.asyncio
|
|
679
|
+
async def test_uses_specified_agent_namespace(self, tmp_path: Path):
|
|
680
|
+
"""SkillAggregator should be constructed with the agent from args."""
|
|
681
|
+
mock_agg_module = _make_aggregator_module(
|
|
682
|
+
tools=[], skills=[], skills_loaded=1, call_result={"ok": True}
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
with patch.dict("sys.modules", {"skskills.aggregator": mock_agg_module}):
|
|
686
|
+
with patch(
|
|
687
|
+
"skcapstone.mcp_tools.skills_tools._home", return_value=tmp_path
|
|
688
|
+
):
|
|
689
|
+
await _handle_skskills_run_tool(
|
|
690
|
+
{"tool": "syncthing-setup.check_status", "agent": "jarvis"}
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
mock_agg_module.SkillAggregator.assert_called_once_with(agent="jarvis")
|
|
694
|
+
|
|
695
|
+
@pytest.mark.asyncio
|
|
696
|
+
async def test_returns_error_when_skskills_missing(self, tmp_path: Path):
|
|
697
|
+
"""Should return an error response when skskills is not installed."""
|
|
698
|
+
with patch.dict("sys.modules", {"skskills.aggregator": None}):
|
|
699
|
+
with patch(
|
|
700
|
+
"skcapstone.mcp_tools.skills_tools._home", return_value=tmp_path
|
|
701
|
+
):
|
|
702
|
+
result = await _handle_skskills_run_tool({"tool": "syncthing-setup.run"})
|
|
703
|
+
|
|
704
|
+
data = json.loads(result[0].text)
|
|
705
|
+
assert "error" in data
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
# ---------------------------------------------------------------------------
|
|
709
|
+
# TestMCPToolRegistration — verify TOOLS and HANDLERS are wired correctly
|
|
710
|
+
# ---------------------------------------------------------------------------
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
class TestMCPToolRegistration:
|
|
714
|
+
"""Verify the skills MCP tools are properly declared in TOOLS and HANDLERS."""
|
|
715
|
+
|
|
716
|
+
def test_tools_list_contains_expected_names(self):
|
|
717
|
+
"""TOOLS list should declare both skskills tools."""
|
|
718
|
+
tool_names = {t.name for t in TOOLS}
|
|
719
|
+
assert "skskills_list_tools" in tool_names
|
|
720
|
+
assert "skskills_run_tool" in tool_names
|
|
721
|
+
|
|
722
|
+
def test_handlers_map_contains_expected_names(self):
|
|
723
|
+
"""HANDLERS dict should map both tool names to callable handlers."""
|
|
724
|
+
assert "skskills_list_tools" in HANDLERS
|
|
725
|
+
assert "skskills_run_tool" in HANDLERS
|
|
726
|
+
assert callable(HANDLERS["skskills_list_tools"])
|
|
727
|
+
assert callable(HANDLERS["skskills_run_tool"])
|
|
728
|
+
|
|
729
|
+
def test_skskills_run_tool_requires_tool_arg(self):
|
|
730
|
+
"""skskills_run_tool inputSchema must declare 'tool' as required."""
|
|
731
|
+
run_tool = next(t for t in TOOLS if t.name == "skskills_run_tool")
|
|
732
|
+
assert "tool" in run_tool.inputSchema["required"]
|
|
733
|
+
|
|
734
|
+
def test_skskills_list_tools_no_required_args(self):
|
|
735
|
+
"""skskills_list_tools inputSchema should have no required arguments."""
|
|
736
|
+
list_tool = next(t for t in TOOLS if t.name == "skskills_list_tools")
|
|
737
|
+
assert list_tool.inputSchema.get("required", []) == []
|
|
738
|
+
|
|
739
|
+
def test_handlers_are_coroutines(self):
|
|
740
|
+
"""Both handlers should be async coroutine functions."""
|
|
741
|
+
import asyncio
|
|
742
|
+
|
|
743
|
+
assert asyncio.iscoroutinefunction(HANDLERS["skskills_list_tools"])
|
|
744
|
+
assert asyncio.iscoroutinefunction(HANDLERS["skskills_run_tool"])
|