Skip to content
Closed
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
2 changes: 1 addition & 1 deletion airbyte/_direct_connectors/connector_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ def build_direct_access_sql_guidance(
title=f"{destination.name} (SQL passthrough destination)",
summary=f"Table layout and naming for the `{destination.name}` destination.",
tags=["destination"],
warnings=[sql_passthrough_notice] if sql_passthrough_notice else [],
)
connections = _destination_connections(destination)
content: list[dict[str, Any]] = []
Expand Down Expand Up @@ -247,7 +248,6 @@ def build_direct_access_sql_guidance(
outline=[],
section_id=None,
content=content,
warnings=[sql_passthrough_notice] if sql_passthrough_notice else [],
)


Expand Down
8 changes: 6 additions & 2 deletions airbyte/cloud/connectors.py
Original file line number Diff line number Diff line change
Expand Up @@ -742,7 +742,7 @@ def _context_layer_inspect(

The connector's docs skill is probed: a skill doc exists only for connectors the
Context layer knows. A 403 or 404 `AirbyteError` from the docs read means the
connector is not enabled for agent access, so a warning is appended to
docs are unavailable, so a warning is appended to
`warnings` and `None` is returned. Any other failure (auth, server, malformed
response, transport) is raised to the caller.
"""
Expand All @@ -764,7 +764,9 @@ def _context_layer_inspect(
except exc.AirbyteError as error:
if not agents_api_util.is_not_enabled_error(error):
raise
warnings.append(f"Connector direct-access docs lookup failed: {error}")
warnings.append(
"Connector direct-access docs are unavailable (access denied or not found)."
)
return None
parsed = _DirectConnectorInspectResult(
connector_id=self.connector_id,
Expand Down Expand Up @@ -937,6 +939,8 @@ def get_direct_access_guidance(self, *, section: str | None = None) -> DirectAcc
except exc.AirbyteError as error:
if not agents_api_util.is_not_enabled_error(error) or section is not None:
raise
# Keep the not-enabled notice for this docs fallback. A rare missing-docs
# 404 can reach it even when access is enabled.
return connector_docs.build_direct_access_sql_guidance(
destination,
sql_passthrough_notice=connector_docs.SQL_PASSTHROUGH_NOT_ENABLED_NOTICE,
Expand Down
10 changes: 5 additions & 5 deletions airbyte/mcp/cloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -1359,7 +1359,7 @@ def _describe_cloud_connector(

try:
result.enabled_features = sorted(connector.enabled_features)
except (AirbyteError, requests.RequestException) as error:
except (AirbyteError, requests.RequestException, ValueError) as error:
result.enabled_features = FEATURES_UNKNOWN
warnings.append(_feature_lookup_warning(error))

Expand All @@ -1372,8 +1372,8 @@ def _describe_cloud_connector(
context_layer = connector._context_layer_inspect( # noqa: SLF001
warnings=warnings,
)
except (AirbyteError, requests.RequestException) as error:
warnings.append(f"Connector direct-access docs lookup failed: {error}")
except (AirbyteError, requests.RequestException, ValueError):
warnings.append("Connector direct-access docs lookup failed.")
else:
if context_layer is not None:
warnings.extend(str(warning) for warning in context_layer.warnings)
Expand All @@ -1393,8 +1393,8 @@ def _describe_cloud_connector(
if with_direct_access_guidance:
try:
docs = connector.get_direct_access_guidance()
except (PyAirbyteError, requests.RequestException) as error:
warnings.append(f"Direct access docs are unavailable: {error}")
except (PyAirbyteError, requests.RequestException, ValueError):
warnings.append("Direct access docs are unavailable.")
else:
result.direct_access_guidance = render_connector_docs_result(docs)

Expand Down
107 changes: 105 additions & 2 deletions tests/unit_tests/test_cloud_describe.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@
from airbyte._direct_connectors import api_util as agents_api_util
from airbyte._direct_connectors import connector_docs
from airbyte.cloud import workspaces as cloud_workspaces
from airbyte.mcp import cloud as cloud_mcp
from airbyte.mcp._docs_results import (
render_connector_docs_result,
render_agent_skill_docs_result,
)
from airbyte.cloud.connections import CloudConnection
from airbyte.cloud.connectors import (
CloudConnector,
Expand Down Expand Up @@ -408,7 +413,9 @@ def test_get_direct_access_guidance_destination_not_enabled_adds_notice(
notice = connector_docs.SQL_PASSTHROUGH_NOT_ENABLED_NOTICE
guidance = destination.get_direct_access_guidance()
assert guidance.content[0] == {"type": "paragraph", "text": notice}
assert guidance.warnings == [notice]
assert guidance.metadata.warnings == [notice]
assert render_connector_docs_result(guidance).warnings == [notice]
assert render_agent_skill_docs_result(guidance).warnings == [notice]
assert guidance.outline == []
assert any(block.get("type") == "table" for block in guidance.content)
assert "execute_external_sql_query" not in _content_text(guidance)
Expand Down Expand Up @@ -545,7 +552,9 @@ def test_get_direct_access_guidance_destination_no_context_layer_notices(
guidance = destination.get_direct_access_guidance()
read_docs.assert_not_called()
assert guidance.content[0] == {"type": "paragraph", "text": notice}
assert guidance.warnings == [notice]
assert guidance.metadata.warnings == [notice]
assert render_connector_docs_result(guidance).warnings == [notice]
assert render_agent_skill_docs_result(guidance).warnings == [notice]
assert guidance.outline == []
assert "execute_external_sql_query" not in _content_text(guidance)
assert "SHOW TABLES" not in _content_text(guidance)
Expand Down Expand Up @@ -807,3 +816,97 @@ def test_is_feature_enabled_wrong_connector_kind_short_circuits(

assert connector.is_feature_enabled(feature) is False
get_features.assert_not_called()


@pytest.mark.parametrize("cache_path", ["fresh", "inspect"])
def test_describe_stringifies_structured_probe_warnings(
monkeypatch: pytest.MonkeyPatch, cache_path: str
) -> None:
workspace = _make_workspace(monkeypatch)
_patch_context_layer(monkeypatch)
_patch_enablement(monkeypatch)
warning = {"code": "partial", "message": "Partial metadata"}
response = {
**SKILL_DOCS_RESPONSE,
"metadata": {**SKILL_DOCS_RESPONSE["metadata"], "warnings": [warning]},
}
read_docs = MagicMock(return_value=response)
monkeypatch.setattr(agents_api_util, "read_cloud_skill_docs", read_docs)
source = _seed_source(workspace, "source-1", "GitHub")
source._connector_definition = SimpleNamespace(name="GitHub") # noqa: SLF001
if cache_path == "inspect":
source._context_layer_inspect(warnings=[]) # noqa: SLF001

result = cloud_mcp._describe_cloud_connector( # noqa: SLF001
source,
with_config=False,
with_replication_details=False,
with_direct_access_guidance=False,
with_data_replication_docs=False,
)

assert result.warnings == [str(warning)]
assert result.model_dump()["warnings"] == [str(warning)]
assert read_docs.call_count == 1


@pytest.mark.parametrize("with_guidance", [False, True])
def test_describe_malformed_docs_preserves_enabled_features(
monkeypatch: pytest.MonkeyPatch, with_guidance: bool
) -> None:
workspace = _make_workspace(monkeypatch)
_patch_context_layer(monkeypatch)
_patch_enablement(monkeypatch)
monkeypatch.setattr(agents_api_util, "read_cloud_skill_docs", lambda **_: {})
source = _seed_source(workspace, "source-1", "GitHub")
source._connector_definition = SimpleNamespace(name="GitHub") # noqa: SLF001

result = cloud_mcp._describe_cloud_connector( # noqa: SLF001
source,
with_config=False,
with_replication_details=False,
with_direct_access_guidance=with_guidance,
with_data_replication_docs=False,
)

assert result.enabled_features == sorted([
ConnectorFeature.DIRECT_ACCESS,
ConnectorFeature.DIRECT_API_QUERY,
])
assert result.warnings == [
"Connector direct-access docs lookup failed.",
*(["Direct access docs are unavailable."] if with_guidance else []),
]


@pytest.mark.parametrize("status", [403, 404])
def test_unavailable_docs_do_not_hide_enabled_features_or_leak_errors(
monkeypatch: pytest.MonkeyPatch, status: int
) -> None:
workspace = _make_workspace(monkeypatch)
_patch_context_layer(monkeypatch)
_patch_enablement(monkeypatch)
read_docs = MagicMock(
side_effect=AirbyteCloudApiError(
status_code=status, context={"response_text": "secret"}
)
)
monkeypatch.setattr(agents_api_util, "read_cloud_skill_docs", read_docs)
source = _seed_source(workspace, "source-1", "GitHub")
source._connector_definition = SimpleNamespace(name="GitHub") # noqa: SLF001

result = cloud_mcp._describe_cloud_connector( # noqa: SLF001
source,
with_config=False,
with_replication_details=False,
with_direct_access_guidance=False,
with_data_replication_docs=False,
)

assert result.enabled_features == sorted([
ConnectorFeature.DIRECT_ACCESS,
ConnectorFeature.DIRECT_API_QUERY,
])
assert result.warnings == [
"Connector direct-access docs are unavailable (access denied or not found)."
]
9 changes: 5 additions & 4 deletions tests/unit_tests/test_cloud_direct_execute.py
Original file line number Diff line number Diff line change
Expand Up @@ -1492,11 +1492,11 @@ def fail(**kwargs: Any) -> Any: # noqa: ANN401


@pytest.mark.parametrize("status_code", [403, 404])
def test_docs_probe_not_enabled_handling_unchanged(
def test_docs_probe_unavailable_emits_neutral_warning(
monkeypatch: pytest.MonkeyPatch,
status_code: int,
) -> None:
"""The skills-docs probe still reads a 403 or 404 as "not enabled", with a warning."""
"""Unavailable docs produce a warning without asserting disabled access."""
workspace = _make_workspace(monkeypatch)

def fail(**_: Any) -> Any: # noqa: ANN401
Expand All @@ -1507,5 +1507,6 @@ def fail(**_: Any) -> Any: # noqa: ANN401
warnings: list[str] = []

assert source._context_layer_inspect(warnings=warnings) is None # noqa: SLF001
assert len(warnings) == 1
assert "docs lookup failed" in warnings[0]
assert warnings == [
"Connector direct-access docs are unavailable (access denied or not found)."
]
Loading