From 97d0c7a8ef1267af80a450246dd82f00fa299068 Mon Sep 17 00:00:00 2001 From: ciregenz Date: Sun, 9 Aug 2026 16:53:14 -0700 Subject: [PATCH] [eric] apps: an app nobody is editing serves its built bundle with no dev-server process (~270MB -> ~0), vite reboots the moment an agent binds or the dist goes stale --- backend/apps/agents/manager/AgentLaunch.py | 7 ++ backend/apps/outputs/outputs.py | 17 ++- backend/apps/outputs/runtime.py | 39 +++++- backend/apps/outputs/static_serve.py | 112 ++++++++++++++++++ .../webapp_template/frontend/vite.config.ts | 2 + backend/tests/test_serve_static_mode.py | 106 +++++++++++++++++ 6 files changed, 277 insertions(+), 6 deletions(-) create mode 100644 backend/apps/outputs/static_serve.py create mode 100644 backend/tests/test_serve_static_mode.py diff --git a/backend/apps/agents/manager/AgentLaunch.py b/backend/apps/agents/manager/AgentLaunch.py index 179110ae..3b544556 100644 --- a/backend/apps/agents/manager/AgentLaunch.py +++ b/backend/apps/agents/manager/AgentLaunch.py @@ -61,6 +61,13 @@ class AgentLaunch(AgentManagerProtocol): ): from backend.apps.outputs.workspace_io import app_workspace_dir bound = app_workspace_dir(config.selected_app_output_ids[0]) + if bound: + # The user is about to EDIT this app: a serve-static runtime must boot vite now or the agent's changes render nowhere (ENG-209). + try: + from backend.apps.outputs.runtime import manager as p_rt_manager + await p_rt_manager.ensure_editing(bound) + except Exception: + pass if bound: config.target_directory = bound diff --git a/backend/apps/outputs/outputs.py b/backend/apps/outputs/outputs.py index fa38e207..6af4b347 100644 --- a/backend/apps/outputs/outputs.py +++ b/backend/apps/outputs/outputs.py @@ -120,18 +120,23 @@ async def serve_workspace_file(workspace_id: str, filepath: str, p_d: str = ""): if not os.path.isfile(full_path): raise HTTPException(status_code=404, detail="File not found") - with open(full_path) as f: - content = f.read() - - if filepath == "index.html": + # endswith, not equality: serve-mode delivers frontend/dist/index.html through this same route and needs the identical injection + token rewrite (ENG-209). + if filepath.endswith("index.html"): + with open(full_path) as f: + content = f.read() input_json, result_json = decode_data_param(p_d) if p_d else ("{}", "null") backend_url_json = backend_url_for_workspace(workspace_id) content = inject_data_into_html(content, input_json, result_json, backend_url_json, with_runtime=True) # Iframe sub-resource fetches (, ') + return str(ws) + + +def test_static_fresh_requires_dist_newer_than_source_and_relative_assets(tmp_path): + ws = p_seed(tmp_path) + assert static_serve.static_fresh(ws) is True + time.sleep(0.02) + os.utime(os.path.join(ws, "frontend", "src", "App.tsx")) + assert static_serve.static_fresh(ws) is False + assert static_serve.static_fresh(p_seed(tmp_path / "abs", relative_assets=False)) is False + assert static_serve.static_fresh(p_seed(tmp_path / "nodist", with_dist=False)) is False + + +def test_start_serves_static_when_not_edited(tmp_path, monkeypatch): + ws = p_seed(tmp_path) + monkeypatch.setattr(static_serve, "workspace_being_edited", lambda p: False) + rt = AppRuntime("ws-t", ws) + assert asyncio.run(rt.start()) is True + assert rt.serve_static is True and rt.process is None and rt.ready is True + assert "/serve/frontend/dist/index.html" in (rt.frontend_url or "") + + +def test_start_skips_serve_when_edited(tmp_path, monkeypatch): + ws = p_seed(tmp_path) + monkeypatch.setattr(static_serve, "workspace_being_edited", lambda p: True) + rt = AppRuntime("ws-t2", ws) + spawned = {"n": 0} + + async def p_no_spawn(env): + spawned["n"] += 1 + return None, ws, "stub" + monkeypatch.setattr(rt, "p_resolve_launch", p_no_spawn) + try: + asyncio.run(rt.start()) + except Exception: + pass + assert rt.serve_static is False and spawned["n"] == 1 + + +def p_manager_no_watcher(monkeypatch): + # The restart-sentinel watcher is a forever loop; tests must not arm it or asyncio.run leaks a task. + monkeypatch.setattr(AppRuntimeManager, "p_ensure_restart_watcher", lambda self: None) + return AppRuntimeManager() + + +def test_ensure_editing_flips_serve_runtime_to_vite(tmp_path, monkeypatch): + ws = p_seed(tmp_path) + mgr = p_manager_no_watcher(monkeypatch) + rt = AppRuntime("ws-t", ws) + rt.serve_static = True + restarted = {"n": 0} + + async def p_fake_start(): + restarted["n"] += 1 + return True + monkeypatch.setattr(rt, "start", p_fake_start) + mgr.runtimes["ws-t"] = rt + asyncio.run(mgr.ensure_editing(ws)) + assert rt.serve_static is False and restarted["n"] == 1 + + +def test_attach_recheck_reboots_when_dist_goes_stale(tmp_path, monkeypatch): + ws = p_seed(tmp_path) + mgr = p_manager_no_watcher(monkeypatch) + rt = AppRuntime("ws-t", ws) + rt.serve_static = True + restarted = {"n": 0} + + async def p_fake_start(): + restarted["n"] += 1 + return True + monkeypatch.setattr(rt, "start", p_fake_start) + mgr.runtimes["ws-t"] = rt + + monkeypatch.setattr(static_serve, "workspace_being_edited", lambda p: False) + asyncio.run(mgr.attach("ws-t", ws)) + assert rt.serve_static is True and restarted["n"] == 0 + + time.sleep(0.02) + os.utime(os.path.join(ws, "frontend", "src", "App.tsx")) + asyncio.run(mgr.attach("ws-t", ws)) + assert rt.serve_static is False and restarted["n"] == 1