import gzip import re import shutil from pathlib import Path import httpx import pytest from starlette.requests import Request from src import main from src.frontend_bundle import build_frontend from src.views import dashboard, feature_bundle, runtime_bundle, service_worker FRONTEND = Path(__file__).resolve().parents[1] / "frontend" PAGE_SCRIPT = re.compile(r'') def test_page_runtime_is_one_deterministic_content_addressed_bundle(tmp_path): first = build_frontend(FRONTEND) second = build_frontend(FRONTEND) assert first.runtime_name == second.runtime_name assert first.runtime_bytes == second.runtime_bytes assert first.runtime_gzip_bytes == second.runtime_gzip_bytes assert gzip.decompress(first.runtime_gzip_bytes) == first.runtime_bytes assert PAGE_SCRIPT.findall(first.dashboard_html) == [] assert first.dashboard_html.count("' in first.dashboard_html assert first.runtime_name.startswith("runtime-") assert first.runtime_name.endswith(".js") assert len(gzip.compress(first.runtime_bytes, mtime=0)) <= 100 * 1024 changed_frontend = tmp_path / "frontend" shutil.copytree(FRONTEND, changed_frontend) (changed_frontend / "widgets.js").write_text( (changed_frontend / "widgets.js").read_text() + "\n// changed\n" ) changed = build_frontend(changed_frontend) assert changed.runtime_name != first.runtime_name assert changed.runtime_bytes != first.runtime_bytes def test_product_workflows_are_stable_lazy_feature_chunks(tmp_path): first = build_frontend(FRONTEND) assert set(first.feature_bundles) == {"issue-capture", "pull-workflow"} capture = first.feature_bundles["issue-capture"] pull_workflow = first.feature_bundles["pull-workflow"] assert b"function createIssueCapture" not in first.runtime_bytes assert b"function createIssueCapture" in capture.runtime_bytes assert b"function createPullSheet" not in first.runtime_bytes assert b"function createReviewController" not in first.runtime_bytes assert b"function createPullSheet" in pull_workflow.runtime_bytes assert b"function createReviewController" in pull_workflow.runtime_bytes assert len(first.runtime_gzip_bytes) <= 95 * 1024 assert f'name="stackchain-feature-issue-capture" content="{capture.runtime_name}"' in first.dashboard_html assert f'name="stackchain-feature-pull-workflow" content="{pull_workflow.runtime_name}"' in first.dashboard_html assert f"BASE + '{capture.runtime_name}'" in first.service_worker_source assert f"BASE + '{pull_workflow.runtime_name}'" in first.service_worker_source changed_frontend = tmp_path / "frontend" shutil.copytree(FRONTEND, changed_frontend) capture_source = changed_frontend / "create-issue-sheet.js" capture_source.write_text(capture_source.read_text() + "\n// feature-only change\n") changed = build_frontend(changed_frontend) assert changed.runtime_name == first.runtime_name assert changed.feature_bundles["issue-capture"].runtime_name != capture.runtime_name pull_source = changed_frontend / "pull-sheet.js" pull_source.write_text(pull_source.read_text() + "\n// pull-workflow-only change\n") pull_changed = build_frontend(changed_frontend) assert pull_changed.runtime_name == first.runtime_name assert pull_changed.feature_bundles["pull-workflow"].runtime_name != pull_workflow.runtime_name @pytest.mark.anyio async def test_feature_chunk_is_immutable_and_rejects_unknown_revision(): build = build_frontend(FRONTEND) capture = build.feature_bundles["issue-capture"] request = Request( {"type": "http", "method": "GET", "path": f"/{capture.runtime_name}", "headers": [(b"accept-encoding", b"identity")]} ) response = await feature_bundle("issue-capture", capture.runtime_digest, request) assert response.body == capture.runtime_bytes assert response.headers["cache-control"] == "public, max-age=31536000, immutable" with pytest.raises(Exception) as rejected: await feature_bundle("issue-capture", "missing", request) assert rejected.value.status_code == 404 def test_css_only_release_gets_a_new_shell_generation(tmp_path): first = build_frontend(FRONTEND) changed_frontend = tmp_path / "frontend" shutil.copytree(FRONTEND, changed_frontend) (changed_frontend / "dashboard.css").write_text( (changed_frontend / "dashboard.css").read_text() + "\n/* release change */\n" ) changed = build_frontend(changed_frontend) assert changed.runtime_name == first.runtime_name assert changed.runtime_bytes == first.runtime_bytes assert changed.shell_digest != first.shell_digest assert f"const CACHE = 'stackchain-dashboard-shell-{changed.shell_digest}';" in ( changed.service_worker_source ) @pytest.mark.parametrize( "asset", [ "manifest.webmanifest", "icons/stackchain-192.png", "icons/stackchain-512.png", "background-issue-sync.js", "service-worker.js", ], ) def test_every_offline_shell_asset_versions_the_shell_generation(tmp_path, asset): first = build_frontend(FRONTEND) changed_frontend = tmp_path / "frontend" shutil.copytree(FRONTEND, changed_frontend) path = changed_frontend / asset path.write_bytes(path.read_bytes() + b"\nrelease-change") changed = build_frontend(changed_frontend) assert changed.shell_digest != first.shell_digest def test_legacy_cache_marker_is_normalized_out_of_build_identity(tmp_path): first = build_frontend(FRONTEND) changed_frontend = tmp_path / "frontend" shutil.copytree(FRONTEND, changed_frontend) worker = changed_frontend / "service-worker.js" worker.write_text( worker.read_text().replace( "const CACHE = 'stackchain-dashboard-shell-v87';", "const CACHE = 'stackchain-dashboard-shell-v999';", ) ) changed = build_frontend(changed_frontend) assert changed.shell_digest == first.shell_digest assert changed.service_worker_source == first.service_worker_source @pytest.mark.anyio async def test_runtime_is_transferred_as_gzip_when_the_client_accepts_it(): transport = httpx.ASGITransport(app=main.app) async with httpx.AsyncClient(transport=transport, base_url="http://test") as client: response = await client.get( f"/{main.FRONTEND_BUILD.runtime_name}", headers={"Accept-Encoding": "gzip"}, ) assert response.status_code == 200 assert response.headers["content-encoding"] == "gzip" assert response.headers["vary"] == "Accept-Encoding" assert int(response.headers["content-length"]) == len(main.FRONTEND_BUILD.runtime_gzip_bytes) assert response.content == main.FRONTEND_BUILD.runtime_bytes assert response.headers["cache-control"] == "public, max-age=31536000, immutable" @pytest.mark.anyio async def test_runtime_route_returns_the_precomputed_gzip_representation(): build = build_frontend(FRONTEND) request = Request( {"type": "http", "method": "GET", "path": f"/{build.runtime_name}", "headers": [(b"accept-encoding", b"gzip")]} ) response = await runtime_bundle(build.runtime_digest, request) assert response.body == build.runtime_gzip_bytes assert response.headers["content-encoding"] == "gzip" assert response.headers["content-length"] == str(len(build.runtime_gzip_bytes)) assert response.headers["vary"] == "Accept-Encoding" @pytest.mark.anyio async def test_runtime_requests_bypass_dynamic_gzip_compression(monkeypatch): class UnexpectedDynamicCompression: def __init__(self, *args, **kwargs): raise AssertionError("immutable runtime entered dynamic compression") monkeypatch.setattr("src.compression.GZipResponder", UnexpectedDynamicCompression) transport = httpx.ASGITransport(app=main.app) async with httpx.AsyncClient(transport=transport, base_url="http://test") as client: response = await client.get( f"/{main.FRONTEND_BUILD.runtime_name}", headers={"Accept-Encoding": "gzip"}, ) assert response.status_code == 200 assert response.headers["content-encoding"] == "gzip" @pytest.mark.anyio @pytest.mark.parametrize("accept_encoding", ["identity", "gzip;q=0, identity;q=1"]) async def test_runtime_stays_unencoded_when_the_client_declines_gzip(accept_encoding): transport = httpx.ASGITransport(app=main.app) async with httpx.AsyncClient(transport=transport, base_url="http://test") as client: response = await client.get( f"/{main.FRONTEND_BUILD.runtime_name}", headers={"Accept-Encoding": accept_encoding}, ) assert response.status_code == 200 assert "content-encoding" not in response.headers assert int(response.headers["content-length"]) == len(main.FRONTEND_BUILD.runtime_bytes) assert response.content == main.FRONTEND_BUILD.runtime_bytes @pytest.mark.anyio async def test_dashboard_shell_is_compressed_without_losing_response_policies(): transport = httpx.ASGITransport(app=main.app) async with httpx.AsyncClient(transport=transport, base_url="http://test") as client: page = await client.get("/", headers={"Accept-Encoding": "gzip"}) stylesheet = await client.get( "/static/dashboard.css", headers={"Accept-Encoding": "gzip"} ) health = await client.get("/healthz", headers={"Accept-Encoding": "gzip"}) assert page.headers["content-encoding"] == "gzip" assert page.headers["cache-control"] == "no-cache" assert page.headers["content-security-policy"] assert main.FRONTEND_BUILD.runtime_name in page.text assert stylesheet.headers["content-encoding"] == "gzip" assert "mobile-task-dock" in stylesheet.text assert health.json() == {"service": "stackchain-dashboard", "status": "ok"} assert "content-encoding" not in health.headers def test_offline_shell_precaches_exact_runtime_without_superseded_page_modules(): build = build_frontend(FRONTEND) assert f"BASE + '{build.runtime_name}'" in build.service_worker_source for source in build.page_sources: if source == "static/background-issue-sync.js": continue assert f"BASE + '{source}'" not in build.service_worker_source @pytest.mark.anyio async def test_runtime_asset_is_immutable_while_html_and_worker_revalidate(): build = build_frontend(FRONTEND) request = Request( {"type": "http", "method": "GET", "path": f"/{build.runtime_name}", "headers": [(b"accept-encoding", b"identity")]} ) html = await dashboard() bundle = await runtime_bundle(build.runtime_digest, request) worker = await service_worker() assert build.runtime_name in html assert bundle.body == build.runtime_bytes assert bundle.headers["cache-control"] == "public, max-age=31536000, immutable" assert worker.headers["cache-control"] == "no-cache" assert build.runtime_name.encode() in worker.body