@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,395 @@
|
|
|
1
|
+
"""Tests for the PeerDirectory transport address registry.
|
|
2
|
+
|
|
3
|
+
Covers:
|
|
4
|
+
- load() on empty / missing / malformed directory
|
|
5
|
+
- add_peer() — creates entry, persists to YAML, overwrites on re-add
|
|
6
|
+
- remove_peer() — removes known peer, returns False for unknown
|
|
7
|
+
- resolve() — returns address or None
|
|
8
|
+
- list_peers() — empty and with entries, sorted
|
|
9
|
+
- update_last_seen() — timestamps known peer, no-op on unknown
|
|
10
|
+
- auto_discover() — discovers from heartbeats dir and outbox dirs
|
|
11
|
+
- YAML persistence round-trip
|
|
12
|
+
- CLI: peers list, peers add, peers discover
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
from datetime import datetime, timezone
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
import pytest
|
|
22
|
+
import yaml
|
|
23
|
+
from click.testing import CliRunner
|
|
24
|
+
|
|
25
|
+
from skcapstone.peer_directory import DirectoryEntry, PeerDirectory
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
# Helpers
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def make_directory(tmp_path: Path) -> PeerDirectory:
|
|
34
|
+
"""Return a PeerDirectory rooted at tmp_path."""
|
|
35
|
+
return PeerDirectory(home=tmp_path)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def write_heartbeat(hb_dir: Path, agent: str, fingerprint: str = "") -> None:
|
|
39
|
+
"""Write a minimal heartbeat JSON file."""
|
|
40
|
+
hb_dir.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
data = {
|
|
42
|
+
"agent_name": agent,
|
|
43
|
+
"status": "alive",
|
|
44
|
+
"hostname": f"{agent}-host",
|
|
45
|
+
"platform": "Linux x86_64",
|
|
46
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
47
|
+
"ttl_seconds": 300,
|
|
48
|
+
"fingerprint": fingerprint,
|
|
49
|
+
}
|
|
50
|
+
(hb_dir / f"{agent}.json").write_text(json.dumps(data), encoding="utf-8")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
# Test 1: Empty directory
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class TestLoad:
|
|
59
|
+
"""Test load() behaviour."""
|
|
60
|
+
|
|
61
|
+
def test_load_missing_file(self, tmp_path):
|
|
62
|
+
"""load() on a fresh home returns empty dict without error."""
|
|
63
|
+
d = make_directory(tmp_path)
|
|
64
|
+
result = d.load()
|
|
65
|
+
assert result == {}
|
|
66
|
+
|
|
67
|
+
def test_load_empty_yaml(self, tmp_path):
|
|
68
|
+
"""load() on an empty YAML file returns empty dict."""
|
|
69
|
+
peers_dir = tmp_path / "peers"
|
|
70
|
+
peers_dir.mkdir()
|
|
71
|
+
(peers_dir / "directory.yaml").write_text("", encoding="utf-8")
|
|
72
|
+
d = make_directory(tmp_path)
|
|
73
|
+
assert d.load() == {}
|
|
74
|
+
|
|
75
|
+
def test_load_malformed_yaml_is_safe(self, tmp_path):
|
|
76
|
+
"""load() on malformed YAML does not raise, returns empty."""
|
|
77
|
+
peers_dir = tmp_path / "peers"
|
|
78
|
+
peers_dir.mkdir()
|
|
79
|
+
(peers_dir / "directory.yaml").write_text("{{{invalid yaml", encoding="utf-8")
|
|
80
|
+
d = make_directory(tmp_path)
|
|
81
|
+
result = d.load()
|
|
82
|
+
# Graceful degradation — no exception
|
|
83
|
+
assert isinstance(result, dict)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# ---------------------------------------------------------------------------
|
|
87
|
+
# Test 2: add_peer / resolve
|
|
88
|
+
# ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class TestAddAndResolve:
|
|
92
|
+
"""Test add_peer() and resolve()."""
|
|
93
|
+
|
|
94
|
+
def test_add_peer_creates_entry(self, tmp_path):
|
|
95
|
+
"""add_peer() returns a DirectoryEntry with correct fields."""
|
|
96
|
+
d = make_directory(tmp_path)
|
|
97
|
+
entry = d.add_peer(
|
|
98
|
+
name="Lumina",
|
|
99
|
+
address="/home/user/.skcapstone/sync/comms/outbox/lumina",
|
|
100
|
+
transport="syncthing",
|
|
101
|
+
fingerprint="AABB1122",
|
|
102
|
+
)
|
|
103
|
+
assert entry.name == "lumina"
|
|
104
|
+
assert entry.address == "/home/user/.skcapstone/sync/comms/outbox/lumina"
|
|
105
|
+
assert entry.transport == "syncthing"
|
|
106
|
+
assert entry.fingerprint == "AABB1122"
|
|
107
|
+
assert entry.last_seen is not None
|
|
108
|
+
|
|
109
|
+
def test_resolve_known_peer(self, tmp_path):
|
|
110
|
+
"""resolve() returns the address of a known peer."""
|
|
111
|
+
d = make_directory(tmp_path)
|
|
112
|
+
d.add_peer("Opus", "/path/to/outbox/opus")
|
|
113
|
+
assert d.resolve("Opus") == "/path/to/outbox/opus"
|
|
114
|
+
|
|
115
|
+
def test_resolve_case_insensitive(self, tmp_path):
|
|
116
|
+
"""resolve() is case-insensitive."""
|
|
117
|
+
d = make_directory(tmp_path)
|
|
118
|
+
d.add_peer("LUMINA", "/outbox/lumina")
|
|
119
|
+
assert d.resolve("lumina") == "/outbox/lumina"
|
|
120
|
+
assert d.resolve("LUMINA") == "/outbox/lumina"
|
|
121
|
+
assert d.resolve("Lumina") == "/outbox/lumina"
|
|
122
|
+
|
|
123
|
+
def test_resolve_unknown_returns_none(self, tmp_path):
|
|
124
|
+
"""resolve() returns None for an unknown peer."""
|
|
125
|
+
d = make_directory(tmp_path)
|
|
126
|
+
assert d.resolve("nobody") is None
|
|
127
|
+
|
|
128
|
+
def test_add_peer_overwrites(self, tmp_path):
|
|
129
|
+
"""Adding the same peer twice updates the entry."""
|
|
130
|
+
d = make_directory(tmp_path)
|
|
131
|
+
d.add_peer("Jarvis", "/old/path")
|
|
132
|
+
d.add_peer("Jarvis", "/new/path", transport="tailscale")
|
|
133
|
+
assert d.resolve("jarvis") == "/new/path"
|
|
134
|
+
# List should still show only one entry
|
|
135
|
+
peers = d.list_peers()
|
|
136
|
+
assert len([p for p in peers if p.name == "jarvis"]) == 1
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# ---------------------------------------------------------------------------
|
|
140
|
+
# Test 3: remove_peer
|
|
141
|
+
# ---------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class TestRemovePeer:
|
|
145
|
+
"""Test remove_peer()."""
|
|
146
|
+
|
|
147
|
+
def test_remove_existing(self, tmp_path):
|
|
148
|
+
"""remove_peer() removes a known peer and returns True."""
|
|
149
|
+
d = make_directory(tmp_path)
|
|
150
|
+
d.add_peer("Grok", "/outbox/grok")
|
|
151
|
+
assert d.remove_peer("Grok") is True
|
|
152
|
+
assert d.resolve("grok") is None
|
|
153
|
+
|
|
154
|
+
def test_remove_unknown_returns_false(self, tmp_path):
|
|
155
|
+
"""remove_peer() returns False when peer is not in directory."""
|
|
156
|
+
d = make_directory(tmp_path)
|
|
157
|
+
assert d.remove_peer("nobody") is False
|
|
158
|
+
|
|
159
|
+
def test_remove_case_insensitive(self, tmp_path):
|
|
160
|
+
"""remove_peer() handles mixed case."""
|
|
161
|
+
d = make_directory(tmp_path)
|
|
162
|
+
d.add_peer("Ava", "/outbox/ava")
|
|
163
|
+
assert d.remove_peer("AVA") is True
|
|
164
|
+
assert d.resolve("ava") is None
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# ---------------------------------------------------------------------------
|
|
168
|
+
# Test 4: list_peers
|
|
169
|
+
# ---------------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class TestListPeers:
|
|
173
|
+
"""Test list_peers()."""
|
|
174
|
+
|
|
175
|
+
def test_empty(self, tmp_path):
|
|
176
|
+
"""list_peers() on empty directory returns []."""
|
|
177
|
+
d = make_directory(tmp_path)
|
|
178
|
+
assert d.list_peers() == []
|
|
179
|
+
|
|
180
|
+
def test_sorted_alphabetically(self, tmp_path):
|
|
181
|
+
"""list_peers() returns entries sorted by name."""
|
|
182
|
+
d = make_directory(tmp_path)
|
|
183
|
+
d.add_peer("Zeta", "/z")
|
|
184
|
+
d.add_peer("Alpha", "/a")
|
|
185
|
+
d.add_peer("Mango", "/m")
|
|
186
|
+
names = [p.name for p in d.list_peers()]
|
|
187
|
+
assert names == sorted(names)
|
|
188
|
+
|
|
189
|
+
def test_returns_all_peers(self, tmp_path):
|
|
190
|
+
"""list_peers() returns every added peer."""
|
|
191
|
+
d = make_directory(tmp_path)
|
|
192
|
+
for i in range(5):
|
|
193
|
+
d.add_peer(f"agent{i}", f"/outbox/agent{i}")
|
|
194
|
+
assert len(d.list_peers()) == 5
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# ---------------------------------------------------------------------------
|
|
198
|
+
# Test 5: update_last_seen
|
|
199
|
+
# ---------------------------------------------------------------------------
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class TestUpdateLastSeen:
|
|
203
|
+
"""Test update_last_seen()."""
|
|
204
|
+
|
|
205
|
+
def test_updates_timestamp(self, tmp_path):
|
|
206
|
+
"""update_last_seen() sets a new ISO timestamp for a known peer."""
|
|
207
|
+
d = make_directory(tmp_path)
|
|
208
|
+
d.add_peer("Opus", "/outbox/opus")
|
|
209
|
+
old_ts = d.resolve("opus") # address won't change, but we want to check last_seen
|
|
210
|
+
|
|
211
|
+
import time
|
|
212
|
+
time.sleep(0.01) # ensure clock advances
|
|
213
|
+
|
|
214
|
+
d.update_last_seen("Opus")
|
|
215
|
+
entry = d.list_peers()[0]
|
|
216
|
+
assert entry.last_seen is not None
|
|
217
|
+
|
|
218
|
+
def test_noop_on_unknown(self, tmp_path):
|
|
219
|
+
"""update_last_seen() on an unknown peer does not raise."""
|
|
220
|
+
d = make_directory(tmp_path)
|
|
221
|
+
d.update_last_seen("ghost") # must not raise
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
# ---------------------------------------------------------------------------
|
|
225
|
+
# Test 6: auto_discover
|
|
226
|
+
# ---------------------------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class TestAutoDiscover:
|
|
230
|
+
"""Test auto_discover()."""
|
|
231
|
+
|
|
232
|
+
def test_discover_from_heartbeats(self, tmp_path):
|
|
233
|
+
"""auto_discover() adds peers found in heartbeat files."""
|
|
234
|
+
hb_dir = tmp_path / "heartbeats"
|
|
235
|
+
write_heartbeat(hb_dir, "lumina", fingerprint="FP123")
|
|
236
|
+
write_heartbeat(hb_dir, "jarvis")
|
|
237
|
+
|
|
238
|
+
d = make_directory(tmp_path)
|
|
239
|
+
added = d.auto_discover(heartbeats_dir=hb_dir)
|
|
240
|
+
|
|
241
|
+
names = {e.name for e in added}
|
|
242
|
+
assert "lumina" in names
|
|
243
|
+
assert "jarvis" in names
|
|
244
|
+
# Transport should default to syncthing
|
|
245
|
+
assert all(e.transport == "syncthing" for e in added)
|
|
246
|
+
|
|
247
|
+
def test_discover_from_outbox_dirs(self, tmp_path):
|
|
248
|
+
"""auto_discover() adds peers from Syncthing outbox directories."""
|
|
249
|
+
outbox_root = tmp_path / "sync" / "comms" / "outbox"
|
|
250
|
+
(outbox_root / "ava").mkdir(parents=True)
|
|
251
|
+
(outbox_root / "mcp-builder").mkdir(parents=True)
|
|
252
|
+
|
|
253
|
+
d = make_directory(tmp_path)
|
|
254
|
+
added = d.auto_discover()
|
|
255
|
+
|
|
256
|
+
names = {e.name for e in added}
|
|
257
|
+
assert "ava" in names
|
|
258
|
+
assert "mcp-builder" in names
|
|
259
|
+
|
|
260
|
+
def test_discover_skips_known(self, tmp_path):
|
|
261
|
+
"""auto_discover() does not overwrite existing entries."""
|
|
262
|
+
hb_dir = tmp_path / "heartbeats"
|
|
263
|
+
write_heartbeat(hb_dir, "lumina")
|
|
264
|
+
|
|
265
|
+
d = make_directory(tmp_path)
|
|
266
|
+
d.add_peer("lumina", "/custom/path")
|
|
267
|
+
added = d.auto_discover(heartbeats_dir=hb_dir)
|
|
268
|
+
|
|
269
|
+
# lumina was already known — should not appear in added
|
|
270
|
+
added_names = {e.name for e in added}
|
|
271
|
+
assert "lumina" not in added_names
|
|
272
|
+
# And the existing address must be preserved
|
|
273
|
+
assert d.resolve("lumina") == "/custom/path"
|
|
274
|
+
|
|
275
|
+
def test_discover_empty_dirs(self, tmp_path):
|
|
276
|
+
"""auto_discover() on empty dirs returns empty list."""
|
|
277
|
+
d = make_directory(tmp_path)
|
|
278
|
+
added = d.auto_discover()
|
|
279
|
+
assert added == []
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
# ---------------------------------------------------------------------------
|
|
283
|
+
# Test 7: YAML persistence round-trip
|
|
284
|
+
# ---------------------------------------------------------------------------
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
class TestPersistence:
|
|
288
|
+
"""Test that entries survive a full save → load cycle."""
|
|
289
|
+
|
|
290
|
+
def test_round_trip(self, tmp_path):
|
|
291
|
+
"""Entries written by one PeerDirectory instance are readable by another."""
|
|
292
|
+
d1 = make_directory(tmp_path)
|
|
293
|
+
d1.add_peer("Lumina", "/outbox/lumina", transport="syncthing", fingerprint="FP99")
|
|
294
|
+
d1.add_peer("Grok", "/outbox/grok", transport="tailscale")
|
|
295
|
+
|
|
296
|
+
# Fresh instance — reads from disk
|
|
297
|
+
d2 = make_directory(tmp_path)
|
|
298
|
+
d2.load()
|
|
299
|
+
assert d2.resolve("lumina") == "/outbox/lumina"
|
|
300
|
+
assert d2.resolve("grok") == "/outbox/grok"
|
|
301
|
+
lumina = next(p for p in d2.list_peers() if p.name == "lumina")
|
|
302
|
+
assert lumina.fingerprint == "FP99"
|
|
303
|
+
|
|
304
|
+
def test_yaml_file_created(self, tmp_path):
|
|
305
|
+
"""add_peer() creates the directory.yaml file."""
|
|
306
|
+
d = make_directory(tmp_path)
|
|
307
|
+
d.add_peer("Opus", "/outbox/opus")
|
|
308
|
+
yaml_path = tmp_path / "peers" / "directory.yaml"
|
|
309
|
+
assert yaml_path.exists()
|
|
310
|
+
data = yaml.safe_load(yaml_path.read_text())
|
|
311
|
+
assert "opus" in data
|
|
312
|
+
|
|
313
|
+
def test_remove_removes_from_yaml(self, tmp_path):
|
|
314
|
+
"""remove_peer() removes the entry from YAML on disk."""
|
|
315
|
+
d = make_directory(tmp_path)
|
|
316
|
+
d.add_peer("Jarvis", "/outbox/jarvis")
|
|
317
|
+
d.remove_peer("Jarvis")
|
|
318
|
+
|
|
319
|
+
yaml_path = tmp_path / "peers" / "directory.yaml"
|
|
320
|
+
data = yaml.safe_load(yaml_path.read_text()) or {}
|
|
321
|
+
assert "jarvis" not in data
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
# ---------------------------------------------------------------------------
|
|
325
|
+
# Test 8: CLI — peers list, add, discover
|
|
326
|
+
# ---------------------------------------------------------------------------
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
class TestCLI:
|
|
330
|
+
"""Test the `skcapstone peers` CLI commands."""
|
|
331
|
+
|
|
332
|
+
def test_peers_help(self):
|
|
333
|
+
"""`peers --help` exits cleanly."""
|
|
334
|
+
from skcapstone.cli import main
|
|
335
|
+
runner = CliRunner()
|
|
336
|
+
result = runner.invoke(main, ["peers", "--help"])
|
|
337
|
+
assert result.exit_code == 0
|
|
338
|
+
assert "list" in result.output
|
|
339
|
+
assert "add" in result.output
|
|
340
|
+
|
|
341
|
+
def test_peers_list_empty(self, tmp_path):
|
|
342
|
+
"""`peers list` on empty directory shows no-peers message."""
|
|
343
|
+
from skcapstone.cli import main
|
|
344
|
+
runner = CliRunner()
|
|
345
|
+
result = runner.invoke(main, ["peers", "list", "--home", str(tmp_path)])
|
|
346
|
+
assert result.exit_code == 0
|
|
347
|
+
assert "No peers" in result.output
|
|
348
|
+
|
|
349
|
+
def test_peers_add_and_list(self, tmp_path):
|
|
350
|
+
"""`peers add` then `peers list` shows the new entry."""
|
|
351
|
+
from skcapstone.cli import main
|
|
352
|
+
runner = CliRunner()
|
|
353
|
+
|
|
354
|
+
add_result = runner.invoke(main, [
|
|
355
|
+
"peers", "add",
|
|
356
|
+
"--name", "Lumina",
|
|
357
|
+
"--address", "/outbox/lumina",
|
|
358
|
+
"--home", str(tmp_path),
|
|
359
|
+
])
|
|
360
|
+
assert add_result.exit_code == 0, add_result.output
|
|
361
|
+
assert "lumina" in add_result.output.lower()
|
|
362
|
+
|
|
363
|
+
list_result = runner.invoke(main, ["peers", "list", "--home", str(tmp_path)])
|
|
364
|
+
assert list_result.exit_code == 0
|
|
365
|
+
assert "lumina" in list_result.output.lower()
|
|
366
|
+
|
|
367
|
+
def test_peers_list_json(self, tmp_path):
|
|
368
|
+
"""`peers list --json-out` produces valid JSON."""
|
|
369
|
+
from skcapstone.cli import main
|
|
370
|
+
runner = CliRunner()
|
|
371
|
+
|
|
372
|
+
runner.invoke(main, [
|
|
373
|
+
"peers", "add",
|
|
374
|
+
"--name", "Grok",
|
|
375
|
+
"--address", "/outbox/grok",
|
|
376
|
+
"--home", str(tmp_path),
|
|
377
|
+
])
|
|
378
|
+
|
|
379
|
+
result = runner.invoke(main, ["peers", "list", "--json-out", "--home", str(tmp_path)])
|
|
380
|
+
assert result.exit_code == 0
|
|
381
|
+
data = json.loads(result.output)
|
|
382
|
+
assert isinstance(data, list)
|
|
383
|
+
assert any(p["name"] == "grok" for p in data)
|
|
384
|
+
|
|
385
|
+
def test_peers_discover_cli(self, tmp_path):
|
|
386
|
+
"""`peers discover` reports newly found peers."""
|
|
387
|
+
from skcapstone.cli import main
|
|
388
|
+
|
|
389
|
+
hb_dir = tmp_path / "heartbeats"
|
|
390
|
+
write_heartbeat(hb_dir, "lumina")
|
|
391
|
+
|
|
392
|
+
runner = CliRunner()
|
|
393
|
+
result = runner.invoke(main, ["peers", "discover", "--home", str(tmp_path)])
|
|
394
|
+
assert result.exit_code == 0
|
|
395
|
+
assert "lumina" in result.output.lower()
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""Tests for the skcapstone peer management module.
|
|
2
|
+
|
|
3
|
+
Covers:
|
|
4
|
+
- add_peer_from_card (success, missing file, invalid JSON, missing name)
|
|
5
|
+
- add_peer_manual (with/without key)
|
|
6
|
+
- list_peers (empty, with peers)
|
|
7
|
+
- get_peer / remove_peer
|
|
8
|
+
- PeerRecord model
|
|
9
|
+
- SKComm peer file creation
|
|
10
|
+
- CLI commands (add, list, remove, show)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
import pytest
|
|
19
|
+
from click.testing import CliRunner
|
|
20
|
+
|
|
21
|
+
from skcapstone.peers import (
|
|
22
|
+
PeerRecord,
|
|
23
|
+
add_peer_from_card,
|
|
24
|
+
add_peer_manual,
|
|
25
|
+
get_peer,
|
|
26
|
+
list_peers,
|
|
27
|
+
remove_peer,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.fixture
|
|
32
|
+
def homes(tmp_path):
|
|
33
|
+
"""Create skcapstone and skcomm home directories."""
|
|
34
|
+
sk = tmp_path / ".skcapstone"
|
|
35
|
+
sc = tmp_path / ".skcomm"
|
|
36
|
+
sk.mkdir()
|
|
37
|
+
sc.mkdir()
|
|
38
|
+
return sk, sc
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@pytest.fixture
|
|
42
|
+
def card_file(tmp_path):
|
|
43
|
+
"""Create a sample identity card file."""
|
|
44
|
+
card = {
|
|
45
|
+
"skcapstone_card": "1.0.0",
|
|
46
|
+
"name": "Lumina",
|
|
47
|
+
"fingerprint": "AABB1122CCDD3344EEFF5566",
|
|
48
|
+
"public_key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\nfakekey\n-----END PGP PUBLIC KEY BLOCK-----",
|
|
49
|
+
"entity_type": "ai",
|
|
50
|
+
"handle": "lumina@skworld.io",
|
|
51
|
+
"email": "lumina@skworld.io",
|
|
52
|
+
"capabilities": ["capauth:identity", "skchat:p2p-chat"],
|
|
53
|
+
"contact_uris": ["capauth:AABB1122CCDD3344"],
|
|
54
|
+
}
|
|
55
|
+
path = tmp_path / "lumina-card.json"
|
|
56
|
+
path.write_text(json.dumps(card, indent=2))
|
|
57
|
+
return path
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class TestPeerRecord:
|
|
61
|
+
"""Test PeerRecord model."""
|
|
62
|
+
|
|
63
|
+
def test_defaults(self):
|
|
64
|
+
"""Record has sensible defaults."""
|
|
65
|
+
p = PeerRecord(name="Test")
|
|
66
|
+
assert p.name == "Test"
|
|
67
|
+
assert p.trust_level == "unknown"
|
|
68
|
+
assert p.source == "manual"
|
|
69
|
+
assert p.added_at != ""
|
|
70
|
+
|
|
71
|
+
def test_serialization(self):
|
|
72
|
+
"""Record round-trips through JSON."""
|
|
73
|
+
p = PeerRecord(
|
|
74
|
+
name="Opus",
|
|
75
|
+
fingerprint="FP123",
|
|
76
|
+
capabilities=["capauth:identity"],
|
|
77
|
+
)
|
|
78
|
+
data = json.loads(p.model_dump_json())
|
|
79
|
+
restored = PeerRecord.model_validate(data)
|
|
80
|
+
assert restored.name == "Opus"
|
|
81
|
+
assert restored.fingerprint == "FP123"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class TestAddPeerFromCard:
|
|
85
|
+
"""Test importing peers from identity cards."""
|
|
86
|
+
|
|
87
|
+
def test_add_from_card(self, card_file, homes):
|
|
88
|
+
"""Card import creates peer records in both registries."""
|
|
89
|
+
sk, sc = homes
|
|
90
|
+
peer = add_peer_from_card(card_file, skcapstone_home=sk, skcomm_home=sc)
|
|
91
|
+
|
|
92
|
+
assert peer.name == "Lumina"
|
|
93
|
+
assert peer.fingerprint == "AABB1122CCDD3344EEFF5566"
|
|
94
|
+
assert peer.entity_type == "ai"
|
|
95
|
+
assert peer.trust_level == "verified"
|
|
96
|
+
assert peer.source == "card"
|
|
97
|
+
assert "capauth:identity" in peer.capabilities
|
|
98
|
+
|
|
99
|
+
assert (sk / "peers" / "lumina.json").exists()
|
|
100
|
+
assert (sc / "peers" / "lumina.yml").exists()
|
|
101
|
+
assert (sc / "peers" / "lumina.pub.asc").exists()
|
|
102
|
+
|
|
103
|
+
def test_missing_card_raises(self, homes):
|
|
104
|
+
"""Nonexistent card raises FileNotFoundError."""
|
|
105
|
+
sk, sc = homes
|
|
106
|
+
with pytest.raises(FileNotFoundError):
|
|
107
|
+
add_peer_from_card(Path("/nope.json"), skcapstone_home=sk, skcomm_home=sc)
|
|
108
|
+
|
|
109
|
+
def test_invalid_json_raises(self, tmp_path, homes):
|
|
110
|
+
"""Invalid JSON raises ValueError."""
|
|
111
|
+
sk, sc = homes
|
|
112
|
+
bad = tmp_path / "bad.json"
|
|
113
|
+
bad.write_text("{{{not json")
|
|
114
|
+
with pytest.raises(ValueError):
|
|
115
|
+
add_peer_from_card(bad, skcapstone_home=sk, skcomm_home=sc)
|
|
116
|
+
|
|
117
|
+
def test_missing_name_raises(self, tmp_path, homes):
|
|
118
|
+
"""Card without name raises ValueError."""
|
|
119
|
+
sk, sc = homes
|
|
120
|
+
no_name = tmp_path / "noname.json"
|
|
121
|
+
no_name.write_text(json.dumps({"fingerprint": "123"}))
|
|
122
|
+
with pytest.raises(ValueError, match="name"):
|
|
123
|
+
add_peer_from_card(no_name, skcapstone_home=sk, skcomm_home=sc)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class TestAddPeerManual:
|
|
127
|
+
"""Test manual peer creation."""
|
|
128
|
+
|
|
129
|
+
def test_add_manual_basic(self, homes):
|
|
130
|
+
"""Manual add creates a peer record."""
|
|
131
|
+
sk, sc = homes
|
|
132
|
+
peer = add_peer_manual(
|
|
133
|
+
name="Opus", email="opus@smilintux.org",
|
|
134
|
+
skcapstone_home=sk, skcomm_home=sc,
|
|
135
|
+
)
|
|
136
|
+
assert peer.name == "Opus"
|
|
137
|
+
assert peer.email == "opus@smilintux.org"
|
|
138
|
+
assert (sk / "peers" / "opus.json").exists()
|
|
139
|
+
|
|
140
|
+
def test_add_manual_with_key(self, tmp_path, homes):
|
|
141
|
+
"""Manual add with public key file imports the key."""
|
|
142
|
+
sk, sc = homes
|
|
143
|
+
key_file = tmp_path / "opus.pub.asc"
|
|
144
|
+
key_file.write_text("-----BEGIN PGP PUBLIC KEY BLOCK-----\nfake\n-----END PGP PUBLIC KEY BLOCK-----")
|
|
145
|
+
|
|
146
|
+
peer = add_peer_manual(
|
|
147
|
+
name="Opus", public_key_path=key_file,
|
|
148
|
+
skcapstone_home=sk, skcomm_home=sc,
|
|
149
|
+
)
|
|
150
|
+
assert peer.public_key != ""
|
|
151
|
+
assert peer.trust_level == "verified"
|
|
152
|
+
assert (sc / "peers" / "opus.pub.asc").exists()
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class TestListPeers:
|
|
156
|
+
"""Test peer listing."""
|
|
157
|
+
|
|
158
|
+
def test_empty_list(self, homes):
|
|
159
|
+
"""No peers returns empty list."""
|
|
160
|
+
sk, _ = homes
|
|
161
|
+
assert list_peers(skcapstone_home=sk) == []
|
|
162
|
+
|
|
163
|
+
def test_list_with_peers(self, card_file, homes):
|
|
164
|
+
"""Added peers appear in listing."""
|
|
165
|
+
sk, sc = homes
|
|
166
|
+
add_peer_from_card(card_file, skcapstone_home=sk, skcomm_home=sc)
|
|
167
|
+
|
|
168
|
+
peers = list_peers(skcapstone_home=sk)
|
|
169
|
+
assert len(peers) == 1
|
|
170
|
+
assert peers[0].name == "Lumina"
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class TestGetPeer:
|
|
174
|
+
"""Test single peer lookup."""
|
|
175
|
+
|
|
176
|
+
def test_get_existing(self, card_file, homes):
|
|
177
|
+
"""Known peer is returned."""
|
|
178
|
+
sk, sc = homes
|
|
179
|
+
add_peer_from_card(card_file, skcapstone_home=sk, skcomm_home=sc)
|
|
180
|
+
|
|
181
|
+
peer = get_peer("Lumina", skcapstone_home=sk)
|
|
182
|
+
assert peer is not None
|
|
183
|
+
assert peer.name == "Lumina"
|
|
184
|
+
|
|
185
|
+
def test_get_unknown(self, homes):
|
|
186
|
+
"""Unknown peer returns None."""
|
|
187
|
+
sk, _ = homes
|
|
188
|
+
assert get_peer("Nobody", skcapstone_home=sk) is None
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class TestRemovePeer:
|
|
192
|
+
"""Test peer removal."""
|
|
193
|
+
|
|
194
|
+
def test_remove_existing(self, card_file, homes):
|
|
195
|
+
"""Removing an existing peer cleans up all files."""
|
|
196
|
+
sk, sc = homes
|
|
197
|
+
add_peer_from_card(card_file, skcapstone_home=sk, skcomm_home=sc)
|
|
198
|
+
|
|
199
|
+
assert remove_peer("Lumina", skcapstone_home=sk, skcomm_home=sc)
|
|
200
|
+
assert not (sk / "peers" / "lumina.json").exists()
|
|
201
|
+
assert not (sc / "peers" / "lumina.yml").exists()
|
|
202
|
+
|
|
203
|
+
def test_remove_unknown(self, homes):
|
|
204
|
+
"""Removing unknown peer returns False."""
|
|
205
|
+
sk, sc = homes
|
|
206
|
+
assert not remove_peer("Nobody", skcapstone_home=sk, skcomm_home=sc)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class TestCLI:
|
|
210
|
+
"""Test peer CLI commands."""
|
|
211
|
+
|
|
212
|
+
def test_peer_help(self):
|
|
213
|
+
"""peer --help works."""
|
|
214
|
+
from skcapstone.cli import main
|
|
215
|
+
runner = CliRunner()
|
|
216
|
+
result = runner.invoke(main, ["peer", "--help"])
|
|
217
|
+
assert result.exit_code == 0
|
|
218
|
+
assert "add" in result.output
|
|
219
|
+
assert "list" in result.output
|
|
220
|
+
assert "remove" in result.output
|
|
221
|
+
assert "show" in result.output
|
|
222
|
+
|
|
223
|
+
def test_peer_list_empty(self, homes):
|
|
224
|
+
"""peer list on empty registry shows message."""
|
|
225
|
+
from skcapstone.cli import main
|
|
226
|
+
sk, _ = homes
|
|
227
|
+
runner = CliRunner()
|
|
228
|
+
result = runner.invoke(main, ["peer", "list", "--home", str(sk)])
|
|
229
|
+
assert result.exit_code == 0
|
|
230
|
+
assert "No peers" in result.output
|
|
231
|
+
|
|
232
|
+
def test_peer_add_from_card_cli(self, card_file, homes):
|
|
233
|
+
"""peer add --card via CLI."""
|
|
234
|
+
from skcapstone.cli import main
|
|
235
|
+
sk, _ = homes
|
|
236
|
+
runner = CliRunner()
|
|
237
|
+
result = runner.invoke(main, [
|
|
238
|
+
"peer", "add", "--card", str(card_file), "--home", str(sk),
|
|
239
|
+
])
|
|
240
|
+
assert result.exit_code == 0
|
|
241
|
+
assert "Lumina" in result.output
|
|
242
|
+
|
|
243
|
+
def test_peer_add_no_args(self):
|
|
244
|
+
"""peer add without args shows usage hint."""
|
|
245
|
+
from skcapstone.cli import main
|
|
246
|
+
runner = CliRunner()
|
|
247
|
+
result = runner.invoke(main, ["peer", "add"])
|
|
248
|
+
assert result.exit_code == 1
|