feat: add draft delete command
This commit is contained in:
@@ -33,9 +33,9 @@ clear operator feedback before adding more architecture.
|
|||||||
[`2026-06-09 product smoke RPC CLI`](superpowers/research/2026-06-09-product-smoke-rpc-cli.md).
|
[`2026-06-09 product smoke RPC CLI`](superpowers/research/2026-06-09-product-smoke-rpc-cli.md).
|
||||||
- Completed: `wf artifact inspect` now accepts `--version` as an alias for the
|
- Completed: `wf artifact inspect` now accepts `--version` as an alias for the
|
||||||
positional version argument.
|
positional version argument.
|
||||||
- Next product cleanup: expose existing draft workspace deletion as safe
|
- Completed: `wf draft delete <workspace_id> --confirm` exposes existing draft
|
||||||
`wf draft delete <workspace_id> --confirm`. Active plan:
|
workspace deletion as a safe CLI command. Implementation:
|
||||||
[`wf draft delete CLI/RPC`](superpowers/plans/2026-06-09-wf-draft-delete-cli-rpc.md).
|
[`wf draft delete CLI/RPC`](historical/superpowers/plans/2026-06-09-wf-draft-delete-cli-rpc.md).
|
||||||
- Separate cleanup: design artifact deletion before exposing a command.
|
- Separate cleanup: design artifact deletion before exposing a command.
|
||||||
Deployments live in the artifact store and can reference artifact versions, so
|
Deployments live in the artifact store and can reference artifact versions, so
|
||||||
`wf artifact delete <artifact_id> <version> --confirm` must reject referenced
|
`wf artifact delete <artifact_id> <version> --confirm` must reject referenced
|
||||||
|
|||||||
@@ -206,6 +206,15 @@ Validate:
|
|||||||
wf draft validate concat_ws
|
wf draft validate concat_ws
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Delete a draft workspace:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wf draft delete concat_ws --confirm
|
||||||
|
```
|
||||||
|
|
||||||
|
Draft deletion removes only the draft workspace. It does not delete artifacts,
|
||||||
|
deployments, or runs.
|
||||||
|
|
||||||
Save as an artifact:
|
Save as an artifact:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -82,6 +82,12 @@ class WorkflowDraftSurface(Protocol):
|
|||||||
workspace_id: str,
|
workspace_id: str,
|
||||||
) -> dict[str, Any]: ...
|
) -> dict[str, Any]: ...
|
||||||
|
|
||||||
|
async def delete_draft_workspace(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
workspace_id: str,
|
||||||
|
) -> dict[str, Any]: ...
|
||||||
|
|
||||||
async def create_artifact_from_workspace(
|
async def create_artifact_from_workspace(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
|
|||||||
@@ -133,6 +133,30 @@ def validate_draft(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.command("delete")
|
||||||
|
def delete_draft(
|
||||||
|
ctx: typer.Context,
|
||||||
|
workspace_id: Annotated[str, typer.Argument(help="Draft workspace id.")],
|
||||||
|
confirm: Annotated[
|
||||||
|
bool,
|
||||||
|
typer.Option(
|
||||||
|
"--confirm",
|
||||||
|
help="Required confirmation for deleting a draft workspace.",
|
||||||
|
),
|
||||||
|
] = False,
|
||||||
|
) -> None:
|
||||||
|
"""Delete a stored draft workspace."""
|
||||||
|
if not confirm:
|
||||||
|
raise typer.BadParameter("pass --confirm to delete a draft workspace")
|
||||||
|
context = load_cli_context(ctx)
|
||||||
|
emit_json(
|
||||||
|
run_cli_operation(
|
||||||
|
context,
|
||||||
|
context.handlers.delete_draft_workspace(workspace_id=workspace_id),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.command("save")
|
@app.command("save")
|
||||||
def save_draft(
|
def save_draft(
|
||||||
ctx: typer.Context,
|
ctx: typer.Context,
|
||||||
|
|||||||
@@ -79,6 +79,16 @@ class RpcDraftClientMixin:
|
|||||||
{"workspace_id": workspace_id},
|
{"workspace_id": workspace_id},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def delete_draft_workspace(
|
||||||
|
self: RpcCaller,
|
||||||
|
*,
|
||||||
|
workspace_id: str,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
return await self._call(
|
||||||
|
"workflow.draft_workspaces.delete",
|
||||||
|
{"workspace_id": workspace_id},
|
||||||
|
)
|
||||||
|
|
||||||
async def create_artifact_from_workspace(
|
async def create_artifact_from_workspace(
|
||||||
self: RpcCaller,
|
self: RpcCaller,
|
||||||
*,
|
*,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from ..models import (
|
|||||||
CreateArtifactFromWorkspaceParams,
|
CreateArtifactFromWorkspaceParams,
|
||||||
CreateDraftFromCapabilityParams,
|
CreateDraftFromCapabilityParams,
|
||||||
CreateWrapperFromWorkspaceParams,
|
CreateWrapperFromWorkspaceParams,
|
||||||
|
DeleteDraftWorkspaceParams,
|
||||||
GetDraftWorkspaceParams,
|
GetDraftWorkspaceParams,
|
||||||
ListDraftWorkspacesParams,
|
ListDraftWorkspacesParams,
|
||||||
PatchDraftParams,
|
PatchDraftParams,
|
||||||
@@ -117,6 +118,19 @@ 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.delete", errors=[WorkflowRpcError]
|
||||||
|
)
|
||||||
|
async def workflow_draft_workspaces_delete(
|
||||||
|
params: DeleteDraftWorkspaceParams = RpcParams(),
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
try:
|
||||||
|
return await server.api.delete_draft_workspace(
|
||||||
|
workspace_id=params.workspace_id,
|
||||||
|
)
|
||||||
|
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||||
|
raise_workflow_rpc_error(exc)
|
||||||
|
|
||||||
@entrypoint.method(
|
@entrypoint.method(
|
||||||
name="workflow.draft_workspaces.create_artifact", errors=[WorkflowRpcError]
|
name="workflow.draft_workspaces.create_artifact", errors=[WorkflowRpcError]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -111,6 +111,10 @@ class ValidateDraftWorkspaceParams(RpcParamsModel):
|
|||||||
workspace_id: str = Field(min_length=1)
|
workspace_id: str = Field(min_length=1)
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteDraftWorkspaceParams(RpcParamsModel):
|
||||||
|
workspace_id: str = Field(min_length=1)
|
||||||
|
|
||||||
|
|
||||||
class CreateArtifactFromWorkspaceParams(RpcParamsModel):
|
class CreateArtifactFromWorkspaceParams(RpcParamsModel):
|
||||||
workspace_id: str = Field(min_length=1)
|
workspace_id: str = Field(min_length=1)
|
||||||
artifact_id: str = Field(min_length=1)
|
artifact_id: str = Field(min_length=1)
|
||||||
|
|||||||
@@ -705,3 +705,46 @@ def test_wf_status_reports_rpc_config_target(monkeypatch, tmp_path) -> None:
|
|||||||
assert payload["target"]["mode"] == "remote"
|
assert payload["target"]["mode"] == "remote"
|
||||||
assert payload["target"]["url"] == "http://test/rpc"
|
assert payload["target"]["url"] == "http://test/rpc"
|
||||||
assert payload["workflow"]["capability_count"] >= 1
|
assert payload["workflow"]["capability_count"] >= 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_wf_draft_delete_requires_confirm(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"]
|
||||||
|
|
||||||
|
result = runner.invoke(app, [*base_args, "draft", "delete", "delete-me"])
|
||||||
|
|
||||||
|
assert result.exit_code != 0
|
||||||
|
assert "confirm" in (result.output).lower()
|
||||||
|
|
||||||
|
|
||||||
|
def test_wf_draft_delete_succeeds_with_confirm(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"]
|
||||||
|
|
||||||
|
runner.invoke(
|
||||||
|
app,
|
||||||
|
[
|
||||||
|
*base_args,
|
||||||
|
"draft",
|
||||||
|
"create-from-capability",
|
||||||
|
"delete-me",
|
||||||
|
"wf.std.constant",
|
||||||
|
"--name",
|
||||||
|
"delete_me_ws",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
result = runner.invoke(app, [*base_args, "draft", "delete", "delete-me", "--confirm"])
|
||||||
|
|
||||||
|
assert result.exit_code == 0, result.output
|
||||||
|
payload = json.loads(result.output)
|
||||||
|
assert payload["workspace_id"] == "delete-me"
|
||||||
|
assert payload["deleted"] is True
|
||||||
|
|||||||
@@ -313,6 +313,29 @@ async def test_rpc_draft_workspace_methods(tmp_path) -> None:
|
|||||||
assert artifact["result"]["artifact_id"] == "remote_artifact"
|
assert artifact["result"]["artifact_id"] == "remote_artifact"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_rpc_draft_workspace_delete(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:
|
||||||
|
await _rpc(
|
||||||
|
client,
|
||||||
|
"workflow.draft_workspaces.create_from_capability",
|
||||||
|
{
|
||||||
|
"workspace_id": "delete-me",
|
||||||
|
"capability_name": "wf.std.constant",
|
||||||
|
"name": "delete_me_ws",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
payload = await _rpc(
|
||||||
|
client,
|
||||||
|
"workflow.draft_workspaces.delete",
|
||||||
|
{"workspace_id": "delete-me"},
|
||||||
|
)
|
||||||
|
assert payload["result"]["workspace_id"] == "delete-me"
|
||||||
|
assert payload["result"]["deleted"] is True
|
||||||
|
|
||||||
|
|
||||||
def _constant_plan() -> RawWorkflowPlan:
|
def _constant_plan() -> RawWorkflowPlan:
|
||||||
return RawWorkflowPlan.model_validate(
|
return RawWorkflowPlan.model_validate(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
from wf_api.models import RawWorkflowPlan, TraceRange
|
from wf_api.models import RawWorkflowPlan, TraceRange
|
||||||
|
from wf_api.surface import WorkflowDraftSurface
|
||||||
from wf_core import END
|
from wf_core import END
|
||||||
from wf_server import build_local_static_workflow_server
|
from wf_server import build_local_static_workflow_server
|
||||||
from wf_transport_rpc_http import RpcWorkflowApiClient, create_rpc_app
|
from wf_transport_rpc_http import RpcWorkflowApiClient, create_rpc_app
|
||||||
@@ -317,3 +318,31 @@ async def test_rpc_workflow_client_draft_workspace_lifecycle(tmp_path) -> None:
|
|||||||
assert validated["status"] in {"valid", "invalid"}
|
assert validated["status"] in {"valid", "invalid"}
|
||||||
assert patched["revision"] == created["revision"] + 1
|
assert patched["revision"] == created["revision"] + 1
|
||||||
assert artifact["artifact_id"] == "client_ws_art"
|
assert artifact["artifact_id"] == "client_ws_art"
|
||||||
|
|
||||||
|
|
||||||
|
def test_rpc_client_satisfies_draft_surface_static_shape() -> None:
|
||||||
|
_: type[WorkflowDraftSurface] = RpcWorkflowApiClient
|
||||||
|
|
||||||
|
|
||||||
|
async def test_rpc_workflow_client_deletes_draft_workspace(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="delete-me",
|
||||||
|
capability_name="wf.std.constant",
|
||||||
|
name="delete_me_ws",
|
||||||
|
)
|
||||||
|
deleted = await client.delete_draft_workspace(workspace_id="delete-me")
|
||||||
|
assert deleted["workspace_id"] == "delete-me"
|
||||||
|
assert deleted["deleted"] is True
|
||||||
|
|
||||||
|
deleted_again = await client.delete_draft_workspace(workspace_id="delete-me")
|
||||||
|
assert deleted_again["workspace_id"] == "delete-me"
|
||||||
|
assert deleted_again["deleted"] is False
|
||||||
|
|||||||
Reference in New Issue
Block a user