feat: accept nested local capability maps
This commit is contained in:
@@ -1423,6 +1423,44 @@ async def test_add_step_from_capability_wires_route_inputs_and_state_outputs(
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_step_from_capability_projects_nested_local_input(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
api, service, authoring = _draft_api(
|
||||
FileWorkflowArtifactStore(tmp_path / "nested_capability_input"),
|
||||
register_echo=True,
|
||||
)
|
||||
service.register_specs("demo.personal", _nested_report)
|
||||
draft = _nested_report_draft()
|
||||
draft["steps"] = {}
|
||||
draft["routes"] = {}
|
||||
await api.create_draft_workspace(workspace_id="nested_add", draft=draft)
|
||||
|
||||
result = await authoring.add_step_from_capability(
|
||||
workspace_id="nested_add",
|
||||
revision=1,
|
||||
step_id="render",
|
||||
capability_name="demo.personal.nested_report",
|
||||
routes={"ok": "__end__"},
|
||||
input_map={"input.title": "report.title"},
|
||||
bind_outputs={},
|
||||
)
|
||||
workspace = await api.get_draft_workspace(
|
||||
workspace_id="nested_add", include_draft=True
|
||||
)
|
||||
validated = await api.validate_draft_workspace(workspace_id="nested_add")
|
||||
|
||||
assert result["revision"] == 2
|
||||
assert workspace["draft"]["input_schema"]["properties"]["title"]["type"] == (
|
||||
"string"
|
||||
)
|
||||
assert workspace["draft"]["steps"]["render"]["input"] == [
|
||||
{"target": "report.title", "path": "input.title"}
|
||||
]
|
||||
assert validated["status"] == "valid", validated["diagnostics"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_step_from_capability_rejects_existing_step_id(
|
||||
tmp_path: Path,
|
||||
|
||||
@@ -515,6 +515,7 @@ def test_wf_draft_map_help_explains_replace_merge_and_validate() -> None:
|
||||
assert "draft validate" in input_help
|
||||
assert "input.text=text" in input_help
|
||||
assert "input.text=local.text" in input_help
|
||||
assert "input.title=report.title" in input_help
|
||||
assert "replaces the full output map" in output_help
|
||||
assert "Use --merge only" in output_help
|
||||
assert "draft validate" in output_help
|
||||
@@ -535,6 +536,7 @@ def test_wf_draft_bind_help_explains_direction() -> None:
|
||||
assert "validate" in help_text
|
||||
assert "project missing schema" in help_text
|
||||
assert "set-input --merge" in help_text
|
||||
assert "local.report.title" in help_text
|
||||
|
||||
|
||||
def test_wf_draft_add_capability_help_explains_explicit_wiring() -> None:
|
||||
@@ -550,7 +552,7 @@ def test_wf_draft_add_capability_help_explains_explicit_wiring() -> None:
|
||||
assert "draft validate" in output
|
||||
assert "wf draft add capability report_ws" in output
|
||||
assert "Repeat the flag" in output
|
||||
assert "--input state.title=title --input state.summary=summary" in output
|
||||
assert "--input state.title=report.title" in output
|
||||
assert (
|
||||
"--bind-output title=state.title --bind-output summary=state.summary" in output
|
||||
)
|
||||
@@ -587,7 +589,7 @@ def test_wf_draft_add_capability_calls_composed_local_handler(monkeypatch) -> No
|
||||
"--route",
|
||||
"ok=__end__",
|
||||
"--input",
|
||||
"input.text=text",
|
||||
"input.text=report.text",
|
||||
"--bind-output",
|
||||
"value=state.value",
|
||||
],
|
||||
@@ -602,7 +604,7 @@ def test_wf_draft_add_capability_calls_composed_local_handler(monkeypatch) -> No
|
||||
assert call["route_from_step"] == "start"
|
||||
assert call["route_from_outcome"] == "ok"
|
||||
assert call["routes"] == {"ok": "__end__"}
|
||||
assert call["input_map"] == {"input.text": "text"}
|
||||
assert call["input_map"] == {"input.text": "report.text"}
|
||||
assert call["bind_outputs"] == {"value": "state.value"}
|
||||
|
||||
|
||||
@@ -1487,6 +1489,62 @@ def test_wf_draft_set_input_rejects_local_prefixed_target() -> None:
|
||||
|
||||
assert result.exit_code == 2
|
||||
output = " ".join(result.output.split())
|
||||
assert "bare local field" in output
|
||||
assert "rootless node-local path" in output
|
||||
assert "input.text=text" in output
|
||||
assert "input.text=local.text" in output
|
||||
|
||||
|
||||
def test_wf_draft_set_input_accepts_nested_rootless_target(monkeypatch) -> None:
|
||||
calls: list[dict[str, Any]] = []
|
||||
|
||||
class FakeHandlers:
|
||||
async def set_step_input_map(self, **kwargs: Any) -> dict[str, Any]:
|
||||
calls.append(kwargs)
|
||||
return {"revision": 2, "status": "valid"}
|
||||
|
||||
context = SimpleNamespace(handlers=FakeHandlers(), verbose=False)
|
||||
monkeypatch.setattr("wf_cli.commands.drafts.load_cli_context", lambda _ctx: context)
|
||||
|
||||
result = runner.invoke(
|
||||
app,
|
||||
[
|
||||
"draft",
|
||||
"set-input",
|
||||
"report_ws",
|
||||
"--revision",
|
||||
"1",
|
||||
"--step",
|
||||
"render",
|
||||
"--map",
|
||||
"input.title=report.title",
|
||||
],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
assert calls[0]["input_map"] == {"input.title": "report.title"}
|
||||
|
||||
|
||||
def test_wf_draft_set_input_rejects_malformed_local_path(monkeypatch) -> None:
|
||||
monkeypatch.setattr(
|
||||
"wf_cli.commands.drafts.load_cli_context",
|
||||
lambda _ctx: (_ for _ in ()).throw(AssertionError("context loaded")),
|
||||
)
|
||||
|
||||
result = runner.invoke(
|
||||
app,
|
||||
[
|
||||
"draft",
|
||||
"set-input",
|
||||
"report_ws",
|
||||
"--revision",
|
||||
"1",
|
||||
"--step",
|
||||
"render",
|
||||
"--map",
|
||||
"input.title=report..title",
|
||||
],
|
||||
)
|
||||
|
||||
assert result.exit_code == 2
|
||||
assert "rootless node-local path" in result.output
|
||||
assert "context loaded" not in result.output
|
||||
|
||||
@@ -1396,6 +1396,58 @@ def test_wf_draft_bind_uses_rpc_target(monkeypatch, tmp_path) -> None:
|
||||
]
|
||||
|
||||
|
||||
def test_wf_draft_set_input_preserves_nested_target_over_rpc(
|
||||
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",
|
||||
"nested_input_ws",
|
||||
"--capability",
|
||||
"wf.std.constant",
|
||||
"--name",
|
||||
"nested_input",
|
||||
],
|
||||
)
|
||||
assert created.exit_code == 0, created.output
|
||||
|
||||
result = runner.invoke(
|
||||
app,
|
||||
[
|
||||
*base_args,
|
||||
"draft",
|
||||
"set-input",
|
||||
"nested_input_ws",
|
||||
"--revision",
|
||||
"1",
|
||||
"--step",
|
||||
"call",
|
||||
"--map",
|
||||
"input.value=payload.value",
|
||||
],
|
||||
)
|
||||
inspected = runner.invoke(
|
||||
app,
|
||||
[*base_args, "draft", "inspect", "nested_input_ws", "--include-draft"],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
assert inspected.exit_code == 0, inspected.output
|
||||
draft = json.loads(inspected.output)["draft"]
|
||||
assert draft["steps"]["call"]["input"] == [
|
||||
{"target": "payload.value", "path": "input.value"}
|
||||
]
|
||||
|
||||
|
||||
def test_wf_draft_add_capability_uses_rpc_target(monkeypatch, tmp_path) -> None:
|
||||
server = build_local_static_workflow_server(tmp_path / "store")
|
||||
_patch_rpc_client_to_server(monkeypatch, server)
|
||||
|
||||
@@ -802,7 +802,7 @@ async def test_rpc_draft_workspace_focused_edit_methods(tmp_path) -> None:
|
||||
"workspace_id": "focused_ws",
|
||||
"revision": 3,
|
||||
"step_id": "call",
|
||||
"input_map": {"input.value": "value"},
|
||||
"input_map": {"input.value": "payload.value"},
|
||||
},
|
||||
)
|
||||
output_mapped = await _rpc(
|
||||
@@ -866,7 +866,7 @@ async def test_rpc_draft_workspace_focused_edit_methods(tmp_path) -> None:
|
||||
assert draft["routes"]["call"]["ok"] == "__end__"
|
||||
assert draft["steps"]["call"]["input"] == [
|
||||
{
|
||||
"target": "value",
|
||||
"target": "payload.value",
|
||||
"path": "input.value",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -760,6 +760,34 @@ async def test_rpc_client_draft_workspace_add_step_from_capability(tmp_path) ->
|
||||
assert result["status"] == "valid"
|
||||
|
||||
|
||||
async def test_rpc_client_preserves_nested_local_path_strings() -> None:
|
||||
calls: list[dict[str, Any]] = []
|
||||
|
||||
class Client(RpcDraftClientMixin):
|
||||
async def _call(self, method: str, params: dict[str, object]):
|
||||
calls.append({"method": method, "params": params})
|
||||
return {"revision": 2}
|
||||
|
||||
client = Client()
|
||||
await client.bind_draft(
|
||||
workspace_id="ws",
|
||||
revision=1,
|
||||
step_id="render",
|
||||
source_path="input.title",
|
||||
target_path="local.report.title",
|
||||
)
|
||||
await client.add_step_from_capability(
|
||||
workspace_id="ws",
|
||||
revision=2,
|
||||
step_id="render",
|
||||
capability_name="demo.report",
|
||||
input_map={"input.title": "report.title"},
|
||||
)
|
||||
|
||||
assert calls[0]["params"]["target_path"] == "local.report.title"
|
||||
assert calls[1]["params"]["input_map"] == {"input.title": "report.title"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("step_id", "step", "expected_wire"),
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user