245 lines
6.7 KiB
Python
245 lines
6.7 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from dataclasses import dataclass
|
|
from typing import Any
|
|
|
|
from typer.testing import CliRunner
|
|
|
|
from wf_cli.app import app
|
|
|
|
|
|
class _ArtifactHandlers:
|
|
def __init__(self) -> None:
|
|
self.calls: list[tuple[str, int]] = []
|
|
|
|
async def inspect_artifact(
|
|
self,
|
|
*,
|
|
artifact_id: str,
|
|
version: int,
|
|
) -> dict[str, Any]:
|
|
self.calls.append((artifact_id, version))
|
|
return {"id": artifact_id, "version": version}
|
|
|
|
async def delete_artifact(
|
|
self,
|
|
*,
|
|
artifact_id: str,
|
|
version: int,
|
|
) -> dict[str, Any]:
|
|
self.calls.append((artifact_id, version))
|
|
return {
|
|
"artifact_id": artifact_id,
|
|
"version": version,
|
|
"deleted": True,
|
|
"blocked_by_deployments": [],
|
|
}
|
|
|
|
async def create_artifact_from_plan(
|
|
self,
|
|
*,
|
|
artifact_id: str,
|
|
version: int,
|
|
title: str,
|
|
plan: dict[str, Any],
|
|
outcomes: tuple[str, ...],
|
|
kind: str = "workflow",
|
|
description: str | None = None,
|
|
required_capabilities: dict[str, dict[str, Any]] | None = None,
|
|
source_bindings: dict[str, str] | None = None,
|
|
created_from_catalog_version: str | None = None,
|
|
) -> dict[str, Any]:
|
|
self.calls.append((artifact_id, version))
|
|
return {
|
|
"artifact_id": artifact_id,
|
|
"version": version,
|
|
"title": title,
|
|
"plan_name": plan["name"],
|
|
"outcomes": list(outcomes),
|
|
"kind": kind,
|
|
"source_bindings": source_bindings,
|
|
}
|
|
|
|
|
|
class _BlockedArtifactHandlers:
|
|
def __init__(self) -> None:
|
|
self.calls: list[tuple[str, int]] = []
|
|
|
|
async def delete_artifact(
|
|
self,
|
|
*,
|
|
artifact_id: str,
|
|
version: int,
|
|
) -> dict[str, Any]:
|
|
self.calls.append((artifact_id, version))
|
|
return {
|
|
"artifact_id": artifact_id,
|
|
"version": version,
|
|
"deleted": False,
|
|
"blocked_by_deployments": ["echo.default"],
|
|
}
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class _Context:
|
|
handlers: _ArtifactHandlers
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class _BlockedContext:
|
|
handlers: _BlockedArtifactHandlers
|
|
|
|
|
|
def test_artifact_inspect_accepts_version_option(monkeypatch) -> None:
|
|
handlers = _ArtifactHandlers()
|
|
monkeypatch.setattr(
|
|
"wf_cli.commands.artifacts.load_cli_context",
|
|
lambda _ctx: _Context(handlers=handlers),
|
|
)
|
|
|
|
result = CliRunner().invoke(
|
|
app,
|
|
["artifact", "inspect", "demo_artifact", "--version", "2"],
|
|
)
|
|
|
|
assert result.exit_code == 0, result.output
|
|
assert json.loads(result.output) == {"id": "demo_artifact", "version": 2}
|
|
assert handlers.calls == [("demo_artifact", 2)]
|
|
|
|
|
|
def test_artifact_inspect_keeps_positional_version(monkeypatch) -> None:
|
|
handlers = _ArtifactHandlers()
|
|
monkeypatch.setattr(
|
|
"wf_cli.commands.artifacts.load_cli_context",
|
|
lambda _ctx: _Context(handlers=handlers),
|
|
)
|
|
|
|
result = CliRunner().invoke(app, ["artifact", "inspect", "demo_artifact", "3"])
|
|
|
|
assert result.exit_code == 0, result.output
|
|
assert json.loads(result.output) == {"id": "demo_artifact", "version": 3}
|
|
assert handlers.calls == [("demo_artifact", 3)]
|
|
|
|
|
|
def test_artifact_delete_requires_confirm(monkeypatch) -> None:
|
|
handlers = _ArtifactHandlers()
|
|
monkeypatch.setattr(
|
|
"wf_cli.commands.artifacts.load_cli_context",
|
|
lambda _ctx: _Context(handlers=handlers),
|
|
)
|
|
|
|
result = CliRunner().invoke(app, ["artifact", "delete", "echo", "1"])
|
|
|
|
assert result.exit_code != 0
|
|
assert "confirm" in result.output.lower()
|
|
|
|
|
|
def test_artifact_delete_confirmed_succeeds(monkeypatch) -> None:
|
|
handlers = _ArtifactHandlers()
|
|
monkeypatch.setattr(
|
|
"wf_cli.commands.artifacts.load_cli_context",
|
|
lambda _ctx: _Context(handlers=handlers),
|
|
)
|
|
|
|
result = CliRunner().invoke(app, ["artifact", "delete", "echo", "1", "--confirm"])
|
|
|
|
assert result.exit_code == 0, result.output
|
|
payload = json.loads(result.output)
|
|
assert payload["artifact_id"] == "echo"
|
|
assert payload["version"] == 1
|
|
assert payload["deleted"] is True
|
|
assert handlers.calls == [("echo", 1)]
|
|
|
|
|
|
def test_artifact_delete_blocked_returns_blocker_ids(monkeypatch) -> None:
|
|
handlers = _BlockedArtifactHandlers()
|
|
monkeypatch.setattr(
|
|
"wf_cli.commands.artifacts.load_cli_context",
|
|
lambda _ctx: _BlockedContext(handlers=handlers),
|
|
)
|
|
|
|
result = CliRunner().invoke(app, ["artifact", "delete", "echo", "1", "--confirm"])
|
|
|
|
assert result.exit_code == 0, result.output
|
|
payload = json.loads(result.output)
|
|
assert payload["deleted"] is False
|
|
assert payload["blocked_by_deployments"] == ["echo.default"]
|
|
|
|
|
|
def test_artifact_create_from_plan_reads_yaml(monkeypatch, tmp_path) -> None:
|
|
handlers = _ArtifactHandlers()
|
|
monkeypatch.setattr(
|
|
"wf_cli.commands.artifacts.load_cli_context",
|
|
lambda _ctx: _Context(handlers=handlers),
|
|
)
|
|
plan_file = tmp_path / "plan.yaml"
|
|
plan_file.write_text(
|
|
"""
|
|
name: yaml_plan
|
|
input_schema: {type: object, properties: {}}
|
|
state_schema: {type: object, properties: {}}
|
|
output_schema: {type: object, properties: {}}
|
|
outcomes: [ok]
|
|
start: __end__
|
|
nodes: []
|
|
edges: []
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
result = CliRunner().invoke(
|
|
app,
|
|
[
|
|
"artifact",
|
|
"create-from-plan",
|
|
str(plan_file),
|
|
"--artifact",
|
|
"yaml_artifact",
|
|
"--version",
|
|
"1",
|
|
"--title",
|
|
"YAML Artifact",
|
|
"--outcome",
|
|
"ok",
|
|
"--binding",
|
|
"local.ops=local.ops",
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 0, result.output
|
|
payload = json.loads(result.output)
|
|
assert payload["artifact_id"] == "yaml_artifact"
|
|
assert payload["plan_name"] == "yaml_plan"
|
|
assert payload["source_bindings"] == {"local.ops": "local.ops"}
|
|
|
|
|
|
def test_artifact_create_from_plan_rejects_non_object_yaml(
|
|
monkeypatch, tmp_path
|
|
) -> None:
|
|
handlers = _ArtifactHandlers()
|
|
monkeypatch.setattr(
|
|
"wf_cli.commands.artifacts.load_cli_context",
|
|
lambda _ctx: _Context(handlers=handlers),
|
|
)
|
|
plan_file = tmp_path / "plan.yaml"
|
|
plan_file.write_text("- not\n- object\n", encoding="utf-8")
|
|
|
|
result = CliRunner().invoke(
|
|
app,
|
|
[
|
|
"artifact",
|
|
"create-from-plan",
|
|
str(plan_file),
|
|
"--artifact",
|
|
"bad",
|
|
"--version",
|
|
"1",
|
|
"--title",
|
|
"Bad",
|
|
],
|
|
)
|
|
|
|
assert result.exit_code != 0
|
|
assert "structured file must contain an object" in result.output.lower()
|