Files
Corey Zumar 18167c9d92 Backwards-compat Config 2: old runner/host -> new server (#990)
* Add Config 2 backwards-compat: old runner/host -> new server

Mirror of the server-version harness for the agent side. Runner and host are
colocated (one install, one version), so a single knob pins both while the
server, client, and tests stay on main.

- tests/_helpers/compat.py: generalize the redirect into a component-parameterized
  core; add runner helpers (OMNIGENT_COMPAT_RUNNER_PYTHON): runner_executable,
  apply_runner_env (neutralize-only — drops the inherited worktree PYTHONPATH in
  compat mode, never force-adds a prepend), compat_runner_cwd, and the
  min_runner_version skip (pinned_runner_version reads OMNIGENT_COMPAT_RUNNER_VERSION;
  runner/host have no /api/version, so the env is the only source). server_* and
  the new runner_* are thin wrappers over the shared core.
- tests/e2e/conftest.py: redirect the runner subprocess (runner_executable +
  apply_runner_env + cwd=compat_runner_cwd); add the runner_version fixture's
  min_runner_version autouse guard; re-exported into tests/integration.
- Redirect all four host-daemon spawns (test_host_e2e x2, claude-native,
  codex-native) the same way so the OLD host launches OLD runners (colocated).
- min_runner_version marker registered in pyproject.
- Composite actions gain a runner_version input (build the old runner/host venv,
  export the redirect env vars); server-compat.yml adds backcompat-runner-{e2e,
  integration} jobs and is renamed Backwards-Compat (now both directions).

The server and runner knobs are orthogonal: each spawn site consults its own,
so a run pins exactly one component.

Out of scope (documented): the 3 niche custom-fixture direct-runner spawns
(filesystem/non-git changed-files, session_resources) keep their workspace-cwd
semantics and stay on the test python; tests/e2e_ui (needs an npm build). Both
run new-runner -> new-server (normal, no breakage) in a Config-2 run.

Verified: 26 unit tests; lint/format clean; both conftests import; and the
redirect provably loads OLD runner code (import omnigent.runner._entry resolves
to the pinned old source only with both the PYTHONPATH drop and the neutral CWD;
either counterfactual loads main).

Signed-off-by: dbczumar <corey.zumar@databricks.com>

* TEMP: enable Backwards-Compat on PR (REVERT before merge)

workflow_dispatch needs the file on the default branch (not merged yet). Add a
pull_request trigger so the backcompat jobs (server + runner directions) run on
this PR for validation. Reverted before merge.

Signed-off-by: dbczumar <corey.zumar@databricks.com>

* Revert temporary PR trigger on Backwards-Compat workflow

Config-2 backcompat validated on the PR (old runner/host -> new server: all
e2e shards + integration green). Restore dispatch/nightly-only triggers — the
backcompat sweep is not meant to run on every PR push.

Signed-off-by: dbczumar <corey.zumar@databricks.com>

* Clarify backcompat job labels: 'latest' -> 'latest-release'

The fallback label read as 'newest/main' but means the latest released TAG —
which is older than main (unreleased). Rename so the job name ('server
latest-release') reconciles with the step ('against old server'): same pinned
release, older than the code under test.

Signed-off-by: dbczumar <corey.zumar@databricks.com>

---------

Signed-off-by: dbczumar <corey.zumar@databricks.com>
2026-06-22 21:50:20 -07:00

170 lines
6.5 KiB
Python

"""
Unit tests for the server-version backwards-compat helpers
(:mod:`tests._helpers.compat`). Pure logic only — no live server.
See ``docs/SERVER_VERSION_COMPAT_CI.md``.
"""
from __future__ import annotations
import os
import sys
import pytest
from tests._helpers import compat
@pytest.mark.parametrize(
("version", "expected"),
[
("0.1.1", (0, 1, 1)),
("0.1.2.dev0", (0, 1, 2)),
("0.1.2rc1", (0, 1, 2)),
("1.2.3.post4", (1, 2, 3)),
("2.0", (2, 0)),
],
)
def test_release_tuple_ignores_suffixes(version: str, expected: tuple[int, ...]) -> None:
assert compat.release_tuple(version) == expected
@pytest.mark.parametrize(
("server", "required", "expected"),
[
# Dev version of X must satisfy a feature gated on X (the whole
# reason we compare release tuples, not full PEP 440 ordering).
("0.1.2.dev0", "0.1.2", True),
# Equal releases.
("0.1.2", "0.1.2", True),
# Newer server runs older-gated features.
("0.2.0", "0.1.2", True),
# Old server skips a newer feature.
("0.1.1", "0.1.2", False),
("0.1.2.dev0", "0.1.3", False),
],
)
def test_meets_min_server_version(server: str, required: str, expected: bool) -> None:
assert compat.meets_min_server_version(server, required) is expected
@pytest.mark.parametrize(
("reported", "override", "expected"),
[
# /api/version is source of truth.
("0.1.1", None, "0.1.1"),
# Backstop used only when the report is missing.
(None, "0.1.1", "0.1.1"),
# Agreement (dev vs final of the same release counts as agreeing).
("0.1.2.dev0", "0.1.2", "0.1.2.dev0"),
("0.1.1", "0.1.1", "0.1.1"),
],
)
def test_reconcile_server_version_ok(
reported: str | None, override: str | None, expected: str
) -> None:
assert compat.reconcile_server_version(reported, override) == expected
def test_reconcile_server_version_disagreement_raises() -> None:
# The PYTHONPATH-shadow tripwire: report and pinned version differ.
with pytest.raises(RuntimeError, match="version mismatch"):
compat.reconcile_server_version("0.1.2.dev0", "0.1.1")
def test_reconcile_server_version_unreadable_without_backstop_raises() -> None:
with pytest.raises(RuntimeError, match="could not read"):
compat.reconcile_server_version(None, None, source="http://localhost:6767")
def test_server_redirect_inert_without_env(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv(compat.COMPAT_SERVER_PYTHON_ENV, raising=False)
monkeypatch.delenv("PYTHONPATH", raising=False)
assert compat.compat_server_python() is None
assert compat.server_executable() == sys.executable
# Inherit CWD (None) outside compat mode.
assert compat.compat_server_cwd() is None
# Normal mode prepends the worktree root to PYTHONPATH.
env: dict[str, str] = {}
compat.apply_server_env(env, "/repo/root")
assert env["PYTHONPATH"].startswith(f"/repo/root{os.pathsep}")
def test_server_redirect_active_with_env(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv(compat.COMPAT_SERVER_PYTHON_ENV, "/srv-venv/bin/python")
assert compat.compat_server_python() == "/srv-venv/bin/python"
assert compat.server_executable() == "/srv-venv/bin/python"
# Compat mode drops the worktree prepend so the pinned install resolves.
env = {"PYTHONPATH": "/repo/root:/preexisting"}
compat.apply_server_env(env, "/repo/root")
assert "PYTHONPATH" not in env
# Compat mode runs the server from a neutral dir that does NOT contain an
# omnigent/ package (else CWD on sys.path[0] would shadow the old install).
cwd = compat.compat_server_cwd()
assert cwd is not None
assert os.path.isdir(cwd)
assert not os.path.exists(os.path.join(cwd, "omnigent"))
# ── Runner / host redirect (Config 2) ──────────────────────────────────
@pytest.mark.parametrize(
("runner", "required", "expected"),
[
("0.2.0.dev0", "0.2.0", True),
("0.2.0", "0.2.0", True),
("0.3.0", "0.2.1", True),
("0.2.0", "0.2.1", False),
],
)
def test_meets_min_runner_version(runner: str, required: str, expected: bool) -> None:
assert compat.meets_min_runner_version(runner, required) is expected
def test_pinned_runner_version_env_driven(monkeypatch: pytest.MonkeyPatch) -> None:
# No env -> None (normal runs: "newest", skip nothing).
monkeypatch.delenv(compat.COMPAT_RUNNER_VERSION_ENV, raising=False)
assert compat.pinned_runner_version() is None
monkeypatch.setenv(compat.COMPAT_RUNNER_VERSION_ENV, "0.2.0")
assert compat.pinned_runner_version() == "0.2.0"
def test_runner_redirect_inert_without_env(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv(compat.COMPAT_RUNNER_PYTHON_ENV, raising=False)
assert compat.compat_runner_python() is None
assert compat.runner_executable() == sys.executable
assert compat.compat_runner_cwd() is None
# Neutralize-only: outside compat mode a pre-set PYTHONPATH is left as-is
# (NOT dropped, NOT a prepend added).
env = {"PYTHONPATH": "/repo/root:/x"}
compat.apply_runner_env(env)
assert env["PYTHONPATH"] == "/repo/root:/x"
# And an env without PYTHONPATH stays without one (no prepend imposed).
bare: dict[str, str] = {}
compat.apply_runner_env(bare)
assert "PYTHONPATH" not in bare
def test_runner_redirect_active_with_env(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv(compat.COMPAT_RUNNER_PYTHON_ENV, "/old-agent/bin/python")
assert compat.compat_runner_python() == "/old-agent/bin/python"
assert compat.runner_executable() == "/old-agent/bin/python"
# Compat mode drops the inherited worktree PYTHONPATH so the pinned old
# runner/host resolves.
env = {"PYTHONPATH": "/repo/root:/x"}
compat.apply_runner_env(env)
assert "PYTHONPATH" not in env
# Neutral CWD with no omnigent/ package (mirrors the server cwd guard).
cwd = compat.compat_runner_cwd()
assert cwd is not None
assert os.path.isdir(cwd)
assert not os.path.exists(os.path.join(cwd, "omnigent"))
def test_server_and_runner_cwds_are_distinct(monkeypatch: pytest.MonkeyPatch) -> None:
# The two knobs are orthogonal; their neutral CWDs must not collide.
monkeypatch.setenv(compat.COMPAT_SERVER_PYTHON_ENV, "/srv/bin/python")
monkeypatch.setenv(compat.COMPAT_RUNNER_PYTHON_ENV, "/old-agent/bin/python")
assert compat.compat_server_cwd() != compat.compat_runner_cwd()