@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,546 @@
|
|
|
1
|
+
"""Tests for the ModelRouter — automatic model selection layer.
|
|
2
|
+
|
|
3
|
+
Covers:
|
|
4
|
+
- Routing by tag to each primary tier (CODE, NUANCE, FAST)
|
|
5
|
+
- Privacy-sensitive forcing LOCAL tier
|
|
6
|
+
- Token-based fallback to REASON
|
|
7
|
+
- Tag-rule priority conflict resolution
|
|
8
|
+
- Config load from YAML
|
|
9
|
+
- Model name resolution per tier
|
|
10
|
+
- MCP tool handler integration
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import textwrap
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
import pytest
|
|
19
|
+
|
|
20
|
+
from skcapstone.blueprints.schema import ModelTier
|
|
21
|
+
from skcapstone.model_router import (
|
|
22
|
+
ModelRouter,
|
|
23
|
+
ModelRouterConfig,
|
|
24
|
+
TagRule,
|
|
25
|
+
TaskSignal,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
# Fixtures
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@pytest.fixture()
|
|
35
|
+
def router() -> ModelRouter:
|
|
36
|
+
"""Return a ModelRouter loaded with the default configuration."""
|
|
37
|
+
return ModelRouter()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
# Tag-based routing
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class TestTagRouting:
|
|
46
|
+
"""Routing decisions driven by tags in the TaskSignal."""
|
|
47
|
+
|
|
48
|
+
def test_code_tags_route_to_code_tier(self, router: ModelRouter) -> None:
|
|
49
|
+
"""A task tagged 'code' and 'refactor' should land on the CODE tier."""
|
|
50
|
+
signal = TaskSignal(
|
|
51
|
+
description="Refactor the authentication module",
|
|
52
|
+
tags=["code", "refactor"],
|
|
53
|
+
)
|
|
54
|
+
decision = router.route(signal)
|
|
55
|
+
assert decision.tier == ModelTier.CODE
|
|
56
|
+
|
|
57
|
+
def test_code_tier_returns_known_model(self, router: ModelRouter) -> None:
|
|
58
|
+
"""CODE tier must resolve to a non-empty model name."""
|
|
59
|
+
signal = TaskSignal(description="Implement login flow", tags=["implement"])
|
|
60
|
+
decision = router.route(signal)
|
|
61
|
+
assert decision.tier == ModelTier.CODE
|
|
62
|
+
assert decision.model_name # not empty
|
|
63
|
+
|
|
64
|
+
def test_marketing_tags_route_to_nuance_tier(self, router: ModelRouter) -> None:
|
|
65
|
+
"""A task tagged 'marketing' and 'creative' should land on NUANCE tier."""
|
|
66
|
+
signal = TaskSignal(
|
|
67
|
+
description="Write landing page copy",
|
|
68
|
+
tags=["marketing", "creative"],
|
|
69
|
+
)
|
|
70
|
+
decision = router.route(signal)
|
|
71
|
+
assert decision.tier == ModelTier.NUANCE
|
|
72
|
+
|
|
73
|
+
def test_format_tag_routes_to_fast_tier(self, router: ModelRouter) -> None:
|
|
74
|
+
"""A task tagged 'format' should resolve to the FAST tier."""
|
|
75
|
+
signal = TaskSignal(description="Reformat this file", tags=["format"])
|
|
76
|
+
decision = router.route(signal)
|
|
77
|
+
assert decision.tier == ModelTier.FAST
|
|
78
|
+
|
|
79
|
+
def test_architecture_tag_routes_to_reason_tier(
|
|
80
|
+
self, router: ModelRouter
|
|
81
|
+
) -> None:
|
|
82
|
+
"""A task tagged 'architecture' should land on the REASON tier."""
|
|
83
|
+
signal = TaskSignal(
|
|
84
|
+
description="Design the data pipeline",
|
|
85
|
+
tags=["architecture", "design"],
|
|
86
|
+
)
|
|
87
|
+
decision = router.route(signal)
|
|
88
|
+
assert decision.tier == ModelTier.REASON
|
|
89
|
+
|
|
90
|
+
def test_case_insensitive_tag_matching(self, router: ModelRouter) -> None:
|
|
91
|
+
"""Tags are matched case-insensitively."""
|
|
92
|
+
signal = TaskSignal(description="Write some code", tags=["CODE", "DEBUG"])
|
|
93
|
+
decision = router.route(signal)
|
|
94
|
+
assert decision.tier == ModelTier.CODE
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# ---------------------------------------------------------------------------
|
|
98
|
+
# Privacy / localhost gates
|
|
99
|
+
# ---------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class TestPrivacyGates:
|
|
103
|
+
"""LOCAL tier is forced by privacy or localhost flags."""
|
|
104
|
+
|
|
105
|
+
def test_privacy_sensitive_forces_local_tier(self, router: ModelRouter) -> None:
|
|
106
|
+
"""privacy_sensitive=True must route to LOCAL regardless of tags."""
|
|
107
|
+
signal = TaskSignal(
|
|
108
|
+
description="Process patient health records",
|
|
109
|
+
tags=["code", "implement"],
|
|
110
|
+
privacy_sensitive=True,
|
|
111
|
+
)
|
|
112
|
+
decision = router.route(signal)
|
|
113
|
+
assert decision.tier == ModelTier.LOCAL
|
|
114
|
+
|
|
115
|
+
def test_privacy_sensitive_returns_local_model(self, router: ModelRouter) -> None:
|
|
116
|
+
"""LOCAL tier should resolve to a configured local model name."""
|
|
117
|
+
signal = TaskSignal(
|
|
118
|
+
description="Summarise confidential notes",
|
|
119
|
+
privacy_sensitive=True,
|
|
120
|
+
)
|
|
121
|
+
decision = router.route(signal)
|
|
122
|
+
assert decision.tier == ModelTier.LOCAL
|
|
123
|
+
assert decision.model_name
|
|
124
|
+
|
|
125
|
+
def test_requires_localhost_forces_local_tier(self, router: ModelRouter) -> None:
|
|
126
|
+
"""requires_localhost=True must route to LOCAL with preferred_node set."""
|
|
127
|
+
signal = TaskSignal(
|
|
128
|
+
description="Run local GPU benchmark",
|
|
129
|
+
requires_localhost=True,
|
|
130
|
+
)
|
|
131
|
+
decision = router.route(signal)
|
|
132
|
+
assert decision.tier == ModelTier.LOCAL
|
|
133
|
+
assert decision.preferred_node == "localhost"
|
|
134
|
+
|
|
135
|
+
def test_privacy_takes_precedence_over_localhost(
|
|
136
|
+
self, router: ModelRouter
|
|
137
|
+
) -> None:
|
|
138
|
+
"""privacy_sensitive wins; preferred_node should not be set to localhost."""
|
|
139
|
+
signal = TaskSignal(
|
|
140
|
+
description="Private task on local machine",
|
|
141
|
+
privacy_sensitive=True,
|
|
142
|
+
requires_localhost=True,
|
|
143
|
+
)
|
|
144
|
+
decision = router.route(signal)
|
|
145
|
+
assert decision.tier == ModelTier.LOCAL
|
|
146
|
+
# privacy path doesn't pin a preferred_node
|
|
147
|
+
assert decision.preferred_node is None
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# ---------------------------------------------------------------------------
|
|
151
|
+
# Token-based fallback
|
|
152
|
+
# ---------------------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class TestTokenFallback:
|
|
156
|
+
"""When no tags match, token count drives the fallback tier."""
|
|
157
|
+
|
|
158
|
+
def test_large_token_count_routes_to_reason(self, router: ModelRouter) -> None:
|
|
159
|
+
"""A task with > 16 000 tokens and no matching tags should use REASON."""
|
|
160
|
+
signal = TaskSignal(
|
|
161
|
+
description="Analyse a large codebase",
|
|
162
|
+
tags=["unknown-tag"],
|
|
163
|
+
estimated_tokens=20_000,
|
|
164
|
+
)
|
|
165
|
+
decision = router.route(signal)
|
|
166
|
+
assert decision.tier == ModelTier.REASON
|
|
167
|
+
|
|
168
|
+
def test_small_token_count_routes_to_fast(self, router: ModelRouter) -> None:
|
|
169
|
+
"""A task with no matching tags and small token budget should use FAST."""
|
|
170
|
+
signal = TaskSignal(
|
|
171
|
+
description="Some unknown task",
|
|
172
|
+
tags=[],
|
|
173
|
+
estimated_tokens=100,
|
|
174
|
+
)
|
|
175
|
+
decision = router.route(signal)
|
|
176
|
+
assert decision.tier == ModelTier.FAST
|
|
177
|
+
|
|
178
|
+
def test_exactly_threshold_tokens_routes_to_fast(
|
|
179
|
+
self, router: ModelRouter
|
|
180
|
+
) -> None:
|
|
181
|
+
"""estimated_tokens == 16 000 (not strictly greater) should remain FAST."""
|
|
182
|
+
signal = TaskSignal(
|
|
183
|
+
description="Borderline task",
|
|
184
|
+
tags=[],
|
|
185
|
+
estimated_tokens=16_000,
|
|
186
|
+
)
|
|
187
|
+
decision = router.route(signal)
|
|
188
|
+
assert decision.tier == ModelTier.FAST
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# ---------------------------------------------------------------------------
|
|
192
|
+
# Priority conflict resolution
|
|
193
|
+
# ---------------------------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class TestTagRulePriority:
|
|
197
|
+
"""Higher-priority rules win when multiple rules match."""
|
|
198
|
+
|
|
199
|
+
def test_higher_priority_rule_wins(self) -> None:
|
|
200
|
+
"""When two rules match the same tags, the higher priority one wins."""
|
|
201
|
+
config = ModelRouterConfig(
|
|
202
|
+
tier_models={
|
|
203
|
+
ModelTier.CODE.value: ["devstral"],
|
|
204
|
+
ModelTier.REASON.value: ["deepseek-r1"],
|
|
205
|
+
},
|
|
206
|
+
tag_rules=[
|
|
207
|
+
TagRule(keywords=["analyze"], tier=ModelTier.CODE, priority=5),
|
|
208
|
+
TagRule(keywords=["analyze"], tier=ModelTier.REASON, priority=15),
|
|
209
|
+
],
|
|
210
|
+
)
|
|
211
|
+
router = ModelRouter(config=config)
|
|
212
|
+
signal = TaskSignal(description="Analyze dependencies", tags=["analyze"])
|
|
213
|
+
decision = router.route(signal)
|
|
214
|
+
assert decision.tier == ModelTier.REASON
|
|
215
|
+
|
|
216
|
+
def test_lower_priority_rule_loses(self) -> None:
|
|
217
|
+
"""The lower-priority rule must not override the higher-priority one."""
|
|
218
|
+
config = ModelRouterConfig(
|
|
219
|
+
tier_models={
|
|
220
|
+
ModelTier.FAST.value: ["haiku"],
|
|
221
|
+
ModelTier.NUANCE.value: ["kimi-k2.5"],
|
|
222
|
+
},
|
|
223
|
+
tag_rules=[
|
|
224
|
+
TagRule(keywords=["copy"], tier=ModelTier.NUANCE, priority=20),
|
|
225
|
+
TagRule(keywords=["copy"], tier=ModelTier.FAST, priority=1),
|
|
226
|
+
],
|
|
227
|
+
)
|
|
228
|
+
router = ModelRouter(config=config)
|
|
229
|
+
signal = TaskSignal(description="Write marketing copy", tags=["copy"])
|
|
230
|
+
decision = router.route(signal)
|
|
231
|
+
assert decision.tier == ModelTier.NUANCE
|
|
232
|
+
|
|
233
|
+
def test_no_overlap_falls_through_to_fallback(self) -> None:
|
|
234
|
+
"""Rules with no keyword overlap must not fire."""
|
|
235
|
+
config = ModelRouterConfig(
|
|
236
|
+
tier_models={
|
|
237
|
+
ModelTier.FAST.value: ["haiku"],
|
|
238
|
+
ModelTier.CODE.value: ["devstral"],
|
|
239
|
+
},
|
|
240
|
+
tag_rules=[
|
|
241
|
+
TagRule(keywords=["code"], tier=ModelTier.CODE, priority=10),
|
|
242
|
+
],
|
|
243
|
+
)
|
|
244
|
+
router = ModelRouter(config=config)
|
|
245
|
+
signal = TaskSignal(description="Some unrelated task", tags=["unknown"])
|
|
246
|
+
decision = router.route(signal)
|
|
247
|
+
# No rule matched; small token budget → FAST
|
|
248
|
+
assert decision.tier == ModelTier.FAST
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
# ---------------------------------------------------------------------------
|
|
252
|
+
# Config load from YAML
|
|
253
|
+
# ---------------------------------------------------------------------------
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
class TestConfigFromYaml:
|
|
257
|
+
"""ModelRouter.from_config loads settings from a YAML file."""
|
|
258
|
+
|
|
259
|
+
def test_load_from_yaml(self, tmp_path: Path) -> None:
|
|
260
|
+
"""A minimal valid YAML config should load without errors."""
|
|
261
|
+
yaml_content = textwrap.dedent(
|
|
262
|
+
"""\
|
|
263
|
+
tier_models:
|
|
264
|
+
fast: [my-fast-model]
|
|
265
|
+
code: [my-code-model]
|
|
266
|
+
reason: [my-reason-model]
|
|
267
|
+
nuance: [my-nuance-model]
|
|
268
|
+
local: [my-local-model]
|
|
269
|
+
tag_rules:
|
|
270
|
+
- keywords: [code, implement]
|
|
271
|
+
tier: code
|
|
272
|
+
priority: 10
|
|
273
|
+
- keywords: [writing, email]
|
|
274
|
+
tier: nuance
|
|
275
|
+
priority: 10
|
|
276
|
+
"""
|
|
277
|
+
)
|
|
278
|
+
config_file = tmp_path / "router_config.yaml"
|
|
279
|
+
config_file.write_text(yaml_content)
|
|
280
|
+
|
|
281
|
+
router = ModelRouter.from_config(config_file)
|
|
282
|
+
|
|
283
|
+
code_signal = TaskSignal(description="Implement feature X", tags=["code"])
|
|
284
|
+
decision = router.route(code_signal)
|
|
285
|
+
assert decision.tier == ModelTier.CODE
|
|
286
|
+
assert decision.model_name == "my-code-model"
|
|
287
|
+
|
|
288
|
+
def test_yaml_nuance_rule(self, tmp_path: Path) -> None:
|
|
289
|
+
"""YAML-loaded NUANCE rule fires correctly on matching tags."""
|
|
290
|
+
yaml_content = textwrap.dedent(
|
|
291
|
+
"""\
|
|
292
|
+
tier_models:
|
|
293
|
+
nuance: [yaml-nuance-model]
|
|
294
|
+
fast: [yaml-fast-model]
|
|
295
|
+
tag_rules:
|
|
296
|
+
- keywords: [writing, email]
|
|
297
|
+
tier: nuance
|
|
298
|
+
priority: 10
|
|
299
|
+
"""
|
|
300
|
+
)
|
|
301
|
+
config_file = tmp_path / "router.yaml"
|
|
302
|
+
config_file.write_text(yaml_content)
|
|
303
|
+
|
|
304
|
+
router = ModelRouter.from_config(config_file)
|
|
305
|
+
signal = TaskSignal(description="Draft an email", tags=["email"])
|
|
306
|
+
decision = router.route(signal)
|
|
307
|
+
assert decision.tier == ModelTier.NUANCE
|
|
308
|
+
assert decision.model_name == "yaml-nuance-model"
|
|
309
|
+
|
|
310
|
+
def test_missing_file_raises(self, tmp_path: Path) -> None:
|
|
311
|
+
"""Loading from a non-existent path must raise FileNotFoundError."""
|
|
312
|
+
with pytest.raises(FileNotFoundError):
|
|
313
|
+
ModelRouter.from_config(tmp_path / "nonexistent.yaml")
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
# ---------------------------------------------------------------------------
|
|
317
|
+
# RouteDecision content
|
|
318
|
+
# ---------------------------------------------------------------------------
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
class TestRouteDecisionContent:
|
|
322
|
+
"""RouteDecision always contains a non-empty reasoning string."""
|
|
323
|
+
|
|
324
|
+
def test_reasoning_is_non_empty(self, router: ModelRouter) -> None:
|
|
325
|
+
"""Every decision must include a human-readable reasoning string."""
|
|
326
|
+
signal = TaskSignal(description="Do something", tags=["code"])
|
|
327
|
+
decision = router.route(signal)
|
|
328
|
+
assert decision.reasoning
|
|
329
|
+
assert len(decision.reasoning) > 0
|
|
330
|
+
|
|
331
|
+
def test_preferred_node_none_by_default(self, router: ModelRouter) -> None:
|
|
332
|
+
"""preferred_node should be None unless locality is required."""
|
|
333
|
+
signal = TaskSignal(description="Regular coding task", tags=["implement"])
|
|
334
|
+
decision = router.route(signal)
|
|
335
|
+
assert decision.preferred_node is None
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
# ---------------------------------------------------------------------------
|
|
339
|
+
# Model name resolution
|
|
340
|
+
# ---------------------------------------------------------------------------
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
class TestModelNameResolution:
|
|
344
|
+
"""Verify the correct concrete model is selected per tier."""
|
|
345
|
+
|
|
346
|
+
def test_default_fast_model(self, router: ModelRouter) -> None:
|
|
347
|
+
signal = TaskSignal(description="quick task", tags=["simple"])
|
|
348
|
+
decision = router.route(signal)
|
|
349
|
+
assert decision.model_name == "llama3.2"
|
|
350
|
+
|
|
351
|
+
def test_default_code_model(self, router: ModelRouter) -> None:
|
|
352
|
+
signal = TaskSignal(description="implement feature", tags=["code"])
|
|
353
|
+
decision = router.route(signal)
|
|
354
|
+
assert decision.model_name == "devstral"
|
|
355
|
+
|
|
356
|
+
def test_default_reason_model(self, router: ModelRouter) -> None:
|
|
357
|
+
signal = TaskSignal(description="system design", tags=["architecture"])
|
|
358
|
+
decision = router.route(signal)
|
|
359
|
+
assert decision.model_name == "deepseek-r1:8b"
|
|
360
|
+
|
|
361
|
+
def test_default_nuance_model(self, router: ModelRouter) -> None:
|
|
362
|
+
signal = TaskSignal(description="write copy", tags=["marketing"])
|
|
363
|
+
decision = router.route(signal)
|
|
364
|
+
assert decision.model_name == "moonshot-v1-128k"
|
|
365
|
+
|
|
366
|
+
def test_default_local_model(self, router: ModelRouter) -> None:
|
|
367
|
+
signal = TaskSignal(description="private task", privacy_sensitive=True)
|
|
368
|
+
decision = router.route(signal)
|
|
369
|
+
assert decision.model_name == "llama3.2"
|
|
370
|
+
|
|
371
|
+
def test_unknown_tier_sentinel(self) -> None:
|
|
372
|
+
"""Missing tier config produces an unknown-{tier} sentinel."""
|
|
373
|
+
config = ModelRouterConfig(tier_models={}, tag_rules=[])
|
|
374
|
+
router = ModelRouter(config=config)
|
|
375
|
+
signal = TaskSignal(description="task")
|
|
376
|
+
decision = router.route(signal)
|
|
377
|
+
assert decision.model_name == "unknown-fast"
|
|
378
|
+
|
|
379
|
+
def test_custom_model_name(self) -> None:
|
|
380
|
+
config = ModelRouterConfig(
|
|
381
|
+
tier_models={"code": ["my-custom-coder"]},
|
|
382
|
+
tag_rules=[TagRule(keywords=["code"], tier=ModelTier.CODE, priority=10)],
|
|
383
|
+
)
|
|
384
|
+
router = ModelRouter(config=config)
|
|
385
|
+
signal = TaskSignal(description="code task", tags=["code"])
|
|
386
|
+
decision = router.route(signal)
|
|
387
|
+
assert decision.model_name == "my-custom-coder"
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
# ---------------------------------------------------------------------------
|
|
391
|
+
# Edge cases
|
|
392
|
+
# ---------------------------------------------------------------------------
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
class TestEdgeCases:
|
|
396
|
+
"""Boundary conditions and unusual inputs."""
|
|
397
|
+
|
|
398
|
+
def test_empty_description(self, router: ModelRouter) -> None:
|
|
399
|
+
signal = TaskSignal(description="", tags=["code"])
|
|
400
|
+
decision = router.route(signal)
|
|
401
|
+
assert decision.tier == ModelTier.CODE
|
|
402
|
+
|
|
403
|
+
def test_token_boundary_16000_is_fast(self, router: ModelRouter) -> None:
|
|
404
|
+
"""Exactly 16000 tokens (not strictly >) stays FAST."""
|
|
405
|
+
signal = TaskSignal(description="boundary", estimated_tokens=16_000)
|
|
406
|
+
decision = router.route(signal)
|
|
407
|
+
assert decision.tier == ModelTier.FAST
|
|
408
|
+
|
|
409
|
+
def test_token_boundary_16001_is_reason(self, router: ModelRouter) -> None:
|
|
410
|
+
signal = TaskSignal(description="boundary", estimated_tokens=16_001)
|
|
411
|
+
decision = router.route(signal)
|
|
412
|
+
assert decision.tier == ModelTier.REASON
|
|
413
|
+
|
|
414
|
+
def test_model_dump_serializable(self, router: ModelRouter) -> None:
|
|
415
|
+
"""RouteDecision.model_dump() produces JSON-serializable dict."""
|
|
416
|
+
import json
|
|
417
|
+
|
|
418
|
+
signal = TaskSignal(description="test", tags=["code"])
|
|
419
|
+
decision = router.route(signal)
|
|
420
|
+
dumped = decision.model_dump()
|
|
421
|
+
serialized = json.dumps(dumped)
|
|
422
|
+
assert isinstance(serialized, str)
|
|
423
|
+
parsed = json.loads(serialized)
|
|
424
|
+
assert parsed["tier"] == "code"
|
|
425
|
+
|
|
426
|
+
def test_all_tag_keywords_covered(self, router: ModelRouter) -> None:
|
|
427
|
+
"""Each default tag rule keyword individually routes to its tier."""
|
|
428
|
+
tier_keywords = {
|
|
429
|
+
ModelTier.CODE: ["code", "refactor", "debug", "test", "implement"],
|
|
430
|
+
ModelTier.REASON: ["architecture", "design", "analyze", "research", "plan"],
|
|
431
|
+
ModelTier.NUANCE: [
|
|
432
|
+
"marketing", "creative", "email", "copy", "comms", "writing",
|
|
433
|
+
],
|
|
434
|
+
ModelTier.FAST: ["format", "rename", "lint", "simple", "trivial"],
|
|
435
|
+
}
|
|
436
|
+
for expected_tier, keywords in tier_keywords.items():
|
|
437
|
+
for kw in keywords:
|
|
438
|
+
signal = TaskSignal(description=f"task-{kw}", tags=[kw])
|
|
439
|
+
decision = router.route(signal)
|
|
440
|
+
assert decision.tier == expected_tier, (
|
|
441
|
+
f"keyword '{kw}' routed to {decision.tier}, expected {expected_tier}"
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
# ---------------------------------------------------------------------------
|
|
446
|
+
# MCP tool handler integration
|
|
447
|
+
# ---------------------------------------------------------------------------
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
class TestMCPModelRouteHandler:
|
|
451
|
+
"""Test the _handle_model_route MCP tool handler."""
|
|
452
|
+
|
|
453
|
+
@pytest.fixture(autouse=True)
|
|
454
|
+
def _import_handler(self):
|
|
455
|
+
from skcapstone.mcp_tools.model_tools import _handle_model_route
|
|
456
|
+
|
|
457
|
+
self.handler = _handle_model_route
|
|
458
|
+
|
|
459
|
+
@pytest.mark.asyncio
|
|
460
|
+
async def test_basic_route(self) -> None:
|
|
461
|
+
result = await self.handler({"description": "implement login"})
|
|
462
|
+
assert len(result) == 1
|
|
463
|
+
import json
|
|
464
|
+
|
|
465
|
+
data = json.loads(result[0].text)
|
|
466
|
+
assert "tier" in data
|
|
467
|
+
assert "model_name" in data
|
|
468
|
+
assert "reasoning" in data
|
|
469
|
+
|
|
470
|
+
@pytest.mark.asyncio
|
|
471
|
+
async def test_route_with_tags(self) -> None:
|
|
472
|
+
import json
|
|
473
|
+
|
|
474
|
+
result = await self.handler({
|
|
475
|
+
"description": "refactor auth module",
|
|
476
|
+
"tags": ["code", "refactor"],
|
|
477
|
+
})
|
|
478
|
+
data = json.loads(result[0].text)
|
|
479
|
+
assert data["tier"] == "code"
|
|
480
|
+
|
|
481
|
+
@pytest.mark.asyncio
|
|
482
|
+
async def test_route_privacy_sensitive(self) -> None:
|
|
483
|
+
import json
|
|
484
|
+
|
|
485
|
+
result = await self.handler({
|
|
486
|
+
"description": "process medical records",
|
|
487
|
+
"privacy_sensitive": True,
|
|
488
|
+
})
|
|
489
|
+
data = json.loads(result[0].text)
|
|
490
|
+
assert data["tier"] == "local"
|
|
491
|
+
|
|
492
|
+
@pytest.mark.asyncio
|
|
493
|
+
async def test_route_localhost(self) -> None:
|
|
494
|
+
import json
|
|
495
|
+
|
|
496
|
+
result = await self.handler({
|
|
497
|
+
"description": "local benchmark",
|
|
498
|
+
"requires_localhost": True,
|
|
499
|
+
})
|
|
500
|
+
data = json.loads(result[0].text)
|
|
501
|
+
assert data["tier"] == "local"
|
|
502
|
+
assert data["preferred_node"] == "localhost"
|
|
503
|
+
|
|
504
|
+
@pytest.mark.asyncio
|
|
505
|
+
async def test_route_with_token_estimate(self) -> None:
|
|
506
|
+
import json
|
|
507
|
+
|
|
508
|
+
result = await self.handler({
|
|
509
|
+
"description": "big analysis",
|
|
510
|
+
"estimated_tokens": 30_000,
|
|
511
|
+
})
|
|
512
|
+
data = json.loads(result[0].text)
|
|
513
|
+
assert data["tier"] == "reason"
|
|
514
|
+
|
|
515
|
+
@pytest.mark.asyncio
|
|
516
|
+
async def test_route_minimal_args(self) -> None:
|
|
517
|
+
"""Handler works with only the required 'description' field."""
|
|
518
|
+
import json
|
|
519
|
+
|
|
520
|
+
result = await self.handler({"description": "anything"})
|
|
521
|
+
data = json.loads(result[0].text)
|
|
522
|
+
assert data["tier"] == "fast"
|
|
523
|
+
|
|
524
|
+
@pytest.mark.asyncio
|
|
525
|
+
async def test_route_empty_description(self) -> None:
|
|
526
|
+
import json
|
|
527
|
+
|
|
528
|
+
result = await self.handler({"description": ""})
|
|
529
|
+
data = json.loads(result[0].text)
|
|
530
|
+
assert "tier" in data
|
|
531
|
+
|
|
532
|
+
@pytest.mark.asyncio
|
|
533
|
+
async def test_route_all_fields(self) -> None:
|
|
534
|
+
"""Handler accepts all optional fields together."""
|
|
535
|
+
import json
|
|
536
|
+
|
|
537
|
+
result = await self.handler({
|
|
538
|
+
"description": "sensitive local code review",
|
|
539
|
+
"tags": ["code"],
|
|
540
|
+
"requires_localhost": False,
|
|
541
|
+
"privacy_sensitive": True,
|
|
542
|
+
"estimated_tokens": 50_000,
|
|
543
|
+
})
|
|
544
|
+
data = json.loads(result[0].text)
|
|
545
|
+
# privacy_sensitive takes precedence
|
|
546
|
+
assert data["tier"] == "local"
|