diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 64b549c2..113941ae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -107,6 +107,8 @@ jobs: env: ENABLE_OPENAI: "true" OPENAI_API_KEY: "sk-dummy" + AWS_ACCESS_KEY_ID: "dummy" + AWS_SECRET_ACCESS_KEY: "dummy" run: poetry run pytest fe-lint-build: name: Frontend Lint and Build diff --git a/skyvern/forge/sdk/artifact/manager.py b/skyvern/forge/sdk/artifact/manager.py index bf163e4c..d7be4e18 100644 --- a/skyvern/forge/sdk/artifact/manager.py +++ b/skyvern/forge/sdk/artifact/manager.py @@ -100,7 +100,12 @@ class ArtifactManager: path: str | None = None, ) -> str: artifact_id = generate_artifact_id() - uri = app.STORAGE.build_log_uri(log_entity_type, log_entity_id, artifact_type) + uri = app.STORAGE.build_log_uri( + organization_id=organization_id, + log_entity_type=log_entity_type, + log_entity_id=log_entity_id, + artifact_type=artifact_type, + ) return await self._create_artifact( aio_task_primary_key=log_entity_id, artifact_id=artifact_id, @@ -123,7 +128,12 @@ class ArtifactManager: path: str | None = None, ) -> str: artifact_id = generate_artifact_id() - uri = app.STORAGE.build_thought_uri(artifact_id, thought, artifact_type) + uri = app.STORAGE.build_thought_uri( + organization_id=thought.organization_id, + artifact_id=artifact_id, + thought=thought, + artifact_type=artifact_type, + ) return await self._create_artifact( aio_task_primary_key=thought.observer_cruise_id, artifact_id=artifact_id, @@ -144,7 +154,12 @@ class ArtifactManager: path: str | None = None, ) -> str: artifact_id = generate_artifact_id() - uri = app.STORAGE.build_task_v2_uri(artifact_id, task_v2, artifact_type) + uri = app.STORAGE.build_task_v2_uri( + organization_id=task_v2.organization_id, + artifact_id=artifact_id, + task_v2=task_v2, + artifact_type=artifact_type, + ) return await self._create_artifact( aio_task_primary_key=task_v2.observer_cruise_id, artifact_id=artifact_id, @@ -164,7 +179,12 @@ class ArtifactManager: path: str | None = None, ) -> str: artifact_id = generate_artifact_id() - uri = app.STORAGE.build_workflow_run_block_uri(artifact_id, workflow_run_block, artifact_type) + uri = app.STORAGE.build_workflow_run_block_uri( + organization_id=workflow_run_block.organization_id, + artifact_id=artifact_id, + workflow_run_block=workflow_run_block, + artifact_type=artifact_type, + ) return await self._create_artifact( aio_task_primary_key=workflow_run_block.workflow_run_block_id, artifact_id=artifact_id, @@ -185,7 +205,12 @@ class ArtifactManager: path: str | None = None, ) -> str: artifact_id = generate_artifact_id() - uri = app.STORAGE.build_ai_suggestion_uri(artifact_id, ai_suggestion, artifact_type) + uri = app.STORAGE.build_ai_suggestion_uri( + organization_id=ai_suggestion.organization_id, + artifact_id=artifact_id, + ai_suggestion=ai_suggestion, + artifact_type=artifact_type, + ) return await self._create_artifact( aio_task_primary_key=ai_suggestion.ai_suggestion_id, artifact_id=artifact_id, diff --git a/skyvern/forge/sdk/artifact/storage/base.py b/skyvern/forge/sdk/artifact/storage/base.py index c6ffc2de..01052b9d 100644 --- a/skyvern/forge/sdk/artifact/storage/base.py +++ b/skyvern/forge/sdk/artifact/storage/base.py @@ -47,26 +47,37 @@ class BaseStorage(ABC): pass @abstractmethod - def build_log_uri(self, log_entity_type: LogEntityType, log_entity_id: str, artifact_type: ArtifactType) -> str: + def build_log_uri( + self, *, organization_id: str, log_entity_type: LogEntityType, log_entity_id: str, artifact_type: ArtifactType + ) -> str: pass @abstractmethod - def build_thought_uri(self, artifact_id: str, thought: Thought, artifact_type: ArtifactType) -> str: + def build_thought_uri( + self, *, organization_id: str, artifact_id: str, thought: Thought, artifact_type: ArtifactType + ) -> str: pass @abstractmethod - def build_task_v2_uri(self, artifact_id: str, task_v2: TaskV2, artifact_type: ArtifactType) -> str: + def build_task_v2_uri( + self, *, organization_id: str, artifact_id: str, task_v2: TaskV2, artifact_type: ArtifactType + ) -> str: pass @abstractmethod def build_workflow_run_block_uri( - self, artifact_id: str, workflow_run_block: WorkflowRunBlock, artifact_type: ArtifactType + self, + *, + organization_id: str, + artifact_id: str, + workflow_run_block: WorkflowRunBlock, + artifact_type: ArtifactType, ) -> str: pass @abstractmethod def build_ai_suggestion_uri( - self, artifact_id: str, ai_suggestion: AISuggestion, artifact_type: ArtifactType + self, *, organization_id: str, artifact_id: str, ai_suggestion: AISuggestion, artifact_type: ArtifactType ) -> str: pass diff --git a/skyvern/forge/sdk/artifact/storage/local.py b/skyvern/forge/sdk/artifact/storage/local.py index bb7c757a..cfd7f764 100644 --- a/skyvern/forge/sdk/artifact/storage/local.py +++ b/skyvern/forge/sdk/artifact/storage/local.py @@ -43,29 +43,40 @@ class LocalStorage(BaseStorage): except Exception: return [] - def build_log_uri(self, log_entity_type: LogEntityType, log_entity_id: str, artifact_type: ArtifactType) -> str: + def build_log_uri( + self, *, organization_id: str, log_entity_type: LogEntityType, log_entity_id: str, artifact_type: ArtifactType + ) -> str: file_ext = FILE_EXTENTSION_MAP[artifact_type] return f"file://{self.artifact_path}/logs/{log_entity_type}/{log_entity_id}/{datetime.utcnow().isoformat()}_{artifact_type}.{file_ext}" - def build_thought_uri(self, artifact_id: str, thought: Thought, artifact_type: ArtifactType) -> str: + def build_thought_uri( + self, *, organization_id: str, artifact_id: str, thought: Thought, artifact_type: ArtifactType + ) -> str: file_ext = FILE_EXTENTSION_MAP[artifact_type] - return f"file://{self.artifact_path}/{settings.ENV}/tasks/{thought.observer_cruise_id}/{thought.observer_thought_id}/{datetime.utcnow().isoformat()}_{artifact_id}_{artifact_type}.{file_ext}" + return f"file://{self.artifact_path}/{settings.ENV}/{organization_id}/tasks/{thought.observer_cruise_id}/{thought.observer_thought_id}/{datetime.utcnow().isoformat()}_{artifact_id}_{artifact_type}.{file_ext}" - def build_task_v2_uri(self, artifact_id: str, task_v2: TaskV2, artifact_type: ArtifactType) -> str: + def build_task_v2_uri( + self, *, organization_id: str, artifact_id: str, task_v2: TaskV2, artifact_type: ArtifactType + ) -> str: file_ext = FILE_EXTENTSION_MAP[artifact_type] - return f"file://{self.artifact_path}/{settings.ENV}/observers/{task_v2.observer_cruise_id}/{datetime.utcnow().isoformat()}_{artifact_id}_{artifact_type}.{file_ext}" + return f"file://{self.artifact_path}/{settings.ENV}/{organization_id}/observers/{task_v2.observer_cruise_id}/{datetime.utcnow().isoformat()}_{artifact_id}_{artifact_type}.{file_ext}" def build_workflow_run_block_uri( - self, artifact_id: str, workflow_run_block: WorkflowRunBlock, artifact_type: ArtifactType + self, + *, + organization_id: str, + artifact_id: str, + workflow_run_block: WorkflowRunBlock, + artifact_type: ArtifactType, ) -> str: file_ext = FILE_EXTENTSION_MAP[artifact_type] - return f"file://{self.artifact_path}/{settings.ENV}/workflow_runs/{workflow_run_block.workflow_run_id}/{workflow_run_block.workflow_run_block_id}/{datetime.utcnow().isoformat()}_{artifact_id}_{artifact_type}.{file_ext}" + return f"file://{self.artifact_path}/{settings.ENV}/{organization_id}/workflow_runs/{workflow_run_block.workflow_run_id}/{workflow_run_block.workflow_run_block_id}/{datetime.utcnow().isoformat()}_{artifact_id}_{artifact_type}.{file_ext}" def build_ai_suggestion_uri( - self, artifact_id: str, ai_suggestion: AISuggestion, artifact_type: ArtifactType + self, *, organization_id: str, artifact_id: str, ai_suggestion: AISuggestion, artifact_type: ArtifactType ) -> str: file_ext = FILE_EXTENTSION_MAP[artifact_type] - return f"file://{self.artifact_path}/{settings.ENV}/ai_suggestions/{ai_suggestion.ai_suggestion_id}/{datetime.utcnow().isoformat()}_{artifact_id}_{artifact_type}.{file_ext}" + return f"file://{self.artifact_path}/{settings.ENV}/{organization_id}/ai_suggestions/{ai_suggestion.ai_suggestion_id}/{datetime.utcnow().isoformat()}_{artifact_id}_{artifact_type}.{file_ext}" async def store_artifact(self, artifact: Artifact, data: bytes) -> None: file_path = None diff --git a/skyvern/forge/sdk/artifact/storage/s3.py b/skyvern/forge/sdk/artifact/storage/s3.py index 3a43d135..6b0ffa97 100644 --- a/skyvern/forge/sdk/artifact/storage/s3.py +++ b/skyvern/forge/sdk/artifact/storage/s3.py @@ -29,13 +29,15 @@ LOG = structlog.get_logger() class S3Storage(BaseStorage): + _PATH_VERSION = "v1" + def __init__(self, bucket: str | None = None) -> None: self.async_client = AsyncAWSClient() self.bucket = bucket or settings.AWS_S3_BUCKET_ARTIFACTS def build_uri(self, artifact_id: str, step: Step, artifact_type: ArtifactType) -> str: file_ext = FILE_EXTENTSION_MAP[artifact_type] - return f"s3://{self.bucket}/{settings.ENV}/{step.task_id}/{step.order:02d}_{step.retry_index}_{step.step_id}/{datetime.utcnow().isoformat()}_{artifact_id}_{artifact_type}.{file_ext}" + return f"s3://{self.bucket}/{self._PATH_VERSION}/{settings.ENV}/{step.task_id}/{step.order:02d}_{step.retry_index}_{step.step_id}/{datetime.utcnow().isoformat()}_{artifact_id}_{artifact_type}.{file_ext}" async def retrieve_global_workflows(self) -> list[str]: uri = f"s3://{self.bucket}/{settings.ENV}/global_workflows.txt" @@ -44,29 +46,40 @@ class S3Storage(BaseStorage): return [] return [line.strip() for line in data.decode("utf-8").split("\n") if line.strip()] - def build_log_uri(self, log_entity_type: LogEntityType, log_entity_id: str, artifact_type: ArtifactType) -> str: + def build_log_uri( + self, *, organization_id: str, log_entity_type: LogEntityType, log_entity_id: str, artifact_type: ArtifactType + ) -> str: file_ext = FILE_EXTENTSION_MAP[artifact_type] - return f"s3://{self.bucket}/{settings.ENV}/logs/{log_entity_type}/{log_entity_id}/{datetime.utcnow().isoformat()}_{artifact_type}.{file_ext}" + return f"s3://{self.bucket}/{self._PATH_VERSION}/{settings.ENV}/{organization_id}/logs/{log_entity_type}/{log_entity_id}/{datetime.utcnow().isoformat()}_{artifact_type}.{file_ext}" - def build_thought_uri(self, artifact_id: str, thought: Thought, artifact_type: ArtifactType) -> str: + def build_thought_uri( + self, *, organization_id: str, artifact_id: str, thought: Thought, artifact_type: ArtifactType + ) -> str: file_ext = FILE_EXTENTSION_MAP[artifact_type] - return f"s3://{self.bucket}/{settings.ENV}/observers/{thought.observer_cruise_id}/{thought.observer_thought_id}/{datetime.utcnow().isoformat()}_{artifact_id}_{artifact_type}.{file_ext}" + return f"s3://{self.bucket}/{self._PATH_VERSION}/{settings.ENV}/{organization_id}/observers/{thought.observer_cruise_id}/{thought.observer_thought_id}/{datetime.utcnow().isoformat()}_{artifact_id}_{artifact_type}.{file_ext}" - def build_task_v2_uri(self, artifact_id: str, task_v2: TaskV2, artifact_type: ArtifactType) -> str: + def build_task_v2_uri( + self, *, organization_id: str, artifact_id: str, task_v2: TaskV2, artifact_type: ArtifactType + ) -> str: file_ext = FILE_EXTENTSION_MAP[artifact_type] - return f"s3://{self.bucket}/{settings.ENV}/observers/{task_v2.observer_cruise_id}/{datetime.utcnow().isoformat()}_{artifact_id}_{artifact_type}.{file_ext}" + return f"s3://{self.bucket}/{self._PATH_VERSION}/{settings.ENV}/{organization_id}/observers/{task_v2.observer_cruise_id}/{datetime.utcnow().isoformat()}_{artifact_id}_{artifact_type}.{file_ext}" def build_workflow_run_block_uri( - self, artifact_id: str, workflow_run_block: WorkflowRunBlock, artifact_type: ArtifactType + self, + *, + organization_id: str, + artifact_id: str, + workflow_run_block: WorkflowRunBlock, + artifact_type: ArtifactType, ) -> str: file_ext = FILE_EXTENTSION_MAP[artifact_type] - return f"s3://{self.bucket}/{settings.ENV}/workflow_runs/{workflow_run_block.workflow_run_id}/{workflow_run_block.workflow_run_block_id}/{datetime.utcnow().isoformat()}_{artifact_id}_{artifact_type}.{file_ext}" + return f"s3://{self.bucket}/{self._PATH_VERSION}/{settings.ENV}/{organization_id}/workflow_runs/{workflow_run_block.workflow_run_id}/{workflow_run_block.workflow_run_block_id}/{datetime.utcnow().isoformat()}_{artifact_id}_{artifact_type}.{file_ext}" def build_ai_suggestion_uri( - self, artifact_id: str, ai_suggestion: AISuggestion, artifact_type: ArtifactType + self, *, organization_id: str, artifact_id: str, ai_suggestion: AISuggestion, artifact_type: ArtifactType ) -> str: file_ext = FILE_EXTENTSION_MAP[artifact_type] - return f"s3://{self.bucket}/{settings.ENV}/ai_suggestions/{ai_suggestion.ai_suggestion_id}/{datetime.utcnow().isoformat()}_{artifact_id}_{artifact_type}.{file_ext}" + return f"s3://{self.bucket}/{self._PATH_VERSION}/{settings.ENV}/{organization_id}/ai_suggestions/{ai_suggestion.ai_suggestion_id}/{datetime.utcnow().isoformat()}_{artifact_id}_{artifact_type}.{file_ext}" async def store_artifact(self, artifact: Artifact, data: bytes) -> None: sc = await self._get_storage_class_for_org(artifact.organization_id) diff --git a/skyvern/forge/sdk/artifact/storage/test_helpers.py b/skyvern/forge/sdk/artifact/storage/test_helpers.py new file mode 100644 index 00000000..a1c543b7 --- /dev/null +++ b/skyvern/forge/sdk/artifact/storage/test_helpers.py @@ -0,0 +1,65 @@ +from datetime import datetime + +from skyvern.forge.sdk.models import Step, StepStatus +from skyvern.forge.sdk.schemas.ai_suggestions import AISuggestion +from skyvern.forge.sdk.schemas.task_v2 import TaskV2, Thought +from skyvern.forge.sdk.schemas.workflow_runs import BlockType, WorkflowRunBlock + +# Constants +TEST_ORGANIZATION_ID = "test-org-123" +TEST_TASK_ID = "tsk_123456789" + + +def create_fake_for_ai_suggestion(ai_suggestion_id: str) -> AISuggestion: + return AISuggestion( + ai_suggestion_id=ai_suggestion_id, + organization_id=TEST_ORGANIZATION_ID, + ai_suggestion_type="test_suggestion_type", + created_at=datetime.utcnow(), + modified_at=datetime.utcnow(), + ) + + +def create_fake_thought(cruise_id: str, thought_id: str) -> Thought: + return Thought( + observer_cruise_id=cruise_id, + observer_thought_id=thought_id, + created_at=datetime.utcnow(), + modified_at=datetime.utcnow(), + organization_id=TEST_ORGANIZATION_ID, + ) + + +def create_fake_step(step_id: str) -> Step: + return Step( + task_id=TEST_TASK_ID, + order=1, + retry_index=0, + step_id=step_id, + created_at=datetime.utcnow(), + modified_at=datetime.utcnow(), + status=StepStatus.created, + is_last=False, + organization_id=TEST_ORGANIZATION_ID, + ) + + +def create_fake_task_v2(observer_cruise_id: str) -> TaskV2: + return TaskV2( + observer_cruise_id=observer_cruise_id, + created_at=datetime.utcnow(), + modified_at=datetime.utcnow(), + status=StepStatus.created, + organization_id=TEST_ORGANIZATION_ID, + ) + + +def create_fake_workflow_run_block(workflow_run_id: str, workflow_run_block_id: str) -> WorkflowRunBlock: + return WorkflowRunBlock( + workflow_run_id=workflow_run_id, + workflow_run_block_id=workflow_run_block_id, + created_at=datetime.utcnow(), + modified_at=datetime.utcnow(), + organization_id=TEST_ORGANIZATION_ID, + block_type=BlockType.TASK, + ) diff --git a/skyvern/forge/sdk/artifact/storage/test_local_storage.py b/skyvern/forge/sdk/artifact/storage/test_local_storage.py new file mode 100644 index 00000000..ef31301d --- /dev/null +++ b/skyvern/forge/sdk/artifact/storage/test_local_storage.py @@ -0,0 +1,102 @@ +import pytest +from freezegun import freeze_time + +from skyvern.config import settings +from skyvern.forge.sdk.artifact.models import ArtifactType, LogEntityType +from skyvern.forge.sdk.artifact.storage.local import LocalStorage +from skyvern.forge.sdk.artifact.storage.test_helpers import ( + create_fake_for_ai_suggestion, + create_fake_step, + create_fake_task_v2, + create_fake_thought, + create_fake_workflow_run_block, +) + +# Test constants +TEST_BUCKET = "test-skyvern-bucket" +TEST_ORGANIZATION_ID = "test-org-123" +TEST_TASK_ID = "tsk_123456789" +TEST_STEP_ID = "step_123456789" +TEST_WORKFLOW_RUN_ID = "wfr_123456789" +TEST_BLOCK_ID = "block_123456789" +TEST_AI_SUGGESTION_ID = "ai_sugg_test_123" + + +@pytest.fixture +def local_storage() -> LocalStorage: + return LocalStorage() + + +@freeze_time("2025-06-09T12:00:00") +class TestLocalStorageBuildURIs: + def test_build_uri(self, local_storage: LocalStorage) -> None: + step = create_fake_step(TEST_STEP_ID) + uri = local_storage.build_uri("artifact123", step, ArtifactType.LLM_PROMPT) + assert ( + uri + == f"file://{local_storage.artifact_path}/{TEST_TASK_ID}/01_0_{TEST_STEP_ID}/2025-06-09T12:00:00_artifact123_llm_prompt.txt" + ) + + def test_build_log_uri(self, local_storage: LocalStorage) -> None: + uri = local_storage.build_log_uri( + organization_id=TEST_ORGANIZATION_ID, + log_entity_type=LogEntityType.WORKFLOW_RUN_BLOCK, + log_entity_id="log_id", + artifact_type=ArtifactType.SKYVERN_LOG, + ) + assert ( + uri + == f"file://{local_storage.artifact_path}/logs/workflow_run_block/log_id/2025-06-09T12:00:00_skyvern_log.log" + ) + + def test_build_thought_uri(self, local_storage: LocalStorage) -> None: + thought = create_fake_thought("cruise123", "thought123") + uri = local_storage.build_thought_uri( + organization_id=TEST_ORGANIZATION_ID, + artifact_id="artifact123", + thought=thought, + artifact_type=ArtifactType.VISIBLE_ELEMENTS_TREE, + ) + assert ( + uri + == f"file://{local_storage.artifact_path}/{settings.ENV}/{TEST_ORGANIZATION_ID}/tasks/cruise123/thought123/2025-06-09T12:00:00_artifact123_visible_elements_tree.json" + ) + + def test_build_task_v2_uri(self, local_storage: LocalStorage) -> None: + task_v2 = create_fake_task_v2("cruise123") + uri = local_storage.build_task_v2_uri( + organization_id=TEST_ORGANIZATION_ID, + artifact_id="artifact123", + task_v2=task_v2, + artifact_type=ArtifactType.HTML_ACTION, + ) + assert ( + uri + == f"file://{local_storage.artifact_path}/{settings.ENV}/{TEST_ORGANIZATION_ID}/observers/cruise123/2025-06-09T12:00:00_artifact123_html_action.html" + ) + + def test_build_workflow_run_block_uri(self, local_storage: LocalStorage) -> None: + workflow_run_block = create_fake_workflow_run_block(TEST_WORKFLOW_RUN_ID, TEST_BLOCK_ID) + uri = local_storage.build_workflow_run_block_uri( + organization_id=TEST_ORGANIZATION_ID, + artifact_id="artifact123", + workflow_run_block=workflow_run_block, + artifact_type=ArtifactType.HAR, + ) + assert ( + uri + == f"file://{local_storage.artifact_path}/{settings.ENV}/{TEST_ORGANIZATION_ID}/workflow_runs/{TEST_WORKFLOW_RUN_ID}/{TEST_BLOCK_ID}/2025-06-09T12:00:00_artifact123_har.har" + ) + + def test_build_ai_suggestion_uri(self, local_storage: LocalStorage) -> None: + ai_suggestion = create_fake_for_ai_suggestion(TEST_AI_SUGGESTION_ID) + uri = local_storage.build_ai_suggestion_uri( + organization_id=TEST_ORGANIZATION_ID, + artifact_id="artifact123", + ai_suggestion=ai_suggestion, + artifact_type=ArtifactType.SCREENSHOT_LLM, + ) + assert ( + uri + == f"file://{local_storage.artifact_path}/{settings.ENV}/{TEST_ORGANIZATION_ID}/ai_suggestions/{TEST_AI_SUGGESTION_ID}/2025-06-09T12:00:00_artifact123_screenshot_llm.png" + ) diff --git a/skyvern/forge/sdk/artifact/storage/test_s3_storage.py b/skyvern/forge/sdk/artifact/storage/test_s3_storage.py new file mode 100644 index 00000000..cd7a80e8 --- /dev/null +++ b/skyvern/forge/sdk/artifact/storage/test_s3_storage.py @@ -0,0 +1,102 @@ +import pytest +from freezegun import freeze_time + +from skyvern.config import settings +from skyvern.forge.sdk.artifact.models import ArtifactType, LogEntityType +from skyvern.forge.sdk.artifact.storage.s3 import S3Storage +from skyvern.forge.sdk.artifact.storage.test_helpers import ( + create_fake_for_ai_suggestion, + create_fake_step, + create_fake_task_v2, + create_fake_thought, + create_fake_workflow_run_block, +) + +# Test constants +TEST_BUCKET = "test-skyvern-bucket" +TEST_ORGANIZATION_ID = "test-org-123" +TEST_TASK_ID = "tsk_123456789" +TEST_STEP_ID = "step_123456789" +TEST_WORKFLOW_RUN_ID = "wfr_123456789" +TEST_BLOCK_ID = "block_123456789" +TEST_AI_SUGGESTION_ID = "ai_sugg_test_123" + + +@pytest.fixture +def s3_storage() -> S3Storage: + return S3Storage(bucket=TEST_BUCKET) + + +@freeze_time("2025-06-09T12:00:00") +class TestS3StorageBuildURIs: + def test_build_uri(self, s3_storage: S3Storage) -> None: + step = create_fake_step(TEST_STEP_ID) + uri = s3_storage.build_uri("artifact123", step, ArtifactType.LLM_PROMPT) + assert ( + uri + == f"s3://{TEST_BUCKET}/v1/{settings.ENV}/{TEST_TASK_ID}/01_0_{TEST_STEP_ID}/2025-06-09T12:00:00_artifact123_llm_prompt.txt" + ) + + def test_build_log_uri(self, s3_storage: S3Storage) -> None: + uri = s3_storage.build_log_uri( + organization_id=TEST_ORGANIZATION_ID, + log_entity_type=LogEntityType.WORKFLOW_RUN_BLOCK, + log_entity_id="log_id", + artifact_type=ArtifactType.SKYVERN_LOG, + ) + assert ( + uri + == f"s3://{TEST_BUCKET}/v1/{settings.ENV}/{TEST_ORGANIZATION_ID}/logs/workflow_run_block/log_id/2025-06-09T12:00:00_skyvern_log.log" + ) + + def test_build_thought_uri(self, s3_storage: S3Storage) -> None: + thought = create_fake_thought("cruise123", "thought123") + uri = s3_storage.build_thought_uri( + organization_id=TEST_ORGANIZATION_ID, + artifact_id="artifact123", + thought=thought, + artifact_type=ArtifactType.VISIBLE_ELEMENTS_TREE, + ) + assert ( + uri + == f"s3://{TEST_BUCKET}/v1/{settings.ENV}/{TEST_ORGANIZATION_ID}/observers/cruise123/thought123/2025-06-09T12:00:00_artifact123_visible_elements_tree.json" + ) + + def test_build_task_v2_uri(self, s3_storage: S3Storage) -> None: + task_v2 = create_fake_task_v2("cruise123") + uri = s3_storage.build_task_v2_uri( + organization_id=TEST_ORGANIZATION_ID, + artifact_id="artifact123", + task_v2=task_v2, + artifact_type=ArtifactType.HTML_ACTION, + ) + assert ( + uri + == f"s3://{TEST_BUCKET}/v1/{settings.ENV}/{TEST_ORGANIZATION_ID}/observers/cruise123/2025-06-09T12:00:00_artifact123_html_action.html" + ) + + def test_build_workflow_run_block_uri(self, s3_storage: S3Storage) -> None: + workflow_run_block = create_fake_workflow_run_block(TEST_WORKFLOW_RUN_ID, TEST_BLOCK_ID) + uri = s3_storage.build_workflow_run_block_uri( + organization_id=TEST_ORGANIZATION_ID, + artifact_id="artifact123", + workflow_run_block=workflow_run_block, + artifact_type=ArtifactType.HAR, + ) + assert ( + uri + == f"s3://{TEST_BUCKET}/v1/{settings.ENV}/{TEST_ORGANIZATION_ID}/workflow_runs/{TEST_WORKFLOW_RUN_ID}/{TEST_BLOCK_ID}/2025-06-09T12:00:00_artifact123_har.har" + ) + + def test_build_ai_suggestion_uri(self, s3_storage: S3Storage) -> None: + ai_suggestion = create_fake_for_ai_suggestion(TEST_AI_SUGGESTION_ID) + uri = s3_storage.build_ai_suggestion_uri( + organization_id=TEST_ORGANIZATION_ID, + artifact_id="artifact123", + ai_suggestion=ai_suggestion, + artifact_type=ArtifactType.SCREENSHOT_LLM, + ) + assert ( + uri + == f"s3://{TEST_BUCKET}/v1/{settings.ENV}/{TEST_ORGANIZATION_ID}/ai_suggestions/{TEST_AI_SUGGESTION_ID}/2025-06-09T12:00:00_artifact123_screenshot_llm.png" + )