This commit is contained in:
lda
2026-06-15 22:37:59 +07:00 Verified
parent fe0b2b4f4d
commit e8152fd4e6
32 changed files with 138 additions and 84 deletions
@@ -145,7 +145,9 @@ def challenge_report_schema_errors(report: dict[str, Any]) -> list[str]:
read = report.get("read") read = report.get("read")
if isinstance(read, dict): if isinstance(read, dict):
missing_read = sorted(CHALLENGE_REPORT_READ_FIELDS.difference(read)) missing_read = sorted(CHALLENGE_REPORT_READ_FIELDS.difference(read))
errors.extend(f"missing challenge_report.read.{field}" for field in missing_read) errors.extend(
f"missing challenge_report.read.{field}" for field in missing_read
)
for field in CHALLENGE_REPORT_READ_FIELDS.intersection(read): for field in CHALLENGE_REPORT_READ_FIELDS.intersection(read):
if not isinstance(read[field], bool): if not isinstance(read[field], bool):
errors.append(f"challenge_report.read.{field} must be boolean") errors.append(f"challenge_report.read.{field} must be boolean")
@@ -156,8 +158,7 @@ def challenge_report_schema_errors(report: dict[str, Any]) -> list[str]:
if isinstance(attempts, dict): if isinstance(attempts, dict):
missing_attempts = sorted(CHALLENGE_REPORT_ATTEMPT_FIELDS.difference(attempts)) missing_attempts = sorted(CHALLENGE_REPORT_ATTEMPT_FIELDS.difference(attempts))
errors.extend( errors.extend(
f"missing challenge_report.attempts.{field}" f"missing challenge_report.attempts.{field}" for field in missing_attempts
for field in missing_attempts
) )
for field in CHALLENGE_REPORT_ATTEMPT_FIELDS.intersection(attempts): for field in CHALLENGE_REPORT_ATTEMPT_FIELDS.intersection(attempts):
value = attempts[field] value = attempts[field]
@@ -467,7 +467,9 @@ def main(argv: list[str] | None = None) -> int:
success_count = sum( success_count = sum(
1 for item in summaries if item["classification"] == "success" 1 for item in summaries if item["classification"] == "success"
) )
print(json.dumps({"success_count": success_count, "trial_count": len(summaries)})) print(
json.dumps({"success_count": success_count, "trial_count": len(summaries)})
)
return 0 return 0
finally: finally:
if managed_server is not None: if managed_server is not None:
+1 -1
View File
@@ -1,11 +1,11 @@
from __future__ import annotations from __future__ import annotations
import html
import json import json
import threading import threading
import uuid import uuid
import webbrowser import webbrowser
from dataclasses import dataclass from dataclasses import dataclass
import html
from http import HTTPStatus from http import HTTPStatus
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from typing import ClassVar from typing import ClassVar
+1 -2
View File
@@ -113,8 +113,7 @@ def _render_markdown_report(payload: MarkdownInput) -> MarkdownOutput:
"Actions:", "Actions:",
] ]
lines.extend( lines.extend(
f"- {item.owner} | {item.task} | {item.due}" f"- {item.owner} | {item.task} | {item.due}" for item in report.action_items
for item in report.action_items
) )
lines.extend(["", "Risks:"]) lines.extend(["", "Risks:"])
lines.extend(f"- {risk}" for risk in report.risks) lines.extend(f"- {risk}" for risk in report.risks)
+1 -5
View File
@@ -24,11 +24,7 @@ def validate_deployment_dependencies(
for logical_source, concrete_source in bindings.items(): for logical_source, concrete_source in bindings.items():
source = sources_by_id.get(logical_source) source = sources_by_id.get(logical_source)
if ( if source is not None and source.platform and concrete_source != logical_source:
source is not None
and source.platform
and concrete_source != logical_source
):
diagnostics.append( diagnostics.append(
DependencyDiagnostic( DependencyDiagnostic(
severity=DiagnosticSeverity.ERROR, severity=DiagnosticSeverity.ERROR,
+3 -1
View File
@@ -112,7 +112,9 @@ def reducer(
def decorate_config( def decorate_config(
raw: ConfigReducerCallable[ConfigT], raw: ConfigReducerCallable[ConfigT],
) -> AuthoredReducer: ) -> AuthoredReducer:
reducer_name = name or getattr(raw, "__name__", "<anonymous config reducer>") reducer_name = name or getattr(
raw, "__name__", "<anonymous config reducer>"
)
reducer_description = description or raw.__doc__ reducer_description = description or raw.__doc__
model_type = config_model model_type = config_model
+2 -3
View File
@@ -22,6 +22,7 @@ def _parse_map_flags(values: list[str] | None) -> dict[str, str]:
parsed[source] = target parsed[source] = target
return parsed return parsed
app = typer.Typer( app = typer.Typer(
name="draft", name="draft",
help="Create, inspect, patch, validate, and save draft workflows.", help="Create, inspect, patch, validate, and save draft workflows.",
@@ -162,9 +163,7 @@ def set_draft_route(
], ],
step_id: Annotated[str, typer.Option("--step", help="Draft step id.")], step_id: Annotated[str, typer.Option("--step", help="Draft step id.")],
outcome: Annotated[str, typer.Option("--outcome", help="Step outcome.")], outcome: Annotated[str, typer.Option("--outcome", help="Step outcome.")],
target: Annotated[ target: Annotated[str, typer.Option("--to", help="Target step id or __end__.")],
str, typer.Option("--to", help="Target step id or __end__.")
],
) -> None: ) -> None:
"""Set one route: steps.<step> outcome -> target.""" """Set one route: steps.<step> outcome -> target."""
context = load_cli_context(ctx) context = load_cli_context(ctx)
+3 -1
View File
@@ -148,5 +148,7 @@ def _source_capability_names(
raise ValueError("source inventory missing capabilities object") raise ValueError("source inventory missing capabilities object")
value = capabilities.get(capability_key) value = capabilities.get(capability_key)
if not isinstance(value, list): if not isinstance(value, list):
raise ValueError(f"source inventory capabilities.{capability_key} must be a list") raise ValueError(
f"source inventory capabilities.{capability_key} must be a list"
)
return [str(item) for item in value] return [str(item) for item in value]
+3 -1
View File
@@ -124,7 +124,9 @@ class ContentAccessService:
return _truncate_resource_payload(payload, max_chars=max_chars) return _truncate_resource_payload(payload, max_chars=max_chars)
def _truncate_resource_payload(payload: dict[str, Any], *, max_chars: int) -> dict[str, Any]: def _truncate_resource_payload(
payload: dict[str, Any], *, max_chars: int
) -> dict[str, Any]:
"""Bound text content returned by source URI reads without mutating upstream payload.""" """Bound text content returned by source URI reads without mutating upstream payload."""
contents = payload.get("contents") contents = payload.get("contents")
@@ -150,7 +150,9 @@ class SourceDiagnosticsProvider:
"fetched_at_epoch_ms": None "fetched_at_epoch_ms": None
if snapshot is None if snapshot is None
else snapshot.fetched_at_epoch_ms, else snapshot.fetched_at_epoch_ms,
"max_age_seconds": None if snapshot is None else snapshot.max_age_seconds, "max_age_seconds": None
if snapshot is None
else snapshot.max_age_seconds,
"node_count": 0 if snapshot is None else len(snapshot.nodes), "node_count": 0 if snapshot is None else len(snapshot.nodes),
"resource_count": 0 if snapshot is None else len(snapshot.resources), "resource_count": 0 if snapshot is None else len(snapshot.resources),
"prompt_count": 0 if snapshot is None else len(snapshot.prompts), "prompt_count": 0 if snapshot is None else len(snapshot.prompts),
@@ -88,7 +88,13 @@ class WorkflowRuntimeService:
deployment: WorkflowDeployment | None, deployment: WorkflowDeployment | None,
artifact: WorkflowArtifact | None, artifact: WorkflowArtifact | None,
saved_subgraph_tree: SavedSubgraphTree | None = None, saved_subgraph_tree: SavedSubgraphTree | None = None,
) -> tuple[Workflow, dict[str, Any], dict[str, Any], dict[str, Any], SourceBindingPlatformContext]: ) -> tuple[
Workflow,
dict[str, Any],
dict[str, Any],
dict[str, Any],
SourceBindingPlatformContext,
]:
"""Resolve bindings once into the executable pieces core expects. """Resolve bindings once into the executable pieces core expects.
Saved-run resume still rebuilds prepared dependencies from the current Saved-run resume still rebuilds prepared dependencies from the current
+7 -1
View File
@@ -151,7 +151,13 @@ class LocalWorkflowRuntimeRunner(WorkflowRuntimeRunner):
deployment: WorkflowDeployment | None, deployment: WorkflowDeployment | None,
artifact: WorkflowArtifact | None, artifact: WorkflowArtifact | None,
saved_subgraph_tree: SavedSubgraphTree | None = None, saved_subgraph_tree: SavedSubgraphTree | None = None,
) -> tuple[Workflow, dict[str, Any], dict[str, Any], dict[str, Any], SourceBindingPlatformContext]: ) -> tuple[
Workflow,
dict[str, Any],
dict[str, Any],
dict[str, Any],
SourceBindingPlatformContext,
]:
plan_node_names = [ plan_node_names = [
node.node for node in plan.nodes if isinstance(node, NodeUse) node.node for node in plan.nodes if isinstance(node, NodeUse)
] ]
+9 -3
View File
@@ -186,7 +186,9 @@ class HttpxOAuthTokenRefresher:
payload = response.json() payload = response.json()
access_token = payload.get("access_token") access_token = payload.get("access_token")
if not isinstance(access_token, str) or not access_token: if not isinstance(access_token, str) or not access_token:
raise ValueError("OAuth token refresh response did not include access_token") raise ValueError(
"OAuth token refresh response did not include access_token"
)
expires_in = payload.get("expires_in") expires_in = payload.get("expires_in")
return OAuthAccessToken( return OAuthAccessToken(
access_token=access_token, access_token=access_token,
@@ -232,7 +234,9 @@ class McpAuthBinder:
if isinstance(auth, EnvAuth): if isinstance(auth, EnvAuth):
raise ValueError("env auth is not supported for MCP HTTP") raise ValueError("env auth is not supported for MCP HTTP")
if isinstance(auth, OpaqueAuth): if isinstance(auth, OpaqueAuth):
raise ValueError(f"opaque auth scheme {auth.scheme!r} is not supported for MCP HTTP") raise ValueError(
f"opaque auth scheme {auth.scheme!r} is not supported for MCP HTTP"
)
raise TypeError(f"unsupported auth variant {type(auth).__name__}") raise TypeError(f"unsupported auth variant {type(auth).__name__}")
async def bind_stdio_auth( async def bind_stdio_auth(
@@ -244,7 +248,9 @@ class McpAuthBinder:
auth = record.auth auth = record.auth
if isinstance(auth, EnvAuth): if isinstance(auth, EnvAuth):
return BoundMcpStdioAuth(env=dict(auth.env)) return BoundMcpStdioAuth(env=dict(auth.env))
if isinstance(auth, BearerAuth | HeaderAuth | OAuthRefreshTokenAuth | OpaqueAuth): if isinstance(
auth, BearerAuth | HeaderAuth | OAuthRefreshTokenAuth | OpaqueAuth
):
raise ValueError(f"{auth.kind} auth is not supported for MCP stdio") raise ValueError(f"{auth.kind} auth is not supported for MCP stdio")
raise TypeError(f"unsupported auth variant {type(auth).__name__}") raise TypeError(f"unsupported auth variant {type(auth).__name__}")
+3 -1
View File
@@ -16,7 +16,9 @@ from wf_sources_mcp.connections import McpSourceConnection
from wf_sources_mcp.transports import HttpSourceTransport, StdioSourceTransport from wf_sources_mcp.transports import HttpSourceTransport, StdioSourceTransport
def _as_stored_auth(auth: AuthRecord | StoredAuthRecord | None) -> StoredAuthRecord | None: def _as_stored_auth(
auth: AuthRecord | StoredAuthRecord | None,
) -> StoredAuthRecord | None:
if auth is None or isinstance(auth, StoredAuthRecord): if auth is None or isinstance(auth, StoredAuthRecord):
return auth return auth
return auth_record_from_compat( return auth_record_from_compat(
+4 -4
View File
@@ -65,10 +65,10 @@ async def _list_optional_capabilities(
root = _root_exception(exc) root = _root_exception(exc)
if isinstance(root, McpError) and root.error.code == METHOD_NOT_FOUND: if isinstance(root, McpError) and root.error.code == METHOD_NOT_FOUND:
return [] return []
if ( if isinstance(root, httpx.HTTPStatusError) and root.response.status_code in {
isinstance(root, httpx.HTTPStatusError) 400,
and root.response.status_code in {400, 404} 404,
): }:
return [] return []
raise raise
+9 -3
View File
@@ -59,7 +59,9 @@ class AuthStore:
def save_auth_record(self, record: NeutralAuthRecord | StoredAuthRecord) -> None: def save_auth_record(self, record: NeutralAuthRecord | StoredAuthRecord) -> None:
raise NotImplementedError raise NotImplementedError
def load_auth_record(self, auth_ref: str) -> NeutralAuthRecord | StoredAuthRecord | None: def load_auth_record(
self, auth_ref: str
) -> NeutralAuthRecord | StoredAuthRecord | None:
raise NotImplementedError raise NotImplementedError
def delete_auth(self, connection_id: str) -> bool: def delete_auth(self, connection_id: str) -> bool:
@@ -139,7 +141,9 @@ class FileAuthStore(AuthStore):
else: else:
self.save_auth(mcp_auth_from_neutral(record)) self.save_auth(mcp_auth_from_neutral(record))
def load_auth_record(self, auth_ref: str) -> NeutralAuthRecord | StoredAuthRecord | None: def load_auth_record(
self, auth_ref: str
) -> NeutralAuthRecord | StoredAuthRecord | None:
path = self._auth_path(auth_ref) path = self._auth_path(auth_ref)
if not path.exists(): if not path.exists():
return None return None
@@ -258,7 +262,9 @@ class FileStore(Store):
def save_auth_record(self, record: NeutralAuthRecord | StoredAuthRecord) -> None: def save_auth_record(self, record: NeutralAuthRecord | StoredAuthRecord) -> None:
self._auth.save_auth_record(record) self._auth.save_auth_record(record)
def load_auth_record(self, auth_ref: str) -> NeutralAuthRecord | StoredAuthRecord | None: def load_auth_record(
self, auth_ref: str
) -> NeutralAuthRecord | StoredAuthRecord | None:
return self._auth.load_auth_record(auth_ref) return self._auth.load_auth_record(auth_ref)
def delete_auth(self, connection_id: str) -> bool: def delete_auth(self, connection_id: str) -> bool:
+1 -3
View File
@@ -43,8 +43,6 @@ def register_methods(
params: DiagnoseSourceParams = RpcParams(), params: DiagnoseSourceParams = RpcParams(),
) -> dict[str, Any]: ) -> dict[str, Any]:
try: try:
return await server.source_admin.diagnose_source( return await server.source_admin.diagnose_source(source_id=params.source_id)
source_id=params.source_id
)
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc: except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
raise_workflow_rpc_error(exc) raise_workflow_rpc_error(exc)
@@ -68,7 +68,9 @@ def test_browser_click_source_human_timeout_cleans_up() -> None:
assert _active_session_count() == 0 assert _active_session_count() == 0
EXAMPLE_DIR = Path(__file__).resolve().parents[2] / "examples" / "browser_click_workflow" EXAMPLE_DIR = (
Path(__file__).resolve().parents[2] / "examples" / "browser_click_workflow"
)
@pytest.mark.asyncio @pytest.mark.asyncio
@@ -234,4 +236,3 @@ async def test_browser_click_workflow_artifact_deployment_run_path(tmp_path) ->
assert run["output"]["after"]["status_text"] == "Button clicked" assert run["output"]["after"]["status_text"] == "Button clicked"
assert run["output"]["closed"] is True assert run["output"]["closed"] is True
assert run["trace_count"] >= 3 assert run["trace_count"] >= 3
+3 -1
View File
@@ -103,7 +103,9 @@ 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() -> None: async def test_call_openapi_operation_maps_invalid_request_to_validation_error() -> (
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"
-1
View File
@@ -1,6 +1,5 @@
from __future__ import annotations from __future__ import annotations
import asyncio
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
+24 -15
View File
@@ -2,7 +2,6 @@
from __future__ import annotations from __future__ import annotations
import asyncio
from dataclasses import replace from dataclasses import replace
from pathlib import Path from pathlib import Path
from typing import Any, cast from typing import Any, cast
@@ -98,27 +97,27 @@ def _deployment_api(
context = context_from_service(service) context = context_from_service(service)
return WorkflowDeploymentApi(context), service return WorkflowDeploymentApi(context), service
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_save_deployment_stores_and_returns_stable_fields(tmp_path: Path) -> None: async def test_save_deployment_stores_and_returns_stable_fields(tmp_path: Path) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "deploy_save") artifact_store = FileWorkflowArtifactStore(tmp_path / "deploy_save")
api, _service = _deployment_api(artifact_store) api, _service = _deployment_api(artifact_store)
result = await api.save_deployment( result = await api.save_deployment(
WorkflowDeployment( WorkflowDeployment(
id="echo.personal", id="echo.personal",
artifact_id="echo", artifact_id="echo",
artifact_version=1, artifact_version=1,
bindings=[ bindings=[{"logical_source": "demo", "concrete_source": "demo.personal"}],
{"logical_source": "demo", "concrete_source": "demo.personal"} ).model_dump(mode="json")
], )
).model_dump(mode="json")
)
assert result["saved"] is True assert result["saved"] is True
assert result["deployment_id"] == "echo.personal" assert result["deployment_id"] == "echo.personal"
assert result["artifact_id"] == "echo" assert result["artifact_id"] == "echo"
assert result["artifact_version"] == 1 assert result["artifact_version"] == 1
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_list_deployments_returns_compact_summaries(tmp_path: Path) -> None: async def test_list_deployments_returns_compact_summaries(tmp_path: Path) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "deploy_list") artifact_store = FileWorkflowArtifactStore(tmp_path / "deploy_list")
@@ -141,7 +140,9 @@ async def test_list_deployments_returns_compact_summaries(tmp_path: Path) -> Non
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_list_deployments_returns_empty_without_artifact_store(tmp_path: Path) -> None: async def test_list_deployments_returns_empty_without_artifact_store(
tmp_path: Path,
) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "deploy_no_store") artifact_store = FileWorkflowArtifactStore(tmp_path / "deploy_no_store")
_api, service = _deployment_api(artifact_store) _api, service = _deployment_api(artifact_store)
context = replace(context_from_service(service), artifact_store=None) context = replace(context_from_service(service), artifact_store=None)
@@ -173,7 +174,9 @@ async def test_delete_deployment_removes_one(tmp_path: Path) -> None:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_validate_deployment_returns_runnable_for_valid_binding(tmp_path: Path) -> None: async def test_validate_deployment_returns_runnable_for_valid_binding(
tmp_path: Path,
) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "deploy_validate_runnable") artifact_store = FileWorkflowArtifactStore(tmp_path / "deploy_validate_runnable")
api, service = _deployment_api(artifact_store, register_echo=True) api, service = _deployment_api(artifact_store, register_echo=True)
artifact_store.save_artifact(_echo_artifact()) artifact_store.save_artifact(_echo_artifact())
@@ -186,7 +189,9 @@ async def test_validate_deployment_returns_runnable_for_valid_binding(tmp_path:
) )
) )
result = await api.validate_deployment(deployment_id="echo.personal", live_check=False) result = await api.validate_deployment(
deployment_id="echo.personal", live_check=False
)
assert result["status"] == "runnable" assert result["status"] == "runnable"
assert result["diagnostics"] == [] assert result["diagnostics"] == []
@@ -202,7 +207,9 @@ class FailingLivenessAdapter:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_validate_deployment_live_check_calls_live_checker(tmp_path: Path) -> None: async def test_validate_deployment_live_check_calls_live_checker(
tmp_path: Path,
) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "deploy_validate_live") artifact_store = FileWorkflowArtifactStore(tmp_path / "deploy_validate_live")
api, service = _deployment_api(artifact_store, register_echo=True) api, service = _deployment_api(artifact_store, register_echo=True)
artifact_store.save_artifact(_echo_artifact()) artifact_store.save_artifact(_echo_artifact())
@@ -219,7 +226,9 @@ async def test_validate_deployment_live_check_calls_live_checker(tmp_path: Path)
cast(BackendAdapter, FailingLivenessAdapter()), cast(BackendAdapter, FailingLivenessAdapter()),
) )
result = await api.validate_deployment(deployment_id="echo.personal", live_check=True) result = await api.validate_deployment(
deployment_id="echo.personal", live_check=True
)
assert result["status"] == "unrunnable" assert result["status"] == "unrunnable"
assert result["diagnostics"][0]["code"] == "source_unreachable" assert result["diagnostics"][0]["code"] == "source_unreachable"
-1
View File
@@ -1,6 +1,5 @@
from __future__ import annotations from __future__ import annotations
import asyncio
from pathlib import Path from pathlib import Path
import pytest import pytest
-1
View File
@@ -1,6 +1,5 @@
from __future__ import annotations from __future__ import annotations
import asyncio
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
+3 -1
View File
@@ -25,7 +25,9 @@ def test_platform_context_uses_identity_for_platform_sources() -> None:
def test_platform_context_rejects_unbound_source() -> None: def test_platform_context_rejects_unbound_source() -> None:
context = SourceBindingPlatformContext(source_bindings={}, read_resource_handler=None) context = SourceBindingPlatformContext(
source_bindings={}, read_resource_handler=None
)
with pytest.raises(KeyError, match="unbound logical source"): with pytest.raises(KeyError, match="unbound logical source"):
context.resolve_source("drive") context.resolve_source("drive")
+3 -1
View File
@@ -214,7 +214,9 @@ edges: []
assert payload["source_bindings"] == {"local.ops": "local.ops"} assert payload["source_bindings"] == {"local.ops": "local.ops"}
def test_artifact_create_from_plan_rejects_non_object_yaml(monkeypatch, tmp_path) -> None: def test_artifact_create_from_plan_rejects_non_object_yaml(
monkeypatch, tmp_path
) -> None:
handlers = _ArtifactHandlers() handlers = _ArtifactHandlers()
monkeypatch.setattr( monkeypatch.setattr(
"wf_cli.commands.artifacts.load_cli_context", "wf_cli.commands.artifacts.load_cli_context",
+17 -9
View File
@@ -77,12 +77,16 @@ class _FakeOAuthClient:
self.auth_kwargs: dict[str, object] = {} self.auth_kwargs: dict[str, object] = {}
self.fetch_calls: list[str] = [] self.fetch_calls: list[str] = []
def create_authorization_url(self, auth_url: str, **kwargs: object) -> tuple[str, str]: def create_authorization_url(
self, auth_url: str, **kwargs: object
) -> tuple[str, str]:
assert auth_url == "https://accounts.google.com/o/oauth2/v2/auth" assert auth_url == "https://accounts.google.com/o/oauth2/v2/auth"
self.auth_kwargs = dict(kwargs) self.auth_kwargs = dict(kwargs)
return self.authorization_url, "state-123" return self.authorization_url, "state-123"
async def fetch_token(self, token_url: str, authorization_response: str) -> dict[str, object]: async def fetch_token(
self, token_url: str, authorization_response: str
) -> dict[str, object]:
self.fetch_calls.append(authorization_response) self.fetch_calls.append(authorization_response)
return { return {
"refresh_token": "refresh", "refresh_token": "refresh",
@@ -144,7 +148,9 @@ async def test_oauth_code_login_flow_callback_can_supply_response() -> None:
assert client.fetch_calls == ["http://127.0.0.1/callback?code=abc&state=state-123"] assert client.fetch_calls == ["http://127.0.0.1/callback?code=abc&state=state-123"]
def test_auth_oauth_login_saves_record_from_provider_profile(monkeypatch, tmp_path) -> None: def test_auth_oauth_login_saves_record_from_provider_profile(
monkeypatch, tmp_path
) -> None:
saved: list[dict[str, object]] = [] saved: list[dict[str, object]] = []
class _FakeAdmin: class _FakeAdmin:
@@ -178,15 +184,17 @@ def test_auth_oauth_login_saves_record_from_provider_profile(monkeypatch, tmp_pa
"auth_url": "https://accounts.google.com/o/oauth2/v2/auth", "auth_url": "https://accounts.google.com/o/oauth2/v2/auth",
"token_url": "https://oauth2.googleapis.com/token", "token_url": "https://oauth2.googleapis.com/token",
"client_id_env": "GOOGLE_OAUTH_CLIENT_ID", "client_id_env": "GOOGLE_OAUTH_CLIENT_ID",
"scopes": ["https://www.googleapis.com/auth/drive.readonly"], "scopes": [
"extra_authorize_params": { "https://www.googleapis.com/auth/drive.readonly"
"access_type": "offline", ],
"prompt": "consent", "extra_authorize_params": {
}, "access_type": "offline",
"prompt": "consent",
},
}
} }
} }
} }
}
), ),
encoding="utf-8", encoding="utf-8",
) )
+4 -4
View File
@@ -170,12 +170,12 @@ def test_wf_config_validate_uses_global_config_when_path_omitted(
payload = json.loads(result.output) payload = json.loads(result.output)
assert payload["valid"] is True assert payload["valid"] is True
assert payload["path"] == str(config_path) assert payload["path"] == str(config_path)
assert payload["sources"] == [ assert payload["sources"] == [{"id": "wf.std", "kind": "stdlib", "status": "ok"}]
{"id": "wf.std", "kind": "stdlib", "status": "ok"}
]
def test_wf_config_validate_reports_python_source_import_failure(tmp_path: Path) -> None: def test_wf_config_validate_reports_python_source_import_failure(
tmp_path: Path,
) -> None:
config_path = tmp_path / "wf.config.json" config_path = tmp_path / "wf.config.json"
config_path.write_text( config_path.write_text(
json.dumps( json.dumps(
+1 -3
View File
@@ -268,9 +268,7 @@ async def test_content_access_uses_stateful_runtime_for_upstream_content() -> No
async def test_read_resource_by_source_uri_reads_upstream() -> None: async def test_read_resource_by_source_uri_reads_upstream() -> None:
service = WfMcpService( service = WfMcpService(store=FileStore(local_temp_root() / "content_source_uri"))
store=FileStore(local_temp_root() / "content_source_uri")
)
service.register_connection( service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal") ConnectionConfig(id="demo.personal", server="demo", account="personal")
) )
@@ -20,7 +20,9 @@ def _connection(**metadata: object) -> ConnectionConfig:
) )
def _provider(tmp_path: Path, connection: ConnectionConfig) -> SourceDiagnosticsProvider: def _provider(
tmp_path: Path, connection: ConnectionConfig
) -> SourceDiagnosticsProvider:
registry = ConnectionRegistry() registry = ConnectionRegistry()
registry.register(connection) registry.register(connection)
return SourceDiagnosticsProvider( return SourceDiagnosticsProvider(
@@ -100,10 +100,12 @@ def test_workflow_runtime_service_prepares_node_registry_and_reducers() -> None:
emit_event=lambda event: None, emit_event=lambda event: None,
) )
workflow, registry, reducers, prepared_subgraphs, platform_context = runtime.prepare_workflow_runtime( workflow, registry, reducers, prepared_subgraphs, platform_context = (
single_echo_plan("runtime_prepare", "demo.personal.echo_tool"), runtime.prepare_workflow_runtime(
deployment=None, single_echo_plan("runtime_prepare", "demo.personal.echo_tool"),
artifact=None, deployment=None,
artifact=None,
)
) )
assert "demo.personal.echo_tool" in [nd.name for nd in workflow.node_defs] assert "demo.personal.echo_tool" in [nd.name for nd in workflow.node_defs]
@@ -129,7 +129,9 @@ def test_file_auth_store_writes_new_stored_auth_record_shape(tmp_path: Path) ->
assert data["metadata"]["provider"] == "google" assert data["metadata"]["provider"] == "google"
def test_file_auth_store_loads_old_format_through_load_auth_record(tmp_path: Path) -> None: def test_file_auth_store_loads_old_format_through_load_auth_record(
tmp_path: Path,
) -> None:
store = FileAuthStore(tmp_path) store = FileAuthStore(tmp_path)
path = tmp_path / "auth" / "github.work.json" path = tmp_path / "auth" / "github.work.json"
path.write_text( path.write_text(
+4 -4
View File
@@ -449,7 +449,9 @@ async def test_rpc_client_draft_workspace_focused_edit_methods(tmp_path) -> None
server = build_local_static_workflow_server(tmp_path / "store") server = build_local_static_workflow_server(tmp_path / "store")
app = create_rpc_app(server) app = create_rpc_app(server)
transport = httpx.ASGITransport(app=app) transport = httpx.ASGITransport(app=app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as http_client: async with httpx.AsyncClient(
transport=transport, base_url="http://test"
) as http_client:
client = RpcWorkflowApiClient( client = RpcWorkflowApiClient(
url="http://test/rpc", url="http://test/rpc",
timeout_seconds=5, timeout_seconds=5,
@@ -503,6 +505,4 @@ async def test_rpc_client_diagnoses_source(tmp_path) -> None:
payload = await Client().diagnose_source(source_id="demo.personal") payload = await Client().diagnose_source(source_id="demo.personal")
assert payload == {"source_id": "demo.personal", "status": "ok"} assert payload == {"source_id": "demo.personal", "status": "ok"}
assert calls == [ assert calls == [("workflow.sources.diagnose", {"source_id": "demo.personal"})]
("workflow.sources.diagnose", {"source_id": "demo.personal"})
]