Skip to content

UN-2977 [FEAT] Introduce users groups with project sharing #1986

Open
kirtimanmishrazipstack wants to merge 18 commits into
mainfrom
UN-2977-Moody-s-Introduce-Group-Based-Project-Sharing
Open

UN-2977 [FEAT] Introduce users groups with project sharing #1986
kirtimanmishrazipstack wants to merge 18 commits into
mainfrom
UN-2977-Moody-s-Introduce-Group-Based-Project-Sharing

Conversation

@kirtimanmishrazipstack
Copy link
Copy Markdown
Contributor

@kirtimanmishrazipstack kirtimanmishrazipstack commented May 22, 2026

What

Adds org-scoped groups as a first-class sharing target alongside the existing per-user share and org-wide share, across all 7 shareable resources (Workflow, Pipeline, API Deployment, Connector, Adapter, Prompt Studio project, and Agentic project). An org admin creates a group, manages its members, and shares resources with the group in one action; access tracks membership automatically.

Why

Admins needed to grant a set of users access to a resource in a single action and have that access follow membership changes, instead of re-sharing to each user individually. Groups are manual-only — login method (SSO or email/password) is authentication only and never drives membership.

How

  • Data model (tenant_account_v2): OrganizationGroup, GroupMembership (explicit through-model), and ResourceGroupShare — a single polymorphic group → resource table that covers every resource type, so no per-resource shared_groups column/migration is needed. shared_groups is exposed as a read property on each model backed by this table.
  • Visibility: each resource manager's for_user(user) unions created_by | shared_users | shared_to_org | group-shared; group ids are read live from GroupMembership, so membership changes take effect immediately. resources_visible_via_groups casts the varchar object_id to the model's UUID PK to avoid a Postgres type mismatch.
  • Permissions: org admins retain full org-wide access to every resource (inherited from the already-merged UN-3479 / UN-3479 [FEAT] Org admin override on resources + ProfileManager sharing #1989, not introduced here); group sharing is additive on top of that and only grants access to non-admin members. All share mutations are funnelled through a single POST /<resource>/{id}/share/ action (the shared_users/shared_groups/shared_to_org fields are read_only on the resource serializers); ShareAuthorizationService diffs requested vs. current state per axis and authorizes each change (owners and admins on every axis). The group resources/ endpoint is admin-only.
  • Reuse: one ResourceShareManagementMixin provides share/ + effective-members/ on every shareable viewset; an admin-gated OrganizationGroupViewSet provides group CRUD + member management.
  • Lifecycle: a post_delete signal on OrganizationMember purges the removed user's group memberships and direct shared_users rows (closes the rejoin backdoor); created_by is untouched.
  • Frontend: a Groups admin page (CRUD + member management + delete-confirm showing affected-resource count) and a batched share modal (edits are local until Apply sends one POST /share/).

Can this PR break any existing features. If yes, please list possible items. If no, please explain why. (PS: Admins do not merge the PR without this section filled)

  • Share mutations via PATCH no longer work. The sharing fields are now read_only on resource serializers; any caller that previously PATCHed shared_users/shared_to_org directly must use the new POST /<resource>/{id}/share/ action. Internal callers and the UI are migrated; external API clients doing direct PATCH-share would silently no-op.
  • Org-member removal now also strips direct shares. The new post_delete signal removes the user from every resource's shared_users (in addition to group memberships). Intentional, but a behavior change vs. before.
  • Org admins keep org-wide access (UN-3479 / UN-3479 [FEAT] Org admin override on resources + ProfileManager sharing #1989). for_user() and the permission classes admit admins; group sharing never restricts them — it only adds access for the group's non-admin members.

Database Migrations

  • tenant_account_v2/0002_organization_group_group_membership.pyOrganizationGroup, GroupMembership.
  • tenant_account_v2/0003_resource_group_share.pyResourceGroupShare.
  • No per-resource migrations (shared_groups is the polymorphic table; shared_users/shared_to_org pre-existed).

Env Config

  • MAX_GROUPS_PER_ORG (default 200) — max groups per org.
  • MAX_MEMBERS_PER_GROUP (default 500) — max members per group.
  • Both optional; exceeding returns HTTP 400 with a structured code.

Relevant Docs

  • Spec: prompting/group-based-sharing/phase-1-resource-sharing/SPEC.md
  • mfbt feature UNS-612.

Related Issues or PRs

  • Cloud counterpart: Zipstack/unstract-cloud#1508 (AgenticProject parallel-copy + auth0) — must merge at or before this PR or agentic group sharing breaks in cloud.
  • Phase 2 (co-owner integration, UN-2022) is a separate follow-up.

Dependencies Versions

  • None.

Notes on Testing

Manual verification per spec §7: create a group, add/remove members, share each resource type with the group, confirm group-only members gain view/run access and lose it on removal, and confirm the quota limits return HTTP 400.

Screenshots

Checklist

I have read and understood the Contribution Guidelines.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 22, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

<review_stack_artifact>

</review_stack_artifact>

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch UN-2977-Moody-s-Introduce-Group-Based-Project-Sharing

@kirtimanmishrazipstack kirtimanmishrazipstack marked this pull request as ready for review May 25, 2026 15:35
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 25, 2026

Greptile Summary

This PR introduces org-scoped groups as a first-class sharing target across all seven shareable resource types (Workflow, Pipeline, API Deployment, Connector, Adapter, Prompt Studio, and Agentic project). Groups are stored via a single polymorphic ResourceGroupShare table rather than per-resource M2M columns, and sharing mutations are consolidated into a new POST /<resource>/{id}/share/ action gated by ShareAuthorizationService.

  • Data model: Three new migrations add OrganizationGroup, GroupMembership (explicit through-model), and ResourceGroupShare with correct indexes and unique constraints; for_user() on each resource manager unions group-shared IDs via a Python-materialised list to work around a Postgres uuid = varchar implicit-cast limitation.
  • Access control: has_group_access() is wired into both object-permission classes; a post_delete signal on OrganizationMember purges group memberships and direct shared_users rows atomically on org-member removal to close the rejoin backdoor.
  • Frontend: A new Groups admin page (CRUD + member management + delete-confirm with blast-radius count) and an updated SharePermission modal that sends one batched POST /share/ on Apply.

Confidence Score: 4/5

Safe to merge after addressing the missing AgenticProject entry in the blast-radius resources endpoint, which silently shows an incomplete resource count in the group-delete confirm dialog for cloud deployments.

The core sharing mechanics — polymorphic ResourceGroupShare table, ShareAuthorizationService auth matrix, for_user() union queries, and the post_delete cleanup signal — are all correctly implemented. The one functional gap is that _collect_resources_shared_with_group in group_views.py omits AgenticProject, so the admin delete-confirm dialog will undercount affected resources in cloud deployments that have agentic projects shared with the group being deleted.

backend/tenant_account_v2/group_views.py (_collect_resources_shared_with_group — missing AgenticProject entry)

Important Files Changed

Filename Overview
backend/tenant_account_v2/group_views.py New GroupViewSet with CRUD and member management; AgenticProject omitted from blast-radius resources endpoint; members GET is open to all org members (inconsistent with the admin-gated resources endpoint).
backend/tenant_account_v2/sharing_helpers.py New central module for group-based resource sharing logic; well-structured with proper UUID casting workaround, correct transaction atomicity, and a clean ShareAuthorizationService.
backend/tenant_account_v2/group_serializers.py Serializers for group CRUD and member management; quota validation has a TOCTOU race that can let concurrent creates slightly exceed MAX_GROUPS_PER_ORG/MAX_MEMBERS_PER_GROUP.
backend/tenant_account_v2/signals.py New signal wiring for org-member removal and resource-delete cleanup; correct cascade semantics and atomic transaction, but cleanup_user_org_access uses N+1 individual remove() calls.
backend/permissions/resource_share_views.py New ResourceShareManagementMixin providing share/ and effective-members/ actions; clean axis-agnostic design with proper input coercion and correct delegation to ShareAuthorizationService.
backend/permissions/permission.py Adds has_group_access() helper and wires it into IsOwnerOrSharedUser and IsOwnerOrSharedUserOrSharedToOrg; correct lazy import to avoid circular deps at startup.
backend/tenant_account_v2/migrations/0003_resource_group_share.py Migration for the polymorphic ResourceGroupShare table; correct indexes on (content_type, object_id) and (organization, group), unique constraint on (group, content_type, object_id).
backend/tenant_account_v2/migrations/0002_organization_group_group_membership.py Migration for OrganizationGroup and GroupMembership; correct unique constraints and composite index on (user, group) for the for_user() subquery path.

Sequence Diagram

sequenceDiagram
    participant Admin
    participant GroupViewSet
    participant ShareModal
    participant ResourceViewSet
    participant ShareAuthorizationService
    participant ResourceGroupShare
    participant ForUser as Resource.for_user()

    Admin->>GroupViewSet: POST /groups/ (create group)
    Admin->>GroupViewSet: "POST /groups/{id}/members/ (add users)"
    GroupViewSet->>GroupMembership: bulk_create memberships

    Admin->>ShareModal: Open share modal for a Workflow
    ShareModal->>ResourceViewSet: "GET /workflow/{id}/ (shared_groups property)"
    ShareModal->>GroupViewSet: GET /groups/ (list groups)

    Admin->>ShareModal: Apply (add group)
    ShareModal->>ResourceViewSet: "POST /workflow/{id}/share/ {shared_groups: [id]}"
    ResourceViewSet->>ShareAuthorizationService: authorize_and_commit(actor, resource, desired)
    ShareAuthorizationService->>ResourceGroupShare: upsert rows

    User->>ForUser: list workflows
    ForUser->>GroupMembership: get user_group_ids
    ForUser->>ResourceGroupShare: get group-shared workflow UUIDs
    ForUser-->>User: "union(owned | shared_users | shared_to_org | group_shared)"

    Admin->>GroupViewSet: "DELETE /groups/{id}/ (delete group)"
    GroupViewSet->>ResourceGroupShare: CASCADE delete all group shares
Loading

Fix All in Claude Code

Prompt To Fix All With AI
Fix the following 4 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 4
backend/tenant_account_v2/group_views.py:196-222
**`AgenticProject` omitted from blast-radius resource listing**

`_collect_resources_shared_with_group` enumerates six resource types but not `AgenticProject`, which is listed in `signals._SHAREABLE_MODELS` and therefore does receive `ResourceGroupShare` rows in cloud deployments. When an admin opens the delete-confirm dialog for a group that has agentic projects shared with it, the endpoint returns an incomplete list — the admin sees no agentic projects in the "affected resources" count and proceeds without realising those projects will silently lose group access on deletion. The data is correctly cascade-deleted (via `ResourceGroupShare → OrganizationGroup` FK), but the UI gives a misleading blast-radius estimate.

The `AgenticProject` import should be guarded with a `try/except LookupError` (mirroring the signal handler's pattern) and appended to `sources` when the app is installed.

### Issue 2 of 4
backend/tenant_account_v2/group_serializers.py:68-70
**Group quota check is a TOCTOU race — concurrent creates can exceed `MAX_GROUPS_PER_ORG`**

Two simultaneous POST requests can both read the same `current` count, both pass the `< MAX_GROUPS_PER_ORG` guard, and both commit, leaving the org with `current + 2` groups. The unique constraint on `(organization, name)` prevents duplicates with the same name but does not cap the total count. Wrapping the count-and-insert in a `select_for_update` on the organization row would make this atomic.

```suggestion
        if self.instance is None:
            from account_v2.models import Organization
            # Lock the organization row to make the count-and-create atomic.
            Organization.objects.filter(pk=organization.pk).select_for_update().get()
            current = OrganizationGroup.objects.filter(organization=organization).count()
            if current >= settings.MAX_GROUPS_PER_ORG:
```

### Issue 3 of 4
backend/tenant_account_v2/group_views.py:119-136
**`members` GET exposes group membership to any org member**

`IsOrgAdminForWrite.has_permission` returns `True` for all GET requests, so any authenticated org member can call `GET /groups/{pk}/members/` and enumerate the full membership list of any group in their org — including groups they are not in. The `resources` endpoint on the same viewset was explicitly gated to admins with the comment "Leaving it open to any org member would leak… resources shared with groups they are not in". The same reasoning applies here: a non-admin can discover that `alice@company.com` belongs to "Executive Board" or "Finance Team" without being a member of those groups.

If org-wide member visibility is intentional, a brief code comment stating that would prevent this from being revisited. If it should be admin-only, adding the same `if not _is_org_admin(request): raise PermissionDenied(...)` guard as in `resources` and `remove_member` would close the gap.

### Issue 4 of 4
backend/tenant_account_v2/signals.py:64-81
**N+1 `shared_users.remove()` calls in `cleanup_user_org_access`**

For each resource type the signal fetches every resource the departing user was shared with, then calls `resource.shared_users.remove(instance.user)` once per row — one `DELETE` statement per resource. If a user was shared with 50 workflows the signal issues 50 individual deletes. The same result can be achieved with a single bulk delete against the M2M through table: `model.shared_users.through.objects.filter(user=instance.user, <fk_to_model>__organization=instance.organization).delete()`. This runs entirely inside the existing `transaction.atomic()` block so rollback semantics are unchanged.

Reviews (9): Last reviewed commit: "Merge branch 'main' of github.com:Zipsta..." | Re-trigger Greptile

Comment thread backend/tenant_account_v2/group_views.py
Comment thread backend/adapter_processor_v2/views.py Outdated
Comment thread backend/tenant_account_v2/group_views.py
Comment thread backend/tenant_account_v2/group_views.py Outdated
Comment thread backend/tenant_account_v2/sharing_helpers.py
Comment thread backend/tenant_account_v2/sharing_helpers.py
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🧹 Nitpick comments (6)
backend/tenant_account_v2/views.py (1)

72-77: ⚡ Quick win

Log the traceback for 500-path exceptions.

Use logger.exception(...) (or exc_info=True) here so incident debugging has stack context.

Proposed fix
-    except Exception as error:
-        logger.error("Error while get User : %s", error)
+    except Exception:
+        logger.exception("Error while getting user organization")
         return Response(
             status=status.HTTP_500_INTERNAL_SERVER_ERROR,
             data={"message": "Internal Error"},
         )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/tenant_account_v2/views.py` around lines 72 - 77, Replace the plain
logger.error call in the exception handler in tenant_account_v2/views.py (the
except Exception as error block that currently logs "Error while get User : %s"
and returns a 500 Response) with a logging call that records the full traceback
(use logger.exception(...) or logger.error(..., exc_info=True)) so the stack
context is captured for the 500-path; keep the same Response payload and status
but ensure the logger call references the same message/context used currently.
frontend/src/components/groups/groups-service.js (1)

13-16: ⚡ Quick win

Rename this to a custom hook (useGroupsService) to match hook usage.

This function calls React hooks (useAxiosPrivate, useSessionStore) but is not named like a hook. Renaming to useGroupsService reduces invalid-call risk and aligns with hook rules across tooling.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/groups/groups-service.js` around lines 13 - 16, The
function groupsService calls React hooks (useAxiosPrivate and useSessionStore)
but isn't named as a hook; rename groupsService to useGroupsService and update
all imports/usages accordingly to follow hook naming conventions and avoid
invalid-hook warnings; ensure the internal logic (including the path
construction using sessionDetails?.orgId.replaceAll) and any exported symbol
names are updated to the new name so consumers import useGroupsService instead
of groupsService.
backend/tenant_account_v2/group_views.py (1)

127-128: 💤 Low value

Fragile access to permission class message.

Accessing self.permission_classes[1].message relies on the exact ordering of the permission_classes list. If the order changes or a class is added/removed, this will silently use the wrong message or raise an IndexError.

Consider using the class directly:

♻️ Proposed fix
-        if not _is_org_admin(request):
-            raise PermissionDenied(self.permission_classes[1].message)
+        if not _is_org_admin(request):
+            raise PermissionDenied(IsOrgAdminForWrite.message)

Apply the same fix at line 150.

Also applies to: 149-150

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/tenant_account_v2/group_views.py` around lines 127 - 128, The code
currently reads a permission message via an index
(self.permission_classes[1].message) which is fragile; replace that indexed
access with the explicit permission class's message (e.g.,
OrgAdminPermission.message or whatever concrete class is present in your
permission_classes) so the code reads raise
PermissionDenied(OrgAdminPermission.message) or the equivalent direct class
reference; update both occurrences (the check that calls _is_org_admin and the
later occurrence at the other spot) and import or reference the actual
permission class name instead of relying on list ordering.
frontend/src/components/groups/GroupMemberManager.jsx (1)

155-159: ⚡ Quick win

onClose prop is optional but required for modal dismissal.

The onClose prop is marked as optional in PropTypes (no .isRequired), but it's passed directly to Modal.onCancel. While this won't crash (Ant Design handles undefined), the modal can't be closed without it.

♻️ Mark onClose as required
 GroupMemberManager.propTypes = {
   open: PropTypes.bool.isRequired,
   group: PropTypes.object,
-  onClose: PropTypes.func,
+  onClose: PropTypes.func.isRequired,
 };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/groups/GroupMemberManager.jsx` around lines 155 -
159, The PropTypes declaration for GroupMemberManager marks onClose optional but
the component passes it to Modal.onCancel; update GroupMemberManager.propTypes
so onClose is required (change the onClose entry to PropTypes.func.isRequired)
to ensure consumers provide a dismissal handler for the Modal.onCancel; keep
open and group prop definitions unchanged.
frontend/src/hooks/useShareModal.js (1)

40-48: 💤 Low value

Handle potential undefined groupsResponse more explicitly.

When groupsApi is not provided, groupsResponse will be undefined. The code at lines 78-86 handles this with the ?.data check, but destructuring from Promise.all with a variable array length could be clearer.

♻️ Consider explicit handling
-      const [usersResponse, sharedUsersResponse, groupsResponse] =
-        await Promise.all(calls);
+      const results = await Promise.all(calls);
+      const [usersResponse, sharedUsersResponse] = results;
+      const groupsResponse = groupsApi?.listGroups ? results[2] : undefined;

Also applies to: 78-86

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/hooks/useShareModal.js` around lines 40 - 48, The destructuring
from Promise.all in useShareModal assumes three results but calls may only
contain two when groupsApi is absent; fix by ensuring calls always has the third
promise (e.g., if groupsApi?.listGroups is falsy push Promise.resolve({ data: []
}) so the array length is constant) and then safely destructure into
usersResponse, sharedUsersResponse, groupsResponse; reference the calls array,
groupsApi.listGroups and the Promise.all destructuring in useShareModal to
implement this change.
frontend/src/components/custom-tools/list-of-tools/ListOfTools.jsx (1)

345-352: ⚡ Quick win

Modal closes even on share failure.

The setOpenSharePermissionModal(false) is in .finally(), so the modal closes even when sharing fails. This matches the useShareModal hook behavior (lines 118-124 in that file), but users won't see their failed changes. Consider keeping the modal open on error so users can retry.

♻️ Keep modal open on failure
     axiosPrivate(requestOptions)
+      .then(() => {
+        setOpenSharePermissionModal(false);
+      })
       .catch((err) => {
         setAlertDetails(handleException(err, "Failed to load"));
-      })
-      .finally(() => {
-        setOpenSharePermissionModal(false);
       });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/custom-tools/list-of-tools/ListOfTools.jsx` around
lines 345 - 352, The modal is always closed because
setOpenSharePermissionModal(false) is in the .finally() of the axiosPrivate
call; change control so the modal is only closed on success: remove
setOpenSharePermissionModal(false) from .finally(), and instead call it inside
the success path (the .then or after a successful axiosPrivate response) while
leaving it out of the .catch so the modal remains open on errors; keep the
existing error handling (setAlertDetails(handleException(err, "Failed to
load"))) in the catch so users see the failure and can retry (refer to
axiosPrivate, setAlertDetails, handleException, and setOpenSharePermissionModal
in ListOfTools.jsx and the useShareModal behavior).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/adapter_processor_v2/serializers.py`:
- Around line 29-41: The view currently reads request.data["shared_users"] in
AdapterInstanceViewSet.partial_update even though BaseAdapterSerializer marks
shared_users as read_only, causing side-effects from ignored payloads; update
partial_update to only run the "clear UserDefaultAdapter links" logic when the
incoming payload actually intends to change shared_users — i.e., check that
"shared_users" is present in request.data AND that the serializer will accept it
(for example ensure serializer.fields["shared_users"].read_only is False or that
"shared_users" appears in serializer.initial_data/validated_data) before
performing the clear/unshare operations, leaving existing behavior unchanged
otherwise.

In `@backend/adapter_processor_v2/views.py`:
- Around line 364-368: The code in partial_update reads request_data and
constructs requested_user_ids with int(...) before serializer validation, which
can raise ValueError for non-integer inputs; update the logic in the
partial_update handler (where requested_user_ids, current_shared_users,
removed_user_ids are computed) to defensively validate shared_users from
request_data: parse/iterate the list and catch non-integer values, and if any
invalid entries exist raise a serializers.ValidationError (or return a 400
response) with a clear message instead of allowing a ValueError to bubble up;
ensure you reference request_data.get("shared_users", []) and build
requested_user_ids only after validating/converting each entry.

In `@backend/tenant_account_v2/share_serializer_mixin.py`:
- Around line 32-44: The create and update flows call
super().create/super().update and then set_resource_share_groups separately,
risking partial writes; wrap the call to super().create (in create) and
super().update (in update) together with the subsequent
set_resource_share_groups call inside a single DB transaction (e.g., using
Django's transaction.atomic()) so both the model save and the group-sharing
update commit or roll back together; modify the methods (create and update in
ShareSerializerMixin) to perform the create/update and the
set_resource_share_groups(instance, [g.id for g in groups]) call within the same
atomic block.

In `@backend/workflow_manager/workflow_v2/urls/workflow.py`:
- Around line 29-30: The new POST share endpoint is currently allowed by the
broader IsOwnerOrSharedUserOrSharedToOrg path; update
WorkflowViewSet.get_permissions so that "share" is included in the same action
set as "destroy", "update", and "partial_update" (i.e., make "share" use the
owner-only permission class returned for those actions) to ensure only owners
can re-share workflows.

In `@frontend/src/components/groups/GroupCreateEditModal.jsx`:
- Around line 33-36: The submit logic currently dereferences group.id unguarded
when building call (mode === "edit" ? service.updateGroup(group.id, values) :
service.createGroup(values)), which can throw if props.group is undefined;
update the handler (the code that sets call, and similar logic around lines
handling update in the submit flow) to first assert or guard that group exists
in edit mode (e.g., if mode === "edit" and !group, abort/throw/return early or
use group?.id and surface an explicit error), and use
service.updateGroup(group.id, values) only after confirming group is defined;
apply the same guard to the similar edit-path at the other location referenced
(around the 82-86 edit logic).

In `@frontend/src/components/groups/groups-service.js`:
- Around line 16-17: The code builds path using
sessionDetails?.orgId.replaceAll(...) which will throw if orgId is
null/undefined; update the expression that computes path so you guard orgId
before calling replaceAll—use optional chaining on orgId
(sessionDetails?.orgId?.replaceAll(...)) or coerce to a safe default (e.g.,
(sessionDetails?.orgId || "").replaceAll(...)) and keep csrfToken usage
unchanged; modify the expression that defines path to reference sessionDetails
and orgId safely.

In `@frontend/src/components/groups/Groups.jsx`:
- Around line 195-200: TopBar's built-in search assumes user rows with fields
like username/email, causing a crash when Groups.jsx passes group rows; update
the wiring so TopBar searches group fields instead: add a new prop to TopBar
(e.g., searchKey or searchFields) and change TopBar.jsx’s search logic
(currently using username.includes(...)/email) to iterate over the provided
key(s) and test those string fields, then in Groups.jsx pass searchKey="name" or
searchFields={['name','description','member_count']} along with the existing
searchData and setFilteredUserList so the filter operates on valid group fields
instead of username/email.

In `@frontend/src/components/navigations/side-nav-bar/SideNavBar.jsx`:
- Around line 126-134: The Platform main nav item in the SideNavBar component
doesn't remain active on the new Groups page because the Platform "active"
condition doesn't include the groups path; update the Platform active-state
check (the expression that computes whether the Platform item is active) to also
consider the Groups route by adding `/${orgName}/groups` (or checking pathname
includes `/groups`) to the boolean expression that determines active, and apply
the same change to the duplicate active-check occurrence mentioned (the earlier
block around lines 162-164) so both places treat the Groups route as part of
Platform.

In `@frontend/src/components/tool-settings/tool-settings/ToolSettings.jsx`:
- Around line 213-215: The code currently closes the share modal in a .finally
block which hides it on both success and failure; remove
setOpenSharePermissionModal(false) from the .finally and instead call
setOpenSharePermissionModal(false) only in the successful resolution handler
(the .then callback of the share request promise), leaving the modal open on
errors so users can retry; ensure the .catch still surfaces the error (e.g.,
error toast/log) but does not close the modal.

---

Nitpick comments:
In `@backend/tenant_account_v2/group_views.py`:
- Around line 127-128: The code currently reads a permission message via an
index (self.permission_classes[1].message) which is fragile; replace that
indexed access with the explicit permission class's message (e.g.,
OrgAdminPermission.message or whatever concrete class is present in your
permission_classes) so the code reads raise
PermissionDenied(OrgAdminPermission.message) or the equivalent direct class
reference; update both occurrences (the check that calls _is_org_admin and the
later occurrence at the other spot) and import or reference the actual
permission class name instead of relying on list ordering.

In `@backend/tenant_account_v2/views.py`:
- Around line 72-77: Replace the plain logger.error call in the exception
handler in tenant_account_v2/views.py (the except Exception as error block that
currently logs "Error while get User : %s" and returns a 500 Response) with a
logging call that records the full traceback (use logger.exception(...) or
logger.error(..., exc_info=True)) so the stack context is captured for the
500-path; keep the same Response payload and status but ensure the logger call
references the same message/context used currently.

In `@frontend/src/components/custom-tools/list-of-tools/ListOfTools.jsx`:
- Around line 345-352: The modal is always closed because
setOpenSharePermissionModal(false) is in the .finally() of the axiosPrivate
call; change control so the modal is only closed on success: remove
setOpenSharePermissionModal(false) from .finally(), and instead call it inside
the success path (the .then or after a successful axiosPrivate response) while
leaving it out of the .catch so the modal remains open on errors; keep the
existing error handling (setAlertDetails(handleException(err, "Failed to
load"))) in the catch so users see the failure and can retry (refer to
axiosPrivate, setAlertDetails, handleException, and setOpenSharePermissionModal
in ListOfTools.jsx and the useShareModal behavior).

In `@frontend/src/components/groups/GroupMemberManager.jsx`:
- Around line 155-159: The PropTypes declaration for GroupMemberManager marks
onClose optional but the component passes it to Modal.onCancel; update
GroupMemberManager.propTypes so onClose is required (change the onClose entry to
PropTypes.func.isRequired) to ensure consumers provide a dismissal handler for
the Modal.onCancel; keep open and group prop definitions unchanged.

In `@frontend/src/components/groups/groups-service.js`:
- Around line 13-16: The function groupsService calls React hooks
(useAxiosPrivate and useSessionStore) but isn't named as a hook; rename
groupsService to useGroupsService and update all imports/usages accordingly to
follow hook naming conventions and avoid invalid-hook warnings; ensure the
internal logic (including the path construction using
sessionDetails?.orgId.replaceAll) and any exported symbol names are updated to
the new name so consumers import useGroupsService instead of groupsService.

In `@frontend/src/hooks/useShareModal.js`:
- Around line 40-48: The destructuring from Promise.all in useShareModal assumes
three results but calls may only contain two when groupsApi is absent; fix by
ensuring calls always has the third promise (e.g., if groupsApi?.listGroups is
falsy push Promise.resolve({ data: [] }) so the array length is constant) and
then safely destructure into usersResponse, sharedUsersResponse, groupsResponse;
reference the calls array, groupsApi.listGroups and the Promise.all
destructuring in useShareModal to implement this change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ab651623-939b-44d0-b5eb-5da241ab7926

📥 Commits

Reviewing files that changed from the base of the PR and between fccb44b and 2cdd2a6.

⛔ Files ignored due to path filters (1)
  • frontend/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (64)
  • backend/adapter_processor_v2/models.py
  • backend/adapter_processor_v2/serializers.py
  • backend/adapter_processor_v2/urls.py
  • backend/adapter_processor_v2/views.py
  • backend/api_v2/api_deployment_views.py
  • backend/api_v2/models.py
  • backend/api_v2/serializers.py
  • backend/api_v2/urls.py
  • backend/backend/settings/base.py
  • backend/connector_v2/models.py
  • backend/connector_v2/serializers.py
  • backend/connector_v2/urls.py
  • backend/connector_v2/views.py
  • backend/permissions/permission.py
  • backend/permissions/resource_share_views.py
  • backend/pipeline_v2/models.py
  • backend/pipeline_v2/serializers/crud.py
  • backend/pipeline_v2/serializers/sharing.py
  • backend/pipeline_v2/urls.py
  • backend/pipeline_v2/views.py
  • backend/prompt_studio/permission.py
  • backend/prompt_studio/prompt_studio_core_v2/models.py
  • backend/prompt_studio/prompt_studio_core_v2/serializers.py
  • backend/prompt_studio/prompt_studio_core_v2/urls.py
  • backend/prompt_studio/prompt_studio_core_v2/views.py
  • backend/sample.env
  • backend/tenant_account_v2/admin.py
  • backend/tenant_account_v2/apps.py
  • backend/tenant_account_v2/group_serializers.py
  • backend/tenant_account_v2/group_views.py
  • backend/tenant_account_v2/groups_urls.py
  • backend/tenant_account_v2/migrations/0002_organization_group_group_membership.py
  • backend/tenant_account_v2/migrations/0003_resource_group_share.py
  • backend/tenant_account_v2/models.py
  • backend/tenant_account_v2/share_serializer_mixin.py
  • backend/tenant_account_v2/sharing_helpers.py
  • backend/tenant_account_v2/signals.py
  • backend/tenant_account_v2/urls.py
  • backend/tenant_account_v2/views.py
  • backend/workflow_manager/workflow_v2/models/workflow.py
  • backend/workflow_manager/workflow_v2/serializers.py
  • backend/workflow_manager/workflow_v2/urls/workflow.py
  • backend/workflow_manager/workflow_v2/views.py
  • frontend/src/components/custom-tools/list-of-tools/ListOfTools.jsx
  • frontend/src/components/deployments/api-deployment/ApiDeployment.jsx
  • frontend/src/components/deployments/api-deployment/api-deployments-service.js
  • frontend/src/components/groups/GroupCreateEditModal.jsx
  • frontend/src/components/groups/GroupMemberManager.jsx
  • frontend/src/components/groups/Groups.css
  • frontend/src/components/groups/Groups.jsx
  • frontend/src/components/groups/groups-service.js
  • frontend/src/components/navigations/side-nav-bar/SideNavBar.jsx
  • frontend/src/components/pipelines-or-deployments/pipeline-service.js
  • frontend/src/components/pipelines-or-deployments/pipelines/Pipelines.jsx
  • frontend/src/components/tool-settings/tool-settings/ToolSettings.jsx
  • frontend/src/components/widgets/share-permission/SharePermission.css
  • frontend/src/components/widgets/share-permission/SharePermission.jsx
  • frontend/src/components/workflows/workflow/Workflows.jsx
  • frontend/src/components/workflows/workflow/workflow-service.js
  • frontend/src/hooks/useShareModal.js
  • frontend/src/index.jsx
  • frontend/src/pages/ConnectorsPage.jsx
  • frontend/src/pages/GroupsPage.jsx
  • frontend/src/routes/useMainAppRoutes.js

Comment thread backend/adapter_processor_v2/serializers.py Outdated
Comment thread backend/adapter_processor_v2/views.py Outdated
Comment thread backend/tenant_account_v2/share_serializer_mixin.py Outdated
Comment thread backend/workflow_manager/workflow_v2/urls/workflow.py
Comment thread frontend/src/components/groups/GroupCreateEditModal.jsx
Comment thread frontend/src/components/groups/groups-service.js Outdated
Comment thread frontend/src/components/groups/Groups.jsx
Comment thread frontend/src/components/navigations/side-nav-bar/SideNavBar.jsx
Comment thread frontend/src/components/tool-settings/tool-settings/ToolSettings.jsx Outdated
Comment thread backend/tenant_account_v2/sharing_helpers.py Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
backend/permissions/permission.py (1)

27-44: ⚡ Quick win

Defensive guard for unauthenticated callers in has_group_access(...)

CustomAuthMiddleware returns 401 for unauthenticated requests on non-whitelisted/non-internal routes, so these permission checks should not normally run with request.user=AnonymousUser. Still, has_group_access(...) directly reads user.group_memberships without an auth check, so adding an early return prevents brittle 500s if call sites/middleware behavior change later.

💡 Proposed fix
 def has_group_access(user: Any, obj: Any) -> bool:
     """Check if a user has access to a resource via group membership.
 
     Reads from the polymorphic ``ResourceGroupShare`` table rather than a
     per-resource ``shared_groups`` M2M (see UN-2977). Callers can OR this
     in safely for any resource — non-shareable objects yield no rows.
     """
+    if not getattr(user, "is_authenticated", False):
+        return False
+
     # Lazy import — ``permissions`` is imported by `account_v2`/`api_v2`
     # before `tenant_account_v2` finishes loading.
     from django.contrib.contenttypes.models import ContentType
     from tenant_account_v2.models import ResourceGroupShare
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/permissions/permission.py` around lines 27 - 44, has_group_access
currently assumes user.group_memberships exists and will 500 for
unauthenticated/None users; add a defensive early return (False) at the top of
has_group_access to handle unauthenticated or missing-user cases by checking
getattr(user, "is_authenticated", False) (or user is None) and/or absence of
group_memberships before accessing user.group_memberships; keep the rest of the
logic using ContentType.objects.get_for_model(type(obj)) and ResourceGroupShare
as-is.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@backend/permissions/permission.py`:
- Around line 27-44: has_group_access currently assumes user.group_memberships
exists and will 500 for unauthenticated/None users; add a defensive early return
(False) at the top of has_group_access to handle unauthenticated or missing-user
cases by checking getattr(user, "is_authenticated", False) (or user is None)
and/or absence of group_memberships before accessing user.group_memberships;
keep the rest of the logic using ContentType.objects.get_for_model(type(obj))
and ResourceGroupShare as-is.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 492cc0b3-3de5-458d-8a27-0939db2156b0

📥 Commits

Reviewing files that changed from the base of the PR and between 2cdd2a6 and e552fb1.

📒 Files selected for processing (7)
  • backend/adapter_processor_v2/models.py
  • backend/api_v2/models.py
  • backend/connector_v2/models.py
  • backend/permissions/permission.py
  • backend/pipeline_v2/models.py
  • backend/prompt_studio/prompt_studio_core_v2/models.py
  • backend/workflow_manager/workflow_v2/models/workflow.py

- group list: validate ?member= as int (400 not 500)
- group views: reference IsOrgAdminForWrite.message, not list index
- sharing_helpers: cast UUID ids in list_resources_shared_with_group
  (fixes GET /groups/{pk}/resources/ crash); treat org-less user as
  non-admin without error-logging noise
- share serializer mixin: make model save + group-share write atomic
- adapter: move default-adapter cleanup to the share action where
  unsharing actually happens (shared_users is read-only on PATCH)
- frontend: guard group id on edit, orgId before replaceAll, TopBar
  search key for non-user rows, /groups nav active state, keep share
  modal open on failure

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread backend/permissions/resource_share_views.py
Comment thread backend/tenant_account_v2/group_views.py Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
backend/adapter_processor_v2/views.py (1)

151-157: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Restrict share action to owners/admin-authorized roles only.

Line 154 puts share behind IsOwnerOrSharedUserOrSharedToOrg, which allows already-shared users to mutate sharing state. That is an ACL escalation path for non-owners. Keep share under owner-only permissions.

Suggested patch
         if self.action in [
             "list_of_shared_users",
             "adapter_info",
-            "share",
             "effective_members",
         ]:
             return [IsOwnerOrSharedUserOrSharedToOrg()]
+        if self.action == "share":
+            return [IsOwner()]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/adapter_processor_v2/views.py` around lines 151 - 157, The current
permission branch bundles "share" with IsOwnerOrSharedUserOrSharedToOrg allowing
shared users to mutate sharing; remove "share" from that list and add a separate
conditional branch for when self.action == "share" that returns an
owner/admin-only permission (e.g., IsOwnerOrAdmin()) instead; update the
permission logic in the same method handling self.action so existing actions
("list_of_shared_users", "adapter_info", "effective_members") keep
IsOwnerOrSharedUserOrSharedToOrg() while "share" explicitly requires
IsOwnerOrAdmin().
backend/tenant_account_v2/sharing_helpers.py (2)

330-337: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep org-scope validation on the service-account path.

This early return skips _check_users_axis() and _check_groups_axis(), so a service account can write shared_users or shared_groups from another organization straight into the resource. Bypassing authorization here may be intentional; bypassing the org-boundary validation is not.

Suggested fix
         if getattr(actor, "is_service_account", False):
+            cls._authorize(
+                actor,
+                resource,
+                desired,
+                is_owner=True,
+                is_admin=True,
+            )
             cls._commit(resource, desired)
             return
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/tenant_account_v2/sharing_helpers.py` around lines 330 - 337, The
early-return for service accounts skips org-boundary validations; ensure the
users/groups axis checks still run for service accounts by invoking
cls._check_users_axis(resource, desired, actor) and
cls._check_groups_axis(resource, desired, actor) before returning on
getattr(actor, "is_service_account", False). In practice, move or add those
calls so service accounts still undergo org-scope validation, then proceed to
cls._commit(resource, desired) (and only skip cls._authorize for service
accounts if intended).

267-286: ⚠️ Potential issue | 🟠 Major

Fix missing org scoping in is_org_admin()

  • is_org_admin() calls AuthenticationController.get_organization_members_by_user(user=...), which returns OrganizationMemberService.get_user_by_id(id=user.id).
  • OrganizationMemberService.get_user_by_id() does OrganizationMember.objects.get(user=id) without filtering by the current organization (no UserContext / organization_id is used), even though OrganizationMember is unique per ("organization", "user").
  • For users with memberships in multiple orgs, this makes the admin-role check ambiguous and can incorrectly evaluate as non-admin. Scope the lookup by current org (e.g., via UserContext) or pass an explicit organization_id.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/tenant_account_v2/sharing_helpers.py` around lines 267 - 286,
is_org_admin currently calls
AuthenticationController.get_organization_members_by_user(user=...) which
ultimately uses OrganizationMemberService.get_user_by_id(user.id) without
scoping to the current organization; for users in multiple orgs this yields
ambiguous/non-admin results. Update is_org_admin to resolve the current
organization (e.g., read organization_id from UserContext or accept/pass an
explicit organization_id) and call the auth controller with that scope (e.g.,
get_organization_members_by_user(user=..., organization_id=...)) or use a method
that filters OrganizationMember by both organization and user; keep the lazy
import of AuthenticationController and preserve the existing try/except behavior
and return False on lookup failures.
backend/tenant_account_v2/group_views.py (1)

150-158: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate user_id before deleting the membership.

list() already normalizes member to an integer, but this path accepts any token from the URL and forwards it straight to Line 156. DELETE /members/foo/ should fail with a deterministic 400 instead of reaching the delete query unvalidated.

Suggested fix
 def remove_member(
     self, request: Request, pk: str | None = None, user_id: str | None = None
 ) -> Response:
     if not _is_org_admin(request):
         raise PermissionDenied(IsOrgAdminForWrite.message)
+    try:
+        member_user_id = int(user_id) if user_id is not None else None
+    except (TypeError, ValueError) as exc:
+        raise ValidationError({"user_id": "Must be a numeric user ID."}) from exc
     group = self._get_group_or_404(pk)
-    deleted, _ = group.memberships.filter(user_id=user_id).delete()
+    deleted, _ = group.memberships.filter(user_id=member_user_id).delete()
     if not deleted:
         raise NotFound("User is not a member of this group.")
     return Response(status=status.HTTP_204_NO_CONTENT)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/tenant_account_v2/group_views.py` around lines 150 - 158, In
remove_member, validate the user_id URL token before calling
group.memberships.filter(...). Convert/normalize user_id to the expected integer
type (e.g., via int(...) or using the same normalization as list()) and catch
conversion errors; if conversion fails, raise a 400 Bad Request (or DRF
ParseError/ValidationError) with a clear message like "Invalid user_id"; only
proceed to the delete call when the user_id is valid to avoid passing arbitrary
tokens to the query.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontend/src/components/widgets/top-bar/TopBar.jsx`:
- Around line 13-14: TopBar currently always renders the hard-coded placeholder
"Search Users" (even when searchKey ≠ "email"); add a new prop searchPlaceholder
to the TopBar component signature with a sensible default (e.g., "Search Users")
and use that prop where the input placeholder is rendered so callers can pass a
custom placeholder for groups or other flows; update the prop destructuring
(include searchPlaceholder) and the JSX input placeholder reference to use
searchPlaceholder instead of the static string.

---

Outside diff comments:
In `@backend/adapter_processor_v2/views.py`:
- Around line 151-157: The current permission branch bundles "share" with
IsOwnerOrSharedUserOrSharedToOrg allowing shared users to mutate sharing; remove
"share" from that list and add a separate conditional branch for when
self.action == "share" that returns an owner/admin-only permission (e.g.,
IsOwnerOrAdmin()) instead; update the permission logic in the same method
handling self.action so existing actions ("list_of_shared_users",
"adapter_info", "effective_members") keep IsOwnerOrSharedUserOrSharedToOrg()
while "share" explicitly requires IsOwnerOrAdmin().

In `@backend/tenant_account_v2/group_views.py`:
- Around line 150-158: In remove_member, validate the user_id URL token before
calling group.memberships.filter(...). Convert/normalize user_id to the expected
integer type (e.g., via int(...) or using the same normalization as list()) and
catch conversion errors; if conversion fails, raise a 400 Bad Request (or DRF
ParseError/ValidationError) with a clear message like "Invalid user_id"; only
proceed to the delete call when the user_id is valid to avoid passing arbitrary
tokens to the query.

In `@backend/tenant_account_v2/sharing_helpers.py`:
- Around line 330-337: The early-return for service accounts skips org-boundary
validations; ensure the users/groups axis checks still run for service accounts
by invoking cls._check_users_axis(resource, desired, actor) and
cls._check_groups_axis(resource, desired, actor) before returning on
getattr(actor, "is_service_account", False). In practice, move or add those
calls so service accounts still undergo org-scope validation, then proceed to
cls._commit(resource, desired) (and only skip cls._authorize for service
accounts if intended).
- Around line 267-286: is_org_admin currently calls
AuthenticationController.get_organization_members_by_user(user=...) which
ultimately uses OrganizationMemberService.get_user_by_id(user.id) without
scoping to the current organization; for users in multiple orgs this yields
ambiguous/non-admin results. Update is_org_admin to resolve the current
organization (e.g., read organization_id from UserContext or accept/pass an
explicit organization_id) and call the auth controller with that scope (e.g.,
get_organization_members_by_user(user=..., organization_id=...)) or use a method
that filters OrganizationMember by both organization and user; keep the lazy
import of AuthenticationController and preserve the existing try/except behavior
and return False on lookup failures.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 76c5a7de-7df4-4781-ad2b-8216a73c37ae

📥 Commits

Reviewing files that changed from the base of the PR and between e552fb1 and 863c736.

📒 Files selected for processing (10)
  • backend/adapter_processor_v2/views.py
  • backend/tenant_account_v2/group_views.py
  • backend/tenant_account_v2/share_serializer_mixin.py
  • backend/tenant_account_v2/sharing_helpers.py
  • frontend/src/components/groups/GroupCreateEditModal.jsx
  • frontend/src/components/groups/Groups.jsx
  • frontend/src/components/groups/groups-service.js
  • frontend/src/components/navigations/side-nav-bar/SideNavBar.jsx
  • frontend/src/components/tool-settings/tool-settings/ToolSettings.jsx
  • frontend/src/components/widgets/top-bar/TopBar.jsx

Comment thread frontend/src/components/widgets/top-bar/TopBar.jsx
…aceholder

- group_views.remove_member: guard non-numeric user_id (400 not 500), mirroring the list action
- TopBar: searchPlaceholder prop (default "Search Users"); Groups passes "Search Groups"

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Comment thread backend/tenant_account_v2/group_views.py
kirtimanmishrazipstack and others added 2 commits May 26, 2026 10:53
The /groups/{pk}/resources/ action is the delete blast-radius view used
only by the admin delete-confirm flow, but IsOrgAdminForWrite permits GET
for any authenticated org member. That let non-members enumerate the
names/UUIDs of resources shared with groups they are not in, contradicting
the model where org admin has no implicit resource access. Gate the action
to org admins, mirroring the existing members POST / remove_member guards.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Group sharing is manual-only after the 2026-05-25 reversal; there is no
IdP group sync. Remove the dead IDP_GROUP_SYNC_INTERVAL_MIN var and the
"LOCAL + IDP combined" wording so sample.env stops documenting a feature
that no longer exists.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor Author

@kirtimanmishrazipstack kirtimanmishrazipstack left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comprehensive review — UN-2977 group-based sharing

Reviewed alongside the cloud counterpart (#1508) since they share the sharing abstractions. Overall this is a careful, well-structured change — multi-tenant isolation on the suspect paths (the varchar→UUID cast, has_group_access, the org-scoped managers) holds up, and the share-authorization matrix is coherent. Findings below are real and currently present in the code.

🔴 Needs a decision before merge

1. Org-admin implicit access: the code does the opposite of what this PR's description says.

The description states (three times): "being an org admin grants no implicit access… Org admin implicit access is explicitly denied — Enforced in for_user() and the permission classes — an admin who is not owner/shared cannot view or run a resource."

The actual code grants admins full access on both axes:

  • for_user() in all 6 managers short-circuits: if OrganizationMemberService.is_user_organization_admin(user): return self.all()pipeline_v2/models.py:35, api_v2/models.py:42, workflow_manager/workflow_v2/models/workflow.py:33, connector_v2/models.py:34, adapter_processor_v2/models.py:41, prompt_studio/prompt_studio_core_v2/models.py:30.
  • Every permission class ORs _is_organization_admin(request)permissions/permission.py:79 and :113 — and IsOwner's docstring affirms "Org admins can manage every resource in their organization."

One of the two is wrong. Either the code is correct and the description (incl. the "Can this PR break" section) is materially stale, or the description reflects the intended invariant and there's a least-privilege/multi-tenancy gap across all 6 resources + the permission layer. Please reconcile against the design source of truth (mfbt UNS-612) and align the description and/or the code. This also drives the cloud parity fix in #1508.

🟠 Important

2. member_count is always 1 when the group list is filtered by member.
tenant_account_v2/group_serializers.py:175-180 applies .filter(memberships__user=user) before .annotate(Count("memberships")). Filtering and aggregating on the same multi-valued relation makes Count run over the constrained join → member_count = 1 for every group in both ?member=me (non-admin "my groups") and ?member=<id> (admin filter). The unfiltered list path is correct. Fix: compute the count via a decoupled Subquery so the member filter can't taint it.

3. SharedGroupsSerializerMixin is dead code, and its docstring advertises a writable API.
shared_groups is read_only=True on all 7 resource serializers, so DRF strips it from validated_data and the mixin's create/update bodies (tenant_account_v2/share_serializer_mixin.py:34-50) always no-op — every group write actually goes through ShareAuthorizationService._commit. The module docstring (:12-19) even shows a writable PrimaryKeyRelatedField(queryset=…) example that contradicts every call site. Delete the mixin (and drop it from the serializers), or make it actually fire. The dead path also skips the org-scope check, so it's a latent footgun if reactivated.

4. No orphan cleanup of ResourceGroupShare when a resource is deleted.
object_id is a plain varchar (no FK/GFK), and tenant_account_v2/signals.py only purges on OrganizationMember removal. Deleting a Workflow/Pipeline/Connector/etc. leaves dangling share rows indefinitely. Add a post_delete receiver for the shareable models mirroring the existing lazy-model loop in signals.py (it already no-ops cleanly for apps that aren't installed, so it can include the cloud AgenticProject).

5. The post_delete purge signal isn't atomic and has no per-step error handling.
signals.py:cleanup_user_org_access bulk-deletes memberships then loops resource.shared_users.remove(...) across the shareable models with no transaction.atomic() and no try/except. A mid-loop failure leaves the user purged from some resources but not others — re-opening the "rejoin backdoor" the docstring claims to close — with no log of the partial purge. Wrap the cleanup in atomic() and log-and-reraise per model.

6. Share modal closes on failure inconsistently across resources.
frontend/src/hooks/useShareModal.js:119-124, ListOfTools.jsx, and Workflows.jsx close the modal in .finally() (success or failure), discarding the user's selection on a 400/403 with only a transient toast; ToolSettings.jsx correctly closes only on success. For partial-authorization rejections the user needs to see what was rejected. Close on .then only, keep open on .catch, and make all resources consistent.

🟡 Suggestions

  • Defensive UUID cast (latent): tenant_account_v2/sharing_helpers.py:137 and :160 do uuid.UUID(s) with only an empty-string guard. The write path stores valid PKs today, but since the column is deliberately varchar, one malformed object_id would 500 the entire resource list for every member of that group. Skip-and-log malformed IDs instead.
  • Stringly-typed share axes (project anti-pattern): the same axis vocabulary is declared three times as bare strings (sharing_helpers.py:306-308, permissions/resource_share_views.py:19, mixin share_axes); group_views.py:204-209 and the access_via literals in compute_effective_members (sharing_helpers.py:192,211,242) duplicate constants that exist elsewhere. A ShareAxis enum + DesiredShareState/AccessVia would collapse these and double as the content_type allowlist the model currently lacks. (AxisDiff already exists, but ShareAuthorizationService re-derives tuples instead of consuming it.)
  • Unbounded queryset: compute_effective_members._add_org_members iterates every org member with no pagination on a synchronous GET (effective-members/). Cap/paginate or .iterator().
  • Adapter default-cleanup runs outside the share transaction (adapter_processor_v2/views.py:264-282) — best-effort cleanup with no try/except after an already-committed share. Fold into the atomic block, or log on failure.
  • Comment accuracy: the resource_share_views.py "axis-agnostic… any number of M2M axes" claim is inaccurate (shared_groups is not M2M); the duplicated - Service accounts see all org resources bullet added above the existing combined line in all 6 for_user docstrings is redundant; the cleanup_user_org_access "closes the rejoin backdoor" wording overstates the (currently non-atomic) guarantee.

✅ Well-handled

Polymorphic single-table design avoids per-resource migration sprawl; migration dependencies + reversibility are correct; quota checks return structured 400s; lazy % logging is consistent; org-scope is re-validated defense-in-depth inside ShareAuthorizationService; the merge-order coupling with #1508 is correctly documented.


🤖 Claude Code–assisted multi-agent review (code quality, silent-failure, type-design, comment accuracy); high-impact findings verified against the code.

kirtimanmishrazipstack and others added 2 commits May 26, 2026 13:45
…uards

Addresses self-review findings on PR #1986:
- member_count: count via a decoupled Subquery so the optional ?member
  filter on the same relation no longer collapses it to 1.
- Add a post_delete receiver purging ResourceGroupShare rows when a
  shareable resource is deleted (object_id is varchar, no FK/cascade).
- Wrap cleanup_user_org_access in transaction.atomic() with per-model
  error handling that logs and re-raises, so a partial purge can't
  re-open the rejoin backdoor.
- Skip-and-log malformed object_id UUIDs instead of 500ing the list.
- Error-handle the post-commit adapter default-cleanup save.
- Comment accuracy: shared_groups is polymorphic (not M2M); drop the
  duplicate service-account docstring bullet; clarify the mixin's
  writable vs read-only usage.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This PR had moved the share-modal close into .finally() (close on
success or failure), discarding the user's selection on a 400/403.
Revert useShareModal, ListOfTools and Workflows to close only on
success — restoring pre-PR behavior and matching ToolSettings — so a
rejected share keeps the modal open for the user to review and retry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@kirtimanmishrazipstack
Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review — worked through every item. Each was validated against the code and git blame before acting.

Fixed & pushed (2d8d098 backend, 1da7105 frontend):

  • Profile manager migration fix #2 member_count — now computed via a decoupled Subquery, so the ?member=me / ?member=<id> filter no longer collapses it to 1.
  • Basic workflow and adapter exception messages #4 orphan ResourceGroupShare — added a post_delete receiver (wired per _SHAREABLE_MODELS, idempotent via dispatch_uid) that purges share rows when a resource is deleted.
  • Add support for configuring and using OCR adapters #5 purge atomicity — cleanup_user_org_access now runs in transaction.atomic() with per-model try/except that logs and re-raises, so a partial purge rolls back instead of re-opening the rejoin backdoor.
  • Corrected alignment of the OnBoard page to accommodate various screen sizes #6 share modals — reverted to close-on-success (useShareModal, ListOfTools, Workflows), restoring pre-PR behavior and matching ToolSettings; a rejected share now keeps the modal open with the selection intact.
  • Suggestion A — malformed object_id UUIDs are skip-and-logged instead of 500ing the whole list.
  • Suggestion C — the post-commit adapter default-cleanup .save() is now wrapped in try/except + logger.exception.
  • Suggestion D — fixed the "M2M axes" wording (shared_groups is polymorphic), removed the duplicate "Service accounts see all org resources" docstring bullet, and clarified the mixin docstring.

Addressed by reply (no code change):

  • fix: Prompt studio index and run issues #1 (admin implicit access): the code is correct, not contradictory. The org-wide admin override comes from the already-merged UN-3479 (UN-3479 [FEAT] Org admin override on resources + ProfileManager sharing #1989) — it's context, not an addition, in this PR's diff, and main's for_user() docstrings already say "org admins see all org resources". The PR description + spec were the stale part; both are now updated to state admins retain org-wide access and group sharing is additive. Reverting admin access would undo a separately-shipped feature, so it's out of scope here.
  • connectors #3 (SharedGroupsSerializerMixin "dead code"): kept intentionally. It's inert for the 6 OSS resource serializers (read_only → writes route through the POST /share/ action), but the cloud AgenticProject serializer consumes it writable, so deleting it would break cloud group sharing. Tightened the module docstring to document both modes.
  • Suggestion B (effective-members/ pagination): deferred. The member set is bounded in practice (MAX_MEMBERS_PER_GROUP=500, org membership in the hundreds); will revisit pagination if orgs grow large enough to warrant it.

Comment thread backend/permissions/permission.py
All 7 resource serializers (6 OSS + cloud AgenticProject) declare
shared_groups as read_only, so DRF strips it from validated_data and the
mixin's create/update never wrote group shares — every group write flows
through ShareAuthorizationService._commit via the POST /share/ action.
The mixin was therefore dead code with a misleading docstring and a
latent org-scope footgun if a field were ever made writable without
re-adding validation. Delete it and unwire it from the 6 serializers;
the read_only shared_groups declarations stay.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@kirtimanmishrazipstack
Copy link
Copy Markdown
Contributor Author

Correction to my earlier reply on #3: I said the cloud AgenticProject serializer consumes SharedGroupsSerializerMixin writable, so deleting it would break cloud. That was wrong — it came from a stale local copy. The cloud PR (#1508) actually ships shared_groups = read_only=True too, so the mixin was dead across all 7 serializers (no writable consumer anywhere; every group write goes through the POST /share/ action).

So I deleted it rather than keeping it — commit befd61c9b removes the mixin file and unwires it from the 6 OSS serializers (read-only shared_groups declarations stay); the cloud side is unwired in unstract-cloud#1508. This also removes the latent org-scope footgun noted in the original review.

…y-s-Introduce-Group-Based-Project-Sharing

# Conflicts:
#	backend/prompt_studio/permission.py
@github-actions
Copy link
Copy Markdown
Contributor

Frontend Lint Report (Biome)

All checks passed! No linting or formatting issues found.

@github-actions
Copy link
Copy Markdown
Contributor

Test Results

Summary
  • Runner Tests: 11 passed, 0 failed (11 total)
  • SDK1 Tests: 347 passed, 0 failed (347 total)

Runner Tests - Full Report
filepath function $$\textcolor{#23d18b}{\tt{passed}}$$ SUBTOTAL
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_logs}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_cleanup}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_cleanup\_skip}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_client\_init}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_get\_image\_exists}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_get\_image}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_get\_container\_run\_config}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_get\_container\_run\_config\_without\_mount}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_run\_container}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_get\_image\_for\_sidecar}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_sidecar\_container}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{TOTAL}}$$ $$\textcolor{#23d18b}{\tt{11}}$$ $$\textcolor{#23d18b}{\tt{11}}$$
SDK1 Tests - Full Report
filepath function $$\textcolor{#23d18b}{\tt{passed}}$$ SUBTOTAL
$$\textcolor{#23d18b}{\tt{tests/file\_storage/test\_impl\_rm.py}}$$ $$\textcolor{#23d18b}{\tt{TestRmHappyPath.test\_bulk\_delete\_succeeds}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/file\_storage/test\_impl\_rm.py}}$$ $$\textcolor{#23d18b}{\tt{TestRmFallback.test\_missing\_md5\_triggers\_individual\_delete\_via\_rm\_file}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/file\_storage/test\_impl\_rm.py}}$$ $$\textcolor{#23d18b}{\tt{TestRmFallback.test\_fallback\_continues\_on\_per\_file\_error}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/file\_storage/test\_impl\_rm.py}}$$ $$\textcolor{#23d18b}{\tt{TestRmFallback.test\_fallback\_swallows\_rmdir\_error}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/file\_storage/test\_impl\_rm.py}}$$ $$\textcolor{#23d18b}{\tt{TestRmFallback.test\_non\_md5\_error\_propagates}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/file\_storage/test\_impl\_rm.py}}$$ $$\textcolor{#23d18b}{\tt{TestRmFallback.test\_md5\_error\_without\_recursive\_propagates}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/file\_storage/test\_impl\_rm.py}}$$ $$\textcolor{#23d18b}{\tt{TestFallbackDoesNotReenterBulkDelete.test\_only\_singular\_delete\_called}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/patches/test\_litellm\_cohere\_timeout.py}}$$ $$\textcolor{#23d18b}{\tt{TestPatchedEmbeddingSyncTimeoutForwarding.test\_timeout\_passed\_to\_client\_post}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/patches/test\_litellm\_cohere\_timeout.py}}$$ $$\textcolor{#23d18b}{\tt{TestPatchedEmbeddingSyncTimeoutForwarding.test\_none\_timeout\_passed\_to\_client\_post}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/patches/test\_litellm\_cohere\_timeout.py}}$$ $$\textcolor{#23d18b}{\tt{TestPatchedEmbeddingSyncTimeoutForwarding.test\_httpx\_timeout\_object\_forwarded}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/patches/test\_litellm\_cohere\_timeout.py}}$$ $$\textcolor{#23d18b}{\tt{TestMonkeyPatchApplied.test\_cohere\_handler\_patched}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/patches/test\_litellm\_cohere\_timeout.py}}$$ $$\textcolor{#23d18b}{\tt{TestMonkeyPatchApplied.test\_bedrock\_handler\_patched}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/patches/test\_litellm\_cohere\_timeout.py}}$$ $$\textcolor{#23d18b}{\tt{TestMonkeyPatchApplied.test\_patch\_module\_loaded\_via\_embedding\_import}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_bedrock\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_llm\_legacy\_no\_auth\_type\_keeps\_keys}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_bedrock\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_llm\_access\_keys\_mode\_keeps\_keys\_and\_strips\_auth\_type}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_bedrock\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_llm\_iam\_role\_mode\_drops\_keys\_even\_when\_present}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_bedrock\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_llm\_iam\_role\_mode\_with\_no\_keys}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_bedrock\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_llm\_access\_keys\_mode\_blank\_keys\_raises}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_bedrock\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_llm\_access\_keys\_mode\_whitespace\_keys\_raises}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_bedrock\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_llm\_unknown\_auth\_type\_raises}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_bedrock\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_llm\_unknown\_bearer\_token\_typo\_raises}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_bedrock\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_llm\_other\_params\_preserved\_through\_strip}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_bedrock\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_llm\_bearer\_token\_mode\_translates\_to\_api\_key}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_bedrock\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_llm\_bearer\_token\_mode\_drops\_stale\_access\_keys}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_bedrock\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_llm\_bearer\_token\_mode\_blank\_token\_raises}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_bedrock\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_llm\_bearer\_token\_mode\_whitespace\_token\_raises}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_bedrock\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_llm\_bearer\_token\_mode\_missing\_token\_raises}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_bedrock\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_llm\_bearer\_token\_strips\_surrounding\_whitespace}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_bedrock\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_llm\_bearer\_token\_survives\_revalidation}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_bedrock\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_llm\_iam\_role\_drops\_stale\_bearer\_token}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_bedrock\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_llm\_access\_keys\_drops\_stale\_bearer\_token}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_bedrock\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_llm\_legacy\_drops\_stray\_bearer\_token}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_bedrock\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_embedding\_legacy\_no\_auth\_type\_keeps\_keys}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_bedrock\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_embedding\_access\_keys\_mode\_keeps\_keys}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_bedrock\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_embedding\_iam\_role\_mode\_drops\_stale\_keys}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_bedrock\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_embedding\_iam\_role\_mode\_with\_no\_keys}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_bedrock\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_embedding\_access\_keys\_mode\_blank\_keys\_raises}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_bedrock\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_embedding\_unknown\_auth\_type\_raises}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_bedrock\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_embedding\_unknown\_bearer\_token\_typo\_raises}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_bedrock\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_embedding\_region\_required\_when\_absent}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_bedrock\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_embedding\_bearer\_token\_mode\_translates\_to\_api\_key}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_bedrock\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_embedding\_bearer\_token\_mode\_drops\_stale\_access\_keys}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_bedrock\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_embedding\_bearer\_token\_mode\_blank\_token\_raises}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_bedrock\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_embedding\_bearer\_token\_mode\_whitespace\_token\_raises}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_bedrock\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_embedding\_bearer\_token\_mode\_missing\_token\_raises}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_bedrock\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_embedding\_bearer\_token\_strips\_surrounding\_whitespace}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_bedrock\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_embedding\_bearer\_token\_survives\_revalidation}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_bedrock\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_embedding\_iam\_role\_drops\_stale\_bearer\_token}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_bedrock\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_embedding\_access\_keys\_drops\_stale\_bearer\_token}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_bedrock\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_embedding\_legacy\_drops\_stray\_bearer\_token}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionContext.test\_round\_trip\_serialization}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionContext.test\_json\_serializable}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionContext.test\_enum\_values\_normalized}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionContext.test\_string\_values\_accepted}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionContext.test\_auto\_generates\_request\_id}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionContext.test\_explicit\_request\_id\_preserved}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionContext.test\_optional\_organization\_id}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionContext.test\_empty\_executor\_params\_default}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionContext.test\_complex\_executor\_params}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionContext.test\_validation\_rejects\_empty\_required\_fields}}$$ $$\textcolor{#23d18b}{\tt{4}}$$ $$\textcolor{#23d18b}{\tt{4}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionContext.test\_all\_operations\_accepted}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionContext.test\_from\_dict\_missing\_optional\_fields}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionResult.test\_success\_round\_trip}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionResult.test\_failure\_round\_trip}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionResult.test\_json\_serializable}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionResult.test\_failure\_requires\_error\_message}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionResult.test\_success\_allows\_no\_error}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionResult.test\_success\_rejects\_error}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionResult.test\_failure\_factory}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionResult.test\_failure\_factory\_no\_metadata}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionResult.test\_error\_none\_in\_success\_dict}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionResult.test\_error\_in\_failure\_dict}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionResult.test\_default\_empty\_dicts}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionResult.test\_from\_dict\_missing\_optional\_fields}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionResult.test\_response\_contract\_extract}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionResult.test\_response\_contract\_index}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionResult.test\_response\_contract\_answer\_prompt}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestBaseExecutor.test\_cannot\_instantiate\_abstract}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestBaseExecutor.test\_concrete\_subclass\_works}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestBaseExecutor.test\_execute\_returns\_result}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorRegistry.test\_register\_and\_get}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorRegistry.test\_get\_returns\_fresh\_instance}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorRegistry.test\_register\_as\_decorator}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorRegistry.test\_list\_executors}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorRegistry.test\_list\_executors\_empty}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorRegistry.test\_get\_unknown\_raises\_key\_error}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorRegistry.test\_get\_unknown\_lists\_available}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorRegistry.test\_duplicate\_name\_raises\_value\_error}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorRegistry.test\_register\_non\_subclass\_raises\_type\_error}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorRegistry.test\_register\_non\_class\_raises\_type\_error}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorRegistry.test\_clear}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorRegistry.test\_execute\_through\_registry}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionOrchestrator.test\_dispatches\_to\_correct\_executor}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionOrchestrator.test\_unknown\_executor\_returns\_failure}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionOrchestrator.test\_executor\_exception\_returns\_failure}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionOrchestrator.test\_exception\_result\_has\_elapsed\_metadata}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionOrchestrator.test\_successful\_result\_passed\_through}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionOrchestrator.test\_executor\_returning\_failure\_is\_not\_wrapped}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_sends\_task\_and\_returns\_result}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_uses\_default\_timeout}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_timeout\_from\_env}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_explicit\_timeout\_overrides\_env}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_timeout\_returns\_failure}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_generic\_exception\_returns\_failure}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_async\_returns\_task\_id}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_no\_app\_raises\_value\_error}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_async\_no\_app\_raises\_value\_error}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_failure\_result\_from\_executor}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_context\_serialized\_correctly}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_with\_callback\_sends\_link\_and\_link\_error}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_with\_callback\_success\_only}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_with\_callback\_error\_only}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_with\_callback\_no\_callbacks}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_with\_callback\_returns\_async\_result}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_with\_callback\_no\_app\_raises\_value\_error}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_with\_callback\_context\_serialized}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_with\_callback\_custom\_task\_id}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_with\_callback\_no\_task\_id\_omits\_kwarg}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorToolShim.test\_platform\_api\_key\_returned}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorToolShim.test\_platform\_api\_key\_missing\_raises}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorToolShim.test\_other\_env\_var\_from\_environ}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorToolShim.test\_missing\_env\_var\_raises}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorToolShim.test\_empty\_env\_var\_raises}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorToolShim.test\_stream\_log\_routes\_to\_logging}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorToolShim.test\_stream\_log\_respects\_level}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorToolShim.test\_stream\_error\_and\_exit\_raises\_sdk\_error}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorToolShim.test\_stream\_error\_and\_exit\_wraps\_original}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_validate\_model\_prefixes\_when\_missing}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_validate\_model\_does\_not\_double\_prefix}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_validate\_model\_blank\_raises}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_validate\_thinking\_disabled\_by\_default}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_validate\_excludes\_control\_fields\_from\_model}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_validate\_thinking\_enabled\_with\_budget}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_validate\_thinking\_overrides\_user\_temperature}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_validate\_thinking\_enabled\_without\_budget\_raises}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_validate\_thinking\_budget\_tokens\_invalid\_type\_raises}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_validate\_thinking\_budget\_tokens\_too\_small\_raises}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_validate\_preserves\_existing\_thinking\_config}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_validate\_does\_not\_mutate\_input}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_thinking\_controls\_not\_pydantic\_fields}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_api\_key\_is\_required}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_adapter\_identity}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_schema\_required\_fields}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_schema\_enable\_thinking\_default\_false}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_schema\_budget\_tokens\_conditional}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_adapter\_registration}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_get\_id\_format}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_get\_adapter\_type}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_get\_name}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_get\_provider}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_json\_schema\_loads}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_json\_schema\_required\_fields}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_json\_schema\_no\_batch\_size\_default}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_json\_schema\_api\_key\_password\_format}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_json\_schema\_model\_default}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_validate\_model\_adds\_prefix}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_validate\_model\_idempotent}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_validate\_model\_does\_not\_mutate\_input}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_validate\_does\_not\_mutate\_input}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_validate\_model\_empty\_string\_raises}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_validate\_model\_whitespace\_only\_raises}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_validate\_model\_none\_raises}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_validate\_model\_missing\_key\_raises}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_validate\_empty\_model\_raises}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_validate\_none\_model\_raises}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_validate\_missing\_api\_key\_raises}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_validate\_calls\_validate\_model}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_validate\_embed\_batch\_size\_none\_by\_default}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_validate\_embed\_batch\_size\_preserved}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_validate\_strips\_extra\_fields}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_validate\_includes\_base\_fields}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_metadata}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestLLMCompatFromLlm.test\_from\_llm\_reuses\_llm\_instance}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestLLMCompatFromLlm.test\_from\_llm\_returns\_llmcompat\_instance}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestLLMCompatFromLlm.test\_from\_llm\_sets\_model\_name}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestLLMCompatFromLlm.test\_from\_llm\_does\_not\_call\_init}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestLLMCompatDelegation.test\_complete\_delegates\_to\_llm}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestLLMCompatDelegation.test\_chat\_delegates\_to\_llm\_complete}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestLLMCompatDelegation.test\_chat\_forwards\_kwargs\_to\_llm}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestLLMCompatDelegation.test\_complete\_forwards\_kwargs\_to\_llm}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestLLMCompatDelegation.test\_acomplete\_delegates\_to\_llm}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestLLMCompatDelegation.test\_achat\_delegates\_to\_llm\_acomplete}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestLLMCompatDelegation.test\_stream\_chat\_not\_implemented}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestLLMCompatDelegation.test\_stream\_complete\_not\_implemented}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestLLMCompatDelegation.test\_astream\_chat\_not\_implemented}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestLLMCompatDelegation.test\_astream\_complete\_not\_implemented}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestLLMCompatDelegation.test\_metadata\_returns\_emulated\_type}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestLLMCompatDelegation.test\_get\_model\_name\_delegates}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestLLMCompatDelegation.test\_get\_metrics\_delegates}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestLLMCompatDelegation.test\_test\_connection\_delegates}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestEmulatedTypes.test\_message\_role\_values}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestEmulatedTypes.test\_chat\_message\_defaults}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestEmulatedTypes.test\_chat\_response\_message\_access}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestEmulatedTypes.test\_completion\_response\_text}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestEmulatedTypes.test\_llm\_metadata\_defaults}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestMessagesToPrompt.test\_single\_user\_message}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestMessagesToPrompt.test\_none\_content\_becomes\_empty\_string}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestMessagesToPrompt.test\_preserves\_all\_messages}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestMessagesToPrompt.test\_multi\_turn\_conversation}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestMessagesToPrompt.test\_empty\_messages\_returns\_empty\_string}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestMessagesToPrompt.test\_string\_role\_fallback}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_openai\_compatible\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_openai\_compatible\_adapter\_is\_registered}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_openai\_compatible\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_openai\_compatible\_validate\_prefixes\_model}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_openai\_compatible\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_openai\_compatible\_validate\_preserves\_prefixed\_model}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_openai\_compatible\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_openai\_compatible\_validate\_normalizes\_blank\_api\_key\_to\_none}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_openai\_compatible\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_openai\_compatible\_schema\_is\_loadable}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_openai\_compatible\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_openai\_compatible\_schema\_exposes\_reasoning\_toggle}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_openai\_compatible\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_openai\_compatible\_validate\_auto\_detects\_reasoning\_for\_known\_families}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_openai\_compatible\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_openai\_compatible\_validate\_preserves\_non\_reasoning\_models}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_openai\_compatible\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_openai\_compatible\_validate\_routes\_reasoning\_via\_extra\_body}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_openai\_compatible\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_reasoning\_omits\_max\_completion\_tokens\_when\_unset}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_openai\_compatible\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_openai\_compatible\_validate\_infers\_reasoning\_from\_effort\_field}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_openai\_compatible\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_reasoning\_state\_survives\_revalidation\_for\_custom\_alias}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_openai\_compatible\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_explicit\_disable\_overrides\_leftover\_reasoning\_effort}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_openai\_compatible\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_openai\_compatible\_validate\_no\_reasoning\_unchanged}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_openai\_compatible\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_openai\_compatible\_validate\_sets\_cost\_model\_for\_openai\_endpoint}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_openai\_compatible\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_openai\_compatible\_validate\_cost\_model\_keeps\_openai\_subprefix}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_openai\_compatible\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_openai\_compatible\_validate\_no\_cost\_model\_for\_other\_gateway}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_openai\_compatible\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_openai\_compatible\_validate\_cost\_model\_stable\_on\_revalidation}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_openai\_compatible\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_openai\_compatible\_adapter\_uses\_distinct\_description\_and\_icon}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_openai\_compatible\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_record\_usage\_uses\_reported\_prompt\_tokens\_without\_estimating}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_openai\_compatible\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_record\_usage\_tolerates\_unmapped\_models\_without\_prompt\_tokens}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_openai\_compatible\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_record\_usage\_uses\_estimated\_prompt\_tokens\_when\_usage\_has\_none}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_platform.py}}$$ $$\textcolor{#23d18b}{\tt{TestPlatformHelperRetry.test\_success\_on\_first\_attempt}}$$ $$\textcolor{#23d18b}{\tt{2}}$$ $$\textcolor{#23d18b}{\tt{2}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_platform.py}}$$ $$\textcolor{#23d18b}{\tt{TestPlatformHelperRetry.test\_retry\_on\_connection\_error}}$$ $$\textcolor{#23d18b}{\tt{2}}$$ $$\textcolor{#23d18b}{\tt{2}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_platform.py}}$$ $$\textcolor{#23d18b}{\tt{TestPlatformHelperRetry.test\_non\_retryable\_http\_error}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_platform.py}}$$ $$\textcolor{#23d18b}{\tt{TestPlatformHelperRetry.test\_retryable\_http\_errors}}$$ $$\textcolor{#23d18b}{\tt{3}}$$ $$\textcolor{#23d18b}{\tt{3}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_platform.py}}$$ $$\textcolor{#23d18b}{\tt{TestPlatformHelperRetry.test\_post\_method\_retry}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_platform.py}}$$ $$\textcolor{#23d18b}{\tt{TestPlatformHelperRetry.test\_retry\_logging}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_prompt.py}}$$ $$\textcolor{#23d18b}{\tt{TestPromptToolRetry.test\_success\_on\_first\_attempt}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_prompt.py}}$$ $$\textcolor{#23d18b}{\tt{TestPromptToolRetry.test\_retry\_on\_errors}}$$ $$\textcolor{#23d18b}{\tt{2}}$$ $$\textcolor{#23d18b}{\tt{2}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_prompt.py}}$$ $$\textcolor{#23d18b}{\tt{TestPromptToolRetry.test\_wrapper\_methods\_retry}}$$ $$\textcolor{#23d18b}{\tt{4}}$$ $$\textcolor{#23d18b}{\tt{4}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_sampling\_strip.py}}$$ $$\textcolor{#23d18b}{\tt{test\_has\_deprecated\_sampling\_params\_positive}}$$ $$\textcolor{#23d18b}{\tt{23}}$$ $$\textcolor{#23d18b}{\tt{23}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_sampling\_strip.py}}$$ $$\textcolor{#23d18b}{\tt{test\_has\_deprecated\_sampling\_params\_negative}}$$ $$\textcolor{#23d18b}{\tt{19}}$$ $$\textcolor{#23d18b}{\tt{19}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_sampling\_strip.py}}$$ $$\textcolor{#23d18b}{\tt{test\_strip\_returns\_copy\_without\_mutating\_input}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_sampling\_strip.py}}$$ $$\textcolor{#23d18b}{\tt{test\_strip\_removes\_all\_three\_sampling\_params}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_sampling\_strip.py}}$$ $$\textcolor{#23d18b}{\tt{test\_strip\_via\_model\_id\_field\_when\_model\_is\_opaque\_aip\_arn}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_sampling\_strip.py}}$$ $$\textcolor{#23d18b}{\tt{test\_strip\_via\_model\_field\_when\_model\_id\_is\_opaque\_aip\_arn}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_sampling\_strip.py}}$$ $$\textcolor{#23d18b}{\tt{test\_strip\_skipped\_when\_both\_fields\_opaque\_and\_logs\_debug}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_sampling\_strip.py}}$$ $$\textcolor{#23d18b}{\tt{test\_strip\_does\_not\_log\_when\_no\_sampling\_params\_present}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_sampling\_strip.py}}$$ $$\textcolor{#23d18b}{\tt{test\_strip\_does\_not\_log\_when\_sampling\_params\_present\_but\_model\_not\_opaque}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_sampling\_strip.py}}$$ $$\textcolor{#23d18b}{\tt{test\_strip\_retains\_temperature\_for\_non\_deprecated\_models}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_sampling\_strip.py}}$$ $$\textcolor{#23d18b}{\tt{test\_validate\_strips\_temperature\_for\_opus\_4\_7}}$$ $$\textcolor{#23d18b}{\tt{3}}$$ $$\textcolor{#23d18b}{\tt{3}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_sampling\_strip.py}}$$ $$\textcolor{#23d18b}{\tt{test\_vertex\_validate\_strips\_temperature\_for\_opus\_4\_7}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_sampling\_strip.py}}$$ $$\textcolor{#23d18b}{\tt{test\_validate\_retains\_temperature\_for\_opus\_4\_6}}$$ $$\textcolor{#23d18b}{\tt{3}}$$ $$\textcolor{#23d18b}{\tt{3}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_sampling\_strip.py}}$$ $$\textcolor{#23d18b}{\tt{test\_vertex\_validate\_retains\_temperature\_for\_gemini}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_connection\_error\_is\_retryable}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_timeout\_is\_retryable}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_http\_error\_retryable\_status\_codes}}$$ $$\textcolor{#23d18b}{\tt{3}}$$ $$\textcolor{#23d18b}{\tt{3}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_http\_error\_non\_retryable\_status\_codes}}$$ $$\textcolor{#23d18b}{\tt{5}}$$ $$\textcolor{#23d18b}{\tt{5}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_http\_error\_without\_response}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_os\_error\_retryable\_errno}}$$ $$\textcolor{#23d18b}{\tt{5}}$$ $$\textcolor{#23d18b}{\tt{5}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_os\_error\_non\_retryable\_errno}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_other\_exception\_not\_retryable}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCalculateDelay.test\_exponential\_backoff\_without\_jitter}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCalculateDelay.test\_exponential\_backoff\_with\_jitter}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCalculateDelay.test\_max\_delay\_cap}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCalculateDelay.test\_max\_delay\_cap\_with\_jitter}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_successful\_call\_first\_attempt}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_retry\_after\_transient\_failure}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_max\_retries\_exceeded}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_retry\_with\_custom\_predicate}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_no\_retry\_with\_predicate\_false}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_exception\_not\_in\_tuple\_not\_retried}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_default\_configuration}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_environment\_variable\_configuration}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_invalid\_max\_retries}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_invalid\_base\_delay}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_invalid\_multiplier}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_jitter\_values}}$$ $$\textcolor{#23d18b}{\tt{2}}$$ $$\textcolor{#23d18b}{\tt{2}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_custom\_exceptions\_only}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_custom\_predicate\_only}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_both\_exceptions\_and\_predicate}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_exceptions\_match\_but\_predicate\_false}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestPreconfiguredDecorators.test\_retry\_platform\_service\_call\_exists}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestPreconfiguredDecorators.test\_retry\_prompt\_service\_call\_exists}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestPreconfiguredDecorators.test\_platform\_service\_decorator\_retries\_on\_connection\_error}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestPreconfiguredDecorators.test\_prompt\_service\_decorator\_retries\_on\_timeout}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryLogging.test\_warning\_logged\_on\_retry}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryLogging.test\_info\_logged\_on\_success\_after\_retry}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryLogging.test\_exception\_logged\_on\_giving\_up}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{TOTAL}}$$ $$\textcolor{#23d18b}{\tt{347}}$$ $$\textcolor{#23d18b}{\tt{347}}$$

@sonarqubecloud
Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant