"""OPENAI4S_WEBUI: dist SPA shell by default, legacy escape hatch, packaging.
Wire tests go through a real ThreadingHTTPServer so `/`, `/index.html`, and
an unknown non-API GET (SPA deep links) share the same `_serve_index` path
the production daemon uses. Packaging assertions are source-level so a
dropped glob fails before `uv build`.
"""
from __future__ import annotations
import http.client
import importlib.util
import json
import re
import sys
import threading
from pathlib import Path
import pytest
from openai4s.config import Config, LLMConfig
from openai4s.server import gateway as gateway_mod
from openai4s.server import local_auth
from tests._ports import bound_gateway_server
ROOT = Path(__file__).resolve().parents[1]
_LEGACY = b"
legacy-shell\n"
_NEXT = (
b"next-shell\n"
b'\n'
)
_ASSET = b"console.log('next-shell')\n"
class _Hub:
def emitter(self, root_frame_id):
return lambda event: None
def broadcast(self, root_frame_id, event):
return None
def _write(webui: Path, rel: str, data: bytes) -> Path:
path = webui / rel
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(data)
return path
def _load_verifier():
path = ROOT / "scripts" / "verify_release_artifacts.py"
spec = importlib.util.spec_from_file_location(
"openai4s_test_verify_release_f04", path
)
assert spec is not None and spec.loader is not None
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
@pytest.fixture()
def webui_next_daemon(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
"""Loopback gateway with both the legacy shell and a dist tree planted."""
webui = tmp_path / "webui"
webui.mkdir()
_write(webui, "index.html", _LEGACY)
_write(webui, "app.js", b"legacy-app\n")
_write(webui, "dist/index.html", _NEXT)
_write(webui, "dist/assets/index-BwbKMGnO.js", _ASSET)
monkeypatch.setattr(gateway_mod, "WEBUI_DIR", webui)
httpd, port = bound_gateway_server()
cfg = Config(
data_dir=tmp_path / "data",
llm=LLMConfig(provider="deepseek", api_key="test-key"),
max_turns=3,
host="127.0.0.1",
port=port,
)
cfg.ensure_dirs()
runner = gateway_mod.SessionRunner(cfg, _Hub(), start_idle_sweeper=False)
handler_cls = gateway_mod.make_handler(cfg, _Hub(), runner)
httpd.RequestHandlerClass = handler_cls
thread = threading.Thread(target=httpd.serve_forever, daemon=True)
thread.start()
token = local_auth.load_or_mint(cfg.data_dir)
try:
yield {"port": port, "token": token, "webui": webui}
finally:
httpd.shutdown()
httpd.server_close()
thread.join(timeout=5)
try:
runner.close()
except Exception: # noqa: BLE001 - teardown must not mask a failure
pass
def _get(
port: int,
path: str,
token: str,
) -> tuple[int, dict[str, str], bytes]:
headers = {local_auth.TOKEN_HEADER: token}
conn = http.client.HTTPConnection("127.0.0.1", port, timeout=10)
try:
conn.request("GET", path, headers=headers)
resp = conn.getresponse()
body = resp.read()
hdrs = {key.lower(): value for key, value in resp.getheaders()}
return resp.status, hdrs, body
finally:
conn.close()
@pytest.mark.parametrize(
"raw, legacy",
[
(None, False),
("", False),
("0", False),
("1", False),
("next", False),
("true", False),
("yes", False),
("on", False),
("LEGACY", False),
("legacy", True),
(" legacy ", True),
],
)
def test_webui_legacy_flag_is_exact_legacy(
monkeypatch: pytest.MonkeyPatch, raw, legacy
):
monkeypatch.delenv("OPENAI4S_WEBUI_NEXT", raising=False)
if raw is None:
monkeypatch.delenv("OPENAI4S_WEBUI", raising=False)
else:
monkeypatch.setenv("OPENAI4S_WEBUI", raw)
assert gateway_mod._webui_legacy_enabled() is legacy
def test_stale_webui_next_env_does_not_select_legacy(monkeypatch: pytest.MonkeyPatch):
"""Retired F-04 name must not flip the default back to the hatch."""
monkeypatch.setenv("OPENAI4S_WEBUI_NEXT", "1")
monkeypatch.delenv("OPENAI4S_WEBUI", raising=False)
assert gateway_mod._webui_legacy_enabled() is False
def test_unset_flag_serves_dist_shell_and_deep_links(webui_next_daemon):
port, token = webui_next_daemon["port"], webui_next_daemon["token"]
for path in ("/", "/index.html", "/projects/p1/frames/f1"):
status, hdrs, body = _get(port, path, token)
assert status == 200, path
assert body == _NEXT, path
assert hdrs["content-type"].startswith("text/html")
assert b"legacy-shell" not in body
def test_legacy_flag_serves_legacy_shell_and_deep_links(
webui_next_daemon, monkeypatch: pytest.MonkeyPatch
):
monkeypatch.setenv("OPENAI4S_WEBUI", "legacy")
port, token = webui_next_daemon["port"], webui_next_daemon["token"]
for path in ("/", "/index.html", "/projects/p1/frames/f1"):
status, hdrs, body = _get(port, path, token)
assert status == 200, path
assert body == _LEGACY, path
assert hdrs["content-type"].startswith("text/html")
assert b"next-shell" not in body
def test_flag_does_not_hide_legacy_static_or_dist_tree(
webui_next_daemon, monkeypatch: pytest.MonkeyPatch
):
"""`/static/` resolution is independent of the SPA-shell switch."""
port, token = webui_next_daemon["port"], webui_next_daemon["token"]
status, _, body = _get(port, "/static/dist/index.html", token)
assert status == 200
assert body == _NEXT
status, _, body = _get(port, "/static/dist/assets/index-BwbKMGnO.js", token)
assert status == 200
assert body == _ASSET
monkeypatch.setenv("OPENAI4S_WEBUI", "legacy")
status, _, body = _get(port, "/static/app.js", token)
assert status == 200
assert body == b"legacy-app\n"
status, _, body = _get(port, "/static/dist/assets/index-BwbKMGnO.js", token)
assert status == 200
assert body == _ASSET
def test_default_without_dist_index_is_404(webui_next_daemon):
(webui_next_daemon["webui"] / "dist" / "index.html").unlink()
status, _, body = _get(webui_next_daemon["port"], "/", webui_next_daemon["token"])
assert status == 404
assert json.loads(body.decode("utf-8"))["error"] == "not found"
def test_legacy_without_dist_still_serves_legacy_shell(
webui_next_daemon, monkeypatch: pytest.MonkeyPatch
):
monkeypatch.setenv("OPENAI4S_WEBUI", "legacy")
(webui_next_daemon["webui"] / "dist" / "index.html").unlink()
status, _, body = _get(webui_next_daemon["port"], "/", webui_next_daemon["token"])
assert status == 200
assert body == _LEGACY
def test_package_data_lists_dist_tree_explicitly():
text = (ROOT / "pyproject.toml").read_text(encoding="utf-8")
start = text.index("[tool.setuptools.package-data]")
end = text.index("[tool.setuptools.exclude-package-data]")
block = text[start:end]
assert '"server/webui/dist/*.html"' in block
assert '"server/webui/dist/assets/*"' in block
def test_wheel_required_pins_dist_index_sentinel():
verifier = _load_verifier()
assert "openai4s/server/webui/dist/index.html" in verifier._WHEEL_REQUIRED
assert "openai4s/server/webui/dist/index.html" in verifier._SDIST_REQUIRED
def test_committed_dist_index_is_external_script_shell():
path = ROOT / "openai4s" / "server" / "webui" / "dist" / "index.html"
html = path.read_text(encoding="utf-8")
assert "/static/dist/" in html
assert re.search(r"