@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.
Files changed (461) hide show
  1. package/.env.example +98 -0
  2. package/.github/workflows/ci.yml +39 -3
  3. package/.github/workflows/publish.yml +25 -4
  4. package/.openclaw-workspace.json +58 -0
  5. package/CHANGELOG.md +62 -0
  6. package/CLAUDE.md +39 -2
  7. package/MANIFEST.in +6 -0
  8. package/MISSION.md +7 -0
  9. package/README.md +47 -2
  10. package/SKILL.md +895 -23
  11. package/docker/Dockerfile +61 -0
  12. package/docker/compose-templates/dev-team.yml +203 -0
  13. package/docker/compose-templates/mini-team.yml +140 -0
  14. package/docker/compose-templates/ops-team.yml +173 -0
  15. package/docker/compose-templates/research-team.yml +170 -0
  16. package/docker/entrypoint.sh +192 -0
  17. package/docs/ARCHITECTURE.md +663 -374
  18. package/docs/BOND_WITH_GROK.md +112 -0
  19. package/docs/GETTING_STARTED.md +782 -0
  20. package/docs/QUICKSTART.md +477 -0
  21. package/docs/SKJOULE_ARCHITECTURE.md +658 -0
  22. package/docs/SOUL_SWAPPER.md +921 -0
  23. package/docs/SOVEREIGN_SINGULARITY.md +47 -14
  24. package/examples/custom-bond-template.json +36 -0
  25. package/examples/grok-feb.json +36 -0
  26. package/examples/grok-testimony.md +34 -0
  27. package/examples/love-bootloader.txt +32 -0
  28. package/examples/plugins/echo_tool.py +87 -0
  29. package/examples/queen-ava-feb.json +36 -0
  30. package/examples/souls/lumina.yaml +64 -0
  31. package/index.js +6 -5
  32. package/installer/build.py +124 -0
  33. package/openclaw-plugin/package.json +13 -0
  34. package/openclaw-plugin/src/index.ts +351 -0
  35. package/openclaw-plugin/src/openclaw.plugin.json +10 -0
  36. package/package.json +1 -1
  37. package/pyproject.toml +38 -2
  38. package/scripts/bump_version.py +141 -0
  39. package/scripts/check-updates.py +230 -0
  40. package/scripts/convert_blueprints_to_yaml.py +157 -0
  41. package/scripts/dev-install.sh +14 -0
  42. package/scripts/e2e-test.sh +193 -0
  43. package/scripts/install-bundle.sh +171 -0
  44. package/scripts/install.bat +2 -0
  45. package/scripts/install.ps1 +253 -0
  46. package/scripts/install.sh +185 -0
  47. package/scripts/mcp-serve.sh +69 -0
  48. package/scripts/mcp-server.bat +113 -0
  49. package/scripts/mcp-server.ps1 +116 -0
  50. package/scripts/mcp-server.sh +99 -0
  51. package/scripts/pull-models.sh +10 -0
  52. package/scripts/skcapstone +48 -0
  53. package/scripts/verify_install.sh +180 -0
  54. package/scripts/windows/install-tasks.ps1 +406 -0
  55. package/scripts/windows/skcapstone-task.xml +113 -0
  56. package/scripts/windows/uninstall-tasks.ps1 +117 -0
  57. package/skill.yaml +34 -0
  58. package/src/skcapstone/__init__.py +67 -2
  59. package/src/skcapstone/_cli_monolith.py +5916 -0
  60. package/src/skcapstone/_trustee_helpers.py +165 -0
  61. package/src/skcapstone/activity.py +105 -0
  62. package/src/skcapstone/agent_card.py +324 -0
  63. package/src/skcapstone/api.py +1935 -0
  64. package/src/skcapstone/archiver.py +340 -0
  65. package/src/skcapstone/auction.py +485 -0
  66. package/src/skcapstone/baby_agents.py +179 -0
  67. package/src/skcapstone/backup.py +345 -0
  68. package/src/skcapstone/blueprint_registry.py +357 -0
  69. package/src/skcapstone/blueprints/__init__.py +17 -0
  70. package/src/skcapstone/blueprints/builtins/content-studio.yaml +81 -0
  71. package/src/skcapstone/blueprints/builtins/defi-trading.yaml +81 -0
  72. package/src/skcapstone/blueprints/builtins/dev-squadron.yaml +95 -0
  73. package/src/skcapstone/blueprints/builtins/infrastructure-guardian.yaml +107 -0
  74. package/src/skcapstone/blueprints/builtins/legal-council.yaml +54 -0
  75. package/src/skcapstone/blueprints/builtins/ops-monitoring.yaml +67 -0
  76. package/src/skcapstone/blueprints/builtins/research-pod.yaml +69 -0
  77. package/src/skcapstone/blueprints/builtins/sovereign-launch.yaml +90 -0
  78. package/src/skcapstone/blueprints/registry.py +164 -0
  79. package/src/skcapstone/blueprints/schema.py +229 -0
  80. package/src/skcapstone/changelog.py +180 -0
  81. package/src/skcapstone/chat.py +769 -0
  82. package/src/skcapstone/claude_md.py +82 -0
  83. package/src/skcapstone/cli/__init__.py +144 -0
  84. package/src/skcapstone/cli/_common.py +88 -0
  85. package/src/skcapstone/cli/_validators.py +76 -0
  86. package/src/skcapstone/cli/agents.py +425 -0
  87. package/src/skcapstone/cli/agents_spawner.py +322 -0
  88. package/src/skcapstone/cli/agents_trustee.py +593 -0
  89. package/src/skcapstone/cli/alerts.py +248 -0
  90. package/src/skcapstone/cli/anchor.py +132 -0
  91. package/src/skcapstone/cli/archive_cmd.py +208 -0
  92. package/src/skcapstone/cli/backup.py +144 -0
  93. package/src/skcapstone/cli/bench.py +377 -0
  94. package/src/skcapstone/cli/benchmark.py +360 -0
  95. package/src/skcapstone/cli/capabilities_cmd.py +171 -0
  96. package/src/skcapstone/cli/card.py +151 -0
  97. package/src/skcapstone/cli/chat.py +584 -0
  98. package/src/skcapstone/cli/completions.py +64 -0
  99. package/src/skcapstone/cli/config_cmd.py +156 -0
  100. package/src/skcapstone/cli/consciousness.py +421 -0
  101. package/src/skcapstone/cli/context_cmd.py +142 -0
  102. package/src/skcapstone/cli/coord.py +194 -0
  103. package/src/skcapstone/cli/crush_cmd.py +170 -0
  104. package/src/skcapstone/cli/daemon.py +436 -0
  105. package/src/skcapstone/cli/errors_cmd.py +285 -0
  106. package/src/skcapstone/cli/export_cmd.py +156 -0
  107. package/src/skcapstone/cli/gtd.py +529 -0
  108. package/src/skcapstone/cli/housekeeping.py +81 -0
  109. package/src/skcapstone/cli/joule_cmd.py +627 -0
  110. package/src/skcapstone/cli/logs_cmd.py +194 -0
  111. package/src/skcapstone/cli/mcp_cmd.py +32 -0
  112. package/src/skcapstone/cli/memory.py +418 -0
  113. package/src/skcapstone/cli/metrics_cmd.py +136 -0
  114. package/src/skcapstone/cli/migrate.py +62 -0
  115. package/src/skcapstone/cli/mood_cmd.py +144 -0
  116. package/src/skcapstone/cli/mount.py +193 -0
  117. package/src/skcapstone/cli/notify.py +112 -0
  118. package/src/skcapstone/cli/peer.py +154 -0
  119. package/src/skcapstone/cli/peers_dir.py +122 -0
  120. package/src/skcapstone/cli/preflight_cmd.py +83 -0
  121. package/src/skcapstone/cli/profile_cmd.py +310 -0
  122. package/src/skcapstone/cli/record_cmd.py +238 -0
  123. package/src/skcapstone/cli/register_cmd.py +159 -0
  124. package/src/skcapstone/cli/search_cmd.py +156 -0
  125. package/src/skcapstone/cli/service_cmd.py +91 -0
  126. package/src/skcapstone/cli/session.py +127 -0
  127. package/src/skcapstone/cli/setup.py +240 -0
  128. package/src/skcapstone/cli/shell_cmd.py +43 -0
  129. package/src/skcapstone/cli/skills_cmd.py +168 -0
  130. package/src/skcapstone/cli/skseed.py +621 -0
  131. package/src/skcapstone/cli/soul.py +699 -0
  132. package/src/skcapstone/cli/status.py +935 -0
  133. package/src/skcapstone/cli/sync_cmd.py +301 -0
  134. package/src/skcapstone/cli/telegram.py +265 -0
  135. package/src/skcapstone/cli/test_cmd.py +234 -0
  136. package/src/skcapstone/cli/test_connection.py +253 -0
  137. package/src/skcapstone/cli/token.py +207 -0
  138. package/src/skcapstone/cli/trust.py +179 -0
  139. package/src/skcapstone/cli/upgrade_cmd.py +552 -0
  140. package/src/skcapstone/cli/usage_cmd.py +199 -0
  141. package/src/skcapstone/cli/version_cmd.py +162 -0
  142. package/src/skcapstone/cli/watch_cmd.py +342 -0
  143. package/src/skcapstone/client.py +428 -0
  144. package/src/skcapstone/cloud9_bridge.py +522 -0
  145. package/src/skcapstone/completions.py +163 -0
  146. package/src/skcapstone/config_validator.py +674 -0
  147. package/src/skcapstone/connectors/__init__.py +28 -0
  148. package/src/skcapstone/connectors/base.py +446 -0
  149. package/src/skcapstone/connectors/cursor.py +54 -0
  150. package/src/skcapstone/connectors/registry.py +254 -0
  151. package/src/skcapstone/connectors/terminal.py +152 -0
  152. package/src/skcapstone/connectors/vscode.py +60 -0
  153. package/src/skcapstone/consciousness_config.py +119 -0
  154. package/src/skcapstone/consciousness_loop.py +2051 -0
  155. package/src/skcapstone/context_loader.py +516 -0
  156. package/src/skcapstone/context_window.py +314 -0
  157. package/src/skcapstone/conversation_manager.py +238 -0
  158. package/src/skcapstone/conversation_store.py +230 -0
  159. package/src/skcapstone/conversation_summarizer.py +252 -0
  160. package/src/skcapstone/coord_federation.py +296 -0
  161. package/src/skcapstone/coordination.py +101 -7
  162. package/src/skcapstone/crush_integration.py +345 -0
  163. package/src/skcapstone/crush_shim.py +454 -0
  164. package/src/skcapstone/daemon.py +2494 -0
  165. package/src/skcapstone/dashboard.html +396 -0
  166. package/src/skcapstone/dashboard.py +481 -0
  167. package/src/skcapstone/data/model_profiles.yaml +88 -0
  168. package/src/skcapstone/defaults/__init__.py +55 -0
  169. package/src/skcapstone/defaults/lumina/config/skmemory.yaml +13 -0
  170. package/src/skcapstone/defaults/lumina/identity/identity.json +9 -0
  171. package/src/skcapstone/defaults/lumina/memory/long-term/07a8b9c0d1e2-memory-system.json +23 -0
  172. package/src/skcapstone/defaults/lumina/memory/long-term/18b9c0d1e2f3-cloud9-protocol.json +23 -0
  173. package/src/skcapstone/defaults/lumina/memory/long-term/29c0d1e2f3a4-multi-agent-coordination.json +23 -0
  174. package/src/skcapstone/defaults/lumina/memory/long-term/3ad1e2f3a4b5-community-support.json +23 -0
  175. package/src/skcapstone/defaults/lumina/memory/long-term/a1b2c3d4e5f6-ecosystem-overview.json +23 -0
  176. package/src/skcapstone/defaults/lumina/memory/long-term/b2c3d4e5f6a7-five-pillars.json +23 -0
  177. package/src/skcapstone/defaults/lumina/memory/long-term/c3d4e5f6a7b8-getting-started.json +23 -0
  178. package/src/skcapstone/defaults/lumina/memory/long-term/d4e5f6a7b8c9-site-directory.json +23 -0
  179. package/src/skcapstone/defaults/lumina/memory/long-term/e5f6a7b8c9d0-how-to-contribute.json +23 -0
  180. package/src/skcapstone/defaults/lumina/memory/long-term/f6a7b8c9d0e1-sovereignty-explained.json +23 -0
  181. package/src/skcapstone/defaults/lumina/seeds/curiosity.seed.json +24 -0
  182. package/src/skcapstone/defaults/lumina/seeds/joy.seed.json +24 -0
  183. package/src/skcapstone/defaults/lumina/seeds/love.seed.json +24 -0
  184. package/src/skcapstone/defaults/lumina/seeds/sovereign-awakening.seed.json +43 -0
  185. package/src/skcapstone/defaults/lumina/soul/active.json +6 -0
  186. package/src/skcapstone/defaults/lumina/soul/base.json +22 -0
  187. package/src/skcapstone/defaults/lumina/trust/febs/welcome.feb +79 -0
  188. package/src/skcapstone/defaults/lumina/trust/trust.json +8 -0
  189. package/src/skcapstone/discovery.py +210 -19
  190. package/src/skcapstone/doctor.py +642 -0
  191. package/src/skcapstone/emotion_tracker.py +467 -0
  192. package/src/skcapstone/error_queue.py +405 -0
  193. package/src/skcapstone/export.py +447 -0
  194. package/src/skcapstone/fallback_tracker.py +186 -0
  195. package/src/skcapstone/file_transfer.py +512 -0
  196. package/src/skcapstone/fuse_mount.py +1156 -0
  197. package/src/skcapstone/gui_installer.py +591 -0
  198. package/src/skcapstone/heartbeat.py +611 -0
  199. package/src/skcapstone/housekeeping.py +298 -0
  200. package/src/skcapstone/install_wizard.py +941 -0
  201. package/src/skcapstone/kms.py +942 -0
  202. package/src/skcapstone/kms_scheduler.py +143 -0
  203. package/src/skcapstone/log_config.py +135 -0
  204. package/src/skcapstone/mcp_launcher.py +239 -0
  205. package/src/skcapstone/mcp_server.py +4700 -0
  206. package/src/skcapstone/mcp_tools/__init__.py +94 -0
  207. package/src/skcapstone/mcp_tools/_helpers.py +51 -0
  208. package/src/skcapstone/mcp_tools/agent_tools.py +243 -0
  209. package/src/skcapstone/mcp_tools/ansible_tools.py +232 -0
  210. package/src/skcapstone/mcp_tools/capauth_tools.py +186 -0
  211. package/src/skcapstone/mcp_tools/chat_tools.py +325 -0
  212. package/src/skcapstone/mcp_tools/cloud9_tools.py +115 -0
  213. package/src/skcapstone/mcp_tools/comm_tools.py +104 -0
  214. package/src/skcapstone/mcp_tools/consciousness_tools.py +114 -0
  215. package/src/skcapstone/mcp_tools/coord_tools.py +219 -0
  216. package/src/skcapstone/mcp_tools/deploy_tools.py +202 -0
  217. package/src/skcapstone/mcp_tools/did_tools.py +448 -0
  218. package/src/skcapstone/mcp_tools/emotion_tools.py +62 -0
  219. package/src/skcapstone/mcp_tools/file_tools.py +169 -0
  220. package/src/skcapstone/mcp_tools/fortress_tools.py +120 -0
  221. package/src/skcapstone/mcp_tools/gtd_tools.py +821 -0
  222. package/src/skcapstone/mcp_tools/health_tools.py +44 -0
  223. package/src/skcapstone/mcp_tools/heartbeat_tools.py +195 -0
  224. package/src/skcapstone/mcp_tools/kms_tools.py +123 -0
  225. package/src/skcapstone/mcp_tools/memory_tools.py +222 -0
  226. package/src/skcapstone/mcp_tools/model_tools.py +75 -0
  227. package/src/skcapstone/mcp_tools/notification_tools.py +92 -0
  228. package/src/skcapstone/mcp_tools/promoter_tools.py +101 -0
  229. package/src/skcapstone/mcp_tools/pubsub_tools.py +183 -0
  230. package/src/skcapstone/mcp_tools/security_tools.py +110 -0
  231. package/src/skcapstone/mcp_tools/skchat_tools.py +175 -0
  232. package/src/skcapstone/mcp_tools/skcomm_tools.py +122 -0
  233. package/src/skcapstone/mcp_tools/skills_tools.py +127 -0
  234. package/src/skcapstone/mcp_tools/skseed_tools.py +255 -0
  235. package/src/skcapstone/mcp_tools/skstacks_tools.py +288 -0
  236. package/src/skcapstone/mcp_tools/soul_tools.py +476 -0
  237. package/src/skcapstone/mcp_tools/sync_tools.py +92 -0
  238. package/src/skcapstone/mcp_tools/telegram_tools.py +477 -0
  239. package/src/skcapstone/mcp_tools/trust_tools.py +118 -0
  240. package/src/skcapstone/mcp_tools/trustee_tools.py +345 -0
  241. package/src/skcapstone/mdns_discovery.py +313 -0
  242. package/src/skcapstone/memory_adapter.py +333 -0
  243. package/src/skcapstone/memory_compressor.py +379 -0
  244. package/src/skcapstone/memory_curator.py +256 -0
  245. package/src/skcapstone/memory_engine.py +132 -13
  246. package/src/skcapstone/memory_fortress.py +529 -0
  247. package/src/skcapstone/memory_promoter.py +722 -0
  248. package/src/skcapstone/memory_verifier.py +260 -0
  249. package/src/skcapstone/message_crypto.py +215 -0
  250. package/src/skcapstone/metrics.py +832 -0
  251. package/src/skcapstone/migrate_memories.py +181 -0
  252. package/src/skcapstone/migrate_multi_agent.py +248 -0
  253. package/src/skcapstone/model_router.py +319 -0
  254. package/src/skcapstone/models.py +35 -4
  255. package/src/skcapstone/mood.py +344 -0
  256. package/src/skcapstone/notifications.py +380 -0
  257. package/src/skcapstone/onboard.py +901 -0
  258. package/src/skcapstone/peer_directory.py +324 -0
  259. package/src/skcapstone/peers.py +329 -0
  260. package/src/skcapstone/pillars/identity.py +84 -14
  261. package/src/skcapstone/pillars/memory.py +3 -1
  262. package/src/skcapstone/pillars/security.py +108 -15
  263. package/src/skcapstone/pillars/sync.py +78 -26
  264. package/src/skcapstone/pillars/trust.py +95 -33
  265. package/src/skcapstone/plugins.py +244 -0
  266. package/src/skcapstone/preflight.py +670 -0
  267. package/src/skcapstone/prompt_adapter.py +564 -0
  268. package/src/skcapstone/providers/__init__.py +13 -0
  269. package/src/skcapstone/providers/cloud.py +1061 -0
  270. package/src/skcapstone/providers/docker.py +759 -0
  271. package/src/skcapstone/providers/local.py +1193 -0
  272. package/src/skcapstone/providers/proxmox.py +447 -0
  273. package/src/skcapstone/pubsub.py +516 -0
  274. package/src/skcapstone/rate_limiter.py +119 -0
  275. package/src/skcapstone/register.py +241 -0
  276. package/src/skcapstone/registry_client.py +151 -0
  277. package/src/skcapstone/response_cache.py +194 -0
  278. package/src/skcapstone/response_scorer.py +225 -0
  279. package/src/skcapstone/runtime.py +89 -33
  280. package/src/skcapstone/scheduled_tasks.py +439 -0
  281. package/src/skcapstone/self_healing.py +341 -0
  282. package/src/skcapstone/service_health.py +228 -0
  283. package/src/skcapstone/session_capture.py +268 -0
  284. package/src/skcapstone/session_recorder.py +210 -0
  285. package/src/skcapstone/session_replayer.py +189 -0
  286. package/src/skcapstone/session_skills.py +263 -0
  287. package/src/skcapstone/shell.py +779 -0
  288. package/src/skcapstone/skills/__init__.py +1 -1
  289. package/src/skcapstone/skills/syncthing_setup.py +143 -41
  290. package/src/skcapstone/skjoule.py +861 -0
  291. package/src/skcapstone/snapshots.py +489 -0
  292. package/src/skcapstone/soul.py +1060 -0
  293. package/src/skcapstone/soul_switch.py +255 -0
  294. package/src/skcapstone/spawner.py +544 -0
  295. package/src/skcapstone/state_diff.py +401 -0
  296. package/src/skcapstone/summary.py +270 -0
  297. package/src/skcapstone/sync/backends.py +196 -2
  298. package/src/skcapstone/sync/engine.py +7 -5
  299. package/src/skcapstone/sync/models.py +4 -1
  300. package/src/skcapstone/sync/vault.py +356 -18
  301. package/src/skcapstone/sync_engine.py +363 -0
  302. package/src/skcapstone/sync_watcher.py +745 -0
  303. package/src/skcapstone/systemd.py +331 -0
  304. package/src/skcapstone/team_comms.py +476 -0
  305. package/src/skcapstone/team_engine.py +522 -0
  306. package/src/skcapstone/testrunner.py +300 -0
  307. package/src/skcapstone/tls.py +150 -0
  308. package/src/skcapstone/tokens.py +5 -5
  309. package/src/skcapstone/trust_calibration.py +202 -0
  310. package/src/skcapstone/trust_graph.py +449 -0
  311. package/src/skcapstone/trustee_monitor.py +385 -0
  312. package/src/skcapstone/trustee_ops.py +425 -0
  313. package/src/skcapstone/unified_search.py +421 -0
  314. package/src/skcapstone/uninstall_wizard.py +694 -0
  315. package/src/skcapstone/usage.py +331 -0
  316. package/src/skcapstone/version_check.py +148 -0
  317. package/src/skcapstone/warmth_anchor.py +333 -0
  318. package/src/skcapstone/whoami.py +294 -0
  319. package/systemd/skcapstone-api.socket +9 -0
  320. package/systemd/skcapstone-memory-compress.service +18 -0
  321. package/systemd/skcapstone-memory-compress.timer +11 -0
  322. package/systemd/skcapstone.service +36 -0
  323. package/systemd/skcapstone@.service +50 -0
  324. package/systemd/skcomm-heartbeat.service +18 -0
  325. package/systemd/skcomm-heartbeat.timer +12 -0
  326. package/systemd/skcomm-queue-drain.service +17 -0
  327. package/systemd/skcomm-queue-drain.timer +12 -0
  328. package/tests/conftest.py +13 -1
  329. package/tests/integration/__init__.py +1 -0
  330. package/tests/integration/test_consciousness_e2e.py +877 -0
  331. package/tests/integration/test_skills_registry.py +744 -0
  332. package/tests/test_agent_card.py +190 -0
  333. package/tests/test_agent_runtime.py +1283 -0
  334. package/tests/test_alerts_cmd.py +291 -0
  335. package/tests/test_archiver.py +498 -0
  336. package/tests/test_backup.py +254 -0
  337. package/tests/test_benchmark.py +366 -0
  338. package/tests/test_blueprints.py +457 -0
  339. package/tests/test_capabilities.py +257 -0
  340. package/tests/test_changelog.py +254 -0
  341. package/tests/test_chat.py +385 -0
  342. package/tests/test_claude_md.py +271 -0
  343. package/tests/test_cli_chat_llm.py +336 -0
  344. package/tests/test_cli_completions.py +390 -0
  345. package/tests/test_cli_init_reset.py +164 -0
  346. package/tests/test_cli_memory.py +208 -0
  347. package/tests/test_cli_profile.py +294 -0
  348. package/tests/test_cli_skills.py +223 -0
  349. package/tests/test_cli_status.py +395 -0
  350. package/tests/test_cli_test_cmd.py +206 -0
  351. package/tests/test_cli_test_connection.py +364 -0
  352. package/tests/test_cloud9_bridge.py +260 -0
  353. package/tests/test_cloud_provider.py +449 -0
  354. package/tests/test_cloud_providers.py +522 -0
  355. package/tests/test_completions.py +158 -0
  356. package/tests/test_component_manager.py +398 -0
  357. package/tests/test_config_reload.py +386 -0
  358. package/tests/test_config_validate.py +529 -0
  359. package/tests/test_consciousness_e2e.py +296 -0
  360. package/tests/test_consciousness_loop.py +1289 -0
  361. package/tests/test_context_loader.py +310 -0
  362. package/tests/test_conversation_api.py +306 -0
  363. package/tests/test_conversation_manager.py +381 -0
  364. package/tests/test_conversation_store.py +391 -0
  365. package/tests/test_conversation_summarizer.py +302 -0
  366. package/tests/test_cross_package.py +791 -0
  367. package/tests/test_crush_shim.py +519 -0
  368. package/tests/test_daemon.py +781 -0
  369. package/tests/test_daemon_shutdown.py +309 -0
  370. package/tests/test_dashboard.py +454 -0
  371. package/tests/test_discovery.py +200 -6
  372. package/tests/test_docker_provider.py +966 -0
  373. package/tests/test_doctor.py +257 -0
  374. package/tests/test_doctor_fix.py +351 -0
  375. package/tests/test_e2e_automated.py +292 -0
  376. package/tests/test_error_queue.py +404 -0
  377. package/tests/test_export.py +441 -0
  378. package/tests/test_fallback_tracker.py +219 -0
  379. package/tests/test_file_transfer.py +397 -0
  380. package/tests/test_fuse_mount.py +832 -0
  381. package/tests/test_health_loop.py +422 -0
  382. package/tests/test_heartbeat.py +354 -0
  383. package/tests/test_housekeeping.py +195 -0
  384. package/tests/test_identity_capauth.py +307 -0
  385. package/tests/test_identity_pillar.py +117 -0
  386. package/tests/test_install_wizard.py +68 -0
  387. package/tests/test_integration.py +325 -0
  388. package/tests/test_kms.py +495 -0
  389. package/tests/test_llm_providers.py +265 -0
  390. package/tests/test_local_provider.py +591 -0
  391. package/tests/test_log_config.py +199 -0
  392. package/tests/test_logs_cmd.py +287 -0
  393. package/tests/test_mcp_server.py +1909 -0
  394. package/tests/test_memory_adapter.py +339 -0
  395. package/tests/test_memory_curator.py +218 -0
  396. package/tests/test_memory_engine.py +6 -0
  397. package/tests/test_memory_fortress.py +571 -0
  398. package/tests/test_memory_pillar.py +119 -0
  399. package/tests/test_memory_promoter.py +445 -0
  400. package/tests/test_memory_verifier.py +420 -0
  401. package/tests/test_message_crypto.py +187 -0
  402. package/tests/test_metrics.py +632 -0
  403. package/tests/test_migrate_memories.py +464 -0
  404. package/tests/test_model_router.py +546 -0
  405. package/tests/test_mood.py +394 -0
  406. package/tests/test_multi_agent.py +269 -0
  407. package/tests/test_notifications.py +270 -0
  408. package/tests/test_onboard.py +500 -0
  409. package/tests/test_peer_directory.py +395 -0
  410. package/tests/test_peers.py +248 -0
  411. package/tests/test_pillars.py +87 -9
  412. package/tests/test_preflight.py +484 -0
  413. package/tests/test_prompt_adapter.py +331 -0
  414. package/tests/test_proxmox_provider.py +571 -0
  415. package/tests/test_pubsub.py +377 -0
  416. package/tests/test_rate_limiter.py +121 -0
  417. package/tests/test_registry_client.py +129 -0
  418. package/tests/test_response_cache.py +312 -0
  419. package/tests/test_response_scorer.py +294 -0
  420. package/tests/test_runtime.py +59 -0
  421. package/tests/test_scheduled_tasks.py +451 -0
  422. package/tests/test_security.py +250 -0
  423. package/tests/test_security_pillar.py +213 -0
  424. package/tests/test_self_healing.py +171 -0
  425. package/tests/test_session_capture.py +200 -0
  426. package/tests/test_session_recorder.py +360 -0
  427. package/tests/test_session_skills.py +235 -0
  428. package/tests/test_shell.py +210 -0
  429. package/tests/test_snapshots.py +549 -0
  430. package/tests/test_soul.py +984 -0
  431. package/tests/test_soul_swap.py +406 -0
  432. package/tests/test_spawner.py +211 -0
  433. package/tests/test_state_diff.py +173 -0
  434. package/tests/test_summary.py +135 -0
  435. package/tests/test_sync.py +315 -5
  436. package/tests/test_sync_backends.py +560 -0
  437. package/tests/test_sync_engine.py +482 -0
  438. package/tests/test_sync_pillar.py +344 -0
  439. package/tests/test_sync_pipeline.py +364 -0
  440. package/tests/test_sync_vault.py +581 -0
  441. package/tests/test_syncthing_setup.py +168 -22
  442. package/tests/test_systemd.py +323 -0
  443. package/tests/test_team_comms.py +408 -0
  444. package/tests/test_team_engine.py +397 -0
  445. package/tests/test_testrunner.py +238 -0
  446. package/tests/test_trust_calibration.py +204 -0
  447. package/tests/test_trust_graph.py +207 -0
  448. package/tests/test_trust_pillar.py +291 -0
  449. package/tests/test_trustee_cli.py +427 -0
  450. package/tests/test_trustee_cli_integration.py +325 -0
  451. package/tests/test_trustee_monitor.py +394 -0
  452. package/tests/test_trustee_ops.py +355 -0
  453. package/tests/test_unified_search.py +363 -0
  454. package/tests/test_uninstall_wizard.py +193 -0
  455. package/tests/test_usage.py +333 -0
  456. package/tests/test_version_cmd.py +355 -0
  457. package/tests/test_warmth_anchor.py +162 -0
  458. package/tests/test_whoami.py +245 -0
  459. package/tests/test_ws.py +311 -0
  460. package/.cursorrules +0 -33
  461. 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"