diff --git a/CHANGELOG.md b/CHANGELOG.md index 97cd80fea..5d2eb97cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -128,6 +128,15 @@ including `plan_id`, `phase`, `state`, `action`, `projects`, and `arguments` fields. (`robot/wf07_cicd_integration.robot`, `robot/helper_wf07_cicd.py`) (#771) +- Added integration Robot Framework test for Specification Workflow Example 14: + Server Mode — Team Collaboration. Exercises server mode configuration, + config-registry diagnostics, namespace management, action publishing with + namespaced actor references and supervised profile metadata, shared action + consumption via `use_action()` with required arguments and project links, + and namespace + phase plan monitoring using mocked LLM providers and + in-memory domain services (`CLEVERAGENTS_TESTING_USE_MOCK_AI=true`). + (`robot/wf14_server_mode_integration.robot`, + `robot/helper_wf14_server_mode.py`) (#778) - Added volatile in-memory `audit_log` to `ReactiveEventBus` — every emitted `DomainEvent` is appended to a volatile in-memory log accessible via the `audit_log` property (defensive copy). Emit ordering now follows the diff --git a/robot/helper_wf14_server_mode.py b/robot/helper_wf14_server_mode.py new file mode 100644 index 000000000..6ba61656b --- /dev/null +++ b/robot/helper_wf14_server_mode.py @@ -0,0 +1,603 @@ +"""Helper script for wf14_server_mode_integration.robot tests. + +Each subcommand is a self-contained check that prints a sentinel on success. +Validates the server mode team collaboration workflow (Specification +Example 14) with mocked LLM providers and in-memory domain services. +""" + +from __future__ import annotations + +import logging +import os +import sys +import tempfile +from collections.abc import Callable +from pathlib import Path + +# Keep helper stderr focused on assertion failures and tracebacks. +logging.disable(logging.CRITICAL) + +# Ensure local source tree is importable +_SRC = str(Path(__file__).resolve().parents[1] / "src") +if _SRC not in sys.path: + sys.path.insert(0, _SRC) + +from cleveragents.application.services.config_service import ( # noqa: E402 + _REGISTRY, + ConfigLevel, + ConfigService, +) +from cleveragents.application.services.plan_lifecycle_service import ( # noqa: E402 + PlanLifecycleService, +) +from cleveragents.config.settings import Settings # noqa: E402 +from cleveragents.domain.models.core.action import ( # noqa: E402 + ActionArgument, + ActionState, +) +from cleveragents.domain.models.core.plan import ( # noqa: E402 + PlanPhase, + ProcessingState, + ProjectLink, +) + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _make_config_service(tmpdir: str) -> ConfigService: + """Create a ConfigService backed by a temporary directory.""" + config_dir = Path(tmpdir) + config_path = config_dir / "config.toml" + return ConfigService(config_dir=config_dir, config_path=config_path) + + +_WF14_CONFIG_ENV_OVERRIDES: tuple[str, ...] = ( + "CLEVERAGENTS_SERVER_URL", + "CLEVERAGENTS_SERVER_TOKEN", + "CLEVERAGENTS_SERVER_SYNC_AUTO", + "CLEVERAGENTS_SERVER_SYNC_INTERVAL", + "CLEVERAGENTS_SERVER_TLS_VERIFY", + "CLEVERAGENTS_SERVER_NAMESPACE", + "CLEVERAGENTS_CORE_NAMESPACE", + "CLEVERAGENTS_NAMESPACE", + "CLEVERAGENTS_CORE_AUTOMATION_PROFILE", + "CLEVERAGENTS_AUTOMATION_PROFILE", +) + + +def _clear_config_env_overrides() -> None: + """Remove env vars that can override config resolution in tests.""" + for env_name in _WF14_CONFIG_ENV_OVERRIDES: + os.environ.pop(env_name, None) + + +# --------------------------------------------------------------------------- +# Subcommands +# --------------------------------------------------------------------------- + + +def config_server_mode() -> None: + """Set and verify server config values using ConfigService.""" + _clear_config_env_overrides() + + with tempfile.TemporaryDirectory() as tmpdir: + svc = _make_config_service(tmpdir) + + # Set server mode config values + svc.set_value("server.url", "https://agents.example.com") + svc.set_value("server.token", "tok_wf14_test_abc123") + svc.set_value("core.namespace", "team-alpha") + + # Read back and verify + data = svc.read_config() + assert data["server.url"] == "https://agents.example.com", ( + "Expected server.url='https://agents.example.com', " + f"got {data.get('server.url')}" + ) + assert data["server.token"] == "tok_wf14_test_abc123", ( + "Expected server.token='tok_wf14_test_abc123', " + f"got {data.get('server.token')}" + ) + assert data["core.namespace"] == "team-alpha", ( + f"Expected core.namespace='team-alpha', got {data.get('core.namespace')}" + ) + + # Verify resolution chain returns correct values + resolved_url = svc.resolve("server.url") + assert resolved_url.value == "https://agents.example.com", ( + "Expected resolve(server.url) to return configured URL, " + f"got {resolved_url.value!r}" + ) + assert resolved_url.source == ConfigLevel.GLOBAL, ( + "Expected resolve(server.url) source=global, " + f"got {resolved_url.source.value}" + ) + + resolved_token = svc.resolve("server.token") + assert resolved_token.value == "tok_wf14_test_abc123", ( + "Expected resolve(server.token) to return configured token, " + f"got {resolved_token.value!r}" + ) + assert resolved_token.source == ConfigLevel.GLOBAL, ( + "Expected resolve(server.token) source=global, " + f"got {resolved_token.source.value}" + ) + + resolved_ns = svc.resolve("core.namespace") + assert resolved_ns.value == "team-alpha", ( + "Expected resolve(core.namespace) to return configured namespace, " + f"got {resolved_ns.value!r}" + ) + assert resolved_ns.source == ConfigLevel.GLOBAL, ( + "Expected resolve(core.namespace) source=global, " + f"got {resolved_ns.source.value}" + ) + + print("wf14-config-server-mode-ok") + + +def diagnostics() -> None: + """Verify config registry contains required server-mode keys and defaults. + + Checks that all server-mode configuration keys are registered and that + their default values match the specification. Server connectivity and + database checks are not exercised here because no real server or + database is available in the integration test environment. + """ + _clear_config_env_overrides() + + # Verify config registry has required server keys + required_keys = [ + "server.url", + "server.token", + "server.sync.auto", + "server.sync.interval", + "server.tls-verify", + "server.namespace", + "core.namespace", + "core.automation-profile", + ] + for key in required_keys: + assert key in _REGISTRY, f"Missing config key: {key}" + + # Verify config service can be instantiated with temp directory + with tempfile.TemporaryDirectory() as tmpdir: + svc = _make_config_service(tmpdir) + + # Verify basic config reads succeed (empty config initially) + data = svc.read_config() + assert len(data) == 0, ( + "Expected empty initial config in temporary directory, " + f"got keys: {sorted(data.keys())}" + ) + + # Verify default values are accessible via resolution + resolved = svc.resolve("server.sync.auto") + assert resolved.value is True, f"Expected True, got {resolved.value}" + assert resolved.source == ConfigLevel.DEFAULT, ( + f"Expected server.sync.auto source=default, got {resolved.source.value}" + ) + + resolved_interval = svc.resolve("server.sync.interval") + assert resolved_interval.value == 300, ( + f"Expected 300, got {resolved_interval.value}" + ) + assert resolved_interval.source == ConfigLevel.DEFAULT, ( + "Expected server.sync.interval source=default, " + f"got {resolved_interval.source.value}" + ) + + resolved_tls = svc.resolve("server.tls-verify") + assert resolved_tls.value is True, f"Expected True, got {resolved_tls.value}" + assert resolved_tls.source == ConfigLevel.DEFAULT, ( + "Expected server.tls-verify source=default, " + f"got {resolved_tls.source.value}" + ) + + # Verify core.namespace default is 'local' + resolved_ns = svc.resolve("core.namespace") + assert resolved_ns.value == "local", ( + f"Expected 'local', got {resolved_ns.value}" + ) + assert resolved_ns.source == ConfigLevel.DEFAULT, ( + f"Expected core.namespace source=default, got {resolved_ns.source.value}" + ) + + # Verify automation-profile default is 'supervised' + resolved_profile = svc.resolve("core.automation-profile") + assert resolved_profile.value == "supervised", ( + f"Expected 'supervised', got {resolved_profile.value}" + ) + assert resolved_profile.source == ConfigLevel.DEFAULT, ( + "Expected core.automation-profile source=default, " + f"got {resolved_profile.source.value}" + ) + + # Server connectivity cannot be tested (no real server) -- verify + # that the server.url default is None (graceful handling) + resolved_url = svc.resolve("server.url") + assert resolved_url.value is None, ( + f"Expected None default for server.url, got {resolved_url.value}" + ) + assert resolved_url.source == ConfigLevel.DEFAULT, ( + f"Expected server.url source=default, got {resolved_url.source.value}" + ) + + print("wf14-diagnostics-ok") + + +def action_namespace() -> None: + """Create and list namespaced actions using PlanLifecycleService.""" + settings = Settings() + lifecycle = PlanLifecycleService(settings=settings) + definition_of_done = "Service is running and health checks pass" + + # Create an action with a namespace prefix + action = lifecycle.create_action( + name="team-alpha/deploy-service", + description="Deploy a microservice to staging", + definition_of_done=definition_of_done, + strategy_actor="openai/gpt-4", + execution_actor="openai/gpt-4", + automation_profile="supervised", + tags=["deploy", "team-alpha"], + ) + + assert str(action.namespaced_name) == "team-alpha/deploy-service", ( + f"Expected 'team-alpha/deploy-service', got '{action.namespaced_name}'" + ) + assert action.state == ActionState.AVAILABLE, "Expected action state AVAILABLE" + assert action.reusable is True, "Expected action.reusable to default to True" + assert action.read_only is False, "Expected action.read_only to default to False" + assert action.automation_profile == "supervised", ( + "Expected action automation_profile='supervised', " + f"got {action.automation_profile!r}" + ) + assert action.definition_of_done == definition_of_done, ( + "Expected definition_of_done to match the configured value" + ) + assert action.tags == ["deploy", "team-alpha"], ( + f"Expected action tags ['deploy', 'team-alpha'], got {action.tags!r}" + ) + + # List actions and verify the namespaced action appears + actions = lifecycle.list_actions() + assert len(actions) == 1, f"Expected 1 action, got {len(actions)}" + action_names = [str(a.namespaced_name) for a in actions] + assert "team-alpha/deploy-service" in action_names, ( + f"'team-alpha/deploy-service' not in {action_names}" + ) + + print("wf14-action-namespace-ok") + + +def action_actor_refs() -> None: + """Create an action with namespaced actor references and verify fields. + + Validates that actions can reference actors from different namespaces + (strategy, execution, review) and that those references are preserved + when the action is retrieved. + """ + settings = Settings() + lifecycle = PlanLifecycleService(settings=settings) + + # Create an action that references namespaced actors + action = lifecycle.create_action( + name="team-alpha/analyze-code", + description="Analyze codebase for improvements", + definition_of_done="Analysis report generated", + strategy_actor="openai/gpt-4", + execution_actor="anthropic/claude-sonnet", + review_actor="openai/gpt-4", + automation_profile="supervised", + ) + + assert action.strategy_actor == "openai/gpt-4", ( + f"Expected strategy_actor 'openai/gpt-4', got {action.strategy_actor!r}" + ) + assert action.execution_actor == "anthropic/claude-sonnet", ( + "Expected execution_actor 'anthropic/claude-sonnet', " + f"got {action.execution_actor!r}" + ) + assert action.review_actor == "openai/gpt-4", ( + f"Expected review_actor 'openai/gpt-4', got {action.review_actor!r}" + ) + assert action.automation_profile == "supervised", ( + "Expected action automation_profile='supervised', " + f"got {action.automation_profile!r}" + ) + + # Verify the action is accessible + fetched = lifecycle.get_action("team-alpha/analyze-code") + assert str(fetched.namespaced_name) == "team-alpha/analyze-code", ( + "Expected fetched action name 'team-alpha/analyze-code', " + f"got {fetched.namespaced_name!s}" + ) + assert fetched.strategy_actor == "openai/gpt-4", ( + "Expected fetched strategy_actor 'openai/gpt-4', " + f"got {fetched.strategy_actor!r}" + ) + assert fetched.execution_actor == "anthropic/claude-sonnet", ( + "Expected fetched execution_actor 'anthropic/claude-sonnet', " + f"got {fetched.execution_actor!r}" + ) + assert fetched.review_actor == "openai/gpt-4", ( + f"Expected fetched review_actor 'openai/gpt-4', got {fetched.review_actor!r}" + ) + + print("wf14-action-actor-refs-ok") + + +def shared_actions() -> None: + """List actions from a specific namespace.""" + settings = Settings() + lifecycle = PlanLifecycleService(settings=settings) + + # Create actions in different namespaces + lifecycle.create_action( + name="team-alpha/shared-lint", + description="Lint code with team standards", + definition_of_done="All lint checks pass", + strategy_actor="openai/gpt-4", + execution_actor="openai/gpt-4", + automation_profile="supervised", + ) + lifecycle.create_action( + name="team-beta/shared-test", + description="Run integration test suite", + definition_of_done="All tests pass", + strategy_actor="openai/gpt-4", + execution_actor="openai/gpt-4", + automation_profile="supervised", + ) + lifecycle.create_action( + name="team-alpha/shared-review", + description="Code review with team conventions", + definition_of_done="Review complete with no blockers", + strategy_actor="openai/gpt-4", + execution_actor="openai/gpt-4", + automation_profile="supervised", + ) + + # Verify total unfiltered count + all_actions = lifecycle.list_actions() + assert len(all_actions) == 3, f"Expected 3 total actions, got {len(all_actions)}" + assert all(a.state == ActionState.AVAILABLE for a in all_actions), ( + "Expected all actions to be AVAILABLE" + ) + + # List only team-alpha namespace + alpha_actions = lifecycle.list_actions(namespace="team-alpha") + assert len(alpha_actions) == 2, ( + f"Expected 2 team-alpha actions, got {len(alpha_actions)}" + ) + alpha_names = [str(a.namespaced_name) for a in alpha_actions] + assert "team-alpha/shared-lint" in alpha_names, ( + f"'team-alpha/shared-lint' not in {alpha_names}" + ) + assert "team-alpha/shared-review" in alpha_names, ( + f"'team-alpha/shared-review' not in {alpha_names}" + ) + # Ensure only team-alpha namespace values are returned. + assert all(n.startswith("team-alpha/") for n in alpha_names), ( + f"Expected only team-alpha names, got {alpha_names}" + ) + + # List team-beta namespace + beta_actions = lifecycle.list_actions(namespace="team-beta") + beta_names = [str(a.namespaced_name) for a in beta_actions] + assert "team-beta/shared-test" in beta_names, ( + f"'team-beta/shared-test' not in {beta_names}" + ) + assert len(beta_actions) == 1, f"Expected 1 beta action, got {len(beta_actions)}" + + print("wf14-shared-actions-ok") + + +def use_shared_action() -> None: + """Use a shared action to create a plan and verify plan attributes. + + Covers Specification Example 14, Step 3: a developer uses a shared + action published by another team member. Verifies that the resulting + plan inherits the action's namespace and enters the expected initial + phase and processing state. + """ + settings = Settings() + lifecycle = PlanLifecycleService(settings=settings) + + # Publish an action (simulating another team member) + target_module_arg = ActionArgument.parse( + "target_module:string:required:Module path to generate tests" + ) + published = lifecycle.create_action( + name="team-alpha/generate-tests", + description="Generate tests for a target module", + definition_of_done="All target modules have test coverage above 80%", + strategy_actor="openai/gpt-4", + execution_actor="openai/gpt-4", + automation_profile="supervised", + arguments=[target_module_arg], + tags=["wf14", "shared"], + ) + assert published.automation_profile == "supervised", ( + "Expected published action automation_profile='supervised', " + f"got {published.automation_profile!r}" + ) + + # Use the shared action (simulating a second developer) + plan = lifecycle.use_action( + action_name="team-alpha/generate-tests", + project_links=[ProjectLink(project_name="local/payment-service")], + arguments={"target_module": "src/payments"}, + ) + + # Verify plan attributes + assert plan.namespaced_name.namespace == "team-alpha", ( + f"Expected namespace 'team-alpha', got '{plan.namespaced_name.namespace}'" + ) + assert plan.phase == PlanPhase.STRATEGIZE, ( + f"Expected phase STRATEGIZE, got {plan.phase}" + ) + assert plan.processing_state == ProcessingState.QUEUED, ( + f"Expected state QUEUED, got {plan.processing_state}" + ) + assert plan.action_name == "team-alpha/generate-tests", ( + f"Expected action_name 'team-alpha/generate-tests', got '{plan.action_name}'" + ) + assert plan.strategy_actor == "openai/gpt-4", ( + f"Expected strategy_actor 'openai/gpt-4', got {plan.strategy_actor!r}" + ) + assert plan.execution_actor == "openai/gpt-4", ( + f"Expected execution_actor 'openai/gpt-4', got {plan.execution_actor!r}" + ) + assert plan.arguments == {"target_module": "src/payments"}, ( + "Expected plan arguments {'target_module': 'src/payments'}, " + f"got {plan.arguments!r}" + ) + assert plan.arguments_order == ["target_module"], ( + f"Expected arguments_order ['target_module'], got {plan.arguments_order!r}" + ) + assert len(plan.project_links) == 1, ( + f"Expected 1 project link, got {len(plan.project_links)}" + ) + assert plan.project_links[0].project_name == "local/payment-service", ( + "Expected project link to local/payment-service, " + f"got {plan.project_links[0].project_name!r}" + ) + assert plan.definition_of_done == published.definition_of_done, ( + "Expected plan definition_of_done to match published action" + ) + assert plan.reusable is True, "Expected plan.reusable=True from action default" + assert plan.read_only is False, "Expected plan.read_only=False from action default" + assert plan.tags == ["wf14", "shared"], ( + f"Expected plan tags ['wf14', 'shared'], got {plan.tags!r}" + ) + + print("wf14-use-shared-action-ok") + + +def plan_namespace() -> None: + """Create plans and verify namespace-filtered listing. + + Covers Specification Example 14, Step 4: monitoring plans across the + team by listing plans filtered by namespace. Creates actions in two + namespaces, uses them to instantiate plans, and verifies that namespace + filtering returns only the expected plans. + """ + settings = Settings() + lifecycle = PlanLifecycleService(settings=settings) + + # Create and use actions in team-alpha namespace + lifecycle.create_action( + name="team-alpha/monitor-lint", + description="Lint code with team standards", + definition_of_done="All lint checks pass", + strategy_actor="openai/gpt-4", + execution_actor="openai/gpt-4", + automation_profile="supervised", + ) + lifecycle.create_action( + name="team-beta/monitor-test", + description="Run integration test suite", + definition_of_done="All tests pass", + strategy_actor="openai/gpt-4", + execution_actor="openai/gpt-4", + automation_profile="supervised", + ) + + alpha_plan = lifecycle.use_action(action_name="team-alpha/monitor-lint") + beta_plan = lifecycle.use_action(action_name="team-beta/monitor-test") + + # Move one plan to Execute to validate phase-aware monitoring. + lifecycle.start_strategize(beta_plan.identity.plan_id) + lifecycle.complete_strategize(beta_plan.identity.plan_id) + beta_plan = lifecycle.execute_plan(beta_plan.identity.plan_id) + + # List all plans + all_plans = lifecycle.list_plans() + assert len(all_plans) == 2, f"Expected 2 plans total, got {len(all_plans)}" + + # List plans filtered to team-alpha namespace + alpha_plans = lifecycle.list_plans(namespace="team-alpha") + assert len(alpha_plans) == 1, f"Expected 1 team-alpha plan, got {len(alpha_plans)}" + assert alpha_plans[0].identity.plan_id == alpha_plan.identity.plan_id, ( + "team-alpha plan ID mismatch" + ) + assert alpha_plans[0].phase == PlanPhase.STRATEGIZE, ( + f"Expected team-alpha phase STRATEGIZE, got {alpha_plans[0].phase}" + ) + assert alpha_plans[0].processing_state == ProcessingState.QUEUED, ( + "Expected team-alpha processing state QUEUED, " + f"got {alpha_plans[0].processing_state}" + ) + assert alpha_plans[0].action_name == "team-alpha/monitor-lint", ( + "Expected team-alpha action_name 'team-alpha/monitor-lint', " + f"got {alpha_plans[0].action_name!r}" + ) + + # List plans filtered to team-beta namespace + beta_plans = lifecycle.list_plans(namespace="team-beta") + assert len(beta_plans) == 1, f"Expected 1 team-beta plan, got {len(beta_plans)}" + assert beta_plans[0].identity.plan_id == beta_plan.identity.plan_id, ( + "team-beta plan ID mismatch" + ) + assert beta_plans[0].phase == PlanPhase.EXECUTE, ( + f"Expected team-beta phase EXECUTE, got {beta_plans[0].phase}" + ) + assert beta_plans[0].processing_state == ProcessingState.QUEUED, ( + "Expected team-beta processing state QUEUED after execute transition, " + f"got {beta_plans[0].processing_state}" + ) + assert beta_plans[0].action_name == "team-beta/monitor-test", ( + "Expected team-beta action_name 'team-beta/monitor-test', " + f"got {beta_plans[0].action_name!r}" + ) + + # Verify phase filtering within namespaces. + alpha_strategize = lifecycle.list_plans( + namespace="team-alpha", phase=PlanPhase.STRATEGIZE + ) + assert len(alpha_strategize) == 1, ( + f"Expected 1 team-alpha strategize plan, got {len(alpha_strategize)}" + ) + assert alpha_strategize[0].identity.plan_id == alpha_plan.identity.plan_id, ( + "team-alpha strategize phase filter returned wrong plan" + ) + + beta_execute = lifecycle.list_plans(namespace="team-beta", phase=PlanPhase.EXECUTE) + assert len(beta_execute) == 1, ( + f"Expected 1 team-beta execute plan, got {len(beta_execute)}" + ) + assert beta_execute[0].identity.plan_id == beta_plan.identity.plan_id, ( + "team-beta execute phase filter returned wrong plan" + ) + + # Verify cross-namespace isolation + gamma_plans = lifecycle.list_plans(namespace="team-gamma") + assert len(gamma_plans) == 0, f"Expected 0 team-gamma plans, got {len(gamma_plans)}" + + print("wf14-plan-namespace-ok") + + +# --------------------------------------------------------------------------- +# Dispatcher +# --------------------------------------------------------------------------- + +_COMMANDS: dict[str, Callable[[], None]] = { + "config-server-mode": config_server_mode, + "diagnostics": diagnostics, + "action-namespace": action_namespace, + "action-actor-refs": action_actor_refs, + "shared-actions": shared_actions, + "use-shared-action": use_shared_action, + "plan-namespace": plan_namespace, +} + +if __name__ == "__main__": + if len(sys.argv) < 2 or sys.argv[1] not in _COMMANDS: + print(f"Usage: {sys.argv[0]} <{'|'.join(_COMMANDS)}>", file=sys.stderr) + sys.exit(2) + fn = _COMMANDS[sys.argv[1]] + fn() diff --git a/robot/wf14_server_mode_integration.robot b/robot/wf14_server_mode_integration.robot new file mode 100644 index 000000000..86ba66903 --- /dev/null +++ b/robot/wf14_server_mode_integration.robot @@ -0,0 +1,82 @@ +*** Settings *** +Documentation Integration test for Workflow Example 14: server mode team collaboration +... Validates server mode configuration, diagnostics, namespace management, +... action publishing to team namespaces, and plan monitoring across namespaces. +... Uses the supervised automation profile as required by the specification. +Resource ${CURDIR}/common.resource +Suite Setup Setup Test Environment +Suite Teardown Cleanup Test Environment + +*** Variables *** +${HELPER} ${CURDIR}/helper_wf14_server_mode.py + +*** Test Cases *** +WF14 Server Mode Config + [Documentation] Set server.url, server.token, core.namespace config values and verify readback + [Tags] integration server-mode wf14 config + ${result}= Run Process ${PYTHON} ${HELPER} config-server-mode cwd=${WORKSPACE} timeout=30s + Log ${result.stdout} + Log ${result.stderr} + Should Be Equal As Integers ${result.rc} 0 + Should Not Contain ${result.stdout}${result.stderr} Traceback + Should Contain ${result.stdout} wf14-config-server-mode-ok + +WF14 Diagnostics + [Documentation] Verify config registry contains required server-mode keys and that default values match the specification + [Tags] integration server-mode wf14 diagnostics + ${result}= Run Process ${PYTHON} ${HELPER} diagnostics cwd=${WORKSPACE} timeout=30s + Log ${result.stdout} + Log ${result.stderr} + Should Be Equal As Integers ${result.rc} 0 + Should Not Contain ${result.stdout}${result.stderr} Traceback + Should Contain ${result.stdout} wf14-diagnostics-ok + +WF14 Action Publish To Namespace + [Documentation] Create a namespaced action and verify listing plus supervised-profile metadata + [Tags] integration server-mode wf14 action namespace publish + ${result}= Run Process ${PYTHON} ${HELPER} action-namespace cwd=${WORKSPACE} timeout=30s + Log ${result.stdout} + Log ${result.stderr} + Should Be Equal As Integers ${result.rc} 0 + Should Not Contain ${result.stdout}${result.stderr} Traceback + Should Contain ${result.stdout} wf14-action-namespace-ok + +WF14 Action With Namespaced Actor References + [Documentation] Create an action referencing actors from different namespaces and verify fields are preserved + [Tags] integration server-mode wf14 action actor + ${result}= Run Process ${PYTHON} ${HELPER} action-actor-refs cwd=${WORKSPACE} timeout=30s + Log ${result.stdout} + Log ${result.stderr} + Should Be Equal As Integers ${result.rc} 0 + Should Not Contain ${result.stdout}${result.stderr} Traceback + Should Contain ${result.stdout} wf14-action-actor-refs-ok + +WF14 Shared Action From Namespace + [Documentation] List actions from a specific namespace and verify shared actions are visible + [Tags] integration server-mode wf14 action namespace shared + ${result}= Run Process ${PYTHON} ${HELPER} shared-actions cwd=${WORKSPACE} timeout=30s + Log ${result.stdout} + Log ${result.stderr} + Should Be Equal As Integers ${result.rc} 0 + Should Not Contain ${result.stdout}${result.stderr} Traceback + Should Contain ${result.stdout} wf14-shared-actions-ok + +WF14 Use Shared Action + [Documentation] Use a shared action with required args/project link and verify inherited actors plus initial phase + [Tags] integration server-mode wf14 plan use-action + ${result}= Run Process ${PYTHON} ${HELPER} use-shared-action cwd=${WORKSPACE} timeout=30s + Log ${result.stdout} + Log ${result.stderr} + Should Be Equal As Integers ${result.rc} 0 + Should Not Contain ${result.stdout}${result.stderr} Traceback + Should Contain ${result.stdout} wf14-use-shared-action-ok + +WF14 Plan List Across Namespace + [Documentation] Create plans from namespaced actions and verify namespace plus phase-filtered monitoring + [Tags] integration server-mode wf14 plan namespace monitoring + ${result}= Run Process ${PYTHON} ${HELPER} plan-namespace cwd=${WORKSPACE} timeout=30s + Log ${result.stdout} + Log ${result.stderr} + Should Be Equal As Integers ${result.rc} 0 + Should Not Contain ${result.stdout}${result.stderr} Traceback + Should Contain ${result.stdout} wf14-plan-namespace-ok