from __future__ import annotations from typing import Any import pytest from pydantic import ValidationError from wf_artifacts import ( DraftWorkspaceStore, FileDraftWorkspaceStore, WorkflowDraftWorkspace, create_draft_workspace, get_draft_workspace, patch_draft_workspace, replace_draft_workspace_document, replace_validated_draft_document, summarize_draft_workspace, ) def test_draft_workspace_stores_mutable_draft_with_revision() -> None: workspace = WorkflowDraftWorkspace( id="echo_draft", revision=1, title="Echo Draft", draft=_draft(), status="valid", diagnostics=[], created_at_epoch_ms=100, updated_at_epoch_ms=100, ) assert workspace.id == "echo_draft" assert workspace.revision == 1 assert workspace.draft["steps"]["echo"]["use"] == "demo.echo" def test_draft_workspace_summary_is_compact() -> None: workspace = WorkflowDraftWorkspace( id="echo_draft", revision=3, draft=_draft(), status="valid", diagnostics=[], created_at_epoch_ms=100, updated_at_epoch_ms=200, ) summary = summarize_draft_workspace(workspace) assert summary["workspace_id"] == "echo_draft" assert summary["revision"] == 3 assert summary["status"] == "valid" assert summary["summary"]["name"] == "echo" assert summary["summary"]["steps"] == ["echo"] assert "draft" not in summary def test_file_draft_workspace_store_round_trips_workspace(tmp_path) -> None: store = FileDraftWorkspaceStore(tmp_path) workspace = WorkflowDraftWorkspace( id="echo_draft", revision=1, draft=_draft(), status="valid", diagnostics=[], created_at_epoch_ms=100, updated_at_epoch_ms=100, ) store.save_workspace(workspace) loaded = store.get_workspace("echo_draft") assert loaded == workspace def test_file_draft_workspace_store_lists_workspaces(tmp_path) -> None: store = FileDraftWorkspaceStore(tmp_path) store.save_workspace( WorkflowDraftWorkspace( id="b", revision=1, draft=_draft(), status="valid", diagnostics=[], created_at_epoch_ms=100, updated_at_epoch_ms=100, ) ) store.save_workspace( WorkflowDraftWorkspace( id="a", revision=1, draft=_draft(), status="valid", diagnostics=[], created_at_epoch_ms=100, updated_at_epoch_ms=100, ) ) assert [workspace.id for workspace in store.list_workspaces()] == ["a", "b"] def test_file_draft_workspace_store_deletes_workspace(tmp_path) -> None: store = FileDraftWorkspaceStore(tmp_path) store.save_workspace( WorkflowDraftWorkspace( id="echo_draft", revision=1, draft=_draft(), status="valid", diagnostics=[], created_at_epoch_ms=100, updated_at_epoch_ms=100, ) ) deleted = store.delete_workspace("echo_draft") deleted_again = store.delete_workspace("echo_draft") assert deleted is True assert deleted_again is False assert store.list_workspaces() == [] def test_file_draft_workspace_store_rejects_path_traversal_ids(tmp_path) -> None: store = FileDraftWorkspaceStore(tmp_path) try: store.get_workspace("../outside") except ValueError as exc: assert "path separators are not allowed" in str(exc) else: raise AssertionError("expected unsafe workspace id to be rejected") def test_create_draft_workspace_validates_and_saves(tmp_path) -> None: store = FileDraftWorkspaceStore(tmp_path) result = create_draft_workspace( store, workspace_id="echo_draft", draft=_draft(), title="Echo Draft", ) loaded = store.get_workspace("echo_draft") assert result["workspace_id"] == "echo_draft" assert result["status"] == "valid" assert loaded.revision == 1 assert loaded.status == "valid" def test_create_draft_workspace_rejects_duplicate_id(tmp_path) -> None: store = FileDraftWorkspaceStore(tmp_path) create_draft_workspace(store, workspace_id="echo_draft", draft=_draft()) result = create_draft_workspace( store, workspace_id="echo_draft", draft=_draft(), title="Replacement", ) loaded = store.get_workspace("echo_draft") assert result["status"] == "conflict" assert result["diagnostics"][0]["code"] == "workspace_exists" assert loaded.revision == 1 assert loaded.title is None def test_patch_draft_workspace_applies_patch_and_increments_revision(tmp_path) -> None: store = FileDraftWorkspaceStore(tmp_path) create_draft_workspace(store, workspace_id="echo_draft", draft=_draft()) result = patch_draft_workspace( store, workspace_id="echo_draft", revision=1, patch=[ { "op": "replace", "path": "/name", "value": "echo_v2", } ], ) loaded = store.get_workspace("echo_draft") assert result["revision"] == 2 assert result["status"] == "valid" assert loaded.revision == 2 assert loaded.draft["name"] == "echo_v2" def test_patch_draft_workspace_rejects_stale_revision(tmp_path) -> None: store = FileDraftWorkspaceStore(tmp_path) create_draft_workspace(store, workspace_id="echo_draft", draft=_draft()) patch_draft_workspace( store, workspace_id="echo_draft", revision=1, patch=[], ) result = patch_draft_workspace( store, workspace_id="echo_draft", revision=1, patch=[], ) assert result["status"] == "conflict" assert result["diagnostics"][0]["code"] == "revision_conflict" assert store.get_workspace("echo_draft").revision == 2 def test_patch_draft_workspace_rejects_duplicate_revision_patch(tmp_path) -> None: store = FileDraftWorkspaceStore(tmp_path) create_draft_workspace(store, workspace_id="echo_draft", draft=_draft()) first = patch_draft_workspace( store, workspace_id="echo_draft", revision=1, patch=[{"op": "replace", "path": "/name", "value": "first"}], ) second = patch_draft_workspace( store, workspace_id="echo_draft", revision=1, patch=[{"op": "replace", "path": "/name", "value": "second"}], ) loaded = store.get_workspace("echo_draft") assert first["revision"] == 2 assert second["status"] == "conflict" assert second["diagnostics"][0]["code"] == "revision_conflict" assert loaded.draft["name"] == "first" def test_patch_draft_workspace_rejects_invalid_patch_without_revision_bump( tmp_path, ) -> None: store = FileDraftWorkspaceStore(tmp_path) create_draft_workspace(store, workspace_id="echo_draft", draft=_draft()) result = patch_draft_workspace( store, workspace_id="echo_draft", revision=1, patch=[{"op": "remove", "path": "/missing"}], ) assert result["status"] == "invalid" assert result["diagnostics"][0]["code"] == "patch_invalid" assert store.get_workspace("echo_draft").revision == 1 def test_replace_validated_draft_document_isolates_persisted_draft() -> None: class ReferenceDraftWorkspaceStore(DraftWorkspaceStore): def __init__(self) -> None: self.workspace: WorkflowDraftWorkspace | None = None def save_workspace(self, workspace: WorkflowDraftWorkspace) -> None: self.workspace = workspace def get_workspace(self, workspace_id: str) -> WorkflowDraftWorkspace: if self.workspace is None or self.workspace.id != workspace_id: raise KeyError(workspace_id) return self.workspace def list_workspaces(self) -> list[WorkflowDraftWorkspace]: return [] if self.workspace is None else [self.workspace] def delete_workspace(self, workspace_id: str) -> bool: if self.workspace is None or self.workspace.id != workspace_id: return False self.workspace = None return True store = ReferenceDraftWorkspaceStore() create_draft_workspace(store, workspace_id="echo_draft", draft=_draft()) replacement = _draft() replacement["name"] = "replacement" replace_validated_draft_document( store, workspace_id="echo_draft", revision=1, draft=replacement, ) replacement["name"] = "mutated_after_save" assert store.get_workspace("echo_draft").draft["name"] == "replacement" def test_replace_draft_workspace_document_revalidates_and_increments_revision( tmp_path, ) -> None: store = FileDraftWorkspaceStore(tmp_path) create_draft_workspace(store, workspace_id="echo_draft", draft=_draft()) replacement = {**_draft(), "name": "replacement"} result = replace_draft_workspace_document( store, workspace_id="echo_draft", revision=1, draft=replacement, node_defs_for_draft=lambda _draft: [], ) assert result["revision"] == 2 assert store.get_workspace("echo_draft").draft["name"] == "replacement" def test_replace_draft_workspace_document_identical_draft_is_a_noop( tmp_path, ) -> None: store = FileDraftWorkspaceStore(tmp_path) create_draft_workspace(store, workspace_id="echo_draft", draft=_draft()) current = store.get_workspace("echo_draft") result = replace_draft_workspace_document( store, workspace_id="echo_draft", revision=1, draft=current.draft, node_defs_for_draft=lambda _draft: [], ) assert result["revision"] == 1 assert store.get_workspace("echo_draft") == current def test_replace_draft_workspace_document_rejects_stale_revision( tmp_path, ) -> None: store = FileDraftWorkspaceStore(tmp_path) create_draft_workspace(store, workspace_id="echo_draft", draft=_draft()) patch_draft_workspace( store, workspace_id="echo_draft", revision=1, patch=[{"op": "replace", "path": "/name", "value": "current"}], ) before = store.get_workspace("echo_draft") result = replace_draft_workspace_document( store, workspace_id="echo_draft", revision=1, draft={**_draft(), "name": "stale"}, node_defs_for_draft=lambda _draft: [], ) assert result["status"] == "conflict" assert result["diagnostics"][0]["code"] == "revision_conflict" assert store.get_workspace("echo_draft") == before def test_replace_draft_workspace_document_rejects_structural_failure( tmp_path, ) -> None: store = FileDraftWorkspaceStore(tmp_path) create_draft_workspace(store, workspace_id="echo_draft", draft=_draft()) before = store.get_workspace("echo_draft") with pytest.raises(ValidationError): replace_draft_workspace_document( store, workspace_id="echo_draft", revision=1, draft={"name": "incomplete"}, node_defs_for_draft=lambda _draft: [], ) assert store.get_workspace("echo_draft") == before def test_replace_draft_workspace_document_persists_fresh_invalid_diagnostics( tmp_path, ) -> None: store = FileDraftWorkspaceStore(tmp_path) create_draft_workspace(store, workspace_id="echo_draft", draft=_draft()) replacement = _draft() replacement["routes"] = {"echo": {"ok": "missing_step"}} result = replace_draft_workspace_document( store, workspace_id="echo_draft", revision=1, draft=replacement, node_defs_for_draft=lambda _draft: [], ) stored = store.get_workspace("echo_draft") assert result["revision"] == 2 assert result["status"] == "invalid" assert result["diagnostics"] assert stored.status == "invalid" assert stored.diagnostics == result["diagnostics"] assert stored.draft["routes"] == replacement["routes"] def test_get_draft_workspace_includes_full_draft_only_when_requested(tmp_path) -> None: store = FileDraftWorkspaceStore(tmp_path) create_draft_workspace(store, workspace_id="echo_draft", draft=_draft()) compact = get_draft_workspace(store, workspace_id="echo_draft") full = get_draft_workspace( store, workspace_id="echo_draft", include_draft=True, ) assert "draft" not in compact assert full["draft"]["steps"]["echo"]["use"] == "demo.echo" def _draft() -> dict[str, Any]: return { "name": "echo", "input_schema": {"type": "object", "properties": {}}, "state_schema": {"fields": {"echoed": {"type": "string"}}}, "output_schema": {"type": "object", "properties": {}}, "start": "echo", "steps": { "echo": { "use": "demo.echo", "input": [], "output": [ { "source": {"root": "local", "parts": ["echoed"]}, "target": {"root": "state", "parts": ["echoed"]}, } ], } }, "routes": {"echo": {"ok": "__end__"}}, }