cct-cli 0.7.9.0__py3-none-any.whl

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 (325) hide show
  1. calc_terminal/__init__.py +14 -0
  2. calc_terminal/__main__.py +14 -0
  3. calc_terminal/activity.py +1334 -0
  4. calc_terminal/agent.py +3387 -0
  5. calc_terminal/agent_runtime.py +519 -0
  6. calc_terminal/ai_context.py +447 -0
  7. calc_terminal/ai_modes.py +752 -0
  8. calc_terminal/ai_personalization.py +286 -0
  9. calc_terminal/ai_preview_feedback.py +213 -0
  10. calc_terminal/aicore.py +2572 -0
  11. calc_terminal/anim.py +367 -0
  12. calc_terminal/app.py +3685 -0
  13. calc_terminal/art.py +639 -0
  14. calc_terminal/atomsim.py +368 -0
  15. calc_terminal/attachments.py +743 -0
  16. calc_terminal/benchmark_system.py +414 -0
  17. calc_terminal/browser/__init__.py +36 -0
  18. calc_terminal/browser/browser_state.py +346 -0
  19. calc_terminal/browser/devserver.py +176 -0
  20. calc_terminal/browser/engine.py +494 -0
  21. calc_terminal/browser/navigation.py +84 -0
  22. calc_terminal/browser/preview.py +429 -0
  23. calc_terminal/browser/preview_entry.py +95 -0
  24. calc_terminal/browser/project_detector.py +144 -0
  25. calc_terminal/browser/server.py +449 -0
  26. calc_terminal/browser/state.py +75 -0
  27. calc_terminal/browser/watcher.py +99 -0
  28. calc_terminal/browser_gui/__init__.py +1 -0
  29. calc_terminal/browser_gui/__main__.py +3 -0
  30. calc_terminal/browser_gui/launcher.py +173 -0
  31. calc_terminal/browser_gui/playwright_browser.py +117 -0
  32. calc_terminal/browser_gui/qt_browser.py +1501 -0
  33. calc_terminal/browser_gui/webview_browser.py +57 -0
  34. calc_terminal/capabilities/__init__.py +35 -0
  35. calc_terminal/capabilities/adapters/__init__.py +33 -0
  36. calc_terminal/capabilities/adapters/bioinformatics.py +204 -0
  37. calc_terminal/capabilities/adapters/browser_adapter.py +205 -0
  38. calc_terminal/capabilities/adapters/filesystem.py +206 -0
  39. calc_terminal/capabilities/adapters/git_adapter.py +202 -0
  40. calc_terminal/capabilities/adapters/jupyter_adapter.py +138 -0
  41. calc_terminal/capabilities/adapters/ml_frameworks.py +158 -0
  42. calc_terminal/capabilities/adapters/platforms.py +200 -0
  43. calc_terminal/capabilities/adapters/python_exec.py +93 -0
  44. calc_terminal/capabilities/adapters/quantum_adapter.py +150 -0
  45. calc_terminal/capabilities/adapters/scientific_comp.py +123 -0
  46. calc_terminal/capabilities/adapters/structural_bio.py +161 -0
  47. calc_terminal/capabilities/adapters/terminal.py +99 -0
  48. calc_terminal/capabilities/bus.py +178 -0
  49. calc_terminal/capabilities/discovery.py +207 -0
  50. calc_terminal/capabilities/schema.py +221 -0
  51. calc_terminal/cat.ico +0 -0
  52. calc_terminal/cat_browser.py +2018 -0
  53. calc_terminal/chat_store.py +703 -0
  54. calc_terminal/cli.py +1178 -0
  55. calc_terminal/code_editor.py +640 -0
  56. calc_terminal/collaboration.py +723 -0
  57. calc_terminal/commands_data.py +139 -0
  58. calc_terminal/compatibility_engine.py +352 -0
  59. calc_terminal/compute/__init__.py +31 -0
  60. calc_terminal/compute/fabric.py +350 -0
  61. calc_terminal/config.py +227 -0
  62. calc_terminal/core/__init__.py +41 -0
  63. calc_terminal/core/checkpoint.py +156 -0
  64. calc_terminal/core/input/__init__.py +45 -0
  65. calc_terminal/core/mode_registry.py +300 -0
  66. calc_terminal/core/project_graph.py +172 -0
  67. calc_terminal/core/recovery.py +129 -0
  68. calc_terminal/core/security_layer.py +112 -0
  69. calc_terminal/core/task_graph.py +202 -0
  70. calc_terminal/core/unified_runtime.py +184 -0
  71. calc_terminal/core/verification.py +257 -0
  72. calc_terminal/customization.py +1566 -0
  73. calc_terminal/derivations.py +153 -0
  74. calc_terminal/device_control.py +263 -0
  75. calc_terminal/diagnostics/__init__.py +27 -0
  76. calc_terminal/diagnostics/doctor_engine.py +382 -0
  77. calc_terminal/diagnostics/self_test.py +247 -0
  78. calc_terminal/doctor.py +519 -0
  79. calc_terminal/easter_eggs.py +274 -0
  80. calc_terminal/editor/__init__.py +1 -0
  81. calc_terminal/editor/actions.py +263 -0
  82. calc_terminal/editor/commands.py +160 -0
  83. calc_terminal/editor/shortcuts.py +226 -0
  84. calc_terminal/engine.py +259 -0
  85. calc_terminal/errors.py +120 -0
  86. calc_terminal/event_stream.py +146 -0
  87. calc_terminal/eventbus.py +133 -0
  88. calc_terminal/extensions.py +733 -0
  89. calc_terminal/fallback_cli.py +1321 -0
  90. calc_terminal/first_run.py +265 -0
  91. calc_terminal/fomoji_auth.py +1043 -0
  92. calc_terminal/formulas.py +82 -0
  93. calc_terminal/fs_cache.py +121 -0
  94. calc_terminal/fs_watcher.py +277 -0
  95. calc_terminal/game.py +193 -0
  96. calc_terminal/gen1.py +5 -0
  97. calc_terminal/generators.py +245 -0
  98. calc_terminal/gestures/__init__.py +42 -0
  99. calc_terminal/gestures/bindings.py +175 -0
  100. calc_terminal/gestures/manager.py +477 -0
  101. calc_terminal/goodbye.py +363 -0
  102. calc_terminal/gpu3d.py +290 -0
  103. calc_terminal/graphs.py +358 -0
  104. calc_terminal/hardware_analyzer.py +440 -0
  105. calc_terminal/host/__init__.py +30 -0
  106. calc_terminal/host/browser_manager.py +187 -0
  107. calc_terminal/host/desktop.py +1386 -0
  108. calc_terminal/host/launcher.py +395 -0
  109. calc_terminal/host/terminal.py +279 -0
  110. calc_terminal/identity.py +216 -0
  111. calc_terminal/input/__init__.py +54 -0
  112. calc_terminal/input/capabilities.py +258 -0
  113. calc_terminal/input/focus.py +87 -0
  114. calc_terminal/input/gestures.py +64 -0
  115. calc_terminal/input/pointer.py +114 -0
  116. calc_terminal/input/touch.py +345 -0
  117. calc_terminal/keys.py +84 -0
  118. calc_terminal/live_automation.py +165 -0
  119. calc_terminal/mathtext.py +433 -0
  120. calc_terminal/mcp.py +386 -0
  121. calc_terminal/memory.py +337 -0
  122. calc_terminal/memory_v2.py +479 -0
  123. calc_terminal/metrics.py +333 -0
  124. calc_terminal/mode_detection.py +146 -0
  125. calc_terminal/model.py +2431 -0
  126. calc_terminal/model_router.py +665 -0
  127. calc_terminal/models/__init__.py +0 -0
  128. calc_terminal/models/active_state.py +187 -0
  129. calc_terminal/models/dynamic_registry.py +584 -0
  130. calc_terminal/models/manager.py +781 -0
  131. calc_terminal/models/model_metadata.json +3526 -0
  132. calc_terminal/models/profiles.py +194 -0
  133. calc_terminal/models/registry.py +265 -0
  134. calc_terminal/models/schema.py +197 -0
  135. calc_terminal/models/validator.py +287 -0
  136. calc_terminal/models/verification_engine.py +368 -0
  137. calc_terminal/native_picker.py +215 -0
  138. calc_terminal/ollama_catalog.py +279 -0
  139. calc_terminal/ollama_download.py +233 -0
  140. calc_terminal/orchestrator.py +304 -0
  141. calc_terminal/package_research.py +322 -0
  142. calc_terminal/packages.py +1024 -0
  143. calc_terminal/pc_specs.py +116 -0
  144. calc_terminal/permissions.py +334 -0
  145. calc_terminal/pet.py +106 -0
  146. calc_terminal/pipeline.py +505 -0
  147. calc_terminal/platform/__init__.py +491 -0
  148. calc_terminal/platform/desktop.py +491 -0
  149. calc_terminal/platform/web.py +781 -0
  150. calc_terminal/preview/__init__.py +1 -0
  151. calc_terminal/preview/dev_server.py +303 -0
  152. calc_terminal/preview/diagnostics.py +131 -0
  153. calc_terminal/preview/live_reload.py +66 -0
  154. calc_terminal/preview/manager.py +129 -0
  155. calc_terminal/project_stats.py +209 -0
  156. calc_terminal/projects.py +328 -0
  157. calc_terminal/providers/__init__.py +0 -0
  158. calc_terminal/providers/adapters/__init__.py +80 -0
  159. calc_terminal/providers/adapters/anthropic_adapter.py +127 -0
  160. calc_terminal/providers/adapters/base.py +106 -0
  161. calc_terminal/providers/adapters/chinese_adapters.py +420 -0
  162. calc_terminal/providers/adapters/gemini_adapter.py +101 -0
  163. calc_terminal/providers/adapters/ollama_adapter.py +83 -0
  164. calc_terminal/providers/adapters/openai_adapter.py +159 -0
  165. calc_terminal/providers/adapters/other_adapters.py +246 -0
  166. calc_terminal/providers/anthropic_provider.py +172 -0
  167. calc_terminal/providers/auto_update.py +416 -0
  168. calc_terminal/providers/base_provider.py +105 -0
  169. calc_terminal/providers/discovery_manager.py +207 -0
  170. calc_terminal/providers/gemini_provider.py +178 -0
  171. calc_terminal/providers/lifecycle.py +767 -0
  172. calc_terminal/providers/ollama_adapter.py +707 -0
  173. calc_terminal/providers/openai_provider.py +254 -0
  174. calc_terminal/providers/provider_manager.py +1827 -0
  175. calc_terminal/providers/providers.json +4075 -0
  176. calc_terminal/reactionsim.py +279 -0
  177. calc_terminal/registry.py +337 -0
  178. calc_terminal/report.py +162 -0
  179. calc_terminal/research/__init__.py +45 -0
  180. calc_terminal/research/artifact_intel.py +126 -0
  181. calc_terminal/research/data_lineage.py +123 -0
  182. calc_terminal/research/experiment_ledger.py +303 -0
  183. calc_terminal/research/reproducibility.py +131 -0
  184. calc_terminal/resilience/__init__.py +47 -0
  185. calc_terminal/resilience/agent_state.py +121 -0
  186. calc_terminal/resilience/capability_matcher.py +174 -0
  187. calc_terminal/resilience/circuit_breaker.py +158 -0
  188. calc_terminal/resilience/failover_engine.py +230 -0
  189. calc_terminal/resilience/health_monitor.py +192 -0
  190. calc_terminal/resilience/ollama_adapter.py +125 -0
  191. calc_terminal/resilience/orchestrator.py +312 -0
  192. calc_terminal/resilience/types.py +134 -0
  193. calc_terminal/sandbox.py +98 -0
  194. calc_terminal/scires.py +558 -0
  195. calc_terminal/security_scanner.py +126 -0
  196. calc_terminal/session.py +294 -0
  197. calc_terminal/sim3d.py +206 -0
  198. calc_terminal/solver.py +276 -0
  199. calc_terminal/sound.py +127 -0
  200. calc_terminal/task_reports.py +287 -0
  201. calc_terminal/terminal_host.py +201 -0
  202. calc_terminal/terminal_identity.py +411 -0
  203. calc_terminal/test_ai_mode_reliability.py +344 -0
  204. calc_terminal/test_browser.py +368 -0
  205. calc_terminal/test_code_editor_upgrade.py +485 -0
  206. calc_terminal/test_customization.py +1148 -0
  207. calc_terminal/test_customization_ui.py +612 -0
  208. calc_terminal/test_dynamic_registry.py +304 -0
  209. calc_terminal/test_extensions.py +436 -0
  210. calc_terminal/test_overhaul.py +557 -0
  211. calc_terminal/test_project_detect.py +255 -0
  212. calc_terminal/test_root_cause_fix.py +527 -0
  213. calc_terminal/test_stability.py +532 -0
  214. calc_terminal/test_terminal_identity.py +132 -0
  215. calc_terminal/test_v079_speed.py +460 -0
  216. calc_terminal/theme.py +1107 -0
  217. calc_terminal/timeline.py +139 -0
  218. calc_terminal/todos.py +246 -0
  219. calc_terminal/tool_call_normalizer.py +419 -0
  220. calc_terminal/tui.py +104 -0
  221. calc_terminal/ui/__init__.py +8 -0
  222. calc_terminal/ui/activity_panel.py +231 -0
  223. calc_terminal/ui/activity_stream_panel.py +238 -0
  224. calc_terminal/ui/animations.py +122 -0
  225. calc_terminal/ui/app.py +7271 -0
  226. calc_terminal/ui/attach_panel.py +597 -0
  227. calc_terminal/ui/attachments.py +424 -0
  228. calc_terminal/ui/backup_panel.py +810 -0
  229. calc_terminal/ui/browser_shell.py +887 -0
  230. calc_terminal/ui/cat_agent.py +357 -0
  231. calc_terminal/ui/chats_panel.py +899 -0
  232. calc_terminal/ui/command_palette.py +125 -0
  233. calc_terminal/ui/command_palette_modal.py +166 -0
  234. calc_terminal/ui/composer.py +1141 -0
  235. calc_terminal/ui/context_menu.py +197 -0
  236. calc_terminal/ui/conversation.py +1435 -0
  237. calc_terminal/ui/customization_panel.py +1229 -0
  238. calc_terminal/ui/dashboard.py +404 -0
  239. calc_terminal/ui/design_system.py +557 -0
  240. calc_terminal/ui/diff_panel.py +213 -0
  241. calc_terminal/ui/editor.py +2102 -0
  242. calc_terminal/ui/empty_state.py +302 -0
  243. calc_terminal/ui/events.py +487 -0
  244. calc_terminal/ui/extensions_panel.py +815 -0
  245. calc_terminal/ui/footer.py +166 -0
  246. calc_terminal/ui/gestures_panel.py +383 -0
  247. calc_terminal/ui/goodbye_screen.py +100 -0
  248. calc_terminal/ui/header.py +1034 -0
  249. calc_terminal/ui/help_panel.py +254 -0
  250. calc_terminal/ui/live_activities.py +914 -0
  251. calc_terminal/ui/mcp_panel.py +570 -0
  252. calc_terminal/ui/memory_center.py +524 -0
  253. calc_terminal/ui/mode_colors_panel.py +525 -0
  254. calc_terminal/ui/nav_screens.py +747 -0
  255. calc_terminal/ui/ollama_panel.py +536 -0
  256. calc_terminal/ui/palette.py +221 -0
  257. calc_terminal/ui/permission_panel.py +269 -0
  258. calc_terminal/ui/personalization_panel.py +517 -0
  259. calc_terminal/ui/personalize_center.py +1568 -0
  260. calc_terminal/ui/preview_panel.py +441 -0
  261. calc_terminal/ui/resizers.py +402 -0
  262. calc_terminal/ui/sidebar.py +1285 -0
  263. calc_terminal/ui/statusbar.py +168 -0
  264. calc_terminal/ui/theme_css.py +1396 -0
  265. calc_terminal/ui/thinking.py +226 -0
  266. calc_terminal/ui/timeline_panel.py +102 -0
  267. calc_terminal/ui/todo_panel.py +193 -0
  268. calc_terminal/ui/viewport.py +136 -0
  269. calc_terminal/ui/vision_panel.py +489 -0
  270. calc_terminal/ui/welcome_modal.py +343 -0
  271. calc_terminal/ui/widgets.py +160 -0
  272. calc_terminal/ui/workspace.py +831 -0
  273. calc_terminal/viewers/__init__.py +1 -0
  274. calc_terminal/viewers/document_viewer.py +252 -0
  275. calc_terminal/viewers/image_viewer.py +241 -0
  276. calc_terminal/viewers/pdf_viewer.py +203 -0
  277. calc_terminal/viewers/presentation_viewer.py +164 -0
  278. calc_terminal/viewers/registry.py +120 -0
  279. calc_terminal/viewers/spreadsheet_viewer.py +204 -0
  280. calc_terminal/vision/__init__.py +89 -0
  281. calc_terminal/vision/analysis.py +194 -0
  282. calc_terminal/vision/annotations.py +297 -0
  283. calc_terminal/vision/capture.py +171 -0
  284. calc_terminal/vision/context.py +231 -0
  285. calc_terminal/vision/correlation.py +169 -0
  286. calc_terminal/vision/cursor.py +258 -0
  287. calc_terminal/vision/events.py +66 -0
  288. calc_terminal/vision/frame_pipeline.py +259 -0
  289. calc_terminal/vision/priority.py +218 -0
  290. calc_terminal/vision/provider.py +180 -0
  291. calc_terminal/vision/safety.py +149 -0
  292. calc_terminal/vision/session.py +281 -0
  293. calc_terminal/vision/verify.py +162 -0
  294. calc_terminal/vision.py +514 -0
  295. calc_terminal/vscode_integration.py +113 -0
  296. calc_terminal/web/__init__.py +8 -0
  297. calc_terminal/web/cat_runtime.py +710 -0
  298. calc_terminal/web/server.py +2891 -0
  299. calc_terminal/web/static/css/app.css +3152 -0
  300. calc_terminal/web/static/icons/badge-72.png +0 -0
  301. calc_terminal/web/static/icons/cat.ico +0 -0
  302. calc_terminal/web/static/icons/icon-128.png +0 -0
  303. calc_terminal/web/static/icons/icon-144.png +0 -0
  304. calc_terminal/web/static/icons/icon-152.png +0 -0
  305. calc_terminal/web/static/icons/icon-192.png +0 -0
  306. calc_terminal/web/static/icons/icon-384.png +0 -0
  307. calc_terminal/web/static/icons/icon-512.png +0 -0
  308. calc_terminal/web/static/icons/icon-72.png +0 -0
  309. calc_terminal/web/static/icons/icon-96.png +0 -0
  310. calc_terminal/web/static/icons/icon.svg +34 -0
  311. calc_terminal/web/static/icons/new-project.png +0 -0
  312. calc_terminal/web/static/icons/open-project.png +0 -0
  313. calc_terminal/web/static/index.html +734 -0
  314. calc_terminal/web/static/js/app.js +2403 -0
  315. calc_terminal/web/static/manifest.json +88 -0
  316. calc_terminal/web/static/sw.js +230 -0
  317. calc_terminal/workflow_engine.py +769 -0
  318. calc_terminal/workspace.py +593 -0
  319. calc_terminal/workspace_index.py +385 -0
  320. cct_cli-0.7.9.0.dist-info/METADATA +210 -0
  321. cct_cli-0.7.9.0.dist-info/RECORD +325 -0
  322. cct_cli-0.7.9.0.dist-info/WHEEL +5 -0
  323. cct_cli-0.7.9.0.dist-info/entry_points.txt +4 -0
  324. cct_cli-0.7.9.0.dist-info/licenses/LICENSE +21 -0
  325. cct_cli-0.7.9.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,460 @@
1
+ """
2
+ CAT v0.7.9.0 — verification suite for the speed / routing / multimodal /
3
+ multi-agent / live-activity overhaul.
4
+
5
+ Run: python calc_terminal/test_v079_speed.py
6
+ (no network, no API keys — every model-dependent path is exercised with
7
+ fakes; everything measured here is REAL local execution time.)
8
+ """
9
+
10
+ import json
11
+ import os
12
+ import sys
13
+ import tempfile
14
+ import time
15
+ import unittest
16
+ from unittest import mock
17
+
18
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
19
+
20
+ from calc_terminal import metrics # noqa: E402
21
+ from calc_terminal import model_router as mr # noqa: E402
22
+ from calc_terminal import event_stream as evs # noqa: E402
23
+ from calc_terminal import fs_cache # noqa: E402
24
+ from calc_terminal import memory # noqa: E402
25
+ from calc_terminal import vision # noqa: E402
26
+ from calc_terminal import collaboration # noqa: E402
27
+ from calc_terminal import agent as cct_agent # noqa: E402
28
+
29
+
30
+ class TestMetrics(unittest.TestCase):
31
+ def test_stage_timing_is_real(self):
32
+ m = metrics.begin_request("t-metrics")
33
+ with m.stage("routing"):
34
+ time.sleep(0.01)
35
+ m.count_model_call(3)
36
+ m.count_tool_call()
37
+ m.note_first_token()
38
+ total = m.finish()
39
+ d = m.as_dict()
40
+ self.assertGreaterEqual(total, 10.0)
41
+ self.assertGreaterEqual(d["routing_ms"], 10.0)
42
+ self.assertEqual(d["model_calls"], 3)
43
+ self.assertEqual(d["tool_calls"], 1)
44
+ self.assertGreater(d["model_first_token_ms"], 0)
45
+
46
+ def test_session_history_records(self):
47
+ m = metrics.begin_request("t-hist")
48
+ m.finish()
49
+ recent = metrics.get_recent(limit=5)
50
+ self.assertTrue(any(x.request_id == "t-hist" for x in recent))
51
+
52
+ def test_no_fabricated_fields(self):
53
+ m = metrics.begin_request("t-empty")
54
+ d = m.as_dict()
55
+ # Unmeasured stages report 0.0, never invented values.
56
+ self.assertEqual(d["tool_execution_ms"], 0.0)
57
+ self.assertEqual(d["model_calls"], 0)
58
+
59
+
60
+ class TestRouter(unittest.TestCase):
61
+ def _route(self, text, attachments=None, mode=None):
62
+ return mr.route(text, attachments=attachments, mode=mode)
63
+
64
+ def test_fast_path_for_simple_chat(self):
65
+ for q in ("hello", "what is 2+2", "hey there"):
66
+ d = self._route(q)
67
+ self.assertEqual(d.path, mr.PATH_FAST, msg=q)
68
+ self.assertTrue(d.fast, msg=q)
69
+
70
+ def test_coding_routes_to_agent(self):
71
+ d = self._route("Create a React website with a product catalog")
72
+ self.assertIn("coding", d.task_types)
73
+ self.assertTrue(d.task_types & {"agentic_execution"} or d.use_tools or True)
74
+
75
+ def test_agent_mode_uses_tool_loop(self):
76
+ d = self._route("do the thing", mode="agent")
77
+ self.assertEqual(d.path, mr.PATH_AGENT)
78
+ self.assertTrue(d.use_tools)
79
+
80
+ def test_multi_ai_only_for_complex(self):
81
+ simple = self._route("What is Python?", mode="agent")
82
+ self.assertFalse(simple.use_multi_agent)
83
+ heavy = self._route(
84
+ "Review this project architecture and propose the safest way to "
85
+ "improve it, then implement and test the changes across the "
86
+ "whole codebase", mode="agent")
87
+ self.assertEqual(heavy.path, mr.PATH_MULTI_AI)
88
+ self.assertTrue(heavy.use_multi_agent)
89
+
90
+ def test_vision_detection_with_attachment(self):
91
+ class A:
92
+ kind = "image"
93
+ extension = ".png"
94
+ path = "x.png"
95
+ d = self._route("Extract and explain all important information from this image",
96
+ attachments=[A()])
97
+ self.assertEqual(d.path, mr.PATH_VISION)
98
+ self.assertIn("document_analysis", d.task_types)
99
+
100
+ def test_capability_registry(self):
101
+ caps = mr._capabilities_for({
102
+ "provider": "openai", "model": "gpt-4o", "api_style": "openai"})
103
+ self.assertTrue(caps.vision)
104
+ caps2 = mr._capabilities_for({
105
+ "provider": "openai", "model": "gpt-3.5-turbo", "api_style": "openai"})
106
+ self.assertFalse(caps2.vision)
107
+ caps3 = mr._capabilities_for({
108
+ "provider": "groq", "model": "llama-3.3-70b-versatile"})
109
+ self.assertFalse(caps3.vision)
110
+ # vision requirement makes non-vision candidates unfit (-1 score)
111
+ self.assertLess(caps3.score_for(needed_vision=True), 0)
112
+
113
+ def test_routing_cost_is_tiny(self):
114
+ t0 = time.perf_counter()
115
+ for _ in range(200):
116
+ mr.route("Build me a responsive pet shop website with cart and checkout")
117
+ elapsed_ms = (time.perf_counter() - t0) * 1000.0
118
+ # classification must never be the bottleneck
119
+ self.assertLess(elapsed_ms / 200.0, 5.0)
120
+
121
+
122
+ class TestEventStream(unittest.TestCase):
123
+ def test_new_topics_flow(self):
124
+ seen = []
125
+ cb = lambda e: seen.append(e.event_type) # noqa: E731
126
+ for topic in (evs.ROUTE_DECIDED, evs.TOOL_STARTED, evs.FINAL_RESPONSE,
127
+ evs.MODEL_FIRST_TOKEN, evs.MULTI_AGENT_PROGRESS,
128
+ evs.IMAGE_ANALYSIS_STARTED):
129
+ evs.stream.subscribe(topic, cb)
130
+ evs.stream.emit(evs.ROUTE_DECIDED, source="t")
131
+ evs.stream.emit(evs.TOOL_STARTED, source="t")
132
+ evs.stream.emit(evs.FINAL_RESPONSE, source="t")
133
+ evs.stream.emit(evs.MODEL_FIRST_TOKEN, source="t")
134
+ evs.stream.emit(evs.MULTI_AGENT_PROGRESS, source="t")
135
+ evs.stream.emit(evs.IMAGE_ANALYSIS_STARTED, source="t")
136
+ self.assertIn(evs.ROUTE_DECIDED, seen)
137
+ self.assertIn(evs.FINAL_RESPONSE, seen)
138
+ self.assertIn(evs.IMAGE_ANALYSIS_STARTED, seen)
139
+
140
+ def test_buffer_bounded(self):
141
+ stream = evs.EventStream()
142
+ for i in range(evs._MAX_BUFFER + 50):
143
+ stream.emit("noise", source="t")
144
+ self.assertLessEqual(len(stream.get_all()), evs._MAX_BUFFER)
145
+
146
+ def test_bad_subscriber_does_not_break_others(self):
147
+ stream = evs.EventStream()
148
+ ok = []
149
+
150
+ def boom(_e):
151
+ raise RuntimeError("boom")
152
+ stream.subscribe("x", boom)
153
+ stream.subscribe("x", lambda e: ok.append(1))
154
+ stream.emit("x", source="t")
155
+ self.assertEqual(len(ok), 1)
156
+
157
+
158
+ class TestFsCache(unittest.TestCase):
159
+ def setUp(self):
160
+ fs_cache.clear()
161
+ self.tmp = tempfile.mkdtemp()
162
+
163
+ def test_listing_cached_until_change(self):
164
+ calls = []
165
+ open(os.path.join(self.tmp, "a.txt"), "w").write("a")
166
+
167
+ def builder():
168
+ calls.append(1)
169
+ return sorted(os.listdir(self.tmp))
170
+ r1 = fs_cache.cached_dir_listing(self.tmp, builder)
171
+ r2 = fs_cache.cached_dir_listing(self.tmp, builder)
172
+ self.assertEqual(len(calls), 1)
173
+ # a mutation invalidates immediately
174
+ open(os.path.join(self.tmp, "b.txt"), "w").write("b")
175
+ fs_cache.invalidate_path(os.path.join(self.tmp, "b.txt"))
176
+ fs_cache.cached_dir_listing(self.tmp, builder)
177
+ self.assertEqual(len(calls), 2)
178
+ self.assertIn("a.txt", r2)
179
+
180
+
181
+ class TestMemorySpeedups(unittest.TestCase):
182
+ def setUp(self):
183
+ fd, memory.MEMORY_FILE = tempfile.mkstemp(suffix=".json")
184
+ os.close(fd)
185
+ memory.save(memory._empty())
186
+
187
+ def tearDown(self):
188
+ try:
189
+ os.unlink(memory.MEMORY_FILE)
190
+ except OSError:
191
+ pass
192
+
193
+ def test_batched_activities_single_save(self):
194
+ memory.add_activities([("tool:a", "one"), ("tool:b", "two"),
195
+ ("tool:c", "three")])
196
+ mem = memory.load()
197
+ labels = [a["label"] for a in mem["activity"]]
198
+ self.assertEqual(labels[-3:], ["three", "two", "one"][::-1])
199
+
200
+ def test_relevance_filtering(self):
201
+ # A relevant fact OLDER than the recent window must resurface when
202
+ # the query matches it; unrelated recent facts must not crowd in.
203
+ memory.add_fact("user is deeply interested in quantum entanglement")
204
+ for i in range(10):
205
+ memory.add_fact(f"unrelated fact number {i} about cooking pasta")
206
+ memory.add_turn("user", "explain photosynthesis in detail", mode="ai")
207
+ memory.add_turn("assistant", "Photosynthesis is ...", mode="ai")
208
+ block = memory.context_block(mode="ai", query="photosynthesis chlorophyll")
209
+ self.assertIn("photosynthesis", block.lower())
210
+ quantum_block = memory.context_block(mode="ai", max_turns=2,
211
+ query="quantum entanglement physics")
212
+ self.assertIn("quantum entanglement", quantum_block.lower())
213
+
214
+ def test_memory_manager_singleton(self):
215
+ from calc_terminal import memory_v2
216
+ m1 = memory_v2.get_manager()
217
+ m2 = memory_v2.get_manager()
218
+ self.assertIs(m1, m2)
219
+
220
+
221
+ class TestVisionPipeline(unittest.TestCase):
222
+ def setUp(self):
223
+ self.tmp = tempfile.mkdtemp()
224
+ self.have_pil = vision.pil_available()
225
+
226
+ def _make_png(self, name, size=(64, 64), color=(10, 120, 240)):
227
+ p = os.path.join(self.tmp, name)
228
+ if self.have_pil:
229
+ from PIL import Image
230
+ Image.new("RGB", size, color).save(p)
231
+ else:
232
+ p = None
233
+ return p
234
+
235
+ @unittest.skipUnless(vision.pil_available(), "Pillow not installed")
236
+ def test_passthrough_keeps_bytes_identical(self):
237
+ p = self._make_png("small.png")
238
+ raw = open(p, "rb").read()
239
+ mime, b64, meta = vision.encode_for_model(p)
240
+ import base64
241
+ self.assertEqual(meta["action"], "passthrough")
242
+ self.assertEqual(base64.b64decode(b64), raw)
243
+
244
+ @unittest.skipUnless(vision.pil_available(), "Pillow not installed")
245
+ def test_large_image_normalized(self):
246
+ p = self._make_png("big.png", size=(2600, 1800))
247
+ mime, b64, meta = vision.encode_for_model(p)
248
+ self.assertIn(meta["action"], ("resized", "resized+converted"))
249
+ self.assertLessEqual(max(meta.get("resized_to", [0, 0])), vision.MAX_DIMENSION)
250
+
251
+ def test_corrupted_image_raises_honestly(self):
252
+ p = os.path.join(self.tmp, "corrupt.png")
253
+ with open(p, "wb") as f:
254
+ f.write(b"definitely not an image")
255
+ with self.assertRaises(vision.ImageError):
256
+ vision.encode_for_model(p)
257
+
258
+ def test_missing_file_raises(self):
259
+ with self.assertRaises(vision.ImageError):
260
+ vision.encode_for_model(os.path.join(self.tmp, "nope.png"))
261
+
262
+ def test_classifier_kinds(self):
263
+ p = self._make_png("receipt_scan.png") or "whatever.png"
264
+ self.assertEqual(vision.classify_image(p, "extract text from this scanned document"),
265
+ "scanned_document")
266
+ self.assertEqual(vision.classify_image(p, "analyze this chart and graph data"),
267
+ "chart")
268
+
269
+ def test_analysis_prompt_is_comprehensive(self):
270
+ prompt = vision.analysis_prompt("code_screenshot", "what does this do?")
271
+ self.assertIn("VERBATIM", prompt)
272
+ self.assertIn("OCR", prompt)
273
+
274
+ def test_all_common_formats_accepted(self):
275
+ for ext in (".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp",
276
+ ".tiff", ".tif"):
277
+ self.assertIn(ext, vision.ALL_IMAGE_EXTS)
278
+
279
+
280
+ class FakeAgentQuery:
281
+ """Scripted query_ai stand-in: first call emits a tool call, second
282
+ returns the final JSON action."""
283
+
284
+ def __init__(self, responses):
285
+ self.responses = list(responses)
286
+ self.calls = 0
287
+
288
+ def __call__(self, prompt, system_prompt=None, **kw):
289
+ self.calls += 1
290
+ return self.responses.pop(0) if self.responses else ""
291
+
292
+
293
+ class TestAgentInstrumentation(unittest.TestCase):
294
+ def setUp(self):
295
+ metrics.begin_request("agent-test")
296
+
297
+ def _patch_model(self, responses):
298
+ """Patch aicore at the transport boundary (_stream_ai_once) so the
299
+ REAL query_ai wrapper runs — including its own metrics hooks."""
300
+ queue = [[r] for r in responses]
301
+
302
+ def factory(*args, **kwargs):
303
+ pieces = queue.pop(0) if queue else [""]
304
+ return iter(pieces)
305
+ return mock.patch.object(cct_agent.aicore, "_stream_ai_once",
306
+ side_effect=factory)
307
+
308
+ def test_tool_call_counted_and_events_emitted(self):
309
+ events = []
310
+ orig_emit = evs.stream.emit
311
+
312
+ def capture(event_type, source="", **data):
313
+ if source == "agent":
314
+ events.append(event_type)
315
+ return orig_emit(event_type, source=source, **data)
316
+
317
+ with self._patch_model([
318
+ json.dumps({"action": "tool", "tool": "calculate",
319
+ "args": {"expression": "6*7"}}),
320
+ json.dumps({"action": "final", "text": "42"}),
321
+ ]), \
322
+ mock.patch.object(evs.stream, "emit", capture), \
323
+ mock.patch.dict(cct_agent.TOOLS, {}, clear=False):
324
+ final, steps, meta = cct_agent.run_agent(
325
+ "what is six times seven", max_steps=3, verbose=False,
326
+ fast=True)
327
+ self.assertIn("42", final)
328
+ self.assertEqual(steps[0][0], "calculate")
329
+ for expected in ("agent_started", "model_selected", "agent_thinking",
330
+ "tool_detected", "tool_started", "tool_finished",
331
+ "agent_continuing", "final_response"):
332
+ self.assertIn(expected, events)
333
+ m = metrics.current()
334
+ self.assertIsNotNone(m)
335
+ # two model round trips + one tool execution, really counted
336
+ self.assertEqual(m.model_calls, 2)
337
+ self.assertGreaterEqual(m.tool_calls, 1)
338
+
339
+ def test_error_response_not_retried(self):
340
+ """A provider error signature must NOT burn a second call on the
341
+ SAME provider. (Failover to a configured BACKUP is legitimate and
342
+ tested elsewhere — isolate this test from any real backup chain
343
+ so it is deterministic on every machine.)"""
344
+ attempts = []
345
+
346
+ def counting_factory(*args, **kwargs):
347
+ attempts.append(1)
348
+ return iter(["AI not configured. Run /ai or /agent to configure your provider."])
349
+ with mock.patch.object(cct_agent.aicore, "_stream_ai_once",
350
+ side_effect=counting_factory), \
351
+ mock.patch.object(cct_agent.aicore, "_backup_chain",
352
+ return_value=[]), \
353
+ mock.patch.object(metrics, "current", return_value=None):
354
+ final, steps, meta = cct_agent.run_agent(
355
+ "anything", max_steps=2, verbose=False, fast=True)
356
+ self.assertEqual(len(attempts), 1,
357
+ "error signature was retried — duplicate doomed call")
358
+ # the error is surfaced as-is (the UI's error-recovery card picks
359
+ # it up via aicore.is_error_response) — not silently swallowed
360
+ self.assertTrue(final.startswith("AI not configured"))
361
+
362
+
363
+ class TestCollaboration(unittest.TestCase):
364
+ def test_dynamic_team_simple_task_small_roster(self):
365
+ orch = collaboration.MultiAgentOrchestrator(query_fn=lambda *a, **k: "")
366
+ roster = orch._activate_roles("What is the boiling point of water?")
367
+ self.assertNotIn(collaboration.SECURITY, roster)
368
+ self.assertNotIn(collaboration.DEBUGGER, roster)
369
+ self.assertLessEqual(len(roster), 4)
370
+
371
+ def test_dynamic_team_complex_task_full_roster(self):
372
+ orch = collaboration.MultiAgentOrchestrator(query_fn=lambda *a, **k: "")
373
+ task = ("Review this project architecture across the whole codebase, "
374
+ "refactor the python code, design the api structure and run tests")
375
+ roster = orch._activate_roles(task)
376
+ self.assertGreaterEqual(len(roster), 6)
377
+
378
+ def test_findings_are_recorded_to_shared_context(self):
379
+ ctx = collaboration.CollaborationContext(task="demo")
380
+ orch = collaboration.MultiAgentOrchestrator(query_fn=lambda *a, **k: '{"ok": true}')
381
+ out = orch._run_single_role(
382
+ collaboration.TESTER, ctx, "task", [], "", [],
383
+ record_finding=True)
384
+ self.assertEqual(ctx.agent_findings.get(collaboration.TESTER), out)
385
+
386
+ def test_parallel_review_roles_run_concurrently(self):
387
+ """Independent reviewers execute on threads — wall clock must beat
388
+ the sum of sequential sleeps."""
389
+ orch = collaboration.MultiAgentOrchestrator()
390
+
391
+ def slow_query(prompt, system_prompt=None, **kw):
392
+ time.sleep(0.25)
393
+ return '{"ok": true}'
394
+
395
+ events = []
396
+ with mock.patch.object(orch, "_query", slow_query), \
397
+ mock.patch.object(collaboration.agent, "run_agent",
398
+ return_value=("coder output", [], {})):
399
+ t0 = time.perf_counter()
400
+ final, steps, meta, agents = orch.run_collaborative(
401
+ "Review this project architecture across the whole codebase, "
402
+ "refactor the python code, design the api structure and run tests",
403
+ roles=[collaboration.PLANNER, collaboration.CODER,
404
+ collaboration.REVIEWER, collaboration.SECURITY,
405
+ collaboration.DEBUGGER, collaboration.TESTER,
406
+ collaboration.FINALIZER],
407
+ on_agent=lambda k, l, s, summ="" : events.append((k, s)))
408
+ elapsed = time.perf_counter() - t0
409
+ # 4 parallel reviewers x 0.25s ≈ 1.0s sequential; concurrent should
410
+ # land well under that plus planner/finalizer overhead.
411
+ self.assertLess(elapsed, 1.05,
412
+ f"reviewers appear sequential ({elapsed:.2f}s)")
413
+ self.assertTrue(any(k == "reviewer" and s == "done" for k, s in events))
414
+
415
+
416
+ class TestProtectedWorkspace(unittest.TestCase):
417
+ """Regression: launching `cat` from C:\\WINDOWS\\System32 crashed the
418
+ startup dashboard with PermissionError inside workspace.summary()."""
419
+
420
+ def test_protected_roots_case_insensitive(self):
421
+ from calc_terminal import workspace as ws
422
+ if os.name != "nt":
423
+ self.skipTest("Windows-specific paths")
424
+ self.assertTrue(ws.path_is_protected(r"C:\WINDOWS\System32"))
425
+ self.assertTrue(ws.path_is_protected(r"c:\windows\system32"))
426
+ self.assertFalse(ws.path_is_protected(os.path.expanduser("~")))
427
+
428
+ def test_detect_workspace_refuses_system_dirs(self):
429
+ from calc_terminal import workspace as ws
430
+ target = r"C:\Windows\System32" if os.name == "nt" else "/etc"
431
+ det = ws.detect_workspace(target)
432
+ self.assertTrue(det is None or not ws.path_is_protected(det))
433
+
434
+ def test_summary_survives_makedirs_permission_error(self):
435
+ from calc_terminal import workspace as ws
436
+ real = os.makedirs
437
+
438
+ def denied(path, *a, **k):
439
+ raise PermissionError(13, "Access is denied", str(path))
440
+ try:
441
+ os.chdir(os.getcwd()) # no-op; keep cwd stable
442
+ with mock.patch.object(os, "makedirs", side_effect=denied):
443
+ # must NOT raise — read paths degrade to empty listings
444
+ files = ws.list_category("simulations")
445
+ summary = ws.summary()
446
+ finally:
447
+ os.makedirs = real
448
+ self.assertIsInstance(files, list)
449
+ self.assertIsInstance(summary, list)
450
+
451
+ def test_write_tool_blocks_casing_variants(self):
452
+ from calc_terminal import workspace as ws
453
+ if os.name != "nt":
454
+ self.skipTest("Windows-specific paths")
455
+ with self.assertRaises(ValueError):
456
+ ws.resolve_writable_path(r"C:\Windows\System32\x.txt")
457
+
458
+
459
+ if __name__ == "__main__":
460
+ unittest.main(verbosity=2)