chore: address coderabbit cleanup notes
This commit is contained in:
@@ -150,6 +150,9 @@ implementation state.
|
||||
catalog snapshots are not mutated. Config-shadowed add is rejected in v1.
|
||||
Remove requires `--confirm` in CLI; local/static servers report
|
||||
unavailable.
|
||||
- Cleanup candidate: consolidate store/source registry id validation patterns
|
||||
(`SOURCE_REGISTRY_ID_PATTERN`, `STORE_ID_PATTERN`) only after another package
|
||||
needs the same rule. Today they intentionally stay close to their stores.
|
||||
- Longer term: make the MCP frontend an adapter over these neutral workflow,
|
||||
source-admin, and config-admin surfaces so the old `wf_mcp` server entry
|
||||
point can shrink or retire.
|
||||
|
||||
@@ -2,6 +2,16 @@
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
### Target Selection Precedence
|
||||
|
||||
1. `--url` CLI override selects an RPC HTTP target.
|
||||
2. `--local` CLI override selects the in-process local target.
|
||||
3. Config file `client.target` selects the configured target.
|
||||
4. Missing target config defaults to local.
|
||||
|
||||
CLI overrides intentionally win over config so one-off diagnostics can point at
|
||||
a different server without editing the config file.
|
||||
|
||||
**Goal:** Add neutral workflow config models and let selected `wf` CLI commands target either local execution or the JSON-RPC HTTP server.
|
||||
|
||||
**Architecture:** Introduce `wf_config` as the protocol-neutral config package. Keep existing `wf_mcp.config.json` loading for compatibility, but add a new `wf.json`-style shape with `client.target`, `server.store`, `server.transports`, and bootstrap `server.sources`. Put the JSON-RPC client adapter in `wf_transport_rpc_http.client`; CLI context chooses local `WorkflowApi` or remote RPC adapter based on config plus CLI overrides.
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
# CodeRabbit Cleanup Follow-Ups Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Resolve non-blocking cleanup findings from the June 4 CodeRabbit review without mixing broad refactors into the focused correctness patch.
|
||||
|
||||
**Architecture:** Keep behavior unchanged unless a task explicitly says otherwise. Prefer documentation, small helpers, and narrowly scoped tests over cross-package refactors. Do not move modules or rename public APIs in this follow-up.
|
||||
|
||||
**Tech Stack:** Python 3.14, Pydantic v2, Typer, FastAPI JSON-RPC, pytest, ruff, basedpyright.
|
||||
|
||||
---
|
||||
|
||||
## Scope
|
||||
|
||||
This plan intentionally excludes the already-fixed review items:
|
||||
|
||||
- CLI config exception tuple syntax in `src/wf_cli/context.py`
|
||||
- CLI source-registry file JSON object validation and exception chaining
|
||||
- RPC method module structure-test coverage
|
||||
- stale long-lived API and CLI/API alignment doc statuses
|
||||
- typed `tmp_path` annotations in `tests/wf_config/test_config_models.py`
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Document Source-Registry Mutation Asymmetry
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/superpowers/plans/2026-06-04-source-registry-mutations.md`
|
||||
|
||||
- [ ] **Step 1: Add rationale under shadow handling**
|
||||
|
||||
Find the section mentioning:
|
||||
|
||||
```markdown
|
||||
A future `allow_shadow` flag can relax add; do not add it in this slice.
|
||||
```
|
||||
|
||||
Append:
|
||||
|
||||
```markdown
|
||||
Rationale: `add` rejects config-shadowed ids to prevent silent no-ops: adding a
|
||||
registry entry that cannot activate while config owns the same id. Existing
|
||||
shadowed registry entries can still be updated, enabled, disabled, or removed so
|
||||
operators can prepare store state for config removal or future `seed` ownership
|
||||
policy.
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify doc diff**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git diff -- docs/superpowers/plans/2026-06-04-source-registry-mutations.md
|
||||
```
|
||||
|
||||
Expected: only the rationale paragraph changed.
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Clarify RPC Target Selection Precedence
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/superpowers/plans/2026-06-03-workflow-config-and-rpc-cli-target.md`
|
||||
|
||||
- [ ] **Step 1: Add precedence table near the plan introduction**
|
||||
|
||||
Add:
|
||||
|
||||
```markdown
|
||||
### Target Selection Precedence
|
||||
|
||||
1. `--url` CLI override selects an RPC HTTP target.
|
||||
2. `--local` CLI override selects the in-process local target.
|
||||
3. Config file `client.target` selects the configured target.
|
||||
4. Missing target config defaults to local.
|
||||
|
||||
CLI overrides intentionally win over config so one-off diagnostics can point at
|
||||
a different server without editing the config file.
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify no code references are changed**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git diff --stat
|
||||
```
|
||||
|
||||
Expected: only the plan document is changed by this task.
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Extract Source-Registry RPC Availability Helper
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/wf_transport_rpc_http/methods_source_registry.py`
|
||||
- Test: `tests/wf_transport_rpc_http/test_source_registry_rpc.py`
|
||||
|
||||
- [ ] **Step 1: Add a helper**
|
||||
|
||||
In `src/wf_transport_rpc_http/methods_source_registry.py`, add:
|
||||
|
||||
```python
|
||||
def _require_source_registry_admin(
|
||||
server: WorkflowServer,
|
||||
*,
|
||||
operation: str,
|
||||
) -> WorkflowSourceRegistrySurface:
|
||||
admin = server.source_registry_admin
|
||||
if admin is None:
|
||||
raise WorkflowRpcError(
|
||||
data={
|
||||
"code": "source_registry_unavailable",
|
||||
"message": (
|
||||
f"source registry admin {operation} are not available "
|
||||
"for this server"
|
||||
),
|
||||
}
|
||||
)
|
||||
return admin
|
||||
```
|
||||
|
||||
Import `WorkflowSourceRegistrySurface` from `wf_api` if needed.
|
||||
|
||||
- [ ] **Step 2: Replace repeated `None` checks**
|
||||
|
||||
Use:
|
||||
|
||||
```python
|
||||
admin = _require_source_registry_admin(server, operation="reads")
|
||||
```
|
||||
|
||||
for list/inspect, and:
|
||||
|
||||
```python
|
||||
admin = _require_source_registry_admin(server, operation="mutations")
|
||||
```
|
||||
|
||||
for add/update/enable/disable/remove.
|
||||
|
||||
- [ ] **Step 3: Run source-registry RPC tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
uv run pytest tests/wf_transport_rpc_http/test_source_registry_rpc.py -q
|
||||
```
|
||||
|
||||
Expected: all tests pass.
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Clarify Chronological Event Ordering
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/wf_api/admin.py`
|
||||
- Test: existing admin API tests if present
|
||||
|
||||
- [ ] **Step 1: Add a comment above `list_events`**
|
||||
|
||||
Add a short comment/docstring note near `WorkflowAdminApi.list_events`:
|
||||
|
||||
```python
|
||||
# Preserve provider order for events; event providers are expected to return
|
||||
# chronological order and callers may rely on that ordering for diagnostics.
|
||||
```
|
||||
|
||||
Do not sort event payloads in this task.
|
||||
|
||||
- [ ] **Step 2: Run admin API tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
uv run pytest tests/wf_api -q
|
||||
```
|
||||
|
||||
Expected: all `wf_api` tests pass.
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Record Shared ID Pattern Follow-Up
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/current_roadmap.md`
|
||||
|
||||
- [ ] **Step 1: Add a small platform cleanup bullet**
|
||||
|
||||
Under the platform cleanup/architecture section, add:
|
||||
|
||||
```markdown
|
||||
- Cleanup candidate: consolidate store/source registry id validation patterns
|
||||
(`SOURCE_REGISTRY_ID_PATTERN`, `STORE_ID_PATTERN`) only after another package
|
||||
needs the same rule. Today they intentionally stay close to their stores.
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify docs only**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git diff -- docs/current_roadmap.md
|
||||
```
|
||||
|
||||
Expected: only the cleanup bullet changed.
|
||||
|
||||
---
|
||||
|
||||
## Final Verification
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
uv run pytest tests/wf_transport_rpc_http/test_source_registry_rpc.py tests/wf_api -q
|
||||
uv run ruff check src/wf_transport_rpc_http/methods_source_registry.py src/wf_api/admin.py docs/superpowers/plans/2026-06-04-source-registry-mutations.md docs/superpowers/plans/2026-06-03-workflow-config-and-rpc-cli-target.md docs/current_roadmap.md
|
||||
uv run basedpyright --level error src/wf_transport_rpc_http/methods_source_registry.py src/wf_api/admin.py
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Expected: pytest exits 0, ruff exits 0, basedpyright exits 0, and `git diff --check` reports no whitespace errors.
|
||||
@@ -42,6 +42,12 @@ For mutation v1:
|
||||
- `update`, `enable`, `disable`, and `remove` may operate on existing registry entries even if they are currently shadowed by config.
|
||||
- A future `allow_shadow` flag can relax `add`; do not add it in this slice.
|
||||
|
||||
Rationale: `add` rejects config-shadowed ids to prevent silent no-ops: adding a
|
||||
registry entry that cannot activate while config owns the same id. Existing
|
||||
shadowed registry entries can still be updated, enabled, disabled, or removed so
|
||||
operators can prepare store state for config removal or future `seed` ownership
|
||||
policy.
|
||||
|
||||
### Full-Registry Validation
|
||||
|
||||
Every mutation must:
|
||||
|
||||
@@ -85,8 +85,11 @@ surface, or plain local CLI utilities.
|
||||
and `wf source list` / `wf source inspect`.
|
||||
- Read-only admin/config operations are now available through JSON-RPC HTTP
|
||||
and `wf admin connections`, `wf admin statuses`, and `wf admin events`.
|
||||
- Next source work is persistence for server-owned dynamic source changes.
|
||||
- Keep mutation out until the store-backed source registry is designed.
|
||||
- Source registry mutations (`add` / `update` / `enable` / `disable` /
|
||||
`remove`) are now implemented for server-owned dynamic source changes.
|
||||
- Remaining source-registry work is migration policy: config can bootstrap
|
||||
or lock sources, while the store-backed registry owns mutable desired
|
||||
state for dynamic sources.
|
||||
|
||||
2. **Mutable source/admin commands**
|
||||
- Config can bootstrap sources, but server-owned dynamic source changes
|
||||
|
||||
@@ -2,7 +2,12 @@
|
||||
|
||||
Date: 2026-06-03
|
||||
|
||||
Status: design spec; implementation not started
|
||||
Status: Slices 1-3 implemented. `wf_server` provides
|
||||
`build_local_static_workflow_server`, `wf_transport_rpc_http` provides JSON-RPC
|
||||
methods and client support, `wf_cli` has target-aware context, and `wf_config`
|
||||
owns neutral config models. WebSocket transport, source providers, auth,
|
||||
streaming/progress, database backend, and live MCP source management remain
|
||||
future work.
|
||||
|
||||
Related:
|
||||
|
||||
@@ -92,12 +97,14 @@ It should prove:
|
||||
|
||||
Implementation status:
|
||||
|
||||
- `wf_server.build_local_static_workflow_server()` constructs a durable
|
||||
- Slice 1 complete: `wf_server.build_local_static_workflow_server()` constructs a durable
|
||||
`WorkflowApi` with required file-backed stores, local `wf.std`/`wf.recipes`
|
||||
sources, and a local runtime runner.
|
||||
- This first slice has no transport adapter. Clients still call the in-process
|
||||
`WorkflowApi` in tests; HTTP/JSON-RPC/WebSocket/MCP transport adapters are
|
||||
later slices.
|
||||
- Slice 2 complete: `wf_transport_rpc_http` provides JSON-RPC 2.0 over HTTP via
|
||||
`create_rpc_app(server)` and the `wf-rpc-server` CLI.
|
||||
- Slice 3 complete: `wf_cli` supports target-aware context with `--local`,
|
||||
`--url`, and `--timeout` overrides, and works with remote RPC targets for
|
||||
capability and run commands.
|
||||
|
||||
First slice should not include:
|
||||
|
||||
@@ -279,10 +286,12 @@ Implementation status:
|
||||
|
||||
- `wf_transport_rpc_http.create_rpc_app(server)` exposes a fixed JSON-RPC
|
||||
method set over an existing `wf_server.WorkflowServer`.
|
||||
- `wf-rpc-server --store-root <path>` starts the local/static server over
|
||||
`/rpc`.
|
||||
- This slice still does not include remote `wf` CLI targeting, auth,
|
||||
streaming/progress, or live upstream MCP source management.
|
||||
- `wf-rpc-server --store-root <path>` and `wf-rpc-server --config <path>` start
|
||||
the local/static server over `/rpc`.
|
||||
- Remote `wf` CLI targeting is implemented through `wf_config` and target-aware
|
||||
context in `wf_cli`.
|
||||
- Auth, streaming/progress, and live upstream MCP source management remain
|
||||
future work.
|
||||
|
||||
Preferred implementation dependency:
|
||||
|
||||
|
||||
@@ -49,6 +49,8 @@ class WorkflowAdminApi:
|
||||
)
|
||||
return {"statuses": statuses, "total": len(statuses)}
|
||||
|
||||
# Preserve provider order for events; event providers are expected to return
|
||||
# chronological order and callers may rely on that ordering for diagnostics.
|
||||
async def list_events(self) -> dict[str, Any]:
|
||||
events = [_payload(event) for event in self.events.list_events()]
|
||||
return {"events": events, "total": len(events)}
|
||||
|
||||
@@ -19,7 +19,9 @@ class WorkflowSourceRegistryProvider(Protocol):
|
||||
class WorkflowSourceRegistryMutationProvider(Protocol):
|
||||
"""Write capabilities for source registry mutation operations."""
|
||||
|
||||
def add_registry_entry(self, entry: Mapping[str, Any]) -> Mapping[str, Any] | object: ...
|
||||
def add_registry_entry(
|
||||
self, entry: Mapping[str, Any]
|
||||
) -> Mapping[str, Any] | object: ...
|
||||
def update_registry_entry(
|
||||
self, source_id: str, patch: Mapping[str, Any]
|
||||
) -> Mapping[str, Any] | object: ...
|
||||
|
||||
@@ -161,8 +161,8 @@ def _read_json_arg(
|
||||
if file_path:
|
||||
try:
|
||||
value = json.loads(Path(file_path).read_text(encoding="utf-8"))
|
||||
except FileNotFoundError:
|
||||
raise typer.BadParameter(f"file not found: {file_path}")
|
||||
except FileNotFoundError as exc:
|
||||
raise typer.BadParameter(f"file not found: {file_path}") from exc
|
||||
except json.JSONDecodeError as exc:
|
||||
raise typer.BadParameter(f"invalid JSON in file: {exc}") from exc
|
||||
return _require_json_object(value, flag_names)
|
||||
|
||||
@@ -258,7 +258,7 @@ def _rpc_timeout_from_optional_config(
|
||||
return override
|
||||
try:
|
||||
config = load_workflow_config(path)
|
||||
except FileNotFoundError, json.JSONDecodeError, ValidationError:
|
||||
except (FileNotFoundError, json.JSONDecodeError, ValidationError):
|
||||
return 30.0
|
||||
target = config.client.target
|
||||
if isinstance(target, RpcHttpTargetConfig):
|
||||
|
||||
@@ -3,7 +3,14 @@ from __future__ import annotations
|
||||
from pathlib import Path
|
||||
from typing import Annotated, Literal
|
||||
|
||||
from pydantic import AnyHttpUrl, BaseModel, ConfigDict, Field, field_validator, model_validator
|
||||
from pydantic import (
|
||||
AnyHttpUrl,
|
||||
BaseModel,
|
||||
ConfigDict,
|
||||
Field,
|
||||
field_validator,
|
||||
model_validator,
|
||||
)
|
||||
|
||||
|
||||
class WorkflowConfigModel(BaseModel):
|
||||
|
||||
@@ -7,7 +7,11 @@ from typing import Any
|
||||
from wf_api.source_registry_admin import WorkflowSourceRegistryMutationProvider
|
||||
|
||||
from ...models import ConnectionConfig
|
||||
from ...source_registry import McpSourceRegistryEntry, SourceRegistryFile, SourceRegistryStore
|
||||
from ...source_registry import (
|
||||
McpSourceRegistryEntry,
|
||||
SourceRegistryFile,
|
||||
SourceRegistryStore,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
@@ -38,7 +42,9 @@ class SourceRegistryAdminProvider(WorkflowSourceRegistryMutationProvider):
|
||||
registry = SourceRegistryFile(sources=sources)
|
||||
self.source_registry_store.save_registry(registry)
|
||||
|
||||
def _entry_map(self, registry: SourceRegistryFile) -> dict[str, McpSourceRegistryEntry]:
|
||||
def _entry_map(
|
||||
self, registry: SourceRegistryFile
|
||||
) -> dict[str, McpSourceRegistryEntry]:
|
||||
return registry.source_map()
|
||||
|
||||
def _require_entry(self, source_id: str) -> McpSourceRegistryEntry:
|
||||
@@ -80,7 +86,9 @@ class SourceRegistryAdminProvider(WorkflowSourceRegistryMutationProvider):
|
||||
self._save(sources)
|
||||
return updated
|
||||
|
||||
def set_registry_entry_enabled(self, source_id: str, enabled: bool) -> McpSourceRegistryEntry:
|
||||
def set_registry_entry_enabled(
|
||||
self, source_id: str, enabled: bool
|
||||
) -> McpSourceRegistryEntry:
|
||||
existing = self._require_entry(source_id)
|
||||
updated = existing.model_copy(update={"enabled": enabled})
|
||||
registry = self._load()
|
||||
|
||||
@@ -13,7 +13,9 @@ from .methods_capabilities import register_methods as register_capability_method
|
||||
from .methods_deployments import register_methods as register_deployment_methods
|
||||
from .methods_drafts import register_methods as register_draft_methods
|
||||
from .methods_runs import register_methods as register_run_methods
|
||||
from .methods_source_registry import register_methods as register_source_registry_methods
|
||||
from .methods_source_registry import (
|
||||
register_methods as register_source_registry_methods,
|
||||
)
|
||||
from .methods_sources import register_methods as register_source_methods
|
||||
|
||||
|
||||
|
||||
@@ -9,7 +9,11 @@ import httpx
|
||||
|
||||
@dataclass(slots=True)
|
||||
class RpcClientTransport:
|
||||
"""Shared JSON-RPC request plumbing for workflow RPC client mixins."""
|
||||
"""Shared JSON-RPC request plumbing for workflow RPC client mixins.
|
||||
|
||||
If `http_client` is provided, that client's own timeout configuration wins;
|
||||
`timeout_seconds` is only used when this transport creates an `AsyncClient`.
|
||||
"""
|
||||
|
||||
url: str
|
||||
timeout_seconds: float = 30.0
|
||||
|
||||
@@ -6,6 +6,7 @@ from fastapi import Body
|
||||
import fastapi_jsonrpc as jsonrpc
|
||||
from fastapi_jsonrpc import Params
|
||||
|
||||
from wf_api import WorkflowSourceRegistrySurface
|
||||
from wf_server import WorkflowServer
|
||||
|
||||
from .errors import WorkflowRpcError, raise_workflow_rpc_error
|
||||
@@ -18,6 +19,25 @@ from .models import (
|
||||
)
|
||||
|
||||
|
||||
def _require_source_registry_admin(
|
||||
server: WorkflowServer,
|
||||
*,
|
||||
operation: str,
|
||||
) -> WorkflowSourceRegistrySurface:
|
||||
admin = server.source_registry_admin
|
||||
if admin is None:
|
||||
raise WorkflowRpcError(
|
||||
data={
|
||||
"code": "source_registry_unavailable",
|
||||
"message": (
|
||||
f"source registry admin {operation} are not available "
|
||||
"for this server"
|
||||
),
|
||||
}
|
||||
)
|
||||
return admin
|
||||
|
||||
|
||||
def register_methods(
|
||||
entrypoint: jsonrpc.Entrypoint,
|
||||
server: WorkflowServer,
|
||||
@@ -33,15 +53,9 @@ def register_methods(
|
||||
default_factory=ListRegistryEntriesParams,
|
||||
),
|
||||
) -> dict[str, Any]:
|
||||
if server.source_registry_admin is None:
|
||||
raise WorkflowRpcError(
|
||||
data={
|
||||
"code": "source_registry_unavailable",
|
||||
"message": "source registry admin reads are not available for this server",
|
||||
}
|
||||
)
|
||||
admin = _require_source_registry_admin(server, operation="reads")
|
||||
try:
|
||||
return await server.source_registry_admin.list_registry_entries(
|
||||
return await admin.list_registry_entries(
|
||||
cursor=params.cursor,
|
||||
limit=params.limit,
|
||||
)
|
||||
@@ -55,15 +69,9 @@ def register_methods(
|
||||
async def workflow_admin_source_registry_inspect(
|
||||
params: InspectRegistryEntryParams = Params(...), # type: ignore[reportArgumentType]
|
||||
) -> dict[str, Any]:
|
||||
if server.source_registry_admin is None:
|
||||
raise WorkflowRpcError(
|
||||
data={
|
||||
"code": "source_registry_unavailable",
|
||||
"message": "source registry admin reads are not available for this server",
|
||||
}
|
||||
)
|
||||
admin = _require_source_registry_admin(server, operation="reads")
|
||||
try:
|
||||
return await server.source_registry_admin.inspect_registry_entry(
|
||||
return await admin.inspect_registry_entry(
|
||||
source_id=params.source_id,
|
||||
)
|
||||
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||
@@ -76,15 +84,9 @@ def register_methods(
|
||||
async def workflow_admin_source_registry_add(
|
||||
params: AddRegistryEntryParams = Params(...), # type: ignore[reportArgumentType]
|
||||
) -> dict[str, Any]:
|
||||
if server.source_registry_admin is None:
|
||||
raise WorkflowRpcError(
|
||||
data={
|
||||
"code": "source_registry_unavailable",
|
||||
"message": "source registry admin mutations are not available for this server",
|
||||
}
|
||||
)
|
||||
admin = _require_source_registry_admin(server, operation="mutations")
|
||||
try:
|
||||
return await server.source_registry_admin.add_registry_entry(
|
||||
return await admin.add_registry_entry(
|
||||
entry=params.entry,
|
||||
)
|
||||
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||
@@ -97,15 +99,9 @@ def register_methods(
|
||||
async def workflow_admin_source_registry_update(
|
||||
params: UpdateRegistryEntryParams = Params(...), # type: ignore[reportArgumentType]
|
||||
) -> dict[str, Any]:
|
||||
if server.source_registry_admin is None:
|
||||
raise WorkflowRpcError(
|
||||
data={
|
||||
"code": "source_registry_unavailable",
|
||||
"message": "source registry admin mutations are not available for this server",
|
||||
}
|
||||
)
|
||||
admin = _require_source_registry_admin(server, operation="mutations")
|
||||
try:
|
||||
return await server.source_registry_admin.update_registry_entry(
|
||||
return await admin.update_registry_entry(
|
||||
source_id=params.source_id,
|
||||
patch=params.patch,
|
||||
)
|
||||
@@ -119,15 +115,9 @@ def register_methods(
|
||||
async def workflow_admin_source_registry_enable(
|
||||
params: RegistryEntryIdParams = Params(...), # type: ignore[reportArgumentType]
|
||||
) -> dict[str, Any]:
|
||||
if server.source_registry_admin is None:
|
||||
raise WorkflowRpcError(
|
||||
data={
|
||||
"code": "source_registry_unavailable",
|
||||
"message": "source registry admin mutations are not available for this server",
|
||||
}
|
||||
)
|
||||
admin = _require_source_registry_admin(server, operation="mutations")
|
||||
try:
|
||||
return await server.source_registry_admin.enable_registry_entry(
|
||||
return await admin.enable_registry_entry(
|
||||
source_id=params.source_id,
|
||||
)
|
||||
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||
@@ -140,15 +130,9 @@ def register_methods(
|
||||
async def workflow_admin_source_registry_disable(
|
||||
params: RegistryEntryIdParams = Params(...), # type: ignore[reportArgumentType]
|
||||
) -> dict[str, Any]:
|
||||
if server.source_registry_admin is None:
|
||||
raise WorkflowRpcError(
|
||||
data={
|
||||
"code": "source_registry_unavailable",
|
||||
"message": "source registry admin mutations are not available for this server",
|
||||
}
|
||||
)
|
||||
admin = _require_source_registry_admin(server, operation="mutations")
|
||||
try:
|
||||
return await server.source_registry_admin.disable_registry_entry(
|
||||
return await admin.disable_registry_entry(
|
||||
source_id=params.source_id,
|
||||
)
|
||||
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||
@@ -161,15 +145,9 @@ def register_methods(
|
||||
async def workflow_admin_source_registry_remove(
|
||||
params: RegistryEntryIdParams = Params(...), # type: ignore[reportArgumentType]
|
||||
) -> dict[str, Any]:
|
||||
if server.source_registry_admin is None:
|
||||
raise WorkflowRpcError(
|
||||
data={
|
||||
"code": "source_registry_unavailable",
|
||||
"message": "source registry admin mutations are not available for this server",
|
||||
}
|
||||
)
|
||||
admin = _require_source_registry_admin(server, operation="mutations")
|
||||
try:
|
||||
return await server.source_registry_admin.remove_registry_entry(
|
||||
return await admin.remove_registry_entry(
|
||||
source_id=params.source_id,
|
||||
)
|
||||
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||
|
||||
@@ -93,7 +93,9 @@ def test_list_pagination() -> None:
|
||||
)
|
||||
|
||||
first = asyncio.run(api.list_registry_entries(limit=2))
|
||||
second = asyncio.run(api.list_registry_entries(cursor=first["next_cursor"], limit=2))
|
||||
second = asyncio.run(
|
||||
api.list_registry_entries(cursor=first["next_cursor"], limit=2)
|
||||
)
|
||||
|
||||
assert [e["id"] for e in first["entries"]] == ["a", "b"]
|
||||
assert first["next_cursor"] == "2"
|
||||
@@ -162,7 +164,9 @@ class FakeMutationProvider:
|
||||
self._entries.append(fe)
|
||||
return asdict(fe)
|
||||
|
||||
def update_registry_entry(self, source_id: str, patch: Mapping[str, Any]) -> dict[str, Any]:
|
||||
def update_registry_entry(
|
||||
self, source_id: str, patch: Mapping[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
for i, e in enumerate(self._entries):
|
||||
if e.id == source_id:
|
||||
merged = asdict(e)
|
||||
@@ -171,7 +175,9 @@ class FakeMutationProvider:
|
||||
return merged
|
||||
raise KeyError(source_id)
|
||||
|
||||
def set_registry_entry_enabled(self, source_id: str, enabled: bool) -> dict[str, Any]:
|
||||
def set_registry_entry_enabled(
|
||||
self, source_id: str, enabled: bool
|
||||
) -> dict[str, Any]:
|
||||
for i, e in enumerate(self._entries):
|
||||
if e.id == source_id:
|
||||
merged = asdict(e)
|
||||
@@ -193,12 +199,23 @@ def _mutation_api(
|
||||
) -> tuple[WorkflowSourceRegistryApi, FakeMutationProvider]:
|
||||
provider = FakeRegistryProvider(list(entries) if entries else [], config_ids)
|
||||
mutation = FakeMutationProvider(list(entries) if entries else [])
|
||||
return WorkflowSourceRegistryApi(provider=provider, mutation_provider=mutation), mutation
|
||||
return WorkflowSourceRegistryApi(
|
||||
provider=provider, mutation_provider=mutation
|
||||
), mutation
|
||||
|
||||
|
||||
def test_add_registry_entry() -> None:
|
||||
api, _ = _mutation_api()
|
||||
new_entry = {"id": "new.source", "kind": "mcp", "enabled": True, "provider": "new", "account": "default", "profile": None, "transport": {"kind": "stdio"}, "auth_ref": None}
|
||||
new_entry = {
|
||||
"id": "new.source",
|
||||
"kind": "mcp",
|
||||
"enabled": True,
|
||||
"provider": "new",
|
||||
"account": "default",
|
||||
"profile": None,
|
||||
"transport": {"kind": "stdio"},
|
||||
"auth_ref": None,
|
||||
}
|
||||
payload = asyncio.run(api.add_registry_entry(entry=new_entry))
|
||||
|
||||
assert payload["entry"]["id"] == "new.source"
|
||||
@@ -208,7 +225,16 @@ def test_add_registry_entry() -> None:
|
||||
|
||||
def test_add_registry_entry_shadowed() -> None:
|
||||
api, _ = _mutation_api(config_ids={"new.source"})
|
||||
new_entry = {"id": "new.source", "kind": "mcp", "enabled": True, "provider": "new", "account": "default", "profile": None, "transport": {"kind": "stdio"}, "auth_ref": None}
|
||||
new_entry = {
|
||||
"id": "new.source",
|
||||
"kind": "mcp",
|
||||
"enabled": True,
|
||||
"provider": "new",
|
||||
"account": "default",
|
||||
"profile": None,
|
||||
"transport": {"kind": "stdio"},
|
||||
"auth_ref": None,
|
||||
}
|
||||
payload = asyncio.run(api.add_registry_entry(entry=new_entry))
|
||||
|
||||
assert payload["entry"]["id"] == "new.source"
|
||||
@@ -219,7 +245,9 @@ def test_update_registry_entry() -> None:
|
||||
api, _ = _mutation_api(
|
||||
entries=[FakeRegistryEntry(id="upd.source", provider="old")],
|
||||
)
|
||||
payload = asyncio.run(api.update_registry_entry(source_id="upd.source", patch={"provider": "new"}))
|
||||
payload = asyncio.run(
|
||||
api.update_registry_entry(source_id="upd.source", patch={"provider": "new"})
|
||||
)
|
||||
|
||||
assert payload["entry"]["id"] == "upd.source"
|
||||
assert payload["entry"]["provider"] == "new"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
@@ -113,7 +114,7 @@ def test_workflow_config_rejects_unwired_stdlib_source_id() -> None:
|
||||
|
||||
|
||||
def test_load_workflow_config_resolves_filesystem_store_relative_to_config(
|
||||
tmp_path,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
config_path = tmp_path / "wf.json"
|
||||
config_path.write_text(
|
||||
@@ -133,7 +134,9 @@ def test_load_workflow_config_resolves_filesystem_store_relative_to_config(
|
||||
assert config.server.store.root == (tmp_path / ".wf_store").resolve()
|
||||
|
||||
|
||||
def test_load_workflow_config_preserves_absolute_filesystem_store(tmp_path) -> None:
|
||||
def test_load_workflow_config_preserves_absolute_filesystem_store(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
absolute_root = (tmp_path / "absolute-store").resolve()
|
||||
config_path = tmp_path / "wf.json"
|
||||
config_path.write_text(
|
||||
|
||||
@@ -214,9 +214,7 @@ def test_server_reload_preserves_source_registry_connections() -> None:
|
||||
client = create_server_client(config, config_path=config_path)
|
||||
async with client:
|
||||
before = await client.call_tool("wf.admin.list_sources", {"limit": 100})
|
||||
before_ids = {
|
||||
source["id"] for source in structured(before)["sources"]
|
||||
}
|
||||
before_ids = {source["id"] for source in structured(before)["sources"]}
|
||||
assert "fixture.registry" in before_ids
|
||||
|
||||
await client.call_tool("wf.admin.reload_config")
|
||||
|
||||
@@ -241,7 +241,9 @@ def test_connection_service_sync_config_shadows_registry_entry() -> None:
|
||||
)
|
||||
|
||||
|
||||
def test_connection_service_sync_registry_disabled_entry_hydrates_disabled_source() -> None:
|
||||
def test_connection_service_sync_registry_disabled_entry_hydrates_disabled_source() -> (
|
||||
None
|
||||
):
|
||||
service = ConnectionService(events=BrokerEventRecorder(EventBus()))
|
||||
catalog = _source_catalog(service)
|
||||
store = FileSourceRegistryStore(local_temp_root() / "registry_disabled")
|
||||
|
||||
@@ -14,13 +14,17 @@ from wf_mcp.source_registry import (
|
||||
)
|
||||
|
||||
|
||||
def _store_with_entries(root: Path, *entries: McpSourceRegistryEntry) -> FileSourceRegistryStore:
|
||||
def _store_with_entries(
|
||||
root: Path, *entries: McpSourceRegistryEntry
|
||||
) -> FileSourceRegistryStore:
|
||||
store = FileSourceRegistryStore(root)
|
||||
store.save_registry(SourceRegistryFile(sources=list(entries)))
|
||||
return store
|
||||
|
||||
|
||||
def _entry(source_id: str, *, provider: str = "github", account: str = "work") -> McpSourceRegistryEntry:
|
||||
def _entry(
|
||||
source_id: str, *, provider: str = "github", account: str = "work"
|
||||
) -> McpSourceRegistryEntry:
|
||||
return McpSourceRegistryEntry(
|
||||
id=source_id,
|
||||
provider=provider,
|
||||
@@ -29,7 +33,9 @@ def _entry(source_id: str, *, provider: str = "github", account: str = "work") -
|
||||
)
|
||||
|
||||
|
||||
def _entry_dict(source_id: str, *, provider: str = "github", account: str = "work") -> dict:
|
||||
def _entry_dict(
|
||||
source_id: str, *, provider: str = "github", account: str = "work"
|
||||
) -> dict:
|
||||
return {
|
||||
"id": source_id,
|
||||
"provider": provider,
|
||||
@@ -44,8 +50,13 @@ def _provider(
|
||||
config_ids: frozenset[str] | None = None,
|
||||
) -> SourceRegistryAdminProvider:
|
||||
store = _store_with_entries(tmp_path / "reg", *(entries or []))
|
||||
connections = [ConnectionConfig(id=cid, server="s", account="a") for cid in (config_ids or frozenset())]
|
||||
return SourceRegistryAdminProvider(source_registry_store=store, config_connections=connections)
|
||||
connections = [
|
||||
ConnectionConfig(id=cid, server="s", account="a")
|
||||
for cid in (config_ids or frozenset())
|
||||
]
|
||||
return SourceRegistryAdminProvider(
|
||||
source_registry_store=store, config_connections=connections
|
||||
)
|
||||
|
||||
|
||||
# -- read tests ------------------------------------------------------------
|
||||
@@ -197,7 +208,9 @@ def test_enable_disable_missing_source_raises_key_error(tmp_path: Path) -> None:
|
||||
|
||||
|
||||
def test_remove_persists_absence_and_does_not_touch_unrelated(tmp_path: Path) -> None:
|
||||
provider = _provider(tmp_path, entries=[_entry("keep.server"), _entry("drop.server")])
|
||||
provider = _provider(
|
||||
tmp_path, entries=[_entry("keep.server"), _entry("drop.server")]
|
||||
)
|
||||
|
||||
result = provider.remove_registry_entry("drop.server")
|
||||
|
||||
|
||||
@@ -6,11 +6,14 @@ from pathlib import Path
|
||||
|
||||
def test_rpc_transport_has_domain_method_modules() -> None:
|
||||
for module_name in (
|
||||
"wf_transport_rpc_http.methods_admin",
|
||||
"wf_transport_rpc_http.methods_capabilities",
|
||||
"wf_transport_rpc_http.methods_drafts",
|
||||
"wf_transport_rpc_http.methods_artifacts",
|
||||
"wf_transport_rpc_http.methods_deployments",
|
||||
"wf_transport_rpc_http.methods_runs",
|
||||
"wf_transport_rpc_http.methods_sources",
|
||||
"wf_transport_rpc_http.methods_source_registry",
|
||||
):
|
||||
module = importlib.import_module(module_name)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user