Files
wehub-resource-sync 4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

268 lines
9.7 KiB
Python

"""
GitLab Integration E2E Tests (cloud-hosted GitLab source).
GitLab is a supplementary investigation source, not a primary alert source.
These tests mirror how GitHub is used: a production alert fires (Grafana/generic),
and the agent queries GitLab to correlate recent commits, MRs, and pipelines.
Required env vars:
GITLAB_ACCESS_TOKEN - Personal access token with read_api + read_repository scope
GITLAB_PROJECT_ID - Project path (e.g. "myorg/myrepo")
Optional env vars:
GITLAB_BASE_URL - GitLab instance URL (defaults to https://gitlab.com/api/v4)
GITLAB_MR_IID - MR IID to post a note on (required only for test_gitlab_post_mr_note)
"""
from __future__ import annotations
import json
import os
from datetime import UTC, datetime
from pathlib import Path
import pytest
from integrations.gitlab import (
DEFAULT_GITLAB_BASE_URL,
build_gitlab_config,
get_gitlab_commits,
get_gitlab_mrs,
get_gitlab_pipelines,
post_gitlab_mr_note,
validate_gitlab_config,
)
FIXTURES_DIR = Path(__file__).parent / "fixtures"
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _require_env() -> tuple[str, str, str]:
"""Return (access_token, base_url, project_id) or skip the test."""
access_token = os.getenv("GITLAB_ACCESS_TOKEN", "").strip()
project_id = os.getenv("GITLAB_PROJECT_ID", "").strip()
base_url = (
os.getenv("GITLAB_BASE_URL", DEFAULT_GITLAB_BASE_URL).strip() or DEFAULT_GITLAB_BASE_URL
)
missing = []
if not access_token:
missing.append("GITLAB_ACCESS_TOKEN")
if not project_id:
missing.append("GITLAB_PROJECT_ID")
if missing:
pytest.skip(f"GitLab env vars not set: {', '.join(missing)}")
return access_token, base_url, project_id
def _gitlab_config(access_token: str, base_url: str):
return build_gitlab_config({"base_url": base_url, "auth_token": access_token})
def _gitlab_project_url(base_url: str, project_id: str) -> str:
"""Build a full GitLab project URL from base_url + project path."""
instance = base_url.replace("/api/v4", "").rstrip("/")
return f"{instance}/{project_id}"
# ---------------------------------------------------------------------------
# 1. Connectivity
# ---------------------------------------------------------------------------
def test_gitlab_connectivity():
"""Verify that the token authenticates and /user responds."""
access_token, base_url, _ = _require_env()
config = _gitlab_config(access_token, base_url)
result = validate_gitlab_config(config)
assert result.ok, f"GitLab connectivity failed: {result.detail}"
assert "@" in result.detail or "Authenticated" in result.detail, (
f"Expected username in detail, got: {result.detail}"
)
# ---------------------------------------------------------------------------
# 2. Tool-level: commits
# ---------------------------------------------------------------------------
def test_gitlab_list_commits():
"""Fetch recent commits for the configured project."""
access_token, base_url, project_id = _require_env()
config = _gitlab_config(access_token, base_url)
since = datetime(2020, 1, 1, tzinfo=UTC).isoformat()
commits = get_gitlab_commits(
config=config,
project_id=project_id,
since=since,
per_page=5,
)
assert isinstance(commits, list), "Expected a list of commits"
if commits:
first = commits[0]
assert "id" in first or "short_id" in first, f"Unexpected commit shape: {first.keys()}"
# ---------------------------------------------------------------------------
# 3. Tool-level: merge requests
# ---------------------------------------------------------------------------
def test_gitlab_list_mrs():
"""Fetch merge requests for the configured project."""
access_token, base_url, project_id = _require_env()
config = _gitlab_config(access_token, base_url)
updated_after = datetime(2020, 1, 1, tzinfo=UTC).isoformat()
mrs = get_gitlab_mrs(
config=config,
project_id=project_id,
state="merged",
updated_after=updated_after,
per_page=5,
)
assert isinstance(mrs, list), "Expected a list of merge requests"
if mrs:
first = mrs[0]
assert "iid" in first or "id" in first, f"Unexpected MR shape: {first.keys()}"
# ---------------------------------------------------------------------------
# 4. Tool-level: pipelines
# ---------------------------------------------------------------------------
def test_gitlab_list_pipelines():
"""Fetch CI/CD pipelines for the configured project."""
access_token, base_url, project_id = _require_env()
config = _gitlab_config(access_token, base_url)
updated_after = datetime(2020, 1, 1, tzinfo=UTC).isoformat()
pipelines = get_gitlab_pipelines(
config=config,
project_id=project_id,
updated_after=updated_after,
per_page=5,
)
assert isinstance(pipelines, list), "Expected a list of pipelines"
if pipelines:
first = pipelines[0]
assert "id" in first, f"Unexpected pipeline shape: {first.keys()}"
assert "status" in first, f"Pipeline missing status: {first.keys()}"
# ---------------------------------------------------------------------------
# 5. End-to-end investigation — GitLab as supplementary evidence source
#
# This is the realistic production path: a Grafana-style alert fires (high
# error rate, service degradation, etc.) and the agent also queries GitLab
# to correlate recent commits and MRs that may have caused it.
#
# GitLab is picked up via repo_url or annotations["repository"], exactly the
# same way GitHub is detected — not from a "gitlab_ci" alert.
# ---------------------------------------------------------------------------
def test_gitlab_investigation_e2e():
"""
Full investigation flow with GitLab as a supplementary evidence source.
Simulates a Grafana-style production alert that includes a repo_url
pointing at the configured GitLab project. The agent is expected to use
GitLab tools (commits, MRs) alongside the alert context to produce a
root cause, mirroring how GitHub is used in kubernetes/datadog e2e tests.
"""
access_token, base_url, project_id = _require_env()
from surfaces.cli.investigation import run_investigation_cli
# Load the shared fixture and patch in the real project URL so the
# The repo_url hint lets the agent correlate the alert with this project.
fixture_path = FIXTURES_DIR / "gitlab_high_error_rate_alert.json"
raw_alert = json.loads(fixture_path.read_text())
repo_url = _gitlab_project_url(base_url, project_id)
for alert in raw_alert.get("alerts", []):
alert.setdefault("annotations", {})["repo_url"] = repo_url
raw_alert.setdefault("commonAnnotations", {})["repo_url"] = repo_url
print(f"\nRunning investigation — primary: Grafana alert, supplementary: GitLab ({project_id})")
investigation_result = run_investigation_cli(raw_alert=raw_alert)
root_cause = investigation_result.get("root_cause", "")
remediation_steps = investigation_result.get("remediation_steps", [])
print(f"Root cause: {root_cause}")
print(f"Remediation steps: {remediation_steps}")
assert root_cause, (
"Investigation produced no root cause. "
"Check that GITLAB_ACCESS_TOKEN has read_api scope, GITLAB_PROJECT_ID is valid, "
"and an LLM key (ANTHROPIC_API_KEY or OPENAI_API_KEY) is set."
)
# ---------------------------------------------------------------------------
# 6. Tool-level: post MR note
# ---------------------------------------------------------------------------
def test_gitlab_post_mr_note():
"""Post a comment on a real MR and verify the note is created.
Reads project and MR context from the fixture annotations, the same way
the agent receives them from a real CI/CD alert.
Required env vars:
GITLAB_MR_IID - IID (not global ID) of an open MR in the configured project
"""
access_token, base_url, project_id = _require_env()
mr_iid = os.getenv("GITLAB_MR_IID", "").strip()
if not mr_iid:
pytest.skip("GITLAB_MR_IID env var not set")
# Load fixture and inject env values into annotations — same pattern as
# test_gitlab_investigation_e2e, mirroring how a real alert carries this context.
fixture_path = FIXTURES_DIR / "gitlab_high_error_rate_alert.json"
raw_alert = json.loads(fixture_path.read_text())
repo_url = _gitlab_project_url(base_url, project_id)
for alert in raw_alert.get("alerts", []):
alert.setdefault("annotations", {})["repo_url"] = repo_url
alert["annotations"]["mr_iid"] = mr_iid
raw_alert.setdefault("commonAnnotations", {})["repo_url"] = repo_url
raw_alert["commonAnnotations"]["mr_iid"] = mr_iid
# Read back from annotations — the same way the agent would in production.
annotations = raw_alert["alerts"][0]["annotations"]
project_id_from_annotation = project_id
mr_iid_from_annotation = annotations["mr_iid"]
config = _gitlab_config(access_token, base_url)
body = "[opensre e2e test] Automated RCA comment — safe to ignore."
result = post_gitlab_mr_note(
config=config,
project_id=project_id_from_annotation,
mr_iid=mr_iid_from_annotation,
body=body,
)
assert isinstance(result, dict), f"Expected a dict response, got: {type(result)}"
assert "id" in result, f"Note response missing 'id': {result.keys()}"
assert result.get("body") == body, f"Posted body mismatch: {result.get('body')!r}"
print(
f"\nPosted note id={result['id']} on MR !{mr_iid_from_annotation} in {project_id_from_annotation}"
)