comodor 0.8.1__tar.gz → 0.8.2__tar.gz
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.
- {comodor-0.8.1 → comodor-0.8.2}/PKG-INFO +1 -1
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/_version.py +2 -2
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/ui/app.py +6 -1
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/web/server.py +32 -4
- {comodor-0.8.1 → comodor-0.8.2}/tests/test_run_loop.py +7 -1
- {comodor-0.8.1 → comodor-0.8.2}/tests/test_web.py +86 -0
- {comodor-0.8.1 → comodor-0.8.2}/.gitignore +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/LICENSE +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/README.md +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/pyproject.toml +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/__init__.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/__main__.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/agent/__init__.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/agent/context.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/agent/loop.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/agent/prompts.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/agent/spawn.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/agent/tokens.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/browser/__init__.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/browser/cdp.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/browser/launch.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/browser/page.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/catalogue.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/cli.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/config.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/doctor.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/events.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/learning/__init__.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/learning/associations.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/learning/bm25.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/learning/hotindex.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/learning/memory.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/learning/progress.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/learning/reflect.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/learning/rules.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/learning/signals.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/learning/store.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/learning/writer.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/mcp/__init__.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/mcp/catalogue.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/mcp/commands.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/mcp/http.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/mcp/manager.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/mcp/protocol.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/net/__init__.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/net/http.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/net/sse.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/paths.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/providers/__init__.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/providers/anthropic.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/providers/base.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/providers/caching.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/providers/fake.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/providers/gateway.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/providers/openai_compat.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/providers/registry.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/safety/__init__.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/safety/checkpoints.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/safety/permissions.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/safety/redact.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/session/__init__.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/session/search.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/session/store.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/setup.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/skills/__init__.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/skills/catalogue.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/skills/commands.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/skills/examples.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/skills/loader.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/skills/propose.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/skills/registry.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/tools/__init__.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/tools/base.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/tools/browse.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/tools/browser.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/tools/delegate.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/tools/fs.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/tools/history.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/tools/mcp.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/tools/overflow.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/tools/registry.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/tools/search.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/tools/shell.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/tools/skills.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/tools/todo.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/tools/web.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/ui/__init__.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/ui/bidi.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/ui/chooser.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/ui/console.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/ui/input/__init__.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/ui/input/keys.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/ui/input/reader.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/ui/layout.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/ui/markdown.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/ui/screen.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/ui/theme.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/ui/widgets/__init__.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/ui/widgets/buttons.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/ui/widgets/chat.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/ui/widgets/history.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/ui/widgets/overlay.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/ui/widgets/panel.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/ui/widgets/progress.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/ui/widgets/prompt.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/ui/widgets/statusbar.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/ui/widgets/toast.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/uninstall.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/update.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/web/__init__.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/web/commands.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/web/index.html +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/web/session.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/src/comodor/workspace.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/tests/conftest.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/tests/support/fake_mcp_server.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/tests/test_agent_loop.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/tests/test_app.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/tests/test_associations.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/tests/test_bidi.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/tests/test_browser.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/tests/test_caching.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/tests/test_chooser.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/tests/test_delegate.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/tests/test_doctor.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/tests/test_history.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/tests/test_input.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/tests/test_layout.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/tests/test_learning.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/tests/test_markdown.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/tests/test_mcp.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/tests/test_mcp_http.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/tests/test_overflow.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/tests/test_performance.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/tests/test_prefix_stability.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/tests/test_progress.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/tests/test_propose.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/tests/test_providers.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/tests/test_real_browser.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/tests/test_reflex.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/tests/test_setup.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/tests/test_skill_budget.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/tests/test_skill_catalogue.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/tests/test_skills.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/tests/test_tools.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/tests/test_uninstall.py +0 -0
- {comodor-0.8.1 → comodor-0.8.2}/tests/test_update.py +0 -0
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.8.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 8,
|
|
21
|
+
__version__ = version = '0.8.2'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 8, 2)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -46,6 +46,11 @@ from .widgets.history import SessionRef
|
|
|
46
46
|
from .widgets.overlay import info_overlay, permission_overlay, select_overlay
|
|
47
47
|
from .widgets.statusbar import StatusModel
|
|
48
48
|
|
|
49
|
+
#: How long quitting waits for a turn in flight. Quitting mid-task should give
|
|
50
|
+
#: the agent a moment to stop rather than abandoning it, so shutting down can
|
|
51
|
+
#: legitimately take this long — which is what anything timing the exit has to
|
|
52
|
+
#: allow for.
|
|
53
|
+
SHUTDOWN_JOIN_SECONDS = 2.0
|
|
49
54
|
IDLE_SLEEP = 0.02
|
|
50
55
|
SPINNER_INTERVAL = 0.1
|
|
51
56
|
# How long the prompt must sit still before recall is warmed for the draft.
|
|
@@ -199,7 +204,7 @@ class App:
|
|
|
199
204
|
if self.mcp is not None:
|
|
200
205
|
self.mcp.close()
|
|
201
206
|
if self.worker and self.worker.is_alive():
|
|
202
|
-
self.worker.join(timeout=
|
|
207
|
+
self.worker.join(timeout=SHUTDOWN_JOIN_SECONDS)
|
|
203
208
|
# Flush whatever the agent produced after the last event we pumped —
|
|
204
209
|
# quitting mid-turn must not lose the transcript.
|
|
205
210
|
self._persist_new_messages()
|
|
@@ -140,20 +140,43 @@ def _handler_for(server: Server) -> type[BaseHTTPRequestHandler]:
|
|
|
140
140
|
return ""
|
|
141
141
|
|
|
142
142
|
def _body(self) -> dict[str, Any]:
|
|
143
|
+
"""Read the body, whatever is going to be done with it.
|
|
144
|
+
|
|
145
|
+
Always read, and read all of it. Bytes left in the socket are read
|
|
146
|
+
by the *next* request on the connection as its request line, and a
|
|
147
|
+
socket closed with unread data sends a reset instead of a close —
|
|
148
|
+
which the client sees as a connection aborted rather than as the
|
|
149
|
+
401 that was actually sent.
|
|
150
|
+
"""
|
|
143
151
|
try:
|
|
144
152
|
length = int(self.headers.get("Content-Length") or 0)
|
|
145
153
|
except ValueError:
|
|
154
|
+
length = 0
|
|
155
|
+
if length <= 0:
|
|
146
156
|
return {}
|
|
147
|
-
|
|
157
|
+
|
|
158
|
+
raw = b""
|
|
159
|
+
remaining = length
|
|
160
|
+
while remaining > 0:
|
|
161
|
+
chunk = self.rfile.read(min(remaining, 65536))
|
|
162
|
+
if not chunk:
|
|
163
|
+
break
|
|
164
|
+
remaining -= len(chunk)
|
|
165
|
+
# Past the cap it is drained and dropped, not left unread.
|
|
166
|
+
if len(raw) < MAX_BODY:
|
|
167
|
+
raw += chunk
|
|
168
|
+
if length > MAX_BODY:
|
|
148
169
|
return {}
|
|
149
170
|
try:
|
|
150
|
-
return json.loads(
|
|
151
|
-
except
|
|
171
|
+
return json.loads(raw or b"{}")
|
|
172
|
+
except ValueError:
|
|
152
173
|
return {}
|
|
153
174
|
|
|
154
175
|
# -- reading ------------------------------------------------------- #
|
|
155
176
|
|
|
156
177
|
def do_GET(self) -> None:
|
|
178
|
+
if self.headers.get("Content-Length"):
|
|
179
|
+
self._body() # rare, but the same rule applies
|
|
157
180
|
parts = urlparse(self.path)
|
|
158
181
|
query = parse_qs(parts.query)
|
|
159
182
|
route = parts.path
|
|
@@ -202,6 +225,10 @@ def _handler_for(server: Server) -> type[BaseHTTPRequestHandler]:
|
|
|
202
225
|
# -- doing --------------------------------------------------------- #
|
|
203
226
|
|
|
204
227
|
def do_POST(self) -> None:
|
|
228
|
+
# Before any verdict: an unread body breaks the connection it
|
|
229
|
+
# arrived on, and the caller sees a reset instead of the refusal.
|
|
230
|
+
body = self._body()
|
|
231
|
+
|
|
205
232
|
if not server.authorised(self._token()):
|
|
206
233
|
self._json(401, {"error": "unauthorised"})
|
|
207
234
|
return
|
|
@@ -213,7 +240,6 @@ def _handler_for(server: Server) -> type[BaseHTTPRequestHandler]:
|
|
|
213
240
|
return
|
|
214
241
|
|
|
215
242
|
route = urlparse(self.path).path
|
|
216
|
-
body = self._body()
|
|
217
243
|
|
|
218
244
|
if route == "/api/send":
|
|
219
245
|
started = server.session.send(str(body.get("text") or ""))
|
|
@@ -243,6 +269,8 @@ def _handler_for(server: Server) -> type[BaseHTTPRequestHandler]:
|
|
|
243
269
|
def do_OPTIONS(self) -> None:
|
|
244
270
|
# No CORS for anyone. A preflight that gets no permissions means a
|
|
245
271
|
# cross-origin fetch cannot send the header the POST routes want.
|
|
272
|
+
if self.headers.get("Content-Length"):
|
|
273
|
+
self._body()
|
|
246
274
|
self._send(405)
|
|
247
275
|
|
|
248
276
|
return Handler
|
|
@@ -83,7 +83,13 @@ def run_with(app, events, monkeypatch, budget: float = 3.0):
|
|
|
83
83
|
app.run()
|
|
84
84
|
finally:
|
|
85
85
|
watchdog.cancel()
|
|
86
|
-
|
|
86
|
+
# Shutting down is allowed to wait for a turn still in flight, so the
|
|
87
|
+
# exit cannot be quicker than that however fast the loop itself is. The
|
|
88
|
+
# tolerance is that budget plus slack, taken from the source rather than
|
|
89
|
+
# guessed, or the two drift apart and the test fails on a loaded machine
|
|
90
|
+
# for a reason that is not a bug.
|
|
91
|
+
allowed = app_module.SHUTDOWN_JOIN_SECONDS + 1.5
|
|
92
|
+
assert time.monotonic() < deadline + allowed, "the loop did not exit"
|
|
87
93
|
return terminal, live
|
|
88
94
|
|
|
89
95
|
|
|
@@ -373,3 +373,89 @@ def test_an_events_own_fields_cannot_overwrite_its_kind(served):
|
|
|
373
373
|
|
|
374
374
|
assert frames and frames[0]["kind"] == "request"
|
|
375
375
|
assert frames[0]["about"] == "permission"
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
# --------------------------------------------------------------------------- #
|
|
379
|
+
# a refusal must still leave the connection usable
|
|
380
|
+
# --------------------------------------------------------------------------- #
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def test_a_refused_post_leaves_the_connection_clean(served):
|
|
384
|
+
"""The bug this exists for. A verdict reached before the body is read
|
|
385
|
+
leaves those bytes in the socket; the next request on the connection reads
|
|
386
|
+
them as its own request line, and a socket closed with unread data sends a
|
|
387
|
+
reset — which the caller sees as an aborted connection rather than the 401
|
|
388
|
+
that was actually sent."""
|
|
389
|
+
import http.client
|
|
390
|
+
|
|
391
|
+
connection = http.client.HTTPConnection("127.0.0.1", served.port, timeout=15)
|
|
392
|
+
try:
|
|
393
|
+
payload = json.dumps({"text": "x" * 50_000})
|
|
394
|
+
# Rejected for the missing guard header, with a body already in flight.
|
|
395
|
+
connection.request("POST", "/api/send", body=payload,
|
|
396
|
+
headers={"Content-Type": "application/json",
|
|
397
|
+
"Cookie": f"{COOKIE}={served.token}"})
|
|
398
|
+
first = connection.getresponse()
|
|
399
|
+
first.read()
|
|
400
|
+
assert first.status == 403
|
|
401
|
+
|
|
402
|
+
# The same connection, reused. If the body was not drained this reads
|
|
403
|
+
# the leftover as a request line and fails.
|
|
404
|
+
connection.request("GET", "/api/state",
|
|
405
|
+
headers={"Cookie": f"{COOKIE}={served.token}"})
|
|
406
|
+
second = connection.getresponse()
|
|
407
|
+
state = json.loads(second.read())
|
|
408
|
+
|
|
409
|
+
assert second.status == 200
|
|
410
|
+
assert "mode" in state
|
|
411
|
+
finally:
|
|
412
|
+
connection.close()
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def test_an_unauthorised_post_leaves_the_connection_clean(served):
|
|
416
|
+
import http.client
|
|
417
|
+
|
|
418
|
+
connection = http.client.HTTPConnection("127.0.0.1", served.port, timeout=15)
|
|
419
|
+
try:
|
|
420
|
+
connection.request("POST", "/api/send",
|
|
421
|
+
body=json.dumps({"text": "y" * 20_000}),
|
|
422
|
+
headers={"Content-Type": "application/json",
|
|
423
|
+
"X-Comodor": "1"})
|
|
424
|
+
first = connection.getresponse()
|
|
425
|
+
first.read()
|
|
426
|
+
assert first.status == 401
|
|
427
|
+
|
|
428
|
+
connection.request("GET", "/api/state",
|
|
429
|
+
headers={"Cookie": f"{COOKIE}={served.token}"})
|
|
430
|
+
second = connection.getresponse()
|
|
431
|
+
second.read()
|
|
432
|
+
|
|
433
|
+
assert second.status == 200
|
|
434
|
+
finally:
|
|
435
|
+
connection.close()
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def test_a_body_over_the_cap_is_drained_rather_than_left_in_the_socket(served):
|
|
439
|
+
"""Refusing to read is what breaks the connection, so an oversized body is
|
|
440
|
+
read and dropped instead."""
|
|
441
|
+
import http.client
|
|
442
|
+
|
|
443
|
+
from comodor.web.server import MAX_BODY
|
|
444
|
+
|
|
445
|
+
connection = http.client.HTTPConnection("127.0.0.1", served.port, timeout=30)
|
|
446
|
+
try:
|
|
447
|
+
connection.request("POST", "/api/send", body="z" * (MAX_BODY + 5_000),
|
|
448
|
+
headers={"Content-Type": "application/json",
|
|
449
|
+
"Cookie": f"{COOKIE}={served.token}",
|
|
450
|
+
"X-Comodor": "1"})
|
|
451
|
+
first = connection.getresponse()
|
|
452
|
+
first.read()
|
|
453
|
+
|
|
454
|
+
connection.request("GET", "/api/state",
|
|
455
|
+
headers={"Cookie": f"{COOKIE}={served.token}"})
|
|
456
|
+
second = connection.getresponse()
|
|
457
|
+
second.read()
|
|
458
|
+
|
|
459
|
+
assert second.status == 200
|
|
460
|
+
finally:
|
|
461
|
+
connection.close()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|