From 81fc88a3cfb1027c67913396e78f31254a93adb4 Mon Sep 17 00:00:00 2001 From: botica Date: Thu, 15 Jan 2026 05:06:13 -0600 Subject: [PATCH] fix(windows): resolve claude.cmd via shutil.which (#124) Co-authored-by: banteg <4562643+banteg@users.noreply.github.com> --- src/takopi/runners/claude.py | 3 ++- tests/test_claude_runner.py | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/takopi/runners/claude.py b/src/takopi/runners/claude.py index 7602178..5fc29c8 100644 --- a/src/takopi/runners/claude.py +++ b/src/takopi/runners/claude.py @@ -2,6 +2,7 @@ from __future__ import annotations import os import re +import shutil from dataclasses import dataclass, field from pathlib import Path from typing import Any @@ -449,7 +450,7 @@ class ClaudeRunner(ResumeTokenMixin, JsonlSubprocessRunner): def build_runner(config: EngineConfig, _config_path: Path) -> Runner: - claude_cmd = "claude" + claude_cmd = shutil.which("claude") or "claude" model = config.get("model") if "allowed_tools" in config: diff --git a/tests/test_claude_runner.py b/tests/test_claude_runner.py index c8c21a8..36c2465 100644 --- a/tests/test_claude_runner.py +++ b/tests/test_claude_runner.py @@ -1,9 +1,11 @@ import json from pathlib import Path +from typing import cast import anyio import pytest +import takopi.runners.claude as claude_runner from takopi.model import ActionEvent, CompletedEvent, ResumeToken, StartedEvent from takopi.runners.claude import ( ClaudeRunner, @@ -62,6 +64,21 @@ def test_claude_resume_format_and_extract() -> None: assert runner.extract_resume("`codex resume sid`") is None +def test_build_runner_uses_shutil_which(monkeypatch) -> None: + expected = r"C:\Tools\claude.cmd" + called: dict[str, str] = {} + + def fake_which(name: str) -> str | None: + called["name"] = name + return expected + + monkeypatch.setattr(claude_runner.shutil, "which", fake_which) + runner = cast(ClaudeRunner, claude_runner.build_runner({}, Path("takopi.toml"))) + + assert called["name"] == "claude" + assert runner.claude_cmd == expected + + def test_translate_success_fixture() -> None: state = ClaudeStreamState() events: list = []