refactor: canonicalize mcp source id helpers
This commit is contained in:
@@ -354,6 +354,9 @@ implementation state.
|
||||
- Completed: upstream MCP adapter lookup (`require_adapter`) now lives in
|
||||
`wf_sources_mcp.adapters`, with `wf_mcp.broker.service.adapters` retained
|
||||
as a compatibility shim.
|
||||
- Completed: MCP source ID validation is canonical in `wf_sources_mcp.ids`.
|
||||
`wf_sources_mcp` no longer imports legacy `wf_mcp.connections` or
|
||||
`wf_mcp.shared.names` for source ID/path-safety checks.
|
||||
The `wf-mcp` script is now a legacy/special-purpose MCP entrypoint, not the
|
||||
preferred durable workflow server. New product paths should target
|
||||
`wf-rpc-server` plus neutral `wf_config`/`wf_server` composition, then keep
|
||||
|
||||
@@ -0,0 +1,374 @@
|
||||
# MCP Source ID Ownership 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:** Complete ownership of MCP source ID validation in `wf_sources_mcp.ids` and remove remaining `wf_sources_mcp` imports from legacy `wf_mcp` ID modules.
|
||||
|
||||
**Architecture:** `wf_sources_mcp.ids` is the canonical home for MCP upstream source ID rules: accepted characters, provider/account splitting, and reserved source IDs. Legacy `wf_mcp` modules may import from it, but `wf_sources_mcp` must not import `wf_mcp.connections` or `wf_mcp.shared.names`. This slice is intentionally small: it does not move broker DTOs or `ConnectionConfig` conversions.
|
||||
|
||||
**Tech Stack:** Python 3.14, pytest, ruff, basedpyright, AST import guards.
|
||||
|
||||
---
|
||||
|
||||
## Why This Pattern Is Correct
|
||||
|
||||
Source IDs are used by upstream MCP source-provider code before the broker gets involved:
|
||||
|
||||
- registry entries use source IDs;
|
||||
- catalog snapshots use source IDs;
|
||||
- auth records are referenced by source IDs;
|
||||
- filesystem stores must reject unsafe source IDs before building paths.
|
||||
|
||||
That makes ID parsing a low-level source-provider invariant. `wf_mcp` can keep compatibility imports, but it should not be the authority for these rules.
|
||||
|
||||
---
|
||||
|
||||
## Hard Boundaries
|
||||
|
||||
- Do not move `ConnectionConfig` or broker DTOs in this slice.
|
||||
- Do not change accepted ID syntax.
|
||||
- Do not change reserved ID values.
|
||||
- Do not change on-disk file layout.
|
||||
- Do not import `wf_mcp` from `src/wf_sources_mcp/ids.py`.
|
||||
- Remove the `wf_sources_mcp.storage.store` lazy import from `wf_mcp.connections`.
|
||||
- Keep `wf_mcp.connections.parse_connection_id` and `wf_mcp.shared.names.RESERVED_CONNECTION_IDS` import-compatible.
|
||||
- Do not commit unless the caller explicitly asks for a commit.
|
||||
|
||||
## File Map
|
||||
|
||||
- Modify `src/wf_sources_mcp/ids.py`: add explicit `validate_connection_id()` helper and keep `parse_connection_id()`.
|
||||
- Modify `src/wf_sources_mcp/storage/store.py`: import ID validation from `wf_sources_mcp.ids`, not `wf_mcp.connections`.
|
||||
- Modify `src/wf_mcp/connections.py`: keep compatibility import from `wf_sources_mcp.ids`.
|
||||
- Modify `src/wf_mcp/shared/names.py`: keep compatibility import from `wf_sources_mcp.ids`.
|
||||
- Modify or create `tests/wf_sources_mcp/test_ids.py`: canonical ID tests.
|
||||
- Modify `tests/wf_sources_mcp/test_import_direction_guard.py`: forbid old ID helper imports from `wf_sources_mcp`.
|
||||
- Modify docs: `docs/current_roadmap.md` and `docs/superpowers/specs/2026-06-03-long-lived-workflow-api-boundary.md`.
|
||||
- Move this plan to `docs/historical/superpowers/plans/` after implementation is verified.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Add Canonical Source ID Tests
|
||||
|
||||
**Files:**
|
||||
- Create: `tests/wf_sources_mcp/test_ids.py`
|
||||
- Optionally modify: `tests/wf_sources_mcp/test_connections.py`
|
||||
|
||||
- [ ] **Step 1: Create focused ID tests**
|
||||
|
||||
Create `tests/wf_sources_mcp/test_ids.py`:
|
||||
|
||||
```python
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from wf_sources_mcp.ids import (
|
||||
CONNECTION_ID_PATTERN,
|
||||
RESERVED_CONNECTION_IDS,
|
||||
parse_connection_id,
|
||||
validate_connection_id,
|
||||
)
|
||||
|
||||
|
||||
def test_validate_connection_id_returns_valid_id() -> None:
|
||||
assert validate_connection_id("github.work") == "github.work"
|
||||
assert validate_connection_id("my_source.default") == "my_source.default"
|
||||
|
||||
|
||||
def test_parse_connection_id_splits_provider_and_account() -> None:
|
||||
assert parse_connection_id("github.work") == ("github", "work")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"source_id",
|
||||
["", "github", ".github.work", "github.", "github/work", "github work", "../bad"],
|
||||
)
|
||||
def test_validate_connection_id_rejects_unsafe_or_unqualified_ids(source_id: str) -> None:
|
||||
with pytest.raises(ValueError):
|
||||
validate_connection_id(source_id)
|
||||
|
||||
|
||||
def test_reserved_connection_ids_are_canonical_source_constants() -> None:
|
||||
assert "wf.admin" in RESERVED_CONNECTION_IDS
|
||||
assert "wf.mcp" in RESERVED_CONNECTION_IDS
|
||||
assert CONNECTION_ID_PATTERN.startswith("^")
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Remove duplicate assertions if needed**
|
||||
|
||||
If `tests/wf_sources_mcp/test_connections.py` now duplicates the exact `parse_connection_id` tests, either leave the overlap if it is small or reduce it to connection-conversion behavior. Do not remove coverage unless `test_ids.py` has equal or stronger assertions.
|
||||
|
||||
- [ ] **Step 3: Run the new tests and verify the expected failure**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
uv run pytest tests/wf_sources_mcp/test_ids.py -q
|
||||
```
|
||||
|
||||
Expected: fail because `validate_connection_id` does not exist yet.
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Add `validate_connection_id()` to `wf_sources_mcp.ids`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/wf_sources_mcp/ids.py`
|
||||
|
||||
- [ ] **Step 1: Implement the helper without changing parsing rules**
|
||||
|
||||
Update `src/wf_sources_mcp/ids.py`:
|
||||
|
||||
```python
|
||||
def validate_connection_id(connection_id: str) -> str:
|
||||
"""Validate one MCP source id and return it unchanged.
|
||||
|
||||
Use this when callers need path-safety and shape validation but do not need
|
||||
provider/account pieces. `parse_connection_id()` remains the splitter.
|
||||
"""
|
||||
|
||||
parse_connection_id(connection_id)
|
||||
return connection_id
|
||||
```
|
||||
|
||||
Add it to `__all__`.
|
||||
|
||||
- [ ] **Step 2: Run canonical ID tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
uv run pytest tests/wf_sources_mcp/test_ids.py tests/wf_sources_mcp/test_connections.py -q
|
||||
```
|
||||
|
||||
Expected: pass.
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Remove Legacy ID Import From `wf_sources_mcp.storage`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/wf_sources_mcp/storage/store.py`
|
||||
|
||||
- [ ] **Step 1: Replace lazy legacy import**
|
||||
|
||||
In `FileCatalogStore._connection_path`, replace:
|
||||
|
||||
```python
|
||||
from wf_mcp.connections import parse_connection_id
|
||||
|
||||
parse_connection_id(connection_id)
|
||||
```
|
||||
|
||||
with a canonical import:
|
||||
|
||||
```python
|
||||
from wf_sources_mcp.ids import validate_connection_id
|
||||
|
||||
validate_connection_id(connection_id)
|
||||
```
|
||||
|
||||
If import cycles allow it, prefer a top-level import near the other `wf_sources_mcp` imports. If a cycle appears, keep it as a local import and add a short comment explaining why.
|
||||
|
||||
- [ ] **Step 2: Run storage tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
uv run pytest tests/wf_sources_mcp tests/wf_mcp/test_store.py -q
|
||||
```
|
||||
|
||||
Expected: pass.
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Add Import Guard for Old ID Modules
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/wf_sources_mcp/test_import_direction_guard.py`
|
||||
|
||||
- [ ] **Step 1: Add forbidden old ID helper import test**
|
||||
|
||||
Append:
|
||||
|
||||
```python
|
||||
def test_wf_sources_mcp_does_not_import_old_wf_mcp_id_modules() -> None:
|
||||
root = Path(__file__).resolve().parents[2] / "src" / "wf_sources_mcp"
|
||||
forbidden = {"wf_mcp.connections", "wf_mcp.shared.names"}
|
||||
violations: list[str] = []
|
||||
|
||||
for py_file in sorted(root.rglob("*.py")):
|
||||
rel = py_file.relative_to(root.parent)
|
||||
module = str(rel.with_suffix("")).replace("/", ".").replace("\\", ".")
|
||||
tree = ast.parse(py_file.read_text(encoding="utf-8"), filename=str(py_file))
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.ImportFrom) and node.module in forbidden:
|
||||
violations.append(f"{module}:{node.lineno}: from {node.module} import ...")
|
||||
elif isinstance(node, ast.Import):
|
||||
for alias in node.names:
|
||||
if alias.name in forbidden:
|
||||
violations.append(f"{module}:{node.lineno}: import {alias.name}")
|
||||
|
||||
assert violations == [], (
|
||||
"wf_sources_mcp still imports old wf_mcp source ID modules:\n"
|
||||
+ "\n".join(f" {violation}" for violation in violations)
|
||||
)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run import guard**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
uv run pytest tests/wf_sources_mcp/test_import_direction_guard.py -q
|
||||
```
|
||||
|
||||
Expected: pass.
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Verify Legacy Compatibility Imports Still Work
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/wf_mcp/test_compat_imports.py`
|
||||
|
||||
- [ ] **Step 1: Add identity tests for legacy paths**
|
||||
|
||||
Append:
|
||||
|
||||
```python
|
||||
def test_wf_mcp_connection_id_helpers_reexport_wf_sources_mcp_ids() -> None:
|
||||
from wf_mcp.connections import parse_connection_id as compat_parse_connection_id
|
||||
from wf_mcp.shared.names import RESERVED_CONNECTION_IDS as compat_reserved_ids
|
||||
from wf_sources_mcp.ids import RESERVED_CONNECTION_IDS, parse_connection_id
|
||||
|
||||
assert compat_parse_connection_id is parse_connection_id
|
||||
assert compat_reserved_ids is RESERVED_CONNECTION_IDS
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run compatibility tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
uv run pytest tests/wf_mcp/test_compat_imports.py tests/wf_mcp/test_store.py -q
|
||||
```
|
||||
|
||||
Expected: pass.
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Update Docs and Archive Plan
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/current_roadmap.md`
|
||||
- Modify: `docs/superpowers/specs/2026-06-03-long-lived-workflow-api-boundary.md`
|
||||
- Move: `docs/superpowers/plans/2026-06-08-wf-sources-mcp-source-id-ownership.md` to `docs/historical/superpowers/plans/2026-06-08-wf-sources-mcp-source-id-ownership.md`
|
||||
|
||||
- [ ] **Step 1: Update `docs/current_roadmap.md`**
|
||||
|
||||
Under the `wf_sources_mcp` cleanup section, add:
|
||||
|
||||
```markdown
|
||||
MCP source ID validation is canonical in `wf_sources_mcp.ids`.
|
||||
`wf_sources_mcp` no longer imports legacy `wf_mcp.connections` or
|
||||
`wf_mcp.shared.names` for source ID/path-safety checks.
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Update the long-lived boundary spec**
|
||||
|
||||
In `docs/superpowers/specs/2026-06-03-long-lived-workflow-api-boundary.md`, add a completed numbered item after the adapter-helper item:
|
||||
|
||||
```markdown
|
||||
22. Complete: MCP source ID validation and reserved source IDs are canonical in
|
||||
`wf_sources_mcp.ids`; legacy `wf_mcp.connections` / `wf_mcp.shared.names`
|
||||
remain compatibility consumers.
|
||||
```
|
||||
|
||||
Renumber the pending broad item if needed.
|
||||
|
||||
- [ ] **Step 3: Archive the plan**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git mv docs/superpowers/plans/2026-06-08-wf-sources-mcp-source-id-ownership.md docs/historical/superpowers/plans/2026-06-08-wf-sources-mcp-source-id-ownership.md
|
||||
```
|
||||
|
||||
Expected: `git status --short` shows the plan under `docs/historical/...`.
|
||||
|
||||
---
|
||||
|
||||
### Task 7: Final Verification
|
||||
|
||||
**Files:**
|
||||
- No code edits unless verification finds a real issue.
|
||||
|
||||
- [ ] **Step 1: Run focused tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
uv run pytest tests/wf_sources_mcp/test_ids.py tests/wf_sources_mcp/test_import_direction_guard.py tests/wf_sources_mcp tests/wf_mcp/test_compat_imports.py tests/wf_mcp/test_store.py -q
|
||||
```
|
||||
|
||||
Expected: all selected tests pass.
|
||||
|
||||
- [ ] **Step 2: Run lint**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
uv run ruff check src/wf_sources_mcp/ids.py src/wf_sources_mcp/storage/store.py src/wf_mcp/connections.py src/wf_mcp/shared/names.py tests/wf_sources_mcp/test_ids.py tests/wf_sources_mcp/test_import_direction_guard.py tests/wf_mcp/test_compat_imports.py
|
||||
```
|
||||
|
||||
Expected: `All checks passed!`
|
||||
|
||||
- [ ] **Step 3: Run typecheck**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
uv run basedpyright --level error src/wf_sources_mcp/ids.py src/wf_sources_mcp/storage/store.py src/wf_mcp/connections.py src/wf_mcp/shared/names.py tests/wf_sources_mcp/test_ids.py tests/wf_mcp/test_compat_imports.py
|
||||
```
|
||||
|
||||
Expected: `0 errors, 0 warnings, 0 notes`.
|
||||
|
||||
- [ ] **Step 4: Check remaining old ID helper imports**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
rg -n "wf_mcp\\.connections|wf_mcp\\.shared\\.names|parse_connection_id|RESERVED_CONNECTION_IDS|validate_connection_id" src/wf_sources_mcp src/wf_mcp tests/wf_sources_mcp tests/wf_mcp
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
- `src/wf_sources_mcp` imports source ID helpers only from `wf_sources_mcp.ids`.
|
||||
- `src/wf_mcp/connections.py` and `src/wf_mcp/shared/names.py` may import from `wf_sources_mcp.ids` for compatibility.
|
||||
- Tests may reference legacy paths only for compatibility assertions.
|
||||
|
||||
- [ ] **Step 5: Check whitespace**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Expected: no whitespace errors. CRLF warnings on Windows are acceptable.
|
||||
|
||||
---
|
||||
|
||||
## Expected Final Report
|
||||
|
||||
The implementer should report:
|
||||
|
||||
- Files created, modified, and moved.
|
||||
- Exact verification commands and pass/fail output.
|
||||
- Confirmation that `wf_sources_mcp.ids` owns source ID validation.
|
||||
- Confirmation that `wf_sources_mcp.storage.store` no longer imports `wf_mcp.connections`.
|
||||
- Confirmation that legacy `wf_mcp` ID paths still work.
|
||||
- Any deviations from this plan.
|
||||
|
||||
Do not claim "full suite passed" unless the full suite was actually run.
|
||||
@@ -151,7 +151,10 @@ First slices should move leaf modules only and leave `wf_mcp` re-export shims:
|
||||
21. Complete: upstream MCP adapter lookup (`require_adapter`) moved to
|
||||
`wf_sources_mcp.adapters`, with `wf_mcp.broker.service.adapters` retained
|
||||
as a compatibility shim.
|
||||
22. Upstream transport/discovery/session services.
|
||||
22. Complete: MCP source ID validation and reserved source IDs are canonical in
|
||||
`wf_sources_mcp.ids`; legacy `wf_mcp.connections` / `wf_mcp.shared.names`
|
||||
remain compatibility consumers.
|
||||
23. Upstream transport/discovery/session services.
|
||||
|
||||
Each slice should add import-direction tests so the new source-provider package
|
||||
does not depend on `wf_mcp.workflow_surface`, `wf_mcp.admin_surface`,
|
||||
|
||||
@@ -6,6 +6,8 @@ from wf_sources_mcp.ids import CONNECTION_ID_PATTERN, parse_connection_id
|
||||
|
||||
from .models import ConnectionConfig
|
||||
|
||||
__all__ = ["CONNECTION_ID_PATTERN", "parse_connection_id"]
|
||||
|
||||
|
||||
def qualify_node_name(connection_id: str, local_name: str) -> str:
|
||||
parse_connection_id(connection_id)
|
||||
|
||||
@@ -25,6 +25,8 @@ from wf_sources_mcp.ids import RESERVED_CONNECTION_IDS
|
||||
|
||||
ADMIN_NAMESPACE = "wf.admin"
|
||||
|
||||
__all__ = ["RESERVED_CONNECTION_IDS"]
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ProxyToolName:
|
||||
|
||||
@@ -28,8 +28,20 @@ def parse_connection_id(connection_id: str) -> tuple[str, str]:
|
||||
return server, account
|
||||
|
||||
|
||||
def validate_connection_id(connection_id: str) -> str:
|
||||
"""Validate one MCP source id and return it unchanged.
|
||||
|
||||
Use this when callers need path-safety and shape validation but do not need
|
||||
provider/account pieces. `parse_connection_id()` remains the splitter.
|
||||
"""
|
||||
|
||||
parse_connection_id(connection_id)
|
||||
return connection_id
|
||||
|
||||
|
||||
__all__ = [
|
||||
"CONNECTION_ID_PATTERN",
|
||||
"RESERVED_CONNECTION_IDS",
|
||||
"parse_connection_id",
|
||||
"validate_connection_id",
|
||||
]
|
||||
|
||||
@@ -129,9 +129,9 @@ class FileCatalogStore(CatalogStore):
|
||||
|
||||
@staticmethod
|
||||
def _connection_path(directory: Path, connection_id: str) -> Path:
|
||||
from wf_mcp.connections import parse_connection_id
|
||||
from wf_sources_mcp.ids import validate_connection_id
|
||||
|
||||
parse_connection_id(connection_id)
|
||||
validate_connection_id(connection_id)
|
||||
root = directory.resolve()
|
||||
path = (directory / f"{connection_id}.json").resolve()
|
||||
if path.parent != root:
|
||||
|
||||
@@ -247,8 +247,19 @@ def test_wf_mcp_broker_discovery_keeps_specs_adapter() -> None:
|
||||
assert broker_specs.__name__ == "specs_from_discovered_tools"
|
||||
|
||||
|
||||
def test_wf_mcp_broker_service_adapter_shim_reexports_wf_sources_mcp_adapter_helper() -> None:
|
||||
def test_wf_mcp_broker_service_adapter_shim_reexports_wf_sources_mcp_adapter_helper() -> (
|
||||
None
|
||||
):
|
||||
from wf_mcp.broker.service.adapters import require_adapter as compat_require_adapter
|
||||
from wf_sources_mcp.adapters import require_adapter
|
||||
|
||||
assert compat_require_adapter is require_adapter
|
||||
|
||||
|
||||
def test_wf_mcp_connection_id_helpers_reexport_wf_sources_mcp_ids() -> None:
|
||||
from wf_mcp.connections import parse_connection_id as compat_parse_connection_id
|
||||
from wf_mcp.shared.names import RESERVED_CONNECTION_IDS as compat_reserved_ids
|
||||
from wf_sources_mcp.ids import RESERVED_CONNECTION_IDS, parse_connection_id
|
||||
|
||||
assert compat_parse_connection_id is parse_connection_id
|
||||
assert compat_reserved_ids is RESERVED_CONNECTION_IDS
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from wf_sources_mcp.ids import (
|
||||
CONNECTION_ID_PATTERN,
|
||||
RESERVED_CONNECTION_IDS,
|
||||
parse_connection_id,
|
||||
validate_connection_id,
|
||||
)
|
||||
|
||||
|
||||
def test_validate_connection_id_returns_valid_id() -> None:
|
||||
assert validate_connection_id("github.work") == "github.work"
|
||||
assert validate_connection_id("my_source.default") == "my_source.default"
|
||||
|
||||
|
||||
def test_parse_connection_id_splits_provider_and_account() -> None:
|
||||
assert parse_connection_id("github.work") == ("github", "work")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"source_id",
|
||||
["", "github", ".github.work", "github.", "github/work", "github work", "../bad"],
|
||||
)
|
||||
def test_validate_connection_id_rejects_unsafe_or_unqualified_ids(
|
||||
source_id: str,
|
||||
) -> None:
|
||||
with pytest.raises(ValueError):
|
||||
validate_connection_id(source_id)
|
||||
|
||||
|
||||
def test_reserved_connection_ids_are_canonical_source_constants() -> None:
|
||||
assert "wf.admin" in RESERVED_CONNECTION_IDS
|
||||
assert "wf.mcp" in RESERVED_CONNECTION_IDS
|
||||
assert CONNECTION_ID_PATTERN.startswith("^")
|
||||
@@ -92,11 +92,15 @@ def test_wf_sources_mcp_does_not_import_old_sdk_protocol_modules() -> None:
|
||||
tree = ast.parse(py_file.read_text(encoding="utf-8"), filename=str(py_file))
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.ImportFrom) and node.module in forbidden:
|
||||
violations.append(f"{module}:{node.lineno}: from {node.module} import ...")
|
||||
violations.append(
|
||||
f"{module}:{node.lineno}: from {node.module} import ..."
|
||||
)
|
||||
elif isinstance(node, ast.Import):
|
||||
for alias in node.names:
|
||||
if alias.name in forbidden:
|
||||
violations.append(f"{module}:{node.lineno}: import {alias.name}")
|
||||
violations.append(
|
||||
f"{module}:{node.lineno}: import {alias.name}"
|
||||
)
|
||||
|
||||
assert violations == [], (
|
||||
"wf_sources_mcp still imports old wf_mcp SDK/runtime protocol modules:\n"
|
||||
@@ -117,11 +121,15 @@ def test_wf_sources_mcp_does_not_import_old_sdk_converter_module() -> None:
|
||||
tree = ast.parse(py_file.read_text(encoding="utf-8"), filename=str(py_file))
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.ImportFrom) and node.module in forbidden:
|
||||
violations.append(f"{module}:{node.lineno}: from {node.module} import ...")
|
||||
violations.append(
|
||||
f"{module}:{node.lineno}: from {node.module} import ..."
|
||||
)
|
||||
elif isinstance(node, ast.Import):
|
||||
for alias in node.names:
|
||||
if alias.name in forbidden:
|
||||
violations.append(f"{module}:{node.lineno}: import {alias.name}")
|
||||
violations.append(
|
||||
f"{module}:{node.lineno}: import {alias.name}"
|
||||
)
|
||||
|
||||
assert violations == [], (
|
||||
"wf_sources_mcp still imports old wf_mcp SDK converter module:\n"
|
||||
@@ -140,11 +148,15 @@ def test_wf_sources_mcp_does_not_import_old_broker_discovery_module() -> None:
|
||||
tree = ast.parse(py_file.read_text(encoding="utf-8"), filename=str(py_file))
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.ImportFrom) and node.module in forbidden:
|
||||
violations.append(f"{module}:{node.lineno}: from {node.module} import ...")
|
||||
violations.append(
|
||||
f"{module}:{node.lineno}: from {node.module} import ..."
|
||||
)
|
||||
elif isinstance(node, ast.Import):
|
||||
for alias in node.names:
|
||||
if alias.name in forbidden:
|
||||
violations.append(f"{module}:{node.lineno}: import {alias.name}")
|
||||
violations.append(
|
||||
f"{module}:{node.lineno}: import {alias.name}"
|
||||
)
|
||||
|
||||
assert violations == [], (
|
||||
"wf_sources_mcp still imports old wf_mcp broker discovery module:\n"
|
||||
@@ -163,11 +175,15 @@ def test_wf_sources_mcp_does_not_import_old_workflow_wrapper_module() -> None:
|
||||
tree = ast.parse(py_file.read_text(encoding="utf-8"), filename=str(py_file))
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.ImportFrom) and node.module in forbidden:
|
||||
violations.append(f"{module}:{node.lineno}: from {node.module} import ...")
|
||||
violations.append(
|
||||
f"{module}:{node.lineno}: from {node.module} import ..."
|
||||
)
|
||||
elif isinstance(node, ast.Import):
|
||||
for alias in node.names:
|
||||
if alias.name in forbidden:
|
||||
violations.append(f"{module}:{node.lineno}: import {alias.name}")
|
||||
violations.append(
|
||||
f"{module}:{node.lineno}: import {alias.name}"
|
||||
)
|
||||
|
||||
assert violations == [], (
|
||||
"wf_sources_mcp still imports old wf_mcp workflow wrapper module:\n"
|
||||
@@ -186,11 +202,15 @@ def test_wf_sources_mcp_does_not_import_old_broker_event_modules() -> None:
|
||||
tree = ast.parse(py_file.read_text(encoding="utf-8"), filename=str(py_file))
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.ImportFrom) and node.module in forbidden:
|
||||
violations.append(f"{module}:{node.lineno}: from {node.module} import ...")
|
||||
violations.append(
|
||||
f"{module}:{node.lineno}: from {node.module} import ..."
|
||||
)
|
||||
elif isinstance(node, ast.Import):
|
||||
for alias in node.names:
|
||||
if alias.name in forbidden:
|
||||
violations.append(f"{module}:{node.lineno}: import {alias.name}")
|
||||
violations.append(
|
||||
f"{module}:{node.lineno}: import {alias.name}"
|
||||
)
|
||||
|
||||
assert violations == [], (
|
||||
"wf_sources_mcp still imports old wf_mcp broker event modules:\n"
|
||||
@@ -209,13 +229,44 @@ def test_wf_sources_mcp_does_not_import_old_broker_service_adapter_module() -> N
|
||||
tree = ast.parse(py_file.read_text(encoding="utf-8"), filename=str(py_file))
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.ImportFrom) and node.module in forbidden:
|
||||
violations.append(f"{module}:{node.lineno}: from {node.module} import ...")
|
||||
violations.append(
|
||||
f"{module}:{node.lineno}: from {node.module} import ..."
|
||||
)
|
||||
elif isinstance(node, ast.Import):
|
||||
for alias in node.names:
|
||||
if alias.name in forbidden:
|
||||
violations.append(f"{module}:{node.lineno}: import {alias.name}")
|
||||
violations.append(
|
||||
f"{module}:{node.lineno}: import {alias.name}"
|
||||
)
|
||||
|
||||
assert violations == [], (
|
||||
"wf_sources_mcp still imports old wf_mcp broker service adapter module:\n"
|
||||
+ "\n".join(f" {violation}" for violation in violations)
|
||||
)
|
||||
|
||||
|
||||
def test_wf_sources_mcp_does_not_import_old_wf_mcp_id_modules() -> None:
|
||||
root = Path(__file__).resolve().parents[2] / "src" / "wf_sources_mcp"
|
||||
forbidden = {"wf_mcp.connections", "wf_mcp.shared.names"}
|
||||
violations: list[str] = []
|
||||
|
||||
for py_file in sorted(root.rglob("*.py")):
|
||||
rel = py_file.relative_to(root.parent)
|
||||
module = str(rel.with_suffix("")).replace("/", ".").replace("\\", ".")
|
||||
tree = ast.parse(py_file.read_text(encoding="utf-8"), filename=str(py_file))
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.ImportFrom) and node.module in forbidden:
|
||||
violations.append(
|
||||
f"{module}:{node.lineno}: from {node.module} import ..."
|
||||
)
|
||||
elif isinstance(node, ast.Import):
|
||||
for alias in node.names:
|
||||
if alias.name in forbidden:
|
||||
violations.append(
|
||||
f"{module}:{node.lineno}: import {alias.name}"
|
||||
)
|
||||
|
||||
assert violations == [], (
|
||||
"wf_sources_mcp still imports old wf_mcp source ID modules:\n"
|
||||
+ "\n".join(f" {violation}" for violation in violations)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user