Skip to content

Commit 899307d

Browse files
ci: fix core-floor + full-stack test failures unblocked by lint fix
- core floor: @skip_no_stack the two stack-dependent autosync tests (test_sync_auto_toggle_*, test_run_once_runs_*) that import inspector.auth / routes.v2_api and fail with ModuleNotFoundError(fastapi) on the numpy-only gate. The pure policy tests still run on core floor; full-stack jobs run the rest. - dashboard_v2: test_import_folder_route_rejects_path_outside_roots hardcoded tempfile.TemporaryDirectory(dir='C:\'), which is FileNotFoundError on Linux CI. Use C:\ on Windows, /tmp on POSIX so the 'outside the import roots' dir exists. - sync: loads_strict relied on the JSON scanner raising RecursionError for deeply nested input, but Python 3.12 parses ~1000-deep input without raising, so the deep-nesting DoS guard (and its test) broke on 3.12. Add an explicit _scan_depth guard (_MAX_BUNDLE_DEPTH=200) so pathological bundles are rejected as ValueError on every Python version, not just where the scanner recurses. ruff clean; affected suites pass (test_sync/autosync/dashboard_v2: 65 tests).
1 parent 52d0952 commit 899307d

3 files changed

Lines changed: 44 additions & 2 deletions

File tree

engraphis/core/sync.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,14 +264,50 @@ def _reject_nonfinite(token: str):
264264
raise ValueError("non-finite JSON constant: %s" % token)
265265

266266

267+
_MAX_BUNDLE_DEPTH = 200 # generous; real bundles are shallow. Explicit DoS guard so
268+
# deeply-nested input is rejected on every Python version (3.12+'s JSON scanner no
269+
# longer raises RecursionError for ~1000-deep input, so we can't rely on that alone).
270+
271+
272+
def _scan_depth(s: str) -> int:
273+
"""Cheap max-nesting-depth scan that skips JSON string literals; used to reject
274+
pathologically deep bundles without relying on the JSON scanner's RecursionError."""
275+
depth = 0
276+
max_depth = 0
277+
in_str = False
278+
esc = False
279+
for ch in s:
280+
if in_str:
281+
if esc:
282+
esc = False
283+
elif ch == "\\":
284+
esc = True
285+
elif ch == '"':
286+
in_str = False
287+
continue
288+
if ch == '"':
289+
in_str = True
290+
elif ch in "[{":
291+
depth += 1
292+
if depth > max_depth:
293+
max_depth = depth
294+
elif ch in "]}":
295+
if depth > 0:
296+
depth -= 1
297+
return max_depth
298+
299+
267300
def loads_strict(data: bytes):
268301
"""Parse untrusted bundle bytes, rejecting the non-standard ``Infinity``/``NaN``
269302
tokens Python's ``json`` accepts by default (they later raise ``OverflowError`` in
270303
``int()`` and would otherwise abort the whole sync run). Deeply-nested input that
271304
would raise ``RecursionError`` in the JSON scanner is normalized to ``ValueError``
272305
so a single hostile bundle can't crash the whole sync run (DoS)."""
306+
text = data.decode("utf-8")
307+
if _scan_depth(text) > _MAX_BUNDLE_DEPTH:
308+
raise ValueError("bundle JSON is nested too deeply")
273309
try:
274-
return json.loads(data.decode("utf-8"), parse_constant=_reject_nonfinite)
310+
return json.loads(text, parse_constant=_reject_nonfinite)
275311
except RecursionError:
276312
raise ValueError("bundle JSON is nested too deeply")
277313

tests/test_autosync.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ def test_five_minute_floor_cannot_be_beaten():
6767
assert autosync.normalize_policy({"cadence_minutes": v})["cadence_minutes"] >= 5
6868

6969

70+
@skip_no_stack
7071
def test_sync_auto_toggle_is_admin_only_but_members_still_write():
7172
from engraphis.inspector.auth import min_role
7273
# Changing team auto-sync is admin-only; reading it stays viewer-visible.
@@ -105,6 +106,7 @@ def test_run_once_skips_without_license(tmp_path, monkeypatch):
105106
assert autosync.run_once(None).get("skipped") == "unlicensed"
106107

107108

109+
@skip_no_stack
108110
def test_run_once_runs_records_and_updates_button_state(tmp_path, monkeypatch):
109111
from engraphis import licensing as lic
110112
from engraphis.config import settings

tests/test_dashboard_v2.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -614,8 +614,12 @@ def test_import_folder_route_rejects_path_outside_roots(monkeypatch, tmp_path):
614614
This guards against path-traversal / symlink escapes.
615615
"""
616616
import os
617+
import sys
617618
import tempfile
618-
with tempfile.TemporaryDirectory(dir="C:\\") as td:
619+
# A directory genuinely OUTSIDE the import roots (HOME): C:\ is outside
620+
# %USERPROFILE% on Windows; /tmp is outside /home on POSIX CI runners.
621+
base = "C:\\" if sys.platform == "win32" else "/tmp"
622+
with tempfile.TemporaryDirectory(dir=base) as td:
619623
outside = td
620624
os.makedirs(outside, exist_ok=True)
621625
test_file = os.path.join(outside, "test.md")

0 commit comments

Comments
 (0)