feat: list source resources and prompts
This commit is contained in:
@@ -473,7 +473,7 @@ def test_builder_adds_explicit_end_node() -> None:
|
||||
assert builder.compile().outcomes == ["ok", "error"]
|
||||
|
||||
|
||||
class _StructuralKeyMap(Mapping[object, object]):
|
||||
class _StructuralKeyMap:
|
||||
def __getitem__(self, key: object) -> object:
|
||||
raise KeyError(key)
|
||||
|
||||
@@ -484,6 +484,8 @@ class _StructuralKeyMap(Mapping[object, object]):
|
||||
return 1
|
||||
|
||||
def items(self) -> list[tuple[dict[str, object], str]]:
|
||||
# Deliberately violates Mapping's item-view contract to exercise the
|
||||
# runtime guard for structural dict keys from malformed mappings.
|
||||
return [
|
||||
(
|
||||
{"root": "input", "parts": ["email.address"]},
|
||||
@@ -494,4 +496,4 @@ class _StructuralKeyMap(Mapping[object, object]):
|
||||
|
||||
def test_input_map_rejects_structural_dict_keys_with_clear_message() -> None:
|
||||
with pytest.raises(TypeError, match="structural path dicts cannot be map keys"):
|
||||
normalize_input_mapping(_StructuralKeyMap())
|
||||
normalize_input_mapping(cast(Mapping[object, object], _StructuralKeyMap()))
|
||||
|
||||
@@ -34,6 +34,34 @@ class BrokenSourceAdmin:
|
||||
raise RuntimeError(f"broken source admin for {source_id}")
|
||||
|
||||
|
||||
class InventorySourceAdmin:
|
||||
async def list_sources(
|
||||
self,
|
||||
*,
|
||||
cursor: str | None = None,
|
||||
limit: int = 50,
|
||||
) -> dict[str, Any]:
|
||||
return {"sources": [], "next_cursor": None, "total": 0}
|
||||
|
||||
async def inspect_source(self, *, source_id: str) -> dict[str, Any]:
|
||||
return {
|
||||
"id": source_id,
|
||||
"capabilities": {
|
||||
"resources": [
|
||||
f"{source_id}.architecture.md",
|
||||
f"{source_id}.startup.md",
|
||||
],
|
||||
"prompts": [
|
||||
f"{source_id}.simple-prompt",
|
||||
f"{source_id}.args-prompt",
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
async def diagnose_source(self, *, source_id: str) -> dict[str, Any]:
|
||||
return {"source_id": source_id, "status": "ok"}
|
||||
|
||||
|
||||
def test_load_cli_context_uses_rpc_client_for_rpc_http_target(tmp_path) -> None:
|
||||
config_path = tmp_path / "wf.json"
|
||||
config_path.write_text(
|
||||
@@ -408,6 +436,64 @@ def test_wf_source_commands_use_rpc_url_override(monkeypatch, tmp_path) -> None:
|
||||
assert '"id": "wf.std"' in inspected.output
|
||||
|
||||
|
||||
def test_wf_source_resources_and_prompts_render_names(monkeypatch, tmp_path) -> None:
|
||||
fake_context = CliContext(
|
||||
config_path=Path("dummy"),
|
||||
service=cast(Any, object()),
|
||||
handlers=build_local_static_workflow_server(tmp_path / "store").api,
|
||||
source_admin=InventorySourceAdmin(),
|
||||
admin=cast(Any, object()),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"wf_cli.commands.sources.load_cli_context_from_typer",
|
||||
lambda _ctx: fake_context,
|
||||
)
|
||||
runner = CliRunner()
|
||||
|
||||
resources = runner.invoke(app, ["source", "resources", "everything.default"])
|
||||
prompts = runner.invoke(app, ["source", "prompts", "everything.default"])
|
||||
|
||||
assert resources.exit_code == 0, resources.output
|
||||
assert resources.output.splitlines() == [
|
||||
"everything.default.architecture.md",
|
||||
"everything.default.startup.md",
|
||||
]
|
||||
assert prompts.exit_code == 0, prompts.output
|
||||
assert prompts.output.splitlines() == [
|
||||
"everything.default.simple-prompt",
|
||||
"everything.default.args-prompt",
|
||||
]
|
||||
|
||||
|
||||
def test_wf_source_resources_json_format(monkeypatch, tmp_path) -> None:
|
||||
fake_context = CliContext(
|
||||
config_path=Path("dummy"),
|
||||
service=cast(Any, object()),
|
||||
handlers=build_local_static_workflow_server(tmp_path / "store").api,
|
||||
source_admin=InventorySourceAdmin(),
|
||||
admin=cast(Any, object()),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"wf_cli.commands.sources.load_cli_context_from_typer",
|
||||
lambda _ctx: fake_context,
|
||||
)
|
||||
|
||||
result = CliRunner().invoke(
|
||||
app,
|
||||
["source", "resources", "everything.default", "--format", "json"],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
payload = json.loads(result.output)
|
||||
assert payload == {
|
||||
"source_id": "everything.default",
|
||||
"resources": [
|
||||
"everything.default.architecture.md",
|
||||
"everything.default.startup.md",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def test_wf_remote_source_inspect_formats_expected_rpc_error(
|
||||
monkeypatch,
|
||||
tmp_path,
|
||||
|
||||
@@ -98,6 +98,7 @@ def test_file_store_saves_and_loads_neutral_auth_record(tmp_path: Path) -> None:
|
||||
|
||||
loaded = store.load_auth_record("github.work")
|
||||
assert loaded is not None
|
||||
assert isinstance(loaded, NeutralAuthRecord)
|
||||
assert loaded.id == record.id
|
||||
assert loaded.scheme == record.scheme
|
||||
assert loaded.payload == record.payload
|
||||
|
||||
@@ -102,6 +102,7 @@ def test_file_store_accepts_neutral_auth_ref_without_connection_shape(
|
||||
|
||||
loaded = store.load_auth_record("api_ci-1")
|
||||
assert loaded is not None
|
||||
assert isinstance(loaded, NeutralAuthRecord)
|
||||
assert loaded.id == "api_ci-1"
|
||||
assert loaded.scheme == "bearer"
|
||||
assert loaded.payload == {"token": "secret"}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import AsyncIterator
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
@@ -156,17 +158,29 @@ class _FakeAdapter(McpSdkAdapter):
|
||||
def __init__(self, session: _FakeSession) -> None:
|
||||
self.fake_session = session
|
||||
|
||||
def _client(self, connection: McpSourceConnection, auth: object | None):
|
||||
@asynccontextmanager
|
||||
async def _client(
|
||||
self,
|
||||
connection: McpSourceConnection,
|
||||
auth: object | None,
|
||||
) -> AsyncIterator[McpSourceClient]:
|
||||
assert connection.id == "demo.personal"
|
||||
assert auth is None
|
||||
return _ClientContext(
|
||||
async with _ClientContext(
|
||||
McpSourceClient(session=self.fake_session, connection=connection)
|
||||
)
|
||||
) as client:
|
||||
yield client
|
||||
|
||||
|
||||
class _ExplodingClientAdapter(McpSdkAdapter):
|
||||
def _client(self, connection: McpSourceConnection, auth: object | None):
|
||||
@asynccontextmanager
|
||||
async def _client(
|
||||
self,
|
||||
connection: McpSourceConnection,
|
||||
auth: object | None,
|
||||
) -> AsyncIterator[McpSourceClient]:
|
||||
raise AssertionError("metadata lookup must not open an MCP session")
|
||||
yield # pragma: no cover
|
||||
|
||||
|
||||
def test_mcp_sdk_adapter_implements_backend_protocol() -> None:
|
||||
|
||||
Reference in New Issue
Block a user