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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- Standalone custom console domains now derive a consistent `api.` hostname
for both CRUD operations and OTLP trace export unless `SPLUNK_AO_API_URL` is
set explicitly.
Comment on lines +12 to +14

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟠 major (documentation): This is filed under ### Fixed, but for a class of existing users it is a breaking change, not a fix. Any standalone deployment whose console host lacks a console./app. prefix previously had api_url fall back to the console host (galileo-core's replace("console", "api") was a no-op) and worked if console and API shared a host. Those deployments now point at api.<console-host> and stop working until SPLUNK_AO_API_URL is set.

The PR description does call this out under "Compatibility / risk", but the CHANGELOG is what users actually read on upgrade, and it currently reads as a pure improvement. Two other behavior changes are also unrecorded: otlp_endpoint for a localhost console on a non-8088 port now retargets to http://localhost:8088, and hosts containing console as a non-leading substring (customer-console.example.com) now derive api.customer-console.example.com instead of customer-api.example.com.

Please add a ### Changed entry with the migration step:

Suggested change
- Standalone custom console domains now derive a consistent `api.` hostname
for both CRUD operations and OTLP trace export unless `SPLUNK_AO_API_URL` is
set explicitly.
### Changed
- **Standalone API URL derivation.** Console hosts without a leading `console.`
or `app.` label now derive an `api.`-prefixed API hostname instead of reusing
the console host. Deployments that serve the console and API from the same
host, or whose API hostname does not follow this convention, must now set
`SPLUNK_AO_API_URL` explicitly. Two related changes: `otlp_endpoint` for a
localhost console now targets the API port (`http://localhost:8088`) rather
than the console port, and `console` is now matched only as a leading label,
so hosts such as `customer-console.example.com` derive
`api.customer-console.example.com`.
### Fixed
- Standalone custom console domains now derive a consistent `api.` hostname
for both CRUD operations and OTLP trace export unless `SPLUNK_AO_API_URL` is
set explicitly.

🤖 Generated by the Astra agent

- Agent Control spans exported over OTLP now include the control discriminator
and complete `agent_control.*` field set required for backend classification
and Controls-card rendering.
Expand Down
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,19 @@ export SPLUNK_AO_API_KEY="your-agent-observability-api-key"
export SPLUNK_AO_CONSOLE_URL="https://console.subdomain.yourcompany.com"
```

For standalone custom domains, the SDK derives the API hostname by replacing a
leading `console.` or `app.` label with `api.`, or by adding an `api.` prefix
when neither label is present. Set `SPLUNK_AO_API_URL` explicitly when your API
does not follow this convention.
Comment on lines +63 to +66

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 minor (documentation): This paragraph documents the console./app.api. mapping and the api. fallback, but omits the localhost branch directly above it in the resolver, which is the case on-prem developers hit first: any console URL containing localhost or 127.0.0.1 resolves to http://localhost:8088 regardless of the console port. Anyone running the API on a different local port must set SPLUNK_AO_API_URL, and nothing tells them so.

Worth one more sentence here, especially since splunk-ao-a2a/README.md:76 advertises http://localhost:8088 as the example console URL:

Suggested change
For standalone custom domains, the SDK derives the API hostname by replacing a
leading `console.` or `app.` label with `api.`, or by adding an `api.` prefix
when neither label is present. Set `SPLUNK_AO_API_URL` explicitly when your API
does not follow this convention.
For standalone custom domains, the SDK derives the API hostname by replacing a
leading `console.` or `app.` label with `api.`, or by adding an `api.` prefix
when neither label is present. Local development console URLs (`localhost` or
`127.0.0.1`) always resolve to `http://localhost:8088`. Set `SPLUNK_AO_API_URL`
explicitly when your API does not follow these conventions — for example when
the console and API share a hostname, or when a local API listens on a port
other than 8088.

🤖 Generated by the Astra agent


> [!TIP]
> Logging your first trace to on-premises Agent Observability? [Visit this guide](https://agent-observability-docs.splunk.com/sdk-redirect/on-prem-first-trace).
> Logging your first trace to on-premises Agent Observability? [Visit this guide](https://agent-observability-docs.splunk.com/sdk-redirect/on-prem-first-trace).
>
> Learn how to find each environment variable in [this guide](https://agent-observability-docs.splunk.com/sdk-redirect/on-prem-keys).

#### Splunk Observability (O11y) Cloud

> [!NOTE]
> [!NOTE]
> As of August 2026, Agent Observability on Splunk Observability Cloud is not yet generally available.

| Environment variable | Description |
Expand Down
12 changes: 10 additions & 2 deletions src/splunk_ao/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from galileo_core.helpers.api_client import ApiClient
from galileo_core.schemas.base_config import GalileoConfig
from splunk_ao.constants import DEFAULT_CONSOLE_URL
from splunk_ao.deployment import DeploymentMode, O11yConfig
from splunk_ao.deployment import DeploymentMode, O11yConfig, resolve_standalone_api_url
from splunk_ao.deployment import resolve_deployment as _resolve_deployment
from splunk_ao.shared.exceptions import ConfigurationError, MissingConfigurationError

Expand Down Expand Up @@ -105,7 +105,15 @@ def set_api_url(cls, api_url: str | Url | None, info: ValidationInfo) -> Url:
"""Derive the O11y API URL from its realm and preserve standalone validation."""
if cls._is_o11y_env():
return Url(O11yConfig.from_env().require_api_url())
return super().set_api_url(api_url, info)

console_url = info.data.get("console_url")
if console_url is None:
return super().set_api_url(api_url, info)

console_url_value = str(console_url)
api_url_value = str(api_url) if api_url is not None else None
resolved_api_url = resolve_standalone_api_url(console_url_value, api_url_value)
return super().set_api_url(resolved_api_url, info)

@model_validator(mode="after")
def set_jwt_token(self) -> "SplunkAOConfig":
Expand Down
21 changes: 20 additions & 1 deletion src/splunk_ao/deployment.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,25 @@ def resolve_deployment() -> DeploymentMode:
)


def resolve_standalone_api_url(console_url: str, api_url: str | None = None) -> str:
"""Return the explicit or console-derived standalone API base URL."""
if api_url:
return api_url

if "localhost" in console_url or "127.0.0.1" in console_url:
return "http://localhost:8088"
Comment on lines +54 to +55

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 minor (bug): The PR description says "Kept localhost development behavior and the /otel/v1/traces endpoint unchanged," but the OTLP endpoint for local dev did change. otlp_endpoint previously had no localhost branch at all — it only did the console./app. replacements — so the console port was carried through:

console_url otlp_endpoint before after
http://localhost:8088 http://localhost:8088/otel/v1/traces unchanged
http://localhost:3000 http://localhost:3000/otel/v1/traces http://localhost:8088/otel/v1/traces

Routing to the API port rather than the console port is very likely the intended outcome, so I'm not asking you to revert it — but it should be stated in the description and the CHANGELOG rather than described as unchanged, and it deserves a test (see the tests/test_deployment.py comment). The hardcoded 8088 also silently discards a non-default local API port; anyone running the API elsewhere locally must now set SPLUNK_AO_API_URL, which is worth the same README note the custom-domain fallback got.

Separately, "localhost" in console_url is a substring test, so https://localhost.customer.example.com also collapses to http://localhost:8088. That over-broad match is inherited verbatim from galileo-core's set_api_url, so it is pre-existing on the CRUD path — but this change newly extends it to OTLP export.

🤖 Generated by the Astra agent

Comment on lines +54 to +55

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 minor (bug): "localhost" in console_url is an unanchored substring test, so a legitimate public host containing that label collapses to http://localhost:8088 — e.g. https://localhost.customer.example.com sends both CRUD traffic and OTLP spans to the developer's own machine.

This over-broad match is inherited verbatim from galileo-core's set_api_url, so on the CRUD path it is pre-existing — but this PR newly extends it to OTLP export, which previously had no localhost branch at all. Since you're already centralizing the logic here, matching the host rather than the whole string closes it:

hostname = urlsplit(base_url if "://" in console_url else f"https://{console_url}").hostname or ""
if hostname in ("localhost", "127.0.0.1", "::1") or hostname.endswith(".localhost"):
    return "http://localhost:8088"

At minimum this deserves a parametrized negative case (https://localhost.customer.example.comhttps://api.localhost.customer.example.com) so the intent is recorded either way.

🤖 Generated by the Astra agent


base_url = console_url.rstrip("/")
if "://" not in base_url:
base_url = f"https://{base_url}"
base_url = base_url.replace("://console.", "://api.", 1).replace("://app.", "://api.", 1)
scheme, host = base_url.split("://", 1)
if not host.startswith("api."):
host = f"api.{host}"
Comment on lines +61 to +63

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 minor (bug): host here is everything after ://, so any path on the console URL is carried into the derived API base. https://customer.example.com/ui yields https://api.customer.example.com/ui, and otlp_endpoint then produces https://api.customer.example.com/ui/otel/v1/traces.

The function's contract is to return an API base URL, so the console's path component should not survive. Pre-PR this was harmless (the host was unchanged, so the path was at least still valid for that origin); now both the host and the path are wrong. Deriving from the parsed hostname — as suggested in the comment on lines 57-65 — drops the path as a side effect. If you'd rather keep the string manipulation, strip the path explicitly:

host = host.partition("/")[0]

A parametrized case such as ("https://customer.example.com/ui", None, "https://api.customer.example.com") would pin this.

🤖 Generated by the Astra agent


return f"{scheme}://{host}"
Comment on lines +57 to +65

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟠 major (bug): base_url.split("://", 1) unpacks into exactly two names, so any console_url without a scheme raises ValueError: not enough values to unpack (expected 2, got 1).

This is reachable, and it is a regression rather than a pre-existing wart:

  • StandaloneConfig.console_url is an unvalidated str, and from_env() copies SPLUNK_AO_CONSOLE_URL straight through (deployment.py:165). Nothing prepends a scheme on this path.
  • With SPLUNK_AO_CONSOLE_URL=customer.example.com, StandaloneConfig.from_env().otlp_endpoint now raises. That property is called during SplunkAOLogger.__init__ (logger/logger.py:394) and SplunkAOOTLPExporter.__init__ (otel.py:139), so logger construction dies with an opaque unpacking error instead of exporting anything.
  • The previous implementation was self.console_url.replace("://console.", "://api.", 1).replace("://app.", "://api.", 1), which left a scheme-less host untouched and produced customer.example.com/otel/v1/traces — wrong, but not an exception.

A scheme-less console URL is a realistic user input: several example .env files in this repo prompt for SPLUNK_AO_CONSOLE_URL=your-splunk-ao-console-url with no scheme, and the ticket's own pseudocode has a dedicated branch for it.

str.partition keeps this a one-line fix and defaults to https, matching what galileo-core's ensure_https_console_url does on the config path:

Suggested change
base_url = console_url.rstrip("/")
base_url = base_url.replace("://console.", "://api.", 1).replace("://app.", "://api.", 1)
scheme, host = base_url.split("://", 1)
if not host.startswith("api."):
host = f"api.{host}"
return f"{scheme}://{host}"
base_url = console_url.rstrip("/")
base_url = base_url.replace("://console.", "://api.", 1).replace("://app.", "://api.", 1)
scheme, separator, host = base_url.partition("://")
if not separator:
scheme, host = "https", base_url
if not host.startswith("api."):
host = f"api.{host}"
return f"{scheme}://{host}"

🤖 Generated by the Astra agent

Comment on lines +57 to +65

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟠 major (bug): The api. prefix is applied to any host that isn't already api.-prefixed, including hosts where an api. subdomain cannot exist. This breaks deployments that worked before this PR on both paths.

Concrete failures:

SPLUNK_AO_CONSOLE_URL before (CRUD + OTLP) after
http://10.0.0.5:8088 http://10.0.0.5:8088 http://api.10.0.0.5:8088
http://ao-prod:8088 http://ao-prod:8088 http://api.ao-prod:8088

IP-literal and single-label (k8s service / hostname) console URLs are the normal shape for on-prem single-ingress installs — the repo's own fixtures and docs use exactly this layout (http://localhost:8088 in splunk-ao-a2a/README.md:76, http://fake.test:8088 in tests/conftest.py:26). Pre-PR, galileo-core's console_url.replace("console", "api") was a no-op on these, so api_url fell back to the console host and worked when console and API share a host+port; otlp_endpoint did the same. After this change both resolve to a hostname that does not resolve in DNS, and the only recovery is setting SPLUNK_AO_API_URL.

127.0.0.1 is already special-cased above, which shows IP-shaped input was contemplated — but 10.0.0.5, 192.168.x.x, and ::1 are not, and neither is any dotless host. Prefixing only makes sense for a dotted DNS name, so guard on that:

Note this also fixes the sibling case where the host part carries a port or path into the startswith("api.") check.

Suggested change
base_url = console_url.rstrip("/")
if "://" not in base_url:
base_url = f"https://{base_url}"
base_url = base_url.replace("://console.", "://api.", 1).replace("://app.", "://api.", 1)
scheme, host = base_url.split("://", 1)
if not host.startswith("api."):
host = f"api.{host}"
return f"{scheme}://{host}"
base_url = console_url.rstrip("/")
if "://" not in base_url:
base_url = f"https://{base_url}"
base_url = base_url.replace("://console.", "://api.", 1).replace("://app.", "://api.", 1)
scheme, host = base_url.split("://", 1)
hostname = host.partition("/")[0].partition(":")[0]
# Only a dotted DNS name can gain an `api.` label. IP literals and
# single-label hosts (k8s services, bare hostnames) must be left alone —
# `api.10.0.0.5` and `api.ao-prod` do not resolve.
is_ip_literal = ":" in hostname or (bool(hostname) and all(part.isdigit() for part in hostname.split(".")))
is_dotted_domain = "." in hostname and not is_ip_literal
if is_dotted_domain and not hostname.startswith("api."):
host = f"api.{host}"
return f"{scheme}://{host}"

🤖 Generated by the Astra agent



@dataclass
class O11yConfig:
"""Configuration for a Splunk Observability Cloud deployment."""
Expand Down Expand Up @@ -150,5 +169,5 @@ def from_env(cls) -> "StandaloneConfig":
@property
def otlp_endpoint(self) -> str:
"""Return the explicit or console-derived OTLP trace endpoint."""
base = self.api_url or self.console_url.replace("://console.", "://api.", 1).replace("://app.", "://api.", 1)
base = resolve_standalone_api_url(self.console_url, self.api_url)
return f"{base.rstrip('/')}/otel/v1/traces"
54 changes: 44 additions & 10 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
from unittest.mock import MagicMock, patch

import pytest
from pydantic import ValidationError
from test_support.config import fast_config_validation

from splunk_ao.config import _BRIDGE, SplunkAOConfig
from splunk_ao.deployment import StandaloneConfig
from splunk_ao.shared.exceptions import ConfigurationError

# Auth env vars cleared in tests that exercise the missing-auth guard.
Expand Down Expand Up @@ -81,9 +83,7 @@ def test_bridge_env_vars_propagates_splunk_ao_to_galileo(splunk_key, galileo_key
with patch.dict(os.environ, {splunk_key: value}, clear=False):
os.environ.pop(galileo_key, None)
SplunkAOConfig._bridge_env_vars()
assert os.environ.get(galileo_key) == value, (
f"Expected {galileo_key}={value!r} after bridging {splunk_key}"
)
assert os.environ.get(galileo_key) == value, f"Expected {galileo_key}={value!r} after bridging {splunk_key}"


@pytest.mark.parametrize("splunk_key,galileo_key", _CANONICAL_BRIDGE_PAIRS)
Expand All @@ -107,9 +107,7 @@ def test_bridge_env_vars_skips_absent_splunk_ao_keys() -> None:
with patch.dict(os.environ, clean_env, clear=True):
SplunkAOConfig._bridge_env_vars()
for _, galileo_key in _ALL_BRIDGE_PAIRS:
assert galileo_key not in os.environ, (
f"{galileo_key} must not be set when its SPLUNK_AO_* source is absent"
)
assert galileo_key not in os.environ, f"{galileo_key} must not be set when its SPLUNK_AO_* source is absent"


# ---------------------------------------------------------------------------
Expand All @@ -127,6 +125,44 @@ def test_default_console_url() -> None:
assert str(config.api_url) == "https://api.galileo.ai/"


def test_standalone_crud_and_otlp_share_custom_api_url_derivation(monkeypatch: pytest.MonkeyPatch) -> None:
# Given: a standalone custom console host without an explicit API URL
console_url = "https://customer.example.com"
monkeypatch.setattr(SplunkAOConfig, "_instance", None)

# When: CRUD configuration and OTLP configuration resolve their endpoints
with patch.dict(os.environ, {}, clear=True), fast_config_validation():
config = SplunkAOConfig.get(console_url=console_url, api_key="key", ssl_context=False)
otlp_endpoint = StandaloneConfig(api_key="key", console_url=console_url).otlp_endpoint

# Then: both consumers use the same derived standalone API base URL
assert str(config.api_url) == "https://api.customer.example.com/"
assert otlp_endpoint == f"{str(config.api_url).rstrip('/')}/otel/v1/traces"


def test_standalone_crud_preserves_explicit_api_url(monkeypatch: pytest.MonkeyPatch) -> None:
# Given: a standalone deployment with an explicit API URL
monkeypatch.setattr(SplunkAOConfig, "_instance", None)
explicit_api_url = "https://backend.example.com/custom/"

# When: CRUD configuration resolves its API URL with health checks mocked
with patch.dict(os.environ, {}, clear=True), fast_config_validation():
config = SplunkAOConfig.get(
console_url="https://customer.example.com", api_url=explicit_api_url, api_key="key", ssl_context=False
)

# Then: the explicit URL remains authoritative
assert str(config.api_url) == explicit_api_url


def test_invalid_console_url_preserves_validation_error() -> None:
# Given: a console URL that fails its own field validation

# When/Then: model construction reports a validation error instead of leaking a KeyError
with pytest.raises(ValidationError, match="console_url"):
SplunkAOConfig(console_url="http://[bad", api_key="key")


def test_no_auth_configured_raises_with_full_options_listed(monkeypatch) -> None:
"""When no auth is configured anywhere, the error lists every supported method."""
# Given: no auth env vars and no cached instance
Expand Down Expand Up @@ -271,8 +307,7 @@ def test_reset_clears_bridged_galileo_env_vars() -> None:

for galileo_key in galileo_keys:
assert galileo_key not in os.environ, (
f"reset() must remove {galileo_key} from os.environ; "
f"found stale value '{os.environ.get(galileo_key)}'"
f"reset() must remove {galileo_key} from os.environ; found stale value '{os.environ.get(galileo_key)}'"
)


Expand Down Expand Up @@ -306,8 +341,7 @@ def test_bridge_picks_up_new_credential_after_reset(monkeypatch) -> None:
# Second bridge — must pick up the new key now that reset() cleared the old one.
SplunkAOConfig._bridge_env_vars()
assert os.environ.get("GALILEO_API_KEY") == "key-rotated", (
"After reset() + credential rotation, bridge must copy the new key; "
"got stale value instead"
"After reset() + credential rotation, bridge must copy the new key; got stale value instead"
)
# Cleanup: monkeypatch will restore SPLUNK_AO_API_KEY, but the bridge wrote
# GALILEO_API_KEY directly to os.environ — remove it so it doesn't leak.
Expand Down
35 changes: 34 additions & 1 deletion tests/test_deployment.py

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 minor (testing): The new test_resolve_standalone_api_url parametrization covers six happy-path shapes but none of the inputs that changed behavior in a risky way:

  1. No scheme ("customer.example.com") — currently raises ValueError. The ticket called this case out explicitly; a parametrized case here would have caught it.
  2. Explicit api_url argument — the if api_url: return api_url short-circuit is only exercised indirectly through SplunkAOConfig in test_config.py and through StandaloneConfig.otlp_endpoint; the resolver's own override behavior is untested.
  3. otlp_endpoint for a localhost console on a non-8088 port — this is the one case whose output actually changed (see the deployment.py:54 comment), and nothing asserts the new value.

Cases 1 and 3 are both inside the problem this PR set out to solve, so they belong in this PR rather than a follow-up.

🤖 Generated by the Astra agent

Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import pytest

from splunk_ao.config import SplunkAOConfig
from splunk_ao.deployment import DeploymentMode, O11yConfig, StandaloneConfig
from splunk_ao.deployment import DeploymentMode, O11yConfig, StandaloneConfig, resolve_standalone_api_url
from splunk_ao.shared.exceptions import AmbiguousConfigurationError, MissingConfigurationError

_DETECTION_ENV_VARS = (
Expand Down Expand Up @@ -218,6 +218,39 @@ def test_missing_standalone_config_names_both_required_variables() -> None:
assert "SPLUNK_AO_CONSOLE_URL" in str(exc_info.value)


@pytest.mark.parametrize(
("console_url", "api_url", "expected"),
[
("https://customer.example.com", None, "https://api.customer.example.com"),
("customer.example.com", None, "https://api.customer.example.com"),
("https://api.customer.example.com", None, "https://api.customer.example.com"),
("http://customer.example.com:8443", None, "http://api.customer.example.com:8443"),
("http://localhost:3000", None, "http://localhost:8088"),
("https://127.0.0.1:9090", None, "http://localhost:8088"),
("https://customer-console.example.com", None, "https://api.customer-console.example.com"),
("https://customer.example.com", "https://backend.example.com/", "https://backend.example.com/"),
],
ids=[
"custom-host",
"custom-host-without-scheme",
"existing-api-prefix",
"scheme-and-port",
"localhost",
"loopback",
"console-substring",
"explicit-api-url",
],
)
def test_resolve_standalone_api_url(console_url: str, api_url: str | None, expected: str) -> None:
# Given: a standalone console URL requiring API-host resolution

# When: the standalone API base URL is derived
resolved = resolve_standalone_api_url(console_url, api_url)

# Then: the expected standalone API base URL is returned
assert resolved == expected


def test_otlp_endpoint_derived_from_console_url_when_api_url_unset() -> None:
cfg = StandaloneConfig(api_key="key", console_url="https://console.demo.galileocloud.io")
assert cfg.otlp_endpoint == "https://api.demo.galileocloud.io/otel/v1/traces"
Expand Down
2 changes: 1 addition & 1 deletion tests/test_exporter_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def test_standalone_exporter_endpoint() -> None:
cfg = StandaloneConfig(api_key="key", console_url="https://ao.example.com")
result = resolve_standalone_exporter_config(cfg, routing=make_routing(project_name="proj1"))

assert result.endpoint == cfg.otlp_endpoint == "https://ao.example.com/otel/v1/traces"
assert result.endpoint == cfg.otlp_endpoint == "https://api.ao.example.com/otel/v1/traces"


def test_standalone_exporter_endpoint_uses_explicit_api_url() -> None:
Expand Down
22 changes: 12 additions & 10 deletions tests/test_prompts_global.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
from splunk_ao import Message, MessageRole
from splunk_ao.prompts import create_prompt, delete_prompt, get_prompt, get_prompts

_API_URL = "http://api.fake.test:8088"


@pytest.fixture
def prompt_template_response():
Expand Down Expand Up @@ -62,12 +64,12 @@ class TestGlobalPromptTemplates:
def test_create_global_prompt(self, respx_mock: MockRouter, prompt_template_response):
"""Test creating a global prompt template."""
# Mock the query API (for uniqueness check)
query_route = respx_mock.post("http://fake.test:8088/templates/query").mock(
query_route = respx_mock.post(f"{_API_URL}/templates/query").mock(
return_value=httpx.Response(200, json={"templates": []})
)

# Mock the create API
create_route = respx_mock.post("http://fake.test:8088/templates").mock(
create_route = respx_mock.post(f"{_API_URL}/templates").mock(
return_value=httpx.Response(200, json=prompt_template_response)
)

Expand All @@ -80,7 +82,7 @@ def test_create_global_prompt(self, respx_mock: MockRouter, prompt_template_resp

def test_get_global_prompt_by_id(self, respx_mock: MockRouter, prompt_template_response):
"""Test retrieving a global prompt template by ID."""
get_route = respx_mock.get(f"http://fake.test:8088/templates/{prompt_template_response['id']}").mock(
get_route = respx_mock.get(f"{_API_URL}/templates/{prompt_template_response['id']}").mock(
return_value=httpx.Response(200, json=prompt_template_response)
)

Expand All @@ -92,7 +94,7 @@ def test_get_global_prompt_by_id(self, respx_mock: MockRouter, prompt_template_r

def test_get_global_prompt_by_name(self, respx_mock: MockRouter, prompt_template_response):
"""Test retrieving a global prompt template by name."""
query_route = respx_mock.post("http://fake.test:8088/templates/query").mock(
query_route = respx_mock.post(f"{_API_URL}/templates/query").mock(
return_value=httpx.Response(
200, json={"templates": [prompt_template_response], "next_starting_token": None}
)
Expand All @@ -106,7 +108,7 @@ def test_get_global_prompt_by_name(self, respx_mock: MockRouter, prompt_template

def test_list_global_prompts(self, respx_mock: MockRouter, prompt_template_response):
"""Test listing global prompt templates."""
query_route = respx_mock.post("http://fake.test:8088/templates/query").mock(
query_route = respx_mock.post(f"{_API_URL}/templates/query").mock(
return_value=httpx.Response(
200, json={"templates": [prompt_template_response], "next_starting_token": None}
)
Expand All @@ -120,7 +122,7 @@ def test_list_global_prompts(self, respx_mock: MockRouter, prompt_template_respo

def test_delete_global_prompt_by_id(self, respx_mock: MockRouter):
"""Test deleting a global prompt template by ID."""
delete_route = respx_mock.delete("http://fake.test:8088/templates/template-id-123").mock(
delete_route = respx_mock.delete(f"{_API_URL}/templates/template-id-123").mock(
return_value=httpx.Response(200, json={"message": "Template deleted successfully"})
)

Expand All @@ -131,14 +133,14 @@ def test_delete_global_prompt_by_id(self, respx_mock: MockRouter):
def test_delete_global_prompt_by_name(self, respx_mock: MockRouter, prompt_template_response):
"""Test deleting a global prompt template by name."""
# Mock query to find template by name
query_route = respx_mock.post("http://fake.test:8088/templates/query").mock(
query_route = respx_mock.post(f"{_API_URL}/templates/query").mock(
return_value=httpx.Response(
200, json={"templates": [prompt_template_response], "next_starting_token": None}
)
)

# Mock delete
delete_route = respx_mock.delete(f"http://fake.test:8088/templates/{prompt_template_response['id']}").mock(
delete_route = respx_mock.delete(f"{_API_URL}/templates/{prompt_template_response['id']}").mock(
return_value=httpx.Response(200, json={"message": "Template deleted successfully"})
)

Expand All @@ -151,13 +153,13 @@ def test_create_prompt_with_unique_name(self, respx_mock: MockRouter, prompt_tem
"""Test that duplicate names get auto-incremented."""
# Mock query to find existing template
existing_template = {**prompt_template_response, "name": "test-template"}
query_route = respx_mock.post("http://fake.test:8088/templates/query").mock(
query_route = respx_mock.post(f"{_API_URL}/templates/query").mock(
return_value=httpx.Response(200, json={"templates": [existing_template], "next_starting_token": None})
)

# Mock create with new unique name
new_template = {**prompt_template_response, "name": "test-template (1)"}
create_route = respx_mock.post("http://fake.test:8088/templates").mock(
create_route = respx_mock.post(f"{_API_URL}/templates").mock(
return_value=httpx.Response(200, json=new_template)
)

Expand Down
Loading