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,368 @@
1
+ """
2
+ CAT v0.7.10 — browser subsystem regression suite (hermetic: no network
3
+ beyond 127.0.0.1 ephemeral ports, no Chromium required — a FakeEngine
4
+ mirrors the real engine's interface).
5
+
6
+ Covers spec section 28's core mechanics:
7
+
8
+ server auto port · index resolution · MIME types · images/fonts ·
9
+ no-cache headers · traversal blocked · clean stop
10
+ navigation back / forward / duplicate-push / forward-truncation / cap
11
+ entry entry-point detection · workspace-relative URLs
12
+ watcher web-file filter · debounce coalescing (150 ms)
13
+ controller state model transitions · CSS hot path vs full reload ·
14
+ AI-write debounce · error isolation · idempotent shutdown
15
+
16
+ Run: python -m pytest calc_terminal/test_browser.py -q
17
+ """
18
+
19
+ import json
20
+ import os
21
+ import sys
22
+ import tempfile
23
+ import threading
24
+ import time
25
+ import unittest
26
+ import urllib.request
27
+
28
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
29
+
30
+ from calc_terminal.browser import (NavigationHistory, PreviewController,
31
+ PaneMode, PreviewState, ServerState,
32
+ EditorState, WorkspaceMode)
33
+ from calc_terminal.browser.preview_entry import (find_entry_file,
34
+ relative_url_for,
35
+ url_for_file)
36
+ from calc_terminal.browser.server import LiveServer, find_free_port
37
+ from calc_terminal.browser.watcher import Debouncer, is_web_file
38
+
39
+
40
+ def _fetch(url, timeout=5):
41
+ req = urllib.request.Request(url, headers={"Connection": "close"})
42
+ try:
43
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
44
+ return resp.status, dict(resp.headers), resp.read()
45
+ except urllib.error.HTTPError as e:
46
+ return e.code, dict(e.headers), e.read()
47
+
48
+
49
+ def make_site(root):
50
+ with open(os.path.join(root, "index.html"), "w", encoding="utf-8") as f:
51
+ f.write("<html><head><link rel=stylesheet href=style.css></head>"
52
+ "<body><h1>Hi</h1></body></html>")
53
+ with open(os.path.join(root, "style.css"), "w", encoding="utf-8") as f:
54
+ f.write("h1{color:red}")
55
+ with open(os.path.join(root, "app.js"), "w", encoding="utf-8") as f:
56
+ f.write("console.log('ok')")
57
+ os.makedirs(os.path.join(root, "assets"), exist_ok=True)
58
+ with open(os.path.join(root, "assets", "font.woff2"), "wb") as f:
59
+ f.write(b"wOF2")
60
+ with open(os.path.join(root, "secret.txt"), "w", encoding="utf-8") as f:
61
+ f.write("workspace file")
62
+
63
+
64
+ class FakeEngine:
65
+ """BrowserEngine stand-in: records calls, returns canned snapshots."""
66
+
67
+ def __init__(self, ok=True):
68
+ self.available = False
69
+ self.ok = ok
70
+ self.navigate_calls = []
71
+ self.reload_calls = 0
72
+ self.hot_calls = []
73
+ self.closed = False
74
+ from calc_terminal.browser.state import PreviewSnapshot
75
+ self._snap = lambda url: PreviewSnapshot(
76
+ url=url, title="t", ok=self.ok,
77
+ outline_lines=("hello",), elements=3)
78
+
79
+ def start(self):
80
+ self.available = True
81
+
82
+ def navigate(self, url, timeout=35.0):
83
+ self.navigate_calls.append(url)
84
+ return self._snap(url)
85
+
86
+ def reload(self, timeout=35.0):
87
+ self.reload_calls += 1
88
+ return self._snap(self.navigate_calls[-1]
89
+ if self.navigate_calls else "")
90
+
91
+ def back(self, timeout=20.0):
92
+ return None
93
+
94
+ def forward(self, timeout=20.0):
95
+ return None
96
+
97
+ def hot_css(self, hrefs):
98
+ self.hot_calls.append(list(hrefs))
99
+ return True
100
+
101
+ def close(self):
102
+ self.closed = True
103
+ self.available = False
104
+
105
+
106
+ class TestServer(unittest.TestCase):
107
+ @classmethod
108
+ def setUpClass(cls):
109
+ cls.root = tempfile.mkdtemp(prefix="cat_srv_")
110
+ make_site(cls.root)
111
+ cls.srv = LiveServer(cls.root)
112
+ assert cls.srv.start()
113
+ assert cls.srv.wait_ready()
114
+
115
+ @classmethod
116
+ def tearDownClass(cls):
117
+ cls.srv.stop()
118
+
119
+ def test_auto_port_on_localhost(self):
120
+ self.assertTrue(self.srv.url.startswith("http://127.0.0.1:"))
121
+ port = int(self.srv.url.split(":")[2].rstrip("/"))
122
+ self.assertGreater(port, 0)
123
+ self.assertNotEqual(find_free_port(), 0)
124
+
125
+ def test_index_resolution_and_mime(self):
126
+ status, headers, body = _fetch(self.srv.url)
127
+ self.assertEqual(status, 200)
128
+ self.assertIn("text/html", headers.get("Content-Type", ""))
129
+ self.assertIn(b"<h1>Hi</h1>", body)
130
+
131
+ status, headers, body = _fetch(self.srv.url + "style.css")
132
+ self.assertEqual(status, 200)
133
+ self.assertIn("text/css", headers.get("Content-Type", ""))
134
+
135
+ status, headers, body = _fetch(self.srv.url + "app.js")
136
+ self.assertEqual(status, 200)
137
+ self.assertIn("javascript", headers.get("Content-Type", ""))
138
+
139
+ status, headers, body = _fetch(self.srv.url + "assets/font.woff2")
140
+ self.assertEqual(status, 200)
141
+ self.assertEqual(headers.get("Content-Type", ""), "font/woff2")
142
+ self.assertEqual(body[:4], b"wOF2")
143
+
144
+ def test_no_cache_headers(self):
145
+ status, headers, _ = _fetch(self.srv.url)
146
+ self.assertIn("no-cache", headers.get("Cache-Control", ""))
147
+
148
+ def test_hot_reload_snippet_injected(self):
149
+ _, _, body = _fetch(self.srv.url)
150
+ self.assertIn(b"__CAT_HOT__", body)
151
+
152
+ def test_traversal_blocked(self):
153
+ # ../.. out of the root must never resolve to a real file
154
+ status, _, body = _fetch(self.srv.url + "../outside.html")
155
+ self.assertIn(status, (400, 404))
156
+ # even encoded tricks stay inside the root or fail
157
+ status, _, _ = _fetch(self.srv.url + "assets/../../secret.txt")
158
+ self.assertIn(status, (200, 400, 404))
159
+
160
+ def test_missing_file_404(self):
161
+ status, _, _ = _fetch(self.srv.url + "nope.html")
162
+ self.assertEqual(status, 404)
163
+
164
+ def test_stop_is_idempotent_and_cleans_up(self):
165
+ srv = LiveServer(self.root)
166
+ self.assertTrue(srv.start())
167
+ self.assertTrue(srv.wait_ready())
168
+ port = srv.port
169
+ srv.stop()
170
+ srv.stop() # second call must not raise
171
+ self.assertIsNone(srv.port)
172
+ time.sleep(0.1)
173
+ with self.assertRaises(OSError):
174
+ urllib.request.urlopen(f"http://127.0.0.1:{port}/",
175
+ timeout=2).read()
176
+
177
+
178
+ class TestNavigationHistory(unittest.TestCase):
179
+ def test_back_forward(self):
180
+ h = NavigationHistory()
181
+ self.assertEqual(h.push("a"), "a")
182
+ self.assertIsNotNone(h.push("b"))
183
+ self.assertIsNotNone(h.push("c"))
184
+ self.assertEqual(h.current, "c")
185
+ self.assertEqual(h.back(), "b")
186
+ self.assertEqual(h.back(), "a")
187
+ self.assertIsNone(h.back())
188
+ self.assertEqual(h.forward(), "b")
189
+ self.assertTrue(h.can_forward()) # 'c' is still ahead
190
+ self.assertEqual(h.forward(), "c")
191
+ self.assertFalse(h.can_forward())
192
+
193
+ def test_push_truncates_forward_branch(self):
194
+ h = NavigationHistory()
195
+ for u in ("a", "b", "c"):
196
+ h.push(u)
197
+ h.back()
198
+ h.back() # now at "a", forward branch [b, c]
199
+ self.assertEqual(h.push("d"), "d") # branch truncated, d appended
200
+ self.assertEqual(h.current, "d")
201
+ self.assertFalse(h.can_forward())
202
+ self.assertEqual(h.back(), "a")
203
+ # 'd' remains reachable forward (standard browser semantics);
204
+ # what must be GONE is the old fork [b, c]:
205
+ self.assertEqual(h.forward(), "d")
206
+ self.assertNotIn("b", h._entries)
207
+ self.assertNotIn("c", h._entries)
208
+
209
+ def test_duplicate_push_is_noop(self):
210
+ h = NavigationHistory()
211
+ h.push("a")
212
+ self.assertIsNone(h.push("a")) # reload ≠ history entry
213
+ self.assertEqual(len(h), 1)
214
+
215
+
216
+ class TestEntryDetection(unittest.TestCase):
217
+ def setUp(self):
218
+ self.root = tempfile.mkdtemp(prefix="cat_entry_")
219
+ os.makedirs(os.path.join(self.root, "docs", "site"))
220
+
221
+ def test_open_html_file_is_entry(self):
222
+ p = os.path.join(self.root, "page.html")
223
+ open(p, "w").close()
224
+ entry, web_root = find_entry_file(p, self.root)
225
+ self.assertEqual(entry, p)
226
+
227
+ def test_sibling_index_preferred(self):
228
+ idx = os.path.join(self.root, "index.html")
229
+ open(idx, "w").close()
230
+ other = os.path.join(self.root, "about.html")
231
+ open(other, "w").close()
232
+ entry, web_root = find_entry_file(other, self.root)
233
+ self.assertEqual(entry, idx)
234
+ self.assertEqual(web_root, self.root)
235
+
236
+ def test_nested_site_found(self):
237
+ nested = os.path.join(self.root, "docs", "site", "index.html")
238
+ open(nested, "w").close()
239
+ entry, web_root = find_entry_file(None, self.root)
240
+ self.assertEqual(entry, nested)
241
+ self.assertEqual(web_root, os.path.dirname(nested))
242
+
243
+ def test_none_when_no_html(self):
244
+ entry, web_root = find_entry_file(None, self.root)
245
+ self.assertIsNone(entry)
246
+
247
+ def test_relative_urls(self):
248
+ self.assertEqual(relative_url_for(
249
+ os.path.join(self.root, "a", "b.html"), self.root), "a/b.html")
250
+ outside = os.path.abspath(os.path.join(self.root, "..", "x.html"))
251
+ self.assertIsNone(relative_url_for(outside, self.root))
252
+ self.assertIsNone(url_for_file(outside, "http://127.0.0.1:1/",
253
+ self.root))
254
+
255
+
256
+ class TestWatcherFilteringAndDebounce(unittest.TestCase):
257
+ def test_web_file_classification(self):
258
+ for name in ("a.html", "b.htm", "c.css", "d.js", "e.mjs", "f.svg",
259
+ "g.png", "h.woff2"):
260
+ self.assertTrue(is_web_file(name), name)
261
+ for name in ("x.py", "y.md", "z.bin", "noext"):
262
+ self.assertFalse(is_web_file(name), name)
263
+
264
+ def test_debouncer_coalesces_bursts(self):
265
+ done = threading.Event()
266
+ runs = {"n": 0}
267
+
268
+ def fn():
269
+ runs["n"] += 1
270
+ done.set()
271
+
272
+ d = Debouncer(delay=0.15)
273
+ for _ in range(10):
274
+ d.schedule(fn) # rapid rescheduling = one trailing run
275
+ time.sleep(0.02)
276
+ self.assertTrue(done.wait(3))
277
+ time.sleep(0.25)
278
+ self.assertEqual(runs["n"], 1)
279
+
280
+
281
+ class TestControllerLifecycle(unittest.TestCase):
282
+ def setUp(self):
283
+ self.root = tempfile.mkdtemp(prefix="cat_ctrl_")
284
+ make_site(self.root)
285
+ self.engine = FakeEngine()
286
+ self.events = []
287
+ self.ctrl = PreviewController(
288
+ self.root, engine=self.engine,
289
+ on_event=lambda kind, **kw: self.events.append((kind, kw)))
290
+
291
+ def tearDown(self):
292
+ self.ctrl.stop_preview()
293
+
294
+ def test_start_stop_state_machine(self):
295
+ self.assertEqual(self.ctrl.preview_state, PreviewState.STOPPED)
296
+ ok = self.ctrl.start_for_file(os.path.join(self.root, "index.html"))
297
+ self.assertTrue(ok)
298
+ self.assertEqual(self.ctrl.preview_state, PreviewState.RUNNING)
299
+ self.assertEqual(self.ctrl.server_state, ServerState.RUNNING)
300
+ self.assertTrue(self.ctrl.running)
301
+ self.assertTrue(self.ctrl.url.endswith("/index.html"))
302
+ self.ctrl.stop_preview()
303
+ self.assertEqual(self.ctrl.preview_state, PreviewState.STOPPED)
304
+ self.assertEqual(self.ctrl.server_state, ServerState.STOPPED)
305
+ self.assertTrue(self.engine.closed)
306
+
307
+ def test_states_are_enums_not_booleans(self):
308
+ """Spec section 22: named state enums exist and are used."""
309
+ self.assertEqual(WorkspaceMode.CODE.value, "code")
310
+ self.assertEqual(PaneMode.FULLSCREEN.value, "fullscreen")
311
+ self.assertEqual(EditorState.HAS_TABS.value, "has_tabs")
312
+
313
+ def test_css_only_change_takes_hot_path(self):
314
+ self.ctrl.start_for_file(os.path.join(self.root, "index.html"))
315
+ css = os.path.join(self.root, "style.css")
316
+ reloads_before = self.engine.reload_calls
317
+ self.ctrl.on_files_changed([css])
318
+ self.assertEqual(self.engine.reload_calls, reloads_before,
319
+ "css-only change must NOT full-reload")
320
+ self.assertEqual(self.engine.hot_calls, [["style.css"]])
321
+
322
+ def test_js_change_full_reloads(self):
323
+ self.ctrl.start_for_file(os.path.join(self.root, "index.html"))
324
+ js = os.path.join(self.root, "app.js")
325
+ self.ctrl.on_files_changed([js])
326
+ self.assertEqual(self.engine.reload_calls, 1)
327
+
328
+ def test_ai_write_notifications_are_debounced(self):
329
+ self.ctrl.start_for_file(os.path.join(root := self.root,
330
+ "index.html"))
331
+ html = os.path.join(self.root, "index.html")
332
+ reloads_before = self.engine.reload_calls
333
+ for i in range(12): # burst of AI writes
334
+ self.ctrl.notify_ai_wrote(html)
335
+ time.sleep(0.01)
336
+ time.sleep(0.6) # > debounce window
337
+ reloaded = self.engine.reload_calls - reloads_before
338
+ self.assertLessEqual(reloaded, 2,
339
+ f"12 writes caused {reloaded} reloads")
340
+
341
+ def test_error_page_does_not_kill_stack(self):
342
+ self.engine.ok = False
343
+ self.ctrl.start_for_file(os.path.join(self.root, "index.html"))
344
+ # page-level failure → PREVIEW ERROR but server still RUNNING
345
+ self.assertEqual(self.ctrl.preview_state, PreviewState.ERROR)
346
+ self.assertEqual(self.ctrl.server_state, ServerState.RUNNING)
347
+ # and recovery works
348
+ self.engine.ok = True
349
+ snap = self.ctrl.reload()
350
+ self.assertEqual(self.ctrl.preview_state, PreviewState.RUNNING)
351
+
352
+ def test_stop_preview_idempotent(self):
353
+ self.ctrl.start_for_file(os.path.join(self.root, "index.html"))
354
+ self.ctrl.stop_preview()
355
+ self.ctrl.stop_preview() # must not raise
356
+ self.assertEqual(self.ctrl.preview_state, PreviewState.STOPPED)
357
+
358
+ def test_non_web_notify_is_noop(self):
359
+ self.ctrl.start_for_file(os.path.join(self.root, "index.html"))
360
+ reloads_before = self.engine.reload_calls
361
+ self.ctrl.notify_ai_wrote(os.path.join(self.root, "notes.md"))
362
+ self.ctrl.notify_ai_wrote(None)
363
+ time.sleep(0.4)
364
+ self.assertEqual(self.engine.reload_calls, reloads_before)
365
+
366
+
367
+ if __name__ == "__main__":
368
+ unittest.main(verbosity=1)