fix: prevent 404 on /api/session/compress/status during session switch
Two-part fix: - Backend: handle_get returns True (not None from j()) for compress/status route, preventing edge-case 404 fallback in do_GET - Frontend: resumeManualCompressionForSession silently returns on 404 instead of showing "Compression failed: not found" toast Includes 6 regression tests covering backend return value, idle/empty session responses, and frontend 404 guard presence.
This commit is contained in:
@@ -3195,7 +3195,8 @@ def handle_get(handler, parsed) -> bool:
|
||||
|
||||
if parsed.path == "/api/session/compress/status":
|
||||
query = parse_qs(parsed.query)
|
||||
return _handle_session_compress_status(handler, query.get("session_id", [""])[0])
|
||||
_handle_session_compress_status(handler, query.get("session_id", [""])[0])
|
||||
return True
|
||||
|
||||
if parsed.path == "/api/session":
|
||||
import time as _time
|
||||
|
||||
@@ -484,6 +484,9 @@ async function resumeManualCompressionForSession(sid){
|
||||
if(!S.session||S.session.session_id!==sid) return;
|
||||
await _applyManualCompressionResult(done, status.focus_topic||'', visibleCount, status.focus_topic?`/compress ${status.focus_topic}`:'/compress');
|
||||
}catch(e){
|
||||
// No active compression job or transient server error — not a real failure.
|
||||
// 404: route missed or session gone; 5xx: backend exception during status check.
|
||||
if(e&&(!e.status||e.status===404||e.status>=500)) return;
|
||||
if(S.session&&S.session.session_id===sid&&typeof setCompressionUi==='function'){
|
||||
const visibleMessages=_manualCompressionVisibleMessages();
|
||||
setCompressionUi({
|
||||
|
||||
139
tests/test_compress_status_404_fix.py
Normal file
139
tests/test_compress_status_404_fix.py
Normal file
@@ -0,0 +1,139 @@
|
||||
"""
|
||||
Regression test: /api/session/compress/status must not return 404.
|
||||
|
||||
Bug: switching sessions triggered resumeManualCompressionForSession(),
|
||||
which called GET /api/session/compress/status. The route handler returned
|
||||
None (from j()) instead of True, causing do_GET's fallback to emit
|
||||
{"error":"not found"} 404 in edge cases. The frontend then showed
|
||||
"Compression failed: not found" toast on every session switch.
|
||||
|
||||
Fix (two-part):
|
||||
1. Backend: handle_get now returns True after _handle_session_compress_status
|
||||
2. Frontend: resumeManualCompressionForSession catches 404 silently
|
||||
"""
|
||||
|
||||
import io
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from api.routes import _handle_session_compress_status, handle_get
|
||||
from tests._pytest_port import BASE
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Reuse the _FakeHandler pattern from test_sprint46
|
||||
# ---------------------------------------------------------------------------
|
||||
class _FakeHandler:
|
||||
def __init__(self):
|
||||
self.wfile = io.BytesIO()
|
||||
self.status = None
|
||||
self.sent_headers = {}
|
||||
|
||||
def send_response(self, status):
|
||||
self.status = status
|
||||
|
||||
def send_header(self, key, value):
|
||||
self.sent_headers[key] = value
|
||||
|
||||
def end_headers(self):
|
||||
pass
|
||||
|
||||
def payload(self):
|
||||
return json.loads(self.wfile.getvalue().decode("utf-8"))
|
||||
|
||||
|
||||
# ======== Backend tests ========
|
||||
|
||||
|
||||
def test_compress_status_returns_200_idle_for_unknown_session():
|
||||
"""The idle case (no active compression) must return 200, not 404."""
|
||||
handler = _FakeHandler()
|
||||
result = _handle_session_compress_status(handler, "nonexistent_session_xyz")
|
||||
body = handler.payload()
|
||||
|
||||
assert handler.status == 200
|
||||
assert body["ok"] is True
|
||||
assert body["status"] == "idle"
|
||||
|
||||
|
||||
def test_compress_status_returns_200_idle_for_empty_session_id():
|
||||
"""Empty session_id should return 400 (bad), not 404."""
|
||||
handler = _FakeHandler()
|
||||
_handle_session_compress_status(handler, "")
|
||||
assert handler.status == 400
|
||||
|
||||
|
||||
def test_handle_get_returns_true_for_compress_status():
|
||||
"""handle_get must return True (not None/False) for compress/status.
|
||||
|
||||
This is the core fix: previously it returned None (from j()), which
|
||||
only worked because 'None is False' is False. But in edge cases
|
||||
(stale process state, exception during response write) the fallback
|
||||
could produce a 404. Returning True is defensive.
|
||||
"""
|
||||
from urllib.parse import urlparse
|
||||
|
||||
handler = _FakeHandler()
|
||||
parsed = urlparse("/api/session/compress/status?session_id=test_resume_sid")
|
||||
result = handle_get(handler, parsed)
|
||||
|
||||
assert result is True, f"handle_get returned {result!r}, expected True"
|
||||
assert handler.status == 200
|
||||
|
||||
|
||||
def test_handle_get_returns_true_for_compress_status_no_sid():
|
||||
"""Even with missing session_id, handle_get returns True (400 handled internally)."""
|
||||
from urllib.parse import urlparse
|
||||
|
||||
handler = _FakeHandler()
|
||||
parsed = urlparse("/api/session/compress/status")
|
||||
result = handle_get(handler, parsed)
|
||||
|
||||
assert result is True
|
||||
# _handle_session_compress_status should return 400 for empty sid
|
||||
assert handler.status == 400
|
||||
|
||||
|
||||
# ======== Frontend static tests ========
|
||||
|
||||
|
||||
def _read_commands_js():
|
||||
with open(
|
||||
Path(__file__).resolve().parents[1] / "static" / "commands.js",
|
||||
encoding="utf-8",
|
||||
) as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def test_frontend_resume_404_silent():
|
||||
"""resumeManualCompressionForSession must silently return on 404/5xx.
|
||||
|
||||
The catch block should check for 404 and 5xx and return early, so
|
||||
switching sessions never shows 'Compression failed' on transient errors.
|
||||
"""
|
||||
src = _read_commands_js()
|
||||
|
||||
# Find the resumeManualCompressionForSession function
|
||||
assert "async function resumeManualCompressionForSession" in src
|
||||
|
||||
# The guard must be present in the catch block
|
||||
assert "e.status===404" in src
|
||||
assert "e.status>=500" in src
|
||||
# Verify it's inside the catch block of resumeManualCompressionForSession
|
||||
fn_start = src.index("async function resumeManualCompressionForSession")
|
||||
# Find the catch block after this function
|
||||
catch_idx = src.index("}catch(e){", fn_start)
|
||||
guard_404 = src.index("e.status===404", fn_start)
|
||||
guard_500 = src.index("e.status>=500", fn_start)
|
||||
assert catch_idx < guard_404 < guard_500, "guards must be inside catch block"
|
||||
|
||||
# The guard must return early (not just log)
|
||||
line_with_guard = src[guard_404 : src.index("\n", guard_500) + 80]
|
||||
assert "return" in line_with_guard, "guard must return early"
|
||||
|
||||
|
||||
def test_frontend_compress_status_call_present():
|
||||
"""Verify the compress/status API call is still in the frontend code."""
|
||||
src = _read_commands_js()
|
||||
assert "/api/session/compress/status" in src
|
||||
assert "resumeManualCompressionForSession" in src
|
||||
Reference in New Issue
Block a user