draft workspace: the promised wf_authoring for LLMs

This commit is contained in:
lda
2026-05-19 05:13:06 +07:00 Verified
parent edbe8c4d84
commit dd577d9731
19 changed files with 2618 additions and 4 deletions
+263
View File
@@ -0,0 +1,263 @@
from __future__ import annotations
from typing import Any
from wf_artifacts import (
FileDraftWorkspaceStore,
WorkflowDraftWorkspace,
create_draft_workspace,
get_draft_workspace,
patch_draft_workspace,
summarize_draft_workspace,
)
def test_draft_workspace_stores_mutable_draft_with_revision() -> None:
workspace = WorkflowDraftWorkspace(
id="echo_draft",
revision=1,
title="Echo Draft",
draft=_draft(),
status="valid",
diagnostics=[],
created_at_epoch_ms=100,
updated_at_epoch_ms=100,
)
assert workspace.id == "echo_draft"
assert workspace.revision == 1
assert workspace.draft["steps"]["echo"]["use"] == "demo.echo"
def test_draft_workspace_summary_is_compact() -> None:
workspace = WorkflowDraftWorkspace(
id="echo_draft",
revision=3,
draft=_draft(),
status="valid",
diagnostics=[],
created_at_epoch_ms=100,
updated_at_epoch_ms=200,
)
summary = summarize_draft_workspace(workspace)
assert summary["workspace_id"] == "echo_draft"
assert summary["revision"] == 3
assert summary["status"] == "valid"
assert summary["summary"]["name"] == "echo"
assert summary["summary"]["steps"] == ["echo"]
assert "draft" not in summary
def test_file_draft_workspace_store_round_trips_workspace(tmp_path) -> None:
store = FileDraftWorkspaceStore(tmp_path)
workspace = WorkflowDraftWorkspace(
id="echo_draft",
revision=1,
draft=_draft(),
status="valid",
diagnostics=[],
created_at_epoch_ms=100,
updated_at_epoch_ms=100,
)
store.save_workspace(workspace)
loaded = store.get_workspace("echo_draft")
assert loaded == workspace
def test_file_draft_workspace_store_lists_workspaces(tmp_path) -> None:
store = FileDraftWorkspaceStore(tmp_path)
store.save_workspace(
WorkflowDraftWorkspace(
id="b",
revision=1,
draft=_draft(),
status="valid",
diagnostics=[],
created_at_epoch_ms=100,
updated_at_epoch_ms=100,
)
)
store.save_workspace(
WorkflowDraftWorkspace(
id="a",
revision=1,
draft=_draft(),
status="valid",
diagnostics=[],
created_at_epoch_ms=100,
updated_at_epoch_ms=100,
)
)
assert [workspace.id for workspace in store.list_workspaces()] == ["a", "b"]
def test_file_draft_workspace_store_rejects_path_traversal_ids(tmp_path) -> None:
store = FileDraftWorkspaceStore(tmp_path)
try:
store.get_workspace("../outside")
except ValueError as exc:
assert "path separators are not allowed" in str(exc)
else:
raise AssertionError("expected unsafe workspace id to be rejected")
def test_create_draft_workspace_validates_and_saves(tmp_path) -> None:
store = FileDraftWorkspaceStore(tmp_path)
result = create_draft_workspace(
store,
workspace_id="echo_draft",
draft=_draft(),
title="Echo Draft",
)
loaded = store.get_workspace("echo_draft")
assert result["workspace_id"] == "echo_draft"
assert result["status"] == "valid"
assert loaded.revision == 1
assert loaded.status == "valid"
def test_create_draft_workspace_rejects_duplicate_id(tmp_path) -> None:
store = FileDraftWorkspaceStore(tmp_path)
create_draft_workspace(store, workspace_id="echo_draft", draft=_draft())
result = create_draft_workspace(
store,
workspace_id="echo_draft",
draft=_draft(),
title="Replacement",
)
loaded = store.get_workspace("echo_draft")
assert result["status"] == "conflict"
assert result["diagnostics"][0]["code"] == "workspace_exists"
assert loaded.revision == 1
assert loaded.title is None
def test_patch_draft_workspace_applies_patch_and_increments_revision(tmp_path) -> None:
store = FileDraftWorkspaceStore(tmp_path)
create_draft_workspace(store, workspace_id="echo_draft", draft=_draft())
result = patch_draft_workspace(
store,
workspace_id="echo_draft",
revision=1,
patch=[
{
"op": "replace",
"path": "/name",
"value": "echo_v2",
}
],
)
loaded = store.get_workspace("echo_draft")
assert result["revision"] == 2
assert result["status"] == "valid"
assert loaded.revision == 2
assert loaded.draft["name"] == "echo_v2"
def test_patch_draft_workspace_rejects_stale_revision(tmp_path) -> None:
store = FileDraftWorkspaceStore(tmp_path)
create_draft_workspace(store, workspace_id="echo_draft", draft=_draft())
patch_draft_workspace(
store,
workspace_id="echo_draft",
revision=1,
patch=[],
)
result = patch_draft_workspace(
store,
workspace_id="echo_draft",
revision=1,
patch=[],
)
assert result["status"] == "conflict"
assert result["diagnostics"][0]["code"] == "revision_conflict"
assert store.get_workspace("echo_draft").revision == 2
def test_patch_draft_workspace_rejects_duplicate_revision_patch(tmp_path) -> None:
store = FileDraftWorkspaceStore(tmp_path)
create_draft_workspace(store, workspace_id="echo_draft", draft=_draft())
first = patch_draft_workspace(
store,
workspace_id="echo_draft",
revision=1,
patch=[{"op": "replace", "path": "/name", "value": "first"}],
)
second = patch_draft_workspace(
store,
workspace_id="echo_draft",
revision=1,
patch=[{"op": "replace", "path": "/name", "value": "second"}],
)
loaded = store.get_workspace("echo_draft")
assert first["revision"] == 2
assert second["status"] == "conflict"
assert second["diagnostics"][0]["code"] == "revision_conflict"
assert loaded.draft["name"] == "first"
def test_patch_draft_workspace_rejects_invalid_patch_without_revision_bump(
tmp_path,
) -> None:
store = FileDraftWorkspaceStore(tmp_path)
create_draft_workspace(store, workspace_id="echo_draft", draft=_draft())
result = patch_draft_workspace(
store,
workspace_id="echo_draft",
revision=1,
patch=[{"op": "remove", "path": "/missing"}],
)
assert result["status"] == "invalid"
assert result["diagnostics"][0]["code"] == "patch_invalid"
assert store.get_workspace("echo_draft").revision == 1
def test_get_draft_workspace_includes_full_draft_only_when_requested(tmp_path) -> None:
store = FileDraftWorkspaceStore(tmp_path)
create_draft_workspace(store, workspace_id="echo_draft", draft=_draft())
compact = get_draft_workspace(store, workspace_id="echo_draft")
full = get_draft_workspace(
store,
workspace_id="echo_draft",
include_draft=True,
)
assert "draft" not in compact
assert full["draft"]["steps"]["echo"]["use"] == "demo.echo"
def _draft() -> dict[str, Any]:
return {
"name": "echo",
"input_schema": {"type": "object", "properties": {}},
"state_schema": {"fields": {"echoed": {"type": "string"}}},
"output_schema": {"type": "object", "properties": {}},
"start": "echo",
"steps": {
"echo": {
"use": "demo.echo",
"in": {},
"out": {"echoed": "state.echoed"},
}
},
"routes": {"echo": {"ok": "__end__"}},
}
+16
View File
@@ -64,6 +64,11 @@ def test_server_exposes_upstream_admin_and_workflow_tools() -> None:
assert "wf.workflow.compile_draft" in names
assert "wf.workflow.create_artifact_from_draft" in names
assert "wf.workflow.patch_draft" in names
assert "wf.workflow.create_draft_workspace" in names
assert "wf.workflow.get_draft_workspace" in names
assert "wf.workflow.patch_draft_workspace" in names
assert "wf.workflow.create_minimal_draft_workspace" in names
assert "wf.workflow.create_artifact_from_workspace" in names
assert "wf.workflow.run_deployment" in names
call_capability_schema = tools_by_name[
"wf.workflow.call_capability"
@@ -72,6 +77,12 @@ def test_server_exposes_upstream_admin_and_workflow_tools() -> None:
assert "source_id" in call_capability_schema["properties"]
assert "kind" in call_capability_schema["properties"]
assert "diagnostics" in call_capability_schema["properties"]
create_workspace_schema = tools_by_name[
"wf.workflow.create_draft_workspace"
].outputSchema
assert create_workspace_schema is not None
assert "workspace_id" in create_workspace_schema["properties"]
assert "revision" in create_workspace_schema["properties"]
echo_result = await client.call_tool(
"fixture.personal.echo_tool",
@@ -165,6 +176,11 @@ def test_server_search_mode_pins_stable_control_and_workflow_tools() -> None:
assert "wf.workflow.list_artifacts" in names
assert "wf.workflow.validate_draft" in names
assert "wf.workflow.create_artifact_from_draft" in names
assert "wf.workflow.create_draft_workspace" in names
assert "wf.workflow.get_draft_workspace" in names
assert "wf.workflow.patch_draft_workspace" in names
assert "wf.workflow.create_minimal_draft_workspace" in names
assert "wf.workflow.create_artifact_from_workspace" in names
assert "wf.workflow.call_capability" in names
assert "wf.workflow.inspect_artifact" in names
assert "wf.workflow.list_deployments" in names
+9
View File
@@ -3,6 +3,7 @@ from __future__ import annotations
import asyncio
import shutil
from wf_artifacts import FileDraftWorkspaceStore
from wf_authoring import NodeSpec
from wf_core import END, NodeUse, RunStatus
from wf_mcp.broker import WfMcpService
@@ -103,6 +104,14 @@ def test_service_installs_builtin_stdlib_specs_by_default() -> None:
)
def test_service_installs_default_draft_workspace_store() -> None:
root = local_temp_root() / "service_default_draft_workspace_store"
service = WfMcpService(store=FileStore(root))
assert isinstance(service.draft_workspace_store, FileDraftWorkspaceStore)
assert service.draft_workspace_store.root == root
def test_service_registers_empty_source_for_connection_without_catalog() -> None:
service = WfMcpService(store=FileStore(local_temp_root() / "empty_source"))
+134 -1
View File
@@ -52,6 +52,12 @@ def changed_echo_tool(payload: ChangedEchoInput) -> ChangedEchoOutput:
return ChangedEchoOutput(echoed=payload.message)
@node(name="mcp_echo_tool", outcomes=("ok", "error"))
def mcp_echo_tool(payload: ChangedEchoInput) -> ChangedEchoOutput:
"""Test fixture that mirrors naive MCP wrappers with ok/error outcomes."""
return ChangedEchoOutput(echoed=payload.message)
@reducer(name="custom.default.multiply")
def multiply(current: int | None, incoming: int) -> int:
return (current or 1) * incoming
@@ -348,6 +354,133 @@ def test_workflow_surface_patches_draft_without_saving() -> None:
assert not artifact_store.list_artifacts()
def test_workflow_surface_creates_and_gets_draft_workspace() -> None:
artifact_store = FileWorkflowArtifactStore(local_temp_root() / "surface_workspace")
handlers = _handlers(artifact_store)
created = asyncio.run(
handlers.create_draft_workspace(
workspace_id="echo_draft",
title="Echo Draft",
draft=_echo_draft(),
)
)
fetched = asyncio.run(
handlers.get_draft_workspace(
workspace_id="echo_draft",
include_draft=True,
)
)
assert created["workspace_id"] == "echo_draft"
assert created["revision"] == 1
assert fetched["draft"]["steps"]["echo"]["use"] == "demo.personal.echo_tool"
def test_workflow_surface_patches_draft_workspace_by_revision() -> None:
artifact_store = FileWorkflowArtifactStore(
local_temp_root() / "surface_workspace_patch"
)
handlers = _handlers(artifact_store)
asyncio.run(
handlers.create_draft_workspace(
workspace_id="echo_draft",
draft=_echo_draft(),
)
)
patched = asyncio.run(
handlers.patch_draft_workspace(
workspace_id="echo_draft",
revision=1,
patch=[{"op": "replace", "path": "/name", "value": "echo_v2"}],
)
)
assert patched["revision"] == 2
assert patched["status"] == "valid"
def test_workflow_surface_creates_minimal_draft_workspace_with_error_route() -> None:
artifact_store = FileWorkflowArtifactStore(
local_temp_root() / "surface_minimal_workspace"
)
service = WfMcpService(
store=FileStore(local_temp_root() / "surface_minimal_workspace_mcp"),
artifact_store=artifact_store,
)
service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal")
)
service.register_specs("demo.personal", mcp_echo_tool)
handlers = WorkflowSurfaceHandlers(service)
result = asyncio.run(
handlers.create_minimal_draft_workspace(
workspace_id="echo_draft",
name="echo",
capability_name="demo.personal.mcp_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 service.draft_workspace_store is not None
workspace = service.draft_workspace_store.get_workspace("echo_draft")
assert result["workspace_id"] == "echo_draft"
assert workspace.draft["routes"]["call"]["ok"] == "__end__"
assert workspace.draft["routes"]["call"]["error"] == "tool_error"
assert workspace.draft["steps"]["tool_error"]["use"] == "wf.std.runtime_error"
assert workspace.draft["steps"]["tool_error"]["in"] == {"state.echoed": "message"}
def test_workflow_surface_creates_artifact_from_workspace() -> None:
artifact_store = FileWorkflowArtifactStore(
local_temp_root() / "surface_workspace_artifact"
)
service = WfMcpService(
store=FileStore(local_temp_root() / "surface_workspace_artifact_mcp"),
artifact_store=artifact_store,
)
service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal")
)
service.register_specs("demo.personal", echo_tool)
handlers = WorkflowSurfaceHandlers(service)
asyncio.run(
handlers.create_draft_workspace(
workspace_id="echo_draft",
draft=_echo_draft(),
)
)
result = asyncio.run(
handlers.create_artifact_from_workspace(
workspace_id="echo_draft",
artifact_id="workspace_echo",
version=1,
title="Workspace Echo",
outcomes=("completed",),
source_bindings={"demo": "demo.personal"},
)
)
artifact = artifact_store.get_artifact("workspace_echo", 1)
assert result["saved"] is True
assert artifact.id == "workspace_echo"
def test_raw_workflow_plan_uses_core_step_and_edge_models() -> None:
plan = RawWorkflowPlan.model_validate(_echo_artifact().plan)
@@ -696,7 +829,7 @@ def test_workflow_surface_calls_saved_wrapper_artifact_with_deployment_bindings(
def _handlers(artifact_store: FileWorkflowArtifactStore) -> WorkflowSurfaceHandlers:
service = WfMcpService(
store=FileStore(local_temp_root() / "surface_mcp"),
store=FileStore(artifact_store.root / "surface_mcp" / str(id(artifact_store))),
artifact_store=artifact_store,
)
return WorkflowSurfaceHandlers(service)