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.
|
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.
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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"])
|
||||||
|
|||||||
@@ -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
@@ -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"]
|
||||||
|
|||||||
Reference in New Issue
Block a user