docs: refine channel connectors foundation plan
This commit is contained in:
@ -32,6 +32,7 @@ Excluded from this plan:
|
|||||||
- QQBot connector.
|
- QQBot connector.
|
||||||
- Frontend connection wizard.
|
- Frontend connection wizard.
|
||||||
- Hot starting/stopping adapters without backend restart.
|
- Hot starting/stopping adapters without backend restart.
|
||||||
|
- Multi-process-safe storage. The JSON stores use `threading.Lock` plus atomic file replace for the single backend process used in phase 1. Production multi-worker deployment needs a file lock or database-backed store.
|
||||||
|
|
||||||
## File Structure
|
## File Structure
|
||||||
|
|
||||||
@ -77,10 +78,7 @@ Create `app-instance/backend/tests/unit/test_channel_connection_store.py`:
|
|||||||
```python
|
```python
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime, timezone
|
|
||||||
|
|
||||||
from beaver.interfaces.channels.connections import (
|
from beaver.interfaces.channels.connections import (
|
||||||
ChannelConnection,
|
|
||||||
ChannelConnectionStore,
|
ChannelConnectionStore,
|
||||||
CredentialStore,
|
CredentialStore,
|
||||||
PairingTokenStore,
|
PairingTokenStore,
|
||||||
@ -578,6 +576,7 @@ class FakeConnector:
|
|||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.validated: list[str] = []
|
self.validated: list[str] = []
|
||||||
|
self.revoked: list[str] = []
|
||||||
|
|
||||||
async def validate(self, connection_id: str) -> ValidationResult:
|
async def validate(self, connection_id: str) -> ValidationResult:
|
||||||
self.validated.append(connection_id)
|
self.validated.append(connection_id)
|
||||||
@ -594,6 +593,7 @@ class FakeConnector:
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def revoke(self, connection_id: str) -> None:
|
async def revoke(self, connection_id: str) -> None:
|
||||||
|
self.revoked.append(connection_id)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@ -654,6 +654,32 @@ def test_connector_registry_materializes_only_connected_connections(tmp_path) ->
|
|||||||
assert connection_store.get(draft.connection_id).status == "draft"
|
assert connection_store.get(draft.connection_id).status == "draft"
|
||||||
|
|
||||||
asyncio.run(run())
|
asyncio.run(run())
|
||||||
|
|
||||||
|
|
||||||
|
def test_connector_registry_revoke_calls_connector_and_updates_store(tmp_path) -> None:
|
||||||
|
async def run() -> None:
|
||||||
|
connection_store = ChannelConnectionStore(tmp_path / "connections.json")
|
||||||
|
credential_store = CredentialStore(tmp_path / "credentials.json")
|
||||||
|
connector = FakeConnector()
|
||||||
|
registry = ChannelConnectorRegistry(connection_store=connection_store, credential_store=credential_store)
|
||||||
|
registry.register(connector)
|
||||||
|
|
||||||
|
connection = connection_store.create(
|
||||||
|
kind="fake",
|
||||||
|
mode="webhook",
|
||||||
|
display_name="Fake",
|
||||||
|
account_id="fake-account",
|
||||||
|
owner_user_id=None,
|
||||||
|
auth_type="token",
|
||||||
|
)
|
||||||
|
connection_store.update_status(connection.connection_id, status="connected", last_error=None)
|
||||||
|
|
||||||
|
await registry.revoke(connection.connection_id)
|
||||||
|
|
||||||
|
assert connector.revoked == [connection.connection_id]
|
||||||
|
assert connection_store.get(connection.connection_id).status == "revoked"
|
||||||
|
|
||||||
|
asyncio.run(run())
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 2: Run tests to verify they fail**
|
- [ ] **Step 2: Run tests to verify they fail**
|
||||||
@ -780,7 +806,7 @@ cd app-instance/backend
|
|||||||
uv run pytest tests/unit/test_channel_connector_registry.py -q
|
uv run pytest tests/unit/test_channel_connector_registry.py -q
|
||||||
```
|
```
|
||||||
|
|
||||||
Expected: `2 passed`.
|
Expected: `3 passed`.
|
||||||
|
|
||||||
- [ ] **Step 6: Commit Task 2**
|
- [ ] **Step 6: Commit Task 2**
|
||||||
|
|
||||||
@ -920,6 +946,32 @@ def test_telegram_connector_validation_failure_sets_error_status(tmp_path) -> No
|
|||||||
assert "invalid token" in (result.error or "")
|
assert "invalid token" in (result.error or "")
|
||||||
|
|
||||||
asyncio.run(run())
|
asyncio.run(run())
|
||||||
|
|
||||||
|
|
||||||
|
def test_telegram_connector_revoke_leaves_store_status_to_registry(tmp_path) -> None:
|
||||||
|
async def run() -> None:
|
||||||
|
connection_store = ChannelConnectionStore(tmp_path / "connections.json")
|
||||||
|
credential_store = CredentialStore(tmp_path / "credentials.json")
|
||||||
|
connection = connection_store.create(
|
||||||
|
kind="telegram",
|
||||||
|
mode="polling",
|
||||||
|
display_name="Telegram Main",
|
||||||
|
account_id="telegram:12345",
|
||||||
|
owner_user_id=None,
|
||||||
|
auth_type="token",
|
||||||
|
)
|
||||||
|
connection_store.update_status(connection.connection_id, status="connected", last_error=None)
|
||||||
|
connector = TelegramConnector(
|
||||||
|
connection_store=connection_store,
|
||||||
|
credential_store=credential_store,
|
||||||
|
client_factory=lambda token: FakeTelegramClient(),
|
||||||
|
)
|
||||||
|
|
||||||
|
await connector.revoke(connection.connection_id)
|
||||||
|
|
||||||
|
assert connection_store.get(connection.connection_id).status == "connected"
|
||||||
|
|
||||||
|
asyncio.run(run())
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 2: Run tests to verify they fail**
|
- [ ] **Step 2: Run tests to verify they fail**
|
||||||
@ -933,7 +985,18 @@ uv run pytest tests/unit/test_telegram_channel_connector.py -q
|
|||||||
|
|
||||||
Expected: fail with `ImportError: cannot import name 'TelegramConnector'`.
|
Expected: fail with `ImportError: cannot import name 'TelegramConnector'`.
|
||||||
|
|
||||||
- [ ] **Step 3: Implement TelegramConnector**
|
- [ ] **Step 3: Verify Telegram dependency**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd app-instance/backend
|
||||||
|
rg -n "python-telegram-bot" pyproject.toml uv.lock | sed -n '1,20p'
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected output includes `python-telegram-bot>=22.0,<23.0`. The default client factory may use `from telegram import Bot`, and `Bot.get_me()` is awaitable in this dependency line.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Implement TelegramConnector**
|
||||||
|
|
||||||
Create `app-instance/backend/beaver/interfaces/channels/connections/telegram.py`:
|
Create `app-instance/backend/beaver/interfaces/channels/connections/telegram.py`:
|
||||||
|
|
||||||
@ -1003,7 +1066,9 @@ class TelegramConnector:
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def revoke(self, connection_id: str) -> None:
|
async def revoke(self, connection_id: str) -> None:
|
||||||
self.connection_store.revoke(connection_id)
|
# Telegram bot tokens do not have a Beaver-managed platform revoke action.
|
||||||
|
# The registry owns local connection state transitions.
|
||||||
|
return None
|
||||||
|
|
||||||
def _bot_token(self, credentials_ref: str | None) -> str:
|
def _bot_token(self, credentials_ref: str | None) -> str:
|
||||||
if not credentials_ref:
|
if not credentials_ref:
|
||||||
@ -1030,7 +1095,7 @@ def _default_client_factory(token: str) -> Any:
|
|||||||
return Bot(token=token)
|
return Bot(token=token)
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 4: Export TelegramConnector**
|
- [ ] **Step 5: Export TelegramConnector**
|
||||||
|
|
||||||
Modify `app-instance/backend/beaver/interfaces/channels/connections/__init__.py`:
|
Modify `app-instance/backend/beaver/interfaces/channels/connections/__init__.py`:
|
||||||
|
|
||||||
@ -1056,7 +1121,7 @@ __all__ = [
|
|||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 5: Run Telegram connector tests**
|
- [ ] **Step 6: Run Telegram connector tests**
|
||||||
|
|
||||||
Run:
|
Run:
|
||||||
|
|
||||||
@ -1065,9 +1130,9 @@ cd app-instance/backend
|
|||||||
uv run pytest tests/unit/test_telegram_channel_connector.py -q
|
uv run pytest tests/unit/test_telegram_channel_connector.py -q
|
||||||
```
|
```
|
||||||
|
|
||||||
Expected: `3 passed`.
|
Expected: `4 passed`.
|
||||||
|
|
||||||
- [ ] **Step 6: Commit Task 3**
|
- [ ] **Step 7: Commit Task 3**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git add app-instance/backend/beaver/interfaces/channels/connections app-instance/backend/tests/unit/test_telegram_channel_connector.py
|
git add app-instance/backend/beaver/interfaces/channels/connections app-instance/backend/tests/unit/test_telegram_channel_connector.py
|
||||||
@ -1082,7 +1147,22 @@ git commit -m "feat: add telegram channel connector"
|
|||||||
- Modify: `app-instance/backend/beaver/interfaces/web/app.py`
|
- Modify: `app-instance/backend/beaver/interfaces/web/app.py`
|
||||||
- Test: `app-instance/backend/tests/unit/test_channel_connector_registry.py`
|
- Test: `app-instance/backend/tests/unit/test_channel_connector_registry.py`
|
||||||
|
|
||||||
- [ ] **Step 1: Extend registry tests for ChannelConfig materialization**
|
- [ ] **Step 1: Verify ChannelConfig fields**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd app-instance/backend
|
||||||
|
uv run python - <<'PY'
|
||||||
|
from dataclasses import fields
|
||||||
|
from beaver.foundation.config.schema import ChannelConfig
|
||||||
|
print([field.name for field in fields(ChannelConfig)])
|
||||||
|
PY
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected output includes `enabled`, `kind`, `mode`, `account_id`, `display_name`, `config`, and `secrets`.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Extend registry tests for ChannelConfig materialization**
|
||||||
|
|
||||||
Append to `app-instance/backend/tests/unit/test_channel_connector_registry.py`:
|
Append to `app-instance/backend/tests/unit/test_channel_connector_registry.py`:
|
||||||
|
|
||||||
@ -1131,7 +1211,7 @@ def test_connector_registry_materializes_channel_configs_with_credentials(tmp_pa
|
|||||||
asyncio.run(run())
|
asyncio.run(run())
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 2: Run registry tests to verify failure**
|
- [ ] **Step 3: Run registry tests to verify failure**
|
||||||
|
|
||||||
Run:
|
Run:
|
||||||
|
|
||||||
@ -1142,7 +1222,7 @@ uv run pytest tests/unit/test_channel_connector_registry.py::test_connector_regi
|
|||||||
|
|
||||||
Expected: fail with `AttributeError: 'ChannelConnectorRegistry' object has no attribute 'materialize_channel_configs'`.
|
Expected: fail with `AttributeError: 'ChannelConnectorRegistry' object has no attribute 'materialize_channel_configs'`.
|
||||||
|
|
||||||
- [ ] **Step 3: Implement channel config materialization**
|
- [ ] **Step 4: Implement channel config materialization**
|
||||||
|
|
||||||
Modify `app-instance/backend/beaver/interfaces/channels/connections/connectors.py`:
|
Modify `app-instance/backend/beaver/interfaces/channels/connections/connectors.py`:
|
||||||
|
|
||||||
@ -1169,7 +1249,7 @@ Add this method to `ChannelConnectorRegistry`:
|
|||||||
return channels
|
return channels
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 4: Add app helpers for connection state paths and registry construction**
|
- [ ] **Step 5: Add app helpers for connection state paths and registry construction**
|
||||||
|
|
||||||
Modify `app-instance/backend/beaver/interfaces/web/app.py` imports:
|
Modify `app-instance/backend/beaver/interfaces/web/app.py` imports:
|
||||||
|
|
||||||
@ -1178,7 +1258,6 @@ from beaver.interfaces.channels.connections import (
|
|||||||
ChannelConnectionStore,
|
ChannelConnectionStore,
|
||||||
ChannelConnectorRegistry,
|
ChannelConnectorRegistry,
|
||||||
CredentialStore,
|
CredentialStore,
|
||||||
PairingTokenStore,
|
|
||||||
TelegramConnector,
|
TelegramConnector,
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
@ -1204,12 +1283,13 @@ def _build_channel_connector_registry(workspace: Path) -> ChannelConnectorRegist
|
|||||||
return registry
|
return registry
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 5: Merge materialized connections into runtime startup**
|
- [ ] **Step 6: Merge materialized connections into runtime startup**
|
||||||
|
|
||||||
Modify the lifespan block in `app-instance/backend/beaver/interfaces/web/app.py` where `ChannelRuntime` is created:
|
Modify the lifespan block in `app-instance/backend/beaver/interfaces/web/app.py` where `ChannelRuntime` is created:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
loaded = attached_service.create_loop().boot()
|
loaded = attached_service.create_loop().boot()
|
||||||
|
app.state.channel_connection_workspace = loaded.workspace
|
||||||
connector_registry = _build_channel_connector_registry(loaded.workspace)
|
connector_registry = _build_channel_connector_registry(loaded.workspace)
|
||||||
app.state.channel_connector_registry = connector_registry
|
app.state.channel_connector_registry = connector_registry
|
||||||
connection_channels = await connector_registry.materialize_channel_configs()
|
connection_channels = await connector_registry.materialize_channel_configs()
|
||||||
@ -1224,7 +1304,7 @@ Modify the lifespan block in `app-instance/backend/beaver/interfaces/web/app.py`
|
|||||||
|
|
||||||
Keep `app.state.channel_connector_registry = connector_registry` before runtime startup so API handlers can reuse the same stores.
|
Keep `app.state.channel_connector_registry = connector_registry` before runtime startup so API handlers can reuse the same stores.
|
||||||
|
|
||||||
- [ ] **Step 6: Run registry tests**
|
- [ ] **Step 7: Run registry tests**
|
||||||
|
|
||||||
Run:
|
Run:
|
||||||
|
|
||||||
@ -1235,7 +1315,7 @@ uv run pytest tests/unit/test_channel_connector_registry.py -q
|
|||||||
|
|
||||||
Expected: all tests pass.
|
Expected: all tests pass.
|
||||||
|
|
||||||
- [ ] **Step 7: Commit Task 4**
|
- [ ] **Step 8: Commit Task 4**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git add app-instance/backend/beaver/interfaces/channels/connections/connectors.py app-instance/backend/beaver/interfaces/web/app.py app-instance/backend/tests/unit/test_channel_connector_registry.py
|
git add app-instance/backend/beaver/interfaces/channels/connections/connectors.py app-instance/backend/beaver/interfaces/web/app.py app-instance/backend/tests/unit/test_channel_connector_registry.py
|
||||||
@ -1265,7 +1345,7 @@ from beaver.interfaces.web.app import create_app
|
|||||||
from beaver.services.agent_service import AgentService
|
from beaver.services.agent_service import AgentService
|
||||||
|
|
||||||
|
|
||||||
def test_channel_connection_api_creates_lists_and_revokes(tmp_path) -> None:
|
def test_channel_connection_api_creates_updates_lists_and_revokes(tmp_path) -> None:
|
||||||
config_path = tmp_path / "config.json"
|
config_path = tmp_path / "config.json"
|
||||||
config_path.write_text('{"agents": {"defaults": {"workspace": "%s"}}, "providers": {}}' % str(tmp_path), encoding="utf-8")
|
config_path.write_text('{"agents": {"defaults": {"workspace": "%s"}}, "providers": {}}' % str(tmp_path), encoding="utf-8")
|
||||||
service = AgentService(config_path=config_path)
|
service = AgentService(config_path=config_path)
|
||||||
@ -1289,11 +1369,26 @@ def test_channel_connection_api_creates_lists_and_revokes(tmp_path) -> None:
|
|||||||
connection_id = body["connection"]["connection_id"]
|
connection_id = body["connection"]["connection_id"]
|
||||||
assert body["connection"]["kind"] == "telegram"
|
assert body["connection"]["kind"] == "telegram"
|
||||||
assert body["connection"]["status"] == "draft"
|
assert body["connection"]["status"] == "draft"
|
||||||
|
assert "credentials_ref" not in body["connection"]
|
||||||
assert body["credentials"] == {"botToken": "***"}
|
assert body["credentials"] == {"botToken": "***"}
|
||||||
|
|
||||||
|
patched = client.patch(
|
||||||
|
f"/api/channel-connections/{connection_id}",
|
||||||
|
json={
|
||||||
|
"displayName": "Telegram Ops",
|
||||||
|
"config": {"maxMessageChars": 2048},
|
||||||
|
"secrets": {"botToken": "token-2"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert patched.status_code == 200
|
||||||
|
assert patched.json()["connection"]["display_name"] == "Telegram Ops"
|
||||||
|
assert patched.json()["connection"]["runtime_config"] == {"maxMessageChars": 2048}
|
||||||
|
assert patched.json()["credentials"] == {"botToken": "***"}
|
||||||
|
|
||||||
listed = client.get("/api/channel-connections")
|
listed = client.get("/api/channel-connections")
|
||||||
assert listed.status_code == 200
|
assert listed.status_code == 200
|
||||||
assert listed.json()[0]["connection_id"] == connection_id
|
assert listed.json()[0]["connection_id"] == connection_id
|
||||||
|
assert "credentials_ref" not in listed.json()[0]
|
||||||
|
|
||||||
revoked = client.post(f"/api/channel-connections/{connection_id}/revoke")
|
revoked = client.post(f"/api/channel-connections/{connection_id}/revoke")
|
||||||
assert revoked.status_code == 200
|
assert revoked.status_code == 200
|
||||||
@ -1354,6 +1449,15 @@ class WebChannelConnectionResponse(BaseModel):
|
|||||||
credentials: dict[str, str] = Field(default_factory=dict)
|
credentials: dict[str, str] = Field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
class WebChannelConnectionUpdateRequest(BaseModel):
|
||||||
|
"""Update editable channel connection setup fields."""
|
||||||
|
|
||||||
|
display_name: str | None = Field(default=None, alias="displayName")
|
||||||
|
account_id: str | None = Field(default=None, alias="accountId")
|
||||||
|
config: dict[str, Any] | None = None
|
||||||
|
secrets: dict[str, str | None] | None = None
|
||||||
|
|
||||||
|
|
||||||
class WebChannelValidationResponse(BaseModel):
|
class WebChannelValidationResponse(BaseModel):
|
||||||
"""Connector validation response."""
|
"""Connector validation response."""
|
||||||
|
|
||||||
@ -1373,6 +1477,7 @@ Modify `app-instance/backend/beaver/interfaces/web/schemas/__init__.py` imports
|
|||||||
```python
|
```python
|
||||||
WebChannelConnectionCreateRequest,
|
WebChannelConnectionCreateRequest,
|
||||||
WebChannelConnectionResponse,
|
WebChannelConnectionResponse,
|
||||||
|
WebChannelConnectionUpdateRequest,
|
||||||
WebChannelValidationResponse,
|
WebChannelValidationResponse,
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -1384,6 +1489,7 @@ Modify imports in `app-instance/backend/beaver/interfaces/web/app.py`:
|
|||||||
from beaver.interfaces.web.schemas import (
|
from beaver.interfaces.web.schemas import (
|
||||||
WebChannelConnectionCreateRequest,
|
WebChannelConnectionCreateRequest,
|
||||||
WebChannelConnectionResponse,
|
WebChannelConnectionResponse,
|
||||||
|
WebChannelConnectionUpdateRequest,
|
||||||
WebChannelValidationResponse,
|
WebChannelValidationResponse,
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
@ -1394,11 +1500,21 @@ Add helper:
|
|||||||
def get_channel_connector_registry(request: Request) -> ChannelConnectorRegistry:
|
def get_channel_connector_registry(request: Request) -> ChannelConnectorRegistry:
|
||||||
registry = getattr(request.app.state, "channel_connector_registry", None)
|
registry = getattr(request.app.state, "channel_connector_registry", None)
|
||||||
if not isinstance(registry, ChannelConnectorRegistry):
|
if not isinstance(registry, ChannelConnectorRegistry):
|
||||||
|
workspace = getattr(request.app.state, "channel_connection_workspace", None)
|
||||||
|
if workspace is None:
|
||||||
agent_service = get_agent_service(request)
|
agent_service = get_agent_service(request)
|
||||||
workspace = agent_service.loader.workspace
|
workspace = agent_service.loader.workspace
|
||||||
registry = _build_channel_connector_registry(workspace)
|
registry = _build_channel_connector_registry(workspace)
|
||||||
request.app.state.channel_connector_registry = registry
|
request.app.state.channel_connector_registry = registry
|
||||||
return registry
|
return registry
|
||||||
|
|
||||||
|
|
||||||
|
def _connection_response_view(connection: Any) -> dict[str, Any]:
|
||||||
|
view = connection.to_dict()
|
||||||
|
view.pop("credentials_ref", None)
|
||||||
|
view.pop("connector_ref", None)
|
||||||
|
view.pop("pairing_session_id", None)
|
||||||
|
return view
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 6: Add connection API routes**
|
- [ ] **Step 6: Add connection API routes**
|
||||||
@ -1413,7 +1529,7 @@ Add routes near existing `/api/channels` routes in `app-instance/backend/beaver/
|
|||||||
@app.get("/api/channel-connections")
|
@app.get("/api/channel-connections")
|
||||||
async def list_channel_connections(request: Request) -> list[dict[str, Any]]:
|
async def list_channel_connections(request: Request) -> list[dict[str, Any]]:
|
||||||
registry = get_channel_connector_registry(request)
|
registry = get_channel_connector_registry(request)
|
||||||
return [connection.to_dict() for connection in registry.connection_store.list()]
|
return [_connection_response_view(connection) for connection in registry.connection_store.list()]
|
||||||
|
|
||||||
@app.post("/api/channel-connections", response_model=WebChannelConnectionResponse)
|
@app.post("/api/channel-connections", response_model=WebChannelConnectionResponse)
|
||||||
async def create_channel_connection(
|
async def create_channel_connection(
|
||||||
@ -1440,10 +1556,37 @@ Add routes near existing `/api/channels` routes in `app-instance/backend/beaver/
|
|||||||
runtime_config=payload.config or {},
|
runtime_config=payload.config or {},
|
||||||
)
|
)
|
||||||
return WebChannelConnectionResponse(
|
return WebChannelConnectionResponse(
|
||||||
connection=connection.to_dict(),
|
connection=_connection_response_view(connection),
|
||||||
credentials=registry.credential_store.redacted(credentials_ref),
|
credentials=registry.credential_store.redacted(credentials_ref),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@app.patch("/api/channel-connections/{connection_id}", response_model=WebChannelConnectionResponse)
|
||||||
|
async def update_channel_connection(
|
||||||
|
connection_id: str,
|
||||||
|
request: Request,
|
||||||
|
payload: WebChannelConnectionUpdateRequest,
|
||||||
|
) -> WebChannelConnectionResponse:
|
||||||
|
registry = get_channel_connector_registry(request)
|
||||||
|
try:
|
||||||
|
connection = registry.connection_store.get(connection_id)
|
||||||
|
except KeyError:
|
||||||
|
raise HTTPException(status_code=404, detail="Channel connection not found")
|
||||||
|
if payload.display_name is not None:
|
||||||
|
connection.display_name = _clean_text(payload.display_name) or connection.display_name
|
||||||
|
if payload.account_id is not None:
|
||||||
|
connection.account_id = _clean_text(payload.account_id) or connection.account_id
|
||||||
|
if payload.config is not None:
|
||||||
|
connection.runtime_config = payload.config
|
||||||
|
if payload.secrets:
|
||||||
|
secrets = {key: value for key, value in payload.secrets.items() if value}
|
||||||
|
if secrets:
|
||||||
|
connection.credentials_ref = registry.credential_store.put(kind=connection.kind, values=secrets)
|
||||||
|
connection = registry.connection_store.update(connection)
|
||||||
|
return WebChannelConnectionResponse(
|
||||||
|
connection=_connection_response_view(connection),
|
||||||
|
credentials=registry.credential_store.redacted(connection.credentials_ref),
|
||||||
|
)
|
||||||
|
|
||||||
@app.get("/api/channel-connections/{connection_id}", response_model=WebChannelConnectionResponse)
|
@app.get("/api/channel-connections/{connection_id}", response_model=WebChannelConnectionResponse)
|
||||||
async def get_channel_connection(connection_id: str, request: Request) -> WebChannelConnectionResponse:
|
async def get_channel_connection(connection_id: str, request: Request) -> WebChannelConnectionResponse:
|
||||||
registry = get_channel_connector_registry(request)
|
registry = get_channel_connector_registry(request)
|
||||||
@ -1452,7 +1595,7 @@ Add routes near existing `/api/channels` routes in `app-instance/backend/beaver/
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
raise HTTPException(status_code=404, detail="Channel connection not found")
|
raise HTTPException(status_code=404, detail="Channel connection not found")
|
||||||
return WebChannelConnectionResponse(
|
return WebChannelConnectionResponse(
|
||||||
connection=connection.to_dict(),
|
connection=_connection_response_view(connection),
|
||||||
credentials=registry.credential_store.redacted(connection.credentials_ref),
|
credentials=registry.credential_store.redacted(connection.credentials_ref),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1471,7 +1614,7 @@ Add routes near existing `/api/channels` routes in `app-instance/backend/beaver/
|
|||||||
display_name=result.display_name,
|
display_name=result.display_name,
|
||||||
error=result.error,
|
error=result.error,
|
||||||
metadata=result.metadata,
|
metadata=result.metadata,
|
||||||
connection=connection.to_dict(),
|
connection=_connection_response_view(connection),
|
||||||
)
|
)
|
||||||
|
|
||||||
@app.post("/api/channel-connections/{connection_id}/revoke", response_model=WebChannelConnectionResponse)
|
@app.post("/api/channel-connections/{connection_id}/revoke", response_model=WebChannelConnectionResponse)
|
||||||
@ -1482,7 +1625,7 @@ Add routes near existing `/api/channels` routes in `app-instance/backend/beaver/
|
|||||||
connection = registry.connection_store.get(connection_id)
|
connection = registry.connection_store.get(connection_id)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise HTTPException(status_code=404, detail="Channel connection not found")
|
raise HTTPException(status_code=404, detail="Channel connection not found")
|
||||||
return WebChannelConnectionResponse(connection=connection.to_dict(), credentials={})
|
return WebChannelConnectionResponse(connection=_connection_response_view(connection), credentials={})
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 7: Run API tests**
|
- [ ] **Step 7: Run API tests**
|
||||||
@ -1567,7 +1710,7 @@ Run:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd app-instance/backend
|
cd app-instance/backend
|
||||||
rg -n "token-1|bad-token|secret-token" tests/unit beaver || true
|
rg -n "token-1|token-2|bad-token|secret-token" tests/unit beaver || true
|
||||||
```
|
```
|
||||||
|
|
||||||
Expected: test fixture strings only appear in test files. They must not appear in implementation files or generated event log code.
|
Expected: test fixture strings only appear in test files. They must not appear in implementation files or generated event log code.
|
||||||
|
|||||||
Reference in New Issue
Block a user