@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,333 @@
|
|
|
1
|
+
"""Tests for LLM token usage tracking."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import threading
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from skcapstone.usage import (
|
|
12
|
+
DailyUsageReport,
|
|
13
|
+
ModelUsageSummary,
|
|
14
|
+
UsageTracker,
|
|
15
|
+
_cost_per_million,
|
|
16
|
+
_today_str,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
# Fixtures
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@pytest.fixture
|
|
26
|
+
def home(tmp_path: Path) -> Path:
|
|
27
|
+
"""Minimal agent home directory."""
|
|
28
|
+
return tmp_path
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.fixture
|
|
32
|
+
def tracker(home: Path) -> UsageTracker:
|
|
33
|
+
"""UsageTracker backed by a temp directory."""
|
|
34
|
+
return UsageTracker(home)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
# Cost table
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class TestCostTable:
|
|
43
|
+
"""Unit tests for _cost_per_million()."""
|
|
44
|
+
|
|
45
|
+
def test_claude_sonnet_priced(self) -> None:
|
|
46
|
+
"""Claude Sonnet returns non-zero pricing."""
|
|
47
|
+
inp, out = _cost_per_million("claude-sonnet-4-6")
|
|
48
|
+
assert inp > 0
|
|
49
|
+
assert out > inp # output always more expensive
|
|
50
|
+
|
|
51
|
+
def test_ollama_free(self) -> None:
|
|
52
|
+
"""Ollama / local models have zero cost."""
|
|
53
|
+
inp, out = _cost_per_million("ollama:llama3.1")
|
|
54
|
+
assert inp == 0.0
|
|
55
|
+
assert out == 0.0
|
|
56
|
+
|
|
57
|
+
def test_passthrough_free(self) -> None:
|
|
58
|
+
"""Passthrough backend is always free."""
|
|
59
|
+
inp, out = _cost_per_million("passthrough")
|
|
60
|
+
assert inp == 0.0
|
|
61
|
+
assert out == 0.0
|
|
62
|
+
|
|
63
|
+
def test_unknown_model_has_nonzero_fallback(self) -> None:
|
|
64
|
+
"""Unknown models get a conservative non-zero price."""
|
|
65
|
+
inp, out = _cost_per_million("some-unknown-model-xyz")
|
|
66
|
+
assert inp > 0
|
|
67
|
+
assert out > 0
|
|
68
|
+
|
|
69
|
+
def test_gpt4o_priced(self) -> None:
|
|
70
|
+
"""GPT-4o returns positive pricing."""
|
|
71
|
+
inp, out = _cost_per_million("gpt-4o")
|
|
72
|
+
assert inp > 0
|
|
73
|
+
assert out > inp
|
|
74
|
+
|
|
75
|
+
def test_claude_opus_more_expensive_than_haiku(self) -> None:
|
|
76
|
+
"""Opus costs more per token than Haiku."""
|
|
77
|
+
opus_inp, opus_out = _cost_per_million("claude-opus-4-6")
|
|
78
|
+
haiku_inp, haiku_out = _cost_per_million("claude-haiku-4-5")
|
|
79
|
+
assert opus_inp > haiku_inp
|
|
80
|
+
assert opus_out > haiku_out
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# ---------------------------------------------------------------------------
|
|
84
|
+
# UsageTracker.record_usage
|
|
85
|
+
# ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class TestRecordUsage:
|
|
89
|
+
"""Tests for the write path."""
|
|
90
|
+
|
|
91
|
+
def test_creates_usage_file(self, tracker: UsageTracker, home: Path) -> None:
|
|
92
|
+
"""record_usage creates tokens-{date}.json."""
|
|
93
|
+
date_str = "2026-03-02"
|
|
94
|
+
tracker.record_usage("ollama:llama3.1", 100, 50, date_str=date_str)
|
|
95
|
+
path = home / "usage" / f"tokens-{date_str}.json"
|
|
96
|
+
assert path.exists()
|
|
97
|
+
|
|
98
|
+
def test_accumulates_calls(self, tracker: UsageTracker) -> None:
|
|
99
|
+
"""Multiple record_usage calls accumulate counters."""
|
|
100
|
+
date_str = "2026-03-02"
|
|
101
|
+
for _ in range(5):
|
|
102
|
+
tracker.record_usage("ollama:llama3.1", 100, 50, date_str=date_str)
|
|
103
|
+
report = tracker.get_daily(date_str)
|
|
104
|
+
summary = report.models["ollama:llama3.1"]
|
|
105
|
+
assert summary.calls == 5
|
|
106
|
+
assert summary.input_tokens == 500
|
|
107
|
+
assert summary.output_tokens == 250
|
|
108
|
+
|
|
109
|
+
def test_multiple_models_tracked_separately(self, tracker: UsageTracker) -> None:
|
|
110
|
+
"""Different models accumulate independently."""
|
|
111
|
+
date_str = "2026-03-02"
|
|
112
|
+
tracker.record_usage("ollama:llama3.1", 100, 50, date_str=date_str)
|
|
113
|
+
tracker.record_usage("claude-sonnet-4-6", 200, 80, date_str=date_str)
|
|
114
|
+
report = tracker.get_daily(date_str)
|
|
115
|
+
assert "ollama:llama3.1" in report.models
|
|
116
|
+
assert "claude-sonnet-4-6" in report.models
|
|
117
|
+
assert report.models["ollama:llama3.1"].calls == 1
|
|
118
|
+
assert report.models["claude-sonnet-4-6"].calls == 1
|
|
119
|
+
|
|
120
|
+
def test_cost_zero_for_local_model(self, tracker: UsageTracker) -> None:
|
|
121
|
+
"""Local ollama models accumulate zero estimated cost."""
|
|
122
|
+
date_str = "2026-03-02"
|
|
123
|
+
tracker.record_usage("ollama:llama3.1", 10_000, 5_000, date_str=date_str)
|
|
124
|
+
report = tracker.get_daily(date_str)
|
|
125
|
+
assert report.models["ollama:llama3.1"].estimated_cost_usd == 0.0
|
|
126
|
+
|
|
127
|
+
def test_cost_nonzero_for_paid_model(self, tracker: UsageTracker) -> None:
|
|
128
|
+
"""Paid models accumulate a positive estimated cost."""
|
|
129
|
+
date_str = "2026-03-02"
|
|
130
|
+
tracker.record_usage("claude-sonnet-4-6", 1_000_000, 100_000, date_str=date_str)
|
|
131
|
+
report = tracker.get_daily(date_str)
|
|
132
|
+
assert report.models["claude-sonnet-4-6"].estimated_cost_usd > 0
|
|
133
|
+
|
|
134
|
+
def test_json_file_readable(self, tracker: UsageTracker, home: Path) -> None:
|
|
135
|
+
"""Persisted file is valid JSON."""
|
|
136
|
+
date_str = "2026-03-02"
|
|
137
|
+
tracker.record_usage("ollama:llama3.1", 100, 50, date_str=date_str)
|
|
138
|
+
path = home / "usage" / f"tokens-{date_str}.json"
|
|
139
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
140
|
+
assert "models" in data
|
|
141
|
+
assert "ollama:llama3.1" in data["models"]
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# ---------------------------------------------------------------------------
|
|
145
|
+
# UsageTracker.get_daily
|
|
146
|
+
# ---------------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class TestGetDaily:
|
|
150
|
+
"""Tests for the daily read path."""
|
|
151
|
+
|
|
152
|
+
def test_empty_day_returns_report(self, tracker: UsageTracker) -> None:
|
|
153
|
+
"""get_daily for a day with no data returns an empty report."""
|
|
154
|
+
report = tracker.get_daily("2099-01-01")
|
|
155
|
+
assert isinstance(report, DailyUsageReport)
|
|
156
|
+
assert report.date == "2099-01-01"
|
|
157
|
+
assert report.models == {}
|
|
158
|
+
|
|
159
|
+
def test_total_tokens_property(self, tracker: UsageTracker) -> None:
|
|
160
|
+
"""total_tokens sums input + output across all models."""
|
|
161
|
+
date_str = "2026-03-02"
|
|
162
|
+
tracker.record_usage("ollama:llama3.1", 100, 40, date_str=date_str)
|
|
163
|
+
tracker.record_usage("claude-sonnet-4-6", 200, 60, date_str=date_str)
|
|
164
|
+
report = tracker.get_daily(date_str)
|
|
165
|
+
assert report.total_input_tokens == 300
|
|
166
|
+
assert report.total_output_tokens == 100
|
|
167
|
+
assert report.total_tokens == 400
|
|
168
|
+
|
|
169
|
+
def test_corrupt_file_returns_empty(self, home: Path) -> None:
|
|
170
|
+
"""A corrupt JSON file returns an empty report instead of crashing."""
|
|
171
|
+
date_str = "2026-03-02"
|
|
172
|
+
usage_dir = home / "usage"
|
|
173
|
+
usage_dir.mkdir(parents=True)
|
|
174
|
+
(usage_dir / f"tokens-{date_str}.json").write_text("not json {{{", encoding="utf-8")
|
|
175
|
+
tracker = UsageTracker(home)
|
|
176
|
+
report = tracker.get_daily(date_str)
|
|
177
|
+
assert isinstance(report, DailyUsageReport)
|
|
178
|
+
assert report.models == {}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# ---------------------------------------------------------------------------
|
|
182
|
+
# UsageTracker.get_weekly / get_monthly
|
|
183
|
+
# ---------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class TestRangeQueries:
|
|
187
|
+
"""Tests for weekly and monthly range queries."""
|
|
188
|
+
|
|
189
|
+
def test_weekly_returns_7_days(self, tracker: UsageTracker) -> None:
|
|
190
|
+
"""get_weekly returns exactly 7 DailyUsageReport objects."""
|
|
191
|
+
reports = tracker.get_weekly()
|
|
192
|
+
assert len(reports) == 7
|
|
193
|
+
|
|
194
|
+
def test_monthly_returns_30_days(self, tracker: UsageTracker) -> None:
|
|
195
|
+
"""get_monthly returns exactly 30 DailyUsageReport objects."""
|
|
196
|
+
reports = tracker.get_monthly()
|
|
197
|
+
assert len(reports) == 30
|
|
198
|
+
|
|
199
|
+
def test_weekly_includes_data(self, tracker: UsageTracker) -> None:
|
|
200
|
+
"""get_weekly includes a day that has data recorded."""
|
|
201
|
+
from datetime import date, timedelta
|
|
202
|
+
today = date.today().strftime("%Y-%m-%d")
|
|
203
|
+
tracker.record_usage("ollama:llama3.1", 100, 50, date_str=today)
|
|
204
|
+
reports = tracker.get_weekly()
|
|
205
|
+
total = sum(r.total_calls for r in reports)
|
|
206
|
+
assert total == 1
|
|
207
|
+
|
|
208
|
+
def test_aggregate_sums_correctly(self, tracker: UsageTracker) -> None:
|
|
209
|
+
"""aggregate() totals across multiple days."""
|
|
210
|
+
tracker.record_usage("ollama:llama3.1", 100, 50, date_str="2026-03-01")
|
|
211
|
+
tracker.record_usage("ollama:llama3.1", 200, 80, date_str="2026-03-02")
|
|
212
|
+
reports = [tracker.get_daily("2026-03-01"), tracker.get_daily("2026-03-02")]
|
|
213
|
+
agg = tracker.aggregate(reports)
|
|
214
|
+
m = agg.models["ollama:llama3.1"]
|
|
215
|
+
assert m.calls == 2
|
|
216
|
+
assert m.input_tokens == 300
|
|
217
|
+
assert m.output_tokens == 130
|
|
218
|
+
|
|
219
|
+
def test_aggregate_empty_list(self, tracker: UsageTracker) -> None:
|
|
220
|
+
"""aggregate([]) returns a safe empty report."""
|
|
221
|
+
agg = tracker.aggregate([])
|
|
222
|
+
assert agg.date == "empty"
|
|
223
|
+
assert agg.models == {}
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
# ---------------------------------------------------------------------------
|
|
227
|
+
# Thread safety
|
|
228
|
+
# ---------------------------------------------------------------------------
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class TestThreadSafety:
|
|
232
|
+
"""Concurrent record_usage calls must not corrupt data."""
|
|
233
|
+
|
|
234
|
+
def test_concurrent_writes_same_model(self, tracker: UsageTracker) -> None:
|
|
235
|
+
"""100 concurrent record_usage calls produce correct totals."""
|
|
236
|
+
date_str = "2026-03-02"
|
|
237
|
+
n = 100
|
|
238
|
+
barrier = threading.Barrier(n)
|
|
239
|
+
|
|
240
|
+
def _record():
|
|
241
|
+
barrier.wait()
|
|
242
|
+
tracker.record_usage("ollama:llama3.1", 10, 5, date_str=date_str)
|
|
243
|
+
|
|
244
|
+
threads = [threading.Thread(target=_record) for _ in range(n)]
|
|
245
|
+
for t in threads:
|
|
246
|
+
t.start()
|
|
247
|
+
for t in threads:
|
|
248
|
+
t.join()
|
|
249
|
+
|
|
250
|
+
report = tracker.get_daily(date_str)
|
|
251
|
+
m = report.models["ollama:llama3.1"]
|
|
252
|
+
assert m.calls == n
|
|
253
|
+
assert m.input_tokens == n * 10
|
|
254
|
+
assert m.output_tokens == n * 5
|
|
255
|
+
|
|
256
|
+
def test_concurrent_writes_different_models(self, tracker: UsageTracker) -> None:
|
|
257
|
+
"""Concurrent writes to different models don't lose data."""
|
|
258
|
+
date_str = "2026-03-02"
|
|
259
|
+
n = 50
|
|
260
|
+
barrier = threading.Barrier(n * 2)
|
|
261
|
+
|
|
262
|
+
def _record_a():
|
|
263
|
+
barrier.wait()
|
|
264
|
+
tracker.record_usage("model-a", 10, 5, date_str=date_str)
|
|
265
|
+
|
|
266
|
+
def _record_b():
|
|
267
|
+
barrier.wait()
|
|
268
|
+
tracker.record_usage("model-b", 20, 10, date_str=date_str)
|
|
269
|
+
|
|
270
|
+
threads = (
|
|
271
|
+
[threading.Thread(target=_record_a) for _ in range(n)]
|
|
272
|
+
+ [threading.Thread(target=_record_b) for _ in range(n)]
|
|
273
|
+
)
|
|
274
|
+
for t in threads:
|
|
275
|
+
t.start()
|
|
276
|
+
for t in threads:
|
|
277
|
+
t.join()
|
|
278
|
+
|
|
279
|
+
report = tracker.get_daily(date_str)
|
|
280
|
+
assert report.models["model-a"].calls == n
|
|
281
|
+
assert report.models["model-b"].calls == n
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
# ---------------------------------------------------------------------------
|
|
285
|
+
# ModelUsageSummary
|
|
286
|
+
# ---------------------------------------------------------------------------
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
class TestModelUsageSummary:
|
|
290
|
+
"""Unit tests for the ModelUsageSummary model."""
|
|
291
|
+
|
|
292
|
+
def test_total_tokens(self) -> None:
|
|
293
|
+
"""total_tokens sums input and output."""
|
|
294
|
+
m = ModelUsageSummary(
|
|
295
|
+
model="test", calls=1, input_tokens=100, output_tokens=50
|
|
296
|
+
)
|
|
297
|
+
assert m.total_tokens == 150
|
|
298
|
+
|
|
299
|
+
def test_defaults(self) -> None:
|
|
300
|
+
"""All counts default to zero."""
|
|
301
|
+
m = ModelUsageSummary(model="test")
|
|
302
|
+
assert m.calls == 0
|
|
303
|
+
assert m.input_tokens == 0
|
|
304
|
+
assert m.output_tokens == 0
|
|
305
|
+
assert m.estimated_cost_usd == 0.0
|
|
306
|
+
assert m.total_tokens == 0
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
# ---------------------------------------------------------------------------
|
|
310
|
+
# DailyUsageReport
|
|
311
|
+
# ---------------------------------------------------------------------------
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
class TestDailyUsageReport:
|
|
315
|
+
"""Unit tests for the DailyUsageReport model."""
|
|
316
|
+
|
|
317
|
+
def test_empty_report_totals(self) -> None:
|
|
318
|
+
"""Empty report has all-zero aggregates."""
|
|
319
|
+
r = DailyUsageReport(date="2026-03-02")
|
|
320
|
+
assert r.total_calls == 0
|
|
321
|
+
assert r.total_tokens == 0
|
|
322
|
+
assert r.total_cost_usd == 0.0
|
|
323
|
+
|
|
324
|
+
def test_total_cost_aggregates(self) -> None:
|
|
325
|
+
"""total_cost_usd sums across all models."""
|
|
326
|
+
r = DailyUsageReport(
|
|
327
|
+
date="2026-03-02",
|
|
328
|
+
models={
|
|
329
|
+
"m1": ModelUsageSummary(model="m1", estimated_cost_usd=0.10),
|
|
330
|
+
"m2": ModelUsageSummary(model="m2", estimated_cost_usd=0.25),
|
|
331
|
+
},
|
|
332
|
+
)
|
|
333
|
+
assert abs(r.total_cost_usd - 0.35) < 1e-9
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
"""Tests for skcapstone version command and doctor --verbose.
|
|
2
|
+
|
|
3
|
+
Covers:
|
|
4
|
+
- gather_version_info() returns expected keys
|
|
5
|
+
- _check_optional_dep() returns version or None
|
|
6
|
+
- _probe_ollama() running / not-running paths
|
|
7
|
+
- _get_daemon_pid() running / not-running paths
|
|
8
|
+
- version CLI: normal output and --json-out
|
|
9
|
+
- doctor CLI: --verbose mode shows all checks
|
|
10
|
+
- doctor CLI: --verbose with --json-out includes all checks
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from unittest.mock import MagicMock, patch
|
|
18
|
+
|
|
19
|
+
import pytest
|
|
20
|
+
from click.testing import CliRunner
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
# Shared fixture
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@pytest.fixture
|
|
29
|
+
def agent_home(tmp_path: Path) -> Path:
|
|
30
|
+
"""Fully initialised agent home with minimal required files."""
|
|
31
|
+
home = tmp_path / ".skcapstone"
|
|
32
|
+
for d in [
|
|
33
|
+
"identity", "memory", "trust", "security", "sync", "config",
|
|
34
|
+
"memory/short-term", "memory/mid-term", "memory/long-term",
|
|
35
|
+
]:
|
|
36
|
+
(home / d).mkdir(parents=True, exist_ok=True)
|
|
37
|
+
|
|
38
|
+
(home / "manifest.json").write_text(json.dumps({
|
|
39
|
+
"name": "TestAgent", "version": "0.1.0",
|
|
40
|
+
}))
|
|
41
|
+
(home / "identity" / "identity.json").write_text(json.dumps({
|
|
42
|
+
"name": "TestAgent",
|
|
43
|
+
"fingerprint": "DEADBEEF12345678",
|
|
44
|
+
"capauth_managed": True,
|
|
45
|
+
}))
|
|
46
|
+
return home
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
# Unit tests for version_cmd helpers
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class TestCheckOptionalDep:
|
|
55
|
+
"""Tests for _check_optional_dep()."""
|
|
56
|
+
|
|
57
|
+
def test_installed_package_returns_version(self):
|
|
58
|
+
"""Returns version string when package is importable."""
|
|
59
|
+
from skcapstone.cli.version_cmd import _check_optional_dep
|
|
60
|
+
|
|
61
|
+
# 'sys' is always importable; give it a fake __version__ to confirm
|
|
62
|
+
with patch("importlib.import_module") as mock_import:
|
|
63
|
+
mock_mod = MagicMock()
|
|
64
|
+
mock_mod.__version__ = "9.9.9"
|
|
65
|
+
mock_import.return_value = mock_mod
|
|
66
|
+
result = _check_optional_dep("fakepkg")
|
|
67
|
+
|
|
68
|
+
assert result == "9.9.9"
|
|
69
|
+
|
|
70
|
+
def test_missing_package_returns_none(self):
|
|
71
|
+
"""Returns None when package raises ImportError."""
|
|
72
|
+
from skcapstone.cli.version_cmd import _check_optional_dep
|
|
73
|
+
|
|
74
|
+
with patch("importlib.import_module", side_effect=ImportError("no module")):
|
|
75
|
+
result = _check_optional_dep("nonexistent_pkg_xyz")
|
|
76
|
+
|
|
77
|
+
assert result is None
|
|
78
|
+
|
|
79
|
+
def test_package_without_version_attr_returns_installed(self):
|
|
80
|
+
"""Returns 'installed' fallback when __version__ is absent."""
|
|
81
|
+
from skcapstone.cli.version_cmd import _check_optional_dep
|
|
82
|
+
|
|
83
|
+
with patch("importlib.import_module") as mock_import:
|
|
84
|
+
mock_mod = MagicMock(spec=[]) # no attributes
|
|
85
|
+
mock_import.return_value = mock_mod
|
|
86
|
+
result = _check_optional_dep("nover_pkg")
|
|
87
|
+
|
|
88
|
+
assert result == "installed"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class TestProbeOllama:
|
|
92
|
+
"""Tests for _probe_ollama()."""
|
|
93
|
+
|
|
94
|
+
def test_running_returns_models(self):
|
|
95
|
+
"""Running Ollama: running=True, models list populated."""
|
|
96
|
+
from skcapstone.cli.version_cmd import _probe_ollama
|
|
97
|
+
|
|
98
|
+
payload = json.dumps({
|
|
99
|
+
"models": [{"name": "llama3:latest"}, {"name": "phi3:mini"}]
|
|
100
|
+
}).encode()
|
|
101
|
+
mock_resp = MagicMock()
|
|
102
|
+
mock_resp.__enter__ = lambda s: s
|
|
103
|
+
mock_resp.__exit__ = MagicMock(return_value=False)
|
|
104
|
+
mock_resp.read.return_value = payload
|
|
105
|
+
|
|
106
|
+
with patch("urllib.request.urlopen", return_value=mock_resp):
|
|
107
|
+
result = _probe_ollama()
|
|
108
|
+
|
|
109
|
+
assert result["running"] is True
|
|
110
|
+
assert "llama3:latest" in result["models"]
|
|
111
|
+
assert "phi3:mini" in result["models"]
|
|
112
|
+
|
|
113
|
+
def test_not_running_on_connection_error(self):
|
|
114
|
+
"""Connection refused: running=False, models=[]."""
|
|
115
|
+
from skcapstone.cli.version_cmd import _probe_ollama
|
|
116
|
+
|
|
117
|
+
with patch("urllib.request.urlopen", side_effect=OSError("connection refused")):
|
|
118
|
+
result = _probe_ollama()
|
|
119
|
+
|
|
120
|
+
assert result["running"] is False
|
|
121
|
+
assert result["models"] == []
|
|
122
|
+
|
|
123
|
+
def test_host_included_in_result(self, monkeypatch):
|
|
124
|
+
"""Custom OLLAMA_HOST appears in the returned dict."""
|
|
125
|
+
from skcapstone.cli.version_cmd import _probe_ollama
|
|
126
|
+
|
|
127
|
+
monkeypatch.setenv("OLLAMA_HOST", "http://my-server:11434")
|
|
128
|
+
with patch("urllib.request.urlopen", side_effect=OSError):
|
|
129
|
+
result = _probe_ollama()
|
|
130
|
+
|
|
131
|
+
assert result["host"] == "http://my-server:11434"
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class TestGetDaemonPid:
|
|
135
|
+
"""Tests for _get_daemon_pid()."""
|
|
136
|
+
|
|
137
|
+
def test_returns_pid_when_running(self, agent_home: Path):
|
|
138
|
+
"""Returns integer PID when daemon is alive."""
|
|
139
|
+
from skcapstone.cli.version_cmd import _get_daemon_pid
|
|
140
|
+
|
|
141
|
+
with patch("skcapstone.daemon.read_pid", return_value=99999):
|
|
142
|
+
result = _get_daemon_pid(agent_home)
|
|
143
|
+
|
|
144
|
+
assert result == 99999
|
|
145
|
+
|
|
146
|
+
def test_returns_none_when_stopped(self, agent_home: Path):
|
|
147
|
+
"""Returns None when no PID file exists."""
|
|
148
|
+
from skcapstone.cli.version_cmd import _get_daemon_pid
|
|
149
|
+
|
|
150
|
+
with patch("skcapstone.daemon.read_pid", return_value=None):
|
|
151
|
+
result = _get_daemon_pid(agent_home)
|
|
152
|
+
|
|
153
|
+
assert result is None
|
|
154
|
+
|
|
155
|
+
def test_returns_none_on_exception(self, agent_home: Path):
|
|
156
|
+
"""Swallows import or runtime errors, returns None."""
|
|
157
|
+
from skcapstone.cli.version_cmd import _get_daemon_pid
|
|
158
|
+
|
|
159
|
+
with patch("skcapstone.daemon.read_pid", side_effect=RuntimeError("oops")):
|
|
160
|
+
result = _get_daemon_pid(agent_home)
|
|
161
|
+
|
|
162
|
+
assert result is None
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class TestGatherVersionInfo:
|
|
166
|
+
"""Tests for gather_version_info()."""
|
|
167
|
+
|
|
168
|
+
def test_contains_all_expected_keys(self, agent_home: Path):
|
|
169
|
+
"""Dict has all required top-level keys."""
|
|
170
|
+
from skcapstone.cli.version_cmd import gather_version_info
|
|
171
|
+
|
|
172
|
+
with patch("urllib.request.urlopen", side_effect=OSError), \
|
|
173
|
+
patch("skcapstone.daemon.read_pid", return_value=None):
|
|
174
|
+
info = gather_version_info(agent_home)
|
|
175
|
+
|
|
176
|
+
assert "package_version" in info
|
|
177
|
+
assert "python_version" in info
|
|
178
|
+
assert "platform" in info
|
|
179
|
+
assert "optional_deps" in info
|
|
180
|
+
assert "ollama" in info
|
|
181
|
+
assert "daemon_pid" in info
|
|
182
|
+
|
|
183
|
+
def test_optional_deps_has_four_packages(self, agent_home: Path):
|
|
184
|
+
"""optional_deps covers watchdog, skcomm, skchat, skseed."""
|
|
185
|
+
from skcapstone.cli.version_cmd import gather_version_info
|
|
186
|
+
|
|
187
|
+
with patch("urllib.request.urlopen", side_effect=OSError), \
|
|
188
|
+
patch("skcapstone.daemon.read_pid", return_value=None):
|
|
189
|
+
info = gather_version_info(agent_home)
|
|
190
|
+
|
|
191
|
+
deps = info["optional_deps"]
|
|
192
|
+
assert set(deps.keys()) == {"watchdog", "skcomm", "skchat", "skseed"}
|
|
193
|
+
|
|
194
|
+
def test_package_version_matches_module(self, agent_home: Path):
|
|
195
|
+
"""package_version matches skcapstone.__version__."""
|
|
196
|
+
from skcapstone import __version__
|
|
197
|
+
from skcapstone.cli.version_cmd import gather_version_info
|
|
198
|
+
|
|
199
|
+
with patch("urllib.request.urlopen", side_effect=OSError), \
|
|
200
|
+
patch("skcapstone.daemon.read_pid", return_value=None):
|
|
201
|
+
info = gather_version_info(agent_home)
|
|
202
|
+
|
|
203
|
+
assert info["package_version"] == __version__
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
# ---------------------------------------------------------------------------
|
|
207
|
+
# CLI integration tests — version command
|
|
208
|
+
# ---------------------------------------------------------------------------
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class TestVersionCommand:
|
|
212
|
+
"""Integration tests for `skcapstone version`."""
|
|
213
|
+
|
|
214
|
+
def _run(self, args: list[str], agent_home: Path):
|
|
215
|
+
from skcapstone.cli import main
|
|
216
|
+
|
|
217
|
+
runner = CliRunner(mix_stderr=False)
|
|
218
|
+
with patch("urllib.request.urlopen", side_effect=OSError), \
|
|
219
|
+
patch("skcapstone.daemon.read_pid", return_value=None):
|
|
220
|
+
return runner.invoke(
|
|
221
|
+
main,
|
|
222
|
+
["version", "--home", str(agent_home)] + args,
|
|
223
|
+
catch_exceptions=False,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
def test_default_output_contains_version(self, agent_home: Path):
|
|
227
|
+
"""Normal output includes the skcapstone package version."""
|
|
228
|
+
from skcapstone import __version__
|
|
229
|
+
|
|
230
|
+
result = self._run([], agent_home)
|
|
231
|
+
assert result.exit_code == 0, result.output
|
|
232
|
+
assert __version__ in result.output
|
|
233
|
+
|
|
234
|
+
def test_default_output_lists_optional_deps(self, agent_home: Path):
|
|
235
|
+
"""Normal output lists all four optional dep names."""
|
|
236
|
+
result = self._run([], agent_home)
|
|
237
|
+
assert result.exit_code == 0
|
|
238
|
+
for pkg in ("watchdog", "skcomm", "skchat", "skseed"):
|
|
239
|
+
assert pkg in result.output
|
|
240
|
+
|
|
241
|
+
def test_json_output_is_valid_and_complete(self, agent_home: Path):
|
|
242
|
+
"""--json-out emits valid JSON with all required keys."""
|
|
243
|
+
result = self._run(["--json-out"], agent_home)
|
|
244
|
+
assert result.exit_code == 0
|
|
245
|
+
data = json.loads(result.output)
|
|
246
|
+
assert "package_version" in data
|
|
247
|
+
assert "python_version" in data
|
|
248
|
+
assert "optional_deps" in data
|
|
249
|
+
assert "ollama" in data
|
|
250
|
+
assert "daemon_pid" in data
|
|
251
|
+
|
|
252
|
+
def test_daemon_running_shown_in_output(self, agent_home: Path):
|
|
253
|
+
"""Shows running + PID when daemon is alive."""
|
|
254
|
+
from skcapstone.cli import main
|
|
255
|
+
|
|
256
|
+
runner = CliRunner(mix_stderr=False)
|
|
257
|
+
with patch("urllib.request.urlopen", side_effect=OSError), \
|
|
258
|
+
patch("skcapstone.daemon.read_pid", return_value=42001):
|
|
259
|
+
result = runner.invoke(
|
|
260
|
+
main,
|
|
261
|
+
["version", "--home", str(agent_home)],
|
|
262
|
+
catch_exceptions=False,
|
|
263
|
+
)
|
|
264
|
+
assert result.exit_code == 0
|
|
265
|
+
assert "42001" in result.output
|
|
266
|
+
|
|
267
|
+
def test_ollama_running_shows_model_count(self, agent_home: Path):
|
|
268
|
+
"""When Ollama is up, output includes model count."""
|
|
269
|
+
from skcapstone.cli import main
|
|
270
|
+
|
|
271
|
+
payload = json.dumps({
|
|
272
|
+
"models": [{"name": "llama3:latest"}, {"name": "mistral:7b"}]
|
|
273
|
+
}).encode()
|
|
274
|
+
mock_resp = MagicMock()
|
|
275
|
+
mock_resp.__enter__ = lambda s: s
|
|
276
|
+
mock_resp.__exit__ = MagicMock(return_value=False)
|
|
277
|
+
mock_resp.read.return_value = payload
|
|
278
|
+
|
|
279
|
+
runner = CliRunner(mix_stderr=False)
|
|
280
|
+
with patch("urllib.request.urlopen", return_value=mock_resp), \
|
|
281
|
+
patch("skcapstone.daemon.read_pid", return_value=None):
|
|
282
|
+
result = runner.invoke(
|
|
283
|
+
main,
|
|
284
|
+
["version", "--home", str(agent_home)],
|
|
285
|
+
catch_exceptions=False,
|
|
286
|
+
)
|
|
287
|
+
assert result.exit_code == 0
|
|
288
|
+
assert "2 model" in result.output
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
# ---------------------------------------------------------------------------
|
|
292
|
+
# CLI integration tests — doctor --verbose
|
|
293
|
+
# ---------------------------------------------------------------------------
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
class TestDoctorVerbose:
|
|
297
|
+
"""Integration tests for `skcapstone doctor --verbose`."""
|
|
298
|
+
|
|
299
|
+
def _run_doctor(self, args: list[str], agent_home: Path):
|
|
300
|
+
from skcapstone.cli import main
|
|
301
|
+
|
|
302
|
+
runner = CliRunner(mix_stderr=False)
|
|
303
|
+
return runner.invoke(
|
|
304
|
+
main,
|
|
305
|
+
["doctor", "--home", str(agent_home)] + args,
|
|
306
|
+
catch_exceptions=False,
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
def test_verbose_shows_passing_checks(self, agent_home: Path):
|
|
310
|
+
"""--verbose prints checks that passed, not just failures."""
|
|
311
|
+
result = self._run_doctor(["--verbose"], agent_home)
|
|
312
|
+
assert result.exit_code == 0
|
|
313
|
+
# At minimum the home:exists check should be present in output
|
|
314
|
+
assert "Agent home directory" in result.output
|
|
315
|
+
|
|
316
|
+
def test_verbose_output_includes_check_names(self, agent_home: Path):
|
|
317
|
+
"""--verbose output contains internal check names in parentheses."""
|
|
318
|
+
result = self._run_doctor(["--verbose"], agent_home)
|
|
319
|
+
assert result.exit_code == 0
|
|
320
|
+
# Internal names like (home:exists) appear in verbose mode
|
|
321
|
+
assert "home:exists" in result.output
|
|
322
|
+
|
|
323
|
+
def test_verbose_shows_summary_line(self, agent_home: Path):
|
|
324
|
+
"""--verbose ends with a 'Summary:' line containing pass/fail counts."""
|
|
325
|
+
result = self._run_doctor(["--verbose"], agent_home)
|
|
326
|
+
assert result.exit_code == 0
|
|
327
|
+
assert "Summary:" in result.output
|
|
328
|
+
assert "passed" in result.output
|
|
329
|
+
|
|
330
|
+
def test_non_verbose_collapses_all_pass_categories(self, agent_home: Path):
|
|
331
|
+
"""Without --verbose, fully-passing categories are on one line."""
|
|
332
|
+
result = self._run_doctor([], agent_home)
|
|
333
|
+
assert result.exit_code == 0
|
|
334
|
+
# Agent Home directory exists, so it should be collapsed
|
|
335
|
+
assert "passed" in result.output
|
|
336
|
+
|
|
337
|
+
def test_verbose_json_includes_all_checks(self, agent_home: Path):
|
|
338
|
+
"""--verbose --json-out still emits the full checks list."""
|
|
339
|
+
result = self._run_doctor(["--verbose", "--json-out"], agent_home)
|
|
340
|
+
assert result.exit_code == 0
|
|
341
|
+
data = json.loads(result.output)
|
|
342
|
+
assert "checks" in data
|
|
343
|
+
assert len(data["checks"]) > 0
|
|
344
|
+
# Every check has a name
|
|
345
|
+
for c in data["checks"]:
|
|
346
|
+
assert "name" in c
|
|
347
|
+
|
|
348
|
+
def test_verbose_help_text_mentions_verbose(self, agent_home: Path):
|
|
349
|
+
"""--help output documents the --verbose flag."""
|
|
350
|
+
from skcapstone.cli import main
|
|
351
|
+
|
|
352
|
+
runner = CliRunner(mix_stderr=False)
|
|
353
|
+
result = runner.invoke(main, ["doctor", "--help"])
|
|
354
|
+
assert result.exit_code == 0
|
|
355
|
+
assert "--verbose" in result.output
|