Merge remote-tracking branch 'origin/farm/f8778fdf/ruff-lsp-windows-venv-scripts'

This commit is contained in:
can1357
2026-06-30 23:53:19 +02:00
4 changed files with 105 additions and 47 deletions
+3
View File
@@ -19,6 +19,9 @@
### Fixed
- Fixed subagent frontmatter `thinkingLevel` being overridden by `modelRoles.task` model suffixes. ([#3915](https://github.com/can1357/oh-my-pi/issues/3915))
### Fixed
- Fixed Ruff LSP auto-detection for Windows Python virtualenvs by checking `.venv/Scripts`, `venv/Scripts`, and `.env/Scripts` before falling back to PATH. ([#3916](https://github.com/can1357/oh-my-pi/issues/3916))
## [16.2.9] - 2026-06-30
+17 -3
View File
@@ -209,13 +209,27 @@ export function hasRootMarkers(cwd: string, markers: string[]): boolean {
* Local bin directories to check before $PATH, ordered by priority.
* Each entry maps a root marker to the bin directory to check.
*/
const PYTHON_ROOT_MARKERS = [
"pyproject.toml",
"requirements.txt",
"setup.py",
"setup.cfg",
"Pipfile",
"pyrightconfig.json",
"ruff.toml",
".ruff.toml",
];
const LOCAL_BIN_PATHS: Array<{ markers: string[]; binDir: string }> = [
// Node.js - check node_modules/.bin/
{ markers: ["package.json", "package-lock.json", "yarn.lock", "pnpm-lock.yaml"], binDir: "node_modules/.bin" },
// Python - check virtual environment bin directories
{ markers: ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile"], binDir: ".venv/bin" },
{ markers: ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile"], binDir: "venv/bin" },
{ markers: ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile"], binDir: ".env/bin" },
{ markers: PYTHON_ROOT_MARKERS, binDir: ".venv/bin" },
{ markers: PYTHON_ROOT_MARKERS, binDir: ".venv/Scripts" },
{ markers: PYTHON_ROOT_MARKERS, binDir: "venv/bin" },
{ markers: PYTHON_ROOT_MARKERS, binDir: "venv/Scripts" },
{ markers: PYTHON_ROOT_MARKERS, binDir: ".env/bin" },
{ markers: PYTHON_ROOT_MARKERS, binDir: ".env/Scripts" },
// Ruby - check vendor bundle and binstubs
{ markers: ["Gemfile", "Gemfile.lock"], binDir: "vendor/bundle/bin" },
{ markers: ["Gemfile", "Gemfile.lock"], binDir: "bin" },
@@ -465,50 +465,6 @@ describe("runSubprocess yield reminders", () => {
expect(createAgentSessionSpy).toHaveBeenCalledTimes(1);
expect(createAgentSessionSpy.mock.calls[0]?.[0]?.thinkingLevel).toBe(Effort.High);
});
it("prefers explicit modelOverride thinking suffix over provided thinking level, including off", async () => {
vi.clearAllMocks();
const modelRegistry = {
refresh: async () => {},
getAvailable: () => [{ provider: "openai", id: "gpt-4o", name: "GPT-4o" }],
} as unknown as import("@oh-my-pi/pi-coding-agent/config/model-registry").ModelRegistry;
const cases = [
{ modelOverride: "openai/gpt-4o:low", expectedThinkingLevel: Effort.Low },
{ modelOverride: "openai/gpt-4o:off", expectedThinkingLevel: "off" },
] as const;
const createAgentSessionSpy = vi.spyOn(sdkModule, "createAgentSession");
for (const [index, testCase] of cases.entries()) {
const session = createMockSession(({ emit }) => {
emit({
type: "tool_execution_end",
toolCallId: `tool-thinking-override-${index}`,
toolName: "yield",
result: {
content: [{ type: "text", text: "Result submitted." }],
details: { status: "success", data: { ok: true } },
},
isError: false,
});
});
createAgentSessionSpy.mockResolvedValue(createSessionResult(session));
await runSubprocess({
...baseOptions,
id: `subagent-thinking-override-${index}`,
modelOverride: testCase.modelOverride,
thinkingLevel: Effort.High,
modelRegistry,
});
}
expect(createAgentSessionSpy).toHaveBeenCalledTimes(2);
expect(createAgentSessionSpy.mock.calls[0]?.[0]?.thinkingLevel).toBe(cases[0].expectedThinkingLevel);
expect(createAgentSessionSpy.mock.calls[1]?.[0]?.thinkingLevel).toBe(cases[1].expectedThinkingLevel);
});
it("fails after 3 reminders when yield is never called for a structured task", async () => {
const prompts: string[] = [];
const session = createMockSession(({ text, promptIndex, emit, state }) => {
@@ -1077,6 +1077,91 @@ describe("lsp regressions", () => {
}
});
it("detects Ruff in Windows virtualenv Scripts directories", async () => {
const originalPlatform = process.platform;
Object.defineProperty(process, "platform", { value: "win32", configurable: true, writable: true });
const tempDir = TempDir.createSync("@omp-lsp-win32-ruff-");
const whichSpy = vi.spyOn(Bun, "which").mockReturnValue(null);
try {
await Bun.write(path.join(tempDir.path(), "pyproject.toml"), '[project]\nname = "demo"\n');
const scriptsDir = path.join(tempDir.path(), ".venv", "Scripts");
await fs.promises.mkdir(scriptsDir, { recursive: true });
const localRuff = path.join(scriptsDir, "ruff.exe");
await Bun.write(localRuff, "");
const config = loadConfig(tempDir.path());
expect(config.servers.ruff?.resolvedCommand).toBe(localRuff);
expect(whichSpy).not.toHaveBeenCalledWith("ruff");
} finally {
Object.defineProperty(process, "platform", { value: originalPlatform, configurable: true, writable: true });
vi.restoreAllMocks();
tempDir.removeSync();
}
});
it("detects Ruff in Windows virtualenv Scripts directories for Ruff-only roots", async () => {
const originalPlatform = process.platform;
Object.defineProperty(process, "platform", { value: "win32", configurable: true, writable: true });
const whichSpy = vi.spyOn(Bun, "which").mockReturnValue(null);
try {
for (const marker of ["ruff.toml", ".ruff.toml"] as const) {
const tempDir = TempDir.createSync("@omp-lsp-win32-ruff-marker-");
try {
await Bun.write(path.join(tempDir.path(), marker), "");
const scriptsDir = path.join(tempDir.path(), ".venv", "Scripts");
await fs.promises.mkdir(scriptsDir, { recursive: true });
const localRuff = path.join(scriptsDir, "ruff.exe");
await Bun.write(localRuff, "");
const config = loadConfig(tempDir.path());
expect(config.servers.ruff?.resolvedCommand).toBe(localRuff);
} finally {
tempDir.removeSync();
}
}
expect(whichSpy).not.toHaveBeenCalledWith("ruff");
} finally {
Object.defineProperty(process, "platform", { value: originalPlatform, configurable: true, writable: true });
vi.restoreAllMocks();
}
});
it("detects pyright and pylsp in Windows virtualenv Scripts for Python-only roots", async () => {
const originalPlatform = process.platform;
Object.defineProperty(process, "platform", { value: "win32", configurable: true, writable: true });
const whichSpy = vi.spyOn(Bun, "which").mockReturnValue(null);
try {
const cases: Array<{ marker: string; server: string; binary: string }> = [
{ marker: "pyrightconfig.json", server: "pyright", binary: "pyright-langserver.exe" },
{ marker: "setup.cfg", server: "pylsp", binary: "pylsp.exe" },
];
for (const { marker, server, binary } of cases) {
const tempDir = TempDir.createSync("@omp-lsp-win32-py-marker-");
try {
await Bun.write(path.join(tempDir.path(), marker), "");
const scriptsDir = path.join(tempDir.path(), ".venv", "Scripts");
await fs.promises.mkdir(scriptsDir, { recursive: true });
const localBin = path.join(scriptsDir, binary);
await Bun.write(localBin, "");
const config = loadConfig(tempDir.path());
expect(config.servers[server]?.resolvedCommand).toBe(localBin);
} finally {
tempDir.removeSync();
}
}
expect(whichSpy).not.toHaveBeenCalledWith("pyright-langserver");
expect(whichSpy).not.toHaveBeenCalledWith("pylsp");
} finally {
Object.defineProperty(process, "platform", { value: originalPlatform, configurable: true, writable: true });
vi.restoreAllMocks();
}
});
it("detects tlaplus files for LSP startup and language ids", async () => {
const tempDir = TempDir.createSync("@omp-lsp-tlaplus-");
const specPath = path.join(tempDir.path(), "Spec.tla");