Skip to content

UN-1824 [FIX] Return HTTP 409 when tool image not found in container registry#1757

Merged
hari-kuriakose merged 16 commits intomainfrom
fix/tool-not-in-registry-409-error
Feb 26, 2026
Merged

UN-1824 [FIX] Return HTTP 409 when tool image not found in container registry#1757
hari-kuriakose merged 16 commits intomainfrom
fix/tool-not-in-registry-409-error

Conversation

@pk-zipstack
Copy link
Contributor

What

  • Return HTTP 409 Conflict instead of HTTP 200/422 when a tool image is not found in the container registry during API deployment execution

Why

  • Previously, when a tool image was not available in the container registry, the API returned HTTP 200 with execution_status: "ERROR" in the response body, or HTTP 422 with a generic error message
  • This made it difficult for API consumers to distinguish between configuration issues (tool not deployed) vs execution errors (tool ran but failed)
  • HTTP 409 Conflict better represents the situation where the platform state conflicts with the request requirements

How

  • Added ToolImageNotFoundError exception in runner to catch Docker image pull failures
  • Moved container config creation inside try block in runner.py to properly catch the exception during image pull
  • Added error_code field to RunnerContainerRunResponse DTO to propagate specific error types
  • Added ToolNotFoundInRegistryError exception in tool-sandbox that's raised when error_code matches
  • Added ToolNotFoundInRegistry API exception (HTTP 409) in backend
  • Updated api_deployment_views.py to detect "not found in container registry" pattern in both top-level and file-level errors
  • Fixed _process_final_output to preserve original error message when secondary exceptions occur during finalization
  • Skip retry attempts for tool not found errors (configuration issue, not transient)

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)

  • No breaking changes for normal workflows - only the HTTP status code changes from 200/422 to 409 for this specific error condition
  • API consumers that check for specific HTTP status codes may need to handle 409 for tool configuration errors
  • The response body structure remains unchanged.

Database Migrations

  • N/A

Env Config

  • N/A

Relevant Docs

  • N/A

Related Issues or PRs

Dependencies Versions

Notes on Testing

  1. Set an invalid tool image tag in backend .env:
    STRUCTURE_TOOL_IMAGE_TAG=0.0.999
    
  2. Restart backend and worker containers
  3. Execute an API deployment that uses the Structure tool
  4. Verify the response returns HTTP 409 with the error message containing "not found in container registry"

Screenshots

image

Checklist

I have read and understood the Contribution Guidelines.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 27, 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

Walkthrough

End-to-end detection and propagation of "tool image not found in registry" across API, runner, tool-sandbox, and workflow layers; new exception types and error_code propagation added; API maps tool-not-found to HTTP 500 and adjusts execution/status error handling and conditional metadata/metrics pruning.

Changes

Cohort / File(s) Summary
API exceptions & views
backend/api_v2/exceptions.py, backend/api_v2/api_deployment_views.py
Add ToolNotFoundInRegistry/helper contains_tool_not_found_error(response); centralize detection in POST/GET flows; map tool-not-found -> 500, other exec errors -> 422; add result_acknowledged handling and conditional metadata/metrics pruning; extend PresignedURLFetchError.
Runner client & exceptions
runner/src/unstract/runner/exception.py, runner/src/unstract/runner/clients/docker_client.py, runner/src/unstract/runner/runner.py
Introduce ToolImageNotFoundError; detect image-pull failures (ImageNotFound, API 404, pull stream "error") and raise it; convert to structured runner responses including error_code; update container run/sidecar flow and command formatting; return structured error result.
Tool sandbox DTO & helpers
unstract/tool-sandbox/src/unstract/tool_sandbox/dto.py, unstract/tool-sandbox/src/unstract/tool_sandbox/exceptions.py, unstract/tool-sandbox/src/unstract/tool_sandbox/helper.py
Add error_code to RunnerContainerRunResponse; add ToolNotFoundInRegistryError; parse runner HTTP/JSON error bodies and embedded responses to raise ToolNotFoundInRegistryError when error_code matches.
Workflow manager / execution helpers
backend/workflow_manager/workflow_v2/file_execution_tasks.py, unstract/workflow-execution/src/unstract/workflow_execution/tools_utils.py
Handle tool-not-found by short-circuiting/raising without retry, logging and returning standardized error results; preserve and propagate existing processing errors during finalization.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant API as API Deployment
    participant Sandbox as Tool-Sandbox
    participant Runner as Runner/Docker
    participant Registry as Container Registry

    Client->>API: POST execution request
    activate API
    API->>Sandbox: request tool run
    activate Sandbox
    Sandbox->>Runner: HTTP request to runner
    activate Runner
    Runner->>Registry: pull tool image
    activate Registry
    Registry-->>Runner: ImageNotFound / 404 / pull stream error
    deactivate Registry
    Runner->>Runner: raise ToolImageNotFoundError and return structured JSON (error_code)
    Runner-->>Sandbox: HTTP error response (JSON with error_code)
    deactivate Runner
    Sandbox->>API: raise ToolNotFoundInRegistryError / propagate error
    deactivate Sandbox
    API->>API: contains_tool_not_found_error() detects pattern
    API-->>Client: 500 Internal Server Error with status/result
    deactivate API
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.83% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: returning HTTP 409 when tool image is not found in container registry, which is the core objective of this PR.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering all required template sections including What, Why, How, breaking changes assessment, testing notes, and the contribution guideline checklist.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/tool-not-in-registry-409-error

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
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

🤖 Fix all issues with AI agents
In `@runner/src/unstract/runner/clients/docker_client.py`:
- Around line 177-205: The pull-stream error handling in the client.api.pull
loop currently raises ToolImageNotFoundError for any stream "error"; update the
logic in the loop that inspects line.get("error") so it discriminates based on
errorDetail and message: extract error_msg = line.get("error") and err_detail =
line.get("errorDetail", {}), then if err_detail.get("code") == 404 or "manifest
unknown" in error_msg.lower() or "not found" in error_msg.lower() raise
ToolImageNotFoundError(repository, image_tag); otherwise log the full error
(include error_msg and err_detail) and re-raise or propagate a generic exception
(so auth 401, rate-limit 429, network errors are not misclassified as
not-found); keep using image_name_with_tag, repository, image_tag and
ToolImageNotFoundError to locate the code to change.
🧹 Nitpick comments (1)
backend/api_v2/api_deployment_views.py (1)

54-88: Prefer explicit error_code checks over string matching.
Since downstream results now carry error_code, checking it directly avoids brittle text matching and future message changes.

♻️ Suggested refinement
-    if isinstance(response, dict):
-        error = response.get("error")
-        result = response.get("result", [])
-    else:
-        error = getattr(response, "error", None)
-        result = getattr(response, "result", []) or []
+    if isinstance(response, dict):
+        error = response.get("error")
+        error_code = response.get("error_code")
+        result = response.get("result", [])
+    else:
+        error = getattr(response, "error", None)
+        error_code = getattr(response, "error_code", None)
+        result = getattr(response, "result", []) or []
+
+    if error_code == ToolNotFoundInRegistry.ERROR_CODE:
+        return True
...
-            if isinstance(item, dict):
-                file_error = item.get("error", "")
+            if isinstance(item, dict):
+                if item.get("error_code") == ToolNotFoundInRegistry.ERROR_CODE:
+                    return True
+                file_error = item.get("error", "")

Copy link
Contributor

@chandrasekharan-zipstack chandrasekharan-zipstack left a comment

Choose a reason for hiding this comment

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

LGTM for the most part, do confirm on whether the 409 status code can be 500 here instead. Between

  • runner -> backend, it can be 409
  • backend -> user, it needs to be 500

Copy link
Contributor

@harini-venkataraman harini-venkataraman left a comment

Choose a reason for hiding this comment

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

Added some comments, otherwise, LGTM

Copy link
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 (2)
runner/src/unstract/runner/runner.py (2)

532-556: ⚠️ Potential issue | 🟠 Major

ToolImageNotFoundError is shadowed by ToolRunException.
ToolImageNotFoundError inherits from ToolRunException, so the current order means the specific handler never runs and error_code is dropped. Swap the order to preserve the 409 mapping.

✅ Fix by reordering exception handlers
-        except ToolRunException as te:
-            self.logger.error(
-                f"Error while running docker container {container_name}: {te}",
-                stack_info=True,
-                exc_info=True,
-            )
-            result = {
-                "type": "RESULT",
-                "result": None,
-                "error": str(te.message),
-                "status": "ERROR",
-            }
-        except ToolImageNotFoundError as e:
+        except ToolImageNotFoundError as e:
             self.logger.error(
                 f"Tool image not found in container registry: {e.image_name}:{e.image_tag}",
                 stack_info=True,
                 exc_info=True,
             )
             result = {
                 "type": "RESULT",
                 "result": None,
                 "error": str(e.message),
                 "error_code": ToolImageNotFoundError.ERROR_CODE,
                 "status": "ERROR",
             }
+        except ToolRunException as te:
+            self.logger.error(
+                f"Error while running docker container {container_name}: {te}",
+                stack_info=True,
+                exc_info=True,
+            )
+            result = {
+                "type": "RESULT",
+                "result": None,
+                "error": str(te.message),
+                "status": "ERROR",
+            }

483-512: ⚠️ Potential issue | 🟠 Major

Capture sidecar-run container and sidecar handles—cleanup is currently a no-op.

The run_container_with_sidecar() method returns tuple[ContainerInterface, ContainerInterface | None], but the current code ignores this return value. The container and sidecar variables, initialized as None at the start of the function, are never updated in the sidecar path. This causes the cleanup block at the end (if container: container.cleanup(...) and if sidecar: sidecar.cleanup(...)) to skip execution for sidecar-based runs, potentially leaking containers if the client doesn't handle cleanup internally. Update the sidecar call to capture both handles:

Suggested fix
if sidecar_config:
-   self.client.run_container_with_sidecar(container_config, sidecar_config)
+   container, sidecar = self.client.run_container_with_sidecar(
+       container_config, sidecar_config
+   )
🤖 Fix all issues with AI agents
In `@backend/api_v2/exceptions.py`:
- Around line 95-124: The ToolNotFoundInRegistry exception currently sets
status_code = 500 but the API contract requires a 409 Conflict for missing tool
images; change the class's status_code from 500 to 409 (leave ERROR_CODE,
default_detail and the __init__ logic intact) so ToolNotFoundInRegistry returns
HTTP 409 Conflict to clients consistently with the stated behavior.
🧹 Nitpick comments (1)
backend/api_v2/exceptions.py (1)

127-170: Field name is correct but consider defensive handling for non-dict items in ExecutionResponse.result.

The ExecutionResponse.result field (from dto.py) is correctly accessed in the code. However, since result is typed as Any | None, items in the result list could theoretically be non-dict objects. The current code at lines 165–170 only processes dict items, which aligns with existing patterns in remove_result_metadata_keys() and remove_result_metrics() methods. Consider extracting error via attribute access for defensive compatibility with potential DTO objects in the result array:

Suggested defensive update
-    if isinstance(result, list):
-        for item in result:
-            if isinstance(item, dict):
-                if _check_error_code(item.get("error")):
-                    return True
+    if isinstance(result, list):
+        for item in result:
+            if isinstance(item, dict):
+                item_error = item.get("error")
+            else:
+                item_error = getattr(item, "error", None)
+            if _check_error_code(item_error):
+                return True

Deepak-Kesavan and others added 2 commits February 26, 2026 12:28
…oolRunException

ToolImageNotFoundError is a subclass of ToolRunException, so it must be
caught first to avoid being unreachable dead code (SonarCloud python:S1045).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
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 (1)
runner/src/unstract/runner/runner.py (1)

465-509: ⚠️ Potential issue | 🔴 Critical

Capture the return value from run_container_with_sidecar() to enable sidecar cleanup.

Line 508 calls self.client.run_container_with_sidecar(...) but discards the return tuple, leaving the sidecar variable as None. This makes the cleanup at line 569 (if sidecar: sidecar.cleanup(...)) unreachable for the sidecar branch, orphaning the sidecar container lifecycle. Assign the return value: container, sidecar = self.client.run_container_with_sidecar(container_config, sidecar_config).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@runner/src/unstract/runner/runner.py` around lines 465 - 509, The sidecar
return value from self.client.run_container_with_sidecar is being ignored so the
local sidecar variable remains None and sidecar.cleanup never runs; change the
call in the branch where sidecar_config is truthy to capture the returned tuple
(e.g., container, sidecar =
self.client.run_container_with_sidecar(container_config, sidecar_config)) so the
existing cleanup logic that calls sidecar.cleanup(...) can execute; update any
subsequent uses that expect the container variable as needed (references:
run_container_with_sidecar, sidecar, container, and the later sidecar.cleanup
call).
🧹 Nitpick comments (1)
runner/src/unstract/runner/runner.py (1)

498-502: Narrow the label-parser exception scope.

Line 501 catches Exception, which can hide unrelated runtime problems in this block. Restrict to parsing errors from ast.literal_eval.

🔧 Proposed fix
-            except Exception as e:
+            except (SyntaxError, ValueError) as e:
                 self.logger.info(f"Invalid labels for logging: {e}")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@runner/src/unstract/runner/runner.py` around lines 498 - 502, The current
broad except in the label-parsing block hides unrelated errors; narrow it to
only handle parsing errors from ast.literal_eval by catching ValueError and
SyntaxError instead of Exception for the block that reads
Env.TOOL_CONTAINER_LABELS and sets container_config["labels"]; keep the same
self.logger.info(...) call for the parse failure so unrelated runtime exceptions
will propagate.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@runner/src/unstract/runner/runner.py`:
- Around line 299-300: The shell command currently builds settings JSON into a
single-quoted string using .replace("'", "\\'") which fails for JSON containing
apostrophes; update the code that constructs the command string (the line that
builds f"python main.py --command RUN --settings '{settings_json}' --log-level
DEBUG") to safely escape the settings by importing shlex and using
shlex.quote(settings_json) instead of manual replace, i.e., call
shlex.quote(settings_json) when interpolating settings_json into the command;
ensure you add the shlex import at the top of runner.py and remove the brittle
.replace usage.

---

Outside diff comments:
In `@runner/src/unstract/runner/runner.py`:
- Around line 465-509: The sidecar return value from
self.client.run_container_with_sidecar is being ignored so the local sidecar
variable remains None and sidecar.cleanup never runs; change the call in the
branch where sidecar_config is truthy to capture the returned tuple (e.g.,
container, sidecar = self.client.run_container_with_sidecar(container_config,
sidecar_config)) so the existing cleanup logic that calls sidecar.cleanup(...)
can execute; update any subsequent uses that expect the container variable as
needed (references: run_container_with_sidecar, sidecar, container, and the
later sidecar.cleanup call).

---

Nitpick comments:
In `@runner/src/unstract/runner/runner.py`:
- Around line 498-502: The current broad except in the label-parsing block hides
unrelated errors; narrow it to only handle parsing errors from ast.literal_eval
by catching ValueError and SyntaxError instead of Exception for the block that
reads Env.TOOL_CONTAINER_LABELS and sets container_config["labels"]; keep the
same self.logger.info(...) call for the parse failure so unrelated runtime
exceptions will propagate.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to Reviews > Disable Cache setting

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between 6873513 and 8213522.

📒 Files selected for processing (1)
  • runner/src/unstract/runner/runner.py

hari-kuriakose and others added 4 commits February 26, 2026 14:08
Replace brittle manual .replace("'", "\\'") with shlex.quote() which
correctly handles all shell-special characters in the settings JSON string.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Contributor

Test Results

Summary
  • Runner Tests: 11 passed, 0 failed (11 total)
  • SDK1 Tests: 63 passed, 0 failed (63 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/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/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{63}}$$ $$\textcolor{#23d18b}{\tt{63}}$$

@sonarqubecloud
Copy link

@hari-kuriakose hari-kuriakose merged commit 4d4a5b4 into main Feb 26, 2026
7 checks passed
@hari-kuriakose hari-kuriakose deleted the fix/tool-not-in-registry-409-error branch February 26, 2026 11:26
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.

5 participants