feat: import draft documents
This commit is contained in:
@@ -123,6 +123,33 @@ def export_draft(
|
||||
raise typer.BadParameter(str(exc)) from exc
|
||||
|
||||
|
||||
@app.command("import")
|
||||
def import_draft(
|
||||
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."),
|
||||
],
|
||||
input_file: Annotated[
|
||||
Path, typer.Option("--file", help="Draft JSON document to import.")
|
||||
],
|
||||
) -> None:
|
||||
"""Replace an existing workspace draft at an expected revision."""
|
||||
draft = parse_json_object_file(input_file, option_name="--file")
|
||||
context = load_cli_context(ctx)
|
||||
emit_json(
|
||||
run_cli_operation(
|
||||
context,
|
||||
context.handlers.replace_draft_workspace_document(
|
||||
workspace_id=workspace_id,
|
||||
revision=revision,
|
||||
draft=draft,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@app.command("create")
|
||||
def create_draft(
|
||||
ctx: typer.Context,
|
||||
|
||||
@@ -2980,3 +2980,96 @@ def test_wf_draft_export_reports_missing_parent_directory(
|
||||
|
||||
assert result.exit_code == 2
|
||||
assert "could not write file" in result.output
|
||||
|
||||
|
||||
def test_wf_draft_import_passes_exact_document_to_replacement_handler(
|
||||
monkeypatch,
|
||||
tmp_path,
|
||||
) -> None:
|
||||
calls: list[dict[str, Any]] = []
|
||||
expected_draft = {
|
||||
"name": "report",
|
||||
"steps": {"finish": {"end": {"outcome": "ok"}}},
|
||||
"routes": {},
|
||||
"output": [{"path": "state.report", "target": "report"}],
|
||||
}
|
||||
|
||||
class FakeHandlers:
|
||||
async def replace_draft_workspace_document(
|
||||
self, **kwargs: Any
|
||||
) -> dict[str, Any]:
|
||||
calls.append(kwargs)
|
||||
return {"workspace_id": "restored", "revision": 5, "status": "valid"}
|
||||
|
||||
context = SimpleNamespace(handlers=FakeHandlers(), verbose=False)
|
||||
monkeypatch.setattr("wf_cli.commands.drafts.load_cli_context", lambda _ctx: context)
|
||||
input_path = tmp_path / "report-draft.json"
|
||||
input_path.write_text(json.dumps(expected_draft), encoding="utf-8")
|
||||
|
||||
result = runner.invoke(
|
||||
app,
|
||||
[
|
||||
"draft",
|
||||
"import",
|
||||
"restored",
|
||||
"--revision",
|
||||
"4",
|
||||
"--file",
|
||||
str(input_path),
|
||||
],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
assert calls == [
|
||||
{
|
||||
"workspace_id": "restored",
|
||||
"revision": 4,
|
||||
"draft": expected_draft,
|
||||
}
|
||||
]
|
||||
assert json.loads(result.output) == {
|
||||
"workspace_id": "restored",
|
||||
"revision": 5,
|
||||
"status": "valid",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("file_kind", "contents", "expected_error"),
|
||||
[
|
||||
("missing", None, "cannot read"),
|
||||
("malformed", "{", "invalid JSON"),
|
||||
("array", "[]", "expected a JSON object"),
|
||||
],
|
||||
)
|
||||
def test_wf_draft_import_rejects_bad_files_before_loading_context(
|
||||
monkeypatch,
|
||||
tmp_path,
|
||||
file_kind: str,
|
||||
contents: str | None,
|
||||
expected_error: str,
|
||||
) -> None:
|
||||
monkeypatch.setattr(
|
||||
"wf_cli.commands.drafts.load_cli_context",
|
||||
lambda _ctx: (_ for _ in ()).throw(AssertionError("context loaded")),
|
||||
)
|
||||
input_path = tmp_path / f"{file_kind}.json"
|
||||
if contents is not None:
|
||||
input_path.write_text(contents, encoding="utf-8")
|
||||
|
||||
result = runner.invoke(
|
||||
app,
|
||||
[
|
||||
"draft",
|
||||
"import",
|
||||
"restored",
|
||||
"--revision",
|
||||
"4",
|
||||
"--file",
|
||||
str(input_path),
|
||||
],
|
||||
)
|
||||
|
||||
assert result.exit_code == 2
|
||||
assert expected_error in " ".join(result.output.split())
|
||||
assert "context loaded" not in result.output
|
||||
|
||||
@@ -872,6 +872,145 @@ def test_wf_draft_export_uses_remote_get_and_writes_only_draft(
|
||||
assert "revision" not in payload
|
||||
|
||||
|
||||
def test_wf_draft_import_uses_exact_remote_replacement_payload(
|
||||
monkeypatch, tmp_path
|
||||
) -> None:
|
||||
server = build_local_static_workflow_server(tmp_path / "store")
|
||||
asyncio.run(
|
||||
server.api.create_empty_draft_workspace(
|
||||
workspace_id="source_ws",
|
||||
name="source",
|
||||
)
|
||||
)
|
||||
asyncio.run(
|
||||
server.api.create_empty_draft_workspace(
|
||||
workspace_id="destination_ws",
|
||||
name="destination",
|
||||
)
|
||||
)
|
||||
source = asyncio.run(
|
||||
server.api.get_draft_workspace(
|
||||
workspace_id="source_ws",
|
||||
include_draft=True,
|
||||
)
|
||||
)
|
||||
expected_draft = source["draft"]
|
||||
_patch_rpc_client_to_server(monkeypatch, server)
|
||||
rpc_calls: list[tuple[str, dict[str, Any]]] = []
|
||||
original_call = RpcClientTransport._call
|
||||
|
||||
async def recording_call(
|
||||
self: RpcClientTransport, method: str, params: dict[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
rpc_calls.append((method, params))
|
||||
return await original_call(self, method, params)
|
||||
|
||||
monkeypatch.setattr(RpcClientTransport, "_call", recording_call)
|
||||
config_path = tmp_path / "wf.json"
|
||||
config_path.write_text('{"version": 1}', encoding="utf-8")
|
||||
input_path = tmp_path / "source-draft.json"
|
||||
input_path.write_text(json.dumps(expected_draft), encoding="utf-8")
|
||||
runner = CliRunner()
|
||||
|
||||
imported = runner.invoke(
|
||||
app,
|
||||
[
|
||||
"--config",
|
||||
str(config_path),
|
||||
"--url",
|
||||
"http://test/rpc",
|
||||
"draft",
|
||||
"import",
|
||||
"destination_ws",
|
||||
"--revision",
|
||||
"1",
|
||||
"--file",
|
||||
str(input_path),
|
||||
],
|
||||
)
|
||||
|
||||
assert imported.exit_code == 0, imported.output
|
||||
assert rpc_calls == [
|
||||
(
|
||||
"workflow.draft_workspaces.replace_document",
|
||||
{
|
||||
"workspace_id": "destination_ws",
|
||||
"revision": 1,
|
||||
"draft": expected_draft,
|
||||
},
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def test_wf_draft_transfer_round_trip_preserves_document_and_destination_id(
|
||||
monkeypatch, tmp_path
|
||||
) -> None:
|
||||
server = build_local_static_workflow_server(tmp_path / "store")
|
||||
asyncio.run(
|
||||
server.api.create_empty_draft_workspace(
|
||||
workspace_id="source_ws",
|
||||
name="source",
|
||||
title="Source workflow",
|
||||
)
|
||||
)
|
||||
asyncio.run(
|
||||
server.api.create_empty_draft_workspace(
|
||||
workspace_id="destination_ws",
|
||||
name="destination",
|
||||
title="Destination workflow",
|
||||
)
|
||||
)
|
||||
_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"]
|
||||
transfer_path = tmp_path / "transfer.json"
|
||||
|
||||
exported = runner.invoke(
|
||||
app,
|
||||
[
|
||||
*base_args,
|
||||
"draft",
|
||||
"export",
|
||||
"source_ws",
|
||||
"--output",
|
||||
str(transfer_path),
|
||||
],
|
||||
)
|
||||
imported = runner.invoke(
|
||||
app,
|
||||
[
|
||||
*base_args,
|
||||
"draft",
|
||||
"import",
|
||||
"destination_ws",
|
||||
"--revision",
|
||||
"1",
|
||||
"--file",
|
||||
str(transfer_path),
|
||||
],
|
||||
)
|
||||
inspected = runner.invoke(
|
||||
app,
|
||||
[
|
||||
*base_args,
|
||||
"draft",
|
||||
"inspect",
|
||||
"destination_ws",
|
||||
"--include-draft",
|
||||
],
|
||||
)
|
||||
|
||||
assert exported.exit_code == 0, exported.output
|
||||
assert imported.exit_code == 0, imported.output
|
||||
assert inspected.exit_code == 0, inspected.output
|
||||
exported_draft = json.loads(transfer_path.read_text(encoding="utf-8"))
|
||||
destination = json.loads(inspected.output)
|
||||
assert destination["workspace_id"] == "destination_ws"
|
||||
assert destination["draft"] == exported_draft
|
||||
|
||||
|
||||
def test_wf_remote_run_resume_interrupted_deployment(monkeypatch, tmp_path) -> None:
|
||||
server = build_local_static_workflow_server(tmp_path / "store")
|
||||
asyncio.run(
|
||||
|
||||
Reference in New Issue
Block a user