f3d80b4628
Auto Release / auto-release (push) Failing after 1s
CI / lint (push) Failing after 0s
CI / screenshot-quality (push) Failing after 3s
CI / pr-policy (push) Has been skipped
CI / test (3.11) (push) Failing after 0s
CI / test (3.12) (push) Failing after 0s
CI / test (3.13) (push) Failing after 3s
CI / coverage (push) Failing after 1s
Legibility / legibility (push) Failing after 3s
913 lines
33 KiB
Python
913 lines
33 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import json
|
|
import shutil
|
|
import tomllib
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from claude_tap import parse_args
|
|
from claude_tap.cli_clients import (
|
|
_KIMI_CODE_SKIP_MIGRATION_MARKER,
|
|
CLIENT_CONFIGS,
|
|
_detect_kimi_code_target,
|
|
_kimi_code_migration_already_handled,
|
|
_materialize_kimi_code_session_index,
|
|
_merge_kimi_code_session_index,
|
|
_normalize_kimi_code_fs_path,
|
|
_patch_kimi_code_config_text,
|
|
_persist_kimi_code_sandbox,
|
|
_prepare_kimi_code_reverse_sandbox,
|
|
_remap_kimi_code_sandbox_paths,
|
|
_reverse_proxy_trace_options,
|
|
run_client,
|
|
)
|
|
|
|
|
|
class _DummyProc:
|
|
def __init__(self) -> None:
|
|
self.pid = 12345
|
|
self.returncode: int | None = None
|
|
|
|
async def wait(self) -> int:
|
|
self.returncode = 0
|
|
return 0
|
|
|
|
def terminate(self) -> None:
|
|
self.returncode = 0
|
|
|
|
def kill(self) -> None:
|
|
self.returncode = -9
|
|
|
|
|
|
def test_kimi_code_registered_in_client_configs() -> None:
|
|
cfg = CLIENT_CONFIGS["kimi-code"]
|
|
assert cfg.cmd == "kimi"
|
|
assert cfg.label == "Kimi Code CLI"
|
|
assert cfg.install_url == "https://github.com/MoonshotAI/kimi-code"
|
|
assert cfg.base_url_env == "KIMI_CODE_BASE_URL"
|
|
assert cfg.default_target == "https://api.kimi.com/coding/v1"
|
|
assert cfg.default_proxy_mode == "reverse"
|
|
|
|
|
|
def test_kimi_legacy_unchanged_in_client_configs() -> None:
|
|
cfg = CLIENT_CONFIGS["kimi"]
|
|
assert cfg.base_url_env == "KIMI_BASE_URL"
|
|
assert cfg.install_url == "https://github.com/MoonshotAI/kimi-cli"
|
|
|
|
|
|
def test_parse_args_kimi_code_defaults_to_reverse_mode() -> None:
|
|
args = parse_args(["--tap-client", "kimi-code"])
|
|
assert args.client == "kimi-code"
|
|
assert args.target == "https://api.kimi.com/coding/v1"
|
|
assert args.proxy_mode == "reverse"
|
|
|
|
|
|
def test_detect_kimi_code_target_reads_managed_provider(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
|
home = tmp_path / "kimi-code-home"
|
|
home.mkdir()
|
|
(home / "config.toml").write_text(
|
|
"""
|
|
default_model = "kimi-code/kimi-for-coding"
|
|
|
|
[providers."managed:kimi-code"]
|
|
type = "kimi"
|
|
base_url = "https://api.kimi.com/coding/v1"
|
|
api_key = ""
|
|
|
|
[models."kimi-code/kimi-for-coding"]
|
|
provider = "managed:kimi-code"
|
|
model = "kimi-for-coding"
|
|
max_context_size = 262144
|
|
""".strip(),
|
|
encoding="utf-8",
|
|
)
|
|
monkeypatch.setenv("KIMI_CODE_HOME", str(home))
|
|
assert _detect_kimi_code_target() == "https://api.kimi.com/coding/v1"
|
|
|
|
|
|
def test_detect_kimi_code_target_uses_shell_base_url(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setenv("KIMI_BASE_URL", "https://gateway.example.com/v1")
|
|
|
|
assert _detect_kimi_code_target() == "https://gateway.example.com/v1"
|
|
|
|
|
|
def test_detect_kimi_code_target_uses_kimi_code_base_url(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setenv("KIMI_CODE_BASE_URL", "https://managed.example.com/coding/v1")
|
|
|
|
assert _detect_kimi_code_target() == "https://managed.example.com/coding/v1"
|
|
|
|
|
|
def test_detect_kimi_code_target_model_arg_overrides_stale_env(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
|
home = tmp_path / "kimi-code-home"
|
|
home.mkdir()
|
|
(home / "config.toml").write_text(
|
|
"""
|
|
[models.selected]
|
|
provider = "managed:kimi-code"
|
|
model = "kimi-for-coding"
|
|
|
|
[providers."managed:kimi-code"]
|
|
type = "kimi"
|
|
base_url = "https://selected.example.com/coding/v1"
|
|
""".strip(),
|
|
encoding="utf-8",
|
|
)
|
|
monkeypatch.setenv("KIMI_CODE_HOME", str(home))
|
|
monkeypatch.setenv("KIMI_MODEL_NAME", "stale")
|
|
monkeypatch.setenv("KIMI_MODEL_BASE_URL", "https://stale.example.com/v1")
|
|
|
|
assert _detect_kimi_code_target(["-m", "selected"]) == "https://selected.example.com/coding/v1"
|
|
|
|
|
|
def test_detect_kimi_code_target_model_arg_without_base_url_uses_default(
|
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
) -> None:
|
|
home = tmp_path / "kimi-code-home"
|
|
home.mkdir()
|
|
(home / "config.toml").write_text(
|
|
"""
|
|
[models.selected]
|
|
provider = "managed:kimi-code"
|
|
model = "kimi-for-coding"
|
|
|
|
[providers."managed:kimi-code"]
|
|
type = "kimi"
|
|
""".strip(),
|
|
encoding="utf-8",
|
|
)
|
|
monkeypatch.setenv("KIMI_CODE_HOME", str(home))
|
|
monkeypatch.setenv("KIMI_MODEL_BASE_URL", "https://stale.example.com/v1")
|
|
|
|
assert _detect_kimi_code_target(["-m", "selected"]) == "https://api.kimi.com/coding/v1"
|
|
|
|
|
|
def test_detect_kimi_code_target_ignores_inactive_model_base_url(
|
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
) -> None:
|
|
home = tmp_path / "kimi-code-home"
|
|
home.mkdir()
|
|
(home / "config.toml").write_text(
|
|
"""
|
|
default_model = "selected"
|
|
|
|
[models.selected]
|
|
provider = "managed:kimi-code"
|
|
model = "kimi-for-coding"
|
|
|
|
[providers."managed:kimi-code"]
|
|
type = "kimi"
|
|
base_url = "https://selected.example.com/coding/v1"
|
|
""".strip(),
|
|
encoding="utf-8",
|
|
)
|
|
monkeypatch.setenv("KIMI_CODE_HOME", str(home))
|
|
monkeypatch.setenv("KIMI_MODEL_BASE_URL", "https://stale.example.com/v1")
|
|
|
|
assert _detect_kimi_code_target() == "https://selected.example.com/coding/v1"
|
|
|
|
|
|
def test_detect_kimi_code_target_reads_config_file_arg(tmp_path: Path) -> None:
|
|
override_config = tmp_path / "override.toml"
|
|
override_config.write_text(
|
|
"""
|
|
default_model = "custom/model"
|
|
|
|
[providers."custom"]
|
|
type = "kimi"
|
|
base_url = "https://custom.example.com/v1"
|
|
|
|
[models."custom/model"]
|
|
provider = "custom"
|
|
model = "model"
|
|
""".strip(),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
assert _detect_kimi_code_target(["--config-file", str(override_config)]) == "https://custom.example.com/v1"
|
|
|
|
|
|
def test_patch_kimi_code_config_text_rewrites_provider_base_url() -> None:
|
|
source = """
|
|
[providers."managed:kimi-code"]
|
|
type = "kimi"
|
|
base_url = "https://api.kimi.com/coding/v1"
|
|
api_key = ""
|
|
""".strip()
|
|
patched, providers = _patch_kimi_code_config_text(source, "http://127.0.0.1:43123")
|
|
assert 'base_url = "http://127.0.0.1:43123"' in patched
|
|
assert providers == ["managed:kimi-code"]
|
|
|
|
|
|
def test_patch_kimi_code_config_text_rewrites_custom_gateway_provider() -> None:
|
|
source = """
|
|
[providers."custom-gateway"]
|
|
type = "kimi"
|
|
base_url="https://gateway.example.com/v1"
|
|
api_key = ""
|
|
""".strip()
|
|
patched, providers = _patch_kimi_code_config_text(source, "http://127.0.0.1:43123")
|
|
assert 'base_url="http://127.0.0.1:43123"' in patched
|
|
assert providers == ["custom-gateway"]
|
|
|
|
|
|
def test_patch_kimi_code_config_text_rewrites_env_kimi_base_url() -> None:
|
|
source = """
|
|
[providers."env-only"]
|
|
type = "kimi"
|
|
|
|
[providers."env-only".env]
|
|
KIMI_BASE_URL = "https://gateway.example.com/v1"
|
|
""".strip()
|
|
patched, providers = _patch_kimi_code_config_text(source, "http://127.0.0.1:43123")
|
|
assert 'KIMI_BASE_URL = "http://127.0.0.1:43123"' in patched
|
|
assert providers == ["env-only"]
|
|
|
|
|
|
def test_patch_kimi_code_config_text_rewrites_inline_comments() -> None:
|
|
source = """
|
|
[providers."managed:kimi-code"]
|
|
type = "kimi"
|
|
base_url = "https://api.kimi.com/coding/v1" # production endpoint
|
|
api_key = ""
|
|
""".strip()
|
|
patched, providers = _patch_kimi_code_config_text(source, "http://127.0.0.1:43123")
|
|
|
|
assert 'base_url = "http://127.0.0.1:43123" # production endpoint' in patched
|
|
assert providers == ["managed:kimi-code"]
|
|
|
|
|
|
def test_patch_kimi_code_config_text_inserts_missing_provider_base_url() -> None:
|
|
source = """
|
|
[providers."managed:kimi-code"]
|
|
type = "kimi"
|
|
api_key = ""
|
|
""".strip()
|
|
patched, providers = _patch_kimi_code_config_text(source, "http://127.0.0.1:43123")
|
|
|
|
assert '[providers."managed:kimi-code"]\nbase_url = "http://127.0.0.1:43123"\n' in patched
|
|
assert providers == ["managed:kimi-code"]
|
|
|
|
|
|
def test_patch_kimi_code_config_text_only_rewrites_selected_provider() -> None:
|
|
source = """
|
|
default_model = "custom/model"
|
|
|
|
[providers."managed:kimi-code"]
|
|
type = "kimi"
|
|
base_url = "https://api.kimi.com/coding/v1"
|
|
api_key = ""
|
|
|
|
[providers."custom"]
|
|
type = "kimi"
|
|
base_url = "https://gateway.example.com/v1"
|
|
api_key = ""
|
|
|
|
[models."custom/model"]
|
|
provider = "custom"
|
|
model = "model"
|
|
max_context_size = 1000
|
|
""".strip()
|
|
patched, providers = _patch_kimi_code_config_text(source, "http://127.0.0.1:43123")
|
|
|
|
assert 'base_url = "https://api.kimi.com/coding/v1"' in patched
|
|
assert 'base_url = "http://127.0.0.1:43123"' in patched
|
|
assert providers == ["custom"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_client_kimi_code_reverse_sets_kimi_code_home(
|
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
) -> None:
|
|
captured: dict[str, object] = {}
|
|
home = tmp_path / "source-home"
|
|
home.mkdir()
|
|
(home / "config.toml").write_text(
|
|
"""
|
|
[providers."managed:kimi-code"]
|
|
type = "kimi"
|
|
base_url = "https://api.kimi.com/coding/v1"
|
|
api_key = "sk-test"
|
|
""".strip(),
|
|
encoding="utf-8",
|
|
)
|
|
monkeypatch.setenv("KIMI_CODE_HOME", str(home))
|
|
|
|
async def fake_create_subprocess_exec(*cmd, **kwargs):
|
|
captured["cmd"] = cmd
|
|
captured["env"] = kwargs["env"]
|
|
return _DummyProc()
|
|
|
|
monkeypatch.setattr("claude_tap.cli.shutil.which", lambda _: "/tmp/kimi")
|
|
monkeypatch.setattr(asyncio, "create_subprocess_exec", fake_create_subprocess_exec)
|
|
monkeypatch.setattr("sys.stdin.isatty", lambda: False)
|
|
monkeypatch.setattr("claude_tap.cli_clients.shutil.rmtree", lambda *_args, **_kwargs: None)
|
|
|
|
code = await run_client(43123, ["--thinking"], client="kimi-code", proxy_mode="reverse")
|
|
|
|
assert code == 0
|
|
assert captured["cmd"] == ("/tmp/kimi", "--thinking")
|
|
env = captured["env"]
|
|
sandbox = Path(env["KIMI_CODE_HOME"])
|
|
assert sandbox.is_dir()
|
|
assert env["KIMI_CODE_BASE_URL"] == "http://127.0.0.1:43123"
|
|
assert env["KIMI_BASE_URL"] == "http://127.0.0.1:43123"
|
|
config = tomllib.loads((sandbox / "config.toml").read_text(encoding="utf-8"))
|
|
provider = config["providers"]["managed:kimi-code"]
|
|
assert provider["base_url"] == "http://127.0.0.1:43123"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_client_kimi_code_reverse_rewrites_config_file_arg(
|
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
) -> None:
|
|
captured: dict[str, object] = {}
|
|
home = tmp_path / "source-home"
|
|
home.mkdir()
|
|
override_config = tmp_path / "override.toml"
|
|
override_config.write_text(
|
|
"""
|
|
[providers."managed:kimi-code"]
|
|
type = "kimi"
|
|
base_url = "https://gateway.example.com/v1"
|
|
api_key = "sk-test"
|
|
""".strip(),
|
|
encoding="utf-8",
|
|
)
|
|
monkeypatch.setenv("KIMI_CODE_HOME", str(home))
|
|
|
|
async def fake_create_subprocess_exec(*cmd, **kwargs):
|
|
captured["cmd"] = cmd
|
|
captured["env"] = kwargs["env"]
|
|
return _DummyProc()
|
|
|
|
monkeypatch.setattr("claude_tap.cli.shutil.which", lambda _: "/tmp/kimi")
|
|
monkeypatch.setattr(asyncio, "create_subprocess_exec", fake_create_subprocess_exec)
|
|
monkeypatch.setattr("sys.stdin.isatty", lambda: False)
|
|
monkeypatch.setattr("claude_tap.cli_clients.shutil.rmtree", lambda *_args, **_kwargs: None)
|
|
|
|
code = await run_client(
|
|
43123,
|
|
["--config-file", str(override_config), "--prompt", "hi"],
|
|
client="kimi-code",
|
|
proxy_mode="reverse",
|
|
)
|
|
|
|
assert code == 0
|
|
cmd = captured["cmd"]
|
|
assert cmd[1] == "--config-file"
|
|
patched_config = Path(cmd[2])
|
|
assert "claude_tap_kimi_code_" in str(patched_config)
|
|
assert patched_config.read_text(encoding="utf-8").count("http://127.0.0.1:43123") == 1
|
|
assert str(override_config) not in cmd
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_client_kimi_code_reverse_rewrites_model_env_override(
|
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
) -> None:
|
|
captured: dict[str, object] = {}
|
|
home = tmp_path / "source-home"
|
|
home.mkdir()
|
|
monkeypatch.setenv("KIMI_CODE_HOME", str(home))
|
|
monkeypatch.setenv("KIMI_MODEL_NAME", "env-model")
|
|
monkeypatch.setenv("KIMI_MODEL_BASE_URL", "https://gateway.example.com/v1")
|
|
|
|
async def fake_create_subprocess_exec(*cmd, **kwargs):
|
|
captured["env"] = kwargs["env"]
|
|
return _DummyProc()
|
|
|
|
monkeypatch.setattr("claude_tap.cli.shutil.which", lambda _: "/tmp/kimi")
|
|
monkeypatch.setattr(asyncio, "create_subprocess_exec", fake_create_subprocess_exec)
|
|
monkeypatch.setattr("sys.stdin.isatty", lambda: False)
|
|
|
|
code = await run_client(43123, ["--prompt", "hi"], client="kimi-code", proxy_mode="reverse")
|
|
|
|
assert code == 0
|
|
env = captured["env"]
|
|
assert env["KIMI_MODEL_BASE_URL"] == "http://127.0.0.1:43123"
|
|
assert env["KIMI_BASE_URL"] == "http://127.0.0.1:43123"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_client_kimi_code_reverse_drops_inactive_model_base_url(
|
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
) -> None:
|
|
captured: dict[str, object] = {}
|
|
home = tmp_path / "source-home"
|
|
home.mkdir()
|
|
monkeypatch.setenv("KIMI_CODE_HOME", str(home))
|
|
monkeypatch.setenv("KIMI_MODEL_BASE_URL", "https://stale.example.com/v1")
|
|
|
|
async def fake_create_subprocess_exec(*cmd, **kwargs):
|
|
captured["env"] = kwargs["env"]
|
|
return _DummyProc()
|
|
|
|
monkeypatch.setattr("claude_tap.cli.shutil.which", lambda _: "/tmp/kimi")
|
|
monkeypatch.setattr(asyncio, "create_subprocess_exec", fake_create_subprocess_exec)
|
|
monkeypatch.setattr("sys.stdin.isatty", lambda: False)
|
|
|
|
code = await run_client(43123, ["--prompt", "hi"], client="kimi-code", proxy_mode="reverse")
|
|
|
|
assert code == 0
|
|
env = captured["env"]
|
|
assert "KIMI_MODEL_BASE_URL" not in env
|
|
assert env["KIMI_BASE_URL"] == "http://127.0.0.1:43123"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_client_kimi_code_reverse_does_not_proxy_non_kimi_model_env_without_base_url(
|
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
) -> None:
|
|
captured: dict[str, object] = {}
|
|
home = tmp_path / "source-home"
|
|
home.mkdir()
|
|
monkeypatch.setenv("KIMI_CODE_HOME", str(home))
|
|
monkeypatch.setenv("KIMI_MODEL_NAME", "claude-env")
|
|
monkeypatch.setenv("KIMI_MODEL_PROVIDER_TYPE", "anthropic")
|
|
|
|
async def fake_create_subprocess_exec(*cmd, **kwargs):
|
|
captured["env"] = kwargs["env"]
|
|
return _DummyProc()
|
|
|
|
monkeypatch.setattr("claude_tap.cli.shutil.which", lambda _: "/tmp/kimi")
|
|
monkeypatch.setattr(asyncio, "create_subprocess_exec", fake_create_subprocess_exec)
|
|
monkeypatch.setattr("sys.stdin.isatty", lambda: False)
|
|
|
|
code = await run_client(43123, ["--prompt", "hi"], client="kimi-code", proxy_mode="reverse")
|
|
|
|
assert code == 0
|
|
env = captured["env"]
|
|
assert "KIMI_MODEL_BASE_URL" not in env
|
|
assert env["KIMI_MODEL_NAME"] == "claude-env"
|
|
assert env["KIMI_BASE_URL"] == "http://127.0.0.1:43123"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_client_kimi_code_model_arg_clears_model_env_override(
|
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
) -> None:
|
|
captured: dict[str, object] = {}
|
|
home = tmp_path / "source-home"
|
|
home.mkdir()
|
|
(home / "config.toml").write_text(
|
|
"""
|
|
[models.selected]
|
|
provider = "managed:kimi-code"
|
|
model = "kimi-for-coding"
|
|
|
|
[providers."managed:kimi-code"]
|
|
type = "kimi"
|
|
base_url = "https://selected.example.com/coding/v1"
|
|
""".strip(),
|
|
encoding="utf-8",
|
|
)
|
|
monkeypatch.setenv("KIMI_CODE_HOME", str(home))
|
|
monkeypatch.setenv("KIMI_MODEL_NAME", "stale")
|
|
monkeypatch.setenv("KIMI_MODEL_BASE_URL", "https://stale.example.com/v1")
|
|
|
|
async def fake_create_subprocess_exec(*cmd, **kwargs):
|
|
captured["env"] = kwargs["env"]
|
|
return _DummyProc()
|
|
|
|
monkeypatch.setattr("claude_tap.cli.shutil.which", lambda _: "/tmp/kimi")
|
|
monkeypatch.setattr(asyncio, "create_subprocess_exec", fake_create_subprocess_exec)
|
|
monkeypatch.setattr("sys.stdin.isatty", lambda: False)
|
|
|
|
code = await run_client(43123, ["-m", "selected", "--prompt", "hi"], client="kimi-code", proxy_mode="reverse")
|
|
|
|
assert code == 0
|
|
env = captured["env"]
|
|
assert "KIMI_MODEL_NAME" not in env
|
|
assert "KIMI_MODEL_BASE_URL" not in env
|
|
assert env["KIMI_BASE_URL"] == "http://127.0.0.1:43123"
|
|
|
|
|
|
def test_kimi_code_migration_already_handled_reads_legacy_marker(
|
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
) -> None:
|
|
real_home = tmp_path / "real-kimi-code"
|
|
real_home.mkdir()
|
|
legacy = tmp_path / ".kimi"
|
|
legacy.mkdir()
|
|
(legacy / ".migrated-to-kimi-code").write_text(
|
|
json.dumps({"target_path": str(real_home)}),
|
|
encoding="utf-8",
|
|
)
|
|
monkeypatch.setattr("claude_tap.cli_clients.Path.home", lambda: tmp_path)
|
|
|
|
assert _kimi_code_migration_already_handled(real_home) is True
|
|
assert _kimi_code_migration_already_handled(tmp_path / "other-home") is False
|
|
|
|
|
|
def test_prepare_kimi_code_reverse_sandbox_writes_skip_marker_when_migrated(
|
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
) -> None:
|
|
real_home = tmp_path / "real-kimi-code"
|
|
real_home.mkdir()
|
|
legacy = tmp_path / ".kimi"
|
|
legacy.mkdir()
|
|
(legacy / ".migrated-to-kimi-code").write_text(
|
|
json.dumps({"target_path": str(real_home)}),
|
|
encoding="utf-8",
|
|
)
|
|
(real_home / "config.toml").write_text(
|
|
'[providers."managed:kimi-code"]\ntype = "kimi"\nbase_url = "https://api.kimi.com/coding/v1"\n',
|
|
encoding="utf-8",
|
|
)
|
|
monkeypatch.setattr("claude_tap.cli_clients.Path.home", lambda: tmp_path)
|
|
monkeypatch.setenv("KIMI_CODE_HOME", str(real_home))
|
|
|
|
sandbox, _, _, _ = _prepare_kimi_code_reverse_sandbox(43123)
|
|
try:
|
|
assert (sandbox / _KIMI_CODE_SKIP_MIGRATION_MARKER).is_file()
|
|
finally:
|
|
shutil.rmtree(sandbox, ignore_errors=True)
|
|
|
|
|
|
def test_prepare_kimi_code_reverse_sandbox_copies_existing_skip_marker(
|
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
) -> None:
|
|
real_home = tmp_path / "real-kimi-code"
|
|
real_home.mkdir()
|
|
(real_home / _KIMI_CODE_SKIP_MIGRATION_MARKER).write_text("keep", encoding="utf-8")
|
|
(real_home / "config.toml").write_text(
|
|
'[providers."managed:kimi-code"]\ntype = "kimi"\nbase_url = "https://api.kimi.com/coding/v1"\n',
|
|
encoding="utf-8",
|
|
)
|
|
monkeypatch.setenv("KIMI_CODE_HOME", str(real_home))
|
|
|
|
sandbox, _, _, _ = _prepare_kimi_code_reverse_sandbox(43123)
|
|
try:
|
|
assert (sandbox / _KIMI_CODE_SKIP_MIGRATION_MARKER).read_text(encoding="utf-8") == "keep"
|
|
finally:
|
|
shutil.rmtree(sandbox, ignore_errors=True)
|
|
|
|
|
|
def test_prepare_kimi_code_reverse_sandbox_replaces_placeholder_config(
|
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
) -> None:
|
|
real_home = tmp_path / "real-kimi-code"
|
|
real_home.mkdir()
|
|
(real_home / "config.toml").write_text(
|
|
"# Placeholder created by kimi-code before login.\n",
|
|
encoding="utf-8",
|
|
)
|
|
monkeypatch.setenv("KIMI_CODE_HOME", str(real_home))
|
|
|
|
sandbox, providers, _, _ = _prepare_kimi_code_reverse_sandbox(43123)
|
|
try:
|
|
config = tomllib.loads((sandbox / "config.toml").read_text(encoding="utf-8"))
|
|
assert providers == ["managed:kimi-code"]
|
|
assert config["providers"]["managed:kimi-code"]["base_url"] == "http://127.0.0.1:43123"
|
|
finally:
|
|
shutil.rmtree(sandbox, ignore_errors=True)
|
|
|
|
|
|
def test_prepare_kimi_code_reverse_sandbox_symlinks_auth_dirs(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
|
home = tmp_path / "source-home"
|
|
oauth_dir = home / "oauth"
|
|
oauth_dir.mkdir(parents=True)
|
|
(oauth_dir / "kimi-code").write_text("", encoding="utf-8")
|
|
credentials_dir = home / "credentials"
|
|
credentials_dir.mkdir(parents=True)
|
|
(credentials_dir / "kimi-code.json").write_text('{"access_token":"test"}', encoding="utf-8")
|
|
(home / "config.toml").write_text(
|
|
'[providers."managed:kimi-code"]\ntype = "kimi"\nbase_url = "https://api.kimi.com/coding/v1"\n',
|
|
encoding="utf-8",
|
|
)
|
|
monkeypatch.setenv("KIMI_CODE_HOME", str(home))
|
|
|
|
sandbox, providers, _, _ = _prepare_kimi_code_reverse_sandbox(43123)
|
|
try:
|
|
assert (sandbox / "oauth").is_symlink()
|
|
assert (sandbox / "oauth").resolve() == oauth_dir.resolve()
|
|
assert (sandbox / "credentials").is_symlink()
|
|
assert (sandbox / "credentials").resolve() == credentials_dir.resolve()
|
|
assert providers == ["managed:kimi-code"]
|
|
finally:
|
|
shutil.rmtree(sandbox, ignore_errors=True)
|
|
|
|
|
|
def test_prepare_kimi_code_reverse_sandbox_creates_auth_dirs_for_first_login(
|
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
) -> None:
|
|
home = tmp_path / "source-home"
|
|
home.mkdir()
|
|
(home / "config.toml").write_text(
|
|
'[providers."managed:kimi-code"]\ntype = "kimi"\nbase_url = "https://api.kimi.com/coding/v1"\n',
|
|
encoding="utf-8",
|
|
)
|
|
monkeypatch.setenv("KIMI_CODE_HOME", str(home))
|
|
|
|
sandbox, _, _, _ = _prepare_kimi_code_reverse_sandbox(43123)
|
|
try:
|
|
assert (home / "oauth").is_dir()
|
|
assert (home / "credentials").is_dir()
|
|
assert (sandbox / "oauth").is_symlink()
|
|
assert (sandbox / "credentials").is_symlink()
|
|
finally:
|
|
shutil.rmtree(sandbox, ignore_errors=True)
|
|
|
|
|
|
def test_prepare_kimi_code_reverse_sandbox_preserves_non_kimi_provider_config(
|
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
) -> None:
|
|
home = tmp_path / "source-home"
|
|
home.mkdir()
|
|
(home / "config.toml").write_text(
|
|
"""
|
|
default_model = "gpt/test"
|
|
|
|
[models."gpt/test"]
|
|
provider = "openai"
|
|
model = "gpt-test"
|
|
|
|
[providers.openai]
|
|
type = "openai"
|
|
base_url = "https://openai.example.com/v1"
|
|
api_key = "sk-test"
|
|
""".strip(),
|
|
encoding="utf-8",
|
|
)
|
|
monkeypatch.setenv("KIMI_CODE_HOME", str(home))
|
|
|
|
sandbox, providers, _, _ = _prepare_kimi_code_reverse_sandbox(43123)
|
|
try:
|
|
config = (sandbox / "config.toml").read_text(encoding="utf-8")
|
|
assert providers == []
|
|
assert 'type = "openai"' in config
|
|
assert 'base_url = "https://openai.example.com/v1"' in config
|
|
assert "managed:kimi-code" not in config
|
|
finally:
|
|
shutil.rmtree(sandbox, ignore_errors=True)
|
|
|
|
|
|
def test_prepare_kimi_code_reverse_sandbox_links_sessions_and_mcp(
|
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
) -> None:
|
|
home = tmp_path / "source-home"
|
|
sessions_dir = home / "sessions"
|
|
sessions_dir.mkdir(parents=True)
|
|
(sessions_dir / "abc.jsonl").write_text("{}", encoding="utf-8")
|
|
plugins_dir = home / "plugins"
|
|
plugins_dir.mkdir()
|
|
(plugins_dir / "installed.json").write_text("[]", encoding="utf-8")
|
|
skills_dir = home / "skills"
|
|
skills_dir.mkdir()
|
|
(skills_dir / "custom-skill.md").write_text("skill", encoding="utf-8")
|
|
(home / "AGENTS.md").write_text("agent instructions", encoding="utf-8")
|
|
(home / "mcp.json").write_text("{}", encoding="utf-8")
|
|
(home / "tui.toml").write_text('theme = "dark"\n', encoding="utf-8")
|
|
(home / "config.toml").write_text(
|
|
'[providers."managed:kimi-code"]\ntype = "kimi"\nbase_url = "https://api.kimi.com/coding/v1"\n',
|
|
encoding="utf-8",
|
|
)
|
|
monkeypatch.setenv("KIMI_CODE_HOME", str(home))
|
|
|
|
sandbox, _, _, _ = _prepare_kimi_code_reverse_sandbox(43123)
|
|
try:
|
|
assert (sandbox / "sessions").is_symlink()
|
|
assert (sandbox / "sessions").resolve() == sessions_dir.resolve()
|
|
assert (sandbox / "plugins").is_symlink()
|
|
assert (sandbox / "plugins").resolve() == plugins_dir.resolve()
|
|
assert (sandbox / "skills").is_symlink()
|
|
assert (sandbox / "skills").resolve() == skills_dir.resolve()
|
|
assert (sandbox / "AGENTS.md").is_symlink()
|
|
assert (sandbox / "AGENTS.md").resolve() == (home / "AGENTS.md").resolve()
|
|
assert (sandbox / "mcp.json").is_symlink()
|
|
assert (sandbox / "mcp.json").resolve() == (home / "mcp.json").resolve()
|
|
assert (sandbox / "tui.toml").is_symlink()
|
|
assert (sandbox / "tui.toml").resolve() == (home / "tui.toml").resolve()
|
|
assert (sandbox / "session_index.jsonl").is_file()
|
|
assert not (sandbox / "session_index.jsonl").is_symlink()
|
|
finally:
|
|
shutil.rmtree(sandbox, ignore_errors=True)
|
|
|
|
|
|
def test_prepare_kimi_code_reverse_sandbox_copies_when_symlinks_fail(
|
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
) -> None:
|
|
home = tmp_path / "source-home"
|
|
oauth_dir = home / "oauth"
|
|
oauth_dir.mkdir(parents=True)
|
|
(oauth_dir / "kimi-code").write_text("oauth-token", encoding="utf-8")
|
|
credentials_dir = home / "credentials"
|
|
credentials_dir.mkdir()
|
|
(credentials_dir / "kimi-code.json").write_text('{"access_token":"test"}', encoding="utf-8")
|
|
(credentials_dir / "stale.json").write_text('{"access_token":"old"}', encoding="utf-8")
|
|
plugins_dir = home / "plugins"
|
|
plugins_dir.mkdir()
|
|
(plugins_dir / "installed.json").write_text('["old-plugin"]', encoding="utf-8")
|
|
skills_dir = home / "skills"
|
|
skills_dir.mkdir()
|
|
(skills_dir / "custom-skill.md").write_text("old skill", encoding="utf-8")
|
|
(home / "AGENTS.md").write_text("old instructions", encoding="utf-8")
|
|
sessions_dir = home / "sessions"
|
|
sessions_dir.mkdir()
|
|
(sessions_dir / "abc.jsonl").write_text("{}", encoding="utf-8")
|
|
(home / "mcp.json").write_text("{}", encoding="utf-8")
|
|
(home / "tui.toml").write_text('theme = "dark"\n', encoding="utf-8")
|
|
(home / "config.toml").write_text(
|
|
'[providers."managed:kimi-code"]\ntype = "kimi"\nbase_url = "https://api.kimi.com/coding/v1"\n',
|
|
encoding="utf-8",
|
|
)
|
|
monkeypatch.setenv("KIMI_CODE_HOME", str(home))
|
|
|
|
def fail_symlink(self: Path, target: Path, target_is_directory: bool = False) -> None:
|
|
raise OSError("symlink unavailable")
|
|
|
|
monkeypatch.setattr(Path, "symlink_to", fail_symlink)
|
|
|
|
sandbox, _, _, _ = _prepare_kimi_code_reverse_sandbox(43123)
|
|
try:
|
|
assert not (sandbox / "oauth").is_symlink()
|
|
assert (sandbox / "oauth" / "kimi-code").read_text(encoding="utf-8") == "oauth-token"
|
|
assert (sandbox / "credentials" / "kimi-code.json").is_file()
|
|
assert (sandbox / "plugins" / "installed.json").is_file()
|
|
assert (sandbox / "skills" / "custom-skill.md").is_file()
|
|
assert (sandbox / "AGENTS.md").is_file()
|
|
assert (sandbox / "sessions" / "abc.jsonl").is_file()
|
|
assert (sandbox / "mcp.json").is_file()
|
|
assert (sandbox / "tui.toml").is_file()
|
|
|
|
(sandbox / "oauth" / "new-login").write_text("persisted", encoding="utf-8")
|
|
(sandbox / "credentials" / "stale.json").unlink()
|
|
(sandbox / "plugins" / "installed.json").write_text('["new-plugin"]', encoding="utf-8")
|
|
(sandbox / "skills" / "custom-skill.md").write_text("new skill", encoding="utf-8")
|
|
(sandbox / "AGENTS.md").write_text("new instructions", encoding="utf-8")
|
|
(sandbox / "tui.toml").write_text('theme = "light"\n', encoding="utf-8")
|
|
_persist_kimi_code_sandbox(home, sandbox)
|
|
|
|
assert (home / "oauth" / "new-login").read_text(encoding="utf-8") == "persisted"
|
|
assert not (home / "credentials" / "stale.json").exists()
|
|
assert (home / "plugins" / "installed.json").read_text(encoding="utf-8") == '["new-plugin"]'
|
|
assert (home / "skills" / "custom-skill.md").read_text(encoding="utf-8") == "new skill"
|
|
assert (home / "AGENTS.md").read_text(encoding="utf-8") == "new instructions"
|
|
assert (home / "tui.toml").read_text(encoding="utf-8") == 'theme = "light"\n'
|
|
finally:
|
|
shutil.rmtree(sandbox, ignore_errors=True)
|
|
|
|
|
|
def test_persist_kimi_code_sandbox_writes_config_edits_without_proxy_url(
|
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
) -> None:
|
|
home = tmp_path / "source-home"
|
|
home.mkdir()
|
|
(home / "config.toml").write_text(
|
|
"""
|
|
[providers."managed:kimi-code"]
|
|
type = "kimi"
|
|
base_url = "https://api.kimi.com/coding/v1"
|
|
""".strip(),
|
|
encoding="utf-8",
|
|
)
|
|
monkeypatch.setenv("KIMI_CODE_HOME", str(home))
|
|
|
|
sandbox, _, _, _ = _prepare_kimi_code_reverse_sandbox(43123)
|
|
try:
|
|
sandbox_config = sandbox / "config.toml"
|
|
sandbox_config.write_text(
|
|
sandbox_config.read_text(encoding="utf-8") + '\n[ui]\ntheme = "dark"\n',
|
|
encoding="utf-8",
|
|
)
|
|
|
|
_persist_kimi_code_sandbox(home, sandbox)
|
|
|
|
persisted = (home / "config.toml").read_text(encoding="utf-8")
|
|
assert "http://127.0.0.1:43123" not in persisted
|
|
assert 'base_url = "https://api.kimi.com/coding/v1"' in persisted
|
|
assert 'theme = "dark"' in persisted
|
|
finally:
|
|
shutil.rmtree(sandbox, ignore_errors=True)
|
|
|
|
|
|
def test_materialize_kimi_code_session_index_rewrites_session_dir(tmp_path: Path) -> None:
|
|
source_home = tmp_path / "home"
|
|
sandbox = tmp_path / "sandbox"
|
|
source_home.mkdir()
|
|
sandbox.mkdir()
|
|
session_dir = source_home / "sessions" / "wd_demo_abcd1234" / "session_test-id"
|
|
session_dir.mkdir(parents=True)
|
|
entry = {
|
|
"sessionId": "session_test-id",
|
|
"sessionDir": str(session_dir),
|
|
"workDir": "/tmp/demo",
|
|
}
|
|
(source_home / "session_index.jsonl").write_text(json.dumps(entry) + "\n", encoding="utf-8")
|
|
|
|
_materialize_kimi_code_session_index(source_home, sandbox)
|
|
|
|
index = json.loads((sandbox / "session_index.jsonl").read_text(encoding="utf-8").strip())
|
|
expected_dir = _normalize_kimi_code_fs_path(str(sandbox / "sessions" / "wd_demo_abcd1234" / "session_test-id"))
|
|
assert index["sessionDir"] == expected_dir
|
|
assert not index["sessionDir"].startswith("/private/")
|
|
|
|
|
|
def test_materialize_kimi_code_session_index_skips_malformed_rows(tmp_path: Path) -> None:
|
|
source_home = tmp_path / "home"
|
|
sandbox = tmp_path / "sandbox"
|
|
source_home.mkdir()
|
|
sandbox.mkdir()
|
|
session_dir = source_home / "sessions" / "wd_demo_abcd1234" / "session_test-id"
|
|
session_dir.mkdir(parents=True)
|
|
entry = {
|
|
"sessionId": "session_test-id",
|
|
"sessionDir": str(session_dir),
|
|
"workDir": "/tmp/demo",
|
|
}
|
|
(source_home / "session_index.jsonl").write_text(
|
|
"{not json}\n" + json.dumps(entry) + "\n[1, 2, 3]\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
_materialize_kimi_code_session_index(source_home, sandbox)
|
|
|
|
lines = (sandbox / "session_index.jsonl").read_text(encoding="utf-8").splitlines()
|
|
assert len(lines) == 1
|
|
index = json.loads(lines[0])
|
|
expected_dir = _normalize_kimi_code_fs_path(str(sandbox / "sessions" / "wd_demo_abcd1234" / "session_test-id"))
|
|
assert index["sessionId"] == "session_test-id"
|
|
assert index["sessionDir"] == expected_dir
|
|
|
|
|
|
def test_merge_kimi_code_session_index_skips_malformed_rows(tmp_path: Path) -> None:
|
|
source_home = tmp_path / "home"
|
|
sandbox = tmp_path / "sandbox"
|
|
source_home.mkdir()
|
|
sandbox.mkdir()
|
|
source_session_dir = source_home / "sessions" / "wd_demo_source" / "session_source"
|
|
sandbox_session_dir = sandbox / "sessions" / "wd_demo_new" / "session_new"
|
|
source_session_dir.mkdir(parents=True)
|
|
sandbox_session_dir.mkdir(parents=True)
|
|
source_entry = {
|
|
"sessionId": "session_source",
|
|
"sessionDir": str(source_session_dir),
|
|
"workDir": "/tmp/source",
|
|
}
|
|
sandbox_entry = {
|
|
"sessionId": "session_new",
|
|
"sessionDir": str(sandbox_session_dir),
|
|
"workDir": "/tmp/new",
|
|
}
|
|
(source_home / "session_index.jsonl").write_text(
|
|
"{bad source row}\n" + json.dumps(source_entry) + "\n",
|
|
encoding="utf-8",
|
|
)
|
|
(sandbox / "session_index.jsonl").write_text(
|
|
"{bad sandbox row}\n" + json.dumps(sandbox_entry) + "\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
_merge_kimi_code_session_index(source_home, sandbox)
|
|
|
|
entries = [
|
|
json.loads(line) for line in (source_home / "session_index.jsonl").read_text(encoding="utf-8").splitlines()
|
|
]
|
|
by_id = {entry["sessionId"]: entry for entry in entries}
|
|
assert set(by_id) == {"session_source", "session_new"}
|
|
assert by_id["session_source"]["sessionDir"] == _normalize_kimi_code_fs_path(str(source_session_dir))
|
|
assert by_id["session_new"]["sessionDir"] == _normalize_kimi_code_fs_path(
|
|
str(source_home / "sessions" / "wd_demo_new" / "session_new")
|
|
)
|
|
|
|
|
|
def test_remap_kimi_code_sandbox_paths_rewrites_session_index_and_state(tmp_path: Path) -> None:
|
|
source_home = tmp_path / "home"
|
|
sandbox = tmp_path / "claude_tap_kimi_code_test"
|
|
source_home.mkdir()
|
|
sandbox.mkdir()
|
|
session_dir = source_home / "sessions" / "wd_demo_abcd1234" / "session_test-id"
|
|
session_dir.mkdir(parents=True)
|
|
sandbox_session_dir = sandbox / "sessions" / "wd_demo_abcd1234" / "session_test-id"
|
|
state = {
|
|
"agents": {
|
|
"main": {
|
|
"homedir": str(sandbox_session_dir / "agents" / "main"),
|
|
}
|
|
}
|
|
}
|
|
(session_dir / "state.json").write_text(json.dumps(state), encoding="utf-8")
|
|
index_entry = {
|
|
"sessionId": "session_test-id",
|
|
"sessionDir": str(sandbox_session_dir),
|
|
"workDir": "/tmp/demo",
|
|
}
|
|
(source_home / "session_index.jsonl").write_text(json.dumps(index_entry) + "\n", encoding="utf-8")
|
|
|
|
_remap_kimi_code_sandbox_paths(source_home, sandbox)
|
|
|
|
index = json.loads((source_home / "session_index.jsonl").read_text(encoding="utf-8").strip())
|
|
assert index["sessionDir"] == _normalize_kimi_code_fs_path(str(session_dir))
|
|
updated_state = json.loads((session_dir / "state.json").read_text(encoding="utf-8"))
|
|
assert updated_state["agents"]["main"]["homedir"] == _normalize_kimi_code_fs_path(
|
|
str(session_dir / "agents" / "main")
|
|
)
|
|
|
|
|
|
def test_kimi_code_reverse_trace_options_do_not_strip_path_prefix() -> None:
|
|
options = _reverse_proxy_trace_options("kimi-code", "https://api.kimi.com/coding/v1")
|
|
assert options == {"strip_path_prefix": "", "force_http": False}
|