Skip to content

Commit 1f5055d

Browse files
committed
test: add tests for CLI
1 parent fe1d246 commit 1f5055d

File tree

6 files changed

+287
-79
lines changed

6 files changed

+287
-79
lines changed

src/humanloop/sync/sync_client.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
import time
88
from humanloop.error import HumanloopRuntimeError
99
import json
10-
import re
1110

1211
if TYPE_CHECKING:
1312
from humanloop.base_client import BaseHumanloop
@@ -41,8 +40,13 @@ def format_api_error(error: Exception) -> str:
4140
# Get the detail from the body
4241
detail = body.get("detail", {})
4342

44-
# Prefer description, fall back to msg
45-
return detail.get("description") or detail.get("msg") or error_msg
43+
# Handle both string and dictionary types for detail
44+
if isinstance(detail, str):
45+
return detail
46+
elif isinstance(detail, dict):
47+
return detail.get("description") or detail.get("msg") or error_msg
48+
else:
49+
return error_msg
4650
except Exception as e:
4751
logger.debug(f"Failed to parse error message: {str(e)}")
4852
return error_msg

tests/custom/conftest.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,6 @@ def get_humanloop_client() -> GetHumanloopClientFn:
8888
def _get_humanloop_client(use_local_files: bool = False) -> Humanloop:
8989
return Humanloop(
9090
api_key=os.getenv("HUMANLOOP_API_KEY"),
91-
base_url="http://localhost:80/v5",
9291
use_local_files=use_local_files,
9392
)
9493

tests/custom/integration/conftest.py

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22
from dataclasses import dataclass
33
import os
44
import time
5-
from typing import Any, ContextManager, Generator
5+
from typing import Any, ContextManager, Generator, List, Union
66
import io
77
from typing import TextIO
88
import uuid
99
import pytest
1010
import dotenv
11-
from tests.custom.types import GetHumanloopClientFn
11+
from humanloop import AgentResponse, PromptResponse
12+
from tests.custom.types import GetHumanloopClientFn, SyncableFile
13+
from click.testing import CliRunner
1214

1315

1416
@dataclass
@@ -177,3 +179,81 @@ def id_for_staging_environment(get_humanloop_client: GetHumanloopClientFn, eval_
177179
if environment.name == "staging":
178180
return environment.id
179181
pytest.fail("Staging environment not found")
182+
183+
184+
@pytest.fixture
185+
def syncable_files_fixture(
186+
get_humanloop_client: GetHumanloopClientFn,
187+
sdk_test_dir: str,
188+
) -> Generator[list[SyncableFile], None, None]:
189+
"""Creates a predefined structure of files in Humanloop for testing sync."""
190+
files: List[SyncableFile] = [
191+
SyncableFile(
192+
path="prompts/gpt-4",
193+
type="prompt",
194+
model="gpt-4",
195+
),
196+
SyncableFile(
197+
path="prompts/gpt-4o",
198+
type="prompt",
199+
model="gpt-4o",
200+
),
201+
SyncableFile(
202+
path="prompts/nested/complex/gpt-4o",
203+
type="prompt",
204+
model="gpt-4o",
205+
),
206+
SyncableFile(
207+
path="agents/gpt-4",
208+
type="agent",
209+
model="gpt-4",
210+
),
211+
SyncableFile(
212+
path="agents/gpt-4o",
213+
type="agent",
214+
model="gpt-4o",
215+
),
216+
]
217+
218+
humanloop_client = get_humanloop_client()
219+
created_files = []
220+
for file in files:
221+
full_path = f"{sdk_test_dir}/{file.path}"
222+
response: Union[AgentResponse, PromptResponse]
223+
if file.type == "prompt":
224+
response = humanloop_client.prompts.upsert(
225+
path=full_path,
226+
model=file.model,
227+
)
228+
elif file.type == "agent":
229+
response = humanloop_client.agents.upsert(
230+
path=full_path,
231+
model=file.model,
232+
)
233+
created_files.append(
234+
SyncableFile(
235+
path=full_path, type=file.type, model=file.model, id=response.id, version_id=response.version_id
236+
)
237+
)
238+
239+
yield created_files
240+
241+
242+
@pytest.fixture
243+
def cli_runner() -> CliRunner:
244+
"""GIVEN a CLI runner
245+
THEN it should be configured to catch exceptions
246+
"""
247+
return CliRunner(mix_stderr=False)
248+
249+
250+
@pytest.fixture
251+
def no_humanloop_api_key_in_env(monkeypatch):
252+
"""Fixture that removes HUMANLOOP_API_KEY from environment variables.
253+
254+
Use this fixture in tests that verify behavior when no API key is available
255+
in the environment (but could still be loaded from .env files).
256+
"""
257+
# Remove API key from environment
258+
monkeypatch.delenv("HUMANLOOP_API_KEY", raising=False)
259+
yield

tests/custom/integration/test_sync.py

Lines changed: 4 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,11 @@
1-
from typing import Generator, List, NamedTuple, Union
1+
from typing import List, Union
22
from pathlib import Path
33
import pytest
4-
from humanloop import FileType, AgentResponse, PromptResponse
4+
from humanloop import AgentResponse, PromptResponse
55
from humanloop.prompts.client import PromptsClient
66
from humanloop.agents.client import AgentsClient
77
from humanloop.error import HumanloopRuntimeError
8-
from tests.custom.types import GetHumanloopClientFn
9-
10-
11-
class SyncableFile(NamedTuple):
12-
path: str
13-
type: FileType
14-
model: str
15-
id: str = ""
16-
version_id: str = ""
17-
18-
19-
@pytest.fixture
20-
def syncable_files_fixture(
21-
get_humanloop_client: GetHumanloopClientFn,
22-
sdk_test_dir: str,
23-
) -> Generator[list[SyncableFile], None, None]:
24-
"""Creates a predefined structure of files in Humanloop for testing sync."""
25-
files: List[SyncableFile] = [
26-
SyncableFile(
27-
path="prompts/gpt-4",
28-
type="prompt",
29-
model="gpt-4",
30-
),
31-
SyncableFile(
32-
path="prompts/gpt-4o",
33-
type="prompt",
34-
model="gpt-4o",
35-
),
36-
SyncableFile(
37-
path="prompts/nested/complex/gpt-4o",
38-
type="prompt",
39-
model="gpt-4o",
40-
),
41-
SyncableFile(
42-
path="agents/gpt-4",
43-
type="agent",
44-
model="gpt-4",
45-
),
46-
SyncableFile(
47-
path="agents/gpt-4o",
48-
type="agent",
49-
model="gpt-4o",
50-
),
51-
]
52-
53-
humanloop_client = get_humanloop_client()
54-
created_files = []
55-
for file in files:
56-
full_path = f"{sdk_test_dir}/{file.path}"
57-
response: Union[AgentResponse, PromptResponse]
58-
if file.type == "prompt":
59-
response = humanloop_client.prompts.upsert(
60-
path=full_path,
61-
model=file.model,
62-
)
63-
elif file.type == "agent":
64-
response = humanloop_client.agents.upsert(
65-
path=full_path,
66-
model=file.model,
67-
)
68-
created_files.append(
69-
SyncableFile(
70-
path=full_path, type=file.type, model=file.model, id=response.id, version_id=response.version_id
71-
)
72-
)
73-
74-
humanloop_client.pull()
75-
76-
yield created_files
8+
from tests.custom.types import GetHumanloopClientFn, SyncableFile
779

7810

7911
@pytest.fixture
@@ -96,7 +28,7 @@ def test_pull_basic(
9628
humanloop_client = get_humanloop_client()
9729

9830
# WHEN running the sync
99-
successful_files = humanloop_client.pull()
31+
humanloop_client.pull()
10032

10133
# THEN our local filesystem should mirror the remote filesystem in the HL Workspace
10234
for file in syncable_files_fixture:

0 commit comments

Comments
 (0)