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,255 @@
1
+ """
2
+ CAT v0.7.10 — project detection + external dev-server unit tests
3
+ (hermetic: pure filesystem + pure regex; NO npm/node required).
4
+
5
+ Covers spec sections 19-20:
6
+
7
+ detector static · vite (config) · vite (dependency) · next · CRA ·
8
+ node-without-dev-script · package.json-without-scripts ·
9
+ missing folder · dependencies_installed()
10
+ url parse Vite/Next/CRA output lines → local URL, 0.0.0.0/[::1]
11
+ normalization, punctuation trimming, non-URL lines
12
+ controller framework detection routes through DevServerProcess and
13
+ never starts CAT's static LiveServer in that mode (a fake
14
+ DevServerProcess proves the wiring without spawning npm)
15
+
16
+ Run: python -m pytest calc_terminal/test_project_detect.py -q
17
+ """
18
+
19
+ import os
20
+ import sys
21
+ import tempfile
22
+ import unittest
23
+
24
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
25
+
26
+ from calc_terminal.browser.project_detector import (
27
+ detect_project, dependencies_installed,
28
+ STATIC, VITE, NEXT, CRA, NODE)
29
+ from calc_terminal.browser.devserver import DevServerProcess, extract_local_url
30
+
31
+
32
+ def _write(root, name, content=""):
33
+ path = os.path.join(root, name)
34
+ os.makedirs(os.path.dirname(path), exist_ok=True)
35
+ with open(path, "w", encoding="utf-8") as f:
36
+ f.write(content)
37
+ return path
38
+
39
+
40
+ class TestProjectDetection(unittest.TestCase):
41
+ def setUp(self):
42
+ self.root = tempfile.mkdtemp(prefix="cat_detect_")
43
+
44
+ def test_static_by_default(self):
45
+ _write(self.root, "index.html", "<h1>hi</h1>")
46
+ d = detect_project(self.root)
47
+ self.assertEqual(d.kind, STATIC)
48
+ self.assertFalse(d.is_framework)
49
+ self.assertTrue(dependencies_installed(d))
50
+
51
+ def test_vite_config_file_wins(self):
52
+ _write(self.root, "index.html")
53
+ _write(self.root, "package.json",
54
+ '{"scripts": {"dev": "vite"}}')
55
+ _write(self.root, "vite.config.ts", "export default {}")
56
+ d = detect_project(self.root)
57
+ self.assertEqual(d.kind, VITE)
58
+ self.assertEqual(d.command, ("npm", "run", "dev"))
59
+ self.assertIn("vite.config.ts", d.evidence)
60
+
61
+ def test_vite_dependency_only(self):
62
+ _write(self.root, "package.json",
63
+ '{"scripts": {"dev": "vite"},'
64
+ ' "devDependencies": {"vite": "^5.0.0"}}')
65
+ d = detect_project(self.root)
66
+ self.assertEqual(d.kind, VITE)
67
+
68
+ def test_nextjs(self):
69
+ _write(self.root, "package.json",
70
+ '{"scripts": {"dev": "next dev"}}')
71
+ _write(self.root, "next.config.js", "module.exports = {}")
72
+ d = detect_project(self.root)
73
+ self.assertEqual(d.kind, NEXT)
74
+
75
+ def test_cra(self):
76
+ _write(self.root, "package.json",
77
+ '{"scripts": {"start": "react-scripts start"}}')
78
+ d = detect_project(self.root)
79
+ self.assertEqual(d.kind, CRA)
80
+ self.assertEqual(d.command, ("npm", "start"))
81
+
82
+ def test_node_without_framework(self):
83
+ _write(self.root, "package.json",
84
+ '{"scripts": {"dev": "node server.js"}}')
85
+ d = detect_project(self.root)
86
+ self.assertEqual(d.kind, NODE)
87
+
88
+ def test_package_without_scripts_falls_back_static(self):
89
+ _write(self.root, "package.json", '{"name": "x"}')
90
+ _write(self.root, "index.html")
91
+ d = detect_project(self.root)
92
+ self.assertEqual(d.kind, STATIC)
93
+
94
+ def test_missing_folder_is_safe(self):
95
+ d = detect_project(os.path.join(self.root, "nope"))
96
+ self.assertEqual(d.kind, STATIC)
97
+
98
+ def test_dependencies_installed_check(self):
99
+ _write(self.root, "package.json", '{"scripts": {"dev": "vite"}}')
100
+ _write(self.root, "vite.config.js")
101
+ d = detect_project(self.root)
102
+ self.assertTrue(d.is_framework)
103
+ self.assertFalse(dependencies_installed(d))
104
+ os.makedirs(os.path.join(self.root, "node_modules"))
105
+ self.assertTrue(dependencies_installed(d))
106
+
107
+
108
+ class TestExtractLocalUrl(unittest.TestCase):
109
+ def test_vite_line(self):
110
+ self.assertEqual(
111
+ extract_local_url(" ➜ Local: http://localhost:5173/"),
112
+ "http://localhost:5173/")
113
+
114
+ def test_next_line(self):
115
+ self.assertEqual(
116
+ extract_local_url("- Local: http://localhost:3000"),
117
+ "http://localhost:3000/")
118
+
119
+ def test_loopback_ip(self):
120
+ self.assertEqual(
121
+ extract_local_url("[1] Web server available at "
122
+ "http://127.0.0.1:8080/main/ ok"),
123
+ "http://127.0.0.1:8080/main/")
124
+
125
+ def test_bind_address_normalized(self):
126
+ url = extract_local_url("listening on http://0.0.0.0:4000/")
127
+ self.assertEqual(url, "http://127.0.0.1:4000/")
128
+
129
+ def test_ipv6_normalized(self):
130
+ url = extract_local_url("ready on http://[::1]:4321/")
131
+ self.assertEqual(url, "http://127.0.0.1:4321/")
132
+
133
+ def test_trailing_punctuation_trimmed(self):
134
+ self.assertEqual(extract_local_url("at http://localhost:5000."),
135
+ "http://localhost:5000/")
136
+
137
+ def test_non_url_lines_rejected(self):
138
+ self.assertIsNone(extract_local_url(""))
139
+ self.assertIsNone(extract_local_url("VITE v5.0.0 ready in 320 ms"))
140
+ self.assertIsNone(extract_local_url("https://example.com/remote-only"))
141
+
142
+
143
+ class TestControllerFrameworkRouting(unittest.TestCase):
144
+ """The controller must reuse the project's own dev server instead
145
+ of stacking CAT's static LiveServer on top of it (spec §19)."""
146
+
147
+ def setUp(self):
148
+ self.root = tempfile.mkdtemp(prefix="cat_fw_")
149
+ _write(self.root, "index.html")
150
+ _write(self.root, "package.json", '{"scripts": {"dev": "vite"}}')
151
+ _write(self.root, "vite.config.js")
152
+ os.makedirs(os.path.join(self.root, "node_modules"), exist_ok=True)
153
+
154
+ def test_framework_routes_through_devserver(self):
155
+ from calc_terminal.browser.preview import PreviewController
156
+ from calc_terminal.browser.state import PreviewSnapshot
157
+
158
+ class FakeDevServer:
159
+ def __init__(self, root, command, on_activity=None):
160
+ self.root, self.command = root, tuple(command)
161
+ self.url = ""
162
+ self.stopped = False
163
+
164
+ def start(self, timeout=45.0):
165
+ self.url = "http://localhost:5173/"
166
+ return True
167
+
168
+ @property
169
+ def running(self):
170
+ return bool(self.url) and not self.stopped
171
+
172
+ def stop(self):
173
+ self.stopped = True
174
+
175
+ created = []
176
+
177
+ def factory(root, command, on_activity=None):
178
+ ds = FakeDevServer(root, command, on_activity)
179
+ created.append(ds)
180
+ return ds
181
+
182
+ class FakeEngine:
183
+ available = True
184
+ started = False
185
+ navigate_calls = []
186
+
187
+ def start(self):
188
+ self.started = True
189
+
190
+ def navigate(self, url, timeout=35.0):
191
+ self.navigate_calls.append(url)
192
+ return PreviewSnapshot(url=url, ok=True, elements=1)
193
+
194
+ def close(self, timeout=6.0):
195
+ pass
196
+
197
+ ctrl = PreviewController(self.root, engine=FakeEngine(),
198
+ on_event=lambda *a, **k: None,
199
+ devserver_factory=factory)
200
+
201
+ ok = ctrl.start_for_file(os.path.join(self.root, "index.html"))
202
+
203
+ self.assertTrue(ok)
204
+ self.assertEqual(ctrl.detection.kind, VITE)
205
+ self.assertEqual(len(created), 1)
206
+ # The engine was pointed at the DEV SERVER URL, not a static one.
207
+ self.assertEqual(FakeEngine.navigate_calls,
208
+ ["http://localhost:5173/"])
209
+ # And CAT's own static LiveServer stayed idle.
210
+ self.assertFalse(ctrl.server.running)
211
+ self.assertTrue(ctrl.running)
212
+ # stop_preview must tear the dev-server down too.
213
+ ctrl.stop_preview()
214
+ self.assertTrue(created[0].stopped)
215
+
216
+ def test_framework_without_deps_uses_static_path(self):
217
+ """node_modules missing: no fake success — the honest fallback
218
+ is CAT's static server for the plain index.html at the root."""
219
+ import shutil
220
+ from calc_terminal.browser.preview import PreviewController
221
+ from calc_terminal.browser.state import PreviewSnapshot
222
+
223
+ shutil.rmtree(os.path.join(self.root, "node_modules"),
224
+ ignore_errors=True)
225
+
226
+ created = []
227
+
228
+ def factory(root, command, on_activity=None):
229
+ created.append(1)
230
+ raise AssertionError("devserver must NOT be started without "
231
+ "node_modules")
232
+
233
+ class NoStartEngine:
234
+ available = False
235
+
236
+ def close(self, timeout=6.0):
237
+ pass
238
+
239
+ ctrl = PreviewController(self.root, engine=NoStartEngine(),
240
+ on_event=lambda *a, **k: None,
241
+ devserver_factory=factory)
242
+ self.assertTrue(ctrl.detection.is_framework)
243
+ self.assertFalse(dependencies_installed(ctrl.detection))
244
+ # Engine unavailable → start fails honestly AFTER choosing the
245
+ # static path; crucially the dev-server factory was never used.
246
+ try:
247
+ ctrl.start_for_file(os.path.join(self.root, "index.html"))
248
+ except Exception:
249
+ pass
250
+ self.assertEqual(created, [])
251
+ ctrl.stop_preview() # must not raise
252
+
253
+
254
+ if __name__ == "__main__":
255
+ unittest.main()