@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
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
"""Tests for the Syncthing setup skill."""
|
|
1
|
+
"""Tests for the Syncthing setup skill — Sovereign Singularity."""
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import xml.etree.ElementTree as ET
|
|
4
4
|
from pathlib import Path
|
|
5
|
+
from unittest.mock import patch
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
5
8
|
|
|
6
9
|
from skcapstone.skills.syncthing_setup import (
|
|
10
|
+
SHARED_FOLDER_ID,
|
|
11
|
+
STIGNORE_CONTENTS,
|
|
12
|
+
configure_syncthing_folder,
|
|
7
13
|
detect_syncthing,
|
|
8
|
-
get_install_instructions,
|
|
9
14
|
ensure_shared_folder,
|
|
15
|
+
get_install_instructions,
|
|
10
16
|
)
|
|
11
17
|
|
|
12
18
|
|
|
@@ -16,7 +22,7 @@ class TestDetectSyncthing:
|
|
|
16
22
|
@patch("shutil.which", return_value="/usr/bin/syncthing")
|
|
17
23
|
def test_found(self, mock_which):
|
|
18
24
|
"""Returns path when syncthing is installed."""
|
|
19
|
-
assert detect_syncthing()
|
|
25
|
+
assert detect_syncthing() is not None
|
|
20
26
|
|
|
21
27
|
@patch("shutil.which", return_value=None)
|
|
22
28
|
def test_not_found(self, mock_which):
|
|
@@ -47,30 +53,170 @@ class TestGetInstallInstructions:
|
|
|
47
53
|
assert "winget" in instructions
|
|
48
54
|
|
|
49
55
|
|
|
56
|
+
def _patch_homes(monkeypatch, tmp_path):
|
|
57
|
+
"""Set AGENT_HOME and SYNC_DIR to a temp directory for testing."""
|
|
58
|
+
agent_home = tmp_path / ".skcapstone"
|
|
59
|
+
sync_dir = agent_home / "sync"
|
|
60
|
+
monkeypatch.setattr("skcapstone.skills.syncthing_setup.AGENT_HOME", agent_home)
|
|
61
|
+
monkeypatch.setattr("skcapstone.skills.syncthing_setup.SYNC_DIR", sync_dir)
|
|
62
|
+
return agent_home, sync_dir
|
|
63
|
+
|
|
64
|
+
|
|
50
65
|
class TestEnsureSharedFolder:
|
|
51
|
-
"""Tests for ensure_shared_folder."""
|
|
66
|
+
"""Tests for ensure_shared_folder — creates the full agent home."""
|
|
52
67
|
|
|
53
|
-
def
|
|
54
|
-
"""Creates
|
|
55
|
-
monkeypatch
|
|
56
|
-
"skcapstone.skills.syncthing_setup.SYNC_DIR",
|
|
57
|
-
tmp_path / "sync",
|
|
58
|
-
)
|
|
59
|
-
from skcapstone.skills.syncthing_setup import ensure_shared_folder
|
|
68
|
+
def test_creates_all_pillar_directories(self, tmp_path, monkeypatch):
|
|
69
|
+
"""Creates every pillar data directory under agent home."""
|
|
70
|
+
agent_home, _ = _patch_homes(monkeypatch, tmp_path)
|
|
60
71
|
|
|
61
72
|
result = ensure_shared_folder()
|
|
62
|
-
|
|
63
|
-
assert
|
|
64
|
-
assert (
|
|
73
|
+
|
|
74
|
+
assert result == agent_home
|
|
75
|
+
assert (agent_home / "identity").is_dir()
|
|
76
|
+
assert (agent_home / "memory" / "short-term").is_dir()
|
|
77
|
+
assert (agent_home / "memory" / "mid-term").is_dir()
|
|
78
|
+
assert (agent_home / "memory" / "long-term").is_dir()
|
|
79
|
+
assert (agent_home / "trust" / "febs").is_dir()
|
|
80
|
+
assert (agent_home / "security").is_dir()
|
|
81
|
+
assert (agent_home / "coordination" / "tasks").is_dir()
|
|
82
|
+
assert (agent_home / "coordination" / "agents").is_dir()
|
|
83
|
+
assert (agent_home / "config").is_dir()
|
|
84
|
+
assert (agent_home / "skills").is_dir()
|
|
85
|
+
|
|
86
|
+
def test_creates_sync_seed_directories(self, tmp_path, monkeypatch):
|
|
87
|
+
"""Creates the sync seed outbox/inbox/archive."""
|
|
88
|
+
agent_home, sync_dir = _patch_homes(monkeypatch, tmp_path)
|
|
89
|
+
|
|
90
|
+
ensure_shared_folder()
|
|
91
|
+
|
|
92
|
+
assert (sync_dir / "outbox").is_dir()
|
|
93
|
+
assert (sync_dir / "inbox").is_dir()
|
|
94
|
+
assert (sync_dir / "archive").is_dir()
|
|
95
|
+
|
|
96
|
+
def test_creates_stignore(self, tmp_path, monkeypatch):
|
|
97
|
+
"""Creates .stignore to protect private keys."""
|
|
98
|
+
agent_home, _ = _patch_homes(monkeypatch, tmp_path)
|
|
99
|
+
|
|
100
|
+
ensure_shared_folder()
|
|
101
|
+
|
|
102
|
+
stignore = agent_home / ".stignore"
|
|
103
|
+
assert stignore.exists()
|
|
104
|
+
contents = stignore.read_text()
|
|
105
|
+
assert "*.key" in contents
|
|
106
|
+
assert "*.pem" in contents
|
|
107
|
+
assert "__pycache__" in contents
|
|
65
108
|
|
|
66
109
|
def test_idempotent(self, tmp_path, monkeypatch):
|
|
67
|
-
"""Calling twice doesn't fail."""
|
|
68
|
-
monkeypatch
|
|
69
|
-
"skcapstone.skills.syncthing_setup.SYNC_DIR",
|
|
70
|
-
tmp_path / "sync",
|
|
71
|
-
)
|
|
72
|
-
from skcapstone.skills.syncthing_setup import ensure_shared_folder
|
|
110
|
+
"""Calling twice doesn't fail or corrupt anything."""
|
|
111
|
+
agent_home, _ = _patch_homes(monkeypatch, tmp_path)
|
|
73
112
|
|
|
74
113
|
ensure_shared_folder()
|
|
75
114
|
ensure_shared_folder()
|
|
76
|
-
|
|
115
|
+
|
|
116
|
+
assert (agent_home / "identity").is_dir()
|
|
117
|
+
assert (agent_home / ".stignore").exists()
|
|
118
|
+
|
|
119
|
+
def test_returns_agent_home_not_sync_dir(self, tmp_path, monkeypatch):
|
|
120
|
+
"""ensure_shared_folder returns agent home (the Syncthing share root)."""
|
|
121
|
+
agent_home, sync_dir = _patch_homes(monkeypatch, tmp_path)
|
|
122
|
+
|
|
123
|
+
result = ensure_shared_folder()
|
|
124
|
+
|
|
125
|
+
assert result == agent_home
|
|
126
|
+
assert result != sync_dir
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class TestConfigureSyncthingFolder:
|
|
130
|
+
"""Tests for configure_syncthing_folder — Syncthing XML config."""
|
|
131
|
+
|
|
132
|
+
def _make_config(self, tmp_path, existing_folder=None):
|
|
133
|
+
"""Create a minimal Syncthing config.xml for testing."""
|
|
134
|
+
config_path = tmp_path / "config.xml"
|
|
135
|
+
root = ET.Element("configuration")
|
|
136
|
+
|
|
137
|
+
if existing_folder:
|
|
138
|
+
folder = ET.SubElement(root, "folder")
|
|
139
|
+
for k, v in existing_folder.items():
|
|
140
|
+
folder.set(k, v)
|
|
141
|
+
|
|
142
|
+
tree = ET.ElementTree(root)
|
|
143
|
+
tree.write(str(config_path), xml_declaration=True)
|
|
144
|
+
return config_path
|
|
145
|
+
|
|
146
|
+
def test_adds_folder_pointing_at_agent_home(self, tmp_path, monkeypatch):
|
|
147
|
+
"""New folder in config points at ~/.skcapstone, not sync/."""
|
|
148
|
+
agent_home, _ = _patch_homes(monkeypatch, tmp_path)
|
|
149
|
+
config_path = self._make_config(tmp_path)
|
|
150
|
+
monkeypatch.setattr(
|
|
151
|
+
"skcapstone.skills.syncthing_setup.SYNCTHING_CONFIG_FILE",
|
|
152
|
+
config_path,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
assert configure_syncthing_folder() is True
|
|
156
|
+
|
|
157
|
+
tree = ET.parse(config_path)
|
|
158
|
+
folders = list(tree.getroot().iter("folder"))
|
|
159
|
+
assert len(folders) == 1
|
|
160
|
+
assert folders[0].get("id") == SHARED_FOLDER_ID
|
|
161
|
+
assert folders[0].get("path") == str(agent_home)
|
|
162
|
+
assert folders[0].get("label") == "SKCapstone Sovereign"
|
|
163
|
+
|
|
164
|
+
def test_upgrades_old_sync_dir_path(self, tmp_path, monkeypatch):
|
|
165
|
+
"""Existing folder pointing at sync/ gets upgraded to agent home."""
|
|
166
|
+
agent_home, sync_dir = _patch_homes(monkeypatch, tmp_path)
|
|
167
|
+
config_path = self._make_config(
|
|
168
|
+
tmp_path,
|
|
169
|
+
existing_folder={
|
|
170
|
+
"id": SHARED_FOLDER_ID,
|
|
171
|
+
"label": "SKCapstone Sync",
|
|
172
|
+
"path": str(sync_dir),
|
|
173
|
+
},
|
|
174
|
+
)
|
|
175
|
+
monkeypatch.setattr(
|
|
176
|
+
"skcapstone.skills.syncthing_setup.SYNCTHING_CONFIG_FILE",
|
|
177
|
+
config_path,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
assert configure_syncthing_folder() is True
|
|
181
|
+
|
|
182
|
+
tree = ET.parse(config_path)
|
|
183
|
+
folder = list(tree.getroot().iter("folder"))[0]
|
|
184
|
+
assert folder.get("path") == str(agent_home)
|
|
185
|
+
assert folder.get("label") == "SKCapstone Sovereign"
|
|
186
|
+
|
|
187
|
+
def test_already_correct_path_is_noop(self, tmp_path, monkeypatch):
|
|
188
|
+
"""Folder already pointing at agent home returns True without writing."""
|
|
189
|
+
agent_home, _ = _patch_homes(monkeypatch, tmp_path)
|
|
190
|
+
config_path = self._make_config(
|
|
191
|
+
tmp_path,
|
|
192
|
+
existing_folder={
|
|
193
|
+
"id": SHARED_FOLDER_ID,
|
|
194
|
+
"path": str(agent_home),
|
|
195
|
+
},
|
|
196
|
+
)
|
|
197
|
+
monkeypatch.setattr(
|
|
198
|
+
"skcapstone.skills.syncthing_setup.SYNCTHING_CONFIG_FILE",
|
|
199
|
+
config_path,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
assert configure_syncthing_folder() is True
|
|
203
|
+
|
|
204
|
+
def test_no_config_file_returns_false(self, tmp_path, monkeypatch):
|
|
205
|
+
"""Returns False when Syncthing config doesn't exist."""
|
|
206
|
+
monkeypatch.setattr(
|
|
207
|
+
"skcapstone.skills.syncthing_setup.SYNCTHING_CONFIG_FILE",
|
|
208
|
+
tmp_path / "nonexistent.xml",
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
assert configure_syncthing_folder() is False
|
|
212
|
+
|
|
213
|
+
def test_corrupt_config_returns_false(self, tmp_path, monkeypatch):
|
|
214
|
+
"""Returns False for unparseable XML."""
|
|
215
|
+
bad_config = tmp_path / "config.xml"
|
|
216
|
+
bad_config.write_text("not xml at all")
|
|
217
|
+
monkeypatch.setattr(
|
|
218
|
+
"skcapstone.skills.syncthing_setup.SYNCTHING_CONFIG_FILE",
|
|
219
|
+
bad_config,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
assert configure_syncthing_folder() is False
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"""Tests for systemd service management module.
|
|
2
|
+
|
|
3
|
+
Tests unit file generation, install/uninstall logic, and status parsing.
|
|
4
|
+
Actual systemctl commands are mocked to avoid system dependencies.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import subprocess
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from unittest.mock import MagicMock, patch
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
from skcapstone.systemd import (
|
|
16
|
+
ALL_UNITS,
|
|
17
|
+
HEARTBEAT_SERVICE,
|
|
18
|
+
HEARTBEAT_TIMER,
|
|
19
|
+
QUEUE_DRAIN_SERVICE,
|
|
20
|
+
QUEUE_DRAIN_TIMER,
|
|
21
|
+
SERVICE_NAME,
|
|
22
|
+
SOCKET_NAME,
|
|
23
|
+
TIMER_UNITS,
|
|
24
|
+
ServiceStatus,
|
|
25
|
+
generate_unit_file,
|
|
26
|
+
install_service,
|
|
27
|
+
service_status,
|
|
28
|
+
systemd_available,
|
|
29
|
+
uninstall_service,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TestGenerateUnitFile:
|
|
34
|
+
"""Tests for unit file generation."""
|
|
35
|
+
|
|
36
|
+
def test_default_unit_file(self) -> None:
|
|
37
|
+
"""Generated unit file contains expected sections and defaults."""
|
|
38
|
+
content = generate_unit_file()
|
|
39
|
+
assert "[Unit]" in content
|
|
40
|
+
assert "[Service]" in content
|
|
41
|
+
assert "[Install]" in content
|
|
42
|
+
assert "ExecStart=skcapstone daemon start --foreground" in content
|
|
43
|
+
assert "Restart=on-failure" in content
|
|
44
|
+
assert "NoNewPrivileges=true" in content
|
|
45
|
+
assert "WantedBy=default.target" in content
|
|
46
|
+
|
|
47
|
+
def test_custom_python_path(self) -> None:
|
|
48
|
+
"""Custom Python path is used in ExecStart."""
|
|
49
|
+
content = generate_unit_file(python_path="/usr/local/bin/skcapstone")
|
|
50
|
+
assert "ExecStart=/usr/local/bin/skcapstone daemon start" in content
|
|
51
|
+
|
|
52
|
+
def test_extra_env_vars(self) -> None:
|
|
53
|
+
"""Extra environment variables are included."""
|
|
54
|
+
content = generate_unit_file(extra_env={"LOG_LEVEL": "debug", "PORT": "8888"})
|
|
55
|
+
assert "Environment=LOG_LEVEL=debug" in content
|
|
56
|
+
assert "Environment=PORT=8888" in content
|
|
57
|
+
|
|
58
|
+
def test_security_hardening_present(self) -> None:
|
|
59
|
+
"""Security directives are in the generated unit."""
|
|
60
|
+
content = generate_unit_file()
|
|
61
|
+
assert "ProtectSystem=strict" in content
|
|
62
|
+
assert "ProtectHome=read-only" in content
|
|
63
|
+
assert "PrivateTmp=true" in content
|
|
64
|
+
assert "ReadWritePaths=" in content
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class TestSystemdAvailable:
|
|
68
|
+
"""Tests for systemd detection."""
|
|
69
|
+
|
|
70
|
+
@patch("skcapstone.systemd._run")
|
|
71
|
+
def test_available_when_systemctl_works(self, mock_run: MagicMock) -> None:
|
|
72
|
+
"""Returns True when systemctl --user works."""
|
|
73
|
+
mock_run.return_value = subprocess.CompletedProcess([], 0, stdout="systemd 256")
|
|
74
|
+
assert systemd_available() is True
|
|
75
|
+
|
|
76
|
+
@patch("skcapstone.systemd._run")
|
|
77
|
+
def test_unavailable_when_systemctl_fails(self, mock_run: MagicMock) -> None:
|
|
78
|
+
"""Returns False when systemctl is missing."""
|
|
79
|
+
mock_run.return_value = subprocess.CompletedProcess([], 1, stdout="")
|
|
80
|
+
assert systemd_available() is False
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class TestInstallService:
|
|
84
|
+
"""Tests for service installation."""
|
|
85
|
+
|
|
86
|
+
@patch("skcapstone.systemd._systemctl")
|
|
87
|
+
def test_install_copies_files(self, mock_ctl: MagicMock, tmp_path: Path) -> None:
|
|
88
|
+
"""Install copies unit files to target directory."""
|
|
89
|
+
mock_ctl.return_value = subprocess.CompletedProcess([], 0)
|
|
90
|
+
|
|
91
|
+
source = tmp_path / "source"
|
|
92
|
+
source.mkdir()
|
|
93
|
+
(source / SERVICE_NAME).write_text("[Unit]\nDescription=Test\n")
|
|
94
|
+
(source / SOCKET_NAME).write_text("[Socket]\nListenStream=127.0.0.1:7777\n")
|
|
95
|
+
|
|
96
|
+
target = tmp_path / "target"
|
|
97
|
+
|
|
98
|
+
result = install_service(
|
|
99
|
+
unit_dir=target, source_dir=source, enable=True, start=True,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
assert result["installed"] is True
|
|
103
|
+
assert (target / SERVICE_NAME).exists()
|
|
104
|
+
assert (target / SOCKET_NAME).exists()
|
|
105
|
+
|
|
106
|
+
@patch("skcapstone.systemd._systemctl")
|
|
107
|
+
def test_install_enables_and_starts(self, mock_ctl: MagicMock, tmp_path: Path) -> None:
|
|
108
|
+
"""Install calls enable and start."""
|
|
109
|
+
mock_ctl.return_value = subprocess.CompletedProcess([], 0)
|
|
110
|
+
|
|
111
|
+
source = tmp_path / "src"
|
|
112
|
+
source.mkdir()
|
|
113
|
+
(source / SERVICE_NAME).write_text("[Unit]\n")
|
|
114
|
+
|
|
115
|
+
result = install_service(
|
|
116
|
+
unit_dir=tmp_path / "tgt", source_dir=source,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
assert result["enabled"] is True
|
|
120
|
+
assert result["started"] is True
|
|
121
|
+
|
|
122
|
+
calls = [c.args[0] for c in mock_ctl.call_args_list]
|
|
123
|
+
enable_calls = [c for c in calls if "enable" in c]
|
|
124
|
+
start_calls = [c for c in calls if "start" in c]
|
|
125
|
+
assert len(enable_calls) >= 1
|
|
126
|
+
assert len(start_calls) >= 1
|
|
127
|
+
|
|
128
|
+
def test_install_missing_source_returns_false(self, tmp_path: Path) -> None:
|
|
129
|
+
"""Install fails gracefully when source unit doesn't exist."""
|
|
130
|
+
result = install_service(
|
|
131
|
+
unit_dir=tmp_path / "tgt",
|
|
132
|
+
source_dir=tmp_path / "nonexistent",
|
|
133
|
+
)
|
|
134
|
+
assert result["installed"] is False
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class TestUninstallService:
|
|
138
|
+
"""Tests for service uninstallation."""
|
|
139
|
+
|
|
140
|
+
@patch("skcapstone.systemd._systemctl")
|
|
141
|
+
def test_uninstall_removes_files(self, mock_ctl: MagicMock, tmp_path: Path) -> None:
|
|
142
|
+
"""Uninstall removes unit files from target directory."""
|
|
143
|
+
mock_ctl.return_value = subprocess.CompletedProcess([], 0)
|
|
144
|
+
|
|
145
|
+
(tmp_path / SERVICE_NAME).write_text("[Unit]\n")
|
|
146
|
+
(tmp_path / SOCKET_NAME).write_text("[Socket]\n")
|
|
147
|
+
|
|
148
|
+
result = uninstall_service(unit_dir=tmp_path)
|
|
149
|
+
|
|
150
|
+
assert result["stopped"] is True
|
|
151
|
+
assert result["disabled"] is True
|
|
152
|
+
assert result["removed"] is True
|
|
153
|
+
assert not (tmp_path / SERVICE_NAME).exists()
|
|
154
|
+
assert not (tmp_path / SOCKET_NAME).exists()
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class TestServiceStatus:
|
|
158
|
+
"""Tests for status querying."""
|
|
159
|
+
|
|
160
|
+
def test_status_not_installed(self, tmp_path: Path) -> None:
|
|
161
|
+
"""Status reports not installed when unit file is missing."""
|
|
162
|
+
with patch("skcapstone.systemd.SYSTEMD_USER_DIR", tmp_path / "nonexistent"):
|
|
163
|
+
status = service_status()
|
|
164
|
+
assert status.installed is False
|
|
165
|
+
assert status.active is False
|
|
166
|
+
|
|
167
|
+
@patch("skcapstone.systemd._systemctl")
|
|
168
|
+
def test_status_running(self, mock_ctl: MagicMock, tmp_path: Path) -> None:
|
|
169
|
+
"""Status reports active when service is running."""
|
|
170
|
+
(tmp_path / SERVICE_NAME).write_text("[Unit]\n")
|
|
171
|
+
|
|
172
|
+
def side_effect(*args):
|
|
173
|
+
cmd = args[0] if args else ""
|
|
174
|
+
if cmd == "is-enabled":
|
|
175
|
+
return subprocess.CompletedProcess([], 0, stdout="enabled\n")
|
|
176
|
+
if cmd == "is-active":
|
|
177
|
+
return subprocess.CompletedProcess([], 0, stdout="active\n")
|
|
178
|
+
if cmd == "show":
|
|
179
|
+
return subprocess.CompletedProcess(
|
|
180
|
+
[], 0,
|
|
181
|
+
stdout="MainPID=12345\nActiveEnterTimestamp=Mon 2026-02-24 05:00:00 UTC\nMemoryCurrent=52428800\nExecMainStatus=0\n",
|
|
182
|
+
)
|
|
183
|
+
return subprocess.CompletedProcess([], 0, stdout="")
|
|
184
|
+
|
|
185
|
+
mock_ctl.side_effect = side_effect
|
|
186
|
+
|
|
187
|
+
with patch("skcapstone.systemd.SYSTEMD_USER_DIR", tmp_path):
|
|
188
|
+
status = service_status()
|
|
189
|
+
|
|
190
|
+
assert status.installed is True
|
|
191
|
+
assert status.enabled is True
|
|
192
|
+
assert status.active is True
|
|
193
|
+
assert status.pid == 12345
|
|
194
|
+
assert "50.0 MB" in status.memory
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class TestServiceStatusModel:
|
|
198
|
+
"""Tests for the ServiceStatus dataclass."""
|
|
199
|
+
|
|
200
|
+
def test_defaults(self) -> None:
|
|
201
|
+
"""Default status is all-false."""
|
|
202
|
+
s = ServiceStatus()
|
|
203
|
+
assert s.installed is False
|
|
204
|
+
assert s.enabled is False
|
|
205
|
+
assert s.active is False
|
|
206
|
+
assert s.pid == 0
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class TestUnitConstants:
|
|
210
|
+
"""Tests for unit file constants and bundled files."""
|
|
211
|
+
|
|
212
|
+
def test_all_units_includes_timers(self) -> None:
|
|
213
|
+
"""ALL_UNITS includes heartbeat and queue drain timers."""
|
|
214
|
+
assert HEARTBEAT_TIMER in ALL_UNITS
|
|
215
|
+
assert QUEUE_DRAIN_TIMER in ALL_UNITS
|
|
216
|
+
assert HEARTBEAT_SERVICE in ALL_UNITS
|
|
217
|
+
assert QUEUE_DRAIN_SERVICE in ALL_UNITS
|
|
218
|
+
|
|
219
|
+
def test_timer_units_list(self) -> None:
|
|
220
|
+
"""TIMER_UNITS contains exactly the two timers."""
|
|
221
|
+
assert len(TIMER_UNITS) == 2
|
|
222
|
+
assert HEARTBEAT_TIMER in TIMER_UNITS
|
|
223
|
+
assert QUEUE_DRAIN_TIMER in TIMER_UNITS
|
|
224
|
+
|
|
225
|
+
def test_all_units_count(self) -> None:
|
|
226
|
+
"""ALL_UNITS has the expected number of units."""
|
|
227
|
+
assert len(ALL_UNITS) == 6
|
|
228
|
+
|
|
229
|
+
def test_bundled_service_file_exists(self) -> None:
|
|
230
|
+
"""The bundled skcapstone.service file exists."""
|
|
231
|
+
from skcapstone.systemd import BUNDLED_DIR
|
|
232
|
+
assert (BUNDLED_DIR / SERVICE_NAME).exists()
|
|
233
|
+
|
|
234
|
+
def test_bundled_heartbeat_timer_exists(self) -> None:
|
|
235
|
+
"""The bundled heartbeat timer file exists."""
|
|
236
|
+
from skcapstone.systemd import BUNDLED_DIR
|
|
237
|
+
assert (BUNDLED_DIR / HEARTBEAT_TIMER).exists()
|
|
238
|
+
|
|
239
|
+
def test_bundled_queue_drain_timer_exists(self) -> None:
|
|
240
|
+
"""The bundled queue drain timer file exists."""
|
|
241
|
+
from skcapstone.systemd import BUNDLED_DIR
|
|
242
|
+
assert (BUNDLED_DIR / QUEUE_DRAIN_TIMER).exists()
|
|
243
|
+
|
|
244
|
+
def test_bundled_heartbeat_service_exists(self) -> None:
|
|
245
|
+
"""The bundled heartbeat service file exists."""
|
|
246
|
+
from skcapstone.systemd import BUNDLED_DIR
|
|
247
|
+
assert (BUNDLED_DIR / HEARTBEAT_SERVICE).exists()
|
|
248
|
+
|
|
249
|
+
def test_bundled_queue_drain_service_exists(self) -> None:
|
|
250
|
+
"""The bundled queue drain service file exists."""
|
|
251
|
+
from skcapstone.systemd import BUNDLED_DIR
|
|
252
|
+
assert (BUNDLED_DIR / QUEUE_DRAIN_SERVICE).exists()
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class TestTimerInstall:
|
|
256
|
+
"""Tests for timer unit installation alongside the main service."""
|
|
257
|
+
|
|
258
|
+
@patch("skcapstone.systemd._systemctl")
|
|
259
|
+
def test_install_copies_all_units(self, mock_ctl: MagicMock, tmp_path: Path) -> None:
|
|
260
|
+
"""Install copies service, socket, and timer units."""
|
|
261
|
+
mock_ctl.return_value = subprocess.CompletedProcess([], 0)
|
|
262
|
+
|
|
263
|
+
source = tmp_path / "source"
|
|
264
|
+
source.mkdir()
|
|
265
|
+
for name in ALL_UNITS:
|
|
266
|
+
(source / name).write_text(f"[Unit]\nDescription={name}\n")
|
|
267
|
+
|
|
268
|
+
target = tmp_path / "target"
|
|
269
|
+
result = install_service(unit_dir=target, source_dir=source)
|
|
270
|
+
|
|
271
|
+
assert result["installed"] is True
|
|
272
|
+
assert result["timers_enabled"] is True
|
|
273
|
+
for name in ALL_UNITS:
|
|
274
|
+
assert (target / name).exists(), f"{name} not copied"
|
|
275
|
+
|
|
276
|
+
@patch("skcapstone.systemd._systemctl")
|
|
277
|
+
def test_install_enables_timers(self, mock_ctl: MagicMock, tmp_path: Path) -> None:
|
|
278
|
+
"""Install enables timer units."""
|
|
279
|
+
mock_ctl.return_value = subprocess.CompletedProcess([], 0)
|
|
280
|
+
|
|
281
|
+
source = tmp_path / "src"
|
|
282
|
+
source.mkdir()
|
|
283
|
+
for name in ALL_UNITS:
|
|
284
|
+
(source / name).write_text("[Unit]\n")
|
|
285
|
+
|
|
286
|
+
install_service(unit_dir=tmp_path / "tgt", source_dir=source)
|
|
287
|
+
|
|
288
|
+
enable_calls = [
|
|
289
|
+
c.args[0] for c in mock_ctl.call_args_list
|
|
290
|
+
if len(c.args) > 0 and c.args[0] == "enable"
|
|
291
|
+
]
|
|
292
|
+
assert len(enable_calls) >= 3
|
|
293
|
+
|
|
294
|
+
@patch("skcapstone.systemd._systemctl")
|
|
295
|
+
def test_uninstall_removes_all_units(self, mock_ctl: MagicMock, tmp_path: Path) -> None:
|
|
296
|
+
"""Uninstall removes all unit files including timers."""
|
|
297
|
+
mock_ctl.return_value = subprocess.CompletedProcess([], 0)
|
|
298
|
+
|
|
299
|
+
for name in ALL_UNITS:
|
|
300
|
+
(tmp_path / name).write_text("[Unit]\n")
|
|
301
|
+
|
|
302
|
+
result = uninstall_service(unit_dir=tmp_path)
|
|
303
|
+
|
|
304
|
+
assert result["removed"] is True
|
|
305
|
+
for name in ALL_UNITS:
|
|
306
|
+
assert not (tmp_path / name).exists(), f"{name} not removed"
|
|
307
|
+
|
|
308
|
+
@patch("skcapstone.systemd._systemctl")
|
|
309
|
+
def test_uninstall_stops_timers_before_service(self, mock_ctl: MagicMock, tmp_path: Path) -> None:
|
|
310
|
+
"""Uninstall stops timers before stopping the main service."""
|
|
311
|
+
calls: list[tuple] = []
|
|
312
|
+
|
|
313
|
+
def track(*args):
|
|
314
|
+
calls.append(args)
|
|
315
|
+
return subprocess.CompletedProcess([], 0)
|
|
316
|
+
|
|
317
|
+
mock_ctl.side_effect = track
|
|
318
|
+
(tmp_path / SERVICE_NAME).write_text("[Unit]\n")
|
|
319
|
+
|
|
320
|
+
uninstall_service(unit_dir=tmp_path)
|
|
321
|
+
|
|
322
|
+
stop_calls = [c[0] for c in calls if c[0] == "stop"]
|
|
323
|
+
assert len(stop_calls) >= 3
|