fix: address source provider review notes
This commit is contained in:
@@ -73,12 +73,13 @@ Add a `kind: "python"` entry under `server.sources[]`:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`path` is resolved relative to the config file. It is important for console
|
`path` is resolved relative to the config file and added to `sys.path` before
|
||||||
scripts: `uv run python` and installed entrypoints do not always have the same
|
import. This makes the module discoverable whether you run through
|
||||||
import path.
|
`uv run python` or an installed entrypoint.
|
||||||
|
|
||||||
The source id prefixes local names. A node named `echo` becomes
|
The source id prefixes local names. A node named `echo` becomes
|
||||||
`local.ops.echo`. A node named `authoring.echo` is also exposed as
|
`local.ops.echo`. If a node uses the authoring namespace, such as
|
||||||
|
`authoring.echo`, that authoring prefix is replaced by the source id, producing
|
||||||
`local.ops.echo`.
|
`local.ops.echo`.
|
||||||
|
|
||||||
## 3. Validate And Start
|
## 3. Validate And Start
|
||||||
|
|||||||
@@ -214,6 +214,8 @@ class WorkflowRunApi:
|
|||||||
allowed = ", ".join(item.value for item in StoredRunStatus)
|
allowed = ", ".join(item.value for item in StoredRunStatus)
|
||||||
raise ValueError(f"status must be one of: {allowed}") from exc
|
raise ValueError(f"status must be one of: {allowed}") from exc
|
||||||
|
|
||||||
|
# File-backed v1 stores keep run listing simple by filtering/sorting in
|
||||||
|
# memory. Move this into store-level pagination if run counts grow large.
|
||||||
records = self._run_store().list_runs()
|
records = self._run_store().list_runs()
|
||||||
if status_filter is not None:
|
if status_filter is not None:
|
||||||
records = [record for record in records if record.status == status_filter]
|
records = [record for record in records if record.status == status_filter]
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ def list_runs(
|
|||||||
str | None,
|
str | None,
|
||||||
typer.Option(
|
typer.Option(
|
||||||
"--status",
|
"--status",
|
||||||
help="Filter by stopped status: completed, failed, or interrupted.",
|
help="Filter by stopped status: completed, failed, interrupted, or blocked.",
|
||||||
),
|
),
|
||||||
] = None,
|
] = None,
|
||||||
cursor: Annotated[
|
cursor: Annotated[
|
||||||
|
|||||||
@@ -14,11 +14,12 @@ from .models import (
|
|||||||
|
|
||||||
|
|
||||||
def load_workflow_config(path: str | Path) -> WorkflowConfigFile:
|
def load_workflow_config(path: str | Path) -> WorkflowConfigFile:
|
||||||
"""Load neutral workflow config and resolve local filesystem paths.
|
"""Load neutral workflow config and resolve local filesystem/source paths.
|
||||||
|
|
||||||
Relative filesystem store roots are config-file relative so `wf --config`
|
Relative filesystem store roots are config-file relative so `wf --config`
|
||||||
behaves the same regardless of the caller's current working directory.
|
behaves the same regardless of the caller's current working directory.
|
||||||
Role-specific store overrides follow the same rule.
|
Role-specific store overrides and Python source import paths follow the
|
||||||
|
same rule.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
config_path = Path(path)
|
config_path = Path(path)
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ from wf_core import ReducerSpec
|
|||||||
from wf_core.runtime.ops.merges import ReducerDefinition
|
from wf_core.runtime.ops.merges import ReducerDefinition
|
||||||
from wf_platform.refs import CapabilityRef
|
from wf_platform.refs import CapabilityRef
|
||||||
|
|
||||||
|
# Source kind is source-origin metadata, not a provider interface:
|
||||||
|
# system=built-ins, connection=upstream/stateful providers, python=trusted local code.
|
||||||
SourceKind = Literal["system", "connection", "python"]
|
SourceKind = Literal["system", "connection", "python"]
|
||||||
JsonObject = dict[str, Any]
|
JsonObject = dict[str, Any]
|
||||||
SOURCE_PREVIEW_LIMIT = 3
|
SOURCE_PREVIEW_LIMIT = 3
|
||||||
|
|||||||
@@ -35,8 +35,12 @@ def collect_static_sources(
|
|||||||
collected: dict[str, CapabilitySource] = {}
|
collected: dict[str, CapabilitySource] = {}
|
||||||
for provider in providers:
|
for provider in providers:
|
||||||
for source_id, source in provider.load_sources().items():
|
for source_id, source in provider.load_sources().items():
|
||||||
|
if source.id != source_id:
|
||||||
|
raise ValueError(
|
||||||
|
f"provider source key {source_id!r} does not match source id {source.id!r}"
|
||||||
|
)
|
||||||
if source_id in collected:
|
if source_id in collected:
|
||||||
raise ValueError(f"duplicate workflow source ids: {[source_id]}")
|
raise ValueError(f"duplicate workflow source ids: {source_id}")
|
||||||
collected[source_id] = source
|
collected[source_id] = source
|
||||||
return collected
|
return collected
|
||||||
|
|
||||||
|
|||||||
@@ -103,9 +103,7 @@ async def test_call_openapi_operation_maps_unexpected_status() -> None:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_call_openapi_operation_maps_invalid_request_to_validation_error() -> (
|
async def test_call_openapi_operation_maps_invalid_request_to_validation_error() -> None:
|
||||||
None
|
|
||||||
):
|
|
||||||
app = load_openapi_app(FIXTURE)
|
app = load_openapi_app(FIXTURE)
|
||||||
operation = next(
|
operation = next(
|
||||||
op for op in load_openapi_operations(FIXTURE) if op.name == "create_pet"
|
op for op in load_openapi_operations(FIXTURE) if op.name == "create_pet"
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ from .conftest import structured
|
|||||||
|
|
||||||
|
|
||||||
def test_proxy_admin_tools_mutate_config_file(tmp_path: Path) -> None:
|
def test_proxy_admin_tools_mutate_config_file(tmp_path: Path) -> None:
|
||||||
tmp_path = tmp_path / "proxy_admin_store"
|
test_root = tmp_path / "proxy_admin_store"
|
||||||
tmp_path.mkdir(parents=True, exist_ok=True)
|
test_root.mkdir(parents=True, exist_ok=True)
|
||||||
config_path = tmp_path / "wf_mcp.config.json"
|
config_path = test_root / "wf_mcp.config.json"
|
||||||
config_path.write_text(
|
config_path.write_text(
|
||||||
json.dumps(
|
json.dumps(
|
||||||
{
|
{
|
||||||
@@ -91,9 +91,9 @@ def test_proxy_admin_tools_mutate_config_file(tmp_path: Path) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_proxy_admin_reload_remounts_connections(tmp_path: Path) -> None:
|
def test_proxy_admin_reload_remounts_connections(tmp_path: Path) -> None:
|
||||||
tmp_path = tmp_path / "proxy_reload_store"
|
test_root = tmp_path / "proxy_reload_store"
|
||||||
tmp_path.mkdir(parents=True, exist_ok=True)
|
test_root.mkdir(parents=True, exist_ok=True)
|
||||||
config_path = tmp_path / "wf_mcp.config.json"
|
config_path = test_root / "wf_mcp.config.json"
|
||||||
config_path.write_text(
|
config_path.write_text(
|
||||||
json.dumps(
|
json.dumps(
|
||||||
{
|
{
|
||||||
@@ -132,9 +132,9 @@ def test_proxy_admin_reload_remounts_connections(tmp_path: Path) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_proxy_admin_reload_sends_list_changed_notifications(tmp_path: Path) -> None:
|
def test_proxy_admin_reload_sends_list_changed_notifications(tmp_path: Path) -> None:
|
||||||
tmp_path = tmp_path / "proxy_reload_notification_store"
|
test_root = tmp_path / "proxy_reload_notification_store"
|
||||||
tmp_path.mkdir(parents=True, exist_ok=True)
|
test_root.mkdir(parents=True, exist_ok=True)
|
||||||
config_path = tmp_path / "wf_mcp.config.json"
|
config_path = test_root / "wf_mcp.config.json"
|
||||||
config_path.write_text(
|
config_path.write_text(
|
||||||
json.dumps(
|
json.dumps(
|
||||||
{
|
{
|
||||||
@@ -166,9 +166,9 @@ def test_proxy_admin_reload_sends_list_changed_notifications(tmp_path: Path) ->
|
|||||||
|
|
||||||
|
|
||||||
def test_proxy_config_mutation_does_not_notify_before_reload(tmp_path: Path) -> None:
|
def test_proxy_config_mutation_does_not_notify_before_reload(tmp_path: Path) -> None:
|
||||||
tmp_path = tmp_path / "proxy_staged_notification_store"
|
test_root = tmp_path / "proxy_staged_notification_store"
|
||||||
tmp_path.mkdir(parents=True, exist_ok=True)
|
test_root.mkdir(parents=True, exist_ok=True)
|
||||||
config_path = tmp_path / "wf_mcp.config.json"
|
config_path = test_root / "wf_mcp.config.json"
|
||||||
config_path.write_text(
|
config_path.write_text(
|
||||||
json.dumps(
|
json.dumps(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -73,6 +73,21 @@ def test_static_source_provider_rejects_duplicate_source_ids() -> None:
|
|||||||
collect_static_sources([provider, FakeSourceProvider()])
|
collect_static_sources([provider, FakeSourceProvider()])
|
||||||
|
|
||||||
|
|
||||||
|
def test_static_source_provider_rejects_source_key_mismatch() -> None:
|
||||||
|
provider = StaticSourceProvider(
|
||||||
|
{
|
||||||
|
"fake.alias": CapabilitySource(
|
||||||
|
id="fake.ops",
|
||||||
|
kind="python",
|
||||||
|
capabilities=CapabilityBuckets(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="does not match source id"):
|
||||||
|
collect_static_sources([provider])
|
||||||
|
|
||||||
|
|
||||||
def test_build_workflow_server_from_workflow_config_uses_local_static_for_no_mcp_sources(
|
def test_build_workflow_server_from_workflow_config_uses_local_static_for_no_mcp_sources(
|
||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user