more half-ass async migrations + basedpyright warning
This commit is contained in:
@@ -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.
|
||||
|
||||
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)
|
||||
|
||||
## 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()`
|
||||
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.
|
||||
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?
|
||||
|
||||
## Docs mgmt
|
||||
### Docs mgmt
|
||||
|
||||
read docs/AGENTS.md
|
||||
|
||||
more later
|
||||
|
||||
# Test suite
|
||||
## Test suite
|
||||
|
||||
```bash
|
||||
uv run /* --env-file .env */ pytest -q
|
||||
uv run ruff check; uv run ruff format
|
||||
uv run basedpyright --level error # error to cut spam
|
||||
# maybe uvx ty
|
||||
uv run basedpyright # --level error # to cut spam if typeCheckingMode = "recommended", but its "basic" now
|
||||
## maybe uvx ty
|
||||
```
|
||||
|
||||
or so i think.
|
||||
|
||||
## project is getting big
|
||||
### project is getting big
|
||||
|
||||
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 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)
|
||||
|
||||
## partial impls
|
||||
### partial impls
|
||||
|
||||
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
|
||||
they see old plans.
|
||||
|
||||
# skills
|
||||
## skills
|
||||
|
||||
im looking at you superpowers
|
||||
|
||||
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.
|
||||
|
||||
@@ -58,3 +58,31 @@ docs are the usual places.
|
||||
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
|
||||
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
@@ -34,7 +34,7 @@ dev = [
|
||||
]
|
||||
|
||||
[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 = ["."]
|
||||
asyncio_mode = "auto"
|
||||
|
||||
@@ -42,7 +42,7 @@ asyncio_mode = "auto"
|
||||
package = true
|
||||
|
||||
[tool.basedpyright]
|
||||
typeCheckingMode = "basic"
|
||||
typeCheckingMode = "basic" # too many errors
|
||||
|
||||
[tool.ruff.format]
|
||||
preview = false
|
||||
|
||||
@@ -88,7 +88,7 @@ def reducer(
|
||||
def decorate_plain(
|
||||
raw: PlainReducerCallable,
|
||||
) -> AuthoredReducer:
|
||||
reducer_name = name or raw.__name__
|
||||
reducer_name = name or getattr(raw, "__name__", "<anonymous plain reducer>")
|
||||
reducer_description = description or raw.__doc__
|
||||
# if no config model is provided, we assume it's a plain reducer and just wrap it directly
|
||||
return AuthoredReducer(
|
||||
@@ -102,6 +102,7 @@ def reducer(
|
||||
)
|
||||
)
|
||||
|
||||
# fail-fast? worse treatment than @node
|
||||
if fn is not None:
|
||||
return decorate_plain(cast(PlainReducerCallable, fn))
|
||||
return decorate_plain
|
||||
@@ -111,7 +112,7 @@ def reducer(
|
||||
def decorate_config(
|
||||
raw: ConfigReducerCallable[ConfigT],
|
||||
) -> AuthoredReducer:
|
||||
reducer_name = name or raw.__name__
|
||||
reducer_name = name or getattr(raw, "__name__", "<anonymous config reducer>")
|
||||
reducer_description = description or raw.__doc__
|
||||
|
||||
model_type = config_model
|
||||
|
||||
@@ -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,
|
||||
) -> None:
|
||||
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"),
|
||||
)
|
||||
|
||||
asyncio.run(run_handler())
|
||||
await run_handler()
|
||||
|
||||
operation = captured["operation"]
|
||||
config = captured["config"]
|
||||
|
||||
@@ -7,6 +7,8 @@ from dataclasses import replace
|
||||
from pathlib import Path
|
||||
from typing import Any, cast
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.wf_mcp.test_support import echo_tool
|
||||
from wf_api.deployments import WorkflowDeploymentApi
|
||||
from wf_artifacts import (
|
||||
@@ -96,13 +98,12 @@ def _deployment_api(
|
||||
context = context_from_service(service)
|
||||
return WorkflowDeploymentApi(context), service
|
||||
|
||||
|
||||
def test_save_deployment_stores_and_returns_stable_fields(tmp_path: Path) -> None:
|
||||
@pytest.mark.asyncio
|
||||
async def test_save_deployment_stores_and_returns_stable_fields(tmp_path: Path) -> None:
|
||||
artifact_store = FileWorkflowArtifactStore(tmp_path / "deploy_save")
|
||||
api, _service = _deployment_api(artifact_store)
|
||||
|
||||
result = asyncio.run(
|
||||
api.save_deployment(
|
||||
result = await api.save_deployment(
|
||||
WorkflowDeployment(
|
||||
id="echo.personal",
|
||||
artifact_id="echo",
|
||||
@@ -112,15 +113,14 @@ def test_save_deployment_stores_and_returns_stable_fields(tmp_path: Path) -> Non
|
||||
],
|
||||
).model_dump(mode="json")
|
||||
)
|
||||
)
|
||||
|
||||
assert result["saved"] is True
|
||||
assert result["deployment_id"] == "echo.personal"
|
||||
assert result["artifact_id"] == "echo"
|
||||
assert result["artifact_version"] == 1
|
||||
|
||||
|
||||
def test_list_deployments_returns_compact_summaries(tmp_path: Path) -> None:
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_deployments_returns_compact_summaries(tmp_path: Path) -> None:
|
||||
artifact_store = FileWorkflowArtifactStore(tmp_path / "deploy_list")
|
||||
api, _service = _deployment_api(artifact_store)
|
||||
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 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]
|
||||
|
||||
|
||||
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")
|
||||
_api, service = _deployment_api(artifact_store)
|
||||
context = replace(context_from_service(service), artifact_store=None)
|
||||
api = WorkflowDeploymentApi(context)
|
||||
|
||||
result = asyncio.run(api.list_deployments())
|
||||
result = await api.list_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")
|
||||
api, _service = _deployment_api(artifact_store)
|
||||
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["deleted"] is True
|
||||
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")
|
||||
api, service = _deployment_api(artifact_store, register_echo=True)
|
||||
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(
|
||||
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["diagnostics"] == []
|
||||
@@ -200,7 +201,8 @@ class FailingLivenessAdapter:
|
||||
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")
|
||||
api, service = _deployment_api(artifact_store, register_echo=True)
|
||||
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()),
|
||||
)
|
||||
|
||||
result = asyncio.run(
|
||||
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["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."""
|
||||
artifact_store = FileWorkflowArtifactStore(tmp_path / "deploy_delegation")
|
||||
service = WfMcpService(
|
||||
@@ -246,8 +247,8 @@ def test_handler_delegation_for_validate_deployment(tmp_path: Path) -> None:
|
||||
context = context_from_service(service)
|
||||
api = WorkflowDeploymentApi(context)
|
||||
|
||||
handler_result = asyncio.run(h.validate_deployment(deployment_id="echo.personal"))
|
||||
api_result = asyncio.run(api.validate_deployment(deployment_id="echo.personal"))
|
||||
handler_result = await h.validate_deployment(deployment_id="echo.personal")
|
||||
api_result = await api.validate_deployment(deployment_id="echo.personal")
|
||||
|
||||
assert handler_result["status"] == api_result["status"]
|
||||
assert len(handler_result["diagnostics"]) == len(api_result["diagnostics"])
|
||||
|
||||
@@ -3,6 +3,8 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.wf_mcp.test_support import echo_tool
|
||||
from wf_api import WorkflowApi
|
||||
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")
|
||||
|
||||
|
||||
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")
|
||||
|
||||
result = asyncio.run(
|
||||
api.call_capability(
|
||||
qualified_name="demo.personal.echo_tool",
|
||||
payload={"text": "hello"},
|
||||
)
|
||||
result = await api.call_capability(
|
||||
qualified_name="demo.personal.echo_tool",
|
||||
payload={"text": "hello"},
|
||||
)
|
||||
|
||||
assert result["kind"] == "node_spec"
|
||||
|
||||
+112
-128
@@ -4,6 +4,8 @@ import asyncio
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.wf_mcp.test_support import echo_tool
|
||||
from wf_api.drafts import WorkflowDraftApi
|
||||
from wf_artifacts import FileDraftWorkspaceStore, FileWorkflowArtifactStore
|
||||
@@ -70,21 +72,20 @@ def _draft_api(
|
||||
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")
|
||||
api, _service = _draft_api(artifact_store)
|
||||
|
||||
result = asyncio.run(
|
||||
api.patch_draft(
|
||||
draft=_echo_draft(),
|
||||
patch=[
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "/steps/echo/input/0/target/parts/0",
|
||||
"value": "message",
|
||||
}
|
||||
],
|
||||
)
|
||||
result = await api.patch_draft(
|
||||
draft=_echo_draft(),
|
||||
patch=[
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "/steps/echo/input/0/target/parts/0",
|
||||
"value": "message",
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
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")
|
||||
api, _service = _draft_api(artifact_store)
|
||||
|
||||
result = asyncio.run(
|
||||
api.create_draft_workspace(
|
||||
workspace_id="echo_ws",
|
||||
title="Echo Workspace",
|
||||
draft=_echo_draft(),
|
||||
)
|
||||
result = await api.create_draft_workspace(
|
||||
workspace_id="echo_ws",
|
||||
title="Echo Workspace",
|
||||
draft=_echo_draft(),
|
||||
)
|
||||
|
||||
assert result["workspace_id"] == "echo_ws"
|
||||
assert result["revision"] == 1
|
||||
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 fetched["workspace_id"] == "echo_ws"
|
||||
assert fetched["title"] == "Echo Workspace"
|
||||
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,
|
||||
) -> None:
|
||||
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_list_workspaces")
|
||||
api, _service = _draft_api(artifact_store)
|
||||
asyncio.run(
|
||||
api.create_draft_workspace(
|
||||
workspace_id="b_draft",
|
||||
title="B Draft",
|
||||
draft=_echo_draft(),
|
||||
)
|
||||
)
|
||||
asyncio.run(
|
||||
api.create_draft_workspace(
|
||||
workspace_id="a_draft",
|
||||
title="A Draft",
|
||||
draft=_echo_draft(),
|
||||
)
|
||||
await api.create_draft_workspace(
|
||||
workspace_id="b_draft",
|
||||
title="B 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"]] == [
|
||||
"a_draft",
|
||||
@@ -146,19 +143,18 @@ def test_list_draft_workspaces_returns_sorted_summaries_without_drafts(
|
||||
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")
|
||||
api, _service = _draft_api(artifact_store)
|
||||
asyncio.run(
|
||||
api.create_draft_workspace(
|
||||
workspace_id="echo_ws",
|
||||
draft=_echo_draft(),
|
||||
)
|
||||
await api.create_draft_workspace(
|
||||
workspace_id="echo_ws",
|
||||
draft=_echo_draft(),
|
||||
)
|
||||
|
||||
deleted = asyncio.run(api.delete_draft_workspace(workspace_id="echo_ws"))
|
||||
deleted_again = asyncio.run(api.delete_draft_workspace(workspace_id="echo_ws"))
|
||||
listed = asyncio.run(api.list_draft_workspaces())
|
||||
deleted = await api.delete_draft_workspace(workspace_id="echo_ws")
|
||||
deleted_again = await api.delete_draft_workspace(workspace_id="echo_ws")
|
||||
listed = await api.list_draft_workspaces()
|
||||
|
||||
assert deleted["workspace_id"] == "echo_ws"
|
||||
assert deleted["deleted"] is True
|
||||
@@ -169,75 +165,62 @@ def test_delete_draft_workspace_is_idempotent(tmp_path: Path) -> None:
|
||||
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")
|
||||
api, _service = _draft_api(artifact_store)
|
||||
asyncio.run(
|
||||
api.create_draft_workspace(
|
||||
workspace_id="echo_ws",
|
||||
draft=_echo_draft(),
|
||||
)
|
||||
await api.create_draft_workspace(
|
||||
workspace_id="echo_ws",
|
||||
draft=_echo_draft(),
|
||||
)
|
||||
|
||||
patched = asyncio.run(
|
||||
api.patch_draft_workspace(
|
||||
workspace_id="echo_ws",
|
||||
revision=1,
|
||||
patch=[{"op": "replace", "path": "/name", "value": "echo_v2"}],
|
||||
)
|
||||
patched = await api.patch_draft_workspace(
|
||||
workspace_id="echo_ws",
|
||||
revision=1,
|
||||
patch=[{"op": "replace", "path": "/name", "value": "echo_v2"}],
|
||||
)
|
||||
|
||||
assert patched["revision"] == 2
|
||||
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,
|
||||
) -> None:
|
||||
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_patch_helpers")
|
||||
api, _service = _draft_api(artifact_store)
|
||||
asyncio.run(
|
||||
api.create_draft_workspace(
|
||||
workspace_id="echo_ws",
|
||||
draft=_echo_draft(),
|
||||
)
|
||||
await api.create_draft_workspace(
|
||||
workspace_id="echo_ws",
|
||||
draft=_echo_draft(),
|
||||
)
|
||||
|
||||
named = asyncio.run(
|
||||
api.set_draft_name(
|
||||
workspace_id="echo_ws",
|
||||
revision=1,
|
||||
name="echo_v2",
|
||||
)
|
||||
named = await api.set_draft_name(
|
||||
workspace_id="echo_ws",
|
||||
revision=1,
|
||||
name="echo_v2",
|
||||
)
|
||||
routed = asyncio.run(
|
||||
api.set_draft_route(
|
||||
workspace_id="echo_ws",
|
||||
revision=2,
|
||||
step_id="echo",
|
||||
outcome="error",
|
||||
target="__end__",
|
||||
)
|
||||
routed = await api.set_draft_route(
|
||||
workspace_id="echo_ws",
|
||||
revision=2,
|
||||
step_id="echo",
|
||||
outcome="error",
|
||||
target="__end__",
|
||||
)
|
||||
input_mapped = asyncio.run(
|
||||
api.set_step_input_map(
|
||||
workspace_id="echo_ws",
|
||||
revision=3,
|
||||
step_id="echo",
|
||||
input_map={"input.text": "message"},
|
||||
)
|
||||
input_mapped = await api.set_step_input_map(
|
||||
workspace_id="echo_ws",
|
||||
revision=3,
|
||||
step_id="echo",
|
||||
input_map={"input.text": "message"},
|
||||
)
|
||||
output_mapped = asyncio.run(
|
||||
api.set_step_output_map(
|
||||
workspace_id="echo_ws",
|
||||
revision=4,
|
||||
step_id="echo",
|
||||
output_map={"echoed": "state.echoed"},
|
||||
)
|
||||
)
|
||||
fetched = asyncio.run(
|
||||
api.get_draft_workspace(workspace_id="echo_ws", include_draft=True)
|
||||
|
||||
output_mapped = await api.set_step_output_map(
|
||||
workspace_id="echo_ws",
|
||||
revision=4,
|
||||
step_id="echo",
|
||||
output_map={"echoed": "state.echoed"},
|
||||
)
|
||||
fetched = await api.get_draft_workspace(workspace_id="echo_ws", include_draft=True)
|
||||
|
||||
assert named["revision"] == 2
|
||||
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")
|
||||
api, service = _draft_api(artifact_store, register_echo=True)
|
||||
draft = _echo_draft()
|
||||
draft["routes"]["echo"] = {"typo": "__end__"}
|
||||
asyncio.run(
|
||||
api.create_draft_workspace(
|
||||
workspace_id="echo_ws",
|
||||
draft=draft,
|
||||
)
|
||||
await api.create_draft_workspace(
|
||||
workspace_id="echo_ws",
|
||||
draft=draft,
|
||||
)
|
||||
|
||||
payload = asyncio.run(api.validate_draft_workspace(workspace_id="echo_ws"))
|
||||
fetched = asyncio.run(api.get_draft_workspace(workspace_id="echo_ws"))
|
||||
payload = await api.validate_draft_workspace(workspace_id="echo_ws")
|
||||
fetched = await api.get_draft_workspace(workspace_id="echo_ws")
|
||||
|
||||
assert payload["revision"] == 1
|
||||
assert payload["status"] == "invalid"
|
||||
@@ -280,40 +262,42 @@ def test_validate_draft_workspace_refreshes_status(tmp_path: Path) -> None:
|
||||
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")
|
||||
api, _service = _draft_api(artifact_store, register_echo=True)
|
||||
|
||||
result = asyncio.run(
|
||||
api.create_minimal_draft_workspace(
|
||||
workspace_id="echo_minimal",
|
||||
name="echo",
|
||||
capability_name="demo.personal.echo_tool",
|
||||
input_schema={
|
||||
"type": "object",
|
||||
"properties": {"text": {"type": "string"}},
|
||||
"required": ["text"],
|
||||
},
|
||||
state_schema={"fields": {"echoed": {"type": "string"}}},
|
||||
output_schema={
|
||||
"type": "object",
|
||||
"properties": {"echoed": {"type": "string"}},
|
||||
"required": ["echoed"],
|
||||
},
|
||||
input_map={"input.text": "text"},
|
||||
output_map={"echoed": "state.echoed"},
|
||||
)
|
||||
result = await api.create_minimal_draft_workspace(
|
||||
workspace_id="echo_minimal",
|
||||
name="echo",
|
||||
capability_name="demo.personal.echo_tool",
|
||||
input_schema={
|
||||
"type": "object",
|
||||
"properties": {"text": {"type": "string"}},
|
||||
"required": ["text"],
|
||||
},
|
||||
state_schema={"fields": {"echoed": {"type": "string"}}},
|
||||
output_schema={
|
||||
"type": "object",
|
||||
"properties": {"echoed": {"type": "string"}},
|
||||
"required": ["echoed"],
|
||||
},
|
||||
input_map={"input.text": "text"},
|
||||
output_map={"echoed": "state.echoed"},
|
||||
)
|
||||
|
||||
assert result["workspace_id"] == "echo_minimal"
|
||||
fetched = asyncio.run(
|
||||
api.get_draft_workspace(workspace_id="echo_minimal", include_draft=True)
|
||||
fetched = await api.get_draft_workspace(
|
||||
workspace_id="echo_minimal", include_draft=True
|
||||
)
|
||||
assert fetched["draft"]["routes"]["call"]["ok"] == "__end__"
|
||||
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."""
|
||||
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_delegation_smoke")
|
||||
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)
|
||||
draft = _echo_draft()
|
||||
|
||||
handler_result = asyncio.run(h.validate_draft(draft=draft))
|
||||
api_result = asyncio.run(api.validate_draft(draft=draft))
|
||||
handler_result = await h.validate_draft(draft=draft)
|
||||
api_result = await api.validate_draft(draft=draft)
|
||||
|
||||
assert handler_result["status"] == api_result["status"]
|
||||
assert handler_result["diagnostics"] == api_result["diagnostics"]
|
||||
|
||||
Reference in New Issue
Block a user