From 70e77cffeeb54d2d020bcbae5bdbd267f3103112 Mon Sep 17 00:00:00 2001 From: Arkadiusz Kwasigroch Date: Thu, 27 Aug 2026 10:53:03 +0200 Subject: [PATCH 1/2] docs(backend): drop the crud split history The crud package is one module per entity now, so the "part of the incremental split, crud/__init__.py still holds the monolith" paragraph in every module docstring describes a state that no longer exists. Same for the AGENTS.md rule, which now states the finished layout instead of a migration in progress. --- apps/backend/AGENTS.md | 6 +++--- apps/backend/src/rhesis/backend/app/crud/architect.py | 4 ---- apps/backend/src/rhesis/backend/app/crud/category.py | 7 +------ apps/backend/src/rhesis/backend/app/crud/comment.py | 4 ---- apps/backend/src/rhesis/backend/app/crud/embedding.py | 4 ---- apps/backend/src/rhesis/backend/app/crud/endpoint.py | 7 +------ apps/backend/src/rhesis/backend/app/crud/experiment.py | 7 +------ apps/backend/src/rhesis/backend/app/crud/file.py | 4 ---- apps/backend/src/rhesis/backend/app/crud/metric.py | 2 +- apps/backend/src/rhesis/backend/app/crud/metric_tuning.py | 4 ---- apps/backend/src/rhesis/backend/app/crud/model.py | 4 ---- apps/backend/src/rhesis/backend/app/crud/organization.py | 4 ---- apps/backend/src/rhesis/backend/app/crud/project.py | 4 ---- apps/backend/src/rhesis/backend/app/crud/prompt.py | 4 ---- apps/backend/src/rhesis/backend/app/crud/requirement.py | 4 ---- apps/backend/src/rhesis/backend/app/crud/source.py | 4 ---- apps/backend/src/rhesis/backend/app/crud/status.py | 7 +------ apps/backend/src/rhesis/backend/app/crud/tag.py | 4 ---- apps/backend/src/rhesis/backend/app/crud/task.py | 4 ---- apps/backend/src/rhesis/backend/app/crud/telemetry.py | 2 +- apps/backend/src/rhesis/backend/app/crud/test.py | 7 +------ apps/backend/src/rhesis/backend/app/crud/test_result.py | 4 ---- apps/backend/src/rhesis/backend/app/crud/test_run.py | 4 ---- apps/backend/src/rhesis/backend/app/crud/token.py | 4 ---- apps/backend/src/rhesis/backend/app/crud/tool.py | 7 +------ apps/backend/src/rhesis/backend/app/crud/topic.py | 7 +------ apps/backend/src/rhesis/backend/app/crud/type_lookup.py | 7 +------ apps/backend/src/rhesis/backend/app/crud/user.py | 4 ---- .../src/rhesis/backend/app/utils/database_exceptions.py | 6 +++--- tests/backend/crud/test_transaction_management.py | 2 +- tests/backend/services/test_endpoint_service.py | 2 +- 31 files changed, 18 insertions(+), 126 deletions(-) diff --git a/apps/backend/AGENTS.md b/apps/backend/AGENTS.md index 88ebb0888c..9c5f891843 100644 --- a/apps/backend/AGENTS.md +++ b/apps/backend/AGENTS.md @@ -29,9 +29,9 @@ as the files around them are touched. ## CRUD layout -`app/crud/` is a package mid-split. `crud/__init__.py` still holds the monolith; per-entity modules -(`crud/explorer.py`, …) take over as the code around them is touched. **Anything that would add to -`crud/__init__.py` goes into a per-entity module instead** — it only shrinks from here. +`app/crud/` is one module per entity — `crud/test.py`, `crud/test_set.py`, `crud/explorer.py`, and so on. +`crud/__init__.py` is empty on purpose; **a new CRUD function goes in its entity's module**, and a +new entity gets a new module. Layering is routers → services → crud, and the same "split, don't grow" rule runs down it: touching a router means its business logic moves into a service; touching a service means its SQL moves into diff --git a/apps/backend/src/rhesis/backend/app/crud/architect.py b/apps/backend/src/rhesis/backend/app/crud/architect.py index 277b1e980f..53890ac1c3 100644 --- a/apps/backend/src/rhesis/backend/app/crud/architect.py +++ b/apps/backend/src/rhesis/backend/app/crud/architect.py @@ -1,9 +1,5 @@ """CRUD operations for architect sessions and their messages. -Part of the incremental split of the ``crud`` monolith: ``crud/__init__.py`` still holds -the bulk of the functions, and per-entity modules like this one take over as the code -around them is touched. - Messages share this module because an ``ArchitectMessage`` only exists as part of a session -- there is no standalone message endpoint. ``get_architect_messages`` builds a raw ``db.query`` chain instead of going through ``QueryBuilder``, so it has to filter diff --git a/apps/backend/src/rhesis/backend/app/crud/category.py b/apps/backend/src/rhesis/backend/app/crud/category.py index 0bea302d87..3dbd19ecc6 100644 --- a/apps/backend/src/rhesis/backend/app/crud/category.py +++ b/apps/backend/src/rhesis/backend/app/crud/category.py @@ -1,9 +1,4 @@ -"""CRUD operations for categories. - -Part of the incremental split of the ``crud`` monolith: ``crud/__init__.py`` still holds -the bulk of the functions, and per-entity modules like this one take over as the code -around them is touched -- see ``apps/backend/AGENTS.md``'s crud-layout rule. -""" +"""CRUD operations for categories.""" import uuid from typing import List, Optional diff --git a/apps/backend/src/rhesis/backend/app/crud/comment.py b/apps/backend/src/rhesis/backend/app/crud/comment.py index b2375bc839..e46f5c3822 100644 --- a/apps/backend/src/rhesis/backend/app/crud/comment.py +++ b/apps/backend/src/rhesis/backend/app/crud/comment.py @@ -1,9 +1,5 @@ """CRUD operations for comments and their emoji reactions. -Part of the incremental split of the ``crud`` monolith: ``crud/__init__.py`` still holds -the bulk of the functions, and per-entity modules like this one take over as the code -around them is touched. - Two behaviours here are worth knowing about. ``delete_comment`` first strips the ``comment_id`` key out of ``Task.task_metadata`` for every task that points at the comment, so deleting a comment does not leave orphaned references behind; that cleanup is committed diff --git a/apps/backend/src/rhesis/backend/app/crud/embedding.py b/apps/backend/src/rhesis/backend/app/crud/embedding.py index 9762831564..e1d5e8d3c6 100644 --- a/apps/backend/src/rhesis/backend/app/crud/embedding.py +++ b/apps/backend/src/rhesis/backend/app/crud/embedding.py @@ -1,9 +1,5 @@ """CRUD operations for embeddings. -Part of the incremental split of the ``crud`` monolith: ``crud/__init__.py`` still holds -the bulk of the functions, and per-entity modules like this one take over as the code -around them is touched. - An ``Embedding`` row stores its vector in a dimension-specific column (``embedding_768``, ``embedding_1536``, ...) because pgvector needs a fixed width per column. The ``ck_embedding_exactly_one_embedding`` constraint requires exactly one of them diff --git a/apps/backend/src/rhesis/backend/app/crud/endpoint.py b/apps/backend/src/rhesis/backend/app/crud/endpoint.py index 1d40f36cdf..893ba51801 100644 --- a/apps/backend/src/rhesis/backend/app/crud/endpoint.py +++ b/apps/backend/src/rhesis/backend/app/crud/endpoint.py @@ -1,9 +1,4 @@ -"""CRUD operations for endpoints. - -Part of the incremental split of the ``crud`` monolith: ``crud/__init__.py`` still holds -the bulk of the functions, and per-entity modules like this one take over as the code -around them is touched -- see ``apps/backend/AGENTS.md``'s crud-layout rule. -""" +"""CRUD operations for endpoints.""" import uuid from typing import Dict, List, Optional diff --git a/apps/backend/src/rhesis/backend/app/crud/experiment.py b/apps/backend/src/rhesis/backend/app/crud/experiment.py index 22ac10e73f..571cfde8a7 100644 --- a/apps/backend/src/rhesis/backend/app/crud/experiment.py +++ b/apps/backend/src/rhesis/backend/app/crud/experiment.py @@ -1,9 +1,4 @@ -"""CRUD operations for experiments. - -Part of the incremental split of the ``crud`` monolith: ``crud/__init__.py`` still holds -the bulk of the functions, and per-entity modules like this one take over as the code -around them is touched -- see ``apps/backend/AGENTS.md``'s crud-layout rule. -""" +"""CRUD operations for experiments.""" from typing import List diff --git a/apps/backend/src/rhesis/backend/app/crud/file.py b/apps/backend/src/rhesis/backend/app/crud/file.py index b17e77f114..debae164da 100644 --- a/apps/backend/src/rhesis/backend/app/crud/file.py +++ b/apps/backend/src/rhesis/backend/app/crud/file.py @@ -1,9 +1,5 @@ """CRUD operations for file attachments. -Part of the incremental split of the ``crud`` monolith: ``crud/__init__.py`` still holds -the bulk of the functions, and per-entity modules like this one take over as the code -around them is touched. - A ``File`` is always an attachment to something else — it carries a generic ``(entity_id, entity_type)`` pair rather than a real foreign key, so every read here filters on both. ``entity_type`` is stored as a plain string; ``create_file`` unwraps a diff --git a/apps/backend/src/rhesis/backend/app/crud/metric.py b/apps/backend/src/rhesis/backend/app/crud/metric.py index 40ff15bc07..2249928b8c 100644 --- a/apps/backend/src/rhesis/backend/app/crud/metric.py +++ b/apps/backend/src/rhesis/backend/app/crud/metric.py @@ -1,6 +1,6 @@ """CRUD operations for metrics and their requirement/test-set associations. -Split out of ``crud/__init__.py``. Import the functions directly:: +Import the functions directly:: from rhesis.backend.app.crud.metric import get_metrics """ diff --git a/apps/backend/src/rhesis/backend/app/crud/metric_tuning.py b/apps/backend/src/rhesis/backend/app/crud/metric_tuning.py index 99c930da24..89ad2652d8 100644 --- a/apps/backend/src/rhesis/backend/app/crud/metric_tuning.py +++ b/apps/backend/src/rhesis/backend/app/crud/metric_tuning.py @@ -1,9 +1,5 @@ """CRUD operations for metric tuning. -Part of the incremental split of the ``crud`` monolith: per-entity modules like -this one take over as the code around them is touched, and nothing new is added -to ``crud/__init__.py``. - Every function here flushes and never commits -- the request session owns the commit (see ``get_db_with_tenant_variables`` in ``database.py``). diff --git a/apps/backend/src/rhesis/backend/app/crud/model.py b/apps/backend/src/rhesis/backend/app/crud/model.py index 8f5a84fe8f..bc7df11e37 100644 --- a/apps/backend/src/rhesis/backend/app/crud/model.py +++ b/apps/backend/src/rhesis/backend/app/crud/model.py @@ -1,9 +1,5 @@ """CRUD operations for models -- the LLM/embedding provider configurations users register. -Part of the incremental split of the ``crud`` monolith: ``crud/__init__.py`` still holds -the bulk of the functions, and per-entity modules like this one take over as the code -around them is touched. - ``update_model`` and ``delete_model`` enforce the ``is_protected`` rules for system models, the pre-seeded rows an organization gets on onboarding. A protected model rejects any change to its core configuration -- name, model_name, provider_type_id, key, endpoint, diff --git a/apps/backend/src/rhesis/backend/app/crud/organization.py b/apps/backend/src/rhesis/backend/app/crud/organization.py index 627e2eae7a..43bbc97693 100644 --- a/apps/backend/src/rhesis/backend/app/crud/organization.py +++ b/apps/backend/src/rhesis/backend/app/crud/organization.py @@ -1,9 +1,5 @@ """CRUD operations for organizations. -Part of the incremental split of the ``crud`` monolith: ``crud/__init__.py`` still holds -the bulk of the functions, and per-entity modules like this one take over as the code -around them is touched. - ``create_organization`` is the one function here that steps outside the normal tenant machinery. Every other entity is created *inside* a tenant, so the org/user GUCs are already set and RLS applies; an organization *is* the tenant, so there is nothing to scope diff --git a/apps/backend/src/rhesis/backend/app/crud/project.py b/apps/backend/src/rhesis/backend/app/crud/project.py index a8023e3619..4a3b05e96e 100644 --- a/apps/backend/src/rhesis/backend/app/crud/project.py +++ b/apps/backend/src/rhesis/backend/app/crud/project.py @@ -1,9 +1,5 @@ """CRUD operations for projects and project membership. -Part of the incremental split of the ``crud`` monolith: ``crud/__init__.py`` still holds -the bulk of the functions, and per-entity modules like this one take over as the code -around them is touched. - Project reads enforce membership, not just organization scope -- ``get_project`` and ``get_projects`` return only projects the caller has a ``project_membership`` row for. Writes to membership route through ``services.organization`` so that a user's diff --git a/apps/backend/src/rhesis/backend/app/crud/prompt.py b/apps/backend/src/rhesis/backend/app/crud/prompt.py index fc49550d7a..950043c447 100644 --- a/apps/backend/src/rhesis/backend/app/crud/prompt.py +++ b/apps/backend/src/rhesis/backend/app/crud/prompt.py @@ -1,9 +1,5 @@ """CRUD operations for prompts and prompt templates. -Part of the incremental split of the ``crud`` monolith: ``crud/__init__.py`` still holds -the bulk of the functions, and per-entity modules like this one take over as the code -around them is touched. - Prompt templates live here too rather than in their own module: a template is a prompt with placeholders left in, so it is the same domain. """ diff --git a/apps/backend/src/rhesis/backend/app/crud/requirement.py b/apps/backend/src/rhesis/backend/app/crud/requirement.py index f8595c4f5f..31626378ad 100644 --- a/apps/backend/src/rhesis/backend/app/crud/requirement.py +++ b/apps/backend/src/rhesis/backend/app/crud/requirement.py @@ -1,9 +1,5 @@ """CRUD operations for requirements. -Part of the incremental split of the ``crud`` monolith: ``crud/__init__.py`` still holds -the bulk of the functions, and per-entity modules like this one take over as the code -around them is touched. - ``_REQUIREMENT_RELATED_FIELDS`` covers exactly what ``RequirementWithMetricsSchema`` in ``routers/requirement.py`` serializes -- user, and each metric with its metric_type, backend_type and tags. Status, organization and project are left out because nothing reads diff --git a/apps/backend/src/rhesis/backend/app/crud/source.py b/apps/backend/src/rhesis/backend/app/crud/source.py index c039ff29fb..4b281ffc70 100644 --- a/apps/backend/src/rhesis/backend/app/crud/source.py +++ b/apps/backend/src/rhesis/backend/app/crud/source.py @@ -1,9 +1,5 @@ """CRUD operations for sources and their chunks. -Part of the incremental split of the ``crud`` monolith: ``crud/__init__.py`` still holds -the bulk of the functions, and per-entity modules like this one take over as the code -around them is touched. - ``Source.content`` is a ``deferred()`` column on the model -- the full extracted text of a document or web page, which no list or detail response returns. ``get_source`` and ``get_sources`` therefore leave it unloaded, and ``get_source_with_content`` exists as a diff --git a/apps/backend/src/rhesis/backend/app/crud/status.py b/apps/backend/src/rhesis/backend/app/crud/status.py index 97f208df5b..4408ba4a7f 100644 --- a/apps/backend/src/rhesis/backend/app/crud/status.py +++ b/apps/backend/src/rhesis/backend/app/crud/status.py @@ -1,9 +1,4 @@ -"""CRUD operations for statuses. - -Part of the incremental split of the ``crud`` monolith: ``crud/__init__.py`` still holds -the bulk of the functions, and per-entity modules like this one take over as the code -around them is touched -- see ``apps/backend/AGENTS.md``'s crud-layout rule. -""" +"""CRUD operations for statuses.""" import uuid from typing import List, Optional diff --git a/apps/backend/src/rhesis/backend/app/crud/tag.py b/apps/backend/src/rhesis/backend/app/crud/tag.py index 49dd02f95c..2fd6fe392a 100644 --- a/apps/backend/src/rhesis/backend/app/crud/tag.py +++ b/apps/backend/src/rhesis/backend/app/crud/tag.py @@ -1,9 +1,5 @@ """CRUD operations for tags and their assignment to entities. -Part of the incremental split of the ``crud`` monolith: ``crud/__init__.py`` still holds -the bulk of the functions, and per-entity modules like this one take over as the code -around them is touched. - ``assign_tag`` and ``remove_tag`` link a tag to any kind of entity through the ``TaggedItem`` table, which stores the target as an ``entity_id`` plus an ``entity_type`` string rather than a foreign key. The entity type drives the lookup of the ORM class: diff --git a/apps/backend/src/rhesis/backend/app/crud/task.py b/apps/backend/src/rhesis/backend/app/crud/task.py index c70a8ba8c8..b9cdc8efad 100644 --- a/apps/backend/src/rhesis/backend/app/crud/task.py +++ b/apps/backend/src/rhesis/backend/app/crud/task.py @@ -1,9 +1,5 @@ """CRUD operations for tasks. -Part of the incremental split of the ``crud`` monolith: ``crud/__init__.py`` still holds -the bulk of the functions, and per-entity modules like this one take over as the code -around them is touched. - ``get_task`` and ``get_tasks`` pass ``selectin_chains=[["comments"]]`` explicitly. ``Task.comments`` is a custom polymorphic relationship (``viewonly``, matched on ``Comment.entity_id`` plus ``entity_type == "Task"``), not one of the CommentsMixin diff --git a/apps/backend/src/rhesis/backend/app/crud/telemetry.py b/apps/backend/src/rhesis/backend/app/crud/telemetry.py index 01602ae032..2ee2fc40dc 100644 --- a/apps/backend/src/rhesis/backend/app/crud/telemetry.py +++ b/apps/backend/src/rhesis/backend/app/crud/telemetry.py @@ -1,6 +1,6 @@ """CRUD operations for OpenTelemetry traces and spans. -Split out of ``crud/__init__.py``. Import the functions directly:: +Import the functions directly:: from rhesis.backend.app.crud.telemetry import query_traces """ diff --git a/apps/backend/src/rhesis/backend/app/crud/test.py b/apps/backend/src/rhesis/backend/app/crud/test.py index b9e2692620..40f9eadbca 100644 --- a/apps/backend/src/rhesis/backend/app/crud/test.py +++ b/apps/backend/src/rhesis/backend/app/crud/test.py @@ -1,9 +1,4 @@ -"""CRUD operations for tests. - -Part of the incremental split of the ``crud`` monolith: ``crud/__init__.py`` still holds -the test-set functions, and per-entity modules like this one take over as the code around -them is touched -- see ``apps/backend/AGENTS.md``'s crud-layout rule. -""" +"""CRUD operations for tests.""" import uuid from typing import Any, Dict, List, Optional diff --git a/apps/backend/src/rhesis/backend/app/crud/test_result.py b/apps/backend/src/rhesis/backend/app/crud/test_result.py index f1e536d039..03482ef226 100644 --- a/apps/backend/src/rhesis/backend/app/crud/test_result.py +++ b/apps/backend/src/rhesis/backend/app/crud/test_result.py @@ -1,9 +1,5 @@ """CRUD operations for test results. -Part of the incremental split of the ``crud`` monolith: ``crud/__init__.py`` still holds -the bulk of the functions, and per-entity modules like this one take over as the code -around them is touched. - ``_TEST_RESULT_RELATED_FIELDS`` is what ``TestResultDetail`` serializes -- the test run, the test, and the test's prompt and requirement. All many-to-one, so eager-loading them in one query costs nothing; without them a results list issues four queries per row. diff --git a/apps/backend/src/rhesis/backend/app/crud/test_run.py b/apps/backend/src/rhesis/backend/app/crud/test_run.py index f5ba4fe286..473e00afe4 100644 --- a/apps/backend/src/rhesis/backend/app/crud/test_run.py +++ b/apps/backend/src/rhesis/backend/app/crud/test_run.py @@ -1,9 +1,5 @@ """CRUD operations for test runs. -Part of the incremental split of the ``crud`` monolith: ``crud/__init__.py`` still holds -the bulk of the functions, and per-entity modules like this one take over as the code -around them is touched. - ``get_test_run`` and ``get_test_runs`` both push ``_defer_endpoint_last_token`` through ``with_custom_filter``. The eager chain down to ``TestConfiguration.endpoint`` would otherwise pull ``Endpoint.last_token`` -- a large encrypted OAuth token that no test run diff --git a/apps/backend/src/rhesis/backend/app/crud/token.py b/apps/backend/src/rhesis/backend/app/crud/token.py index a5dd755688..faaa49c243 100644 --- a/apps/backend/src/rhesis/backend/app/crud/token.py +++ b/apps/backend/src/rhesis/backend/app/crud/token.py @@ -1,9 +1,5 @@ """CRUD operations for API tokens. -Part of the incremental split of the ``crud`` monolith: ``crud/__init__.py`` still holds -the bulk of the functions, and per-entity modules like this one take over as the code -around them is touched. - Token rows are exempt from the ambient scope auto-filter, so organization scoping here is explicit: ``get_user_tokens``, ``count_user_tokens``, ``revoke_user_tokens`` and ``get_token_by_value`` all take ``organization_id`` and apply the filter by hand. Passing diff --git a/apps/backend/src/rhesis/backend/app/crud/tool.py b/apps/backend/src/rhesis/backend/app/crud/tool.py index 6a2a00525d..b3a70beb33 100644 --- a/apps/backend/src/rhesis/backend/app/crud/tool.py +++ b/apps/backend/src/rhesis/backend/app/crud/tool.py @@ -1,9 +1,4 @@ -"""CRUD operations for tools. - -Part of the incremental split of the ``crud`` monolith: ``crud/__init__.py`` still holds -the bulk of the functions, and per-entity modules like this one take over as the code -around them is touched -- see ``apps/backend/AGENTS.md``'s crud-layout rule. -""" +"""CRUD operations for tools.""" import uuid from typing import List, Optional diff --git a/apps/backend/src/rhesis/backend/app/crud/topic.py b/apps/backend/src/rhesis/backend/app/crud/topic.py index 1039291add..0b7d3b315e 100644 --- a/apps/backend/src/rhesis/backend/app/crud/topic.py +++ b/apps/backend/src/rhesis/backend/app/crud/topic.py @@ -1,9 +1,4 @@ -"""CRUD operations for topics. - -Part of the incremental split of the ``crud`` monolith: ``crud/__init__.py`` still holds -the bulk of the functions, and per-entity modules like this one take over as the code -around them is touched -- see ``apps/backend/AGENTS.md``'s crud-layout rule. -""" +"""CRUD operations for topics.""" import uuid from typing import List, Optional diff --git a/apps/backend/src/rhesis/backend/app/crud/type_lookup.py b/apps/backend/src/rhesis/backend/app/crud/type_lookup.py index d39a16fe2f..1fff863f81 100644 --- a/apps/backend/src/rhesis/backend/app/crud/type_lookup.py +++ b/apps/backend/src/rhesis/backend/app/crud/type_lookup.py @@ -1,9 +1,4 @@ -"""CRUD operations for type lookups. - -Part of the incremental split of the ``crud`` monolith: ``crud/__init__.py`` still holds -the bulk of the functions, and per-entity modules like this one take over as the code -around them is touched -- see ``apps/backend/AGENTS.md``'s crud-layout rule. -""" +"""CRUD operations for type lookups.""" import uuid from typing import List, Optional diff --git a/apps/backend/src/rhesis/backend/app/crud/user.py b/apps/backend/src/rhesis/backend/app/crud/user.py index c95860d846..da8fb1b0a7 100644 --- a/apps/backend/src/rhesis/backend/app/crud/user.py +++ b/apps/backend/src/rhesis/backend/app/crud/user.py @@ -1,9 +1,5 @@ """CRUD operations for users. -Part of the incremental split of the ``crud`` monolith: ``crud/__init__.py`` still holds -the bulk of the functions, and per-entity modules like this one take over as the code -around them is touched. - Users are the one entity that regularly exists *outside* an organization, so most of these functions deliberately sidestep the tenant machinery the rest of ``crud`` relies on. ``create_user`` builds the ``User`` row directly instead of going through ``create_item`` diff --git a/apps/backend/src/rhesis/backend/app/utils/database_exceptions.py b/apps/backend/src/rhesis/backend/app/utils/database_exceptions.py index 0b5d4b9b06..2adf10cb52 100644 --- a/apps/backend/src/rhesis/backend/app/utils/database_exceptions.py +++ b/apps/backend/src/rhesis/backend/app/utils/database_exceptions.py @@ -173,11 +173,11 @@ def handle_database_exceptions( Usage: @handle_database_exceptions(entity_name="demographic") def create_demographic(...): - return crud.create_demographic(...) + return demographic_crud.create_demographic(...) @handle_database_exceptions(entity_name="organization") async def create_organization(...): - return crud.create_organization(...) + return organization_crud.create_organization(...) """ def decorator(func: Callable) -> Callable: @@ -253,7 +253,7 @@ def with_database_error_handling( Usage: with with_database_error_handling(entity_name="demographic"): - return crud.create_demographic(...) + return demographic_crud.create_demographic(...) """ class DatabaseErrorContext: diff --git a/tests/backend/crud/test_transaction_management.py b/tests/backend/crud/test_transaction_management.py index 09b43eec9f..6d39e0ff1a 100644 --- a/tests/backend/crud/test_transaction_management.py +++ b/tests/backend/crud/test_transaction_management.py @@ -10,7 +10,7 @@ - Proper transaction isolation - Data integrity after operations -Functions tested from app/crud.py: +Functions tested from app/crud/: - create_organization - update_user - create_user diff --git a/tests/backend/services/test_endpoint_service.py b/tests/backend/services/test_endpoint_service.py index 6291c841c9..f7a42d9683 100644 --- a/tests/backend/services/test_endpoint_service.py +++ b/tests/backend/services/test_endpoint_service.py @@ -314,7 +314,7 @@ async def test_full_invocation_flow(self): mock_invoker.invoke = AsyncMock(return_value={"status": "success", "data": "response"}) # Mock database -- _get_endpoint is mocked directly rather than the DB - # query chain, since it now delegates to crud.get_endpoint internally. + # query chain, since it now delegates to endpoint_crud.get_endpoint internally. mock_db = Mock(spec=Session) input_data = {"message": "test input"} From 7137cc8592dc69df093d1fc47abf5ceee81b2374 Mon Sep 17 00:00:00 2001 From: Arkadiusz Kwasigroch Date: Thu, 27 Aug 2026 10:53:20 +0200 Subject: [PATCH 2/2] refactor(backend): extract test set crud module Finishes the crud split. The TestSet functions move into crud/test_set.py and crud/__init__.py is left empty, so callers switch to direct submodule imports. Emptying __init__.py also removes the names it re-exported by accident, which several modules were relying on: crud.get_item_detail, crud.include and crud.delete_item are crud_utils/query_utils names, and crud.schemas.TestConfigurationUpdate reached schemas through the package. Each now imports from its real home. Also restores delete_test_configuration, which went missing when test_configuration was extracted -- DELETE /test_configurations/{id} has been raising AttributeError since. test_service_security.py passed the crud package itself into its table of functions to check for an organization_id parameter. The loop guards with hasattr, so an empty __init__.py would not fail it -- the five test set assertions would just quietly stop checking anything. Points them at test_set_crud. --- .../src/rhesis/backend/app/crud/__init__.py | 315 +---------------- .../src/rhesis/backend/app/crud/explorer.py | 14 +- .../backend/app/crud/test_configuration.py | 30 +- .../src/rhesis/backend/app/crud/test_set.py | 316 +++++++++++++++++- .../rhesis/backend/app/routers/experiments.py | 4 +- .../rhesis/backend/app/routers/explorer.py | 5 +- .../src/rhesis/backend/app/routers/test.py | 5 +- .../backend/app/routers/test_configuration.py | 4 +- .../rhesis/backend/app/routers/test_set.py | 14 +- .../rhesis/backend/app/services/experiment.py | 8 +- .../app/services/explorer/evaluation.py | 6 +- .../app/services/explorer/responses.py | 6 +- .../app/services/explorer/suggestions.py | 6 +- .../backend/app/services/explorer/tests.py | 19 +- .../backend/app/services/explorer/utils.py | 5 +- .../app/services/metric_tuning/test_sets.py | 2 +- .../backend/app/services/organization.py | 7 +- .../rhesis/backend/app/services/test_set.py | 16 +- .../app/services/tool/mcp/operations.py | 1 - .../rhesis/backend/jobs/embedding/graph.py | 9 +- .../rhesis/backend/jobs/execution/modes.py | 4 +- .../src/rhesis/backend/jobs/test_set.py | 4 +- tests/backend/crud/test_test_crud.py | 9 +- tests/backend/crud/test_test_set_crud.py | 19 +- .../test_attach_tests_to_existing_test_set.py | 5 +- .../backend/security/test_service_security.py | 55 +-- tests/backend/services/explorer/test_tests.py | 7 +- tests/backend/services/garak/test_sync.py | 4 +- tests/backend/services/test_test.py | 8 +- tests/backend/services/test_test_set.py | 21 +- tests/backend/test_secret_equality.py | 4 +- 31 files changed, 475 insertions(+), 457 deletions(-) diff --git a/apps/backend/src/rhesis/backend/app/crud/__init__.py b/apps/backend/src/rhesis/backend/app/crud/__init__.py index 299e62635f..6ac0afe4ff 100644 --- a/apps/backend/src/rhesis/backend/app/crud/__init__.py +++ b/apps/backend/src/rhesis/backend/app/crud/__init__.py @@ -1,314 +1 @@ -""" -This code implements the CRUD operations for the models in the application. -""" - -import logging -import uuid -from typing import List, Optional - -from sqlalchemy import and_ -from sqlalchemy.orm import Session - -from rhesis.backend.app import models, schemas -from rhesis.backend.app.utils.crud_utils import ( - create_item, - delete_item, - get_item_detail, - update_item, -) -from rhesis.backend.app.utils.hidden_rows import exclude_metric_owned -from rhesis.backend.app.utils.query_utils import QueryBuilder, include - -logger = logging.getLogger(__name__) - - -# TestSet CRUD -def get_test_set( - db: Session, test_set_id: uuid.UUID, organization_id: str = None, user_id: str = None -) -> Optional[models.TestSet]: - """ - Get a test set by its UUID, applying proper visibility filtering and organization scoping. - - Raises ``ItemDeletedException`` for a soft-deleted test set. - """ - return get_item_detail( - db, models.TestSet, test_set_id, organization_id=organization_id, user_id=user_id - ) - - -# Relationships serialized by TestSetDetailSchema. All many-to-one -- excludes -# collection relationships (prompts, tests, metrics, test_configurations) -# because those produce cartesian-product joins or lazy fan-out and none of -# these endpoints serialize them. comments/tasks/files/tags ARE serialized -# (via CountsMixin.counts / TagsMixin.tags) -- see with_default_derived_field_loads. -# license_type/owner/assignee/organization/project: unused, excluded. -_TEST_SET_RELATED_FIELDS = ( - include(models.TestSet.status), - include(models.TestSet.test_set_type), - include(models.TestSet.user), -) - - -def get_test_sets( - db: Session, - skip: int = 0, - limit: int = 10, - sort_by: str = "created_at", - sort_order: str = "desc", - filter: str | None = None, - has_runs: bool | None = None, - organization_id: str = None, - user_id: str = None, -) -> List[models.TestSet]: - """ - Get test sets with detail loading and proper filtering. - Public test sets are visible regardless of organization. - Organization filtering is applied when organization_id is provided. - """ - query_builder = ( - QueryBuilder(db, models.TestSet) - .with_related(*_TEST_SET_RELATED_FIELDS) - .with_default_derived_field_loads() - .with_organization_filter(organization_id) # Apply organization filtering - .with_visibility_filter(user_id) - .with_odata_filter(filter) - .with_pagination(skip, limit) - .with_sorting(sort_by, sort_order) - ) - - # Add test runs filter if specified - if has_runs is not None: - - def has_runs_filter(query): - logger.info(f"Applying has_runs filter: {has_runs}") - - if has_runs: - # Only test sets that have test runs - filtered_query = ( - query.join(models.TestConfiguration).join(models.TestRun).distinct() - ) - logger.info("Applied filter for test sets WITH runs") - return filtered_query - else: - # Only test sets that don't have test runs - subquery_builder = QueryBuilder(db, models.TestSet).with_organization_filter( - organization_id - ) - subquery = ( - subquery_builder.build() - .join(models.TestConfiguration) - .join(models.TestRun) - .distinct() - .with_entities(models.TestSet.id) - .subquery() - ) - filtered_query = query.filter(~models.TestSet.id.in_(subquery)) - logger.info("Applied filter for test sets WITHOUT runs") - return filtered_query - - query_builder = query_builder.with_custom_filter(has_runs_filter) - - # Exclude explorer test sets (they use the dedicated /explorer API) - # A metric's tuning test set is reachable only through its metric. - return ( - query_builder.with_custom_filter(lambda q: q.filter(models.TestSet.explorer_row.is_(False))) - .with_custom_filter(exclude_metric_owned(models.TestSet)) - .all() - ) - - -def create_test_set( - db: Session, test_set: schemas.TestSetCreate, organization_id: str = None, user_id: str = None -) -> models.TestSet: - """Create test_set.""" - return create_item(db, models.TestSet, test_set, organization_id, user_id) - - -def update_test_set( - db: Session, - test_set_id: uuid.UUID, - test_set: schemas.TestSetUpdate, - organization_id: str = None, - user_id: str = None, -) -> Optional[models.TestSet]: - """Update test_set.""" - return update_item(db, models.TestSet, test_set_id, test_set, organization_id, user_id) - - -def delete_test_set( - db: Session, test_set_id: uuid.UUID, organization_id: str, user_id: str -) -> Optional[models.TestSet]: - return delete_item( - db, models.TestSet, test_set_id, organization_id=organization_id, user_id=user_id - ) - - -def get_test_set_by_nano_id_or_slug( - db: Session, identifier: str, organization_id: str = None, user_id: str = None -) -> Optional[models.TestSet]: - """ - Get a test set by its nano_id or slug, applying proper visibility filtering. - - Raises ``ItemDeletedException`` for a soft-deleted test set. - """ - from rhesis.backend.app.utils.crud_utils import _check_and_raise_if_deleted - - item = ( - QueryBuilder(db, models.TestSet) - .with_deleted() - .with_related(*_TEST_SET_RELATED_FIELDS) - .with_organization_filter(organization_id) - .with_visibility_filter(user_id) - .with_custom_filter( - lambda q: q.filter( - (models.TestSet.nano_id == identifier) | (models.TestSet.slug == identifier) - ) - ) - .first() - ) - return _check_and_raise_if_deleted(item, models.TestSet, identifier, False) - - -def resolve_test_set( - identifier: str, db: Session, organization_id: str = None -) -> Optional[models.TestSet]: - """ - Resolve a test set from any valid identifier (UUID, nano_id, or slug). - Returns None if not found or if there's an error parsing the identifier. - - Raises: - ItemDeletedException: If the identifier resolves to a soft-deleted - test set. Not caught here so callers get the same 410 behavior as - a direct ID lookup. - """ - try: - # First try UUID - try: - identifier_uuid = uuid.UUID(identifier) - db_test_set = get_test_set( - db, test_set_id=identifier_uuid, organization_id=organization_id - ) - except ValueError: - # If not UUID, try nano_id or slug - db_test_set = get_test_set_by_nano_id_or_slug( - db, identifier, organization_id=organization_id - ) - - return db_test_set - except ValueError: - return None - - -def get_test_sets_for_test( - db: Session, - test_id: uuid.UUID, - skip: int = 0, - limit: int = 10, - sort_by: str = "created_at", - sort_order: str = "desc", - organization_id: str = None, - user_id: str = None, - filter: str | None = None, -) -> tuple[List[models.TestSet], int]: - """ - Get test sets that contain a given test with pagination, sorting and filtering. - - Args: - db: Database session - test_id: ID of the test to find test sets for - skip: Number of items to skip - limit: Maximum number of items to return - sort_by: Field to sort by - sort_order: Sort order (asc/desc) - organization_id: Organization ID for tenant scoping - user_id: User ID for tenant scoping - filter: OData filter string - - Returns: - Tuple containing: - - List of test sets with their related objects loaded - - Total count before pagination - """ - query_builder = ( - QueryBuilder(db, models.TestSet) - .with_related(*_TEST_SET_RELATED_FIELDS) - .with_organization_filter(organization_id) - .with_visibility_filter(user_id) - .with_custom_filter( - lambda q: q.join(models.test.test_test_set_association).filter( - and_( - models.test.test_test_set_association.c.test_id == test_id, - models.test.test_test_set_association.c.organization_id == organization_id, - ) - ) - ) - .with_odata_filter(filter) - ) - - total_count = query_builder.count() - items = query_builder.with_pagination(skip, limit).with_sorting(sort_by, sort_order).all() - - return items, total_count - - -def get_test_set_tests( - db: Session, - test_set_id: uuid.UUID, - skip: int = 0, - limit: int = 10, - sort_by: str = "created_at", - sort_order: str = "desc", - filter: str | None = None, -) -> tuple[List[models.Test], int]: - """ - Get tests associated with a test set with pagination, sorting and filtering support. - - Args: - db: Database session - test_set_id: ID of the test set to get tests for - skip: Number of items to skip - limit: Maximum number of items to return - sort_by: Field to sort by - sort_order: Sort order (asc/desc) - filter: OData filter string - - Returns: - Tuple containing: - - List of tests with their related objects loaded - - Total count of tests before pagination - """ - query_builder = ( - QueryBuilder(db, models.Test) - # Eager-load the relationships TestDetailSchema serializes. with_related - # picks the strategy per name from its own cardinality, so this can never - # regress into the 22-join cartesian product that previously materialized - # multi-GB intermediate result sets on this endpoint -- accidentally adding - # a one-to-many name here (e.g. test_results/trace) would - # route through selectin, not joinedload. - .with_related( - include(models.Test.prompt), - include(models.Test.test_type), - include(models.Test.user), - include(models.Test.assignee), - include(models.Test.owner), - include(models.Test.topic), - include(models.Test.requirement), - include(models.Test.category), - include(models.Test.status), - ) - .with_visibility_filter() - .with_custom_filter( - lambda q: q.join(models.test.test_test_set_association).filter( - models.test.test_test_set_association.c.test_set_id == test_set_id - ) - ) - .with_odata_filter(filter) - ) - - # Get total count before pagination - total_count = query_builder.count() - - # Get paginated results - items = query_builder.with_pagination(skip, limit).with_sorting(sort_by, sort_order).all() - - return items, total_count +"""CRUD operations, one module per entity. Import the module you need directly.""" diff --git a/apps/backend/src/rhesis/backend/app/crud/explorer.py b/apps/backend/src/rhesis/backend/app/crud/explorer.py index 47ab1ec149..8dafdae182 100644 --- a/apps/backend/src/rhesis/backend/app/crud/explorer.py +++ b/apps/backend/src/rhesis/backend/app/crud/explorer.py @@ -1,9 +1,5 @@ """CRUD operations specific to Explorer test sets. -Part of the incremental split of the ``crud`` monolith: ``crud/__init__.py`` still holds -the bulk of the functions, and per-entity modules like this one take over as the code -around them is touched. - Every function here flushes and never commits -- the request session owns the commit (see ``get_db_with_tenant_variables`` in ``database.py``). """ @@ -17,13 +13,11 @@ from sqlalchemy.orm import Session, contains_eager, joinedload from rhesis.backend.app import models, schemas - -# _TEST_SET_RELATED_FIELDS is imported rather than relocated: three other functions in -# the monolith use the same tuple, and moving it would mean deciding a new home for a -# TestSet-wide constant while this module only owns the Explorer slice. -# crud/__init__.py never imports this module, so the parent-package import is cycle-free. -from rhesis.backend.app.crud import _TEST_SET_RELATED_FIELDS from rhesis.backend.app.crud.embedding import create_embedding, get_embedding_by_hash + +# _TEST_SET_RELATED_FIELDS lives with the test-set CRUD: the test-set reads all share the +# tuple, and this module only owns the Explorer slice. +from rhesis.backend.app.crud.test_set import _TEST_SET_RELATED_FIELDS from rhesis.backend.app.models.enums import ModelType from rhesis.backend.app.models.test import test_test_set_association from rhesis.backend.app.schemas.explorer_metadata import ( diff --git a/apps/backend/src/rhesis/backend/app/crud/test_configuration.py b/apps/backend/src/rhesis/backend/app/crud/test_configuration.py index 5fa4011a60..8fec270dc3 100644 --- a/apps/backend/src/rhesis/backend/app/crud/test_configuration.py +++ b/apps/backend/src/rhesis/backend/app/crud/test_configuration.py @@ -1,9 +1,4 @@ -"""CRUD operations for test configurations. - -Part of the incremental split of the ``crud`` monolith: ``crud/__init__.py`` still holds -the bulk of the functions, and per-entity modules like this one take over as the code -around them is touched -- see ``apps/backend/AGENTS.md``'s crud-layout rule. -""" +"""CRUD operations for test configurations.""" import uuid from typing import List, Optional @@ -11,7 +6,12 @@ from sqlalchemy.orm import Session from rhesis.backend.app import models, schemas -from rhesis.backend.app.utils.crud_utils import create_item, get_item_detail, update_item +from rhesis.backend.app.utils.crud_utils import ( + create_item, + delete_item, + get_item_detail, + update_item, +) from rhesis.backend.app.utils.query_utils import QueryBuilder @@ -79,3 +79,19 @@ def update_test_configuration( organization_id, user_id, ) + + +def delete_test_configuration( + db: Session, + test_configuration_id: uuid.UUID, + organization_id: str = None, + user_id: str = None, +) -> Optional[models.TestConfiguration]: + """Delete test_configuration.""" + return delete_item( + db, + models.TestConfiguration, + test_configuration_id, + organization_id=organization_id, + user_id=user_id, + ) diff --git a/apps/backend/src/rhesis/backend/app/crud/test_set.py b/apps/backend/src/rhesis/backend/app/crud/test_set.py index b5d1a9e78e..3ccb625bb7 100644 --- a/apps/backend/src/rhesis/backend/app/crud/test_set.py +++ b/apps/backend/src/rhesis/backend/app/crud/test_set.py @@ -1,17 +1,315 @@ -"""CRUD operations for test sets. - -Part of the incremental split of the ``crud`` monolith: ``crud/__init__.py`` still holds -most test-set functions, and new code goes into modules like this one instead of growing -the monolith further -- see ``apps/backend/AGENTS.md``'s crud-layout rule. -""" +"""CRUD operations for test sets.""" +import logging import uuid -from typing import Dict, List +from typing import Dict, List, Optional +from sqlalchemy import and_ from sqlalchemy.orm import Session -from rhesis.backend.app import models -from rhesis.backend.app.utils.crud_utils import bulk_delete_by_ids +from rhesis.backend.app import models, schemas +from rhesis.backend.app.utils.crud_utils import ( + bulk_delete_by_ids, + create_item, + delete_item, + get_item_detail, + update_item, +) +from rhesis.backend.app.utils.hidden_rows import exclude_metric_owned +from rhesis.backend.app.utils.query_utils import QueryBuilder, include + +logger = logging.getLogger(__name__) + + +def get_test_set( + db: Session, test_set_id: uuid.UUID, organization_id: str = None, user_id: str = None +) -> Optional[models.TestSet]: + """ + Get a test set by its UUID, applying proper visibility filtering and organization scoping. + + Raises ``ItemDeletedException`` for a soft-deleted test set. + """ + return get_item_detail( + db, models.TestSet, test_set_id, organization_id=organization_id, user_id=user_id + ) + + +# Relationships serialized by TestSetDetailSchema. All many-to-one -- excludes +# collection relationships (prompts, tests, metrics, test_configurations) +# because those produce cartesian-product joins or lazy fan-out and none of +# these endpoints serialize them. comments/tasks/files/tags ARE serialized +# (via CountsMixin.counts / TagsMixin.tags) -- see with_default_derived_field_loads. +# license_type/owner/assignee/organization/project: unused, excluded. +_TEST_SET_RELATED_FIELDS = ( + include(models.TestSet.status), + include(models.TestSet.test_set_type), + include(models.TestSet.user), +) + + +def get_test_sets( + db: Session, + skip: int = 0, + limit: int = 10, + sort_by: str = "created_at", + sort_order: str = "desc", + filter: str | None = None, + has_runs: bool | None = None, + organization_id: str = None, + user_id: str = None, +) -> List[models.TestSet]: + """ + Get test sets with detail loading and proper filtering. + Public test sets are visible regardless of organization. + Organization filtering is applied when organization_id is provided. + """ + query_builder = ( + QueryBuilder(db, models.TestSet) + .with_related(*_TEST_SET_RELATED_FIELDS) + .with_default_derived_field_loads() + .with_organization_filter(organization_id) # Apply organization filtering + .with_visibility_filter(user_id) + .with_odata_filter(filter) + .with_pagination(skip, limit) + .with_sorting(sort_by, sort_order) + ) + + # Add test runs filter if specified + if has_runs is not None: + + def has_runs_filter(query): + logger.info(f"Applying has_runs filter: {has_runs}") + + if has_runs: + # Only test sets that have test runs + filtered_query = ( + query.join(models.TestConfiguration).join(models.TestRun).distinct() + ) + logger.info("Applied filter for test sets WITH runs") + return filtered_query + else: + # Only test sets that don't have test runs + subquery_builder = QueryBuilder(db, models.TestSet).with_organization_filter( + organization_id + ) + subquery = ( + subquery_builder.build() + .join(models.TestConfiguration) + .join(models.TestRun) + .distinct() + .with_entities(models.TestSet.id) + .subquery() + ) + filtered_query = query.filter(~models.TestSet.id.in_(subquery)) + logger.info("Applied filter for test sets WITHOUT runs") + return filtered_query + + query_builder = query_builder.with_custom_filter(has_runs_filter) + + # Exclude explorer test sets (they use the dedicated /explorer API) + # A metric's tuning test set is reachable only through its metric. + return ( + query_builder.with_custom_filter(lambda q: q.filter(models.TestSet.explorer_row.is_(False))) + .with_custom_filter(exclude_metric_owned(models.TestSet)) + .all() + ) + + +def create_test_set( + db: Session, test_set: schemas.TestSetCreate, organization_id: str = None, user_id: str = None +) -> models.TestSet: + """Create test_set.""" + return create_item(db, models.TestSet, test_set, organization_id, user_id) + + +def update_test_set( + db: Session, + test_set_id: uuid.UUID, + test_set: schemas.TestSetUpdate, + organization_id: str = None, + user_id: str = None, +) -> Optional[models.TestSet]: + """Update test_set.""" + return update_item(db, models.TestSet, test_set_id, test_set, organization_id, user_id) + + +def delete_test_set( + db: Session, test_set_id: uuid.UUID, organization_id: str, user_id: str +) -> Optional[models.TestSet]: + return delete_item( + db, models.TestSet, test_set_id, organization_id=organization_id, user_id=user_id + ) + + +def get_test_set_by_nano_id_or_slug( + db: Session, identifier: str, organization_id: str = None, user_id: str = None +) -> Optional[models.TestSet]: + """ + Get a test set by its nano_id or slug, applying proper visibility filtering. + + Raises ``ItemDeletedException`` for a soft-deleted test set. + """ + from rhesis.backend.app.utils.crud_utils import _check_and_raise_if_deleted + + item = ( + QueryBuilder(db, models.TestSet) + .with_deleted() + .with_related(*_TEST_SET_RELATED_FIELDS) + .with_organization_filter(organization_id) + .with_visibility_filter(user_id) + .with_custom_filter( + lambda q: q.filter( + (models.TestSet.nano_id == identifier) | (models.TestSet.slug == identifier) + ) + ) + .first() + ) + return _check_and_raise_if_deleted(item, models.TestSet, identifier, False) + + +def resolve_test_set( + identifier: str, db: Session, organization_id: str = None +) -> Optional[models.TestSet]: + """ + Resolve a test set from any valid identifier (UUID, nano_id, or slug). + Returns None if not found or if there's an error parsing the identifier. + + Raises: + ItemDeletedException: If the identifier resolves to a soft-deleted + test set. Not caught here so callers get the same 410 behavior as + a direct ID lookup. + """ + try: + # First try UUID + try: + identifier_uuid = uuid.UUID(identifier) + db_test_set = get_test_set( + db, test_set_id=identifier_uuid, organization_id=organization_id + ) + except ValueError: + # If not UUID, try nano_id or slug + db_test_set = get_test_set_by_nano_id_or_slug( + db, identifier, organization_id=organization_id + ) + + return db_test_set + except ValueError: + return None + + +def get_test_sets_for_test( + db: Session, + test_id: uuid.UUID, + skip: int = 0, + limit: int = 10, + sort_by: str = "created_at", + sort_order: str = "desc", + organization_id: str = None, + user_id: str = None, + filter: str | None = None, +) -> tuple[List[models.TestSet], int]: + """ + Get test sets that contain a given test with pagination, sorting and filtering. + + Args: + db: Database session + test_id: ID of the test to find test sets for + skip: Number of items to skip + limit: Maximum number of items to return + sort_by: Field to sort by + sort_order: Sort order (asc/desc) + organization_id: Organization ID for tenant scoping + user_id: User ID for tenant scoping + filter: OData filter string + + Returns: + Tuple containing: + - List of test sets with their related objects loaded + - Total count before pagination + """ + query_builder = ( + QueryBuilder(db, models.TestSet) + .with_related(*_TEST_SET_RELATED_FIELDS) + .with_organization_filter(organization_id) + .with_visibility_filter(user_id) + .with_custom_filter( + lambda q: q.join(models.test.test_test_set_association).filter( + and_( + models.test.test_test_set_association.c.test_id == test_id, + models.test.test_test_set_association.c.organization_id == organization_id, + ) + ) + ) + .with_odata_filter(filter) + ) + + total_count = query_builder.count() + items = query_builder.with_pagination(skip, limit).with_sorting(sort_by, sort_order).all() + + return items, total_count + + +def get_test_set_tests( + db: Session, + test_set_id: uuid.UUID, + skip: int = 0, + limit: int = 10, + sort_by: str = "created_at", + sort_order: str = "desc", + filter: str | None = None, +) -> tuple[List[models.Test], int]: + """ + Get tests associated with a test set with pagination, sorting and filtering support. + + Args: + db: Database session + test_set_id: ID of the test set to get tests for + skip: Number of items to skip + limit: Maximum number of items to return + sort_by: Field to sort by + sort_order: Sort order (asc/desc) + filter: OData filter string + + Returns: + Tuple containing: + - List of tests with their related objects loaded + - Total count of tests before pagination + """ + query_builder = ( + QueryBuilder(db, models.Test) + # Eager-load the relationships TestDetailSchema serializes. with_related + # picks the strategy per name from its own cardinality, so this can never + # regress into the 22-join cartesian product that previously materialized + # multi-GB intermediate result sets on this endpoint -- accidentally adding + # a one-to-many name here (e.g. test_results/trace) would + # route through selectin, not joinedload. + .with_related( + include(models.Test.prompt), + include(models.Test.test_type), + include(models.Test.user), + include(models.Test.assignee), + include(models.Test.owner), + include(models.Test.topic), + include(models.Test.requirement), + include(models.Test.category), + include(models.Test.status), + ) + .with_visibility_filter() + .with_custom_filter( + lambda q: q.join(models.test.test_test_set_association).filter( + models.test.test_test_set_association.c.test_set_id == test_set_id + ) + ) + .with_odata_filter(filter) + ) + + # Get total count before pagination + total_count = query_builder.count() + + # Get paginated results + items = query_builder.with_pagination(skip, limit).with_sorting(sort_by, sort_order).all() + + return items, total_count def bulk_delete_test_sets( diff --git a/apps/backend/src/rhesis/backend/app/routers/experiments.py b/apps/backend/src/rhesis/backend/app/routers/experiments.py index d5d9cde569..7366b3da5a 100644 --- a/apps/backend/src/rhesis/backend/app/routers/experiments.py +++ b/apps/backend/src/rhesis/backend/app/routers/experiments.py @@ -19,7 +19,6 @@ from fastapi import Depends, HTTPException, Query, Request, Response, status from sqlalchemy.orm import Session -from rhesis.backend.app import crud from rhesis.backend.app.auth.capabilities import Permission from rhesis.backend.app.auth.principal import resolve_principal_from_request from rhesis.backend.app.auth.rbac import authorize_object, project_id_from_scope @@ -52,6 +51,7 @@ to_read, unbind_environment, ) +from rhesis.backend.app.utils.crud_utils import delete_item router = RhesisRouter( prefix="/experiments", @@ -229,7 +229,7 @@ def delete_experiment( unbind_environment(db, project=project, environment_name=env_name) snapshot = to_read(db_experiment) - crud.delete_item( + delete_item( db, Experiment, experiment_id, diff --git a/apps/backend/src/rhesis/backend/app/routers/explorer.py b/apps/backend/src/rhesis/backend/app/routers/explorer.py index 07bc53af71..9e9fa87b44 100644 --- a/apps/backend/src/rhesis/backend/app/routers/explorer.py +++ b/apps/backend/src/rhesis/backend/app/routers/explorer.py @@ -16,8 +16,9 @@ from fastapi.responses import StreamingResponse from sqlalchemy.orm import Session -from rhesis.backend.app import crud, models, schemas +from rhesis.backend.app import models, schemas from rhesis.backend.app.auth.user_utils import require_current_user_or_token +from rhesis.backend.app.crud import test_set as test_set_crud from rhesis.backend.app.crud.explorer import only_explorer_test_sets from rhesis.backend.app.dependencies import ( get_tenant_context, @@ -86,7 +87,7 @@ def _resolve_test_set_or_raise(identifier: str, db: Session, organization_id: str): """Resolve a test set by identifier (UUID, nano_id, or slug).""" - db_test_set = crud.resolve_test_set(identifier, db, organization_id) + db_test_set = test_set_crud.resolve_test_set(identifier, db, organization_id) if db_test_set is None: raise HTTPException( status_code=404, diff --git a/apps/backend/src/rhesis/backend/app/routers/test.py b/apps/backend/src/rhesis/backend/app/routers/test.py index f9457fd880..9c31368e9c 100644 --- a/apps/backend/src/rhesis/backend/app/routers/test.py +++ b/apps/backend/src/rhesis/backend/app/routers/test.py @@ -8,13 +8,14 @@ from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session -from rhesis.backend.app import crud, models, schemas +from rhesis.backend.app import models, schemas from rhesis.backend.app.auth.capabilities import Permission, capability from rhesis.backend.app.auth.quota_gates import require_quota from rhesis.backend.app.auth.user_utils import require_current_user_or_token from rhesis.backend.app.crud import endpoint as endpoint_crud from rhesis.backend.app.crud import file as file_crud from rhesis.backend.app.crud import test as test_crud +from rhesis.backend.app.crud import test_set as test_set_crud from rhesis.backend.app.dependencies import ( get_tenant_context, get_tenant_db_session, @@ -262,7 +263,7 @@ def get_test_test_sets( if db_test is None: raise HTTPException(status_code=404, detail="Test not found") - items, count = crud.get_test_sets_for_test( + items, count = test_set_crud.get_test_sets_for_test( db=db, test_id=test_id, skip=skip, diff --git a/apps/backend/src/rhesis/backend/app/routers/test_configuration.py b/apps/backend/src/rhesis/backend/app/routers/test_configuration.py index 73fceb2272..7adf11f878 100644 --- a/apps/backend/src/rhesis/backend/app/routers/test_configuration.py +++ b/apps/backend/src/rhesis/backend/app/routers/test_configuration.py @@ -6,7 +6,7 @@ from fastapi.responses import JSONResponse from sqlalchemy.orm import Session -from rhesis.backend.app import crud, models, schemas +from rhesis.backend.app import models, schemas from rhesis.backend.app.auth.capabilities import Permission, capability from rhesis.backend.app.auth.quota_gates import require_quota from rhesis.backend.app.auth.user_utils import require_current_user_or_token @@ -173,7 +173,7 @@ def delete_test_configuration( if db_test_configuration is None: raise HTTPException(status_code=404, detail="Test configuration not found") - return crud.delete_test_configuration( + return test_configuration_crud.delete_test_configuration( db=db, test_configuration_id=test_configuration_id, organization_id=organization_id, diff --git a/apps/backend/src/rhesis/backend/app/routers/test_set.py b/apps/backend/src/rhesis/backend/app/routers/test_set.py index 85c0f79c63..6645d7cc07 100644 --- a/apps/backend/src/rhesis/backend/app/routers/test_set.py +++ b/apps/backend/src/rhesis/backend/app/routers/test_set.py @@ -8,7 +8,7 @@ from pydantic import BaseModel from sqlalchemy.orm import Session -from rhesis.backend.app import crud, models, schemas +from rhesis.backend.app import models, schemas from rhesis.backend.app.auth.capabilities import Permission, capability from rhesis.backend.app.auth.quota_gates import require_quota from rhesis.backend.app.auth.user_utils import require_current_user_or_token @@ -89,7 +89,7 @@ def resolve_test_set_or_raise(identifier: str, db: Session, organization_id: str Raises: HTTPException: 404 error if test set is not found """ - db_test_set = crud.resolve_test_set(identifier, db, organization_id) + db_test_set = test_set_crud.resolve_test_set(identifier, db, organization_id) if db_test_set is None: raise HTTPException(status_code=404, detail="Test Set not found with provided identifier") # A metric's tuning test set is reachable only through its metric. Hiding it @@ -318,7 +318,7 @@ def create_test_set( ): """Create a new test set.""" organization_id, user_id = tenant_context - return crud.create_test_set( + return test_set_crud.create_test_set( db=db, test_set=test_set, organization_id=organization_id, user_id=user_id ) @@ -367,7 +367,7 @@ def read_test_sets( logger.info(f"test_sets endpoint called with has_runs={has_runs}") organization_id, user_id = tenant_context - results = crud.get_test_sets( + results = test_set_crud.get_test_sets( db=db, skip=skip, limit=limit, @@ -425,7 +425,7 @@ def delete_test_set( current_user: User = Depends(require_current_user_or_token), ): organization_id, user_id = tenant_context - db_test_set = crud.delete_test_set( + db_test_set = test_set_crud.delete_test_set( db, test_set_id=test_set_id, organization_id=organization_id, user_id=user_id ) if db_test_set is None: @@ -447,7 +447,7 @@ def update_test_set( """Update an existing test set by UUID, nano_id, or slug.""" organization_id, user_id = tenant_context test_set_id = resolve_test_set_or_raise(test_set_identifier, db, organization_id).id - db_test_set = crud.update_test_set( + db_test_set = test_set_crud.update_test_set( db, test_set_id=test_set_id, test_set=test_set, @@ -520,7 +520,7 @@ def get_test_set_tests( db_test_set = resolve_test_set_or_raise( test_set_identifier, db, str(current_user.organization_id) ) - items, count = crud.get_test_set_tests( + items, count = test_set_crud.get_test_set_tests( db=db, test_set_id=db_test_set.id, skip=skip, diff --git a/apps/backend/src/rhesis/backend/app/services/experiment.py b/apps/backend/src/rhesis/backend/app/services/experiment.py index f272890d79..7ada4ea256 100644 --- a/apps/backend/src/rhesis/backend/app/services/experiment.py +++ b/apps/backend/src/rhesis/backend/app/services/experiment.py @@ -31,7 +31,7 @@ from sqlalchemy.orm import Session from sqlalchemy.orm.exc import DetachedInstanceError -from rhesis.backend.app import crud, models +from rhesis.backend.app import models from rhesis.backend.app.crud.project import get_project from rhesis.backend.app.models.experiment import Experiment from rhesis.backend.app.models.project import Project @@ -53,6 +53,8 @@ validate_environment_name, validate_values_against_schema, ) +from rhesis.backend.app.utils.crud_utils import get_item_detail +from rhesis.backend.app.utils.query_utils import include logger = logging.getLogger(__name__) @@ -78,13 +80,13 @@ def get_visible_experiment( - Anything else surfaces as 404 (never 403). Returning 404 keeps experiment existence from leaking across users / orgs. """ - db_experiment = crud.get_item_detail( + db_experiment = get_item_detail( db, Experiment, experiment_id, organization_id=organization_id, user_id=user_id, - related_fields=(crud.include(Experiment.project),), + related_fields=(include(Experiment.project),), ) if db_experiment is None: raise HTTPException( diff --git a/apps/backend/src/rhesis/backend/app/services/explorer/evaluation.py b/apps/backend/src/rhesis/backend/app/services/explorer/evaluation.py index 133c8cd7f2..68430c958e 100644 --- a/apps/backend/src/rhesis/backend/app/services/explorer/evaluation.py +++ b/apps/backend/src/rhesis/backend/app/services/explorer/evaluation.py @@ -8,7 +8,7 @@ from sqlalchemy.orm import Session -from rhesis.backend.app import crud +from rhesis.backend.app.crud import test_set as test_set_crud from rhesis.backend.app.crud.explorer import set_explorer_test_metadata from rhesis.backend.app.crud.metric import get_metrics from rhesis.backend.app.schemas.explorer import ( @@ -324,7 +324,9 @@ async def evaluate_tests_for_explorer_set( """ sdk_metrics = resolve_sdk_metrics(db, organization_id, user_id, metric_names) - db_test_set = crud.resolve_test_set(test_set_identifier, db, organization_id=organization_id) + db_test_set = test_set_crud.resolve_test_set( + test_set_identifier, db, organization_id=organization_id + ) if db_test_set is None: raise ValueError(f"Test set not found with identifier: {test_set_identifier}") diff --git a/apps/backend/src/rhesis/backend/app/services/explorer/responses.py b/apps/backend/src/rhesis/backend/app/services/explorer/responses.py index 232492127f..4bae06e7b5 100644 --- a/apps/backend/src/rhesis/backend/app/services/explorer/responses.py +++ b/apps/backend/src/rhesis/backend/app/services/explorer/responses.py @@ -5,7 +5,7 @@ from sqlalchemy.orm import Session -from rhesis.backend.app import crud +from rhesis.backend.app.crud import test_set as test_set_crud from rhesis.backend.app.crud.explorer import set_explorer_test_outputs from rhesis.backend.app.schemas.explorer import ( GenerateOutputsFailedItem, @@ -67,7 +67,9 @@ async def generate_outputs_for_tests( GenerateOutputsResponse Counts plus the per-test ``updated`` and ``failed`` items. """ - db_test_set = crud.resolve_test_set(test_set_identifier, db, organization_id=organization_id) + db_test_set = test_set_crud.resolve_test_set( + test_set_identifier, db, organization_id=organization_id + ) if db_test_set is None: raise ValueError(f"Test set not found with identifier: {test_set_identifier}") diff --git a/apps/backend/src/rhesis/backend/app/services/explorer/suggestions.py b/apps/backend/src/rhesis/backend/app/services/explorer/suggestions.py index 28b542411e..b99366dfd3 100644 --- a/apps/backend/src/rhesis/backend/app/services/explorer/suggestions.py +++ b/apps/backend/src/rhesis/backend/app/services/explorer/suggestions.py @@ -9,7 +9,7 @@ from pydantic import BaseModel, Field, create_model from sqlalchemy.orm import Session -from rhesis.backend.app import crud +from rhesis.backend.app.crud import test_set as test_set_crud from rhesis.backend.app.crud import user as user_crud from rhesis.backend.app.quota.enforcement import stream_error_message from rhesis.backend.app.schemas.explorer import GenerateSuggestionsResponse, SuggestedTest @@ -149,7 +149,9 @@ def _prepare_suggestion_context( Returns a context dict with resolved model, prompt, topic_value, and sample_size, or ``None`` when there are no eligible tests. """ - db_test_set = crud.resolve_test_set(test_set_identifier, db, organization_id=organization_id) + db_test_set = test_set_crud.resolve_test_set( + test_set_identifier, db, organization_id=organization_id + ) if db_test_set is None: raise ValueError(f"Test set not found: {test_set_identifier}") diff --git a/apps/backend/src/rhesis/backend/app/services/explorer/tests.py b/apps/backend/src/rhesis/backend/app/services/explorer/tests.py index 8bb27a7748..98e41efccc 100644 --- a/apps/backend/src/rhesis/backend/app/services/explorer/tests.py +++ b/apps/backend/src/rhesis/backend/app/services/explorer/tests.py @@ -6,12 +6,13 @@ from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.orm import Session -from rhesis.backend.app import crud, models, schemas +from rhesis.backend.app import models, schemas # Imported as a module rather than by name: this file's own public # get_explorer_test_sets() wraps the crud function of the same name. from rhesis.backend.app.crud import explorer as crud_explorer from rhesis.backend.app.crud import test as test_crud +from rhesis.backend.app.crud import test_set as test_set_crud from rhesis.backend.app.models.user import User from rhesis.backend.app.schemas.explorer import ( ExportExplorerTestSetResponse, @@ -182,7 +183,7 @@ def create_explorer_test_set( description=description, test_set_type_id=test_set_type_lookup.id, ) - new_set = crud.create_test_set( + new_set = test_set_crud.create_test_set( db=db, test_set=test_set_data, organization_id=organization_id, @@ -231,7 +232,7 @@ def delete_explorer_test_set( Resolves the test set by UUID, nano_id, or slug. Raises ValueError if the set is missing or is not flagged as Explorer-owned. """ - db_test_set = crud.resolve_test_set(test_set_identifier, db, organization_id) + db_test_set = test_set_crud.resolve_test_set(test_set_identifier, db, organization_id) if db_test_set is None: raise ValueError("Test set not found with provided identifier") if not is_explorer_test_set(db_test_set): @@ -243,7 +244,7 @@ def delete_explorer_test_set( _delete_session_tests(db, [db_test_set.id], organization_id, user_id) - deleted = crud.delete_test_set( + deleted = test_set_crud.delete_test_set( db, test_set_id=db_test_set.id, organization_id=organization_id, @@ -355,12 +356,12 @@ def _copy_test_set_tests( copied = 0 skipped = 0 skipped_test_ids: List[str] = [] - # Must stay within crud.get_test_set_tests pagination max (100). + # Must stay within test_set_crud.get_test_set_tests pagination max (100). batch_size = 100 skip = 0 while True: - items, total = crud.get_test_set_tests( + items, total = test_set_crud.get_test_set_tests( db=db, test_set_id=source_test_set_id, skip=skip, @@ -430,7 +431,7 @@ def import_explorer_test_set_from_source( ItemDeletedException If source_test_set_identifier resolves to a soft-deleted test set. """ - db_source = crud.resolve_test_set(source_test_set_identifier, db, organization_id) + db_source = test_set_crud.resolve_test_set(source_test_set_identifier, db, organization_id) if db_source is None: raise ValueError("Test set not found with provided identifier") @@ -524,7 +525,7 @@ def export_regular_test_set_from_explorer( ItemDeletedException If source_test_set_identifier resolves to a soft-deleted test set. """ - db_source = crud.resolve_test_set(source_test_set_identifier, db, organization_id) + db_source = test_set_crud.resolve_test_set(source_test_set_identifier, db, organization_id) if db_source is None: raise ValueError("Test set not found with provided identifier") @@ -547,7 +548,7 @@ def export_regular_test_set_from_explorer( attributes=None, test_set_type_id=test_set_type_lookup.id, ) - new_set = crud.create_test_set( + new_set = test_set_crud.create_test_set( db=db, test_set=test_set_data, organization_id=organization_id, diff --git a/apps/backend/src/rhesis/backend/app/services/explorer/utils.py b/apps/backend/src/rhesis/backend/app/services/explorer/utils.py index 0474e2de93..e3961786a2 100644 --- a/apps/backend/src/rhesis/backend/app/services/explorer/utils.py +++ b/apps/backend/src/rhesis/backend/app/services/explorer/utils.py @@ -4,7 +4,8 @@ from sqlalchemy.orm import Session -from rhesis.backend.app import crud, models +from rhesis.backend.app import models +from rhesis.backend.app.crud import test_set as test_set_crud from rhesis.backend.app.schemas.explorer import TestTreeNode from rhesis.backend.app.schemas.explorer_metadata import parse_explorer_test_metadata from rhesis.backend.app.services.explorer.invocation import NO_OUTPUT @@ -62,7 +63,7 @@ def _get_test_set_tests_from_db( all_tests: list[models.Test] = [] while True: - items, _count = crud.get_test_set_tests( + items, _count = test_set_crud.get_test_set_tests( db=db, test_set_id=test_set_id, skip=skip, diff --git a/apps/backend/src/rhesis/backend/app/services/metric_tuning/test_sets.py b/apps/backend/src/rhesis/backend/app/services/metric_tuning/test_sets.py index 2215d8e909..192291bf44 100644 --- a/apps/backend/src/rhesis/backend/app/services/metric_tuning/test_sets.py +++ b/apps/backend/src/rhesis/backend/app/services/metric_tuning/test_sets.py @@ -18,8 +18,8 @@ from rhesis.backend.app import models, schemas from rhesis.backend.app.constants import TestSetType -from rhesis.backend.app.crud import create_test_set from rhesis.backend.app.crud import metric_tuning as crud_metric_tuning +from rhesis.backend.app.crud.test_set import create_test_set from rhesis.backend.app.utils.crud_utils import get_or_create_type_lookup logger = logging.getLogger(__name__) diff --git a/apps/backend/src/rhesis/backend/app/services/organization.py b/apps/backend/src/rhesis/backend/app/services/organization.py index eb0fe1b9b8..b97f60fa78 100644 --- a/apps/backend/src/rhesis/backend/app/services/organization.py +++ b/apps/backend/src/rhesis/backend/app/services/organization.py @@ -10,11 +10,12 @@ from sqlalchemy.orm import Session, joinedload, with_parent from sqlalchemy.orm.attributes import flag_modified -from rhesis.backend.app import crud, models, schemas +from rhesis.backend.app import models, schemas from rhesis.backend.app.config.settings import get_application_settings from rhesis.backend.app.constants import REQUIREMENT_LIST_KEY from rhesis.backend.app.crud import endpoint as endpoint_crud from rhesis.backend.app.crud import tag as tag_crud +from rhesis.backend.app.crud import test_set as test_set_crud from rhesis.backend.app.crud import user as user_crud from rhesis.backend.app.database import temporary_project_scope from rhesis.backend.app.models.enums import ModelType @@ -25,9 +26,9 @@ from rhesis.backend.app.services.test_set import execute_test_set_on_endpoint from rhesis.backend.app.utils.crud_utils import ( create_default_rhesis_model, - get_or_create_requirement, get_or_create_category, get_or_create_entity, + get_or_create_requirement, get_or_create_status, get_or_create_topic, get_or_create_type_lookup, @@ -1010,7 +1011,7 @@ def execute_initial_test_runs(db: Session, organization_id: str, user_id: str) - # Get all test sets for the organization print(f"\nFetching test sets for organization: {organization_id}") - test_sets = crud.get_test_sets( + test_sets = test_set_crud.get_test_sets( db=db, organization_id=organization_id, # Use default limit (10) - sufficient for initial data diff --git a/apps/backend/src/rhesis/backend/app/services/test_set.py b/apps/backend/src/rhesis/backend/app/services/test_set.py index 137e7d7e25..17b0004598 100644 --- a/apps/backend/src/rhesis/backend/app/services/test_set.py +++ b/apps/backend/src/rhesis/backend/app/services/test_set.py @@ -13,10 +13,10 @@ from rhesis.backend.app import models, schemas from rhesis.backend.app.constants import ( - REQUIREMENT_LIST_KEY, ERROR_BULK_CREATE_FAILED, ERROR_INVALID_UUID, ERROR_TEST_SET_NOT_FOUND, + REQUIREMENT_LIST_KEY, EntityType, TestResultStatus, TestSetType, @@ -572,7 +572,7 @@ def update_test_set_attributes( """ from uuid import UUID - from rhesis.backend.app.crud import get_test_set + from rhesis.backend.app.crud.test_set import get_test_set from rhesis.backend.app.utils.database_exceptions import ItemDeletedException # Validate UUID @@ -629,9 +629,9 @@ def get_last_completed_test_run( Raises: ItemDeletedException: If test_set_identifier resolves to a - soft-deleted test set (via crud.resolve_test_set). + soft-deleted test set (via test_set_crud.resolve_test_set). """ - from rhesis.backend.app import crud + from rhesis.backend.app.crud import test_set as test_set_crud from rhesis.backend.app.models.status import Status from rhesis.backend.app.models.test_configuration import ( TestConfiguration, @@ -640,7 +640,9 @@ def get_last_completed_test_run( from rhesis.backend.jobs.enums import RunStatus # Resolve test set - db_test_set = crud.resolve_test_set(test_set_identifier, db, organization_id=organization_id) + db_test_set = test_set_crud.resolve_test_set( + test_set_identifier, db, organization_id=organization_id + ) if db_test_set is None: return None @@ -741,8 +743,8 @@ def execute_test_set_on_endpoint( PermissionError: For access control errors RuntimeError: For execution errors """ - from rhesis.backend.app import crud from rhesis.backend.app.crud import endpoint as endpoint_crud + from rhesis.backend.app.crud import test_set as test_set_crud logger.info( f"Starting test set execution for identifier: {test_set_identifier} " @@ -757,7 +759,7 @@ def execute_test_set_on_endpoint( # Resolve test set logger.debug(f"Resolving test set with identifier: {test_set_identifier}") - db_test_set = crud.resolve_test_set( + db_test_set = test_set_crud.resolve_test_set( test_set_identifier, db, organization_id=str(current_user.organization_id) ) if db_test_set is None: diff --git a/apps/backend/src/rhesis/backend/app/services/tool/mcp/operations.py b/apps/backend/src/rhesis/backend/app/services/tool/mcp/operations.py index c3520c2cc6..0b55078459 100644 --- a/apps/backend/src/rhesis/backend/app/services/tool/mcp/operations.py +++ b/apps/backend/src/rhesis/backend/app/services/tool/mcp/operations.py @@ -2,7 +2,6 @@ import logging from typing import Any, Dict, List, Optional, Tuple -from rhesis.backend.app import crud from rhesis.backend.app.config.settings import get_model_settings from rhesis.backend.app.database import get_db_with_tenant_variables from rhesis.backend.app.services.tool.exceptions import ToolConfigurationError diff --git a/apps/backend/src/rhesis/backend/jobs/embedding/graph.py b/apps/backend/src/rhesis/backend/jobs/embedding/graph.py index d20348c73d..ad721026d0 100644 --- a/apps/backend/src/rhesis/backend/jobs/embedding/graph.py +++ b/apps/backend/src/rhesis/backend/jobs/embedding/graph.py @@ -113,12 +113,12 @@ def _ensure_embeddings_for_entities( def _collect_test_set_entity_ids(db, test_set_id: UUID) -> list[UUID]: """Return visible test IDs for a test set (excludes soft-deleted tests).""" - from rhesis.backend.app import crud + from rhesis.backend.app.crud import test_set as test_set_crud entity_ids: list[UUID] = [] skip = 0 while True: - items, _count = crud.get_test_set_tests( + items, _count = test_set_crud.get_test_set_tests( db=db, test_set_id=test_set_id, skip=skip, @@ -199,12 +199,13 @@ def _run_embedding_graph( def _run_test_set_embedding_graph(db, *, test_set_id: str, user_id: str) -> None: - from rhesis.backend.app import crud, models + from rhesis.backend.app import models + from rhesis.backend.app.crud import test_set as test_set_crud test_set_uuid = UUID(test_set_id) def load_parent(db_session, user): - return crud.get_test_set( + return test_set_crud.get_test_set( db_session, test_set_uuid, str(user.organization_id), diff --git a/apps/backend/src/rhesis/backend/jobs/execution/modes.py b/apps/backend/src/rhesis/backend/jobs/execution/modes.py index 0112ff43c3..9fcef8a4c8 100644 --- a/apps/backend/src/rhesis/backend/jobs/execution/modes.py +++ b/apps/backend/src/rhesis/backend/jobs/execution/modes.py @@ -9,7 +9,7 @@ from sqlalchemy.orm import Session -from rhesis.backend.app import crud +from rhesis.backend.app import schemas from rhesis.backend.app.crud import test_configuration as test_configuration_crud from rhesis.backend.app.models.test import Test from rhesis.backend.app.models.test_configuration import TestConfiguration @@ -90,7 +90,7 @@ def set_execution_mode( test_configuration_crud.update_test_configuration( db, test_config.id, - crud.schemas.TestConfigurationUpdate(**update_data), + schemas.TestConfigurationUpdate(**update_data), organization_id=organization_id, user_id=user_id, ) diff --git a/apps/backend/src/rhesis/backend/jobs/test_set.py b/apps/backend/src/rhesis/backend/jobs/test_set.py index ab30a267c6..905664ccfc 100644 --- a/apps/backend/src/rhesis/backend/jobs/test_set.py +++ b/apps/backend/src/rhesis/backend/jobs/test_set.py @@ -1,8 +1,8 @@ import logging from typing import Any, List, Optional, Union -from rhesis.backend.app import crud from rhesis.backend.app.constants import TestSetType +from rhesis.backend.app.crud import test_set as test_set_crud from rhesis.backend.app.crud import user as user_crud from rhesis.backend.app.database import get_db_with_tenant_variables from rhesis.backend.app.models.enums import NotificationEventType @@ -65,7 +65,7 @@ def count_test_sets(self): # Use tenant-aware database session with explicit organization_id and user_id with get_db_with_tenant_variables(org_id or "", user_id or "", project_id or "") as db: # Get all test sets with the proper tenant context - test_sets = crud.get_test_sets(db, organization_id=org_id, user_id=user_id) + test_sets = test_set_crud.get_test_sets(db, organization_id=org_id, user_id=user_id) total_count = len(test_sets) self.log_with_context("info", "Total test sets counted", total_count=total_count) diff --git a/tests/backend/crud/test_test_crud.py b/tests/backend/crud/test_test_crud.py index a2ee2eae24..aaf1c00a1b 100644 --- a/tests/backend/crud/test_test_crud.py +++ b/tests/backend/crud/test_test_crud.py @@ -18,9 +18,10 @@ import pytest from sqlalchemy.orm import Session -from rhesis.backend.app import crud, models +from rhesis.backend.app import models from rhesis.backend.app.constants import EXPLORER_REQUIREMENT_NAME from rhesis.backend.app.crud import test as test_crud +from rhesis.backend.app.crud import test_set as test_set_crud from rhesis.backend.app.models.test import test_test_set_association from rhesis.backend.app.services import test_set as test_set_service from rhesis.backend.app.utils.crud_utils import count_items @@ -114,7 +115,7 @@ def test_update_test_refreshes_test_set_attributes_on_requirement_change( organization_id=test_org_id, user_id=authenticated_user_id, ) - seeded_test_set = crud.get_test_set( + seeded_test_set = test_set_crud.get_test_set( test_db, test_set.id, organization_id=test_org_id, @@ -134,7 +135,7 @@ def test_update_test_refreshes_test_set_attributes_on_requirement_change( assert result is not None assert result.requirement_id == robustness.id - reloaded_test_set = crud.get_test_set( + reloaded_test_set = test_set_crud.get_test_set( test_db, test_set.id, organization_id=test_org_id, @@ -211,7 +212,7 @@ def test_update_test_skips_explorer_test_set_attribute_refresh( user_id=authenticated_user_id, ) - reloaded_explorer_test_set = crud.get_test_set( + reloaded_explorer_test_set = test_set_crud.get_test_set( test_db, explorer_test_set.id, organization_id=test_org_id, diff --git a/tests/backend/crud/test_test_set_crud.py b/tests/backend/crud/test_test_set_crud.py index 2459f8a7ad..66e88c5a83 100644 --- a/tests/backend/crud/test_test_set_crud.py +++ b/tests/backend/crud/test_test_set_crud.py @@ -11,8 +11,9 @@ import pytest from sqlalchemy.orm import Session -from rhesis.backend.app import crud, models +from rhesis.backend.app import models from rhesis.backend.app.crud import test as test_crud +from rhesis.backend.app.crud import test_set as test_set_crud from rhesis.backend.app.utils.database_exceptions import ItemDeletedException @@ -34,12 +35,12 @@ def test_get_test_set_raises_for_deleted( test_db.refresh(test_set) test_set_id = test_set.id - crud.delete_test_set( + test_set_crud.delete_test_set( test_db, test_set_id, organization_id=test_org_id, user_id=authenticated_user_id ) with pytest.raises(ItemDeletedException): - crud.get_test_set(test_db, test_set_id, organization_id=test_org_id) + test_set_crud.get_test_set(test_db, test_set_id, organization_id=test_org_id) def test_get_test_set_by_nano_id_or_slug_raises_for_deleted( self, test_db: Session, test_org_id: str, authenticated_user_id: str @@ -56,17 +57,19 @@ def test_get_test_set_by_nano_id_or_slug_raises_for_deleted( test_set_id = test_set.id slug = test_set.slug - crud.delete_test_set( + test_set_crud.delete_test_set( test_db, test_set_id, organization_id=test_org_id, user_id=authenticated_user_id ) with pytest.raises(ItemDeletedException): - crud.get_test_set_by_nano_id_or_slug(test_db, slug, organization_id=test_org_id) + test_set_crud.get_test_set_by_nano_id_or_slug( + test_db, slug, organization_id=test_org_id + ) def test_get_test_set_returns_none_for_nonexistent(self, test_db: Session, test_org_id: str): import uuid - result = crud.get_test_set(test_db, uuid.uuid4(), organization_id=test_org_id) + result = test_set_crud.get_test_set(test_db, uuid.uuid4(), organization_id=test_org_id) assert result is None @@ -75,7 +78,7 @@ def test_get_test_set_returns_none_for_nonexistent(self, test_db: Session, test_ class TestUpdateTestSetAttributesSoftDeleteHandling: """update_test_set_attributes must still no-op when a linked test set is deleted. - Regression test: crud.get_test_set now raises ItemDeletedException instead of + Regression test: test_set_crud.get_test_set now raises ItemDeletedException instead of returning None, so update_test_set_attributes (called by test_crud.update_test for every test set a test belongs to) must catch that itself -- otherwise updating a test would fail with a 410 just because an unrelated linked test set was @@ -123,7 +126,7 @@ def test_update_test_succeeds_when_linked_test_set_is_deleted( ) test_db.commit() - crud.delete_test_set( + test_set_crud.delete_test_set( test_db, test_set.id, organization_id=test_org_id, user_id=authenticated_user_id ) diff --git a/tests/backend/jobs/test_attach_tests_to_existing_test_set.py b/tests/backend/jobs/test_attach_tests_to_existing_test_set.py index 53e2191b43..7d9f6f05a3 100644 --- a/tests/backend/jobs/test_attach_tests_to_existing_test_set.py +++ b/tests/backend/jobs/test_attach_tests_to_existing_test_set.py @@ -16,7 +16,8 @@ import pytest from sqlalchemy.orm import Session -from rhesis.backend.app import crud, models +from rhesis.backend.app import models +from rhesis.backend.app.crud import test_set as test_set_crud from rhesis.backend.app.utils.database_exceptions import ItemDeletedException @@ -52,7 +53,7 @@ def test_raises_item_deleted_exception_for_deleted_test_set( test_db.commit() test_db.refresh(test_set) - crud.delete_test_set( + test_set_crud.delete_test_set( test_db, test_set.id, organization_id=test_org_id, user_id=authenticated_user_id ) diff --git a/tests/backend/security/test_service_security.py b/tests/backend/security/test_service_security.py index 7e5ce4adc7..a2922e37e6 100644 --- a/tests/backend/security/test_service_security.py +++ b/tests/backend/security/test_service_security.py @@ -10,8 +10,9 @@ import pytest from sqlalchemy.orm import Session -from rhesis.backend.app import crud, models +from rhesis.backend.app import models from rhesis.backend.app.crud import tag as tag_crud +from rhesis.backend.app.crud import test_set as test_set_crud from tests.backend.routes.fixtures.data_factories import TagDataFactory @@ -164,7 +165,7 @@ def test_get_test_set_organization_filtering(self, test_db: Session): import inspect # Verify that get_test_set accepts organization_id parameter - signature = inspect.signature(crud.get_test_set) + signature = inspect.signature(test_set_crud.get_test_set) assert "organization_id" in signature.parameters, ( "get_test_set should accept organization_id for test set scoping" ) @@ -203,7 +204,7 @@ def test_get_test_set_organization_filtering(self, test_db: Session): test_db.commit() # User from org1 should be able to access the test set - result_org1 = crud.get_test_set( + result_org1 = test_set_crud.get_test_set( test_db, test_set.id, organization_id=str(org1.id), user_id=str(user1.id) ) assert result_org1 is not None @@ -211,7 +212,7 @@ def test_get_test_set_organization_filtering(self, test_db: Session): assert str(result_org1.organization_id) == str(org1.id) # User from org2 should NOT be able to access the test set - result_org2 = crud.get_test_set( + result_org2 = test_set_crud.get_test_set( test_db, test_set.id, organization_id=str(org2.id), user_id=str(user2.id) ) assert result_org2 is None @@ -220,14 +221,14 @@ def test_get_test_set_organization_filtering(self, test_db: Session): with pytest.raises( ValueError, match="organization_id is required for TestSet but was not provided" ): - crud.get_test_set(test_db, test_set.id) + test_set_crud.get_test_set(test_db, test_set.id) def test_create_test_set_organization_scoping(self, test_db: Session): """🔒 SECURITY: Test that create_test_set properly scopes test sets to organizations""" import inspect # Verify that create_test_set accepts organization_id parameter - signature = inspect.signature(crud.create_test_set) + signature = inspect.signature(test_set_crud.create_test_set) assert "organization_id" in signature.parameters, ( "create_test_set should accept organization_id for test set scoping" ) @@ -254,7 +255,7 @@ def test_create_test_set_organization_scoping(self, test_db: Session): "visibility": "organization", } - result = crud.create_test_set( + result = test_set_crud.create_test_set( test_db, test_set_data, organization_id=str(org.id), user_id=str(user.id) ) @@ -269,7 +270,7 @@ def test_delete_test_set_organization_filtering(self, test_db: Session): import inspect # Verify that delete_test_set accepts organization_id parameter - signature = inspect.signature(crud.delete_test_set) + signature = inspect.signature(test_set_crud.delete_test_set) assert "organization_id" in signature.parameters, ( "delete_test_set should accept organization_id for test set scoping" ) @@ -301,12 +302,12 @@ def test_delete_test_set_organization_filtering(self, test_db: Session): "is_published": False, "visibility": "organization", } - test_set = crud.create_test_set( + test_set = test_set_crud.create_test_set( test_db, test_set_data, organization_id=str(org1.id), user_id=str(user1.id) ) # User from org1 should be able to delete the test set - result_org1 = crud.delete_test_set( + result_org1 = test_set_crud.delete_test_set( test_db, test_set.id, organization_id=str(org1.id), user_id=str(user1.id) ) assert result_org1 is not None # Test set was found and deleted @@ -318,12 +319,12 @@ def test_delete_test_set_organization_filtering(self, test_db: Session): "is_published": False, "visibility": "organization", } - test_set2 = crud.create_test_set( + test_set2 = test_set_crud.create_test_set( test_db, test_set_data2, organization_id=str(org1.id), user_id=str(user1.id) ) # User from org2 should NOT be able to delete the test set from org1 - result_org2 = crud.delete_test_set( + result_org2 = test_set_crud.delete_test_set( test_db, test_set2.id, organization_id=str(org2.id), user_id=str(user2.id) ) assert result_org2 is None # Test set was not found/deleted due to organization filtering @@ -333,7 +334,7 @@ def test_update_test_set_organization_filtering(self, test_db: Session): import inspect # Verify that update_test_set accepts organization_id parameter - signature = inspect.signature(crud.update_test_set) + signature = inspect.signature(test_set_crud.update_test_set) assert "organization_id" in signature.parameters, ( "update_test_set should accept organization_id for test set scoping" ) @@ -365,13 +366,13 @@ def test_update_test_set_organization_filtering(self, test_db: Session): "is_published": False, "visibility": "organization", } - test_set = crud.create_test_set( + test_set = test_set_crud.create_test_set( test_db, test_set_data, organization_id=str(org1.id), user_id=str(user1.id) ) # User from org1 should be able to update the test set update_data = {"name": f"Updated TestSet {unique_id}"} - result_org1 = crud.update_test_set( + result_org1 = test_set_crud.update_test_set( test_db, test_set.id, update_data, organization_id=str(org1.id) ) assert result_org1 is not None @@ -380,7 +381,7 @@ def test_update_test_set_organization_filtering(self, test_db: Session): # User from org2 should NOT be able to update the test set from org1 update_data2 = {"name": f"Should Not Update {unique_id}"} - result_org2 = crud.update_test_set( + result_org2 = test_set_crud.update_test_set( test_db, test_set.id, update_data2, organization_id=str(org2.id) ) assert result_org2 is None # Test set was not found/updated due to organization filtering @@ -395,7 +396,7 @@ def test_get_test_sets_organization_filtering(self, test_db: Session): import inspect # Verify that get_test_sets accepts organization_id parameter - signature = inspect.signature(crud.get_test_sets) + signature = inspect.signature(test_set_crud.get_test_sets) assert "organization_id" in signature.parameters, ( "get_test_sets should accept organization_id for filtering" ) @@ -453,7 +454,7 @@ def test_get_test_sets_organization_filtering(self, test_db: Session): test_db.commit() # Get test sets for org1 - should return at least the 2 we created - result_org1 = crud.get_test_sets(test_db, organization_id=str(org1.id)) + result_org1 = test_set_crud.get_test_sets(test_db, organization_id=str(org1.id)) assert len(result_org1) >= 2 # At least the 2 we created, could be more from initial data assert all(str(ts.organization_id) == str(org1.id) for ts in result_org1) @@ -463,7 +464,7 @@ def test_get_test_sets_organization_filtering(self, test_db: Session): assert f"Test Set 2 Org 1 {unique_id}" in test_set_names_org1 # Get test sets for org2 - should return at least the 1 we created - result_org2 = crud.get_test_sets(test_db, organization_id=str(org2.id)) + result_org2 = test_set_crud.get_test_sets(test_db, organization_id=str(org2.id)) assert len(result_org2) >= 1 # At least the 1 we created, could be more from initial data assert all(str(ts.organization_id) == str(org2.id) for ts in result_org2) @@ -475,9 +476,9 @@ def test_get_test_sets_organization_filtering(self, test_db: Session): with pytest.raises( ValueError, match="organization_id is required for TestSet but was not provided" ): - crud.get_test_sets(test_db) + test_set_crud.get_test_sets(test_db) - # Note: get_test_set_by_name function doesn't exist in crud.py, so this test is removed + # Note: there is no get_test_set_by_name function, so this test is removed @pytest.mark.security @@ -493,12 +494,12 @@ def test_service_functions_accept_organization_filtering(self, test_db: Session) (tag_crud, "get_tag"), (tag_crud, "create_tag"), (tag_crud, "delete_tag"), - (crud, "get_test_set"), - (crud, "create_test_set"), - (crud, "delete_test_set"), - (crud, "update_test_set"), - (crud, "get_test_sets"), - # Note: get_test_set_by_name function doesn't exist in crud.py + (test_set_crud, "get_test_set"), + (test_set_crud, "create_test_set"), + (test_set_crud, "delete_test_set"), + (test_set_crud, "update_test_set"), + (test_set_crud, "get_test_sets"), + # Note: there is no get_test_set_by_name function ] for module, func_name in service_functions: diff --git a/tests/backend/services/explorer/test_tests.py b/tests/backend/services/explorer/test_tests.py index 36d9b919dd..bcbd75d90c 100644 --- a/tests/backend/services/explorer/test_tests.py +++ b/tests/backend/services/explorer/test_tests.py @@ -6,7 +6,8 @@ from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session -from rhesis.backend.app import crud, models +from rhesis.backend.app import models +from rhesis.backend.app.crud import test_set as test_set_crud from rhesis.backend.app.crud.explorer import create_explorer_test, get_test_ids_in_test_sets from rhesis.backend.app.database import without_soft_delete_filter from rhesis.backend.app.schemas.explorer import TestTreeNode, TopicNode @@ -1291,7 +1292,7 @@ def test_exports_tests_skips_markers_and_preserves_topics( assert new_set.explorer_row is False assert (new_set.attributes or {}).get("adaptive_settings") is None - items, total = crud.get_test_set_tests( + items, total = test_set_crud.get_test_set_tests( db=test_db, test_set_id=new_set.id, skip=0, @@ -1359,7 +1360,7 @@ def flaky_create_explorer_test(db, **kwargs): assert result.skipped == 3 assert len(result.skipped_test_ids) == 3 - items, total = crud.get_test_set_tests( + items, total = test_set_crud.get_test_set_tests( db=test_db, test_set_id=result.test_set.id, skip=0, diff --git a/tests/backend/services/garak/test_sync.py b/tests/backend/services/garak/test_sync.py index 1b2e8e9e48..8d8ae89425 100644 --- a/tests/backend/services/garak/test_sync.py +++ b/tests/backend/services/garak/test_sync.py @@ -7,7 +7,7 @@ from faker import Faker from sqlalchemy.orm import Session -from rhesis.backend.app import crud +from rhesis.backend.app.crud import test_set as test_set_crud from rhesis.backend.app.models.test import Test, test_test_set_association from rhesis.backend.app.models.test_set import TestSet from rhesis.backend.app.services.garak.probes import GarakProbeInfo @@ -152,7 +152,7 @@ def deleted_garak_test_set(self, test_db: Session, test_org_id, authenticated_us test_db.commit() test_db.refresh(test_set) - crud.delete_test_set( + test_set_crud.delete_test_set( test_db, test_set.id, organization_id=test_org_id, user_id=authenticated_user_id ) return test_set diff --git a/tests/backend/services/test_test.py b/tests/backend/services/test_test.py index f789b99d0b..b54387aaeb 100644 --- a/tests/backend/services/test_test.py +++ b/tests/backend/services/test_test.py @@ -519,7 +519,7 @@ def test_create_test_set_associations_test_set_deleted( self, test_db: Session, authenticated_user_id, test_org_id ): """A soft-deleted test set must not crash with ItemDeletedException.""" - from rhesis.backend.app import crud + from rhesis.backend.app.crud import test_set as test_set_crud test_set_data = create_test_set_data() test_set = models.TestSet( @@ -528,7 +528,7 @@ def test_create_test_set_associations_test_set_deleted( test_db.add(test_set) test_db.commit() - crud.delete_test_set( + test_set_crud.delete_test_set( test_db, test_set.id, organization_id=test_org_id, user_id=authenticated_user_id ) @@ -657,7 +657,7 @@ def test_remove_test_set_associations_test_set_deleted( self, test_db: Session, authenticated_user_id, test_org_id ): """A soft-deleted test set must not crash with ItemDeletedException.""" - from rhesis.backend.app import crud + from rhesis.backend.app.crud import test_set as test_set_crud test_set_data = create_test_set_data() test_set = models.TestSet( @@ -666,7 +666,7 @@ def test_remove_test_set_associations_test_set_deleted( test_db.add(test_set) test_db.commit() - crud.delete_test_set( + test_set_crud.delete_test_set( test_db, test_set.id, organization_id=test_org_id, user_id=authenticated_user_id ) diff --git a/tests/backend/services/test_test_set.py b/tests/backend/services/test_test_set.py index 71db0aadd2..765b17abf3 100644 --- a/tests/backend/services/test_test_set.py +++ b/tests/backend/services/test_test_set.py @@ -13,12 +13,13 @@ from pydantic import ValidationError from sqlalchemy.orm import Session -from rhesis.backend.app import crud, models, schemas +from rhesis.backend.app import models, schemas from rhesis.backend.app.constants import ( EXPLORER_REQUIREMENT_NAME, TestSetType, TestType, ) +from rhesis.backend.app.crud import test_set as test_set_crud from rhesis.backend.app.schemas.validators import resolve_test_type from rhesis.backend.app.services import test_set as test_set_service @@ -243,7 +244,7 @@ def test_execute_test_set_on_endpoint_success( # Mock all the dependencies with ( - patch("rhesis.backend.app.crud.resolve_test_set") as mock_resolve_test_set, + patch("rhesis.backend.app.crud.test_set.resolve_test_set") as mock_resolve_test_set, patch("rhesis.backend.app.crud.endpoint.get_endpoint") as mock_get_endpoint, patch( "rhesis.backend.app.services.test_set._validate_user_access" @@ -328,8 +329,8 @@ def test_execute_test_set_on_endpoint_test_set_not_found( # User already exists from authenticated_user_id fixture - get it from DB user = test_db.query(models.User).filter(models.User.id == authenticated_user_id).first() - # Mock crud.resolve_test_set to return None - with patch("rhesis.backend.app.crud.resolve_test_set") as mock_resolve_test_set: + # Mock test_set_crud.resolve_test_set to return None + with patch("rhesis.backend.app.crud.test_set.resolve_test_set") as mock_resolve_test_set: mock_resolve_test_set.return_value = None # Call the function and expect ValueError @@ -360,7 +361,7 @@ def test_execute_test_set_on_endpoint_endpoint_not_found( # Mock dependencies with ( - patch("rhesis.backend.app.crud.resolve_test_set") as mock_resolve_test_set, + patch("rhesis.backend.app.crud.test_set.resolve_test_set") as mock_resolve_test_set, patch("rhesis.backend.app.crud.endpoint.get_endpoint") as mock_get_endpoint, ): mock_resolve_test_set.return_value = test_set @@ -454,7 +455,7 @@ def test_execute_test_set_on_endpoint_empty_test_set( user = test_db.query(models.User).filter(models.User.id == authenticated_user_id).first() with ( - patch("rhesis.backend.app.crud.resolve_test_set", return_value=test_set), + patch("rhesis.backend.app.crud.test_set.resolve_test_set", return_value=test_set), patch("rhesis.backend.app.crud.endpoint.get_endpoint", return_value=endpoint), patch( "rhesis.backend.app.services.test_set._validate_user_access", @@ -534,7 +535,7 @@ def test_execute_test_set_on_endpoint_with_metrics( # Mock all the dependencies with ( - patch("rhesis.backend.app.crud.resolve_test_set") as mock_resolve_test_set, + patch("rhesis.backend.app.crud.test_set.resolve_test_set") as mock_resolve_test_set, patch("rhesis.backend.app.crud.endpoint.get_endpoint") as mock_get_endpoint, patch( "rhesis.backend.app.services.test_set._validate_user_access" @@ -781,7 +782,7 @@ def test_effective_type_precedence_is_shared(self): @pytest.mark.unit @pytest.mark.service class TestGetTestSetsExcludesExplorer: - """crud.get_test_sets must omit explorer sets (general test set list API).""" + """test_set_crud.get_test_sets must omit explorer sets (general test set list API).""" def test_get_test_sets_excludes_explorer_metadata_requirement( self, test_db: Session, authenticated_user_id, test_org_id @@ -804,7 +805,7 @@ def test_get_test_sets_excludes_explorer_metadata_requirement( test_db.add_all([regular, explorer]) test_db.commit() - results = crud.get_test_sets( + results = test_set_crud.get_test_sets( test_db, organization_id=str(test_org_id), user_id=str(authenticated_user_id), @@ -834,7 +835,7 @@ def test_raises_for_deleted_test_set( test_db.commit() test_db.refresh(test_set) - crud.delete_test_set( + test_set_crud.delete_test_set( test_db, test_set.id, organization_id=test_org_id, user_id=authenticated_user_id ) diff --git a/tests/backend/test_secret_equality.py b/tests/backend/test_secret_equality.py index 1245a427c5..2ee57bb64f 100644 --- a/tests/backend/test_secret_equality.py +++ b/tests/backend/test_secret_equality.py @@ -67,8 +67,8 @@ ALLOWED_SITES: frozenset[tuple[str, int]] = frozenset( { # content_hash is a SHA-based fingerprint for version dedup, not a secret - ("apps/backend/src/rhesis/backend/app/services/experiment.py", 150), - ("apps/backend/src/rhesis/backend/app/services/experiment.py", 206), + ("apps/backend/src/rhesis/backend/app/services/experiment.py", 152), + ("apps/backend/src/rhesis/backend/app/services/experiment.py", 208), # auth_token_project_id is a UUID project reference, not a secret token; # comparing it to another project UUID is safe (no timing oracle risk) ("apps/backend/src/rhesis/backend/app/services/connector/manager.py", 1005),