mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
117 lines
5.0 KiB
Python
117 lines
5.0 KiB
Python
"""The fresh-app-cannot-boot-a-backend chain (Haik, 2026-08-14, stable 1.7.7).
|
|
|
|
Root cause, verified against the shipped bundle: the packaged interpreter has no `ensurepip` and
|
|
no `pip`, so `python -m venv` yields a venv holding three symlinks and nothing else. The warm-cache
|
|
builder left that hollow venv on disk on every failure path, and `backend_init.sh` reused it on a
|
|
directory-exists check, so a known-broken venv was copied into every new app while three layers
|
|
printed success. These pin each link so the chain cannot silently reform.
|
|
"""
|
|
|
|
import inspect
|
|
import os
|
|
import re
|
|
|
|
from backend.apps.outputs import view_builder_templates
|
|
|
|
|
|
TEMPLATE_DIR = os.path.join(os.path.dirname(inspect.getfile(view_builder_templates)), "webapp_template")
|
|
|
|
|
|
def p_builder_src() -> str:
|
|
return inspect.getsource(view_builder_templates.p_ensure_warm_python_venv)
|
|
|
|
|
|
# --------------------------------------------------------------- never leave known-bad output
|
|
|
|
|
|
def test_every_failure_path_deletes_the_half_built_venv():
|
|
src = p_builder_src()
|
|
# Each `return None` in the body must be preceded by a cleanup; count them as pairs.
|
|
returns = src.count("return None")
|
|
cleanups = src.count("shutil.rmtree(venv_dir, ignore_errors=True)")
|
|
assert returns >= 3, "the builder should still have its failure paths"
|
|
assert cleanups >= returns, (
|
|
f"{returns} failure paths but only {cleanups} cleanups; a hollow venv left on disk "
|
|
"gets copied into the next app forever"
|
|
)
|
|
|
|
|
|
def test_the_builder_refuses_a_venv_with_no_pip():
|
|
src = p_builder_src()
|
|
assert "os.path.exists(pip)" in src, "a venv without pip must be discarded, not populated"
|
|
|
|
|
|
def test_the_sentinel_means_usable_not_merely_attempted():
|
|
src = p_builder_src()
|
|
i_probe = src.find("import fastapi, typeguard, httpx, uvicorn")
|
|
i_sentinel = src.find('open(sentinel, "w"')
|
|
assert i_probe != -1, "the builder must prove the venv can import what the template needs"
|
|
assert i_probe < i_sentinel, "the import probe must run BEFORE the sentinel is written"
|
|
|
|
|
|
# --------------------------------------------------------------- the consumer honors the sentinel
|
|
|
|
|
|
def test_backend_init_gates_reuse_on_the_sentinel_not_directory_existence():
|
|
init = open(os.path.join(TEMPLATE_DIR, "backend_init.sh"), encoding="utf-8").read()
|
|
assert "CACHE_SENTINEL" in init, "the reuse gate must consult .populated"
|
|
reuse = re.search(r'if \[\[ -d "\$CACHE_VENV".*?\]\]; then\n\s*echo "Reusing', init, re.S)
|
|
assert reuse and "CACHE_SENTINEL" in reuse.group(0), (
|
|
"the branch that copies the cache must require the sentinel; -d alone copies hollow venvs"
|
|
)
|
|
|
|
|
|
# --------------------------------------------------------------- the interpreter must be capable
|
|
|
|
|
|
def test_the_packaged_build_keeps_ensurepip():
|
|
build = open(os.path.join(os.path.dirname(view_builder_templates.__file__), "..", "..", "..",
|
|
"scripts", "build-python-env.sh"), encoding="utf-8").read()
|
|
assert 'rm -rf "$PYTHON_ENV_DIR/lib/python3.13/ensurepip"' not in build, (
|
|
"stripping ensurepip makes every app-backend venv hollow; that is the root cause"
|
|
)
|
|
assert "cannot create a venv with pip" in build, (
|
|
"the build must PROVE the bundled interpreter can make a working venv, not assume it"
|
|
)
|
|
|
|
|
|
# --------------------------------------------------------------- host env must not leak
|
|
|
|
|
|
def test_pythonpath_is_stripped_from_workspace_subprocesses():
|
|
from backend.apps.outputs.runtime import AppRuntime
|
|
src = inspect.getsource(AppRuntime.p_spawn_env_base)
|
|
assert "PYTHONPATH" in src, (
|
|
"the host's PYTHONPATH points at the app bundle's site-packages and shadows the "
|
|
"workspace venv, so pip reports success while installing nothing"
|
|
)
|
|
|
|
|
|
# --------------------------------------------------------------- serve mode vs a real backend
|
|
|
|
|
|
def test_serve_mode_never_engages_for_an_app_that_declares_a_backend():
|
|
from backend.apps.outputs.runtime import AppRuntime
|
|
src = inspect.getsource(AppRuntime.p_start_new_mode)
|
|
assert "p_declares_backend" in src, (
|
|
"serve mode spawns nothing; engaging it for an app with a backend serves a bundle "
|
|
"against an API that was never started, and returns True"
|
|
)
|
|
assert src.index("p_declares_backend") < src.index("self.serve_static = True")
|
|
|
|
|
|
def test_a_restart_re_decides_serve_mode():
|
|
from backend.apps.outputs.runtime import AppRuntime
|
|
src = inspect.getsource(AppRuntime.start)
|
|
assert "self.serve_static = False" in src, "a stale serve_static survives the restart meant to clear it"
|
|
|
|
|
|
def test_the_restart_sentinel_watcher_can_see_serve_mode_runtimes():
|
|
from backend.apps.outputs.runtime import AppRuntimeManager
|
|
src = inspect.getsource(AppRuntimeManager)
|
|
watcher = src[src.find("RESTART_SENTINEL_NAME"):]
|
|
assert "peer.serve_static" in watcher, (
|
|
"a serve-mode runtime has no process, so gating on `running` made restart.sh dead on "
|
|
"exactly the runtimes that needed restarting"
|
|
)
|