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,527 @@
1
+ """CCT root-cause engineering fix — regression tests.
2
+
3
+ Covers the full model→tool→execution→result→model pipeline:
4
+ - XML <invoke> / <minimax:toolcall> / bare-invoke tool calls EXECUTE
5
+ - JSON protocol tool calls execute
6
+ - multi-tool-call responses execute
7
+ - agent continues after tool results and finishes
8
+ - permission modes gate execution (ask/full/restricted)
9
+ - archive tools: list/delete/validate/extract/add roundtrip with
10
+ safe-replace semantics (original preserved on failure)
11
+ - clean_final_text strips ALL internal tool markup
12
+ - paths_from_paste handles drag-and-drop path formats, rejects prose
13
+ - workspace read/write path resolution incl. attachment exception
14
+
15
+ Run: python -m pytest calc_terminal/test_root_cause_fix.py -q
16
+ """
17
+ import io
18
+ import json
19
+ import os
20
+ import shutil
21
+ import sys
22
+ import tempfile
23
+ import unittest
24
+ import zipfile
25
+
26
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
27
+
28
+ from calc_terminal import agent as cct_agent
29
+ from calc_terminal import workspace as cct_workspace
30
+ from calc_terminal import permissions as perm
31
+
32
+
33
+ class _ScriptedModel:
34
+ """Replaces aicore.query_ai with a canned response sequence so the
35
+ real run_agent loop can be driven end-to-end with no network."""
36
+
37
+ def __init__(self, script):
38
+ self.script = list(script)
39
+ self.calls = []
40
+
41
+ def __call__(self, prompt, system_prompt=None, **kw):
42
+ self.calls.append(prompt)
43
+ i = min(len(self.calls) - 1, len(self.script) - 1)
44
+ return self.script[i]
45
+
46
+
47
+ class _TempWorkspace:
48
+ def __init__(self):
49
+ self._old = None
50
+ self.root = tempfile.mkdtemp(prefix="cct-test-ws-")
51
+
52
+ def __enter__(self):
53
+ self._old = cct_workspace._active_project_root
54
+ cct_workspace.set_active_project(self.root)
55
+ return self
56
+
57
+ def __exit__(self, *a):
58
+ if self._old is None:
59
+ cct_workspace._active_project_root = None
60
+ else:
61
+ cct_workspace.set_active_project(self._old)
62
+ shutil.rmtree(self.root, ignore_errors=True)
63
+
64
+
65
+ def _run_agent(script, mode="full", **kw):
66
+ """Run run_agent under FULL permission mode with a scripted model."""
67
+ perm.manager.set_mode(mode)
68
+ model = _ScriptedModel(script)
69
+ orig = cct_agent.aicore.query_ai
70
+ cct_agent.aicore.query_ai = model
71
+ try:
72
+ final, steps, meta = cct_agent.run_agent(
73
+ "test request", verbose=False,
74
+ permission_callback=lambda *a, **k: "allow_once", **kw)
75
+ finally:
76
+ cct_agent.aicore.query_ai = orig
77
+ return final, steps, meta, model
78
+
79
+
80
+ class TestXMLToolCallsExecute(unittest.TestCase):
81
+ """Requirements #1/#2/#4: XML-style tool calls are parsed AND really
82
+ executed; the agent then continues and finishes."""
83
+
84
+ def test_invoke_xml_executes_real_tool(self):
85
+ with _TempWorkspace():
86
+ final, steps, meta, model = _run_agent([
87
+ 'I\'ll start by inspecting the workspace...\n'
88
+ '<invoke name="runcommand">\n'
89
+ ' <parameter name="command">echo hello-from-cct</parameter>\n'
90
+ '</invoke>',
91
+ '{"action": "final", "text": "done"}',
92
+ ])
93
+ self.assertEqual([s[0] for s in steps], ["run_terminal"])
94
+ obs = steps[0][2]
95
+ self.assertIn("hello-from-cct", obs) # REAL command output
96
+ self.assertIn("success", obs.lower())
97
+ self.assertEqual(len(model.calls), 2) # loop CONTINUED after result
98
+
99
+ def test_listdirectory_alias_resolves_and_executes(self):
100
+ with _TempWorkspace():
101
+ open(os.path.join(cct_workspace.root_dir(), "marker.txt"), "w").write("x")
102
+ final, steps, meta, model = _run_agent([
103
+ '<invoke name="listdirectory">\n'
104
+ ' <parameter name="path">.</parameter>\n'
105
+ '</invoke>',
106
+ '{"action": "final", "text": "ok"}',
107
+ ])
108
+ self.assertEqual(steps[0][0], "list_directory")
109
+ self.assertIn("marker.txt", steps[0][2])
110
+
111
+ def test_minimax_wrapped_xml_executes(self):
112
+ with _TempWorkspace():
113
+ final, steps, meta, model = _run_agent([
114
+ '<minimax:toolcall>\n'
115
+ ' <invoke name="listdirectory">\n'
116
+ ' <parameter name="path">.</parameter>\n'
117
+ ' </invoke>\n'
118
+ '</minimax:toolcall>',
119
+ '{"action": "final", "text": "ok"}',
120
+ ])
121
+ self.assertEqual(steps[0][0], "list_directory")
122
+ self.assertTrue(os.path.isabs(steps[0][2].split("'")[1]))
123
+
124
+ def test_bare_unclosed_invoke_executes(self):
125
+ with _TempWorkspace():
126
+ final, steps, meta, model = _run_agent([
127
+ '<invoke name="calculate">\n'
128
+ '<parameter name="expression">6*7</parameter>',
129
+ '{"action": "final", "text": "ok"}',
130
+ ])
131
+ self.assertEqual(steps[0][0], "calculate")
132
+ self.assertIn("42", steps[0][2])
133
+
134
+ def test_json_protocol_still_works(self):
135
+ with _TempWorkspace():
136
+ final, steps, meta, model = _run_agent([
137
+ '{"action": "tool", "tool": "calculate", "args": {"expression": "2+2"}}',
138
+ '{"action": "final", "text": "four"}',
139
+ ])
140
+ self.assertEqual(final, "four")
141
+ self.assertIn("= 4", steps[0][2])
142
+
143
+ def test_multiple_tool_calls_in_one_response(self):
144
+ with _TempWorkspace():
145
+ final, steps, meta, model = _run_agent([
146
+ '<invoke name="calculate">'
147
+ '<parameter name="expression">2+3</parameter></invoke>'
148
+ '<invoke name="calculate">'
149
+ '<parameter name="expression">10*10</parameter></invoke>',
150
+ '{"action": "final", "text": "both done"}',
151
+ ])
152
+ self.assertEqual(len(steps), 2)
153
+ self.assertIn("= 5", steps[0][2])
154
+ self.assertIn("= 100", steps[1][2])
155
+
156
+ def test_write_then_verify_file_roundtrip(self):
157
+ with _TempWorkspace():
158
+ target = os.path.join(cct_workspace.root_dir(), "gen", "app.py")
159
+ write_call = ('<invoke name="create file">\n'
160
+ '<parameter name="path">gen/app.py</parameter>\n'
161
+ '<parameter name="content">print("hi")</parameter>\n'
162
+ '</invoke>')
163
+ final, steps, meta, model = _run_agent([
164
+ write_call,
165
+ '<invoke name="readfile">\n'
166
+ '<parameter name="path">gen/app.py</parameter>\n</invoke>',
167
+ '{"action": "final", "text": "created and verified"}',
168
+ ])
169
+ self.assertEqual(steps[0][0], "write_file")
170
+ self.assertTrue(os.path.isfile(target))
171
+ self.assertIn('print("hi")', steps[1][2]) # verified by real read
172
+
173
+ def test_on_step_and_on_tool_result_callbacks_fire(self):
174
+ with _TempWorkspace():
175
+ started, finished = [], []
176
+ perm.manager.set_mode("full")
177
+ model = _ScriptedModel([
178
+ '<invoke name="calculate"><parameter name="expression">1+1</parameter></invoke>',
179
+ '{"action": "final", "text": "ok"}',
180
+ ])
181
+ orig = cct_agent.aicore.query_ai
182
+ cct_agent.aicore.query_ai = model
183
+ try:
184
+ cct_agent.run_agent(
185
+ "t", verbose=False,
186
+ on_step=lambda n, a: started.append(n),
187
+ on_tool_result=lambda n, a, o: finished.append((n, o)))
188
+ finally:
189
+ cct_agent.aicore.query_ai = orig
190
+ self.assertEqual(started, ["calculate"])
191
+ self.assertEqual(len(finished), 1)
192
+ self.assertEqual(finished[0][0], "calculate")
193
+ self.assertIn("= 2", finished[0][1])
194
+
195
+
196
+ class TestPermissionModesGateExecution(unittest.TestCase):
197
+ """Requirement #6/#7: modes are real execution policies."""
198
+
199
+ def test_restricted_denies_write_without_prompt_and_reports_error(self):
200
+ with _TempWorkspace():
201
+ decisions = []
202
+ perm.manager.set_mode("restricted")
203
+
204
+ def cb(key, label, path, reason):
205
+ decisions.append(key)
206
+ return "deny"
207
+
208
+ model = _ScriptedModel([
209
+ '<invoke name="write_file">\n'
210
+ '<parameter name="path">blocked.txt</parameter>\n'
211
+ '<parameter name="content">nope</parameter>\n</invoke>',
212
+ '{"action": "final", "text": "understood"}',
213
+ ])
214
+ orig = cct_agent.aicore.query_ai
215
+ cct_agent.aicore.query_ai = model
216
+ try:
217
+ final, steps, meta = cct_agent.run_agent(
218
+ "t", verbose=False, permission_callback=cb)
219
+ finally:
220
+ cct_agent.aicore.query_ai = orig
221
+ self.assertEqual(decisions, ["write_files"])
222
+ target = os.path.join(cct_workspace.root_dir(), "blocked.txt")
223
+ self.assertFalse(os.path.exists(target)) # nothing written
224
+ self.assertTrue(
225
+ any(w in steps[0][2].lower() for w in ("denied", "rejected")),
226
+ f"honest error to model, got: {steps[0][2][:80]}")
227
+
228
+ def test_ask_mode_prompts_and_allow_once_writes(self):
229
+ with _TempWorkspace():
230
+ asked = []
231
+ perm.manager.set_mode("ask")
232
+
233
+ def cb(key, label, path, reason):
234
+ asked.append((key, label))
235
+ return "allow_once"
236
+
237
+ model = _ScriptedModel([
238
+ '<invoke name="write_file">\n'
239
+ '<parameter name="path">allowed.txt</parameter>\n'
240
+ '<parameter name="content">yes</parameter>\n</invoke>',
241
+ '{"action": "final", "text": "did it"}',
242
+ ])
243
+ orig = cct_agent.aicore.query_ai
244
+ cct_agent.aicore.query_ai = model
245
+ try:
246
+ final, steps, meta = cct_agent.run_agent(
247
+ "t", verbose=False, permission_callback=cb)
248
+ finally:
249
+ cct_agent.aicore.query_ai = orig
250
+ self.assertEqual([a[0] for a in asked], ["write_files"])
251
+ self.assertTrue(os.path.isfile(
252
+ os.path.join(cct_workspace.root_dir(), "allowed.txt")))
253
+
254
+ def test_full_access_runs_without_prompting(self):
255
+ with _TempWorkspace():
256
+ prompted = []
257
+ perm.manager.set_mode("full")
258
+
259
+ model = _ScriptedModel([
260
+ '<invoke name="delete file">\n'
261
+ '<parameter name="path">old.txt</parameter>\n</invoke>',
262
+ '{"action": "final", "text": "deleted"}',
263
+ ])
264
+ # create the file first
265
+ open(os.path.join(cct_workspace.root_dir(), "old.txt"), "w").close()
266
+ orig = cct_agent.aicore.query_ai
267
+ cct_agent.aicore.query_ai = model
268
+ try:
269
+ final, steps, meta = cct_agent.run_agent(
270
+ "t", verbose=False,
271
+ permission_callback=lambda *a: prompted.append(a))
272
+ finally:
273
+ cct_agent.aicore.query_ai = orig
274
+ self.assertEqual(prompted, []) # never asked
275
+ self.assertFalse(os.path.exists(
276
+ os.path.join(cct_workspace.root_dir(), "old.txt")))
277
+ self.assertIn("Deleted", steps[0][2])
278
+
279
+ def test_install_full_access_no_prompt_ask_prompts_restricted_refuses(self):
280
+ from calc_terminal import packages as pkgs
281
+ # remembered-decision store must not leak between assertions
282
+ for manager_name in ("pip",):
283
+ pass
284
+ with _TempWorkspace():
285
+ perm.manager.set_mode("full")
286
+ proceed, denial = cct_agent._check_permission(
287
+ "install_packages", {"packages": "cct-never-a-real-pkg"},
288
+ lambda *a, **k: "allow_once")
289
+ self.assertTrue(proceed)
290
+
291
+ perm.manager.set_mode("ask")
292
+ asked = []
293
+ proceed, denial = cct_agent._check_permission(
294
+ "install_packages", {"packages": "cct-never-a-real-pkg"},
295
+ lambda k, l, p, r: (asked.append(k), "allow_once")[1])
296
+ self.assertTrue(proceed)
297
+ self.assertEqual(asked, ["install_packages"])
298
+
299
+ perm.manager.set_mode("restricted")
300
+ proceed, denial = cct_agent._check_permission(
301
+ "install_packages", {"packages": "cct-never-a-real-pkg"},
302
+ lambda *a, **k: "allow_once")
303
+ self.assertFalse(proceed)
304
+ self.assertIn("Restricted", denial)
305
+
306
+
307
+ class TestArchiveTools(unittest.TestCase):
308
+ """Requirements #9/#10/#21: real archive inspection + controlled
309
+ modification with original-preservation."""
310
+
311
+ def setUp(self):
312
+ self._ws = _TempWorkspace()
313
+ self._ws.__enter__()
314
+ self.zip_path = os.path.join(cct_workspace.root_dir(), "project.zip")
315
+ with zipfile.ZipFile(self.zip_path, "w") as zf:
316
+ zf.writestr("folders/", "")
317
+ zf.writestr("folders/images/", "")
318
+ zf.writestr("folders/images/a.png", b"pngdata")
319
+ zf.writestr("folders/data/", "")
320
+ zf.writestr("folders/data/b.csv", "x,y\n1,2")
321
+ zf.writestr("readme.txt", "hello")
322
+
323
+ def tearDown(self):
324
+ self._ws.__exit__()
325
+
326
+ def test_archive_list_reports_folders_and_files(self):
327
+ out = cct_agent.TOOLS["archive_list"]["run"]({"path": "project.zip"})
328
+ self.assertIn("project.zip", out)
329
+ self.assertIn("folders/", out)
330
+ self.assertIn("readme.txt", out)
331
+ self.assertIn("folder", out.lower())
332
+
333
+ def test_delete_all_folders_keeps_files_and_validates(self):
334
+ delete = cct_agent.TOOLS["archive_delete_entries"]["run"]
335
+ out = delete({"path": "project.zip",
336
+ "entries": ["folders/", "folders/images/", "folders/data/"]})
337
+ self.assertIn("Deleted", out)
338
+ validate = cct_agent.TOOLS["archive_validate"]["run"]
339
+ verdict = validate({"path": "project.zip"})
340
+ self.assertIn("VALID", verdict)
341
+ with zipfile.ZipFile(self.zip_path) as zf:
342
+ names = zf.namelist()
343
+ self.assertNotIn("folders/", names)
344
+ self.assertNotIn("folders/images/a.png", names)
345
+ self.assertIn("readme.txt", names)
346
+
347
+ def test_failed_modification_preserves_original(self):
348
+ # simulate a mid-rewrite crash by pointing at a corrupt zip
349
+ corrupt = os.path.join(cct_workspace.root_dir(), "corrupt.zip")
350
+ with open(corrupt, "wb") as f:
351
+ f.write(b"PK\x03\x04garbage-not-a-zip")
352
+ ok, msg = cct_agent._rewrite_zip_safely(corrupt, lambda item: True)
353
+ self.assertFalse(ok)
354
+ with open(corrupt, "rb") as f:
355
+ self.assertEqual(f.read(), b"PK\x03\x04garbage-not-a-zip")
356
+
357
+ def test_json_string_list_args_are_coerced(self):
358
+ """XML <parameter> values arrive as strings — a JSON-encoded
359
+ list must still delete the right entries."""
360
+ out = cct_agent.TOOLS["archive_delete_entries"]["run"](
361
+ {"path": "project.zip", "entries": '["folders/", "folders/images/"]'})
362
+ self.assertIn("Deleted", out)
363
+ with zipfile.ZipFile(self.zip_path) as zf:
364
+ self.assertEqual(zf.namelist(), ["readme.txt"])
365
+
366
+ def test_extract_add_validate_roundtrip(self):
367
+ extract = cct_agent.TOOLS["archive_extract"]["run"]
368
+ out = extract({"path": "project.zip"})
369
+ self.assertIn("Extracted", out)
370
+ extracted_dir = os.path.join(cct_workspace.root_dir(), "project")
371
+ self.assertTrue(os.path.isdir(extracted_dir))
372
+ add = cct_agent.TOOLS["archive_add_entries"]["run"]
373
+ new_txt = os.path.join(cct_workspace.root_dir(), "extra.txt")
374
+ open(new_txt, "w").write("added")
375
+ out = add({"path": "project.zip", "files": [new_txt],
376
+ "entries": [{"name": "docs/generated.md", "content": "# gen"}]})
377
+ self.assertIn("Added", out)
378
+ with zipfile.ZipFile(self.zip_path) as zf:
379
+ names = zf.namelist()
380
+ self.assertIn("docs/generated.md", names)
381
+ self.assertEqual(zf.read("docs/generated.md"), b"# gen")
382
+ self.assertIn("VALID", cct_agent.TOOLS["archive_validate"]["run"]({"path": "project.zip"}))
383
+
384
+ def test_attached_zip_outside_home_is_operable(self):
385
+ # an attached archive on another drive/root is still in scope (#22)
386
+ outside = tempfile.mkdtemp(prefix="cct-outside-")
387
+ try:
388
+ zpath = os.path.join(outside, "attached.zip")
389
+ with zipfile.ZipFile(zpath, "w") as zf:
390
+ zf.writestr("deep/folder/x.txt", "data")
391
+ zf.writestr("top.txt", "t")
392
+
393
+ class _Att:
394
+ path = zpath
395
+ id = "att-x"
396
+ name = "attached.zip"
397
+ kind = "archive"
398
+ size = 1
399
+ extraction_status = "ready"
400
+
401
+ cct_agent.set_attachments([_Att()])
402
+ try:
403
+ listed = cct_agent.TOOLS["archive_list"]["run"]({"path": zpath})
404
+ self.assertIn("deep/folder/x.txt", listed)
405
+ deleted = cct_agent.TOOLS["archive_delete_entries"]["run"](
406
+ {"path": zpath, "entries": ["deep/folder/"]})
407
+ self.assertIn("Deleted", deleted)
408
+ with zipfile.ZipFile(zpath) as zf:
409
+ self.assertEqual(zf.namelist(), ["top.txt"])
410
+ finally:
411
+ cct_agent.clear_attachments()
412
+ finally:
413
+ shutil.rmtree(outside, ignore_errors=True)
414
+
415
+
416
+ class TestMarkupNeverLeaks(unittest.TestCase):
417
+ """Requirements #23/#24: internal protocol stays internal."""
418
+
419
+ def test_clean_final_text_shapes(self):
420
+ cases = [
421
+ ('{"action": "final", "text": "hello"}', "hello"),
422
+ ('I created it.\n<invoke name="write_file">\n'
423
+ '<parameter name="path">x</parameter>\n</invoke>', "I created it."),
424
+ ('<minimax:toolcall><invoke name="ls">'
425
+ '<parameter name="path">/w</parameter></invoke></minimax:toolcall>', ""),
426
+ ('before <tool_call><name>f</name><arguments>{}</arguments>'
427
+ '</tool_call> after', "before after"),
428
+ ]
429
+ for raw, expected in cases:
430
+ self.assertEqual(cct_agent.clean_final_text(raw).strip(), expected.strip())
431
+
432
+ def test_empty_after_cleanup_falls_back_to_step_summary(self):
433
+ with _TempWorkspace():
434
+ final, steps, meta, model = _run_agent([
435
+ '<invoke name="calculate"><parameter name="expression">5+5</parameter></invoke>',
436
+ '<invoke name="calculate"><parameter name="expression">5+5</parameter></invoke>'
437
+ '<minimax:toolcall></minimax:toolcall>',
438
+ ])
439
+ # last response was pure markup -> summary built from real steps
440
+ self.assertIn("calculate", final)
441
+ self.assertIn("= 10", final)
442
+
443
+ def test_strip_tool_markup_for_clipboard(self):
444
+ dirty = ("Answer text.\n"
445
+ "<invoke name=\"runcommand\">\n"
446
+ "<parameter name=\"command\">ls -la</parameter>\n"
447
+ "<parameter name=\"description\">List workspace contents</parameter>\n"
448
+ "</invoke>\ntrailing words")
449
+ cleaned = cct_agent.strip_tool_markup(dirty)
450
+ self.assertNotIn("<invoke", cleaned)
451
+ self.assertNotIn("<parameter", cleaned)
452
+ self.assertIn("Answer text.", cleaned)
453
+ self.assertIn("trailing words", cleaned)
454
+
455
+
456
+ class TestDragDropPathPaste(unittest.TestCase):
457
+ """Requirements #14/#15: drag-drop arrives as path paste."""
458
+
459
+ def _mkfile(self):
460
+ fd, p = tempfile.mkstemp(suffix=".txt")
461
+ os.write(fd, b"hi")
462
+ os.close(fd)
463
+ return p
464
+
465
+ def test_single_path(self):
466
+ p = self._mkfile()
467
+ try:
468
+ self.assertEqual(cct_ui_paths(p), [os.path.normpath(p)])
469
+ finally:
470
+ os.unlink(p)
471
+
472
+ def test_nul_separated_multi_drop(self):
473
+ a, b = self._mkfile(), self._mkfile()
474
+ try:
475
+ got = cct_ui_paths(f"{a}\x00{b}")
476
+ self.assertEqual(got, [os.path.normpath(a), os.path.normpath(b)])
477
+ finally:
478
+ os.unlink(a); os.unlink(b)
479
+
480
+ def test_quoted_crlf_paths(self):
481
+ p = self._mkfile()
482
+ try:
483
+ got = cct_ui_paths(f'"{p}"\r\n')
484
+ self.assertEqual(got, [os.path.normpath(p)])
485
+ finally:
486
+ os.unlink(p)
487
+
488
+ def test_prose_is_not_a_path_paste(self):
489
+ self.assertIsNone(cct_ui_paths("please delete all folders in compressed file"))
490
+ self.assertIsNone(cct_ui_paths("C:\\definitely\\not\\here.txt"))
491
+
492
+ def test_directory_paste_is_detected_but_rejected_at_attach(self):
493
+ d = tempfile.mkdtemp(prefix="cct-dirdrop-")
494
+ try:
495
+ got = cct_ui_paths(d)
496
+ self.assertEqual(got, [os.path.normpath(d)]) # detected...
497
+ # ...then rejected by the attach validator (returns None chip)
498
+ from calc_terminal.ui import attachments as ui_att
499
+ if ui_att.AttachmentBar is not None:
500
+ # pure-function level: Attachment object creation works;
501
+ # the widget-level rejection is covered by attach_files_from_ui
502
+ pass
503
+ finally:
504
+ shutil.rmtree(d, ignore_errors=True)
505
+
506
+
507
+ def cct_ui_paths(text):
508
+ from calc_terminal.ui.attachments import paths_from_paste
509
+ return paths_from_paste(text)
510
+
511
+
512
+ class TestWorkspacePathResolution(unittest.TestCase):
513
+ def test_readable_path_allows_anywhere_non_blocked(self):
514
+ p = cct_workspace.resolve_readable_path(tempfile.gettempdir())
515
+ self.assertTrue(os.path.isabs(p))
516
+
517
+ def test_readable_path_blocks_system_roots(self):
518
+ with self.assertRaises(ValueError):
519
+ cct_workspace.resolve_readable_path("C:\\Windows\\System32")
520
+
521
+ def test_write_outside_home_still_refused(self):
522
+ with self.assertRaises(ValueError):
523
+ cct_workspace.resolve_writable_path("D:\\some-other-place\\f.txt")
524
+
525
+
526
+ if __name__ == "__main__":
527
+ unittest.main(verbosity=2)