Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions recce/state/cloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,9 @@ def _load_state_from_session(self) -> RecceState:
current_artifacts = self._download_session_artifacts(self.recce_cloud, org_id, project_id, self.session_id)

logger.debug(f"Downloading base session artifacts for project {project_id}")
base_artifacts = self._download_base_session_artifacts(self.recce_cloud, org_id, project_id)
base_artifacts = self._download_base_session_artifacts(
self.recce_cloud, org_id, project_id, session_id=self.session_id
)

# 3. Try to download existing recce_state, otherwise create new state
try:
Expand Down Expand Up @@ -299,12 +301,17 @@ def _download_session_recce_state(self, recce_cloud, org_id: str, project_id: st

return state

def _download_base_session_artifacts(self, recce_cloud, org_id: str, project_id: str) -> dict:
"""Download manifest and catalog for the base session, return JSON data directly."""
def _download_base_session_artifacts(
self, recce_cloud, org_id: str, project_id: str, session_id: str = None
) -> dict:
"""Download manifest and catalog for the base session, return JSON data directly.

If session_id is provided, the server resolves PR-specific base if available.
"""
import requests

# Get download URLs for base session
presigned_urls = recce_cloud.get_base_session_download_urls(org_id, project_id)
presigned_urls = recce_cloud.get_base_session_download_urls(org_id, project_id, session_id=session_id)

artifacts = {}

Expand Down
9 changes: 7 additions & 2 deletions recce/util/recce_cloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -341,9 +341,14 @@ def get_download_urls_by_session_id(self, org_id: str, project_id: str, session_
presigned_urls[key] = self._replace_localhost_with_docker_internal(url)
return presigned_urls

def get_base_session_download_urls(self, org_id: str, project_id: str) -> dict[str, str]:
"""Get download URLs for the base session of a project."""
def get_base_session_download_urls(self, org_id: str, project_id: str, session_id: str = None) -> dict[str, str]:
"""Get download URLs for the base session of a project.

If session_id is provided, the server resolves PR-specific base if available.
"""
api_url = f"{self.base_url_v2}/organizations/{org_id}/projects/{project_id}/base-session/download-url"
if session_id:
api_url += f"?session_id={session_id}"
response = self._request("GET", api_url)
if response.status_code != 200:
raise RecceCloudException(
Expand Down
73 changes: 73 additions & 0 deletions tests/state/test_cloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,79 @@ def test_export_state_to_session_upload_failure(self, mock_put):
self.assertIn("Internal Server Error", result_message)
self.assertIsNone(result_etag)

@patch.object(CloudStateLoader, "_download_session_recce_state")
@patch.object(CloudStateLoader, "_download_base_session_artifacts")
@patch.object(CloudStateLoader, "_download_session_artifacts")
def test_load_state_from_session_passes_session_id_to_base_download(
self,
mock_download_session_artifacts,
mock_download_base_session_artifacts,
mock_download_session_state,
):
"""Verify _load_state_from_session passes session_id to _download_base_session_artifacts."""
loader = CloudStateLoader(cloud_options={"api_token": "token", "session_id": "test_session"})
loader.catalog = "session"
loader.session_id = "test_session"

loader.recce_cloud = Mock()
loader.recce_cloud.get_session.return_value = {
"org_id": "org1",
"project_id": "proj1",
}

mock_download_session_artifacts.return_value = {"manifest": "current_manifest", "catalog": "current_catalog"}
mock_download_base_session_artifacts.return_value = {"manifest": "base_manifest", "catalog": "base_catalog"}
mock_download_session_state.return_value = RecceState()

loader._load_state_from_session()

# Verify session_id was passed to _download_base_session_artifacts
mock_download_base_session_artifacts.assert_called_once_with(
loader.recce_cloud, "org1", "proj1", session_id="test_session"
)

@patch("requests.get")
def test_download_base_session_artifacts_passes_session_id(self, mock_get):
"""Verify _download_base_session_artifacts passes session_id to get_base_session_download_urls."""
loader = CloudStateLoader(cloud_options={"api_token": "token", "session_id": "test_session"})
loader.catalog = "session"

mock_cloud = Mock()
mock_cloud.get_base_session_download_urls.return_value = {
"manifest_url": "http://base_manifest.url",
"catalog_url": "http://base_catalog.url",
}

mock_response = Mock()
mock_response.status_code = 200
mock_response.json.side_effect = ["base_manifest_data", "base_catalog_data"]
mock_get.return_value = mock_response

loader._download_base_session_artifacts(mock_cloud, "org1", "proj1", session_id="test_session")

mock_cloud.get_base_session_download_urls.assert_called_once_with("org1", "proj1", session_id="test_session")

@patch("requests.get")
def test_download_base_session_artifacts_without_session_id(self, mock_get):
"""Verify _download_base_session_artifacts works without session_id (backward compat)."""
loader = CloudStateLoader(cloud_options={"api_token": "token", "session_id": "test_session"})
loader.catalog = "session"

mock_cloud = Mock()
mock_cloud.get_base_session_download_urls.return_value = {
"manifest_url": "http://base_manifest.url",
"catalog_url": "http://base_catalog.url",
}

mock_response = Mock()
mock_response.status_code = 200
mock_response.json.side_effect = ["base_manifest_data", "base_catalog_data"]
mock_get.return_value = mock_response

loader._download_base_session_artifacts(mock_cloud, "org1", "proj1")

mock_cloud.get_base_session_download_urls.assert_called_once_with("org1", "proj1", session_id=None)

@patch.object(CloudStateLoader, "_download_session_recce_state")
@patch.object(CloudStateLoader, "_download_base_session_artifacts")
@patch.object(CloudStateLoader, "_download_session_artifacts")
Expand Down
67 changes: 67 additions & 0 deletions tests/util/test_recce_cloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,5 +227,72 @@ def test_list_sessions_missing_key(self, mock_request):
self.assertEqual(result, [])


class TestRecceCloudBaseSessionDownloadUrls(unittest.TestCase):
"""Test cases for get_base_session_download_urls with optional session_id."""

def setUp(self):
self.token = "test-api-token"
self.cloud = RecceCloud(self.token)

@patch("recce.util.recce_cloud.RecceCloud._request")
def test_get_base_session_download_urls_without_session_id(self, mock_request):
"""Without session_id, URL has no query params."""
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {
"presigned_urls": {
"manifest_url": "http://manifest.url",
"catalog_url": "http://catalog.url",
}
}
mock_request.return_value = mock_response

result = self.cloud.get_base_session_download_urls("org1", "proj1")

expected_url = f"{self.cloud.base_url_v2}/organizations/org1/projects/proj1/base-session/download-url"
mock_request.assert_called_once_with("GET", expected_url)
self.assertIn("manifest_url", result)
self.assertIn("catalog_url", result)

@patch("recce.util.recce_cloud.RecceCloud._request")
def test_get_base_session_download_urls_with_session_id(self, mock_request):
"""With session_id, URL includes ?session_id= query param."""
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {
"presigned_urls": {
"manifest_url": "http://manifest.url",
"catalog_url": "http://catalog.url",
}
}
mock_request.return_value = mock_response

result = self.cloud.get_base_session_download_urls("org1", "proj1", session_id="abc-123")

expected_url = (
f"{self.cloud.base_url_v2}/organizations/org1/projects/proj1/base-session/download-url?session_id=abc-123"
)
mock_request.assert_called_once_with("GET", expected_url)
self.assertIn("manifest_url", result)

@patch("recce.util.recce_cloud.RecceCloud._request")
def test_get_base_session_download_urls_with_session_id_none(self, mock_request):
"""Explicit session_id=None behaves same as no session_id."""
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {
"presigned_urls": {
"manifest_url": "http://manifest.url",
"catalog_url": "http://catalog.url",
}
}
mock_request.return_value = mock_response

self.cloud.get_base_session_download_urls("org1", "proj1", session_id=None)

expected_url = f"{self.cloud.base_url_v2}/organizations/org1/projects/proj1/base-session/download-url"
mock_request.assert_called_once_with("GET", expected_url)


if __name__ == "__main__":
unittest.main()
Loading