@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,254 @@
|
|
|
1
|
+
"""Tests for sovereign agent backup and restore."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import tarfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from skcapstone.backup import (
|
|
12
|
+
BackupManifest,
|
|
13
|
+
create_backup,
|
|
14
|
+
list_backups,
|
|
15
|
+
restore_backup,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _setup_agent_home(tmp_path: Path) -> Path:
|
|
20
|
+
"""Create a fake agent home directory with test data.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
tmp_path: Temporary directory from pytest.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Path: The fake agent home.
|
|
27
|
+
"""
|
|
28
|
+
home = tmp_path / ".skcapstone"
|
|
29
|
+
|
|
30
|
+
(home / "config").mkdir(parents=True)
|
|
31
|
+
(home / "config" / "config.yaml").write_text('agent_name: "TestAgent"\n')
|
|
32
|
+
|
|
33
|
+
(home / "identity").mkdir()
|
|
34
|
+
(home / "identity" / "profile.json").write_text('{"name": "TestAgent"}')
|
|
35
|
+
(home / "identity" / "public.asc").write_text("-----BEGIN PGP PUBLIC KEY-----\ntest\n-----END PGP PUBLIC KEY-----\n")
|
|
36
|
+
|
|
37
|
+
(home / "memory").mkdir()
|
|
38
|
+
(home / "memory" / "mem1.json").write_text('{"id": "mem1", "title": "test memory"}')
|
|
39
|
+
(home / "memory" / "mem2.json").write_text('{"id": "mem2", "title": "another memory"}')
|
|
40
|
+
|
|
41
|
+
(home / "trust").mkdir()
|
|
42
|
+
(home / "trust" / "FEB_test.feb").write_text('{"emotional_payload": {}}')
|
|
43
|
+
|
|
44
|
+
(home / "soul").mkdir()
|
|
45
|
+
(home / "soul" / "lumina.yaml").write_text('name: lumina\npersonality: warm\n')
|
|
46
|
+
|
|
47
|
+
(home / "conversations").mkdir()
|
|
48
|
+
(home / "conversations" / "conv1.json").write_text('{"id": "conv1", "messages": []}')
|
|
49
|
+
|
|
50
|
+
# Ephemeral dirs — must NOT be backed up
|
|
51
|
+
(home / "sync").mkdir()
|
|
52
|
+
(home / "sync" / "seed.json").write_text('{"ephemeral": true}')
|
|
53
|
+
|
|
54
|
+
(home / "heartbeats").mkdir()
|
|
55
|
+
(home / "heartbeats" / "opus.json").write_text('{"alive": true}')
|
|
56
|
+
|
|
57
|
+
(home / "coordination" / "tasks").mkdir(parents=True)
|
|
58
|
+
(home / "coordination" / "tasks" / "task1.json").write_text('{"status": "done"}')
|
|
59
|
+
|
|
60
|
+
(home / "manifest.json").write_text('{"name": "TestAgent", "version": "0.1.0"}')
|
|
61
|
+
(home / "agent-card.json").write_text('{"name": "TestAgent", "fingerprint": "abc123"}')
|
|
62
|
+
|
|
63
|
+
return home
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class TestCreateBackup:
|
|
67
|
+
"""Tests for backup creation."""
|
|
68
|
+
|
|
69
|
+
def test_create_basic_backup(self, tmp_path: Path) -> None:
|
|
70
|
+
"""Happy path: backup creates a valid tar.gz archive."""
|
|
71
|
+
home = _setup_agent_home(tmp_path)
|
|
72
|
+
out_dir = tmp_path / "backups"
|
|
73
|
+
|
|
74
|
+
result = create_backup(home=home, output_dir=out_dir, agent_name="TestAgent")
|
|
75
|
+
|
|
76
|
+
assert result["file_count"] > 0
|
|
77
|
+
assert result["archive_size"] > 0
|
|
78
|
+
assert Path(result["filepath"]).exists()
|
|
79
|
+
assert result["filepath"].endswith(".tar.gz")
|
|
80
|
+
|
|
81
|
+
def test_backup_contains_expected_files(self, tmp_path: Path) -> None:
|
|
82
|
+
"""Archive contains identity, memory, config, soul, conversations, and manifest."""
|
|
83
|
+
home = _setup_agent_home(tmp_path)
|
|
84
|
+
result = create_backup(home=home, output_dir=tmp_path / "out")
|
|
85
|
+
|
|
86
|
+
with tarfile.open(result["filepath"], "r:gz") as tar:
|
|
87
|
+
names = tar.getnames()
|
|
88
|
+
|
|
89
|
+
assert any("config/config.yaml" in n for n in names)
|
|
90
|
+
assert any("identity/profile.json" in n for n in names)
|
|
91
|
+
assert any("memory/mem1.json" in n for n in names)
|
|
92
|
+
assert any("manifest.json" in n for n in names)
|
|
93
|
+
assert any("soul/lumina.yaml" in n for n in names)
|
|
94
|
+
assert any("conversations/conv1.json" in n for n in names)
|
|
95
|
+
|
|
96
|
+
def test_backup_excludes_ephemeral_dirs(self, tmp_path: Path) -> None:
|
|
97
|
+
"""Ephemeral dirs (sync/, heartbeats/, coordination/tasks/) are not backed up."""
|
|
98
|
+
home = _setup_agent_home(tmp_path)
|
|
99
|
+
result = create_backup(home=home, output_dir=tmp_path / "out")
|
|
100
|
+
|
|
101
|
+
with tarfile.open(result["filepath"], "r:gz") as tar:
|
|
102
|
+
names = tar.getnames()
|
|
103
|
+
|
|
104
|
+
assert not any("/sync/" in n or n.endswith("/sync") for n in names)
|
|
105
|
+
assert not any("/heartbeats/" in n or n.endswith("/heartbeats") for n in names)
|
|
106
|
+
assert not any("coordination/tasks" in n for n in names)
|
|
107
|
+
|
|
108
|
+
def test_backup_manifest_has_checksums(self, tmp_path: Path) -> None:
|
|
109
|
+
"""Manifest contains SHA-256 checksums for all files."""
|
|
110
|
+
home = _setup_agent_home(tmp_path)
|
|
111
|
+
result = create_backup(home=home, output_dir=tmp_path / "out")
|
|
112
|
+
|
|
113
|
+
manifest = result["manifest"]
|
|
114
|
+
assert len(manifest["files"]) > 0
|
|
115
|
+
for filepath, checksum in manifest["files"].items():
|
|
116
|
+
assert len(checksum) == 64
|
|
117
|
+
|
|
118
|
+
def test_backup_excludes_pycache(self, tmp_path: Path) -> None:
|
|
119
|
+
"""Backup skips __pycache__ directories."""
|
|
120
|
+
home = _setup_agent_home(tmp_path)
|
|
121
|
+
cache_dir = home / "memory" / "__pycache__"
|
|
122
|
+
cache_dir.mkdir()
|
|
123
|
+
(cache_dir / "module.cpython-312.pyc").write_text("bytecode")
|
|
124
|
+
|
|
125
|
+
result = create_backup(home=home, output_dir=tmp_path / "out")
|
|
126
|
+
|
|
127
|
+
with tarfile.open(result["filepath"], "r:gz") as tar:
|
|
128
|
+
names = tar.getnames()
|
|
129
|
+
|
|
130
|
+
assert not any("__pycache__" in n for n in names)
|
|
131
|
+
assert not any(".pyc" in n for n in names)
|
|
132
|
+
|
|
133
|
+
def test_backup_missing_home_raises(self, tmp_path: Path) -> None:
|
|
134
|
+
"""Backup raises FileNotFoundError for missing home."""
|
|
135
|
+
with pytest.raises(FileNotFoundError):
|
|
136
|
+
create_backup(home=tmp_path / "nonexistent")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class TestRestoreBackup:
|
|
140
|
+
"""Tests for backup restoration."""
|
|
141
|
+
|
|
142
|
+
def test_restore_roundtrip(self, tmp_path: Path) -> None:
|
|
143
|
+
"""Backup then restore produces identical files."""
|
|
144
|
+
home = _setup_agent_home(tmp_path)
|
|
145
|
+
result = create_backup(home=home, output_dir=tmp_path / "out")
|
|
146
|
+
|
|
147
|
+
restore_target = tmp_path / "restored"
|
|
148
|
+
restore_result = restore_backup(
|
|
149
|
+
archive_path=result["filepath"],
|
|
150
|
+
target_home=restore_target,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
assert restore_result["file_count"] > 0
|
|
154
|
+
assert restore_result["verified"] is True
|
|
155
|
+
assert len(restore_result["errors"]) == 0
|
|
156
|
+
|
|
157
|
+
assert (restore_target / "config" / "config.yaml").exists()
|
|
158
|
+
assert (restore_target / "identity" / "profile.json").exists()
|
|
159
|
+
assert (restore_target / "memory" / "mem1.json").exists()
|
|
160
|
+
assert (restore_target / "soul" / "lumina.yaml").exists()
|
|
161
|
+
assert (restore_target / "conversations" / "conv1.json").exists()
|
|
162
|
+
|
|
163
|
+
def test_restore_detects_tampered_file(self, tmp_path: Path) -> None:
|
|
164
|
+
"""Verification catches files that don't match manifest checksums."""
|
|
165
|
+
home = _setup_agent_home(tmp_path)
|
|
166
|
+
result = create_backup(home=home, output_dir=tmp_path / "out")
|
|
167
|
+
|
|
168
|
+
restore_target = tmp_path / "tampered"
|
|
169
|
+
restore_backup(
|
|
170
|
+
archive_path=result["filepath"],
|
|
171
|
+
target_home=restore_target,
|
|
172
|
+
verify=False,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# Tamper after extraction, then verify by comparing to manifest
|
|
176
|
+
(restore_target / "memory" / "mem1.json").write_text("TAMPERED!")
|
|
177
|
+
|
|
178
|
+
from skcapstone.backup import BackupManifest, _sha256_file
|
|
179
|
+
|
|
180
|
+
manifest = BackupManifest(**result["manifest"])
|
|
181
|
+
errors = []
|
|
182
|
+
for rel_path, expected in manifest.files.items():
|
|
183
|
+
f = restore_target / rel_path
|
|
184
|
+
if f.exists() and _sha256_file(f) != expected:
|
|
185
|
+
errors.append(rel_path)
|
|
186
|
+
|
|
187
|
+
assert len(errors) > 0
|
|
188
|
+
assert any("mem1" in e for e in errors)
|
|
189
|
+
|
|
190
|
+
def test_restore_missing_archive_raises(self, tmp_path: Path) -> None:
|
|
191
|
+
"""Restore raises FileNotFoundError for missing archive."""
|
|
192
|
+
with pytest.raises(FileNotFoundError):
|
|
193
|
+
restore_backup(archive_path="/nonexistent.tar.gz")
|
|
194
|
+
|
|
195
|
+
def test_restore_no_verify(self, tmp_path: Path) -> None:
|
|
196
|
+
"""Restore with verify=False skips checksum checks."""
|
|
197
|
+
home = _setup_agent_home(tmp_path)
|
|
198
|
+
result = create_backup(home=home, output_dir=tmp_path / "out")
|
|
199
|
+
|
|
200
|
+
restore_target = tmp_path / "noverify"
|
|
201
|
+
restore_result = restore_backup(
|
|
202
|
+
archive_path=result["filepath"],
|
|
203
|
+
target_home=restore_target,
|
|
204
|
+
verify=False,
|
|
205
|
+
)
|
|
206
|
+
assert restore_result["file_count"] > 0
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class TestListBackups:
|
|
210
|
+
"""Tests for backup listing."""
|
|
211
|
+
|
|
212
|
+
def test_list_empty_dir(self, tmp_path: Path) -> None:
|
|
213
|
+
"""Empty directory returns empty list."""
|
|
214
|
+
assert list_backups(tmp_path) == []
|
|
215
|
+
|
|
216
|
+
def test_list_nonexistent_dir(self, tmp_path: Path) -> None:
|
|
217
|
+
"""Missing directory returns empty list."""
|
|
218
|
+
assert list_backups(tmp_path / "nope") == []
|
|
219
|
+
|
|
220
|
+
def test_list_with_backups(self, tmp_path: Path) -> None:
|
|
221
|
+
"""Lists backup archives sorted newest first."""
|
|
222
|
+
home = _setup_agent_home(tmp_path)
|
|
223
|
+
out = tmp_path / "backups"
|
|
224
|
+
|
|
225
|
+
create_backup(home=home, output_dir=out)
|
|
226
|
+
create_backup(home=home, output_dir=out)
|
|
227
|
+
|
|
228
|
+
backups = list_backups(out)
|
|
229
|
+
assert len(backups) == 2
|
|
230
|
+
assert all(b["filename"].endswith(".tar.gz") for b in backups)
|
|
231
|
+
assert all(b["size"] > 0 for b in backups)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class TestBackupManifest:
|
|
235
|
+
"""Tests for the manifest model."""
|
|
236
|
+
|
|
237
|
+
def test_manifest_defaults(self) -> None:
|
|
238
|
+
"""Manifest has sensible defaults."""
|
|
239
|
+
m = BackupManifest()
|
|
240
|
+
assert m.version == "0.9.0"
|
|
241
|
+
assert m.files == {}
|
|
242
|
+
assert m.total_size == 0
|
|
243
|
+
|
|
244
|
+
def test_manifest_serialization(self) -> None:
|
|
245
|
+
"""Manifest roundtrips through JSON."""
|
|
246
|
+
m = BackupManifest(
|
|
247
|
+
backup_id="test-123",
|
|
248
|
+
agent_name="Jarvis",
|
|
249
|
+
files={"config.yaml": "abc123"},
|
|
250
|
+
)
|
|
251
|
+
data = json.loads(m.model_dump_json())
|
|
252
|
+
loaded = BackupManifest(**data)
|
|
253
|
+
assert loaded.backup_id == "test-123"
|
|
254
|
+
assert loaded.files["config.yaml"] == "abc123"
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
"""Tests for ``skcapstone benchmark`` — LLM backend latency benchmarking.
|
|
2
|
+
|
|
3
|
+
Covers:
|
|
4
|
+
- BenchmarkRunner.detect_backends() availability detection
|
|
5
|
+
- BenchmarkRunner.run_backend() per-backend call and error handling
|
|
6
|
+
- BenchmarkRunner.run_all() result aggregation (skip_unavailable logic)
|
|
7
|
+
- CLI rendering: table output and JSON output modes
|
|
8
|
+
- Passthrough always succeeds with zero external dependencies
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
from unittest.mock import MagicMock, patch
|
|
16
|
+
|
|
17
|
+
import pytest
|
|
18
|
+
from click.testing import CliRunner
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
# Helpers
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _make_runner(prompt: str = "Hello", timeout: float = 5.0):
|
|
27
|
+
from skcapstone.cli.benchmark import BenchmarkRunner
|
|
28
|
+
|
|
29
|
+
return BenchmarkRunner(prompt=prompt, timeout=timeout)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _ok(backend: str, ms: float = 42.0, model: str = "test-model") -> dict:
|
|
33
|
+
return {"backend": backend, "status": "ok", "ms": ms, "model": model, "error": None}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _unavail(backend: str) -> dict:
|
|
37
|
+
return {"backend": backend, "status": "unavailable", "ms": None, "model": None, "error": None}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
# detect_backends
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class TestDetectBackends:
|
|
46
|
+
"""BenchmarkRunner.detect_backends() tests."""
|
|
47
|
+
|
|
48
|
+
def test_passthrough_always_available(self):
|
|
49
|
+
"""passthrough must always be True regardless of env."""
|
|
50
|
+
runner = _make_runner()
|
|
51
|
+
with patch.object(runner, "_probe_ollama", return_value=False):
|
|
52
|
+
detected = runner.detect_backends()
|
|
53
|
+
assert detected["passthrough"] is True
|
|
54
|
+
|
|
55
|
+
def test_ollama_available_when_probe_succeeds(self):
|
|
56
|
+
"""ollama is True when _probe_ollama returns True."""
|
|
57
|
+
runner = _make_runner()
|
|
58
|
+
with patch.object(runner, "_probe_ollama", return_value=True):
|
|
59
|
+
detected = runner.detect_backends()
|
|
60
|
+
assert detected["ollama"] is True
|
|
61
|
+
|
|
62
|
+
def test_ollama_unavailable_when_probe_fails(self):
|
|
63
|
+
"""ollama is False when _probe_ollama returns False."""
|
|
64
|
+
runner = _make_runner()
|
|
65
|
+
with patch.object(runner, "_probe_ollama", return_value=False):
|
|
66
|
+
detected = runner.detect_backends()
|
|
67
|
+
assert detected["ollama"] is False
|
|
68
|
+
|
|
69
|
+
def test_cloud_backend_requires_env_var(self):
|
|
70
|
+
"""Cloud backends are True only when the right env var is set."""
|
|
71
|
+
runner = _make_runner()
|
|
72
|
+
env_cases = {
|
|
73
|
+
"anthropic": "ANTHROPIC_API_KEY",
|
|
74
|
+
"openai": "OPENAI_API_KEY",
|
|
75
|
+
"grok": "XAI_API_KEY",
|
|
76
|
+
"kimi": "MOONSHOT_API_KEY",
|
|
77
|
+
"nvidia": "NVIDIA_API_KEY",
|
|
78
|
+
}
|
|
79
|
+
with patch.object(runner, "_probe_ollama", return_value=False):
|
|
80
|
+
# No keys set — all cloud backends False
|
|
81
|
+
clean_env = {k: "" for k in env_cases.values()}
|
|
82
|
+
with patch.dict(os.environ, clean_env, clear=False):
|
|
83
|
+
# Temporarily remove the keys
|
|
84
|
+
for var in env_cases.values():
|
|
85
|
+
os.environ.pop(var, None)
|
|
86
|
+
detected = runner.detect_backends()
|
|
87
|
+
for name in env_cases:
|
|
88
|
+
assert detected[name] is False, f"{name} should be unavailable without key"
|
|
89
|
+
|
|
90
|
+
def test_anthropic_available_with_env_var(self):
|
|
91
|
+
"""anthropic becomes available when ANTHROPIC_API_KEY is set."""
|
|
92
|
+
runner = _make_runner()
|
|
93
|
+
with patch.object(runner, "_probe_ollama", return_value=False), \
|
|
94
|
+
patch.dict(os.environ, {"ANTHROPIC_API_KEY": "sk-test-key"}):
|
|
95
|
+
detected = runner.detect_backends()
|
|
96
|
+
assert detected["anthropic"] is True
|
|
97
|
+
|
|
98
|
+
def test_all_backends_listed(self):
|
|
99
|
+
"""detect_backends() covers all known backends."""
|
|
100
|
+
from skcapstone.cli.benchmark import BACKENDS
|
|
101
|
+
|
|
102
|
+
runner = _make_runner()
|
|
103
|
+
with patch.object(runner, "_probe_ollama", return_value=False):
|
|
104
|
+
detected = runner.detect_backends()
|
|
105
|
+
for name in BACKENDS:
|
|
106
|
+
assert name in detected, f"Backend '{name}' missing from detect_backends() result"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# ---------------------------------------------------------------------------
|
|
110
|
+
# run_backend — passthrough
|
|
111
|
+
# ---------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class TestRunBackendPassthrough:
|
|
115
|
+
"""Passthrough backend — always ok, no external calls."""
|
|
116
|
+
|
|
117
|
+
def test_passthrough_returns_ok(self):
|
|
118
|
+
"""_bench_passthrough returns status=ok with a float ms value."""
|
|
119
|
+
runner = _make_runner()
|
|
120
|
+
result = runner._bench_passthrough()
|
|
121
|
+
assert result["status"] == "ok"
|
|
122
|
+
assert isinstance(result["ms"], float)
|
|
123
|
+
assert result["model"] == "mock"
|
|
124
|
+
assert result["error"] is None
|
|
125
|
+
|
|
126
|
+
def test_passthrough_ms_is_non_negative(self):
|
|
127
|
+
"""Passthrough latency must be >= 0."""
|
|
128
|
+
runner = _make_runner()
|
|
129
|
+
result = runner._bench_passthrough()
|
|
130
|
+
assert result["ms"] >= 0.0
|
|
131
|
+
|
|
132
|
+
def test_run_backend_passthrough_via_dispatch(self):
|
|
133
|
+
"""run_backend('passthrough') dispatches to _bench_passthrough."""
|
|
134
|
+
runner = _make_runner()
|
|
135
|
+
result = runner.run_backend("passthrough")
|
|
136
|
+
assert result["backend"] == "passthrough"
|
|
137
|
+
assert result["status"] == "ok"
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# ---------------------------------------------------------------------------
|
|
141
|
+
# run_backend — error handling
|
|
142
|
+
# ---------------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class TestRunBackendErrors:
|
|
146
|
+
"""run_backend() error-handling for failed or unsupported backends."""
|
|
147
|
+
|
|
148
|
+
def test_run_backend_catches_exception(self):
|
|
149
|
+
"""Exceptions from _bench_* are caught and returned as status=error."""
|
|
150
|
+
runner = _make_runner()
|
|
151
|
+
with patch.object(runner, "_bench_ollama", side_effect=RuntimeError("connection refused")):
|
|
152
|
+
result = runner.run_backend("ollama")
|
|
153
|
+
assert result["status"] == "error"
|
|
154
|
+
assert result["ms"] is None
|
|
155
|
+
assert "connection refused" in result["error"]
|
|
156
|
+
|
|
157
|
+
def test_run_backend_unsupported_name(self):
|
|
158
|
+
"""Unknown backend name returns status=unsupported."""
|
|
159
|
+
runner = _make_runner()
|
|
160
|
+
result = runner.run_backend("nonexistent_backend_xyz")
|
|
161
|
+
assert result["status"] == "unsupported"
|
|
162
|
+
assert result["ms"] is None
|
|
163
|
+
|
|
164
|
+
def test_run_backend_error_truncates_long_message(self):
|
|
165
|
+
"""Long exception messages are truncated to 120 chars."""
|
|
166
|
+
runner = _make_runner()
|
|
167
|
+
long_msg = "x" * 200
|
|
168
|
+
with patch.object(runner, "_bench_ollama", side_effect=RuntimeError(long_msg)):
|
|
169
|
+
result = runner.run_backend("ollama")
|
|
170
|
+
assert len(result["error"]) <= 120
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# ---------------------------------------------------------------------------
|
|
174
|
+
# run_all — aggregation
|
|
175
|
+
# ---------------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class TestRunAll:
|
|
179
|
+
"""BenchmarkRunner.run_all() aggregation logic."""
|
|
180
|
+
|
|
181
|
+
def test_run_all_skips_unavailable_by_default(self):
|
|
182
|
+
"""Unavailable backends appear with status=unavailable, not called."""
|
|
183
|
+
runner = _make_runner()
|
|
184
|
+
# All unavailable except passthrough
|
|
185
|
+
unavail = {name: False for name in runner.detect_backends()}
|
|
186
|
+
unavail["passthrough"] = True
|
|
187
|
+
|
|
188
|
+
with patch.object(runner, "detect_backends", return_value=unavail), \
|
|
189
|
+
patch.object(runner, "_bench_passthrough", return_value=_ok("passthrough")):
|
|
190
|
+
results = runner.run_all()
|
|
191
|
+
|
|
192
|
+
by_name = {r["backend"]: r for r in results}
|
|
193
|
+
assert by_name["passthrough"]["status"] == "ok"
|
|
194
|
+
for name, avail in unavail.items():
|
|
195
|
+
if not avail:
|
|
196
|
+
assert by_name[name]["status"] == "unavailable"
|
|
197
|
+
|
|
198
|
+
def test_run_all_returns_one_result_per_backend(self):
|
|
199
|
+
"""run_all() always returns exactly len(BACKENDS) results."""
|
|
200
|
+
from skcapstone.cli.benchmark import BACKENDS
|
|
201
|
+
|
|
202
|
+
runner = _make_runner()
|
|
203
|
+
with patch.object(runner, "detect_backends", return_value={n: False for n in BACKENDS}):
|
|
204
|
+
# passthrough would still be run — override detect_backends to all-False
|
|
205
|
+
# except passthrough
|
|
206
|
+
avail = {n: False for n in BACKENDS}
|
|
207
|
+
avail["passthrough"] = True
|
|
208
|
+
with patch.object(runner, "detect_backends", return_value=avail), \
|
|
209
|
+
patch.object(runner, "_bench_passthrough", return_value=_ok("passthrough")):
|
|
210
|
+
results = runner.run_all()
|
|
211
|
+
|
|
212
|
+
assert len(results) == len(BACKENDS)
|
|
213
|
+
|
|
214
|
+
def test_run_all_collects_errors(self):
|
|
215
|
+
"""Failing backends are included with status=error, not raised."""
|
|
216
|
+
runner = _make_runner()
|
|
217
|
+
avail = {n: False for n in runner.detect_backends()}
|
|
218
|
+
avail["ollama"] = True
|
|
219
|
+
|
|
220
|
+
with patch.object(runner, "detect_backends", return_value=avail), \
|
|
221
|
+
patch.object(runner, "_bench_ollama", side_effect=OSError("network down")):
|
|
222
|
+
results = runner.run_all()
|
|
223
|
+
|
|
224
|
+
ollama_result = next(r for r in results if r["backend"] == "ollama")
|
|
225
|
+
assert ollama_result["status"] == "error"
|
|
226
|
+
assert "network down" in ollama_result["error"]
|
|
227
|
+
|
|
228
|
+
def test_run_all_includes_ok_results(self):
|
|
229
|
+
"""Successful backends appear with status=ok and a float ms."""
|
|
230
|
+
runner = _make_runner()
|
|
231
|
+
avail = {n: False for n in runner.detect_backends()}
|
|
232
|
+
avail["passthrough"] = True
|
|
233
|
+
|
|
234
|
+
with patch.object(runner, "detect_backends", return_value=avail), \
|
|
235
|
+
patch.object(runner, "_bench_passthrough", return_value=_ok("passthrough", ms=7.5)):
|
|
236
|
+
results = runner.run_all()
|
|
237
|
+
|
|
238
|
+
pt = next(r for r in results if r["backend"] == "passthrough")
|
|
239
|
+
assert pt["status"] == "ok"
|
|
240
|
+
assert pt["ms"] == 7.5
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
# ---------------------------------------------------------------------------
|
|
244
|
+
# Ollama benchmark (mocked HTTP)
|
|
245
|
+
# ---------------------------------------------------------------------------
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class TestBenchOllama:
|
|
249
|
+
"""_bench_ollama with mocked urllib."""
|
|
250
|
+
|
|
251
|
+
def test_bench_ollama_ok(self):
|
|
252
|
+
"""Successful Ollama call returns status=ok with ms and model."""
|
|
253
|
+
runner = _make_runner()
|
|
254
|
+
fake_response = json.dumps({"model": "llama3.2", "response": "Hi!"}).encode()
|
|
255
|
+
|
|
256
|
+
mock_resp = MagicMock()
|
|
257
|
+
mock_resp.__enter__ = lambda s: s
|
|
258
|
+
mock_resp.__exit__ = MagicMock(return_value=False)
|
|
259
|
+
mock_resp.read.return_value = fake_response
|
|
260
|
+
|
|
261
|
+
with patch("urllib.request.urlopen", return_value=mock_resp):
|
|
262
|
+
result = runner._bench_ollama()
|
|
263
|
+
|
|
264
|
+
assert result["status"] == "ok"
|
|
265
|
+
assert result["model"] == "llama3.2"
|
|
266
|
+
assert isinstance(result["ms"], float)
|
|
267
|
+
assert result["ms"] >= 0
|
|
268
|
+
|
|
269
|
+
def test_bench_ollama_network_error_propagates(self):
|
|
270
|
+
"""Network errors from urlopen are re-raised (caught by run_backend)."""
|
|
271
|
+
runner = _make_runner()
|
|
272
|
+
|
|
273
|
+
with patch("urllib.request.urlopen", side_effect=OSError("refused")):
|
|
274
|
+
with pytest.raises(OSError):
|
|
275
|
+
runner._bench_ollama()
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
# ---------------------------------------------------------------------------
|
|
279
|
+
# CLI command tests
|
|
280
|
+
# ---------------------------------------------------------------------------
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
class TestBenchmarkCLI:
|
|
284
|
+
"""CLI integration tests using CliRunner."""
|
|
285
|
+
|
|
286
|
+
def _invoke(self, *args):
|
|
287
|
+
from skcapstone.cli import main
|
|
288
|
+
runner = CliRunner()
|
|
289
|
+
return runner.invoke(main, ["benchmark", *args])
|
|
290
|
+
|
|
291
|
+
def test_help(self):
|
|
292
|
+
"""benchmark --help exits 0 and shows key options."""
|
|
293
|
+
result = self._invoke("--help")
|
|
294
|
+
assert result.exit_code == 0
|
|
295
|
+
assert "--prompt" in result.output
|
|
296
|
+
assert "--timeout" in result.output
|
|
297
|
+
assert "--json-out" in result.output
|
|
298
|
+
|
|
299
|
+
def test_json_output(self):
|
|
300
|
+
"""--json-out emits valid JSON list."""
|
|
301
|
+
from skcapstone.cli.benchmark import BenchmarkRunner
|
|
302
|
+
|
|
303
|
+
fake_results = [_ok("passthrough"), _unavail("ollama")]
|
|
304
|
+
with patch.object(BenchmarkRunner, "run_all", return_value=fake_results):
|
|
305
|
+
result = self._invoke("--json-out")
|
|
306
|
+
|
|
307
|
+
assert result.exit_code == 0
|
|
308
|
+
data = json.loads(result.output)
|
|
309
|
+
assert isinstance(data, list)
|
|
310
|
+
assert data[0]["backend"] == "passthrough"
|
|
311
|
+
assert data[0]["status"] == "ok"
|
|
312
|
+
|
|
313
|
+
def test_table_output_contains_backend_names(self):
|
|
314
|
+
"""Default table output contains backend names."""
|
|
315
|
+
from skcapstone.cli.benchmark import BenchmarkRunner
|
|
316
|
+
|
|
317
|
+
fake_results = [
|
|
318
|
+
_ok("passthrough", ms=1.2),
|
|
319
|
+
_unavail("ollama"),
|
|
320
|
+
]
|
|
321
|
+
with patch.object(BenchmarkRunner, "run_all", return_value=fake_results):
|
|
322
|
+
result = self._invoke()
|
|
323
|
+
|
|
324
|
+
assert result.exit_code == 0
|
|
325
|
+
assert "passthrough" in result.output
|
|
326
|
+
assert "ollama" in result.output
|
|
327
|
+
|
|
328
|
+
def test_custom_prompt_passed_to_runner(self):
|
|
329
|
+
"""--prompt value is forwarded to BenchmarkRunner."""
|
|
330
|
+
from skcapstone.cli.benchmark import BenchmarkRunner
|
|
331
|
+
|
|
332
|
+
with patch.object(BenchmarkRunner, "__init__", return_value=None) as mock_init, \
|
|
333
|
+
patch.object(BenchmarkRunner, "run_all", return_value=[]):
|
|
334
|
+
self._invoke("--prompt", "Ping", "--json-out")
|
|
335
|
+
|
|
336
|
+
call_kwargs = mock_init.call_args
|
|
337
|
+
assert call_kwargs is not None
|
|
338
|
+
# prompt is passed as keyword or positional arg
|
|
339
|
+
all_args = list(call_kwargs.args) + list(call_kwargs.kwargs.values())
|
|
340
|
+
assert "Ping" in all_args
|
|
341
|
+
|
|
342
|
+
def test_fastest_summary_shown_in_table(self):
|
|
343
|
+
"""When at least one backend succeeds, fastest backend is shown."""
|
|
344
|
+
from skcapstone.cli.benchmark import BenchmarkRunner
|
|
345
|
+
|
|
346
|
+
fake_results = [
|
|
347
|
+
_ok("passthrough", ms=3.0),
|
|
348
|
+
_ok("ollama", ms=200.0),
|
|
349
|
+
]
|
|
350
|
+
with patch.object(BenchmarkRunner, "run_all", return_value=fake_results):
|
|
351
|
+
result = self._invoke()
|
|
352
|
+
|
|
353
|
+
assert result.exit_code == 0
|
|
354
|
+
assert "Fastest" in result.output
|
|
355
|
+
assert "passthrough" in result.output
|
|
356
|
+
|
|
357
|
+
def test_no_backends_available_message(self):
|
|
358
|
+
"""When all backends are unavailable, a friendly message is shown."""
|
|
359
|
+
from skcapstone.cli.benchmark import BenchmarkRunner, BACKENDS
|
|
360
|
+
|
|
361
|
+
all_unavail = [_unavail(name) for name in BACKENDS]
|
|
362
|
+
with patch.object(BenchmarkRunner, "run_all", return_value=all_unavail):
|
|
363
|
+
result = self._invoke()
|
|
364
|
+
|
|
365
|
+
assert result.exit_code == 0
|
|
366
|
+
assert "No backends available" in result.output or "unavailable" in result.output
|