ouroboros/tests/test_launcher_sync.py
Ouroboros cc88a32090 Qualify experimental Android delivery without blocking desktop releases
Select emulator coverage from Android changes and tags, retain observed Android build and verification outcomes, and publish only proof-accepted artifacts. Keep the seven desktop assets mandatory while excluding any incomplete or unverified Android pair. Make existing native labels English, link the recorded capability follow-ups, and align inherited tests with typed delegation and UTF-8 reads. Global function-count debt remains disclosed pending the owner decision.
2026-09-18 02:10:23 +03:00

711 lines
28 KiB
Python

import importlib
import json
import os
import pathlib
import subprocess
import sys
import types
import ouroboros.launcher_bootstrap as bootstrap_module
REPO_ROOT = pathlib.Path(__file__).resolve().parents[1]
BUILD_REPO_BUNDLE = REPO_ROOT / "scripts" / "build_repo_bundle.py"
# These keys must be scrubbed from every ``build_repo_bundle.py``
# subprocess: GitHub Actions tag-push runs set GITHUB_REF_* globally,
# which otherwise bleeds into temp-repo subprocesses and confuses
# ``_resolve_release_tag``.
_BUILD_BUNDLE_ENV_SCRUB_KEYS = (
"OUROBOROS_RELEASE_TAG",
"GITHUB_REF",
"GITHUB_REF_TYPE",
"GITHUB_REF_NAME",
)
def _scrubbed_env() -> "dict[str, str]":
env = dict(os.environ)
for key in _BUILD_BUNDLE_ENV_SCRUB_KEYS:
env.pop(key, None)
return env
def _reload_bootstrap():
return importlib.reload(bootstrap_module)
def _log_stub():
return types.SimpleNamespace(
info=lambda *args, **kwargs: None,
warning=lambda *args, **kwargs: None,
error=lambda *args, **kwargs: None,
)
def _make_context(bundle_dir, repo_dir):
return bootstrap_module.BootstrapContext(
bundle_dir=bundle_dir,
repo_dir=repo_dir,
data_dir=repo_dir.parent / "data",
settings_path=repo_dir.parent / "settings.json",
embedded_python=sys.executable,
app_version="4.50.0-rc.2",
hidden_run=subprocess.run,
save_settings=lambda settings: None,
log=_log_stub(),
)
def _run(cmd, *, cwd):
subprocess.run(cmd, cwd=str(cwd), check=True, capture_output=True, text=True)
def _git_output(cwd, *args):
return subprocess.run(
["git", *args],
cwd=str(cwd),
check=True,
capture_output=True,
text=True,
).stdout.strip()
def _make_bundle_source(tmp_path):
src = tmp_path / "src"
src.mkdir()
_run(["git", "init"], cwd=src)
_run(["git", "config", "user.name", "Test"], cwd=src)
_run(["git", "config", "user.email", "test@example.com"], cwd=src)
_run(["git", "checkout", "-b", "ouroboros"], cwd=src)
_run(["git", "remote", "add", "origin", "https://github.com/razzant/ouroboros.git"], cwd=src)
(src / "VERSION").write_text("4.50.0-rc.2\n", encoding="utf-8")
(src / "server.py").write_text("print('bundle-v1')\n", encoding="utf-8")
_run(["git", "add", "VERSION", "server.py"], cwd=src)
_run(["git", "commit", "-m", "bundle v1"], cwd=src)
_run(["git", "tag", "-a", "v4.50.0-rc.2", "-m", "Release v4.50.0-rc.2"], cwd=src)
return src
def _write_bundle(repo_src, bundle_dir):
bundle_dir.mkdir(parents=True, exist_ok=True)
subprocess.run(
[
sys.executable,
str(BUILD_REPO_BUNDLE),
"--repo-root",
str(repo_src),
"--output-bundle",
str(bundle_dir / "repo.bundle"),
"--output-manifest",
str(bundle_dir / "repo_bundle_manifest.json"),
],
check=True,
capture_output=True,
text=True,
env=_scrubbed_env(),
)
def test_ensure_managed_repo_clones_from_embedded_bundle(tmp_path):
bootstrap = _reload_bootstrap()
src = _make_bundle_source(tmp_path)
bundle_dir = tmp_path / "bundle"
repo_dir = tmp_path / "repo"
_write_bundle(src, bundle_dir)
ctx = _make_context(bundle_dir, repo_dir)
outcome = bootstrap.ensure_managed_repo(ctx)
assert outcome == "created"
assert (repo_dir / ".git").is_dir()
assert (repo_dir / "server.py").read_text(encoding="utf-8") == "print('bundle-v1')\n"
assert _git_output(repo_dir, "branch", "--show-current") == "ouroboros"
meta = bootstrap.load_repo_manifest(repo_dir)
assert meta["managed_remote_branch"] == "ouroboros"
assert meta["managed_local_branch"] == "ouroboros"
assert meta["release_tag"] == "v4.50.0-rc.2"
assert meta["bundle_sha256"]
assert (repo_dir / ".git" / bootstrap.BOOTSTRAP_PIN_MARKER_NAME).exists()
def test_ensure_managed_repo_preserves_origin_on_unchanged_boot(tmp_path):
bootstrap = _reload_bootstrap()
src = _make_bundle_source(tmp_path)
bundle_dir = tmp_path / "bundle"
repo_dir = tmp_path / "repo"
_write_bundle(src, bundle_dir)
ctx = _make_context(bundle_dir, repo_dir)
assert bootstrap.ensure_managed_repo(ctx) == "created"
_run(["git", "remote", "add", "origin", "https://github.com/example/fork.git"], cwd=repo_dir)
outcome = bootstrap.ensure_managed_repo(ctx)
assert outcome == "unchanged"
remotes = set(_git_output(repo_dir, "remote").splitlines())
assert remotes == {"managed", "origin"}
def test_sync_existing_repo_from_bundle_replaces_legacy_snapshot(tmp_path):
bootstrap = _reload_bootstrap()
src = _make_bundle_source(tmp_path)
bundle_dir = tmp_path / "bundle"
repo_dir = tmp_path / "repo"
_write_bundle(src, bundle_dir)
repo_dir.mkdir()
(repo_dir / "server.py").write_text("print('legacy-snapshot')\n", encoding="utf-8")
ctx = _make_context(bundle_dir, repo_dir)
bootstrap.sync_existing_repo_from_bundle(ctx)
assert (repo_dir / ".git").is_dir()
assert (repo_dir / "server.py").read_text(encoding="utf-8") == "print('bundle-v1')\n"
archived = list((ctx.data_dir / "archive" / "managed_repo").iterdir())
assert archived
assert (archived[0] / "server.py").read_text(encoding="utf-8") == "print('legacy-snapshot')\n"
def test_ensure_managed_repo_preserves_checkout_when_embedded_bundle_changes(tmp_path):
bootstrap = _reload_bootstrap()
src = _make_bundle_source(tmp_path)
bundle_dir = tmp_path / "bundle"
repo_dir = tmp_path / "repo"
_write_bundle(src, bundle_dir)
ctx = _make_context(bundle_dir, repo_dir)
assert bootstrap.ensure_managed_repo(ctx) == "created"
_run(["git", "remote", "add", "origin", "https://github.com/example/fork.git"], cwd=repo_dir)
(repo_dir / "server.py").write_text("print('local-self-modification')\n", encoding="utf-8")
_run(["git", "add", "server.py"], cwd=repo_dir)
_run(["git", "commit", "-m", "local self modification"], cwd=repo_dir)
local_head = _git_output(repo_dir, "rev-parse", "HEAD")
(src / "server.py").write_text("print('bundle-v2')\n", encoding="utf-8")
_run(["git", "add", "server.py"], cwd=src)
_run(["git", "commit", "-m", "bundle v2"], cwd=src)
# Re-point the annotated release tag onto the new HEAD so the bundle
# builder's HEAD-tag check still passes (the test is exercising the
# bundle-replacement path, not a VERSION bump).
_run(["git", "tag", "-d", "v4.50.0-rc.2"], cwd=src)
_run(["git", "tag", "-a", "v4.50.0-rc.2", "-m", "Release v4.50.0-rc.2 (v2)"], cwd=src)
_write_bundle(src, bundle_dir)
outcome = bootstrap.ensure_managed_repo(ctx)
assert outcome == "metadata-updated"
assert _git_output(repo_dir, "rev-parse", "HEAD") == local_head
assert (repo_dir / "server.py").read_text(encoding="utf-8") == "print('local-self-modification')\n"
assert set(_git_output(repo_dir, "remote").splitlines()) == {"managed", "origin"}
assert bootstrap.load_repo_manifest(repo_dir)["source_sha"] == _git_output(src, "rev-parse", "HEAD")
assert not (ctx.data_dir / "archive" / "managed_repo").exists()
def test_load_bundle_manifest_rejects_app_version_mismatch(tmp_path):
bootstrap = _reload_bootstrap()
src = _make_bundle_source(tmp_path)
bundle_dir = tmp_path / "bundle"
repo_dir = tmp_path / "repo"
_write_bundle(src, bundle_dir)
ctx = bootstrap.BootstrapContext(
bundle_dir=bundle_dir,
repo_dir=repo_dir,
data_dir=repo_dir.parent / "data",
settings_path=repo_dir.parent / "settings.json",
embedded_python=sys.executable,
app_version="4.50.0-rc.3",
hidden_run=subprocess.run,
save_settings=lambda settings: None,
log=_log_stub(),
)
try:
bootstrap.load_bundle_manifest(ctx)
assert False, "Expected app_version mismatch to raise"
except RuntimeError as exc:
assert "app_version" in str(exc)
def test_load_bundle_manifest_rejects_release_tag_mismatch(tmp_path):
bootstrap = _reload_bootstrap()
src = _make_bundle_source(tmp_path)
bundle_dir = tmp_path / "bundle"
repo_dir = tmp_path / "repo"
_write_bundle(src, bundle_dir)
manifest_path = bundle_dir / "repo_bundle_manifest.json"
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
manifest["release_tag"] = "v4.50.0-rc.3"
manifest_path.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
ctx = _make_context(bundle_dir, repo_dir)
try:
bootstrap.load_bundle_manifest(ctx)
assert False, "Expected release_tag mismatch to raise"
except RuntimeError as exc:
assert "release_tag" in str(exc)
def test_ensure_managed_repo_rejects_tampered_bundle(tmp_path):
bootstrap = _reload_bootstrap()
src = _make_bundle_source(tmp_path)
bundle_dir = tmp_path / "bundle"
repo_dir = tmp_path / "repo"
_write_bundle(src, bundle_dir)
(bundle_dir / "repo.bundle").write_bytes(b"tampered-bundle")
ctx = _make_context(bundle_dir, repo_dir)
try:
bootstrap.ensure_managed_repo(ctx)
assert False, "Expected bundle hash mismatch to raise"
except RuntimeError as exc:
assert "bundle hash mismatch" in str(exc)
# ---------------------------------------------------------------------------
# Host service port cleanup (formerly test_launcher_host_service_cleanup.py)
# ---------------------------------------------------------------------------
def test_host_service_cleanup_uses_configured_port(monkeypatch):
import launcher
killed: list[int] = []
monkeypatch.setenv("OUROBOROS_HOST_SERVICE_PORT", "9876")
monkeypatch.setattr(launcher, "_kill_stale_on_port", lambda port: killed.append(port))
launcher._kill_stale_runtime_ports(8765)
assert killed == [8765, 9876]
def test_agent_lifecycle_preflight_cleans_host_service_port(monkeypatch):
import launcher
killed: list[int] = []
class FakeProcess:
pid = 12345
returncode = 0
def wait(self):
launcher._shutdown_event.set()
launcher._shutdown_event.clear()
monkeypatch.setattr(launcher, "_host_service_port", lambda: 9876)
monkeypatch.setattr(launcher, "_kill_stale_on_port", lambda port: killed.append(port))
# The per-generation stray sweep must never signal a real process from a test.
monkeypatch.setattr(launcher, "_reap_same_install_strays", lambda reason: [])
monkeypatch.setattr(launcher, "start_agent", lambda port: FakeProcess())
monkeypatch.setattr(launcher, "_poll_port_file", lambda timeout=30, abort_event=None: 8765)
monkeypatch.setattr(launcher, "_wait_for_server", lambda port, timeout=30.0, abort_event=None: True)
monkeypatch.setattr(launcher, "_agent_job", None)
monkeypatch.setattr(launcher, "log", types.SimpleNamespace(info=lambda *args, **kwargs: None))
try:
launcher.agent_lifecycle_loop(port=8765)
finally:
launcher._shutdown_event.clear()
assert killed[:2] == [8765, 9876]
def test_start_agent_exports_presentation_posture(monkeypatch, tmp_path):
"""Producer contract (Owner Surface Fact): the launcher's OWN env assembly
must export OUROBOROS_PRESENTATION — a monkeypatched reader test cannot
catch a dead wire (the retired is_desktop lesson)."""
import launcher
data_dir = tmp_path / "data"
repo_dir = tmp_path / "repo"
repo_dir.mkdir()
(repo_dir / "server.py").write_text("print('server')\n", encoding="utf-8")
captured = {}
class FakeStdout:
def readline(self):
return b""
class FakeProcess:
pid = 4242
stdout = FakeStdout()
def fake_popen(cmd, **kwargs):
captured["env"] = kwargs.get("env") or {}
return FakeProcess()
monkeypatch.setattr(launcher, "IS_WINDOWS", False)
monkeypatch.setattr(launcher, "DATA_DIR", data_dir)
monkeypatch.setattr(launcher, "REPO_DIR", repo_dir)
monkeypatch.setattr(launcher, "EMBEDDED_PYTHON", sys.executable)
monkeypatch.setattr(launcher, "_load_settings", lambda: {})
monkeypatch.setattr(launcher, "_apply_settings_to_env", lambda _settings: None)
monkeypatch.setattr(launcher, "subprocess_new_group_kwargs", lambda: {"start_new_session": True})
monkeypatch.setattr(launcher, "_hidden_popen", fake_popen)
monkeypatch.setattr(launcher, "process_group_id", lambda _pid: 4242)
monkeypatch.setattr(launcher, "_headless", False)
launcher.start_agent(port=9871)
assert captured["env"]["OUROBOROS_PRESENTATION"] == "desktop_window"
monkeypatch.setattr(launcher, "_headless", True)
launcher.start_agent(port=9872)
assert captured["env"]["OUROBOROS_PRESENTATION"] == "browser_fallback"
monkeypatch.setattr(launcher, "_external_ui", True)
monkeypatch.setenv("OUROBOROS_PRESENTATION", "android_app")
launcher.start_agent(port=9873)
assert captured["env"]["OUROBOROS_PRESENTATION"] == "android_app"
monkeypatch.delenv("OUROBOROS_PRESENTATION")
launcher.start_agent(port=9874)
assert captured["env"]["OUROBOROS_PRESENTATION"] == "web"
def test_start_agent_unix_uses_process_group_and_writes_server_record(monkeypatch, tmp_path):
import launcher
data_dir = tmp_path / "data"
repo_dir = tmp_path / "repo"
repo_dir.mkdir()
(repo_dir / "server.py").write_text("print('server')\n", encoding="utf-8")
captured = {}
class FakeStdout:
def readline(self):
return b""
class FakeProcess:
pid = 12345
stdout = FakeStdout()
def fake_popen(cmd, **kwargs):
captured["cmd"] = cmd
captured["kwargs"] = kwargs
return FakeProcess()
monkeypatch.setattr(launcher, "IS_WINDOWS", False)
monkeypatch.setattr(launcher, "DATA_DIR", data_dir)
monkeypatch.setattr(launcher, "REPO_DIR", repo_dir)
monkeypatch.setattr(launcher, "EMBEDDED_PYTHON", sys.executable)
monkeypatch.setattr(launcher, "_load_settings", lambda: {})
monkeypatch.setattr(launcher, "_apply_settings_to_env", lambda _settings: None)
monkeypatch.setattr(launcher, "subprocess_new_group_kwargs", lambda: {"start_new_session": True})
monkeypatch.setattr(launcher, "_hidden_popen", fake_popen)
monkeypatch.setattr(launcher, "process_group_id", lambda _pid: 12345)
proc = launcher.start_agent(port=9876)
assert proc.pid == 12345
assert captured["kwargs"]["start_new_session"] is True
record = json.loads((data_dir / "state" / "server_process.json").read_text(encoding="utf-8"))
assert record["pid"] == 12345
assert record["pgid"] == 12345
assert record["requested_port"] == 9876
assert record["port"] == 9876
assert record["repo_dir"] == str(repo_dir.resolve())
assert record["server_path"] == str((repo_dir / "server.py").resolve())
assert record["argv"] == [sys.executable, str((repo_dir / "server.py").resolve())]
assert record["created_at"]
def test_recorded_server_cleanup_ignores_unrelated_pid(monkeypatch, tmp_path):
import launcher
data_dir = tmp_path / "data"
repo_dir = tmp_path / "repo"
(data_dir / "state").mkdir(parents=True)
repo_dir.mkdir()
server_py = repo_dir / "server.py"
server_py.write_text("print('server')\n", encoding="utf-8")
(data_dir / "state" / "server_process.json").write_text(
json.dumps({
"pid": 22222,
"pgid": 22222,
"server_path": str(server_py.resolve()),
"repo_dir": str(repo_dir.resolve()),
"port": 8765,
}),
encoding="utf-8",
)
killed = []
monkeypatch.setattr(launcher, "IS_WINDOWS", False)
monkeypatch.setattr(launcher, "DATA_DIR", data_dir)
monkeypatch.setattr(launcher, "REPO_DIR", repo_dir)
monkeypatch.setattr(launcher, "pid_is_alive", lambda _pid: True)
monkeypatch.setattr(launcher, "process_group_id", lambda _pid: 22222)
monkeypatch.setattr(launcher, "process_command", lambda _pid: "/usr/bin/python unrelated.py")
monkeypatch.setattr(launcher, "kill_pid_tree", lambda pid, **kw: killed.append(("pid", pid)))
monkeypatch.setattr(launcher, "kill_process_group_id", lambda pgid, **kw: killed.append(("pgid", pgid)))
monkeypatch.setattr(launcher, "terminate_process_group_id", lambda pgid, **kw: killed.append(("term", pgid)))
launcher._cleanup_recorded_server_process("test")
assert killed == []
assert not (data_dir / "state" / "server_process.json").exists()
def test_recorded_server_cleanup_kills_verified_process_group(monkeypatch, tmp_path):
import launcher
data_dir = tmp_path / "data"
repo_dir = tmp_path / "repo"
(data_dir / "state").mkdir(parents=True)
repo_dir.mkdir()
server_py = repo_dir / "server.py"
server_py.write_text("print('server')\n", encoding="utf-8")
(data_dir / "state" / "server_process.json").write_text(
json.dumps({
"pid": 33333,
"pgid": 33333,
"server_path": str(server_py.resolve()),
"repo_dir": str(repo_dir.resolve()),
"port": 8765,
}),
encoding="utf-8",
)
killed = []
monkeypatch.setattr(launcher, "IS_WINDOWS", False)
monkeypatch.setattr(launcher, "DATA_DIR", data_dir)
monkeypatch.setattr(launcher, "REPO_DIR", repo_dir)
monkeypatch.setattr(launcher, "pid_is_alive", lambda _pid: True)
monkeypatch.setattr(launcher, "process_group_id", lambda _pid: 33333)
monkeypatch.setattr(launcher, "process_command", lambda _pid: f"{sys.executable} {server_py}")
monkeypatch.setattr(launcher, "current_process_group_id", lambda: 99999)
monkeypatch.setattr(launcher, "time", types.SimpleNamespace(sleep=lambda *_a, **_k: None))
monkeypatch.setattr(launcher, "terminate_process_group_id", lambda pgid, **kw: killed.append(("term", pgid)))
monkeypatch.setattr(launcher, "kill_process_group_id", lambda pgid, **kw: killed.append(("killpg", pgid)))
monkeypatch.setattr(launcher, "kill_pid_tree", lambda pid, **kw: killed.append(("pid", pid)))
launcher._cleanup_recorded_server_process("test")
assert ("term", 33333) in killed
assert ("killpg", 33333) in killed
assert ("pid", 33333) in killed
assert not (data_dir / "state" / "server_process.json").exists()
def test_recorded_server_cleanup_ignores_mismatched_process_group(monkeypatch, tmp_path):
import launcher
data_dir = tmp_path / "data"
repo_dir = tmp_path / "repo"
(data_dir / "state").mkdir(parents=True)
repo_dir.mkdir()
server_py = repo_dir / "server.py"
server_py.write_text("print('server')\n", encoding="utf-8")
record_path = data_dir / "state" / "server_process.json"
record_path.write_text(
json.dumps({
"pid": 44444,
"pgid": 55555,
"server_path": str(server_py.resolve()),
"repo_dir": str(repo_dir.resolve()),
"port": 8765,
}),
encoding="utf-8",
)
killed = []
monkeypatch.setattr(launcher, "IS_WINDOWS", False)
monkeypatch.setattr(launcher, "DATA_DIR", data_dir)
monkeypatch.setattr(launcher, "REPO_DIR", repo_dir)
monkeypatch.setattr(launcher, "pid_is_alive", lambda _pid: True)
monkeypatch.setattr(launcher, "process_group_id", lambda _pid: 44444)
monkeypatch.setattr(launcher, "process_command", lambda _pid: f"{sys.executable} {server_py}")
monkeypatch.setattr(launcher, "terminate_process_group_id", lambda pgid, **kw: killed.append(("term", pgid)))
monkeypatch.setattr(launcher, "kill_process_group_id", lambda pgid, **kw: killed.append(("killpg", pgid)))
monkeypatch.setattr(launcher, "kill_pid_tree", lambda pid, **kw: killed.append(("pid", pid)))
launcher._cleanup_recorded_server_process("test")
assert killed == []
assert not record_path.exists()
def test_update_server_process_record_port_records_actual_port(monkeypatch, tmp_path):
import launcher
data_dir = tmp_path / "data"
record_dir = data_dir / "state"
record_dir.mkdir(parents=True)
record_path = record_dir / "server_process.json"
record_path.write_text(
json.dumps({
"pid": 123,
"pgid": 123,
"server_path": "/tmp/server.py",
"repo_dir": "/tmp/repo",
"requested_port": 8765,
"port": 8765,
}),
encoding="utf-8",
)
monkeypatch.setattr(launcher, "DATA_DIR", data_dir)
launcher._update_server_process_record_port(123, 8769)
record = json.loads(record_path.read_text(encoding="utf-8"))
assert record["requested_port"] == 8765
assert record["port"] == 8769
assert record["port_updated_at"]
def test_start_agent_windows_assigns_job_before_resume_and_records(monkeypatch, tmp_path):
import launcher
data_dir = tmp_path / "data"
repo_dir = tmp_path / "repo"
repo_dir.mkdir()
(repo_dir / "server.py").write_text("print('server')\n", encoding="utf-8")
calls: list[tuple[str, object]] = []
class FakeStdout:
def readline(self):
return b""
class FakeProcess:
pid = 54321
stdout = FakeStdout()
def kill(self):
calls.append(("kill", self.pid))
def fake_popen(_cmd, **kwargs):
calls.append(("popen_flags", kwargs.get("creationflags")))
calls.append(("popen_env", kwargs.get("env", {})))
return FakeProcess()
monkeypatch.setattr(launcher, "IS_WINDOWS", True)
monkeypatch.setattr(launcher, "DATA_DIR", data_dir)
monkeypatch.setattr(launcher, "REPO_DIR", repo_dir)
monkeypatch.setattr(launcher, "EMBEDDED_PYTHON", sys.executable)
monkeypatch.setattr(launcher, "_CREATE_NEW_PROCESS_GROUP", 0x200)
monkeypatch.setattr(launcher, "_CREATE_SUSPENDED", 0x4)
monkeypatch.setattr(launcher, "_load_settings", lambda: {})
monkeypatch.setattr(launcher, "_apply_settings_to_env", lambda _settings: None)
monkeypatch.setattr(launcher, "_hidden_popen", fake_popen)
monkeypatch.setattr(launcher, "create_kill_on_close_job", lambda **kw: calls.append(("create_job", kw)) or "job")
monkeypatch.setattr(launcher, "assign_pid_to_job", lambda job, pid: calls.append(("assign", (job, pid))) or True)
monkeypatch.setattr(launcher, "resume_process", lambda pid: calls.append(("resume", pid)) or True)
proc = launcher.start_agent(port=8765)
assert proc.pid == 54321
env = next(value for key, value in calls if key == "popen_env")
assert env["PYTHONDONTWRITEBYTECODE"] == "1"
assert env["PYTHONPYCACHEPREFIX"] == str(data_dir / "state" / "pycache")
lifecycle_calls = [call for call in calls if call[0] != "popen_env"]
assert lifecycle_calls[:4] == [
("popen_flags", 0x204),
("create_job", {"allow_breakaway": True}),
("assign", ("job", 54321)),
("resume", 54321),
]
record = json.loads((data_dir / "state" / "server_process.json").read_text(encoding="utf-8"))
assert record["pid"] == 54321
assert record["port"] == 8765
def test_launcher_reexports_the_windows_runtime_leaf():
"""The Windows pythonnet/pywebview preparation left launcher.py whole.
Upstream's final cutoff grew launcher.py past this branch's 1500-line module
ceiling, so the Windows-only runtime preparation moved to its own leaf. It is
an extraction, not a rewrite: launcher.py re-exports the SAME objects under the
SAME names, so every prior importer, every monkeypatch target and the packaging
hook checks that read launcher.py's source see no change at all.
"""
import launcher
from ouroboros import launcher_windows_runtime
assert (
launcher._prepare_windows_webview_runtime
is launcher_windows_runtime._prepare_windows_webview_runtime
)
assert launcher._show_windows_message is launcher_windows_runtime._show_windows_message
# The DLL-directory handle list moved WITH its only reader and was never part of
# launcher.py's surface, so it is deliberately not re-exported.
assert not hasattr(launcher, "_windows_dll_dir_handles")
assert isinstance(launcher_windows_runtime._windows_dll_dir_handles, list)
def test_external_source_launcher_keeps_older_seed_version_and_local_head(tmp_path, monkeypatch):
import launcher
bootstrap = _reload_bootstrap()
src = _make_bundle_source(tmp_path)
seed, repo = tmp_path / 'seed', tmp_path / 'repo'
_write_bundle(src, seed)
(seed / 'VERSION').write_text('4.50.0-rc.2\n')
original_manifest = (seed / 'repo_bundle_manifest.json').read_bytes()
assert bootstrap.ensure_managed_repo(_make_context(seed, repo)) == 'created'
(repo / 'VERSION').write_text('4.50.1\n')
(repo / 'server.py').write_text("print('personal body')\n")
_run(['git', 'add', '-A'], cwd=repo)
_run(['git', 'commit', '-m', 'personal source update'], cwd=repo)
personal_head = _git_output(repo, 'rev-parse', 'HEAD')
monkeypatch.setattr(launcher, '_external_seed_bundle', seed)
monkeypatch.setattr(launcher, 'APP_VERSION', '4.50.1')
monkeypatch.setattr(launcher, 'REPO_DIR', repo)
monkeypatch.setattr(launcher, 'DATA_DIR', tmp_path / 'data')
monkeypatch.setattr(launcher, 'SETTINGS_PATH', tmp_path / 'data' / 'settings.json')
context = launcher._bootstrap_context()
assert context.app_version == '4.50.0-rc.2'
assert context.bundle_dir == seed
assert bootstrap.ensure_managed_repo(context) == 'unchanged'
assert _git_output(repo, 'rev-parse', 'HEAD') == personal_head
assert (repo / 'VERSION').read_text(encoding="utf-8").strip() == '4.50.1'
assert (seed / 'repo_bundle_manifest.json').read_bytes() == original_manifest
def test_external_restart_reexecs_current_source_after_sync_in_same_process(tmp_path, monkeypatch):
import launcher
class ReexecReached(BaseException):
pass
calls = []
process = types.SimpleNamespace(pid=12345, returncode=launcher.RESTART_EXIT_CODE,
wait=lambda: None)
monkeypatch.setattr(launcher, '_external_seed_bundle', tmp_path / 'seed')
monkeypatch.setattr(launcher, 'REPO_DIR', tmp_path / 'repo')
monkeypatch.setattr(launcher, '_launch_argv', ['--no-ui', '--seed-bundle', str(tmp_path / 'seed')])
monkeypatch.setattr(launcher, '_pre_generation_cleanup', lambda port: [])
monkeypatch.setattr(launcher, 'update_external_host', lambda *a: calls.append('native') or {})
monkeypatch.setattr(launcher, 'start_agent', lambda port: calls.append('core') or process)
monkeypatch.setattr(launcher, '_poll_port_file', lambda **kw: 8765)
monkeypatch.setattr(launcher, '_update_server_process_record_port', lambda *a: None)
monkeypatch.setattr(launcher, '_wait_for_server', lambda *a, **kw: True)
monkeypatch.setattr(launcher, '_cleanup_recorded_server_group_for_pid', lambda *a: None)
monkeypatch.setattr(launcher, '_sync_existing_repo_from_bundle', lambda: calls.append('source'))
monkeypatch.setattr(launcher, '_install_deps', lambda: calls.append('deps') or True)
monkeypatch.setattr(launcher, 'release_pid_lock', lambda: calls.append('release'))
monkeypatch.setattr(launcher.time, 'sleep', lambda *a: None)
def reexec(executable, argv):
calls.append(('exec', executable, argv))
raise ReexecReached()
monkeypatch.setattr(launcher.os, 'execv', reexec)
launcher._shutdown_event.clear()
launcher._agent_restart_requested.clear()
try:
try:
launcher.agent_lifecycle_loop()
except ReexecReached:
pass
else:
raise AssertionError('The same process did not re-exec the source launcher')
finally:
launcher._shutdown_event.clear()
assert calls[:5] == ['native', 'core', 'source', 'deps', 'release']
assert calls[5] == ('exec', launcher.EMBEDDED_PYTHON,
[launcher.EMBEDDED_PYTHON, str(tmp_path / 'repo' / 'launcher.py'),
'--no-ui', '--seed-bundle', str(tmp_path / 'seed')])