80f1664a0d
CI / benchmark-publish (pull_request) Has been skipped
CI / build (pull_request) Successful in 23s
CI / lint (pull_request) Successful in 3m20s
CI / typecheck (pull_request) Successful in 3m54s
CI / security (pull_request) Successful in 4m13s
CI / quality (pull_request) Successful in 4m17s
CI / integration_tests (pull_request) Successful in 9m21s
CI / unit_tests (pull_request) Successful in 9m42s
CI / docker (pull_request) Successful in 14s
CI / e2e_tests (pull_request) Successful in 12m1s
CI / coverage (pull_request) Successful in 11m41s
CI / status-check (pull_request) Successful in 1s
CI / build (push) Successful in 26s
CI / lint (push) Successful in 3m19s
CI / quality (push) Successful in 3m41s
CI / typecheck (push) Successful in 3m56s
CI / benchmark-regression (push) Has been skipped
CI / security (push) Successful in 4m2s
CI / integration_tests (push) Successful in 9m3s
CI / unit_tests (push) Successful in 9m24s
CI / docker (push) Successful in 1m9s
CI / e2e_tests (push) Successful in 13m51s
CI / coverage (push) Successful in 11m25s
CI / status-check (push) Successful in 1s
CI / benchmark-publish (push) Failing after 24m52s
CI / benchmark-regression (pull_request) Failing after 37m6s
Implemented Robot Framework integration test suite validating the CI/CD workflow (Specification Example 7). Tests cover ci-profile configuration (automation-profile, format, log level), idempotent resource and project registration with duplicate-detection assertions, three validation tools (ci-lint, ci-typecheck, ci-tests) registration and resource attachment via ToolRegistryService, action creation with typed arguments and invariants per spec Step 2, plan lifecycle with phase-by-phase completion through all phases (strategize, execute, apply) until terminal applied state, and JSON output structure verification including plan_id, phase, state, action, projects, and arguments fields. Post-review fixes applied (round 1): - H1: Fix truncated invariant #2 text to match spec line 39051 - H2: Fix definition_of_done to match spec's 5-bullet-point format - M1: Register all 3 spec validations (ci-lint, ci-typecheck, ci-tests) - M3: Add branch property to resource registration per spec Step 3 - M4: Replace phantom resource_id with real registered resource - M5: Add projects and arguments field assertions to JSON output test - M7: Replace 'polling-based' with 'phase-by-phase' in docs/comments - L1: Use timezone-aware datetime (timezone.utc) - L2: Use tempfile for resource path instead of hardcoded /tmp - L3: Clarify json_output() docstring to reflect as_cli_dict() scope - L4: Tighten attachment count assertion from >= 1 to == 1 (now == 3) Post-review fixes applied (round 2): - M2: Use Setup Test Environment With Database Isolation for pabot safety - L1: Replace remaining hardcoded /tmp paths with tempfile (validation_attach, resource_idempotent duplicate registration) - L2: Fix stale 'Polling-based' comment missed in round 1 - L5: Align action description with spec line 39027 - M1/L3/L6: Document automation_profile propagation gap in use_action() with TODO for when production code wires the field onto Plan - L4: Add comment explaining local actor name deviation from spec Includes CHANGELOG update describing the integration test scope. ISSUES CLOSED: #771
189 lines
5.9 KiB
Python
189 lines
5.9 KiB
Python
"""Shared utilities for M1-M6 E2E verification helpers.
|
|
|
|
Provides subprocess-based CLI invocation, workspace setup, and
|
|
common assertions for real (non-mocked) end-to-end tests.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import contextlib
|
|
import json
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
|
|
def run_cli(
|
|
*args: str,
|
|
workspace: str,
|
|
env_extra: dict[str, str] | None = None,
|
|
timeout: int = 120,
|
|
) -> subprocess.CompletedProcess[str]:
|
|
"""Run ``python -m cleveragents <args>`` in a subprocess.
|
|
|
|
Uses the same Python interpreter as the running process.
|
|
Inherits ``CLEVERAGENTS_HOME``, ``CLEVERAGENTS_AUTO_APPLY_MIGRATIONS``,
|
|
and ``CLEVERAGENTS_TESTING_USE_MOCK_AI`` from the environment (set by
|
|
``robot/common.resource`` ``Setup Test Environment``).
|
|
|
|
Parameters
|
|
----------
|
|
*args:
|
|
CLI arguments (e.g. ``"action", "create", "--config", path``).
|
|
workspace:
|
|
Working directory for the subprocess.
|
|
env_extra:
|
|
Additional environment variables to merge.
|
|
timeout:
|
|
Subprocess timeout in seconds.
|
|
"""
|
|
env = os.environ.copy()
|
|
# Ensure test-critical variables are set
|
|
env.setdefault("CLEVERAGENTS_AUTO_APPLY_MIGRATIONS", "true")
|
|
env.setdefault("CLEVERAGENTS_TESTING_USE_MOCK_AI", "true")
|
|
env.setdefault("NO_COLOR", "1")
|
|
if env_extra:
|
|
env.update(env_extra)
|
|
return subprocess.run(
|
|
[sys.executable, "-m", "cleveragents", *args],
|
|
cwd=workspace,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=timeout,
|
|
env=env,
|
|
)
|
|
|
|
|
|
def run_cli_json(
|
|
*args: str,
|
|
workspace: str,
|
|
env_extra: dict[str, str] | None = None,
|
|
) -> tuple[subprocess.CompletedProcess[str], dict | list | None]:
|
|
"""Run CLI command with ``--format json`` and parse output.
|
|
|
|
Returns the completed process and parsed JSON (or ``None``).
|
|
"""
|
|
full_args = [*args, "--format", "json"]
|
|
result = run_cli(*full_args, workspace=workspace, env_extra=env_extra)
|
|
parsed: dict | list | None = None
|
|
if result.returncode == 0 and result.stdout.strip():
|
|
with contextlib.suppress(json.JSONDecodeError):
|
|
parsed = json.loads(result.stdout)
|
|
return result, parsed
|
|
|
|
|
|
def setup_workspace(prefix: str = "e2e_") -> str:
|
|
"""Create an isolated workspace directory with a ready database.
|
|
|
|
Sets ``CLEVERAGENTS_HOME`` and ``CLEVERAGENTS_DATABASE_URL`` to the
|
|
workspace so all tables are available for every CLI command.
|
|
|
|
When a pre-migrated template database is available (either via the
|
|
``CLEVERAGENTS_TEMPLATE_DB`` environment variable or at the default
|
|
``build/.template-migrated.db`` path), the template is copied
|
|
instead of running full Alembic migrations. This reduces per-test
|
|
setup from ~1-3 s to ~1 ms, which is critical for parallel
|
|
execution under pabot where many workers set up workspaces
|
|
concurrently.
|
|
|
|
Returns the absolute path to the workspace.
|
|
"""
|
|
workspace = tempfile.mkdtemp(prefix=prefix)
|
|
os.environ["CLEVERAGENTS_HOME"] = workspace
|
|
db_path = os.path.join(workspace, "cleveragents_e2e.db")
|
|
db_url = f"sqlite:///{db_path}"
|
|
os.environ["CLEVERAGENTS_DATABASE_URL"] = db_url
|
|
|
|
# Fast path: copy pre-migrated template DB instead of running
|
|
# 25+ Alembic migrations (avoids I/O contention under pabot).
|
|
template = os.environ.get("CLEVERAGENTS_TEMPLATE_DB")
|
|
if not template:
|
|
default_template = os.path.join(
|
|
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
|
"build",
|
|
".template-migrated.db",
|
|
)
|
|
if os.path.isfile(default_template):
|
|
template = default_template
|
|
|
|
if template and os.path.isfile(template):
|
|
shutil.copy2(template, db_path)
|
|
else:
|
|
# Fallback: run Alembic migrations (slow path).
|
|
from cleveragents.infrastructure.database.migration_runner import (
|
|
MigrationRunner,
|
|
)
|
|
|
|
runner = MigrationRunner(db_url)
|
|
runner.init_or_upgrade(require_confirmation=False)
|
|
|
|
return workspace
|
|
|
|
|
|
def cleanup_workspace(workspace: str) -> None:
|
|
"""Remove a workspace directory and unset env vars, ignoring errors."""
|
|
shutil.rmtree(workspace, ignore_errors=True)
|
|
os.environ.pop("CLEVERAGENTS_HOME", None)
|
|
os.environ.pop("CLEVERAGENTS_DATABASE_URL", None)
|
|
|
|
|
|
def fail(msg: str) -> None:
|
|
"""Print failure message and exit with code 1."""
|
|
print(f"FAIL: {msg}", file=sys.stderr)
|
|
raise SystemExit(1)
|
|
|
|
|
|
def is_expected_provider_unavailable(output: str) -> bool:
|
|
"""Return True when output matches known provider-unavailable failures."""
|
|
hay = output.lower()
|
|
patterns = (
|
|
"provider openai is not configured",
|
|
"provider 'openai' is not configured",
|
|
"openai_api_key is not set",
|
|
"openai_api_key not found",
|
|
"no provider configured",
|
|
)
|
|
return any(pattern in hay for pattern in patterns)
|
|
|
|
|
|
def write_yaml(content: str) -> str:
|
|
"""Write YAML content to a temporary file and return its path."""
|
|
fd, path = tempfile.mkstemp(suffix=".yaml")
|
|
with os.fdopen(fd, "w") as fh:
|
|
fh.write(content)
|
|
return path
|
|
|
|
|
|
def init_bare_git_repo() -> str:
|
|
"""Create a bare git repository with an initial commit.
|
|
|
|
Returns the path to the repository.
|
|
"""
|
|
repo_dir = tempfile.mkdtemp(prefix="e2e_git_")
|
|
cmds = [
|
|
["git", "init"],
|
|
["git", "config", "user.email", "test@example.com"],
|
|
["git", "config", "user.name", "Test"],
|
|
]
|
|
for cmd in cmds:
|
|
subprocess.run(cmd, cwd=repo_dir, capture_output=True, check=True)
|
|
|
|
readme = Path(repo_dir) / "README.md"
|
|
readme.write_text("# Test repo\n")
|
|
subprocess.run(
|
|
["git", "add", "."],
|
|
cwd=repo_dir,
|
|
capture_output=True,
|
|
check=True,
|
|
)
|
|
subprocess.run(
|
|
["git", "commit", "-m", "Initial commit"],
|
|
cwd=repo_dir,
|
|
capture_output=True,
|
|
check=True,
|
|
)
|
|
return repo_dir
|