Files
computer-playbook/tests/unit/python/cli/administration/deploy/ci/test_runs.py
T
kevinveenbirkenbachandClaude Opus 5 7162b7bf4c Merge fork/feature/svc-net-tor into feature/ai-agent-employees
834 commits from the Tor onion-service branch against 299 of ours, from merge
base 7b646bdece: 127 conflicted files, 3506 files changed.

The two sides are largely orthogonal. Theirs adds svc-net-tor and a `tor` service
across the role tree, plus two sweeping refactors: shipped scripts sorted by
language (files/ruby/, files/python/, files/shell/, scripts/lint/*.sh) and the
unit suite moved under tests/unit/python/. Ours is requirement 036, the native AI
gateway, plus the MCP role work. Most conflicts were both sides appending to the
same service map, resolved as unions.

Resolutions that were not unions:

Credentials moved namespace. Theirs renamed meta/schema.yml to meta/secrets.yml
and re-addressed credentials as secrets.credentials.<name>. Git carried our
litellm_api_key blocks through the rename, but every consumer we added still read
'credentials.<name>', which no longer resolves. All 45 call sites are migrated.

plugins/filter/container_volumes.py is deleted by theirs and relocated to
plugins/lookup/container_volumes.py, where it carries the same tmpfs defect our
f85996b43 had just fixed: a hard-coded {"size": ...} that silently drops a per
mount `mode`. The fix is re-applied in the relocated file, as that commit's merge
note anticipated.

compose.yml keeps only INFINITO_PLAYWRIGHT_REPORTS_BASE_DIR. Theirs also forwards
INFINITO_PLAYWRIGHT_STAGE_BASE_DIR; that line is deliberately dropped, per the
analysis recorded in 45804e9ad -- STAGE has a drift-proof role-vars fallback,
while forwarding it promotes a setdefault-pinnable .env value above that SPOT.

tasks/stages/01_constructor.yml takes their include, ./tasks/utils/setup/group.yml,
because their refactor removed tasks/groups/*-roles.yml, leaving our include
target nonexistent. Our task name is kept, since svc-virt is in the loop.

Makefile swarm-zombie is a union: their validate plus teardown, then our
lab_subnet cleanup, which their side dropped while leaving lab_subnet.sh as the
only orphaned reference in the file.

01_bootstrap.sh keeps our deterministic --ip pinning, which distinguishes an
already-attached node from a real failure, over their `|| true`.

Their language-sorted layout orphaned references our side still held: zammad's
two scripts/apply/*.rb, jenkins' java-with-ca-trust.sh, and the notify topics
their handlers renamed to hyphenated form. All corrected.

roles/sys-svc-compose/files/shell/swarm/stack_ready.sh gains our latched-update
block: paused, rollback_paused, rollback_started and rollback_completed exit 2
rather than polling a state that cannot resolve without another deploy.

config_from_run is unioned to (title, logged, jobs); the production caller in
trigger/__main__.py now passes jobs, without which our suite suppression would
have been inert.

READMEs are regenerated rather than hand-merged: 15 LiteLLM rows and 91 Tor rows
now derive from the merged metadata, which is itself evidence the meta merge is
coherent.

Verified: 5161 unit tests pass. The single failure, test_query.py TestSortSpec,
also fails on fork/feature/svc-net-tor at b51d2f9919, measured in a baseline
worktree -- so this merge introduces no new unit-test failure. Two modules are
excluded from that run because `baudolo`, a dependency their branch introduces,
is not installed on this host.
Not verified: no deploy has run against the merged tree, and make quality has not
been run end to end here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 16:32:30 +02:00

483 lines
18 KiB
Python

from __future__ import annotations
import unittest
from typing import ClassVar
from cli.administration.deploy.ci import gh, runs
from tests.utils.ci.job_names import deploy_job_name
from tests.utils.ci.run_name import render
from utils.github import run_name
def _job(name: str, conclusion: str | None, status: str = "completed") -> dict:
return {"name": name, "conclusion": conclusion, "status": status}
class TestParseRoleStatuses(unittest.TestCase):
def test_maps_compose_and_swarm_jobs_to_modes(self) -> None:
jobs = [
_job(deploy_job_name("docker", "web-app-baserow"), "success"),
_job(deploy_job_name("swarm", "web-app-baserow"), "failure"),
_job(deploy_job_name("docker", "svc-db-postgres", "0"), "success"),
_job("🍯 Lure swarms", "success"),
_job("⛵ Navigate compositions", "success"),
]
statuses = runs.parse_role_statuses(jobs)
self.assertEqual(
statuses,
{
"web-app-baserow": {"docker": "success", "swarm": "failure"},
"svc-db-postgres": {"docker": "success"},
},
)
def test_maps_host_jobs_to_modes(self) -> None:
jobs = [
_job(deploy_job_name("host", "svc-storage-nfs-server", "0,1"), "failure"),
]
self.assertEqual(
runs.parse_role_statuses(jobs),
{"svc-storage-nfs-server": {"host": "failure"}},
)
def test_running_job_is_not_completed(self) -> None:
jobs = [
_job(deploy_job_name("docker", "web-app-x"), None, status="in_progress")
]
self.assertEqual(
runs.parse_role_statuses(jobs), {"web-app-x": {"docker": "running"}}
)
def test_green_variant_shard_never_masks_a_failed_sibling(self) -> None:
for shards in (
[("1", "failure"), ("0", "success")],
[("0", "success"), ("1", "failure")],
):
with self.subTest(shards=shards):
jobs = [
_job(deploy_job_name("swarm", "web-app-gitlab", variant), state)
for variant, state in shards
]
self.assertEqual(
runs.parse_role_statuses(jobs),
{"web-app-gitlab": {"swarm": "failure"}},
)
def test_cancelled_shard_dominates_success_but_not_failure(self) -> None:
jobs = [
_job(deploy_job_name("swarm", "web-app-x", "0"), "success"),
_job(deploy_job_name("swarm", "web-app-x", "1"), "cancelled"),
_job(deploy_job_name("swarm", "web-app-x", "2"), "failure"),
]
self.assertEqual(
runs.parse_role_statuses(jobs), {"web-app-x": {"swarm": "failure"}}
)
class TestAppOfJob(unittest.TestCase):
def test_extracts_app_from_orchestrated_name(self) -> None:
name = deploy_job_name("swarm", "web-app-matomo", "0,1")
self.assertEqual(runs.app_of_job(name), "web-app-matomo")
def test_none_for_non_deploy_job(self) -> None:
self.assertIsNone(runs.app_of_job("🍯 Lure swarm"))
class TestParseRoleUrls(unittest.TestCase):
def test_maps_job_urls_per_mode(self) -> None:
jobs = [
{
"name": deploy_job_name("docker", "web-app-x"),
"url": "https://gh/c",
"status": "completed",
"conclusion": "success",
},
{
"name": deploy_job_name("swarm", "web-app-x"),
"url": "https://gh/s",
"status": "completed",
"conclusion": "failure",
},
{
"name": deploy_job_name("swarm", "web-app-y"),
"url": None,
"status": "completed",
"conclusion": "success",
},
]
self.assertEqual(
runs.parse_role_urls(jobs),
{"web-app-x": {"docker": "https://gh/c", "swarm": "https://gh/s"}},
)
class TestTotalState(unittest.TestCase):
"""``total`` is green when every mode that ran is green; an absent mode is N/A."""
def test_both_success(self) -> None:
self.assertEqual(
runs.total_state({"docker": "success", "swarm": "success"}), "success"
)
def test_any_failure_fails(self) -> None:
self.assertEqual(
runs.total_state({"docker": "success", "swarm": "failure"}), "failure"
)
def test_cancelled_counts_as_failure(self) -> None:
self.assertEqual(
runs.total_state({"docker": "success", "swarm": "cancelled"}), "failure"
)
def test_running_counts_as_failure(self) -> None:
self.assertEqual(
runs.total_state({"docker": "success", "swarm": "running"}), "failure"
)
def test_missing_mode_is_na_not_failure(self) -> None:
self.assertEqual(runs.total_state({"docker": "success"}), "success")
class TestCellSymbols(unittest.TestCase):
def test_symbol_mapping(self) -> None:
self.assertEqual(runs.cell("success"), runs.PASS)
self.assertEqual(runs.cell("failure"), runs.FAIL)
self.assertEqual(runs.cell("cancelled"), runs.ABORT)
self.assertEqual(runs.cell("timed_out"), runs.ABORT)
self.assertEqual(runs.cell("running"), runs.RUNNING)
self.assertEqual(runs.cell("missing"), runs.MISSING)
class TestFailedRoles(unittest.TestCase):
_STATUSES: ClassVar[dict[str, dict[str, str]]] = {
"web-app-b": {"docker": "success", "swarm": "success"},
"web-app-a": {"docker": "success", "swarm": "failure"},
"web-app-c": {"docker": "cancelled", "swarm": "success"},
"web-app-d": {"docker": "success"},
}
def test_total_scope_default(self) -> None:
self.assertEqual(
runs.failed_roles(self._STATUSES),
["web-app-a", "web-app-c"],
)
def test_swarm_scope_skips_roles_without_a_swarm_job(self) -> None:
self.assertEqual(runs.failed_roles(self._STATUSES, "swarm"), ["web-app-a"])
def test_docker_scope(self) -> None:
self.assertEqual(runs.failed_roles(self._STATUSES, "docker"), ["web-app-c"])
def test_strict_total_excludes_cancelled(self) -> None:
self.assertEqual(
runs.failed_roles(self._STATUSES, strict=True),
["web-app-a"],
)
def test_strict_swarm_keeps_hard_failure(self) -> None:
self.assertEqual(
runs.failed_roles(self._STATUSES, "swarm", strict=True), ["web-app-a"]
)
def test_strict_docker_excludes_cancelled(self) -> None:
self.assertEqual(runs.failed_roles(self._STATUSES, "docker", strict=True), [])
class TestRunIdFromUrl(unittest.TestCase):
def test_extracts_id(self) -> None:
self.assertEqual(
gh.run_id_from_url("https://example.com/o/r/actions/runs/28140817070"),
"28140817070",
)
def test_raises_without_id(self) -> None:
with self.assertRaises(ValueError):
gh.run_id_from_url("https://example.com/o/r/actions")
class TestSlugFromUrl(unittest.TestCase):
def test_run_url(self) -> None:
self.assertEqual(
gh.slug_from_url(
"https://github.com/kevinveenbirkenbach/infinito-nexus-core/actions/runs/28141113779/job/83339311104"
),
"kevinveenbirkenbach/infinito-nexus-core",
)
def test_ssh_remote(self) -> None:
self.assertEqual(
gh.slug_from_url("git@github.com:infinito-nexus/core.git"),
"infinito-nexus/core",
)
def test_https_remote(self) -> None:
self.assertEqual(
gh.slug_from_url("https://github.com/owner/repo.git"), "owner/repo"
)
def test_raises_on_non_github(self) -> None:
with self.assertRaises(ValueError):
gh.slug_from_url("https://example.com/x/y")
class TestInputsFromJobs(unittest.TestCase):
"""The title truncates; a called job's log does not."""
LOG: ClassVar[str] = "\n".join(
[
"2026-08-08T10:22:59.5Z ##[group] Inputs",
"2026-08-08T10:22:59.5Z distros: arch",
"2026-08-08T10:22:59.5Z whitelist: ",
"2026-08-08T10:22:59.5Z priority: "
+ " ".join(f"web-app-{n}" for n in range(69)),
"2026-08-08T10:22:59.5Z chunk_gate: true",
"2026-08-08T10:22:59.5Z ##[endgroup]",
"2026-08-08T10:22:59.6Z ##[group]Run echo hi",
"2026-08-08T10:22:59.6Z priority: not-an-input",
]
)
def _read(
self, jobs: list[dict], logs: dict[int, str]
) -> tuple[dict[str, str], list[int]]:
"""Read *jobs* against a fake ``gh`` serving *logs* by job id.
Args:
jobs: the run's jobs.
logs: job id -> log body. A missing id stands for a job whose log
blob does not exist, which ``gh`` reports as a bare HTTP 404.
Returns:
the parsed inputs and the job ids the reader asked for, in order.
"""
asked: list[int] = []
def fake_gh(
args: list[str], repo: str | None = None, check: bool = True
) -> str:
self.assertIn("--allow-escape-sequences", args)
self.assertFalse(check)
job_id = int(args[-1].split("/")[-2])
asked.append(job_id)
return logs.get(job_id, "")
original = runs._gh
runs._gh = fake_gh
try:
return runs.inputs_from_jobs(jobs, "o/r"), asked
finally:
runs._gh = original
def test_reads_the_full_priority_from_the_first_called_job(self) -> None:
jobs = [
{"name": "🎲 Pick distro(s)", "databaseId": 1},
{"name": "🎛️ Orchestrate CI (manual) / 🏁 Finish pipeline", "databaseId": 2},
]
inputs, asked = self._read(jobs, {2: self.LOG})
self.assertEqual(asked, [2])
self.assertEqual(len(inputs["priority"].split()), 69)
self.assertEqual(inputs["distros"], "arch")
self.assertEqual(inputs["whitelist"], "")
self.assertEqual(inputs["chunk_gate"], "true")
def test_skipped_jobs_are_passed_over(self) -> None:
"""A skipped job uploads no log blob, so reading it is a bare 404."""
jobs = [
{
"name": "🎛️ Orchestrate CI (manual) / test-workspace",
"databaseId": 1,
"conclusion": "skipped",
},
{
"name": "🎛️ Orchestrate CI (manual) / 🎯 Selection / ✅ Validation",
"databaseId": 2,
"conclusion": "success",
},
]
inputs, asked = self._read(jobs, {2: self.LOG})
self.assertEqual(asked, [2])
self.assertEqual(len(inputs["priority"].split()), 69)
def test_shallower_jobs_win_over_deeper_ones(self) -> None:
jobs = [
{
"name": "🎛️ Orchestrate CI (manual) / 🎯 Selection / ✅ Validation",
"databaseId": 2,
"conclusion": "success",
},
{
"name": "🎛️ Orchestrate CI (manual) / 🏁 Finish pipeline",
"databaseId": 1,
"conclusion": "success",
},
]
_, asked = self._read(jobs, {1: self.LOG, 2: self.LOG})
self.assertEqual(asked, [1])
def test_an_expired_log_falls_through_instead_of_aborting(self) -> None:
jobs = [
{
"name": "🎛️ Orchestrate CI (manual) / 🏁 Finish pipeline",
"databaseId": 1,
"conclusion": "success",
},
{
"name": "🎛️ Orchestrate CI (manual) / 🎯 Selection / ✅ Validation",
"databaseId": 2,
"conclusion": "success",
},
]
inputs, asked = self._read(jobs, {2: self.LOG})
self.assertEqual(asked, [1, 2])
self.assertEqual(len(inputs["priority"].split()), 69)
def test_a_run_whose_logs_are_all_gone_reads_nothing(self) -> None:
jobs = [
{
"name": "🎛️ Orchestrate CI (manual) / test-workspace",
"databaseId": 1,
"conclusion": "skipped",
},
{
"name": "🎛️ Orchestrate CI (manual) / 🏁 Finish pipeline",
"databaseId": 2,
"conclusion": "success",
},
]
inputs, asked = self._read(jobs, {})
self.assertEqual(asked, [2])
self.assertEqual(inputs, {})
def test_a_run_without_a_called_job_reads_nothing(self) -> None:
self.assertEqual(runs.inputs_from_jobs([{"name": "🎲 Pick"}], "o/r"), {})
class TestConfigFromTitle(unittest.TestCase):
"""A retrigger has to reproduce the source run's configuration.
The REST API answers ``inputs: null`` for a finished workflow_dispatch, so
the run title entry-manual-steer.yml builds is the only record of it. Guessing
wrong is silent: entry-manual-steer.yml defaults to debian alone, so a failure
that only reproduces on centos comes back green.
"""
def test_every_config_input_renders_a_segment_in_the_run_name(self) -> None:
rendered = set(run_name.heads()) | set(run_name.markers())
self.assertLessEqual(set(runs.CONFIG_INPUTS), rendered)
def test_the_distros_are_read_back_from_a_manual_run(self) -> None:
title = render({"distros": "debian arch centos"})
self.assertEqual(runs.config_from_title(title)["distros"], "debian arch centos")
def test_a_priority_segment_does_not_bleed_into_them(self) -> None:
title = render({"distros": "debian", "priority": "web-app-x"})
self.assertEqual(runs.config_from_title(title)["distros"], "debian")
def test_the_selection_is_not_part_of_the_configuration(self) -> None:
title = render({"distros": "debian", "priority": "web-app-x"})
self.assertNotIn("priority", runs.config_from_title(title))
def test_a_full_title_round_trips_every_config_input(self) -> None:
dispatched = {
"distros": "debian arch",
"mode": "swarm",
"lifecycles": "stable",
"filesystem": "ext4",
"chunk_gate": "false",
"workspace": "true",
"instructions": "false",
}
self.assertEqual(runs.config_from_title(render(dispatched)), dispatched)
def test_a_value_input_on_its_default_records_nothing(self) -> None:
recovered = runs.config_from_title(render({"distros": "debian"}))
self.assertEqual(recovered["distros"], "debian")
self.assertNotIn("mode", recovered)
def test_a_run_from_another_entry_point_yields_no_override(self) -> None:
self.assertEqual(runs.config_from_title("CI: Pull Request"), {})
def test_priority_roles_without_any_job_are_reported(self) -> None:
statuses = {"web-app-a": {"swarm": "failure"}}
self.assertEqual(
runs.untriggered_priority("web-app-a web-app-b web-app-c", statuses),
["web-app-b", "web-app-c"],
)
def test_a_pinned_priority_entry_keeps_its_axes(self) -> None:
statuses = {"web-app-a": {"swarm": "failure"}}
self.assertEqual(
runs.untriggered_priority(
"web-app-a#1@compose+tor web-app-b#1@compose+tor "
"web-app-b#0,2@swarm+clearnet",
statuses,
),
["web-app-b#0,2@swarm+clearnet", "web-app-b#1@compose+tor"],
)
def test_a_run_without_a_priority_line_reports_nothing(self) -> None:
self.assertEqual(runs.untriggered_priority("", {}), [])
def test_a_truncated_priority_entry_aborts_instead_of_passing_through(self) -> None:
with self.assertRaises(SystemExit):
runs.untriggered_priority("web-app-a 网络应用·Funkwha...", {})
def test_the_tor_mode_is_carried_over_from_the_job_log(self) -> None:
title = render({"mode": "swarm"})
self.assertEqual(
runs.config_from_run(title, {"tor": "exclusive"})["tor"], "exclusive"
)
def test_a_run_whose_log_records_no_tor_mode_carries_none(self) -> None:
title = render({"mode": "swarm"})
self.assertNotIn("tor", runs.config_from_run(title, {}))
self.assertNotIn("tor", runs.config_from_run(title))
def _suite_job(self, suite: str, conclusion: str) -> dict:
return {
"name": f"🎛️ Orchestrate CI (manual) / {runs.SUITE_JOB_IDS[suite]} / 📖 X",
"status": "completed",
"conclusion": conclusion,
}
def test_a_suite_the_source_run_passed_is_not_asked_for_again(self) -> None:
for suite in runs.SUITE_JOB_IDS:
with self.subTest(suite):
title = render({suite: "true"})
jobs = [self._suite_job(suite, "success")]
self.assertEqual(runs.config_from_run(title, jobs=jobs)[suite], "false")
def test_a_suite_that_failed_is_asked_for_again(self) -> None:
for suite in runs.SUITE_JOB_IDS:
with self.subTest(suite):
title = render({suite: "true"})
jobs = [self._suite_job(suite, "failure")]
self.assertEqual(runs.config_from_run(title, jobs=jobs)[suite], "true")
def test_a_suite_that_never_ran_is_asked_for_again(self) -> None:
title = render({"workspace": "true"})
jobs = [{"name": "🧹 Lint", "status": "completed", "conclusion": "success"}]
self.assertEqual(runs.config_from_run(title, jobs=jobs)["workspace"], "true")
def test_one_failing_shard_keeps_the_whole_suite(self) -> None:
title = render({"workspace": "true"})
jobs = [
self._suite_job("workspace", "success"),
self._suite_job("workspace", "failure"),
]
self.assertEqual(runs.config_from_run(title, jobs=jobs)["workspace"], "true")
if __name__ == "__main__":
unittest.main()