more half-ass async migrations + basedpyright warning

This commit is contained in:
lda
2026-06-12 08:07:15 +07:00 Verified
parent 862d3340da
commit 510590239a
8 changed files with 206 additions and 179 deletions
+28 -16
View File
@@ -1,68 +1,80 @@
# pitfalls / guide # Agent guide
## tech stack ## pitfalls / guide
### tech stack
Python baseline is 3.14 (`requires-python = ">=3.14"`). Python 3.14 syntax is allowed; do not "fix" valid new syntax just because it looks unusual. Python baseline is 3.14 (`requires-python = ">=3.14"`). Python 3.14 syntax is allowed; do not "fix" valid new syntax just because it looks unusual.
Example new syntax: Example new syntax:
- Parentheses-Free Exceptions (PEP 758) <!-- coderabbit --> - Parentheses-Free Exceptions (PEP 758) <!-- coderabbit! -->
## extra fields ### extra fields
prefer asserts actual['field'] == expected['field'] over assert actual == expected unless we know better (eg. no extra fields allowed) prefer asserts actual['field'] == expected['field'] over assert actual == expected unless we know better (eg. no extra fields allowed)
## tests ### tests
Prefer pytest `tmp_path` for test-local filesystem state. Avoid fixed paths under `local_temp_root()` for tests that create durable files unless the test explicitly cleans or needs cross-process persistence; stale files there can change later test runs. Prefer pytest `tmp_path` for test-local filesystem state. Avoid fixed paths under `local_temp_root()` for tests that create durable files unless the test explicitly cleans or needs cross-process persistence; stale files there can change later test runs. (almost never the case btw)
Now that pytest-asyncio is installed, prefer `async def test_x()` Now that pytest-asyncio is installed, prefer `async def test_x()`
instead of `def test_x(): async def scenario(): ...; asyncio.run(scenario())` instead of `def test_x(): async def scenario(): ...; asyncio.run(scenario())`
## mgmt ### mgmt
More packages please. we spent a while cleaning flatten packages/modules; putting files of similar interests in folders and sub-folders. More packages please. we spent a while cleaning flatten packages/modules; putting files of similar interests in folders and sub-folders.
example: some of tests/ and some packages. (simple example: src/pack/foo_bar.py -> src/pack/foo/bar.py) example: some of tests/ and some packages. (simple example: src/pack/foo_bar.py -> src/pack/foo/bar.py)
Lets just do that from the start this time, ok? Lets just do that from the start this time, ok?
## Docs mgmt ### Docs mgmt
read docs/AGENTS.md read docs/AGENTS.md
more later more later
# Test suite ## Test suite
```bash ```bash
uv run /* --env-file .env */ pytest -q uv run /* --env-file .env */ pytest -q
uv run ruff check; uv run ruff format uv run ruff check; uv run ruff format
uv run basedpyright --level error # error to cut spam uv run basedpyright # --level error # to cut spam if typeCheckingMode = "recommended", but its "basic" now
# maybe uvx ty ## maybe uvx ty
``` ```
or so i think. or so i think.
## project is getting big ### project is getting big
scope your calls lads. else timeouts. not good for rapid testings scope your calls lads. else timeouts. not good for rapid testings
# code ## code
## docstrings/comment ### docstrings/comment
- add docstrings or comments around weird or non-obvious logic. - add docstrings or comments around weird or non-obvious logic.
- Add docstrings explaining compound return types that otherwise say nothing (e.g. `tuple[list[str], Any]`) - Add docstrings explaining compound return types that otherwise say nothing (e.g. `tuple[list[str], Any]`)
- Polish the thing (at least its docs) if you keep using it (helper fn, common class) - Polish the thing (at least its docs) if you keep using it (helper fn, common class)
## partial impls ### partial impls
If code has a partial implementation and docs mention the limitation, add a If code has a partial implementation and docs mention the limitation, add a
short comment or docstring at the code seam too. Future agents see code before short comment or docstring at the code seam too. Future agents see code before
they see old plans. they see old plans.
# skills ## skills
im looking at you superpowers im looking at you superpowers
skills screaming at you IMPORTANT CRITICAL bs. Use your best judgements. maybe they are critical idk you tell me skills screaming at you IMPORTANT CRITICAL bs. Use your best judgements. maybe they are critical idk you tell me
### mcp tools
<!-- looking at you opencode/mimo -->
`serena-agent` is likely set up. You want to use it for symbol discovery (akin to the `outline` tab in vscode)
(maybe for symbol renames as well). It is a strong tool!
if you reach for it to do general file editing, built-in tools may be better
If you notice an MCP tool that seems irrelevant to the project or is cluttering your available tools, mention it so we can disable it.
+28
View File
@@ -58,3 +58,31 @@ docs are the usual places.
If code has a partial implementation and docs mention the limitation, add a If code has a partial implementation and docs mention the limitation, add a
short comment or docstring at the code seam too. Future agents see code before short comment or docstring at the code seam too. Future agents see code before
they see old plans. they see old plans.
# docs formatting
## (caution: unstable) markdown formatting
use `pnpx markdownlint-cli --fix '(glob the md)'`
Note that this will mess things up, if you dont already follow the strict rules of markdownlint
### pitfall
all about indenting.
````md
1. you have a list, ordered or not?
this line ends the list, because it has no indents.
```bash
## even this code block has to be indented
```
2. otherwise this will be "fixed" and renumbered to 1.
````
### why not prettier? it does much more
when prettier supports compact table i'll switch to it
+2 -2
View File
@@ -34,7 +34,7 @@ dev = [
] ]
[tool.pytest.ini_options] [tool.pytest.ini_options]
addopts = "-p no:cacheprovider -n auto --basetemp .pytest-tmp" addopts = "-p no:cacheprovider -n auto --basetemp .pytest-tmp" # in-workspace temp dir
pythonpath = ["."] pythonpath = ["."]
asyncio_mode = "auto" asyncio_mode = "auto"
@@ -42,7 +42,7 @@ asyncio_mode = "auto"
package = true package = true
[tool.basedpyright] [tool.basedpyright]
typeCheckingMode = "basic" typeCheckingMode = "basic" # too many errors
[tool.ruff.format] [tool.ruff.format]
preview = false preview = false
+3 -2
View File
@@ -88,7 +88,7 @@ def reducer(
def decorate_plain( def decorate_plain(
raw: PlainReducerCallable, raw: PlainReducerCallable,
) -> AuthoredReducer: ) -> AuthoredReducer:
reducer_name = name or raw.__name__ reducer_name = name or getattr(raw, "__name__", "<anonymous plain reducer>")
reducer_description = description or raw.__doc__ reducer_description = description or raw.__doc__
# if no config model is provided, we assume it's a plain reducer and just wrap it directly # if no config model is provided, we assume it's a plain reducer and just wrap it directly
return AuthoredReducer( return AuthoredReducer(
@@ -102,6 +102,7 @@ def reducer(
) )
) )
# fail-fast? worse treatment than @node
if fn is not None: if fn is not None:
return decorate_plain(cast(PlainReducerCallable, fn)) return decorate_plain(cast(PlainReducerCallable, fn))
return decorate_plain return decorate_plain
@@ -111,7 +112,7 @@ def reducer(
def decorate_config( def decorate_config(
raw: ConfigReducerCallable[ConfigT], raw: ConfigReducerCallable[ConfigT],
) -> AuthoredReducer: ) -> AuthoredReducer:
reducer_name = name or raw.__name__ 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 -2
View File
@@ -96,7 +96,7 @@ def test_openapi_source_can_be_registered_and_inspected_by_service(
) )
def test_source_node_passes_operation_config_and_payload_to_execution( async def test_source_node_passes_operation_config_and_payload_to_execution(
monkeypatch, monkeypatch,
) -> None: ) -> None:
captured: dict[str, Any] = {} captured: dict[str, Any] = {}
@@ -137,7 +137,7 @@ def test_source_node_passes_operation_config_and_payload_to_execution(
RuntimeContext(current_node_id="petstore.default.get_pet"), RuntimeContext(current_node_id="petstore.default.get_pet"),
) )
asyncio.run(run_handler()) await run_handler()
operation = captured["operation"] operation = captured["operation"]
config = captured["config"] config = captured["config"]
+24 -23
View File
@@ -7,6 +7,8 @@ from dataclasses import replace
from pathlib import Path from pathlib import Path
from typing import Any, cast from typing import Any, cast
import pytest
from tests.wf_mcp.test_support import echo_tool from tests.wf_mcp.test_support import echo_tool
from wf_api.deployments import WorkflowDeploymentApi from wf_api.deployments import WorkflowDeploymentApi
from wf_artifacts import ( from wf_artifacts import (
@@ -96,13 +98,12 @@ def _deployment_api(
context = context_from_service(service) context = context_from_service(service)
return WorkflowDeploymentApi(context), service return WorkflowDeploymentApi(context), service
@pytest.mark.asyncio
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 = asyncio.run( result = await api.save_deployment(
api.save_deployment(
WorkflowDeployment( WorkflowDeployment(
id="echo.personal", id="echo.personal",
artifact_id="echo", artifact_id="echo",
@@ -112,15 +113,14 @@ def test_save_deployment_stores_and_returns_stable_fields(tmp_path: Path) -> Non
], ],
).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
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")
api, _service = _deployment_api(artifact_store) api, _service = _deployment_api(artifact_store)
artifact_store.save_deployment( artifact_store.save_deployment(
@@ -132,7 +132,7 @@ def test_list_deployments_returns_compact_summaries(tmp_path: Path) -> None:
) )
) )
result = asyncio.run(api.list_deployments()) result = await api.list_deployments()
assert len(result["deployments"]) == 1 assert len(result["deployments"]) == 1
assert result["deployments"][0]["id"] == "echo.personal" assert result["deployments"][0]["id"] == "echo.personal"
@@ -140,18 +140,20 @@ def test_list_deployments_returns_compact_summaries(tmp_path: Path) -> None:
assert "bindings" not in result["deployments"][0] assert "bindings" not in result["deployments"][0]
def test_list_deployments_returns_empty_without_artifact_store(tmp_path: Path) -> None: @pytest.mark.asyncio
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)
api = WorkflowDeploymentApi(context) api = WorkflowDeploymentApi(context)
result = asyncio.run(api.list_deployments()) result = await api.list_deployments()
assert result["deployments"] == [] assert result["deployments"] == []
def test_delete_deployment_removes_one(tmp_path: Path) -> None: @pytest.mark.asyncio
async def test_delete_deployment_removes_one(tmp_path: Path) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "deploy_delete") artifact_store = FileWorkflowArtifactStore(tmp_path / "deploy_delete")
api, _service = _deployment_api(artifact_store) api, _service = _deployment_api(artifact_store)
artifact_store.save_deployment( artifact_store.save_deployment(
@@ -163,14 +165,15 @@ def test_delete_deployment_removes_one(tmp_path: Path) -> None:
) )
) )
result = asyncio.run(api.delete_deployment(deployment_id="echo.personal")) result = await api.delete_deployment(deployment_id="echo.personal")
assert result["deployment_id"] == "echo.personal" assert result["deployment_id"] == "echo.personal"
assert result["deleted"] is True assert result["deleted"] is True
assert artifact_store.list_deployments() == [] assert artifact_store.list_deployments() == []
def test_validate_deployment_returns_runnable_for_valid_binding(tmp_path: Path) -> None: @pytest.mark.asyncio
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())
@@ -183,9 +186,7 @@ def test_validate_deployment_returns_runnable_for_valid_binding(tmp_path: Path)
) )
) )
result = asyncio.run( result = await api.validate_deployment(deployment_id="echo.personal", live_check=False)
api.validate_deployment(deployment_id="echo.personal", live_check=False)
)
assert result["status"] == "runnable" assert result["status"] == "runnable"
assert result["diagnostics"] == [] assert result["diagnostics"] == []
@@ -200,7 +201,8 @@ class FailingLivenessAdapter:
raise OSError("stdio process exited") raise OSError("stdio process exited")
def test_validate_deployment_live_check_calls_live_checker(tmp_path: Path) -> None: @pytest.mark.asyncio
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())
@@ -217,15 +219,14 @@ def test_validate_deployment_live_check_calls_live_checker(tmp_path: Path) -> No
cast(BackendAdapter, FailingLivenessAdapter()), cast(BackendAdapter, FailingLivenessAdapter()),
) )
result = asyncio.run( result = await api.validate_deployment(deployment_id="echo.personal", live_check=True)
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"
def test_handler_delegation_for_validate_deployment(tmp_path: Path) -> None: @pytest.mark.asyncio
async def test_handler_delegation_for_validate_deployment(tmp_path: Path) -> None:
"""WorkflowSurfaceHandlers.validate_deployment delegates to WorkflowDeploymentApi.""" """WorkflowSurfaceHandlers.validate_deployment delegates to WorkflowDeploymentApi."""
artifact_store = FileWorkflowArtifactStore(tmp_path / "deploy_delegation") artifact_store = FileWorkflowArtifactStore(tmp_path / "deploy_delegation")
service = WfMcpService( service = WfMcpService(
@@ -246,8 +247,8 @@ def test_handler_delegation_for_validate_deployment(tmp_path: Path) -> None:
context = context_from_service(service) context = context_from_service(service)
api = WorkflowDeploymentApi(context) api = WorkflowDeploymentApi(context)
handler_result = asyncio.run(h.validate_deployment(deployment_id="echo.personal")) handler_result = await h.validate_deployment(deployment_id="echo.personal")
api_result = asyncio.run(api.validate_deployment(deployment_id="echo.personal")) api_result = await api.validate_deployment(deployment_id="echo.personal")
assert handler_result["status"] == api_result["status"] assert handler_result["status"] == api_result["status"]
assert len(handler_result["diagnostics"]) == len(api_result["diagnostics"]) assert len(handler_result["diagnostics"]) == len(api_result["diagnostics"])
+7 -6
View File
@@ -3,6 +3,8 @@ from __future__ import annotations
import asyncio import asyncio
from pathlib import Path from pathlib import Path
import pytest
from tests.wf_mcp.test_support import echo_tool from tests.wf_mcp.test_support import echo_tool
from wf_api import WorkflowApi from wf_api import WorkflowApi
from wf_api.artifacts import WorkflowArtifactApi from wf_api.artifacts import WorkflowArtifactApi
@@ -40,14 +42,13 @@ def test_workflow_api_composes_domain_services(tmp_path: Path) -> None:
assert not hasattr(api, "backend") assert not hasattr(api, "backend")
def test_workflow_api_direct_capability_call(tmp_path: Path) -> None: @pytest.mark.asyncio
async def test_workflow_api_direct_capability_call(tmp_path: Path) -> None:
api = _api(tmp_path / "wf_api_direct_composition") api = _api(tmp_path / "wf_api_direct_composition")
result = asyncio.run( result = await api.call_capability(
api.call_capability( qualified_name="demo.personal.echo_tool",
qualified_name="demo.personal.echo_tool", payload={"text": "hello"},
payload={"text": "hello"},
)
) )
assert result["kind"] == "node_spec" assert result["kind"] == "node_spec"
+112 -128
View File
@@ -4,6 +4,8 @@ import asyncio
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
import pytest
from tests.wf_mcp.test_support import echo_tool from tests.wf_mcp.test_support import echo_tool
from wf_api.drafts import WorkflowDraftApi from wf_api.drafts import WorkflowDraftApi
from wf_artifacts import FileDraftWorkspaceStore, FileWorkflowArtifactStore from wf_artifacts import FileDraftWorkspaceStore, FileWorkflowArtifactStore
@@ -70,21 +72,20 @@ def _draft_api(
return WorkflowDraftApi(context), service return WorkflowDraftApi(context), service
def test_patch_draft_applies_json_patch(tmp_path: Path) -> None: @pytest.mark.asyncio
async def test_patch_draft_applies_json_patch(tmp_path: Path) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_patch") artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_patch")
api, _service = _draft_api(artifact_store) api, _service = _draft_api(artifact_store)
result = asyncio.run( result = await api.patch_draft(
api.patch_draft( draft=_echo_draft(),
draft=_echo_draft(), patch=[
patch=[ {
{ "op": "replace",
"op": "replace", "path": "/steps/echo/input/0/target/parts/0",
"path": "/steps/echo/input/0/target/parts/0", "value": "message",
"value": "message", }
} ],
],
)
) )
assert result["status"] == "valid" assert result["status"] == "valid"
@@ -94,49 +95,45 @@ def test_patch_draft_applies_json_patch(tmp_path: Path) -> None:
} }
def test_create_draft_workspace_creates_workspace(tmp_path: Path) -> None: @pytest.mark.asyncio
async def test_create_draft_workspace_creates_workspace(tmp_path: Path) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_create_workspace") artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_create_workspace")
api, _service = _draft_api(artifact_store) api, _service = _draft_api(artifact_store)
result = asyncio.run( result = await api.create_draft_workspace(
api.create_draft_workspace( workspace_id="echo_ws",
workspace_id="echo_ws", title="Echo Workspace",
title="Echo Workspace", draft=_echo_draft(),
draft=_echo_draft(),
)
) )
assert result["workspace_id"] == "echo_ws" assert result["workspace_id"] == "echo_ws"
assert result["revision"] == 1 assert result["revision"] == 1
fetched = asyncio.run( fetched = await api.get_draft_workspace(workspace_id="echo_ws", include_draft=True)
api.get_draft_workspace(workspace_id="echo_ws", include_draft=True)
)
assert fetched["workspace_id"] == "echo_ws" assert fetched["workspace_id"] == "echo_ws"
assert fetched["title"] == "Echo Workspace" assert fetched["title"] == "Echo Workspace"
assert fetched["draft"]["steps"]["echo"]["use"] == "demo.personal.echo_tool" assert fetched["draft"]["steps"]["echo"]["use"] == "demo.personal.echo_tool"
def test_list_draft_workspaces_returns_sorted_summaries_without_drafts( @pytest.mark.asyncio
async def test_list_draft_workspaces_returns_sorted_summaries_without_drafts(
tmp_path: Path, tmp_path: Path,
) -> None: ) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_list_workspaces") artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_list_workspaces")
api, _service = _draft_api(artifact_store) api, _service = _draft_api(artifact_store)
asyncio.run( await api.create_draft_workspace(
api.create_draft_workspace( workspace_id="b_draft",
workspace_id="b_draft", title="B Draft",
title="B Draft", draft=_echo_draft(),
draft=_echo_draft(),
)
)
asyncio.run(
api.create_draft_workspace(
workspace_id="a_draft",
title="A Draft",
draft=_echo_draft(),
)
) )
result = asyncio.run(api.list_draft_workspaces()) await api.create_draft_workspace(
workspace_id="a_draft",
title="A Draft",
draft=_echo_draft(),
)
result = await api.list_draft_workspaces()
assert [workspace["workspace_id"] for workspace in result["workspaces"]] == [ assert [workspace["workspace_id"] for workspace in result["workspaces"]] == [
"a_draft", "a_draft",
@@ -146,19 +143,18 @@ def test_list_draft_workspaces_returns_sorted_summaries_without_drafts(
assert "draft" not in result["workspaces"][0] assert "draft" not in result["workspaces"][0]
def test_delete_draft_workspace_is_idempotent(tmp_path: Path) -> None: @pytest.mark.asyncio
async def test_delete_draft_workspace_is_idempotent(tmp_path: Path) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_delete_workspace") artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_delete_workspace")
api, _service = _draft_api(artifact_store) api, _service = _draft_api(artifact_store)
asyncio.run( await api.create_draft_workspace(
api.create_draft_workspace( workspace_id="echo_ws",
workspace_id="echo_ws", draft=_echo_draft(),
draft=_echo_draft(),
)
) )
deleted = asyncio.run(api.delete_draft_workspace(workspace_id="echo_ws")) deleted = await api.delete_draft_workspace(workspace_id="echo_ws")
deleted_again = asyncio.run(api.delete_draft_workspace(workspace_id="echo_ws")) deleted_again = await api.delete_draft_workspace(workspace_id="echo_ws")
listed = asyncio.run(api.list_draft_workspaces()) listed = await api.list_draft_workspaces()
assert deleted["workspace_id"] == "echo_ws" assert deleted["workspace_id"] == "echo_ws"
assert deleted["deleted"] is True assert deleted["deleted"] is True
@@ -169,75 +165,62 @@ def test_delete_draft_workspace_is_idempotent(tmp_path: Path) -> None:
assert listed["workspaces"] == [] assert listed["workspaces"] == []
def test_patch_draft_workspace_updates_revision(tmp_path: Path) -> None: @pytest.mark.asyncio
async def test_patch_draft_workspace_updates_revision(tmp_path: Path) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_patch_workspace") artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_patch_workspace")
api, _service = _draft_api(artifact_store) api, _service = _draft_api(artifact_store)
asyncio.run( await api.create_draft_workspace(
api.create_draft_workspace( workspace_id="echo_ws",
workspace_id="echo_ws", draft=_echo_draft(),
draft=_echo_draft(),
)
) )
patched = asyncio.run( patched = await api.patch_draft_workspace(
api.patch_draft_workspace( workspace_id="echo_ws",
workspace_id="echo_ws", revision=1,
revision=1, patch=[{"op": "replace", "path": "/name", "value": "echo_v2"}],
patch=[{"op": "replace", "path": "/name", "value": "echo_v2"}],
)
) )
assert patched["revision"] == 2 assert patched["revision"] == 2
assert patched["status"] == "valid" assert patched["status"] == "valid"
def test_draft_workspace_patch_helpers_update_revision_and_bindings( @pytest.mark.asyncio
async def test_draft_workspace_patch_helpers_update_revision_and_bindings(
tmp_path: Path, tmp_path: Path,
) -> None: ) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_patch_helpers") artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_patch_helpers")
api, _service = _draft_api(artifact_store) api, _service = _draft_api(artifact_store)
asyncio.run( await api.create_draft_workspace(
api.create_draft_workspace( workspace_id="echo_ws",
workspace_id="echo_ws", draft=_echo_draft(),
draft=_echo_draft(),
)
) )
named = asyncio.run( named = await api.set_draft_name(
api.set_draft_name( workspace_id="echo_ws",
workspace_id="echo_ws", revision=1,
revision=1, name="echo_v2",
name="echo_v2",
)
) )
routed = asyncio.run( routed = await api.set_draft_route(
api.set_draft_route( workspace_id="echo_ws",
workspace_id="echo_ws", revision=2,
revision=2, step_id="echo",
step_id="echo", outcome="error",
outcome="error", target="__end__",
target="__end__",
)
) )
input_mapped = asyncio.run( input_mapped = await api.set_step_input_map(
api.set_step_input_map( workspace_id="echo_ws",
workspace_id="echo_ws", revision=3,
revision=3, step_id="echo",
step_id="echo", input_map={"input.text": "message"},
input_map={"input.text": "message"},
)
) )
output_mapped = asyncio.run(
api.set_step_output_map( output_mapped = await api.set_step_output_map(
workspace_id="echo_ws", workspace_id="echo_ws",
revision=4, revision=4,
step_id="echo", step_id="echo",
output_map={"echoed": "state.echoed"}, output_map={"echoed": "state.echoed"},
)
)
fetched = asyncio.run(
api.get_draft_workspace(workspace_id="echo_ws", include_draft=True)
) )
fetched = await api.get_draft_workspace(workspace_id="echo_ws", include_draft=True)
assert named["revision"] == 2 assert named["revision"] == 2
assert routed["revision"] == 3 assert routed["revision"] == 3
@@ -259,20 +242,19 @@ def test_draft_workspace_patch_helpers_update_revision_and_bindings(
] ]
def test_validate_draft_workspace_refreshes_status(tmp_path: Path) -> None: @pytest.mark.asyncio
async def test_validate_draft_workspace_refreshes_status(tmp_path: Path) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_validate_workspace") artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_validate_workspace")
api, service = _draft_api(artifact_store, register_echo=True) api, service = _draft_api(artifact_store, register_echo=True)
draft = _echo_draft() draft = _echo_draft()
draft["routes"]["echo"] = {"typo": "__end__"} draft["routes"]["echo"] = {"typo": "__end__"}
asyncio.run( await api.create_draft_workspace(
api.create_draft_workspace( workspace_id="echo_ws",
workspace_id="echo_ws", draft=draft,
draft=draft,
)
) )
payload = asyncio.run(api.validate_draft_workspace(workspace_id="echo_ws")) payload = await api.validate_draft_workspace(workspace_id="echo_ws")
fetched = asyncio.run(api.get_draft_workspace(workspace_id="echo_ws")) fetched = await api.get_draft_workspace(workspace_id="echo_ws")
assert payload["revision"] == 1 assert payload["revision"] == 1
assert payload["status"] == "invalid" assert payload["status"] == "invalid"
@@ -280,40 +262,42 @@ def test_validate_draft_workspace_refreshes_status(tmp_path: Path) -> None:
assert fetched["status"] == "invalid" assert fetched["status"] == "invalid"
def test_create_minimal_draft_workspace_minimal_success_path(tmp_path: Path) -> None: @pytest.mark.asyncio
async def test_create_minimal_draft_workspace_minimal_success_path(
tmp_path: Path,
) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_minimal_workspace") artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_minimal_workspace")
api, _service = _draft_api(artifact_store, register_echo=True) api, _service = _draft_api(artifact_store, register_echo=True)
result = asyncio.run( result = await api.create_minimal_draft_workspace(
api.create_minimal_draft_workspace( workspace_id="echo_minimal",
workspace_id="echo_minimal", name="echo",
name="echo", capability_name="demo.personal.echo_tool",
capability_name="demo.personal.echo_tool", input_schema={
input_schema={ "type": "object",
"type": "object", "properties": {"text": {"type": "string"}},
"properties": {"text": {"type": "string"}}, "required": ["text"],
"required": ["text"], },
}, state_schema={"fields": {"echoed": {"type": "string"}}},
state_schema={"fields": {"echoed": {"type": "string"}}}, output_schema={
output_schema={ "type": "object",
"type": "object", "properties": {"echoed": {"type": "string"}},
"properties": {"echoed": {"type": "string"}}, "required": ["echoed"],
"required": ["echoed"], },
}, input_map={"input.text": "text"},
input_map={"input.text": "text"}, output_map={"echoed": "state.echoed"},
output_map={"echoed": "state.echoed"},
)
) )
assert result["workspace_id"] == "echo_minimal" assert result["workspace_id"] == "echo_minimal"
fetched = asyncio.run( fetched = await api.get_draft_workspace(
api.get_draft_workspace(workspace_id="echo_minimal", include_draft=True) workspace_id="echo_minimal", include_draft=True
) )
assert fetched["draft"]["routes"]["call"]["ok"] == "__end__" assert fetched["draft"]["routes"]["call"]["ok"] == "__end__"
assert fetched["draft"]["steps"]["call"]["use"] == "demo.personal.echo_tool" assert fetched["draft"]["steps"]["call"]["use"] == "demo.personal.echo_tool"
def test_delegation_smoke_validate_draft_equivalence(tmp_path: Path) -> None: @pytest.mark.asyncio
async def test_delegation_smoke_validate_draft_equivalence(tmp_path: Path) -> None:
"""WorkflowSurfaceHandlers.validate_draft delegates to WorkflowDraftApi.""" """WorkflowSurfaceHandlers.validate_draft delegates to WorkflowDraftApi."""
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_delegation_smoke") artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_delegation_smoke")
mcp_root = artifact_store.root / "delegation_mcp" mcp_root = artifact_store.root / "delegation_mcp"
@@ -332,8 +316,8 @@ def test_delegation_smoke_validate_draft_equivalence(tmp_path: Path) -> None:
api = WorkflowDraftApi(context) api = WorkflowDraftApi(context)
draft = _echo_draft() draft = _echo_draft()
handler_result = asyncio.run(h.validate_draft(draft=draft)) handler_result = await h.validate_draft(draft=draft)
api_result = asyncio.run(api.validate_draft(draft=draft)) api_result = await api.validate_draft(draft=draft)
assert handler_result["status"] == api_result["status"] assert handler_result["status"] == api_result["status"]
assert handler_result["diagnostics"] == api_result["diagnostics"] assert handler_result["diagnostics"] == api_result["diagnostics"]