mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-08 11:46:35 +00:00
834 commits from the Tor onion-service branch against 299 of ours, from merge base7b646bdece: 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 ourf85996b43had 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 in45804e9ad-- 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 atb51d2f9919, 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>
483 lines
18 KiB
Python
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()
|