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.
- calc_terminal/__init__.py +14 -0
- calc_terminal/__main__.py +14 -0
- calc_terminal/activity.py +1334 -0
- calc_terminal/agent.py +3387 -0
- calc_terminal/agent_runtime.py +519 -0
- calc_terminal/ai_context.py +447 -0
- calc_terminal/ai_modes.py +752 -0
- calc_terminal/ai_personalization.py +286 -0
- calc_terminal/ai_preview_feedback.py +213 -0
- calc_terminal/aicore.py +2572 -0
- calc_terminal/anim.py +367 -0
- calc_terminal/app.py +3685 -0
- calc_terminal/art.py +639 -0
- calc_terminal/atomsim.py +368 -0
- calc_terminal/attachments.py +743 -0
- calc_terminal/benchmark_system.py +414 -0
- calc_terminal/browser/__init__.py +36 -0
- calc_terminal/browser/browser_state.py +346 -0
- calc_terminal/browser/devserver.py +176 -0
- calc_terminal/browser/engine.py +494 -0
- calc_terminal/browser/navigation.py +84 -0
- calc_terminal/browser/preview.py +429 -0
- calc_terminal/browser/preview_entry.py +95 -0
- calc_terminal/browser/project_detector.py +144 -0
- calc_terminal/browser/server.py +449 -0
- calc_terminal/browser/state.py +75 -0
- calc_terminal/browser/watcher.py +99 -0
- calc_terminal/browser_gui/__init__.py +1 -0
- calc_terminal/browser_gui/__main__.py +3 -0
- calc_terminal/browser_gui/launcher.py +173 -0
- calc_terminal/browser_gui/playwright_browser.py +117 -0
- calc_terminal/browser_gui/qt_browser.py +1501 -0
- calc_terminal/browser_gui/webview_browser.py +57 -0
- calc_terminal/capabilities/__init__.py +35 -0
- calc_terminal/capabilities/adapters/__init__.py +33 -0
- calc_terminal/capabilities/adapters/bioinformatics.py +204 -0
- calc_terminal/capabilities/adapters/browser_adapter.py +205 -0
- calc_terminal/capabilities/adapters/filesystem.py +206 -0
- calc_terminal/capabilities/adapters/git_adapter.py +202 -0
- calc_terminal/capabilities/adapters/jupyter_adapter.py +138 -0
- calc_terminal/capabilities/adapters/ml_frameworks.py +158 -0
- calc_terminal/capabilities/adapters/platforms.py +200 -0
- calc_terminal/capabilities/adapters/python_exec.py +93 -0
- calc_terminal/capabilities/adapters/quantum_adapter.py +150 -0
- calc_terminal/capabilities/adapters/scientific_comp.py +123 -0
- calc_terminal/capabilities/adapters/structural_bio.py +161 -0
- calc_terminal/capabilities/adapters/terminal.py +99 -0
- calc_terminal/capabilities/bus.py +178 -0
- calc_terminal/capabilities/discovery.py +207 -0
- calc_terminal/capabilities/schema.py +221 -0
- calc_terminal/cat.ico +0 -0
- calc_terminal/cat_browser.py +2018 -0
- calc_terminal/chat_store.py +703 -0
- calc_terminal/cli.py +1178 -0
- calc_terminal/code_editor.py +640 -0
- calc_terminal/collaboration.py +723 -0
- calc_terminal/commands_data.py +139 -0
- calc_terminal/compatibility_engine.py +352 -0
- calc_terminal/compute/__init__.py +31 -0
- calc_terminal/compute/fabric.py +350 -0
- calc_terminal/config.py +227 -0
- calc_terminal/core/__init__.py +41 -0
- calc_terminal/core/checkpoint.py +156 -0
- calc_terminal/core/input/__init__.py +45 -0
- calc_terminal/core/mode_registry.py +300 -0
- calc_terminal/core/project_graph.py +172 -0
- calc_terminal/core/recovery.py +129 -0
- calc_terminal/core/security_layer.py +112 -0
- calc_terminal/core/task_graph.py +202 -0
- calc_terminal/core/unified_runtime.py +184 -0
- calc_terminal/core/verification.py +257 -0
- calc_terminal/customization.py +1566 -0
- calc_terminal/derivations.py +153 -0
- calc_terminal/device_control.py +263 -0
- calc_terminal/diagnostics/__init__.py +27 -0
- calc_terminal/diagnostics/doctor_engine.py +382 -0
- calc_terminal/diagnostics/self_test.py +247 -0
- calc_terminal/doctor.py +519 -0
- calc_terminal/easter_eggs.py +274 -0
- calc_terminal/editor/__init__.py +1 -0
- calc_terminal/editor/actions.py +263 -0
- calc_terminal/editor/commands.py +160 -0
- calc_terminal/editor/shortcuts.py +226 -0
- calc_terminal/engine.py +259 -0
- calc_terminal/errors.py +120 -0
- calc_terminal/event_stream.py +146 -0
- calc_terminal/eventbus.py +133 -0
- calc_terminal/extensions.py +733 -0
- calc_terminal/fallback_cli.py +1321 -0
- calc_terminal/first_run.py +265 -0
- calc_terminal/fomoji_auth.py +1043 -0
- calc_terminal/formulas.py +82 -0
- calc_terminal/fs_cache.py +121 -0
- calc_terminal/fs_watcher.py +277 -0
- calc_terminal/game.py +193 -0
- calc_terminal/gen1.py +5 -0
- calc_terminal/generators.py +245 -0
- calc_terminal/gestures/__init__.py +42 -0
- calc_terminal/gestures/bindings.py +175 -0
- calc_terminal/gestures/manager.py +477 -0
- calc_terminal/goodbye.py +363 -0
- calc_terminal/gpu3d.py +290 -0
- calc_terminal/graphs.py +358 -0
- calc_terminal/hardware_analyzer.py +440 -0
- calc_terminal/host/__init__.py +30 -0
- calc_terminal/host/browser_manager.py +187 -0
- calc_terminal/host/desktop.py +1386 -0
- calc_terminal/host/launcher.py +395 -0
- calc_terminal/host/terminal.py +279 -0
- calc_terminal/identity.py +216 -0
- calc_terminal/input/__init__.py +54 -0
- calc_terminal/input/capabilities.py +258 -0
- calc_terminal/input/focus.py +87 -0
- calc_terminal/input/gestures.py +64 -0
- calc_terminal/input/pointer.py +114 -0
- calc_terminal/input/touch.py +345 -0
- calc_terminal/keys.py +84 -0
- calc_terminal/live_automation.py +165 -0
- calc_terminal/mathtext.py +433 -0
- calc_terminal/mcp.py +386 -0
- calc_terminal/memory.py +337 -0
- calc_terminal/memory_v2.py +479 -0
- calc_terminal/metrics.py +333 -0
- calc_terminal/mode_detection.py +146 -0
- calc_terminal/model.py +2431 -0
- calc_terminal/model_router.py +665 -0
- calc_terminal/models/__init__.py +0 -0
- calc_terminal/models/active_state.py +187 -0
- calc_terminal/models/dynamic_registry.py +584 -0
- calc_terminal/models/manager.py +781 -0
- calc_terminal/models/model_metadata.json +3526 -0
- calc_terminal/models/profiles.py +194 -0
- calc_terminal/models/registry.py +265 -0
- calc_terminal/models/schema.py +197 -0
- calc_terminal/models/validator.py +287 -0
- calc_terminal/models/verification_engine.py +368 -0
- calc_terminal/native_picker.py +215 -0
- calc_terminal/ollama_catalog.py +279 -0
- calc_terminal/ollama_download.py +233 -0
- calc_terminal/orchestrator.py +304 -0
- calc_terminal/package_research.py +322 -0
- calc_terminal/packages.py +1024 -0
- calc_terminal/pc_specs.py +116 -0
- calc_terminal/permissions.py +334 -0
- calc_terminal/pet.py +106 -0
- calc_terminal/pipeline.py +505 -0
- calc_terminal/platform/__init__.py +491 -0
- calc_terminal/platform/desktop.py +491 -0
- calc_terminal/platform/web.py +781 -0
- calc_terminal/preview/__init__.py +1 -0
- calc_terminal/preview/dev_server.py +303 -0
- calc_terminal/preview/diagnostics.py +131 -0
- calc_terminal/preview/live_reload.py +66 -0
- calc_terminal/preview/manager.py +129 -0
- calc_terminal/project_stats.py +209 -0
- calc_terminal/projects.py +328 -0
- calc_terminal/providers/__init__.py +0 -0
- calc_terminal/providers/adapters/__init__.py +80 -0
- calc_terminal/providers/adapters/anthropic_adapter.py +127 -0
- calc_terminal/providers/adapters/base.py +106 -0
- calc_terminal/providers/adapters/chinese_adapters.py +420 -0
- calc_terminal/providers/adapters/gemini_adapter.py +101 -0
- calc_terminal/providers/adapters/ollama_adapter.py +83 -0
- calc_terminal/providers/adapters/openai_adapter.py +159 -0
- calc_terminal/providers/adapters/other_adapters.py +246 -0
- calc_terminal/providers/anthropic_provider.py +172 -0
- calc_terminal/providers/auto_update.py +416 -0
- calc_terminal/providers/base_provider.py +105 -0
- calc_terminal/providers/discovery_manager.py +207 -0
- calc_terminal/providers/gemini_provider.py +178 -0
- calc_terminal/providers/lifecycle.py +767 -0
- calc_terminal/providers/ollama_adapter.py +707 -0
- calc_terminal/providers/openai_provider.py +254 -0
- calc_terminal/providers/provider_manager.py +1827 -0
- calc_terminal/providers/providers.json +4075 -0
- calc_terminal/reactionsim.py +279 -0
- calc_terminal/registry.py +337 -0
- calc_terminal/report.py +162 -0
- calc_terminal/research/__init__.py +45 -0
- calc_terminal/research/artifact_intel.py +126 -0
- calc_terminal/research/data_lineage.py +123 -0
- calc_terminal/research/experiment_ledger.py +303 -0
- calc_terminal/research/reproducibility.py +131 -0
- calc_terminal/resilience/__init__.py +47 -0
- calc_terminal/resilience/agent_state.py +121 -0
- calc_terminal/resilience/capability_matcher.py +174 -0
- calc_terminal/resilience/circuit_breaker.py +158 -0
- calc_terminal/resilience/failover_engine.py +230 -0
- calc_terminal/resilience/health_monitor.py +192 -0
- calc_terminal/resilience/ollama_adapter.py +125 -0
- calc_terminal/resilience/orchestrator.py +312 -0
- calc_terminal/resilience/types.py +134 -0
- calc_terminal/sandbox.py +98 -0
- calc_terminal/scires.py +558 -0
- calc_terminal/security_scanner.py +126 -0
- calc_terminal/session.py +294 -0
- calc_terminal/sim3d.py +206 -0
- calc_terminal/solver.py +276 -0
- calc_terminal/sound.py +127 -0
- calc_terminal/task_reports.py +287 -0
- calc_terminal/terminal_host.py +201 -0
- calc_terminal/terminal_identity.py +411 -0
- calc_terminal/test_ai_mode_reliability.py +344 -0
- calc_terminal/test_browser.py +368 -0
- calc_terminal/test_code_editor_upgrade.py +485 -0
- calc_terminal/test_customization.py +1148 -0
- calc_terminal/test_customization_ui.py +612 -0
- calc_terminal/test_dynamic_registry.py +304 -0
- calc_terminal/test_extensions.py +436 -0
- calc_terminal/test_overhaul.py +557 -0
- calc_terminal/test_project_detect.py +255 -0
- calc_terminal/test_root_cause_fix.py +527 -0
- calc_terminal/test_stability.py +532 -0
- calc_terminal/test_terminal_identity.py +132 -0
- calc_terminal/test_v079_speed.py +460 -0
- calc_terminal/theme.py +1107 -0
- calc_terminal/timeline.py +139 -0
- calc_terminal/todos.py +246 -0
- calc_terminal/tool_call_normalizer.py +419 -0
- calc_terminal/tui.py +104 -0
- calc_terminal/ui/__init__.py +8 -0
- calc_terminal/ui/activity_panel.py +231 -0
- calc_terminal/ui/activity_stream_panel.py +238 -0
- calc_terminal/ui/animations.py +122 -0
- calc_terminal/ui/app.py +7271 -0
- calc_terminal/ui/attach_panel.py +597 -0
- calc_terminal/ui/attachments.py +424 -0
- calc_terminal/ui/backup_panel.py +810 -0
- calc_terminal/ui/browser_shell.py +887 -0
- calc_terminal/ui/cat_agent.py +357 -0
- calc_terminal/ui/chats_panel.py +899 -0
- calc_terminal/ui/command_palette.py +125 -0
- calc_terminal/ui/command_palette_modal.py +166 -0
- calc_terminal/ui/composer.py +1141 -0
- calc_terminal/ui/context_menu.py +197 -0
- calc_terminal/ui/conversation.py +1435 -0
- calc_terminal/ui/customization_panel.py +1229 -0
- calc_terminal/ui/dashboard.py +404 -0
- calc_terminal/ui/design_system.py +557 -0
- calc_terminal/ui/diff_panel.py +213 -0
- calc_terminal/ui/editor.py +2102 -0
- calc_terminal/ui/empty_state.py +302 -0
- calc_terminal/ui/events.py +487 -0
- calc_terminal/ui/extensions_panel.py +815 -0
- calc_terminal/ui/footer.py +166 -0
- calc_terminal/ui/gestures_panel.py +383 -0
- calc_terminal/ui/goodbye_screen.py +100 -0
- calc_terminal/ui/header.py +1034 -0
- calc_terminal/ui/help_panel.py +254 -0
- calc_terminal/ui/live_activities.py +914 -0
- calc_terminal/ui/mcp_panel.py +570 -0
- calc_terminal/ui/memory_center.py +524 -0
- calc_terminal/ui/mode_colors_panel.py +525 -0
- calc_terminal/ui/nav_screens.py +747 -0
- calc_terminal/ui/ollama_panel.py +536 -0
- calc_terminal/ui/palette.py +221 -0
- calc_terminal/ui/permission_panel.py +269 -0
- calc_terminal/ui/personalization_panel.py +517 -0
- calc_terminal/ui/personalize_center.py +1568 -0
- calc_terminal/ui/preview_panel.py +441 -0
- calc_terminal/ui/resizers.py +402 -0
- calc_terminal/ui/sidebar.py +1285 -0
- calc_terminal/ui/statusbar.py +168 -0
- calc_terminal/ui/theme_css.py +1396 -0
- calc_terminal/ui/thinking.py +226 -0
- calc_terminal/ui/timeline_panel.py +102 -0
- calc_terminal/ui/todo_panel.py +193 -0
- calc_terminal/ui/viewport.py +136 -0
- calc_terminal/ui/vision_panel.py +489 -0
- calc_terminal/ui/welcome_modal.py +343 -0
- calc_terminal/ui/widgets.py +160 -0
- calc_terminal/ui/workspace.py +831 -0
- calc_terminal/viewers/__init__.py +1 -0
- calc_terminal/viewers/document_viewer.py +252 -0
- calc_terminal/viewers/image_viewer.py +241 -0
- calc_terminal/viewers/pdf_viewer.py +203 -0
- calc_terminal/viewers/presentation_viewer.py +164 -0
- calc_terminal/viewers/registry.py +120 -0
- calc_terminal/viewers/spreadsheet_viewer.py +204 -0
- calc_terminal/vision/__init__.py +89 -0
- calc_terminal/vision/analysis.py +194 -0
- calc_terminal/vision/annotations.py +297 -0
- calc_terminal/vision/capture.py +171 -0
- calc_terminal/vision/context.py +231 -0
- calc_terminal/vision/correlation.py +169 -0
- calc_terminal/vision/cursor.py +258 -0
- calc_terminal/vision/events.py +66 -0
- calc_terminal/vision/frame_pipeline.py +259 -0
- calc_terminal/vision/priority.py +218 -0
- calc_terminal/vision/provider.py +180 -0
- calc_terminal/vision/safety.py +149 -0
- calc_terminal/vision/session.py +281 -0
- calc_terminal/vision/verify.py +162 -0
- calc_terminal/vision.py +514 -0
- calc_terminal/vscode_integration.py +113 -0
- calc_terminal/web/__init__.py +8 -0
- calc_terminal/web/cat_runtime.py +710 -0
- calc_terminal/web/server.py +2891 -0
- calc_terminal/web/static/css/app.css +3152 -0
- calc_terminal/web/static/icons/badge-72.png +0 -0
- calc_terminal/web/static/icons/cat.ico +0 -0
- calc_terminal/web/static/icons/icon-128.png +0 -0
- calc_terminal/web/static/icons/icon-144.png +0 -0
- calc_terminal/web/static/icons/icon-152.png +0 -0
- calc_terminal/web/static/icons/icon-192.png +0 -0
- calc_terminal/web/static/icons/icon-384.png +0 -0
- calc_terminal/web/static/icons/icon-512.png +0 -0
- calc_terminal/web/static/icons/icon-72.png +0 -0
- calc_terminal/web/static/icons/icon-96.png +0 -0
- calc_terminal/web/static/icons/icon.svg +34 -0
- calc_terminal/web/static/icons/new-project.png +0 -0
- calc_terminal/web/static/icons/open-project.png +0 -0
- calc_terminal/web/static/index.html +734 -0
- calc_terminal/web/static/js/app.js +2403 -0
- calc_terminal/web/static/manifest.json +88 -0
- calc_terminal/web/static/sw.js +230 -0
- calc_terminal/workflow_engine.py +769 -0
- calc_terminal/workspace.py +593 -0
- calc_terminal/workspace_index.py +385 -0
- cct_cli-0.7.9.0.dist-info/METADATA +210 -0
- cct_cli-0.7.9.0.dist-info/RECORD +325 -0
- cct_cli-0.7.9.0.dist-info/WHEEL +5 -0
- cct_cli-0.7.9.0.dist-info/entry_points.txt +4 -0
- cct_cli-0.7.9.0.dist-info/licenses/LICENSE +21 -0
- 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)
|