recce-nightly 1.10.0.20250629__py3-none-any.whl → 1.25.0.20251112a20664__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- recce/VERSION +1 -1
- recce/__init__.py +5 -0
- recce/adapter/dbt_adapter/__init__.py +116 -74
- recce/artifact.py +76 -3
- recce/cli.py +665 -69
- recce/config.py +2 -2
- recce/connect_to_cloud.py +1 -1
- recce/core.py +3 -3
- recce/data/404.html +1 -22
- recce/data/__next.__PAGE__.txt +10 -0
- recce/data/__next._full.txt +23 -0
- recce/data/__next._index.txt +8 -0
- recce/data/__next._tree.txt +12 -0
- recce/data/_next/static/JwV_pqetN5WamZZ7aGdfH/_buildManifest.js +11 -0
- recce/data/_next/static/JwV_pqetN5WamZZ7aGdfH/_clientMiddlewareManifest.json +1 -0
- recce/data/_next/static/chunks/0a2b2dd4b57049c2.js +1 -0
- recce/data/_next/static/chunks/19c10d219a6a21ff.js +1 -0
- recce/data/_next/static/chunks/24fd885c7180a612.js +1 -0
- recce/data/_next/static/chunks/27e66b2eab4adc32.js +19 -0
- recce/data/_next/static/chunks/67b1c6a62f19d429.js +110 -0
- recce/data/_next/static/chunks/71f88fcc615bf282.js +1 -0
- recce/data/_next/static/chunks/917619ab62a32388.js +1 -0
- recce/data/_next/static/chunks/93ba5a62932b704f.js +4 -0
- recce/data/_next/static/chunks/a43a2a5e06d5a92b.js +1 -0
- recce/data/_next/static/chunks/a6c78b24bd8b84fc.js +1 -0
- recce/data/_next/static/chunks/ba2d87265a68599d.css +2 -0
- recce/data/_next/static/chunks/c117fd1c1382dd83.js +11 -0
- recce/data/_next/static/chunks/c9425ca46eebdde9.js +1 -0
- recce/data/_next/static/chunks/cc8a9eadba012be0.css +6 -0
- recce/data/_next/static/chunks/e124bccf574a3361.css +1 -0
- recce/data/_next/static/chunks/e392ad92847c3e17.js +1 -0
- recce/data/_next/static/chunks/e4ce95efe88dae79.js +11 -0
- recce/data/_next/static/chunks/e69c777814fea6ed.js +2 -0
- recce/data/_next/static/chunks/turbopack-21cfd73037ff57ab.js +3 -0
- recce/data/_next/static/media/favicon.a8d38d84.ico +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-800-normal.d80d830d.woff2 +0 -0
- recce/data/_next/static/media/{montserrat-cyrillic-800-normal.bd5c9f50.woff → montserrat-cyrillic-800-normal.f9d58125.woff} +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.076c2a93.woff2 +0 -0
- recce/data/_next/static/media/montserrat-latin-800-normal.cde454cc.woff2 +0 -0
- recce/data/_next/static/media/{montserrat-latin-800-normal.fc315020.woff → montserrat-latin-800-normal.d5761935.woff} +0 -0
- recce/data/_next/static/media/montserrat-latin-ext-800-normal.40ec0659.woff2 +0 -0
- recce/data/_next/static/media/{montserrat-latin-ext-800-normal.2e5381b2.woff → montserrat-latin-ext-800-normal.b671449b.woff} +0 -0
- recce/data/_next/static/media/{montserrat-vietnamese-800-normal.20c545e6.woff → montserrat-vietnamese-800-normal.9f7b8541.woff} +0 -0
- recce/data/_next/static/media/montserrat-vietnamese-800-normal.f9eb854e.woff2 +0 -0
- recce/data/_not-found/__next._full.txt +17 -0
- recce/data/_not-found/__next._index.txt +8 -0
- recce/data/_not-found/__next._not-found.__PAGE__.txt +5 -0
- recce/data/_not-found/__next._not-found.txt +4 -0
- recce/data/_not-found/__next._tree.txt +10 -0
- recce/data/_not-found.html +1 -0
- recce/data/_not-found.txt +17 -0
- recce/data/auth_callback.html +1 -1
- recce/data/index.html +1 -27
- recce/data/index.txt +23 -8
- recce/event/__init__.py +9 -8
- recce/event/collector.py +6 -2
- recce/event/track.py +10 -0
- recce/github.py +1 -1
- recce/mcp_server.py +632 -0
- recce/models/types.py +23 -2
- recce/pull_request.py +1 -1
- recce/run.py +23 -16
- recce/server.py +165 -11
- recce/state/__init__.py +31 -0
- recce/state/cloud.py +632 -0
- recce/state/const.py +26 -0
- recce/state/local.py +56 -0
- recce/state/state.py +119 -0
- recce/state/state_loader.py +174 -0
- recce/summary.py +2 -1
- recce/tasks/dataframe.py +59 -2
- recce/tasks/rowcount.py +4 -1
- recce/tasks/schema.py +4 -1
- recce/tasks/valuediff.py +1 -1
- recce/util/api_token.py +11 -2
- recce/util/breaking.py +9 -0
- recce/util/cll.py +1 -2
- recce/util/io.py +2 -2
- recce/util/lineage.py +14 -18
- recce/util/perf_tracking.py +85 -0
- recce/util/recce_cloud.py +229 -5
- recce/yaml/__init__.py +2 -2
- recce_cloud/__init__.py +15 -0
- recce_cloud/api/__init__.py +17 -0
- recce_cloud/api/base.py +104 -0
- recce_cloud/api/client.py +150 -0
- recce_cloud/api/exceptions.py +26 -0
- recce_cloud/api/factory.py +63 -0
- recce_cloud/api/github.py +72 -0
- recce_cloud/api/gitlab.py +78 -0
- recce_cloud/artifact.py +57 -0
- recce_cloud/ci_providers/__init__.py +9 -0
- recce_cloud/ci_providers/base.py +82 -0
- recce_cloud/ci_providers/detector.py +147 -0
- recce_cloud/ci_providers/github_actions.py +136 -0
- recce_cloud/ci_providers/gitlab_ci.py +130 -0
- recce_cloud/cli.py +303 -0
- recce_cloud/upload.py +213 -0
- {recce_nightly-1.10.0.20250629.dist-info → recce_nightly-1.25.0.20251112a20664.dist-info}/METADATA +31 -27
- recce_nightly-1.25.0.20251112a20664.dist-info/RECORD +178 -0
- {recce_nightly-1.10.0.20250629.dist-info → recce_nightly-1.25.0.20251112a20664.dist-info}/top_level.txt +1 -0
- tests/adapter/dbt_adapter/test_dbt_cll.py +68 -17
- tests/recce_cloud/__init__.py +0 -0
- tests/recce_cloud/test_ci_providers.py +351 -0
- tests/recce_cloud/test_cli.py +372 -0
- tests/recce_cloud/test_client.py +273 -0
- tests/recce_cloud/test_platform_clients.py +279 -0
- tests/test_cli.py +106 -3
- tests/test_cli_mcp_optional.py +45 -0
- tests/test_cloud_listing_cli.py +324 -0
- tests/test_core.py +147 -0
- tests/test_mcp_server.py +332 -0
- tests/test_server.py +6 -6
- tests/test_summary.py +14 -6
- recce/data/_next/static/Mrb9CZ3toH6Q8xrzNzCrg/_buildManifest.js +0 -1
- recce/data/_next/static/chunks/181-acc61ddada3bc0ca.js +0 -43
- recce/data/_next/static/chunks/1bff33f1-1ef85cf5e658a751.js +0 -1
- recce/data/_next/static/chunks/217-879a84d70f7a907c.js +0 -2
- recce/data/_next/static/chunks/29e3cc0d-60045b2e47aa3916.js +0 -1
- recce/data/_next/static/chunks/36e1c10d-8e7be4a6c1f6ab2d.js +0 -1
- recce/data/_next/static/chunks/3998a672-03adacad07b346ac.js +0 -1
- recce/data/_next/static/chunks/3a92ee20-1081c360214f9602.js +0 -1
- recce/data/_next/static/chunks/41-f30276c289169376.js +0 -9
- recce/data/_next/static/chunks/450c323b-fd94e7ffaa4a5efa.js +0 -1
- recce/data/_next/static/chunks/47d8844f-929aed9b1c73a905.js +0 -1
- recce/data/_next/static/chunks/608-3b079b544e5d5f5e.js +0 -15
- recce/data/_next/static/chunks/6dc81886-adbfa45836061d79.js +0 -1
- recce/data/_next/static/chunks/7a8a3e83-edf6dc64b5d5f0a5.js +0 -1
- recce/data/_next/static/chunks/7f27ae6c-d5f0438edd5c2a5b.js +0 -1
- recce/data/_next/static/chunks/86730205-cfb14e3f051bab35.js +0 -1
- recce/data/_next/static/chunks/8d700b6a.8bb140898499c512.js +0 -1
- recce/data/_next/static/chunks/92-68460b15fe448f33.js +0 -1
- recce/data/_next/static/chunks/9746af58-a42b7d169cacadf0.js +0 -1
- recce/data/_next/static/chunks/a30376cd-de84559016d7e133.js +0 -1
- recce/data/_next/static/chunks/app/_not-found/page-01ed58b7f971d311.js +0 -1
- recce/data/_next/static/chunks/app/layout-292f035bb0d2a98e.js +0 -1
- recce/data/_next/static/chunks/app/page-598f8acc82179d01.js +0 -1
- recce/data/_next/static/chunks/b63b1b3f-4282bdcf459e075c.js +0 -1
- recce/data/_next/static/chunks/bbda5537-9ec25eb1dd62348a.js +0 -1
- recce/data/_next/static/chunks/c132bf7d-08cb668a789d6afd.js +0 -1
- recce/data/_next/static/chunks/ce84277d-2e5d1d46910cf052.js +0 -1
- recce/data/_next/static/chunks/febdd86e-c6b525341634b860.js +0 -54
- recce/data/_next/static/chunks/fee69bc6-2dbccaf9b90474e6.js +0 -1
- recce/data/_next/static/chunks/framework-ded83d71b51ce901.js +0 -1
- recce/data/_next/static/chunks/main-app-39061b0166c47f55.js +0 -1
- recce/data/_next/static/chunks/main-b5b3ae20a1405261.js +0 -1
- recce/data/_next/static/chunks/pages/_app-437c455677d62394.js +0 -1
- recce/data/_next/static/chunks/pages/_error-e7650df18ca04bde.js +0 -1
- recce/data/_next/static/chunks/webpack-7b49d5ba7e3a434d.js +0 -1
- recce/data/_next/static/css/17a96168e3a9db13.css +0 -1
- recce/data/_next/static/css/35c6679a098e1e34.css +0 -1
- recce/data/_next/static/css/951e2e0eea2d4a5b.css +0 -14
- recce/data/_next/static/css/a2b12b4ba4227f0a.css +0 -3
- recce/data/_next/static/media/montserrat-cyrillic-800-normal.22628180.woff2 +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.94a63aea.woff2 +0 -0
- recce/data/_next/static/media/montserrat-latin-800-normal.6f8fa298.woff2 +0 -0
- recce/data/_next/static/media/montserrat-latin-ext-800-normal.013b84f9.woff2 +0 -0
- recce/data/_next/static/media/montserrat-vietnamese-800-normal.c0035377.woff2 +0 -0
- recce/state.py +0 -786
- recce_nightly-1.10.0.20250629.dist-info/RECORD +0 -154
- tests/test_state.py +0 -134
- /recce/data/_next/static/{Mrb9CZ3toH6Q8xrzNzCrg → JwV_pqetN5WamZZ7aGdfH}/_ssgManifest.js +0 -0
- /recce/data/_next/static/chunks/{polyfills-42372ed130431b0a.js → a6dad97d9634a72d.js} +0 -0
- /recce/data/_next/static/media/{montserrat-cyrillic-ext-800-normal.e6e0d8d0.woff → montserrat-cyrillic-ext-800-normal.a4fa76b5.woff} +0 -0
- /recce/data/_next/static/media/{reload-image.79aabb7d.svg → reload-image.7aa931c7.svg} +0 -0
- {recce_nightly-1.10.0.20250629.dist-info → recce_nightly-1.25.0.20251112a20664.dist-info}/WHEEL +0 -0
- {recce_nightly-1.10.0.20250629.dist-info → recce_nightly-1.25.0.20251112a20664.dist-info}/entry_points.txt +0 -0
- {recce_nightly-1.10.0.20250629.dist-info → recce_nightly-1.25.0.20251112a20664.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for platform-specific Recce Cloud API clients.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from unittest.mock import patch
|
|
7
|
+
from urllib.parse import urlparse
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from recce_cloud.api.factory import create_platform_client
|
|
12
|
+
from recce_cloud.api.github import GitHubRecceCloudClient
|
|
13
|
+
from recce_cloud.api.gitlab import GitLabRecceCloudClient
|
|
14
|
+
from recce_cloud.ci_providers.base import CIInfo
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TestGitHubRecceCloudClient:
|
|
18
|
+
"""Tests for GitHub Actions API client."""
|
|
19
|
+
|
|
20
|
+
def test_init(self):
|
|
21
|
+
"""Test client initialization."""
|
|
22
|
+
client = GitHubRecceCloudClient(token="test_token", repository="owner/repo")
|
|
23
|
+
assert client.token == "test_token"
|
|
24
|
+
assert client.repository == "owner/repo"
|
|
25
|
+
parsed = urlparse(client.api_host)
|
|
26
|
+
# Accept main domain or subdomains:
|
|
27
|
+
assert parsed.hostname == "cloud.datarecce.io" or (
|
|
28
|
+
parsed.hostname and parsed.hostname.endswith(".cloud.datarecce.io")
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def test_touch_recce_session_pr(self):
|
|
32
|
+
"""Test touch_recce_session for PR context."""
|
|
33
|
+
client = GitHubRecceCloudClient(token="test_token", repository="owner/repo")
|
|
34
|
+
|
|
35
|
+
with patch.object(client, "_make_request") as mock_request:
|
|
36
|
+
mock_request.return_value = {
|
|
37
|
+
"session_id": "test_session_id",
|
|
38
|
+
"manifest_upload_url": "https://s3.aws.com/manifest",
|
|
39
|
+
"catalog_upload_url": "https://s3.aws.com/catalog",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
response = client.touch_recce_session(branch="feature-branch", adapter_type="postgres", cr_number=123)
|
|
43
|
+
|
|
44
|
+
assert response["session_id"] == "test_session_id"
|
|
45
|
+
assert response["manifest_upload_url"] == "https://s3.aws.com/manifest"
|
|
46
|
+
assert response["catalog_upload_url"] == "https://s3.aws.com/catalog"
|
|
47
|
+
|
|
48
|
+
# Verify correct API endpoint was called
|
|
49
|
+
mock_request.assert_called_once()
|
|
50
|
+
call_args = mock_request.call_args
|
|
51
|
+
assert call_args[0][0] == "POST"
|
|
52
|
+
assert "github/owner/repo/touch-recce-session" in call_args[0][1]
|
|
53
|
+
assert call_args[1]["json"]["branch"] == "feature-branch"
|
|
54
|
+
assert call_args[1]["json"]["adapter_type"] == "postgres"
|
|
55
|
+
assert call_args[1]["json"]["pr_number"] == 123
|
|
56
|
+
|
|
57
|
+
def test_touch_recce_session_base(self):
|
|
58
|
+
"""Test touch_recce_session for base branch context."""
|
|
59
|
+
client = GitHubRecceCloudClient(token="test_token", repository="owner/repo")
|
|
60
|
+
|
|
61
|
+
with patch.object(client, "_make_request") as mock_request:
|
|
62
|
+
mock_request.return_value = {
|
|
63
|
+
"session_id": "base_session_id",
|
|
64
|
+
"manifest_upload_url": "https://s3.aws.com/manifest",
|
|
65
|
+
"catalog_upload_url": "https://s3.aws.com/catalog",
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
client.touch_recce_session(branch="main", adapter_type="snowflake")
|
|
69
|
+
|
|
70
|
+
# Verify pr_number is not in the payload when cr_number is None
|
|
71
|
+
call_args = mock_request.call_args
|
|
72
|
+
assert "pr_number" not in call_args[1]["json"]
|
|
73
|
+
|
|
74
|
+
def test_upload_completed(self):
|
|
75
|
+
"""Test upload_completed notification."""
|
|
76
|
+
client = GitHubRecceCloudClient(token="test_token", repository="owner/repo")
|
|
77
|
+
|
|
78
|
+
with patch.object(client, "_make_request") as mock_request:
|
|
79
|
+
mock_request.return_value = {}
|
|
80
|
+
|
|
81
|
+
client.upload_completed(session_id="test_session_id")
|
|
82
|
+
|
|
83
|
+
mock_request.assert_called_once()
|
|
84
|
+
call_args = mock_request.call_args
|
|
85
|
+
assert call_args[0][0] == "POST"
|
|
86
|
+
assert "github/owner/repo/upload-completed" in call_args[0][1]
|
|
87
|
+
assert call_args[1]["json"]["session_id"] == "test_session_id"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class TestGitLabRecceCloudClient:
|
|
91
|
+
"""Tests for GitLab CI API client."""
|
|
92
|
+
|
|
93
|
+
def test_init(self):
|
|
94
|
+
"""Test client initialization."""
|
|
95
|
+
client = GitLabRecceCloudClient(
|
|
96
|
+
token="test_token",
|
|
97
|
+
project_path="group/project",
|
|
98
|
+
repository_url="https://gitlab.com/group/project",
|
|
99
|
+
)
|
|
100
|
+
assert client.token == "test_token"
|
|
101
|
+
assert client.project_path == "group/project"
|
|
102
|
+
assert client.repository_url == "https://gitlab.com/group/project"
|
|
103
|
+
|
|
104
|
+
def test_touch_recce_session_mr(self):
|
|
105
|
+
"""Test touch_recce_session for MR context."""
|
|
106
|
+
client = GitLabRecceCloudClient(
|
|
107
|
+
token="test_token",
|
|
108
|
+
project_path="group/project",
|
|
109
|
+
repository_url="https://gitlab.com/group/project",
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
with patch.object(client, "_make_request") as mock_request:
|
|
113
|
+
mock_request.return_value = {
|
|
114
|
+
"session_id": "test_session_id",
|
|
115
|
+
"manifest_upload_url": "https://s3.aws.com/manifest",
|
|
116
|
+
"catalog_upload_url": "https://s3.aws.com/catalog",
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
response = client.touch_recce_session(
|
|
120
|
+
branch="feature-branch",
|
|
121
|
+
adapter_type="postgres",
|
|
122
|
+
cr_number=456,
|
|
123
|
+
commit_sha="abc123def456",
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
assert response["session_id"] == "test_session_id"
|
|
127
|
+
|
|
128
|
+
# Verify correct API endpoint and payload
|
|
129
|
+
call_args = mock_request.call_args
|
|
130
|
+
assert call_args[0][0] == "POST"
|
|
131
|
+
assert "gitlab/group/project/touch-recce-session" in call_args[0][1]
|
|
132
|
+
payload = call_args[1]["json"]
|
|
133
|
+
assert payload["branch"] == "feature-branch"
|
|
134
|
+
assert payload["adapter_type"] == "postgres"
|
|
135
|
+
assert payload["mr_iid"] == 456
|
|
136
|
+
assert payload["commit_sha"] == "abc123def456"
|
|
137
|
+
assert payload["repository_url"] == "https://gitlab.com/group/project"
|
|
138
|
+
|
|
139
|
+
def test_touch_recce_session_base(self):
|
|
140
|
+
"""Test touch_recce_session for base branch context."""
|
|
141
|
+
client = GitLabRecceCloudClient(
|
|
142
|
+
token="test_token",
|
|
143
|
+
project_path="group/project",
|
|
144
|
+
repository_url="https://gitlab.com/group/project",
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
with patch.object(client, "_make_request") as mock_request:
|
|
148
|
+
mock_request.return_value = {
|
|
149
|
+
"session_id": "base_session_id",
|
|
150
|
+
"manifest_upload_url": "https://s3.aws.com/manifest",
|
|
151
|
+
"catalog_upload_url": "https://s3.aws.com/catalog",
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
client.touch_recce_session(branch="main", adapter_type="bigquery", commit_sha="base123")
|
|
155
|
+
|
|
156
|
+
# Verify mr_iid is not in the payload when cr_number is None
|
|
157
|
+
call_args = mock_request.call_args
|
|
158
|
+
assert "mr_iid" not in call_args[1]["json"]
|
|
159
|
+
|
|
160
|
+
def test_upload_completed(self):
|
|
161
|
+
"""Test upload_completed notification."""
|
|
162
|
+
client = GitLabRecceCloudClient(
|
|
163
|
+
token="test_token",
|
|
164
|
+
project_path="group/project",
|
|
165
|
+
repository_url="https://gitlab.com/group/project",
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
with patch.object(client, "_make_request") as mock_request:
|
|
169
|
+
mock_request.return_value = {}
|
|
170
|
+
|
|
171
|
+
client.upload_completed(session_id="test_session_id", commit_sha="commit456")
|
|
172
|
+
|
|
173
|
+
mock_request.assert_called_once()
|
|
174
|
+
call_args = mock_request.call_args
|
|
175
|
+
assert call_args[0][0] == "POST"
|
|
176
|
+
assert "gitlab/group/project/upload-completed" in call_args[0][1]
|
|
177
|
+
payload = call_args[1]["json"]
|
|
178
|
+
assert payload["session_id"] == "test_session_id"
|
|
179
|
+
assert payload["commit_sha"] == "commit456"
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class TestFactoryCreatePlatformClient:
|
|
183
|
+
"""Tests for create_platform_client factory function."""
|
|
184
|
+
|
|
185
|
+
def test_create_github_client(self):
|
|
186
|
+
"""Test creating GitHub client."""
|
|
187
|
+
ci_info = CIInfo(platform="github-actions", repository="owner/repo")
|
|
188
|
+
|
|
189
|
+
client = create_platform_client(token="test_token", ci_info=ci_info)
|
|
190
|
+
|
|
191
|
+
assert isinstance(client, GitHubRecceCloudClient)
|
|
192
|
+
assert client.repository == "owner/repo"
|
|
193
|
+
|
|
194
|
+
def test_create_github_client_from_env(self):
|
|
195
|
+
"""Test creating GitHub client with environment fallback."""
|
|
196
|
+
with patch.dict(os.environ, {"GITHUB_REPOSITORY": "owner/repo"}):
|
|
197
|
+
ci_info = CIInfo(platform="github-actions")
|
|
198
|
+
|
|
199
|
+
client = create_platform_client(token="test_token", ci_info=ci_info)
|
|
200
|
+
|
|
201
|
+
assert isinstance(client, GitHubRecceCloudClient)
|
|
202
|
+
assert client.repository == "owner/repo"
|
|
203
|
+
|
|
204
|
+
def test_create_github_client_missing_repository(self):
|
|
205
|
+
"""Test error when GitHub repository information is missing."""
|
|
206
|
+
ci_info = CIInfo(platform="github-actions")
|
|
207
|
+
|
|
208
|
+
with patch.dict(os.environ, {}, clear=True):
|
|
209
|
+
with pytest.raises(ValueError, match="GitHub repository information is required"):
|
|
210
|
+
create_platform_client(token="test_token", ci_info=ci_info)
|
|
211
|
+
|
|
212
|
+
def test_create_gitlab_client(self):
|
|
213
|
+
"""Test creating GitLab client."""
|
|
214
|
+
with patch.dict(
|
|
215
|
+
os.environ,
|
|
216
|
+
{"CI_PROJECT_URL": "https://gitlab.com/group/project"},
|
|
217
|
+
):
|
|
218
|
+
ci_info = CIInfo(platform="gitlab-ci", repository="group/project")
|
|
219
|
+
|
|
220
|
+
client = create_platform_client(token="test_token", ci_info=ci_info)
|
|
221
|
+
|
|
222
|
+
assert isinstance(client, GitLabRecceCloudClient)
|
|
223
|
+
assert client.project_path == "group/project"
|
|
224
|
+
assert client.repository_url == "https://gitlab.com/group/project"
|
|
225
|
+
|
|
226
|
+
def test_create_gitlab_client_from_env(self):
|
|
227
|
+
"""Test creating GitLab client with environment fallback."""
|
|
228
|
+
with patch.dict(
|
|
229
|
+
os.environ,
|
|
230
|
+
{
|
|
231
|
+
"CI_PROJECT_PATH": "group/project",
|
|
232
|
+
"CI_PROJECT_URL": "https://gitlab.com/group/project",
|
|
233
|
+
},
|
|
234
|
+
):
|
|
235
|
+
ci_info = CIInfo(platform="gitlab-ci")
|
|
236
|
+
|
|
237
|
+
client = create_platform_client(token="test_token", ci_info=ci_info)
|
|
238
|
+
|
|
239
|
+
assert isinstance(client, GitLabRecceCloudClient)
|
|
240
|
+
assert client.project_path == "group/project"
|
|
241
|
+
|
|
242
|
+
def test_create_gitlab_client_missing_project_path(self):
|
|
243
|
+
"""Test error when GitLab project path is missing."""
|
|
244
|
+
with patch.dict(os.environ, {}, clear=True):
|
|
245
|
+
ci_info = CIInfo(platform="gitlab-ci")
|
|
246
|
+
|
|
247
|
+
with pytest.raises(ValueError, match="GitLab project path is required"):
|
|
248
|
+
create_platform_client(token="test_token", ci_info=ci_info)
|
|
249
|
+
|
|
250
|
+
def test_create_gitlab_client_missing_project_url(self):
|
|
251
|
+
"""Test error when GitLab project URL is missing."""
|
|
252
|
+
with patch.dict(os.environ, {"CI_PROJECT_PATH": "group/project"}, clear=True):
|
|
253
|
+
ci_info = CIInfo(platform="gitlab-ci", repository="group/project")
|
|
254
|
+
|
|
255
|
+
with pytest.raises(ValueError, match="GitLab project URL is required"):
|
|
256
|
+
create_platform_client(token="test_token", ci_info=ci_info)
|
|
257
|
+
|
|
258
|
+
def test_create_client_unsupported_platform(self):
|
|
259
|
+
"""Test error for unsupported platform."""
|
|
260
|
+
ci_info = CIInfo(platform="unsupported-ci")
|
|
261
|
+
|
|
262
|
+
with pytest.raises(ValueError, match="Unsupported platform"):
|
|
263
|
+
create_platform_client(token="test_token", ci_info=ci_info)
|
|
264
|
+
|
|
265
|
+
def test_auto_detect_ci_info(self):
|
|
266
|
+
"""Test automatic CI detection when ci_info is not provided."""
|
|
267
|
+
with patch.dict(
|
|
268
|
+
os.environ,
|
|
269
|
+
{
|
|
270
|
+
"GITHUB_ACTIONS": "true",
|
|
271
|
+
"GITHUB_REPOSITORY": "owner/repo",
|
|
272
|
+
"GITHUB_SHA": "abc123",
|
|
273
|
+
},
|
|
274
|
+
clear=True,
|
|
275
|
+
):
|
|
276
|
+
client = create_platform_client(token="test_token")
|
|
277
|
+
|
|
278
|
+
assert isinstance(client, GitHubRecceCloudClient)
|
|
279
|
+
assert client.repository == "owner/repo"
|
tests/test_cli.py
CHANGED
|
@@ -5,8 +5,10 @@ from click.testing import CliRunner
|
|
|
5
5
|
|
|
6
6
|
from recce.cli import run as cli_command_run
|
|
7
7
|
from recce.cli import server as cli_command_server
|
|
8
|
+
from recce.cli import snapshot as cli_command_snapshot
|
|
9
|
+
from recce.cli import upload_session as cli_command_upload_session
|
|
8
10
|
from recce.core import RecceContext
|
|
9
|
-
from recce.state import
|
|
11
|
+
from recce.state import CloudStateLoader
|
|
10
12
|
|
|
11
13
|
|
|
12
14
|
def test_cmd_version():
|
|
@@ -48,11 +50,11 @@ class TestCommandServer(TestCase):
|
|
|
48
50
|
@patch.object(RecceContext, "verify_required_artifacts")
|
|
49
51
|
@patch("recce.util.recce_cloud.get_recce_cloud_onboarding_state")
|
|
50
52
|
@patch("recce.cli.uvicorn.run")
|
|
51
|
-
@patch("recce.cli.
|
|
53
|
+
@patch("recce.cli.CloudStateLoader")
|
|
52
54
|
def test_cmd_server_with_cloud(
|
|
53
55
|
self, mock_state_loader_class, mock_run, mock_get_recce_cloud_onboarding_state, mock_verify_required_artifacts
|
|
54
56
|
):
|
|
55
|
-
mock_state_loader = MagicMock(spec=
|
|
57
|
+
mock_state_loader = MagicMock(spec=CloudStateLoader)
|
|
56
58
|
mock_state_loader.verify.return_value = True
|
|
57
59
|
mock_state_loader.review_mode = True
|
|
58
60
|
mock_get_recce_cloud_onboarding_state.return_value = "completed"
|
|
@@ -65,6 +67,82 @@ class TestCommandServer(TestCase):
|
|
|
65
67
|
mock_state_loader_class.assert_called_once()
|
|
66
68
|
mock_run.assert_called_once()
|
|
67
69
|
|
|
70
|
+
@patch.object(RecceContext, "verify_required_artifacts")
|
|
71
|
+
@patch("recce.util.recce_cloud.get_recce_cloud_onboarding_state")
|
|
72
|
+
@patch("recce.cli.uvicorn.run")
|
|
73
|
+
@patch("recce.cli.CloudStateLoader")
|
|
74
|
+
@patch("recce.cli.prepare_api_token", return_value="test_api_token")
|
|
75
|
+
def test_cmd_server_with_session_id(
|
|
76
|
+
self,
|
|
77
|
+
mock_prepare_api_token,
|
|
78
|
+
mock_state_loader_class,
|
|
79
|
+
mock_run,
|
|
80
|
+
mock_get_recce_cloud_onboarding_state,
|
|
81
|
+
mock_verify_required_artifacts,
|
|
82
|
+
):
|
|
83
|
+
"""Test that --session-id automatically enables cloud and review mode"""
|
|
84
|
+
mock_state_loader = MagicMock(spec=CloudStateLoader)
|
|
85
|
+
mock_state_loader.verify.return_value = True
|
|
86
|
+
mock_state_loader.review_mode = True
|
|
87
|
+
mock_get_recce_cloud_onboarding_state.return_value = "completed"
|
|
88
|
+
mock_verify_required_artifacts.return_value = True, None
|
|
89
|
+
|
|
90
|
+
mock_state_loader_class.return_value = mock_state_loader
|
|
91
|
+
|
|
92
|
+
# Test with --session-id (should automatically enable cloud and review)
|
|
93
|
+
result = self.runner.invoke(cli_command_server, ["--session-id", "test-session-123", "--single-env"])
|
|
94
|
+
|
|
95
|
+
# Should succeed
|
|
96
|
+
assert result.exit_code == 0
|
|
97
|
+
|
|
98
|
+
# Should create CloudStateLoader with session_id in cloud_options
|
|
99
|
+
mock_state_loader_class.assert_called_once()
|
|
100
|
+
call_args = mock_state_loader_class.call_args
|
|
101
|
+
assert call_args.kwargs["review_mode"] is True
|
|
102
|
+
assert "session_id" in call_args.kwargs["cloud_options"]
|
|
103
|
+
assert call_args.kwargs["cloud_options"]["session_id"] == "test-session-123"
|
|
104
|
+
|
|
105
|
+
mock_run.assert_called_once()
|
|
106
|
+
|
|
107
|
+
@patch.object(RecceContext, "verify_required_artifacts")
|
|
108
|
+
@patch("recce.util.recce_cloud.get_recce_cloud_onboarding_state")
|
|
109
|
+
@patch("recce.cli.uvicorn.run")
|
|
110
|
+
@patch("recce.cli.CloudStateLoader")
|
|
111
|
+
@patch("recce.cli.prepare_api_token", return_value="test_api_token")
|
|
112
|
+
def test_cmd_server_with_share_url(
|
|
113
|
+
self,
|
|
114
|
+
mock_prepare_api_token,
|
|
115
|
+
mock_state_loader_class,
|
|
116
|
+
mock_run,
|
|
117
|
+
mock_get_recce_cloud_onboarding_state,
|
|
118
|
+
mock_verify_required_artifacts,
|
|
119
|
+
):
|
|
120
|
+
"""Test that --share-url automatically enables cloud and review mode"""
|
|
121
|
+
mock_state_loader = MagicMock(spec=CloudStateLoader)
|
|
122
|
+
mock_state_loader.verify.return_value = True
|
|
123
|
+
mock_state_loader.review_mode = True
|
|
124
|
+
mock_get_recce_cloud_onboarding_state.return_value = "completed"
|
|
125
|
+
mock_verify_required_artifacts.return_value = True, None
|
|
126
|
+
|
|
127
|
+
mock_state_loader_class.return_value = mock_state_loader
|
|
128
|
+
|
|
129
|
+
# Test with --share-url (should automatically enable cloud and review)
|
|
130
|
+
result = self.runner.invoke(
|
|
131
|
+
cli_command_server, ["--share-url", "https://cloud.recce.io/share/abc123", "--single-env"]
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# Should succeed
|
|
135
|
+
assert result.exit_code == 0
|
|
136
|
+
|
|
137
|
+
# Should create CloudStateLoader with share_id in cloud_options
|
|
138
|
+
mock_state_loader_class.assert_called_once()
|
|
139
|
+
call_args = mock_state_loader_class.call_args
|
|
140
|
+
assert call_args.kwargs["review_mode"] is True
|
|
141
|
+
assert "share_id" in call_args.kwargs["cloud_options"]
|
|
142
|
+
assert call_args.kwargs["cloud_options"]["share_id"] == "abc123"
|
|
143
|
+
|
|
144
|
+
mock_run.assert_called_once()
|
|
145
|
+
|
|
68
146
|
@patch.object(RecceContext, "verify_required_artifacts")
|
|
69
147
|
@patch("os.path.isdir", side_effect=lambda path: True if path == "existed_folder" else False)
|
|
70
148
|
@patch("recce.cli.uvicorn.run")
|
|
@@ -131,3 +209,28 @@ class TestCommandRun(TestCase):
|
|
|
131
209
|
|
|
132
210
|
self.runner.invoke(cli_command_run, [])
|
|
133
211
|
mock_cli_run.assert_called_once()
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class TestCommandUploadSession(TestCase):
|
|
215
|
+
def setUp(self):
|
|
216
|
+
self.runner = CliRunner()
|
|
217
|
+
pass
|
|
218
|
+
|
|
219
|
+
@patch("recce.cli.prepare_api_token", return_value="unittest_token")
|
|
220
|
+
@patch("recce.cli.upload_artifacts_to_session", return_value=0)
|
|
221
|
+
def test_cmd_upload_session(self, mock_upload_artifacts_to_session, mock_prepare_api_token):
|
|
222
|
+
self.runner.invoke(
|
|
223
|
+
cli_command_upload_session,
|
|
224
|
+
["--session-id", "unittest_session", "--api-token", mock_prepare_api_token.return_value],
|
|
225
|
+
)
|
|
226
|
+
mock_upload_artifacts_to_session.assert_called_once_with(
|
|
227
|
+
"target", session_id="unittest_session", token="unittest_token", debug=False
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
self.runner.invoke(
|
|
231
|
+
cli_command_snapshot,
|
|
232
|
+
["--snapshot-id", "unittest_session", "--api-token", mock_prepare_api_token.return_value],
|
|
233
|
+
)
|
|
234
|
+
mock_upload_artifacts_to_session.assert_called_once_with(
|
|
235
|
+
"target", session_id="unittest_session", token="unittest_token", debug=False
|
|
236
|
+
)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Test that CLI can be imported even when mcp is not available.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from unittest.mock import patch
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_cli_can_be_imported_without_mcp():
|
|
10
|
+
"""Test that recce.cli can be imported even if mcp package is not available"""
|
|
11
|
+
# This test verifies that the CLI module doesn't fail to import
|
|
12
|
+
# when mcp is not installed, since mcp is an optional dependency
|
|
13
|
+
from recce import cli
|
|
14
|
+
|
|
15
|
+
assert cli is not None
|
|
16
|
+
assert hasattr(cli, "cli")
|
|
17
|
+
assert hasattr(cli, "mcp_server")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_mcp_server_command_fails_gracefully_without_mcp():
|
|
21
|
+
"""Test that mcp-server command shows helpful error when mcp is not available"""
|
|
22
|
+
# Mock sys.modules to simulate mcp not being installed
|
|
23
|
+
with patch.dict(sys.modules, {"mcp": None, "mcp.server": None, "mcp.server.stdio": None, "mcp.types": None}):
|
|
24
|
+
# Remove mcp_server from modules to force reimport
|
|
25
|
+
if "recce.mcp_server" in sys.modules:
|
|
26
|
+
del sys.modules["recce.mcp_server"]
|
|
27
|
+
|
|
28
|
+
from recce.cli import mcp_server
|
|
29
|
+
|
|
30
|
+
# The function should exist
|
|
31
|
+
assert mcp_server is not None
|
|
32
|
+
|
|
33
|
+
# When called, it should handle ImportError gracefully
|
|
34
|
+
# (We can't easily test the actual execution without more mocking,
|
|
35
|
+
# but we've verified the function exists and can be imported)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_cli_command_exists():
|
|
39
|
+
"""Test that both server and mcp-server commands are registered"""
|
|
40
|
+
from recce.cli import cli
|
|
41
|
+
|
|
42
|
+
# Check that both commands exist
|
|
43
|
+
commands = {cmd.name for cmd in cli.commands.values()}
|
|
44
|
+
assert "server" in commands
|
|
45
|
+
assert "mcp_server" in commands or "mcp-server" in commands
|