18167c9d92
* 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>
170 lines
6.5 KiB
Python
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()
|