feat: add focused draft cli edits
This commit is contained in:
@@ -54,6 +54,9 @@ clear operator feedback before adding more architecture.
|
|||||||
- Completed: the opencode browser-click challenge harness is local-first via
|
- Completed: the opencode browser-click challenge harness is local-first via
|
||||||
`wf --config examples/browser_click_workflow/wf.config.json --local`, with
|
`wf --config examples/browser_click_workflow/wf.config.json --local`, with
|
||||||
optional `--start-server` / `--server-url` modes for JSON-RPC-path trials.
|
optional `--start-server` / `--server-url` modes for JSON-RPC-path trials.
|
||||||
|
- Completed: focused draft edit helpers are exposed through RPC/CLI, and
|
||||||
|
`wf deploy create` is accepted as an alias for `wf deploy save`. Docs now
|
||||||
|
distinguish draft shape from raw plan shape for agent authoring.
|
||||||
- Keep status read-only; do not mutate registry, auth, config, or stores.
|
- Keep status read-only; do not mutate registry, auth, config, or stores.
|
||||||
|
|
||||||
## Priority 2: Durable Run/Resume Hardening
|
## Priority 2: Durable Run/Resume Hardening
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+23
-1
@@ -286,6 +286,22 @@ wf draft patch concat_ws \
|
|||||||
--input-file draft-patch.json
|
--input-file draft-patch.json
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Focused draft edit commands cover common graph edits without writing RFC 6902
|
||||||
|
patches directly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wf draft set-name concat_ws --revision 1 --name concat_ws_v2
|
||||||
|
wf draft set-route concat_ws --revision 2 --step call --outcome ok --to __end__
|
||||||
|
wf draft set-input concat_ws --revision 3 --step call --map input.items=items --map input.separator=separator
|
||||||
|
wf draft set-output concat_ws --revision 4 --step call --map value=state.value
|
||||||
|
```
|
||||||
|
|
||||||
|
`set-input` maps graph source paths to node-local input fields:
|
||||||
|
`input.text=text` means `input.text -> local.text`.
|
||||||
|
|
||||||
|
`set-output` maps node-local output fields to workflow state paths:
|
||||||
|
`text=state.text` means `local.text -> state.text`.
|
||||||
|
|
||||||
Validate:
|
Validate:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -346,6 +362,10 @@ wf artifact create-from-plan workflow.plan.json \
|
|||||||
--binding local.ops=local.ops
|
--binding local.ops=local.ops
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`artifact create-from-plan` expects the raw workflow plan shape (`nodes`,
|
||||||
|
`edges`, `node`). It does not accept draft workspace shape (`steps`, `routes`,
|
||||||
|
`use`).
|
||||||
|
|
||||||
Prefer draft workspaces for iterative authoring. Use `create-from-plan` when a
|
Prefer draft workspaces for iterative authoring. Use `create-from-plan` when a
|
||||||
compiler, fixture, or advanced client already has a complete raw workflow plan.
|
compiler, fixture, or advanced client already has a complete raw workflow plan.
|
||||||
|
|
||||||
@@ -368,6 +388,9 @@ wf deploy save concat_ws.default \
|
|||||||
--version 1
|
--version 1
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`wf deploy create` is accepted as an alias for `wf deploy save`; docs use
|
||||||
|
`save` as the canonical verb because deployments are mutable records.
|
||||||
|
|
||||||
Save a deployment from JSON:
|
Save a deployment from JSON:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -527,5 +550,4 @@ wf explain --input-file validation-output.json
|
|||||||
- The CLI reuses `wf_mcp` service/config/store wiring in v1.
|
- The CLI reuses `wf_mcp` service/config/store wiring in v1.
|
||||||
- Config loading registers stores and connections, but not arbitrary in-memory
|
- Config loading registers stores and connections, but not arbitrary in-memory
|
||||||
test `NodeSpec` functions.
|
test `NodeSpec` functions.
|
||||||
- Targeted draft editing helpers such as `wf draft step add` are not in v1.
|
|
||||||
- `wf` does not replace MCP resources/prompts or interactive MCP clients.
|
- `wf` does not replace MCP resources/prompts or interactive MCP clients.
|
||||||
|
|||||||
@@ -718,6 +718,26 @@ workflow can wrap a raw MCP call and map result content to outcomes such as
|
|||||||
`found`, `not_found`, `unauthorized`, or `rate_limited` when those meanings are
|
`found`, `not_found`, `unauthorized`, or `rate_limited` when those meanings are
|
||||||
known for that tool.
|
known for that tool.
|
||||||
|
|
||||||
|
## Raw Plan Import
|
||||||
|
|
||||||
|
`wf artifact create-from-plan` imports a complete raw workflow plan. This is an
|
||||||
|
advanced/compiler path, not the interactive draft authoring path.
|
||||||
|
|
||||||
|
Raw plans use execution-model fields such as:
|
||||||
|
|
||||||
|
- `nodes`
|
||||||
|
- `edges`
|
||||||
|
- node field `node`
|
||||||
|
|
||||||
|
Draft workspaces use authoring fields such as:
|
||||||
|
|
||||||
|
- `steps`
|
||||||
|
- `routes`
|
||||||
|
- step field `use`
|
||||||
|
|
||||||
|
Do not pass draft JSON to `artifact create-from-plan`; save drafts with
|
||||||
|
`wf draft save`.
|
||||||
|
|
||||||
## Open Questions
|
## Open Questions
|
||||||
|
|
||||||
- How should child workflow interrupts compose with parent workflow execution
|
- How should child workflow interrupts compose with parent workflow execution
|
||||||
|
|||||||
@@ -667,6 +667,22 @@ the authoring envelope while raw JSON Schema fields remain plain JSON objects.
|
|||||||
path. It validates and compiles the same draft workspace, but fixes the saved
|
path. It validates and compiles the same draft workspace, but fixes the saved
|
||||||
artifact kind to `wrapper` so clients do not need to pass `kind` manually.
|
artifact kind to `wrapper` so clients do not need to pass `kind` manually.
|
||||||
|
|
||||||
|
## Focused Edit Commands
|
||||||
|
|
||||||
|
For routine edits, prefer focused commands over hand-written JSON Patch:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wf draft set-name <workspace_id> --revision <n> --name <name>
|
||||||
|
wf draft set-route <workspace_id> --revision <n> --step <step_id> --outcome ok --to <target_step_or___end__>
|
||||||
|
wf draft set-input <workspace_id> --revision <n> --step <step_id> --map input.text=text
|
||||||
|
wf draft set-output <workspace_id> --revision <n> --step <step_id> --map text=state.text
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `draft patch` when these focused commands do not cover the structural edit.
|
||||||
|
|
||||||
|
Drafts are not raw workflow plans. Drafts use `steps`, `routes`, and step field
|
||||||
|
`use`. Raw plans use `nodes`, `edges`, and node field `node`.
|
||||||
|
|
||||||
## Patching Drafts
|
## Patching Drafts
|
||||||
|
|
||||||
`patch_draft` accepts JSON Patch operations.
|
`patch_draft` accepts JSON Patch operations.
|
||||||
|
|||||||
@@ -32,11 +32,18 @@ workflow construction inside a Python script.
|
|||||||
|
|
||||||
Two product-facing authoring paths are acceptable:
|
Two product-facing authoring paths are acceptable:
|
||||||
|
|
||||||
- create a draft from one capability, patch it with RFC 6902 JSON Patch, then
|
- draft path: create a draft from one capability, use focused draft edit
|
||||||
validate/save/deploy/run it;
|
commands or your own RFC 6902 JSON Patch, then validate/save/deploy/run it;
|
||||||
- or, if you already have a complete raw workflow plan file, use
|
- raw-plan path: write your own complete raw workflow plan file and use
|
||||||
`wf artifact create-from-plan` before deploy/run.
|
`wf artifact create-from-plan` before deploy/run.
|
||||||
|
|
||||||
|
Do not mix the formats. Drafts use `steps`, `routes`, and step field `use`.
|
||||||
|
Raw plans use `nodes`, `edges`, and node field `node`. Do not pass draft JSON to
|
||||||
|
`wf artifact create-from-plan`.
|
||||||
|
|
||||||
|
The deployment command is `wf deploy save`; `wf deploy create` is accepted as an
|
||||||
|
alias.
|
||||||
|
|
||||||
Do not use a pre-existing generated patch or raw-plan answer file. If you find
|
Do not use a pre-existing generated patch or raw-plan answer file. If you find
|
||||||
one, ignore it and author your own workflow definition.
|
one, ignore it and author your own workflow definition.
|
||||||
|
|
||||||
|
|||||||
@@ -33,11 +33,16 @@ wf cap call <capability> --input '{"field":"value"}'
|
|||||||
wf draft create-from-capability <workspace_id> <capability>
|
wf draft create-from-capability <workspace_id> <capability>
|
||||||
wf draft inspect <workspace_id> --include-draft
|
wf draft inspect <workspace_id> --include-draft
|
||||||
wf draft patch <workspace_id> --revision <n> --input-file patch.json
|
wf draft patch <workspace_id> --revision <n> --input-file patch.json
|
||||||
|
wf draft set-name <workspace_id> --revision <n> --name <name>
|
||||||
|
wf draft set-route <workspace_id> --revision <n> --step <step_id> --outcome <outcome> --to <target>
|
||||||
|
wf draft set-input <workspace_id> --revision <n> --step <step_id> --map input.text=text
|
||||||
|
wf draft set-output <workspace_id> --revision <n> --step <step_id> --map text=state.text
|
||||||
wf draft validate <workspace_id>
|
wf draft validate <workspace_id>
|
||||||
wf draft save <workspace_id> --artifact <artifact_id> --version <n> --title <title>
|
wf draft save <workspace_id> --artifact <artifact_id> --version <n> --title <title>
|
||||||
|
|
||||||
wf artifact create-from-plan workflow.plan.json --artifact <artifact_id> --version <n> --title <title>
|
wf artifact create-from-plan workflow.plan.json --artifact <artifact_id> --version <n> --title <title>
|
||||||
wf deploy save <deployment_id> --artifact <artifact_id> --version <n> --binding <logical>=<concrete>
|
wf deploy save <deployment_id> --artifact <artifact_id> --version <n> --binding <logical>=<concrete>
|
||||||
|
wf deploy create <deployment_id> --artifact <artifact_id> --version <n>
|
||||||
wf deploy validate <deployment_id>
|
wf deploy validate <deployment_id>
|
||||||
wf run start <deployment_id> --input-file input.json
|
wf run start <deployment_id> --input-file input.json
|
||||||
wf run trace <run_id> --from 0 --limit 25
|
wf run trace <run_id> --from 0 --limit 25
|
||||||
@@ -54,3 +59,5 @@ wf run trace <run_id> --from 0 --limit 25
|
|||||||
- Do not treat wrapper hints as semantic guarantees.
|
- Do not treat wrapper hints as semantic guarantees.
|
||||||
- If validation fails, run `wf explain <code>` or `wf explain --input-file <validation-output.json>`.
|
- If validation fails, run `wf explain <code>` or `wf explain --input-file <validation-output.json>`.
|
||||||
- Do not use planning-session specs or implementation plans as user-facing runtime guidance.
|
- Do not use planning-session specs or implementation plans as user-facing runtime guidance.
|
||||||
|
- Do not confuse draft shape with raw plan shape: drafts use `steps/routes/use`;
|
||||||
|
raw plans use `nodes/edges/node`.
|
||||||
|
|||||||
@@ -41,9 +41,12 @@ low-level escape hatch or you already have a complete compiler/generated plan.
|
|||||||
lifecycle behavior.
|
lifecycle behavior.
|
||||||
- Treat wrapper hints as scaffolding, not semantic truth.
|
- Treat wrapper hints as scaffolding, not semantic truth.
|
||||||
- Use draft workspaces for iterative authoring; avoid rewriting full drafts.
|
- Use draft workspaces for iterative authoring; avoid rewriting full drafts.
|
||||||
|
- Prefer focused draft edit commands before hand-writing JSON Patch.
|
||||||
- If a complete raw JSON/YAML plan already exists, the CLI escape hatch is
|
- If a complete raw JSON/YAML plan already exists, the CLI escape hatch is
|
||||||
`wf artifact create-from-plan`; do not write helper scripts that call
|
`wf artifact create-from-plan`; do not write helper scripts that call
|
||||||
`WorkflowApi.create_artifact_from_plan` directly.
|
`WorkflowApi.create_artifact_from_plan` directly.
|
||||||
|
- Use `artifact create-from-plan` only for complete raw plans; do not pass draft
|
||||||
|
JSON to it.
|
||||||
- Use explicit source bindings at deployment time.
|
- Use explicit source bindings at deployment time.
|
||||||
- Keep traces bounded with `trace_range`.
|
- Keep traces bounded with `trace_range`.
|
||||||
- Do not expect a newly saved workflow to become a new MCP tool mid-session.
|
- Do not expect a newly saved workflow to become a new MCP tool mid-session.
|
||||||
|
|||||||
@@ -69,6 +69,21 @@ Prefer focused helpers over JSON Patch for common edits:
|
|||||||
- `set_step_input_map`
|
- `set_step_input_map`
|
||||||
- `set_step_output_map`
|
- `set_step_output_map`
|
||||||
|
|
||||||
|
CLI equivalents:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wf draft set-name <workspace_id> --revision <n> --name <name>
|
||||||
|
wf draft set-route <workspace_id> --revision <n> --step <step_id> --outcome ok --to <target>
|
||||||
|
wf draft set-input <workspace_id> --revision <n> --step <step_id> --map input.text=text
|
||||||
|
wf draft set-output <workspace_id> --revision <n> --step <step_id> --map text=state.text
|
||||||
|
```
|
||||||
|
|
||||||
|
`set-input` direction: `input.text=text` means graph source `input.text` maps to
|
||||||
|
node-local target `local.text`.
|
||||||
|
|
||||||
|
`set-output` direction: `text=state.text` means node-local source `local.text`
|
||||||
|
maps to graph target `state.text`.
|
||||||
|
|
||||||
Use JSON Patch for structural edits the helpers do not cover.
|
Use JSON Patch for structural edits the helpers do not cover.
|
||||||
|
|
||||||
For larger patches, write a JSON Patch array to a file and pass it with
|
For larger patches, write a JSON Patch array to a file and pass it with
|
||||||
|
|||||||
@@ -18,12 +18,14 @@ validated, runnable deployment.
|
|||||||
- MCP: `wf.workflow.create_draft_workspace_from_capability`
|
- MCP: `wf.workflow.create_draft_workspace_from_capability`
|
||||||
- CLI: `wf draft create-from-capability <workspace_id> <capability>`
|
- CLI: `wf draft create-from-capability <workspace_id> <capability>`
|
||||||
5. Inspect/patch/validate the workspace until valid.
|
5. Inspect/patch/validate the workspace until valid.
|
||||||
- Prefer focused helpers when available.
|
- Use focused CLI commands (`set-name`, `set-route`, `set-input`, `set-output`)
|
||||||
- Use JSON Patch only for general edits.
|
for common edits.
|
||||||
|
- Use JSON Patch only for general structural edits.
|
||||||
6. Save an artifact.
|
6. Save an artifact.
|
||||||
- Full workflow: `create_artifact_from_workspace`
|
- Full workflow: `create_artifact_from_workspace`
|
||||||
- Reusable wrapper: `create_wrapper_from_workspace`
|
- Reusable wrapper: `create_wrapper_from_workspace`
|
||||||
7. Save and validate a deployment.
|
7. Save and validate a deployment.
|
||||||
|
- `wf deploy save` (or `wf deploy create` alias)
|
||||||
8. Run the deployment.
|
8. Run the deployment.
|
||||||
9. Inspect the run summary first; read bounded traces only when needed.
|
9. Inspect the run summary first; read bounded traces only when needed.
|
||||||
|
|
||||||
@@ -43,6 +45,10 @@ wf artifact create-from-plan workflow.plan.json \
|
|||||||
--binding <logical_source>=<concrete_source>
|
--binding <logical_source>=<concrete_source>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Do not pass draft JSON to `artifact create-from-plan`. Drafts use `steps`,
|
||||||
|
`routes`, and step field `use`. Raw plans use `nodes`, `edges`, and node field
|
||||||
|
`node`.
|
||||||
|
|
||||||
Then continue with the normal deployment and run steps:
|
Then continue with the normal deployment and run steps:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -76,6 +76,42 @@ class WorkflowDraftSurface(Protocol):
|
|||||||
patch: list[dict[str, Any]],
|
patch: list[dict[str, Any]],
|
||||||
) -> dict[str, Any]: ...
|
) -> dict[str, Any]: ...
|
||||||
|
|
||||||
|
async def set_draft_name(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
workspace_id: str,
|
||||||
|
revision: int,
|
||||||
|
name: str,
|
||||||
|
) -> dict[str, Any]: ...
|
||||||
|
|
||||||
|
async def set_draft_route(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
workspace_id: str,
|
||||||
|
revision: int,
|
||||||
|
step_id: str,
|
||||||
|
outcome: str,
|
||||||
|
target: str,
|
||||||
|
) -> dict[str, Any]: ...
|
||||||
|
|
||||||
|
async def set_step_input_map(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
workspace_id: str,
|
||||||
|
revision: int,
|
||||||
|
step_id: str,
|
||||||
|
input_map: dict[str, str],
|
||||||
|
) -> dict[str, Any]: ...
|
||||||
|
|
||||||
|
async def set_step_output_map(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
workspace_id: str,
|
||||||
|
revision: int,
|
||||||
|
step_id: str,
|
||||||
|
output_map: dict[str, str],
|
||||||
|
) -> dict[str, Any]: ...
|
||||||
|
|
||||||
async def validate_draft_workspace(
|
async def validate_draft_workspace(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
|
|||||||
@@ -98,6 +98,61 @@ def save_deployment(
|
|||||||
] = None,
|
] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Save a workflow deployment from flags or a JSON object."""
|
"""Save a workflow deployment from flags or a JSON object."""
|
||||||
|
_save_deployment_command(
|
||||||
|
ctx,
|
||||||
|
deployment_id=deployment_id,
|
||||||
|
artifact_id=artifact_id,
|
||||||
|
version=version,
|
||||||
|
binding=binding,
|
||||||
|
input_json=input_json,
|
||||||
|
input_file=input_file,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.command("create")
|
||||||
|
def create_deployment(
|
||||||
|
ctx: typer.Context,
|
||||||
|
deployment_id: Annotated[str | None, typer.Argument(help="Deployment id.")] = None,
|
||||||
|
artifact_id: Annotated[
|
||||||
|
str | None, typer.Option("--artifact", help="Artifact id.")
|
||||||
|
] = None,
|
||||||
|
version: Annotated[
|
||||||
|
int | None, typer.Option("--version", min=1, help="Artifact version.")
|
||||||
|
] = None,
|
||||||
|
binding: Annotated[
|
||||||
|
list[str] | None,
|
||||||
|
typer.Option("--binding", help="Logical=concrete source binding. Repeatable."),
|
||||||
|
] = None,
|
||||||
|
input_json: Annotated[
|
||||||
|
str | None, typer.Option("--input", help="Full deployment JSON object.")
|
||||||
|
] = None,
|
||||||
|
input_file: Annotated[
|
||||||
|
Path | None,
|
||||||
|
typer.Option("--input-file", help="Path to full deployment JSON object."),
|
||||||
|
] = None,
|
||||||
|
) -> None:
|
||||||
|
"""Alias for `deploy save`; creates or updates a deployment record."""
|
||||||
|
_save_deployment_command(
|
||||||
|
ctx,
|
||||||
|
deployment_id=deployment_id,
|
||||||
|
artifact_id=artifact_id,
|
||||||
|
version=version,
|
||||||
|
binding=binding,
|
||||||
|
input_json=input_json,
|
||||||
|
input_file=input_file,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _save_deployment_command(
|
||||||
|
ctx: typer.Context,
|
||||||
|
*,
|
||||||
|
deployment_id: str | None,
|
||||||
|
artifact_id: str | None,
|
||||||
|
version: int | None,
|
||||||
|
binding: list[str] | None,
|
||||||
|
input_json: str | None,
|
||||||
|
input_file: Path | None,
|
||||||
|
) -> None:
|
||||||
try:
|
try:
|
||||||
if input_json is not None or input_file is not None:
|
if input_json is not None or input_file is not None:
|
||||||
payload = parse_json_input(input_json=input_json, input_file=input_file)
|
payload = parse_json_input(input_json=input_json, input_file=input_file)
|
||||||
|
|||||||
@@ -10,6 +10,18 @@ from wf_cli.formats import ListOutputFormat, emit_list_payload
|
|||||||
from wf_cli.io import CliInputError, emit_json, parse_bindings, parse_json_value
|
from wf_cli.io import CliInputError, emit_json, parse_bindings, parse_json_value
|
||||||
from wf_cli.remote_errors import run_cli_operation
|
from wf_cli.remote_errors import run_cli_operation
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_map_flags(values: list[str] | None) -> dict[str, str]:
|
||||||
|
parsed: dict[str, str] = {}
|
||||||
|
for item in values or []:
|
||||||
|
source, separator, target = item.partition("=")
|
||||||
|
if separator != "=" or not source or not target:
|
||||||
|
raise typer.BadParameter("--map must use source=target")
|
||||||
|
if source in parsed:
|
||||||
|
raise typer.BadParameter(f"duplicate --map for {source!r}")
|
||||||
|
parsed[source] = target
|
||||||
|
return parsed
|
||||||
|
|
||||||
app = typer.Typer(
|
app = typer.Typer(
|
||||||
name="draft",
|
name="draft",
|
||||||
help="Create, inspect, patch, validate, and save draft workflows.",
|
help="Create, inspect, patch, validate, and save draft workflows.",
|
||||||
@@ -118,6 +130,122 @@ def patch_draft(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.command("set-name")
|
||||||
|
def set_draft_name(
|
||||||
|
ctx: typer.Context,
|
||||||
|
workspace_id: Annotated[str, typer.Argument(help="Draft workspace id.")],
|
||||||
|
revision: Annotated[
|
||||||
|
int, typer.Option("--revision", min=1, help="Expected workspace revision.")
|
||||||
|
],
|
||||||
|
name: Annotated[str, typer.Option("--name", help="New draft workflow name.")],
|
||||||
|
) -> None:
|
||||||
|
"""Set the draft workflow name without writing JSON Patch manually."""
|
||||||
|
context = load_cli_context(ctx)
|
||||||
|
emit_json(
|
||||||
|
run_cli_operation(
|
||||||
|
context,
|
||||||
|
context.handlers.set_draft_name(
|
||||||
|
workspace_id=workspace_id,
|
||||||
|
revision=revision,
|
||||||
|
name=name,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.command("set-route")
|
||||||
|
def set_draft_route(
|
||||||
|
ctx: typer.Context,
|
||||||
|
workspace_id: Annotated[str, typer.Argument(help="Draft workspace id.")],
|
||||||
|
revision: Annotated[
|
||||||
|
int, typer.Option("--revision", min=1, help="Expected workspace revision.")
|
||||||
|
],
|
||||||
|
step_id: Annotated[str, typer.Option("--step", help="Draft step id.")],
|
||||||
|
outcome: Annotated[str, typer.Option("--outcome", help="Step outcome.")],
|
||||||
|
target: Annotated[
|
||||||
|
str, typer.Option("--to", help="Target step id or __end__.")
|
||||||
|
],
|
||||||
|
) -> None:
|
||||||
|
"""Set one route: steps.<step> outcome -> target."""
|
||||||
|
context = load_cli_context(ctx)
|
||||||
|
emit_json(
|
||||||
|
run_cli_operation(
|
||||||
|
context,
|
||||||
|
context.handlers.set_draft_route(
|
||||||
|
workspace_id=workspace_id,
|
||||||
|
revision=revision,
|
||||||
|
step_id=step_id,
|
||||||
|
outcome=outcome,
|
||||||
|
target=target,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.command("set-input")
|
||||||
|
def set_step_input_map(
|
||||||
|
ctx: typer.Context,
|
||||||
|
workspace_id: Annotated[str, typer.Argument(help="Draft workspace id.")],
|
||||||
|
revision: Annotated[
|
||||||
|
int, typer.Option("--revision", min=1, help="Expected workspace revision.")
|
||||||
|
],
|
||||||
|
step_id: Annotated[str, typer.Option("--step", help="Draft step id.")],
|
||||||
|
mapping: Annotated[
|
||||||
|
list[str] | None,
|
||||||
|
typer.Option(
|
||||||
|
"--map",
|
||||||
|
help="Input binding SOURCE=LOCAL_TARGET. Repeatable. Example: input.text=text",
|
||||||
|
),
|
||||||
|
] = None,
|
||||||
|
) -> None:
|
||||||
|
"""Replace one step's input map without writing JSON Patch manually."""
|
||||||
|
input_map = _parse_map_flags(mapping)
|
||||||
|
context = load_cli_context(ctx)
|
||||||
|
emit_json(
|
||||||
|
run_cli_operation(
|
||||||
|
context,
|
||||||
|
context.handlers.set_step_input_map(
|
||||||
|
workspace_id=workspace_id,
|
||||||
|
revision=revision,
|
||||||
|
step_id=step_id,
|
||||||
|
input_map=input_map,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.command("set-output")
|
||||||
|
def set_step_output_map(
|
||||||
|
ctx: typer.Context,
|
||||||
|
workspace_id: Annotated[str, typer.Argument(help="Draft workspace id.")],
|
||||||
|
revision: Annotated[
|
||||||
|
int, typer.Option("--revision", min=1, help="Expected workspace revision.")
|
||||||
|
],
|
||||||
|
step_id: Annotated[str, typer.Option("--step", help="Draft step id.")],
|
||||||
|
mapping: Annotated[
|
||||||
|
list[str] | None,
|
||||||
|
typer.Option(
|
||||||
|
"--map",
|
||||||
|
help="Output binding LOCAL_SOURCE=STATE_TARGET. Repeatable. Example: text=state.text",
|
||||||
|
),
|
||||||
|
] = None,
|
||||||
|
) -> None:
|
||||||
|
"""Replace one step's output map without writing JSON Patch manually."""
|
||||||
|
output_map = _parse_map_flags(mapping)
|
||||||
|
context = load_cli_context(ctx)
|
||||||
|
emit_json(
|
||||||
|
run_cli_operation(
|
||||||
|
context,
|
||||||
|
context.handlers.set_step_output_map(
|
||||||
|
workspace_id=workspace_id,
|
||||||
|
revision=revision,
|
||||||
|
step_id=step_id,
|
||||||
|
output_map=output_map,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.command("validate")
|
@app.command("validate")
|
||||||
def validate_draft(
|
def validate_draft(
|
||||||
ctx: typer.Context,
|
ctx: typer.Context,
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ from .models import (
|
|||||||
ResumeRunParams,
|
ResumeRunParams,
|
||||||
SaveArtifactParams,
|
SaveArtifactParams,
|
||||||
SaveDeploymentParams,
|
SaveDeploymentParams,
|
||||||
|
SetDraftNameParams,
|
||||||
|
SetDraftRouteParams,
|
||||||
|
SetStepInputMapParams,
|
||||||
|
SetStepOutputMapParams,
|
||||||
StartRunParams,
|
StartRunParams,
|
||||||
TraceRangeParams,
|
TraceRangeParams,
|
||||||
ValidateDeploymentParams,
|
ValidateDeploymentParams,
|
||||||
@@ -62,6 +66,10 @@ __all__ = [
|
|||||||
"ResumeRunParams",
|
"ResumeRunParams",
|
||||||
"SaveArtifactParams",
|
"SaveArtifactParams",
|
||||||
"SaveDeploymentParams",
|
"SaveDeploymentParams",
|
||||||
|
"SetDraftNameParams",
|
||||||
|
"SetDraftRouteParams",
|
||||||
|
"SetStepInputMapParams",
|
||||||
|
"SetStepOutputMapParams",
|
||||||
"StartRunParams",
|
"StartRunParams",
|
||||||
"TraceRangeParams",
|
"TraceRangeParams",
|
||||||
"ValidateDeploymentParams",
|
"ValidateDeploymentParams",
|
||||||
|
|||||||
@@ -69,6 +69,74 @@ class RpcDraftClientMixin:
|
|||||||
{"workspace_id": workspace_id, "revision": revision, "patch": patch},
|
{"workspace_id": workspace_id, "revision": revision, "patch": patch},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def set_draft_name(
|
||||||
|
self: RpcCaller,
|
||||||
|
*,
|
||||||
|
workspace_id: str,
|
||||||
|
revision: int,
|
||||||
|
name: str,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
return await self._call(
|
||||||
|
"workflow.draft_workspaces.set_name",
|
||||||
|
{"workspace_id": workspace_id, "revision": revision, "name": name},
|
||||||
|
)
|
||||||
|
|
||||||
|
async def set_draft_route(
|
||||||
|
self: RpcCaller,
|
||||||
|
*,
|
||||||
|
workspace_id: str,
|
||||||
|
revision: int,
|
||||||
|
step_id: str,
|
||||||
|
outcome: str,
|
||||||
|
target: str,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
return await self._call(
|
||||||
|
"workflow.draft_workspaces.set_route",
|
||||||
|
{
|
||||||
|
"workspace_id": workspace_id,
|
||||||
|
"revision": revision,
|
||||||
|
"step_id": step_id,
|
||||||
|
"outcome": outcome,
|
||||||
|
"target": target,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
async def set_step_input_map(
|
||||||
|
self: RpcCaller,
|
||||||
|
*,
|
||||||
|
workspace_id: str,
|
||||||
|
revision: int,
|
||||||
|
step_id: str,
|
||||||
|
input_map: dict[str, str],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
return await self._call(
|
||||||
|
"workflow.draft_workspaces.set_step_input_map",
|
||||||
|
{
|
||||||
|
"workspace_id": workspace_id,
|
||||||
|
"revision": revision,
|
||||||
|
"step_id": step_id,
|
||||||
|
"input_map": input_map,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
async def set_step_output_map(
|
||||||
|
self: RpcCaller,
|
||||||
|
*,
|
||||||
|
workspace_id: str,
|
||||||
|
revision: int,
|
||||||
|
step_id: str,
|
||||||
|
output_map: dict[str, str],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
return await self._call(
|
||||||
|
"workflow.draft_workspaces.set_step_output_map",
|
||||||
|
{
|
||||||
|
"workspace_id": workspace_id,
|
||||||
|
"revision": revision,
|
||||||
|
"step_id": step_id,
|
||||||
|
"output_map": output_map,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
async def validate_draft_workspace(
|
async def validate_draft_workspace(
|
||||||
self: RpcCaller,
|
self: RpcCaller,
|
||||||
*,
|
*,
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ from ..models import (
|
|||||||
ListDraftWorkspacesParams,
|
ListDraftWorkspacesParams,
|
||||||
PatchDraftParams,
|
PatchDraftParams,
|
||||||
PatchDraftWorkspaceParams,
|
PatchDraftWorkspaceParams,
|
||||||
|
SetDraftNameParams,
|
||||||
|
SetDraftRouteParams,
|
||||||
|
SetStepInputMapParams,
|
||||||
|
SetStepOutputMapParams,
|
||||||
ValidateDraftParams,
|
ValidateDraftParams,
|
||||||
ValidateDraftWorkspaceParams,
|
ValidateDraftWorkspaceParams,
|
||||||
)
|
)
|
||||||
@@ -105,6 +109,72 @@ def register_methods(
|
|||||||
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||||
raise_workflow_rpc_error(exc)
|
raise_workflow_rpc_error(exc)
|
||||||
|
|
||||||
|
@entrypoint.method(
|
||||||
|
name="workflow.draft_workspaces.set_name", errors=[WorkflowRpcError]
|
||||||
|
)
|
||||||
|
async def workflow_draft_workspaces_set_name(
|
||||||
|
params: SetDraftNameParams = RpcParams(),
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
try:
|
||||||
|
return await server.api.set_draft_name(
|
||||||
|
workspace_id=params.workspace_id,
|
||||||
|
revision=params.revision,
|
||||||
|
name=params.name,
|
||||||
|
)
|
||||||
|
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||||
|
raise_workflow_rpc_error(exc)
|
||||||
|
|
||||||
|
@entrypoint.method(
|
||||||
|
name="workflow.draft_workspaces.set_route", errors=[WorkflowRpcError]
|
||||||
|
)
|
||||||
|
async def workflow_draft_workspaces_set_route(
|
||||||
|
params: SetDraftRouteParams = RpcParams(),
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
try:
|
||||||
|
return await server.api.set_draft_route(
|
||||||
|
workspace_id=params.workspace_id,
|
||||||
|
revision=params.revision,
|
||||||
|
step_id=params.step_id,
|
||||||
|
outcome=params.outcome,
|
||||||
|
target=params.target,
|
||||||
|
)
|
||||||
|
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||||
|
raise_workflow_rpc_error(exc)
|
||||||
|
|
||||||
|
@entrypoint.method(
|
||||||
|
name="workflow.draft_workspaces.set_step_input_map",
|
||||||
|
errors=[WorkflowRpcError],
|
||||||
|
)
|
||||||
|
async def workflow_draft_workspaces_set_step_input_map(
|
||||||
|
params: SetStepInputMapParams = RpcParams(),
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
try:
|
||||||
|
return await server.api.set_step_input_map(
|
||||||
|
workspace_id=params.workspace_id,
|
||||||
|
revision=params.revision,
|
||||||
|
step_id=params.step_id,
|
||||||
|
input_map=params.input_map,
|
||||||
|
)
|
||||||
|
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||||
|
raise_workflow_rpc_error(exc)
|
||||||
|
|
||||||
|
@entrypoint.method(
|
||||||
|
name="workflow.draft_workspaces.set_step_output_map",
|
||||||
|
errors=[WorkflowRpcError],
|
||||||
|
)
|
||||||
|
async def workflow_draft_workspaces_set_step_output_map(
|
||||||
|
params: SetStepOutputMapParams = RpcParams(),
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
try:
|
||||||
|
return await server.api.set_step_output_map(
|
||||||
|
workspace_id=params.workspace_id,
|
||||||
|
revision=params.revision,
|
||||||
|
step_id=params.step_id,
|
||||||
|
output_map=params.output_map,
|
||||||
|
)
|
||||||
|
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||||
|
raise_workflow_rpc_error(exc)
|
||||||
|
|
||||||
@entrypoint.method(
|
@entrypoint.method(
|
||||||
name="workflow.draft_workspaces.validate", errors=[WorkflowRpcError]
|
name="workflow.draft_workspaces.validate", errors=[WorkflowRpcError]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -111,6 +111,34 @@ class PatchDraftWorkspaceParams(RpcParamsModel):
|
|||||||
patch: list[dict[str, Any]]
|
patch: list[dict[str, Any]]
|
||||||
|
|
||||||
|
|
||||||
|
class SetDraftNameParams(RpcParamsModel):
|
||||||
|
workspace_id: str = Field(min_length=1)
|
||||||
|
revision: int = Field(ge=1)
|
||||||
|
name: str = Field(min_length=1)
|
||||||
|
|
||||||
|
|
||||||
|
class SetDraftRouteParams(RpcParamsModel):
|
||||||
|
workspace_id: str = Field(min_length=1)
|
||||||
|
revision: int = Field(ge=1)
|
||||||
|
step_id: str = Field(min_length=1)
|
||||||
|
outcome: str = Field(min_length=1)
|
||||||
|
target: str = Field(min_length=1)
|
||||||
|
|
||||||
|
|
||||||
|
class SetStepInputMapParams(RpcParamsModel):
|
||||||
|
workspace_id: str = Field(min_length=1)
|
||||||
|
revision: int = Field(ge=1)
|
||||||
|
step_id: str = Field(min_length=1)
|
||||||
|
input_map: dict[str, str]
|
||||||
|
|
||||||
|
|
||||||
|
class SetStepOutputMapParams(RpcParamsModel):
|
||||||
|
workspace_id: str = Field(min_length=1)
|
||||||
|
revision: int = Field(ge=1)
|
||||||
|
step_id: str = Field(min_length=1)
|
||||||
|
output_map: dict[str, str]
|
||||||
|
|
||||||
|
|
||||||
class ValidateDraftWorkspaceParams(RpcParamsModel):
|
class ValidateDraftWorkspaceParams(RpcParamsModel):
|
||||||
workspace_id: str = Field(min_length=1)
|
workspace_id: str = Field(min_length=1)
|
||||||
|
|
||||||
|
|||||||
@@ -943,3 +943,151 @@ def test_wf_local_uses_selected_config_sources(tmp_path: Path) -> None:
|
|||||||
assert {capability["name"] for capability in payload["capabilities"]} == {
|
assert {capability["name"] for capability in payload["capabilities"]} == {
|
||||||
"local.ops.echo"
|
"local.ops.echo"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_wf_draft_focused_edit_commands_use_rpc_target(monkeypatch, tmp_path) -> None:
|
||||||
|
server = build_local_static_workflow_server(tmp_path / "store")
|
||||||
|
_patch_rpc_client_to_server(monkeypatch, server)
|
||||||
|
config_path = tmp_path / "wf.json"
|
||||||
|
config_path.write_text('{"version": 1}', encoding="utf-8")
|
||||||
|
runner = CliRunner()
|
||||||
|
base_args = ["--config", str(config_path), "--url", "http://test/rpc"]
|
||||||
|
|
||||||
|
created = runner.invoke(
|
||||||
|
app,
|
||||||
|
[
|
||||||
|
*base_args,
|
||||||
|
"draft",
|
||||||
|
"create-from-capability",
|
||||||
|
"focused_ws",
|
||||||
|
"wf.std.constant",
|
||||||
|
"--name",
|
||||||
|
"focused_initial",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert created.exit_code == 0, created.output
|
||||||
|
|
||||||
|
named = runner.invoke(
|
||||||
|
app,
|
||||||
|
[
|
||||||
|
*base_args,
|
||||||
|
"draft",
|
||||||
|
"set-name",
|
||||||
|
"focused_ws",
|
||||||
|
"--revision",
|
||||||
|
"1",
|
||||||
|
"--name",
|
||||||
|
"focused_renamed",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
routed = runner.invoke(
|
||||||
|
app,
|
||||||
|
[
|
||||||
|
*base_args,
|
||||||
|
"draft",
|
||||||
|
"set-route",
|
||||||
|
"focused_ws",
|
||||||
|
"--revision",
|
||||||
|
"2",
|
||||||
|
"--step",
|
||||||
|
"call",
|
||||||
|
"--outcome",
|
||||||
|
"ok",
|
||||||
|
"--to",
|
||||||
|
"__end__",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
input_mapped = runner.invoke(
|
||||||
|
app,
|
||||||
|
[
|
||||||
|
*base_args,
|
||||||
|
"draft",
|
||||||
|
"set-input",
|
||||||
|
"focused_ws",
|
||||||
|
"--revision",
|
||||||
|
"3",
|
||||||
|
"--step",
|
||||||
|
"call",
|
||||||
|
"--map",
|
||||||
|
"input.value=value",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
output_mapped = runner.invoke(
|
||||||
|
app,
|
||||||
|
[
|
||||||
|
*base_args,
|
||||||
|
"draft",
|
||||||
|
"set-output",
|
||||||
|
"focused_ws",
|
||||||
|
"--revision",
|
||||||
|
"4",
|
||||||
|
"--step",
|
||||||
|
"call",
|
||||||
|
"--map",
|
||||||
|
"value=state.value",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
inspected = runner.invoke(
|
||||||
|
app,
|
||||||
|
[*base_args, "draft", "inspect", "focused_ws", "--include-draft"],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert named.exit_code == 0, named.output
|
||||||
|
assert routed.exit_code == 0, routed.output
|
||||||
|
assert input_mapped.exit_code == 0, input_mapped.output
|
||||||
|
assert output_mapped.exit_code == 0, output_mapped.output
|
||||||
|
assert inspected.exit_code == 0, inspected.output
|
||||||
|
payload = json.loads(inspected.output)
|
||||||
|
draft = payload["draft"]
|
||||||
|
assert draft["name"] == "focused_renamed"
|
||||||
|
assert draft["routes"]["call"]["ok"] == "__end__"
|
||||||
|
assert draft["steps"]["call"]["input"] == [
|
||||||
|
{
|
||||||
|
"target": {"root": "local", "parts": ["value"]},
|
||||||
|
"path": {"root": "input", "parts": ["value"]},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
assert draft["steps"]["call"]["output"] == [
|
||||||
|
{
|
||||||
|
"source": {"root": "local", "parts": ["value"]},
|
||||||
|
"target": {"root": "state", "parts": ["value"]},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_wf_deploy_create_alias_saves_deployment(monkeypatch, tmp_path) -> None:
|
||||||
|
server = build_local_static_workflow_server(tmp_path / "store")
|
||||||
|
asyncio.run(
|
||||||
|
server.api.create_artifact_from_plan(
|
||||||
|
artifact_id="alias_artifact",
|
||||||
|
version=1,
|
||||||
|
title="Alias Artifact",
|
||||||
|
plan=_constant_plan(),
|
||||||
|
outcomes=("ok",),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
_patch_rpc_client_to_server(monkeypatch, server)
|
||||||
|
config_path = tmp_path / "wf.json"
|
||||||
|
config_path.write_text('{"version": 1}', encoding="utf-8")
|
||||||
|
runner = CliRunner()
|
||||||
|
|
||||||
|
created = runner.invoke(
|
||||||
|
app,
|
||||||
|
[
|
||||||
|
"--config",
|
||||||
|
str(config_path),
|
||||||
|
"--url",
|
||||||
|
"http://test/rpc",
|
||||||
|
"deploy",
|
||||||
|
"create",
|
||||||
|
"alias_artifact.default",
|
||||||
|
"--artifact",
|
||||||
|
"alias_artifact",
|
||||||
|
"--version",
|
||||||
|
"1",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert created.exit_code == 0, created.output
|
||||||
|
payload = json.loads(created.output)
|
||||||
|
assert payload["deployment_id"] == "alias_artifact.default"
|
||||||
|
|||||||
@@ -644,6 +644,89 @@ async def test_rpc_create_artifact_from_plan(tmp_path) -> None:
|
|||||||
assert inspected["result"]["plan"]["name"] == "rpc_constant"
|
assert inspected["result"]["plan"]["name"] == "rpc_constant"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_rpc_draft_workspace_focused_edit_methods(tmp_path) -> None:
|
||||||
|
server = build_local_static_workflow_server(tmp_path / "store")
|
||||||
|
app = create_rpc_app(server)
|
||||||
|
transport = httpx.ASGITransport(app=app)
|
||||||
|
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
|
||||||
|
created = await _rpc(
|
||||||
|
client,
|
||||||
|
"workflow.draft_workspaces.create_from_capability",
|
||||||
|
{
|
||||||
|
"workspace_id": "focused_ws",
|
||||||
|
"capability_name": "wf.std.constant",
|
||||||
|
"name": "focused_initial",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert created["result"]["workspace_id"] == "focused_ws"
|
||||||
|
|
||||||
|
named = await _rpc(
|
||||||
|
client,
|
||||||
|
"workflow.draft_workspaces.set_name",
|
||||||
|
{
|
||||||
|
"workspace_id": "focused_ws",
|
||||||
|
"revision": 1,
|
||||||
|
"name": "focused_renamed",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
routed = await _rpc(
|
||||||
|
client,
|
||||||
|
"workflow.draft_workspaces.set_route",
|
||||||
|
{
|
||||||
|
"workspace_id": "focused_ws",
|
||||||
|
"revision": 2,
|
||||||
|
"step_id": "call",
|
||||||
|
"outcome": "ok",
|
||||||
|
"target": "__end__",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
input_mapped = await _rpc(
|
||||||
|
client,
|
||||||
|
"workflow.draft_workspaces.set_step_input_map",
|
||||||
|
{
|
||||||
|
"workspace_id": "focused_ws",
|
||||||
|
"revision": 3,
|
||||||
|
"step_id": "call",
|
||||||
|
"input_map": {"input.value": "value"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
output_mapped = await _rpc(
|
||||||
|
client,
|
||||||
|
"workflow.draft_workspaces.set_step_output_map",
|
||||||
|
{
|
||||||
|
"workspace_id": "focused_ws",
|
||||||
|
"revision": 4,
|
||||||
|
"step_id": "call",
|
||||||
|
"output_map": {"value": "state.value"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
fetched = await _rpc(
|
||||||
|
client,
|
||||||
|
"workflow.draft_workspaces.get",
|
||||||
|
{"workspace_id": "focused_ws", "include_draft": True},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert named["result"]["revision"] == 2
|
||||||
|
assert routed["result"]["revision"] == 3
|
||||||
|
assert input_mapped["result"]["revision"] == 4
|
||||||
|
assert output_mapped["result"]["revision"] == 5
|
||||||
|
draft = fetched["result"]["draft"]
|
||||||
|
assert draft["name"] == "focused_renamed"
|
||||||
|
assert draft["routes"]["call"]["ok"] == "__end__"
|
||||||
|
assert draft["steps"]["call"]["input"] == [
|
||||||
|
{
|
||||||
|
"target": {"root": "local", "parts": ["value"]},
|
||||||
|
"path": {"root": "input", "parts": ["value"]},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
assert draft["steps"]["call"]["output"] == [
|
||||||
|
{
|
||||||
|
"source": {"root": "local", "parts": ["value"]},
|
||||||
|
"target": {"root": "state", "parts": ["value"]},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
async def test_rpc_diagnoses_source(tmp_path) -> None:
|
async def test_rpc_diagnoses_source(tmp_path) -> None:
|
||||||
server = build_local_static_workflow_server(tmp_path / "store")
|
server = build_local_static_workflow_server(tmp_path / "store")
|
||||||
app = create_rpc_app(server)
|
app = create_rpc_app(server)
|
||||||
|
|||||||
@@ -445,6 +445,53 @@ async def test_rpc_client_creates_artifact_from_plan(tmp_path) -> None:
|
|||||||
assert inspected["id"] == "client_plan"
|
assert inspected["id"] == "client_plan"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_rpc_client_draft_workspace_focused_edit_methods(tmp_path) -> None:
|
||||||
|
server = build_local_static_workflow_server(tmp_path / "store")
|
||||||
|
app = create_rpc_app(server)
|
||||||
|
transport = httpx.ASGITransport(app=app)
|
||||||
|
async with httpx.AsyncClient(transport=transport, base_url="http://test") as http_client:
|
||||||
|
client = RpcWorkflowApiClient(
|
||||||
|
url="http://test/rpc",
|
||||||
|
timeout_seconds=5,
|
||||||
|
http_client=http_client,
|
||||||
|
)
|
||||||
|
await client.create_draft_workspace_from_capability(
|
||||||
|
workspace_id="client_focused_ws",
|
||||||
|
capability_name="wf.std.constant",
|
||||||
|
name="client_initial",
|
||||||
|
)
|
||||||
|
|
||||||
|
named = await client.set_draft_name(
|
||||||
|
workspace_id="client_focused_ws",
|
||||||
|
revision=1,
|
||||||
|
name="client_renamed",
|
||||||
|
)
|
||||||
|
routed = await client.set_draft_route(
|
||||||
|
workspace_id="client_focused_ws",
|
||||||
|
revision=2,
|
||||||
|
step_id="call",
|
||||||
|
outcome="ok",
|
||||||
|
target="__end__",
|
||||||
|
)
|
||||||
|
input_mapped = await client.set_step_input_map(
|
||||||
|
workspace_id="client_focused_ws",
|
||||||
|
revision=3,
|
||||||
|
step_id="call",
|
||||||
|
input_map={"input.value": "value"},
|
||||||
|
)
|
||||||
|
output_mapped = await client.set_step_output_map(
|
||||||
|
workspace_id="client_focused_ws",
|
||||||
|
revision=4,
|
||||||
|
step_id="call",
|
||||||
|
output_map={"value": "state.value"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert named["revision"] == 2
|
||||||
|
assert routed["revision"] == 3
|
||||||
|
assert input_mapped["revision"] == 4
|
||||||
|
assert output_mapped["revision"] == 5
|
||||||
|
|
||||||
|
|
||||||
async def test_rpc_client_diagnoses_source(tmp_path) -> None:
|
async def test_rpc_client_diagnoses_source(tmp_path) -> None:
|
||||||
calls: list[tuple[str, dict[str, object]]] = []
|
calls: list[tuple[str, dict[str, object]]] = []
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user