@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,394 @@
|
|
|
1
|
+
"""Tests for the agent mood tracker."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import threading
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from skcapstone.mood import (
|
|
12
|
+
MoodSnapshot,
|
|
13
|
+
MoodTracker,
|
|
14
|
+
_classify_social,
|
|
15
|
+
_classify_stress,
|
|
16
|
+
_classify_success,
|
|
17
|
+
_compute_summary,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
# Axis classifier unit tests
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TestClassifySuccess:
|
|
27
|
+
"""Unit tests for _classify_success."""
|
|
28
|
+
|
|
29
|
+
def test_high_rate_is_happy(self) -> None:
|
|
30
|
+
""">=90% success rate maps to 'happy'."""
|
|
31
|
+
assert _classify_success(0.95) == "happy"
|
|
32
|
+
assert _classify_success(1.0) == "happy"
|
|
33
|
+
assert _classify_success(0.9) == "happy"
|
|
34
|
+
|
|
35
|
+
def test_moderate_rate_is_content(self) -> None:
|
|
36
|
+
"""70–89% maps to 'content'."""
|
|
37
|
+
assert _classify_success(0.80) == "content"
|
|
38
|
+
assert _classify_success(0.70) == "content"
|
|
39
|
+
|
|
40
|
+
def test_borderline_rate_is_neutral(self) -> None:
|
|
41
|
+
"""50–69% maps to 'neutral'."""
|
|
42
|
+
assert _classify_success(0.60) == "neutral"
|
|
43
|
+
assert _classify_success(0.50) == "neutral"
|
|
44
|
+
|
|
45
|
+
def test_low_rate_is_frustrated(self) -> None:
|
|
46
|
+
"""<50% maps to 'frustrated'."""
|
|
47
|
+
assert _classify_success(0.49) == "frustrated"
|
|
48
|
+
assert _classify_success(0.0) == "frustrated"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class TestClassifySocial:
|
|
52
|
+
"""Unit tests for _classify_social."""
|
|
53
|
+
|
|
54
|
+
def test_high_frequency_is_social(self) -> None:
|
|
55
|
+
""">=10 msgs/hr maps to 'social'."""
|
|
56
|
+
assert _classify_social(10.0) == "social"
|
|
57
|
+
assert _classify_social(20.0) == "social"
|
|
58
|
+
|
|
59
|
+
def test_medium_frequency_is_active(self) -> None:
|
|
60
|
+
"""3–9 msgs/hr maps to 'active'."""
|
|
61
|
+
assert _classify_social(5.0) == "active"
|
|
62
|
+
assert _classify_social(3.0) == "active"
|
|
63
|
+
|
|
64
|
+
def test_low_frequency_is_quiet(self) -> None:
|
|
65
|
+
"""0.5–2 msgs/hr maps to 'quiet'."""
|
|
66
|
+
assert _classify_social(1.0) == "quiet"
|
|
67
|
+
assert _classify_social(0.5) == "quiet"
|
|
68
|
+
|
|
69
|
+
def test_no_activity_is_isolated(self) -> None:
|
|
70
|
+
"""<0.5 msgs/hr maps to 'isolated'."""
|
|
71
|
+
assert _classify_social(0.1) == "isolated"
|
|
72
|
+
assert _classify_social(0.0) == "isolated"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class TestClassifyStress:
|
|
76
|
+
"""Unit tests for _classify_stress."""
|
|
77
|
+
|
|
78
|
+
def test_very_low_errors_are_calm(self) -> None:
|
|
79
|
+
"""<5% error rate maps to 'calm'."""
|
|
80
|
+
assert _classify_stress(0.0) == "calm"
|
|
81
|
+
assert _classify_stress(0.04) == "calm"
|
|
82
|
+
|
|
83
|
+
def test_low_errors_are_relaxed(self) -> None:
|
|
84
|
+
"""5–14% maps to 'relaxed'."""
|
|
85
|
+
assert _classify_stress(0.10) == "relaxed"
|
|
86
|
+
assert _classify_stress(0.05) == "relaxed"
|
|
87
|
+
|
|
88
|
+
def test_moderate_errors_are_tense(self) -> None:
|
|
89
|
+
"""15–29% maps to 'tense'."""
|
|
90
|
+
assert _classify_stress(0.20) == "tense"
|
|
91
|
+
assert _classify_stress(0.15) == "tense"
|
|
92
|
+
|
|
93
|
+
def test_high_errors_are_stressed(self) -> None:
|
|
94
|
+
""">=30% maps to 'stressed'."""
|
|
95
|
+
assert _classify_stress(0.30) == "stressed"
|
|
96
|
+
assert _classify_stress(1.0) == "stressed"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class TestComputeSummary:
|
|
100
|
+
"""Unit tests for _compute_summary."""
|
|
101
|
+
|
|
102
|
+
def test_stressed_overrides_all(self) -> None:
|
|
103
|
+
"""'stressed' dominates regardless of other axes."""
|
|
104
|
+
assert _compute_summary("happy", "social", "stressed") == "stressed"
|
|
105
|
+
|
|
106
|
+
def test_frustrated_overrides_non_stressed(self) -> None:
|
|
107
|
+
"""'frustrated' wins when stress is not 'stressed'."""
|
|
108
|
+
assert _compute_summary("frustrated", "social", "calm") == "frustrated"
|
|
109
|
+
assert _compute_summary("frustrated", "active", "relaxed") == "frustrated"
|
|
110
|
+
|
|
111
|
+
def test_tense_follows_frustrated(self) -> None:
|
|
112
|
+
"""'tense' wins when not frustrated."""
|
|
113
|
+
assert _compute_summary("content", "active", "tense") == "tense"
|
|
114
|
+
|
|
115
|
+
def test_isolated_when_not_engaged(self) -> None:
|
|
116
|
+
"""Isolation surfaces when not otherwise stressed or frustrated."""
|
|
117
|
+
assert _compute_summary("neutral", "isolated", "calm") == "isolated"
|
|
118
|
+
|
|
119
|
+
def test_flourishing_when_happy_and_active(self) -> None:
|
|
120
|
+
"""Happy + socially active → 'flourishing'."""
|
|
121
|
+
assert _compute_summary("happy", "social", "calm") == "flourishing"
|
|
122
|
+
assert _compute_summary("happy", "active", "calm") == "flourishing"
|
|
123
|
+
|
|
124
|
+
def test_happy_without_social(self) -> None:
|
|
125
|
+
"""Happy but quiet stays 'happy'."""
|
|
126
|
+
assert _compute_summary("happy", "quiet", "calm") == "happy"
|
|
127
|
+
|
|
128
|
+
def test_content_maps_to_content(self) -> None:
|
|
129
|
+
"""content + quiet + calm → 'content'."""
|
|
130
|
+
assert _compute_summary("content", "quiet", "calm") == "content"
|
|
131
|
+
|
|
132
|
+
def test_fallback_is_neutral(self) -> None:
|
|
133
|
+
"""neutral + quiet + calm → 'neutral'."""
|
|
134
|
+
assert _compute_summary("neutral", "quiet", "calm") == "neutral"
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# ---------------------------------------------------------------------------
|
|
138
|
+
# MoodTracker integration tests
|
|
139
|
+
# ---------------------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@pytest.fixture
|
|
143
|
+
def tracker(tmp_path: Path) -> MoodTracker:
|
|
144
|
+
"""MoodTracker using a temp home directory."""
|
|
145
|
+
return MoodTracker(home=tmp_path)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class TestMoodTrackerUpdate:
|
|
149
|
+
"""Tests for MoodTracker.update()."""
|
|
150
|
+
|
|
151
|
+
def test_happy_high_success(self, tracker: MoodTracker) -> None:
|
|
152
|
+
"""High response rate produces 'happy' success_mood."""
|
|
153
|
+
snap = tracker.update(messages=100, responses=95, errors=0)
|
|
154
|
+
assert snap.success_mood == "happy"
|
|
155
|
+
assert snap.summary in ("happy", "flourishing")
|
|
156
|
+
|
|
157
|
+
def test_frustrated_low_success(self, tracker: MoodTracker) -> None:
|
|
158
|
+
"""Low response rate produces 'frustrated' success_mood and summary."""
|
|
159
|
+
snap = tracker.update(messages=100, responses=10, errors=0)
|
|
160
|
+
assert snap.success_mood == "frustrated"
|
|
161
|
+
assert snap.summary == "frustrated"
|
|
162
|
+
|
|
163
|
+
def test_stressed_high_errors(self, tracker: MoodTracker) -> None:
|
|
164
|
+
"""High error rate produces 'stressed' and overrides success in summary."""
|
|
165
|
+
snap = tracker.update(messages=100, responses=90, errors=40)
|
|
166
|
+
assert snap.stress_mood == "stressed"
|
|
167
|
+
assert snap.summary == "stressed"
|
|
168
|
+
|
|
169
|
+
def test_calm_low_errors(self, tracker: MoodTracker) -> None:
|
|
170
|
+
"""Near-zero errors produce 'calm' stress mood."""
|
|
171
|
+
snap = tracker.update(messages=50, responses=49, errors=1)
|
|
172
|
+
assert snap.stress_mood == "calm"
|
|
173
|
+
|
|
174
|
+
def test_social_high_frequency(self, tracker: MoodTracker) -> None:
|
|
175
|
+
"""Many messages in a short window → 'social'."""
|
|
176
|
+
# 100 messages over 1 hour = 100 msgs/hr
|
|
177
|
+
snap = tracker.update(messages=100, responses=90, errors=0, window_hours=1)
|
|
178
|
+
assert snap.social_mood == "social"
|
|
179
|
+
|
|
180
|
+
def test_isolated_no_messages(self, tracker: MoodTracker) -> None:
|
|
181
|
+
"""Few messages in a long window → 'isolated'."""
|
|
182
|
+
snap = tracker.update(messages=2, responses=2, errors=0, window_hours=24)
|
|
183
|
+
assert snap.social_mood == "isolated"
|
|
184
|
+
|
|
185
|
+
def test_zero_messages_defaults_to_neutral(self, tracker: MoodTracker) -> None:
|
|
186
|
+
"""Zero messages produce safe default rates (no division by zero)."""
|
|
187
|
+
snap = tracker.update(messages=0, responses=0, errors=0)
|
|
188
|
+
assert snap.success_rate == 1.0
|
|
189
|
+
assert snap.error_rate == 0.0
|
|
190
|
+
assert snap.summary in ("neutral", "isolated") # isolated because 0 msgs/hr
|
|
191
|
+
|
|
192
|
+
def test_rates_are_clamped_to_four_decimals(self, tracker: MoodTracker) -> None:
|
|
193
|
+
"""success_rate and error_rate are rounded to 4 decimal places."""
|
|
194
|
+
snap = tracker.update(messages=3, responses=2, errors=1)
|
|
195
|
+
# 2/3 ≈ 0.6667, 1/3 ≈ 0.3333
|
|
196
|
+
assert snap.success_rate == round(2 / 3, 4)
|
|
197
|
+
assert snap.error_rate == round(1 / 3, 4)
|
|
198
|
+
|
|
199
|
+
def test_updated_at_is_set(self, tracker: MoodTracker) -> None:
|
|
200
|
+
"""updated_at is populated after update."""
|
|
201
|
+
snap = tracker.update(messages=5, responses=5, errors=0)
|
|
202
|
+
assert snap.updated_at != ""
|
|
203
|
+
assert "T" in snap.updated_at # ISO-8601 format
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
# ---------------------------------------------------------------------------
|
|
207
|
+
# Persistence
|
|
208
|
+
# ---------------------------------------------------------------------------
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class TestMoodPersistence:
|
|
212
|
+
"""Tests for save / load round-trip."""
|
|
213
|
+
|
|
214
|
+
def test_update_persists_file(self, tmp_path: Path) -> None:
|
|
215
|
+
"""update() writes mood.json to the home directory."""
|
|
216
|
+
tracker = MoodTracker(home=tmp_path)
|
|
217
|
+
tracker.update(messages=10, responses=9, errors=0)
|
|
218
|
+
assert (tmp_path / "mood.json").exists()
|
|
219
|
+
|
|
220
|
+
def test_reload_recovers_state(self, tmp_path: Path) -> None:
|
|
221
|
+
"""A second MoodTracker in the same home loads the saved snapshot."""
|
|
222
|
+
t1 = MoodTracker(home=tmp_path)
|
|
223
|
+
t1.update(messages=20, responses=18, errors=1)
|
|
224
|
+
|
|
225
|
+
t2 = MoodTracker(home=tmp_path)
|
|
226
|
+
snap = t2.snapshot
|
|
227
|
+
assert snap.messages_processed == 20
|
|
228
|
+
assert snap.responses_sent == 18
|
|
229
|
+
assert snap.errors == 1
|
|
230
|
+
|
|
231
|
+
def test_corrupt_file_yields_neutral(self, tmp_path: Path) -> None:
|
|
232
|
+
"""Corrupt mood.json is silently ignored; tracker starts neutral."""
|
|
233
|
+
mood_path = tmp_path / "mood.json"
|
|
234
|
+
mood_path.write_text("not valid json {{{", encoding="utf-8")
|
|
235
|
+
tracker = MoodTracker(home=tmp_path)
|
|
236
|
+
snap = tracker.snapshot
|
|
237
|
+
assert snap.summary == "neutral"
|
|
238
|
+
|
|
239
|
+
def test_missing_file_yields_neutral(self, tmp_path: Path) -> None:
|
|
240
|
+
"""Absent mood.json yields a neutral default snapshot."""
|
|
241
|
+
tracker = MoodTracker(home=tmp_path / "nonexistent")
|
|
242
|
+
snap = tracker.snapshot
|
|
243
|
+
assert snap.summary == "neutral"
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
# ---------------------------------------------------------------------------
|
|
247
|
+
# update_from_metrics
|
|
248
|
+
# ---------------------------------------------------------------------------
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class TestUpdateFromMetrics:
|
|
252
|
+
"""Tests for MoodTracker.update_from_metrics()."""
|
|
253
|
+
|
|
254
|
+
def test_reads_consciousness_metrics(self, tmp_path: Path) -> None:
|
|
255
|
+
"""update_from_metrics reads from ConsciousnessMetrics.to_dict()."""
|
|
256
|
+
from skcapstone.metrics import ConsciousnessMetrics
|
|
257
|
+
|
|
258
|
+
cm = ConsciousnessMetrics(home=tmp_path, persist_interval=0)
|
|
259
|
+
for _ in range(5):
|
|
260
|
+
cm.record_message("peer-a")
|
|
261
|
+
for _ in range(4):
|
|
262
|
+
cm.record_response(50.0, "ollama", "fast")
|
|
263
|
+
cm.record_error()
|
|
264
|
+
|
|
265
|
+
tracker = MoodTracker(home=tmp_path)
|
|
266
|
+
snap = tracker.update_from_metrics(cm)
|
|
267
|
+
assert snap.messages_processed == 5
|
|
268
|
+
assert snap.responses_sent == 4
|
|
269
|
+
assert snap.errors == 1
|
|
270
|
+
|
|
271
|
+
def test_bad_metrics_object_returns_current_snapshot(self, tmp_path: Path) -> None:
|
|
272
|
+
"""update_from_metrics with a broken object returns existing snapshot."""
|
|
273
|
+
|
|
274
|
+
class _BrokenMetrics:
|
|
275
|
+
def to_dict(self):
|
|
276
|
+
raise RuntimeError("broken")
|
|
277
|
+
|
|
278
|
+
tracker = MoodTracker(home=tmp_path)
|
|
279
|
+
snap_before = tracker.snapshot
|
|
280
|
+
snap_after = tracker.update_from_metrics(_BrokenMetrics())
|
|
281
|
+
assert snap_after.summary == snap_before.summary
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
# ---------------------------------------------------------------------------
|
|
285
|
+
# load_snapshot classmethod
|
|
286
|
+
# ---------------------------------------------------------------------------
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
class TestLoadSnapshot:
|
|
290
|
+
"""Tests for MoodTracker.load_snapshot()."""
|
|
291
|
+
|
|
292
|
+
def test_returns_default_when_no_file(self, tmp_path: Path) -> None:
|
|
293
|
+
"""Returns a neutral MoodSnapshot when no file exists."""
|
|
294
|
+
snap = MoodTracker.load_snapshot(home=tmp_path)
|
|
295
|
+
assert isinstance(snap, MoodSnapshot)
|
|
296
|
+
assert snap.summary == "neutral"
|
|
297
|
+
|
|
298
|
+
def test_returns_saved_snapshot(self, tmp_path: Path) -> None:
|
|
299
|
+
"""Returns the persisted snapshot when mood.json exists."""
|
|
300
|
+
t = MoodTracker(home=tmp_path)
|
|
301
|
+
t.update(messages=30, responses=28, errors=0)
|
|
302
|
+
snap = MoodTracker.load_snapshot(home=tmp_path)
|
|
303
|
+
assert snap.messages_processed == 30
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
# ---------------------------------------------------------------------------
|
|
307
|
+
# describe()
|
|
308
|
+
# ---------------------------------------------------------------------------
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
class TestDescribe:
|
|
312
|
+
"""Tests for MoodTracker.describe()."""
|
|
313
|
+
|
|
314
|
+
def test_describe_contains_summary(self, tracker: MoodTracker) -> None:
|
|
315
|
+
"""describe() includes the summary word."""
|
|
316
|
+
tracker.update(messages=10, responses=10, errors=0)
|
|
317
|
+
text = tracker.describe()
|
|
318
|
+
assert "Mood summary" in text
|
|
319
|
+
|
|
320
|
+
def test_describe_contains_all_axes(self, tracker: MoodTracker) -> None:
|
|
321
|
+
"""describe() mentions all three mood axes."""
|
|
322
|
+
tracker.update(messages=10, responses=9, errors=0)
|
|
323
|
+
text = tracker.describe()
|
|
324
|
+
assert "Success" in text
|
|
325
|
+
assert "Social" in text
|
|
326
|
+
assert "Stress" in text
|
|
327
|
+
|
|
328
|
+
def test_describe_contains_updated_at(self, tracker: MoodTracker) -> None:
|
|
329
|
+
"""describe() includes the updated_at timestamp."""
|
|
330
|
+
tracker.update(messages=5, responses=5, errors=0)
|
|
331
|
+
text = tracker.describe()
|
|
332
|
+
assert "Updated" in text
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
# ---------------------------------------------------------------------------
|
|
336
|
+
# Thread safety
|
|
337
|
+
# ---------------------------------------------------------------------------
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
class TestThreadSafety:
|
|
341
|
+
"""Tests for concurrent MoodTracker access."""
|
|
342
|
+
|
|
343
|
+
def test_concurrent_updates_are_safe(self, tmp_path: Path) -> None:
|
|
344
|
+
"""Concurrent update() calls do not raise exceptions."""
|
|
345
|
+
tracker = MoodTracker(home=tmp_path)
|
|
346
|
+
errors: list[Exception] = []
|
|
347
|
+
|
|
348
|
+
def _work(i: int) -> None:
|
|
349
|
+
try:
|
|
350
|
+
tracker.update(messages=i + 1, responses=i, errors=0)
|
|
351
|
+
except Exception as exc:
|
|
352
|
+
errors.append(exc)
|
|
353
|
+
|
|
354
|
+
threads = [threading.Thread(target=_work, args=(i,)) for i in range(20)]
|
|
355
|
+
for t in threads:
|
|
356
|
+
t.start()
|
|
357
|
+
for t in threads:
|
|
358
|
+
t.join()
|
|
359
|
+
|
|
360
|
+
assert errors == [], f"Unexpected errors: {errors}"
|
|
361
|
+
# Snapshot must still be a valid MoodSnapshot
|
|
362
|
+
snap = tracker.snapshot
|
|
363
|
+
assert isinstance(snap, MoodSnapshot)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
# ---------------------------------------------------------------------------
|
|
367
|
+
# MoodSnapshot model
|
|
368
|
+
# ---------------------------------------------------------------------------
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
class TestMoodSnapshot:
|
|
372
|
+
"""Tests for MoodSnapshot model."""
|
|
373
|
+
|
|
374
|
+
def test_defaults_are_neutral(self) -> None:
|
|
375
|
+
"""Default snapshot is neutral / quiet / calm."""
|
|
376
|
+
snap = MoodSnapshot()
|
|
377
|
+
assert snap.summary == "neutral"
|
|
378
|
+
assert snap.success_mood == "neutral"
|
|
379
|
+
assert snap.social_mood == "quiet"
|
|
380
|
+
assert snap.stress_mood == "calm"
|
|
381
|
+
|
|
382
|
+
def test_json_serializable(self) -> None:
|
|
383
|
+
"""MoodSnapshot serializes to valid JSON."""
|
|
384
|
+
snap = MoodSnapshot(
|
|
385
|
+
messages_processed=5,
|
|
386
|
+
responses_sent=5,
|
|
387
|
+
errors=0,
|
|
388
|
+
summary="happy",
|
|
389
|
+
updated_at="2026-03-02T12:00:00+00:00",
|
|
390
|
+
)
|
|
391
|
+
data = snap.model_dump_json()
|
|
392
|
+
parsed = json.loads(data)
|
|
393
|
+
assert parsed["summary"] == "happy"
|
|
394
|
+
assert parsed["messages_processed"] == 5
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"""Tests for multi-agent daemon isolation.
|
|
2
|
+
|
|
3
|
+
Covers:
|
|
4
|
+
- Per-agent home directory resolution (opus → agents/opus/, jarvis → agents/jarvis/)
|
|
5
|
+
- Per-agent port assignment (opus=7777, jarvis=7778, unknown → next available)
|
|
6
|
+
- Default (no-agent) mode keeps backward-compatible home and port
|
|
7
|
+
- SKCAPSTONE_AGENT env var propagation
|
|
8
|
+
- DaemonConfig accepts distinct homes and ports for simultaneous agents
|
|
9
|
+
- PID files are isolated per agent home
|
|
10
|
+
- is_running / read_pid are home-scoped (no cross-agent interference)
|
|
11
|
+
- CLI --agent option resolves correct home path
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from unittest.mock import MagicMock, patch
|
|
20
|
+
|
|
21
|
+
import pytest
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
# Helpers
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _make_agent_home(tmp_path: Path, agent: str) -> Path:
|
|
30
|
+
"""Create a minimal agent home inside tmp_path/agents/<agent>/."""
|
|
31
|
+
home = tmp_path / "agents" / agent
|
|
32
|
+
home.mkdir(parents=True)
|
|
33
|
+
return home
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
# 1. _resolve_agent_home — home directory isolation
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class TestResolveAgentHome:
|
|
42
|
+
def test_named_agent_uses_agents_subdir(self, tmp_path: Path):
|
|
43
|
+
"""--agent opus → ~/.skcapstone/agents/opus/"""
|
|
44
|
+
from skcapstone.cli.daemon import _resolve_agent_home
|
|
45
|
+
|
|
46
|
+
with patch("skcapstone.cli.daemon.SKCAPSTONE_ROOT", str(tmp_path)):
|
|
47
|
+
result = _resolve_agent_home("opus", str(tmp_path))
|
|
48
|
+
|
|
49
|
+
assert result == (tmp_path / "agents" / "opus").expanduser()
|
|
50
|
+
|
|
51
|
+
def test_jarvis_uses_own_subdir(self, tmp_path: Path):
|
|
52
|
+
"""--agent jarvis → ~/.skcapstone/agents/jarvis/"""
|
|
53
|
+
from skcapstone.cli.daemon import _resolve_agent_home
|
|
54
|
+
|
|
55
|
+
with patch("skcapstone.cli.daemon.SKCAPSTONE_ROOT", str(tmp_path)):
|
|
56
|
+
result = _resolve_agent_home("jarvis", str(tmp_path))
|
|
57
|
+
|
|
58
|
+
assert result == (tmp_path / "agents" / "jarvis").expanduser()
|
|
59
|
+
|
|
60
|
+
def test_no_agent_uses_home_arg(self, tmp_path: Path):
|
|
61
|
+
"""No --agent flag → use the --home value directly (backward compat)."""
|
|
62
|
+
from skcapstone.cli.daemon import _resolve_agent_home
|
|
63
|
+
|
|
64
|
+
custom_home = str(tmp_path / "custom")
|
|
65
|
+
result = _resolve_agent_home(None, custom_home)
|
|
66
|
+
assert result == Path(custom_home).expanduser()
|
|
67
|
+
|
|
68
|
+
def test_opus_and_jarvis_homes_are_distinct(self, tmp_path: Path):
|
|
69
|
+
"""Opus and Jarvis home paths must not overlap."""
|
|
70
|
+
from skcapstone.cli.daemon import _resolve_agent_home
|
|
71
|
+
|
|
72
|
+
with patch("skcapstone.cli.daemon.SKCAPSTONE_ROOT", str(tmp_path)):
|
|
73
|
+
opus_home = _resolve_agent_home("opus", str(tmp_path))
|
|
74
|
+
jarvis_home = _resolve_agent_home("jarvis", str(tmp_path))
|
|
75
|
+
|
|
76
|
+
assert opus_home != jarvis_home
|
|
77
|
+
assert "opus" in str(opus_home)
|
|
78
|
+
assert "jarvis" in str(jarvis_home)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
# 2. _resolve_agent_port — port isolation
|
|
83
|
+
# ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class TestResolveAgentPort:
|
|
87
|
+
def test_opus_gets_7777(self):
|
|
88
|
+
"""opus always gets port 7777."""
|
|
89
|
+
from skcapstone.cli.daemon import _resolve_agent_port
|
|
90
|
+
|
|
91
|
+
assert _resolve_agent_port("opus", None) == 7777
|
|
92
|
+
|
|
93
|
+
def test_jarvis_gets_7778(self):
|
|
94
|
+
"""jarvis always gets port 7778."""
|
|
95
|
+
from skcapstone.cli.daemon import _resolve_agent_port
|
|
96
|
+
|
|
97
|
+
assert _resolve_agent_port("jarvis", None) == 7778
|
|
98
|
+
|
|
99
|
+
def test_explicit_port_overrides_agent_default(self):
|
|
100
|
+
"""Explicit --port always wins over the agent default."""
|
|
101
|
+
from skcapstone.cli.daemon import _resolve_agent_port
|
|
102
|
+
|
|
103
|
+
assert _resolve_agent_port("opus", 9999) == 9999
|
|
104
|
+
assert _resolve_agent_port("jarvis", 8000) == 8000
|
|
105
|
+
|
|
106
|
+
def test_no_agent_defaults_to_7777(self):
|
|
107
|
+
"""Single-agent / no-flag mode uses 7777."""
|
|
108
|
+
from skcapstone.cli.daemon import _resolve_agent_port
|
|
109
|
+
|
|
110
|
+
assert _resolve_agent_port(None, None) == 7777
|
|
111
|
+
|
|
112
|
+
def test_unknown_agent_gets_next_port(self):
|
|
113
|
+
"""An agent not in AGENT_PORTS gets max(ports)+1."""
|
|
114
|
+
from skcapstone import AGENT_PORTS
|
|
115
|
+
from skcapstone.cli.daemon import _resolve_agent_port
|
|
116
|
+
|
|
117
|
+
expected = max(AGENT_PORTS.values()) + 1
|
|
118
|
+
result = _resolve_agent_port("brandnew", None)
|
|
119
|
+
assert result == expected
|
|
120
|
+
|
|
121
|
+
def test_opus_and_jarvis_ports_differ(self):
|
|
122
|
+
"""Opus and Jarvis must listen on different ports."""
|
|
123
|
+
from skcapstone.cli.daemon import _resolve_agent_port
|
|
124
|
+
|
|
125
|
+
assert _resolve_agent_port("opus", None) != _resolve_agent_port("jarvis", None)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# ---------------------------------------------------------------------------
|
|
129
|
+
# 3. AGENT_PORTS registry in __init__
|
|
130
|
+
# ---------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class TestAgentPortsRegistry:
|
|
134
|
+
def test_opus_registered(self):
|
|
135
|
+
from skcapstone import AGENT_PORTS
|
|
136
|
+
|
|
137
|
+
assert "opus" in AGENT_PORTS
|
|
138
|
+
assert AGENT_PORTS["opus"] == 7777
|
|
139
|
+
|
|
140
|
+
def test_jarvis_registered(self):
|
|
141
|
+
from skcapstone import AGENT_PORTS
|
|
142
|
+
|
|
143
|
+
assert "jarvis" in AGENT_PORTS
|
|
144
|
+
assert AGENT_PORTS["jarvis"] == 7778
|
|
145
|
+
|
|
146
|
+
def test_all_ports_unique(self):
|
|
147
|
+
from skcapstone import AGENT_PORTS
|
|
148
|
+
|
|
149
|
+
ports = list(AGENT_PORTS.values())
|
|
150
|
+
assert len(ports) == len(set(ports)), "Duplicate ports in AGENT_PORTS"
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# ---------------------------------------------------------------------------
|
|
154
|
+
# 4. PID-file isolation — is_running / read_pid are home-scoped
|
|
155
|
+
# ---------------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class TestPidIsolation:
|
|
159
|
+
def test_pid_file_written_to_agent_home(self, tmp_path: Path):
|
|
160
|
+
"""PID file is created inside the agent's own home directory."""
|
|
161
|
+
from skcapstone.daemon import DaemonConfig, DaemonService
|
|
162
|
+
|
|
163
|
+
opus_home = _make_agent_home(tmp_path, "opus")
|
|
164
|
+
config = DaemonConfig(home=opus_home, port=7777)
|
|
165
|
+
|
|
166
|
+
svc = DaemonService(config)
|
|
167
|
+
# Call _write_pid directly without starting the full daemon.
|
|
168
|
+
svc._write_pid()
|
|
169
|
+
|
|
170
|
+
pid_file = opus_home / "daemon.pid"
|
|
171
|
+
assert pid_file.exists()
|
|
172
|
+
assert int(pid_file.read_text().strip()) == os.getpid()
|
|
173
|
+
|
|
174
|
+
def test_pid_files_are_isolated_between_agents(self, tmp_path: Path):
|
|
175
|
+
"""Writing opus PID does not affect jarvis PID file."""
|
|
176
|
+
from skcapstone.daemon import DaemonConfig, DaemonService, read_pid
|
|
177
|
+
|
|
178
|
+
opus_home = _make_agent_home(tmp_path, "opus")
|
|
179
|
+
jarvis_home = _make_agent_home(tmp_path, "jarvis")
|
|
180
|
+
|
|
181
|
+
opus_svc = DaemonService(DaemonConfig(home=opus_home, port=7777))
|
|
182
|
+
opus_svc._write_pid()
|
|
183
|
+
|
|
184
|
+
# Jarvis home has no PID file → read_pid returns None.
|
|
185
|
+
assert read_pid(jarvis_home) is None
|
|
186
|
+
|
|
187
|
+
def test_is_running_false_without_pid_file(self, tmp_path: Path):
|
|
188
|
+
"""is_running returns False when no PID file exists."""
|
|
189
|
+
from skcapstone.daemon import is_running
|
|
190
|
+
|
|
191
|
+
empty_home = _make_agent_home(tmp_path, "nobody")
|
|
192
|
+
assert is_running(empty_home) is False
|
|
193
|
+
|
|
194
|
+
def test_read_pid_returns_current_pid_after_write(self, tmp_path: Path):
|
|
195
|
+
"""read_pid returns the PID we just wrote."""
|
|
196
|
+
from skcapstone.daemon import DaemonConfig, DaemonService, read_pid
|
|
197
|
+
|
|
198
|
+
home = _make_agent_home(tmp_path, "opus")
|
|
199
|
+
svc = DaemonService(DaemonConfig(home=home, port=7777))
|
|
200
|
+
svc._write_pid()
|
|
201
|
+
|
|
202
|
+
assert read_pid(home) == os.getpid()
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
# ---------------------------------------------------------------------------
|
|
206
|
+
# 5. DaemonConfig — simultaneous distinct configs
|
|
207
|
+
# ---------------------------------------------------------------------------
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class TestDaemonConfigMultiAgent:
|
|
211
|
+
def test_two_configs_have_distinct_homes_and_ports(self, tmp_path: Path):
|
|
212
|
+
"""Two DaemonConfig instances for opus/jarvis stay isolated."""
|
|
213
|
+
from skcapstone.daemon import DaemonConfig
|
|
214
|
+
|
|
215
|
+
opus_home = _make_agent_home(tmp_path, "opus")
|
|
216
|
+
jarvis_home = _make_agent_home(tmp_path, "jarvis")
|
|
217
|
+
|
|
218
|
+
opus_cfg = DaemonConfig(home=opus_home, port=7777)
|
|
219
|
+
jarvis_cfg = DaemonConfig(home=jarvis_home, port=7778)
|
|
220
|
+
|
|
221
|
+
assert opus_cfg.home != jarvis_cfg.home
|
|
222
|
+
assert opus_cfg.port != jarvis_cfg.port
|
|
223
|
+
assert opus_cfg.port == 7777
|
|
224
|
+
assert jarvis_cfg.port == 7778
|
|
225
|
+
|
|
226
|
+
def test_log_files_are_in_respective_homes(self, tmp_path: Path):
|
|
227
|
+
"""Each agent's log file lives under its own home."""
|
|
228
|
+
from skcapstone.daemon import DaemonConfig
|
|
229
|
+
|
|
230
|
+
opus_home = _make_agent_home(tmp_path, "opus")
|
|
231
|
+
jarvis_home = _make_agent_home(tmp_path, "jarvis")
|
|
232
|
+
|
|
233
|
+
opus_cfg = DaemonConfig(home=opus_home, port=7777)
|
|
234
|
+
jarvis_cfg = DaemonConfig(home=jarvis_home, port=7778)
|
|
235
|
+
|
|
236
|
+
assert str(opus_cfg.log_file).startswith(str(opus_home))
|
|
237
|
+
assert str(jarvis_cfg.log_file).startswith(str(jarvis_home))
|
|
238
|
+
assert opus_cfg.log_file != jarvis_cfg.log_file
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
# ---------------------------------------------------------------------------
|
|
242
|
+
# 6. SKCAPSTONE_AGENT env-var path derivation in __init__
|
|
243
|
+
# ---------------------------------------------------------------------------
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
class TestAgentHomeEnvVar:
|
|
247
|
+
def test_env_var_produces_agents_subdir(self, monkeypatch):
|
|
248
|
+
"""SKCAPSTONE_AGENT=opus → AGENT_HOME includes agents/opus."""
|
|
249
|
+
import importlib
|
|
250
|
+
|
|
251
|
+
monkeypatch.setenv("SKCAPSTONE_AGENT", "opus")
|
|
252
|
+
monkeypatch.setenv("SKCAPSTONE_ROOT", "/tmp/sk")
|
|
253
|
+
|
|
254
|
+
import skcapstone as pkg
|
|
255
|
+
importlib.reload(pkg)
|
|
256
|
+
|
|
257
|
+
assert "agents/opus" in pkg.AGENT_HOME or "agents\\opus" in pkg.AGENT_HOME
|
|
258
|
+
|
|
259
|
+
def test_no_env_var_uses_root_directly(self, monkeypatch):
|
|
260
|
+
"""Without SKCAPSTONE_AGENT, AGENT_HOME == SKCAPSTONE_ROOT."""
|
|
261
|
+
import importlib
|
|
262
|
+
|
|
263
|
+
monkeypatch.delenv("SKCAPSTONE_AGENT", raising=False)
|
|
264
|
+
monkeypatch.setenv("SKCAPSTONE_ROOT", "/tmp/sk")
|
|
265
|
+
|
|
266
|
+
import skcapstone as pkg
|
|
267
|
+
importlib.reload(pkg)
|
|
268
|
+
|
|
269
|
+
assert pkg.AGENT_HOME == pkg.SHARED_ROOT
|