Skip to content

Commit 5861737

Browse files
committed
add integration tests (30 tests), CI workflow with secret API key
1 parent ee0fec1 commit 5861737

4 files changed

Lines changed: 290 additions & 0 deletions

File tree

.github/workflows/test.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: Tests
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
15+
- uses: actions/setup-python@v5
16+
with:
17+
python-version: "3.12"
18+
19+
- name: Install uv
20+
run: curl -LsSf https://astral.sh/uv/install.sh | sh
21+
22+
- name: Install dependencies
23+
run: |
24+
uv venv
25+
uv pip install -e ".[test]"
26+
27+
- name: Run tests
28+
env:
29+
JINA_API_KEY: ${{ secrets.JINA_API_KEY }}
30+
run: uv run pytest tests/ -v --tb=short

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ dependencies = [
3131
"click>=8.1.0",
3232
]
3333

34+
[project.optional-dependencies]
35+
test = ["pytest>=8.0"]
36+
3437
[project.urls]
3538
Homepage = "https://jina.ai"
3639
Documentation = "https://github.com/jina-ai/cli#readme"

tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# tests

tests/test_cli.py

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
"""Integration tests for jina-cli using real Jina API.
2+
3+
Requires JINA_API_KEY env var (set via GitHub Secrets in CI).
4+
"""
5+
6+
import json
7+
import os
8+
import subprocess
9+
import sys
10+
11+
import pytest
12+
13+
# Skip all tests if no API key
14+
pytestmark = pytest.mark.skipif(
15+
not os.environ.get("JINA_API_KEY"),
16+
reason="JINA_API_KEY not set",
17+
)
18+
19+
20+
def run_jina(*args: str, stdin: str | None = None) -> subprocess.CompletedProcess:
21+
"""Run jina CLI and capture output."""
22+
result = subprocess.run(
23+
[sys.executable, "-m", "jina_cli.main", *args],
24+
capture_output=True,
25+
text=True,
26+
timeout=60,
27+
input=stdin,
28+
env={**os.environ},
29+
)
30+
return result
31+
32+
33+
class TestRead:
34+
def test_read_url(self):
35+
r = run_jina("read", "https://example.com")
36+
assert r.returncode == 0
37+
assert "Example Domain" in r.stdout
38+
39+
def test_read_json(self):
40+
r = run_jina("read", "https://example.com", "--json")
41+
assert r.returncode == 0
42+
data = json.loads(r.stdout)
43+
assert "data" in data or "content" in data or "url" in data
44+
45+
def test_read_stdin(self):
46+
r = run_jina("read", stdin="https://example.com\n")
47+
assert r.returncode == 0
48+
assert "Example" in r.stdout
49+
50+
51+
class TestSearch:
52+
def test_search_basic(self):
53+
r = run_jina("search", "what is jina ai", "-n", "3")
54+
assert r.returncode == 0
55+
assert "jina" in r.stdout.lower()
56+
57+
def test_search_json(self):
58+
r = run_jina("search", "jina ai", "-n", "2", "--json")
59+
assert r.returncode == 0
60+
data = json.loads(r.stdout)
61+
assert "results" in data
62+
assert len(data["results"]) > 0
63+
64+
def test_search_arxiv(self):
65+
r = run_jina("search", "--arxiv", "attention mechanism", "-n", "2", "--json")
66+
assert r.returncode == 0
67+
data = json.loads(r.stdout)
68+
assert "results" in data
69+
70+
def test_search_human_readable(self):
71+
"""Default output should be human-readable, not raw JSON."""
72+
r = run_jina("search", "python", "-n", "2")
73+
assert r.returncode == 0
74+
# Should NOT start with { (raw JSON)
75+
assert not r.stdout.strip().startswith("{")
76+
# Should have title + URL pattern
77+
assert "http" in r.stdout
78+
79+
80+
class TestEmbed:
81+
def test_embed_single(self):
82+
r = run_jina("embed", "hello world")
83+
assert r.returncode == 0
84+
assert "dim=" in r.stdout
85+
86+
def test_embed_json(self):
87+
r = run_jina("embed", "hello world", "--json")
88+
assert r.returncode == 0
89+
data = json.loads(r.stdout)
90+
assert isinstance(data, list)
91+
assert len(data) > 0
92+
assert "embedding" in data[0]
93+
94+
def test_embed_multiple(self):
95+
r = run_jina("embed", "hello", "world", "foo")
96+
assert r.returncode == 0
97+
lines = [l for l in r.stdout.strip().split("\n") if l.startswith("[")]
98+
assert len(lines) == 3
99+
100+
def test_embed_stdin(self):
101+
r = run_jina("embed", stdin="hello\nworld\n")
102+
assert r.returncode == 0
103+
assert "dim=" in r.stdout
104+
105+
106+
class TestRerank:
107+
def test_rerank_basic(self):
108+
r = run_jina("rerank", "pet animal", stdin="cat is cute\ndog is loyal\nfish can swim\n")
109+
assert r.returncode == 0
110+
# Should have score format [x.xxxx]
111+
assert "[" in r.stdout and "]" in r.stdout
112+
113+
def test_rerank_json(self):
114+
r = run_jina("rerank", "pet", "--json", stdin="cat\ndog\nfish\n")
115+
assert r.returncode == 0
116+
data = json.loads(r.stdout)
117+
assert isinstance(data, list)
118+
assert len(data) > 0
119+
120+
def test_rerank_no_stdin(self):
121+
"""Should error with exit 1 when no stdin provided."""
122+
r = run_jina("rerank", "query")
123+
assert r.returncode == 1
124+
assert "Error" in r.stderr or "Fix" in r.stderr
125+
126+
127+
class TestDedup:
128+
def test_dedup_basic(self):
129+
r = run_jina("dedup", stdin="hello world\nhello world\ngoodbye world\n")
130+
assert r.returncode == 0
131+
lines = [l for l in r.stdout.strip().split("\n") if l]
132+
# Should have fewer unique items
133+
assert len(lines) <= 3
134+
135+
def test_dedup_no_stdin(self):
136+
r = run_jina("dedup")
137+
assert r.returncode == 1
138+
139+
140+
class TestBibtex:
141+
def test_bibtex_basic(self):
142+
r = run_jina("bibtex", "attention is all you need")
143+
assert r.returncode == 0
144+
# DBLP/Semantic Scholar may rate limit; just check it doesn't error
145+
146+
def test_bibtex_json(self):
147+
r = run_jina("bibtex", "BERT", "--json")
148+
assert r.returncode == 0
149+
data = json.loads(r.stdout)
150+
assert isinstance(data, list)
151+
152+
153+
class TestExpand:
154+
def test_expand_basic(self):
155+
r = run_jina("expand", "how to train embeddings")
156+
assert r.returncode == 0
157+
lines = [l for l in r.stdout.strip().split("\n") if l]
158+
assert len(lines) > 1 # Should return multiple expansions
159+
160+
161+
class TestPrimer:
162+
def test_primer(self):
163+
r = run_jina("primer")
164+
assert r.returncode == 0
165+
assert "jina.ai" in r.stdout.lower() or "reader" in r.stdout.lower()
166+
167+
168+
class TestScreenshot:
169+
def test_screenshot_stdout(self):
170+
"""Without -o, should print URL not binary data."""
171+
r = run_jina("screenshot", "https://example.com")
172+
assert r.returncode == 0
173+
# Should output a URL or base64 data indicator, not raw binary
174+
assert r.stdout.strip()
175+
176+
177+
class TestHelp:
178+
"""Test progressive help disclosure."""
179+
180+
def test_layer0_no_args(self):
181+
"""jina with no args shows command list to stderr."""
182+
r = run_jina()
183+
assert r.returncode == 0
184+
assert "jina read" in r.stderr
185+
assert "jina search" in r.stderr
186+
# stdout should be empty (no data pollution)
187+
assert r.stdout.strip() == ""
188+
189+
def test_layer1_no_args(self):
190+
"""Subcommand with no args shows short usage to stderr."""
191+
r = run_jina("embed")
192+
assert r.returncode == 1
193+
assert "Usage" in r.stderr or "jina embed" in r.stderr
194+
195+
def test_layer2_help_flag(self):
196+
"""--help shows full options."""
197+
r = run_jina("search", "--help")
198+
assert r.returncode == 0
199+
assert "--json" in r.stdout or "--json" in r.stderr
200+
201+
def test_typo_suggestion(self):
202+
"""Typo should suggest correct command."""
203+
r = run_jina("rea")
204+
assert r.returncode != 0
205+
assert "read" in r.stderr.lower() or "rerank" in r.stderr.lower()
206+
207+
208+
class TestErrorHandling:
209+
def test_invalid_api_key(self):
210+
"""Should exit 1 with actionable error for bad key."""
211+
env = {**os.environ, "JINA_API_KEY": "invalid-key"}
212+
result = subprocess.run(
213+
[sys.executable, "-m", "jina_cli.main", "embed", "test"],
214+
capture_output=True,
215+
text=True,
216+
timeout=30,
217+
env=env,
218+
)
219+
assert result.returncode == 1
220+
assert "Fix" in result.stderr or "key" in result.stderr.lower()
221+
222+
def test_missing_api_key(self):
223+
"""Should exit 1 with helpful message when key missing."""
224+
env = {k: v for k, v in os.environ.items() if k != "JINA_API_KEY"}
225+
result = subprocess.run(
226+
[sys.executable, "-m", "jina_cli.main", "embed", "test"],
227+
capture_output=True,
228+
text=True,
229+
timeout=30,
230+
env=env,
231+
)
232+
assert result.returncode == 1
233+
assert "JINA_API_KEY" in result.stderr
234+
235+
236+
class TestExitCodes:
237+
def test_success_exit_0(self):
238+
r = run_jina("primer")
239+
assert r.returncode == 0
240+
241+
def test_user_error_exit_1(self):
242+
"""Missing required input should exit 1."""
243+
r = run_jina("embed")
244+
assert r.returncode == 1
245+
246+
247+
class TestPipe:
248+
def test_search_to_rerank(self):
249+
"""search | rerank pipe should work."""
250+
# First search
251+
search = run_jina("search", "jina ai", "-n", "3")
252+
assert search.returncode == 0
253+
# Then pipe to rerank
254+
rerank = run_jina("rerank", "search foundation models", stdin=search.stdout)
255+
assert rerank.returncode == 0
256+
assert "[" in rerank.stdout # scores

0 commit comments

Comments
 (0)