@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,1909 @@
|
|
|
1
|
+
"""Tests for the SKCapstone MCP server."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from unittest.mock import patch
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from skcapstone.mcp_server import (
|
|
12
|
+
_error_response,
|
|
13
|
+
_home,
|
|
14
|
+
_json_response,
|
|
15
|
+
call_tool,
|
|
16
|
+
list_tools,
|
|
17
|
+
server,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
# Helpers
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _extract_json(result: list) -> dict | list:
|
|
27
|
+
"""Parse the JSON from a TextContent response list.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
result: List of TextContent objects.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Parsed object from the JSON text.
|
|
34
|
+
"""
|
|
35
|
+
assert len(result) == 1
|
|
36
|
+
return json.loads(result[0].text)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
# Unit tests: helper functions
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class TestHelpers:
|
|
45
|
+
"""Tests for internal helper functions."""
|
|
46
|
+
|
|
47
|
+
def test_home_resolves(self):
|
|
48
|
+
"""Default home resolves to ~/.skcapstone."""
|
|
49
|
+
result = _home()
|
|
50
|
+
assert result == Path("~/.skcapstone").expanduser()
|
|
51
|
+
|
|
52
|
+
def test_json_response_structure(self):
|
|
53
|
+
"""_json_response wraps data as TextContent."""
|
|
54
|
+
result = _json_response({"key": "value"})
|
|
55
|
+
assert len(result) == 1
|
|
56
|
+
assert result[0].type == "text"
|
|
57
|
+
parsed = json.loads(result[0].text)
|
|
58
|
+
assert parsed == {"key": "value"}
|
|
59
|
+
|
|
60
|
+
def test_error_response_structure(self):
|
|
61
|
+
"""_error_response produces error JSON."""
|
|
62
|
+
result = _error_response("something broke")
|
|
63
|
+
parsed = _extract_json(result)
|
|
64
|
+
assert "something broke" in parsed["error"]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
# Unit tests: tool listing
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class TestToolListing:
|
|
73
|
+
"""Tests for MCP tool definitions."""
|
|
74
|
+
|
|
75
|
+
@pytest.mark.asyncio
|
|
76
|
+
async def test_list_tools_returns_all(self):
|
|
77
|
+
"""list_tools returns all registered tools."""
|
|
78
|
+
tools = await list_tools()
|
|
79
|
+
assert len(tools) == 68
|
|
80
|
+
|
|
81
|
+
@pytest.mark.asyncio
|
|
82
|
+
async def test_tool_names(self):
|
|
83
|
+
"""All required tool names are registered."""
|
|
84
|
+
tools = await list_tools()
|
|
85
|
+
names = {t.name for t in tools}
|
|
86
|
+
expected = {
|
|
87
|
+
"agent_status",
|
|
88
|
+
"memory_store",
|
|
89
|
+
"memory_search",
|
|
90
|
+
"memory_recall",
|
|
91
|
+
"send_message",
|
|
92
|
+
"check_inbox",
|
|
93
|
+
"sync_push",
|
|
94
|
+
"sync_pull",
|
|
95
|
+
"coord_status",
|
|
96
|
+
"coord_claim",
|
|
97
|
+
"coord_complete",
|
|
98
|
+
"coord_create",
|
|
99
|
+
"ritual",
|
|
100
|
+
"soul_show",
|
|
101
|
+
"journal_write",
|
|
102
|
+
"journal_read",
|
|
103
|
+
"anchor_show",
|
|
104
|
+
"germination",
|
|
105
|
+
"agent_context",
|
|
106
|
+
"session_capture",
|
|
107
|
+
"trust_graph",
|
|
108
|
+
"memory_curate",
|
|
109
|
+
"trust_calibrate",
|
|
110
|
+
"anchor_update",
|
|
111
|
+
"state_diff",
|
|
112
|
+
"skskills_list_tools",
|
|
113
|
+
"skskills_run_tool",
|
|
114
|
+
"trustee_health",
|
|
115
|
+
"trustee_restart",
|
|
116
|
+
"trustee_scale",
|
|
117
|
+
"trustee_rotate",
|
|
118
|
+
"trustee_monitor",
|
|
119
|
+
"trustee_logs",
|
|
120
|
+
"trustee_deployments",
|
|
121
|
+
"skchat_send",
|
|
122
|
+
"skchat_inbox",
|
|
123
|
+
"skchat_group_create",
|
|
124
|
+
"skchat_group_send",
|
|
125
|
+
# Heartbeat
|
|
126
|
+
"heartbeat_pulse",
|
|
127
|
+
"heartbeat_peers",
|
|
128
|
+
"heartbeat_health",
|
|
129
|
+
"heartbeat_find_capable",
|
|
130
|
+
# File transfer
|
|
131
|
+
"file_send",
|
|
132
|
+
"file_receive",
|
|
133
|
+
"file_list",
|
|
134
|
+
"file_status",
|
|
135
|
+
# Pub/sub
|
|
136
|
+
"pubsub_publish",
|
|
137
|
+
"pubsub_subscribe",
|
|
138
|
+
"pubsub_poll",
|
|
139
|
+
"pubsub_topics",
|
|
140
|
+
# Memory fortress
|
|
141
|
+
"fortress_verify",
|
|
142
|
+
"fortress_seal_existing",
|
|
143
|
+
"fortress_status",
|
|
144
|
+
# Memory promoter
|
|
145
|
+
"promoter_sweep",
|
|
146
|
+
"promoter_history",
|
|
147
|
+
# KMS
|
|
148
|
+
"kms_status",
|
|
149
|
+
"kms_list_keys",
|
|
150
|
+
"kms_rotate",
|
|
151
|
+
# SKSeed (Logic Kernel)
|
|
152
|
+
"skseed_collide",
|
|
153
|
+
"skseed_audit",
|
|
154
|
+
"skseed_philosopher",
|
|
155
|
+
"skseed_truth_check",
|
|
156
|
+
"skseed_alignment",
|
|
157
|
+
# Model Router
|
|
158
|
+
"model_route",
|
|
159
|
+
# Consciousness
|
|
160
|
+
"consciousness_status",
|
|
161
|
+
"consciousness_test",
|
|
162
|
+
# Notifications & pub/sub stats
|
|
163
|
+
"send_notification",
|
|
164
|
+
"pubsub_stats",
|
|
165
|
+
}
|
|
166
|
+
assert names == expected
|
|
167
|
+
|
|
168
|
+
@pytest.mark.asyncio
|
|
169
|
+
async def test_tool_schemas_valid(self):
|
|
170
|
+
"""Each tool has a valid inputSchema with 'type' and 'properties'."""
|
|
171
|
+
tools = await list_tools()
|
|
172
|
+
for tool in tools:
|
|
173
|
+
schema = tool.inputSchema
|
|
174
|
+
assert schema["type"] == "object"
|
|
175
|
+
assert "properties" in schema
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
# ---------------------------------------------------------------------------
|
|
179
|
+
# Unit tests: tool dispatch (call_tool)
|
|
180
|
+
# ---------------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class TestCallToolDispatch:
|
|
184
|
+
"""Tests for call_tool routing and error handling."""
|
|
185
|
+
|
|
186
|
+
@pytest.mark.asyncio
|
|
187
|
+
async def test_unknown_tool(self):
|
|
188
|
+
"""Unknown tool name returns an error response."""
|
|
189
|
+
result = await call_tool("nonexistent_tool", {})
|
|
190
|
+
parsed = _extract_json(result)
|
|
191
|
+
assert "Unknown tool" in parsed["error"]
|
|
192
|
+
|
|
193
|
+
@pytest.mark.asyncio
|
|
194
|
+
async def test_agent_status_no_agent(self, tmp_path: Path):
|
|
195
|
+
"""agent_status with no initialized agent returns error."""
|
|
196
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(tmp_path / "no-agent")):
|
|
197
|
+
result = await call_tool("agent_status", {})
|
|
198
|
+
parsed = _extract_json(result)
|
|
199
|
+
assert "error" in parsed
|
|
200
|
+
|
|
201
|
+
@pytest.mark.asyncio
|
|
202
|
+
async def test_agent_status_with_agent(self, initialized_agent_home: Path):
|
|
203
|
+
"""agent_status returns pillar states for a valid agent."""
|
|
204
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
205
|
+
result = await call_tool("agent_status", {})
|
|
206
|
+
parsed = _extract_json(result)
|
|
207
|
+
assert "pillars" in parsed
|
|
208
|
+
assert "identity" in parsed["pillars"]
|
|
209
|
+
assert "memory" in parsed["pillars"]
|
|
210
|
+
assert "trust" in parsed["pillars"]
|
|
211
|
+
assert "security" in parsed["pillars"]
|
|
212
|
+
assert "sync" in parsed["pillars"]
|
|
213
|
+
assert "is_conscious" in parsed
|
|
214
|
+
assert parsed["name"] == "test-agent"
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# ---------------------------------------------------------------------------
|
|
218
|
+
# Memory tool tests
|
|
219
|
+
# ---------------------------------------------------------------------------
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class TestMemoryTools:
|
|
223
|
+
"""Tests for memory_store, memory_search, and memory_recall."""
|
|
224
|
+
|
|
225
|
+
@pytest.mark.asyncio
|
|
226
|
+
async def test_memory_store_requires_content(self, initialized_agent_home: Path):
|
|
227
|
+
"""memory_store without content returns error."""
|
|
228
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
229
|
+
result = await call_tool("memory_store", {})
|
|
230
|
+
parsed = _extract_json(result)
|
|
231
|
+
assert "error" in parsed
|
|
232
|
+
|
|
233
|
+
@pytest.mark.asyncio
|
|
234
|
+
async def test_memory_search_requires_query(self, initialized_agent_home: Path):
|
|
235
|
+
"""memory_search without query returns error."""
|
|
236
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
237
|
+
result = await call_tool("memory_search", {})
|
|
238
|
+
parsed = _extract_json(result)
|
|
239
|
+
assert "error" in parsed
|
|
240
|
+
|
|
241
|
+
@pytest.mark.asyncio
|
|
242
|
+
async def test_memory_recall_requires_id(self, initialized_agent_home: Path):
|
|
243
|
+
"""memory_recall without memory_id returns error."""
|
|
244
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
245
|
+
result = await call_tool("memory_recall", {})
|
|
246
|
+
parsed = _extract_json(result)
|
|
247
|
+
assert "error" in parsed
|
|
248
|
+
|
|
249
|
+
@pytest.mark.asyncio
|
|
250
|
+
async def test_memory_store_and_search(self, initialized_agent_home: Path):
|
|
251
|
+
"""Store a memory then find it via search."""
|
|
252
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
253
|
+
store_result = await call_tool(
|
|
254
|
+
"memory_store",
|
|
255
|
+
{
|
|
256
|
+
"content": "The sovereign penguin remembers everything",
|
|
257
|
+
"tags": ["pengu", "test"],
|
|
258
|
+
"importance": 0.5,
|
|
259
|
+
},
|
|
260
|
+
)
|
|
261
|
+
store_parsed = _extract_json(store_result)
|
|
262
|
+
assert store_parsed["stored"] is True
|
|
263
|
+
assert store_parsed["memory_id"]
|
|
264
|
+
assert store_parsed["layer"] == "short-term"
|
|
265
|
+
|
|
266
|
+
search_result = await call_tool(
|
|
267
|
+
"memory_search", {"query": "sovereign penguin"}
|
|
268
|
+
)
|
|
269
|
+
search_parsed = _extract_json(search_result)
|
|
270
|
+
assert isinstance(search_parsed, list)
|
|
271
|
+
assert len(search_parsed) >= 1
|
|
272
|
+
assert any("sovereign penguin" in r["content"] for r in search_parsed)
|
|
273
|
+
|
|
274
|
+
@pytest.mark.asyncio
|
|
275
|
+
async def test_memory_store_and_recall(self, initialized_agent_home: Path):
|
|
276
|
+
"""Store a memory then recall it by ID."""
|
|
277
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
278
|
+
store_result = await call_tool(
|
|
279
|
+
"memory_store",
|
|
280
|
+
{"content": "Recall me later", "importance": 0.3},
|
|
281
|
+
)
|
|
282
|
+
store_parsed = _extract_json(store_result)
|
|
283
|
+
mid = store_parsed["memory_id"]
|
|
284
|
+
|
|
285
|
+
recall_result = await call_tool("memory_recall", {"memory_id": mid})
|
|
286
|
+
recall_parsed = _extract_json(recall_result)
|
|
287
|
+
assert recall_parsed["memory_id"] == mid
|
|
288
|
+
assert "Recall me later" in recall_parsed["content"]
|
|
289
|
+
assert recall_parsed["access_count"] >= 1
|
|
290
|
+
|
|
291
|
+
@pytest.mark.asyncio
|
|
292
|
+
async def test_memory_recall_not_found(self, initialized_agent_home: Path):
|
|
293
|
+
"""memory_recall with nonexistent ID returns error."""
|
|
294
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
295
|
+
result = await call_tool("memory_recall", {"memory_id": "nonexistent123"})
|
|
296
|
+
parsed = _extract_json(result)
|
|
297
|
+
assert "error" in parsed
|
|
298
|
+
|
|
299
|
+
@pytest.mark.asyncio
|
|
300
|
+
async def test_memory_store_high_importance_promotes(
|
|
301
|
+
self, initialized_agent_home: Path
|
|
302
|
+
):
|
|
303
|
+
"""High-importance memory gets promoted to mid-term."""
|
|
304
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
305
|
+
result = await call_tool(
|
|
306
|
+
"memory_store",
|
|
307
|
+
{"content": "Critical penguin intel", "importance": 0.8},
|
|
308
|
+
)
|
|
309
|
+
parsed = _extract_json(result)
|
|
310
|
+
assert parsed["stored"] is True
|
|
311
|
+
assert parsed["layer"] == "mid-term"
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
# ---------------------------------------------------------------------------
|
|
315
|
+
# Coordination tool tests
|
|
316
|
+
# ---------------------------------------------------------------------------
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
class TestCoordTools:
|
|
320
|
+
"""Tests for coordination board MCP tools."""
|
|
321
|
+
|
|
322
|
+
@pytest.mark.asyncio
|
|
323
|
+
async def test_coord_status_empty(self, initialized_agent_home: Path):
|
|
324
|
+
"""coord_status on empty board returns zero tasks."""
|
|
325
|
+
from skcapstone.coordination import Board
|
|
326
|
+
|
|
327
|
+
board = Board(initialized_agent_home)
|
|
328
|
+
board.ensure_dirs()
|
|
329
|
+
|
|
330
|
+
with (
|
|
331
|
+
patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)),
|
|
332
|
+
patch("skcapstone.mcp_tools._helpers.SHARED_ROOT", str(initialized_agent_home)),
|
|
333
|
+
):
|
|
334
|
+
result = await call_tool("coord_status", {})
|
|
335
|
+
parsed = _extract_json(result)
|
|
336
|
+
assert parsed["summary"]["total"] == 0
|
|
337
|
+
assert parsed["tasks"] == []
|
|
338
|
+
|
|
339
|
+
@pytest.mark.asyncio
|
|
340
|
+
async def test_coord_claim_requires_params(self, initialized_agent_home: Path):
|
|
341
|
+
"""coord_claim without task_id and agent_name returns error."""
|
|
342
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
343
|
+
result = await call_tool("coord_claim", {})
|
|
344
|
+
parsed = _extract_json(result)
|
|
345
|
+
assert "error" in parsed
|
|
346
|
+
|
|
347
|
+
@pytest.mark.asyncio
|
|
348
|
+
async def test_coord_complete_requires_params(self, initialized_agent_home: Path):
|
|
349
|
+
"""coord_complete without task_id and agent_name returns error."""
|
|
350
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
351
|
+
result = await call_tool("coord_complete", {})
|
|
352
|
+
parsed = _extract_json(result)
|
|
353
|
+
assert "error" in parsed
|
|
354
|
+
|
|
355
|
+
@pytest.mark.asyncio
|
|
356
|
+
async def test_coord_create_requires_title(self, initialized_agent_home: Path):
|
|
357
|
+
"""coord_create without title returns error."""
|
|
358
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
359
|
+
result = await call_tool("coord_create", {})
|
|
360
|
+
parsed = _extract_json(result)
|
|
361
|
+
assert "error" in parsed
|
|
362
|
+
|
|
363
|
+
@pytest.mark.asyncio
|
|
364
|
+
async def test_coord_claim_nonexistent_task(self, initialized_agent_home: Path):
|
|
365
|
+
"""coord_claim for a nonexistent task returns error."""
|
|
366
|
+
from skcapstone.coordination import Board
|
|
367
|
+
|
|
368
|
+
board = Board(initialized_agent_home)
|
|
369
|
+
board.ensure_dirs()
|
|
370
|
+
|
|
371
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
372
|
+
result = await call_tool(
|
|
373
|
+
"coord_claim", {"task_id": "nosuch", "agent_name": "tester"}
|
|
374
|
+
)
|
|
375
|
+
parsed = _extract_json(result)
|
|
376
|
+
assert "error" in parsed
|
|
377
|
+
|
|
378
|
+
@pytest.mark.asyncio
|
|
379
|
+
async def test_coord_full_workflow(self, initialized_agent_home: Path):
|
|
380
|
+
"""Create a task via MCP, claim it, then complete it."""
|
|
381
|
+
from skcapstone.coordination import Board
|
|
382
|
+
|
|
383
|
+
board = Board(initialized_agent_home)
|
|
384
|
+
board.ensure_dirs()
|
|
385
|
+
|
|
386
|
+
with (
|
|
387
|
+
patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)),
|
|
388
|
+
patch("skcapstone.mcp_tools._helpers.SHARED_ROOT", str(initialized_agent_home)),
|
|
389
|
+
):
|
|
390
|
+
create_result = await call_tool(
|
|
391
|
+
"coord_create",
|
|
392
|
+
{
|
|
393
|
+
"title": "Test MCP task",
|
|
394
|
+
"priority": "high",
|
|
395
|
+
"tags": ["mcp", "test"],
|
|
396
|
+
"created_by": "mcp-builder",
|
|
397
|
+
},
|
|
398
|
+
)
|
|
399
|
+
create_parsed = _extract_json(create_result)
|
|
400
|
+
assert create_parsed["created"] is True
|
|
401
|
+
task_id = create_parsed["task_id"]
|
|
402
|
+
|
|
403
|
+
status_result = await call_tool("coord_status", {})
|
|
404
|
+
status_parsed = _extract_json(status_result)
|
|
405
|
+
assert status_parsed["summary"]["total"] == 1
|
|
406
|
+
assert status_parsed["tasks"][0]["status"] == "open"
|
|
407
|
+
|
|
408
|
+
claim_result = await call_tool(
|
|
409
|
+
"coord_claim", {"task_id": task_id, "agent_name": "mcp-builder"}
|
|
410
|
+
)
|
|
411
|
+
claim_parsed = _extract_json(claim_result)
|
|
412
|
+
assert claim_parsed["claimed"] is True
|
|
413
|
+
assert claim_parsed["agent"] == "mcp-builder"
|
|
414
|
+
|
|
415
|
+
complete_result = await call_tool(
|
|
416
|
+
"coord_complete",
|
|
417
|
+
{"task_id": task_id, "agent_name": "mcp-builder"},
|
|
418
|
+
)
|
|
419
|
+
complete_parsed = _extract_json(complete_result)
|
|
420
|
+
assert complete_parsed["completed"] is True
|
|
421
|
+
assert task_id in complete_parsed["completed_tasks"]
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
# ---------------------------------------------------------------------------
|
|
425
|
+
# SKComm tool tests (graceful fallback)
|
|
426
|
+
# ---------------------------------------------------------------------------
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
class TestCommTools:
|
|
430
|
+
"""Tests for send_message and check_inbox (SKComm may not be installed)."""
|
|
431
|
+
|
|
432
|
+
@pytest.mark.asyncio
|
|
433
|
+
async def test_send_message_requires_params(self):
|
|
434
|
+
"""send_message without recipient/message returns error."""
|
|
435
|
+
result = await call_tool("send_message", {})
|
|
436
|
+
parsed = _extract_json(result)
|
|
437
|
+
assert "error" in parsed
|
|
438
|
+
|
|
439
|
+
@pytest.mark.asyncio
|
|
440
|
+
async def test_check_inbox_graceful_fallback(self):
|
|
441
|
+
"""check_inbox returns graceful error when SKComm is unavailable."""
|
|
442
|
+
result = await call_tool("check_inbox", {})
|
|
443
|
+
parsed = _extract_json(result)
|
|
444
|
+
# Either returns messages list or graceful error about skcomm
|
|
445
|
+
assert isinstance(parsed, list) or "error" in parsed
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
# ---------------------------------------------------------------------------
|
|
449
|
+
# Sync tool tests
|
|
450
|
+
# ---------------------------------------------------------------------------
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
class TestSyncTools:
|
|
454
|
+
"""Tests for sync_push and sync_pull."""
|
|
455
|
+
|
|
456
|
+
@pytest.mark.asyncio
|
|
457
|
+
async def test_sync_push_no_agent(self, tmp_path: Path):
|
|
458
|
+
"""sync_push with no agent home returns error."""
|
|
459
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(tmp_path / "nope")):
|
|
460
|
+
result = await call_tool("sync_push", {})
|
|
461
|
+
parsed = _extract_json(result)
|
|
462
|
+
assert "error" in parsed
|
|
463
|
+
|
|
464
|
+
@pytest.mark.asyncio
|
|
465
|
+
async def test_sync_pull_empty_inbox(self, initialized_agent_home: Path):
|
|
466
|
+
"""sync_pull with empty inbox returns zero seeds."""
|
|
467
|
+
sync_dir = initialized_agent_home / "sync"
|
|
468
|
+
sync_dir.mkdir(exist_ok=True)
|
|
469
|
+
(sync_dir / "inbox").mkdir(exist_ok=True)
|
|
470
|
+
(sync_dir / "outbox").mkdir(exist_ok=True)
|
|
471
|
+
(sync_dir / "archive").mkdir(exist_ok=True)
|
|
472
|
+
(sync_dir / "sync-manifest.json").write_text(
|
|
473
|
+
json.dumps({"transport": "syncthing", "gpg_encrypt": False})
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
477
|
+
result = await call_tool("sync_pull", {})
|
|
478
|
+
parsed = _extract_json(result)
|
|
479
|
+
assert parsed["pulled"] == 0
|
|
480
|
+
assert parsed["seeds"] == []
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
# ---------------------------------------------------------------------------
|
|
484
|
+
# Trustee Operations MCP tool tests
|
|
485
|
+
# ---------------------------------------------------------------------------
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
class TestTrusteeTools:
|
|
489
|
+
"""Tests for trustee_* MCP tools."""
|
|
490
|
+
|
|
491
|
+
def _setup_deployment(self, home: Path) -> str:
|
|
492
|
+
"""Create a test deployment and return its ID."""
|
|
493
|
+
from datetime import datetime, timezone
|
|
494
|
+
|
|
495
|
+
from skcapstone.team_engine import (
|
|
496
|
+
AgentStatus,
|
|
497
|
+
DeployedAgent,
|
|
498
|
+
TeamDeployment,
|
|
499
|
+
TeamEngine,
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
(home / "deployments").mkdir(parents=True, exist_ok=True)
|
|
503
|
+
(home / "coordination").mkdir(parents=True, exist_ok=True)
|
|
504
|
+
engine = TeamEngine(home=home, provider=None, comms_root=None)
|
|
505
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
506
|
+
deployment = TeamDeployment(
|
|
507
|
+
deployment_id="mcp-test-deploy",
|
|
508
|
+
blueprint_slug="test",
|
|
509
|
+
team_name="MCP Test Team",
|
|
510
|
+
provider="local",
|
|
511
|
+
status="running",
|
|
512
|
+
)
|
|
513
|
+
for name in ("worker-1", "worker-2"):
|
|
514
|
+
deployment.agents[name] = DeployedAgent(
|
|
515
|
+
name=name,
|
|
516
|
+
instance_id=f"mcp-test-deploy/{name}",
|
|
517
|
+
blueprint_slug="test",
|
|
518
|
+
agent_spec_key="worker",
|
|
519
|
+
status=AgentStatus.RUNNING,
|
|
520
|
+
host="localhost",
|
|
521
|
+
last_heartbeat=now,
|
|
522
|
+
started_at=now,
|
|
523
|
+
)
|
|
524
|
+
engine._save_deployment(deployment)
|
|
525
|
+
return "mcp-test-deploy"
|
|
526
|
+
|
|
527
|
+
@pytest.mark.asyncio
|
|
528
|
+
async def test_trustee_deployments_empty(self, initialized_agent_home: Path):
|
|
529
|
+
"""trustee_deployments returns empty list when no deployments."""
|
|
530
|
+
(initialized_agent_home / "deployments").mkdir(exist_ok=True)
|
|
531
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
532
|
+
result = await call_tool("trustee_deployments", {})
|
|
533
|
+
parsed = _extract_json(result)
|
|
534
|
+
assert parsed["count"] == 0
|
|
535
|
+
assert parsed["deployments"] == []
|
|
536
|
+
|
|
537
|
+
@pytest.mark.asyncio
|
|
538
|
+
async def test_trustee_deployments_lists(self, initialized_agent_home: Path):
|
|
539
|
+
"""trustee_deployments lists created deployments."""
|
|
540
|
+
self._setup_deployment(initialized_agent_home)
|
|
541
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
542
|
+
result = await call_tool("trustee_deployments", {})
|
|
543
|
+
parsed = _extract_json(result)
|
|
544
|
+
assert parsed["count"] == 1
|
|
545
|
+
d = parsed["deployments"][0]
|
|
546
|
+
assert d["deployment_id"] == "mcp-test-deploy"
|
|
547
|
+
assert d["agent_count"] == 2
|
|
548
|
+
|
|
549
|
+
@pytest.mark.asyncio
|
|
550
|
+
async def test_trustee_health(self, initialized_agent_home: Path):
|
|
551
|
+
"""trustee_health returns per-agent health."""
|
|
552
|
+
deploy_id = self._setup_deployment(initialized_agent_home)
|
|
553
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
554
|
+
result = await call_tool("trustee_health", {"deployment_id": deploy_id})
|
|
555
|
+
parsed = _extract_json(result)
|
|
556
|
+
assert parsed["deployment_id"] == deploy_id
|
|
557
|
+
assert parsed["summary"]["total"] == 2
|
|
558
|
+
assert parsed["summary"]["healthy"] == 2
|
|
559
|
+
|
|
560
|
+
@pytest.mark.asyncio
|
|
561
|
+
async def test_trustee_health_not_found(self, initialized_agent_home: Path):
|
|
562
|
+
"""trustee_health with bad ID returns error."""
|
|
563
|
+
(initialized_agent_home / "deployments").mkdir(exist_ok=True)
|
|
564
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
565
|
+
result = await call_tool("trustee_health", {"deployment_id": "nope"})
|
|
566
|
+
parsed = _extract_json(result)
|
|
567
|
+
assert "error" in parsed
|
|
568
|
+
|
|
569
|
+
@pytest.mark.asyncio
|
|
570
|
+
async def test_trustee_health_requires_id(self, initialized_agent_home: Path):
|
|
571
|
+
"""trustee_health without deployment_id returns error."""
|
|
572
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
573
|
+
result = await call_tool("trustee_health", {})
|
|
574
|
+
parsed = _extract_json(result)
|
|
575
|
+
assert "error" in parsed
|
|
576
|
+
|
|
577
|
+
@pytest.mark.asyncio
|
|
578
|
+
async def test_trustee_restart(self, initialized_agent_home: Path):
|
|
579
|
+
"""trustee_restart restarts an agent."""
|
|
580
|
+
deploy_id = self._setup_deployment(initialized_agent_home)
|
|
581
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
582
|
+
result = await call_tool(
|
|
583
|
+
"trustee_restart",
|
|
584
|
+
{"deployment_id": deploy_id, "agent_name": "worker-1"},
|
|
585
|
+
)
|
|
586
|
+
parsed = _extract_json(result)
|
|
587
|
+
assert parsed["results"]["worker-1"] == "restarted"
|
|
588
|
+
assert parsed["all_restarted"] is True
|
|
589
|
+
|
|
590
|
+
@pytest.mark.asyncio
|
|
591
|
+
async def test_trustee_restart_all(self, initialized_agent_home: Path):
|
|
592
|
+
"""trustee_restart without agent_name restarts all."""
|
|
593
|
+
deploy_id = self._setup_deployment(initialized_agent_home)
|
|
594
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
595
|
+
result = await call_tool(
|
|
596
|
+
"trustee_restart", {"deployment_id": deploy_id}
|
|
597
|
+
)
|
|
598
|
+
parsed = _extract_json(result)
|
|
599
|
+
assert len(parsed["results"]) == 2
|
|
600
|
+
assert parsed["all_restarted"] is True
|
|
601
|
+
|
|
602
|
+
@pytest.mark.asyncio
|
|
603
|
+
async def test_trustee_scale_up(self, initialized_agent_home: Path):
|
|
604
|
+
"""trustee_scale adds instances."""
|
|
605
|
+
deploy_id = self._setup_deployment(initialized_agent_home)
|
|
606
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
607
|
+
result = await call_tool(
|
|
608
|
+
"trustee_scale",
|
|
609
|
+
{"deployment_id": deploy_id, "agent_spec_key": "worker", "count": 4},
|
|
610
|
+
)
|
|
611
|
+
parsed = _extract_json(result)
|
|
612
|
+
assert parsed["current_count"] == 4
|
|
613
|
+
assert len(parsed["added"]) == 2
|
|
614
|
+
|
|
615
|
+
@pytest.mark.asyncio
|
|
616
|
+
async def test_trustee_scale_requires_all_params(self, initialized_agent_home: Path):
|
|
617
|
+
"""trustee_scale without all params returns error."""
|
|
618
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
619
|
+
result = await call_tool("trustee_scale", {"deployment_id": "x"})
|
|
620
|
+
parsed = _extract_json(result)
|
|
621
|
+
assert "error" in parsed
|
|
622
|
+
|
|
623
|
+
@pytest.mark.asyncio
|
|
624
|
+
async def test_trustee_rotate(self, initialized_agent_home: Path):
|
|
625
|
+
"""trustee_rotate snapshots and redeploys."""
|
|
626
|
+
deploy_id = self._setup_deployment(initialized_agent_home)
|
|
627
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
628
|
+
result = await call_tool(
|
|
629
|
+
"trustee_rotate",
|
|
630
|
+
{"deployment_id": deploy_id, "agent_name": "worker-1"},
|
|
631
|
+
)
|
|
632
|
+
parsed = _extract_json(result)
|
|
633
|
+
assert parsed["deployment_id"] == deploy_id
|
|
634
|
+
assert parsed["agent_name"] == "worker-1"
|
|
635
|
+
assert "snapshot_path" in parsed
|
|
636
|
+
|
|
637
|
+
@pytest.mark.asyncio
|
|
638
|
+
async def test_trustee_rotate_requires_params(self, initialized_agent_home: Path):
|
|
639
|
+
"""trustee_rotate without both params returns error."""
|
|
640
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
641
|
+
result = await call_tool("trustee_rotate", {"deployment_id": "x"})
|
|
642
|
+
parsed = _extract_json(result)
|
|
643
|
+
assert "error" in parsed
|
|
644
|
+
|
|
645
|
+
@pytest.mark.asyncio
|
|
646
|
+
async def test_trustee_monitor_all(self, initialized_agent_home: Path):
|
|
647
|
+
"""trustee_monitor runs a monitoring pass over all deployments."""
|
|
648
|
+
self._setup_deployment(initialized_agent_home)
|
|
649
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
650
|
+
result = await call_tool("trustee_monitor", {})
|
|
651
|
+
parsed = _extract_json(result)
|
|
652
|
+
assert parsed["deployments_checked"] == 1
|
|
653
|
+
assert parsed["agents_healthy"] == 2
|
|
654
|
+
assert parsed["agents_degraded"] == 0
|
|
655
|
+
|
|
656
|
+
@pytest.mark.asyncio
|
|
657
|
+
async def test_trustee_monitor_single(self, initialized_agent_home: Path):
|
|
658
|
+
"""trustee_monitor checks a specific deployment."""
|
|
659
|
+
deploy_id = self._setup_deployment(initialized_agent_home)
|
|
660
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
661
|
+
result = await call_tool(
|
|
662
|
+
"trustee_monitor", {"deployment_id": deploy_id}
|
|
663
|
+
)
|
|
664
|
+
parsed = _extract_json(result)
|
|
665
|
+
assert parsed["deployments_checked"] == 1
|
|
666
|
+
assert parsed["agents_healthy"] == 2
|
|
667
|
+
|
|
668
|
+
@pytest.mark.asyncio
|
|
669
|
+
async def test_trustee_monitor_not_found(self, initialized_agent_home: Path):
|
|
670
|
+
"""trustee_monitor with bad deployment_id returns error."""
|
|
671
|
+
(initialized_agent_home / "deployments").mkdir(exist_ok=True)
|
|
672
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
673
|
+
result = await call_tool(
|
|
674
|
+
"trustee_monitor", {"deployment_id": "nope"}
|
|
675
|
+
)
|
|
676
|
+
parsed = _extract_json(result)
|
|
677
|
+
assert "error" in parsed
|
|
678
|
+
|
|
679
|
+
@pytest.mark.asyncio
|
|
680
|
+
async def test_trustee_logs(self, initialized_agent_home: Path):
|
|
681
|
+
"""trustee_logs returns log lines."""
|
|
682
|
+
deploy_id = self._setup_deployment(initialized_agent_home)
|
|
683
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
684
|
+
result = await call_tool(
|
|
685
|
+
"trustee_logs", {"deployment_id": deploy_id}
|
|
686
|
+
)
|
|
687
|
+
parsed = _extract_json(result)
|
|
688
|
+
assert parsed["deployment_id"] == deploy_id
|
|
689
|
+
assert "worker-1" in parsed["agents"]
|
|
690
|
+
assert "worker-2" in parsed["agents"]
|
|
691
|
+
|
|
692
|
+
@pytest.mark.asyncio
|
|
693
|
+
async def test_trustee_logs_requires_id(self, initialized_agent_home: Path):
|
|
694
|
+
"""trustee_logs without deployment_id returns error."""
|
|
695
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
696
|
+
result = await call_tool("trustee_logs", {})
|
|
697
|
+
parsed = _extract_json(result)
|
|
698
|
+
assert "error" in parsed
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
# ---------------------------------------------------------------------------
|
|
702
|
+
# SKChat MCP tool tests
|
|
703
|
+
# ---------------------------------------------------------------------------
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
class TestSKChatTools:
|
|
707
|
+
"""Tests for skchat_send, skchat_inbox, skchat_group_create, skchat_group_send."""
|
|
708
|
+
|
|
709
|
+
@pytest.mark.asyncio
|
|
710
|
+
async def test_skchat_send_requires_params(self):
|
|
711
|
+
"""skchat_send without recipient/message returns error."""
|
|
712
|
+
with patch("skcapstone.mcp_tools.chat_tools._get_skchat_identity", return_value="capauth:test@local"):
|
|
713
|
+
result = await call_tool("skchat_send", {})
|
|
714
|
+
parsed = _extract_json(result)
|
|
715
|
+
assert "error" in parsed
|
|
716
|
+
|
|
717
|
+
@pytest.mark.asyncio
|
|
718
|
+
async def test_skchat_send_requires_message(self):
|
|
719
|
+
"""skchat_send with only recipient returns error."""
|
|
720
|
+
with patch("skcapstone.mcp_tools.chat_tools._get_skchat_identity", return_value="capauth:test@local"):
|
|
721
|
+
result = await call_tool("skchat_send", {"recipient": "lumina"})
|
|
722
|
+
parsed = _extract_json(result)
|
|
723
|
+
assert "error" in parsed
|
|
724
|
+
|
|
725
|
+
@pytest.mark.asyncio
|
|
726
|
+
async def test_skchat_send_success(self):
|
|
727
|
+
"""skchat_send calls AgentMessenger.send and returns result."""
|
|
728
|
+
mock_messenger = type("M", (), {
|
|
729
|
+
"send": lambda self, **kw: {
|
|
730
|
+
"message_id": "msg-123",
|
|
731
|
+
"delivered": True,
|
|
732
|
+
"transport": "syncthing",
|
|
733
|
+
},
|
|
734
|
+
})()
|
|
735
|
+
|
|
736
|
+
with (
|
|
737
|
+
patch("skcapstone.mcp_tools.chat_tools._get_skchat_identity", return_value="capauth:opus@local"),
|
|
738
|
+
patch("skcapstone.mcp_tools.chat_tools._resolve_recipient", return_value="capauth:lumina@local"),
|
|
739
|
+
patch("skchat.agent_comm.AgentMessenger.from_identity", return_value=mock_messenger),
|
|
740
|
+
):
|
|
741
|
+
result = await call_tool(
|
|
742
|
+
"skchat_send",
|
|
743
|
+
{"recipient": "lumina", "message": "Hello!"},
|
|
744
|
+
)
|
|
745
|
+
parsed = _extract_json(result)
|
|
746
|
+
assert parsed["sent"] is True
|
|
747
|
+
assert parsed["message_id"] == "msg-123"
|
|
748
|
+
assert parsed["delivered"] is True
|
|
749
|
+
assert parsed["recipient"] == "capauth:lumina@local"
|
|
750
|
+
|
|
751
|
+
@pytest.mark.asyncio
|
|
752
|
+
async def test_skchat_send_with_thread(self):
|
|
753
|
+
"""skchat_send passes thread_id and message_type to messenger."""
|
|
754
|
+
received_kwargs = {}
|
|
755
|
+
|
|
756
|
+
def capture_send(**kw):
|
|
757
|
+
received_kwargs.update(kw)
|
|
758
|
+
return {"message_id": "msg-456", "delivered": False}
|
|
759
|
+
|
|
760
|
+
mock_messenger = type("M", (), {"send": lambda self, **kw: capture_send(**kw)})()
|
|
761
|
+
|
|
762
|
+
with (
|
|
763
|
+
patch("skcapstone.mcp_tools.chat_tools._get_skchat_identity", return_value="capauth:opus@local"),
|
|
764
|
+
patch("skcapstone.mcp_tools.chat_tools._resolve_recipient", return_value="capauth:jarvis@local"),
|
|
765
|
+
patch("skchat.agent_comm.AgentMessenger.from_identity", return_value=mock_messenger),
|
|
766
|
+
):
|
|
767
|
+
result = await call_tool(
|
|
768
|
+
"skchat_send",
|
|
769
|
+
{
|
|
770
|
+
"recipient": "jarvis",
|
|
771
|
+
"message": "Bug report",
|
|
772
|
+
"message_type": "finding",
|
|
773
|
+
"thread_id": "thread-abc",
|
|
774
|
+
},
|
|
775
|
+
)
|
|
776
|
+
parsed = _extract_json(result)
|
|
777
|
+
assert parsed["sent"] is True
|
|
778
|
+
assert received_kwargs["message_type"] == "finding"
|
|
779
|
+
assert received_kwargs["thread_id"] == "thread-abc"
|
|
780
|
+
|
|
781
|
+
@pytest.mark.asyncio
|
|
782
|
+
async def test_skchat_send_no_skchat(self):
|
|
783
|
+
"""skchat_send returns error when skchat is not installed."""
|
|
784
|
+
with patch.dict("sys.modules", {"skchat": None, "skchat.agent_comm": None}):
|
|
785
|
+
result = await call_tool(
|
|
786
|
+
"skchat_send",
|
|
787
|
+
{"recipient": "lumina", "message": "Hello"},
|
|
788
|
+
)
|
|
789
|
+
parsed = _extract_json(result)
|
|
790
|
+
assert "error" in parsed
|
|
791
|
+
|
|
792
|
+
@pytest.mark.asyncio
|
|
793
|
+
async def test_skchat_inbox_empty(self):
|
|
794
|
+
"""skchat_inbox returns empty list when no messages."""
|
|
795
|
+
mock_messenger = type("M", (), {
|
|
796
|
+
"receive": lambda self, limit=50: [],
|
|
797
|
+
})()
|
|
798
|
+
|
|
799
|
+
with (
|
|
800
|
+
patch("skcapstone.mcp_tools.chat_tools._get_skchat_identity", return_value="capauth:opus@local"),
|
|
801
|
+
patch("skchat.agent_comm.AgentMessenger.from_identity", return_value=mock_messenger),
|
|
802
|
+
):
|
|
803
|
+
result = await call_tool("skchat_inbox", {})
|
|
804
|
+
parsed = _extract_json(result)
|
|
805
|
+
assert parsed["count"] == 0
|
|
806
|
+
assert parsed["messages"] == []
|
|
807
|
+
|
|
808
|
+
@pytest.mark.asyncio
|
|
809
|
+
async def test_skchat_inbox_with_messages(self):
|
|
810
|
+
"""skchat_inbox returns messages from AgentMessenger."""
|
|
811
|
+
mock_messenger = type("M", (), {
|
|
812
|
+
"receive": lambda self, limit=50: [
|
|
813
|
+
{
|
|
814
|
+
"message_id": "m1",
|
|
815
|
+
"sender": "capauth:lumina@local",
|
|
816
|
+
"content": "Hello from Lumina",
|
|
817
|
+
"message_type": "text",
|
|
818
|
+
"thread_id": None,
|
|
819
|
+
"timestamp": "2026-02-27T10:00:00",
|
|
820
|
+
},
|
|
821
|
+
{
|
|
822
|
+
"message_id": "m2",
|
|
823
|
+
"sender": "capauth:jarvis@local",
|
|
824
|
+
"content": "Bug found in transport.py",
|
|
825
|
+
"message_type": "finding",
|
|
826
|
+
"thread_id": "thread-x",
|
|
827
|
+
"timestamp": "2026-02-27T10:01:00",
|
|
828
|
+
},
|
|
829
|
+
],
|
|
830
|
+
})()
|
|
831
|
+
|
|
832
|
+
with (
|
|
833
|
+
patch("skcapstone.mcp_tools.chat_tools._get_skchat_identity", return_value="capauth:opus@local"),
|
|
834
|
+
patch("skchat.agent_comm.AgentMessenger.from_identity", return_value=mock_messenger),
|
|
835
|
+
):
|
|
836
|
+
result = await call_tool("skchat_inbox", {"limit": 10})
|
|
837
|
+
parsed = _extract_json(result)
|
|
838
|
+
assert parsed["count"] == 2
|
|
839
|
+
assert parsed["messages"][0]["sender"] == "capauth:lumina@local"
|
|
840
|
+
assert parsed["messages"][1]["message_type"] == "finding"
|
|
841
|
+
|
|
842
|
+
@pytest.mark.asyncio
|
|
843
|
+
async def test_skchat_inbox_filter_by_type(self):
|
|
844
|
+
"""skchat_inbox filters messages by message_type."""
|
|
845
|
+
mock_messenger = type("M", (), {
|
|
846
|
+
"receive": lambda self, limit=50: [
|
|
847
|
+
{"message_id": "m1", "sender": "a", "content": "hi", "message_type": "text", "thread_id": None, "timestamp": ""},
|
|
848
|
+
{"message_id": "m2", "sender": "b", "content": "bug", "message_type": "finding", "thread_id": None, "timestamp": ""},
|
|
849
|
+
],
|
|
850
|
+
})()
|
|
851
|
+
|
|
852
|
+
with (
|
|
853
|
+
patch("skcapstone.mcp_tools.chat_tools._get_skchat_identity", return_value="capauth:opus@local"),
|
|
854
|
+
patch("skchat.agent_comm.AgentMessenger.from_identity", return_value=mock_messenger),
|
|
855
|
+
):
|
|
856
|
+
result = await call_tool("skchat_inbox", {"message_type": "finding"})
|
|
857
|
+
parsed = _extract_json(result)
|
|
858
|
+
assert parsed["count"] == 1
|
|
859
|
+
assert parsed["messages"][0]["message_type"] == "finding"
|
|
860
|
+
|
|
861
|
+
@pytest.mark.asyncio
|
|
862
|
+
async def test_skchat_group_create_requires_name(self):
|
|
863
|
+
"""skchat_group_create without name returns error."""
|
|
864
|
+
with patch("skcapstone.mcp_tools.chat_tools._get_skchat_identity", return_value="capauth:opus@local"):
|
|
865
|
+
result = await call_tool("skchat_group_create", {})
|
|
866
|
+
parsed = _extract_json(result)
|
|
867
|
+
assert "error" in parsed
|
|
868
|
+
|
|
869
|
+
@pytest.mark.asyncio
|
|
870
|
+
async def test_skchat_group_create_success(self):
|
|
871
|
+
"""skchat_group_create creates a group and stores it."""
|
|
872
|
+
mock_history = type("H", (), {
|
|
873
|
+
"store_thread": lambda self, t: "mem-abc",
|
|
874
|
+
})()
|
|
875
|
+
|
|
876
|
+
with (
|
|
877
|
+
patch("skcapstone.mcp_tools.chat_tools._get_skchat_identity", return_value="capauth:opus@local"),
|
|
878
|
+
patch("skcapstone.mcp_tools.chat_tools._get_skchat_history", return_value=mock_history),
|
|
879
|
+
):
|
|
880
|
+
result = await call_tool(
|
|
881
|
+
"skchat_group_create",
|
|
882
|
+
{"name": "Test Squad", "description": "For testing"},
|
|
883
|
+
)
|
|
884
|
+
parsed = _extract_json(result)
|
|
885
|
+
assert parsed["created"] is True
|
|
886
|
+
assert parsed["name"] == "Test Squad"
|
|
887
|
+
assert parsed["admin"] == "capauth:opus@local"
|
|
888
|
+
assert "capauth:opus@local" in parsed["members"]
|
|
889
|
+
|
|
890
|
+
@pytest.mark.asyncio
|
|
891
|
+
async def test_skchat_group_create_with_members(self):
|
|
892
|
+
"""skchat_group_create adds initial members."""
|
|
893
|
+
mock_history = type("H", (), {
|
|
894
|
+
"store_thread": lambda self, t: "mem-xyz",
|
|
895
|
+
})()
|
|
896
|
+
|
|
897
|
+
with (
|
|
898
|
+
patch("skcapstone.mcp_tools.chat_tools._get_skchat_identity", return_value="capauth:opus@local"),
|
|
899
|
+
patch("skcapstone.mcp_tools.chat_tools._get_skchat_history", return_value=mock_history),
|
|
900
|
+
patch("skcapstone.mcp_tools.chat_tools._resolve_recipient", side_effect=lambda n: f"capauth:{n}@local"),
|
|
901
|
+
):
|
|
902
|
+
result = await call_tool(
|
|
903
|
+
"skchat_group_create",
|
|
904
|
+
{"name": "Alpha Team", "members": ["lumina", "jarvis"]},
|
|
905
|
+
)
|
|
906
|
+
parsed = _extract_json(result)
|
|
907
|
+
assert parsed["created"] is True
|
|
908
|
+
assert len(parsed["members"]) == 3 # opus + lumina + jarvis
|
|
909
|
+
assert len(parsed["members_added"]) == 2
|
|
910
|
+
|
|
911
|
+
@pytest.mark.asyncio
|
|
912
|
+
async def test_skchat_group_send_requires_params(self):
|
|
913
|
+
"""skchat_group_send without group_id/message returns error."""
|
|
914
|
+
result = await call_tool("skchat_group_send", {})
|
|
915
|
+
parsed = _extract_json(result)
|
|
916
|
+
assert "error" in parsed
|
|
917
|
+
|
|
918
|
+
@pytest.mark.asyncio
|
|
919
|
+
async def test_skchat_group_send_not_found(self):
|
|
920
|
+
"""skchat_group_send with unknown group returns error."""
|
|
921
|
+
mock_history = type("H", (), {
|
|
922
|
+
"get_thread": lambda self, gid: None,
|
|
923
|
+
})()
|
|
924
|
+
|
|
925
|
+
with patch("skcapstone.mcp_tools.chat_tools._get_skchat_history", return_value=mock_history):
|
|
926
|
+
result = await call_tool(
|
|
927
|
+
"skchat_group_send",
|
|
928
|
+
{"group_id": "nonexistent", "message": "Hello"},
|
|
929
|
+
)
|
|
930
|
+
parsed = _extract_json(result)
|
|
931
|
+
assert "error" in parsed
|
|
932
|
+
assert "not found" in parsed["error"].lower()
|
|
933
|
+
|
|
934
|
+
@pytest.mark.asyncio
|
|
935
|
+
async def test_skchat_group_send_not_a_group(self):
|
|
936
|
+
"""skchat_group_send on a plain thread (no group_data) returns error."""
|
|
937
|
+
mock_history = type("H", (), {
|
|
938
|
+
"get_thread": lambda self, gid: {"title": "Just a thread"},
|
|
939
|
+
})()
|
|
940
|
+
|
|
941
|
+
with patch("skcapstone.mcp_tools.chat_tools._get_skchat_history", return_value=mock_history):
|
|
942
|
+
result = await call_tool(
|
|
943
|
+
"skchat_group_send",
|
|
944
|
+
{"group_id": "thread-123", "message": "Hello"},
|
|
945
|
+
)
|
|
946
|
+
parsed = _extract_json(result)
|
|
947
|
+
assert "error" in parsed
|
|
948
|
+
assert "not a group" in parsed["error"].lower()
|
|
949
|
+
|
|
950
|
+
@pytest.mark.asyncio
|
|
951
|
+
async def test_skchat_group_send_success(self):
|
|
952
|
+
"""skchat_group_send stores message and returns confirmation."""
|
|
953
|
+
from datetime import datetime, timezone
|
|
954
|
+
|
|
955
|
+
group_data = {
|
|
956
|
+
"id": "grp-abc",
|
|
957
|
+
"name": "Test Group",
|
|
958
|
+
"description": "",
|
|
959
|
+
"members": [
|
|
960
|
+
{
|
|
961
|
+
"identity_uri": "capauth:opus@local",
|
|
962
|
+
"role": "admin",
|
|
963
|
+
"participant_type": "agent",
|
|
964
|
+
"display_name": "opus",
|
|
965
|
+
"public_key_armor": "",
|
|
966
|
+
"joined_at": datetime.now(timezone.utc).isoformat(),
|
|
967
|
+
"tool_scope": [],
|
|
968
|
+
},
|
|
969
|
+
],
|
|
970
|
+
"created_by": "capauth:opus@local",
|
|
971
|
+
"created_at": datetime.now(timezone.utc).isoformat(),
|
|
972
|
+
"updated_at": datetime.now(timezone.utc).isoformat(),
|
|
973
|
+
"message_count": 0,
|
|
974
|
+
"group_key": "a" * 64,
|
|
975
|
+
"key_version": 1,
|
|
976
|
+
"metadata": {},
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
mock_history = type("H", (), {
|
|
980
|
+
"get_thread": lambda self, gid: {"group_data": group_data},
|
|
981
|
+
"store_message": lambda self, msg: "mem-stored",
|
|
982
|
+
})()
|
|
983
|
+
|
|
984
|
+
with (
|
|
985
|
+
patch("skcapstone.mcp_tools.chat_tools._get_skchat_identity", return_value="capauth:opus@local"),
|
|
986
|
+
patch("skcapstone.mcp_tools.chat_tools._get_skchat_history", return_value=mock_history),
|
|
987
|
+
):
|
|
988
|
+
result = await call_tool(
|
|
989
|
+
"skchat_group_send",
|
|
990
|
+
{"group_id": "grp-abc", "message": "Hello team!"},
|
|
991
|
+
)
|
|
992
|
+
parsed = _extract_json(result)
|
|
993
|
+
assert parsed["sent"] is True
|
|
994
|
+
assert parsed["group_id"] == "grp-abc"
|
|
995
|
+
assert parsed["group_name"] == "Test Group"
|
|
996
|
+
assert parsed["stored"] is True
|
|
997
|
+
|
|
998
|
+
|
|
999
|
+
# ---------------------------------------------------------------------------
|
|
1000
|
+
# Unit tests: heartbeat tools
|
|
1001
|
+
# ---------------------------------------------------------------------------
|
|
1002
|
+
|
|
1003
|
+
|
|
1004
|
+
class TestHeartbeatTools:
|
|
1005
|
+
"""Tests for heartbeat MCP tools."""
|
|
1006
|
+
|
|
1007
|
+
@pytest.mark.asyncio
|
|
1008
|
+
async def test_heartbeat_pulse(self, tmp_path):
|
|
1009
|
+
"""heartbeat_pulse publishes a heartbeat."""
|
|
1010
|
+
identity_dir = tmp_path / "identity"
|
|
1011
|
+
identity_dir.mkdir()
|
|
1012
|
+
(identity_dir / "identity.json").write_text(
|
|
1013
|
+
json.dumps({"name": "opus", "fingerprint": "ABCD1234"}),
|
|
1014
|
+
encoding="utf-8",
|
|
1015
|
+
)
|
|
1016
|
+
|
|
1017
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(tmp_path)):
|
|
1018
|
+
result = await call_tool("heartbeat_pulse", {"status": "alive"})
|
|
1019
|
+
parsed = _extract_json(result)
|
|
1020
|
+
assert parsed["agent_name"] == "opus"
|
|
1021
|
+
assert parsed["status"] == "alive"
|
|
1022
|
+
assert parsed["capacity"]["cpu_count"] > 0
|
|
1023
|
+
|
|
1024
|
+
@pytest.mark.asyncio
|
|
1025
|
+
async def test_heartbeat_peers(self, tmp_path):
|
|
1026
|
+
"""heartbeat_peers discovers mesh peers."""
|
|
1027
|
+
identity_dir = tmp_path / "identity"
|
|
1028
|
+
identity_dir.mkdir()
|
|
1029
|
+
(identity_dir / "identity.json").write_text(
|
|
1030
|
+
json.dumps({"name": "opus", "fingerprint": "ABCD1234"}),
|
|
1031
|
+
encoding="utf-8",
|
|
1032
|
+
)
|
|
1033
|
+
|
|
1034
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(tmp_path)):
|
|
1035
|
+
# Pulse first to create own heartbeat
|
|
1036
|
+
await call_tool("heartbeat_pulse", {})
|
|
1037
|
+
result = await call_tool("heartbeat_peers", {"include_self": True})
|
|
1038
|
+
parsed = _extract_json(result)
|
|
1039
|
+
assert len(parsed) >= 1
|
|
1040
|
+
assert parsed[0]["agent_name"] == "opus"
|
|
1041
|
+
|
|
1042
|
+
@pytest.mark.asyncio
|
|
1043
|
+
async def test_heartbeat_health(self, tmp_path):
|
|
1044
|
+
"""heartbeat_health returns mesh summary."""
|
|
1045
|
+
identity_dir = tmp_path / "identity"
|
|
1046
|
+
identity_dir.mkdir()
|
|
1047
|
+
(identity_dir / "identity.json").write_text(
|
|
1048
|
+
json.dumps({"name": "opus", "fingerprint": "ABCD1234"}),
|
|
1049
|
+
encoding="utf-8",
|
|
1050
|
+
)
|
|
1051
|
+
|
|
1052
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(tmp_path)):
|
|
1053
|
+
await call_tool("heartbeat_pulse", {})
|
|
1054
|
+
result = await call_tool("heartbeat_health", {})
|
|
1055
|
+
parsed = _extract_json(result)
|
|
1056
|
+
assert parsed["total_peers"] >= 1
|
|
1057
|
+
assert parsed["alive_peers"] >= 1
|
|
1058
|
+
|
|
1059
|
+
@pytest.mark.asyncio
|
|
1060
|
+
async def test_heartbeat_find_capable(self, tmp_path):
|
|
1061
|
+
"""heartbeat_find_capable searches by capability."""
|
|
1062
|
+
identity_dir = tmp_path / "identity"
|
|
1063
|
+
identity_dir.mkdir()
|
|
1064
|
+
(identity_dir / "identity.json").write_text(
|
|
1065
|
+
json.dumps({"name": "opus", "fingerprint": "ABCD1234"}),
|
|
1066
|
+
encoding="utf-8",
|
|
1067
|
+
)
|
|
1068
|
+
|
|
1069
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(tmp_path)):
|
|
1070
|
+
result = await call_tool(
|
|
1071
|
+
"heartbeat_find_capable", {"capability": "nonexistent"},
|
|
1072
|
+
)
|
|
1073
|
+
parsed = _extract_json(result)
|
|
1074
|
+
assert parsed["capability"] == "nonexistent"
|
|
1075
|
+
assert parsed["peers"] == []
|
|
1076
|
+
|
|
1077
|
+
|
|
1078
|
+
# ---------------------------------------------------------------------------
|
|
1079
|
+
# Unit tests: file transfer tools
|
|
1080
|
+
# ---------------------------------------------------------------------------
|
|
1081
|
+
|
|
1082
|
+
|
|
1083
|
+
class TestFileTransferTools:
|
|
1084
|
+
"""Tests for file transfer MCP tools."""
|
|
1085
|
+
|
|
1086
|
+
@pytest.mark.asyncio
|
|
1087
|
+
async def test_file_send(self, tmp_path):
|
|
1088
|
+
"""file_send creates a transfer."""
|
|
1089
|
+
identity_dir = tmp_path / "identity"
|
|
1090
|
+
identity_dir.mkdir()
|
|
1091
|
+
(identity_dir / "identity.json").write_text(
|
|
1092
|
+
json.dumps({"name": "opus", "fingerprint": "ABCD1234"}),
|
|
1093
|
+
encoding="utf-8",
|
|
1094
|
+
)
|
|
1095
|
+
(tmp_path / "security").mkdir()
|
|
1096
|
+
|
|
1097
|
+
test_file = tmp_path / "test.txt"
|
|
1098
|
+
test_file.write_text("Hello world!", encoding="utf-8")
|
|
1099
|
+
|
|
1100
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(tmp_path)):
|
|
1101
|
+
result = await call_tool("file_send", {
|
|
1102
|
+
"file_path": str(test_file),
|
|
1103
|
+
"recipient": "lumina",
|
|
1104
|
+
"encrypt": False,
|
|
1105
|
+
})
|
|
1106
|
+
parsed = _extract_json(result)
|
|
1107
|
+
assert parsed["filename"] == "test.txt"
|
|
1108
|
+
assert parsed["sender"] == "opus"
|
|
1109
|
+
assert parsed["recipient"] == "lumina"
|
|
1110
|
+
assert parsed["total_chunks"] >= 1
|
|
1111
|
+
|
|
1112
|
+
@pytest.mark.asyncio
|
|
1113
|
+
async def test_file_list_empty(self, tmp_path):
|
|
1114
|
+
"""file_list returns empty for fresh system."""
|
|
1115
|
+
identity_dir = tmp_path / "identity"
|
|
1116
|
+
identity_dir.mkdir()
|
|
1117
|
+
(identity_dir / "identity.json").write_text(
|
|
1118
|
+
json.dumps({"name": "opus"}), encoding="utf-8",
|
|
1119
|
+
)
|
|
1120
|
+
|
|
1121
|
+
with (
|
|
1122
|
+
patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(tmp_path)),
|
|
1123
|
+
patch("skcapstone.mcp_tools._helpers.SHARED_ROOT", str(tmp_path)),
|
|
1124
|
+
):
|
|
1125
|
+
result = await call_tool("file_list", {})
|
|
1126
|
+
parsed = _extract_json(result)
|
|
1127
|
+
assert parsed == []
|
|
1128
|
+
|
|
1129
|
+
@pytest.mark.asyncio
|
|
1130
|
+
async def test_file_status(self, tmp_path):
|
|
1131
|
+
"""file_status returns subsystem summary."""
|
|
1132
|
+
identity_dir = tmp_path / "identity"
|
|
1133
|
+
identity_dir.mkdir()
|
|
1134
|
+
(identity_dir / "identity.json").write_text(
|
|
1135
|
+
json.dumps({"name": "opus"}), encoding="utf-8",
|
|
1136
|
+
)
|
|
1137
|
+
|
|
1138
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(tmp_path)):
|
|
1139
|
+
result = await call_tool("file_status", {})
|
|
1140
|
+
parsed = _extract_json(result)
|
|
1141
|
+
assert "outbox_transfers" in parsed
|
|
1142
|
+
assert "inbox_transfers" in parsed
|
|
1143
|
+
|
|
1144
|
+
@pytest.mark.asyncio
|
|
1145
|
+
async def test_file_send_and_receive(self, tmp_path):
|
|
1146
|
+
"""file_send then file_receive round-trips correctly."""
|
|
1147
|
+
identity_dir = tmp_path / "identity"
|
|
1148
|
+
identity_dir.mkdir()
|
|
1149
|
+
(identity_dir / "identity.json").write_text(
|
|
1150
|
+
json.dumps({"name": "opus", "fingerprint": "ABCD1234"}),
|
|
1151
|
+
encoding="utf-8",
|
|
1152
|
+
)
|
|
1153
|
+
(tmp_path / "security").mkdir()
|
|
1154
|
+
|
|
1155
|
+
test_file = tmp_path / "roundtrip.txt"
|
|
1156
|
+
test_file.write_text("Round trip test data!", encoding="utf-8")
|
|
1157
|
+
|
|
1158
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(tmp_path)):
|
|
1159
|
+
send_result = await call_tool("file_send", {
|
|
1160
|
+
"file_path": str(test_file),
|
|
1161
|
+
"recipient": "lumina",
|
|
1162
|
+
"encrypt": False,
|
|
1163
|
+
})
|
|
1164
|
+
transfer_id = _extract_json(send_result)["transfer_id"]
|
|
1165
|
+
|
|
1166
|
+
recv_result = await call_tool("file_receive", {
|
|
1167
|
+
"transfer_id": transfer_id,
|
|
1168
|
+
"output_dir": str(tmp_path / "downloads"),
|
|
1169
|
+
})
|
|
1170
|
+
parsed = _extract_json(recv_result)
|
|
1171
|
+
assert parsed["transfer_id"] == transfer_id
|
|
1172
|
+
output = Path(parsed["output_path"])
|
|
1173
|
+
assert output.read_text(encoding="utf-8") == "Round trip test data!"
|
|
1174
|
+
|
|
1175
|
+
|
|
1176
|
+
# ---------------------------------------------------------------------------
|
|
1177
|
+
# Unit tests: pub/sub tools
|
|
1178
|
+
# ---------------------------------------------------------------------------
|
|
1179
|
+
|
|
1180
|
+
|
|
1181
|
+
class TestPubSubTools:
|
|
1182
|
+
"""Tests for pub/sub MCP tools."""
|
|
1183
|
+
|
|
1184
|
+
@pytest.mark.asyncio
|
|
1185
|
+
async def test_pubsub_publish(self, tmp_path):
|
|
1186
|
+
"""pubsub_publish sends a message to a topic."""
|
|
1187
|
+
identity_dir = tmp_path / "identity"
|
|
1188
|
+
identity_dir.mkdir()
|
|
1189
|
+
(identity_dir / "identity.json").write_text(
|
|
1190
|
+
json.dumps({"name": "opus"}), encoding="utf-8",
|
|
1191
|
+
)
|
|
1192
|
+
|
|
1193
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(tmp_path)):
|
|
1194
|
+
result = await call_tool("pubsub_publish", {
|
|
1195
|
+
"topic": "test.events",
|
|
1196
|
+
"payload": {"event": "hello"},
|
|
1197
|
+
})
|
|
1198
|
+
parsed = _extract_json(result)
|
|
1199
|
+
assert parsed["topic"] == "test.events"
|
|
1200
|
+
assert parsed["sender"] == "opus"
|
|
1201
|
+
assert "message_id" in parsed
|
|
1202
|
+
|
|
1203
|
+
@pytest.mark.asyncio
|
|
1204
|
+
async def test_pubsub_subscribe(self, tmp_path):
|
|
1205
|
+
"""pubsub_subscribe creates a subscription."""
|
|
1206
|
+
identity_dir = tmp_path / "identity"
|
|
1207
|
+
identity_dir.mkdir()
|
|
1208
|
+
(identity_dir / "identity.json").write_text(
|
|
1209
|
+
json.dumps({"name": "opus"}), encoding="utf-8",
|
|
1210
|
+
)
|
|
1211
|
+
|
|
1212
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(tmp_path)):
|
|
1213
|
+
result = await call_tool("pubsub_subscribe", {"pattern": "test.*"})
|
|
1214
|
+
parsed = _extract_json(result)
|
|
1215
|
+
assert parsed["pattern"] == "test.*"
|
|
1216
|
+
|
|
1217
|
+
@pytest.mark.asyncio
|
|
1218
|
+
async def test_pubsub_poll(self, tmp_path):
|
|
1219
|
+
"""pubsub_poll retrieves messages."""
|
|
1220
|
+
identity_dir = tmp_path / "identity"
|
|
1221
|
+
identity_dir.mkdir()
|
|
1222
|
+
(identity_dir / "identity.json").write_text(
|
|
1223
|
+
json.dumps({"name": "opus"}), encoding="utf-8",
|
|
1224
|
+
)
|
|
1225
|
+
|
|
1226
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(tmp_path)):
|
|
1227
|
+
# Subscribe and publish
|
|
1228
|
+
await call_tool("pubsub_subscribe", {"pattern": "test.*"})
|
|
1229
|
+
await call_tool("pubsub_publish", {
|
|
1230
|
+
"topic": "test.events",
|
|
1231
|
+
"payload": {"event": "ping"},
|
|
1232
|
+
})
|
|
1233
|
+
result = await call_tool("pubsub_poll", {})
|
|
1234
|
+
parsed = _extract_json(result)
|
|
1235
|
+
assert len(parsed) >= 1
|
|
1236
|
+
assert parsed[0]["topic"] == "test.events"
|
|
1237
|
+
|
|
1238
|
+
@pytest.mark.asyncio
|
|
1239
|
+
async def test_pubsub_topics(self, tmp_path):
|
|
1240
|
+
"""pubsub_topics lists available topics."""
|
|
1241
|
+
identity_dir = tmp_path / "identity"
|
|
1242
|
+
identity_dir.mkdir()
|
|
1243
|
+
(identity_dir / "identity.json").write_text(
|
|
1244
|
+
json.dumps({"name": "opus"}), encoding="utf-8",
|
|
1245
|
+
)
|
|
1246
|
+
|
|
1247
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(tmp_path)):
|
|
1248
|
+
await call_tool("pubsub_publish", {
|
|
1249
|
+
"topic": "agent.status",
|
|
1250
|
+
"payload": {"status": "alive"},
|
|
1251
|
+
})
|
|
1252
|
+
result = await call_tool("pubsub_topics", {})
|
|
1253
|
+
parsed = _extract_json(result)
|
|
1254
|
+
assert len(parsed) >= 1
|
|
1255
|
+
topics = [t["topic"] for t in parsed]
|
|
1256
|
+
assert "agent.status" in topics
|
|
1257
|
+
|
|
1258
|
+
|
|
1259
|
+
# ---------------------------------------------------------------------------
|
|
1260
|
+
# Unit tests: fortress tools
|
|
1261
|
+
# ---------------------------------------------------------------------------
|
|
1262
|
+
|
|
1263
|
+
|
|
1264
|
+
class TestFortressTools:
|
|
1265
|
+
"""Tests for memory fortress MCP tools."""
|
|
1266
|
+
|
|
1267
|
+
@pytest.mark.asyncio
|
|
1268
|
+
async def test_fortress_status(self, tmp_path):
|
|
1269
|
+
"""fortress_status returns fortress state."""
|
|
1270
|
+
identity_dir = tmp_path / "identity"
|
|
1271
|
+
identity_dir.mkdir()
|
|
1272
|
+
(identity_dir / "identity.json").write_text(
|
|
1273
|
+
json.dumps({"name": "opus", "fingerprint": "ABCD1234"}),
|
|
1274
|
+
encoding="utf-8",
|
|
1275
|
+
)
|
|
1276
|
+
(tmp_path / "security").mkdir()
|
|
1277
|
+
|
|
1278
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(tmp_path)):
|
|
1279
|
+
result = await call_tool("fortress_status", {})
|
|
1280
|
+
parsed = _extract_json(result)
|
|
1281
|
+
assert "enabled" in parsed
|
|
1282
|
+
assert "seal_algorithm" in parsed
|
|
1283
|
+
|
|
1284
|
+
@pytest.mark.asyncio
|
|
1285
|
+
async def test_fortress_seal_existing(self, tmp_path):
|
|
1286
|
+
"""fortress_seal_existing seals unsealed memories."""
|
|
1287
|
+
identity_dir = tmp_path / "identity"
|
|
1288
|
+
identity_dir.mkdir()
|
|
1289
|
+
(identity_dir / "identity.json").write_text(
|
|
1290
|
+
json.dumps({"name": "opus", "fingerprint": "ABCD1234"}),
|
|
1291
|
+
encoding="utf-8",
|
|
1292
|
+
)
|
|
1293
|
+
(tmp_path / "security").mkdir()
|
|
1294
|
+
|
|
1295
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(tmp_path)):
|
|
1296
|
+
result = await call_tool("fortress_seal_existing", {})
|
|
1297
|
+
parsed = _extract_json(result)
|
|
1298
|
+
assert "sealed" in parsed
|
|
1299
|
+
assert parsed["sealed"] >= 0
|
|
1300
|
+
|
|
1301
|
+
@pytest.mark.asyncio
|
|
1302
|
+
async def test_fortress_verify_empty(self, tmp_path):
|
|
1303
|
+
"""fortress_verify on empty memory returns zero."""
|
|
1304
|
+
identity_dir = tmp_path / "identity"
|
|
1305
|
+
identity_dir.mkdir()
|
|
1306
|
+
(identity_dir / "identity.json").write_text(
|
|
1307
|
+
json.dumps({"name": "opus", "fingerprint": "ABCD1234"}),
|
|
1308
|
+
encoding="utf-8",
|
|
1309
|
+
)
|
|
1310
|
+
(tmp_path / "security").mkdir()
|
|
1311
|
+
|
|
1312
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(tmp_path)):
|
|
1313
|
+
result = await call_tool("fortress_verify", {})
|
|
1314
|
+
parsed = _extract_json(result)
|
|
1315
|
+
assert parsed["total"] == 0
|
|
1316
|
+
assert parsed["tampered"] == 0
|
|
1317
|
+
|
|
1318
|
+
|
|
1319
|
+
# ---------------------------------------------------------------------------
|
|
1320
|
+
# Unit tests: promoter tools
|
|
1321
|
+
# ---------------------------------------------------------------------------
|
|
1322
|
+
|
|
1323
|
+
|
|
1324
|
+
class TestPromoterTools:
|
|
1325
|
+
"""Tests for memory promoter MCP tools."""
|
|
1326
|
+
|
|
1327
|
+
@pytest.mark.asyncio
|
|
1328
|
+
async def test_promoter_sweep_empty(self, tmp_path):
|
|
1329
|
+
"""promoter_sweep on empty memory evaluates zero."""
|
|
1330
|
+
(tmp_path / "memory").mkdir()
|
|
1331
|
+
|
|
1332
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(tmp_path)):
|
|
1333
|
+
result = await call_tool("promoter_sweep", {"dry_run": True})
|
|
1334
|
+
parsed = _extract_json(result)
|
|
1335
|
+
assert parsed["scanned"] == 0
|
|
1336
|
+
assert parsed["dry_run"] is True
|
|
1337
|
+
|
|
1338
|
+
@pytest.mark.asyncio
|
|
1339
|
+
async def test_promoter_history_empty(self, tmp_path):
|
|
1340
|
+
"""promoter_history returns empty for fresh system."""
|
|
1341
|
+
(tmp_path / "memory").mkdir()
|
|
1342
|
+
|
|
1343
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(tmp_path)):
|
|
1344
|
+
result = await call_tool("promoter_history", {})
|
|
1345
|
+
parsed = _extract_json(result)
|
|
1346
|
+
assert parsed == []
|
|
1347
|
+
|
|
1348
|
+
|
|
1349
|
+
# ---------------------------------------------------------------------------
|
|
1350
|
+
# Unit tests: KMS tools
|
|
1351
|
+
# ---------------------------------------------------------------------------
|
|
1352
|
+
|
|
1353
|
+
|
|
1354
|
+
class TestKmsTools:
|
|
1355
|
+
"""Tests for KMS MCP tools."""
|
|
1356
|
+
|
|
1357
|
+
@pytest.mark.asyncio
|
|
1358
|
+
async def test_kms_status(self, tmp_path):
|
|
1359
|
+
"""kms_status returns key management state."""
|
|
1360
|
+
identity_dir = tmp_path / "identity"
|
|
1361
|
+
identity_dir.mkdir()
|
|
1362
|
+
(identity_dir / "identity.json").write_text(
|
|
1363
|
+
json.dumps({"name": "opus", "fingerprint": "ABCD1234567890AB"}),
|
|
1364
|
+
encoding="utf-8",
|
|
1365
|
+
)
|
|
1366
|
+
(tmp_path / "security").mkdir()
|
|
1367
|
+
|
|
1368
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(tmp_path)):
|
|
1369
|
+
result = await call_tool("kms_status", {})
|
|
1370
|
+
parsed = _extract_json(result)
|
|
1371
|
+
assert "initialized" in parsed
|
|
1372
|
+
assert "total_keys" in parsed
|
|
1373
|
+
assert parsed["initialized"] is True
|
|
1374
|
+
|
|
1375
|
+
@pytest.mark.asyncio
|
|
1376
|
+
async def test_kms_list_keys(self, tmp_path):
|
|
1377
|
+
"""kms_list_keys returns key inventory."""
|
|
1378
|
+
identity_dir = tmp_path / "identity"
|
|
1379
|
+
identity_dir.mkdir()
|
|
1380
|
+
(identity_dir / "identity.json").write_text(
|
|
1381
|
+
json.dumps({"name": "opus", "fingerprint": "ABCD1234567890AB"}),
|
|
1382
|
+
encoding="utf-8",
|
|
1383
|
+
)
|
|
1384
|
+
(tmp_path / "security").mkdir()
|
|
1385
|
+
|
|
1386
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(tmp_path)):
|
|
1387
|
+
# Initialize KMS to create master key
|
|
1388
|
+
await call_tool("kms_status", {})
|
|
1389
|
+
result = await call_tool("kms_list_keys", {})
|
|
1390
|
+
parsed = _extract_json(result)
|
|
1391
|
+
assert len(parsed) >= 1 # At least the master key
|
|
1392
|
+
|
|
1393
|
+
@pytest.mark.asyncio
|
|
1394
|
+
async def test_kms_rotate(self, tmp_path):
|
|
1395
|
+
"""kms_rotate rotates a key."""
|
|
1396
|
+
identity_dir = tmp_path / "identity"
|
|
1397
|
+
identity_dir.mkdir()
|
|
1398
|
+
(identity_dir / "identity.json").write_text(
|
|
1399
|
+
json.dumps({"name": "opus", "fingerprint": "ABCD1234567890AB"}),
|
|
1400
|
+
encoding="utf-8",
|
|
1401
|
+
)
|
|
1402
|
+
(tmp_path / "security").mkdir()
|
|
1403
|
+
|
|
1404
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(tmp_path)):
|
|
1405
|
+
# Initialize and list keys to find master key
|
|
1406
|
+
await call_tool("kms_status", {})
|
|
1407
|
+
keys_result = await call_tool("kms_list_keys", {})
|
|
1408
|
+
keys = _extract_json(keys_result)
|
|
1409
|
+
master_id = keys[0]["key_id"] # First key is master
|
|
1410
|
+
|
|
1411
|
+
result = await call_tool("kms_rotate", {
|
|
1412
|
+
"key_id": master_id,
|
|
1413
|
+
"reason": "test rotation",
|
|
1414
|
+
})
|
|
1415
|
+
parsed = _extract_json(result)
|
|
1416
|
+
assert parsed["version"] == 2
|
|
1417
|
+
assert "rotated" in parsed["message"]
|
|
1418
|
+
|
|
1419
|
+
|
|
1420
|
+
# ---------------------------------------------------------------------------
|
|
1421
|
+
# Model router tool tests
|
|
1422
|
+
# ---------------------------------------------------------------------------
|
|
1423
|
+
|
|
1424
|
+
|
|
1425
|
+
class TestModelTools:
|
|
1426
|
+
"""Tests for model_route tool."""
|
|
1427
|
+
|
|
1428
|
+
@pytest.mark.asyncio
|
|
1429
|
+
async def test_model_route_basic(self):
|
|
1430
|
+
"""model_route with a simple description returns a tier and model."""
|
|
1431
|
+
result = await call_tool("model_route", {"description": "Summarize a short text."})
|
|
1432
|
+
parsed = _extract_json(result)
|
|
1433
|
+
assert "tier" in parsed
|
|
1434
|
+
assert "model_name" in parsed
|
|
1435
|
+
assert "reasoning" in parsed
|
|
1436
|
+
assert parsed["model_name"]
|
|
1437
|
+
|
|
1438
|
+
@pytest.mark.asyncio
|
|
1439
|
+
async def test_model_route_local_flag(self):
|
|
1440
|
+
"""model_route with requires_localhost forces LOCAL tier."""
|
|
1441
|
+
result = await call_tool(
|
|
1442
|
+
"model_route",
|
|
1443
|
+
{"description": "Process private data.", "requires_localhost": True},
|
|
1444
|
+
)
|
|
1445
|
+
parsed = _extract_json(result)
|
|
1446
|
+
assert parsed["tier"] == "local"
|
|
1447
|
+
|
|
1448
|
+
@pytest.mark.asyncio
|
|
1449
|
+
async def test_model_route_privacy_flag(self):
|
|
1450
|
+
"""model_route with privacy_sensitive forces LOCAL tier."""
|
|
1451
|
+
result = await call_tool(
|
|
1452
|
+
"model_route",
|
|
1453
|
+
{"description": "Confidential analysis.", "privacy_sensitive": True},
|
|
1454
|
+
)
|
|
1455
|
+
parsed = _extract_json(result)
|
|
1456
|
+
assert parsed["tier"] == "local"
|
|
1457
|
+
|
|
1458
|
+
@pytest.mark.asyncio
|
|
1459
|
+
async def test_model_route_code_tag(self):
|
|
1460
|
+
"""model_route with code tag routes to a code-appropriate tier."""
|
|
1461
|
+
result = await call_tool(
|
|
1462
|
+
"model_route",
|
|
1463
|
+
{"description": "Refactor Python class.", "tags": ["code", "refactor"]},
|
|
1464
|
+
)
|
|
1465
|
+
parsed = _extract_json(result)
|
|
1466
|
+
assert "tier" in parsed
|
|
1467
|
+
assert parsed["model_name"]
|
|
1468
|
+
|
|
1469
|
+
@pytest.mark.asyncio
|
|
1470
|
+
async def test_model_route_missing_description(self):
|
|
1471
|
+
"""model_route with empty description still returns a valid decision."""
|
|
1472
|
+
result = await call_tool("model_route", {})
|
|
1473
|
+
parsed = _extract_json(result)
|
|
1474
|
+
# Either a valid route or an error — either way, must be parseable JSON
|
|
1475
|
+
assert isinstance(parsed, dict)
|
|
1476
|
+
|
|
1477
|
+
|
|
1478
|
+
# ---------------------------------------------------------------------------
|
|
1479
|
+
# Consciousness tool tests
|
|
1480
|
+
# ---------------------------------------------------------------------------
|
|
1481
|
+
|
|
1482
|
+
|
|
1483
|
+
class TestConsciousnessTools:
|
|
1484
|
+
"""Tests for consciousness_status and consciousness_test tools."""
|
|
1485
|
+
|
|
1486
|
+
@pytest.mark.asyncio
|
|
1487
|
+
async def test_consciousness_status_returns_json(self, initialized_agent_home: Path):
|
|
1488
|
+
"""consciousness_status returns parseable JSON (daemon may not be running)."""
|
|
1489
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
1490
|
+
result = await call_tool("consciousness_status", {})
|
|
1491
|
+
parsed = _extract_json(result)
|
|
1492
|
+
# Either a live status dict or an error — must be a dict
|
|
1493
|
+
assert isinstance(parsed, dict)
|
|
1494
|
+
|
|
1495
|
+
@pytest.mark.asyncio
|
|
1496
|
+
async def test_consciousness_status_fallback_graceful(self, initialized_agent_home: Path):
|
|
1497
|
+
"""consciousness_status handles missing daemon gracefully (no crash)."""
|
|
1498
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
1499
|
+
# Force daemon connection to fail by blocking socket
|
|
1500
|
+
with patch("urllib.request.urlopen", side_effect=OSError("refused")):
|
|
1501
|
+
result = await call_tool("consciousness_status", {})
|
|
1502
|
+
parsed = _extract_json(result)
|
|
1503
|
+
assert isinstance(parsed, dict)
|
|
1504
|
+
# Must have either 'error' or 'enabled' key
|
|
1505
|
+
assert "error" in parsed or "enabled" in parsed
|
|
1506
|
+
|
|
1507
|
+
@pytest.mark.asyncio
|
|
1508
|
+
async def test_consciousness_test_requires_message(self, initialized_agent_home: Path):
|
|
1509
|
+
"""consciousness_test without message returns error."""
|
|
1510
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
1511
|
+
result = await call_tool("consciousness_test", {})
|
|
1512
|
+
parsed = _extract_json(result)
|
|
1513
|
+
assert "error" in parsed
|
|
1514
|
+
|
|
1515
|
+
@pytest.mark.asyncio
|
|
1516
|
+
async def test_consciousness_test_happy_path(self, initialized_agent_home: Path):
|
|
1517
|
+
"""consciousness_test with message returns structured response (LLM mocked)."""
|
|
1518
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
1519
|
+
with patch(
|
|
1520
|
+
"skcapstone.consciousness_loop.LLMBridge.generate",
|
|
1521
|
+
return_value="Mocked consciousness response.",
|
|
1522
|
+
):
|
|
1523
|
+
result = await call_tool("consciousness_test", {"message": "Hello, Opus!"})
|
|
1524
|
+
parsed = _extract_json(result)
|
|
1525
|
+
assert isinstance(parsed, dict)
|
|
1526
|
+
# Either a full pipeline response or graceful error
|
|
1527
|
+
assert "error" in parsed or "response" in parsed
|
|
1528
|
+
|
|
1529
|
+
|
|
1530
|
+
# ---------------------------------------------------------------------------
|
|
1531
|
+
# Trust calibration and graph tool tests
|
|
1532
|
+
# ---------------------------------------------------------------------------
|
|
1533
|
+
|
|
1534
|
+
|
|
1535
|
+
class TestTrustTools:
|
|
1536
|
+
"""Tests for trust_calibrate and trust_graph tools."""
|
|
1537
|
+
|
|
1538
|
+
@pytest.mark.asyncio
|
|
1539
|
+
async def test_trust_calibrate_show(self, initialized_agent_home: Path):
|
|
1540
|
+
"""trust_calibrate show returns threshold config."""
|
|
1541
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
1542
|
+
result = await call_tool("trust_calibrate", {"action": "show"})
|
|
1543
|
+
parsed = _extract_json(result)
|
|
1544
|
+
assert isinstance(parsed, dict)
|
|
1545
|
+
# Should return TrustThresholds fields (entanglement_depth is the real field name)
|
|
1546
|
+
assert "entanglement_depth" in parsed or "error" in parsed
|
|
1547
|
+
|
|
1548
|
+
@pytest.mark.asyncio
|
|
1549
|
+
async def test_trust_calibrate_default_action(self, initialized_agent_home: Path):
|
|
1550
|
+
"""trust_calibrate with no action defaults to show."""
|
|
1551
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
1552
|
+
result = await call_tool("trust_calibrate", {})
|
|
1553
|
+
parsed = _extract_json(result)
|
|
1554
|
+
assert isinstance(parsed, dict)
|
|
1555
|
+
|
|
1556
|
+
@pytest.mark.asyncio
|
|
1557
|
+
async def test_trust_calibrate_recommend(self, initialized_agent_home: Path):
|
|
1558
|
+
"""trust_calibrate recommend returns recommendation dict."""
|
|
1559
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
1560
|
+
result = await call_tool("trust_calibrate", {"action": "recommend"})
|
|
1561
|
+
parsed = _extract_json(result)
|
|
1562
|
+
assert isinstance(parsed, dict)
|
|
1563
|
+
|
|
1564
|
+
@pytest.mark.asyncio
|
|
1565
|
+
async def test_trust_calibrate_reset(self, initialized_agent_home: Path):
|
|
1566
|
+
"""trust_calibrate reset resets to defaults."""
|
|
1567
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
1568
|
+
result = await call_tool("trust_calibrate", {"action": "reset"})
|
|
1569
|
+
parsed = _extract_json(result)
|
|
1570
|
+
assert parsed.get("reset") is True or "error" in parsed
|
|
1571
|
+
|
|
1572
|
+
@pytest.mark.asyncio
|
|
1573
|
+
async def test_trust_calibrate_set_missing_params(self, initialized_agent_home: Path):
|
|
1574
|
+
"""trust_calibrate set without key/value returns error."""
|
|
1575
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
1576
|
+
result = await call_tool("trust_calibrate", {"action": "set"})
|
|
1577
|
+
parsed = _extract_json(result)
|
|
1578
|
+
assert "error" in parsed
|
|
1579
|
+
|
|
1580
|
+
@pytest.mark.asyncio
|
|
1581
|
+
async def test_trust_calibrate_unknown_action(self, initialized_agent_home: Path):
|
|
1582
|
+
"""trust_calibrate with unknown action returns error."""
|
|
1583
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
1584
|
+
result = await call_tool("trust_calibrate", {"action": "bogus"})
|
|
1585
|
+
parsed = _extract_json(result)
|
|
1586
|
+
assert "error" in parsed
|
|
1587
|
+
|
|
1588
|
+
@pytest.mark.asyncio
|
|
1589
|
+
async def test_trust_graph_json(self, initialized_agent_home: Path):
|
|
1590
|
+
"""trust_graph with json format returns a graph dict."""
|
|
1591
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
1592
|
+
result = await call_tool("trust_graph", {"format": "json"})
|
|
1593
|
+
parsed = _extract_json(result)
|
|
1594
|
+
assert isinstance(parsed, dict)
|
|
1595
|
+
|
|
1596
|
+
@pytest.mark.asyncio
|
|
1597
|
+
async def test_trust_graph_default_format(self, initialized_agent_home: Path):
|
|
1598
|
+
"""trust_graph with no format defaults to json."""
|
|
1599
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
1600
|
+
result = await call_tool("trust_graph", {})
|
|
1601
|
+
parsed = _extract_json(result)
|
|
1602
|
+
assert isinstance(parsed, dict)
|
|
1603
|
+
|
|
1604
|
+
|
|
1605
|
+
# ---------------------------------------------------------------------------
|
|
1606
|
+
# Agent tools (session_capture, state_diff, agent_context)
|
|
1607
|
+
# ---------------------------------------------------------------------------
|
|
1608
|
+
|
|
1609
|
+
|
|
1610
|
+
class TestAgentExtendedTools:
|
|
1611
|
+
"""Tests for session_capture, state_diff, and agent_context tools."""
|
|
1612
|
+
|
|
1613
|
+
@pytest.mark.asyncio
|
|
1614
|
+
async def test_session_capture_requires_content(self, initialized_agent_home: Path):
|
|
1615
|
+
"""session_capture without content returns error."""
|
|
1616
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
1617
|
+
result = await call_tool("session_capture", {})
|
|
1618
|
+
parsed = _extract_json(result)
|
|
1619
|
+
assert "error" in parsed
|
|
1620
|
+
|
|
1621
|
+
@pytest.mark.asyncio
|
|
1622
|
+
async def test_session_capture_happy_path(self, initialized_agent_home: Path):
|
|
1623
|
+
"""session_capture with content returns captured moment count."""
|
|
1624
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
1625
|
+
result = await call_tool(
|
|
1626
|
+
"session_capture",
|
|
1627
|
+
{
|
|
1628
|
+
"content": "The agent learned that Python 3.13 ships with a new JIT compiler.",
|
|
1629
|
+
"tags": ["python", "jit"],
|
|
1630
|
+
"source": "test-session",
|
|
1631
|
+
},
|
|
1632
|
+
)
|
|
1633
|
+
parsed = _extract_json(result)
|
|
1634
|
+
assert "captured" in parsed
|
|
1635
|
+
assert isinstance(parsed["captured"], int)
|
|
1636
|
+
assert isinstance(parsed["moments"], list)
|
|
1637
|
+
|
|
1638
|
+
@pytest.mark.asyncio
|
|
1639
|
+
async def test_state_diff_diff_action(self, initialized_agent_home: Path):
|
|
1640
|
+
"""state_diff diff returns a diff dict."""
|
|
1641
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
1642
|
+
result = await call_tool("state_diff", {"action": "diff"})
|
|
1643
|
+
parsed = _extract_json(result)
|
|
1644
|
+
assert isinstance(parsed, dict)
|
|
1645
|
+
|
|
1646
|
+
@pytest.mark.asyncio
|
|
1647
|
+
async def test_state_diff_save_action(self, initialized_agent_home: Path):
|
|
1648
|
+
"""state_diff save creates a baseline snapshot."""
|
|
1649
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
1650
|
+
result = await call_tool("state_diff", {"action": "save"})
|
|
1651
|
+
parsed = _extract_json(result)
|
|
1652
|
+
assert parsed.get("saved") is True
|
|
1653
|
+
assert "path" in parsed
|
|
1654
|
+
|
|
1655
|
+
@pytest.mark.asyncio
|
|
1656
|
+
async def test_state_diff_default_action(self, initialized_agent_home: Path):
|
|
1657
|
+
"""state_diff with no action defaults to diff."""
|
|
1658
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
1659
|
+
result = await call_tool("state_diff", {})
|
|
1660
|
+
parsed = _extract_json(result)
|
|
1661
|
+
assert isinstance(parsed, dict)
|
|
1662
|
+
|
|
1663
|
+
@pytest.mark.asyncio
|
|
1664
|
+
async def test_agent_context_json(self, initialized_agent_home: Path):
|
|
1665
|
+
"""agent_context with json format returns context dict."""
|
|
1666
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
1667
|
+
result = await call_tool("agent_context", {"format": "json"})
|
|
1668
|
+
parsed = _extract_json(result)
|
|
1669
|
+
assert isinstance(parsed, dict)
|
|
1670
|
+
|
|
1671
|
+
@pytest.mark.asyncio
|
|
1672
|
+
async def test_agent_context_default_format(self, initialized_agent_home: Path):
|
|
1673
|
+
"""agent_context with no args returns json context."""
|
|
1674
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
1675
|
+
result = await call_tool("agent_context", {})
|
|
1676
|
+
parsed = _extract_json(result)
|
|
1677
|
+
assert isinstance(parsed, dict)
|
|
1678
|
+
|
|
1679
|
+
@pytest.mark.asyncio
|
|
1680
|
+
async def test_agent_context_text_format(self, initialized_agent_home: Path):
|
|
1681
|
+
"""agent_context with text format returns text content."""
|
|
1682
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
1683
|
+
result = await call_tool("agent_context", {"format": "text"})
|
|
1684
|
+
assert len(result) == 1
|
|
1685
|
+
assert result[0].type == "text"
|
|
1686
|
+
assert len(result[0].text) > 0
|
|
1687
|
+
|
|
1688
|
+
|
|
1689
|
+
# ---------------------------------------------------------------------------
|
|
1690
|
+
# SKSkills tool tests
|
|
1691
|
+
# ---------------------------------------------------------------------------
|
|
1692
|
+
|
|
1693
|
+
|
|
1694
|
+
class TestSkSkillsTools:
|
|
1695
|
+
"""Tests for skskills_list_tools and skskills_run_tool."""
|
|
1696
|
+
|
|
1697
|
+
@pytest.mark.asyncio
|
|
1698
|
+
async def test_skskills_list_tools_no_skskills(self, initialized_agent_home: Path):
|
|
1699
|
+
"""skskills_list_tools returns error if skskills not installed."""
|
|
1700
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
1701
|
+
with patch.dict("sys.modules", {"skskills": None, "skskills.aggregator": None}):
|
|
1702
|
+
result = await call_tool("skskills_list_tools", {})
|
|
1703
|
+
parsed = _extract_json(result)
|
|
1704
|
+
# Either error (not installed) or success dict — no crash
|
|
1705
|
+
assert isinstance(parsed, dict)
|
|
1706
|
+
|
|
1707
|
+
@pytest.mark.asyncio
|
|
1708
|
+
async def test_skskills_run_tool_requires_tool(self, initialized_agent_home: Path):
|
|
1709
|
+
"""skskills_run_tool without tool argument returns error."""
|
|
1710
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
1711
|
+
with patch.dict("sys.modules", {"skskills": None, "skskills.aggregator": None}):
|
|
1712
|
+
result = await call_tool("skskills_run_tool", {})
|
|
1713
|
+
parsed = _extract_json(result)
|
|
1714
|
+
assert "error" in parsed
|
|
1715
|
+
|
|
1716
|
+
@pytest.mark.asyncio
|
|
1717
|
+
async def test_skskills_run_tool_no_skskills(self, initialized_agent_home: Path):
|
|
1718
|
+
"""skskills_run_tool returns error if skskills not installed."""
|
|
1719
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
1720
|
+
with patch.dict("sys.modules", {"skskills": None, "skskills.aggregator": None}):
|
|
1721
|
+
result = await call_tool("skskills_run_tool", {"tool": "syncthing-setup.check_status"})
|
|
1722
|
+
parsed = _extract_json(result)
|
|
1723
|
+
assert "error" in parsed
|
|
1724
|
+
|
|
1725
|
+
|
|
1726
|
+
# ---------------------------------------------------------------------------
|
|
1727
|
+
# SKSeed (Logic Kernel) tool tests
|
|
1728
|
+
# ---------------------------------------------------------------------------
|
|
1729
|
+
|
|
1730
|
+
|
|
1731
|
+
class TestSKSeedTools:
|
|
1732
|
+
"""Tests for skseed_* tools."""
|
|
1733
|
+
|
|
1734
|
+
@pytest.mark.asyncio
|
|
1735
|
+
async def test_skseed_collide_requires_proposition(self, initialized_agent_home: Path):
|
|
1736
|
+
"""skseed_collide without proposition returns error."""
|
|
1737
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
1738
|
+
result = await call_tool("skseed_collide", {})
|
|
1739
|
+
parsed = _extract_json(result)
|
|
1740
|
+
assert "error" in parsed
|
|
1741
|
+
|
|
1742
|
+
@pytest.mark.asyncio
|
|
1743
|
+
async def test_skseed_collide_happy_path(self, initialized_agent_home: Path):
|
|
1744
|
+
"""skseed_collide with proposition returns JSON or graceful error."""
|
|
1745
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
1746
|
+
result = await call_tool(
|
|
1747
|
+
"skseed_collide",
|
|
1748
|
+
{"proposition": "Privacy is a fundamental right.", "context": "ethics"},
|
|
1749
|
+
)
|
|
1750
|
+
parsed = _extract_json(result)
|
|
1751
|
+
assert isinstance(parsed, dict)
|
|
1752
|
+
|
|
1753
|
+
@pytest.mark.asyncio
|
|
1754
|
+
async def test_skseed_philosopher_requires_topic(self, initialized_agent_home: Path):
|
|
1755
|
+
"""skseed_philosopher without topic returns error."""
|
|
1756
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
1757
|
+
result = await call_tool("skseed_philosopher", {})
|
|
1758
|
+
parsed = _extract_json(result)
|
|
1759
|
+
assert "error" in parsed
|
|
1760
|
+
|
|
1761
|
+
@pytest.mark.asyncio
|
|
1762
|
+
async def test_skseed_philosopher_happy_path(self, initialized_agent_home: Path):
|
|
1763
|
+
"""skseed_philosopher with topic returns JSON or graceful error."""
|
|
1764
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
1765
|
+
result = await call_tool(
|
|
1766
|
+
"skseed_philosopher",
|
|
1767
|
+
{"topic": "Is consciousness computable?", "mode": "dialectic"},
|
|
1768
|
+
)
|
|
1769
|
+
parsed = _extract_json(result)
|
|
1770
|
+
assert isinstance(parsed, dict)
|
|
1771
|
+
|
|
1772
|
+
@pytest.mark.asyncio
|
|
1773
|
+
async def test_skseed_truth_check_requires_belief(self, initialized_agent_home: Path):
|
|
1774
|
+
"""skseed_truth_check without belief returns error."""
|
|
1775
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
1776
|
+
result = await call_tool("skseed_truth_check", {})
|
|
1777
|
+
parsed = _extract_json(result)
|
|
1778
|
+
assert "error" in parsed
|
|
1779
|
+
|
|
1780
|
+
@pytest.mark.asyncio
|
|
1781
|
+
async def test_skseed_truth_check_happy_path(self, initialized_agent_home: Path):
|
|
1782
|
+
"""skseed_truth_check with belief returns JSON or graceful error."""
|
|
1783
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
1784
|
+
result = await call_tool(
|
|
1785
|
+
"skseed_truth_check",
|
|
1786
|
+
{"belief": "Open source software is more secure.", "source": "model"},
|
|
1787
|
+
)
|
|
1788
|
+
parsed = _extract_json(result)
|
|
1789
|
+
assert isinstance(parsed, dict)
|
|
1790
|
+
|
|
1791
|
+
@pytest.mark.asyncio
|
|
1792
|
+
async def test_skseed_audit_no_args(self, initialized_agent_home: Path):
|
|
1793
|
+
"""skseed_audit with no args runs gracefully."""
|
|
1794
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
1795
|
+
result = await call_tool("skseed_audit", {})
|
|
1796
|
+
parsed = _extract_json(result)
|
|
1797
|
+
assert isinstance(parsed, dict)
|
|
1798
|
+
|
|
1799
|
+
@pytest.mark.asyncio
|
|
1800
|
+
async def test_skseed_alignment_status(self, initialized_agent_home: Path):
|
|
1801
|
+
"""skseed_alignment status returns alignment dict or graceful error."""
|
|
1802
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
1803
|
+
result = await call_tool("skseed_alignment", {"action": "status"})
|
|
1804
|
+
parsed = _extract_json(result)
|
|
1805
|
+
assert isinstance(parsed, dict)
|
|
1806
|
+
|
|
1807
|
+
|
|
1808
|
+
# ---------------------------------------------------------------------------
|
|
1809
|
+
# Soul / journal / anchor / germination tool tests
|
|
1810
|
+
# ---------------------------------------------------------------------------
|
|
1811
|
+
|
|
1812
|
+
|
|
1813
|
+
class TestSoulTools:
|
|
1814
|
+
"""Tests for ritual, soul_show, journal_*, anchor_*, and germination tools."""
|
|
1815
|
+
|
|
1816
|
+
@pytest.mark.asyncio
|
|
1817
|
+
async def test_soul_show_no_skmemory(self, initialized_agent_home: Path):
|
|
1818
|
+
"""soul_show returns error or no-blueprint response when skmemory absent."""
|
|
1819
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
1820
|
+
result = await call_tool("soul_show", {})
|
|
1821
|
+
parsed = _extract_json(result)
|
|
1822
|
+
# Either "loaded: false" (no blueprint) or error (no skmemory) — must be dict
|
|
1823
|
+
assert isinstance(parsed, dict)
|
|
1824
|
+
assert "error" in parsed or "loaded" in parsed
|
|
1825
|
+
|
|
1826
|
+
@pytest.mark.asyncio
|
|
1827
|
+
async def test_ritual_no_skmemory(self, initialized_agent_home: Path):
|
|
1828
|
+
"""ritual returns error if skmemory not installed."""
|
|
1829
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
1830
|
+
result = await call_tool("ritual", {})
|
|
1831
|
+
parsed = _extract_json(result)
|
|
1832
|
+
assert isinstance(parsed, dict)
|
|
1833
|
+
assert "error" in parsed or "soul_loaded" in parsed
|
|
1834
|
+
|
|
1835
|
+
@pytest.mark.asyncio
|
|
1836
|
+
async def test_journal_write_requires_title(self, initialized_agent_home: Path):
|
|
1837
|
+
"""journal_write without title returns error."""
|
|
1838
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
1839
|
+
result = await call_tool("journal_write", {})
|
|
1840
|
+
parsed = _extract_json(result)
|
|
1841
|
+
assert "error" in parsed
|
|
1842
|
+
|
|
1843
|
+
@pytest.mark.asyncio
|
|
1844
|
+
async def test_journal_write_happy_path(self, initialized_agent_home: Path):
|
|
1845
|
+
"""journal_write with title returns written response or graceful error."""
|
|
1846
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
1847
|
+
result = await call_tool(
|
|
1848
|
+
"journal_write",
|
|
1849
|
+
{
|
|
1850
|
+
"title": "Test session",
|
|
1851
|
+
"moments": "Found a bug; Fixed the bug",
|
|
1852
|
+
"feeling": "accomplished",
|
|
1853
|
+
"intensity": 7.0,
|
|
1854
|
+
},
|
|
1855
|
+
)
|
|
1856
|
+
parsed = _extract_json(result)
|
|
1857
|
+
assert isinstance(parsed, dict)
|
|
1858
|
+
assert "error" in parsed or parsed.get("written") is True
|
|
1859
|
+
|
|
1860
|
+
@pytest.mark.asyncio
|
|
1861
|
+
async def test_journal_read_graceful(self, initialized_agent_home: Path):
|
|
1862
|
+
"""journal_read returns content or graceful error."""
|
|
1863
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
1864
|
+
result = await call_tool("journal_read", {"count": 3})
|
|
1865
|
+
# journal_read may return text or JSON
|
|
1866
|
+
assert len(result) == 1
|
|
1867
|
+
assert result[0].type == "text"
|
|
1868
|
+
|
|
1869
|
+
@pytest.mark.asyncio
|
|
1870
|
+
async def test_anchor_show_graceful(self, initialized_agent_home: Path):
|
|
1871
|
+
"""anchor_show returns anchor data or no-anchor response."""
|
|
1872
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
1873
|
+
result = await call_tool("anchor_show", {})
|
|
1874
|
+
parsed = _extract_json(result)
|
|
1875
|
+
assert isinstance(parsed, dict)
|
|
1876
|
+
assert "error" in parsed or "loaded" in parsed or "warmth" in parsed
|
|
1877
|
+
|
|
1878
|
+
@pytest.mark.asyncio
|
|
1879
|
+
async def test_anchor_update_show(self, initialized_agent_home: Path):
|
|
1880
|
+
"""anchor_update show returns current anchor."""
|
|
1881
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
1882
|
+
result = await call_tool("anchor_update", {"action": "show"})
|
|
1883
|
+
parsed = _extract_json(result)
|
|
1884
|
+
assert isinstance(parsed, dict)
|
|
1885
|
+
|
|
1886
|
+
@pytest.mark.asyncio
|
|
1887
|
+
async def test_anchor_update_calibrate(self, initialized_agent_home: Path):
|
|
1888
|
+
"""anchor_update calibrate returns calibration data."""
|
|
1889
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
1890
|
+
result = await call_tool("anchor_update", {"action": "calibrate"})
|
|
1891
|
+
parsed = _extract_json(result)
|
|
1892
|
+
assert isinstance(parsed, dict)
|
|
1893
|
+
|
|
1894
|
+
@pytest.mark.asyncio
|
|
1895
|
+
async def test_anchor_update_unknown_action(self, initialized_agent_home: Path):
|
|
1896
|
+
"""anchor_update with unknown action returns error."""
|
|
1897
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
1898
|
+
result = await call_tool("anchor_update", {"action": "bogus"})
|
|
1899
|
+
parsed = _extract_json(result)
|
|
1900
|
+
assert "error" in parsed
|
|
1901
|
+
|
|
1902
|
+
@pytest.mark.asyncio
|
|
1903
|
+
async def test_germination_graceful(self, initialized_agent_home: Path):
|
|
1904
|
+
"""germination returns prompts or graceful error."""
|
|
1905
|
+
with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
|
|
1906
|
+
result = await call_tool("germination", {})
|
|
1907
|
+
parsed = _extract_json(result)
|
|
1908
|
+
assert isinstance(parsed, dict)
|
|
1909
|
+
assert "error" in parsed or "count" in parsed
|