@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,1283 @@
|
|
|
1
|
+
"""Tests for LocalProvider agent runtime lifecycle.
|
|
2
|
+
|
|
3
|
+
Covers:
|
|
4
|
+
- Path resolution helpers (soul blueprints, skills)
|
|
5
|
+
- Session and crush config builders
|
|
6
|
+
- provision(), configure(), start(), stop(), health_check(), destroy()
|
|
7
|
+
- Crush binary launch path vs. stub fallback
|
|
8
|
+
- Session state → AgentStatus mapping
|
|
9
|
+
- Edge cases and failure scenarios
|
|
10
|
+
|
|
11
|
+
All subprocess calls are mocked so no real processes or binaries are required.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import signal
|
|
19
|
+
import subprocess
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any, Dict
|
|
22
|
+
from unittest.mock import MagicMock, call, patch
|
|
23
|
+
|
|
24
|
+
import pytest
|
|
25
|
+
|
|
26
|
+
from skcapstone.blueprints.schema import (
|
|
27
|
+
AgentRole,
|
|
28
|
+
AgentSpec,
|
|
29
|
+
ModelTier,
|
|
30
|
+
ProviderType,
|
|
31
|
+
ResourceSpec,
|
|
32
|
+
)
|
|
33
|
+
from skcapstone.providers.local import (
|
|
34
|
+
LocalProvider,
|
|
35
|
+
_SESSION_STATE_FILE,
|
|
36
|
+
_PID_FILE,
|
|
37
|
+
_SESSION_CONFIG_FILE,
|
|
38
|
+
_CRUSH_CONFIG_FILE,
|
|
39
|
+
_STATE_RUNNING,
|
|
40
|
+
_STATE_STOPPED,
|
|
41
|
+
_STATE_ERROR,
|
|
42
|
+
_STATE_IDLE,
|
|
43
|
+
_build_crush_config,
|
|
44
|
+
_build_session_config,
|
|
45
|
+
_find_crush_binary,
|
|
46
|
+
_is_claude_binary,
|
|
47
|
+
_pid_is_alive,
|
|
48
|
+
_read_pid,
|
|
49
|
+
_read_session_state,
|
|
50
|
+
_resolve_skill_paths,
|
|
51
|
+
_resolve_soul_blueprint_path,
|
|
52
|
+
_session_state_to_agent_status,
|
|
53
|
+
_stub_script,
|
|
54
|
+
_write_session_state,
|
|
55
|
+
)
|
|
56
|
+
from skcapstone.team_engine import AgentStatus
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
# Fixtures and helpers
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _make_spec(
|
|
65
|
+
role: str = "worker",
|
|
66
|
+
model: str = "fast",
|
|
67
|
+
soul_blueprint: str | None = None,
|
|
68
|
+
skills: list | None = None,
|
|
69
|
+
env: dict | None = None,
|
|
70
|
+
model_name: str | None = None,
|
|
71
|
+
) -> AgentSpec:
|
|
72
|
+
"""Build a minimal AgentSpec for testing."""
|
|
73
|
+
return AgentSpec(
|
|
74
|
+
role=AgentRole(role),
|
|
75
|
+
model=ModelTier(model),
|
|
76
|
+
model_name=model_name,
|
|
77
|
+
resources=ResourceSpec(),
|
|
78
|
+
soul_blueprint=soul_blueprint,
|
|
79
|
+
skills=skills or [],
|
|
80
|
+
env=env or {},
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _provision_result(
|
|
85
|
+
work_dir: str,
|
|
86
|
+
pid: int | None = None,
|
|
87
|
+
session_config: Dict[str, Any] | None = None,
|
|
88
|
+
) -> Dict[str, Any]:
|
|
89
|
+
"""Build a typical provision_result dict."""
|
|
90
|
+
result: Dict[str, Any] = {"host": "localhost", "work_dir": work_dir}
|
|
91
|
+
if pid is not None:
|
|
92
|
+
result["pid"] = pid
|
|
93
|
+
if session_config is not None:
|
|
94
|
+
result["session_config"] = session_config
|
|
95
|
+
return result
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@pytest.fixture()
|
|
99
|
+
def provider(tmp_path: Path) -> LocalProvider:
|
|
100
|
+
"""LocalProvider with tmp_path as both home and work_dir."""
|
|
101
|
+
return LocalProvider(
|
|
102
|
+
home=tmp_path / "home",
|
|
103
|
+
work_dir=tmp_path / "agents",
|
|
104
|
+
repo_root=tmp_path / "repo",
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@pytest.fixture()
|
|
109
|
+
def agent_dir(tmp_path: Path) -> Path:
|
|
110
|
+
"""Create and return a fake agent working directory."""
|
|
111
|
+
d = tmp_path / "agents" / "test-agent"
|
|
112
|
+
d.mkdir(parents=True)
|
|
113
|
+
(d / "memory").mkdir()
|
|
114
|
+
(d / "scratch").mkdir()
|
|
115
|
+
return d
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# ---------------------------------------------------------------------------
|
|
119
|
+
# _find_crush_binary
|
|
120
|
+
# ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class TestFindCrushBinary:
|
|
124
|
+
"""Tests for _find_crush_binary helper."""
|
|
125
|
+
|
|
126
|
+
def test_returns_none_when_not_on_path(self):
|
|
127
|
+
with patch("shutil.which", return_value=None):
|
|
128
|
+
assert _find_crush_binary() is None
|
|
129
|
+
|
|
130
|
+
def test_returns_crush_path_when_found(self):
|
|
131
|
+
with patch("shutil.which", side_effect=lambda x: "/usr/bin/crush" if x == "crush" else None):
|
|
132
|
+
result = _find_crush_binary()
|
|
133
|
+
assert result == "/usr/bin/crush"
|
|
134
|
+
|
|
135
|
+
def test_returns_claude_path_as_fallback_when_crush_absent(self):
|
|
136
|
+
def _which(name):
|
|
137
|
+
if name == "claude":
|
|
138
|
+
return "/bin/claude"
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
with patch("shutil.which", side_effect=_which):
|
|
142
|
+
result = _find_crush_binary()
|
|
143
|
+
assert result == "/bin/claude"
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# ---------------------------------------------------------------------------
|
|
147
|
+
# _resolve_soul_blueprint_path
|
|
148
|
+
# ---------------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class TestResolveSoulBlueprintPath:
|
|
152
|
+
"""Tests for _resolve_soul_blueprint_path helper."""
|
|
153
|
+
|
|
154
|
+
def test_returns_none_for_none_input(self, tmp_path):
|
|
155
|
+
assert _resolve_soul_blueprint_path(None, tmp_path) is None
|
|
156
|
+
|
|
157
|
+
def test_returns_absolute_path_unchanged_when_exists(self, tmp_path):
|
|
158
|
+
soul = tmp_path / "lumina.yaml"
|
|
159
|
+
soul.write_text("soul: lumina")
|
|
160
|
+
result = _resolve_soul_blueprint_path(str(soul), tmp_path)
|
|
161
|
+
assert result == str(soul)
|
|
162
|
+
|
|
163
|
+
def test_resolves_via_soul_blueprints_dir(self, tmp_path):
|
|
164
|
+
blueprint_dir = tmp_path / "soul-blueprints" / "blueprints" / "lumina"
|
|
165
|
+
blueprint_dir.mkdir(parents=True)
|
|
166
|
+
result = _resolve_soul_blueprint_path("lumina", tmp_path, repo_root=tmp_path)
|
|
167
|
+
assert result == str(blueprint_dir)
|
|
168
|
+
|
|
169
|
+
def test_resolves_direct_under_soul_blueprints(self, tmp_path):
|
|
170
|
+
soul_file = tmp_path / "soul-blueprints" / "sentinel.yaml"
|
|
171
|
+
soul_file.parent.mkdir(parents=True)
|
|
172
|
+
soul_file.write_text("soul: sentinel")
|
|
173
|
+
result = _resolve_soul_blueprint_path(
|
|
174
|
+
"sentinel.yaml", tmp_path, repo_root=tmp_path
|
|
175
|
+
)
|
|
176
|
+
assert result == str(soul_file)
|
|
177
|
+
|
|
178
|
+
def test_returns_original_value_when_unresolvable(self, tmp_path):
|
|
179
|
+
result = _resolve_soul_blueprint_path("nonexistent", tmp_path)
|
|
180
|
+
assert result == "nonexistent"
|
|
181
|
+
|
|
182
|
+
def test_resolves_relative_to_work_dir(self, tmp_path):
|
|
183
|
+
soul_file = tmp_path / "soul.yaml"
|
|
184
|
+
soul_file.write_text("soul: local")
|
|
185
|
+
result = _resolve_soul_blueprint_path("soul.yaml", tmp_path)
|
|
186
|
+
assert result == str(soul_file)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# ---------------------------------------------------------------------------
|
|
190
|
+
# _resolve_skill_paths
|
|
191
|
+
# ---------------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class TestResolveSkillPaths:
|
|
195
|
+
"""Tests for _resolve_skill_paths helper."""
|
|
196
|
+
|
|
197
|
+
def test_empty_list_returns_empty(self):
|
|
198
|
+
assert _resolve_skill_paths([]) == []
|
|
199
|
+
|
|
200
|
+
def test_absolute_existing_path_kept(self, tmp_path):
|
|
201
|
+
skill = tmp_path / "my.skill"
|
|
202
|
+
skill.write_text("skill")
|
|
203
|
+
result = _resolve_skill_paths([str(skill)])
|
|
204
|
+
assert result == [str(skill)]
|
|
205
|
+
|
|
206
|
+
def test_unknown_skill_kept_as_is(self, tmp_path):
|
|
207
|
+
result = _resolve_skill_paths(["unknown-skill"], repo_root=tmp_path)
|
|
208
|
+
assert result == ["unknown-skill"]
|
|
209
|
+
|
|
210
|
+
def test_multiple_unknown_skills_kept_as_is(self, tmp_path):
|
|
211
|
+
result = _resolve_skill_paths(["known", "unknown"], repo_root=tmp_path)
|
|
212
|
+
assert result == ["known", "unknown"]
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# ---------------------------------------------------------------------------
|
|
216
|
+
# _build_session_config
|
|
217
|
+
# ---------------------------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class TestBuildSessionConfig:
|
|
221
|
+
"""Tests for _build_session_config builder."""
|
|
222
|
+
|
|
223
|
+
def test_required_keys_present(self, tmp_path):
|
|
224
|
+
spec = _make_spec(role="coder", model="reason")
|
|
225
|
+
config = _build_session_config("agent-1", "team-1", spec, tmp_path)
|
|
226
|
+
for key in ("agent_name", "team_name", "role", "model", "model_tier",
|
|
227
|
+
"soul_blueprint", "skills", "memory_dir", "scratch_dir",
|
|
228
|
+
"state_file", "env"):
|
|
229
|
+
assert key in config
|
|
230
|
+
|
|
231
|
+
def test_agent_and_team_name_set(self, tmp_path):
|
|
232
|
+
spec = _make_spec()
|
|
233
|
+
config = _build_session_config("my-agent", "my-team", spec, tmp_path)
|
|
234
|
+
assert config["agent_name"] == "my-agent"
|
|
235
|
+
assert config["team_name"] == "my-team"
|
|
236
|
+
|
|
237
|
+
def test_model_name_override_used(self, tmp_path):
|
|
238
|
+
spec = _make_spec(model_name="kimi-k2.5")
|
|
239
|
+
config = _build_session_config("a", "t", spec, tmp_path)
|
|
240
|
+
assert config["model"] == "kimi-k2.5"
|
|
241
|
+
|
|
242
|
+
def test_model_tier_always_set(self, tmp_path):
|
|
243
|
+
spec = _make_spec(model="code")
|
|
244
|
+
config = _build_session_config("a", "t", spec, tmp_path)
|
|
245
|
+
assert config["model_tier"] == "code"
|
|
246
|
+
|
|
247
|
+
def test_soul_blueprint_resolved(self, tmp_path):
|
|
248
|
+
soul_dir = tmp_path / "soul-blueprints" / "blueprints" / "lumina"
|
|
249
|
+
soul_dir.mkdir(parents=True)
|
|
250
|
+
spec = _make_spec(soul_blueprint="lumina")
|
|
251
|
+
config = _build_session_config("a", "t", spec, tmp_path, repo_root=tmp_path)
|
|
252
|
+
assert config["soul_blueprint"] == str(soul_dir)
|
|
253
|
+
|
|
254
|
+
def test_skills_resolved_via_absolute_path(self, tmp_path):
|
|
255
|
+
skill_file = tmp_path / "my-skill" / "skill.yaml"
|
|
256
|
+
skill_file.parent.mkdir(parents=True)
|
|
257
|
+
skill_file.write_text("name: my-skill")
|
|
258
|
+
spec = _make_spec(skills=[str(skill_file.parent)])
|
|
259
|
+
config = _build_session_config("a", "t", spec, tmp_path, repo_root=tmp_path)
|
|
260
|
+
assert config["skills"] == [str(skill_file.parent)]
|
|
261
|
+
|
|
262
|
+
def test_unknown_skills_passed_through(self, tmp_path):
|
|
263
|
+
spec = _make_spec(skills=["unknown-skill"])
|
|
264
|
+
config = _build_session_config("a", "t", spec, tmp_path, repo_root=tmp_path)
|
|
265
|
+
assert config["skills"] == ["unknown-skill"]
|
|
266
|
+
|
|
267
|
+
def test_env_vars_included(self, tmp_path):
|
|
268
|
+
spec = _make_spec(env={"MY_KEY": "my_value"})
|
|
269
|
+
config = _build_session_config("a", "t", spec, tmp_path)
|
|
270
|
+
assert config["env"]["MY_KEY"] == "my_value"
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
# ---------------------------------------------------------------------------
|
|
274
|
+
# _build_crush_config
|
|
275
|
+
# ---------------------------------------------------------------------------
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
class TestBuildCrushConfig:
|
|
279
|
+
"""Tests for _build_crush_config builder."""
|
|
280
|
+
|
|
281
|
+
def test_has_schema_key(self, tmp_path):
|
|
282
|
+
config = _build_crush_config("agent", {}, tmp_path)
|
|
283
|
+
assert "$schema" in config
|
|
284
|
+
|
|
285
|
+
def test_session_block_present(self, tmp_path):
|
|
286
|
+
session = {"agent_name": "a", "model": "fast", "role": "worker", "skills": []}
|
|
287
|
+
config = _build_crush_config("a", session, tmp_path)
|
|
288
|
+
assert "session" in config
|
|
289
|
+
assert config["session"]["agent_name"] == "a"
|
|
290
|
+
|
|
291
|
+
def test_no_none_in_context_paths(self, tmp_path):
|
|
292
|
+
config = _build_crush_config("a", {"soul_blueprint": None}, tmp_path)
|
|
293
|
+
assert None not in config["options"]["context_paths"]
|
|
294
|
+
|
|
295
|
+
def test_soul_blueprint_in_context_paths_when_set(self, tmp_path):
|
|
296
|
+
session = {"soul_blueprint": "/path/to/soul.yaml"}
|
|
297
|
+
config = _build_crush_config("a", session, tmp_path)
|
|
298
|
+
assert "/path/to/soul.yaml" in config["options"]["context_paths"]
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
# ---------------------------------------------------------------------------
|
|
302
|
+
# LocalProvider.provision
|
|
303
|
+
# ---------------------------------------------------------------------------
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
class TestProvision:
|
|
307
|
+
"""Tests for LocalProvider.provision()."""
|
|
308
|
+
|
|
309
|
+
def test_creates_work_dir(self, provider, tmp_path):
|
|
310
|
+
spec = _make_spec()
|
|
311
|
+
result = provider.provision("my-agent", spec, "my-team")
|
|
312
|
+
assert Path(result["work_dir"]).exists()
|
|
313
|
+
|
|
314
|
+
def test_creates_memory_and_scratch_dirs(self, provider):
|
|
315
|
+
spec = _make_spec()
|
|
316
|
+
result = provider.provision("agent-x", spec, "team-y")
|
|
317
|
+
wd = Path(result["work_dir"])
|
|
318
|
+
assert (wd / "memory").is_dir()
|
|
319
|
+
assert (wd / "scratch").is_dir()
|
|
320
|
+
|
|
321
|
+
def test_writes_config_json(self, provider):
|
|
322
|
+
spec = _make_spec()
|
|
323
|
+
result = provider.provision("agent-x", spec, "team-y")
|
|
324
|
+
config_file = Path(result["work_dir"]) / "config.json"
|
|
325
|
+
assert config_file.exists()
|
|
326
|
+
data = json.loads(config_file.read_text())
|
|
327
|
+
assert data["agent_name"] == "agent-x"
|
|
328
|
+
|
|
329
|
+
def test_writes_session_json(self, provider):
|
|
330
|
+
spec = _make_spec()
|
|
331
|
+
result = provider.provision("agent-x", spec, "team-y")
|
|
332
|
+
session_file = Path(result["work_dir"]) / _SESSION_CONFIG_FILE
|
|
333
|
+
assert session_file.exists()
|
|
334
|
+
|
|
335
|
+
def test_returns_host_localhost(self, provider):
|
|
336
|
+
spec = _make_spec()
|
|
337
|
+
result = provider.provision("agent-x", spec, "team-y")
|
|
338
|
+
assert result["host"] == "localhost"
|
|
339
|
+
|
|
340
|
+
def test_session_config_in_result(self, provider):
|
|
341
|
+
spec = _make_spec(soul_blueprint="lumina", skills=["code"])
|
|
342
|
+
result = provider.provision("agent-x", spec, "team-y")
|
|
343
|
+
assert "session_config" in result
|
|
344
|
+
sc = result["session_config"]
|
|
345
|
+
assert sc["agent_name"] == "agent-x"
|
|
346
|
+
assert sc["model_tier"] == "fast"
|
|
347
|
+
|
|
348
|
+
def test_edge_agent_name_with_hyphens(self, provider):
|
|
349
|
+
spec = _make_spec()
|
|
350
|
+
result = provider.provision("team-abc-worker-1", spec, "team-abc")
|
|
351
|
+
assert Path(result["work_dir"]).exists()
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
# ---------------------------------------------------------------------------
|
|
355
|
+
# LocalProvider.configure
|
|
356
|
+
# ---------------------------------------------------------------------------
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
class TestConfigure:
|
|
360
|
+
"""Tests for LocalProvider.configure()."""
|
|
361
|
+
|
|
362
|
+
def test_returns_true_on_success(self, provider, tmp_path):
|
|
363
|
+
spec = _make_spec()
|
|
364
|
+
pr = provider.provision("agent-c", spec, "team-c")
|
|
365
|
+
assert provider.configure("agent-c", spec, pr) is True
|
|
366
|
+
|
|
367
|
+
def test_writes_crush_json(self, provider, tmp_path):
|
|
368
|
+
spec = _make_spec()
|
|
369
|
+
pr = provider.provision("agent-c", spec, "team-c")
|
|
370
|
+
provider.configure("agent-c", spec, pr)
|
|
371
|
+
crush_file = Path(pr["work_dir"]) / _CRUSH_CONFIG_FILE
|
|
372
|
+
assert crush_file.exists()
|
|
373
|
+
data = json.loads(crush_file.read_text())
|
|
374
|
+
assert "$schema" in data
|
|
375
|
+
|
|
376
|
+
def test_returns_false_when_work_dir_missing(self, provider):
|
|
377
|
+
spec = _make_spec()
|
|
378
|
+
assert provider.configure("ghost", spec, {}) is False
|
|
379
|
+
|
|
380
|
+
def test_crush_json_contains_session_block(self, provider):
|
|
381
|
+
spec = _make_spec(model="code", role="coder")
|
|
382
|
+
pr = provider.provision("agent-d", spec, "team-d")
|
|
383
|
+
provider.configure("agent-d", spec, pr)
|
|
384
|
+
crush_file = Path(pr["work_dir"]) / _CRUSH_CONFIG_FILE
|
|
385
|
+
data = json.loads(crush_file.read_text())
|
|
386
|
+
assert data["session"]["role"] == "coder"
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
# ---------------------------------------------------------------------------
|
|
390
|
+
# LocalProvider.start — crush binary path
|
|
391
|
+
# ---------------------------------------------------------------------------
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
class TestStartWithCrushBinary:
|
|
395
|
+
"""Tests for LocalProvider.start() when crush binary is available."""
|
|
396
|
+
|
|
397
|
+
@pytest.fixture()
|
|
398
|
+
def _patched_popen(self):
|
|
399
|
+
"""Patch subprocess.Popen to return a fake process."""
|
|
400
|
+
mock_proc = MagicMock()
|
|
401
|
+
mock_proc.pid = 42000
|
|
402
|
+
with patch("subprocess.Popen", return_value=mock_proc) as mock_popen:
|
|
403
|
+
yield mock_popen, mock_proc
|
|
404
|
+
|
|
405
|
+
def test_spawns_crush_subprocess(self, provider, tmp_path, _patched_popen):
|
|
406
|
+
mock_popen, _ = _patched_popen
|
|
407
|
+
spec = _make_spec()
|
|
408
|
+
pr = provider.provision("agent-s", spec, "team-s")
|
|
409
|
+
provider.configure("agent-s", spec, pr)
|
|
410
|
+
|
|
411
|
+
with patch(
|
|
412
|
+
"skcapstone.providers.local._find_crush_binary",
|
|
413
|
+
return_value="/usr/bin/crush",
|
|
414
|
+
):
|
|
415
|
+
result = provider.start("agent-s", pr)
|
|
416
|
+
|
|
417
|
+
assert result is True
|
|
418
|
+
mock_popen.assert_called_once()
|
|
419
|
+
cmd = mock_popen.call_args[0][0]
|
|
420
|
+
assert cmd[0] == "/usr/bin/crush"
|
|
421
|
+
assert "run" in cmd
|
|
422
|
+
|
|
423
|
+
def test_sets_pid_in_provision_result(self, provider, tmp_path, _patched_popen):
|
|
424
|
+
_, mock_proc = _patched_popen
|
|
425
|
+
mock_proc.pid = 55555
|
|
426
|
+
spec = _make_spec()
|
|
427
|
+
pr = provider.provision("agent-pid", spec, "team-s")
|
|
428
|
+
provider.configure("agent-pid", spec, pr)
|
|
429
|
+
|
|
430
|
+
with patch(
|
|
431
|
+
"skcapstone.providers.local._find_crush_binary",
|
|
432
|
+
return_value="/usr/bin/crush",
|
|
433
|
+
):
|
|
434
|
+
provider.start("agent-pid", pr)
|
|
435
|
+
|
|
436
|
+
assert pr["pid"] == 55555
|
|
437
|
+
|
|
438
|
+
def test_writes_pid_file(self, provider, tmp_path, _patched_popen):
|
|
439
|
+
_, mock_proc = _patched_popen
|
|
440
|
+
mock_proc.pid = 11111
|
|
441
|
+
spec = _make_spec()
|
|
442
|
+
pr = provider.provision("agent-pf", spec, "team-s")
|
|
443
|
+
provider.configure("agent-pf", spec, pr)
|
|
444
|
+
|
|
445
|
+
with patch(
|
|
446
|
+
"skcapstone.providers.local._find_crush_binary",
|
|
447
|
+
return_value="/usr/bin/crush",
|
|
448
|
+
):
|
|
449
|
+
provider.start("agent-pf", pr)
|
|
450
|
+
|
|
451
|
+
pid_file = Path(pr["work_dir"]) / _PID_FILE
|
|
452
|
+
assert pid_file.read_text().strip() == "11111"
|
|
453
|
+
|
|
454
|
+
def test_writes_session_state_file(self, provider, tmp_path, _patched_popen):
|
|
455
|
+
_, mock_proc = _patched_popen
|
|
456
|
+
mock_proc.pid = 22222
|
|
457
|
+
spec = _make_spec()
|
|
458
|
+
pr = provider.provision("agent-ss", spec, "team-s")
|
|
459
|
+
provider.configure("agent-ss", spec, pr)
|
|
460
|
+
|
|
461
|
+
with patch(
|
|
462
|
+
"skcapstone.providers.local._find_crush_binary",
|
|
463
|
+
return_value="/usr/bin/crush",
|
|
464
|
+
):
|
|
465
|
+
provider.start("agent-ss", pr)
|
|
466
|
+
|
|
467
|
+
state = json.loads((Path(pr["work_dir"]) / _SESSION_STATE_FILE).read_text())
|
|
468
|
+
assert state["status"] == _STATE_RUNNING
|
|
469
|
+
assert state["pid"] == 22222
|
|
470
|
+
|
|
471
|
+
def test_passes_soul_blueprint_in_env(self, provider, tmp_path, _patched_popen):
|
|
472
|
+
mock_popen, _ = _patched_popen
|
|
473
|
+
spec = _make_spec(soul_blueprint="lumina")
|
|
474
|
+
pr = provider.provision("agent-soul", spec, "team-s")
|
|
475
|
+
provider.configure("agent-soul", spec, pr)
|
|
476
|
+
|
|
477
|
+
with patch(
|
|
478
|
+
"skcapstone.providers.local._find_crush_binary",
|
|
479
|
+
return_value="/usr/bin/crush",
|
|
480
|
+
):
|
|
481
|
+
provider.start("agent-soul", pr)
|
|
482
|
+
|
|
483
|
+
env = mock_popen.call_args[1]["env"]
|
|
484
|
+
assert "SOUL_BLUEPRINT" in env
|
|
485
|
+
# Value may be resolved path or original slug
|
|
486
|
+
assert "lumina" in env["SOUL_BLUEPRINT"].lower() or env["SOUL_BLUEPRINT"] == "lumina"
|
|
487
|
+
|
|
488
|
+
def test_passes_skills_as_json_in_env(self, provider, tmp_path, _patched_popen):
|
|
489
|
+
mock_popen, _ = _patched_popen
|
|
490
|
+
spec = _make_spec(skills=["code-review", "docs"])
|
|
491
|
+
pr = provider.provision("agent-skills", spec, "team-s")
|
|
492
|
+
provider.configure("agent-skills", spec, pr)
|
|
493
|
+
|
|
494
|
+
with patch(
|
|
495
|
+
"skcapstone.providers.local._find_crush_binary",
|
|
496
|
+
return_value="/usr/bin/crush",
|
|
497
|
+
):
|
|
498
|
+
provider.start("agent-skills", pr)
|
|
499
|
+
|
|
500
|
+
env = mock_popen.call_args[1]["env"]
|
|
501
|
+
parsed_skills = json.loads(env["AGENT_SKILLS"])
|
|
502
|
+
assert "code-review" in parsed_skills
|
|
503
|
+
assert "docs" in parsed_skills
|
|
504
|
+
|
|
505
|
+
def test_passes_model_tier_in_env(self, provider, tmp_path, _patched_popen):
|
|
506
|
+
mock_popen, _ = _patched_popen
|
|
507
|
+
spec = _make_spec(model="reason")
|
|
508
|
+
pr = provider.provision("agent-model", spec, "team-s")
|
|
509
|
+
provider.configure("agent-model", spec, pr)
|
|
510
|
+
|
|
511
|
+
with patch(
|
|
512
|
+
"skcapstone.providers.local._find_crush_binary",
|
|
513
|
+
return_value="/usr/bin/crush",
|
|
514
|
+
):
|
|
515
|
+
provider.start("agent-model", pr)
|
|
516
|
+
|
|
517
|
+
env = mock_popen.call_args[1]["env"]
|
|
518
|
+
assert env["AGENT_MODEL_TIER"] == "reason"
|
|
519
|
+
|
|
520
|
+
def test_returns_false_on_popen_error(self, provider, tmp_path):
|
|
521
|
+
spec = _make_spec()
|
|
522
|
+
pr = provider.provision("agent-err", spec, "team-s")
|
|
523
|
+
provider.configure("agent-err", spec, pr)
|
|
524
|
+
|
|
525
|
+
with patch(
|
|
526
|
+
"skcapstone.providers.local._find_crush_binary",
|
|
527
|
+
return_value="/usr/bin/crush",
|
|
528
|
+
):
|
|
529
|
+
with patch("subprocess.Popen", side_effect=OSError("permission denied")):
|
|
530
|
+
result = provider.start("agent-err", pr)
|
|
531
|
+
|
|
532
|
+
assert result is False
|
|
533
|
+
|
|
534
|
+
def test_returns_false_when_work_dir_missing(self, provider):
|
|
535
|
+
result = provider.start("ghost", {})
|
|
536
|
+
assert result is False
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
# ---------------------------------------------------------------------------
|
|
540
|
+
# LocalProvider.start — stub fallback
|
|
541
|
+
# ---------------------------------------------------------------------------
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
class TestStartStubFallback:
|
|
545
|
+
"""Tests for LocalProvider.start() stub when crush is not available."""
|
|
546
|
+
|
|
547
|
+
@pytest.fixture()
|
|
548
|
+
def _patched_popen(self):
|
|
549
|
+
mock_proc = MagicMock()
|
|
550
|
+
mock_proc.pid = 9999
|
|
551
|
+
with patch("subprocess.Popen", return_value=mock_proc) as mock_popen:
|
|
552
|
+
yield mock_popen, mock_proc
|
|
553
|
+
|
|
554
|
+
def test_falls_back_to_stub(self, provider, _patched_popen):
|
|
555
|
+
mock_popen, _ = _patched_popen
|
|
556
|
+
spec = _make_spec()
|
|
557
|
+
pr = provider.provision("agent-stub", spec, "team-s")
|
|
558
|
+
|
|
559
|
+
with patch(
|
|
560
|
+
"skcapstone.providers.local._find_crush_binary",
|
|
561
|
+
return_value=None,
|
|
562
|
+
):
|
|
563
|
+
result = provider.start("agent-stub", pr)
|
|
564
|
+
|
|
565
|
+
assert result is True
|
|
566
|
+
mock_popen.assert_called_once()
|
|
567
|
+
# Stub uses python -c ...
|
|
568
|
+
cmd = mock_popen.call_args[0][0]
|
|
569
|
+
assert cmd[0] == os.sys.executable
|
|
570
|
+
assert cmd[1] == "-c"
|
|
571
|
+
|
|
572
|
+
def test_stub_writes_running_state(self, provider, _patched_popen):
|
|
573
|
+
_, mock_proc = _patched_popen
|
|
574
|
+
mock_proc.pid = 7777
|
|
575
|
+
spec = _make_spec()
|
|
576
|
+
pr = provider.provision("agent-stub2", spec, "team-s")
|
|
577
|
+
|
|
578
|
+
with patch(
|
|
579
|
+
"skcapstone.providers.local._find_crush_binary",
|
|
580
|
+
return_value=None,
|
|
581
|
+
):
|
|
582
|
+
provider.start("agent-stub2", pr)
|
|
583
|
+
|
|
584
|
+
state = json.loads(
|
|
585
|
+
(Path(pr["work_dir"]) / _SESSION_STATE_FILE).read_text()
|
|
586
|
+
)
|
|
587
|
+
assert state["status"] == _STATE_RUNNING
|
|
588
|
+
assert state["pid"] == 7777
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
# ---------------------------------------------------------------------------
|
|
592
|
+
# LocalProvider.stop
|
|
593
|
+
# ---------------------------------------------------------------------------
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
class TestStop:
|
|
597
|
+
"""Tests for LocalProvider.stop()."""
|
|
598
|
+
|
|
599
|
+
def test_returns_true_when_no_pid(self, provider, agent_dir):
|
|
600
|
+
pr = _provision_result(str(agent_dir))
|
|
601
|
+
assert provider.stop("no-pid-agent", pr) is True
|
|
602
|
+
|
|
603
|
+
def test_returns_true_when_pid_already_dead(self, provider, agent_dir):
|
|
604
|
+
pr = _provision_result(str(agent_dir), pid=99999999)
|
|
605
|
+
with patch("skcapstone.providers.local._pid_is_alive", return_value=False):
|
|
606
|
+
result = provider.stop("dead-agent", pr)
|
|
607
|
+
assert result is True
|
|
608
|
+
|
|
609
|
+
def test_sends_sigterm(self, provider, agent_dir):
|
|
610
|
+
pr = _provision_result(str(agent_dir), pid=12345)
|
|
611
|
+
# Return True once (pre-SIGTERM check), then False (loop exit)
|
|
612
|
+
alive_seq = iter([True] + [False] * 60)
|
|
613
|
+
with patch("os.kill") as mock_kill:
|
|
614
|
+
with patch(
|
|
615
|
+
"skcapstone.providers.local._pid_is_alive",
|
|
616
|
+
side_effect=alive_seq,
|
|
617
|
+
):
|
|
618
|
+
provider.stop("agent-term", pr)
|
|
619
|
+
|
|
620
|
+
mock_kill.assert_any_call(12345, signal.SIGTERM)
|
|
621
|
+
|
|
622
|
+
def test_writes_stopped_state_after_stop(self, provider, agent_dir):
|
|
623
|
+
pr = _provision_result(str(agent_dir), pid=12345)
|
|
624
|
+
alive_seq = iter([True] + [False] * 60)
|
|
625
|
+
with patch("os.kill"):
|
|
626
|
+
with patch(
|
|
627
|
+
"skcapstone.providers.local._pid_is_alive",
|
|
628
|
+
side_effect=alive_seq,
|
|
629
|
+
):
|
|
630
|
+
provider.stop("agent-state", pr)
|
|
631
|
+
|
|
632
|
+
state = _read_session_state(agent_dir)
|
|
633
|
+
assert state is not None
|
|
634
|
+
assert state["status"] == _STATE_STOPPED
|
|
635
|
+
|
|
636
|
+
def test_sends_sigkill_after_timeout(self, provider, agent_dir):
|
|
637
|
+
pr = _provision_result(str(agent_dir), pid=12345)
|
|
638
|
+
# Always alive so that SIGKILL path is triggered
|
|
639
|
+
with patch("os.kill") as mock_kill:
|
|
640
|
+
with patch(
|
|
641
|
+
"skcapstone.providers.local._pid_is_alive",
|
|
642
|
+
return_value=True,
|
|
643
|
+
):
|
|
644
|
+
with patch("skcapstone.providers.local._STOP_TIMEOUT_SECONDS", 0):
|
|
645
|
+
with patch(
|
|
646
|
+
"skcapstone.providers.local._STOP_KILL_TIMEOUT_SECONDS", 0
|
|
647
|
+
):
|
|
648
|
+
provider.stop("slow-agent", pr)
|
|
649
|
+
|
|
650
|
+
mock_kill.assert_any_call(12345, signal.SIGTERM)
|
|
651
|
+
mock_kill.assert_any_call(12345, signal.SIGKILL)
|
|
652
|
+
|
|
653
|
+
def test_reads_pid_from_pid_file_when_not_in_result(self, provider, agent_dir):
|
|
654
|
+
(agent_dir / _PID_FILE).write_text("54321")
|
|
655
|
+
pr = _provision_result(str(agent_dir)) # no pid key
|
|
656
|
+
|
|
657
|
+
alive_seq = iter([True] + [False] * 60)
|
|
658
|
+
with patch("os.kill"):
|
|
659
|
+
with patch(
|
|
660
|
+
"skcapstone.providers.local._pid_is_alive",
|
|
661
|
+
side_effect=alive_seq,
|
|
662
|
+
):
|
|
663
|
+
result = provider.stop("file-pid-agent", pr)
|
|
664
|
+
|
|
665
|
+
assert result is True
|
|
666
|
+
|
|
667
|
+
def test_returns_false_on_sigterm_oserror(self, provider, agent_dir):
|
|
668
|
+
pr = _provision_result(str(agent_dir), pid=12345)
|
|
669
|
+
with patch(
|
|
670
|
+
"skcapstone.providers.local._pid_is_alive", return_value=True
|
|
671
|
+
):
|
|
672
|
+
with patch("os.kill", side_effect=OSError("eperm")):
|
|
673
|
+
result = provider.stop("perm-agent", pr)
|
|
674
|
+
|
|
675
|
+
assert result is False
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
# ---------------------------------------------------------------------------
|
|
679
|
+
# LocalProvider.health_check
|
|
680
|
+
# ---------------------------------------------------------------------------
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
class TestHealthCheck:
|
|
684
|
+
"""Tests for LocalProvider.health_check()."""
|
|
685
|
+
|
|
686
|
+
def test_returns_stopped_when_no_pid_no_state(self, provider, agent_dir):
|
|
687
|
+
pr = _provision_result(str(agent_dir))
|
|
688
|
+
assert provider.health_check("agent", pr) == AgentStatus.STOPPED
|
|
689
|
+
|
|
690
|
+
def test_running_state_file_returns_running(self, provider, agent_dir):
|
|
691
|
+
_write_session_state(agent_dir, {"status": "running", "pid": 9999})
|
|
692
|
+
pr = _provision_result(str(agent_dir), pid=9999)
|
|
693
|
+
with patch("skcapstone.providers.local._pid_is_alive", return_value=True):
|
|
694
|
+
result = provider.health_check("agent", pr)
|
|
695
|
+
assert result == AgentStatus.RUNNING
|
|
696
|
+
|
|
697
|
+
def test_idle_state_file_returns_running(self, provider, agent_dir):
|
|
698
|
+
_write_session_state(agent_dir, {"status": "idle", "pid": 9999})
|
|
699
|
+
pr = _provision_result(str(agent_dir), pid=9999)
|
|
700
|
+
with patch("skcapstone.providers.local._pid_is_alive", return_value=True):
|
|
701
|
+
result = provider.health_check("agent", pr)
|
|
702
|
+
assert result == AgentStatus.RUNNING
|
|
703
|
+
|
|
704
|
+
def test_stopped_state_file_returns_stopped(self, provider, agent_dir):
|
|
705
|
+
_write_session_state(agent_dir, {"status": "stopped"})
|
|
706
|
+
pr = _provision_result(str(agent_dir))
|
|
707
|
+
result = provider.health_check("agent", pr)
|
|
708
|
+
assert result == AgentStatus.STOPPED
|
|
709
|
+
|
|
710
|
+
def test_error_state_file_returns_degraded(self, provider, agent_dir):
|
|
711
|
+
_write_session_state(agent_dir, {"status": "error", "pid": 9999})
|
|
712
|
+
pr = _provision_result(str(agent_dir), pid=9999)
|
|
713
|
+
with patch("skcapstone.providers.local._pid_is_alive", return_value=True):
|
|
714
|
+
result = provider.health_check("agent", pr)
|
|
715
|
+
assert result == AgentStatus.DEGRADED
|
|
716
|
+
|
|
717
|
+
def test_running_state_but_dead_pid_returns_degraded(self, provider, agent_dir):
|
|
718
|
+
_write_session_state(agent_dir, {"status": "running", "pid": 9999})
|
|
719
|
+
pr = _provision_result(str(agent_dir), pid=9999)
|
|
720
|
+
with patch("skcapstone.providers.local._pid_is_alive", return_value=False):
|
|
721
|
+
result = provider.health_check("agent", pr)
|
|
722
|
+
assert result == AgentStatus.DEGRADED
|
|
723
|
+
|
|
724
|
+
def test_no_state_file_alive_pid_returns_running(self, provider, agent_dir):
|
|
725
|
+
pr = _provision_result(str(agent_dir), pid=9999)
|
|
726
|
+
with patch("skcapstone.providers.local._pid_is_alive", return_value=True):
|
|
727
|
+
result = provider.health_check("agent", pr)
|
|
728
|
+
assert result == AgentStatus.RUNNING
|
|
729
|
+
|
|
730
|
+
def test_no_state_file_dead_pid_returns_stopped(self, provider, agent_dir):
|
|
731
|
+
pr = _provision_result(str(agent_dir), pid=9999)
|
|
732
|
+
with patch("skcapstone.providers.local._pid_is_alive", return_value=False):
|
|
733
|
+
result = provider.health_check("agent", pr)
|
|
734
|
+
assert result == AgentStatus.STOPPED
|
|
735
|
+
|
|
736
|
+
def test_reads_pid_from_pid_file(self, provider, agent_dir):
|
|
737
|
+
(agent_dir / _PID_FILE).write_text("12345")
|
|
738
|
+
pr = _provision_result(str(agent_dir))
|
|
739
|
+
with patch("skcapstone.providers.local._pid_is_alive", return_value=True):
|
|
740
|
+
result = provider.health_check("agent", pr)
|
|
741
|
+
assert result == AgentStatus.RUNNING
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
# ---------------------------------------------------------------------------
|
|
745
|
+
# LocalProvider.destroy
|
|
746
|
+
# ---------------------------------------------------------------------------
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
class TestDestroy:
|
|
750
|
+
"""Tests for LocalProvider.destroy()."""
|
|
751
|
+
|
|
752
|
+
def test_removes_work_dir(self, provider, agent_dir):
|
|
753
|
+
pr = _provision_result(str(agent_dir))
|
|
754
|
+
with patch.object(provider, "stop", return_value=True):
|
|
755
|
+
result = provider.destroy("agent", pr)
|
|
756
|
+
assert result is True
|
|
757
|
+
assert not agent_dir.exists()
|
|
758
|
+
|
|
759
|
+
def test_calls_stop_first(self, provider, agent_dir):
|
|
760
|
+
pr = _provision_result(str(agent_dir))
|
|
761
|
+
with patch.object(provider, "stop", return_value=True) as mock_stop:
|
|
762
|
+
provider.destroy("agent", pr)
|
|
763
|
+
mock_stop.assert_called_once_with("agent", pr)
|
|
764
|
+
|
|
765
|
+
def test_returns_true_even_when_dir_missing(self, provider, tmp_path):
|
|
766
|
+
pr = _provision_result(str(tmp_path / "ghost"))
|
|
767
|
+
with patch.object(provider, "stop", return_value=True):
|
|
768
|
+
result = provider.destroy("ghost", pr)
|
|
769
|
+
assert result is True
|
|
770
|
+
|
|
771
|
+
def test_empty_provision_result_returns_true(self, provider):
|
|
772
|
+
result = provider.destroy("nobody", {})
|
|
773
|
+
assert result is True
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
# ---------------------------------------------------------------------------
|
|
777
|
+
# _session_state_to_agent_status
|
|
778
|
+
# ---------------------------------------------------------------------------
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
class TestSessionStateToAgentStatus:
|
|
782
|
+
"""Tests for the state → AgentStatus mapper."""
|
|
783
|
+
|
|
784
|
+
def test_running_with_live_pid(self):
|
|
785
|
+
with patch("skcapstone.providers.local._pid_is_alive", return_value=True):
|
|
786
|
+
result = _session_state_to_agent_status(
|
|
787
|
+
{"status": "running", "pid": 1234}, 1234
|
|
788
|
+
)
|
|
789
|
+
assert result == AgentStatus.RUNNING
|
|
790
|
+
|
|
791
|
+
def test_running_with_dead_pid_returns_degraded(self):
|
|
792
|
+
with patch("skcapstone.providers.local._pid_is_alive", return_value=False):
|
|
793
|
+
result = _session_state_to_agent_status(
|
|
794
|
+
{"status": "running", "pid": 1234}, 1234
|
|
795
|
+
)
|
|
796
|
+
assert result == AgentStatus.DEGRADED
|
|
797
|
+
|
|
798
|
+
def test_idle_returns_running(self):
|
|
799
|
+
with patch("skcapstone.providers.local._pid_is_alive", return_value=True):
|
|
800
|
+
result = _session_state_to_agent_status({"status": "idle", "pid": 1}, 1)
|
|
801
|
+
assert result == AgentStatus.RUNNING
|
|
802
|
+
|
|
803
|
+
def test_stopped_returns_stopped(self):
|
|
804
|
+
result = _session_state_to_agent_status({"status": "stopped"}, None)
|
|
805
|
+
assert result == AgentStatus.STOPPED
|
|
806
|
+
|
|
807
|
+
def test_error_returns_degraded(self):
|
|
808
|
+
with patch("skcapstone.providers.local._pid_is_alive", return_value=True):
|
|
809
|
+
result = _session_state_to_agent_status({"status": "error", "pid": 1}, 1)
|
|
810
|
+
assert result == AgentStatus.DEGRADED
|
|
811
|
+
|
|
812
|
+
def test_unknown_status_returns_degraded(self):
|
|
813
|
+
result = _session_state_to_agent_status({"status": "banana"}, None)
|
|
814
|
+
assert result == AgentStatus.DEGRADED
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
# ---------------------------------------------------------------------------
|
|
818
|
+
# _stub_script
|
|
819
|
+
# ---------------------------------------------------------------------------
|
|
820
|
+
|
|
821
|
+
|
|
822
|
+
class TestStubScript:
|
|
823
|
+
"""Tests for the stub process script generator."""
|
|
824
|
+
|
|
825
|
+
def test_returns_string(self, tmp_path):
|
|
826
|
+
script = _stub_script("my-agent", str(tmp_path / "state.json"))
|
|
827
|
+
assert isinstance(script, str)
|
|
828
|
+
assert len(script) > 0
|
|
829
|
+
|
|
830
|
+
def test_script_contains_agent_name(self, tmp_path):
|
|
831
|
+
script = _stub_script("my-agent", str(tmp_path / "state.json"))
|
|
832
|
+
assert "my-agent" in script
|
|
833
|
+
|
|
834
|
+
def test_script_contains_state_file_path(self, tmp_path):
|
|
835
|
+
state_path = str(tmp_path / "state.json")
|
|
836
|
+
script = _stub_script("agent", state_path)
|
|
837
|
+
assert state_path in script
|
|
838
|
+
|
|
839
|
+
def test_script_is_valid_python(self, tmp_path):
|
|
840
|
+
script = _stub_script("test-agent", str(tmp_path / "state.json"))
|
|
841
|
+
compile(script, "<stub>", "exec")
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
# ---------------------------------------------------------------------------
|
|
845
|
+
# Integration: full provision → configure → start → health_check → stop
|
|
846
|
+
# ---------------------------------------------------------------------------
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
class TestFullLifecycle:
|
|
850
|
+
"""End-to-end lifecycle tests with mocked subprocess."""
|
|
851
|
+
|
|
852
|
+
def test_lifecycle_with_crush(self, provider, tmp_path):
|
|
853
|
+
mock_proc = MagicMock()
|
|
854
|
+
mock_proc.pid = 88888
|
|
855
|
+
|
|
856
|
+
spec = _make_spec(
|
|
857
|
+
role="coder",
|
|
858
|
+
model="code",
|
|
859
|
+
soul_blueprint="lumina",
|
|
860
|
+
skills=["code-review"],
|
|
861
|
+
env={"EXTRA": "val"},
|
|
862
|
+
)
|
|
863
|
+
|
|
864
|
+
with patch("subprocess.Popen", return_value=mock_proc):
|
|
865
|
+
with patch(
|
|
866
|
+
"skcapstone.providers.local._find_crush_binary",
|
|
867
|
+
return_value="/usr/bin/crush",
|
|
868
|
+
):
|
|
869
|
+
pr = provider.provision("lifecycle-agent", spec, "team-lc")
|
|
870
|
+
provider.configure("lifecycle-agent", spec, pr)
|
|
871
|
+
started = provider.start("lifecycle-agent", pr)
|
|
872
|
+
|
|
873
|
+
assert started is True
|
|
874
|
+
assert pr["pid"] == 88888
|
|
875
|
+
|
|
876
|
+
# Health check: session state says running, pid alive
|
|
877
|
+
with patch("skcapstone.providers.local._pid_is_alive", return_value=True):
|
|
878
|
+
status = provider.health_check("lifecycle-agent", pr)
|
|
879
|
+
assert status == AgentStatus.RUNNING
|
|
880
|
+
|
|
881
|
+
# Stop
|
|
882
|
+
alive_seq = iter([True] + [False] * 60)
|
|
883
|
+
with patch("os.kill"):
|
|
884
|
+
with patch(
|
|
885
|
+
"skcapstone.providers.local._pid_is_alive",
|
|
886
|
+
side_effect=alive_seq,
|
|
887
|
+
):
|
|
888
|
+
stopped = provider.stop("lifecycle-agent", pr)
|
|
889
|
+
|
|
890
|
+
assert stopped is True
|
|
891
|
+
|
|
892
|
+
# Post-stop health check
|
|
893
|
+
status_after = provider.health_check("lifecycle-agent", pr)
|
|
894
|
+
assert status_after == AgentStatus.STOPPED
|
|
895
|
+
|
|
896
|
+
def test_lifecycle_with_stub_fallback(self, provider):
|
|
897
|
+
mock_proc = MagicMock()
|
|
898
|
+
mock_proc.pid = 77777
|
|
899
|
+
spec = _make_spec()
|
|
900
|
+
|
|
901
|
+
with patch("subprocess.Popen", return_value=mock_proc):
|
|
902
|
+
with patch(
|
|
903
|
+
"skcapstone.providers.local._find_crush_binary",
|
|
904
|
+
return_value=None,
|
|
905
|
+
):
|
|
906
|
+
pr = provider.provision("stub-agent", spec, "team-stub")
|
|
907
|
+
provider.configure("stub-agent", spec, pr)
|
|
908
|
+
started = provider.start("stub-agent", pr)
|
|
909
|
+
|
|
910
|
+
assert started is True
|
|
911
|
+
|
|
912
|
+
with patch("skcapstone.providers.local._pid_is_alive", return_value=True):
|
|
913
|
+
status = provider.health_check("stub-agent", pr)
|
|
914
|
+
assert status == AgentStatus.RUNNING
|
|
915
|
+
|
|
916
|
+
|
|
917
|
+
# ---------------------------------------------------------------------------
|
|
918
|
+
# _is_claude_binary
|
|
919
|
+
# ---------------------------------------------------------------------------
|
|
920
|
+
|
|
921
|
+
|
|
922
|
+
class TestIsClaudeBinary:
|
|
923
|
+
"""Tests for _is_claude_binary helper."""
|
|
924
|
+
|
|
925
|
+
def test_returns_true_for_claude(self):
|
|
926
|
+
assert _is_claude_binary("/bin/claude") is True
|
|
927
|
+
|
|
928
|
+
def test_returns_true_for_claude_in_usr(self):
|
|
929
|
+
assert _is_claude_binary("/usr/local/bin/claude") is True
|
|
930
|
+
|
|
931
|
+
def test_returns_false_for_crush(self):
|
|
932
|
+
assert _is_claude_binary("/usr/bin/crush") is False
|
|
933
|
+
|
|
934
|
+
|
|
935
|
+
# ---------------------------------------------------------------------------
|
|
936
|
+
# LocalProvider.start — claude binary path
|
|
937
|
+
# ---------------------------------------------------------------------------
|
|
938
|
+
|
|
939
|
+
|
|
940
|
+
class TestStartWithClaudeBinary:
|
|
941
|
+
"""Tests for LocalProvider.start() when claude binary is found."""
|
|
942
|
+
|
|
943
|
+
@pytest.fixture()
|
|
944
|
+
def _patched_popen(self):
|
|
945
|
+
"""Patch subprocess.Popen to return a fake process."""
|
|
946
|
+
mock_proc = MagicMock()
|
|
947
|
+
mock_proc.pid = 33000
|
|
948
|
+
with patch("subprocess.Popen", return_value=mock_proc) as mock_popen:
|
|
949
|
+
yield mock_popen, mock_proc
|
|
950
|
+
|
|
951
|
+
def test_spawns_claude_subprocess(self, provider, tmp_path, _patched_popen):
|
|
952
|
+
mock_popen, _ = _patched_popen
|
|
953
|
+
spec = _make_spec()
|
|
954
|
+
pr = provider.provision("agent-claude", spec, "team-c")
|
|
955
|
+
provider.configure("agent-claude", spec, pr)
|
|
956
|
+
|
|
957
|
+
with patch(
|
|
958
|
+
"skcapstone.providers.local._find_crush_binary",
|
|
959
|
+
return_value="/bin/claude",
|
|
960
|
+
):
|
|
961
|
+
result = provider.start("agent-claude", pr)
|
|
962
|
+
|
|
963
|
+
assert result is True
|
|
964
|
+
mock_popen.assert_called_once()
|
|
965
|
+
cmd = mock_popen.call_args[0][0]
|
|
966
|
+
assert cmd[0] == "/bin/claude"
|
|
967
|
+
assert "-p" in cmd
|
|
968
|
+
|
|
969
|
+
def test_claude_cmd_includes_model(self, provider, tmp_path, _patched_popen):
|
|
970
|
+
mock_popen, _ = _patched_popen
|
|
971
|
+
spec = _make_spec(model_name="claude-opus-4-6")
|
|
972
|
+
pr = provider.provision("agent-cm", spec, "team-c")
|
|
973
|
+
provider.configure("agent-cm", spec, pr)
|
|
974
|
+
|
|
975
|
+
with patch(
|
|
976
|
+
"skcapstone.providers.local._find_crush_binary",
|
|
977
|
+
return_value="/bin/claude",
|
|
978
|
+
):
|
|
979
|
+
provider.start("agent-cm", pr)
|
|
980
|
+
|
|
981
|
+
cmd = mock_popen.call_args[0][0]
|
|
982
|
+
model_idx = cmd.index("--model")
|
|
983
|
+
assert cmd[model_idx + 1] == "claude-opus-4-6"
|
|
984
|
+
|
|
985
|
+
def test_claude_cmd_includes_system_prompt(self, provider, tmp_path, _patched_popen):
|
|
986
|
+
mock_popen, _ = _patched_popen
|
|
987
|
+
spec = _make_spec(soul_blueprint="lumina")
|
|
988
|
+
pr = provider.provision("agent-sp", spec, "team-c")
|
|
989
|
+
provider.configure("agent-sp", spec, pr)
|
|
990
|
+
|
|
991
|
+
with patch(
|
|
992
|
+
"skcapstone.providers.local._find_crush_binary",
|
|
993
|
+
return_value="/bin/claude",
|
|
994
|
+
):
|
|
995
|
+
provider.start("agent-sp", pr)
|
|
996
|
+
|
|
997
|
+
cmd = mock_popen.call_args[0][0]
|
|
998
|
+
assert "--system-prompt" in cmd
|
|
999
|
+
|
|
1000
|
+
def test_claude_cmd_includes_output_format(self, provider, tmp_path, _patched_popen):
|
|
1001
|
+
mock_popen, _ = _patched_popen
|
|
1002
|
+
spec = _make_spec()
|
|
1003
|
+
pr = provider.provision("agent-of", spec, "team-c")
|
|
1004
|
+
provider.configure("agent-of", spec, pr)
|
|
1005
|
+
|
|
1006
|
+
with patch(
|
|
1007
|
+
"skcapstone.providers.local._find_crush_binary",
|
|
1008
|
+
return_value="/bin/claude",
|
|
1009
|
+
):
|
|
1010
|
+
provider.start("agent-of", pr)
|
|
1011
|
+
|
|
1012
|
+
cmd = mock_popen.call_args[0][0]
|
|
1013
|
+
of_idx = cmd.index("--output-format")
|
|
1014
|
+
assert cmd[of_idx + 1] == "stream-json"
|
|
1015
|
+
|
|
1016
|
+
def test_claude_cmd_includes_session_id(self, provider, tmp_path, _patched_popen):
|
|
1017
|
+
mock_popen, _ = _patched_popen
|
|
1018
|
+
spec = _make_spec()
|
|
1019
|
+
pr = provider.provision("agent-si", spec, "team-c")
|
|
1020
|
+
provider.configure("agent-si", spec, pr)
|
|
1021
|
+
|
|
1022
|
+
with patch(
|
|
1023
|
+
"skcapstone.providers.local._find_crush_binary",
|
|
1024
|
+
return_value="/bin/claude",
|
|
1025
|
+
):
|
|
1026
|
+
provider.start("agent-si", pr)
|
|
1027
|
+
|
|
1028
|
+
cmd = mock_popen.call_args[0][0]
|
|
1029
|
+
assert "--session-id" in cmd
|
|
1030
|
+
|
|
1031
|
+
def test_claude_cmd_includes_dangerously_skip_permissions(
|
|
1032
|
+
self, provider, tmp_path, _patched_popen
|
|
1033
|
+
):
|
|
1034
|
+
mock_popen, _ = _patched_popen
|
|
1035
|
+
spec = _make_spec()
|
|
1036
|
+
pr = provider.provision("agent-dsp", spec, "team-c")
|
|
1037
|
+
provider.configure("agent-dsp", spec, pr)
|
|
1038
|
+
|
|
1039
|
+
with patch(
|
|
1040
|
+
"skcapstone.providers.local._find_crush_binary",
|
|
1041
|
+
return_value="/bin/claude",
|
|
1042
|
+
):
|
|
1043
|
+
provider.start("agent-dsp", pr)
|
|
1044
|
+
|
|
1045
|
+
cmd = mock_popen.call_args[0][0]
|
|
1046
|
+
assert "--dangerously-skip-permissions" in cmd
|
|
1047
|
+
|
|
1048
|
+
def test_sets_pid_in_provision_result(self, provider, tmp_path, _patched_popen):
|
|
1049
|
+
_, mock_proc = _patched_popen
|
|
1050
|
+
mock_proc.pid = 44444
|
|
1051
|
+
spec = _make_spec()
|
|
1052
|
+
pr = provider.provision("agent-cpid", spec, "team-c")
|
|
1053
|
+
provider.configure("agent-cpid", spec, pr)
|
|
1054
|
+
|
|
1055
|
+
with patch(
|
|
1056
|
+
"skcapstone.providers.local._find_crush_binary",
|
|
1057
|
+
return_value="/bin/claude",
|
|
1058
|
+
):
|
|
1059
|
+
provider.start("agent-cpid", pr)
|
|
1060
|
+
|
|
1061
|
+
assert pr["pid"] == 44444
|
|
1062
|
+
|
|
1063
|
+
def test_writes_session_state_with_claude_backend(
|
|
1064
|
+
self, provider, tmp_path, _patched_popen
|
|
1065
|
+
):
|
|
1066
|
+
_, mock_proc = _patched_popen
|
|
1067
|
+
mock_proc.pid = 55000
|
|
1068
|
+
spec = _make_spec()
|
|
1069
|
+
pr = provider.provision("agent-csb", spec, "team-c")
|
|
1070
|
+
provider.configure("agent-csb", spec, pr)
|
|
1071
|
+
|
|
1072
|
+
with patch(
|
|
1073
|
+
"skcapstone.providers.local._find_crush_binary",
|
|
1074
|
+
return_value="/bin/claude",
|
|
1075
|
+
):
|
|
1076
|
+
provider.start("agent-csb", pr)
|
|
1077
|
+
|
|
1078
|
+
state = json.loads(
|
|
1079
|
+
(Path(pr["work_dir"]) / _SESSION_STATE_FILE).read_text()
|
|
1080
|
+
)
|
|
1081
|
+
assert state["status"] == _STATE_RUNNING
|
|
1082
|
+
assert state["backend"] == "claude"
|
|
1083
|
+
assert state["pid"] == 55000
|
|
1084
|
+
|
|
1085
|
+
def test_returns_false_on_popen_error(self, provider, tmp_path):
|
|
1086
|
+
spec = _make_spec()
|
|
1087
|
+
pr = provider.provision("agent-cerr", spec, "team-c")
|
|
1088
|
+
provider.configure("agent-cerr", spec, pr)
|
|
1089
|
+
|
|
1090
|
+
with patch(
|
|
1091
|
+
"skcapstone.providers.local._find_crush_binary",
|
|
1092
|
+
return_value="/bin/claude",
|
|
1093
|
+
):
|
|
1094
|
+
with patch(
|
|
1095
|
+
"subprocess.Popen", side_effect=OSError("not found")
|
|
1096
|
+
):
|
|
1097
|
+
result = provider.start("agent-cerr", pr)
|
|
1098
|
+
|
|
1099
|
+
assert result is False
|
|
1100
|
+
|
|
1101
|
+
|
|
1102
|
+
# ---------------------------------------------------------------------------
|
|
1103
|
+
# Claude session environment
|
|
1104
|
+
# ---------------------------------------------------------------------------
|
|
1105
|
+
|
|
1106
|
+
|
|
1107
|
+
class TestClaudeSessionEnv:
|
|
1108
|
+
"""Tests for claude session environment variable passing."""
|
|
1109
|
+
|
|
1110
|
+
@pytest.fixture()
|
|
1111
|
+
def _patched_popen(self):
|
|
1112
|
+
mock_proc = MagicMock()
|
|
1113
|
+
mock_proc.pid = 66000
|
|
1114
|
+
with patch("subprocess.Popen", return_value=mock_proc) as mock_popen:
|
|
1115
|
+
yield mock_popen, mock_proc
|
|
1116
|
+
|
|
1117
|
+
def test_passes_agent_name_in_env(self, provider, _patched_popen):
|
|
1118
|
+
mock_popen, _ = _patched_popen
|
|
1119
|
+
spec = _make_spec()
|
|
1120
|
+
pr = provider.provision("env-agent", spec, "team-env")
|
|
1121
|
+
provider.configure("env-agent", spec, pr)
|
|
1122
|
+
|
|
1123
|
+
with patch(
|
|
1124
|
+
"skcapstone.providers.local._find_crush_binary",
|
|
1125
|
+
return_value="/bin/claude",
|
|
1126
|
+
):
|
|
1127
|
+
provider.start("env-agent", pr)
|
|
1128
|
+
|
|
1129
|
+
env = mock_popen.call_args[1]["env"]
|
|
1130
|
+
assert env["AGENT_NAME"] == "env-agent"
|
|
1131
|
+
|
|
1132
|
+
def test_passes_model_in_env(self, provider, _patched_popen):
|
|
1133
|
+
mock_popen, _ = _patched_popen
|
|
1134
|
+
spec = _make_spec(model="reason")
|
|
1135
|
+
pr = provider.provision("env-model", spec, "team-env")
|
|
1136
|
+
provider.configure("env-model", spec, pr)
|
|
1137
|
+
|
|
1138
|
+
with patch(
|
|
1139
|
+
"skcapstone.providers.local._find_crush_binary",
|
|
1140
|
+
return_value="/bin/claude",
|
|
1141
|
+
):
|
|
1142
|
+
provider.start("env-model", pr)
|
|
1143
|
+
|
|
1144
|
+
env = mock_popen.call_args[1]["env"]
|
|
1145
|
+
assert env["AGENT_MODEL_TIER"] == "reason"
|
|
1146
|
+
|
|
1147
|
+
def test_passes_soul_in_env(self, provider, _patched_popen):
|
|
1148
|
+
mock_popen, _ = _patched_popen
|
|
1149
|
+
spec = _make_spec(soul_blueprint="lumina")
|
|
1150
|
+
pr = provider.provision("env-soul", spec, "team-env")
|
|
1151
|
+
provider.configure("env-soul", spec, pr)
|
|
1152
|
+
|
|
1153
|
+
with patch(
|
|
1154
|
+
"skcapstone.providers.local._find_crush_binary",
|
|
1155
|
+
return_value="/bin/claude",
|
|
1156
|
+
):
|
|
1157
|
+
provider.start("env-soul", pr)
|
|
1158
|
+
|
|
1159
|
+
env = mock_popen.call_args[1]["env"]
|
|
1160
|
+
assert "SOUL_BLUEPRINT" in env
|
|
1161
|
+
assert "lumina" in env["SOUL_BLUEPRINT"].lower() or env["SOUL_BLUEPRINT"] == "lumina"
|
|
1162
|
+
|
|
1163
|
+
def test_passes_skills_as_json_in_env(self, provider, _patched_popen):
|
|
1164
|
+
mock_popen, _ = _patched_popen
|
|
1165
|
+
spec = _make_spec(skills=["code-review", "docs"])
|
|
1166
|
+
pr = provider.provision("env-skills", spec, "team-env")
|
|
1167
|
+
provider.configure("env-skills", spec, pr)
|
|
1168
|
+
|
|
1169
|
+
with patch(
|
|
1170
|
+
"skcapstone.providers.local._find_crush_binary",
|
|
1171
|
+
return_value="/bin/claude",
|
|
1172
|
+
):
|
|
1173
|
+
provider.start("env-skills", pr)
|
|
1174
|
+
|
|
1175
|
+
env = mock_popen.call_args[1]["env"]
|
|
1176
|
+
parsed = json.loads(env["AGENT_SKILLS"])
|
|
1177
|
+
assert "code-review" in parsed
|
|
1178
|
+
assert "docs" in parsed
|
|
1179
|
+
|
|
1180
|
+
|
|
1181
|
+
# ---------------------------------------------------------------------------
|
|
1182
|
+
# Fallback chain priority: crush → claude → stub
|
|
1183
|
+
# ---------------------------------------------------------------------------
|
|
1184
|
+
|
|
1185
|
+
|
|
1186
|
+
class TestStartFallbackChain:
|
|
1187
|
+
"""Tests for the three-tier fallback: crush → claude → stub."""
|
|
1188
|
+
|
|
1189
|
+
@pytest.fixture()
|
|
1190
|
+
def _patched_popen(self):
|
|
1191
|
+
mock_proc = MagicMock()
|
|
1192
|
+
mock_proc.pid = 99000
|
|
1193
|
+
with patch("subprocess.Popen", return_value=mock_proc) as mock_popen:
|
|
1194
|
+
yield mock_popen, mock_proc
|
|
1195
|
+
|
|
1196
|
+
def test_crush_binary_uses_crush_session(self, provider, _patched_popen):
|
|
1197
|
+
mock_popen, _ = _patched_popen
|
|
1198
|
+
spec = _make_spec()
|
|
1199
|
+
pr = provider.provision("fb-crush", spec, "team-fb")
|
|
1200
|
+
provider.configure("fb-crush", spec, pr)
|
|
1201
|
+
|
|
1202
|
+
with patch(
|
|
1203
|
+
"skcapstone.providers.local._find_crush_binary",
|
|
1204
|
+
return_value="/usr/bin/crush",
|
|
1205
|
+
):
|
|
1206
|
+
provider.start("fb-crush", pr)
|
|
1207
|
+
|
|
1208
|
+
cmd = mock_popen.call_args[0][0]
|
|
1209
|
+
assert cmd[0] == "/usr/bin/crush"
|
|
1210
|
+
assert "run" in cmd
|
|
1211
|
+
# Should NOT have -p flag (that's the claude path)
|
|
1212
|
+
assert "-p" not in cmd
|
|
1213
|
+
|
|
1214
|
+
def test_claude_binary_uses_claude_session(self, provider, _patched_popen):
|
|
1215
|
+
mock_popen, _ = _patched_popen
|
|
1216
|
+
spec = _make_spec()
|
|
1217
|
+
pr = provider.provision("fb-claude", spec, "team-fb")
|
|
1218
|
+
provider.configure("fb-claude", spec, pr)
|
|
1219
|
+
|
|
1220
|
+
with patch(
|
|
1221
|
+
"skcapstone.providers.local._find_crush_binary",
|
|
1222
|
+
return_value="/bin/claude",
|
|
1223
|
+
):
|
|
1224
|
+
provider.start("fb-claude", pr)
|
|
1225
|
+
|
|
1226
|
+
cmd = mock_popen.call_args[0][0]
|
|
1227
|
+
assert cmd[0] == "/bin/claude"
|
|
1228
|
+
assert "-p" in cmd
|
|
1229
|
+
|
|
1230
|
+
def test_no_binary_uses_stub(self, provider, _patched_popen):
|
|
1231
|
+
mock_popen, _ = _patched_popen
|
|
1232
|
+
spec = _make_spec()
|
|
1233
|
+
pr = provider.provision("fb-stub", spec, "team-fb")
|
|
1234
|
+
provider.configure("fb-stub", spec, pr)
|
|
1235
|
+
|
|
1236
|
+
with patch(
|
|
1237
|
+
"skcapstone.providers.local._find_crush_binary",
|
|
1238
|
+
return_value=None,
|
|
1239
|
+
):
|
|
1240
|
+
provider.start("fb-stub", pr)
|
|
1241
|
+
|
|
1242
|
+
cmd = mock_popen.call_args[0][0]
|
|
1243
|
+
assert cmd[0] == os.sys.executable
|
|
1244
|
+
assert cmd[1] == "-c"
|
|
1245
|
+
|
|
1246
|
+
def test_lifecycle_with_claude(self, provider):
|
|
1247
|
+
"""End-to-end lifecycle with claude binary."""
|
|
1248
|
+
mock_proc = MagicMock()
|
|
1249
|
+
mock_proc.pid = 98765
|
|
1250
|
+
|
|
1251
|
+
spec = _make_spec(
|
|
1252
|
+
role="coder",
|
|
1253
|
+
model="code",
|
|
1254
|
+
soul_blueprint="lumina",
|
|
1255
|
+
skills=["code-review"],
|
|
1256
|
+
)
|
|
1257
|
+
|
|
1258
|
+
with patch("subprocess.Popen", return_value=mock_proc):
|
|
1259
|
+
with patch(
|
|
1260
|
+
"skcapstone.providers.local._find_crush_binary",
|
|
1261
|
+
return_value="/bin/claude",
|
|
1262
|
+
):
|
|
1263
|
+
pr = provider.provision("claude-lc", spec, "team-lc")
|
|
1264
|
+
provider.configure("claude-lc", spec, pr)
|
|
1265
|
+
started = provider.start("claude-lc", pr)
|
|
1266
|
+
|
|
1267
|
+
assert started is True
|
|
1268
|
+
assert pr["pid"] == 98765
|
|
1269
|
+
|
|
1270
|
+
# Health check
|
|
1271
|
+
with patch("skcapstone.providers.local._pid_is_alive", return_value=True):
|
|
1272
|
+
status = provider.health_check("claude-lc", pr)
|
|
1273
|
+
assert status == AgentStatus.RUNNING
|
|
1274
|
+
|
|
1275
|
+
# Stop
|
|
1276
|
+
alive_seq = iter([True] + [False] * 60)
|
|
1277
|
+
with patch("os.kill"):
|
|
1278
|
+
with patch(
|
|
1279
|
+
"skcapstone.providers.local._pid_is_alive",
|
|
1280
|
+
side_effect=alive_seq,
|
|
1281
|
+
):
|
|
1282
|
+
stopped = provider.stop("claude-lc", pr)
|
|
1283
|
+
assert stopped is True
|