fix(coding-agent): probed all Python runtimes to bypass broken managed env

- Replaced single-candidate resolution with `enumeratePythonRuntimes`, returning venv, managed env, and system interpreter in priority order.
- Availability check now probes each candidate and falls through to the first that executes, so a stale `uv`-managed Python no longer fails the whole session.
- Kernel spawn reuses the probed runtime from the availability result instead of re-resolving independently.
- Expanded test coverage for enumeration, fallback, and env isolation between candidates.
This commit is contained in:
can1357
2026-05-31 01:58:31 +02:00
parent 5d3adfab7f
commit 06dc3976f0
5 changed files with 158 additions and 46 deletions
@@ -1,5 +1,8 @@
import { describe, expect, it } from "bun:test";
import { filterEnv } from "@oh-my-pi/pi-coding-agent/eval/py/runtime";
import { afterEach, describe, expect, it, vi } from "bun:test";
import * as fs from "node:fs";
import * as path from "node:path";
import { enumeratePythonRuntimes, filterEnv, resolvePythonRuntime } from "@oh-my-pi/pi-coding-agent/eval/py/runtime";
import * as piUtils from "@oh-my-pi/pi-utils";
describe("Python gateway environment filtering", () => {
it("filters sensitive and unknown variables from shell env", () => {
@@ -42,3 +45,59 @@ describe("Python gateway environment filtering", () => {
expect(filtered.LC_MESSAGES).toBe("en_US.UTF-8");
});
});
describe("enumeratePythonRuntimes", () => {
afterEach(() => {
vi.restoreAllMocks();
});
const managedDir = path.join(path.sep, "fake", ".omp", "python-env");
const managedBin = path.join(managedDir, process.platform === "win32" ? "Scripts" : "bin");
const managedPy = path.join(managedBin, process.platform === "win32" ? "python.exe" : "python");
const systemPy = path.join(path.sep, "usr", "bin", "python3");
it("enumerates the managed env AND the system interpreter so a broken managed env can fall through", () => {
vi.spyOn(piUtils, "getPythonEnvDir").mockReturnValue(managedDir);
vi.spyOn(piUtils, "$which").mockImplementation(bin => (bin === "python" ? systemPy : null));
// Only the managed interpreter physically exists; no project/active venv.
vi.spyOn(fs, "existsSync").mockImplementation(candidate => candidate === managedPy);
const runtimes = enumeratePythonRuntimes(path.join(path.sep, "work"), {
PATH: path.join(path.sep, "usr", "bin"),
});
expect(runtimes.map(r => r.pythonPath)).toEqual([managedPy, systemPy]);
const [managed, system] = runtimes;
expect(managed.venvPath).toBe(managedDir);
expect(managed.env.VIRTUAL_ENV).toBe(managedDir);
expect(managed.env.PATH).toBe(`${managedBin}${path.delimiter}${path.join(path.sep, "usr", "bin")}`);
// The system candidate must not inherit the managed env's VIRTUAL_ENV/PATH mutation.
expect(system.venvPath).toBeUndefined();
expect(system.env.VIRTUAL_ENV).toBeUndefined();
expect(system.env.PATH).toBe(path.join(path.sep, "usr", "bin"));
});
it("falls back to the system interpreter when no managed env or venv exists", () => {
vi.spyOn(piUtils, "getPythonEnvDir").mockReturnValue(managedDir);
vi.spyOn(piUtils, "$which").mockImplementation(bin => (bin === "python" ? systemPy : null));
vi.spyOn(fs, "existsSync").mockReturnValue(false);
const runtimes = enumeratePythonRuntimes(path.join(path.sep, "work"), {
PATH: path.join(path.sep, "usr", "bin"),
});
expect(runtimes.map(r => r.pythonPath)).toEqual([systemPy]);
expect(resolvePythonRuntime(path.join(path.sep, "work"), {}).pythonPath).toBe(systemPy);
});
it("throws from resolvePythonRuntime when no interpreter can be found", () => {
vi.spyOn(piUtils, "getPythonEnvDir").mockReturnValue(managedDir);
vi.spyOn(piUtils, "$which").mockReturnValue(null);
vi.spyOn(fs, "existsSync").mockReturnValue(false);
expect(enumeratePythonRuntimes(path.join(path.sep, "work"), {})).toEqual([]);
expect(() => resolvePythonRuntime(path.join(path.sep, "work"), {})).toThrow("Python executable not found");
});
});