recce-nightly 1.20.0.20250921__py3-none-any.whl → 1.20.0.20250922__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/yaml/__init__.py CHANGED
@@ -34,7 +34,7 @@ def dump(data, stream: Any = None, *, transform: Any = None) -> Any:
34
34
 
35
35
  def safe_load_yaml(file_path):
36
36
  try:
37
- with open(file_path, "r") as f:
37
+ with open(file_path, "r", encoding="utf-8") as f:
38
38
  payload = safe_load(f)
39
39
  except yaml.YAMLError as e:
40
40
  print(e)
@@ -45,7 +45,7 @@ def safe_load_yaml(file_path):
45
45
 
46
46
 
47
47
  def round_trip_load_yaml(file_path):
48
- with open(file_path, "r") as f:
48
+ with open(file_path, "r", encoding="utf-8") as f:
49
49
  try:
50
50
  payload = load(f)
51
51
  except yaml.YAMLError as e:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: recce-nightly
3
- Version: 1.20.0.20250921
3
+ Version: 1.20.0.20250922
4
4
  Summary: Environment diff tool for dbt
5
5
  Home-page: https://github.com/InfuseAI/recce
6
6
  Author: InfuseAI Dev Team
@@ -1,35 +1,35 @@
1
- recce/VERSION,sha256=927IbuVVxqKUqOGffZnfB1fLn7HHSPlGOrpZ2KnDc9w,16
1
+ recce/VERSION,sha256=wXGuVXhTu2oySz9JJn3AKSWPgQeSVlf0cUIBQMPwSNQ,16
2
2
  recce/__init__.py,sha256=yNb0QT-yoStex0VZALNJvUwtPLommoVCStcow31guqo,2392
3
- recce/artifact.py,sha256=pwI377OgHIbb8HI9k-2h1K7byO2_VXfO63JXs1Ws-24,9229
4
- recce/cli.py,sha256=Ro3vvno_korHXZwT_putOJJSNLtuvf7fJWfhCbHAfe8,46109
5
- recce/config.py,sha256=A4CbKcIdwQ4A9Q2ba4riHUshVQw1D-qDelMOdlt2khU,4661
6
- recce/connect_to_cloud.py,sha256=_SkX2pdyXa9FNyaHvItyYVPF2nZxy2JnCyd_o_psh2Y,4750
3
+ recce/artifact.py,sha256=BdNW5eQQqEKu5cRzA_XusKMIN73V7-MTQP0VxnLdd9E,9265
4
+ recce/cli.py,sha256=ws50nwIzWvYcLNPDfcI6N69k5DaaIONKxloqa-5UdE8,52989
5
+ recce/config.py,sha256=noWimlOdGHAtBhNoApK5gRTQMwNPmUGAmZu39MV7szo,4697
6
+ recce/connect_to_cloud.py,sha256=CT7ELcVCTNa9ukSp_gMrJ0Av2UCUpF9H03tIk9y7U-4,4768
7
7
  recce/core.py,sha256=MtBWxemvCQDdUITwkU2JyuQYqcjlA0a76M8Kg53ECxQ,10754
8
8
  recce/diff.py,sha256=L2_bzQ3__PO-0aeir8PHF8FvSOUmQ8WcDXgML1-mHdY,748
9
9
  recce/exceptions.py,sha256=SclQ678GrHGjw7p_ZFJ3vZaL_yMU5xABeIAm2u_W2bk,592
10
10
  recce/git.py,sha256=8Eg-6NzL-KjA3rT-ibbAyaCwGlzV0JqH3yGikrJNMDA,2344
11
- recce/github.py,sha256=PEpM6ZRiinsPbXSWj4aJCKbZrN1jUXzpzAfJq_CGah4,7420
12
- recce/pull_request.py,sha256=aW0B1NE2LUKTam1S4TQ7smXB9KLE1DV8GnyBqNXA6j8,3832
13
- recce/run.py,sha256=PNafwUUJdIG8b1o0y1QuvjACpA3E-5a3keH45CrWHN0,14044
14
- recce/server.py,sha256=2xy9AkHJx2c_PNtgmg4QwbbbVLfBP68ZGsskEzHtbRQ,22730
11
+ recce/github.py,sha256=vIwHTpB8YWEUcHQlW0nToUUKmGGO0NcmB_Q5RvxQLLE,7438
12
+ recce/pull_request.py,sha256=dLHsdiMtL4NC6Vkwp8Mq4baJmnqbm19lfiDti9MREN0,3850
13
+ recce/run.py,sha256=ctp-K2FmrsGrS4xEZKDMbMtitNw2QKJ6Tq_0OvCWaaM,14080
14
+ recce/server.py,sha256=Q9Uc_YV8pgE0txZB9eVB9N1CD2HWpj2sZZxnhtQu_aQ,22748
15
15
  recce/summary.py,sha256=Mbxvxr9KazR5o9icqhhjiGHsoAiWxQU4PdN7HytBJ1c,19154
16
16
  recce/adapter/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
17
  recce/adapter/base.py,sha256=T_JNeLHgiHSaegw-DbrvHOaYjMyZcjj2Qtg5cWh_fco,3548
18
18
  recce/adapter/sqlmesh_adapter.py,sha256=IU3N-F6ToDoO7_bV5vsG8pmTuDcbFtewTIuCxedTaRM,5046
19
- recce/adapter/dbt_adapter/__init__.py,sha256=C5xRPDs3q86CMgewa0HTKayJc3d5KkQ1cqYN8s0nKno,66822
19
+ recce/adapter/dbt_adapter/__init__.py,sha256=o7OowlYfuE1jBaHxrJ5PyiEBmK4OpdpscwxvtpMIG_M,66840
20
20
  recce/adapter/dbt_adapter/dbt_version.py,sha256=M7aedZIWslXnJsryK8Ki4OL_t2oAKxy4uE2pRwfWIkk,1228
21
21
  recce/apis/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
22
  recce/apis/check_api.py,sha256=KMCXSMl1qqzx2jQgRqCrD4j_cY3EHBbM3H2-t-6saAU,6227
23
23
  recce/apis/check_func.py,sha256=gktbCcyk3WGvWRJJ-wDnwv7NrIny2nTHWLl1-kdiVRo,4183
24
24
  recce/apis/run_api.py,sha256=eOaxOxXDkH59uqGCd4blld7edavUx7JU_DCd2WAYrL8,3416
25
25
  recce/apis/run_func.py,sha256=6wC8TDU-h7TLr2VZH7HNsWaUVlQ9HBN5N_dwqfi4lMY,7440
26
- recce/data/404.html,sha256=AkLP52yWPUFEj7Bjv8H1-ohLuofIUB7mXlrnACMTgr4,59087
26
+ recce/data/404.html,sha256=dCcXldhUn0qHfrsGUz0OkfkX0DeunuDNj5XejmbLtzY,59087
27
27
  recce/data/auth_callback.html,sha256=H-XfdlAFiw5VU2RpKBVQbwh1AIqJrPHrFA0S09nNJZA,94779
28
28
  recce/data/favicon.ico,sha256=B2mBumUOnzvUrXrqNkrc5QfdDXjzEXRcWkWur0fJ6sM,2565
29
- recce/data/index.html,sha256=njKsbzX3bAMWX-vSgKzglqI8WYtwh8_Onbkv4_CXHWM,77450
30
- recce/data/index.txt,sha256=TkkxXfVjA-exikwv1Ly33RO_PDGQNJ8jOsYfb2AzuT4,6409
31
- recce/data/_next/static/7VDijMrfP7uONDsXVHgCK/_buildManifest.js,sha256=avQsL1oPzYwcdZvar5cABpd9QZyaNp5U46Y9ET0fq24,544
32
- recce/data/_next/static/7VDijMrfP7uONDsXVHgCK/_ssgManifest.js,sha256=Z49s4suAsf5y_GfnQSvm4qtq2ggxEbZPfEDTXjy6XgA,80
29
+ recce/data/index.html,sha256=Zc3ClQPBBSjN1PSQFptMz6r-628eO-RUuEBvCOwerZo,77450
30
+ recce/data/index.txt,sha256=HaAqSxEF7jpkRqQT9X9FBQjx18N2HqppVzzyxDzHzws,6409
31
+ recce/data/_next/static/15wXG4tmVWP4KygQN515-/_buildManifest.js,sha256=avQsL1oPzYwcdZvar5cABpd9QZyaNp5U46Y9ET0fq24,544
32
+ recce/data/_next/static/15wXG4tmVWP4KygQN515-/_ssgManifest.js,sha256=Z49s4suAsf5y_GfnQSvm4qtq2ggxEbZPfEDTXjy6XgA,80
33
33
  recce/data/_next/static/chunks/068b80ea-5225768a06b94b6f.js,sha256=9-CgOMA67KPg0s3LBhBMLS7nl5icw5BMDhs8Zk-TjO4,4628
34
34
  recce/data/_next/static/chunks/0ddaf06c-47655700b71b89f7.js,sha256=ZAiDuPV8ad-3MSwYva4LCOLRFOyaSENn2vBB6ZSU3So,696
35
35
  recce/data/_next/static/chunks/12f8fac4-10d6e05f7330d12b.js,sha256=79b2y12TyKWM49nc8dsZRwojyWcC7Yz-P7-f3wBfy9s,152792
@@ -87,7 +87,7 @@ recce/data/imgs/feedback/thumbs-up.png,sha256=VF3BH8bmYEqcSsMDJO57xMqW4t6crCXUXa
87
87
  recce/data/logo/recce-logo-white.png,sha256=y3re8iEucJnMUkAkRS2CjWHTlOydyvgWdWjuQKcXDbk,46923
88
88
  recce/event/CONFIG,sha256=w8_AVcNu_JF-t8lNmjOqtsXbeOawMzpEhkISaMlm-iU,48
89
89
  recce/event/SENTRY_DNS,sha256=nWXZevLC4qDScvNtjs329X13oxqvtFI_Kiu6cfCDOBA,83
90
- recce/event/__init__.py,sha256=q82uD7GvAvp9nknyrOC2E-OtGsk0x-aM_rWEqzpkEgY,8613
90
+ recce/event/__init__.py,sha256=gf8c9cZaAW6nx0hmvQWeSYTmM1uICHn25qW81t9eNTY,8721
91
91
  recce/event/collector.py,sha256=Tqe5nMAMvDveaJ1_AfGaq23y5ccoRqhIC1-fTdVZAVg,5720
92
92
  recce/event/track.py,sha256=3wBzpB8l2gtnDzjh-wJatmLeBbtnP0q-BLQZQVQ23sM,5310
93
93
  recce/models/__init__.py,sha256=F7cgALtdWnwv37R0eEgKZ_yBsMwxWnUfo3jAZ3u6qyU,209
@@ -116,18 +116,19 @@ recce/util/api_token.py,sha256=NE8lIFcr2GZ6ft4FS3zcsbvZaFdBF-e-fMYAYL2PDkM,2938
116
116
  recce/util/breaking.py,sha256=ajCOWMR8Qw492wCiuz5_DKqpI3uoj_qHuJjvelEoSkw,13068
117
117
  recce/util/cache.py,sha256=QB6wzxe0M3jNTwP0M27Ys8F2hF-oda4-LyXXG9THuZQ,646
118
118
  recce/util/cll.py,sha256=p8BrsG_1Lm-jXO8n4ulUgFw13C4mN0WjCXZbihDxQrs,13253
119
- recce/util/io.py,sha256=fNiIafNxPlTF6NNXlugzeJjfVGGrP3RXb39ERzRWD3Q,3459
119
+ recce/util/io.py,sha256=UTUDncG4habAZZSZ1Y7i7Zpzu6FBtsfWI3y96k-0zkc,3495
120
120
  recce/util/lineage.py,sha256=0XXOO05eJ3GPUNX9V9GBQS2iRFXxWZJoJbV7w-jQ0t8,2470
121
121
  recce/util/logger.py,sha256=6UgLFkRiur9jJfu2ZRdo4LUvMw4f75V-l-1HT1-sgKo,747
122
122
  recce/util/onboarding_state.py,sha256=kwFirKlfXdl5WFkR_nmilqGKFyELNcSPMqYq-by35fk,1991
123
123
  recce/util/perf_tracking.py,sha256=FjhrdbbXIgybxS_oPpFsJ9VUDR93d7bUs8VNNqpXNxw,2483
124
124
  recce/util/pydantic_model.py,sha256=KumKuyCjbTzEMsKLE4-b-eZfp0gLhYDdmVtw1-hxiJw,587
125
- recce/util/recce_cloud.py,sha256=45-HmG3yW51VLssmwQQRZjoDDM9LwEq2UHTMMykX16Y,14961
125
+ recce/util/recce_cloud.py,sha256=kr0PnnSltLP6Y5RLwsqTMwDBC2u5fdnhYbVQgX9i86A,16622
126
126
  recce/util/singleton.py,sha256=1cU99I0f9tjuMQLMJyLsK1oK3fZJMsO5-TbRHAMXqds,627
127
- recce/yaml/__init__.py,sha256=EgXYlFeJZchatUClRDXbIC5Oqb2_nBvB2NqItYVihio,1292
128
- recce_nightly-1.20.0.20250921.dist-info/licenses/LICENSE,sha256=CQjjMy9aYPhfe8xG_bcpIfKtNkdxLZ5IOb8oPygtUhY,11343
127
+ recce/yaml/__init__.py,sha256=PAym5akbtL24Ag7VR7EW8SS2VINNaJU06esbfe-ek-U,1328
128
+ recce_nightly-1.20.0.20250922.dist-info/licenses/LICENSE,sha256=CQjjMy9aYPhfe8xG_bcpIfKtNkdxLZ5IOb8oPygtUhY,11343
129
129
  tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
130
130
  tests/test_cli.py,sha256=CFtP4eJrtcHwpzoEgJAyveQcqxAmDQuWXPrLc_Iny7Q,6626
131
+ tests/test_cloud_listing_cli.py,sha256=77As9n0Sngdh53dfHkShmqDETMTOohlqw1xXO9cIago,14208
131
132
  tests/test_config.py,sha256=ODDFe_XF6gphmSmmc422dGLBaCCmG-IjDzTkD5SJsJE,1557
132
133
  tests/test_connect_to_cloud.py,sha256=b2fgV8L1iQBdEwh6RumMsIIyYg7GtMOMRz1dvE3WRPg,3059
133
134
  tests/test_core.py,sha256=WgCFm8Au3YQI-V5UbZ6LA8irMNHRc7NWutIq2Mny0LE,6242
@@ -153,8 +154,8 @@ tests/tasks/test_row_count.py,sha256=21PaP2aq-x8-pqwzWHRT1sixhQ8g3CQNRWOZTTmbK0s
153
154
  tests/tasks/test_schema.py,sha256=7ds4Vx8ixaiIWDR49Lvjem4xlPkRP1cXazDRY3roUak,3121
154
155
  tests/tasks/test_top_k.py,sha256=YR_GS__DJsbDlQVaEEdJvNQ3fh1VmV5Nb3G7lb0r6YM,1779
155
156
  tests/tasks/test_valuediff.py,sha256=_xQJGgxsXoy2NYk_Z6Hsw2FlVh6zk2nN_iUueyRN1e8,2046
156
- recce_nightly-1.20.0.20250921.dist-info/METADATA,sha256=C9jUp7DvpC5WQU9voaMdyd0uwduEeSjN9hMQfxGeDHU,9366
157
- recce_nightly-1.20.0.20250921.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
158
- recce_nightly-1.20.0.20250921.dist-info/entry_points.txt,sha256=oqoY_IiwIqXbgrIsPnlqUqao2eiIeP2dprowkOlmeyg,40
159
- recce_nightly-1.20.0.20250921.dist-info/top_level.txt,sha256=6PKGVpf75idP0C6KEaldDzzZUauIxNu1ZDstau1pI4I,12
160
- recce_nightly-1.20.0.20250921.dist-info/RECORD,,
157
+ recce_nightly-1.20.0.20250922.dist-info/METADATA,sha256=42dxxn8jsX9k-bBfh8Ww6OaS1DU3AxMlsRgqU2zOAAE,9366
158
+ recce_nightly-1.20.0.20250922.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
159
+ recce_nightly-1.20.0.20250922.dist-info/entry_points.txt,sha256=oqoY_IiwIqXbgrIsPnlqUqao2eiIeP2dprowkOlmeyg,40
160
+ recce_nightly-1.20.0.20250922.dist-info/top_level.txt,sha256=6PKGVpf75idP0C6KEaldDzzZUauIxNu1ZDstau1pI4I,12
161
+ recce_nightly-1.20.0.20250922.dist-info/RECORD,,
@@ -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()