"""Sprint 6 tests: Escape from editor, Phase D validation, HTML extraction, cron create, session export.""" import json, uuid, pathlib, urllib.request, urllib.error REPO_ROOT = pathlib.Path(__file__).parent.parent.resolve() BASE = "http://127.0.0.1:8788" # isolated test server def get(path): with urllib.request.urlopen(BASE + path, timeout=10) as r: return json.loads(r.read()), r.status def get_raw(path): with urllib.request.urlopen(BASE + path, timeout=10) as r: return r.read(), r.headers, r.status def post(path, body=None): data = json.dumps(body or {}).encode() req = urllib.request.Request(BASE + path, data=data, headers={"Content-Type": "application/json"}) try: with urllib.request.urlopen(req, timeout=10) as r: return json.loads(r.read()), r.status except urllib.error.HTTPError as e: return json.loads(e.read()), e.code def make_session_tracked(created_list, ws=None): body = {} if ws: body["workspace"] = str(ws) d, _ = post("/api/session/new", body) sid = d["session"]["session_id"] created_list.append(sid) return sid, pathlib.Path(d["session"]["workspace"]) # ── Phase E: HTML served from static/index.html ── def test_index_html_served(): raw, headers, status = get_raw("/") assert status == 200 assert b"sidebarResize" in raw, "Resize handle not found in HTML" assert b"cronCreateForm" in raw, "Cron create form not found in HTML" assert b"btnExportJSON" in raw, "Export JSON button not found in HTML" def test_index_html_file_exists(): p = REPO_ROOT / "static/index.html" assert p.exists(), "static/index.html does not exist" assert p.stat().st_size > 5000, "index.html seems too small" def test_server_py_has_no_html_string(): txt = (REPO_ROOT / "server.py").read_text() assert 'HTML = r"""' not in txt, "server.py still contains inline HTML string" assert "doctype html" not in txt.lower(), "server.py still contains raw HTML" # ── Phase D: remaining endpoint validation ── def test_approval_respond_requires_session_id(): result, status = post("/api/approval/respond", {"choice": "deny"}) assert status == 400 def test_approval_respond_rejects_invalid_choice(cleanup_test_sessions): sid, _ = make_session_tracked(cleanup_test_sessions) result, status = post("/api/approval/respond", {"session_id": sid, "choice": "INVALID"}) assert status == 400 def test_file_raw_requires_session_id(): try: get_raw("/api/file/raw?path=test.png") assert False, "Expected 400" except urllib.error.HTTPError as e: assert e.code == 400 def test_file_raw_unknown_session(): try: get_raw("/api/file/raw?session_id=nosuchsession&path=test.png") assert False, "Expected 404" except urllib.error.HTTPError as e: assert e.code == 404 # ── Cron create ── def test_cron_create_requires_prompt(): result, status = post("/api/crons/create", {"schedule": "0 9 * * *"}) assert status == 400 assert "prompt" in result.get("error", "").lower() def test_cron_create_requires_schedule(): result, status = post("/api/crons/create", {"prompt": "Say hello"}) assert status == 400 assert "schedule" in result.get("error", "").lower() def test_cron_create_invalid_schedule(): result, status = post("/api/crons/create", { "prompt": "Say hello", "schedule": "not_a_valid_schedule_xyz" }) assert status == 400 def test_cron_create_success(): uid = uuid.uuid4().hex[:6] result, status = post("/api/crons/create", { "name": f"test-job-{uid}", "prompt": "Just say 'hello' and nothing else.", "schedule": "every 999h", # far future -- won't actually run during test "deliver": "local", }) assert status == 200, f"Expected 200 got {status}: {result}" assert result["ok"] is True assert "job" in result job_id = result["job"]["id"] # Verify it appears in the cron list jobs, _ = get("/api/crons") ids = [j["id"] for j in jobs["jobs"]] assert job_id in ids, f"Created job {job_id} not in list" # ── Session export ── def test_session_export_requires_session_id(): try: get_raw("/api/session/export") assert False except urllib.error.HTTPError as e: assert e.code == 400 def test_session_export_unknown_session(): try: get_raw("/api/session/export?session_id=nosuchsession") assert False except urllib.error.HTTPError as e: assert e.code == 404 def test_session_export_returns_json(cleanup_test_sessions): sid, _ = make_session_tracked(cleanup_test_sessions) raw, headers, status = get_raw(f"/api/session/export?session_id={sid}") assert status == 200 assert "application/json" in headers.get("Content-Type", "") data = json.loads(raw) assert data["session_id"] == sid assert "messages" in data assert "title" in data # ── Resizable panels: static files present ── def test_static_index_has_resize_handles(): raw, _, status = get_raw("/") assert status == 200 assert b"sidebarResize" in raw assert b"rightpanelResize" in raw def test_app_js_has_resize_logic(): """Sprint 9: app.js replaced by modules. Resize logic lives in boot.js.""" raw, _, status = get_raw("/static/boot.js") assert status == 200 assert b"_initResizePanels" in raw assert b"hermes-sidebar-w" in raw assert b"hermes-panel-w" in raw