@smilintux/skcapstone 0.1.0 → 0.2.4
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 +880 -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 +191 -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 +398 -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 +357 -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 +264 -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,591 @@
|
|
|
1
|
+
"""Tests for LocalProvider — local process-backed agent deployment.
|
|
2
|
+
|
|
3
|
+
All subprocess and filesystem side effects are controlled via tmp_path
|
|
4
|
+
and unittest.mock so no real crush/claude binary is required.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import signal
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Dict
|
|
14
|
+
from unittest.mock import MagicMock, patch
|
|
15
|
+
|
|
16
|
+
import pytest
|
|
17
|
+
|
|
18
|
+
from skcapstone.blueprints.schema import AgentRole, AgentSpec, ModelTier, ResourceSpec
|
|
19
|
+
from skcapstone.providers.local import (
|
|
20
|
+
LocalProvider,
|
|
21
|
+
_build_crush_config,
|
|
22
|
+
_build_session_config,
|
|
23
|
+
_find_crush_binary,
|
|
24
|
+
_is_claude_binary,
|
|
25
|
+
_pid_is_alive,
|
|
26
|
+
_read_pid,
|
|
27
|
+
_read_session_state,
|
|
28
|
+
_resolve_skill_paths,
|
|
29
|
+
_resolve_soul_blueprint_path,
|
|
30
|
+
_session_state_to_agent_status,
|
|
31
|
+
_stub_script,
|
|
32
|
+
_write_session_state,
|
|
33
|
+
)
|
|
34
|
+
from skcapstone.team_engine import AgentStatus
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
# Helpers
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _make_spec(
|
|
43
|
+
role: str = "worker",
|
|
44
|
+
model: str = "fast",
|
|
45
|
+
memory: str = "2g",
|
|
46
|
+
cores: int = 1,
|
|
47
|
+
skills: list | None = None,
|
|
48
|
+
soul_blueprint: str | None = None,
|
|
49
|
+
) -> AgentSpec:
|
|
50
|
+
return AgentSpec(
|
|
51
|
+
role=AgentRole(role),
|
|
52
|
+
model=ModelTier(model),
|
|
53
|
+
resources=ResourceSpec(memory=memory, cores=cores),
|
|
54
|
+
skills=skills or [],
|
|
55
|
+
env={},
|
|
56
|
+
soul_blueprint=soul_blueprint,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
# _find_crush_binary
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class TestFindCrushBinary:
|
|
66
|
+
def test_returns_path_when_found(self):
|
|
67
|
+
with patch("shutil.which", return_value="/usr/bin/crush"):
|
|
68
|
+
result = _find_crush_binary()
|
|
69
|
+
assert result == "/usr/bin/crush"
|
|
70
|
+
|
|
71
|
+
def test_returns_none_when_not_found(self):
|
|
72
|
+
with patch("shutil.which", return_value=None):
|
|
73
|
+
result = _find_crush_binary()
|
|
74
|
+
assert result is None
|
|
75
|
+
|
|
76
|
+
def test_falls_back_to_claude(self):
|
|
77
|
+
def _which(name):
|
|
78
|
+
return "/usr/local/bin/claude" if name == "claude" else None
|
|
79
|
+
|
|
80
|
+
with patch("shutil.which", side_effect=_which):
|
|
81
|
+
result = _find_crush_binary()
|
|
82
|
+
assert result == "/usr/local/bin/claude"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# ---------------------------------------------------------------------------
|
|
86
|
+
# _is_claude_binary
|
|
87
|
+
# ---------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class TestIsCaudeBinary:
|
|
91
|
+
def test_returns_true_for_claude(self):
|
|
92
|
+
assert _is_claude_binary("/usr/local/bin/claude") is True
|
|
93
|
+
|
|
94
|
+
def test_returns_false_for_crush(self):
|
|
95
|
+
assert _is_claude_binary("/usr/bin/crush") is False
|
|
96
|
+
|
|
97
|
+
def test_returns_false_for_python(self):
|
|
98
|
+
assert _is_claude_binary("/usr/bin/python3") is False
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# ---------------------------------------------------------------------------
|
|
102
|
+
# _resolve_soul_blueprint_path
|
|
103
|
+
# ---------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class TestResolveSoulBlueprintPath:
|
|
107
|
+
def test_returns_none_for_empty(self, tmp_path):
|
|
108
|
+
result = _resolve_soul_blueprint_path(None, tmp_path)
|
|
109
|
+
assert result is None
|
|
110
|
+
|
|
111
|
+
def test_returns_absolute_path_unchanged(self, tmp_path):
|
|
112
|
+
bp = tmp_path / "LUMINA.md"
|
|
113
|
+
bp.write_text("soul")
|
|
114
|
+
result = _resolve_soul_blueprint_path(str(bp), tmp_path)
|
|
115
|
+
assert result == str(bp)
|
|
116
|
+
|
|
117
|
+
def test_resolves_via_repo_root_blueprints(self, tmp_path):
|
|
118
|
+
bp_dir = tmp_path / "soul-blueprints" / "blueprints" / "lumina"
|
|
119
|
+
bp_dir.mkdir(parents=True)
|
|
120
|
+
result = _resolve_soul_blueprint_path("lumina", tmp_path, repo_root=tmp_path)
|
|
121
|
+
assert result == str(bp_dir)
|
|
122
|
+
|
|
123
|
+
def test_resolves_relative_to_work_dir(self, tmp_path):
|
|
124
|
+
bp = tmp_path / "my_soul.md"
|
|
125
|
+
bp.write_text("soul")
|
|
126
|
+
result = _resolve_soul_blueprint_path("my_soul.md", tmp_path)
|
|
127
|
+
assert result == str(bp)
|
|
128
|
+
|
|
129
|
+
def test_returns_original_when_unresolvable(self, tmp_path):
|
|
130
|
+
result = _resolve_soul_blueprint_path("ghost_blueprint", tmp_path)
|
|
131
|
+
assert result == "ghost_blueprint"
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# ---------------------------------------------------------------------------
|
|
135
|
+
# _resolve_skill_paths
|
|
136
|
+
# ---------------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class TestResolveSkillPaths:
|
|
140
|
+
def test_keeps_non_existent_as_is(self):
|
|
141
|
+
result = _resolve_skill_paths(["my-skill"])
|
|
142
|
+
assert result == ["my-skill"]
|
|
143
|
+
|
|
144
|
+
def test_resolves_absolute_existing_path(self, tmp_path):
|
|
145
|
+
skill_file = tmp_path / "my_skill.yaml"
|
|
146
|
+
skill_file.write_text("skill: true")
|
|
147
|
+
result = _resolve_skill_paths([str(skill_file)])
|
|
148
|
+
assert result == [str(skill_file)]
|
|
149
|
+
|
|
150
|
+
def test_empty_list_returns_empty(self):
|
|
151
|
+
assert _resolve_skill_paths([]) == []
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# ---------------------------------------------------------------------------
|
|
155
|
+
# _build_session_config
|
|
156
|
+
# ---------------------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class TestBuildSessionConfig:
|
|
160
|
+
def test_returns_required_keys(self, tmp_path):
|
|
161
|
+
spec = _make_spec()
|
|
162
|
+
config = _build_session_config("agent-1", "team-a", spec, tmp_path)
|
|
163
|
+
assert config["agent_name"] == "agent-1"
|
|
164
|
+
assert config["team_name"] == "team-a"
|
|
165
|
+
assert config["role"] == "worker"
|
|
166
|
+
assert "model" in config
|
|
167
|
+
assert "memory_dir" in config
|
|
168
|
+
assert "scratch_dir" in config
|
|
169
|
+
assert "state_file" in config
|
|
170
|
+
|
|
171
|
+
def test_soul_blueprint_resolved(self, tmp_path):
|
|
172
|
+
spec = _make_spec(soul_blueprint=None)
|
|
173
|
+
config = _build_session_config("agent-1", "team", spec, tmp_path)
|
|
174
|
+
assert config["soul_blueprint"] is None
|
|
175
|
+
|
|
176
|
+
def test_model_tier_used_as_fallback(self, tmp_path):
|
|
177
|
+
spec = _make_spec(model="fast")
|
|
178
|
+
with patch(
|
|
179
|
+
"skcapstone.providers.local._resolve_model_via_router",
|
|
180
|
+
return_value="claude-haiku-4-5",
|
|
181
|
+
):
|
|
182
|
+
config = _build_session_config("a", "t", spec, tmp_path)
|
|
183
|
+
assert config["model"] == "claude-haiku-4-5"
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
# ---------------------------------------------------------------------------
|
|
187
|
+
# _build_crush_config
|
|
188
|
+
# ---------------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class TestBuildCrushConfig:
|
|
192
|
+
def test_has_schema_key(self, tmp_path):
|
|
193
|
+
session_cfg = {"agent_name": "a", "soul_blueprint": None, "model": "fast", "role": "worker", "skills": []}
|
|
194
|
+
cfg = _build_crush_config("a", session_cfg, tmp_path)
|
|
195
|
+
assert "$schema" in cfg
|
|
196
|
+
|
|
197
|
+
def test_session_keys_present(self, tmp_path):
|
|
198
|
+
session_cfg = {
|
|
199
|
+
"agent_name": "bob",
|
|
200
|
+
"soul_blueprint": "/blueprints/lumina",
|
|
201
|
+
"model": "code",
|
|
202
|
+
"role": "coder",
|
|
203
|
+
"skills": ["sk1"],
|
|
204
|
+
"memory_dir": str(tmp_path / "memory"),
|
|
205
|
+
"state_file": str(tmp_path / "state.json"),
|
|
206
|
+
}
|
|
207
|
+
cfg = _build_crush_config("bob", session_cfg, tmp_path)
|
|
208
|
+
assert cfg["session"]["agent_name"] == "bob"
|
|
209
|
+
assert cfg["session"]["role"] == "coder"
|
|
210
|
+
assert cfg["session"]["model"] == "code"
|
|
211
|
+
|
|
212
|
+
def test_none_soul_removed_from_context_paths(self, tmp_path):
|
|
213
|
+
session_cfg = {"agent_name": "a", "soul_blueprint": None, "model": "fast", "role": "worker", "skills": []}
|
|
214
|
+
cfg = _build_crush_config("a", session_cfg, tmp_path)
|
|
215
|
+
assert None not in cfg["options"]["context_paths"]
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
# ---------------------------------------------------------------------------
|
|
219
|
+
# Session state helpers
|
|
220
|
+
# ---------------------------------------------------------------------------
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class TestSessionStateHelpers:
|
|
224
|
+
def test_write_and_read_roundtrip(self, tmp_path):
|
|
225
|
+
state = {"status": "running", "pid": 1234}
|
|
226
|
+
_write_session_state(tmp_path, state)
|
|
227
|
+
result = _read_session_state(tmp_path)
|
|
228
|
+
assert result == state
|
|
229
|
+
|
|
230
|
+
def test_read_missing_returns_none(self, tmp_path):
|
|
231
|
+
result = _read_session_state(tmp_path)
|
|
232
|
+
assert result is None
|
|
233
|
+
|
|
234
|
+
def test_read_corrupt_returns_none(self, tmp_path):
|
|
235
|
+
(tmp_path / "session_state.json").write_text("not-json")
|
|
236
|
+
assert _read_session_state(tmp_path) is None
|
|
237
|
+
|
|
238
|
+
def test_read_pid_returns_int(self, tmp_path):
|
|
239
|
+
(tmp_path / "agent.pid").write_text("5678\n")
|
|
240
|
+
assert _read_pid(tmp_path) == 5678
|
|
241
|
+
|
|
242
|
+
def test_read_pid_missing_returns_none(self, tmp_path):
|
|
243
|
+
assert _read_pid(tmp_path) is None
|
|
244
|
+
|
|
245
|
+
def test_read_pid_invalid_returns_none(self, tmp_path):
|
|
246
|
+
(tmp_path / "agent.pid").write_text("notanumber")
|
|
247
|
+
assert _read_pid(tmp_path) is None
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
# ---------------------------------------------------------------------------
|
|
251
|
+
# _pid_is_alive
|
|
252
|
+
# ---------------------------------------------------------------------------
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class TestPidIsAlive:
|
|
256
|
+
def test_alive_process(self):
|
|
257
|
+
# Use os.getpid() — current process is always alive.
|
|
258
|
+
assert _pid_is_alive(os.getpid()) is True
|
|
259
|
+
|
|
260
|
+
def test_dead_process(self):
|
|
261
|
+
with patch("os.kill", side_effect=ProcessLookupError):
|
|
262
|
+
assert _pid_is_alive(99999) is False
|
|
263
|
+
|
|
264
|
+
def test_permission_denied_counts_as_alive(self):
|
|
265
|
+
with patch("os.kill", side_effect=OSError("permission denied")):
|
|
266
|
+
assert _pid_is_alive(1) is True
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
# ---------------------------------------------------------------------------
|
|
270
|
+
# _session_state_to_agent_status
|
|
271
|
+
# ---------------------------------------------------------------------------
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class TestSessionStateToAgentStatus:
|
|
275
|
+
def test_running_status(self):
|
|
276
|
+
with patch("skcapstone.providers.local._pid_is_alive", return_value=True):
|
|
277
|
+
status = _session_state_to_agent_status({"status": "running", "pid": 1}, 1)
|
|
278
|
+
assert status == AgentStatus.RUNNING
|
|
279
|
+
|
|
280
|
+
def test_idle_status(self):
|
|
281
|
+
with patch("skcapstone.providers.local._pid_is_alive", return_value=True):
|
|
282
|
+
status = _session_state_to_agent_status({"status": "idle", "pid": 1}, 1)
|
|
283
|
+
assert status == AgentStatus.RUNNING
|
|
284
|
+
|
|
285
|
+
def test_running_but_dead_pid_is_degraded(self):
|
|
286
|
+
with patch("skcapstone.providers.local._pid_is_alive", return_value=False):
|
|
287
|
+
status = _session_state_to_agent_status({"status": "running", "pid": 1}, 1)
|
|
288
|
+
assert status == AgentStatus.DEGRADED
|
|
289
|
+
|
|
290
|
+
def test_error_status_is_degraded(self):
|
|
291
|
+
status = _session_state_to_agent_status({"status": "error"}, None)
|
|
292
|
+
assert status == AgentStatus.DEGRADED
|
|
293
|
+
|
|
294
|
+
def test_stopped_status(self):
|
|
295
|
+
status = _session_state_to_agent_status({"status": "stopped"}, None)
|
|
296
|
+
assert status == AgentStatus.STOPPED
|
|
297
|
+
|
|
298
|
+
def test_unknown_status_is_degraded(self):
|
|
299
|
+
status = _session_state_to_agent_status({"status": "whatever"}, None)
|
|
300
|
+
assert status == AgentStatus.DEGRADED
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
# ---------------------------------------------------------------------------
|
|
304
|
+
# LocalProvider.provision
|
|
305
|
+
# ---------------------------------------------------------------------------
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
class TestLocalProviderProvision:
|
|
309
|
+
@pytest.fixture()
|
|
310
|
+
def provider(self, tmp_path):
|
|
311
|
+
return LocalProvider(
|
|
312
|
+
home=tmp_path / "home",
|
|
313
|
+
work_dir=tmp_path / "agents",
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
def test_creates_agent_directories(self, provider, tmp_path):
|
|
317
|
+
spec = _make_spec()
|
|
318
|
+
result = provider.provision("agent-1", spec, "team-a")
|
|
319
|
+
agent_dir = Path(result["work_dir"])
|
|
320
|
+
assert (agent_dir / "memory").is_dir()
|
|
321
|
+
assert (agent_dir / "scratch").is_dir()
|
|
322
|
+
|
|
323
|
+
def test_writes_session_json(self, provider):
|
|
324
|
+
spec = _make_spec()
|
|
325
|
+
result = provider.provision("agent-1", spec, "team-a")
|
|
326
|
+
session_file = Path(result["work_dir"]) / "session.json"
|
|
327
|
+
assert session_file.exists()
|
|
328
|
+
data = json.loads(session_file.read_text())
|
|
329
|
+
assert data["agent_name"] == "agent-1"
|
|
330
|
+
assert data["team_name"] == "team-a"
|
|
331
|
+
|
|
332
|
+
def test_host_is_localhost(self, provider):
|
|
333
|
+
result = provider.provision("a", _make_spec(), "t")
|
|
334
|
+
assert result["host"] == "localhost"
|
|
335
|
+
|
|
336
|
+
def test_returns_session_config(self, provider):
|
|
337
|
+
result = provider.provision("a", _make_spec(), "t")
|
|
338
|
+
assert "session_config" in result
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
# ---------------------------------------------------------------------------
|
|
342
|
+
# LocalProvider.configure
|
|
343
|
+
# ---------------------------------------------------------------------------
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
class TestLocalProviderConfigure:
|
|
347
|
+
@pytest.fixture()
|
|
348
|
+
def provider(self, tmp_path):
|
|
349
|
+
return LocalProvider(
|
|
350
|
+
home=tmp_path / "home",
|
|
351
|
+
work_dir=tmp_path / "agents",
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
def test_writes_crush_json(self, provider):
|
|
355
|
+
spec = _make_spec()
|
|
356
|
+
prov = provider.provision("agent-cfg", spec, "team")
|
|
357
|
+
ok = provider.configure("agent-cfg", spec, prov)
|
|
358
|
+
assert ok is True
|
|
359
|
+
crush_file = Path(prov["work_dir"]) / "crush.json"
|
|
360
|
+
assert crush_file.exists()
|
|
361
|
+
|
|
362
|
+
def test_configure_missing_work_dir_returns_false(self, provider):
|
|
363
|
+
result = provider.configure("a", _make_spec(), {})
|
|
364
|
+
assert result is False
|
|
365
|
+
|
|
366
|
+
def test_configure_invalid_work_dir_returns_false(self, provider):
|
|
367
|
+
result = provider.configure("a", _make_spec(), {"work_dir": ""})
|
|
368
|
+
assert result is False
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
# ---------------------------------------------------------------------------
|
|
372
|
+
# LocalProvider.start — stub path (no crush binary)
|
|
373
|
+
# ---------------------------------------------------------------------------
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
class TestLocalProviderStart:
|
|
377
|
+
@pytest.fixture()
|
|
378
|
+
def provider(self, tmp_path):
|
|
379
|
+
return LocalProvider(
|
|
380
|
+
home=tmp_path / "home",
|
|
381
|
+
work_dir=tmp_path / "agents",
|
|
382
|
+
crush_binary=None,
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
def test_start_missing_work_dir_returns_false(self, provider):
|
|
386
|
+
result = provider.start("a", {})
|
|
387
|
+
assert result is False
|
|
388
|
+
|
|
389
|
+
def test_start_with_stub_when_no_binary(self, provider):
|
|
390
|
+
spec = _make_spec()
|
|
391
|
+
prov = provider.provision("agent-stub", spec, "team")
|
|
392
|
+
with patch("skcapstone.providers.local._find_crush_binary", return_value=None):
|
|
393
|
+
ok = provider.start("agent-stub", prov)
|
|
394
|
+
assert ok is True
|
|
395
|
+
assert prov.get("pid") is not None
|
|
396
|
+
# Clean up the spawned stub
|
|
397
|
+
try:
|
|
398
|
+
os.kill(prov["pid"], signal.SIGTERM)
|
|
399
|
+
except OSError:
|
|
400
|
+
pass
|
|
401
|
+
|
|
402
|
+
def test_start_with_crush_binary(self, provider, tmp_path):
|
|
403
|
+
spec = _make_spec()
|
|
404
|
+
prov = provider.provision("agent-crush", spec, "team")
|
|
405
|
+
mock_proc = MagicMock()
|
|
406
|
+
mock_proc.pid = 12345
|
|
407
|
+
with patch("subprocess.Popen", return_value=mock_proc) as mock_popen:
|
|
408
|
+
with patch(
|
|
409
|
+
"skcapstone.providers.local._find_crush_binary",
|
|
410
|
+
return_value="/usr/bin/crush",
|
|
411
|
+
):
|
|
412
|
+
ok = provider.start("agent-crush", prov)
|
|
413
|
+
assert ok is True
|
|
414
|
+
assert prov["pid"] == 12345
|
|
415
|
+
mock_popen.assert_called_once()
|
|
416
|
+
|
|
417
|
+
def test_start_with_claude_binary(self, provider, tmp_path):
|
|
418
|
+
spec = _make_spec()
|
|
419
|
+
prov = provider.provision("agent-claude", spec, "team")
|
|
420
|
+
mock_proc = MagicMock()
|
|
421
|
+
mock_proc.pid = 99999
|
|
422
|
+
with patch("subprocess.Popen", return_value=mock_proc):
|
|
423
|
+
with patch(
|
|
424
|
+
"skcapstone.providers.local._find_crush_binary",
|
|
425
|
+
return_value="/usr/bin/claude",
|
|
426
|
+
):
|
|
427
|
+
ok = provider.start("agent-claude", prov)
|
|
428
|
+
assert ok is True
|
|
429
|
+
assert prov["pid"] == 99999
|
|
430
|
+
|
|
431
|
+
def test_start_crush_oserror_returns_false(self, provider, tmp_path):
|
|
432
|
+
spec = _make_spec()
|
|
433
|
+
prov = provider.provision("agent-err", spec, "team")
|
|
434
|
+
with patch("subprocess.Popen", side_effect=OSError("no such file")):
|
|
435
|
+
with patch(
|
|
436
|
+
"skcapstone.providers.local._find_crush_binary",
|
|
437
|
+
return_value="/usr/bin/crush",
|
|
438
|
+
):
|
|
439
|
+
ok = provider.start("agent-err", prov)
|
|
440
|
+
assert ok is False
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
# ---------------------------------------------------------------------------
|
|
444
|
+
# LocalProvider.stop
|
|
445
|
+
# ---------------------------------------------------------------------------
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
class TestLocalProviderStop:
|
|
449
|
+
@pytest.fixture()
|
|
450
|
+
def provider(self, tmp_path):
|
|
451
|
+
return LocalProvider(
|
|
452
|
+
home=tmp_path / "home",
|
|
453
|
+
work_dir=tmp_path / "agents",
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
def test_stop_no_pid_returns_true(self, provider, tmp_path):
|
|
457
|
+
ok = provider.stop("a", {"work_dir": str(tmp_path)})
|
|
458
|
+
assert ok is True
|
|
459
|
+
|
|
460
|
+
def test_stop_already_dead_returns_true(self, provider, tmp_path):
|
|
461
|
+
with patch("skcapstone.providers.local._pid_is_alive", return_value=False):
|
|
462
|
+
ok = provider.stop("a", {"pid": 12345, "work_dir": str(tmp_path)})
|
|
463
|
+
assert ok is True
|
|
464
|
+
|
|
465
|
+
def test_stop_sends_sigterm(self, provider, tmp_path):
|
|
466
|
+
kill_calls = []
|
|
467
|
+
|
|
468
|
+
def fake_kill(pid, sig):
|
|
469
|
+
kill_calls.append((pid, sig))
|
|
470
|
+
|
|
471
|
+
# First call: process is alive (don't early-exit), then dead (exit wait loop)
|
|
472
|
+
alive_iter = iter([True, False])
|
|
473
|
+
|
|
474
|
+
with patch("os.kill", side_effect=fake_kill):
|
|
475
|
+
with patch(
|
|
476
|
+
"skcapstone.providers.local._pid_is_alive",
|
|
477
|
+
side_effect=lambda p: next(alive_iter, False),
|
|
478
|
+
):
|
|
479
|
+
with patch("time.sleep"):
|
|
480
|
+
ok = provider.stop("a", {"pid": 12345, "work_dir": str(tmp_path)})
|
|
481
|
+
assert any(sig == signal.SIGTERM for _, sig in kill_calls)
|
|
482
|
+
assert ok is True
|
|
483
|
+
|
|
484
|
+
def test_stop_process_lookup_error_returns_true(self, provider, tmp_path):
|
|
485
|
+
with patch("os.kill", side_effect=ProcessLookupError):
|
|
486
|
+
with patch("skcapstone.providers.local._pid_is_alive", return_value=True):
|
|
487
|
+
ok = provider.stop("a", {"pid": 12345, "work_dir": str(tmp_path)})
|
|
488
|
+
assert ok is True
|
|
489
|
+
|
|
490
|
+
def test_stop_sigterm_oserror_returns_false(self, provider, tmp_path):
|
|
491
|
+
with patch("os.kill", side_effect=OSError("permission denied")):
|
|
492
|
+
with patch("skcapstone.providers.local._pid_is_alive", return_value=True):
|
|
493
|
+
ok = provider.stop("a", {"pid": 12345, "work_dir": str(tmp_path)})
|
|
494
|
+
assert ok is False
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
# ---------------------------------------------------------------------------
|
|
498
|
+
# LocalProvider.destroy
|
|
499
|
+
# ---------------------------------------------------------------------------
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
class TestLocalProviderDestroy:
|
|
503
|
+
def test_destroy_removes_directory(self, tmp_path):
|
|
504
|
+
provider = LocalProvider(
|
|
505
|
+
home=tmp_path / "home",
|
|
506
|
+
work_dir=tmp_path / "agents",
|
|
507
|
+
)
|
|
508
|
+
spec = _make_spec()
|
|
509
|
+
prov = provider.provision("agent-del", spec, "team")
|
|
510
|
+
agent_dir = Path(prov["work_dir"])
|
|
511
|
+
assert agent_dir.exists()
|
|
512
|
+
|
|
513
|
+
with patch.object(provider, "stop", return_value=True):
|
|
514
|
+
ok = provider.destroy("agent-del", prov)
|
|
515
|
+
assert ok is True
|
|
516
|
+
assert not agent_dir.exists()
|
|
517
|
+
|
|
518
|
+
def test_destroy_missing_work_dir_returns_true(self, tmp_path):
|
|
519
|
+
provider = LocalProvider(
|
|
520
|
+
home=tmp_path / "home",
|
|
521
|
+
work_dir=tmp_path / "agents",
|
|
522
|
+
)
|
|
523
|
+
with patch.object(provider, "stop", return_value=True):
|
|
524
|
+
ok = provider.destroy("a", {})
|
|
525
|
+
assert ok is True
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
# ---------------------------------------------------------------------------
|
|
529
|
+
# LocalProvider.health_check
|
|
530
|
+
# ---------------------------------------------------------------------------
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
class TestLocalProviderHealthCheck:
|
|
534
|
+
@pytest.fixture()
|
|
535
|
+
def provider(self, tmp_path):
|
|
536
|
+
return LocalProvider(
|
|
537
|
+
home=tmp_path / "home",
|
|
538
|
+
work_dir=tmp_path / "agents",
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
def test_health_running_from_state_file(self, provider, tmp_path):
|
|
542
|
+
work_dir = tmp_path / "agent-hc"
|
|
543
|
+
work_dir.mkdir()
|
|
544
|
+
_write_session_state(work_dir, {"status": "running", "pid": 1})
|
|
545
|
+
with patch("skcapstone.providers.local._pid_is_alive", return_value=True):
|
|
546
|
+
status = provider.health_check("a", {"work_dir": str(work_dir), "pid": 1})
|
|
547
|
+
assert status == AgentStatus.RUNNING
|
|
548
|
+
|
|
549
|
+
def test_health_stopped_from_state_file(self, provider, tmp_path):
|
|
550
|
+
work_dir = tmp_path / "agent-stopped"
|
|
551
|
+
work_dir.mkdir()
|
|
552
|
+
_write_session_state(work_dir, {"status": "stopped"})
|
|
553
|
+
status = provider.health_check("a", {"work_dir": str(work_dir)})
|
|
554
|
+
assert status == AgentStatus.STOPPED
|
|
555
|
+
|
|
556
|
+
def test_health_no_work_dir_no_pid_returns_stopped(self, provider):
|
|
557
|
+
status = provider.health_check("a", {})
|
|
558
|
+
assert status == AgentStatus.STOPPED
|
|
559
|
+
|
|
560
|
+
def test_health_pid_alive_no_state_file(self, provider, tmp_path):
|
|
561
|
+
work_dir = tmp_path / "no-state"
|
|
562
|
+
work_dir.mkdir()
|
|
563
|
+
with patch("skcapstone.providers.local._pid_is_alive", return_value=True):
|
|
564
|
+
status = provider.health_check("a", {"work_dir": str(work_dir), "pid": 1})
|
|
565
|
+
assert status == AgentStatus.RUNNING
|
|
566
|
+
|
|
567
|
+
def test_health_pid_dead_no_state_file(self, provider, tmp_path):
|
|
568
|
+
work_dir = tmp_path / "dead-pid"
|
|
569
|
+
work_dir.mkdir()
|
|
570
|
+
with patch("skcapstone.providers.local._pid_is_alive", return_value=False):
|
|
571
|
+
status = provider.health_check("a", {"work_dir": str(work_dir), "pid": 1})
|
|
572
|
+
assert status == AgentStatus.STOPPED
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
# ---------------------------------------------------------------------------
|
|
576
|
+
# _stub_script
|
|
577
|
+
# ---------------------------------------------------------------------------
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
class TestStubScript:
|
|
581
|
+
def test_contains_agent_name(self):
|
|
582
|
+
script = _stub_script("my-agent", "/tmp/state.json")
|
|
583
|
+
assert "my-agent" in script
|
|
584
|
+
|
|
585
|
+
def test_contains_state_file(self):
|
|
586
|
+
script = _stub_script("a", "/tmp/agent_state.json")
|
|
587
|
+
assert "/tmp/agent_state.json" in script
|
|
588
|
+
|
|
589
|
+
def test_contains_signal_handling(self):
|
|
590
|
+
script = _stub_script("a", "/tmp/s.json")
|
|
591
|
+
assert "SIGTERM" in script
|