from __future__ import annotations from pathlib import Path import pytest from examples.browser_click_workflow.ops import ( CollectSnapshotsInput, OpenPageInput, WaitForClickInput, _active_session_count, _collect_snapshots, _get_session, _open_click_page, _wait_for_click, ) from wf_config import load_workflow_config from wf_server.config import build_workflow_server_from_workflow_config def test_browser_click_source_simulates_click_and_cleans_up() -> None: opened = _open_click_page( OpenPageInput(button_label="Launch Workflow", open_browser=False) ) assert opened.before.clicked is False assert opened.before.button_text == "Launch Workflow" assert opened.before.status_text == "Waiting for click" clicked = _wait_for_click( WaitForClickInput( session_id=opened.session_id, simulate=True, timeout_seconds=2 ) ) result = _collect_snapshots( CollectSnapshotsInput( session_id=opened.session_id, before=opened.before, after=clicked.after, ) ) assert result.before.clicked is False assert result.after.clicked is True assert result.after.status_text == "Button clicked" assert result.closed is True assert _active_session_count() == 0 def test_browser_click_source_human_timeout_cleans_up() -> None: opened = _open_click_page(OpenPageInput(open_browser=False)) with pytest.raises(TimeoutError, match="timed out waiting for click"): _wait_for_click( WaitForClickInput( session_id=opened.session_id, simulate=False, timeout_seconds=0.01, ) ) _collect_snapshots( CollectSnapshotsInput( session_id=opened.session_id, before=opened.before, after=opened.before, ) ) assert _active_session_count() == 0 def test_browser_click_session_events_are_bounded() -> None: opened = _open_click_page(OpenPageInput(open_browser=False)) try: session = _get_session(opened.session_id) for index in range(20): session.add_event(f"event-{index}") snapshot = session.snapshot() assert len(snapshot.events) == 10 assert snapshot.events == [f"event-{index}" for index in range(10, 20)] finally: _collect_snapshots( CollectSnapshotsInput( session_id=opened.session_id, before=opened.before, after=opened.before, ) ) def test_browser_click_open_browser_failure_does_not_fail_workflow(monkeypatch) -> None: def fail_open(_url: str) -> bool: raise RuntimeError("no browser") monkeypatch.setattr( "examples.browser_click_workflow.ops.webbrowser.open", fail_open ) opened = _open_click_page(OpenPageInput(open_browser=True)) try: assert opened.url.startswith("http://127.0.0.1:") assert opened.before.clicked is False finally: _collect_snapshots( CollectSnapshotsInput( session_id=opened.session_id, before=opened.before, after=opened.before, ) ) EXAMPLE_DIR = ( Path(__file__).resolve().parents[2] / "examples" / "browser_click_workflow" ) @pytest.mark.asyncio async def test_browser_click_workflow_artifact_deployment_run_path(tmp_path) -> None: config = load_workflow_config(EXAMPLE_DIR / "wf.config.json") config.server.store.root = tmp_path / "store" server = build_workflow_server_from_workflow_config(config) plan = { "name": "browser_click_case_study", "input_schema": { "type": "object", "properties": { "button_label": {"type": "string"}, "open_browser": {"type": "boolean"}, "simulate": {"type": "boolean"}, "timeout_seconds": {"type": "number"}, }, "required": ["button_label", "open_browser", "simulate", "timeout_seconds"], }, "state_schema": { "type": "object", "properties": { "opened": {"type": "object", "reducer": "wf.std.replace"}, "clicked": {"type": "object", "reducer": "wf.std.replace"}, "result": {"type": "object", "reducer": "wf.std.replace"}, }, }, "output_schema": { "type": "object", "properties": { "before": {"type": "object"}, "after": {"type": "object"}, "closed": {"type": "boolean"}, }, "required": ["before", "after", "closed"], }, "outcomes": ["ok"], "start": "open", "nodes": [ { "id": "open", "type": "node", "node": "local.browser_click.open_click_page", "input": [ { "path": {"root": "input", "parts": ["button_label"]}, "target": {"root": "local", "parts": ["button_label"]}, }, { "path": {"root": "input", "parts": ["open_browser"]}, "target": {"root": "local", "parts": ["open_browser"]}, }, ], "output": [ { "source": {"root": "local", "parts": []}, "target": {"root": "state", "parts": ["opened"]}, } ], }, { "id": "wait", "type": "node", "node": "local.browser_click.wait_for_click", "input": [ { "path": {"root": "state", "parts": ["opened", "session_id"]}, "target": {"root": "local", "parts": ["session_id"]}, }, { "path": {"root": "input", "parts": ["simulate"]}, "target": {"root": "local", "parts": ["simulate"]}, }, { "path": {"root": "input", "parts": ["timeout_seconds"]}, "target": {"root": "local", "parts": ["timeout_seconds"]}, }, ], "output": [ { "source": {"root": "local", "parts": []}, "target": {"root": "state", "parts": ["clicked"]}, } ], }, { "id": "collect", "type": "node", "node": "local.browser_click.collect_snapshots", "input": [ { "path": {"root": "state", "parts": ["opened", "session_id"]}, "target": {"root": "local", "parts": ["session_id"]}, }, { "path": {"root": "state", "parts": ["opened", "before"]}, "target": {"root": "local", "parts": ["before"]}, }, { "path": {"root": "state", "parts": ["clicked", "after"]}, "target": {"root": "local", "parts": ["after"]}, }, ], "output": [ { "source": {"root": "local", "parts": []}, "target": {"root": "state", "parts": ["result"]}, } ], }, ], "edges": [ {"from": "open", "outcome": "ok", "to": "wait"}, {"from": "wait", "outcome": "ok", "to": "collect"}, {"from": "collect", "outcome": "ok", "to": "__end__"}, ], "output": [ { "path": {"root": "state", "parts": ["result", "before"]}, "target": {"root": "local", "parts": ["before"]}, }, { "path": {"root": "state", "parts": ["result", "after"]}, "target": {"root": "local", "parts": ["after"]}, }, { "path": {"root": "state", "parts": ["result", "closed"]}, "target": {"root": "local", "parts": ["closed"]}, }, ], } await server.api.create_artifact_from_plan( artifact_id="browser_click_case_study", version=1, title="Browser Click Case Study", plan=plan, outcomes=["ok"], source_bindings={"local.browser_click": "local.browser_click"}, ) await server.api.save_deployment( { "id": "browser_click_case_study.default", "artifact_id": "browser_click_case_study", "artifact_version": 1, "bindings": {"local.browser_click": "local.browser_click"}, } ) run = await server.api.run_deployment( deployment_id="browser_click_case_study.default", workflow_input={ "button_label": "Launch Workflow", "open_browser": False, "simulate": True, "timeout_seconds": 2, }, ) assert run["status"] == "completed" assert run["output"]["before"]["clicked"] is False assert run["output"]["after"]["clicked"] is True assert run["output"]["after"]["status_text"] == "Button clicked" assert run["output"]["closed"] is True assert run["trace_count"] >= 3