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,344 @@
1
+ """
2
+ CAT AI Mode Reliability & Small-Model Compatibility Test Suite.
3
+
4
+ Verifies all 42 sections and Acceptance Criteria of the
5
+ CAT AI Mode Reliability Contract:
6
+ - Test A: Normal chat greeting (no context leak)
7
+ - Test B: Context contamination regression (Research SMTP -> Chat hello)
8
+ - Test C: Mode switching isolation (Notebook -> Research -> Plan -> Debugger -> Chat)
9
+ - Test D: Ollama streaming parser & deduplication
10
+ - Test E: Request isolation, cancellation, and stream reset
11
+ - Test F: Small/base model capability detection (deepseek-coder:1.3b-base-q8_0)
12
+ - Test G: Context token budgeting & priority trimming
13
+ - Test H: Repetition detection (GenerationGuard)
14
+ - Test I: Provider failure handling
15
+ - Test J: Model-vs-CAT diagnostic test (HELLO_TEST)
16
+ """
17
+
18
+ import unittest
19
+ import uuid
20
+ import time
21
+ from unittest.mock import MagicMock, patch
22
+
23
+ from calc_terminal.ai_context import (
24
+ AIContextManager,
25
+ AIMessage,
26
+ AIRequest,
27
+ AIMode,
28
+ NotebookCell,
29
+ ResearchContext,
30
+ DebuggerContext,
31
+ PlanContext,
32
+ ContextBudget,
33
+ get_context_manager,
34
+ simple_token_estimate,
35
+ )
36
+ from calc_terminal.models.profiles import (
37
+ ModelCapabilities,
38
+ ModelProfile,
39
+ detect_capabilities,
40
+ get_model_profile,
41
+ )
42
+ from calc_terminal.providers.ollama_adapter import (
43
+ OllamaProvider,
44
+ GenerationGuard,
45
+ OutputSanitizer,
46
+ RequestState,
47
+ )
48
+ from calc_terminal.session import ChatSession, Turn
49
+
50
+
51
+ class TestAIModeReliability(unittest.TestCase):
52
+
53
+ def setUp(self):
54
+ self.ctx_mgr = AIContextManager()
55
+
56
+ # -------------------------------------------------------------------------
57
+ # Test A: Normal Chat Greeting
58
+ # -------------------------------------------------------------------------
59
+ def test_a_normal_chat_greeting_has_no_unrelated_context(self):
60
+ """User sends 'hello' in Chat mode: context contains only concise instructions and user prompt."""
61
+ profile = get_model_profile("ollama", "deepseek-coder:1.3b-base-q8_0")
62
+ req = AIRequest(
63
+ session_id="test-sess-1",
64
+ request_id=str(uuid.uuid4()),
65
+ mode="chat",
66
+ user_message="hello",
67
+ history=[],
68
+ model=profile.model,
69
+ provider=profile.provider,
70
+ )
71
+ ctx = self.ctx_mgr.build_context(req, profile=profile)
72
+
73
+ # Context must contain minimal system prompt and user message 'hello'
74
+ self.assertTrue(ctx.is_minimal, "Small base model must use minimal prompt mode")
75
+ self.assertEqual(len(ctx.messages), 2, "Greeting with no history must have exactly system + user")
76
+ self.assertEqual(ctx.messages[0]["role"], "system")
77
+ self.assertEqual(ctx.messages[1]["role"], "user")
78
+ self.assertEqual(ctx.messages[1]["content"], "hello")
79
+
80
+ # Must NOT contain research, notebook, or tool contamination
81
+ full_content = " ".join(m["content"] for m in ctx.messages)
82
+ self.assertNotIn("Conversation so far:", full_content)
83
+ self.assertNotIn("SMTP", full_content)
84
+ self.assertNotIn("IMAP", full_content)
85
+ self.assertNotIn("Notebook", full_content)
86
+
87
+ # -------------------------------------------------------------------------
88
+ # Test B: Context Contamination Regression Test (SMTP Research -> Chat Hello)
89
+ # -------------------------------------------------------------------------
90
+ def test_b_previous_research_does_not_leak_into_chat_greeting(self):
91
+ """Large research response (SMTP/IMAP) must NOT contaminate subsequent Chat 'hello' prompt."""
92
+ session = ChatSession()
93
+
94
+ # 1. Simulate prior Research mode turn with massive SMTP/IMAP documentation
95
+ large_research_text = (
96
+ "## 4.17 Sending and receiving email with Python\n"
97
+ "SMTP (Simple Mail Transfer Protocol) and IMAP (Internet Message Access Protocol) "
98
+ "are standard protocols for email transfer in Python. smtplib.SMTP('localhost') "
99
+ "connects to the server, and email.mime constructs RFC 2822 messages..."
100
+ * 20
101
+ )
102
+ t_user = Turn("turn-1", "user", "research SMTP in Python", mode="research")
103
+ t_asst = Turn("turn-2", "assistant", large_research_text, mode="research")
104
+ session.turns.extend([t_user, t_asst])
105
+
106
+ # 2. User switches to Chat mode and sends "hello"
107
+ chat_history = session.as_prompt_history(mode="chat")
108
+
109
+ # Mode isolation: Research turns must NOT be in chat history
110
+ history_roles = [r for r, _ in chat_history]
111
+ history_texts = [t for _, t in chat_history]
112
+ combined_history = " ".join(history_texts)
113
+
114
+ self.assertNotIn("SMTP", combined_history, "Chat history must not contain SMTP research dump")
115
+ self.assertNotIn("IMAP", combined_history, "Chat history must not contain IMAP research dump")
116
+
117
+ # 3. Build context for 'hello' with deepseek-coder:1.3b-base-q8_0
118
+ profile = get_model_profile("ollama", "deepseek-coder:1.3b-base-q8_0")
119
+ messages_hist = [
120
+ AIMessage(id="m1", role=r, content=t, mode="chat") for r, t in chat_history
121
+ ]
122
+ req = AIRequest(
123
+ session_id="test-sess-b",
124
+ request_id=str(uuid.uuid4()),
125
+ mode="chat",
126
+ user_message="hello",
127
+ history=messages_hist,
128
+ model=profile.model,
129
+ provider=profile.provider,
130
+ )
131
+ ctx = self.ctx_mgr.build_context(req, profile=profile)
132
+
133
+ # Assert no research text in built prompt
134
+ full_prompt = " ".join(m["content"] for m in ctx.messages)
135
+ self.assertNotIn("SMTP", full_prompt)
136
+ self.assertNotIn("IMAP", full_prompt)
137
+ self.assertNotIn("4.17 Sending and receiving email", full_prompt)
138
+ self.assertNotIn("Conversation so far:", full_prompt)
139
+
140
+ # -------------------------------------------------------------------------
141
+ # Test C: Mode Switching Isolation
142
+ # -------------------------------------------------------------------------
143
+ def test_c_mode_switching_isolates_contexts(self):
144
+ """Mode switching across Notebook -> Research -> Plan -> Debugger -> Chat preserves strict isolation."""
145
+ session = ChatSession()
146
+
147
+ # Add turns across different modes
148
+ session.turns.append(Turn("t1", "user", "calculate derivative", mode="notebook"))
149
+ session.turns.append(Turn("t2", "assistant", "derivative is 2x", mode="notebook"))
150
+ session.turns.append(Turn("t3", "user", "research quantum physics", mode="research"))
151
+ session.turns.append(Turn("t4", "assistant", "quantum theory details...", mode="research"))
152
+ session.turns.append(Turn("t5", "user", "create roadmap", mode="plan"))
153
+ session.turns.append(Turn("t6", "assistant", "Phase 1: architecture...", mode="plan"))
154
+ session.turns.append(Turn("t7", "user", "trace IndexError", mode="debugger"))
155
+ session.turns.append(Turn("t8", "assistant", "line 42 index out of range", mode="debugger"))
156
+
157
+ # In Chat mode: only conversational pleasantries, no research or debug traces
158
+ chat_hist = session.as_prompt_history(mode="chat")
159
+ chat_text = " ".join(t for _, t in chat_hist)
160
+ self.assertNotIn("quantum theory", chat_text)
161
+ self.assertNotIn("Phase 1: architecture", chat_text)
162
+ self.assertNotIn("line 42 index out of range", chat_text)
163
+
164
+ # In Debugger mode: only debugger turns
165
+ dbg_hist = session.as_prompt_history(mode="debugger")
166
+ dbg_text = " ".join(t for _, t in dbg_hist)
167
+ self.assertIn("line 42 index out of range", dbg_text)
168
+ self.assertNotIn("quantum theory", dbg_text)
169
+ self.assertNotIn("Phase 1: architecture", dbg_text)
170
+
171
+ # In Plan mode: only plan turns
172
+ plan_hist = session.as_prompt_history(mode="plan")
173
+ plan_text = " ".join(t for _, t in plan_hist)
174
+ self.assertIn("Phase 1: architecture", plan_text)
175
+ self.assertNotIn("line 42 index out of range", plan_text)
176
+
177
+ # -------------------------------------------------------------------------
178
+ # Test D: Ollama Streaming Parser & Output Sanitizer
179
+ # -------------------------------------------------------------------------
180
+ def test_d_ollama_streaming_and_sanitizer(self):
181
+ """Ollama stream yields only clean content, and OutputSanitizer strips stray prompt artifacts."""
182
+ sanitizer = OutputSanitizer()
183
+
184
+ # Strips accidental prefix
185
+ self.assertEqual(sanitizer.sanitize_first_chunk("Assistant: Hello world"), "Hello world")
186
+ self.assertEqual(sanitizer.sanitize_first_chunk("CAT AI: Here is the code"), "Here is the code")
187
+ self.assertEqual(sanitizer.sanitize_first_chunk("Normal text"), "Normal text")
188
+
189
+ # Preserves markdown code blocks without breaking syntax
190
+ code_block = "```python\ndef foo():\n return 42\n```"
191
+ self.assertEqual(sanitizer.sanitize_final(code_block), code_block)
192
+
193
+ # Mock Ollama streaming response chunks
194
+ mock_chunks = [
195
+ b'{"message": {"content": "Assistant: Hel"}, "done": false}\n',
196
+ b'{"message": {"content": "lo "}, "done": false}\n',
197
+ b'{"message": {"content": "there!"}, "done": true}\n',
198
+ ]
199
+
200
+ mock_resp = MagicMock()
201
+ mock_resp.status_code = 200
202
+ mock_resp.iter_lines.return_value = [c.decode("utf-8") for c in mock_chunks]
203
+
204
+ with patch("requests.post", return_value=mock_resp):
205
+ provider = OllamaProvider("http://localhost:11434")
206
+ streamed = list(provider.stream_chat("llama3.2", [{"role": "user", "content": "hi"}]))
207
+
208
+ result_text = "".join(streamed)
209
+ self.assertEqual(result_text, "Hello there!")
210
+ self.assertNotIn("Assistant:", result_text)
211
+ self.assertNotIn('{"message"', result_text)
212
+
213
+ # -------------------------------------------------------------------------
214
+ # Test E: Request Isolation, Stream Reset, & Cancellation
215
+ # -------------------------------------------------------------------------
216
+ def test_e_request_isolation_and_cancellation(self):
217
+ """New request allocates fresh request ID, resets stream, and cancellation terminates cleanly."""
218
+ provider = OllamaProvider("http://localhost:11434")
219
+ self.assertEqual(provider.state, RequestState.IDLE)
220
+
221
+ # Reset stream for Request A
222
+ id_a = provider.reset_stream("req-A")
223
+ self.assertEqual(id_a, "req-A")
224
+ self.assertEqual(provider.state, RequestState.STARTING)
225
+
226
+ # Reset stream for Request B (cancels A)
227
+ id_b = provider.reset_stream("req-B")
228
+ self.assertEqual(id_b, "req-B")
229
+ self.assertNotEqual(id_a, id_b)
230
+
231
+ # Cancel active request
232
+ cancelled = provider.cancel_active()
233
+ self.assertTrue(cancelled)
234
+ self.assertEqual(provider.state, RequestState.CANCELLED)
235
+
236
+ # -------------------------------------------------------------------------
237
+ # Test F: Small Model Profile (deepseek-coder:1.3b-base-q8_0)
238
+ # -------------------------------------------------------------------------
239
+ def test_f_small_model_capabilities_and_profile(self):
240
+ """deepseek-coder:1.3b-base-q8_0 is classified as small/base model with minimal prompting."""
241
+ model_name = "deepseek-coder:1.3b-base-q8_0"
242
+ caps = detect_capabilities("ollama", model_name)
243
+
244
+ self.assertTrue(caps.is_small_model, "1.3b must be identified as small model")
245
+ self.assertTrue(caps.is_base_model, "base-q8_0 must be identified as base model")
246
+ self.assertFalse(caps.instruction_following, "Base model does not have full instruction-following capability")
247
+ self.assertFalse(caps.tool_calling, "Small base model must not be expected to execute agent tool loops")
248
+
249
+ profile = get_model_profile("ollama", model_name)
250
+ self.assertEqual(profile.prompt_template, "minimal")
251
+ self.assertEqual(profile.max_context_tokens, 1536)
252
+ self.assertEqual(profile.reserved_output_tokens, 512)
253
+ self.assertEqual(profile.recommended_temperature, 0.3)
254
+ self.assertGreaterEqual(profile.generation_options.get("repeat_penalty", 1.0), 1.15)
255
+
256
+ # -------------------------------------------------------------------------
257
+ # Test G: Context Token Budgeting & Priority Trimming
258
+ # -------------------------------------------------------------------------
259
+ def test_g_context_token_budgeting_trims_lowest_priority(self):
260
+ """When history exceeds available budget, older turns are trimmed while user prompt is preserved."""
261
+ profile = get_model_profile("ollama", "deepseek-coder:1.3b-base-q8_0")
262
+
263
+ # Create 10 history turns with 200 characters each
264
+ history = [
265
+ AIMessage(id=f"h{i}", role="user" if i % 2 == 0 else "assistant",
266
+ content=f"This is historical turn number {i} " * 5, mode="chat")
267
+ for i in range(10)
268
+ ]
269
+
270
+ req = AIRequest(
271
+ session_id="sess-budget",
272
+ request_id=str(uuid.uuid4()),
273
+ mode="chat",
274
+ user_message="My most important current request",
275
+ history=history,
276
+ model=profile.model,
277
+ provider=profile.provider,
278
+ )
279
+
280
+ ctx = self.ctx_mgr.build_context(req, profile=profile)
281
+
282
+ # User request MUST be present
283
+ self.assertEqual(ctx.messages[-1]["content"], "My most important current request")
284
+ # System prompt MUST be present
285
+ self.assertEqual(ctx.messages[0]["role"], "system")
286
+ # Total tokens must not exceed maximum budget
287
+ self.assertLessEqual(ctx.token_estimate, profile.max_context_tokens)
288
+
289
+ # -------------------------------------------------------------------------
290
+ # Test H: Repetition Detection (GenerationGuard)
291
+ # -------------------------------------------------------------------------
292
+ def test_h_repetition_detection_stops_runaway_loops(self):
293
+ """GenerationGuard halts repetitive output like 'hihihihihihi...' or identical sentences."""
294
+ guard = GenerationGuard(max_repeated_chars=16, max_sentence_repeats=3)
295
+
296
+ # 1. Feed normal tokens
297
+ ok, chunk = guard.feed("Hello there! How can I help you today? ")
298
+ self.assertTrue(ok)
299
+ self.assertEqual(chunk, "Hello there! How can I help you today? ")
300
+
301
+ # 2. Feed repetitive token loop 'hihihihihihihihihi...'
302
+ ok, chunk = guard.feed("hihihihihihihihihihihihihihihihihi")
303
+ self.assertFalse(ok, "Guard must detect runaway repetitive loop")
304
+ self.assertIn("repetitive output pattern detected", chunk)
305
+
306
+ # 3. Test sentence loop with a new guard
307
+ guard2 = GenerationGuard(max_repeated_chars=20, max_sentence_repeats=3)
308
+ guard2.feed("The quick brown fox jumps over the lazy dog. ")
309
+ guard2.feed("The quick brown fox jumps over the lazy dog. ")
310
+ ok, chunk = guard2.feed("The quick brown fox jumps over the lazy dog. ")
311
+ self.assertFalse(ok, "Guard must detect repeating identical sentences")
312
+ self.assertIn("sentence repetition loop detected", chunk)
313
+
314
+ # -------------------------------------------------------------------------
315
+ # Test I: Provider Failure Handling
316
+ # -------------------------------------------------------------------------
317
+ def test_i_provider_failure_returns_explicit_error_without_corrupting(self):
318
+ """Connection failure produces clean, user-friendly error string."""
319
+ with patch("requests.post", side_effect=Exception("Connection refused")):
320
+ provider = OllamaProvider("http://localhost:99999")
321
+ streamed = list(provider.stream_chat("model", [{"role": "user", "content": "hi"}]))
322
+
323
+ result = "".join(streamed)
324
+ self.assertIn("Ollama streaming failure", result)
325
+ self.assertEqual(provider.state, RequestState.FAILED)
326
+
327
+ # -------------------------------------------------------------------------
328
+ # Test J: Model-vs-CAT Diagnostic Test (HELLO_TEST)
329
+ # -------------------------------------------------------------------------
330
+ def test_j_model_vs_cat_diagnostic_test(self):
331
+ """run_hello_test verifies whether Ollama produces expected exact token."""
332
+ mock_resp = MagicMock()
333
+ mock_resp.status_code = 200
334
+ mock_resp.json.return_value = {"message": {"content": "HELLO_TEST"}}
335
+
336
+ with patch("requests.post", return_value=mock_resp):
337
+ provider = OllamaProvider("http://localhost:11434")
338
+ ok, reply = provider.run_hello_test("llama3.2")
339
+ self.assertTrue(ok)
340
+ self.assertEqual(reply, "HELLO_TEST")
341
+
342
+
343
+ if __name__ == "__main__":
344
+ unittest.main()