Steering (submitting mid-stream) dropped any pending composer attachments. Now the pending files are uploaded before the /steer payload is sent, the uploaded file paths are appended to the steer text so the active agent can inspect them with tools, file chips stay staged if the steer fails (so nothing is lost), and file-only busy submissions are handled. Contributor stage; gate-pass. Co-authored-by: ruizanthony <ruizanthony@users.noreply.github.com>
77 lines
2.7 KiB
Python
77 lines
2.7 KiB
Python
from pathlib import Path
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
UI_JS = ROOT / "static" / "ui.js"
|
|
I18N_JS = ROOT / "static" / "i18n.js"
|
|
CONFIG_PY = ROOT / "api" / "config.py"
|
|
UPLOAD_PY = ROOT / "api" / "upload.py"
|
|
|
|
|
|
def _function_body(src: str, name: str) -> str:
|
|
marker = f"function {name}"
|
|
start = src.index(marker)
|
|
signature_end = src.index(")", start)
|
|
brace = src.index("{", signature_end)
|
|
depth = 0
|
|
for idx in range(brace, len(src)):
|
|
if src[idx] == "{":
|
|
depth += 1
|
|
elif src[idx] == "}":
|
|
depth -= 1
|
|
if depth == 0:
|
|
return src[brace : idx + 1]
|
|
raise AssertionError(f"{name} function body not found")
|
|
|
|
|
|
def test_upload_limit_constant_matches_server_limit():
|
|
"""The browser preflight should read the runtime upload limit."""
|
|
ui = UI_JS.read_text(encoding="utf-8")
|
|
config = CONFIG_PY.read_text(encoding="utf-8")
|
|
|
|
assert "window.__HERMES_CONFIG__.maxUploadBytes" in ui
|
|
assert 'MAX_UPLOAD_BYTES = _env_mb_bytes("HERMES_WEBUI_MAX_UPLOAD_MB", 20)' in config
|
|
|
|
|
|
def test_file_picker_rejects_oversize_files_before_queueing():
|
|
"""Selecting an oversized file should never add it to pending uploads."""
|
|
src = UI_JS.read_text(encoding="utf-8")
|
|
body = _function_body(src, "addFiles")
|
|
|
|
size_gate = body.index("f&&f.size>MAX_UPLOAD_BYTES")
|
|
status_notice = body.index("_showUploadTooLarge(f)")
|
|
push_pending = body.index("S.pendingFiles.push(f)")
|
|
|
|
assert size_gate < status_notice < push_pending
|
|
assert "continue;" in body[size_gate:push_pending]
|
|
|
|
|
|
def test_pending_uploads_skip_fetch_for_oversize_files():
|
|
"""Restored or queued oversized files should fail locally before fetch()."""
|
|
src = UI_JS.read_text(encoding="utf-8")
|
|
body = _function_body(src, "uploadPendingFiles")
|
|
|
|
size_gate = body.index("f&&f.size>MAX_UPLOAD_BYTES")
|
|
form_data = body.index("const fd=new FormData()")
|
|
upload_fetch = body.index("fetch(url")
|
|
|
|
assert size_gate < form_data < upload_fetch
|
|
assert "throw new Error(_uploadTooLargeMessage(f))" in body[size_gate:form_data]
|
|
|
|
|
|
def test_upload_too_large_has_user_facing_message():
|
|
"""The status toast should explain the upload limit instead of a network reset."""
|
|
i18n = I18N_JS.read_text(encoding="utf-8")
|
|
ui = UI_JS.read_text(encoding="utf-8")
|
|
|
|
assert "upload_too_large" in i18n
|
|
assert "Maximum upload size is" in i18n
|
|
assert "_uploadTooLargeMessage(file)" in ui
|
|
|
|
|
|
def test_archive_extraction_limit_tracks_upload_limit():
|
|
"""Archive extraction guard should scale with the configured upload limit."""
|
|
upload = UPLOAD_PY.read_text(encoding="utf-8")
|
|
|
|
assert "_MAX_EXTRACTED_BYTES = 10 * MAX_UPLOAD_BYTES" in upload
|