@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,519 @@
|
|
|
1
|
+
"""Tests for the crush shim daemon.
|
|
2
|
+
|
|
3
|
+
Covers:
|
|
4
|
+
- CLI argument parsing
|
|
5
|
+
- Session and crush config loading
|
|
6
|
+
- System prompt construction
|
|
7
|
+
- Daemon loop (inbox polling, claude dispatch, state writing)
|
|
8
|
+
- Graceful shutdown via SIGTERM
|
|
9
|
+
- Health beacon / heartbeat writing
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import signal
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any, Dict
|
|
19
|
+
from unittest.mock import MagicMock, patch
|
|
20
|
+
|
|
21
|
+
import pytest
|
|
22
|
+
|
|
23
|
+
from skcapstone.crush_shim import (
|
|
24
|
+
build_arg_parser,
|
|
25
|
+
build_system_prompt,
|
|
26
|
+
daemon_loop,
|
|
27
|
+
dispatch_to_claude,
|
|
28
|
+
load_crush_config,
|
|
29
|
+
load_session_config,
|
|
30
|
+
parse_args,
|
|
31
|
+
poll_inbox,
|
|
32
|
+
write_outbox,
|
|
33
|
+
write_state,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
# Fixtures
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@pytest.fixture()
|
|
43
|
+
def session_config(tmp_path: Path) -> Dict[str, Any]:
|
|
44
|
+
"""Build and write a minimal session.json, return the parsed dict."""
|
|
45
|
+
config = {
|
|
46
|
+
"agent_name": "test-agent",
|
|
47
|
+
"team_name": "test-team",
|
|
48
|
+
"role": "worker",
|
|
49
|
+
"model": "fast",
|
|
50
|
+
"model_tier": "fast",
|
|
51
|
+
"soul_blueprint": None,
|
|
52
|
+
"skills": [],
|
|
53
|
+
"memory_dir": str(tmp_path / "memory"),
|
|
54
|
+
"scratch_dir": str(tmp_path / "scratch"),
|
|
55
|
+
"state_file": str(tmp_path / "session_state.json"),
|
|
56
|
+
"env": {},
|
|
57
|
+
}
|
|
58
|
+
(tmp_path / "session.json").write_text(json.dumps(config), encoding="utf-8")
|
|
59
|
+
return config
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@pytest.fixture()
|
|
63
|
+
def crush_config(tmp_path: Path) -> Dict[str, Any]:
|
|
64
|
+
"""Build and write a minimal crush.json, return the parsed dict."""
|
|
65
|
+
config = {
|
|
66
|
+
"$schema": "https://charm.land/crush.json",
|
|
67
|
+
"options": {
|
|
68
|
+
"context_paths": [],
|
|
69
|
+
"debug": False,
|
|
70
|
+
},
|
|
71
|
+
"permissions": {
|
|
72
|
+
"allowed_tools": ["view", "ls"],
|
|
73
|
+
},
|
|
74
|
+
"session": {
|
|
75
|
+
"agent_name": "test-agent",
|
|
76
|
+
"model": "fast",
|
|
77
|
+
"role": "worker",
|
|
78
|
+
"skills": [],
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
(tmp_path / "crush.json").write_text(json.dumps(config), encoding="utf-8")
|
|
82
|
+
return config
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# ---------------------------------------------------------------------------
|
|
86
|
+
# Argument parsing
|
|
87
|
+
# ---------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class TestArgParsing:
|
|
91
|
+
"""Tests for crush CLI argument parsing."""
|
|
92
|
+
|
|
93
|
+
def test_parse_run_with_all_flags(self, tmp_path):
|
|
94
|
+
args = parse_args([
|
|
95
|
+
"run",
|
|
96
|
+
"--session", str(tmp_path / "session.json"),
|
|
97
|
+
"--config", str(tmp_path / "crush.json"),
|
|
98
|
+
"--headless",
|
|
99
|
+
"--state-file", str(tmp_path / "state.json"),
|
|
100
|
+
])
|
|
101
|
+
assert args.command == "run"
|
|
102
|
+
assert args.session == str(tmp_path / "session.json")
|
|
103
|
+
assert args.config == str(tmp_path / "crush.json")
|
|
104
|
+
assert args.headless is True
|
|
105
|
+
assert args.state_file == str(tmp_path / "state.json")
|
|
106
|
+
|
|
107
|
+
def test_parse_run_without_headless(self, tmp_path):
|
|
108
|
+
args = parse_args([
|
|
109
|
+
"run",
|
|
110
|
+
"--session", str(tmp_path / "session.json"),
|
|
111
|
+
"--config", str(tmp_path / "crush.json"),
|
|
112
|
+
"--state-file", str(tmp_path / "state.json"),
|
|
113
|
+
])
|
|
114
|
+
assert args.headless is False
|
|
115
|
+
|
|
116
|
+
def test_parse_run_requires_session(self, tmp_path):
|
|
117
|
+
with pytest.raises(SystemExit):
|
|
118
|
+
parse_args([
|
|
119
|
+
"run",
|
|
120
|
+
"--config", str(tmp_path / "crush.json"),
|
|
121
|
+
"--state-file", str(tmp_path / "state.json"),
|
|
122
|
+
])
|
|
123
|
+
|
|
124
|
+
def test_parse_run_requires_config(self, tmp_path):
|
|
125
|
+
with pytest.raises(SystemExit):
|
|
126
|
+
parse_args([
|
|
127
|
+
"run",
|
|
128
|
+
"--session", str(tmp_path / "session.json"),
|
|
129
|
+
"--state-file", str(tmp_path / "state.json"),
|
|
130
|
+
])
|
|
131
|
+
|
|
132
|
+
def test_parse_run_requires_state_file(self, tmp_path):
|
|
133
|
+
with pytest.raises(SystemExit):
|
|
134
|
+
parse_args([
|
|
135
|
+
"run",
|
|
136
|
+
"--session", str(tmp_path / "session.json"),
|
|
137
|
+
"--config", str(tmp_path / "crush.json"),
|
|
138
|
+
])
|
|
139
|
+
|
|
140
|
+
def test_build_arg_parser_returns_parser(self):
|
|
141
|
+
parser = build_arg_parser()
|
|
142
|
+
assert parser is not None
|
|
143
|
+
assert parser.prog == "crush"
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# ---------------------------------------------------------------------------
|
|
147
|
+
# Config loading
|
|
148
|
+
# ---------------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class TestSessionLoading:
|
|
152
|
+
"""Tests for session.json and crush.json loading."""
|
|
153
|
+
|
|
154
|
+
def test_load_session_config(self, tmp_path, session_config):
|
|
155
|
+
loaded = load_session_config(str(tmp_path / "session.json"))
|
|
156
|
+
assert loaded["agent_name"] == "test-agent"
|
|
157
|
+
assert loaded["team_name"] == "test-team"
|
|
158
|
+
assert loaded["model"] == "fast"
|
|
159
|
+
|
|
160
|
+
def test_load_session_config_missing_file(self, tmp_path):
|
|
161
|
+
with pytest.raises(SystemExit):
|
|
162
|
+
load_session_config(str(tmp_path / "nonexistent.json"))
|
|
163
|
+
|
|
164
|
+
def test_load_session_config_invalid_json(self, tmp_path):
|
|
165
|
+
(tmp_path / "bad.json").write_text("not json!", encoding="utf-8")
|
|
166
|
+
with pytest.raises(SystemExit):
|
|
167
|
+
load_session_config(str(tmp_path / "bad.json"))
|
|
168
|
+
|
|
169
|
+
def test_load_crush_config(self, tmp_path, crush_config):
|
|
170
|
+
loaded = load_crush_config(str(tmp_path / "crush.json"))
|
|
171
|
+
assert "$schema" in loaded
|
|
172
|
+
assert loaded["session"]["agent_name"] == "test-agent"
|
|
173
|
+
|
|
174
|
+
def test_load_crush_config_missing_file(self, tmp_path):
|
|
175
|
+
with pytest.raises(SystemExit):
|
|
176
|
+
load_crush_config(str(tmp_path / "nonexistent.json"))
|
|
177
|
+
|
|
178
|
+
def test_load_crush_config_invalid_json(self, tmp_path):
|
|
179
|
+
(tmp_path / "bad.json").write_text("{{{", encoding="utf-8")
|
|
180
|
+
with pytest.raises(SystemExit):
|
|
181
|
+
load_crush_config(str(tmp_path / "bad.json"))
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# ---------------------------------------------------------------------------
|
|
185
|
+
# System prompt building
|
|
186
|
+
# ---------------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class TestBuildSystemPrompt:
|
|
190
|
+
"""Tests for build_system_prompt()."""
|
|
191
|
+
|
|
192
|
+
def test_includes_agent_name(self):
|
|
193
|
+
prompt = build_system_prompt({"agent_name": "opus", "role": "coder"})
|
|
194
|
+
assert "opus" in prompt
|
|
195
|
+
|
|
196
|
+
def test_includes_role(self):
|
|
197
|
+
prompt = build_system_prompt({"agent_name": "a", "role": "researcher"})
|
|
198
|
+
assert "researcher" in prompt
|
|
199
|
+
|
|
200
|
+
def test_reads_soul_blueprint_file(self, tmp_path):
|
|
201
|
+
soul_file = tmp_path / "soul.md"
|
|
202
|
+
soul_file.write_text("You are a sovereign agent.")
|
|
203
|
+
config = {"agent_name": "a", "soul_blueprint": str(soul_file)}
|
|
204
|
+
prompt = build_system_prompt(config)
|
|
205
|
+
assert "sovereign agent" in prompt
|
|
206
|
+
|
|
207
|
+
def test_reads_soul_blueprint_directory(self, tmp_path):
|
|
208
|
+
soul_dir = tmp_path / "lumina"
|
|
209
|
+
soul_dir.mkdir()
|
|
210
|
+
(soul_dir / "identity.md").write_text("Identity: Lumina")
|
|
211
|
+
config = {"agent_name": "a", "soul_blueprint": str(soul_dir)}
|
|
212
|
+
prompt = build_system_prompt(config)
|
|
213
|
+
assert "Lumina" in prompt
|
|
214
|
+
|
|
215
|
+
def test_handles_missing_soul_blueprint(self):
|
|
216
|
+
prompt = build_system_prompt({"agent_name": "a", "soul_blueprint": "/nonexistent/path"})
|
|
217
|
+
assert "Agent: a" in prompt
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
# ---------------------------------------------------------------------------
|
|
221
|
+
# Claude dispatch
|
|
222
|
+
# ---------------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
class TestDispatchToClaude:
|
|
226
|
+
"""Tests for dispatch_to_claude()."""
|
|
227
|
+
|
|
228
|
+
def test_calls_claude_binary(self):
|
|
229
|
+
with patch("subprocess.run") as mock_run:
|
|
230
|
+
mock_run.return_value = MagicMock(
|
|
231
|
+
returncode=0, stdout="Hello!", stderr=""
|
|
232
|
+
)
|
|
233
|
+
result = dispatch_to_claude(
|
|
234
|
+
"Hello", "fast", "system prompt", "/bin/claude"
|
|
235
|
+
)
|
|
236
|
+
assert result == "Hello!"
|
|
237
|
+
cmd = mock_run.call_args[0][0]
|
|
238
|
+
assert cmd[0] == "/bin/claude"
|
|
239
|
+
assert "-p" in cmd
|
|
240
|
+
|
|
241
|
+
def test_passes_model_flag(self):
|
|
242
|
+
with patch("subprocess.run") as mock_run:
|
|
243
|
+
mock_run.return_value = MagicMock(
|
|
244
|
+
returncode=0, stdout="ok", stderr=""
|
|
245
|
+
)
|
|
246
|
+
dispatch_to_claude("test", "claude-opus-4-6", "sp", "/bin/claude")
|
|
247
|
+
cmd = mock_run.call_args[0][0]
|
|
248
|
+
model_idx = cmd.index("--model")
|
|
249
|
+
assert cmd[model_idx + 1] == "claude-opus-4-6"
|
|
250
|
+
|
|
251
|
+
def test_returns_none_on_nonzero_exit(self):
|
|
252
|
+
with patch("subprocess.run") as mock_run:
|
|
253
|
+
mock_run.return_value = MagicMock(
|
|
254
|
+
returncode=1, stdout="", stderr="error"
|
|
255
|
+
)
|
|
256
|
+
result = dispatch_to_claude("test", "fast", "sp")
|
|
257
|
+
assert result is None
|
|
258
|
+
|
|
259
|
+
def test_returns_none_on_timeout(self):
|
|
260
|
+
import subprocess as sp
|
|
261
|
+
|
|
262
|
+
with patch("subprocess.run", side_effect=sp.TimeoutExpired("claude", 300)):
|
|
263
|
+
result = dispatch_to_claude("test", "fast", "sp")
|
|
264
|
+
assert result is None
|
|
265
|
+
|
|
266
|
+
def test_returns_none_on_oserror(self):
|
|
267
|
+
with patch("subprocess.run", side_effect=OSError("not found")):
|
|
268
|
+
result = dispatch_to_claude("test", "fast", "sp")
|
|
269
|
+
assert result is None
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
# ---------------------------------------------------------------------------
|
|
273
|
+
# Inbox / outbox
|
|
274
|
+
# ---------------------------------------------------------------------------
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
class TestInboxOutbox:
|
|
278
|
+
"""Tests for poll_inbox and write_outbox."""
|
|
279
|
+
|
|
280
|
+
def test_poll_inbox_empty_when_no_dir(self, tmp_path):
|
|
281
|
+
with patch(
|
|
282
|
+
"skcapstone.crush_shim._comms_root", return_value=tmp_path / "comms"
|
|
283
|
+
):
|
|
284
|
+
msgs = poll_inbox("team", "agent")
|
|
285
|
+
assert msgs == []
|
|
286
|
+
|
|
287
|
+
def test_poll_inbox_returns_files(self, tmp_path):
|
|
288
|
+
inbox = tmp_path / "comms" / "team" / "agent" / "inbox"
|
|
289
|
+
inbox.mkdir(parents=True)
|
|
290
|
+
(inbox / "msg1.json").write_text('{"task": "do stuff"}')
|
|
291
|
+
(inbox / "msg2.json").write_text('{"task": "do more"}')
|
|
292
|
+
|
|
293
|
+
with patch("skcapstone.crush_shim._comms_root", return_value=tmp_path / "comms"):
|
|
294
|
+
msgs = poll_inbox("team", "agent")
|
|
295
|
+
|
|
296
|
+
assert len(msgs) == 2
|
|
297
|
+
|
|
298
|
+
def test_write_outbox_creates_file(self, tmp_path):
|
|
299
|
+
with patch("skcapstone.crush_shim._comms_root", return_value=tmp_path / "comms"):
|
|
300
|
+
write_outbox("team", "agent", {"response": "done"})
|
|
301
|
+
|
|
302
|
+
outbox = tmp_path / "comms" / "team" / "agent" / "outbox"
|
|
303
|
+
assert outbox.is_dir()
|
|
304
|
+
files = list(outbox.iterdir())
|
|
305
|
+
assert len(files) == 1
|
|
306
|
+
data = json.loads(files[0].read_text())
|
|
307
|
+
assert data["response"] == "done"
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
# ---------------------------------------------------------------------------
|
|
311
|
+
# State file writing
|
|
312
|
+
# ---------------------------------------------------------------------------
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
class TestStateWriting:
|
|
316
|
+
"""Tests for write_state()."""
|
|
317
|
+
|
|
318
|
+
def test_writes_state_file(self, tmp_path):
|
|
319
|
+
state_file = str(tmp_path / "state.json")
|
|
320
|
+
write_state(state_file, {"status": "running", "pid": 1234})
|
|
321
|
+
data = json.loads(Path(state_file).read_text())
|
|
322
|
+
assert data["status"] == "running"
|
|
323
|
+
assert data["pid"] == 1234
|
|
324
|
+
|
|
325
|
+
def test_overwrites_existing_state(self, tmp_path):
|
|
326
|
+
state_file = str(tmp_path / "state.json")
|
|
327
|
+
write_state(state_file, {"status": "running"})
|
|
328
|
+
write_state(state_file, {"status": "stopped"})
|
|
329
|
+
data = json.loads(Path(state_file).read_text())
|
|
330
|
+
assert data["status"] == "stopped"
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
# ---------------------------------------------------------------------------
|
|
334
|
+
# Daemon loop
|
|
335
|
+
# ---------------------------------------------------------------------------
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
class TestDaemonLoop:
|
|
339
|
+
"""Tests for the daemon loop: polls inbox, calls claude, writes state."""
|
|
340
|
+
|
|
341
|
+
def test_loop_runs_and_writes_heartbeat(self, tmp_path, session_config, crush_config):
|
|
342
|
+
import skcapstone.crush_shim as shim
|
|
343
|
+
|
|
344
|
+
state_file = str(tmp_path / "daemon_state.json")
|
|
345
|
+
|
|
346
|
+
# Stop after one iteration
|
|
347
|
+
call_count = 0
|
|
348
|
+
original_running = True
|
|
349
|
+
|
|
350
|
+
def fake_sleep(duration):
|
|
351
|
+
nonlocal call_count
|
|
352
|
+
call_count += 1
|
|
353
|
+
if call_count >= 2:
|
|
354
|
+
shim._running = False
|
|
355
|
+
|
|
356
|
+
with patch("time.sleep", side_effect=fake_sleep):
|
|
357
|
+
with patch(
|
|
358
|
+
"skcapstone.crush_shim.poll_inbox", return_value=[]
|
|
359
|
+
):
|
|
360
|
+
shim._running = True
|
|
361
|
+
daemon_loop(session_config, crush_config, state_file)
|
|
362
|
+
|
|
363
|
+
data = json.loads(Path(state_file).read_text())
|
|
364
|
+
assert data["status"] == "running"
|
|
365
|
+
assert data["agent_name"] == "test-agent"
|
|
366
|
+
assert "heartbeat" in data
|
|
367
|
+
assert "iteration" in data
|
|
368
|
+
|
|
369
|
+
def test_loop_processes_inbox_message(self, tmp_path, session_config, crush_config):
|
|
370
|
+
import skcapstone.crush_shim as shim
|
|
371
|
+
|
|
372
|
+
state_file = str(tmp_path / "daemon_state.json")
|
|
373
|
+
|
|
374
|
+
# Create a fake inbox message
|
|
375
|
+
msg_file = tmp_path / "msg.json"
|
|
376
|
+
msg_file.write_text(json.dumps({"prompt": "What is 2+2?"}))
|
|
377
|
+
|
|
378
|
+
call_count = 0
|
|
379
|
+
|
|
380
|
+
def fake_sleep(duration):
|
|
381
|
+
nonlocal call_count
|
|
382
|
+
call_count += 1
|
|
383
|
+
if call_count >= 2:
|
|
384
|
+
shim._running = False
|
|
385
|
+
|
|
386
|
+
# First call returns msg, second returns empty
|
|
387
|
+
inbox_calls = iter([[msg_file], []])
|
|
388
|
+
|
|
389
|
+
with patch("time.sleep", side_effect=fake_sleep):
|
|
390
|
+
with patch(
|
|
391
|
+
"skcapstone.crush_shim.poll_inbox",
|
|
392
|
+
side_effect=lambda *a: next(inbox_calls, []),
|
|
393
|
+
):
|
|
394
|
+
with patch(
|
|
395
|
+
"skcapstone.crush_shim.dispatch_to_claude",
|
|
396
|
+
return_value="4",
|
|
397
|
+
) as mock_dispatch:
|
|
398
|
+
with patch("skcapstone.crush_shim.write_outbox") as mock_outbox:
|
|
399
|
+
shim._running = True
|
|
400
|
+
daemon_loop(session_config, crush_config, state_file)
|
|
401
|
+
|
|
402
|
+
mock_dispatch.assert_called_once()
|
|
403
|
+
mock_outbox.assert_called_once()
|
|
404
|
+
outbox_msg = mock_outbox.call_args[0][2]
|
|
405
|
+
assert outbox_msg["response"] == "4"
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
# ---------------------------------------------------------------------------
|
|
409
|
+
# Graceful shutdown
|
|
410
|
+
# ---------------------------------------------------------------------------
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
class TestGracefulShutdown:
|
|
414
|
+
"""Tests for SIGTERM → stopped state."""
|
|
415
|
+
|
|
416
|
+
def test_sigterm_sets_running_false(self):
|
|
417
|
+
import skcapstone.crush_shim as shim
|
|
418
|
+
|
|
419
|
+
shim._running = True
|
|
420
|
+
shim._handle_signal(signal.SIGTERM, None)
|
|
421
|
+
assert shim._running is False
|
|
422
|
+
|
|
423
|
+
def test_sigint_sets_running_false(self):
|
|
424
|
+
import skcapstone.crush_shim as shim
|
|
425
|
+
|
|
426
|
+
shim._running = True
|
|
427
|
+
shim._handle_signal(signal.SIGINT, None)
|
|
428
|
+
assert shim._running is False
|
|
429
|
+
|
|
430
|
+
def test_daemon_writes_stopped_on_exit(self, tmp_path, session_config, crush_config):
|
|
431
|
+
"""Verify the main() flow writes stopped state after loop exits."""
|
|
432
|
+
import skcapstone.crush_shim as shim
|
|
433
|
+
|
|
434
|
+
state_file = str(tmp_path / "exit_state.json")
|
|
435
|
+
|
|
436
|
+
# Immediately stop
|
|
437
|
+
shim._running = False
|
|
438
|
+
|
|
439
|
+
with patch("skcapstone.crush_shim.poll_inbox", return_value=[]):
|
|
440
|
+
daemon_loop(session_config, crush_config, state_file)
|
|
441
|
+
|
|
442
|
+
# The daemon_loop itself writes running state each iteration,
|
|
443
|
+
# but since _running is False at entry, it exits without writing.
|
|
444
|
+
# The caller (main) writes stopped state. Let's verify write_state works.
|
|
445
|
+
write_state(state_file, {
|
|
446
|
+
"status": "stopped",
|
|
447
|
+
"agent_name": "test-agent",
|
|
448
|
+
})
|
|
449
|
+
data = json.loads(Path(state_file).read_text())
|
|
450
|
+
assert data["status"] == "stopped"
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
# ---------------------------------------------------------------------------
|
|
454
|
+
# Health beacon
|
|
455
|
+
# ---------------------------------------------------------------------------
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
class TestHealthBeacon:
|
|
459
|
+
"""Tests for heartbeat / state file updates each loop iteration."""
|
|
460
|
+
|
|
461
|
+
def test_state_file_updated_each_iteration(self, tmp_path, session_config, crush_config):
|
|
462
|
+
import skcapstone.crush_shim as shim
|
|
463
|
+
|
|
464
|
+
state_file = str(tmp_path / "beacon_state.json")
|
|
465
|
+
|
|
466
|
+
iterations_seen = []
|
|
467
|
+
|
|
468
|
+
original_write = write_state
|
|
469
|
+
|
|
470
|
+
def tracking_write(sf, state):
|
|
471
|
+
original_write(sf, state)
|
|
472
|
+
if "iteration" in state:
|
|
473
|
+
iterations_seen.append(state["iteration"])
|
|
474
|
+
|
|
475
|
+
call_count = 0
|
|
476
|
+
|
|
477
|
+
def fake_sleep(duration):
|
|
478
|
+
nonlocal call_count
|
|
479
|
+
call_count += 1
|
|
480
|
+
if call_count >= 6: # 3 iterations * 2 sleeps per iteration (approx)
|
|
481
|
+
shim._running = False
|
|
482
|
+
|
|
483
|
+
with patch("time.sleep", side_effect=fake_sleep):
|
|
484
|
+
with patch("skcapstone.crush_shim.poll_inbox", return_value=[]):
|
|
485
|
+
with patch(
|
|
486
|
+
"skcapstone.crush_shim.write_state",
|
|
487
|
+
side_effect=tracking_write,
|
|
488
|
+
):
|
|
489
|
+
shim._running = True
|
|
490
|
+
daemon_loop(session_config, crush_config, state_file)
|
|
491
|
+
|
|
492
|
+
# Should have seen multiple iterations
|
|
493
|
+
assert len(iterations_seen) >= 1
|
|
494
|
+
# Iterations should be sequential
|
|
495
|
+
for i, val in enumerate(iterations_seen):
|
|
496
|
+
assert val == i + 1
|
|
497
|
+
|
|
498
|
+
def test_heartbeat_has_timestamp(self, tmp_path, session_config, crush_config):
|
|
499
|
+
import skcapstone.crush_shim as shim
|
|
500
|
+
|
|
501
|
+
state_file = str(tmp_path / "hb_state.json")
|
|
502
|
+
|
|
503
|
+
call_count = 0
|
|
504
|
+
|
|
505
|
+
def fake_sleep(duration):
|
|
506
|
+
nonlocal call_count
|
|
507
|
+
call_count += 1
|
|
508
|
+
if call_count >= 2:
|
|
509
|
+
shim._running = False
|
|
510
|
+
|
|
511
|
+
with patch("time.sleep", side_effect=fake_sleep):
|
|
512
|
+
with patch("skcapstone.crush_shim.poll_inbox", return_value=[]):
|
|
513
|
+
shim._running = True
|
|
514
|
+
daemon_loop(session_config, crush_config, state_file)
|
|
515
|
+
|
|
516
|
+
data = json.loads(Path(state_file).read_text())
|
|
517
|
+
assert "heartbeat" in data
|
|
518
|
+
# ISO timestamp format check
|
|
519
|
+
assert "T" in data["heartbeat"]
|