214 lines
7.1 KiB
Python
214 lines
7.1 KiB
Python
import os
|
|
|
|
from free_claude_code.cli.claude_env import build_claude_proxy_env
|
|
from free_claude_code.cli.managed.claude import (
|
|
MANAGED_CLAUDE_MODEL_TIER,
|
|
ManagedClaudeConfig,
|
|
ManagedClaudeParseState,
|
|
ManagedClaudeTaskRequest,
|
|
build_managed_claude_env,
|
|
build_managed_claude_invocation,
|
|
extract_managed_claude_session_id,
|
|
parse_managed_claude_stdout_line,
|
|
)
|
|
from free_claude_code.cli.managed.diagnostics import classify_managed_claude_stderr
|
|
|
|
|
|
def _config(**overrides: object) -> ManagedClaudeConfig:
|
|
workspace_path = overrides.get("workspace_path", os.path.normpath("/tmp/workspace"))
|
|
proxy_root_url = overrides.get("proxy_root_url", "http://localhost:8082")
|
|
raw_allowed_dirs = overrides.get("allowed_dirs")
|
|
allowed_dirs: list[str] = []
|
|
if raw_allowed_dirs is not None:
|
|
assert isinstance(raw_allowed_dirs, list)
|
|
for directory in raw_allowed_dirs:
|
|
assert isinstance(directory, str)
|
|
allowed_dirs.append(directory)
|
|
claude_bin = overrides.get("claude_bin", "claude")
|
|
auth_token = overrides.get("auth_token", "proxy-token")
|
|
|
|
assert isinstance(workspace_path, str)
|
|
assert isinstance(proxy_root_url, str)
|
|
assert isinstance(claude_bin, str)
|
|
assert isinstance(auth_token, str)
|
|
return ManagedClaudeConfig(
|
|
workspace_path=workspace_path,
|
|
proxy_root_url=proxy_root_url,
|
|
allowed_dirs=allowed_dirs,
|
|
claude_bin=claude_bin,
|
|
auth_token=auth_token,
|
|
)
|
|
|
|
|
|
def test_managed_claude_builds_new_task_command_and_env() -> None:
|
|
invocation = build_managed_claude_invocation(
|
|
config=_config(allowed_dirs=[os.path.normpath("/tmp/extra")]),
|
|
request=ManagedClaudeTaskRequest(prompt="hello"),
|
|
base_env={"PATH": "keep", "ANTHROPIC_API_KEY": "official"},
|
|
)
|
|
|
|
assert invocation.argv[:4] == (
|
|
"claude",
|
|
"--model",
|
|
MANAGED_CLAUDE_MODEL_TIER,
|
|
"-p",
|
|
)
|
|
assert "hello" in invocation.argv
|
|
assert "--output-format" in invocation.argv
|
|
assert "stream-json" in invocation.argv
|
|
assert "--add-dir" in invocation.argv
|
|
assert os.path.normpath("/tmp/extra") in invocation.argv
|
|
assert "--settings" not in invocation.argv
|
|
assert invocation.env["PATH"] == "keep"
|
|
assert invocation.env["ANTHROPIC_BASE_URL"] == "http://localhost:8082"
|
|
assert invocation.env["ANTHROPIC_AUTH_TOKEN"] == "proxy-token"
|
|
assert invocation.env["CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY"] == "1"
|
|
assert invocation.env["CLAUDE_CODE_AUTO_COMPACT_WINDOW"] == "190000"
|
|
assert invocation.env["CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC"] == "1"
|
|
assert "ANTHROPIC_API_URL" not in invocation.env
|
|
assert "ANTHROPIC_API_KEY" not in invocation.env
|
|
assert invocation.trace_metadata["client_cli_id"] == "claude"
|
|
assert invocation.trace_metadata["claude_binary"] == "claude"
|
|
assert invocation.trace_metadata["managed_model_tier"] == MANAGED_CLAUDE_MODEL_TIER
|
|
|
|
|
|
def test_managed_claude_builds_resume_and_fork_commands() -> None:
|
|
resume = build_managed_claude_invocation(
|
|
config=_config(),
|
|
request=ManagedClaudeTaskRequest(prompt="again", session_id="sess_1"),
|
|
base_env={},
|
|
)
|
|
fork = build_managed_claude_invocation(
|
|
config=_config(),
|
|
request=ManagedClaudeTaskRequest(
|
|
prompt="branch", session_id="sess_1", fork_session=True
|
|
),
|
|
base_env={},
|
|
)
|
|
|
|
assert resume.argv[:6] == (
|
|
"claude",
|
|
"--resume",
|
|
"sess_1",
|
|
"--model",
|
|
MANAGED_CLAUDE_MODEL_TIER,
|
|
"-p",
|
|
)
|
|
assert "--fork-session" not in resume.argv
|
|
assert fork.argv[:7] == (
|
|
"claude",
|
|
"--resume",
|
|
"sess_1",
|
|
"--fork-session",
|
|
"--model",
|
|
MANAGED_CLAUDE_MODEL_TIER,
|
|
"-p",
|
|
)
|
|
assert "--fork-session" in fork.argv
|
|
|
|
|
|
def test_managed_claude_uses_native_plan_storage() -> None:
|
|
invocation = build_managed_claude_invocation(
|
|
config=_config(),
|
|
request=ManagedClaudeTaskRequest(prompt="hello"),
|
|
base_env={},
|
|
)
|
|
|
|
assert "--settings" not in invocation.argv
|
|
|
|
|
|
def test_managed_claude_env_uses_sentinel_when_proxy_auth_blank() -> None:
|
|
env = build_managed_claude_env(
|
|
proxy_root_url="http://localhost:8082",
|
|
auth_token="",
|
|
base_env={"ANTHROPIC_AUTH_TOKEN": "stale"},
|
|
)
|
|
|
|
assert env["ANTHROPIC_AUTH_TOKEN"] == "fcc-no-auth"
|
|
|
|
|
|
def test_managed_claude_env_only_adds_noninteractive_process_settings() -> None:
|
|
base_env = {
|
|
"PATH": "keep",
|
|
"ANTHROPIC_API_URL": "https://api.anthropic.com/v1",
|
|
"ANTHROPIC_API_KEY": "official-key",
|
|
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "0",
|
|
}
|
|
proxy_env = build_claude_proxy_env(
|
|
proxy_root_url="http://localhost:8082",
|
|
auth_token="proxy-token",
|
|
base_env=base_env,
|
|
)
|
|
|
|
managed_env = build_managed_claude_env(
|
|
proxy_root_url="http://localhost:8082",
|
|
auth_token="proxy-token",
|
|
base_env=base_env,
|
|
)
|
|
|
|
assert managed_env == {
|
|
**proxy_env,
|
|
"TERM": "dumb",
|
|
"PYTHONIOENCODING": "utf-8",
|
|
}
|
|
|
|
|
|
def test_managed_claude_stderr_classifier_filters_known_benign_notice() -> None:
|
|
diagnostics = classify_managed_claude_stderr(
|
|
"claude.ai connectors are disabled in this environment"
|
|
)
|
|
|
|
assert diagnostics.has_benign
|
|
assert diagnostics.benign_lines == (
|
|
"claude.ai connectors are disabled in this environment",
|
|
)
|
|
assert diagnostics.fatal_text is None
|
|
|
|
|
|
def test_managed_claude_stderr_classifier_preserves_unknown_lines() -> None:
|
|
diagnostics = classify_managed_claude_stderr(
|
|
"claude.ai connectors are disabled in this environment\nFatal error"
|
|
)
|
|
|
|
assert diagnostics.has_benign
|
|
assert diagnostics.fatal_text == "Fatal error"
|
|
|
|
|
|
def test_managed_claude_extracts_session_ids() -> None:
|
|
assert extract_managed_claude_session_id({"session_id": "direct"}) == "direct"
|
|
assert extract_managed_claude_session_id({"sessionId": "camel"}) == "camel"
|
|
assert (
|
|
extract_managed_claude_session_id({"init": {"session_id": "nested"}})
|
|
== "nested"
|
|
)
|
|
assert (
|
|
extract_managed_claude_session_id({"result": {"sessionId": "result"}})
|
|
== "result"
|
|
)
|
|
assert extract_managed_claude_session_id({"conversation": {"id": "conv"}}) == "conv"
|
|
assert extract_managed_claude_session_id({"type": "message"}) is None
|
|
assert extract_managed_claude_session_id("not a dict") is None
|
|
|
|
|
|
def test_managed_claude_parser_emits_session_info_once() -> None:
|
|
state = ManagedClaudeParseState()
|
|
|
|
first = list(parse_managed_claude_stdout_line('{"session_id": "sess_1"}', state))
|
|
second = list(parse_managed_claude_stdout_line('{"session_id": "sess_2"}', state))
|
|
|
|
assert first == [
|
|
{"type": "session_info", "session_id": "sess_1"},
|
|
{"session_id": "sess_1"},
|
|
]
|
|
assert second == [{"session_id": "sess_2"}]
|
|
|
|
|
|
def test_managed_claude_parser_returns_raw_for_non_json() -> None:
|
|
events = list(
|
|
parse_managed_claude_stdout_line(
|
|
"not json", ManagedClaudeParseState(log_raw_cli_diagnostics=False)
|
|
)
|
|
)
|
|
|
|
assert events == [{"type": "raw", "content": "not json"}]
|