All checks were successful
Tests / Declarative: Post Actions passed: 1192
A multibranch project is a folder, not a job: its /api/json carries no lastBuild at all, only a jobs array with one child per branch. Detection read lastBuild, so hermes-code-demo-branches was returned as skipped on every tick since it was created and no branch could ever be triaged. Expand a folder into its branch jobs and process each. Jenkins addresses them as parent/job/branch, which the existing fetch already builds correctly, and branch names stay URL-encoded because re-encoding them yields a 404. Expansion is capped by ARIADNE_HERMES_MAX_BRANCHES (default 5): a repository with many active branches would otherwise multiply the watched job count without limit and could open one incident per failing branch in a single tick. A name containing a slash is refused outright, since it would escape the parent and address an unrelated job. The Jenkins transport moves to its own module so the orchestrator reads as decision logic, and jobs[name] rides along on the existing request rather than costing a second call per job per tick. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
92 lines
3.5 KiB
Python
92 lines
3.5 KiB
Python
"""Tests for expanding Jenkins multibranch folders into branch jobs."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from ariadne.services import hermes_multibranch as module
|
|
|
|
|
|
def _folder(*names: str) -> dict:
|
|
return {"jobs": [{"name": n} for n in names]}
|
|
|
|
|
|
def test_a_folder_has_child_jobs_and_no_last_build() -> None:
|
|
assert module.is_folder(_folder("master")) is True
|
|
|
|
|
|
def test_a_buildable_job_is_never_treated_as_a_folder() -> None:
|
|
"""Both conditions are required, or a real job could be expanded away."""
|
|
|
|
assert module.is_folder({"lastBuild": {"number": 1}, "jobs": [{"name": "x"}]}) is False
|
|
assert module.is_folder({"lastBuild": {"number": 1}}) is False
|
|
assert module.is_folder({}) is False
|
|
assert module.is_folder(None) is False
|
|
assert module.is_folder("not-a-dict") is False
|
|
|
|
|
|
def test_branches_expand_to_nested_jenkins_paths() -> None:
|
|
"""Jenkins addresses a branch as parent/job/branch."""
|
|
|
|
paths = module.branch_job_paths(_folder("master", "develop"), "demo-branches")
|
|
assert paths == ["demo-branches/job/master", "demo-branches/job/develop"]
|
|
|
|
|
|
def test_encoded_branch_names_are_passed_through_untouched() -> None:
|
|
"""Re-encoding or decoding a branch name yields a 404."""
|
|
|
|
paths = module.branch_job_paths(_folder("hermes-repair%2F4"), "demo")
|
|
assert paths == ["demo/job/hermes-repair%2F4"]
|
|
|
|
|
|
def test_expansion_is_capped() -> None:
|
|
"""A busy repository must not multiply the watched job count without limit."""
|
|
|
|
folder = _folder(*[f"b{i}" for i in range(20)])
|
|
assert len(module.branch_job_paths(folder, "demo")) == module.DEFAULT_MAX_BRANCHES
|
|
assert len(module.branch_job_paths(folder, "demo", max_branches=2)) == 2
|
|
assert module.branch_job_paths(folder, "demo", max_branches=0) == []
|
|
assert module.branch_job_paths(folder, "demo", max_branches=-1) == []
|
|
|
|
|
|
def test_malformed_children_are_skipped_not_fatal() -> None:
|
|
folder = {"jobs": [None, {"name": ""}, {"name": " "}, {"nope": 1}, {"name": "ok"}]}
|
|
assert module.branch_job_paths(folder, "demo") == ["demo/job/ok"]
|
|
|
|
|
|
def test_a_name_containing_a_slash_is_refused() -> None:
|
|
"""A raw slash would escape the parent and address an unrelated job."""
|
|
|
|
assert module.branch_job_paths(_folder("a/b"), "demo") == []
|
|
|
|
|
|
def test_non_folders_expand_to_nothing() -> None:
|
|
assert module.branch_job_paths({"lastBuild": {"number": 3}}, "demo") == []
|
|
assert module.branch_job_paths(None, "demo") == []
|
|
|
|
|
|
def test_tick_expands_a_folder_into_its_branches(monkeypatch) -> None:
|
|
"""End to end: a multibranch job must stop being silently skipped."""
|
|
|
|
from ariadne.services import hermes_autotriage as autotriage
|
|
from tests.hermes_autotriage_harness import _prepare
|
|
|
|
env = _prepare(monkeypatch)
|
|
|
|
payloads = {
|
|
"hermes-triage-demo": {"jobs": [{"name": "master"}, {"name": "hermes-repair%2F4"}]},
|
|
"hermes-triage-demo/job/master": {
|
|
"lastBuild": {"number": 3, "result": "SUCCESS", "building": False, "url": "u"}
|
|
},
|
|
"hermes-triage-demo/job/hermes-repair%2F4": {
|
|
"lastBuild": {"number": 4, "result": "SUCCESS", "building": False, "url": "u"}
|
|
},
|
|
}
|
|
monkeypatch.setattr(
|
|
autotriage.hermes_jenkins_client, "fetch_job_payload", lambda job: payloads.get(job)
|
|
)
|
|
|
|
jobs = autotriage.run_hermes_autotriage(env.storage)["jobs"]
|
|
|
|
assert jobs["hermes-triage-demo"]["status"] == "folder"
|
|
assert jobs["hermes-triage-demo/job/master"]["status"] == "healthy"
|
|
assert jobs["hermes-triage-demo/job/hermes-repair%2F4"]["status"] == "healthy"
|