recce-nightly 1.2.0.20250506__py3-none-any.whl → 1.26.0.20251124__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.
Potentially problematic release.
This version of recce-nightly might be problematic. Click here for more details.
- recce/VERSION +1 -1
- recce/__init__.py +27 -22
- recce/adapter/base.py +11 -14
- recce/adapter/dbt_adapter/__init__.py +810 -480
- recce/adapter/dbt_adapter/dbt_version.py +3 -0
- recce/adapter/sqlmesh_adapter.py +24 -35
- recce/apis/check_api.py +39 -28
- recce/apis/check_func.py +33 -27
- recce/apis/run_api.py +25 -19
- recce/apis/run_func.py +29 -23
- recce/artifact.py +119 -51
- recce/cli.py +1299 -323
- recce/config.py +42 -33
- recce/connect_to_cloud.py +138 -0
- recce/core.py +55 -47
- recce/data/404.html +1 -1
- recce/data/__next.__PAGE__.txt +10 -0
- recce/data/__next._full.txt +23 -0
- recce/data/__next._head.txt +8 -0
- recce/data/__next._index.txt +8 -0
- recce/data/__next._tree.txt +5 -0
- recce/data/_next/static/52aV_JrNUZU6dMFgvTQEO/_buildManifest.js +11 -0
- recce/data/_next/static/52aV_JrNUZU6dMFgvTQEO/_clientMiddlewareManifest.json +1 -0
- recce/data/_next/static/chunks/02b996c7f6a29a06.js +4 -0
- recce/data/_next/static/chunks/19c10d219a6a21ff.js +1 -0
- recce/data/_next/static/chunks/2df9ec28a061971d.js +11 -0
- recce/data/_next/static/chunks/3098c987393bda15.js +1 -0
- recce/data/_next/static/chunks/393dc43e483f717a.css +2 -0
- recce/data/_next/static/chunks/399e8d91a7e45073.js +2 -0
- recce/data/_next/static/chunks/4d0186f631230245.js +1 -0
- recce/data/_next/static/chunks/5794ba9e10a9c060.js +11 -0
- recce/data/_next/static/chunks/715761c929a3f28b.js +110 -0
- recce/data/_next/static/chunks/71f88fcc615bf282.js +1 -0
- recce/data/_next/static/chunks/80d2a95eaf1201ea.js +1 -0
- recce/data/_next/static/chunks/9979c6109bbbee35.js +1 -0
- recce/data/_next/static/chunks/99d638224186c118.js +1 -0
- recce/data/_next/static/chunks/d003eb36240e92f3.js +1 -0
- recce/data/_next/static/chunks/d3167cdfec4fc351.js +1 -0
- recce/data/_next/static/chunks/e124bccf574a3361.css +1 -0
- recce/data/_next/static/chunks/f40141db1bdb46f0.css +6 -0
- recce/data/_next/static/chunks/fcc53a88741a52f9.js +1 -0
- recce/data/_next/static/chunks/turbopack-b1920d28cfb1f28d.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.f9d58125.woff +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.076c2a93.woff2 +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.a4fa76b5.woff +0 -0
- recce/data/_next/static/media/montserrat-latin-800-normal.cde454cc.woff2 +0 -0
- recce/data/_next/static/media/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.b671449b.woff +0 -0
- recce/data/_next/static/media/montserrat-vietnamese-800-normal.9f7b8541.woff +0 -0
- recce/data/_next/static/media/montserrat-vietnamese-800-normal.f9eb854e.woff2 +0 -0
- recce/data/_next/static/media/reload-image.7aa931c7.svg +4 -0
- recce/data/_not-found/__next._full.txt +17 -0
- recce/data/_not-found/__next._head.txt +8 -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 +3 -0
- recce/data/_not-found.html +1 -0
- recce/data/_not-found.txt +17 -0
- recce/data/auth_callback.html +68 -0
- recce/data/imgs/reload-image.svg +4 -0
- recce/data/index.html +1 -27
- recce/data/index.txt +23 -7
- recce/diff.py +6 -12
- recce/event/__init__.py +86 -74
- recce/event/collector.py +33 -22
- recce/event/track.py +49 -27
- recce/exceptions.py +1 -1
- recce/git.py +7 -7
- recce/github.py +57 -53
- recce/mcp_server.py +716 -0
- recce/models/__init__.py +4 -1
- recce/models/check.py +6 -7
- recce/models/run.py +1 -0
- recce/models/types.py +131 -28
- recce/pull_request.py +27 -25
- recce/run.py +165 -121
- recce/server.py +303 -111
- 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 +188 -143
- recce/tasks/__init__.py +19 -3
- recce/tasks/core.py +11 -13
- recce/tasks/dataframe.py +82 -18
- recce/tasks/histogram.py +69 -34
- recce/tasks/lineage.py +2 -2
- recce/tasks/profile.py +152 -86
- recce/tasks/query.py +139 -87
- recce/tasks/rowcount.py +37 -31
- recce/tasks/schema.py +18 -15
- recce/tasks/top_k.py +35 -35
- recce/tasks/valuediff.py +216 -152
- recce/util/__init__.py +3 -0
- recce/util/api_token.py +80 -0
- recce/util/breaking.py +87 -85
- recce/util/cll.py +274 -219
- recce/util/io.py +22 -17
- recce/util/lineage.py +65 -16
- recce/util/logger.py +1 -1
- recce/util/onboarding_state.py +45 -0
- recce/util/perf_tracking.py +85 -0
- recce/util/recce_cloud.py +322 -72
- recce/util/singleton.py +4 -4
- recce/yaml/__init__.py +7 -10
- recce_cloud/__init__.py +24 -0
- recce_cloud/api/__init__.py +17 -0
- recce_cloud/api/base.py +111 -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 +76 -0
- recce_cloud/api/gitlab.py +82 -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 +245 -0
- recce_cloud/upload.py +214 -0
- {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.26.0.20251124.dist-info}/METADATA +68 -37
- recce_nightly-1.26.0.20251124.dist-info/RECORD +180 -0
- {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.26.0.20251124.dist-info}/WHEEL +1 -1
- {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.26.0.20251124.dist-info}/top_level.txt +1 -0
- tests/adapter/dbt_adapter/conftest.py +9 -5
- tests/adapter/dbt_adapter/dbt_test_helper.py +37 -22
- tests/adapter/dbt_adapter/test_dbt_adapter.py +0 -15
- tests/adapter/dbt_adapter/test_dbt_cll.py +656 -41
- tests/adapter/dbt_adapter/test_selector.py +22 -21
- 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 +333 -0
- tests/tasks/conftest.py +1 -1
- tests/tasks/test_histogram.py +58 -66
- tests/tasks/test_lineage.py +36 -23
- tests/tasks/test_preset_checks.py +45 -31
- tests/tasks/test_profile.py +339 -15
- tests/tasks/test_query.py +46 -46
- tests/tasks/test_row_count.py +65 -46
- tests/tasks/test_schema.py +65 -42
- tests/tasks/test_top_k.py +22 -18
- tests/tasks/test_valuediff.py +43 -32
- tests/test_cli.py +174 -60
- tests/test_cli_mcp_optional.py +45 -0
- tests/test_cloud_listing_cli.py +324 -0
- tests/test_config.py +7 -9
- tests/test_connect_to_cloud.py +82 -0
- tests/test_core.py +151 -4
- tests/test_dbt.py +7 -7
- tests/test_mcp_server.py +332 -0
- tests/test_pull_request.py +1 -1
- tests/test_server.py +25 -19
- tests/test_summary.py +29 -17
- recce/data/_next/static/Kcbs3GEIyH2LxgLYat0es/_buildManifest.js +0 -1
- recce/data/_next/static/chunks/1f229bf6-d9fe92e56db8d93b.js +0 -1
- recce/data/_next/static/chunks/29e3cc0d-8c150e37dff9631b.js +0 -1
- recce/data/_next/static/chunks/368-7587b306577df275.js +0 -65
- recce/data/_next/static/chunks/36e1c10d-bb0210cbd6573a8d.js +0 -1
- recce/data/_next/static/chunks/3998a672-eaad84bdd88cc73e.js +0 -1
- recce/data/_next/static/chunks/3a92ee20-3b5d922d4157af5e.js +0 -1
- recce/data/_next/static/chunks/450c323b-1bb5db526e54435a.js +0 -1
- recce/data/_next/static/chunks/47d8844f-79a1b53c66a7d7ec.js +0 -1
- recce/data/_next/static/chunks/6dc81886-c94b9b91bc2c3caf.js +0 -1
- recce/data/_next/static/chunks/6ef81909-694dc38134099299.js +0 -1
- recce/data/_next/static/chunks/700-3b65fc3666820d00.js +0 -2
- recce/data/_next/static/chunks/7a8a3e83-d7fa409d97b38b2b.js +0 -1
- recce/data/_next/static/chunks/7f27ae6c-413f6b869a04183a.js +0 -1
- recce/data/_next/static/chunks/8d700b6a-f0b1f6b9e0d97ce2.js +0 -1
- recce/data/_next/static/chunks/9746af58-d74bef4d03eea6ab.js +0 -1
- recce/data/_next/static/chunks/a30376cd-7d806e1602f2dc3a.js +0 -1
- recce/data/_next/static/chunks/app/_not-found/page-8a886fa0855c3105.js +0 -1
- recce/data/_next/static/chunks/app/layout-9102e22cb73f74d6.js +0 -1
- recce/data/_next/static/chunks/app/page-cee661090afbd6aa.js +0 -1
- recce/data/_next/static/chunks/b63b1b3f-7395c74e11a14e95.js +0 -1
- recce/data/_next/static/chunks/c132bf7d-8102037f9ccf372a.js +0 -1
- recce/data/_next/static/chunks/c1ceaa8b-a1e442154d23515e.js +0 -1
- recce/data/_next/static/chunks/cd9f8d63-cf0d5a7b0f7a92e8.js +0 -54
- recce/data/_next/static/chunks/ce84277d-f42c2c58049cea2d.js +0 -1
- recce/data/_next/static/chunks/e24bf851-0f8cbc99656833e7.js +0 -1
- recce/data/_next/static/chunks/fee69bc6-f17d36c080742e74.js +0 -1
- recce/data/_next/static/chunks/framework-ded83d71b51ce901.js +0 -1
- recce/data/_next/static/chunks/main-a0859f1f36d0aa6c.js +0 -1
- recce/data/_next/static/chunks/main-app-0225a2255968e566.js +0 -1
- recce/data/_next/static/chunks/pages/_app-d5672bf3d8b6371b.js +0 -1
- recce/data/_next/static/chunks/pages/_error-ed75be3f25588548.js +0 -1
- recce/data/_next/static/chunks/webpack-567d72f0bc0820d5.js +0 -1
- recce/data/_next/static/css/c9ecb46a4b21c126.css +0 -14
- recce/data/_next/static/media/montserrat-cyrillic-800-normal.22628180.woff2 +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-800-normal.31d693bb.woff +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.7e2c1e62.woff +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-800-normal.97e20d5e.woff +0 -0
- recce/data/_next/static/media/montserrat-latin-ext-800-normal.013b84f9.woff2 +0 -0
- recce/data/_next/static/media/montserrat-latin-ext-800-normal.aff52ab0.woff +0 -0
- recce/data/_next/static/media/montserrat-vietnamese-800-normal.5f21869b.woff +0 -0
- recce/data/_next/static/media/montserrat-vietnamese-800-normal.c0035377.woff2 +0 -0
- recce/state.py +0 -753
- recce_nightly-1.2.0.20250506.dist-info/RECORD +0 -142
- tests/test_state.py +0 -123
- /recce/data/_next/static/{Kcbs3GEIyH2LxgLYat0es → 52aV_JrNUZU6dMFgvTQEO}/_ssgManifest.js +0 -0
- /recce/data/_next/static/chunks/{polyfills-42372ed130431b0a.js → a6dad97d9634a72d.js} +0 -0
- {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.26.0.20251124.dist-info}/entry_points.txt +0 -0
- {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.26.0.20251124.dist-info}/licenses/LICENSE +0 -0
tests/test_cli.py
CHANGED
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
from unittest import TestCase
|
|
2
|
-
from unittest.mock import
|
|
2
|
+
from unittest.mock import MagicMock, patch
|
|
3
3
|
|
|
4
4
|
from click.testing import CliRunner
|
|
5
5
|
|
|
6
|
-
from recce.cli import
|
|
6
|
+
from recce.cli import run as cli_command_run
|
|
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
|
|
7
10
|
from recce.core import RecceContext
|
|
8
|
-
from recce.state import
|
|
11
|
+
from recce.state import CloudStateLoader
|
|
9
12
|
|
|
10
13
|
|
|
11
14
|
def test_cmd_version():
|
|
12
|
-
from recce.cli import version
|
|
13
15
|
from recce import __version__
|
|
16
|
+
from recce.cli import version
|
|
17
|
+
|
|
14
18
|
runner = CliRunner()
|
|
15
19
|
result = runner.invoke(version, [])
|
|
16
20
|
assert result.exit_code == 0
|
|
17
|
-
assert result.output.replace(
|
|
21
|
+
assert result.output.replace("\n", "") == __version__
|
|
18
22
|
|
|
19
23
|
|
|
20
24
|
class TestCommandServer(TestCase):
|
|
@@ -22,90 +26,175 @@ class TestCommandServer(TestCase):
|
|
|
22
26
|
self.runner = CliRunner()
|
|
23
27
|
pass
|
|
24
28
|
|
|
25
|
-
@patch.object(RecceContext,
|
|
26
|
-
@patch(
|
|
29
|
+
@patch.object(RecceContext, "verify_required_artifacts")
|
|
30
|
+
@patch("recce.cli.uvicorn.run")
|
|
27
31
|
def test_cmd_server(self, mock_run, mock_verify_required_artifacts):
|
|
28
32
|
from recce.server import app
|
|
33
|
+
|
|
29
34
|
mock_verify_required_artifacts.return_value = True, None
|
|
30
|
-
self.runner.invoke(cli_command_server, [
|
|
31
|
-
mock_run.assert_called_once_with(app, host=
|
|
35
|
+
self.runner.invoke(cli_command_server, ["--host", "unittest", "--port", 5566, "--single-env"])
|
|
36
|
+
mock_run.assert_called_once_with(app, host="unittest", port=5566, lifespan="on")
|
|
32
37
|
|
|
33
|
-
@patch(
|
|
38
|
+
@patch("recce.cli.uvicorn.run")
|
|
34
39
|
def test_cmd_server_with_cloud_without_password(self, mock_run):
|
|
35
40
|
# Should fail if no password is provided
|
|
36
|
-
result = self.runner.invoke(cli_command_server, [
|
|
41
|
+
result = self.runner.invoke(cli_command_server, ["--cloud"])
|
|
37
42
|
assert result.exit_code == 1
|
|
38
43
|
|
|
39
|
-
@patch(
|
|
44
|
+
@patch("recce.cli.uvicorn.run")
|
|
40
45
|
def test_cmd_server_with_cloud_without_token(self, mock_run):
|
|
41
46
|
# Should fail if no token is provided
|
|
42
|
-
result = self.runner.invoke(cli_command_server, [
|
|
47
|
+
result = self.runner.invoke(cli_command_server, ["--cloud", "--password", "unittest"])
|
|
43
48
|
assert result.exit_code == 1
|
|
44
49
|
|
|
45
|
-
@patch.object(RecceContext,
|
|
46
|
-
@patch(
|
|
47
|
-
@patch(
|
|
48
|
-
@patch(
|
|
49
|
-
def test_cmd_server_with_cloud(
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
@patch.object(RecceContext, "verify_required_artifacts")
|
|
51
|
+
@patch("recce.util.recce_cloud.get_recce_cloud_onboarding_state")
|
|
52
|
+
@patch("recce.cli.uvicorn.run")
|
|
53
|
+
@patch("recce.cli.CloudStateLoader")
|
|
54
|
+
def test_cmd_server_with_cloud(
|
|
55
|
+
self, mock_state_loader_class, mock_run, mock_get_recce_cloud_onboarding_state, mock_verify_required_artifacts
|
|
56
|
+
):
|
|
57
|
+
mock_state_loader = MagicMock(spec=CloudStateLoader)
|
|
58
|
+
mock_state_loader.verify.return_value = True
|
|
59
|
+
mock_state_loader.review_mode = True
|
|
60
|
+
mock_get_recce_cloud_onboarding_state.return_value = "completed"
|
|
61
|
+
mock_verify_required_artifacts.return_value = True, None
|
|
62
|
+
|
|
63
|
+
mock_state_loader_class.return_value = mock_state_loader
|
|
64
|
+
self.runner.invoke(
|
|
65
|
+
cli_command_server, ["--cloud", "--password", "unittest", "--cloud-token", "unittest", "--single-env"]
|
|
66
|
+
)
|
|
67
|
+
mock_state_loader_class.assert_called_once()
|
|
68
|
+
mock_run.assert_called_once()
|
|
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)
|
|
52
122
|
mock_state_loader.verify.return_value = True
|
|
53
123
|
mock_state_loader.review_mode = True
|
|
54
|
-
mock_get_recce_cloud_onboarding_state.return_value =
|
|
124
|
+
mock_get_recce_cloud_onboarding_state.return_value = "completed"
|
|
55
125
|
mock_verify_required_artifacts.return_value = True, None
|
|
56
126
|
|
|
57
127
|
mock_state_loader_class.return_value = mock_state_loader
|
|
58
|
-
|
|
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
|
|
59
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
|
+
|
|
60
144
|
mock_run.assert_called_once()
|
|
61
145
|
|
|
62
|
-
@patch.object(RecceContext,
|
|
63
|
-
@patch(
|
|
64
|
-
@patch(
|
|
65
|
-
@patch(
|
|
66
|
-
def test_cmd_server_with_single_env(self,
|
|
67
|
-
mock_app_state, mock_run, mock_isdir, mock_verify_required_artifacts):
|
|
146
|
+
@patch.object(RecceContext, "verify_required_artifacts")
|
|
147
|
+
@patch("os.path.isdir", side_effect=lambda path: True if path == "existed_folder" else False)
|
|
148
|
+
@patch("recce.cli.uvicorn.run")
|
|
149
|
+
@patch("recce.server.AppState")
|
|
150
|
+
def test_cmd_server_with_single_env(self, mock_app_state, mock_run, mock_isdir, mock_verify_required_artifacts):
|
|
68
151
|
mock_verify_required_artifacts.return_value = True, None
|
|
69
|
-
self.runner.invoke(
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
])
|
|
152
|
+
self.runner.invoke(
|
|
153
|
+
cli_command_server,
|
|
154
|
+
["--single-env", "--target-path", "existed_folder", "--target-base-path", "non_existed_folder"],
|
|
155
|
+
)
|
|
74
156
|
mock_run.assert_called_once()
|
|
75
157
|
|
|
76
158
|
# Onboarding mode should be set to True
|
|
77
159
|
app_state_call_args = mock_app_state.call_args
|
|
78
|
-
app_state_flag = app_state_call_args.kwargs[
|
|
79
|
-
assert
|
|
80
|
-
assert app_state_flag[
|
|
81
|
-
assert
|
|
82
|
-
assert app_state_flag[
|
|
160
|
+
app_state_flag = app_state_call_args.kwargs["flag"]
|
|
161
|
+
assert "single_env_onboarding" in app_state_flag
|
|
162
|
+
assert app_state_flag["single_env_onboarding"] is True
|
|
163
|
+
assert "show_relaunch_hint" in app_state_flag
|
|
164
|
+
assert app_state_flag["show_relaunch_hint"] is True
|
|
83
165
|
|
|
84
166
|
# The target_base_path should be set to the same as target_path
|
|
85
167
|
verify_required_artifacts_args = mock_verify_required_artifacts.call_args
|
|
86
|
-
assert
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
@patch(
|
|
92
|
-
@patch(
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
168
|
+
assert (
|
|
169
|
+
verify_required_artifacts_args.kwargs["target_path"]
|
|
170
|
+
== verify_required_artifacts_args.kwargs["target_base_path"]
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
@patch.object(RecceContext, "verify_required_artifacts")
|
|
174
|
+
@patch("os.path.isdir", side_effect=lambda path: True if path == "existed_folder" else False)
|
|
175
|
+
@patch("recce.cli.uvicorn.run")
|
|
176
|
+
@patch("recce.server.AppState")
|
|
177
|
+
def test_cmd_server_with_single_env_but_review_mode_enabled(
|
|
178
|
+
self, mock_app_state, mock_run, mock_isdir, mock_verify_required_artifacts
|
|
179
|
+
):
|
|
96
180
|
mock_verify_required_artifacts.return_value = True, None
|
|
97
|
-
self.runner.invoke(
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
181
|
+
self.runner.invoke(
|
|
182
|
+
cli_command_server,
|
|
183
|
+
[
|
|
184
|
+
"existed_state_file",
|
|
185
|
+
"--review",
|
|
186
|
+
"--single-env",
|
|
187
|
+
"--target-path",
|
|
188
|
+
"existed_folder",
|
|
189
|
+
"--target-base-path",
|
|
190
|
+
"non_existed_folder",
|
|
191
|
+
],
|
|
192
|
+
)
|
|
104
193
|
mock_run.assert_called_once()
|
|
105
194
|
app_state_call_args = mock_app_state.call_args
|
|
106
|
-
app_state_flag = app_state_call_args.kwargs[
|
|
107
|
-
assert
|
|
108
|
-
assert app_state_flag[
|
|
195
|
+
app_state_flag = app_state_call_args.kwargs["flag"]
|
|
196
|
+
assert "single_env_onboarding" in app_state_flag
|
|
197
|
+
assert app_state_flag["single_env_onboarding"] is False
|
|
109
198
|
|
|
110
199
|
|
|
111
200
|
class TestCommandRun(TestCase):
|
|
@@ -113,10 +202,35 @@ class TestCommandRun(TestCase):
|
|
|
113
202
|
self.runner = CliRunner()
|
|
114
203
|
pass
|
|
115
204
|
|
|
116
|
-
@patch.object(RecceContext,
|
|
117
|
-
@patch(
|
|
205
|
+
@patch.object(RecceContext, "verify_required_artifacts")
|
|
206
|
+
@patch("recce.cli.cli_run")
|
|
118
207
|
def test_cmd_run(self, mock_cli_run, mock_verify_required_artifacts):
|
|
119
208
|
mock_verify_required_artifacts.return_value = True, None
|
|
120
209
|
|
|
121
210
|
self.runner.invoke(cli_command_run, [])
|
|
122
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
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from unittest.mock import Mock, patch
|
|
3
|
+
|
|
4
|
+
from click.testing import CliRunner
|
|
5
|
+
|
|
6
|
+
from recce.cli import list_organizations, list_projects, list_sessions
|
|
7
|
+
from recce.exceptions import RecceConfigException
|
|
8
|
+
from recce.util.recce_cloud import RecceCloudException
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestCloudListingCLI(unittest.TestCase):
|
|
12
|
+
"""Test cases for the cloud listing CLI commands."""
|
|
13
|
+
|
|
14
|
+
def setUp(self):
|
|
15
|
+
"""Set up test fixtures."""
|
|
16
|
+
self.runner = CliRunner()
|
|
17
|
+
|
|
18
|
+
@patch("recce.cli.prepare_api_token")
|
|
19
|
+
@patch("recce.util.recce_cloud.RecceCloud")
|
|
20
|
+
def test_list_organizations_success(self, mock_recce_cloud, mock_prepare_token):
|
|
21
|
+
"""Test successful list-organizations command."""
|
|
22
|
+
# Setup mocks
|
|
23
|
+
mock_prepare_token.return_value = "test-token"
|
|
24
|
+
mock_cloud_instance = Mock()
|
|
25
|
+
mock_cloud_instance.list_organizations.return_value = [
|
|
26
|
+
{"id": 1, "name": "org1", "display_name": "Organization 1"},
|
|
27
|
+
{"id": 2, "name": "org2", "display_name": "Organization 2"},
|
|
28
|
+
]
|
|
29
|
+
mock_recce_cloud.return_value = mock_cloud_instance
|
|
30
|
+
|
|
31
|
+
# Test command
|
|
32
|
+
result = self.runner.invoke(list_organizations, [])
|
|
33
|
+
|
|
34
|
+
# Assertions
|
|
35
|
+
self.assertEqual(result.exit_code, 0)
|
|
36
|
+
self.assertIn("Organizations", result.output)
|
|
37
|
+
self.assertIn("org1", result.output)
|
|
38
|
+
self.assertIn("Organization 1", result.output)
|
|
39
|
+
self.assertIn("org2", result.output)
|
|
40
|
+
self.assertIn("Organization 2", result.output)
|
|
41
|
+
mock_cloud_instance.list_organizations.assert_called_once()
|
|
42
|
+
|
|
43
|
+
@patch("recce.cli.prepare_api_token")
|
|
44
|
+
@patch("recce.util.recce_cloud.RecceCloud")
|
|
45
|
+
def test_list_organizations_empty(self, mock_recce_cloud, mock_prepare_token):
|
|
46
|
+
"""Test list-organizations command with no organizations."""
|
|
47
|
+
# Setup mocks
|
|
48
|
+
mock_prepare_token.return_value = "test-token"
|
|
49
|
+
mock_cloud_instance = Mock()
|
|
50
|
+
mock_cloud_instance.list_organizations.return_value = []
|
|
51
|
+
mock_recce_cloud.return_value = mock_cloud_instance
|
|
52
|
+
|
|
53
|
+
# Test command
|
|
54
|
+
result = self.runner.invoke(list_organizations, [])
|
|
55
|
+
|
|
56
|
+
# Assertions
|
|
57
|
+
self.assertEqual(result.exit_code, 0)
|
|
58
|
+
self.assertIn("No organizations found", result.output)
|
|
59
|
+
|
|
60
|
+
@patch("recce.cli.prepare_api_token")
|
|
61
|
+
def test_list_organizations_invalid_token(self, mock_prepare_token):
|
|
62
|
+
"""Test list-organizations command with invalid token."""
|
|
63
|
+
# Setup mock to raise exception
|
|
64
|
+
mock_prepare_token.side_effect = RecceConfigException("Invalid token")
|
|
65
|
+
|
|
66
|
+
# Test command
|
|
67
|
+
result = self.runner.invoke(list_organizations, [])
|
|
68
|
+
|
|
69
|
+
# Assertions
|
|
70
|
+
self.assertEqual(result.exit_code, 1)
|
|
71
|
+
|
|
72
|
+
@patch("recce.cli.prepare_api_token")
|
|
73
|
+
@patch("recce.util.recce_cloud.RecceCloud")
|
|
74
|
+
def test_list_projects_with_cli_arg(self, mock_recce_cloud, mock_prepare_token):
|
|
75
|
+
"""Test list-projects command with CLI argument."""
|
|
76
|
+
# Setup mocks
|
|
77
|
+
mock_prepare_token.return_value = "test-token"
|
|
78
|
+
mock_cloud_instance = Mock()
|
|
79
|
+
mock_cloud_instance.list_projects.return_value = [
|
|
80
|
+
{"id": 1, "name": "project1", "display_name": "Project 1"},
|
|
81
|
+
{"id": 2, "name": "project2", "display_name": "Project 2"},
|
|
82
|
+
]
|
|
83
|
+
mock_recce_cloud.return_value = mock_cloud_instance
|
|
84
|
+
|
|
85
|
+
# Test command with CLI argument
|
|
86
|
+
result = self.runner.invoke(list_projects, ["--organization", "8"])
|
|
87
|
+
|
|
88
|
+
# Assertions
|
|
89
|
+
self.assertEqual(result.exit_code, 0)
|
|
90
|
+
self.assertIn("Projects in Organization 8", result.output)
|
|
91
|
+
self.assertIn("project1", result.output)
|
|
92
|
+
self.assertIn("Project 1", result.output)
|
|
93
|
+
mock_cloud_instance.list_projects.assert_called_once_with("8")
|
|
94
|
+
|
|
95
|
+
@patch("recce.cli.prepare_api_token")
|
|
96
|
+
@patch("recce.util.recce_cloud.RecceCloud")
|
|
97
|
+
def test_list_projects_with_env_var(self, mock_recce_cloud, mock_prepare_token):
|
|
98
|
+
"""Test list-projects command with environment variable."""
|
|
99
|
+
# Setup mocks
|
|
100
|
+
mock_prepare_token.return_value = "test-token"
|
|
101
|
+
mock_cloud_instance = Mock()
|
|
102
|
+
mock_cloud_instance.list_projects.return_value = [{"id": 1, "name": "project1", "display_name": "Project 1"}]
|
|
103
|
+
mock_recce_cloud.return_value = mock_cloud_instance
|
|
104
|
+
|
|
105
|
+
# Test command with environment variable
|
|
106
|
+
result = self.runner.invoke(list_projects, [], env={"RECCE_ORGANIZATION_ID": "8"})
|
|
107
|
+
|
|
108
|
+
# Assertions
|
|
109
|
+
self.assertEqual(result.exit_code, 0)
|
|
110
|
+
self.assertIn("Projects in Organization 8", result.output)
|
|
111
|
+
self.assertIn("project1", result.output)
|
|
112
|
+
mock_cloud_instance.list_projects.assert_called_once_with("8")
|
|
113
|
+
|
|
114
|
+
@patch("recce.cli.prepare_api_token")
|
|
115
|
+
@patch("recce.util.recce_cloud.RecceCloud")
|
|
116
|
+
def test_list_projects_cli_overrides_env(self, mock_recce_cloud, mock_prepare_token):
|
|
117
|
+
"""Test list-projects command where CLI argument overrides environment variable."""
|
|
118
|
+
# Setup mocks
|
|
119
|
+
mock_prepare_token.return_value = "test-token"
|
|
120
|
+
mock_cloud_instance = Mock()
|
|
121
|
+
mock_cloud_instance.list_projects.return_value = [{"id": 1, "name": "project1", "display_name": "Project 1"}]
|
|
122
|
+
mock_recce_cloud.return_value = mock_cloud_instance
|
|
123
|
+
|
|
124
|
+
# Test command with both env var and CLI arg (CLI should win)
|
|
125
|
+
result = self.runner.invoke(list_projects, ["--organization", "10"], env={"RECCE_ORGANIZATION_ID": "8"})
|
|
126
|
+
|
|
127
|
+
# Assertions
|
|
128
|
+
self.assertEqual(result.exit_code, 0)
|
|
129
|
+
self.assertIn("Projects in Organization 10", result.output)
|
|
130
|
+
# Verify CLI argument (10) was used, not env var (8)
|
|
131
|
+
mock_cloud_instance.list_projects.assert_called_once_with("10")
|
|
132
|
+
|
|
133
|
+
@patch("recce.cli.prepare_api_token")
|
|
134
|
+
def test_list_projects_missing_organization(self, mock_prepare_token):
|
|
135
|
+
"""Test list-projects command with missing organization ID."""
|
|
136
|
+
# Setup mocks
|
|
137
|
+
mock_prepare_token.return_value = "test-token"
|
|
138
|
+
|
|
139
|
+
# Test command without organization ID
|
|
140
|
+
result = self.runner.invoke(list_projects, [])
|
|
141
|
+
|
|
142
|
+
# Assertions
|
|
143
|
+
self.assertEqual(result.exit_code, 1)
|
|
144
|
+
self.assertIn("Organization ID is required", result.output)
|
|
145
|
+
self.assertIn("--organization", result.output)
|
|
146
|
+
self.assertIn("RECCE_ORGANIZATION_ID", result.output)
|
|
147
|
+
|
|
148
|
+
@patch("recce.cli.prepare_api_token")
|
|
149
|
+
@patch("recce.util.recce_cloud.RecceCloud")
|
|
150
|
+
def test_list_projects_empty(self, mock_recce_cloud, mock_prepare_token):
|
|
151
|
+
"""Test list-projects command with no projects."""
|
|
152
|
+
# Setup mocks
|
|
153
|
+
mock_prepare_token.return_value = "test-token"
|
|
154
|
+
mock_cloud_instance = Mock()
|
|
155
|
+
mock_cloud_instance.list_projects.return_value = []
|
|
156
|
+
mock_recce_cloud.return_value = mock_cloud_instance
|
|
157
|
+
|
|
158
|
+
# Test command
|
|
159
|
+
result = self.runner.invoke(list_projects, ["--organization", "8"])
|
|
160
|
+
|
|
161
|
+
# Assertions
|
|
162
|
+
self.assertEqual(result.exit_code, 0)
|
|
163
|
+
self.assertIn("No projects found in organization 8", result.output)
|
|
164
|
+
|
|
165
|
+
@patch("recce.cli.prepare_api_token")
|
|
166
|
+
@patch("recce.util.recce_cloud.RecceCloud")
|
|
167
|
+
def test_list_sessions_with_cli_args(self, mock_recce_cloud, mock_prepare_token):
|
|
168
|
+
"""Test list-sessions command with CLI arguments."""
|
|
169
|
+
# Setup mocks
|
|
170
|
+
mock_prepare_token.return_value = "test-token"
|
|
171
|
+
mock_cloud_instance = Mock()
|
|
172
|
+
mock_cloud_instance.list_sessions.return_value = [
|
|
173
|
+
{"id": "session1", "name": "PR-123", "is_base": False},
|
|
174
|
+
{"id": "session2", "name": "Base Session", "is_base": True},
|
|
175
|
+
]
|
|
176
|
+
mock_recce_cloud.return_value = mock_cloud_instance
|
|
177
|
+
|
|
178
|
+
# Test command with CLI arguments
|
|
179
|
+
result = self.runner.invoke(list_sessions, ["--organization", "8", "--project", "7"])
|
|
180
|
+
|
|
181
|
+
# Assertions
|
|
182
|
+
self.assertEqual(result.exit_code, 0)
|
|
183
|
+
self.assertIn("Sessions in Project 7", result.output)
|
|
184
|
+
self.assertIn("PR-123", result.output)
|
|
185
|
+
self.assertIn("Base Session", result.output)
|
|
186
|
+
self.assertIn("✓", result.output) # Base session marker
|
|
187
|
+
mock_cloud_instance.list_sessions.assert_called_once_with("8", "7")
|
|
188
|
+
|
|
189
|
+
@patch("recce.cli.prepare_api_token")
|
|
190
|
+
@patch("recce.util.recce_cloud.RecceCloud")
|
|
191
|
+
def test_list_sessions_with_env_vars(self, mock_recce_cloud, mock_prepare_token):
|
|
192
|
+
"""Test list-sessions command with environment variables."""
|
|
193
|
+
# Setup mocks
|
|
194
|
+
mock_prepare_token.return_value = "test-token"
|
|
195
|
+
mock_cloud_instance = Mock()
|
|
196
|
+
mock_cloud_instance.list_sessions.return_value = [{"id": "session1", "name": "Session 1", "is_base": False}]
|
|
197
|
+
mock_recce_cloud.return_value = mock_cloud_instance
|
|
198
|
+
|
|
199
|
+
# Test command with environment variables
|
|
200
|
+
result = self.runner.invoke(list_sessions, [], env={"RECCE_ORGANIZATION_ID": "8", "RECCE_PROJECT_ID": "7"})
|
|
201
|
+
|
|
202
|
+
# Assertions
|
|
203
|
+
self.assertEqual(result.exit_code, 0)
|
|
204
|
+
self.assertIn("Sessions in Project 7", result.output)
|
|
205
|
+
self.assertIn("Session 1", result.output)
|
|
206
|
+
mock_cloud_instance.list_sessions.assert_called_once_with("8", "7")
|
|
207
|
+
|
|
208
|
+
@patch("recce.cli.prepare_api_token")
|
|
209
|
+
@patch("recce.util.recce_cloud.RecceCloud")
|
|
210
|
+
def test_list_sessions_mixed_env_and_cli(self, mock_recce_cloud, mock_prepare_token):
|
|
211
|
+
"""Test list-sessions command with mixed environment variables and CLI args."""
|
|
212
|
+
# Setup mocks
|
|
213
|
+
mock_prepare_token.return_value = "test-token"
|
|
214
|
+
mock_cloud_instance = Mock()
|
|
215
|
+
mock_cloud_instance.list_sessions.return_value = [{"id": "session1", "name": "Session 1", "is_base": False}]
|
|
216
|
+
mock_recce_cloud.return_value = mock_cloud_instance
|
|
217
|
+
|
|
218
|
+
# Test command with env var for org and CLI arg for project
|
|
219
|
+
result = self.runner.invoke(list_sessions, ["--project", "9"], env={"RECCE_ORGANIZATION_ID": "8"})
|
|
220
|
+
|
|
221
|
+
# Assertions
|
|
222
|
+
self.assertEqual(result.exit_code, 0)
|
|
223
|
+
self.assertIn("Sessions in Project 9", result.output)
|
|
224
|
+
# Verify it used env var for org (8) and CLI arg for project (9)
|
|
225
|
+
mock_cloud_instance.list_sessions.assert_called_once_with("8", "9")
|
|
226
|
+
|
|
227
|
+
@patch("recce.cli.prepare_api_token")
|
|
228
|
+
def test_list_sessions_missing_organization(self, mock_prepare_token):
|
|
229
|
+
"""Test list-sessions command with missing organization ID."""
|
|
230
|
+
# Setup mocks
|
|
231
|
+
mock_prepare_token.return_value = "test-token"
|
|
232
|
+
|
|
233
|
+
# Test command without organization ID
|
|
234
|
+
result = self.runner.invoke(list_sessions, ["--project", "7"])
|
|
235
|
+
|
|
236
|
+
# Assertions
|
|
237
|
+
self.assertEqual(result.exit_code, 1)
|
|
238
|
+
self.assertIn("Organization ID is required", result.output)
|
|
239
|
+
self.assertIn("--organization", result.output)
|
|
240
|
+
self.assertIn("RECCE_ORGANIZATION_ID", result.output)
|
|
241
|
+
|
|
242
|
+
@patch("recce.cli.prepare_api_token")
|
|
243
|
+
def test_list_sessions_missing_project(self, mock_prepare_token):
|
|
244
|
+
"""Test list-sessions command with missing project ID."""
|
|
245
|
+
# Setup mocks
|
|
246
|
+
mock_prepare_token.return_value = "test-token"
|
|
247
|
+
|
|
248
|
+
# Test command without project ID
|
|
249
|
+
result = self.runner.invoke(list_sessions, ["--organization", "8"])
|
|
250
|
+
|
|
251
|
+
# Assertions
|
|
252
|
+
self.assertEqual(result.exit_code, 1)
|
|
253
|
+
self.assertIn("Project ID is required", result.output)
|
|
254
|
+
self.assertIn("--project", result.output)
|
|
255
|
+
self.assertIn("RECCE_PROJECT_ID", result.output)
|
|
256
|
+
|
|
257
|
+
@patch("recce.cli.prepare_api_token")
|
|
258
|
+
@patch("recce.util.recce_cloud.RecceCloud")
|
|
259
|
+
def test_list_sessions_empty(self, mock_recce_cloud, mock_prepare_token):
|
|
260
|
+
"""Test list-sessions command with no sessions."""
|
|
261
|
+
# Setup mocks
|
|
262
|
+
mock_prepare_token.return_value = "test-token"
|
|
263
|
+
mock_cloud_instance = Mock()
|
|
264
|
+
mock_cloud_instance.list_sessions.return_value = []
|
|
265
|
+
mock_recce_cloud.return_value = mock_cloud_instance
|
|
266
|
+
|
|
267
|
+
# Test command
|
|
268
|
+
result = self.runner.invoke(list_sessions, ["--organization", "8", "--project", "7"])
|
|
269
|
+
|
|
270
|
+
# Assertions
|
|
271
|
+
self.assertEqual(result.exit_code, 0)
|
|
272
|
+
self.assertIn("No sessions found in project 7", result.output)
|
|
273
|
+
|
|
274
|
+
@patch("recce.cli.prepare_api_token")
|
|
275
|
+
@patch("recce.util.recce_cloud.RecceCloud")
|
|
276
|
+
def test_list_sessions_api_error(self, mock_recce_cloud, mock_prepare_token):
|
|
277
|
+
"""Test list-sessions command with API error."""
|
|
278
|
+
# Setup mocks
|
|
279
|
+
mock_prepare_token.return_value = "test-token"
|
|
280
|
+
mock_cloud_instance = Mock()
|
|
281
|
+
mock_cloud_instance.list_sessions.side_effect = RecceCloudException("Access denied", "Forbidden", 403)
|
|
282
|
+
mock_recce_cloud.return_value = mock_cloud_instance
|
|
283
|
+
|
|
284
|
+
# Test command
|
|
285
|
+
result = self.runner.invoke(list_sessions, ["--organization", "8", "--project", "7"])
|
|
286
|
+
|
|
287
|
+
# Assertions
|
|
288
|
+
self.assertEqual(result.exit_code, 1)
|
|
289
|
+
self.assertIn("Error", result.output)
|
|
290
|
+
|
|
291
|
+
@patch("recce.cli.prepare_api_token")
|
|
292
|
+
@patch("recce.util.recce_cloud.RecceCloud")
|
|
293
|
+
def test_sessions_base_session_display(self, mock_recce_cloud, mock_prepare_token):
|
|
294
|
+
"""Test that base sessions are properly marked with checkmark."""
|
|
295
|
+
# Setup mocks
|
|
296
|
+
mock_prepare_token.return_value = "test-token"
|
|
297
|
+
mock_cloud_instance = Mock()
|
|
298
|
+
mock_cloud_instance.list_sessions.return_value = [
|
|
299
|
+
{"id": "session1", "name": "Regular Session", "is_base": False},
|
|
300
|
+
{"id": "session2", "name": "Base Session", "is_base": True},
|
|
301
|
+
{"id": "session3", "name": "Another Regular", "is_base": False},
|
|
302
|
+
]
|
|
303
|
+
mock_recce_cloud.return_value = mock_cloud_instance
|
|
304
|
+
|
|
305
|
+
# Test command
|
|
306
|
+
result = self.runner.invoke(list_sessions, ["--organization", "8", "--project", "7"])
|
|
307
|
+
|
|
308
|
+
# Assertions
|
|
309
|
+
self.assertEqual(result.exit_code, 0)
|
|
310
|
+
output_lines = result.output.split("\n")
|
|
311
|
+
|
|
312
|
+
# Find the base session line and verify it has the checkmark
|
|
313
|
+
base_session_line = None
|
|
314
|
+
for line in output_lines:
|
|
315
|
+
if "Base Session" in line:
|
|
316
|
+
base_session_line = line
|
|
317
|
+
break
|
|
318
|
+
|
|
319
|
+
self.assertIsNotNone(base_session_line)
|
|
320
|
+
self.assertIn("✓", base_session_line)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
if __name__ == "__main__":
|
|
324
|
+
unittest.main()
|